从Naive RAG到Agentic RAG:生产级检索增强生成系统的工程实践
2026/9/20 4:38:36 网站建设 项目流程

1. 先复盘Naive RAG:看起来很美,一上线就露馅

如果你过去半年在认真调RAG,大概率经历过这样的场面:本地跑Demo时,代码链路顺滑得像德芙,随便问一句都能给出不错答案;可一旦接上真实业务文档、扔进生产环境,效果立刻变得薛定谔——同一个问题换个问法答案就跑偏,用户问点带时间、带条件、跨章节的问题更是直接翻车。我从Naive RAG一路做到Agentic RAG,最大体会是:RAG的复杂度不是线性增加的,而是当你把“检索出的内容对不对”当作核心问题时,整套系统的设计逻辑都要跟着变。

这篇不是论文复述,也不是开源项目洗稿,而是我基于FastAPI + LangChain + LangGraph + pgvector这条技术栈,从零搭起一套生产级Agentic RAG的真实记录。适合两类人看:一类是已经用Naive RAG做过原型、正被召回质量折磨的工程师;另一类是想直接切入Agentic RAG,但不想被眼花缭乱的概念带偏的初学者。你会看到Naive RAG到底死在哪、中间态做了哪些关键演进、Agentic RAG用什么样的架构把“检索”从一次性函数变成决策循环,以及每一步在工程上到底怎么落地。

1.1 一次检索打天下的三个硬伤

Naive RAG的标准流程,很多人闭着眼都能背:文档切块、Embedding入库、用户提问后做一次向量检索、取Top-K拼进Prompt、交给LLM生成。这个流程对“单点事实型问题”确实够用,比如“公司去年营收是多少”,只要文档里有这句话,无论向量检索还是全文检索都能把它捞出来。但真实业务里,用户不会总问单点事实,他们会问三类Naive RAG根本扛不住的问题:

第一类,语义漂移型。用户问“去年Q3各产品线的营收对比”,知识库里对应表述是“2023年7月至9月,A/B/C三条产品线的分季度收入分别为……”。两句话在字面上几乎没有重合的Token,向量相似度可能只有0.5多一点,Top-5里很可能只有一条沾边,其余全是噪声。第二类,多跳/聚合型。“我们华东区哪些客户同时采购过A产品和B产品,各自合同金额是多少”这种问题,需要先定位A产品相关的客户名单,再交叉筛选B产品的合同,再走一遍金额汇总,单次向量检索拿到的是半截信息。第三类,动态条件型。用户带着明确的条件和约束:“最近30天处理的工单里,有多少条属于P0级别且尚未关闭?”如果知识库只是按固定方式切块的静态文本,这类问题根本没有对应的“原生答案块”,Naive RAG检索得再准也是空中楼阁。

我用一个比喻来解释:Naive RAG就像你让一个实习生拿着索引卡去档案室找答案,每次只允许他跑一趟、只准带回几张卡。问题简单、索引卡上刚好有答案时,他表现很好;可一旦答案被分散在好几张卡里,或者问题本身需要“先找客户名单、再找合同、再算加总”的逻辑链,他连第一步该用哪张卡都很迷茫。

1.2 我实测过的“可解释失败”案例

理论说再多,不如一个具体的失败案例有说服力。我给客服知识库做过一套Naive RAG,文档是产品手册和工单记录混合体。测试时我问了一个非常业务的问题:“某客户报错E-1024,说是断电后出现的,怎么排查?”按人类直觉,正确答案应该融合三块信息:E-1024错误码的定义、断电场景的特有处理流程、客户设备型号对应的说明书章节。

Naive RAG的结果呢?它召回了三条:一条是E-1024错误码定义,一条讲的是“电源模块更换步骤”,还有一条是另一个客户用相同错误码的工单记录。生成时LLM在三条碎片里硬拼,居然编出了一套“先换电源模块再重置系统”的流程。单独看每步操作都来自文档,但整体流程是幻觉级别的拼接——文档里根本没有“E-1024与断电场景绑定”的因果描述。问题出在哪?出在切块把因果关系切断了,而向量检索又不具备“发现文档间逻辑关联”的能力。

