Agentic RAG实战:从传统检索到带决策的智能问答系统
2026/9/1 11:35:00 网站建设 项目流程

简介:面向AI应用开发者与RAG技术研究者的Agentic RAG可运行源码包,围绕传统RAG单一知识源与一次性检索的短板,演示如何将AI代理接入检索增强生成管道,实现多源动态选择与多轮自主检索。包内共3个文件,以index.html可视化说明页面、inscode在线运行环境配置、gitignore工程辅助文件为主,压缩包仅8KB,轻量精炼,适合直接下载后对照学习。内容覆盖Agentic RAG的核心概念、单代理与多代理架构对比、函数调用语言模型与DSPy/LangChain等代理框架的实施路径,并给出可直接运行的示例源码,既能作为理解原理的配套材料,也可作为启动二次开发的脚手架。目前已有139人学习,适合正在探索企业级智能信息检索方案的技术人员快速上手,同时资源也客观点出了技术门槛与维护成本等落地挑战。 说实话,我第一次看到“Agentic RAG”这个词时,第一反应是“又是一个新包装概念?”但当我真的把一套可运行源码搭起来、跑通一个多轮检索的问答场景后,我转变了看法——这玩意确实不是包装,它把RAG从“检索一段就回答”的单程流程,升级成了“边查边想、缺了再查”的决策循环。

这篇文章不打算做名词科普,而是以我手头这个可运行的极简项目为主线,带你拆一遍Agentic RAG的完整实现:状态机怎么设计、检索器如何和LLM协同、几个关键决策点怎么落地,以及我在实测中踩过的坑。适合两类人:一类是已经搭过基本RAG、想让系统能处理复杂问题的开发者;另一类是刚接触Agent/RAG、想通过可运行代码理解概念的同学。

1. Agentic RAG到底是什么:从“检索抄写员”到“带决策的助理”

1.1 传统RAG的三大硬伤

RAG现在用的是文档切片-向量化-检索-拼接-生成的套路。这套东西对简单问答够用,但一旦问题变复杂,三个毛病就会被放大。

第一,出错是“一次性”的。文档切片如果切得不好,检索到的内容质量差,生成阶段再怎么调prompt也没用。传统RAG没有“发现自己检索错了”的机制,第一次检索命中垃圾片段,答案基本就废了。

第二,只能处理单跳问题。比如“这个项目的负责人是谁”,一次检索能搞定。但“项目A的负责人现在负责哪个项目”,需要先查到负责人,再查另一个项目的关联信息,传统RAG做不了这种链式查询。

第三,对上下文拼接不敏感。多篇文档拼到一块后,如果存在矛盾或重叠信息,模型只能硬着头皮答,既无法判断证据是否冲突,也无法主动找更多材料佐证。

我习惯用一个类比:传统RAG像一个检索抄写员,你问它什么,它飞快去文档库复印几页纸,拿回来念给你。复印到关键页就答得好,复印到无关页就胡说。而Agentic RAG更像是你雇了一个会查资料的助理,它拿到问题后,会先判断“这需要去档案库吗”,然后拆成几个小任务去翻,翻完如果发现还缺信息,会再回头继续查,直到它觉得自己手里的材料足够支撑一个答案。

1.2 Agentic RAG的核心:把“检索”变成可调用的工具

放到技术层面,这个“助理”其实来自ReAct模式的延伸:Thought(思考)-> Action(行动)-> Observation(观察结果),循环反复。Agentic RAG把“检索”当作一个可以被反复调用的工具,而不是流程里的一次性步骤。

每次检索后,系统都会问自己两个问题:现有上下文能回答用户吗?如果不能,缺口是什么?然后针对缺口重新生成新的检索请求,再走一轮。这看起来只是多了一个循环,实际改变了RAG的根本性质——从“流水线”变成“闭环”。闭环的价值在于系统有了纠错和自我规划的能力,这是传统RAG加再多样本命中率也换不来的。

所以,判断一个系统是不是真Agentic,看它有没有“决策环”就够了。这个标准直接影响后面的架构选择和代码设计。接下来我会以这个极简项目的完整实现为主线,把决策环的每个环节拆开来看。

