# 四个内置 Workflow

> 拆读 compose、deep-research、fact-check、research-experiment 四个脚本的阶段、分工、并行、数据流与失败处理。

- 作者：David（道雾轩）
- 专栏：MiMo Code 源码解读（https://daiw.org/manual/mimo-code.md）
- 最后更新：2026-09-17
- 原文：https://daiw.org/manual/mimo-code/builtin-workflows
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

上一篇讲了运行时给脚本的原语，这一篇看 MiMo 自己怎么用它们。四个脚本都在 `packages/opencode/src/workflow/builtin/` 下，构建时由 Bun 宏读进二进制（`packages/opencode/src/workflow/builtin.macro.ts:6`），启动时逐个解析 meta、按 `name` 注册，meta 写坏了直接抛错（`packages/opencode/src/workflow/builtin.ts:35`）：

| 名字 | 行数 | meta 里的阶段 | 用途 |
| --- | --- | --- | --- |
| `compose` | 749 | Brainstorm → Design → Implement → Verify → Review → Report → Merge | 无人值守地走完一次功能开发 |
| `deep-research` | 227 | Brief → Plan → Research → Reflect → Write → Review | 多源调研，产出带引用的 Markdown 报告 |
| `fact-check` | 398 | Plan → Search → Extract → Group → Crosscheck → Report | 对具体说法做对抗式核查 |
| `research-experiment` | 180 | Baseline → Loop → Audit → Report | 围绕一个可机械度量的指标自动做实验 |

meta 里的阶段只是给人看的目录，脚本实际调用的 `phase()` 不完全一致：compose 多一个 Fix（`packages/opencode/src/workflow/builtin/compose.js:610`），deep-research 多出“Research (delta)”和 Fix，fact-check 的 Search、Extract 只写在 agent 选项里，没有调用 `phase()`，transcript 会从 Plan 直接跳到 Group。

## 怎么用

先打开 `MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL`，模型用 `workflow({ operation: "run", name, args })` 调用。四个脚本对 `args` 的要求各不相同：

| 名字 | `args` |
| --- | --- |
| `compose` | 必填 `task`；可选 `type`（feature、bugfix、refactor、feedback）、`feature_name`、`skip_brainstorm`、`skip_report`、`maxConcurrent`、`isolate_worktrees`。宿主额外注入 `_composeDocsDir` |
| `deep-research` | 必填 `question`（可直接传字符串）；可选 `depth`（quick、standard、deep）、`context`。宿主注入 `dir` 与 `today` |
| `fact-check` | 只接受一个非空字符串 |
| `research-experiment` | 必填 `dir`、`goal`、`metric`、`evalCmd`、`editable`；可选 `guardCmd`、`lowerIsBetter`（默认 true）、`maxIters`（默认 15）、`targetValue` |

只有 compose 和 deep-research 的参数会被宿主规整和补充（`packages/opencode/src/tool/workflow.ts:235`）。工具说明声称内置 workflow 的 `args` 既可以是字符串也可以是对象（`packages/opencode/src/tool/workflow.txt:29`），但 fact-check 只认字符串，传对象会直接返回“No research question provided”（`packages/opencode/src/workflow/builtin/fact-check.js:109`）。

用户侧的入口是 `/deep-research`：它的模板先让模型在需求含糊时问两三个澄清问题，再调用 deep-research（`packages/opencode/src/command/index.ts:74`）。

