从零设计一个通用智能电话平台:FreeSWITCH + ESL + AI 实战架构

12 阅读11分钟

从零设计一个通用智能电话平台:FreeSWITCH + ESL + AI 实战架构

很多团队做“AI 接电话”,第一版是把机器人直接塞进 FreeSWITCH 拨号计划。Demo 能跑,但很快就会遇到:换家 ASR 要改呼叫代码,加分机要改 XML,转人工后通话莫名挂断,话单对不上。

这篇文章换个思路:先设计通用的电话平台,再让 AI 以插件方式接进来。 下面挑出最关键、最容易踩坑的部分。

一、场景与目标

平台覆盖三类场景:客服接听(机器人先接,复杂问题转人工)、外呼回访/通知(回访、提醒、预约确认)、人机协同(机器人带摘要转人工,坐席处理完再转回机器人做满意度调查)。

目标是五个“能”:能打通(呼入呼出、可打断)、可插拔(换机器人或供应商不改呼叫代码)、能运营(配置在管理台改,不登服务器)、能验证(浏览器就能测全链路)、能扩展(单机平滑扩到集群)。

二、分层架构:让 FreeSWITCH 只做执行者

通用智能电话平台分层架构

整体分五层:

层做什么
接入层运营商中继、SIP 话机、浏览器软电话;集群期加 Kamailio 做注册和分发
媒体层FreeSWITCH:信令、媒体、转码、放音录音、桥接
控制层配置服务(xml_curl)、呼叫控制引擎(ESL)、排队与外呼调度
AI 接入层AI 接入网关 + 外部机器人或 ASR/TTS/LLM 适配器
业务层管理台、坐席工具条、开放 API、质检报表

核心原则只有一句:FreeSWITCH 只负责信令、媒体和执行命令;配置放数据库,决策放在 ESL 程序里,AI 通过统一协议接入。

配套原则:只有控制引擎持有 ESL,AI 要打断或转人工就发事件给它,不直接碰通道;异常时要么挂机要么回到等待,不留僵尸通道;全链路一个 call_id,从通道变量带到日志、AI 请求和话单。

三、配置数据库化:mod_xml_curl + 一键发布

改 XML 再 reloadxml 的做法,分机和节点一多就难管。mod_xml_curl 的思路是:FreeSWITCH 需要配置时,发 HTTP 请求向你的服务要。

<param name="gateway-url" value="http://10.0.0.11:8080/fs/xml"
       bindings="directory|dialplan|configuration"/>

配置服务按请求里的 section 分发:directory 返回分机,dialplan 返回拨号计划,configuration 再按 key_value 区分网关、ACL 等。几个要点:

  • 查不到就返回 <result status="not found"/>,不要返回空内容;
  • 分机密码下发 a1-hash,不下发明文。

静态与动态的边界:全局变量、模块列表、profile 骨架、ESL 配置放静态 XML;分机、网关、路由、ACL 放数据库。静态 XML 里还要留一个兜底拨号计划:配置服务挂了时播放“系统繁忙”,而不是让客户听到一片寂静。

配置数据库化前后对比

入库后分两类:

  • 路由和分机:保存即生效。 来电或注册时实时拉取,下一通电话、下一次注册就用新配置。如果开了 cacheable,保存后由程序自动执行 xml_flush_cache。
  • 网关和 ACL:要点「发布」。 网关是 FreeSWITCH 主动去运营商注册的,分机则是话机来注册,改了不会自动生效。

配置发布流程

点发布时先校验,再通过 ESL 执行命令:新增网关用 sofia profile external rescan,修改或删除先 killgw 再 rescan;ACL 用 reloadacl。再检查网关是否 REGED,成功标记“已发布”,失败提示报错。多节点规模大了,再考虑灰度。

四、呼叫控制:统一 park,ESL 接管

所有电话先 park

拨号计划不再写复杂的路由,只返回一个统一入口:

<action application="set" data="vr_call_id=${uuid}"/>
<action application="set" data="park_timeout=20"/>
<action application="park"/>

控制引擎通过 ESL(inbound 模式)收到 CHANNEL_PARK 后,按号码、日程、坐席状态决定去机器人、IVR 还是人工。呼入、外呼、转接走同一套状态机;park_timeout 防止引擎故障时通道一直挂着。

事件按通话分配到固定处理单元

一通电话的 ANSWER、PLAYBACK_STOP、HANGUP 如果并发处理,很容易乱序。可靠做法是:固定 N 个 worker,按通话 ID 哈希分配,同一通电话永远在同一个 worker 里串行处理,不同电话并行。

