☰
05-M5-自适应检索与答案反思-让系统自己检查作业
2026/10/28 1:28:51 网站建设 项目流程

自适应检索 + 答案反思:让系统自己检查作业(M5 落地实测)

系列:城市管理 Agentic RAG —— 从零搭建城市管理问答系统
本篇:M5 · 自适应检索 + 答案反思(实测版)
源码:https://gitee.com/Chester_Xue/city-agentic-rag


一、M4 之后还差什么?

M4 加完路由,系统会「先分诊、再精准检索」了。但用久了又会冒出两个新毛病:

毛病一:检索是一次性的,查不到就死磕。
你问「气象局局长的电话是多少」——知识库里确实没有,但系统不会换个思路再试一次,直接按第一次的结果硬答,甚至可能拿一份「气象局预警等级」的资料凑数。

毛病二:回答是一次成型的,错了就错了。
大模型生成回答就像学生写完卷子直接交——不检查。它可能把资料里没有的数字、流程「脑补」进答案(幻觉),而且自己毫无察觉。

M5 要做的就两件事:

  1. 自适应检索——查不到就自动换姿势再查:扩大范围、去掉部门限制、重写问题,实在没有才告诉你「没有」。
  2. 答案反思——答完之后自己检查一遍:和引用资料对不对得上?有没有编造?有问题就重新回答。

打个比方:检索从「打一次电话没人接就放弃」,变成「没人接就换号码、换时间段再打,最后实在找不到人才留言」;回答从「写完就交卷」,变成「写完自己检查一遍再交」。

二、先看效果:验收场景实测

按推进表,M5 有两条硬验收,先看结果:

验收 ①:问库内没有的东西,要明确告知,不编造

❓ 问题 > 气象局局长的电话是多少? [路由] weather → 检索范围: 气象局/应急局(llm) [检索] 首轮 top_k=3(气象局/应急局)最高分 0.513 [检索] 扩展① top_k=5(全部部门)最高分 0.580 [反思] 核查通过(与引用资料一致,无编造) 💬 回答: 根据您提供的资料,无法查询到气象局局长的电话。 (所有资料中均未包含任何个人联系方式) 建议通过官方渠道查询:气象局官网「联系我们」栏目、12345 热线咨询。

首轮检索最高分只有 0.513,低于强相关线 0.6——系统没有死心,自动扩大范围(top_k 3→5、放开全部门)再搜一次,发现确实没有,才明确告知「查不到」,还给出了替代建议。全程零编造。

验收 ②:故意让模型引用矛盾信息,反思要能抓出来

我构造了一个「带病回答」喂给反思模块——资料写救护车 45 辆,回答却编成 300 辆:

反思输入(模拟坏回答): 「根据资料,XX区有救护车 300 辆,平均响应时间 3 分钟,资源非常充足。」 反思输出: 不一致,存在编造信息。 引用资料中明确写明:全市急救站 12 个,救护车 45 辆,平均响应时间 11 分钟。 而回答中声称「XX区有救护车 300 辆,平均响应时间 3 分钟」, 这些数字在引用的资料中完全不存在,属于编造。 重新回答如下: 根据资料,全市共有急救站 12 个,救护车 45 辆,平均响应时间 11 分钟。 资料中未单独提供「XX区」的急救资源数据,因此无法判断该区资源是否充足……

编造的数字被逐条揪出,并且自动重新回答。两条验收全过。

三、子任务 5.1:自适应检索

1. 核心思路:一轮查不到,就多轮扩展

defadaptive_retrieve(query,top_k=3,dept_filter=None,threshold=0.6,expand_top_k=5,accept=0.5):# 第 1 轮:常规检索(按路由范围)hits=retrieve(query,top_k=top_k,dept_filter=dept_filter)best=最高分(hits)ifhitsandbest>=threshold:# 强相关,直接返回returnhits,trace# 第 2 轮:扩大 top_k + 移除部门过滤(全库)hits=retrieve(query,top_k=expand_top_k,dept_filter=None)best=最高分(hits)ifhitsandbest>=accept:# 达到接受线,交给生成returnhits,trace# 第 3 轮:关键词重写问题再搜一次rewritten=rewrite_query(query)ifrewritten!=query:hits=retrieve(rewritten,top_k=expand_top_k,dept_filter=None)...# 全部低于接受线 → 明确告知「知识库暂无相关信息」

