一段由助手生成的解析逻辑上线两周后出了 bug,把合法的输入判为非法。复盘时没人认领:"这是它生成的"。而代码里也没有任何标记说明它是生成的、谁审过、依据什么规则写。于是修复变成了"重新设计这段逻辑",因为它连"为什么这么写"都不知道。
生成代码的所有权问题不是法律问题,而是工程责任问题:谁负责它的正确性、谁能在它出问题时解释它、以及它该不该被 commit。这个说法有个更尖锐的版本:代码的所有权不体现在 commit 作者里,而体现在"谁能解释它"里。而解释能力来自"审过并验证过",不来自"跑了生成器"。
没有归属的代码,等于没有维护者。
1. 三种"看起来解决了"的假归属
| 做法 | 问题 |
|---|---|
| commit 作者写自己 | 名义归属,实际没审 |
| 加一行 "AI-generated" 注释 | 只标记来源,不产生责任 |
| 全组共担 | 等于无人负责 |
第二项最常见也最无效:标记了来源,但没有标记"谁验证过什么"。而排障需要的是后者。
第三项在事故复盘里表现为"大家都以为别人看过"。第一项(作者写自己)的问题在"署名即担责"这个默认预期被破坏:团队以为有人负责,而那个人以为自己只是"按了按钮"。这类错位往往在第一次事故时才暴露。
2. 归属要落成四件事
| 事项 | 具体 |
|---|---|
| 谁审过 | 一个具体的人名 |
| 依据什么 | 需求/规格/测试 |
| 验证到什么程度 | 跑了什么测试 |
| 出问题时找谁 | 同一个名字 |
第三项的判据要具体:"跑了什么"能写成一行(如"预发跑通 3 条真实样例 + 6 个边界用例"),才是可核查的。写"手动验证过"等于没写。
第三项是关键:"我审过了"与"我在真实数据上验证过"是两件事。而事故往往发生在"审过但没验证"的地方。
第四项的价值在长期:半年后这段代码出问题,需要有人能解释"当初为什么这么写"。
// PR 描述里的最小归属信息(三行)
生成:Claude/Codex,提示见 tickets/AI-118 附件
验证:补了 6 个边界用例(见 test),并在预发跑通全流程
责任:@zhangsan 复审并接受该实现;逻辑变更需 @zhangsan 复核
3. 该不该 commit 的三种情况
| 情况 | 处置 |
|---|---|
| 生成 + 人改写 + 测试 | 正常 commit |
| 生成 + 人读懂但未改 | 可 commit,需标注 |
| 生成但没人读懂 | 不该 commit |
第三项的判据很硬:没人能解释这段代码为什么这么写,那它就是一个未来事故。第三项在实践中常被"能跑就行"绕过。一个可执行的门槛是:让作者用三句话讲清"它做了什么、依赖什么假设、什么地方会坏"——讲不出来就不该 commit。
而"看起来对"不是判据。
第二项允许,但要接受"未来维护成本更高":改动它的人需要重新理解生成逻辑。
还有一个"生成范围"的实践:把生成的部分单独放一个文件或一个函数,而不是散落在已有逻辑里。这样"哪段是生成的"不靠标记,而靠结构——而结构不会随着次数增加而失效。
// 结构化的隔离(比注释标记更持久)
parse_x.py 生成:输入解析(含边界处理,见 test_parse_x.py)
service.py 手写:业务编排,只调用 parse_x 的函数
// 好处:替换生成部分时,改动范围清晰;评审时只看一个文件
4. 让生成代码可维护的四个动作
| 动作 | 效果 |
|---|---|
| 补注释写"为什么" | 未来可读 |
| 补测试写"边界" | 行为可验证 |
| 拆小函数 | 出问题可定位 |
| 记录生成时的假设 | 假设变了能发现 |
第四项的典型例子:生成代码假设"输入的 JSON 一定有某字段""时间一定是 UTC""字符串一定不含全角"。这些假设在演示数据里成立,在真实数据里未必。
第四项最容易被跳过:生成代码里常含"隐含假设"(数据格式、字段必有、编码固定),而这些假设没写下来,就无人知道何时失效。
生成代码还有一个"质量分布"特征值得知道:它在"平均质量"上不差,但在"极端情况"上更差。也就是说,它写正常路径往往比人更周到,而错误路径与罕见输入更容易被简化掉。
// 生成代码的三处典型缺口(按出现频率)
1 错误处理被简化:吞异常、去掉重试、日志只剩一句
2 边界缺失:空值、超长、非法格式没有分支
3 假设未声明:格式、时区、编码依赖,代码里看不出来
// 评审优先级:按 1 → 2 → 3 看,而不是从第一行读到最后一行
5. 与评审的关系
| 评审对象 | 关注点 |
|---|---|
| 生成代码 | 边界、假设、错误处理 |
| 手写代码 | 同样的东西,但作者能解释 |
| 混合时 | 先分清哪部分是生成的 |
第三项是实践难点:PR 里混着手写与生成,评审者不知道哪段该重点看。第二项(手写代码)不该被过度信任:手写同样会漏边界。差异只在于"手写的人通常能解释自己的假设",而生成的那段不能——所以评审重点在"能不能解释",不在"是谁写的"。
所以"标记生成范围"对评审效率有直接帮助。
差异化的关注点:生成代码最常错在"错误处理与边界",因为它倾向于写"正常路径的优雅实现"。
// 评审清单:生成代码的前三项
1 错误处理:异常路径是否有覆盖,是否吞掉了错误
2 边界:空、超大、非法输入是否被处理
3 隐含假设:依赖的格式/字段/编码是否写下来
// 手写代码通常不会在这三项上集体缺失,而生成代码会
落到工具上,最有效的一步是"把归属信息变成可检查的字段":PR 模板里加三项(是否含生成代码、谁复审、验证方式),CI 上检查"含生成代码但未填复审人"的 PR 不能合并。
// PR 模板里的三项(CI 可校验)
generated: true # 是否包含 AI 生成代码
reviewer_responsible: "@zhangsan" # 复审并接受的人
verified_by: "pre-prod run of 3 real cases + 6 edge tests"
// CI 规则:generated=true 且 reviewer_responsible 为空 → 阻止合并
6. 排错:五种"生成代码出问题"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 出问题无人能解释 | 未读懂就 commit | 强制解释门槛 |
| 修复等于重写 | 无"为什么"注释 | 补注释与假设 |
| 边界 bug 集中在生成段 | 生成偏正常路径 | 评审重点看边界 |
| 评审漏看 | 不知哪段是生成的 | 标记范围 |
| 反复改同一段 | 隐含假设未记录 | 写下假设 |
第四项(评审漏看)还有一个"规模"因素:生成代码的 PR 往往一次几百行,而评审者在大 diff 上会滑到"看起来没问题"就通过。所以"分批提交"本身就是一种质量措施。
第五项最消耗:同一段代码被三个人改过,每次都"再修一次",因为没人知道它依赖什么假设。
7. 边界:什么情况不必计较归属
| 场景 | 为什么 | 做法 |
|---|---|---|
| 一次性脚本(用完即删) | 无维护需求 | 不标记 |
| 测试用的辅助代码 | 不影响生产 | 简单标记 |
| 文档与注释生成 | 低风险 | 不标记 |
| 已完全重写为人工实现 | 已是人工代码 | 不标记 |
判断口径一句话:这段代码会不会在半年后仍需有人维护。最后一条与"团队"有关的边界:归属不是惩罚,而是让维护成本可见。当"这段是生成的、只有张三能解释"成为公开信息,团队自然会调整"生成多少、审多少"的比例——而不是靠制度强制。
会,就必须有归属与假设记录;不会(临时脚本),标记来源就够。
8. 自测
- "加一行 AI-generated 注释"为什么是假归属?排障真正需要的是什么?
- 归属要落成的四件事里,哪一件最能区分"审过"与"验证过"?
- "生成但没人读懂"为什么不应当 commit?"看起来对"为什么不是判据?
- 生成代码最常错在哪两类地方?为什么评审要差异化关注?
- 为什么"标记生成范围"能提高评审效率?
- 判断"是否需要计较归属"的那个问题是什么?