从静态RAG到Agentic RAG:构建具备推理与决策能力的智能知识库问答系统 做企业知识库问答系统做了三年我最初的方案就是最朴素的静态RAG把文档切片、做向量化、存进数据库用户提问时召回top-k片段丢给大模型生成答案。这套流程在“某个文档里写了什么”这类单点查询上确实能用但一旦问题变成“今年Q3手机壳SKU退货率为什么环比上升了18%给出一份改进建议”静态RAG就非常吃力。它不知道先查退货明细表再查质检报告也不知道要分几步拆解问题更不会在第一次检索结果不够时重新换个角度再查一次。这正是Agentic RAG要解决的问题——让RAG从“查一段拼一段”的静态流水线进化为具备理解、推理、决策能力的智能检索体。我参考的是当前社区里比较主流的实现思路FastAPI做服务框架LangGraph编排Agent状态流底层用LangChain做检索链向量存储用pgvector。整套系统跑下来最明显的感受是Agentic RAG不是给RAG加一个Agent壳子而是把检索从一个孤立的步骤变成可以被反复评估、改写、重试、决策的核心能力。技术选型和工程细节我今天一次性讲透尽量让想落地的人少走弯路。1. 静态RAG的瓶颈为什么单纯“检索生成”解决不了复杂问题静态RAG的经典链路是查询向量化 → 向量检索 → TopK上下文注入 → LLM生成回答。这个链路天然假设了一个前提用户的第一个问题就是最正确的检索表达式且答案就在某一片文档里。现实中这两个假设几乎都不成立。1.1 多跳问题如何让向量检索失效我遇到过的最典型场景用户问“服务器频繁告警的时候我们该参考哪个应急预案预案里的回滚操作是否需要二次审批”。这个问题如果用原始query直接查向量库第一步“服务器频繁告警”能召回到和告警相关的文档但“回滚操作是否需要二次审批”这个子问题的答案往往在变更管理制度文档里和告警预案可能相距十万八千里。静态RAG的top-k窗口根本覆盖不到。这本质上是一个多跳推理问题需要先确定告警应急预案再从预案内容中提取“回滚”关键字重新构造一个检索式去查管理制度文档。静态RAG没有“重新检索”这个动作一次召回、一次生成问题就结束了。但Agentic RAG里Agent会在第一轮生成后检查自己掌握的上下文是否足够回答完整问题如果发现缺失就会调用一次新的检索动作。1.2 查询意图识别用户说“它”的时候Agent得知道“它”是谁另一个静态RAG的硬伤是缺乏对话记忆管理。用户第一句问“Redis内存满了怎么办”第二句追问“如果用了持久化还不行呢”。第二句里的“还不行”指代的是第一轮所有操作方案都不生效的情况。静态RAG把每一轮问题独立处理第二句就变成了一句语义不完整的查询检索质量自然崩塌。Agentic RAG通过状态机制解决指代消解。我在LangGraph里维护一个conversation_state每一轮新问题进入之前会先把历史对话的关键实体、操作步骤、未解决问题塞进一个“对话摘要节点”让Agent判断当前问题是全新查询还是上一轮的延续。如果Agent判定是追问它会改写查询表达式把指代对象补齐再进入检索节点。这个“先理解、再设问”的过程是Agentic RAG区别于静态RAG的核心分水岭。1.3 决策能力缺失静态RAG不知道“什么时候不用查”很多开发者忽略了一个点不是所有问题都需要找回文档。用户问“帮我把昨天的日报摘要写一下”这根本不需要检索用户问“对比表格里两种方案的优缺点”如果向量库里没有现成的对比表检索只会召回来一堆不相关的碎片反而污染上下文。静态RAG无法做这类“是否需要工具”的判断它只会机械地走一遍检索链路。Agentic RAG则可以把“检索”和“生成”解耦成一个可路由的决策点Agent先判断该问题是否需要外部知识。如果需要是查向量库还是调SQL工具还是请求Web API如果不需要直接交给大模型生成。这个路由决策背后是一个二分类判断任务我实测下来用带few-shot示例的LLM prompt做router比训练一个独立分类器要灵活得多。2. Agentic RAG的核心逻辑把检索改造成一条可迭代的决策链路2.1 理解从Query改写开始Agentic RAG的第一步不是检索而是理解。我这里的理解不是指大模型“读懂”了用户的话而是指系统能从检索效率维度去重构用户的问题。原始query往往带着口语化的冗余表达、模糊指代、隐含条件直接在向量空间里匹配效果很差。查询改写Query Rewriting要做三件事补全指代、拆分复合意图、补充同义扩展。举个例子原始query“那个内存泄漏的问题后来修复后的监控数据怎么样”改写后变成“Redis内存泄漏修复后的监控数据内存使用率、GC耗时、慢查询数量时间范围修复后的两周”。这个改写不是文字润色而是把用户心照不宣的上下文显式化了。我用的改写prompt里有几个强制输出字段entities、time_range、metrics_list、implicit_context结构化之后既可以作为检索条件也可以帮助后续的评估节点判断召回结果是否真正匹配了意图。2.2 推理Agent如何规划检索路径当Agent判定需要多步检索时它会做检索规划。我在LangGraph里用一个planner节点输出一个检索计划计划格式是一组有序的检索目标。比如“分析A和B方案的成本差异”planner可能会生成三个检索目标获取方案A的预算明细、获取方案B的预算明细、获取历史项目中对这两个方案的实际使用记录。这个规划过程用的是LLM的zero-shot推理能力但要做约束不然LLM会给出很多虚构的检索目标。我在planner设计上做了两点限制检索目标必须是向量库中真实存在的语义集合每一个目标都对应一个过滤条件filter。比如“预算明细”对应doc_typebudget_detail没有filter的检索目标会被Agent判定为无效目标。检索目标之间如果有依赖关系必须显式声明。比如“先查事故报告中的故障码再根据故障码查对应维修手册”第二步依赖第一步的输出planner必须把这种依赖关系编码进状态图。2.3 决策评估-再检索的闭环评估闭环是Agentic RAG最关键的机制。在完成首轮检索和生成之后系统会进入一个evaluator节点判断“目前生成的答案是否足以回答用户问题”。这个判断不是凭空评判答案好坏而是做三件事答案是否引用了实际检索到的片段引用片段与查询意图是否直接相关如果答案是“部分回答”缺失的部分是否指向了未覆盖的上下文我在实现里没有依赖一个全能的LLM来一次性做判断而是拆成了两个子任务。第一个子任务是上下文充分性评估就是把检索片段整体交给一个评分模型判断这些片段是否覆盖了查询中的所有必要实体第二个子任务是答案生成后的忠实度评估判断最终答案是否严格基于检索片段有没有幻觉。如果第一个子任务判定片段不充分Agent会重新走一遍改写和检索路径但是检索词会带上上一轮缺失的实体关键词。这就是“Agentic”的精髓——检索不是一个一次性操作而是一个可以根据反馈循环执行的决策动作。3. 基于LangGraphpgvector的落地实现细节架构确定之后工程实现上我遇到的最大挑战是怎么把“决策”“规划”“评估”这些听起来很玄的概念映射到可靠的代码上。LangGraph在这一块帮了大忙它本质上是一个有状态图执行引擎正好适合Agent的多跳、循环、条件分支逻辑。3.1 状态图设计从线性到循环的核心转变我用LangGraph定义的状态是一个TypedDict包含了query、rewritten_query、retrieved_contexts、plan、evaluation_result、answer等字段。图上最基本的路径是understand_node → retrieve_node → evaluate_node → generate_node。但你如果想要Agentic能力必须允许evaluate_node返回need_retry让图重新走到retrieve_node而不是直接走到generate_node。看一个我来简化过的核心代码这是LangGraph里条件路由的写法from langgraph.graph import StateGraph, END from typing import TypedDict, List, Literal class AgentState(TypedDict): query: str rewritten_query: str retrieved_contexts: List[str] has_retrieved: bool evaluation_result: Literal[sufficient, insufficient, need_tool] answer: str def route_after_evaluate(state: AgentState) - str: if state[evaluation_result] insufficient: return retrieve elif state[evaluation_result] need_tool: return tools else: return generate graph StateGraph(AgentState) graph.add_node(understand, understand_node) graph.add_node(retrieve, retrieve_node) graph.add_node(evaluate, evaluate_node) graph.add_node(tools, tools_node) graph.add_node(generate, generate_node) graph.add_edge(understand, retrieve) graph.add_edge(retrieve, evaluate) graph.add_conditional_edges( evaluate, route_after_evaluate, {retrieve: retrieve, tools: tools, generate: generate} ) graph.add_edge(tools, generate) graph.add_edge(generate, END) graph.set_entry_point(understand) app graph.compile()这段代码表面上看是画了一个图实际上定义了Agent的决策边界如果评估节点认为上下文不足Agent会回到retrieve重新检索但不会无限循环因为我在state里加了一个iteration_count字段超过3轮就强制进入generate避免检索死循环。def route_after_evaluate(state: AgentState) - str: if state.get(iteration_count, 0) 3: return generate if state[evaluation_result] insufficient: return retrieve ...3.2 检索节点pgvector的召回与重排策略向量存储我最后选了pgvector而不是独立的向量数据库原因是这个项目本身就有PostgreSQL承载业务数据往外再接一套Milvus会增加运维复杂度。pgvector已经够用在百万级向量规模的场景下。建表和索引的核心是选择合适的距离函数和索引类型。我用的距离函数是余弦距离因为大家在落地RAG时Embedding模型的向量通常都已经做过归一化余弦距离和欧氏距离在归一化后是单调等价的但余弦距离在语义相似度场景下更直观。HNSW索引比IVFFlat在召回质量上更稳定尤其是数据频繁增删的时候。CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE doc_chunks ( id BIGSERIAL PRIMARY KEY, content TEXT NOT NULL, metadata JSONB NOT NULL DEFAULT {}, embedding vector(1536) ); CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops);注意索引参数要根据数据规模合理安排ef_search控制查询精度我一般设成100ef_construction控制建索引质量批量导入时设成100增量导入时如果感觉召回率低了会重建索引。检索时我很少只做一次裸检索而是采用了“向量召回关键词补偿”的混合策略。因为纯向量检索对专有名词、缩写、型号这类token匹配非常不敏感。PostgreSQL的全文检索和pgvector可以在同一条SQL里完成先通过tsvector倒排索引把候选集缩小到几百条再用向量距离做精排。WITH keyword_candidates AS ( SELECT id, content, metadata FROM doc_chunks WHERE to_tsvector(simple, content) plainto_tsquery(simple, 内存泄漏) ORDER BY ts_rank(to_tsvector(simple, content), plainto_tsquery(simple, 内存泄漏)) DESC LIMIT 200 ) SELECT id, content, metadata, 1 - (embedding $1) AS similarity FROM keyword_candidates ORDER BY embedding $1 LIMIT 10;这里有个细节关键词候选集不要直接取top10给LLM因为关键词命中的片段中很多是同主题不同内容的干扰项必须再用向量相似度精排一轮。3.3 FastAPI接口层既要支持流式还要支持任务编排FastAPI是服务层比较稳妥的选择主要因为原生async支持做流式响应很舒服而且pydantic v2做请求校验比手写一堆if判断要整洁得多。我的接口层设计成两个端点一个处理一次性请求一个处理流式响应。流式响应在Agentic RAG里作用很大。因为Agent会经过多轮检索和评估用户如果一直等最终answer时长可能从2秒膨胀到6秒甚至8秒体验非常差。流式的思路是把Agent的执行过程拆成事件流每完成一个节点就推送一个事件。from fastapi import FastAPI from fastapi.responses import StreamingResponse from pydantic import BaseModel app FastAPI() class QueryRequest(BaseModel): query: str conversation_id: str | None None async def agent_events(query: str): state initial_state(query) async for event in app.astream(state): if retrieve in event: yield fevent: retrieve\ndata: {json.dumps({node: retrieve})}\n\n if generate in event: chunk event[generate][answer] yield fdata: {json.dumps({chunk: chunk})}\n\n yield event: done\ndata: {}\n\n app.post(/agentic-rag/query) async def query_agent(req: QueryRequest): return StreamingResponse( agent_events(req.query), media_typetext/event-stream )前端接SSE流之后可以先把“正在检索”“正在评估”这样的阶段状态渲染给用户生成阶段再逐字打字输出用户的等待感知会大大降低。4. 工程调优与避坑我在实践里趟过的坑4.1 检索死循环与上下文膨胀问题第一个坑就是检索循环。开始我的evaluate节点只有纯LLM判断结果遇到一个很刁钻的问题LLM每次都回答“上下文不足需要更多信息”Agent因此反复进入retrieve节点最后把整个向量库里相关的片段全拉出来了答案反而越来越乱。后来我给循环加了两个硬性限制最大迭代次数设为3每次重检索时query改写必须携带“本轮缺失实体”字段不能盲目去搜同一批内容。这样既避免了死循环也保证了每一轮检索都往没覆盖到的方向去查。第二个坑是上下文膨胀。多轮检索意味着每轮都要追加新的片段如果全部塞进prompt,LLM的上下文窗口很容易被塞满而且大模型会“迷失在中间”对片段中间部分的信息不敏感。我的处理方式是不追加全部片段而是维护一个全局上下文池在进入generate节点之前做一个基于相似度的片段去重和裁剪只保留与当前问题最相关的约3000 token内容。4.2 评估环节的不确定性如何让判断更稳evaluate节点用的是LLM任何基于LLM的判断都有概率波动。同样一组检索片段同一个评估prompt连续跑10次可能有1~2次给出不同结论这个问题被很多人忽略了。我做的缓解方案是引入投票式评估评估节点不以单次LLM调用为准而是连续调用两次评估以更严格的评估结果作为最终判断。比如两次都判定“充分”才进入生成阶段有一次判定“不足”就视为不足。代价是评估时延略有增加但整体准确率提升了大概10多个百分点。async def evaluate_node(state: AgentState) - dict: results [] for _ in range(2): r await llm_evaluate_sufficiency(state[retrieved_contexts], state[query]) results.append(r) final_vote insufficient if insufficient in results else sufficient return {evaluation_result: final_vote}另外评估prompt里不要写“如果你认为”“你觉得”这种开放式描述要写“仅当检索片段中出现与查询实体完全匹配的信息时才判定为sufficient”把判断标准收敛成硬性规则波动会小很多。4.3 pgvector调优中的召回率陷阱用pgvector时还有一个很隐蔽的坑HNSW索引如果构建参数不匹配向量维度会导致查询静默失败返回0行。embedding是1536维pgvector的HNSW对向量维度、distance函数和索引存储格式都很敏感。如果改了embedding模型比如从text-embedding-ada-002换成了别的1024维模型旧索引必须重建否则查询直接报错或者召回全空。还有一点很多人在pgvector里排序用的是embedding - $1的裸距离遇到不同的embedding模型时这个距离的绝对数值范围可能差异很大。有的模型距离普遍在0.7以上有的在0.3以下。如果你在业务代码里写死了“相似度小于0.5就认为相关”的阈值换模型之后逻辑会全面失准。我建议永远只用相对排序结果不用绝对距离做阈值判断。4.4 可观测性Agent过程不透明是排障最大敌人Agentic RAG本质上是多步状态流转一旦答案质量出了问题很难定位是改写错了、检索没召回、还是评估误判。所以我在设计系统时就强制要求LangGraph的每一个节点都把输入输出的快照写入日志。每个请求会记录一个trace_id日志里能看到完整的流转顺序原始问题 → 改写结果 → 检索条件 → 召回的片段ID列表 → 评估结论 → 重检轮次编号。这个可观测性设计在调优期救了我很多次。有一次某个领域问题反复答错看日志发现是planner节点生成了一个检索目标但对应的filter条件和向量库里的metadata字段名不一致导致检索返回空。没有trace日志这种问题靠肉眼猜是很难定位的。如果你用LangGraph可以直接用LangSmith做链路追踪也可以在节点里自己写结构化日志核心是不要让Agent变成黑盒。5. 从静态RAG升级到Agentic RAG的选型建议很多朋友会问我的项目到底该不该上Agentic RAG我的判断依据很简单如果你的查询场景大部分是单点事实查询比如查一个产品的规格参数静态RAG完全够用Agent化反而增加延迟和复杂度。但如果你的问题集合里有超过三成是多跳推理、横向对比、指代消解、多工具调用型问题那就值得投资一套Agentic RAG。技术栈上我目前比较推荐的组合是服务层FastAPI自带OpenAPI文档和异步支持接口维护成本低Agent编排LangGraph有状态图和条件路由机制做多轮检索循环比纯LangChain链式调用要干净很多LLM接入LangChain统一封装便于在OpenAI、开源模型、企业内部模型之间切换向量存储pgvector如果你的业务库已经在PostgreSQL上选它运维成本最低向量规模上亿再说Milvus或QdrantEmbedding模型中文场景建议用基于中文语料微调的模型英文场景用openai系列尽量不要混用这个组合不是“最推荐”的而是我在生产环境中验证过比较稳的组合。选型时如果你有更趁手的组件替换起来也很容易因为LangGraph的节点隔离了组件依赖。再分享一个经验Agentic RAG的prompt设计比传统RAG要复杂好几个量级。传统RAG只需要设计一个生成promptAgentic RAG需要维护planner prompt、router prompt、evaluate prompt、rewrite prompt、final answer prompt五套提示词。提示词之间还要做一致性约束比如planner输出的检索目标格式必须能被retrieve节点的解析器正确读取evaluate节点的判断标准又要依赖rewrite节点输出的实体字段。建议提前设计好一套JSON格式的协议贯穿所有prompt节点不要用自然语言文本做节点间通信否则排障会非常折磨人。写在最后一点真实的实践体会从静态RAG切到Agentic RAG不是简单换一个技术框架而是思路上的转变。静态RAG把所有复杂性都压给了“检索一次就要中”Agentic RAG则把“检索失败”变成可预期的正常路径允许系统通过理解、推理、决策去尝试多次检索。事实上我在生产环境里做过一个统计有17%的问题在第一轮检索后就被评估节点判定为上下文不足其中一大半通过第二轮改写检索拿到了答案。如果还做静态RAG这部分问题就只能答错或者答非所问。这个方向还在快速演进目前社区里热议的GraphRAG、Ontology RAG本质上都是在往“让检索尽量理解语义关系”的方向走。而Agentic RAG更侧重的是“让检索成为一个可以根据反馈自我修正的过程”。如果你正在做一个知识库问答或者企业信息助手我建议可以在小范围内先跑一个Agentic RAG的POC拿你们业务中最难的50个问题去做评测对比看看有没有实质提升再决定要不要全量推广。实践出来的结论永远比网上的争论更有参考价值。