对 Planning 模式的复盘:计划不会错,错的是你以为计划不会错
我最早搭 Planning 模式的时候,觉得它比 ReAct 优雅很多。先花一点时间把任务想清楚,拆成步骤,排好依赖,再一条条往下走。不像 ReAct 那样边想边做,每一步都临场拍脑袋。Planning 看起来更有条理,也更可控。
真把系统跑起来以后,这个信心很快就塌了。
最先暴露的问题不是”计划写得不好”,而是计划执行到一半,某个前提突然不成立了。可能是外部数据变了,可能是模型一开始就把用户意图理解偏了,也可能是某个中间步骤返回的结果和预期完全不一样。这时候计划还在,但基础已经松了。
我一开始的反应很朴素:那就改计划呗。让模型重新规划一遍,接着往下跑。但很快发现这件事远没有”重新规划”这四个字听起来那么干净。前两步已经执行了,有些结果写进了数据库,有些请求已经发到了外部系统,有些状态已经不可逆。你不是在一张白纸上重新画图,你是在一个已经被前两步改变过的世界里,试图搞清楚哪些还能用、哪些必须撤回、撤不回的怎么补救。
这篇文章想做的,是把 Planning 模式里那些不显眼但真正咬人的问题摊开讲。不是再介绍一遍 Planning 是什么,而是把从”计划失效”到”系统止损”之间的那段路,一步步走一遍。
计划执行到一半前提错了,前两步是不是白做了
这个问题我一开始想得特别简单:前提错了,那前面的产出大概率也得扔。后来真碰上几次线上案例,才发现”前两步白做了”这个判断本身就很危险。
有一次系统帮用户做一个多步骤的数据整理任务,计划是五步。第一步从数据库读了一批记录,第二步根据读到的内容做了一些计算和中间态写入,第三步要基于前两步的结果去调一个外部 API。结果第三步发现第一步的一个前提——用户说的”活跃用户”其实指的是另一个口径——根本对不上。
我当时的第一反应是把前两步全回滚。但仔细一看,第一步读出来的数据本身没问题,只是被拿去做了一个错误方向的计算。第二步的中间态写入虽然是基于错误前提的,但写入本身没有副作用,只存在本地临时表里。真正受影响的其实只有第二步的计算逻辑和第三步要传给外部 API 的参数。
这件事让我意识到,”前两步白做了”是一个需要分析才能下的结论,不是一个默认判断。有些步骤的产出和错误前提无关,比如纯粹的日志记录、无关分支的数据读取、已经验证过的边界条件;有些步骤虽然基于错误前提,但产物是可逆的,比如写进了可以回滚的临时表、创建了可以标记无效的记录;但有些步骤一旦执行了,影响就扩散出去了,比如已经发出去的通知、已经扣掉的费用、已经触发的下游流程。
所以后来我形成了一个很朴素的反应链条:先停,再看,再决定怎么修。
停是最优先的。发现前提不成立,第一步不是分析,是暂停所有还没执行的后续步骤。因为你还没搞清楚错误半径有多大,继续跑只会让局面更乱。
然后是判断影响范围。这件事不能只靠模型自己想一下。系统需要为每一步记录结构化信息:依赖了什么前提,读了什么资源,写了什么资源,有没有外部副作用。有了这些轨迹以后,系统可以做依赖分析,找出哪些步骤是建立在这个错误前提上的,哪些虽然排在前面但跟这个前提无关。
最后才是决定怎么处理前两步的产出。处理方式我后来习惯拆成三类:能撤销的就真正回滚;不能撤销但能修正的,做补偿动作;连补偿都做不了的,先把错误状态冻结住,打上标记,防止继续扩散。
“止损”的重点不一定是把世界恢复成什么都没发生过,而是让错误状态变得可见、可追踪、不再扩大。
回滚不是 undo,这件事系统要比模型先想清楚
早些时候我对回滚的理解就是 undo。第一步创建了一条记录,回滚就是删掉;第二步关联了三个子项,回滚就是取消关联。很干净,很理想。
真实系统里大多数动作根本没有 undo。外部 API 不提供删除接口,已经发出去的邮件收不回来,已经扣掉的钱不能自动退。如果你把回滚默认设计成 undo,系统遇到不可逆操作就会直接卡死,或者干脆跳过不管。
我后来更愿意把”回滚”这个词换掉,换成”补偿”。补偿的范围比 undo 大得多,也现实得多。
比如系统调了一个外部 CRM 创建了一条客户记录,然后又给这条记录关联了三个子项。后来发现前提错了,这条记录不该创建。但外部系统没有 undo 接口。这时候的补偿不是”删掉”,而是一连串动作:先停掉所有依赖这条记录的后续步骤;如果外部系统支持打标,就把这条记录标成 invalid;子项能删就删,删不掉也标成无效关联;在本地系统里写一条补偿记录,说明这次执行产生了哪些外部对象、现在是什么状态;后续读到这些对象时,必须能识别出这是错误执行遗留物。如果影响到了用户、资金或合规,就直接升级成人工处理。
这意味着每个工具在注册的时候,不能只声明”我能做什么”,还得声明”我做错了能怎么补”。比如一个工具可以说明:create_customer 的补偿方式是 archive_customer;link_child_item 的补偿方式是 unlink_child_item;如果连标记都做不到,那就标成 irreversible。
这样做的好处是,模型出错时不需要自己发明补偿方案,而是在工具预定义的动作集合里选。低风险的补偿可以自动执行,中风险的走策略审核,高风险的要求人工批准。选择权可以给模型,但动作空间必须由系统收窄。
我也踩过一个坑:把补偿逻辑也交给了模型自由发挥。模型有时候会想出一些”看起来合理但实际会制造新问题”的补偿动作。补偿本身也是副作用,它同样需要被约束。
模型事后回忆”我刚才依赖了什么前提”,这件事基本不靠谱
有一个设计我试过,后来放弃了:让模型在每一步执行完以后,回头补填”我刚才依赖了哪些前提”。
听起来挺自然,但跑起来几乎没法用。模型不是在记录,而是在回忆。回忆就会编、会漏、会合理化。尤其是复杂步骤,它往往会把一些隐含假设直接跳过,或者把推断出来的东西说成”已知事实”。你以为它在写日志,其实它在写回忆录。
后来我换了一个方向:不是执行后补录,而是执行前声明。
对于所有会产生决策分叉或副作用的步骤,模型在真正执行之前,先输出一个结构化的声明,至少包括:这一步要做什么;基于哪些已知事实;额外假设了什么;哪些是已验证事实、哪些只是推断;假设如果错了会污染哪些对象;这一步有没有副作用。
像”用户地址在深圳”这种语义前提,不是框架自动猜出来的,而是模型在准备执行时必须显式写出来的。写不出来,或者明显说不清,那这一步就不该直接执行,应该先插一个验证步骤去查,或者先问用户。关键点:假设必须前置暴露,不能事后补。
这样系统里实际上会有两张图。一张是 runtime effect graph,由框架自动记录——调了什么 API、读了什么数据库、写了什么文件。另一张是 semantic assumption graph,由模型在执行前声明——认为用户意图是什么、业务规则是什么、哪些判断是推断。第三步发现前提错了以后,系统不是只看其中一张图,而是两张一起分析。
这两张图的分工我也想过一阵子。框架能看到的是可观测的副作用,但看不到纯语义的前提。模型能看到语义,但不可靠。两者结合,才能得到一个相对完整的依赖视图。不能指望任何一方单独把这件事做好。
同一个模型自己规划又自己审计,不是可靠性方案
把假设图做出来以后,我碰到了下一个问题:这些假设到底谁来审计?
最自然的做法是让同一个模型既负责规划,又负责检查自己的计划。我也确实这么试了一段时间。结果很快发现,这更像是续写,不像独立检查。模型很容易把自己刚刚生成的东西继续当成事实往下传。它不是在找问题,它是在给自己的选择做解释。
后来我把这件事想明白了:如果规划和审计用的是同一个模型、同一个上下文,那审计能起的作用非常有限。它最多算第一轮的显式化——把隐含前提摊出来了——但远远算不上验证。
真正让审计变得有意义的,是做了几层拆分。
第一层是来源标注。Planner 在产出计划的时候,必须给每个关键前提打上来源标签:用户直接说的、工具查到的、数据库读到的、还是推断出来的。最关键的不是让它说”我很确定”,而是让它说明”这句话的证据从哪来”。一个前提如果既不是用户说的,也不是工具查出来的,那就只能算推断,不能算已验证事实。
第二层是上下文隔离。Reviewer 不应该完整复用 Planner 的上下文。我通常只给它看:用户原始输入、已执行工具的返回结果、Planner 声明出来的事实和假设、当前准备执行的高风险动作。不给它看 Planner 的长链条推理过程。这样它是在看声明和证据,不是在延续之前的思路。
第三层是独立的 claim checking。Reviewer 的任务不是泛泛地评价”这个计划好不好”,而是具体地查:哪些被写成事实的内容其实没有证据来源;哪些关键前提在工具返回里找不到支撑;哪些外部副作用依赖了推断项;哪些地方必须先补验证、不能直接执行。
第四层是规则拦截。最终让不让这一步执行,不由模型自己决定。只要一个前提是推断出来的,同时这一步又要触发外部副作用——写数据库、调外部 API、发消息、扣费——系统就应该卡住。模型可以提出”我猜是这样”,但猜测不能直接驱动副作用。
这四层叠起来以后,系统的行为变得稳定很多。不是模型变聪明了,而是不管模型够不够聪明,系统都有一套不依赖模型自觉的硬约束。
关于要不要用不同模型来做规划和审计,我的优先级是先做上下文隔离,再做角色隔离,高风险场景才考虑换不同模型。上下文隔离的效果往往比换 prompt 更直接。即使还是同一个底座模型,只要 Reviewer 不看 Planner 的推理链,它的独立性就会明显更强。
不是所有任务都值得上完整流程
把上面这些机制全串起来,你会发现一件事:完整 Planning 流程的成本不低。Planner 出计划,每一步要写声明,Reviewer 要做 claim checking,Orchestrator 要过 policy gate,高风险步骤还要设计验证和补偿。这套东西如果每个任务都跑一遍,延迟和 token 消耗很快就会让系统失去实用性。
我后来不再把完整 Planning 当默认值。默认应该是轻量执行,只有在风险足够高的时候才升级到重流程。
判断要不要上完整流程,我主要看五个指标:副作用强不强——是不是会写数据库、调外部 API、发消息、扣费;可逆性高不高——错了以后能不能撤销,还是只能补偿甚至补不了;影响范围大不大——是只影响当前会话,还是会波及用户、订单、资金;前提不确定性高不高——是不是严重依赖语义推断、缺信息;单次错误成本高不高——不只是钱,也包括合规、用户信任和人工修复成本。
不是”任务长”就该上重流程,而是”副作用大、不可逆、前提脆弱”才值得上。
我把任务分成四档。
最轻的一档是纯检索或纯生成,比如问天气、改文案。这类基本没有副作用,甚至不需要正式 Planning,直接走轻量 ReAct 就行。
第二档是低风险执行。有少量工具调用,但基本无持久化副作用。这类走一个简短计划就行,不做独立 Reviewer。保留一个很轻的 policy gate:一旦模型想做外部写操作,就要求显式确认参数来源,或者直接升档。
第三档是中风险执行。会写内部系统或触发有限副作用,但可逆、范围小。这类保留 Planning,也保留 policy gate,但 Reviewer 不是每一步都跑,而是只检查带副作用或高不确定性的步骤。
第四档才是前面讲的完整流程:Planner、步骤声明、独立 Reviewer、policy gate、执行后验证。留给外部副作用强、不可逆、涉及资金或合规的场景。
关键的一点是:风险分级不是一次性判出来的。系统在接单时先粗分,出计划后再精分——因为真正决定风险的不仅是用户怎么说,还是系统打算怎么做。用户说”清理旧日志”听起来很常规,但如果 Planner 选出来的动作是连接生产集群、执行 DELETE、批量影响三十天数据、没有 dry-run,那风险等级就会被直接拉高。执行过程中如果发现下一步要写库或关键参数其实是推断出来的,还要能动态升档。
风险判断不由单一角色来做。用户输入的语义、工具的元数据(只读还是可写还是 destructive)、计划中实际要执行的动作类型、前提的不确定性程度,这些信号共同决定风险等级。我习惯取偏保守的上界,而不是平均值。只要其中任意一项把任务拉高,系统就不能按低风险继续走。
这套升降档的机制做下来以后,系统不会因为每件事都走重流程而变得又慢又贵,也不会因为走了轻流程而漏掉真正危险的操作。
我后来不太愿意再回答”Planning 和 ReAct 哪个更好”这种问题
早些时候我确实会去比较这两个范式。现在更觉得这个问题本身就问偏了。
Planning 和 ReAct 不是对立关系,更像是不同不确定性结构下的不同工具。如果任务的路径已经比较清楚,只是步骤比较长,或者有明显的分阶段结构,让 Planning 先把大框架铺出来,通常比在在线循环里反复决策更稳。但如果任务的不确定性主要来自信息本身——不知道该去哪找,找到了也不知道对不对,中途随时可能根据反馈换方向——那 ReAct 的边走边看就很有价值。
更常见的情况是两者混着用。Planning 负责把任务的大框架搭出来,避免系统在明显可规划的部分反复消耗推理成本。局部那些真的要根据反馈随时改路的地方,留给 ReAct 式的短闭环。底下再用统一的 runtime 做结果清洗、状态管理和异常检测。
这种结构看起来不如”单一范式”干净,工程上却通常更稳。
我更喜欢把这种混合理解成职责分配。Planning 负责骨架,ReAct 负责局部探索,runtime 负责盯住不管上层怎么切范式底下都会出现的执行问题。这样分下来,系统虽然不像某些架构图里那么整齐,排查问题时反而更容易定位:计划出了毛病去查 planner,执行出了问题去查 runtime,状态丢了去查 context 管理。
回头看,我现在更认可的是”计划可以错,但系统不能失控”
如果现在让我重新搭一个 Planning 系统,我最先下手的不会是让模型把计划写得更漂亮。
我会先把几件事立住:每一步在执行前必须声明前提和副作用;系统要为每一步记录结构化轨迹;工具注册时必须声明可逆等级和支持的补偿动作;审计和规划要做上下文隔离;风险分级不是单点判断,而是多源信号取保守上界;完整流程只用于高风险任务,默认走轻控制,遇险升档。
这套东西对我最大的帮助,不是让架构图变好看,而是出了问题以后终于知道该去哪一层找。计划偏了,去看前提声明和依赖分析;补偿失败了,去看工具注册的补偿能力够不够;审计没拦住,去看 Reviewer 是不是和 Planner 共享了太多上下文;轻流程出了事,去看升档机制是不是被绕过了。
我也越来越反感那种”只要模型足够聪明,计划就不会出错”的想象。只要系统真的进了生产,你迟早要面对外部数据不稳定、用户意图模糊、工具返回格式不统一、某些操作根本不可逆这些现实。到那个时候,真正让系统站住的,不是 planner 写出了一份多完美的计划,而是计划失效以后系统还能不能快速止损、恢复、继续往前走。
Planning 模式真正考验的不是”能不能想清楚”,而是”想错了以后还能不能兜住”。系统可靠性不来自计划的正确率,而来自错误暴露以后的响应速度和恢复能力。
一句话:计划可以错,但系统不能失控。