NEE's Blog

从零构建亚 500ms 的 SOTA 语音 Agent

March 02, 2026

本文翻译自 Building sub-500ms SOTA voice agents from scratch,原载于 Hacker News。


过去六个月,我一直在一家创业公司工作,为全球最大的消费品公司之一构建 Agent 原型。作为这项工作的一部分,我们的团队依赖现成的语音 Agent 平台来帮助公司更高效地运营。虽然我无法透露商业细节,但技术结论非常明确:语音 Agent 功能强大,而且有像 Vapi 和 ElevenLabs 这样优秀的开箱即用抽象,让搭建语音 Agent 变得轻而易举。

但是:这些抽象也隐藏了相当多的复杂性。

就在我开始写这篇文章的几天前,ElevenLabs 刚刚完成了该领域最大规模的融资轮之一,GPT-5.3 和 Claude 4.6 等新一代前沿模型也相继发布。这让我思考:我真的能自己构建语音 Agent 的编排层吗? 不仅仅是一个玩具实验,而是能够达到像 Vapi 这样一体化平台相近性能的东西?

令我惊讶的是,我可以。这大约花了一天时间和约 100 美元的 API 额度——结果是 端到端响应时间达到 ~400ms,延迟性能比 Vapi 的同类配置快 2 倍

为什么语音 Agent 很难

与文本 Agent 相比,语音 Agent 是一个巨大的复杂性跃升。

文本 Agent 相对简单,因为用户的行动协调着一切。模型生成文本,用户阅读、输入回复,然后点击”发送”。这个行动定义了轮次边界。在用户明确推进流程之前,不需要发生任何事情。

语音不是这样工作的。编排是持续的、实时的,必须同时小心管理多个模型。在任何时刻,系统都必须决定:用户是在说话,还是在听? 而这两种状态之间的转换正是所有困难的所在。

当用户开始说话时,Agent 必须立即停止说话——取消生成、取消语音合成、刷新所有缓冲的音频。当用户停止说话时,系统必须确信他们已经说完,然后以最小的延迟开始响应。任何一个做错了,对话就会感觉有问题。

这不仅仅是测量音量那么简单。人类语言包括停顿、犹豫、填充音、背景噪音,以及不应该打断 Agent 的非语言确认。这些问题的下游表现是每个人都能注意到的:端到端延迟、尴尬的沉默、Agent 打断你,或者跟你说话时重叠。

我们从潜意识层面判断语音通信的质量,因为它深深地根植于我们的本质。在文本中可以接受的小时间错误——这里的停顿,那里的延迟——在语音中立即感觉不对劲。

在实践中,一个好的语音 Agent 不是关于任何一个单独的模型。它是一个编排问题。你把多个组件串在一起,体验的质量几乎完全取决于这些部分在时间上如何协调。

一体化 SDK 的问题是,你会得到一长串要调整的参数,而不真正理解哪些重要或为什么。当感觉不对时,很难知道问题出在哪里。这就是促使我深入一层自己构建核心循环的原因。

出发点:轮流对话循环

在写任何代码之前,我花时间在编辑器之外与 ChatGPT 一起迭代架构。我发现在不熟悉的领域工作时这很有用:先建立心智模型,然后实现。

我对 Agent 编码的目标总是相同的。我想要足够好地理解我正在构建的结构,以至于我可以打开任何文件并立即看到它为什么存在以及它如何融入系统。

经过几次迭代,我把整个问题简化为单个循环和一个微型状态机。核心上,语音 Agent 只需要回答一个问题:用户是在说话,还是在听?

有两个状态:

  • 用户正在说话
  • 用户正在听

以及两个发生一切的转换:

  • 当用户开始说话时,我们必须立即停止所有 Agent 音频和生成
  • 当用户停止说话时,我们必须以尽可能小的延迟开始生成和流式传输 Agent 响应

这个轮流检测逻辑是每个语音系统的核心,所以我决定从那里开始。

第一轮:VAD 和预录响应

对于第一个实现,我故意避免了转录、语言模型和文本转语音。我想要仍然在方向上像语音 Agent 的最简单的检查点。

设置是最小的。一个小型 FastAPI 服务器处理来自 Twilio 的传入 WebSocket 连接,它以 ~20ms 帧流式传输 base64 编码的 μ-law 音频数据包。每个数据包被解码并输入到语音活动检测 (Voice Activity Detection, VAD) 模型——在我的情况下是 Silero VAD。

