ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent 记忆与上下文工程实战(7):子任务分解与状态机:Plan-and-Execute 的失败面

AI Agent 记忆与上下文工程实战(7):子任务分解与状态机:Plan-and-Execute 的失败面 窗口预算的最后一块账是任务骨架上一篇把工具定义从固定税改成了调度资源窗口的三大户——对话历史、检索注入、工具 schema——各有各的预算方案。但长任务里还有一类开销既不是记忆也不是工具而是任务自身的骨架一个需求分解成十几个子任务计划文本、每个节点的产出、失败重试的残骸全都往上下文里堆。用 ReAct 风格一步一问的 Agent 对此无感因为它的计划就是消息流水本身而一旦上 Plan-and-Execute——先出完整计划、再逐节点执行——计划树就成了第二份账本它既是上下文里最大的单块固定开销又是失败传播的载体。本篇拆这两件事计划树怎么记账才不撑爆窗口缺陷晚发现时状态机怎么决定返工多少、漏掉多少。Plan-and-Execute 与 ReAct谁在为计划付账ReAct 的开销模型简单粗暴每一步把思考-行动-观察追加进历史轨迹线性增长第 n 步要为前 n-1 步的原始输出付重发税。它没有计划树也就没有计划树的病——但也没有计划树的好处没有显式依赖无法并行失败后没有哪些步骤作废的答案只能整体重来。Plan-and-Execute 把决策前移一次生成分解树节点可验收的子任务边数据依赖然后按拓扑序执行。换来的能力是并行调度、进度可视、失败可定位付出的代价是计划与产物两坨新开销以及一个隐蔽的坑——多数实现把计划所有节点产物原文全量留在上下文里于是窗口占用变成第 n 步 ≈ 计划文本 前 n-1 个节点的完整产出。用第 488 篇的话说骨架在膨胀而骨架里的大部分内容在节点完成后就不再需要原文了——下游节点消费的应该是结论与指针不是上游的全部工作纸。关键的概念转换计划树不是上下文的一部分计划树就是记忆本体。上下文里只需要存它的一个投影——状态表。每个节点一行编号 | 依赖 | 状态 | 产物摘要 | 产物指针状态取 pending/running/done/failed/stale 之一产物原文落盘、按指针取用。节点的完成是一次状态迁移事件同时把派生信息摘要压进投影。这与第二篇的压缩纪律一脉相承离开窗口的内容按可再生性分流能重建的一行都不要多留。实验一两种记账法的窗口对账模拟一个 12 节点的依赖树形状近似真实需求T5 汇合 T3/T4T12 汇合 T10/T11节点产物体量与失败率固定种子生成两种记账法跑同一失败轨迹全量日志把每次成功的产物原文与失败报错全留窗口状态表摘要只留计划、每节点一行状态加一段定点摘要。窗口 8000 单位。importrandom WINDOW8000PLAN_COST1500# 初始计划文本(12 节点的任务分解验收标准)NODES[T%d%iforiinrange(1,13)]DEPS{T2:[T1],T3:[T1],T4:[T2],T5:[T3,T4],T6:[T5],T7:[T5],T8:[T6],T9:[T6,T7],T10:[T8],T11:[T9],T12:[T10,T11]}sizerrandom.Random(494)OUTCOME{n:sizer.randint(420,980)forninNODES}# 节点执行产物体量ERR_FULL300# 一次失败的完整报错FAIL_P{n:sizer.uniform(0.05,0.30)forninNODES}# 各节点失败率MAX_RETRY5print(节点产物体量: T1..T12 ,, .join(str(OUTCOME[n])forninNODES))print(节点失败率 :,, .join(%.2f%FAIL_P[n]forninNODES))defsimulate(mode):prngrandom.Random(4941)# 两种模式共享同一失败轨迹done,steps,retries,failedset(),0,0,0peakover0trace[]queuelist(NODES)whilequeue:nqueue[0]ifnotall(dindonefordinDEPS.get(n,[])):queue.append(queue.pop(0))continuequeue.pop(0)attempt0whileTrue:attempt1steps1okprng.random()FAIL_P[n]ifmode全量日志:ctxPLAN_COSTsum(OUTCOME[d]fordindone)\sum(ERR_FULLfor_inrange(retries))else:# 状态表: 计划每完成节点(行摘要)每失败(一行)ctxPLAN_COSTlen(done)*(46130)retries*12peakmax(peak,ctx)overctxWINDOWifok:done.add(n)trace.append((n,steps,ctx))breakretries1ifattemptMAX_RETRY:failed1done.add(n)# 标记为失败状态占位, 下游自行决定trace.append((n!,steps,ctx))breakreturnsteps,retries,failed,peak,over,traceformodein(全量日志,状态表摘要):steps,retries,failed,peak,over,tracesimulate(mode)print(\n%s: %d 步完成 12 节点(重试 %d 次, 最终失败 %d 个), 峰值上下文 %d, 超窗步数 %d%(mode,steps,retries,failed,peak,over))ifmode全量日志:forn,s,cintrace:print( 完成 %s 于第 %2d 步, 上下文已到 %5d %s%(n,s,c,- 超窗ifcWINDOWelse))full_sumPLAN_COSTsum(OUTCOME.values())table_sumPLAN_COST12*(46130)print(\n稳态账: 全量日志 %d (计划 %d12 份产物原文 %d), 状态表 %d, 压缩到 %.0f%%%(full_sum,PLAN_COST,sum(OUTCOME.values()),table_sum,100.0*table_sum/full_sum))print(关键差别不在稳态大小, 在重试: 全量日志把每次失败的 %d 单位原文都留下,%ERR_FULL)print( 状态表只记一行状态变化——失败越多, 两种记账法的窗口差距越大。)运行输出节点产物体量: T1..T12 573, 439, 447, 911, 700, 866, 703, 822, 840, 624, 836, 826 节点失败率 : 0.21, 0.23, 0.16, 0.28, 0.07, 0.28, 0.17, 0.11, 0.29, 0.24, 0.28, 0.05 全量日志: 19 步完成 12 节点(重试 7 次, 最终失败 0 个), 峰值上下文 11361, 超窗步数 6 完成 T1 于第 4 步, 上下文已到 2400 完成 T2 于第 5 步, 上下文已到 2973 完成 T3 于第 6 步, 上下文已到 3412 完成 T4 于第 8 步, 上下文已到 4159 完成 T5 于第 9 步, 上下文已到 5070 完成 T6 于第 10 步, 上下文已到 5770 完成 T7 于第 12 步, 上下文已到 6936 完成 T8 于第 13 步, 上下文已到 7639 完成 T9 于第 15 步, 上下文已到 8761 - 超窗 完成 T10 于第 16 步, 上下文已到 9601 - 超窗 完成 T11 于第 18 步, 上下文已到 10525 - 超窗 完成 T12 于第 19 步, 上下文已到 11361 - 超窗 状态表摘要: 19 步完成 12 节点(重试 7 次, 最终失败 0 个), 峰值上下文 3520, 超窗步数 0 稳态账: 全量日志 10087 (计划 150012 份产物原文 8587), 状态表 3612, 压缩到 36% 关键差别不在稳态大小, 在重试: 全量日志把每次失败的 300 单位原文都留下, 状态表只记一行状态变化——失败越多, 两种记账法的窗口差距越大。同一棵失败轨迹两种命运全量日志从 T9 起连续 6 步超窗——而 T9 之后恰恰是最依赖前文结论的汇合阶段撞墙的代价要么是组装器截断丢掉上游结论模型开始忘了计划重复劳动或引用空指针要么是请求直接失败。状态表法峰值只有 3520窗口占用与计划规模近似线性、与产物大小解耦。注意稳态那行状态表不是把产物压缩了而是换了一种存储——原文落盘、窗口只留投影。这正是第 3 篇断言与过程分离在多步执行上的翻版。重试次数的账也要看7 次重试在全量日志里留了 2100 单位的报错原文在状态表里只是 84 单位的状态变化——失败率越高的真实环境两种记账的差距越悬殊。实验二缺陷晚发现的返工经济学状态机真正的价值不在执行顺畅时在翻车时。设 T5 的中间产物有缺陷直到闭包最后一个节点 T12 完成才暴露。三种处置的返工账与质量账按依赖闭包精确失效重跑、不敢信任任何中间结果的全量重跑、只改 T5 但下游沿用旧产物的打补丁不回滚。importrandom rngrandom.Random(4942)NODES[T%d%iforiinrange(1,13)]DEPS{T2:[T1],T3:[T1],T4:[T2],T5:[T3,T4],T6:[T5],T7:[T5],T8:[T6],T9:[T6,T7],T10:[T8],T11:[T9],T12:[T10,T11]}defdescendants(root):T5 的下游依赖闭包: 它的缺陷会传染到哪些节点。outset()stack[root]whilestack:nstack.pop()forminNODES:ifninDEPS.get(m,[])andmnotinout:out.add(m)stack.append(m)returnout COST{n:rng.randint(2,6)forninNODES}# 节点重跑成本(步)P_STALE0.30# 单节点读到旧值导致产出错的概率closuredescendants(T5)print(缺陷源 T5, 各节点重跑成本: , .join(%s%d%(n,COST[n])forninNODES))print(T5 的依赖闭包(%d 个节点): %s%(len(closure),, .join(sorted(closure,keylambdax:int(x[1:])))))print(发现时机: 闭包内最后一名成员 %s 完成后才暴露——典型的缺陷晚发现%max(closure,keylambdax:int(x[1:])))strategies{}strategies[闭包失效](COST[T5]sum(COST[n]forninclosure),1.0)strategies[全量重跑](sum(COST.values()),1.0)patch_rerunCOST[T5]hazards1.0forninclosure:hazards*(1-P_STALE)# 全部 7 个下游都干净的概率strategies[打补丁不回滚](patch_rerun,hazards)print(\n策略 返工步数 最终交付正确率(全下游干净))forname,(cost,acc)instrategies.items():print(%-12s %4d %.3f%(name,cost,acc))wastesum(COST.values())-(COST[T5]sum(COST[n]forninclosure))print(\n全量重跑比闭包失效多花 %d 步——多出来的全是与缺陷无关的 T1-T4 白工;%waste)print(打补丁省了 %d 步返工, 却把 %d 个下游节点暴露在旧值污染下, 正确率 %.3f:%(COST[T5]sum(COST[n]forninclosure)-patch_rerun,len(closure),strategies[打补丁不回滚][1]))print( 等于用交付质量换工时, 且污染是静默的——没有状态机的失效传播, 错误不会被发现, 只会被交付。)运行输出缺陷源 T5, 各节点重跑成本: T12, T24, T36, T43, T53, T64, T72, T84, T94, T105, T115, T124 T5 的依赖闭包(7 个节点): T6, T7, T8, T9, T10, T11, T12 发现时机: 闭包内最后一名成员 T12 完成后才暴露——典型的缺陷晚发现 策略 返工步数 最终交付正确率(全下游干净) 闭包失效 31 1.000 全量重跑 46 1.000 打补丁不回滚 3 0.082 全量重跑比闭包失效多花 15 步——多出来的全是与缺陷无关的 T1-T4 白工; 打补丁省了 28 步返工, 却把 7 个下游节点暴露在旧值污染下, 正确率 0.082: 等于用交付质量换工时, 且污染是静默的——没有状态机的失效传播, 错误不会被发现, 只会被交付。三行账读出三层含义。闭包失效 31 步 vs 全量重跑 46 步依赖边就是止损半径——把计划写成显式 DAG 的全部意义就是让机器能算出谁被污染了与其无差别重做不如沿边失效。打补丁不回滚最刺眼只花 3 步但 7 个下游逐个有 30% 概率读到旧值全体干净的概率剩 0.082——将近九成的交付带伤而且带伤是静默的没有任何节点会主动报错因为它们的输入看起来合法。这就是为什么状态必须显式建模stale上游作废时下游 done 要自动降级为 stale 并阻断验收让污染变成员工可见的红色状态而不是交付后用户发现的错误。返工半径还有一个隐藏开关——检测时机闭包越大、发现越晚返工越贵所以计划树要配节点级验收物每节点留下可独立校验的产物与断言让缺陷尽量在自己的小闭包里暴露而不是攒到大汇合。工程化改进四件套一是状态表投影上下文中常驻的只有状态表每节点一行编号/依赖/状态/摘要/指针产物原文落盘消费时按指针取该取的那段——注意这与第 4 篇的注入纪律联动取回的内容放尾部、带编号。二是显式迁移状态变更只允许通过事件attempt_ok/attempt_fail/invalidate/repair_done任何模块不得私改表迁移函数里同时做下游传播invalidate 沿出度递归把 done 降 stale。三是失败预算每节点带重试上限与失败动作跳过并标记/换方案/升级人工防止单点失败把整个循环卷进重试风暴——实验一里 7 次重试若无上限19 步能变 190 步。四是验收物done 状态必须由断言触发测试通过、schema 校验、数值对账模型说做完了不算完成这条直接决定缺陷发现时机也就决定实验二那张表的返工列。常见陷阱一是计划一次性生成到天边50 个节点的计划里 30 个是臆测执行到一半现实翻脸计划树作废前面的产物成了没人认领的孤儿——层级分解先粗后细、执行一层再展开一层才是稳态。二是把节点产物原文留在窗口以防万一万一不出现税每步都交落盘摘要才是默认留原文要写明理由。三是用自然语言维护状态T3 应该已经完成了吧没有状态位模型每次读到都要重新推理一遍进度推理结果还随上下文扰动漂移。四是重跑时不清下游T5 重做后 T6-T12 的 done 原地不动旧结论继续被引用——这是打补丁不回滚事故的常规成因。五是汇合节点没有冲突检测T12 的输入 T10/T11 结论互相矛盾组装器照单全收模型随机采信——汇合处要做断言一致性检查冲突就打回较小的闭包。落地清单计划树以 DAG 数据结构存节点依赖边验收断言先粗分、执行一层展开一层上下文只放状态表投影每节点一行产物落盘挂指针按需取回并放尾部状态迁移全部走事件函数invalidate 自动沿边传播降级下游为 stale每节点配重试上限与失败动作done 只能由验收断言触发记录缺陷发现位置 vs 缺陷节点的距离指标发现越晚返工越贵据此决定验收物密度单任务的计划树治理讲完了但真实世界还有更狠的任务跑到一半进程没了——断电、限流、发布重启。恢复时哪些状态可以直接续、哪些产物已经过期、checkpoint 存什么才不变成把窗口抄在硬盘上下一篇《AI Agent 记忆与上下文工程实战8长任务的记忆治理checkpoint、恢复与失效策略》把崩溃当成默认事件来设计。参考来源Wang et al., Plan-and-Solve Prompting: Improving Zero-Shot Chain-of-Thought Reasoninghttps://arxiv.org/abs/2305.04091Yao et al., ReAct: Synergizing Reasoning and Acting in Language Modelshttps://arxiv.org/abs/2210.03629Shinn et al., Reflexion: Language Agents with Verbal Reinforcement Learninghttps://arxiv.org/abs/2303.11366LangGraph Documentation, Persistence checkpointinghttps://langchain-ai.github.io/langgraph/concepts/persistence/Wikipedia, Finite-state machinehttps://en.wikipedia.org/wiki/Finite-state_machine
返回列表