一个 8 年历史的模块要"加一个字段",有人直接把相关文件丢给助手,让它"加上字段并适配"。它改了 14 个文件,编译通过——而其中一处改到了"给旧版客户端返回的兼容分支",于是老版本 App 直接解析失败。
遗留代码的特殊性不在"老",而在"它有看不见的约束":兼容分支、历史数据、第三方对接、以及"某个诡异的写法为什么存在"。AI 能"读代码",但"读不出约束"——这个"读不出约束"是 AI 的结构性局限:它只看"现在这份代码",而"为什么这么写"发生在"过去的某次会议"里——于是它给出的"适配"是"按当下代码形状推的",而不是"按约束推的"。
而那些约束正是"改坏"的地方。
1. "看不见的约束"的三种来源
| 来源 | 表现 |
|---|---|
| 兼容分支(版本/客户端) | if (version < 3) 之类 |
| 历史数据形状 | 某些字段"可能为空/可能是脏值" |
| 业务约定(口头) | "这个字段别动,财务对账用" |
第三项是"最没法读出来"的:它不在代码里,而在人的记忆里第三项的"补救"只有一个:问人。而"问谁"要看"这模块多久没人动"——于是"找出最后一个提交者"是最快的路径。
——于是"AI 读完所有代码"也不会知道。
2. "先理解"的三种手段
| 手段 | 说明 |
|---|---|
| 让 AI"画出调用图与数据流" | 快速建立结构 |
| 让 AI"解释这段为什么这么写" | 会有推测,需核对 |
| 用"测试/日志"验证理解 | 最可靠 |
第二项的"危险"在于"解释会让人放心":"为性能考虑"听起来合理,于是不再追问——而实际可能只是"当年随手写的"。所以它的输出要当"假设",不是"结论"。
第二项要当心:它会给"看似合理的解释"("为了性能"、"为了兼容"),而真实原因可能只是"当年写错了"。
第三项是"唯一可靠"的:跑一遍、看日志、加断言——于是"理解"变成"可验证的"。
// "理解"的三个可验证动作(比"读代码"可靠)
1 找出"这个模块的入口"(谁调它、被谁调)
2 跑一次真实请求,记录"经过哪些分支"
3 对该分支加一行日志/断言,验证"我的理解对不对"
// 判据:能否说出"这段代码在什么条件下会走到"
3. "先改"的三种风险
| 风险 | 后果 |
|---|---|
| 改到兼容分支 | 旧客户端崩 |
| 改到"脏数据"路径 | 线上数据异常 |
| 改到"没人懂的那段" | 引入更隐蔽的 bug |
第一项的"识别"是"搜"version"、"apiLevel"、"old"、"legacy"这类词":它们往往标记着"兼容分支"——而 AI 未必会把这些当作"不可动"。
第一项在"多版本共存"的系统里最常见:代码里有两套逻辑,而"哪套在跑"取决于"客户端版本"。
"提示"要写"约束清单",否则 AI 会"按代码形状"改:
// 交给助手时的"约束前置"(四行,成本 1 分钟)
约束:
1 不得修改"兼容分支"(含 version/apiLevel/legacy 的 if)
2 不得修改"给旧版客户端的响应字段"(见 api-v2.md)
3 历史数据可能为空/超长,新代码必须判空
4 只允许改"新逻辑路径",旧路径保持行为不变
// 收益:它从"按形状改"变成"按约束改",diff 规模通常减半
4. "小步改"的三种做法
| 做法 | 效果 |
|---|---|
| 先加"观测"(日志/指标),不改逻辑 | 最安全 |
| 加"开关",新旧并存 | 可回滚 |
| 一次只改一个分支 | 可定位 |
第一项是"被低估"的:先让"这段代码的行为"可见(打点、日志),再改第一项的"具体做法"是"给每个"可能受影响的分支"加一个计数":于是"改前/改后"的分布差异立刻可见——而它不需要"理解全部逻辑"。
——于是"改坏了"能立刻看出"哪条路径变了"。
// 第一步:不改逻辑,只加观测
if (legacyPath) {
metrics.counter("legacy.path.used").increment(); // 先知道"它在跑"
}
// 第二步:加开关(新旧并存)
if (flags.newCalcEnabled() && !legacyPath) { ...新逻辑... } else { ...原逻辑... }
// 第三步:灰度放大,观察指标后再删旧分支
5. "旧分支何时能删"的三种判据
| 判据 | 说明 |
|---|---|
| 调用量为 0(连续 N 天) | 最硬 |
| 客户端版本分布(旧版本 < 1%) | 需产品拍板 |
| 有"数据迁移"完成证据 | 需数据侧确认 |
第一项的"门槛"要看"流量周期":"30 天 0 次"对"日活型"系统够,对"月结型"系统要"3 个月"——否则会"删掉每月跑一次的分支"。
第一项是"最可靠"的:"这个分支最近 30 天调用 0 次"——而它需要"你先加了观测"。
"调用量"这类证据要"能在删之前拿到",而不是"删完再看":
// 三步拿到"可删证据"(按周看)
1 加计数:分支进入时打一个 counter(一次提交)
2 观察:连续 4 周该 counter 为 0(且总流量无异常)
3 删除:删分支 + 保留"回滚一次"的能力(镜像/开关)
// 判据:没有第 1 步,第 2 步永远拿不到 → 于是"没人敢删"
6. 排错:五种"改遗留代码的问题"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 老客户端崩 | 改了兼容分支 | 加观测 + 开关 |
| 线上数据异常 | 脏数据路径 | 先摸数据形状 |
| 改完没人敢删 | 无"调用量"证据 | 先加观测 |
| 越改越乱 | 一次改太多 | 一次一分支 |
| 评审看不出风险 | diff 太大 | 拆 PR |
第五项很实际:**AI 一次改 14 个文件,评审只能"看个大概"**第五项的"修法"是"按"风险面"拆 PR":兼容相关一个 PR、新逻辑一个 PR、清理一个 PR——于是"每个 PR 都能被真正评审"。
——于是"风险"被"diff 规模"掩盖。
"理解"还有一个"低成本"的入口:让 AI"生成一份"行为清单",然后你只核对"关键几条"。
// 一份"行为清单"(用来核对,而不是用来信)
1 入口:两个(HTTP /api/order、定时任务 reconcile)
2 分支:version<3 走旧逻辑;金额>10 万走人工审核
3 外部依赖:支付网关(超时 5s)、库存服务(重试 2 次)
4 数据假设:status 可能为空字符串(历史脏值)
// 用法:逐条"验证"(跑一次/查日志),验过的才当作约束
7. 边界:什么情况可以直接改
| 场景 | 为什么 | 做法 |
|---|---|---|
| 纯新增(无旧路径) | 无兼容影响 | 直接改 |
| 有新测试覆盖的模块 | 出错会被测出 | 直接改 |
| 内部工具(无外部版本) | 无兼容包袱 | 直接改 |
| 已确认"无人调用"的代码 | 无影响面 | 直接删 |
最后一条与"知识"有关的边界:改完要"补记录":"为什么这个分支不能删"写进注释或 ADR——否则下一个接手的人(和下一个 AI)会再踩一次。
判断口径一句话:"这段代码的行为,会不会被'我看不到的东西'依赖"。会(老客户端、外部对接、历史数据),必须先"让它可见"再改;不会(内部、纯新增),可以直接改。
最后一个"协作"现实:遗留代码的"约束"往往掌握在"不太写代码的人"手里(运维、财务、老员工)。所以"先理解"不只是"读代码 + 问 AI",还包括"找到那个知道'为什么'的人"——而这一步"AI 永远替代不了"。
8. 自测
- "看不见的约束"的三种来源里,哪一种"读代码也读不出来"?
- 让 AI"解释这段为什么这么写"有什么风险?什么是"唯一可靠"的理解方式?
- "先加观测再改"为什么被低估?它让"改坏了"变成什么?
- "旧分支何时能删"的三种判据里,哪一种最硬?它依赖什么前提?
- 为什么"AI 一次改 14 个文件"会让评审失效?
- 判断"能否直接改"的那个问题是什么?