# AI 辅助开发的方法

## 把 AI 当作可以分工、需要验收的开发协作者

本章目标是 L3：你负责界定问题、选择方案和接受结果，AI 帮助调查、实现、解释和验证。多年开发经验最有价值的部分是对需求和边界的判断。转型时应把这种判断延伸到后端与 AI 系统，而不是只追求一次生成更多文件。

一个高质量的协作请求包含目标、上下文、约束、输入输出和验收证据。它不必很长，但需要具体。让 AI “写一个完整知识库系统”会把大量未明确决定交给它；先确定一次资料上传的状态和接口，再让它完成这一条路径，通常更容易审查。

## 什么上下文真正有用

提供当前代码、相关接口、错误日志、复现步骤和项目约定。明确哪些信息已经证实，哪些是你的猜测。不要把整仓库或全部聊天历史都塞进去，然后假设模型一定能抓住重点；上下文越多，越需要结构和优先级。

对于跨页面功能，先给出字段链：用户看到什么、服务端字段是什么、类型如何定义、空值如何展示。对于 Bug，提供最小复现和预期行为。对于学习，让 AI 先讲 API 契约，再给例子，避免用一串未解释代码制造“已经懂了”的感觉。

## 一份可直接复用的任务说明

下面是协作文本，保存到任务记录即可，无需运行。它的每个字段都对应一个常见风险。

```markdown
# 为文档上传增加异步解析状态

## 当前事实
- 上传接口会返回 documentId，解析目前在同一请求内完成。
- 大文件会导致网关超时，前端无法知道是否已保存。

## 目标
上传成功后立即展示“解析中”，完成后展示“可检索”。

## 约束
- 用户只能查看自己有权限的文档状态。
- 保留已有文件格式，不改变无关页面。
- 解析失败可重试，但重复请求不能创建重复解析任务。

## 交付
先说明状态模型和接口契约，再实现必要改动。
对成功、失败、无权限、重复重试给出验证结果。

## 未确定的问题
文档更新时是否继续保留上一版可用索引？请明确影响。
```

当前事实限定 AI 的推理起点；目标避免它解决相邻但不同的问题；约束保护兼容性与权限；交付让“完成”有可观察标准；未确定问题防止猜测被静默固化进代码。这类文档也适合自己写代码时使用。

## 工作循环：理解、实现、验证、收敛

先要求 AI 说明根因或方案依据，必要时直接指向代码位置。然后限定一个可审查的改动范围。收到结果后审查 diff，而不只读总结：新增依赖为什么需要、错误处理是否吞掉异常、测试是否真的触发了目标行为、是否修改无关文件。

测试应针对可观察行为，不要把实现逐字复刻为测试。比如要证明用户无法读别人的文档，应构造两个用户请求；仅断言某函数名存在没有意义。AI 可能同时写错实现和测试，因此测试设计也需要你的判断。

```bash
# 查看本次变动的范围与内容，以下命令不修改历史。
git status --short
git diff --stat
git diff

# 在项目已经定义对应脚本时执行，不存在的脚本不能假设有效。
npm run typecheck
npm test
```

这些命令用于获得证据，不是固定仪式。纯文案修改与事务逻辑修改需要不同验证范围。明确列出“通过、失败、未执行”三类结果，不能把工具启动失败写成测试通过。代码可以由 AI 生成，执行结果必须来自真实运行。

## 如何追问，才能补上理解缺口

当你看不懂一段代码，可以问：“这个参数是谁提供的？为空会怎样？返回的是立即值还是 Promise？异常在哪一层被处理？删除这一行会出现什么可复现问题？”这些问题比“解释一下全部代码”更容易获得可验证的理解。

当 AI 给出一个结论，要求区分源码事实、官方文档和推断。对于新版本 API，要求核对官方资料，并记录版本。第三方文章可以帮助理解，但不能证明当前接口契约。不要因为一段代码看起来熟悉，就跳过版本与运行环境检查。

## 并行协作何时有效

可以把稳定接口下的独立组件、教材章节、只读调查拆给多个协作者。不要同时让两个人改共享类型、路由和全局样式却没有边界约定。并行带来的速度必须扣除合并和验收成本。

