**摘要:**一台 Galaxy S23 Ultra 上的离线语音 Agent 曾测得 908ms 首音频和 1,116MB PSS。但真正值得复用的不是这两个数字,而是四阶段职责、状态感知 Tool Schema、顺序执行和可观测时间锚点组成的系统合同。
**关键词:**离线语音 Agent、Android、VAD、EOU、Tool Router、端云协同
目录
- 测试条件与数字边界
- 四阶段职责,而不是模型清单
- 状态感知的能力边界
- 为什么阶段数字不能简单相加
- 顺序执行与内存账本
- 可迁移的系统合同与端云升级
908ms 是这个案例里最容易传播、也最容易被误读的数字。它看起来像一句结论:只要选对小模型,Android 手机就能在一秒内完成离线语音交互。真正成立的结论要窄得多:在一台 Galaxy S23 Ultra 上,一条面向有限设备控制任务、CPU-only、四阶段顺序执行的语音流水线,曾被原作者测得从用户停止说话到首个播出音频为 908ms。它不是“所有 Android 都能一秒响应”的证明,更不是一个通用离线对话 Agent 的基准。
这个案例值得迁移的,也不是把四个模型名称原样搬进另一个 App。它真正提供了一种系统拆法:把语音活动检测、话轮结束与转写、状态感知的工具路由、流式语音合成拆成可独立测量、替换和约束的组件;把开放式生成缩成有限 Tool Schema;把设备状态和权限变成运行时能力边界;把用户感知延迟拆成从 Speech End、Final Transcript、Tool Call 到 First Presented Audio 的一组时间锚点。
以下内容基于 Hugging Face 社区作者文章和 speech-android 指定提交进行分析,没有在 Galaxy S23 Ultra 上复现。性能数字均保留为原作者报告,不改写为本文实测。
先钉死 908ms 的测试条件

原文是 Hugging Face 的 Community Article,不是 Hugging Face 官方 Benchmark。文章发表于 2026 年 7 月 18 日,测量条件写明为 Galaxy S23 Ultra、2026 年 7 月 17 日、speech-android Commit f01841a、CPU-only(原文测试条件与测量表,指定提交)。
在这组条件下,原作者报告:Speech End 到 Final Transcript 为 217ms;Tool Routing 为 12 次平均 294ms,而且只计算 Routing Decode;TTS 首个音频为 179ms;Speech End 到首个播出音频为 908ms;Resident App Memory 为 1,116MB PSS(原文测量表)。首次运行下载的模型文件合计约 620MB(原文下载说明)。
“约 620MB 下载”和“1,116MB PSS”描述的不是同一件事。前者是落盘资产体积,后者是运行进程在测量时的比例集大小。权重映射、推理运行时、工作区、音频缓冲、Android/JVM 对象、原生堆、共享库和内存分配碎片都可能进入运行时内存账本。因此,1.2GB 这个标题数字更接近运行中的 App 内存量级,而不是 APK 大小,也不是四个模型文件的简单求和。
四个阶段不是模型清单,而是四种职责

