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

规则只能审「能写成规则的那部分」,大概占一半,而且往往不是最贵的一半。 三条规则命中三处、误报零——但它对另外三处一无所知。 审查意见要可执行:有行号、有具体动作、有可验证的结果。

📝 本章和前面几章的关系

上一篇让 CI 自己修。这一篇让它自己看。 为什么「AI 审过了」这句话要加限定? 因为审查有两种:一种查「这段代码长什么样」,一种查「这段代码想干什么」。 前者能写成规则,后者不能。而 PR 里最贵的问题基本都在后者。 Demo 在一份二十几行的代码里埋了六处问题:密钥、裸 except、未使用变量、除零、字段不一致、 职责过多。 三条规则跑一遍,命中前三处,对后三处完全无感。

一、AI 审查能看什么

能看的是「形式」:语法上、模式上有问题的地方。

类别 例子 为什么能自动
安全隐患 硬编码密钥、SQL 拼接 有固定模式
语言陷阱 裸 except、可变默认参数 语法树里查得到
死代码 未使用变量、不可达分支 数据流分析能做
风格不一致 命名、行宽、导入顺序 有明确规则

这四类有一个共同点:判据是「这段代码本身」,不需要知道外面的世界。 所以它们能被写成规则,也能被稳定地跑一千次。 Demo 里三条规则:正则找密钥、语法树找裸 except、数据流找未使用变量。 一共命中三处,误报零。 这个结果说明规则写对了,但不说明代码审完了。 「误报零」这一点值得单独说。 规则审查最容易死在误报上:报十条错八条,人就再也不看了。 所以宁可少写几条规则,也要保证每条都准。

二、AI 审查看不见什么

看不见的是「意图」:这段代码本来想干什么。

问题 为什么规则看不见
calc(a, 0) 会除零 要知道 a 的取值域,得看上游
字段名与上游不一致 跨文件契约,本地看不出来
run() 职责过多 架构判断,没有唯一答案

Demo 里这三处是人工标注的,规则一条都没报。 不是规则写得不好,是这三类问题本质上需要「上下文」——而规则只看一个文件。 注意第二处「字段名与上游不一致」。 这类问题在跨端开发里最常见,也最贵——它不会报错,只会让数据慢慢对不上。 而规则永远看不见它,因为两边各看各的都没毛病。 所以「AI 审过了」这句话的准确版本是: 「能写成规则的那部分审过了」。 这句话不是贬低,它的实际价值很大——因为那部分恰好是旧审查里最耗注意力的部分。

三、让审查意见可执行

一条意见能不能被执行,看三样:有没有行号、有没有具体动作、有没有可验证的结果。

含糊意见 可执行意见
这里应该处理异常 第 12 行裸 except 会吞掉 KeyboardInterrupt
变量名不好 第 20 行 unused 只赋值没读,删掉或改名并打印
要加测试 给 calc 加一条 b=0 的用例,断言抛 ZeroDivisionError

左边那种意见,作者只能回一句「你觉得应该怎么改」。 一轮往返,什么都不推进。 右边那种,作者看完就能动手,或者能明确反驳—— 「KeyboardInterrupt 我们确实要放出去,改成 except OSError 可以」。 两种回应都比「你说得对,我再想想」有价值。 所以给 Agent 的审查任务里要写清这一条。 不写,它默认给你「建议加强异常处理」——正确、礼貌、完全没用。 还有一条同样重要:意见要限量。 一次报四十条,人会全部忽略;一次报五条,人会逐条看。 排序比数量重要——先报「会出事」的,再报「不好看」的。

四、审查者角色的变化

以前 现在
通读全部 diff 找问题 先看规则没覆盖的部分
自己列问题清单 判断清单里哪几条该修
检查格式和明显错误 把这类交给规则

这个变化的关键不是「省时间」,是「注意力花在哪」。 格式问题占了旧审查时间的一大部分,而它恰恰最该自动化。 省下来的注意力去哪了? 去回答那个只有人能回答的问题:这段代码想干什么,它干对了吗。 这件事机器人做不了,因为它要的是意图,而意图不在 diff 里。

五、动手 Demo:AI 辅助审查:三条规则能看见什么,看不见什么

一份二十几行的代码,六处问题。三条规则各跑一遍,看命中几处。 三条规则分别是:正则找密钥、语法树找裸 except、数据流找未使用变量。 规则是真跑的:语法树用 ast 解析,未使用变量做的是赋值与读取的集合比对。

tree = ast.parse(AUDIT_SRC)

# 规则一:硬编码密钥(正则)
for i, ln in enumerate(AUDIT_SRC.split("\n"), 1):
    if re.search(r"[\"']sk-[A-Za-z0-9-]{8,}[\"']", ln):
        print("硬编码密钥", i)

# 规则二:裸 except(语法树)
for node in ast.walk(tree):
    if isinstance(node, ast.ExceptHandler) and node.type is None:
        print("裸 except", node.lineno)

# 规则三:未使用变量(赋值集合减读取集合)
for name, ln in assigned.items():
    if name not in used and not name.isupper():
        print("未使用变量", ln, name)

实跑输出:

