跳到正文

LLM 推理引擎架构与多模型协同 ​

本文档详细说明系统二(callout-server)内置的 LLM 推理客户端架构、语音级思考链抑制机制、触顶截断友好诊断、鲁棒 JSON 提取算法以及三级业务模型分工体系。


1. 语音通话场景下的 LLM 推理核心挑战 ​

与传统网页端 Chatbot 交互不同,智能语音外呼场景对大模型推理有着严苛的确定性与时延要求:

  1. 极度敏感的首包时延(TTFT):电话接通后,客户的耐受静默时间通常只有 1~1.5 秒;冗长不受控的模型推理或思考过程会导致客户误以为“线路故障”而直接挂机;
  2. 确定性的结构化输出:NLU 意图决策与槽位抽取必须输出严格契合接口协议的 JSON 对象,不能包含解释性杂文、代码块标记或 Markdown 装饰;
  3. 不同厂商协议方言差异:以 DeepSeek、Qwen、vLLM、GPUStack 等为代表的现代模型推理中台,在思考链开关、参数命名上存在方言差异;
  4. 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: 30s
SSE 逐字流式打字
采用 SSE 流式下发首包,对接 TTS 流式播放;支持客户随时插话打断(Interruptible)
3. 话术策略编排 (Script Tier)一句话 Prompt 生成、复杂流程仿真、策略诊断qwen3.6-27b-fp8 等中大参数推理模型timeout: 60s
maxTokens: 3000
开放上下文推理与长文本生成能力,保障生成 Prompt 的专业性与口语化表达质量

6. 配置加载与热刷新机制 ​

系统采用「文件默认兜底 + 数据库持久化 + 内存极速热缓存」的三层配置模型:

                    ┌───────────────────────────────┐
                    │ apps/callout-server/config.toml│
                    │         [llm] 静态配置         │
                    └───────────────┬───────────────┘
                                    │ (未配置时自动回退)
                                    ▼
                    ┌───────────────────────────────┐
                    │ PostgreSQL 数据库             │
                    │ callout.ai_llm_configs 表     │
                    └───────────────┬───────────────┘
                                    │ (服务启动加载 / 页面保存即时同步)
                                    ▼
                    ┌───────────────────────────────┐
                    │ Node.js / Bun 内存单例缓存    │
                    │  let activeLlmConfig: ...     │
                    └───────────────┬───────────────┘
                                    │ (毫秒级零 DB 开销路由)
                                    ▼
                   [统一 OpenAI 客户端与各业务 Handler]
  1. 零 DB 访问开销:高频的外呼通话在执行 NLU / Chat 时,直接从进程内单例 activeLlmConfig 获取当前有效的模型端点、API Key 与超参,不产生任何数据库查询负担;
  2. 可视化热更新:管理员在 Web 端「AI 策略中心 → 模型配置」(#/ai/models)修改配置并点击保存后,接口会同步刷新内存单例,全系统即刻生效,无需重启进程;
  3. 安全脱敏保证:API Key 永远在服务端持有并落库;下发至前端的配置统一由 maskApiKey 生成掩码(如 gpustack_7f3a****f1a2),杜绝敏感凭据泄露风险。

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。