work-graph-self-planning-and-loop-back-requirements.md 23.1 KB

Planner 动态自规划 Work Graph(含节点判断与失败回路)——需求文档

日期: 2026-08-08 性质: 需求文档(BRD/PRD 层面),不含详细技术设计。回应用户 2026-08-07/08-08 两轮讨论:先是"要不要让 Planner 自己判断要不要有 test_script 节点",深入后明确为一个更完整的诉求——Planner 第一步应该是自己规划 Work Graph(要不要 brd/prd/sdd/benchmark/test 的组合、未来还有更多 tool/skill 可组合调用),并为每个节点规划输入输出边约束、判断成功/失败的方式,失败时把结果送回更早的节点形成回路(loop),并把 loop 过程中的产出记录下来供后续轮次参考改进。详细设计(SDD 层面:graph_manifest 的具体 JSON 形状、topological_layers()/run_graph() 的具体改造点、循环历史的具体持久化方式)留到本文档确认后单独产出。 状态: ✅ 已实现(2026-08-08)。原「8. 开放问题」的 7 个分叉点均已拍板,记录见「8. 已确认的决策」,正文相应章节已按决策更新。详细设计见 docs/todo/work-graph-self-planning-and-loop-back-design.md;Part A(判断+回路引擎)与 Part B(Planner 自规划)均已实现,python3 -m unittest discover -s src(692 tests)+ discover -s supervisor -p "test_*.py"(126 tests)全绿。尚未做真实 RUN_REAL_AGENT_E2E=1 端到端验证(需要真实 API 额度,等待用户决定是否现在跑)。

1. 背景

1.1 现状:三条独立的、能力有限的机制

  • Planner 自己的文档链(work_graph_plan.py::_PROMPT_TEMPLATE):过一层真实的 LLM 规划调用,但提示词硬编码要求"必须为 brd/prd/sdd/benchmark/test_dataset 各出一个节点"(test_script 已于 2026-08-07 从这个固定清单里移除,见 docs/todo/validator-test-script-narrative-output-issue.md)——节点集合本质是固定的,规划这一步只是在确认依赖关系,不是真的自由决策"要不要有某个节点"。这是 2026-08-06 的一次明确设计决定(docs/done/dynamic-work-graph-design-skill-design.md §3 原话:"不是让 Claude 完全自由决定 Planner 要生成哪些节点——Planner 那条链是'架构常量'")。
  • _GENERIC_PROMPT_TEMPLATE(同文件,服务 Coder/Validator 的子任务图):真正让 LLM 自由规划 {node_id, depends_on, task_prompt},但目前只有 work_graph_fork.py(人工发起的 time-travel 调试功能)在用,没有接入任何生产路径的自动执行;且和 Planner 的固定链一样,只是一张纯 DAG,没有判断/回路概念。
  • work_graph_orchestrate.py::run_graph() 的验证-重试机制:_verify_and_maybe_regenerate() 只支持"某节点验证失败 → 原地重新生成同一个节点一次 → 还失败就整体 ConsistencyCheckFailedError"。这不是回路——失败结果不会被送回更早的节点,历史反馈也只保留最近一轮("; ".join(feedback) 直接拼进 reflect prompt,不会累积多轮)。

1.2 两个此前未被充分利用的既有事实

  • edge_type(zero_token/needs_judgment)字段已经存在于 GRAPH_MANIFEST_SCHEMA,但从未被任何执行逻辑读取——topological_layers() 只看 depends_on/from-to 决定分层,完全不管 edge_type 的值,是纯文档字段。
  • topological_layers() 对任何环直接 raise ValueError——现在的执行引擎从数据结构到算法都是严格 DAG,没有"跳回更早节点"这种控制流的容身之处。docs/sdd/01-M-模型设计.md:60 里 work_graph_run.graph_manifest 字段的文档说明其实从一开始就写了"概念上应涵盖 fan-out/fan-in 分组、loop 规则",只是从未真正实现过。

