本文基于一次 Claude Code workflow 的 Reqable 抓包和本地运行产物,拆解 Claude Code dynamic workflow 的实现原理、工作机制和 agent 交接方式。
一句话概括:Claude Code workflow 的本质不是“模型在一次请求里并发思考”,而是“主模型生成一段 JavaScript 编排脚本,Claude Code 本地 runtime 执行脚本并启动多个普通子代理会话”。子代理通过 StructuredOutput 返回结构化 JSON,本地 runtime 再把这些 JSON 作为普通 JavaScript 值传给后续阶段,最终把脚本的 return 值通过 <task-notification> 回灌主会话。
本仓库保留了同一次 workflow 运行的两类材料:
- 原始抓包:
reqable/ - 整理后的证据:
workflow-artifacts/
本次运行的关键 ID:
| 字段 | 值 |
|---|---|
| Session ID | 915155ec-d5cc-44ba-b5eb-fb3e513a1d8f |
| Workflow run ID | wf_1b6a0439-04e |
| Task ID | wts66al8s |
| 主题 | 桌游设计锦标赛:并行发明概念,评委团打分,综合冠军设计稿 |
关键证据文件:
| 文件 | 说明 |
|---|---|
workflow-artifacts/workflow-tool-prompt.md |
主模型看到的 Workflow 工具提示词,包含触发规则、JS DSL、并发语义、恢复规则和质量模式。 |
workflow-artifacts/workflow-tool-schema.json |
主模型看到的 Workflow 工具 schema。 |
workflow-artifacts/main-workflow-tool-use.json |
主模型实际发出的 tool_use: Workflow,其中包含完整内联 JS。 |
workflow-artifacts/boardgame-tournament-wf_1b6a0439-04e.js |
Claude Code 持久化保存的本次 workflow JS。 |
workflow-artifacts/subagent-system-prompt.md |
注入 workflow 子代理的系统提示词。 |
workflow-artifacts/structured-output-schemas/ |
agent(..., { schema }) 传给 StructuredOutput 的 JSON schema。 |
workflow-artifacts/subagent-prompts/ |
去重后的 21 个逻辑子代理 prompt。 |
workflow-artifacts/examples/ |
子代理 StructuredOutput 工具调用和 tool_result 回灌样例。 |
workflow-artifacts/journal.jsonl |
本地 workflow runtime 的 agent start/result 日志。 |
workflow-artifacts/workflow-task-notification.md |
workflow 完成后注入主会话的最终通知。 |
除非特别说明,下面的结论都来自这些抓包、提示词、JS 脚本和本地运行文件。无法从材料直接证明的内部实现细节,会明确标注为推断。
Claude Code workflow 可以分成四层:
- 主会话层:主模型看到一个名为
Workflow的本地工具。 - 编排脚本层:主模型生成一段 JavaScript DSL,描述 fan-out、pipeline、评审、聚合和最终返回。
- 本地 runtime 层:Claude Code 持久化并执行这段 JS,负责并发、缓存、日志、schema 校验和恢复。
- 子代理层:每个
agent()变成一个普通 Messages API 子会话,子代理通过StructuredOutput工具返回结构化结果。
流程可以画成这样:
用户显式触发 workflow
-> 主会话把 Workflow 工具暴露给主模型
-> 主模型生成 JavaScript workflow script
-> 主模型调用 tool_use: Workflow({ script })
-> Claude Code 本地 runtime 持久化 script 并启动后台任务
-> runtime 执行 JS DSL: agent / pipeline / parallel / phase / log
-> 每个 agent() 启动一个子代理 Messages API 请求
-> 子代理调用 StructuredOutput 返回 JSON
-> runtime 校验 JSON 并把它作为 agent() 的 resolved value
-> JS 代码计算、排序、拼 prompt、继续启动后续 agent
-> JS 最终 return 一个对象
-> Claude Code 通过 <task-notification> 把结果回灌主会话
这个结构里,模型真正“设计”的是 workflow 脚本;并发执行、进度显示、子代理启动、结构化返回和最终通知,都是 Claude Code 本地 runtime 的职责。
在主会话请求里,Claude Code 把 Workflow 当作一个普通工具交给主模型。它的 schema 见 workflow-tool-schema.json,核心输入字段包括:
| 字段 | 作用 |
|---|---|
script |
内联 workflow JavaScript。 |
name |
运行内置或 .claude/workflows/ 下的命名 workflow。 |
args |
直接传给脚本的参数,在脚本中以全局 args 访问。 |
scriptPath |
从本地脚本路径运行 workflow。 |
resumeFromRunId |
基于已有 run 恢复,复用未变更 agent 的缓存结果。 |
提示词明确要求:首次调用通常通过 script 传入内联 JS,不要先写文件。Claude Code 会自动把脚本持久化到 session 目录,并在工具返回中给出路径。后续迭代可以编辑这个文件,再通过 scriptPath 运行。
本次主模型实际调用保存在 main-workflow-tool-use.json。调用后,工具没有同步返回最终设计稿,而是先返回后台任务信息,见 workflow-launch-tool-result.md:
Workflow launched in background. Task ID: wts66al8s
Run ID: wf_1b6a0439-04e
Script file: .../boardgame-tournament-wf_1b6a0439-04e.js
这证明 Workflow 是后台任务模型:第一次工具结果只是“任务已启动”和“如何恢复”,最终业务结果会在完成后另行通知。
Workflow 工具提示词对触发条件非常严格。主模型只能在用户明确 opt-in 时调用 workflow。有效 opt-in 包括:
- 用户消息包含
workflow或workflows,并且系统提醒确认。 - Ultracode 开启,并且系统提醒确认。
- 用户明确说要运行 workflow、多代理编排、fan out agents。
- 某个 skill 或 slash command 指示调用
Workflow。 - 用户要求运行特定命名 workflow。
提示词还强调:即使任务明显受益于并行,只要用户没有 opt-in,也不能擅自调用 Workflow。模型应该使用单个 Agent 工具,或者先解释 workflow 能做什么、预计成本,再询问用户是否运行。
这个设计说明 Claude Code 把 workflow 视为高成本、多代理的显式能力,而不是普通默认行为。
本次生成的 workflow 脚本是 boardgame-tournament-wf_1b6a0439-04e.js。它看起来像普通 JS,但运行环境是受限 DSL:
- 必须以
export const meta = {...}开头。 meta必须是纯字面量,不能有变量、函数调用、spread 或模板插值。- 脚本体处于 async context,可以直接
await。 - 可以使用
JSON、Math、Array等普通 JS 内置对象。 - 不能使用 Node.js API 和文件系统 API。
- 不能使用
Date.now()、Math.random()、无参new Date(),因为这些会破坏恢复缓存。 - 脚本是 JavaScript,不是 TypeScript,类型标注、interface、泛型都会解析失败。
meta 负责 workflow 的展示信息:
export const meta = {
name: 'boardgame-tournament',
description: '桌游设计锦标赛:并行发明概念 → 评委团打分 → 综合出冠军设计稿',
phases: [
{ title: 'Invent', detail: '5 个不同设计哲学的智能体并行发明桌游概念' },
{ title: 'Judge', detail: '每个概念由 3 个不同视角的评委打分' },
{ title: 'Synthesize', detail: '取冠军概念并嫁接落选者最佳创意,产出完整设计稿' },
],
}meta.phases 和后面的 phase()、agent({ phase }) 配合,用于 workflow UI 的进度分组。
Workflow 提示词定义了一组脚本 hook。它们是 workflow 的核心编程模型。
agent(prompt, opts) 启动一个子代理。
常用 opts:
| 字段 | 说明 |
|---|---|
label |
进度树里的 agent 标签。 |
phase |
显式归属阶段。 |
schema |
强制子代理通过 StructuredOutput 返回符合 schema 的 JSON。 |
model |
可选模型覆盖。 |
isolation: 'worktree' |
为会修改文件的并行 agent 创建隔离 git worktree。 |
agentType |
使用自定义子代理类型。 |
没有 schema 时,agent() 返回子代理最终文本。有 schema 时,子代理会被强制调用 StructuredOutput,agent() 返回校验后的对象,不需要父流程再解析自然语言。
pipeline(items, stage1, stage2, ...) 是提示词推荐的默认多阶段模式。它的关键特征是没有阶段间全局 barrier。
也就是说,item A 完成 stage 1 后可以立即进入 stage 2,不需要等待 item B、C、D 全部完成 stage 1。这样 wall-clock 更接近“最慢单个 item 的完整链路”,而不是“每个阶段最慢任务之和”。
本次脚本就是典型 pipeline:
const results = await pipeline(
LENSES,
(lens) => agent(...invent prompt...),
(concept, lens) => parallel(...judge agents...)
)含义是:5 个设计方向并行发明概念;某个概念一生成,就立刻进入评审阶段,不等其他概念全部生成完。
parallel(thunks) 并行执行多个 thunk,并等待全部结束。它是 barrier。
提示词强调,只有当下一步确实需要全部结果时才使用 barrier,例如:
- 需要跨全部结果去重或合并。
- 需要知道总数是否为 0 来整体跳过下一阶段。
- 下一阶段 prompt 需要引用“其他发现”。
本次脚本在每个概念的 3 个评委之间使用 parallel:
parallel(
JUDGES.map((j) => () =>
agent(...judge prompt..., { schema: JUDGE_SCHEMA })
)
).then((votes) => {
const valid = votes.filter(Boolean)
const avg = valid.length ? valid.reduce((s, v) => s + v.score, 0) / valid.length : 0
return { concept, lens: lens.key, votes: valid, avg }
})这里 barrier 是合理的,因为计算平均分必须等同一个概念的 3 个评委都返回。
phase(title) 用来切换阶段;log(message) 用来在 workflow 进度中输出叙事信息。
本次脚本使用 log() 标记开赛和冠军概念,并在综合阶段前调用 phase('Synthesize')。
工具提示词还定义了三个更高级的 hook:
args: 从Workflow工具输入传给脚本的参数,必须传真实 JSON,不要传 JSON 字符串。budget: 读取本轮 token 目标、已花费和剩余额度,可用来动态控制 agent 数量或循环深度。workflow(nameOrRef, args): 在当前 workflow 内嵌套运行另一个 workflow,子 workflow 共享并发上限、agent 计数、abort signal 和 token budget。只允许一层嵌套。
本次桌游示例没有使用这三个高级能力,但它们说明 workflow 脚本可以被参数化、受预算约束,也可以组合已有 workflow。
本次 workflow 是一个“设计锦标赛”。
脚本中定义了 5 个设计方向:
const LENSES = [
{ key: 'coop-survival', desc: '对抗不断升级威胁的合作生存玩法' },
{ key: 'social-deduction', desc: '隐藏身份、虚张声势的社交推理玩法' },
{ key: 'tile-spatial', desc: '空间拼块铺设与领地建设玩法' },
{ key: 'economic-engine', desc: '经济引擎构筑与资源转化玩法' },
{ key: 'dexterity-realtime', desc: '手部灵巧度与实时动作玩法' },
]又定义了 3 个评审维度:
const JUDGES = [
{ key: 'originality', desc: '原创性与新颖度...' },
{ key: 'fun', desc: '趣味性与重玩价值...' },
{ key: 'clarity', desc: '规则清晰度与上手门槛...' },
]逻辑 agent 数量:
| 阶段 | 数量 | 说明 |
|---|---|---|
| Invent | 5 | 每个设计方向一个发明 agent。 |
| Judge | 15 | 每个概念由 3 个评委独立打分,5 x 3。 |
| Synthesize | 1 | 取冠军概念,嫁接落选概念优点,生成最终单页设计稿。 |
| 合计 | 21 | 与最终通知中的 agent_count 一致。 |
最终 workflow-task-notification.md 中的 usage 也记录了:
<usage>
<agent_count>21</agent_count>
<subagent_tokens>232900</subagent_tokens>
<tool_uses>23</tool_uses>
<duration_ms>257654</duration_ms>
</usage>注意:逻辑 agent 数不等于 HTTP 请求数。一个逻辑 agent 可能因为工具调用、tool_result continuation、重试等产生多次 Messages API 请求。因此抓包文件数量会多于 21。
从抓包看,workflow 子代理请求不是把所有指令都拼成一个巨大的纯文本 prompt。更准确的理解是:Claude Code 本地 runtime 在发起 POST /v1/messages?beta=true 时,把不同来源的内容放进 Anthropic Messages API 的不同槽位。
以 invent 请求 reqable/[448]、judge 请求 reqable/[493] 和 synthesize 请求 reqable/[682] 对照,可以看到请求大致由这些块组成:
| 请求位置 | 内容来源 | 作用 |
|---|---|---|
| HTTP headers | Claude Code CLI / 本地代理 | session id、beta flags、版本、认证占位等传输元信息。 |
model |
workflow agent 配置 | 本次是 claude-opus-4-8。如果 agent(..., { model }) 覆盖,理论上会影响这里。 |
messages[0].role = user |
runtime 组装 | 子代理本轮 user 输入。里面通常包含一个 <system-reminder> 上下文块,再跟 JS agent(prompt) 生成的任务 prompt。 |
messages[0].content[0].text |
Claude Code 会话上下文 | <system-reminder>,包含用户全局指令、当前日期等。 |
messages[0].content[1].text |
workflow JS 的 agent(prompt) 求值结果 |
真正的子任务说明,例如“请发明一个原创桌游概念...”或“你是桌游评委...”。 |
system[] |
Claude Code runtime | 顶层 system 消息,包括 Claude Code 身份、workflow subagent 系统提示词、环境信息等。 |
tools[] |
Claude Code runtime + agent(..., { schema }) |
子代理可用工具。普通工具来自 Claude Code;StructuredOutput 工具由 schema 动态生成。 |
tools[].StructuredOutput.input_schema |
workflow JS 的 schema 对象 | 强制子代理最终返回的结构。schema 不放在 user prompt 里,而是作为工具 schema 进入请求。 |
metadata / output_config / max_tokens |
Claude Code runtime | 用户/session 元数据、effort、输出上限等运行配置。 |
也就是说,一次 workflow subagent 请求更像这个伪代码:
function buildWorkflowAgentRequest({ prompt, schema, model, sessionContext, toolRegistry }) {
return {
model: model ?? resolvedSessionModel,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: renderSystemReminder(sessionContext),
},
{
type: 'text',
text: prompt,
cache_control: maybeEphemeral(prompt),
},
],
},
],
system: [
{ type: 'text', text: renderBillingOrRuntimeHeader() },
{ type: 'text', text: "You are Claude Code, Anthropic's official CLI for Claude." },
{ type: 'text', text: renderWorkflowSubagentSystemPrompt() },
],
tools: [
...baseClaudeCodeTools(toolRegistry),
schema && {
name: 'StructuredOutput',
description: 'Use this tool to return your final response in the requested structured format...',
input_schema: schema,
},
].filter(Boolean),
output_config: { effort: 'xhigh' },
stream: true,
}
}这段伪代码不是 Claude Code 源码,只是根据抓包结构推算出的组装模型。抓包能直接证明的是:任务 prompt 在 messages[0].content 中,workflow subagent 规则在顶层 system[] 中,StructuredOutput schema 在 tools[] 中。
本次 workflow 的业务 prompt 不是 Claude Code runtime 自己写死的,而是主模型生成的 JS 模板字符串。runtime 执行 JS 时,对 agent(prompt, opts) 的第一个参数做正常 JavaScript 求值,得到最终 user prompt。
invent 阶段来自脚本里的模板:
(lens) => agent(
`请发明一个原创桌游概念,核心围绕「${lens.desc}」。\n` +
`要求:具体、富有创意;起一个令人难忘的名字;给出一个真正新颖的核心钩子。\n` +
`避免陈词滥调,不要照搬任何已有游戏。用中文作答。`,
{ label: `invent:${lens.key}`, phase: 'Invent', schema: CONCEPT_SCHEMA }
)当 lens.desc = '空间拼块铺设与领地建设玩法' 时,请求里的任务 prompt 就变成:
请发明一个原创桌游概念,核心围绕「空间拼块铺设与领地建设玩法」。
要求:具体、富有创意;起一个令人难忘的名字;给出一个真正新颖的核心钩子。
避免陈词滥调,不要照搬任何已有游戏。用中文作答。
这个结果和 subagent-prompts/448-invent.md 以及 reqable/[448] 中的 messages[0].content[1].text 对得上。
judge 阶段的 prompt 则把上游 agent 的结构化 JSON 再序列化进文本:
agent(
`你是桌游评委,专门从这个维度评审:「${j.desc}」。\n` +
`待评概念:\n${JSON.stringify(concept, null, 2)}\n` +
`请给出 1-10 分并简述理由。用中文作答。`,
{ label: `judge:${lens.key}:${j.key}`, phase: 'Judge', schema: JUDGE_SCHEMA }
)这里的 concept 不是自然语言摘要,而是 invent agent 通过 StructuredOutput 返回的 JSON 对象。runtime 把它作为 JS 值传给下一阶段,然后由 JSON.stringify(concept, null, 2) 变成 judge prompt 中的格式化 JSON。抓包 reqable/[493] 里可以看到完整嵌入后的概念对象。
synthesize 阶段同理,只是拼进去的是冠军和落选概念数组:
agent(
`请把这个胜出的桌游概念打磨成一份完整的单页游戏设计稿。\n\n` +
`【冠军概念】\n${JSON.stringify(winner.concept, null, 2)}\n\n` +
`【落选概念(可酌情各取其最佳一个创意嫁接,使设计更强)】\n` +
`${JSON.stringify(runnersUp.map((r) => r.concept), null, 2)}\n\n` +
`产出要素:组件清单、开局布置、回合流程、获胜条件、招牌特色机制。用中文作答。`,
{ label: 'synthesize', phase: 'Synthesize', schema: SYNTH_SCHEMA }
)所以 prompt 拼接的关键不是“把所有 agent 历史都塞给下一个 agent”,而是“由 JS 明确选择哪些结构化字段进入下一个 prompt”。在本次例子里:
- invent agent 只看到自己的设计方向,不知道其他 4 个 invent agent。
- judge agent 只看到一个待评概念和自己的评审维度,不知道其他评委的评分。
- synthesize agent 看到冠军概念和落选概念列表,但看不到所有 judge rationale,除非 JS 作者显式把
votes也 stringify 进去。
这点很重要:workflow 的上下文传播是显式的、由 JS 脚本控制的,不是自动共享所有子代理 transcript。
workflow 子代理不是直接沿用主会话系统提示词。Claude Code 会注入专门的 workflow subagent system prompt,见 subagent-system-prompt.md。
核心规则是:
You MUST call the StructuredOutput tool exactly once to return your final answer.
Do NOT put your answer in a text response.
The script reads ONLY the StructuredOutput tool call.
If the schema validation fails, read the error and call StructuredOutput again.
这段提示词证明:在 agent(prompt, { schema }) 模式下,子代理的最终结果不是自然语言文本,而是一次 StructuredOutput 工具调用。父级 JS runtime 只读取这个工具调用里的 input。
系统提示词还给子代理加了几条 workflow 专属约束:
- bash 调用之间 cwd 会重置,所以涉及路径要用绝对路径。
- 最终响应里应提供相关绝对路径。
- 不要创建报告、总结、发现、分析类
.md文件。 - 成功调用
StructuredOutput后结束本轮,不需要再补确认。
这些规则说明 workflow subagent 是一个被父流程读取返回值的执行单元,不是一个面向用户聊天的助手。
本次所有 agent() 都带了 schema,所以每个子代理都有一个 StructuredOutput 工具。schema 来自 workflow JS 中定义的对象,并被整理到了:
structured-output-schemas/concept-schema.jsonstructured-output-schemas/judge-schema.jsonstructured-output-schemas/synthesize-schema.json
以 invent 阶段为例,CONCEPT_SCHEMA 要求子代理返回:
| 字段 | 说明 |
|---|---|
name |
游戏名。 |
tagline |
一句话标语。 |
players |
玩家人数。 |
playtime |
游戏时长。 |
coreMechanic |
核心机制。 |
howItWorks |
玩法概述。 |
hook |
真正新颖的钩子。 |
examples/invent-structured-output-handoff-492.json 展示了一次完整交接:
- assistant 先输出简短思考文本。
- assistant 调用
tool_use: StructuredOutput。 StructuredOutput.input是完整的桌游概念 JSON。- user 角色回灌
tool_result: Structured output provided successfully。
这之后,JS runtime 在 journal.jsonl 里记录该 agent 的 result。后续阶段使用的是 StructuredOutput.input 对象,而不是 assistant 文本。
严格说,子代理之间没有直接通信。它们不会互相发消息,也不共享完整上下文。交接由本地 JS runtime 通过普通数据流完成。
本次流程是:
- invent agent 返回
conceptJSON。 pipeline把这个concept作为下一阶段函数参数传入。- judge 阶段用
JSON.stringify(concept, null, 2)把概念嵌入新的 prompt。 - 3 个 judge agent 分别返回
{ score, rationale }。 - JS 计算平均分,返回
{ concept, lens, votes, avg }。 - 所有 pipeline item 结束后,JS 排序得到
ranked。 - synthesize prompt 嵌入
winner.concept和runnersUp.map((r) => r.concept)。 - synthesize agent 返回最终设计稿 JSON。
- JS 最终
return { winner, ranking, allConcepts, finalDesign }。
所以 workflow 的“agent 交接”本质是:
StructuredOutput JSON
-> JS resolved value
-> JS 计算/排序/拼接 prompt
-> 下一个 agent 的输入 prompt
这和很多人直觉里的“多个 agent 在同一个群聊中互相讨论”不同。Claude Code workflow 更像一个确定性的 orchestration runtime,agent 是异步函数调用,prompt 是输入,StructuredOutput JSON 是返回值。
journal.jsonl 记录了本地 runtime 的 agent 生命周期。典型记录有两类:
{"type":"started","key":"...","agentId":"..."}
{"type":"result","key":"...","agentId":"...","result":{...}}invent agent 的 result 是一个完整概念对象;judge agent 的 result 是 { score, rationale };synthesize agent 的 result 是最终设计稿对象。
workflow-final-state.json 是更完整的本地状态文件,包含:
runIdtimestamptaskIdscriptscriptPathresult- agent 元信息
- token 和工具调用统计
这说明 workflow runtime 不只是把模型输出流式转发给用户,而是维护了一份可检查、可恢复的本地运行状态。
由于 Workflow 是后台任务,主模型第一次调用工具时不会拿到最终结果。任务完成后,Claude Code 注入一条新的 user message,格式是 <task-notification>,见 workflow-task-notification.md。
它包含:
| 字段 | 作用 |
|---|---|
task-id |
对应启动阶段返回的后台任务 ID。 |
tool-use-id |
主模型最初调用 Workflow 工具时的 tool use ID。 |
output-file |
本地保存 workflow 输出的文件路径。 |
status |
本次为 completed。 |
summary |
workflow 的人类可读摘要。 |
result |
workflow JS 最终 return 的 JSON 序列化结果。 |
usage |
子代理数量、token 消耗、工具调用数和耗时。 |
这一步是 workflow 和主会话之间的最终交接。主模型收到这条通知后,再基于 result 向用户总结结果。
Workflow 工具提示词和启动返回都说明支持恢复:
Workflow({
scriptPath: ".../boardgame-tournament-wf_1b6a0439-04e.js",
resumeFromRunId: "wf_1b6a0439-04e"
})恢复规则是:未变更的最长 agent 调用前缀直接返回缓存结果;第一个变更或新增调用及其后续步骤重新运行。同一个 script 加同一个 args 可以 100% 命中缓存。
这也解释了为什么 workflow 脚本禁止 Date.now()、Math.random() 和无参 new Date()。这些非确定性值会破坏“同样脚本和同样输入对应同样 agent 调用”的缓存假设。
从设计上看,resume 依赖三个条件:
- 脚本控制流是确定性的。
agent()调用的 prompt 和 opts 可被稳定 hash。- 本地 journal 或 state 能保存已完成 agent 的结果。
第 1、2 点来自工具提示词和脚本限制;第 3 点可以从 journal.jsonl 和 workflow-final-state.json 看到证据。
Workflow 提示词不只是 API 文档,它还写入了很多编排模式偏好。
提示词反复强调 DEFAULT TO pipeline()。原因很实际:如果阶段之间没有全量依赖,就不应该让快的 item 等慢的 item。
错误倾向是:
const a = await parallel(...)
const b = a.map(...).filter(...)
const c = await parallel(b.map(...))如果中间只是 map/filter/flatten,不需要等待全量结果。应把转换放进 pipeline stage,让每个 item 自己往下流。
barrier 合理场景包括:
- 全量去重后再验证。
- 总数为 0 时整体跳过后续工作。
- 后续 prompt 需要“其他发现”作为对比上下文。
本次对每个概念的 3 个评委使用 barrier,就是因为平均分必须等三个评分都回来。
本次桌游 workflow 实际采用了提示词推荐的 Judge panel 模式:
多个方向独立生成
-> 多个评委并行评分
-> 选出 winner
-> 综合 winner 并嫁接 runners-up 优点
这个模式适合解空间很宽的任务,因为单次迭代容易困在一个方案里;多方案并行加评委打分能扩大探索面。
workflow 里最重要的工程选择之一,是让子代理返回 schema 校验后的 JSON,而不是让父流程从自然语言中提取信息。
这带来几个好处:
- 后续 JS 不需要写脆弱的文本解析。
- schema 失败可以在工具调用层重试。
- 结果可以直接
sort、map、reduce、JSON.stringify。 - journal 和 final state 里保存的是结构化对象,便于恢复和检查。
Claude Code 主会话中也有普通 Agent 工具。两者的边界大致是:
| 能力 | Agent | Workflow |
|---|---|---|
| 启动方式 | 主模型委托一个独立 agent。 | 主模型生成 JS 编排脚本。 |
| 适合场景 | 单个探索、单个审查、单个实现任务。 | fan-out、pipeline、评分、对抗验证、大范围迁移、审计。 |
| 控制流 | 主要由模型自然语言驱动。 | 由 JS 明确控制 loops、conditionals、parallel、pipeline。 |
| 返回值 | 通常是文本。 | 可通过 schema 强制结构化 JSON。 |
| 成本 | 通常较低。 | 可能启动几十个 agent,token 成本高。 |
| 使用条件 | 普通委托即可。 | 需要用户显式 opt-in。 |
所以 workflow 不是 Agent 的替代品,而是当任务需要确定性多代理控制流时的更高层编排工具。
从本次证据可以确认:
Workflow是 Claude Code 暴露给主模型的本地工具。- 主模型负责生成 workflow JS,并通过
tool_use: Workflow交给本地 runtime。 Workflow调用后先启动后台任务,立即返回 task ID、run ID、script path。- workflow JS 不是完整 Node.js 程序,而是受限的 JavaScript DSL。
agent(prompt, { schema })会启动 workflow 子代理,并强制它调用StructuredOutput。- 子代理返回的是 schema 校验后的 JSON,不是父流程解析后的自然语言。
pipeline和parallel的语义由本地 runtime 实现,主模型只是在 JS 中声明控制流。- 子代理之间不直接通信,交接由 JS runtime 通过 JSON 值和新 prompt 完成。
- 最终结果通过
<task-notification>作为新的 user message 回灌主会话。 - 本地 journal 和 final state 记录了 agent 开始、结果、脚本、最终输出和统计信息。
有些内部细节从抓包和本地文件不能完全证明,只能基于现象推断:
- runtime 内部如何 hash
agent()调用来命中缓存。 - 并发调度器的具体实现代码。
StructuredOutputschema 校验失败后的重试细节在源码层如何实现。/workflowsUI 如何读取 progress tree。- agent transcript 文件与 journal 的精确写入顺序。
不过这些不影响本文的核心结论:Claude Code workflow 的可见机制已经足以证明它是“模型生成脚本,本地 runtime 执行脚本,子代理结构化返回”的架构。
如果想按证据自己复盘,可以按这个顺序读:
- 读
workflow-tool-prompt.md,理解主模型看到的完整规则。 - 读
workflow-tool-schema.json,确认工具输入字段。 - 读
main-workflow-tool-use.json,看主模型实际传入的 JS。 - 读
boardgame-tournament-wf_1b6a0439-04e.js,理解本次 workflow 的控制流。 - 读
subagent-system-prompt.md,看子代理为什么必须调用StructuredOutput。 - 读
subagent-prompts/index.md,看 21 个逻辑 agent 的输入 prompt。 - 读
examples/invent-structured-output-handoff-492.json,看一次完整的结构化输出交接。 - 读
journal.jsonl,看 runtime 如何记录 agent started/result。 - 读
workflow-task-notification.md,看最终结果如何回灌主会话。
Claude Code workflow 把多代理协作拆成了两个清晰层次:
- 模型层:主模型负责设计编排脚本,子模型负责完成具体子任务。
- 工程层:本地 runtime 负责确定性控制流、并发、schema 校验、缓存、日志和结果回灌。
这个设计的关键价值是把“多代理协作”从纯自然语言协议,变成了可执行、可恢复、可检查的数据流程序。agent() 像异步函数,prompt 是输入,StructuredOutput JSON 是返回值,pipeline() 和 parallel() 是调度结构,最终 return 是 workflow 的产品。
这也解释了为什么它适合审计、迁移、研究、评审、设计锦标赛这类任务:它不是让一堆 agent 随机聊天,而是让主模型写出一个确定性的多代理实验框架,再由 Claude Code 本地 runtime 把这个框架执行到底。