
发布边界:协议、状态机和预算属于工程设计;供应商事件名只在对应产品边界内成立,不冒充作者已完成的线上实测。
摘要
语音 Agent 的低延迟不只是缩短静音阈值。EOU 决定何时允许一轮产生后果,Barge-in 决定旧一代工作何时失去提交资格,延迟指标必须证明取消、静音和结果丢弃发生在同一时间线上。本文给出统一 Turn Protocol、Generation fencing、已听前缀和可复现实验方法。
关键词
EOU、Barge-in、Turn Protocol、Generation ID、语音 Agent
目录
- 先把四种“结束”拆开
- EOU 不是一个阈值,而是一笔事务
- Turn ID 还不够,必须有 Generation ID
- Barge-in 是一组取消,不是一个静音按钮
- 状态提交必须以“用户实际听到什么”为准
- 延迟必须和正确性共用时间锚点
- 可复现测试矩阵
- 结论:先定义失效,再谈快
用户说:“把客厅空调调到二十度……”系统在停顿处判定一轮结束,LLM 发出工具调用,TTS 开始播“好的,正在为你……”。用户立刻打断:“算了,先别动。”
音频前端做对了表面上的事:停掉扬声器,清空播放队列。用户听不到旧回答了,界面也显示 Agent 正在听。但旧工具请求已经越过网络。随后,它返回成功,设备真的被调到二十度;运行时还把结果写进会话历史。下一轮模型看到的是“操作已完成”,用户感知到的却是“我已经打断”。这不是单纯的 Barge-in 失败,也不是 TTS 停得不够快,而是旧一轮的执行权没有随着打断一起失效。
这类竞态说明:EOU(End of Utterance,话语结束)、Barge-in 和端到端延迟不是三个可以分别调参的模块。EOU 事件只能提出“用户可能说完”的候选,应用层 EOT(End of Turn)或 turn commit 才把输入从“仍可能修订”提升为“允许执行”;Barge-in 决定哪一代工作从此失去提交资格;延迟指标则必须证明这次提交、撤销、静音和结果丢弃发生在同一条时间线上。三者若不共享 Turn ID、取消契约、状态提交规则和时间锚点,所谓低延迟只会让错误更早并发、更快越过不可逆边界。

先把四种“结束”拆开
工程中最常见的误区,是把所有结束事件都压成一个布尔值 final=true。实际系统至少存在四类信号,它们的输入、输出和可证明的事实不同。
| 机制 | 主要输入 | 典型输出 | 能证明什么,不能证明什么 |
|---|---|---|---|
| VAD | 波形、能量、频谱及声学特征 | speech_started、speech_stopped 或 speech/non-speech 状态 | 能说明声学活动发生变化;不能说明一句话、一个意图或一轮对话已经完整 |
| Endpointing | VAD 状态加静音持续时间,部分实现再结合 STT 状态 | 停顿边界、供应商特定的结束标记 | 能说明出现了足够长的停顿;不能证明用户不再续说,也不能自动等价为语义提交 |
| UtteranceEnd | 已转写词及其时间戳、词间空档 | 词间空档达到阈值后的事件 | 能绕开部分非语音噪声造成的 VAD 问题;仍是基于转写空档的启发式,不是语义完成证明 |
| 语义 EOU | 转写文本、上下文,有时结合声学特征 | 完成概率、EndOfTurn 或动态等待决策 | 能利用句法、语义和上下文判断“是否像说完了”;仍受模型、语言、领域和阈值约束,不是跨供应商标准 |
Deepgram 的文档把这些边界写得很清楚。其 Endpointing 基于 VAD 检测从语音到静音的转换,达到配置时长后返回 speech_final=true;UtteranceEnd 则查看 finalized 与 interim 结果中的词时间,在词间空档足够长时独立发事件。两者可以同时启用,也可能先后或只出现其一。12 因此,把 UtteranceEnd 当成更高级的 speech_final,或者把两者做 OR 后直接执行高风险工具,都会丢掉原始语义。
is_final 也不是整轮结束。在 Deepgram 的流式转写中,is_final=true 表示某个音频片段的文本不再修订;长输入可能先后产生多个 finalized 片段,而 speech_final 仍为 false。完整 utterance 需要累计 finalized 片段,再以结束信号决定何时提交。官方示例甚至出现 is_final=false、speech_final=true 的组合,说明两个字段本来就在不同轴上。13 “转写分段完成”和“用户整轮完成”必须使用不同状态字段。
供应商命名也不能拼成虚构标准。OpenAI Realtime 把 server_vad 和 semantic_vad 都放在 turn detection 配置下:前者按静音分块,后者按用户所说内容估计是否完成,并通过 eagerness 调整等待策略。4 Deepgram Flux 使用 EagerEndOfTurn、TurnResumed、EndOfTurn 和 turn_index;其 eot_threshold、eager_eot_threshold、eot_timeout_ms 只适用于 Flux/v2,并非通用参数。5 LiveKit 又把 VAD、STT endpointing、语义 turn detector、realtime model 内建检测和手动提交列为不同模式。67 设计内部协议时,可以把它们归一到“候选结束”“恢复说话”“确认结束”等抽象事件,但必须保留 provider、provider_event、置信度和原始载荷,不能假设字段同义。