1.3 一个必须正面回应的架构决定

docs/graphlemon_architecture.md"方案 A"明确设想过:Work Graph 内部的 fan-out/fan-in/loop/verifier 应该直接用 Claude Code 原生 Dynamic Workflow 原语(agent()/parallel()/pipeline(),类似 loop-until-dry)编排,而不是手撸 Python 引擎。但 work_graph_orchestrate.py 的实际实现故意背离了这个设想(一个节点一个独立 claude -p 子进程 + Python 拓扑引擎逐节点持久化状态),原因写在该模块自己的 docstring 里:REQ-06 断点续跑——原生原语跑在一次不透明的 claude -p 调用里,中途崩溃就会丢失全部循环历史和已完成节点状态;现在的设计换来的是"进程重启后能从任意已完成节点继续,不重跑"。

本需求延续这个既有背离,在 Python 引擎里新增回路能力,而不是回退去用原生原语——因为 REQ-06 是已经在生产里被真实依赖的能力(self-hosting 崩溃恢复、worktree resume),原生原语给不了这个保证。

2. 目标

  1. graph_manifest 的节点能声明式地表达"怎么判断这个节点成功/失败"(机械检查引用,或另一次 LLM 判断),不再要求调用方在 e2e_pipeline.py 里为每种 artifact_type 手写专门的 verify_fn 分支。
  2. graph_manifest 的节点能声明式地表达"失败之后怎么办":原地重试(现状)、跳回更早的某个指定节点重新执行(新增)、或直接判定整图失败(现状的显式化)。
  3. 触发"跳回更早节点"时,被跳回节点到失败节点之间的整条下游链要被重新执行(下游数据是基于坏输入产出的,不能继续用);同时,这个节点每一轮被拒绝的历史(不只是最近一轮)要被完整地喂给它自己的下一次重新生成,让它能看到"过去几次分别为什么被拒绝",而不是每次都从零开始猜。
  4. 这套回路能力必须有可配置的上限,且在进程中途崩溃重启后能正确恢复"当前是第几轮",不能死循环也不能重置归零。
  5. 在回路执行引擎具备之后,让 Planner 自己的规划调用能真正决定"这个 goal 需要哪些节点",而不是像现在这样被提示词硬编码死一份固定清单——这份清单本身也应该是可扩展的数据,为未来接入更多 tool/skill 节点类型留出空间(这轮不要求真的新增新类型,只要求机制本身可扩展)。
  6. 长期方向(本轮不实施,仅作为设计约束):用户明确的最终目标是"Planner 改造成功后,Coder、Validator、甚至未来新增的 org graph node,都需要具备同样的能力——先 plan 自己的 work graph,再按照 work graph 执行"。本轮的 Part A(judgment/on_fail 机制)设计必须是 org-agent 无关的通用能力,不能是 Planner 专属的特殊逻辑,即使这轮只把它接到 Planner 一个 org node 上(见「3. 非目标」第 1 条、「8. 已确认的决策」第 6 条)。

