四个内置 Workflow
拆读 compose、deep-research、fact-check、research-experiment 四个脚本的阶段、分工、并行、数据流与失败处理。
上一篇讲了运行时给脚本的原语,这一篇看 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 运行时),用 !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 | 一个子智能体,没人可问,把猜测写进 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):
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流程:
- Plan:一个子智能体把问题拆成 3 到 6 条互不重叠的搜索线(schema 的
minItems、maxItems),提示词里要的是 5 条。 - Search → Extract:用
pipeline串起两级,不同搜索线之间互不等待(fact-check.js:194)。第一级每条线一个搜索子智能体,返回最多 6 个带fit评级的结果;第二级按“主机名加路径”规范化 URL 去重,扣读取预算,再用parallel给每个新来源派一个阅读子智能体,返回来源等级与最多 5 条可证伪的事实和原文引述。预算耗尽后仍会放行fit为 high 的结果(fact-check.js:221),所以注释里的“hard cap”对高相关页面并不硬。 - 排序截断:按事实权重(key、support、aside),再按来源等级排序,取前 25 条。
- Group:一个子智能体把说同一件事的事实合并,每组核查一次;失败就每条事实各成一组。
- 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)。
- 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 模型的精简工具集。