Agentic RAG:受控多跳检索与证据闭环设计
发布于
到了现在 我终于明白 当初您那句话真正的意义,感到幸苦的时候 就回来吧,无论何时。
文档性质:Phase 3 新增模块的学习说明与系统架构
代码核对日期:2026-08-25
当前实现:基于 LangGraph 的受控 Agentic Multi-Hop Retrieval
能力边界:当前 Phase 3 输出多跳证据、路由决策与完整 Trace,不负责生成最终自然语言答案
Phase 2 已经完成一条固定的 Advanced RAG 链路:问题经过 Dense 与 BM25 双路召回、RRF 融合和 Reranker 精排,得到一次检索的 Top-K 证据。它解决了“怎样把相关证据找得更全、排得更准”,但整条链路仍然只执行一次。
当问题需要先发现中间实体,再围绕中间实体继续查找时,一次检索可能只找回证据链的一部分。例如:
创办 A 公司的人后来领导了哪家机构?
完整回答至少需要两项事实:
- A 公司的创办者是谁;
- 这位创办者后来领导了哪家机构。
如果第一轮只检索到“A 公司由张三创办”,固定链路不会主动把“张三”变成下一轮查询。Phase 3 因此在 Phase 2 检索器外增加一个有状态、受预算约束的控制闭环:先规划必须找到的证据,再逐跳检索、逐项验收,证据缺失时决定进入下一跳或纠正当前查询。
本文先分别说明 Phase 3 新增模块解决什么问题、依据什么原理工作,再给出完整的 Agentic RAG 架构。
1. Phase 3 新增模块
1.1 Models:把检索结果扩展成可执行、可审计的 Agent 状态
Phase 2 的核心对象是 SearchHit。它回答“某个 Chunk 在 Dense、BM25、RRF 和 Rerank 中排第几”。Phase 3 还需要表达四类新信息:问题被怎样规划、哪些事实必须有证据、每一跳做了什么,以及整个任务为什么结束。
EvidenceRequirement:必须由检索证据直接支持的原子事实
EvidenceRequirement(
requirement_id="R1",
description="证明 A 公司的创办者是谁。",
)
Router 不直接填写答案,而是把完整回答拆成一组可独立核验的需求。requirement_id 是后续 Grader 对齐需求和证据的稳定标识,description 说明必须查明的事实。
RouteDecision:一次路由规划
RouteDecision(
route="multi_hop",
query="A 公司的创办者是谁?",
reason="需要先发现创办者,再查询其后续任职。",
requirements=[R1, R2],
)
其中,route 只能是 single_hop 或 multi_hop;query 是马上执行的首跳检索词,不一定等于原始问题;requirements 是最终完成任务前必须全部被真实 Chunk 支持的证据清单。
RequirementAssessment:一项需求当前是否有证据
RequirementAssessment(
requirement_id="R1",
status="supported",
evidence_chunk_ids=["chunk-a17"],
)
status 只有 supported 和 missing。标记为 supported 时不能只给自然语言理由,还要绑定一个或多个真实 chunk_id。
EvidenceDecision:一次证据验收的整体裁决
EvidenceDecision(
verdict="continue",
reason="R1 已满足,R2 仍缺少证据。",
next_query="张三后来领导了哪家机构?",
next_requirement_id="R2",
failure_reason=None,
requirement_assessments=[...],
)
它同时保存逐项评估和流程决策:complete 表示全部需求已经被支持;continue 表示取得了有效进展,但要查下一项事实;insufficient 表示当前查询没有可靠进展,应尝试纠错。
HopTrace:一次检索尝试的可回放记录
HopTrace(
hop_index=2,
is_correction=False,
query="张三后来领导了哪家机构?",
hits=[...],
decision=EvidenceDecision(...),
elapsed_seconds=2.41,
stage_calls={...},
)
同一个 hop_index 可以出现普通尝试和纠错尝试。is_correction=True 表示它没有进入新事实,而是在同一跳中用修正后的查询重新检索。
AgenticRetrievalResult:完整 Agentic 检索结果
AgenticRetrievalResult(
question="创办 A 公司的人后来领导了哪家机构?",
route_decision=RouteDecision(...),
final_hits=[...],
traces=[...],
termination_reason="complete",
total_hops=2,
correction_count=0,
elapsed_seconds=5.82,
stage_calls={...},
)
它不只给最终 final_hits,还保留从 Router 到每次 Grader 判断的完整过程。这使 Phase 3 可以区分路由失败、检索失败、证据判断失败、预算耗尽和正常完成,而不是把所有错误都归结为“最终没有答对”。
1.2 Query Router:把原始问题转换成首跳查询与证据计划
Router 的职责不是检索,也不是回答,而是回答两个控制问题:
- 当前问题一次检索是否足够;
- 完整回答必须由哪些原子证据直接支持。
Router 的输入只有原始问题:
问题:创办 A 公司的人后来领导了哪家机构?
当前 Prompt 模板为:
你是一个多跳检索路由助手(Query Router)。
请分析用户问题是否需要分解为多个检索步骤(多跳检索,发现中间实体/线索后才能找到最终答案),还是可以通过单次检索直接找到答案。
规则:
1. 如果是简单事实、单实体或单个主题问题,判定为 single_hop,query 保持或轻微优化原始问题。
2. 如果问题涉及实体跳转(如“A 创立者的毕业院校”)、对比多方事实或时间线推理,判定为 multi_hop,query 为寻找第一个中间实体的首跳检索词。
3. 将原问题拆成完整回答时必须由检索证据直接支持的最小需求清单。不要填写答案,不要凭常识补充问题中没有的条件。
4. single_hop 通常只有 1 项需求;multi_hop 应包含多个彼此可独立核验的需求。需求数不等于检索跳数:一次检索可以覆盖多项需求。
5. 不要把最终答案综合、比较结论或“确认前述实体相同”单独列为需求;这些应由前述原子证据自然推出。
请严格仅输出以下 JSON 格式:
{
"route": "single_hop" | "multi_hop",
"query": "首步检索查询词",
"reason": "判断依据",
"requirements": [
{"requirement_id": "R1", "description": "必须检索并证明的条件"}
]
}
问题:{question}
一份合理的多跳输出是:
{
"route": "multi_hop",
"query": "A 公司的创办者是谁?",
"reason": "需要先确定创办者这一中间实体,再检索其后续领导机构。",
"requirements": [
{"requirement_id": "R1", "description": "证明 A 公司的创办者是谁。"},
{"requirement_id": "R2", "description": "证明该创办者后来领导了哪家机构。"}
]
}
校验成功后,node_route() 将首跳状态初始化为:
{
"route_decision": decision,
"current_query": decision.query,
"current_hop": 1,
"is_correction": False,
"correction_count_current_hop": 0,
}
1.3 retrieve_hop:每一跳继续复用 Phase 2 检索器
Phase 3 没有重新实现 Dense、BM25、RRF 或 Reranker。node_retrieve() 把当前 current_query 直接传给 Phase 2 的 retrieve_hits():
retrieval_res = retrieve_hits(
config=config,
embedder=embedder,
question=current_query,
profile=config.agentic.base_profile,
reranker=reranker,
generator=generator,
dense_store=dense_store,
bm25_store=bm25_store,
top_k=config.retrieval.top_k,
)
当前 base_profile=hybrid-rerank,所以每一跳执行:
当前查询
├─ Dense Top-20
└─ BM25 Top-20
↓
RRF Top-30
↓
Rerank Top-K
Agentic RAG 改变的是“调用几次、每次查什么、何时停止”,不是 Phase 2 检索算法本身。FAISS 与 BM25 索引在状态图运行前加载一次,后续所有跳复用同一只读索引,避免每跳重复加载。
1.4 Evidence Grader:逐项验收累计证据,而不是猜答案
Evidence Grader 同时读取原始问题、当前查询、Router 的全部需求,以及截至当前所有跳的累计证据。不同跳的命中先按轮询方式交织并去重,再连同真实 chunk_id、来源和正文进入 Prompt。
当前 Prompt 模板为:
你是一个多跳检索证据评估助手(Evidence Grader)。
请根据累计检索证据逐项评估证据需求。你的任务是判断证据集合是否完整,不是猜测最终答案。
原始问题:{question}
当前步查询词:{current_query}
必须覆盖的证据需求:
{requirements}
截至当前累计检索到的证据段落:
{passages_with_chunk_ids}
强制规则:
1. 必须为每一项 requirement 输出一项 assessment,不能遗漏或增加需求。
2. 只有段落直接支持需求时才标记 supported,并绑定一个或多个真实存在的 chunk_id;否则标记 missing。
3. 已经知道或可以猜出最终答案,不代表证据完整。只要有任何 requirement 为 missing,就不得返回 complete。
4. 有缺失需求且本轮取得了有效进展时返回 continue,并针对一个缺失需求生成下一跳查询。
5. 本轮没有取得可靠进展时返回 insufficient,并说明失败原因。
请严格仅输出以下 JSON 格式:
{
"verdict": "complete" | "continue" | "insufficient",
"requirement_assessments": [
{"requirement_id": "R1", "status": "supported" | "missing", "evidence_chunk_ids": ["真实 chunk_id"]}
],
"next_requirement_id": "下一步要解决的缺失需求 ID(若 continue)或 null",
"next_query": "下一跳具体查询词(若 continue)或 null",
"reason": "判断简要理由",
"failure_reason": "证据不足的原因(若 insufficient)或 null"
}
Grader 的三个 verdict 含义不同:
| verdict | 证据状态 | 后续动作 |
|---|---|---|
complete | 所有需求均已有直接证据 | 进入finalize |
continue | 本轮取得进展,但仍缺少下一项事实 | 增加current_hop,使用 next_query 检索 |
insufficient | 当前查询没有取得可靠进展 | 不增加跳数,进入 Query Corrector |
1.5 Query Corrector:同一事实没有找到时,换一种检索表达
continue 和 insufficient 代表两种不同问题:
continue = 已找到部分事实,下一步要查新的事实
insufficient = 当前事实没有找到,应该修正当前查询
Query Corrector 接收原始问题、失败查询、Grader 的失败原因和所有未满足需求:
原始问题:{question}
失败查询词:{failed_query}
失败原因:{failure_reason}
未满足的证据需求:{missing_requirements}
当前 Prompt 要求:
你是一个检索词纠错助手(Query Corrector)。
当前检索词未能检索到足够证据,请根据失败原因和未满足的证据需求,重新改写生成更具针对性、同义替换或关键词更精准的检索词。
请严格仅输出以下 JSON 格式:
{
"corrected_query": "改写后的新检索词",
"reason": "改写理由"
}
例如:
{
"corrected_query": "张三 后来 担任 负责人 机构",
"reason": "加入任职关系词,减少仅命中早期生平的结果。"
}
成功后,节点设置 is_correction=True、当前跳纠错次数加一,并回到 retrieve_hop。它不会增加 current_hop,因为目标仍然是寻找同一项缺失事实。
若 Corrector 最终失败,代码保留原查询、消耗一次纠错预算并重新检索;默认每跳最多纠错一次,因此不会形成无限循环。
1.6 多跳证据汇总:先保留需求证据,再按跳轮询补位
多跳检索结束后,系统会得到多组结果:第 1 跳找到“谁创办了 A 公司”,第 2 跳找到“这个人后来领导了什么机构”。现在还差最后一个问题:怎样把不同跳的结果合成一组 final_hits?
不能直接把所有 Reranker 分数放在一起从高到低排序。因为每一跳的问题不同,分数只表示 Chunk 与“当前这一跳查询”的相关程度。第 1 跳的 0.92 和第 2 跳的 0.86,不是同一场考试里的两个成绩。
当前系统采用两个简单原则。
第一,必要证据优先。 Evidence Grader 已经指出哪些 Chunk 分别支持 R1、R2 等证据需求。系统先把这些 Chunk 放入最终结果,保证完整证据链不会被普通的相似结果挤掉。
第二,剩余位置按跳轮流补充。 必要证据放好后,如果 Top-K 还有空位,就依次从第 1 跳、第 2 跳、第 3 跳取一个候选,再进入下一轮。重复 Chunk 会跳过。
先放:R1 证据 → R2 证据
再补:hop 1 第 1 名 → hop 2 第 1 名 → hop 1 第 2 名 → hop 2 第 2 名
例如,Grader 确认 bound-r1 支持 R1,bound-r2 支持 R2,最终需要 5 个 Chunk:
final_hits = [bound-r1, bound-r2, h1-r1, h2-r1, h2-r2]
前两个位置保证问题所需的两项事实都在;后三个位置从不同跳轮流补充背景证据。最终结果不会尝试计算一个新的“跨跳总分”,而是优先保证证据完整,再兼顾不同检索跳的多样性。
代码中,compose_final_hits() 负责整个组合过程,interleave_hop_hits() 负责后半段的轮流补位。理解这两个原则即可,不需要把它们当成新的检索算法。
1.7 AgenticState 与 LangGraph:把循环显式写成状态转移
普通 RAG 从问题到答案只向前执行一次,局部变量就足以传递数据。Agentic RAG 会多次检索、判断和回退:第 2 跳必须知道第 1 跳找到了什么,纠错时也必须知道当前失败的是哪一个查询。因此,它需要一本贯穿整次任务的“共享工作笔记”,这就是 AgenticState。
这本工作笔记主要记四类信息:
- 任务计划:原始问题、Router 判断和必须找到的证据;
- 当前位置:现在是第几跳、正在查什么、是否属于纠错;
- 证据进度:各跳已经找到的 Chunk,以及哪些需求已满足;
- 运行结果:每次尝试的 Trace、最终证据和结束原因。
节点不需要私下记忆之前发生的事情。它读取当前 State,完成自己的工作,再把新增或变化的内容写回 State。
例如,系统开始时只知道用户问题;Router 执行后,State 中出现首跳查询和 R1、R2;第 1 跳检索后加入 hits₁;Grader 判断 R1 已满足、R2 缺失,于是写入下一跳查询;第 2 跳完成后,State 才拥有完整证据链和全部 Trace。
如果把 AgenticState 理解为共享工作笔记,那么 LangGraph 就是根据笔记内容指挥下一步的“流程调度员”。它不负责 Dense、BM25、RRF、Rerank,也不负责判断证据语义;这些工作仍由前面介绍的节点完成。LangGraph 只做两件事:
- 按顺序调用节点;
- 根据 Grader 的 verdict 选择下一条边。
主流程始终是:
Router → Retrieve → Grader
真正体现 Agentic 的地方发生在 Grader 之后:
complete:证据已经完整,进入finalize;continue:已经取得进展,但还缺下一项事实,增加一跳后重新检索;insufficient:当前查询没有可靠进展,不增加跳数,纠正查询后重试。
循环必须有上限,否则错误的 Router 或 Grader 可能让系统反复检索。当前有三层保护:
最多 3 跳
每跳最多纠错 1 次
整张图最多流转 25 次
达到跳数或纠错预算后,系统进入 finalize,保留已经找到的证据并记录对应终止原因;若 Router、检索或 Grader 本身失败,也会进入同一个结束节点,而不是让流程失控。
因此,LangGraph 在本项目中的价值不是提供新的 RAG 算法,而是让“当前状态 → 判断条件 → 下一步动作”变成一张明确、可回放、不会无限循环的流程图。
1.8 agentic_eval.:用同一题逐题比较固定 Baseline 与 Agentic 系统
Agentic 流程增加调用次数和延迟,不能因为“会循环”就认为质量一定提高。agentic_eval.py 使用独立的 MultiHop-RAG 公开基准,把同一道题分别交给:
Baseline:一次 hybrid-rerank 检索
Agentic:Router + 多跳 hybrid-rerank + Grader / Corrector
两套系统复用相同 FAISS、BM25 索引和基座 Profile。评测记录 Evidence Coverage@4/@10、Complete Evidence@4/@10、Hits、MAP、MRR、延迟;Agentic 额外记录平均跳数、纠错率、路由分布和终止原因。system=both 时按相同题号计算成对 Delta 与 95% 置信区间,避免只比较两个独立平均数。
开发集和测试集按 inference_query、comparison_query、temporal_query 三类分层抽样并保存题目 ID。评测支持题目级并发和 checkpoint.json 断点恢复,但 Profile、语料与评分口径保持固定。
2. Agentic RAG 架构
2.1 LangGraph 状态转化图
状态在六个节点之间逐步变化:
route_query写入路由计划、首跳查询和current_hop=1;retrieve_hop写入本跳命中与累计证据;grade_evidence写入需求评估、verdict 和 Trace;continue经过prepare_next_hop增加跳数,再回到检索;insufficient经过correct_query修正当前查询,再回到检索;complete、预算耗尽或节点失败进入finalize,写入最终证据和终止原因。
3. Agentic RAG 评测:会多跳,不等于检索一定更好
前面的章节说明了 Agentic RAG 如何规划、检索、验收和循环,但系统能够完整执行这些节点,并不能证明它比一次固定检索更有效。Agentic 流程还增加了 Router、Grader 和多轮检索,质量收益必须与额外成本一起观察。
本章使用一次已完成的 MultiHop-RAG 开发集评测:
runs/multihoprag/agentic_evaluations/dev-20260825-223259-005945
评测只检查检索证据,不调用最终答案生成。因此,它能回答“正确证据是否被找回、是否排得更靠前”,不能回答“最终自然语言答案是否正确”。
3.1 评测要回答三个问题
Agentic RAG 的检索评测可以从浅到深分成三层:
- 前排排序:正确证据是否更早出现在 Top-4;
- 完整证据:Top-10 是否找齐回答问题所需的全部事实;
- 运行代价:为了这些变化,增加了多少跳数、模型调用和延迟。
其中,Evidence Coverage 表示 Gold 事实被覆盖的比例;Complete Evidence 要求一题的全部 Gold 事实都出现;MRR 观察第一条正确证据排得有多靠前。
这三层不能互相替代。第一条正确证据排到第 1 名,不代表其余必要事实也已经找齐;同样,Agent 自己返回 complete,也不代表外部 Gold 一定确认完整。
3.2 Baseline 与 Agentic 如何公平比较
本次使用固定的 dev 60 题:inference_query、comparison_query、temporal_query 各 20 题。Baseline 与 Agentic 复用同一份英文语料、FAISS/BM25 索引、hybrid-rerank 基座 Profile、Top-10 和 Gold 评分规则。
两套系统的主要区别只有检索控制方式:
Baseline:问题 → 一次 Hybrid-Rerank → Top-10
Agentic:问题 → Router → 最多 3 跳 Hybrid-Rerank
→ Grader / Corrector → Top-10
每道题同时得到一条 Baseline 记录和一条 Agentic 记录,因此可以逐题计算 Delta,而不是只比较两个独立平均数。本次两边均完成 60/60 题,没有检索执行失败记录;Agentic 内部另有 1 题以 router_failed 结束,并按实际空结果参与评分。
3.3 核心结果:前排证据明显改善,Top-10 完整性没有提升
| 指标 | Baseline | Agentic | 逐题平均变化 |
|---|---|---|---|
| Evidence Coverage@4 | 25.42% | 43.06% | +17.64 个百分点 |
| Complete Evidence@4 | 5.00% | 15.00% | +10.00 个百分点 |
| MRR@10 | 0.3516 | 0.5903 | +0.2387 |
| Evidence Coverage@10 | 48.61% | 50.28% | +1.67 个百分点 |
| Complete Evidence@10 | 20.00% | 20.00% | 无变化 |
Top-4 的两项指标和 MRR 都明显提高。逐题配对结果中,Coverage@4 的 95% 置信区间为 [+8.86,+26.42] 个百分点,Complete@4 为 [+1.04,+18.96] 个百分点,MRR@10 为 [+0.1323,+0.3452],区间均位于 0 以上。
但把观察范围扩大到 Top-10 后,结论不同:Coverage@10 只提高 1.67 个百分点,95% 置信区间 [-6.66,+9.99] 跨过 0;Complete@10 完全没有变化,Baseline 和 Agentic 都只有 20%。
因此,本次结果最准确的解释是:
Agentic RAG 更擅长把正确证据推到前面,但没有稳定找回更多完整证据。
这与 1.6 的多跳证据汇总机制一致:Grader 绑定证据和跨跳组合可以改善最终前排顺序,但如果某项事实从未被任何一跳召回,最终汇总也无法凭空补出证据。
3.4 不同问题类型的收益并不一致
| 问题类型 | Coverage@10 变化 | Complete@10 变化 | MRR@10 变化 | 观察 |
|---|---|---|---|---|
| Inference | -5.0 pp | +5.0 pp | +0.1729 | 排序提高,但总体事实覆盖略降 |
| Comparison | +7.5 pp | +10.0 pp | +0.3131 | 三类中收益最稳定 |
| Temporal | +2.5 pp | -15.0 pp | +0.2303 | 第一条证据更靠前,但完整证据明显下降 |
Comparison 问题通常需要从多篇文章中找到可以直接对照的事实,逐跳检索与证据绑定在这类问题上最容易形成清晰收益。Temporal 问题则不仅要找到多篇文章,还要正确保持时间顺序和时间点约束;当前 Agentic 虽然提升了前排排名,却更容易漏掉组成完整时间链的部分证据。
所以不能把整体 MRR 提升写成“Agentic 对所有多跳问题都更好”。当前证据支持的是:排序收益较普遍,完整性收益依赖问题类型,Temporal 仍是明显短板。
3.5 Agent 实际运行了多少跳,又为什么结束
60 道题平均执行 2.2 跳。Router 将 45 题判为 multi_hop,15 题判为 single_hop;只有 2 题发生查询纠错,纠错率为 3.3%。这说明当前 Agentic 的额外检索主要来自 continue → prepare_next_hop,而不是频繁触发 Corrector。
最终有 42 题由 Grader 判定 complete,17 题达到 max_hops_reached,1 题 router_failed。达到最大跳数并不表示完全没有正确证据,只表示在三跳预算内,Grader 仍未确认所有 Router 需求都已满足。
这些过程指标用于解释 Agent 怎样运行,不能代替 Gold 检索指标。complete 是系统内部判断,Complete Evidence@10 才是外部基准对证据完整性的检查。
3.6 最值得关注的问题:Grader 自认为完成,不等于 Gold 真的完整
在 42 道 termination_reason=complete 的题中,只有 10 题同时达到 Gold Complete Evidence@10,另外 32 题仍缺少至少一项 Gold 事实。也就是说,Grader 经常认为 Router 需求已经被证据覆盖,但外部标注并不认可这条证据链完整。
相反,在 17 道达到最大跳数的题中,有 2 题其实已经达到 Gold Complete Evidence@10。这说明 Grader 也存在少量“证据已经完整,却仍然继续检索”的情况。
3.7 质量提升付出了多少成本
Agentic 每题平均调用约 2.22 次 Embedding、2.22 次 Rerank 和 2.22 次 Grader,并额外调用一次 Router;平均总耗时为 93.434 秒。Baseline 每题只执行一次 Hybrid-Rerank。
本次 Baseline 平均耗时为 0.944 秒,中位数为 0.884 秒;Agentic 平均为 93.434 秒,中位数为 85.302 秒。
4. 总结
Phase 3 的核心变化可以总结为:
Phase 2 负责把一次查询检索好;Phase 3 负责根据证据状态决定下一次查什么、是否要纠错,以及何时可以停止。
这也是当前实现中“Agentic”的准确边界:检索能力仍来自透明的 Phase 2 模块,LLM 负责生成受约束的控制信号,确定性门控和预算负责限制错误与循环,LangGraph 只负责编排可回放的状态转移。
从当前 dev 评测看,这套控制层已经改善了前排证据排序,但证据完整性、Grader 校准和运行成本仍然是下一阶段需要解决的核心问题。