Jujutsu 的代码审查工作流,对 LLM 做代码审查有什么启发?

最近读到 Ben Gesoff 的一篇文章,讲他用 Jujutsu (jj) 这个版本控制系统来做大型 PR 审查。核心思路很漂亮:用 jj duplicate 复制变更 → jj new --insert-before @ 创建一个空提交作为”已审查”锚点 → 逐文件 jj squash 把已审文件标记到父提交中。审查状态被持久化到版本历史里,不需要在脑子里记账。

读完后我一直在想一个问题:这套思路对 LLM 做代码审查有没有指导意义?

直接说结论:有,但不是你以为的那种”让 LLM 也用 jj”。 真正有价值的是它对审查过程本身的建模方式。


审查的本质是什么

先拆清楚 Gesoff 的工作流到底做了什么。

表面上是三个 jj 命令。但底层是一个状态机

1
待审查 → 部分完成 → 全部完成 → 输出审查意见

每个文件经历的状态转换是:待审审查中已审(squashed)。状态被记录在版本历史中,而不是审查者的脑海里。

这个模型的关键特征是:

  1. 增量——一次只处理一个文件,限制信息摄入量
  2. 收敛——每处理完一个文件,将其结论压缩到累积状态(父提交)中
  3. 可暂停——状态持久化,随时中断随时恢复
  4. 可验证——jj diff --stat 一目了然地显示还剩什么

这四个特征,恰恰是 LLM 做代码审查时最难做好的地方。逐个来看。


增量:一次只消化一个 chunk

LLM 的 context window 从 8K 涨到 128K 再到 200K,看起来好像可以塞下整个 PR 了。但能吃 ≠ 能消化

当你在一个 prompt 里塞了 40 个文件的 diff,模型的表现和人类面对同样场景时一样——开头几段分析最详尽,中间开始泛泛而谈,尾部出现模式化的套话。这并不是 context window 不够,而是模型在长上下文中对信息的分辨率会衰减(众所周知的”lost in the middle”效应)。

Gesoff 的做法是一次处理一个文件,squash 完再看下一个。LLM 审查也应该遵循同样的节奏。

可行的做法是这样:不要让模型一次性看见整个 PR。构建一个迭代循环:

1
2
3
4
5
对于每个文件/逻辑chunk:
1. 给模型当前文件 + 累积审查摘要
2. 模型输出对该文件的意见
3. 将意见收敛到累积摘要中
4. 进入下一个文件

累积摘要在这里扮演的角色,就是 jj 的”已审查提交”——一个不断增长的收敛状态。


收敛:把”看完了”变成一段摘要

这是整个模式里最关键的一步,也是最容易被忽视的。

Gesoff 用 jj squash 把已审文件从 working copy 移到父提交。这个操作本质上是一个信息压缩——文件的逐行 diff 不再参与后续的认知处理,取而代之的是一个结论(”这个文件没问题”或”这个函数需要改名”)。

LLM 审查中对应的做法是:不要让模型保留所有已审查文件的完整内容。相反,让它为每个已审文件生成一段精炼的结论,并只把这段结论传给下一轮。

举个例子,不好的做法是:

1
用户:审查以下 3 个文件。文件1:[完整diff]。文件2:[完整diff]。文件3:[完整diff]。

更好的做法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
回合 1:
用户:审查文件 A。输出你的结论。
模型:[分析,结论]

回合 2:
用户:文件 A 结论是"逻辑正确,命名需改进"。
现在审查文件 B。
模型:[分析,结论]

回合 3:
用户:累积结论如下——
文件 A:逻辑正确,命名需改进
文件 B:缺少边界条件检查
现在审查文件 C。
模型:[分析,结论]

每轮只给模型它当下需要的信息(当前文件 + 之前的收敛结论),而不是让它自行在几十个文件的 diff 中筛选相关部分。

这个迭代模式的可靠度远高于一次性塞入。它在每次对话中只暴露有限的信息,审查质量可以通过每轮独立评估来验证。


可暂停:LLM 不需要,但流程需要

Gesoff 工作流的一个卖点是”随时放下,回来时一切如初”。这对人类审查者很重要——短期记忆不可靠。

LLM 不需要这个特性,因为它的”记忆”只存在于当前对话中。但如果你把审查拆成多轮、甚至多个对话,你需要一种方式来传递累积的审查状态。这就是收敛阶段生成的摘要的作用——它充当了跨越对话边界的”检查点”。

特别是当 PR 大到超过单次对话的 token 预算(无论是成本还是 context window)时,分段审查 + 序列化状态是唯一的出路。这和 jj 处理超大型变更的策略是同一个道理——不是一次搞定,而是切碎、消化、汇总、再继续。


可验证:进度可视化

jj 审查的进度是透明的——jj diff --stat 告诉你还剩多少文件。LLM 审查也应该有类似的可视化。

具体来说,审查进度可以这样设计:

文件 状态 结论
src/api.py 已审 结构合理,命名需改进
src/db.py 已审 缺少连接池,已标注
src/utils.py 审查中
src/config.py 待审

