OpenSpec 实战:用 Spec Coding 从零开发语音购物助手
灿若繁星先生 Lv6

最近博客停更两周,没摸鱼,是把所有时间都砸在了一个新项目 voice-shopping 上 —— 一个基于多智能体的语音购物助手。但比项目本身更值得说的是:整个项目我没写一行"自由发挥"的代码,全程用 OpenSpec 的 Spec Coding 流程驱动 Claude Code 来做。这篇就不展开聊技术栈了,重点说说 OpenSpec 这套玩法在真实项目里到底怎么跑、踩了哪些坑、什么场景下值。

项目基本情况

voice-shopping 是个语音购物助手,用户通过语音和 AI 助手自然交互,系统自动完成意图理解、需求澄清、商品推荐、情感应答。技术栈一句话:

  • Java 21 + Spring Boot 4.0.5 多模块工程(common / infrastructure / ai / business / web 五层)
  • AgentScope Java 1.0.11 编排 4 个 Worker Agent + 1 个手写 Orchestrator 状态机
  • qwen-max / qwen-turbo / qwen-plus 三档模型分场景使用,text-embedding-v3 1024 维向量
  • PostgreSQL + pgvector 关系数据 + HNSW 向量检索 + JSONB 属性过滤
  • Redis 7.x 三层记忆架构(短期记忆 List、会话状态 Hash、scope 缓存)
  • 阿里云 NLS 流式 ASR(Paraformer 实时版)+ TTS(CosyVoice)
  • Sa-Token 1.45.0 多商家认证 + 行级数据隔离
  • WebSocket 全双工,二进制帧传 PCM、文本帧传控制 JSON

亮点是 多视角点评团(Perspective Team) 机制 —— 同一批候选商品由"价格敏感型 / 专业极客型 / 新手小白型"三个并行 Agent 各自点评,通过事件总线合流,给到 EmotionAgent 包装成最终话术,比单一推荐链路更接近真实导购场景。

整体链路大致长这样:

flowchart LR
  U([用户语音]) -->|PCM| ASR[ASR
Paraformer] ASR -->|文本| ORCH{Orchestrator
状态机} ORCH --> CL[ClarifyAgent] ORCH --> REC[RecAgent] ORCH --> EM[EmotionAgent] REC -.候选.-> P1[价格敏感] REC -.候选.-> P2[专业极客] REC -.候选.-> P3[新手小白] P1 & P2 & P3 -->|事件总线| EM EM -->|文本| TTS[TTS
CosyVoice] TTS -->|流式音频| U

意图分支路由:

意图 链路 备注
PRODUCT_RECOMMENDATION Clarify → Rec → (Perspective?) → Emotion 主链路
PRODUCT_COMPARE Rec(基于上轮重排) → Emotion last_recommendations 改写过滤条件
CLARIFY_NEEDED Clarify 循环直到 READY
ORDER_CONFIRM OrderService 走原子扣库存 AFTER_COMMIT 行为回流
CHITCHAT ChitchatReplyPool(不调 LLM) 模板池随机抽
OUT_OF_SCOPE EmotionAgent 礼貌拒绝

但说实话,项目本身的技术选型不是这篇的重点。重点是:这种规模的项目如果让我自己手撸提示词驱动 Claude Code 一把梭,大概率写到一半就跑偏。OpenSpec 是我目前找到的最稳的协作范式。

为什么选择 OpenSpec / Spec Coding

写过几个 AI 协作项目就会发现一个问题:你和 Claude Code 之间的"理解差"是项目失败的最大变量

  • 你说"加个用户画像",模型给你来个 7 张表的复杂建模
  • 你说"做个简单的推荐",模型给你接了 3 个 RPC + Redis 多级缓存
  • 你说"先把这个小功能跑起来",模型顺手把 5 个相关文件全重构了

每次返工不仅心累,还会让你怀疑 AI 协作到底有没有意义。Spec Coding 的本质是把"你脑子里那个版本"先落到文档里,让模型在动手之前先和你对齐。

