Skip to content

停止条件、超时与失败边界 ​

学习目标 ​

  • 为 Agent Loop 设计可判定的成功、失败、暂停和预算停止条件。
  • 区分 MaxSteps、RetryBudget、Action Timeout 和 Run Deadline。
  • 把可恢复错误与不可恢复错误转换成稳定的 StopReason 和用户可读结果。

前置知识 ​

  • 已阅读本章前五篇,理解 Goal、Action、Observation、RunState 和 RunStatus。
  • 已了解 Tool 的副作用需要由 Runtime 控制;本文不依赖在线模型或外部服务。

为什么必须显式停止 ​

Goal Reached ​

Goal Reached 表示 Success Criteria 已被 Observation 证实。它是最优先的正常停止路径;模型说“完成了”不够,必须有状态或工具结果支持。

MaxSteps ​

MaxSteps 限制一次 Run 最多允许多少个可计费或可产生副作用的执行步骤。它是防止循环失控的硬边界,不是让模型自行决定何时停的提示。

RetryBudget ​

RetryBudget 限制同一个失败动作可以重试多少次。它应按动作或错误类别计数,不能把每次全局循环都当作新重试,否则一个坏工具会消耗所有预算或造成重试风暴。

Action Timeout ​

Action Timeout 限制单次 Model 调用或 Tool 执行的等待时间。它释放当前等待,但不必然撤销已经发给外部服务的副作用;取消能力和外部幂等性要另行设计。

Run Deadline ​

Run Deadline 是整个 Run 的绝对截止时间。即使每个动作各自没有超时,只要达到 Deadline,Runtime 也应拒绝开始下一步并返回 timeout。

Awaiting Approval ​

等待用户审批不是失败,也不是成功。Runtime 应进入 waiting 状态,保存待审批 Action 和过期时间,收到明确批准或拒绝后再恢复;不能让模型继续猜测审批结果。

阅读提示:审批等待只有收到明确批准才回到检查链;拒绝和过期分别进入终态。其他停止分支仍由 Runtime 根据可验证的 Observation、硬边界和预算决定。

错误的恢复边界 ​

可恢复错误 ​

可恢复错误是通过重试、补充输入或换用受限动作,有合理机会继续的错误,例如临时网络超时、缺少非敏感字段或工具返回暂时不可用。恢复策略必须有预算和退避,不能无限重试。

不可恢复错误 ​

不可恢复错误意味着当前 Run 不应继续执行,例如动作违反硬约束、工具不存在、权限明确拒绝、输出无法解析且已达到修复预算。Runtime 应保存原因并返回失败,而不是反复把同一个输入送给 Model。

不确定结果 ​

网络连接中断时,外部动作可能已经发生,也可能没有发生。此时不能简单当作失败后重试;应进入 unknown 或 waiting,先通过查询、幂等键或人工确认判断真实状态。

FailureBoundary ​

FailureBoundary 是决定“当前错误能否继续留在本次 Run 内处理”的边界。它把底层异常转换成三类结果:可以在预算内恢复、必须暂停等待外部确认、或立即结束并交给上层处理。边界的所有者应是 Runtime 或业务 Workflow,而不是 Model。

失败转换 ​

底层异常应转换成包含来源、类别、是否可恢复和安全提示的结构化错误。对用户显示简洁原因,对日志保留诊断上下文;不要把凭据、完整隐私数据或内部堆栈直接交给 Model。

常见失控模式 ​

空回答循环 ​

Model 连续返回空文本或缺少动作时,Runtime 应计入解析失败预算,并在超限后以 empty_output 停止。把空结果当作“继续问一次”会产生没有进展的循环。

重复动作循环 ​

相同输入反复触发同一个 Action,且 Observation 没有变化时,应识别为 no_progress 或 loop_detected。仅增加 MaxSteps 只能延后故障,并不会让系统取得新信息。

工具结果不一致 ​

工具声称成功,但随后查询不到预期状态,说明 Postcondition 未满足或外部状态最终一致。Runtime 应将其标为 inconsistent_result,避免继续根据成功假设执行下一步。

错误重试风暴 ​

多个层都实现重试时,单次调用可能被指数放大。RetryBudget 应有明确的所有者;子工具已经重试过的错误要携带 attempt 信息,父 Run 不应无条件重新开始整条流程。

一个带预算的本地运行器 ​

下面的代码用预先定义的动作序列模拟成功、重复无进展和预算耗尽,展示 StopReason 如何由 Runtime 判定;它不访问网络,也不执行危险操作。

python
from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class RunResult:
    status: str
    stop_reason: str
    output: str | None = None


def run_with_budget(observations: list[str], max_steps: int = 4, retry_budget: int = 1) -> RunResult:
    previous = None
    retries = 0

    for step_index, observation in enumerate(observations, start=1):
        if step_index > max_steps:
            return RunResult("failed", "max_steps")

        if not observation:
            retries += 1
            if retries > retry_budget:
                return RunResult("failed", "empty_output")
            continue

        if observation == previous:
            return RunResult("failed", "loop_detected")

        previous = observation
        if observation.startswith("done:"):
            # 成功条件:Observation 明确给出可交付结果。
            return RunResult("completed", "goal_reached", observation[5:])

    # 输出契约:没有成功条件且没有其他错误时,不伪造最终答案。
    return RunResult("failed", "max_steps")