另一个典型翻车来自跨文档聚合。知识库里有一份Q2销售总结、一份客户拜访纪要、一份产品白皮书。用户问“上季度Top客户对新产品的主要顾虑是什么”,答案实际上分散在三份文档里:销售总结里提到“某客户签约延迟”,纪要里写了“担心新老系统迁移成本”,白皮书里有些功能承诺。Naive RAG把三份文档各捞出来一段,LLM生成的回复看似全面,可每个信息点的来源和信任等级完全没被标注,用户没法分辨哪些是事实、哪些是推测。这是Naive RAG在生产环境被业务方诟病最多的问题:它给出的回复没有证据链条,所以无法审计。

1.3 别急着上Agent:先把Vanilla链路量化到能数清楚代价

我在接Agentic架构之前,花了整整两周做一件事:给Naive RAG的每一环装上“仪表盘”。因为你不上Agent还好,一上Agent,所有错误都会被放大——如果查询改写不准,工具调用就会选错;如果初始检索质量差,后面的反思评估就要多迭代好几轮;如果检索上下文太杂,LLM的生成质量立刻断崖下跌。没有基线数据,你根本说不清是Agent决策错了,还是最底层的检索和质量评估坏了。

我建议至少在这几个维度打点:召回的Top-5里有多少条真实相关(Context Precision),答案里有多少内容能在原文中找到证据(Faithfulness),以及不同Embedding模型在你这套文档上的A/B效果。不用一次做全,先用最便宜的方案:挑20~50条有标准答案的测试问题,人工标注每条问题的“理想检索结果”,再对比Naive RAG实际返回结果和最终答案,把失败原因粗分成“检索漏了”“检索到但排序太靠后”“内容相关但答案生成错了”三类。做完这个分类,你心里就有底了,后面每一步优化也都能精确归因。我见过很多人一步跳到Agentic RAG,结果迭代了半天发现最差的一环其实是Chunk Size设置不合理或者Embedding模型跟领域文本严重不匹配——这种问题加再多的Agent节点也救不回来。

2. 演进必经的过渡带:Query改写、Hybrid Search与Re-rank

RAG从Naive走向Agentic,中间的真正转折点不是某一天突然开始用LangGraph,而是一步一步把“检索”做厚。如果你问我哪一项投入产出比最高,我毫不犹豫选查询路由与改写——绝大多数检索失败,根因不是Embedding太差,而是LLM还不具备人类的“改写直觉”。

2.1 检索前的第一道闸:查询路由与改写

查询路由解决的是方向问题,改写解决的是精度问题。拿上面那个E-1024的例子,原始问题“某客户报错E-1024,说是断电后出现的,怎么排查”,如果直接拿去做Embedding检索,向量空间里“E-1024”的权重会被“断电”“排查”这些高频词稀释。我做的第一个改造是加一个Query Rewrite节点:用LLM把原始问题拆解成两条独立检索子查询——“E-1024 错误码 定义”和“断电后 恢复 E-1024 不能 启动 重启 解决步骤”,分别去检索,再合并结果。检索命中率肉眼可见地提升了,Top-5里的有效上下文从一条变成两三条。

这里的关键点是,改写不是简简单单重写一遍,而是要跟你的知识库结构对齐。如果知识库是按产品型号拆分文档的,你需要从问题中解析出型号并加到查询条件里;如果用户问题带了模糊的时间(“上一季度”“最近一个月”),你需要让改写器输出规范化的时间范围,方便后面的Metadata Filter用。我在工程上通常让LLM输出结构化JSON,比如:

{ "rewritten_queries": ["E-1024 错误码 含义", "断电 后 重启 E-1024 无法 进入系统"], "filters": {"product_line": "A1", "doc_type": "manual"} }

这样检索层拿到的就是明确的查询词和过滤条件,而不是一段口语化的问题原文。查询路由则可以理解成一个if-else:问题是算财务指标,走表格类工具;问题是查合同条款,走全文检索;问题是开放性的技术咨询,走向量检索加生成。路由错了,后面再好的检索也都是空转。

2.2 多路召回:向量和全文检索不是替代关系