3. 非目标(本轮明确不做)

  1. 本轮实施范围仍只覆盖 Planner,不把 Coder/Validator 也接上自规划。_VALIDATOR_MANIFEST 现在完全是硬编码的 Python 字面量,连"过一层 LLM 规划"都没有;Coder 现在是合成单节点图。用户已确认长期方向是所有 org agent(含未来新增的)都要具备同样能力(见「2. 目标」第 6 条),但落地顺序是"Planner 改造验证成功后,再依次推广"——不是本轮的实施范围,本轮只是设计时不能把机制设计成 Planner 专属。
  2. 不回退到 Claude Code 原生 Dynamic Workflow 原语(见 1.3)。继续在 Python 层的 work_graph_orchestrate.py 里扩展执行引擎,保住 REQ-06 断点续跑。
  3. 不追求完全通用的图执行框架。回路的触发方式限定为"某节点判断失败 → 跳回自己声明的 on_fail.target_node_id",不支持任意节点间的条件分支、多目标跳转、或图结构在运行时被重新规划——这些是更大的能力,不在这轮范围内。
  4. 不重新设计跨 Org 的既有 loop 机制(org_level_loop.py/org_loop_route.py:Validator FAIL/INCONCLUSIVE → 判根因 → 路由回 Planner/Coder,loop_round 封顶 3 轮)。本需求的回路是"同一个 Org Agent 自己的 Work Graph 内部"这一层,比跨 Org 的 loop 粒度更细,两者独立计数、不共享额度、不互相替代——内层耗尽自己的上限后照旧抛 ConsistencyCheckFailedError,外层机制不变地接管。
  5. 不在本需求里解决 test_script/validator_test_script 节点"叙述文字而非代码"的结构性根因。这个节点已于 2026-08-07 因该问题禁用(docs/todo/validator-test-script-narrative-output-issue.md),根因是"探索后收尾习惯",跟本需求要解决的问题(节点集合是否可以自由规划、失败能否形成回路)完全正交。用户已确认(见「8. 已确认的决策」第 7 条):Part B 上线后,test_script 可以直接作为可选节点提供给 Planner 自由规划选择,不需要等这个结构性问题修复;该结构性问题的真正修复安排在另一份文档里,跟"给 Coder agent 加入 test_script 模块"一起解决——本需求只负责不阻塞它,不负责修它。
  6. 不修改现有 _GENERIC_PROMPT_TEMPLATE 路径本身(Coder/Validator 子任务图、work_graph_fork.py 用的那条)。如果这轮的 judgment/on_fail 字段设计得足够通用,未来可以顺带让这条路径也受益,但不是本轮的验收范围。

4. 功能需求

分两部分:Part A 是执行引擎扩展(判断 + 回路能力),Part B 是 Planner 自规划节点集合。Part B 依赖 Part A 先完成——自由规划出的图如果没有真正的失败回路可用,只是重新引入"LLM 自由吐图结构"的脆弱性(2026-08-03 那次真实故障:LLM 漏给 depends_on/edges,benchmark 排到 sdd 前面执行,直接 KeyError 崩溃),却拿不到新能力。

Part A:节点判断 + 失败回路执行引擎(work_graph_orchestrate.py)

