3.2 终止条件与失败处理


3.2 终止条件与失败处理

本节摘要:主循环的工程含量不在"转起来",在"停得下来、坏了不雪崩"。终止分两大类:正常终止(模型不再请求工具、给出最终答案——注意要防"假完成",模型有时会提前宣布成功)与异常终止。异常终止有四类触发器:限额触发(最大步数、token 预算、成本上限、单步超时)、空转检测(连续 N 步无实质进展,如重复调用同一工具同参数)、错误风暴熔断(连续失败超过阈值即熔断,防止"报错→重试→更怪的错误→更频繁重试"的雪崩)、外部取消(用户 Ctrl-C、上游任务超时)。失败后的最后一课是"优雅放弃":与其静默返回半成品,不如生成一份结构化交代——做到了哪、卡在哪、建议人接手做什么。本节给出熔断器的参考实现。

学习目标

阅读完本节,你应当能够:

  1. 列出四类异常终止触发器并各举一例。
  2. 实现空转检测与错误风暴熔断器。
  3. 说明"假完成"的成因与最小防线。
  4. 写出优雅放弃的报告结构。

一、为什么终止是工程问题

一个没有终止设计的循环,失败模式全在账单和事故报告里(以下均为示意场景):

事故 循环行为 根因
空转烧钱 连续 37 次调用 read_file("app.py") 无空转检测
错误风暴 工具报错→模型换姿势再试→再报错→……步数耗尽 无熔断,把"重试"全留给模型自由发挥
假完成 测试还没跑,模型宣布"已修复" 无完成校验
僵尸任务 卡在一个 30 分钟不返回的工具调用上 无单步超时

**核心认识:模型不是一个可靠的停机方。**它是概率机器,在"差不多了"和"再试试"之间的选择并不稳定——停机的责任必须由 harness 的确定性代码承担。这就是本节全部内容的动机。

二、四类终止条件

正常终止:模型无 tool_calls 且给出最终答案(3.1 节已实现) │ 防线:完成校验(能验就验——跑测试、查文件存在性),防"假完成" ▼ 异常终止四类触发器 ├─ ① 限额触发:max_steps / token 预算 / 成本上限 / 单步超时 ├─ ② 空转检测:连续 N 步"无实质进展" ├─ ③ 错误风暴熔断:连续 M 次工具失败(或失败率超阈值) └─ ④ 外部取消:用户中断 / 上游超时 / 平台关停信号

① 限额是保底,必须硬编码在循环里(mini_loop.py 已有 max_steps)。单步超时常被遗忘:给每个工具执行加超时(如 120s),否则一个挂死的命令就能拖住整个会话。

② 空转检测的实用判据是"重复签名":把每步的工具调用记为 (工具名, 参数哈希),连续 N 步签名相同或签名集合不再变化,即判空转。注意与合法重试区分——同一签名连续出现 2 次可能是合理重试,5 次就是死循环(阈值示意,按任务调)。

③ 熔断见下节。

④ 外部取消要求循环每拍开头检查取消标志,且工具执行要能被中断——这在终端形态(Ctrl-C)里容易,在常驻形态(第 2.2 节 OpenClaw 类)里要专门设计。

三、错误风暴与熔断器

错误风暴的机理:工具失败 → 模型看到错误信息 → 尝试修复性重试 → 由于上下文已被污染(历史里堆满失败记录),决策质量下降 → 更容易失败。每一步失败都在消耗上下文预算与模型判断力,失败是会复利的

熔断器的参考实现(写法示意,以实际工程为准):

# breaker.py —— 错误风暴熔断器(写法示意,以实际工程为准) class Breaker: def __init__(self, max_consecutive: int = 3, cooldown_steps: int = 2): self.fails = 0 # 连续失败计数 self.max_consecutive = max_consecutive self.cooldown = cooldown_steps def record(self, ok: bool) -> str | None: """每次工具执行后调用;返回 None 表示正常,返回字符串表示熔断动作。""" self.fails = 0 if ok else self.fails + 1 if self.fails >= self.max_consecutive: self.fails = 0 return f"连续失败 {self.max_consecutive} 次,熔断:暂停工具执行 {self.cooldown} 步" return None def tripped(self) -> bool: return self.fails >= self.max_consecutive

熔断后的三级响应(从轻到重):

  1. 降级:回填一条结构化消息("连续失败,请先复述当前目标,再换一种方法"),迫使模型跳出当前思路——上下文的"重置拍"。
  2. 换路:切备用工具/备用模型(判断成本很低时甚至可以让判断原语来选哪条路,见第 4.2 节)。
  3. 交还人:生成优雅放弃报告,终止任务。

💡 熔断不是"失败",而是 harness 对模型的元反馈:模型没有天然的"我已经连环撞墙"的自我意识,熔断器替它数着,并在适当时机把"你该停了"作为观察喂回去。这是第 8 章 Hook 反馈回路在循环内部的第一个实例。

四、假完成与优雅放弃

假完成:模型说"已修复"但测试没跑、甚至改动根本没写进文件。最小防线是完成校验——任务有客观验收标准(测试、命令输出、文件状态)时,在终止前用确定性代码验一遍;验不过,把证据回填("你说修好了,但 pytest 仍报 3 个失败,输出如下……"),让模型继续。这一个小改动对编码智能体的实际可靠性的提升,常常超过换一个更强的模型(社区经验)。

优雅放弃:所有终止路径(限额、熔断、取消)最终都汇到同一个出口——给用户一份结构化交代,而不是静默半成品:

{ "status": "aborted", "reason": "max_steps_reached", "steps_used": 30, "completed": ["已定位 bug 在 auth.py:42", "已修改一处类型错误"], "blocked_at": "运行 pytest 时连续 3 次超时", "suggest_next": "人工检查 pytest 超时是否与环境有关;改动已保存在工作区" }

一句话原则:放弃也是一种交付。用户拿到"做到哪、卡在哪、建议下一步",半成品的工作就没有白费;拿到一句 "Error: task failed",前面 29 步就全部沉没。

本节要点回顾

  1. 停机责任在 harness:模型不是可靠的停机方;四类异常触发器(限额、空转、熔断、取消)必须由确定性代码承担。
  2. 空转检测用重复签名(工具名 + 参数哈希),与合法重试用阈值区分。
  3. 错误风暴会复利:失败污染上下文、降低决策质量;熔断器三级响应——降级重述、换路、交还人。
  4. 假完成用完成校验防,优雅放弃用结构化报告交付——放弃也是一种交付。

循环的正向与反向路径都齐了。还剩第一拍"感知"里那个被反复推迟的问题:每一步到底给模型看什么?下一节讲上下文组装的时机与策略——循环质量的天花板,就压在这一格上。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U