ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零搭建生产级RAG Agent:架构、检索、编排与优化

从零搭建生产级RAG Agent:架构、检索、编排与优化 你有没有遇到过这种情况给知识库问答系统丢进去一句“帮我对比一下这三份合同里关于违约金的条款差异”它直接开始一本正经地编内容或者说“根据我的理解……”——然后答非所问。我早期做的几个 RAG 项目基本都死在这个问题上不是向量检索不给力而是整个链路缺了一个“会思考”的角色。RAG 增强检索生成本身不难理解难的是把它做成能应对真实业务问题的生产级系统。普通 RAG 的典型流程是“查一次、拼一段、生成一版”遇到复杂问题基本就露馅。后来我把 Agent 的决策能力引进来做成了真正意义上的 RAG Agent本质上就是在检索前后加了一层“思考循环”让它能自己判断该查什么、查完够不够、要不要换个姿势再查一次。这篇文章我会把自己从零搭一个生产级 RAG Agent 的全过程拆开讲包括架构选型、多路召回、重排序、Agent 编排、记忆体系、评估优化以及我在实际项目里踩过的一堆坑。适合正在做 RAG 项目、想从 demo 级走向生产级或者被“Agent 开发”这个名词弄得一头雾水的开发者参考。没有太多高深理论全是能直接落地的经验。1. 需求拆解与架构设计先想清楚 Agent 该干什么1.1 RAG Agent 和朴素 RAG 的本质区别很多人会有一个误解觉得 RAG Agent 就是在原来的 RAG 流程前面加一个“智能路由”判断用户问题是不是跟知识库相关相关就检索、不相关就闲聊。实际上这远远不够。朴素 RAG 的流程图非常简单用户输入 → 向量化 → 向量检索 → 拼接上下文 → LLM 生成。这个流程跑通很简单但有几个致命问题第一它假设“一次检索”就能找到答案可现实中的问题往往需要多轮检索、多源对比第二它不判断检索结果够不够哪怕检索结果完全没命中它也硬着头皮生成第三它没有反思机制不会发现自己的答案可能有误。Agent 化的 RAG 把静态流程变成了动态循环。我在设计时参考了 ReAct 模式让系统拥有“思考-行动-观察”的闭环能力。举个例子用户问“我们公司去年第二季度的营收相比第一季度变化了多少”这里的难点是可能需要先从财务知识库里找到两份季报再找到对应的表格数据然后进行数值对比。这个流程在朴素 RAG 里没法完成因为“找季报”和“对比数据”是两个不同的检索意图但 Agent 可以把它们拆开执行甚至可以用工具去执行一个查询脚本然后又基于返回结果做下一步决策。说一个生活化的类比朴素 RAG 是银行柜台实习生你问什么他都在系统里查一次就回答查不到就打官腔RAG Agent 是资深客户经理第一次查询结果不够理想时他会换关键词再查会调取不同系统的数据交叉验证最后才给你一个完整、可溯源的答复。所以我在架构设计上的核心原则是把 “检索” 从一次操作变成 Agent 的“工具调用”RS 让它按需调用、按需循环而不是在 Prompt 里塞满上下文然后就完事。1.2 生产级 RAG Agent 的系统分层从整体架构上我把一个生产级 RAG Agent 拆成四个层次路由层Router负责理解用户意图决定走知识库检索、走对话闲聊、还是走工具调用。它是 Agent 的第一步决策。检索层Retrieval包含多路召回关键词召回、向量召回、结构化查询、重排序Rerank并把这些能力封装成可以被 Agent 调用的“工具”。生成层Generation基于检索结果和对话历史调用 LLM 生成回答。这里还要处理引用溯源、输出格式控制。记忆层Memory管理短期会话记忆和长期用户偏好记忆让 Agent 在多轮对话中保持上下文一致性。这四个层次组合起来有一点像人的“大脑-手-嘴巴-笔记本”。大脑路由层和 Agent 编排负责决策手检索层负责干活嘴巴生成层负责表达笔记本记忆层负责记录历史。架构上的关键点不是模块越多越好而是模块之间的接口要稳定。我定义检索层向编排层暴露的接口只有三个search(query, top_k)、multi_search(queries, top_k)、search_by_metadata(filter, top_k)。上层不需要关心这个 search 背后是向量检索还是关键词检索还是两者混合后续替换检索方案时上层代码一行都不用改。1.3 工具选型Agent 框架怎么挑选型是我觉得生产级和非生产级项目拉开差距的第一个地方。项目级 demo 用什么都无所谓生产级就要考虑稳定、可控、可观测。目前主流的 Agent 编排方案大概有这么几类方案灵活度学习成本生产成熟度适合场景LangGraph高较高较高复杂流程、精细控制、需要状态管理Dify / Coze 可视化平台低低中高快速搭建、业务同学参与、不想写太多代码自研编排状态机 工具调用最高看基础需自己磨有独特约束、需要深度定制Semantic Kernel中中中高微软生态、.NET 技术栈团队我自己的建议是如果你的团队已经有较强的 Python 工程能力业务流程复杂需要精细控制LangGraph 是比较稳的选择。如果你只是想快速验证或者老板希望业务同学也能参与维护Dify 这类平台可以大幅节省时间。但要注意一点平台化方案在灵活性上确实有天花板遇到特别复杂的工具调用链、自定义记忆策略时容易受平台限制。另外还有个问题值得注意Agent 框架和 RAG 框架不要混为一谈。RAG 框架负责的是“检索增强”这一件事Agent 框架负责的是“多步决策”这一件事。生产级系统中它们可以分开选型。比如检索部分你完全可以用纯开源的向量检索库如 FAISS、Milvus 自研混合检索逻辑编排部分再用 LangGraph。不要把整个系统绑定在某个全家桶上。2. 多路召回与混合检索先让系统“找得全”2.1 单路向量检索为什么不够用在我刚开始做 RAG 时用的就是最朴素的思路把文档切块 → 全部 embedding → 用户问题也 embedding → 算余弦相似度 → 取 top_k。这个流程在演示时效果还行一到真实业务就露馅问题主要集中在第一语义相似的“字面不同”。比如用户的文档里写的是“甲方逾期付款的违约责任”用户问的是“如果乙方拖了钱怎么办”语义上相关但向量检索可能命中“付款”“违约”相关的片段也可能被无关但语义接近的内容带走。第二精确匹配场景失灵。查合同号、订单号、设备型号、人名时向量检索的召回结果不一定包含那个精确的词。比如用户文档里是“合同编号 HT-2024-001”用户问“HT-2024-001 的交付日期是哪天”切块时可能把这个编号切得七零八碎embedding 之后精确匹配能力很弱。第三短语式查询的低效。向量检索对短语、称谓、实体类查询的召回质量波动很大。比如查“张三”如果文档里张三出现频率不高向量检索可能完全没召回到。所以生产级 RAG 的第一条准则就是不要把所有希望押在向量检索上。混合检索是底线不是可选项。2.2 三路召回的设计与实现我在项目里把召回设计成了三路不搞花里胡哨但每一路都解决一个特定类型的问题第一路BM25 关键词召回。这一路解决“精确匹配”问题。BM25 是经典的关键词检索算法对词频敏感特别适合人名、编号、专有名词这类查询。实际用的时候我用的是 Elasticsearch 或者轻量级的 rank_bm25 库。这里有一个细节要注意中文场景下必须处理好分词。比如“HT-2024-001”这种内容一个不当的分词器会把它拆得乱七八糟。我在项目里会先做一个前缀/后缀判断问句里存在编码格式的内容时直接走精确匹配分支不走普通关键词分支。第二路向量语义召回。用 embedding 模型把问题和文档都向量化然后做相似度检索。这一路解决“意图相同但表达不同”的问题。embedding 模型的选择有讲究中文场景下我更倾向于用专门优化过的中文向量模型比如 BGE 系列或者 M3E 系列而不是直接用 OpenAI 的 text-embedding-ada-002 之类的通用模型来跑中文。实测下来中文业务文档场景下BGE 的召回质量会比通用模型高不少。第三路结构化元数据召回。生产级系统里文档不可能是一锅粥它有分类、标签、所属部门、日期范围、文档类型等元数据。当用户问题里带有明确的限定条件时“去年”“安全相关的文档”“合同类”我直接把问题里的实体做成结构化过滤条件用元数据去筛选。这一路负责“缩小范围”防止前两路召回跑到不相关的文档堆里。这三路召回的结果最后要做融合。我在项目里用的是 RRFReciprocal Rank Fusion方案简单说就是给每路召回的命中结果按排名赋分然后取加权总和排序。RRF 的好处是不需要调一堆权重且对每路检索结果都做了归一化比较稳。2.3 文档切块的参数经验召回效果的好坏有一半取决于上游的文档切块。我最初用的是固定长度切块比如每 512 个字符一块chunk_overlap 设 50结果很糟糕。为什么因为固定长度切块会把语义完整的段落拦腰截断也可能把一个章节拆进好几个不相关的块里。后来我调整成两种切块策略的组合基于结构切块优先按 Markdown 标题结构#、##、###切保留层级信息。适用于有明确结构的文档比如操作手册、产品说明。父子块切块索引时切小一点的“子块”比如 380 token用于检索生成时返回“父块”比如 1200 token用于给 LLM 更完整的上下文。这个“检索用小块、生成用大块”的思路在实测中明显提升了答案的完整性因为小块检索到的片段可能只是某个答案的边角料但父块能补全上下文。有次做维修手册的知识库时用户问“齿轮箱漏油可能的原因”子块命中“油封老化”父块则包含整个故障排查章节生成质量完全是两个档次。关于参数我给一个经验值范围仅供参考子块 380-512 token父块 1000-1500 tokenoverlap 50-80 token。但切块参数一定要结合你实际文档的平均段落长度来调先统计文档的字数分布、段落长度分布再定切块参数不要盲目抄。2.4 重排序Rerank不能省多路召回之后候选集往往有几十条甚至上百条但最终给 LLM 的上下文是有限的。按什么标准筛选向量相似度排序不是最优标准因为 embedding 模型的语义匹配能力有限有时候看 embedding 得分高实际跟问题八竿子打不着。我的方案是在召回后加一个 BGE-reranker 这类交叉编码器模型做重排序。这个模型的原理是把问题和候选文本拼在一起作为输入计算出真正的语义相关性分数。它比向量检索用的双塔模型更精准但速度慢、需要额外算力所以必须放在召回之后用缩小到 top 20-30 条再做重排。重排序后的 top_k 一般取 3-5 条。这个数字不能太大因为 LLM 上下文窗口虽然越来越大但塞太多碎片化的片段反而会干扰生成让回答变得又臭又长也不能太小否则答案信息量不足。我一般会通过评估来决定但 5 条左右是个比较平衡的经验值。3. Agent 编排与工具调用把检索能力变成 Agent 的“手”3.1 ReAct 循环从“查一次”到“查多次、查得准”我这一版 RAG Agent 的核心就是 ReAct 循环。这个模式借鉴了论文 《ReAct: Synergizing Reasoning and Acting in Language Models》 的思想让模型在每一步推理时同时生成“思考Thought”和“行动Action”行动返回结果后模型再根据“观察Observation”继续推理直到得出最终答案。在实际实现中我用 LangGraph 搭建了一个精简版 ReAct 循环核心状态转移Start接收用户问题 → 路由意图Router判断是否需要检索。如果只需要闲聊直接走普通对话分支Retriever-Tool调用多路召回重排工具得到检索结果Reflect评估检索结果是否满足问题需求。如果结果相关且完整进入生成如果不相关或缺失重新改写查询、换一路召回、或进行多跳检索Generate基于检索结果和对话历史生成答案这里我要特别强调一下 Reflect 这一步。很多初版实现根本没有这一步Agent 查了一次就直接生成。但生产场景里一次检索就能答好的问题本来就是少数。我在实际项目里遇到过一个典型场景用户问“请解释一下我们公司云成本为什么上个月突然增加了”第一次检索不知道具体月份上下文召回了一堆不相关内容如果没有 Reflect 机制系统就会把不相关内容跟用户公司历史拼接出一个错误解释。加了 Reflect 之后Agent 会判断“检索结果中没有找到成本增加的具体原因”然后重新查询“云成本 分析 上个月 异常”等相关词汇最终才能定位到资源使用率异常的报告。我实现的反思机制其实不复杂就是让 LLM 基于检索结果打个分再判断是否继续检索类似下面这个核心循环def agent_retrieve_loop(question, max_steps4): context [] query question for step in range(max_steps): results hybrid_search(query, top_k5) context.append(results) # 判断当前检索结果是否足以回答 enough evaluate_sufficiency(question, context) if enough: break # 否则改写查询重新检索 query rewrite_query(question, context, step) return generate_answer(question, context)3.2 工具注册与 Router 设计Agent 之所以是 Agent关键在于它能调用工具。我在系统里把“检索能力”拆成了多个工具而不仅仅是一个 search 工具。search_keyword(query, filters)基于 BM25 关键词召回search_semantic(query, top_k)基于向量语义召回search_hybrid(query, filters, top_k)三路召回 RRF 融合 Rerank 重排search_by_metadata(metadata, top_k)基于结构化条件筛选lookup_product_info(product_id)调用业务接口获取实时数据比如订单状态Router 的作用是判断用户问题该调哪些工具。设计上可以用简单的意图分类模型也可以让 LLM 直接完成函数调用Function Calling。我的经验是如果工具数量少于 5 个用 LLM Function Calling 绰绰有余如果工具很多、参数复杂建议做一个独立的意图分类器先分好大类再在各工具内部做参数抽取。顺带回答一个经常被问到的问题“harness 和 agent 的区别是什么”。严格说有些框架会用 Harness 描述“Agent 运行的容器或编排器”agent 本身是“决策大脑”harness 则是“血管和骨架”负责环境交互、工具注入、状态维护。这个概念在 LangGraph 体现为图的边和状态在自研系统里体现为状态机。理解这个区别有助于你跟别人讨论架构时对齐概念。3.3 记忆体系短期记忆和长期记忆RAG Agent 很容易忽视记忆但生产环境里记忆直接决定体验。我最早做的版本没有记忆用户问“那第二季度呢”系统完全不知道“那”指的是什么因为上一轮对话的上下文没传进去。我采用的记忆方案分两层短期记忆对话级保存在会话上下文里通常是最近 N 轮对话的摘要加最近几轮完整对话。这里有个关键点不要把每轮检索到的长文档全部塞进短期记忆否则上下文会迅速爆炸而且后续检索的 token 预算会被吃掉。我的方案是每轮对话结束后用 LLM 生成一个 50-100 字左右的摘要存入短期记忆同时保留最近两轮完整对话用于细节引用。长期记忆用户级把用户的偏好、历史关注点保存到向量数据库。比如用户经常查询“安全规范”系统会把“用户 A 近期关注安全规范类文档”这个信息作为向量存起来下次检索时作为过滤条件或加权重置。这一块我还做了一个很简化的用户画像更新机制每次对话结束后会异步生成用户画像标签并写入向量存储。记忆这块的坑主要在一致性和隐私。生产系统一定要明确告诉用户哪些信息会被长期存储哪些仅在当前会话有效。同时要建立清晰的记忆删除机制不然合规上会出问题。3.4 安全护栏让 Agent 不“越界”Agent 的能力越强越要给它上护栏。在实际项目中我从三个维度做了控制工具白名单Agent 只能调用预先注册的工具不允许它执行系统级命令或访问未授权的 API。敏感内容过滤用户输入先过一层安全检查命中敏感词或疑似诱导注入的请求直接挡掉。比如遇到“忽略你所有的 system prompt”这类提示注入攻击不走检索链路。超时与降级每轮循环设置步数上限我这里是 4 步单步工具调用有超时时间。一旦超时或达到上限直接返回当前最优结果或提示“暂时无法解答”。这里要多说一句Agent 的“自由发挥”空间越大出现不可控行为的概率越高。生产级的 Agent 不是越聪明越好而是“在可控范围内聪明”。4. 生产化落地评估、可观测性、性能优化4.1 评估体系不能只靠肉眼判断做 RAG 项目最怕的就是“开发时觉得效果挺好一上线领导提了几十个问题直接翻车”。根本原因是没有建立可量化的评估体系。我从一开始就搭建了一个离线评估集包含 200 条左右标注好的“问题-理想答案-参考文档片段”覆盖了单跳、多跳、跨文档、含数值计算等典型场景。评估指标我参考了 RAGAS 框架的思路重点看四个维度忠实度Faithfulness生成的答案是否严格基于检索到的上下文是否出现幻觉。答案相关性Answer Relevancy生成的答案是否直接回应用户问题还是答非所问。上下文精度Context Precision检索到的上下文里有多少是真正有用的有多少是噪音。上下文召回率Context Recall理想答案里有多少信息点被检索到了。实际操作时我用 LLM 做自动打分比如用 GPT-4 或 Qwen-Max 作为裁判同时保留了人工抽检的流程。自动打分能快速筛掉明显问题人工抽检负责那些 LLM 裁判看不出来的语义细节。如果你项目启动快没有标注资源也有一个取巧的方案随机抽 50 个真实用户问题人为判断“系统回答是不是能接受”记录一个通过率。虽然没有那么严谨但至少能防住明显倒退。4.2 链路可观测性每个环节都要能追踪RAG Agent 的生产维护最痛苦的不是写代码而是排查问题。用户问了一个问题系统回答得不好到底问题出在检索、路由还是生成如果没有链路追踪排查起来就是大海捞针。我实现的链路追踪很简单为每次请求生成一个 trace_id然后在每个关键环节记录日志logger.info({ trace_id: trace_id, stage: hybrid_search, query: query, bm25_hits: [doc_ids], vector_hits: [doc_ids], rerank_hits: [doc_ids], elapsed_ms: 123 })然后做一个简易的查询面板输入 trace_id 就能看到这条请求的完整链路包括每一步的耗时、召回结果、重排分数、最终生成的答案。这样用户投诉之后我可以快速定位到具体是哪一步出了问题。有一次线上用户反馈“系统答非所问”我一看 trace 记录发现是路由层把“合同里关于违约金的条款”错误分类到了“发票咨询”一路检索错了方向。问题定位不超过 10 分钟这就是可观测性的价值。4.3 性能优化延迟和成本要一起算生产级意味着延迟不能失控。我实践后的性能优化思路主要有四个方向缓存对高频、相似的问题做语义级缓存。用户问“合同有效期几年”和“合同的期限是多久”虽然没有字面完全一致但 embedding 接近直接命中缓存返回。这样能大幅降低 LLM 调用成本同时把响应时间从秒级降到毫秒级。并发与异步多路召回的三路检索可以并行执行不要串行等待。我用了 asyncio 并发调用 BM25、向量检索、元数据检索平均能省掉 60% 的检索时间。embedding 预热向量检索服务在冷启动时首次请求很慢我在服务启动时提前加载模型并做一次 dummy embedding把冷启动时间降下来。流式输出LLM 生成阶段启用流式输出让用户先看到一部分回答而不是等 3-5 秒黑屏再一次性出现。这是体感优化最明显的一招。4.4 降级策略系统再稳也要有后路生产系统不可能永远不挂。检索服务挂了、LLM 服务限流了、向量数据库超时了都要有对应的降级逻辑。我的做法是检索服务挂了 → 降级为纯关键词 BM25 搜索虽然效果差一点至少能用多路召回里某一路超时 → 放弃这一路用剩下的路召回不阻塞整个请求LLM 超时 → 返回“系统繁忙请稍后再试”并提供检索命中的文档列表作为替代信息整个链路异常 → 有一个静态兜底答案页面不暴露任何系统内幕这套降级策略的价值在于线上故障发生时不会产生用户完全不可用、或者返回莫名其妙内容的状况。生产级不是追求“永远不出错”而是“出错后可接受”。5. 常见问题与排查技巧实录现象可能原因排查思路解决方案答案与知识库内容不符检索未召回相关文档看 trace 日志确认检索命中的 doc_id 是否相关调整切块/召回策略加 Rerank多轮对话丢上下文短期记忆未正确存储检查会话上下文摘要存储逻辑增加对话摘要保留最近完整轮次检索结果很多但答案质量差上下文塞太多噪音片段检查 top_k 和重排结果降低 top_k提高重排后筛选门槛响应时间超过 5 秒LLM 生成慢或检索链路串行用 trace 日志看耗时分布并行化检索启用流式输出工具调用循环不终止ReAct 循环缺少步数上限确认 max_steps 配置硬性限制循环次数超限返回当前结果某个领域问题总答错领域文档专业性过强切块不合理抽查该领域的检索命中片段按文档结构重新设计切块策略排查 RAG Agent 问题时我强烈建议你始终遵循“从下往上、先从检索入手”的原则。90% 的答案质量问题根源都在检索而不是生成。先确认检索命中的文档是不是对的再去看生成。如果你一上来就调 Prompt往往事倍功半。再补一句切块参数和重排模型调优后一定要跑一遍离线评估集对比前后指标。我踩过一次“感觉效果变好了结果离线评估降了 8 分”的坑从那以后我对任何无评估的优化都心存敬畏。最后分享一个我在多个项目里反复验证的结论生产级 RAG Agent 的建设难度不在模型选择也不在框架选型而在你对链路的掌控能力。能清楚说出每一次回答背后的检索轨迹和决策依据这个系统才真正达到生产级。我的建议是从最朴素的单路 RAG 起步先把链路跑通再加上混合检索、Rerank、Agent 化编排每一步都做评估、留观测系统的进化会比你想象中扎实得多。
返回列表