AI代理工程化框架:六组件机制驱动,解决回归与失忆难题
1. 项目概述:构建一个能“扛事”的AI代理工程化框架
如果你和我一样,尝试过用Claude Code、GPT Engineer这类AI编码代理去构建一个正经的、需要迭代数周的真实项目,那你大概率经历过这种场景:昨天还好好的功能,今天AI代理跑完一个会话后,莫名其妙就坏了;或者,你让它加个新功能,它吭哧吭哧写半天代码,最后你发现它完全理解错了需求,做出来的东西根本不能用。更让人头疼的是,项目稍微复杂点,功能之间开始有依赖、共享代码,AI代理改东墙塌西墙的“回归问题”就会频繁出现,让你不得不花大量时间去“救火”,所谓的“自动化”反而成了负担。
这正是我在用Claude Code构建Skilldeck这个桌面应用时,花了三周时间踩遍的坑。我发现,大多数关于“AI代理工程化”的讨论,都停留在四个基础组件上:系统提示词、任务列表、进度文件和几个测试。这套组合拳能让AI开始干活,但远远不足以让一个项目在数周的自主开发、需求演变和功能交织中保持连贯性和可靠性。它缺乏一种机制,来确保AI不仅是在“写代码”,更是在“正确地构建软件”。
于是,我总结并实践了一套包含六个组件的工程化框架,我称之为“六件套缰绳”。前四个是“入场券”,业界基本已有共识;而后两个,才是区分“理论上可行”与“能在真实、演进中的代码库中存活下来”的关键。这套框架的核心思想,是把软件开发中那些隐性的、依赖人类工程师经验和纪律的实践——比如回归测试、变更影响分析、工作项管理——变成显性的、可被AI代理理解和执行的机制。接下来,我将为你彻底拆解这六个组件,分享每一个的设计逻辑、具体实现,以及我在实践中积累的血泪教训。
2. 核心设计思路:从“指令驱动”到“机制驱动”的范式转变
在深入细节之前,我们必须先统一思想:为什么传统的“写份详细说明书”给AI代理的方式会失败?答案在于软件工程的复杂性本质上是状态空间的爆炸。一个项目有无数种可能的状态(代码组合、配置、环境),而人类的指令是静态的、有限的,无法覆盖所有边界情况。AI代理基于这些指令行动时,就像在一个没有地图和指南针的迷宫里乱撞。
2.1 识别三类核心失败模式
在Skilldeck项目初期,我观察到了三种高频的失败模式,这也是本框架要解决的核心问题:
- “宣告胜利”式失败:AI代理写完了代码,甚至通过了它自己写的单元测试,就认为功能完成了。但实际上,功能从用户角度看根本不可用。比如,它可能写了一个“创建文件”的API,但前端按钮根本没绑定这个事件;或者文件创建了,但路径错了。这种失败源于AI缺乏对“完成”的客观、端到端的定义。
- “上下文失忆”式失败:AI代理每次会话都是独立的。新会话开始时,它只能通过读取文件来重建项目状态。如果进度记录不准确或过时,它就会基于错误的前提开始工作,比如试图去修复一个并不存在的Bug,或者重复构建已经完成的功能。
- “回归”式失败:这是最隐蔽、破坏力最大的一种。AI代理成功构建了功能A,但在构建功能B时,无意中修改了功能A所依赖的共享模块(比如一个全局状态管理store或一个工具函数),导致功能A悄然失效。由于没有自动化的影响分析,这个Bug可能直到很久以后才会被发现,排查成本极高。
2.2 机制优于指令:构建自洽的反馈闭环
基于以上问题,我的设计思路从“给AI更聪明的指令”转变为“为AI设计更健壮的机制”。指令告诉AI“做什么”,而机制定义了“如何验证做得对”、“如何安全地做”以及“做错了如何发现”。一个好的机制应该形成一个闭环:AI的行动会产生可观测的结果,这个结果会被自动验证,验证的结论会反馈回来指导AI的下一步行动,或阻止其进行破坏性提交。
这个六组件框架,就是六个相互咬合的齿轮,共同构成了这个闭环:
- 地面实况与验证层构成了“定义与检验”环,解决了“宣告胜利”问题。
- 记忆系统与启动仪式构成了“状态与上下文”环,解决了“上下文失忆”问题。
- 系统契约与功能准入协议构成了“影响与变更”环,解决了“回归”与“范围蔓延”问题。
接下来,我们逐一拆解每个齿轮的构造与运作原理。
3. 组件一:地面实况——项目功能的唯一可信源
第一个组件,我称之为“地面实况”。它的核心文件是一个JSON文件,例如feature_list.json。请务必理解,这不是需求文档,也不是产品待办列表。它是经过验证的项目现实的权威记录。
关键区别:文档描述的是“意图”,而地面实况反映的是“已验证的现实”。AI可以轻易地生成或修改描述性文档,但它不能单方面决定一个功能是否“通过”。通过与否,必须由一个客观的、自动化的机制来裁决。
3.1 数据结构设计:超越简单的待办清单
一个典型的功能条目远不止“名称”和“描述”。以下是我在Skilldeck项目中使用的结构,每个字段都有其战略目的:
{ "id": "F005", "name": "创建新技能", "description": "用户可以在库视图中创建新技能。操作完成后,对应的.md文件应保存在磁盘上。", "steps": [ "点击‘新建技能’按钮", "验证新技能出现在列表中", "验证对应的.md文件在磁盘上被创建" ], "touches": ["store.skills", "ipc.skills", "preload"], "depends_on": ["F004"], "passes": false, "notes": "" }id&name&description: 基础标识,用于人类和AI快速理解。steps:这是黄金字段。它用可验证的、端到端的用户操作步骤来定义“完成”。每一步都应该是Playwright这类E2E测试工具能够执行和断言的动作。这强制了功能定义的客观性和可测试性。touches:这是大多数框架缺失的字段。它列出这个功能所依赖或影响的共享代码表面。例如,一个“创建技能”功能可能会触及状态管理库(store)、进程间通信(ipc)模块和Electron的预加载脚本(preload)。这个字段是后续实现“回归门禁”的基石。没有它,你就无法知道修改一个文件会影响哪些功能。depends_on: 功能依赖关系。AI在每次循环开始时检查这个列表,如果依赖的功能没有标记为passes: true,它就会跳过当前功能,寻找下一个可构建的。这防止了AI尝试在“用户登录”功能完成之前去构建“用户仪表盘”。passes: 核心状态字段。只有false和true两种状态。初始化和修改必须极其谨慎。notes: 用于记录验证时间、测试结果摘要或任何临时备注。
3.2 安全操作指南:如何与地面实况交互
地面实况文件是神圣的,尤其是passes字段。绝对不能让AI通过简单的字符串查找和替换来更新它。JSON对空格和格式敏感,字符串匹配极易出错,可能导致文件损坏。
正确做法是使用一个Node.js脚本(或任何你项目的主语言)来原子化地更新状态:
# 这是一个安全的更新命令示例 node -e " const fs = require('fs'); const featureList = JSON.parse(fs.readFileSync('feature_list.json', 'utf8')); const targetFeature = featureList.features.find(f => f.id === 'F005'); if (targetFeature) { targetFeature.passes = true; targetFeature.notes = '于2023-10-27通过E2E测试验证'; fs.writeFileSync('feature_list.json', JSON.stringify(featureList, null, 2)); } "你必须建立一条铁律:只有自动化验证流程(如CI/CD流水线或本地验证脚本)在成功运行后,才有权将passes字段从false翻转为true。AI代理本身、甚至开发者手动操作,都不应该直接修改这个字段。这确保了状态变更的权威性。
4. 组件二:记忆系统——跨越会话的连续性保障
AI代理没有记忆。每次启动新会话,它都像一张白纸。记忆系统(通常是一个文本文件,如claude-progress.txt)的唯一使命,就是在会话之间传递准确的、结构化的项目上下文。
4.1 记忆文件的双重职责
- 会话启动时的定位器:告诉AI“我们上次干到哪了”、“项目现在健康吗”、“接下来该做什么”。这避免了AI每次都要重新分析整个代码库来推断状态(这既慢又不准)。
- 会话结束时的记录仪:强制AI在每次会话结束时,必须格式化地记录下本次的成果、状态和后续建议。这是给未来会话(以及监督项目的你)的一份宝贵日志。
4.2 会话模板:强制结构化记录
一个松散的、散文式的记录是没用的。你必须定义一个模板,强制AI按字段填充。以下是我使用的模板:
### 会话 12 — 实现技能搜索功能 (2023-10-27) **发生了什么:** - 完成了搜索输入框的UI组件(`SearchBar.vue`)。 - 实现了基于技能名称和标签的过滤逻辑(`useSkillSearch`组合式函数)。 - 将搜索状态集成到主技能Store中。 **已完成的功能:** - F009 (技能搜索) **尝试过但未完成的功能:** - 无 **当前应用状态:** - 应用可正常编译启动。 - 所有现有E2E测试通过(共8个)。 - 单元测试覆盖率保持92%。 **下次会话应进行:** 1. 开始实现F010(技能标签管理)。 2. 优先构建标签选择器UI组件。 **阻塞项:** - 无。关键字段解读:
- 发生了什么:具体的技术实现摘要,而非“写了些代码”。
- 已完成的功能:必须引用
feature_list.json中的ID,建立直接关联。 - 当前应用状态:明确回答“能编译吗?”、“测试能过吗?”这两个核心健康度问题。
- 阻塞项:如果会话因三次尝试失败而终止,这里必须包含具体的错误日志、三次尝试的不同方法、以及AI对根本原因的假设。模糊的“遇到了错误”是在浪费你的调试时间。
4.3 将写入与提交绑定
你必须建立一个机制:只有在成功将本次会话记录写入进度文件后,才允许AI执行git commit。可以在你的自动化脚本或AI的指令中明确这一点。这确保了记忆的连续性不会被意外打断。如果一次会话异常结束没有记录,下一次会话就必须从“恢复模式”开始,这增加了复杂性。
5. 组件三:启动仪式——确保每次会话始于清洁状态
启动仪式是一个在每次AI会话开始时自动运行的脚本(如init.sh或init.py)。它的哲学是:绝不假设环境是好的,而是主动验证并修复基础假设。
一个残缺的启动仪式只会检查Node.js是否存在。而一个健壮的启动仪式,需要检查一连串可能出错的环节。
5.1 启动仪式的检查清单
#!/bin/bash # init.sh set -e # 遇到任何错误立即退出 echo "=== 启动环境检查 ===" # 1. 验证工作目录正确 if [ ! -f "CLAUDE.md" ]; then echo "❌ 错误:未在项目根目录找到CLAUDE.md文件。请确保在正确的目录下启动。" exit 1 fi # 2. 验证必需工具链 echo "检查Node.js..." node --version > /dev/null 2>&1 || { echo "❌ Node.js未安装或不在PATH中"; exit 1; } echo "检查npm..." npm --version > /dev/null 2>&1 || { echo "❌ npm未安装"; exit 1; } # 3. 确保Git仓库已初始化 if [ ! -d ".git" ]; then echo "未发现Git仓库,正在初始化..." git init git add . git commit -m "harness: 初始化项目仓库" fi # 4. 检查并处理未提交的更改(至关重要!) if ! git diff --quiet; then echo "⚠️ 警告:发现从上一次会话遗留的未提交更改。" echo "这可能意味着上一次会话未正常完成。" echo "请决定:" echo " 1. 如果这些更改属于一个已完成且测试通过的功能,请先提交它们。" echo " 2. 如果这些更改是半成品或已损坏,建议执行 'git checkout .' 还原。" echo "程序暂停。请在处理完未提交更改后重新运行本脚本。" exit 1 fi # 5. 同步远程仓库(可选但推荐) echo "尝试从远程仓库拉取最新更改..." git pull --rebase origin main 2>/dev/null || echo "提示:远程拉取失败或未设置远程仓库,继续本地开发。" # 6. 报告项目状态 echo "---" echo "项目状态概览:" node -e " const fs = require('fs'); const features = JSON.parse(fs.readFileSync('./feature_list.json')).features; const passed = features.filter(f => f.passes).length; const total = features.length; console.log(\`✅ 通过的功能: \${passed} / \${total}\`); " # 7. 提示下一个待办功能 node -e " const fs = require('fs'); const features = JSON.parse(fs.readFileSync('./feature_list.json')).features; const nextFeature = features.find(f => !f.passes && f.depends_on.every(depId => features.find(f => f.id === depId)?.passes)); if (nextFeature) { console.log(\`🎯 下一个推荐功能: \${nextFeature.id} - \${nextFeature.name}\`); console.log(\` 描述: \${nextFeature.description}\`); } else { console.log('🎉 所有功能已完成,或存在未满足的依赖关系。请检查feature_list.json。'); } " echo "=== 环境检查通过,可以开始工作 ==="第4步“未提交更改检查”是大多数框架遗漏的杀手级特性。想象一下:上次AI会话构建了一半功能,然后崩溃了,留下了一堆半成品代码。如果没有这个检查,新会话会把这些半成品代码当作新的起点,导致错误累积,问题会像滚雪球一样越来越大。强制AI(和你)在开始时面对这个状态,并做出明确决定(提交或还原),是保持代码库清洁的关键。
启动仪式还将关键信息(通过率、下一个功能)直接打印出来,让AI在消耗宝贵的上下文Token去分析文件之前,就掌握了核心上下文。
6. 组件四:验证层——功能完成的终极裁判
验证层是你的“事实核查部门”。它的唯一标准是:从最终用户的角度看,这个功能是否真的工作了?这意味着它必须是端到端(E2E)测试,而不是单元测试或类型检查。
我强烈推荐使用Playwright或Cypress这类现代E2E测试框架。它们可以启动真实的应用(无论是Web应用还是像Electron这样的桌面应用),模拟用户点击、输入,并断言UI和系统状态的变化。
6.1 编写不可欺骗的E2E测试
一个功能的验证测试必须覆盖其steps字段中定义的每一步。测试应该同时验证UI状态和副作用(如文件系统、网络请求)。
// verify.spec.ts import { test, expect } from '@playwright/test'; import fs from 'fs'; import path from 'path'; const LIBRARY_DIR = path.join(__dirname, '../user-data/library'); // 测试前清理环境 function cleanSkilldeck() { if (fs.existsSync(LIBRARY_DIR)) { const files = fs.readdirSync(LIBRARY_DIR); for (const file of files) { if (file.endsWith('.md')) { fs.unlinkSync(path.join(LIBRARY_DIR, file)); } } } } test('F005 - 创建新技能', async ({ page }) => { // 1. 清理旧数据,确保测试从一个干净的状态开始 cleanSkilldeck(); // 2. 启动应用(假设你的测试配置能启动Electron或开发服务器) await page.goto('http://localhost:3000'); // 或使用Playwright的Electron支持 // 3. 执行用户操作步骤 await page.click('[data-testid="new-skill-btn"]'); // 4. 断言UI状态:新技能项应该出现在列表中 await page.waitForSelector('[data-testid="skill-item"]'); const skillItems = await page.locator('[data-testid="skill-item"]').count(); expect(skillItems).toBeGreaterThan(0); // 5. 断言副作用(文件系统):对应的.md文件应该被创建 // 这是“地面实况”的真正验证点! const files = fs.readdirSync(LIBRARY_DIR).filter(f => f.endsWith('.md')); expect(files.length).toBeGreaterThan(0); // 可选:进一步验证文件内容或名称 const newFileName = files[0]; const fileContent = fs.readFileSync(path.join(LIBRARY_DIR, newFileName), 'utf8'); expect(fileContent).toContain('# '); // 简单的格式检查 });关键实践:
>// system-contract.json - 不变量部分 { "invariants": [ { "id": "INV-001", "description": "config.json 必须是符合特定结构的有效JSON", "check": "node scripts/check-invariants/valid-config.js", "triggers": "always" }, { "id": "INV-002", "description": "所有导出的React组件必须使用 memo 或 forwardRef 包裹", "check": "node scripts/check-invariants/react-memo-check.js", "triggers": "on_commit" }, { "id": "INV-003", "description": "不允许存在未处理的Promise拒绝(全局监听)", "check": "node scripts/check-invariants/no-unhandled-rejection.js", "triggers": "always" }, { "id": "INV-004", "description": "IPC通道不能重复注册", "check": "node scripts/check-invariants/no-duplicate-ipc.js", "triggers": "always" } ] }每个不变量检查都是一个独立的Node脚本,返回0表示成功,非0表示失败。
triggers字段决定何时运行:always: 在每次提交前必须运行。on_commit: 在提交时运行,但失败只警告,不阻塞(视项目严格程度而定)。
这些检查能捕获那些单元测试覆盖不到,但一旦违反就会导致系统崩溃的底层问题。例如,
INV-004可以防止在Electron应用中,两个不同的模块试图注册同一个IPC通道名,从而导致不可预测的行为。7.2 代码表面映射:精准的回归测试定位器
这是系统契约的“魔法”部分。它定义了项目中那些被多个功能共享的“代码表面”,并记录了每个功能依赖哪些表面。
// system-contract.json - 表面映射部分 { "surfaces": { "store.skills": { "files": ["src/store/skillsStore.ts"], "affected_features": ["F004", "F005", "F006", "F007", "F008", "F009", "F010"] }, "preload": { "files": ["electron/preload.ts"], "affected_features": ["F004", "F005", "F006", "F007", "F008", "F011", "F012", "F013", "F014"] }, "utils.formatters": { "files": ["src/utils/dateFormatter.ts", "src/utils/textSanitizer.ts"], "affected_features": ["F002", "F003", "F015"] } } }它是如何工作的?
- 当AI完成一个功能(比如F011)并准备提交时,一个脚本(如
get-regression-tests.js)会分析git diff,找出本次提交修改了哪些文件。 - 脚本将这些文件与
surfaces映射进行匹配。例如,如果修改了electron/preload.ts,它就属于preload表面。 - 脚本找出所有依赖
preload表面的功能ID(F004, F005, ..., F014)。 - 脚本输出一个Playwright的
--grep参数,用于只运行这些可能受影响的功能的E2E测试。
# 自动化回归门禁流程 # 1. 运行新功能自身的测试 npx playwright test verify.spec.ts --grep "F011" # 2. 运行所有“总是检查”的不变量 node scripts/check-invariants.js --trigger always # 3. 计算并运行回归测试集 AFFECTED_FEATURES=$(node scripts/get-regression-tests.js F011) if [ -n "$AFFECTED_FEATURES" ]; then npx playwright test verify.spec.ts --grep "$AFFECTED_FEATURES" fi # 4. 只有以上全部通过,才更新状态并提交 node -e "...(更新F011.passes为true的脚本)..." git add . git commit -m "feat(F011): 实现项目注册功能"这个机制的精妙之处在于:它不会运行全部31个测试(假设你有31个功能),也不会天真地只运行新功能的测试。它精准地运行那些可能被本次修改影响到的功能的测试。这就在安全性和效率之间取得了绝佳的平衡。我在Skilldeck项目中遇到的搜索功能回归问题,如果当时有这个机制,会在修改
preload.ts后立即运行F009(搜索)的测试,从而在提交前就发现并修复问题。8. 组件六:功能准入协议——控制需求蔓延的守门员
最后一个组件是关于“流程”的。它规定了新的功能需求如何被正式地、安全地引入到系统中。没有这个协议,你和AI在聊天中随口一提的“加个XX功能吧”,就会绕过整个缰绳系统:没有明确的验收步骤(
steps),没有记录代码影响面(touches),自然也就没有回归测试的保护。这个功能就算做出来,也是一个“黑户”,是未来维护的噩梦。功能准入协议通常作为一条明确的规则写在给AI的指令文件(如
CLAUDE.md)中。当AI接收到一个新的功能请求时,它必须遵循以下五步流程:8.1 五步准入流程
- 检查:AI首先要在
feature_list.json中搜索是否已存在类似功能。这是为了避免重复建设或功能冗余。 - 起草:AI需要创建一个完整的功能草案条目,包含所有必要字段:
id: 按序列生成。name&description: 清晰的功能名称和描述。steps:至少4个具体的、端到端的、可被Playwright测试验证的用户操作步骤。这是将模糊需求转化为可验证标准的关键。touches: AI需要分析这个功能会涉及哪些现有代码模块,并列出对应的“表面”键名。如果涉及新模块,需要注明。depends_on: 分析功能依赖关系。passes: 初始为false。
- 确认:AI必须将起草的完整条目展示给你(人类),并明确询问:“我计划添加这个功能。草案如下:[展示条目]。这符合您的期望吗?如果确认,我将正式注册并开始构建。”必须等待你的明确确认(如“确认”或“是的”),才能继续。这一步至关重要,它能捕获两类问题:
- 范围误解:你说“按标签搜索”,AI可能理解成要做一个带过滤面板的复杂搜索系统。确认步骤让你有机会纠正。
- 不可实现的规格:草案可能要求一个尚未存在的基础设施。确认步骤促使你们提前讨论可行性。
- 注册:获得确认后,AI需要原子化地更新两个文件:
- 将新功能条目写入
feature_list.json。 - 如果
touches中提到了新的代码表面,需要将其添加到system-contract.json的surfaces映射中。 - 写入后,必须验证两个JSON文件格式有效。
- 将新功能条目写入
- 排序:如果你说“添加这个功能并继续工作”,AI必须先完成当前正在构建的功能,然后再开始新的。绝不能丢下半成品去开新坑。这保证了工作的原子性和可追踪性。
8.2 为什么确认步骤不是形式主义
两分钟的确认对话,可以避免未来两小时甚至两天的错误开发。它强制在编码开始之前,就对“完成标准”达成精确共识。这本质上是将敏捷开发中的“需求细化”环节,以一种轻量级但强制的方式,嵌入到了与AI协作的流程中。
9. 整合运作:构建可靠的自主开发循环
当这六个组件全部就位并整合后,AI代理的工作循环就从一种“探索性编程”转变为一种“受控的工程化构建”。整个流程变得机械、可预测且安全。
9.1 完整的自主循环
循环开始: 1. 执行启动仪式 (`init.sh`)。确保环境干净,状态已知。 2. 读取记忆文件 (`claude-progress.txt`)。了解上次进度和当前上下文。 3. 查询地面实况 (`feature_list.json`),找到下一个 `passes=false` 且所有依赖都已满足的功能。如果没有,写入最终会话记录并停止。 4. 实现该功能。 5. 运行该功能的专属E2E测试 (`verify.spec.ts --grep FXXX`)。如果失败超过3次,则写入阻塞记录到记忆文件并停止循环(等待人类介入)。 6. 运行系统契约中的不变量检查 (`check-invariants.js`)。如果失败,必须先修复这些底层问题才能继续。 7. 运行回归门禁:根据本次修改的文件,计算出受影响的功能,并运行这些功能的E2E测试。如果失败,必须先修复回归问题才能继续。 8. 只有第5、6、7步全部通过,才在地面实况中将该功能标记为 `passes=true`,更新记忆文件,并执行Git提交。 9. 回到步骤1,开始下一个循环。这个循环定义了三种明确的停止条件:
- 成功完成:所有功能都
passes=true。 - 阻塞:某个功能尝试三次仍无法通过自身测试。
- 违反契约:不变量检查或回归测试失败,且AI无法在合理尝试内修复。
这样的设计,目标不是创造一个永不失败的AI——那是不可能的。目标是创造一个失败模式清晰、问题能被立即发现且不会累积的AI开发伙伴。它的失败是“安全的失败”,不会把项目拖入无法理解的混乱状态。
10. 实操部署与避坑指南
理论很美好,但落地到具体项目时,你会遇到各种细节问题。以下是我从Skilldeck项目实践中总结出的关键实操要点和避坑经验。
10.1 分阶段部署策略
不要试图一次性引入所有六个组件。那会带来巨大的初始复杂度。我建议按以下顺序分阶段部署:
- 阶段一(建立基线):先实现地面实况(
feature_list.json) 和验证层(Playwright测试)。用这两个组件来定义“完成”并客观验证它。这是最基础的价值。 - 阶段二(保障连续性):加入记忆系统和启动仪式。这解决了跨会话的上下文丢失问题,让开发过程更连贯。
- 阶段三(防御退化):最后引入系统契约和功能准入协议。当你的项目功能超过10个,且共享代码增多时,这两个组件对维护长期稳定性的价值才会完全显现。
10.2 常见问题与排查技巧
问题:AI总是忘记写记忆文件或格式不对。
- 技巧:在你的AI指令(
CLAUDE.md)中,将“会话结束时必须按模板更新claude-progress.txt”作为一条铁律,并放在显眼位置。甚至可以提供一个模板代码块让它复制。更激进的做法是,写一个commit.sh脚本,该脚本会检查记忆文件是否已更新,如果没有,则拒绝执行git commit。
- 技巧:在你的AI指令(
问题:回归测试集计算错误,漏掉了本应测试的功能。
- 排查:首先检查
system-contract.json中的surfaces映射是否准确。一个常见错误是,当创建了新文件或移动了文件时,忘记更新映射。可以写一个简单的验证脚本,定期检查surfaces中列出的所有文件是否都存在,以及所有在affected_features中提到的功能ID是否都存在于feature_list.json中。 - 技巧:让
get-regression-tests.js脚本在输出要运行的测试模式时,也打印出推理过程:“检测到文件A被修改 -> 属于表面X -> 影响功能F1, F2, F3”。这增加了可调试性。
- 排查:首先检查
问题:E2E测试不稳定(Flaky Tests),有时过有时不过。
- 技巧:这是E2E测试的经典难题。除了使用Playwright的自动等待、重试机制外,关键是要确保测试的独立性和可重复性。每个测试必须能独立运行,不依赖外部服务状态或前一个测试留下的数据。充分利用
beforeEach和afterEach钩子进行彻底的清理和设置。对于与时间相关的操作,使用模拟时钟。
- 技巧:这是E2E测试的经典难题。除了使用Playwright的自动等待、重试机制外,关键是要确保测试的独立性和可重复性。每个测试必须能独立运行,不依赖外部服务状态或前一个测试留下的数据。充分利用
问题:功能依赖关系 (
depends_on) 变得复杂,形成循环依赖或死锁。- 排查:可以写一个小脚本,在每次更新
feature_list.json后,检查依赖图是否有环。这是一个经典的图论问题(检测有向图环),可以用深度优先搜索实现。 - 技巧:鼓励将功能拆分为更小、更独立的单元。如果A和B互相依赖,很可能它们应该被合并成一个更大的功能C,或者你需要引入一个中间功能A1来打破循环。
- 排查:可以写一个小脚本,在每次更新
问题:AI在实现功能时,擅自修改了其他功能的
steps描述,导致测试与描述不符。- 铁律:在
CLAUDE.md中明确规定:只有passes和notes字段可以被自动化流程或人类更新。name,description,steps,touches,depends_on字段的修改,必须经过人类审查和确认。这些是需求的本质,不应被AI单方面更改。
- 铁律:在
10.3 工具链与脚本示例
为了让这套框架运转顺畅,你需要准备一些辅助脚本。这里提供几个核心脚本的思路:
scripts/next-feature.js: 读取feature_list.json,找出下一个可构建的功能。逻辑是:找到第一个passes=false且其depends_on列表中所有功能都已passes=true的功能。scripts/mark-passing.js <feature_id>: 安全地将指定功能的passes字段更新为true,并可选地添加备注。这个脚本应由验证通过后的CI流程或本地验证命令调用,而不是由AI直接执行。scripts/check-invariants.js: 遍历system-contract.json中的invariants,根据triggers字段执行对应的检查脚本,并汇总报告。scripts/get-regression-tests.js <current_feature_id>: 接受当前功能ID作为参数,通过git diff找出变更文件,对照surfaces映射,计算出受影响的功能ID列表,并输出为Playwright的--grep参数格式(如"F004|F005|F009")。
将这些脚本准备好,并在
CLAUDE.md中清晰地告诉AI如何以及何时使用它们,能极大降低AI的理解负担和出错概率。11. 总结与个人体会
回顾这六个组件,它们共同构成了一套让AI代理进行可靠软件开发的“操作系统”。前四个组件(地面实况、记忆系统、启动仪式、验证层)确保了单次任务执行的正确性和跨任务状态的连续性。而后两个组件(系统契约、功能准入协议)则将视角提升到了系统演进的层面,防止在增加新功能的过程中破坏已有的价值。
我个人最大的体会是:与AI协作开发,最重要的不是教它写出更聪明的算法,而是为它设计更笨的流程。人类的智慧体现在设计这些容错、验证和反馈的机制上。一旦机制建立,AI就变成了一个不知疲倦、严格遵循流程的执行者。它仍然会犯错,但错误会被机制捕获在局部,而不会扩散到整个系统。
这套框架听起来有些重量级,但对于任何计划让AI代理参与超过几天、功能超过十个的严肃项目来说,它所避免的混乱和节省的调试时间,将远远超过初始的设置成本。它不是银弹,但它是一副非常结实的“缰绳”,能让你驾驭AI的强大能力,朝着正确的方向稳步前进,而不是在代码的荒野中迷失。
最终,你得到的不仅仅是一个由AI构建的项目,更是一个拥有完整可验证历史、清晰依赖关系、和抗回归能力的、可维护的代码库。这对于项目的长期健康,其价值不亚于功能本身。