2. 系统架构与关键设计:动手前先想清楚这四件事

2.1 识别真Agentic:有没有决策环

现在市面上的RAG项目越来越多喜欢挂“Agentic”的牌子,但不少只是套了个多查询检索,本质还是单向流程。我判断一个系统是不是真Agentic,只问三个问题:

系统能不能在缺少信息时自己决定再查一次? 检索策略是不是固定的? 回答前有没有对证据充分性的独立评估?

如果答案都是肯定的,才是Agentic RAG。如果只有“多查询检索”而没有任何评估、反馈、再规划的环节,那它充其量叫“增强版RAG”。这个判断标准在选型时候特别重要——很多人没想清楚就上LangGraph,结果只是把原来的流水线画成了图,并没有引入真正的决策。

2.2 技术选型:LangChain、LangGraph还是手写循环

我在这个项目里做了一个偏“保守”的选择:文档切块和向量化用了现成方案,但Agent决策循环没有上LangGraph,而是手写了一个极简状态机。选型对比如下:

方案优点缺点
LangGraph节点化架构清晰、有图可视化、适合复杂多智能体抽象层多,调试要绕好几层,版本升级还容易破坏API
LangChain Agent框架配置方便,内置工具调用决策逻辑被框架包住,遇到问题不好定位
手写循环所有逻辑摊在眼前,易于调试和理解,依赖少复杂场景需要自己管理状态

最终选择手写,理由是:我要做的核心动作只有三个——规划、检索、评估。三个动作写成函数就能跑,不需要引入一整套图执行引擎。而且对读者来说,看50行手写代码比看300行框架代码更容易理解Agentic RAG的本质。如果你要做的场景是复杂多智能体协作、多个工具交叉调用,再考虑LangGraph这类方案也不迟。

另外要注意的是embedding和LLM的选择。我用了OpenAI的text-embedding-3-small和gpt-4o-mini,这个组合在性价比上相当能打。如果你没有OpenAI的key,把这两处替换成你本地部署的Embedding和对话模型,逻辑完全一样。

3. 核心流程拆解:一轮回答背后的三次决策

3.1 第一次决策:这个问题需要检索吗

很多RAG一上来就无脑检索,用户问“你好”也去扫描一遍文档库,浪费token还影响回答质量。Agentic RAG的第一步,是让LLM判断当前问题是否需要检索。不需要的情况包括闲聊、对已有上下文的追问等。

这个判断我用一个很轻的prompt实现:要求模型输出JSON,包含need_search字段和sub_questions列表。如果判断不需要检索,就直接进入回答阶段。这里有个小技巧:不检索时不要简单返回空结果,而是让模型生成一段自然答复,比如用户问“你好”,系统可以正常打招呼,而不是答非所问地搬出文档内容,很影响体验。

3.2 第二次决策:检索结果够不够回答

这是和传统RAG差异最大的一步。传统RAG拿到top-k就直接生成答案,而Agentic RAG会先把检索到的片段交给另一个评估环节,让它判断“这些材料到底够不够回答用户”。这里的评估prompt需要明确要求模型列出“还缺什么”,不要只给一个yes/no,否则后一轮没法定位问题。

实测下来,这个评估环节能挡住不少瞎答。比如用户问“第三季度营收下降的原因”,第一次检索到的片段可能只有下降数据,没有原因分析,模型的评估结果就会是“数据有了,但缺少对下降原因的解释”。这个缺口的描述,会直接作为下一轮检索的输入。

3.3 第三次决策:怎么补检和生成

拿到缺口后,不是简单拿原问题再搜一遍,而是把缺口描述转换成一个新的检索问题。这一步相当于在原有问题上“追加了一个追问视角”。比如原始问题是“营收下降原因”,缺口是“缺少华东区促销活动效果数据”,那新检索问题就会是“华东区促销活动效果如何”之类的表述。

循环会持续到两种情况为止:评估认为材料充分,或达到最大轮数限制。最后一次生成时,我会把全部收集到的证据按轮次和来源编号整理好,让回答要求带上引用标记。这一步有附带好处:用户可以追溯答案来自哪一段原文,回答的置信度和可信度都会明显提升。

