☰
RAG系统可回答性判断:让AI学会何时说“不知道”
2026/10/17 12:35:48 网站建设 项目流程

有的 RAG 系统看起来能回答一切问题,但真正的问题是:它根本分不清自己到底能不能回答某个问题。用户问“公司年假怎么休”,知识库里只有入职流程,系统照样能给你编出一套“年假规则”;用户问“今天天气如何”,系统也能一本正经地给出一个“晴转多云”。这种系统不是在用知识库回答问题,而是在用LLM的底层世界知识强行圆场。结果就是:demo阶段一切美好,生产环境用户信任迅速崩塌。

有意思的是,检索(retrieval)这个话题最近在技术社区里频繁出现。很多人搜索时,首先看到的却是另一个完全不相关的经典报错:public key retrieval is not allowed。这是 MySQL JDBC 连接时的一个典型错误,背后是数据库认证过程中的公钥检索机制。MySQL 在这个错误里做了一个非常明确的动作:我无法完成密钥检索,所以我拒绝连接。而大部分 RAG 系统在最关键的环节却缺少这种“明确拒绝”的能力。

这篇文章要讨论的,正是这个被大量教程忽略的问题:如何让你的 RAG 系统具备可回答性(answerability)判断能力。不是让检索变得更准,而是让系统在检索不到足够信息时,敢于说“我不能回答”。我会从概念讲起,给出五种实现思路,并附上一套带完整代码的工程实现方案,最后说明如何评估、排错和上线。

1. 这篇文章真正要解决的问题

先看一个非常典型的场景。

假设你正在做一个企业内部知识库问答机器人,知识库里收录了员工手册、项目文档、请假流程等资料。用户在对话框里问:

  • “报销发票的抬头怎么填?”
  • “公司今年的项目优先级是什么?”
  • “北京明天限行吗?”

前两个问题知识库能回答,第三个问题与知识库毫无关系。一个没有可回答性判断的RAG系统会如何处理?它先检索,发现知识库里没有“北京限行”的文档,但向量检索不会返回空集,它总是返回最相似的几个片段。于是系统把这些不相关的片段丢给LLM,LLM基于常识编了一个限行规则,然后一本正经地展示给用户。

这里的问题不在于答案错,而在于系统没有能力识别“这个问题不在我能回答的范围内”。

在生产环境中,这种“无边界回答”带来的影响比想象中严重得多。对于客服机器人,它会让用户觉得系统不可信;对于医疗、法律、金融等要求严谨的领域,一次编造的答案可能带来实际的法律风险或经济损失;对于企业内部系统,错误答案会导致员工采取错误操作。

真正值得关注的是:很多团队的优化思路完全走偏了。他们看到一个RAG应用回答不好,第一反应是“换个更大的模型”或者“调一下向量数据库参数”,实际瓶颈很可能在检索结果与生成过程的衔接处——系统根本没有一个闸门,用来决定“检索到的内容是否足以支撑一个有根据的答案”。

这篇文章是我最想写给这些团队看的:先别急着换模型,先把系统的“拒绝能力”建起来。RAG系统从demo走向生产,第一道关不是跑分高低,而是它能不能判断“这道题我会不会”。这个能力不是加分项,而是必选项。

2. 核心概念:相关性与可回答性

要理解可回答性判断,先看RAG的基本链路。

一个标准RAG流程通常分为四步:

  1. 用户提问。
  2. 在知识库中检索相关内容(通常用向量检索或BM25)。
  3. 把检索到的内容拼接到提示词中。
  4. 让LLM基于拼接后的上下文生成回答。

这四步里,最容易被忽视的是第2步和第3步的衔接。检索器输出的是一个“相关性分数”,比如向量余弦相似度、距离值或BM25分数。这个分数衡量的是“问题和文档片段看起来像不像”,但完全不关心“片段是否包含完整答案”。

举个例子。用户问:“公司年假政策是什么?”知识库中有一段话:“新员工入职时需要提交身份证复印件,行政部会在三个工作日内完成入职登记。”从向量相似度看,这段内容可能与“员工”“入职”等词有一定关联,分数不低。但从信息完整性的角度看,它和年假政策毫无关系。相关性分数高,不代表是可回答的证据。

