Windmill「Design It Twice」并行子代理模式:为一个深模块生成并比较多套接口设计
2026/9/13 6:28:02 网站建设 项目流程

Windmill「Design It Twice」并行子代理模式:为一个深模块生成并比较多套接口设计

【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill

本文围绕 Windmill 仓库中 vendored 的 Agent 技能文件 .agents/skills/codebase-design/DESIGN-IT-TWICE.md 展开,完整讲解其「Design It Twice」并行子代理设计模式:先框定问题空间、再并行派发 3 个以上带着互不相同设计约束的子代理各自产出一套“截然不同”的接口方案、最后按深度(depth)、局部性(locality)与接缝(seam)位置逐一比较并给出有立场的推荐。读完你应能理解该模式在 Windmill 架构改进流程中的确切触发位置,并掌握可直接复用的子代理提示词模板、输出清单与比较维度,把「第一个想到的接口未必是最优」这一原则落地为可操作的工作流。

该模式在 Windmill 仓库中的位置

Windmill 在.agents/skills/下维护了一组供 AI 编码助手(Claude Code、Codex、Pi 等 CLI)调用的技能文件。.agents/skills/codebase-design/目录内含三个文件,构成一个自洽的小技能簇:

  • SKILL.md — 定义“深模块”设计的共享词汇表(module、interface、seam、adapter、leverage 等)与设计原则;
  • DEEPENING.md — 在给定依赖条件下如何安全地把一组浅模块“加深”,定义了四类依赖分类与接缝纪律;
  • DESIGN-IT-TWICE.md — 本文主角:当用户想为某个已选定的“加深候选”(deepening candidate)探索替代接口时,使用这套并行子代理模式。

三者的调用关系可以从源码结构中直接看到:SKILL.md 末尾的 “Going deeper” 一节明确指向另外两个伴生文件,分别对应“带依赖地加深一个模块簇”与“探索替代接口”。而.agents/skills/codebase-design/DESIGN-IT-TWICE.md又位于更大的架构改进流程末端——improve-codebase-architecture/SKILL.md 的 “Grilling loop” 阶段写道:“想为加深后的模块探索替代接口?运行/codebase-design技能并使用它的 design-it-twice 并行子代理模式。”

从 UPSTREAM.md 可以确认,codebase-designimprove-codebase-architecturegrillinggrill-medomain-modeling这五个技能是从外部仓库 vendored 而来并钉死在特定 commit 上的,它们形成一个依赖闭包:improve-codebase-architecture的架构词汇取自codebase-design,领域模型维护取自domain-modeling,删掉任何一个都会破坏其余部分。理解这一点有助于理解为什么DESIGN-IT-TWICE.md里反复引用兄弟文件的词汇而不是就地重复定义——它被设计为技能簇中的一环,而非独立文档。

模式本身的思想来源在文档首行即被标明:基于 Ousterhout 的 “Design It Twice”——你的第一个想法很可能不是最好的。因此该模式不是“让 AI 再想想”,而是制度性地强制生成多个激进差异化的候选设计,再按统一标准裁决。

前置词汇:子代理必须使用的统一语言

在讲流程之前必须先交代词汇表,因为DESIGN-IT-TWICE.md的步骤 2 明确要求:每个子代理的简报(brief)中都要同时包含 SKILL.md 的架构词汇与CONTEXT.md的领域词汇,让每个子代理“用一致的命名来称呼事物”。这些术语来自 SKILL.md 的 Glossary,且规定“精确使用这些词——不要用 component、service、API、boundary 来替代”。核心术语如下:

术语定义要点
Module(模块)任何拥有接口与实现的东西;刻意规模无关——一个函数、一个类、一个包或跨层切片均可
Interface(接口)调用方要正确使用模块所必须知道的一切:类型签名之外,还包括不变量(invariants)、顺序约束、错误模式、必需配置与性能特征
Implementation(实现)/ Adapter(适配器)实现是模块内部;适配器是在某个接缝处满足接口的具体事物,描述“角色”(它填哪个槽位)而非“物质”(里面是什么)
Depth(深度)接口处的杠杆(leverage):调用方(或测试)每单位需要学习的接口所能调动的行为量。大量行为藏在小接口后 = 深;接口与实现复杂度相当 = 浅
Seam(接缝)源自 Michael Feathers:一个“可以改变行为而无需在该处编辑”的位置,即模块接口所在的位置。接缝放在哪里本身就是一个独立设计决策
Leverage(杠杆)/ Locality(局部性)深度带给调用方的回报是 leverage(一份实现跨 N 个调用点与 M 个测试持续回报),带给维护者的回报是 locality(变更、bug、知识与验证集中在一处,“修一次,处处修好”)

此外还有几条贯穿整个设计过程的原则,它们直接决定了后续比较与裁决的尺度:

  • 深度是接口的属性,不是实现的属性。深模块内部可以由小的、可 mock、可替换的部分组成,只要这些部分不属于接口;模块既可以在接口处有外部接缝,也可以在实现内部有内部接缝(仅供自己的测试使用)。
  • 删除测试(The deletion test):想象删掉这个模块——如果复杂度随之消失,它是透传层;如果复杂度会在 N 个调用方身上重新出现,说明它赚到了自己的存在价值。
  • 接口即测试面。调用方和测试跨越同一条接缝;如果你需要测试到接口“后面”,说明模块形状可能不对。
  • 一个适配器 = 假设性接缝;两个适配器 = 真实接缝。除非某处确实存在两种变化,不要引入接缝。

DESIGN-IT-TWICE.md的“展示与比较”步骤(步骤 3)要求的三个比较维度——depth(接口处的杠杆)、locality(变更集中在哪里)、seam placement(接缝放在哪里)——全部来自这套词汇表。没有这张表,后续步骤中“radically different”“where leverage is high, where it's thin”等表述都无法被一致执行。

流程第一步:框定问题空间(Frame the problem space)

在派发任何子代理之前,编排者(主代理)要先为所选的加深候选写一份面向用户的问题空间说明。按文档规定,这份说明必须包含三样东西:

  1. 约束:任何新接口都需要满足的约束条件;
  2. 依赖及其类别:模块将要依赖什么,以及每个依赖属于 DEEPENING.md 定义的哪一类(见下文“四类依赖”一节);
  3. 粗略的示意性代码草图(illustrative code sketch)——注意其定位是“让约束变得具体”的手段,不是一个提案。它用来把抽象约束落到代码形状上,避免子代理在错误的约束下发散。

文档随后给了一条关键的流程纪律:把这份说明展示给用户之后,立即进入步骤 2,不要等待。理由写在原文里:“用户一边读一边思考,子代理在并行地干活。”这是一个刻意的时延设计——人类阅读与 AI 并行计算同时发生,问题空间文档既不是等待确认的提案,也不是阻塞点。

流程第二步:并行派发子代理(Spawn sub-agents)

这是整个模式的核心。文档规定:至少并行派发 3 个子代理,每个必须为该模块产出一套“截然不同的”(radically different)接口。“radically different”不是措辞上的客气——为此文档给每个子代理分配了互相冲突的设计目标,从源头保证候选方案之间的差异是结构性的:

  • Agent 1:“最小化接口——目标最多 1–3 个入口点。最大化每个入口点的杠杆。”
  • Agent 2:“最大化灵活性——支持尽可能多的用例与扩展。”
  • Agent 3:“为最常见的调用方优化——让默认场景变得平凡(trivial)。”
  • Agent 4(如适用):“围绕 ports & adapters 设计跨接缝依赖。”

可以看到,Agent 1 与 Agent 2 在“接口规模”这一轴上是对立的(极小 vs. 极宽),Agent 3 则代表“为多数优化”的现实主义路线,Agent 4 把解耦问题(ports & adapters)本身作为设计约束。四个方向恰好覆盖了接口设计中最常出现分歧的决策轴。

每个子代理的输入:独立的技术简报

文档要求每个子代理收到一份独立的技术简报(technical brief),内容包括:

  • 相关文件路径;
  • 耦合细节;
  • 依赖类别(引用 DEEPENING.md 的分类);
  • 接缝后面(behind the seam)是什么。

