2026/10/14 4:59:00

Agent编排上生产:从串行超时到分层降级的工程实践

Agent编排上生产:从串行超时到分层降级的工程实践 1. 从一次线上事故说起为什么“能跑通”和“敢上生产”是两回事去年冬天的一个凌晨我被一通电话叫醒。某个内部智能助手系统在高峰期突然开始大面积超时用户侧看到的是转圈、报错、重复提交。排查下来根因并不复杂编排链路里有一个负责意图识别的模型服务响应变慢而我们的编排器当时是“串行等待”的写法——前一个环节不返回后面所有环节全部堵死。更糟的是重试逻辑没有做退避反而在服务已经过载的情况下不断加压最终把整条链路拖垮。那次事故之后我花了大概两周时间把整套 Agent 编排的每一个环节都重新梳理了一遍核心动作只有一个给每个环节都加上失败降级。这篇文章就是那次改造的完整复盘包括我为什么这么设计、每个环节具体怎么降级、参数怎么定、踩过哪些坑。先说清楚这篇文章适合谁看。如果你正在做基于大模型的 Agent 系统或者任何由多个服务节点串联而成的智能编排链路并且已经开始考虑“上生产”这件事那这篇内容应该能帮你少走不少弯路。如果你还停留在本地 Demo 阶段也可以先了解一下生产环境和实验环境到底差在哪——差别不在于功能能不能实现而在于失败的时候系统会怎么表现。我先把结论摆在这里Agent 编排上生产真正难的不是“让每个环节都能成功”而是“让每个环节失败时整体还能给出一个可接受的結果”。这两件事的工程复杂度差了一个数量级。下面我按整体设计、核心环节拆解、实操落地、问题排查四个部分展开尽量把每个决策背后的“为什么”讲透。2. 整体设计思路把“失败”当成一等公民来设计2.1 为什么串行编排在生产环境几乎必然出问题很多人做 Agent 编排的第一版思路都很直接用户输入进来先做意图识别再检索知识库再调用工具再让模型生成回答最后做一次格式化输出。整条链路用await串起来逻辑清晰本地测试也没问题。但生产环境的现实是每个环节的可用性不是 100%而是各自独立的概率。假设一条链路有 5 个环节每个环节单次成功率是 99%那么整条链路一次跑通的概率是 0.99 的 5 次方约等于 95%。看起来还行但如果你一天有 10 万次请求那就是 5000 次失败。而且这还没算上延迟抖动、依赖服务限流、网络分区这些情况。更关键的是串行编排里任何一个环节“卡住”都会导致整个请求的响应时间被拉长。用户等 3 秒和等 30 秒体验是天壤之别。所以我的第一个设计原则就是任何环节都必须有超时任何超时都必须有降级路径。2.2 降级不是“兜底一句话”而是分层设计我见过不少团队对降级的理解就是“出错的时候返回一句‘抱歉请稍后再试’”。这不算降级这叫放弃。真正的降级应该是分层的我把它分成四层层级策略适用场景用户体验L1 重试同环节换实例/换参数重试瞬时抖动、单实例故障几乎无感知只是稍慢L2 降级换更轻量的替代方案主服务持续不可用结果质量下降但可用L3 跳过跳过非关键环节环节非核心缺失可接受结果不完整但主流程通L4 兜底返回预设的保底结果全链路不可用明确告知不假装成功这四层不是随便选的而是根据环节的“关键程度”和“替代成本”来定的。比如意图识别环节如果识别不出来我可以降级成“按默认意图处理”而不是直接报错但如果是安全审核环节那就不能跳过只能兜底拒绝。2.3 编排器本身也要能“自我降级”还有一个容易被忽略的点编排器自己也是一个服务。如果编排器挂了那所有降级逻辑都没意义。所以我在设计时做了两件事一是编排逻辑尽量无状态方便水平扩展二是编排器内部维护一个“健康度评分”当某个下游环节连续失败次数超过阈值时直接进入“熔断”状态后续请求不再尝试该环节直接走降级路径。这个健康度评分我是这么算的每个环节维护一个滑动窗口我用的 60 秒窗口内记录成功、失败、超时三种计数。当失败率超过 50% 且样本数大于 20 时触发熔断熔断持续 30 秒之后进入半开状态放少量请求试探。这套逻辑参考了常见的熔断器模式但阈值是根据我们实际流量调出来的。提示熔断阈值不要照搬网上的默认值。流量小的系统样本数阈值要调低否则永远触发不了流量大的系统失败率阈值可以调高一点避免误熔断。3. 核心环节拆解每个节点到底怎么降级3.1 意图识别环节从“必须准确”到“可接受模糊”意图识别通常是 Agent 编排的第一环它的输出决定了后续走哪条分支。这个环节的降级我做了三层设计。第一层是超时重试。意图识别一般调用的是分类模型或小模型响应时间通常在 100 到 300 毫秒。我把超时设成 800 毫秒超过就重试一次重试时换一个实例如果有多个实例的话。这里有个细节重试不要用同一个连接否则可能还是打到同一个已经出问题的实例上。第二层是降级到规则匹配。如果模型服务连续失败我会降级到一套基于关键词和正则的规则匹配。这套规则覆盖了最高频的 20 个意图准确率肯定不如模型但能保证大部分常见请求还能走通。规则匹配的代码我单独维护不依赖任何外部服务纯本地计算响应时间在 5 毫秒以内。第三层是默认意图兜底。如果规则也匹配不上就归到“通用问答”意图走一个最宽松的处理分支。这个分支不依赖任何特定工具只做基础的检索和生成保证至少能给用户一个回复。实测下来这套设计把意图识别环节的“硬失败率”从原来的 1.2% 降到了 0.05% 以下。代价是有一部分请求的意图识别不够精准但相比直接报错用户接受度高得多。3.2 知识检索环节从“精准召回”到“有总比没有强”知识检索是另一个容易出问题的环节尤其是当检索依赖向量数据库或外部搜索服务时。这个环节的降级我分成了四种情况。第一种是向量检索超时。向量检索的延迟波动比较大尤其是数据量大或者并发高的时候。我把超时设成 1.5 秒超时后降级到关键词检索。关键词检索用的是数据库的全文索引速度更快但召回质量差一些。这里的关键是两种检索的返回格式要统一否则后续环节还要做适配增加复杂度。第二种是向量检索返回空结果。这种情况不一定是故障可能是知识库里确实没有相关内容。我的处理是如果向量检索为空自动触发一次关键词检索作为补充如果关键词也为空就返回一个“未找到相关知识”的标记让生成环节知道要谨慎回答。第三种是检索服务整体不可用。这时候我会降级到一个本地缓存的热门知识库里面存的是最近 7 天被检索最多的 500 条内容。这个缓存每天凌晨更新一次不依赖实时服务。虽然覆盖面有限但能兜住一部分高频问题。第四种是检索结果质量太差。这个比较难自动判断我的做法是给检索结果打一个“置信度分数”如果分数低于阈值就在生成环节的提示词里加一句“检索结果可能不准确请谨慎引用”。这不算严格意义上的降级但能减少模型胡编乱造的概率。3.3 工具调用环节从“必须成功”到“失败也要有交代”工具调用是 Agent 编排里最不可控的环节因为工具往往是外部服务你既控制不了它的可用性也控制不了它的延迟。这个环节的降级我做得最细。首先是超时设置。不同工具的超时应该不一样查询类工具可以给 2 秒写入类工具可以给 5 秒但无论如何不能无限等。我见过有人把超时设成 30 秒结果一个请求卡住后面全排队。超时时间要根据工具的 P99 延迟来定一般是 P99 的 1.5 到 2 倍。其次是重试策略。查询类工具可以重试因为幂等写入类工具要谨慎重试除非工具有幂等键。重试次数我一般设 2 次第一次立即重试第二次延迟 500 毫秒再试。如果还失败就进入降级。降级路径分两种如果这个工具是“必须成功”的比如支付、下单那就直接返回失败并明确告知用户哪一步没完成如果这个工具是“锦上添花”的比如推荐、补充信息那就跳过在最终结果里标注“该信息暂不可用”。这里有个经验降级的时候一定要给用户一个明确的交代。最怕的是工具失败了但系统假装成功返回一个不完整的结果用户还以为一切正常。我宁愿明确说“当前无法查询到实时数据以下是基于历史数据的参考”也不要让用户产生误解。3.4 生成环节从“一次生成”到“多级保底”生成环节是用户最终感知到的环节它的降级直接决定用户体验。我做了四级保底。第一级是正常生成用主模型带完整的上下文和检索结果。第二级是如果主模型超时或失败降级到备用模型。备用模型可以是一个更小、更快的模型质量差一些但响应快。第三级是如果备用模型也失败降级到模板生成。模板生成就是根据意图和检索结果套一个预设的回复模板虽然生硬但至少信息是准确的。第四级是如果模板也生成不了比如检索结果为空就返回一个标准的兜底话术并建议用户换个问法或稍后再试。这四级保底的关键是每一级的输出格式要一致这样前端不需要做额外判断。我在生成环节的出口做了一个统一的格式化层不管走哪条路径最终输出的结构都是一样的。注意备用模型的选择要考虑“能力差距”。如果备用模型太弱生成的内容可能还不如模板。我的经验是备用模型至少要是主模型能力的 70% 左右否则降级的意义不大。4. 实操落地从代码结构到参数配置的完整方案4.1 编排器的代码结构怎么组织我把编排器分成了四个模块流程定义、环节执行、降级策略、监控上报。流程定义负责描述“有哪些环节、顺序是什么”环节执行负责实际调用降级策略负责判断失败类型和选择降级路径监控上报负责记录每个环节的状态。这种分法的好处是降级策略可以独立于具体环节来配置。比如我可以定义一个通用的“超时降级”策略然后应用到多个环节上不需要每个环节都写一遍。下面是一个简化的配置示例# 环节配置示例伪代码仅示意结构 pipeline_config { intent_recognition: { timeout_ms: 800, retry: {max_attempts: 2, backoff_ms: 200}, fallback: [rule_based, default_intent], critical: False }, knowledge_retrieval: { timeout_ms: 1500, retry: {max_attempts: 1}, fallback: [keyword_search, local_cache], critical: False }, tool_invocation: { timeout_ms: 2000, retry: {max_attempts: 2, backoff_ms: 500}, fallback: [skip_with_notice], critical: True }, generation: { timeout_ms: 5000, retry: {max_attempts: 1}, fallback: [backup_model, template, generic_response], critical: True } }这个配置里critical字段很关键。它决定了这个环节失败时是走“跳过”还是走“兜底”。非关键环节可以跳过关键环节必须兜底。4.2 超时和重试参数怎么定超时和重试参数不能拍脑袋定要有数据支撑。我的做法是先跑一周的监控收集每个环节的延迟分布然后按 P99 的 1.5 倍来设超时。重试次数则根据失败类型来定如果是网络抖动重试 1 到 2 次就够如果是服务过载重试反而有害这时候应该直接降级。这里有个计算过程可以分享。假设某个环节的 P50 延迟是 200 毫秒P99 是 800 毫秒那么超时设成 1200 毫秒比较合理。如果设成 800 毫秒会有 1% 的正常请求被误判为超时如果设成 2000 毫秒虽然误判少了但一旦真的出问题用户要等 2 秒才能看到降级结果。1.2 秒是一个平衡点。重试的退避时间也有讲究。我一般用指数退避第一次重试等 200 毫秒第二次等 500 毫秒第三次等 1 秒。但重试总次数不超过 2 次因为重试太多会放大下游压力。如果两次重试都失败说明问题不是瞬时的直接降级更明智。4.3 降级路径的触发条件怎么判断降级路径的触发条件不能只看“成功/失败”还要看失败的类型。我把失败分成了几类超时、连接错误、服务返回错误码、返回结果不符合预期。不同类型的失败降级策略不一样。超时和连接错误优先重试服务返回错误码要看错误码的含义如果是限流就等一等再重试如果是参数错误就直接降级返回结果不符合预期这个最难判断我的做法是加一层校验校验不通过就降级。校验的逻辑可以很简单比如检查返回结果是否为空、字段是否齐全、数值是否在合理范围内。这层校验不需要很复杂但能拦住大部分“看起来成功实际上有问题”的情况。4.4 监控和告警怎么配套降级逻辑上线后如果没有监控你根本不知道降级有没有生效、生效了多少次。我在每个环节的出口都埋了点记录四个指标总请求数、成功数、降级数、失败数。降级数又按降级类型细分比如“超时降级”“错误降级”“熔断降级”。告警策略我设了两级如果某个环节的降级率在 5 分钟内超过 10%发预警如果超过 30%发告警。预警只是通知告警要有人处理。这样既能及时发现问题又不会因为偶尔的抖动频繁打扰。还有一个细节降级日志要记录足够的信息方便事后排查。我一般会记录请求 ID、环节名称、失败原因、降级路径、耗时。这样出问题的时候可以快速定位是哪个环节、什么原因、走了哪条降级路径。5. 常见问题与排查技巧实录5.1 降级逻辑本身出问题怎么办这是最尴尬的情况降级逻辑本来是为了兜底结果它自己挂了。我遇到过两次一次是规则匹配的正则写错了导致所有请求都匹配到同一个意图另一次是本地缓存的读取逻辑有并发问题高并发时返回了脏数据。解决办法有两个一是降级逻辑要尽量简单不要引入新的外部依赖二是降级逻辑也要有测试不能因为它是“兜底”就忽略测试。我现在会把降级路径纳入常规的集成测试确保主路径和降级路径都能跑通。5.2 降级导致结果质量下降用户投诉怎么办降级必然带来质量下降关键是怎么管理用户预期。我的做法是如果走了降级路径在最终结果里加一个不显眼的提示比如“当前为简化模式”或“部分信息可能不完整”。这样用户知道情况不会产生误解。另外降级率本身也是一个需要监控的业务指标。如果某个环节的降级率持续偏高说明主路径有问题应该去修主路径而不是长期依赖降级。5.3 熔断后怎么恢复熔断容易恢复难。我的做法是半开状态放 5% 的流量去试探如果连续 10 个请求都成功就恢复正常如果有失败就继续熔断。这个“连续成功”的阈值可以根据流量调整流量大的系统可以设高一点流量小的设低一点。还有一个坑熔断恢复后不要一下子放全量流量否则可能再次压垮下游。我一般会做一个渐进恢复从 10% 开始每隔 30 秒增加 10%直到 100%。这个过程大概需要 5 分钟虽然慢但稳。5.4 常见问题速查表问题现象可能原因排查方向解决建议降级率突然飙升下游服务故障查看下游服务健康状态确认是否触发熔断必要时手动降级降级后结果格式错乱降级路径输出格式不一致检查各降级路径的返回结构统一格式化层确保输出一致重试导致下游压力更大重试策略过于激进查看重试次数和退避时间减少重试次数增加退避时间熔断后一直不恢复半开试探一直失败检查下游是否真的恢复确认下游健康后再手动恢复降级逻辑报错降级代码本身有 bug查看降级路径的日志简化降级逻辑补充测试5.5 几个我踩过的坑第一个坑是降级路径没有超时。有一次我写了一个降级到本地缓存的逻辑结果缓存读取因为锁竞争卡住了降级路径自己超时了。后来我给所有降级路径也加了超时一般设成 200 毫秒超过就直接走下一级。第二个坑是降级日志太多。刚开始我把每次降级都记详细日志结果日志量暴涨反而影响了性能。后来改成只记录关键信息并且做了采样比如每 100 次降级记录一次详细日志。第三个坑是忘了测试降级路径。主路径测试得很充分降级路径从来没跑过结果真出问题的时候降级路径一跑就报错。现在我强制要求降级路径也要有测试用例覆盖率不低于主路径的 80%。6. 一些个人体会和后续可以做的事这套降级体系上线后我们系统的整体可用性从 99.5% 提升到了 99.95% 以上用户侧的报错率下降了 90% 多。但更重要的是团队的心态变了以前出问题的时候大家很慌现在知道每个环节都有兜底排查问题的时候也更从容。如果让我给正在做类似事情的人一个建议那就是不要等到出事故了才加降级。降级逻辑应该在设计阶段就考虑进去而不是事后补丁。补丁式的降级往往考虑不周全而且容易和主逻辑耦合后期维护成本很高。后续我还在做两件事一是把降级策略配置化让运营人员也能调整降级阈值不需要改代码二是做降级的自动化演练定期模拟下游故障验证降级路径是否真的有效。这两件事做完整个体系的健壮性应该还能再上一个台阶。最后分享一个小技巧降级路径的命名要清晰比如fallback_to_keyword_search就比fallback_2好得多。出问题的时候日志里一眼就能看出走了哪条降级路径排查效率会高很多。这个细节看起来不起眼但在凌晨三点排查问题的时候真的能救命。