编辑工具:apply_patch vs Edit
Chord 提供两种互补的文件编辑工具,按模型的训练背景分别优化。
- 选格式:快速对比与工具选择:Chord 默认发哪个工具、怎么改。
- 发送形式:apply_patch 工具(Codex 补丁格式)与 Edit(替换)工具给出各自的格式与匹配规则。
- 日常使用:建议工作流与特定任务指南。
- 审批:权限说明写入范围与自动放行。
| 特性 | 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, codex-auto-review | Claude, Qwen, GLM, MiniMax, DeepSeek, Gemini |
Chord 自动根据当前模型选择合适的工具:
- gpt-5 及之后主版本家族(
gpt-5、gpt-5-mini、gpt-5-nano、gpt-5-codex、任意gpt-5.*名称,以及gpt-6-astra等更高的主版本)及codex-auto-review→apply_patch(Codex 补丁格式) - 其他所有模型(gpt-3.5、gpt-4/4o、
gpt-oss-*、o 系列、Claude、Qwen、GLM、DeepSeek、Gemini 等)→edit(old_string/new_string)
gpt-4/4o、gpt-3.5 和 o 系列都不是补丁原生(patch-native)模型:它们训练时 apply_patch 还不存在(该工具随 GPT-5 在 2025 年 8 月引入),实测结果为负面,或模型未经训练。gpt-5 及之后的每个主版本家族都默认使用补丁工具面,与 Codex 模型目录一致;gpt-6-astra 这类更高的主版本也在其中,因为 apply_patch 是 Codex 的第一方训练数据,会随 OpenAI 模型代际延续。不携带补丁信号的模型可以用 compat.apply_patch.enabled: false 关掉。
补丁原生模型保留 apply_patch 时,Chord 还会隐藏 write 和 delete:补丁格式本身已覆盖它们(*** Add File: 创建、*** Delete File: 删除),与这些模型训练时熟悉的原生 Codex CLI 工具面一致。回退组合会保留 write/delete:非补丁原生模型仅因 edit 被禁用才拿到 apply_patch 时仍能看到它们;补丁原生模型被降级到 edit 时也需要 write 才能创建文件。
Freeform(custom tool)发送形式
Section titled “Freeform(custom tool)发送形式”在 OpenAI 兼容的 Responses 端点上,gpt-5 及之后家族或 codex-auto-review 模型还会把 apply_patch 作为 freeform custom tool(type: "custom",随请求带上 Lark grammar)发送,而不是 JSON function tool。Chord 会把 grammar 放进请求的 format.definition 字段;服务端支持约束解码时,它会在模型生成过程中限制补丁的协议结构。当前 grammar 与 Codex 的定义保持一致,要求至少一个文件操作、非空新增文件和合法的补丁行结构。客户端仍会用自己的解析器和执行器再次校验,因此未受约束的响应(未携带 grammar、被网关改写,或服务端并未执行约束)仍可走兼容兜底;grammar 不负责判断上下文是否来自最新文件,也不保证修改符合用户意图。其他模型一律收到 JSON function 形式的调用;非 Responses 端点没有 custom tool 类型,也只能用 function 形式。
接受 Responses 请求但拒绝 custom tool 的主机没有内置例外:那里的补丁原生模型默认按 freeform 形式发送,网关会报出带操作指引的错误。这类主机请设置 compat.apply_patch.freeform: false 强制改用 JSON function 形式。
上面所有默认值都可以在 compat.apply_patch 下按 provider 或按模型覆盖(三态:省略 = 保持推断):
providers: my-relay: type: responses compat: apply_patch: enabled: true # 工具面:保留 apply_patch(隐藏 edit + write/delete) freeform: false # 发送形式:JSON function tool,不用 custom openai: type: responses models: gpt-5.5: compat: apply_patch: freeform: false # 模型级覆盖:名字像但网关不支持enabled: true对任意模型采用完整补丁原生语义(保留 patch、隐藏edit/write/delete、prompt 改用 patch-only 指引)。enabled: false强制 edit 工具面,即使对补丁原生模型也生效。freeform: true强制 custom tool 形式;freeform: false强制 JSON function 形式。
如果网关把 custom tool 错误降级成 {"input": "..."}(而不是 {"patch": "..."}),Chord 会返回指向 compat.apply_patch.freeform: false 的可操作错误;设置后请求会以 function tool 发送。
apply_patch 工具(Codex 补丁格式)
Section titled “apply_patch 工具(Codex 补丁格式)”在 Responses 的 freeform 形式下,Lark grammar 是随 custom tool 一起发送给服务端的生成约束,服务端据此限制补丁的协议骨架:整个补丁至少包含一个文件操作,新增文件至少包含一行 + 内容,更新块必须符合 Codex 的行和 hunk 结构。它不能验证文件中的锚点是否唯一、文件是否在读取后发生变化,也不能判断修改是否符合语义。Chord 收到结果后仍会通过客户端解析器和事务执行器复核;因此 grammar 丢失或服务端未执行时,客户端仍负责拒绝非法结果或按兼容规则解析。
单个 patch 参数携带 Codex 补丁正文。Chord 接受完整的补丁;缺少 *** Begin Patch 和/或 *** End Patch 起止标记时,也会在解析前补齐。正文里可以包含任意数量的文件操作:
*** 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"}可选的头部锚点
Section titled “可选的头部锚点”你可以在 @@ 之后添加文本来帮助定位模糊的代码块:
@@ func processUser(id int) error { if id < 0 {- return errors.New("invalid")+ return fmt.Errorf("invalid user ID: %d", id) }重要提示:只使用你已验证存在于文件中的头部。头部是软锚点:头部文本找不到时,匹配会仅回退到块主体。
hunk 行在匹配时不含换行符,因此 read 返回的 LF 文本适用于任何换行方式的文件。未改动的行保留原有换行符。文件统一使用 CRLF 或 CR 时,新增行也使用同一种换行符;文件混用多种换行符时,新增行沿用它所替换或紧邻的那一行的换行符。更新后的文件总以换行符结尾。
同一个补丁内的所有操作会在修改任何文件之前基于同一份文件系统快照完成规划。补丁级预检失败时(例如语法错误、路径存在不安全重叠、无法读取快照),所有文件都保持不变。操作级失败时(例如 Update 源文件缺失、Add 目标已存在),Chord 会丢弃这个文件组,其他独立文件组仍可提交。如果任一已规划文件在提交前被外部修改,本次提交会整体拒绝,不写入原本可成功的文件。提交中途写入失败时,本次已经写入的变更也会回滚。
原子性是按文件划分的,不是按整个补丁。每个文件是一个独立单元:触及同一文件的所有操作(包括针对同一路径的连续 *** Update File: 段)要么一起提交,要么一起回滚。某个文件失败时,同一个补丁里其他独立的文件仍然会被应用并落盘。失败结果会列出已经提交、无需重做的修改,并说明哪些操作组没有应用及每项原因。先解决每项错误,再用当前工作区内容重建失败操作并只提交这些操作;不要重新发送已提交的文件,也不用重发补丁。
失败会拖垮整个文件组:同一文件前面的操作在内存里匹配成功、后面却失败时,该文件组的所有操作都算未应用,并从最终计划中排除。更早成功的前置文件组仍可提交;依赖失败组的后续文件组则会一起排除。结果会列出整条失败依赖链上的所有操作(含那些匹配成功的操作),方便按原文补回。
移动操作会把源路径和目标路径放进同一个依赖边界。移动失败后,后续触及任一路径的操作也会被拒绝,并一起计入未应用操作。如果源文件组在前面的移动暂时成功后才失败,依赖移动目标的操作也会一起回滚。这样结果不会在丢弃前置操作后,误报依赖它的目标修改已经提交,后续修订操作时也不会漏掉这条依赖链。
如果补丁失败且没有已应用的 diff,工具卡片会保留请求补丁(requested patch)预览,并把它和错误分开展示;执行成功后则切换为最终 diff。
- 「hunk not found (N/M)」:指定差异块与当前文件不匹配。错误会标出第一条期望完整行;诊断预览发生截断时,会明确写成「行前缀」。如果能够判断,还会说明该文本只是某个较长行的片段,或位于前一个差异块之前。同文件前面的差异块在内存里匹配成功、后面却失败时,该文件组的所有块都没有应用。先重新读取目标范围,用当前文件的完整行重建失败块,保留同组其他块,再提交修订后的操作参考。
- 「cannot add file that already exists」:
*** Add File:的目标已存在;改用*** Update File:。 - 「apply_patch contains overlapping operations」:同一补丁中的两个操作所触及的路径互为包含关系(例如
dir与dir/file),或通过不同名称解析到同一个文件;把它们合并为一个操作。针对完全相同路径的连续*** Update File:段是被允许的,并按顺序应用。 - 「changed after planning」:文件在验证与提交之间被修改;没有任何写入,基于当前内容重试。
- 「apply_patch partially applied: N changes committed, M file groups not applied: …」:部分独立修改已提交,其他操作组没有应用(单数时使用「change」 / 「file group」)。「Applied patch」下的修改已经落盘,不要重做。「Not applied」会列出每个失败操作组的路径和原因。先解决每项原因,再用当前文件内容重建失败操作组,只提交这些操作;不要重发已提交的修改,也不用回显整个补丁。
Edit(替换)工具
Section titled “Edit(替换)工具”{ "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(默认)仅替换第一个。
一次修改同文件的多处内容
Section titled “一次修改同文件的多处内容”用 edits 提交互不重叠的替换,减少工具往返:
{"path":"server.go","edits":[{"old_string":"const port = 8080","new_string":"const port = 3000"},{"old_string":"const retries = 2","new_string":"const retries = 3"}]}每项包含 old_string、new_string 和可选的 replace_all。edits 不能与顶层替换字段混用。需要改动的项都匹配原始文件,不能依赖另一项刚插入的文本。批量替换在清理参数中的不可见字符、适配文件换行方式后精确匹配,不使用单次替换的尾随换行、标点或空白容错。任一项失败都会拒绝整批、不写入任何替换,一次结果里列出全部失败项及原因,修正后重发完整批次。old_string 与 new_string 相同的项视为没有请求改动,工具会跳过该项,不验证文本是否存在,也不计入替换数;整批都没有改动时不写盘,只报告无变更。成功时只写入一次,并统一返回诊断。
示例:单次替换
Section titled “示例:单次替换”{ "path": "server.go", "old_string": "const port = 8080", "new_string": "const port = 3000"}示例:重命名变量
Section titled “示例:重命名变量”{ "path": "handler.go", "old_string": "userID", "new_string": "userId", "replace_all": true}- 「old_string not found in file」:即使经过标点容错,精确文本也不存在。检查空白、缩进和换行符。差异是字符级(漏字或多字)时,错误还会指出文件中最近的匹配块(行号、相似度以及具体的差异行),让你不用重读整个文件就能看出那一处字符错误(比如缺
)或双逗号,,)。整行漂移、展示的差异行不足以重建原文时,错误会改为点明漂移,并给出最近匹配区间的read坐标(offset/limit),或建议改用更小的 2-4 行锚点。 - 「old_string found N times」:找到多个匹配。可以:
- 添加更多上下文使其唯一
- 设置
replace_all: true如果你想替换所有出现
- 「old_string and new_string are identical」:单次替换会报错。批量替换会跳过该项并标注 skipped;整批都没有改动时返回无变更,不写盘。
同一目标文件反复多次近似匹配失败时,agent 会从第二次失败起在面向模型的结果里追加一条提示:先重新读取目标区间(或改用 write 整段落地),不要凭记忆重打同一段旧文本。提示不会出现在界面上,任意一次成功或进入新 turn 后计数重置。
零宽字符清理
Section titled “零宽字符清理”edit、apply_patch 和 write 的写入路径会清理模型泄漏进工具参数的零宽格式字符与悬空组合符号(零宽空格、零宽不连字、emoji 序列之外的零宽连字、词连接符、流中 BOM、软连字符;以及没有可见基字符可依附的变音符号,比如位于文本开头或前面只有空白的游离长音符)。这些字符不携带内容,悬空的变音符号也无法改变任何字符的含义,清理不会改变文本含义;留着会把不可见字节写进文件。清理发生时,工具结果会报告具体清理了哪些码点(例如 U+200B×2, U+0304×1),让模型学会不再输出它们。组合符号只要落在某个可见基字符(字母、数字、符号或标点)上就保持不变:越南语、阿拉伯语、天城文等文字的合法变音与堆叠序列,以及数学记法中数字上的 U+0305 上划线都不会被误删。apply_patch 只清理补丁新增行里的这类字符,文件里补丁未触及的内容永远不会被扫描或改写。
单次和批量替换都接受 read 返回的 LF 文本。文件统一使用 CRLF 或 CR 换行时,替换后保留文件原有的换行方式;文件混用多种换行符时,old_string 中的每个换行可以匹配任意一种换行符,替换内容沿用被替换片段的换行方式。替换文本不要包含 READ_RESULT 元数据行。
尾随换行符容错
Section titled “尾随换行符容错”工具会自动处理轻微的尾随换行符差异:
- 如果
old_string有最后的\n但匹配项没有(反之亦然),并且匹配是唯一的,编辑会继续。 - 这减少了因换行符不匹配导致的重试。
精确匹配与尾随换行匹配都失败时,工具会按常见标点变体等价的方式重试,与 apply_patch 使用同一套 1:1 归一化规则:
- 弯引号与直引号(
「 」↔" ",‘ ’↔' ') - 破折号(
–、—、−↔-) - 全角与半角 CJK 标点(
,;:.!?())
该兜底仅在归一化后的 old_string 有唯一匹配时应用,会在工具结果中报告使用情况,并对未改动的上下文保留文件原有的标点。多个归一化匹配会以「found N times」报错。
分隔标点旁的一个空格也被视为可选:: 与带尾随空格的 :(以及丢掉空格时的 :the)会匹配同一段文本,词间空格(diff and 与 diffand)同样视为可选。这覆盖了把 ": " 编码为单个 token、随后又把它重现为 : 或丢掉空格的模型。折叠范围刻意收窄:只有紧跟在 , ; : . ! ? ( 之后(或紧跟在 ) 之前)的一个空格、以及两个词字符之间的一个空格是可选的。双空格、引号或破折号后的空格、缩进与换行符仍然有效,因此真正的布局差异仍会以「old_string not found」失败,而不会被静默套上错误的编辑。工具结果会报告何时使用了容错;工具描述刻意不提及它,让模型仍然以精确匹配为目标。
没有可见基字符的组合符号(位于行首、或前面只有空白的 mark)在归一化时会被折叠掉:这是 token 化伪影,复制标题(如 ### [U+0304].2.1)时带进来的游离长音符,真实文件内容里不会出现在那个位置。只要 mark 落在任何可见基字符(字母、数字、符号或标点)上就保持原样:阿拉伯语、天城文、越南语等文字的合法变音(含堆叠序列)以及数字或符号上的数学记号都不会被误伤。
两个编辑工具都不要求预先 read:它们在执行时都会读取当前磁盘内容。为了可靠编辑,仍建议遵循以下做法:
- 在尚未确认精确文本、路径或差异块锚点时,先检查目标区域。可以使用
read或grep。 - 使用最小的唯一块(2-4 行)。大的上下文块更容易过时。
- 失败后重新读取。如果块或字符串匹配失败,文件可能已更改,在重试之前再次读取。
特定任务指南
Section titled “特定任务指南”两个工具都很适用。根据模型训练选择:
- apply_patch:需要位置控制时更好(例如,「更改此函数中的第一个出现」)。
- Edit:对于具有清晰边界的简单查找替换更好。
重命名/重构
Section titled “重命名/重构”- Edit 配合
replace_all: true:在一个文件中重命名变量。 - Shell 配合语言自带的重构命令(如
gopls rename):用于跨多个文件的符号感知重命名。
- 创建、删除或移动文件 →
apply_patch原生支持(*** Add File:/*** Delete File:/*** Move to:);使用edit的模型用 Write 和 Delete - 跨多个文件的批量文本替换 → 使用 Shell 配合
sd或sed - 跨文件的符号重命名 → 使用 Shell 配合语言自带的重构命令
两个工具共享文件权限族(基于路径的授权)。对路径的单次批准适用于两个编辑工具。
在权限规则、hook 过滤器和技能 allowed_tools 中,正式名称是 edit 和 apply_patch。patch 作为 apply_patch 的旧别名仍被接受,已有配置可以继续工作。
配置任一编辑工具名即可;一个编辑器的规则会作用到另一个编辑器,除非另一个编辑器也有自己的显式规则:
统一配置(推荐):
permission: edit: allow # apply_patch 和 edit 工具都允许禁用某一种格式(高级):
permission: edit: allow apply_patch: deny # 补丁原生模型(gpt-5 家族/codex-auto-review)会退回使用 edit权限回退规则:
- 如果仅配置了
edit,apply_patch继承相同的权限 - 如果仅配置了
apply_patch,edit继承相同的权限 - 这也包括
deny:edit: deny也会禁用apply_patch,除非apply_patch同时有自己的显式规则 - 如果两者都配置了,各自使用自己的显式规则
- 单独的
edit或apply_patch规则会同时作用于两个工具,并覆盖通配符规则
示例:
edit: allow→ 两个工具都允许;补丁原生模型(gpt-5 家族/codex-auto-review)通常看到apply_patch,其他模型通常看到editedit: allow, apply_patch: deny→ apply_patch 拒绝,edit 允许;补丁原生模型退回使用editapply_patch: allow, edit: deny→ apply_patch 允许,edit 拒绝;非 GPT 模型退回使用apply_patch*: deny, apply_patch: allow→ 两个工具都允许(edit 继承 apply_patch 规则)*: allow, apply_patch: deny→ 两个工具都拒绝(edit 继承 apply_patch 拒绝)
为什么是两个工具?
Section titled “为什么是两个工具?”edit 适合通过原文定位局部替换;apply_patch 用补丁描述新增、修改、移动和删除。Chord 按模型选择工具,通常无需手动调整。遇到端点或模型不支持默认格式时,可以通过 compat.apply_patch.enabled 覆盖选择。
apply_patch 分三轮精确匹配差异块上下文:先精确匹配,再忽略行尾空白,然后忽略两端空白。标点/空白容错(引号、破折号、全角 CJK 标点,以及分隔标点后的可选空格)刻意不作为第四轮:它是一个独立的步骤,且必须恰好落在一个位置;容错匹配若命中多个位置,会列出候选行号并拒绝应用,而不是静默取第一个。重复出现的代码块仍需要足够的邻近上下文(或 *** End of File 标记)来明确目标位置。
对于所有能够解码为文本的文件,最后还会尝试把常见中文标点与 ASCII 标点视为等价,并且与 edit 工具一样,把分隔标点旁的一个空格视为可选(:、带尾随空格的 : 与 :the 会匹配同一行)。两个工具共用同一套归一化与保留规则。这包括源码文件、.env.example 这类 dotenv 文件以及无扩展名的文本文件。只有完整差异块得到唯一匹配时才会应用该容错;替换行中未改变的部分会保留当前文件的原始标点,工具结果也会明确报告使用了容错。多候选匹配会被拒绝;仅出现在某个较长行内部的片段只用于诊断,不会自动执行行内替换。二进制文件或无法解码的文件不会进入这层容错,因为它们会在 hunk 匹配前的文本解码阶段失败。
Q:我可以强制使用特定工具吗?
A:可以。用 compat.apply_patch.enabled: true 选择补丁工具,false 选择替换工具;通常保留自动选择即可。
Q:如果我的模型未被识别怎么办?
A:默认情况下,未识别的模型使用 edit(替换)工具。gpt-5 及之后家族(gpt-5、gpt-5-mini、gpt-5-nano、gpt-5-codex、任意 gpt-5.* 名称,以及 gpt-6-astra 等更高的主版本)和 codex-auto-review 使用 apply_patch;任意模型都可以用 compat.apply_patch.enabled 覆盖。
Q:两个工具支持相同的文件类型吗? A:是的。两者都适用于任何文本文件(检测到的编码:UTF-8、UTF-16、GB18030 等)。二进制文件会被拒绝。
Q:我可以在同一对话中使用两个工具吗? A:一次只有一个工具可见,基于活动模型。你不会同时看到两者。