[控场AI]
· 10 分钟阅读· 5,354 字

Living Specs:让AI编码规范不再写完即死

Living Specs:让AI编码规范不再写完即死

用每能力一份持续更新、就近存放、自动加载与折回的living spec,替代写完即冻结的规范档案。

文章针对传统 spec-driven development 中规范写完即被遗忘、累积成无人阅读的档案这一问题,提出 living spec 方案:每个能力(如计费、认证)维护一份持续更新的规范,只记录需求与场景而不记录实现细节;在 specify 阶段按文件 glob 匹配并自动加载相关规范(含父级),功能完成时把变更以 ADDED/MODIFIED/REMOVED/RENAMED 折回规范,折回内容随代码进入同一 PR diff 从而被真正 review。规范可选集中存放或就近放在代码旁,由侧边栏树统一导航。对绕过流程的快速修复,采用漂移检测与 living-sync 同步而非强制门禁,同步以编辑方式保留人工内容。

写一份规范,交给AI编码代理,功能上线——然后这份规范就再也没人打开过。这几乎是所有尝试spec-driven development(规范驱动开发)的团队都会踩的坑。VS Code插件SpecKit Companion的作者在写了238份规范后,发现自己的specs文件夹成了一个只增不减的档案馆。

他提出的解法是living spec:一份对应当前能力(如计费、认证、成员档案)的持续更新规范,每次涉及该能力的编码任务开始前被自动加载,任务完成后又被自动改写回来。这套闭环机制,正在把"规范"从一个一次性快照变成AI代理的可复用上下文来源。

rss source: Living Specs in SpecKit

传统规范的三个结构性缺陷

每次从零开始理解代码

在常规spec-kit流程中,specify、plan、tasks、implement四个阶段各产出一份markdown文件。当一份新规范涉及某个对话框时,代理会重新调查它的数据模型、周边组件和既有约定——然后把学到的东西写下来,存进一个下一轮运行根本不会读的文件夹。

相同的工作被重复执行,每次都从代码重新推导。作者指出,这些理解本来是真实且被记录下来的,问题只在于它被归档了,而没有成为下一次运行的起点。

找不到"这个功能现在到底怎么工作"

这是作者作为人类开发者感受最深的问题。以他的仓库中"任务如何被标记完成"这条线为例,他迭代了多次,每次都生成独立的规范。结果文件夹里有五份相关规范,其中四份已被取代,但没有任何一份标明哪份是当前有效的。

他在文中引用了一个观点:多数团队为一份"会留下来"的规范付费,实际得到的却是一份写完就被遗忘的规范。规范本质上是某一时刻意图的快照,快照不会自己更新。堆到94份,得到的是档案,不是文档。

代理只读它附近的东西

当你让Claude或其他模型处理某个功能而不走规范流程时,它会读取你提到的那部分代码附近的文件。靠近的代码是真实信息,但有两个问题:邻近代码未必是你认可的标准,代理会照抄没人背书的约定;而specs文件夹这个唯一记录"预期行为"的地方,因为快速膨胀且无法判断何为当前版本,实际上不可检索,代理只能绕开它。

这引出了整个设计的核心观察:一个紧挨着它所描述代码的markdown文件,无论是否走规范流程、无论哪个供应商的模型、无论什么提示词,都会被读到。位置本身就是功能。作者还引用了Chroma关于上下文退化(context rot)的研究,说明模型会在会话结束前遗忘早前被告知的内容,而一个能跨越会话存活的文件正是把它带回来的方式。

上下文退化(context rot) 指的是大语言模型在长会话中,随着上下文窗口被逐渐填满,对早前信息的注意力与召回能力下降的现象。Chroma 的研究通过“大海捞针”式实验发现:即使关键信息仍在上下文窗口内,模型在上下文变长后提取该信息的准确率也会显著下降。对编码代理而言,这意味着会话初期加载的架构约定、需求背景,到会话后期可能已“形同虚设”。Living spec 的应对思路不是扩大窗口,而是把关键约定固化成一个磁盘文件——每次新会话按需重新读入,相当于把上下文重置回干净状态,从而绕开退化问题。

Living Spec的设计取舍

灵感来自OpenSpec

作者参考了OpenSpec的做法:每个能力维护一份持久规范,变更上线时折回其中。他同时分析了788个使用该模式的开源仓库,发现两个关键问题。

第一,折回步骤是手动的,而跳过它就是记录死亡的开始——代码继续演进,规范原地不动,几个月后才有人发现。第二,没有人维护没人读的文字,即使是成熟仓库也把purpose那一行留成占位符。这两点直接决定了SpecKit Companion的差异化设计。

配置:一个YAML文件决定开关

功能默认关闭,直到项目根目录出现living-specs.yml。每个能力有名字和匹配的文件glob:

# 📃 living-specs.yml
enabled: true
capabilities:
  - name: member-profiles
    match: ['src/features/profile/**', 'src/api/members/**']
  - name: directory-search
    match: ['src/features/directory-search/**']
    spec: src/features/directory-search/directory-search.spec.md # 👈 lives next to the code

match是最重要的部分,它决定一次变更属于哪个能力。spec行决定规范文件放在哪里:不写则集中存在capabilities/下(OpenSpec风格),写了则放在它所描述代码的文件夹里。两种方式可以按能力自由选择,/speckit.companion.living-move随时能迁移。

只写"要做什么",不写"怎么做的"

一份living spec只包含需求(Requirements)和场景(Scenarios),不写组件名、端点路径或数据模型。作者的解释是:代码可以在规范之下被重构而规范不失效,而容易过时的部分(怎么实现的)恰恰是代理随时能从代码里读到的。


