四个内置 Workflow

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

作者 David更新于 第 23 篇(共 33 篇)

上一篇讲了运行时给脚本的原语,这一篇看 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 里的阶段用途
compose749Brainstorm → Design → Implement → Verify → Review → Report → Merge无人值守地走完一次功能开发
deep-research227Brief → Plan → Research → Reflect → Write → Review多源调研,产出带引用的 Markdown 报告
fact-check398Plan → Search → Extract → Group → Crosscheck → Report对具体说法做对抗式核查
research-experiment180Baseline → 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 运行时),用 !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):

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):

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):

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):

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一个子智能体,没人可问,把猜测写进 Assumptionsbrief.md
Plan一个子智能体,schema 为 briefContext 加 anglesplan.json,角度数截到上限
Research每个角度一个子智能体,parallel 并行findings/F<n>.md
Reflect一个子智能体找缺口,最多给出 deltaAngles 个补充角度;quick 深度跳过reflect.json,随后并行跑补充角度
Write唯一的写作者,只能引用 findings 里出现过的 URLREPORT.md
Review冷启动的审查者,抽查 5 条引用,schema 返回 critical 与 summaryREVIEW.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):

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):
).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)。

  1. 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):

// 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 沙箱——同一个 QuickJS 沙箱的另一种用法:让模型写一段脚本批量调用工具,以及 GPT 模型的精简工具集。

本页目录