Article
多智能体并行编码的符号级冲突预防与语义合并
多智能体并行编码的符号级冲突预防与语义合并
在同一个仓库上跑多个 AI 编码智能体,很快就会撞墙。一个重构 auth,一个写测试,一个修导航 bug —— 它们互相覆盖文件,抢同一个分支,最后得从 git reflog 里倒推发生了什么。
过去 18 个月,社区的解法收敛到了 git worktrees:每个智能体一个独立工作目录,各占一个分支,共享一个 .git。Claude Code、Codex、Cursor 都内置了 worktree 隔离。有效。两个 worktree 里的智能体碰不到对方的文件。
但 worktrees 只解决了一半问题。
隔离解决不了集成
Worktrees 防止智能体工作时互相踩。对合并时发生什么,它什么都不做。而痛点就在合并时。arXiv 上一项异步 SWE 智能体研究提到,冲突只在集成时暴露,修复”不是一行补丁,而是至少一个智能体工作的完整修订”。
触发我写这个工具的点:大部分”冲突”其实不算冲突。两个智能体各改各的函数,恰好挨在同一个文件里。谁都没碰对方的代码。但 git 按行合并,相邻的行块在它看来就是撞了。
举个例子:
base: ours: theirs:
function a(){return 1;} function a(){return 111;} function a(){return 1;}
function b(){return 2;} function b(){return 2;} function b(){return 222;}
ours 只动了 a。theirs 只动了 b。正确结果只有一个。问 git:
$ git merge-file -p ours.js base.js theirs.js
<<<<<<< ours.js
function a(){return 111;}
function b(){return 2;}
=======
function a(){return 1;}
function b(){return 222;}
>>>>>>> theirs.js
冲突了。得人来解(或者再花一次智能体调用),因为 git 不知道什么是”函数”。
符号级锁:防止碰撞
symlock 做的第一件事:不让两个智能体编辑同一个符号(函数/类/方法)。编辑前先声明:
$ symlock symbols auth.ts
function login L1-3
function logout L5-7
class TokenStore L9-16
method TokenStore.issue L10-12
$ symlock claim auth.ts login # agent A → ok
$ symlock claim auth.ts logout # agent B → ok, different function
$ symlock claim auth.ts login # agent B → CONFLICT (exit 2)
CONFLICT: login (L1-3) overlaps an active claim:
- login held by agent A (L1-3)
符号边界用 tree-sitter 解析,不是正则。TypeScript/JavaScript、Python、Go、Rust 都一样用 —— 包括 Go 的接收者方法(Server.Start)和 Rust 的 impl 方法(Cache.get)。声明存在 .symlock/locks.json,靠 OS 文件锁保护,不同 worktree 的智能体可以安全竞争。退出码是契约:2 表示”有人占了重叠区域,退开”。编排器可以分支处理;智能体 skill 可以让智能体转去改别的而不是死等。
预防成本低,该做默认。但有时候两个智能体改同一个文件是合理的。这是第二部分。
符号感知的三向合并
symlock merge 做保守的符号感知三向合并。同样的输入,git 直接放弃:
$ symlock merge --base base.js --ours ours.js --theirs theirs.js
function a(){return 111;} # your change
function b(){return 222;} # their change, cleanly combined
算法分三步:
- 先用
diffycrate 跑一遍纯行级三向合并。干净就直接用 —— 文本合并已经正确的情况下没必要复杂化。不相邻的编辑本身就能合并成功。 - 只有冲突时,解析三个版本,判断一个窄条件:
ours和theirs是否改了不相交的顶级符号集合,且符号之外的内容(导入、排版、兄弟代码)完全没变?如果是,那”冲突”只是文本相邻导致的,直接取各方修改的符号版本拼回文件。 - 其他情况一律交人。两边改了同一个函数?冲突。符号增删改名?冲突。导入行挪了位置?冲突。重名符号?解析失败?全是冲突。
最后一道闸:重建后的文件重新解析、重新校验符号集,对不上就丢弃。
核心原则:合并错了比不合并严重得多。拒绝一次,代价是手动解决。合错一次,代价是生产环境的静默 bug。两边不对称,工具处理方式也不对称。
两个 bug,一个原则
说两个我自己踩过的坑。保守这条路不是拍脑袋选的,是踩坑踩出来的。
Bug 1 — CRLF。 最初的重建逻辑按行拆文件,用 \n 拼回去。喂一个 Windows 文件(\r\n 结尾)进去,它高高兴兴”合并”了 —— 顺手把全文件的行尾都改成了 LF。这正是工具要防的静默损坏。修复方式不是聪明地处理 CRLF,是检测到 \r 就直接拒绝,回退到字节级保留的行级冲突。保证不了正确,就不做。
Bug 2 — 合并驱动根本没工作。 README 里写了怎么把 symlock 配成 git 合并驱动。我写了个集成测试,跑真正的 git merge……次次都落到冲突。原因:Git 传给合并驱动的三个版本是临时文件,没有有意义的扩展名。我的语言检测靠扩展名,认不出来就放弃,返回”不支持的语言”。宣传的功能从来没跑通过。Git 会通过 %P 传真实路径,修复就是加个 --path 参数 —— 但如果没有端到端测试来验证我写进文档的用法,我根本发现不了。
Bug 2 的收获:文档里写了什么工作流,就测那个工作流。合并函数的单测全绿。用户实际会跑的东西是坏的。
现在没问题了,可以作为 git 公民正常工作:
# .gitattributes
*.ts merge=symlock
*.go merge=symlock
# .git/config
[merge "symlock"]
driver = symlock merge --base %O --ours %A --theirs %B -o %A --path %P
配好之后,git merge 自动解决不相交符号的碰撞;两边真改了同一个函数,就出正常的冲突标记。每次提交 CI 都会验证这两种情况。
为什么不用 LLM 合并
LLM 合并的结果看起来合理,但不可证明。它解了你的冲突,顺手改了隔壁的拼写,还悄悄”优化”了一行没人让它碰的代码 —— 你不知道哪行。对于”两个编辑不重叠,拼起来”这种机械任务,不需要模型的判断,需要的是确定性。symlock 的合并是确定的:输入一样,输出永远一样,每一步都可以手动核对。LLM 负责写代码,合并负责正确。
它是什么
symlock 不是编排器,也不是仪表盘 —— 那类东西已经很多了,基于 worktree 的会话管理器做得也不错。它是底下缺的那层基础设施:约 6MB 的静态二进制,JSON 输出,退出码契约,可以作为 git 合并驱动或智能体 skill 用。任何编排器都能调它 —— Claude Squad、cmux、Vibe Kanban,或者你自己写的。Apache-2.0,开源,没有”团队人数少于 N 免费”的附加条件。
目前支持四种语言,17 个测试(包含反例)。一条硬规则:正确,否则拒绝。
- 代码:https://github.com/echoVic/symlock
- 安装智能体 skill:
npx skills add echoVic/symlock - 安装二进制:
cargo install --git https://github.com/echoVic/symlock
如果你在并行跑智能体,又烦手动解那些 git 以为是冲突的合并,可以试试 —— 遇到该拒绝但没拒绝的情况,告诉我。
Keep Reading