人与 AI 的分工说清楚了:人负责判断方向、划定边界,AI 负责把决策落成代码。接下来最容易被忽略的问题是,人的判断怎样才能稳定地传给 AI,而不是只在一次对话里短暂生效?

这正是规范驱动开发(Specification-Driven Development,简称 SDD)要解决的问题。它的重点不是“多写几份文档”,而是先把需求、边界和验收条件写成可检查的约束,再让 AI 在这些约束内完成实现。

📷 [图片 token=Tvcgbjil3ooQ7VxSWTsccbCZnwh(未能下载,见飞书原文)]

[!SUCCESS] **本讲核心:**Prompt 告诉 AI 这一次要做什么;OpenSpec 则把“为什么做、做到什么程度、哪些不能动、如何证明完成”保存为项目可持续使用的工程上下文。

本文会直接使用课程项目 OncallAgent 的真实仓库来讲解。这个项目不是单页 Demo,而是一个本地优先的 AIOps Agent 工作台,包含 FastAPI 后端、Vue 3 前端、流式聊天、知识库 RAG、MCP 工具、告警诊断、SQLite 持久化和 Milvus 向量检索。功能之间存在大量跨层依赖,因此特别适合用来理解 SDD 的价值。

📷 [图片 token=T2cDbJcwEoSexbxJTvfc0rYZnHp(未能下载,见飞书原文)]

一、Prompt 很清楚,为什么项目还是会跑偏

假设你对 AI 说:“修一下聊天流式输出,让回答逐字出现,同时别把上一轮的引用带到下一轮。”这句话对人来说不难理解,但对实现来说仍然留下了许多空白。

“逐字出现”应该由后端拆分 SSE 事件,还是由前端拿到整段文本后做动画?推理过程和工具调用事件要不要一起拆?逐字输出以后,最终保存到数据库的正文能不能变化?新一轮开始时应该立刻清空引用,还是等回答完成再清空?重新打开历史会话时,展示全部历史引用,还是只展示最新回答的引用?

📷 [图片 token=AmjhbswK1oQ2e8xlyj5cqUfInqg(未能下载,见飞书原文)]

这些问题没有写清楚,AI 就只能根据训练数据和当前代码自行补全。它可能给出一个能运行的版本,却未必是你真正需要的版本。更麻烦的是,同一个需求在不同对话里执行两次,AI 自行补全的答案也可能不同。

只给任务描述使用 OpenSpec 变更
意图停留在聊天上下文里意图写入 proposal,可回看、可讨论
边界由 AI 临场猜测Goals、Non-Goals 和设计决策明确边界
“看起来能用”就可能结束Requirement 与 Scenario 给出可验证结果
下一次修改容易忘记来龙去脉归档保留完整决策历史,主规格记录当前事实

所以,SDD 与“把 Prompt 写详细一点”并不是同一件事。详细 Prompt 可以提升一次输出的质量;SDD 解决的是一个项目在几十次甚至几百次 AI 协作之后,仍然保持同一套产品逻辑和工程约束。

📷 [图片 token=VzISbwvuKoqYvtx5u9scQc7anHb(未能下载,见飞书原文)]

二、一个真实变更如何被规范“锁定”

仓库中有一个已经完成并归档的变更,名称是 fix-chat-turn-streaming。它处理的正是上面提到的两个问题:回答看似使用 SSE,较大的模型 chunk 却会整段出现;“本次回答引用”还可能残留上一轮内容。

如果只把任务交给 AI,它可能只改前端,也可能把所有 SSE 事件都拆碎,甚至顺手调整数据库结构。OpenSpec 的做法,是先建立一个完整的“变更包”。

📷 [图片 token=Unb3b09FDoPuUKx7BuHc19Kjnac(未能下载,见飞书原文)]

proposal:先回答为什么改,以及影响哪里

proposal.md 记录问题、期望变化和影响范围。这个变更明确影响聊天前端 store、后端 SSE 编排服务和相关测试,同时明确不修改共享事件类型、知识引用结构与数据库模型。这样一来,AI 在动手之前就知道哪些区域属于任务范围,哪些区域不应被顺手重构。

📷 [图片 token=DNK3bJAU1ohJnpxo52ycyQukn2c(未能下载,见飞书原文)]

spec:把模糊体验改写成可观察结果

“流式效果更顺滑”无法直接验收。delta spec 把它改写成了接近测试用例的场景:

WHEN Agent 一次产生“收到。”三个字符
THEN 后端按“收”“到”“。”的顺序发出 content.delta
AND sequence 逐个递增
AND 最终持久化正文仍然等于“收到。”

引用隔离也不再用“不要串轮次”这种模糊说法,而是明确:新一轮发送开始时清空旧引用;重新加载会话时只读取最新一条 assistant 消息的引用;如果最新回答没有引用,即使更早的回答有引用,当前引用区域也要保持隐藏。

