多 Agent 任务做得多了,我发现主 Agent 很容易同时承担两种工作:一边理解代码、判断问题,一边记住谁先做、谁后做、哪些结果还没有验证。任务变大之后,后者本身就会消耗不少上下文。

Codex Dynamic Workflow 是我围绕这个问题做的一个插件:用 JavaScript 表达调度,把需要模型判断的工作留在 agent() 调用里。本文以仓库 v0.2.0 的实现为例,介绍它怎样运行,以及使用时最容易误解的边界。

把编排从聊天中拿出来

以代码审查为例,可以让几个 Agent 分别检查正确性、接口兼容性和资源管理,再逐条验证发现,最后汇总。自然语言可以描述这个过程,但每次调度、等待、重试和收集结果都让主模型临场决定,流程就难以检查和复用。

写成程序后,循环次数、依赖关系、过滤条件和失败分支都有明确位置。模型仍然负责读代码和提出判断,程序负责决定这些判断如何进入下一步。

这里的“确定性”指编排规则可以检查和重放,不是模型每次都返回相同答案。工作流也不必是一张提前列完节点的静态图:下游任务数量可以由上游结果决定,新的发现可以产生新的验证调用,这就是它的动态部分。

运行链路:从插件入口到 Codex 子进程

插件分为三个层次:

层次 职责
Skill 与 MCP 入口 帮助主 Agent 选择流程,并接收运行、校验、状态查询等请求
JavaScript 运行引擎 执行控制流,管理并发、预算、结果校验和运行记录
Codex CLI 适配层 将 agent() 转为 codex exec --json 子进程,解析事件和最终结果
插件运行架构用户经 MCP 或 CLI 调用 JS 引擎,引擎管理并发 Codex 子进程并接收结果。主 Agent / 用户目标、输入、workflow 脚本MCP 入口或 CLIworkflow_run / cdw runJavaScript 编排引擎依赖 · 并发队列 · 预算 · Schema · JournalCodex Acodex exec --jsonCodex Bcodex exec --jsonCodex Ccodex exec --json事件、结果与用量回传引擎,决定下一步每次派发是独立进程;MCP 是入口,不是执行引擎
控制流由引擎推进,代码理解与判断由子 Agent 完成。图中的并发仍受运行上限约束。

这是插件在 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 与屏障的等待关系Pipeline 中 A 审查完成即可验证;屏障模式等待 A 和 B 的审查都结束才汇总。时间 →(示意,非实测耗时)pipelineA审查验证B审查验证A 完成审查就验证,不必等 Bparallel + barrierA审查B审查汇总 / 后续阶段等所有分支完成屏障用于必须看齐结果的步骤
不同条目可同时处于不同阶段。同一条目内部仍按依赖顺序执行;phase 名称不会建立屏障。

pipeline(items, ...stages) 让每个条目独立经过各阶段。正确性审查先完成,就可以立即进入验证,不必等兼容性审查。阶段参数包含前一阶段结果、原始条目和索引,便于保留上下文。

parallel(thunks) 则并发启动分支,并等齐结果再返回。只有汇总必须看齐所有结果时才需要这种屏障,例如多方案比较或跨结果去重。并行不意味着所有调用都会立即运行,实际派发仍受并发上限约束。

这两个函数决定依赖关系,phase() 主要组织进度展示。把阶段取不同名字,不会自动建立依赖;改变等待方式需要改变代码。

Schema 校验解决格式,不替你判断事实

带 schema 的调用通过 Codex CLI 的结构化输出接口,并在引擎内再次校验。格式或字段不符合要求时,引擎可以把错误反馈给模型进行有限纠正。v0.2.0 将进程失败重试与 Schema 纠正分开管理,默认额外 Schema 纠正次数为五次。

但 { confirmed: true, evidence: "..." } 结构合法,不代表结论正确。重要结论仍要对应文件、复现步骤或实际测试输出。结构化输出的价值,是让程序能够可靠读取结果并执行分支,而不是把自然语言判断变成事实保证。

终态执行失败会返回 null;流水线会跳过该条目后续阶段。参数错误等异常仍需处理。汇总时不能简单丢弃空值后宣称“全部通过”,而应报告缺失的覆盖范围。

断点恢复:复用未变的调用前缀

每次运行会保存脚本、调用记录与结果。恢复时,journal 按调用序号和输入指纹检查历史记录;只有成功且输入匹配的连续前缀可以复用。第一次不匹配之后,后续调用重新执行。

最长未变前缀恢复仅 C 变化时复用 A B,B 变化时只复用 A,B C 重跑。按记录的调用顺序比较输入指纹上次成功运行A成功记录B成功记录C成功记录仅 C 改变A复用缓存B复用缓存C重新执行B 改变A复用缓存B重新执行C重新执行第一次不匹配后,后续调用不再命中旧缓存缓存结果 ≠ 文件快照;worktree 调用会打断缓存前缀
这里用串行 A → B → C 说明规则。并发流程以真实记录的调用序号为准,不能只按图上位置判断缓存。

假设原流程依次产生 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 将其中稳定的编排落实到代码。它不消除模型的不确定性,而是让不确定的输出进入一个可检查、有边界、能留下记录的过程。

源码与延伸阅读