核心摘要
低延迟语音 Agent 是分布式实时系统,正确性不能只靠“所有组件都流式化”。系统必须区分可修订的 ASR 假设与已提交的用户轮次,用 Generation Epoch 隔离每次回答,确认客户端真正播放了哪些音频,并在可比较的时钟上测量具名事件。Barge-in 不是一个 VAD 回调,而是一套取消、播放清理与状态对账协议。发布门禁还必须同时验证对话节奏、任务完成、工具副作用、策略遵循和故障恢复。
目录
- 核心要点
- 生产正确性模型
- 把延迟定义为事件区间
- 参考架构与时钟边界
- ASR 假设与轮次提交
- 轮次管理不等于语音活动检测
- Generation Epoch 与播放事实
- 面向客户端播放的打断协议
- 传输选择与 WebRTC 可观测性
- 安全隐私与工具副作用
- 评测契约
- 架构选型
- 最佳实践
- 常见问题
- 总结
核心要点
- ASR Partial 是假设,不是对话事实。 它只能启动可安全丢弃且没有外部副作用的工作。
- Turn Commit 是应用事件。 VAD 停止、ASR Final 和语义完整度都只是提交依据,三者不能混为一谈。
- 取消需要 Epoch 与播放确认。 否则迟到的模型 Token、TTS Chunk 或客户端缓冲音频会串入下一轮。
- 用户体感延迟应尽量用同一时钟测量。 没有同步模型和误差范围时,不能直接相减浏览器与服务端的 Wall Clock。
- 重叠语音有不同含义。 真正打断需要让出话权,附和、对他人说话或背景人声通常不需要。
- 语音体验和任务正确性必须共用发布门禁。 回答很快、声音自然但执行了错误 Tool Call,仍然是失败。
生产正确性模型
生产级语音 Agent 应把一次用户轮次和一次 Assistant Generation 拆成归属明确的状态转换。“监听、思考、说话”过于粗糙,无法解释文本是否仍可修订、音频只是合成还是已经播放,以及外部动作是否真正完成。
至少需要区分四类记录:
| 记录 | 含义 | 是否持久化 |
|---|---|---|
| ASR Hypothesis | 与音频区间绑定、后续可修订的文本 | 否 |
| Committed User Turn | 应用接受的转写和音频边界 | 是 |
| Assistant Draft | 某个 Epoch 中已生成或已合成的内容 | 否 |
| Heard Assistant Prefix | 客户端确认用户已经听到的语义单元 | 是 |
工具执行还应拥有独立状态机。已提交 Transcript 可以提出动作,但只有可信应用代码能够授权。当请求发出后连接超时,状态应进入 outcome_unknown;此时盲目重试可能重复付款、预订或发消息。
把延迟定义为事件区间
只有明确起点、终点、时钟和纳入规则,语音延迟才能用于工程诊断。单一的“响应延迟”会把 Endpointing、模型推理、语音合成、网络传输和客户端缓冲混在一起。
| 事件 | 定义 |
|---|---|
user_speech_start |
客户端检测到本轮相关话语开始 |
user_speech_end |
客户端观察到本轮最后一个语音帧 |
turn_committed |
编排器接受稳定轮次并允许处理 |
asr_final |
ASR Provider 把转写标为 Final |
first_model_token |
模型输出首个 Token,不保证它可用 |
first_accepted_unit |
编排器接受首个可进入语音合成的语义单元 |
first_tts_byte |
TTS 输出首批音频字节 |
first_playable_audio |
客户端已经拥有足够的解码音频 |
playback_started |
输出设备开始播放有效回答 |
playback_stopped |
用户打断后,旧 Epoch 不再可听 |
常用区间可以定义为:
endpointing = turn_committed - user_speech_end
decision = first_accepted_unit - turn_committed
synthesis_and_delivery = first_playable_audio - first_accepted_unit
perceived_response = playback_started - user_speech_end
barge_in_stop = playback_stopped - interruption_speech_start
不要把某组固定毫秒数字写成行业标准。先锁定语言、口音、任务、Codec、设备、网络、并发、模型修订版和工具路径,再按切片报告分位数。把宽带环境中的寒暄与弱网下的支付流程混在一个 p95 中,无法形成可执行的 SLO。
短确认语也不能伪造速度。“我帮你查一下”可以减少慢 Tool 的等待感,但只有在动作确实被允许时才能播放,而且不能把它计作“首个有效答案”。
参考架构与时钟边界
Voice Gateway 负责媒体接入,Orchestrator 负责 Turn Commit、鉴权、Epoch、取消和 Trace 关联。客户端同时观察采集与播放,因此最适合提供用户体感时间的第一方证据。
同一进程内用 Monotonic Clock 记录 Duration。跨浏览器、Gateway、模型 Provider 和 Tool Service 时,应保留每个事件所属的 Clock Domain 与 Correlation ID。Wall Clock 可以辅助对齐 Trace,但只有记录了时钟同步方式和误差上界,才能跨机器计算区间。
端到端体感响应与打断停止时间,优先使用客户端 Monotonic Timeline 上的两个事件。服务端 Span 用于解释时间消耗,不应伪装成同一只时钟。
ASR 假设与轮次提交
流式语音识别会随着新音频修订 Hypothesis。Partial 可以预热缓存、检索候选文档或启动可取消解码,但不能直接进入持久历史,更不能触发不可逆动作。
type Hypothesis = {
revision: number;
text: string;
audioEndMs: number;
providerFinal: boolean;
};
type UserTurn =
| { state: "candidate"; hypothesis: Hypothesis }
| {
state: "committed";
turnId: string;
transcript: string;
audioStartMs: number;
audioEndMs: number;
};
function mayStartWork(kind: "retrieval" | "tool_effect", turn: UserTurn): boolean {
if (kind === "retrieval") return true; // 结果在 Commit 前始终是推测性的。
return turn.state === "committed"; // 后续仍然必须做鉴权。
}
Provider 的 Final 不一定等于应用的 Committed。应用可能等待 Endpointing 证据、合并用户纠正、等待 Push-to-talk 松开,或拒绝空白和低置信片段。反过来,手动控制也可能先提交音频边界,再等待最终 Transcript。
OpenAI Realtime VAD 文档当前描述了 Server VAD、Semantic VAD 和 Speech Start/Stop 事件。这是厂商当前 API,不是通用 Turn Protocol。LiveKit Turn Management也明确区分 VAD、Endpointing、Semantic Detection、Manual Clear、Commit 与 Interrupt。工程上可以用这些 API 实现自己的契约,但不能让厂商事件替代应用契约。
轮次管理不等于语音活动检测
VAD 只能回答是否出现类似语音的声音,不能回答谁在说、对谁说,以及是否要取得话权。把每次 Voice Activity 都视为打断,会在家庭、车载、办公室和电话环境中产生大量误截断。
至少要分别测试四类 Overlap:
| 重叠类型 | 常见意图 | 期望行为 |
|---|---|---|
| 用户打断 | 纠正、停止或替换当前请求 | 快速让出话权并处理新提交轮次 |
| 附和 | “嗯”“对”但不抢话 | 通常继续原回答,不重启轮次 |
| 对他人说话 | 用户在和身边的人交流 | 保持输出,必要时简短确认 Addressee |
| 背景人声 | 远场第三人或媒体声音 | 忽略且不污染对话状态 |
Full-Duplex-Bench v1.5把这四类场景作为受控测试,并将 Stop Latency 定义为用户开始说话到模型停止,将 Response Latency 定义为用户结束说话到模型重新开始。论文中的具体模型结果只适用于其音频资产和实验协议;可复用的是按场景判断正确行为,而不是追求一个统一的“打断率”。
Overlap Classifier 可以组合声学方向、设备上下文、词义线索、时长、对话状态和显式按钮,但仍会误判。因此需要 Repair Path:错误 Duck 时尽量恢复原音频,Addressee 置信度低时简短澄清,且不能根据声音特征推断已认证身份。
Generation Epoch 与播放事实
每次 Assistant Response 都应分配不可变的 Generation Epoch,使各层能够拒绝过期输出。用户提交新轮次、策略变化或打断使旧回答失效时,都应递增 Epoch。
每个文本或音频单元至少携带:
{
"sessionId": "sess_123",
"turnId": "turn_009",
"generationEpoch": 18,
"unitId": "unit_004",
"text": "您的订单计划在",
"audioStartSample": 38400,
"audioEndSample": 62400
}
模型可能已生成十个 Unit,TTS 合成了七个,服务端发送了五个,而客户端只播放了三个。若把完整生成文本当作用户已听内容写回历史,下一轮上下文就会失真。持久对话应根据 Playback Progress 或最终 Playback Ack 更新,而不是根据 Generation Complete 更新。
如果 Ack 丢失,应把 Heard Boundary 标为不确定,不能根据服务端 Send Time 编造精确文本前缀。在完成对账前,下一轮应避免“正如我刚才所说”这类依赖播放事实的表达。
面向客户端播放的打断协议
可靠 Barge-in 需要协调检测、分类、取消、客户端播放和状态对账。仅清空服务端 TTS Queue 不够,因为音频可能已经在网络途中、已解码或正在扬声器播放。
from dataclasses import dataclass
from typing import Awaitable, Callable, Optional
@dataclass(frozen=True)
class PlaybackAck:
epoch: int
last_played_unit_id: Optional[str]
class VoiceSession:
def __init__(
self,
cancel_model: Callable[[int], Awaitable[None]],
cancel_tts: Callable[[int], Awaitable[None]],
flush_client: Callable[[int], Awaitable[PlaybackAck]],
) -> None:
self.epoch = 0
self.cancel_model = cancel_model
self.cancel_tts = cancel_tts
self.flush_client = flush_client
self.committed_transcript: list[dict[str, str]] = []
async def interrupt(self, overlap_class: str) -> Optional[PlaybackAck]:
if overlap_class != "user_interruption":
return None
invalid_epoch = self.epoch
self.epoch += 1 # 此后必须拒绝 invalid_epoch 的迟到 Chunk。
await self.cancel_model(invalid_epoch)
await self.cancel_tts(invalid_epoch)
ack = await self.flush_client(invalid_epoch)
# 通过当前 Epoch 的不可变 Text-Audio Manifest 解析 unit_id。
if ack.last_played_unit_id is not None:
self.committed_transcript.append({
"role": "assistant",
"heardThrough": ack.last_played_unit_id,
})
return ack
客户端必须拒绝 Epoch 已失效的媒体 Chunk。完整流程是:
- 检测与 Agent 输出重叠的用户语音;
- 分类为打断、附和、对他人说话或背景语音;
- 使当前 Generation Epoch 失效;
- 协作式取消模型流与 TTS;
- 携带失效 Epoch 向客户端发送 Flush;
- 停止或降低播放,并回传最后播放的 Unit 或 Sample Cursor;
- 只提交已确认的 Assistant Prefix;
- 继续收集可修订的用户 ASR Hypothesis;
- 提交新的 User Turn;
- 完成鉴权并启动新 Epoch。
每一步取消都要设置 Timeout。Timeout 只能说明状态进入 Degraded 或 Unknown,不能证明取消成功。
传输选择与 WebRTC 可观测性
传输应根据媒体环境选择,而不是根据“WebRTC 一定更快”之类口号。WebRTC 提供浏览器实时媒体 API、拥塞机制、Jitter Buffer、回声控制集成和 getStats();WebSocket 在应用完全控制两端时可能更简单;SIP 受电话 Codec 与基础设施约束;Native Mobile Stack 则有平台专属的音频路由。
WebRTC 1.0 Recommendation定义浏览器实时通信 API,但不定义 Voice Agent Pipeline 或业务 SLO。WebRTC Stats定义 RTP、丢包、Jitter、Candidate Pair 和 Audio Playout 等字段;当前文档仍是 Candidate Recommendation Draft,不能假定所有浏览器都暴露全部字段。
累计型指标应采样两次再计算区间增量:
async function sampleInboundAudio(peerConnection) {
const report = await peerConnection.getStats();
return [...report.values()]
.filter((stat) => stat.type === "inbound-rtp" && stat.kind === "audio")
.map((stat) => ({
id: stat.id,
timestamp: stat.timestamp,
packetsLost: stat.packetsLost,
jitter: stat.jitter
}));
}
实现时要 Feature Detect 并保留原始样本。网络指标用于解释 Packet Delivery,不能替代 Turn Commit、Accepted Semantic Unit、Flush Request、Playback Ack 或 Tool Outcome 等应用事件。
安全隐私与工具副作用
语音属于不可信输入,不是身份或授权。系统应从 Session 获取已认证 Principal,在可信代码中检查 Tenant 与 Object Ownership,验证精确 Tool Arguments,并对高影响动作执行与具体参数绑定的确认。
边界至少包括:
- ASR Partial 只能启动可撤销计算;
- Committed Text 可以提出 Tool Call,但不能自行授权;
- Tool Execution 使用 Idempotency Key 和显式 Outcome State;
- 确认语句必须说明精确动作、对象、金额和目的地;
- 打断不能悄悄改变已经确认动作的目标;
- Audio、Transcript、Trace 与 Replay 必须有同意、留存、脱敏和访问策略;
- 即使单独部署 Speaker Recognition,也不能替代 Session Authentication。
不要仅以“方便调试”为由长期保存原始音频。应按声明目的保留最小产物,限制 Replay 权限,并确保删除流程覆盖派生 Transcript、Cache、Index 与 Evaluation Dataset。
评测契约
发布门禁应同时复现声学体验与业务任务。tau-Voice把可验证任务完成、全双工交互和真实音频条件纳入同一评测。论文报告的模型分数只属于其任务和模拟协议;更长期的价值是这种联合评测结构。
workload:
languages: [en-US, zh-CN]
devices: [laptop-headset, mobile-speaker]
codecs: [opus, telephony-narrowband]
networkProfiles: [clean, jitter, loss, reconnect]
taskSlices: [information, retrieval, read-only-tool, state-changing-tool]
overlapScenarios: [interruption, backchannel, talking-to-others, background-speech]
timingEvents:
- user_speech_end
- turn_committed
- first_accepted_unit
- first_playable_audio
- playback_started
- interruption_speech_start
- playback_stopped
releaseMetrics:
- task_completion
- tool_argument_accuracy
- policy_adherence
- false_cutoff_rate
- false_interruption_rate
- recovery_success
- stale_audio_after_cancel
- perceived_response_p50_p95_p99_by_slice
- cost_per_successful_turn
CI 中使用确定性 Audio Fixture,发布前再运行抽样端到端 Session。结果中必须记录 Model、Prompt、Endpointing、Codec 和 Client Revision;有条件时对候选方案做 Paired Trial。Policy Adherence 或 Task Completion 回归时,不能用延迟改善抵消。
架构选型
级联架构与 Audio-native Model 暴露不同控制面,但都不能取代应用层契约。
| 维度 | ASR 加 LLM 加 TTS 级联 | Audio-native Model |
|---|---|---|
| 中间文本 | 显式,便于检查 | 可能缺失或只是辅助信号 |
| 组件替换 | 可独立选择 ASR、模型和 TTS | 通常与模型服务耦合 |
| 声学上下文 | 可能在转写边界丢失 | 可继续供模型使用 |
| 取消 | 需要协调多个 Queue 和 Provider | 仍要协调模型、传输和播放 |
| 工具安全 | 必须由应用鉴权 | 必须由应用鉴权 |
| 评测 | 组件指标加端到端任务 | 端到端任务加声学行为 |
必须用同一 Workload 比较。不能断言级联一定更可控,也不能断言原生语音一定更快、更自然。Provider 实现、模型版本、Prompt、语言、网络、客户端缓冲和任务策略都可能反转结论。
最佳实践
- 先命名事件,再设置 SLO。 没有起止语义的指标无法定位回归。
- 严格分离 Hypothesis 与 Committed State。 只有可低成本丢弃时才允许推测执行。
- 每个输出单元携带 Epoch。 模型、TTS、Gateway 与客户端都拒绝迟到数据。
- 记录 Heard Prefix。 Generated、Synthesized、Sent 与 Played 是不同状态。
- 先分类 Overlap,再让出话权。 分别优化打断、附和、旁话和背景语音。
- Duration 使用 Monotonic Clock。 关联跨服务事件时保留时钟边界。
- 模型之外完成副作用鉴权。 声音与 Transcript 都不能授予权限。
- 同时门禁任务与交互质量。 按真实 Workload Slice 报告分布,不使用混合平均值掩盖问题。
常见问题
生产级语音 Agent 应该达到多少延迟?
不存在通用数字。先定义 user_speech_end、turn_committed、first_accepted_unit、first_playable_audio 和 playback_started,再在代表性设备、语言、网络与任务上测量区间。报告 p50、p95、p99,同时观察任务成功率和重问率。快速播放一句无用占位语不等于快速完成回答。
ASR Partial 可以直接触发工具调用吗?
它可以准备检索或缓存查询等可撤销工作,但不能进入持久历史或造成外部副作用,因为后续音频可能改写它。Turn Commit 之后,可信代码还必须检查身份、对象访问、参数、策略和必要确认。
可靠打断处理需要做什么?
系统要使旧 Epoch 失效、取消模型和 TTS、停止客户端已经缓冲的音频,并记录用户实际听到的 Assistant Prefix。随后等待新 User Turn 稳定,再授权下一次回答。只清服务端 Queue 会遗漏在途和客户端缓冲数据。
应该选择级联还是原生语音模型?
应使用同一契约实测。级联架构暴露 Transcript 和独立组件,原生语音模型可能保留声学与韵律上下文。两者都需要播放感知取消、Tool Authorization、Trace、隐私控制和端到端评测;架构名称本身不能证明速度或质量。
全双工行为应该如何测试?
分别测试真实打断、附和、对他人说话和背景语音。检查 Agent 是否正确 Respond 或 Resume、需要停止时音频多久真正不可听、能否恢复,以及取消后是否仍有旧音频或 Tool Effect 泄漏。最后把这些结果与 Task Completion 和 Policy Adherence 一起门禁。
总结
可靠的语音 Agent 延迟工程始于状态语义,而不是固定毫秒预算。显式提交用户轮次,用 Generation Epoch 隔离回答,根据客户端 Playback 对账历史,并把 Barge-in 实现为分布式取消协议。在可比较时钟上测量体感延迟,分别测试各类 Overlap,只有对话质量、任务正确性、策略遵循与恢复能力同时通过时才发布。