打断不是静音

发布边界:协议、状态机和预算属于工程设计;供应商事件名只在对应产品边界内成立,不冒充作者已完成的线上实测。

摘要

语音 Agent 的低延迟不只是缩短静音阈值。EOU 决定何时允许一轮产生后果,Barge-in 决定旧一代工作何时失去提交资格,延迟指标必须证明取消、静音和结果丢弃发生在同一时间线上。本文给出统一 Turn Protocol、Generation fencing、已听前缀和可复现实验方法。

关键词

EOU、Barge-in、Turn Protocol、Generation ID、语音 Agent

目录

用户说:“把客厅空调调到二十度……”系统在停顿处判定一轮结束,LLM 发出工具调用,TTS 开始播“好的,正在为你……”。用户立刻打断:“算了,先别动。”

音频前端做对了表面上的事:停掉扬声器,清空播放队列。用户听不到旧回答了,界面也显示 Agent 正在听。但旧工具请求已经越过网络。随后,它返回成功,设备真的被调到二十度;运行时还把结果写进会话历史。下一轮模型看到的是“操作已完成”,用户感知到的却是“我已经打断”。这不是单纯的 Barge-in 失败,也不是 TTS 停得不够快,而是旧一轮的执行权没有随着打断一起失效。

这类竞态说明:EOU(End of Utterance,话语结束)、Barge-in 和端到端延迟不是三个可以分别调参的模块。EOU 事件只能提出“用户可能说完”的候选,应用层 EOT(End of Turn)或 turn commit 才把输入从“仍可能修订”提升为“允许执行”;Barge-in 决定哪一代工作从此失去提交资格;延迟指标则必须证明这次提交、撤销、静音和结果丢弃发生在同一条时间线上。三者若不共享 Turn ID、取消契约、状态提交规则和时间锚点,所谓低延迟只会让错误更早并发、更快越过不可逆边界。

四种结束不能压成 final

先把四种“结束”拆开

工程中最常见的误区,是把所有结束事件都压成一个布尔值 final=true。实际系统至少存在四类信号,它们的输入、输出和可证明的事实不同。

机制主要输入典型输出能证明什么,不能证明什么
VAD波形、能量、频谱及声学特征speech_startedspeech_stopped 或 speech/non-speech 状态能说明声学活动发生变化;不能说明一句话、一个意图或一轮对话已经完整
EndpointingVAD 状态加静音持续时间,部分实现再结合 STT 状态停顿边界、供应商特定的结束标记能说明出现了足够长的停顿;不能证明用户不再续说,也不能自动等价为语义提交
UtteranceEnd已转写词及其时间戳、词间空档词间空档达到阈值后的事件能绕开部分非语音噪声造成的 VAD 问题;仍是基于转写空档的启发式,不是语义完成证明
语义 EOU转写文本、上下文,有时结合声学特征完成概率、EndOfTurn 或动态等待决策能利用句法、语义和上下文判断“是否像说完了”;仍受模型、语言、领域和阈值约束,不是跨供应商标准

Deepgram 的文档把这些边界写得很清楚。其 Endpointing 基于 VAD 检测从语音到静音的转换,达到配置时长后返回 speech_final=trueUtteranceEnd 则查看 finalized 与 interim 结果中的词时间,在词间空档足够长时独立发事件。两者可以同时启用,也可能先后或只出现其一。12 因此,把 UtteranceEnd 当成更高级的 speech_final,或者把两者做 OR 后直接执行高风险工具,都会丢掉原始语义。

is_final 也不是整轮结束。在 Deepgram 的流式转写中,is_final=true 表示某个音频片段的文本不再修订;长输入可能先后产生多个 finalized 片段,而 speech_final 仍为 false。完整 utterance 需要累计 finalized 片段,再以结束信号决定何时提交。官方示例甚至出现 is_final=falsespeech_final=true 的组合,说明两个字段本来就在不同轴上。13 “转写分段完成”和“用户整轮完成”必须使用不同状态字段。

供应商命名也不能拼成虚构标准。OpenAI Realtime 把 server_vadsemantic_vad 都放在 turn detection 配置下:前者按静音分块,后者按用户所说内容估计是否完成,并通过 eagerness 调整等待策略。4 Deepgram Flux 使用 EagerEndOfTurnTurnResumedEndOfTurnturn_index;其 eot_thresholdeager_eot_thresholdeot_timeout_ms 只适用于 Flux/v2,并非通用参数。5 LiveKit 又把 VAD、STT endpointing、语义 turn detector、realtime model 内建检测和手动提交列为不同模式。67 设计内部协议时,可以把它们归一到“候选结束”“恢复说话”“确认结束”等抽象事件,但必须保留 providerprovider_event、置信度和原始载荷,不能假设字段同义。

EOU 是事务,不是阈值

EOU 不是一个阈值,而是一笔事务

EOU 的正确抽象不是“何时调用 LLM”,而是“何时允许这一轮产生可提交后果”。建议把用户轮建模为状态机:

OPEN -> SPECULATIVE -> COMMITTED -> COMPLETED

其中任何尚未完成的状态都可以进入 REVOKEDSPECULATIVE 还可以因用户续说回到 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 还不够

Turn ID 还不够,必须有 Generation ID