我见过很多团队做RAG时,从第一天就只上向量检索,完全忽略BM25或PostgreSQL自带的全文检索。但向量检索有个天然缺陷:它适合“语义相似”的召回,却不擅长“关键词精确命中”。业务场景里大量问题其实是指令式的清晰关键词,比如查“提单号BL2024001234”“工单号INC-778899”,这种精确ID型的查询,纯向量召回的表现非常不稳定,有时候相似ID会混淆,有时候关键数字会被语义向量稀释。

所以我在做中间态演进时,毫不犹豫上了Hybrid Search:一路走pgvector的向量检索,一路走PostgreSQL的tsvector全文检索,两路结果用RRF(Reciprocal Rank Fusion)合并。RRF的公式不复杂:对每篇文档在所有检索结果中的排名取倒数,将多路排名的倒数相加,最终按总分排序。它最大的好处是不需要调权重,对不同路的分数分布不敏感,这在多路召回场景里省了很多调参的头痛时间。你只需要给不同来源的文档打上来源标记,RRF把名次信息汇总即可。

从实际效果看,混合检索把我的客服知识库的Recall@5从0.61提升到0.79。原因也很简单:向量检索负责“语义相关”,全文检索负责“关键词精确命中”,两者重叠的部分用RRF放大排名,不重叠的部分互为兜底。这条经验后来一直带到了Agentic架构里——两个检索工具,dense_search和keyword_search,根据需要分别调用或并行调用。

2.3 精排兜底是性价比最高的一环

即使做了多路召回,Top-10里仍然可能有四到五条是无关片段。如果不做精排,把这些内容全塞给LLM,一方面浪费Token,更重要的是无关内容会干扰生成,甚至诱发幻觉。精排(Re-rank)是这整个演进过程里性价比最高的投资。

我用的是Cross-Encoder结构的Re-rank模型,它在CPU上跑即可,对每条候选Query-Doc对打分,输出0到1之间的相关性分数。为什么用Cross-Encoder而不是向量相似度再算一遍?因为Cross-Encoder让Query和Doc在注意力层做了完整交互,能建模“这个文档是否真正回答了这个问题”,而向量检索只是“文档与问题的隐空间距离接近”。我用一段本地模拟测试做了直观对比:向量检索Top-10里排第三的是错误码定义,但与问题的相关性其实不如排第七的断电恢复流程,Cross-Encoder精排后正确顺序被大幅纠正,相关文档被提到最前面。

精排还有一个附加好处:它可以成为Agentic循环里“相关性评估”的轻量替代品。如果你不想在每个节点都调用LLM做判断(贵、慢、不稳定),完全可以用一个Re-rank模型的threshold来卡——低于0.3的候选不进入上下文。这样,重排模型既是检索的后处理,也是Agent判断“检索是否成功”的哨兵。

3. Agentic RAG的本质:检索从“函数”变成“决策循环”

走到Hybrid Search加Re-rank这块,其实已经比Naive RAG能扛事多了。但对于开头说的那三类问题——动态条件、多跳聚合、跨文档推理,它依然是“一次检索打天下”的变体,只不过把“一次”做得更好。真正拉开代差的,是让LLM具备计划、执行、评估、再执行的闭环能力。这就是Agentic RAG。

3.1 为什么LangGraph适合做这个循环

我用LangGraph之前试过两条路:一条是用LangChain的AgentExecutor串工具,另一条是自己在FastAPI里手写状态机。它们的致命问题都很一致:流程一旦复杂,控制逻辑就变成一团乱麻。

LangChain AgentExecutor的抽象太黑了——它默认的行为是“LLM决定下一步,然后行动,再把观察结果喂回去”,看起来很智能,但它不擅长让开发者严格定义“什么情况下必须走哪条边”。我在需要“检索结果不合格时进行查询改写并重试”这个场景时,发现AgentExecutor的思考链路常常不稳定,同一个问题跑五次可能有三种不同的工具调用顺序。

手写状态机的问题则是反方向的,逻辑完全透明了,但状态维护和回溯逻辑全堆在业务代码里,维护成本越来越高。比如中间要加一个“如果重试超过2次就转人工”的边,手写代码要改一大片,而LangGraph只需改一行条件边。

