第一次用 AI 改代码的人常撞到同一个结果:原本能跑的模块,被"顺手优化"之后测试红了,而你说不清它动了哪一行。怪模型没意义——真正的问题是这次改动的半径超过了你愿意核对的半径。AI 编程的工作流不是"让模型写代码",而是把一次改动切到你能验证的大小,然后对它负责。这篇按一次任务的生命周期排:选入口 → 给上下文 → 写任务 → 小步交付 → 验证 → 读 diff → 提交。
1. 症状对照:坏在哪一步
| 现象 | 真实原因 | 对应环节 |
|---|---|---|
| 改完能跑,但写法和你项目格格不入 | 没给既有抽象与约定 | 上下文 |
| 一次改了 8 个文件,review 不动 | 任务粒度大于核对能力 | 交付节奏 |
| 报错说某个 API 不存在 | 幻觉,产物没人跑过 | 验证 |
| 功能对了,但两处无关逻辑被删 | 只看了新增行 | 读 diff |
| "快了一倍",两周后返工 | 只算了打字时间 | 收益核算 |
这五条都能在后面的环节里找到对应动作。
2. 四类入口各自擅长什么
| 入口 | 形态 | 擅长 | 不擅长 |
|---|---|---|---|
| 内联补全 | 光标处续写 | 重复模式、样板、局部改写 | 跨文件重构、需要全局理解 |
| 对话 | 贴代码问答 | 解释报错、比较方案、写独立函数 | 修改它看不见的文件 |
| 编辑器内 Agent | 多文件编辑 + 调工具 | 有明确验收标准的改动 | 目标含糊、范围开放的任务 |
| 命令行 Agent | 在仓库里自己跑命令 | 批量替换、跑测试、修流水线 | 需要人做取舍的判断 |
选错入口的代价不是"效果差一点",而是你拿到一份无法验证的产物:补全给你一行,你能读;Agent 给你一次提交,你得有能力审。
3. 补全的适用面与禁区
补全的强项是模式延续:你已经写了三遍的东西,第四遍让它续。弱项是需要判断力的地方。
# 好:意图写在注释里,补全只需延续结构
# 把 payload 里的字段名统一转成 snake_case,保持与既有 to_snake 一致
def normalize_keys(payload: dict) -> dict:
...
# 坏:注释只说"优化一下",模型只能猜
# TODO 优化
def normalize_keys(payload: dict) -> dict:
...
| 场景 | 用补全 | 理由 |
|---|---|---|
| 写第 N 个相似的 DTO/字段映射 | 是 | 模式稳定,错了肉眼可见 |
补 __repr__、日志语句、参数校验 |
是 | 局部、可读、无副作用 |
| 跨 5 个文件重命名一个概念 | 否 | 需要全局一致性,用 Agent 或 IDE 重构 |
| 改并发/事务/缓存失效逻辑 | 否 | 正确性依赖上下文,必须自己写或逐行审 |
| 不熟的框架 API | 否 | 补全最爱在此处编造方法名 |
4. 上下文:给什么,给多少
"给太多等于没给"不是比喻。上下文里塞进十几个无关文件,模型的注意力被稀释,会开始在错误的地方找相似模式——它模仿了隔壁模块的写法,而那个模块的约定恰好是你要避开的。
| 必给 | 给了更好 | 给了有害 |
|---|---|---|
| 要改的文件全文 | 直接调用方的签名 | 整个仓库目录树 |
| 完整报错栈(含行号) | 相邻的同类实现 | 与任务无关的历史代码 |
| 项目约定(命名、分层、错误处理) | 相关测试文件 | 大段被注释掉的旧版本 |
| 验收方式(跑哪条命令) | 依赖版本 | 三份互相矛盾的规范文档 |
【上下文包】
1 目标文件:src/order/refund_service.py(全文)
2 调用方:api/refund.py 第 42-58 行
3 报错:TypeError: unsupported operand ... (贴原始栈,别转述)
4 约定:金额用 Decimal;错误统一抛 BizError;不引入新依赖
5 验收:pytest tests/order/test_refund.py -q
第 4 条最容易漏。你不写"不引入新依赖",它就会给你装一个库;你不写"错误抛 BizError",它就返回 None。
5. 把需求写成任务:四要素
模糊指令的典型形态是"帮我改一下这个退款逻辑"。它缺的是可判定性。一个可交付的任务包含四段:
目标:退款金额为 0 时不要调用支付网关,直接置为成功
约束:只改 refund_service.apply();保持现有函数签名;
金额用 Decimal;不新增依赖;不动 api 层
验收:新增用例 test_zero_amount_skips_gateway 通过;
现有 12 个退款用例全绿
不要做:不要重命名方法、不要调整日志格式、不要顺手修 lint
| 要素 | 缺了会怎样 |
|---|---|
| 目标 | 它改了一个"相关"的地方,功能没动 |
| 约束 | 改动扩散到全仓,review 成本爆炸 |
| 验收 | 你说不清算不算完成,来回拉锯 |
| 不要做 | 顺手重构把无关行为改了 |
"不要做"这一条看着多余,实际是最省时间的一条:模型倾向于"既然来了就一起改好",而你的 review 预算只够看一处。
6. 小步交付:一次一个可验证的点
# 任务拆成三次提交,每次都能单独回滚
git switch -c feat/refund-zero
# 第 1 步:只加用例,跑红
git commit -am "test: zero amount skips gateway (red)"
# 第 2 步:只改 apply() 的一个分支,跑绿
git commit -am "fix: skip gateway when amount is zero"
# 第 3 步:只补日志,不影响行为
git commit -am "log: record zero-amount refund"
判断粒度是否合适:这次 diff 你能否在 5 分钟内逐行读完并说出每行的意图。读不完就继续拆。要它"重写整个模块"的请求,产出必然是一份你只能整体接受或整体拒绝的东西,而这两者都不是 review。
7. 验证责任:不跑就用等于赌博
# 拿到产物后的固定动作,顺序不要换
git diff --stat # 先看波及面
python -m compileall src # 最低成本的语法/导入检查
ruff check src # 或项目既有 linter
pytest tests/order -q # 相关用例
pytest -q # 全量,确认没碰坏别处
顺序的理由:--stat 在最便宜的位置拦住"改了 20 个文件";编译检查拦住幻觉 API;linter 拦住风格漂移;相关用例定位行为;全量用例抓那些"看起来无关"的连带破坏。
跳过任何一步,你都在用"它说它改了"替代"我确认它改了"。AI 的自我描述不是证据,命令输出才是。
8. 读 diff:先看它删了什么
git diff -U0 # 只看变更行,噪音最少
git diff --diff-filter=D # 只看被删除的文件
git diff | grep '^-' | grep -v '^---' # 只看删除行
大多数人从新增行读起,因为那是"它做的事"。真正的风险在删除行:被删掉的一个 if、一行 return、一个 await,都能让功能在正常路径下照样跑通,只在边界条件下炸。
| 先看 | 找什么 |
|---|---|
| 删除行 | 有没有删掉守卫条件、错误分支、并发保护 |
| 新增依赖 | 是否引入了项目没在用的库 |
| 新增函数 | 是不是重复实现了已有的工具方法 |
| 改动范围 | 是否越过了任务声明的边界 |
最后一条是隐形陷阱:产物把断言从 == 100 改成 != 0,测试就绿了,而行为已经错了。
9. 三个真实失败模式
| 模式 | 表现 | 识别与处置 |
|---|---|---|
| 幻觉 API | 调用不存在的方法、参数名错、版本对不上 | 跑编译或类型检查;查官方文档核对签名 |
| 静默改动无关代码 | diff 里混进格式化、重命名、注释删除 | git diff --stat 先扫文件数;按块 review |
| 绕开既有抽象 | 直接 new 客户端、绕过 repository、复制粘贴而非复用 | review 时问"这行为什么没走 X";要求它先读该抽象 |
三个模式的共同点是:产物本身常常能跑。所以靠运行来发现问题是不够的,必须配合 diff 阅读与对项目约定的先验知识。这也是为什么"不熟的领域不要靠 AI 学"——你没有先验,就识别不出绕开抽象这件事。
10. 提交粒度、review 关注点与真实收益
| review 关注点 | 具体动作 |
|---|---|
| 边界与错误路径 | 只喂异常输入,不看正常路径 |
| 抽象一致性 | 是否复用既有 service/repository/util |
| 隐式行为变化 | 日志、异常类型、返回值语义有没有变 |
| 可测试性 | 新增逻辑能否被单独构造用例 |
| 收益类型 | 真实吗 | 说明 |
|---|---|---|
| 打字速度 | 真,但占比小 | 编码时间里打字本来就不是大头 |
| 样板与重复代码 | 真,收益稳定 | 越模板化的仓库收益越高 |
| 陌生 API 的上手时间 | 真,但要核对 | 省的是查文档,代价是验证 |
| 返工减少 | 假,除非你加了验证 | 不跑测试的"完成"只是看起来完成 |
| 减少思考 | 假 | 判断、取舍、边界设计仍然全是你的活 |
把"打字快了"当成主要收益,就会得出"AI 让效率翻倍"的错误结论;真实收益集中在机械重复与首版草稿两处,而这两处的下一道工序都是人工核对。
11. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 生成的代码 import 报错 | 幻觉模块名或版本不符 | 查 pip show/go doc 核对真实 API |
| 改了 12 个文件 | 任务约束缺失 | 回退,重发带"只改 X 文件"的任务 |
| 测试通过但线上行为变了 | 断言被改宽 | 看测试文件的 diff,恢复原断言 |
| 它不读你指的抽象 | 只给了文件名没给内容 | 把该抽象的方法签名贴进上下文 |
| 每次产出风格都不一样 | 没给约定 | 在任务里固化命名与错误处理规则 |
12. 边界:这些活别指望工作流救你
| 场景 | 原因 | 替代做法 |
|---|---|---|
| 你不熟的领域 | 你无法验证,等于放弃 review | 先读文档与既有实现,再让 AI 做机械部分 |
| 认证、加密、权限判定 | 错误是静默的,测试难覆盖 | 自己写,或对照成熟实现逐行核 |
| 需要长期维护的核心模块 | 风格与抽象的一致性比速度重要 | 只让 AI 做补全与小改 |
| 性能关键路径 | 需要测量而非生成 | 先 profile,再定向优化 |
一句话收口:AI 提升的是"产出草稿"的速度,"确认草稿正确"的成本仍然在你这边。工作流的全部意义,是让后者的成本可控。
自测
- 一次 Agent 改动涉及 8 个文件,你先用哪条命令判断该不该继续?
- 为什么"不要做"这一段在任务描述里最省时间?
- 上下文里塞进大量无关文件,具体会造成什么偏差?
- 读 diff 为什么建议先看删除行而不是新增行?
- "测试变绿"在什么情况下反而是危险信号?
- 哪两类任务即使工作流再规范也不该主要交给 AI?