OpenSpec 实战演练:从“猜数字”到“暗黑模式”的规范驱动开发之旅

前言:

上一节我们介绍了 OpenSpec 的安装与配置,这一节我们将通过两个连贯的迭代案例------从零开发"猜数字游戏" 和增加"暗黑模式" ,完整演示 OpenSpec 的 探索 (Explore) → 提案 (Propose) → 实施 (Implement) → 归档 (Archive) 全流程。

通过这两个案例,你将亲眼见证规范文档如何在迭代中积累,并最终形成可追溯的工程知识。

1. OpenSpec 的 4 阶段工作流

OpenSpec 的工作流可分为 4 个阶段,每个阶段对应一条 OpenSpec 斜杠命令,如表 1 所示。

表 1:OpenSpec 的 4 阶段工作流

阶段 命令 作用
探索 /openspec-explore 了解代码库现状,分析现有能力与约束
提案 /openspec-propose 创建结构化规范文档
实施 /openspec-apply-change 按照 proposal 逐步实现代码,并严格对照规范验证
归档 /openspec-archive-change 将已完成的变更归档,形成可追溯的知识库

OpenSpec 的整体流程如图 1 所示。

图 1:OpenSpec 的整体流程


2. 前置准备:配置项目上下文

在开始实战之前,请确认已按照上一节完成 OpenSpec 的安装与初始化。若尚未初始化,需执行以下命令:

复制代码
mkdir guess-number-game && cd guess-number-game
openspec init

接着编辑 openspec/config.yaml,填入猜数字游戏项目的技术栈信息。示例如下:

复制代码
schema: spec-driven

context: |
  Tech stack: HTML5, CSS3, Vanilla JavaScript
  Storage: localStorage
  No backend, pure frontend application

rules:
  proposal:
    - Always include "## Why" and "## What Changes" sections
  specs:
    - Use Given/When/Then format for scenarios

核心提示: config.yaml 中的 context 字段内容就是实际项目词频,以把 OpenSpec 为 AI 提供固定的上下文。例如,当项目是纯前端应用时,应填写"HTML5+CSS3+Vanilla JavaScript"。如果是 Python 后台项目,则补充本应用 Python 版本和本地使用的框架,如"Python 3.12+FastAPI+SQLite"。

配置完成后,有两种驱动方式可以选择以下两种方案驱动工作流:

  • 方式 1:OpenSpec 斜杠命令(推荐) :在 TRAE 的 AI 对话中输入 /openspec-explore、/openspec-propose、/openspec-apply-change、/openspec-archive-change 等命令,自动触发对应的技能工作流。
  • 方式 2:手动输入指令:在 AI 对话中手动输入"请使用 OpenSpec 的 xxx Skill"指令,灵活控制每一步的操作。

3. 第一阶段:探索 (Explore)

探索阶段的目的是让 AI 深入了解代码库现状,在不写任何代码的前提下,理清现有功能、技术约束和已有规范,为后续的提案奠定坚实基础。

对于是否需要探索,可参考表 2 的判断标准。

表 2:探索阶段的适用场景判断

场景 是否需要探索 说明
全新项目,从零开始 通常不需要 代码为空,没有需要了解的现状
在已有项目基础上迭代 必须进行 需要了解现有实现,避免冲突
接手他人代码进行维护 强烈建议 摸清历史包袱和技术债
修复 Bug 或优化性能 建议进行 需要定位问题根源

对猜数字游戏这个案例而言,由于是一个全新项目,代码库为空,所以可以跳过探索阶段,直接进入提案阶段。


4. 第二阶段:提案 (Propose)

提案阶段是 OpenSpec 工作流的核心。执行 /openspec-propose 命令后,AI 会生成完整的规范化文档结构。

4.1 文档结构解析

  • proposal.md:描述为什么做(背景、目标)和做什么(功能范围、影响的能力)。
  • design.md:描述怎么做(技术方案、架构决策)。
  • specs/ :能力目录(如 specs/game-core/spec.md),每个功能的详细规范(数据模型、场景、验收标准)。
  • tasks.md:将实现过程拆解为可执行的任务列表。

这些文档共同构成了"规范即真理"的事实来源,后续的代码实现必须严格遵循这些文档。

