低延迟不是更快地猜:EOU、Barge-in 与 Turn Protocol 为什么必须统一
TL;DR
- 场景:语音 Agent 把"停顿 → 提交工具 → 用户打断"误判为"用户取消",但旧工具请求已越过网络并最终修改生产环境,运行时把成功结果写进历史。问题不是单点失败,而是 EOU、Barge-in、提交、撤销没有共享同一时间线和身份。
- 结论:EOU 不是阈值而是事务;Barge-in 是一组取消而不是一个静音按钮;Turn ID 还不够,必须有 Generation ID 与 cancel_epoch 做 fencing。延迟指标必须和误截断、误延长、陈旧结果共用同一时间锚点,否则所谓低延迟只会让错误更早并发。
- 产出:OPEN/SPECULATIVE/COMMITTED/REVOKED 四态用户轮状态机;session_id/turn_id/generation_id/task_id/cancel_epoch 五键事件信封;可落地的事件信封 JSON;六条全局不变量;九种故障注入场景的可复现测试矩阵;并把"取消、撤销、提交"和"延迟"绑到同一时钟。
版本矩阵
| 功能 | 状态 | 说明 |
|---|---|---|
Deepgram FluxEagerEndOfTurn/TurnResumed/EndOfTurn/turn_index状态机 | ✅ 已验证 | Deepgram 官方 Flux launch 文档明确:EagerEndOfTurn 用于"medium confidence for speculative response generation";TurnResumed 表示用户续说,需取消已准备响应;EndOfTurn 为高置信度提交 |
Deepgram Fluxeot_threshold默认 0.7,eot_silence_threshold_ms默认 5000ms | ✅ 已验证 | 官方 launch 文档原话:“developers only need to set: eot_threshold: confidence level required to trigger EndOfTurn (default = 0.7). eot_silence_threshold_ms: fallback silence duration to force turn end (default = 5000ms)” |
| Deepgram Flux eager 模式会多 50-70% LLM 调用 | ✅ 已验证 | 官方 launch 文档原话:“With eager_eot_threshold set to 0.3–0.5, you can expect EagerEndOfTurn 150–250ms earlier than EndOfTurn, at the cost of 50–70% more LLM calls” |
| Deepgram Flux EndOfTurn 95% 在 1.5s 内触发 | ✅ 已验证 | 官方 launch 文档原话:“Flux typically achieves a p90 (p95) latency of 1 second (1.5 seconds)” |
OpenAI Realtimeserver_vad/semantic_vadturn detection | ✅ 已验证 | 官方文档 turn_detection 配置支持 type=server_vad(按静音分块)与 semantic_vad(按语义估计是否完成并支持 eagerness) |
OpenAI Realtimeconversation.item.truncate截断未听音频 | ✅ 已验证 | 官方 Twilio DemosessionManager.ts代码:interrupt 时计算audio_end_ms = elapsedMs,发送conversation.item.truncate强制服务端上下文与用户实际听到时长匹配 |
OpenAI RealtimecancelResponse(id, sampleCount)行为 | ✅ 已验证 | 官方 reference client 文档原文:“This method will cause the model to immediately cease generation, but also truncate the item being played by removing all audio after sampleCount and clearing the text response” |
| OpenAI Realtime WebRTC/SIP vs WebSocket 缓冲归属不同 | ✅ 已验证 | 官方 Realtime 文档:WebRTC/SIP 由服务端掌握输出缓冲可在用户打断时自动截断;WebSocket 由客户端管理播放,必须发送conversation.item.truncate |
Twilio Media Streamsclear清空缓冲 /mark双重触发 | ✅ 已验证 | 官方 Twilio Media Streams 文档:clear清空服务端缓冲音频;mark既可能因音频正常播完返回,也可能因缓冲被 clear 返回,不能一律解释为"用户听完" |
| LiveKit 默认中断路径停止讲话 + 截历史到用户听到部分 | ✅ 已验证 | LiveKit 官方 Turns 文档:默认中断路径会停止讲话并把历史截到用户实际听到的部分;这是框架语义,不是 WebRTC 本身提供的事务保证 |
| gRPC 取消语义 + 取消前修改不回滚 | ✅ 已验证 | gRPC 官方 Cancellation 文档原文:“A cancellation terminates the RPC immediately so that no further work is done. Changes made before a cancellation are not rolled back” |
| gRPC 库不能直接中断应用层 handler | ✅ 已验证 | gRPC 官方 Cancellation 文档:“the client cancellation operation should propagate to the whole call chain so each end can cancel in time. For server-side, the server can check whether the specific RPC has timed out, or how much time is left” |
Web AudioAudioScheduledSourceNode.stop()只控制本地音频节点 | ⚠️ 待验证 | 本文引用了规范层面的"stop() 后该 source 输出静音",但本轮 search 未直接命中 W3C Web Audio 1.1 规范原文;建议直接核对 W3C TR |
WebRTCremoveTrack/replaceTrack(null)不会传播为 Agent 取消 | ⚠️ 待验证 | 本文按 WebRTC 规范语义给出该结论,本轮 search 未直接命中 W3C WebRTC 规范原文;建议核对 W3C TR 原文 |
LiveKit STT 模式min_delay会在 STT end-of-speech 后再加一次等待 | ⚠️ 待验证 | 本文按 LiveKit Turn Detector 文档引用该行为;本轮 search 未直接命中 livekit.io 官方原页;建议核对 livekit.io/agents/logic/turns/turn-detector 原页 |
取消契约的精细分类(cancel_observed/cancel_completed/cancel_unsupported/too_late/side_effect_committed) | ⚠️ 本文方法 | 这是本文给出的协议设计要求,不是某家供应商现有事件 |
发布边界:协议、状态机和预算属于工程设计;供应商事件名只在对应产品边界内成立,不冒充作者已完成的线上实测。
摘要
语音 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 调整等待策略。4Deepgram Flux 使用EagerEndOfTurn、TurnResumed、EndOfTurn和turn_index;其eot_threshold、eager_eot_threshold、eot_timeout_ms只适用于 Flux/v2,并非通用参数。5LiveKit 又把 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也明确要求客户端停播并丢弃缓冲,但这仍是客户端侧动作。12Web Audio 规范规定,AudioScheduledSourceNode.stop()后该 source 输出静音,但这只描述本地音频节点,不会替你取消远端模型、工具或 TTS。13即使在 WebRTC 层调用removeTrack或replaceTrack(null),规范定义的也是停止媒体发送,不会自动传播成 Agent Runtime 的模型或工具取消。14Deepgram 的AgentAudioDone也只表示服务端发完最后一个音频块,不表示用户已经听完;客户端仍需观察本地输出队列。15
供应商在这里有不同责任边界。OpenAI Realtime 的 WebRTC/SIP 连接由服务端掌握输出缓冲,可在用户打断时自动截断未播放音频;WebSocket 连接则由客户端管理播放,客户端必须停播、记录已播放时长,并发送conversation.item.truncate删除未听部分。16Twilio 双向 Media Streams 的clear会清空其缓冲音频;mark既可能因音频正常播完返回,也可能因缓冲被 clear 返回,所以应用仍要记录清空原因与 generation 状态,不能把收到 mark 一律解释为“用户听完”。17LiveKit 的默认中断路径会停止讲话并把历史截到用户实际听到的部分,这是框架语义,不是 WebRTC 本身提供的事务保证。7
由此可得一个硬规则:停止声音、停止生成、停止执行、停止提交是四个独立动作。Barge-in 必须把它们绑定到同一个取消域,但不能把任一动作的成功当作其他动作已经成功。
状态提交必须以“用户实际听到什么”为准
语音 Agent 的会话历史不应只记录“模型生成了什么”,而应区分:
- 模型生成文本;
- TTS 已生成音频;
- 音频已进入播放队列;
- 音频实际播放到哪个 sample 或毫秒位置;
- 哪一部分因打断被截断。
否则下一轮模型会以为用户听过整段旧回答,产生“如我刚才所说”之类的错位。OpenAI 的 WebSocket 截断流程要求客户端记录已播放时长,正是因为服务端不知道耳端进度。16Deepgram 也明确区分服务端发完音频与用户听完音频。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 明确说明其 transcriptstart、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 和提交前有效性检查。
低延迟最重要的单一指标是什么?
没有单一指标。结束延迟必须与误截断、误延长、打断静音和陈旧结果一起观察。
参考资料
错误速查卡
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
| 用户听不到旧回答,但旧工具已修改生产 | Barge-in 只停了扬声器,工具结果仍按原 generation apply 写历史 | 检查 generation phase 是否被原子置为 REVOKED、cancel_epoch 是否单调递增 | 先原子撤销提交权 → fanout_cancel → playback_drop;fencing 校验失败必须进 stale_result_dropped |
| 误把"用户说完了"提交给 LLM,续说后整段失效 | EOU 候选事件被当作提交信号,缺少 SPECULATIVE/COMMITTED 边界 | 检查 EOU commit 是否校验 turn_id open + transcript_revision 未被替换 | 把 user turn 建模为 OPEN/SPECULATIVE/COMMITTED/REVOKED 状态机 |
| 工具重复执行同一条写操作 | 不可逆操作在 speculative 阶段发出,没有 idempotency key | 检查 tool_call_id + idempotency_key 是否唯一并由 generation 持有 | 不可逆动作必须 prepare/commit;带幂等键;可查询终态 |
| 旧 generation 的 token 写入对话历史 | “已发送取消"被当作"远端已停止”,本地继续接 token | 检查 cancel_observed/cancel_completed/cancel_unsupported 三个事件是否区分 | 协议应区分 cancel_observed、cancel_completed、cancel_unsupported、too_late、side_effect_committed |
| 下一轮模型引用"如我刚才所说"但用户没听到 | 历史只记录了 generated_content,没有 delivered_prefix | 检查 committed_history 是否只含 heard prefix | 拆 generated_content / delivered_prefix / committed_history;中断时只提交 heard prefix |
| P95 偏高但 P50 正常,被误归因为"网络慢" | 重复 endpointing(STT 内部 min_delay + 运行时 EOU 等待) | 拆解 latency:供应商检测等待 + 网络传输 + 运行时二次等待 + 提交排队 | 取消运行时 min_delay 或允许运行时基于 STT end-of-speech 直接提交 |
| 同一段 LLM 输出被记录两次 | 取消后迟到 token 仍进入 reducer | 检查 generation_id + cancel_epoch 校验 | reducer 入口做 fencing;迟到结果进 stale_result_dropped 审计流 |
| Twilio mark 一律被解释为"用户听完" | mark 既可能因音频正常播完返回,也可能因 clear 返回 | 检查 mark 事件与 clear 事件是否同时记录及原因字段 | 应用必须记录 mark 触发原因(normal_end / cleared)与 generation 状态 |
| Web Audio stop() 后以为远端也停了 | AudioScheduledSourceNode.stop()只控制本地音频节点 | 检查本地 source 是否停了,但远端 LLM/TTS 未撤销 | 同步撤销 Playback / Gateway / TTS / LLM-Tool / History 五条链路 |
| WebRTC removeTrack 以为能取消模型/工具 | 规范定义的 removeTrack/replaceTrack 只停止媒体发送 | 检查 RtpSender.setTrack(null) 与 Agent Runtime 取消是否独立 | Agent Runtime 必须独立撤销 generation,不能依赖 WebRTC track 状态传播 |
cancel_requested当作远端已停止 | 客户端意图被当作远端执行保证 | 检查远端是否到达 cancel_observed / cancel_completed | 协议必须区分 cancel_requested(意图)与 cancel_observed(远端观察)和 cancel_completed(已停止) |
| VAD 检测到静音就直接调用工具 | VAD 误当作整轮结束 | 检查 VAD 与 EOU commit 之间是否有 SPECULATIVE 阶段 | VAD 只能进 SPECULATIVE;COMMIT 必须在 turn.committed 之后 |
is_final=true当作整轮完成 | 文档已说明is_final是片段级而非 utterance 级 | 检查 speech_final 与 is_final 是否同时使用 | 累计 finalized 片段 + 完整结束信号才提交;区分 is_final / speech_final / EndOfTurn |
| UtteranceEnd OR speech_final 当作更高置信度 | 两者语义不同轴,OR 后丢失原始信息 | 检查 OR 之前是否分别记录了原始事件与 confidence | 保留 provider / provider_event / confidence;不私自 OR |
| 取消前已做的副作用无法回滚却被承诺"已撤销" | gRPC 官方明确"Changes made before a cancellation are not rolled back" | 检查 side_effect_committed 事件是否与 cancel 分离记录 | 协议必须区分 too_late 与 side_effect_committed;幂等键 + 可查询终态是底线 |
| 回声造成假打断,旧 generation 被错误撤销 | AEC 未开或置信度低时仍按 interrupt 撤销 | 检查 interrupt 事件是否带 AEC 置信度 | 不无条件撤销;保留检测置信度与恢复路径;可回滚 |
eot_threshold调得很低但 false start 暴增 | eager 阈值降低会多 50-70% LLM 调用,false starts 也增加 | 检查 speculative_waste / false_truncation 是否同步上升 | 把 eot_threshold 调整到 P95 EOT latency 与 false_truncation 联合最优点 |
| 同一 turn 多次 candidate 提交互相污染 | 只有 turn_id 没有 generation_id | 检查事件信封是否带 generation_id + cancel_epoch | 五键事件信封:session_id / turn_id / generation_id / task_id / cancel_epoch |
| OpenAI WebSocket 模式没发 conversation.item.truncate | 客户端以为 WebSocket 也会自动截断 | 检查 sessionManager 是否发送 truncate | WebSocket 模式客户端必须记录已播放时长并显式发送 conversation.item.truncate |
| 网络 jitter 大时取消乱序导致权威状态被旧 generation 覆盖 | 先发网络 cancel,后置本地 REVOKED | 检查 on_interrupt 的原子顺序 | 顺序必须为:原子置 REVOKED + cancel_epoch++ → fanout_cancel → playback_drop |
| 取消与 LLM token 到达 race 导致 fence 失效 | reducer 在 fence 校验前已写历史 | 检查 reducer 入口的 generation_id + cancel_epoch 校验顺序 | 所有异步结果必须先过 fencing 才进 reducer / 历史 / 设备状态 |
Endpointing,官方或第一方资料,访问日期 2026-07-23。 ↩︎ ↩︎
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。 ↩︎ ↩︎
LiveKit Turns Overview,官方或第一方资料,访问日期 2026-07-23。 ↩︎ ↩︎
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。 ↩︎ ↩︎
Realtime Conversations: Interruption and Truncation,官方或第一方资料,访问日期 2026-07-23。 ↩︎ ↩︎
Twilio Media Streams: WebSocket Messages,官方或第一方资料,访问日期 2026-07-23。 ↩︎
Measuring STT Latency,官方或第一方资料,访问日期 2026-07-23。 ↩︎ ↩︎
Session Observability,官方或第一方资料,访问日期 2026-07-23。 ↩︎