从感觉编程到规范驱动开发:AI时代软件工程的质量保障实践
1. 从“感觉编程”到“规范驱动开发”的思维跃迁
最近在团队里做Code Review,经常看到一些让人哭笑不得的提交。一个简单的用户登录功能,AI生成出来的代码洋洋洒洒几百行,里面混着三四种不同的错误处理风格,甚至还“贴心”地给我预埋了几个从没讨论过的“增强功能”——比如自动把用户登录地点的天气信息存进数据库。问起作者为什么这么写,得到的回答往往是:“我跟Copilot说‘做一个登录’,它生成出来看着差不多,跑起来也没报错,我就提交了。” 我把这种工作模式称为“感觉编程”,它正在成为新手工程师快速制造Bug的流水线。
“感觉编程”的流程很有代表性:你脑子里有个模糊的需求,用自然语言描述给AI,比如“给API加个分页”。AI吐出一段代码,你运行一下,没红字错误,功能好像也管用,就合入主线。这个过程快吗?确实快,半小时搞定一个需求。但可持续吗?绝对是一场灾难。当项目规模稍微大一点,这种模式会立刻暴露出三个致命问题:首先,生成的是无法审查的代码块,动辄500行起步,逻辑东一块西一块,Reviewer根本无从下手;其次,AI会自作主张添加你从未要求的功能,美其名曰“最佳实践”,实则是技术债的种子;最可怕的是,当你试图让AI修正其中一个问题时,它可能会基于错误的理解,重写你半个代码库的架构。
那么,资深工程师是怎么利用AI的?答案不是更复杂的提示词,而是一种完全不同的工作范式:规范驱动开发。我们不直接向AI提问,而是先为AI编写一份它能够严格执行的“施工图纸”。这套方法的核心,是将产品需求、技术设计和AI指令三层文档分离并串联起来,把原本模糊的“感觉”,转化为精确的、可验证的“规范”。接下来,我就结合一个具体的JWT认证案例,拆解这背后的完整工作流和实操细节。
2. 规范驱动开发的核心:三份不可或缺的文档
规范驱动开发不是简单地“把需求写详细点”,它是一套严谨的、将人类意图无损传递给AI的文档体系。这套体系由三份环环相扣的文档构成,每一份都有其不可替代的使命。跳过任何一步,都会让“感觉”重新渗入流程。
2.1 产品需求文档:定义“做什么”与“为什么”
PRD是你的第一份,也是最重要的锚点文档。它的目标读者首先是产品经理、设计师和工程师自己,用于对齐业务目标。在AI开发语境下,它更是AI理解业务上下文的基础。一份合格的PRD必须回答清楚两个问题:我们到底要构建什么?以及,为什么它值得被构建?
以“为我们的内部管理后台添加用户登录”这个需求为例。一份“感觉式”的PRD可能只有一句话。而一份规范的PRD则需要展开:
核心用户与场景:管理员需要登录后台管理系统,查看运营数据。当前所有人共享一个超级密码,存在安全风险且无法审计操作记录。功能需求清单:
- 用户使用邮箱和密码登录。
- 登录成功后,后续请求需携带凭证以访问受保护接口。
- 支持“记住我”功能,在浏览器关闭后一段时间内保持登录状态。
- 提供安全的退出登录机制。非功能需求:
- 安全性:密码需加盐哈希存储,传输必须使用HTTPS。
- 体验:登录失败需给出明确但不过于详细的提示(如“邮箱或密码错误”)。
- 可审计性:关键操作需记录操作者ID。
PRD里不涉及任何技术选型(比如用JWT还是Session),它只关乎业务逻辑和用户体验。这份文档是后续所有工作的源头,也是评审AI产出是否偏离目标的根本依据。
2.2 技术设计文档:规划“如何做”的蓝图
有了PRD,我们知道了目的地。技术设计文档则是描绘通往目的地具体路径的工程蓝图。它的目标读者是研发团队,核心是做出技术决策并阐述其合理性。这是将产品语言翻译为技术语言的关键一步。
继续上面的登录案例,TDD需要明确以下内容:
架构决策:
- 认证方案选择:采用无状态的JWT认证,而非有状态的Session。理由是后台管理系统为前后端分离架构,且无需支持复杂的实时会话管理,JWT更轻量且易于水平扩展。
- Token存储位置:Access Token置于HTTP Authorization Header,Refresh Token通过HttpOnly Cookie下发,以平衡安全性与便捷性。数据模型设计:
User表需新增password_hash、last_login_at等字段。- 是否需要单独的
RefreshToken表用于黑名单机制?决定暂不需要,初期通过较短的Access Token有效期来降低风险。API合约定义: POST /api/auth/login:请求体、成功响应(含access_token, expires_in)、错误响应(401, 429)。POST /api/auth/refresh:如何利用Cookie中的refresh token获取新的access token。POST /api/auth/logout:如何安全地使token失效。与其他系统的交互:明确本次开发不会修改现有的用户权限系统,新认证层需与现有权限中间件兼容。
TDD的价值在于,它迫使你在写第一行代码前,思考清楚所有技术细节和边界情况。这份文档本身,就是一次深度的设计评审。
2.3 AI规范文档:给AI的精确指令集
这是直接面向AI的“可执行文件”。它基于前两份文档生成,但不是简单的复制粘贴,而是将需求和设计,转化为AI能理解且能原子化执行的指令序列。AI规范文档的核心特点是精确、无歧义、可验证。
一个糟糕的提示是:“为我的Express.js API添加JWT认证。” 而一个规范的AI Spec应该是这样的:
## 任务:实现基于JWT的用户认证模块 **上下文**:基于已批准的PRD#123和TDD#456,为Node.js/Express后台管理系统实现登录、刷新、登出功能。 ## 严格遵循的架构与技术栈 - 后端框架:Express.js 4.x - 数据库ORM:Prisma - JWT库:jsonwebtoken - 密码哈希:bcrypt - 环境变量管理:dotenv ## 需要实现的具体端点与逻辑 ### 1. 登录端点 (`POST /api/auth/login`) **请求体**: ```json { "email": "string", "password": "string", "rememberMe": "boolean" }处理逻辑:
- 验证请求体格式。
- 根据email从
User表查找用户,使用Prisma。 - 使用
bcrypt.compare验证密码哈希。 - 若失败,返回HTTP 401,统一消息:“Invalid email or password”。
- 若成功: a. 生成JWT Access Token:payload包含
userId和role,密钥来自process.env.JWT_SECRET,有效期:rememberMe为真时7天,为假时2小时。 b. 生成Refresh Token:随机字符串,存入数据库User.refreshToken字段(单设备登录,新token覆盖旧的),有效期30天。 c. 在响应中返回:{ “access_token”: “xxx”, “expires_in”: 7200 }。 d. 将Refresh Token通过HttpOnly、Secure、SameSite=Strict的Cookie设置到客户端,路径为/api/auth/refresh。
2. 刷新端点 (POST /api/auth/refresh)
逻辑:
- 从Cookie中读取
refreshToken。 - 在数据库中查找匹配此token且未过期的用户记录。
- 验证通过后,生成新的Access Token和Refresh Token(规则同登录),更新数据库中的Refresh Token,并设置新的Cookie。
- 返回新的Access Token。
3. 登出端点 (POST /api/auth/logout)
逻辑:
- 从Cookie中读取
refreshToken。 - 清除数据库中相应用户的
refreshToken字段(设为NULL)。 - 清除客户端的Refresh Token Cookie(设置过期时间为过去)。
- 返回204 No Content。
必须创建的中间件
authenticateJWT:从Authorization: Bearer <token>头中提取token,用jsonwebtoken.verify验证,将解码后的用户信息(至少含userId)注入req.user。验证失败统一返回401。
严格的代码风格与禁区
- 错误处理:使用异步中间件,所有错误通过
next(error)传递,由顶层的错误处理中间件统一格式化返回。 - 数据库操作:必须使用Prisma Client,并包含
try...catch。 - 安全:绝对禁止在任何日志、响应中明文记录密码或完整的JWT。
- 禁止修改:不得修改现有的
/api/admin/*路由权限检查逻辑,只需确保新的authenticateJWT中间件能与其兼容。
看到区别了吗?后者几乎是一个可以直接粘贴执行的伪代码清单。它限定了技术栈、明确了每个步骤的输入输出、规定了错误处理方式,甚至划定了AI绝对不能触碰的代码区域。这才是能让AI生成出可审查、可预测代码的关键。 ## 3. 五步工作流:从需求到可合并代码的实操路径 有了三份文档的理论基础,我们来梳理将其落地的标准化工作流。这个过程就像一条流水线,每一步都为下一步奠定基础,确保产出物的质量。 ### 3.1 第一步:撰写与评审产品需求文档 不要独自闭门造车。即使是个人项目,也试着从用户视角写下PRD。核心是举办一次简短的“三方对齐会”(产品、设计、研发),哪怕只有你一个人,也扮演这三个角色过一遍。评审的重点是: * **需求是否完整**?有没有遗漏的边缘情况?比如,“忘记密码”功能是否在本期范围? * **目标是否清晰**?每个人对“做好登录”的理解是否一致? * **成功标准是否可衡量**?是“功能上线”就行,还是“登录成功率大于99.9%”? 实操心得:PRD评审通过后,将其固定在项目Wiki或Issue中,并锁定版本。后续任何需求变更,都必须先更新PRD并重新评审,从源头上控制范围蔓延。 ### 3.2 第二步:细化技术设计文档 基于锁定的PRD,开始进行技术决策。这一步的关键是**做选择题并给出理由**。例如: * 选择JWT而不是Session,是因为我们的部署架构是无状态的。 * Refresh Token存数据库而不存Redis,是因为初期复杂度低,且数据一致性更容易保证。 * 密码加密选用bcrypt而不是argon2,是因为当前Node.js版本对bcrypt支持更稳定。 将所有这些决策、接口定义、数据模型变更,用清晰的图表和列表写在TDD中。然后,发起一次技术评审。评审的重点是: * **方案是否可行**?是否有难以实现的技术难点? * **方案是否最优**?有没有更简单、更稳定的替代方案? * **方案是否安全**?是否存在已知的安全漏洞(如JWT密钥强度不足)? * **对现有系统影响**?本次修改是否会破坏现有功能? 注意事项:TDD评审通过后,它就成了开发的“宪法”。AI编码、人工编码都必须遵循它。这能有效防止技术决策在实现阶段被随意篡改。 ### 3.3 第三步:生成精准的AI规范文档 这是将人类智慧“编码”成AI指令的一步。我的经验是,以TDD为骨架,将每一个技术决策和API合约,拆解成AI能执行的原子任务。一个高效的技巧是使用“**给定-当-那么**”的格式来描述逻辑: * **给定**一个有效的邮箱和密码 * **当**用户调用登录接口 * **那么**系统应验证密码,生成双Token,并正确设置Cookie和响应。 将TDD中所有类似的逻辑点都如此拆解,就构成了AI Spec的主体。然后,务必在文档末尾加上“**禁区与风格**”章节,明确告诉AI哪些文件不能动、必须使用哪种错误处理模式、代码注释规范等。这份文档本身,也应该作为代码仓库的一部分进行版本管理。 ### 3.4 第四步:运行AI智能体并生成差异代码 现在,才是打开你的Copilot Chat、Cursor AI或Claude Code的时候。不要直接给它看代码文件,而是将**AI规范文档**作为最主要的上下文喂给它。你可以这样说:“请根据以下Spec,在现有的Express项目(简要描述项目结构)中实现所述功能。请严格遵循Spec中的所有要求,包括技术栈、逻辑步骤和代码禁区。” 接下来,AI会开始生成代码。规范驱动开发在这里的另一个优势显现了:**你不需要它一次性生成全部功能**。你可以要求它按照Spec中的模块顺序,逐个端点或逐个中间件地生成。例如,先让它实现“登录端点”,你审查并确认无误后,再让它基于已实现的代码上下文,继续完成“刷新端点”。 这样做的好处是,每次生成的代码块都是小而精的,便于聚焦审查,也降低了AI因上下文过长而“失忆”或胡编乱造的风险。 ### 3.5 第五步:审查代码差异,而非海量代码 这是与传统“感觉编程”评审天差地别的一步。当AI基于精确的Spec完成编码后,你作为评审者,不需要再像大海捞针一样去审查几百行陌生代码的逻辑是否正确。你的审查重点转变为两个: 1. **一致性审查**:将AI生成的代码(Diff),与**AI规范文档**逐条比对。它是否严格遵循了每一条指令?Token有效期对吗?错误响应格式对吗?Cookie属性设置了吗?这个过程就像质检员对照图纸检查零件尺寸,高效且准确。 2. **上下文集成审查**:检查生成的代码与现有代码库的集成点。引入的中间件是否被正确挂载到路由上?新的数据库字段迁移脚本是否生成?是否有意外的全局变量污染或命名冲突? 如果审查通过,你就可以自信地合并代码。因为你知道,所有的业务逻辑和技术决策,早在PRD和TDD阶段就已经过深思熟虑和团队评审,AI只是一个没有偏差的执行者。 ## 4. 深入案例:JWT认证模块的规范驱动实现与避坑指南 让我们将上述理论,完全套入到“为Express.js后台添加JWT认证”这个具体案例中,看看每一步的实操细节和可能遇到的坑。 ### 4.1 需求与设计的转化:从模糊到精确的挑战 最初的模糊需求是“加个登录”。通过PRD,我们明确了要支持“记住我”。在TDD阶段,这个需求转化为了一个具体的技术决策:**Access Token的有效期根据`rememberMe`字段动态变化**。这是一个关键的细节,如果不在设计阶段明确,AI很可能会生成一个固定有效期的Token,导致用户体验不符。 在编写AI Spec时,这个决策需要被进一步拆解为可执行的逻辑: > 登录成功时,判断请求体中的`rememberMe`布尔值。若为`true`,则`access_token`有效期`expires_in`设为`604800`秒(7天);若为`false`或未提供,则设为`7200`秒(2小时)。此逻辑必须同时应用于Token的JWT payload过期时间和响应的`expires_in`字段。 这就是“规范”的力量:它把一个容易遗漏的产品细节,变成了AI必须遵守的硬性规则。 ### 4.2 安全实现的魔鬼细节 安全无小事,而AI对安全的理解是肤浅的。规范文档必须充当安全专家。 **密码存储**:在Spec中必须明确写出“使用`bcrypt.hash`进行哈希,盐轮数建议为12”。绝不能只写“哈希密码”,否则AI可能选用不安全的MD5或SHA-1。 **JWT密钥**:必须指令“从`process.env.JWT_SECRET`读取密钥,该密钥必须是长度至少32位的随机字符串”。并补充说明“在项目根目录的`.env.example`文件中添加`JWT_SECRET=`示例,提醒开发者配置”。 **Refresh Token防篡改**:指令中需明确“Refresh Token应是一个加密安全的随机字符串(可使用`crypto.randomBytes`生成),存储于数据库,并用于查询验证”。这防止了客户端伪造Token。 **Cookie安全属性**:这是重灾区。必须逐字写明:“设置Cookie时,属性必须包含:`httpOnly: true`(防止JS访问)、`secure: process.env.NODE_ENV === ‘production‘`(生产环境HTTPS)、`sameSite: ‘strict‘`(防CSRF)”。 > 注意:我曾见过AI生成的代码漏掉了`httpOnly`,导致Refresh Token暴露给前端JavaScript,这是一个严重的高危漏洞。在Spec中显式强调每一条安全属性,是杜绝此类问题的唯一方法。 ### 4.3 错误处理与日志记录的规范 混乱的错误处理是“感觉编程”代码的典型特征。在Spec中,我们必须统一错误处理范式。 **指令示例**: “所有异步操作(如数据库查询、JWT验证)必须使用`try…catch`包裹。在catch块中,调用`next(error)`将错误传递给Express错误处理中间件。**禁止**在路由处理函数中直接使用`res.status(500).json(...)`。” “创建统一的错误响应格式。在项目的错误处理中间件中,确保生产环境下返回`{ “error”: “Internal Server Error” }`,开发环境下可包含`message`和`stack`。” 对于日志,指令需明确边界:“仅在登录成功/失败、Token刷新、登出时记录信息级别日志,日志内容需脱敏(如记录userId但不记录token)。**严禁**在日志中记录密码、完整的JWT或任何个人身份信息。” ### 4.4 与现有系统的无缝集成 新模块不是孤岛。Spec必须定义清晰的集成合约。 **中间件挂载**:“新建的`authenticateJWT`中间件,需要在`app.js`或主路由文件中,在所有需要认证的API路由之前全局挂载,或按路由组挂载。” **用户对象扩展**:“`authenticateJWT`中间件验证成功后,应将解码出的payload(至少包含`userId`)赋值给`req.user`。确保现有权限检查中间件能兼容地从`req.user`读取用户信息。” **数据库迁移**:“使用Prisma,需生成并包含对应的迁移文件(`prisma migrate dev`),在`User`模型中添加`refreshToken String?`字段。” ## 5. 常见问题、排查技巧与效能提升实录 即便遵循了规范流程,在实际操作中仍会遇到各种问题。以下是我在实践中总结的常见“坑点”及解决方案。 ### 5.1 AI生成的代码不遵循Spec怎么办? 这是最常遇到的问题。根本原因通常有两个:一是Spec本身存在二义性,二是AI的上下文理解不足。 **排查与解决**: 1. **检查Spec的精确性**:立即回看AI违反的那条指令。是不是表述不够明确?例如,“验证密码”应改为“使用`bcrypt.compare`函数比对请求中的`password`和数据库中的`password_hash`字段”。 2. **提供更具体的上下文**:如果项目结构复杂,在给AI的指令开头,简要描述相关文件的位置。例如:“项目使用Express,路由定义在`src/routes/auth.js`中,用户模型在`src/models/User.js`。” 3. **分步执行,即时反馈**:不要让它一次性生成所有代码。让它先写登录接口的控制器函数。审查通过后,再让它基于这个函数,去写验证密码的工具函数。像教实习生一样,一步步引导。 4. **使用“修正”指令**:如果AI跑偏,直接指出:“你生成的代码在XX行没有按照Spec要求设置`sameSite: ‘strict‘`属性。请根据Spec第Y条修正。” AI会根据你的精确反馈进行调整。 ### 5.2 如何高效审查AI生成的代码? 面对一份完整的Diff,采用分层审查法: * **第一层:架构符合性**。快速扫视生成的文件和代码结构,是否与TDD中规划的模块划分一致(如是否新建了`/utils/auth.js`存放JWT函数)? * **第二层:关键安全点**。直奔主题,搜索“password”、“secret”、“token”、“cookie”等关键词,逐项核对安全规范是否被严格执行。 * **第三层:核心逻辑流**。针对每个端点,沿着“请求输入 -> 验证 -> 业务处理 -> 数据库操作 -> 响应输出”的路径走读一遍,看逻辑是否与Spec描述的完全一致。 * **第四层:代码风格与集成**。检查错误处理是否统一、导入导出是否规范、是否无意中引入了未使用的依赖、是否修改了Spec中声明的“禁区”代码。 ### 5.3 规范驱动开发真的不会降低效率吗? 初期看,写三份文档比直接问AI要慢。但从整个项目生命周期看,它的效率提升是巨大的。 **时间节省在哪儿?** 1. **零返工**:因需求不清或设计缺陷导致的代码推倒重来,基本被杜绝。 2. **审查时间锐减**:评审者从理解数百行杂乱代码,变为对照Spec做“填空题”,审查耗时可能减少80%。 3. **调试时间减少**:由于边界情况已在Spec中定义,运行时诡异Bug的数量显著下降。 4. **知识传承标准化**:新成员加入时,阅读PRD和TDD能快速理解系统全貌,AI Spec则是最佳的实现参考手册。 一个实用的技巧是:为你的项目创建PRD、TDD和AI Spec的**模板文件**。下次启动类似功能时,你只需要在模板上修改,而不是从头创作,能极大提升文档阶段的效率。 ### 5.4 针对不同AI工具(Copilot, Cursor, Claude)的适配技巧 不同的AI工具有不同的特性和优势,规范驱动开发是通用的,但指令的微调可以让你事半功倍。 * **GitHub Copilot Chat**:它深度集成在IDE中,对项目上下文感知最强。在提问时,可以多使用“在当前文件中”、“参考`userService.js`的模式”这样的短语,让它更好地利用现有代码风格。 * **Cursor**:其“Composer”模式非常适合分步实现复杂Spec。你可以将AI Spec的不同章节(如“登录端点”、“刷新端点”)分别粘贴到不同的Composer任务中,让它依次完成,保持上下文清晰。 * **Claude Code**:长上下文能力出色,且对指令的理解非常精准。你可以将完整的PRD、TDD和AI Spec一次性喂给它,并说“请基于以上所有文档,生成实现代码”。它往往能给出集成度更高、更连贯的产出。 无论使用哪种工具,核心原则不变:**永远让AI基于文档工作,而不是基于你即时的、模糊的“感觉”**。当你养成先写规范再编码的习惯后,你会发现,你与AI的协作从未如此顺畅、可靠。你不再是一个Bug的修复者,而是一个复杂系统的清晰构建者。