key = ev.get("variable_vr_call_id") or ev["Unique-ID"]
await queues[hash(key) % N].put(ev)   # 有界队列,满了就背压或丢弃非关键事件

用整通电话的 call_id 做键(没有时退回 UUID),A、B 两条腿的事件也不会互相抢。

几个必知的命令组合

  • 外呼:应用预先生成 origination_uuid,先写缓存再发 bgapi originate ... &park(),接通后由程序决定下一步;
  • 转人工:桥接前设置 park_after_bridge=true,一方挂断后另一方回到 park,不会被连带挂掉;
  • 拆桥转回机器人:uuid_transfer <uuid> -both 'set:park_after_bridge=true,park:' inline;

外呼流程

Redis 状态别忘了 TTL

通话状态存 Redis,必须设置过期时间。ESL 断线期间漏掉挂机事件,没有 TTL 就会留下永久脏数据。ESL 重连后立刻用 show channels 和 Redis 对账:Redis 有、FreeSWITCH 没有的,补一张“异常结束”话单再清理。断线重连用指数退避(1s、2s……最多 30s),别用每分钟轮询。

五、可插拔 AI 接入:一套协议接万家

媒体怎么出来

FreeSWITCH 到 AI 这一段用 mod_audio_stream:uuid_audio_stream <uuid> start <ws地址> mono 8k <元数据>,把通话音频以 PCM 帧推到 WebSocket。需要注意:社区版的下行是“服务端回 base64 音频,模块落成文件再播放”,真正的双向流式是商业版功能。所以 MVP 建议按句返回、逐句播放,第一句尽量短。

统一协议 + 两种接入模式

平台提供一个 AI 接入网关,把 FreeSWITCH 的媒体转换成统一的 WebSocket 协议:

方向消息作用
平台 → 机器人session.start / session.end会话开始/结束,带场景、音频格式
平台 → 机器人二进制 PCM、dtmf、playback.finished用户声音、按键、播放进度
机器人 → 平台response.audio、transcript.final播报音频、识别文本
机器人 → 平台response.cancel打断:平台立即 uuid_break
机器人 → 平台action.transfer / action.hangup转人工(带摘要)/ 挂机

接入方有两种选择:

  • 整机接入:自带 VAD、ASR、LLM、TTS 的机器人,实现这套协议就能上电话线路;
  • 组件接入:不想自建机器人,就由平台内置编排调用 ASR、TTS、LLM 适配器,换供应商只换适配器。

可插拔 AI 接入:统一协议 + 两种接入模式

我开源的 voice-robot 就是“整机接入”的示例。它原本是浏览器语音机器人(腾讯 ASR、火山方舟、火山流式 TTS),已具备流式输出和 cancel + generation_id 打断。接电话时只需加一层协议转换:服务端补上原本在浏览器做的 VAD,自动提交每一轮,把 generation_id 映射为 response_id。

呼入 + AI 对话时序

打断与延迟

打断的关键是:用户开口持续超过阈值(如 200~400ms,需调参)或识别出非空文字时,控制引擎执行 uuid_break <uuid> all,同时取消当前这轮的 LLM 和 TTS。“嗯、好”这类附和语可以配置为不打断,合规声明可以标记为不可打断。

延迟方面,句尾检测、ASR、LLM 首字、TTS 首包加起来粗略估算 1~3 秒,需实测;靠全链路流式、首句短、固定话术预合成来压缩。电话是 8k 窄带,优先选 8k 电话 ASR 模型。

六、浏览器 WebSocket 软电话:系统验证的入口

打开网页就能登录分机、拨打和接听,是验证全系统最省事的方式。

选型:SIP over WSS + WebRTC

FreeSWITCH 有两条路:Sofia 的 ws/wss 端口(标准 SIP,客户端用 SIP.js 或 JsSIP),或 mod_verto(私有 JSON-RPC)。推荐前者:标准协议,客户端成熟,集群期可直接放到 Kamailio 后面。

FreeSWITCH 配置要点

  • 证书:wss-binding 默认 :7443,证书放 wss.pem。没有证书时 FS 会自动生成自签证书,但浏览器会拒绝连接。推荐用域名申请正式证书;只有 IP 时,Let's Encrypt 已能签发 IP 证书,但有效期只有约 6 天,必须自动续期。
  • 公网地址:云主机上 ext-sip-ip、ext-rtp-ip 要设成公网 IP,否则 SDP 里是内网地址,表现为单通或无声。domain 也别用默认的内网 IP,否则注册认证会失败。
  • 加密与穿透:WebRTC 强制 DTLS-SRTP;企业网络封 UDP 时需要 TURN(如 coturn),TURN 凭证由后端按登录动态生成。
  • 编解码:浏览器用 OPUS,运营商中继多为 PCMA/PCMU,由 FreeSWITCH 转码,记得压测 CPU。
  • 端口:开放 7443 和 RTP 端口段;8021(ESL)绝不对公网开放。

