Part 3 规划与分解:让 agent 学会”先想后做”

墨叔:老哥,前面两 Part 你已知道 agent 是「模型 + 循环」,也知道怎么给它接上工具这只手。可接下来这个坑,是大多数人第一次让 agent 干「稍微大点的事」就摔跟头的地方——你让它写个功能、改一整套模块,它干到一半就跑偏、重复、甚至自己跟自己打架。这 Part 我要跟你掰扯清楚:模型为什么「不会规划」,以及我们怎么替它把「先想后做」这层补上。


3.1 规划为什么难:LLM 的”想一步走一步”缺陷

老哥,我先问你一个会让你有点不舒服的问题:你有没有试过让 agent「帮我把这个支付模块重构成策略模式,顺便把测试补上」?它一开始挺像样,列了几个文件,改着改着,突然开始重复你已经说过不要的写法,或者后半段用的命名规范和前半段对不上,最后你一看,整个活儿散架了。你心里骂一句:这玩意儿怎么连「从头到尾想清楚再动手」都不会?

来,墨叔把这层底裤扒开给你看。

模型本质上是个「局部续写器」。你再回想 Part 1 那句狠话:它只干一件事——在给定一段文字时,预测「接下来最合理的那段文字」。注意,是「给定这段文字」「预测接下来一段」。它做每一个决策时,眼前只有「当前这张工作台(上下文窗口)上摊着的东西」,它据此猜「下一步最该写什么」。它没有一个独立存在于脑外的、覆盖整个任务的「全局蓝图」。

这跟「规划」差在哪?规划这事儿,人是怎么干的?你接一个重构任务,脑子里先有一张总图:目标长啥样、现状啥样、中间要跨哪几座桥、哪步依赖哪步。你是在「全局视角」下,反推「第一步该干啥」。而模型呢?它从第一句开始,就是走一步看一步——它不在任何一步「俯瞰全局」,它每一步都只是「基于到目前为止写下的字,续写下一段」。

我给你一个能扎进记忆的比喻。

模型像个被蒙着眼睛、只能看见脚下三米路的送货员。他手里没有整张城市地图,只知道「现在脚下的路牌写着 A 街,我上一步从 B 街过来,我记忆里老板说终点是 C 仓库」。他每走一步,只看当前脚下的路决定下一步往哪拐。大多数时候路网规整,他瞎摸也能到;可一旦路开始分叉、出现死胡同、或者中途有个路口他上次走过但记混了方向——他就原地打转,或者越走越偏,而且他自己并不知道偏了,因为他看不见全局,他只觉得「我脚下这步走得挺合理啊」。

反过来,你写 Go 时依赖的编译器,干的是完全相反的事。编译器拿到你整份源码,先全局扫描一遍,建成一棵完整的 AST(抽象语法树),再做类型检查、再做优化、最后才生成机器码。它是「先有全貌,再有动作」。你写代码时你的脑子也是这么干的:你先在脑里把函数签名、调用关系、边界情况过一遍,才落键盘。人类和编译器都是「全局分析 → 再执行」,唯独模型是「执行即分析、分析即续写」,这两套心智模型是拧着的。

所以老哥,你现在该品出来了:模型的「想一步走一步」不是 bug,是它的出厂设定。你不能用「人家的工程师都会先画流程图再写」这个期待去要求它,因为它压根没有「流程图」这个内部数据结构。它有的只是「脚下三米」。

这就解释了长任务为什么容易「中途迷失」。任务越长,步骤越多,模型脚下的路牌(上下文)就越杂。两个问题会同时炸: 一是目标稀释——你最初说的「目标是 C 仓库」,被中间几十轮工具返回、几百行代码、一堆报错信息稀释到窗户口落里,模型走到后半程,已经不太记得「为啥出发」了(你见过 Part 1 讲的「lost in the middle」吧,中间塞太多,首尾反而被忽略); 二是前后矛盾——因为每一步都是局部合理的续写,前半段定的规范、选的技术、起的名字,到后半段模型可能「换了种说法」就接不上了,它不会主动回头检查「我前面是不是定了用 gRPC」。

我给你一个真实的翻车案例。有次我让一个 agent「生成一整套包含用户、订单、库存三个服务的微服务脚手架」。前半程它很乖,定了「服务间用 gRPC 通信,proto 统一定义在 api/ 目录」。走到订单服务时,它写着写着,可能因为某次工具返回里出现过 REST 示例,它后半段就开始用 HTTP+JSON 调库存服务了。最后交付的「脚手架」里,用户→订单是 gRPC,订单→库存是 REST,协议不统一,编译都过不了。根因就一个:它没有一个贯穿全程、可随时回看的全局约束清单,每一步都只是在「当下上下文」里挑一个「看起来顺」的写法。

你可能会杠一句:人干活不也是一步步干吗?凭啥人能长程一致,模型就漂?墨叔点破这层。人的长程一致,靠的是脑子里始终挂着一张「不变的约束网」:语言规范、团队约定、架构红线,这些是「前提」而不是「上下文里的某句话」——它们不随你走到第几步而消失,因为它们在你脑里是「长期驻留」的,不是「摊在桌面上、会被新单子挤掉」的。模型呢?它的「约束」只能活在上下文窗口这张会燃烧的纸上(你记得 Part 1 那张纸吧)。它前半段写的「用 gRPC」,只是纸上的某几行字;走到后半段,纸上堆满了新代码、新报错,那几行「用 gRPC」被挤到纸角甚至被截断,模型「看不见」了,自然就按新出现的 REST 示例续写。一句话:人把约束存在脑里(持久),模型把约束存在纸上(易逝)——这不是智商差,是存储架构差。

给你一个和你本行咬合极紧的连接。你写 Go,编译器做「全局数据流分析(dataflow analysis)」:它扫描整份代码,算出「每个变量在每一点可能取什么值」「哪些分支可达」,再做死代码消除、常量折叠。这个分析是跨整个编译单元、基于确定的语法树的,所以它能发现「你第 3 行定的类型,到第 200 行对不上了」。模型完全没有这个能力——它不建语法树,不做跨全文的数据流分析,它只在「当前窗口的那段文字」上做概率续写。你早就在享受「编译器替你保证全局一致」,而 agent 缺的正是一个等价的「全局一致性保证者」。注意:这个保证者不是模型自己,而是我们下一节要造的「计划 + 对齐机制」——相当于我们手动给 agent 配了个「会跨步检查约束的编译器前端」。

还有个容易被忽略的点:模型「想一步走一步」的代价,会随任务长度非线性恶化。你让它干 3 步的小活,窗口里信息少,偏离概率低;干 30 步的大活,窗口被塞爆、目标被稀释、前后矛盾的概率不是乘 10,而是乘几十——因为每一步的偏差都会「污染」下一步的上下文,错误像滚雪球。这就是为什么「给 agent 一个长任务」和「给 agent 十个短任务串起来」,后者往往更稳:每个短任务窗口干净、约束清晰,雪球滚不起来。这条经验,后面 Part 5 工程化讲「把大任务切片」时会再回来。

