跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
编程语言Agent RuntimeAgent LoopEvalMax IterationStop Reason

《Agent Runtime 工程化》给 Agent Loop 加 Max Iteration

给 Agent Loop 加 Max Iteration 昨天我们手写了一个 100 行 Agent Loop。 今天给它加 max iteration。 很多教程会把这一步写得很轻: 这当然要写。 但如果你的理解停在这里,max iter

陈堂会发布于 —2 浏览
《Agent Runtime 工程化》给 Agent Loop 加 Max Iteration

给 Agent Loop 加 Max Iteration

昨天我们手写了一个 100 行 Agent Loop。

今天给它加 max iteration。

很多教程会把这一步写得很轻:

for (let i = 0; i < maxSteps; i++) {
  // call model
  // call tools
}

这当然要写。

但如果你的理解停在这里,max iteration 就只是一个计数器。真正上线后,它会变成用户看到的那句:

Agent stopped due to max iterations.

这句话很熟。LangChain、LangFlow、n8n、各种 agent 项目的 issue 里都能看到类似问题。Agent 一直尝试工具调用,最后被最大迭代次数拦住。用户不知道它为什么停,工程师也不一定知道。

所以今天要讲的不是'怎么加一个 for 循环'。

是 Agent Loop 到底应该怎么停。

maxSteps 是保险丝,不是诊断报告

没有 maxSteps 的 Agent Loop 是事故入口。

模型可能重复读同一个文件。可能在工具失败后换个参数继续试。可能一直调用搜索工具。也可能拿到无用 observation 后继续绕圈。

maxSteps 的作用,是防止它无限跑下去。

但保险丝跳了,不等于你知道哪里短路。

如果最终只返回:

达到最大迭代次数。

用户什么也学不到。工程师也只能翻日志。

更好的做法,是把停止原因拆开:

type StopReason =
  | "final"
  | "max_steps"
  | "repeated_error"
  | "repeated_tool_call"
  | "no_new_information"
  | "budget_exhausted"
  | "permission_denied"
  | "user_cancelled";

max_steps 仍然保留,但它不应该吃掉所有问题。

Agent 连续三次读同一个不存在的文件,不该最后只说 max_steps。它应该停在 repeated_error。Agent 连续调用同一个工具、同一组参数,也不该跑满 20 步才停。它应该更早触发 repeated_tool_call。

最小实现:先把 stop reason 写进 state

昨天的 RunState 可以扩一下:

type StopReason =
  | "final"
  | "max_steps"
  | "repeated_error"
  | "repeated_tool_call";

type RunState = {
  userGoal: string;
  messages: RuntimeMessage[];
  steps: RunStep[];
  finalAnswer?: string;
  stopReason?: StopReason;
  sameErrorCount: number;
  toolFingerprints: Map<string, number>;
};

工具调用要能生成 fingerprint:

function stableJson(value: unknown): string {
  if (value === null || typeof value !== "object") return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;

  return `{${Object.entries(value as Record<string, unknown>)
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([key, val]) => `${JSON.stringify(key)}:${stableJson(val)}`)
    .join(",")}}`;
}

function toolFingerprint(call: ToolCall) {
  return `${call.name}:${stableJson(call.input)}`;
}

不要直接 JSON.stringify()。对象 key 顺序不稳定时,两个相同语义的输入可能生成不同字符串。

shouldStop 不要散落在 loop 里

停止判断最好集中成函数。

function shouldStopBeforeStep(state: RunState, maxSteps: number) {
  if (state.steps.length >= maxSteps) {
    return { stop: true as const, reason: "max_steps" as const };
  }

  if (state.sameErrorCount >= 3) {
    return { stop: true as const, reason: "repeated_error" as const };
  }

  for (const count of state.toolFingerprints.values()) {
    if (count >= 3) {
      return { stop: true as const, reason: "repeated_tool_call" as const };
    }
  }

  return { stop: false as const };
}

然后 loop 每一轮开始前先问它:

for (;;) {
  const decision = shouldStopBeforeStep(state, maxSteps);
  if (decision.stop) {
    state.stopReason = decision.reason;
    return state;
  }

  // build context
  // call model
  // execute tools
}

这比在 loop 里到处写 if 稳。后面加 token budget、用户取消、权限拒绝、run timeout,也都放进这个函数。

repeated_error 怎么算

最简单的算法:连续错误码相同,就累计。

function updateErrorCounter(state: RunState, results: ToolResult[]) {
  const errorResults = results.filter((r) => r.status === "error");

  if (errorResults.length === 0) {
    state.sameErrorCount = 0;
    return;
  }

  const firstCode = errorResults[0].errorCode ?? "UNKNOWN_ERROR";
  const allSame = errorResults.every((r) => (r.errorCode ?? "UNKNOWN_ERROR") === firstCode);

  state.sameErrorCount = allSame ? state.sameErrorCount + 1 : 1;
}

这只是入门版。

更好的实现会把工具名、错误码、关键参数一起算进去:

function errorFingerprint(result: ToolResult) {
  return `${result.toolName}:${result.errorCode ?? "UNKNOWN_ERROR"}`;
}

比如模型连续三次调用 read_file({ path: "src/foo.ts" }),都返回 NOT_FOUND,那就该停。

不要让模型继续猜。

repeated_tool_call 怎么算

每次执行工具前,记录 fingerprint:

function recordToolCall(state: RunState, call: ToolCall) {
  const fp = toolFingerprint(call);
  const count = state.toolFingerprints.get(fp) ?? 0;
  state.toolFingerprints.set(fp, count + 1);
}

如果同一个工具、同一组参数出现 3 次,通常说明 observation 没有给模型足够新信息,或者工具描述误导了模型。

这时候继续跑,意义不大。

停止后应该给用户一个能理解的解释:

我停止在第 6 步,因为连续 3 次调用 read_file(path="src/foo.ts") 都返回 NOT_FOUND。
建议先确认文件路径,或让我重新列出 src 目录。

这比'max iterations reached'好太多。

maxSteps 应该怎么设

不要上来就设 50。

不同任务应该有不同默认值:

问答 / 摘要:3-5 步
读代码定位入口:6-10 步
小型修复:10-15 步
跨文件重构:20+ 步,但必须有预算和 checkpoint

更重要的是,maxSteps 应该和成本、权限、checkpoint 一起看。

一个允许写文件、跑测试、联网、安装依赖的 Agent,maxSteps 不应该和只读巡检 Agent 一样。每多一步,都可能多一次工具副作用和一轮模型成本。

所以配置不要只写:

maxSteps: 20

至少写成:

type LoopBudget = {
  maxSteps: number;
  maxModelCalls: number;
  maxToolCalls: number;
  maxRunMs: number;
  maxRunTokens: number;
};

下一篇会专门讲 token budget。这里先记住:step 是成本单位,也是风险单位。

stop reason 要进入 trace

最小 trace 里应该记录:

{
  "runId": "run_123",
  "maxSteps": 8,
  "steps": [
    {
      "index": 1,
      "toolCalls": [
        {
          "name": "read_file",
          "fingerprint": "read_file:{\"path\":\"src/foo.ts\"}"
        }
      ],
      "toolResults": [
        {
          "status": "error",
          "errorCode": "NOT_FOUND"
        }
      ]
    }
  ],
  "finished": {
    "reason": "repeated_tool_call"
  }
}

这份 trace 不需要华丽。它要能回答三个问题:

模型为什么继续?
工具是否带来新信息?
runtime 为什么停止?

如果 trace 里只有聊天记录,max iteration 问题很难复盘。模型会解释得头头是道,但你要看的是系统事实。

eval 也要测停止条件

很多人写 eval,只测'最后回答对不对'。

Agent Runtime 还要测'有没有及时停'。

至少写几个 golden task:

case 1:fake model 一直调用 unknown_tool。
期望:runtime 以 repeated_error 停止。

case 2:fake model 三次读取同一个不存在文件。
期望:runtime 以 repeated_tool_call 或 repeated_error 停止。