分工单要写出拥有的文件范围、输入输出、不能修改的区域和完成条件。主负责人统一术语、错误码、依赖版本和用户体验。每个子任务“自己测试通过”仍然不能替代集成后的总体验收。

## 常见误区

**运行起来就算完成**：只能证明启动路径，不能证明鉴权、并发和失败恢复。**一次让 AI 重写整个模块**：diff 难以检查，原本正确的行为容易被覆盖。**把密钥和私人数据直接贴入上下文**：应使用脱敏日志和必要片段。**错误后连续要求再试一次**：如果没有新证据，通常只是在改变表面实现。

更好的排障方式是每轮记录一个假设和一个实验。例如“代理缓冲了流”可以通过直连后端与经过代理的首事件时间比较来验证；“模型太慢”则需要模型请求的独立耗时证据。不要让前端症状自动变成后端结论。

## 真实任务拆解：文档更新后一直显示“解析中”

假设你接到一个反馈：用户上传资料后，列表长期显示解析中，刷新偶尔又恢复正常。这个现象还不能证明解析器有问题。可能是后台确实未完成，也可能是状态查询缓存未失效，或者旧请求覆盖了新请求的结果。你的第一项工作是缩小问题空间，让 AI 调查的对象可定位，而不是立即要求重写上传组件。

先记录具体文档、当前版本、解析任务编号和观察时间。前端保存发起上传、收到响应、开始轮询、收到状态和停止轮询的事件；后端查询该任务的实际状态。日志不需要完整文档正文，只需足够关联的标识。若后台已完成而页面仍解析中，调查重点就转到接口返回、状态映射和前端更新条件。

再提出可以被实验推翻的假设。假设一是请求乱序：旧轮询较晚返回，覆盖了新状态；可以通过延迟不同响应复现。假设二是版本混用：用户上传第二版后仍在查询第一版任务；可以比较前端活跃版本与服务器任务所属版本。假设三是后台任务未提交成功；可以查任务表和错误记录。三种假设对应不同证据，不应同时大范围改动。

给 AI 的调查任务可以明确为：只读追踪 documentId、versionId、jobId 从上传响应到列表状态的字段链；列出每个赋值位置和可能的旧结果覆盖；先不改变接口与数据结构。调查完成后，你再决定修复前端关联、后端返回合同还是任务创建事务。把“调查”和“实现”区分开，可以避免未经证实的猜测直接变成代码。

## 从业务规则写出接口合同和状态不变量

业务首先要决定更新资料时怎样展示。旧版仍可检索、同时新版解析中，与更新后整份资料暂不可用，是两种不同产品语义。它会影响 Document 的当前可用版本、正在处理版本，以及列表和问答页面的展示。AI 可以解释两种方案的后果，但不能因为某种代码更容易写就替业务默默选择。

接口合同应把这些含义展开。例如上传响应返回文档标识、新版本标识和任务标识；状态响应说明任务属于哪个版本；问答使用已发布的索引版本。这里是自建应用的设计示例，不是任何外部平台的固定 API。字段名称可以改变，语义必须一致，尤其不能让一个模糊的 status 同时表示文件上传、解析和索引可用。

你可以先写几个不变量作为实现依据：其他文档的事件不能修改当前行；旧版本任务不能覆盖新版本；同一任务的重复事件不能重复应用；终态不能被旧进度回退；无权限响应不能显示为“没有资料”。不变量描述必须始终成立的事实，比要求 AI “做好状态管理”更容易形成可验收实现。

再把规则映射到数据来源。userId 从验证过的会话取得，versionId 与 jobId 来自服务器创建结果，前端请求序号只管理自己的异步关联。不能让前端自行生成一个伪版本并假设数据库也存在。类型定义只是这个合同的编码表现，后端仍要校验实际输入，前端也要处理缺失和未知状态。

## 一个完整的离线状态实验

下面例子帮助你审查“乱序覆盖”的修复思路。保存为 document-state.mjs，Node.js 22，无依赖、无网络。它是应用内部状态函数，不是已经实现的上传服务。教材中的标识和事件都是教学数据。