📷 [图片 token=MDiibQv9LovRfAx83m6cajmbnhw(未能下载,见飞书原文)]

design:不仅写怎么做,也写为什么这样做

design.md 把关键取舍固定下来:只在后端 ChatStreamingService 边界拆分最终回答正文,因为这里能区分正文与推理、工具调用、引用等其他事件;持久化仍按原始 chunk 拼接,避免改变最终消息;推理、工具、引用和完成事件保持原来的结构与粒度。

这段设计说明非常重要。单独写“不要拆工具事件”只是命令,补上原因之后,AI 才能理解这是为了控制事件数量、保护既有时序和共享 SSE 契约,而不是一个可以随意删除的偏好。

📷 [图片 token=Fja2btSQiouTe4xSVmOcPjXEnub(未能下载,见飞书原文)]

tasks:把规范变成可以逐项完成的工作

tasks.md 将工作拆成后端逐字符输出、前端引用隔离、回归测试和交付验证。AI 每完成一项就勾掉一项,人也能随时看到剩余工作,而不是等到最后才发现“后端改了,前端测试没补”。

最终实现与规范一一对应:后端在 apps/backend/src/super_ai/chat/streaming.py 中逐字符发出正文事件;前端在 apps/frontend/src/stores/chat.ts 中发送前清空引用,并在加载会话时只选择最新 assistant 的 citations;前后端测试分别覆盖事件顺序、持久化一致性和跨轮引用隔离。

📷 [图片 token=XgNrbjPtDo4cZsxhVMscyx4Snff(未能下载,见飞书原文)]

三、OpenSpec 把 SDD 变成一条可执行流水线

理论上的 SDD 常被概括为“先规范、再实现、再验证”。OpenSpec 进一步把这套思路落成了仓库结构和操作流程。对学生来说,可以先记住下面这条主线:

理解现状 → 建立 change → 写清规格与设计 → 按任务实现 → 对照规格验证 → 同步并归档

📷 [图片 token=HZo3bjFWTo8SZKxKZybcoBtSnwg(未能下载,见飞书原文)]

第一步:理解现状,而不是立刻生成代码

先阅读相关主规格、现有实现和测试,确认当前系统已经支持什么、这次只改变什么。OncallAgent 的 AGENTS.md 明确要求:新增功能、用户可观察行为变化和非平凡缺陷修复,应先创建或继续一个聚焦的 OpenSpec change。

这一步看似慢,实际上是在避免最昂贵的错误:AI 按一个错误前提写出大量正确代码。比如,仓库已经规定 packages/api-contracts 是 HTTP、错误码、OpenAPI 与 SSE 类型的唯一事实来源。如果没有先读现状,AI 很容易在前端或后端再复制一套临时 DTO,功能能跑,却给项目留下两套契约。

📷 [图片 token=Q5X6bV3vMo9vJxxcbtuc1OkDnsf(未能下载,见飞书原文)]

第二步:建立一个边界清楚的 change

变更名使用简短的 kebab-case,例如 fix-chat-turn-streaming。基础命令如下:

openspec new change fix-chat-turn-streaming
openspec status --change fix-chat-turn-streaming

在这个仓库中,也可以通过配套的 AI skills 按完整生命周期推进:

$openspec-propose
$openspec-apply-change
$openspec-verify-change
$openspec-archive-change

一个标准 change 会逐步形成 proposal.mdspecs/design.mdtasks.md。四类文件回答四个不同问题:为什么改、外部行为是什么、内部怎样实现、具体先做什么后做什么。

📷 [图片 token=DEPab2usvosF4gxtI8xcHYxenQc(未能下载,见飞书原文)]

第三步:AI 按任务实现,人持续做决策

进入 apply 阶段后,AI 读取完整变更上下文,再按 tasks 逐项实现。此时人的工作不是盯着每一行代码,而是判断实现有没有偏离目标:是否触碰 Non-Goals,是否破坏共享契约,是否遗漏权限过滤,是否增加了不必要的依赖,是否为场景补上了测试。

任务清单也不是一张“许愿单”。每完成一项就应立即更新状态;如果实现过程中发现设计不成立,应返回修改 design 或 spec,而不是让代码偷偷偏离文档。规范可以迭代,但规范与实现不能各走各的。

📷 [图片 token=Rsy8biY7qodNHaxQosMc5NHMnlh(未能下载,见飞书原文)]

第四步:验证的对象不是“代码能运行”,而是“需求被证明”

验证至少包含三个层面。首先看完整性:tasks 是否全部完成,Requirement 是否都有实现落点。其次看正确性:每个 Scenario 是否有对应逻辑与测试。最后看一致性:实现是否遵守 design、全局仓库规则和既有代码模式。