顺着这个,老哥你该推出来一个判断:agent 天生更适合「短而封闭」的任务,而非「长而开放」的任务。一个「帮我把这个函数重构成更可读的版本」是封闭的(输入输出清晰、窗口装得下全貌),模型自己续写都稳;一个「帮我做个电商系统」是开放的(跨度无穷、约束散落各处),再聪明的模型也会在中途迷失。这反过来帮你做技术选型:你决定「要不要上 agent」时,先看任务「能不能被切成若干封闭的短任务」——能切,agent 好用;切不动,要么先人工把架构定死(把开放变封闭),要么别指望 agent 一次搞定。你写 Go 时也不会把一个超长事务写在一句里、赌它原子成功,而是拆成可独立提交的小事务——agent 任务也一样,先切片,再让循环转。这条「封闭优先」的原则,是 3.1 所有机理落到你日常决策上的那根线。

那这事儿有救吗?有。但救法不是「等模型变聪明」,而是「我们替它把全局蓝图造出来,并强制它每步都对着蓝图走」。这就是 3.2 要讲的 Plan-and-Execute——先画地图,再走路。但在进 3.2 之前,你先把这一节的因果链嚼碎:模型是局部续写器 → 没有全局蓝图 → 长任务目标稀释、前后矛盾 → 我们必须外挂一个「全局计划」并让它每步对齐。

【思考题】

  1. 既然模型「想一步走一步」,那为什么它写「单文件里的一个小函数」往往还挺靠谱,而你一让它跨多个文件干大事就容易翻车?这中间的分界在哪?
  2. 你作为后端,日常写代码时其实也是「一行一行敲」的,为什么你不会像模型那样前后矛盾?你和模型在「顺序执行」这件事上的本质区别是什么?
  3. 蒙眼送货员的比喻里,「老板说终点是 C 仓库」这句对应 agent 里的什么?如果这句话被中途的噪音盖住了,除了「重新贴回目标」,你还能想到什么结构性办法防止他走偏?

【参考答案】

  1. 分界在「上下文窗口能不能装下完整的相关事实」。写单文件小函数时,这个函数要用的类型、要调的兄弟函数、命名约定,基本都在当前窗口里「摊得开」,模型脚下三米就是全局,局部续写≈全局正确。一旦跨多文件,信息被拆散在几十个文件、几百次工具返回里,窗口装不下「全貌」,模型每一步只能基于「局部可见」做决策,全局一致性就崩了。这印证了 Part 1 的「上下文窗口是工作台」——任务复杂度超过工作台承载力,规划能力就退化。所以一个实用原则是:任务能被「摊进窗口的事实」完整覆盖时,模型自己续写也行;覆盖不了,就必须外挂计划。
  2. 你和模型都「顺序执行」,但你的顺序执行是被一个先存的全局设计约束着的:你在落键盘前,脑里/文档里已有函数签名、模块边界、命名规范。你敲每一行,都是在「核对设计」而非「凭感觉续写」。而模型敲每一行,只是「基于上文预测下一行」——它脑里没有那个先存的设计,设计是「随着文字生成而被临时构造出来的」。一句话:你是「先有图,再照图施工」;模型是「边施工边在墙上画施工图」。前者前后一致因为图在先,后者漂移因为图跟着施工一起长。
  3. 「终点是 C 仓库」对应 agent 的系统提示里的「总目标 / 约束条款」(或者更广义的「贯穿全程不可违背的硬规则」)。光「重新贴回目标」是治标——每轮往窗口里重申目标,能缓解稀释,但窗口满了还是会被挤。更结构性的办法有三:一是把目标与约束外置成不可变的长期记忆/配置文件,任何一步执行前都从外部读回来(对应 Part 4 的长期记忆);二是让计划本身成为「每步都要对齐的清单」,执行器每步开工前先核对「这步是否违反计划的某某约束」;三是设置独立的监督者(critic / 校验步骤),专门盯着「当前动作是否偏离总目标」,一旦发现偏差就拉回。这三条其实是 3.2~3.4 的主线,你现在给的直觉已经摸到门了。

小结:模型是「局部续写器」,出厂设定就是想一步走一步、没有全局蓝图;长任务因此会目标稀释、前后矛盾。这不是它笨,是它根本没有「全局计划」这个内部数据结构——所以解法不是等它变聪明,而是我们外挂一张它每步都能对齐的地图。


3.2 Plan-and-Execute:先画地图再走路

老哥,上一节你认清了病根:模型自己不会画全局地图。那墨叔接下来这招,就是「既然你不会画,我帮你画,而且画完让你照着走」。这套打法行内叫 Plan-and-Execute——把「规划」和「执行」拆成两个角色,让一个负责想清全局,一个负责落地细节。

你先抓住最核心的那句话:计划,变成了一个可以拿来检查的中间产物。什么意思?在纯 ReAct 里,模型的「计划」是散落在每轮推理里的——它边走边想,计划即动作,你根本抓不住它「到底打算怎么干」。而 Plan-and-Execute 强制模型先一次性(或阶段性的)产出一份结构化的步骤清单:第一步干啥、第二步干啥、每步预期产出是啥。这份清单是显式的、落了地的文本,你(和人类)能在它动手之前先审一遍。

我给你一个落到实处的 Python 示意,把骨架钉死:

# Plan-and-Execute 示意(非可运行工程)
from typing import List, Dict
 
def planner(goal: str, context: str) -> List[Dict]:
    """规划器:只看目标和已知上下文,产出步骤清单,不碰任何工具。"""
    prompt = f"目标:{goal}\n已知背景:{context}\n请输出一个有序步骤清单,每步含 id/动作/预期产出。"
    plan_text = model_chat(prompt, mode="plan")        # 只让模型"想",不让它调工具
    steps = parse_plan(plan_text)                       # 解析成 [{id, action, expect}, ...]
    return steps
 
def executor(steps: List[Dict], tools: Dict[str, callable]) -> List[Dict]:
    """执行器:逐步落地,每步可内部用 ReAct 调工具,并回写状态。"""
    results = []
    for step in steps:
        # 执行器只盯着"当前这一步",细节靠工具调用去探
        outcome = run_one_step(step, tools)             # 内部仍是一个小 agent loop
        step["status"] = outcome["ok"] and "done" or "failed"
        step["output"] = outcome["summary"]
        results.append(step)
        if not outcome["ok"]:
            # 失败时不是硬挺,而是把状态暴露给上层(人或规划器)决定要不要重规划
            break
    return results
 
# 主流程:先画地图,再走路,地图错了还能重画
goal = "把支付模块的校验逻辑抽成策略模式并补测试"
plan = planner(goal, context="现有代码结构见 main.go / pay.go")
for attempt in range(2):                                # 至多重规划两次
    report = executor(plan, tools)
    if all(s["status"] == "done" for s in plan):
        break
    plan = planner(goal, context=f"上次失败报告:{report}")   # 带着失败经验重画地图

老哥,你盯着这段代码,品三个点。

第一,planner 和 executor 的「信息职责」分开了。planner 只负责「想全局、列清单」,它这一轮不调任何工具——因为它要的是俯瞰视角,不是被某个文件的具体细节带偏(你回想 3.1 讲的「脚下三米」陷阱:如果规划时也让它去读文件,它又掉回局部续写了)。executor 反过来,只盯着「当前这一步」去探、去调工具、去落地,它不需要操心「后面还有几步」。

第二,计划是「可检查的文本产物」。注意 plan 是个列表,你完全可以在 executor 开工前,把它打印出来给人看:「哥,我打算这么干,你同意不?」——这就是 Claude Code 的 plan mode 那个「我先列计划,你确认我再动」的底子。把模糊的意图变成显式的、可审阅的步骤,是人类信任 agent 的关键一跃。

