电商篇:Spec Coding——让 AI 按仓库规范干活,而不是靠猜
本文是全栈电商微服务从 0 到 1 再到 10系列的电商篇。上一篇:电商篇:客户支持——配置自定义域名邮箱;下一篇:电商篇:性能——首页迁到 SSR,PageSpeed 移动端从 61 做到 100。
这个系列的代码有相当一部分由 AI 编码工具(Claude Code、Codex)写出。工具能写代码之后,真正的问题变成:它的行为是由你定义的,还是由它猜的? JavaGuide 的《Spec Coding 规范驱动编程实战》把这个问题讲得很清楚,本文不重复它的论证,而是把它的框架逐条对到电商仓库里已经在跑的机制上——哪些早就有、哪些是读完之后补的、哪些刻意没做。
先分清 Vibe 和 Spec
Vibe Coding 不是原罪。验证想法、一次性脚本、影响面很小的内部工具,写一堆规范反而拖效率。JavaGuide 给的判据只有一条:这段代码要活多久。 两天就扔的脚本靠感觉走;超过一周、需要别人维护、接数据库或外部接口的代码,至少把约束、边界和验收标准写清楚。
电商仓库用的是另一个维度的判据——改动规模,写在 AGENTS.md 的 E3 策略里:
| 规模 | 含义 | 动手方式 |
|---|---|---|
| L1 | 单文件局部修改 | 只读预计要改的文件,改完跑最便宜的验证 |
| L2 | 少数文件跨文件修改 | 同上,验证变红才扩大范围 |
| L3 | 仓库级重构 | 才值得 plan mode、子代理、高 reasoning effort |
两个判据不冲突:「活多久」决定要不要写 Spec,「多大」决定读多少上下文。AI 动手前要先说出估计的规模,最多做一次廉价探测——这条本身就是一种 Spec:约束的不是代码,是 AI 的工作方式。
四步流程在仓库里对应什么
JavaGuide 用 GitHub Spec Kit 的四步(Specify、Plan、Tasks、Implement)作为可操作的工作流。电商仓库没有引入 Spec Kit,但每一步都有对应的落点:
| 阶段 | JavaGuide 的产出 | 电商仓库的落点 |
|---|---|---|
| Specify | requirements.md | .scratch/<feature>/spec.md,本地 markdown 入库,不用 GitHub/GitLab Issues |
| Plan | design.md | docs/design/ 按微服务分目录的架构真相源;context/ 三层知识库放约束与踩坑 |
| Tasks | tasks.md | .scratch/<feature>/issues/NN-<slug>.md,一单一文件,Status: 行承载 triage 状态 |
| Implement | 把相关 Spec 塞进提示词 | context/team/runbook.md §0.1 按改动类型路由必读文档,提交前跑固定的验收锚点 |
差别在 Plan 这一层。JavaGuide 的 design.md 是一份文档;电商仓库把它拆成两种性质不同的东西:
- 需要解释「为什么」的经验进
context/,按「换个服务还成立吗」分成团队级、框架工程级、服务级三层; - AI 每次都要现搜一遍的结构性事实进
.service-matrix.yaml:服务注册名、网关路径前缀、依赖关系、Config Center 键。它是事实表不是文档,CI 里的structcheck测试会核对它与backend/services/目录、网关接线是否一致。
这么拆的原因是 JavaGuide 也提到的那条:AI 训练数据再多,也不知道你项目里那些特定的规则。矩阵文件里区分了 depends_on(代码里真的接线了)和 depends_on_planned(设计要求但尚未接线),AI 现搜代码看不出这个差别,猜的结果通常是把后者当成已实现。
规范放在哪,怎么被读到
JavaGuide 把规范分成两层:几乎每个会话都要带上的(技术栈、代码风格、边界条件),和当前任务相关时才带的(项目愿景、命令清单、目录结构、Git 规范)。
电商仓库的分层与此一致,但多了一道硬约束——常驻层有字节预算。AGENTS.md 是所有 AI 工具共同的行为基线,每轮整份注入上下文,verify-context.sh 门禁限制它不超过 14000 字节。超限时先把内容搬进 context/ 对应层,不提额度。写本文时它是 13991 字节,还剩 9 字节;这意味着任何新增说明都必须放到按需层,常驻层只留索引和硬规则。
按需层的入口是 runbook.md §0.1 的路由表,形式是「你要动的是什么 → 先读哪份 → 不读会怎样」:
| 你要动的是 | 先读 | 不读会怎样 |
|---|---|---|
| proto / API 契约 | proto-design.md | 字段裸奔、破坏兼容性、炸前后端生成代码 |
| Redis(缓存/锁/去重/计数) | go-redis.md | 抓到已 Close 的旧客户端;非幂等命令被默认重试执行多次 |
| 数据库表结构 / 迁移 | db-migrations.md | sqlc 生成物落后 schema;种子不幂等重跑翻倍 |
| 往文档里写集群数字 | live-facts.md | 在集群故障期采数,把故障态固化成「现状」 |
第三列是这张表里最值钱的部分。JavaGuide 说规则必须写具体,「重要变更需确认」AI 没法执行;同样的道理,「先读 X」如果不写「不读会怎样」,AI 会把它当成可选项。
三色标签怎么落成硬规则
JavaGuide 的三色标签——✅ Always 自动执行、⚠️ Ask first 需确认、🚫 Never 绝对禁止——在电商仓库对应 AGENTS.md 硬规则 6。它列举得很具体:git commit、git push、分支合入、kubectl apply/delete、helm 装卸、docker push、workspace 之外的写入与删除,都是「不可逆动作」,需要用户授权。
仓库在此基础上多写了一半:授权一旦给出就直接执行,不要二次确认。 用户说「提交」「推上去」「部署到 dev」就是授权,AI 不要复述一遍风险再问「确认吗」。理由写在规则里:重复确认拖慢工作,还让用户对提示麻木,削弱真正该拦的那次。JavaGuide 建议「刚开始宁严勿松,跑一周后再放宽」,这条是放宽之后的形态。
授权也不跨范围升级:授权 apply 到 dev 不等于授权 apply 到 prod,授权 commit 不等于授权 push。subagent 永久不得执行其中任何一项,因为它拿不到用户的授权上下文。
Never 规则推进到 CI
JavaGuide 强调 Never 规则不能只靠 AI 自觉,需要多层防线:Spec 声明、配置模板、pre-commit hook、AI IDE 配置、密钥隔离。电商仓库的对应物:
| 防线 | 电商仓库 |
|---|---|
| Spec 声明 | 硬规则 4:凭据只存在 Config Center 和本地环境,仓库里只写主机名和端口 |
| Pre-commit | gitleaks --staged,工具缺失即红;细节见 GitOps 篇 01 |
| Pre-push | verify-quick.sh:后端 build/vet/test、前端 pnpm ready、凭据全历史扫描、部署清单双真相源等价,五路并行 |
| CI | verify-context.sh 是 main 上唯一必需的检查,守文档结构、链接可达、预算、决策记录格式 |
门禁本身也要被验证。仓库里有过两次同构事故:commitlint 钩子搭起来九个月一次都没生效——钩子文件的全部内容是创建它的那条安装命令;一套冻结验收机制脚本、CI、CODEOWNERS 三处接线都正确,却从未放进过任何一组真实冻结集,恒返回退出码 0。两次得出同一条判据:门禁的存在性不等于有效性,要验的是「它红过吗」。 所以 verify-context.sh 配了 verify-context-canary.sh:每类检查注入一个已知违规样本,断言门禁真的变红且违规标签正确。
验收标准:每条都要能验
JavaGuide 说「保证安全」「代码优雅」「性能要好」写了跟没写差不多。这条在电商仓库原本只有一句「交付时如实说明测了什么、没测什么」,读完文章后补成了格式要求,写在 docs/agents/issue-tracker.md:spec.md 与每张实现单的验收标准,每条必须落在五种形态之一。
| 形态 | 写法示例 |
|---|---|
| 命令 + 期望结果 | cd backend && go test -short ./services/order/... rc=0 |
| 请求 + 状态码/响应字段 | POST /api/v1/orders/export 超过 5000 条返回 409,body 含 code: EXPORT_TOO_LARGE |
| 数字 + 单位 + 阈值 | 导出 5000 条 P95 < 2 s;时间范围 > 31 天拒绝 |
| 文件/路径断言 | order.proto 的 ExportRequest 每个字段带 buf.validate 约束 |
| 具名测试 | TestExport_CrossTenantDenied 存在且通过 |
判据一条:读完这一行,能不能不问任何人就判定通过或不通过。「安全」要写成「越权租户返回 403 且不泄露是否存在」,「边界」要写成「空时间 / 越界时间 / 无权限 / 无数据四种输入各有断言」。
覆盖率数字刻意没写进模板。JavaGuide 的原话是「覆盖率阈值别机械套」,比百分比更重要的是权限、金额、状态迁移、重试、异常补偿这几类关键分支有没有断言——把这些分支点名写进验收标准,比 80% 有用。
这一条只有约定,没有门禁。自由文本上的形容词黑名单误报率高,会逼人改措辞而不是改内容;先观察一段时间再决定要不要机械化。
完成自检:不接受「已完成」三个字
JavaGuide 的建议是任务写完后让 AI 按清单逐项确认,确认不了的写出原因。电商仓库把它做成了关单条件:实现单的 Status: 改成 done 之前,文件里必须有 ## 完成自检,每条用 —— 接证据或原因。
## 完成自检
- [x] 验收标准逐条通过 —— #1 `go test -short ./services/order/...` rc=0;#2 curl 返回 409- [x] 跑过的锚点 —— `scripts/verify-quick.sh` 绿- [x] 触发的专项门禁 —— 改了 proto:`go test -count=1 ./structcheck/...` 绿- [x] TODO.md 已更新 —— 「微服务与交易闭环」P0 第 N 项- [ ] 索引命中未验证 —— 本地无 5000 条样本,线上 EXPLAIN 待人工最后一行是这个格式的关键:未验证是合法状态,假装验证了不是。 未勾选项必须保留并写原因,不许删行。verify-context.sh 的 [SELFCHECK] 检查兜底:标了 done 没有这一节、或条目没有 —— 后的证据,门禁红;canary 里专门放了一个「含带原因的未勾选项」的绿探针,防止门禁把如实写「未验证」的人误伤。
Spec 不是写完就扔:反向索引
JavaGuide 提到一个高频翻车场景:Task-001 完成时错误格式是 {"code": ..., "message": ...},两周后规范加了 trace_id,但 Task-001 的代码已经没人管了。它的建议是每个 Spec 文件维护「依赖此文件的模块」列表。
这正是电商仓库缺的那一半。runbook.md §0.1 回答的是正向问题——动某类代码前读哪份文档;规范改了之后要回头核对哪些实现,没有任何机制。补法是给 context/ 与 docs/design/ 的文档 frontmatter 加一个可选字段:
---name: proto-designlayer: teamdescription: …affects: - backend/api - backend/structcheck/rpc_method_test.go---配套两件事:
verify-context.sh的[AFFECTS]检查要求每个路径真实存在、不含 glob。指向已删路径的索引比没有更糟——它会让人以为核对过了。scripts/spec-impact.sh按 git diff 双向查:改了文档,列出登记的实现点并给出验证命令;改了代码,列出声明依赖它的文档,提醒回写。
首批只登记了四份文档。登记时机是写规则时顺手写,不做一次性全仓补全——那会写出大量 backend/services 这种泛路径,一份文档挂十几个泛目录等于没挂。
JavaGuide 还建议 CI 在 Spec 变动时自动跑受影响模块的测试。电商仓库没做:CI 只由发布 tag 触发且跑全量测试,按文档改动裁剪测试范围没有收益,本地脚本给出命令、由人跑就够了。
规则的理由比规则更难重建
规范会被改。JavaGuide 说每次 Code Review 发现问题就回去改 Spec,电商仓库多了一条:改 harness 本身(硬规则、门禁脚本、CI 门禁)时,必须在 evolution-log.md 追加一条,四要素齐全——改了什么、为什么、触发它的具体事故、怎么验证的。[EVOLOG] 门禁强制。
原因是规则能从代码读出来,理由不能。一条规则被改对之后,半年后另一个人(或另一个 AI 会话)看到它觉得「太松了」或「太啰嗦」,凭直觉改回去,当初那次事故就原样重演。
JavaGuide 没讲的一层:花多少
文章通篇在讲如何约束 AI 读什么、做什么,没有讲如何约束它花多少。执行授权不等于消费授权:几十 token 的输入可以在后台变成几百次付费决策,用户看不见也拦不住。电商仓库的 AGENTS.md 有一节「消费边界」,来自一次真实事故——让 Agent 分析怎么做一个插件,5 分钟内派出 12 个子代理、输入约 1964 万 token、缓存命中恒为 0。
落成的规则:派子代理前先说出数量和理由,单轮超过 3 个先问用户;每次扩张(新子代理、检索由目录扩到全仓、切更贵模型)回复里说一句;出现「同一检索重复执行」「连续两轮缓存命中为 0 而输入有大段稳定前缀」即停下报告;任务结束按 usage 报请求数、token、子代理数。
E3 管「读多少」,这一节管「花多少」。两者一起才是完整的执行边界。
落地时踩的坑
把上面三条补进仓库的过程本身踩了一个坑,值得记下来,因为它正是「实现与规范静默漂移」的微观版本。
给四份文档种 affects: 字段时 awk 首次出错,随手 git checkout -- 四个文件「还原」——没有先看 git status。工作树里躺着上一个会话尚未提交的产出:proto-design.md 新增的一整节评审规则被整段抹掉。恢复靠的是 AI 工具的会话记录里那条 edit 调用的原文。
教训与本文主题同构:改动没进任何索引,靠运气才被看见。对任何文件做 checkout 或 restore 之前先 git status --porcelain <file>——这条已经写进 evolution-log 的事故栏。
另一个小坑在 canary 沙箱:首次把 .scratch/ 整目录复制进沙箱,把一份带 <!-- embed: --> 指令的 demo 文档也带了进去,它引用的源文件不在沙箱树里,干净沙箱探针假红。收窄为只复制门禁真正扫描的 .scratch/*/issues/*.md 后才绿。门禁的扫描范围和 canary 的沙箱范围要一起改,这也是一种「两处真相源必须等价」。
小结
对照下来,JavaGuide 那篇文章的主张电商仓库大部分已经有了,读完补的是三处:验收标准的形态、关单前的完成自检、规范到实现的反向索引。三处都遵循同一个纪律——能机械判定的进门禁,门禁要有 canary 证明它红过,门禁的理由写进日志。
JavaGuide 结尾那句话值得原样保留:那个合并按钮,永远应该握在你自己手里。仓库里的写法是硬规则 6:不可逆动作需要授权;授权一旦给出,就直接做。