4.2 实战演示:创建"猜数字游戏"

现在我们在一个纯网页应用实现"猜数字游戏"功能。在 TRAE 的 Agent 模式中,新建一个任务(即创建一个会话),输入以下提示词,触发 AI 的推理:

/openspec-propose 请为"猜数字游戏"创建规范文档

AI 执行后,会在 openspec/changes 目录下创建一个名为 guess-number-game 的子目录,并在该子目录下创建多个文件。此文件名称将是变更的任务 ID。在后续的流程中,将通过这个任务 ID 来推进流程。

执行完 /openspec-propose 后,AI 创建的目录和文件如下:

复制代码
openspec/changes/guess-number-game/
├── proposal.md      # 提议文档
├── design.md        # 技术设计文档
├── tasks.md         # 任务清单
└── specs/           # 规范清单
    ├── game-core/   # 游戏核心逻辑规范
    ├── game-state/  # 游戏状态管理规范
    ├── score-storage/ # 存储功能规范
    ├── history-view/  # 历史记录浏览规范
    └── statistics/    # 统计数据展示规范

AI 还会简单列出各个文件的要点,以便于用户审查。例如,在这个项目中,AI 列出的审查信息如下:

1. proposal.md
复制代码
## Why
猜数字游戏是一个经典的互动游戏。通过网页应用形式实现,可以提供便捷的娱乐体验
,该游戏能够锻炼玩家的逻辑推理能力,同时也记录成绩可以让玩家追踪自己的游戏历史和表现。

## What Changes

    创建猜数字游戏网页应用,支持 1~100 范围内的随机数生成
    实现 7 次猜测机会机制,每次猜测后给出"太高"、"太低"或"正确"的反馈
    游戏状态持久化存储到 localStorage
    提供历史记录查看功能
    提供游戏统计数据展示(胜率、平均猜测次数等)

## Capabilities
### New Capabilities

    'game-core': 游戏核心逻辑,包括随机数生成、猜测验证和反馈提示
    'game-state': 游戏状态管理,包括剩余猜测次数、当前目标数字等
    'score-storage': localStorage 存储管理,自动保存游戏记录
    'history-view': 历史记录查看界面,展示过往猜测记录
    'statistics': 统计数据计算和展示

### Modified Capabilities

    -: 无现有功能能力修改

## Impact

    新增 HTML/CSS/JS 文件实现游戏前端业务逻辑
    使用 localStorage 进行数据持久化
    无后端依赖,纯前端实现
2. design.md
复制代码
design.md 将 proposal.md 中的"做什么"转化为"怎么做",它确保了以下事项:

    1.技术选型有依据:每个技术选择都有明确的理由,而不是"因为 AI 默认用了这个"。
    2.架构设计有规划:在写代码之前明确了目录结构、localStorage 存储策略。
    3.关键决策有记录:游戏核心算法(二分查找策略)、localStorage 存储策略、
响应式设计方案等决策被显式记录下来,供后续参考。
    4.验收标准可追溯:每条验收标准都有对应的实现方式和验证方法。

接下来 AI 生成规范,规范有多个文件,位于 specs 目录下,使用的是 Gherkin 格式。规范详细定义了边界条件、异常处理以及用户交互细节,如下所示:

复制代码
# game-core Specification

## Purpose
TODO - created by archiving change guess-number-game. Update Purpose after
 archiving.

## Requirements
### Requirement: Generate random target number
The system SHALL generate a random integer between 1 and 100 (inclusive) as the 
target number when starting a new game.

#### Scenario: Generate target on new game
- **WHEN** user starts a new game
- **THEN** system generates a random integer between 1 and 100
- **AND** the generated number is stored as the target number
3. tasks.md

提案阶段的最后一步,AI 会根据技术方案生成任务列表 tasks.md,具体内容如下:

复制代码
## 项目初始化与基础文件
- [ ] 1.1 创建 index.html 主页面
- [ ] 1.2 创建 styles.css 样式文件
- [ ] 1.3 创建 script.js 脚本逻辑文件

