Agent 停不下来怎么办:步数、预算、时间三道闸门怎么设
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
Agent 停不下来,多数时候不是模型笨,而是你交给它的任务没有一个机器能判定的终止条件。 大多数人第一反应是换个更强的模型、把任务描述写得更细,结果换完照样在同一个地方绕。真正的原因通常朴素得多:任务的”做完了”只存在于你脑子里,Agent 拿不到这个信号,于是它只能靠”还能不能再改一点”来自我判断,而这个判断永远都是”能”。
这篇讲的是工程侧的护栏怎么搭。人在旁边发现思路卡住、该怎么换轨,AI 修不好时的思维循环那篇讲得更细;单纯的花费控制在Agent 成本失控里;人该在哪些节点介入、怎么设审批点,看Agent 的人机协同设计。本篇只管一件事:闸门设在哪、怎么判断该拉哪一道、拉了之后怎么把现场交回给人。
一、先别急着加限制,把”停不下来”分成四类
同样是”跑了很久还在动”,底下的病因完全不同。上来就把步数上限调小,结果往往是本来能做完的任务被腰斩,你还以为是模型不行。
先看进展。判断进展的办法不玄乎:每一轮结束时,工作区有没有实质变化。最粗暴的做法是在旁边开个终端,隔一会儿看一次改动规模。
git status --short
git diff --stat
如果连续几轮 git diff --stat 的输出几乎不变,或者变化在两个版本之间来回横跳(改回去又改回来),那就是原地打转,不是”正在深入”。
第二种是目标漂移。改动规模一直在涨,但涨的地方跟你要它做的事关系越来越远——你让它修一个函数,它顺手重构了目录结构、加了配置项、写了新的抽象层。这类的特征是:每一步单独看都合理,合在一起完全跑题。
第三种是工具报错死循环。Agent 调某个工具一直失败,它换个参数再来一次,再失败再换,永不放弃。这类的识别信号是日志里同一类报错反复出现,比如权限被拒、连接超时(ETIMEDOUT)、对端断开(ECONNRESET),或者服务端持续返回 429、500。
这里要先分清两种”反复出现”,因为处置动作正好相反:
- 退避后能恢复的瞬时错误,典型是限流(429)、偶发的连接超时、对端偶发 5xx。这类该重试,但必须是带指数退避加随机抖动的有限次重试,且退避时长优先听服务端的(比如响应里给了
Retry-After就按它来)。判据是:拉长间隔后成功率明显变好。 - 重试多少次结果都一样的确定性错误,典型是鉴权失败(401/403)、路径或资源不存在(404)、请求参数不合法(400)、本地权限被拒。这类重试零收益,纯烧资源。判据是:退避到很长的间隔,错误依然一字不差。
分不清就手动跑同一个调用两次,中间隔久一点,看结果变不变。变了按第一类处理,不变按第二类——后者是必须停机的那一种,别让 Agent 在上面反复试。工具侧的排错另有讲究,见Agent 工具调错和Agent 失败重试。
第四种最隐蔽:成功判据缺失。任务本身没有可验证的完成信号,比如”把这个模块优化一下""让代码更清晰”。这种任务没有终点,Agent 会一直优化下去,而且每一步都能自圆其说。
二、判别表:从现象倒推该做什么
表里前四行就是上面那四类,后两行是实践里常撞见的变体(计划循环、上下文污染),一并列出来对着看。用法是从最左列的现象出发,而不是从你猜的原因出发。
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 改动规模几轮不变,或同一处反复改回去 | 无进展重试 | 连续几轮 git diff --stat 输出几乎一致;日志里同一文件反复读写 | 立刻停。把当前 diff 和它最后一次的判断留下,人接手定位 |
| 改动一直在涨但偏离原任务 | 目标漂移 / 任务边界没划 | 看新增文件是否在你指定的目录之外;对照任务描述逐条勾 | 停机后收窄范围,把任务拆成带验收条件的小步 |
| 日志里同类报错反复出现 | 确定性错误被当成瞬时错误在重试 | 隔久一点手动跑两次同样的调用:错误一字不变就是确定性的,间隔拉长后能成就是限流之类的瞬时问题 | 确定性的立刻停机修环境;瞬时的改成有限次退避重试,别让它无限试到把额度耗光 |
| 一直在”完善”,产出越来越啰嗦 | 成功判据缺失 | 问自己:什么命令跑通了算做完?答不上来就是缺判据 | 停机,先把验收命令写出来再重启 |
| 每轮都说”接下来我将…”,但不真正落盘 | 计划循环,动作层没执行 | 检查文件修改时间戳是否更新 | 停机,把任务改成一次只做一件可落盘的事 |
| 输出突然变碎、开始重复上文 | 上下文被挤压或污染 | 看它是否重复引用早期已废弃的结论 | 停机并重开会话,把有效结论手工带过去 |
这张表的用法是:先对现象,再动手。不要看到”跑太久”就统一按超时处理——超时真正能挡住的只有卡在某个外部调用上一动不动那一种;无进展重试、目标漂移、成功判据缺失这几类,Agent 一直在动,超时要么根本不触发,要么触发时活儿已经干歪了一大半,收拾起来比早停贵得多。
三、三道闸门各自挡什么、怎么定
步数、预算、时间是三种不同量纲的闸门,覆盖面不重叠,别指望其中一道能兜住全部。
步数闸门挡的是循环。它的优点是判断确定、触发点可复现:跑到第 N 轮无条件停。缺点是它对单步开销一无所知——一步里调用一次大文件读取,和一步里跑完整套测试,计一样的数。
定值的办法不是拍脑袋,而是量基线。挑三到五个你认为”典型”的同类任务,让它正常跑完,记下每次实际用了多少轮,取里面最大的那个,再留一点余量当上限。留多少余量看你的容忍度:留太紧会误杀正常任务,留太松等于没设。基线跟任务类型强相关,换一类任务就要重新量。
预算闸门挡的是资源被悄悄吃掉。它比步数更贴近你真正在意的东西——你怕的从来不是它多跑几轮,是多跑的那几轮把资源烧完了还没结果。预算闸门的实现方式各家不同,能力也不同,有的只能事后看账,有的可以在任务级别设上限,具体以你所用产品的官方最新说明为准。
不管平台提供什么,你自己那一层的监控要有。最低限度是把每次任务的消耗记下来,形成一条可对比的曲线,异常任务一眼能看出来。这里的关键判断是:预算闸门是最后一道,不是第一道。等它触发的时候,钱已经花了。前面两道闸门的意义就是让预算闸门尽量别被触发。
时间闸门挡的是挂死。它是唯一能处理”卡在某个调用上一动不动”的闸门——步数不涨、预算不涨,只有墙上时钟在走。所有可能阻塞的外部调用都该有超时,这一层最好在代码里显式写死,别依赖默认值:
import requests
resp = requests.get(url, timeout=(5, 30)) # 连接超时 / 读取超时分开设
curl --connect-timeout 5 --max-time 60 https://example.com/api
时间闸门的上限怎么定,取决于你能不能盯着。如果这是个你会守在旁边的任务,闸门可以设得贴近你的注意力窗口;如果是后台跑的,就得考虑”最坏情况下它空转多久你才会发现”,然后往前收。
三道一起用的顺序是:时间闸门最外层兜挂死,步数闸门管循环,预算闸门做最终封顶。
四、触发之后:停机不等于杀进程
这是最多人做错的一步。闸门触发了,进程一杀,终端一清,然后你面对一个改了一半的工作区,不知道它改到哪、为什么这么改、能不能回退。这种”停机”比不停还糟糕,因为它把一个可诊断的现场变成了一堆无主的改动。
一次合格的交回,至少要留下四样东西。
一是可回滚的现场。 这件事必须在 Agent 开始跑之前就安排好,事后补不了。最简单的做法是让它在独立分支或独立工作树里干活,主干永远干净:
git switch -c agent/task-0728
# 或者用工作树,主目录完全不受影响
git worktree add ../wt-task-0728 -b agent/task-0728
跑之前打一个基准点,随时能整块回去:
git add -A && git commit -m "baseline before agent run"
停机后要回退,git reset --hard <基准点> 能把已跟踪文件恢复原样。这里有个很容易踩的坑:reset --hard 不会动未跟踪文件,而 Agent 最常干的事之一就是新建文件(临时脚本、缓存目录、它自己写的笔记)。只 reset 一次,工作区看着”回去了”,实际躺着一堆新文件,下次跑的时候又被读进上下文当成既有代码。要真回到原样,得再清一次:
git status --short # 先看清楚要删什么,别上来就 clean
git clean -nd # -n 是预演,只打印将被删除的路径
git clean -fd # 确认无误后再真删(-d 含目录)
git clean 默认不碰被 .gitignore 忽略的文件,这通常正是你想要的——虚拟环境、依赖目录不该被误删。反过来说,如果 Agent 把垃圾写进了被忽略的路径,光靠 clean 也带不走,那部分只能眼睛过一遍。没有基准点,回滚就只能靠手工挑改动,那基本等于重做。
二是差异摘要。 停机的第一动作是把 git diff 完整存下来,别让它在后续操作里丢掉。人接手时看的第一样东西就是这个。
三是它停在哪一步、当时打算做什么。 大部分工具都会留下运行记录,把最后几轮的动作和判断摘出来,比让人重新猜要快得多。
四是待决问题清单。 这是从”停机”过渡到”人接手”的关键。停机时的状态往往是它卡在某个岔路口,比如两个方案选不定、某个接口的行为跟预期不符。把这些写成三五条明确的问题,人回答完就能重启,而不是从零复述一遍任务。
交接物准备好了,人的介入才有意义。至于介入点本身怎么排布——哪些动作必须先问、哪些可以放手——那是另一个话题,本篇只管闸门这一层。
五、什么情况下别再折腾了
工程师最容易犯的错是沉没成本:已经跑了这么久,再让它试一次说不定就好了。下面几条是我的止损线,命中任何一条就停,不要再加轮次。
同一个错误出现第三次,且每次的修法本质相同。 换个参数、换个写法、换个库,看起来在尝试不同方案,实际上都在同一个错误假设下打转。第三次是个经验阈值,不是定律,但比”再试试”可靠。这时候正确的动作是人去读一遍原始报错,很可能问题根本不在 Agent 改的那一层。
修改范围开始超出你能审的量。 你的审阅能力是硬约束。当 diff 已经大到你不可能逐行看完,即使它真的修好了,你也没法负责地合进去。这时候停机、回滚、把任务拆小,比让它继续做完更省时间。
它开始动你没让它动的东西。 配置文件、依赖版本、构建脚本、测试断言——尤其是改测试让测试通过,这是最危险的一类”修好了”。发现这个立刻停,别评估,直接回滚。
环境本身有问题的时候,一步都别多跑。 网络走代理、内网自签证书导致证书链校验失败、依赖装错版本,这些环境问题会让 Agent 收到一堆语义错误的反馈,然后基于错误反馈做出错误改动。此时它跑得越久,你要清理的垃圾越多。先修环境,再谈任务。
换条路的判断依据。 如果连续两次重启(每次都换了任务描述、换了切入点)都停在同一个位置,说明卡点不在执行层,在你对问题的理解上。这时候不是再调闸门参数,而是自己去把那一小块搞明白——通常十几分钟的手工调试,抵得上它半天的自动尝试。
六、避坑清单
只设步数不设时间。 会踩是因为步数看起来最直观,一眼能数。但一个卡在网络调用上的任务步数根本不涨,闸门永远不触发,它就那么挂着。避法是给所有外部调用显式设超时,并且在任务外层再套一层墙上时钟的上限。
闸门设在提醒层而不是执行层。 会踩是因为在任务描述里写”最多改三个文件”看着就够了。但这类约束是软的,模型可以忽略它,而且它没有理由认为自己违反了。避法是把硬限制放在代码或配置里——工具只暴露受限的能力,超范围的操作根本调不到,而不是靠自觉。
触发闸门后自动重启。 会踩是因为觉得重试能提高成功率。但如果停机原因是环境故障或判据缺失,重启只会把同样的失败再来一遍,而且这次没人看着。避法是把”停机”和”重启”分成两个决策,中间必须有人或有一条明确的自动判据(比如只有在错误类型是可重试的瞬时错误时才重启)。
没有基准点就开跑。 会踩是因为任务看着小,“就改一个函数”,不值当提交一次。结果它顺手动了别的地方,回滚时挑不出来。避法是把打基准点做成开跑前的固定动作,跟启动命令绑在一起,不留判断余地。
把闸门参数当成一次性配置。 会踩是因为设完之后就没人再看。但任务类型变了、代码库长大了、模型换了,基线全都会变,旧参数要么天天误杀要么形同虚设。避法是定期回看触发记录:如果闸门几乎从不触发,说明设松了;如果正常任务频繁被拦,说明设紧了,两种都要调。
用增大上限来”解决”停不下来。 会踩是因为触发闸门时那种”就差一点”的感觉特别真实。但闸门频繁触发本身就是信号,说明任务粒度不对。避法是把触发当成拆任务的提示,而不是调参的提示——一个需要不断加上限才能做完的任务,多半应该被拆成三个各自有验收条件的任务。
日志只记结果不记过程。 会踩是因为跑通的时候没人看日志。等出问题想复盘,发现只有最终输出,中间每一轮做了什么、为什么改主意,全没了。避法是把每轮的动作和关键判断落到文件里,成本很低,出事时能省几个小时。
收个尾
闸门的意义不是限制 Agent,是把”什么时候该人接手”这个决定从临场感觉变成可执行的规则。三道闸门里,步数管循环、时间管挂死、预算兜底,缺一道就漏一类;而三道都设了但停机后交不出现场,等于白设。
开跑前过一遍这份自检:
- 这个任务的”做完了”能用一条命令验证吗?答不上来就先别开跑。
- 基准点打了吗?回滚动作(reset 加 clean)演练过吗?
- 步数上限是从实测基线来的,还是拍的?
- 所有外部调用都显式设了超时吗?
- 后台跑的任务,最坏情况下空转多久你会发现?
- 闸门触发时,diff、停在哪一步、待决问题这三样会自动留下来吗?
- 最近一个月,闸门触发过几次?一次没有和天天有,都得调。