# 需求描述 优先级
FR-A1 graph_manifest 的节点应支持声明式的 judgment 配置(机械检查引用,或触发一次额外 LLM 判断调用),替代现在"调用方在 Python 里为每种 artifact_type 手写 verify_fn 分支"的模式。 必须
FR-A2 graph_manifest 的节点应支持声明式的 on_fail 配置,至少支持三种行为:retry_self(现状:原地重新生成同一节点一次)、loop_back(跳转到指定的更早节点重新执行)、hard_fail(不重试,直接判定整图失败——现状默认行为的显式化)。 必须
FR-A3 触发 loop_back 时,不只重跑 target_node_id 本身,还要重跑它到触发失败的节点之间的整条下游链,因为这条链上的产出都是基于(即将被修正的)过期输入生成的。 必须
FR-A4 loop_back 的每一轮反馈(这一轮产出了什么、为什么被判定失败)必须可完整还原,且 target_node_id 重新执行时,累积的完整历史(不只是最近一轮)要被喂进它的提示词。不新增专门的历史表——复用现有 execution_event 日志机制(node_start/node_completed 等既有事件),每次 loop_back 触发/重新执行时在事件里记录 predecessor/successor 节点关联,让"这个节点被打回过几次、每次因为什么"可以直接从现有事件流重建,同时让现有的图可视化机制(v_graph_visualizer)能把回路自然地呈现成图上的一条边,不需要另外维护一份历史、也不需要事后做双份数据的一致性对账。 必须
FR-A5 每一对 (loop_back 目标节点, 触发回路的失败节点) 需要独立计数,并有上限;超限时整个 run_graph() 调用判定失败(复用/扩展现有 ConsistencyCheckFailedError 语义),不允许无限循环。上限不是写死的全局常量——复用 settings_config.py(现有 org_concurrency_config 的同类模式:DB 存储、get_/upsert_ 读写接口)新增一张按 (org_node, node 类型或 node_id 模式) 维度可配置的上限表,允许后续根据真实调用经验(哪些节点类型容易被反复打回、上限设多少更合理)动态调整,不需要改代码重新部署;节点自身也可以在 on_fail.max_attempts 里覆盖这个默认值。 必须
FR-A6 循环计数与历史必须能扛得住 REQ-06 式的断点续跑:进程在循环中途崩溃重启后,应正确恢复"当前是第几轮、历史反馈有哪些",不能从零重置,也不能因为状态丢失而死循环。 必须
FR-A7 topological_layers() 的静态分层只应基于"数据依赖边"(depends_on/consumes);节点声明的 on_fail.target_node_id(运行时才可能触发的控制流跳转)不参与静态拓扑排序,不应被当成图里的环而报错。 必须
FR-A8 不声明 judgment/on_fail 字段的既有 graph_manifest(例如 Coder 现在的合成单节点图)行为必须完全不变——这两个字段可选,缺省等价于现状(judgment=none 直接通过、on_fail=hard_fail 即现在的"验证失败重试一次后硬失败")。 必须
FR-A9 judgment/on_fail 机制的实现(graph_manifest schema、run_graph() 的执行逻辑、FR-A5 的上限配置表)必须是 org-agent 无关的通用能力——不能依赖任何 org_node == "planner" 的特判。这轮只有 Planner 会真正产出带这些字段的 graph_manifest,但 Coder/Validator 未来接入时,不应该需要改 work_graph_orchestrate.py 本身,只需要它们自己的规划提示词开始产出这些字段(呼应「2. 目标」第 6 条的长期方向)。 必须

Part B:Planner 自规划节点集合(依赖 Part A)

# 需求描述 优先级
FR-B1 Planner 的规划提示词从"必须产出固定清单里的每一个节点"改为:给定一份可用节点类型清单,由 LLM 自己判断这个 goal 需要哪些节点、它们的依赖关系,以及(复用 Part A)每个节点的 judgment/on_fail 配置。 必须
FR-B2 可用节点类型清单本身应是可扩展的数据(而不是硬编码在提示词字符串里的固定枚举),为未来接入更多 tool/skill 节点类型留出空间;本轮不要求真的新增除现有文档类型之外的新类型。 应该
FR-B3 自由规划出的 graph_manifest 必须经过与现有 _normalize_graph_manifest 同等严格的健壮性校验/兜底,防止 2026-08-03 那次真实故障重演(LLM 输出遗漏 depends_on/edges,导致执行顺序错误甚至崩溃)——节点集合变自由不代表可以降低这层鲁棒性要求。 必须
FR-B4 brd/prd/sdd/benchmark/test_dataset(以及 test_script,一旦被规划出来)全部由 LLM 自由规划,不保留任何代码级的固定顺序/固定节点集合兜底——已确认(「8. 已确认的决策」第 1 条):完全推翻 docs/done/dynamic-work-graph-design-skill-design.md §3"Planner 链是架构常量"的决定。FR-B3 的健壮性校验兜底(防止 LLM 漏给 depends_on/edges)因此变得更重要:没有任何硬编码的顺序可以兜底,健壮性完全依赖 FR-B3。 必须