Silero 是一个微型开源模型(约 2MB),可以快速确定短音频块是否包含语音。轮流对话是一个比语音检测难得多的任务,但 VAD 仍然是一个有用的原语,特别是用于决定音频是否应该转发到更昂贵的下游系统。

在此基础上,我构建了一个简单的状态机:一个布尔标志,表示用户当前是在说话还是在听。当系统检测到语音结束时,它向呼叫者播放预录的 WAV 文件。当语音恢复时,它通过 Twilio WebSocket 发送 clear 信号,立即刷新所有缓冲的音频并停止播放。

我这样开始是为了隔离问题最难的部分——轮流检测——而不连接系统的其余部分。

结果虽然基础,但已经很令人印象深刻:当我停止说话时,Agent 立即响应;当我打断它时,它立即停止。即使没有转录或生成,循环在某种程度上 感觉 是对话式的。

这也给了我一个有用的延迟基线。使用急切的轮次结束和预录响应,该系统代表了语音 Agent 可能感觉到的延迟下限。

VAD-only 方法的局限

第一轮有价值,但它的局限性很明显。

检测语音的存在不等于知道用户何时完成了他们的想法。说话慢的人可能会在句子中间停顿几秒钟。纯 VAD 会急切地决定轮次已经结束,并过早开始说话。

在实践中,真正的轮流对话需要将低级音频信号与来自转录本身的高级语义线索结合起来。这意味着 VAD-only 方法无法扩展到真正的系统。

它确实给我的是干净的控制流模型和可靠的延迟基线来对比。有了这些,是时候连接完整的管道了。

第二轮:Flux 和真正的语音 Agent 管道

下一步是用为生产设计的东西替换我手工制作的轮流检测:Deepgram 的 Flux。

Flux 是一个流式 API,在单个模型中结合了转录和轮流检测。你给它输入连续的音频流,它发出事件——最重要的是,”轮次开始”和”轮次结束”,结尾包含最终转录。

这替换了我架构的核心。Flux 成为 Agent 何时应该说话以及何时应该立即停止倾听的事实来源。

在此基础上,我构建了一个专用的 Agent 轮次管道。当 Flux 发出用户轮次结束的信号时,这个管道启动一个实时序列:

  1. 转录和对话历史被发送到 LLM 开始生成
  2. 第一个 token 一到达,它就通过 WebSocket 流式传输到文本转语音服务
  3. TTS 产生的每个音频数据包都直接转发到出站的 Twilio socket

核心思想是将每个流管道化以最大程度地减少延迟。

一个重要的细节是保持文本转语音连接的温暖。 建立到 ElevenLabs 的新鲜 WebSocket 会增加几百毫秒的延迟,所以我保持了一小池预连接的 socket。仅此一项就将响应时间削减了大约 300ms。

打断是对称处理的。当 Flux 检测到用户开始说话时,Agent 管道立即被取消:飞行中的 LLM 生成被停止,TTS 被拆除,clear 消息被发送到 Twilio 以刷新任何排队的音频。Agent 立即沉默,Flux 恢复监听下一个轮次结束。

本地运行

我的第一个测试是完全在本地运行编排,主要是为了理解地理放置如何影响延迟。我在土耳其南部的一个偏远木屋里构建了这个项目的大部分,一边旅行和徒步旅行,所以这个设置远非理想。

在服务器测量的平均端到端延迟约为 1.6 秒。根据 Twilio 的说法,他们的媒体边缘在上面增加了大约 ~100ms,使总感知延迟达到约 1.7s

这仍然与 Vapi 同类配置的 ~840ms 延迟相去甚远——慢了两倍多。在那个时候,延迟变得明显。对话开始感觉犹豫。停顿拉伸得刚好感觉尴尬。

这是一个有用的提醒:即使有正确的架构,地理也很重要。

部署到生产环境

在我们的架构中,每包音频都在三个外部服务之间跳跃。如果你想最小化延迟,编排层需要在物理上靠近它们。

为了进一步改善延迟,我在 Railway 的 EU 区域部署了系统,并配置了 Twilio、Deepgram 和 ElevenLabs 也使用它们的 EU 部署(注意:ElevenLabs 默认自动选择最近的区域)。

差异是立竿见影的:

在服务器测量的平均延迟下降到 ~690ms,这意味着总端到端延迟约为 ~790ms(包括 Twilio 的边缘)——提升了 2 倍以上!

作为比较,Vapi 中的等效配置——使用相同的 STT、LLM 和 TTS 模型——估计约为 ~840ms。在这个设置中,自定义编排实际上比 Vapi 自己的估计快了 大约 50ms