EOU 不是一个阈值,而是一笔事务
EOU 的正确抽象不是“何时调用 LLM”,而是“何时允许这一轮产生可提交后果”。建议把用户轮建模为状态机:
OPEN -> SPECULATIVE -> COMMITTED -> COMPLETED
其中任何尚未完成的状态都可以进入 REVOKED;SPECULATIVE 还可以因用户续说回到 OPEN。候选 EOU 只允许系统做可丢弃的工作,例如启动草稿生成、预取只读数据、准备 TTS 文本切分。确认 EOU 才授予正式提交权:把用户消息写入权威历史、允许有副作用的工具进入 commit 阶段、把可播放回复绑定到当前 generation。
这正是 eager EOT 的真实成本。Deepgram 明确说明,降低 eager 阈值会更早触发,也会产生更多 false starts;若随后收到 TurnResumed,在途回复应被取消或丢弃,且 eager 模式会增加 LLM 请求。8 因而“降低阈值减少等待”只描述了收益的一半。另一半是误截断、无效模型调用、错误工具规划和更多待撤销音频。若运行时没有 speculative/committed 边界,提前几十或几百毫秒启动的草稿就会越过工具与状态提交点,优化立即变成一致性缺陷。
确认也不能只看事件名。一个稳健的提交动作至少应校验:当前 turn_id 仍处于开放状态;候选所依据的 transcript_revision 未被后续转写替换;当前 generation 未被打断;供应商事件满足本产品的提交策略;硬超时只作为降级路径而非“语义正确”的证明。Deepgram Flux 保证同一生命周期中 EndOfTurn 文本与紧邻的 EagerEndOfTurn 文本匹配,并在续说时先发 TurnResumed,这是该供应商的状态机契约,不应外推到其他 STT。6
还有一个常被忽略的尾延迟来源:重复 endpointing。LiveKit 文档说明,在 STT 模式下,运行时的 min_delay 会加在 STT 供应商的 end-of-speech 信号之后。9 如果 STT 已等一次静音,Agent Runtime 又无条件再等一次,P95/P99 会出现并非模型慢、而是两个结束策略串联造成的“误延长”。所有 EOU 延迟都应分解为供应商检测等待、网络传输、运行时二次等待和提交排队,而不是统称“ASR latency”。

