
最近在做语音客服机器人的时候我彻底把语音识别、大模型对话、语音合成、业务系统串在一起的方案推翻重做了一遍。用户的需求本身不复杂接电话、听“我这个月话费多少”“我想改预约时间”之类的话然后查系统、播报结果。真正让我头疼的是场景一换就崩语速快一点被拦腰截断用户插话时系统还在自说自话并发两路通话时上下文直接串线。后来我改用 Elastic Agent Builder 来构建语音 agents把会话流程交给它统一编排才从“能跑”变成“能稳定跑”。这篇文章想把我这段时间在 Elastic Agent Builder 上构建语音 agents 的过程完整整理出来包括为什么语音 Agent 不能简单理解成“语音识别大模型”、Agent Builder 到底帮我解决了什么、实际创建 Agent 时的参数怎么配以及上线后最容易翻车的几个问题和排查思路。适合正在做语音客服、电话外呼、会议室助手这类项目并且想找一个相对可靠的编排层的人参考。如果你已经熟悉大模型对话接口这篇文章可以直接当作一个操作手册来看。1. 先把“语音 Agent”拆开它并不只是“语音识别 大模型”1.1 一条语音链路至少有四段少一段都跑不稳一个语音 Agent 从用户开口到听到回复实际链路比大多数人想的长得多。至少包含四段音频采集与 VAD判断用户有没有在说话什么时候算是说完了一句话。ASR 语音识别把音频变成文本。这里要特别注意区分“中间结果”和“最终结果”还要带标点带一定的时间戳能力。Agent 决策把文本交给一个会调用工具的大模型让它结合上下文给出回答或者触发某个业务动作。TTS 语音合成把回复文本变成语音播放出去同时要支持用户随时打断。踩过坑的人都知道最麻烦的从来不是某个单独的模型效果不好而是这四段之间怎么配合。比如 VAD 觉得用户说完了但 ASR 只识别出一半ASR 给出了最终结果但 Agent 工具调用还在等业务系统返回这时 TTS 要不要先播一句“请稍等”用户中途打断TTS 到底停还是不停停了之后 Agent 的上下文怎么接着写。这些都不是单个模型能解决的它们属于会话编排问题。我一直用一句话类比语音 Agent 不是“一个很聪明的 AI”而是一个“前台接线员业务专员话术播报机”的团队。前台负责听和说业务专员负责查和算话术播报机负责把结果讲出来。这个团队能不能高效运转看的是协作流程而不是其中某个人多聪明。1.2 Agent Builder 解决的是“编排”而不是“模型”很多人第一反应是我用代码自己写个循环也能把这些环节串起来。确实能小 Demo 跑起来很容易但真实场景会越写越乱。原因在于语音场景是异步的、多路的。用户可能在任意时刻插话工具调用结果可能在 TTS 正在播放时到达同一个进程里可能同时挂着多通电话每通电话的上下文都不同。用硬编码的方式管理这些状态最后往往会陷入“改一个切换逻辑坏了另一个切换逻辑”的循环。Elastic Agent Builder 这类工具解决的核心问题就是编排。它把 VAD、ASR、Agent 决策、工具调用、TTS 这些环节抽象成可配置的节点让我们统一管理会话状态每个 session 独立的上下文、槽位、状态机。工具注册不用写胶水代码直接配置工具描述和参数格式。运行时策略超时、重试、兜底转人工都能在配置层面解决。全链路日志每个事件都有记录方便排查“这句话到底哪里断了”。要特别强调一点Agent Builder 本身不是 ASR也不是 TTS它只是一个编排层。你需要自己接入语音识别和语音合成服务Builder 只负责按流程调度它们。这反而是一个优势因为 ASR 和 TTS 的选型可以随时换不影响 Agent 核心逻辑。1.3 什么场景适合用 Agent Builder 做语音 Agent以我自己的经验下面几类场景很适合电话客服机器人查账单、查订单、改预约、报政策业务是固定的工具调用明确。智能外呼回访、提醒、邀约话术相对标准状态管理简单。企业内线语音助手帮员工查会议室、找人、发起审批靠工具链解决问题。语音填单/语音笔记用户口述内容Agent 整理成结构化表单再朗读确认。不适合的场景也有。比如要求端到端延迟低于 300 毫秒的实时同传或者必须完全离线、音频不出内网的高保密场景。前者建议直接做底层音频流处理不要在通用编排层里绕后者要先确认 ASR/TTS 模型是否支持纯本地部署编排层是否能跑在内网环境否则数据链路会有问题。2. 设计阶段语音 Agent 需要想清楚的三件事2.1 会话状态别把上下文只放在“用户说的话”里做语音 Agent 最容易犯的错误是把上下文简单理解成“历史对话文本”。真正跑起来才知道除了对话历史还有大量会话状态需要维护用户当前在哪个流程阶段、已经提供了哪些信息、缺哪个槽位、当前是否在等待工具返回、TTS 是否正在播放。我习惯给语音会话定义一个状态集合类似这样IDLE空闲等待用户开口。LISTENING正在听用户说话。ASR_WAIT等待语音识别返回最终结果。THINKINGAgent 正在生成回复或调用工具。TOOL_WAIT已经发起工具调用等待业务系统返回。SPEAKINGTTS 正在播报。TRANSFER准备转人工。END会话结束。为什么要显式定义状态因为语音场景里很多问题其实是“状态不一致”。比如 TTS 还在播放ASR 同时识别到了用户的声音如果系统不知道当前正处于 SPEAKING 状态它可能把用户的话当成背景噪音或者反过来把自己播放的声音当成用户指令。有了状态机Builder 就可以在收到音频事件时判断“当前是否允许打断”而不是每次都凭感觉处理。在 Elastic Agent Builder 里每个会话都会分配一个 session_id所有状态都必须挂在这个会话维度下绝对不能用全局变量存。这一点听起来简单但并发一上来最容易翻车的就是这里。后面我会专门讲并发串线的排查。2.2 工具注册先想清楚用户会用什么样的“口语”触发动作语音用户不会说“调用查账单工具”他们只会说“我这个月话费多少”“帮我看看上次扣了多少”。所以工具设计不能按照后端接口的习惯来写要站在自然语言的角度去写。我在一个模拟客服项目里注册了三个工具查账单参数为 user_id、账期查询用户某段时间的费用明细。改预约参数为 user_id、原预约时间、新预约时间修改线下服务预约。转人工参数为原因说明把当前会话转给人工坐席。在 Agent Builder 里配置工具时要把每个工具的描述写得口语化但又要让模型容易触发。例如“查账单”的描述可以写成当用户询问费用、话费、消费、扣款时使用。参数尽量精简少用嵌套结构否则模型容易填错。另一个关键策略是工具参数不足时让 Agent 主动反问而且一次只问最关键的一个。语音场景和文字聊天不一样用户没法在屏幕上看到表单系统也不能一次抛三个问题让用户选。如果 Agent 同时问“您想查哪个账期账户后四位是什么需要短信还是语音播报”用户大概率会懵。所以 prompt 里一定要写清楚缺参数时只追问一个最关键的信息。工具数量也有讲究。语音 Agent 的工具最好控制在五个以内超过之后模型路由的准确率会明显下降。业务工具特别多时建议拆成多个 Agent或者做一层意图路由先判断大类再进入子流程。2.3 事件循环让音频流和 Agent 循环对齐这是我从纯代码方案切到 Agent Builder 的最核心原因。大模型对话接口通常是一个请求-响应循环发一段文本进去得到一个文本回复。但语音流是一个持续产生的事件流两者天然有个对齐问题。如果直接把 ASR 文本丢进大模型问题会很多用户话说完了但 ASR 还在出中间结果Agent 回复还没生成完用户已经又开口了TTS 播放结束但 Agent 还没拿到下一轮 ASR 结果。我现在的处理方式是在语音流和 Agent 主循环之间加一层“语音桥”把音频事件转换成 Agent 可以消费的消息事件。整个会话的时序大致是VAD_STOP 事件检测到用户说完 - ASR_FINAL 事件识别最终文本 - AGENT_REQUEST携带 session_id 和文本进入 Agent - AGENT_RESPONSE 或 TOOL_CALLAgent 给出结果或发起工具调用 - TTS_QUEUE把回复文本交给 TTS 排队 - TTS_START - TTS_STOP。同时如果在 SPEAKING 过程中检测到用户又开口会产生 BARGE_IN 事件系统立刻停止 TTS 并重新进入 LISTENING。在 Elastic Agent Builder 里每一步都可以配置回调或 Webhook。这样我可以在工具调用返回后再决定是继续播报还是回到追问流程。最重要的是每一条消息都带上 session_id 和 event_id出问题时能完整还原时间线。3. 实操在 Elastic Agent Builder 里构建一个可跑通的语音 Agent3.1 第一步准备语音服务和模型选型别只看单项指标Agent Builder 不内置 ASR 和 TTS所以先把外部服务准备好。我的选型思路按关注点来排ASR必须支持流式识别能区分中间结果和最终结果中文识别要带标点。有没有热词功能很重要。做客服场景一定要把“查账单、改预约、转人工”这类词配进热词表否则用户说“改预约”容易被识别成“该约”或者“改约”。LLM优先选支持 function calling 的模型。语音场景对首字延迟更敏感所以选型时别只看总分要看 first token 时间。模型太聪明但响应慢用户体验反而不如一个稍小但快的模型。TTS必须支持流式合成和打断。千万不要选那种要等到整段音频全部生成完才返回的 TTS否则用户在 Agent 回复后的整整两秒内听不到任何声音第一反应就是“电话断了”。服务核心关注点我的推荐配置ASR流式识别、带标点、热词、中间/最终结果某云服务商流式接口热词表三个LLM支持 function calling、首字延迟低体量中等、对工具调用友好的模型temperature 0.2TTS流式合成、支持打断、中文音色稳定流式中文女声语速 1.0知识库如果要做 RAG建议单独配向量检索工具按业务文档拆分片段先不追求高精度这里说的外部服务都可以按自己环境替换。优先级是ASR 的稳定性大于准确率TTS 的流式能力大于音色LLM 的工具调用可靠性和延迟大于对话质量。3.2 第二步创建 Agent配置提示词和工具打开 Elastic Agent Builder 控制台新建一个 Agent。我的习惯是先把会话状态机搭出来再写 System Prompt最后注册工具。System Prompt 可以参考下面这一段你是电话客服助手。用户通过电话与你交流因此你必须遵守以下规则 1. 回复必须简洁、口语化最多两句话禁止使用 markdown、列表或符号。 2. 一次只追问一个关键信息不要连续追问多个问题。 3. 查询结果出来之后直接播报核心信息不要加上多余解释。 4. 如果工具调用失败或者用户表达强烈不满直接调用“转人工”工具。 5. 不要编造用户不存在的信息。这段 Prompt 里每一条都有针对性。第一点是让模型知道媒介是语音不是文字聊天第二点防止模型在语音场景里抛多选问题第三点是防止 TTS 播报一长串数字和解释第四点是兜底策略第五点是最基本的幻觉控制。接下来注册工具。在 Builder 里工具本质是一个 JSON Schema 描述。以“改预约”为例{ name: 改预约, description: 当用户要求修改预约时间时使用, parameters: { type: object, properties: { user_id: { type: string, description: 用户编号 }, original_time: { type: string, description: 原预约时间 }, new_time: { type: string, description: 新预约时间 } }, required: [user_id, original_time, new_time] } }然后把模型参数设成temperature 0.2max_tokens 200。temperature 要低语音场景容不下天马行空的表达max_tokens 要限制否则模型一高兴生成一大段TTS 播起来就是灾难。这一步我建议在 Builder 里保存为一个版本。后面每次改 Prompt、调工具描述都单独存版本方便回滚和对比。3.3 第三步通过 WebSocket 接入语音流Agent 创建好之后Builder 会提供一个流式接入地址。我用的是 WebSocket 方式原因是适合双向持续通信客户端持续上传音频服务端持续回传 Agent 事件和 TTS 音频帧。消息协议大致如下客户端发服务端{type:session.start,session_id:xxx,config:{}}客户端发服务端二进制音频帧audio.frame服务端发客户端{type:agent.text,text:...}服务端发客户端{type:tool.call,tool:改预约}服务端发客户端{type:tts.frame,audio:base64编码音频}一个简化的 Python 客户端长这样import asyncio import json import base64 import uuid import websockets async def voice_session(audio_stream, on_tts_frame): session_id str(uuid.uuid4()) async with websockets.connect(ws://your-builder-endpoint/v1/audio/session) as ws: # 1. 开始会话 await ws.send(json.dumps({ type: session.start, session_id: session_id, config: {voice: zh-CN-female} })) # 2. 一边发音频一边收事件 async def sender(): async for frame in audio_stream: await ws.send(frame) async def receiver(): async for raw in ws: msg json.loads(raw) if msg[type] agent.text: print(当前返回文本:, msg[text]) elif msg[type] tool.call: print(要调用工具:, msg[tool]) elif msg[type] tts.frame: await on_tts_frame(base64.b64decode(msg[audio])) await asyncio.gather(sender(), receiver())这个例子我把 VAD 忽略了真实项目里建议在客户端做 VAD或让 ASR 服务直接返回 VAD 事件。Builder 本身也支持“静音自动结束”可以在配置里设置一个静音阈值超时后自动判定用户说完把当前累积音频送入 ASR。我建议先把协议和日志跑通再考虑做打断。一开始就把 barge-in 加上容易分不清问题是出在协议上还是打断逻辑上。3.4 第四步关键参数配置建议下面这张表是我在几轮真实录音测试后留下的参数不一定适合所有场景但可以作为一个起步配置。参数推荐值说明VAD 静音阈值0.4 - 0.7 秒值太小容易切断用户值太大会明显增加响应延迟单轮等待超时10 秒用户超过 10 秒不说话主动询问或引导LLM max_tokens150 - 250控制回复长度语音场景不宜过长LLM temperature0.2低温减少随机表达TTS 首帧缓冲200 - 400 毫秒太低容易卡顿太高会让用户觉得反应慢barge-in开启用户开口时打断 TTS 播放工具调用超时3 秒超过 3 秒业务系统未返回走兜底话术会话空闲释放60 秒长时间无交互结束会话并清理状态重试次数2ASR 异常或 Agent 超时的重试上限参数里的逻辑其实都来自同一个原则语音交互对延迟的耐心很低但对错误打断的容忍度更低。宁可让用户等一秒也不能把话给拦腰截断。4. 上线后最常翻车的 5 个问题排查与调优4.1 用户话还没说完就被截断这个现象在测试阶段几乎必然出现。用户在电话里说“我想查一下上个月话费”结果 Agent 只听进去“我想查一下”就开始播报“请问您要查什么”。先不要急着改 VAD 参数。我的排查顺序是看日志里 VAD_STOP 事件在什么时间触发对比音频时长。看 ASR 返回的是中间结果还是最终结果。如果中间结果也被送进 Agent那就是触发条件错了。看 ASR 热词表有没有覆盖业务关键词。用户说“话费”被识别成“花费”或“花费了”都会导致断句混乱。绝大多数情况下问题不是 VAD 太敏感而是把 ASR 的中间结果当作最终结果用了。正确做法是只有在拿到 final 结果后再触发 Agent除非你在做实时的“边听边显示”效果不然没必要让 Agent 处理临时文本。4.2 Agent 回复太长TTS 播报像机关枪这个问题根子在 LLM不在 TTS。模型默认倾向于给完整的、有解释性的回答但语音场景用户根本记不住。解决办法有两个。一是在 Prompt 里写死“最多两句话”二是把 max_tokens 压在 250 以内。我还会在 Builder 里开“长文本分句”让 TTS 按标点分段播放避免整段文本一次性合成造成首字延迟过大。经验值语音场景下单次播报最好控制在 60 字以内。超过 60 字用户容易走神超过 120 字建议直接拆成两轮播报并加一句“还有一部分内容要继续听吗”。4.3 并发通话之间的状态串线这是我最开始用硬编码方案时踩得最深的一个坑。两路电话同时进来A 用户查账单B 用户改预约结果 A 的通话里突然播报“您的预约已修改成功”。原因很典型会话状态存成了全局变量或者回调处理时没有绑定 session_id。排查时重点看日志里的事件是不是同一 session_id尤其是 TTS 播放事件和 Agent 回复事件。在 Agent Builder 里这个问题的根治办法是把所有会话状态放到会话存储中以 session_id 作为唯一 key。代码里永远不要设置一个“当前会话”的概念因为语音网关是并发的根本不存在单一的“当前”。4.4 打断barge-in失效用户听到播报觉得不对想插话纠正系统却还在自说自话直到播完才肯听。这比延迟高更让人恼火。常见原因有四个播放 TTS 时ASR 被关掉了。很多实现为了省资源在 SPEAKING 状态暂停识别结果用户说话没人听。没有回声消除TTS 播放的声音被 ASR 当成用户输入形成错误打断。打断阈值设得比正常语音音量高用户普通说话音量触发不了。停止 TTS 之后没有清空播放缓冲声音还在继续播。我的做法是TTS 播放期间保持 ASR 半开状态开启回声消除检测到用户声音后立即停止 TTS 播放并清空缓冲然后重新进入 LISTENING。整个过程最好在 200 毫秒内完成。4.5 延迟太高用户以为电话断了语音场景里从用户说完到听到回复超过 3 秒就会被认为“卡了”超过 5 秒就可能直接挂电话。我实测下来一条典型的链路延迟大概是下面这样环节典型耗时优化方向网络 RTT50ms尽量同区域部署VAD 判定200ms静音阈值调低一点但不能太低ASR 返回 final800ms开启热词表、声学模型适配Agent first token1200ms选首字延迟低的模型限制上下文长度TTS 首帧300ms用流式 TTS不需要等整段合成总计约 2.5s已经属于可接受范围如果超过这个范围先看日志时间戳定位瓶颈在哪个环节不要盲目换模型。曾经有一段时间我觉得 Agent 太慢换成更大的模型结果延迟更高。后来才发现瓶颈是知识库检索每次都全量扫描和模型没有关系。还有一个技巧常用问题可以做静态回复缓存。比如“你们营业时间到几点”这类问题根本不需要走业务工具直接在编排层加一个规则判断命中就返回固定话术能省几百毫秒。5. 我留下的一份“最小可用配置”和几条体会5.1 一份可以直接复用的最小配置清单如果你和我一样第一次构建语音 Agent建议先照抄下面这份配置跑通后再逐项优化ASR流式接口开启热词表词表包含“查账单、改预约、转人工”。LLM支持 function callingtemperature 0.2max_tokens 200超时 3 秒。TTS流式中文音色开启打断首帧缓冲 300 毫秒。VAD静音阈值 0.5 秒。工具三个查账单、改预约、转人工。会话状态IDLE、LISTENING、THINKING、TOOL_WAIT、SPEAKING、TRANSFER、END。兜底工具连续失败三次自动转人工Agent 超时两次播报“信号不太好我帮您转人工”。这套配置虽然简单但足够处理一批标准的客服电话。它最大的价值是让你在最短时间内把链路跑通之后再根据真实录音去调。5.2 我实际跑下来的几条体会第一不要一上来就追低延迟。语音 Agent 的稳定性优先级永远高于性能。状态不乱、不误打断、上下文清晰这些基础没做好延迟再低用户也会觉得这个机器人很蠢。第二Prompt 要以“短”为美。语音 Agent 的 Prompt 不需要长篇大论描述人设把媒介特点、回复长度、工具触发规则、兜底逻辑写清楚就够了。我在一段 150 字的 Prompt 和一段 600 字的 Prompt 之间做过对比前者在语音场景的完成率反而更高。第三日志一定要事件化。不要只记“用户说了什么机器人回了什么”要记 VAD_STOP、ASR_FINAL、AGENT_RESPONSE、TTS_START、BARGE_IN 这些事件以及对应时间。没有事件日志出了问题基本只能靠猜。第四Builder 的版本管理要随手用。每改一次 Prompt 或工具描述就存一个版本并用一组固定测试音频做回归。语音场景很敏感可能一个标点符号的变化都会改变模型的断句方式。最后再分享一个小技巧上线前准备几段“故意很难”的测试录音包括用户语速极快、吞字、背景有电视声、说话说到一半改口。把这些录音在 Builder 里回放一遍比接十通真人测试电话都管用。语音 Agent 好不好用往往不是看它多聪明而是看它在这些真实环境里能不能不崩。