delta spec 是 OpenSpec 适配存量项目的关键。它不重新抄写整个系统规范,只描述这一次 change 相对当前主规格新增、修改、删除或重命名了什么。这样多个 change 可以各自在独立目录里工作,主规格在归档前保持稳定,评审者也能直接看到“差异”而不是在长文档里找变化。
📷 [图片 token=DzT6bpezcoF8HrxIc8vcKkg5n7b(未能下载,见飞书原文)]
文件位置和 capability 的关系
openspec/changes/<change-name>/
└── specs/
└── <capability>/
└── spec.md
目录名 <capability> 对应长期系统能力。例如主案例的 change 名是 limit-qwen-embedding-batch-size,但 delta spec 位于 specs/qwen-openai-provider/spec.md。前者是一次修复,后者是被修改的长期能力。
这个区分让主规格按领域组织,而不是按工单组织。一次变更结束后,change 进入 archive;qwen-openai-provider 仍然是系统当前能力的一部分,后续关于聊天模型、Embedding 或 rerank 的变更可以继续修改同一 capability。
📷 [图片 token=BijtbCzjkoSGF2xLg8bc1QcXnPb(未能下载,见飞书原文)]
四种变化操作
📷 [图片 token=RyrIbSx78ogmTwx66dFcX6PQn5d(未能下载,见飞书原文)]
ADDED:新增以前不存在的行为
## ADDED Requirements
### Requirement: Qwen embedding batch compatibility
后端 SHALL 确保每个请求最多包含 10 条文本……
#### Scenario: Large embedding input is split
- **WHEN** 文档索引包含超过 10 个 chunk
- **THEN** 客户端 MUST 拆分为兼容批次并保持顺序
归档同步时,这类 requirement 会加入主规格。如果同名 requirement 已经存在,本地 sync Skill 会把它视为隐式修改,而不是机械追加重复内容。
📷 [图片 token=SGvpbynZ0ofNVLxORjmcwNAmn2g(未能下载,见飞书原文)]
MODIFIED:修改已有行为的一部分
MODIFIED 可以只写需要变化的描述或新增场景,不必复制整个旧 requirement。Agent 会读取 delta 和 main spec,智能合并变化,同时保留 delta 没有提到的其他场景。这样 delta 表达的是变化意图,不是整块覆盖文件。
📷 [图片 token=TkSFblV0Eo52ycx3TDQcYmi3nmb(未能下载,见飞书原文)]
REMOVED:明确废弃行为
REMOVED 应写出要删除的完整 requirement 名称和必要原因。同步后,主规格中对应 requirement 应消失。OncallAgent 的 wiki-sync 会校验最新 REMOVED 操作是否真的已经从主规格移除,避免 change 虽然归档,旧行为却仍被主规格宣称有效。
📷 [图片 token=IU6kbn89Lou9IQxYvLic1kpAnTd(未能下载,见飞书原文)]
RENAMED:改变 requirement 名称
RENAMED 用 FROM/TO 指出重命名。它表达的是同一行为契约的名称演进,而不是删除旧行为后随便新增一个不相关行为。同步器需要同时处理旧名称消失和新名称出现。
📷 [图片 token=PRrpbt5VKovTlHx2OYvcuQbanHf(未能下载,见飞书原文)]
Requirement 应该写什么
Requirement 描述系统必须具备的行为,常用 SHALL、MUST、MUST NOT 表示强约束。它应回答“系统对调用者作出什么承诺”,而不是“代码用哪个类完成”。
主案例的 requirement 同时包含两个承诺:单个 Embedding 请求最多 10 条文本;任意数量输入都返回完整且顺序对应的向量集合。前半句约束外部请求兼容性,后半句保护业务语义。如果只写“设置 chunk_size=10”,即使代码参数存在,也无法保证输入输出顺序和完整性。
📷 [图片 token=Nlyyb95GVo5NPcxtDPKcY9XInqg(未能下载,见飞书原文)]
Scenario 为什么比一句需求更重要
Scenario 把抽象 requirement 转换成可验证例子。OncallAgent 常用 WHEN/THEN,必要时可以增加 GIVEN 和 AND:
#### Scenario: Large document indexing completes
- **WHEN** 可访问文档被拆分为超过 10 个有效 chunk,
且 Embedding 与 Milvus 可用
- **THEN** 索引任务 MUST 生成全部向量、写入全部 chunk,
并标记为 succeeded
这个场景覆盖的不只是拆批,还覆盖最终业务结果。Provider 单测可以证明请求被拆成 10+1,文档索引测试则证明 11 个业务 chunk 最终完整落库。一个 requirement 可以由多层测试共同提供证据。
📷 [图片 token=IKNybtbQ4ozzToxQYYicOpeynEb(未能下载,见飞书原文)]
为什么 spec 不写实现细节
如果 spec 写“在 provider.py 第 150 行给 OpenAIEmbeddings 传 chunk_size=10”,文件改名、封装替换或依赖升级都会让规格失效,即使系统行为仍然正确。实现位置、类名和参数属于 design;spec 只要保证每批不超过上限、顺序完整和索引成功。
这种分离让实现可以重构。未来即使不再使用 LangChain,只要新 Provider 仍兑现同样行为,主规格无需重写;如果厂商上限变化,才创建新的 delta 修改行为契约。
📷 [图片 token=AVlmbU1lVoD3YOxPCdycgCtintg(未能下载,见飞书原文)]
怎样审查 delta spec
**操作类型是否准确。**新增能力用 ADDED,修改已有约束用 MODIFIED。不能为了省事全部写 ADDED,否则主规格可能出现重复 requirement。
**场景是否覆盖边界。**主案例同时写小输入、超过上限的大输入和最终索引完成,覆盖正常路径、边界分支与业务结果。
行为是否可观察。“代码更优雅”“架构更合理”无法作为 Scenario 结果。应能通过测试、API、状态或持久化结果判断。
**是否遗漏权限和失败语义。**OncallAgent 的知识、MCP、聊天和 AIOps 数据都按 owner/tenant 隔离;涉及这些能力时,Scenario 必须包含授权和越权行为。真实工具失败也要如实返回,不能把失败写成成功。
📷 [图片 token=P2Skb6FdMoudZTxCgd3c5tF1nQc(未能下载,见飞书原文)]
面试表达
delta spec 只描述这次变化,不复制整份主规格。它按 capability 归档,用 ADDED、MODIFIED、REMOVED、RENAMED 表达意图,再用 Requirement 和 WHEN/THEN Scenario 定义可验收行为。这样我能把“代码改了什么”提升为“系统承诺发生了什么变化”,归档时再智能合并到主规格。