跳到正文
阿里雲開發者社區 AI·· 4 小時前AI 評分56

電話中的 RAG:模型已開口、知識庫結果才返回,怎麼辦?

电话里的 RAG:模型已经开口了,知识库结果才回来,怎么办?

AI 導讀

SmartCall 作者介紹「音訊保持(Audio Hold)」方案,讓模型先生成並緩衝 PCM,同時並行檢索知識庫,命中後注入知識重新組織回答,未命中則釋放原音訊。方案將並行檢索上限設為 1200ms、音訊保持窗口設為 1500ms,並把門控限制為純記憶體判斷,避免阻塞模型事件執行緒。檢索命中會增加數百毫秒首響延遲;檢索失敗、逾時或異常時則採用 fail-open,讓模型原答作為後備。

正文

2026-11-04 8

版权

版权声明:

本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《 阿里云开发者社区用户服务协议》和 《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写 侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

简介: 这是我在做 SmartCall(一个 Java + Asterisk + 大模型的开源 AI 呼叫中心)时,纠结最久、也返工最多的一个设计。文本场景的 RAG 教程很多,但搬到电话里,第一个卡住的问题不是召回率,而是:检索的那一两秒,电话里放什么?

这是我在做 SmartCall(一个 Java + Asterisk + 大模型的开源 AI 呼叫中心)时,
纠结最久、也返工最多的一个设计。文本场景的 RAG 教程很多,但搬到电话里,
第一个卡住的问题不是召回率,而是:检索的那一两秒,电话里放什么?


一、问题的形状

给 AI 客服挂企业知识库,在网页对话里是很成熟的事:用户提问 → 检索 → 拼进 Prompt → 模型生成。
整个过程用户看着"正在输入…",等两秒完全能接受。

电话里不行。电话是一个没有加载状态、且时间轴不可暂停的媒介:

  • 静默超过 1.5 秒,用户的第一反应是"喂?还在吗?",然后是挂机;
  • 音频是易逝的,PCM 流过去了就是过去了,没法像文本那样"缓存起来晚点发";
  • 用户随时可能打断,你扣住的每一毫秒,都在压缩后面被打断时的反应余量。

于是矛盾很具体:知识库检索是阻塞的(HTTP + 向量召回 + 相似度过滤,几百毫秒到两秒),
而端到端实时语音模型在用户说完话之后,几乎立刻就开口了。

你来不及在模型说话之前把知识塞给它。


二、两种常规做法,以及它们为什么不够

做法 A:先检索,再让模型说话。
把检索做在轮次开始之前,查到结果再放模型生成。逻辑最干净,代价是电话里出现一段静默。
检索快的时候没人察觉,检索慢的时候(三方知识库抖动、多库串行)就是明显的"卡壳"。
更糟的是,你为了消除静默只能把超时压得很短,压到 300ms 之后召回质量就崩了。

做法 B:模型先说,检索结果下一轮用。
不阻塞当前轮,把命中的知识写进上下文,下一轮生效。听起来很优雅,实际体验是答非所问:
用户问"你们怎么退款",AI 先按通用常识答了一遍"一般可以联系售后",
用户追问一句,AI 才说出正确的七天退款政策。在电话里,第一轮答错就意味着信任崩了。

两种做法的共同点是:它们都把检索和播报当成串行关系,要么让播报等检索,要么让检索等下一轮。


三、第三条路:让模型照常说,但把话先扣在手里

SmartCall 里的做法是乐观执行 + 可撤销,内部叫「音频保持(Audio Hold)」:

用户说完:"你们怎么退款"
   │
   ├─ ① 纯内存门控(微秒级):这句话值不值得查知识库?
   │
   ├─ ② 扣住音频:模型照常生成,但输出的 PCM 先进缓冲区,不投喂播放器
   │
   ├─ ③ 切业务线程异步检索:多库并行,整体等待有上限
   │
   └─ ④ 二选一收尾:
         命中 → 丢弃缓冲,把知识片段作为指令注入,让模型重新组织这句话
         未命中 / 超时 / 异常 → 缓冲原样放出,用户听到的是模型的原话

关键在于第 ② 步和第 ④ 步的配合:模型并没有被阻塞,它一直在生成,
只是生成的音频暂时进了缓冲区。所以:

  • 检索命中:缓冲被丢弃,用户听到的是知识增强后的回答,代价是多了几百毫秒延迟;
  • 检索失败:缓冲被释放补投,用户听到的是模型自答,延迟几乎为零,因为音频早就生成好了。

这条路径还配了一条铁律:负责这件事的轮次拦截器恒不认领——它永远返回"不拦截",
模型的自答始终是兜底,知识库只做增强,不做替换。任何一环出问题(检索挂了、超时了、
相似度不达标、播报句柄丢了),系统的行为都退化回"没有知识库的 AI 客服",
而不是"卡住的 AI 客服"。这就是 fail-open:在电话场景里,答得普通远比答不出来安全。