LangGraph解决的是“可控的图结构+灵活的状态传递”。它把整个流程建模成一张图:节点就是函数(Plan、Retrieve、Grade、Rewrite、Generate),边可以是普通边或条件边,状态就是一张全局快照,所有节点都可以读写。它对Agentic RAG来说最合适的点在于:允许你把分析判断显式建模成“节点”,而不是藏在Prompt里的隐形逻辑。比如我明确建了一个Grade节点,验证库里的文档是不是真能回答问题;不够就走到Rewrite节点,够了就直接去Generate。每一个决策点都在图里一目了然,出了问题直接看图定位,这是AgentExecutor给不了的。

3.2 一个生产级Agentic RAG的最小状态机

我们用一个可运行的简化版来拆解。先定义状态:

from typing import TypedDict, List, Optional class AgentState(TypedDict): question: str plans: List[str] retrieved_docs: List[dict] filtered_docs: List[dict] answer: str iterations: int max_iterations: int reflection: str

整体Flow是这样一条循环链:PlanNode根据问题生成检索计划(拆子问题、选工具);RetrieveNode按计划执行多路检索;GradeNode判断当前上下文是否足够回答;如果不够,RewriteNode改写查询或调整工具,然后回到Retrieve;如果够了,GenerateNode输出终答。

from langgraph.graph import StateGraph, END graph = StateGraph(AgentState) graph.add_node("plan", plan_node) graph.add_node("retrieve", retrieve_node) graph.add_node("grade", grade_node) graph.add_node("rewrite", rewrite_node) graph.add_node("generate", generate_node) graph.add_edge("plan", "retrieve") graph.add_edge("retrieve", "grade") graph.add_conditional_edges( "grade", decide_next_step, { "generate": "generate", "rewrite": "rewrite", "retrieve": "retrieve" }, ) graph.add_edge("rewrite", "retrieve") graph.add_edge("generate", END) graph.set_entry_point("plan")

这里最重要的不是每行API,而是那个decide_next_step——它才是Agent的“大脑”。这个函数接收当前状态,返回下一个节点名。我在生产里不会直接让LLM自由发挥,而是给一个结构化决策模板,让它输出一个JSON:

{ "verdict": "sufficient" | "insufficient" | "conflict", "reason": "已有两段文档互相矛盾,需要额外检索第三方来源", "next_action": "retrieve_with_modified_query" }

同样,PlanNode也不需要LLM从零自由发挥,而是基于工具清单来规划。如果注册了3个工具,PlanNode的任务就是从工具清单里选1到2个,加上改写后的query,形成一个可执行的工具调用计划。这样既保留了Agent的灵活性,又避免了完全自由带来的不可控。

3.3 工具设计:把“检索”拆成Agent手里的可调用工具

传统Naive RAG只有一个隐式工具:“向量检索Top-K”。Agentic RAG里,工具设计成了开放集合,我把它分成四个类型:

工具类型作用典型场景
语义检索工具走Embedding+pgvector开放式问题、语义相关查找
关键词检索工具走PostgreSQL tsvector精确ID、型号、错误码
结构化数据工具走SQL/Pandas统计、聚合、条件筛选
外部知识工具走Web/API知识库覆盖不到的最新资讯

这四种工具统一交给PlanNode选择。比如用户问“最近30天P0工单的关闭率是多少”,PlanNode直接选择结构化数据工具,不再走“先检索文档片段再让LLM瞎编”的路子;用户问“E-1024报错怎么处理”,PlanNode同时选择语义检索加关键词检索,多路结果融合后再送Grade节点评估。

工具拆分带来的另一个好处是可观测性。哪个工具被调用、返回了什么、耗时多久、被Grade节点打了什么分,全都可以记录在LangGraph的状态里。上线后我通过查看P50/P90工具调用链,能快速定位是哪一步拖了回答质量的后腿。这种“万物皆工具、调用可追溯”的设计,是Agentic RAG与Naive RAG在生产运维层面的本质差别。

4. 完整实战:FastAPI + LangChain + LangGraph + pgvector落地

