返回博客

使用 Claude Code 进行规格驱动开发

使用 Claude Code 进行规格驱动开发

在 Claude Code 中输入"add user authentication"的开发者每次都会得到不同的结果。可能是 JWT,可能是会话 Cookie,也可能是带有刷新令牌和 PKCE 的完整 OAuth2 流程。代理不知道你想要什么,因为你没有告诉它。你给了它一个方向,不是一个目的地。

我观察到的那些从 Claude Code 持续获得可交付产出的开发者有一个共同习惯:在把工作交给代理之前先写规格说明。不是小说,不是只有三句话上下文的 Jira 工单。而是一个具体的文档,在任何人写一行代码之前就定义了"完成"的样子。

这不是什么新智慧。规格优先的开发在 AI 之前已经存在了几十年。但在代理场景下,跳过规格的成本更高,写规格的收益更大。人类开发者可以在实现过程中停下来问"等等,你说的是密码认证还是 SSO?"代理会默默选择一个然后继续。等你发现的时候,它已经做错了东西,你花了 20 分钟审查需要丢弃的代码。

这篇文章将介绍我每天与 Claude Code 一起使用的规格驱动生命周期:如何编写代理可以执行的规格、用 plan-before-code 检查点早期捕获误解,以及一个比"能编译"更严格的验证协议。

为什么"直接做"在代理场景下会失败

让我们具体看看失败模式。当你给 Claude Code 一个模糊的指令时,三件事会出错:

静默假设。 代理用自己的假设填补规格中的每一个空白。有时这些假设是合理的,有时不是。在读到输出之前你不会知道属于哪种情况。使用模糊指令时,你最终审查输出的仔细程度会超过写规格所需的精力。

不可复现的结果。 用同一个模糊的提示词运行两次,你会得到两个不同的实现。不只是变量名或格式不同,而是架构决策不同、库不同、错误处理策略不同。如果你无法复现输出,就无法围绕它构建可靠的流程。

审查成为瓶颈。 当代理做了所有决策,你就必须验证所有决策。一个你理解每个选择的 400 行 diff 需要 5 分钟审查。一个代理选择了数据库模式、API 形状、错误代码和验证逻辑的 400 行 diff 需要 30 分钟,因为你在从实现中逆向推导规格。

解决方案不是更好的提示词,而是把重要的决策前置到一个代理可以执行的文档中。

规格驱动的生命周期

工作流有五个阶段。每个阶段都有明确的进入条件和退出条件。

第一阶段:头脑风暴。 探索问题空间。约束是什么?有哪些方法?之前试过什么?这里你大声思考,独自或与 Claude Code 在对话模式中。退出条件:你有了首选方案并理解了取舍。

第二阶段:审查。 对方案进行压力测试。什么可能出错?有哪些边界情况?这与代码库中已有的东西冲突吗?如果你与多个代理合作,这里架构代理或第二意见很有价值。退出条件:你确信方案是可靠的。

第三阶段:规格。 写下你的决定。问题陈述、建议方案、要修改的文件、可机械验证的验收标准和测试计划。这就是契约。退出条件:任何人(人类或代理)都能读这个规格并准确知道要构建什么以及如何验证。

第四阶段:实现。 代理按规格执行。不是按模糊的想法,而是按有可测试标准的具体文档。退出条件:代理声称完成并发布了验证证据。

第五阶段:验证。 你(或 QA 代理)确认实现与规格匹配。不是"看起来对不对",而是"是否满足每个验收标准"。退出条件:每个标准都已检查,失败的返回第四阶段。

关键洞察:第 1-3 阶段成本低。中等规模功能需要 10-20 分钟。第四阶段需要实现所需的时间。第五阶段需要 5-10 分钟。跳过第 1-3 阶段并不能节省 10-20 分钟。它的代价是审查、调试和重做方向错误的工作所需的时间。

好的代理规格长什么样

这是一个真实的规格模板。不是用户故事,不是产品需求文档。是一个告诉代理确切要构建什么的工作文档。

## Problem
The filter bar resets when switching workspaces. Users lose their
filter state and have to re-apply filters every time they switch.

## Approach
Persist filter state per-workspace in localStorage. Key the stored
state by workspace database path so filters don't bleed across
workspaces.

## Files to Modify
- lib/local-storage.ts: Add getWorkspaceFilters / setWorkspaceFilters
- components/filter-bar.tsx: Read initial state from localStorage,
  write on every change
- hooks/use-workspace.ts: Trigger filter restore on workspace switch

## Acceptance Criteria
1. Select workspace A, set filters to status=open + type=bug
2. Switch to workspace B. Filters reset to defaults.
3. Switch back to workspace A. Filters restore to status=open + type=bug.
4. Close the browser tab, reopen. Filters for the active workspace
   are still applied.
5. bd list --status=open --type=bug output matches the filtered table.

## Out of Scope
- Server-side filter persistence
- Filter presets / saved filter combinations
- URL-based filter state (query params)