## 实现游戏核心逻辑
- [ ] 2.1 实现随机数生成函数 (1~100)
- [ ] 2.2 实现猜测验证函数 (范围检查、格式检查、重置检查)
- [ ] 2.3 实现猜测比较逻辑 (太高/太低/正确)
- [ ] 2.4 实现游戏状态管理 (目标数字、猜测次数、已猜测序列)

## 实现用户界面
- [ ] 3.1 实现游戏标题和说明区域
- [ ] 3.2 实现剩余次数显示组件
- [ ] 3.3 实现已猜测数字显示区域
- [ ] 3.4 实现输入框和提交按钮
- [ ] 3.5 实现反馈消息区域
- [ ] 3.6 实现游戏结果弹窗 (胜利/失败)
- [ ] 3.7 添加响应式布局适配

## 实现 localStorage 存储
- [ ] 4.1 实现游戏记录保存函数
- [ ] 4.2 实现历史记录加载函数
- [ ] 4.3 实现历史记录显示组件
- [ ] 4.4 实现统计计算逻辑 (胜率、最佳成绩)
- [ ] 4.5 测试浏览器兼容性

## 测试和优化
- [ ] 5.1 测试输入验证逻辑
- [ ] 5.2 测试游戏流程 (胜利/失败路径)
- [ ] 5.3 测试 localStorage 持久化功能
- [ ] 5.4 测试响应式设计
- [ ] 5.5 修复发现的 bug

## 优化和完善
- [ ] 6.1 添加动画效果支持 (渐入渐出)
- [ ] 6.2 添加键盘快捷键
- [ ] 6.3 优化 UI 样式

可以看到,我们只是使用 /openspec-propose 提出了一个需求,AI 就为我们创建了这么多文件。这些文件共同构成了"猜数字游戏"功能的完整事实来源。后续代码必须严格遵循这些文档。这正是规范驱动编程的"规范即真理"执行的体现。


5. 第三阶段:实施 (Implement)

在提案与规范确认无误后,即可进入实施阶段,开始编写代码。

在 TRAE 中输入以下命令,即可启动编码流程:

复制代码
/openspec-apply-change guess-number-game

若不指定具体一个任务变更,则在直接写如下:

复制代码
/openspec-apply-change

执行后,AI 将自动加载当前变更目录(如 openspec/changes/2026-05-11-guess-number-game/)下的所有规范文件,包括 proposal.md、design.md、specs/ 及 tasks.md,并严格按照 tasks.md 中的任务顺序,逐条实现代码。每完成一项任务,AI 会自动将该任务从未完成标记为已完成。

AI 执行 /openspec-apply-change 后,生成或修改后的文件如下:

复制代码
index.html          # 主页面
css/
└── styles.css      # 样式文件
js/
├── game.js         # 游戏核心逻辑
├── storage.js      # 存储管理
└── ui.js           # UI 交互逻辑
openspec/changes/guess-number-game/
├── proposal.md     # 提议文档
├── design.md       # 技术设计文档
├── specs/          # 规范清单
└── tasks.md        # 任务清单 (已全部完成)

在本案例中,最重要的业务逻辑位于 game.js,代码如下:

复制代码
class GuessNumberGame {
    constructor() {
        this.targetNumber = null;
        this.remainingGuesses = 7;
        this.guessHistory = [];
        this.gameStatus = 'playing'; // playing, won, lost
        this.minGuesses = 1;
        this.maxGuesses = 100;
    }

    // 生成随机目标数字
    generateTargetNumber() {
        return Math.floor(Math.random() * (this.maxGuesses - this.minGuesses + 1)) + this.minGuesses;
    }

    // 开始新游戏
    startNewGame() {
        this.targetNumber = this.generateTargetNumber();
        this.remainingGuesses = this.maxGuesses;
        this.guessHistory = [];
        this.gameStatus = 'playing';
        this.saveCurrentGame();
        return this.getGameState();
    }

    // 验证输入
    validateInput(input) {
        const number = parseInt(input, 10);

        if (isNaN(number)) {
            return { valid: false, message: '请输入有效的数字' };
        }

        if (number < this.minGuesses || number > this.maxGuesses) {
            return { valid: false, message: `请输入 ${this.minGuesses}-${this.maxGuesses} 之间的数字` };
        }

        return { valid: true, number };
    }