5. 验收标准

  1. 构造一个 graph_manifest:节点 A 产出,节点 B 依赖 A、声明 judgment 机械检查、on_fail=loop_back 指向 A;用 mock 让 B 连续两轮判断失败、第三轮通过。断言:A 被重新执行了两次(不是原地重试 B),且 A 第二次、第三次重新执行时收到的提示词里包含前面所有轮次的失败反馈(不只是最近一轮)。
  2. 同上场景,把 B 的失败改成"每一轮都失败",断言在达到可配置上限后,run_graph() 判定整图失败并明确抛出(而不是无限循环挂起),且失败前 A 已经产出的内容仍按现有"部分成功节点落盘"机制(_write_completed_node_artifacts)被正确保留。
  3. 模拟"回路进行到第 2 轮时进程中断重启",断言恢复执行后能正确识别"已经是第 2 轮"并继续,而不是把计数器归零重新从第 1 轮开始,也不是把已经持久化的历史反馈丢掉。
  4. 一个声明了 on_fail.target_node_id 指向更早节点的 graph_manifest,topological_layers()/图校验逻辑不应把它当作静态环拒绝(不抛 ValueError)。
  5. 一个完全不使用 judgment/on_fail 字段的既有 graph_manifest(例如 Coder 当前用的合成单节点图),跑一遍 run_graph(),行为与改造前逐字节一致——现有 src/test_work_graph_orchestrate.py/src/test_e2e_pipeline.py 里未显式测试新字段的用例,不修改断言即可继续通过。
  6. (Part B)给 Planner 一个明显不需要 test_dataset 的 goal(例如纯配置项调整),断言自规划产出的 graph_manifest 里确实没有 test_dataset 节点,且整条链路(Coder 拿到的 test_dataset_content 走 "N/A: not provided" 兜底)不因为节点缺失而报错——复用现有的 "test_dataset" in artifact_ids 容错逻辑,不需要新代码。
  7. (Part B)人为构造一个自由规划回复"遗漏 depends_on"的场景(模拟 2026-08-03 那次真实故障的输入),断言健壮性兜底仍能纠正执行顺序,不崩溃。
  8. (Part B)给 Planner 一个明显需要机械化验收脚本的 goal,可用节点类型清单里包含 test_script,断言 Planner 能够(不是必须,是"能够")自由规划出一个 test_script 节点——确认 test_script 已按「8. 已确认的决策」第 7 条重新开放为可选节点,不再被清单排除。
  9. (FR-A5)验证上限配置确实按 (org_node, node 类型) 生效:给同一个 org_node 下两种不同 node 类型分别配置不同上限,构造两个各自持续失败的 loop_back 场景,断言各自在自己配置的上限触发失败,不是共用同一个全局值;再验证节点自身 on_fail.max_attempts 能覆盖配置表里的默认值。
  10. (FR-A4)验证 loop_back 历史确实通过 execution_event 而非新表重建:触发一次多轮 loop_back 后,仅查询 execution_event 表(不查询任何新增表)即可还原"这个节点被打回几次、每次的 predecessor/successor 是谁、每次的反馈内容"。

6. 约束与假设

  • 复用 REQ-06 已经建立的"work_node_execution.output_payload 是节点成功后的权威持久化数据"这一既有约定;回路历史不新增专门的表,完整复用 execution_event 现有机制(见 FR-A4),必须遵循"重启后可完整恢复"这一约束,不引入新的、易丢失的临时状态。
  • 复用 org_loop_route.py::_LOOP_LIMIT 的既有模式("计数 + 达到上限即转 blocked_pending_human")作为 Part A 回路上限触发后果的参照,但两者是独立的计数器,不共享同一个 task.loop_round 字段;上限的具体数值来源改为复用 settings_config.py 的 DB 配置模式(见 FR-A5),不是抄一个固定常量。
  • judgment.kind="llm" 复用 _check_prd_consistency 现有模式:额外发起一次 agent() 调用(agentType='planner'),self-target 场景下走独立 worktree 隔离,避免重新设计一套判断机制。
  • Part A 完成后,Coder 现在的合成单节点图、_GENERIC_PROMPT_TEMPLATE 路径都不强制迁移使用新字段——新字段是可选能力,不是所有 graph_manifest 生产者都必须立刻采用;但机制本身必须 org-agent 无关(见 FR-A9),为后续推广铺路。

