拒绝 AI 乱写代码:实战OpenSpec规范驱动开发 # 什么是 OpenSpec? 你可以将其理解为一套**工程化协议**。它强制让你和 AI 在写代码之前,先通过标准化的 Markdown 文档就“做什么”和“怎么做”达成共识(Spec-Driven Development, SDD)。通过积累的Spec,AI可以大大缓解“由于缺乏上下文而产生幻觉”的问题。 - **传统模式**:Prompt -\> Code (vibe code) - **OpenSpec 模式**:Prompt -\> **Proposal** (提议) -\> **Spec** (规范) -\> **Code** # 安装openspec并初始化项目 安装工具 ```js npm install -g @fission-ai/openspec ``` 在你的项目根目录下运行: ``` openspec init ``` 初始化后,它会在项目根目录生成一个非常重要的文件,通常叫 `openspec/AGENTS.md` (或者针对 Cursor 的 `.cursor/rules/openspec.md`)。 这个文件本质上是一段**超长的系统提示词(System Prompt)** 。它里面写着类似这样的话(翻译后): > “你是一个 OpenSpec 智能代理。 > 当用户要求‘创建 Proposal’时,不要直接写代码。 > 请按以下步骤操作: > > 1. 在 `openspec/changes/` 下创建一个新目录。 > 2. 生成 `proposal.md`,包含 Context 和 Goals。 > 3. 生成 `tasks.md`...” 在每个项目开始的时候,你将`AGENTS.md`给AI,并对AI下达指令,AI会理解并按提示创建合理的结构,也就是拆解后的任务。 # 开始练手 我使用 Qwen CLI (配合 `qwen.qwen3-235b` 模型) 来进行这次实战。 ## 背景 现有代码中返回给上层的refunds列表中有过滤逻辑,`RefundID` 为 `null` 或 `0` 时会被过滤。 ```go for _, r := range p.Refunds { // Check if refund record is valid if r.RefundID == 0 || r.Amount == 0 || r.CreateTime == 0 { continue } // Add valid refund records refunds = append(refunds, &entity.Refund{ ID: strconv.FormatInt(r.RefundID, 10), ...... }) } ``` 但这个检查是不合适的,因为历史原因,有一些refund record的RefundID可能为null,转换后就会变成0,导致被错误过滤。 同时,代码直接将数据库 的refund ID 暴露给了前端,存在隐私风险。我们需要去除过滤逻辑并屏蔽 ID 的传递。 ## 从一句Prompt开始 首先,将`openspec/AGENTS.md`作为上下文传递给AI(你可以直接告诉AI,也可以用@的方式)。然后对AI说: ```js 创建一个 OpenSpec proposal,目标是: 在payment list接口中,会返回下层业务的Refunds。因为历史原因RefundID可能为null或0,导致数据被过滤。 你需要去除过滤逻辑。同时,不需要将refunds.id返回给上层。 ``` 观察 AI 的操作。如果配置正确,会看到 AI 开始执行一系列**文件操作(File System Tools)** : 1. **读取文件**:它可能会先读一下 `openspec/project.md`(如果有)来了解项目背景。 2. **创建目录**:它会执行 `mkdir openspec/changes/YOURTASKNAME`(名字由它定)。 3. **创建文件**:它会在该目录下创建 `proposal.md` 和 `tasks.md`。 结构类似下图:  对于这个任务,最重要的三个文件如下: `proposal.md` —— 它是【需求文档 / PR 描述】 **核心作用:定义“为什么要做” (The Why)** - **它的内容**:包含变更的背景(Context)、目标(Goals)、非目标(Non-Goals)以及用户故事。 - **工程师视点**:这就像你在 Jira 里开的一张 Ticket,或者是 GitHub Pull Request 的 Description 部分。 - **给 AI 看的作用**:防止 AI 跑偏。比如你的目标是“移除退款过滤”,Proposal 会明确界定“只移除前端过滤”还是“后端 API 也要改”。 - **生命周期**:它是一次性的。任务归档后,它只作为历史记录存在。 `spec.md` —— 它是【技术方案 / 架构蓝图】 **核心作用:定义“做成什么样” (The What)** - **它的内容**:这是最重要的文件。它包含 API 接口定义、数据结构变更、边缘情况处理、组件行为等**技术细节**。注意截图里的路径是 `specs/payment/spec.md`,这说明它正在定义(或修改)支付模块的规范。 - **工程师视点**:这就像是 RFC 文档、Swagger/OpenAPI 定义或者详细的技术设计文档(TDD)。 - **给 AI 看的作用**:这是 **Source of Truth(事实之源)** 。当 AI 开始写代码时,它**不应该**再看 Proposal(那是意图),而应该严格遵守 Spec(这是法律)。 - **生命周期**:它是持久的。当你运行 `openspec archive` 后,这个文件会被合并到项目根目录的 `openspec/specs/` 中,成为项目永久的技术资产。 `tasks.md` —— 它是【实施清单 / Todo List】 **核心作用:定义“怎么一步步做” (The How)** - **它的内容**:一系列可执行的、原子化的步骤。通常带有 Checkbox `[ ]`。 - 1. 修改 `RefundService` 移除过滤逻辑 - 2. 更新 `PaymentController` 测试用例 - 3. 验证前端列表显示 这个时候,AI一般会停下来,给用户机会**人工Review并微调这些任务**。 ## 审查并微调任务 1. **打开** **`spec.md`**:检查 AI 设计的接口对不对?有没有漏掉副作用?(例如:移除过滤会不会导致数据量过大搞崩前端?如果 Spec 里没写,现在就手动加上:“Spec 补充:需增加分页处理以应对全量数据”)。 2. **打开** **`tasks.md`**:看看步骤合不合理?有没有漏掉测试? 如果审核通过,请进入最重要的一步,对AI说: ```js 我Review通过,请开始执行这些任务,在执行中,同步更新openspec目录中的关联内容 ``` AI在执行任务的时候,你可以随时打断并修改openspec中的内容,或者让AI操作后自主更新。 ## **归档(Archive)** 当确认任务已完成,并且所有单元测试都运行无误后。用户需要确认`task.md`中的所有勾都被打上。然后在终端中执行: ```js openspec archive TASKNAME # 在这个示例中是 openspec archive remove-refund-filtering ``` 这时终端会提示:  按Y,如果是第一次会报错: ```js payment: target spec does not exist; only ADDED requirements are allowed for new specs. Aborted. No files were changed. ``` 这是因为对应的`openspec/specs/payment/spec.md` 还没创建,建议直接让AI帮助解决: ```js openspec archive remove-refund-filtering --yes 我执行它报错了,请解决 ``` 最终,AI在几次尝试后,会正确得到这样的结构(放心, AGENTS.md中的提示足够支持AI完成初始化操作): ```js ├── openspec │ ├── AGENTS.md │ ├── changes │ │ └── archive │ │ └── 2025-12-19-remove-refund-filtering │ │ ├── proposal.md │ │ ├── specs │ │ └── tasks.md │ ├── project.md │ └── specs │ └── payment │ └── spec.md ``` 原来的任务`remove-refund-filtering`已经被归档,并且specs下创建了最重要的规范文件。后续的需求开发都会读取这个文件,作为AI的“长期记忆”。 不要忘记将specs下的文件添加到项目中并提交。archive目录建议可以保留,作为历史。 下次,如果我们要继续开发,可以对AI说: ```js 我要修改支付模块。请先读取 specs/payment/spec.md,然后创建一个 Proposal 来支持封装业务的部分退款(Partial Refund)。 详情:...... ``` 项目随着迭代,AI会自然沉淀出了一套始终与代码同步的技术文档,AI在Spec的指导下,会越来越聪明。 来自 大脸猪 写于 2025-12-19 16:18 -- 更新于2025-12-19 16:22 -- 0 条评论