| 阶段 | 原案例组件与体积 | 系统职责 | 可替换边界 |
|---|---|---|---|
| VAD | Silero VAD v5,约 2MB,ONNX Runtime | 判断当前音频帧是否含语音,驱动监听、打断和分段 | 输入 PCM,输出语音活动事件或概率 |
| EOU / ASR | Parakeet-EOU 120M INT8,约 153MB | 流式转写,并参与判断这一轮话是否已经结束 | 输入语音片段,输出 partial、final transcript 与话轮事件 |
| Tool Router | FunctionGemma 270M,327MB Base 加 9.5MB LoRA | 在有限工具集合中生成结构化调用,不承担开放域聊天 | 输入规范化文本、可用工具和状态,输出单个 Tool Call |
| Streaming TTS | Pocket TTS 100M INT8,约 126MB | 将确定后的短回复持续产出音频帧,让播放早于完整合成结束 | 输入文本,输出可立即播放的音频流 |
这里必须区分 VAD 和 EOU。VAD 解决的是声学问题:这一小段波形里有没有人在说话。EOU 解决的是交互问题:用户是否已经表达完这一轮意图。一个人说“把音量调到……”后停顿半秒,VAD 可能只看到静音;EOU 还需要结合转写内容、停顿长度和任务语法判断这是思考停顿还是话轮结束。反过来,没有 VAD,系统会持续把背景声送进后续链路,打断与录音状态也更难管理。
指定提交中的 Android 配置把 endOfSpeechSilenceSec 设为 0.8 秒,并保留 SpeechEnded 与 TranscriptionCompleted 两类不同事件(ControlAgentActivity.kt:语音配置与事件,事件处理)。这说明端侧语音 Agent 不是一个“听见静音就调用 LLM”的黑箱,而是一组由声学事件、转写完成事件和状态机共同推进的阶段。
这种拆分还改变了故障定位方式。VAD 误报会造成背景声触发或漏掉开头;EOU 过早会截断尚未说完的命令,过晚则直接增加等待;ASR 错误会把正确意图变成错误文本;Router 错误表现为工具名或参数错误;工具执行可能因为权限、资源或系统状态失败;TTS 即使已生成音频,也可能卡在播放器启动或首帧呈现。把所有问题合并成一个“Agent 成功率”或“端到端耗时”,只能看到结果,无法找到修复点。流水线架构的价值正在于每个边界都能记录输入、输出、置信、耗时和失败原因,并允许用局部回放数据单独回归。
Router 的核心不是 270M,而是状态感知的能力边界

FunctionGemma 在这里不是缩小版通用助手。指定提交定义的工具面只有八项:按联系人拨号、拨号码、查联系人、查音乐、播放音乐、停止音乐、调媒体音量、列出能力(ControlTools.kt:工具声明)。它接收的是经过规范化的短命令,不接收长对话历史;每次生成都使用新的单轮 Conversation,避免 KV Cache 随命令累积(LiteRtLmRuntime.kt:单轮运行时)。
更重要的是,运行时不会把全部工具无条件交给模型。代码读取 musicPlaying,只有音乐正在播放时才暴露 stop_music;随后要求解析结果必须恰好包含一个 Tool Call,零个或多个都拒绝执行(ControlTools.kt:状态过滤与单调用约束)。紧凑提示词只携带当前可用函数名和 playing / idle 状态,而不是把无限设备能力塞进上下文(CompactPrompt.kt)。实际路由路径也会记录输入、可用工具、调用数量和最终选择,形成可检查的路由遥测(ControlAgentActivity.kt:路由与执行)。
这是一条比“换更大的端侧模型”更可迁移的原则:能力集合应由系统状态生成,而不是由模型自行想象。可以把它抽象为:
available_tools = f(device_state, permission, user_context, risk_policy, connectivity)
原项目公开代码只实现了其中一部分状态过滤,主要示例是音乐播放状态与 Android 权限。把用户上下文、风险策略和网络状态加入这个函数,是基于其结构的工程推导,不是原项目已经实现的事实。
代码还把“决定做什么”和“如何向用户确认”分开。路由运行时在模型开始生成 say 参数时就停止继续解码,设备执行层根据真实联系人、媒体检索结果或音量值生成确定性反馈(LiteRtLmRuntime.kt:Routing-only Decode,ControlTools.kt:结果感知反馈)。这减少了无价值的文本生成,也避免模型在联系人不存在、媒体检索失败时仍说出成功话术。模型负责选择结构化动作,系统负责验证参数、执行动作和陈述真实结果。
这条路线能够在小模型上成立,与任务边界高度相关。联系人、媒体和音量控制有明确的动词、参数与设备状态,回复通常只有一句,执行结果可以由本地 API 验证,也不需要检索开放知识。原文明确说明 LoRA 针对精确 Tool Schema 和紧凑设备状态序列化进行训练,运行时再只提供当前有效工具(原文 Router 说明)。因此,270M 模型的任务不是“理解世界并规划一切”,而是在一个经过产品设计压缩的动作空间里完成结构化分类与参数提取。把工具从八项扩到几百项、加入多轮追问、开放问答或跨应用复杂规划,Prompt 长度、歧义、生成长度、内存和可靠性都会变化,原有延迟不能沿用。
这也意味着 Tool Schema 本身是需要版本管理的产品接口。工具名、参数类型、必填项、枚举范围、权限前置条件和状态可见性一旦改变,LoRA 训练分布、Prompt 序列化、解析器和执行器都可能失配。迁移时应把 Schema 版本写入 trace 和模型包元数据,建立兼容性测试:旧模型遇到新工具应看不到它,新模型遇到旧执行器应被拒绝,参数越界应由执行层截断或失败,而不是让模型用自然语言自行补救。
217、294、179 不能相加还原 908