身份与两条 WebSocket

浏览器不保存固定 SIP 密码:坐席登录平台后拿到临时 SIP 密码(存 Redis,有效期与登录会话一致),注册时 FreeSWITCH 通过 xml_curl 查询,平台根据登录态返回 a1-hash 或拒绝。登出时执行 flush_inbound_reg,让注册立即失效。

坐席页面上有两条 WebSocket:信令 WebSocket 连 FreeSWITCH,负责注册和通话;业务 WebSocket 连平台,负责坐席状态、来电弹屏、置忙置闲。规则是“SIP 已注册 + 业务连接在线 + 状态空闲”三者同时满足才分配来电,并且先弹屏再振铃。

浏览器坐席通话时序

怎么验证

环境搭好后,先用两个浏览器分别登录 1000 和 1001 互拨,确认双向有声;再从外线打进来,看浏览器坐席能否先弹屏后振铃;最后走一遍“机器人接听 → 转人工”。这三步都通了,说明注册、媒体、路由和 AI 接入整条链路是通的。

七、IVR 与话单

树形 IVR:解释执行,而不是生成配置文件

常见的 IVR 配置是一棵树:每个节点有名称、按键、话术、处理方式(进入下级菜单、转坐席、转分机、收号,比如身份证后四位),* 重听、# 返回上级。节点表核心字段:

ivr_node(id, flow_id, parent_id, node_name, dtmf_key, prompt_text,
         action_type, action_target, collect_cfg, timeout_sec, max_retries)

点击“应用 IVR”时:校验按键是否冲突、目标是否存在,预合成话术音频,生成一个不可变版本。执行方式推荐由 ESL 程序解释执行:控制引擎逐节点下发 play_and_get_digits,根据按键走下一节点。相比生成 ivr.conf,它能直接转排队、转机器人、记录按键轨迹,发布不用 reload;ivr.conf 可留作兜底菜单。

注意一个小冲突:收号节点里 # 是结束键,可以约定“不输入数字直接按 #”才算返回上级。

billsec 和 duration 的区别

话单列表里最容易被问的就是“通话时长”和“持续时长”:

  • duration(持续时长):从开始呼叫到结束,包含振铃,未接通也有值;
  • billsec(通话时长):从接通到结束,是计费时长,未接通为 0。

还有一个坑:呼入时 IVR 或机器人一接听,客户这条腿就算“接通”了,所以客户腿的 billsec 包含了 IVR、机器人对话和排队时间。坐席的通话时长应该取坐席腿的 billsec。另外一通电话至少两条腿,统计前要先判断主腿,否则数量翻倍;话单经消息队列异步写库,并与原生话单对账。

八、安全坑点:默认密码 1234

FreeSWITCH 默认配置自带 1000~1019 共 20 个演示分机,密码都是 vars.xml 里的 default_password,官方默认值是 1234。服务器有公网 IP 时,扫描器很快就会找上门,注册成功就能用你的线路往外打,账单可能一夜暴涨。

必做清单:

  • 上线前修改默认密码;改用 xml_curl 下发分机后,删除静态目录里的演示用户。否则 xml_curl 返回“查不到”时,FreeSWITCH 会回退到静态配置,旧密码依然有效;
  • ESL 只监听内网并配置 ACL,xml_curl 接口要鉴权,日志里不要打印凭据;
  • 默认禁止国际、高费率号段,单分机、单租户限制并发和每日呼出量,异常呼出立即告警并自动停用;
  • 外呼要有用户同意凭证、黑名单和时段控制。按工信部规定,呼叫中心呼出需留存不少于 30 日的录音和同意凭证,不允许商业营销类呼出,具体以法务意见为准。

写在最后

这套设计的关键选择就三个:配置进数据库,决策进 ESL 程序,AI 走统一协议。做到了,换供应商、加节点、改流程都不用动 FreeSWITCH。

想动手的话,可以拿 voice-robot 加一台 FreeSWITCH,从“浏览器登录分机、拨通机器人、说话打断它”开始。欢迎评论区交流踩过的坑。