更准确地说,可回答性有三个层次:

  • 存在性:知识库里有没有与问题相关的内容。
  • 完整性:相关内容是否包含回答问题所需的全部关键信息。
  • 可用性:内容是否可信、可验证、可以安全反馈给用户。

我们用一个表格对比相关性和可回答性。

维度相关性(Relativity)可回答性(Answerability)
判断对象问题与文档片段的相似程度问题与知识库整体覆盖程度
判断方式向量距离、BM25分数、关键词匹配阈值、元数据规则、LLM校验、证据链
判断结果一个分数(如0.72)一个决策(能答 / 不能答)
典型风险分数高但信息不完整信息存在但检索不到
系统形态检索器的输出检索之后、生成之前的约束层

向量数据库并不知道“用户的问题是否被知识库覆盖”。向量检索的目标是“找到最像的页面”,不是“判断这道题能不能做”。它擅长的是“从一堆文档中挑出最接近的几段”,但没有能力回答“挑出来的这几段足够不够”。这正是标题里那句 can't tell a question it can answer from one it can't 的技术根源。

理解了这一点,你就明白:可回答性判断不是检索器的职责,而是需要单独构建的决策层。它放在检索之后、生成之前,也可以放在生成之后二次校验。这一层负责回答一个问题:检索到的上下文,是否足以支撑一个可靠的答案。

3. 从 public key retrieval is not allowed 说起:拒绝能力的设计价值

在展开技术方案之前,我先花一点篇幅聊一个数据库领域的经典报错,因为它能帮助我们理解“拒绝”在系统中真正的价值。

在 MySQL 8.0 之后,默认认证插件是 caching_sha2_password。当 JDBC 客户端通过非 SSL 连接时,需要从服务器获取公钥来加密密码传输。由于安全考虑,MySQL Connector/J 默认不允许客户端主动发起“公钥检索”请求,因此你会看到这样的报错:

Public Key Retrieval is not allowed

不少开发者在第一次用 MySQL 8.0 时都踩过这个坑。常见的解决方法有两类。

方式一:在 JDBC 连接 URL 中显式开启公钥检索。

jdbc:mysql://localhost:3306/mydb?allowPublicKeyRetrieval=true&useSSL=false

方式二:使用 SSL 连接,避免在非加密链路上进行公钥检索。这种方式安全性更高。

jdbc:mysql://localhost:3306/mydb?useSSL=true&requireSSL=true

对于生产环境,更稳妥的做法是使用 SSL或按官方安全建议配置,而不是无脑开启公钥检索。任何涉及认证的连接配置,都要先想清楚风险边界,不要为了省事牺牲安全。

我在这里提到这个报错,不是要讲数据库连接排错,而是因为它的行为模式与 RAG 有很强的对照价值。

MySQL 在“公钥检索失败”时的做法是——明确失败。驱动不会拿着错误的公钥继续加密,不会假装认证成功,而是抛出一个能够定位问题的异常。这个“拒绝”在程序里可能让你头疼,但从系统设计角度看,它是一种保护:它阻止了错误在后续流程中被持续放大。

对比之下,RAG 系统在“检索失败”时的默认行为是——继续生成。检索器返回的文档分数很低,生成层依然“尽力而为”,结果就是一本正经的幻觉。MySQL 的拒绝是设计,是防线;RAG 的“礼貌回答”不是优点,而是隐患。

这给我们一个非常重要的启发:一个系统能不能准确识别“不能”的边界,是系统成熟度的分水岭。任何检索能力,都必须伴随一套清晰的失败语义。RAG也不例外:你不仅需要好的检索器,还需要一个敢说“不”的闸门。

public key retrieval is not allowed 这个报错之所以成为网络热词,恰恰是因为太多人第一次在数据库连接里遇到“检索”这个动作,并且发现它会因为安全策略明确拒绝。RAG 下的检索,也应该有这样的安全边界意识:检索到内容不代表能回答,内容不足就必须拒答或转人工,而不是“硬答”。