第三,失败能触发「重规划」,而不是傻乎乎一条道走到黑。看主流程里那个 for attempt:executor 跑挂了,把「失败报告」喂回 planner,让它带着新信息重画地图。这又把 3.1 的「目标稀释、前后矛盾」从结构上兜住了——计划不是一次生成就焊死,而是能根据真实执行反馈迭代。

我给你个生活化类比收一下。Plan-and-Execute 就像装修队的标准流程:先出施工图和报价单(plan),业主签字,再按图施工(execute)。图是能审、能改、能追责的。而纯 ReAct 像个「边干边想的游击工长」:他脑子里有图,但那图只在他自己脑子里,你审不了,他干到一半改主意你也不知道,最后墙刷了一半发现和水电冲突。显式计划的价值,不在「让模型更聪明」,而在「让过程可干预、可追责、可迭代」——这恰好是工程系统最看重的属性。

给你一个和你本行的强连接。你做后端大需求时,难道不是先写技术方案 / 设计文档吗?那份文档就是你的「plan」:列出要改哪些模块、接口怎么变、数据怎么迁。Plan-and-Execute 不过是把「你写设计文档」这步,显式地交给模型先产出一份草稿,再由你或它自己执行。区别只是:你的设计文档是人写的、静态的;agent 的 plan 是模型生成的、且能根据执行反馈动态重画。你早就在用「先规划再执行」这套工程纪律,现在只是把它变成 agent 循环里的一个明确环节。

再说个真实经验。很多一线 agent 系统(包括你说的 Claude Code 的 plan 模式)都验证了:让模型先只输出计划、不执行,计划的合理度明显高于「边干边想」。原因很朴素——「只规划」那一轮,模型的注意力全在「怎么拆任务」上,没有被工具返回的具体噪音(报错、长文件)拽偏;而一旦开始执行,上下文被细节淹没,再让它「兼顾全局规划」就力不从心了。所以「规划与执行分离」不是花活,是顺着模型「局部续写」的弱点,给它配了个专门负责全局的角色。

不过老哥,我得给你泼半盆冷水,免得你以为「分离」就万事大吉。Plan-and-Execute 有个隐藏陷阱:计划会过时(plan staleness)。你列计划时基于「想象中的代码现状」,可执行到第 3 步,真实代码已经变了——比如你原计划「在 user.go 加个方法」,结果第 2 步因为报错,user.go 被改成了另一副模样。这时还僵硬照着旧计划走,第 3 步就对不上现实了。所以老练的实现,不会「列一次计划、闭眼执行到底」,而是分阶段规划(hierarchical planning):先列粗粒度大计划(先探、再写、再测),每进入一个大阶段前,再让 planner 基于「当前真实状态」细化这一阶段的细计划。这就像你写大需求:先有总体技术方案,落到每个 sprint 前再细化本周的 task——你不会年初就把十二月每天写哪行代码定死。模型也一样,计划要「滚动更新」,不是「一次焊死」。

还有个粒度耦合点你得记住:3.2 的「计划步骤」和 3.3 的「任务粒度」是同一件事的两面。planner 列的步骤如果太粗(比如「实现注册功能」),executor 内部就得再跑一个小 ReAct loop 去探细节,那「规划与执行分离」的好处(planner 不被细节拽偏)就打折了——因为细节探查又溜回执行层,executor 自己又成了个局部续写器。所以好的实践是:planner 列的步骤,粒度刚好到「executor 一步之内不用再犹豫顺序就能干完」(这正是 3.3 要讲的「刚好能独立完成且有明确产出」)。计划层和粒度层是咬合的齿轮,调一个另一个也得跟着调。

给你一个工程落地的连接。你自己带需求时,技术方案文档是不是也「先总体、再分阶段细化、执行中根据实际微调」?Plan-and-Execute 的精髓,就是把这个你早就在用的「文档驱动开发」纪律,显式地编进 agent 循环:planner 产出「文档草稿」,executor 当「按文档实现的工长」,重规划当「根据实现偏差修订文档」。你不是不会,你只是没把「写设计文档」这步,从「人写给人看」变成「模型写给模型循环看、且能被代码校验」。这一步平移,是 agent 从玩具变生产级的关键一跃——后面 Part 5 工程化会反复用到「计划即规格(spec)」这个思想。

最后给你一个「代价账」的提醒,免得你以为分离只有好处。Plan-and-Execute 比纯 ReAct 多花了一次(甚至多次)模型调用:先调 planner 出计划,再调 executor 跑每步。这意味延迟和成本都上去了——你用「先规划再执行」换来了「更稳、可审阅」,代价是「更慢、更贵」。这和你本行的「编译型 vs 解释型」是同一个权衡:编译语言先全局分析再跑(慢一点启动,但全局一致、运行快),解释语言边跑边解析(启动快,但错了才炸)。agent 选「先规划」还是「边干边想」,本质就是选「编译型」还是「解释型」——任务越复杂、越不能出错,越该付这笔「规划税」;任务越短越封闭,纯 ReAct 更划算。没有免费的正确,只有合算的取舍。你当年在「强类型编译」和「脚本快速验证」之间反复权衡,现在这套直觉原样搬来定 agent 的规划策略就行。

还有一层价值你得看到:显式计划是人类介入(HITL)最自然的一个抓手。纯 ReAct 跑起来是一股不可中断的文本流,你想拦都不知道从哪拦;而 Plan-and-Execute 在「计划产出、执行前」天然有个停顿点——你可以让循环在 executor 开工前把 plan 亮给你,「哥,我打算这么干,你点确认我再动」。这一个确认环节,把「概率程序」最危险的「擅自行动」风险,关进了「人审」这道确定性闸门里(呼应 Part 1 的 HITL 心法)。尤其涉及删文件、发请求、改生产配置这类危险动作,先亮计划再执行,比边干边拦稳一万倍。所以 plan mode 不只是「更稳」,更是「更安全、更可控」——这是它在一线生产级 agent 里被普遍采用的根本原因。

【思考题】

  1. 为什么 planner 这一轮「最好不要让它调工具」?如果让 planner 边规划边读文件,会发生什么(结合 3.1 的脚下三米陷阱说)?
  2. 计划作为「可检查的中间产物」,除了给人审阅,你还想到它能拿来干什么?(提示:想想「重规划」和「进度追踪」)
  3. 你本行的「技术方案文档」和 agent 的 plan,除了都是先规划再执行,还有一个关键区别:你的文档一般不自己改自己。agent 的 plan 为什么要设计成「能根据失败重画」?这是优势还是隐患?

