直接回答
原生推理接口应描述应用任务与生命周期,而不是把某个厂商的引擎对象泄漏到 UI 代码。定义一个小型契约,覆盖初始化、单次请求或流式请求、取消和关闭;用应用自有类型表示进度和失败;把模型路径与后端配置放进适配器;在接入原生运行时前先用假实现测试契约。接口并不能让不同运行时自动互换。它提供一个受控边界,使应用可以用相同验收条件评估不同适配器,而不用把原生会话类型塞进 ViewModel 或界面。 边界需要明确所有权和并发。决定由一个进程级协调器拥有单个引擎,还是每个对话单独隔离;并决定谁可以关闭原生状态。大多数应用屏幕应由 ViewModel 管理任务状态,再调用 Repository 或运行时协调器;Composable 只绘制状态并转发用户操作。不要在 UI 构造函数或重组期间创建引擎。原生初始化可能耗时,应暴露初始化状态且不能阻塞主线程。流式请求还要定义订阅者离开时的行为:要么请求继续由应用持有并可恢复,要么取消传到原生工作并释放资源。 不要只用“AI 失败”这样的字符串。文件不存在、制品不匹配、引擎初始化失败、不支持的操作、取消、响应格式错误和内存不足,需要不同的恢复与用户说明。技术诊断可以保留隐私安全的元数据,但不要默认记录私人提示词。如果产品逻辑负责输出结构,解析应由应用层完成;token 生成和任务验收需要分开。 接口也要防止无意并发。一个原生引擎未必支持同一会话或引擎同时执行多个请求。若运行时契约要求串行,就序列化请求;若要并行独立会话,先用测试证明,不要假设。每次请求应携带请求标识,防止被取消的旧响应覆盖新屏幕状态。进程重建后,应根据可持久化的模型标识与任务元数据重建运行时,而不是依赖内存中的原生句柄。只保存恢复用户任务所需的少量数据,其派生结果仍应遵循应用的隐私和删除规则。 实用的接口状态包括 unavailable、initializing、ready、running、cancelling、failed 和 closed。可以用 Flow 表示增量事件,但要区分部分文本、已验证最终输出、工具请求与失败。适配器把每个运行时的 API 映射到此契约。假适配器可测试 UI 行为、生命周期变化、取消竞争、空输出、结构化解析失败与回退,而不加载模型。真实适配器仍须用固定制品和运行时验证。 不要把共享接口说成质量、速度、格式支持或硬件加速等价的证据。LiteRT-LM 与 llama.cpp 适配器可能在格式、API、支持操作和失败行为上不同。只有把能力差异作为显式字段保留下来,而非藏在“可用”布尔值之后,这个边界才真正有用。
替换运行时而不重写所有界面
Android 应用有一个 Compose 页面和一个直接导入原生引擎类的 ViewModel。团队想比较第二种运行时、加入取消并测试进程重建,但每次实验都要改界面,导致故障难以隔离。
决策
引入应用自有推理契约;只在模型格式和功能确有支持时添加适配器。UI 任务状态不保存原生对象。
预期结果
同一 UI 契约可先用假实现测试,再接入每个真实适配器;不支持的运行时差异仍会显式显示。
需要收集的证据
只有将以下证据与同一模型文件和测试运行关联起来,结论才可复核:
- 初始化和请求状态契约
- 引擎所有权与并发规则
- 假实现和真实适配器的测试记录
只有记录了模型身份、构建配置、实际后端、任务样例和失败情况,运行时比较才可复核。
实施流程
- 先写任务契约. 列出用户请求、输入类型、输出验收属性、进度事件、取消行为和安全失败状态。除非某项约束确实由运行时负责,否则不要把产品验收逻辑塞进运行时接口。
- 定义所有者与串行规则. 选择由哪个 Repository 或进程级协调器创建、关闭引擎;明确请求是否串行、如何用请求标识阻止旧响应更新新页面,以及订阅者消失时会发生什么。
- 先用假实现搭建测试. 用假实现覆盖初始化、流式输出、取消、超时或失败、格式错误输出、进程重建和 ViewModel 状态。假实现应能模拟延迟和乱序事件,避免 UI 只在即时成功情形下被测试。
- 适配运行时但不抹平差异. 把原生库映射到应用自有状态,同时将文件格式、后端支持和任务能力作为显式元数据保留。只有模型与操作受支持时,才对各适配器运行同一个验收样例。
让每个阶段的状态都可观察。失败信息应指出阶段、模型、运行时和恢复动作,同时避免记录用户私密内容。
验收标准
- Compose 页面和 ViewModel 不保存原生引擎或对话对象。
- 每个请求都有明确所有者、取消路径、关闭规则和旧结果保护。
- 假适配器覆盖生命周期及错误场景;每个真实适配器都有对应制品的测试记录。
决策对照表
| 维度 | 选择或证据 | 解读方式 |
|---|---|---|
| ViewModel 直接持有原生引擎 | 带厂商类型的快速原型 | 耦合生命周期和测试到单一运行时,并可能在重建时泄漏资源。 |
| 应用自有接口 | 稳定任务与生命周期契约加适配器 | 支持假实现和受控对比,但不隐藏运行时差异。 |
| 单一通用成功布尔值 | 最低细节状态模型 | 会隐藏初始化、回退、取消、解析和资源释放失败。 |
需要避免的失败模式
- 让 ViewModel 直接持有原生对话对象
- 把多种运行时伪装成完全兼容
- 忽略取消后旧响应覆盖新结果
还要避免静默回退、版本未固定、缺少取消处理,以及把不同配置的结果混在一起下结论。失败运行也应与成功记录一并保存。
最小复现记录
| 层级 | 记录内容 |
|---|---|
| 设备 | 厂商、型号、芯片、内存档位、操作系统版本 |
| 软件 | 应用版本和 Git 提交号 |
| 模型 | 系列、变体、版本、格式、文件长度、摘要、量化方式 |
| 运行时 | 名称、版本、请求后端及实际后端证据 |
| 工作负载 | 样例版本、输入摘要、提示词摘要、输出策略 |
| 结果 | 完成、失败、取消、回退及保护隐私的诊断信息 |
发布检查表
- ☐ 主要搜索问题已回答,且没有未经支持的数字。
- ☐ 所有模型文件和运行时版本均已固定。
- ☐ 代表性任务和失败处理策略均有说明。
- ☐ 生命周期、取消、清理和回退均已测试。
- ☐ 用户内容与网络边界均有说明。
- ☐ 结论只适用于被记录的配置。
- ☐ 新增测试结果时链接到原始方法或证据。
来源与相关证据
- LiteRT-LM repository
- llama.cpp repository
- Google AI Edge LiteRT-LM Kotlin API
- Android Developers 应用架构指南
- Android Developers Activity 生命周期
中文部署与排错专题见。奇连 AI 端侧专题。
常见问题
适配器能让 LiteRT-LM 与 llama.cpp 完全互换吗?
不能。它们可能采用不同的文件格式、API、支持操作、后端和失败语义。适配器提供受控集成边界,但应用仍需公开每个运行时的能力与证据。
ViewModel 应该拥有原生引擎吗?
ViewModel 可以持有任务状态并协调 Repository,但长生命周期的原生资源需要明确所有者,并在屏幕重建时正确复用或关闭。不要让 Composable 隐式创建或销毁引擎。
屏幕离开时流式请求应该怎样处理?
先决定应用是否独立持有请求,还是离开任务就取消。把该策略写进契约;运行时支持时将取消传播到原生工作,并确保部分输出不会被误当成验收通过的最终结果。