TECHNICAL ARTICLE

语音 Agent 的双工,不等于原生双工

结合最近一个项目的实时语音实践,拆解 WebSocket 全双工、ASR / Agent / TTS 级联、VAD 与 Barge-in,并说明它和原生 Audio-to-Audio 双工模型的真正区别。

最近在一个项目里设计实时语音对话能力时,我们遇到了一个看似简单、实际很容易说不清楚的问题:

使用 ASR、Agent 和 TTS 级联出来的语音对话,到底算不算双工?它和模型原生的语音双工又有什么区别?

如果只看产品体验,用户可以在 Agent 说话时继续开口,系统能立刻停止播放、取消当前生成并开始听新的问题,这当然已经具备双工对话最重要的特征。

但如果只看模型链路,声音先被识别成文字,文字交给 Agent 推理,回答再经过 TTS 变成声音,它又明显不是端到端的 Audio-to-Audio。

这两个判断并不矛盾。更准确的结论是:

这套设计采用的是级联式应用层全双工语音架构,而不是原生端到端语音双工模型。

“全双工”和“原生双工”描述的不是同一个层面。前者主要回答输入与输出能不能同时发生、用户能不能自然打断;后者回答模型是否直接理解和生成音频。

先把“双工”拆成四个层次

很多语音产品说自己支持双工,但“双工”至少可能指四件不同的事。

层次关注的问题当前项目的实现
传输层上行与下行数据能否同时传输浏览器与后端使用全双工 WebSocket
流式处理层ASR、Agent、TTS 能否边输入边输出ASR 流式转写,Agent 流式生成,TTS 流式返回 PCM
交互层Agent 播放时用户能否继续说话和打断持续录音 + Local VAD + Barge-in
模型层模型是否直接消费和生成音频否,仍然是 ASR → Agent → TTS 级联

因此,判断一个语音系统是不是双工,不能只看它是否使用名为 bidirection 的接口,也不能只看它是不是 Audio-to-Audio 模型。

一个系统完全可能在传输层和交互层实现真正的全双工,同时在模型层保持级联。这正是这个项目当前选择的路线。

整体架构:三条实时链路由 VoiceSession 编排

这套语音链路由浏览器、FastAPI 语音编排层和语音服务三部分组成。

Voice Agent 级联式全双工语音对话架构

麦克风 PCM 上行和 TTS PCM 下行可以并发;VoiceSession 负责状态、轮次、取消和 ASR / TTS 生命周期。

浏览器不是一个被动播放器

浏览器同时承担录音、预处理、本地语音检测和音频播放:

  • Web Audio 获取麦克风输入,并将设备原生采样率重采样为 PCM16 mono 16kHz;
  • 音频以 20ms 小帧持续发送,后端聚合成 200ms 数据块交给 ASR;
  • 下行 PCM16 24kHz 使用播放时间游标连续排程,减少片段之间的空隙和爆音;
  • 即使 Agent 正在播放回复,麦克风仍然保持采集;
  • Local VAD 检测到用户重新开口后,立即触发打断。

浏览器与 FastAPI 之间只需要一条 WebSocket。二进制帧承载上下行 PCM,JSON 承载 voice.start、状态变化、转写事件、Agent 文本增量和 playback.clear 等控制消息。

这条连接在传输意义上是真正的全双工:麦克风音频上行时,下行语音与控制消息仍然可以同时到达。

VoiceSession 是语音系统真正的控制中心

语音体验并不是把 ASR、LLM 和 TTS 三个 API 串起来就结束了。真正决定对话是否自然的,是中间的 VoiceSession。

它需要同时维护:

  • idlelisteningthinkingspeaking 等会话状态;
  • 当前轮次和 Turn Lock,避免两个回答交叉执行;
  • ASR 流、Agent task、TTS pump 和 TTS Session 的生命周期;
  • VAD 事件、partial transcript 与 idle commit;
  • 当前播放是否需要清空,以及哪些异步任务必须被取消;
  • 文字聊天和语音聊天共用的 session_id、消息时间线和用量记录。

换句话说,VoiceSession 不是一个简单的 WebSocket Handler,而是一套面向实时语音的任务编排器。

一次语音轮次是怎样流动的