7. 风险与已知权衡

  • 用户已知情并接受:Part B 上线后 test_script 类节点可能被自由规划回来,narrative-output 问题可能重现。docs/todo/validator-test-script-narrative-output-issue.md 记录的根因是"探索后收尾习惯",跟本需求解决的问题(节点集合能不能自由规划、失败能否形成回路)完全正交,本需求不修它。用户决策(「8. 已确认的决策」第 7 条):不等结构性修复,真正的修复安排在"给 Coder agent 加入 test_script 模块"那份文档里一起解决——本需求上线后,如果自举/真实任务里又复现了叙述文字问题,这是已知的、被接受的风险,不是本需求的回归。
  • 回路能力本身可能被滥用成隐藏的高成本重试:一个设计不当的 judgment(例如过于严格的 LLM 判断标准)配合 loop_back,可能导致某个节点反复被打回、消耗大量 token 才勉强通过上限。FR-A5 的按 (org_node, node 类型) 可配置上限缓解了"一刀切"的问题,但初始默认值仍需要设计阶段给出具体理由,不能只是"抄一个数字";上线后应该根据真实调用数据回头调整(这正是选择"DB 配置表"而不是"硬编码常量"的原因)。
  • Part B 是对已有架构决定的正式推翻,不是渐进式增强。docs/done/dynamic-work-graph-design-skill-design.md §3 需要在 Part B 实施后同步更新,避免文档和代码再次出现本 session 里多次遇到的"文档记录的意图与实际代码不一致"的问题。
  • Part A + Part B 一并实施(已确认,见「8. 已确认的决策」第 5 条),没有中间的"仅 Part A 先跑一段时间验证稳定性"的缓冲阶段——意味着 Part A 的回路引擎第一次被真实使用,就是在"节点集合本身也是自由规划、不受固定链条约束"这个更不可预测的场景下,两个新变量同时引入,出问题时定位"是回路引擎的 bug 还是自由规划出了一个奇怪的图结构"会比分阶段更难。这是用户已知情的权衡,不是本需求遗漏。

8. 已确认的决策(2026-08-08)

  1. Part B 的推翻力度:全部自由规划。brd/prd/sdd/benchmark/test_dataset(以及重新开放的 test_script)不保留任何代码级固定顺序/固定节点集合兜底,完全推翻 docs/done/dynamic-work-graph-design-skill-design.md §3 的既有决定(对应 FR-B4)。健壮性完全依赖 FR-B3 的校验兜底。
  2. 回路上限:不设单一全局默认值,按 (org_node, node 类型) 可配置,复用 settings_config.py 的 DB 配置模式,根据实际调用经验动态调整(对应 FR-A5)。
  3. judgment.kind="llm" 的实现:复用现有 _check_prd_consistency 模式(额外发起一次 agent() 调用,agentType='planner',self-target 场景走独立 worktree)。
  4. 回路历史持久化:不新增专门的表,完整复用 execution_event 现有日志机制,记录 predecessor/successor 节点关联,天然可被现有图可视化机制呈现为有向图上的边(对应 FR-A4)。
  5. 落地顺序:Part A、Part B 一起做,不分阶段(见「7. 风险」最后一条,这是已知权衡,不是遗漏)。
  6. Validator/Coder 是否一起做:本轮仍只落地 Planner,但 Part A 的机制设计必须 org-agent 无关(FR-A9)——用户明确的长期方向是 Planner 验证成功后,Coder、Validator、乃至未来新增的 org graph node 都要具备"先 plan 自己的 work graph,再按图执行"的同一能力(见「2. 目标」第 6 条)。
  7. test_script 是否要等结构性修复:不需要等。Part B 上线后,test_script 直接作为可选节点开放给 Planner 自由规划;narrative-output 问题的真正结构性修复安排在"给 Coder agent 加入 test_script 模块"的另一份文档里一起解决,不是本需求的阻塞项(见「7. 风险」第一条)。