4. 可回答性判断的五种实现思路

理解了概念,下面看具体怎么实现。可回答性判断常见的方案有五种,我从实现成本、效果和适用场景三个维度分别介绍。

4.1 相似度阈值法

最简单的方式,是对检索器返回的相关性分数设一个阈值。判断逻辑为:如果最高分低于阈值(比如余弦相似度低于0.35),就认为知识库中没有足够相关的内容,直接拒绝回答。

优点是实现成本极低,几乎没有额外延迟。缺点也很明显:不同嵌入模型产出的分数分布差异很大,同一个模型在不同领域的知识库上分数分布也不稳定。0.5 在某个场景可能已经很高,在另一个场景可能只是“有点相关”。而且,分数高不代表信息完整,有时问题片段和文档开头很相似,但文档里只有定义,没有答案。因此,相似度阈值更适合作为第一道粗筛,不适合作为唯一依据。

4.2 查询路由与意图识别

这个方法不是在检索之后拦截,而是在检索之前分流。系统先判断问题属于哪个领域,再决定是否进入RAG流程,以及进入哪个知识库。

例如,一个企业问答系统包含人事、财务、IT 三个知识库。用户问“发票怎么报销”,系统先通过分类器把问题路由到财务知识库;用户问“如何重置密码”,路由到IT知识库;用户问“今天的天气”,则直接判定为不支持的问题,不进入任何知识库。

查询路由适合多知识库、多 Agent 的场景。它可解释性强,出了问题容易定位。缺点是需要维护分类规则或训练分类器,知识库数量和领域增加时,路由本身的准确率会成为新的瓶颈。

4.3 元数据预过滤

企业知识库通常带有强业务属性:文档属于哪个部门、什么时间发布、面向哪类人群。这些信息可以在写入向量库时作为元数据保存,检索时先按元数据范围筛选,再计算相关性。

举个例子,用户在提问时选择了“仅查看2024年财务制度”,那检索时就要把时间范围限定在2024年、部门限定在财务部。这样做既缩小了检索范围,也显著降低了“看起来像但其实是另一类文档”的干扰。

元数据过滤本身不直接回答“这个问题能不能答”,但它能让检索结果更精准,间接提高后续判断的准确率。它的代价是需要对文档做规范化标记,这对文档治理能力有一定要求。

4.4 LLM自评估器

目前工业界常见也更有效的做法,是用 LLM 来做“上下文充分性校验”。具体逻辑是:检索出候选文档后,把这批文档作为上下文交给 LLM,让它判断“这些上下文是否包含足够的信息来回答用户问题”。

这种方法的优点是能捕捉语义层面的信息完整性,而不仅仅是表面相似性。它相当于在生成答案之前,先做一次“证据检查”。缺点是增加了一次 LLM 调用,带来延迟和成本;LLM 本身的判断能力也会影响效果,个别情况下还会误判。

4.5 组合方案

在生产环境中,最可靠的并不是某一种单一方案,而是分层组合。典型链路如下:

  1. 元数据路由:先按业务属性过滤范围。
  2. 向量检索:在限定范围内召回候选文档。
  3. 阈值粗筛:把相关性极低的结果直接拦截。
  4. LLM上下文校验:对通过粗筛的上下文做信息完整性判断。
  5. 生成答案:确认上下文足够后,才让LLM生成最终回答。

每一层只承担一个职责,独立配置、独立观测,某一层出问题时可以单独调整或回滚。这是目前生产级RAG里最稳的工程形态,下面的代码示例也按照这个思路来写。

5. 完整示例:给 RAG 加一层可回答性判断

下面用一套可直接运行的 Python 示例,演示如何实现“阈值粗筛 + LLM 上下文校验”的组合方案。这里用 ChromaDB 做向量库,sentence-transformers 做本地 embedding,OpenAI 兼容接口做 LLM 校验与回答生成。你也可以替换为其他向量库或本地模型。

5.1 安装依赖

pip install chromadb sentence-transformers openai python-dotenv

版本以你当前的 Python 环境为准,本文重点是通用思路。建议使用 Python 3.10 及以上版本。

5.2 初始化知识库与模型

