- 书籍: Database Playbook
- 系列: Pocket Guides for Developers — 8 本关于 agents、RAG、提示词、可观测性、事件、数据库和系统设计的简短书籍 — 完整系列
- 我的项目: Hermes IDE | GitHub — 一个面向使用 Claude Code 和其他 AI 编码工具的开发者的 IDE
- 我: xgabriel.com | GitHub
工单说:为订单添加软删除。Claude Code 在分支 agent-a 上处理了此事,Codex 在分支 agent-b 上处理了此事,两者都报告了工作完成。于是你按显而易见的方式比较这两个分支:
git diff agent-a agent-b
Git 按你的要求执行。它会比较两个分支的最新提交(git diff docs),于是 Claude 添加而 Codex 未添加的每一行会显示为删除,而 Codex 添加而 Claude 未添加的每一行会显示为新增。两个智能体以相同方式编写的行会消失。你实际上是在读取“撤销 Claude,然后应用 Codex”这一个补丁,两种方法的更改逐行交织在一起。
Git 自 version 2.19 起就有了处理此问题的命令:git range-diff。它会把两个分支上的提交配对,然后比较补丁,而不是比较文件。
设置竞赛:一个工单,两个工作树
完整工单如下:DELETE /orders/:id 隐藏订单而不是删除该行,并且 GET /orders 会跳过被隐藏的订单。这需要一次模式更改,而这就是两个智能体往往会做出不同调用的地方。
为每个智能体分配独立的分支和工作目录,使用 git worktree:
cd ~/code/app
git worktree add -b agent-a ../app-agent-a main
git worktree add -b agent-b ../app-agent-b main
-b 会在 main 分支上创建分支并在新文件夹中检出。默认情况下,Git 拒绝检出另一个工作树已经拥有的分支,这样两个智能体就不会最终共享同一个分支。
工作树是一个全新的检出,因此像 .env 这样的被 git 忽略的文件不在其中(Claude Code 工作树文档)。将你的应用所需的文件复制过去,并在两个文件夹中安装依赖,否则智能体测试运行会因与工单无关的原因而失败。
现在启动智能体。在第一个终端中,Claude Code 使用 claude "query" 将工单作为第一个提示词:
TICKET='Add soft delete to orders: DELETE /orders/:id
sets a flag instead of removing the row, GET /orders
skips deleted orders. Write a migration and tests.
Commit your work when the tests pass.'
cd ~/code/app-agent-a && claude "$TICKET"
在第二个终端中,执行 cd ~/code/app-agent-b,运行 codex 并粘贴相同的文本。文字相同,起始提交也相同,因此分支之间的任何差异都来源于这些 Agent。
Claude Code 自带的 --worktree 标志用于 Claude 会话,位于 .claude/worktrees/<name>。使用两个供应商时,普通的 git worktree 会将两个文件夹保持在同一命名方案下。
工单的最后一行很重要:range-diff 比较的是提交,而不是工作树。如果 Agent 在有未提交的更改时停止,运行 git -C ../app-agent-b status,查看内容,然后自行提交。
第一遍:每个 Agent 触及了什么?
在阅读任何代码之前,先了解每个回答的形状:
git log --oneline main..agent-a
git log --oneline main..agent-b
git diff --stat main...agent-a
git diff --stat main...agent-b
在 git log 中,两个点列出了 main 没有的 Agent 分支提交。在 git diff 中,三个点把分支与它从 main 分离的点比较,这样你只能看到 Agent 的更改(git diff 文档)。
检查以下三点。每个分支是否都有迁移文件?每个分支是否都有测试?哪些文件只出现在一个列表而不在另一个列表中?如果一个 Agent 只改动了少量文件,而另一个 Agent 改动了十几个文件(其中可能包括锁文件或 CI 配置),那么你已经对第二个分支提出了问题。

