Grok Agent 有界循环
Agent 循环出事故的样子很一致:不是报错,而是一直在跑。模型调工具、拿到结果、再调、再拿——每轮都「合理」,合起来却是一个没有出口的循环。等发现时,账单已经翻了几倍,而日志里只有一串工具名。
这一节把循环关进笼子:步数、时间、预算三重上限,每轮都检查,任一触发就停。
一、问题不是「会不会死循环」,而是「谁负责喊停」
模型本身没有「该停了」的概念。它每轮都在做局部最优:这个问题还缺信息,那就再调一次工具。全局的「再调下去不值得」必须由调用方判断。
所以有界循环的核心不是加一个计数器,而是在循环入口处定义三种「不值得继续」:
- 步数:工具调用次数到顶,说明任务复杂度超出预期。
- 时间:总耗时到顶,说明用户在等,或上游在超时。
- 预算:累计成本到顶,说明这次任务的投入产出已经不划算。
三种上限对应三种事故:失控、超时、烧钱。只设一种,另外两种照样会来。
二、最小可运行示例:三重上限的循环骨架
下面的脚本不联网、不需要真实密钥。它用一段 mock 的「计划」模拟每轮工具调用,演示三种上限各自怎么终止。
"""有界循环(mock):步数、时间、预算三重上限,任一触发即停。"""
from dataclasses import dataclass
@dataclass
class Limits:
max_steps: int = 5
max_seconds: float = 30.0
max_usd: float = 0.05
def run(plan: list[dict], limits: Limits) -> tuple[str, int, list[str]]:
steps, spent, elapsed, log = 0, 0.0, 0.0, []
for item in plan:
if steps >= limits.max_steps:
return "stopped:steps", steps, log
if elapsed + item["seconds"] > limits.max_seconds:
return "stopped:time", steps, log
if spent + item["usd"] > limits.max_usd:
return "stopped:budget", steps, log
steps += 1
elapsed += item["seconds"]
spent += item["usd"]
log.append(item["name"])
return "done", steps, log
PLAN = [{"name": f"step{i}", "seconds": 5.0, "usd": 0.005} for i in range(1, 7)]
def offline_check() -> None:
cases = [
("默认上限", Limits()),
("时间上限", Limits(max_steps=10, max_seconds=12.0, max_usd=1.0)),
("预算上限", Limits(max_steps=10, max_seconds=100.0, max_usd=0.012)),
("全放开", Limits(max_steps=10, max_seconds=100.0, max_usd=1.0)),
]
for label, lim in cases:
reason, steps, _ = run(PLAN, lim)
print(f"{label}:{reason} 步数={steps}")
if __name__ == "__main__":
offline_check()
真正上线时,plan 换成「模型返回 → 执行工具 → 回传结果 → 再问模型」的真实循环,三个计数在每轮开头累加。判断放在调用模型之前,才能在花钱之前停住。
三、三重上限的触发与终止动作
| 上限 | 触发条件 | 终止动作 |
|---|---|---|
| 步数 | 工具调用次数 ≥ max_steps |
停止并返回已完成部分 |
| 时间 | 累计耗时 ≥ max_seconds |
停止并标记超时 |
| 预算 | 累计成本 ≥ max_usd |
停止并告警 |
三件要强调的事:
- 终止不是失败。有界循环停在步数上限,应当返回「已完成的部分 + 未完成的原因」,而不是抛一个异常。用户需要知道任务做到哪了。
- 判断要前置。把上限检查放在「调用模型之前」,而不是「收到结果之后」——后者已经花掉了这一轮的钱。
- 三种上限要同时存在。只有步数,遇到「每步都很慢」照样超时;只有时间,遇到「每步都很快但很多」照样烧钱。三者互补,不是冗余。
四、预期输出与异常分支
运行循环骨架,预期输出:
默认上限:stopped:steps 步数=5
时间上限:stopped:time 步数=2
预算上限:stopped:budget 步数=2
全放开:done 步数=6
换成真实 Agent 时,若出现异常,按下面顺序定位:
| 现象 | 优先怀疑 | 第一步动作 |
|---|---|---|
| 循环跑了几十轮 | 没有步数上限 | 加计数器并前置判断 |
| 单次任务超时被杀 | 没有时间上限 | 在循环入口累加耗时 |
| 账单突然上升 | 没有预算上限 | 按单次任务设成本上限 |
| 停了但没结果 | 终止时丢弃了上下文 | 返回已完成部分与原因 |
最后一行尤其重要:停在半路时,「什么都没有」和「做了一半并说明原因」是完全不同的体验。有界循环的价值不只是省钱,还包括可控地交付一个部分结果。
五、判据卡
| 判断项 | 该这么做 | 不该这么做 |
|---|---|---|
| 上限设置 | 步数 + 时间 + 预算三者并存 | 只设一个计数器 |
| 判断时机 | 调用模型之前 | 收到结果之后 |
| 终止行为 | 返回部分结果与原因 | 抛异常或静默退出 |
| 上限取值 | 按任务类型分别定 | 全站一个固定值 |
| 监控 | 记录触发次数与原因 | 只在事故后回看账单 |
这张卡的重点是三种上限互补。五条里有三条在说:不要用一个维度去覆盖三类事故。
六、来源与核验
- 官方 Tools Overview:
https://docs.x.ai/developers/tools/overview(核验日期 2026-11-04)。工具调用由 token 用量与调用次数两部分计价、一次请求可能触发多次工具调用的口径来自该页。 - 官方 Function Calling:
https://docs.x.ai/developers/tools/function-calling(核验日期 2026-11-04)。五步调用流程与工具回传方式以该页为准;官方 JavaScript 示例用stopWhen: stepCountIs(5)体现步数上限,可作为「官方也有此概念」的旁证。 - 本节的三重上限模型与循环骨架为本书原创的工程框架,具体阈值需按你的任务、模型与预算实测确定。
七、动手试
- 给你的 Agent 设三组阈值(保守 / 标准 / 宽松),分别写清适用场景,并说明为什么「全站一个值」不合理。
- 把
run()改成「终止时返回部分结果」的版本:除了原因与步数,再带上已完成步骤的摘要。 - 设计一条监控指标:当「步数上限触发率」在某天突然上升时,你怀疑是提示词变了、工具变慢,还是任务变复杂?写出你的排查顺序。