ARTICLE DETAIL

资讯详情

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

Agent Harness模型降级实战:从故障分级到任务恢复的设计指南

Agent Harness模型降级实战:从故障分级到任务恢复的设计指南 说出来你可能不信我一开始搞 Agent Harness压根没想把“模型降级”当成一个正经模块来做。起因是有一次主用的大模型服务在下午高峰期连续抖动本来跑得好好的任务队列像被人掐住了喉咙请求全卡在等待上。我盯着日志发现大量 Agent 子任务的执行状态一直是 running超时告警刷了满屏用户那边反馈“机器人在转圈”。那一次事故之后我才明白做 Agent 系统模型能力只是地基能不能在不同的模型之间平滑切换、在故障期间保住任务不丢不坏才是 Agent Harness 真正的价值所在。所以这篇就从我自己的实践出发聊聊 Agent Harness 里的“模型降级”模块怎么设计。会讲清楚为什么降级不能靠“主模型挂了就换备用”这种粗暴思路以及我在故障分级、上下文切换、任务恢复、防雪崩这几个环节里踩过哪些坑、最后是怎么落地的。1. 一次大规模故障暴露了 Harness 层的降级空白1.1 Agent Harness 到底是什么先对齐一下概念。很多做 Agent 应用的人一开始会直接用 LangChain、LlamaIndex 这类框架或者直接调 Anthropic、OpenAI 的 API把 Prompt、工具调用、上下文管理全堆在一个 Application 里。一开始没毛病但等任务复杂度上来你会发现真正需要自己掌控的不是“怎么调模型”而是“怎么把一次用户请求拆成可执行的任务图、怎么管理多轮状态、怎么处理工具调用闭环、怎么在模型不可用时保证系统仍然可用”。这一层就是 Harness 层。Agent Harness 在我这里更像一个执行编排层它不绑定具体模型也不替你写业务逻辑而是提供任务分解、会话状态持久化、模型路由、工具调用注册与执行、异常恢复这些通用能力。说白了它就是介于“模型 API”和“真实业务 Agent”之间的那一层骨架。模型降级放在这个骨架里属于可靠性体系的一部分但它是底线能力——因为模型服务一定会故障区别只是什么时候故障。1.2 我把“模型降级”做成模块后要解决什么一开始我没把降级做成模块只是简单地在 Agent 调用模型失败后 catch 异常再换个 base_url 重试。结果一上线就出问题部分请求超时后备用模型虽然接住了但上下文的角色信息对不上工具调用也经常失败用户看到的结果是“回答得驴唇不对马嘴”。系统看起来没崩体验却是崩的。做完这轮重构后我给自己定了三个目标。第一降级必须分级不是所有超时都值得切换切换是代价很高的动作。第二降级切换不能只换模型地址要连上下文结构、工具 Schema、输出格式一起适配。第三降级发生在任务中间时正在执行的子任务不能丢已完成的工具调用不能重复执行。这三点其实是模型降级模块的完整需求边界后面的设计和实现全是围绕它们展开的。2. 降级判定是系统工程先分清故障等级再做切换决策2.1 曾踩过的坑对任何超时都直接切换最开始我处理模型故障的逻辑非常简单如果主模型连续两次请求超时我就把后续流量切到备用。表面看没问题但实际跑了一周后发现很多“超时”只是网络抖动或单次请求上下文太长导致的偶发慢并不代表主模型已经挂了。有一次主模型响应变慢P95 延迟从 2 秒涨到 8 秒我设的阈值是 5 秒结果系统立刻切到备用。不巧的是备用模型是一个能力弱一些、对复杂任务支持不太好的模型切过去之后 Agent 的推理质量直线下滑工具调用成功率从 95% 掉到 60%。主模型那边其实十分钟后就恢复了但流量已经被降级策略带着跑偏了。那次之后我明白了一个道理降级判定的核心不是“快”而是“准”。2.2 探测层、服务层、业务层三级健康检查后来我把健康检查设计成了三层每一层给出不同的故障置信度。探测层每 30 秒对主模型发一次最小化的探测请求比如“请回答 ok”只看连通性和响应时间。这层用来发现“网络不通”“服务 completely down”这类最明显的故障但无法判断“模型本身是否在正常完成复杂推理”。如果连续 3 次探测失败判定该模型进入 L3 级故障。服务层统计过去 2 分钟内真实请求的成功率、P50/P95 延迟。成功率通过请求返回码和异常类型统计延迟用滑动窗口记录。这一层能发现模型服务处于“半死不活”的状态——没有完全挂但已经明显劣化对 Agent 应用来说体验极差。连续 2 分钟请求错误率超过 20%或 P95 延迟超过基线 2 倍且持续超过 3 分钟判定进入 L2 级故障。业务层由 Agent 业务侧主动上报。比如某个 Agent 在调用工具后拿到的结果无法通过基本格式校验或者连续多次 Tool Call 都没有返回有效参数。这种抖动模型 API 自己是不知道的只有业务侧能感觉到。当单条会话在最近 5 轮里出现 3 次以上业务失败时会把该会话标记为“业务级异常”此时即使服务层没有降级也可触发会话级别的快速切换。三个层级各有各的分工探测层解决“不可用”服务层解决“劣化”业务层解决“看得到吃不到”。分层最大的好处是让降级决策有依据而不是拍脑袋靠直觉。2.3 降级判定状态机与冷却恢复策略每层判定结果会汇总到一个状态机里。每个模型都有四个状态绿色健康、黄色疑似劣化只对新请求做部分降级、红色确认故障切走全部流量、黑色彻底摘除等待人工介入或长时间自动恢复。等状态从绿色跳黄流程如下立刻把新请求中 30% 切到备用链路同时继续监测主模型。为什么不直接 100% 切走因为很多时候所谓的“疑似劣化”只是一次短时间的网络分区或限流抖动直接全切会引发备用链路流量飙升反而制造新的不稳定。如果主模型持续劣化超过 2 分钟状态从黄色进入红色此时所有新请求全部走备用链路不再犹豫。关于恢复这里关键问题是回切时机。模型服务出问题之后不是它恢复响应就代表真的稳定了。我见过多次回切后立刻再次抖动的情况整体故障时间反而变长。所以我在红色状态加了冷却窗口主模型需要连续 5 次健康探测全部通过且服务层延迟低于正常基线 P95 持续 10 分钟以上状态才会从红色转黄色再按 20%、50%、100% 的比例逐步回切。整个过程至少需要 15 分钟避免刚恢复就被流量冲垮。3. 请求路由与 Provider 适配切换不是换一个 base_url 就行3.1 不同模型框架的参数差异降级判定只是第一步流量切过去之后真正麻烦的事情才开始。不同模型提供商之间API 协议、参数语义、消息格式全都不一样。最初我会天真地想“反正模型都是文本进文本出把接口封装成 openai 格式不就统一了吗”实际上会有不少隐蔽差异。比如 Anthropic Messages API 的消息结构里assistant 消息可以有 thinking 块和 text 块工具调用有自己的 tool_use 块格式OpenAI 的 chat.completions 里工具调用是 tool_calls 数组参数结构和系统提示的 role 语义也略有差异。如果只做一个薄适配层就会发现降级后新模型往往还要拿旧模型的整段历史消息去理解一旦里面有大量 provider 特有的元数据字段模型很容易被干扰。后来我参考社区里 Harness 类框架的做法把模型请求抽象成一个中间协议内部所有 Agent 逻辑只和 Message、ToolCall、ToolResult 这三类结构打交道。具体发送给哪个 provider 时再通过 adapter 转换成对应格式。这个抽象层的价值在降级场景下是最大的因为切换发生时唯一需要变化的就是路由配置业务代码完全无感。3.2 工具调用和结构化输出的 Schema 对齐比消息格式更隐蔽的坑是工具 Schema。Agent 应用里模型需要根据任务来决定调用哪些工具、怎么填参数。以我的经验不同模型对 function calling 的支持差别很大。举例来说主用模型可能支持 parallel_tool_calls允许一次返回多个工具调用但切到某个开源模型或小参数模型后它同一轮可能只能执行一个工具甚至有些模型对工具定义里 enum、object 嵌套的支持也有 bug。如果 Harness 不感知这种能力差异直接拿主模型的工具列表发给备用模型很容易出现参数格式错乱或者模型反复请求一个 Harness 不认识的工具。我给降级模块加了一个能力对齐检查在决定把流量切到某个备用链路前Harness 会先比较主链路和备用链路注册的工具 Schema逐字段做一次兼容性检查。如果目标模型不支持某类工具Harness 有两种处理方式一是把这类工具从候选工具列表中临时摘除并修改 Prompt 策略告诉 Agent 当前可用工具集合有变化二是如果核心工具缺失则推迟降级继续尝试其他可选模型。发现目标模型不支持 parallel_tool_calls 时我干脆把主链路的多工具并行请求改写成串行队列由 Harness 逐个等待工具结果后再送模型继续推理。结构化输出也一样不同 provider 的 JSON mode、function calling-based output、格式约束方式不同我统一在内部定义 JSON Schema再由 adapter 转换成目标 provider 支持的格式。这样 Agent 业务才能保证降级前后拿到的永远是同一套结构而不是还要为不同模型各写一份 Prompt。3.3 降级前的能力对齐检查与 dry-run这里补充一个我强烈建议的做法降级切换前不要直接放流量先跑一次 dry-run。所谓 dry-run就是在降级策略生效、流量正式切换前Harness 会拿当前真实会话的上下文用目标模型做一次最小成本的推理请求不发真实工具调用只看返回能否被正确解析。我是在项目上线后才补上这个环节的。之前有一次切换没有做 dry-run新模型在工具调用解析阶段疯狂报错用户等来的是一堆“抱歉我暂时无法处理”之类的兜底话术。问题倒不在模型本身而是我们的 adapter 对 provider 返回的 tool call id 格式处理不一致工具结果回来的时候匹配不上。后来每次触发降级前都会对关键工具跑一次冒烟测试返回结构必须通过校验才允许正式切流量。配置层面我是在一块独立的模型路由配置里维护的大致长这样model_router: default_strategy: priority providers: - id: primary type: anthropic model: claude-sonnet-latest weight: 85 timeout_ms: 20000 health_check: probe_interval_sec: 30 error_rate_threshold: 0.2 p95_latency_threshold_ms: 15000 - id: fallback-a type: openai model: gpt-5-mini weight: 15 timeout_ms: 15000 dry_run: true给备用链路设置不同的 timeout_ms也是一个小技巧不做的话主链路如果已经因为延迟超时而故障备用链路用同样超时配置也一样容易超时等于没降级。4. 子任务恢复与状态传递降级最烧脑的其实是命中和幂等4.1 任务快照的具体结构模型降级最容易忽略的地方其实是 Agent 正在执行中的任务。如果一次用户请求已经被 Agent 拆分成多个子任务有些子任务已经完成比如已经查完了数据库有些正在进行模型正在组织下一步调用此时底层模型突然故障Harness 如果不保存中间状态切换后一切都要重来。最坏的情况下用户会发现同样的扣费动作被重复执行了两次。我的做法是给每个任务建立快照机制走到关键节点就存一次不依赖进程内变量。快照数据包含这几个部分任务元数据task_id、user_id、目标模型 provider、创建时间。对话状态完整消息历史内部协议格式不包含 provider 特有字段。子任务执行图每个子任务的输入、输出、当前状态、依赖关系。工具调用结果表已经成功调用的工具包含工具名、请求参数、返回结果、request_id、时间戳。快照存到中心 KV 或 Redis 里key 就是 task_id每次子任务完成或状态变化时做一次更新。这样降级切换时新的模型实例拿到的不是空上下文而是一份结构完整的执行记录。4.2 恢复不重跑的设计快照有了恢复逻辑就很关键。降级切换发生时代码基本是重新加载 task_id 的快照判断当前执行到哪一步然后继续往下走。但不能无脑执行要区分每个子任务的执行状态。我定义了几种状态not_started还没执行过可以直接调度执行。in_progress正在执行中但模型已经故障无法确认是否完成。completed执行完毕结果已验证跳过。failed明确失败可以重试。重点是 in_progress 状态。它意味着原模型在故障前可能已经执行了一部分逻辑也可能什么都没做。对纯读操作来说重新执行一次问题不大但对有副作用的工具调用比如发邮件、扣款、写数据库重复执行是绝对不允许的。我的策略是in_progress 的子任务先重置为 not_started但所有与它关联的“工具副作用”全部强制走幂等控制而不是关掉重试。代码里大致是这样一层的逻辑def recover_task(task_snapshot: TaskSnapshot) - ExecutionPlan: plan ExecutionPlan(task_idtask_snapshot.task_id) for sub in task_snapshot.subtasks: if sub.status ExecStatus.COMPLETED: plan.skip(sub) # 已完成直接跳过 elif sub.status ExecStatus.IN_PROGRESS: # 标记中断但保留已产生的 tool_result plan.mark_interrupted(sub) plan.retry_from_last_milestone(sub, max_retry3) else: plan.enqueue(sub) # 无关任务正常排队 return plan这样设计的目的很简单把“重试”和“重放”分开。已经成功的工具调用不能被重放只有那些无法确认成功的调用才允许在幂等保护下重试。4.3 工具调用的副作用管理与幂等控制讲到幂等就不得不单独强调一点在 Agent Harness 里做模型降级最大的风险可能不是模型回答变差而是任务状态混乱导致业务事故。我有一个非常惨痛的教训早期某次降级恢复时由于快照没有记录工具调用结果系统把同一个“创建订单”的工具执行了三遍。用户收到三封确认邮件直接被惹毛了。后来我把工具调用封装成了一个带幂等键的 RPC。每个工具调用在执行前先生成一个 request_id执行结果连同这个 request_id 一起保存到快照表中。重新执行工具调用前Harness 会先检查该 request_id 是否已经有成功记录有就直接返回历史结果不再真正调用工具。副作用控制再进一步是把工具按能力分为幂等型和非幂等型。查询、读取类默认可以重跑写入类的工具在工具定义里必须声明 idempotent_keys或者由业务方提供一个 dedupKey 生成函数。Harness 在降级后执行子任务时如果发现工具没有幂等键配置宁可失败返回人工处理也不冒险自动重试。这个设计让我在后续多次真实故障里都能比较放心地做降级因为切换不会以牺牲正确性为代价。任务要么顺利完成要么明确失败等待人工介入最怕的“看起来成功实际重复执行”基本没再出现过。5. 防止降级演变成雪崩错峰、限流和缓存兜底5.1 为什么全量瞬时切过去很危险模型降级还有一个隐秘的坑它本身可能制造新的故障。当主模型开始抖动一些 Agent 进程会比平时等待更久队列里积压大量任务。如果降级策略是“检测到故障全量切到备用”那备用模型本来健康却会在同一秒内迎来所有积压请求峰值往往达到平时的数倍。备用模型也不是弹性无限的它遭遇限流之后Agent 会报新的错误然后 Harness 又可能触发了对“备用模型的降级”……我第一次遇到这个情况时整整十几分钟主备两条链路全都在报错系统真的成了“全家挂”。那次之后我再也没用过全局瞬时切换。5.2 分组错峰降级和动态配额现在的做法是让降级过程从“一刀切”变成“分批错峰”。当主模型状态达到红色不立刻把所有实例的流量切走而是把实例按概率分成几组第一批比如 30%在状态变更后的 10 秒内完成切换第二批再等一个随机延迟20~60 秒第三批再错峰跟进。在 Harness 实例有多个副本的情况下我会把降级状态放到配置中心或 Redis各个实例 watch 到状态变化后各自计算自己的延迟偏移量。这个偏移量可以简单地用实例 ID 的哈希值加一个随机数实现目的是让切换请求分散在几十秒甚至几分钟的时间窗口内避免瞬时流量冲击。备用链路的调用配额也做了动态调整。平时备用模型池只保留比较小的预留配额比如总量的 15%。当降级开始后Harness 会先发一个配额扩容请求把备用模型池的 QPS 上限上调到足够支撑全量再逐步放流量。顺序不能反否则流量过去也是被限流效果和没切换差不多。5.3 用摘要压缩和静态缓存压制峰值降级时还有一个不可忽略的操作识别降级期间的限载策略。主链路任务上下文很长特别是多轮 Agent 任务每条请求可能携带几千个 token。如果备用模型的上下文窗口比主模型小或者备用链路成本更高直接全量转发显然不理智。我给 Harness 加了一个上下文压缩处理在降级切换那一刻对会话历史做摘要只保留最近两轮完整消息更早的历史交给一个摘要模型压成一段 summary再放进系统提示里。这样既保留了关键信息又显著降低了备用链路的负载。另一个实战技巧是静态缓存兜底。很多 Agent 任务其实是在重复回答类似问题比如某个群里昨天有人问过“账单怎么查”今天又有人问。正常时无所谓但降级期间与其把所有请求都压到备用模型不如让 Harness 在服务层拦截一部分可以命中缓存的任务直接把之前生成的答案做模板化输出给真实模型链路腾出空间。三条链路同时工作的降级矩阵是链路触发条件目标容量超时策略核心保护措施主用模型健康全量20s重试限制备用模型黄/红状态弹性扩容初始30%逐步到100%15s上下文压缩、dry-run、幂等静态缓存命中缓存不限200ms关键词匹配、TTL管理6. 实测效果与三个实战复盘6.1 加上降级后实测数据变化整套模块上线后又遇到过一次主模型服务不稳定前前后后大概持续了 40 分钟。当时故障期间的数据对比让我印象很深。没有降级模块时主模型只要能通系统就会死等大量任务累计超时最终成功率只有 31%。加了降级后同样的故障窗口里任务成功率恢复到了 91% 以上。用户侧感知最明显的变化是 P95 响应时间从“长时间无响应”降到了 18 秒左右虽然没有平时约 3 秒那么快但对一个正在经历故障的系统来说至少用户知道机器人还在工作。备用模型那条链路的 P95 也控制在 8 秒内。这不是备用模型本身多厉害而是因为我们在降级前做了上下文压缩在切换时做了错峰同时还用静态缓存挡掉了一部分高频问题。降级过程更像一个“有计划地限载并转移”而不是恐慌式地把所有请求抛给另一个模型。6.2 复盘1回切太早会二次抖动第一次完整跑完降级流程时我在回切上栽了个跟头。主模型服务探测已经恢复正常我看状态机将模型从红色置为绿色立刻恢复全部流量。结果不到三分钟主模型再次劣化系统又切到备用链路。一小时内来回切了两次备用模型也跟着不稳定。后来我总结原因就是回切时没有考虑稳定期恢复探测通过的样本量也不够。现在做回切一定会经过冷却窗口模型必须连续 10 分钟健康、探测请求全部成功延迟 P95 低于阈值的 1.5 倍才能开始回切流量。而且回切过程仍然按百分比梯度放量不是一瞬间全部回切。这个改动之后再也没有出现过切换震荡。6.3 复盘2工具 Schema 不同导致的断裂还有一次比较典型的问题是备用模型虽然回答很快但 Agent 的任务执行率很低。排查日志发现模型返回的 Tool Call 里多次包含一个它自己幻想出来的工具名而不是我们注册的那些。原因是两个模型能同时接收的上下文里我们对工具描述的风格不同导致备用模型理解偏差。后来我在工具调用注册时给每个工具加了别名字段并在切换时自动为主链路和备用链路各生成一份更适合该模型的工具描述提示词。同一时间也加强了前面提过的 dry-run 机制保证切换前 Harness 已经验证过目标模型能如实返回已注册工具的参数。6.4 复盘3备用链路配额忘了调整还有一次故障期间备用模型的 API 返回大量 429 限流错误速度反而比主链路还慢。查到底是我们给备用链路的配额限制还停留在日常状态——只预留了 15% 的流量能力一旦切过去 50% 以上流量就撑不住了。这个问题其实应该在降级启动时同步处理配额扩容而不是事到临头才发现。现在我的做法是在模型路由配置里为两条链路都维护实时 QPS 水位降级启动时Harness 自动向配置中心发起配额调整请求确认目标链路容量就绪后再继续切流量。如果确认 3 次仍然达不到要求harness 会进入降级保护模式宁可拒绝一部分非核心任务也不让故障扩散。这套降级模块从设计到落地前后改了好几版。我最大的感受是Agent Harness 里的模型降级从来不是一个“换模型”的动作而是一整套围绕故障识别、协议适配、状态恢复、流量保护的工程体系。很多网上讲 Agent 开发的文章都集中在 Prompt 和工具调用怎么写真正到了生产环境能让你系统活下来的反而是这些看似不起眼的降级细节。希望这篇里的思路和踩坑记录能给你做类似模块时一些参考。
返回列表