【参考答案】

  1. 让 planner 边规划边读文件,它就又掉回 3.1 的「脚下三米」陷阱了:它本该俯瞰全局列清单,结果一读某个具体文件,注意力被那个文件里的细节(命名、报错、长代码)拽走,列出来的计划会过度偏向「刚读到的那个文件」,丢失跨文件的全局视角,甚至提前陷入某个实现细节里纠结。planner 的价值就在于「站在高空看全貌」,而工具调用是「钻进地面看细节」——两者混在一起,高空视角就没了。所以好的实现里,planner 的输入只有「目标 + 已知的粗粒度背景」,它产出的是「步骤级」清单,细节探查留给 executor。
  2. 计划除了给人审,至少还能干三件事:一是做进度追踪的标尺——executor 每完成一步就打勾,人和系统一眼看出「完成 3/7」,而不是面对一片模糊的对话流;二是做重规划的锚点——失败时对照计划知道「挂在第几步、前面哪些已验证可用」,重画时不用从零来;三是做预算/权限闸门——可以在计划阶段就预估「这活要调几次危险工具」,提前让人确认范围,而不是执行到一半才问。一句话:显式计划把「不可见的黑盒推理」变成了「可观测、可治理的工程对象」,这正是后面 Part 5 可观测性的前置。
  3. agent 的 plan 要能「按失败重画」,根因是模型的初始计划也是概率性的、可能错,而真实执行会暴露它计划时没料到的约束(比如某文件其实不存在、某接口签名和想的不一样)。静态焊死的计划碰上这种「计划与现实的偏差」就只能硬撞墙。能重画是优势:它让系统有「根据事实纠偏」的能力,呼应 3.1 说的「用真实反馈拉回地面」。但隐患也真:重规划可能无限循环(计划 A 失败→计划 B 又失败),或重画时又把前面已验证的对的步骤推翻,造成来回返工。所以工程上要给重规划加「次数上限 + 已验证步骤锁定」,不是无脑重来。这又回到 Part 1 那句:信任流程,不信任单次输出。

小结:Plan-and-Execute 把「规划」和「执行」拆成两个角色,让计划成为显式、可审阅、可重画的中间产物。planner 只俯瞰全局列清单(不碰工具),executor 只落地当前步,失败了带着反馈重画地图——这顺着模型「局部续写」的弱点,给它配了个专职全局角色。


3.3 任务分解的粒度控制

老哥,3.2 你把计划这个角色立起来了。可紧接着一个让你头疼的实操问题来了:计划里的「每一步」,到底该细到什么程度?这步写「重构整个支付模块」,太虚;写「打开 pay.go,把第 42 行的 if err != nil 改成 errors.Wrap,保存」,又太碎。这中间的「刚好」在哪?这就是任务分解的粒度控制,是规划这一 Part 里最吃手感的一节。

先说两个极端各自的坑,你先有痛感。

太粗——一步干太多。比如计划里写「实现用户注册功能」。模型拿到这一步,要在「这一步内部」同时搞定:校验参数、查重、密码哈希、写库、发欢迎邮件、返回 token。它一步之内要决策太多、要输出的内容太长,而且没法在中间验证。结果常常是:它凭训练记忆「编」出一部分逻辑(幻觉),或者把几件事搅在一起写出半对半错的代码,你拿到手根本分不清哪段是对的、哪段是编的。太粗的本质问题是:单步的「产出」不可验证、不可追责,错了你都定位不到是哪半步错的。

太细——一步只干一丁点。比如「读取 pay.go 第一行」「读取第二行」……这种。坑在另一头:步骤爆炸。一个本来 5 步能讲清的任务,被拆成 50 步,计划本身就长到塞爆上下文窗口;executor 每步都要走一遍「读计划→调工具→写回状态」的开销,token 和延迟都疯涨;更糟的是,模型在 50 步的琐碎里更容易中途走神、前后对不上——你给它太碎的指令,它反而失去「这一步是为了啥」的全局感。太细的本质问题是:管理成本淹没任务本身,上下文被步骤噪声淹没。

那「刚好」的粒度长啥样?墨叔给你一句能当尺子的话:一个步骤,应当是「模型能独立完成、且有明确可验证产出」的最小任务单元。拆成三要素你更好抓—— 一是「独立」:这一步不依赖「还没发生的后续步骤」的结果,它自己就能开干; 二是「明确产出」:干完这步,能说清「交付了什么」——一个文件、一段通过测试的代码、一条查询结果; 三是「可验证」:这个产出能用代码或简单判断确认「做没做成」,而不是只能靠模型自己说「我做好了」。

我给你个生活化类比。装修拆任务:太粗是「把房子装修好」——工人听完懵,因为他不知道从铲墙还是从买砖开始,干完你也没法验收「装修好了没」。太细是「拿起锤子、敲第三块砖的右上角、敲两下、放下锤子」——工长被自己写的操作手册淹死了,而且敲两下砖和「装修好」之间差着十万八千里,他早忘了为啥敲。刚好是「把客厅墙面铲掉重刷」——这是一个能独立开工(不用等别的房间)、有明确产出(铲干净且刷完的墙)、可验收(看一眼平不平、白不白)的任务单元。

给你一个和你本行的强连接。你写函数时天天在做「粒度决策」:一个函数该多大?你本能遵循单一职责原则——一个函数干一件的事,参数清晰、返回值明确、能单独测。agent 的步骤粒度,几乎就是「单一职责」在任务层面的翻版:一个步骤 = 一个「单一职责的任务」,有明确的「输入(已知上下文)」和「输出(可验证产物)」。你早就懂「函数太胖难测、太碎难读」,现在只是把这套直觉,从「代码函数」平移到「agent 步骤」。区别仅在于:函数的边界由编译器/你强制,步骤的边界由 planner(模型)生成、由你审阅——所以你审计划时,用的就是「这个函数拆得合不合理」的那套眼光。

再说个真实的经验法则,帮你落地。我判断一步「粗了还是细了」,常看两个信号: 信号一,这一步如果失败了,我能不能一句话说清「错在哪」?如果说不清(比如「实现注册功能」挂了,你不知道是查重还是发邮件挂了),就是太粗,该拆。 信号二,这一步内部,模型要不要「再规划一次」?如果一步里模型明显需要先想「先干 A 还是先干 B」,说明这步其实包了好几个子任务,该拆成几步、或者用 3.2 说的「executor 内部再跑一个小 ReAct loop」去探。反过来,如果一步小到「就是把 X 复制到 Y」,那它产出太弱、管理成本占比太高,该并。 最舒服的粒度,是「模型一步之内不用再犹豫顺序、干完立刻能验收」。

还有个容易忽略的点:粒度还和「工具返回的体积」耦合。你回想 Part 1「上下文窗口是工作台」——一个步骤如果会触发一个超长的工具返回(比如「读整个项目」),那这步本质上就「粗」了,因为它一下往工作台堆太多。所以控制粒度,也等于控制「每一步往窗口里塞多少东西」。细一点、每步只读该读的那块,窗口才不会被一次淹没。

光说道理抽象,老哥,我给你一个具体对照实例,把粒度差异摊开看。拿「重构支付模块的校验逻辑成策略模式」这个任务,三种拆法:

粗版(一步):「重构支付模块校验为策略模式。」——executor 这一步内部要同时搞定:抽象出 Strategy 接口、把原有 if-else 分支搬进具体策略、处理上下文注入、保证调用方无感、补测试。它一步输出的代码几百行,你拿到手分不清哪段对、哪段编,编译挂了都不知道挂哪。

细版(五十步):「打开 pay.go 第 1 行」「读第 2 行」「找校验函数起点」「复制这段到新文件」……——计划本身比代码还长,executor 每步走一遍「读计划→调工具→写回」,token 烧爆,模型在琐碎里忘了「为啥读这行」,中途某步读串了行,后面全歪。

刚好版(五步):1) 在 strategy/ 下新建 PaymentValidator 接口与两个具体策略(参数校验、风控校验),产出可编译的接口文件;2) 把 pay.go 原有校验逻辑分别搬进两个策略,产出搬移后的策略文件;3) 改 pay.go 调用方改为依赖接口(依赖注入),产出改后的调用方;4) 为三个文件补单元测试,产出测试文件;5) 跑 go build + go test 验证,产出通过/失败报告。每一步都「独立能开干、有明确文件产出、能跑构建验收」。这版你审起来轻松,executor 干起来也稳——因为它每步只操心一件事。