把 217ms、294ms 和 179ms 相加得到 690ms,距离 908ms 还有 218ms。这个差值不能直接命名为“框架开销”,因为四个数字的统计口径并不一致。
217ms 是 Speech End 到 Final Transcript。294ms 是 12 次 Routing-only Decode 的平均值,不是同一条 908ms 端到端样本里的必然路由耗时。179ms 是 TTS 首个音频指标,而代码同时跟踪模型首帧回调、AudioTrack 播放启动和首帧实际呈现,它们不是同一个时间点。908ms 则从 Speech End 锚定到首个播出音频,天然还覆盖转写事件交接、文本规范化、状态读取、Prompt 构造、解析、工具执行、TTS 调度、播放器启动与系统调度等路径。
指定提交的端到端计算把 turnAnchorMs 放在 Speech End,把 TTS 开始前已经发生的时间与首音频时间合并为 roundMs;之后还会用 AudioTrack 的 first presentation 时间更新指标(ControlAgentActivity.kt:端到端时间锚点,首帧呈现)。在没有原始逐轮日志的情况下,正确做法是保留这些口径差异,而不是用三个阶段数字倒算第四个数字。
同样,12 次 Routing 只说明一个很小样本中的平均延迟。它没有给出工具选择准确率、参数正确率、长尾延迟、热降频后的稳定性,也没有覆盖口音、噪声、远场和多语言条件,因此不能被写成可靠性证明。
顺序执行降低峰值叠加,不等于“内存只看最大模型”

原文把顺序执行列为控制内存的重要设计:阶段一个接一个运行,避免多个推理工作区、激活张量和临时缓冲同时达到峰值(原文内存解释)。这条方向成立,但不能进一步推导成“整条流水线的峰值内存等于最大单模型”。
模型权重和推理引擎可能在不同阶段之间保持加载,VAD 与音频管线可能持续驻留,Android Runtime、LiteRT-LM、ONNX Runtime、播放器和 App 状态也会共同占用内存。项目中的 MemoryMonitor 从 /proc/self/smaps_rollup 读取进程 PSS,并以 250ms 周期更新峰值(MemoryMonitor.kt)。原作者报告的 1,116MB PSS 本身就明显高于任何一个单独下载模型。因此,顺序执行应被理解为减少工作区峰值重叠,而不是把总内存公式简化为 max(model_size)。
真正可迁移的是系统合同
第一份可迁移资产是阶段合同。VAD、EOU/ASR、Router、TTS 都应有独立输入输出、错误语义、超时、取消机制和指标。这样,换 ASR 不必重写工具执行,换 Router 不必改音频播放,升级 TTS 也不应改变权限策略。模型只是合同的一种实现。
第二份资产是外置状态机。端侧小模型的能力不应靠更长 Prompt 补足,而应靠更窄的候选空间、显式状态和执行前校验。对于移动端设备控制,权限未授予、媒体不存在、当前没有播放、联系人查找失败都应先在系统层形成状态,再决定哪些工具可见、哪些参数可接受、哪些动作必须终止。指定代码中的拨号通过 ACTION_DIAL 打开系统拨号界面,而不是静默直接呼叫(ControlAgentActivity.kt:拨号执行),这也说明高影响动作可以保留系统确认界面。
第三份资产是面向感知延迟的流式设计。Pocket TTS 以固定 80ms 帧持续输出,播放可以在完整回复合成结束前开始(原文流水线说明)。类似思路也适用于 ASR partial、Router 的结构化早停和工具执行后的短确认。优化目标不应只写“总耗时”,而应分别记录 Speech End、Final Transcript、Route Ready、Action Done、TTS First Chunk、Playback Start、First Presented Audio 和 Response Complete。只有时间锚点固定,跨设备、跨版本和跨模型比较才有意义。
第四份资产是可观测性。端侧问题常常不是模型单点失败,而是音频焦点、热降频、线程调度、播放器缓冲、权限状态和模型加载共同造成。路由输入、可见工具、选中工具、解析失败原因、各阶段耗时、当前与峰值 PSS、首帧回调与首帧呈现之间的差值,都应进入同一 Turn Trace。这样才能区分“模型慢”“模型选错”“工具执行慢”和“声音已经生成但没有及时播出”。
从端侧案例推导边云升级协议

