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/.cppASR、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 绑定模块动态返回配置。

加载和调用分为三个阶段:

  1. 模块加载:仅解析全局 settings(如 threads),不缓存任何机器人条目。
  2. application 调用:按 robot_id 现读本地 llm_robot.conf 中的 <robot id="..."> JSON 原文(兼容回退 name 属性),命中后立即创建新对象并反序列化;修改本地静态配置后无需重载模块即可生效。
  3. 动态 XML 回退:本地未命中时,把 Robot-Id 和通道变量放入 SWITCH_EVENT_REQUEST_PARAMS 请求 XML registry;返回的 JSON 只用于本次调用,同样不缓存。

mod_xml_local 已实现 llm_robot.conf 的动态处理器。本地与动态配置均按需现读,模块内不保留机器人配置快照。

全局设置

配置默认值有效范围说明
threads101..128共享事件线程池大小

机器人字段

JSON 字段默认值说明
name空机器人名称,建议与 XML robot@name 一致
asr_engine空FreeSWITCH ASR 引擎(运行必需)
tts_engine空普通 TTS 引擎
flowing_tts_engine空流式 TTS 引擎(主回复链路当前未启用)
voice空TTS 音色
volume50(0..100)TTS 音量
speech_rate0(-500..500)TTS 语速
pitch_rate0(-500..500)TTS 音调
base_url空OpenAI 兼容服务基础 URL(运行必需)
api_key空LLM 凭据,必须通过安全配置渠道提供
model空Chat Completions 模型名(运行必需)
temperature0.3生成温度
max_tokens512→256反序列化后截断到最多 256
max_history_turns20(0..100)保留的对话轮数;0 不保留历史
enable_thinkingfalse注入 Chat Completions 根请求体
system_prompt空每次 LLM 请求首部的 system 消息
welcome空启动后首次播放内容
goodbye空关键词、静音或识别超时时的结束语
goodbye_keys空数组Aho-Corasick 挂机关键词
goodbye_delay1(0..60 秒)结束语播放后到挂机的延时,0 立即挂机;在 Hangup() 内统一生效,覆盖关键词、静音、识别超时和 hangup 工具全部挂机路径
is_breakfalse是否允许用户讲话中断播放
silence_timeout5(1..20 秒)单次静音等待
max_silence_count3(1..20)达到次数后播放结束语并挂机
silences空数组静音提示语,按顺序轮播
detect_timeout600(0..1200 秒)总识别超时;0 关闭
tools空数组暴露给模型的函数工具配置

工具调用

每个 tools 元素包含:

字段说明
name工具名称
description发送给模型的工具说明
parametersOpenAI 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__:

字段说明
uuidFreeSWITCH 呼叫 UUID
roleuser 或 assistant
content识别文本或播放文本
create_time记录创建时间
break机器人播放是否被打断
elapseLLM 响应耗时(毫秒)
tool播放来源(welcome/hangup/transfer/silence)
usageprompt_tokens、completion_tokens、total_tokens

该变量可能包含客户对话内容,后续消费按敏感业务数据处理。

停止与卸载顺序

单通呼叫结束时,管理器执行:

  1. 标记实例为停止,取消静音和识别超时任务。
  2. 从 _running_robots 删除 UUID,阻止后续调度任务获得实例。
  3. 禁止新异步任务并等待现有任务完成。
  4. 把聊天记录写入仍然有效的 FreeSWITCH channel。
  5. 返回 application,让 FreeSWITCH 会话线程继续释放资源。

模块卸载时先阻止新分发,清理本地配置快照,停止全部运行实例,等待异步任务完成,再销毁线程池。必须保持"停止生产 → 等待消费 → 释放资源"的顺序。

依赖与构建

依赖用途
FreeSWITCH libfreeswitch核心平台
openai-cppChat Completions 客户端
cpp-httplib(OpenSSL)HTTP 连接复用
nlohmann/jsonJSON 解析
inja模板渲染
ThreadPool共享事件线程池
easycpp缓存、序列化、工具类
aho-corasick关键词匹配
fmt格式化
licensecc授权校验
libcurl、pthread网络传输、线程

按照仓库规范,只能从 /root/fs 使用统一脚本编译:

./fs.sh compile freeswitch

编译成功不等于镜像已发布、打标签或服务已重启,这些步骤需要分别授权。

开发检查清单

修改本模块时至少确认:

  1. 配置字段是否同时更新了反序列化、默认值/范围和本文档。
  2. 新事件是否复制或明确转移了 switch_event_t 所有权。
  3. 新异步任务是否进入 BeginAsyncOperation()/EndAsyncOperation() 计数。
  4. 调度任务是否通过 UUID 查找实例并校验 task id。
  5. 停止流程是否拒绝新任务、等待旧任务,并最终释放机器人实例。
  6. 播放、用户讲话、静音提示和 LLM 回复是否可能互相覆盖或死锁。
  7. 工具固定参数是否覆盖模型参数,终止工具是否正确停止机器人。
  8. 日志是否包含 UUID 和底层错误,同时避免请求体、凭据及客户隐私泄漏。
  9. 通话退出前是否等待异步操作完成并成功写入 chat_message__。
  10. 使用 git diff --check 做静态检查;只有得到明确授权后才执行仓库编译脚本。

这篇文章把 AI 外呼的链路和落地要点讲清楚了。mod_llm_robot 这个开源模块的完整源码我已开源,可以在下面的仓库获取:

  • GitHub:https://github.com/pzhu1015/sales
  • Gitee(国内访问更快):https://gitee.com/pzhu1015/sales

如果你正在做或打算做智能外呼 / 呼叫中心,除了这个模块,还有 FreeSWITCH 部署、ASR/TTS 对接、Kamailio 负载、录音上云等相关方案,欢迎到仓库交流和看更多实现。

欢迎技术交流,一起把 AI 外呼做好。


本文为技术经验分享,欢迎收藏转发。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