range-diff 通过比较补丁来将两个分支上的提交配对。
git range-diff: 配对提交,比较补丁
两个分支都从 main 开始,因此两种形式都有效:
git range-diff main..agent-a main..agent-b
# same thing, shorter:
git range-diff main agent-a agent-b
三参数形式的文档说明相当于传入 <base>..<rev1> 和 <base>..<rev2>(range-diff 文档)。这是这些文档中的示例输出,展示了你可以得到的所有类型的行:
-: ------- > 1: 0ddba11 Prepare for the inevitable!
1: c0debee = 2: cab005e Add a helpful message at the start
2: f00dbal ! 3: decafe1 Describe a bug
@@ -1,3 +1,3 @@
Author: A U Thor <author@example.com>
-TODO: Describe a bug
+Describe a bug
@@ -324,5 +324,6
This is expected.
-+What is unexpected is that it will also crash.
++Unexpectedly, it also crashes. This is a bug, and the jury is
++still out there how to fix it best. See ticket #314 for details.
Contact
3: bedead < -: ------- TO-UNDO
(文档将内部块缩进四个空格。我把它改为两个空格,以便在这里显示行。)
每个标头行将第一个范围(左)的提交与第二个范围(右)的提交配对。
-
=表示两个补丁相同。 -
!表示提交对应但有差异,其下缩进块是这两个补丁的差异。 -
>左侧带破折号,是仅第二个范围拥有的提交。 -
<右侧带破折号,是仅第一个范围拥有的提交。
在 ! 块中,将第一列视为 range-diff 标记,第二列视为原始补丁。-+What is unexpected... 是第一个补丁添加、第二个补丁未添加的行。++Unexpectedly, it also crashes... 是仅第二个补丁添加的行。在你的比赛中,-+ 行属于 Claude,++ 行属于 Codex。
该块还以作者和信息行开头,因为 range-diff 在决定哪些提交对应时会比较“作者信息、提交消息和提交差异”。两个智能体很少写出相同的提交消息,因此可以跳过每对的这部分。
当两个答案无法配对时
两个独立的解决方案可能返回为一个 < 行和一个 > 行,中间没有 !。range-diff 认为这些补丁差异太大,无法视为同一次提交的两个版本。这已经是一个答案:智能体选择了不同的设计。
无论如何想看到补丁的差异,可以提升创建因子。它的默认值是 60,文档建议在 range-diff 错误地将大量更改视为完全重写时尝试更大的值:
git range-diff --creation-factor=100 main agent-a agent-b
如果它们仍然无法配对,请进一步提高。
提交次数导致另一种不匹配。如果 Claude 提交了三次而 Codex 只提交了一次,最多只能形成一对。请改为比较压缩后的副本,这样既不会动到任何一个分支:
base=$(git merge-base agent-a agent-b)
a=$(git commit-tree 'agent-a^{tree}' -p "$base" -m squashed)
b=$(git commit-tree 'agent-b^{tree}' -p "$base" -m squashed)
git range-diff "$base" "$a" "$b"
git commit-tree 会根据树创建一个新的提交对象并打印其 ID。每个都保存着共享基础之上一个分支的最终文件,使用相同提交信息,提交信息噪声也随之消失。没有任何分支指向它们,且智能体的分支保持原样。
range-diff 也接受 pathspec,用于将两个范围限制到这些路径。对于此票据,从以下 schema 开始:
git range-diff --creation-factor=100 "$base" "$a" "$b" \
-- db/migrations
两个独立的迁移是共享很少的小补丁,因此它们仍可能以 < 行和 > 行的形式出现。如果出现这种情况,请用 git diff main...agent-a -- db/migrations 并排查看,同样也适用于 agent-b。
先阅读迁移再查看代码
假设 Claude 的分支存储一个时间戳,而 Codex 的分支存储一个标志:
-- agent-a
ALTER TABLE orders ADD COLUMN deleted_at timestamptz;
-- agent-b
ALTER TABLE orders
ADD COLUMN is_deleted boolean NOT NULL DEFAULT false;
两种方式都不会重写表。PostgreSQL 18 文档指出,非易失默认值存储在表的元数据中,而没有默认值的列为 NULL,且「在这两种情况下都不需要重写表」。因此选择取决于语义:deleted_at 记录订单被隐藏的时间,而标志位则不记录。
在每个分支的迁移中,查找:
- 易失默认值,例如
clock_timestamp()。根据同一页文档,这会导致整个表及其索引被重写。 - 在
orders表上创建普通的CREATE INDEX。标准索引构建会「在完成之前锁定表的写入(但不锁定读取)」(CREATE INDEX 文档)。 - 在你的工具把迁移放在事务里时使用
CREATE INDEX CONCURRENTLY。它不能在事务块中运行,因此迁移在运行时会失败。 - 查询更改,在新列上进行过滤,但没有匹配过滤条件的索引。
在两个分支上运行测试,然后合并其一
从主检出目录,在各自文件夹中运行每个分支测试。用你自己的测试命令替换 npm test:
for br in agent-a agent-b; do
echo "== $br"
git diff --stat main..."$br" | tail -n 1
if (cd "../app-$br" && npm test); then
echo "$br: tests pass"
else
echo "$br: tests fail"
fi
done
测试不通过的分支直接淘汰。在两个通过测试的分支之间,先选在繁忙的 orders 表上运行迁移。再选惊喜文件更少的较小 diff。如果 range-diff 将这些提交配对,你就在选择同一个修复的两种风格。如果没有配对,你就在选择一种设计。
落地胜者并清理:
git merge --no-ff agent-a
git worktree remove ../app-agent-a
git worktree remove ../app-agent-b
git branch -d agent-a
git branch -D agent-b
git worktree remove 会拒绝删除包含已修改或未跟踪文件的文件夹,除非你传入 --force(文档)。保持该标志关闭,这样残留的工作会阻止你。-D 用于未合并的失败分支。
Hermes IDE 中的同样的竞态
我构建 Hermes IDE,因此在阅读此部分时请结合这一点考虑。按下 ⌘N(Windows 和 Linux 上为 Ctrl+Shift+N),输入工单,选择 Claude Code,然后启动。再用 Codex 重复一次。每个任务都在各自的工作区中以 hermes/<name> 分支启动,工单作为智能体的首个提示词。新工作区会在几秒钟内获取依赖并分配独立端口,这覆盖了上面设置步骤的一半(安装部分)。