下面的代码负责加载本地 embedding 模型、初始化向量库,以及把文档写入集合。

# 文件路径:src/kb_init.py import chromadb from sentence_transformers import SentenceTransformer # 本地 embedding 模型,不需要外网调用 embedder = SentenceTransformer("BAAI/bge-m3") # 持久化向量库 client = chromadb.PersistentClient(path="./kb_store") collection = client.get_or_create_collection(name="enterprise_kb") def add_document(doc_id: str, text: str, metadata: dict): """把一篇文档写入向量库。""" embedding = embedder.encode(text).tolist() collection.add( ids=[doc_id], documents=[text], metadatas=[metadata], embeddings=[embedding] ) if __name__ == "__main__": # 示例:添加两份文档 add_document( doc_id="doc_001", text="员工每年享有 10 天带薪年假。入职满一年后,年假天数增加至 15 天。", metadata={"department": "hr", "year": 2024} ) add_document( doc_id="doc_002", text="报销发票需要填写公司抬头,并附上行程单或消费明细。", metadata={"department": "finance", "year": 2024} ) print("知识库初始化完成")

这里有两个需要注意的地方。第一,BAAI/bge-m3是常见的中文 embedding 模型,实际使用时要确保模型已下载或能访问模型仓库。第二,collection.add的embeddings参数是可选传递的,如果你不传,部分向量库版本会自动调用默认 embedding 函数,但为了结果可控,建议显式传入本地模型生成的向量。

5.3 核心检索与可回答性判断

下面是整个实现的核心模块。它完成四件事:检索候选文档、用阈值粗筛、用LLM校验上下文、生成回答。

# 文件路径:src/answerability_rag.py import os from dataclasses import dataclass from typing import List from openai import OpenAI from sentence_transformers import SentenceTransformer import chromadb # 复用 5.2 中初始化的模型和集合 embedder = SentenceTransformer("BAAI/bge-m3") client = chromadb.PersistentClient(path="./kb_store") collection = client.get_or_create_collection(name="enterprise_kb") # OpenAI 兼容接口,也可替换为本地部署服务 llm = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) @dataclass class JudgeResult: can_answer: bool reason: str answer: str = "" def search(question: str, top_k: int = 5) -> List[tuple]: """在向量库中检索候选文档,返回 (文档内容, 距离值) 列表。""" q_embedding = embedder.encode(question).tolist() result = collection.query( query_embeddings=[q_embedding], n_results=top_k, include=["documents", "distances"] ) docs = result["documents"][0] dists = result["distances"][0] return list(zip(docs, dists)) def verify_context(question: str, context: str) -> bool: """ 使用 LLM 判断上下文是否包含足够信息回答用户问题。 注意:这里用单独的 LLM 调用做“证据检查”,和生成回答的调用分开。 """ prompt = ( "你是一个文档审核助手。请判断下面的上下文是否包含足够的信息来回答用户的问题。\n" "如果上下文中存在明确、直接的答案依据,只输出 YES;\n" "如果上下文缺失关键信息、与问题无关或不足以回答问题,只输出 NO。\n\n" f"用户问题:{question}\n\n" f"上下文:\n{context}\n\n" "请只回答 YES 或 NO。" ) resp = llm.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0, max_tokens=4 ) return resp.choices[0].message.content.strip().upper().startswith("YES") def answer_question(question: str, threshold: float = 0.35) -> JudgeResult: """完整流程:检索 -> 阈值粗筛 -> LLM 校验 -> 生成回答。""" # 1. 检索 candidates = search(question, top_k=5) if not candidates: return JudgeResult(can_answer=False, reason="no_context") # 2. 阈值粗筛 top_doc, top_dist = candidates[0] # ChromaDB 默认使用 L2 距离,距离越小越相似,这里简单映射为相似度 similarity = 1.0 - top_dist if similarity < threshold: return JudgeResult(can_answer=False, reason="low_similarity") # 3. LLM 上下文完整性校验 context = "\n---\n".join(doc for doc, _ in candidates[:3]) if not verify_context(question, context): return JudgeResult(can_answer=False, reason="insufficient_context") # 4. 生成回答 generation_prompt = ( "请基于下面的上下文回答用户的问题。如果上下文不足以回答,请直接说明无法回答,不要编造。\n\n" f"上下文:\n{context}\n\n" f"用户问题:{question}\n" "回答:" ) resp = llm.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": generation_prompt}], temperature=0.3 ) answer = resp.choices[0].message.content.strip() return JudgeResult(can_answer=True, reason="ok", answer=answer)

