Windmill codebase-design 技能:设计深层模块的共享设计词汇与方法论
2026/9/13 22:13:51 网站建设 项目流程

Windmill codebase-design 技能:设计深层模块的共享设计词汇与方法论

【免费下载链接】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 仓库中.agents/skills/codebase-design/技能所定义的一套"深层模块"(deep module)设计方法论:从 module / interface / seam / adapter 等核心术语的精确词表,到"删除测试"、"接口即测试面"等设计原则,再到依赖分类下的安全加深流程(DEEPENING)与并行子智能体接口探索(Design It Twice)。读完后你将掌握一套可直接用于模块重构、接口评审与可测试性设计的统一架构语言,并理解 Windmill 的 AI 辅助开发体系如何把这套语言落地到实际的架构审查流程中。

1. 文档在仓库中的位置:Windmill 的 .agents 技能体系

Windmill 是一个开源开发者平台(内部工具、工作流、API 集成、后台任务与 UI 一体化),其仓库在.agents/skills/目录下维护了一组面向 AI 编码智能体的"技能"(skill)文件,例如adding-a-triggerrust-backendsvelte-frontendprgrilling等。codebase-design是其中承担架构设计词汇层的一个技能,其定义见 SKILL.md,并配有两份延伸文档:

  • DEEPENING.md —— 如何在给定依赖条件下安全地"加深"一簇浅层模块;
  • DESIGN-IT-TWICE.md —— 用并行子智能体探索同一模块的多种截然不同的接口设计。

需要说明的一点是:根据 UPSTREAM.md 的记载,codebase-design属于从外部公开技能仓库 vendored(固定提交、MIT 许可)进来的一组技能,与grill-megrillingimprove-codebase-architecturedomain-modeling共同构成一个依赖闭包——improve-codebase-architecture的架构词汇直接取自codebase-design,因此移除或改写其中任何一个都会破坏其他技能。这也解释了为什么该文档反复强调术语必须"原样使用"(use these terms exactly):词表是多个技能之间的契约,漂移即破坏集成。

在 Windmill 中实际消费这套词汇的是 improve-codebase-architecture/SKILL.md。它明确要求智能体在扫描代码库寻找"加深机会"(deepening opportunities)时,所有建议都必须精确使用codebase-design的术语(module、interface、depth、seam、adapter、leverage、locality),不得退化为 "component"、"service"、"API"、"boundary"。

2. 核心设计目标:深层模块

技能开篇给出全篇的设计目标:

Designdeep modules: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface.

即:设计深层模块——把大量行为藏在一个小接口背后,放在一条干净的接缝(seam)处,并且能够透过该接口进行测试。文档给出的三个价值维度是:

  • 对调用方的杠杆(leverage for callers):学会一小块接口,换取大量现成行为;
  • 对维护者的局部性(locality for maintainers):变更、缺陷、知识与验证集中在一处;
  • 对所有人的可测试性(testability for everyone):调用方与测试跨过同一条接缝,行为可通过接口直接观测。

3. 共享设计词汇表(Glossary)

这是整个技能最核心的部分。文档要求精确使用以下术语,禁止用 "component"、"service"、"API"、"boundary" 等词替代——"语言的一致性是这件事的全部意义(Consistent language is the whole point)"。

术语精确定义文档明确避免的替代词
Module(模块)任何"有接口、有实现"的东西。刻意保持规模无关(scale-agnostic):可以是一个函数、一个类、一个包,或横跨多个层的切片。unit、component、service
Interface(接口)调用方为正确使用模块必须知道的一切:类型签名只是其一,还包括不变量(invariants)、顺序约束(ordering constraints)、错误模式(error modes)、必需配置、性能特征API、signature(太窄,只指类型层面的表面)
Implementation(实现)模块内部的东西,其代码主体。与Adapter区分:一样东西可以是"小适配器 + 大实现"(如一个 Postgres repository),也可以是"大适配器 + 小实现"(如一个 in-memory fake)。当接缝本身是话题时用 "adapter",其他情况用 "implementation"。
Depth(深度)接口处的杠杆:调用方(或测试)每学会一个单位接口所能驱动的行为量。接口小而行为多则模块深(deep),接口复杂到与实现差不多则模块浅(shallow)
Seam(接缝)出自 Michael Feathers:一个"可以在不编辑该处的情况下改变行为"的位置,即模块接口所在的位置。接缝放在哪里本身是一个独立的设计决策,与接缝后面放什么无关。boundary(与 DDD 的 bounded context 语义冲突)
Adapter(适配器)在接缝处满足某个接口的具体实现。它描述的是角色(占据哪个槽位),而不是内容(里面是什么)。
Leverage(杠杆)调用方从深度中获得的收益:每学会一个单位接口换更多能力。一份实现在 N 个调用点与 M 个测试中反复兑现。
Locality(局部性)维护者从深度中获得的收益:变更、缺陷、知识与验证集中在一处,而不是散落在各调用方。"修一次,处处修好(Fix once, fixed everywhere)"。

