Commit 0d7a045a3d0504a332cf86be63e178ac9d72ed02

Authored by graphlemon
1 parent a9c0f8bb

graphlemon: auto-commit uncommitted changes for fdbf3122-5ab9-4d14-81c1-e1b8ed7f…

…d9ae-main-checkout-drift
.claude/agent-memory/planner/MEMORY.md
... ... @@ -2,3 +2,4 @@
2 2 - [scheduler.py busy-timeout comment missing](project_scheduler_busy_timeout_comment_missing.md) — as of 2026-08-03 _BUSY_TIMEOUT_MS (scheduler.py:78) has no inline comment; PL does not self-edit, packages as a Unit for Code Agent
3 3 - [deploy_health_check.py startup-timeout comment: RESOLVED](project_deploy_health_check_startup_timeout_comment_missing.md) — comment now present above HEALTH_CHECK_STARTUP_TIMEOUT (line 24); re-verify by reading file before re-flagging
4 4 - [deploy_health_check.py HTTP-timeout comment missing](project_deploy_health_check_http_timeout_comment_missing.md) — HEALTH_CHECK_HTTP_TIMEOUT (line 26) has no comment directly above it, unlike its sibling STARTUP_TIMEOUT; distinct open fact, don't conflate the two
  5 +- [artifact overview click-through bug: 3 real gaps](project_artifact_overview_click_through_bug.md) — renderArtifactSummaryList has no data-artifact-id/click (confirmed); router has zero query-string support (#/artifacts?id=x falls through to Not found); showPreview() already exists, just never called from a URL param
... ...
.claude/agent-memory/planner/project_artifact_overview_click_through_bug.md 0 → 100644
  1 +---
  2 +name: project-artifact-overview-click-through-bug
  3 +description: Investigation findings for the "Planner 产出总览" list-item click-through bug (frontend/app.js), needed by whoever writes the SDD/benchmark content for this task
  4 +metadata:
  5 + type: project
  6 +---
  7 +
  8 +Investigated 2026-08-04 for the "Planner 产出总览面板列表项点击无反应" bug-fix task
  9 +(graph_manifest planned same session). Root cause is real and narrower than it first
  10 +looks — three separate gaps, not one:
  11 +
  12 +1. `renderArtifactSummaryList()` (frontend/app.js:1344) renders bare `<li>` with no
  13 + `data-artifact-id` and no click handler — confirmed, matches the task's stated root cause.
  14 +2. The router (`frontend/app.js:77-132`, `routes` object + `render()`) does exact-string
  15 + lookup on `window.location.hash` (`routes[hash]`), with only two regex-based dynamic
  16 + routes today: `TASK_DETAIL_RE` (`#/tasks/:id`) and `SKILL_AUDIT_RE`
  17 + (`#/skills/:id/audit`). There is **no query-string parsing anywhere** in the router —
  18 + a hash like `#/artifacts?artifact_id=xxx` would fail `routes[hash]` *and* both regexes
  19 + and fall through to "Not found". Fixing this bug requires extending routing (either a
  20 + new `#/artifacts/:id`-style dynamic route matched the same way as `TASK_DETAIL_RE`, or
  21 + adding query-string stripping/parsing to `render()` before route lookup) — it is not
  22 + just a matter of adding an `href`.
  23 +3. `mountArtifactBrowser()` (frontend/app.js:1582) already has the exact mechanism needed
  24 + to *display* a specific artifact once you have its id — `showPreview(artifactId)` sets
  25 + `selectedArtifactId` and fetches `/api/artifacts/:id` — but it is only ever invoked from
  26 + a click on `li[data-artifact-id]` inside `refreshList()`, never from a URL param. The
  27 + comment at app.js:1353-1356 ("v_artifact_browser and v_task_board don't parse URL query
  28 + params today ... out of scope") was written about the *"查看全部"* link specifically
  29 + (intentionally unfiltered) — it is not evidence that per-item deep-linking is already
  30 + supported or was rejected; don't misread it as covering this case.
  31 +
  32 +**How to apply**: when writing the SDD for this task, the technical design must cover (a)
  33 +adding `data-artifact-id` + click → hash navigation in `renderArtifactSummaryList`, (b) a
  34 +route-level change (new dynamic route or hash query-parsing) so `#/artifacts` can carry an
  35 +artifact id, and (c) `mountArtifactBrowser` reading that id on mount and calling the
  36 +existing `showPreview()`/`refreshList()` flow to preselect it — reusing `c_artifact_get` /
  37 +`GET /api/artifacts/:artifact_id` as-is, no new content-preview logic. The "查看全部" links
  38 +(app.js:1387-1390) must keep pointing at plain `#/artifacts` / `#/tasks` — do not add the
  39 +id param to those.
... ...
docs/reference/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).diff-with-graphlemon.md 0 → 100644
  1 +# 对照笔记:《Graph Engineering with Claude: 14-Step roadmap》vs graphlemon 实际代码
  2 +
  3 +- **对照对象**:[Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md](./Graph%20Engineering%20with%20Claude_%2014-Step%20roadmap%20from%200%20to%20graph%20architect%20(Full%20Course).md)(本文简称"14 步版")
  4 +- **和已有对照的关系**:`docs/graphlemon_architecture.md` §10 在 **2026-07-29(设计阶段,尚未开始实现)** 就对 14 步版做过一轮逐条映射,并在 §10.1 决策"Work Graph 执行层直接复用 Claude Code 原生动态工作流"。当时是设计意图,不是已验证的实现。本笔记的定位不同:**现在全部 43 个 Unit 已实现并自举运行**,所以这次是拿 14 步版去对照 `src/work_graph_orchestrate.py`、`src/work_graph_execute.py`、`src/work_graph_plan.py` 的真实代码,检验当初 §10.1 的设计意图有多少真的落地、有多少在实现过程中被推翻或修正。
  5 +- **状态**:讨论草稿,待后续会话继续展开。
  6 +- **整理日期**:2026-08-04
  7 +
  8 +---
  9 +
  10 +## 0. headline:Work Graph 执行层并没有真的用上 Claude 原生 `parallel()`/`pipeline()`
  11 +
  12 +这是本次对照发现的最重要的一条,也是对 §10.1 原决策的一处实质性修正,值得放在最前面单独讲清楚。
  13 +
  14 +`src/work_graph_orchestrate.py` 的模块 docstring 直接写明了这个偏离,是有意为之:
  15 +
  16 +> Design: each node's actual work happens via its OWN one-node dynamic workflow script (`write_workflow_script` + `execute_work_graph`), **not one big multi-node script for the whole graph**. This is a deliberate trade-off, not the "amortize orchestration cost across many nodes in one `claude -p` call" pattern 03-C §3.5 originally motivated `execute_work_graph` with — REQ-06's breakpoint-recovery requirement (resume after a process restart without re-running already-`done` nodes) is only achievable with per-node, Python-level persisted state, which is fundamentally incompatible with wrapping the WHOLE graph in one opaque `claude` subprocess call Python has no visibility into mid-flight.
  17 +
  18 +再看 `run_graph()` 的实际实现(`work_graph_orchestrate.py:196-270`):
  19 +
  20 +```python
  21 +ordered = topological_order(graph_manifest) # Kahn 算法,拒绝有环图
  22 +outputs = {}
  23 +for node in ordered: # 纯 Python 顺序 for 循环
  24 + ...
  25 + run_fn = _make_run_fn(...) # 每个节点单独渲染成一个"单节点"JS 脚本
  26 + output = work_node_run.run_work_node(...) # 一次一个节点地跑,串行
  27 + outputs[node_id] = output
  28 +```
  29 +
  30 +也就是说:
  31 +
  32 +- **graph_manifest 里没有依赖关系的两个节点(本该 fan-out 并行)也是一个跑完再跑下一个**,没有任何线程池/并发调度。14 步版第 5 步 `parallel()` 描述的"N 个独立节点同时起飞"在 graphlemon 现在的执行引擎里完全没有发生。
  33 +- 每个节点被单独渲染成一个**只有一次 `agent()` 调用的"单节点"JS 脚本**(`_render_node_script`),再通过 `write_workflow_script` + `execute_work_graph`(一次独立的 `claude -p "Run /<script>"` 子进程)跑掉。Claude Code 原生动态工作流引擎在这里只是被当作"带 schema 校验的单次 agent 调用"的壳,14 步版真正的价值——同一个脚本里 `parallel()`/`pipeline()` 编排一整批子智能体、编排本身零 token——完全没被用上。
  34 +- 原因写得很诚实:**这是为了换取 REQ-06 的断点续跑能力**。如果把整个 Work Graph 塞进一次 `claude` 子进程调用(14 步版和 §10.1 原方案设想的做法),Python 侧在执行过程中对每个节点的完成状态完全不可见,进程一旦重启就没法"从已完成的节点之后继续",只能整图重跑。graphlemon 选择了牺牲并行度换取可恢复性。
  35 +
  36 +**对 §10.1 的修正**:架构文档 §10.1 说"Work Graph 内部的 fan-out/fan-in/loop/verifier 直接用 Claude Code 写的 JS 编排脚本执行"——这句话在真实代码里没有成立。真正发生的是:graphlemon 自己的 Python 代码(`work_graph_orchestrate.py`)承担了图的拓扑调度(DAG 排序、逐节点执行、状态持久化),Claude 原生动态工作流退化成了"给单个节点调用套一层 schema 校验壳"的工具,而不是图执行引擎本身。这个决策本身未必是错的(断点续跑对 7×24 黑灯工厂是硬需求,14 步版也没有讨论进程重启这个场景),但它意味着 14 步版里跟"图内部拓扑"相关的第 5/6/7/11/12/13 条技法,在 graphlemon 目前的 Work Graph 执行层里基本都不适用或未生效,详见下表。
  37 +
  38 +## 1. 逐条对照表
  39 +
  40 +| # | 14 步版技法 | graphlemon 现状 | 依据 |
  41 +|---|---|---|---|
  42 +| 1 | 节点是工作,边是数据流转 | ✅ 概念对应到位:`build_prompt(node, outputs)` 把已完成节点的 `output_payload` 显式传给依赖它的下游节点提示词,拓扑序保证依赖已就绪 | `work_graph_orchestrate.py::run_graph` |
  43 +| 2 | 线性脚本是退化的图 | 🔶 反例现状:目前观察到的真实 Planner 运行(如任务 `43dc7333`)里 `graph_manifest` 就是 `n_brd → n_prd → n_sdd → n_benchmark` 的纯单链,架构文档 §6.1 设想的 PRD/SDD/测试用例 fan-out 候选点没有被 Planning 阶段实际生成——目前跑出来的就是 14 步版说的"退化的图" | 本会话早前查询该任务的 `work_graph_runs[0].graph_manifest` |
  44 +| 3 | 给每个节点一个契约(schema 强制) | ✅ 已实现:每个 Work Node 都有 `input_schema_ref`/`output_schema_ref`,`execute_work_graph` 通过 `--json-schema` 让 Claude Code 在工具调用层强制校验单节点输出 | `work_graph_orchestrate.py::_ensure_node_row`;`work_graph_execute.py::execute_work_graph` |
  45 +| 4 | 边是数据契约,简单聚合应零 token | ⚪ 不适用:由于没有真正的 fan-out,也就没有需要"聚合"的多分支结果——这条原则目前在 graphlemon 里没有用武之地 | 同上(无 fan-in 场景) |
  46 +| 5 | `parallel()` 扇出 | ❌ 未实现:见第 0 节,`run_graph` 是纯顺序 for 循环,即使 `graph_manifest` 里两个节点互不依赖也不会并发执行 | `work_graph_orchestrate.py:248` |
  47 +| 6 | `parallel()` 是屏障,仅在需要全量结果时用 | ⚪ 不适用:没有并行分支,自然没有"要不要设屏障"的选择 | 同上 |
  48 +| 7 | 菱形拓扑:split→work→merge | 🔶 仅存在于架构文档的设计意图(§6.1 fan-out 候选点、§6.2 Coder 模块级并行),未见于当前执行引擎产出的真实 `graph_manifest` | 架构文档 §6.1/§6.2 vs 实际 `run_graph` 实现 |
  49 +| 8 | 路由节点:智能体分类,代码选边 | ✅ 但只在 **Org Graph 层**成立:Validator 打 `route: fast_lane\|full_replan`,`org_loop_route.py` 用 `_NEW_STATUS`/`_NEW_ORG_NODE` 查表纯代码路由。**Work Graph 内部**(单次 `run_graph` 调用里)没有条件分支能力——`topological_order` 是静态 DAG,不支持运行时按结果选边 | `org_loop_route.py`;`work_graph_orchestrate.py::topological_order` |
  50 +| 9 | Verifier 三种打法(对抗式/多视角/评审团) | 🔶 部分实现:`root_cause_trace.judge_root_cause` 是"多视角差异化校验"的实现(正确性/需求完整性/Skill 缺陷三视角,≥2/3 一致才定案),但只用在 Validator 的 bug 根因溯源场景。`run_graph` 自身的 `verify_fn` 是单一验证器 + 失败重试一次,不是多视角/对抗式/评审团模式 | `root_cause_trace.py`;`work_graph_orchestrate.py::_verify_and_maybe_regenerate` |
  51 +| 10 | worktree 节点隔离 | ✅ 已实现,且比 14 步版描述的更严格:self-target 场景无条件强制 worktree(不只是"并行写入时才需要") | `work_node_run.py`;CLAUDE.md「Isolation」 |
  52 +| 11 | 循环收敛:loop-until-dry | ❌ 未实现,且**结构上不可能在单次 `run_graph` 内实现**:`topological_order` 对有环的 `graph_manifest` 直接抛异常("graph_manifest has a dependency cycle")——graphlemon 的 Work Graph 被设计成严格 DAG,不支持图内回路。收敛/重试目前只发生在两个更粗的粒度:Org-level Loop(固定 3 轮上限,跨 Org Graph)和 `verify_fn` 的单节点重试一次 | `work_graph_orchestrate.py::topological_order`;`docs/sdd/04-P-流程与状态流转.md` |
  53 +| 12 | 按节点分层选模型 | 🔶 管线已打通但未接线:`_render_node_script` 确实支持往生成的 JS 脚本里塞 `model` 选项,`run_fn(cwd, model=None)` 参数也在,**但没有任何调用方真正传入非 None 的 model**——`graph_manifest` 的节点结构里也没有 `model` 字段。基础设施就位,缺的是"谁来决定每个节点该用哪个模型"这层策略 | `work_graph_orchestrate.py::_render_node_script`/`_make_run_fn`;`agent_profile.py`(Org Agent 固定 `claude-sonnet-5`) |
  54 +| 13 | 拓扑即成本延迟(pipeline 优先于 parallel) | ⚪ 不适用:这条讨论的是"该用 `parallel()` 还是 `pipeline()`",但 graphlemon 现在两个都没用上,是第三种情况——Python 层顺序调度,不存在这个选择 | 同第 0 节 |
  55 +| 14 | 让 Claude 自己画图(self-routing / 动态工作流) | 🔶 只在"规划"层成立,"执行"层不成立:`plan_work_graph()` 确实是让 Claude 动态生成本次任务专属的 `graph_manifest`(节点+依赖,"画图"这件事本身是 Claude 做的),但落地执行时**不是**交给 Claude 自己写的 `agent()`/`parallel()`/`pipeline()` 编排脚本去跑——是 graphlemon 自己的 Python `run_graph()` 拿着这份"图纸"逐节点手动施工 | `work_graph_plan.py::plan_work_graph`(画图)vs `work_graph_orchestrate.py::run_graph`(施工) |
  56 +
  57 +## 2. 这次对照带来的认知更新
  58 +
  59 +- **上一份对照笔记(5 步版)里"Work Graph 层直接用 Claude Code 原生动态工作流,直接拿到零 token 编排"这句话需要修正**:这是 §10.1 的设计意图和已验证的技术可行性(spike 确认了单次 `agent()`/`pipeline()` 脚本能被 `claude -p` 非交互调用),但**生产代码选择了不这么用**,原因是断点续跑的硬需求和"整图塞进一次不透明子进程调用"根本冲突。这不是疏漏,`work_graph_orchestrate.py` 的 docstring 把权衡写得很清楚,但确实是本轮对照之前没有校准准确的一点。
  60 +- **"退化的图"不是历史遗留,是当前生产状态**:14 步版第 2 步讲的"你的线性脚本已经是一张图了,只是最惨的那种单链"——这句话现在精确描述的是 graphlemon 真实跑出来的 Planner Work Graph(BRD→PRD→SDD→Benchmark 纯串行),而不是一个已经被超越的反面教材。架构文档 §6.1 设想的并行候选点目前停留在文档层面。
  61 +- **loop-until-dry 不是"优先级不够高还没做",而是当前 DAG 执行模型结构性不支持**:之前那份对照笔记(5 步版)把它归类为"已知简化项/未来优化候选",这次深入代码后发现更准确的说法是——要支持它,`work_graph_orchestrate.py` 得先支持图内有环(`topological_order` 目前直接拒绝),这是比"加一个循环判断逻辑"大得多的变更,涉及整个执行模型。
  62 +
  63 +## 3. 待讨论的开放问题(供下一轮会话展开)
  64 +
  65 +- [ ] 现在的"每节点一次独立 `claude -p` 子进程"设计,断点续跑收益是否值得放弃 fan-out 并行?如果要两者兼得,是否可以在 Python 层(而非 Claude JS 层)用线程池并行执行 `graph_manifest` 里同一拓扑层级(同一批无依赖节点)的多个节点,同时仍然保留逐节点的持久化状态?这样能同时保住 REQ-06 和 14 步版的并行收益,不需要依赖 Claude 的 `parallel()`。
  66 +- [ ] Planning Skill(`plan_work_graph` 的 prompt,`work_graph_plan.py:67` 附近)目前只要求"一个 artifact_type 一个节点",没有引导 Claude 去识别哪些节点其实互不依赖、可以并行——要不要把 14 步版第 1/2 步"对每个箭头问是否真的有数据流动"这个提问显式加进 Planning Skill 的提示词,让它在生成 `graph_manifest` 阶段就标注可并行的节点组,即使执行层暂时还是串行,至少数据模型上先把"这两个节点无依赖"的信息留出来?
  67 +- [ ] `_render_node_script` 已经预留了 `model` 参数但没人用——按节点选模型这层策略该由谁决定?是 Planning Skill 在生成 `graph_manifest` 时就给每个节点标注推荐模型,还是在 `run_graph` 里按 `org_node`/`artifact_type` 写一张静态映射表?
  68 +- [ ] `root_cause_trace` 的三视角评审团目前只服务于 Validator 的 bug 归因,`run_graph` 的 `verify_fn` 是单一验证器——要不要把三视角评审团模式也下沉到 `verify_fn` 里,作为高风险节点(比如 SDD、代码变更)的可选验证强度?
... ...
docs/reference/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md 0 → 100644
  1 +# Graph Engineering with Claude: 14\-Step roadmap from 0 to graph architect \(Full Course\)
  2 +
  3 +
  4 +
  5 +img\_placeholder
  6 +
  7 +
  8 +
  9 +大多数人尝试构建多步智能体时,最终得到的都是一条直线。第一步、第二步、第三步——每一步都乖乖地等上一步做完才开始。
  10 +
  11 +Most people who try to build a multi\-step agent end up with a straight line\. Step one, step two, step three \- each waiting politely for the last to finish before it starts\.
  12 +
  13 +
  14 +
  15 +十分之九的人会发现,其中一半的步骤根本不需要等待。
  16 +
  17 +**9/10 notice that half those steps never needed to wait at all\.**
  18 +
  19 +
  20 +
  21 +它们不路由,不分支,不并行。它们只是排队——一个脑袋,一个上下文,一次只做一件事——直到窗口被填满,智能体忘记自己在做什么。
  22 +
  23 +They don’t **route\.** They don’t **branch\.** They don’t **parallelize\.** They just queue \- one head, one context, one thing at a time, until the window fills up and the agent forgets what it was doing\.
  24 +
  25 +
  26 +
  27 +> Follow my Substack to get fresh AI alpha:
  28 +>
  29 +> [movez\.substack\.com](https://movez.substack.com/)
  30 +>
  31 +>
  32 +>
  33 +>
  34 +
  35 +这套十四步执行路线图,能把单文件单行式简单任务拆解成完整任务图谱:该图谱可分发至海量智能体集群并行运算、自主校验全部检索结果,最终收敛输出一套单智能体完全无法承载处理规模的完整结论。
  36 +
  37 +***This is the 14\-step roadmap*** that turns that single\-file line into a **graph**: one that fans out across a fleet, verifies its own findings, and converges on a result a lone agent could never hold\.
  38 +
  39 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=NjNkMGVhZTU5ZGM4MmQzMDA1ZTQ4NDIyMWUxZGJmYmJfYzEzMDlhYWUxNDFhNTliYjBiNGJkZmY3NzUyNzFkNmJfSUQ6NzY2NDkxMjQwNTYwNDgxNDAyM18xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  40 +
  41 +
  42 +
  43 +这里有一个没人明说的转变:prompt提示词是一句话,loop循环是一个周期,harness框架是智能体脚下的地板。
  44 +
  45 +Here’s the shift nobody spells out\. A prompt is a sentence\. A loop is a cycle\. A harness is the floor the agent stands on\.
  46 +
  47 +
  48 +
  49 +但工作本身的形状——什么在什么之前运行、什么可以同时运行、什么必须等待其他所有东西——这个形状就是一个图。节点负责思考,边负责传递结果。
  50 +
  51 +But the **shape of the work itself** \- what runs before what, what can run at the same time, what has to wait for everything else \- that shape is a graph\. Nodes do the thinking\. Edges carry the results\.
  52 +
  53 +
  54 +
  55 +Claude Code 已经内置了直接构建这些图的工具:动态工作流。
  56 +
  57 +Claude Code shipped the tooling to build these graphs directly: **dynamic workflows**\.
  58 +
  59 +
  60 +
  61 +Claude 编写一个普通的 JavaScript 编排脚本,然后生成一个协调的子智能体舰队来执行它——而协调本身不消耗任何模型 token,因为它是代码,不是对话。
  62 +
  63 +Claude writes a plain JavaScript orchestration script, then spawns a coordinated fleet of subagents to execute it \- and the coordination itself costs zero model tokens, because it’s code, not a conversation\.
  64 +
  65 +
  66 +
  67 +
  68 +
  69 +## Nodes are jobs\. Edges are what *flows\.*
  70 +
  71 +\#\# 节点代表工作,边代表流转关系
  72 +
  73 +
  74 +
  75 +一个图只有两样东西(节点\+边 Nodes\+Edges),把它们搞清楚就能解决大部分困惑。
  76 +
  77 +**节点**是一个工作单元——一个智能体、一个有界任务、一个输入一个输出。
  78 +
  79 +A graph has exactly two things, and getting them straight fixes most of the confusion\.
  80 +
  81 +A **node** is a unit of work \- one agent, one bounded job, one input in and one output out\.
  82 +
  83 +
  84 +
  85 +**边**是一个依赖关系:含义是这个节点的输出作为另一个节点的输入。仅此而已。
  86 +
  87 +An **edge** is a dependency: it says *this node’s output feeds that node’s input\.* Nothing more\.
  88 +
  89 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=Y2RkMTFlMzlmNTkwZjcxMDQ2Y2RiOGU5YWE4ZGQ0YTFfYjI5MmQ5ZmNjMGJkYzU4NDU2YjAwZTZkNmU0NDUxZDZfSUQ6NzY2NDkxMzQ2MDEzMzM2Njc0MV8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  90 +
  91 +常见的错误是把「然后」当作边。「总结文件然后告诉我天气」——这两者之间没有边,天气不消费摘要的数据。
  92 +
  93 +The mistake is treating “and then” as an edge\. “Summarize the file *and then* tell me the weather” has no edge between the two \- the weather doesn’t consume the summary\.
  94 +
  95 +
  96 +
  97 +那是两个不相连的节点,被线性脚本不必要地串联在一起。边只存在于数据真正跨越它的时候。
  98 +
  99 +That’s two disconnected nodes that a linear script needlessly chains\. The edge only exists when data actually moves across it\.
  100 +
  101 +
  102 +
  103 +学会对你的智能体中每个「然后」提问:下一步是否读取上一步的输出?如果不是,就没有边,等待是浪费的。
  104 +
  105 +Learn to ask, for every “and then” in your agent: **does the next step read the last step’s output?** If not, there is no edge, and the wait is wasted\.
  106 +
  107 +
  108 +
  109 +python
  110 +
  111 +1\. 用方框和箭头画出来。方框是一个 agent\(\) 调用,箭头是一个变量从一个调用的返回值传到另一个的提示词里。如果你画不出箭头——如果没有变量跨越——两个方框就是独立的,而独立性是你在本课程剩余部分要利用的东西。
  112 +
  113 +```Python
  114 +Draw it as boxes and arrows. A box is an agent() call.
  115 +An arrow is a variable passed from one call’s return into another’s
  116 +prompt. If you can’t draw the arrow - if no variable crosses - the two
  117 +boxes are independent, and independence is the thing you’ll exploit
  118 +for the rest of this course.
  119 +```
  120 +
  121 +
  122 +
  123 +
  124 +
  125 +## Your linear script is a *degenerate* graph
  126 +
  127 +\#\# 你的线性脚本是一个退化的图
  128 +
  129 +
  130 +
  131 +当你把智能体写成「先做A,再做B,再做C,再做D」时,你已经画了一个图——一条不分叉的单链。每个节点恰好有一条边进、一条边出。
  132 +
  133 +When you write an agent as “do A, then B, then C, then D,” you’ve drawn a graph \- a single unbranching chain\. Every node has exactly one edge in and one edge out\.
  134 +
  135 +
  136 +
  137 +它能正确运行,但也运行得又慢又脆,因为链条没有冗余:如果C卡住了,D永远不会执行,A的工作被困在上游无处可去。
  138 +
  139 +It runs correctly\. It also runs slowly and fragile, because a chain has no redundancy: if C stalls, D never happens, and A’s work is trapped upstream with nowhere to go\.
  140 +
  141 +
  142 +
  143 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MDdhNDcyOGVkNzZjMjI1NTUyNmNmMzg1NWVhYzJhNTVfNDJjNDNmYmU3ZDJlYzUxYjQwMTRmYjQ3Yjk1ODAzMmFfSUQ6NzY2NDkwOTcxODAyNDQ0MDc2OF8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  144 +
  145 +图工程的第一项真正技能是重画这条链。拿起你的线性智能体,对每条箭头问第一步的问题。
  146 +
  147 +The first real skill of graph engineering is **redrawing the chain**\. Take your linear agent and, for each arrow, ask the Step 1 question\.
  148 +
  149 +
  150 +
  151 +大多数链条有两三条不携带数据的箭头——它们只是你碰巧输入东西的顺序。
  152 +
  153 +Most chains have two or three arrows that don’t carry data \- they’re just the order you happened to type things in\.
  154 +
  155 +
  156 +
  157 +剪掉这些箭头,链条就坍缩成更宽的东西:几个可以同时运行的独立节点,喂给一个需要它们全部的单一节点。
  158 +
  159 +Cut those arrows and the chain collapses into something wider: a few independent nodes that could all run at once, feeding a single node that needs them all\.
  160 +
  161 +
  162 +
  163 +
  164 +
  165 +## Give every node a *contract*
  166 +
  167 +\#\# 给每个节点一个契约
  168 +
  169 +一个你无法推理的节点就是一个你无法并行的节点。解决方案是契约:有界输入、有界输出、恰好一个任务。
  170 +
  171 +A node you can’t reason about is a node you can’t parallelize\. The fix is a contract: **bounded input, bounded output, exactly one job\.**
  172 +
  173 +
  174 +
  175 +输入是节点读取的任何东西——显式传入,永远不从共享窗口假设。输出是一个定义好的形状,最好经过验证,这样下一个节点可以无猜测地消费它。
  176 +
  177 +The input is whatever the node reads \- passed in explicitly, never assumed from a shared window\. The output is a defined shape, ideally validated, so the next node can consume it without guessing\.
  178 +
  179 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=OTdjODczZTBmNDQzYmY0N2FkODFkMjAwNDhiYmI2MTdfNmJmZTVhZWZiMjViNjQxMDYyZWQ0MzUxMDMxYWYwYjdfSUQ6NzY2NDkwOTgwMTkzNTExMzQxOV8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  180 +
  181 +在工作流中,这个契约用 schema 强制执行。当你给 Claude 一个带 JSON schema 的 agent\(\) 调用时,Claude 生成的子智能体被迫返回验证过的结构化数据——验证发生在工具调用层,所以 Claude 在不匹配时重试,而不是给你一段需要解析和祈祷的自由文本。
  182 +
  183 +In a workflow this contract is enforced with a **schema**\. When you hand Claude an agent\(\) call with a JSON schema, the subagent Claude spawns is forced to return validated structured data \- validation happens at the tool\-call layer, so Claude retries on mismatch instead of handing you free text you have to parse and pray over\.
  184 +
  185 +
  186 +
  187 +这就是一个 Claude 能接入图的节点和一个只有人类读输出才能工作的节点之间的区别。
  188 +
  189 +This is the difference between a node Claude can wire into a graph and a node that only works when a human reads its output\.
  190 +
  191 +
  192 +
  193 +python
  194 +
  195 +// 真正有契约的节点:有界输入,验证输出,单一任务。
  196 +
  197 +```Python
  198 +// A node with a real contract: bounded in, validated out, one job.
  199 +const ITEM = {
  200 + type: 'object', additionalProperties: false,
  201 + properties: {
  202 + title: { type: 'string' },
  203 + url: { type: 'string' },
  204 + impact: { type: 'string', enum: ['high', 'medium', 'low'] },
  205 + },
  206 + required: ['title', 'url', 'impact'],
  207 +};
  208 +const result = await agent(source.prompt, {
  209 + label: `research:${source.key}`,
  210 + schema: ITEM, // forces validated structured output
  211 + agentType: 'general-purpose',
  212 +});
  213 +// result is now a shape the next node can trust — not free text.
  214 +```
  215 +
  216 +
  217 +
  218 +
  219 +
  220 +## 04\. Treat the edge as a *data contract*
  221 +
  222 +\#\# 04\. 把边当作数据契约
  223 +
  224 +
  225 +
  226 +边不只是简单表示 “任务 A 执行完再跑任务 B”,它更是一份数据传输约定:A 输出固定格式的数据结构,而 B 的设计本身就适配读取该结构。如果以传输的数据(而非执行先后顺序)来命名边,两件事会变得清晰易懂。
  227 +
  228 +An edge isn’t just “B comes after A\.” It’s a promise about what crosses: A produces *this shape*, and B is built to consume *this shape\.* When you name the edge by its data \- not its order \- two things get easier\.
  229 +
  230 +
  231 +
  232 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MTQ1ZjRmZGZkZGY3OWJhMTY2MzFhMzdmNTM2ZTIzMzVfNzk0ZWVmZmUzYTg1MDZhYmQ4ODc2ZDJjNjdiNzU4ZDJfSUQ6NzY2NDkxNzIzMzc1NjA2NDcyNF8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  233 +
  234 +你可以一眼看出边是否真实(数据真的在移动吗?),而且只要形状保持不变,你可以替换任一端的节点而不破坏图。
  235 +
  236 +You can see instantly whether the edge is real \(does data actually move?\), and you can swap the node on either end without breaking the graph, as long as the shape holds\.
  237 +
  238 +
  239 +
  240 +实际实现中,边由原生 JavaScript 编写。在任务分发与结果汇总之间的归约步骤(扁平化、去重、过滤),本质就是一段处理各节点输出数据结构的代码。
  241 +
  242 +In practice, the edge lives in plain JavaScript\. The reduce step between fan\-out and synthesis \- flatten, dedupe, filter \- is just code operating on the shapes your nodes returned\.
  243 +
  244 +
  245 +
  246 +全程无需智能体参与。基于图谱架构设计有一项不易察觉的显著优势:大量原本需要消耗大模型算力令牌处理的逻辑,本质只是一条数据流边,而数据流边完全零算力开销。
  247 +
  248 +**No agent needed\.** One of the quiet wins of graph thinking: a huge amount of what people burn model tokens on is really an edge, and edges are free\.
  249 +
  250 +
  251 +
  252 +python
  253 +
  254 +人们很容易下意识新建智能体来 “合并多份结果”,但一定要避免这种做法。 如果合并操作仅需扁平化、去重,直接用 `results.flatMap(...)` 搭配集合(Set)即可;运算结果确定、瞬时完成,完全不消耗模型 Token。 智能体应当留给需要主观判断的场景,而非单纯的数据流转处理。 倘若图谱里每条数据边都依靠智能体中转,相当于为自身的基础数据流链路持续付出不必要的算力成本。
  255 +
  256 +```Python
  257 +The temptation is to spawn an agent to “combine the results.” Resist
  258 +it. If combining means flatten-and-dedupe, that’s results.flatMap(...)
  259 +and a Set — deterministic, instant, zero tokens. Save agents for
  260 +judgment, not for plumbing. A graph where every edge is an agent is a
  261 +graph paying rent on its own wiring.
  262 +```
  263 +
  264 +
  265 +
  266 +
  267 +
  268 +## Fan out with *parallel\(\)*
  269 +
  270 +\#\# 用 parallel\(\) 实现任务并行分发
  271 +
  272 +
  273 +
  274 +这是能大幅压缩整体资源开销的核心设计思路。当存在 N 个相互独立节点(例如 N 份待核查数据源、N 个待评审文件、N 条待审计路由)时,切勿采用串行链式执行方案。
  275 +
  276 +This is the move that pays for everything\. When you have N independent nodes \- N sources to check, N files to review, N routes to audit \- you don’t chain them\.
  277 +
  278 +
  279 +
  280 +你告诉 Claude 把它们fan\-out(任务分解分发)并同时运行。在工作流中这就是 parallel\(\):Claude 接收一个延迟执行函数 thunk 数组,为每个 thunk 单独生成一个子智能体,全部并发执行,然后把结果数组返回给你。
  281 +
  282 +You tell Claude to fan them out and run them at once\. In a workflow that’s parallel\(\): Claude takes an array of thunks and spawns one subagent per thunk, all executing concurrently, then hands you back the array of results\.
  283 +
  284 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=Y2NlMjdmZWI0NWU3NTIwMDc2NDAxNGYyOWNkODFlMDJfOWM4NjY3MzY5NzdkMGU3NzI3ZWY3NDA0MjcyMzI1YmVfSUQ6NzY2NDkyMTcxMDI1Mjc4ODkxNl8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  285 +
  286 +两个细节让它稳健。第一,parallel\(\) 是一个屏障——它等所有 thunk 完成才返回,所以下一阶段看到的是完整集合。第二,抛异常的 thunk 解析为 null 而不是拒绝整个批次,所以一个不稳定的智能体不会搞砸整个运行。
  287 +
  288 +Two details make it robust\. First, parallel\(\) is a **barrier** \- it waits for every thunk before it returns, so the next stage sees the complete set\. Second, a thunk that throws resolves to null instead of rejecting the whole batch, so one flaky agent can’t sink the run\.
  289 +
  290 +
  291 +
  292 +总是对结果做 \.filter\(Boolean\)。并发量受限于你的核心数,多余的会排队,所以你可以传一百个 thunk 它们都会完成——只是每次跑一小批。
  293 +
  294 +Always \.filter\(Boolean\) the results\. Concurrency is capped around your core count and the excess queues, so you can pass a hundred thunks and they’ll all finish \- just a handful at a time\.
  295 +
  296 +
  297 +
  298 +python
  299 +
  300 +1\. phase\('Research'\);
  301 +// 九个来源,九个智能体,全部同时。
  302 +const raw = await parallel\(
  303 + SOURCES\.map\(\(s\) =\> \(\) =\>
  304 + agent\(s\.prompt, \{
  305 + label: \`research:$\{s\.key\}\`,
  306 + phase: 'Research',
  307 + schema: ITEM\_SCHEMA, // 每个节点返回验证过的 JSON
  308 + agentType: 'general\-purpose',
  309 + \}\),
  310 + \),
  311 +\);
  312 +const collected = raw\.filter\(Boolean\); // 丢掉失败智能体的 null
  313 +
  314 +```Python
  315 +phase('Research');
  316 +// Nine sources, nine agents, all at once.
  317 +const raw = await parallel(
  318 + SOURCES.map((s) => () =>
  319 + agent(s.prompt, {
  320 + label: `research:${s.key}`,
  321 + phase: 'Research',
  322 + schema: ITEM_SCHEMA, // each node returns validated JSON
  323 + agentType: 'general-purpose',
  324 + }),
  325 + ),
  326 +);
  327 +const collected = raw.filter(Boolean); // drop the nulls from failed agents
  328 +```
  329 +
  330 +fan\-out(分发并行执行)存在于 Claude 写的代码中,不在模型对话中。Claude 自己的上下文从不同时持有九个来源——每个子智能体携带自己的上下文,只有最终答案回来。
  331 +
  332 +The fan\-out lives in code Claude wrote, not in a model conversation\. Claude’s own context never holds nine sources at once \- each subagent carries its own, and only the final answer comes back\.
  333 +
  334 +
  335 +
  336 +这就是让 Claude 把工作流扩展到几十或几百个子智能体而不淹没会话的原因。编排层不消耗 token,因为它不是 Claude 思考的另一轮。
  337 +
  338 +That’s what lets Claude scale a workflow to **dozens or hundreds of subagents** without drowning the session\. The orchestration layer costs zero tokens because it isn’t another turn of Claude thinking\.
  339 +
  340 +
  341 +
  342 +
  343 +
  344 +## Fan in at a *barrier*
  345 +
  346 +\#\# 基于屏障节点完成结果扇入聚合
  347 +
  348 +Fan\-in :结果聚合,汇总归约
  349 +
  350 +
  351 +
  352 +fan\-out只有在有东西收集它时才有用。fin\-in是边汇聚的节点——一个智能体(或一段代码)同时看到所有上游结果,并做需要整个集合才能做的事:跨来源去重、按影响排序、总量为空时提前退出。这是屏障值得其墙钟成本的唯一地方。
  353 +
  354 +A fan\-out is only useful if something gathers it\. The fan\-in is the node where edges converge \- where one agent \(or one piece of code\) sees *all* the upstream results at once and does something that requires the whole set: dedupe across sources, rank by impact, early\-exit if the total came back empty\. This is the one place a barrier earns its wall\-clock cost\.
  355 +
  356 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=YTcyOWVhNTQ4NDZmZmFmMjAwMjE2ZmYyMzNmNWQxNTRfNThjYzBjYzY3YWM5ODNmYzNhNjQzMDdhNGY1NmUzOWVfSUQ6NzY2NDkyMTcyNDIxNjk3MDQyNV8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  357 +
  358 +保障任务图谱高效运行的核心准则:
  359 +
  360 +仅当某一阶段确实需要集齐所有前置任务结果时,才使用屏障同步机制。需要对全部数据源统一去重?那就启用屏障 —— 这么做是合理的。
  361 +
  362 +The rule that keeps graphs fast: **use a barrier only when a stage genuinely needs every prior result together\.** Deduping across all sources? Barrier \- correct\.
  363 +
  364 +
  365 +
  366 +python
  367 +
  368 +1\. // 边:纯 JS,无智能体,零 token。
  369 +const flat = collected\.flatMap\(\(c\) =\> c\.items\);
  370 +log\(\`Collected $\{flat\.length\} items\`\);
  371 +phase\('Curate'\);
  372 +// 屏障节点:需要整个集合来去重\+排序。
  373 +const curated = await agent\(
  374 + \`Dedupe and rank these by impact:\\n$\{JSON\.stringify\(flat\)\}\`,
  375 + \{ phase: 'Curate', schema: CURATED\_SCHEMA \},
  376 +\);
  377 +
  378 +```Python
  379 +// The edge: plain JS, no agent, zero tokens.
  380 +const flat = collected.flatMap((c) => c.items);
  381 +log(`Collected ${flat.length} items`);
  382 +phase('Curate');
  383 +// The barrier node: needs the WHOLE set to dedupe + rank.
  384 +const curated = await agent(
  385 + `Dedupe and rank these by impact:\n${JSON.stringify(flat)}`,
  386 + { phase: 'Curate', schema: CURATED_SCHEMA },
  387 +);
  388 +```
  389 +
  390 +
  391 +
  392 +如果只是单纯对列表做扁平化处理?直接依托数据流边内联处理即可。 有一套直白严苛的判断标准:倘若你的代码写成「并行分发 → 数据转换 → 再次并行分发」,且中间转换步骤不存在多条数据之间的交叉依赖,那本该改用流式管道处理,完全省去屏障同步。
  393 +
  394 +Just flattening a list? That’s an edge, do it inline\. The smell test is brutal and simple: if you wrote parallel → transform → parallel, and that middle transform has no cross\-item dependency, you should have used a pipeline and skipped the barrier entirely\.
  395 +
  396 +
  397 +
  398 +
  399 +
  400 +## The *diamond*: split → work → merge
  401 +
  402 +\#\# 菱形执行模型:拆分任务 → 并行处理 → 汇总合并
  403 +
  404 +
  405 +
  406 +把fan\-out和fan\-in放在一起,你就得到了每个严肃智能体图的主力拓扑:菱形执行模型。
  407 +
  408 +Put fan\-out and fan\-in together and you get the workhorse topology of every serious agent graph: the **diamond**\.
  409 +
  410 +
  411 +
  412 +单个节点负责任务拆分,大量节点并行执行处理,再由单个节点汇总合并。市场信息巡检、依赖项审计、代码评审、深度调研报告等场景底层都采用这套拓扑结构;只需替换数据源与提示词,同一套基础框架就能适配各类业务需求。
  413 +
  414 +
  415 +
  416 +One node splits the job, many nodes do the work in parallel, one node merges\. It’s the shape behind a market scan, a dependency audit, a code review, a research report \- swap the sources and prompts and the same skeleton adapts\.
  417 +
  418 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MDdhZWY2NDg5NWE0YWFjYzdjYTNmODM3MmMxNjI1NjNfNDhhYjM4NTViMjBmNjUzNmUxOTc1NWRmMDQwZDM2ZmZfSUQ6NzY2NDkxMDIxNzY5NjcwOTU3MF8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  419 +
  420 +这套标准范式有个值得牢记的命名:扇出分发 → 归约处理 → 汇总生成。 扇出阶段拓宽信息覆盖范围;依靠原生代码做归约,精简海量原始数据;最后交由终端智能体整合输出完整结论。
  421 +
  422 +The canonical form has a name worth memorizing: **fan out → reduce → synthesize\.** Fan out to gather breadth, reduce with plain code to compress it, synthesize with a final agent to write the answer\.
  423 +
  424 +
  425 +
  426 +一旦你看到了菱形,你就不再问「如何让我的智能体做更多步骤」,而是开始问「拆分在哪里,合并在哪里」——这才是真正能扩展的问题。
  427 +
  428 +Once you see the diamond, you stop asking “how do I make my agent do more steps” and start asking “where’s the split, where’s the merge” \- which is the question that actually scales\.
  429 +
  430 +
  431 +
  432 +
  433 +
  434 +## Route the edge at runtime with a *conditional*
  435 +
  436 +\#\# 通过条件分支在运行时动态路由数据流边
  437 +
  438 +
  439 +
  440 +并非所有任务图谱都是静态固定的。有时要走哪条数据流边,取决于上游节点的输出结果。路由节点会解析返回数据,判定启用哪一条下游执行分支:比如先对工单分类,再分流至对应的处理程序;或是先判断代码变更体量,再选择快速简易评审,或是启动完整全量审计。
  441 +
  442 +Not every graph is fixed\. Sometimes the edge to take depends on what a node found\. A **router node** inspects a result and decides which downstream path fires \- classify the ticket, then branch to the right handler; check the diff size, then either do a quick review or spin up a full audit\.
  443 +
  444 +
  445 +
  446 +在工作流中这只是对节点验证输出的 JavaScript if 或 switch,因为控制流存在于代码中。
  447 +
  448 +In a workflow this is just a JavaScript if or switch on a node’s validated output, because control flow lives in code\.
  449 +
  450 +
  451 +
  452 +这里确定性成了特性而非限制。路由器的决策可以是 Claude 驱动的(子智能体分类),但路由是 Claude 写的代码——所以对相同分类每次都以相同方式运行。
  453 +
  454 +This is where determinism becomes a feature, not a limitation\. The router’s *decision* can be Claude\-powered \(a subagent classifies\), but the *routing* is code Claude wrote \- so it runs the same way every time for the same classification\.
  455 +
  456 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=OGFlMzNjYzFjYjRkNmM0OWY4MzMxMTYzZmNmYjAzMmNfMjM1MDljMzRiMGQxMjcyMGMxMTI5YjBkODFhYjJlZTRfSUQ6NzY2NDkxMDM3NTA3OTE4NTM4NF8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  457 +
  458 +
  459 +
  460 +节点环节依托 Claude 的大模型判断能力,数据流边环节依靠脚本代码稳定可靠。不会出现意外情况 —— 比如莫名其妙触发 “Claude 自行跳过审计流程”,因为跳过流程的逻辑必须显式定义在任务图谱中,而当前图谱并未配置该分支。
  461 +
  462 +You get Claude’s judgment at the node and the script’s reliability at the edge\. No emergent “Claude decided to skip the audit” surprises \- because the skip would have to be written into the graph, and it isn’t\.
  463 +
  464 +
  465 +
  466 +python
  467 +
  468 +1\. // 路由节点:智能体分类,代码选边。
  469 +const \{ severity \} = await agent\(
  470 + \`Classify this diff's risk:\\n$\{diff\}\`,
  471 + \{ schema: \{ type: 'object',
  472 + properties: \{ severity: \{ enum: \['low', 'high'\] \} \},
  473 + required: \['severity'\] \} \},
  474 +\);
  475 +let review;
  476 +if \(severity === 'high'\) \{
  477 + // 重路径:全面并行审计
  478 + review = await parallel\(FILES\.map\(\(f\) =\> \(\) =\> agent\(\`Audit $\{f\}\`\)\)\);
  479 +\} else \{
  480 + // 轻路径:一次快速审查
  481 + review = await agent\(\`Quick review of $\{diff\}\`\);
  482 +\}
  483 +
  484 +```Python
  485 +// Router node: an agent classifies, code picks the edge.
  486 +const { severity } = await agent(
  487 + `Classify this diff's risk:\n${diff}`,
  488 + { schema: { type: 'object',
  489 + properties: { severity: { enum: ['low', 'high'] } },
  490 + required: ['severity'] } },
  491 +);
  492 +let review;
  493 +if (severity === 'high') {
  494 + // heavy path: full parallel audit
  495 + review = await parallel(FILES.map((f) => () => agent(`Audit ${f}`)));
  496 +} else {
  497 + // light path: one quick pass
  498 + review = await agent(`Quick review of ${diff}`);
  499 +}
  500 +```
  501 +
  502 +
  503 +
  504 +
  505 +
  506 +## Put a *verifier* on the edge
  507 +
  508 +\#\# 在数据流边配置放验证器
  509 +
  510 +
  511 +
  512 +任务图谱真正的核心优势,不在于创建更多智能体,而在于你能搭建一套标准化流程框架包裹智能体,让输出结果具备可信赖度。
  513 +
  514 +The real leverage of a graph isn’t more agents \- it’s the structure you can wrap around them to produce confidence\.
  515 +
  516 +
  517 +
  518 +校验节点部署在数据流边上,数据放行至下游前会先经过它,它的唯一作用就是筛除无效检测结果。数据校验通过才可继续流转;一旦校验失败,该结果就不会参与最终结论的生成。
  519 +
  520 +A verifier node sits on the edge before a result is allowed downstream, and its only job is to try to *kill* the finding\. If it survives, it passes\. If not, it never reaches the answer\.
  521 +
  522 +
  523 +
  524 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=ZGExYmZlYTEzNWQ2Yjc2OTQ2ZTRjODMxYTYxMzRmMTFfMjUxMGEwNzdkMzlhNWI4YzlmNWQ2ZTk1YzBkYTUyMTdfSUQ6NzY2NDkxMDQ1MzUwNDU2MDA4MF8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  525 +
  526 +有三种模式值得掌握。
  527 +
  528 +Three patterns are worth having in your hands\.
  529 +
  530 +对抗式校验:针对每一条检测结论,启动 N 个独立质疑智能体,指令其逐条反驳该结论;仅当多数质疑校验均未推翻结论时,才保留这条结果。
  531 +
  532 +多视角差异化校验:为每个校验器分配专属评判维度 —— 逻辑正确性、安全风险、问题可复现性;多元视角能捕捉到批量相同校验无法发现的各类失效场景。
  533 +
  534 +评审专家组机制:从不同角度生成 N 套解决方案,并行交由多名评审打分;以最优方案为基底汇总输出,同时吸纳其他备选方案中的优质内容。
  535 +
  536 +- **Adversarial verify:** for each finding, spawn N independent skeptics prompted to refute it; keep it only if a majority survive\.
  537 +
  538 +- **Perspective\-diverse verify:** give each verifier a distinct lens \- correctness, security, does\-it\-reproduce \- because diversity catches failure modes that N identical checks never will\.
  539 +
  540 +- **Judge panel:** generate N attempts from different angles, score them with parallel judges, synthesize from the winner while grafting the best of the runners\-up\.
  541 +
  542 +这正是让一个真实团队移植 Bun 运行时并把对抗性代码审查内建到循环中的模式。
  543 +
  544 +This is exactly the pattern that let a real team port the Bun runtime with adversarial code review baked into the loop\.
  545 +
  546 +
  547 +
  548 +
  549 +
  550 +## Isolate nodes so one failure can’t *poison* the graph
  551 +
  552 +\#\# 节点隔离,避免单点故障污染整个任务图谱
  553 +
  554 +###
  555 +
  556 +串行链路中故障会连锁扩散:若节点 C 执行失败,节点 D 将无法启动,整条流程直接中断。而在任务图谱架构里,故障应当被限制在发生问题的单个节点内。
  557 +
  558 +In a chain, a failure cascades \- C dies, D never runs, the whole thing halts\. In a graph, failure should be **contained to its node\.**
  559 +
  560 +
  561 +
  562 +这一点其实已经部分实现:在 `parallel()` 内部,若某个延迟执行函数抛出异常,其结果会解析为 `null`;即便有一个子代理执行失败,其余八个正常的子代理仍能正常返回数据。调用 `.filter(Boolean)` 过滤空值,就是实现故障隔离的手段。
  563 +
  564 +That’s already partly true: a thunk that throws inside parallel\(\) resolves to null, so eight good agents still return while one bad one drops out\. Your \.filter\(Boolean\) is the containment\.
  565 +
  566 +
  567 +
  568 +设计每一处扇入聚合逻辑时,都要兼容缺失输入数据,不要默认所有上游结果都齐全。
  569 +
  570 +Design every fan\-in to tolerate missing inputs rather than assume a full set\.
  571 +
  572 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=NzJlZDk4MzQ3Y2Q4ZDE4OTBkMWYzY2I2ZDk0YWIyM2FfOGU0YjFmMjFmOTZiNjMwZGE2MTA0Yjc4MzY2N2U3YzdfSUQ6NzY2NDkxMDU0MjgwODg3ODM0OV8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  573 +
  574 +更微妙的失败是节点互相踩踏。当智能体并行写文件时,它们可能冲突。
  575 +
  576 +The subtler failure is nodes stepping on each other\. When agents write files in parallel, they can collide\.
  577 +
  578 +
  579 +
  580 +解决方案是隔离:worktree——每个智能体在自己的 git worktree 中运行,在沙箱中做工作,然后干净地合并。
  581 +
  582 +The fix is isolation: "worktree" \- each agent runs in its own git worktree, does its work in a sandbox, and merges cleanly\.
  583 +
  584 +
  585 +
  586 +仅当多个节点确实并行执行时,才启用屏障同步机制。屏障是专为并行拓扑配套的安全防护措施,并非所有流程运行都必须承担的固定性能开销。
  587 +
  588 +Reach for it only when nodes actually write in parallel\. it’s the seatbelt for the one topology that needs it, not a default tax on every run\.
  589 +
  590 +
  591 +
  592 +
  593 +
  594 +## Add a cycle \- but make it *converge*
  595 +
  596 +\#\# 加一个循环——但要让它收敛
  597 +
  598 +
  599 +
  600 +有时你直到开始才知道工作有多大:未知规模的探索、发现一个bug会暴露三个更多的bug扫描。这需要循环——一条回到更早节点的受控边。
  601 +
  602 +Sometimes you don’t know how big the job is until you’re in it: unknown\-size discovery, a bug sweep where finding one bug reveals three more\. That needs a **cycle** \- a controlled edge back to an earlier node\.
  603 +
  604 +
  605 +
  606 +危险很明显:不收敛的循环是无限循环,会生成智能体直到你的预算耗尽。
  607 +
  608 +The danger is obvious: a cycle that doesn’t converge is an infinite loop that spawns agents until your budget is gone\.
  609 +
  610 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=YzQzNTJmZWNmZjA0ZTk2MGM1YTMwN2FmZDE4ZThjZWVfY2ZkOTQwZDI3NjUyOTZiODcxYWEyNDQwMGQ2NDdhNDhfSUQ6NzY2NDkyMTkyNzk1Mzg0NTUxNV8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  611 +
  612 +这套收敛模式叫做「循环直至无新增结果」:持续启动检索探查节点,连续 K 轮都不再产出新内容时,才终止循环。决定该模式成败的关键细节 —— 也是绝大多数人初次实现都会踩的坑 —— 是去重比对所依据的基准对象
  613 +
  614 +The pattern that converges is **loop\-until\-dry:** keep spawning finders until K consecutive rounds surface nothing new, then stop\. The one detail that makes or breaks it \- and the mistake almost everyone makes the first time \- is what you dedupe against\.
  615 +
  616 +
  617 +
  618 +去重校验要和所有已检索到的条目做比对,不能只拿已确认通过的结果去重。否则被校验驳回的无效结论会每一轮重复出现,循环永远无法收敛;最终你的系统会持续消耗算力,反复挖掘完全相同的无效死胡同。
  619 +
  620 +Dedupe against everything *seen*, not just against confirmed results\. Otherwise rejected findings reappear every round, the loop never runs dry, and you’ve built a machine that pays to rediscover the same dead ends forever\.
  621 +
  622 +
  623 +
  624 +```Plain Text
  625 +const seen = new Set(); const confirmed = []; let dry = 0;
  626 +
  627 +while (dry < 2) { // stop after 2 empty rounds
  628 + const found = (await parallel(
  629 + FINDERS.map((f) => () => agent(f.prompt, { schema: BUGS }))
  630 + )).filter(Boolean).flatMap((r) => r.bugs);
  631 +
  632 + const fresh = found.filter((b) => !seen.has(key(b)));
  633 + if (!fresh.length) { dry++; continue; } // nothing new → toward dry
  634 + dry = 0;
  635 + fresh.forEach((b) => seen.add(key(b))); // dedupe vs SEEN, not confirmed
  636 +
  637 + // diverse-lens verify each fresh finding before it counts
  638 + const judged = await parallel(fresh.map((b) => () =>
  639 + parallel(['correctness', 'security', 'repro'].map((lens) => () =>
  640 + agent(`Judge "${b.desc}" via ${lens} — real?`, { schema: VERDICT })))
  641 + .then((v) => ({ b, real: v.filter(Boolean).filter((x) => x.real).length >= 2 }))));
  642 +
  643 + confirmed.push(...judged.filter((v) => v.real).map((v) => v.b));
  644 +}
  645 +```
  646 +
  647 +
  648 +
  649 +
  650 +
  651 +## Tier the models across the *nodes*
  652 +
  653 +\#\# 按节点职能分层选用梯度规格的大模型
  654 +
  655 +##
  656 +
  657 +不是每个节点都需要你最好的模型。图让这变得明显,而单个智能体永远不会:有些节点是有界且重复的(提取这个字段、分类这个工单),有些承载真正的判断(综合报告、裁决发现)。
  658 +
  659 +Not every node needs your best model\. A graph makes this obvious in a way a single agent never does: some nodes are bounded and repetitive \(extract this field, classify this ticket\), and some carry the real judgment \(synthesize the report, adjudicate the finding\)\.
  660 +
  661 +
  662 +
  663 +无聊的节点用更便宜的模型跑,把你的昂贵 token 花在判断真正存在的地方。
  664 +
  665 +Run the boring nodes on a cheaper model and spend your expensive tokens where judgment actually lives\.
  666 +
  667 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MjQ0YjFjMWJjYzVjZjVmODhiMzAzYWRkOTQ5ZTkyMzZfODI1N2IxZWNhZGI5MTA4M2Q5NTcwMmI1M2UzOWNiNTlfSUQ6NzY2NDkxMDY2NTA2ODgwOTQ5OF8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  668 +
  669 +在工作流中,Claude 生成的每个子智能体默认继承你的会话模型,除非脚本覆盖它——所以默认情况下一次大运行全部按你的会话层级计费。单个 agent\(\) 调用上的 model 选项告诉 Claude 只把那个节点路由到别处。
  670 +
  671 +In a workflow every subagent Claude spawns inherits your session model unless the script overrides it \- so by default a big run bills entirely at your session tier\. The model option on a single agent\(\) call tells Claude to route just that node elsewhere\.
  672 +
  673 +
  674 +
  675 +大运行前检查 /model,然后让 Claude 把扇出的重复节点路由到更便宜的模型,把合并节点留在高端。这就是把一个吃 token 的图从昂贵变成经济的杠杆,不需要改变它的形状。
  676 +
  677 +**Check /model before a large run,** then have Claude route the fan\-out’s repetitive nodes down to a cheaper model and keep the merge node up\. This is the lever that turns a token\-hungry graph from expensive into economical without touching its shape\.
  678 +
  679 +
  680 +
  681 +
  682 +
  683 +## Topology *is* your cost and latency
  684 +
  685 +\#\# 拓扑就是你的成本和延迟
  686 +
  687 +
  688 +
  689 +任务图谱的拓扑结构绝非锦上添花的表面设计,而是直接影响实际运行耗时的核心关键变量。绝大多数人都会踩坑的一个选择:`parallel()`(并行)与 `pipeline()`(流式管道)。`parallel()` 依赖屏障机制,所有分支必须等待最慢的节点执行完毕,下一阶段才能启动。
  690 +
  691 +The shape of the graph isn’t cosmetic \- it’s the single biggest lever on wall\-clock time\. The choice that trips everyone up: parallel\(\) versus pipeline\(\)\. A parallel\(\) barrier makes *everything* wait for the slowest node before the next stage starts\.
  692 +
  693 +
  694 +
  695 +`pipeline()` 以流式方式让每条数据独立走完全流程,全程无屏障阻塞:条目 A 处理到第三阶段时,条目 B 可能还停留在第一阶段。处理速度快的数据会提前完成,不会卡在慢速任务后方空等。
  696 +
  697 +A pipeline\(\) streams each item through all stages independently, with no barrier \- item A can be in stage 3 while item B is still in stage 1\. Fast items finish early instead of idling behind slow ones\.
  698 +
  699 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=NjVkN2UyZGMzOTBhMWI5Yzk0ZDVhMDE4ZDE4NDZkOWZfMWE4NmU0NDdlYTFlYThjNTQwZjFlNThkMjA0NGViMTJfSUQ6NzY2NDkxMDc1MTU4NDczNDQyNl8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  700 +
  701 +
  702 +
  703 +默认采用流式管道 `pipeline()`;仅当某一阶段确实需要一次性拿到上游全部结果时,才使用屏障机制 —— 例如全量数据去重、基于全部统计结果提前终止流程、需要对比多条检索结论的综合提示词场景。 “代码写起来更整洁”“各阶段逻辑划分清晰” 都不足以作为启用屏障的理由;屏障带来的延迟是真实、可量化、纯粹浪费的耗时。逻辑分层清晰,不等于必须同步阻塞等待所有任务。
  704 +
  705 +**Default to pipeline\(\)\.** Reach for a barrier only when a stage truly needs every prior result at once \- a cross\-set dedupe, an early\-exit on the total, a prompt that compares against “the other findings\.” “It’s cleaner code” and “the stages feel separate” are not reasons; barrier latency is real, measurable, wasted time\. Separate is not the same as synchronized\.
  706 +
  707 +
  708 +
  709 +
  710 +
  711 +## Let Claude draw the graph \- *self\-routing*
  712 +
  713 +\#\# 交由 Claude 自主生成任务图谱:自路由机制
  714 +
  715 +
  716 +
  717 +最后一种方案:对于无法预先规划流程的任务,不再手动编写固化的任务图谱。
  718 +
  719 +The final move is to stop drawing the graph by hand for jobs you can’t plan in advance\.
  720 +
  721 +
  722 +
  723 +借助动态工作流,你只需描述最终目标,Claude 会自行生成任务编排脚本:拆解整体任务、选择并行分发策略、调度一组协同工作的子智能体,最后整合输出结论。系统会生成适配本次任务专属的流程拓扑,而非一套寄希望于能适配所有场景的固化图谱。
  724 +
  725 +With **dynamic workflows**, you describe the objective and Claude writes the orchestration script itself\- decomposing the task, choosing the fan\-out, spawning a coordinated fleet of subagents, and synthesizing the result\. You get a graph tailored to *this* run instead of a fixed one you hoped would fit\.
  726 +
  727 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MTBhZTQ1MzBiZGE2OWZmZGU1NWVlMjZkMTQ2YWUyNDZfMTI2OWU3OGQ1Zjk1MmVlMGE3NTVmMTE1M2I5ZDI1MTBfSUQ6NzY2NDkxMDgxNjU4OTk5MDg4NV8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  728 +
  729 +一共有三种启用动态工作流的方式:
  730 +
  731 +第一种,在提示词中直接写明 “workflow(工作流)”,Claude 会为当前任务自动生成一套流程;
  732 +
  733 +第二种,调用已保存或打包好的预置工作流 ——`/deep-research` 就是一套已投产上线的标准任务图谱,完整链路为:范围界定 → 并行检索 → 内容拉取 → 对抗式校验 → 结果综合,和本教程讲解的标准框架完全一致。
  734 +
  735 +There are three ways in\. Say the word **“workflow”** in your prompt and Claude writes one for the task\. Run a saved or bundled one \- /deep\-research is a real graph shipping in production: scope → parallel search → fetch → adversarial verify → synthesize, the exact skeleton from this course\.
  736 +
  737 +
  738 +
  739 +第三种方式:开启极速代码模式(ultracode),会话内每一项有一定复杂度的任务,Claude 都会自动规划专属工作流。 某次执行效果理想时,按下快捷键 `s` 即可将编排脚本保存至 `.claude/workflows/` 目录;脚本自带版本管理,可通过名称重复调用,任何克隆该代码仓库的人都能直接启动这套任务图谱。
  740 +
  741 +Or turn on ultracode and Claude plans a workflow for every substantial task in the session\. When a run is good, press s to save its script into \.claude/workflows/ \- version\-controlled, re\-runnable by name, a graph anyone who clones the repo can launch\.
  742 +
  743 +
  744 +
  745 +python
  746 +
  747 +执行一套工作流,对 `src/routes/` 下所有接口路由开展鉴权缺失审计:为每一个路由文件单独创建一个子智能体,每条检测结果都先经过校验,再输出审计报告。 ● Claude 已生成任务编排脚本・后台启动执行中… /workflows — auth\-audit(鉴权审计工作流)・运行中 ✓ 范围界定 1/1,消耗 2100 Token,耗时 4 秒 ✓ 并行扇出 18/18,18 个路由文件各分配独立子智能体 ◯ 校验环节 11/18,每条检测结论配备 3 票制对抗质疑校验器…… ○ 汇总合成 0/1,等待校验阶段完成 会话全程保持可交互状态,子智能体集群后台持续运算不阻塞界面
  748 +
  749 +```Python
  750 +› Run a workflow to audit every route under src/routes/ for missing
  751 +auth. Spawn one agent per route file, then verify each finding before
  752 +reporting. ● Claude wrote an orchestration script · launching in
  753 +background… /workflows — auth-audit · running ✓ Scope 1/1 2.1k tok ·
  754 +4s ✓ Fan-out 18/18 one agent per route file ◯ Verify 11/18 3-vote
  755 +skeptics per finding… ○ Synthesize 0/1 waiting on verify session stays
  756 +responsive — keep working while the fleet runs
  757 +```
  758 +
  759 +
  760 +
  761 +\#\# 本周用 Claude 构建的六个图
  762 +
  763 +## Six graphs to build with *Claude* this week
  764 +
  765 +![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=NWExMDAwMmYxNTRkZTdjMjRkMDA1MmQ1NjU5NDM2NWFfNzRjYTIwNDBkYzQ0MmUzYjcwODMzZTA1ZjI3OTNiNzhfSUQ6NzY2NDkxMDkwODkxNzI3MTUwNV8xNzg1ODM0Mjk2OjE3ODU5MjA2OTZfVjM)
  766 +
  767 +- **Security sweep across every route\. **Claude spawns **one subagent per route file**, each hunting for missing auth checks, then a verifier pass confirms every finding before it reaches the report\. Breadth no single context could hold\.
  768 +
  769 +全链路安全扫描
  770 +
  771 +对每一条业务路径执行安全巡检。Claude 会为每个路由文件单独生成一个子智能体,分别检索缺失的权限校验逻辑;所有风险点需经过校验器二次核验,确认无误后才会录入报告。覆盖广度远超单一上下文所能承载的分析范围。
  772 +
  773 +
  774 +
  775 +- **Cited report with /deep\-research\.** A graph that ships in Claude Code already\. Claude decomposes your question into distinct angles, runs parallel searches, dedupes sources, then **adversarially verifies every claim** with three\-vote skeptics before writing\.
  776 +
  777 +基于 /deep\-research 指令生成引证调研报告
  778 +
  779 +该功能图谱已内置在 Claude Code 中。Claude 将你的查询拆解为多个独立分析维度,并行执行多源检索、去重重复资料;随后启用三方复核机制对每一条结论交叉质证,全部核验通过后再撰写最终报告。
  780 +
  781 +
  782 +
  783 +- **Port a module, file by file\.** The Bun ceiling, scaled to your repo\. Claude fans out translation across files, runs the test suite as a gate on each, and loops the failures back \- **adversarial review** catching what a single pass would ship broken\.
  784 +
  785 +逐文件迁移代码模块
  786 +
  787 +突破 Bun 运行时性能上限,适配你的完整代码仓库。Claude 并行批量转换多个文件,每完成一份文件就执行全套测试用例作为准入关卡;编译 / 测试失败的文件会回流重处理。对抗式复核机制能揪出单次粗转极易遗留的代码缺陷。
  788 +
  789 +
  790 +
  791 +- **Adversarial review of a diff\.** Claude routes on diff size: a small change gets one quick pass, a large one triggers a **full parallel audit** with reviewers on distinct lenses \- correctness, security, performance \- then a judge panel synthesizes\.
  792 +
  793 +代码变更差分对抗评审
  794 +
  795 +Claude 根据代码改动规模分流评审策略:小幅变更执行一轮快速核查;大规模改动将启动全量并行审计,多评审智能体分别从逻辑正确性、安全漏洞、运行性能三大维度独立审查,最后由合议模块整合各方结论输出综合意见。
  796 +
  797 +
  798 +
  799 +- Ecosystem scan on a schedule\. Save it once, re\-run it forever\. Claude checks many sources in parallel \- eleases, blogs, discussion \- ranks by impact at a barrier, and writes the digest\. **Version\-controlled in \.claude/workflows/**, launchable by name\.
  800 +
  801 +定时生态全域巡检
  802 +
  803 +配置一次,永久复用。Claude 并行拉取多渠道信息:版本发布公告、技术博客、社区讨论帖;按业务影响权重过滤低价值内容,自动生成精简摘要。工作流配置存入版本库 `.claude/workflows/` 目录,支持通过名称一键启动。
  804 +
  805 +
  806 +
  807 +- **Discovery of unknown size\.** You don’t know how many bugs are there\. Claude runs finders in parallel, dedupes each new find **against everything seen**, verifies survivors, and keeps looping until two rounds turn up nothing new \- then stops\.
  808 +
  809 +未知范围全域漏洞探查
  810 +
  811 +适用于漏洞总量未知的场景。Claude 并行启动多套漏洞检索器,新发现问题自动与已有结果去重,留存条目全部完成核验;持续循环检索,连续两轮无新增问题后自动终止扫描。
  812 +
  813 +
  814 +
  815 +
  816 +
  817 +## Conclusion:
  818 +
  819 +\#\# 结语:
  820 +
  821 +
  822 +
  823 +提示者问问题。架构师画图。
  824 +
  825 +A prompter asks a question\. An architect draws a *graph\.*
  826 +
  827 +
  828 +
  829 +线性智能体从来不是天花板——它只是第一个形状,每个人都会伸手去拿的那个,因为它匹配我们打字的方式。一行,一个脑袋,一次一件事。
  830 +
  831 +The linear agent was never the ceiling \- it was just the first shape, the one everyone reaches for because it matches how we type\. **One line, one head, one thing at a time\.**
  832 +
  833 +
  834 +
  835 +一旦你能看到节点和边,你就不再要求智能体做更多,而是要求图做得更宽:在独立工作处扇出,在信心要紧处给边加门,在判断不需要处分层模型。
  836 +
  837 +Once you can see the nodes and the edges, you stop asking the agent to do more and start asking the graph to do it wider: fan out where the work is independent, gate the edges where confidence matters, tier the models where judgment doesn’t\.
  838 +
  839 +
  840 +
  841 +大多数人会继续排队走直线。学会画图的人将运行一个舰队——而永远不会注意到其他人卡在的天花板。
  842 +
  843 +Most people will keep queueing steps in a line\. **The ones who learn to draw the graph will run a fleet** \- and never notice the ceiling the rest are stuck under\.
  844 +
... ...
docs/reference/graph-engineering-vs-loop-engineering-overview.diff-with-graphlemon.md 0 → 100644
  1 +# 对照笔记:《Loop Engineering 就凉了?一文看懂 Graph Engineering 的"新范式"》vs graphlemon 架构
  2 +
  3 +- **对照对象**:[graph-engineering-vs-loop-engineering-overview.md](./graph-engineering-vs-loop-engineering-overview.md)(本文简称"综述版")
  4 +- **背景**:综述版和之前对照过的 5 步版/14 步版不同——不是"怎么动手搭 Dynamic Workflows"的教程,而是行业视角的综述:术语起源、五层演进、Anthropic 官方模式、拓扑分类、框架对比、真实生产案例。本笔记只针对综述版的论点跟 graphlemon 现状对照,不重复之前两份笔记已经覆盖的内容(worktree 隔离、Verifier 独立上下文、loop-until-dry、按节点选模型等,见 [graph-engineering-1000-agents-5-steps.diff-with-graphlemon.md](./graph-engineering-1000-agents-5-steps.diff-with-graphlemon.md))。
  5 +- **状态**:讨论草稿,待后续会话继续展开。
  6 +- **整理日期**:2026-08-04
  7 +
  8 +---
  9 +
  10 +## 1. 五层演进定位:graphlemon 同时占了 Loop 和 Graph 两层
  11 +
  12 +综述版把 Prompt→Context→Harness→Loop→Graph 排成递进的五层,主张"Loop 解决单智能体持续工作,Graph 解决多智能体/工具/人的组织"。
  13 +
  14 +graphlemon 的架构里两层都在,而且是**嵌套关系**,不是二选一:
  15 +
  16 +- **Org-level Loop**(`docs/graphlemon_architecture.md` §4.3):Validator → Planner 的回退边,本质就是综述版说的 Loop——同一个任务反复"试一下 → 看结果 → 调整 → 再来",直到通过或 3 轮未收敛转 `blocked_pending_human`。
  17 +- **Org Graph + Work Graph 两层图**:Org Graph 三节点(角色间协作)+ 每个节点内部动态生成的 Work Graph(fan-out/fan-in/loop),是综述版说的 Graph。
  18 +
  19 +也就是说 graphlemon 不是"从 Loop 升级到了 Graph",而是**把 Loop 当成 Graph 里的一条边**(Org-level Loop 只是 Org Graph 上 Validator→Planner 这一条回退边的别名)。这跟综述版"Loop 是 Graph 的组成部分之一,不是被取代"的立场是一致的,比综述版举的例子(每日简报单节点 Loop vs 三节点 Graph 二选一)更进一步——graphlemon 把两者做成了同一张图里的不同粒度。
  20 +
  21 +## 2. "运动员兼裁判"问题:Validator 独立评判 + 三视角评审团已经落地
  22 +
  23 +综述版第 08 章的核心论点:多数智能体系统翻车是因为"模型既当运动员又当裁判",解法是让 Verifier 用独立干净的上下文,专门试图推翻结论。
  24 +
  25 +graphlemon 不仅做到了"Validator 是独立 Org Agent"(这点在上一份笔记已确认),根因溯源还直接实现了综述版提到的**"评审团"式多视角交叉审**:
  26 +
  27 +```python
  28 +# src/root_cause_trace.py
  29 +def judge_root_cause(perspective_fns):
  30 + """要求 >= 2/3 视角一致才定案(04-P: "至少 2/3 视角一致才定案"),
  31 + 否则 raise InconclusiveEscalateError,附带三视角原始判断供人工裁决。"""
  32 +```
  33 +
  34 +三个视角分别是"正确性视角 / 需求完整性视角 / Skill 系统性缺陷视角",跟综述版给的"三种验证打法"(快速核对 / 多角度交叉审 / 评审团)里最重的一种直接对应——而且 graphlemon 是**票数不足自动升级人工**,不是简单多数决,比综述版描述的"扛得住才放行"更严格。
  35 +
  36 +## 3. 拓扑:Coder 的 Supervisor 就是"主管模式",Planner 的四步链是"流水线+菱形"组合
  37 +
  38 +综述版第 06 章给出三种经典拓扑:菱形(fan-out/fan-in)、主管模式(Orchestrator-Workers)、流水线(Pipeline)。这三种在 graphlemon 里都能对上号,而且是早于这篇综述版就已经用中文命名过的概念:
  39 +
  40 +| 综述版拓扑 | graphlemon 对应设计 |
  41 +|---|---|
  42 +| 主管模式(Orchestrator-Workers) | Coder Work Graph 的 **Supervisor(调度子 Agent)**——常驻节点监控所有并行/串行 Work Node,发现卡点后重新调度或升级(架构文档 §6.2)。这跟综述版 Anthropic Research 系统"主智能体分析问题、生成子智能体、子智能体并行搜集、汇总"的描述几乎逐字对应。 |
  43 +| 流水线(Pipeline) | Planner Work Graph 主干 BRD→PRD→SDD→Benchmark,后者依赖前者的确定版本(架构文档 §6.1),是教科书式的固定步骤链。 |
  44 +| 菱形(Fan-out/Fan-in) | Planner 在 PRD/SDD/测试用例阶段的 fan-out 候选点(多场景并行撰写 PRD 片段、多技术方案候选、功能/边界/性能测试并行生成),再 fan-in 合并 + 一致性检查(架构文档 §6.1)。 |
  45 +
  46 +三种拓扑在 graphlemon 里不是互斥选型,而是像综述版说的"可以拼装嵌套的积木"——Coder 的主管模式套着若干模块级的菱形并行实现,Planner 是流水线套着阶段内的菱形。
  47 +
  48 +## 4. Anthropic 官方五种工作流模式的隐式覆盖
  49 +
  50 +综述版第 07 章列的五种模式(路由 Routing、并行化 Parallelization、编排者-工作者 Orchestrator-Workers、评估-优化 Evaluator-Optimizer、提示链 Prompt Chaining),graphlemon 目前没有显式引用这套术语,但设计上已经全部隐式覆盖:
  51 +
  52 +| Anthropic 模式 | graphlemon 对应机制 |
  53 +|---|---|
  54 +| Routing | Validator 在 bug 记录里打 `route: fast_lane \| full_replan` 标签,`org_loop_route.py` 用**纯代码**(`_NEW_STATUS`/`_NEW_ORG_NODE` 查表)决定走 Coder 快速通道还是 Planner 完整重规划——不是再派一个 Agent 去决定路由,分类交给智能体,走哪条边用代码,正是综述版第 08 章"让模型的判断力落在节点上,让代码的可靠性落在边上"的原话实现。 |
  55 +| Parallelization | Planner/Coder Work Graph 的 fan-out(见上表菱形)。 |
  56 +| Orchestrator-Workers | Coder 的 Supervisor(见上表主管模式)。 |
  57 +| Evaluator-Optimizer | Org-level Loop:Validator 评估 → 不通过则打回 Planner/Coder 优化 → 再评估,循环上限 3 轮。 |
  58 +| Prompt Chaining | Planner 的 BRD→PRD→SDD→Benchmark 主干(见上表流水线)。 |
  59 +
  60 +## 5. "工作图 vs 角色图"治理红线——精确对应 graphlemon 的 Org Graph / Work Graph 边界
  61 +
  62 +这是综述版里对 graphlemon 最有价值的一条新论点(第 10 章末尾):
  63 +
  64 +> 图允许"任务怎么拆、怎么合"现场灵活调整,这叫**工作图**,可以快变。但"谁有权改数据库、谁能绕过审批"这类长期权限,绝不能让模型现场发挥,这叫**角色图**,必须慢变、可审计。
  65 +
  66 +这条红线跟 graphlemon 现有的两层设计几乎是同一件事的两种命名:
  67 +
  68 +- **角色图 = Org Graph**:Planner/Coder/Validator 三个角色的职责边界、谁能做什么(Planner 不写代码、Coder 不判定验收、Validator 不直接改代码/需求)是人工固定设计、代码级写死的(架构文档 §3"关键设计原则 1":Org Graph 改动属于架构变更,需要人工评审,不由系统自主修改)。
  69 +- **工作图 = Work Graph**:每次任务全新规划生成的节点/边/fan-out/fan-in 是"快变"的,且明确允许系统自主生成(架构文档 §3"关键设计原则 2")。
  70 +
  71 +上一份对照笔记(5 步版)已经提出"Org Graph 固定 / Work Graph 动态"这条边界,但当时是从"什么时候不该用 Graph"的角度推出来的。综述版这次是从**权限治理**角度独立给出同一条边界,两条路径殊途同归——这进一步印证了 graphlemon 这条设计边界不是巧合,值得真正写进架构文档,用综述版这个"工作图/角色图"的命名来强化表述(比自造的"固定/动态"说法更贴合行业共识)。
  72 +
  73 +## 6. 持久化执行(Durable Execution):断点恢复已实现,Pause/Resume 已实现,Time-Travel 调试未实现
  74 +
  75 +综述版第 11 章重点讲 LangGraph 的 checkpointer:每个"超级步"存快照,带来人在回路、记忆、时间旅行调试、容错(`pending writes`:部分节点失败时已成功节点的输出不丢)四个能力。逐项对照:
  76 +
  77 +| LangGraph 能力 | graphlemon 现状 |
  78 +|---|---|
  79 +| 容错(部分失败保留已成功节点输出) | ✅ 已具备:`work_node_execution` 按节点独立持久化状态,单节点失败不影响其他已完成节点的产出(架构文档 §7.1"断点续跑与持久化"是硬需求)。 |
  80 +| 人在回路(暂停/恢复) | ✅ 已实现:`src/task_execution.py`(2026-08-03 提交)的手动 Start/Pause/Stop,复用 `blocked_pending_human` 状态 + `user_paused` 标记,`org_loop_route._manual_resume` 恢复暂停前状态。 |
  81 +| 记忆(多轮交互间保留上下文) | ✅ 已具备:`.claude/agent-memory/{planner,coder,validator}/`,`memory: project` 字段,随 Git 历史版本化(架构文档 §7.3)。 |
  82 +| 时间旅行调试(回到任意历史检查点重放/分叉) | ❌ 未实现,也未见规划。graphlemon 目前是"从当前状态继续/重试",不支持"回到某个历史节点重新分叉执行"。这是综述版指出但 graphlemon 尚未覆盖的一个真实缺口,值得评估是否有必要——大概率优先级不高(自举开发场景下,人工 review git 历史 + 手动重跑节点已经能覆盖大部分需求),但如果未来要支持"审计整个代码库"类大规模任务,调试大图执行路径时可能会需要。 |
  83 +
  84 +## 7. 值不值得用图:graphlemon 的场景天然通过综述版的成本检验
  85 +
  86 +综述版第 09/10 章的核心决策框架:Graph 的额外开销(维护多份提示词、设计节点间状态结构、应对新失败模式)对"只跑一次的任务"是纯税,对"每天都要跑的""价值足够高的"任务才值。Anthropic 给的三个该用多智能体的场景是:并行可分解、任务复杂到单一上下文装不下、价值高到能覆盖 token 成本。
  87 +
  88 +graphlemon 的定位(7×24 黑灯工厂,持续产出、持续迭代)天然落在综述版认为"值"的一侧,不需要重新论证——但这也是一个提醒:**graphlemon 内部具体到某个 Work Node 该不该再往下 fan-out**,应该套用同一把尺子(是否真的并行可分解、是否真的装不下单一上下文、边际收益是否覆盖 token 成本),而不是默认能拆就拆。目前架构文档里 fan-out 候选点(§6.1 的 PRD/SDD/测试用例并行)都是"候选"而非强制,这个判断目前留给 Planning Skill 生成 Work Graph 时临场决定,综述版的三条尺子可以作为这个临场判断的显式检查清单。
  89 +
  90 +## 8. 待讨论的开放问题(供下一轮会话展开)
  91 +
  92 +- [ ] 是否要把综述版"工作图 vs 角色图"这个命名正式写进 `docs/graphlemon_architecture.md`,替换/补强现有"Org Graph 固定 / Work Graph 动态"的表述?
  93 +- [ ] Time-travel 调试(回到历史检查点分叉重放)要不要立项?还是维持"用 git 历史 + 手动重跑"的现状,等到真正遇到大规模 Work Graph 调试痛点再补?
  94 +- [ ] Planning Skill 生成 Work Graph 时,是否需要显式把"并行可分解 / 单一上下文装不下 / 收益覆盖成本"这三条尺子写进 Prompt,作为 fan-out 决策的强制检查项,而不是隐式判断?
  95 +- [ ] root_cause_trace 的三视角评审团机制目前只在 `test_failed` 根因溯源场景用到,综述版把这类"重仓verifier"归为"高风险场景才用的重打法"——graphlemon 其他地方(比如 Coder 的自测 reflection)要不要评估是否也该按风险分级选验证强度,而不是统一同一套。
... ...
docs/todo/graph-engineering-open-items.md 0 → 100644
  1 +# Graph Engineering 系列对照笔记——待确认事项清单
  2 +
  3 +**日期**: 2026-08-04
  4 +**性质**: 从三份对照笔记里提取出的全部"待讨论的开放问题",去重合并后按主题整理,供逐条确认方案。每条确认后会展开成独立的需求文档(参考 `docs/work-graph-parallel-execution-requirements.md` 的产出方式),本文件本身只做索引和现状追踪,不写详细方案。
  5 +**来源文档**:
  6 +- `docs/reference/graph-engineering-1000-agents-5-steps.diff-with-graphlemon.md`(简称"5 步版对照")
  7 +- `docs/reference/graph-engineering-vs-loop-engineering-overview.diff-with-graphlemon.md`(简称"综述版对照")
  8 +- `docs/reference/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).diff-with-graphlemon.md`(简称"14 步版对照")
  9 +
  10 +三份笔记里有几条开放问题实质上是同一件事的不同角度,已在下面合并并标注了原始出处。
  11 +
  12 +---
  13 +
  14 +## 状态图例
  15 +
  16 +- 🔲 待确认:还没讨论方案
  17 +- 🟡 讨论中/已产出需求文档,待评审
  18 +- ✅ 已确认,进入设计/实现
  19 +- ⛔ 已确认不做(附理由)
  20 +
  21 +---
  22 +
  23 +## A. 架构文档要不要正式采用"工作图 / 角色图"命名
  24 +
  25 +**状态**:🔲 待确认
  26 +
  27 +**来源**:5 步版对照 §4 开放问题 3 + 综述版对照 §8 开放问题 1(同一件事,两份笔记从不同角度各自提出)
  28 +
  29 +**背景**:综述版文章提出一条治理红线——"工作图"(任务怎么拆怎么合,可以快变)vs "角色图"(谁有权改什么,必须慢变可审计)。这条边界跟 graphlemon 现有的 Org Graph(人工固定设计,架构变更需人工评审)/ Work Graph(每次任务动态生成)边界几乎是同一件事的两种命名。5 步版对照笔记是从"什么时候不该用 Graph"的角度独立推出同一条边界的。
  30 +
  31 +**需要确认的问题**:是否要把 `docs/graphlemon_architecture.md` 里"Org Graph 固定 / Work Graph 动态"的表述,正式替换或补强为综述版这套"工作图 / 角色图"命名?这是纯文档措辞层面的决策,不涉及代码改动。
  32 +
  33 +---
  34 +
  35 +## B. loop-until-dry 要不要在 Validator 落地
  36 +
  37 +**状态**:🔲 待确认
  38 +
  39 +**来源**:5 步版对照 §4 开放问题 1
  40 +
  41 +**背景**:Org-level Loop 目前固定 3 轮上限(`docs/sdd/04-P-流程与状态流转.md` 已标注这是"未来优化候选")。14 步版对照进一步发现:这不只是优先级问题——`work_graph_orchestrate.py::topological_order` 对有环的 `graph_manifest` 直接报错,Work Graph 被设计成严格 DAG,图内根本不支持回路,要支持 loop-until-dry 需要动执行模型本身,不是加一个判断逻辑那么简单。
  42 +
  43 +**需要确认的问题**:这条要不要立项?如果要,是在 Org-level Loop 这个更粗的粒度上做(比如把"3 轮固定"换成"连续 K 轮无新 bug 才停",不涉及 Work Graph 内部执行模型),还是真的要改到 Work Graph 内部支持图内回路(工作量大得多)?
  44 +
  45 +---
  46 +
  47 +## C. 按节点分层选模型——策略归属
  48 +
  49 +**状态**:🔲 待确认
  50 +
  51 +**来源**:5 步版对照 §4 开放问题 2 + 14 步版对照 §1 表格第 12 行 / §3 开放问题 3
  52 +
  53 +**背景**:`agent_profile.py` 里 Planner/Coder/Validator 三个 Org Agent 目前全部固定 `claude-sonnet-5`。14 步版对照进一步发现:Work Graph 层的管线其实已经打通了一半——`_render_node_script` 支持往生成的单节点脚本里塞 `model` 参数,`run_fn(cwd, model=None)` 也留了口子,但没有任何调用方真正传入非 None 的值,`graph_manifest` 的节点结构里也没有 `model` 字段。基础设施就位,缺的是"谁来决定每个节点该用哪个模型"这层策略。
  54 +
  55 +**需要确认的问题**:
  56 +1. Org Graph 三节点(Planner/Coder/Validator)要不要差异化选型?
  57 +2. Work Graph 内部节点的模型策略该由谁定——Planning Skill 生成 `graph_manifest` 时就给每个节点标注推荐模型,还是在 `run_graph()` 里按 `org_node`/`artifact_type` 写一张静态映射表?
  58 +
  59 +---
  60 +
  61 +## D. Time-travel 调试要不要立项
  62 +
  63 +**状态**:🔲 待确认
  64 +
  65 +**来源**:综述版对照 §6
  66 +
  67 +**背景**:LangGraph 的 checkpointer 支持回到任意历史检查点重放/分叉。graphlemon 目前的容错(部分节点失败保留已成功节点输出)、人在回路(Start/Pause/Stop)、记忆(`.claude/agent-memory/`)都已具备,唯独"回到某个历史节点重新分叉执行"没有,现状是"用 git 历史 + 手动重跑"。
  68 +
  69 +**需要确认的问题**:这条要不要立项,还是维持现状,等真正遇到大规模 Work Graph 调试痛点(比如「Work Graph 节点并行执行」上线后图变复杂了)再评估?倾向于暂不立项,但需要你确认。
  70 +
  71 +---
  72 +
  73 +## E. Planning Skill 提示词强化——识别可并行节点
  74 +
  75 +**状态**:🔲 待确认
  76 +
  77 +**来源**:综述版对照 §7 开放问题 3 + 14 步版对照 §3 开放问题 2(同一主题:让 Planning 阶段更早识别并行机会)
  78 +
  79 +**背景**:`work_graph_plan.py` 里 Planning Skill 的 prompt(约在 `work_graph_plan.py:67` 附近)目前只要求"一个 artifact_type 一个节点",没有引导 Claude 识别哪些节点互不依赖。目前观察到的真实 `graph_manifest`(如 BRD→PRD→SDD→Benchmark)是纯串行链,架构文档 §6.1 设想的并行候选点从未真正被生成过。
  80 +
  81 +**需要确认的问题**:要不要把 14 步版"对每个箭头问是否真的有数据流动"的提问方式、以及 Anthropic 官方"并行可分解 / 单一上下文装不下 / 收益覆盖成本"这三条尺子,显式写进 Planning Skill 的 prompt,让它在生成 `graph_manifest` 阶段就标注可并行的节点组?
  82 +
  83 +**依赖关系**:这条和「G. Work Graph 节点并行执行」是互补的——G 解决"执行层能不能并发跑",E 解决"规划层会不会生成值得并发的图"。E 做了但 G 没做,并行能力用不上;G 做了但 E 没做,`graph_manifest` 大概率还是纯串行链,G 的能力也没有实际发挥空间。建议两条一起排期,但可以先确认 G(已有需求文档),E 单独讨论。
  84 +
  85 +---
  86 +
  87 +## F. 三视角评审团机制要不要推广到其他验证点
  88 +
  89 +**状态**:🔲 待确认
  90 +
  91 +**来源**:综述版对照 §7 开放问题 4 + 14 步版对照 §3 开放问题 4(同一主题)
  92 +
  93 +**背景**:`root_cause_trace.judge_root_cause`(正确性/需求完整性/Skill 缺陷三视角,≥2/3 一致才定案,否则升级人工)目前只服务于 Validator 的 `test_failed` 根因溯源场景。`run_graph()` 自身的 `verify_fn` 是单一验证器 + 失败重试一次,没有多视角/对抗式/评审团这类"重打法"。综述版文章把这类重仓验证归为"高风险场景才用",暗示应该按风险分级选验证强度,而不是所有节点统一同一套。
  94 +
  95 +**需要确认的问题**:要不要把三视角评审团模式也下沉到 `verify_fn` 里,作为高风险节点(比如 SDD、代码变更)的可选验证强度?如果要,"高风险"的判定标准是什么(节点类型固定标注,还是运行时动态判断)?
  96 +
  97 +---
  98 +
  99 +## G. Work Graph 节点并行执行(Python 线程池方案)
  100 +
  101 +**状态**:🟡 已产出需求文档,待评审 → 详见 `docs/work-graph-parallel-execution-requirements.md`
  102 +
  103 +**来源**:14 步版对照 §3 开放问题 1
  104 +
  105 +**背景**:`run_graph()` 目前严格串行执行所有节点,即使 `graph_manifest` 里的节点互不依赖。已经讨论过按拓扑层分批 + 线程池并发的方案,核心风险(SQLite 并发写入、worktree 创建竞态、配额错误处理)都已识别,且发现 `backend_server.py` 里已有可复用的"共享连接 + `threading.Lock()`"先例。已产出完整需求文档(目标/非目标/功能需求/验收标准/风险/开放问题),可以直接进入评审。
  106 +
  107 +**需要确认的问题**:这份需求文档本身要不要按现在的范围继续往设计阶段走?还是需要先调整需求文档里的某些点?
  108 +
  109 +---
  110 +
  111 +## 建议的确认顺序
  112 +
  113 +G 已经有现成的需求文档,最接近可以推进的状态,建议先确认。E 和 G 强相关(不改 Planning Skill,G 做出来的并行能力可能用不上真实的并行图),可以放在 G 之后一起看。A(纯文档命名)、B、C、D、F 相对独立,可以任意顺序讨论。
  114 +
  115 +你想先确认哪一条?
... ...
docs/work-graph-parallel-execution-requirements.md 0 → 100644
  1 +# Work Graph 节点并行执行——需求文档
  2 +
  3 +**日期**: 2026-08-04
  4 +**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应对 `docs/reference/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).diff-with-graphlemon.md` 开放问题第 1 条的讨论——"现在的每节点独立子进程设计,断点续跑收益是否值得放弃 fan-out 并行?能否两者兼得?"。详细设计(SDD 层面:`topological_layers()` 的具体实现、线程池调度器接口、锁的粒度)留到需求确认后单独产出。
  5 +**状态**: 草案,待评审。
  6 +
  7 +## 1. 背景
  8 +
  9 +`src/work_graph_orchestrate.py::run_graph()` 目前用 Kahn 算法把 `graph_manifest` 拍平成一条扁平的拓扑序,用纯 Python `for` 循环逐个节点执行——即使两个节点在 `graph_manifest` 里互相没有 `depends_on` 关系(本可以并行),现在也是严格排队执行,跑完一个才开始下一个。
  10 +
  11 +这是一个记录在案的、有意为之的权衡(见该模块 docstring):早先方案(`docs/graphlemon_architecture.md` §10.1)设想直接复用 Claude Code 原生动态工作流的 `parallel()`/`pipeline()` 来跑整个 Work Graph,但那要求把整张图塞进一次不透明的 `claude` 子进程调用,Python 侧在执行过程中对每个节点的完成状态完全不可见——这跟 REQ-06"进程重启后从已完成节点继续、不重跑"的断点续跑硬需求根本冲突。所以最终选择了"每个节点一次独立的 `claude -p` 子进程调用 + Python 层持久化每个节点的状态",代价是放弃了图内并行。
  12 +
  13 +随着 Work Graph 规模变大(例如 Planner 阶段 PRD/SDD 的多场景/多方案 fan-out 候选,`docs/graphlemon_architecture.md` §6.1 已经设想过、但目前从未真正在生成的 `graph_manifest` 里出现过),纯串行执行会让原本可以同时进行的工作被迫排队,拖长任务的墙钟时间。本需求要解决的问题:**在不牺牲现有断点续跑能力的前提下,让 `graph_manifest` 里互不依赖的节点能够真正并发执行**。
  14 +
  15 +## 2. 目标
  16 +
  17 +1. `graph_manifest` 中没有依赖关系(无论是直接 `depends_on` 还是通过其他节点间接依赖)的节点集合,能够在同一时间窗口内并发执行,而不是被迫排队。
  18 +2. 现有的断点续跑语义(进程重启后,已经落库为 `done` 的节点不重跑,从未完成的节点继续)在并行改造后必须继续成立,不能因为改成并发就丢失这个能力。
  19 +3. 现有的每节点独立 worktree 隔离(self-target 场景无条件强制)在并发场景下继续正确工作,不出现多个节点互相踩写文件。
  20 +4. 现有的单节点验证-重试机制(`verify_fn`:验证失败则原地重跑一次,验证通过才允许下游节点开始)语义保持不变。
  21 +5. 并发规模可控,不因为一次任务的 `graph_manifest` 节点数变多就无限制地同时拉起等量的 `claude` 子进程,导致失控的 token/账单开销或系统资源耗尽。
  22 +
  23 +## 3. 非目标(本轮明确不做)
  24 +
  25 +1. **不做完整的"随时有依赖满足就调度"的数据流式调度器**。本轮只做"按拓扑层分批 + 层内并发、层间屏障"的粗粒度并行——层边界由 Kahn 算法的天然分层给出,不追求"某个节点的依赖一满足就立刻插队执行,不必等同层其他节点跑完"这种更细粒度的最优调度。理由:现在的 Work Graph 规模是个位数到十几个节点,层级屏障和真正的最优调度之间的墙钟差异预计很小,不值得为此引入更复杂的调度器和更难验证的正确性。
  26 +2. **不改变"每个节点是一次独立 `claude -p` 子进程调用"这个既有设计**。本需求只改变"什么时候触发这次调用"(从严格串行改成同层并发触发),不改变每次调用本身的机制(仍然是单节点 workflow 脚本 + `--json-schema` 校验),也不重新尝试把整图塞进一次 Claude 原生 `parallel()`/`pipeline()` 编排脚本。
  27 +3. **不引入按节点选模型的策略**。`_render_node_script` 已经预留的 `model` 参数、"哪个节点该用哪个模型"这层策略是独立的开放问题(见另一份对照笔记开放问题第 3 条),本需求不涉及。
  28 +4. **不改变 Org-level Loop 的三轮上限、loop-until-dry 机制**。本需求只影响单次 `run_graph()` 调用内部的节点调度,不涉及跨 Org Graph 的重试/收敛逻辑,也不试图让 `graph_manifest` 支持图内有环(`topological_order` 目前对有环图直接报错,这个限制维持不变)。
  29 +5. **不承诺降低 token 成本**。并行改造换来的是墙钟时间缩短,节点总数、每个节点的调用量不变,token 总消耗预计基本持平(详见「7. 风险与已知权衡」)。
  30 +
  31 +## 4. 功能需求
  32 +
  33 +| # | 需求描述 | 优先级 |
  34 +|---|---|---|
  35 +| FR-1 | `run_graph()` 在拿到 `graph_manifest` 后,应能识别出可以并发执行的节点分组(同一拓扑层内、彼此无依赖关系的节点集合),而不是像现在这样拍平成一条无结构信息的顺序列表。 | 必须 |
  36 +| FR-2 | 同一分组内的多个节点应能真正并发触发各自的 `claude -p` 子进程调用,而不是等前一个节点完全结束才开始下一个。 | 必须 |
  37 +| FR-3 | 并发执行期间,每个节点各自的持久化状态写入(节点行创建、状态流转、执行事件日志)必须保持数据完整性——不能因为多线程同时写同一个 SQLite 连接而出现"database is locked"报错、脏写、或状态丢失。 | 必须 |
  38 +| FR-4 | 并发执行期间,多个节点各自需要创建 git worktree 时(`needs_worktree=True` 场景)不能相互冲突,每个节点仍然拿到一个独立、干净的工作区。 | 必须 |
  39 +| FR-5 | 若同一分组内某个节点执行失败(含配额限制 `UsageLimitReachedError`、一般执行错误、`verify_fn` 验证失败且重试后仍不通过),已经在并发执行中的其他兄弟节点应当被允许自然跑完,不因为一个节点失败就中途取消其他正在消耗资源的调用;该分组内全部节点结束后再统一判定整个 `run_graph()` 调用的最终结果(成功/失败/需要升级)。 | 必须 |
  40 +| FR-6 | 并发度必须可配置且有合理的默认上限,防止一次任务的 `graph_manifest` 节点数暴涨时同时拉起等量的 `claude` 子进程。 | 必须 |
  41 +| FR-7 | 进程在某个分组执行到一半时中断重启后,恢复执行应能正确识别"这个分组里哪些节点已经 `done`、哪些还没跑完",只重新执行未完成的部分,不重跑已完成节点——这个语义必须和现在的单节点断点续跑完全等价,只是作用范围从"单个节点"扩展到了"一个分组内的多个节点"。 | 必须 |
  42 +| FR-8 | `verify_fn` 的语义保持不变:一个节点的输出必须先通过验证(或验证失败重试后通过),该节点的结果才能进入下游节点可读取的 `outputs`;依赖该节点的下游节点所在的分组,必须等到这一验证真正完成后才能开始执行,不能提前读取未经验证的中间结果。 | 必须 |
  43 +| FR-9 | 现有调用方(`e2e_pipeline.py` 等)对 `run_graph()` 的调用方式和返回值格式(`{node_id: output_payload}`)应保持兼容,不需要因为内部改成并行调度而修改调用方代码。 | 应该 |
  44 +| FR-10 | 并行执行的分组信息(哪些节点被并发调度、每组的起止时间)应当可观测——至少体现在现有的 `execution_event`/日志机制里,方便事后排查"某次运行到底并发了多少节点、并发是否生效"。 | 应该 |
  45 +
  46 +## 5. 验收标准
  47 +
  48 +1. 给定一个 `graph_manifest`,其中包含至少一组"两个节点互不依赖、都依赖同一个上游节点"的结构(例如 A→{B, C}→D),跑一次 `run_graph()`,能观测到 B 和 C 的 `claude -p` 子进程调用在时间上有重叠(不是 B 完全结束后 C 才开始)。
  49 +2. 用 mock 的 `run_fn`(不真实调用 `claude`,返回固定输出,可注入人为延迟)写单元测试:断言同一分组内的节点确实并发触发(例如用一个共享计数器/时间戳断言重叠),且分组之间保持严格先后顺序(下一层的节点不会在上一层未全部完成前开始)。
  50 +3. 模拟"进程在某个分组执行到一半时中断"(例如分组内两个节点,mock 让其中一个成功落库为 `done`、另一个抛异常模拟中断),重新调用 `run_graph()`,断言已完成的节点不会被重新执行(`run_fn` 没有被再次调用),只有未完成的节点被重新调度。
  51 +4. 并发场景下跑一轮包含多个节点写入 SQLite 的测试,不出现 `sqlite3.OperationalError: database is locked` 或类似的并发写入异常。
  52 +5. 模拟 `needs_worktree=True` 场景下同一分组内多个节点并发创建 worktree,断言每个节点拿到的 worktree 路径互不相同、互不冲突,且 `git worktree list` 能看到全部预期数量的 worktree。
  53 +6. 模拟同一分组内一个节点抛 `UsageLimitReachedError`、另一个节点正常完成,断言正常完成的节点的输出被正确保留(不因为兄弟节点失败而丢失),同时整体调用仍然正确地向上抛出/传播配额限制信号。
  54 +7. 现有 `src/test_work_graph_orchestrate.py`、`src/test_work_graph_execute.py`、`src/test_e2e_pipeline.py` 中的既有测试用例在改造后无需修改断言即可继续通过(验证行为兼容性),除非该用例本身就是在断言"严格串行"这一将被改变的实现细节(这类用例需要明确识别出来,逐一确认是否要更新)。
  55 +
  56 +## 6. 约束与假设
  57 +
  58 +- 复用 `src/backend_server.py::_Server` 里已经验证过的模式:共享一个 `sqlite3.connect(..., check_same_thread=False)` 连接 + `threading.Lock()` 保护并发写入,不引入连接池或每线程独立连接这类更重的方案。
  59 +- 复用现有的 `work_node_run.py` 断点续跑机制("已 `done` 的节点短路,直接返回持久化输出"),并行改造不重新设计这一层,只是让触发多个节点执行这件事本身从串行变成并发。
  60 +- 复用现有的每节点 worktree 隔离机制(`work_node_run.py::create_worktree`),如果发现并发创建 worktree 本身有 git 层面的竞态,允许在"创建 worktree"这一步单独加锁串行化,不要求整个节点执行都串行——真正耗时的 `claude -p` 调用部分仍然并发。
  61 +- 默认并发上限取一个保守值(具体数字留给设计阶段结合实测确定,不在需求阶段写死),避免一次运行同时拉起过多 `claude` 子进程。
  62 +
  63 +## 7. 风险与已知权衡
  64 +
  65 +- **不降低 token 成本,只缩短墙钟时间**:并行改造让节点调用"同时发生"而不是排队,但总的节点数量、每个节点的 token 消耗不变。如果验收标准或后续讨论里出现"并行后应该更省钱"的预期,需要提前澄清纠正。
  66 +- **层级屏障不是最优调度**:如果某一层里有一个节点明显比同层其他节点慢很多,下一层要等这个最慢的节点跑完才能开始,即使下一层里有些节点其实只依赖同层里已经跑完的那些节点。本需求接受这个次优性(见「3. 非目标」第 1 条),如果未来图规模变大、这个次优性变成真实瓶颈,需要另立需求评估数据流式调度器。
  67 +- **SQLite 并发写入的锁粒度需要在设计阶段仔细确定**:过粗的锁(整个 `run_graph()` 调用期间持锁)会退化成事实上的串行,失去并行的意义;过细的锁需要更仔细地审视哪些写入操作之间存在真正的竞态。这是详细设计阶段需要重点验证的部分,不是需求阶段能定论的。
  68 +- **worktree 创建的 git 层面竞态目前只是推测,未经验证**:需要在设计/实现阶段用真实的 `git worktree add` 并发调用做实测,确认是否真的需要额外加锁,还是 git 本身已经能安全处理并发创建。
  69 +
  70 +## 8. 开放问题(留给详细设计阶段回答)
  71 +
  72 +- [ ] `topological_layers()` 的具体实现:是在现有 `topological_order()` 旁边新增一个函数,还是重构 `topological_order()` 本身让它返回分层结构,由调用方按需拍平?
  73 +- [ ] 并发上限(`max_workers`)默认值定多少?是否需要按 Org Agent 类型(Planner 偏文档生成、Coder 偏代码修改)给不同的默认上限?是否需要暴露成可配置项(管理后台/环境变量)?
  74 +- [ ] FR-5 的"该分组内全部节点结束后再统一判定整体结果"具体的异常聚合/优先级规则是什么(比如同组里既有配额限制又有一般执行错误,该抛出哪一个、还是都记录?)?
  75 +- [ ] 是否需要在 `execution_event` 里新增一种事件类型来标记"分组开始/分组结束",还是复用现有的 `node_start`/`node_completed` 事件、靠时间戳重叠反推并发情况就够用?
  76 +- [ ] worktree 创建竞态如果实测确认存在,加锁的粒度是"整个 `create_worktree` 函数"还是可以更细(比如只锁 `git worktree add` 这一条命令的调用)?
... ...