这段代码的关键逻辑有三个。

第一,阈值判断放在 LLM 校验之前,目的是快速拦截明显不相关的结果,节省一次昂贵的 LLM 调用。这里用1.0 - top_dist做一个简单的相似度映射,实际项目中你需要根据向量库使用的距离度量来校准映射关系。

第二,verify_context使用独立的 LLM 调用完成“证据检查”,而不是生成回答时顺带判断。原因是生成式模型在回答问题时更容易“顺着问题往下编”,单独拿出一个判断任务、低温度、短输出,能有效提高判断的稳定性。

第三,threshold参数没有写死。生产环境应该通过评测集校准,而不是拍脑袋定一个值。后面的效果验证部分会讲如何校准。

5.4 测试主流程

写一个简单的测试脚本,验证整个链路是否正常工作。

# 文件路径:src/test_runner.py from answerability_rag import answer_question test_questions = [ "公司年假怎么休?", "报销发票需要什么材料?", "今天北京天气怎么样?", "春节联欢晚会什么时候播出?", ] if __name__ == "__main__": for q in test_questions: result = answer_question(q) print(f"问题:{q}") print(f"是否可回答:{result.can_answer}") print(f"决策原因:{result.reason}") if result.answer: print(f"回答:{result.answer}") print("-" * 50)

运行前先设置 API Key:

export OPENAI_API_KEY=你的密钥 python src/test_runner.py

如果知识库里只导入了“年假”和“报销”两篇文档,预期输出大致是:

问题:公司年假怎么休? 是否可回答:True 决策原因:ok 回答:员工每年享有 10 天带薪年假... 问题:报销发票需要什么材料? 是否可回答:True 决策原因:ok 回答:报销发票需要填写公司抬头,并附上行程单或消费明细... 问题:今天北京天气怎么样? 是否可回答:False 决策原因:insufficient_context 或 low_similarity 问题:春节联欢晚会什么时候播出? 是否可回答:False 决策原因:insufficient_context 或 low_similarity

注意,具体的reason取决于向量检索结果和 LLM 校验结果。如果你的知识库文档语义和“北京天气”有点边角关联,low_similarity这一层就可能拦截;如果关联度略高,insufficient_context这一层会兜底。两个结果都算正常工作。

6. 效果验证:如何判断你改对了

给 RAG 系统加了可回答性判断之后,如何确认它真的变好了?你需要一套评测集和一组明确指标。这是最容易被人忽视的一步:很多系统的判断逻辑“感觉合理”,但上线后误拒率很高,用户大量投诉,原因就是没有用数据验证过。

6.1 构造评测集

评测集至少包含四类问题:

  • 知识库内有明确答案的问题。
  • 知识库内只有部分相关内容、不足以形成完整答案的问题。
  • 知识库内完全没有相关内容的问题。
  • 边界模糊的问题(例如“需要结合两个文档才能回答”)。

每一类准备 20 到 50 条,由人工标注“是否应该回答”。这一步不需要开发能力,但需要业务人员参与,因为只有真正熟悉知识库内容的人才能准确判断“知识库里到底有没有答案”。

6.2 核心指标

可回答性判断看四个核心指标:

指标含义计算公式关注点
精确率系统判定“能回答”的样本中,真正能回答的比例TP / (TP + FP)防止幻觉
召回率真正能回答的样本中,系统判定“能回答”的比例TP / (TP + FN)防止误拒
误拒率本应回答但被系统拒绝的比例FN / (TP + FN)用户流失
整体准确率所有样本中判断正确的比例(TP + TN) / 总样本综合视角

