最近我身边的人聊 AI 编程,基本都绕不开一个现象:模型续航越来越长,能力看起来什么都会一点,但产出的代码却越来越难“收住”。你让它补个接口,它顺手把整个模块的命名风格统一了;你让它修一个超时问题,它自作主张把日志框架换掉。OpenSpec 这个名字在工作中被频繁提起,背后其实就一句话:AI 写代码,先立规矩再动手。
这里说的“规矩”,不是团队 Wiki 里那种泛泛的开发规范,而是把需求边界、验收标准、文件操作范围,写成 Agent 能读取、能校验的规矩文件。我把它引入日常项目之后,最直观的感受是 Code Review 的压力小了很多。这篇不打算复读官方文档,而是想把我踩过的坑、跑通的路子,以及它到底解决什么问题讲清楚。如果你也被 AI 的“自由发挥”折腾过,大概率用得上。
1. 模型越强,越要先把“边界”说清楚
1.1 一次把“改提示文案”做成“改业务策略”的复盘
前阵子在维护一个售后系统,客户反馈登录报错信息太生硬,于是向 AI 助手提了个很常见的需求:“把所有错误提示统一成友好文案”。
结果 Agent 真的照做了。它不仅改了前端的 error code 映射,还把后端的密码锁定、风控拦截这类安全相关提示也替换成了“操作失败,请稍后重试”。表面看文案统一了,实际上把用户最需要知道的异常原因给吞了。上线后不到半天,客服那边就陆续收到“为什么账号被锁了也不告诉我”的投诉。
回看整个过程,Agent 并没有偷懒,它甚至执行得很卖力,问题在于我的需求描述太开放。我当时只给了意图,没给边界。模型能力越强,越会顺着自然语言自动补全上下文,而这种“自动补全”一旦落到业务规则上,就是一场豪赌。
OpenSpec 针对的正是这个环节。它不会直接帮你写更多业务代码,也没有改变大模型的底层能力,而是用一个外部约束层,把“你以为说了”和“AI 真的收到”之间的那条裂缝填上。
1.2 模糊描述最终会引发哪几类失控
站在只看结果的角度,AI 写代码失控通常不是某一次的偶发,而是反复出现下面几类情况:
- 范围蔓延。一句“给订单模块加个导出”能被拆成导出中心、异步任务、权限管理和前端页面,最后写出来的东西比预期大三倍。
- 越界修改。为了解决 A 模块的一个缺陷,模型会把 B 模块里它认为“不够好”的地方一起改掉,搅乱整个提交历史。
- 验收缺失。模型跑完自认为“完成了”,但由于没有预先定义什么叫“完成”,人和 AI 的判断标准完全不一致,反复来回扯皮。
- 上下文遗忘。任务越长,后面的改动越容易偏离最初的目标,尤其是 CLI 会话,聊着聊着模型就把需求缩小或放大了。
如果你也遇到过其中任何一条,说明问题不在于“换一个更强的模型”就能解决。更强的模型只会把模糊意图执行得更彻底,错误被放大而不是被修正。
1.3 所谓立规矩,是把“人话需求”翻译成“可执行契约”
过去我们写需求文档,是给产品经理或程序员看;在 OpenSpec 的语境里,这些规矩是给大模型“看”的。所以它不需要写成一篇华丽的 PRD,而是要把自然语言拆成三样东西:
- 本次改动要覆盖的范围;
- 绝对不能碰的边界;
- 改动完成后,拿什么标准来验收。
我后来把“统一错误提示”这个需求重新写成了一份规格,只加了两条硬性约束:安全类提示保持原样;只允许修改 src/error-messages.ts 一个文件。同样的模型,再跑一遍就没有越界。模型没有变,输出的质量完全不一样,差别就在于,模型在拿到任务的同时,也拿到了一套足够明确的执行契约。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. OpenSpec 的核心文件不是摆设:spec、tasks、change 各管一段
2.1 spec.md:把“为什么做、做什么、不做什么”钉死
很多人以为 OpenSpec 会引入一套复杂的文件格式,实际用下来,最核心的载体就是一个 Markdown 文件:spec.md。
在一个特性目录里,spec.md 通常是第一份被创建的文件。它回答三个问题:当前系统哪里不对、这次希望变成什么样、哪些改动明确不在范围内。
我习惯的模板大致长这样:
markdown复制# [FEAT-001] 统一订单导出提示
## 背景
运营每天要从后台导出订单,文件生成失败时只有一句“系统错误”,
运营无法判断是数据量太大、权限不足还是任务没开始。
## 目标
当导出任务失败时,根据失败原因给出可读提示,并保留失败记录。
## 非目标
- 不做异步导出改造
- 不改动下载完成后的文件格式
- 不新增独立的通知中心
## 验收标准
- [ ] 数据量超过 5 万行且未开启分批时,提示“数据量过大,请开启分批导出”
- [ ] 当前登录人缺少导出权限时,提示“没有导出权限”
- [ ] 所有提示不得出现堆栈信息
有这份 spec 垫底,Agent 执行时至少不会跑到“下载中心”或“消息通知”里去。非目标这段尤其重要,AI 默认会把需求理解得比你想的更宽,必须有反向约束。
2.2 tasks.md:把大目标拆成 AI 能一次只推进一格的小关卡
有些 Agent 工具本身可以拆任务,但拆出来的任务质量依赖对话上下文;而 OpenSpec 希望任务拆解结果沉淀成文件,而不是只存在于聊天窗口里。这份文件就是 tasks.md。
它的粒度要控制在“一次改动能被 Code Review”的范围内。不是“实现整个导出中心”,而是:
markdown复制# [FEAT-001] 执行任务
- [ ] task1: 阅读 src/export/error.ts,列出当前所有错误码及文案
- [ ] task2: 根据 spec.md 中的映射表,在 src/export/error.ts 新增错误码文案
- [ ] task3: 补充 src/export/error.test.ts,覆盖“数据量大”和“无权限”两个用例
- [ ] task4: 运行 npm test,确保相关用例通过
task 不要求写成伪代码,但必须能通过阅读直接判断是否完成。要让 AI 每完成一个 task 就停下来,等人确认完再进入下一个,避免它一口气把整个文件列表全改完。
2.3 change 与影响记录:给 Agent 一张“做过什么”的流水账
比 spec 和 tasks 更容易被忽略的,是 change 记录。
命令行类的 Agent 有时会因为上下文太长,把之前已经确认过的东西忘掉。如果每个阶段都有一份类似 change.md 的变更记录,把“这次实际改了什么、为什么这么改、还剩下什么”写清楚,那后续不管重启多少次会话,新会话都能通过这份记录快速回到现场。
markdown复制# FEAT-001 变更记录
## 已确认
- 错误文案映射表按 spec.md 第 2 节执行
- 安全类错误码由后端保留,不在本次范围
## 进行中
- task3 尚未完成,测试文件只写了“无权限”分支
## 风险
- error.ts 中还有 3 个历史错误码没有匹配到新文案
这份文件既是给 Agent 的接力棒,也是给人类 Review 的现场证据。很多项目跑着跑着规格就崩了,原因就是没人维护这层“最近发生了什么”的记录,Agent 每次都像第一次进项目一样迷茫。
3. 最小可用安装:不改项目结构也能让规矩生效
3.1 先区分一下“工具封装”和“文件约定”
OpenSpec 的社区实现很多,有人把它做成 CLI,有人把它做成了 IDE 插件,还有人只当成一套目录约定用。我不想在这里把某种封装定为一尊,因为工具迭代速度很快,今天能用的命令,下个月可能就废弃了。真正稳定的,是那套“先有 spec,再拆 tasks,最后迁移代码”的文件约定。
如果你拿到的开源版本提供了类似 openspec init 的管理命令,那么直接在项目根目录初始化即可,它会帮你创建目录骨架。如果暂时不想引入新依赖,自己手工建目录也完全可以。下面是我目前用得最顺的最小结构:
text复制openspec/
├── rules/
│ └── agent.md # 给 Agent 的第一条入口规则
└── specs/
└── rain-noise/ # 一个特性一个目录
├── spec.md
├── tasks.md
└── change.md
首次建立时,直接在终端执行:
bash复制mkdir -p openspec/rules openspec/specs
touch openspec/rules/agent.md
这就完成了最基础的“安装”。不需要迁移老代码,也不需要改 CI。
3.2 rules/agent.md:入口文件是 Agent 的第一份规则
目录建好之后,真正重要的工作是写 rules/agent.md。这个文件相当于给 Agent 的“宪法”,每次开始工作前它应该先读这个文件,再读某个特性目录下的 spec 和 tasks。
我初始版本的 agent.md 大概只写了四条硬规矩:
markdown复制# Agent 工作规则
1. 先读 openspec/rules/agent.md,再读需求对应的 spec.md。
2. 禁止修改 tasks.md 中没有列出的文件;
如果确实需要修改,必须先停下来告诉用户原因。
3. 实现代码前先阅读 spec.md 的“验收标准”,
任何实现必须能解释自己满足了哪一条。
4. 每次只完成一个 task,完成后停下等待确认。
这几条看起来朴素,但能挡住大量“AI 自告奋勇型”的改动。特别是第二条,很多模型只要没被明确禁止,就会觉得“相关文件都可以优化一下”,有了这条规则,它至少会先停下来请示。
3.3 不要急着一步到位,先跑通一个特性闭环
我见过不少人搭建规则目录时,一上来就想把整个项目的所有模块全部拆成 spec,结果拆了两天就放弃。OpenSpec 的落地策略应该是“先拿一个小功能做闭环”:选一个改动范围可控、验收标准比较明确的 feature,从 spec 开始走一遍,确认人机配合顺畅,再慢慢扩展。
第一次跑闭环时,可以把流程强制设成四步:写 spec,让 AI 只读不写;再让 AI 基于 spec 生成 tasks,人确认;执行 tasks 里的单个任务;最后对照验收标准跑验证。这套顺序可能比想象中慢,但它会把“AI 替我乱写”变成“AI 执行我已经审查过的计划”。
4. 接入 Codex、Claude Code、Cursor 等工具时的实际姿势
4.1 不要把规则放在聊天窗口里,让 Agent 从项目文件中读取
很多人有一个直觉:把规范写在对话框里,跟 Agent 说“接下来按这个规范做”。问题在于,聊天上下文是一次性的。换一个新会话,AI 可能完全不知道项目里存在 openspec 目录,更不会自觉去读。
正确做法是把 OpenSpec 的入口暴露在各 Agent 默认会读取的文件里。现在比较通用的做法是,在项目根目录维护 AGENTS.md 或对应工具入口文件,然后在里面加一句话:
markdown复制- 开始任何代码修改前,先读取 openspec/rules/agent.md。
- 如果存在 openspec/specs/<当前需求>/spec.md,
严格按 spec 验收标准执行。
有些 Agent 会在生成代码前自动读取所有项目文档。如果入口文件本身内容过长,反而会稀释核心规则,所以入口只要能“指路”即可,真正的详细约束还是放在 openspec 目录里。
4.2 在 Codex CLI / Claude Code / opencode-ai 等终端 Agent 中
终端型 Agent 用起来更像“远程实习生”,你说一句,它开始干活。没有图形界面辅助时,规则文件的价值反而最大。
给这类工具的最初指令我一般会写得非常机械:
text复制请先阅读项目根目录的 AGENTS.md 和 openspec/rules/agent.md。
本次需求目录是 openspec/specs/rain-noise/,先读 spec.md,
再按 tasks.md 中的编号顺序完成任务。
当前只需要执行 task1,完成执行后输出执行结果,不要继续做 task2。
注意,用命令行 Agent 时上下文丢失发生在多轮对话之后,所以每轮要让 Agent 重新读文件,而不是相信它记忆里的内容。设计规矩时也要考虑这一点:核心规则简单、少、可重复读;细节规则放在文件里,不要靠对话复述。
4.3 Superpower 这类能力包和 OpenSpec 怎么分工
很多人会问,那 Superpower 这类工具和 OpenSpec 是不是重叠?
以我现在实践的理解,它们是两层不同分工。Superpower 一类的能力包/提示词增强工具,往往负责的是通用编码方法论,比如告诉模型要“先计划再动手”“用 TDD 方式推进”“不要一次写太多文件”。这些规则不依赖于具体项目,任何仓库都可以用。
OpenSpec 则更贴近具体仓库和具体业务,它关心的是“这个订单导出功能有哪些验收点”“哪些文件属于这次范围”。前者管通用思维习惯,后者管项目需求边界。所以实际使用中更合理的组合是:通过 Superpower 或类似能力包让 Agent 具备良好的工程习惯,通过 OpenSpec 告诉它当前仓库的具体规矩。如果反过来了,用 Superpower 去猜业务需求,用 OpenSpec 去教模型怎么写测试,那两边都会很别扭。
落到操作上,我会把通用工程习惯写进 Agent 的全局配置,把 OpenSpec 的路径和当前特性目录写进项目入口,两者互不覆盖。
5. 实战演练:一句“写个雨声生成器”是怎么被 OpenSpec 收敛的
5.1 最原始的提示词为什么容易翻车
前阵子有人问我,想做一个雨声生成器,给模型一句话:“写一个 Python 雨声生成器”,但几次生成结果都不满意。有的方案引入了一堆库,有的干脆做成 Web 服务,他其实只想要一个能在本地生成 .wav 文件的小工具。
这个例子里,“雨声生成器”五个字的信息量极低。模型不知道你接受什么格式、要不要 GUI、音频时长多少、要不要循环、允许哪些第三方依赖。于是我给这个需求补了一份 spec,整个过程很能说明 OpenSpec 的实战价值。
5.2 为雨声工具立下的规格文件
先建立目录并创建 spec.md:
text复制openspec/specs/rain-noise/
└── spec.md
文件中除了背景和目标,还明确写了非目标和验收标准:
markdown复制# 命令行雨声生成器
## 背景
需要一个本地可运行的 Python 工具,
生成一段可循环播放的雨声背景音频,用于专注计时场景。
## 目标
- 通过命令行参数指定音频时长和雨声强度
- 输出为 wav 文件,供播放器循环播放
## 非目标
- 不做图形界面
- 不做 Web 服务
- 不依赖在线音频接口
- 不生成超过 100MB 的音频文件
## 验收标准
- 命令 `python rain.py --duration 10 --intensity gentle` 可运行
- 生成的 wav 文件时长约 10 秒
- 同一参数重复运行时,两次生成的文件不应完全相同
- 使用 numpy 与 scipy 实现波形合成,不引入其他重量级框架
如果你直接把上面这段交给一个 Agent,它至少不会给出一个 Flask 应用。非目标里的“不做 Web 服务”就是约束模型“自由发挥”的关键一句。
5.3 tasks 和验收如何驱动 Agent 输出
spec 写好后,再让 AI 基于 spec 生成 tasks,这一步会用到 OpenSpec 的典型工作流。我拿到的一份 tasks 大致是:
markdown复制# 执行任务
- [ ] task1: 先确认环境已有 numpy/scipy,并阅读 spec.md 非目标
- [ ] task2: 在项目根目录创建 rain.py,
实现“生成白噪声 + 低通滤波”的雨声底噪
- [ ] task3: 在 rain.py 中加入随机雨滴脉冲,
强度参数控制脉冲密度
- [ ] task4: 运行示例命令,确认生成 wav 文件且时长正确
在整个执行过程中,Agent 曾经一度想“顺便”加一个基于 matplotlib 的波形预览图。这个想法本身没问题,但它不在 spec 的目标里,也不在 task 清单里,所以按规则它必须先停下询问。这个过程不是限制创意,而是把关键决策权交回给用户,按需变更。
5.4 验收时的人工检查点
最后生成的文件结构大致是:
text复制rain.py
output/
└── gentle_10s.wav
验证时不要只看 AI 自己说“完成”,而是复跑验收命令,并打开 wav 文件人工听一遍。很多模型生成的雨声听起来更像持续白噪声,缺少雨滴的层次感,这是只靠单元测试无法发现的。此时需要回到 spec,在验收标准里补一条更具体的听感描述,再让 Agent 迭代。OpenSpec 的价值就在这种循环中体现:需求变了,先改 spec,再改代码,而不是直接让模型“再调调”。
6. 踩坑复盘:从“AI 看都不看规矩”到稳定运行
6.1 问题一:spec 写得很完整,Agent 就是当没看见
我刚开始用这套工作流时,把 spec 写得非常详细,细到每个函数名都列了出来。结果 Agent 仍然按照自己的思路写出了完全不同的结构。排查之后发现,问题出在 Agent 的“输入链路”上。
我虽然把 spec 文件写进了项目,但启动会话时只对 Agent 说“按项目规范改代码”,没有明确告诉它去读哪个文件。很多模型默认不会主动遍历目录找 openspec/specs,它更习惯直接看当前打开的代码文件或 README。
后来我把入口规则改成了强指令:
text复制先读 openspec/rules/agent.md,再读 openspec/specs/rain-noise/spec.md。
未读取前不要开始编码。
命令型 AI 对“先读”“然后”这类顺序词执行率远高于单纯说“遵守规范”。所以规则文件本身写得好不好是一回事,能不能被模型真正读进上下文是另一回事。
6.2 问题二:Agent 改了不在任务范围内的代码,排查链很长
还有一次,Agent 在执行过程中发现某个旧测试和新实现冲突,于是它直接修改了测试文件的 mock 数据。表面看似合理,但改完之后测试通过,掩盖的其实是业务逻辑回归。
定位这个问题的链路是这样的:先看 diff,确认改动文件是否在 tasks.md 允许范围内;再看任务描述,发现它根本没有被安排修改测试;最后回看规则,rules/agent.md 中只写了“禁止修改没有列出的文件”,但 Agent 把“修改测试”理解为“完成任务的必要步骤”。
解决办法是在规则里加了更细致的限定:
markdown复制- 如果现有代码或测试与 spec 冲突,禁止擅自修改既有测试;
应暂停并报告冲突内容,由用户决定是否调整