    // 进行猜测
    makeGuess(input) {
        if (this.gameStatus !== 'playing') {
            return {
                success: false,
                message: '游戏已结束,请开始新游戏',
                gameOver: true,
                won: false
            };
        }

        const validation = this.validateInput(input);
        if (!validation.valid) {
            return {
                success: false,
                message: validation.message,
                gameOver: false,
                won: false
            };
        }

        const guess = validation.number;
        this.remainingGuesses--;

        let result = '';
        let isCorrect = false;

        if (guess === this.targetNumber) {
            result = 'correct';
            isCorrect = true;
            this.gameStatus = 'won';
        } else if (guess > this.targetNumber) {
            result = 'too-high';
        } else {
            result = 'too-low';
        }

        this.guessHistory.push({
            number: guess,
            result: result
        });

        // 检查是否用完所有机会
        if (!isCorrect && this.remainingGuesses === 0) {
            this.gameStatus = 'lost';
        }

        this.saveCurrentGame();

        // 如果游戏结束,保存记录
        if (this.gameStatus !== 'playing') {
            this.saveGameRecord();
        }

        return {
            success: true,
            message: this.getFeedbackMessage(result, isCorrect),
            result: result,
            remainingGuesses: this.remainingGuesses,
            gameOver: this.gameStatus !== 'playing',
            won: this.gameStatus === 'won',
            guessHistory: this.guessHistory,
            targetNumber: this.gameStatus !== 'playing' ? this.targetNumber : null
        };
    }

    // 获取反馈消息
    getFeedbackMessage(result, isCorrect) {
        if (isCorrect) {
            return `恭喜!你猜对了!答案就是 ${this.targetNumber}!`;
        }

        if (this.gameStatus === 'lost') {
            return ` 游戏结束!正确答案是 ${this.targetNumber},再试一次吧!`;
        }

        if (result === 'too-high') {
            return `太高了!你还有 ${this.remainingGuesses} 次机会`;
        }

        return `太低了!你还有 ${this.remainingGuesses} 次机会`;
    }

    // 获取游戏状态
    getGameState() {
        return {
            targetNumber: this.gameStatus !== 'playing' ? this.targetNumber : null,
            remainingGuesses: this.remainingGuesses,
            guessHistory: this.guessHistory,
            gameStatus: this.gameStatus,
            maxGuesses: this.maxGuesses
        };
    }

    // 保存当前游戏状态到 localStorage
    saveCurrentGame() {
        try {
            const gameState = {
                targetNumber: this.targetNumber,
                remainingGuesses: this.remainingGuesses,
                guessHistory: this.guessHistory,
                gameStatus: this.gameStatus
            };
            localStorage.setItem('guessNumberGame_current', JSON.stringify(gameState));
        } catch (e) {
            console.error('保存游戏状态失败:', e);
        }
    }

    // 从 localStorage 恢复游戏状态
    loadCurrentGame() {
        try {
            const saved = localStorage.getItem('guessNumberGame_current');
            if (saved) {
                const gameState = JSON.parse(saved);
                this.targetNumber = gameState.targetNumber;
                this.remainingGuesses = gameState.remainingGuesses;
                this.guessHistory = gameState.guessHistory;
                this.gameStatus = gameState.gameStatus;
                return true;
            }
        } catch (e) {
            console.error('加载游戏状态失败:', e);
        }
        return false;
    }

    // 清除当前游戏状态
    clearCurrentGame() {
        try {
            localStorage.removeItem('guessNumberGame_current');
        } catch (e) {
            console.error('清除游戏状态失败:', e);
        }
    }

    // 保存游戏记录
    saveGameRecord() {
        const record = {
            timestamp: Date.now(),
            targetNumber: this.targetNumber,
            guesses: this.guessHistory.length,
            isWon: this.gameStatus === 'won',
            guessHistory: this.guessHistory
        };

        try {
            const records = StorageManager.getRecords();
            records.unshift(record);
            StorageManager.saveRecords(records);
        } catch (e) {
            console.error('保存游戏记录失败:', e);
        }
    }
}

5.1 代码与规范的严格对齐

AI 执行 /openspec-apply-change 后,生成的代码必须逐条对照规范文档中的验收标准进行验证。表 3 列出了针对核心验收标准的验证要点及验证方法。