你一眼就能看出「刚好版」为什么舒服:它把 3.3 那三要素(独立、明确产出、可验证)全占满了。粗版占不满「可验证」,细版占不满「独立」(太碎以至于失去目标感)。这个实例你存着,以后审任何 agent 计划,都拿它当模板去量。

再给你个真实经验收尾。我见过一个团队把粒度走极端:他们让 planner「尽量拆细,越细越好」,结果一个「加限流」的需求拆出 60 多步,agent 跑了四十多步还没到核心,context 被步骤噪声淹掉,半路开始重复调同一个读文件工具。后来他们加了一条 planner 约束:「单任务的计划步骤数控制在 3~8 步,超出说明某步太粗需内部细化、或整体需分层规划」——立刻稳了。所以粒度不是「越细越可控」,而是「有个甜区」;甜区的位置,取决于任务复杂度、工具返回体积、窗口大小三者的拉扯。你调 agent 时,这个甜区要靠实测去摸,没有万能数。

还有个你做评测时会用到的延伸:粒度还直接决定任务好不好被自动评估(eval)。粗步骤的产出是一大坨代码,你很难写一条断言判断「这步对没对」——只能人眼 review,eval 成本高、主观;细步骤的产出小而明确(「创建了 X 文件」「返回了 Y 值」),你反而容易用确定性代码批量验证。所以「刚好粒度」不仅让 agent 干得稳,还顺手让你的评测变便宜、变客观——这又和 Part 9 的 eval 串上了。反过来说,如果你发现某个任务「怎么写 eval 都写不出」,八成是粒度太粗、产出不可验证,回去把步骤拆细,eval 自然就写出来了。粒度、可靠性、可评测性,这三件事是绑在一起的,你调其一,另两个跟着变。

给你一个和你本行的收口连接。你做 code review 时,最怕什么 PR?要么是「改了 30 个文件、你半天看不完」的大 PR(对应粗版,不可验证、不可追责),要么是「一行一 commit、历史刷屏」的碎提交(对应细版,管理成本爆炸)。你喜欢的 PR 是「单一职责、能独立测、描述清楚改了啥」的中等粒度——这就是 3.3 的粒度三要素在协作层面的翻版。你看,agent 步骤粒度这门手艺,和你带团队、审代码的手感是同一套;你早就是个「粒度高手」,只是没意识到它能直接用来审 agent 计划。

给你一个和你本行的收口连接。你做 code review 时,最怕什么 PR?要么是「改了 30 个文件、你半天看不完」的大 PR(对应粗版,不可验证、不可追责),要么是「一行一 commit、历史刷屏」的碎提交(对应细版,管理成本爆炸)。你喜欢的 PR 是「单一职责、能独立测、描述清楚改了啥」的中等粒度——这就是 3.3 的粒度三要素在协作层面的翻版。你看,agent 步骤粒度这门手艺,和你带团队、审代码的手感是同一套;你早就是个「粒度高手」,只是没意识到它能直接用来审 agent 计划。

【思考题】

  1. 你审一份 agent 计划时,怎么一眼判断「某一步太粗了」?给一个你自己的判断信号(不限于我文里说的)。
  2. 「粒度还和工具返回体积耦合」这句话,和你写 Go 时的什么习惯是同一个直觉?(提示:想想一次查库返回多少行、一次 RPC 拿多少字段)
  3. 如果 planner 老是把任务拆得太碎(50 步那种),除了人工合并,你能在流程上怎么「结构性」地逼它拆粗一点?反过来太粗呢?

【参考答案】

  1. 我的一个实用信号是:这一步的「验收标准」能不能写成一行断言。比如「实现注册功能」你写不出一行验收(是看库里有记录?看返回 200?看邮件发出?都不完整),就是太粗;而「在 user.go 新增 Validate() 方法,输入非法邮箱返回 error」你立刻能写「调用 Validate 传非法串,断言非 nil」——这就是刚好。一句话:验收写不出单行断言的步骤,粗了,拆。
  2. 同一个直觉是「按需取数、别一把梭」。你写 Go 查库,绝不会 SELECT * 然后在前端慢慢挑,而是只查需要的字段、分页拿;调 RPC 也只请求必要的字段,避免一次拉爆内存/带宽。agent 步骤粒度也是:每一步只「探」该探的那块上下文、只触发「刚好够落地」的工具返回,别一步「读整个项目」把窗口撑爆。你早就在用「控制单次 IO 的数据量」这套手感,现在它平移成了「控制单步往上下文窗口塞的量」。底层都是稀缺资源(内存/带宽 vs 上下文窗口)的管理。
  3. 结构性逼粗:在 planner 的提示里加硬约束,比如「每步必须是一个能独立交付可运行产物的任务,禁止出现『读取第 X 行』这类原子操作」「计划总步数不超过 N」。更稳的是在 executor 层做「步骤合并」——连续几步如果都只动同一个文件且无分支,自动并成一步。反过来逼细:提示里加「任一步若需内部再决策顺序,则必须拆成多步」「任一步失败难以定位时,拆」。再狠一点,用「重规划反馈」:太粗导致失败定位不清,就把失败报告喂回 planner 时说「上次的步骤粒度太大,请拆细」。这是用 3.2 的重规划机制来治粒度病——流程自己迭代出好粒度。

小结:任务粒度太粗则单步不可验证、易幻觉;太细则步骤爆炸、上下文淹没。刚好是「模型能独立完成、有明确可验证产出」的最小单元——这几乎就是你写函数时的「单一职责」在任务层面的翻版,且粒度还和单步工具返回的体积耦合。


3.4 反思与自我修正:Critic 循环

老哥,前面你让 agent 一步步干,可有个要命的问题一直没解决:模型走完一步,它怎么知道自己对没对?它可没有「单元测试」这种内置良知。它输出的代码,可能编译不过;它改的配置,可能语法错;它给的回答,可能答非所问。如果循环只会「执行→往下走」,那错一步就带着错往下滚,越滚越歪。这一节墨叔教你给 agent 装一个「回头看」的机制——Critic 循环(反思与自我修正)。

先讲核心机制。Critic 循环很简单,就三步成环:执行 → 批评 → 改写。模型(或另一个模型调用)干完一步,不直接进下一步,而是先让「批评者」对这一步的结果做个评估:「这步产出对不对?符不符合要求?有没有遗漏或错误?」如果批评者挑出问题,就把「原产出 + 批评意见」一起喂回去,让模型改一版,再批评、再改,直到批评者说「这版 OK」或达到重试上限。

你品一下这和普通「一次性生成」的区别。一次性生成是:模型吐一段,你拿走,对错全凭运气。Critic 循环是:模型吐一段,有个「审稿人」先挑刺,挑出来它自己改。前者像你写完代码直接提交;后者像你写完先自己 review 一遍(或更狠——让同事 review),改完再提交。显然后者错得少,因为错误在「交付前」被一道反馈拦下来了。

