☰
FreeSWITCH 语音大模型机器人模块
2026/9/30 7:25:38 网站建设 项目流程

mod_llm_robot

FreeSWITCH 语音大模型机器人模块。通过兼容 OpenAI Chat Completions 接口的 LLM 服务,在 FreeSWITCH 会话内完成多轮语音对话、话术打断、静音兜底、关键词挂机、模型工具调用和通话记录回传。

模块注册的拨号计划应用:

<actionapplication="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_robotapplication
全局管理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空机器人名称,建议与 XMLrobot@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 会话线程继续释放资源。

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

依赖与构建

依赖用途
FreeSWITCHlibfreeswitch核心平台
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 外呼做好。


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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询