读脚本前先记住一点：宿主返回的 `null` 进沙箱后是 `undefined`（见 [Workflow 运行时](https://daiw.org/manual/mimo-code/workflow-runtime)），用 `!r` 判断失败没问题，写成 `r === null` 就永远不成立。compose 和 research-experiment 里都有这样的判断，下文会点出来。

## compose

compose 的目标是不打扰用户，把一个请求一路做到提交。全程没有人应答，Brainstorm 的提示词甚至要求子智能体把本该问用户的问题“自己问、自己答”。

**Brainstorm**：先用 `glob` 列出 specs、plans、reports 目录下已有的文档，有的话提示子智能体判断这次是不是对已有功能的修订（`compose.js:197`）。随后派一个子智能体，按 `BRAINSTORM_SHAPE` 返回项目上下文、假设、自问自答、候选方案、选定方案与理由、`amends` 等字段。给了 `skip_brainstorm` 或子智能体失败，就用一份空的合成上下文顶上（`compose.js:232`）。这些字段被拼成 `contextDigest` 与 `intentBlock`，分别塞进 Design 与 Implement、Review 的提示词。

**类型判定**：不单独开一轮模型调用。`args.type` 合法就用它，否则用正则猜：同时提到 PR 或 review 与 feedback、comment、address 之一的算 feedback，出现 bug、crash、error 一类词的算 bugfix，其余算 feature（`compose.js:260`）。类型决定 Design 用哪个技能（`packages/opencode/src/workflow/builtin/compose.js:267`）：

```js
const SKILL_BY_TYPE = {
  feature: "compose:plan",
  refactor: "compose:plan",
  bugfix: "compose:debug",
  feedback: "compose:feedback",
}
```

**Design**：分两步。第一个子智能体不带 schema，负责用 `write` 工具写出 spec 和 plan 两个文件；脚本只检查 specs、plans 目录里有没有 `.md`，没有就带着“上次你没写文件”的提示再派一次（`compose.js:329`）。第二个子智能体读计划，按 `DESIGN_SHAPE` 抽出任务列表，每个任务有 `id`、`description`、`acceptance`、可选的 `files` 与 `dependsOn`。抽不出任务返回 `design-failed`；缺失或重复的 ID 补成 `T1`、`T2`；再用 Kahn 算法按依赖拓扑排序成若干批，有环返回 `design-cycle`（`compose.js:372`）。

**Implement 与 Verify**：最多 3 轮（`MAX_TDD_ATTEMPTS`，`compose.js:18`）。每轮按批执行，一批里多于一个任务时自动隔离，每个任务一个 worktree 并行跑，否则在主工作区串行（`packages/opencode/src/workflow/builtin/compose.js:490`）：

```js
const ISOLATE = isolateBatch(batchIds)
const limit = ISOLATE ? Math.min(MAX_CONCURRENT, tasks.length) : 1
for (let i = 0; i < tasks.length; i += limit) {
  const chunk = tasks.slice(i, i + limit)
  const results = await parallel(chunk.map((t) => () => runImplementTask(t, failuresOrEmpty, ISOLATE)))
```

`MAX_CONCURRENT` 默认 8，可用 `maxConcurrent` 改；`isolate_worktrees` 强制开或关隔离（`compose.js:156`）。提示词要求每个实现者按 `compose:tdd` 先写测试再写实现，并在各自的 worktree 里提交。有改动的 worktree 交给一个集成子智能体合并回主工作区，按 `INTEGRATE_SHAPE` 报告已合并、冲突和未改动的任务。然后派验证子智能体跑项目的类型检查、测试、构建，按 `VERIFY_SHAPE` 如实上报（`packages/opencode/src/workflow/builtin/compose.js:546`）：

```js
phase("Verify")
verify = await runVerify()
if (verify) verifyHistory.push(verify)
const conflictText = attemptConflicts.length ? "\nIntegrate conflicts: " + JSON.stringify(attemptConflicts) : ""
const passed = verify && verify.allPassed && attemptConflicts.length === 0
```

通过就写一份本轮报告进入审查；不通过就派 `compose:debug` 子智能体带着失败信息修，下一轮**全部批次**带着失败摘要重新实现；3 轮都不过返回 `verify-exhausted`（`compose.js:565`）。

**Review 与 Fix**：审查子智能体先对照验收标准查规格符合性，再看代码质量，按 `REVIEW_SHAPE` 把问题分成 critical、important、minor。有 critical 就进入最多 2 轮修复（`MAX_REVIEW_FIX_ATTEMPTS`，`compose.js:19`）：每条 critical 派一个修复子智能体（多于一条时同样隔离并行），集成、复验、再审查，写一份报告。两轮后仍有 critical，返回 `readyToMerge: false` 结束。

**Report 与 Merge**：汇总报告由子智能体写，脚本只检查文件在不在，不在就重派一次（`compose.js:691`）。最后派合并子智能体按 `MERGE_SHAPE` 选择 commit、commit+push 或 commit+pr，没提交成功返回 `merge-failed`。

对照代码，有几处行为值得知道：

- **审查失败按通过处理**：第一次审查子智能体失败时，脚本把结果当作没有 critical（`compose.js:605`）；修复轮里再审查失败，critical 同样为空，循环退出，照样走到合并。决定去留的只是 `critical` 数组，`readyToMerge` 字段从不参与判断，修复轮的复验结果也只记录不拦截。
- **失败任务可能被记成成功**：`perTaskResults` 用 `r === null` 判断失败（`compose.js:503`、`compose.js:511`），子智能体失败时脚本拿到的是 `undefined`，于是隔离模式下记成 `pristine`，非隔离模式下记成 `ok`。这只影响返回结果和报告里的统计，是否通过仍由验证与审查决定。
- **compose 技能加载不了**：每个提示词都要求子智能体“先用 `skill` 工具加载 `compose:xxx`”，但子智能体的类型是 general，默认规则对 `compose:*` 是 deny（`packages/opencode/src/agent/agent.ts:114`），`skill` 工具在 `ctx.ask` 这一步就会得到 `DeniedError`（`packages/opencode/src/tool/skill.ts:58`）。代码里没找到为 workflow 子智能体放开这条规则的地方，除非用户自己在配置里放行，这些技能只能停留在提示词里的名字。

## deep-research

deep-research 按深度设了硬预算，写在脚本里而不是交给模型判断（`packages/opencode/src/workflow/builtin/deep-research.js:33`）：

```js
const DEPTH = {
  quick: { angles: 3, queryBudget: 4, deltaAngles: 0, sources: 8 },
  standard: { angles: 5, queryBudget: 6, deltaAngles: 3, sources: 15 },
  deep: { angles: 8, queryBudget: 8, deltaAngles: 4, sources: 25 },
}[_a.depth ?? "standard"];
```

其中 `sources` 在脚本里没有被用到。沙箱没有 `Date`，缺 `today` 直接抛错，由宿主从自己的时钟注入（`tool/workflow.ts:185`）。

| 阶段 | 派谁 | 产物 |
| --- | --- | --- |
| Brief | 一个子智能体，没人可问，把猜测写进 Assumptions | `brief.md` |
| Plan | 一个子智能体，schema 为 `briefContext` 加 `angles` | `plan.json`，角度数截到上限 |
| Research | 每个角度一个子智能体，`parallel` 并行 | `findings/F<n>.md` |
| Reflect | 一个子智能体找缺口，最多给出 `deltaAngles` 个补充角度；quick 深度跳过 | `reflect.json`，随后并行跑补充角度 |
| Write | 唯一的写作者，只能引用 findings 里出现过的 URL | `REPORT.md` |
| Review | 冷启动的审查者，抽查 5 条引用，schema 返回 `critical` 与 `summary` | `REVIEW.md` |

研究子智能体的提示词是锁死的模板：最多 `queryBudget` 次搜索，精读 3 到 6 个结果，没读过的页面不许引用，每条发现带原文引述、URL、来源类型与置信度，目标 5 到 12 条（`deep-research.js:41`）。审查报告里 `critical` 大于 0 时，再派一个子智能体只修一次，不再复审（`deep-research.js:214`）。

每个阶段开头都先检查自己的产物文件在不在，在就跳过（例如 `deep-research.js:81`、`deep-research.js:129`）。所以即使不走日志续跑，把同一个问题再跑一遍也只补缺的部分，这就是 meta 描述里说的“Convergent (resumable via file checkpoints)”。脚本里的 `exists`、`readFile` 相对 workspace 根目录解析，不传 `workspace` 时就是项目 worktree 的根目录。Brief、Plan、Research 或 Write 没产出文件时脚本直接抛错，整个运行失败。

## fact-check

fact-check 的可调参数都在文件头（`packages/opencode/src/workflow/builtin/fact-check.js:16`）：

```js
const JURY_SIZE = 3        // crosscheck voters per fact
const REJECT_QUORUM = 2    // reject votes that kill a fact AND the min valid votes needed to keep one
const SOURCE_BUDGET = 15   // hard cap on how many URLs we actually read
const FACT_CAP = 25        // hard cap on facts that reach crosscheck
```

流程：

1. **Plan**：一个子智能体把问题拆成 3 到 6 条互不重叠的搜索线（schema 的 `minItems`、`maxItems`），提示词里要的是 5 条。
2. **Search → Extract**：用 `pipeline` 串起两级，不同搜索线之间互不等待（`fact-check.js:194`）。第一级每条线一个搜索子智能体，返回最多 6 个带 `fit` 评级的结果；第二级按“主机名加路径”规范化 URL 去重，扣读取预算，再用 `parallel` 给每个新来源派一个阅读子智能体，返回来源等级与最多 5 条可证伪的事实和原文引述。预算耗尽后仍会放行 `fit` 为 high 的结果（`fact-check.js:221`），所以注释里的“hard cap”对高相关页面并不硬。
3. **排序截断**：按事实权重（key、support、aside），再按来源等级排序，取前 25 条。
4. **Group**：一个子智能体把说同一件事的事实合并，每组核查一次；失败就每条事实各成一组。
5. **Crosscheck**：对每组并行开 3 人陪审团，陪审员用模型组 `lite`，任务是“尽力推翻”，拿不准就投反对。计票规则（`packages/opencode/src/workflow/builtin/fact-check.js:305`）：

```js
).then(rulings => {
  // A null ruling (skip or agent error) counts as an abstention.
  const cast = rulings.filter(Boolean)
  const rejects = cast.filter(v => v.reject).length
  // A fact is kept only if it was genuinely adjudicated: a quorum of real
  // votes AND fewer than REJECT_QUORUM of them rejecting. Too many
  // abstentions means "unproven", which must not slip through as kept
  // (otherwise all-abstain → rejects=0 → false keep).
  const abstain = JURY_SIZE - cast.length
  const kept = cast.length >= REJECT_QUORUM && rejects < REJECT_QUORUM
```

有效票不足 2 张（弃权太多）同样不保留。`lite` 是内置档位名，没配置 `model_groups.lite` 时解析成全局默认模型（`packages/opencode/src/provider/provider.ts:1924`）。

6. **Report**：一个子智能体合并保留下来的事实，给出结论、分级的发现、局限与后续问题。

这里的失败处理偏向“交还半成品”：没抽到事实、全部被否决、报告子智能体失败，都会返回一个说明原因的结果对象，报告失败时附上未合并的已核实事实（`fact-check.js:367`），不会让整次运行失败。

## research-experiment

research-experiment 把“改代码、跑评测、保留或回滚”这个循环交给脚本控制，子智能体只负责一次尝试。状态存在 `artifacts/state.json` 里，每轮写一次，续跑时从记录的轮次接着来（`packages/opencode/src/workflow/builtin/research-experiment.js:52`）。

**Baseline**：一个子智能体写 `PLAN.md`，提交当前代码作为基线，原样执行评测命令并把输出重定向到 `artifacts/run_0.log`，提取指标、跑守卫命令、建 `results.tsv`。拿不到基线指标就抛错。

**Loop**：每轮一个无记忆的子智能体，按文件恢复上下文，在 `editable` 范围内做最小改动，以 `trial iter <n>:` 开头提交，跑评测，追加 `LOG.md`，只上报指标、守卫结果和一句假设，**不许自己决定去留**。升级阶梯写在脚本里（`packages/opencode/src/workflow/builtin/research-experiment.js:80`）：

```js
// escalation ladder — deterministic, the agent cannot drift it
if (state.mode === "NORMAL" && state.fails >= 3) state.mode = "REFINE"
if (state.fails >= 5) {
  state.pivots++
  state.fails = 0
  state.mode = state.pivots >= 2 ? "RESEARCH" : "PIVOT-NEW"
  if (state.pivots >= 3) {
    stopReason = "3 pivots without progress"
    break
  }
}
```

连败 3 次改为在当前假设上微调，连败 5 次换一个正交方向，第二次转向时先查文献，第三次转向就停。每轮的判定也由脚本做：指标变好且守卫通过才算 KEPT；否则派一个子智能体执行 `git reset --hard HEAD~1`，并要求它先确认最新提交确实是本轮的 trial 提交。脚本随后用 `readFile`、`writeFile` 自己往 `results.tsv` 追加一行。达到 `targetValue` 提前结束。

**Audit 与 Report**：一个不了解项目的审计子智能体看基线以来的全部 diff，检查有没有篡改评测、硬编码输出、数据泄漏这类刷分手段，写 `AUDIT.md`；最后一个子智能体写 `REPORT.md`，数字只能来自 `results.tsv`。

这个脚本有三处和运行时或技能包对不上：

- 每轮子智能体的选项写的是 `timeout_ms: 1000 * 60 * 30`（`research-experiment.js:114`），而运行时认的键是 `timeoutMs`（`packages/opencode/src/workflow/runtime.ts:237`），这 30 分钟的单轮超时实际不生效。
- 判定写的是 `r === null ? "AGENT_FAIL"`（`research-experiment.js:123`），子智能体失败时拿到的是 `undefined`，于是记成 RUN_FAIL，AGENT_FAIL 永远不会出现；计入连败次数的效果相同。
- 提示词让子智能体加载“auto-research”技能来用 `paper_search.py` 和 `references/experiment-loop.md`（`research-experiment.js:36`），内置技能包里没有这个名字的技能，这两个文件实际在 `super-research` 技能包里（`packages/opencode/src/skill/builtin/.bundle/super-research/scripts/paper_search.py`）。

下一篇：[exec 与 QuickJS 沙箱](https://daiw.org/manual/mimo-code/exec-sandbox)——同一个 QuickJS 沙箱的另一种用法：让模型写一段脚本批量调用工具，以及 GPT 模型的精简工具集。