为什么「带反馈」比「一次生成」可靠?回到 Part 1 那句狠话:模型只「续写」,不「验证」。一次性生成时,它顺着自己的思路写完,错的地方它自己意识不到——因为它没有「外部事实」来打它的脸。但 Critic 循环里,「批评」这一步,本质上是人为制造一次「外部视角」:让模型(或另一个调用)站在一个「检查者」的角色,拿着原始要求去对照产出。这个「检查者视角」和「生成者视角」是分开的,于是能抓到生成者自己陷进去时看不到的毛病。

给你一个 Python 示意,把「执行→批评→改写」这个小循环钉死:

# Critic 循环示意(非可运行工程)
def critic_loop(task: str, draft: str, max_rounds: int = 3) -> str:
    current = draft
    for r in range(max_rounds):
        # 1. 批评:让模型站"检查者"视角,对照原任务挑刺
        critique = model_chat(
            f"任务:{task}\n当前产出:{current}\n"
            f"请作为严格审稿人,指出这版产出哪里不满足任务、有什么错误或遗漏。"
            f"若已满足,只回 OK。"
        )
        if critique.strip() == "OK":
            return current                      # 2a. 过了,交付
        # 2b. 改写:把"原产出 + 批评"一起喂回,让它改一版
        current = model_chat(
            f"任务:{task}\n上一版:{current}\n审稿意见:{critique}\n"
            f"请根据意见改写,产出修正版。"
        )
    return current                             # 3. 兜底:到上限仍不完美,返回最后一版给人定夺
 
# 用法:agent 每步产出后,先过一遍 critic 再往下走
step_output = do_one_step(goal, tools)
verified = critic_loop(goal, step_output)      # 先自检,再进入下一步

老哥,看这段你注意一个关键点:critic 和 generator 可以是「同一个模型的不同角色」。上面示意里 model_chat 调了两次,其实是同一模型被两套不同的提示「扮演」了生成者和审稿人。这叫 self-reflection(自我反思)。它便宜、好实现,但有个软肋:模型「自己审自己」往往不够客观——它生成时的那套错误前提,审稿时可能还带着,于是「挑刺」挑不到根上。就像你刚写完一段沾沾自喜的代码,自己 review 容易盲区。

所以工程上常升级一档:用独立的 critic。要么换一个更克制的提示强制它「只挑刺不许客气」,要么干脆用另一个模型实例 / 更强的模型当审稿人。独立 critic 的核心是视角隔离——审稿者不被「这是我自己写的所以不忍心改」这种东西带偏(模型没有情绪,但会被「生成时的上下文」带偏)。这就像代码评审:自己 review 有用,但让没参与写的同事 review,常常一眼看出你陷进去的坑。

我给你个生活化类比收一下。Critic 循环像你写完方案后,自己念一遍、再让搭档挑一遍。一次性生成是「想完直接发群里」;self-reflection 是「发之前自己默念检查」;独立 critic 是「让搭档过目」。三者的出错率递减,成本递增。agent 干活也是这个谱:能上 critic 的环节就别裸奔一次性生成,尤其是「出错代价高」的产出(比如要落库的代码、要外发的邮件)。

和你本行的强连接,来了。你 CI 里的 lint + 单测 + 代码评审,本质就是「确定性的 critic」。它们比模型的 critic 狠在哪?它们用代码事实说话:单测不过就是不过,编译器报错就是报错,不跟你讲情面。模型的 critic 是「概率性的、语言层面的」——它能挑「这段逻辑好像没处理空指针」「命名和前面不一致」,但挑不出「这个边界条件算错了」。所以最稳的工程做法是两层 critic 叠加:模型的 critic 负责「方向、结构、可读性、是否符合要求」这类语言层检查;确定性的测试/编译/lint 负责「事实层对错」。模型挑方向,测试定生死。这恰好呼应 Part 1「凡必然正确的放代码」——critic 里「必然对不对」那部分,必须交给确定性工具,模型只做「像不像样」的软检查。

说个真实经验。让 agent 写代码时,最经典的 Critic 循环就是:生成代码 → 跑测试/编译 → 把报错当 critique 喂回去 → 它改 → 再跑。这比「让它自己检查自己写的代码对不对」强太多,因为「测试报错」是硬事实,不是模型自说自话。我见过一个 case:agent 写了个解析函数,自己 review 说「看起来没问题」,但一跑测试,某个时区边界 case 挂了;把报错喂回去,它立刻改对了。区别就在:自我感觉良好(软) vs 测试打脸(硬)。所以你做 agent,永远优先把「能跑的确定性检查」接成 critic,而不是只靠模型自省。

不过老哥,critic 也不是银弹,我得把它的边界也讲清,免得你迷信。第一个边界:critic 自己也可能是个「概率性错误源」。你让模型当审稿人,它挑的刺可能是错的——比如它说「这里少了个 nil 检查」,其实那个分支不可能为 nil,你照它的意见改,反而引入冗余甚至新 bug。所以「模型的 critic 意见」和「模型的生成」一样,都不能全信,得用确定性检查去验证 critic 意见本身是否合理。这也是为什么我强调「硬事实优先」:测试报错是硬事实,模型挑刺是软意见,软意见要过一遍硬检查再采纳。

第二个边界,更微妙:critic 可能「挑不出它自己写不出的错」。模型在「数学正确性、复杂并发安全」这类它需要真推导才能判定的事上,既写不对、也审不对。你让它审稿一段涉及锁顺序的并发代码,它大概率挑不出「这里会死锁」——因为它自己就不会做锁分析。这类「必然正确」的判定,必须交给确定性工具(死锁检测、race detector、类型检查),模型 critic 在这儿是摆设。一句话:critic 擅长「语言层、结构层、一致性层」的软检查,不擅长「事实推导层」的硬检查,后者永远是代码的地盘。

第三个边界,关于「何时批评」。critic 不一定只放在「产出之后」,也可以放在「过程中」。进阶打法是 process critic(过程批评):不让模型一口气生成最终答案,而是让它先列「我打算怎么干」,critic 先审这个思路对不对,思路过了再让它动手——这其实是把 Critic 循环前移成了 Plan-and-Execute 里的「计划审阅」。生成后批评 + 过程中批评,两道一起上,错误在更早的环节就被拦下,返工成本更低。你回想 3.2 说的「计划作为可检查的中间产物」,那本质就是一种「对计划的批评」。所以 Critic 循环和 Plan-and-Execute 不是两件事,是同一套「用反馈纠偏」思想在不同环节的化身。

给你一个和你本行咬合的连接。你写代码信奉的 TDD(测试驱动开发),本质就是「最硬的 critic」:先写测试(定义「什么是对的」),再写实现,实现跑不过测试就被打回重写——这不就是「生成 → 测试当 critic → 改写」吗?你早就在用 Critic 循环,只是那个 critic 是你写的测试用例(确定性),不是模型。agent 的 Critic 循环,不过是把「你写的测试」和「模型的软审稿」叠在一起用。你当年从「写完再测」进化到「先写测试再写码」,错了能立刻发现;agent 也一样,把 critic 接进循环,比「生成完直接交付」可靠一个量级。你不是要学新东西,你是要把你 TDD 的肌肉记忆,移植到「让模型自己跑这个循环」上。

【思考题】

  1. 为什么「让模型自己检查自己写的答案」往往不够可靠?根因和它「续写不验证」的出厂设定有啥关系?
  2. self-reflection(同一模型自我反思)和独立 critic(另一个调用/模型审稿),你分别在什么场景下选哪个?成本怎么权衡?
  3. 你 CI 里的单测是「确定性 critic」,模型的反思是「概率性 critic」。如果要你给一个 agent 接 critic,你会怎么把这两者「叠」起来用,而不是二选一?