Turn ID 还不够,必须有 Generation ID
一轮用户输入可能触发多次候选提交:第一次短停顿启动草稿,用户续说后撤销;第二次候选又启动新草稿;最终确认才成为正式响应。它们属于同一个 turn_id,却不是同一代工作。因此协议至少要有以下关联键:
session_id:会话范围;turn_id:一轮用户意图的逻辑身份;generation_id:该轮的一次推理/执行尝试;task_id:LLM、工具、TTS、播放等子任务;tool_call_id、audio_segment_id:外部调用与可播放片段身份;cancel_epoch或 fencing token:用于拒绝旧代迟到结果。
一个可落地的事件信封如下。字段名不是标准,关键是所有层共享同一组身份和时间锚点。
{
"session_id": "s-7f",
"turn_id": "t-18",
"generation_id": "g-18.2",
"cancel_epoch": 4,
"event_id": "e-991",
"parent_event_id": "e-977",
"kind": "tool.result",
"phase": "speculative|committed|revoked|completed",
"transcript_revision": 12,
"provider": "internal-or-vendor",
"provider_event": "raw-event-name",
"seq": 1842,
"ts_mono_ns": 4829910020031,
"ts_wall_utc": "2026-07-23T20:15:31.842Z",
"payload": {}
}
generation_id 的核心作用是 fencing,而不只是日志关联。任何异步结果进入 reducer、会话历史、设备状态或播放队列之前,都必须验证:“这个 generation 是否仍是该 turn 的有效持有者,且当前 phase 是否允许这种提交?”验证失败的结果进入 stale_result_dropped 审计流,不得靠调用方“尽量别返回”来保证安全。
Barge-in 到来时,正确顺序是先在本地原子撤销旧 generation 的提交资格,再向各下游发送取消。伪代码可简化为:
on_interrupt(new_user_audio):
old = active_generation
atomic:
old.phase = REVOKED
old.cancel_epoch += 1
deny_future_commits(old)
active_generation = open_new_turn_or_generation()
fanout_cancel(old)
playback_drop(old)
这个顺序解决最危险的竞态:即使工具结果在 fanout_cancel 之前或期间到达,它也因 fencing 校验失败而不能污染权威状态。反过来,若先发网络取消、后标记本地失效,迟到结果可能正好穿过窗口。