更重要的是,主观差异是明显的。对话感觉响应迅速。打断工作干净利落。Agent 不再感觉在每个回复之前都在犹豫。

模型选择

到目前为止,在这个项目中,我一直在使用 gpt-4o-mini,这似乎是 OpenAI 可用的最低延迟模型。然而,在深入挖掘之后,我发现 Groq 的 llama-3.3-70b 的推理延迟可能快达 3 倍。

我想亲自验证这一点,所以我在我的生产服务器上设置了一个小型测试工具。它在一系列模型中运行了 360 次聊天完成,在每个请求收到第一个 token 后立即取消。以下是结果:

正如你所看到的,Groq 的模型让 OpenAI 的一切都望尘莫及。据我所知,这是在不运行自己的推理基础设施的情况下可实现的最低延迟。这真的令人印象深刻——~80ms 比人类眨眼还快,人类眨眼通常被引用为约 100ms。

我用 Groq 的 llama-3.3-70b 换掉了 gpt-4o-mini,结果真的让我惊讶:

除了第一轮之外,对话感觉流畅和敏捷。平均端到端延迟徘徊在 ~400ms 左右,我很难跟上——回听录音时,听起来我比 Agent 花了更长的时间来回复。

在这个延迟下,打断处理也感觉明显更好。Agent 的声音几乎在我开始说话后立即停止,使交互感觉比以前经历的任何东西都更接近真正的对话。

技术要点总结

我真的很惊讶我可以通过一个完整的倍数击败现成的提供商。从在真正的生产用例中广泛使用 Vapi 和 ElevenLabs Agent SDK 的经验中,我发现我的初始原型能够可靠地实现 2 倍的延迟改进,这对于提供自然和愉快的语音 Agent 交互来说是一件大事。

从零构建语音 Agent 教会了我什么实际上对让 AI 语音对话感觉敏捷很重要:

延迟

用户体验为”响应性”的是从他们停止说话到听到 Agent 响应的第一个音节的时间。该路径通过轮流检测、转录、LLM 首 token 时间、文本转语音合成、出站音频缓冲以及它们之间的所有网络跳跃。你通过识别哪些阶段位于关键路径上并确保没有任何东西不必要地阻塞来优化这一点。

模型选择和 TTFT(首 Token 时间)

在语音系统中,接收第一个 LLM token 是整个管道可以开始移动的时刻。TTFT 占总延迟的一半以上,因此选择像 Groq 这样的延迟优化推理设置产生了最大的差异。模型大小似乎也很重要:更大的模型可能对于一些复杂的用例是必需的,但它们也施加了在对话环境中非常明显的延迟成本。正确的模型取决于工作,但 TTFT 是真正重要的指标。

管道化 Agent 轮次

生产语音 Agent 不能作为 STT → LLM → TTS 三个顺序步骤构建。Agent 轮次必须是一个流式管道:LLM token 一到达就流入 TTS,音频帧立即流向电话。目标是永远不要不必要地阻塞生成。任何在继续之前等待完整响应的东西都是在浪费时间。

取消飞行中的调用

打断处理必须立即传播到 Agent 轮次的所有部分。当用户开始说话时,系统必须同时取消 LLM 生成、拆除 TTS 并刷新任何缓冲的出站音频。缺少任何一个都会让打断感觉有问题。

地理是一等设计参数

一旦你编排多个外部服务——电话、STT、TTS、LLM——放置就会主导一切。如果这些服务不在同一位置,延迟会迅速累积。移动编排层并使用正确的区域端点将端到端延迟削减了一半。服务放置产生巨大差异。

综合起来,这些教训解释了为什么语音感觉欺骗性地难。实时系统是不宽容的,人类对时间错误极其敏感。

现成 vs 定制

这不是反对像 Vapi 或 ElevenLabs 这样的平台的论点。这些系统提供的远不止编排:API、可观察性、可靠性和深度配置选项,这些都需要真正的努力来复制。对于大多数团队来说,重建所有这些将是一个错误——能够测试和验证语音 Agent 应用而不达到这种技术深度真的很棒,这就是我最初对这项技术感到兴奋的原因。

但自己构建语音 Agent——即使是精简版——仍然是一个值得的练习。它强迫你理解参数实际上控制什么、为什么存在某些默认值以及真正的瓶颈在哪里。这种理解使你更擅长配置现成的平台,在某些情况下,当你的用例需要时,让你构建更定制的东西。

语音是一个编排问题。一旦你看清循环,它就变成了一个可解决的工程问题。

comments powered by Disqus