
1. 为什么语音质检这件事值得用 FunASR 加 FreeSWITCH 来做做客服中心质检系统的朋友大概率都经历过这样的场景每天几千通电话录音躺在存储里质检员靠人耳抽听覆盖率不到百分之三等发现问题时客户早就流失了。传统方案要么买昂贵的商业语音识别授权要么用开源方案但识别准确率惨不忍睹尤其是带口音、带背景噪声的坐机录音转写出来一堆错别字质检规则根本没法跑。我这次要聊的这套组合核心思路是把FreeSWITCH当作电话接入和媒体处理的中枢把FunASR当作语音识别引擎中间用WebSocket做实时音频流的双向通道。FreeSWITCH 负责接电话、录音、放音、转接这些脏活累活FunASR 负责把音频变成文字质检规则再基于文字做关键词命中、情绪分析、语速检测。这套架构最大的好处是实时性——不用等通话结束再离线转写而是在通话进行中就能拿到识别结果质检从事后抽查变成事中干预。适合谁来读这篇内容如果你正在做呼叫中心、客服质检、电话机器人、录音分析这类项目或者你手上有 FreeSWITCH 环境想接一个中文识别引擎那这篇基本可以当作落地参考。如果你只是想了解 FunASR 怎么部署前半部分也够用。我会把踩过的坑、参数怎么调、WebSocket 怎么串、热词怎么配这些细节都摊开讲尽量让你少走弯路。需要提前说明的是下面涉及的具体配置和代码是基于我在实际项目中的做法整理的不同版本的 FreeSWITCH 和 FunASR 在细节上可能有差异你落地时以自己环境的实际表现为准。2. FunASR 在 Linux 上的部署与模型选型2.1 环境准备与依赖安装FunASR 是阿里达摩院开源的一套语音识别工具链底层依赖 PyTorch模型仓库放在 ModelScope 上。部署第一步是把 Python 环境弄干净我强烈建议用 conda 建独立环境不要往系统 Python 里塞否则后面 torch 版本冲突会让你怀疑人生。conda create -n funasr python3.10 -y conda activate funasr pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install funasr modelscope这里有个细节如果你机器上有 NVIDIA 显卡torch 一定要装 CUDA 版本CPU 版本跑实时识别会非常吃力。我实测过同样的 Paraformer 模型GPU 上单路音频的实时率能到 0.1 以下也就是 10 秒音频 1 秒内出结果CPU 上勉强到 0.8 左右并发一上来就顶不住。显卡显存建议 8G 起步如果要跑多路并发16G 更稳妥。装完之后验证一下import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出 True 和你的显卡型号说明环境没问题。如果 False检查驱动和 CUDA 版本是否匹配这一步不通过后面全是白搭。2.2 模型选择Paraformer 还是 SenseVoiceFunASR 提供了好几个模型常用的有 Paraformer-large、Paraformer-streaming、SenseVoice-Small 这几个。选哪个取决于你的场景模型特点适用场景显存占用Paraformer-large非流式精度最高离线录音转写约 4GParaformer-streaming流式低延迟实时通话识别约 3GSenseVoice-Small多语言带情感和事件检测需要情绪分析的质检约 2GParaformer-zh中文优化热词支持好中文客服质检约 3G做实时语音质检我推荐Paraformer-streaming做在线识别配合SenseVoice-Small做离线复核。原因是流式模型虽然精度略低于非流式但延迟能控制在几百毫秒内质检规则可以边通话边跑。通话结束后再用高精度模型重新转写一遍做最终归档和深度分析。模型加载代码大概长这样from funasr import AutoModel model AutoModel( modelparaformer-zh-streaming, model_revisionv2.0.4, devicecuda:0, disable_updateTrue )disable_updateTrue这个参数建议加上否则每次启动都会去 ModelScope 检查更新网络不好的时候会卡很久。模型第一次加载会自动下载到~/.cache/modelscope目录下载完大概几个 G提前留好磁盘空间。2.3 热词配置让识别结果贴合你的业务热词是语音质检里最容易被忽视但效果最明显的功能。客服通话里经常出现产品名、专有名词、人名通用模型很容易识别错。比如花呗可能被识别成花费借呗识别成借被质检规则一跑全是误报。FunASR 的热词配置通过hotword参数传入格式是词 权重res model.generate( inputaudio_data, hotword花呗 20 借呗 20 芝麻信用 15 蚂蚁森林 15, cache{}, is_finalTrue )权重范围一般是 10 到 100数值越大模型越倾向于识别成这个词。但要注意权重不是越高越好设太高会导致其他正常词汇被强行纠正。我的经验是常用业务词给 20 到 30生僻词给 50 左右具体要拿真实录音测。热词文件可以动态加载不用重启服务。你可以把热词存在数据库或 Redis 里每次识别请求前拉取最新的热词串。这样运营同学改热词不用找你发版体验会好很多。3. FreeSWITCH 侧的通话接入与媒体流处理3.1 FreeSWITCH 安装与基础配置FreeSWITCH 在 Linux 上的安装官方源和编译安装我都试过。生产环境建议用官方提供的包管理方式省事且稳定。以 Debian 系为例apt-get 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 http://files.freeswitch.org/repo/deb/debian-release/ lsb_release -sc main /etc/apt/sources.list.d/freeswitch.list apt-get update apt-get install -y freeswitch-meta-all装完之后systemctl start freeswitch启动默认配置下 5060 端口是 SIP 信令5080 是内部 SIP。如果你只是做录音质检不需要对外暴露 SIP内网跑就行。Windows 上装 FreeSWITCH 也可以但生产环境我不推荐。Windows 版本的稳定性和并发能力跟 Linux 差不少而且很多模块在 Windows 上编译有问题。如果只是本地开发调试Windows 版凑合能用装完在conf目录下改配置用freeswitch.exe启动即可。3.2 用 mod_audio_stream 把音频推给识别引擎FreeSWITCH 本身不带 WebSocket 音频推流功能需要装mod_audio_stream这个模块。它的作用是把通话中的音频以 WebSocket 协议实时推送到指定服务端同时也能接收服务端返回的音频做放音。编译安装cd /usr/src git clone https://github.com/amigniter/mod_audio_stream.git cd mod_audio_stream make make install然后在conf/autoload_configs/modules.conf.xml里加上mod_audio_stream重启 FreeSWITCH。在拨号方案里这样用action applicationaudio_stream dataws://127.0.0.1:8000/voice_socket/这行配置的意思是把这通电话的音频流通过 WebSocket 推到本机 8000 端口的/voice_socket路径。音频格式默认是 16kHz 单声道 PCM正好是 FunASR 需要的格式省去了重采样。有个坑要注意mod_audio_stream推流是单向的默认只推不收。如果你需要服务端返回音频比如放提示音要在 data 里加参数具体看模块文档。另外这个模块对 FreeSWITCH 版本有要求太老的版本编译会报错建议用 1.10 以上。3.3 通话生命周期与音频分片策略一通电话从接入到挂断音频是连续不断的流。识别引擎需要按一定节奏切分音频太短了识别不准太长了延迟高。我的做法是按静音检测切分服务端收到音频流后用 VAD语音活动检测判断哪里是说话、哪里是停顿在停顿处切一刀把这一段的音频送去识别。FunASR 的流式模型自带 VAD 能力你可以在服务端维护一个音频缓冲区每收到 600ms 的音频就喂给模型一次模型返回增量识别结果。当检测到超过 800ms 的静音时认为一句话结束输出最终结果。这个策略的好处是识别结果跟人说话的节奏对齐质检规则可以按句来匹配而不是按固定时间窗口。比如你要检测客服有没有说您好请问有什么可以帮您按句匹配就比按 5 秒窗口匹配准确得多。4. WebSocket 通道的设计与 Python 服务端实现4.1 为什么选 WebSocket 而不是其他协议FreeSWITCH 往外推音频可选的方式有几种RTP 推流、HTTP 上传、WebSocket。RTP 延迟最低但需要额外的信令控制HTTP 上传做不到实时WebSocket 是折中方案里最合适的——它基于 TCP有连接状态支持双向通信服务端可以随时返回识别结果FreeSWITCH 也能接收指令做放音或转接。WebSocket 还有一个好处是穿透性好走 80 或 443 端口基本不会被网络设备拦。如果你需要跨机房部署WebSocket 比 RTP 省心得多。4.2 Python 服务端骨架服务端我用 FastAPI 加 websockets 库来实现FastAPI 负责 HTTP 接口和健康检查websockets 负责音频通道。核心处理函数大概是这样import asyncio import json import numpy as np from fastapi import FastAPI, WebSocket, WebSocketDisconnect from funasr import AutoModel app FastAPI() model AutoModel(modelparaformer-zh-streaming, devicecuda:0, disable_updateTrue) app.websocket(/voice_socket) async def voice_socket(websocket: WebSocket) - None: await websocket.accept() cache {} buffer bytearray() try: while True: data await websocket.receive_bytes() buffer.extend(data) if len(buffer) 9600: # 约 300ms 的 16k 16bit 音频 audio np.frombuffer(bytes(buffer), dtypenp.int16).astype(np.float32) / 32768.0 res model.generate( inputaudio, cachecache, is_finalFalse, chunk_size[0, 10, 5] ) if res and res[0][text]: await websocket.send_text(json.dumps({ type: partial, text: res[0][text] }, ensure_asciiFalse)) buffer.clear() except WebSocketDisconnect: pass finally: if buffer: audio np.frombuffer(bytes(buffer), dtypenp.int16).astype(np.float32) / 32768.0 res model.generate(inputaudio, cachecache, is_finalTrue) if res and res[0][text]: await websocket.send_text(json.dumps({ type: final, text: res[0][text] }, ensure_asciiFalse))这段代码有几个关键点。chunk_size[0, 10, 5]是流式模型的参数表示每次看 10 帧、往前看 5 帧这个配置在延迟和精度之间比较平衡。cache是流式识别的状态缓存同一通电话要一直复用同一个 cache否则上下文会断。is_finalTrue在连接断开时调用把缓冲区里剩余的音频做最终识别。4.3 多路并发的资源管理一通电话一个 WebSocket 连接100 路并发就是 100 个连接。Python 的 asyncio 处理这种 IO 密集型场景没问题但 GPU 推理是串行的需要做队列管理。我的做法是搞一个推理线程池WebSocket 收到音频后丢进队列推理线程从队列取任务算完把结果通过 asyncio 的call_soon_threadsafe回传给对应的连接。这样 GPU 利用率能拉满又不会因为并发太高把显存撑爆。显存估算Paraformer-streaming 单路推理大概占 500MB 到 1GB16G 显存理论上能跑十几路并发。但实际要考虑峰值和碎片我一般按 8 路配一台 16G 的机器留足余量。5. 代理、TLS 与连接稳定性那些事5.1 Nginx 代理 WebSocket 的配置要点生产环境一般不会让 FreeSWITCH 直接连识别服务中间会加一层 Nginx 做负载均衡和 TLS 终止。Nginx 代理 WebSocket 需要显式配置 Upgrade 头location /voice_socket { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }proxy_read_timeout一定要调大默认 60 秒通话超过 60 秒没数据传输就会被断开。设成 3600 秒基本够用。另外proxy_buffering建议关掉音频流不需要缓冲开了反而增加延迟。5.2 TLS 验证策略与自签证书如果 FreeSWITCH 和识别服务不在同一台机器建议走 wss 加密。FreeSWITCH 侧有个tls-verify-policy参数控制是否验证服务端证书。内网自签证书的情况下可以设成none跳过验证但生产环境还是建议配正规证书设成in或all。自签证书生成openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj /CNvoice.internalNginx 里配上ssl_certificate和ssl_certificate_keyFreeSWITCH 侧用wss://连接即可。注意证书的 CN 要跟实际访问的域名一致否则验证会失败。5.3 断线重连与心跳机制WebSocket 长连接最怕的是中间网络设备悄悄断开连接两边都不知道。解决办法是加心跳服务端每隔 30 秒发一个 ping 帧客户端收到后回 pong。如果连续 3 次没收到 pong就认为连接已死主动关闭并重连。FreeSWITCH 的mod_audio_stream对断线重连的支持一般我的做法是在拨号方案里加一个检测如果 audio_stream 返回失败就重新执行一次。或者在服务端做兜底连接断了之后把已识别的部分结果先存下来避免数据丢失。6. 质检规则引擎与识别结果的落地6.1 从识别文本到质检规则识别出来的文本是一串字符串质检规则要基于这串字符串做判断。常见的规则类型有几种关键词命中检测是否出现投诉退款曝光等敏感词话术合规检测客服是否说了开场白、结束语、禁语语速检测统计单位时间内的字数过快或过慢都算异常静默检测检测通话中是否有超过 10 秒的静默情绪识别结合 SenseVoice 的情感标签判断客户情绪关键词命中用简单的字符串匹配或正则就行话术合规需要按句切分后逐句匹配。语速检测需要记录每句话的时间戳这个在流式识别时就要打上。6.2 热词动态更新与规则联动热词和质检规则是联动的。比如运营新上了一个产品叫星云计划那热词里要加星云计划质检规则里也要加对应的检测项。我的做法是把热词和规则都放在数据库里用一个管理后台维护识别服务定时拉取最新配置。热词更新后不需要重启模型下次generate调用时传入新的 hotword 字符串即可。但要注意热词太多会影响识别速度建议控制在 500 个词以内按业务线分组不同业务线用不同的热词组。6.3 识别结果的存储与检索每通电话的识别结果要存下来方便后续检索和复盘。存储结构建议分两层一层是原始识别结果按通话 ID 存 JSON包含每句话的文本、开始时间、结束时间、置信度另一层是质检结果按规则 ID 存命中记录。检索用 Elasticsearch 比较合适把识别文本按句索引支持全文检索和关键词高亮。这样质检员想找所有提到退款的通话一个查询就出来了比翻录音快得多。7. 实测中遇到的几个典型问题与排查思路7.1 识别结果断句混乱最开始跑的时候发现识别出来的文本没有标点一整段连在一起质检规则没法按句匹配。原因是流式模型默认不输出标点。解决办法是接一个标点恢复模型FunASR 提供了ct-punc模型可以在识别结果后面再过一遍punc_model AutoModel(modelct-punc, devicecuda:0) res punc_model.generate(inputraw_text)加上标点恢复后文本可读性大幅提升按句切分也准确了。代价是增加一点延迟但质检场景对延迟不敏感值得加。7.2 高并发下显存溢出压测的时候跑到第 10 路并发服务直接 OOM 挂了。排查发现是每个 WebSocket 连接都持有独立的 cache 和模型引用显存没释放。解决办法是把模型做成全局单例所有连接共享同一个模型实例cache 按连接 ID 隔离。另外加一个并发上限超过就排队不要硬扛。7.3 音频格式不匹配导致识别为空有段时间发现某些通话识别出来是空的查了半天发现是 FreeSWITCH 推过来的音频采样率不是 16k。原因是拨号方案里某些分支用了不同的编解码。解决办法是在audio_stream配置里强制指定采样率或者在服务端做重采样兜底。用librosa或scipy都能做但会增加 CPU 开销最好还是在源头统一格式。7.4 WebSocket 连接被中间设备重置跨机房部署时遇到过连接跑几分钟就被重置的情况抓包发现是中间的安全设备对长连接有超时限制。解决办法是把心跳间隔从 60 秒缩短到 20 秒让连接保持活跃。另外把 Nginx 的proxy_read_timeout和proxy_send_timeout都调大双管齐下。8. 一些让系统更稳的工程经验8.1 灰度发布与 A/B 测试识别模型更新或者热词调整后不要一次性全量上线。我的做法是拿 10% 的流量走新配置对比新旧配置的识别准确率和质检命中率确认没问题再逐步放量。对比指标主要看字准确率和句准确率字准确率反映整体识别质量句准确率反映断句和标点恢复的效果。8.2 监控指标要盯哪些生产环境必须监控几个核心指标WebSocket 连接数、识别延迟 P99、GPU 显存使用率、识别失败率、质检规则命中率。连接数突降说明有断连问题延迟 P99 飙升说明 GPU 扛不住了失败率上升说明模型或网络有问题。这些指标接到 Prometheus 加 Grafana配好告警基本能覆盖大部分故障场景。8.3 日志要记全但要分级识别服务的日志量很大每句话都记会撑爆磁盘。我的做法是分级INFO 级别只记连接建立、断开、异常DEBUG 级别记每句话的识别结果默认关闭排查问题时临时打开。日志里一定要带通话 ID 和连接 ID否则出了问题根本对不上是哪通电话。8.4 模型预热与优雅重启服务重启后第一次识别会特别慢因为模型要加载到显存。解决办法是启动时先跑一段测试音频做预热把模型和 CUDA 上下文都初始化好。重启用优雅方式先停止接收新连接等现有连接处理完再退出避免通话中断。这套系统我从零搭到稳定运行大概花了三周其中一半时间花在调识别准确率和排查连接稳定性上。FunASR 的识别效果在中文场景下确实能打配合热词和标点恢复质检规则的可执行性比通用方案高一个档次。FreeSWITCH 的媒体处理能力也足够成熟mod_audio_stream虽然小众但够用。如果你也在做类似的项目建议先把单路跑通再逐步加并发和规则不要一上来就追求大而全。