一次完整的交互大致经过九步:

  1. 浏览器发送 voice.start,绑定当前 Agent 和会话。
  2. 麦克风持续产生 PCM16 16kHz、20ms 音频帧。
  3. VoiceSession 将音频聚合为 200ms 数据块,发送给流式 ASR。
  4. ASR 持续返回 partial 和 final transcript,页面同步显示转写过程。
  5. final transcript 被提交给 Agent Harness。
  6. LangGraph Agent 流式返回 text_delta,页面先显示文字。
  7. TTS pump 从文本流中切出完整句,清理 Markdown 后送入 TTS queue。
  8. TTS 流式返回 PCM16 24kHz 音频,浏览器边收边播。
  9. 如果用户在思考或播放阶段重新开口,Local VAD 发出 Speech.START,触发全链路打断。

这里有一个对延迟很重要的设计:Agent 与 TTS 不是前后串行等待,而是异步流水线。

Agent 还在生成后面的内容时,前面已经完成的句子就可以进入 TTS。这样不必等整个答案生成完才开始说话,又能在句子边界上做 Markdown 清理和更稳定的语音合成。

它不是极限低延迟方案,但在可控性、语义完整度和首音速度之间取得了更现实的平衡。

真正困难的不是“能听能说”,而是自然打断

很多语音 Demo 看起来已经能对话,但用户一旦在播放过程中插话,系统就会暴露问题:旧音频还在播放、上一轮 Agent 继续调用工具、新问题与旧答案混在一起,甚至 TTS 的残留片段会在下一轮突然出现。

所以,Barge-in 不能只做成前端的“停止播放”。一次可靠的打断至少要同时完成:

Speech.START
  → 停止并清空所有已排程的浏览器 AudioBufferSourceNode
  → 重置未来播放时间游标
  → 发送 playback.clear / interrupt
  → 取消当前 LLM task
  → 取消 TTS pump task
  → 关闭并废弃当前 TTS Session
  → 回到 listening

被取消的 TTS WebSocket 不应该继续复用。重新建立会话虽然增加一点连接成本,却能避免上一轮的 SessionCanceled、残留音频或协议状态污染下一轮。

ASR 也需要轮次边界。当前语句提交以后重建上游识别流,可以避免前一轮累积的转写上下文影响新的问题。

另外,播放期间持续录音会带来扬声器回灌。浏览器侧的回声消除、降噪和自动增益是基础,但 VAD 阈值、设备差异和外放音量仍然会直接影响误打断率。双工体验最终不是一个布尔值,而是一组需要持续评测的工程指标。

它与原生 Audio-to-Audio 双工有什么区别

原生双工语音模型通常直接接收连续音频并直接生成音频。声音不需要先完整压缩成文字语义,模型可以同时利用词义、停顿、语速、重音、情绪和其他声学信息。

级联式全双工与原生 Audio-to-Audio 双工架构对比

左侧级联架构保留 ASR、Agent、TTS 以及转写、工具和事件检查点;右侧原生架构用统一实时模型直接连接输入与输出音频。两者都能实现双工交互,但优化目标不同。

级联式全双工与原生端到端双工的主要区别可以概括为:

维度级联式应用层全双工原生 Audio-to-Audio 双工
核心链路Audio → ASR → Text Agent → TTS → AudioAudio → Realtime Model → Audio
中间语义有明确 transcript 和文本回答语义与音频状态更多保留在模型内部
延迟来源ASR endpoint、Agent 首 token、分句、TTS 首包实时模型直接处理音频,理论链路更短
语气理解主要依赖 ASR 可保留的信息可以直接利用停顿、语气和情绪
语音表现由 TTS 音色和文本控制更容易生成连续、自然的韵律响应
工具与工作流可直接复用成熟 Text Agent、LangGraph、工具和 HITL需要模型或外层 Runtime 提供可靠工具编排
可观测性每一层都有文本、事件和明确状态内部决策更端到端,调试与归因更困难
审计与存档transcript 天然适合搜索、审计和持久化往往需要额外转写或结构化记录
供应商替换ASR、模型、TTS 可以分别替换能力更集中于实时模型供应商
故障定位可以判断问题出在识别、推理还是合成体验统一,但错误边界更模糊

原生双工的优势,是更短的感知—响应路径和更完整的声学上下文。它更接近人与人交谈:用户没有说完时,模型已经开始理解意图;语气变化、犹豫和情绪也可以成为输入;输出声音不必等到完整文本句子确定后才合成。

