一、一个晚上十一次修改零进展shop订单服务的取消功能又出问题了。这次是一条边界用例已取消的订单再次调用取消接口时应该返回一个明确的错误而不是静默成功。你把这条用例交给 Agent它开始工作。前四十分钟你很满意它改了状态判断、补了测试、跑了命令。四十分钟之后你回来看结果发现测试还是红的于是它继续改。晚上十点你数了一下它的修改轮次十一次。翻开它的过程记录能看到一个很有规律的图形21:12 修改 src/orders/services/cancel_order.py - 失败CANCELLED 订单被允许再次取消 21:24 修改 src/orders/services/cancel_order.py - 失败PAID 订单被允许取消 21:37 修改 src/orders/services/cancel_order.py - 失败CANCELLED 订单被允许再次取消 21:52 修改 tests/orders/test_cancel.py - 失败删掉断言后仍有一条集成用例不通过 22:08 修改 src/orders/services/cancel_order.py - 失败PAID 订单被允许取消 ...每一轮它都在动代码确实变了测试确实跑了错误确实读了。但两件事一直在原地交换允许不该允许的状态或者拒绝不该拒绝的状态。中间它甚至试着改测试被集成用例拦下之后又回到代码里继续换。这里没有偷懒也没有明显的愚蠢。它面对的处境是每修好一个用例另一个用例就红。它每次都在尝试解决眼前那一条失败信息而两条失败信息在互相打脸。这个现象有个名字叫振荡oscillation。它和前面几篇讲的东西不冲突闭环的工作方式是对的反馈也是真实的缺的是另一件东西——什么时候该停下来。这一篇就讲这件事飞轮为什么不是“转得越久越好”以及停止条件具体怎么写。还有两个细节值得注意。第一那天晚上的每一轮都留下了痕迹修改的文件、跑过的命令、读到的失败。这些痕迹本身是有价值的问题在于它们组织得很差——十一轮之后谁也说不清哪些尝试还有效。第二你之所以在晚上十点才发现是因为没有任何一个环节会告诉你“这次循环开始重复了”。如果有一行字在第三轮结束时出现这个晚上会缩短两个小时。第三件值得注意的事是第十一轮的失败信息和第三轮几乎一样。把两轮的输出放在一起对比会发现失败用例名相同、异常类型相同、栈顶文件相同、断言相同。也就是说中间那八轮没有改变问题的性质。这八轮的时间不是被浪费在“探索”上而是被浪费在“重复”上——这正是需要一个停止条件的地方。二、先把词讲明白飞轮flywheel指“改代码、跑检查、读结果、再改”的循环。它之所以有用是因为每一轮都会带来新的信息。它的危险也在这句话里只有带来新信息的时候才有用。振荡oscillation循环在两种状态之间来回摆动比如修好 A 就弄坏 B修好 B 又弄坏 A。它的外观和“努力修复”很像区别是它的状态没有净前进。失败指纹failure fingerprint用几个稳定的字段描述一次失败比如失败用例名、异常类型、栈顶文件与行号、关键断言。就像指纹能区分不同的人指纹能区分“同一种失败”和“新的失败”。指纹相同的两次失败含义几乎总是“没有获得新信息”。新信息能让下一轮做出不同判断的东西比如新的错误信息、新的日志、新的用例、规格上的澄清。仅仅“再试一次”不产生新信息。停止条件stopping condition预先约定的、达到就停止循环的判据。它有两种形态数量型最多几轮和信号型出现某类失败就停。升级escalation停止循环之后把问题交给人并附上一份可读的状态说明。它不是放弃而是把问题交给更有判断力的一方。预算一次任务允许消耗的时间、轮次或者费用上限。预先设预算的意义是让“继续”变成一个需要理由的决定而不是默认状态。接管条件明确写出“什么情况下必须由人接手”比如缺少凭证、规格互相矛盾、需要访问生产环境的数据。它和停止条件是一对前者说的是“谁接”后者说的是“什么时候交”。振荡信号能提示“循环开始重复”的具体现象比如同一指纹再次出现、改动文件数持续增加、失败用例在两个固定集合之间跳。它的作用是把主观的“感觉不太对”变成可观察的事实。净进展把这一轮和上一轮对比之后问题范围是否变小。净进展为零不代表没有动作只代表动作没有改变局面。判断一个循环值不值得继续看的就是净进展。顺带把“预算”和“停止条件”的区别说清预算是这一次任务允许消耗的上限通常以时间或轮次计算停止条件是“满足某个条件就立刻停”。预算像是给油箱设定的容量停止条件像是仪表盘上的报警灯。两者常常一起用先用报警灯抓住振荡再用预算兜住那些“看起来每轮都有进展”的长循环。还有一个词在后面的例子里会反复出现账本。它指的是一份很简单的记录每次循环往里面加一行写着这一轮的指纹和轮次。账本不需要数据库一个 JSON 文件或者一张表格就够。它的存在只为一件事——让“这是第几次、是不是老问题”这两个问题有确定的答案而不是靠回忆。三、循环为什么会振荡振荡的根源不在模型的努力程度而在信息结构。把它拆开看有三个常见的成因。第一个成因是失败信号不完整。测试报告通常只说“这一条失败了”不说“为什么失败”。当两条用例对同一段逻辑提出相反要求时任何一次修改都会让其中一条红。Agent 看不到冲突本身只看到当前一条失败于是它一轮修一边来回摆动。第二个成因是没有新信息还在重试。这种情况的一个典型标志就是失败指纹不变。指纹里的字段稳定意味着失败的性质没变那么下一轮能做的事只有两种换个猜法或者获取新信息。没有新信息时换猜法就是振荡。这一点有研究结论支持。Huang 等2023在Large Language Models Cannot Self-Correct Reasoning YetarXiv:2310.01798里给出的结论是缺少外部反馈时模型很难通过自我修正改进推理某些情况下修正后反而更差。把它翻译成本篇的话如果每一轮只是“再想一遍”而没有从外部带回新的信号那么循环的质量不会随轮次提升。第三个成因是修改范围在悄悄扩大。为了修一条取消订单的用例它可能顺手改了状态枚举、改了鉴权中间件、改了测试夹具。范围一扩大新的失败就有了新的来源指纹不再是原来的指纹看起来像“又在前进”实际是在不同的坑之间跳。这三种成因都指向同一个结论循环要有价值必须满足两个条件中的至少一个——要么每轮带来新信息要么每轮把范围收得更小。条件都不满足时继续跑下去只是在消耗时间和预算。那么为什么不能指望执行者自己发现“该停了”因为它缺少一个参照点。对它来说再试一次的成本很低而停止意味着任务没有完成。在没有明确停止条件的情况下继续尝试永远是更自然的选择。停止条件必须由流程提供就像验收条件必须由流程提供一样。具体来说它缺少的参照点有三样。第一它不知道这次任务允许花多少轮因为没有人告诉过它。第二它不知道同一指纹已经出现过几次因为没有人记账。第三它不知道停下来的后果是什么——如果停止意味着“任务失败”它会倾向于继续如果停止意味着“把问题交给能做决定的人”它就只是一次普通的转交。三样里最容易补的是前两样把它们写进任务单加一个小脚本记账。停止条件还需要区分两种不同的失败。有些失败说明“还差一点”比如用例名写错、字段名不匹配这类可以继续有些失败说明“这条路不通”比如规格本身互相矛盾、缺少生产环境凭证、需要的接口不存在这类必须停下来交给人。区分它们的依据不是失败的数量而是失败的性质它是不是能用当前的权限和信息解决。振荡的三种常见形态振荡不只有一种样子能认出来才能设对停止条件。形态现象识别信号该用的停止条件交替红绿同一个用例反复红绿或两个用例轮流失败两个指纹交替出现同一指纹累计出现两次即停原地打转同一个文件反复被改失败指纹始终不变指纹完全一致指纹不变即停轮次上限兜底范围扩散每次改动都牵扯更多文件失败换成了新问题改动文件数持续增加改动范围超过任务边界即停第一种最容易识别也最容易被误认为“在认真修问题”因为每一轮都在处理一条真实的失败。第二种看起来更沉闷失败信息完全一样这种情况往往说明缺少必要的信息或者权限。第三种最危险因为它看起来进展最快——每轮都有新的错误冒出来像是有事在发生实际上是把一个问题换成了三个。三种形态对应三种不同的补救方向交替红绿多半说明约束冲突需要人来裁决原地打转多半说明缺少信息需要补材料或者授权范围扩散说明任务边界不清需要在任务单里把可改范围写得更死。把三种形态和前面提到的“成本”放在一起看会得到一个排序上的结论最应该被优先拦住的是范围扩散因为它会同时引入新的问题和新的失败信号让原本的问题更难辨认其次是交替红绿它会消耗大量时间但至少问题本身没变最后是原地打转它是三者里最容易被察觉的因为失败信息完全一致。设置停止条件时按这个顺序排优先级收益最大。四、把那个晚上复盘一遍下面这个shop项目是虚构示例这十一次修改也是构造出来的但它们的形状来自真实工作中常见的一类循环。我们要做的是把这次振荡拆成可识别的信号然后给它装一个停止条件。4.1 从“十一次修改”里提取指纹把那次循环压缩成一张表只保留四列轮次改动位置失败用例指纹1services/cancel_order.pytest_paid_order_cannot_be_cancelled8f3c21a0b7d42services/cancel_order.pytest_cancelled_order_rejected1b90ee7c33af3services/cancel_order.pytest_paid_order_cannot_be_cancelled8f3c21a0b7d44tests/orders/test_cancel.pytest_cancelled_order_rejected集成用例5d0aa91c7e025services/cancel_order.pytest_cancelled_order_rejected1b90ee7c33af…………两张指纹8f3c21a0b7d4与1b90ee7c33af交替出现。到第三轮时事实已经很清楚两次修改在两个互斥的方向之间摆动第三条路径没有被尝试过。如果循环里有一个记账的人它在这一轮就该停下。指纹的四个字段是这样定义的失败用例名含文件路径 异常类型AssertionError、ValueError、TimeoutError…… 栈顶文件与行号失败发生在哪一行 关键断言或关键错误消息这四个字段的选取标准是“稳定”。栈顶行号会随着改动变化这没关系因为它区分的是两次不同位置的失败而不稳定的部分比如耗时、内存地址、随机生成的 id不要放进去否则同一种失败会算出不同指纹停止条件就失效了。提取它们不需要复杂工具pytest -q的输出里已经有前两个字段堆栈里能看到第三、第四个pytest-qtests/orders21|tail-n404.2 一个很小的记账脚本把判断放在人的记忆里是行不通的写成一个脚本只需要几十行# tools/fix_loop_guard.py给修正循环记账轮次超上限或同一指纹再次出现就请求停止。importhashlibimportjsonimportsysfrompathlibimportPath STATE_FILEPath(.codex/fix-loop.state.json)MAX_ROUNDS3# 修正轮次上限SAME_FINGERPRINT_LIMIT2# 同一指纹累计出现次数上限deffingerprint(failed_test:str,error_type:str,top_file_line:str,assertion:str)-str:raw|.join([failed_test,error_type,top_file_line,assertion])returnhashlib.sha256(raw.encode(utf-8)).hexdigest()[:12]defload_state()-dict:ifSTATE_FILE.exists():returnjson.loads(STATE_FILE.read_text(encodingutf-8))return{rounds:0,fingerprints:[]}defmain()-int:payloadjson.loads(sys.argv[1])fpfingerprint(**payload)stateload_state()state[rounds]1state[fingerprints].append(fp)STATE_FILE.parent.mkdir(parentsTrue,exist_okTrue)STATE_FILE.write_text(json.dumps(state,ensure_asciiFalse,indent2),encodingutf-8)repeatssum(1foriteminstate[fingerprints]ifitemfp)ifstate[rounds]MAX_ROUNDS:print(fSTOP: 修正轮次超过上限{MAX_ROUNDS})return2ifrepeatsSAME_FINGERPRINT_LIMIT:print(fSTOP: 指纹{fp}累计出现{repeats}次没有新信息)return2print(fCONTINUE: round{state[rounds]}fingerprint{fp})return0if__name____main__:raiseSystemExit(main())它的行为很直接每记录一轮就检查两件事。轮次超过上限就停同一个指纹第二次出现就停。第二种检查正好抓住上面那种 A、B、A 的振荡——第三次虽然换了方向但指纹回到了老样子。跑起来的样子$ python tools/fix_loop_guard.py {failed_test:tests/orders/test_cancel.py::test_paid_order_cannot_be_cancelled,error_type:AssertionError,top_file_line:services/cancel_order.py:24,assertion:status is PENDING} CONTINUE: round1 fingerprint8f3c21a0b7d4 $ python tools/fix_loop_guard.py {failed_test:tests/orders/test_cancel.py::test_cancelled_order_rejected,error_type:AssertionError,top_file_line:services/cancel_order.py:26,assertion:status is CANCELLED} CONTINUE: round2 fingerprint1b90ee7c33af $ python tools/fix_loop_guard.py {failed_test:tests/orders/test_cancel.py::test_paid_order_cannot_be_cancelled,error_type:AssertionError,top_file_line:services/cancel_order.py:24,assertion:status is PENDING} STOP: 指纹 8f3c21a0b7d4 累计出现 2 次没有新信息 exit2退出码 2 是刻意的选择它和“检查失败”区分开。如果脚本返回 1外面看起来和测试失败一样流程会去修返回 2 表示“停下来交给判断力更强的一方”。这个区分让自动化流程可以直接处理两种不同的情况。为什么这个小东西值得单独存在而不是靠人看日志判断因为“数轮次”和“比指纹”是两件机器做得比人好的事它们机械、重复、容易在中途放松注意力时出错。把这两件事交给脚本人就可以把注意力放在判断上——是约束冲突还是缺信息还是范围失控。它也带来一个附带好处任务结束之后账本本身就是一份复盘材料。哪一轮最费时间、哪一轮开始出现重复、哪一次修改让范围扩大都能从这几行记录里看出来。相比“那天晚上大概改了十来次”这种描述账本给出的是一份可以拿来讨论的事实。4.3 停止之后写一份升级说明停下来只是动作接下来要做的是把上下文交接出去。一份能用的升级说明包含五块任务已取消订单再次调用取消接口时应返回明确错误 状态未完成已在第 3 轮按停止条件停下 失败指纹 8f3c21a0b7d4 tests/orders/test_cancel.py::test_paid_order_cannot_be_cancelled AssertionError services/cancel_order.py:24 assert status is PAID 1b90ee7c33af tests/orders/test_cancel.py::test_cancelled_order_rejected AssertionError services/cancel_order.py:26 assert status is CANCELLED 尝试过的修改 1) 判断写成 status PENDING - CANCELLED 用例通过PAID 用例失败 2) 判断写成 status ! CANCELLED - PAID 用例通过CANCELLED 用例失败 两个候选方向 A. 明确列出允许取消的状态集合 {PENDING}其余一律拒绝推荐 B. 继续细分错误PAID 与 CANCELLED 返回不同错误消息 需要你判断的问题接口是否需要为两种不可取消状态给出不同的错误消息这份说明里最关键的是最后一行。循环停下来通常不是因为“不会写代码”而是因为缺一个决定。把需要决定的问题单独写出来接手的人可以很快回答如果只丢一句“测试没通过”接手的人要先重走一遍整个循环。写升级说明时有一个容易被跳过的部分把已经排除的可能也写下来。比如“确认过不是事务边界的问题因为把事务去掉之后失败不变”。这类信息对下一个接手的人价值很高因为它能避免重复排查。它是循环过程中少数会被完整保留的资产之一。4.4 人接手之后在上面这个例子里接手的人大概花五分钟做了三件事确认业务规则是“只有PENDING可以取消”选择候选方向 A把错误消息的区分留给以后的接口设计。然后改动一行跑一次命令结束。值得注意的是这五分钟里没有一行代码是 Agent 写不出来的。它写不出来的原因是信息不足它不知道接口是否需要区分错误消息也无法从测试里读出这个决定的答案。这正是升级的意义——不是把人当作更强的执行者而是把人当作能做决定的一方。4.5 失败用例很多时怎么聚焦真实任务里经常一次红十几条用例。这时候如果每一轮都盯着“还剩几条”循环会一直在动但没有方向。更好的做法是先做一次分类把失败分成三组组 1同源由同一个原因引起的失败通常集中在同一个文件的堆栈上 组 2连带因为修组 1 而产生的新失败等组 1 修好之后再看 组 3无关本来就在失败和这次改动无关分类之后只把组 1 放进循环并且只允许针对它的指纹做停止判断。组 2 先记着不动组 3 单独记一条“已知失败”避免它污染指纹。这个做法有个额外好处它把“测试全绿”这个模糊目标换成了“组 1 全绿”。目标变小之后停止条件的判断也更清楚——组 1 的指纹重复了就停组 1 的指纹在变化就继续。4.6 停止条件也适用于人停止条件不只针对 Agent。人做调试时同样会陷入振荡而且更隐蔽因为人对自己的判断有更强的信心。把同一套规则用在自己身上并不丢人反而省时间当你发现自己在同一个文件里改了三轮、每次都在两个方向之间换就停下来写下“需要决定的问题”然后找人讨论。这也是为什么前面反复强调停止条件要写在任务单里。写在任务单里它对谁都适用写在某个人的经验里它只在那个人的状态好时有效。4.7 把停止条件接进流程停止条件如果只写在文章里下一次还是会发生同样的事。它必须出现在执行者能看到的地方。最省事的位置是任务单停止条件 - 修正轮次上限3 轮 - 同一失败指纹累计出现 2 次即停 - 出现下列任一信号立刻交给人类需要生产环境凭据、规格互相矛盾、依赖的接口不存在 停止后的交付物升级说明当前状态 / 失败指纹 / 尝试过的修改 / 候选方向 / 需要决定的问题四行字带来的变化很具体执行者在第三轮结束时知道该做什么而不是继续刷第十一次改动。在自动化流程里还需要让“停止”有独立的出口。上面那个脚本返回 2外层流程可以据此做三件事不重试、记录状态、通知人。这里最容易出的错是把 2 当成普通的失败并自动重跑那样等于把停止条件又拆掉了。还有一类停止原因值得单独处理因为缺少权限或凭据而停下。这类情况不需要人来判断业务规则只需要授权。把它做成一次明确的审批请求比让循环反复尝试更省时间。命令规则可以承担这个角色把某类命令写成需要审批decisionprompt执行时就会停下来问一次多条规则同时匹配时取最严格的一条forbidden 比 prompt 严格prompt 比 allow 严格。想确认某条命令的结果可以先用codex execpolicy check --pretty --rules 文件 -- 命令看一眼。最后是一个容易被忽略的细节账本要按任务隔离。.codex/fix-loop.state.json这类文件在每次任务开始时清空否则上一轮任务的指纹会残留在里面让新任务误触停止条件。把状态文件放在不进入版本库的目录也能避免它污染提交。4.8 把停止条件写进长期规则任务单适用于这一次长期规则适用于每一类任务。AGENTS.md里可以加一小节只写那些跨任务都成立的约定## 修正循环 - 同一失败指纹累计出现两次即停止写出升级说明 - 修正轮次上限 3 轮需要放宽时说明理由 - 出现下列信号立即停止并交给人类需要生产环境凭据、规格互相矛盾、依赖的接口不存在 - 停止后的交付物状态 / 指纹 / 尝试过的修改 / 候选方向 / 需要决定的问题四行的篇幅换来的是默认行为。执行者不需要每次重新讨论要不要设停止条件因为它已经是项目约定的一部分。规则文件里的这一节也应该保持简短——它写的是约定不是操作手册具体怎么记账、脚本放在哪里放在工具目录的说明里更合适。需要提醒的是规则里的话最好和脚本的实际阈值保持一致。如果规则写“两次即停”脚本写 3那么第一次触发的实际行为会让人困惑此后大家就会开始不信任规则。改阈值的时候两边一起改是最省事也最容易被忽略的一步。五、反例与代价反例一把“再试一次”当默认行为。没有记账、没有轮次上限、没有指纹检查。这种情况下的循环看起来一直在工作实际是在用时间换随机的运气。代价不只是时间上下文里会堆满过期结论下一个接手的人要先花时间分辨哪些尝试还有效。反例二阈值定得太紧或太松。一轮就停会把大量本来能自己解决的问题推给人二十轮才停等于没有停止条件。比较实用的起点是三轮修正上限加上同一指纹两次即停。它不需要是最终答案但必须是一个明确的数字先让流程有边界。反例三只看轮次不看指纹。有些循环每一轮都在“前进”——每轮失败的是不同的用例轮次计数器不断增加看起来很有进展。但把指纹列出来会发现三个指纹在循环出现。只看轮次的停止条件抓不到这种情况这就是为什么指纹检查和轮次检查要一起用。反例四把停止当成失败。停止之后如果没有交接说明接手的人需要从头重建上下文成本可能超过继续循环。升级说明看起来像额外的文书工作其实它是在把“已经花掉的时间”变成可用的资产。反例五指纹字段选得不稳。把耗时、随机生成的标识、内存地址放进指纹会导致同一个问题每次算出不同的指纹停止条件永远不触发。判断字段是否合适的办法是同一个失败重复跑两次指纹应该完全一样。反例六用修改测试来让循环结束。这是振荡里最危险的一步。循环本身没有问题问题在于终止它的方式删掉断言、把断言改宽、给用例加跳过标记都会让红色消失。有效的防护是让关键测试的改动需要单独说明比如要求测试文件的删除或断言修改在提交里被标记出来。反例七把停止条件当成一次性的设置。阈值设完之后如果从不回看可能出现两种偏差随着项目稳定三轮上限显得过紧于是大家开始手动忽略停止信号随着任务复杂度上升三轮又显得过松循环在没有方向的情况下跑满三轮。定期统计“停止之后有多少次确实需要人介入”才能让阈值跟着项目走。反例八停止之后没有记录。循环停下来人解决了问题然后谁也说不清当时是什么情况。几周后同类问题再次出现所有信息要从零开始重建。正确的做法是把这次的升级说明留在任务记录里特别是那一条“需要决定的问题”——它往往对应着一条应该补进文档或者检查的规则。反例九把停止条件交给模型自己执行。有的团队会在提示里写“如果你连续失败三次就停下来问我”。这句话有价值但它的执行者还是模型而模型正处在“想继续尝试”的状态里。更可靠的分工是模型负责记录指纹流程负责判断要不要停。判断的逻辑交给脚本、交给流程而不是交给当事的一方。六种反例的共同点是它们都让循环看起来在运转而实际的进展为零。停止条件的作用就是把“看起来在动”和“确实在前进”分开指纹变了说明有新信息指纹没变说明该换个方式——要么换方法要么换人。如果把这几条反例浓缩成一句判断任何一次重试都要能回答“这一轮和上一轮有什么不同”。不同点可以是新的错误信息、缩小了范围、排除了一种可能或者获得了新的信息输入。答不上来的时候这一轮大概率只是把同样的动作又做了一遍。六、落地步骤第一步给这次任务写下停止条件。做什么在任务单里写清轮次上限、指纹上限以及需要立即交给人的信号。为什么执行者在循环里最容易失去判断“还要不要继续”的参照点而这个参照点必须在开始之前就定好。怎么检查把这三行交给一个没参与的人问他“什么时候该停”。他能回答说明写得够具体。第二步定义指纹字段。做什么确定四个稳定字段失败用例名、异常类型、栈顶文件行号、关键断言。为什么指纹是判断“有没有新信息”的唯一依据字段不稳就等于没有依据。怎么检查对同一次失败连续记录两次比对两个指纹是否一致。第三步把记账放进循环。做什么用一个小脚本或者一份表格记录每一轮的指纹和轮次。为什么人在连续处理几条失败时容易丢失全局记账把“这是第几次、是不是老问题”变成可查的事实。怎么检查跑一次真实任务确认账本里有每一轮的指纹记录并且能看到重复。第四步让停止有独立的信号。做什么让停止返回一个与普通失败不同的退出码并在外层流程里区分处理。为什么停止意味着“需要人判断”普通失败意味着“继续修”两者被混在一起时停止条件会退化成一次重试。怎么检查在外层流程里故意触发一次停止确认它没有自动重跑。第五步准备升级说明模板。做什么把状态、指纹、尝试过的修改、候选方向、需要决定的问题固定成五块。为什么接手成本主要花在重建上下文上模板把这块成本压到最低。怎么检查拿一份写好的升级说明给第三方看看他能否在五分钟内说清该做什么决定。第六步把权限类停止单独处理。做什么把需要凭据或高危命令的情况做成一次明确的审批请求而不是循环里的猜测。为什么这类问题的瓶颈不是推理是授权用循环去等授权是纯粹的浪费。怎么检查确认这类命令在命令规则里被标成需要审批并且能正确触发。第七步按任务清理账本。做什么每次任务开始时清空状态文件任务结束时把关键指纹留在记录里。为什么旧状态会误触停止旧的指纹记录则是复盘时最有用的材料。怎么检查任务结束时看一眼账本能不能说出这次任务最难的一轮是哪一轮。第八步定期回看阈值。做什么每隔一段时间统计一次有多少次任务触发了停止其中有多少确实需要人介入。为什么阈值太紧会让停止变得频繁而无用太松则失去作用。统计数据是调阈值的唯一依据。怎么检查如果停止后大部分情况下人都能一句话解决说明阈值可能偏紧如果人接手后发现信息不足说明低于阈值时漏掉了该停的信号。第九步把停止之后得到的结论沉淀下来。做什么把“需要决定的问题”变成一条规则、一条检查或者一份文档补充。为什么同一个决定第二次被问到时说明上一次的答案没有被留下来。怎么检查看最近三次停止记录每次是否都产生了一处可以查到的改动。如果没有说明升级说明写完就丢了。第十步让停止信号出现在执行者视野里。做什么把停止条件、当前轮次和当前指纹写进任务执行过程中可见的位置比如任务单顶部或者一份共享的状态文件。为什么判断“要不要继续”需要同时看到历史和现状而处在循环里的人最容易丢失的正是这两样。怎么检查在循环中途随机问一次“这是第几轮、这次指纹和上一轮一样吗”如果答案需要翻日志才能得到说明信号还不够显眼。把八步合成一份可以直接复制的模板任务____________________________________ max_fix_rounds ____ # 修正轮次上限 same_fingerprint_limit ____ # 同一指纹累计出现次数上限 立即交给人的信号____________________________ 指纹字段用例名 异常类型 栈顶文件行号 关键断言 停止时退出码____与普通失败区分开 升级说明五块状态 / 指纹 / 尝试过的修改 / 候选方向 / 需要决定的问题 账本位置____________________不进版本库每任务清空这份模板有一个隐含的好处它把“继续尝试”变成了需要条件的动作。没有触发停止条件就继续触发了就停。两种结果都有明确依据不再取决于当下谁更想坚持一下。七、FAQ问为什么是三轮而不是五轮或者十轮三轮是一个起点不是定理。它的依据是成本前两轮通常能解决信息不足造成的失败比如文件名写错、字段名不匹配到第三轮还在同一个问题上打转说明缺的多半是判断而不是尝试。轮次上限要配合指纹检查一起用因为有些问题会在两轮里“看起来有进展”实际是在两个方向之间摆动。如果你的任务类型确实需要更多轮比如大范围重构可以调高但要同时提高记录质量否则只是把振荡跑得更久。问指纹里的栈顶行号会变这样还算同一个指纹吗会变而且这是有意义的。行号变化说明失败的位置变了可能意味着你改动了代码结构。判断标准是如果这次改动确实解决了上一次的问题那么指纹应该变成新的如果只是把判断条件从一种写法换成另一种写法行号变了但失败性质没变这时候你需要在指纹里加上关键断言这个字段它通常能把这个差异体现出来。把关键断言写进指纹是让指纹更能反映问题性质的关键。问如果每一轮都有一点点新信息还要停吗要区分“新信息”和“新细节”。有效的进展会让问题的范围变小比如从“两个用例都失败”变成“只剩一个用例失败”或者从“不知道哪里错”变成“知道是事务边界的问题”。无效的进展只是换了一个失败方向。一个实用的判断方法看候选方案的数量。如果每轮之后你脑子里可选的方案在变少说明在收敛如果每轮都在增加新的可能那就是在发散该停了。问多个用例同时失败时指纹该取哪一个取最能代表这次失败性质的那一个通常是第一个失败或者修改目标直接对应的那一个然后把其余失败列在升级说明里。不要把所有失败拼成一个指纹那样任何一点变化都会改变指纹重复检测会失效。如果失败足够多更好的做法是先只处理一个用例把其他用例暂时标注为“已知失败”让循环聚焦在一个目标上。问停止条件会不会让 Agent 太早放弃停止条件和放弃不是一回事。放弃是没有任何产出地结束停止是带着完整状态和人交接。真正会让人担心的场景是“它明明能自己解决却被轮次上限打断了”。这种情况的解决办法不是取消上限而是把上限和指纹检查分开轮次上限到达时先看一眼指纹有没有变化变化了就允许再来一轮指纹没变就直接停。问不同任务的阈值要不要不一样要。判断依据是失败的性质和回滚成本。修改一个字段名或者补一条日志两轮足够涉及迁移、并发和外部集成的改动轮次可以放宽但要把“需要生产环境”这类信号设为立即停止。更实际的做法是给任务分两类普通改动用默认阈值高风险改动用更严格的信号检查加人工确认。问有没有不需要停止条件的任务有探索性任务就是典型。比如“研究一下这个模块为什么慢”“找出可能导致超时的几个原因”这类任务没有明确的完成标准循环的终止条件是“信息够了”。但即便如此也应该有预算最多花多少时间、最多跑多少次。探索任务最容易无限延伸因为总能再找一条线索。问怎么防止循环通过修改测试来终止让测试的改动可见。三种做法把测试文件的删除与断言修改单独列在提交摘要里在评审清单里加一条“这次有没有同时修改被测代码和断言”在门禁里要求测试文件的变化必须由人确认。第三种最硬但会带来一些摩擦适合关键模块第一种最轻适合大多数项目。问停止之后人应该先看什么先看“需要决定的问题”那一行。如果那一行写清楚了接下来的动作就只是决定加执行如果那一行写不出来说明升级说明还没写完。之后再看尝试过的修改避免重复走同一条路。最后看指纹它决定了要不要把这次的失败补进测试或者规则里防止下一轮再从同一个地方开始。问这套办法和前面几篇讲的东西怎么衔接第一篇讲的是要有闭环也就是检查从“说法”变成“命令的结果”这一篇讲的是闭环转起来之后什么时候该停。第二篇的四个节点里停止条件属于“失败后”那个节点第四篇的配对思路在这里也适用——停止条件是前馈提前写清触发之后的升级说明是反馈事后交接。把这几件事拼起来一次任务才算既有方向又有边界。问停止条件需要写在提示词里吗需要但它不是唯一的地方。提示词里的版本让执行者知道边界在哪里任务单里的版本让这次任务有具体数字脚本里的版本负责实际判断。三处说的是同一件事时最有效。只有提示词那一处时实际上是在依赖执行者自己遵守——前面几篇反复说明过这类依赖只能提高概率不能保证结果。问如果人也不确定该怎么决定怎么办那说明这次停止暴露的是一个真正的问题而不是流程噪音。处理方式是把问题升级成一次讨论写清两个候选方向和各自的代价交给对这块业务最熟悉的人。比起让循环继续猜这样做的信息量更大也更容易留下结论——因为讨论通常会产生一句话而那句话可以变成规则或者检查的一部分。问这套做法对小任务会不会太重小任务可以用最轻的版本只在任务单里写一行“同一失败重复两次就停并告诉我卡在哪”。不需要脚本不需要账本文件也不需要统计。等到你发现自己每周都要处理一次循环问题时再把它升级成脚本和阈值。重量应该跟着问题的频率走而不是跟着方法论走。问停止条件会不会让任务看起来像是失败了这取决于你怎么描述它。如果任务记录里写的是“失败”接手的人会带着压力来处理如果写的是“按约定停止等待一个决定”事情的性质就变成了正常的流程节点。语言会影响后续行为这一点在流程里很实际把停止描述成一次正常的交接团队才会真的用它而不是硬撑到最后一刻。八、动手练习与小结这次的练习产出三样东西都可以直接用在下一次任务里。第一样是停止条件三行。打开你手头的任务模板加上停止条件 - max_fix_rounds ____ - same_fingerprint_limit ____ - 立即交给人的信号____________________第二样是指纹提取方式。写清你的项目里怎么拿到那四个字段失败用例名从哪一行输出里读、异常类型在哪、栈顶行号在哪、关键断言怎么取。这份说明应该短到能贴在任务单里。第三样是一次真实的停止记录。找一次最近发生过的循环把它的指纹列出来看看有没有重复。如果有重复说明当时就该停如果没有重复说明那次循环的问题更可能是信息不足需要补的是任务描述而不是停止条件。如果你手头暂时没有合适的循环可以复盘可以先做一件更小的事给你最常用的那条验证命令加一个“轮次记录”的习惯。每次重跑之前先写一行“第几轮、这次要看什么变化”。这个动作只花几秒钟但它会让你在第三轮的时候突然意识到“我这次期待看到的东西和上一轮一样”。自检时问四个问题这三个数字写进任务单了吗指纹字段稳定吗停止有没有独立的信号升级说明的五块能填齐吗四个问题都能回答你的循环就有了边界。还有一个更省事的做法把这四个问题做成任务模板里的四行填空。模板的作用在第三篇里说过——它让正确的做法变成最省力的做法。当你不需要每次都重新组织这几行字时停止条件才真正成为流程的一部分而不是一份需要被记住的约定。再补一个小练习用来验证你的判断找一次已经结束的任务假装它正在第二轮失败然后问自己“如果现在再改一次我期待看到什么变化”。答不出具体变化时说明这一轮大概率会重复答得出时说明你已经知道下一步该验证什么。回到那个晚上。十一次修改里前两次是有价值的它们带来了两次真实的失败信息暴露出两条互斥约束。第三次开始循环就进入了重复。如果当时有一行停止条件第三次结束时它会停下来把“接口是否需要区分两种不可取消状态的错误消息”这个问题交给能回答的人剩下的时间可以用来做别的事。这一篇是这一系列的第五篇也是最后一篇收束在“边界”上的文章。第一篇讲闭环第二篇讲控制的四个节点第三篇讲文字规则和可执行检查的区别第四篇讲前馈和反馈的分工这一篇讲循环什么时候该停。五篇连起来是一条线让 Agent 的工作可判定、可限制、可验证、可沉淀并且在必要的时候可以体面地停下来。如果只能带走一句话可以带走这条飞轮的价值来自每一轮带来的新信息而不是轮次本身。指纹相同的一轮无论改了多少行代码都没有让它转得更远。