
📰 科技要闻
• 韩国央行维持利率不变,中东局势不确定性促使其保持谨慎(来源:Investing.com中文)
• 超25亿港元南下抢筹!17家港股IPO获险资基石认购,泰康领衔重仓科技赛道(来源:Investing.com中文)
• AI 内容共创平台 FunloomAI 再获数千万 Pre-A 轮融资,让创作回归创意本身(来源:36氪)
• 鸿蒙智行发布新一代问界 M9、蔚来正式推出 ES9(来源:少数派)
• Google 一名员工被指控利用内部信息在 Polymarket 上赢得 120 万美元(来源:The Verge)
先说一个故事。某周三下午三点,我们团队的 AI Code Review 流水线突然在一条 MR 上挂了 47 分钟才出结果,留下 89 条 inline comment——其中 60 条是误报。开发者在群里炸锅,PM 直接 @ 我:"这玩意儿是不是该下线了?"
复盘下来,问题不是 AI 不行,是我们把 demo 阶段的流水线当生产用了。这条 MR 改了 38 个文件,我们老老实实把每个文件的全文都塞给了 LLM,单次调用 token 直接打到 180k,触发了多次重试;上下文里塞了大量无关代码,AI 开始"看见啥就报啥"。
那次事故让我意识到:上一篇文章里讲的"加载完整源码、Prompt 工程、inline comment 回写"——这些只是地基。从地基到能扛住每天几十条 MR 的生产系统,中间隔着四层"防爆护栏"。
这一篇就把这四层全拆开讲。不讲理论,全是踩过的坑。
一、为什么会爆?先量化"上下文炸药包"
很多人以为 MR 大小和 token 数是线性关系。错。是雪崩式的。
从我们 200 多条 MR 里抽样,得到一组让人后背发凉的数据:
MR 文件数 | 平均 Token | 表现 |
|---|---|---|
≤ 5 文件 | 28k | 单轮搞定,岁月静好 |
10-15 文件 | 75k | 开始接近上限 |
20+ 文件 | 145k | 一半触发上限错误 |
30+ 文件重构 | 220k | 全军覆没 |
为什么是非线性?因为大 MR 往往是"功能改动 + 配置文件 + 测试 + 资源文件"全打包,每个文件都是一份完整源码,加起来就爆了。
三种典型爆点
• 爆点 A:单文件巨型类——Android 项目里某些祖传 ViewModel 或 Repository 动辄 1500+ 行,改动只有 20 行,但全文塞进去就是 15k token。
• 爆点 B:批量自动生成代码——proto 生成的 java、Room 的 schema、navigation 的 directions 类,占了变更里相当大的比例,但其实完全不需要 AI Review。
• 爆点 C:跨模块重构——一次重构 30 个文件,每个文件改动不大,但加起来上下文必爆。这是 demo 阶段最容易翻车的场景。
一个反直觉的发现
我们做了个实验:故意给 AI 喂超长上下文(200k token),让它 review 同一个 MR。结果误报率比正常 50k 上下文时高 3.2 倍。
为啥?因为 LLM 在长上下文里会"主动找事干"——它会把一些不相关的文件里的潜在问题也拉进来报,制造噪音。这就是为什么"塞得越多≠效果越好"。
二、防爆护栏 1:智能裁剪
不是所有文件都值得 Review。这是第一道闸。
2.1 文件级过滤
生产环境的白名单/黑名单:
REVIEW_INCLUDE = {
# 核心源码
".kt", ".java", ".kts",
# 关键配置
"build.gradle",
"AndroidManifest.xml",
}REVIEW_EXCLUDE = [
# 自动生成
"**/build/**",
"**/generated/**",
"**/*Binding.java",
"**/*Args.kt",
# 资源 / 锁文件
"**/res/drawable/**",
"**/gradle.lockfile",
# 大型 mock
"**/test/resources/**",
# 二进制
"**/*.png", "**/*.so",
]
实测过滤效果:30 个文件的 MR 平均能砍掉 12 个,token 量直降 35%。
2.2 单文件裁剪:只保留"相关上下文"
上一篇里我说"给 AI 完整源码"——这是理想态。生产环境的现实是:800 行的 ViewModel 改了 20 行,你不能塞全文。
三段式裁剪算法:
def trim_for_review(file_path,
diff_hunks):
source = read_lines(file_path)
keep = set()# 1. 改动行 ±50 行
for hunk in diff_hunks:
keep.update(range(
max(0, hunk.start - 50),
min(len(source), hunk.end + 50)
))# 2. 类成员声明(让 AI
# 知道这个文件有什么)
for i, line in enumerate(source):
if is_class_decl(line) or \
is_method_signature(line):
keep.add(i)# 3. 改动方法引用的其他方法
refs = extract_method_calls(
diff_hunks, source)
for name in refs:
rng = locate_method(source, name)
if rng:
keep.update(rng)return render_with_omissions(
source, keep)
实测:单文件平均能压缩到原文 30% 左右,AI 判断质量损失 < 5%。
2.3 用 AST 而不是行号(进阶)
更进阶的玩法:用 Kotlin Compiler API 或 ts-morph 这类 AST 工具做语义级裁剪:
• 改动落在哪个函数?只保留这个函数 + 它调用的私有方法 + 它依赖的类成员
• 改动落在类声明?保留整个类骨架(成员签名)+ 改动函数完整体
工程量比行号方案大 3 倍,但对大型遗留代码(单文件 2000+ 行)效果质变。
一个反直觉的实践
不要保留 import 区块。我们做过对比,让 AI 看到 import 列表反而会让它把"未使用的 import"当作主要问题报上来——属于纯噪音。Lint 已经能搞定 import 检查,AI 别凑热闹。
三、防爆护栏 2:Token 预算管理
3.1 预算分配的优先级金字塔
把每次 LLM 调用当成一次"预算分配会议":
总预算: 100k token
├─ 系统 prompt: 3k (固定)
├─ Diff 内容: 30-40k (最高优先级,必须完整)
├─ 被改文件源码(裁剪后): 40-50k
├─ 跨文件依赖(被引用方): 5-10k
└─ 输出预留: 8k (Review 评论本身要写)
铁律:永远先满足 diff 完整性,再考虑上下文丰富度。Diff 缺失 > 上下文不足——前者会让 AI 完全看不懂改了啥,后者只是误报率高一点。
3.2 应急模式:超大 MR 的兜底
当 diff 本身就超 70% 预算(典型场景:文档/json 大改、批量重命名),切换到应急模式。我们生产环境是三级降级:
• 降级 1(30-50 文件):只发 diff,不带源码。在 prompt 里告诉 AI"本次因 MR 体量过大未加载完整文件,请只对 diff 范围内可独立判断的问题做 review"。
• 降级 2:分片调用。把文件按目录拆成 N 组,每组独立调用一次 LLM。
• 降级 3(> 50 文件):直接拒绝。在 MR 上留评论:"本 MR 涉及 X 个文件、约 Y 万行 diff,超过 AI Review 处理上限,建议拆分。"
说实话,降级 3 这条规则我们叫"一 MR 一目的",反馈意外地好——很多开发者只是懒得拆,不是真的非合不可。
3.3 缓存:被低估的省钱大杀器
很多人忽视了:MR 是会被反复推送的。开发者改了一行重新 push,整个流水线又跑一遍。如果不做缓存,token 浪费极其严重。
def cached_review(file_path,
commit_sha, diff):
key = f"{file_path}:" \
f"{commit_sha}:" \
f"{hash(diff)}"if result := cache.get(key):
return resultreview = llm_review(
file_path, diff)
cache.set(key, review,
ttl=7 * 86400)
return review
实测在生产环境,缓存命中率能稳定在 25-30%(同一文件被多个 MR 覆盖、rebase 后部分 commit 不变等)。这是纯白捡的成本节约。
四、防爆护栏 3:触发策略
4.1 哪些 MR 不该跑 AI Review
不是每个 MR 都值得跑。我们生产环境的"跳过清单":
ai_review:
rules:
# 1. 分支整合类合并
- if: '$TARGET =~ /^(release|main)/'
when: never# 2. 仅文档变更
- changes:
- "**/*.md"
- "docs/**/*"
when: never# 3. 仅版本号 bump
- if: '$MSG =~ /^chore: bump/'
when: never# 4. revert MR
- if: '$MSG =~ /^Revert/'
when: never# 5. 自动 PR 工具
- if: '$USER =~ /dependabot/'
when: never# 默认:跑
- when: on_success
4.2 feature → release 这条线为什么必须跳
这是个真实踩过的坑。feature 分支已经在合到 develop 时跑过一次 AI Review。当 release 周期开始,从 develop 切 release 分支再走 MR,相当于同样的代码再 review 一次。结果是:
• AI 还会再生成一遍 review 评论(开发者吐槽:你看过的代码再看一遍干嘛)
• 因为基线分支变了(从 develop 变成 release),diff 范围也变了,可能产生新的、矛盾的评论
• 浪费 token,制造噪音
4.3 commit 粒度 Review:多 issue PR 的解法
很多团队会遇到一种情况:一个 PR 包含多个 commit,每个 commit 对应不同 issue。
按 commit 切片是正解:
def review_pr_by_commits(mr):
commits = get_mr_commits(mr.iid)if len(commits) > 5:
post_comment(mr.iid,
"建议拆分 MR:每个 MR"
"聚焦一个目的。")
returnfor commit in commits:
issue_id = parse_issue(
commit.message)# 关键:把 issue 描述
# 当业务上下文喂给 AI
ctx = ""
if issue_id:
issue = get_issue(issue_id)
ctx = f"## 业务背景\n" \
f"{issue.title}\n" \
f"{issue.description}"diff = get_commit_diff(
commit.sha)
review = ai_review(
diff=diff,
extra_context=ctx,
)
post_commit_comments(
mr.iid, commit.sha, review)
这种方式的好处:AI 拿到了业务上下文,能判断这个 commit 是否真的解决了对应 issue 的问题,能直接缓解 AI 不懂业务规则的痛点。评论挂在 commit 维度,开发者一看就知道"这条评论是针对哪个 issue 的改动"。
五、防爆护栏 4:分层 Review 架构
最关键的认知转变:别想着一次 LLM 调用解决所有 review 任务。
把 review 拆成三个层次的独立调用,每次只关注一个维度:
Layer 1: 文件级 Review(并行 N 次)
• 输入:单文件 diff + 该文件源码(裁剪)
• 关注:行级问题、空安全、API 误用、资源泄漏
• Prompt 简单,质量高,并发安全
↓
Layer 2: 跨文件 Review(1 次)
• 输入:每个文件的 review 摘要 + 文件签名
• 关注:跨文件状态一致、调用关系、模式违反
• 上下文小(只是摘要),但能发现孤立 review 看不到的问题
↓
Layer 3: 架构级 Review(可选,1 次)
• 输入:MR 描述 + 改动文件清单 + 关键摘要
• 关注:架构合理性、设计模式、是否符合项目分层约定
• 仅大型 MR 触发,结果作为人工 reviewer 提示
5.1 文件级 Review 并行化
async def file_level_review(files):
sem = asyncio.Semaphore(5)async def _one(f):
async with sem:
return await llm_review(
file=f,
prompt=FILE_PROMPT,
max_tokens=20_000,
)tasks = [_one(f) for f in files]
results = await asyncio.gather(
*tasks, return_exceptions=True)
return [r for r in results
if not isinstance(
r, Exception)]
实测 30 文件 MR:串行 ~8 分钟,并行 ~90 秒。
5.2 跨文件 Review 怎么写 Prompt
这一层是最有"魔法"的。Prompt 模板:
# 跨文件 Review Prompt你是一个 Android 高级工程师,
负责 code review 的最后把关。以下是某 MR 中各文件的 review
摘要,每个摘要包含:
- 文件路径
- 改动内容简述
- 单文件 review 已发现的问题
- 该文件对外暴露的关键接口{file_summaries}请只关注**跨文件的潜在问题**:
1. A 文件改了数据结构,
B 文件序列化是否同步?
2. 接口签名变更,所有调用方
是否都已更新?
3. 状态管理在多个 ViewModel
之间是否一致?
4. 协程/线程切换在跨层调用
时是否合理?输出格式:JSON 数组,
每个元素 {files: [...],
issue: "...",
severity: "high|medium"}不要重复单文件 review 已发现
的问题。
5.3 一个让团队信任度飙升的小技巧
每条 AI 评论加一个置信度标记:
[AI · 高置信] LiveData 没有移除
observer,会导致内存泄漏[AI · 建议] 命名 'mData' 不太
符合 Kotlin 风格,建议改为
'data'[AI · 待人工确认] 业务逻辑可能
不符合 PRD,请 reviewer 验证
开发者一眼就知道哪些必须改、哪些可以讨论、哪些只是参考。我们这个改动让 AI 评论的"被采纳率"从 35% 涨到 68%。
六、实战收益数据
加了这四道护栏前后的对比(基于我们团队 6 个月数据):
指标 | 加护栏前 | 加护栏后 | 变化 |
|---|---|---|---|
单 MR 平均 token | 95k | 38k | -60% |
大 MR 失败率 | 47% | 4% | -91% |
Review 平均耗时 | 4.2 分钟 | 1.8 分钟 | -57% |
评论被采纳率 | 35% | 68% | +94% |
月度 LLM 成本 | ¥18,000 | ¥6,200 | -65% |
人类 reviewer 工作量 | 100% | 35% | -65% |
最有意思的不是成本下降,是"被采纳率从 35% 涨到 68%"。
这说明什么?说明之前 AI Review 不被信任,根源不在 AI 本身——而在于上下文喂得太多,AI 开始"主动找事",制造了大量噪音。精准、节制的 AI Review,比"塞越多越好"的 AI Review 更有价值。
七、回到那个 89 条评论的 MR
写到这儿,回到开篇那个被 89 条评论刷屏的 MR。
我们后来用上面这套架构重新跑了一遍:
• 47 → 23 个文件(黑名单过滤掉自动生成代码)
• 220k → 65k token(智能裁剪 + 分层调用)
• 89 → 31 条评论(去重 + 置信度过滤)
• 47 分钟 → 2 分 50 秒
PM 那次没再 @ 我,开发者反馈"这次 AI 看出了一个我们忽略的协程泄漏"。
AI Code Review 不是"接上 LLM 就能用"的简单事——demo 阶段你看到的 80% 工作量在 prompt,剩下 20% 在工程;生产环境正好反过来,80% 工作量都在防爆护栏。
本文是「AI Code Review 系列」第二篇,承接上一篇《AI Code Review:让每一行代码都有AI审查员》。如果你对其中某一道护栏的实现细节感兴趣,欢迎在评论区留言,我会针对性补一篇深挖。