print(run_with_budget(["working", "done:report ready"]))
print(run_with_budget(["working", "working"]))
print(run_with_budget(["", "", "done:late"], retry_budget=1))
# 输出:RunResult(status='completed', stop_reason='goal_reached', output='report ready')
# 输出:RunResult(status='failed', stop_reason='loop_detected', output=None)
# 输出:RunResult(status='failed', stop_reason='empty_output', output=None)

示例中的 observations 代表已经经过工具或模型边界的结果;真实 Runtime 还要用单调时钟检查 Deadline,并把异常转换成同一类 RunResult。成功、失败和预算耗尽都有可见的原因。

StopReason 的稳定分类 ​

StopReason 应使用有限、可枚举的分类,例如 goal_reached、max_steps、retry_exhausted、timeout、cancelled、awaiting_approval、tool_error、constraint_rejected 和 unknown_outcome。展示文案可以变化,分类不应依赖自然语言匹配。

用户提示与内部诊断 ​

对用户可说“在截止时间内未能确认部署状态,请稍后查询”;对日志应同时记录工具名、动作摘要、耗时和请求标识。两者的详细程度不同,但都不能捏造成功。

超时后的副作用 ​

超时只证明本地等待到达上限,不能证明远程动作被撤销。对付款、发布、删除等动作,要提供查询状态、幂等键或人工确认路径;没有这些能力时,应优先进入 unknown/waiting,而不是自动重试。

取消与失败的区别 ​

用户主动取消和工具自然失败是不同的 StopReason。取消应尽快阻止新的 Action,并把当前任务标为 cancelled;已经发出的外部请求是否停止,要由执行器和服务协议决定。第 07 篇会专门讨论取消与异步执行。

源码阅读时寻找失败边界 ​

找预算的真正拥有者 ​

搜索 max_steps、timeout、retry 或 deadline 等字段,确认它们是在 Model 调用前、Tool 调用前还是整个 Run 层检查。多个层各自计数时,计算总预算和重试倍增风险。

找异常转换点 ​

查看工具异常是否被转换成结构化结果、是否保留 cause、是否会触发下一轮 Model。若异常直接被 catch 后返回空字符串,后续很容易形成空回答循环。

找成功判定 ​

确认成功是由工具返回、状态查询、schema 校验还是模型文本触发。模型文本可以作为候选信号,但需要可验证的 Observation 或后置条件支持。

找最终出口 ​

任何 return、raise、event 或 callback 都可能是运行出口。检查它是否写入 RunStatus、StopReason 和最终结果引用,避免用户看到错误但状态仍显示 running。

与主线项目的对应关系 ​

smolagents ​

smolagents 的执行器通常需要限制迭代、解析失败和工具错误。阅读时重点看何时把模型输出视为最终结果、何时继续、何时抛出执行异常;具体配置名不要当作跨版本契约。

OpenAI Agents SDK ​

OpenAI Agents SDK 的 Runner、guardrail 和工具执行共同影响停止。源码阅读要区分模型完成、工具错误、转交和外部取消,并确认事件中是否能找到对应的 StopReason。

LangGraph ​

LangGraph 的图边可以通过条件限制循环,节点和执行器可承载超时、重试与中断。图上存在一条边不代表它必然安全;节点仍需处理外部副作用和不确定结果。

DeepSeek Harness ​

DeepSeek Harness 的 Driver、Hook 和事件机制可能加入审批、取消或失败传播。阅读时优先确认父 Run 是否会等待插件结果,以及插件失败是否被错误地降级成继续运行。

易混点 ​

  • 超时不是撤销:本地停止等待不等于远端副作用被回滚。
  • MaxSteps 不是 RetryBudget:前者限制总动作次数,后者限制同一失败路径的重试次数。
  • 取消不是失败:用户取消应保留 cancelled 语义,不要伪装成 tool_error。
  • 未知结果不是失败成功二选一:外部动作状态未知时应查询、等待或人工确认。
  • 模型说完成不等于 Goal Reached:成功必须对应可验证的 Observation 或 Postcondition。

课后小问 ​

  1. 远程发布请求超时后,为什么不能立即再发一次?

    答案:超时只说明本地没有及时收到结果,远程发布可能已经成功,第二次请求可能造成重复副作用。

    解析:应先用请求标识查询状态,或依赖幂等键保证重复请求安全;无法确认时进入 unknown/waiting 比猜测失败更可靠。

  2. 为什么空回答需要单独的停止原因?

    答案:空回答通常是解析或模型输出问题,和工具失败、用户取消的修复路径不同。

    解析:结构化 StopReason 让监控知道应检查模型响应、提示或解析器,而不是盲目增加工具重试。

本节小结 ​

  • Agent 必须同时定义成功、预算耗尽、超时、取消、等待和失败出口。
  • MaxSteps、RetryBudget、Action Timeout 和 Run Deadline 属于不同层级的限制。
  • 可恢复错误要有有限预算,不确定结果要查询或等待,硬约束失败不能自动放宽。
  • StopReason 是程序、日志和用户提示之间的稳定契约。

快速回顾 ​

  • 能说明 Goal Reached、MaxSteps、RetryBudget、Action Timeout 和 Run Deadline 的区别。
  • 能判断超时后何时重试、何时进入 unknown/waiting。
  • 能为解析失败、工具异常、循环无进展和用户取消选择不同 StopReason。
  • 下一篇阅读 同步、异步、取消与流式执行,把时间边界扩展到任务调度和实时输出。