纸上谈兵聊完,进入实战环节。我用一条完整技术栈搭了一套生产级Agentic RAG服务:FastAPI做服务层,LangGraph做Agent编排,LangChain做工具和模型抽象,PostgreSQL+pgvector做存储和向量检索。下面按存储、Agent、服务、配置四层拆开讲。

4.1 存储层:pgvector的schema与索引设计

先建表。文档切片表是整套系统的地基:

CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE documents ( id BIGSERIAL PRIMARY KEY, chunk_text TEXT NOT NULL, metadata JSONB, embedding vector(1024), created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);

选1024维,是为了适配bge-m3这类开源中文Embedding模型;如果你用OpenAI的text-embedding-3-large,那维度是3072,也可以做降维,但要注意降维后的质量损耗。数据库侧我用HNSW索引,而不是IVFFlat,原因是HNSW在数据量中等(几十万到几百万)且写入频繁时表现更稳,查询耗时能稳定在几十毫秒内,而IVFFlat则要定期重建训练聚类。对RAG这种“写入持续、查询并发”的形态,HNSW更省心。

metadata这一列是Agentic RAG的胜负手。因为它可以存文档来源、日期、产品线、文档类型等结构化字段,才能支撑后续的Metadata Filter。比如用户问“2024年Q1的E系列产品故障率”,如果只做纯向量检索,所有文档一视同仁,这个month/产品线的约束根本进不了检索条件;但有了JSONB字段和GIN索引,就可以在SQL里加上WHERE metadata->>'product_line' = 'E' AND metadata->>'period' = '2024Q1',把范围大幅收窄。没有metadata结构化的向量库,谈不上真正的Agentic RAG,因为Agent无法按条件筛选工具结果。

4.2 Agent层:plan-retrieve-grade-rewrite-generate的工程实现

LangChain在这里的作用,是把底层模型和工具抽象成统一接口。我用LangChain的create_react_tool封装检索工具,用ChatOpenAI(或本地的Ollama/LLaMA,按企业合规选型)做LLM。LangGraph负责状态机编排。

RetrieveNode的内部实现是混合检索加精排的整合:

def retrieve_node(state: AgentState) -> AgentState: docs = [] for q in state["plans"]: # dense retrieval dense_hits = pgvector_search(q, top_k=10) # keyword retrieval via tsvector keyword_hits = tsvector_search(q, top_k=10) docs.extend(dense_hits + keyword_hits) # deduplicate and rerank reranked = bge_reranker.rerank(state["question"], docs) filtered = [d for d in reranked if d["score"] >= 0.4] return {**state, "retrieved_docs": filtered[:5]}

GradeNode用LLM做判断题,输出一个结构化结果:

grade_prompt = """ 你是检索质量评估员。根据给定的问题与检索到的文档,判断这些文档是否足以回答问题。 输出JSON:{"verdict": "sufficient"或"insufficient"或"conflict", "reason": "...", "missing_keywords": "补充检索的关键词"} """

这里还有个细节:GradeNode不是我随便拍拍脑袋设计的,而是直接对标了RAGAS里的answer_relevancyfaithfulness这两个指标。GradeNode的verdict相当于在线版的“context sufficiency”评判,离线评估与在线判断用同一套标准,这样你才可能用离线的指标去预测在线体验。

RewriteNode则是把GradeNode给的重试建议转成新的检索Query。它会读取上一次的missing_keywords,结合原问题进行扩展改写,同时调整metadata过滤条件。我限制最大迭代次数为3——超过3次还检索不到高质量内容,就直接进入GenerateNode,让LLM在提醒“资料不足”的前提下基于已有信息作答,避免无限循环拖垮响应时间。这个边界非常重要,后面会细说。

4.3 服务层:FastAPI如何接入LangGraph并管理多用户会话

FastAPI侧要做的核心事情有三件:接收请求、调用Agent图、返回流式结果。这里的坑集中在异步与并发上。在Async路由里直接调用graph.invoke()会阻塞事件循环,正确做法是用graph.ainvoke(),或者在同步函数里套run_in_executor