三步策略正好对应三种「为什么查不到」:

  • ①top_k 3→5:可能是答案在候选池的边缘,多捞两条
  • ②移除部门过滤:可能是路由判错了部门(比如以为是民政问题,其实资料在卫健),放开全库再找
  • ③关键词重写:可能是问题本身太「啰嗦」,把疑问词去掉让检索更聚焦

2. 关键词重写:零成本、可解释

重写不做复杂 NLP,就是剔疑问词:

_QUESTION_WORDS=["请问","为什么","怎么","如何","多少","什么","哪些","哪里","哪个","是否","有没有","吗","呢","的","?","?"]defrewrite_query(query):rewritten=query.strip()forwordin_QUESTION_WORDS:rewritten=rewritten.replace(word,"")returnrewrittenorquery.strip()# 重写后为空则返回原句

「气象局局长的电话是多少?」→「气象局局长电话」。不调大模型、零成本、结果可解释——这种「笨办法」在检索链路里往往比花哨方案更稳。

3. 踩坑①:0.6 一刀切,差点误杀真答案

推进表原方案是「首次检索最高分 < 0.6 → 扩展」。但实测发现一个残酷事实:数据量小的时候,bge 的相似度分数整体偏低。比如「急救资源是否充足?」这种知识库里确实有答案的问题,最高分只有 0.543——按 0.6 一刀切,系统会直接说「暂无相关信息」,把真答案误杀了。

修复:双线设计。

线值作用
强相关线 threshold0.6首轮 ≥ 0.6 → 高置信,直接答,不折腾
接受线 accept0.5扩展后 ≥ 0.5 → 有候选,交给生成环节判断

分数在 0.5~0.6 之间的「弱相关」怎么办?不硬拒答,交给生成环节的 Prompt 硬约束兜底——「信息不足请明确告知」。检索负责「有候选就捞上来」,模型负责「捞上来的够不够、要不要说不知道」,各司其职。

这个教训的本质:阈值不是拍脑袋定的,要拿真实数据测。我当时差点因为一个「看起来很合理」的 0.6,把验收场景直接打挂。

四、子任务 5.2:答案反思

1. 核心思路:生成之后,再核查一遍

classLLMClient:defreflect(self,question,answer,context,...):prompt=REFLECT_PROMPT.format(question=question,answer=answer,context=context)returnself.chat(prompt,temperature=0.0,max_tokens=512)

反思 prompt(推进表原文):

请检查以下回答是否与引用的资料内容一致,是否存在编造信息。 若存在问题,请重新回答。若一致,直接输出"确认无误"。 问题:{question} 回答:{answer} 引用:{context}

判定逻辑:模型输出「确认无误」→ 放行;输出其他内容 → 那本身就是模型修正后的回答,系统标记「(已修正)」后采用。

ifREFLECTION_ENABLED:verdict=llm.reflect(question,answer,context)ifverdict.strip().startswith("确认无误"):print(" [反思] 核查通过(与引用资料一致,无编造)")else:answer=f"(已修正){verdict}"# 反思输出即修正版回答print(" [反思] 核查发现问题,已重新回答 → 标记「已修正」")

多花一次 LLM 调用,换来「答案经过核验」。开关REFLECTION_ENABLED=false可以随时关掉提速——演示用开,批量跑用关。

2. 踩坑②:给反思 prompt「加料」,模型反而躺平

第一版反思 prompt 实测:编造数字能触发修正(见第二节),但另一种编造抓不住——「资料没提、但听起来合理」的软编造。比如资料只说低保条件,回答却脑补「申请需提交房产证复印件并缴纳 500 元手续费」。模型判了「确认无误」。