## Test Plan
- Unit test: getWorkspaceFilters returns stored value for matching
  workspace path
- Unit test: setWorkspaceFilters writes correct key format
- Manual test: steps 1-5 from acceptance criteria above

注意这个规格包含什么和不包含什么。它不解释 localStorage 如何工作,Claude Code 知道这些。它不说明为什么我们选择 localStorage 而不是 URL params,那是在头脑风暴阶段发生的。它列出了代理应该触及的每个文件,这意味着如果代理开始修改这个列表之外的文件,那就是警告信号。它包含一个超出范围的部分,防止代理进行镀金。

验收标准是最重要的部分。每一条都是具有可观察结果的具体操作。不是"过滤器应该持久化",那是模糊的。"切换到工作区 A,验证过滤器是 status=open + type=bug"是可测试的。代理可以执行它。QA 审查者可以验证它。

Plan-before-code 模式

这是一个能在大多数实现错误变成代码之前捕获它们的协议:要求代理在写任何东西之前发布其实现计划。

在实践中,这看起来像任务上的结构化评论。在代理打开任何文件之前,它写下确切打算做什么。

PLAN: Persist filter state per-workspace using localStorage.

1. Add workspace-scoped storage helpers to lib/local-storage.ts:
   - getWorkspaceFilters(dbPath: string): FilterState | null
   - setWorkspaceFilters(dbPath: string, filters: FilterState): void
   - Key format: "beadbox:filters:{dbPath}"

2. Update components/filter-bar.tsx:
   - On mount, read stored filters for current workspace
   - On every filter change, write to localStorage
   - Accept databasePath as prop (currently not passed)

3. Update hooks/use-workspace.ts:
   - On workspace switch, emit event that filter-bar listens for
   - Filter-bar reads new workspace's stored filters on event

4. Threading databasePath:
   - filter-bar needs databasePath from the workspace context
   - Will add it to the WorkspaceProvider context value

Files: lib/local-storage.ts, components/filter-bar.tsx,
       hooks/use-workspace.ts, components/workspace-provider.tsx

Test: Unit tests for storage helpers, manual test of all 5
acceptance criteria.

代理生成这个大约需要 30 秒。你读它大约需要 2 分钟。在这 2 分钟内,你可以发现实现后需要 20 分钟才能修复的问题:

  • 代理是否在触及规格之外的文件?(添加 workspace-provider.tsx 不在规格中。这可以吗还是范围蔓延?)
  • 方案合理吗?(为工作区切换使用事件发射器可能过度工程化。更简单的 prop 变更可能就够了。)
  • 是否缺少步骤?(当工作区被删除时清理过期的 localStorage 条目怎么办?)

计划是一个检查点。如果看起来正确,让代理继续。如果看起来不对,纠正计划。无论哪种方式,你投入了 2 分钟而不是 20 分钟。

这就是 Beadbox 要解决的问题。

实时查看整个 agent 队列正在做什么。

Beta 期间免费试用 →

验证不是"能编译"

我在 Claude Code 中看到的最常见失败模式不是代理写了糟糕的代码,而是没有人对照规格验证输出。

代理说"DONE"。开发者浏览 diff,看起来合理,就合并了。两天后有人发现这个功能没有处理验收标准中的边界情况 #3。现在你在生产环境调试,而不是在 5 分钟的验证步骤中捕获它。

验证意味着机械地检查每个验收标准。不是"似乎能用",不是"测试通过"(测试可能是错误的或不完整的)。规格中的每个标准都获得一个具体的检查。

正确的完成报告是这样的:

DONE: Filter bar now persists selected filters across workspace
switches using per-workspace localStorage.

Changes:
- lib/local-storage.ts: Added getWorkspaceFilters/setWorkspaceFilters
  with key format "beadbox:filters:{dbPath}"
- components/filter-bar.tsx: Reads stored filters on mount, writes
  on change. Accepts databasePath prop.
- hooks/use-workspace.ts: Triggers filter restore on workspace switch
  via callback prop.

QA Verification:
1. Open http://localhost:41420, select workspace A
2. Set filters to status=open, type=bug
3. Switch to workspace B via header dropdown
4. Switch back to workspace A
5. Verify filters are still status=open, type=bug
   -> Confirmed: filters restore correctly
6. Close tab, reopen. Filters persist.
   -> Confirmed: localStorage key present, filters applied on mount
7. Run: bd list --status=open --type=bug
   -> Output matches filtered table contents (14 beads)

Acceptance criteria:
- [x] Filters persist across workspace switches (steps 2-5)
- [x] Filters survive browser restart (step 6)
- [x] Filtered view matches bd CLI output (step 7)
- [x] Filters don't bleed between workspaces (step 3: workspace B
      shows defaults)

Unit tests: 3 added (storage read/write/key format). All passing.

Commit: a1b2c3d

这和"DONE: Fixed the filter bar"之间的区别就是 5 分钟 QA 通过和 30 分钟调查之间的区别。DONE 评论中的每个声明都有具体检查支持。每个验收标准都映射到一个验证步骤。审查者确切知道构建了什么、如何验证的、以及如果有问题该看哪里。

