数据截至 (上游 commit f1087ff151b0)
Codex apply_patch — 模糊补丁格式与四级容错匹配
这章讲什么: Codex 不让模型重写整个文件,也不用标准 unified diff,而是用一套自家的
apply_patch格式。本章讲它长什么样、为什么这么设计,以及最精彩的部分——定位改动位置时的四级容错匹配,让「模型大致写对」也能精确落地。这是整个项目最值得单独拿走借鉴的工程亮点。
1. 它要解决的小问题
让模型改代码,有三种常见路子,各有痛点:
| 路子 | 痛点 |
|---|---|
| 整文件重写 | 模型得逐字复现整个文件,长文件又慢又容易把没动的地方改坏 |
标准 unified diff(@@ -12,7 +12,9 @@) | 行号必须精确,模型经常算错行号导致补丁打不上 |
| 行号编辑(「把第 42 行换成…」) | 模型对行号同样不可靠,且文件一变全乱 |
Codex 要的是:模型只需大致描述「改哪段、改成啥」,不必精确到行号、甚至不必逐字复现上下文,系统也能可靠地把改动落到正确位置。
2. 思路/直觉:用「上下文锚点」代替「行号」
核心点子:别让模型记行号,让它给几行紧挨改动的原文当「锚点」。系统拿锚点去文件里搜,搜到了就在那儿动手。
这跟 git apply 容忍上下文漂移是一个思路,但 Codex 把容忍度做得更高——锚点甚至不必和原文一字不差(见 §5)。
格式用一组醒目的 *** ... 标记包起来,模型很难写错边界:
*** Begin Patch
*** Update File: src/seek_sequence.rs
@@ fn normalise(s: &str) -> String
s.trim()
.chars()
- .map(|c| match c {
+ .map(|c| match c { // 归一化常见 Unicode 标点
*** End Patch
读法:@@ 后面那行是上下文锚点(通常是函数/类定义那一行),用来缩小搜索范围;下面以 (空格,保留)、-(删)、+(增)前缀逐行描述改动——和 diff 像,但没有行号。
3. 支持的几种 hunk
一个补丁由若干 hunk 组成,Codex 只支持三种动作,简单到模型很难用错:
| Hunk 类型 | 标记 | 作用 |
|---|---|---|
| 新增文件 | *** Add File: <路径> 后跟若干 + 行 | 整文件新建 |
| 删除文件 | *** Delete File: <路径> | 删整个文件 |
| 更新文件 | *** Update File: <路径>(可带 *** Move to: 重命名) | 在文件内做若干上下文锚定的改动 |
这三种就是 Hunk 枚举(apply-patch/src/parser.rs:65 起):AddFile、DeleteFile、UpdateFile { path, move_path, chunks }。每个更新文件的改动是一个 UpdateFileChunk,带 change_context(那行 @@ 锚点)+ 一段要替换的连续行。
格式的权威语法是一段 Lark 文法,就写在 parser 顶部注释里:
// codex-rs/apply-patch/src/parser.rs:6 起(节选)
// start: begin_patch environment_id? hunk+ end_patch
// add_hunk: "*** Add File: " filename LF add_line+
// update_hunk: "*** Update File: " filename LF change_move? change?
// change_line: ("+" | "-" | " ") /(.+)/ LF
解析入口是 parse_patch(parser.rs,经 lib.rs re-export)。注意 parser 只解析与校验,不碰文件系统;它有意比文法更宽松,容忍标记前后的空白(注释 parser.rs:23-24)。