表 3:验证要点及验证方法

验证要点 验证方法
随机数生成 进行多次游戏,确认目标数字在 1~100 范围内且随机
输入验证 测试 0、101、非数字、空输入、重复数字
反馈显示 猜大、猜小、猜对,分别验证提示文字
次数限制 连续猜错 7 次后检查游戏是否结束
历史记录 刷新页面后检查记录是否存在
统计数据 验证胜率计算公式是否正确

5.2 界面效果

实施阶段确保生成的代码与规范严格对齐,具体表现在以下几方面:

  1. 游戏逻辑与规范一致:随机数范围、猜测次数限制、反馈规则与规范文档完全一致。
  2. 存储机制与规范一致 :localStorage 键名、数据结构与规范文档完全一致。
  3. 验收标准可验证:每条验收标准都有对应的代码实现和验证方法。
  4. 技术约束被遵守:纯前端实现,无后端依赖,响应式设计。

6. 第四阶段:归档 (Archive)

在功能实现并通过验收后,我们需要将当前的变更从"活跃状态"转变为"历史资产"。

6.1 执行归档命令

在 TRAE 中输入如下命令,将当前变更从 openspec/changes/ 移动到 openspec/changes/archive/,完成归档:

复制代码
# 在 TRAE 中输入
/openspec-archive-change guess-number-game

归档不仅仅是"移动文件",它标志着该功能的规范文档已进入只读状态,除非有新的变更提案,否则不再修改。归档后的文件会纳入版本管理,成为项目历史的一部分。手动执行下面的命令,将规范文档纳入版本管理,和代码一起同源管理:

复制代码
git add openspec/changes/archive/2026-05-11-guess-number-game/
git commit -m "feat: archive guess-number-game change"

6.2 归档的价值

归档赋予规范文档以下 3 个重要属性:

  1. 可追溯性:每个功能的完整需求、设计和实现决策都有据可查。
  2. 可复用性:遇到类似功能时,直接参考已归档的规范,无须从头设计。
  3. 可审计性:规范文档纳入版本管理,团队可以看到每个变更的完整历史。

7. 进阶实战:增加暗黑模式

接下来为猜数字游戏增加暗黑模式,演示 OpenSpec 如何在已有功能基础上新增能力。因为本次变更是增加新功能,所以使用 /openspec-explore 进行探索。

7.1 探索:了解现有实现

在 TRAE 中,通过以下提示词即可触发探索。AI 会探索当前的项目目录、功能,并给出实现建议。

复制代码
/openspec-explore 为猜数字游戏增加暗黑模式

AI 会基于对现有代码的分析,给出相应的设计方案。例如,在本案例中,AI 给出了下列建议:

当前代码结构分析
复制代码
├── index.html          # 主页面(无主题切换按钮)
├── css/styles.css      # 样式文件(纯亮色主题)
├── js/game.js          # 游戏核心逻辑
├── js/storage.js       # 存储管理
└── js/ui.js            # UI 交互逻辑
暗黑模式实现方案

方案:CSS 变量 + JavaScript 切换

核心思路 :使用 CSS 自定义属性 (CSS Variables) 定义主题颜色,通过 JavaScript 切换 'dark' class 来改变主题。

需要修改的文件

文件 修改内容
index.html 添加主题切换按钮
css/styles.css 添加 CSS 变量和暗黑模式样式
js/ui.js 添加主题切换逻辑和持久化

配色方案对比

元素 亮色模式 暗黑模式
背景渐变 #667eea → #764ba2 #1a1a2e → #16213e
容器背景 white #1f1f3a
游戏区域 #f8f9fa #2a2a4a
文字颜色 #333 #e0e0e0
边框颜色 #eee #3a3a5a

实现步骤规划

  1. 修改 CSS :定义 CSS 变量,添加 .dark 类样式
  2. 修改 HTML:添加主题切换按钮(太阳/月亮图标)
  3. 修改 JS:添加切换逻辑,保存到 localStorage
  4. 处理边界情况:页面加载时获取用户偏好

潜在风险与考虑

  • 图标颜色:FontAwesome 图标在暗黑模式下可能需要调整
  • 动画效果:确保动画在两种模式下都正常显示
  • 兼容性:CSS 变量在现代浏览器中支持良好
  • 性能:切换主题是纯 CSS 操作,性能开销小