OpenSpec 是一套围绕这个理念的工具,核心是三份文档 + 一份主 spec:

  • proposal.md:本次变更的 Why / What / Impact —— 给"决策者"看的
  • design.md:技术设计、关键决策、备选方案、权衡 —— 给"架构师"看的
  • tasks.md:拆解到可勾选粒度的任务清单 —— 给"执行者"看的
  • openspec/specs/<capability>/:各个 capability 的当前形态 —— 项目的"知识快照"

文档对齐之后再开始写代码,跑偏概率断崖式下降。

特别强调一下 主 spec 这个概念 —— 它是项目能力的最新形态,每次 archive 时由 OpenSpec 自动把 change 里的增量合并进去。新一轮 explore/propose 时,模型会先读主 spec 了解项目当前长什么样,所以它不会"忘记"上一版做过什么。这一点比你给一个新 ChatGPT 会话从头讲项目要靠谱得多。

OpenSpec 实战流程

我现在每个版本都按下面这个固定流程跑,对应 4 个 slash command:

flowchart LR
  A["/opsx:explore
聊方案"] --> B["/opsx:propose
生成提案"] B --> C{"人工
逐字读完
三份文档"} C -- 不满意 --> A C -- 通过 --> D["/opsx:apply
按 tasks 执行"] D --> E{"人工验证
功能正常?"} E -- 否 --> F["让 Claude 调试
调不出自己 debug"] F --> E E -- 是 --> G[git commit] G --> H["/opsx:archive
归档版本"]

Step 1:/opsx:explore 先聊清楚

这一步不写任何文档,纯讨论。我会把这一版想做什么、为什么做、有什么约束抛给模型,让它反过来问我细节。

比如做"成本控制 + 指标埋点"那一版,我开头只说了"想砍 LLM 成本,加点埋点",然后 explore 阶段我们来回聊了:

  • 砍哪些?默认开启的 Perspective 团队要不要关掉?(关 → 单轮成本下降 ~46%)
  • CHITCHAT 闲聊兜底分支白调一次 EmotionAgent 才被替换成模板,浪费要不要止损?(彻底跳 LLM)
  • Clarify 单字段缺失场景能不能直接用模板问句?(4 类常用 slot 走模板,多字段才走 LLM)
  • 埋点用 logfmt 还是 JSON?写到主日志还是独立文件?(logfmt + 独立 cost-metrics.log 文件)
  • 埋点放哪一层?要不要改 Service 签名?(不要 —— 用 MDC 拿 sessionId)
  • 灰度策略本期做不做?(不做 —— 写进 proposal 的"非目标")

explore 阶段是反向 PUA 模型的最佳时机:让它把你没想到的边界、风险、设计权衡全暴露出来。我经常聊到一半发现"原来这个改动会牵动那个模块",赶紧调整范围。

我自己的小 trick:让模型每聊几轮就主动列一遍"目前我理解的本期范围 / 非目标 / 待澄清",等它把这三栏列得跟我心里想的一样了,就可以收尾去 propose 了。这样 propose 出来的 proposal.md 就不会突然冒出某个我们没聊过的"额外功能"。

Step 2:/opsx:propose 生成提案

聊清楚后跑 /opsx:propose,模型会在 openspec/changes/<change-name>/ 目录下生成一整套文档:

1
2
3
4
5
6
7
8
9
openspec/changes/cost-control-and-metrics-logging/
├── proposal.md # Why / What Changes / Capabilities / Impact
├── design.md # 关键决策、权衡、备选方案
├── tasks.md # 任务清单(可勾选)
└── specs/ # 涉及到的 capability spec 增量
├── cost-metrics-logging/spec.md (新增 capability)
├── chitchat-reply-pool/spec.md (新增)
├── orchestrator-service/spec.md (修改)
└── ...

specs/ 是最容易被忽略但最关键的一部分 —— 它是本次变更对主 spec 的"补丁"。OpenSpec 把每个 capability 当成可独立增删改的单元,archive 时这些 spec 增量合并进 openspec/specs/,下次 propose 时模型读到的就是最新形态。

这里有个绝对不能省的步骤:人工逐字读完三份文档。

我自己的经验是,模型生成的提案 80% 时候是对的,但那剩下的 20% —— 比如"顺手"把不该改的接口签名改了、把不该删的 prompt 删了、把"保留作回滚资产"理解成"反正用不上删了"—— 一旦放过去,后面 apply 阶段就是灾难。

具体阅读三份文档的时候我有自己的优先级:

  1. proposal.md 的 What Changes 一条条对照心里那份清单:少了什么?多了什么?
  2. proposal.md 的 Impact 重点看"代码删除"段落:模型敢删的东西,往往是你没想到要删的
  3. design.md 看关键决策对不对:选了 A 方案没选 B 方案的理由你认不认
  4. tasks.md 数行数:超过 20 行立刻触发"是不是该拆"的警觉
  5. specs/ 看 capability 名字:新增了哪些、改了哪些、有没有 REMOVED 的

如果发现哪条不符合预期,就直接把对应段落贴出来让模型改,或者再起一轮 explore 重新对齐。确认下一版不做的功能,一定要明确告知模型移出本次范围,否则它会"贴心地"顺手帮你做。

Step 3:/opsx:apply 开始执行

文档锁定后跑 /opsx:apply,模型会按 tasks.md 顺序执行任务,每完成一项就把 checkbox 从 [ ] 改成 [x]

这一步我做两件事:

  1. 盯着 tasks.md 的进度,每完成几项瞄一眼实现细节,不对就立刻打断纠偏,别等全跑完才回头看
  2. 跑完后人工验证功能,发现不对让 Claude Code 调试直到正常,调不出来自己上手 debug

第二点很关键:有些功能在模型看来是"正常"的,实现却不符合你的预期

举两个真实翻车的例子:

翻车 1:ASR 埋点签名污染

成本控制那版我让它给 ASRService 加埋点,proposal 里只写了"在 onComplete/stop 时记录 audioMs"。它直接改了 ASRService.startStream 的方法签名往里塞 sessionId/userId 参数,跑起来确实能埋点,但污染了几乎所有上层调用 —— OrchestratorService、WebSocketHandler、控制器全得改。我自己 debug 才发现完全可以用 SLF4J 的 MDC 拿 sessionId(WebSocket onOpen 时 MDC.put("sessionId", id)),ASR 那边 MDC.get("sessionId") 直接取,签名一行不用改。

后来我把这条经验抽成 design.md 里的一个固定决策:“业务字段透传一律用 MDC,禁止侵入 Service 签名”,下个 change 模型就再也没犯过这个错。

翻车 2:删过头

cost-control 这版 design.md 里写了"删除已废弃的 RecommendReasonService",我没仔细看放过去了,结果 apply 阶段它顺手把 prompts/rec.txt 也删了 —— 虽然确实废弃,但保留作回滚资产更稳。还好提前在 proposal.md 里写了"保留 prompts/emotion.txt 不删"的明确边界,否则可能误删的更多。

翻车 3:tests 写得太深

Orchestrator 那一版让它顺手补单测,结果它写了一堆 mock 满天飞的"白盒测试",把 SessionService 内部调用顺序都校验了。后来一改实现细节单测就挂一片。我现在 proposal 里会明确写"单测只校验对外契约 + 关键 fail-fast 路径",把测试粒度先框死。

经验总结一句话:调不动的时候自己 debug,往往能发现模型理解偏差。让模型重试 2 次还不对,立刻自己上 —— 5 分钟 debug 出来的根因,比模型自己再调 5 轮都准。

Step 4:/opsx:archive 归档版本

功能验证 OK + tasks.md 全勾选 + 一个或多个独立 git commit 提交完成后,跑 /opsx:archive 把当前 change 移到 openspec/changes/archive/<日期>-<change-name>/,同时把 specs 增量合并到主 spec 里。

archive 这一步看起来只是文件搬家,实际上它在做三件重要的事:

  1. change 不再被 propose 干扰:归档后下次 explore/propose 时模型只能看到 archive 里的快照(用于回顾),不会把它当"待办"
  2. 主 spec 更新openspec/specs/<capability>/spec.md 是项目能力的最新形态,archive 时增量被 merge 进去,下个 change explore 时模型读到的就是最新版
  3. change 历史可追溯:archive 目录里保留完整的 proposal/design/tasks,code review 时翻回来对照 git diff 一目了然

我现在 archive 之前还会顺便扫一眼 openspec/specs/ 下有没有 capability 是 REMOVED 的,确认一下知识库的"瘦身"。

两个真实案例:从 explore 到归档

光说流程太抽象,挑两个 change 完整讲一下。

案例 1:add-perspective-team —— 旁路并行的设计

这是 6/13 那批 4 个 Agent change 里最有意思的一个。需求很简单:在主推荐链路给出 Top-K 商品后,启动"价格顾问 / 专业跑者 / 入门买家"三个 Agent 各自点评一句,给用户更立体的导购感。

explore 阶段聊出来的关键决策

问题 选择 理由
串在主链路里还是旁路 旁路 3 次 LLM 调用 1.2-1.8s,串进主链路会割裂语音体验
三个 Agent 并行还是顺序 MsgHub 内顺序广播 AgentScope 自带 MsgHub 能让后发言的 Agent 看到前者的发言,更像真讨论
Profile 加载和召回两条腿要不要并行 互不依赖,串行白白等 ~200ms,用 CompletableFuture.thenCombine 合流
失败如何降级 空字符串 不阻塞主链路,主推荐照常
用户开口这种横切信号怎么处理 Spring ApplicationEvent 预热缓存、内容审计跟主链路解耦,@Async @EventListener

propose 出来的 proposal 节选(What Changes 部分):

1
2
3
4
5
6
7
8
9
10
- 新增 `perspective-team` 能力:基于 io.agentscope.core.pipeline.MsgHub
- 新增 MultiAgentModelConfig:注册 multiAgentChatModel Bean (qwen-plus)
- 新增 PerspectiveHubService.discuss(sessionId, utterance, items) → String
- 三角色 prompt 文件:prompts/perspective/{price,pro,beginner}.txt
- 新增 `voice-event-bus` 能力:解耦"用户开口"横切信号
- 新增 UserSpokenEvent record + VoiceEventPublisher + VoiceEventListeners
- 修改 `rec-orchestration` 能力:新增 ParallelRecommendService
- 用 CompletableFuture.supplyAsync 并行 profile.load 与召回链
- thenCombine 合流后接 rerank → top K → reasons
- RecommendOrchestrator 类不改,维持二者并存(A/B 切换)

apply 时的小翻车:模型一开始把 MsgHub 的 try-with-resources 写漏了,导致连续跑两次第二次会卡住 —— 我跑功能验证发现的,让它修。这种细节 propose 阶段是看不出来的,只能靠跑出来才知道。

最终 commit:555c9ed feat(perspective): 实现多视角点评团 + 推荐并行合流 + 事件总线,一个 change 一个 commit,干净。

案例 2:merchant-data-isolation —— BREAKING 改造怎么落

这是 6/15 的一个破坏性变更:把 WebSocket 鉴权从 query ?userId= 改成 Sa-Token 的 Sec-WebSocket-Protocol: Bearer-{token} 头校验,整条链路同步加 merchant_id 行级过滤。

为什么单独拎出来讲:BREAKING + 跨多个模块(auth、session、scope、rec、ws)+ 需要 Flyway 迁移 + 涉及 RFC 6455 协议细节 —— 这种 change 最容易写崩。

explore 阶段重点确认的事情

  1. 是否兼容旧 query userId?→ 不兼容,前端配合改一次到位(不留兼容代码 = 不留技术债)
  2. WebSocket 子协议响应头要回写吗?→ 必须(RFC 6455 要求,不回写 Chrome 会报错)
  3. 商品检索全平台兜底要不要做?→ ,Redis scope 缓存 miss 时降级为"仅该用户、全平台",记 WARN 日志
  4. session.channel 加新枚举值需要 Flyway 迁移吗?→ 需要,V6 扩 CHECK 约束
  5. 单元测试覆盖到哪一层?→ scope filter 合流的关键路径 + 握手鉴权 401 路径

proposal 里写的 Impact 段(节选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
代码:
- 新增模块:voice-shopping-web/auth/*、voice-shopping-web/session/*
voice-shopping-business/scope/*
voice-shopping-infrastructure/repository/AppUser*
- 修改:VoiceHandshakeInterceptor 重写
WebSocketConfig 注入 Sa-Token
VoiceWebSocketHandler 不再依赖 query userId
RecommendCandidatesService / RecommendOrchestrator
注入 SessionScopeCache + ScopeFilterBuilder
SearchController 带 sessionId 时叠加 scope

数据:
- Flyway V6 扩 session.channel CHECK 约束(加 MERCHANT_HOME)
- Redis 引入 vs:scope:{sessionId} 命名空间

回滚方案:
- 代码 revert + V7 反向迁移收回 CHECK 约束
- Redis scope key 不需清理(TTL 自动过期)

这个 proposal 给我的感觉就是"清单已经齐了,剩下的纯体力活"。apply 阶段几乎一口气跑完,中间只调了一个 bug:模型一开始忘了在握手失败时回写 sub-protocol 响应头,浏览器握手卡死,我让它对照 RFC 6455 修了一下。

关键是这种 BREAKING 改造,proposal 里"回滚方案"段必须明写。万一上线发现握手失败率飙了,知道怎么撤回是命根子。

最终 commit:a10b5d0 feat(merchant-isolation): 实现多商家数据隔离与权限模型

真实开发流程:从 git log 看 OpenSpec 节奏

voice-shopping 项目目前一共归档了 15 个 change,对应 git log 上每条独立 feature commit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2026-06-11  maven-project-scaffold              初始化 Maven 多模块骨架
2026-06-12 agent-factory-and-prompt-loader Agent 工厂 + Prompt 加载器
2026-06-12 user-profile-and-session-memory 用户画像 + 会话短期记忆
2026-06-12 vector-search-impl 向量检索 + FAQ 全链路
2026-06-12 voice-channel ASR/TTS 流式 + WebSocket
2026-06-13 add-emotion-agent EmotionAgent 情感应答
2026-06-13 add-perspective-team 多视角点评团 + 事件总线
2026-06-13 clarify-agent ClarifyAgent 规则+LLM 混合
2026-06-13 rec-agent-product-recommendation RecAgent 商品推荐管线
2026-06-14 add-orchestrator-service Orchestrator 编排层接入
2026-06-14 optimize-session-and-long-term-memory 会话短期+跨会话长期记忆
2026-06-15 merchant-data-isolation 多商家数据隔离 + 权限
2026-06-15 order-placement-flow 下单确认闭环 + 原子扣库存
2026-06-15 order-subject-scoped-query 订单查询主体强制过滤
2026-06-16 latency-optimization-h5-streaming 延迟优化 + H5 流式改造

每一行都对应一个完整的 OpenSpec 闭环:explore → propose → 看文档 → apply → 验证 → archive → commit。

按主题分类一下,看得更清楚:

timeline
  title voice-shopping 15 个 change 时间线
  06-11 : 项目骨架 (1)
  06-12 : 基础能力 (4)
agent工厂 / 画像记忆 / 向量检索 / 语音通道 06-13 : Agent实现 (4)
Emotion / Perspective / Clarify / Rec 06-14 : 编排与记忆 (2)
Orchestrator / 长期记忆 06-15 : 多商家与订单 (3)
数据隔离 / 下单 / 订单主体过滤 06-16 : 性能优化 (1)
延迟与流式

可以看到每个 change 都很小:

  • 6/12 一天连做了 4 个 change:agent 工厂、画像记忆、向量检索、语音通道,每个都是独立可交付的小步。这一天平均每个 change 从 explore 到 archive 大约 1.5-2 小时
  • 6/13 拆成 4 个 Agent 各自一个 change:没把"做完所有 Agent"打包成一个大版本。事后看这是最聪明的决策 —— 4 个 Agent 各有 4 套 prompt,混着提交根本对不上 review
  • 6/14 把 orchestrator 和长期记忆拆开:虽然记忆系统会被 orchestrator 调用,但二者职责完全不同。先做 orchestrator 骨架(用 stub memory),再把记忆系统单独迭代
  • 6/15 把"商家隔离"和"订单主体过滤"拆成两个 change:都是数据隔离主题,但前者建权限模型,后者收口订单查询,关注点不同。订单主体过滤还引入了一条很重要的工程规范:“Repository 派生查询必须显式带主体维度”,单独一个 change 做这事比夹在订单流程里好

如果哪个版本跑偏了,git revert 一个 commit 就完事,不用心疼,因为它本来就只代表一个独立的 change

心得体会

跑了 15 个 change 下来,几条最重要的经验:

1. 尽可能拆小,小步快跑

最大的坑是想"一次性吃个大胖子"。

我刚开始做 Orchestrator 那一版差点犯错,本来想一个 change 把"状态机 + 4 个 Agent 接入 + WebSocket 改造"全做了,explore 阶段聊到一半发现 tasks.md 列了 30 多项,立刻拆。最后拆成"orchestrator 状态机骨架"和"WebSocket 全双工接入"两个独立 change,实施起来心态稳很多。

判断标准很简单

  • tasks.md 超过 20 项 → 考虑拆
  • 涉及 3 个以上模块的代码改动 → 考虑拆
  • 既有 BREAKING 又有非 breaking 的特性 → 必须拆
  • 既改 spec 又改实现 → 看能不能把 spec 改单独提一个 change

调试阶段一旦出问题,change 越大越难定位。

2. 每个 change 一个独立 git commit(或几个)

每完成一个 change 就独立 commit,不要混着提交。原因有三:

  • 跑偏了 git revert 直接回滚,干净利落
  • 不能保证你认真看过 OpenSpec 生成的所有文档,万一漏了,回滚成本极低
  • code review 时一个 commit 对应一份 proposal,前后呼应,很容易看清意图

我的 commit message 也按一个固定模板:feat(<scope>): 一句话能力描述,scope 用 change 名字简化版(perspective / orchestrator / merchant-isolation),跟 archive 目录名对得上,一眼能反查到对应的 proposal。

3. 三份文档必须认真看

proposal.md / design.md / tasks.md 不是给模型看的,是给你看的

最容易踩的坑是模型"顺带"做的修改。除了前面说的"删过头",还有几种我吃过亏的:

  • 顺手统一命名:模型觉得 RecommendOrchestratorParallelRecommendService 命名不一致,悄悄把前者改名 —— 但前者已经在 README 里写过了,引用全断
  • 顺手补防御性代码:你说"加 ASR 埋点",它顺手把 ASRService 里所有 null 检查都补了一遍,PR diff 大三倍
  • 顺手给 record 加方法:明明只是数据载体,被加了 validate() toMap() 一堆"看起来有用"的方法

Tip:用 diff 工具把两次 propose 的文档比一比,能快速看出本轮新增/变化的边界。如果第二次 propose 比第一次多出来一段你没要求的内容,立刻揪它出来。

4. 明确告知不做的事情

如果 explore 阶段聊到的某个能力本版本不做,一定要明确写进 proposal.md 的"不影响项"或"非目标"段落

模型默认会把所有讨论过的内容尽量塞进当前版本,写一句"灰度策略本期不做"比反复让它从 tasks.md 里删要省事得多。

我自己甚至会主动让模型在 proposal 末尾加一段"非目标 / Out of Scope"列表,把所有讨论过但本期不做的事情明文写出来。这样下次 explore 想做这事的时候,搜一下就能知道"上次为什么没做、当时怎么权衡的"。

5. 仔细看模型推荐的方案,调不对就自己 debug

有些场景调一两轮不通,大概率不是模型笨,是它理解和你不一样

前面说的 ASR 埋点签名污染就是典型例子:模型觉得"传 sessionId 进来很合理",我觉得"用 MDC 拿就行不要改签名",光让它"再调一遍"调不出来,自己看 5 分钟代码就知道关键分歧在哪。

判断准则:让模型重试 2 次还不对,立刻自己 debug。不要陷入"我再让它试一次说不定就好了"的赌徒心态。

debug 出根因后还有一个动作:把这次的分歧写成一条决策放进下次的 design.md。比如那次之后我在仓库 CLAUDE.md 加了一条:“业务字段透传一律用 MDC,禁止侵入 Service 签名”,后续 change 模型就再没犯过同样的错 —— spec 在帮你"教育"模型。

6. 主 spec 是地图,change 是脚印

跑了几次之后我才意识到:openspec/specs/ 下的主 spec 才是这套体系真正的资产,change 只是过程产物。

主 spec 记录的是"项目当前长什么样":哪些 capability 存在、各自的契约是什么、哪些已经 REMOVED。新人接手项目,先读主 spec 比读代码快 10 倍 —— 因为它是用人话写的"接口承诺"。

每次 explore 阶段,模型读的也是主 spec。如果你发现模型频繁"忘记"上一版做过的设计,大概率是主 spec 里那段写得不够清楚,应该回头补。

7. CLAUDE.md 是"会话级 spec"

OpenSpec 管的是"项目能力 spec",但有些规范是整个项目通用的(命名约定、错误处理风格、测试策略),这些放进每个 change 的 design.md 都太啰嗦。我的做法是把它们沉淀到仓库根的 CLAUDE.md 里 —— 每次 Claude Code 启动会自动读取,相当于"会话级 spec"。

举几条我现在 CLAUDE.md 里的硬规矩:

  • 接口请求/响应禁止用 Map<String, Object>,必须定义专用 record
  • 所有接口返回 ApiResult<T>,不用 ResponseEntity
  • Repository 涉及主体(user/merchant)的查询必须显式带主体维度,如 findByIdAndUserId
  • 业务字段透传走 MDC,禁止侵入 Service 签名
  • Flyway 已执行的迁移脚本禁止修改

这些规矩进了 CLAUDE.md 之后,几乎每个 change 都能省掉一次"模型又写错了被我改回去"的循环。

适用场景

OpenSpec 不是银弹,我个人感觉它最值的场景是:

  • 从 0 到 1 的项目:每一步都需要设计对齐,文档驱动收益最大
  • 复杂业务功能:涉及多模块、多 Agent、多边界条件
  • 重构老代码:proposal 阶段就能把"改动范围 / 不改动范围"框死
  • 跨多人 / 多 session 协作:spec 是共享语言,下次接着干不用从头讲
  • BREAKING 变更:proposal 强制你写"回滚方案",相当于上线前体检

不太适合的场景:

  • 改一行代码的小修小补:写 proposal 的时间够你改 10 个 bug 了
  • 纯探索性 demo:还没想清楚要做什么的时候,spec 反而是束缚
  • 完全独立的工具脚本:没有"项目演进"概念,主 spec 没意义

工具清单

我用的具体工具栈,给想试的同学参考:

工具 用途
Claude Code 主交互界面,跑所有 slash command
OpenSpec slash commands /opsx:explore /opsx:propose /opsx:apply /opsx:archive
Git 每个 change 独立 commit,archive 后立即提交
CLAUDE.md 仓库级长期规范
openspec/config.yaml OpenSpec 配置(capability 模板、spec 风格)
docs/data/ 项目独立维护的"数据契约文档"(表字段、Agent DTO、ER 图等),与 spec 互补

总结

回到开头那个问题:博客为什么停更两周?

因为我把这两周该写博客的精力,全压到了 OpenSpec 文档上。写 spec 的过程本质上就是在把脑子里模糊的想法用结构化语言写清楚 —— 这本身就是写技术博客的最重要的练习。15 个 change 的 proposal/design/tasks 加起来好几万字,比这两周不写的文章字数多得多,质量还更高(因为每一句都对应一行代码)。

跑下来最大的感受是:Spec Coding 不是给模型用的,是给你自己用的。模型只是个忠实的执行者,它产出的代码质量上限取决于你 spec 写得有多清楚。当你把"加个推荐功能"翻译成 proposal 里 30 行 What Changes、design 里 5 个关键决策、tasks 里 18 个可验证的子任务 —— 你脑子里那个模糊的想法就已经清晰了大半。剩下的事情交给 Claude Code 跑就行。

如果你也在用 Claude Code 做正经项目,强烈推荐试试 OpenSpec 工作流:

  • /opsx:explore 把方案聊到位,让模型替你想清楚边界
  • /opsx:propose 生成三份文档,人工逐字读完
  • /opsx:applytasks.md 推进,盯着进度,不对立刻拦
  • /opsx:archive 归档收尾,每个 change 独立 commit
  • CLAUDE.md + openspec/specs/ 沉淀仓库级 / 项目级长期规范

核心心法一句话:拆得越小,跑得越稳。Spec 写得越清,模型偏得越少。

下一篇会聊聊 voice-shopping 里 Orchestrator 状态机的设计 —— 怎么用一个手写 Service 把 4 个 Agent 串成全双工流式管道,包括 phase 转移、last_recommendations 的快照设计、PRODUCT_COMPARE 怎么基于上轮重排。