规范驱动开发:让AI生成符合预期的KMP代码

1 阅读11分钟

本文译自「Design a screen, get a Clean Architecture feature — Spec-Driven Development that keeps AI-generated KMP code from drifting」,原文链接proandroiddev.com/design-a-sc…,由Ali Sadeghi发布于2026年7月23日。

AI 生成的图像由 Gemini 提供。

描述简单来说,就是让AI在Android和iOS平台上完成设计、构建、测试和审查,并强制执行“整洁架构”(Clean Architecture),而不是仅仅寄希望于此。

使用AI生成功能时,有一个没人会提醒你的潜在问题:每个功能单独来看似乎都没问题。第一个屏幕很简洁,第二个屏幕也很简洁。但到了第五个屏幕,应用就出现了两种不同的状态约定、一个仓库悄悄地访问ViewModel,以及一个屏幕文件夹,其中的布局完全不一致。并非某个提示本身有问题,而是代码库发生了偏移,因为在提示之间,架构没有得到任何约束。

你无法通过改进提示来解决这个问题。你需要将架构作为模型必须满足的约束,而不是一个可以随意重新解释的建议。

这就是KMPilot的作用:我构建的一个模板,旨在让AI严格遵循“整洁架构”。我把它应用到了一个真正的应用程序上:Kickoff26,一个 2026 年世界杯配套应用,它是先设计后功能构建的。这篇文章记录了我从中学到的关于如何保持 AI 生成代码规范的经验。

为什么 AI 生成的代码会偏离规范

当你一次只处理一个提示来构建应用程序时,会出现四个问题。

模式偏离。 第一个提示生成了一个 UiState 密封类。第五个提示“改进”了它,现在同一个应用程序中存在两种不同的状态约定。

层泄漏。 从 A 到 B 的最快路径通常是通过一个原本不应该了解其他层的捷径。模型会选择这条捷径,因为它可以编译通过。

测试消失了。 它们是“先让它能运行”这种做法的首要牺牲品,而且直到所有程序都无法运行时,才有人注意到这一点。

设计永远无法与屏幕匹配。 模型图的边角是 24dp,并且带有特定的着色标题。最终发布的版本与设计图大致相同。但如果将这种情况乘以 20 个屏幕,应用程序看起来就完全不像是精心设计的了。

这些都不是智能问题,而是内存和执行方面的问题。该模型没有持久的记录来证明这套代码库是如何运作的,而且今天没有任何机制可以阻止它以不同的方式运行。

模型本身从来都不是问题所在。问题在于没有任何东西能够约束它的工作。

模式:规范驱动开发,应用于 KMP

规范驱动开发并非我首创。 GitHub 的 spec-kitOpenSpec 等工具已经让这种做法在通用代码库中流行起来:先编写规范,然后让 AI 基于规范进行构建,而不是在不断滚动的聊天记录中进行规划。其理念很简单:将约定写下来,放在代码旁边,让代码遵循规范。我的做法是将其应用于 Kotlin 多平台领域,并分为三个部分:

  • 架构是约束。 整洁的架构,每个功能都具有相同的结构,且不可更改。这并非出于礼貌:一个钩子会物理阻止对功能代码的直接编辑,因此模型_无法_悄悄地重塑它。它在结构内部执行。

  • 设计是输入,而不是事后考虑。 屏幕最初是一个已批准的原型,原型中的元素会流入代码。

  • 动态的 spec.md 文件就是规范。 每个功能对应一个规范,版本控制,并随着代码的更改而更新。它是模型在执行任何操作之前读取的内存。

底层,KMPilot 是一组运行在 Claude Code 之上的技能和代理Kickoff26 中的每个屏幕都是基于它构建的。

功能实际上是如何构建的

KMPilot 管道

以“比赛”选项卡为例:小组赛浏览器加上淘汰赛、32 强赛到决赛。没有任何一个提示可以构建它。它以一系列简短的技能组合在一起,每个技能都有一个工作,每个技能都留下一个工件,供下一个技能读取。

它从设计开始,带有***/***design-ui**。**你用简单的语言描述屏幕:

/design-ui 比赛 — 一个选项卡,可在小组赛赛程列表(可按比赛日和分组进行过滤)和从 32 强赛到决赛的淘汰赛列表之间切换

它通过 MCP 连接驱动 Stitch(Google 的 AI 设计工具):它生成一个模型,你可以通过与它交谈来完善它(将实时徽章设为红色,收紧括号间距),直到它正确为止。批准它是有趣的部分发生的地方。该技能从 Stitch 中拉回完成的屏幕,并通过令牌提取脚本运行它,该脚本直接从设计中提取每种颜色、半径、字体和间距值,而不是目测它们,然后下载模型使用的确切图标和图像。所有这些都落在蓝图中:设计捕获为明确的 Compose 指令,直至负进球差变成_error_红色,支架的连接器变成_Canvas_。屏幕将 Stitch 作为合约而不是屏幕截图,因此下一步将逐个代币构建它,而不是近似构建它。

附带的比赛屏幕:小组赛/淘汰赛、比赛日和小组过滤器、真实旗帜和比分。每个颜色、半径、字体和间距值都来自 Stitch 模型,而不是目测。

**然后使用 */***create-feature 进行构建。该技能是系统的核心。它已经有了设计、蓝图;你提供的是数据契约、API 端点和返回的形状:

/create-feature matches — 来自 GET /get/games 的赛程,其中每场比赛都有 home_team_id、away_team_id、local_date、stadium_id、matchday、type、finished 和 time_elapsed

从那里开始,它分阶段进行,在每个阶段之间停下来等待你的签字:

  1. 它将请求变成一个简短的PRD(功能的作用、屏幕、数据、边缘情况),然后等待。
  2. 一旦你批准,它会将 PRD 分解为离散的任务(数据层、UI、布线),然后再次等待。
  3. 只有在第二次确认后,它才会将任务交给并行运行的专门代理,每个代理都拥有一层:
  • 数据:JSON、存储库、Ktor 调用的可序列化模型
  • ui: ViewModel、Compose 屏幕及其组件
  • 集成: 依赖注入、导航、Gradle 连接
  • **平台:**共享接口背后的每个平台代码,仅当功能达到设备功能(GPS、相机、生物识别)时

匹配的是普通网络,所以前三个覆盖了它。因为每个代理都拥有一个单独的图层,所以它们永远不会发生碰撞,而且返回的也不是你手工完成的草图。它是一个完整的有线功能模块,每个功能的布局方式都相同:

特征结构

跨数据、演示文稿、 和 DI 的 32 个文件,每个文件均由单个设计和单个构建命令生成,其布局与应用程序中的所有其他功能完全相同。 (_在 GitHub 上浏览)。)*)

可预测的结构意味着你审查行为,而不是样板文件。

但该模块只是 _/_create-feature 生成的一半。除此之外,该技能还会编写 spec.md 并将其存储在功能树外部的 .claude/docs/matches/spec.md 中,并与项目一起进行版本控制和提交。该规范是功能的记忆,是结构化的而不是自由形式的:元数据标头(版本、状态、日期)、功能的目标和非目标、包含基本原理和被拒绝的替代方案的设计决策表,以及以 GIVEN / WHEN / THEN 场景编写的需求。匹配规范的修剪部分:

功能示例规范(匹配)

_这是简短的版本。 _ full matches/spec.md 存在于仓库中。版本号和注明日期的变更日志使每个功能的历史记录一目了然。

**稍后使用 */***modify-feature 进行更改。一旦某个功能存在,你就永远不会对其进行手动编辑;你描述了这一变化:

/modify-feature matches — 添加一个“实时”过滤器芯片,仅显示正在进行的匹配

它首先读取规范,根据已记录的决策计划更改,并通过相同的代理编辑功能,而不是手动编辑功能:hook 物理地阻止对 feature/ 下文件的原始编辑。完成后,它会写回规范:一个新版本,以及该变更日志中的新日期行。因为它从规范开始,所以它建立在现有设计的基础上,而不是重新调整它,并且代码和规范永远不会分开更新,因此两者不会相互偏离。

善意地询问模特并不是强制执行。阻止写入是。

剩下的技能都是门,同样的方式跑。 /verify-ui matches 根据设计标记重新检查构建的屏幕,_/_test-feature matches 编写测试套件(夹具、存储库、ViewModel、UI 和端到端传递),并且 _/_review-feature matches 根据架构规则审核结果。他们中的任何一个人都可以将工作交还给他人。这就是核心循环; 完整技能目录涵盖了其余部分。

诚实的部分

我一开始并不相信这一点。几周以来,我一直期待着打开这个项目并发现常见的人工智能蔓延:做同一件事的三种方法,一个层悄悄地泄漏到另一个层,无论如何我最终都会自己编写测试。它从未出现过。我最接近混乱的是我自己的:我让每个功能保留自己的网络层副本,到第四个功能时,重复就无法忽视。这通常是你一直推迟的清理工作,因为它涉及到一切。在这里,我描述了一次更改,规格准确地告诉了我每个功能决定了什么以及原因,并且它是在一个下午完成的,没有破坏任何东西。

它还很年轻:Kickoff26 仍然说_正在开发_,并且 KMPilot 有粗糙的边缘我还没有打磨。但在观察了人工智能辅助的代码库快进腐烂了几个月之后,我不断回想起的部分是,这个代码库并没有。一切都没有漂移。这就是重点。

尝试一下

如果你编写 Kotlin Multiplatform,并且你已经看到 AI 生成的代码下的代码库漂移,那么即使没有模板,该模式也值得窃取。让架构成为一个约束。将设计作为输入。给模型一个活生生的规格来阅读。

KMPilot 是我实际使用的版本,MIT 许可的,一个命令即可启动:

curl -fsSL https://raw.githubusercontent.com/ThisIsSadeghi/KMPilot/main/install.sh \
 | bash -s <MyApp> <com.acme.myapp>

Repo和完整管道:github.com/ThisIsSadeghi/KMPilot。如果这个想法引起了共鸣,一颗Star是告诉我继续建造它的最便宜的方式。

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!