并特别强调:这份简报与步骤 1 中面向用户的问题空间说明是相互独立的两份材料。一个是给人类读者看的约束叙述,一个是给子代理执行用的工程输入——两者不能互相替代。此外,简报必须同时注入 SKILL.md 的架构词汇与CONTEXT.md的领域词汇,使每个子代理产出的命名与架构语言、项目领域语言保持一致(这一点对多代理并行尤其重要:词汇不统一时,后续的比较与合成都无法进行)。

Windmill 的领域词汇表就在仓库根目录的 CONTEXT.md 中,它为项目特有概念钉死命名,例如:Step(flow 中的一个节点,代码中类型为FlowModule,刻意避免用“module”一词以防与架构意义的 module 混淆)、Step setting(retries、timeout、concurrency limit 等逐步运行时选项)、Trigger step(polling flow 的第一步)、Member / Role / Owner(权限体系)。当子代理为某个 Windmill 模块设计接口时,这些术语保证候选方案谈的是同一个领域对象。

每个子代理的输出:五项交付物

文档为每个子代理规定了统一的输出格式,五项缺一不可:

  1. Interface—— 类型、方法、参数,外加不变量、顺序约束、错误模式(注意这比“类型签名”宽得多,呼应 SKILL.md 对 interface 的定义);
  2. Usage example—— 展示调用方如何使用它;
  3. 实现隐藏了什么—— 接缝后面的内容;
  4. 依赖策略与适配器—— 对应 DEEPENING.md 的依赖分类与接缝纪律;
  5. Trade-offs—— 杠杆在哪里高、在哪里薄。

这份输出清单本身值得注意:它把“接口设计”从写类型签名的活动,扩展为同时交付不变量、错误模式、用法示例与权衡分析的活动——这五项恰好就是“接口即测试面”原则所需要的全部信息,因为后续测试要跨越的正是这个完整接口。

支撑机制:DEEPENING.md 的四类依赖

步骤 1 要求标注依赖类别、步骤 2 的简报要求携带依赖类别、子代理输出的第 4 项要求给出“依赖策略与适配器”——三者都锚定在 DEEPENING.md 的依赖分类上。该文件把候选模块的依赖分为四类,类别决定了加深后的模块如何跨越接缝被测试:

类别特征加深策略
1. In-process(进程内)纯计算、内存状态、无 I/O总是可加深——合并模块,直接通过新接口测试,无需适配器
2. Local-substitutable(本地可替代)存在本地测试替身(如 PGLite 之于 Postgres、内存文件系统)存在替身则可加深;加深后的模块用替身在测试套件中运行;接缝是内部接缝,模块外部接口上不设 port
3. Remote but owned(远端但自有)自己拥有的跨网络边界服务(微服务、内部 API)在接缝处定义 port(接口):深模块拥有逻辑,传输以适配器形式注入;测试用内存适配器,生产用 HTTP/gRPC/队列适配器
4. True external(真外部)不控制自己的第三方服务(Stripe、Twilio 等)加深后的模块以注入 port 的形式接收外部依赖;测试提供 mock 适配器

