🛠️ 第12章 场景四:PR、Review 与 CI/CD 自动化
📄 一句话概括

自修复的边界不在技术,在「这个错要不要人来判断」。 修法唯一的错能自动修,修法取决于「本来想干什么」的不能。 成功率的分母是「尝试过的」,不是「全部失败」——混着算会把「不敢试」当成「修不好」。

📝 本章和前面几章的关系

上一篇把无人值守的前提补齐了。这一篇让它在失败时自己动一下手。 为什么不是「全自动修」? 因为有一类失败的修法不唯一:断言失败,你改代码还是改断言? 这个决定需要知道「本来想干什么」,而 CI 日志里没有这个信息。 Demo 造四类真实失败:缺模块、语法错、断言失败、导入不存在的符号。 分类器按错误码分流,只对前两类动手,两类真修好了,两类交给人工。

一、失败信号的三种来源

来源 典型错误 修法唯一吗
依赖层 找不到模块 唯一:补上
语法层 少括号、缩进错 唯一:改对
行为层 断言失败、超时 不唯一:要看意图

三层的分界线是「有没有唯一正确答案」。 前两层有:缺什么补什么,错在哪改哪。 第三层没有:测试挂了,可能是代码错了,也可能是测试写错了。 这条分界线比「难不难」好用得多。 因为「难」是相对的,同一个错换个上下文就不难了; 而「修法唯不唯一」是这个错本身的属性,不随上下文变。 Demo 里六种错误码按这条线分了组: ModuleNotFoundError、SyntaxError、IndentationError 能自动修; AssertionError、ImportError、TimeoutError 不能。 ImportError 归到「不能」这一边,可能有人不同意。 但 from math import sqrtt 这种错,你无法判断是「名字打错了」还是「版本里没这个函数」—— 前者改一个字母,后者要升级依赖。两个动作差别很大。

二、自动修复的边界

Demo 里分类器判定完之后,只对两类动手:新建缺失的模块、补上缺的右括号。 两次都真修好了,重跑退出码 0。 另外两类直接跳过,标「留给人工」。 注意这里没有「试一试看能不能修」——对一个不该自动修的错误动手,比不修更糟。

看着能自动修 为什么不该
断言失败就改断言 等于把测试删了
导入失败就加 try except 把错误藏起来
超时就调大超时 可能它真的死循环了
失败就重跑三次 不稳定测试会一直不稳定

这四条的共同点:它们让 CI 变绿,但没让问题消失。 判据可以写得很直白:把这个修复去掉,问题会回来吗? 会回来,那不叫修,叫掩盖。 还有一条更隐蔽的规矩:修复动作本身也要有范围。 「新建缺失的模块」听起来安全,但如果它建在错误的目录下,你会得到一个能跑但结构错的仓库。 所以修复动作也要声明它能碰哪些路径——这跟 10.2 的越界检测是同一件事。

三、一次自修复的完整记录

自动修复失败之后,人不该从头查。 他要看到「试过什么、结果如何」,从那里接着往下。

字段 记什么 为什么
失败类型 识别出的错误码 决定要不要试
尝试的修法 一句话 事后能复盘
结果 通过 / 仍失败 算成功率
人工接手点 停在哪一步 交接不丢信息

四个字段最容易被漏的是最后一个。 没有它,接手的人要先读一遍日志才知道发生了什么—— 而日志里最有用的一段,可能已经被你的修复动作覆盖了。 还有一个数字要注意:成功率怎么算。 Demo 里是「尝试了 2 个、成功 2 个、百分之百」。 如果分母写成「全部 4 个失败」,就变成百分之五十—— 那个数看着更保守,其实错了:它把「判断为不该试」算成了失败。 这两个数的差别会影响决策。 用百分之五十,你会觉得「自动修复不好用,先关掉」; 用百分之百,你会想「那能不能再多识别几类」。 同一个系统,两个方向相反的结论。

四、动手 Demo:CI 自修复:哪些能自动修,哪些必须交给人

四段真实的失败代码:缺模块、语法错、断言失败、导入不存在的符号。 每段真跑一次,按错误码分类,再决定动不动手。 分类是按退出码和错误类型判的,不是按用例名——所以「缺模块」那条如果换成别的模块名,结论一样。

RULES = [
    ("ModuleNotFoundError", "补依赖或补模块", True),
    ("SyntaxError", "改语法", True),
    ("IndentationError", "改缩进", True),
    ("AssertionError", "要判断是代码错还是断言错", False),
    ("ImportError", "要判断是写错名还是版本不对", False),
    ("TimeoutError", "要判断是慢还是死循环", False),
]

def classify(text):
    for key, fix, auto in RULES:
        if key in text:
            return key, fix, auto
    return "未知", "人工看", False

实跑输出:

--------------------------------------------------------------------
CI 自修复:哪些能自动修,哪些必须交给人
--------------------------------------------------------------------

  自修复的边界不在技术,在「这个错要不要人来判断」。四类失败各跑一遍。

  ① 失败分类表
  --------------------------------------------------------------------