值得注意的两个细节:

  1. Interface 的定义远宽于"方法签名"。错误模式、顺序约束、性能特征都是接口的一部分——这意味着接口设计评审时不能只看类型。
  2. Adapter 与 Implementation 的分工解释了为什么文档禁止用 "component/service":同一对象在"讨论接缝"与"讨论内部"两种语境下应切换术语,否则无法区分"槽位"与"槽位里的东西"。

4. 深 vs 浅:深度是接口属性

文档用两幅 ASCII 图给出对照:

┌─────────────────────┐ │ Small Interface │ ← Few methods, simple params ├─────────────────────┤ │ │ │ Deep Implementation│ ← Complex logic hidden │ │ └─────────────────────┘
┌─────────────────────────────────┐ │ Large Interface │ ← Many methods, complex params ├─────────────────────────────────┤ │ Thin Implementation │ ← Just passes through └─────────────────────────────────┘

深层模块 = 小接口 + 大量实现浅层模块 = 大接口 + 少量实现(应避免,典型形态是"接口几乎和实现一样复杂、实现只是透传")。设计接口时文档给出三个自检问题:

  • 能否减少方法数量?(Can I reduce the number of methods?)
  • 能否简化参数?(Can I simplify the parameters?)
  • 能否把更多复杂度藏到内部?(Can I hide more complexity inside?)

5. 四条设计原则

SKILL.md 的 Principles 一节给出四条原则,每条都是可操作的判定规则:

  1. 深度是接口的属性,不是实现的属性。一个深层模块内部完全可以由小的、可 mock、可替换的部件组成——只是这些部件不属于接口。模块因此可以同时拥有内部接缝(internal seam,私有于实现、供模块自己的测试使用)和位于其接口处的外部接缝(external seam)。
  2. 删除测试(The deletion test)。想象删掉这个模块:如果复杂度随之消失,说明它只是透传(pass-through);如果复杂度会在 N 个调用方中重新出现,说明它"挣得了自己的存在"(earning its keep)。
  3. 接口就是测试面(The interface is the test surface)。调用方和测试跨过同一条接缝。如果你发现自己想测试接口之外(past the interface)的东西,模块的形状多半是错的。
  4. 一个适配器意味着假想接缝,两个适配器才是真实接缝。除非有东西真的在某条接缝上发生变化,否则不要引入接缝。

第 4 条与 DEEPENING.md 中的"接缝纪律"(Seam discipline)相互印证:单个适配器的接缝只是纯粹的间接层(just indirection);通常生产实现 + 测试实现这两个适配器同时成立时,port 才值得引入。

6. 面向可测试性的接口设计

"好的接口让测试变得自然而然(Good interfaces make testing natural)"。文档给出三条规则,并各配一组 TypeScript 对照示例:

规则 1:接受依赖,而不是自己创建依赖

// Testable function processOrder(order, paymentGateway) {} // Hard to test function processOrder(order) { const gateway = new StripeGateway(); }

规则 2:返回结果,而不是制造副作用

// Testable function calculateDiscount(cart): Discount {} // Hard to test function applyDiscount(cart): void { cart.total -= discount; }

规则 3:保持小表面(Small surface area)

方法越少 = 需要的测试越少;参数越少 = 测试准备越简单。这条与第 4 节的"深 vs 浅"自检问题是一体两面:小表面同时是调用方的杠杆来源和测试成本的下界。

7. 概念间的关系(Relationships)

文档用一个关系清单把术语网络钉死:

  • 一个Module恰好拥有一个Interface(它呈现给调用方与测试的表面);
  • DepthModule的属性,以其Interface为度量基准;
  • SeamModuleInterface所在的位置;
  • Adapter位于Seam之上并满足Interface
  • Depth为调用方产出Leverage,为维护者产出Locality

这段文字实质上定义了一张概念图:Module → Interface(位于 Seam)→ Adapter 满足之,而Depth作为ModuleInterface的度量,派生出两个收益端。任何评审对话中若出现术语混用(例如把 seam 说成 boundary),都可以回到这张图仲裁。

8. 明确拒绝的表述框架(Rejected framings)

