AI 编程工作流:从需求拆解到上线回滚
我用 AI 写代码四年,踩过的最大坑不是「模型不会写」,而是「AI 说写完了,就以为它真写完了」。前两年我反复在一个循环里转:AI 生成一段代码 → 看一眼能跑 → 提 PR → 测试环境报错 → 修一下 → 上线生产 → 凌晨收到告警 → 紧急回滚 → 怪 AI 不靠谱。问题从来不在模型,而在我把它当成了「写代码的人」,而它实际是「按指令改文件的人」。该上测试的地方没上,该评审的地方没评,该回滚的地方没准备预案。
这篇文章把这条工作流完整拆开:任务卡拆解 → 读仓库与小步实现 → 真实测试与评审 → 提交/CI/生产三态分离 → 上线后观测与回滚。你照着走一遍,至少能把「AI 写完就上线」的故障率压掉八成。
配套阅读:AI 编程入门实战(从零到第一个能跑的脚本)、AI 编程应用场景(生成、补全、评审、CI/CD 各自用法)、Cursor/Trae/Copilot 对比(工具选型)、Prompt 调试方法(AI 答非所问时怎么定位)、AI 写作工作流(内容侧的同类 SOP)、AI 绘画工作流(图片交付的同类 SOP)、Cursor 进阶技巧(编辑器侧高效用法)、Cursor 入门(基础配置)、Prompt 工程基础(如何写好指令)、AI 编程工具推荐(工具横向对比)、AI 生产力技巧(整体提效清单)、ChatGPT vs Claude(编程模型横向对比)。
交付单位不是「生成的代码」,是「可验证的改动」
把 AI 写代码想成「AI 替我实现了某个功能」,从一开始就跑偏了。AI 真正能稳定交付的,是一个可验证的改动:能跑测试、能通过 lint、能编译通过、能看 diff、能独立回滚。功能、需求、上线是这一连串改动累积起来的结果,而不是单个改动的目标。
这一节先定几件交付前必须想清楚的事。
交付单位 = 一次可回滚的 commit
不要把「实现整个登录重构」当成一个改动。要拆成:
- 抽出 user repository 接口(独立 commit,独立测试)
- 替换一个调用点为接口实现(独立 commit,独立测试)
- 接入新的 token 校验(独立 commit,独立测试)
- 跑全量回归 + 上线 + 监控(合并上线)
每一个 commit 都是「可验证的改动」:能跑自己的测试、能独立回滚、能告诉评审人改了什么。AI 在这种粒度下最稳,因为它只需要回答「这个文件、这处逻辑、这个测试」三件事,而不是「帮我重写登录模块」。
AI 不是写代码的人,是按指令改文件的人
AI 没有「我完成了」的判断力,它只有「我生成了 diff」的执行力。你不问,它不说;你不测,它不验证;你不评审,它就当通过。这一条要写进团队肌肉记忆。
任务卡 = AI 时代的 PRD
任务卡(task card)是这套工作流的核心。每接到一个需求,第一步不是打开 IDE 问 AI「怎么实现」,而是把下面八栏填完:
| 字段 | 含义 | 示例 |
|---|---|---|
| 目标 | 一句话说清这次改动要达成什么 | 「让搜索结果支持按标签过滤」 |
| 用户场景 | 谁在什么场景下会用 | 「运营筛选带某个标签的文章」 |
| 输入/输出 | 输入什么、输出什么数据结构 | 「输入 tag 列表,输出过滤后的文章 ID」 |
| 非目标 | 这次不做的事 | 「不做排序、不做全文搜索、不做权限」 |
| 验收标准 | 怎么算「做完了」 | 「三种标签组合都返回正确条数,空标签返回全部」 |
| 失败处理 | 边界条件 + 异常路径 | 「标签不存在 → 返回空;标签是空字符串 → 返回 400」 |
| 参考资料 | 类似实现 / 依赖版本 / 测试命令 | 「参考 article-service 现有 list 接口、用 jest 跑单测」 |
| 改动范围 | 大概涉及哪几个文件、哪几个接口 | 「新增 SearchFilter,新增 2 个 endpoint,改 1 个 service」 |
填完这张卡,AI 才能真正开工。没有这张卡,AI 会按它自己的理解「自由发挥」,三步之后你就会发现它改了八处你没想到的地方。
第一步:读仓库,再下笔
让 AI 写代码之前,最容易被忽略的一步是:让它先读仓库。不读仓库,AI 写出来的代码几乎一定会踩三类坑:
- 命名不一致:项目里叫
user_id,AI 写成userId - 依赖版本错:项目里用 axios 1.6,AI 写成 fetch 拦截器
- 风格不一致:项目用 4 空格,AI 写成 2 空格
这三类坑看着小,评审起来全是噪音,会让真正的问题被淹没。
让 AI 先读的清单
把下面这些塞进 prompt,强制 AI 在写代码前读完:
写代码前,先读完下面这些:
1. README.md(项目目标 + 启动命令)
2. 项目根的 AGENTS.md / CLAUDE.md / .cursorrules(如有约定)
3. package.json / pyproject.toml / go.mod(依赖版本)
4. 同目录下相邻的两个文件(参考命名 + 风格)
5. 类似功能的一个实现(如有)
6. 测试运行命令(jest / pytest / go test)
读完列一个「我看到了什么」的小结,再开始改。
「读完列小结」这一条是关键——它强迫 AI 把读到的内容显式说出来,避免它读一半就开始写。读不到位的小结会立刻暴露在评审环节。
用 AGENTS.md 把仓库约定固化
读仓库约定最好固化成文件,不要每次都重复 prompt。常见做法:
- 项目根放
AGENTS.md:命名规范、错误处理、日志格式、测试要求 - Cursor 用户用
.cursorrules:IDE 启动时自动注入 - Claude Code 用户用
CLAUDE.md:会话启动时自动读
固化后,AI 不用每次都「先问」,直接按文件里的约定走。这条对老项目特别重要——约定散在历史 commit 和老员工脑子里,AI 没法自动学到。
第二步:小步实现,每步有 diff
让 AI 改代码,永远不要让它「一次性写完整个模块」。要拆成五步小循环,每步独立评审:
小循环的五步
- 数据/API:先定义接口、类型、mock 数据。能跑通「空实现 + mock」。
- 核心逻辑:实现主流程,但保持调用方还是 mock 状态。
- 调用方/UI:把真实调用方接进去,能跑通端到端。
- 错误处理:补异常路径、补日志、补监控埋点。
- 文档:更新 README、注释、CHANGELOG。
每一步独立 commit、独立 review、独立测试。AI 一次性写五步,问题会藏在第五步里,到时候回滚都难拆。
每个 diff 必须能独立解释
评审环节会问「这个 commit 改了什么」。让 AI 在每个 commit 之前先输出三件事:
- 改动文件清单(新增 / 修改 / 删除)
- 每个文件为什么改(一句话)
- 怎么验证(跑哪个测试、看哪个 endpoint)
这三件事要写到 commit message 里。AI 写代码容易,写 commit message 反而更暴露它是否真的理解了改动——如果 commit message 含糊,这个 commit 大概率也含糊。
警惕 AI 的「过度实现」
AI 有一个高频毛病:用户没要的功能它会自动加上。比如「加一个搜索过滤」,它可能顺手加上分页、排序、缓存、统计、导出 CSV。这些全是用户没要的需求,每加一个都是新的测试负担和评审负担。
明确告诉 AI:
只实现我列在任务卡里的功能。
不要加排序、不要加分页、不要加缓存、不要加统计、不要加导出。
如果你觉得需要加,先停下来问我。
第三步:真实测试,AI 不能自证正确
测试是这套工作流最容易塌的环节,因为「AI 写完看着对」给人强烈的「已经测过」的错觉。AI 不能自证正确——它没跑过、没编译过、没断言过、没看到失败。
必须跑的真实测试
下面这些必须跑过、看输出、贴日志,才算「改完了」:
| 测试类型 | 命令 | 检查什么 |
|---|---|---|
| Lint | npm run lint / ruff check / golangci-lint run | 命名、风格、import 顺序 |
| 类型检查 | tsc --noEmit / mypy . | 类型错误、缺失 import |
| 构建 | npm run build / go build / pytest --collect-only | 编译/打包是否通过 |
| 单元测试 | jest / pytest / go test ./... | 业务逻辑边界 |
| 集成测试 | pytest tests/integration | 跨模块/跨服务 |
| 边界测试 | 自定义边界用例 | 空值、极值、并发 |
| 失败测试 | 故意输入错误数据 | 异常路径是否走对 |
| 回归测试 | 跑完整 test suite | 没把别的功能改坏 |
三类必须补的测试
AI 默认不写但必须补的测试:
- 边界用例:空数组、空字符串、null、undefined、极大值、极小值
- 失败用例:输入不合法数据时是否抛错而不是静默返回错值
- 并发用例:两个请求同时改同一资源时是否竞态
这三类 AI 不会主动写,必须在任务卡里显式列:
验收标准里必须包含:
- 空输入返回 400,不返回 []
- 重复请求幂等(连续两次只生效一次)
- 并发 100 请求,最终状态一致
测试输出要落到日报
AI 跑测试的输出全部要贴进 commit message 或日报。理由:
- 评审人要看真实的「passed N / failed M」
- 出了问题能回溯是哪个测试先红
- 防止 AI 撒谎说「测试通过」实际根本没跑
第四步:评审清单,9 件事不能漏
评审环节是这套工作流的最后一道人工闸门。下面 9 件事每件都要看,少一件都可能带病上线。
评审 9 件套
- 范围:diff 是否只改了任务卡列的范围?有没有顺手改无关文件?
- 兼容性:改了接口签名了吗?调用方都更新了吗?
- 密钥:有没有把 API key、token、密码写进代码或日志?
- 日志:关键路径有没有埋点?日志级别是否合适?
- 输入校验:用户输入有没有校验?SQL/XSS/路径穿越防了吗?
- 权限:接口鉴权是否对?权限粒度是否够细?
- 依赖:新加的依赖是否必需?license 是否允许?
- 数据库迁移:有没有 schema 变更?迁移脚本可逆吗?
- 失败路径:异常分支都覆盖了吗?异常信息是否泄露内部细节?
让 AI 写评审纪要
把上面 9 条塞进 prompt,让 AI 自己先回答:
按下面 9 条逐一回答(每条不超过 3 句话):
1. 范围
2. 兼容性
3. 密钥
4. 日志
5. 输入校验
6. 权限
7. 依赖
8. 数据库迁移
9. 失败路径
如果某条不适用,回答「N/A + 为什么」。
AI 自己的回答 + diff = 评审材料。人类评审人只需要在 AI 的回答基础上追问。
三类必须人工评审的改动
下面三类改动 AI 自我评审不可信,必须人工过:
- 认证/授权改动:登录、token、权限判断。AI 容易漏边界。
- 支付/扣费改动:钱相关的逻辑。AI 不懂业务规则。
- 数据删除/迁移改动:删数据、改 schema、AI 容易绕过安全检查。
第五步:提交 / CI / 生产,三态分离
代码进了 git 不等于上线了。「我 push 了」和「上线了」之间隔了 CI 跑通、镜像构建、灰度发布、生产部署、健康检查。把这三态分开看,能避免绝大多数「我以为上线了」的事故。
三态时间线
T0: git commit(本地提交)
T1: git push(远端收到)
T2: CI 通过(自动化测试 + 镜像构建 + 安全扫描)
T3: 灰度发布(1% / 5% / 20% 流量)
T4: 全量发布(100% 流量)
T5: 生产验证(探针 / 业务指标 / 错误率)
每一步都要看,不是「push 了就完事」。
灰度发布不能省
小改动可以一次全量,任何涉及以下场景的改动必须灰度:
- 改接口签名
- 改数据库 schema
- 改缓存 key
- 改权限判断
- 改第三方调用(支付、短信、推送)
灰度的最小做法:先开 1% 流量,盯 10 分钟错误率;再开 10%,盯 30 分钟;最后全量。
生产探针
上线后必须主动验证,不能等用户报错:
| 探针类型 | 命令 / 工具 | 频率 |
|---|---|---|
| 健康检查 | curl /health | 每分钟一次,连 5 分钟 |
| 业务关键路径 | curl /api/search?tag=test | 上线后立即 + 10min 后 |
| 错误率 | 看监控大盘 / Sentry | 上线后 30min 内 |
| 资源占用 | CPU / 内存 / 连接数 | 上线后 30min 内 |
| 业务指标 | 订单量、活跃用户数 | 上线后 1h 内 |
第六步:回滚预案 + 事后复盘
上线前必须准备好回滚预案,不是上线后。
回滚预案四件套
- 回滚命令:比如
kubectl rollout undo deployment/api或git revert HEAD && git push。 - 回滚触发条件:错误率 >1% / 关键接口 5xx >0.5% / 业务指标下跌 >20%。
- 回滚负责人:谁能拍板回滚?周末/夜间怎么联系?
- 回滚验证:回滚后跑什么验证?预期什么结果?
预案要写在部署文档里,不能在群里口头说。
上线后 24 小时内的三件事
- 盯监控:错误率、响应时间、业务指标,至少盯 24 小时。
- 收告警:任何上线后才出现的告警都先排查,不要「先看看再说」。
- 记录:把这次上线的指标(响应时间、错误率、资源占用)和上线前对比,写进 CHANGELOG。
事后复盘清单
上线 1 周后做一次复盘,回答下面 5 个问题:
- 这次上线有没有出问题?是什么问题?
- AI 在哪一步帮上了?哪一步添了乱?
- 任务卡的哪些字段写得到位?哪些没写?
- 评审环节哪几条本来该发现但漏了?
- 下次类似改动,怎么改进任务卡和评审清单?
复盘写到团队的 retros/ 目录里,下次接需求时翻一翻。
完整案例:给内容站加搜索过滤
下面用一个完整案例把上面六步串起来。需求:给一个内容站的文章列表加按标签过滤的功能。
任务卡
目标:让搜索结果支持按标签过滤
用户场景:运营筛选带某个标签的文章
输入/输出:
输入:tag 字符串数组,可选;page / pageSize
输出:{ items: [...], total: N }
非目标:不做排序、不做全文搜索、不做权限
验收标准:
- 三种标签组合都返回正确条数
- 空 tag 返回全部文章
- tag 不存在返回空数组(不报错)
- tag 是空字符串返回 400
- 并发 100 请求,最终状态一致
失败处理:tag 是 null/undefined → 视为空数组
参考资料:
- 现有 article-service list 接口
- jest 跑单测
- DB 用 PostgreSQL,tags 列是 text[]
改动范围:
- 新增 SearchFilter(在 article-service)
- 新增 GET /api/articles?tags=... endpoint
- 改 article-repository.findMany 接收 tags 参数
三步小循环
第一步:定义接口 + mock 数据
// src/services/SearchFilter.ts
export interface SearchParams {
tags?: string[];
page?: number;
pageSize?: number;
}
export interface SearchResult {
items: Article[];
total: number;
}
export class SearchFilter {
filter(params: SearchParams): SearchResult {
// TODO: 第二步实现
return { items: [], total: 0 };
}
}
测试 + commit:feat(search): add SearchFilter interface and empty implementation。
第二步:核心逻辑
filter(params: SearchParams): SearchResult {
const { tags = [], page = 1, pageSize = 20 } = params;
if (tags.some(t => t === '')) {
throw new BadRequestError('empty tag');
}
// 调用 repository
return this.repository.findByTags(tags, page, pageSize);
}
测试 + commit:feat(search): implement SearchFilter core logic。
第三步:接入 endpoint + 真实数据
// src/api/articles.ts
router.get('/articles', async (req, res) => {
const tags = (req.query.tags as string)?.split(',') ?? [];
const result = await searchFilter.filter({ tags, ...req.query });
res.json(result);
});
测试 + commit:feat(search): wire up GET /articles endpoint with tags query。
跑测试 + 贴日志
$ npm run lint
> eslint src/
✓ 0 errors
$ tsc --noEmit
✓ No type errors
$ jest --testPathPattern=search
PASS src/services/__tests__/SearchFilter.test.ts
✓ empty tags returns all articles
✓ single tag filters correctly
✓ multiple tags (AND)
✓ non-existent tag returns []
✓ empty string tag throws 400
✓ concurrent 100 requests consistent
Tests: 6 passed, 6 total
评审回答(AI 草稿 + 人工追问)
1. 范围:只改了 SearchFilter + endpoint + repository.findByTags,没改其他文件。
2. 兼容性:GET /articles 旧调用仍然能用(tags 可选)。
3. 密钥:无。
4. 日志:filter 入口打了 log.info,含 tags 数量。
5. 输入校验:空字符串 → 400;非法 tag 字符 → 400。
6. 权限:现有 article 接口无需登录,保持一致。
7. 依赖:无新增。
8. DB 迁移:tags 列已存在,无需迁移。
9. 失败路径:DB 错误 → 抛 500,error message 不含 SQL 细节。
灰度 + 生产验证
T0: git push → T1: CI 通过(5min)
T2: 灰度 1% → 监控 10min,错误率 0.01% 正常 → 全量
T3: 探针 curl /api/articles?tags=test → 200 OK,返回 12 条
T4: 监控大盘 30min 内 0 告警
回滚预案
回滚命令:kubectl rollout undo deployment/api
触发条件:错误率 >1% / 5xx >0.5% / 文章列表接口响应时间 >2s
负责人:@oncall(PagerDuty)
验证:curl /api/articles → 200,旧行为可用
一页式交付清单
最后把整个工作流压成一张一页式清单,下次接需求照着走:
接需求
- 填任务卡 8 栏(目标 / 用户场景 / 输入输出 / 非目标 / 验收标准 / 失败处理 / 参考资料 / 改动范围)
- 任务卡里显式列边界、失败、并发三类验收
写代码
- AI 先读仓库清单(README / AGENTS.md / 依赖 / 相邻文件 / 类似实现 / 测试命令)
- AI 输出「我看到了什么」小结
- 五步小循环:数据/API → 核心逻辑 → 调用方/UI → 错误处理 → 文档
- 每个 commit 独立可验证 + 可回滚
- 禁止 AI「过度实现」:没列的功能先停下问
跑测试
- Lint 通过
- 类型检查通过
- 构建通过
- 单元 / 集成 / 边界 / 失败 / 回归 五类全跑
- 输出贴进 commit message
评审
- AI 自评 9 件套(范围 / 兼容 / 密钥 / 日志 / 校验 / 权限 / 依赖 / 迁移 / 失败)
- 人工重点评审:认证、支付、数据删除
- diff 只动了任务卡列的范围
上线
- 提交 / CI / 生产 三态分开
- 涉及 schema / 接口签名 / 权限 必走灰度
- 生产探针:health + 关键 endpoint + 监控 + 资源
回滚 + 复盘
- 上线前写好回滚预案 4 件(命令 / 触发 / 负责人 / 验证)
- 上线后 24h 盯监控
- 1 周后复盘 5 问,写进 retros/
常见误区
最后列六个我见过的、最容易翻车的点:
- 「AI 写完看着对,就上线了」 —— AI 不跑测试、不看输出、不懂业务,必须人工验。
- 「任务卡随便写」 —— 没有任务卡,AI 会自由发挥,三步之后就失控。
- 「一次写整个模块」 —— 拆不开 diff,评审看不动,回滚也难。
- 「push 了就算上线」 —— push 是 T0,上线是 T5,中间还有 CI、灰度、生产验证。
- 「没回滚预案就上线」 —— 出了问题不知道谁拍板、不知道怎么回滚、不知道回滚后该看什么。
- 「不复盘」 —— 同样的坑会反复踩,团队累积不出 SOP。
避开这六条,工作流就跑顺了。