OpenSpec 本身需要执行结构校验:

openspec validate --all

但这条命令不能替代代码测试。聊天或 SSE 变更还应运行共享契约测试、相关后端 pytest 和前端 Vitest;数据库变更要验证 Alembic migration;可见界面变化需要浏览器验收。只有实际执行并通过的检查,才可以写进交付结果。

📷 [图片 token=G6dKbEmmuoOiUvxTOP6cw6ozntg(未能下载,见飞书原文)]

第五步:同步主规格并归档,让项目拥有长期记忆

变更通过验证后,将 delta specs 同步到 openspec/specs/,再把 change 移入 archive。此后,主规格表达“系统现在应该怎样工作”,archive 保留“当时为什么这样改”。

这一步解决了 AI 没有稳定长期记忆的问题。下一次处理聊天流时,AI 不需要依赖某个人记得半个月前的一次对话,只需要读取当前主规格和历史变更,就能恢复关键决策。

📷 [图片 token=TPHYbiAtNokMc5xqQeDcLTcznDd(未能下载,见飞书原文)]

四、仓库里的规范不是一份文件,而是四层约束

随着项目变复杂,把所有规则都塞进一个超长提示词并不可取。OncallAgent 采用分层的事实来源,每一层负责不同问题。

层次仓库位置负责回答的问题
全局工程规则AGENTS.md整个仓库长期遵守什么原则
当前产品规格openspec/specs/系统现在对用户承诺什么行为
本次变更规范openspec/changes/<change>/这次为什么改、改什么、不改什么
机器可检查契约packages/api-contracts/ 与测试接口形状、事件类型和场景如何被自动验证

AGENTS.md 像项目宪法。例如,FastAPI 路由保持薄层,业务逻辑放进 service 或 repository;前端沿用 Vue 3、TypeScript strict、Pinia 和既有 client;所有用户数据必须携带 owner 或 tenant scope;日志不得记录密钥、用户消息、工具参数值和模型正文。

openspec/specs/ 像现行法律。这里分别描述聊天、知识库、MCP、AIOps、后台任务、认证授权等能力当前必须满足的行为。

活动 change 像一次修正案。它只处理一个聚焦问题,用 proposal、delta spec、design 和 tasks 说明变化。完成后,真正需要长期保留的规则进入主规格,过程材料则进入归档。

packages/api-contracts 和测试则把文字约束变成机器能检查的边界。例如共享 SSE 类型明确有哪些事件,前后端都从同一处消费;测试进一步证明逐字符事件顺序、错误结构和引用隔离没有被破坏。

📷 [图片 token=HzABb4GHFojbyrxOX59ciR5Cntf(未能下载,见飞书原文)]

五、能约束 AI 的规范,要满足四个条件

条件一:描述可观察行为,不写空泛形容词

“体验要流畅”“代码要优雅”“权限要安全”都无法直接检查。更有效的写法是:最终回答的每个 content.delta 只包含一个字符;跨 tenant 读取返回统一 403;知识检索必须携带当前用户和知识库过滤;空结果返回空数组,不生成虚构内容。

判断方法很简单:两位同学看到同一条规范,能否写出基本相同的验收测试?如果不能,规范仍然太模糊。

📷 [图片 token=AeoJbbUlZosANQxI5YwcHVQjnNc(未能下载,见飞书原文)]

条件二:明确 Non-Goals,主动缩小解释空间

规范不仅要说“做什么”,还要说“这次不做什么”。聊天逐字流变更明确不调整 LangChain Agent、不修改数据库、不改变推理和工具事件粒度。Non-Goals 能阻止 AI 把一个小修复扩展成跨模块重构,也能帮助人判断某个“顺手优化”是否应该拆成另一个 change。

📷 [图片 token=IXtZbkbyOohVOlxcyoZc5DuwnDb(未能下载,见飞书原文)]

条件三:重要取舍要说明原因

涉及工程权衡时,只写禁止项往往不够。比如仓库规定前后端不能各自复制接口 DTO,因为 packages/api-contracts 是唯一事实来源;AIOps 不能伪造日志、告警和工具结果,因为诊断报告必须能追溯到真实证据链。原因会帮助 AI 在没有逐字覆盖的新场景中做出同方向判断。

📷 [图片 token=Q4TLbdJXfoKp8ixje1McmDWznxe(未能下载,见飞书原文)]

条件四:每条要求都能映射到验证

好的 spec 天然带着测试入口。WHEN 描述前置条件,THEN 描述必须出现的结果,MUST 与 SHALL 表示不可随意降级的约束。如果某条 Requirement 找不到实现位置,也找不到测试或手工验收方法,它大概率还没有写到可执行的程度。

📷 [图片 token=BawQbb2EVoZeSdxjOZqcAxSOn8f(未能下载,见飞书原文)]