多用户会话管理是我踩过最多坑的地方。RAG在生产环境是会被很多用户同时调的,LangGraph State如果直接存在内存里,进程重启后用户会话就丢了。我的做法是把会话状态存到PostgreSQL:每轮对话结束后,把AgentState序列化为JSON存进一张agent_sessions表,下次用户发消息时先把State加载回来,再作为初始状态传给LangGraph。

app = FastAPI() @app.post("/api/chat") async def chat(req: ChatRequest): state = load_session(req.session_id) state["question"] = req.message final_state = await agent_graph.ainvoke(state) save_session(req.session_id, final_state) return {"answer": final_state["answer"], "trace": final_state["trace"]}

这样设计还有一个好处:调用链Trace也跟着State一起落库了。每次用户问答,哪个工具被选、检索返回了什么、Grade给了什么分、迭代了几轮,全都能在管理后台回放。出了问题,你不用靠用户截图反馈,直接查Trace日志就能复现。

4.4 让流程真正能跑起来的三个配置细节

这块内容很碎,但经验值最高。第一个是RecursionLimit。LangGraph默认的递归上限是25步,如果你没显式配置,一个包含4到5次迭代的Agent流程很容易触发RecursionLimit异常。我在初始化图时直接设:

app = graph.compile(checkpointer=pg_checkpointer) graph_config = {"recursion_limit": 30}

第二个是LLM的temperature和输出格式。Plan、Grade、Rewrite这类中间决策节点,temperature一律调到0,并让模型按JSON Schema输出;只有最后的Generate节点可以保留0.3左右,保证回答有自然语言多样性。别在决策节点追求创造力,否则Agent会“发挥”出你意想不到的工具选择。

第三个是pgvector的查询超时和连接池。检索响应慢,很多时候不是Agent慢,而是数据库连接池打满。我至少会用psycopg_pool的连接池配置成20个连接,并给慢查询设超时阈值,防止某个超大metadata过滤条件把CPU拖死。向量检索和常规CRUD服务共用数据库时尤其要注意,优先保证SQL关键路径的稳定性。

5. 效果评估与踩坑记录:从Demo到可上线之间隔着什么

Agentic RAG最让人头疼的问题,就是Demo跑起来“看起来什么都会”,上线之后“什么都救不了”。因为Demo问题少、范围窄,Agent随便转转就能答对;而真实用户的问题是长尾分布,你要验证它不是靠感觉,而是靠一整套评估体系和足够的边界测试。

5.1 用RAGAS和你自己的case set做纵向对比

我强烈建议在项目一开始就把评估框架搭起来,而不是等到功能做完了再补。我用的是RAGAS这套工具,主要衡量四个维度:Faithfulness(忠实度),看答案是否能在上下文中找到证据;Answer Relevancy(答案相关性),看答案是否切题;Context Precision(上下文精确性),看检索到的内容里有多少真正相关;Context Recall(上下文召回率),看相关文档是否都被检索到了。

但光有公开指标不够,一定要建自己的Gold Case Set。我从真实用户日志里挑了100条problematic question,每条标注了“理想工具调用链”和“理想答案关键点”,做成回归集。每次改查询改写提示词、换Embedding模型、调整Re-rank阈值,都在这100条上跑一遍,观察指标有没有回退。

纵向对比表格最直观:

版本FaithfulnessAnswer RelevancyContext Precision平均迭代轮数P95响应时间
Naive RAG(纯向量)0.720.580.4311.8s
+Hybrid Search+Re-rank0.810.760.6412.1s
+Agentic(Plan-Grade-Rewrite)0.870.830.722.43.6s

这套数据说明两件事:第一,Agentic RAG确实带来了质量提升;第二,代价是响应时间和迭代轮数上去了。所以在生产环境,我不会对所有流量都跑完整Agent循环,而是先用查询路由做个分类:简单事实性问题走轻量链路(直接Hybrid Search+Re-rank+Generate),多跳复杂问题才走完整Agent循环。这和很多商业产品的“Strict/Moderate/Open”分级思路是一致的——不是所有用户问题都值得花3.6秒去求解。

5.2 我在落地过程中踩过的五个坑

