做知识库问答项目做了快半年,我最大的感触是:RAG 的上限,取决于它会不会自我纠错。刚上手那会儿,我用的是教科书级别的标准流程——用户提问,向量检索,拼 Prompt,丢给 LLM 生成。简单问题还好,一旦遇到“帮我对比一下 A 和 B 两套方案的优劣,并结合我们今年的预算给个倾向性建议”这种复合问题,系统就开始答非所问:要么检索不全,要么拿到的文档和问题八竿子打不着,要么模型对着几段相似文本自信地编了一个答案。最后朋友一句话点醒我:你的 RAG 只会执行,不会反思。于是我把目光投向 LangGraph,开始构建一个真正“会反思”的 Agentic RAG 问答 Agent。这篇文章就把我从 0 到 1 的实现过程、代码细节和踩坑记录完整展开,适合已经跑通过基础 RAG、想进一步做智能体化改造的开发者参考。
1. 先把问题看透:传统 RAG 到底卡在哪里
1.1 你用的“伪 RAG”其实只是一条流水线
很多教程里教的 RAG,本质是一条固定流水线:query -> 向量检索 -> 拼上下文 -> 生成。它的问题不在于“检索”和“生成”这两个动作本身,而在于没有任何一个环节会审视自己的输出质量。检索结果的排名是固定的,哪怕排第一的那段文本和问题根本不相关,流水线也会把它喂给大模型;生成的答案只要格式对,就没人检查它是不是答非所问。
我拿一个真实场景说明。我的知识库里有一批产品手册和售后工单。用户问:“上次那个登录报错的问题,后来有没有在 2.3.1 版本里修掉?”传统 RAG 的做法是把“登录报错”“2.3.1 版本”拆成语义向量去检索。但售后的工单里描述问题时用的词可能是“认证失败”“session 过期”,版本号在文档里又经常被写成v2.3.1、2_3_1各种格式。向量检索很可能召回一堆“相关但不对路”的片段,LLM 只能基于这些错误上下文硬答。结果就是:用户拿到一个看似流畅、实则没解决任何问题的答案。
这类失败有一个共同特征:系统缺少“知道自己不知道”的能力。它不评判当前检索结果是否足够支撑回答,也不追问“还缺什么信息”。这就是传统 RAG 的天花板。
1.2 从“检索增强生成”到“Agentic RAG”,差别在哪里
Agentic RAG 不是把检索流程包一层 Agent 壳子,它的核心变化是:把“检索—评估—再检索—生成”的控制权交给智能体,让每一步都由模型根据当前状态做决策。整个流程变成了一个闭环回路,而不是单向管道。
我整理过一张对比表(虽然博客里画不了图,但表格更直观):
| 维度 | 传统 RAG | Agentic RAG |
|---|---|---|
| 决策方式 | 固定管道,检索后直接生成 | 模型自主决定是否检索、检索什么、是否重查 |
| 查询处理 | 用户原句直接向量化 | 支持条件路由、查询改写、拆解子问题 |
| 质量反馈 | 无反馈,一次到底 | 生成后进行自我评估,失败则触发补偿机制 |
| 适用问题 | 简单事实型问答 | 多跳推理、对比类、需要实时知识的问题 |
| 可解释性 | 黑盒,无法追踪 | 每一步状态可见,可回溯、可干预 |
| 工程复杂度 | 低,几行代码跑完 | 高,需要图编排、状态管理 |
有一件事得说清楚:Agentic RAG 不是万能药。如果你只做“某产品的退货政策是什么”这类单跳检索,传统 RAG 就够了,引入反思回路反而增加延迟和 token 开销。但做知识库问答 Agent,用户的问题一定五花八门,所以我很早就决定:要么不做,要做就做成“会反思”的。
1.3 顺便把三类知识库的边界理清楚
热搜词里有人问“kg 知识库、rag 知识库和结构知识库怎么区分”,这里顺带说一嘴。知识库只是数据的组织形式,和上层是不是 Agentic 没有必然关系:
- 结构知识库:指强 schema 约束的数据源,比如关系型数据库里的订单表、配置表,适合精确查询(SQL 或参数化查询)。
- RAG 知识库:通常指非结构化文本切块 + 向量化后组成的集合,适合模糊语义检索,但无法做精准条件过滤。
- 知识图谱(KG):用实体和关系组织三元组,适合回答“谁和谁之间有什么关系”这类多跳问题。
做 Agentic RAG 时,我推荐你把它们混着用。比如用户问“最近一个月哪些工单提到了登录异常”,普通向量库回答不了“最近一个月”这个时间约束,如果我把工单也同步进了结构化表,让 Agent 决定走 SQL 还是走向量检索,效果会好很多。这篇文章后面的架构里我会预留工具扩展位,实际上就是为这种异构数据源准备的。
2. 方案选型:为什么是 LangGraph,而不是自研状态机
2.1 LangGraph 解决的核心矛盾:状态可见 + 循环可控
一开始我考虑过两条路:一是自己用while循环 + 一堆if-else写状态机,二是直接用 LangChain 自带的create_retrieval_chain之类的高层封装。前者写起来自由,但一旦节点多了,状态没有统一管理,很快就变成意大利面条代码;后者太死,根本没法插入“反思”这个环节。
LangGraph 正好卡在中间。它把 Agent 的每一步定义成图节点,节点之间的跳转由条件边决定,而所有共享数据都放在一个全局状态对象里。这意味着:
- 每一步之间不是隐式传递字符串,而是结构化更新
state,调试时可以打印任意时刻的完整快照; - 图允许形成环,这是传统 LangChain 链式封装很难做到的,而“反思”恰恰需要回路:生成 -> 评估 -> 不通过 -> 重写查询 -> 再检索;
- 可以用
interrupt或节点回调在中间步骤插入人工审核,这在生产环境里非常关键。
2.2 状态、节点、条件边:LangGraph 的三要素
我用大白话拆一下 LangGraph 的基本概念。一个图有三个东西必须想清楚:
- State(状态):相当于所有节点共用的“工作台”。所有节点只能通过
state读取和写入信息。 - Node(节点):一段 Python 函数。输入是当前
state,输出是一个字典,里面是要更新的字段。 - Edge(边):定义节点之间的跳转。普通边是无条件跳转,条件边根据当前
state里某个字段的值动态决定下一步去哪个节点。
我的第一版代码里状态结构长这样:
from typing import Annotated, TypedDict from langgraph.graph.message import add_messages class RAGState(TypedDict): question: str # 原始问题 rewritten_question: str # 改写后的问题 documents: list # 检索到的文档 generation: str # 最终答案 reflections: list # 反思记录 iteration: int # 当前轮次,防止死循环 messages: Annotated[list, add_messages] # Agent 的消息历史注意messages字段用了add_messages这个 reducer,LangGraph 会用它的语义来做消息追加,而不是整体覆盖。如果不用 reducer,后写入节点的值会直接把前一个节点写的字段覆盖掉。这个细节很容易踩坑,后面调试部分我再展开。
2.3 我最终敲定的五节点工作流
我的知识库场景比较复杂,用户问题跨度很大,最终设计了一个五个核心节点 + 两个条件路由的图。流程可以这样描述:
- 入口判断(agent 节点):大模型基于当前问题决定下一步动作——需要检索,还是已有足够上下文可以回答。
- 检索(retrieve 节点):把当前查询向量化,从知识库召回 top_k 文档。
- 生成(generate 节点):基于检索到的文档生成带引用的答案。
- 反思(reflect 节点):把问题、答案、文档一起喂给模型,让模型判断答案是否“完整回答”了问题,输出
pass或fail,并给出缺失信息。 - 查询改写(rewrite 节点):如果反思失败,模型根据缺失信息改写查询,回到检索节点再来一轮。
条件路由的核心逻辑是:反思后判断pass则直接返回最终答案,判断fail则判断迭代次数是否超限,没超限就进入改写节点,超限就用当前已有答案兜底返回。这样一个回路,既给了 Agent 自我纠错的机会,又不会无限循环烧钱。
3. 代码实战:构建会反思的 Agentic RAG
3.1 环境准备与项目结构
我建议用uv或pip新建一个干净环境,Python 版本 3.10 以上。核心依赖如下:
pip install langgraph langchain langchain-openai langchain-community langchain-text-splitters faiss-cpu我用的是 FAISS 做向量库,因为本地跑、小规模知识库场景 FAISS 完全够用,不用额外起服务。如果你检索量在百万级以上,再考虑 ES、Milvus 或者 pgvector 不迟。嵌入模型我用的是本地化的 BGE 系列(BAAI/bge-small-zh-v1.5),好处是不走外部 API,响应稳定,适合国内网络环境。
项目结构大致是:
agentic_rag/ ├── main.py # 主图组装与调用入口 ├── nodes.py # 五个节点的具体实现 ├── state.py # 状态定义 ├── retriever.py # 向量库与检索器初始化 └── prompts.py # 各节点的 prompt 模板3.2 定义状态和检索器
先看状态定义,这一部分对应前面设计里的RAGState,不再重复。重点是检索器初始化。我封装了一个函数,把文档切分、向量化、构建检索器一步做完:
# retriever.py from langchain_community.vectorstores import FAISS from langchain_huggingface import HuggingFaceEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.documents import Document def build_retriever(docs, chunk_size=512, chunk_overlap=80, top_k=6): # 使用递归字符切分器,优先按段落切,再按句号兜底 splitter = RecursiveCharacterTextSplitter( chunk_size=chunk_size, chunk_overlap=chunk_overlap, separators=["\n\n", "\n", "。", "!", "?", ".", " "], keep_separator="end", ) chunks = splitter.split_documents(docs) embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", encode_kwargs={"normalize_embeddings": True}, ) vectorstore = FAISS.from_documents(chunks, embeddings) return vectorstore.as_retriever(search_kwargs={"k": top_k})这里有个值得说道的点:separators里我把中文句号、感叹号、问号都加进去了,而且设了keep_separator="end",意思是切分时把标点符号保留在上一块结尾。默认的RecursiveCharacterTextSplitter对中文支持不太行,经常把一个完整句子从中间切断,导致语义碎片化。这个问题在后续检索质量上影响很大。
3.3 五个节点逐一实现
接下来是最关键的部分。先看prompts.py里三个核心模板:
# prompts.py FROM_SYSTEM = """你是一个知识库问答助手,请基于【上下文】内容回答问题。 如果上下文中没有足够信息,明确说“知识库中没有相关资料”,不要编造。 回答结尾附上引用的来源编号,格式:[来源1][来源2]。""" REWRITE_SYSTEM = """你是一个查询优化专家。下面是用户问题、已有上下文片段以及当前答案缺失的信息。 请重写一个更精确的搜索查询,帮助在知识库中找回缺失信息。 只输出重写后的查询本身,不要任何解释。""" REFLECT_SYSTEM = """你是答案质量评估专家。请判断下面的回答是否完整回答了用户问题。 判断标准: 1. 是否直接回答了问题核心,而不是答偏或回避; 2. 是否基于给定上下文,没有编造; 3. 是否有关键信息缺失,导致用户无法据此行动。 如果回答满足要求,输出:PASS 如果不满足,输出:FAIL,并在下一行用一句话说明缺失什么。"""注意这些 prompt 里我特意加了“不要编造”“缺失就承认缺失”的约束。反思节点尤其重要,如果这个 prompt 写得含糊,模型很容易变成“全票通过”的复读机,整个反思回路就没有意义了。
然后实现节点。nodes.py里五个函数核心逻辑如下:
# nodes.py from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2) # 1. 入口决策 Agent def agent_node(state: RAGState) -> dict: if not state.get("documents"): return {"next_action": "retrieve"} return {"next_action": "generate"} # 2. 检索节点 def retrieve_node(state: RAGState) -> dict: query = state.get("rewritten_question") or state["question"] docs = retriever.invoke(query) return {"documents": docs, "iteration": state.get("iteration", 0) + 1} # 3. 生成节点 def generate_node(state: RAGState) -> dict: context = "\n\n".join( f"[来源{i+1}] {d.page_content}" for i, d in enumerate(state["documents"]) ) resp = llm.invoke( [ {"role": "system", "content": FROM_SYSTEM}, {"role": "user", "content": f"问题:{state['question']}\n\n上下文:{context}"}, ] ) return {"generation": resp.content} # 4. 反思节点 def reflect_node(state: RAGState) -> dict: pret = llm.invoke( [ {"role": "system", "content": REFLECT_SYSTEM}, {"role": "user", "content": f"用户问题:{state['question']}\n\n当前回答:{state['generation']}"}, ] ) result = pret.content.strip() if result.startswith("PASS"): return {"reflections": [result], "should_continue": "pass"} missing = result.split("\n")[1] if "\n" in result else "" return {"reflections": [result], "should_continue": "fail", "missing_info": missing} # 5. 查询改写节点 def rewrite_node(state: RAGState) -> dict: resp = llm.invoke( [ {"role": "system", "content": REWRITE_SYSTEM}, {"role": "user", "content": ( f"原始问题:{state['question']}\n" f"缺失信息:{state.get('missing_info', '')}\n" f"已有部分答案:{state['generation']}" )}, ] ) return {"rewritten_question": resp.content.strip()}这个实现里我做了一个重要决定:入口的agent_node目前逻辑很简化,只是判断“有没有文档”。真正复杂场景下,这里应该是一个完整的 ReAct 风格的 LLM 决策,可以决定“调用检索工具”还是“直接回答”。我为了演示反思回路先保持简单,但你完全可以把agent_node升级成带工具调用的 LLM 节点。
3.4 用 StateGraph 组装反思闭环
最后是主图组装。LangGraph 的核心 API 是StateGraph,我先创建图,注册节点,然后按条件边把它们连起来:
# main.py from langgraph.graph import StateGraph, END def route_to_next(state: RAGState) -> str: if state.get("should_continue") == "pass": return "end" if state.get("iteration", 0) >= 3: return "end" return "rewrite" builder = StateGraph(RAGState) builder.add_node("agent", agent_node) builder.add_node("retrieve", retrieve_node) builder.add_node("generate", generate_node) builder.add_node("reflect", reflect_node) builder.add_node("rewrite", rewrite_node) builder.set_entry_point("agent") builder.add_edge("agent", "retrieve") builder.add_edge("retrieve", "generate") builder.add_edge("generate", "reflect") builder.add_conditional_edges("reflect", route_to_next, { "rewrite": "rewrite", "end": END, }) builder.add_edge("rewrite", "retrieve") app = builder.compile() # 调用 if __name__ == "__main__": result = app.invoke({ "question": "登录报错的问题在 2.3.1 版本里修复了吗?", "iteration": 0, "documents": [], }) print(result["generation"]) print("反思记录:", result["reflections"])这里的关键点是条件边的映射表。route_to_next返回的是字符串,add_conditional_edges里的字典负责把这个字符串映射到目标节点名。我留着iteration >= 3的兜底,是为了防止反思失败时无限循环:“连续改写了三次还找不到资料,就老老实实用最后一次生成的答案返回,同时把反思记录抛给调用方,让上层知道这次回答可能不可靠。”
这种“答不上来也明说”的设计,在我看来是生产级 RAG 和玩具级 RAG 的分水岭。用户宁可看到一句“知识库中暂无相关资料”,也不愿意看到模型编一个看似专业的假答案。
4. 调试与踩坑实录:这些坑我替你踩过了
4.1 调参经验:chunk_size 和 top_k 怎么定
讲真,RAG 项目里决定检索质量的第一因素不是模型,而是切块参数。我一开始用默认的chunk_size=1000,结果复合问题经常只被分在同一个大块里,导致检索召回的内容要么互相重复、要么漏掉关键分支。后来我做了个小实验,把 chunk_size 分别设成 256、512、768,对 50 个测试问题做了对比:
| chunk_size | 平均检索命中率 | 生成答案完整率 | 备注 |
|---|---|---|---|
| 256 | 78% | 65% | 上下文碎片化,需要 top_k 调大 |
| 512 | 86% | 82% | 最均衡,我最终选了这个 |
| 768 | 83% | 77% | 块太大,细粒度信息容易丢 |
规律是:中文文档在 512 左右是个甜点。因为 bge-small-zh 这类中文嵌入模型对 256 到 512 token 的文本段建模效果最好,太长会稀释语义,太短又会丢掉上下文。top_k不要盲目调大,我的知识库文档质量参差,top_k 从 6 改到 10 之后,检索到的段落相关性明显下降,反而拉低了生成质量。最终定在top_k=6。
我还发现一个细节:chunk_overlap=80对中文特别重要。中文没有天然空格分词,切块时经常把一个概念切两半,overlap 能让相邻块共享一些边界文本,避免关键术语被截断。
4.2 “反思”容易写成假反思,prompt 得硬核
第一次跑通反思回路时,我挺失望的:输出结果里reflections几乎都是PASS,哪怕答案明显有遗漏。查了几轮才发现问题出在两个地方:
一是反思 prompt 缺少具体标准。“是否完整回答”这种表述太模糊,模型倾向给好评。我后来在 prompt 里加了三段硬性判断标准(直接回答、基于上下文、关键信息缺失),并且要求输出必须是PASS或FAIL+ 一句话说明,逼模型表态。
二是模型自评默认不愿“自我否定”。后来我在反思节点里把“当前答案”和“检索到的所有文档”都喂给了评估模型,让评估模型不是凭空挑错,而是拿着原文对照检查。这个改动让 fail 率从不到 5% 提升到了 30% 左右,反思回路终于开始发挥真实作用。
4.3 token 成本与上下文长度控制
反思回路的效果上来了,账单问题也跟着来了。一次不通过的查询,实际要跑:agent 决策 + 检索 + 生成 + 反思 + 改写 + 再检索 + 再生成 + 再反思,最多可以到三轮迭代,token 消耗是传统 RAG 的 5 到 8 倍。
我做了三个优化:
- 把反思和生成拆成两个模型调用,反思用更小的
gpt-4o-mini,生成用gpt-4o。这样反思虽然多了一轮调用,但 token 单价低。 - 改写节点只传缺失信息摘要,不把全部历史消息塞进去。上面代码里我只传了“原始问题 + 缺失信息 + 已有答案摘要”,这就是刻意控制的。
- 限制最大迭代次数为 3,从概率上讲,三轮检索还补不上信息,再多也是如此。
如果你做的是垂直领域的专业知识库,可以考虑在反思节点前加一层“置信度判断”,当模型对答案本身置信度很高时,连反思都跳过。这个 trick 能省下大约 30% 的 token 成本。
4.4 本地搭建与并发瓶颈
热词里有人问“怎么在 Mac 上搭建 RAG 知识库”,我本地的开发环境就是一台 MacBook。LangGraph + FAISS + BGE 嵌入模型这套组合,在 Mac 上跑得很顺,不需要 GPU。嵌入模型用的是 CPU 推理,2000 篇文档切块后入库大概 3 分钟,完全可接受。但要注意:FAISS 默认不支持增量添加,我每次更新文档库都是重建索引。如果是频繁更新的场景,建议用FAISS.add_documents()配合持久化,或者直接换 pgvector。
并发这块我也踩了坑。LangGraph 的图状态对象不是线程安全的,多个请求同时invoke同一个编译后的图时,state会被互相污染——我在测试接口时发现一个用户的查询偶尔会带上另一个用户的检索结果。解决方案是每个请求创建独立的图实例,或者用app.ainvoke在异步事件循环里隔离。异步版本适合 FastAPI 集成:
# 在 FastAPI 路由中 @app.post("/ask") async def ask(question: str): async with async_app.astream({"question": question, "iteration": 0, "documents": []}): last_event = None async for event in async_app.astream(...): last_event = event return last_event更稳妥的方式是维护一个“图实例池”,每个请求从池里取一个干净的图对象,用完归还。这样可以规避 LangGraph 内部状态被并发请求覆盖的问题。
4.5 常见问题速查表
| 症状 | 可能原因 | 解决方向 |
|---|---|---|
| 检索结果和问题完全不相关 | 嵌入模型选型差,或文档切块太粗暴 | 换中文专用嵌入模型,调 chunk_size 和 overlap |
| 反思节点全部 PASS,不触发重查 | 反思 prompt 太模糊 | 加三条硬性判断标准,把上下文文档也喂给评估模型 |
| 一模一样的问题回答不稳定 | LLM 温度太高 | 生成节点温度降到 0.2 以下,反思节点用 0 |
| 多次改写后还是答不上来 | 知识库里真的没有相关信息 | 迭代上限兜底,明确告知用户缺失资料 |
| 接口并发后答案串了 | 共享图实例的 state 被污染 | 每个请求独立图实例,或用 async 图 |
| 本地内存占用持续上涨 | FAISS 索引常驻 + 大文档列表 | 流式处理 chunks,用完释放,避免把全部文档塞进 state |
我最后悔的一件事是前期没做“检索—生成”两阶段联调。单独看检索质量、单独看生成质量都没问题,一旦接上反思回路,很多问题才暴露。建议你从一开始就给测试集打好标签,每条用例标注“期望检索到的来源”“期望答案要点”,这样调参时有个客观标尺。
5. 从“会反思”到“能干活”:扩展思路与适用边界
5.1 知识库到底能不能存图片、怎么处理多模态
热词里有人问“RAG 知识库能存储图片嘛”,这里统一回答:普通的文本向量检索链路存不了图片,但可以存“图片的描述”。我试过的常见做法是把图片先用多模态模型(比如 GPT-4o、Qwen-VL)生成一段详细描述,再把描述文本切块进入向量库。用户问“文档里那张架构图讲了什么”,检索到的是图片描述文本,生成答案时仍然可以把图片以 Markdown 形式附在上下文里,前端展示给用户。
如果图片本身包含大量图表信息,只靠描述会损失细节。更重量的方案是引入多模态向量模型,比如 CLIP、SigLIP 做图文联合嵌入,然后把图片和文本统一进同一个向量空间。但工程复杂度和资源占用会直线上升。我的建议是:90% 的文档型知识库场景,图片转描述就够用,没必要一开始就上多模态检索。
5.2 Agentic RAG 和纯 Agent 的边界在哪
热搜词里有“agent框架与编排”“harness 和 agent 区别”,我借这个地方把边界说透。Agentic RAG 本质上是“带检索工具的 Agent”,它和通用 Agent 的区别在于:RAG Agent 的目标是回答知识库问题,动作空间收敛在“检索、改写、生成、反思”这几个节点上;而通用 Agent 可以自主调用任意工具、操作外部系统、执行多步任务。
实践里我更喜欢一种分层方式:底层是 LangGraph 这个编排“骨架”,上层定义节点的行为策略。RAG Agent 只是骨架上的一个专用 harness——把检索、生成、反思这些工具按固定拓扑串起来,同时保留 Agent 的自主决策能力。至于“Multi-Agent 架构”“Agent 记忆”这些话题,属于同一套图编排方法里的其他玩法,等你的 RAG Agent 稳定跑通了再扩展不迟。
5.3 什么时候你根本不需要反思回路
说实话,不是每个 RAG 项目都值得上 Agentic 这套方案。我在下面这些场景里会把反思回路关掉:
- FAQ 问答:问题很固定,直接做“问题相似度匹配 + 标准答案返回”就行,让 LLM 生成反而是浪费;
- 低延迟场景:比如在线客服首响必须低于 2 秒,一次检索 + 一次生成的链路已经极限了;
- 知识库很干净,且问题大多是单跳查询:反思和改写带来的提升不大,成本却翻倍。
一个比较务实的做法是在图里加一个“快速通道”路由,让入口 agent 节点判断:如果问题明显是简单事实类,直接走retrieve -> generate -> END;只有遇到多跳、对比、追问类问题时,才进入反思回路。这样既保住了复杂问题的质量,又不会让简单问题变慢变贵。
个人体验:调试反思回路时最让我惊喜的一个瞬间
项目收尾时,我拿了一个“刁钻”问题测试:用户问“2.3.1 版本发布说明里有没有提到登录认证超时的修复,跟 2.3.0 的改动有什么区别”。第一轮检索,答案只提到了“认证超时修复”,但没说和 2.3.0 的对比,反思节点给出 FAIL,理由是“缺少版本间差异信息”。查询改写节点把问题改写成“2.3.1 vs 2.3.0 登录认证超时改动对比”,第二轮检索真就把两个版本的发布说明片段都拉了出来,生成了一版带对比和引用的回答。看到这个结果的时候我还是挺感慨的——它不再是一个机械的检索管道,而是真的像一个项目助理,发现自己漏了关键信息,会主动补充后再提交答案。
当然,这套系统离“完美”还差得远,比如:反思节点偶尔会误判、改写节点可能把问题改得面目全非、多模态扩展还没做深。但如果你也在做知识库问答,我建议不要只停在“能检索、能生成”这一步,给系统加上反思闭环,你会看到完全不一样的回答质量。最后提醒一句:先把测试集建好,再做 Agentic 化改造,否则你会被各种“薛定谔的检索质量”搞崩溃。