资讯动态

AI Agent运行时守护:看门狗与死循环熔断器实战

发布时间:2026/10/5 5:33:01 来源:尧图企业网站定制
做 AI Agent 的人总会在某个深夜收到一条让自己瞬间清醒的账单通知。提示流编排器跑了一个多小时其中一个子 Agent 掉进死循环每次都认为自己还差最后一步于是几万次请求涌向同一个接口平台预算以指数速度被吞掉。这个开源系列写到第 13 篇我想把在提示流编排器里从 0 到 1 设计运行时看门狗与死循环熔断器的过程完整拆开讲包括那些只在真实踩坑之后才拿得到手的经验。这套东西适合所有正在做 Agent 开发、多 Agent 协作编排或者单纯被 API 费用吓过的开发者。你可以把它理解成编排器里的第三层防线限流管入口熔断管反复失败预算管燃烧速度而看门狗负责让一切沉默的任务尽早暴露。不是等 Agent 真的把信用卡刷爆了再去复盘而是让它在失控的第一时间就被按住、隔离、终止。1. 先说结论为什么要在编排器里内置守护层1.1 Agent 失控的真实画像死循环、调用风暴与账单警报很多人觉得 Agent 失控是概率极低的事但真正跑过生产环境的人都知道这几乎是必然发生的。我见过最常见的失控场景有三种。第一种是典型的死循环Agent 在规划阶段总是认为自己没拿到足够信息于是反复调用同一个搜索工具把同样的关键词换着花样翻来覆去找。表面上看每一次都有意义但整体就是一个丁点儿进展都没有的圆圈。第二种是级联放大主 Agent 派了 10 个子 Agent其中一个子 Agent 又各自派了 10 个孙 Agent突然整棵任务树膨胀到几千个节点每个节点都在等上一层的输出互相阻塞又互相唤醒。第三种最隐蔽是高消耗重复Agent 每一步都在调用超大上下文的模型单步成本奇高虽然步数不多但每走一步都在烧大钱。这三种场景的共性是光靠超时取消根本拦不住。死循环里的单次调用往往能在超时时间内正常返回问题出在步数无限膨胀上级联放大里的每个子任务单独看都还健康但整体已经失控高消耗重复更是连错误都不报一切看起来都很正常只有账单在报警。所以编排器的守护层必须同时关注两个维度时间维度和资源维度前者靠看门狗后者靠熔断器和预算机制。1.2 守护层在整个编排架构中的位置我在这套开源编排器里把运行时划分成三层调度层、执行层、守护层。调度层负责把用户请求拆成 DAG决定哪个 Agent 先跑、哪个后跑执行层负责任务的具体执行包括调用大模型、调用工具、传递上下文。而守护层不直接参与任务逻辑它的职责是在旁边盯着一旦发现异常就插手干预。守护层再细分承担的角色各不相同守护组件监控对象主要手段触发后的动作看门狗任务是否还在推进心跳检测、多级超时标记卡死、取消任务、强制终止熔断器任务是否在反复失败滑动窗口失败率统计快速失败、暂停该路径调用预算守卫费用和 Token 消耗速度预算信封、按步预扣结算中止高消耗任务、切换低配模型这三层互相配合但职责不重叠。看门狗解决没消息的问题熔断器解决一直错的问题预算守卫解决太贵了的问题。你可以只实现其中一个但生产环境里缺了任何一环都会出大事故。1.3 三层防线的工作顺序实际运行时这三层是按时间先后和触发条件交织作用的。一个 Agent 启动后预算守卫先做一个预扣动作把这步可能花掉的钱从预算池里暂时冻结然后任务进入执行看门狗启动超时计时器执行过程中如果某个工具调用连续报错熔断器开始计数失败率一旦超过阈值就打开熔断后续同样调用会立刻走快速失败分支不再真正发请求。如果任务一直正常返回看门狗会不断收到心跳超时计时器一直重置预算守卫在每一步执行完后做结算把没用完的预扣额度退回。等到整个流程走完三层防线几乎没有存在感——这就是最好的状态。真正体现价值的是异常时刻死循环触发看门狗把任务 kill 掉连续失败触发熔断器让后续请求快速失败预算异常消耗触发守卫直接掐断任务链。2. 运行时看门狗把卡死和沉默及时暴露出来2.1 看门狗的基本原理心跳、定时器与喂狗看门狗这个概念是从嵌入式系统借来的。硬件看门狗芯片里有个计数器系统正常运行时会不断喂狗——也就是重置计数器一旦系统卡死没人喂狗计数器溢出看门狗就强制重启系统。这套逻辑放到编排器里完全成立每个 Agent 任务就是一个被监控的进程编排器就是那个看门狗芯片。我在实现里为每个任务维护一个 heartbeat这个 heartbeat 不是时间戳那么简单而是一个包含任务 ID、当前步骤、状态码的轻量级事件。Agent 在执行关键节点时主动上报比如我开始调大模型了我拿到结果了我正在分析下一步。编排器收到心跳后重置任务的 deadline。这里有个很多人第一次实现时会犯的错只检测有没有心跳不检测心跳内容是否合理。有的 Agent 卡在一个 while 循环里每次都上报心跳我还活着但事实上它根本没推进。所以我的心跳事件里必须带当前步骤编号——如果连续 N 次心跳的步骤编号没有变化就算它有心跳也判定为卡死。这个细节是血泪教训换来的。2.2 多级超时策略单步、单轮、整条链路看门狗的超时策略必须分层只用一档超时时间几乎必错。单步调用超时、单轮任务超时、整条链路超时分别解决不同粒度的问题。单步调用超时作用于最底层比如一次 LLM API 调用或一次工具调用超时时间通常按 P95 延迟的 3 到 5 倍来设。我这边对主流大模型接口设的是 60 到 120 秒对内部工具调用设的是 10 到 30 秒。单轮任务超时作用于一个 Agent 从拿到输入到给出输出之间默认 10 分钟。整条链路超时作用于整个编排流程默认 60 分钟。为什么要多级因为卡死是分层的。可能是底层 API 卡住也可能是 Agent 在做思考时陷入无限循环还可能是一整条流水线在等某个子任务的结果。如果只检查单步超时Agent 死循环里每一步都在超时边缘返回就不会被触发如果只检查链路超时那要等 60 分钟才能发现一个其实在 5 分钟内就该暴露的问题。三层叠放哪个先爆就处理哪个。2.3 从 cancel 到 kill看门狗触发后的隔离动作看门狗判定任务异常之后接下来的动作顺序很重要。我的做法是分三步标记、取消、强制回收。第一步先把任务状态从 RUNNING 标记为 TIMEOUT 或 STALLED同时把异常事件广播到事件总线让预算守卫和熔断器同步感知。第二步调用调度层的取消接口当任务还处在可响应位置时比如在等待大模型返回可以直接取消协程或异步任务。这一步是优雅的能尽量让已完成的子任务正常返回避免副作用悬空。第三步是强制回收。任务如果卡在阻塞 IO 里、进了 C 扩展的循环、或者干脆把事件循环给堵死了普通取消根本没用。这时候我会在单独的看门狗线程里直接对执行单元做强制终止把它占用的资源全部释放同时把它的失败原因写进任务日志。注意这一步必须保证只回收任务自己的资源不能把同一个进程里其他正常任务给带崩。所以隔离性是看门狗实现中最重要的一条工程红线。3. 死循环熔断器防止 Agent 在同一个坑里反复烧钱3.1 熔断器的三态状态机与滑动窗口统计熔断器Circuit Breaker是微服务架构里非常成熟的容错模式但用到 Agent 编排里触发条件和恢复逻辑要重新设计。状态机还是那三态CLOSED 表示一切正常请求照常放行OPEN 表示熔断打开请求直接快速失败不再真正发往大模型或工具HALF_OPEN 表示试探阶段放少量流量进来验证下游是否恢复。我在实现里统计用的是固定大小滑动窗口而不是简单的计数器。比如维护一个长度为 60 秒的时间桶每秒一个计数单元格记录这一秒内的成功数、失败数和消耗 Token 数。这样过去一分钟内失败率是多少就变成了对窗口内格子的求和时间复杂度 O(1)每分钟滚动更新一次。滑动窗口和普通计数器的差别很关键普通计数器是从任务开始累计一旦累计值没归零失败率会一直偏高导致熔断恢复困难滑动窗口只看最近一分钟的状态更贴合 LLM 调用波动大的现实。实测下来同一个 Agent 在同样的超时参数下用滑动窗口的误杀率比用全局计数器低了将近一半。3.2 触发维度与参数设计失败率、预算消耗速率、最小采样量熔断触发的维度要能覆盖 Agent 场景的三种异常连续失败、错误率爬升、预算异常燃烧。我分别设置了三个维度的阈值并且在代码里是或的关系——任何一个条件满足都触发熔断。参数默认值说明window_size60s滑动窗口大小failure_rate_threshold50%窗口内失败率阈值min_request_volume10最小请求量避免样本太少误判consecutive_failures5连续失败次数阈值budget_burn_rate3x当前消耗速率 / 历史平均速率half_open_max_requests2半开状态允许的最大试探请求数min_request_volume是我特别想强调的参数。窗口内只有 2 个请求失败了 1 个失败率 50%但样本量太小直接熔断会误伤正常任务。所以必须等请求量达到 10 个之后才参考失败率。这在 Agent 场景里尤其重要因为很多任务链里单个 API 的调用频率并不高可能要过几分钟才能攒够采样量。预算消耗速率也需要特别注意。我遇到过一种情况Agent 每一步都成功返回没有任何失败发生但它每一步都在用越来越大的上下文单步 Token 消耗从 1 万涨到 50 万整条链路的消耗速度飙升。这种异常失败率统计根本抓不到必须单独监控预算燃烧速率一旦当前窗口的消耗速度超过历史均值的三倍直接打开熔断。3.3 半开探测与恢复策略熔断不能一断了之熔断器打开之后不能永远关着也不能到了时间就立刻全量放行。我采用半开探测策略熔断打开后经过一段冷却时间通常是 30 秒到 2 分钟进入 HALF_OPEN 状态放少量请求进来试探下游是否恢复。半开状态的请求数量必须有上限我这边默认是 2 个同时对试探请求的结果做严格判断只要有一个失败立刻重新回到 OPEN 状态并且冷却时间翻倍形成一种指数退避。如果两个试探请求都成功才把状态切到 CLOSED并且清空滑动窗口里的累计数据重新开始统计。这里有个编排器场景特有的问题熔断的对象是任务路径还是具体 Agent还是具体工具。我建议做三级熔断。第一级按工具熔断比如某个搜索 API 连续报 500那只要调用这个工具的 Agent 都会快速失败第二级按 Agent 类型熔断比如某个总结 Agent 总是触发一次性消耗暴跌就单独熔断它第三级按模型供应商熔断比如某个供应商的接口大面积超时所有走它家 API 的任务全部快速失败。三级之间逐层收窄既不影响无关任务又能精准止血。4. 和成本控制联动预算信封、预扣与动态降级4.1 预算信封与按步预扣慢一步钱就没了熔断器处理一直在错预算守卫处理太贵了。预算控制不能等钱花完了再报警那已经晚了必须让每个 Agent、每条任务链都拥有一个预算信封Budget Envelope并且按步预扣、按步结算。预扣的意思是任务执行某一步之前先估算这一步可能消耗的 Token 数比如以 max_tokens 为准从预算池里冻结这部分额度。执行完拿到实际用量后结算实际消耗再把剩余额度解冻退回。这个机制防住的正是那种看起来每一步都没超预算但总体预算被慢慢啃光再突然崩掉的场景。举个例子。某个子 Agent 任务链的预算是 100 万 Token它每一步的 max_tokens 是 4000。它要是死循环跑 300 步每步都预扣 4000那么第 250 步左右预算池就会降到 0。预算守卫在做第 251 步预扣时发现余额不足就直接触发熔断并终止整个链路。这样即便看门狗的超时判断还没把它揪出来预算已经把上限卡死了。4.2 Token 估算误差与动态配额调整预扣机制的问题在于估算误差。max_tokens 只是上限实际输出往往远小于这个值。如果每步都按 max_tokens 预扣会导致预算池很快被冻结完大量冻结额度在每步结算后才慢慢释放实际上很多任务根本用不了预定那么多。我的处理方式是引入一个估算因子。预扣额 该步预计消耗 × 估算因子估算因子默认是 1.2代表稍微多预留一点余量。同时统计每个 Agent 的历史实际消耗 / max_tokens比例比如某个 Agent 平均只用掉 max_tokens 的 60%那它的估算因子就可以动态调到 0.8。这样既留有余量又不会过度冻结。动态调这个因子时需要格外注意一次都不能低估。宁可多冻结也不能少冻结否则可能出现实际消耗超过了剩余预算的透支事故。所以我给估算因子设了硬下限 1.0除非有极其充分的历史证据否则不会低于 1.0。预算安全优先于资源利用率。4.3 熔断后的降级路径换模型、走缓存、给用户兜底熔断和预算触发的瞬间编排器不能简单地把错误甩给用户而是应该走降级路径。我在这套编排器里实现了一个三级降级机制按优先级从高到低排列。第一级走缓存。同一个问题如果之前跑过直接把历史答案取出来不再调大模型。这在对话型 Agent 里很有效。第二级换模型。主模型是大参数旗舰模型预算触发后自动切换到小参数模型虽然回答质量会降一点但费用可能降到十分之一。第三级是简化流程把原本要派给 5 个子 Agent 的任务链临时压缩成一个 Agent 单步完成用更少的步骤把结果凑出来。降级不是无脑执行的每一步都要和预算守卫确认降级后的预算是否够用。比如从旗舰模型切到小模型后预算池可能立刻多出几千块可用额度任务就能继续如果小模型也不够就放弃执行把预算不足且已降级这个状态明确告诉调用方绝对不能硬撑着跑。5. 落地实现核心数据结构与状态机代码5.1 看门狗实现TTL 重置与超时回调看门狗的核心就是一个带过期时间的注册表以及一个定期扫描的定时器。我把它设计成不依赖任何框架的独立模块这样你在任何编排器里都能直接用。关键代码大致是这样的interface Heartbeat { taskId: string; step: number; // 当前推进到的步骤编号 status: string; // RUNNING | WAITING | FINISHED timestamp: number; // 本次心跳时间 } class TaskWatchdog { private registry new Mapstring, WatchdogEntry(); // 每次收到心跳重置该任务的后台到期时间 heartbeat(hb: Heartbeat) { const entry this.registry.get(hb.taskId); if (!entry) return; if (hb.step entry.lastStep entry.silentCount 2) { // 连续多次心跳步骤没有推进触发“假活”检测逻辑 this.markStalled(hb.taskId, step stuck at ${hb.step}); return; } entry.lastStep hb.step; entry.deadlineAt Date.now() entry.timeoutMs; entry.silentCount hb.step entry.lastStep ? entry.silentCount 1 : 0; } // 定时扫描所有任务发现超过 deadlineAt 的任务就回调处理 private scan() { const now Date.now(); for (const [taskId, entry] of this.registry) { if (now entry.deadlineAt) { this.onTimeout(taskId, entry); } } } }这里最关键的是silentCount。我第一次实现时只判断有没有心跳结果被一个while(true)里的 Agent 坑惨了它每 200 毫秒就发一个心跳但步骤永远是同一个数字导致永远不超时。加了步骤变化检测之后才真正把假活给揪出来。定时器我建议用独立的setInterval间隔设在 1 到 2 秒。扫描间隔不能太短否则在高并发下会反复遍历大 Map 消耗 CPU也不能太长否则超时判定延迟明显。另外回调函数onTimeout里绝对不能做耗时操作只做标记和投递事件真正 cancel/kill 的逻辑放到异步任务池里执行避免看门狗自身的定时器被堵住。5.2 熔断器实现环形滑动窗口与三态迁移熔断器的实现核心是滑动窗口计数器和状态机。我用一个环形数组来实现滑动窗口数组长度对应窗口秒数每秒一个格子新数据覆盖最旧的数据。class CircuitBreaker { state: CLOSED | OPEN | HALF_OPEN CLOSED; private window new Array(60).fill(null).map(() ({ success: 0, failure: 0, tokens: 0 })); private cursor 0; private openedAt 0; record(result: success | failure, tokens: number) { const bucket this.window[this.cursor]; result success ? bucket.success : bucket.failure; bucket.tokens tokens; this.evaluate(); } private evaluate() { if (this.state OPEN) return; const agg this.aggregate(); const total agg.success agg.failure; const failureRate total 0 ? 0 : agg.failure / total; // 最小采样量保护 if (total 10) return; if (failureRate 0.5 || agg.failure 5) { this.state OPEN; this.openedAt Date.now(); this.emit(breaker.open, { reason: failure_rate_high }); } } }evaluate在每次调用完成后执行开销很小。要注意把窗口滚动和状态迁移做成同一个锁保护的区域否则并发下会出现窗口已经滚动但状态还是旧数据算出来的情况。单机版本用互斥锁就够了分布式版本我把这个模块做成了独立服务用 Redis 存窗口数据。5.3 事件总线让整条链路感知熔断看门狗触发了、熔断器打开了、预算要到顶了这些事件不能只停留在各自的模块内部必须广播给整条编排链路。我用一个轻量级事件总线来做这件事核心就是发布订阅模式。type EventMap { task.stalled: TaskEvent; breaker.open: BreakerEvent; budget.exhausted: BudgetEvent; } bus.emit(breaker.open, { agentId, toolName, failureRate, tokens }); bus.on(breaker.open, (e) { costGuard.freeze(e.agentId); taskScheduler.cancelChildren(e.agentId); alarm.notify(Agent ${e.agentId} 触发熔断); });事件总线的好处是解耦各守护模块。看门狗不需要知道预算守卫的存在它只管发task.stalled事件预算守卫也不需要理解熔断器的统计原理收到事件后冻结相关 Agent 的额度就行。这样整套守护层的模块可以独立演进我之前把每个模块互相硬编码调用结果改一个就要连带改三个重构一次掉一层皮。5.4 用 FakeAgent 做验证死循环、慢调用、突发预算写代码容易验证难。我做了一套专门用来拆台的 FakeAgent每个测试用例都故意制造一种失控行为验证守护层能不能在预期时间内按下停止键。第一种 FakeAgent 是死循环型它每执行一步都上报心跳但步骤编号永远不变。验证目标是看门狗能在silentCount 2之后的合理时间内触发task.stalled。第二种是慢调用型模拟一个 API 调用 5 分钟不返回验证单步超时是否生效。第三种是预算爆破型每一步都消耗双倍于预估的 Token验证预扣机制能不能在预算池见底前截断任务。每次跑完测试我都会重点检查两个东西熔断后有没有立刻放新的请求进来、看门狗 kill 任务后有没有留下孤儿子线程。这两个问题用普通功能测试很难暴露只有把 FakeAgent 跑上几十轮配合线程和请求计数才能看出来。6. 常见问题与避坑实录实战篇6.1 误杀率太高P95 调参法和优雅宽限我最初给看门狗设超时用的是平均延迟结果误杀率非常高。核心原因是大模型接口的延迟分布极其不均匀偶尔一次调用会飙到平均值的 10 倍但它是正常波动。后来我改成用 P95 延迟来做基准采集最近 1000 次调用的耗时分布超时时间 P95 × 3 或者 P95 30 秒取两者中的较大值。即便如此任务启动阶段还是有误杀风险。所以我加了优雅宽限机制任务刚创建的前 30 秒内看门狗只记录不执行超时回调哪怕已经过了 deadline 也等宽限期结束再判。这是因为 Agent 冷启动时可能要加载工具、初始化上下文心跳上报比运行时慢是正常的。调参还有个原则宁可多等 5 秒不要提前 kill 一个正常任务。看门狗误杀导致的损失往往比多等几秒更严重。一个正常任务被误杀它的所有子任务都要跟着取消整条链路白跑这个代价通常比超时消耗的几秒高得多。6.2 分布式场景下熔断状态不一致怎么办单机版把熔断器状态放内存没有问题但编排器一旦横向扩成多副本就会出现状态不一致副本 A 已经熔断了某个模型供应商副本 B 还在疯狂往同一个供应商发请求。这个问题我用单机版真没料到直到压测时跑了 8 个副本才发现。分布式方案我后来是做成 Sidecar 模式的每个编排实例里的熔断器不再本地计算状态而是把 record 请求发到独立的守护服务由守护服务统一统计窗口数据并返回放行还是熔断的决策。这样状态只有一份但会引入额外网络开销请求量高的时候守护服务本身可能成为瓶颈。折中方案是每实例本地算一份状态同时定期和守护服务对账允许短暂的最多 10 秒不一致窗口。如果你也在做多副本编排器我的建议很直接先想清楚你能容忍的错误放行请求量是多少。10 秒不一致意味着熔断后最多还有几万个请求漏过去如果你对成本敏感就应该上 Sidecar 模式如果不敏感本地状态加对账能省很多维护功夫。6.3 快速定位肇事 Agent链路 ID 与采样日志一旦熔断触发排在第一位的问题永远是到底是哪个 Agent 烧的钱。如果日志里没有链路 ID这个问题会浪费你整个下午。我在编排器里给每一次用户请求分配全局 TraceID下游所有 Agent 的日志都带上这个 ID同时每一步记录 Agent 名称、模型名称、输入 Token、输出 Token、耗时。这样排查时只需要按 TraceID 过滤就能看到整条任务链每一环的消耗明细。预算异常这种问题光有链路 ID 还不够。我做了一个消耗 TopN 看板实时统计当前窗口内消耗最多的 10 条任务链并且每分钟记录一次快照。熔断出现后直接看最近 5 分钟的快照一眼就能看出哪个 Agent 的 Token 消耗曲线是 45 度上扬的。这套东西调试时帮了我大忙比对着日志翻感受直观太多。6.4 预算守卫的精度陷阱预扣、结算与信用额度预算守卫最隐蔽的坑是结算延迟。大模型接口返回后实际 Token 数要解析响应体才能知道而解析本身需要时间如果任务在结算完成前就被 kill可能有一笔预扣的额度永远没被释放导致预算池凭空少一块。我解决这个问题用了两级预算模式。第一级是硬预算任务链总预算的 90% 作为熔断线到达即触发熔断第二级是信用额度剩下 10% 预留给已预扣但尚未结算的额度。每次结算完成后信用额度里的份额会动态调整。这样即使有未结算的任务也不会因为精度问题误伤正常流量。还有一次我碰到预算没超但任务莫名被中止的诡异事故排查到最后发现是并发更新预算池时出现了负数余额原因是我用了a a - b而不是原子操作。所有预算池的扣减都必须用原子操作或者加锁这个细节在并发量上来之后会变成大事故。我把所有预算相关的方法都改成原子自减之后这个事故就再也没出现过了。我个人在做完这套守护层之后最大的感受是Agent 编排器在功能上做得再花哨都不如在失控时能不能及时刹车这个问题上多花心思。看门狗、熔断器、预算守卫这三件套看起来都是老生常谈的模式但真正放进 Agent 场景里就会冒出假活心跳预算消耗速率熔断按步预扣这些新问题。如果把 Agent 比作一匹快马那这套守护层就是缰绳和马嚼子——跑得快很重要但能随时刹住才是敢让它跑起来的前提。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