文档专门列出三个"不采用"的定义及其理由,这对读者避免误用非常关键:

  1. 拒绝"深度 = 实现行数 / 接口行数"(Ousterhout 的比值定义):它奖励往实现里灌水。本文档采用depth-as-leverage(深度即杠杆)——调用方每单位接口知识换到的行为量。
  2. 拒绝把 "Interface" 理解为 TypeScript 的interface关键字或类的公有方法:太窄。此处的 interface 涵盖调用方必须知道的每一个事实(不变量、错误模式、配置、性能特征)。
  3. 拒绝 "Boundary" 一词:与 DDD 的 bounded context 语义重载。统一说seaminterface

9. DEEPENING:给定依赖条件时安全加深一簇浅层模块

DEEPENING.md 回答的问题是:选定了一个加深候选(deepening candidate)之后,它的依赖决定加深后的模块如何跨过接缝被测试。方法是先给依赖分类,类别决定测试策略。

9.1 依赖的四种类别

类别定义可否加深与测试方式
1. In-process(进程内)纯计算、内存状态、无 I/O。永远可加深——直接合并模块,透过新接口测试,无需适配器。
2. Local-substitutable(本地可替代)依赖存在本地测试替身(如 Postgres 用 PGLite、文件系统用内存实现)。替身存在即可加深。替身在测试套件中运行;接缝是内部的,模块外部接口上不放 port。
3. Remote but owned(远程但自有,Ports & Adapters)自己拥有的跨网络服务(微服务、内部 API)。在接缝处定义port(接口)。深模块拥有逻辑,传输作为adapter注入;测试用内存适配器,生产用 HTTP/gRPC/队列适配器。
4. True external(真正外部,Mock)不自己控制的服务(Stripe、Twilio 等)。加深后的模块把外部依赖作为注入的 port 接收;测试提供 mock 适配器。

文档为第 3 类给出了一种标准建议句式,可视为模板:

"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."(在接缝处定义 port,为生产实现 HTTP 适配器、为测试实现内存适配器,使逻辑即使跨网络部署也仍位于一个深模块中。)

9.2 接缝纪律(Seam discipline)

  • 一个适配器 = 假想接缝,两个适配器 = 真实接缝。除非至少两个适配器同时成立(通常是生产 + 测试),否则不要引入 port。
  • 内部接缝 vs 外部接缝。深模块可以拥有内部接缝(私有于实现、供其自身测试使用)以及位于接口处的外部接缝。不要因为测试用到了内部接缝,就把它暴露到接口上。

