1. 项目概述:从"检索问答"到"可编排的智能检索"
先说结论:Agentic RAG Pipeline不是把一个大模型包一层检索工具那么简单,它是一个让模型像人一样去"查资料、筛资料、质疑资料"的完整流程系统。这两年只要做知识库问答、企业文档助理、垂直领域客服,迟早都会从普通RAG迁到Agentic RAG这条路上来,因为传统RAG的瓶颈卡得很死。
传统RAG的做法,一句话就能说清楚:把用户问题拿去向量检索,取回最像的几个片段,塞进提示词,让模型根据片段回答。这个模式在Demo里非常惊艳,放到生产环境里却经常翻车。我见过最典型的几个事故:用户问的是"上个月的销售报表为什么和财务对不上",普通RAG直接召回一堆"销售报表格式说明",模型一本正经地编了个对账结论;用户连续追问"那华东区呢""换成季度维度再看看",每一轮都是独立检索,上下文完全断开;还有的时候检索库里没有相关内容,模型照样硬答,根本不会说"我不知道"。
Agentic RAG解决的就是这一类"问答本身是一个决策过程"的场景。它把一次问答拆成多步:先判断该不该检索、检索哪个库、要不要换个关键词再查、查回来的片段够不够、答案有没有依据、需要不需要追问补充。每一步都像人在查档案一样,能停能退能重来,而不是一把梭到底。
这个项目是我在做一个企业内部知识库问答系统时的完整实践复盘。系统要覆盖销售数据、技术文档、客服话术、竞品方案四类差异很大的资料,用户提问方式从一句话指令到复杂对比都有。项目从普通的"向量检索+生成"一路演进到Agent编排,上了路由、工具调用、自我校验、缓存和分布式索引,最后跑成了一个能扛住日活十万级调用、回答质量可回归测试的生产级服务。
如果你正准备从传统RAG往Agentic RAG升级,或者已经遇到"召回不精准、多轮断裂、答案不可信"这几个老问题,这篇内容应该能帮你少踩一半的坑。下文会完整拆解我的架构选型、核心实现、生产级改造和故障排查过程,尽量讲清楚每一个关键选择背后的原因,而不是只给一个结论。
2. 架构设计与技术选型:先把场景想清楚,再选轮子
2.1 传统RAG的四个硬伤,决定了架构必须变
在动手设计Agentic架构之前,我先复盘了传统RAG在真实场景里的四个硬伤,这些硬伤直接决定了哪些组件必须引入。
第一,单次检索的召回上限太低。一个用户的真实需求往往不是一句话能表达完的。比如"帮我找一下去年华东大客户的续约情况和主要流失原因",这里其实包含了实体(华东、大客户、去年)、动作(续约、流失)、维度(原因)三个检索面。普通的相似度检索只做一次,它只能用整个问句去匹配,很可能把"续约情况"和"流失原因"两个面都匹配得模棱两可。解决这个问题的思路不是把向量模型调得更强,而是引入"多路检索":先让Agent拆解需求,分别检索,再汇总。
第二,召回片段和用户问题在语义上"相似"但逻辑上无关。这是我调试中最头疼的一类问题。向量检索返回的top_k,很多是"文字风格像"而不是"逻辑上能回答问题"。比如用户问"预算审批流程需要几个工作日",召回结果可能是"预算编制工作日的计算方式",词面高度相关,实际答非所问。传统RAG没有机制去校验"这段内容到底能不能回答问题",只能靠模型硬生成。
第三,多轮对话上下文维护基本为零。用户说"那换成季度呢",普通RAG会把这句话拿去检索,结果什么都查不到,因为它没结合上一轮的"华东大客户续约"一起检索。生产环境里用户根本不是一次问完的,这个问题不解决,系统就没法真正上线。
第四,没有"不知道"的机制。传统RAG的提示词里写一百遍"如果资料中没有答案请告知用户",模型也会因为生成习惯而编造。真正有效的做法是在流程层面拦截:检索结果经相关度校验后低于阈值的,直接走"无法回答"分支,根本不进入生成阶段。这就是Agentic和普通RAG的本质区别——决策逻辑从提示词挪到了代码和流程里。
2.2 核心组件拆分:六个环节各司其职
基于上面四个硬伤,我把整个Pipeline拆成了六个环节:意图解析、检索规划、多路执行、结果融合、答案生成、质量自检。每个环节都不是新造的轮子,而是把已有技术按特定方式编排起来。
先看意图解析。这里不是简单做意图分类,而是用模型把用户的原始输入整理成一份"调研计划",包含需要检索的子问题、限定条件、预期返回格式。比如用户问"分析一下今年华南区客户的流失原因",解析结果可能是:子问题一"华南区客户流失率",子问题二"流失原因分类及占比",限定条件是"今年、华南"。这一步做得扎实,后面每一步都会有正确的输入。
再看检索规划和执行。规划决定用哪些检索工具,执行则并行调用这些工具。我这边架构上支持三类检索工具:向量库检索、结构化数据库查询、API接口检索。向量库负责非结构化资料,SQL工具负责销售、财务等结构化数据,API负责对接公司内部系统。Agent根据意图决定调哪个工具,这一步也是生产环境中收益最明显的地方——以前所有问题都打向量库,现在订单类问题直接走SQL,准确率提升了不止一个量级。
结果融合是很容易被忽略的环节。多路检索会返回很多片段,而且各自来源不同,直接全塞给模型会把上下文撑爆,还会让模型被不相关信息带偏。我的做法是加一层重排器,把所有候选片段重新打分,去掉明显不相关的,只保留跟子问题真正对齐的片段。这等于在"检索"和"生成"之间加了一道质量门禁。
最后是质量自检。模型给出答案之后,不能直接返回用户,而是把答案和支撑片段交给一个独立的校验节点打分,检查三件事:答案有没有脱离资料、关键数字和原文是否矛盾、用户问题里的条件是否都被覆盖到。校验不通过就触发重新检索或者二次生成。这个环节听起来简单,实际效果非常惊人——我上线后跑了两周的日志,发现自检能拦下大概12%左右的错误回答,而且绝大多数是那种"看起来合理但实际错了"的答案。
2.3 技术选型的取舍:模型可以通用,流程必须可控
选型层面,我的核心原则是"模型疯狂换可以,流程骨架不能乱"。现在基座模型迭代速度太快,今天用的模型可能三个月后就想换掉,但如果流程代码里到处耦合着模型API的细节,每次换模型都是灾难。所以我在设计上做了一层薄薄的模型适配层,统一管理调用、超时、重试和回退,业务代码只依赖这个抽象接口。
编排框架的选择也很关键。市面上的Agent框架很多,从最基础的ReAct范式到图编排范式都有。ReAct范式写起来简单,一个循环里反复"思考-行动-观察",但它的控制流很弱,很难表达"先并行检索两路,再根据结果决定走SQL还是向量库"这种带条件分支的流程。我最后选的是图编排范式:把上面说的六个环节定义成节点,节点之间用显式的边连接,允许条件跳转、并行分支和循环回退。代价是代码量比ReAct多不少,换来的是每个环节都可以单独插桩、定向调试、权限控制。
向量数据库的选择上,我测过纯托管服务和自建开源方案。托管服务省心但参数和索引策略都被锁死,自建方案初期麻烦但后续调优空间很大。考虑到我们的数据量增长很快且查询模式多样,最终选择了自建的方式,把向量索引、标量过滤、动态字段更新全都掌握在自己手里。这里有一点要特别提醒:不要因为贪图方便把所有数据放进同一个collection,一定要按业务域分库,否则不同语义空间的向量会互相干扰,召回质量会显著下降。
目录索引更新的策略也值得展开一下。生产环境里文档是持续变动的,今天改了合同模板,明天上线了新政策,如果索引不更新,Agent检索到的永远是老内容。我采用的方式是增量更新加版本管理:每个文档块都带版本时间戳,定时任务扫描变更,只对变更部分重新切块和向量化。这样既能控制计算成本,又保证检索到的是最新内容。
3. 核心实现拆解:每一步都有明确目的
3.1 文档处理与向量索引:切块参数不是玄学
文档处理这个环节,很多人会低估它的影响。切块大小、重叠长度、是否保留层级信息,这些参数直接决定了后续所有环节的上限。一个典型的反例是:把一份二十页的技术方案当成一个块去向量化,检索时确实能召回这份文档,但它是"整块召回",模型读到的上下文里有大量无关章节,生成的答案自然稀碎。
我最终的切块方案是这样的:按语义段落切分,块大小控制在400到600个token之间,相邻块重叠80到120个token。这个数字不是拍脑袋定的,它是根据我们业务文档的平均段落长度和问题粒度测试出来的。块太小了,语义不完整,一个问题的答案被切到两三个块里,检索只能召回一半;块太大了,噪声增多,重排器压力变大。重叠区域是为了防止检索时正好落在段落边界导致关键信息丢失。切完后,每个块会标注来源文档ID、章节路径、标题层级、更新时间,这些元数据在后面做过滤和展示时非常有用。
向量化的细节同样值得注意。嵌入模型我们试过多个,最后选了一个在中文业务文档上表现稳定的通用模型,维度是1024。关键的调优点是:嵌入之前要做文本清洗,把表格转成带标题的文字描述、把PDF的页眉页脚去掉、把"第X页共Y页"这类噪音剔除干净。不做清洗的话,检索结果里会混进大量页眉噪声,尤其是技术文档,几乎每页的页眉都是同一段话,会严重干扰相关性排序。
索引构建策略上,我用的是IVF倒排加乘积量化的组合方式。检索时先通过倒排索引缩小候选集,再计算向量距离精排。生产环境为了性能,牺牲一点召回率是必要的。但这里有一个重要原则:如果业务要求高召回率,就要随时准备切回暴力检索模式。所以我留了一个开关,支持按查询动态选择检索模式,而不是全量走ANN索引。
3.2 Agent编排层实现:状态机是核心,工具是边界
Agent编排层是整个Pipeline的大脑。我的实现不是让模型自由发挥,而是把它约束在一个状态机里。每个节点只干一件确定的事,模型的能力被用在"做出选择"上,而不是"决定流程怎么走"上。
举个例子,路由节点的内部逻辑是这样实现的:
def route_query(intent: Intent) -> str: """ 根据意图解析结果决定走哪个检索分支。 返回: vector / sql / api / none """ if intent.query_type == "structured_data_analysis": # 订单、财务、人效这类问题,直接走结构化查询 return "sql" if intent.query_type == "knowledge_retrieval": # 方案文档、流程制度、FAQ 这类问题,走语义检索 return "vector" if intent.query_type == "real_time_status": # 涉及实时状态的问题,走 API 调用 return "api" if intent.need_more_info: # 信息不足以定位问题,进入澄清对话 return "clarify" return "none"这个函数看起来简陋,但它把Agent的行为限定在了几种模式里,不会出现"模型自由发挥去调用一个不存在的工具"这种生产环境事故。这里有个经验:不要试图让模型自己发明工具,工具集必须是提前定义好的白名单,模型只能做选择题,不能做填空题。
工具定义也要注意两个细节。第一,每个工具的描述要写得像"给人类助手的指令"一样清楚,包括它能干什么、不能干什么、输入参数怎么填。模型就是靠这些描述来决定调哪个工具的,描述含糊等于让模型盲猜。第二,工具的参数要做强校验,尤其要防止LLM输出非法参数。我的做法是在工具入口统一做一层schema校验,不合法就返回一个友好的错误信息给Agent,让它自己修正,而不是直接让Pipeline崩溃。
节点之间的状态传递也得仔细设计。我把中间结果设计成不可变的结构,每个节点只读取上一个节点的输出,写入自己名字下的新字段。这样最大的好处是可以随意回放任意一条请求的完整执行轨迹,对定位问题帮助极大。排查的时候,只要看每个节点的输入输出就能知道是哪一步出了岔子,不需要在日志汪洋里捞针。
3.3 缓存、并发与限流:可扩展性的第一道地基
很多做RAG的人都忽略了缓存,我最初也没做缓存,结果上线第一周就被同样的热门问题打了三千多次向量库,成本和延迟双双爆表。后来我加了两层缓存,问题立刻缓解。
第一层是答案级别的语义缓存。用户问的问题经常有同义表述,比如"上个月退款率多少"和"上月退款比率"本质是一样的。如果还按字符串精确匹配,缓存毫无用处。我的做法是把用户问题先做一次嵌入,然后用向量相似度去缓存里找历史问题,相似度高于0.92的直接复用答案。这层缓存命中率在我们的场景里能有20%以上,效果很明显。
第二层是检索结果缓存。单纯缓存答案有一个问题:如果资料更新了,缓存里的答案还是旧的。所以我把缓存键设计成"查询向量+资料版本号"的组合。资料版本号变了,缓存自然失效,不需要手动清理。这个设计在文档频繁更新的业务里非常实用。
并发和限流这块,直接关系到生产稳定性。Agentic RAG的每个请求会调用多次模型和检索工具,比普通RAG的耗时和资源消耗大好几倍,如果不做限流,一个流量尖峰就能把下游模型服务的配额打爆。我的方案是双层限流:入口层用令牌桶限制总QPS,节点层给每个模型调用单独设置并发数和超时时间。再有就是熔断机制,某个依赖服务连续报错超过阈值,就自动切换降级策略,比如检索服务挂了直接返回友好提示,而不是让Agent无限重试把整个链路拖死。
4. 生产级改造:可扩展性、可观测性与评估
4.1 水平扩展思路:无状态优先,有状态隔离
很多人一听到"可扩展"就开始讨论分布式向量库,但我的经验是先把无状态部分做干净。Agent编排层本身是无状态的,状态只存在于请求上下文里,这意味着我可以随时横向扩容编排服务的实例数。真正有状态的是向量索引、缓存和对话会话,这三部分要单独设计。
向量索引的水平扩展,我采用的是按业务域分片加副本的方式。每个业务域维护独立的分片,比如销售文档一个分片、技术文档一个分片。查询来的时候,先在路由层确定要访问哪些分片,再去对应分片上检索。这样单个分片的数据量不会无限膨胀,内存占用稳定可控。加新业务域的时候,直接挂新分片就行,不动老集群。
缓存服务的扩展要点是"冷热分离"。热数据放在内存层,比如每天活跃问题的缓存,冷数据放到专门的低成本存储。我设了一个简单的规则:一周内被命中超过五次的访问进入热缓存,其他全部下沉。这个策略让热缓存命中率稳定在70%左右,内存占用却控制了80%以内。
会话状态的处理也值得注意。Agentic RAG的多轮对话会把上下文越攒越长,如果全部放在内存里,并发一高就会吃满内存。我的做法是只保留当前任务窗口内的信息,比如最近六轮的关键实体和检索结果,而不是把每轮的完整中间状态都堆在内存里。任务结束就把压缩后的摘要写回会话存储,下次继续时重新加载。
4.2 可观测性设计:没有追踪,优化无从谈起
Agentic RAG的请求链路比普通接口长得多,一个请求可能经历多次模型调用、多次检索、一轮自检重试,任何一个环节变慢都会影响整体体验。所以可观测性必须从第一天就开始做,不能等出了问题再补。
我做的第一件事,是给每个请求生成一个全链路追踪ID。这个ID从入口开始,贯穿意图解析、路由、检索、生成、自检、返回,每跳落一条监控日志,记录节点名、耗时、输入输出摘要和状态。查询的时候,一条命令就能拉出这个请求完整走过了哪些节点、每步花了多长时间。排查"为什么这么慢"的时候,真的事半功倍。
第二件事是自定义三个核心指标:端到端耗时、节点级耗时、工具调用成功率。端到端耗时是用户体验的直接反馈,节点级耗时能快速定位瓶颈是在检索还是模型生成,工具调用成功率则暴露Agent选错工具或参数错误的频率。这三个指标配合分位数统计,我可以看到P50用户在体验什么水平、P95用户到底多卡,不会被平均数的平滑效果骗过去。
第三件事是埋点追踪Agent的决策过程。每次Agent选择了哪条路由、每个重排分数是多少、自检为什么通过或不通过,都要记录。这么做的目的是让评估不只是看"最终答案对不对",而是能回溯到"是哪一步决策导致答案不对"。这个功能在后面对比不同路由策略时非常有用,比如我调整了某个检索阈值,可以通过历史日志看出它对后续所有步骤的连锁影响。
4.3 离线评估与回归测试:让优化不再靠感觉
生产级和Demo级的本质区别,在于有没有一整套评估机制。没有评估,你改了路由逻辑或检索参数,根本不知道是变好了还是变坏了。我在这套系统上花了很大精力做离线评估集,大概两千多条真实用户问题,每条都人工标注了预期检索结果和标准答案。每次改动上线前,先跑一遍评估集,对比改动前后的指标变化。
评估指标的选取也要谨慎。单看正确率不够,我还要看几个分项指标:检索命中率,就是标准答案对应的文档块有没有被召回;排序质量,就是重排后标准块排在什么位置;最终答案的完整度和忠实度,完整度关注用户问题里的关键条件有没有都被回答,忠实度关注答案有没有超出检索资料的内容。这四项分开计算,谁出了问题一目了然。
我踩过一个很典型的评估坑:一开始只看"答案和标准答案的相似度",结果模型开始用非常中庸的措辞来换取高分,回答是"说得对但都是废话"。后来加入了"必须引用资料片段ID"的硬约束,每个关键论点都要标记来源,评估时校验这些引用是否存在且相关,这套系统的"言之有据"水平一下就上来了。这个机制对生产环境意义重大,因为用户投诉最多的就是"AI在胡说八道"。
5. 常见问题与排查技巧实录
5.1 检索召回质量差:先从数据侧找原因,别急着调参数
我发现有相当多的人一看到检索结果不好就调top_k和相似度阈值,这其实是本末倒置。检索质量差的根源,一大半在数据侧,只有一小半在参数侧。
最常见的问题是文档切块不合理。比如一篇文章的某个结论分布在多个段落里,切块时被物理分隔开,检索自然召回不全。解决方法是切块前先做结构分析,把同一个逻辑单元的内容尽量放进一个块里,而不是死板地按字符数切。这个调整往往比换嵌入模型更见效。
第二个原因是检索时缺少元数据过滤。前面提到每个块都带文档ID和章节路径,但很多人构建索引时存了这些信息,查询时却忘了用。比如用户只看2024年的资料,那就应该在向量检索前先按年份过滤,让嵌入模型只需要在正确的子集里找相似,而不是在全部文档里大海捞针。加上过滤之后,召回准确率的提升肉眼可见。
第三个原因是重排器没有用对。向量检索的初排是"语义相似",重排器是"问答相关",这两者的判断逻辑完全不同。如果你的系统只做了初排没有重排,建议优先补重排器,而不是继续调top_k。我上线重排器之后,把top_k从8调到6,不仅回答质量变好,生成阶段的token消耗还降了不少,属于用更少的输入得到了更好的输出。
5.2 Agent陷入循环或路径爆炸:给Agent画一条边界线
Agent化之后,最常遇到的新问题就是循环。模型会反复调用同一个检索工具,得到的还是同样的结果,然后又决定再检索一次,白白消耗算力和时间。这个问题我第一次上线的时候几乎每周都碰到,后来做了三件事才算压住。
第一,给每个工具调用加上"重复检测"。同一个查询在同一个Agent会话里如果已经调用过同样的工具两次,第二次返回结果时直接附带标记,告诉Agent"你之前已经查过这个了,如果没有新条件,不要再重复调用"。这个检测在下层拦截,比在提示词里叮嘱模型要可靠得多。
第二,给整个Agent流程设置最大步数。我设置的默认值是8步,超过8步还没生成出最终答案,就强制走"总结当前已有信息并生成部分答案"的兜底分支,或者直接返回需要更多信息。这个限制必须写在硬代码里,不能只靠模型自觉。
第三,给单次检索增加"时间与范围衰减"机制。同一个会话里,如果Agent已经看过多轮检索结果,继续检索时系统会自动把上下文里的历史结果标记为"已知信息",并降低它们在新一轮路由中的权重。这能在流程层面抑制"反复看同一批资料"的行为。
5.3 延迟、成本与稳定性问题:核心瓶颈在模型调用次数
Agentic RAG的延迟和成本,本质上跟模型调用次数强相关。普通RAG平均一次生成调用,Agentic RAG的核心链路往往有三到五次模型调用,多轮场景会更多。延迟压不下来,先看调用次数是否合理。
我优化的第一个突破口是"能不用模型就不用模型"。在我的流程里,路由就是一段轻量规则代码,绝大多数情况下不需要调模型做意图分类。只有规则判断不了的时候,才升级到模型解析。这个设计让大概60%的请求只走了规则路由,一次模型调用都不增加。类似的思想也用在自检环节:先跑规则检查,比如数字是否对得上、引用片段ID是否存在于结果中,规则通过就直接放行,只有规则检查存疑的请求才进入模型二次校验。这一步直接把自检环节的平均耗时砍掉了一半还多。
第二个突破口是提示词的瘦身。一开始为了让Agent行为稳定,提示词写得很长,动辄两千多token。后来发现很多内容是冗余的,真正约束行为的核心指令只有几百token。提示词越长,首token延迟越高,成本也越高。精简提示词之后,端到端耗时平均降了10%,生成质量没有下降。
第三个突破口才是模型本身的提速。如果前面的环节都优化过了,延迟还差,再考虑换更快的模型或开流式输出。流式输出在生产环境里其实是双刃剑,它确实能改善首字等待体验,但如果你在自检通过前就把内容流给用户,又遇到自检不通过需要重答,体验反而更差。我的做法是只有通过自检的答案才开启流式返回,宁可让用户多等一秒,也不要让错误答案先发出去再撤回。
说到稳定性,还要提一下依赖服务的超时和重试策略。模型服务偶发超时在生产环境是常态,不能指望它永远不超时。我给每次调用设置了两档超时:快速超时和整体超时。快速超时如果发现模型服务响应异常,立刻切换到备用模型服务;整体超时则直接放弃本次调用,走降级分支。重试次数我不能设太多,最多两次,再多就是浪费资源了,因为Agentic链路里每次重试的成本比传统RAG高太多了。
我在实际运维中还发现一个很容易被忽视的坑:会话长度失控。Agentic RAG的多轮对话会把每轮的工具调用结果、中间思考过程都放进历史里,轮次一多,上下文越来越大,每次请求的模型输入成本也跟着水涨船高。我的做法是两个限制:会话轮次最多二十轮,超过就强制总结归档;每轮只保留最终的检索结果和答案摘要,中间的决策中间态全部丢弃。这个改动让长会话的成本曲线从线性增长变成了平台期爬升。
这套系统上线到现在,给我最深的体会是:Agentic RAG的难点根本不在"让模型变得聪明",而在"让流程足够克制"。模型本身的能力已经足够强,真正需要下功夫的是给它一个结构清晰的决策边界、一套可信的工具、一扇能看到运行状况的观测窗口,以及一个能随时回退重试的安全网。每一步看起来都不复杂,组合在一起才会产生质变。如果你正在做类似的东西,我建议从一条最小链路开始,先把"路由到检索到生成到自检"跑通,再逐个环节加深,而不是一上来就铺很大的摊子——等每块砖都历过生产环境的检验,再谈扩展性和全场景覆盖,会稳得多。