**需求(Requirements)与场景(Scenarios)** 的写法源自行为驱动开发(BDD)中的 Gherkin 语法传统,其核心是用 Given/When/Then 或 When/Then 结构把一条需求表达为可验证的具体行为。与用例(use case)不同,场景刻意保持小而具体,一份规范通常由若干条需求、每条需求下挂多个场景组成。这种结构对 AI 代理尤其友好:需求层给出“必须成立什么”的约束,场景层给出可对照检查的输入输出,代理在实现和验收时都能逐条映射。由于场景只描述外部可观察的行为,不绑定内部实现,代码重构时规范无需改动。
# Member Profiles

## Purpose

Every Teamboard member has one profile: a name, a role, a team and a short bio.
This capability owns who may read a profile, who may change it, and what the
rest of the product is allowed to assume about its fields.

## Requirements

### A member edits only their own profile

A profile update MUST be accepted only from the member it belongs to or from a
workspace admin.

#### Scenario: a member opens another member's profile
- **WHEN** the viewer is neither the owner nor an admin
- **THEN** the profile renders read-only and the update endpoint refuses the change

侧边栏树让"就近放置"不失控

如果每个规范都散落在源码树里,导航会变成灾难。作者的解法是Living Specs侧边栏视图:所有能力以树形展示,按位置分组,集中存放的和就近存放的同处一棵树。规范以页面而非原始markdown渲染。作者认为这是让co-located specs变得实用的关键——其他工具没有做这部分。

折回机制:规范作为交付的副产品被维护

开始前加载,完成后改写

一轮规范运行开始时,specify阶段会找出相关区域的living specs并在起草前加载,而且不止加载你点名的叶子节点,还包括层级中的父级,让新规范从整个区域的当前事实出发。plan和tasks都基于它工作。

改写发生在最后。代理为每个被改动的能力在功能自己的规范里追加一小节,说明改了什么。功能标记完成时,这一节折回对应的living spec:新增需求追加、修改需求替换旧文本、删除需求移除。四类操作分别是ADDED、MODIFIED、REMOVED、RENAMED。

因为这一节写在功能规范里,它会和代码出现在同一个PR diff中,living spec的变更被真正review,而不是盲目应用。如果一轮运行加载了某份living spec却什么都没写给它,完成时会被点名并提示"无内容可折回",跳过折回变成可见事件而非静默失败。

作者举了自己第一个真实案例:一个webview渲染改动。工单里完全没提HTML安全,但那个区域的living spec提了,这条规则一路穿过所有阶段,最终变成一个可勾选的验收项。规则之所以到达了工作现场,是因为规范被加载了,而不是因为有人记得。

对抗"规范会冻结"的质疑

作者正面回应了Kent Beck对spec-driven development的批评:问题从来不是"你把东西写下来",而是"你写下来之后它们在代码演进时冻结了"。完成时折回就是解冻。

他也承认反方观点的合理性:可执行测试是更持久的契约,因为它能跨越模型升级保持不变,而散文会被重新解释。但他认为living spec承载了测试无法承载的东西,这正是该功能存在的理由。

漂移(drift) 在软件工程中原本指制品之间逐渐失去同步的状态,例如文档与代码、接口定义与实现之间的偏差。在 living spec 语境下,漂移指代码已经变更、而对应能力的规范未随之折回的情况。它与“规范过时”的区别在于可检测性:过时是笼统判断,漂移可以通过比对上次规范提交以来的文件变更精确计算出来,并按能力归因。漂移检测因此成为一种轻量治理手段——不阻止开发者绕过流程,但让“绕过了什么”在侧边栏和命令输出中显式可见,把技术债从隐性变为可追踪。

处理"绕过流程"的现实情况

大型spike、探索性功能、周五的快速修复——作者承认他自己也经常不走spec-kit。而在几十人的团队中,不是每个人都用同一套AI工作流,这很正常。代码动了、规范没动,正是那788个仓库里记录死亡的方式。

Companion对此的处理是漂移检测(drift),而不是强制门禁。侧边栏会为代码在上次规范提交后发生变更的能力显示漂移徽章:

# 👇 what moved, uncommitted work included
/speckit.companion.living-drift --working

# 👇 update every affected spec
/speckit.companion.living-sync

漂移检测按能力列出变更文件并说明滑落方式:tracked文件走了流程但变更没折回(漏同步);unspeced文件完全在流程外变更(最该先看)。同步操作按能力分组变更、读取实际改动并更新受影响规范。

关键设计是"编辑而非重新生成":变更未触及的每条需求和场景逐字保留,同步永远不会抹平某人手写的内容。修改保持未提交状态,让代码和规范一起被review。

落地成本与开放问题

这套机制要求团队真正采纳它:添加领域、组织团队、决定living specs放在哪里,并且要有意识地处理漂移。作者坦承还有一个未想清楚的问题:specify和plan阶段会收集大量信息——数据模型、架构、决策——目前living spec只保留"功能怎么工作",其余不留。这些产物是否该被持久化、它们是增进了对功能的理解还是只是增加了上下文、是否还能保持可维护,作者明确表示这是开放问题,不是路线图。

上手路径按文章顺序排列:安装扩展、创建living-specs.yml并设enabled: true、对你最常解释的那个区域跑/speckit.companion.living-adopt src/<area>并阅读草稿、走一轮完整流程检查Overview的折回徽章、出现漂移时跑living-sync并把规范修改和代码一起提交。

90多份没人打开的规范教给作者的东西,不如那几份每次会话都被加载的规范多。这句话大概是整篇文章最凝练的结论。

#living spec#spec-driven development#SpecKit Companion#AI编码规范#规范驱动开发#OpenSpec#AI编程工作流#上下文管理
分享:

相关推荐