Barge-in 是一组取消,不是一个静音按钮
完整 Barge-in 至少包含五条链路。
第一,检测链路确认用户开始说话,并区分真实语音、回声、环境噪声和短促非语音。检测事件只负责提出 interrupt,不直接证明新一轮已完整。
第二,运行时撤销旧 generation,停止接受其模型 token、工具计划、工具结果和历史写入。对于已经开始的 LLM 请求,发送 provider 支持的 cancel;但本地必须继续丢弃取消后的 token,因为“已发送取消”不等于“远端已经停止”。
第三,工具链路按副作用等级处理。只读工具可以在撤销后直接丢结果;幂等写操作必须携带 idempotency key、turn/generation metadata 和可查询终态;不可逆动作不应在 speculative 阶段发出,必要时采用 prepare/commit、显式确认或补偿事务。gRPC 的官方取消指南指出,库通常不能直接中断应用层 handler,服务端需要主动检查取消并停止处理;向上游的取消传播在不同语言实现中也不完全相同。10 因此客户端的 cancel_requested 只是意图,不是远端停止执行、停止计费或撤销副作用的证明。协议应区分 cancel_observed、cancel_completed、cancel_unsupported、too_late 和 side_effect_committed。
第四,TTS 生成链路停止继续合成。Deepgram TTS 的 Clear 清除其服务端内部文本与音频生成缓冲,并尽快停止发送新音频块;它不代表浏览器、移动端、电话网关或声卡里的已收音频被清除。11
第五,播放链路立即停当前 source,清掉所有属于旧 generation 的排队 chunk,并拒绝取消后迟到的音频。Deepgram Voice Agent 的 UserStartedSpeaking 也明确要求客户端停播并丢弃缓冲,但这仍是客户端侧动作。12 Web Audio 规范规定,AudioScheduledSourceNode.stop() 后该 source 输出静音,但这只描述本地音频节点,不会替你取消远端模型、工具或 TTS。13 即使在 WebRTC 层调用 removeTrack 或 replaceTrack(null),规范定义的也是停止媒体发送,不会自动传播成 Agent Runtime 的模型或工具取消。14 Deepgram 的 AgentAudioDone 也只表示服务端发完最后一个音频块,不表示用户已经听完;客户端仍需观察本地输出队列。15
供应商在这里有不同责任边界。OpenAI Realtime 的 WebRTC/SIP 连接由服务端掌握输出缓冲,可在用户打断时自动截断未播放音频;WebSocket 连接则由客户端管理播放,客户端必须停播、记录已播放时长,并发送 conversation.item.truncate 删除未听部分。16 Twilio 双向 Media Streams 的 clear 会清空其缓冲音频;mark 既可能因音频正常播完返回,也可能因缓冲被 clear 返回,所以应用仍要记录清空原因与 generation 状态,不能把收到 mark 一律解释为“用户听完”。17 LiveKit 的默认中断路径会停止讲话并把历史截到用户实际听到的部分,这是框架语义,不是 WebRTC 本身提供的事务保证。7
由此可得一个硬规则:停止声音、停止生成、停止执行、停止提交是四个独立动作。 Barge-in 必须把它们绑定到同一个取消域,但不能把任一动作的成功当作其他动作已经成功。
状态提交必须以“用户实际听到什么”为准
语音 Agent 的会话历史不应只记录“模型生成了什么”,而应区分:
- 模型生成文本;
- TTS 已生成音频;
- 音频已进入播放队列;
- 音频实际播放到哪个 sample 或毫秒位置;
- 哪一部分因打断被截断。
否则下一轮模型会以为用户听过整段旧回答,产生“如我刚才所说”之类的错位。OpenAI 的 WebSocket 截断流程要求客户端记录已播放时长,正是因为服务端不知道耳端进度。16 Deepgram 也明确区分服务端发完音频与用户听完音频。15
建议把 assistant 输出拆成 generated_content、delivered_prefix 和 committed_history。模型 token 可以持续写入临时缓冲;只有可映射到已播放音频的前缀进入 delivered 记录。发生中断时,权威历史提交 heard prefix,未听部分标为 revoked。文本与音频无法精确对齐时,不要伪造逐字截断;保留音频播放游标、原始文本、对齐置信度和截断策略,让后续模型知道这是近似历史。
工具状态也要分层:tool_planned、tool_dispatched、tool_side_effect_committed、tool_result_received、tool_result_applied。撤销后收到结果,可以阻止 applied,却未必能逆转 side_effect_committed。把两个状态混成一个 tool_done,正是开篇竞态无法审计的原因。