这是一个简单的标记,但它的存在改变了审查体验——你不必猜测 LLM 看到了哪些文件、跳过了哪些。它既是进度提示,也是质量审计的基础。


还要更深一层:从消费到操作

前面几点是”把 jj 的工作流模式翻译给 LLM”,但 Gesoff 的工作流里有一个更深层的转变——审查者从消费者变成了操作者

传统的 web UI 审查中,审查者是 diff 的消费者:你看别人写的差异,然后给出评价。Gesoff 的工作流里,审查者在本地操作这些变更,用自己的操作来标记判断——squash 一个文件等于说”这没问题”。

这对 LLM 审查的启示是:

“消费”模式的局限——只让 LLM 读 diff 然后给意见,信息流是单向的。模型只能对已有的代码做出反应,无法验证自己的判断。你问它”这段代码有 bug 吗”,它可能猜错但你没法发现,因为它不会去跑测试来确认。

“操作”模式的潜力——让 LLM 在本地仓库中直接操作变更,用执行来验证判断:

消费模式 操作模式
“这段代码有 bug” 复现 crash,确认后再报告
“这个变量命名不好” 重命名后运行测试,确认无破坏
“这个逻辑应该抽成函数” 直接重构,编译通过后给出 diff
“这个边界条件没处理” 补一个测试用例,运行通过后附加到 PR

这是一个彻底的角色转换:LLM 不是审查的观察者,而是审查的操作者。 它把自己的审查意见落实为可验证的代码变更,然后用户只需要 review 这些变更——一个 meta-review,而不是从头读 diff。

这种模式已经在一些代理式 IDE 中初步出现了。Cursor Agent 可以在审查过程中直接修改代码,Claude Code 可以运行测试来验证假设。它们不再满足于告诉你”这里有风险”,而是会直接修给你看。


可行性:哪些现在就能做,哪些还需要等

现在就能做的:

  1. 迭代式审查 prompt——把大 PR 拆成小 chunk,每轮只审查一个 chunk,累积摘要传给下一轮。不需要任何特殊工具,只是 prompt engineering。

  2. 审查进度表——在 prompt 中维护一个 Markdown 表格,记录每个文件的审查状态。LLM 每轮更新这个表格。人类可读、可审计。

  3. 分阶段反馈——第一阶段只看结构和设计(不读具体代码),第二阶段读关键模块的核心逻辑,第三阶段做逐行细审。每个阶段的输出作为下阶段的上下文。

需要工具支持的:

  1. 审查状态持久化——跨对话的审查状态传递。目前需要用外部文件或数据库来存累积摘要和工作进度。短期内可以在文件系统上用 JSON/Markdown 做简单的 checkpoint。

  2. 本地代码操作——让 LLM 直接操作本地仓库的代码变更(而不是光读 diff)。这需要代理式工具的支持(Cursor、Claude Code 等),纯 Web 端的 Code Review 做不到。

  3. 自动化评论提交——LLM 审查完所有文件后,自动把 inline 评论批量提交到 PR 平台。类似 Gesoff 正在构思的 jj interdiff 脚本。


坦率地说:几件我还不太确定的事

LLM 的收敛质量可靠吗? 人类在 squash 一个文件时,知道什么是”看完了”——LLM 呢?它可能会漏掉关键问题但自信满满地给出”这个文件没问题”的结论。如果收敛的质量不可靠,整个增量策略就建立在流沙上。目前的缓解措施是让收敛摘要包含具体依据(”因为 X、Y、Z 原因,这个文件在功能上正确”),而不是笼统的判断。

增量策略会不会丢失跨文件的关联性? 一个变更的 bug 往往不在单个文件里,而在多个文件的交互中。如果你把它们分开审查,可能每个文件看着都没问题,但合在一起就有问题。Gesoff 的应对是审查者对整个变更有一个”大局理解”之后再逐文件细审——但 LLM 没有这个先验的大局理解。可能需要第一阶段做全局速览,第二阶段再做增量收敛。

操作模式的反馈回路成本多高? 让 LLM 跑测试、改代码来验证自己的审查判断,听起来很对,但每次操作都有延迟和成本。如果 PR 里有 30 个需要验证的点,全部跑一遍可能比人肉身审查还慢。需要区分”哪些判断值得验证”和”哪些靠读代码就够了”。


最后

Gesoff 的工作流给我的真正启发不是 jj 的命令行技巧,而是它重新定义了审查者和代码变更之间的关系。审查不是”从外部评价代码”,而是”在内部逐步消耗变更”。

把这个思路应用到 LLM 审查中,核心的迁移不是技术栈(不需要 LLM 学会 jj),而是将审查从一次性反应改造成一个迭代的、可收敛、可暂停、可验证的过程

当你的 PR 大到”一次性看不过来”的时候,也许问题不在于工具不够强,而在于你试图一次性看完它。


这篇文章的部分想法来源于 Ben Gesoff 的 Reviewing large changes with Jujutsu