GitHub Spec Kit 实战(四):读懂和干预 /speckit.plan------AI 最自由发挥的一步
前两篇讲了 specify 怎么写不偏、constitution 怎么定规矩。到 /speckit.plan 这一步,AI 最容易"自由发挥",也是人最该把关的地方。
为什么?
specify阶段有"不许写技术栈"的强约束,AI 戴着手铐写constitution是项目级硬规则,AI 读到会遵守plan阶段是第一次要 AI 选型 ------技术栈、依赖、架构、项目结构,全靠 AI 推断。完全没有"不许发挥"的限制
我前前后后看了 30 多份 AI 出的 plan.md,翻车方式高度雷同:
- 选型"看起来很合理"但和项目现状冲突(明明项目用 Spring Boot,AI 给规划了一套 Express + Prisma)
- "一次性做了 8 个功能模块"------P1 之外的也顺手规划了
- 选型决策没记录 rationale------3 个月后没人记得"为什么用 X"
- Constitution Check 流于形式------AI 自己写的、自检的、自通过的
这篇文章把 /speckit.plan 怎么读、怎么改、怎么卡住 AI 讲透。
plan.md 的骨架:6 个必填节
仓库的 plan-template.md 给的骨架:
| 章节 | 填什么 |
|---|---|
| Summary | 一句话讲明白做什么 + 怎么实现 |
| Technical Context | 6 个字段:语言/版本、主依赖、存储、测试、目标平台、项目类型 |
| Constitution Check | 双重门禁------Phase 0 前 + Phase 1 后各查一次 |
| Project Structure | 文档 + 源码目录树(3 选 1,未选的要删) |
| Complexity Tracking | 违反宪法时的"罪状表"(多数 plan 留空) |
| Phase 0/1 产物 | research.md / data-model.md / contracts/ / quickstart.md |
plan.md 命令源码里写明:
Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION)
Phase 1: Generate data-model.md, contracts/, quickstart.md
Re-evaluate Constitution Check post-design
两阶段不是装饰------Phase 0 解决"用什么",Phase 1 解决"怎么用"。AI 必须先做完 Phase 0 才有 Phase 1。
第一个关卡:Technical Context 别让 AI 瞎填
Technical Context 有 6 个字段,每个都可能 NEEDS CLARIFICATION:
**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
AI 翻车点 1:直接把 NEEDS CLARIFICATION 全填上"合理默认"
我看到的 plan 经常长这样:
Language/Version : TypeScript 5.6
Primary Dependencies : React 18, Express 4, Prisma
Storage: PostgreSQL 16
AI 自己拍脑袋填的。没有问你,没有写 rationale。
plan.md 命令源码里的原话是"mark unknowns as NEEDS CLARIFICATION"------AI 应该不知道时标不知道,不是 AI 替用户决定。
正确做法: 给 plan 命令加约束(写在 constitution 的 Additional Constraints 里):
markdown
### VI. Plan Stage Discipline
- Technical Context 字段中,AI 推断出的选型必须用 [INFERRED: 原因] 标注
- 任何 [INFERRED] 项必须在 Phase 0 research.md 中给出 Decision/Rationale/Alternatives
- 用户未在 spec 中明确提到的技术选型,必须保留 NEEDS CLARIFICATION 让用户确认
读 plan 的第一步:打开 Technical Context,逐项检查------
- 有没有
NEEDS CLARIFICATION留下没解决的?(不该有,Phase 0 必须解决) - 有没有 AI 推断但没标 INFERRED 的?(项目里搜"Primary Dependencies"等关键字段,看 AI 是否偷偷填了)
- 推断是否和 codebase 现状冲突?(项目用 Spring Boot,AI 规划 Express------这就是冲突)
第二个关卡:Constitution Check 双重门禁
plan-template.md 顶部明文写:
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
两重门禁。Phase 0 前查一次------技术选型是否违反宪法;Phase 1 后查一次------数据模型、接口契约是否违反宪法。
AI 翻车点 2:Constitution Check 自写自审
我见过太多 plan 的 Constitution Check 段是:
Constitution Check
- ✅ Library-First: 本 feature 设计为独立模块
- ✅ CLI Interface: 暴露 CLI
- ✅ Test-First: 测试先行
- ✅ Observability: 结构化日志
All gates pass.
AI 自己写的、自检的、全部通过。这等于没有检查。
正确做法 :把 Constitution Check 改造成机械检查而不是 AI 自评。比如改成:
markdown
## Constitution Check
| Principle | Gate | Status | Evidence |
|-----------|------|--------|----------|
| I. Library-First | 是否独立 library? | ✅ | `packages/photo-album/` 独立目录,含独立 package.json |
| II. CLI Interface | 是否暴露 CLI? | ⚠️ | CLI 计划在 v1.1,v1 仅提供 HTTP API |
| III. Test-First (NON-NEGOTIABLE) | 测试是否先写? | ✅ | tasks.md 中 [TEST] 任务排在 [IMPL] 之前 |
| IV. Integration Testing | 是否规划集成测试? | ✅ | tasks.md 含 `tests/integration/` 任务 |
| V. Observability | 是否带结构化日志? | ✅ | data-model.md 含 audit_log 实体 |
**Violations requiring justification**: II. CLI Interface 推迟到 v1.1
(见 Complexity Tracking 第 1 条)
关键差异:
- 每条原则都问具体问题("是否独立 library?"),不是抽象的"是否符合"
- Status 用 ✅ / ⚠️ / ❌ 三档,不是非黑即白
- Evidence 列指向具体证据(文件路径、章节),不是"已规划"
- 违反项必须进 Complexity Tracking
第三个关卡:Project Structure 别让 AI 留 3 套选项
plan-template.md 给的源码段是这样:
markdown
### Source Code (repository root)
```text
# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
src/
├── models/
...
# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
backend/
...
frontend/
...
# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
api/
...
ios/ or android/
...
plan-template.md 注释里明文要求:
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout for this feature. Delete unused options and expand the chosen structure with real paths (e.g., apps/admin, packages/something). The delivered plan must not include Option labels.
AI 翻车点 3:3 个 Option 全部保留
我看过一个 plan,三套结构全留着,最后加一句"Selected: Option 1"。这是偷懒------AI 没做选型决策。
正确读法 :plan 里如果出现 Option 1 / Option 2 / Option 3 文字,直接打回让 AI 改。要求 AI:
- 选 1 个 Option
- 删掉另外 2 个的代码块
- 选中的 Option 展开成真实路径 (不是
src/models/这种占位) - 在
Structure Decision段写为什么选这个
相册例子改后:
markdown
### Source Code (repository root)
```text
packages/
├── photo-album-core/ # 核心库,Library-First 原则
│ ├── src/
│ │ ├── models/
│ │ ├── services/
│ │ └── lib/
│ └── tests/
│ ├── unit/
│ └── integration/
└── photo-album-cli/ # CLI 入口,CLI Interface 原则
└── src/
apps/
└── web/ # Web 界面,v1.1
├── src/
└── tests/
Structure Decision : 选 Option 1 变体 ------把源码拆成 packages/(核心库 + CLI)和 apps/(前端),因为宪法 Principle I 要求每个 feature 起步为独立 library,Principle II 要求暴露 CLI。
## 第四个关卡:读 research.md 看 AI 怎么选型
Phase 0 产出的 `research.md` 是 plan 里**最值得读的一份**。`plan.md` 命令源码里规定的格式:
> - Decision: [what was chosen]
> - Rationale: [why chosen]
> - Alternatives considered: [what else evaluated]
**AI 翻车点 4:Decision + Rationale 齐全,但 Alternatives 是凑数的**
我看过的 research.md 一半长这样:
> ### Question 1: 数据库选型
>
> **Decision**: PostgreSQL
> **Rationale**: 团队熟悉
> **Alternatives considered**: MySQL, MongoDB
**"团队熟悉"不是 rationale**。Alternatives 凑了 2 个名字、没讲为什么 rejected------3 个月后没人能回溯决策。
**正确版:**
```markdown
### Question 1: 主数据库选型
**Decision**: PostgreSQL 16
**Rationale**:
1. 团队 12 个工程师中 10 个有 PG 生产经验
2. ACID 强一致,FR-005(相册嵌套限制)需事务约束
3. EXIF 元数据(FR-001)用 JSONB 存储可减少表数量
4. TimescaleDB 扩展可应对 SC-001 性能要求
**Alternatives considered**:
- MySQL 8: 团队经验相同,但 JSON 支持弱于 PG;事务隔离级别对嵌套相册场景不够
- MongoDB: 无强事务,与 FR-005 冲突;运维成本高(团队无 NoSQL 经验,见 constitution Principle VI)
- SQLite: 单机性能优秀,但 SC-002 要求 5000 张照片 5 秒内检索,多用户并发不可行
**Trade-offs**:
- PG 学习曲线低(团队熟)
- 但 PG 写性能弱于 MongoDB 大批量插入------本场景是读多写少,可接受
每条 Alternative 都要讲为什么 rejected ------这才叫选型。读 plan 阶段最快的"过稿"方法:扫一眼 research.md 的 Alternatives 字段,全是敷衍描述的 plan 风险高,全部详细比较的 plan 风险低。
第五个关卡:data-model.md 别让 AI 替你做领域决策
Phase 1 产出的 data-model.md,由 plan.md 命令源码规定:
- Extract entities from feature spec →
data-model.md:
- Entity name, fields, relationships
- Validation rules from requirements
- State transitions if applicable
AI 翻车点 5:字段过多,把数据库 schema 写进 spec
我看过的 data-model.md:
markdown
### Photo
- id: BIGSERIAL PRIMARY KEY
- file_path: VARCHAR(500) NOT NULL
- exif_date: TIMESTAMP WITH TIME ZONE
- file_size: BIGINT
- mime_type: VARCHAR(50)
- upload_time: TIMESTAMP DEFAULT NOW()
- album_id: BIGINT REFERENCES albums(id)
- user_id: BIGINT REFERENCES users(id) NOT NULL
- created_at: TIMESTAMP DEFAULT NOW()
- updated_at: TIMESTAMP DEFAULT NOW()
- deleted_at: TIMESTAMP
这是 DDL,不是 data model。 字段类型、大小、DEFAULT、REFERENCES------全是 schema 决策。plan 阶段不该决定这些。
正确版:
markdown
### Photo
- **id**: 系统生成的唯一标识
- **file_path**: 照片存储路径(本地或对象存储,由 storage 层决定)
- **exif_date**: 拍摄日期(来自 EXIF,FR-001)
- **file_size**: 文件大小(用于 UI 显示)
- **mime_type**: 文件类型(用于前端展示)
- **album_id**: 所属相册(强约束:每张照片属且仅属一个相册,FR-005)
- **uploaded_at**: 上传时间(用于 UI 排序)
**Validation rules**:
- exif_date 缺失时使用 uploaded_at 作为 fallback(FR-001 的兜底)
- file_size > 0
- mime_type 必须在白名单 [image/jpeg, image/png, image/heic]
**State transitions**:
- 上传中 → 已归档(exif_date 解析完成)
- 已归档 → 已重分组(用户拖拽,FR-004)
- 已归档 → 已删除(v2 引入软删,v1 物理删除)
关键差异:
- 不写 SQL 类型(
BIGSERIAL/TIMESTAMP) - 不写 schema 细节(
REFERENCES/DEFAULT) - 写业务约束("每张照片属且仅属一个相册"------这条直接来自 FR-005)
- 写验证规则(白名单、兜底策略)
- 写状态机(v1 没有但预留扩展)
读 plan 阶段最快的"过稿"方法 2 :data-model.md 里出现 SQL DDL(CREATE TABLE、PRIMARY KEY、FOREIGN KEY、DEFAULT)------直接打回。
第六个关卡:contracts/ 和 quickstart.md
plan.md 命令源码对这两个的说明:
Define interface contracts (if project has external interfaces) →
/contracts/:
Identify what interfaces the project exposes to users or other systems
Document the contract format appropriate for the project type
Examples: public APIs for libraries, command schemas for CLI tools, endpoints for web services, grammars for parsers, UI contracts for applications
Skip if project is purely internal (build scripts, one-off tools, etc.)
Create quickstart validation guide →quickstart.md:Document runnable validation scenarios that prove the feature works end-to-end
Include prerequisites, setup commands, test/run commands, and expected outcomes
AI 翻车点 6:contracts 写成 API 文档,quickstart 写成 README
markdown
# contracts/api.md
## POST /api/photos
Body: multipart/form-data
Response: 200 OK
...
# quickstart.md
## Run the server
npm install
npm start
contracts 不是 API 文档 ------是契约。Web 服务的契约是 OpenAPI/GraphQL schema,CLI 工具是命令 schema(argparse 定义、exit code 列表、stdout 格式),库的契约是 public API 接口签名。
quickstart 不是 README ------是可执行的端到端验证场景 。plan.md 命令明文要求"runnable validation scenarios"------每个场景要能跑、跑完有明确预期结果。
相册例子的 quickstart:
markdown
## 验证场景 1: 单张照片自动归类
**Prerequisites**:
- PostgreSQL 已启动且空库
- 编译产物已生成
- 测试目录有 `fixtures/photo_2024_03_15.jpg`(EXIF 含 2024-03-15)
**Steps**:
```bash
$ pbm-cli upload fixtures/photo_2024_03_15.jpg
Expected output:
[INFO] Uploading photo...
[INFO] EXIF date detected: 2024-03-15
[INFO] Created album "2024 年 3 月" (id=1)
[INFO] Photo added to album 1
[OK] Upload complete
Validation:
sql
SELECT COUNT(*) FROM albums WHERE title = '2024 年 3 月'; -- 应返回 1
SELECT COUNT(*) FROM photos WHERE album_id = 1; -- 应返回 1
每个场景都包含:**前置条件**、**可执行命令**、**预期输出**、**验证 SQL**------**可以直接在 CI 里跑**。
## Complexity Tracking:违反宪法时的"罪状表"
`plan-template.md` 最后一段:
> **Fill ONLY if Constitution Check has violations that must be justified**
>
> | Violation | Why Needed | Simpler Alternative Rejected Because |
> |-----------|------------|-------------------------------------|
> | [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
> | [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
**关键三列**:
1. **Violation**: 违反了哪条
2. **Why Needed**: 当前什么需求必须违反
3. **Simpler Alternative Rejected Because**: 为什么简单的方案不行
**AI 翻车点 7:Complexity Tracking 写"我觉得需要"**
我看过的:
> | Violation | Why Needed | Simpler Alternative Rejected Because |
> |-----------|------------|-------------------------------------|
> | 引入 Kafka | 当前需求 | 简单方案不行 |
**等于没写**。`Why Needed` 是当前需求的具体痛点,不是"当前需求"。`Simpler Alternative Rejected Because` 要讲清楚试过哪个简单方案、卡在哪。
**正确版:**
> | Violation | Why Needed | Simpler Alternative Rejected Because |
> |-----------|------------|-------------------------------------|
> | 引入 Redis 缓存(违反 Principle VII Simplicity 倾向) | SC-001 要求 5000 张照片 5 秒内检索,PG 单表扫描 P95 850ms 不达标 | 1) 试过加 PG 索引:P95 降到 220ms,仍不达标<br>2) 试过分区表:维护成本高,小团队不划算<br>3) 试过只查最近 90 天照片:UX 部门拒绝(用户期望翻历史照片) |
> | 拆 4 个微服务(违反 Principle I 单一 library 倾向) | 用户上传链路涉及 EXIF 解析、AI 标签、缩略图、CDN 推送,单进程无法满足 SC-001 的 30s 要求 | 1) 试过单进程异步队列:P95 仍超时<br>2) 试过拆 2 个服务(上传/处理分离):处理服务仍是瓶颈<br>3) 折中方案:保留 2 服务,AI 标签和缩略图作为上传服务的 worker 进程 |
**关键特征**:
- 每个 Violation 都**试过简单方案**
- `Rejected Because` 列出**至少 2 个被否的方案**
- 数字、命令、版本号都给------可复现的拒绝理由
## plan 阶段过稿清单
拿到 plan.md 后,按这个清单走一遍:
Technical Context
-
没有未解决的 NEEDS CLARIFICATION
-
AI 推断项都标了 INFERRED 并有 research.md 对应
-
选型与 codebase 现状不冲突
Constitution Check
-
每条原则问具体问题,不是抽象陈述
-
Evidence 列指向文件/章节
-
违反项进了 Complexity Tracking
Project Structure
-
只保留 1 个 Option
-
展开成真实路径
-
Structure Decision 写为什么选
-
每个 Decision 有 Rationale
-
每个 Decision 有 Alternatives considered(≥2 个)
-
Alternatives 都写为什么 rejected
-
没有 SQL DDL(CREATE TABLE / PRIMARY KEY / FOREIGN KEY)
-
字段都是业务术语
-
Validation rules 引用具体 FR
-
State transitions 标明 v1/v2 范围
contracts/
-
是契约不是文档(OpenAPI/argparse/exit code list)
-
跳过项有说明("纯内部工具,跳过")
-
每个场景有 Prerequisites/Steps/Expected/Validation
-
命令可直接复制执行
-
预期输出可断言
Complexity Tracking
-
每行 Violation 试过 ≥1 个简单方案
-
Rejected Because 含具体数据
任何一项 ❌ 就打回让 AI 改。
plan.md命令是 spec 之后最值得花时间审的------后面/speckit.tasks直接消费 plan.md,plan 错了任务清单全错。写 plan 时卡住 AI 的 3 个实用技巧
最后说几个我在 constitution 里加的、专门管 plan 阶段的"硬规则":
1. INFERRED 标记强制
markdown### VII. Plan Stage: Mark All Inferences Technical Context 中任何 AI 推断的技术选型,必须用 [INFERRED] 包裹。 未标 [INFERRED] 的选型视为用户已确认。 Phase 0 research.md 中每个 [INFERRED] 必须有对应 Decision/Rationale/Alternatives。
效果:AI 不再"偷偷"填字段,每个推断都暴露在阳光下。
2. Project Structure 单一选项
markdown
### VIII. Plan Stage: Single Structure Only
plan.md 的 Project Structure 必须只保留 1 个 Option,
且必须展开为真实路径(不允许 src/、models/ 这种占位)。
保留多个 Option 视为 plan 失败。
效果:AI 不再"三选一"敷衍,必须做选型决策。
3. data-model.md 无 DDL
markdown
### IX. Plan Stage: data-model.md is Domain, Not Schema
data-model.md 不得出现 SQL DDL 关键字(CREATE/ALTER/DROP/INDEX/REFERENCES/DEFAULT)。
字段类型、大小、约束由 /speckit.tasks 阶段决定。
效果:data-model.md 聚焦业务领域,schema 决策留给 tasks。
小结
| 关卡 | 核心防翻车点 |
|---|---|
| Technical Context | 不让 AI 偷偷填,强制 INFERRED 标注 |
| Constitution Check | 双重门禁、机械检查、Evidence 指向文件 |
| Project Structure | 只保留 1 个 Option,展开真实路径 |
| research.md | 每个 Decision 都有 Alternatives + rejected 理由 |
| data-model.md | 没有 SQL DDL,业务约束代替 |
| contracts/ | 是契约不是文档 |
| quickstart.md | 可执行的端到端场景 |
| Complexity Tracking | 试过简单方案再写 Violation |
plan 阶段是 spec-kit 工作流里最需要人把关的一步 ------spec 阶段 AI 是戴手铐的,tasks 阶段 AI 是按 plan 走的。只有 plan 阶段 AI 第一次"选型",最容易跑偏。
下一篇会写 /speckit.tasks 怎么拆------task 颗粒度、依赖关系、并行度,都是这个阶段的活。