在 Codex 中实现 Dynamic Workflow:把多 Agent 编排写成代码
多 Agent 任务做得多了,我发现主 Agent 很容易同时承担两种工作:一边理解代码、判断问题,一边记住谁先做、谁后做、哪些结果还没有验证。任务变大之后,后者本身就会消耗不少上下文。
Codex Dynamic Workflow 是我围绕这个问题做的一个插件:用 JavaScript 表达调度,把需要模型判断的工作留在 agent() 调用里。本文以仓库 v0.2.0 的实现为例,介绍它怎样运行,以及使用时最容易误解的边界。
把编排从聊天中拿出来
以代码审查为例,可以让几个 Agent 分别检查正确性、接口兼容性和资源管理,再逐条验证发现,最后汇总。自然语言可以描述这个过程,但每次调度、等待、重试和收集结果都让主模型临场决定,流程就难以检查和复用。
写成程序后,循环次数、依赖关系、过滤条件和失败分支都有明确位置。模型仍然负责读代码和提出判断,程序负责决定这些判断如何进入下一步。
这里的“确定性”指编排规则可以检查和重放,不是模型每次都返回相同答案。工作流也不必是一张提前列完节点的静态图:下游任务数量可以由上游结果决定,新的发现可以产生新的验证调用,这就是它的动态部分。
运行链路:从插件入口到 Codex 子进程
插件分为三个层次:
| 层次 | 职责 |
|---|---|
| Skill 与 MCP 入口 | 帮助主 Agent 选择流程,并接收运行、校验、状态查询等请求 |
| JavaScript 运行引擎 | 执行控制流,管理并发、预算、结果校验和运行记录 |
| Codex CLI 适配层 | 将 agent() 转为 codex exec --json 子进程,解析事件和最终结果 |
这是插件在 Codex 外围增加的编排能力,不是改写 Codex 的原生调度器。MCP 提供调用入口;实际执行、并发和恢复逻辑在插件引擎中。
具体来说,引擎解析脚本的 meta,在受限上下文中注入 agent、pipeline、parallel 等函数。一次 agent() 调用经过参数解析、缓存检查和并发排队后,交给 CLI 适配层。适配层通过标准输入传递提示词,读取 JSONL 事件,提取最终输出、thread ID 和用量,再交回引擎。
这也意味着它不是把多段提示词塞进同一个聊天上下文。每次实际派发都有独立的 Codex 执行过程,进程启动和上下文准备都有成本。
一个可以运行的小例子
下面的脚本先按两个维度审查改动,再让独立执行者复核每个维度的结果。为了便于理解,例子按维度整体复核;更细的流程可以让上游返回发现列表,再逐条派发验证。
保存为 review-flow.js:
export const meta = {
name: 'review-flow',
description: 'Review changes and independently check each report',
};
const VERDICT = {
type: 'object',
required: ['confirmed', 'evidence'],
additionalProperties: false,
properties: {
confirmed: { type: 'boolean' },
evidence: { type: 'string' },
},
};
const dimensions = ['correctness', 'API compatibility'];
const revision = args.revision;
if (typeof revision !== 'string' || !/^[0-9a-f]{40}$/.test(revision)) {
throw new Error('Pass a full Git commit SHA in args.revision');
}
phase('Review and verify');
const results = await pipeline(
dimensions,
dimension => agent(
`Review commit ${revision} for ${dimension}. Read the code. ` +
'Report only concrete failure scenarios and evidence. Do not edit files.',
{ label: `review:${dimension}`, sandbox: 'read-only' },
),
(report, dimension) => agent(
`Independently check this ${dimension} report for commit ${revision}. ` +
'Try to disprove it using the code. Do not edit files. ' +
`Return confirmed=false if evidence is insufficient. Report:\n${report}`,
{
label: `verify:${dimension}`,
sandbox: 'read-only',
schema: VERDICT,
},
),
);
return {
confirmed: results.filter(result => result && result.confirmed),
incompleteDimensions: dimensions.filter((_, i) => results[i] === null),
};
准备好 Node.js 20 或更高版本,以及已配置可用的 Codex CLI。通过 npm 安装 CLI 后,在待审查的项目目录执行:
npm install -g codex-dynamic-workflow
cdw doctor
cdw validate ./review-flow.js
cdw run ./review-flow.js \
--args '{"revision":"REPLACE_WITH_FULL_COMMIT_SHA"}' \
--concurrency 2 --budget 20000
不想全局安装时,可以使用 npx codex-dynamic-workflow 代替 cdw。cdw init 可以生成一个只读入门示例,默认写入 workflow.js,不会覆盖已有文件;cdw doctor 检查本地依赖与目录,不调用模型,也不验证模型权限。npm CLI 不会自动注册 Codex 插件,会话内的 Skill 与 MCP 入口仍按仓库安装说明配置。
运行前将占位符替换为待审查提交的完整 SHA。validate 检查脚本结构,不调用模型;run 才真正派发。例子把提交写进输入,既明确审查对象,也让变更后的提交影响恢复缓存。它审查指定提交,不包含未提交改动。
脚本保留了 incompleteDimensions,因为调用失败返回空值和“没有发现问题”不能混为一谈。confirmed=false 也只代表这一条报告未被证据支持,不是对整个仓库正确性的证明。
pipeline 与 parallel:差别在等待关系
pipeline(items, ...stages) 让每个条目独立经过各阶段。正确性审查先完成,就可以立即进入验证,不必等兼容性审查。阶段参数包含前一阶段结果、原始条目和索引,便于保留上下文。
parallel(thunks) 则并发启动分支,并等齐结果再返回。只有汇总必须看齐所有结果时才需要这种屏障,例如多方案比较或跨结果去重。并行不意味着所有调用都会立即运行,实际派发仍受并发上限约束。
这两个函数决定依赖关系,phase() 主要组织进度展示。把阶段取不同名字,不会自动建立依赖;改变等待方式需要改变代码。
Schema 校验解决格式,不替你判断事实
带 schema 的调用通过 Codex CLI 的结构化输出接口,并在引擎内再次校验。格式或字段不符合要求时,引擎可以把错误反馈给模型进行有限纠正。v0.2.0 将进程失败重试与 Schema 纠正分开管理,默认额外 Schema 纠正次数为五次。
但 { confirmed: true, evidence: "..." } 结构合法,不代表结论正确。重要结论仍要对应文件、复现步骤或实际测试输出。结构化输出的价值,是让程序能够可靠读取结果并执行分支,而不是把自然语言判断变成事实保证。
终态执行失败会返回 null;流水线会跳过该条目后续阶段。参数错误等异常仍需处理。汇总时不能简单丢弃空值后宣称“全部通过”,而应报告缺失的覆盖范围。
断点恢复:复用未变的调用前缀
每次运行会保存脚本、调用记录与结果。恢复时,journal 按调用序号和输入指纹检查历史记录;只有成功且输入匹配的连续前缀可以复用。第一次不匹配之后,后续调用重新执行。
假设原流程依次产生 A、B、C 三次调用。只修改 C 的提示词时,A 与 B 可以复用;修改 B 时,B 和 C 都需要重跑。实际运行中的顺序以记录的调用序号为准,尤其要留意动态并发产生的调用。
指纹覆盖解析后的提示词、模型配置、Schema、工作目录等输入,但不会自动给整个仓库或外部世界做快照。同一路径下的文件变了,不一定体现在指纹里。需要重新检查当前状态时,就启动新运行,或把提交、数据版本等显式纳入调用输入。
恢复也不等于回滚。历史文本无法重建工作区里的副作用,因此当前实现遇到 worktree 隔离调用会打断缓存前缀并重新执行。涉及写入的任务必须考虑幂等性、已发生的操作与实际文件状态,不能把缓存命中当作产物仍然存在的证明。
预算、工作区和后台生命周期
v0.2.0 的预算按 output tokens 记账,是停止新派发的阈值。它不是总 token 用量,也不是费用硬上限;已经运行的请求可能在阈值之后继续产生输出。并发数、单调用超时和有限重试,需要一起配置。
只读审查可以共享仓库。并行修改则更适合独立 worktree;如果后续验证必须看到前一阶段的修改,两次调用要使用相同的 worktreeKey。验证另一个工作区里的旧代码,不构成对修改结果的验收。
后台模式通过 workflow_run 的 background: true 启动,并返回 run ID。workflow_status 查看进度,workflow_inspect 查看实际结果。workflow_pause 停止新派发,但不冻结正在执行的调用;workflow_cancel 用于取消,不能撤销已经产生的外部操作。恢复后应以返回的 run ID 和最终状态为准。
MCP 调用中的 cwd 必须是目标项目的绝对路径。把它误设为插件安装目录,Agent 就会在错误的仓库里工作。
为什么脚本不能随便做 I/O
编排脚本运行在独立的受限上下文中,不提供 process、require、fetch 等普通 Node.js I/O 能力,也不允许依赖当前时间或随机数来决定流程。需要的时间和输入由 args 显式传入,文件或网络操作交给子 Agent。
这样做是为了减少重放时的隐藏输入。它不等于为子 Agent 或外部操作提供完整安全隔离;子进程的工作目录、沙箱与授权范围仍然要单独约束。
它什么时候值得用
多维度审查、批量迁移、逐条验证和多方案评估,通常有可重复的控制结构,适合写成 workflow。单文件小修改或一两次查询就能回答的问题,直接与 Agent 协作往往更简单。
我的理解是:Loop Engineering 建立反馈,Graph Engineering 显式表达依赖,Dynamic Workflow 将其中稳定的编排落实到代码。它不消除模型的不确定性,而是让不确定的输出进入一个可检查、有边界、能留下记录的过程。