对比单次投递、文件分块、检索选择、MapReduce四种PR打包方式,量化了Token成本、覆盖完整度和延迟差异。
一个 Pull Request 的 diff 如何打包进 LLM Prompt,对审查成本、覆盖率、可靠性产生的影响,往往比模型选择本身更大。四种打包策略的对比——单次投递、按文件分块、检索筛选、Map-Reduce 总结——表明没有一种策略能在所有形态的 PR 上取得绝对优势。选择哪种策略,取决于 PR 规模、Token 预算,以及漏掉一个 Bug 对团队的代价。
AI 审查机器人必须将无限制的 diff 塞进有限的上下文窗口。一个涉及四十个文件的 PR 轻松就能超出廉价模型的 Token 预算。粗暴的做法——直接粘贴整个 diff 然后祈祷——要么导致静默截断,要么账单爆炸。打包这一层决定了模型实际能看到 diff 的哪些部分,它理应得到和 Prompt 本身同等的设计关注。
以下四种策略代表了开源审查机器人和 Agent 框架中常见的模式。每种策略在 Token 成本、上下文完整性、延迟三个维度上各有取舍。
整个 diff 进入一条 Prompt,模型一次完成全部审查。这是最简单的策略,也是大多数教程展示的方案。
def pack_single_shot(diff: str) -> str:
return (
'Review this pull request diff. '
'Report bugs, style issues, and security problems.\n\n'
f'```
{% endraw %}
diff\n{diff}\n
{% raw %}
```'
)
单次投递保留了跨文件的上下文,这在 Bug 仅因两个文件同时变更才出现时尤为关键。它的弱点是硬上限:一旦 diff 超出上下文窗口,该策略要么失败,要么丢失尾部内容。
diff 按文件边界切分,每个文件作为单独的审查请求发出,结果最后合并。这是大多数 CI 机器人在经历第一次截断事故后采用的策略。
def split_by_file(diff: str):
files = []
current = []
current_path = None
for line in diff.splitlines(keepends=True):
if line.startswith('diff --git '):
if current:
files.append((current_path, ''.join(current)))
current = [line]
current_path = line.split(' b/')[-1].strip()
else:
current.append(line)
if current:
files.append((current_path, ''.join(current)))
return files
def pack_per_file(files):
return [
f'Review this file for bugs and security issues.\n### {path}\n```
{% endraw %}
diff\n{patch}\n
{% raw %}
```'
for path, patch in files
]
按文件分块去除了规模上限,因为每次调用只需容纳一个文件。代价是丢失了跨文件上下文:一次破坏 import 的重命名,或者一个文件中改动导致另一个文件中的假设失效,这类问题都捕捉不到。
机器人不再审查所有内容,而是先对 PR 描述和每个文件的开头若干行做 Embedding,然后选取与变更最相关的 top-K 个文件。这种策略适用于 monorepo——PR 涉及几十个文件,但实际语义相关的只有少数几个。
def select_files(pr_description: str, files, top_k=8):
# Pseudocode: swap in your preferred embedding API.
desc_vec = embed(pr_description)
scored = []
for path, patch in files:
score = cosine(desc_vec, embed(path + '\n' + patch[:2000]))
scored.append((score, path, patch))
scored.sort(reverse=True)
return scored[:top_k]
检索策略使 Token 成本与 PR 规模脱钩,变得平坦,这让它在超大 diff 场景下很有吸引力。风险在于 Embedding 模型可能漏掉一个文件——而这个文件重要的原因并未在描述中提及。
每个文件在第一轮被总结,然后第二轮对合并后的总结进行审查。这是文档问答 pipeline 和长上下文 Agent 常用的模式。
def map_reduce(files, summarize_fn, review_fn):
summaries = [summarize_fn(path, patch) for path, patch in files]
return review_fn('\n\n'.join(summaries))
Map-Reduce 能处理任意大规模 PR,并产出连贯的最终审查结果。它的代价是双倍延迟,而且可能丢失代码审查赖以生效的行级细节:一个有 Bug 的函数的总结往往会省略 Bug 实际所在的那一行。
为了让四种策略在同等条件下比较,团队构建了一个小型框架,对每种策略在一组带有预设 Bug 的语料上运行,并报告四项指标:Token 成本、审查覆盖率、延迟、Bug 捕获率。
import json, time
def benchmark(strategies, pr_diff, pr_description, seeded_bugs, review_fn):
results = []
for name, pack_fn in strategies.items():
start = time.time()
packed = pack_fn(pr_diff, pr_description)
output = review_fn(packed)
elapsed = time.time() - start
results.append({
'strategy': name,
'latency_s': round(elapsed, 2),
'tokens': estimate_tokens(packed),
'caught': sum(1 for b in seeded_bugs if b in output),
})
return results
该框架要求提供一个封装了模型调用的 review_fn,因此兼容任意 Provider。预设 Bug 语料是一组注入到测试仓库中的已知缺陷字符串,这让捕获率变得可衡量,而不是靠"感觉"。
对比没有产生单一赢家,而是产生了一张决策表。以下这张表是团队在将新仓库接入机器人时使用的版本。
表格背后的规律很简单:小 diff 用最简策略,每次升级都以牺牲上下文换取可控成本。那些对所有 PR 都用同一种策略的团队,最终要么多花钱,要么漏 Bug。
基准测试运行在开源项目 MonkeyCode 的免费服务器选项上,使用了其免费模型访问权限和一千万 Token 的免费额度。披露:本文作为 MonkeyCode 产品推广的一部分撰写。免费层之所以重要,是因为四种策略的对比将 Token 消耗乘以四倍,如果放在付费 API 上运行,这个实验就会从技术决策变成预算决策。
该框架使用基于字符数的 Token 估算,对比场景下足够准确,但不适合计费。预设 Bug 是合成产生的,因此捕获率是现实世界有效性的下界,而非承诺。结果同样取决于所选模型和仓库,这意味着决策表是一个起点,而非金科玉律。
对于 PR 始终较小的团队,直接用单次投递,把时间花在别处。需要全仓库推理(即答案依赖于 diff 之外的代码)的团队,应该使用检索增强的 Agent,而不是任何一种打包策略。有严格数据政策的团队,在优化打包层之前,先要确认向托管模型发送 diff 是否被允许。
Diff 打包是一个可调参数,而非固定的实现细节,上述四种策略构成了从最廉价到最注重上下文的完整光谱。本文中的框架可对接任意 Git 仓库,免费层足够用来在真实 PR 上尝试全部四种策略。数字会和上面的表格不同——而这个差异恰恰是团队做出正确选择所需的信息。