codecamp

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) 体现步数上限,可作为「官方也有此概念」的旁证。
  • 本节的三重上限模型与循环骨架为本书原创的工程框架,具体阈值需按你的任务、模型与预算实测确定。

七、动手试

  1. 给你的 Agent 设三组阈值(保守 / 标准 / 宽松),分别写清适用场景,并说明为什么「全站一个值」不合理。
  2. 把 run() 改成「终止时返回部分结果」的版本:除了原因与步数,再带上已完成步骤的摘要。
  3. 设计一条监控指标:当「步数上限触发率」在某天突然上升时,你怀疑是提示词变了、工具变慢,还是任务变复杂?写出你的排查顺序。
Grok 未知工具白名单
Grok 内置工具怎么选
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }