
内部的客服知识库问答系统上了生产用的标准 RAG 架构向量检索 LLM 生成。上线前 demo 效果不错上线第三天运营就来找我了有个用户问怎么取消自动续费系统给了一套操作流程用户照着做了发现根本不对。拉出来一查知识库里确实有续费相关的文档但那篇文档讲的是续费规则和计费标准压根没有取消操作的内容。检索系统把它排在了 top-1相似度 0.87LLM 拿着这篇文档给用户编了一个[设置 → 账户管理 → 关闭自动续费]的步骤。问题是我们的 prompt 里明确写了如果提供的资料中没有相关信息请直接告知用户你无法回答。写了也没用LLM 照样编。向量检索永远不会返回空结果大家可能对 RAG 有一个错误的预期如果用户问的问题不在知识库范围内检索应该返回空。但向量数据库不是这么工作的你扔一个 query 进去它做的事情是在向量空间里找距离最近的 K 个文档。不管这些文档跟 query 有没有关系它都会给你返回 K 个结果。0.87 的相似度看起来很高对吧但这三篇文档没有一篇能回答怎么取消。它们只是跟续费这个词在语义空间里距离很近而已。这就是第一个坑embedding 模型分不清同一主题和能回答这个问题之间的差别。它看到的是续费这个概念在两边都出现了语义向量距离就近了。但续费规则和取消续费操作是两件完全不同的事情。相似度阈值为什么不靠谱一般做法是设一个阈值比如低于 0.7 就不采纳然并卵跑几天就知道不行。同一个 embedding 模型不同领域的 query相似度分布差异极大。我们内部测过一组数据客服场景下强相关文档的相似度集中在 0.82 ~ 0.92但换到技术文档场景强相关文档的相似度掉到 0.68 ~ 0.78。客服知识库: 强相关 query-doc 对: cosine_similarity 分布 [0.82, 0.92] 弱相关 query-doc 对: cosine_similarity 分布 [0.75, 0.85] 不相关 query-doc 对: cosine_similarity 分布 [0.65, 0.78] 技术文档知识库: 强相关 query-doc 对: cosine_similarity 分布 [0.68, 0.78] 不相关 query-doc 对: cosine_similarity 分布 [0.55, 0.70]客服场景里 0.78 可能是不相关的噪声技术文档场景里 0.78 已经是最强相关了一个全局阈值根本覆盖不了这个差异。更要命的是同一个知识库里不同类型的问题相似度分布也不一样。短问题退款流程和长问题我上个月买的年度会员用了三个月想退剩下的钱怎么操作召回同一篇文档的相似度可以差 0.1 以上。LLM 为什么不听话检索层拦不住那让 LLM 在 prompt 里写清楚如果提供的资料中没有相关信息请直接告知用户无法回答。问题在于 LLM 在有 context 注入的情况下默认行为就是基于 context 组织答案而不是先去质疑 context 是否真的相关。发给 LLM 的 prompt 系统角色: 你是一个客服助手基于以下资料回答用户问题。 如果资料中没有相关信息请直接告知用户你无法回答。 资料: 《自动续费规则说明》 自动续费将在会员到期前 7 天自动扣款... 扣款金额按当前套餐价格执行... 续费成功后会员有效期自动延长... 用户问题: 怎么取消自动续费LLM 看到的是context 里有自动续费四个字跟用户的问题高度相关。它不会像人类一样先停下来想这篇文档讲的是续费规则不是取消操作。它的推理过程更接近于用户问续费 → context 里有续费相关内容 → 基于这些内容组织答案。至于答案里的具体操作步骤是从哪来的是 LLM 的参数记忆里的通用知识。它见过太多设置 → 账户管理 → 关闭XXX这种操作路径context 给了它一个续费的主题锚点它就顺着自己的参数知识编了一套看起来合理的步骤。这就是为什么 prompt 里写不知道就说不知道效果很差。LLM 不觉得自己不知道它觉得 context 里有相关内容只是内容不够详细所以它自动补全了。真正有效的方案生成前加一道相关性校验检索结果拿到之后别直接塞给 LLM 生成答案先用 LLM 做一步判断这些文档能不能用来回答这个问题。第一步 prompt相关性判断: 你是一个文档相关性评估器。判断以下每条文档是否能直接用来回答用户的问题。 注意相关的定义不是文档和问题属于同一主题而是文档内容能直接用来回答用户的具体问题。 对每条文档输出 JSON: { doc_id: 文档编号, relevance: relevant | partially_relevant | irrelevant, reason: 一句话说明判断理由 } 用户问题: 怎么取消自动续费 文档1: 《自动续费规则说明》... 文档2: 《会员套餐与计费标准》... 文档3: 《支付方式绑定指南》...LLM 输出: {doc_id: 1, relevance: irrelevant, reason: 文档讲的是续费规则和扣款机制没有取消操作的说明} {doc_id: 2, relevance: irrelevant, reason: 文档讲的是套餐价格与取消续费无关} {doc_id: 3, relevance: irrelevant, reason: 文档讲的是支付方式绑定不涉及取消续费}三条全部 irrelevant这时候系统就可以直接拒答告诉用户这个问题超出了我的知识范围建议联系人工客服。这里有几个关键细节逐条打标不要笼统问不要问这些文档和问题相关吗LLM 面对一堆文档时倾向于给模糊评价。逐条判断 给理由准确率高很多。相关的定义要严格限定不是属于同一主题而是能直接回答问题。这一句话的差异直接影响拒答的准确率。前者会把续费规则判为相关后者不会。用 structured output 约束输出格式不约束的话 LLM 容易跑偏一会给你写一段分析一会给你改格式。JSON schema 约束住输出稳定后续代码也好解析。拒答要分层不要一刀切全部不相关就直接拒答这没问题但如果有一条 partially_relevant处理方式应该不一样。比如用户问怎么取消自动续费取消后剩余天数会不会退款知识库里有退款政策但没有取消操作。这种情况下把退款政策相关的内容给出来同时告诉用户取消操作的部分你回答不了比一刀切的我无法回答体验好得多。最怕的是另一种误杀文档里明明有答案但相关性校验判成了 irrelevant用户问什么都说不知道。拒答率一旦超过 20%用户反馈里一半在骂问什么都说不知道产品口碑直接崩。检索层也不是完全没招相关性校验靠 LLM意味着多一次 API 调用延迟和成本都上去了。检索层能做的粗筛越多LLM 那一步的压力就越小。一个低成本的信号是 token overlapquery 里的关键词和检索回来的文档做词级别的 overlap 检查。def token_overlap_ratio(query_tokens, doc_tokens): 计算 query 和 doc 之间的关键词重叠率 query_set set(query_tokens) doc_set set(doc_tokens) overlap query_set doc_set return len(overlap) / len(query_set) if query_set else 0 query 怎么取消自动续费 doc 自动续费将在会员到期前7天自动扣款... query_tokens [取消, 自动, 续费] doc_tokens [自动, 续费, 会员, 到期, 扣款] ratio token_overlap_ratio(query_tokens, doc_tokens) # ratio 2/3 0.67 取消没出现在文档里取消这个核心动作词在文档里完全没出现但向量相似度 0.87。token overlap 能提供一个额外的信号向量说很相关但关键词缺了核心动词这次检索结果的置信度应该打折。另一个信号是多路召回交叉验证向量检索 和 BM25 各跑一遍看两边 top-K 的重叠度。重叠度高说明两种检索策略一致认为这些文档相关可信度高。重叠度低说明 semantic match 和 lexical match 之间分歧很大结果不该被无条件信任。说在最后如果上线之后拒答率长期在 20% ~ 30%说明两件事至少有一件成立知识库覆盖有缺口或者产品层面没让用户搞清楚这个系统能干什么这两个问题调 prompt 解决不了。拒答日志应该作为知识库迭代的输入源高频被拒的问题定期拉出来看哪些是知识库该覆盖但没覆盖的补进去哪些是这个系统本来就不该回答的在产品交互上做引导。