级联架构的优势,则是可控、可替换、可观测,而且能够复用已经成熟的 Agent 工程体系。对于需要工具调用、权限控制、Human-in-the-loop、会话审计和结构化产物的企业 Agent,这些能力往往比少几百毫秒更重要。

所以“原生”不天然等于“更适合所有 Agent”。它是另一组产品取舍。

WebSocket 全双工,也不等于 WebRTC

另一个容易混在一起的概念,是实时语音传输。

当前架构使用 WebSocket 传输原始 PCM。它实现简单,JSON 控制事件和二进制音频可以复用一条连接,也很适合普通 Web 应用与后端服务编排。

但它不是完整的实时媒体栈。WebRTC 还包含 RTP、DTLS、抖动缓冲、网络拥塞控制、带宽自适应、Opus 编码和更成熟的回声处理能力。

因此:

  • 在网络稳定的浏览器语音助手场景里,WebSocket + PCM 足够直接;
  • 在移动网络、跨地域、长时间通话和强弱网环境里,WebRTC 的媒体能力更有优势;
  • 是否使用 WebRTC,与模型是不是 Audio-to-Audio 是两个独立选择。

一个系统可以使用 WebRTC 传输音频,但后端仍然是 ASR → Agent → TTS;也可以通过 WebSocket 连接原生实时语音模型。不要把媒体协议和模型架构混为一谈。

为什么当前阶段选择级联式全双工

在这个项目里,语音不是一个独立的聊天产品,而是现有 Agent 工作能力的另一个入口。

用户通过文字或语音发起请求,背后使用的是同一个 Agent、同一个 session_id、同一套 LangGraph Harness、工具系统、会话历史和权限边界。这意味着语音模式不能为了追求“像真人”而丢掉 Agent 已有的工作能力。

级联架构让这件事变得自然:

  • ASR 把语音转换成 Agent 已经理解的输入形式;
  • Agent 仍然可以调用工具、进入工作流、触发中断和人工确认;
  • 文本增量同时用于 UI、持久化和 TTS;
  • TTS 只负责表达,不接管任务逻辑;
  • 每个阶段都能被取消、重试、监控和替换。

从工程角度看,这不是退而求其次,而是先守住 Agent 的可靠性,再通过应用层编排获得接近自然对话的实时体验。

后续应该怎么继续演进

这套架构还可以沿几个方向继续优化。

第一是从完整句 TTS 逐步走向更细粒度的稳定语义片段。目标不是盲目逐字符发送,而是在不破坏语义和 Markdown 清理的前提下降低首音延迟。

第二是建立真正的语音 Eval,而不是只测接口是否连通。至少应该持续记录:

  • 首个 partial transcript 延迟;
  • 用户停顿到 final transcript 的 endpoint latency;
  • Agent 首 token 与 TTS 首音延迟;
  • 打断信号到音频完全停止的延迟;
  • 误打断率与漏打断率;
  • 播放队列 underrun、爆音和片段间空隙;
  • ASR 错误对工具调用与任务结果的影响。

第三是根据场景引入 WebRTC、语义 VAD 和更完善的回声处理,让弱网和外放环境更加稳定。

第四是保留混合路线。闲聊、陪伴和强情绪表达可以交给原生 Audio-to-Audio;工具密集、需要审计和结构化交付的工作任务仍走级联 Agent。两条链路共享会话与权限,而不是强迫所有语音需求使用同一种模型。

双工是一种系统能力,不是一个模型标签

语音 Agent 是否自然,不由某一个 ASR、TTS 或实时模型单独决定。

它取决于音频是否持续采集、上下行是否真正并发、状态是否清晰、任务能否及时取消、播放能否瞬间清空、回声是否被抑制、轮次是否会互相污染,以及 Agent 的工具和上下文能否完整保留。

当前设计不是原生端到端语音模型,但它已经在传输层、处理层和交互层实现了真实的双工能力。

更重要的是,它没有为了语音重新发明一套 Agent,而是让语音进入现有的 Agent Harness:

声音是新的交互通道,Agent 仍然是工作的执行主体,VoiceSession 负责把实时对话变成一条可控、可打断、可观测的执行链路。

这可能比简单地接入一个“原生双工模型”,更接近企业语音 Agent 真正需要解决的问题。


延伸阅读:上下文管理才是 Agent 时代真正的 Skill · AI Agent 工程:从 Demo 到可靠系统