自修复的边界不在技术,在「这个错要不要人来判断」。 修法唯一的错能自动修,修法取决于「本来想干什么」的不能。 成功率的分母是「尝试过的」,不是「全部失败」——混着算会把「不敢试」当成「修不好」。
上一篇把无人值守的前提补齐了。这一篇让它在失败时自己动一下手。 为什么不是「全自动修」? 因为有一类失败的修法不唯一:断言失败,你改代码还是改断言? 这个决定需要知道「本来想干什么」,而 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 秒实验
- 翻你们 CI 最近二十次失败,按「修法唯不唯一」分个类。 唯一的那部分,占比可能比你预期的高。
- 挑一类「修法唯一」的失败,写一条自动修。 跑两周,看它成功率和误修率。
- 给你的自动修复加一条「修复范围」声明。 声明不出来的,说明这个修复动作设计得太宽。
常见误区
- 「让 AI 看着日志自己修就行。」 它就着日志能改的地方,往往是把报错藏起来的地方。 先分类,再决定动不动手。
- 「自动修复成功率低,不如关掉。」 先看分母。 分母写成「全部失败」的话,一个画对了边界的好系统会显示成一半对一半错。
- 「修不动的就先重跑一遍。」 重跑能让不稳定测试看起来通过,也会让真问题晚一天被发现。
自测三问(点开对答案)
1. 能自动修和不能自动修的分界线是什么? —— **修法唯不唯一。** **不看难不难,看这个错本身有没有唯一正确答案。** 2. 为什么导入失败不建议自动修? —— **因为分不清是名字打错还是版本里没这个函数。** **前者改字母,后者升依赖,动作差别很大。** 3. 自修复成功率的分母该是什么? —— **尝试过的次数。** **用「全部失败」当分母,会把「判断为不该试」算成失败。**下一篇讲审查:三条规则能看见一半问题,另一半它看不见。
- ☐ (本篇完,下一篇 12.3 AI 辅助代码审查)