
先说结论这个项目做到最后你会发现真正难的不是通义千问怎么接而是FreeSWITCH这边怎么把语音流、会话状态、打断逻辑和LLM的对话能力缝在一起。我前后折腾了两周多才跑通一个能稳定拨打、能听懂人话、回复不跑偏的“大模型呼叫中心”雏形。这篇文章把整个思路、架构、踩坑过程都摊开讲给想动手做同类项目的朋友一条能直接照抄的路径。大模型呼叫中心本质上是把传统的IVR按键导航替换成“自然语言对话”入口。用户打进来不用再听“请按1、请按2”而是直接说“我要查余额”“我忘密码了”系统识别意图、调用对应能力、用语音回复。这一套链路里FreeSWITCH负责电话接入和媒体处理通义千问负责理解和生成中间还有ASR和TTS两个环节把语音和文本互相转换。项目适合有一定FreeSWITCH基础、想往AI方向转型的通信开发者也适合企业里想低成本验证“AI客服能不能用”的技术负责人参考。1. 项目整体设计与技术选型1.1 为什么核心底座选FreeSWITCH市面上能做呼叫中心底座的方案不少Asterisk、Kamailio、自研SIP网关各有拥趸。但这个场景我坚持用FreeSWITCH理由很直接它的媒体处理能力和事件驱动模型跟大模型对话的“一问一答”节奏天然匹配。FreeSWITCH最核心的优势是媒体控制权和事件机制的分离。通话过程中的音频流转发、录音、放音、编解码转换都由FreeSWITCH统一管理而上层业务逻辑可以通过Event SocketESL或嵌入式脚本Lua、Python实时监听和干预。这意味着LLM不需要碰任何底层信令只需要通过HTTP接口接收“用户说了什么”然后返回“系统该说什么”媒体交互完全交给FreeSWITCH去执行。另外一个重要原因是FreeSWITCH对语音编码和音频格式的处理非常成熟。无论是PCM、Opus还是G.729它都能直接处理这对接ASR和TTS非常关键。ASR识别的音频格式、TTS生成的音频编码经常跟电话线路默认格式不一致FreeSWITCH的sox和convert模块可以自动转码省去大量格式适配工作。1.2 通义千问在大模型选型里的位置对话模型的选型我做了对比测试最终在通义千问qwen-plus和本地部署的开源Qwen2.5系列之间来回切换两种方式我都跑通了。选通义千问的核心理由有四个第一中文对话质量稳定。呼叫中心场景对口语化表达、数字播报、常见业务问询的理解要求很高。千问系列在中文语义理解上明显有优势特别是对“余额”“挂失”“人工客服”这类金融/通信业务术语的识别准确率比较高。第二接口简单、生态成熟。通义千问的OpenAI兼容接口直接可用SDK覆盖主流语言这对接入调试省了非常多事。我甚至不需要额外的封装库直接用HTTP请求就能完成对话调用。第三支持流式输出首字延迟低。呼叫中心场景最怕用户等太久如果TTS要等LLM全部生成完才开始播报体验会非常差。通义千问的流式输出可以边生成边交给TTS首字延迟能压到1秒以内这在实际通话中体验差异性非常大。第四兼容开源部署路径。项目后期如果想完全本地化运行可以直接切到Ollama或vLLM部署的Qwen2.5-7B/14B模型API形式不变只需要改base_url。这给项目保留了很强的灵活性前期用API验证效果后期按合规或成本要求做私有化部署切换成本极低。1.3 整体架构与媒体流路径整个系统的数据流可以用一句话概括用户在电话里说的话变成文字进大模型大模型生成的文字变成语音播给用户听。拆开看核心节点如下接入层FreeSWITCH负责SIP注册、呼入呼出控制支持PSTN网关或WebRTC接入。ASR模块接收用户语音输出识别文本。我用的是FunASR的Paraformer流式模型也可以换成Whisper或阿里云实时语音识别API。LLM模块通义千问对话接口接收ASR文本和上下文历史输出回复文本可附带工具调用指令。TTS模块把LLM回复文本转成语音文件我用的是CosyVoice和Edge TTS双方案备选。会话状态机负责管理当前通话在“播报—监听—识别—回复”哪个阶段同时维护多轮对话的上下文。这里重点提一下为何把ASR单独拆出来而不是直接用通义千问的多模态能力。大模型呼叫中心里的ASR不只是“识别文字”还要处理电话线路的带宽限制、噪声、口音、打断重说等场景。专用ASR模型在这些方面比通用大模型的表现好得多而且流式识别可以在用户说完之前就给出中间结果让系统更快响应。2. 核心细节解析与实操要点2.1 FreeSWITCH的拨号计划与媒体控制逻辑FreeSWITCH的拨号计划Dialplan是控制通话流程的“交通规则”。大模型呼叫中心与传统IVR最大的区别在于传统IVR是固定分支流程而大模型呼叫中心是动态对话循环。传统IVR的Dialplan长这样先播放一段菜单等待用户按键根据按键跳转到不同分机或功能模块。而大模型呼叫中心需要的是一个“对话循环”播放欢迎语 → 录音/识别用户输入 → 调用LLM生成回复 → TTS播报 → 再次进入等待状态如此循环直到通话结束。我实际操作中最稳定的方案是把Dialplan写得尽量简单把复杂逻辑全部放到Lua脚本里。在public.xml或default.xml里只留一个入口所有呼入号码都指向同一个extension然后调用Lua脚本。这样后续修改对话流程不需要重启FreeSWITCH改完Lua文件立即生效。示例Dialplan配置如下extension nameai_callcenter condition fielddestination_number expression^(9527)$ action applicationanswer/ action applicationlua dataai_ivr.lua/ /condition /extension关键点在于answer动作必须放在执行Lua之前否则音频通道没建立后续所有播放和录音操作都会失败。另外号码9527是自己定义的测试接入号实际生产环境建议用单独的DID别跟公司现有的分机号段冲突。2.2 对话循环的完整状态转换进入Lua脚本后整个通话逻辑由状态机驱动。我设计了以下状态INIT初始化会话生成一个唯一的session_id清空对话上下文。GREETING播放欢迎语比如“您好欢迎致电AI服务中心请问有什么可以帮您”LISTENING播放提示音后开始录音等用户说完或静音超时。RECOGNIZING把录音文件交给ASR识别得到文本。THINKING把文本发给通义千问等待回复这可能包含工具调用。SPEAKING把回复文本转成TTS音频播放给用户。END结束通话或转人工队列。这七个状态是串行执行的但实际运维中我强烈建议把状态切换的日志打全特别是带上session_id和时间戳。大模型响应慢、TTS生成失败、ASR超时这些异常都能靠日志快速定位。状态转换代码核心逻辑如下-- 核心对话循环 while session:ready() do -- 1. 播放欢迎语/提示音 session:streamFile(greeting_wav) -- 2. 等待用户说话录音到文件 session:record(output_wav, 20, 0, 2000, 6000) -- 3. 调用ASR服务上传录音文件 local text asr_recognize(session_id, output_wav) -- 4. 调用通义千问传入当前文本和上下文 local reply, tool_call llm_chat(session_id, text) -- 5. 检查是否需要工具调用查询订单/转人工等 if tool_call human then session:execute(transfer, agent_queue) break end -- 6. 调用TTS将回复文本合成语音 local reply_wav tts_synthesize(reply) session:streamFile(reply_wav) -- 7. 回到等待继续循环 end这段逻辑看起来简单但真正跑起来会发现录音的时机把握最考验功力。如果录音启动太早会把系统播报的尾音录进去导致ASR识别到多余内容启动太晚用户开头几个字就丢了。我用的是record命令的静音参数来触发自动停止但录音前的等待时间要根据线路实测调整。2.3 ASR与TTS的选型和接法ASR环节我推荐FunASRParaformer-large作为首选。原因有三个中文识别准确率高、支持流式识别、完全本地化部署免费商用。3.5G显存就能跑一台192.168.1.20的GPU服务器就能扛住几十路并发。如果公司本来就有云服务预算用阿里云实时语音识别也可以但要注意上传音频格式一般要求8kHz或16kHz的PCM/WAV。TTS环节就一个要求声音自然到用户听不出是机器。别用那种机械感特别明显的旧版TTS电话里放出来用户体验非常差。我试过Edge TTS、CosyVoice和ChatTTS最终稳定用的是CosyVoice因为它的语气停顿和韵律更接近真人而且支持流式合成可以在LLM输出过程中预生成语音片段。TTS生成后有个细节必须注意音频的采样率和编码格式要跟FreeSWITCH播放通道匹配。FreeSWITCH默认使用8kHz电话质量PCM而大多数TTS默认生成的是24kHz或44.1kHz的音频。直接streamFile会导致播放速度翻倍或声音变调。我踩过这个坑后来在生成TTS后强制转成8000 Hz, 16-bit, mono的WAV格式问题才解决。# 将TTS生成的24kHz音频转成8kHz电话质量 ffmpeg -y -i tts_output.wav -ar 8000 -ac 1 -sample_fmt s16 tts_output_8k.wav另外提醒一下TTS音频缓存非常重要。同一个回复文本如果重复被查比如用户反复问同一件事每次都重新调TTS又费时间又费算力。我做了个简单的MD5文本到音频文件的缓存目录命中直接播文件实测能省掉70%的TTS调用。2.4 上下文管理与多轮对话体验大模型呼叫中心最容易被忽视的是上下文管理。如果你每轮对话都把全部历史发给LLM对话超过五轮就会触发Token长度限制而且模型容易被旧话题带偏。我的做法是维护一个滑动窗口只保留最近六轮用户输入和系统回复超过六轮就把最早的丢弃。还有一个隐藏技巧在系统提示词里注入当前时间、用户主叫号码、历史服务记录。比如用户之前查过余额下一轮他再问“那能查明细吗”模型结合上下文就知道“那”指的是“余额明细”。system_prompt f 你是某企业客服助手请用自然、简洁的方式回答用户问题。 当前时间2025年6月18日 星期四 14:30 用户主叫号码{caller_id} 历史对话摘要{summary} 回答要求不超过80字直接给出结论。 若用户情绪激动要求转人工或问题超出能力范围回复“正在为您转接人工客服请稍候”同时触发转人工动作。 这个摘要机制是我自己做的用一个小的总结模型定期压缩早期对话内容。如果不想维护这么大的系统直接保留滑动窗口也够用只是超过十轮以后模型可能忘记早期提到的关键信息比如用户名、订单号等。3. 实操过程与核心环节实现3.1 环境准备FreeSWITCH的安装与基础配置我是在一台16核32G内存的Linux服务器上跑的磁盘用SSD因为录音文件和音频转码的I/O压力很大。Windows上装FreeSWITCH做开发调试也行但生产环境强烈建议Linux稳定性和并发能力不是一个级别。安装流程直接用官方源最省事Debian/Ubuntu系执行apt update apt install -y gnupg2 wget lsb-release wget -O - https://files.freeswitch.org/repo/deb/debian-release/fsstretch-archive-keyring.asc | apt-key add - echo deb https://files.freeswitch.org/repo/deb/debian-release/ $(lsb_release -sc) main /etc/apt/sources.list.d/freeswitch.list apt update apt install -y freeswitch-meta-all装完以后启动服务用fs_cli进入控制台输入sofia status确认SIP模块正常。这时先做一步验证注册一个软电话比如MicroSIP或Zoiper到FreeSWITCH然后拨打9527测一下基础通话能不能通。基础链路没通之前不要碰任何AI相关模块这是排查效率最高的路径。开发调试时有个非常有用的参数console.log级别调到DEBUG。在autoload_configs/logfile.conf.xml里把level改成debug你可以看到完整的呼叫流程、媒体协商细节、每个动作的耗时。生产环境再调回info避免日志刷爆磁盘。3.2 FIFO队列与转人工机制真实呼叫中心不能只有AI机器人转人工是刚需。FreeSWITCH的FIFO模块实现了排队功能我配置了一个agent_queue队列坐席用的是预配置的SIP分机。extension nameagent_queue condition fielddestination_number expression^(8900)$ action applicationanswer/ action applicationfifo datasupport_queue in/ /condition /extension转人工动作在Lua脚本里执行当通义千问返回的回复中包含“转人工”意图时调用session:execute(transfer, 8900)把当前通话转到排队队列。FreeSWITCH会自动分配队列中空闲的坐席如果坐席忙则播放等待音乐。这里有个体验细节转人工之前最好通过TTS播“正在为您转接人工客服请稍候”然后等语音播完再执行transfer。如果立即转移通话用户可能只听到半句话就没了体验非常突兀。另外FIFO队列可以设置超时策略比如30秒无人接听就挂断或转语音信箱。我用的是fifo_moh命令给等待中的用户播放音乐避免用户以为通话断了。3.3 通义千问接入的两种方式第一种方式最直接用OpenAI兼容REST API。通义千问提供了dashscope.aliyuncs.com/compatible-mode/v1的endpoint直接把base_url指过去API Key从阿里云控制台获取。import requests def llm_chat(session_id, user_text, context): messages [ {role: system, content: system_prompt}, {role: user, content: f用户输入{user_text}} ] # 加上滑动窗口内的历史对话 for item in context: messages.append({role: item[role], content: item[content]}) resp requests.post( https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions, headers{Authorization: fBearer {API_KEY}}, json{ model: qwen-plus, messages: messages, temperature: 0.7, stream: False }, timeout30 ) return resp.json()[choices][0][message][content]第二种方式是本地部署开源Qwen模型用Ollama或vLLM。我推荐先用Ollama做快速验证一条命令就能拉起千问模型服务但生产环境并发高的话建议上vLLM吞吐能力和显存利用率好很多。本地部署切换也非常简单只需把Python脚本里的endpoint从阿里云改成http://localhost:8000/v1其他代码完全不动。这条路径的价值在后期非常明显特别是企业有数据合规要求不允许把对话内容传到外部API时。3.4 打断检测让用户随时插话大模型回答往往比较长TTS播放可能要几十秒用户可能中途就想打断说“不是这个意思”“你错了”。如果系统不理会打断继续播完体验会非常差。纯靠FreeSWITCH层面实现打断有两种做法一是检测DTMF信号用户按任意键打断二是实时检测音频能量。我只实现了DTMF打断效果够用但不够自然。后来用媒体Bug方案监听用户侧音频检测到超过阈值的音量波动就触发打断事件才算真正实现“插话打断”。实现思路是在播放TTS的同时启动一个uuid_audio监听用户输入音频。检测到能量突刺后立即执行session:break中断当前播放然后马上启动录音把用户后半句话录下来。打断功能最麻烦的是误触发。用户咳嗽一声、环境噪声过大都可能导致系统误以为在插话频繁打断播放录音用户也会被搞疯。我的经验是把能量阈值调高一点同时要求连续200毫秒超过阈值才触发打断。宁可漏掉一次真实打断也不要频繁误触发。3.5 语音活动检测与静音超时录音时需要判断用户什么时候说完。FreeSWITCH的原生record命令支持静音检测但参数需要根据实际线路调优。session:record(file_path, max_time, sil_threshold, sil_seconds, min_seconds)的四个关键参数我实测下来的推荐值max_time20单次录音最长20秒。超过20秒用户还没说完强制结束录音。实际业务中大多数问询在10秒内能说完20秒足够。sil_threshold200静音判定阈值值越小越灵敏。200是经验值如果背景噪声大就调高到300否则系统永远检测不到静音超时。sil_seconds2连续静音2秒认为用户说完。min_seconds3最短录音时长3秒。如果用户只是说了一个“嗯”3秒保护时间能避免把这个语气词当成完整输入。调试经验先让同事在安静环境打测试电话看日志里录音文件的时长和静音检测是否正常然后在有背景噪声的环境再测一遍微调阈值。4. 常见问题与排查技巧实录4.1 常见问题速查表做这个项目过程中我把踩过的坑和解决办法整理成了一张表基本覆盖了这个项目能遇到的大部分常见问题。现象可能原因解决办法拨打测试号码无法接通Dialplan未生效或Sofia模块异常检查fs_cli中sofia status是否正常号码是否匹配Dialplan正则呼入后无欢迎语音音频格式不兼容或TTS转码失败确认WAV格式为8000Hz、16bit、单声道用sox检查文件头ASR识别结果空白或乱码录音文件采样率太高或太低统一转成8000Hz/16000Hz电话线路用8000HzLLM返回速度慢用户等待过长网络延迟或模型参数太大改用qwen-turbo做首轮快速响应或用本地模型用户一句话被截断录音静音参数太灵敏调高sil_threshold增加min_secondsTTS播放时声音变快变尖采样率不匹配44.1kHz直接在8kHz通道播放播放前强制转码或使用set命令指定playback_rate8000打断功能失效媒体Bug未挂载或监听通道不对确认uuid_audio是监听用户输入通道不是播放通道用户转人工后坐席听不到声音媒体协商问题或转接后没有桥接媒体检查SIP中继的inbound-proxy-media配置并发高时FreeSWITCH内存暴涨录音文件未及时删除Lua脚本中录音结束立即删除临时WAV文件4.2 排查实录一次“ASR永远识别不到第一句”的问题项目调试到第三天我遇到了一个特别诡异的问题用户拨打进来播放欢迎语然后用户说话录音文件生成也正常但ASR返回的永远是空字符串。我一度怀疑是FunASR服务挂了但单独上传录音文件测试完全正常。后来逐步排查发现问题出在录音文件的前导静音被截断上。FreeSWITCH的record命令在检测到语音开始前的静音段时会把静音“吃掉”一部分但我设置的sil_threshold偏小导致录音文件开头直接是用户说话的中间部分ASR拿到的是一个“半截话头”。解决办法很粗暴但也有效在ASR之前给录音文件加一个固定的500ms前置静音。这相当于给识别器一个干净的起始标志让它更准确地切分语音起始点。ffmpeg -f lavfi -i anullsrcr8000:clmono -t 0.5 -q:a 9 silence.wav ffmpeg -i user_input.wav -i silence.wav -filter_complex [0:a][1:a]concatn2:v0:a1 prepend_silence.wav加了这500ms静音后ASR识别准确率瞬间提升了一个档次。4.3 大模型响应延迟优化的三个技巧呼叫中心场景对延迟的容忍度很低超过3秒用户就会不耐烦。实测下来通义千问接口第一次调用通常要2~4秒这里面有网络开销和模型推理时间。我做的第一个优化是对话预热。通话接通后播放欢迎语的同时并行向通义千问发送一个“在线测试”的请求把连接池提前激活这样等用户真正提问时模型正好处于活跃状态响应时间能缩短30%以上。第二个优化是流式输出加TTS预生成。通义千问支持streamTrue模式文本逐字吐出TTS模块边接收边合成不需要等完整回复生成完再播。实测首字延迟能从2.5秒压到0.8秒体验已经完全达到商业客服机器人水平。第三个优化是模型分级。我配置了两个模型qwen-turbo做第一轮快速响应qwen-plus做后续复杂问题的深度回答。简单问询用turbo足够只有涉及多轮推理、工具调用时才切到plus。实测成本能降一半速度更快。4.4 park/hold与呼叫保持的配置心得呼叫中心场景有一个常被忽视但很重要的功能通话保持。用户说“稍等一下我去查个东西”AI客服需要能把当前通话临时保持过一段时间再恢复。FreeSWITCH里实现保持很简单action applicationpark datahold/但真正考验人的是保持期间的等待音乐和恢复通话时上下文不丢。我踩过一个坑用户保持后系统恢复对话时LLM的上下文还停留在“用户说要去查东西”这一步用户回来后说“我查到了”模型一脸懵因为它不知道这中间发生了什么。解决办法是在保持开始和结束时各向LLM发送一条系统消息“用户要求保持通话已等待X秒”“用户已返回通话请继续之前的话题”。这样模型就能正确拼接上下文对话体验无缝衔接。4.5 稳定性建设日志、监控与优雅降级最后讲讲生产化必须做的三件事。完整的链路日志我用一个自定义日志表记录每一通电话的session_id、通话时长、ASR识别结果、LLM回复内容、TTS合成时长、是否转人工。这些数据既能用于问题排查也是后续优化对话质量的重要素材。关键指标监控ASR识别成功率、LLM响应时间、TTS失败率、通话转人工率这四个指标我写了个脚本每隔30秒打点一次超过阈值就告警。尤其是转人工率异常飙升大概率是LLM系统提示词被干扰或者上下文管理出了问题。优雅降级通义千问API偶尔会超时或返回异常此时不能直接挂断用户电话。我在Lua脚本里加了catch逻辑一旦LLM调用失败立即播放“系统正忙我们将为您转接人工客服请稍候”然后执行转人工。用户感知到的只是“转人工”而不是“系统故障”投诉率会低很多。5. 后期扩展思路这个项目跑通以后后续扩展方向非常明确把工具调用Function Calling做实让AI客服不只是“聊天”而是能真正操作业务系统。比如用户说“帮我查一下上个月电话费”LLM识别意图后返回一个query_bill的工具调用代码侧根据用户手机号查询CRM系统把查询结果作为工具返回内容再次传给LLMLLM组织成自然语言回复。通义千问的Function Calling接口已经非常成熟只要在请求里声明tools参数模型会自动判断是否需要调用工具以及调用哪个工具。绑定现有的企业业务系统后这个呼叫中心就从“说话机器人”升级成了真正能办事的“AI坐席”。另一个扩展方向是多模态交互比如用户发短信进来也能走同一套LLM逻辑只是输入输出从语音变成文本架构上几乎不用改动。短信渠道和电话渠道共用同一套意图识别和业务逻辑只是接入层不同运营成本会大幅降低。这个项目真正的价值在于它把最前沿的大模型能力和最传统的电话通信打通了。用户不需要下载App不需要注册账号拿起电话就能享受AI服务。从技术角度看架构清晰、模块解耦每一步都可以单独替换优化从业务角度看能给企业省下一大笔坐席人力成本同时把服务时间从8小时延长到7x24小时。如果你们团队既有通信基础又有AI开发能力照着这篇文章的思路动手试一个吧。真正跑通第一通电话的时候那种把两个世界连在一起的成就感值得折腾。