Skip to content

OpenSpec 避坑实战:从“弃坑”到避坑,规范驱动开发的正确姿势

前言

在上篇文章中大概认识了 OpenSpec 如何使用和规范驱动开发的流程,这篇文章我们将结合日常开发实践,看一下在接入 OpenSpec 进行规范驱动开发时应该注意哪些问题。

在进入正文前,请确保你已经熟悉 SDD 开发的理念,并且已经掌握了 OpenSpec 的工作流程。

避免需求提案不明确,改动范围过大

在了解到 SDD 和 OpenSpec 后感觉很新鲜, 我立马使用 /openspec-proposal 重构 xxx 页面 生成了一个提案, 结果 OpenSpec 思考了半天生成了一大堆 specs design proposal tasks, 我知道下一步需要评审了, 结果打开 proposal.md 一看 200行+, design.md 200行+, tasks.md 中也有 几十个任务,光是对这几个文件的评审就让我头大,让我一下子失去了使用 OpenSpec 的兴趣。

实际上,OpenSpec 是面向存量代码的规范驱动开发工具。在项目初期,规范文档还不完善时,它对项目的理解有限。此时,一个过于庞大的需求改动会导致:

  1. 没有其他规范文档参考,OpenSpec 无法生成正确的提案。
  2. 提案范围过大,评审成本过高。
  3. SDD 同样是一种严肃开发, 过大的需求无法限定改动范围,导致无法管控改动范围。
  4. 过大的需求, 会生成过大的上下文, 浪费 LLM 资源。

对于重构一个页面的提案, 我们可以按照可以先借助 CursorAsk 模式, 帮我们拆分一下任务, 然后再使用 OpenSpec 生成提案。

例如我有一个 React 列表页面, 上面是一组筛选表单, 下面是根据筛选表单加载的列表组件, 由于之前是使用 antdForm Table 写的, 数据加载状态、分页也都是在页面中写的, 我想使用 @ant-design/pro-componentsProTable 对这个页面进行重构。我把这个需求给到 cursor , 他给我把重构任务拆分了三个阶段, 然后我就可以每个阶段都使用 OpenSpec 生成提案。

cursor 任务拆分

markdown
阶段一:准备工作
任务 1.1:创建 API 函数
任务 1.2:创建类型定义

阶段二:提取配置
任务 2.1:创建 const.tsx 文件
任务 2.2:迁移搜索配置
任务 2.3:迁移表格列配置

阶段三:重构主组件
任务 3.1:替换组件结构
任务 3.2:集成 TablePageV2
任务 3.3:处理删除操作

根据上面的任务拆分 提出三个 OpenSpec 提案

bash
/openspec-proposal xx 页面中的 API 重新定义到 apis/[biz].ts 中, 并生成对应的类型定义, 不要改动当前页面代码
bash
/openspec-proposal 根据  xx 页面的逻辑, 创建 const.tsx 文件, 并生成搜索配置和表格列配置, 不要改动当前页面代码
bash
/openspec-proposal 重构 xx 页面, 使用已经定义好的 API 和类型定义,使用 ProTable 替换原来的 Table 组件, 相关配置引用 const 中的定义

这样带来好处是:

  1. 每个需求提案变得可控,并且在真正重构之前并不会影响现有逻辑。
  2. 每个提案的审查文档会变得更加简介, 降低提案审查的成本。
  3. 在第一个提案归档后,OpenSpec 会按照这个页面重建一个 页面名称/spec.md 文件, 作为后续开发相关功能的指导规范。
  4. 在生成第二个提案时,OpenSpec 会根据第一个提案的规范文档, 生成第二个提案的规范文档, 从而保证第二个提案的正确性。

OpenSpec 擅长的是基于现有代码进行规范驱动的增量开发,所以需要从小到大逐渐构建完善的规范文档,这样 OpenSpec 的使用成本才会逐步变低, 效率逐步提升。

避免过早的应用提案

还是一个带有搜索项的表格页面, 在搜索框中有一个时间范围的搜索组件,目前这个时间范围有三个月的默认值, 现有由于业务调整,我需要把这个默认值去掉。

tsx
 <Form.Item
    initialValue={[dayjs().subtract(3, "month").startOf("day"), dayjs().endOf("day")]}
    label="创建时间"
    name="createAt"
>
    <RangePickerPro />
</Form.Item>

这个需求很小, 不用拆分任务直接提出一个 OpenSpec 需求提案

