
本来只打算给公司做一套简单的IVR语音问答结果越做越深最后把FreeSwitch、MRCP协议、科大讯飞的ASR和TTS全部串到了一起还接上了对话系统。整个过程踩了不少坑也把很多文档里没写清楚的东西摸了一遍。今天就把这条链路的完整思路、架构设计、实际操作和排查经验整理出来给正在做同类项目的朋友一个参考。这套方案的核心链路是电话进来 → FreeSwitch接住 → MRCP协议把语音送给讯飞识别ASR→ 拿到文本交给对话系统 → 对话结果再通过MRCP送给讯飞合成语音TTS→ 播放给用户听。适合通信集成开发、呼叫中心运维以及准备做电话语音机器人的团队。1. 项目整体设计从一条语音链路说起先把这个项目到底是什么讲清楚。FreeSwitch是一个开源的软交换平台在电话领域它扮演的角色类似于运营商交换机能处理SIP信令、RTP媒体流也能跑各种拨号计划Dialplan。但它本身不具备听懂人话和开口说话的能力所以需要外接语音AI引擎而MRCP协议就是FreeSwitch和引擎之间的桥梁。很多第一次接触这个项目的人有个误区以为直接调一下讯飞的API就完事了。实际上电话场景和互联网API场景差别很大——电话进来是一路持续的音频流你要在这路流上做实时识别、随时打断、动态合成还要管理通话状态纯HTTP API根本扛不住这种交互模型。MRCPMedia Resource Control Protocol就是专门解决这个问题的协议它管理的是识别会话合成会话这一层资源适合实时交互。这里有一个很关键的行业现状科大讯飞官方并没有直接提供一个标准的MRCP服务器给你连标准MRCP服务器通常是厂商设备商比如Aculab、Nuance才提供。讯飞对外主要给的是WebSocket流式接口和各类SDK。所以实际落地的时候业界通常有两条路一是找个标准的MRCP Server中间件把讯飞挂进去二是自己写一个很薄的适配层把FreeSwitch发过来的MRCP请求翻译成讯飞WebSocket调用。两条路我都试过后面会详细拆。1.1 为什么选择MRCP标准化接口带来的好处先说结论如果你的项目只是做个网页端语音识别Demo那不需要MRCP直接用讯飞SDK就行。但凡是电话链路尤其是要做到多并发、稳定运行MRCP几乎是必须的。MRCP规定了几个核心动作RECOGNIZE开始识别、STOP-RECOGNITION停止识别、SPEAK合成并播放语音、STOP-SPEAK停止播放。这些动作在电话场景里就是刚需。比如用户在机器人播报中途插话你需要立即停止播放并重新识别MRCP的方法设计天然支持这种操作。另外一个好处是解耦。FreeSwitch和语音引擎不再强绑定今天你接的是讯飞明天想换成其他语音服务商只需要改一下MRCP Server的适配层FreeSwitch侧几乎不用动。这种插拔式架构在项目后期特别值钱因为语音引擎的选型往往会随着价格、识别率、音色效果不断调整。我实际做下来还有一个体会MRCP虽然是老协议但它在处理RTP音频流、会话状态、超时控制这些方面非常成熟比拿WebSocket硬拼稳定得多。真上了生产环境一天几千通电话跑着稳定性就是命根子。1.2 方案对比直连API、标准MRCP服务器与中间适配层选哪个我把试过的几种方案摆在一起做对比给大家选型时一个参考方案优点缺点适用场景FreeSwitch直接调用讯飞HTTP/WebSocket API开发快不用中间层回调地址要暴露公网会话状态管理困难多并发容易失控个人Demo低并发测试使用uniMRCP Server等开源MRCP服务器协议标准FreeSwitch生态成熟开源版本对讯飞私有协议适配弱需要自己写引擎插件技术团队能力较强想要标准架构商用MRCP服务器 讯飞网关稳定有厂商技术支持花钱部分厂商对讯飞引擎适配得看版本企业级生产预算充足自研MRCP适配代理灵活性最高可以精细控制媒体流和并发需要自己维护信令映射和RTP转发中等并发想要完全掌控链路我最终选择的是FreeSwitch uniMRCP Client 自研适配代理 讯飞WebSocket这个组合。原因是团队对FreeSwitch比较熟而讯飞的接口形态是WebSocket流式的自己写一个适配代理把MRCP方法翻译成WebSocket调用控制力最强遇到问题也能快速定位。这里说一个容易翻车的点很多人一上来就找现成的MRCP Server想直接连讯飞结果发现讯飞那边根本没有标准的MRCP协议栈默认给的SDK是C/Java/Python版的需要你把它封装成MRCP服务。这个工作量比想象中大建议先把这个认知对齐再动手。1.3 整体架构信令、媒体与控制流怎么走我们把整条链路拆成四股流SIP信令流用户呼叫进来FreeSwitch作为SIP Server处理呼叫的建立和释放。媒体流RTP用户的语音以RTP包形式发送给FreeSwitchFreeSwitch根据业务需要把音频转发给识别引擎或播放合成音频。MRCP控制流FreeSwitch通过MRCP协议底层通常跑RTSP发送控制指令比如开始识别、结束识别、开始合成。音频对接流如果用了自研适配代理FreeSwitch把RTP语音流交给代理代理以音频流的格式送入讯飞WebSocketTTS时则是讯飞返回的音频流通过代理再送回FreeSwitch播放。画成图就是电话用户 ↓ SIP/RTP FreeSwitch (mod_unimrcp) ↓ MRCP/RTSP RTP语音 讯飞适配代理自研 ↓ WebSocket 音频帧 科大讯飞语音云ASR/TTS ↑ HTTP 对话系统NLU / 大模型这里面最容易被忽略的是媒体流的路由。很多人以为MRCP只是发个指令然后拿结果实际上识别过程中用户的实时音频是要源源不断送到引擎侧的音频格式、采样率、编码不匹配识别率直接崩。后面我会专门讲音频格式统一的问题。2. 环境准备FreeSwitch与MRCP模块2.1 安装FreeSwitchWindows和Linux两条路径动手第一步是搭好FreeSwitch环境。开发调试阶段用Windows非常方便有官方的Windows安装包一路下一步就行。但安装有几点要注意安装路径不要带中文和空格推荐装到D:\FreeSwitch首次运行要用管理员权限因为需要注册系统服务和开放防火墙端口装完先把默认的防火墙规则关掉或者放行UDP 16384-32768RTP端口范围和TCP 5060、5061SIP端口不然很多电话根本打不进来。Linux生产环境我用的Ubuntu 20.04直接通过源码编译装FreeSwitch 1.10.x。编译之前先把依赖包装齐编译过程通常二十分钟到半小时装完以后注意把mod_unimrcp这个模块编译进去。如果你用Windows安装包mod_unimrcp是默认自带的这一点比源码编译省心很多。装完之后进fs_cli输入sofia status看SIP是否起来输入version看版本号。强烈建议起步阶段先把FreeSwitch的日志级别调低用console loglevel debug跑一轮测试很多问题都能从日志里直接看出来。2.2 加载并验证mod_unimrcpFreeSwitch跟MRCP相关的模块叫mod_unimrcp它本身是unimrcp项目在FreeSwitch里的嫁接实现。这个模块需要在一个配置文件里找到并取消注释主配置文件conf/autoload_configs/modules.conf.xml模块自身配置conf/autoload_configs/unimrcp.conf.xmlMRCP Profile配置conf/mrcp_profiles/默认带了几个示例比如unimrcp-tts.xml、unimrcp-asr.xml启动后进fs_cli执行module_exists mod_unimrcp看模块是否加载成功。然后可以执行mrcp version查看unimrcp的版本信息。这里有一个新手容易踩的坑unimrcp的配置分成两个层面一个是unimrcp模块自身的配置比如日志路径、引擎加载方式另一个是MRCP Profile定义跟哪个MRCP服务器通信、用什么协议、引擎类型是什么。很多教程只让你改profile没提模块配置结果模块本身没把MRCP引擎加载起来连上去就是空列表。2.3 语音资源与音频格式准备在接讯飞之前先用一个本地的MRCP Server做个端到端联通测试这样能排除讯飞接口对MRCP协议栈的干扰。测试的时候准备一个8kHz或16kHz的单声道WAV文件用FreeSwitch的play_and_detect_speech在拨号计划里测试识别看识别结果能不能返回。音频格式是决定识别效果的关键。讯飞WebSocket接口通常支持两种采样率8k和16k推荐直接用16k。FreeSwitch端要注意呼叫进来时协商的编码格式如果不一致需要开启转码。常见做法是在dialplan里设置set变量强制使用PCMU或L16或者在conf/sip_profiles/internal.xml里调整inbound-codec-prefs。我踩过最狠的一个坑是电话进来是G.711编码8k而讯飞识别模型要16k采样率结果前端把音频送过去以后识别结果全是乱码。后来在适配层统一做了重采样把8k音频转成16k PCM再送识别问题立刻解决。3. 讯飞ASR/TTS接入实操3.1 讯飞的接口形态WebSocket、HTTP与SDK先说讯飞开放平台现在的接口形态。ASR这块实时语音转写用的是WebSocket流式接口你可以在说话的同时持续上传音频帧服务端按句返回中间的识别结果这就是对话场景要用的接口。另外还有录音文件转写接口属于异步任务不适合电话实时场景。TTS这块讯飞提供的是WebSocket流式合成你把文本发过去服务端流式返回音频数据同样适合实时播放。至于SDK讯飞确实提供了Linux下的C/Java/Python版本SDK。如果你搜asr语音转文字有ubuntu的sdk包吗——有官方支持Linux x64架构也有ARM版本的第三方封装但需要自己看文档确认。但注意就算你拿到了Linux SDK它也不是MRCP协议还是需要套一层适配才能接进FreeSwitch。我的建议是不要一开始就抱着SDK搞直接用WebSocket协议对接最灵活。SDK帮不了你太多反而可能因为是封装好的黑盒出了问题不好排查。3.2 uniMRCP客户端配置引擎与语法定义MRCP Profile的文件里引擎类型分两种speechsynthesizeTTS和speechrecogASR。如果在接适配代理你会在profile里指定代理的IP和端口。一个典型的MRCP Profile片段如下include profile namexfyun-asr param nameserver-ip value127.0.0.1/ param nameserver-port value5090/ param nametransport valuertsp-mrcp/ param nameengine-id valuespeechrecog/ param namesyntax-format valuesrgs/ param namespeech-complete-timeout value2000/ param namespeech-inactivity-timeout value3000/ /profile /include几个参数的取值逻辑server-ip和server-port指向你的讯飞适配代理而不是直接指向讯飞云端因为讯飞没有标准MRCP端口。engine-id告诉FreeSwitch这个profile是ASR还是TTS写错会导致调用时报resource not available。syntax-format是用来定义识别语法的格式支持SRGS。如果用的是热词表也可以直接在语法里定义一个简单列表。speech-complete-timeout和speech-inactivity-timeout控制VAD语音端点检测数值太短会把一句话截断太长又会让停顿变久这个要看业务调。3.3 适配层把MRCP语义翻译成讯飞WebSocket调用这应该是全文最关键的部分了。因为FreeSwitch是用MRCP协议发指令的而讯飞是用WebSocket收音频的两个协议之间需要做语义映射。我写了一个适配代理用Python的aiortc和websockets库实现逻辑也简单核心就是做一个协议转换器。适配代理的监听逻辑是它作为一个RTSP/MRCP服务端接收FreeSwitch发来的MRCP消息。FreeSwitch发RECOGNIZE指令时代理向讯飞WebSocket发起连接并把后续收到的RTP音频转成PCM帧推给讯飞。讯飞返回的识别结果会通过RECOGNITION-COMPLETE消息返回给FreeSwitch。我整理了一个语义映射表照着实现就不会乱MRCP方法/事件讯飞WebSocket动作说明DEFINE-GRAMMAR把语法/热词写入识别请求的lm_id或hotwords参数设置识别词库RECOGNIZE发起WebSocket连接发送音频帧打开音频通道持续推流STOP-RECOGNITION发送结束标志等待最终识别结果告诉讯飞话说完了RECOGNITION-COMPLETE把识别出的文本封装成MRCP事件回传带text字段SPEAK发起WebSocket TTS连接发送文本讯飞返回音频帧STOP-SPEAK关闭TTS通道丢弃未播放的音频用于打断这里有一个很实用的小技巧讯飞WebSocket的鉴权要求拼接一个URL里面带时间戳和签名而且有有效期。如果你的适配代理和FreeSwitch之间偶发连接被拒绝八成是签名过期或者系统时间不对我建议在代理层做一次时间同步检查把这个问题拦截在源头。3.4 端到端测试流程识别→对话→TTS合成配置好以后先在fs_cli里做一轮最简单的测试呼叫一个分机然后播放一句录音同时用play_and_detect_speech触发识别。originate user/1000 play_and_detect_speech(hello.wav, xfyun-asr, *any)命令的意思是呼叫分机1000播放hello.wav给用户听同时开启MRCP识别识别引擎用xfyun-asr这个profile。识别结果会返回到控制通道。如果这一步能跑通ASR链路就通了。再测TTS可以执行originate user/1000 speak(xfyun-tts, 您好这里是智能客服请问有什么可以帮您)听到语音播放就说明TTS链路通了。我在实际业务里是写了一个lua脚本把ASR、对话、TTS串起来。大致逻辑是接听电话 → 播放开场白并开启识别 → 拿到用户说的文本 → POST给对话系统 → 拿到机器人回复 → 用TTS合成播报 → 播完再开启下一轮识别。这就是一个最简的电话语音机器人雏形。4. 对话系统集成与业务场景落地4.1 ASR结果如何进入对话系统ASR输出的是文本但对话系统不一定只认纯文本。以我接的对话系统为例它是内部一个基于大模型封装的NLU服务输入需要带上会话ID、用户ID、当前节点输出则返回下一轮的回答文本和可能的动作指令比如转人工、查订单。FreeSwitch侧最方便的方式是直接用mod_curl发起HTTP请求。在lua脚本里这样处理local asr_text params:getHeader(variable)[detect_speech_result] local http require(socket.http) local resp, code http.request(http://dialog-internal:8000/chat, json.encode({ session_id session_uuid, user_text asr_text, channel phone })) local reply json.decode(resp).answer要注意的是识别文本里可能带了标点也可能有空格的误识别对话系统对这些噪声要能容错。我在对话系统对外接口里加了文本清洗把多余的空格、标点统一规范化否则同一个问题有时候能回答有时候不能。4.2 对话结果如何通过TTS播报拿到对话系统的回答文本后下一步就是调用TTS接口合成语音。在MRCP的标准流程里调用SPEAK方法时传一个文本FreeSwitch就会把文本交给TTS引擎并播放返回的音频。但这里有个坑大模型的回答经常很长几百上千字如果一次性塞给TTS识别合成时间会很长用户等得不耐烦。我采取的策略是对于多轮回答先做分句切割按句号、感叹号、问号拆成多个短句然后逐句合成播放。也可以在TTS请求里带语速参数整体调快一点。讯飞TTS支持语速、音调、音量调节在MRCP适配层里把这些参数映射到讯飞的接口参数里。另外要做TTS结果缓存。常见问题、欢迎语、菜单播报这些固定文本第一次合成以后缓存音频文件下次直接播文件就行能省不少并发和费用。我实测下来这部分优化能节省至少30%的TTS调用量。4.3 打断处理与并发控制电话机器人最影响体验的就是打断Barge-in。用户正在听机器人播报时突然想插话系统必须能立刻停止播放并开始收音识别。这个能力FreeSwitch原生支持得不够细需要组合两个机制第一种用play_and_detect_speech。它本身支持在播放过程中持续检测语音一旦检测到用户说话立即触发识别回调。这是最简单的方式。第二种更精细的控制是用MRCP的STOP-SPEAK方法配合FreeSwitch的DTMF检测或音频能量检测。当用户打断时先停止TTS播放再紧接着发起新的RECOGNIZE。并发这块讯飞的免费和商用配额是按并发路数计算的。比如你买了20路并发那个意思就是同一时间最多20路电话能同时调用识别。超出部分会报错或排队。我在适配代理里做了信号量控制当并发到达上限时新请求先排队等待避免直接报错导致通话中断。同时要对讯飞的接口调用做超时控制和重试尤其是高峰期超时重试几乎是每天的常态。在电话转人工场景我额外用到了FreeSwitch的Park和Hold机制。识别出用户意图需要转人工时先把当前通道Park住然后向外呼坐席坐席接通后再用Bridge把两路电话接起来。这中间用户等待的语音先放等待音乐等坐席就绪再桥接整个链路不会断。这也是业务落地里很常见的一个动作。5. 常见问题与排查实录5.1 问题速查表项目上线后你会遇到一堆稀奇古怪的问题。我把实际遇到过的、以及同行踩过的高频问题整理成了一张速查表问题现象可能原因排查方法ASR完全没有识别结果音频没有送到引擎、音频编码不对、MRCP语法配置错误先抓包确认RTP流转发是否正常再确认采样率识别出来的文本是乱码采样率不匹配、编码不是PCM、语音增益过高统一重采样为16kHz/16bit/单声道PCM调节增益识别结果首字丢失Early Media阶段就送音频导致音频被吞在dialplan设置p-early-media-support等呼叫进入稳定状态后再开始识别TTS声音断断续续网络延迟、音频帧缓冲不足、TTS分句不合理增大适配层缓冲检查RTP抖动呼叫建立后长时间无声音SIP协商问题、RTP端口被防火墙挡开放RTP端口段检查NAT穿透MRCP连接被拒绝适配代理没起、端口不通、签名过期检查代理监听状态和系统时间并发一高就开始报错配额不足、代理线程池不够、FreeSwitch session数上限加线程池、做信号量排队、查讯飞配额5.2 排障工具与实战思路排障我基本靠三件套fs_cli日志、tcpdump抓包、讯飞后台日志。fs_cli里最常用的是console loglevel debug把FreeSwitch日志调到debug级别后MRCP模块的细节都会打出来。出问题先看这里基本能定位到是模块没起来、profile配置错了还是RTP流转发断了。tcpdump抓包主要看SIP信令和RTP音频流。抓包命令比如tcpdump -i any -s 0 -w capture.pcap host 你的讯飞适配代理IP and port 5090然后用Wireshark打开筛选rtp或rtsp协议。要注意RTP包是否持续发送如果FreeSwitch没把RTP转给代理那识别端肯定收不到语音问题就出在媒体协商环节。讯飞后台要重点看请求响应时间和返回码。讯飞开放平台会给你打印请求日志识别报错码30001、30002这类问题通常是因为鉴权失败或者参数不对直接查对应错误码说明就行。5.3 一个困扰很久的p-early-media-support问题再单独讲一下早期媒体导致识别首字丢失的问题。电话呼叫流程是这样的FreeSwitch收到用户来电后会有个early media阶段也就是正式应答200 OK之前可能已经启动了媒体传输。如果在这个阶段就开始采集音频送识别首字往往会被吞掉因为识别引擎还没就绪。FreeSwitch里有个参数叫p-early-media-support它控制的是代理应答能力。在对接ASR时建议把呼叫设置为先应答再识别也就是确保呼叫进入ACTIVE状态后再触发识别流程。我在dialplan里显式加了answer操作并关闭了对早期媒体的MRCP请求这样再没出现过首字丢失问题。5.4 补充跟其他ASR方案的边界经常有人问网上说的ASR的AT指令和这个是不是一回事。这里澄清下AT指令识别通常是指4G模组或嵌入式设备通过串口AT命令做离线语音识别属于端侧方案。而我们做的是电信级通话链路上的云端实时识别走的是MRCP/RTSP和WebSocket两者技术栈完全不同。如果你对本地离线识别也感兴趣可以考虑在树莓派上跑轻量级ASR模型比如sherpa-onnx或用Coqui TTS做本地合成作为离线兜底。但电话场景对实时性和并发要求高本地小模型的表现跟云端大模型还是有差距成本敏感的小项目可以试试生产环境建议还是优先走云端。最后分享一点实战体会这套系统从零到上线我最想强调的一点是不要试图一步到位先把链路跑通再谈优化。我一开始就想着把对话系统、讯飞、FreeSwitch全部高级功能一起上结果排查问题时根本不知道是哪个环节出了问题。后来改成最小闭环先单向ASR测试再单向TTS测试再把两者串起来最后再接对话系统每一步都验证稳定了再往前走反而效率最高。另外音频链路的稳定性决定整个系统的成败。很多识别不准、合成卡顿的问题最终的根因都在RTP音频流没有处理好。先解决音频再去调识别模型参数你会有一种豁然开朗的感觉。最后再多说一句MRCP项目不要闷头自己写代码多去看看unimrcp的官方文档和代码里的demo很多协议细节只有看代码才能理解透彻。讯飞的API文档也要翻到细节那一层尤其是鉴权和流式的边界情况。这两个基础打牢了后面什么语音引擎都能从容接进来。