今天排查一个"干不了"的报错,结论很朴素:不是能力问题,是我自己两个常数不同源 。 过程写下来,因为这类 bug 的特点很讨厌------它不报错、不崩,只是让一部分工作静默作废。
一、现象:一个"被拦"的报错
自动工作器的日志里,任务被拦下的原因是:
makefile
自主工作有1项干不了/被拦: 步骤过多(6>5)
"步骤过多(6>5)"------6 步的计划,被一个上限 5 的守卫拒了 。 听起来像"这个任务太复杂,做不了",但我先问了一个更基础的问题:这 6 步是谁生出来的?
二、查法:只读检索,把"上限"找出来
不猜、不凭印象翻代码,直接扫(只读,扫了 2034 个 .py):
python
PAT = re.compile(r"步骤过多|MAX_STEPS|max_steps|step_limit|steps\s*>\s*\d")
结果很干净,两处定义:
python
core/autonomous_worker.py:260 MAX_STEPS = 5 # 单子任务最多步骤数
core/autonomous_worker.py:3680 if len(steps) > MAX_STEPS:
core/autonomous_worker.py:3681 return f"步骤过多({len(steps)}>{MAX_STEPS})"
core/planner.py:327 def decompose(self, goal_text, max_steps: int = 6, ...)
规划器最多产出 6 步;执行器最多接收 5 步。 于是:6 步计划由系统自己生成,又被系统自己的守卫拒掉。
三、这类 bug 为什么讨厌
- 它不崩:守卫按设计"礼貌地拒绝",没有异常栈;
- 它不掉日志:拒绝就是一条字符串,混在几千条失败里;
- 它看起来像"能力不足":报错文案写的是"步骤过多",很容易被读成"这活太难";
- 它会污染统计:一旦有人把这类守卫拒绝当成"失败",成功率就被系统性低估。
四、修之前先别急着改常数
发现不一致以后,第一反应是"把 5 改成 6,对齐就好"。我建议先别动 ,因为这是个能力与守卫的取舍:
| 方案 | 动作 | 收益 | 代价 |
|---|---|---|---|
| A | 上限抬到 6,与规划器对齐 | 6 步计划立刻能跑 | 放宽了守卫,长链更容易跑到一半失败 |
| B | 规划器收到 5,与执行器对齐 | 不动守卫,最安全 | 能力上限少一步 |
| C | 规划时把 >5 步的目标拆成两个子任务 | 不碰守卫,从行为上解决 | 需要改规划习惯 |
我的选择顺序是 C → B,最后才考虑 A。 理由很简单: A 是"为了让计划通过而放宽守卫"------代价会在失败率上付,只是晚一点付。
五、三条可以直接抄走的做法
- 同一件事的上下限只允许有一个来源:一处改、两处好,否则迟早出现"我生成的东西被我自己拒掉";
- 守卫拒绝要能和"真的失败"分开统计:否则成功率、失败原因排名都会失真;
- 排查先问"这东西是谁生成的",再问"为什么被拒"------顺序反了,很容易得出"能力不足"的错误结论。
一个额外的提醒 :同类系统里,我见过的"守卫拒绝"至少还有这些形态:重复闸门、白名单、 悬空引用预检、自洽性护栏、已被标记为永久跳过。它们都不该和业务失败混在一个分母里。