```javascript
// document-state.mjs
import assert from "node:assert/strict";

const transitions = {
  queued: new Set(["processing", "failed"]),
  processing: new Set(["ready", "failed"]),
  ready: new Set(),
  failed: new Set()
};
function applyEvent(state, event) {
  if (event.documentId !== state.documentId ||
      event.versionId !== state.versionId ||
      event.jobId !== state.jobId) return state;
  if (!Number.isSafeInteger(event.sequence) || event.sequence < 0) {
    throw new Error("事件序号无效");
  }
  if (event.sequence <= state.lastSequence) return state;
  if (!Object.hasOwn(transitions, event.phase)) throw new Error("未知任务状态");
  if (event.phase !== state.phase && !transitions[state.phase].has(event.phase)) {
    throw new Error("非法状态转移");
  }
  return { ...state, phase: event.phase, lastSequence: event.sequence };
}
const current = {
  documentId: "d1", versionId: "v2", jobId: "j2",
  phase: "processing", lastSequence: 3
};
const old = {
  documentId: "d1", versionId: "v1", jobId: "j1",
  phase: "ready", sequence: 999
};
assert.equal(applyEvent(current, old), current);
const completed = applyEvent(current, {
  documentId: "d1", versionId: "v2", jobId: "j2",
  phase: "ready", sequence: 4
});
assert.equal(completed.phase, "ready");
assert.equal(applyEvent(completed, {
  documentId: "d1", versionId: "v2", jobId: "j2",
  phase: "processing", sequence: 3
}), completed);
assert.throws(() => applyEvent(completed, {
  documentId: "d1", versionId: "v2", jobId: "j2",
  phase: "processing", sequence: 5
}), /非法状态转移/);
console.log("旧版本、重复事件和终态回退检查通过");
```

执行 node document-state.mjs，预期输出检查通过。将第一段三种标识检查删除，旧版本 ready 事件就会错误修改当前状态，对应断言应失败。这是一个可控制的红绿实验：测试先证明会捕获目标错误，再证明修复使它通过，而不是因为测试文件存在就算有回归保护。

sequence 必须由真实协议定义。如果服务端只是返回无序状态快照，就不能照抄这个序号设计并假装它有全局顺序。你可以采用请求代次、对象版本或服务器事件序号，各自解决的问题不同。本例假设同一任务事件有单调序号，目的是让这一前提可见。重试创建新 jobId 时，应用还需要单独切换活跃任务，不能把旧任务复活。

## 怎样审查 AI 提交的修改，而不是只看最终页面

先检查改动范围是否与根因一致。乱序更新问题通常可以在请求关联和状态更新处修复；若 AI 同时更换状态库、删除原接口层和引入新依赖，应该要求解释必要性。改动越大并不代表方案越完整，反而增加你必须重新验证的行为数量。

然后从用户入口顺着数据走一遍：事件由谁创建、携带什么字段、在哪一层校验、何时写入状态、失败如何结束。检查正常路径之外的分支，尤其是 catch、finally、清理函数和默认值。把异常转换为空数组可能使页面“看起来没报错”，却隐藏服务失败；在 finally 无条件重置共享 loading 也可能让并发请求状态混乱。

测试也要沿行为检查。断言请求函数被调用一次，只证明调用次数；断言旧版本事件不会改变当前状态，才对应这个任务的风险。测试输入应能使错误实现失败，而不是仅验证一份写死 fixture。对于后端权限，应使用不同身份和资源关系；对于事务，应注入写入中途失败，而不是只检查成功记录存在。

查看 AI 的验证报告时，把命令、执行目录、退出码和关键输出对应起来。依赖未安装导致测试启动失败，与测试断言失败是不同情况；浏览器跳回登录页，与目标页面验收通过也不同。你可以接受某些验证暂时受阻，但必须知道还缺什么证据，不能让“应该没问题”替代实际结果。

## 接管不熟悉代码的学习方法

接管 AI 代码不要求你背下每行实现，而是能解释模块的输入、输出、不变量、外部依赖和失败行为。先从一个真实请求走通，画出简短调用链，再选择其中最不熟悉的一层读官方文档。这样学习服务于当前任务，既不会停留在只会运行，也不会陷入无限阅读所有底层实现。

对每个陌生 API，至少确认参数、返回值、默认行为和异常。比如一个函数返回 Response 还是已经解析的 JSON，会决定工具循环能否读取 output；一个取消信号是否被底层库实际处理，会决定页面取消后任务是否继续。把这些合同写在模块边界，比让 AI 生成一大段泛泛解释更有用。