四、三个把这套机制钉死的约束

思路本身不复杂,真正花时间的是三个细节。

1. 一个不等式:检索上限必须小于保持窗口

项目里这两个常量的关系是硬约束:并行检索的整体等待上限是 1200ms,音频保持窗口是 1500ms。

1200 < 1500 不是随手写的。检索超时之后,你还需要时间去组织注入指令、等模型重新出音频。
如果检索能拖到保持窗口之外,缓冲已经释放、模型原话已经播出去了,这时候再注入就变成"AI 打断自己"——
用户会听到一句话说到一半,突然换个说法重讲一遍。

所以迟到的检索结果必须被主动丢弃,而不是尽力注入。保持窗口本身还要留余量:
它约等于你自己的三方检索 P95 再加一点冗余,换了知识库服务就得重新压测调整。

2. 一条红线:门控绝不碰 LLM、HTTP、embedding

判断"这句话值不值得查"这一步,看起来很适合用语义相似度或者小模型来做,但绝对不能。
因为门控跑在模型事件线程上——同一条线程还要负责音频投喂和 speech_started(用户开口)事件的处理。

在这条线程上做一次 200ms 的 embedding 或 HTTP 调用,这 200ms 内的音频会卡顿、打断会失灵。
用户不会说"你的门控阻塞了事件线程",他只会说"这机器人反应好慢、还打不断"。

所以门控被限制成纯内存的字符串和数值判断,分了几级:过短、寒暄确认词("嗯""好的""谢谢")、
纯数字标点(用户报手机号那一串不该去查知识库)、排除项("转人工""投诉"优先走技能)、
轮次窗口、领域关键词与疑问词的组合。语义精度的活儿,交给检索回来之后的相似度阈值去把关。

判断"要不要做重活"的逻辑,本身必须极轻。 这一点在很多实时系统里都成立。

3. 一个反直觉的坑:陈旧判定不能用轮次号

异步检索回来时,得先确认"这通电话还在等这个结果吗"——用户可能已经打断了、已经转人工了、
会话可能已经关了。最自然的写法是比对轮次号:触发时记下 turnSeq,回来发现变了就丢弃。

这个写法是错的,而且错得很隐蔽:同一句用户话触发的 response.created 事件本身会递增轮次号。
于是检索任务兴冲冲回来,一看轮次号对不上,把自己完全正确的结果误杀了。
现象是"知识库明明配了、也命中了,电话里就是听不出来",日志里还是一片正常的未命中。

正确的锚点是发言作用域:只有接管、转人工、会话关闭、代次重建、用户开启新一句、本轮被打断,
这些才让作用域失效。轮次号只用来做冷却记账(相邻轮不重复检索),不参与陈旧判定。

顺带一个小的取舍:检索 query 的构造不调 LLM 做指代改写,
而是把"上一轮模型说了什么 + 这一轮用户说了什么"拼接后截断。
用户问"那这个多少钱",光靠这一句检索不到任何东西,但拼上一轮的产品介绍就够了。
在延迟敏感的场景,廉价但够用的方案,通常比精确但慢的方案更正确。


五、代价,以及它没解决什么

老实说这套机制不是免费的:

  • 命中知识库的那一轮,首响延迟会增加(保持窗口内的那几百毫秒);
  • 音频缓冲有内存成本,需要设容量上限,异常长响应下要能兜住;
  • 门控是关键词级别的,业务复杂时误放和漏放都得靠调参,需要先小范围灰度;
  • 它解决的是"知识注入的时序",不解决"知识本身的质量"——切片策略和召回精度还是得单独打磨。

但它换来的是一个我觉得更重要的性质:知识库这个外部依赖,从"关键路径"降级成了"增强路径"。
它挂了、慢了、不准了,通话都不会因此变得更糟,只是变得普通。


六、最后

除了本文聊的实时语音知识库,SmartCall 里还有可视化 IVR 流程编排、端到端实时语音模型接入
(服务端 VAD 打断、采样率自动协商)、技能编排(转人工/挂机/短信等有副作用的动作从 Prompt 里剥离)、
以及全链路内网私有化部署支持。这些留到后面单独写。

项目基于 Apache 2.0 协议开源,仓库地址:https://gitee.com/gdzWork/SmartCall

注释写得比较细,如果你也在做电话 + 大模型这个方向,欢迎去翻一翻。
音频保持的窗口策略、门控分级、陈旧判定这几处都还有优化空间,觉得有更优解的话,提 Issue / PR 就行。

來源:阿里雲開發者社區 AI · developer.aliyun.com