FreeSWITCH对接智能客服实战:分机与AI对话的高效集成方案
最近在帮公司做客服系统智能化升级,遇到了一个挺有意思的技术挑战:怎么让现有的FreeSWITCH电话系统和智能客服AI顺畅地“对话”。特别是,我们希望能让用户直接拨打某个分机号,就能和AI客服聊起来,而不是非得通过复杂的IVR菜单或者转接人工。折腾了一阵子,总算摸出了一套比较高效的集成方案,今天就来分享一下实战心得。

1. 背景与痛点:为什么传统方式“跑不动”了?
我们之前的客服系统,用户打电话进来,先听一段长长的语音导航(“业务咨询请按1,技术支持请按2…”),然后排队等人工。高峰期排队时间长,用户体验差,客服同事压力也大。引入AI客服的初衷很简单:让简单、重复的问题由AI先处理,解放人力。
但真动手了才发现,把AI“塞进”电话系统里,没那么简单:
- 呼叫路由复杂:怎么让用户拨一个特定的分机号(比如8888)就直接进入AI对话流程,而不是走原来的IVR?这需要深度定制FreeSWITCH的拨号计划(Dialplan)。
- 语音处理延迟:电话是实时语音流(RTP),而AI接口通常是HTTP/WebSocket请求。语音要先转成文本(ASR),AI处理完文本,再转回语音(TTS)播报。这个“转来转去”的过程,如果处理不好,用户会感觉AI反应“慢半拍”,对话很不自然。
- 对话状态维护困难:AI对话往往不是一问一答就结束,需要多轮。比如用户问“我的订单”,AI需要反问“请问订单号是多少?”。如何在FreeSWITCH的通话上下文中,记住当前对话到了哪一步,并且把历史信息带给AI,是个技术难点。
- 资源与性能:AI服务调用通常有延迟,如果同时很多路通话进来,FreeSWITCH的媒体线程会不会被阻塞?如何管理并发请求,避免系统被拖垮?
2. 技术选型:为什么是FreeSWITCH + mod_dptools + Lua?
面对这些痛点,我们评估了几种方案:
- 方案A:直接API调用:在FreeSWITCH外部写一个服务,通过ESL(Event Socket Library)监听通话事件,然后控制FreeSWITCH。这种方式灵活,但架构复杂,延迟高(多了一次网络通信),开发调试也麻烦。
- 方案B:使用专用中间件:有些商业的CPaaS平台或智能语音中间件提供对接方案。好处是省心,但往往黑盒、定制性差、成本高,且可能形成厂商锁定。
- 方案C:FreeSWITCH内置模块+Lua脚本:利用FreeSWITCH自带的
mod_dptools和mod_lua,直接在拨号计划里用Lua脚本处理呼叫逻辑和AI交互。
我们最终选择了方案C,主要基于以下几点考虑:
- 高效与低延迟:Lua脚本在FreeSWITCH进程内执行,直接操作通话和媒体流,省去了进程间或网络间通信的开销,延迟最低。
- 灵活与可控:拨号计划和Lua脚本完全由我们掌控,可以精细地控制呼叫的每一个环节,定制AI对话逻辑、错误处理、超时重试等。
- 成熟与稳定:
mod_dptools是FreeSWITCH的核心模块,提供了play_and_get_digits、speak、record等丰富的语音处理函数,与Lua结合能轻松实现“播放语音-收号-录音-转发”的完整流程。 - 社区与生态:FreeSWITCH和Lua都有活跃的社区,遇到问题比较容易找到资料和解决方案。
简单说,这个方案就像在FreeSWITCH这个强大的“电话交换机”内部,安装了一个轻量、敏捷的“AI对话机器人”,沟通效率最高。
3. 核心实现:一步步搭建AI对话分机
3.1 FreeSWITCH呼叫路由配置
首先,我们需要在FreeSWITCH的拨号计划(通常是conf/dialplan/default.xml)中,为AI客服分机(比如8888)设置一个路由。
<extension name="ai_customer_service">
<condition field="destination_number" expression="^8888$">
<action application="lua" data="ai_customer_service.lua"/>
</condition>
</extension>
这段配置的意思是:当有呼叫的目的地号码是8888时,就执行ai_customer_service.lua这个脚本。所有复杂的AI交互逻辑,都将在这个Lua脚本里完成。
3.2 Lua脚本实现AI接口调用
这是最核心的部分。脚本需要处理:接听电话、播放欢迎语、启动语音识别、调用AI接口、播放AI回复,并循环这个过程。
-- ai_customer_service.lua
session = require “freeswitch.Session” — 引入session对象
api = freeswitch.API() — 引入API对象
— 1. 获取当前通话会话
local ses = session:new()
if not ses:ready() then return end
— 2. 播放欢迎语
ses:answer()
ses:streamFile(“ivr/ai_welcome.wav”) — 播放预录制的欢迎语音,如“您好,我是AI客服,请说出您的问题”
— 3. 进入主对话循环
local ai_service_url = “https://your-ai-service.com/v1/chat”
local api_key = “your-secret-api-key”
local conversation_id = ses:get_uuid() — 使用通话UUID作为本次对话的会话ID,用于保持上下文
while ses:ready() do
— 4. 录音并识别为文本 (ASR)
— 这里使用mod_dptools的`record`和外部ASR服务,示例使用一个假设的`asr_transcribe`函数
local audio_file_path = “/tmp/” .. conversation_id .. “_input.wav”
ses:execute(“record”, audio_file_path .. “ 5 200”) — 录音5秒,静音200毫秒后停止
local user_speech_text = asr_transcribe(audio_file_path) — 调用ASR服务将录音文件转文本
os.remove(audio_file_path) — 清理临时文件
if not user_speech_text or user_speech_text == “” then
ses:streamFile(“ivr/no_input.wav”)
break — 如果没识别到内容,播放提示音并退出
end
— 5. 构造请求,调用AI接口
local http = require “socket.http”
local ltn12 = require “ltn12”
local json = require “cjson”
local request_body = json.encode({
session_id = conversation_id,
query = user_speech_text,
— 可以附带更多上下文,比如用户基本信息(从数据库或CRM获取)
})
local response_body = {}
local headers = {
[“Content-Type”] = “application/json”,
[“Authorization”] = “Bearer ” .. api_key,
[“Content-Length”] = tostring(#request_body)
}
local res, code, response_headers = http.request{
url = ai_service_url,
method = “POST”,
headers = headers,
source = ltn12.source.string(request_body),
sink = ltn12.sink.table(response_body),
— 设置超时非常重要,避免通话线程被长时间阻塞
create = function()
local req_sock = socket.tcp()
req_sock:settimeout(5) — 设置5秒超时
return req_sock
end
}
— 6. 处理AI响应
if code == 200 then
local response = json.decode(table.concat(response_body))
local ai_reply_text = response.reply
— 7. 文本转语音并播放 (TTS)
local tts_audio_file = tts_synthesize(ai_reply_text, conversation_id) — 调用TTS服务
ses:streamFile(tts_audio_file)
os.remove(tts_audio_file) — 播放后清理
— 检查AI是否建议转人工或结束对话
if response.action == “transfer_to_human” then
ses:execute(“transfer”, “8888 XML default”) — 转接到人工坐席分机
break
elseif response.action == “end_call” then
ses:streamFile(“ivr/goodbye.wav”)
break
end
— 否则,循环继续,等待用户下一句语音
else
— AI服务调用失败,播放错误提示并转人工或挂机
freeswitch.consoleLog(“ERR”, “AI API call failed: ” .. tostring(code) .. “\n”)
ses:streamFile(“ivr/error.wav”)
ses:execute(“transfer”, “8888 XML default”)
break
end
end
— 8. 挂断通话
ses:hangup()
脚本关键点解析:
- 会话管理:
session:new()获取当前通话对象,ses:ready()判断通话是否仍在进行。 - ASR/TTS集成:示例中的
asr_transcribe和tts_synthesize是伪函数,你需要替换成调用真实ASR(如阿里云、腾讯云语音识别)和TTS服务的代码。通常是通过HTTP API发送音频文件或文本,接收文本或音频文件。 - 超时处理:在HTTP请求中设置
sock:settimeout(5)至关重要,防止网络问题导致FreeSWITCH媒体线程无限期挂起。 - 上下文保持:我们将每次通话的UUID作为
session_id传递给AI服务,AI服务端可以利用这个ID来存储和检索本次对话的历史,从而实现多轮对话的连贯性。 - 错误处理与降级:对ASR失败、AI接口调用失败等情况都有处理,例如播放提示音或转接人工,保证服务的鲁棒性。
- 资源清理:及时删除本地临时录音文件和TTS音频文件,避免磁盘空间被占满。
3.3 对话状态管理与上下文保持
多轮对话的关键在于“记住”之前说过什么。我们的方案是:
- 客户端(Lua脚本)传递会话ID:如上所述,将
conversation_id(即通话UUID)随每次请求发送给AI服务。 - 服务端(AI客服)维护对话状态:AI服务在内存或Redis等缓存中,以
session_id为key,存储一个对话上下文对象。这个对象通常包含:- 历史对话记录(Q-A对)
- 用户已提供的槽位信息(例如订单号、姓名、问题类型等)
- 当前对话所处的节点或意图
- 上下文传递:Lua脚本除了发送用户当前语句,还可以从外部系统(如CRM)查询用户信息,一并作为上下文传给AI,使AI的回答更个性化。
4. 性能优化:让AI对话更流畅
当并发量上来后,一些优化措施必不可少:
-
连接池与请求批处理(针对ASR/TTS):
- 频繁地创建和销毁HTTP连接开销很大。可以为ASR和TTS服务维护一个HTTP连接池(在Lua中可以用
keepalive池)。 - 如果AI服务支持,可以考虑将短语音打包进行批量识别,但实时对话中通常不适用。
- 频繁地创建和销毁HTTP连接开销很大。可以为ASR和TTS服务维护一个HTTP连接池(在Lua中可以用
-
语音流处理的延迟优化:
- 流式ASR:如果ASR服务支持流式识别(如WebSocket发送音频流),可以在用户说话的同时就上传音频并接收部分识别结果,能大幅减少“等待录音结束-上传-识别”的总时间。
- TTS预加载与缓存:对于一些固定的、常用的回复语(如“请问您还有什么问题吗?”),可以提前合成好音频文件并缓存起来,直接播放文件,省去实时调用的时间。
-
高并发下的资源管理:
- 限制并发AI调用:在Lua脚本中引入简单的信号量机制,或通过外部计数器,限制同时向AI服务发起的请求数,防止AI服务被压垮。
- 监控与告警:监控FreeSWITCH的CPU、内存、通话通道数,以及AI接口的响应时间和错误率。设置阈值告警。
5. 避坑指南
-
常见配置错误:
- 拨号计划不生效:检查
dialplan/default.xml是否被正确加载,表达式^8888$是否正确。 - Lua脚本权限问题:确保FreeSWITCH运行用户有权限读取和执行Lua脚本文件。
- 音频格式问题:ASR和TTS服务对音频格式(编码、采样率、位深)有要求。确保FreeSWITCH录音(
record)和播放(streamFile)的格式与之匹配,必要时使用session:execute(“playback”, …)或session:execute(“record”, …)时指定格式参数。
- 拨号计划不生效:检查
-
语音质量优化:
- 调整
record函数的静音检测参数,使其更符合实际环境,避免过早切断用户说话或长时间录音。 - 在播放TTS音频前,可以适当插入极短的静音(
session:sleep(50)),让语音切换更自然。 - 选择高质量的TTS引擎,并调整语速、语调,使其更接近真人。
- 调整
-
安全防护:
- API密钥保护:不要将AI服务的API密钥硬编码在Lua脚本中。可以通过FreeSWITCH的
db(如SQLite)或外部配置服务动态获取。 - 输入验证:虽然主要输入是语音转文本,但如果AI接口返回的内容包含可执行指令(极少见),需要做过滤。
- 限流:在FreeSWITCH入口或AI服务前端,对来自FreeSWITCH的请求进行速率限制,防止恶意调用。
- API密钥保护:不要将AI服务的API密钥硬编码在Lua脚本中。可以通过FreeSWITCH的
6. 总结与展望
通过FreeSWITCH + mod_dptools + Lua的方案,我们成功地将智能客服AI无缝集成到了电话系统中,实现了“分机直通AI”的高效场景。这套方案的核心优势在于低延迟、高可控、易集成,特别适合对实时性要求高、需要深度定制的企业级应用。

一个简单的呼叫流程时序图:
sequenceDiagram
participant U as 用户
participant F as FreeSWITCH
participant L as Lua脚本
participant AI as AI服务(含ASR/TTS)
U->>F: 拨打分机 8888
F->>L: 触发Lua脚本
L->>F: 接听并播放欢迎语
loop 多轮对话
U->>F: 说话
F->>L: 录音
L->>AI: 发送音频进行ASR
AI-->>L: 返回识别文本
L->>AI: 发送文本请求对话
AI-->>L: 返回回复文本
L->>AI: 发送文本请求TTS
AI-->>L: 返回音频文件
L->>F: 播放TTS音频
F->>U: 听到AI回复
end
L->>F: 挂断或转接
当然,这只是个起点。在此基础上,还可以探索更多高级功能:
- 情感识别与应对:在ASR后加入情感分析,让AI能感知用户情绪(焦急、不满),并调整回复策略。
- 实时语音打断(Barge-in):允许用户在AI说话时直接插话,提升交互自然度。这需要更复杂的媒体控制和事件处理。
- 与业务系统深度集成:AI在对话中可以直接查询订单状态、生成工单,甚至完成简单的业务办理(如重置密码),真正成为“业务能手”。
最后留个思考题: 在我们当前的方案中,多轮对话的上下文完全由AI服务端维护。如果我想在FreeSWITCH侧(Lua脚本中)也能感知和控制对话流程(例如,实现一个简单的、状态明确的业务流程,如“收集订单号->验证->查询->播报”),应该如何设计Lua脚本与AI服务之间的交互协议,才能让两者更好地协同工作,既保持AI的语义理解能力,又能实现精准的流程控制呢?
希望这篇笔记能对正在探索FreeSWITCH与智能客服对接的朋友有所帮助。这条路走通了,对于提升客服效率和用户体验,效果是立竿见影的。
更多推荐

所有评论(0)