9.3 测试策略:替换,而不是叠加(replace, don't layer)

  • 浅层模块上的旧单元测试,在"深模块接口处的测试"存在之后就变成了浪费——删掉它们
  • 新测试写在加深后模块的接口处。接口就是测试面
  • 测试断言通过接口可观测的结果,而不是内部状态;
  • 测试应当能在内部重构后存活——它们描述行为而非实现。如果一个测试因为实现变了就必须修改,说明它测试越过了接口。

10. DESIGN-IT-TWICE:并行子智能体探索备选接口

DESIGN-IT-TWICE.md 把 Ousterhout 的 "Design It Twice" 思想(第一个想法不太可能是最好的)操作化为一个三步流程,面向 AI 智能体工作流:

第 1 步:框定问题空间(Frame the problem space)。在派生子智能体之前,先写一段面向用户的问题空间说明:

  • 新接口必须满足的约束;
  • 它将依赖的依赖项,以及各自属于第 9.1 节的哪个类别;
  • 一段粗略的示意性代码草图——不是提案,只是让约束具体化。

展示给用户后立即进入第 2 步,让用户在子智能体并行工作期间阅读思考。

第 2 步:并行派生子智能体。至少 3 个,每个必须产出截然不同的接口设计。给每个子智能体的技术简报相互独立(文件路径、耦合细节、依赖类别、接缝后是什么),且各带一条不同的设计约束:

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

简报中须同时包含 SKILL.md 的架构词汇与 CONTEXT.md 的领域词汇,使各子智能体命名保持一致。每个子智能体必须输出五样东西:

  1. 接口(类型、方法、参数,外加不变量、顺序、错误模式);
  2. 展示调用方如何使用的示例;
  3. 实现藏在接缝之后的内容;
  4. 依赖策略与适配器(对应 DEEPENING 的类别);
  5. 权衡——杠杆在哪高、哪里薄。

第 3 步:依次呈现并比较。按顺序呈现每个设计让用户消化,然后用文字比较,比较维度固定为三个:depth(接口处的杠杆)、locality(变更集中何处)、seam placement(接缝放置)。比较之后必须给出自己的推荐——哪个设计最强、为什么;如果不同设计的元素可以良好组合,提出混合方案。文档特别强调:要敢于下判断(Be opinionated),用户要的是一个强观点,而不是一个菜单。

11. 在 Windmill 仓库中如何被实际使用

理解这套词汇的落地,关键是看它的消费方 improve-codebase-architecture/SKILL.md 定义的工作流:

  1. Explore(探索):先读项目的领域词汇表 CONTEXT.md,再让子智能体走查代码库,专门寻找:理解一个概念需要在哪一堆小模块之间来回跳转?哪些模块是浅的?哪些纯函数只为可测试性而抽出、真正的 bug 却藏在调用方式里(缺乏 locality)?哪些紧耦合模块在接缝处泄漏?哪些部分难以透过现有接口测试?并对任何疑似浅层的对象做删除测试——删除它会集中复杂度(想要的信号)还是只是移动复杂度?
  2. Present(呈现):把候选"加深机会"写成自包含 HTML 报告,每个候选一张卡片(Files / Problem / Solution / Benefits / Before-After 图 / 推荐强度徽章StrongWorth exploringSpeculative),Benefits 必须用 locality 与 leverage 的语言表述。呈现时不预先提出接口,而是问用户"这些里你想探索哪个?"
  3. Grilling loop(追问循环):用户选定候选后走决策树(约束、依赖、加深模块的形状、接缝后是什么、哪些测试幸存);对话中若给加深模块起了 CONTEXT.md 里没有的概念名,就补进词表;若需要探索备选接口,则回到本文的 Design It Twice 并行子智能体模式。

这里体现出 Windmill 的双层词汇体系

  • 架构名词(module、interface、seam、depth、adapter、leverage、locality)固定来自codebase-design
  • 领域名词固定来自 CONTEXT.md,其中钉住了 Windmill 的专有领域语言,例如Step(流中的一个节点,代码中类型为FlowModule,刻意避免与架构意义上的 module 混用——这正是 codebase-design 词表"避免词"清单在领域层的实际应用)、Step setting(重试、错误处理、超时、并发限制、缓存、防抖、提前停止、跳过、挂起、休眠、生命周期等按步运行时选项)、Configured(某个设置对象存在于步骤上;刻意不等于"会改变运行时行为",sleep0也算 configured)、Trigger step(轮询流的第一步,空返回意味着无新数据,流提前停止并标记 skipped 而非 failed)、Member / Role / Owner等权限词汇,以及各自的避免词。

HTML-REPORT.md 进一步约束报告文风:架构名词与动词必须直接取自/codebase-design词表,"简洁不是术语漂移的借口";遇到不在词表里的词,先找词表里现成的,不要自造。从源码结构看,这一整套约定共同保证了:无论人类评审还是 AI 智能体输出,谈论 Windmill 架构时 "step intake module" 这样的表达(领域词 + 架构词)是唯一合法形式,而 "FooBarHandler" 或 "Order service" 一类命名会被视为词表违规。

另外,UPSTREAM.md 还记录了本仓库对上游技能做的最小化本地改动(压平目录、把 markdown 链接改为仓库根路径的散文引用、移除 ADR 相关条款等),并说明刷新方式——这对想理解"为什么这些 SKILL.md 文件里用仓库根路径写同伴文件引用、而不是相对链接"的读者是直接依据。

12. 落地清单:如何把这套方法论用到一次真实重构

综合三个文档,一次完整的"发现浅层 → 设计深模块 → 落地"流程可以归纳为:

  1. 识别:对候选模块做删除测试;检查它是浅层的(接口复杂度 ≈ 实现复杂度、纯透传)还是深模块;
  2. 分类:按 In-process / Local-substitutable / Remote-but-owned / True-external 给依赖归类,由此确定测试替身或 port/adapter 方案;
  3. 设计:回答三个自检问题(减方法、简参数、藏复杂度);若拿不准,走 Design It Twice,用 3+ 个带不同约束的并行设计互相竞争;
  4. 比较:固定按 depth、locality、seam placement 三轴比较,给出明确推荐(必要时混合);
  5. 落地与测试:只在该模块的接口处写测试(接口即测试面),删除被取代的浅层模块旧单测(replace, don't layer);接缝上确认"两个适配器"成立,否则先不引入 port;
  6. 保持语言一致:全程使用本文词表;领域命名同步更新到 CONTEXT.md。

这套方法论的价值不在于任何单条原则,而在于词表 + 原则 + 流程构成的闭环:术语精确使评审可仲裁(第 7 节的关系图),原则给出判定规则(删除测试、接缝纪律),流程保证执行不漂移(依赖分类决定测试策略,Design It Twice 防止第一个想法赢)。在 Windmill 这样一个横跨 Rust 后端、Svelte 前端、多语言脚本运行时的仓库里,让智能体与人类共享同一套架构语言,正是这套技能体系被引入.agents/skills/的直接目的。

【免费下载链接】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),仅供参考

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

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

立即咨询