【参考答案】

  1. 根因还是 Part 1 那句:模型只「续写」不「验证」。它生成答案时,是顺着一套前提/思路续写下来的;让它「自己检查自己」,它还是在同一套上下文、同一套前提里续写「检查意见」——它陷进去的错误前提(比如误以为某接口返回数组),审稿时大概率还拿着,于是挑不出根上的错。就像一个自己写的、自己又审的人,容易「当局者迷」。要破这个,就得引入「不同视角」或「硬事实」:不同视角=独立 critic(不带生成时的语境),硬事实=测试/lint(用代码结果打脸,不靠模型自评)。所以「自评」有用但天花板低,关键错误得靠外部视角或外部事实兜底。
  2. 选 self-reflection 的场景:产出「出错代价低」、且需要快速低成本迭代的环节,比如草稿润色、格式对齐、先过一遍明显遗漏。它便宜(不额外换模型)、延迟低。选独立 critic 的场景:产出「出错代价高」或「需要客观挑刺」的环节,比如要落库/要外发的代码、方案是否符合硬约束。独立 critic 用「不同提示/不同模型」制造视角隔离,能抓 self-reflection 的盲区,但多一次模型调用、成本和延迟都上去了。权衡尺子:出错代价 × 发生概率 超过一次额外调用的成本,就上独立 critic。工程上常见的是:self-reflection 做「第一道软筛」,独立 critic 或确定性测试做「最后一道硬闸」。
  3. 叠加而不是二选一,最稳的做法是「模型挑方向,测试定生死」:agent 每步产出后,先过 self-reflection(便宜,抓格式/结构/明显遗漏),再跑确定性检查(编译、单测、lint、schema 校验)——这一步是硬事实,不过就生成报错当 critique 喂回去让它改;如果任务允许,最后再上一个独立 critic 做「是否符合原始需求的软核对」。三层分别是:软自评→硬事实→独立软核。关键点:凡是「必然对不对」的判断,只信确定性那层,绝不问模型「你对了吗」;模型的 critic 只用于「语言层、方向层」它擅长的地方。这就是把 Part 1「必然正确放代码」在 critic 机制里的具体落地。

小结:Critic 循环是「执行→批评→改写」的自修正环,用「外部视角/硬事实」拦下一次性生成发现不了的错误。self-reflection 便宜但有盲区,独立 critic 客观但贵,最稳是「模型挑方向、确定性测试定生死」两层叠加。


3.5 真实经验:规划失败的 5 种常见形态

老哥,前面 3.1~3.4 把「为什么会失败」的机理讲透了,也给了 Plan-and-Execute、粒度控制、Critic 循环三件兵器。这一节我不讲机理了,讲血淋淋的现场:规划这一层,真实项目里最容易以哪 5 种姿势翻车。每种我给你「现象 + 根因 + 应对」,你以后一撞上就能对号入座。

形态一:计划过于乐观(低估复杂度) 现象:planner 列了 5 步,信心满满,结果执行到第 2 步就发现「这模块依赖一个它根本不知道的老接口」,5 步计划直接作废,或者硬着头皮编。 根因:planner 那一轮「不读细节」(3.2 说的,为了保全局视角),所以它对真实复杂度的感知是「基于训练记忆的想象」,不是「基于代码事实」。它不知道那个老接口有多坑,于是计划看起来干净,落地全是雷。 应对:两招。一是给 planner 一点「侦察预算」——允许它在列计划前,先调一两个只读工具(如 grep 接口定义、读目录结构)探一下真实地形,再列计划(注意只给只读、低成本侦察,别让它陷进细节);二是接受计划是「假设」而非「承诺」,靠 3.2 的「失败→重规划」兜底,别指望一次计划就完美。

形态二:步骤间依赖没理清 现象:计划写「步骤 3:用步骤 2 生成的配置 X」,结果步骤 2 没真正产出 X(只产出个类似的 Y),步骤 3 拿着 Y 硬干,下游全错,而且错误很隐蔽——因为每步单独看都「跑完了」。 根因:planner 列步骤时,对「上一步到底交付什么精确产物」描述模糊(「生成配置」vs「生成名为 config.yaml、含字段 a/b 的文件」),executor 各步按自己理解交差,接口对不上。这是「粒度太粗 + 产出不可验证」(3.3)的并发症。 应对:在计划里把每步的「输入依赖」和「输出契约」写死——像定义函数签名一样定义步骤接口:「步骤 3 输入:config.yaml(含 a/b);输出:service 启动成功」。再配合 Critic 循环(3.4)在步骤交接处校验「上一步产物是否满足下一步的输入契约」,不满足就拦截、触发重规划。把步骤间关系当「模块间接口」来管,你就熟了。

形态三:卡在死循环 现象:executor 反复调同一个工具、或 planner 和 executor 来回踢皮球——planner 说「执行失败重规划」,新计划又导向同一个会失败的步骤,无限套娃,token 烧光、任务没动。 根因:没有「失败记忆」和「次数上限」。模型不记得「这步我已经试过两次了」,于是同样的错误策略被反复尝试;或者重规划没「锁定已验证步骤」,每次都从头画,又把对的推翻重踩坑。 应对:三道闸。一是全局「步数/重试次数上限」(Part 1 就强调过,任何 agent 循环必须有保险丝);二是「失败记忆」——把「步骤 X 已失败、原因是 Y、已试过方案 Z」写进上下文,重规划时强制「不得重复方案 Z」;三是「已验证步骤锁定」,重规划只重画失败后的部分,不碰前面已 done 的。死循环的本质是「没有学习过往失败」,你给它记忆和上限,它就老实了。

形态四:偏离原目标(目标漂移) 现象:你让 agent「给登录加限流」,它干着干着,开始顺手「重构了整个 session 模块」「顺带把登录的存储从 Redis 换成 MySQL」,最后限流是加了,但引入了一堆你没要的改动,review 时你头皮发麻。 根因:3.1 讲的「目标稀释」在作祟——长任务里,原始目标被中间海量上下文冲淡,模型每步只在「脚下三米」里挑「顺手的事」,于是「顺手多干点」显得合理。模型没有「这不在范围内」的自觉。 应对:把「范围约束」显式写进系统提示和每步上下文:「只做 X,禁止改动 Y/Z,若发现需要改 Y 先停下问我」。再配一个独立 critic(3.4)专门盯「当前改动是否超出原目标」,越界就拉回。本质是给 agent 划「围栏」——你回想 Part 2 工具机制里「副作用与安全」,范围失控也是一种副作用,得用护栏框住。

形态五:工具失败没备选 现象:计划第 4 步「调用 API 拉取数据」,结果那个 API 超时/鉴权挂了,executor 拿到报错,planner 也没说「挂了怎么办」,于是整个任务卡死在这一步,前面 3 步白干。 根因:计划只写了「 happy path(顺利路径)」,没写「异常路径」。真实世界工具会挂、网络会抖、权限会失效,而 planner 在「想象世界」里列计划,默认一切顺利。 应对:两招。一是在工具层做「失败兜底」(Part 2 讲过:parse 失败、报错、超时怎么兜),让单次工具调用本身有重试/降级;二是在计划层给关键步骤写「备选路径」:「若 API 挂,改用本地缓存 / 或先跳过此步标记待补 / 或停下告警」。更稳的是把「工具不可用」当一种 critic 反馈,触发重规划时带上「某工具当前不可用,请换方案」。happy path 思维是人在写代码时也有的毛病(你我也常漏写 fallback),agent 这儿的代价是被放大成「整任务瘫痪」,所以格外要显式化备选。