一轮用户输入可能触发多次候选提交:第一次短停顿启动草稿,用户续说后撤销;第二次候选又启动新草稿;最终确认才成为正式响应。它们属于同一个 turn_id,却不是同一代工作。因此协议至少要有以下关联键:

  • session_id:会话范围;
  • turn_id:一轮用户意图的逻辑身份;
  • generation_id:该轮的一次推理/执行尝试;
  • task_id:LLM、工具、TTS、播放等子任务;
  • tool_call_idaudio_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 是一组取消,不是一个静音按钮

完整 Barge-in 至少包含五条链路。

第一,检测链路确认用户开始说话,并区分真实语音、回声、环境噪声和短促非语音。检测事件只负责提出 interrupt,不直接证明新一轮已完整。

第二,运行时撤销旧 generation,停止接受其模型 token、工具计划、工具结果和历史写入。对于已经开始的 LLM 请求,发送 provider 支持的 cancel;但本地必须继续丢弃取消后的 token,因为“已发送取消”不等于“远端已经停止”。

第三,工具链路按副作用等级处理。只读工具可以在撤销后直接丢结果;幂等写操作必须携带 idempotency key、turn/generation metadata 和可查询终态;不可逆动作不应在 speculative 阶段发出,必要时采用 prepare/commit、显式确认或补偿事务。gRPC 的官方取消指南指出,库通常不能直接中断应用层 handler,服务端需要主动检查取消并停止处理;向上游的取消传播在不同语言实现中也不完全相同。10 因此客户端的 cancel_requested 只是意图,不是远端停止执行、停止计费或撤销副作用的证明。协议应区分 cancel_observedcancel_completedcancel_unsupportedtoo_lateside_effect_committed

第四,TTS 生成链路停止继续合成。Deepgram TTS 的 Clear 清除其服务端内部文本与音频生成缓冲,并尽快停止发送新音频块;它不代表浏览器、移动端、电话网关或声卡里的已收音频被清除。11

第五,播放链路立即停当前 source,清掉所有属于旧 generation 的排队 chunk,并拒绝取消后迟到的音频。Deepgram Voice Agent 的 UserStartedSpeaking 也明确要求客户端停播并丢弃缓冲,但这仍是客户端侧动作。12 Web Audio 规范规定,AudioScheduledSourceNode.stop() 后该 source 输出静音,但这只描述本地音频节点,不会替你取消远端模型、工具或 TTS。13 即使在 WebRTC 层调用 removeTrackreplaceTrack(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 的会话历史不应只记录“模型生成了什么”,而应区分:

  1. 模型生成文本;
  2. TTS 已生成音频;
  3. 音频已进入播放队列;
  4. 音频实际播放到哪个 sample 或毫秒位置;
  5. 哪一部分因打断被截断。

否则下一轮模型会以为用户听过整段旧回答,产生“如我刚才所说”之类的错位。OpenAI 的 WebSocket 截断流程要求客户端记录已播放时长,正是因为服务端不知道耳端进度。16 Deepgram 也明确区分服务端发完音频与用户听完音频。15

建议把 assistant 输出拆成 generated_contentdelivered_prefixcommitted_history。模型 token 可以持续写入临时缓冲;只有可映射到已播放音频的前缀进入 delivered 记录。发生中断时,权威历史提交 heard prefix,未听部分标为 revoked。文本与音频无法精确对齐时,不要伪造逐字截断;保留音频播放游标、原始文本、对齐置信度和截断策略,让后续模型知道这是近似历史。

工具状态也要分层:tool_plannedtool_dispatchedtool_side_effect_committedtool_result_receivedtool_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 startduration 不保证适合精确延迟测量。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

  1. Endpointing,官方或第一方资料,访问日期 2026-07-23。 2

  2. End of Speech Detection While Live Streaming,官方或第一方资料,访问日期 2026-07-23。

  3. Configure Endpointing and Interim Results,官方或第一方资料,访问日期 2026-07-23。

  4. Voice Activity Detection (VAD),官方或第一方资料,访问日期 2026-07-23。

  5. Configure the Voice Agent,官方或第一方资料,访问日期 2026-07-23。

  6. Understanding the Flux State Machine,官方或第一方资料,访问日期 2026-07-23。 2

  7. LiveKit Turns Overview,官方或第一方资料,访问日期 2026-07-23。 2

  8. Optimize Voice Agent Latency with Eager End of Turn,官方或第一方资料,访问日期 2026-07-23。

  9. LiveKit Turn Detector,官方或第一方资料,访问日期 2026-07-23。

  10. gRPC Cancellation,官方或第一方资料,访问日期 2026-07-23。

  11. TTS WebSocket Clear,官方或第一方资料,访问日期 2026-07-23。

  12. User Started Speaking,官方或第一方资料,访问日期 2026-07-23。

  13. Web Audio API 1.1,官方或第一方资料,访问日期 2026-07-23。

  14. WebRTC: Real-Time Communication in Browsers,官方或第一方资料,访问日期 2026-07-23。

  15. Agent Audio Done,官方或第一方资料,访问日期 2026-07-23。 2

  16. Realtime Conversations: Interruption and Truncation,官方或第一方资料,访问日期 2026-07-23。 2

  17. Twilio Media Streams: WebSocket Messages,官方或第一方资料,访问日期 2026-07-23。

  18. Measuring STT Latency,官方或第一方资料,访问日期 2026-07-23。 2

  19. Session Observability,官方或第一方资料,访问日期 2026-07-23。