由于改动点很小,OpenSpec 生成的任务项也比较简单

然后就是直接就应用提案,生成代码了

bash
/openspec-apply remove-batchManagement-createAt-default

但是当应用之后发现去掉了默认时间范围的参数后,接口报错了,列表获取接口对时间范围参数限制为必传, 这是因为我在提案中没有考虑到接口的参数问题,导致提案不正确。

遇到这种情况,虽然我们可以回头修改proposal.md和tasks.md来调整方案,然后重新应用提案。但是如果我们遵循 OpenSpec 规范先行,代码后行 的核心理念。在提案阶段把问题可能出现的边际情况说清楚,能显著减少后期重复评审和修改的成本,尤其是在节省 Token 消耗方面

关注提案的澄清环节

在生成提案的时候,如果对于逻辑可能遇到的需要确认的细节我们没有在提案时就说明出来, 在提案应用后可能会导致提案不正确。

例如我们需要有一个 生成支持用户名,密码登录页面的页面 的需求, 在提案阶段我们最好明确: 关于用户名、密码安全,是否有具体要求?例如,最小长度、需要包含字符类型? 如果我们提案时我们没有说明, 那么 OpenSpec 可能会生成一个会基于其训练数据的提案, 但这很可能不符合你的项目特定上下文。 所以在提案中可以说明用户名最小长度为 6 位, 密码需要包含字母、数字、特殊字符等。

OpenSpec 也会根据提案生成一些需要澄清的问题, 在 CLI AI 工具中会以交互的方式进行询问

保持变更的原子性原则

尽量保持每个变更提案只解决一个明确的问题或实现一个完整的功能点。这有助于保持代码提交的整洁和规范的清晰

完善你的 project.md 文件

openspec/AGENTS.md 中, 提到了在任何阶段都需要检查包含 openspec/project.md 的上下文, 所以 project.md 文件是 OpenSpec 工作流中非常重要的一个文件。

在 OpenSpec 初始化的时候, 我们使用了 OpenSpec 提供的生成 project.md 的 prompt。

Please read openspec/project.md and help me fill it out
with details about my project, tech stack, and conventions

通过此 Prompt 生成的project.md可能不完全符合你的项目实际。你可能有一些全局性的项目规则或特定规范需要加入。因此,务必仔细审阅project.md,并随着项目发展,定期依据实际需求对其进行调整和完善。

评审阶段的三个重要文件

  • proposal.md 是提案的描述, 包括需求背景、需求改动点、影响范围等方面描述整个需求的实施方案。 评审环节可以从这个文件开始, 确保提案在方向上符合需求。
  • design.md 是设计文档,对于稍微复杂的需求,OpenSpec 会生成 design.md 文件, 用来描述整个页面的设计方案。 通过阅读 design.md 文件, 可以了解整个页面的设计方案, 确保设计方案符合需求。
  • tasks.md 是任务列表, 包括任务名称、任务描述、任务状态等方面描述整个任务的执行情况。 通过阅读 tasks.md 文件, 可以了解整个任务的执行情况, 确保任务拆分合理, 任务执行符合需求。

除了这三个文件, spec.md 文件是 OpenSpec 针对提案生成的需求描述和测试场景(用例), 在实际的开发中,我一般会大概的扫一眼,一般不做改动, 因为在实际的评审环节如果改动了上面三个文件, spec.md 文件也会随之更新。如果需要关注测试用例可以关注这个文件。

总结

  1. SDD 和 OpenSpec 都不是 AI 编程的“银弹”,只有结合实际情况, 逐步完善规范文档, 才能提高开发效率和代码质量。
  2. 在 OpenSpec 的工作流中, 优化提案、评审方案、评审任务拆分的合理性应该占据整个工作流的大部分时间, 尽量让 LLM 负责代码的生成。
  3. OpenSpec 的核心是规范先行,代码后行,所以要先说清楚,再动手做会减少重复评审和应用的成本, 尤其是 token 成本。
  4. 保证需求的原子性, 尽量保持每个变更提案只解决一个明确的问题或实现一个完整的功能点。这有助于保持代码提交的整洁和规范的清晰。
  5. 完善你的 project.md 文件, 让它符合你的项目需求。
  6. 规范文档的积累是一个长期过程,需要持续维护和迭代。只有这样,才能逐步提升 AI 编程的效率与准确性,真正实现 规范驱动开发 的目标。

公众号会持续输出,欢迎关注。 如果对你有帮助,欢迎点赞、收藏、关注。