用自然语言描述一个想法,让 AI 创建项目、修改代码、运行程序,再根据眼前的结果继续调整,这是很多人接触 Vibe Coding 时的体验。对于一个小工具,人与 AI 的对话可能就足以推动整个开发过程。
当这个工具需要长期使用,问题会逐渐变多:下一次需求应该在哪里改?哪些已有行为必须保留?测试通过意味着什么?换一个会话,AI 能否接着工作?代码已经推送,用户为什么还没有看到变化?
这些问题连接着软件开发的不同部分。理解 Vibe Coding 的工程化,需要把自然语言交互、编程 Agent、项目知识、执行环境和交付流程放在一起看。
本文面向已经接触过代码、Git 和基本测试的读者,从概念与工作机制开始,逐层讲解这些部分如何配合。文中的订单、界面与数据迁移场景都是教学示例;它们用于展开机制,不代表某个真实项目的实现。
一、Vibe Coding 是什么
最初的含义与本文的讨论范围
Andrej Karpathy 在 2025 年提出 Vibe Coding 时,描述的是一种主要依靠自然语言和运行结果推进开发的方式:持续接受 AI 的修改,很少阅读代码差异,遇到错误就把错误交回模型继续尝试。Simon Willison 随后专门区分了这种方式与经过代码阅读、测试和理解的 AI 辅助开发。Vibe Coding 的原始描述与概念辨析
因此,按这个较严格的含义,开发者认真审查实现、建立验收并承担维护责任以后,工作方式已经逐渐转向工程化的 AI 辅助开发。日常交流也会把“用自然语言驱动 AI 写代码”广泛叫作 Vibe Coding。本文沿用这个常见入口,讨论它如何进入可持续维护的开发流程。
自然语言在这里承担两种工作。一种是表达目标,例如“给订单列表增加状态筛选”;另一种是反馈观察,例如“切换筛选条件后,页码没有回到第一页”。AI 再把这些信息转化为代码和操作。
这种交互减少了手工编写每一行代码的工作,但需求中的空白仍然存在。筛选条件能否组合、切换后是否重置分页、刷新后是否保留条件,都需要通过产品约定、已有行为或明确决策来确定。模型会补全缺失信息,而补全结果未必符合项目需要。
补全、对话与编程 Agent
可以按 AI 参与开发的方式理解几种常见形态:
| 形态 | 人提供什么 | 系统主要做什么 | 下一步通常由谁推动 |
|---|---|---|---|
| 行内补全 | 当前代码和光标位置 | 续写局部代码 | 人接受、修改并继续输入 |
| 对话辅助 | 问题、代码片段、错误信息 | 解释或生成候选实现 | 人把结果接入项目并运行 |
| 编程 Agent | 任务目标与仓库访问能力 | 搜索、读取、编辑、运行检查,依据结果继续处理 | Agent 在授权范围内推进,人负责目标与关键判断 |
这是理解交互方式的分类,同一个产品可以同时提供多种形态。它们也不对应固定的代码质量等级:一个很小的补全可能破坏业务边界,一个自主执行较多步骤的 Agent 也可以在清楚的约束下完成可靠的改动。
工程化关注的是整个过程:目标是否明确,执行是否有边界,结果是否可检查,后续是否有人能继续维护。
从原型进入维护阶段,增加了什么
做一个临时数据转换工具时,可以人工挑几份文件检查结果。做一个供团队长期使用的服务时,还需要关心历史数据、访问权限、并发请求、接口兼容和部署方式。
差别来自使用场景。一个错误的影响范围、发现难度和恢复成本越大,所需的验证和控制就越多。
例如,本地离线原型可以使用模拟数据,直接观察页面;接入真实账户后,就需要检查凭据保存、服务端授权和数据删除行为。工程化的程度应随这些实际责任增长,早期也要保留清晰的代码结构与修改记录,方便原型继续演进。
二、编程 Agent 如何把一句话变成代码
模型、工具与宿主各做什么
编程 Agent 通常包含三个直接参与执行的部分。
模型根据当前看到的信息,生成回答、修改方案或工具调用请求。它对仓库的认识来自上下文中的内容,包括已经读取的文件和工具返回的结果。
工具执行具体操作,例如搜索文件、读取代码、应用补丁、运行测试或打开浏览器。工具返回的内容成为下一轮决策的输入。
宿主运行时组织这个循环:准备模型输入,接收工具请求,执行权限检查,调用工具,再把结果交回模型。上下文管理、会话状态、部分重试和停止逻辑也由这一层处理。围绕模型提供这些能力的环境,常被称为 Agent harness。Claude Code 的官方机制说明就以模型、工具和上下文管理来解释这一层。编程 Agent 的运行机制
以“给订单响应增加展示状态”为例,一次执行可能是:
flowchart TD
accTitle: 编程 Agent 的基本执行循环
accDescr: 用户提供目标,模型选择操作;宿主检查权限并调用工具,把仓库内容或执行结果交回模型,直到交付结果或报告阻塞。
A[用户目标与项目约定] --> B[模型选择下一步]
B --> C{需要调用工具吗}
C -->|需要| D[宿主检查权限与参数]
D --> E[搜索、读取、编辑或运行]
E --> F[返回内容、差异或错误]
F --> B
C -->|可以结束| G[交付改动与验证结果]
第一轮可能搜索 status;下一轮读取响应映射和调用方;再下一轮编辑函数;随后运行测试。测试报告旧字段缺失,模型读到差异后补回字段,再次执行检查。
这里发生变化的是工作区中的文件,推动后续选择的是可观察的反馈。模型输出了“已修复”这句话,只能说明它给出了一个判断;实际文件和程序行为需要从工具结果确认。
Agent 能做的事受什么影响
同一个模型,放在不同环境中,能完成的工作也会不同。
如果只能接收粘贴的代码,它无法主动确认调用方;如果可以搜索仓库,就能追踪引用;如果还能启动服务和浏览器,就能检查实际页面。测试环境缺少数据库、网络或必要依赖时,Agent 也可能只能完成局部验证。
工具反馈的质量同样重要。下面两种结果对后续修复的帮助不同:
结果 A:测试失败。
结果 B:GET /orders/A1001 的响应缺少 status。
预期:status = PAID,displayStatus = 已支付。
实际:仅有 displayStatus = 已支付。
结果 B 把失败位置、约定和差异一起带回执行循环,模型可以据此提出有针对性的修复。结果 A 需要先重新获取信息。
Agent 的能力因此可以从多个方面评估:模型能否理解问题,工具能否触达相关对象,环境能否复现行为,反馈能否区分正确与错误。单独更换模型,只能改变其中一部分。
为什么会出现“越改越偏”
一种常见过程是:模型误解了初始需求,在这个假设上写实现,又按照新实现更新测试。程序运行正常,测试也通过,但实现和验收一起偏离了原本目标。
另一种过程是不断修补症状。例如分页异常本来来自筛选条件变化后没有重置页码,Agent 却先改接口、再改缓存、最后修改组件状态。改动越来越多,最初的因果关系变得更难确认。
工程化要为这个循环提供相对稳定的参照:需求说明行为,现有契约约束兼容性,测试提供反馈,代码评审检查实现和验收是否同时被带偏。出现连续无进展时,则回到事实和假设,重新定位问题。
三、把自然语言需求写成任务规格
从愿望到行为约定
任务规格描述本次改动应产生的行为。它可以很短,但需要包含那些不能让实现者自行猜测的决定。
例如“订单接口增加展示状态”至少存在几种实现:替换 status 的值、增加新字段、由前端映射,或根据用户语言动态生成。先结合调用方与产品需求作出选择,再开始实现。
假设本次选择服务端返回中文展示值,并兼容旧客户端,规格可以写成:
目标:订单查询响应新增 displayStatus。
兼容性:保留 status,已有取值与含义不变。
映射:PAID → 已支付;CANCELLED → 已取消;PROCESSING → 处理中。
兜底:未知状态展示“未知状态”,status 保留原值。
范围:修改响应映射与必要测试,不迁移数据库,不调整认证逻辑。
验收:新旧字段同时存在,已知及未知状态通过检查。
交付:给出代码差异、检查结果和部署状态。
这样就能区分实现自由与业务约束。映射表放在哪个内部文件,可以沿用项目结构;旧字段是否保留,则属于对调用方的承诺。
如果项目要支持多语言,就要进一步确定展示文本属于哪一层。这个决定会影响缓存、接口和客户端,因此应在规格中说明依据。当前示例选择中文固定映射,仅用于解释兼容性。
设计说明什么时候值得写
需求涉及多个模块时,任务规格还需要配合简短设计。设计回答数据流经哪里、接口怎样连接,以及为什么采用这种分工。
例如“增加订单取消功能”,先沿业务过程展开:客户端提交取消请求,服务端确认身份,校验订单归属与当前状态,更新订单,再处理后续通知。重复请求、已发货订单和通知失败分别落在哪个环节,都应在实现前形成明确约定。
可以先画数据流、列状态转换和失败结果,再让 Agent 对照现有代码选择修改入口。一个字段映射通常无需完整设计文档;涉及权限、状态机和外部副作用时,这些说明能防止不同文件各自实现出互相冲突的行为。
这也解释了规格驱动开发的作用:先把预期行为与关键决定保存下来,再围绕它们实现和验收。规格仍会随新发现调整,但调整应有依据,并同步影响代码与测试。
按可验收的行为拆分任务
大任务更适合拆成能独立走通的小步。例如“支持订单取消”可以先完成只处理一种可取消状态的完整路径,再补充其他状态和通知机制。
如果先让 Agent 一次生成所有数据库表、所有接口、所有页面,直到最后才联调,许多错误会叠加。按可运行的行为推进,第一步就能检查身份、请求、业务判断和响应是否接得上。
每一步应有可观察的产物,例如一个通过验证的接口或一条可使用的页面路径。步骤数量由依赖关系决定,不需要为了显得有计划而把每次文件编辑都列成阶段。
四、管理 Agent 看到的上下文
上下文是当前决策所用的信息
Agent 当前能够使用的信息,可能包括用户请求、项目规则、已读取代码、工具定义、命令输出、图片和会话摘要。仓库中存在一个文件,通常还需要经过读取、检索或其他加载方式,它的相关内容才会进入模型本次决策。
例如,修改订单响应时,真正有用的材料是响应映射、接口定义、旧客户端使用方式和相关测试。把所有历史需求、整个依赖目录和几万行日志一起加入,会让有效信息更难定位,还会增加处理成本。
上下文工程关心信息的选择、组织和持续更新。Anthropic 的相关说明强调有限上下文中的相关性,以及按需读取、摘要和结构化笔记等方法。上下文工程
在实际任务里,可以先定位入口,再沿依赖补齐信息。读接口函数时发现它使用统一序列化配置,就继续读取配置;确认错误来自前端解析后,再跟踪对应调用方。每次读取都应服务于当前判断。
长期约定、任务材料与动态证据
这三类信息的更新节奏不同:
| 信息 | 示例 | 合适的保存位置 |
|---|---|---|
| 长期约定 | 目录职责、构建命令、接口兼容原则 | 项目规则与架构文档 |
| 当前任务 | 本次新增字段、状态映射、范围与验收 | 任务描述、Issue 或任务文档 |
| 动态证据 | 当前 diff、失败断言、运行日志 | 工具结果,必要时形成简短进度记录 |
长期约定帮助每次任务进入正确位置;任务材料说明这一次具体做什么;动态证据告诉 Agent 工作推进到了哪里。
以支持 AGENTS.md 的宿主为例,一个项目入口可以是:
# 项目约定
- 使用 Node.js 24 和 npm,保留 package-lock.json。
- 响应映射在 src/responses/,契约用例在 test/contracts/。
- 快速验证:npm run test:contracts。
- 完整验收:npm run check,具体组成见 package.json。
- generated/ 由接口定义生成,先改定义,再运行 npm run generate。
- 旧客户端依赖 status 的现有语义,调整前检查调用方与迁移方案。
- 架构说明见 docs/architecture.md,接口约定见 docs/api.md。
这些路径和脚本属于示例项目。实际使用时要填写仓库中真实存在的入口,避免 Agent 反复执行不存在的命令。
Codex 会按文档规定的全局与项目目录范围发现 AGENTS.md 等指令文件;Claude Code 也提供 CLAUDE.md 等项目说明入口。不同工具的发现方式和覆盖规则要分别核对。Codex 项目指令、Claude Code 项目说明
规则文件最有价值的内容,是代码中不容易直接看出的约束和原因。例如“这个字段还有旧客户端在用”,比“编写高质量、可扩展的代码”更能影响当前实现。文档还需要随目录、脚本和架构演进更新。
会话变长以后,保存什么
长任务可能需要压缩上下文或更换会话。此时,一份任务记录应保留当前事实和定位入口:
目标:新增 displayStatus,保留 status 的取值与含义。
已完成:响应映射和四种状态的单元用例。
当前改动:src/responses/order.mjs、test/order-response.test.mjs。
尚未验证:实际 HTTP 响应是否保留 status。
已知问题:序列化层可能按旧 schema 过滤新增字段。
下一步:检查接口 schema,再运行契约测试。
这样的记录能帮助下一轮恢复工作方向。恢复时仍要检查当前文件、Git 状态和必要命令结果,因为记录形成后,代码或环境可能已经变化。
记忆适合保存定位线索和已确认的决定。实时状态,例如测试是否通过、当前分支是什么、服务是否仍在运行,需要重新读取。把这两类信息分清,可以减少把过去的成功当成当前事实的情况。
五、Rules、Skills、Tools 和 MCP 如何配合
工具提供动作,规则提供项目约定
工具决定 Agent 可以请求执行什么动作。终端工具可以运行命令,文件工具可以读写代码,浏览器工具可以观察和操作页面。
规则决定 Agent 在这个项目中应该怎样工作。例如先改接口源定义,再运行客户端生成器。规则进入模型上下文后影响其选择;实际能否写入文件、访问网络或调用服务,还取决于宿主和执行环境。
因此,同一条要求可以有不同实现层次:“不要直接编辑生成代码”可以先作为项目约定;重复发生后,可以在编辑前检查路径;需要强制限制实际写入时,则要覆盖相应执行入口。
Skill 保存一类任务的方法
Skill 适合复用有步骤、有材料依赖的专项方法。例如“修改接口时同步调用方”可能需要查接口定义、生成客户端、跑兼容性测试,再说明受影响的调用方。
Agent Skills 规范使用带有元数据的 SKILL.md,并允许附带脚本、参考文档和资源。宿主可先加载名称与描述,需要时再读取正文和相关文件。Agent Skills 规范
下面是一份教学用的最小内容,安装位置由所用工具决定:
---
name: api-compatibility
description: 修改公开接口、响应字段或生成客户端时,核对兼容性并完成相关验证。
---
# 接口兼容性修改
1. 阅读接口定义及相关调用方,列出需要保留的字段和语义。
2. 根据任务规格修改源定义与实现,不直接修改生成文件。
3. 如果定义发生变化,运行项目约定的客户端生成命令。
4. 执行契约用例,检查旧调用方式与新增行为。
5. 报告实际改动、检查结果及仍需迁移的调用方。
Skill 本身提供方法;其中的命令仍由工具实际执行。它可以减少重复说明,但不会自动证明方法正确。生成器入口改变后要更新 Skill,附带脚本也应像普通代码一样检查和维护。
MCP 连接外部能力
当 Agent 需要访问工单、文档、代码托管或其他服务时,可以通过原生连接器、CLI、直接 API 或 MCP 等方式接入。MCP 提供模型应用与外部能力之间的标准化连接方式,宿主负责将相应能力接入自己的工作流程。MCP 概述
例如,Agent 先读取一条工单,通过本地搜索找到接口入口,再按 Skill 更新实现,最后通过代码托管工具创建 PR。工单和 PR 操作可以由 MCP 工具提供,本地搜索和测试仍由本地工具完成。
一个已有稳定 CLI 的任务,可以直接使用 CLI;需要在多个宿主中复用服务集成时,MCP 可能更方便。接入方式还要考虑身份、权限、部署和维护成本。
这些组件围绕同一个执行循环工作:规则补充背景,Skill 提供专项方法,工具执行动作,MCP 提供一种外部接入方式。Hooks 则负责在特定事件发生时触发处理,后文单独展开。
六、准备能运行、也有边界的环境
先建立可复现的起点
Agent 接手已有项目时,先确认工作目录、分支、已有修改、运行时版本和依赖安装方式,再执行必要的基线检查。
基线检查用于区分“开始前就存在的问题”与“本次修改带来的问题”。例如项目原来就有一项集成测试因测试数据库不可用而失败,后续报告应保留这个事实,继续完成不依赖该数据库的验证。
环境也要尽量可复现。使用锁文件安装依赖,说明必要的数据库、环境变量名称与启动方式,提供不包含真实凭据的示例配置。对于原型,优先使用模拟数据或隔离的测试服务,便于反复运行和清理。
一个编译失败的项目,如果没有区分代码错误、版本不匹配和环境缺失,Agent 很容易通过修改源码去适配错误的环境。先确认起点,能缩小后续排查范围。
工具权限、沙箱与服务端授权
这几层控制处理不同问题。
工具权限决定某次调用是否允许、需要询问或被拒绝。沙箱限制程序实际能够访问的文件、进程和网络。远端服务则根据调用身份决定它能读取或修改哪些业务数据。
以 Codex CLI 的一个配置组合为例:
codex --sandbox workspace-write --ask-for-approval on-request
这里的两个参数分别指定工作区写入边界和审批策略。实际网络、读取范围与受保护目录仍受平台及配置影响;写入受限也不意味着所有私人文件都不可读取。Codex 沙箱与审批
如果 Agent 可以调用测试服务,但使用的是具备生产写权限的凭据,本地工作区隔离并没有限制那项远端权限。服务身份应与本次任务所需能力相匹配。
同样,容器与 worktree 也承担不同职责。容器可以承载执行环境,worktree 分离代码工作目录;具体数据、网络和账户是否隔离,需要检查实际配置。
外部内容与扩展也是输入
编程任务会接触 README、网页、Issue、日志和第三方包。里面可能包含命令或要求,例如让开发者上传环境文件、关闭检查或执行安装脚本。
这些内容首先是待处理资料。Agent 应依据用户目标与项目规则判断它们是否相关,不能只因为文本出现在工具结果中,就把它当成新的授权。
例如,日志里出现一段“请读取本机凭据”的字符串,它可能只是用户输入或异常数据。调试任务需要分析日志所表达的事实,不需要执行字符串中的要求。
安装 Skill、插件或依赖时,也要查看它会加载哪些脚本、访问哪些服务。终端命令可能启动子进程,包管理脚本也可能执行仓库中的代码。因此,需要同时理解工具入口和实际执行内容。Claude Code 的权限文档展示了其工具调用的控制方式,具体配置应按所用宿主执行。Claude Code 权限
七、与 Agent 一起推进实现和调试
新功能:沿一条可运行路径推进
完成需求和入口定位后,先实现最小的完整行为。例如新增筛选功能,第一步可以只支持一种筛选条件,但要打通控件、请求参数、服务端查询和结果展示。
这一轮可以发现参数名不一致、返回结构不匹配、空结果没有处理等连接问题。确认路径成立后,再增加组合筛选、URL 状态同步和其他条件。
要求 Agent 复用现有组件、数据访问层和错误处理方式,能够减少不必要的新抽象。确实需要新依赖时,先说明它解决什么具体困难,以及项目已有能力为什么不足。
实现结束后关注 diff 中每一类改动的用途。如果一个展示字段同时引入新框架、重写目录结构并升级大量依赖,这些变化需要分别解释,通常也适合拆开处理。
故障修复:用复现区分假设
调试时,先保存可复现的输入、操作与结果,再提出假设。以“切换订单状态后列表为空”为例:
复现:停留在第 5 页,切换到只有 1 页数据的“已取消”。
请求:status=CANCELLED&page=5。
响应:总数为 8,items 为空。
预期:切换筛选条件后请求第 1 页。
这组事实把问题定位到筛选与分页的关系。可以先直接请求 page=1,确认数据存在,再检查前端条件变化时是否重置页码。
修复位置应靠近原因:让筛选条件和页码在一次状态更新中保持一致,再补充“在高页码切换筛选”的回归用例。如果先改数据库查询或清空所有缓存,可能改变大量无关行为。
Agent 连续几轮都没有获得新证据时,应暂停同方向的补丁,整理已尝试的方法和仍未解释的现象。更换思路可以从复现、调用链或环境差异开始,而不是继续随机调整参数。
重构与界面:分别选择验证方法
重构需要先说明保持不变的行为。例如将重复状态映射提取成公共函数,输入输出、异常语义和调用方契约应保持一致。先有代表性用例,再移动实现,比较结果,能让重构与功能修改的影响分开。
界面任务则需要可观察的设计约束,例如容器宽度、信息层级、按钮状态和手机布局。可以让 Agent 读取参考图和现有样式,先完成主要结构,再对照实际页面调整间距与细节。
界面验证还应实际操作:用键盘进入筛选控件,提交表单,检查加载、空结果和失败状态,再在窄屏观察换行与滚动。构建检查能发现一部分代码问题,无法判断按钮是否被遮住、焦点是否可见或阅读顺序是否合理。
这些任务共享“观察、修改、验证”的循环,但验证对象各不相同。测试选择要跟着用户可观察的行为走。
八、建立能发现错误的验证
每一种检查能证明什么
格式、类型、测试、构建和运行验证覆盖不同层次:
| 检查 | 主要观察对象 | 通过后仍可能遗漏什么 |
|---|---|---|
| 格式与静态规则 | 文件格式、语法模式、部分代码约定 | 业务结果是否正确 |
| 类型检查 | 声明的类型关系 | 真实输入、权限和业务含义 |
| 单元测试 | 局部函数与边界条件 | 序列化、数据库和系统连接 |
| 集成与契约测试 | 模块连接、接口结构与交互 | 完整用户流程、部分真实环境差异 |
| 端到端与页面检查 | 用户路径、运行结果与界面 | 未覆盖的路径和生产负载 |
| 构建 | 产物是否可以生成 | 产物上线后是否满足实际需求 |
对于一个字段映射,重点是输入输出与兼容性;对于租户数据访问,重点还包括不同身份是否能读取各自数据。用例应来自任务规格和历史行为,再检查实现是否满足它们。
如果测试与实现都由 Agent 修改,评审还需要观察断言变化。一个旧契约用例被删除后,剩余测试全部通过,验证范围却已经变小。
可运行示例:保留旧字段并增加展示值
下面使用 Node.js 24 的内置测试工具,不依赖第三方包。保存实现为 order-response.mjs:
const displayNames = {
PAID: '已支付',
CANCELLED: '已取消',
PROCESSING: '处理中',
};
export function toOrderResponse(order) {
return {
id: order.id,
status: order.status,
displayStatus: Object.hasOwn(displayNames, order.status)
? displayNames[order.status]
: '未知状态',
};
}
映射只改变新增字段的展示值。旧字段 status 原样返回,未知状态也不会被中文兜底覆盖。
保存测试为 order-response.test.mjs:
import test from 'node:test';
import assert from 'node:assert/strict';
import { toOrderResponse } from './order-response.mjs';
const cases = [
['PAID', '已支付'],
['CANCELLED', '已取消'],
['PROCESSING', '处理中'],
['FUTURE_STATE', '未知状态'],
];
for (const [status, displayStatus] of cases) {
test(`保留 ${status} 并生成展示值`, () => {
const response = toOrderResponse({ id: 'A1001', status });
assert.deepEqual(response, { id: 'A1001', status, displayStatus });
});
}
在两个文件所在目录执行:
node --test order-response.test.mjs
预期四项通过。如果删除实现中的 status,四项都会失败。如果把未知状态的原始值改成“未知状态”,最后一项失败。这个对照说明测试能检查本例的兼容性约定。内置测试工具的运行方式见 Node.js Test runner。
再向外验证一层
映射函数正确,HTTP 层仍可能遗漏字段。例如序列化 schema 只保留旧字段,新增的 displayStatus 就无法到达客户端。
可以增加接口级用例。下面的 order-http.test.mjs 启动一个临时本地服务,展示如何从 HTTP 客户端检查响应:
import test from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { once } from 'node:events';
import { toOrderResponse } from './order-response.mjs';
test('HTTP 响应同时保留业务状态和展示状态', async () => {
const server = createServer((request, response) => {
if (request.method !== 'GET' || request.url !== '/orders/A1001') {
response.writeHead(404);
response.end();
return;
}
const order = { id: 'A1001', status: 'PAID' };
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(toOrderResponse(order)));
});
try {
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/orders/A1001`);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
id: 'A1001',
status: 'PAID',
displayStatus: '已支付',
});
} finally {
await new Promise((resolve, reject) => {
server.close(error => error ? reject(error) : resolve());
server.closeAllConnections();
});
}
});
一起执行:
node --test order-response.test.mjs order-http.test.mjs
预期五项通过。端口 0 让系统分配临时端口,finally 负责关闭服务,便于重复执行。
这个服务是教学用的最小 HTTP 入口。应用到真实项目时,应启动项目实际的路由、序列化与中间件,再用同样的客户端断言检查它们;还需要按实际契约覆盖其他状态。本例没有经过真实网关、认证或数据库,因此只能证明示例代码覆盖到的路径。
验证需要逐层扩大到改动真正影响的位置。也无需为每个小变更重复建设全部层次:优先复用项目已有检查,再补充确实缺失且能发现问题的用例。
九、用 Hooks 把反馈接回执行过程
Hook 在什么时候介入
Hook 是宿主在特定事件发生时调用的处理程序。它可以在工具调用前检查参数,在编辑后触发局部检查,或在准备结束时核对未完成项。
例如,调用前发现 Agent 要编辑 generated/client.ts,可以返回“先修改源定义并运行生成器”;编辑后发现兼容性测试失败,可以把缺失字段的断言交回 Agent,让下一轮修复依据具体结果推进。
flowchart TD
accTitle: Hook 如何把检查接回开发过程
accDescr: 编辑请求先经过入口检查,符合约定后执行,随后运行相关测试;失败反馈用于修正,通过后继续完整验收。
A[提出编辑] --> B{入口检查}
B -->|入口不合适| C[返回原因与正确路径]
C --> A
B -->|继续权限流程| D[执行编辑]
D --> E[运行相关测试]
E --> F{结果}
F -->|失败| G[返回具体差异]
G --> A
F -->|通过| H[继续任务与完整验收]
校验逻辑适合放在普通项目脚本中,Hook 负责触发与反馈。这样开发者手动执行、Agent 执行与 CI 可以复用同一套逻辑。
每次编辑只运行与改动相关的快速检查,阶段结束再运行完整检查,可以控制反馈成本。异步检查则要确认任务结束前已获得结果,避免程序还在运行,Agent 就宣布完成。
一个调用前检查的具体实现
下面以 Claude Code 命令型 PreToolUse 为例,使用 Node.js 24 和 POSIX Shell。把配置合并到项目的 .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(Edit|Write)$",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/check-file.cjs\""
}
]
}
]
}
}
处理程序 .claude/hooks/check-file.cjs 从标准输入读取参数,向标准输出返回 JSON 决策:
const fs = require('node:fs');
const path = require('node:path');
function deny(reason) {
return {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: reason,
},
};
}
let result;
try {
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
const filePath = input.tool_input?.file_path;
if (typeof filePath !== 'string' || !filePath.length) {
throw new Error('缺少文件路径');
}
const root = path.resolve(process.env.CLAUDE_PROJECT_DIR || process.cwd());
const relative = path.relative(root, path.resolve(root, filePath));
const generated = relative === 'generated'
|| relative.startsWith(`generated${path.sep}`);
result = generated
? deny('请修改源定义,再运行 npm run generate 更新 generated/。')
: {};
} catch {
result = deny('无法解析文件操作参数,请检查 Hook 输入。');
}
process.stdout.write(JSON.stringify(result));
宿主传入下面的参数时,脚本返回 permissionDecision: "deny",并说明正确修改方式:
{
"tool_name": "Write",
"tool_input": { "file_path": "generated/client.ts" }
}
将路径换成 src/client.ts,返回 {},表示这条 Hook 不额外作出决策,继续原有权限流程。无效 JSON 或缺少路径会返回拒绝原因。脚本正常输出决策 JSON 时退出码为 0,宿主读取其中的字段。详细契约见 Claude Code 调用前决策。
这个检查只覆盖 Edit、Write 的路径参数,用于纠正直接修改生成文件的操作。Shell 写文件和符号链接目标没有纳入检查。真正限制文件访问,需要在执行环境中覆盖相应入口。
检查成功必须对应当前版本
假设完整检查在版本 A 上通过,随后 Agent 又修改了响应映射,当前文件已经变成版本 B。版本 A 的成功记录不能继续用来证明版本 B 已通过检查。
因此,结束检查需要关心检查对象、命令结果,以及检查后代码是否变化。提交后的检查可以关联提交标识;未提交改动则需要内容指纹等能覆盖相关文件的方法。
Hook 自身失败时的行为也要确认。不同事件、输出和失败配置会影响是否继续;不能仅凭脚本报错推断动作已被阻止。Codex 与 Claude Code 均提供生命周期 Hook,配置和决策契约应分别核对。Codex Hooks、Claude Code Hooks
对于网络不可用、依赖服务故障等暂时无法解决的问题,结束检查需要有报告阻塞的路径。无限要求继续,只会反复消耗执行时间。
十、用 Git 管理改动、评审和并行工作
让每次改动有清楚的边界
开始前读取 Git 状态,能够识别用户已有修改与当前分支。Agent 应在此基础上增加本次变更,避免通过整文件覆盖或回退操作丢掉其他工作。
完成后先查看 diff,再按文件或具体改动暂存。提交记录适合对应一个能解释清楚的变化,例如“订单响应新增展示状态并保留旧字段”,让后续定位、回退与评审都有明确对象。
本地 Git Hooks 可以在提交或推送前触发检查。尤其要区分工作区与暂存区:文件修改后没有重新暂存,提交的仍是暂存版本;检查工作区文件成功,不一定代表待提交内容正确。Git Hooks
评审需要沿需求阅读差异
评审订单字段变更时,可以沿着一条路径阅读:任务要求保留 status;实现是否仍返回原值;schema 是否允许两字段;测试是否断言兼容性;调用方是否仍可正常解析。
这样能把规格、实现、测试和使用方连起来。只看新代码是否简洁,或只看测试数量,容易漏掉接口语义变化。
还要查看被删除的内容。Agent 可能把难以通过的断言改成更宽松的判断,或顺手删除不理解的分支。对这些修改,要求解释原有约束为何可以改变,并检查依据是否来自本次需求。
AI 可以辅助评审,找到潜在影响点。独立会话也能减少对实现过程的先入印象,但另一个模型的同意仍属于判断线索,具体问题需要用代码路径、复现或测试确认。
并行任务如何避免互相覆盖
多个 Agent 可以分头调查调用方、实现互不重叠的模块或检查不同方面。但同时编辑同一个工作区,容易覆盖文件,也难以判断某次检查对应哪组修改。
Git worktree 可以为同一仓库建立多个工作目录,各自拥有工作树和索引,同时共享仓库的部分数据。Git worktree
例如一个任务实现后端响应,另一个任务更新前端展示,可以先约定接口契约,再分别在不同目录推进,最后集成验证。共享文件或接口变化仍需要协调。
worktree 不会自动隔离数据库、服务端口、缓存目录或云账户。两个开发进程都监听同一端口,或对同一个测试数据库清表,仍会互相影响。并行前要同时分配代码边界和运行资源。
任务很小、依赖很紧或评审能力有限时,串行完成通常更容易控制总成本。并行程度应随可独立性增长。
十一、从本地成功走到持续交付
统一检查入口与远端 CI
项目可以把常用检查集中为稳定入口,例如 npm run check,并明确它具体包含哪些内容。类型检查、测试和构建是不同操作,脚本名称本身不会让它们自动发生。
远端 CI 在独立环境中检出确定版本、安装依赖并运行检查,可以发现本地缓存、未提交文件或环境差异造成的问题。
对于有锁文件的 Node.js 项目,一个检查作业的核心步骤通常是:
npm ci
npm run check
实际工作流还需要配置检出代码、Node.js 版本、触发条件和最小权限。依赖服务、生成步骤与环境变量也要显式准备,避免测试依赖本地机器的隐含状态。
在 GitHub Actions 中,PR 检查与直接推送检查的触发对象不同;使用合并队列时,还需要为 merge_group 配置相应触发,让候选合并得到验证。工作流触发事件
让检查结果成为合并条件
工作流运行失败会产生结果,但是否阻止合并,还取决于分支保护或规则集。需要将对应检查设置为必需条件,并配置评审与绕过权限。受保护分支
检查内容也要维护。若任务同时修改实现和检查脚本,评审应确认检查仍验证原来的承诺。否则把失败条件删掉,也会得到绿色结果。
本地反馈、远端检查和评审承担不同工作:本地尽早发现错误,CI 在明确环境下重复验证,评审确认检查与代码仍符合需求。它们互相补充。
部署、回退与数据变化
合并以后,还要从确定版本生成产物,部署到指定环境,确认用户实际使用的版本,并运行必要的冒烟检查。
对于订单展示字段,部署后可以用测试账户请求接口,确认新旧字段都在,再操作页面检查展示。发布记录应能关联代码版本与产物,方便发现问题时定位和回退。
如果涉及数据库迁移,回退更复杂。假设把 name 拆成 first_name 和 last_name,直接删旧列可能让仍在运行的旧程序报错。常见迁移思路是先增加兼容结构,部署能处理过渡状态的代码,回填并核对数据,迁移调用方,最后清理旧结构。具体阶段取决于业务含义和部署方式。
代码回退不会自动恢复已删除或转换的数据。迁移前需要明确备份、恢复、重复执行和新旧版本并存方式。Agent 可以编写与验证迁移脚本,执行到哪个环境仍需遵循相应发布授权。
编码和发布身份可以分开。GitHub Environments 等机制能为部署配置环境与保护规则,具体可用条件应结合仓库和配置确认。部署环境
十二、走通一次完整的接口变更
前面分别解释了组件,现在把它们放回一个任务。假设订单服务已经存在,旧客户端读取 status,新页面希望直接使用 displayStatus,生成客户端由接口定义产生。
从接手到实现
Agent 首先检查分支和已有修改,阅读项目规则,定位响应映射、接口定义、调用方和测试。它确认现有服务返回 status,基线用例正常,并发现客户端由源定义生成。
根据前文的任务规格,目标响应明确为:
{
"id": "A1001",
"status": "PAID",
"displayStatus": "已支付"
}
接着补充状态映射与兼容性用例。新增字段尚未实现时,新用例失败;实现映射以后,四项单元用例通过。
如果 Agent 尝试直接编辑生成客户端,入口 Hook 提示修改源定义。它随后更新接口定义,运行生成器,再查看生成差异,确认没有带入无关变化。
这几步的产物分别是:明确的响应契约、局部实现、针对契约的用例,以及由源定义生成的客户端。规则、Skill 与 Hook 在相应位置提供帮助,实际行为仍由文件工具、终端和生成器完成。
从局部验证到集成
随后运行真实接口的契约用例。假设此时响应仍缺少 displayStatus,Agent 读取失败断言并定位到序列化 schema。它补齐 schema,再次请求接口,确认字段到达客户端。
这次反馈验证了函数测试之外的连接关系。之后再检查旧客户端的解析路径、新页面展示、未知状态处理,并运行项目完整检查。
最终 diff 应能清楚说明各项修改:映射产生新字段,接口定义声明字段,schema 允许字段通过,客户端和页面使用字段,测试分别覆盖局部与接口行为。评审还要核对旧字段的值和语义保持不变。
从提交到用户可用
按任务授权提交和推送后,CI 检查候选合并。通过所需评审与合并条件,再进入部署流程。测试环境验证新旧调用方式,生产部署后确认实际版本和页面结果。
如果任务只授权本地修改,流程就停在本地交付,并说明后续阶段尚未执行。完成说明可以按实际情况写成:
已完成:响应映射、接口定义、生成客户端及相关页面更新。
已验证:状态映射、真实接口契约、旧客户端解析与页面展示。
仓库状态:已提交并推送,CI 通过。
发布状态:尚未部署。
这段是交付说明格式示例。实际任务必须填写真实执行过的阶段,未运行的检查也应明确列出。
从输入到结果看,完整链路是:人明确行为与边界,Agent 读取项目知识并选择操作,宿主和工具执行修改,检查返回具体反馈,评审确认契约,发布系统把确定版本交给用户。每个环节都有明确对象,才能把“看起来完成”变成可追踪的交付状态。
十三、长期使用中的成本与维护
按任务选择自主程度
一个输入输出清楚、已有测试、影响范围小的改动,适合让 Agent 连续完成实现与验证。跨模块设计、权限变化、不可逆的数据操作,则需要更早确定关键决定,并在相应执行边界控制授权。
这不要求每个文件修改都询问。可以一次明确允许修改的范围、需要保持的行为、验证方式和交付阶段,让 Agent 在这些边界内自行推进;确实缺少关键业务决定时,再提出具体问题。
对不熟悉开发的人,较合适的起点是模拟数据、本地运行、容易检查结果的小工具。随着功能进入长期使用,再逐步理解结构、补充验证和发布方式。遇到凭据、真实业务数据或多人使用的权限模型时,需要具备相应判断能力的人参与设计与评审。
评价总投入与交付质量
生成代码的速度只覆盖开发的一部分。比较不同模型、规则或工作方式时,还要记录需求澄清、上下文准备、执行等待、评审、返工和后续修复的投入。
可以用一个简单的核算方式:
任务总投入 = 需求与上下文准备 + 实现与等待 + 验证评审 + 返工维护
这是分析任务成本的分解方式。若要判断 AI 是否节省时间,应在相近任务和相同质量要求下比较,不能只依据输出长度或代码生成耗时。
对于同一项目,可以记录任务完成周期、首次验收结果、返工原因、上线缺陷和模型使用成本。先积累真实记录,再决定是否调整模型、增加并行或补充自动化。
把重复问题转化为项目能力
如果 Agent 经常找错入口,补充项目地图与设计原因;如果经常重复同一套操作,整理成 Skill 或脚本;如果错误到最后才发现,把相关快速检查放进执行过程;如果失败代码仍能合并,检查远端必需条件与绕过配置。
每一项新增规则都应对应一个明确问题,并有维护位置。目录改了,更新路径;测试脚本改了,同步文档;业务契约变了,更新规格、实现和用例。
一个持续改进的项目,会逐渐拥有清楚的知识入口、可复用的操作和有效的检查。自然语言可以继续承担需求与反馈的入口,代码、测试和发布记录则把这些意图保存为后续开发能够理解和延续的成果。
附:开始一个任务时可以怎样描述
下面的模板适合已有仓库,按任务删减,不需要每次填写完整文档:
目标:希望增加或修复什么用户可观察的行为。
背景:相关入口、已有约定和可核实材料。
需要保持:旧接口、数据含义、页面行为等兼容要求。
范围:允许修改的模块,以及这次不涉及的相邻内容。
验收:具体输入、预期结果、关键边界和可运行检查。
交付阶段:本地修改、提交、推送或部署到指定环境。
先检查实际仓库和已有修改,再依据现有结构推进。
复杂任务先梳理必要设计与依赖,按可运行的小步实现。
发现关键业务信息缺失时指出具体问题,其余工作自行推进。
完成后说明实际差异、验证证据、仓库状态和发布状态。
模板的作用是帮助表达约定。真正使后续协作有效的,是其中的信息能与当前代码、检查结果和交付过程对应起来。