第一个坑是Pydantic v1/v2与LangChain LS的兼容性问题。新项目直接上Pydantic v2问题不大,但老项目里如果还留着Pydantic v1的模型,LangChain在序列化时会报各种莫名的AttributeError,排查起来相当费劲。建议:LangGraph和LangChain的版本锁死用同一套,虚拟环境隔离干净,别混装。

第二个坑是Grade节点的LLM判断不稳定。同一个问题、同一批文档,跑三次Grade,有时返回sufficient,有时返回insufficient。我反复调prompt后发现,根源在于decision prompt没有给模型“正反例”。修改方式是先给两个few-shot示例,并明确告诉模型:“只有回答所需的所有关键实体都出现在文档中,才判定sufficient”。同时把llm temperature设为0,并要求输出JSON结构。改完以后,同一输入的判断一致性明显提升。

第三个坑是metadata过滤条件在pgvector里没有正确下推。早期我图省事,先用向量检索取Top-100,再在应用层按metadata过滤,结果大量之前没有元数据约束的文档挤占Top-100名额,即使后来做精排也救不回来——因为真正的相关文档早被过滤掉了。正确做法是把metadata过滤条件写进SQL的WHERE子句,让数据库在向量搜索之前先按字段缩小候选集。

第四个坑是多轮对话里的历史污染。用户连续问了三轮之后,第四轮的Query如果不做上下文清洗,经常变成“那它呢”这种指代词,Agent改写时会把前几轮的所有信息都带进Embedding,检索出来的结果一片混乱。我的处理方式是:每一轮的用户消息发送前,先从聊天历史里抽取出“对话摘要”,只把摘要和当前问题合并作为检索Query,而不是把完整历史全部拼接进去。

第五个坑是工具选型被“结构化陷阱”绑架。结构化数据工具确实好,但如果业务数据并没有一个整洁的关系库,硬把它做成SQL工具,Agent会不断生成SQL然后报错重试,浪费大量迭代次数。我后来给SQL工具前面加了一层“表结构预览”,让PlanNode先看表名、字段名、样例值再决定要不要走SQL。这个看似不起眼的修改,把SQL工具的首轮成功率提升了三分之一。

5.3 上线前必须验证的边界条件

除了指标,我还会做一组边界测试。这些case通常不会被写进标准评估集,但它们最能暴露系统的底层脆弱性。

空检索测试:当知识库里没有任何相关内容时,Agent会不会硬答?我要求Generate节点在Grade判定insufficient后,输出格式必须是“抱歉,我未找到相关资料”,并附上已检索过的关键词列表。这样用户至少能知道系统“尝试过什么”,而不是被幻觉答案误导。

超时熔断测试:Agent最多迭代3次,如果3次之后Grade仍判定不满足,必须直接进入Generate或返回兜底回复。千万不能让单个请求无限循环,否则下游服务和数据库连接都会被打爆。

长文本上下文测试:当多轮检索结果为条目越来越多时,会不会超出模型上下文窗口?现在主流模型支持128K上下文,但窗口大不代表生成效果好——上下文越长,检索噪声越多。我给最终生成阶段设置了“最大上下文条数”和“单条最大字符数”两个上限,超出的部分直接截断,优先保留Re-rank分数最高的那几条。

并发测试:用Locust模拟50并发用户同时提问,观察LangGraph的checkpointer、pgvector连接池、LLM的rate limit三者之间会不会互相拖垮。这类测试做到后面,基本都是在调限流和缓冲队列,而不是调模型效果了。很多人做Agentic RAG死在最后一步,就是只测了单条质量,没测并发下的稳定性。

最后说一点个人体会:Agentic RAG不会天然解决Naive RAG的缺陷,它只是给你提供了一套系统框架来暴露和迭代这些缺陷。如果底层文档切得不合理、Embedding模型选得不对、评估集不够贴业务,那么Agent的每一次循环都会把这些问题放大一遍。我后来能在线上稳定运行这套系统,靠的不是迷信Agent概念,而是把评估集、灰度、Trace、监控这些“工程基建”也当成RAG系统的一部分来做。哪怕你的场景还没有复杂到需要完整Agent循环,也可以先把Grade和Rewrite这两个节点想清楚——它们真的会是整个链路中最有价值的部分。

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

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

立即咨询