跳转到内容

编辑工具:apply_patch vs Edit

Chord 提供两种互补的文件编辑工具,针对不同模型的训练背景进行了优化。

特性 apply_patch 工具 Edit(替换)工具
格式 Codex 补丁信封(*** Begin Patch*** End Patch)+ @@ 差异块 文本匹配(old_string → new_string)
最适合 使用 OpenAI apply_patch 训练的模型 使用 Claude Code 或类似替换接口训练的模型
作用范围 单次调用多个文件:新增、更新、删除、移动 单次调用一个已有文件
位置控制 上下文行 + 可选的头部锚点 精确字符串匹配
多次出现 不适用(基于上下文) replace_all 参数
典型模型 gpt-5.5, gpt-5.3-codex, o3, o4 Claude, Qwen, GLM, MiniMax, DeepSeek, Gemini

Chord 自动根据当前模型选择合适的工具:

  • GPT/o 系列模型apply_patch(Codex 信封)
  • 所有其他模型edit(old_string/new_string)

你无需手动选择——系统只会向每个模型暴露合适的工具。

当补丁原生(GPT/o 系列)模型保留 apply_patch 时,Chord 还会隐藏 writedelete:信封本身已覆盖它们(*** Add File: 创建、*** Delete File: 删除),与这些模型训练时熟悉的原生 Codex CLI 工具面一致。回退组合会保留 write/delete:非 GPT 模型仅因 edit 被禁用才拿到 apply_patch 时仍能看到它们;补丁原生模型被降级到 edit 时也需要 write 才能创建文件。


单个 patch 参数携带完整的 Codex 信封,可以包含任意数量的文件操作:

