FreeSWITCH 语音大模型机器人模块
mod_llm_robot
FreeSWITCH 语音大模型机器人模块。通过兼容 OpenAI Chat Completions 接口的 LLM 服务,在 FreeSWITCH 会话内完成多轮语音对话、话术打断、静音兜底、关键词挂机、模型工具调用和通话记录回传。
模块注册的拨号计划应用:
<action application="llm_robot" data="<robot_id>"/>
robot_id 对应 llm_robot.conf 中 <robot id="..."> 的 id 属性(无 id 属性时兼容回退 name 属性)。
功能概览
| 功能 | 说明 |
|---|---|
| 多轮语音对话 | ASR + LLM + TTS 闭环,支持对话上下文 |
| 话术打断 | 用户说话时可中断机器人播放 |
| 静音兜底 | 连续静音轮播提示,达到上限后播放结束语挂机 |
| 关键词挂机 | Aho-Corasick 多关键词匹配识别本地挂机意图 |
| 工具调用 | 支持模型调用 hangup/transfer 内置工具 |
| 识别超时 | 整通总超时控制 |
| 聊天记录 | 通话退出序列化为 JSON 写入通道变量 chat_message__ |
| 配置隔离 | 每次 application 调用创建独立实例,会话状态完全隔离 |
架构总览
┌─────────────────────────────────┐
│ Dialplan │
│ llm_robot(robot_id) │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ mod_llm_robot.cpp │
│ (模块入口 / application) │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ LLMRobotManager │
│ (全局管理器 / 单例) │
│ ┌──────────────────────────┐ │
│ │ 本地配置 JSON 快照 │ │
│ │ 运行实例索引 (by UUID) │ │
│ │ 共享事件线程池 │ │
│ └──────────────────────────┘ │
└──────────────┬──────────────────┘
│ 每次调用创建
┌──────────────▼──────────────────┐
│ LLMRobot (单呼叫实例) │
│ ┌──────────────────────────┐ │
│ │ ASR 事件 / LLM 调用 │ │
│ │ TTS 播放 / 工具执行 │ │
│ │ 状态机 / 对话历史 │ │
│ │ KeepAliveHttpClient │ │
│ └──────────────────────────┘ │
└──┬────────┬────────┬────────────┘
│ │ │
┌──────────────▼──┐ ┌───▼─────┐ ┌▼──────────────┐
│ FreeSWITCH ASR │ │ LLM API │ │ FreeSWITCH │
│ (语音识别) │ │(Chat │ │ TTS/播放 │
│ │ │Complet.)│ │ │
└─────────────────┘ └─────────┘ └───────────────┘
模块内分层
| 层次 | 主要文件 | 职责 |
|---|---|---|
| FreeSWITCH 入口 | mod_llm_robot.cpp | 模块加载/卸载、注册 llm_robot application |
| 全局管理 | llm_robot_manager.h/.cpp | 本地配置文本、运行实例索引、共享线程池、事件分发 |
| 单呼叫领域对象 | llm_robot.h/.cpp | ASR、LLM、TTS、工具、超时、状态机、历史 |
| 对话封装 | chat_app.h | 基于 olrea/openai-cpp(header-only + libcurl) 的对话与工具调用,enable_thinking 等扩展字段直接入请求体 |
| 事件与数据 | llm_robot_event.h、llm_chat_message.h | 事件所有权、聊天记录 DTO |
| 类型声明 | ptrs.h | 共享指针类型和前置声明(唯一定义点) |
核心设计原理
配置与运行实例分离
LLMRobotManager 不缓存可运行的机器人原型对象。模块加载时只把 <robot> 节点的原始 JSON 保存到 _local_robot_configs;application 每次调用都复制 JSON 文本,创建新的 LLMRobot 并执行 DeSerialize()。Session、Channel、对话历史、Token 统计和播放状态都属于本次通话实例。
_running_robots 只用于按呼叫 UUID 索引正在运行的实例,供 FreeSWITCH 调度任务和事件回调定位对象。它不是配置缓存,实例在通话停止时删除。
事件所有权与异步分发
播放回调收到的 FreeSWITCH 事件只在回调期间有效。模块先通过 switch_event_dup() 复制事件,再由 LLMRobotEvent(RAII)接管 switch_event_t*,析构调用 switch_event_destroy()。事件进入管理器的共享线程池,避免在媒体回调中直接执行阻塞的 LLM 请求。
每个机器人使用 BeginAsyncOperation()/EndAsyncOperation() 记录已入队和执行中的任务。退出时先禁止接收新任务,再等待计数归零,之后才允许释放 Session 和 Channel 裸指针。这是模块避免异步回调访问已释放 FreeSWITCH 会话的核心生命周期约束。
状态协调
单呼叫实例用原子变量、互斥锁和条件变量协调以下状态:
| 状态 | 含义 |
|---|---|
Started | 实例仍可处理通话逻辑 |
ReplyBusy | 正在构建或等待 LLM 回复 |
Playing | 正在 TTS 或播放音频 |
SilencePlaying | 当前播放由静音兜底触发 |
UserSpeaking | 已收到用户开始说话事件 |
Breaking | 当前播放正在被用户打断 |
PlaybackMutex 和 PlaybackCv 用于避免普通回复、静音提示和用户讲话互相覆盖。
UUID 间接查找
静音和识别超时任务只携带复制后的 UUID,不长期捕获 FreeSWITCH 会话裸指针。任务触发后通过 _running_robots 重新查找实例,并核对 task id,再进入异步操作计数。旧任务、已取消任务或已退出的呼叫会被丢弃。
LLM 调用
每个 ChatApp 各自持有一个 openai::OpenAI 实例(独立 curl session,天然并发安全),base_url/model/api_key/temperature/max_tokens/enable_thinking 全部经配置传入。日志只记录目标、状态和响应长度,不记录 API Key 或完整请求体。
当前主对话路径调用同步、非流式 Chat Completions。SpeakStart/Feed/End/Wait 和 HTTP 流式回调能力已存在,但尚未接入 ThinkReply() 主回复链路。
呼叫流程
Dialplan mod_llm_robot LLMRobotManager LLMRobot FreeSWITCH ASR/TTS Event Pool LLM API
│ │ │ │ │ │ │
│── llm_robot(id) ──────>│ │ │ │ │ │
│ │── GetConfig(id) ────>│ │ │ │ │
│ │<── local JSON ───────│ │ │ │ │
│ │── DeserializeRobot ──────────────────────>│ │ │ │
│ │ │── register UUID ──>│ │ │ │
│ │ │── Start ──────────>│ │ │ │
│ │ │ │── start ASR + welcome >│ │ │
│ │ │ │ │── speech event ───>│ │
│ │ │ │<── OnEvent ───────────│<── copy+enqueue ──│ │
│ │ │ │── LLM call ──────────────────────────────────────────────────>│
│ │ │ │<── reply text ────────────────────────────────────────────────│
│ │ │ │── TTS/playback ──────>│ │ │
│ │ │ │ │ │ │
│ │ │ │── stop timers + ASR ─>│ │ │
│ │ │── wait async ops ──>│ │ │ │
│ │ │ │── set chat_message__ ─>│ │ │
│ │ │── remove UUID ────>│ │ │ │
ASR 事件处理
| 事件 | 当前行为 |
|---|---|
begin-speaking | 标记用户讲话、取消静音任务;启用打断或正在播放静音话术时中止播放 |
detected-speech | 记录用户文本、清零静音计数、匹配挂机词,否则调用 LLM |
closed | 清除用户讲话状态 |
CHANNEL_HANGUP | 停止实例并从运行缓存移除 |
DTMF | 当前只记录按键事件,不执行业务逻辑 |
当 ReplyBusy 为真时,新识别文本追加到历史,但不会并行发起第二个 LLM 请求。
播放、打断与静音
- 播放内容以
http://或https://开头时调用switch_ivr_play_file()。 - 其他内容先 URL 解码和 inja 渲染,再调用
switch_ivr_speak_text()。 - 模板上下文包含
caller和callee,并注册了get()、post()和递归render()回调。 is_break=true时,用户说话会中断普通播放;静音提示始终允许被打断。- 每次有效识别重置静音计数。连续静音先轮播
silences,达到max_silence_count后播放goodbye并挂机。 detect_timeout是整通识别阶段的总超时,不因单次成功识别而重新计时。
配置加载
模块使用的 FreeSWITCH 配置名称是 llm_robot.conf。仓库内的 llm_robot.conf.xml 是静态配置示例;实际运行环境也可以由 XML 绑定模块动态返回配置。
加载和调用分为三个阶段:
- 模块加载:仅解析全局
settings(如threads),不缓存任何机器人条目。 - application 调用:按
robot_id现读本地llm_robot.conf中的<robot id="...">JSON 原文(兼容回退name属性),命中后立即创建新对象并反序列化;修改本地静态配置后无需重载模块即可生效。 - 动态 XML 回退:本地未命中时,把
Robot-Id和通道变量放入SWITCH_EVENT_REQUEST_PARAMS请求 XML registry;返回的 JSON 只用于本次调用,同样不缓存。
mod_xml_local 已实现 llm_robot.conf 的动态处理器。本地与动态配置均按需现读,模块内不保留机器人配置快照。
全局设置
| 配置 | 默认值 | 有效范围 | 说明 |
|---|---|---|---|
threads | 10 | 1..128 | 共享事件线程池大小 |
机器人字段
| JSON 字段 | 默认值 | 说明 |
|---|---|---|
name | 空 | 机器人名称,建议与 XML robot@name 一致 |
asr_engine | 空 | FreeSWITCH ASR 引擎(运行必需) |
tts_engine | 空 | 普通 TTS 引擎 |
flowing_tts_engine | 空 | 流式 TTS 引擎(主回复链路当前未启用) |
voice | 空 | TTS 音色 |
volume | 50(0..100) | TTS 音量 |
speech_rate | 0(-500..500) | TTS 语速 |
pitch_rate | 0(-500..500) | TTS 音调 |
base_url | 空 | OpenAI 兼容服务基础 URL(运行必需) |
api_key | 空 | LLM 凭据,必须通过安全配置渠道提供 |
model | 空 | Chat Completions 模型名(运行必需) |
temperature | 0.3 | 生成温度 |
max_tokens | 512→256 | 反序列化后截断到最多 256 |
max_history_turns | 20(0..100) | 保留的对话轮数;0 不保留历史 |
enable_thinking | false | 注入 Chat Completions 根请求体 |
system_prompt | 空 | 每次 LLM 请求首部的 system 消息 |
welcome | 空 | 启动后首次播放内容 |
goodbye | 空 | 关键词、静音或识别超时时的结束语 |
goodbye_keys | 空数组 | Aho-Corasick 挂机关键词 |
goodbye_delay | 1(0..60 秒) | 结束语播放后到挂机的延时,0 立即挂机;在 Hangup() 内统一生效,覆盖关键词、静音、识别超时和 hangup 工具全部挂机路径 |
is_break | false | 是否允许用户讲话中断播放 |
silence_timeout | 5(1..20 秒) | 单次静音等待 |
max_silence_count | 3(1..20) | 达到次数后播放结束语并挂机 |
silences | 空数组 | 静音提示语,按顺序轮播 |
detect_timeout | 600(0..1200 秒) | 总识别超时;0 关闭 |
tools | 空数组 | 暴露给模型的函数工具配置 |
工具调用
每个 tools 元素包含:
| 字段 | 说明 |
|---|---|
name | 工具名称 |
description | 发送给模型的工具说明 |
parameters | OpenAI function tool 的 JSON Schema |
arguments | 服务端固定参数,覆盖模型生成的同名参数 |
工具必须同时存在于机器人配置和本地 ToolFunctions 白名单中才能执行:
| 工具 | 行为 |
|---|---|
hangup | 播放 message(缺省使用 goodbye),写入聊天记录,然后挂机 |
transfer | 播放 message,按 to 转入拨号计划并停止机器人 |
transfer.arguments.to 应由服务端配置固定,避免模型改写目标号码。请求设置 parallel_tool_calls=false,同时兼容 tool_calls 和 function_call 响应。配置其他名称的工具只暴露给模型,执行阶段被白名单拒绝。
聊天记录输出
模块维护两套数据:
- ChatHistory:发给模型的有限上下文,仅保留 user/assistant 文本。
- ChatMessages:通话审计记录,保存全部已识别用户文本和实际播放文本。
退出时 ChatMessages 序列化为 JSON 数组写入 chat_message__:
| 字段 | 说明 |
|---|---|
uuid | FreeSWITCH 呼叫 UUID |
role | user 或 assistant |
content | 识别文本或播放文本 |
create_time | 记录创建时间 |
break | 机器人播放是否被打断 |
elapse | LLM 响应耗时(毫秒) |
tool | 播放来源(welcome/hangup/transfer/silence) |
usage | prompt_tokens、completion_tokens、total_tokens |
该变量可能包含客户对话内容,后续消费按敏感业务数据处理。
停止与卸载顺序
单通呼叫结束时,管理器执行:
- 标记实例为停止,取消静音和识别超时任务。
- 从
_running_robots删除 UUID,阻止后续调度任务获得实例。 - 禁止新异步任务并等待现有任务完成。
- 把聊天记录写入仍然有效的 FreeSWITCH channel。
- 返回 application,让 FreeSWITCH 会话线程继续释放资源。
模块卸载时先阻止新分发,清理本地配置快照,停止全部运行实例,等待异步任务完成,再销毁线程池。必须保持"停止生产 → 等待消费 → 释放资源"的顺序。
依赖与构建
| 依赖 | 用途 |
|---|---|
FreeSWITCH libfreeswitch | 核心平台 |
| openai-cpp | Chat Completions 客户端 |
| cpp-httplib(OpenSSL) | HTTP 连接复用 |
| nlohmann/json | JSON 解析 |
| inja | 模板渲染 |
| ThreadPool | 共享事件线程池 |
| easycpp | 缓存、序列化、工具类 |
| aho-corasick | 关键词匹配 |
| fmt | 格式化 |
| licensecc | 授权校验 |
| libcurl、pthread | 网络传输、线程 |
按照仓库规范,只能从 /root/fs 使用统一脚本编译:
./fs.sh compile freeswitch
编译成功不等于镜像已发布、打标签或服务已重启,这些步骤需要分别授权。
开发检查清单
修改本模块时至少确认:
- 配置字段是否同时更新了反序列化、默认值/范围和本文档。
- 新事件是否复制或明确转移了
switch_event_t所有权。 - 新异步任务是否进入
BeginAsyncOperation()/EndAsyncOperation()计数。 - 调度任务是否通过 UUID 查找实例并校验 task id。
- 停止流程是否拒绝新任务、等待旧任务,并最终释放机器人实例。
- 播放、用户讲话、静音提示和 LLM 回复是否可能互相覆盖或死锁。
- 工具固定参数是否覆盖模型参数,终止工具是否正确停止机器人。
- 日志是否包含 UUID 和底层错误,同时避免请求体、凭据及客户隐私泄漏。
- 通话退出前是否等待异步操作完成并成功写入
chat_message__。 - 使用
git diff --check做静态检查;只有得到明确授权后才执行仓库编译脚本。
这篇文章把 AI 外呼的链路和落地要点讲清楚了。mod_llm_robot 这个开源模块的完整源码我已开源,可以在下面的仓库获取:
- GitHub:https://github.com/pzhu1015/sales
- Gitee(国内访问更快):https://gitee.com/pzhu1015/sales
如果你正在做或打算做智能外呼 / 呼叫中心,除了这个模块,还有 FreeSWITCH 部署、ASR/TTS 对接、Kamailio 负载、录音上云等相关方案,欢迎到仓库交流和看更多实现。
欢迎技术交流,一起把 AI 外呼做好。
本文为技术经验分享,欢迎收藏转发。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)