第 42 章:Agent 超时与 Watchdog

“90 秒什么都没发生”还不是故障描述,除非你能说出循环当时究竟在等待什么。


快速通道(5 分钟掌握核心)

  1. 显式建模阻塞阶段;单看墙上时间无法区分卡死与正当工作
  2. 仅在等待远端 LLM 响应时累计空闲时间,包括最终的强制停止总结
  3. 每个阶段实例只报告一次轻度空闲;硬性空闲用类型化原因取消,并预留清理时间
  4. 流间隙 Watchdog 是另一只时钟——它在传输层内观察数据块间隔
  5. 阶段所有权一旦不可信,就停用计时动作并暴露结构性错误

公开源码快照:实现细节取自 Kocoro 公开仓库 origin/main4ec6772 提交,复核日期为 2026-07-27。章节讲的是不变量;具体常量应以当前源码为准。

42.1 从一只杀错工作的计时器说起

一个 Agent 要求图像服务渲染大型素材。八分钟很慢,但合理;工具拥有十分钟超时限制,而且仍在取得进展。

另一次运行中,LLM 流发出半句话后,TCP 连接陷入沉默。没有终止事件,没有错误,也没有新字节。此时等待八分钟不是耐心,而是卡死。

给两者套一只“整轮超时”计时器,就必须选择破坏哪次运行。90 秒会杀掉合法工具;十分钟会让死亡数据流长时间占用工作线程。每次出现任何活动就重置,又会让一个交替执行无用工作的循环永远活着。

问题不在于选出正确时长,而在于:不同组件拥有不同类型的等待

42.2 Watchdog 需要阶段模型

循环暴露显式 TurnPhase,而不是根据最近哪只计时器被触碰来猜测状态:

阶段计入 LLM 空闲?等待的所有者
init循环构建
setup本地初始化
awaiting LLM整轮 Watchdog
retrying LLM重试策略
compacting压缩协调器
awaiting approval审批界面与策略
executing tools工具专属超时
injecting message追问注入路径
force stop整轮 Watchdog
done

只有严格等待远端模型响应的阶段才计入空闲。Bash 命令、审批对话框或本地历史重写,从循环视角也可能很安静,但它们有不同所有者和不同的合法时长。

这样,每只超时计时器都只回答一个问题:

这个阶段由哪个组件承诺推进?什么观测证明它正在推进?

如果没有所有者,再加一只计时器只是掩盖设计缺口;如果两只计时器同时拥有同一段等待,它们迟早会对用户应看到哪个错误产生分歧。

42.3 Soft Idle 与 Hard Idle 是两个决策

公开快照的默认值是 Soft Idle 90 秒、Hard Idle 540 秒。这些数字描述一个具体传输栈,不代表普遍耐心。

Soft Idle 发出状态:这个阶段已经多久没有 LLM 活动。它不取消。每次阶段迁移都有单调递增的序号,同一阶段实例只触发一次。离开 awaiting_llm、重试,再重新进入 awaiting_llm,新的阶段实例可以再次报告。

Hard Idle 发出终态状态,并用 ErrHardIdleTimeout 取消运行。540 秒默认值比 600 秒传输上限少 60 秒,让取消信号有时间传播,也让清理能在外层 HTTP 自己放弃前完成。

这种分离对运维很重要。Soft 负责可见性,Hard 负责策略。可以只修改其中之一,而不必假装另一项也改变了。

类型化取消原因同样重要。用户取消和 Watchdog 取消都会关闭上下文,但不是同一种结果。调用方必须分别呈现“你停止了它”和“Provider 不再取得进展”;恢复逻辑也必须知道重试会不会违背用户意图。

42.4 流空闲是另一只时钟

整轮 Watchdog 知道循环在 awaiting_llm 里停留多久,却不拥有阻塞 SSE 响应体的内部机制。

因此网关在 CompleteStream 内部运行逐数据块计时器。每收到一行就重置;达到配置间隙的一半时记录警告,达到完整间隙时取消扫描器并关闭响应体。快照默认值是 90 秒。

它捕捉的是外层整轮计时器不擅长处理的故障:连接已经交付一些内容,却不再产生字节,也不返回错误。此时自动改用非流式模式重试同一个上游,可能只是再买一次完整的传输等待。因此循环把流间隙错误当成部分结果:保留已经收到的文本,如果一个字也没有则插入明确的占位文本,记录超出截止时间的状态,然后退出。

三只时钟回答不同问题:

  • 整轮空闲: 这个 LLM 阶段是否存在太久?
  • 流空闲: 这条传输最近是否产生过数据块?
  • 工具超时: 这个工具是否超出自己的执行契约?

不要让其中一只由另一只的事件重置。截图工具完成不能宽恕先前卡住的 Provider 数据流;一个 Provider token 也不能延长工具的截止时间。

42.5 嵌套调用仍需所有权

压缩是本地阶段,所以 Watchdog 不应计算整理历史的 CPU 工作。但压缩也会调用 LLM 来持久化经验或生成摘要;这段嵌套的远端调用确实需要覆盖。

阶段跟踪器用 EnterTransient(PhaseAwaitingLLM) 处理它,并返回一个幂等的恢复闭包:

restore := tracker.EnterTransient(PhaseAwaitingLLM)
summary, err := client.Complete(ctx, request)
restore()

