把FreeSWITCH、阿里云SDMMRCP-SERVER和语音机器人这几个词放一起很多人第一反应是要去翻一堆协议文档其实真没想象中那么吓人。我最近刚把一套生产环境打通从容器准备到第一通电话真正能被机器人识别并回复前后大概就是一顿饭的功夫。这篇文章就把这条路原原本本走一遍Docker里怎么部署FreeSWITCH、怎么加载MRCP模块、怎么对齐阿里云SDM的服务端点、拨号计划怎么写再加上我在实际环境里踩过的那些容器网络坑、识别超时坑、Early Media坑一次性讲清楚。适合正在做呼叫中心外呼、语音IVR、语音机器人项目或者准备把FreeSWITCH搬进容器的同学参考。1. 先理清语音机器人的整条链路1.1 从“打电话”到“机器人听懂”发生了什么语音机器人表面上看起来就是一个会接电话的机器人但底层至少要完成“听-想-说”三条链路听用户说话系统把语音转成文字对应ASR自动语音识别。想拿到文字后根据业务规则或对话引擎决定回复什么内容对应NLU/对话管理。说把回复文字转成语音放给用户听对应TTS语音合成。传统做法是自己在应用层调ASR/TTS的HTTP API一顿开发之后还要处理音频编码、断句、VAD语音活动检测、超时重试。麻烦的点在于这些能力如果不和呼叫会话绑定媒体流的处理和业务状态机就得自己额外维护。FreeSWITCH在这里扮演的角色是“通信中枢”。它负责SIP呼叫、媒体流接入、编解码转换、放音、收号、录音。而阿里云SDM对外体现为MRCP-SERVER负责提供ASR和TTS能力。两者之间的桥梁是MRCP协议。MRCPMedia Resource Control Protocol简单理解就是一个媒体资源控制协议它让FreeSWITCH可以把音频流“发送”给远端的ASR引擎然后拿回识别文字也可以把一段文字发给TTS引擎让它生成语音流返回。所以整个会话的流程是用户打电话进来FreeSWITCH接起来把媒体流通过MRCP送到阿里云SDMSDM识别完成后把文本结果返回给FreeSWITCHFreeSWITCH根据逻辑生成回复文字再通过MRCP把文字送给SDM做合成最终把音频放给用户。整个过程对用户来说就像在和一个真人通话。这套架构最有价值的地方在于识别、对话、合成三个环节被拆开了业务逻辑不需要绑定具体ASR/TTS厂商哪天换成别的MRCP服务只需要改profile配置就行。1.2 为什么选FreeSWITCH uniMRCP 阿里云SDM这个组合可选的开源软交换不少比如Asterisk、Kamailio、FreeSWITCH语音机器人项目我更推荐FreeSWITCH。原因是FreeSWITCH的原生并发模型和媒体处理能力更强mod_unimrcp模块可以直接对接标准MRCP服务器配置方式清晰排障时日志分段准确。Asterisk当然也能做但涉及到复杂媒体协商和并发媒体转发时FreeSWITCH的调试体验更友好。uniMRCP是业内非常常见的开源MRCP客户端实现。FreeSWITCH的mod_unimrcp就是基于它开发出来的。它的作用是把FreeSWITCH发起的识别/合成请求翻译成标准MRCP协议消息再通过SIPMRCP和远端的MRCP-SERVER建立会话。这样一来FreeSWITCH不需要关心对端ASR/TTS厂家的私有协议细节只需配置好一个MRCP profile。选择阿里云SDM这样的云服务而不是自建ASR/TTS服务核心原因是成本和效果之间的平衡。自建识别引擎要准备GPU机器、训练数据、维护模型中小团队根本扛不住。公有云的MRCP服务已经把识别、合成、VAD、断句这些能力封装好了按量付费上线快。更重要的是云服务商做的语音识别在中文场景、方言场景、嘈杂环境下的表现通常比自己模型的可用性高很多。我做这个方案时最看重的一点是纯标准协议。MRCP是RFC标准协议不绑定厂商私有SDK因此后续就算换供应商也不需要重写FreeSWITCH上的业务代码。这个灵活性对长期运营的项目来说比“接口好用”更值钱。2. Docker环境下的FreeSWITCH准备2.1 镜像到底用哪个别直接拉官方默认版Docker部署FreeSWITCH最常踩的第一个坑就是镜像里没带mod_unimrcp。很多人直接docker pull freeswitch/freeswitch起来敲load mod_unimrcp发现找不到模块。原因很简单官方镜像为了保持精简很多模块默认不打包mod_unimrcp又依赖uniMRCP库编译链复杂普通镜像根本不会带。我的做法是维护自己的镜像。Dockerfile大致长这样FROM freeswitch/freeswitch:1.10 AS base # 安装编译 mod_unimrcp 所需的依赖 RUN apt-get update apt-get install -y \ unzip \ autoconf \ automake \ libtool \ libsofia-sip-ua-dev \ libspandsp-dev \ libltdl-dev \ gcc \ g \ make \ wget # 下载并编译 uniMRCP RUN wget https://github.com/unispeech/unimrcp/archive/refs/tags/v1.7.0.tar.gz \ tar zxf v1.7.0.tar.gz \ cd unimrcp-1.7.0 \ ./bootstrap \ ./configure --prefix/usr/local/unimrcp \ make make install # 下载并编译 mod_unimrcp RUN cd /usr/src/freeswitch/src/mod/applications/mod_unimrcp \ make -f Makefile.mod \ make -f Makefile.mod install # 复制 uniMRCP 配置 COPY mrcp.conf.xml /etc/freeswitch/autoload_configs/ COPY unimrcp.xml /usr/local/unimrcp/conf/这段Dockerfile只是示意版本号需要根据你用的FreeSWITCH源码版本调整。实际生产我建议直接用官方发布的“带unimrcp”标记的镜像标签或者提前在本地编译好模块文件再用COPY放进容器里。自己从头编译的好处是可以控制编译参数坏处是首次构建时间较长而且对编译环境要求高建议在构建机上用多阶段构建构建产物镜像保持精简。如果你只需要快速验证可以试试社区里别人做好的镜像但一定要确认mod_unimrcp.so确实存在并且版本和FreeSWITCH版本匹配。版本不匹配会出现模块加载后行为诡异、函数符号找不到之类的问题非常难受。2.2 启动容器时的两个关键选择第一个选择是网络模式。语音通信和Web服务不一样除了SIP的5060端口外媒体RTP要占用大量UDP端口。如果按默认bridge模式启动容器需要把几千个RTP端口全部映射到宿主机既繁琐又容易漏。我第一版就是用了bridge模式结果电话能接通但经常听不到声音排查到最后发现是RTP端口映射丢了。第二个选择是配置目录挂载。把配置文件、录音目录、日志目录挂载到宿主机是容器运维的基础操作这里特别提醒日志一定要挂出来否则容器一重建排障线索全丢了。我实际启动容器的命令参考如下mkdir -p /opt/freeswitch/conf mkdir -p /opt/freeswitch/log mkdir -p /opt/freeswitch/recordings docker run -d \ --name freeswitch \ --network host \ -v /opt/freeswitch/conf:/etc/freeswitch \ -v /opt/freeswitch/log:/var/log/freeswitch \ -v /opt/freeswitch/recordings:/var/lib/freeswitch/recordings \ -e TZAsia/Shanghai \ my-freeswitch:1.10-unimrcp--network host在容器里面直接用宿主机网络SIP和RTP都不需要做端口映射延迟也更低语音场景强烈推荐。缺点是端口隔离基本没有了但内网部署语音服务问题不大加好iptables规则就行。挂载配置目录时要注意宿主机上/opt/freeswitch/conf下的文件即是容器内的/etc/freeswitch配置。所以你可以先在宿主机放好所有配置再启动容器不用进容器改文件这对自动化部署非常友好。2.3 验证MRCP模块是否正常加载启动容器不算完真正关键的是确认mod_unimrcp加载成功。进入容器控制台docker exec -it freeswitch /usr/local/freeswitch/bin/fs_cli然后在 fs_cli 里执行module_exists mod_unimrcp如果返回true说明模块已经编译进系统。再执行load mod_unimrcp正常会看到类似“Successfully loaded”的输出。如果没有把报错信息贴到搜索里多半是uniMRCP库路径没找到或者模块版本和主程序不匹配。模块加载正常后再检查unimrcp.conf.xml里的config path配置确保指向了uniMRCP的配置目录。很多异常表现都是配置文件路径错了导致unimrcp内部跑不起来但FreeSWITCH本身不报错最后只能靠日志推断。这里有个小技巧在fs_cli执行sofia status确认SIP模块正常再执行unimrcp status看MRCP模块的profile加载情况。unimrcp status输出能看到已经加载的profile名字和状态如果profile没加载后面拨号计划里调用unimrcp应用时会直接报错“invalid profile”。3. 对接阿里云SDM(MRCP-SERVER)的核心配置3.1 配置MRCP协议栈unimrcp的ProfileuniMRCP的配置核心是Profile。一个Profile定义了一个远端MRCP服务器的连接参数SIP地址、端口、传输协议、RTP端口范围、识别引擎和合成引擎类型。对接阿里云SDM时你会在控制台拿到MRCP-SERVER的服务地址、端口和应用凭证把这些填进Profile就行。典型的/usr/local/unimrcp/conf/unimrcp.xml片段如下profile namealiyun-sdm version2 param nameclient-ip value127.0.0.1/ param nameclient-port value5090/ param nameserver-ip valueYOUR_SDM_MRCP_SERVER_ADDRESS/ param nameserver-port value5090/ param namesip-transport valuetcp/ param namertp-ip value0.0.0.0/ param namertp-port-min value40000/ param namertp-port-max value42000/ param namedtmf-type valuerfc4733/ param namespeech-synth-engine valuealiyun-tts/ param namespeech-recog-engine valuealiyun-asr/ param nameauth-header valueBearer YOUR_TOKEN/ /profile这个配置里client-ip和client-port是本机uniMRCP监听的SIP地址server-ip和server-port是阿里云SDM的MRCP端点。sip-transport设为TCP是因为MRCP over TCP比UDP稳定得多语音识别这种长连接场景TCP能避免很多丢包导致识别断流的问题。rtp-port-min和rtp-port-max定义了媒体端口范围要和防火墙策略保持一致。生产环境我建议把范围控制在1000个端口以内减少安全暴露面。如果用的是--network host这些端口就是宿主机的端口需要确保没被其他程序占用。auth-header是阿里云SDM要求的鉴权方式不同时期控制台不同有些用的http认证头有些用自定义SIP头携带appKey。实际以你开通服务后拿到的对接文档为准但原则都一样uniMRCP支持把自定义的SIP头或请求头带过去把凭证加上就行。3.2 配置FreeSWITCH的MRCP客户端配置完uniMRCP这一层还要让FreeSWITCH知道用哪个Profile去访问。这个配置在FreeSWITCH这边的autoload_configs/unimrcp.conf.xml里。configuration nameunimrcp.conf descriptionUniMRCP Client settings param namelog-level valueINFO/ param namedefault-tts-profile valuealiyun-sdm/ param namedefault-asr-profile valuealiyun-sdm/ param nameenable-early-media valuetrue/ /settings profiles profile namealiyun-sdm version2/ /profiles /configuration这里最关键的是把default-tts-profile和default-asr-profile都指向unimrcp.xml里定义的profile名。很多新手卡在这一步unimrcp.xml那边配置对了但FreeSWITCH不认因为两边的profile名称没对应上。enable-early-media这里提一句后面单独讲。这个参数涉及机器人能不能在通话接通前就开始收用户语音对体验影响很大。配置完成后重启FreeSWITCH或加载模块再次到fs_cli执行unimrcp status确认profile状态正常、引擎类型正确。我遇到过一种情况status显示profile存在但后续调用时一直报“engine not found”最后定位发现是unimrcp.xml里引擎名写错了和FreeSWITCH这边不匹配。3.3 写一个能跑的语音机器人拨号计划配置链通了之后最重要的就是拨号计划。下面是一个最简但完整可用的例子用户拨打1001机器人播报欢迎语然后开始录音识别识别结果打到日志里并读取识别文本拼接回复内容。extension namerobot_test condition fielddestination_number expression^1001$ action applicationanswer/ action applicationsleep data500/ action applicationunimrcp datasynth speak您好我是智能语音助手请简单描述您的问题。 voicexiaoyun/ action applicationunimrcp datarecognize timeout8000 maxduration10000/ log levelINFO message识别结果: ${recognize_result}/ action applicationunimrcp datasynth speak您说的是${recognize_result}。感谢您的来电再见。 voicexiaoyun/ action applicationhangup/ /condition /extensionunimrcp应用有两个核心操作synth表示TTS合成放音recognize表示ASR识别。synth里的speak参数是要合成的文字voice参数选择音色具体音色列表以阿里云SDM服务提供的为准。recognize里的timeout是等待用户开口的超时时间maxduration是单次识别的最大时长单位都是毫秒。识别完成后结果会写到通道变量里。不同mod_unimrcp版本的变量名会有差异常见的是recognize_result但也有版本叫unimrcp_result建议先用info日志把变量值打出来确认一下。我在验证阶段就吃过亏代码写对了但变量名用错日志里始终是空的排查了半小时才发现。实际生产环境这里还需要加play_and_detect_speech这类应用可以在播放提示音的同时就开始检测用户说话让交互更自然。FreeSWITCH的mod_dptools里也有play_and_detect_speech配合unimrcp模块可以做打断放音、边放边识别这是语音机器人交互体验的关键。3.4 让识别提速Early Media的处理很多语音机器人在外呼场景下有个体验问题用户接起电话的瞬间系统要等SIP 200 OK完成、answer、媒体就绪后才开始放音识别中间会有一两秒的“静默期”用户感觉像是电话没通。解决这个问题的关键就是Early Media。Early Media早媒体是指在SIP会话正式接通200 OK之前先把媒体流建立起来并开始传音。FreeSWITCH默认在answer后才会建立媒体但通过设置可以让它支持早媒体。在拨号计划或channel设置里可以用action applicationset datap-early-mediatrue/这个p-early-media参数会让被叫侧在发送180 Ringing时就把媒体通道打开。这样外呼时用户在听到振铃或者接起前的瞬间系统就能开始放音。不过要注意Early Media和MRCP识别之间有个隐藏坑如果MRCP识别引擎在早媒体阶段就启动了但实际用户还没接听或还在路由器播放彩铃识别到的会是彩铃声导致误识别。所以我的经验是早媒体放提示音没问题但ASR识别一定要在确认用户接听并且媒体激活之后再启动。一般做法是Early Media阶段只放“正在为您接通”这种公告音等真正接通后再进入识别流程。还有一种情况是FreeSWITCH配置里没有加载p-early-media支持表现为设置了变量但媒体不建立。检查一下mod_sofia是否支持以及SIP消息里183 Session Progress是否被透传。这些参数问题建议直接用sngrep抓包看SIP信令比盲猜快得多。4. 常见问题与排查技巧4.1 问题速查表我整理了在部署过程中最容易遇到的问题做成一张速查表建议收藏。症状可能原因解决办法Docker启动FreeSWITCH后没有mod_unimrcp镜像里没编译该模块换带unimrcp的镜像或自编译unimrcp status显示profile为invalidunimrcp.xml里profile配置错误检查server-ip/port/引擎名是否匹配呼叫接通但听不到提示音RTP端口没映射/防火墙拦截使用host网络检查rtp端口范围ASR识别一直超时无结果MRCP-SERVER地址不可达或鉴权失败用tcpdump抓包检查auth-header识别结果乱码编解码协商不对确保使用PCMU/PCMA部分场景强制L16合成TTS声音断续网络延迟高或RTP端口不够扩大rtp-port范围确认网络QoSDocker Desktop启动失败提示virtualization support not detected宿主机没开虚拟化BIOS里开启VT-x/AMD-V或启用WSL2这个表里的问题基本覆盖了我在测试和生产阶段遇到的大部分情况。每一项背后都有一堆细节下面挑两个典型场景详细讲。4.2 一次真实的排障识别一直超时的背后原因有一次我在测试环境对接阿里云SDM拨号计划里recognize一直返回超时日志里看不到任何识别文本。从FreeSWITCH一遍遍查配置profile也正常模块也加载了就是不出结果。后来实在没办法了在宿主机上用tcpdump -i any port 5090抓包发现FreeSWITCH和SDM之间的MRCP信令其实已经建立了但RTP媒体流一直没有从FreeSWITCH侧正常发出。最后定位到rtp-ip配置写成了0.0.0.0结果在某些版本的uniMRCP里0.0.0.0被当作有效IP用于SIP消息体导致媒体包发到了一个错误地址。改成具体网卡IP比如内网IP或容器的实际IP后问题立刻解决。这个案例说明一个问题MRCP连接建立成功不代表媒体流就通了。SIP信令走得好但RTP媒体用的是另一套端口和IP任何一个环节不一致都会导致“看起来连通实际没法用”。排查网络问题时建议同时开两份抓包一份抓SIP信令确认会话状态一份抓RTP端口确认媒体包有没有互发。只要出现“信令有、媒体无”或“媒体有、信令断”问题基本就定位了。4.3 容器环境下的隐藏坑Docker部署语音服务除了配置问题还有几个环境相关的坑值得单独说。第一个坑是容器时间和宿主机不一致。语音服务对日志时间戳和MRCP鉴权时间要求很严格阿里云SDM的鉴权如果用的临时token时间偏差大有可能会鉴权失败。我在Dockerfile或启动参数里显式指定了时区比如-e TZAsia/Shanghai并且建议同时挂载/etc/localtime到容器。第二个坑是容器的系统资源限制。FreeSWITCH处理语音媒体时CPU和内存占用波动较大如果容器设置了严格的CPU配额忙时会出现媒体抖动。建议给FreeSWITCH容器分配足够的CPU和内存尤其是并发通话多的时候--cpus和--memory不要卡太死。第三个坑是日志的循环管理。容器里的FreeSWITCH日志默认不轮转时间长了会撑爆磁盘。我在宿主机配置了logrotate定期切割容器挂载出来的日志目录。第四个坑比较隐蔽Docker Desktop在Windows/Mac上使用NAT网络时RTP媒体包会被额外做一层地址转换加上虚拟化调度的不确定因素语音延迟会比Linux本机部署高。生产环境如果在Windows上跑Docker最好尽早切换到Linux服务器或者至少用--network host模式降低网络层开销。Windows上还经常遇到Docker Desktop启动失败提示“virtualization support not detected”这种问题本质上是宿主机没有开启虚拟化或WSL2没有正确安装属于环境问题处理完再继续部署。5. 一些个人体会这套方案跑通之后我最大的感受是语音机器人的技术门槛不在FreeSWITCH也不在MRCP协议而在工程化的耐心。很多问题看着是配置问题实际是网络问题看着是网络问题实际是环境问题。排障的时候不要一个点钻到底沿着“信令-媒体-应用”三层链路逐层检查会快很多。就我个人来说有几点想特别分享先把单通验证跑通再写业务。我建议你参考文章里的拨号计划用一个测试分机拨1001先确认TTS能放音、ASR能回字再进入对话流程设计不然业务和链路耦合在一起出了问题都不知道该查哪边。把密钥和地址用环境变量或配置中心管理不要直接写死在镜像里。容器化的意义就在于可复制密钥写死在镜像里镜像一分享就等于把凭证也分享了这个坑别踩。语音机器人的体验优化功夫在识别之外。比如超时策略、拒识策略、打断策略、静音检测灵敏度这些参数调优比纠结用哪家ASR更影响用户感知。第一次调通后建议在真实电话线路上多测几轮把各种“用户不说话”“用户说得太快”“背景有噪音”的情况都覆盖一遍。最后再分享一个小技巧在生产环境正式割接前先用sngrep tcpdump录制几通完整电话的信令和媒体交互保存为pcap文件。一旦后面出了诡异问题这些录音文件就是最直观的排障依据。这个习惯帮我解决过好几次“明明代码没变但突然不好用了”的悬案。