工作台的 Worktrees 标签页:分别为 orders-api、web-app 和 orders-types 各有一个 Hermes 工作区,每个工作区都有其 hermes/ 分支、任务、年龄和磁盘大小,以及剩余磁盘空间。
这些分支是普通的 Git 分支,因此本文中的所有操作都适用于它们。运行 git branch --list 'hermes/*' 可以得到两个分支名,而 git range-diff main hermes/<one> hermes/<other> 可以比较它们。Hermes 不会为你自动执行 range-diff。
它所补充的正是评审的另一半。每个智能体的轮次都会被保存,因此你可以撤销地对单个轮次进行差异比较或恢复。按下 ⌘G(Ctrl+Shift+G)打开工作台,它会按轮次或按文件展示每个智能体的更改,并带有风险标记,同时会将行注释发回给编写该行的智能体。range-diff 会告诉你两个答案之间的差异。轮次视图会告诉你每个智能体是如何得到其答案的。

工作台(⌘G)在按轮次视图中显示的 Claude Code 任务(位于 orders-api):共有两轮,第一轮的 diff 展示了在 src/db.ts 中添加 refunded 状态的更改、已更改文件列表,以及根据这些轮次草拟的提交信息。
当你选定后,Land 按钮会提交、打开拉取请求或将更改合并到任务起始的分支,并且可以撤销。你的检查可以决定任务何时算完成。Hermes 是免费的,采用 BSL 1.1 协议开源,可在 macOS、Windows 和 Linux 上运行。它与 Anthropic、OpenAI 或 Nous Research 无关。点此下载,源码位于 GitHub。
如果这对你有帮助
了解 range-diff 后,这次竞赛的 Git 部分就变得机械化。难点在于 schema:是时间戳还是标志,是当前索引还是后续索引,或者是可以在繁忙表上运行的迁移。Database Playbook 是我编写的简短指南,帮助你为正在构建的系统选出合适存储和形状。它是 8 份简短的 Pocket Guides for Developers 之一,内容涵盖 agents、RAG、提示词、可观测性、事件、数据库和系统设计:完整系列在此。