在探索阶段可以和 AI 反复对话,确定方案。在方案确定好后,可以直接使用 /openspec-propose 进行提案。

7.2 提案:定义暗黑模式规范

在 TRAE 中使用以下提示词进行提案,提案会自动使用探索阶段确定的方案。

复制代码
/openspec-propose 为猜数字游戏增加暗黑模式

TRAE 执行完提案后,会在 openspec/changes 目录下创建一个名为 add-dark-mode 的子目录,其中 add-dark-mode 就是本次变更的任务 ID。

和前面的流程一样,提案会创建相应的 proposal.md、design.md、规范文档和 tasks.md,这里不再赘述。

7.3 归档:形成知识库

在完成了功能开发并归档后可以看到,openspec/changes/archive/ 目录下保存了已完成变更的完整规范文档:

  1. 2026-05-11-guess-number-game/:猜数字游戏的原始实现。
  2. 2026-05-11-add-dark-mode/:暗黑模式的功能迭代。

这个目录结构本身就是项目的历史记录。当有团队新成员加入时,其只需浏览 openspec/changes/archive/ 目录,就能了解项目的完整演进历史------每个功能是什么时候加的、为什么加、设计了什么、是如何实现的,这些信息都在规范文档中,而不是散落在 Git 提交历史和办公聊天工具中。


8. 其他操作与总结

除了上述核心步骤,OpenSpec 还提供以下辅助操作:

  1. 规范审查:在规范文档编写完成后,提交给团队成员审查,重点检查功能范围是否清晰、数据模型是否合理、验收标准是否可验证。
  2. 规范复用:遇到类似功能时,直接参考已归档的规范文档,修改功能范围和数据模型即可快速复用。

此外,4 个阶段可根据任务复杂度灵活裁剪,具体建议如表 4 所示。

表 4:不同复杂度任务的工作流裁剪建议

任务复杂度 推荐工作流
简单任务(修复 Bug、小改动) 直接实施,可跳过探索和提案
中等任务(新增单个功能) 提案→实施→归档
功能迭代(在已有功能基础上新增功能) 探索→提案→实施→归档
复杂任务(涉及多个功能、架构变更) 完整 4 阶段

OpenSpec 的四个阶段构成了一个从"模糊需求"到"可追溯规范"的完整循环。探索阶段让 AI 了解现状,提案阶段让 AI 明确目标,实施阶段让 AI 按规范行动,归档阶段将这一切固化为工程资产。这正是规范驱动编程的核心价值------通过结构化的流程保障质量下限,并将每次迭代的知识转化为团队可传承的资产。

相关推荐
码农小旋风2 小时前
SDD规范驱动开发:AI时代的软件工程新范式
人工智能·驱动开发·软件工程
幼儿园蛋小黑7 小时前
DHT11 温湿度传感器驱动与 I2C 子系统驱动开发详解
驱动开发
zbyyd8 小时前
Linux 驱动开发实战:从手写 LED 字符设备驱动到设备树
linux·驱动开发
昌原的儿子LEO8 小时前
嵌入式 Linux I2C 驱动开发核心知识总结:设备树与 platform 架构全解析
linux·驱动开发·架构
gwf2168 小时前
常见驱动问题定位与解决:设备识别、性能不达预期、CQ错误码 —— 资深RDMA驱动调试排错指南
驱动开发·linux内核·性能调优·rdma·故障排查·mlx5·irdma
万物智能信息科技9 小时前
气体传感器MQ-2芯片驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
驱动开发·嵌入式硬件·华为·开源·harmonyos
硅基手札12 小时前
【I2C 技术系列 11】I2C Linux 驱动框架
linux·驱动开发·stm32·单片机·嵌入式硬件·mcu·计算机外设
碧海银沙音频科技研究院12 小时前
炬芯ATS3089智能手表烧录方法
人工智能·驱动开发·嵌入式硬件·算法
wuyk5551 天前
嵌入式驱动开发:量产级工程化实战(第9篇)——低功耗下的通信可靠性
c语言·驱动开发·stm32·单片机·嵌入式硬件