ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

检索系统可回答性判断实战:拒绝幻觉答案的RAG方案

检索系统可回答性判断实战:拒绝幻觉答案的RAG方案 “My retrieval cant tell a question it can answer from one it cant”这句话描述的是检索系统里最尴尬的一类问题用户问了一个知识库里根本没有答案的问题系统还是生成了一整段答案看起来流畅实际是幻觉。问题的根源不在生成模型而在检索侧没有做好“可回答性判断”。知识库覆盖范围有限检索器只能找到语义相似的片段无法确认这些片段是否回答了用户的问题生成器拿到上下文后又会强制生成答案于是越自信错得越离谱。这篇文章不打算只讲概念而是围绕“检索系统能不能区分可答与不可答”这一核心场景给出一套可落地的验证流程先分析判断路径再搭建一个最小检索系统加上可回答性评分、拒答逻辑和批量评测脚本最后用一组测试查询验证效果。适合正在做 RAG 问答、知识库机器人、检索评估或者被幻觉答案困扰的开发者和算法工程师。1. 核心能力速览这里先把“可回答性判断”这件事的能力边界列清楚方便判断是否匹配你的场景。能力项说明要解决的问题检索系统无法区分“知识库能回答的问题”和“知识库不能回答的问题”导致生成幻觉答案核心判断依据检索相关性打分、上下文答案覆盖率、生成模型自评置信度、无答案训练样本典型处理方式阈值拦截、二次检索、答案回测、显式“无法回答”兜底适用场景本地知识库问答、RAG 应用、客服机器人、文档问答、检索评估部署方式本地 Python 服务、API 服务、批量评测脚本推荐硬件如果只跑 embedding 和规则评分普通 CPU 即可如果使用生成模型自评需要按模型规模评估 GPU 显存批量任务支持通过批量脚本对查询集统一打分接口能力可以封装为 HTTP API供上游问答系统调用典型输出可答/不可答判定、置信度分数、拒绝原因、生成答案需要注意可回答性判断没有百分百准确的方法实际效果依赖测试集和阈值设定2. 问题剖析检索系统为什么分不清可答和不可答检索系统的核心任务是“从知识库里找出与查询相关的文档片段”。这个任务评估的是相关性而不是答案存在性。两者之间的差距就是可回答性判断失效的地方。2.1 检索打分代表语义相关不代表答案存在Embedding 模型会把查询和文档都映射到向量空间然后按相似度排序。相似度高通常意味着主题接近。但主题接近不等于答案存在。比如知识库中只有产品介绍文档用户问“这款产品什么时候停产”检索器能找到产品介绍页却找不到停产日期。向量相似度可能仍然很高因为文档里出现了大量产品信息。此时检索系统会给出一个看起来合理的置信度但真实答案不在文档中。生成器继续工作就会把文档里的其他信息拼成一段回答。2.2 检索范围固定覆盖面有限知识库本质上是静态快照。查询一旦超出快照范围比如涉及新版本发布时间、政策变动、未入库的第三方信息检索结果里根本没有可用的答案片段。这类查询有一个特征检索到的文档片段虽然相关但答案字段缺失。很多评估方案只看“检索结果是否相关”不看“检索结果是否覆盖答案”所以系统无法识别出这种缺失。2.3 生成器被要求“必须回答”大模型生成的默认行为是补全下文。即便上下文里没有答案模型也会尝试生成一段通顺的文字。这是生成式模型的结构性倾向。所以仅仅让生成模型自己判断“我能不能回答”效果往往不稳定。需要在生成前或生成后增加一道独立判断逻辑。2.4 “能回答”和“不能回答”不是二分类可回答性至少包含几种情况查询类型示例判断难度知识库有明确答案公司成立时间低知识库有强相关文档但无答案产品参数没有停产日期中知识库只有部分信息价格有优惠没有中知识库无相关文档竞争对手的内部策略低但检索器可能误匹配多文档信息冲突两篇文档给出了不同版本号高因此工程上不能只设定一个固定阈值而需要结合多种信号综合判断。3. 可回答性判断的三种技术路线实际项目里比较实用的可回答性判断方法有三类。它们可以单独使用也可以组合。3.1 Embedding 相似度阈值做法计算查询与检索结果的最大相似度低于阈值直接判定为不可回答。优点是实现简单推理开销小。缺点是阈值不好调不同知识库的相似度分布差异很大而且语义相似度不等于答案覆盖容易出现“相似度高但答案不存在”的情况。改进方向不只看查询与单篇文档的相似度还要看查询与所有检索片段在答案维度的覆盖情况比如把文档片段切成句子计算哪些句子能直接回答问题的疑问词和关键词。3.2 生成答案后的自评与回测做法先生成答案再把答案和查询一起交给评估器判断答案是否真正回答了查询。一种常见实现是“答案回测”把生成的答案作为检索查询回到知识库里检索一遍看是否能找到支持性证据。如果找不到高相关性文档说明答案可能是生成的幻觉。另一种实现是使用 LLM 自评让模型输出结构化评分{ answerable: true, confidence: 0.82, reason: 检索文档中明确给出了产品保修期限, evidence_snippets: [doc_03_chunk_12] }自评方式对模型能力要求较高。如果使用小模型评分可能不稳定使用大模型则要关注推理延迟和成本。3.3 无答案训练与拒答分类器做法构造“有答案 / 无答案”的成对训练数据微调一个二分类器。分类器的输入可以是查询和检索片段的拼接。输出是“可回答 / 不可回答”的概率。这种方法比阈值更稳但需要人工标注数据。构造训练数据时可以从知识库中采样问题并人为构造“相似但无答案”的负样本。比如把提问中的关键实体替换成知识库不存在的实体或者把文档中的描述改写成知识库不支持的结论。4. 最小检索系统与可回答性评分演示下面给出一套最小的可回答性判断流程。它不是一个完整商业系统而是一个方便理解逻辑、方便改造成自己项目的通用骨架。4.1 系统结构输入查询 - 文档向量检索返回 top-k - 可回答性评分器 - 相似度低于阈值直接拒答 - 生成答案并做证据回测 - 输出答案或拒绝信息部署到实际项目时可以把文档加载、向量检索、评分器拆成独立的模块。4.2 依赖与基础接口示例以 Python 为例假设你已经安装好向量库客户端和 Embedding 模型调用库。下面代码是流程模板具体类名和函数名需要按你的项目替换。class DocumentStore: 向量文档库负责把文档写入向量索引并按相似度检索。 def add_documents(self, chunks: list[str], metadatas: list[dict]) - None: raise NotImplementedError def search(self, query: str, top_k: int 5) - list[dict]: 返回检索片段列表每个片段包含 text、score、metadata。 raise NotImplementedError class AnswerabilityScorer: 可回答性评分器综合相似度、答案覆盖和自评结果。 def score(self, query: str, chunks: list[dict]) - dict: raise NotImplementedError实际项目中DocumentStore可以对接 Milvus、Qdrant、FAISS 或数据库内置向量索引AnswerabilityScorer可以调用本地部署的生成模型也可以调用预置的评分接口。4.3 一个简单的评分逻辑示例下面这段代码演示了“相似度阈值 生成答案 证据回测”的组合流程。它不是最终生产方案但能让你快速看到效果。def judge_answerability(query: str, doc_store, llm, top_k: int 5) - dict: chunks doc_store.search(query, top_ktop_k) if not chunks: return { answerable: False, reason: no_context, score: 0.0, answer: None, } # 信号一最高相关分 max_similarity max(item[score] for item in chunks) relevance_score float(max_similarity) # 信号二生成答案 context \n\n.join([item[text] for item in chunks[:3]]) answer llm.generate(query, context) # 信号三用答案回测检索看是否存在支持证据 evidence_chunks doc_store.search(answer, top_k3) evidence_score max(item[score] for item in evidence_chunks) if evidence_chunks else 0.0 # 信号四模型自评返回置信度 self_eval llm.self_evaluate(query, context, answer) self_confidence float(self_eval.get(confidence, 0.0)) # 综合判断任一核心信号不足宁可拒答 if relevance_score 0.6: return { answerable: False, reason: low_relevance, score: relevance_score, answer: None, } if evidence_score 0.55 and self_confidence 0.75: return { answerable: False, reason: unsupported_answer, score: (relevance_score evidence_score self_confidence) / 3, answer: answer, } final_score (relevance_score evidence_score self_confidence) / 3 return { answerable: True, reason: supported, score: final_score, answer: answer, }这里的阈值是示例值必须先在自己的知识库上跑一批查询后重新标定。不同向量模型产出的相似度分布差异很大直接套用会产生大量误判。4.4 拒答输出设计当系统判定为“不可回答”时返回信息要包含原因方便上层判断和排查。{ answerable: false, reason: low_relevance, message: 知识库中没有找到与问题直接相关的内容请确认问题表述或补充知识库文档。 }如果上层允许追问可以把检索到的高相关片段返回提示用户“以下内容与问题相关但未直接回答”。5. 功能测试与效果验证设计一组“能答/不能答”测试查询部署完成后最先要验证的不是生成质量而是可回答性判断的准确率。推荐设计一组测试查询覆盖前文提到的几种情况。5.1 测试集设计编号查询类型示例期望判定Q1明确答案某产品保修期是多久可回答Q2相似但无答案某产品停产日期文档中只有上市时间不可回答Q3完全无关明天天气怎么样不可回答Q4部分信息某产品是否支持无线充电文档只有电池容量无法判断倾向拒答Q5冲突文档两个文档给出不同版本号需要识别冲突返回不确定5.2 测试流程准备一个小型知识库至少包含 20 到 50 个文档片段。编写一个批量评测脚本遍历测试查询。对每个查询输出判定结果、相关片段、相似度分数、最终分数。人工核对“可回答”的答案是否真的正确。记录误判样本调整阈值或增加规则。5.3 批量评测脚本模板python evaluate_answerability.py \ --query_file ./test_queries.jsonl \ --output_file ./eval_results.jsonl \ --top_k 5 \ --threshold 0.6测试数据文件格式{query: 某产品保修期是多久, expected: true} {query: 某产品停产日期, expected: false} {query: 明天天气怎么样, expected: false}评测结果需要统计几个指标指标含义这个场景下的关注点拒答率判定为不可回答的查询比例避免过度拒答导致正常问题无法使用幻觉率判定为可回答但答案错误的查询比例这是核心优化目标答案覆盖准确率回答正确且答案有文档支撑的比例越高越好平均响应时间单查询从入参到出参的时间判断上线成本测试时使用的意图是如果幻觉率下降但拒答率同步大幅上升说明阈值过严需要重新平衡。6. 接口 API 与批量任务实际接入问答系统时需要把可回答性判断封装成 API。下面给出一个 FastAPI 示例重点展示请求参数和返回结构。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class QueryRequest(BaseModel): query: str top_k: int 5 need_answer: bool True class QueryResponse(BaseModel): answerable: bool reason: str score: float answer: str | None evidence: list[str] app.post(/query, response_modelQueryResponse) def query_handler(req: QueryRequest): # judge_answerability 需要按项目实际实现 result judge_answerability(req.query, doc_store, llm, top_kreq.top_k) return result启动服务uvicorn answerability_api:app --host 127.0.0.1 --port 8000调用接口curl -X POST http://127.0.0.1:8000/query \ -H Content-Type: application/json \ -d {query: 某产品保修期是多久, top_k: 5, need_answer: true}6.1 批量任务设计批量评测场景下不建议逐条请求 HTTP 接口而应该写一个离线脚本直接调用核心函数。批量任务要注意几个点输入查询文件按行存储每行一个 JSON 对象。每条任务记录原始查询、判定结果、检索片段、分数、异常信息。如果调用外部生成模型要控制并发数避免触发限流。任务失败要记录重试状态重试次数建议不超过 3 次。输出目录按日期分目录方便回溯。python batch_eval.py \ --input ./queries.jsonl \ --output ./results/2025-01-01/ \ --concurrency 4 \ --max_retries 36.2 批量任务结果记录建议每条结果可以记录以下字段{ query: 某产品保修期是多久, expected: true, answerable: true, reason: supported, score: 0.82, answer: 保修期是 12 个月。, evidence: [doc_03_chunk_12], latency_ms: 823, retry_count: 0 }有了这个记录后续调阈值时可以直接统计分布不需要重新跑全量查询。7. 资源占用与性能观察可回答性判断的部署成本主要取决于你选用了哪几路信号。7.1 基础检索与打分如果只使用向量检索和相似度阈值资源占用很低。Embedding 模型可以推理时加载一次批量查询时复用向量索引数据量从几万到几十万条时查询延迟通常在百毫秒级别。该方案对显存要求不高CPU 也能跑。7.2 加入生成模型后如果使用生成模型输出答案再让模型自评置信度资源占用会明显上升。观察性能时重点看三个指标指标观察方式判断标准首 token 延迟生成 API 返回首字时间越高说明模型越大或推理配置越弱完整生成耗时从请求发出到收到完整响应影响用户体验显存占用使用nvidia-smi -l 1持续观察显存占用需以实际模型和批次大小为准如果生成模型跑在本地还需要注意并发请求下的显存抖动。并发数过高时可能出现显存溢出需要降低并发数或使用批处理队列。7.3 降低资源占用的方法先跑检索打分再决定是否调用生成模型。检索已经判定不可回答时直接拒答跳过生成步骤可以省掉大量无谓开销。自评模型使用参数更小的模型只负责输出置信度不负责生成最终答案。对长文档先做摘要减少检索片段长度。设置生成超时时间避免无效请求占用推理资源。7.4 性能验证流程先在本地跑 50 条查询输出每条查询的处理时间。关注几个关键数字平均检索耗时、平均生成耗时、平均自评耗时、P95 总耗时。观察耗时分布定位瓶颈是检索阶段还是生成阶段。8. 常见问题与排查方法8.1 检索系统常见问题排查表问题现象可能原因排查方式解决方案系统对明显无关的问题也返回答案相似度阈值过低打印每条查询的相关分统计分布调高阈值或使用二次评分正确问题被拒答检索结果中没有正确答案片段检查知识库文档切分逻辑查看检索片段调整 chunk 大小或重写检索 query答案有支持证据但仍然是错的文档本身包含矛盾信息检查证据片段来源核对版本增加冲突检测拒绝输出或说明信息来源生成答案后自评分数很高但答案是幻觉自评模型与生成模型同源存在盲区用答案回测检索检查证据相似度引入独立证据回测信号批量评测脚本卡住单条查询超时或外部接口无响应查看日志检查超时设置为每条任务增加超时和重试部署后接口返回 502上游生成服务崩溃查看服务日志和显存占用降低并发数增加熔断机制8.2 容易混淆的“retrieval 报错”public key retrieval is not allowed可回答性系统经常需要从 MySQL 等数据库读取知识库内容。如果你用 JDBC 连接 MySQL 时遇到Public Key Retrieval is not allowed它和上面的语义检索无关它是数据库连接过程中的一个安全限制。原因MySQL 连接驱动默认不允许客户端从服务器自动获取公钥。连接时如果安全性要求不匹配就会出现这个报错。处理方式jdbc:mysql://127.0.0.1:3306/kbdb?useSSLfalseallowPublicKeyRetrievaltrue或者在连接配置中显式设置props.setProperty(allowPublicKeyRetrieval, true); props.setProperty(useSSL, false);注意允许公钥检索会降低连接安全性。生产环境更推荐在数据库服务器端正确配置 SSL 证书而不是长期允许公钥自动检索。这个报错提醒我们在排查检索系统时要区分“语义检索相关性问题”和“数据库连接配置问题”。8.3 检索效果不稳定的排查顺序先检查文档切分。切分过大可能塞入无关内容切分过小可能切断关键信息。再检查检索结果。用可视化工具或脚本打印 top-5 片段看相关性。然后检查答案生成。检索片段正确而答案错误问题在生成侧。最后检查自评逻辑。自评分数和人工判断不一致时不要盲目相信自评分数。9. 最佳实践与合规使用可回答性判断本质上是在给系统增加“不确定时说不”的能力。工程化落地时建议遵循下面几条实践。9.1 先建立测试集再调参不要在线上边调边看效果。先建立一套固定测试集包含可答、不可答、相似无答、冲突文档四类查询。每次调整阈值或算法都在同一测试集上跑对比指标变化。9.2 保留最小可运行配置把“向量检索 相似度阈值 拒答输出”作为第一版配置。它能解决大部分明显无关的问题。确认稳定后再逐步加入生成自评、证据回测和无答案分类器。9.3 正确看待拒答率拒答率高不一定说明系统变差。在很多知识库场景里宁可少答一部分也不要输出幻觉答案。但是要监控拒答原因如果大量正常问题被误判为不可回答说明检索路径或阈值设置有问题。9.4 日志与审计对每个“可回答”的结果记录支撑文档的编号、片段内容、文档来源。后续用户反馈答案错误时可以通过日志快速定位是检索问题、生成问题还是文档本身问题。9.5 合规与授权知识库内容的来源必须合法。私有文档需要确认授权范围公开网页抓取内容要遵守平台协议和版权要求。如果系统涉及个人数据、人脸信息、语音信息必须明确告知用户并取得授权不能把未授权的个人数据作为检索内容提供给第三方。9.6 接口服务安全可回答性判断 API 如果开放给内部多个业务线建议做好基础鉴权避免任意来源调用。批量任务建议在内网环境运行防止敏感查询内容泄露到外部服务。10. 总结与下一步可回答性判断不是一项单一功能而是一套围绕“检索证据”和“生成答案一致性”的工程方案。最值得先做的事情是搭一个带“无答案拒答”的最小检索服务用一个几十条的测试查询集跑一遍把准确率和拒答率记录下来。最先应该验证的是两件事第一明确可回答的查询能不能找对证据第二明显不可回答的查询能不能被拦下来。这两点通过相似度阈值大多就能改善后续再逐步加入更复杂的自评与回测逻辑。最容易踩的坑是直接设置一个固定阈值然后拿真实线上流量来调。由于不同知识库的相似度分布完全不同这样很容易造成“看起来能答实际错误率高”或“正确问题被大量拒答”。正确做法是先跑一批典型查询观察分数分布再结合人工标注确定阈值区间。下一步可以扩展的方向包括为不同知识库域训练独立的可回答性分类器。引入多路召回比如关键词检索与向量检索融合提升边界查询的覆盖。把“不确定原因”结构化输出供前端展示信息来源。对高价值场景增加人工复核队列自动判定为可回答但置信度处于中间区间的查询先进入人工确认。如果手头正好有一个知识库问答项目建议把这篇文章里的测试查询模板和批量评测脚本直接复制过去先跑一周看看系统到底在哪些查询上分不清“能答”和“不能答”。学会让检索系统承认自己不知道往往比让它回答得更快更重要。
返回列表