尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用RTC自建语音智能体:从音频采集到ASR/LLM/TTS串联的全流程指南

用RTC自建语音智能体:从音频采集到ASR/LLM/TTS串联的全流程指南 做语音实时互动智能体一开始我其实是冲着OpenAI语音模型去的确实强实时对话效果拉满。可真要往产品里放的时候问题就来了调试链路不透明、延迟不可控、想在中间插一个自定义拦截或改写逻辑非常麻烦。后来我干脆把底层换成了RTC SDK自己做音频采集、传输和播放上面再接ASR、LLM、TTS三个独立组件结果从零到跑通端到端demo只花了一个周末。这篇文章我把整套思路和代码落地方案完整拆出来覆盖方案选型、音频参数、前端采集、服务端Agent串联、回声与延迟排查。适合正在做语音客服、销售陪练、面试模拟、外教对话这类应用的开发者参考产品经理读完也能理解为什么这条路落地更快。1. 为什么说RTC路线比OpenAI语音模型落地更快方案对比与选型1.1 OpenAI语音模型方案与RTC方案的实质区别先不要急着一上来就写代码方案对比没想清楚后面每一步都是坑。我当初实际对比过两条路线。一条是直接用OpenAI的实时语音API音频进去、文本/音频出来看起来确实最简单。SDK帮你做了VAD、打断、编解码demo体验也很棒。但有个核心问题这条链路对我来说是个黑盒。想加一条用户说到敏感词就静音的拦截规则接口层面做不到想调整VAD敏感度控制台不暴露更麻烦的是整条链路强依赖单一厂商线上一旦出问题你只能等对方修复自己毫无还手之力。另一条就是题目里说的方案用RTC SDK把音频采集、传输、播放的通路自己建起来AI部分对接独立的ASR、LLM、TTS模块。前期确实要多花半天到一天把音频链路调通但之后每个环节都能看到、能改、能优化推进速度反而快很多。这两种方案的核心差异我列了一张对比表对比维度OpenAI实时语音APIRTC SDK 自选AI组件音频链路黑盒SDK内部处理全程可控每个节点可观测组件替换强绑定OpenAI全家桶ASR/LLM/TTS各自独立随时换延迟优化受制于远端服务可调项有限链路分层可逐段打点定位打断/回声接口层面有限控制利用3A前处理和VAD精细控制并发成本按分钟计费规模化成本偏高自选组件成本可按量调优在原型阶段OpenAI那条路确实是真香半天出效果。但一旦要考虑真实用户、真实网络、真实并发黑盒链路会让你非常被动。我选择RTC方案不是因为OpenAI不好而是因为做产品讲究可维护、可优化、可替换这三点RTC方案天然占优。1.2 RTC方案解决的实际问题我实际踩过之后总结下来RTC方案主要解决了三个问题。第一是调试链路透明。语音智能体最怕不知道在哪一段出了问题。用户说了一句话到底是采集没采到、传输丢了包、ASR识别错了还是LLM输出太慢RTC方案可以逐段打点音频帧从哪里来、到哪里去、延迟多少毫秒全都可以埋日志。我后来把每个环节的耗时都打进Prometheus哪个环节变慢一眼就能看出来。第二是音频前处理成熟。回声消除、噪声抑制、自动增益这三件套在RTC里已经被打磨了很多年效果比很多AI团队自己拼的WebSocketWebRTC方案要稳得多。这三个处理环节没做好语音智能体基本没法用。我前几次联调被回声折磨得够呛后面发现就是AEC(回声消除)没配置对RTC SDK里把开关打开问题立刻解决。第三是模型可替换。今天ASR用A厂的流式识别明天想换B厂的低时延方案只需要改接入代码里的一小段不用动骨架。LLM更是这样今天用这个模型明天换那个模型对RTC链路零影响。这种解耦能力对快速迭代太重要了。2. 动手前先定架构语音智能体的关键参数与延迟预算2.1 一个可落地的语音智能体长什么样整个系统就三块用户端、RTC频道、Agent服务端。可以用文字图表示用户端Web / 小程序 / App │ RTC SDK 采集麦克风48kHz / Opus编码 ▼ RTC 频道弱网对抗、3A处理、音频路由 │ ▼ Agent服务端 RTC SDK订阅音频流 → 重采样16kHz → 流式ASR → LLM生成回复 → TTS合成 → 经RTC推回频道 ▼ 用户端扬声器播放这个架构里RTC频道相当于一条双向高速公路。用户上车发布音频Agent在另一边下车订阅音频处理完再上车推回去。为什么不用传统的WebSocket直连因为实时语音场景对网络抖动、丢包、延迟的容忍度极低RTC链路是专门为这些问题优化的拥塞控制、丢包重传、音视频同步全都内建好了。你不需要自己重新发明轮子。我当时在服务端还加了一个可选的旁路把用户和AI的对话全程录音存下来用于事后质检和模型效果评估。这个在RTC里也有现成的流录制能力省了单独搭录音服务。2.2 音频技术参数采样率、编码器、码率怎么选音频参数是语音智能体最容易翻车的地方我直接给出一套实测可行的配置环节参数说明麦克风采集48kHz / 单声道主流设备原生支持减少前处理失真传输编码Opus默认语音编码抗丢包能力好语音码率32-48kbps语音场景足够清晰省带宽送ASR前重采样到16kHz / 单声道 / PCM几乎所有ASR引擎的标准输入格式TTS合成16kHz或24kHz PCM推回RTC前统一转Opus为什么都用Opus因为它内建了前向纠错FEC和丢包隐藏PLC弱网下声音不容易断。这也是RTC链路最大的优势之一。绝大多数RTC服务商都把Opus作为默认编码你在SDK初始化时几乎不用操心。需要注意的跨层问题是采样率一致性。各个ASR、TTS服务到底要多少采样率接之前一定确认好。我见过不少项目ASR那边用的16kTTS合成的却是24k中间没有做重采样导致播放端要么变调、要么破音。这个问题排查起来很隐蔽因为报错往往不是直接提示采样率不匹配而是声音有点怪。2.3 延迟预算首句延迟与打断机制用户说了一句话AI什么时候开口回答这个时间差就是首句延迟。行业里比较能接受的期望值是1.5秒以内做得好的能压到1秒左右。我把整条链路的延迟拆开算过首句延迟 网络RTT VAD判定 ASR首包 LLM首Token TTS首包 播放缓冲 示例100ms 250ms 300ms 400ms 250ms 200ms 1500ms从公式能看出来每个环节都要抠。网络RTT和播放缓冲是基础设施成本压缩空间小VAD判定、ASR、LLM、TTS这四块才是优化大头。后文第五章我会讲我实际压延迟的做法。打断机制也要提前设计。用户说话时AI正在播放回复这时候必须能停得下来。标准做法是Agent在播放TTS的同时持续接收用户的麦克风音频流用轻量级VAD做能量检测一旦检测到用户开口立即中断播放。这里一定不要等ASR转写结果ASR正常情况下需要几百毫秒对打断场景来说太慢。3. 前端篇5分钟跑通麦克风采集与RTC推流3.1 准备前端工程与SDK接入我先以Web端为例用Vite加原生JavaScript方便你把核心逻辑看明白。如果你做小程序或者AppSDK版本不一样但流程完全一致。首先要做的三件事在RTC服务商控制台注册账号创建应用拿到App ID。写一个后端接口比如/api/rtc/token用来动态签发加入频道的临时Token。生产环境绝对不能把App ID裸着放前端安全性和权限控制全靠Token。安装RTC Web SDK。npm create vitelatest voice-agent-web --template vanilla cd voice-agent-web npm install rtc-sdk-web注意包名以你实际接入的RTC服务商为准我用的是示意写法。Android和iOS端通常还有环境问题Android SDK Manager偶尔会报拉不到SDK版本把Platform Tools手动装一遍基本就能解决iOS端则要注意麦克风权限描述Info.plist里必须写清楚。3.2 核心代码采集、处理、入会、发布前端代码的核心就四步初始化客户端、创建麦克风音轨、加入频道、发布音频。我给出关键代码import RTCClient from rtc-sdk-web; const client new RTCClient({ appId: your_app_id, channel: voice-agent-demo }); async function startAgent() { // 1. 从后端换取动态Token const { token } await fetch(/api/rtc/token).then(res res.json()); // 2. 创建麦克风音轨重点是把3A处理打开 const micTrack await client.createMicrophoneAudioTrack({ encoderConfig: speech_standard, advancedOptions: { echoCancellation: true, // AEC 回声消除 noiseSuppression: aggressive, // NS 噪声抑制 autoGainControl: true // AGC 自动增益 } }); // 3. 加入频道并发布音频 await client.join({ token, uid: web_user_ Date.now() }); await client.publish(micTrack); // 4. 订阅Agent端推回的回复音频 client.on(user-published, async (user, mediaType) { if (mediaType audio) { await client.subscribe(user, audio); user.audioTrack.play(); } }); } startAgent();这段代码里最容易被忽略的是advancedOptions里那三个开关。很多教程把这一步略过去导致读者抄完代码总觉得声音不对劲要么回声大要么环境噪声哗啦哗啦的。这三个开关对应的是音频前处理三大件语音场景必须全开。encoderConfig: speech_standard也值得说。这个配置会让SDK按语音场景优化编码自动设定合适的采样率和码率而不是拿默认的音乐模式来跑。语音智能体场景不需要高频细节省下来的码率都该让给抗丢包能力。3.3 前端调通的验收标准前端接完先别急着接服务端我自己总结了一套验收清单对麦克风说一句话扬声器里不应该出现自己的回声。环境噪音明显被压住但不至于把整个人声也削掉。加入频道后在控制台后台能看到这个用户上线。打开浏览器的音频轨道检查能看到本地的音量和码率数据是稳定波动的。移动端稍微有些额外要求。安卓WebView里页面必须在用户点击手势后再调用getUserMedia相关API否则麦克风权限弹不出来。微信小程序要用小程序专用SDK不能直接拿Web版跑。iOS端在微信内打开的页面音频播放会有HTML5的AudioContext限制需要在用户交互后主动resume()。这套前端调通之后一个能实时通话的底座就立住了。接下来才是把AI接进去。4. 服务端篇让Agent听懂、思考、回话的完整串联4.1 服务端如何拿到用户音频流服务端接RTC通常有两种方式一种是用服务端SDK直接把频道里的音频流转成PCM回调给业务代码另一种是消息通知配合拉流接口。我推荐回调方式省去自己维护拉流和解码的麻烦。# agent_service.py 示例 import asyncio from rtc_sdk import RTCEngine ENGINE RTCEngine(app_idyour_app_id) ENGINE.on_audio_frame async def on_audio_frame(user_id, pcm_data, sample_rate): # 回调拿到的是解码后的PCM音频 # 注意采样率可能是48k必须先重采样到16k pcm_16k resample(pcm_data, sample_rate, 16000) asr_engine.feed(user_id, pcm_16k) if asr_engine.is_sentence_end(user_id): await handle_sentence(user_id, asr_engine.get_text(user_id))这里最关键的一步是重采样。很多ASR服务只接受16kHz单声道PCM而RTC回调给你的音频通常是48kHz。如果直接把48k数据扔给ASR轻则识别率下降重则直接报无效采样率。重采样这一步用现成的音频处理库就能做不建议自己写滤波容易引入噪声。4.2 流式ASR接入与用户说完了判定ASR有两种取法等用户说完一次性识别非流式或者边说话边出字流式。语音智能体必须用流式。原因很简单非流式意味着你要等用户彻底停顿后几百毫秒才开始识别再加上识别时间、LLM时间、TTS时间首句延迟直接奔3秒去了没人受得了。判定一句话说完标准做法是VAD静音检测连续一段时间检测不到有效语音就认为当前句子结束。静音窗口一般取250到350毫秒。窗口太短遇到用户自然停顿就会过早断句窗口太长延迟又上去了。这个参数在实际调优时很关键。ASR的识别结果也有讲究。流式ASR一般会同时返回部分结果和最终结果。我的经验是部分结果可以用于实时展示和轻量判断但最终触发LLM的一定要等最终结果。如果用部分结果去触发LLM经常会出现用户说了一半AI已经开始答非所问的情况。4.3 LLM与TTS串联让AI开口说话LLM部分本身不复杂复杂的是上下文管理和超时兜底。我维护了一个按用户隔离的会话对象里面存最近十轮对话。每次LLM调用都带完整上下文同时给一个明确约束如果5秒内没有返回结果就返回一句兜底文案比如我还在想你可以再说一次吗避免用户莫名其妙等半天。TTS选型主要看首包延迟和自然度。首包延迟指的是从传入文本到返回第一段音频的时间好的TTS能做到150到300毫秒。合成出来的音频在推回RTC播放前要统一采样率到RTC链路能接受的范围再编码成Opus。这里我给出一个串联代码async def handle_sentence(user_id, user_text): # 1. 拿到LLM回复 reply await llm.chat(user_id, user_text) # 2. TTS合成拿到PCM音频 audio_pcm await tts.synthesize(reply) # 3. 通过RTC推流给该用户 await ENGINE.play_audio_to_user(user_id, audio_pcm)实际项目里llm.chat和tts.synthesize后面都有各自的超时与重试。我把LLM超时设成5秒TTS超时设成3秒任一环节超时就走兜底逻辑绝不让用户无限等待。这样虽然偶尔会有答非所问但至少对话链路不会卡死用户体验是连贯的。4.4 会话状态与打断逻辑每个用户需要独立维护状态我把它放在一个session对象里session { context: [], # 多轮上下文 is_playing: False, # 当前是否在播放TTS last_vad_time: 0, # 上次检测到语音的时间戳 interrupted: False # 是否发生过打断 }打断逻辑是语音智能体体验的分水岭。废话不多说直接看代码async def play_reply_with_interrupt(user_id, tts_audio): play_task asyncio.create_task(ENGINE.play_audio_to_user(user_id, tts_audio)) while not play_task.done(): frame await ENGINE.get_next_audio_frame(user_id) if detect_speech(frame): # 用户一开口立刻中断播放 play_task.cancel() session[interrupted] True break await asyncio.sleep(0.05)有一个容易忽略的细节中断播放之后AI刚才没说完的那句话还要不要保留进上下文我的建议是保留但把最后一句标记为被用户打断。这样LLM在下一轮接话时知道用户已经转移话题了不会还执着于把之前没说完的内容补完上下文衔接会自然很多。5. 联调到上线回声、延迟、弱网我踩过的坑与排查记录5.1 回声音量大到根本没法用第一次联调我在自己电脑上听到自己的声音从对方端绕回来第一反应是换耳机、换麦克风。排查到最后发现问题根本不是硬件而是AEC没生效。排查顺序我整理成了固定套路确认采集端三个3A开关真的打开了。日志里直接打出来别只看代码。确认播放端音量没有超过安全阈值。音量拉满时扬声器声音容易串回麦克风。确认不是把远端音频错误地又送回了编码器。最后解决方式很简单显式开启回声消除并把播放音量调到70%左右回声立刻消失。这个坑属于只要配置正确就不存在的类型但真出问题时会让你怀疑人生。5.2 首句延迟卡在2.5秒压不下来上线前测试用户问一句话AI平均要2.5秒才开口。这个延迟完全不能接受。我逐段埋点得到这样的分布环节耗时占比网络RTT80ms3%VAD判定750ms30%ASR首包500ms20%LLM首Token500ms20%TTS首包300ms12%播放缓冲200ms8%其他170ms7%VAD判定占了750ms明显有问题。查下来是静音窗口设了500ms太保守了。我把静音窗口从500ms收到250msVAD判定时间直接降到400ms以内。另外ASR从非流式换成了流式首包模式又省了200ms。最终整体首句延迟压到1.3秒左右体感好非常多。提示延迟优化一定要先打点再动手不要凭感觉瞎调。每个环节耗了多少用日志全部打出来谁高优化谁一通乱调只会让问题更复杂。5.3 弱网环境下声音断断续续联调时坐的位置离路由器远了一点声音就开始断断续续一句话能丢一半。这是典型的弱网问题。RTC对抗弱网的手段主要是前向纠错FEC和码率自适应。FEC能在丢包时恢复一部分音频帧码率自适应则根据网络质量自动调整发送码率宁可降低音质也不要让语音断掉。我把SDK里的编码配置改成语音优先并开启码率自适应弱网下的表现从完全没法对话变成偶有轻微卡顿。如果你用的RTC SDK没有这个开关建议换一个成熟的服务商语音智能体对弱网的要求比视频会议还高因为轻微卡顿会影响ASR识别准确性。5.4 上线前必查清单我把上线前检查项整理成一张表格每次发版前对一遍省了很多线上事故检查项通过标准回声测试无自激和明显回声首句延迟目标环境下1.5秒以内打断响应用户开口500ms内停止播放弱网模拟20%丢包下对话基本可理解并发压测预期在线人数下CPU无异常ASR兜底超时/无结果时回复兜底文案Token续期频道内Token过期前自动刷新录音存档合规前提下的会话质检录音可拉取5.5 其他小坑记录除了上面三个大坑还有几个小问题值得提一下。一个是Web端和移动端编解码兼容出现过Web端正常、小程序端声音变调的情况最后是采样率设置不一致导致统一成16kHz后解决。另一个是LLM偶尔返回超长文本TTS合成可能要花好几秒我加了最大长度截断超长回复自动拆分并只播前两段。还有一次测试时发现某个用户的网络环境频繁切换导致RTC频道反复掉线重连我一开始以为是SDK的问题后来发现是Token有效期设得太短用户长时间挂机后token过期。把Token有效期延长到2小时并写了一个静默续期逻辑问题消失。这类问题都很细但每一个都可能在线上给你一记闷棍。6. 语音智能体还能扩展什么从demo到产品的进阶方向跑通最小闭环之后想要做成一个能商业化的产品下面的扩展方向基本绕不开。第一个是知识库接入。现在的LLM只会基于训练数据回答如果要做一个销售陪练或者客服问答智能体必须接上企业自己的知识库。做法是把RAG套在LLM前面用户问题先做embedding召回相关文档再让LLM基于文档内容生成回答。这个能力在架构上完全不影响RTC链路。第二个是主动对话能力。目前Agent都是用户说一句AI回一句但真实场景里AI应该能主动反问。比如用户犹豫时问一句需要我给你介绍一下方案吗这个需要修改整个调度的触发方式不只是给LLM加个prompt能解决的。核心改动在Agent服务端的会话管理模块。第三个是质检与数据回流。语音智能体上线后会产生大量对话数据用好这些数据才能持续优化。把RTC侧录制的音频连同ASR文本、LLM回复一起落到数据仓库顺便接入情绪识别模型对用户情绪做标记。后面做效果评估、prompt调优、甚至私有模型微调都靠这批数据。最后再分享一点个人体会做完这个语音智能体项目我最大的感受是难点不在于大模型而在于把音频工程做好。模型用OpenAI还是国产开源差别更多在体验上限但音频链路做不好再强的模型也救不了场。我自己沉淀下来的开发顺序是先用RTC把整体闭环跑通让用户能开口说话、Agent能回话且不啸叫不回音然后再去调LLM、调prompt。顺序一旦反过来你就会在无数音频问题上反复返工被延迟、回声、断音折磨得怀疑人生。如果你也准备做语音智能体建议就从这篇文章里的最小闭环开始。先把前端采集和服务端ASR跑通听到自己说的话被正确转成文字的那一刻后面的事情就顺了。这中间遇到任何问题记住一句话音频链路每一步都可以打点延迟高就拆开优化回声大就检查3A断音就调弱网策略。稳住能成。
返回列表