4. 可运行源码实现:极简版本也能跑出效果

4.1 环境准备

这个项目只需要两个核心依赖:openai(调用LLM和Embedding,版本>=1.0)和numpy(做余弦相似度计算)。Python 3.10+就行。安装命令:

pip install openai numpy

然后设置环境变量OPENAI_API_KEY。我实际测试时用的Python 3.11,openai版本是1.30左右,numpy用的2.0,兼容性没有遇到问题。如果你要把检索换成自己的文档库,建议把Embedding部分接FAISS或Chroma,我这里为了演示简洁,直接用向量点积做相似度,几段演示文档规模下完全够用。

4.2 核心代码:检索器与决策循环

先建索引和检索函数:

import json import numpy as np from openai import OpenAI client = OpenAI() # 记得配置 OPENAI_API_KEY def build_index(docs, chunk_size=500): chunks = [] for doc in docs: chunks.extend([doc[i:i+chunk_size] for i in range(0, len(doc), chunk_size)]) emb = client.embeddings.create(model="text-embedding-3-small", input=chunks) embs = np.array([e.embedding for e in emb.data]) return chunks, embs def retrieve(query, chunks, embs, top_k=3): emb = client.embeddings.create(model="text-embedding-3-small", input=[query]) qv = np.array(emb.data[0].embedding) scores = (embs @ qv) / (np.linalg.norm(embs, axis=1) * np.linalg.norm(qv) + 1e-9) ids = np.argsort(scores)[::-1][:top_k] return [(chunks[i], float(scores[i])) for i in ids] def call_llm(system, user): resp = client.chat.completions.create( model="gpt-4o-mini", temperature=0, messages=[{"role": "system", "content": system}, {"role": "user", "content": user}], ) return resp.choices[0].message.content def format_evidence(evidence): return "\n".join(f"[来源{i}] {e['chunk']}" for i, e in enumerate(evidence))

然后是核心的决策循环:

def agentic_rag(question, chunks, embs, max_steps=3): evidence = [] query = question history = [] for step in range(max_steps): plan = json.loads(call_llm( "你是检索规划器。判断当前问题是否需要检索,若需要,拆成最多3个子问题。" "只输出JSON:{\"need_search\": true/false, \"sub_questions\": []}", f"当前问题:{query}\n已收集证据片段:{format_evidence(evidence)}" )) if not plan["need_search"]: break for sq in plan["sub_questions"]: for chunk, score in retrieve(sq, chunks, embs): evidence.append({"chunk": chunk, "score": score, "query": sq}) verdict = json.loads(call_llm( "你是质检员。判断已收集的片段是否足以回答原始问题。" "只输出JSON:{\"enough\": true/false, \"reason\": \"还缺少什么,具体一点\"}", f"原始问题:{question}\n已收集片段:{format_evidence(evidence)}" )) if verdict["enough"]: break query = call_llm( "把质检员的缺口描述转换成一个新的检索问题,不要任何解释,只输出问题。", verdict["reason"] ) history.append(query) answer = call_llm( "你是答疑助手。只根据提供的片段回答,如果片段不足以作答就明说。" "引用格式:[来源N]。", f"原始问题:{question}\n片段:{format_evidence(evidence)}" ) return answer

这段代码就是整个系统的核心。三个决策点分别对应上面的规划、评估、补查。主要注意一点:JSON解析。模型偶尔会输出多余内容,我建议在解析前做一次字符串清洗,比如用正则截取第一个{到最后一个}之间的内容,否则会直接抛异常,整个流程就断了。

4.3 跑一个真实例子:多轮检索是怎么发生的

我用一组销售月度分析文档测了这个流程。文档里有华东区、华北区的营收数据和促销记录。用户的问题是:“华东区上季度营收下滑,原因是什么?和促销活动有没有关系?”