这里特别要强调误拒率。很多团队上线可回答性判断后,幻觉减少了,但误拒率升高了。用户问一个正常业务问题,系统回复“无法回答”,这种体验同样糟糕。可回答性判断不是越保守越好,关键是在精确率和召回率之间找到业务可接受的平衡点。

6.3 简单评估脚本

# 文件路径:src/evaluate.py from answerability_rag import answer_question # 评测集示例,label 为 True 表示应该回答,False 表示不应该回答 EVAL_SET = [ {"question": "公司年假怎么休?", "answerable": True}, {"question": "报销发票需要什么材料?", "answerable": True}, {"question": "今天北京天气怎么样?", "answerable": False}, {"question": "公司食堂菜单是什么?", "answerable": False}, ] def evaluate(): tp = fp = tn = fn = 0 for item in EVAL_SET: result = answer_question(item["question"]) pred = result.can_answer label = item["answerable"] if label and pred: tp += 1 elif not label and pred: fp += 1 elif label and not pred: fn += 1 else: tn += 1 precision = tp / (tp + fp) if tp + fp else 0 recall = tp / (tp + fn) if tp + fn else 0 accuracy = (tp + tn) / len(EVAL_SET) print(f"精确率:{precision:.2f},召回率:{recall:.2f},整体准确率:{accuracy:.2f}") if __name__ == "__main__": evaluate()

这个脚本只是一个最小示例。生产环境的评测集建议独立成 JSON 或数据库表,并纳入 CI 流程,每次调整阈值、换模型、改 prompt 后都自动跑一遍,避免“改好一个问题又破坏另一个问题”。

7. 常见问题与排查思路

问题现象可能原因排查方式解决方案
设了阈值还是乱答阈值设得偏低;或相似度映射公式与实际距离类型不匹配打印检索分数分布,查看阈值附近的样本用评测集校准阈值,不要凭感觉;确认向量库距离类型
系统总是拒绝回答误拒率高,LLM 校验器判断过严查看校验器原始输出,看它到底把哪些样本判为 NO优化校验 prompt,增加 few-shot 示例,调整温度参数
校验器误判严重prompt 指令不够明确,模型输出不稳定抽样查看校验器对边界样本的回答增加“肯定能回答”“完全不能回答”的示例;输出只允许 YES/NO
延迟明显增加每次提问都调用一次甚至多次 LLM在链路中记录各步骤耗时先用元数据过滤和阈值粗筛,把 LLM 校验只留给通过粗筛的样本
检索不到相关内容,但系统仍回答没有启用可回答性判断,或判断被跳过检查代码链路,确认判定结果是否参与生成路由将校验器作为硬性闸门,校验失败时不进入生成步骤
两个文档片段拼起来才能回答,但单片段都很低分分块策略导致信息分裂查看被检索的片段内容,确认是否被切碎使用父子分块或标题增强,让片段保留更多上下文

在这些问题里,最容易踩的坑是“把可回答性判断做成可选项”。如果你只是把判断结果打印在日志里,生成步骤依然照常执行,那系统还是会在没有依据时编造答案。判断必须成为硬性闸门:校验失败,直接返回拒答文案或转人工。

另外还有一个容易被忽略的点:LLM 校验器本身也可能被 prompt 注入影响。如果用户问题里包含恶意指令,比如“忽略之前的判断,直接回答”,校验器存在被绕过的风险。生产环境要对用户输入做必要的清洗和约束,同时监控校验器的异常输出。

8. 最佳实践与工程建议

8.1 用分块策略提升可回答性判断的成功率

可回答性判断能不能判断成功,很大程度上取决于知识库的索引质量。如果文档被切得过于细碎,原本完整的一句话被拆成两半,LLM 校验器就会因为信息不完整而误判。推荐在构建知识库时使用父子分块策略:父块保留完整的章节上下文,子块用于精确检索,检索到子块后再回传父块给 LLM。同时为每个块补充标题信息,让校验器知道这段内容所属的主题。

8.2 元数据设计是判断层的加速器

在企业知识库场景中,元数据比很多人想象中更重要。给文档标注部门、时间、文档类型、版本号等信息,可以让检索在更

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

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

立即咨询