六、AI 跑偏时,别只修代码,还要修轨道

第一次写出的规范一定不完整,这很正常。SDD 的关键不在于开工前预测所有问题,而在于把每次偏差转化为下一次可以复用的约束。

仍以聊天逐字流为例。如果 AI 把 reasoning 事件也拆成单字符,修复代码只是处理这一次错误;把“非正文事件保持原粒度”补进 Scenario,才是在修轨道。如果前端重新加载会话时又聚合了全部历史引用,除了改 store,还应把“只取最新 assistant citations”写进当前主规格和回归测试。

📷 [图片 token=NQlrbknTjob6w1xVlSTc0A6JnAb(未能下载,见飞书原文)]

仓库中的许多全局规则也是这样沉淀出来的:API/SSE 统一走共享契约;路由不承载业务逻辑;SQLite 通过 repository 边界访问;Milvus 检索必须带 tenant 过滤;MCP 与 AIOps 不得用 mock 结果冒充真实成功;真实凭据不能进入 Git、日志或错误响应。

每当 AI 偏离预期,可以按下面三个问题复盘:

  1. 这是一次偶然实现错误,还是规范没有说明?

  2. 这条约束只属于当前任务,还是以后所有模块都应遵守?

  3. 怎样把它写成一个可以通过测试或检查证明的 Scenario?

如果只影响本次实现,就更新 change 的 spec、design 或 tasks;如果是长期产品行为,就在归档时同步进主规格;如果是全仓库都要遵守的工程原则,就补充到 AGENTS.md 或共享契约。这样,偏差不会白白发生,而会成为项目知识的一部分。

📷 [图片 token=NX1vbYcWTo48T3xDS2sclCgXnec(未能下载,见飞书原文)]

七、第一次实践 OpenSpec,可以从小需求开始

不要一上来给整个系统写一份几十页的“大一统规范”。选择一个用户能感知、范围又足够小的变化最合适,例如“知识文档上传失败时保留可重试状态”“MCP 连接检查显示安全错误摘要”或“聊天最新回答没有引用时隐藏引用区”。

📷 [图片 token=XWycbkh7VoPz6uxgRZYcCtQ5nue(未能下载,见飞书原文)]

动手前检查:

  • 我已经阅读相关主规格、实现和测试,而不是只看需求描述。

  • 我能用一句话说明用户当前遇到的问题。

  • 我写清了这次变化的范围与 Non-Goals。

  • 每条 Requirement 至少有一个可观察的 Scenario。

实现中检查:

  • AI 正在按 tasks 逐项实现,完成后立即更新任务状态。

  • 实现没有越过 change 边界,也没有绕开共享契约和 tenant 规则。

  • 发现设计问题时,我先更新规范,再继续写代码。

交付前检查:

  • 我运行了 openspec validate --all

  • 相关前端、后端、契约或迁移测试已经真实执行。

  • delta specs 已同步到主规格,change 已验证并归档。

这份清单的意义不是增加仪式感,而是帮助初学者把“我觉得做完了”转换为“我能证明它做完了”。当这种思维形成习惯后,你会发现 review 变得更快,因为检查不再依赖临场感觉,而是逐条核对已经约定的行为。

📷 [图片 token=HVNrbJe5coI5Z8xKAkhce0q2nf7(未能下载,见飞书原文)]

总结:真正的轨道,是可以验证和积累的决策

SDD 不是要求人把所有代码细节提前设计完,也不是让 AI 失去发挥空间。它要固定的是目标、边界和验收标准,把实现路径中的重复劳动交给 AI,把需要判断的工程取舍留给人。

在 OncallAgent(Agent Py)仓库中,OpenSpec 让这套方法形成了闭环:AGENTS.md 提供全局工程纪律,openspec/specs/ 保存当前产品事实,change 记录一次变化的 proposal、spec、design 与 tasks,共享契约和测试负责自动验证,archive 留下决策历史。

📷 [图片 token=UoV8bizSmonTugx8I74cdRURnix(未能下载,见飞书原文)]

没有规范时,AI 也许能很快写出一段可运行代码;有了 OpenSpec,它才更有机会连续完成一个长期演进、跨前后端并且需要真实验证的工程项目。

**记住这条公式:**先把需求写成可验证的 change,再让 AI 实现;发现偏差就更新规范,验证通过后同步并归档。规范越清晰,返工越少;项目积累得越久,这套轨道的价值越大。

下一步可以选择仓库里的一个小改动,亲手创建第一个 OpenSpec change。不要从“请帮我写代码”开始,而要先回答四个问题:为什么改、外部行为是什么、哪些东西不能动、如何证明完成。

📷 [图片 token=PmYDbOdzrocP56xjIJQchsponJb(未能下载,见飞书原文)]