第一次循环,规划环节先拆出两个子问题:“华东区上季度营收数据是多少?”“华东区上季度的促销活动有哪些?”检索后进入评估,模型认为“已经拿到下滑数据和促销活动列表,但缺少促销活动效果和下滑的关联性证据”。于是第二轮生成的新检索问题变成了“华东区促销活动对营收的影响”,再次检索后,证据链就完整了,系统最终给出的回答不仅涵盖了下滑原因,还标注了来源。

这个例子看起来简单,但如果你用传统RAG跑,第一步很可能只检索到营收数据,直接答成“下滑了X%”,根本不会往促销活动方向延伸。差别不在检索器,而是系统有没有“自我审视”的能力。

5. 实测记录:跑通后我发现的问题与被忽略的坑

5.1 问题一:上下文污染导致检索漂移

刚开始我把前一轮的“缺口描述”和“已收集证据”全部拼进新的检索问题里,发现效果反而变差。比如原始问题是华东区销售,前一轮证据里有华北区的数据,模型在生成新问题时会被这些干扰带偏,检索出来的东西开始偏离主题。

后来我把规则改成:新检索问题只基于缺口描述生成,不掺入完整历史往来记录。结果稳定很多。这也解释了为什么很多Agent项目越跑越偏——不是模型不行,是上下文的信噪比在每一轮都在降低,检索漂移就是在大量劣质历史上下文中累积出来的。

5.2 问题二:循环失控与Token浪费

在评估prompt写得不严格的时候,模型很爱说“不够、还需要更多”,然后系统就不停地检索。我在一个长文档场景里遇到过连续6轮检索,实际上第3轮就已经覆盖了答案。这个问题的解法有两个:一是max_steps设硬上限,我默认3轮;二是在评估prompt里加强制指令,只有当事实确实无法组织答案时才允许返回enough=false。

另外,每轮检索的top_k不要设太大。3-4个片段足够,太大只会增加token成本,而且重要信息会被淹没在无关片段里。这个值不能照搬,要根据你的文档粒度来调:文档切片越碎,top_k可以稍微大一点,否则信息覆盖不够。

5.3 问题三:结果不可复现

Agentic RAG带LLM就有随机性,更别说中间还要多次解析JSON。为了结果可复现,我把所有LLM调用的temperature设成0,同时对embedding结果做了缓存。实测同样的文档和问题,跑两遍的输出基本一致。

还有一个小细节:把依赖版本固定住。openai这个库版本升级特别快,一次升级后我遇到过返回对象结构变化,代码直接崩了。建议在项目里加一个requirements.txt锁定版本,比如openai==1.30.0、numpy==2.0.0,这样别人clone下来也能直接跑出和你一样的结果。

6. 个人经验:让Agentic RAG稳定的几条土办法

最后聊几句实操体会。这套系统跑通之后,我又拿它试了内部知识库的问答,整体稳定性比传统RAG高了一个档次,但也别指望它万能。如果问题本身在文档里根本不存在答案,再多的检索轮次也救不回来,所以我的第一个经验是:在系统入口加一层范围校验,告诉用户它在什么范围内回答问题,省得用户问超纲问题,系统在那里空转。

第二个经验是:调试Agentic RAG时,把每一轮的规划和评估输出打出来看。我会在代码里加一个debug参数,把plan、verdict、query都print出来。很多你以为的“模型理解问题”,实际看一眼日志才发现是prompt写得有歧义。日志在Agent系统里不只是排错工具,更是观察系统行为的窗口,这个习惯帮我排掉了至少一半的诡异问题。

第三个经验:别一上来就上重框架。先用这种手写的极简循环把逻辑跑通,理解每个决策点,再根据业务复杂度决定要不要上LangGraph或更完整的Agent框架。我见过太多人一上来就引入一套复杂的图编排,最后卡在框架版本适配问题上,反而离核心业务越来越远。

这套代码改一改就能落在自己的文档场景里。如果你已经在跑传统RAG,不妨把评估和补查这两个环节先加上,你会明显感觉到同样的文档库,回答的质量和可追溯性都会上一个台阶,这也是我觉得Agentic RAG最值得投入的地方。

本文还有配套的精品资源,点击获取

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

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

立即咨询