--------------------------------------------------------------------
AI 辅助审查:三条规则能看见什么,看不见什么
--------------------------------------------------------------------

  一份二十几行的代码里埋了六处问题。三条规则跑一遍,看命中几处。

  ① 被测文件
  --------------------------------------------------------------------

  [被审代码 · 本沙箱真跑]
  | # -*- coding: utf-8 -*-
  | import os
  | 
  | API_KEY = "sk-live-8f2a91b7c4d5e6f0"      # 硬编码密钥
  | 
  | 
  | def load(path):
  |     try:
  |         f = open(path)
  |         data = f.read()
  |         return data
  |     except:
  |         return ""
  | 
  | 
  | def total(rows):
  |     s = 0
  |     for r in rows:
  |         s += r["amount"]
  |     unused = len(rows) * 2                 # 未使用变量
  |     return s
  | 
  | 
  | def calc(a, b):
  |     return a / b
  | 
  | 
  | def run():
  |     cfg = load("conf.ini")
  |     a = total([{"amount": 1}, {"amount": 2}])
  |     b = calc(a, 0)
  |     return b + len(cfg)
  六处问题:密钥、裸 except、未使用变量、除零、字段不一致、职责过多。


  ② 三条规则扫一遍
  --------------------------------------------------------------------
| 规则 | 行号 | 命中 |
|---|---|---|
| 硬编码密钥 | 4 | API_KEY = "sk-live-8f2a91b7c4d5e6f |
| 裸 except | 12 | except: |
| 未使用变量 | 20 | unused |

  三条规则命中 3 处,误报 0 处。

  ③ 另外三处,规则看不见
  --------------------------------------------------------------------
| 问题 | 为什么规则看不见 |
|---|---|
| calc(a, 0) 会除零 | 业务逻辑错,规则看不出来 |
| total 的字段名 amount 与上游不一致 | 跨文件契约,本地看不出来 |
| run() 职责过多 | 架构判断,没有唯一答案 |

  三处都需要「知道这段代码本来想干什么」才能判断。规则只知道语法长什么样。
  ! 所以「AI 审过了」这句话要加限定:它审的是「能写成规则的那部分」。
  这部分大概占全部问题的一半,而且往往不是最贵的那一半。

  ④ 让审查意见可执行
  --------------------------------------------------------------------
| 含糊意见 | 可执行意见 |
|---|---|
| 这里应该处理异常 | 第 12 行裸 except 会吞掉 KeyboardInterrupt,改成 except OSError |
| 变量名不好 | 第 20 行 unused 只赋值没读,删掉或改成 row_count 并打印 |
| 要加测试 | 给 calc 加一条 b=0 的用例,断言抛 ZeroDivisionError |

  右边的共同点:有行号、有具体动作、有可验证的结果。
  左边那种意见,作者只能回一句「你觉得应该怎么改」。
  ! 给 Agent 的审查任务里要写清「每条意见必须带行号和改法」。
  不写,它给你一份「建议加强异常处理」——正确、礼貌、没用。

  ⑤ 审查者角色的变化
  --------------------------------------------------------------------
| 以前 | 现在 |
|---|---|
| 通读全部 diff 找问题 | 先看规则没覆盖的部分 |
| 自己列问题清单 | 判断清单里的哪几条该修 |
| 检查格式和明显错误 | 把这类交给规则,省下注意力 |

  这个变化的关键不是「省时间」,是「注意力花在哪」。
  格式问题占了旧审查时间的一大部分,而它恰恰是最该自动化的那部分。

三条规则命中三处,误报零。 这个结果要连着下一段看:另外三处它一处都没报。 四条规则里,只有「未使用变量」用的是数据流。 前两条是模式匹配,看起来很土,但对这两类问题足够准确—— 这里有个取舍:能简单做对的,就别上复杂方法。 那段「另外三处规则看不见」的表,是这一篇的落点。 三处都需要「知道这段代码本来想干什么」——而这件事,规则做不到,人也只能靠读上下文。 最后那张「含糊 vs 可执行」的对照,注意右边每条都带了行号。 行号不是格式要求,它是「让意见能被核对」的最低条件—— 没有行号,作者连「你说的哪里」都问不出口。

30 秒实验

  1. 翻一次你收到的最含糊的审查意见,试着把它改写成带行号的版本。 改不动,说明那条意见本身就没想清。
  2. 给你项目写三条规则:一条密钥、一条裸 except、一条未使用变量。 跑一周,看误报率。
  3. 数一下你上次审查报了几条意见。 超过十条的,想想对方实际改了几条。

常见误区

  1. 「AI 审过了就没问题了。」 它审的是能写成规则的那部分。 最贵的问题往往在它看不见的那一半。
  2. 「规则越多越全越好。」 误报会杀死规则审查。 报十条错八条,人就再也不看了。宁少而准。
  3. 「审查意见写得专业一点显得认真。」 专业不等于可执行。 「建议加强异常处理」很专业,但没人知道该改哪一行。
自测三问(点开对答案) 1. AI 审查能看见的四类问题有什么共同点? —— **判据是「这段代码本身」,不需要知道外面的世界。** **所以能写成规则,也能稳定地跑一千次。** 2. 为什么「字段名与上游不一致」规则永远看不见? —— **因为两边各看各的都没毛病。** **它需要跨文件的契约信息,而规则只看一个文件。** 3. 可执行的审查意见要带哪三样? —— **行号、具体动作、可验证的结果。** **少哪样,作者都只能回一句「你说的哪里」。**

下一篇把范围从代码扩到依赖:三类安全扫描,各管一段。

相关笔记