Article
Claude Code Workflow 实现原理:一个可恢复的 Agent 编排引擎
结论先行
@anthropic-ai/[email protected] 的 workflow,不是普通 prompt 模板,而是一个正式暴露给运行时的编排工具。
它的核心机制不是”让模型自己记住步骤”,而是把多步任务写成一段受约束的脚本 DSL,再交给运行时做三件事:
- 解析与校验:确认脚本结构合法,而且可恢复
- 注册为后台任务:把执行请求变成
local_workflow一类的异步任务 - 按 phase 调度子 agent:推进
agent()、parallel()、pipeline()等步骤,并把进度、结果、停止状态写回运行态,供resumeFromRunId续跑
一句话概括:Claude Code 的 workflow 是一个”可编译、可持久化、可恢复”的多阶段 agent 编排引擎。
分析范围
- 版本:
@anthropic-ai/[email protected] - 分析对象:
package.jsoninstall.cjscli-wrapper.cjssdk-tools.d.tslinux_x64/package/claude的字符串与运行痕迹
- 分析目标: 只解释最新版 workflow 的实现原理本身
- 明确不包含: 跨版本对比、历史演进
证据基础
| 证据位置 | 关键内容 | 直接结论 |
|---|---|---|
package.json 第 4-37 行 | wrapper 包只分发 bin/claude.exe、install.cjs、cli-wrapper.cjs、sdk-tools.d.ts,真正执行体通过平台 optionalDependencies 提供 | workflow 核心执行逻辑不在公开 wrapper JS,而在平台二进制 |
install.cjs 第 4-10 行 | postinstall 会把平台原生二进制复制到 bin/claude.exe | workflow runtime 被封装进原生执行体 |
cli-wrapper.cjs 第 128-133 行 | fallback 逻辑本质是 spawnSync(binaryPath, process.argv.slice(2), ...) | wrapper 只是定位并拉起原生程序 |
sdk-tools.d.ts 第 2267-2297 行 | WorkflowInput 明确要求 export const meta = { name, description, phases } + agent()/parallel()/pipeline()/phase() | workflow 使用显式 DSL,而不是自由格式脚本 |
sdk-tools.d.ts 第 2291-2297、3107-3134 行 | scriptPath 会被持久化;resumeFromRunId 支持恢复;WorkflowOutput 返回 taskId / runId / scriptPath | workflow 从设计上就是异步、可恢复任务 |
| 原生二进制字符串痕迹 | 出现 local_workflow、TaskStop、resumeFromRunId、workflow.js、sourcesContent、sourceMappingURL 等标识 | workflow 执行链路和脚本编译环境都在原生包内 |
发布形态与执行边界
先把边界讲清楚,否则后面的 workflow 运行时很容易看偏。
wrapper 包不是执行核心
公开 npm 包只是一个很薄的壳:
package.json决定平台依赖装哪个包install.cjs负责安装期复制原生二进制cli-wrapper.cjs是 postinstall 失效时的兜底启动器sdk-tools.d.ts暴露工具协议
真正的 workflow 逻辑不在这些 JS 文件中。它们负责的是:找到平台正确的二进制、启动它、把公开工具接口描述出来。
workflow 运行时被封装进平台二进制
Linux x64 平台包中的 claude 是单个 ELF 可执行文件,不是普通 shell wrapper,而是完整运行时。
但从二进制字符串中能看到 sourceMappingURL、sourcesContent、workflow.js 这类痕迹——它内部仍然承载 JS/TS 打包产物与脚本编译逻辑,只是对外变成了单文件原生分发。
对分析来说,这意味着:
- 公开可读层:主要是 schema 和 wrapper 边界
- 真实执行层:在原生二进制里
- 分析策略:先从 tool schema 确认能力面,再用二进制字符串与运行行为反推 runtime 结构
Workflow 的公开契约
最新版 workflow 的核心入口,是 sdk-tools.d.ts 里的 WorkflowInput 与 WorkflowOutput。
WorkflowInput 的四个关键字段
1. script
注释写得明确:
- 必须以
export const meta = { name, description, phases }开头 meta必须是纯字面量- 脚本体使用
agent()、parallel()、pipeline()、phase()
workflow 不是随便给一段 JS 就能跑,而是要求脚本满足一个固定编排语法面。
2. name
用来引用预定义 workflow。两种来源:
- built-in workflow
**.claude/workflows/**下的本地 workflow
运行时里存在一个统一的 workflow 解析入口:不管是内置模板还是工作区模板,最终都要被转换成”可执行脚本文本”。
3. scriptPath
最值得注意的字段之一。
每次 Workflow 调用都会把脚本持久化到 session 目录,并把 scriptPath 返回给调用方。后续如果要继续迭代,应当优先修改这个文件并再次以 scriptPath 调用,而不是反复发送整段脚本。
这个设计说明 workflow 不是”提示词即程序”,而是显式地把脚本对象落地成文件资产。
4. resumeFromRunId
- 恢复是 same-session only
- 已完成且
(prompt, opts)未变化的agent()调用,可直接命中缓存 - 只有新增或修改过的调用才重跑
- 恢复前必须先
TaskStop
这几条组合起来,说明 workflow runtime 不是简单顺序执行器,而是带节点级缓存与恢复语义的执行引擎。
Workflow 脚本 DSL 的本质
Claude Code 没有把 workflow 做成 YAML,也没有做成纯 JSON 配置,而是做成一套轻量 DSL。
graph TD
subgraph "Workflow DSL 结构"
META[meta: name + description + phases] --> PHASE1[phase '规划']
META --> PHASE2[phase '实现']
META --> PHASE3[phase '验证']
PHASE1 --> AG1["agent('分析需求')"]
PHASE2 --> PAR["parallel()"]
PAR --> AG2["agent('写前端')"]
PAR --> AG3["agent('写后端')"]
PHASE3 --> PIPE["pipeline()"]
PIPE --> AG4["agent('跑测试')"]
AG4 --> AG5["agent('修复问题')"]
end
最小组成:
meta:声明 workflow 的名字、描述、阶段信息phase():建立阶段边界agent():定义一个实际由子 agent 执行的工作单元parallel():在同一个阶段中并发派发多个子任务pipeline():让前一步输出进入后一步
从工程角度看,这个 DSL 有两个核心作用:
它不是给用户读的,而是给 runtime 编译的
agent()/parallel()/pipeline()/phase() 这些原语,不只是语义标签,而是执行器可以识别的结构节点。workflow 运行时不是”先把脚本转成自然语言计划,再由模型理解”,而是会根据这些原语构造实际执行拓扑。
它天然支持调度语义
phase()提供阶段性的同步点parallel()提供并发 fork 点pipeline()提供数据依赖链agent()提供执行单元边界
换句话说,这个 DSL 的真正价值是:把 prompt 编排问题,提升成结构化调度问题。
从调用到执行:workflow 主链路
flowchart TD
A[主 Agent 发起 Workflow tool call] --> B{来源归一化}
B -->|scriptPath| C[读取磁盘脚本]
B -->|name| D[解析 built-in / .claude/workflows/]
B -->|script| E[使用 inline 脚本]
C --> F[编译前检查]
D --> F
E --> F
F -->|结构检查| G{meta 合法?}
G -->|否| H[报错: Invalid workflow script]
G -->|是| I{确定性检查}
I -->|使用 Date.now/Math.random| J[报错: breaks resume]
I -->|通过| K[任务注册]
K --> L[分配 taskId + runId]
L --> M[持久化 scriptPath]
M --> N[后台执行器推进]
N --> O[按 phase 调度]
O --> P[agent 节点执行]
O --> Q[parallel 并发分支]
O --> R[pipeline 数据依赖]
P --> S{所有 phase 完成?}
Q --> S
R --> S
S -->|否| O
S -->|是| T[TaskCompleted]
1. 入口:由主 agent 发起 Workflow tool call
workflow 在运行时中表现为一个正式 tool。调用方通过 tool schema 传入:
script/name/scriptPath(三选一)- 可选
args - 可选
resumeFromRunId
这一步的输出不是完整任务结果,更像是一个”启动成功的任务句柄”。
2. 归一化:把不同来源解析成脚本文本
运行时需要先把三种来源统一起来:
scriptPath优先,直接读取磁盘脚本- 若没有,则看
name,解析成 built-in 或.claude/workflows/对应脚本 - 若仍没有,则使用 inline
script
这一步不只是”取脚本”,而是在确定后续所有恢复和缓存的锚点:
- 脚本文本决定要执行什么
scriptPath决定以后怎么重新加载runId决定当前执行实例是谁
3. 编译前检查:验证脚本是否满足恢复要求
二进制中能看到几条关键的错误提示:
export const meta = { name, description, phases } must be the FIRST statement in the script
meta must be a pure literal
Invalid workflow script:
Date.now() / new Date() are unavailable in workflow scripts (breaks resume)
Math.random() is unavailable in workflow scripts (breaks resume)
三层检查:
- 结构检查:
meta是否在第一句、字段是否合法 - 语法检查:脚本是否可编译成
workflow.js - 确定性检查:是否使用了破坏恢复能力的时间/随机接口
一个很硬的结论:恢复能力是 workflow 设计的前提,不是附加能力。
4. 任务注册:把 workflow 变成后台任务
WorkflowOutput.status 暴露的是 async_launched 和 remote_launched。调用 workflow 后,运行时更关心的是”任务是否成功发车”,而不是”是否已经跑完”。
二进制里出现 local_workflow、LocalWorkflowTask、TaskCreated、TaskCompleted、TaskStop 等字符串,说明 workflow 在本地执行时会进入一个专门的任务生命周期:
- 创建任务 → 分配
taskId→ 分配runId - 持久化脚本与执行上下文
- 交给后台执行器继续推进
workflow 调用更像 launch,而不是 call-and-wait。
5. 执行推进:按 phase 调度子 agent
从 DSL 与公开返回结构看,执行期语义已经清楚:
agent()是最小执行单元phase()把多个单元组织成阶段parallel()让一个阶段内出现并发分支pipeline()明确输出依赖关系
运行时至少要维护:
- 当前 phase 指针
- 每个
agent()节点的状态 - 并发分支的完成情况
- 每个节点的输出缓存
- 整个 run 的停止/恢复信息
workflow 本质上不是”单次大模型推理”,而是一个真正的任务图执行器。
为什么它能恢复
恢复能力是最新版 workflow 最值得研究的部分。
flowchart LR
subgraph 首次执行
A1[脚本持久化 scriptPath] --> A2[执行生成 runId]
A2 --> A3[节点逐个执行]
A3 --> A4[每个节点缓存 prompt+opts+result]
end
subgraph 恢复流程
B1[TaskStop 停止旧 run] --> B2[传入 resumeFromRunId]
B2 --> B3[装载 scriptPath]
B3 --> B4[重新生成任务图]
B4 --> B5{对比节点签名}
B5 -->|prompt+opts 未变| B6[命中缓存 ✓ 跳过]
B5 -->|新增或修改| B7[重新执行]
end
A4 -.->|状态持久化| B2
scriptPath 是”程序”锚点
恢复时不能只靠 runId。runId 只能告诉 runtime”你要接哪次执行”,但不能告诉它”程序定义是什么”。
所以运行时把脚本持久化成 scriptPath,让恢复过程可以重新读到同一份 workflow 脚本,再进行重新解析和编译。
runId 是”实例”锚点
runId 负责定位某次具体执行:
- 哪些 phase 已经完成
- 哪些
agent()节点已经成功 - 哪些结果已经写入缓存
- 当前停止点在哪里
没有它,就无法在后续调用里指定”从哪次执行继续”。
恢复不是全量重跑,而是增量重放
resumeFromRunId 的核心机制:
已完成且
(prompt, opts)未变化的agent()节点直接复用缓存;只有修改过或新增的节点才重跑。
runtime 内部至少保留了:
- 每个
agent()节点的输入签名 - 节点执行结果
- 节点完成状态
- 这些状态与特定
runId的绑定关系
恢复的实际语义不是”从头跑一遍”,而是:
- 重新装载脚本
- 重新生成任务图
- 根据旧
runId对齐已完成节点 - 命中未变化节点的缓存
- 只对新增或变更部分继续执行
为什么禁用 Date.now() 和 Math.random()
因为这两类接口会破坏”相同脚本 + 相同输入 = 相同执行边界”的假设。
一旦允许时间和随机性,运行时就很难判定:
- 一个节点是否真的”没有变化”
- 旧缓存是否仍然安全可复用
- 恢复后跳过哪些步骤不会导致语义漂移
这类限制不是保守,而是恢复系统成立的基础条件。
TaskStop 的角色
resumeFromRunId 明确要求:恢复前要先 TaskStop 停掉旧运行。
TaskStop 在 workflow 体系里的意义不只是”停止执行”,而是:
- 给当前 run 一个明确的终止边界
- 确保不会出现两个执行器同时推进同一个 workflow
- 把当前已完成状态稳定写回运行态
- 为后续 resume 留下合法切点
更接近 checkpoint-aware interrupt,而不是简单 kill。
Built-in 与本地 workflow 的统一抽象
从 name 字段注释和二进制里的 autopilot、bugfix 等文本痕迹看,运行时内部存在内置 workflow 库。
关键在于,这些内置 workflow 并没有走独立的硬编码分支,而是被统一纳入 name → script → Workflow runtime 这条链路。
系统在设计上追求的是:
- 定义方式统一:都以脚本形式存在
- 解析方式统一:都进入相同的编译/校验流程
- 执行方式统一:都被注册成 workflow 任务
- 恢复方式统一:都可绑定
scriptPath/runId语义
只有统一到同一条管道,workflow 才能真正扩展,不会出现”内置逻辑一套、用户自定义一套”的双轨系统。
Runtime 形态判断
代码中明确存在的部分
- workflow 是独立 tool,不是提示词隐式模式
- workflow 使用显式 DSL:
meta + phase()/agent()/parallel()/pipeline() - 运行结果是异步任务句柄,不是同步最终值
- 支持
scriptPath持久化与runId恢复 - 恢复机制依赖节点级缓存与受控停止
- 运行时明确要求脚本确定性
高可信推断
- runtime 内部维护的是 phase / agent 节点级状态图
parallel()会被真正映射成并发调度,不是 prompt 上的”你并行处理一下”pipeline()会被当作数据依赖边,不是普通顺序列表agent()节点对应独立子任务或子会话执行单元- 缓存命中逻辑至少依赖
(prompt, opts),可能还会叠加脚本或上下文签名
最终判断
这套 workflow runtime 更像:
DSL 编译器 + 后台任务系统 + 节点级缓存恢复机制 的混合体。
不是传统有限状态机,也不是简单 planner,更不是只靠模型记忆流程。它真正的价值在于:把 agent 编排转成一个可被运行时管理的程序。
工程取舍
用脚本约束换取恢复能力
得到:可以 resume、可以缓存 step 结果、可以做增量重跑
代价:脚本不能随意写、不能依赖随机数和当前时间、meta 和 DSL 结构必须严格受控
用后台任务换取长流程编排能力
得到:可承载多阶段任务、可停止/恢复/观察、不阻塞前台交互线程
代价:需要维护任务生命周期、调用方拿到的是句柄不是即时结果、状态管理复杂度上升
用统一脚本抽象换取扩展性
得到:built-in 与本地 workflow 共用执行链路、扩展新 workflow 不必改主执行器、用户自定义和官方内置共享调度机制
代价:运行时需要维护脚本解析、校验、执行、恢复整套体系
可复用的 5 个设计模式
-
把多步 agent 任务升格为 DSL — 只要有
phase / parallel / pipeline / agent这类原语,系统就能从”聊天策略”进化为”程序化编排” -
把恢复能力前置到脚本语义层 — 禁止非确定性接口,比事后补 checkpoint 更稳
-
把”程序定义”和”运行实例”拆开 —
scriptPath表示定义,runId表示实例。利于调试、恢复和审计 -
缓存到节点级,不缓存到整体级 — 只有这样,恢复时才能跳过已稳定节点,只重跑变化部分
-
统一内置模板与用户扩展的执行管道 — 这是 workflow 系统能长期扩展的关键
下一步最有价值的实验
如果要继续深入,最有效的是直接做一次动态实验:
- 构造一个最小 workflow 脚本
- 跑一次
- 在中途
TaskStop - 用
resumeFromRunId恢复 - 观察 session 目录、脚本持久化文件、任务状态和 transcript 的实际落盘
这一步能直接验证:scriptPath 实际存在哪、runId 如何关联状态、恢复时哪些节点被跳过、phase 进度如何持久化。
一句话总结
最新版 Claude Code 的 workflow,本质上不是”模型自己分步骤完成任务”,而是运行时把受约束的 workflow 脚本编译成一个可恢复的后台任务图,再用 phase/subagent 级状态推进执行、停止与续跑。
Keep Reading