以下是从该架构继续推导的边云设计,不是原项目事实。最稳妥的升级方式不是把端侧流水线替换成云端大模型,而是保持同一套 Tool Schema、状态快照和结果协议:本地优先完成 VAD、EOU/ASR、低风险路由和短 TTS;当本地没有可用工具、解析失败、置信不足、任务需要开放知识,或者策略要求更高等级确认时,再把规范化文本、允许的工具子集、必要状态和 trace id 发送到云端。云端返回的仍然应是受约束 Tool Call 或明确的不可执行结果,设备侧继续做权限检查和最终执行。
工具还应按风险分级。调节媒体音量可以低摩擦执行;查找联系人或音乐属于读取型动作,应避免在语音反馈中泄露过多结果;拨号、发消息、支付、门锁和车辆控制属于更高风险动作,需要确认、系统 UI、二次认证或完全禁止离线自动执行。风险分级不是让模型“更谨慎地想”,而是让运行时改变可见工具、参数范围和确认协议。
最后,复现实验应先复制测量方法,再复制结论。至少要固定设备 SoC、Android 版本、CPU/NPU 路径、冷启动或热启动、模型与运行时版本、线程数、温控状态、电量、音频输入方式、说话距离、噪声条件、语言与口音、命令集合、样本数和 P50/P95/P99。没有这些条件,“1.2GB”“CPU-only”“908ms”都只是一个特定系统切片。
验收指标也应从“模型能不能跑”升级为“任务是否可控地完成”。一条完整语音控制用例至少包含:是否正确起听、是否在正确位置结束、最终文本是否保留关键实体、是否只暴露允许工具、是否生成唯一合法调用、参数是否通过校验、动作是否真实成功、反馈是否与真实结果一致、首音频是否在预算内出现、失败时是否保持安全状态。只有这些指标同时成立,端侧 Agent 才是一个系统,而不是四个能够单独演示的模型。
这个案例证明的是:对于受限的设备控制任务,端侧语音 Agent 可以由多个小型、专职、可测量的组件组成,并通过状态感知的 Tool Router 把模型能力关进明确边界。它没有证明手机里已经装下万能 Agent,也没有证明所有手机、语言和声学环境都能复制同一组数字。真正值得复用的不是 908ms,而是让每一毫秒、每一个工具和每一次状态转换都可解释、可替换、可拒绝。
FAQ
908ms 能代表所有 Android 手机吗?
不能。它绑定 Galaxy S23 Ultra、CPU-only、指定提交和原作者测试条件。
1.2GB 是模型下载体积吗?
不是。约 620MB 是首次下载量,1,116MB 是测试时进程 PSS。
为什么不直接放一个更大的端侧模型?
有限设备控制更需要职责分离、状态约束和可拒绝执行;单纯扩大模型不能替代这些系统边界。