RAG与Agent合体上线:从原型到生产的工程化实战指南

📅 发布时间:2026/10/16 8:10:35
RAG与Agent合体上线:从原型到生产的工程化实战指南
1. 原型跑通不等于能上线问题到底出在哪RAG 和 Agent 这两个词过去一年在各种技术社区里几乎被聊烂了。单独拿 RAG 来说用向量库加一个大模型把文档切片、做嵌入、检索、拼 prompt半天就能跑出一个能回答问题的 Demo。单独拿 Agent 来说定义几个工具函数写一个 ReAct 循环让模型自己决定调哪个工具也能很快看到效果。分开写的时候每个模块都挺听话测试用例也都能过。但一旦把两者合到一起准备上线事情就开始不对劲了。我见过太多团队卡在这个阶段本地跑得好好的一上测试环境就开始超时并发稍微高一点检索结果就开始串Agent 调了几轮工具之后回答质量断崖式下跌更离谱的是同样的输入两次请求返回的结果完全不一样排查起来毫无头绪。这个问题的本质不是 RAG 不行也不是 Agent 不行而是两个独立系统在工程层面耦合时暴露出了原型阶段被掩盖的所有问题。原型阶段我们关注的是“能不能跑通”而生产环境关注的是“能不能稳定、可控、可观测地跑通”。这两者之间的差距远比大多数人想象的要大。这篇文章面向的是已经写过 RAG 或 Agent 原型、正准备往生产环境推进的开发者。我会从架构设计、核心细节、实操落地、问题排查四个维度把从原型到生产这条路上踩过的坑和对应的工程化方案完整拆一遍。不管你是刚跑通第一个 Demo还是已经在做上线前的压测应该都能从中找到对自己有用的东西。2. 合体上线的核心矛盾与架构思路拆解2.1 为什么分开写容易合起来就崩先说清楚一个基本事实RAG 和 Agent 分开写的时候它们各自运行在一个相对封闭、边界清晰的环境里。RAG 的输入是用户问题输出是一段拼接好的上下文和回答Agent 的输入是一个目标输出是一系列工具调用和最终结果。两者的状态管理、错误处理、超时控制都是独立的。合体之后情况变了。Agent 需要在推理过程中动态决定是否调用检索工具检索回来的内容又要作为下一轮推理的输入推理结果可能触发新的工具调用新的工具调用又可能再次触发检索。这个循环一旦形成状态空间就爆炸了。我画一个简单的对比你就明白了。单独 RAG 的调用链是线性的问题 → 检索 → 生成 → 返回。单独 Agent 的调用链是循环的但循环的每一步都是确定的工具调用。合体之后调用链变成了嵌套循环外层是 Agent 的推理循环内层是 RAG 的检索生成循环而且内层的输出会影响外层的决策外层的决策又会改变内层的检索策略。这种嵌套结构带来的直接后果有三个。第一延迟不可控。一次用户请求可能触发多轮检索和多轮工具调用每轮都有网络开销和模型推理开销累加起来很容易超过前端等待阈值。第二错误会级联放大。检索返回了一条不相关的文档Agent 基于这条文档做了一个错误决策错误决策又触发了一个不该调用的工具最终结果完全跑偏。第三状态难以追踪。出了问题之后你很难快速定位是哪一轮检索、哪一次工具调用、哪一个 prompt 拼接环节出了错。2.2 合体架构的三种常见模式与选型逻辑在实际工程中RAG 和 Agent 的合体方式大致有三种模式每种模式的复杂度和适用场景不同。第一种是检索前置模式。用户问题先经过 RAG 检索把检索结果作为上下文注入 Agent 的初始 prompt然后 Agent 在这个上下文基础上进行推理和工具调用。这种模式的好处是检索只发生一次延迟可控状态简单。缺点是 Agent 在推理过程中无法动态调整检索策略如果初始检索结果不理想后续推理就会一直受限于这个不好的上下文。第二种是检索即工具模式。把 RAG 封装成一个工具函数Agent 在推理过程中可以随时调用。这种模式灵活度最高Agent 可以根据当前推理状态决定什么时候检索、检索什么。但问题也很明显Agent 可能频繁调用检索工具导致延迟飙升也可能该调用的时候不调用完全依赖自身知识胡编。第三种是混合模式。初始阶段做一次前置检索保证 Agent 有一个基本的上下文底座同时在 Agent 的工具集中保留检索工具允许它在必要时进行补充检索。这种模式在灵活性和可控性之间取了一个平衡也是我在实际项目中用得最多的方案。选型的核心判断依据是你的业务场景对延迟和准确率的容忍度。如果是客服问答这类对延迟敏感、问题相对固定的场景检索前置模式就够了。如果是复杂的研究分析类任务需要多轮信息收集和推理那检索即工具模式或者混合模式更合适。但无论选哪种都需要在工程层面做好超时控制、重试策略和降级方案。2.3 生产环境对系统的四个硬性要求原型阶段我们只关心功能生产环境必须同时满足四个要求缺一不可。可观测性是第一条。每一次请求的完整调用链包括检索了哪些文档、Agent 做了哪些决策、调用了哪些工具、每步耗时多少都必须有日志记录。没有可观测性出了问题就是盲人摸象。确定性是第二条。同样的输入在同样的上下文条件下系统应该给出稳定可预期的输出。大模型本身有随机性但工程层面可以通过固定随机种子、缓存检索结果、限制工具调用轮数等手段来提高确定性。容错性是第三条。检索服务挂了怎么办模型接口超时怎么办工具调用返回了异常格式怎么办这些在原型阶段可以不管在生产环境必须每一个都有对应的处理策略。成本可控是第四条。Agent 多轮推理加上多轮检索token 消耗可能是单次 RAG 的十倍甚至几十倍。如果不做成本控制账单会教你做人。3. 核心细节解析与实操要点3.1 检索层的工程化改造从“能搜到”到“搜得稳”原型阶段的检索通常很简单把文档切片做嵌入存向量库查询时取 top-k。但生产环境里检索层需要做大量工程化改造。切片策略是第一道关。固定长度切片是最省事的做法但效果往往不好。我一般会采用语义切片加滑动窗口的方式先按段落或标题做语义分割保证每个切片在语义上相对完整然后在切片之间保留一定的重叠区域避免关键信息被切断。切片长度控制在 300 到 500 个 token 之间这个范围在检索精度和上下文完整性之间比较平衡。多路召回是第二道关。单一向量检索在某些场景下召回率不够我会同时使用向量检索和关键词检索然后把两路结果做融合排序。向量检索擅长语义匹配关键词检索擅长精确匹配两者互补。融合排序可以用简单的加权分数也可以用更复杂的重排序模型。检索结果过滤是第三道关。不是所有检索回来的内容都值得塞进 prompt。我会设置一个相关性阈值低于阈值的直接丢弃同时做去重处理避免相似内容重复占用上下文窗口。如果检索结果为空或者全部低于阈值系统应该有一个明确的降级策略比如返回“暂时无法回答该问题”而不是硬编。# 检索层核心参数配置示例 RETRIEVAL_CONFIG { chunk_size: 400, # 切片长度token chunk_overlap: 80, # 切片重叠长度 top_k_vector: 10, # 向量检索召回数量 top_k_keyword: 10, # 关键词检索召回数量 final_top_k: 5, # 最终注入 prompt 的数量 relevance_threshold: 0.65, # 相关性阈值 rerank_model: cross-encoder # 重排序模型类型 }注意相关性阈值的设定需要根据你的嵌入模型和业务数据做校准不能直接照搬。建议先用一批标注数据跑一遍看分数分布再定阈值。3.2 Agent 决策循环的边界控制Agent 的决策循环是合体系统中最容易失控的部分。原型阶段我们通常只限制一个最大轮数比如 10 轮但生产环境需要更精细的控制。轮数限制要分层次。我会设置三个级别的限制单次请求的总轮数上限、连续检索调用的次数上限、连续同一工具调用的次数上限。总轮数上限防止无限循环连续检索上限防止 Agent 陷入“检索-不满意-再检索”的死循环连续同一工具上限防止某个工具被反复调用。超时控制要分阶段。整个请求有一个总超时时间比如 30 秒每一轮推理有一个单轮超时比如 8 秒每一次工具调用有一个调用超时比如 5 秒。任何一级超时触发都要有对应的处理逻辑单轮超时可以重试一次工具调用超时可以跳过该工具继续推理总超时则直接返回当前已得到的最佳结果。决策收敛要有引导。在 Agent 的 prompt 里我会明确告诉它如果已经收集到足够的信息应该尽快给出最终答案而不是继续调用工具。同时在每一轮推理后我会检查当前已收集的信息是否足以回答用户问题如果足够就主动终止循环不再等待 Agent 自己决定。# Agent 循环控制参数 AGENT_CONFIG { max_total_turns: 8, # 总轮数上限 max_retrieval_calls: 3, # 连续检索调用上限 max_same_tool_calls: 2, # 同一工具连续调用上限 total_timeout: 30, # 总超时秒 per_turn_timeout: 8, # 单轮超时秒 tool_call_timeout: 5, # 工具调用超时秒 enable_early_stop: True # 启用提前终止 }3.3 上下文窗口的动态管理合体系统里上下文窗口是最稀缺的资源。Agent 的推理历史、检索回来的文档、工具调用的结果全都要塞进上下文窗口。如果不做管理很快就会超出模型的最大 token 限制。我的做法是分层管理加动态裁剪。把上下文分成四个区域系统指令区、检索文档区、推理历史区、当前轮输入区。系统指令区固定不变占用最少检索文档区按相关性排序优先保留高分文档推理历史区只保留最近几轮的关键信息更早的历史做摘要压缩当前轮输入区保证完整。当总 token 数接近模型上限时裁剪顺序是先压缩推理历史再减少检索文档数量最后才动系统指令。系统指令是 Agent 行为的基石任何时候都不应该被裁剪。实操心得我习惯在每轮推理后计算一次当前上下文的 token 占用如果超过阈值就触发裁剪。这个计算可以用 tiktoken 之类的库来做开销很小但能避免很多“莫名其妙”的失败。3.4 工具调用的幂等性与错误隔离Agent 调用的工具可能是查询数据库、调用外部 API、执行计算任务等等。这些工具在生产环境里必须满足两个要求幂等性和错误隔离。幂等性意味着同一个工具用同样的参数调用多次结果应该一致。这对于重试机制至关重要。如果工具不幂等重试就可能导致数据重复写入或者其他副作用。对于不幂等的工具我会在调用层做去重记录本次请求中已经调用过的工具和参数组合如果重复出现直接返回缓存结果。错误隔离意味着一个工具调用失败不应该导致整个请求崩溃。每个工具调用都应该被 try-catch 包裹失败时返回一个结构化的错误信息给 Agent让 Agent 决定是重试、换工具还是放弃。同时工具调用失败要记录日志方便后续排查。# 工具调用封装示例 def safe_tool_call(tool_name, params, cache, timeout5): cache_key f{tool_name}:{hash(str(params))} if cache_key in cache: return cache[cache_key] try: result execute_with_timeout(tool_name, params, timeout) cache[cache_key] result return {status: success, data: result} except TimeoutError: return {status: timeout, message: 工具调用超时} except Exception as e: return {status: error, message: str(e)}4. 实操过程与核心环节实现4.1 环境准备与依赖选型在动手搭建之前先把技术栈定下来。我的选型原则是成熟优先、可替换、可观测。向量库方面原型阶段用内存版或者轻量级本地库就够了生产环境建议用支持持久化和水平扩展的方案。嵌入模型可以选择 API 调用或者本地部署API 调用省事但有网络延迟和成本本地部署可控但需要 GPU 资源。大模型方面推理能力强的模型适合做 Agent 的决策核心响应快的模型适合做检索结果的初步筛选和摘要。组件原型阶段选择生产环境建议选型理由向量库内存向量索引支持持久化的向量数据库数据不丢失支持并发查询嵌入模型API 调用本地部署或 API 加缓存降低延迟和成本大模型单一模型大小模型分工决策用大模型筛选用小模型缓存无Redis 或内存缓存减少重复检索和重复推理日志print结构化日志系统支持调用链追踪依赖安装这块我建议用虚拟环境隔离把 RAG 相关依赖和 Agent 相关依赖分开管理避免版本冲突。特别是嵌入模型库和 Agent 框架可能对同一个底层库有不同版本要求提前隔离能省很多事。4.2 检索服务的搭建与参数调优检索服务的搭建分四步文档加载、切片、嵌入、入库。文档加载阶段要注意格式兼容性。PDF、Word、Markdown、HTML 的解析方式不同我一般会统一转成纯文本再处理。解析过程中要保留标题层级和段落结构这些信息对后续切片很有帮助。切片阶段的核心参数是切片长度和重叠长度。我的经验值是技术文档切片长度 400 token 左右重叠 80 token对话记录切片长度 200 token 左右重叠 40 token。切片太长会导致检索精度下降太短会导致上下文不完整。嵌入阶段要注意批量处理和错误重试。大批量文档嵌入时分批发送每批 100 条左右失败的重试两次仍然失败就记录到失败列表不影响其他文档入库。入库之后用一批测试问题做检索效果评估。我会看三个指标召回率相关文档是否被检索到、精确率检索结果中相关文档的比例、排序质量最相关的文档是否排在前面。根据评估结果调整切片参数和检索参数。# 检索效果评估示例 def evaluate_retrieval(test_queries, ground_truth, retriever): metrics {recall: [], precision: [], mrr: []} for query, relevant_ids in zip(test_queries, ground_truth): results retriever.search(query, top_k10) retrieved_ids [r.id for r in results] # 召回率 recall len(set(retrieved_ids) set(relevant_ids)) / len(relevant_ids) metrics[recall].append(recall) # 精确率 precision len(set(retrieved_ids) set(relevant_ids)) / len(retrieved_ids) metrics[precision].append(precision) # MRR平均倒数排名 for i, rid in enumerate(retrieved_ids): if rid in relevant_ids: metrics[mrr].append(1 / (i 1)) break else: metrics[mrr].append(0) return {k: sum(v) / len(v) for k, v in metrics.items()}4.3 Agent 推理循环的完整实现Agent 推理循环的实现我拆成六个步骤。第一步初始化上下文。把系统指令、用户问题、前置检索结果如果采用混合模式组装成初始 prompt。第二步调用模型推理。把当前上下文发给大模型获取模型的输出。输出可能是最终答案也可能是工具调用请求。第三步解析模型输出。如果是最终答案进入第七步如果是工具调用请求提取工具名称和参数。第四步执行工具调用。根据工具名称路由到对应的工具函数带上超时和错误处理。第五步处理工具结果。把工具返回的结果格式化后追加到上下文同时更新调用计数和缓存。第六步检查循环条件。如果达到轮数上限、超时、或者检测到可以提前终止就强制模型给出最终答案否则回到第二步。第七步返回最终结果。把最终答案、调用链日志、耗时统计一起返回。# Agent 推理循环核心逻辑 def agent_loop(user_query, tools, config): context build_initial_context(user_query) call_history [] for turn in range(config[max_total_turns]): # 检查总超时 if time.time() - start_time config[total_timeout]: break # 模型推理 response call_llm(context, timeoutconfig[per_turn_timeout]) # 解析输出 parsed parse_response(response) if parsed[type] final_answer: return build_result(parsed[content], call_history) # 工具调用 tool_name parsed[tool_name] tool_params parsed[tool_params] tool_result safe_tool_call(tool_name, tool_params, cache) # 更新上下文 context append_tool_result(context, tool_name, tool_result) call_history.append({turn: turn, tool: tool_name, result: tool_result}) # 检查提前终止条件 if should_stop_early(context, call_history, config): context append_stop_instruction(context) # 达到上限强制生成最终答案 final call_llm(context \n请基于以上信息给出最终答案。) return build_result(final, call_history)4.4 缓存策略与性能优化缓存是合体系统性能优化的关键手段。我在三个层面做缓存。检索结果缓存同样的查询在短时间内重复出现时直接返回缓存的检索结果。缓存键是查询文本的哈希过期时间设置 5 到 10 分钟。这个缓存能大幅减少向量库的查询压力。嵌入缓存文档切片和查询文本的嵌入向量缓存起来避免重复计算。嵌入计算是 CPU 或 GPU 密集型操作缓存命中率通常很高。模型推理缓存对于完全相同的上下文缓存模型的输出。这个缓存命中率相对较低但在一些高频重复场景下很有用。需要注意的是如果模型输出有随机性缓存时要固定随机种子。# 多级缓存配置 CACHE_CONFIG { retrieval_cache: {ttl: 600, max_size: 10000}, embedding_cache: {ttl: 3600, max_size: 50000}, llm_cache: {ttl: 300, max_size: 5000} }实操心得缓存键的生成要小心。查询文本直接哈希可能因为标点或空格差异导致缓存不命中我一般会先做标准化处理去多余空格、统一标点再哈希。5. 常见问题与排查技巧实录5.1 超时问题的分层排查方法超时是合体系统上线后最常见的问题。排查超时要分层进行不能笼统地看总耗时。第一层检索耗时。如果检索耗时超过 2 秒说明向量库查询慢或者嵌入计算慢。检查向量库索引是否合理、嵌入模型是否在 CPU 上跑、是否有大量并发查询。第二层模型推理耗时。如果单轮推理超过 5 秒说明模型响应慢或者上下文太长。检查模型服务负载、上下文 token 数是否接近上限、是否有不必要的长文本输入。第三层工具调用耗时。如果某个工具调用超过 3 秒检查该工具依赖的外部服务是否正常、网络是否有抖动、是否有重试导致的额外延迟。第四层循环轮数。如果总轮数接近上限说明 Agent 决策效率低。检查 prompt 是否清晰、工具描述是否准确、是否有工具反复调用但结果不理想的情况。问题现象可能原因排查方法解决方向总耗时超 30 秒循环轮数过多查看调用链日志收紧轮数上限单轮推理超 8 秒上下文过长统计 token 数启用上下文裁剪检索超 2 秒向量库负载高查看向量库监控加缓存或扩容工具调用超 5 秒外部服务慢单独测试工具加超时和降级5.2 检索结果不相关的排查思路检索结果不相关Agent 基于不相关的上下文推理最终答案自然好不了。排查这个问题我一般按以下顺序来。先看查询改写。用户原始问题可能很短或者很口语化直接拿去做向量检索效果不好。我会在检索前加一步查询改写用模型把用户问题改写成更适合检索的形式。这一步能显著提升召回质量。再看切片质量。如果切片本身语义不完整检索再准也没用。随机抽一批切片出来人工检查看是否包含完整的信息单元。然后看嵌入模型。不同的嵌入模型在不同领域的数据上表现差异很大。如果通用嵌入模型在你的专业领域数据上效果不好考虑换一个在该领域微调过的模型或者用领域数据做一次微调。最后看重排序。如果召回阶段没问题但排序不好加一个重排序模型。重排序模型对 top-20 的结果做精细打分把最相关的排到前面效果通常比单纯依赖向量相似度好很多。5.3 Agent 决策跑偏的典型场景与修复Agent 决策跑偏有几个典型场景我逐个说。场景一该检索时不检索。Agent 觉得自己知道答案直接编了一个。修复方法是在系统指令里明确要求涉及事实性问题必须先检索检索不到才能说不知道。场景二反复检索同一内容。Agent 对检索结果不满意反复用相似的查询去检索。修复方法是设置连续检索次数上限同时在 prompt 里告诉它如果连续两次检索结果相似应该基于现有信息给出答案。场景三工具参数错误。Agent 调用了正确的工具但参数格式不对。修复方法是把工具的参数 schema 写清楚在 prompt 里给出参数示例同时在工具调用层做参数校验和自动修正。场景四忽略工具返回的错误。工具返回了错误信息Agent 当作正常结果继续推理。修复方法是在工具返回的错误信息里加上明确的标记比如[ERROR]并在 prompt 里告诉 Agent 遇到错误标记时应该怎么处理。5.4 成本失控的预警与优化成本失控通常来得悄无声息。等发现的时候账单已经翻了好几倍。我一般会设置三个预警线。单请求 token 消耗预警如果单个请求的 token 消耗超过阈值比如 10000 token记录日志并告警。这说明上下文管理可能出了问题。日 token 消耗预警设置日消耗上限达到 80% 时告警达到 100% 时触发限流。单用户频率预警如果某个用户在短时间内发起大量请求触发频率限制。这可能是恶意使用也可能是程序 bug 导致的循环调用。优化方向主要是三个减少不必要的检索调用、压缩上下文长度、用小模型做初步筛选。特别是第三点用一个小模型先判断检索结果是否相关只把相关的传给大模型能省不少 token。5.5 上线前的检查清单上线前我会过一遍这个清单每一项都确认无误才发布。检索层切片参数已校准、相关性阈值已评估、缓存已启用、降级策略已配置Agent 层轮数上限已设置、超时已配置、工具幂等性已验证、错误处理已覆盖上下文管理token 计数已接入、裁剪策略已测试、系统指令不可裁剪可观测性调用链日志已接入、关键指标已监控、告警规则已配置成本控制token 消耗已监控、限流已配置、缓存命中率已统计压测并发 10 请求下无超时、无错误、结果稳定注意压测一定要用真实数据不要用构造的简单问题。真实数据的分布和复杂度才能暴露真正的问题。6. 从原型到生产的经验沉淀6.1 我踩过的三个印象最深的坑第一个坑是检索缓存键设计不当。早期我用查询文本的原始字符串做缓存键结果用户输入多一个空格、少一个标点缓存就不命中缓存形同虚设。后来改成先标准化再哈希命中率从 30% 提升到 70% 以上。第二个坑是Agent 循环没有提前终止机制。有一次线上请求Agent 连续检索了 12 轮每轮都返回相似但不完全一样的结果Agent 就一直觉得“还不够”继续检索。总耗时 45 秒用户早就关了页面。后来加了提前终止判断连续两轮检索结果相似度超过阈值就强制收敛。第三个坑是上下文裁剪把系统指令裁掉了。有一次裁剪逻辑写错了优先裁掉了系统指令结果 Agent 完全失去了行为约束开始胡乱调用工具。这个 bug 排查了很久因为日志里看不出明显异常只是行为变得很奇怪。后来把系统指令设为不可裁剪问题才解决。6.2 关于合体系统的一些个人体会做了几个合体项目之后我最大的体会是RAG 和 Agent 的合体难点不在 AI 部分而在工程部分。模型能力决定了上限但工程实现决定了下限。一个工程做得好的系统即使模型能力一般也能稳定输出可用的结果一个工程做得差的系统即使模型再强上线后也会问题百出。另一个体会是不要追求一步到位。我见过一些团队一开始就想做一个全能的 Agent什么工具都接什么场景都覆盖。结果复杂度失控根本没法调试。我的建议是先做检索前置模式把 RAG 做稳然后逐步引入一两个工具观察 Agent 的决策行为最后再开放更多工具和更复杂的检索策略。每一步都确保稳定之后再走下一步。还有一个体会是日志和监控的投入永远不亏。合体系统的调用链很长没有完善的日志出了问题根本无从下手。我在项目初期就接入了结构化日志和调用链追踪虽然前期花了一些时间但后期排查问题的效率提升了好几倍。6.3 后续可以继续优化的方向如果基础版本已经稳定运行可以考虑几个优化方向。检索策略的自适应根据用户问题的类型自动选择检索策略。事实型问题用精确检索分析型问题用多路召回加重排序。Agent 决策的缓存复用把常见的决策路径缓存起来相似问题直接复用之前的决策序列减少模型推理次数。多模型协作用小模型做意图识别和工具路由用大模型做复杂推理和最终生成在成本和效果之间找更好的平衡。用户反馈闭环收集用户对回答的反馈用反馈数据持续优化检索排序和 Agent 的 prompt。这些方向不需要一次性全做根据业务需求和资源情况逐步推进就行。关键是先把基础版本做稳有了稳定的基线后续优化才有意义。