Spec-Kit 实战:基于规范驱动开发落地项目新功能
复盘一篇个人项目真正接入 Spec-Kit 的完整过程——不只是介绍工具,而是把真正踩过的坑、实际产出的代码、以及这套方法论对 AI 生成代码质量的提升讲清楚
项目开发的某些痛点
在实际用 AI 写前端代码的过程中,碰到过这么几个反复出现的问题:
给 ChatGPT/Claude 一段自然语言描述,它开始凭空发明不存在的 API、不存在的库、完全偏离项目架构的设计
每次新对话都要重新描述一遍项目背景,Token 烧得飞快,代码质量还不可控
需求稍微复杂一点(跨多个文件、涉及数据库迁移、需要前后端协同),AI 就开始"跑偏",输出几轮之后就完全失去上下文
Spec-Kit 就是 GitHub 为了解决这一类问题给出的答案——它不生成代码,它约束 AI 怎么生成代码
以下以问卷系统项目为背景,完整复盘用 Spec-Kit 从零落地一个现有项目的「物料管理」功能的整个过程
Spec-Kit 是什么
SDD(规范驱动开发)不是新概念,但 Spec-Kit 让它"可执行"了
Spec-Driven Development 的核心思想很简单:在写代码之前,先把"要做什么"写成一份结构化的、可验证的规范文档
但传统 SDD 有一个天然的落地障碍——规范是人写的,代码也是人写的,两者之间的同步靠的是人的纪律性。项目一赶进度,规范文档就成了摆设
Spec-Kit 做的事情就是把规范文档变成 AI 代码生成的"硬约束":
Constitution(项目宪法):定义项目级的不可违背规则——用什么技术栈、什么不能做、代码规范是什么。AI 在生成任何代码之前,必须遵守这份宪法
Spec(需求规格):用结构化模板描述"用户要什么",严格禁止写实现细节。这不是给开发者看的 PRD,而是给 AI 看的约束条件
Plan(实施计划):AI 根据 Spec 和 Constitution 自动生成的技术方案,包括接口设计、数据模型、文件结构
Tasks(任务拆分):将 Plan 拆成可独立执行、可独立验证的任务列表,每个任务对应具体的文件路径
Implement(批量执行):AI 按 Tasks 逐任务生成代码,每完成一个用户故事就可以停下来独立验证
和普通 AI 代码生成的根本区别:
环境依赖与安装
Spec-Kit 目前通过 uvx(Python 生态的 npx 等价物)分发,安装本质上是把一个 CLI 工具拉下来,然后在项目根目录执行初始化
前置条件:
# 确保 uv 已安装(Windows 下同样适用)
uv --version # >= 0.6.x初始化命令(项目实际执行的):
uvx --from git+https://github.com/github/spec-kit.git specify init . \
--here \
--integration claude \
--script ps \
--force参数解释:
--here:直接装进当前目录,不新建子目录。这个项目已经是已有的 monorepo,不需要套一层--integration claude:生成 Claude Code 专用的技能文件(放在.claude/skills/下),后续在 Claude Code 里直接敲/speckit-specify就能用--script ps:辅助脚本用 PowerShell 版本(Windows 环境)--force:幂等安装,已有文件会被覆盖
初始化完成后,项目里多了三样东西:
.specify/目录——工具链状态、模板、脚本、constitution 配置文件.claude/skills/speckit-*目录——10 个 Claude Code 技能(specify / clarify / plan / tasks / implement / analyze / checklist / constitution / converge / taskstoissues)specs/目录——后续所有新功能的需求规格、实施方案、任务列表都会以NNN-feature-name/格式存放在这下面
Constitution:项目的"宪法"
初始化后有一个最关键的文件:.specify/memory/constitution.md。它是整个 Spec-Kit 工作流的基石——AI 在任何阶段生成任何内容之前,都必须先检查这份宪法
回到我的项目实际来看,constitution 里定义了 10 条核心原则,摘几条最能说明问题的:
Principle I(模块边界完整性):
q-serveris the single source of truth for persistence. Code MUST NOT reach into another app'ssrc/via relative imports.
这条规则直接约束了 AI 在生成代码时的模块引用方式——它不能为了图方便直接从 app/q-editor/src 引用东西到 app/q-server/src,必须通过 packages/common 共享包。在后续物料管理功能中,前端和后端的共享类型定义就严格遵守了这一条——FileType 枚举和 ALLOWED_IMAGE_TYPES 常量统一维护在 packages/common/src/survey/survey-file.interface.ts 中
Principle VII(代码规范非协商门禁):
All TypeScript/Vue/JS files MUST pass the monorepo root ESLint flat config and Prettier with zero warnings on changed files.
这条保证了 AI 生成的代码不会引入新的 lint 错误——所有文件在提交前都经过了 Prettier → ESLint → cspell 三道检查,而 Spec-Kit 生成代码时会自动遵循这套规范
Principle III(统一 API 契约与响应信封):
The canonical response envelope for every HTTP API MUST be
{ code: number, msg: string, data: T | null }, wherecode: 0denotes success.
这意味着 AI 生成任何新接口时,都不能发明自己的返回格式。物料管理模块的 7 个新接口全部使用这套标准信封,和项目里已有的 50+ 个接口完全一致
有了 constitution 这个"前置护栏",Spec-Kit 的后续阶段才有了可执行的基础——它不是给开发者看的文档,它是 AI 代码生成的硬约束
「物料管理」功能完整实战
业务背景
我的问卷系统平台上有大量与问卷、用户相关的图片资源:
问卷题目中的图片选择题(PicItem)
手写签名图片(Signature)
问卷封面图
用户头像
这些图片分散在不同功能模块的上传入口中,管理员无法在一个地方看到"平台上到底有哪些图片"。更麻烦的是,删除冗余文件时可能破坏线上已发布问卷的展示
所以需要做一个物料管理功能——一个管理员专属的模块,能统一浏览、筛选、删除、审核全平台所有图片资源
接下来完整还原用 Spec-Kit 把这套需求从一段自然语言变成 45 个可执行任务、最终产出真实业务代码的整个过程
第一步:用 constitution skill 编写项目宪法
在前面安装阶段,constitution 已经写好了。但 Spec-Kit 提供了一个 /speckit-constitution 技能,可以在后续迭代中修改或增补宪法,它会在更新后自动检查所有依赖模板是否仍然兼容
我的 constitution 是手动写的(因为项目已有明确的架构设计),核心围绕四个包(q-server / frontend / q-editor / ai-service)的技术栈锁定与模块边界约束展开
第二步:用 specify skill 编写需求规格
在 Claude Code 中输入:
/speckit-specify 为问卷系统开发一个完整的物料管理功能模块...Specify 技能做了三件关键的事:
自动编号:检查
specs/下已有 001/002/003 三个特性目录,自动分配004-material-management生成结构化 spec 文档:基于
.specify/templates/spec-template.md模板,填充用户故事、功能需求、成功标准强制质量检查:生成完毕后自动检查是否存在
[NEEDS CLARIFICATION]标记、所有需求是否可验证
生成出来的 spec.md 摘一段关键内容(User Story 2——这是真正影响架构设计的决策点):
### User Story 2 - 管理员删除物料 (Priority: P2)
**Acceptance Scenarios**:
1. **Given** 管理员在物料列表页选中一条物料,**When** 执行删除操作,
**Then** 该物料的记录与其对应的底层文件均被移除,列表中不再展示
2. **Given** 一条物料仍被某个已发布问卷的题目直接引用,
**When** 管理员尝试删除该物料,
**Then** 系统阻止本次删除,并明确告知管理员该物料当前仍被哪些对象引用,
要求先解除引用关系后才能删除注意第 2 条——Specify 阶段帮我发现了两个需要用户决策的关键分歧点:
这两个决策如果等到写代码的时候才发现,要么会做错(按错误假设实现),要么会反复返工。Specify 阶段强制在需求层面解决了,避免了后续浪费
Specify 阶段的核心价值:
强制把"想做什么"完整地想清楚,而不是边写边改
检测出需求中的模糊点和分歧点,在动手之前一次性敲定
产出的 spec 全是业务语言,不涉及任何技术实现,但足够具体到能被 AI 翻译成 plan
第三步:用 plan skill 生成实施方案
Spec 写好后,用 /speckit-plan 进入实施规划阶段:
/speckit-planPlan 阶段本质上是 AI 把 spec 文档"翻译"成技术方案的过程。它产出了以下几个文件:
plan.md:技术上下文(Node.js 22、Fastify 5、Prisma 7、TypeScript)、Constitution 合规检查(逐一过 10 条原则,确认无冲突)、项目结构(前端 app/frontend/src/views/media-asset-management/、后端 app/q-server/src/modules/media-asset/、共享包 packages/common/src/)
research.md:几个关键技术决策的调研记录,比如"存量 SurveyFile 模型如何平滑升级为 MediaAsset 而不丢数据"——这个问题在 research 阶段确认了方案:用 Prisma 的 RENAME TABLE 而非删表重建,并为存量行的新字段设置合理默认值
data-model.md:数据模型设计,定义了 MediaAsset 模型的全部字段及其与 User、Survey 的关系。核心变更是在原有 SurveyFile 表上新增 resource_type、review_status、reviewed_by 等字段,并新增 MediaAssetReviewer 反向关联
contracts/media-assets-api.md:7 个接口的契约定义(方法、路径、请求体 Zod Schema、响应体结构、错误码约定)
Plan 阶段产出了一个关键的设计决策:把"将 SurveyFile 重命名为 MediaAsset"放在了 Phase 2(Foundational)的最前面,因为它会影响 5 处存量代码——这个依赖关系如果搞错顺序,后续所有任务都会在错误的基础上开发
第四步:用 tasks skill 拆分任务
用 /speckit-tasks 把 Plan 拆成可执行的任务流水线:
/speckit-tasks生成出来的 tasks.md 包含 45 个任务,按优先级分成了 7 个阶段:
Phase 1: Setup(4 个任务)—— 文件骨架,空实现
Phase 2: Foundational(8 个任务)—— 数据模型迁移 + 存量代码适配
Phase 3: US1 浏览资产(8 个任务)—— P1,MVP 交付物
Phase 4: US2 删除治理(7 个任务)—— P2
Phase 5: US3 审核状态(8 个任务)—— P3,预留给未来 AI 审核 Agent
Phase 6: US4 直接上传(6 个任务)—— P4
Phase 7: Polish(4 个任务)—— lint、type-check、文档、安全回归每个任务都严格遵循了这个格式:
- [ ] T009 在 app/q-server/src/modules/media-asset/media-asset.service.ts
中实现共享的引用检测函数 detectReferences(fileUrl)...格式里的标签含义:
[P]:可并行执行(不同文件、无依赖)[US1]:归属某个用户故事
这引出了 Spec-Kit 在项目推进层面最实用的一个设计:按用户故事(User Story)组织任务,而不是按技术层(前端 / 后端 / 数据库)组织
为什么这很重要?因为这样每个 User Story 都可以独立开发和验证。做完 US1(浏览资产),管理员就能看到物料列表了,已经是一个可部署的价值交付。不需要等全部 45 个任务做完才能上线
第五步:用 implement skill 批量生成代码
用 /speckit-implement 按 tasks.md 的顺序逐任务执行代码生成
这是整个流程里最"真实"的阶段——也是最能体现 Spec-Kit 和普通 AI 编码区别的地方。我拣几个有代表性的产出片段来说
任务 T005(数据库 Schema 变更)的核心部分:
// 重命名后的 MediaAsset 模型(prisma/schema.prisma)
model MediaAsset {
id BigInt @id @default(autoincrement())
survey_id BigInt?
user_id BigInt
file_url String @db.VarChar(1024)
file_key String @db.VarChar(512)
file_name String @db.VarChar(255)
mime_type String @db.VarChar(127)
file_size BigInt
file_type FileType @default(survey_option_image)
resource_type String @default("image")
review_status ReviewStatus @default(pending)
reviewed_by BigInt?
reviewed_at DateTime?
review_comment String? @db.Text
updated_at DateTime @updatedAt
created_at DateTime @default(now())
survey Survey? @relation(fields: [survey_id], references: [id], onDelete: SetNull)
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
reviewer User? @relation("MediaAssetReviewer", fields: [reviewed_by], references: [id])
@@index([review_status])
@@index([user_id, review_status])
@@map("media_assets")
}配套的安全迁移 SQL(手写,因为 Prisma Migrate Dev 默认会把表重命名误判为删表重建):
-- 用 RENAME 保留存量数据,而不是 DROP + CREATE
ALTER TABLE survey_files RENAME TO media_assets;
ALTER INDEX survey_files_pkey RENAME TO media_assets_pkey;
-- 新增字段,历史数据统一标记为 pending
ALTER TABLE media_assets ADD COLUMN resource_type VARCHAR DEFAULT 'image';
ALTER TABLE media_assets ADD COLUMN review_status "ReviewStatus" DEFAULT 'pending';
-- ... 后续索引与约束重命名任务 T016(列表查询 Service)的核心逻辑:
// media-asset.service.ts
async listMediaAssets(query: MediaAssetListQuery) {
const { page, pageSize, userId, surveyId, reviewStatus, keyword } = query;
const where: Prisma.MediaAssetWhereInput = {};
if (userId) where.user_id = BigInt(userId);
if (surveyId) where.survey_id = BigInt(surveyId);
if (reviewStatus) where.review_status = reviewStatus;
if (keyword) {
where.OR = [
{ file_name: { contains: keyword } },
{ file_url: { contains: keyword } }
];
}
const [list, total] = await Promise.all([
this.fastify.prisma.mediaAsset.findMany({
where,
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { created_at: "desc" },
include: { user: { select: { email: true, username: true } } }
}),
this.fastify.prisma.mediaAsset.count({ where })
]);
return { list, total, page, pageSize };
}任务 T009(引用检测——这是整个功能里最"有嚼头"的一行代码):
async detectReferences(fileUrl: string): Promise<MediaAssetReference[]> {
const refs: MediaAssetReference[] = [];
// 扫描所有未被软删除且状态为草稿/发布的问卷
const surveys = await this.fastify.prisma.survey.findMany({
where: { deleted_at: null, status: { in: [0, 1] } },
select: { id: true, title: true, components: { select: { config: true } } }
});
for (const survey of surveys) {
for (const comp of survey.components) {
const configStr = JSON.stringify(comp.config);
if (configStr.includes(fileUrl)) {
refs.push({ type: "survey_component", surveyId: survey.id, surveyTitle: survey.title });
break;
}
}
}
// 检查用户头像引用
const avatarRefs = await this.fastify.prisma.userProfile.findMany({
where: { OR: [{ avatar_url: fileUrl }] },
select: { user_id: true }
});
for (const p of avatarRefs) {
refs.push({ type: "user_avatar", userId: p.user_id });
}
return refs;
}这段代码看似简单,但它依赖了 constitution 里定义的"模块边界"原则——它调用的是 fastify.prisma.mediaAsset,而不是直接访问数据库驱动,保证了和其他模块(survey-crud、file、upload)的调用方式完全一致
Spec-Kit 约束对代码质量的提升——以 Prisma 模型重命名为例:
存量 SurveyFile 模型在整个代码库里有 5 处引用:
modules/survey/file/file.routes.tsmodules/survey/file/file.service.tsmodules/survey/index.tsmodules/survey/survey-crud/survey-crud.service.tsmodules/survey/upload/upload.routes.ts
如果不用 Spec-Kit 而是自然语言描述"帮我把 SurveyFile 改成 MediaAsset",AI 大概率会漏掉其中几处。Spec-Kit 的 tasks 强制列出了每一处需要修改的文件路径,implement 阶段会逐一处理,这就是"结构化的任务拆分"和"自由对话式编码"的核心区别
实战总结
Spec-Kit 落地物料管理项目的实际收益
Token 消耗大幅下降:constitution 持久化了项目背景知识,AI 在每次对话开始时自动加载,不需要我在 prompt 里重复描述技术栈、模块结构、编码规范。这在多轮迭代开发中节省了大量上下文窗口
架构一致性得到保障:物料管理 7 个接口的返回格式和项目里已有的 50+ 个接口完全一致——这不是巧合,是 constitution 里"统一 API 信封格式"原则被 AI 严格遵守的结果。没有出现"这个接口作者喜欢用
success: boolean,那个接口用code: number"的分裂情况数据迁移安全性:
SurveyFile → MediaAsset的表重命名涉及 11 条存量数据(问卷签名图片等)。AI 在 plan 阶段就明确了"用 RENAME TABLE 而非删表重建"的策略,在实际执行迁移时存量数据完好无损地保留了下来。如果没有 Spec-Kit 的结构化规划,这类"数据库变更 + 存量数据保护"的场景在自由对话中几乎一定会翻车每个 User Story 可独立验证:做完 US1(浏览资产)后,管理员已经能在 1 分钟内通过筛选(按用户/问卷/审核状态)从全平台图片中定位到目标物料。这是一个完整可交付的 MVP,不需要等 US2-US4 全部完成
开发过程踩过的坑
坑一:Prisma Migrate 默认行为是删表重建
Prisma Migrate Dev 在检测到"模型名称变了"时,它的默认 diff 会生成 DROP TABLE survey_files + CREATE TABLE media_assets——这会把 11 条存量数据直接炸掉
Spec-Kit 的 tasks 里虽然规划了"执行迁移"这一步,但AI 无法代替连接真实的数据库。最终的解决方案是手动写迁移 SQL,用 ALTER TABLE ... RENAME TO 保留数据。教训:任务列表中的 pnpm prisma migrate dev 这类命令不能完全信任 AI 自动执行,需要人工检查迁移 SQL 的内容
坑二:backdrop-filter 让 fixed 定位失效
这句不属于 Spec-Kit,但属于之前开发中踩过的一个有意思的 CSS 坑——给导航栏加 backdrop-filter: blur() 后,嵌套在其中、没有设置 append-to-body 的 Element Plus Drawer/Dialog 的 position: fixed 定位会失效。原因:CSS 规范规定 backdrop-filter 会让宿主元素成为 fixed 后代的新 containing block。解决方案是给这三处弹层加 append-to-body 属性
坑三:Implement 阶段容易出现"只生成后端, 遗漏前端"
Spec-Kit 的 tasks 虽然是前后端并行的,但当前 AI 模型在处理大型任务列表时,后端的任务因为逻辑密度更高(Service 层、数据库交互、中间件注册),通常会占据更大的注意力权重。在 implement 阶段,需要人工逐项核对:每看到一个后端接口的任务,就去检查同 User Story 下是否对应了一个前端 API 封装 + 前端组件的任务。我的做法是在 tasks.md 旁边开了一个临时 checklist,逐任务手动打勾
关于 SDD 的几个观点
"Prompt Engineering 的上限是约定"——对于单文件、单函数的代码生成,prompt 技巧是有效的;但对于跨多文件、涉及数据库迁移、需要前后端协同的功能,没有 structured spec 做约束,prompt 再精致也无法消除架构层的偏差
"Constitution 是团队 AI 协作的最低共识"——如果团队里每个人给 AI 的描述方式都不一样,生成的代码风格一定会分裂。把不可违背的规则写进 constitution,等于在 AI 层面建立了和团队协作一样的"代码约定"
"不是所有项目都适合 Spec-Kit"——对于业务逻辑极轻的展示类前端项目(活动页、营销页),Spec-Kit 的 overhead 可能超过它的收益。但对于有多个业务模块、需要长期迭代维护的项目,Spec-Kit 的"一次性建立约束,持续降低后续 Token 成本"的模型是非常有价值的
普通前端项目 vs AI 生成类项目,分别适不适合接入
普通前端项目(已有团队、已有代码规范):如果项目只有 1-2 个页面且逻辑极轻,可能不必要。但如果项目有以下特征之一,就值得考虑—— (1) 有 3 个以上业务模块,且模块之间有共享数据;(2) 前后端分离,需要保持 API 契约一致性;(3) 多人协作,需要统一的代码生成规范
AI 生成类项目(以 AI 为核心生产力的项目):基本上是全场景适用。因为这类项目的核心痛点是"AI 产出质量不可控"——Spec-Kit 的 constitution + spec + plan + tasks 四层约束链条正好针对性解决这个问题
写在最后
Spec-Kit 不是银弹。它不能代替连数据库、不能代替调试 CSS、也不能自动搞定 docker-compose 起服务。但它解决了 AI 辅助编码中最核心的一个问题:约束
Spec-kit 基于规范驱动开发落地项目新功能
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。



评论交流
欢迎留下你的想法