配套的两条接缝纪律测试策略同样被设计流程反复引用:

  • “一个适配器 = 假设性接缝,两个适配器 = 真实接缝”:除非至少两个适配器都有正当理由(典型是生产 + 测试),否则不要引入 port——单适配器接缝只是间接层。
  • 内部接缝与外部接缝要分清:不要把内部接缝仅仅因为测试用到就暴露到接口上。
  • 测试策略是“替换,不是叠加”(replace, don't layer):加深后,针对旧浅模块的单元测试就成了垃圾,删掉;新测试写在加深后模块的接口处;测试断言的是通过接口可观察的结果而非内部状态;测试应当能挺过内部重构——如果一个测试在实现变化时必须跟着改,说明它测试越过了接口。

DESIGN-IT-TWICE而言,这套分类的实际作用是:它让四个子代理在“依赖策略与适配器”这一输出项上有共同的坐标系,比较时才谈得通。

流程第三步:展示、比较与裁决(Present and compare)

文档对呈现方式的规定同样具体:

  1. 顺序呈现,而非并列铺开。设计逐个展示,让用户能够消化(absorb)每一个,然后再比较。这与步骤 2 的并行生产形成对照——生产并行,消费串行。
  2. 用散文(prose)比较,且比较维度固定为三个depth(接口处的杠杆有多大)、locality(变更集中在哪里)、seam placement(接缝放在哪里)。这三个维度全部来自前置词汇表,不允许临场发明比较标准。
  3. 给出自己的推荐。比较之后,编排者必须表态:你认为哪套设计最强、为什么。如果不同设计中的元素可以很好地组合,提出混合方案(hybrid)。原文的要求是 “Be opinionated — the user wants a strong read, not a menu.”(要有立场——用户要的是一个强判断,而不是一份菜单。)

最后一条是整个模式的收尾价值观:并行生成是为了防止锚定在第一直觉上,但流程的终点不是“民主投票”,而是编排者给出有依据的强推荐。多方案是手段,裁决是交付物。

在整个架构改进流程中的位置

把 improve-codebase-architecture/SKILL.md 的完整流程读一遍,可以看清DESIGN-IT-TWICE处在链条的哪一环:

  1. Explore(探索):先定范围再扫描(YAGNI)——用户指明了方向就照做,否则走一遍git log --oneline找近期变更的热点区域;先读CONTEXT.md领域词汇表;然后派子代理有机地走读代码,注意理解一个概念要在多少小模块间跳转、哪些模块是浅的、纯函数被抽出仅为可测但真正的 bug 藏在调用方式里(没有 locality)、哪些模块跨接缝泄漏、哪些部分未被测试或难以通过现有接口测试。对任何疑似浅模块跑一遍删除测试
  2. Present candidates as an HTML report(以 HTML 报告呈现候选):把每个加深候选渲染成卡片(涉及文件、问题、方案、以 locality/leverage 表述的收益、Before/After 图、推荐强度徽章),写到系统临时目录(不入仓库),以可视化方式呈现,最后问用户:“你想深入探索哪一个?” 该报告刻意不在此阶段提出接口
  3. Grilling loop(拷问循环):用户选定候选后,跑/grilling技能走决策树——约束、依赖、加深后模块的形状、接缝后是什么、哪些测试存活;过程中通过/domain-modeling技能随时更新领域模型(给深模块起了CONTEXT.md中没有的名字?就地加词)。而当走到“想为加深后的模块探索替代接口”这一步时,才进入DESIGN-IT-TWICE.md描述的并行子代理模式

也就是说,DESIGN-IT-TWICE是整个架构改进流水线上最靠近“定稿”的环节:候选已经选出(improve-codebase-architecture)、约束与依赖已经过拷问(grilling),此时才值得投入多个并行子代理去竞争性地设计接口。这也解释了为什么它的前置条件写得如此精确——“当用户想为已选定的加深候选探索替代接口时”。

小结

DESIGN-IT-TWICE.md 篇幅不长,但它把“Design It Twice”这一设计直觉压缩成了一个可执行协议:先框定约束(含依赖类别与示意草图,展示后立即推进),再并行派发 3+ 个带互斥设计目标的子代理(最小接口 / 最大灵活性 / 最常见调用方 / ports & adapters),每个按五项固定清单交付(接口、用法、隐藏内容、依赖策略、权衡),最后顺序呈现并按 depth、locality、seam placement 三个维度散文比较,给出有立场的推荐或混合方案。其可执行性建立在两个配套文件之上:SKILL.md 提供统一的架构词汇与原则(深度、删除测试、接口即测试面、双适配器才算真实接缝),DEEPENING.md 提供依赖四分类与“替换不叠加”的测试策略。对 Windmill 这样多语言、多工作空间的代码库,该模式的价值在于把“接口怎么设计”从一次性直觉判断,变成多候选并行生成、统一标准裁决的重复流程——这正是 “your first idea is unlikely to be the best” 的操作化。

【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询