老哥,这 5 种不是教科书上分门别类的「理论病」,它们在真实任务里常常结伴出现、还会连锁。我给你一个综合串案,你看它们怎么拧成一股绳把任务搞死:某次让 agent「给订单服务加个幂等防重」。planner 乐观地列了 4 步(形态一,低估了「幂等要动数据库索引和缓存」的复杂度);步骤间它含糊写了「步骤 2 加去重键」,没声明去重键存在哪(形态二,依赖不清);executor 跑步骤 2 时调 Redis 写去重键,Redis 那次刚好超时(形态五,工具无备选),executor 没兜底就卡住;重规划时没「失败记忆」,又生成了「再试一次 Redis」的同款计划(形态三,死循环苗头);而整个过程中,因为窗口被报错刷屏,模型把「只加幂等、别动别的」这围栏忘了,顺手把订单表结构也改了(形态四,目标漂移)。一次任务,五种病全中——你看,它们不是选择题,是组合拳。

所以老哥,我教你把这张表当「起飞前检查单(pre-flight checklist)」用,而不是等摔了才对照。你在让 agent 跑任何稍大的任务前,拿这 5 条过一遍计划:

  • 计划有没有「侦察过真实复杂度」、还是纯靠想象?(防形态一)
  • 每步的「输入依赖 / 输出契约」写死没?像函数签名那样清晰吗?(防形态二)
  • 有没有「步数 / 重试上限 + 失败记忆」?(防形态三)
  • 系统提示里「范围围栏」写没写、有没有独立 critic 盯越界?(防形态四)
  • 每个关键工具调用,有没有 fallback / 备选路径?(防形态五)

这一遍 Checklist 走完,五种病在「计划阶段」就被掐掉大半,比等执行时爆了再救便宜得多。你看,这又是「把显式计划当可检查产物」(3.2)的红利——正因为它显式,你才能拿 checklist 逐条审;纯 ReAct 黑盒推理,你连「审什么」都抓不住。

给你一个和你本行的收口连接,这节就收。你写后端五年,最熟的就是防御性编程:参数校验、降级、熔断、重试上限、幂等、事务回滚——这一整套,正是「预期世界会出错」的工程纪律。你现在回头看这 5 种失败形态和对应应对,是不是每一招都眼熟?形态一的「侦察预算」≈ 你上线前先灰度探测;形态二的「步骤接口契约」≈ 你定义的 API schema;形态三的「失败记忆 + 上限」≈ 你写的重试上限和熔断;形态四的「围栏」≈ 你给接口加的权限校验;形态五的「备选路径」≈ 你写的服务降级。原来如此——规划失败的 5 种形态,本质上是「概率程序」把「确定性程序早就在防的异常」放大并重新演了一遍。你不是要学新医术,你是要把你防御性编程的功力,从「防网络超时、空指针」平移到「防模型瞎指挥、幻觉、采样偏差」。这层平移通了,Part 3 就真毕业了。

老哥,你把这 5 种翻车对照一下,会发现它们不是孤立的,底层都指向同一件事:模型活在「想象中的顺利世界」,而真实执行在「会出错、会越界、会循环」的世界。Plan-and-Execute(显式计划 + 重规划)、粒度控制(可验证产出)、Critic 循环(反思 + 硬事实)、围栏与兜底(范围约束 + 工具 fallback)——这几件兵器,恰恰就是分别对着这 5 种病的药。你把它们串成一张「病症→药方」的表,规划这一 Part 就真正落地了。

【思考题】

  1. 这 5 种失败形态里,你觉得哪一种在你自己写代码时也最常犯(作为人类工程师)?这说明模型和人在规划上有什么「共同的脆弱点」?
  2. 形态二「步骤间依赖没理清」,我建议「像定义函数签名一样定义步骤接口」。但函数签名是编译器强制校验的,agent 步骤接口靠什么强制?这里差的那层「硬约束」你怎么补?
  3. 如果只能给初学 agent 的你(老哥)留一条「防规划翻车」的口诀,你会总结成什么?为什么是这条而不是别的?

【参考答案】

  1. 我最常犯的是「形态一:计划过于乐观」和「形态五:happy path 思维」——人类工程师也爱低估复杂度、懒写 fallback,这恰恰说明模型和人在「面对不确定性的规划」上有共同脆弱点:都倾向于在信息不全时假设顺利。区别是:人会吃一堑长一智(记忆 + 经验),而裸模型每次都从零来、不记失败。所以 agent 工程里大量功夫,是在「补模型缺的那层失败记忆和 fallback 纪律」——而这层纪律,恰恰是你作为老后端早就有的(你写代码会写重试、会写降级),现在只是把它显式地装进 agent 循环。人机和模型在这点上是「同病」,解法也同源:用结构把「假设顺利」纠正成「预期会出错」。
  2. 函数签名有编译器做硬校验,agent 步骤接口没有——差的那层「硬约束」得靠三方面补:一是 schema 化步骤契约,让计划里的「输入/输出」是结构化字段(不是自由文本),executor 交接时用确定性代码校验「上一步产物是否满足下一步声明的输入 schema」,不满足直接报错而非含糊继续;二是 Critic 循环在步骤边界做「契约核对」(3.4);三是把「步骤接口不匹配」作为一种明确的失败信号,喂回重规划并强制「下版计划必须显式声明每步输入输出」。简言之:用 schema + 代码校验 + critic,把「函数签名的编译器强制」在 agent 世界里用工程手段近似出来。软约束(文字描述)必然漏,硬约束(代码校验)才兜得住。
  3. 我的口诀是:「先画可验的图,每步对着图,错了就重画,别信它说行。」 拆开:先画(Plan-and-Execute 显式计划)、可验(粒度控制让每步产出可验证)、对着图(executor 每步对齐计划+范围围栏)、错了重画(重规划+Critic 兜底)、别信它说行(确定性测试定生死,不靠模型自评)。为什么是这条而不是「多调几次模型」之类?因为它把整 Part 3 的五个机制浓缩成一条可操作的纪律,且每半句都对应一个真实失败形态——它不是玄学,是顺着模型「局部续写、假设顺利、不验证」的弱点,逐一上的锁。你记住这句,规划翻车率能砍掉一大半。

小结:规划失败的 5 种形态——计划乐观、依赖不清、死循环、目标漂移、工具无备选——底层都源于「模型活在想象中的顺利世界」。Plan-and-Execute、粒度控制、Critic 循环、围栏与兜底,正是对症的药;串成「病症→药方」表,规划这一 Part 就真正落地了。


墨叔收个尾:老哥,Part 3 到这。你该看清了——模型自己不会规划,不是它笨,是它出厂就是「局部续写器」。我们干的活,是用 Plan-and-Execute 替它画可验的图、用粒度控制把图拆成对得起的步骤、用 Critic 循环让它干完回头看、用围栏和兜底框住它「假设顺利」的臭毛病。下一部分(Part 4 记忆),咱聊怎么让 agent 别「转几圈就忘了自己是谁」——那张会燃烧的纸(上下文窗口),到底怎么管。