Chapter 02
任务分解家族:沿权衡轴走一遍
上一章立起了「承诺↔适应」这条轴,并把 ReAct 放在最适应的右端。这一章从 ReAct 出发,沿轴向"提前承诺"端走——每走一步,看它换来什么、又失去什么。四个范式(ReAct → Plan-and-Execute → ReWOO → LLMCompiler)就是这条轴上由右向左的四个落点。
本章你将建立的 schema
- 四个范式 = 同一条「承诺↔适应」轴上由右到左的四个落点,不是四套孤立机制
- Plan-and-Execute 把「想」和「做」拆给两个模型,靠"贵规划器跑一次 + 廉价执行器跑 N 次"省钱
- ReWOO 省 token 的真实机制 = 不把 observation 重复编码回推理循环——不是少调工具
- LLMCompiler 把计划编译成一张可并行的 DAG,像 CPU 乱序执行那样调度独立子任务
- 承诺得越多,越省 token、越能并行,但对"计划外的意外"越盲
2.1ReAct 再看一眼:轴的最右端
ReAct = 每行动一步就把真实观察喂回、再想下一步——最适应,也最贵、最串行。它是本章后三个范式都要打的靶子。
本章后面三个范式(Plan-and-Execute、ReWOO、LLMCompiler)的论文,全都拿 ReAct 当对照基线来证明自己更省、更快、更准。不先把 ReAct 的成本机制看透,后面"省 5 倍 token""降 3.7 倍延迟"这些数字就失去了锚点——它们都是相对 ReAct 而言的。
底层机制(比文档深一层):ReAct 的成本来自一个常被误读的地方。它每一步都把累积到当前的全部上下文——原始 prompt 加上之前所有的 Thought、Action、Observation——重新编码进模型,再生成下一个 Thought。于是第 1 步编码 1 份上下文,第 2 步编码"上下文 + obs1",第 3 步编码"上下文 + obs1 + obs2"……每条观察一旦进入轨迹,就会在剩下的每一步里被反复重新编码。轨迹有 N 步,最早那条观察就被编码了约 N 次。这使得总 token 消耗随轨迹长度增长(注意力计算的二次项还会让延迟涨得更陡),而且整个过程严格串行:第 N 步的 Thought 必须等第 N−1 步的 Observation 落地才能开始。
ReAct 贵,不是因为它调了很多次工具,而是因为同一条观察被重新编码了很多次。后三个范式各自攻击这条成本链的不同环节:Plan-and-Execute 让"想下一步"这件贵事少做几次;ReWOO 直接让观察不再回流到推理循环;LLMCompiler 打掉"串行"这一条,让独立步骤并行跑。把这句话记住,下面每一节都在拆它。
假设一个 ReAct 任务要跑 8 步,每步的工具观察都不算短。如果只想把账单降下来,最该动手减少的是"工具调用次数",还是"每步重新喂回的上下文体积"?
展开答案(先停 10 秒再点)
是后者——每步重新喂回的上下文体积。工具调用本身(外部 API)通常不是 token 账单的大头;大头是每一步都把越来越长的历史塞回 LLM 重新编码。8 步里最早那条观察会被编码约 8 次。这正是 §2.3 ReWOO 的攻击点:它把观察彻底踢出推理循环,让推理器只为每条观察"付一次费"。记住这个判断,到 ReWOO 那节会兑现。
2.2Plan-and-Execute:把「想」和「做」分开
先让一个强模型一次性列出完整步骤,再让一个廉价模型逐步执行;每步做完过一道重规划闸门。
ReAct 把"想下一步"和"做这一步"揉在同一个循环里,每一步都要请出同一个(通常很贵的)模型重新通读全部历史去推理。可多数步骤其实是机械执行,根本不需要顶配模型的推理力。把"想"(规划)和"做"(执行)拆开,就能用一个强模型把计划想一次,剩下的执行交给廉价模型——既省钱,又让规划器一次性看到全局、不被中间细节带偏。
底层机制(比文档深一层):这里必须拆开两个长期被工程师混为一谈的名字——Plan-and-Solve 和 Plan-and-Execute,它们是两个不同的东西。
Plan-and-Solve(Wang et al. 2023,arXiv 2305.04091)是一篇论文,本质是一次 zero-shot 的 prompt 改写:把 CoT 的 "Let's think step by step" 换成 "Let's first devise a plan, then carry out the plan step by step";PS+ 版本再加一句"抽取相关变量和数值"。计划和求解在同一次生成里完成,没有 few-shot 示例、没有第二个模型。它的收益是统计意义上的错误抑制:缺步错误从 12% 降到 7%、计算错误从 7% 降到 5%。它属于上一章提过的"先定计划"思想的最早形态,但它并不引入独立的执行器。
Plan-and-Execute(LangChain 的工程产物,受 BabyAGI 启发)则是另一回事:一个强planner 模型只跑一次,产出完整的步骤列表;然后一个廉价 executor 逐条执行每一步。成本结构因此是 1×强模型 + N×廉价模型,当步骤数 N 大于约 3 时就开始比"N 次都用强模型"的 ReAct 划算。它还在每一步执行后加一道 replan(重规划)闸门:拿到这一步的真实结果后,回看计划是否需要修改——这正是它没有把承诺推到最左端、仍保留一定适应性的原因。
Plan-and-Solve(论文,一次生成的 prompt 技巧)≠ Plan-and-Execute(LangChain 的双模型架构 + 重规划闸门)。名字像、思想沾边,但一个是"改一句 prompt",一个是"拆成 planner/executor 两个角色 + 失败回到 planner"。工程讨论里这两个名字几乎天天被混用——看到时先问一句:到底指哪一个。
像装修:设计师(强模型)先出一版完整施工图,工人(廉价模型)照图逐项施工,每完成一项工头回看图纸要不要改。类比失效在沟通成本:真实工地里设计师和工人能随时对话澄清,而这里 planner 把图交给 executor 后,两者只通过那份计划文本沟通——planner 表达不清的地方,executor 会照着错的执行,错误就这么传下去(planner→executor 的误差传播是它的固有代价)。
这一步沿轴向左挪买到了成本(N 大时省钱)和专注(规划器一次看全局、执行器只管干活),付出的是重规划延迟(每步后都要过闸门)和planner→executor 的误差传播。它仍在轴的中段——既非 ReAct 的全适应,也非下面 ReWOO 的全盲承诺。
2.3ReWOO:把 observation 踢出推理循环
规划器先用占位变量写好整张蓝图,工具一次性填空,求解器读一遍蓝图加证据就出答案——观察从不回流到推理。
§2.1 钉死的结论是:ReAct 贵在"同一条观察被反复重新编码"。Plan-and-Execute 减少了"想"的次数,但执行器每步仍在通读历史。ReWOO(Xu et al. 2023,arXiv 2305.18323)把这条成本链从根上切断——既然观察重新编码是元凶,那就让观察压根不进推理循环。
底层机制(比文档深一层):ReWOO 拆成三个模块。Planner(规划器)一口气写出整张蓝图,形式是一串 (Plan, #E) 元组:每条计划带一个 #E1, #E2… 这样的占位变量,代表"这一步工具的输出"——而此刻这个输出还不存在,规划器是在不知道结果的情况下先把整条链写完的。Workers(工人)随后把这些工具各跑一次,把真实输出填进对应的 #E。Solver(求解器)最后把"蓝图 + 填好的证据"读一遍,给出答案。
关键就在这里,也是上一章 §1.2 那个 callout 埋下的伏笔的兑现:ReAct 在每一步都把"prompt + 之前全部 Thought/Action/Observation"重新喂进 LLM,观察被一遍遍重新编码;ReWOO 只在开头付一次规划器的 prompt 费用,之后观察永远不回流到推理器。于是在 HotpotQA 上,ReWOO 用约 2,000 token 拿到 42.4% 准确率,而 ReAct 是约 10,000 token、40.8% 准确率——token 少 5 倍,准确率还略高。
这 5 倍省的是"不重复编码 observation",不是"少调了几次工具"。ReWOO 调的工具次数和 ReAct 同量级——它省在推理器再也不用一次次重读越堆越长的观察历史。这个区分是本章最大的"啊哈":token 成本的杀手是重新编码,不是调用次数。
蓝图写出来大致长这样(占位变量 #E1 在被填值前就被 #E2 的计划引用了):
Plan: 搜索《盗梦空间》的导演
#E1 = Search["盗梦空间 导演"]
Plan: 用 #E1 的结果查这位导演下一部上映的电影
#E2 = Search["导演 #E1 下一部 上映"]
# Planner 一次性写完上面整张蓝图(此刻 #E1/#E2 都还没有值)
# Workers 跑工具填值;Solver 读「蓝图 + 填好的 #E」一次性作答
像填一张报销表的草稿:先把整张表的栏目和公式列好(蓝图),栏里先写占位符"待填"(#E),最后一次性把发票金额填进去算总额——不必每填一个数字就把整张表从头读一遍。类比失效在依赖:报销表的栏目互相独立,而 ReWOO 的 #E2 计划可以引用 #E1,所以它不是完全无序的填空,而是带前后引用的蓝图。
代价:蓝图是蒙着眼睛承诺的。当某一步的真实工具结果本该让计划改道时,ReWOO 没有观察回流、改不了道——这是它沿轴又向左挪一格付出的适应性。
给 ReWOO 更多工具,反而更差。论文里 7 工具版的 ReWOO 在 2 工具版能做对的任务上失败了;20 条失败轨迹里有 17 条是工具误用——规划器在蒙眼状态下挑错了工具,而又没有观察能当场把它拽回来。工具越多,蒙眼挑错的概率越大。这正是"提前承诺"代价的具体长相:承诺得越早,越没有机会用真实反馈纠正选择。
有人想给 ReWOO 接上"每填完一个 #E 就让规划器看一眼、必要时改后续计划"。这么改之后,它在轴上会往哪端移动?又会把刚省下的什么重新赔回去?
展开答案(先停 10 秒)
会往适应端(右)移动——它重新引入了观察回流,换回了"中途纠偏"的能力,正好补上工具误用那个失败模式。但代价是把 ReWOO 最核心的省 token 机制赔了回去:一旦规划器要在每步后重看证据,观察就又开始反复进入推理循环,5 倍的 token 优势随之蒸发。这恰恰说明轴是连续的——任何往适应端的移动,都在用 token 换纠错。改到极端,它就退化回 Plan-and-Execute 乃至 ReAct 了。
2.4LLMCompiler:把计划编译成可并行的 DAG
规划器把任务编译成一张带依赖的 DAG,调度单元像 CPU 乱序执行那样,一旦某个任务的依赖就绪就立刻派发,独立任务并行跑。
前三个范式都没碰 ReAct 的另一条成本:串行。ReAct 一次只想一步、必须等观察落地才能想下一步,于是彼此独立的子任务(比如"分别查三个城市的天气")也被迫排队。LLMCompiler(Kim et al. 2023/2024,arXiv 2312.04511,ICML'24)专门打这条:把计划编译成一张显式标出依赖的图,没有依赖关系的任务就同时发出去。
底层机制(比文档深一层):三个部件协作。Function Calling Planner(规划器)产出一张任务 DAG,用 $1, $2… 占位变量把任务间的依赖关系编码进去(某任务的参数写成 $1,就表示它要等 1 号任务的输出)。Task Fetching Unit(任务获取单元,论文明确按 CPU 的指令获取 / 乱序执行建模)负责:把已解析出的变量值代入等待中的任务,并在某个任务的依赖一就绪就立刻把它派发出去。Executor(执行器)则把彼此独立的任务并发执行。这一下同时拔掉了 ReAct 的串行瓶颈和它两个有名的失败模式——重复地把之前已经发过的调用又生成一遍、以及过早停止。
收益是全方位的:相对 ReAct 延迟最多降 3.7 倍、成本最多降 6.7 倍、准确率高约 9%;在 Game-of-24 上甚至靠"执行器→规划器"的动态重规划反馈,比 ToT 还快约 2 倍。
$1 $2,须等两者就绪才跑。
注意:task1 与 task2 上下并排、同时发出,这就是"独立子任务并行"的样子——ReAct 会把这两个强行排成前后两步。像现代 CPU 的乱序执行:指令之间没有数据依赖,就不必按书写顺序排队,谁的操作数先备齐谁先执行——任务获取单元就是这个"调度器"。类比失效在确定性:CPU 的指令依赖在编译期完全已知且固定,而 LLMCompiler 的 DAG 是 LLM 当场生成的,会把 $1/$2 的参数映射错(论文里约 8% 的失败正是这种参数映射错误),CPU 调度器不会犯这种"看错依赖"的错。
代价 / 边界:当依赖关系只有在运行时才能知道(数据相关的分支——比如"先查 A,由 A 的结果决定下一步查 B 还是查 C"),LLMCompiler 没法预先把整张图编出来,只能停下来重新编译。这正好把它钉回轴上:它擅长的是"依赖结构提前可知、且有大量独立子任务"的场景——也就是上一章 §1.1 那个"分别查三城天气"指向的"提前承诺"端。沿轴它比 ReWOO 还靠左一点,因为它承诺的不只是一条线性蓝图,而是一整张并行结构。
2.5跨概念综合:四个范式在轴上的位置
把四个范式按"承诺多少"摆到那条轴上:ReAct 在最右(最适应——每步看真实结果再决定,零提前承诺);Plan-and-Execute 在中段(承诺一份计划,但每步后留了重规划的门,仍能适应);ReWOO 更靠左(蒙眼承诺整条线性蓝图,观察不回流,省 token 但不能中途改道);LLMCompiler 最靠左(承诺的是一整张可并行 DAG,把独立子任务同时发出去)。由右到左,每挪一格都在用"适应能力"换"更省 / 更并行"——这就是上一章那条轴在本章的完整展开。三个范式攻击的还是 §2.1 那条成本链的不同环节:Plan-and-Execute 减少"贵推理"的次数、ReWOO 砍掉观察回流、LLMCompiler 打掉串行。
| 范式 | LLM 调用与 token 成本 | 能否并行 | 能否中途适应 | 最适合的任务 | 主要代价 |
|---|---|---|---|---|---|
| ReAct | 每步重编码全部历史,token 随轨迹增长(基线) | 否,严格串行 | 能,每步都看真实观察 | 顺序依赖、强需当场纠错的多跳任务 | 贵、慢;易陷思考/行动死循环 |
| Plan-and-Execute | 1×强 + N×廉价,N>3 时省于 ReAct | 默认顺序执行 | 能,每步后过重规划闸门 | 大框架可预知、个别步骤需纠偏 | 重规划延迟;planner→executor 误差传播 |
| ReWOO | 规划器仅 1 次、观察不回流:约省 64% token(约 5×) | 工人可一并执行,但蓝图为线性链 | 否,蓝图蒙眼承诺 | 依赖链清晰、工具少而稳定的多跳问答 | 计划盲定;工具越多越易误用(17/20 失败为工具误用) |
| LLMCompiler | 延迟最多降 3.7×、成本最多降 6.7×、准确率约 +9% | 能,独立任务并发 | 有限,靠执行器→规划器动态重编译 | 依赖结构提前可知、含大量独立子任务 | 运行时才知的数据相关分支须停下重编译;约 8% 失败为参数映射错 |
表里没有标注"选中"的那一行——因为这不是一场有赢家的比拼,而是一条连续光谱。选哪个,取决于任务在"怕意外"和"怕贵 / 怕慢"之间偏向哪头:怕意外、需当场纠错就往右(ReAct / Plan-and-Execute),依赖清晰、想省钱或想并行就往左(ReWOO / LLMCompiler)。
"把一篇长论文分成 8 段、每段各自生成一句摘要、最后拼起来"——这个任务摆到轴上,最该落在哪个范式?
展开答案(先停 10 秒)
最该落在 LLMCompiler(或退一步 ReWOO)那端。8 段摘要彼此独立、依赖结构提前完全可知(就是"8 个并列任务 + 1 个汇总任务"),没有任何一段的结果会改变其他段怎么做——这正是"提前承诺 + 并行"的甜点区。用 ReAct 跑等于把 8 个能同时做的事强行排成 8 步串行,既慢又贵。这题直接呼应上一章 §1.1 "分别查三城天气"埋下的那个判断:独立子任务指向轴的提前承诺端。
§本章 self-check
先合上教程,把答案写在纸上或编辑器里。写完再点开对照——直接点开等于把这一节再读一遍。
- ReWOO 在 HotpotQA 上比 ReAct 省了约 5 倍 token。这 5 倍省的到底是什么?是"少调了工具"还是别的?
- Plan-and-Solve 和 Plan-and-Execute 是不是一回事?用一句话说出各自最本质的形态。
- ReWOO 和 LLMCompiler 都"提前承诺计划",但它们各自省下的东西不同——一个省的是什么,另一个省的是什么?是 token 还是延迟?
- 给 ReWOO 接更多工具为什么反而更差?这件事和那条「承诺↔适应」轴有什么关系?
答案(先做完再展开)
- 省的是"不再把 observation 重复编码进推理循环"。ReAct 每步都重读越堆越长的观察历史,ReWOO 的规划器只在开头付一次费、观察填进
#E后不回流。工具调用次数和 ReAct 同量级——省的不是调用次数。 - 不是一回事。Plan-and-Solve = 一篇论文里的一次 zero-shot prompt 改写("先定计划再逐步执行",计划与求解同一次生成,无独立执行器);Plan-and-Execute = LangChain 的双角色架构(强 planner 跑一次 + 廉价 executor 跑 N 次 + 每步后重规划闸门)。
- ReWOO 省的主要是 token(不重复编码观察);LLMCompiler 省的主要是 延迟(把独立子任务并行、打掉串行瓶颈,最多降 3.7× 延迟,附带降成本、升准确率)。一个攻"重复编码",一个攻"串行"。
- 因为蓝图是蒙眼承诺的:工具越多,规划器在没有观察反馈的情况下挑错工具的概率越大(论文里 17/20 条失败是工具误用),而它又没有观察回流来当场纠正。这正是轴左端"提前承诺"代价的具体长相——承诺越早,越没机会用真实反馈纠偏。
给一个"混合依赖"任务设计调度
任务:「给定 5 个 GitHub 仓库 URL,分别拉取各自最新 release 的 changelog,汇总成一份对比表;但其中第 1 个仓库若拉取失败,则改用它的 tag 列表兜底」。把它摆到表 2.1 上:哪部分天然适合 LLMCompiler 的并行承诺?哪部分必须保留 ReAct/重规划式的临场适应?写出你会怎么把两端缝在一起,并指出缝合处最容易出问题的地方。
提示(卡住再展开)
把任务切成两层看。5 个仓库的 changelog 拉取彼此独立、依赖结构提前可知 → 这是 LLMCompiler 并行 DAG 的甜点区,5 个任务一起发。但"第 1 个失败则改用 tag 列表"是一条运行时才知的数据相关分支——拉取结果(成功/失败)决定走哪条路,这正是 LLMCompiler 必须停下重编译、或交给一层重规划/ReAct 式适应来兜的地方。缝合处的风险:那个"失败兜底"分支若也被规划器提前蒙眼写进 DAG,就退回了 ReWOO 的工具误用风险——它没看到真实的失败结果,就承诺了兜底路径。务实做法是"独立部分并行承诺 + 易错分支保留观察回流",也就是 §2.5 说的"按任务在轴上偏哪头分段处理"。