LLM 推理引擎架构与多模型协同
本文档详细说明系统二(callout-server)内置的 LLM 推理客户端架构、语音级思考链抑制机制、触顶截断友好诊断、鲁棒 JSON 提取算法以及三级业务模型分工体系。
1. 语音通话场景下的 LLM 推理核心挑战
与传统网页端 Chatbot 交互不同,智能语音外呼场景对大模型推理有着严苛的确定性与时延要求:
- 极度敏感的首包时延(TTFT):电话接通后,客户的耐受静默时间通常只有 1~1.5 秒;冗长不受控的模型推理或思考过程会导致客户误以为“线路故障”而直接挂机;
- 确定性的结构化输出:NLU 意图决策与槽位抽取必须输出严格契合接口协议的 JSON 对象,不能包含解释性杂文、代码块标记或 Markdown 装饰;
- 不同厂商协议方言差异:以 DeepSeek、Qwen、vLLM、GPUStack 等为代表的现代模型推理中台,在思考链开关、参数命名上存在方言差异;
- Token 挤压导致的截断陷阱:在小
max_tokens限制下,如果模型暗自生成了数千字的思考链(Reasoning Process),往往会导致最终的主文本(Content)被截断甚至为空,造成传统JSON.parse抛出晦涩异常。
为了解决上述痛点,callout-server 在 src/services/ai/common/openai-client.ts 与 admin/llm-config-service.ts 中构建了面向语音业务优化的 LLM 推理中枢。
2. 思考链自适应抑制机制(Thinking Suppression)
2.1 为什么必须抑制思考链
在新一代推理模型(如 DeepSeek-R1、Qwen-Thinking)中,模型默认会输出长篇的思维链条。在通话业务的 NLU 决策/抽取环节:
- 时延放大:思考过程会额外消耗 1~5 秒的 GPU 推理时间;
- 吞噬 Token 预算:非流式 JSON 请求通常将
maxTokens设在 300~500 以节约成本并防失控,思考链直接把 Token 额度耗尽,使真正的 JSON 结果无法产出。
因此,除离线话术生成(Script)或显式调试外,实时通话推理链路默认全量抑制思考链。
2.2 跨厂商自适应协议归一化
buildOpenAIRequestBody 函数通过探测模型名称与推理服务 URL,自动抹平协议差异:
typescript
// 伪代码逻辑示意 (来自 common/openai-client.ts)
const isDeepSeek =
modelLower.includes("deepseek") || urlLower.includes("deepseek");
const isQwen = modelLower.includes("qwen") || urlLower.includes("gpustack");
if (shouldDisableThinking) {
if (isDeepSeek) {
// DeepSeek 官方标准协议
payload.thinking = { type: "disabled" };
delete payload.chat_template_kwargs;
} else if (isQwen) {
// Qwen / vLLM / GPUStack 标准模板参数
payload.chat_template_kwargs = {
...rawChatTemplateKwargs,
enable_thinking: false
};
if (!isDeepSeek) delete payload.thinking;
}
}- DeepSeek 适配:注入
thinking: { type: "disabled" },并自动剔除不兼容的chat_template_kwargs,防止上游报错; - Qwen / vLLM / GPUStack 适配:注入
chat_template_kwargs: { enable_thinking: false },避免非流式请求下message.content为null; - 优雅降级与自定义:支持节点级
extraParams或enableThinking参数进行个性化覆盖。
3. 触顶 max_tokens 截断友好诊断
在传统客户端中,若模型输出截断,业务层通常只能捕获到 SyntaxError: Unexpected end of JSON input,排查人员无法区分是“模型网络故障”、“提示词写错”还是“Token 耗尽”。
callout-server 建立了细分归因诊断体系:
typescript
const choice = data?.choices?.[0];
const content = choice?.message?.content ?? "";
const finishReason = choice?.finish_reason;
const reasoning = choice?.message?.reasoning_content;
if (!content.trim()) {
if (finishReason === "length") {
throw new Error(
`模型输出触顶 max_tokens 限制 (${config.maxTokens}) 导致截断,未生成有效 JSON 内容。${
reasoning
? "模型思考链消耗了过多 token,请调大 maxTokens 或关闭思考模式。"
: "请调大 maxTokens 参数。"
}`
);
}
throw new Error("模型返回内容为空,未能解析出有效的 JSON 结构");
}- 精准归因:当捕获到
finish_reason === "length"时,主动拦截并不走普通的 JSON 解析失败; - 根因分析:如果同时检测到
reasoning_content存在,明确指出是思考链吞噬了 Token 预算,直接给出运维操作建议(“调大 maxTokens 或关闭思考模式”)。
4. 鲁棒 JSON 提取算法(Robust JSON Extraction)
即便利落开启了 response_format: { type: "json_object" },部分模型在返回时仍偶尔包裹 Markdown 代码块(如 json ... )或在前后附带少许问候语。
parseJsonObject 函数采用三级鲁棒提取算法,杜绝格式波动导致的业务中断:
[原始模型响应文本 rawText]
│
▼
1. 清除首尾空白字符 (trim)
│
▼
2. Markdown 代码块正则解构: /```(?:json)?\s*([\s\S]*?)```/i
- 匹配成功 ➔ 提取内部 candidate
- 匹配失败 ➔ 保留 trimmed 文本
│
▼
3. 边界闭包定位: 搜索第一个 '{' 与最后一个 '}'
- 校验 startIdx !== -1 且 endIdx >= startIdx
- 精准切片: candidate.slice(startIdx, endIdx + 1)
│
▼
4. 安全反序列化: JSON.parse(jsonText)
- 校验结果必须为非空非 Array 的纯 Object
│
▼
[输出清洗后的纯净结构化字典 Record<string, unknown>]该算法保证了即便模型在 JSON 前后输出自然语言碎语,系统依然能精准切出结构化数据,确保意图路由或字段抽取的稳定性。
5. 三级业务模型协同与分工体系
为了兼顾高并发成本、低通话时延与复杂业务理解能力,系统将 LLM 使用场景明确划分为三级金字塔分工:
| 业务分级 | 典型场景 | 推荐模型规格 | 超时与 Token 限制 | 设计考量与特性 |
|---|---|---|---|---|
| 1. NLU 决策/抽取 (NLU Tier) | 节点意图分类、槽位提取、条件分支分流 | qwen3.5-2b 等小参数轻量模型 | timeout: 30s (业务要求 <1s)maxTokens: 300 | 强行抑制思考链,使用严格 JSON 模式;极速吞吐,降低外呼并发成本 |
| 2. 实时流式交互 (Chat Tier) | 多轮对话节点、动态追问、拒识兜底安抚 | qwen3.5-2b 等交互型模型 | timeout: 30sSSE 逐字流式打字 | 采用 SSE 流式下发首包,对接 TTS 流式播放;支持客户随时插话打断(Interruptible) |
| 3. 话术策略编排 (Script Tier) | 一句话 Prompt 生成、复杂流程仿真、策略诊断 | qwen3.6-27b-fp8 等中大参数推理模型 | timeout: 60smaxTokens: 3000 | 开放上下文推理与长文本生成能力,保障生成 Prompt 的专业性与口语化表达质量 |
6. 配置加载与热刷新机制
系统采用「文件默认兜底 + 数据库持久化 + 内存极速热缓存」的三层配置模型:
┌───────────────────────────────┐
│ apps/callout-server/config.toml│
│ [llm] 静态配置 │
└───────────────┬───────────────┘
│ (未配置时自动回退)
▼
┌───────────────────────────────┐
│ PostgreSQL 数据库 │
│ callout.ai_llm_configs 表 │
└───────────────┬───────────────┘
│ (服务启动加载 / 页面保存即时同步)
▼
┌───────────────────────────────┐
│ Node.js / Bun 内存单例缓存 │
│ let activeLlmConfig: ... │
└───────────────┬───────────────┘
│ (毫秒级零 DB 开销路由)
▼
[统一 OpenAI 客户端与各业务 Handler]- 零 DB 访问开销:高频的外呼通话在执行 NLU / Chat 时,直接从进程内单例
activeLlmConfig获取当前有效的模型端点、API Key 与超参,不产生任何数据库查询负担; - 可视化热更新:管理员在 Web 端「AI 策略中心 → 模型配置」(
#/ai/models)修改配置并点击保存后,接口会同步刷新内存单例,全系统即刻生效,无需重启进程; - 安全脱敏保证:API Key 永远在服务端持有并落库;下发至前端的配置统一由
maskApiKey生成掩码(如gpustack_7f3a****f1a2),杜绝敏感凭据泄露风险。