*** Begin Patch
*** Update File: src/main.go
@@ func main() {
上下文行
-删除的行
+添加的行
上下文行
*** Add File: docs/new.md
+# 新文档
+第一行。
*** Delete File: tmp/old.txt
*** End Patch

支持的操作:

  • *** Add File: path —— 创建文件;每一行正文都以 + 开头。
  • *** Update File: path —— 用一个或多个 @@ 差异块修改文件。
  • *** Move to: newpath —— 更新的同时重命名;必须紧跟在对应的 *** Update File: 行之后。纯重命名也需要至少一个(可以只含上下文的)差异块。
  • *** Delete File: path —— 删除文件;无正文。
  • *** End of File —— 放在差异块之后,把该块钉在文件末尾(当同样的内容也出现在文件前部时很有用)。

差异块按顺序应用;每个块从上一个块的应用位置之后开始匹配第一个出现。针对同一规范化路径的连续普通 *** Update File: 段遵循 Codex 的顺序语义:每一段都在上一段的“内存结果”上继续修补,整个信封最终作为一个文件变更提交(因此后续段不匹配时工作区保持不变,不会暴露 Codex 的部分写入行为)。块内的行保留原始的 ' '/+/- 前缀,因此文件内容本身以 *** 加一个空格开头时仍是普通上下文——只有以不带前缀的 *** 加一个空格开头的行才是协议标记。

  • 你的模型使用 OpenAI 的 apply_patch 或类似的基于补丁的接口训练
  • 改动跨多个文件,或在修改内容的同时创建/删除/移动文件
  • 需要通过上下文行进行精确的位置控制
{
"patch": "*** Begin Patch\n*** Update File: main.go\n@@\n func main() {\n-\tfmt.Println(\"hello\")\n+\tfmt.Println(\"hello, world\")\n }\n*** End Patch"
}

旧的单文件参数({"path": ..., "patch": "@@\n..."})仍然被接受,会被规范化为针对该路径的 Update 信封。

你可以在 @@ 之后添加文本来帮助定位模糊的代码块:

@@ func processUser(id int) error {
if id < 0 {
- return errors.New("invalid")
+ return fmt.Errorf("invalid user ID: %d", id)
}

重要提示:只使用你已验证存在于文件中的头部。头部是软锚点:当头部文本找不到时,匹配会仅回退到块主体。

同一个信封内的所有操作会在修改任何文件之前基于同一份文件系统快照完成规划和验证。如果规划失败(文件缺失、同一路径存在重叠操作、Add 目标已存在),任何文件都不会改变。如果文件在规划与提交之间被外部修改,整个补丁会被拒绝。如果提交中途写入失败,已提交的操作会被回滚。

  • “hunk not found (N/M)”:指定差异块与当前文件不匹配。错误会包含简短的期望行摘要;如果能够判断,还会说明该文本只是某个较长行的片段,或位于前一个差异块之前。重新读取目标范围,并使用当前文件的完整行重建差异块。
  • “cannot add file that already exists”*** Add File: 的目标已存在;改用 *** Update File:
  • “apply_patch contains overlapping operations”:同一信封中的两个操作所触及的路径互为包含关系(例如 dirdir/file),或通过不同名称解析到同一个文件;把它们合并为一个操作。针对完全相同路径的连续 *** Update File: 段是被允许的,并按顺序应用。
  • “changed after planning”:文件在验证与提交之间被修改;没有任何写入——基于当前内容重试。

{
"path": "main.go",
"old_string": "fmt.Println(\"hello\")",
"new_string": "fmt.Println(\"hello, world\")",
"replace_all": false
}
  • 你的模型没有专门针对补丁格式进行训练
  • 改动很直接:查找精确文本 → 替换为新文本
  • 你想在文件中重命名变量/标识符(replace_all: true
  • old_string(必需):要查找的精确文本。必须精确匹配缩进、空白和换行符。
  • new_string(必需):替换文本。
  • replace_all(可选):true 替换所有出现,false(默认)仅替换第一个。
{
"path": "server.go",
"old_string": "const port = 8080",
"new_string": "const port = 3000"
}
{
"path": "handler.go",
"old_string": "userID",
"new_string": "userId",
"replace_all": true
}
  • “old_string not found in file”:精确文本不存在。检查空白、缩进和换行符。
  • “old_string found N times”:找到多个匹配。可以:
    • 添加更多上下文使其唯一
    • 设置 replace_all: true 如果你想替换所有出现
  • “old_string and new_string are identical”:无需更改。

工具会自动处理轻微的尾随换行符差异:

  • 如果 old_string 有最后的 \n 但匹配项没有(反之亦然),并且匹配是唯一的,编辑会继续。
  • 这减少了因换行符不匹配导致的重试。

两个编辑工具都不要求预先 read:它们在执行时都会读取当前磁盘内容。为了可靠编辑,仍建议遵循以下做法:

  1. 在尚未确认精确文本、路径或 hunk 锚点时,先检查目标区域。可以使用 readgreplsp
  2. 使用最小的唯一块(2-4 行)。大的上下文块更容易过时。
  3. 失败后重新读取。如果块或字符串匹配失败,文件可能已更改——在重试之前再次读取。

两个工具都很适用。根据模型训练选择:

  • apply_patch:当你需要位置控制时更好(例如,“更改此函数中的第一个出现”)。
  • Edit:对于具有清晰边界的简单查找替换更好。
  • Edit 配合 replace_all: true:在一个文件中重命名变量。
  • LSP 工具:用于跨多个文件的符号感知重命名。
  • 创建、删除或移动文件 → apply_patch 原生支持(*** Add File: / *** Delete File: / *** Move to:);使用 edit 的模型用 WriteDelete
  • 跨多个文件的批量文本替换 → 使用 Shell 配合 sdsed
  • 跨文件的符号重命名 → 使用 LSP

两个工具共享文件权限族(基于路径的授权)。对路径的单次批准适用于两个编辑工具。

在权限规则、hook 过滤器和技能 allowed_tools 中,正式名称是 editapply_patchpatch 作为 apply_patch 的旧别名仍被接受,已有配置可以继续工作。

配置任一编辑工具名即可;一个编辑器的规则会作用到另一个编辑器,除非另一个编辑器也有自己的显式规则:

统一配置(推荐):

permission:
edit: allow # apply_patch 和 edit 工具都允许

禁用某一种格式(高级):

permission:
edit: allow
apply_patch: deny # GPT/o 系列模型会退回使用 edit

权限回退规则

  • 如果仅配置了 editapply_patch 继承相同的权限
  • 如果仅配置了 apply_patchedit 继承相同的权限
  • 这也包括 denyedit: deny 也会禁用 apply_patch,除非 apply_patch 同时有自己的显式规则
  • 如果两者都配置了,各自使用自己的显式规则
  • 单独的 editapply_patch 规则会同时作用于两个工具,并覆盖通配符规则

示例

  • edit: allow → 两个工具都允许;GPT/o 系列模型通常看到 apply_patch,其他模型通常看到 edit
  • edit: allow, apply_patch: deny → apply_patch 拒绝,edit 允许;GPT/o 系列模型退回使用 edit
  • apply_patch: allow, edit: deny → apply_patch 允许,edit 拒绝;非 GPT 模型退回使用 apply_patch
  • *: deny, apply_patch: allow → 两个工具都允许(edit 继承 apply_patch 规则)
  • *: allow, apply_patch: deny → 两个工具都拒绝(edit 继承 apply_patch 拒绝)

模型根据其训练数据表现出强烈的偏好:

  • GPT 模型在训练中见过大量的 @@-风格补丁(OpenAI 的 apply_patch
  • Claude、Qwen 和类似模型在直观的查找替换格式上表现更好

实证测试(Aider 的 edit-bench、内部 chord 指标)显示:

  • GPT 模型:补丁格式成功率 91-96%,替换格式约 70%
  • 非 GPT 模型:替换格式成功率 81-96%,补丁格式 44-79%

apply_patch 分多轮匹配差异块上下文:先精确匹配,再忽略行尾空白,然后忽略两端空白,最后规范化常见的 Unicode 引号、破折号和空白变体。重复出现的代码块仍需要足够的邻近上下文(或 *** End of File 标记)来明确目标位置。

对于所有能够解码为文本的文件,最后还会尝试把常见中文标点与 ASCII 标点视为等价。这包括源码文件、.env.example 这类 dotenv 文件以及无扩展名的文本文件。只有完整差异块得到唯一匹配时才会应用该容错;替换行中未改变的部分会保留当前文件的原始标点,工具结果也会明确报告使用了容错。多候选匹配会被拒绝;仅出现在某个较长行内部的片段只用于诊断,不会自动执行行内替换。二进制文件或无法解码的文件不会进入这层容错,因为它们会在 hunk 匹配前的文本解码阶段失败。

  • Replace:对于小编辑通常减少 20-40% 的 token(无需上下文行)
  • apply_patch:由于上下文和信封需要更多 token,但对复杂和多文件编辑具有更好的精度
  • 两个工具在写入前都验证块/字符串
  • 两个都支持 LSP 集成(工作区通知)
  • 两个都参与相同的并发编辑控制(基于路径的锁定)
  • 两个都生成统一差异用于显示(无论输入格式如何)

如果你正在从只有一个编辑工具的系统升级:

  1. 无需操作:Chord 自动为每个模型选择正确的工具
  2. SessionImport 兼容性:历史编辑调用会被映射:
    • codex 提供者 → apply_patch 工具
    • 其他提供者 → edit 工具
  3. 权限连续性:两个工具共享文件权限族,patch 规则会按 apply_patch 解读

Q:我可以强制使用特定工具吗? A:工具选择是自动的且特定于模型。覆盖它可能会降低成功率。

Q:如果我的模型未被识别怎么办? A:默认情况下,未识别的模型使用 edit(替换)工具。GPT/o 系列模型使用 apply_patch

Q:两个工具支持相同的文件类型吗? A:是的。两者都适用于任何文本文件(检测到的编码:UTF-8、UTF-16、GB18030 等)。二进制文件会被拒绝。

Q:我可以在同一对话中使用两个工具吗? A:一次只有一个工具可见,基于活动模型。你不会同时看到两者。

Q:hashline(内容寻址锚点)怎么样? A:目前未启用。这是在生产工作负载中验证后的潜在未来增强功能。