Beads 作为规格容器

刚才描述的生命周期需要一个存在的地方。规格、计划评论、实现、完成报告、验证结果。全部附在一个任务上,在一个地方。

这就是 beads 解决的问题。Beads 是一个开源的、本地优先的问题追踪器,专为这个工作流设计。每个"bead"是一个任务,有描述(你的规格)、评论线程(计划和完成报告)、状态(open、in_progress、ready_for_qa、closed)以及优先级、依赖关系和分配等元数据。

以下是使用 bd CLI 的实际规格驱动生命周期:

用规格创建 bead:

bd create --title "Persist filter state across workspace switches" \
  --description "## Problem
The filter bar resets when switching workspaces...

## Acceptance Criteria
1. Select workspace A, set filters...
2. Switch to workspace B..." \
  --type feature --priority p2

代理认领工作并发布计划:

bd update bb-a1b2 --claim --actor eng1
bd comments add bb-a1b2 --author eng1 "PLAN: Persist filter state
per-workspace using localStorage.

1. Add workspace-scoped storage helpers...
2. Update filter-bar component...
3. ..."

代理完成工作并发布完成报告:

bd comments add bb-a1b2 --author eng1 "DONE: Filter bar now persists
selected filters across workspace switches.

QA Verification:
1. Open http://localhost:41420...

Acceptance criteria:
- [x] Filters persist across workspace switches
- [x] Filters survive browser restart
...

Commit: a1b2c3d"

bd update bb-a1b2 --status ready_for_qa

QA 接手并验证:

bd show bb-a1b2  # 读取规格和 DONE 评论
# 执行验证步骤
bd comments add bb-a1b2 --author qa1 "QA PASS: All 5 acceptance
criteria verified. Filters persist, restore, and match bd CLI output."

整个生命周期都在 bead 中。规格是描述。计划是评论。完成报告是评论。QA 结果是评论。六个月后,如果有人问"过滤器持久化是怎么工作的,为什么我们选择了 localStorage 而不是 URL params?",答案就在 bead 的评论线程中。

当你只有一个规格通过这个流水线时,终端和 bd show 就够了。但当你并行运行多个规格时,这个工作流才真正展现其价值。

扩展规格驱动开发

想象真实场景:你有三个 Claude Code 代理,每个实现不同的规格。代理 A 在构建过滤器持久化功能。代理 B 在添加工作区统计的新 API 端点。代理 C 在修复 WebSocket 重连 bug。每个都在规格驱动生命周期的某个阶段。

在终端中,你需要运行 bd list 查看所有活动 bead,然后对每个运行 bd show 检查状态和最新评论。三个并行工作流的快照需要六个命令。乘以五个或十个代理,你花在检查状态上的时间比审查计划还多。

这就是 Beadbox 的用武之地。Beadbox 是一个实时仪表板,显示工作区中每个 bead 的状态。哪些规格是开放的等待代理。哪些有已发布的计划需要你审查。哪些正在进行中。哪些已准备好 QA 验证。当代理通过 bd CLI 写评论和更改状态时,一切实时更新。

规格驱动开发不需要 Beadbox。CLI 处理整个生命周期。但当你并行运行多个规格驱动工作流时,能够一目了然地看到流水线而不是逐个轮询每个代理的状态,会改变你审查计划、解除代理阻塞和发现停滞工作的速度。

Beadbox 在测试期间免费,其运行的 beads CLI 是开源的

与工具无关的永恒真理

无论你用 beads、GitHub Issues、Linear 还是纯文本文件,规格驱动模式之所以有效,是因为它解决了代理运作方式中的一个根本不对称:执行快但判断差。你花在写清晰规格上的每一分钟,都能节省审查错误输出、调试静默假设和重做偏离方向工作的多个分钟。

原则:

  1. 在"开始"之前定义"完成"。 验收标准不是可选的。它们是使验证成为可能的唯一事物。

  2. 计划是检查点,不是官僚主义。 30 秒的计划评论节省 20 分钟的重写。审查计划,而不是代码。

  3. 验证是协议,不是感觉。 "看起来不错"不是验证。将每个验收标准映射到具体检查才是验证。

  4. 规格是唯一的事实来源。 当实现和规格不一致时,实现是错的。这条规则存在是因为代理不会质疑一个糟糕的计划。他们会忠实地执行它,忠实地产出错误的结果。

  5. 范围边界防止漂移。 要修改的文件的明确列表和超出范围部分防止代理"改进"你没有要求改进的东西。

投入很小:为一个实现需要一小时的功能写 10-20 分钟的规格。回报很大:一致的结果、可审查的输出,以及关于构建了什么和为什么的永久记录。

如果你正在构建这样的工作流,请在 GitHub 上给 Beadbox 加星。

Like what you read?

Beadbox is a real-time dashboard for AI agent coordination. Free during the beta.

Share