| 错误类型 | 定型修法 | 能自动修吗 |
|---|---|---|
| ModuleNotFoundError | 补依赖或补模块 | 能 |
| SyntaxError | 改语法 | 能 |
| IndentationError | 改缩进 | 能 |
| AssertionError | 要判断是代码错还是断言错 | 不能 |
| ImportError | 要判断是写错名还是版本不对 | 不能 |
| TimeoutError | 要判断是慢还是死循环 | 不能 |

  能自动修的三类有个共同点:修法唯一。不能的三类也有个共同点:修法取决于「本来想干什么」。

  ② 四个真实失败,逐个分类
  --------------------------------------------------------------------
| 用例 | 退出码 | 识别为 | 处置 |
|---|---|---|---|
| 缺模块 | 1 | ModuleNotFoundError | 自动 |
| 语法错 | 1 | SyntaxError | 自动 |
| 断言失败 | 1 | AssertionError | 交人 |
| 导入不存在的符号 | 1 | ImportError | 交人 |

  四个用例里两个判定为可自动修,两个交人。

  ③ 只对「可自动修」的动手
  --------------------------------------------------------------------
| 用例 | 修法 | 动作 | 结果 |
|---|---|---|---|
| 缺模块 | 新建 notyet.py | 修了 | 通过 |
| 语法错 | 补右括号 | 修了 | 通过 |
| 断言失败 | — | 不试 | 留给人工 |
| 导入不存在的符号 | — | 不试 | 留给人工 |

  尝试了 2 个,成功 2 个,成功率 100%。
  ! 这个成功率的分母是「尝试过的」,不是「全部失败」。混着算会把「不敢试」当成「修不好」。

  ④ 自修复不该做的事
  --------------------------------------------------------------------
| 看着能自动修 | 为什么不该 |
|---|---|
| 断言失败就把断言改掉 | 那等于把测试删了 |
| 导入失败就加 try except | 把错误藏起来,下次更难查 |
| 超时就调大超时时间 | 可能它真的死循环了 |
| 失败就重跑三次 | 不稳定测试会一直不稳定 |

  四条的共同点:它们让 CI 变绿,但没有让问题消失。
  ! 判据可以写得很直白:修完之后,如果把这个修复去掉,问题会回来吗?会回来,那不叫修;
  不会回来,说明你改的是问题本身。

  ⑤ 一次自修复的完整记录
  --------------------------------------------------------------------
| 字段 | 记什么 | 为什么 |
|---|---|---|
| 失败类型 | 识别出的错误码 | 决定要不要试 |
| 尝试的修法 | 一句话 | 事后能复盘 |
| 结果 | 通过 / 仍失败 | 算成功率 |
| 人工接手点 | 停在哪一步 | 交接不丢信息 |

  最后一行最容易漏。自动修失败之后,人不该从头查——他要看到「试过什么、结果如何」,从那里接着往下。

四行分类、两行动手。 分类表的意义在于它是「事先写好的」,不是「跑起来再判断」。 事先写好,你能审它;跑起来判断,你只能看结果。 那两行「不试」要看清楚:断言失败和导入失败都是直接跳过,连试都不试。 这跟「试了没成功」是两回事——前者说明边界画对了,后者说明边界画错了。 成功率那个数(尝试 2、成功 2)在正文里会再算一遍分母。 这一篇最值钱的一句话就是「分母是尝试过的」——因为混着算会让一个好系统看起来不能用的。 最后那张记录表的四行,第三行和第四行是一对。 「结果」给统计用,「人工接手点」给交接用。 只记结果的系统,人接手时要重新读一遍日志。

30 秒实验

  1. 翻你们 CI 最近二十次失败,按「修法唯不唯一」分个类。 唯一的那部分,占比可能比你预期的高。
  2. 挑一类「修法唯一」的失败,写一条自动修。 跑两周,看它成功率和误修率。
  3. 给你的自动修复加一条「修复范围」声明。 声明不出来的,说明这个修复动作设计得太宽。

常见误区

  1. 「让 AI 看着日志自己修就行。」 它就着日志能改的地方,往往是把报错藏起来的地方。 先分类,再决定动不动手。
  2. 「自动修复成功率低,不如关掉。」 先看分母。 分母写成「全部失败」的话,一个画对了边界的好系统会显示成一半对一半错。
  3. 「修不动的就先重跑一遍。」 重跑能让不稳定测试看起来通过,也会让真问题晚一天被发现。
自测三问(点开对答案) 1. 能自动修和不能自动修的分界线是什么? —— **修法唯不唯一。** **不看难不难,看这个错本身有没有唯一正确答案。** 2. 为什么导入失败不建议自动修? —— **因为分不清是名字打错还是版本里没这个函数。** **前者改字母,后者升依赖,动作差别很大。** 3. 自修复成功率的分母该是什么? —— **尝试过的次数。** **用「全部失败」当分母,会把「判断为不该试」算成失败。**

下一篇讲审查:三条规则能看见一半问题,另一半它看不见。

相关笔记