延迟必须和正确性共用时间锚点
端到端延迟不能只测“用户停说到首包音频”。那会奖励提前误判 EOU、提前启动无效 LLM、提前播出随后被截断的回答。Deepgram 将 transcript latency 与 EOT latency 分开,并提醒 finalized 结果会混入 endpoint 等待;精确 EOT 还需要真实 speech-end 锚点。其文档同时建议看代表性样本上的 P50、P95、P99,而不是单次或平均值。18
一条可重放时间线至少记录:
audio.user_speech_start_ground_truth
audio.user_speech_end_ground_truth
vad.speech_started / vad.speech_stopped
eou.candidate_received
turn.speculative_started
turn.resumed
turn.committed
llm.request_sent / first_token / cancel_sent / cancel_ack / terminal
tool.dispatched / cancel_sent / side_effect_committed / result_received / result_applied
tts.request_sent / first_audio_received / clear_sent / cleared
playback.enqueued / first_sample / last_sample / queue_cleared / output_silent
barge_in.ground_truth_start / detected / generation_revoked
stale_result_dropped
同一进程的时长用 monotonic clock 计算,跨节点关联保留 UTC wall clock、时钟同步状态和单调 seq。不要直接拿供应商词时间戳当毫秒级墙钟;Deepgram 明确说明其 transcript start、duration 不保证适合精确延迟测量。18 其 Voice Agent 可观测性指南也建议给每个收发帧加自有时间戳和序列号,并保存 function call、barge-in 与 latency 事件。19
核心指标应成组发布:
- EOU commit latency:
turn.committed - speech_end_ground_truth; - response-to-ear:
playback.first_sample - speech_end_ground_truth; - barge detection latency:
barge_in.detected - barge_in.ground_truth_start; - residual playback:
playback.output_silent - barge_in.ground_truth_start,并另报从 detection 起算的系统处置时长; - cancel propagation:从 generation revoked 到 LLM、tool、TTS、playback 各自 terminal/ack;
- false truncation rate:标注仍属同一用户轮,却已提交或开始不可逆执行的比例;
- false extension rate:标注已结束,但提交超过产品自定 SLO 的比例;
- speculative waste:被撤销的模型请求、token、TTS 音频和未播放字节;
- stale tool execution:generation 撤销后仍开始或完成副作用的次数;
- stale result drop:迟到结果被 fencing 拒绝的次数。
P50 说明常态,P95/P99 暴露抖动、排队、网络与重复 endpointing;误截断、误延长、残留播放和 stale tool execution 则约束“快但错”。这些指标必须按语言、噪声、回声、设备、网络、句式和打断位置分层,不能用一个平均延迟掩盖尾部竞态。
可复现测试矩阵
测试不需要虚构“线上二百轮”。更可靠的做法是用双轨音频语料、确定性 fake service 和故障注入构造可重复实验:一轨是用户近讲,另一轨是 Agent 扬声器回灌;语料带人工标注的 speech start、speech end、意图边界和 barge-in 起点。Fake LLM、tool、TTS 可配置首包延迟、取消是否协作、结果乱序、重复投递和“副作用已提交后才收到取消”。网络层注入 jitter、丢包、重连与跨通道乱序。
| 场景 | 注入方式 | 必须成立的协议断言 | 主要观测 |
|---|---|---|---|
| 句中自然停顿后续说 | 在一个意图中插入不同长度静音 | 候选 EOU 可启动草稿,但不得提前提交不可逆工具;续说后旧 generation 被撤销 | false truncation、speculative waste |
| 真正结束但有背景噪声 | 叠加音乐、敲击或电话铃声 | VAD、UtteranceEnd、语义 EOU 的原始事件分别留存;最终只提交一次 | EOU P50/P95/P99、false extension |
| LLM 生成中打断 | 首 token 前后分别触发 barge-in | 本地先 revoke;取消后 token 全部被丢弃;新轮不继承未听旧文本 | cancel propagation、stale token drop |
| 工具请求在途时打断 | 工具设置可协作取消、不可取消、too-late 三种模式 | 迟到结果不得 apply;副作用终态可审计;重复请求由 idempotency key 抑制 | stale execution、side-effect committed |
| TTS 已生成但未播放 | 延长客户端队列 | TTS clear 与本地 queue clear 都执行;旧 generation 后续音频不得入队 | 未播放字节、queue clear latency |
| 正在播放时打断 | 在不同播放游标触发 | output 进入静音;历史只提交 heard prefix;旧 chunk 全部失效 | residual playback、截断游标 |
| 回声造成假打断 | 提高扬声器回灌并切换 AEC | 不得无条件撤销;检测置信度和恢复路径可追踪 | false barge-in、恢复延迟 |
| 取消与结果乱序 | 让 result 先于或后于 cancel ack 到达 | fencing 决定能否提交,消息到达顺序不得改变权威结果 | stale result drop、终态唯一性 |
| 断线重连与重复投递 | 重放事件、复用 tool_call_id | 每个 turn/generation 只有一个终态;副作用不重复 | duplicate suppression、审计完整率 |
除场景断言外,还应做六条全局不变量测试:被撤销 generation 的结果永不进入权威历史;不可逆工具默认不得在 speculative phase 发出;旧 generation 音频在 queue clear 后不能再次入队;每个 generation 只有一个终态;每次副作用都有幂等键和可查询终态;用户历史与实际听到的前缀一致。只要其中一条不成立,低延迟数据就不具备发布价值。
结论:先定义失效,再谈快
实时语音系统的关键单位不是 ASR 请求、LLM 请求或一段 TTS 音频,而是带身份、阶段和提交权的一代 turn execution。EOU 提出或确认一次提交;Barge-in 撤销旧代提交权;LLM、工具、TTS 和播放器通过同一个 generation fencing 决定结果是否仍可进入系统;可观测性用同一组时间锚点证明撤销是否及时、尾部是否稳定、旧结果是否被隔离。
没有这层 Turn Protocol,团队会得到一组彼此“优化成功”的局部指标:ASR 更早 final,LLM 更早发请求,TTS 更早出首包,播放器更快静音。与此同时,误截断增加、无效调用增加、旧工具继续执行、历史记录与用户耳中内容分叉。协议的作用不是让系统变慢,而是把推测、提交、撤销和交付变成可验证的状态变化。只有在旧工作能够被可靠失效之后,提前计算才是真正的低延迟,而不是更快地产生竞态。
FAQ
VAD 检测到静音后能不能直接调用工具?
不建议。VAD 只证明声学活动变化,应先进入可撤销候选阶段;有副作用工具需要确认提交和代际校验。
收到取消请求是否代表远端任务已经停止?
不代表。取消可能只是表达不再需要结果,远端 handler 仍可能执行,因此还需要 fencing 和提交前有效性检查。
低延迟最重要的单一指标是什么?
没有单一指标。结束延迟必须与误截断、误延长、打断静音和陈旧结果一起观察。
参考资料
Footnotes
-
Endpointing,官方或第一方资料,访问日期 2026-07-23。 ↩ ↩2
-
End of Speech Detection While Live Streaming,官方或第一方资料,访问日期 2026-07-23。 ↩
-
Configure Endpointing and Interim Results,官方或第一方资料,访问日期 2026-07-23。 ↩
-
Voice Activity Detection (VAD),官方或第一方资料,访问日期 2026-07-23。 ↩
-
Configure the Voice Agent,官方或第一方资料,访问日期 2026-07-23。 ↩
-
Understanding the Flux State Machine,官方或第一方资料,访问日期 2026-07-23。 ↩ ↩2
-
LiveKit Turns Overview,官方或第一方资料,访问日期 2026-07-23。 ↩ ↩2
-
Optimize Voice Agent Latency with Eager End of Turn,官方或第一方资料,访问日期 2026-07-23。 ↩
-
LiveKit Turn Detector,官方或第一方资料,访问日期 2026-07-23。 ↩
-
gRPC Cancellation,官方或第一方资料,访问日期 2026-07-23。 ↩
-
TTS WebSocket Clear,官方或第一方资料,访问日期 2026-07-23。 ↩
-
User Started Speaking,官方或第一方资料,访问日期 2026-07-23。 ↩
-
Web Audio API 1.1,官方或第一方资料,访问日期 2026-07-23。 ↩
-
WebRTC: Real-Time Communication in Browsers,官方或第一方资料,访问日期 2026-07-23。 ↩
-
Agent Audio Done,官方或第一方资料,访问日期 2026-07-23。 ↩ ↩2
-
Realtime Conversations: Interruption and Truncation,官方或第一方资料,访问日期 2026-07-23。 ↩ ↩2
-
Twilio Media Streams: WebSocket Messages,官方或第一方资料,访问日期 2026-07-23。 ↩
-
Measuring STT Latency,官方或第一方资料,访问日期 2026-07-23。 ↩ ↩2
-
Session Observability,官方或第一方资料,访问日期 2026-07-23。 ↩