外层阶段仍是 compacting;只有远端阻塞区间暂时归 LLM Watchdog 所有。忘记恢复,循环会停在错误阶段;完全不进入临时阶段,卡死的摘要器就会隐形。

这说明,把手工“暂停 Watchdog / 恢复 Watchdog”散落在业务逻辑里很脆弱。阶段拥有计时策略,嵌套工作借用阶段,再归还它。

42.6 阶段跟踪器说谎怎么办

这里有一个危险诱惑:把跟踪器称为“失败即关闭”,只要状态不一致就取消。

但不一致的跟踪器无法告诉你循环正在等待模型,还是合法地执行工具。根据坏状态行动,可能杀掉正当工作。快照选择了相反的生产取舍:任何结构性违规都会把跟踪器标为无效,记录警告,并让 Watchdog 观察器在本次运行剩余时间内停用。在测试或 SHANNON_PHASE_STRICT=1 下,同一违规会触发 panic,避免开发过程把它常态化。

因此行为是:

development / strict run: structural bug  panic
ordinary production run: structural bug  warning + watchdog disabled

这可能让真正的卡死继续存活;另一种选择却可能在证据不可信时取消一次合法、破坏性或昂贵的操作。两边都有代价。重要的是明确选择,并让失去的覆盖可见。

恢复闭包是幂等的,消掉一类所有权错误;退出断言能捕捉忘记结束的临时阶段;一个临时阶段尚未结束时再做顶层 Enter,本身也会构成违规。这些检查把“计时器感觉不对”变成具体的阶段嵌套错误。

42.7 超时是一种结果,不只是错误

Hard Idle 触发时,取消信号必须到达四个边界:

  1. 正在进行的 Provider 调用或数据流;
  2. 从流增量启动的任何推测性工具工作;
  3. 检查点与持久层,避免这一轮被错误标成完整;
  4. 调用方,并携带阶段与部分输出状态。

只返回 context canceled 不够。没有类型化原因,界面无法区分 Watchdog 与用户意图;没有部分文本,一个九分钟的回答会因为最后十秒卡住而全部消失;没有终态运行状态,第 40 章所述的持久层及其调用方,也无法区分因超时截断的一轮与正常完成。

同样,Soft 事件应包含活动阶段与实测空闲时长。“Agent slow”对运维人员毫无帮助;“No LLM activity for 90s, phase=force_stop”则明确指出最终总结调用是所有者。

Watchdog 应当限制沉默,而不是掩盖沉默。

42.8 快照证据

观察4ec6772 源码
单次 Agent 运行的显式阶段清单phase.go L12
只有 awaiting LLM 与 force stop 计入空闲phase.go L58
嵌套阻塞调用借用临时阶段phase.go L138
结构性违规令观察器失效,并警告或 panicphase.go L117
Watchdog 的 Soft/Hard 语义与跟踪器失效行为watchdog.go L19
快照默认值:Soft 90 秒、Hard 540 秒、流间隙 90 秒config.go L162
流响应体拥有自己的逐数据块空闲 Watchdoggateway.go L1144
流间隙退出时保留部分输出loop.go L3679

这些内容描述的是一个有日期的实现与传输栈,不是普遍的超时数值。

42.9 常见陷阱

整个轮次只有一只超时计时器。 它会杀掉合法工具,或对死亡的 Provider 等太久。

任何活动都重置计时。 无关进展可以让真正卡死的所有者永远活着。

把压缩全部排除。 本地包装层不计入空闲,但里面的摘要调用仍需 LLM 覆盖。

把审批等待计作 Agent 空闲。 那段停顿归人类所有;用模型计时器取消它是类别错误。

只有阶段名称,没有迁移标识。 再次进入 awaiting_llm 时永远无法重新触发 Soft 告警。

流间隙超时后自动回退。 同一个上游可能再次卡死,让延迟和成本翻倍。

根据无效的阶段数据取消。 所有权一旦不可信,计时器就不知道自己正在杀什么。

只返回通用取消错误。 调用方失去原因、阶段、部分输出与选择恢复方式的能力。

本章要点

  1. 时间有所有者。 只有位于已知阻塞阶段内,时长才有意义。
  2. Soft 是可见性,Hard 是策略。 两者应独立配置、独立观测。
  3. 流间隙需要传输层时钟。 整轮经过时间不能替代数据块间隔。
  4. 嵌套远端工作借用远端阶段。 阶段所有权胜过四处散落的暂停与恢复。
  5. 不可信的计时数据不能触发自信的行动。 暴露结构性错误,并有意识地选择生产取舍。

下一章:第 43 章处理另一种没有进展的状态:循环仍在活动、仍在产生事件,却始终原地踏步。

引用本文 / Cite
Zhang, Wayland (2026). 第 42 章:Agent 超时与 Watchdog. In AI Agent 架构:从单体到企业级多智能体. https://waylandz.com/ai-agent-book/%E7%AC%AC42%E7%AB%A0-Agent%E8%B6%85%E6%97%B6%E4%B8%8EWatchdog/
@incollection{zhang2026aiagent_42_-Agent_Watchdog,
  author = {Zhang, Wayland},
  title = {第 42 章:Agent 超时与 Watchdog},
  booktitle = {AI Agent 架构:从单体到企业级多智能体},
  year = {2026},
  url = {https://waylandz.com/ai-agent-book/%E7%AC%AC42%E7%AB%A0-Agent%E8%B6%85%E6%97%B6%E4%B8%8EWatchdog/}
}