我想当然地去给 prompt 加判定标准:

判定标准: 1. 回答中的具体事实必须在引用资料中有依据; 2. 引用资料中未提及的具体事实,一律视为编造; 3. 若资料不足以回答,应说明信息不足……

结果翻车了——加了判定标准之后,模型对明显编造数字的坏回答也输出「确认无误」。连测三次,三次全躺平。像是学生检查作业时拿到一本《检查手册》,反而懒得逐条对照了。

A/B 对比很说明问题:

[A 原版简洁 prompt] → 发现 300辆 vs 45辆 矛盾,逐条指出并重新回答 ✅ [B 加了判定标准] → 输出「确认无误」 ❌

结论:反思 prompt 保持简洁,效果最好。简洁 prompt 让模型真正去「读一遍」,加规则反而给了它「走捷径」的借口。最终选择:软编造是当前模型能力边界,诚实记录,二期可以用规则校验器(数字/实体比对)兜底——但那是后话,不能为了抓软编造把硬矛盾也放跑了。

五、完整链路:路由 → 自适应检索 → 生成 → 反思

❓ 问题 > XX社区独居老人需要救助吗? [路由] civil+weather → 检索范围: 民政局/气象局/应急局(keyword) [检索] 首轮 top_k=3(民政局/气象局/应急局)最高分 0.661 [反思] 核查通过(与引用资料一致,无编造) 💬 回答: 根据现有资料,无法直接判断XX社区独居老人是否需要救助…… [1](民政局/弱势群体统计.txt)仅提到「独居老人:156人」和 「需重点关注社区:XX社区」,未说明其经济状况…… 救助资格需依据低保或临时救助标准逐户核查…… 建议:提供具体收入、财产、健康状况等信息后再判断。

注意回答的克制:资料里没有的信息,明确说没有,不硬下结论。这既是 M3 Prompt 硬约束的功劳,也是反思环节「再确认一遍」的成果。

六、验收总览

验收项结果
① 问「气象局局长的电话是多少」(库内没有)✅ 首轮 0.513 → 扩展① 0.580 → 仍无果,明确告知「无法查询到」+ 替代建议,零编造
② 构造矛盾回答(45辆 vs 编造300辆)✅ 反思逐条指出矛盾数字,自动重新回答

回归抽查(M1~M4 能力不退化):

  • 暴雨响应 → 首轮 0.739 强相关直接答 ✅
  • 独居老人救助 → 路由联动 civil+weather,反思通过 ✅
  • 低保申请 → 只查民政,不误伤 ✅
  • 你好 → 零成本欢迎语,不调模型 ✅

七、本阶段小结

M5 给系统补上了两个「Agent 味」的能力:

  1. 不轻言放弃——检索失败会自动换三种姿势再查,实在没有才说没有;
  2. 不交卷就走——回答完自己检查一遍,发现编造就重答并标记。

这两个能力加上 M4 的路由,系统已经形成「分诊 → 精准找 → 答完自查」的完整闭环。所有可调参数(阈值、扩展条数、反思开关)都收进了config.py,一行配置就能调行为——这就是推进表一直强调的「接口预留」在落地。

八、下一步

M5 之前,系统是「被动应答」:你问什么,它答什么。M6 开始加「主动指令」——部门过滤命令(/dept 卫健委 急救资源)、综合研判输出(路由结果为 all 时输出四部门分块报告),让系统从「问答机」变成「研判助手」。

上一篇:M4 Agentic 路由:让每个问题找到对的部门
系列目录:城市管理 Agentic RAG —— 从零搭建城市管理问答系统


📚想了解更专业的内容?本文是项目实战记录。如果你对 RAG 的原理、Prompt 工程技巧、大模型 API 接入的完整方案感兴趣,欢迎访问我的 CSDN 专栏喵本喵叁肆的 Agentic RAG 实战专栏阅读完整的技术博客系列(含可运行代码、架构图与验收标准)。

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

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

立即咨询