一个没有设最大步数上限的 AI Agent,遇到一个它搞不定的任务后陷入了死循环,一夜之间烧掉了我们大半个月的模型预算:一次 Agent 失控的深度复盘
那是一个让我看到账单时手都在抖的早晨:我们上线了一个能自动调用工具(查数据库、调接口、读文件)来完成任务的 AI Agent,前一天测试一切正常。可第二天一早,模型 API 的账单告警疯狂轰炸——一夜之间,Token 消耗是平时的几百倍,大半个月的预算被一个晚上烧光了。我冲进日志一看,头皮发麻:有一个 Agent 任务,从头一天晚上到第二天早上,循环执行了几万步,反反复复地调用同一个工具、得到同样的失败结果、再换个说法调一遍、又失败……它就这么不知疲倦地、永不停止地循环了一整夜。我盯着那几万行几乎一模一样的日志,终于看清了真相:我们的 Agent 用的是经典的"思考→行动→观察→再思考"(ReAct)循环,可我们压根没给这个循环设一个"最大步数"的上限。当它遇到一个它能力范围内根本完成不了的任务(那条数据其实不存在,但它坚信能查到)时,它就陷入了"失败 → 换个方式重试 → 又失败 → 再换 → 再失败"的死循环,而没有任何机制能让它停下来——直到把钱烧光。这篇就把这次"Agent 循环失控、烧光预算"的坑,从头到尾复盘一遍。
故障现场:一个没有任何"刹车"的 Agent 循环
问题代码,是一个简化版的 Agent 主循环——它有"油门"(不停地思考、行动),却没有任何"刹车":
# ✗ 出问题的 Agent 主循环: 没有任何终止保护
def run_agent(task: str):
messages = [{"role": "user", "content": task}]
while True: # ✗ 雷! 无限循环, 没有最大步数上限
response = llm.chat(messages, tools=TOOLS) # 让大模型思考下一步
if response.tool_calls:
# 大模型决定调用工具
for call in response.tool_calls:
result = execute_tool(call) # 执行工具(查库/调接口...)
messages.append({"role": "tool", "content": result})
# ✗ 然后回到 while 顶部, 继续让大模型思考——周而复始
else:
# 大模型给出了最终答案, 才退出
return response.content
# 致命问题:
# - 唯一的退出条件是"大模型主动给出最终答案(不再调工具)";
# - 但如果任务它【根本完不成】(数据不存在/工具一直失败/它想不通),
# 大模型会【永远】认为"我还需要再试一个工具"→ 永远不给最终答案;
# - → while True 永远转下去, 每一圈都是一次(昂贵的)大模型调用 + 工具调用;
# → 一夜几万圈, Token和钱哗哗地烧, 没有任何东西能让它停。
# 还缺的其他"刹车":
# - 没有"重复动作检测": 它一遍遍调同一个工具、传几乎一样的参数, 没人发现这是在原地打转;
# - 没有"Token/成本预算上限": 烧了多少了? 没人统计、没有阈值熔断;
# - 没有"超时": 一个任务跑了一整夜, 没有时间上限把它掐掉。
第一次理清这个 while True 时,我冷汗直冒:"我给了它调用工具的能力、给了它自主决策的循环,却唯独忘了给它一个'什么时候必须停下来'的约束。"这个坑最危险的地方在于:它把"自主性"和"失控"之间那条本就很细的线,彻底暴露了——Agent 的强大,正在于它能"自主地、循环地决策和行动";可一旦这个"自主循环"没有边界,这份强大就瞬间变成失控:它会用你给它的能力(调工具、花钱),不知疲倦地把灾难放大。而且,和传统程序的 bug(崩了就停了)不同,Agent 的失控是"越界地、持续地消耗真实资源(钱、配额、对外部系统的调用)"——它不会崩,它会一直"努力地"做错事,直到把资源耗尽。下面就来拆解,为什么 Agent 必须有"刹车"。
第一件事:搞懂 Agent 循环为什么必须有"硬性边界"
我顺着这次事故,把"Agent 自主循环为什么天然需要约束"彻底想清楚了。
为什么 Agent 的自主循环, 必须有"硬性的终止边界"?
【核心: Agent的退出依赖"大模型自己判断任务完成"——这是【不可靠】的, 必须有外部硬约束兜底】
1. Agent 循环的本质:
它是一个"大模型决策 → 执行 → 把结果喂回大模型 → 再决策"的循环;
循环的"出口", 是大模型判断"任务完成了, 我给最终答案"。
2. 问题: "大模型判断任务完成"这个出口, 是【不可靠】的:
- 任务本身可能【无解】(数据不存在、工具坏了、目标自相矛盾);
- 大模型可能【固执】: 它不会"认输", 反而一直"我再换个方法试试";
- 大模型可能【绕圈】: 在几个动作之间反复横跳, 自以为在进展;
- → 这些情况下, 那个"出口"永远不会到达 → 循环永不终止。
3. 危险被放大: 这个循环的每一圈, 都在【消耗真实资源】:
- 每圈一次大模型调用(花钱、占配额);
- 每圈可能调用真实工具(查库、调外部接口——还可能有副作用!);
- → 失控的循环 = 持续地、加速地烧钱 + 轰炸外部系统。
4. 根本原则: 不能把"是否停止"【完全】交给大模型自己决定:
- 大模型是"概率性、不保证收敛"的, 你不能假设它"一定会在合理步数内停";
- 必须由【外层的、确定性的程序】给它套上【硬性的边界】:
最大步数、预算上限、超时、重复检测——这些是"刹车", 由代码强制执行。
类比: Agent像一个很能干但有时钻牛角尖的实习生;
你给他自主权(好), 但必须规定"最多试N次、最多花X钱、超时就来找我"(边界);
否则他可能为一个死任务, 一个人在工位上耗到天亮、还刷爆了公司的报销额度。
一句话: Agent的退出靠大模型自判, 不可靠; 必须由外层程序强制套上最大步数/预算/超时/
重复检测等硬性边界, 让"自主"始终在"可控"的笼子里——自主性必须配上确定性的约束。
这套道理,是整个坑的根。Agent 循环的本质是"大模型决策→执行→结果喂回→再决策",循环的出口是大模型判断"任务完成";但这个出口不可靠:任务可能无解、大模型可能固执("我再换个方法试")或绕圈,这些情况下出口永不到达、循环永不终止。而每一圈都在消耗真实资源(每圈一次花钱的大模型调用 + 可能有副作用的真实工具调用),失控循环 = 持续加速地烧钱 + 轰炸外部系统。根本原则是:不能把"是否停止"完全交给大模型自己决定——它是概率性、不保证收敛的,必须由外层的、确定性的程序套上硬性边界(最大步数、预算、超时、重复检测)。就像一个能干但会钻牛角尖的实习生,给自主权的同时必须规定"最多试 N 次、最多花 X 钱、超时来找我"。一句话:Agent 退出靠大模型自判不可靠;必须由外层程序强制套上最大步数/预算/超时/重复检测等硬性边界,让自主始终在可控的笼子里。
第二件事:正解——给 Agent 套上最大步数、预算、超时、重复检测四道刹车
搞懂了原理,正解就清晰了:给 Agent 循环套上多道确定性的硬约束——最大步数上限、Token/成本预算上限、整体超时、重复动作检测;任一道触发就优雅终止并兜底。
# ✓ 正解: 给 Agent 主循环套上多道"刹车"
def run_agent(task: str, max_steps=15, budget_tokens=50000, timeout_sec=120):
messages = [{"role": "user", "content": task}]
used_tokens = 0
start = time.time()
recent_actions = [] # 记录最近的动作, 用于重复检测
for step in range(max_steps): # ★ 刹车一: 最大步数上限(不再 while True)
# ★ 刹车二: 预算上限
if used_tokens >= budget_tokens:
return finish("达到Token预算上限", messages)
# ★ 刹车三: 超时
if time.time() - start > timeout_sec:
return finish("达到超时上限", messages)
response = llm.chat(messages, tools=TOOLS)
used_tokens += response.usage.total_tokens
if not response.tool_calls:
return response.content # 正常出口: 大模型给出最终答案
for call in response.tool_calls:
# ★ 刹车四: 重复动作检测(同样的工具+参数反复调 = 在原地打转)
sig = (call.name, json.dumps(call.arguments, sort_keys=True))
recent_actions.append(sig)
if recent_actions.count(sig) >= 3: # 同一动作出现3次 = 死循环征兆
return finish(f"检测到重复动作 {call.name}, 疑似死循环, 终止", messages)
result = execute_tool(call)
messages.append({"role": "tool", "content": result})
# ★ 兜底: 步数耗尽仍没完成 → 优雅退出, 而不是无限转下去
return finish("达到最大步数仍未完成, 优雅终止", messages)
def finish(reason: str, messages) -> str:
logger.warning(f"Agent提前终止: {reason}")
# 让大模型基于已有信息给一个"尽力而为"的回答 + 说明没完成
return llm.chat(messages + [{"role":"user",
"content": f"由于{reason}, 请基于目前已知信息给出最好的回答, 并说明哪些没能完成。"}]).content
# ====== 配套防护(同样重要) ======
# 1. 工具调用要幂等/可控副作用: Agent可能重试调用工具, 有副作用的工具(下单/发邮件/写数据)
# 要做幂等(带幂等键), 否则重试会重复执行 → 重复下单/重复发信。
# 2. 全局成本熔断: 不只单个任务有预算, 整个系统/账号层面也要有日成本上限和告警,
# 一旦异常飙升就自动熔断(本文若有这层, 就不会烧一整夜)。
# 3. 高风险工具要人工确认(human-in-the-loop): 删数据、转账、对外发布等危险动作,
# Agent不能自己拍板, 要暂停等人确认。
# 4. 可观测: 记录每一步的思考/动作/结果/token, 出问题能快速定位(本文靠日志才查到)。
# 核心: Agent循环必须有多道确定性硬约束——最大步数、预算上限、超时、重复检测, 触发即优雅终止;
# 配合工具幂等、全局成本熔断、高危人工确认、全程可观测; 给"自主"装上"刹车"和"护栏"。
修复的核心,是"给自主的 Agent 装上多道确定性的刹车与护栏"。四道刹车:最大步数上限(用 for range(max_steps) 取代 while True)、Token/成本预算上限、整体超时、重复动作检测(同样的工具+参数反复调就判定死循环);任一触发就 finish 优雅终止(让大模型基于已知信息尽力回答并说明没完成)。配套防护同样重要:工具幂等(有副作用的工具带幂等键,防重试重复执行)、全局成本熔断(系统级日成本上限+告警,本文若有这层就不会烧一整夜)、高危动作人工确认(human-in-the-loop)、全程可观测(记录每步思考/动作/结果/token)。归根结底:Agent 循环必须有最大步数/预算/超时/重复检测多道硬约束,触发即优雅终止;配合工具幂等、全局成本熔断、高危人工确认、全程可观测。
第三件事:AI Agent 工程的其他常见坑
排查后我把 AI Agent 工程化相关的其他常见坑也系统梳理了一遍。
AI Agent 工程的其他常见坑
# 1. 循环没有终止边界(本文): 死循环烧光预算。→ 最大步数+预算+超时+重复检测。
# 2. 工具有副作用却没幂等: Agent重试导致重复下单/发信/扣款。→ 工具幂等(幂等键)。
# 3. 上下文无限增长: 每步都往messages堆, 很快超上下文窗口/越来越贵。→ 历史摘要/裁剪。
# 4. 工具返回结果过大: 一个工具吐回几万字塞爆context。→ 截断/摘要工具输出。
# 5. 高危操作Agent自己拍板: 删库/转账没有人工确认。→ human-in-the-loop审批。
# 6. 工具描述写不清: 大模型不知道何时该用/参数怎么填, 乱调工具。→ 清晰的工具说明+参数schema。
# 7. 没有可观测/追踪: 出了问题不知道Agent每步在想啥做啥。→ 记录每步trace。
# 8. 完全信任Agent的输出: 把它的结果直接当真执行。→ 关键结果要校验/兜底。
# 共同根源: Agent = "不确定的大模型" + "能产生真实副作用的工具" + "自主循环";
# 它把大模型的"不确定性"和工具的"真实威力"用循环放大了, 任何环节失控后果都被放大。
# 核心: 把Agent当成"强大但不可全信的自主体"来工程化: 套硬性边界(步数/预算/超时)、
# 工具幂等、上下文管理、高危人工确认、全程可观测、输出校验; 自主性必须配套强约束与护栏。
排查让我把 Agent 工程的其他坑也梳理清了。一、循环没终止边界(本文)。二、工具有副作用却没幂等(重试重复执行)。三、上下文无限增长(超窗口/越来越贵)。四、工具返回过大(塞爆 context)。五、高危操作 Agent 自己拍板(要人工确认)。六、工具描述写不清(乱调工具)。七、没有可观测/追踪。八、完全信任 Agent 输出。它们的共同根源是:Agent = "不确定的大模型" + "能产生真实副作用的工具" + "自主循环";它把大模型的不确定性和工具的真实威力用循环放大了,任何环节失控后果都被放大。核心是:把 Agent 当成"强大但不可全信的自主体"来工程化:套硬性边界、工具幂等、上下文管理、高危人工确认、全程可观测、输出校验;自主性必须配套强约束与护栏。下面这张图,是这次 Agent 循环失控的成因与解法:
第四件事:Agent 必备的"刹车与护栏"速查表
这次踩坑后,我把一个生产级 Agent"必须有哪些刹车与护栏"整理成一张表,逐条对照落地。
| 护栏 | 防的是什么 | 怎么做 |
|---|---|---|
| 最大步数 | 死循环(本文) | for max_steps, 非while True |
| 预算上限 | 烧光token/钱 | 累计token超阈值即停 |
| 超时 | 单任务跑太久 | 整体时间上限 |
| 重复动作检测 | 原地打转 | 同动作多次即判死循环 |
| 工具幂等 | 重试重复副作用 | 幂等键 |
| 全局成本熔断 | 系统级失控 | 日成本上限+告警 |
| 高危人工确认 | 危险操作误执行 | human-in-the-loop |
| 可观测 | 出问题难定位 | 记录每步trace |
这张表把 Agent 的安全网钉清了。核心是:一个能真正放到生产的 Agent,光有"聪明的大脑(大模型)"远远不够,它必须被一整套"刹车与护栏"包裹起来——限制它能跑多久(步数/超时)、能花多少(预算/熔断)、防它原地打转(重复检测)、防它重复闯祸(幂等)、拦住它的危险动作(人工确认)、让它全程可被观察(trace)。它给我的最大启发是:赋予一个系统"自主性/能力"和给它配套"约束/安全机制",必须是同步进行的——能力越大,需要的约束越强;本文的惨痛正源于"能力(自主调工具、花钱)给足了,约束(边界、护栏)却一个没配"的严重失衡。这其实是一条工程通则:"能力"和"控制"要匹配——你给一个组件越大的权力(花钱、动数据、调外部、自主决策),就越要给它越严密的约束(限额、审批、隔离、可回滚、可观测);"放权"的同时必须"设限",二者失衡(有权无限)就是灾难的温床;这在 Agent、在微服务、在权限设计、在自动化系统里,都一样成立。能力与约束同步配套、放权的同时设限——是这个 Agent 失控坑带给我的、关于设计自主系统的核心认知。
第五件事:Agent 与传统程序的本质差异
这次事故也让我深刻意识到,写 Agent 和写传统程序,思维上有几处根本不同。我整理成表。
| 维度 | 传统程序 | AI Agent |
|---|---|---|
| 控制流 | 开发者写死的确定逻辑 | 大模型动态决定下一步 |
| 是否会终止 | 逻辑保证(一般) | 不保证, 需外部强制边界 |
| 出错方式 | 报错/崩溃就停 | "努力地"持续做错事 |
| 资源消耗 | 大致可预估 | 可能失控暴涨 |
| 可预测性 | 高(确定性) | 低(概率性) |
| 该怎么对待 | 信任逻辑 | 约束+监控+兜底 |
这张表道出了 Agent 编程的"范式转变"。核心是:传统程序的控制流是开发者写死的、确定的(你能推断它一定会走到哪、一定会停);而 Agent 的控制流是大模型动态决定的、概率性的——它不保证终止、出错时会"努力地持续做错事"、资源消耗可能失控暴涨。它给我的深刻启发是:写 Agent,需要一次思维范式的切换——从"我(开发者)精确地控制程序每一步做什么"(命令式、确定性),转向"我设定目标和边界,让大模型在边界内自主地探索如何达成"(目标式、概率性);在这种范式下,我的核心职责不再是"写死每一步逻辑",而是"设计好护栏、约束、监控和兜底,让一个不完全可控的智能体,在安全的框架内发挥作用"。这是一种全新的工程心态:从"控制过程"转向"约束边界 + 监督结果"——你管不了(也不该试图精确管)Agent 中间怎么想、怎么试,但你必须管住它的边界(不能越界)、成本(不能失控)、危险动作(不能乱来)、和最终结果(要校验);就像管理一个能干的下属:你给目标、定规矩、看结果,而不是替他做每个动作。完成从"控制过程"到"约束边界、监督结果"的范式切换——是这个坑带给我的、关于如何驾驭 AI Agent 这类概率性自主系统的最深认知。
第六件事:上线一个 Agent 前,我现在的检查习惯
现在每当我要把一个 Agent 放到生产,我都会按这张图把护栏过一遍:
这张图的精髓,是"上线前把刹车、护栏、监控一项项确认齐"。先确认有最大步数上限(没有就停)、预算/超时/重复检测齐全、有副作用的工具做幂等且高危动作人工确认、有全局成本熔断和每步 trace,最后小流量灰度盯着监控再放量。这套习惯,让我从"Agent 能跑通就上"变成了"上线前先确认护栏齐不齐"——核心始终是:Agent 上生产前,确定性的边界、护栏、监控、兜底必须先配齐,再灰度放量。
我立下的几条规矩
这场"Agent 死循环烧光预算"的事故,换来了我做 AI Agent 时,刻进骨子里的几条铁律:
- Agent 循环必须有最大步数上限。用 for max_steps,绝不裸用 while True。
- 退出不能只靠大模型自判。它不保证收敛,必须有外部确定性边界兜底。
- 配齐预算、超时、重复动作检测。多道刹车,任一触发就优雅终止。
- 有副作用的工具必须幂等。Agent 会重试,否则重复下单/扣款。
- 系统级要有成本熔断和告警。异常飙升自动刹住,别烧一整夜。
- 高危操作必须人工确认。删库/转账 Agent 不能自己拍板。
- 能力与约束同步配套。放权多大,设限就要多严;全程可观测、结果要校验。
写在最后
回头看,这场由"一个 while True 没有上限"引发的、一夜烧光预算的事故,真正教给我的,远不止"Agent 循环要设最大步数"这一个技巧。它让我对"赋予一个东西'自主行动的能力',就必须同时赋予它'不会失控的约束'",有了一次代价高昂的体会。我栽跟头,根源在于我被 Agent 的"智能"迷惑了,产生了一种危险的过度信任:我下意识地以为,既然它"聪明"到能自己思考、自己决定调什么工具,那它自然也"聪明"到知道什么时候该停下来——就像一个聪明人不会傻到为一件不可能的事耗一整夜。可这恰恰是我对它本质的误判:大模型的"智能",是一种"生成下一步看起来合理的行动"的能力,它并不内禀"知止"的智慧——它不会累、不会心疼钱、不会"觉得不对劲而停下来反思";你给它一个目标,它就会"锲而不舍"地一步步试下去,哪怕这条路根本走不通、哪怕代价高到离谱,它也不会自己喊停。"能干"和"知止",在它身上是两件完全分开的事。这让我领悟到一个驾驭一切"自主系统"的根本原则:"自主性"和"自我约束"不是一回事——一个系统能"自主地做很多事",绝不意味着它能"自主地、恰当地约束自己";"知道何时停止、何时收手、何时止损"这种"自我约束",往往不会自发产生,而必须由外部、用确定性的机制,明确地施加上去;越是给一个系统强大的自主能力,就越要从外部为它装好"它自己不会有"的那道刹车。认清自主能力不等于自我约束、必须从外部为自主系统装上确定性的刹车——这,是我用一夜烧光的预算,换来的、关于 AI Agent、也关于如何安全地驾驭一切强大而不完全可控的系统的、最朴素也最昂贵的领悟。如果这篇复盘,能让你在上线下一个 Agent 前,先回头确认一句"它的循环,有上限吗?它失控了,有什么能刹住它?",那我们那一夜烧掉的预算,就还算买了个教训。
—— 别看了 · 2026