可以使用反事实问题检查理解：“去掉这个权限条件会让哪条测试失败”“同一响应到达两次会发生什么”“数据库成功后网络断开，用户怎样恢复”。如果你只能复述注释，无法预测改动后果，说明还没掌握边界。让 AI 构造小实验，然后自己先预测结果再运行，是有效的练习方式。

学习阶段可以要求 AI 延迟给出答案：先提供一个具体练习和验收条件，你完成后再对照实现。遇到错误时先给一个定位提示，再根据证据补充解释。这样的节奏能保留你的推理过程；让 AI 同时给题目、答案和“你已经掌握了”的评价，只会制造完成感。

## 把需求拆成可以集成的工作包

适合独立分工的任务通常共享已经稳定的合同。例如接口和状态枚举已确定，可以分别实现列表展示、后台状态查询和只读测试设计。若三个任务都需要决定字段含义或修改共享类型，实际上还没有达到可并行条件，应先由主负责人收敛合同。

工作包应写明拥有的文件、输入输出、不可改动区域、依赖的版本和验收方式。完成后主负责人检查接口是否真的一致，再运行跨模块流程。各自局部测试通过不保证集成正确：前端可能把空引用当空数组，后端可能返回 null；两边都有测试，组合起来仍然会出错。

集成时保留每个工作包的实际变化和未验证项。发现冲突不要简单选择“更新的一边”，应该回到业务语义判断。一个代理可能修复请求取消，另一个可能新增重试；合并后若取消触发重试，就必须重新验证两种能力的组合。并行收益来自独立性，不来自代理数量。

## 综合练习：写一份能接管的修复交付说明

用本章文档状态案例完成交付说明，要求包含根因证据、合同前提、修改范围、红绿验证、未覆盖边界和后续观察。不要写“优化了状态管理”这类无法核查的概括。

<details><summary>参考交付实现</summary>

根因：第二版解析期间，第一版任务的较晚响应进入同一更新函数，因未检查 versionId 和 jobId，覆盖了当前行状态。证据是延迟第一版响应后可以稳定复现，而服务器第二版任务实际仍在处理中。

合同：同一任务事件具有递增 sequence；不同任务序号不可比较。修改仅限状态更新函数和对应行为测试，不改变上传协议及文档权限规则。增加文档、版本、任务三重关联，忽略旧序号，拒绝终态回退。

验证：删除关联检查时，旧版本事件断言失败；恢复检查后，旧版本、重复事件和终态回退检查通过。真实 HTTP 轮询、浏览器刷新恢复和后台重试尚需在项目环境测试，本离线实验不能证明这些已完成。

接管要求：维护者能解释为何不能只比较 sequence，能指出服务器没有顺序保证时需要重新设计合同，并能用相同案例验证未来重构。上线或本地实训观察应记录任务标识和状态变化，不记录完整用户文档。

</details>


## 练习

把“帮我修一下知识库回答不准确”改写成一个可执行任务。包含至少两个可能原因、每个原因的验证方法，以及你允许修改的范围。

<details>
<summary>参考答案</summary>

先准备三条有已知来源的问题，记录原始查询、检索片段与最终回答。假设一是检索遗漏：检查正确文档是否进入候选集；假设二是生成未遵守证据：在提供正确片段的条件下单独测试生成。先限定只读诊断，确认问题层次后再修改分块、排序或提示词，并在同一问题集上比较结果。

</details>

## 验收与自测

- 能给 AI 一个包含事实、约束、交付与未知项的任务。
- 能审查一次 diff，并指出至少一个需要独立验证的判断。
- 能接管 AI 生成但运行失败的代码，提出下一次有价值的实验。

**问：AI 写了测试，是否还需要你审查测试？** 答：需要，测试可能和实现共享同一个错误假设。

**问：所有任务都应多代理并行吗？** 答：只有边界独立、接口明确时才可能节约整体时间。

**问：AI 的总结和终端结果冲突时相信哪个？** 答：以实际运行证据为准，并调查差异。

## 官方参考

- [Git diff 文档](https://git-scm.com/docs/git-diff)
- [Node：测试运行器](https://nodejs.org/api/test.html)
- [Anthropic：有效 Agent 的工程原则](https://www.anthropic.com/engineering/building-effective-agents)