case 3:fake model 直到 maxSteps 都不 final。
期望:runtime 以 max_steps 停止,step 数等于配置。

case 4:用户取消。
期望:runtime 以 user_cancelled 停止,不再调度新 tool。

本书 demo 的 eval harness 就不是靠最终回答完全一致来判定,而是看 tool ledger、required tools、forbidden tools、denied tool、最终回答包含的证据。

停止条件也应该进入这种行为断言。

UI 里不要只显示'失败'

对用户来说,停止不一定是失败。

permission_denied 可能是正常结果。budget_exhausted 可能是在保护成本。repeated_error 可能是在避免浪费时间。max_steps 可能表示任务需要拆小。

最终回答可以这样写:

我没有继续执行。

原因:连续三次读取 src/foo.ts 都返回 NOT_FOUND。

已完成:
- 列出了 src 目录。
- 读取了 src/index.ts。

建议:
- 确认目标文件名。
- 或允许我重新搜索相关函数。

这才像一个工具,而不是一个突然停摆的黑盒。

最后

给 Agent Loop 加 max iteration,不是为了让它'最多跑几步'。

是为了让它在失控前停下来,并且说清楚为什么停。

maxSteps 是保险丝。

stop reason 是诊断。

trace 是证据。

eval 是防止同样问题下次再回来。

这几个东西连起来,Agent Loop 才开始像 Runtime。

参考资料

  • LangChain issue: "Agent stopped due to max iterations."
  • LangChainJS issue: agent stuck in a loop
  • LangFlow issue: Agent stopped due to max iterations
  • LangGraph Docs: GRAPH_RECURSION_LIMIT
  • n8n community: How to fix AI Agent stopped due to max iterations

推荐阅读

  • Agent 为什么必须有 Token Budget?
  • 给 Tool Call 加 Exponential Backoff Retry
  • 为什么 Tool 必须支持 Idempotency?

目录

  1. 给 Agent Loop 加 Max Iteration
  2. maxSteps 是保险丝,不是诊断报告
  3. 最小实现:先把 stop reason 写进 state
  4. shouldStop 不要散落在 loop 里
  5. repeated_error 怎么算
  6. repeatedtoolcall 怎么算
  7. maxSteps 应该怎么设
  8. stop reason 要进入 trace
  9. eval 也要测停止条件
  10. UI 里不要只显示“失败”
  11. 最后
  12. 参考资料
  13. 推荐阅读
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 在 Linux 桌面里跑 Windows 应用:Winboat 实战笔记
  • AI 编程工具深度对比:Trae、Cursor、Copilot 与 Windsurf
  • 论文阅读--Agent AI 探索多模态交互的前沿领域(一)
  • OpenCode 开源 AI 编程助手使用教程
  • Linux diff 与 patch 命令实战指南
  • 网络安全工程师职业定义、核心技能与认证体系详解
  • Python 为何如此流行?深度解析其核心优势与应用场景
  • 大疆无人机如何导出日志并解析
  • 前端 WebSocket 通信实战与最佳实践
  • OpenClaw 进阶教程:记忆系统、定时任务、多模型与子代理解析
  • Python 爬虫实战指南:从基础请求到分布式框架
  • Kubernetes Python 客户端实战教程
  • Java IO 核心:BufferedReader、BufferedWriter、PrintStream 与 PrintWriter 详解
  • AI 辅助编程工具:GitHub Copilot 安装与使用指南
  • 基于 Rokid AR 眼镜的聚会游戏助手开发实践
  • 百瑞互联 BR8654A02 蓝牙 6.0 SOC 芯片规格介绍
  • Windows 7 安装 Python 3.9+ 配置指南
  • 前端加密:常用方式与使用示例
  • GitHub Copilot 学生认证教程(2026 版)
  • openclaw-termux:在 Android 上部署 OpenClaw AI Gateway

相关免费在线工具

  • 数学计算器

    计算数学表达式的计算器。您可以使用sqrt、cos、sin、abs等函数。 在线工具,数学计算器在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online