openinterpreter apply_patch 补丁格式完全指南:模板指令、完整语法与 Rust 解析器实现
2026/9/7 19:41:01 网站建设 项目流程

openinterpreter apply_patch 补丁格式完全指南:模板指令、完整语法与 Rust 解析器实现

【免费下载链接】openinterpreterA coding agent for open models like Kimi K3 and GLM 5.3项目地址: https://gitcode.com/GitHub_Trending/op/openinterpreter

本篇指南围绕 openinterpreter 仓库中的apply_patch工具指令模板展开,讲解这种"文件导向的裁剪版 diff 格式"的完整语法、书写规则与调用方式,并结合codex-rs/apply-patch中的解析器与应用器源码,说明每一条格式约定背后的实现逻辑。读完本文,你可以独立写出合法、可唯一匹配、可安全应用的补丁,并理解 agent 在哪些情况下会收到何种错误反馈。

1. apply_patch 是什么:一份注入模型提示词的工具说明书

apply_patch是 openinterpreter(基于 Rust 实现的编码代理,codex-rs工作区)用于编辑文件的标准工具。它不是一个泛化的文本替换工具,而是一个"高层信封(envelope)"式的文件操作协议:模型通过 shell 命令面(shell command surface)调用apply_patch,传入一段结构化的补丁文本,代理解析后对文件执行新增、删除、更新(含重命名)三类操作。

仓库中这份工具的使用说明就存放在指令模板里:

  • 模板文件:codex-rs/prompts/templates/apply_patch_tool_instructions.md
  • 模板编译期嵌入:codex-rs/prompts/src/apply_patch.rs 通过include_str!将其编译为常量APPLY_PATCH_TOOL_INSTRUCTIONS,并从 codex-rs/prompts/src/lib.rs 对外导出;
  • 实际拼接进系统提示词时,它与基础指令用换行连接,例如 codex-rs/core/tests/suite/prompt_caching.rs 中[base_instructions, APPLY_PATCH_TOOL_INSTRUCTIONS.to_string()].join("\n")的用法;此外 core 侧还维护了合并版提示词文件 codex-rs/core/prompt_with_apply_patch_instructions.md。

也就是说:本文接下来的所有格式约定,都是模型在运行时"逐字读到"的指令。模板注释写明其面向 gpt-4.1 这类模型(见 apply_patch.rs 第 1 行),这也是解析器默认采用宽松(lenient)模式的原因,后文第 7 节会展开。

2. 补丁的整体结构:信封与文件操作

apply_patch的补丁语言是一种裁剪版、文件导向的 diff 格式,设计目标是"易于解析、安全应用"。你可以把它理解为一个高层信封:

*** Begin Patch [ one or more file sections ] *** End Patch

信封内部是一串文件操作(file operations)。每个操作必须以三行 header 之一开头,明确指出你要执行的动作:

Header语义后续内容
*** Add File: <path>创建新文件其后每一行都是+行(即文件的初始内容)
*** Delete File: <path>删除已存在的文件后面不能跟随任何内容
*** Update File: <path>就地修补一个已存在的文件(可选重命名)一个或多个 hunk

*** Update Fileheader 之后,可以紧跟一行:

*** Move to: <new path>

用于在更新的同时把文件重命名/移动到新的相对路径。

2.1 hunk 与三种行前缀

每个 Update File 操作由一个或多个 hunk 组成,每个 hunk 以@@开头(@@后可以跟一个 hunk header,用来指明代码片段所属的类或函数)。hunk 内部每一行都必须以下列三种前缀之一开头:

  • (空格):上下文行,表示保持不变;
  • -:删除行(old_code);
  • +:新增行(new_code)。

解析器把这些内容组织为UpdateFileChunk结构,见 codex-rs/apply-patch/src/parser.rs:

pub struct UpdateFileChunk { /// 用于缩小 chunk 定位范围的一行上下文(通常是类/方法/函数定义) pub change_context: Option<String>, /// 应当被 `new_lines` 替换的连续旧行块 pub old_lines: Vec<String>, pub new_lines: Vec<String>, /// 若为 true,`old_lines` 必须出现在源文件末尾 pub is_end_of_file: bool, }

其中change_context正是@@后 hunk header 的解析结果——它是"把补丁定位到哪个函数"的关键。

3. hunk 的书写规则:上下文行数与 @@ 定位

3.1 默认 3 行上下文,且相邻变更不重复

模板对[context_before][context_after]的要求(原文规则):

  • 默认展示每个变更上方紧邻的 3 行下方紧邻的 3 行代码;
  • 如果一次变更距离上一次变更不足 3 行,不要把前一次变更的[context_after]行复制到后一次变更的[context_before]中(即相邻 hunk 的上下文不要重叠重复)。

3.2 用@@header 指明所属类/函数

当 3 行上下文不足以在文件中唯一定位代码片段时,使用@@后跟 header 指明片段所属的类或函数,例如:

@@ class BaseClass [3 lines of pre-context] - [old_code] + [new_code] [3 lines of post-context]

3.3 重复代码块:多行@@逐级跳转

如果某段代码在一个类或函数里重复出现次数太多,以至于单条@@加 3 行上下文仍然无法唯一确定位置,可以使用多个@@语句逐级跳到正确上下文:

@@ class BaseClass @@ def method(): [3 lines of pre-context] - [old_code] + [new_code] [3 lines of post-context]

3.4 文件尾标记:*** End of File

完整语法还允许在 hunk 末尾追加一行*** End of File,表示该 chunk 的旧行必须落在文件结尾(解析为is_end_of_file: true)。仓库中专门有对应测试场景 022_update_file_end_of_file_marker 和 021_update_file_deletion_only(删除整段内容、不新增行)可以查阅。

4. 完整形式语法(BNF 与 Lark 对照)

模板给出的完整文法定义如下:

Patch := Begin { FileOp } End Begin := "*** Begin Patch" NEWLINE End := "*** End Patch" NEWLINE FileOp := AddFile | DeleteFile | UpdateFile AddFile := "*** Add File: " path NEWLINE { "+" line NEWLINE } DeleteFile := "*** Delete File: " path NEWLINE UpdateFile := "*** Update File: " path NEWLINE [ MoveTo ] { Hunk } MoveTo := "*** Move to: " newPath NEWLINE Hunk := "@@" [ header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ] HunkLine := (" " | "-" | "+") text NEWLINE

解析器实现中使用的官方 Lark 文法(见 codex-rs/apply-patch/src/parser.rs 的模块注释)与上述 BNF 语义一致,并额外揭示了两个模板未强调的细节:

start: begin_patch environment_id? hunk+ end_patch begin_patch: "*** Begin Patch" LF environment_id: "*** Environment ID: " filename LF end_patch: "*** End Patch" LF? add_line: "+" /(.+)/ LF -> line change_context: ("@@" | "@@ " /(.+)/) LF eof_line: "*** End of File" LF

两点补充事实:

  1. 补丁信封支持可选的*** Environment ID: <id>行,解析结果保存在ApplyPatchArgs.environment_id(见 codex-rs/apply-patch/src/lib.rs),用于多执行环境场景下标明补丁应作用于哪个环境;
  2. Lark 文法中end_patch的结尾LF?说明最后一个换行符是可选的——实现比模板更宽容,解析器注释明确写道 "The parser below is a little more lenient than the explicit spec and allows for leading/trailing whitespace around patch markers"(见 parser.rs 第 24-25 行)。测试场景 017_whitespace_padded_hunk_header 和 018_whitespace_padded_patch_markers 验证了空白容忍行为。

5. 一个组合多操作的完整示例

一个补丁可以把多种操作混在一起。以下示例完整继承了模板给出的用法:新增文件、更新并重命名文件、删除文件:

*** Begin Patch *** Add File: hello.txt +Hello world *** Update File: src/app.py *** Move to: src/main.py @@ def greet(): -print("Hi") +print("Hello, world!") *** Delete File: obsolete.txt *** End Patch

逐行解读:

  • *** Add File: hello.txt:创建hello.txt,其下所有行都必须带+前缀,这里文件内容为Hello world
  • *** Update File: src/app.py+*** Move to: src/main.py:先对src/app.py应用 hunk(把greet()里的print("Hi")改为print("Hello, world!")),再把结果写入src/main.py并删除原文件;@@ def greet():是 hunk header,把变更定位到greet()函数内;
  • *** Delete File: obsolete.txt:删除文件,其后不跟任何行。

在实现侧,Move to的执行顺序是"先写新路径、后删旧路径"(见 codex-rs/apply-patch/src/lib.rs 中Hunk::UpdateFile分支:先对dest_uri执行write_file_with_missing_parent_retry,成功后再ensure_not_directory+fs.remove删除原文件)。目标目录若不存在会自动递归创建父目录(lib.rs 第 629-665 行 的write_file_with_missing_parent_retry)。测试场景 004_move_to_new_directory 与 010_move_overwrites_existing_destination 覆盖了"移动到不存在的目录"和"移动目标已存在会被覆盖"两种情况。

6. 三条必须记住的规则

模板原文以 "It is important to remember" 列出的三条铁律:

  1. 必须带 header:每个文件操作都要写明意图(Add / Delete / Update),不能只贴 diff 行;
  2. 新建文件也要+前缀:即使是*** Add File创建的初始内容,每一行都要以+开头;
  3. 路径只能是相对路径,绝不使用绝对路径File references can only be relative, NEVER ABSOLUTE

关于第 3 条需要说明一个实现事实:解析器与执行器在技术上也接受绝对路径Hunk::resolve_path通过cwd.join(...)统一处理相对与绝对路径,测试test_apply_patch_hunks_accept_relative_and_absolute_paths见 codex-rs/apply-patch/src/lib.rs),但指令明确要求模型只用相对路径——这是为了把补丁锚定在会话的工作目录上,避免模型越权写入工作区之外的位置。作为作者,请遵守相对路径约定。

7. 如何调用 apply_patch

7.1 标准调用形式

模板给出的调用方式是把它作为 shell 工具的一个"程序名"调用,第二个参数是整段补丁文本(\n为转义换行):

shell {"command":["apply_patch","*** Begin Patch\n*** Add File: hello.txt\n+Hello, world!\n*** End Patch\n"]}

command数组的形如[程序名, 参数...]apply_patch的第一个参数就是完整补丁字符串。

7.2 解析器实际接受的调用形态

从源码看,调用识别逻辑在 codex-rs/apply-patch/src/invocation.rs 的maybe_parse_apply_patch

  • 命令名apply_patch与别名applypatch都认(常量APPLY_PATCH_COMMANDS = ["apply_patch", "applypatch"],见 invocation.rs 第 27 行);
  • 直接调用["apply_patch", "<patch 文本>"]
  • shell heredoc 形式bash -lc "apply_patch <<'EOF' ... EOF"(也支持zsh/sh-lc/-c、PowerShell-commandcmd /c),解析器会提取 heredoc 正文作为补丁;若 heredoc 前有cd <path> &&,该目录会被记录为workdir,作为解析相对路径的基准(invocation.rs 第 119 行 注释:Shell heredoc form: (optional cd <path> &&) apply_patch <<'EOF' ...)。

7.3 两种重要的调用错误

隐式调用会被明确拒绝。如果你把裸补丁文本直接当命令(而不是["apply_patch", ...]),验证函数maybe_parse_apply_patch_verified会识别出"这是一个补丁,但没有显式调用 apply_patch",并返回错误(invocation.rs 第 147-158 行),错误文案见 codex-rs/apply-patch/src/lib.rs:

patch detected without explicit call to apply_patch. Rerun as ["apply_patch", "<patch>"]

heredoc 包裹的补丁会被自动剥离。解析器默认运行在宽松模式(PARSE_IN_STRICT_MODE = false,见 parser.rs 第 47-53 行)。原因是个别模型(注释点名 gpt-4.1)习惯把补丁写成"<<'EOF'\n*** Begin Patch\n...\nEOF\n"这样的 heredoc 字符串作为单个参数传入,而工具实际以类execvpe(3)方式执行、并不经过 shell,heredoc 会被当成字面量。宽松模式下解析器会检测首行<<EOF/<<'EOF'/<<"EOF"且末行以EOF结尾、总行数不少于 4 的情况,自动剥掉 heredoc 标记再按严格边界校验(parser.rs 第 217-239 行)。

7.4 独立可执行程序与自调用协议

apply-patch除了作为库被调用,还有一个独立可执行入口(codex-rs/apply-patch/src/main.rs、standalone_executable.rs),支持从 stdin 读取补丁。此外 core 可执行文件自调用内部apply_patch路径时使用特殊argv[1]标志--codex-run-as-apply-patch(常量CODEX_CORE_APPLY_PATCH_ARG1,见 lib.rs 第 34-41 行)。这两点属于进程调用协议层面,普通使用者只需记住 7.1 的调用形式即可。

8. 解析与应用流程:源码级原理

整条调用链可以概括为:边界校验 → 流式解析出 hunk → 逐 hunk 定位替换 → 写文件系统 → 打印摘要

8.1 解析:parse_patch

入口parse_patch(parser.rs 第 130-137 行)按当前模式校验首末行标记后,把文本交给StreamingPatchParser增量解析出Hunk列表。Hunk是三种变体的枚举:AddFile { path, contents }DeleteFile { path }UpdateFile { path, move_path, chunks }(parser.rs 第 64-82 行),其中UpdateFile.chunks要求按文件中出现的先后顺序排列(源码注释:一个 chunk 的change_context必须出现在前一个 chunk 之后)。

解析阶段的典型错误:

  • 首行不是*** Begin PatchThe first line of the patch must be '*** Begin Patch'
  • 末行不是*** End PatchThe last line of the patch must be '*** End Patch'
  • *** Update File后没有任何 hunk →Update file hunk for path '...' is empty
  • 非法 hunk header(如@@后内容不符合约定)→ 带行号的InvalidHunkError

对应回归测试:005_rejects_empty_patch、008_rejects_empty_update_hunk、013_rejects_invalid_hunk_header。

8.2 定位:@@header 如何变成行号

应用阶段的核心是compute_replacements(lib.rs 第 712-801 行)。对每个 chunk:

  1. 若 chunk 带change_context(即@@ header),先用序列搜索函数(codex-rs/apply-patch/src/seek_sequence.rs)从"上一个 chunk 结束位置"开始向前找到该上下文行,找不到即报Failed to find context '...' in <path>
  2. 然后在上下文之后的范围内,对old_lines逐行精确匹配;若直接匹配失败且old_lines以空行结尾(代表被替换区域的终止换行符),会去掉末尾空行重试一次,以稳妥处理触及文件末尾的修改;
  3. 每个成功定位的 chunk 记为(start_index, old_len, new_lines)三元组,最终按位置排序、从后往前应用替换(apply_replacements,lib.rs 第 803-829 行),避免前面的替换移动后面 chunk 的坐标。

这解释了第 3 节两条书写规则的必要性:上下文必须唯一可寻址,且 chunk 顺序不能乱——定位是"从前往后单指针扫描"的,乱序的 chunk 会导致Failed to find expected lines错误(对应场景 006_rejects_missing_context)。

8.3 执行:逐 hunk 落盘与部分成功语义

apply_hunks_to_files(lib.rs 第 361-566 行)按补丁顺序执行各 hunk:

  • AddFile:写入内容;若目标已存在,会先记录被覆盖的旧内容(overwritten_content)再覆盖——场景 011_add_overwrites_existing_file 验证了"Add 覆盖已存在文件是允许的";
  • DeleteFile:先ensure_not_directory目录不能删,场景 012_delete_directory_fails),不存在则报错(场景 007_rejects_missing_file_delete);
  • UpdateFile:读原文件 → 计算新内容 → 有Move to时先写新路径再删旧路径,否则原地写回。Update File要求目标文件必须已存在(场景 009_requires_existing_file_for_update)。

部分成功不自动回滚。失败时返回的ApplyPatchFailure携带AppliedPatchDelta——"在失败观测点之前已确定落盘的那些文本变更"(lib.rs 第 247-273 行),exact标志标明 delta 是否精确(写失败可能已截断文件,此时 delta 标记为不精确)。场景 015_failure_after_partial_success_leaves_changes 的期望结果里保留了失败前已创建的文件。实践含义是:多文件补丁应尽量把高风险操作放在后面,或在出错后核对已落盘部分再修正,代理无法保证事务性回滚。

8.4 成功输出

全部 hunk 成功后,print_summary(lib.rs 第 868-885 行)以 git 风格输出:

Success. Updated the following files: A <新增文件路径> M <修改/移动文件路径> D <删除文件路径>

A/M/D顺序固定为新增、修改、删除。

9. 测试场景索引:用仓库自带的回归用例自检

apply-patch的端到端回归测试位于 codex-rs/apply-patch/tests/fixtures/scenarios/,每个场景目录含input/(变更前文件树)、expected/(期望结果)与patch.txt(待应用补丁)。值得对照阅读的有:

场景验证点
001_add_file新增文件,+行构成全部内容
002_multiple_operations单补丁混合增/删/改
003_multiple_chunks同一文件多个 hunk
014_update_file_appends_trailing_newline无结尾换行文件的处理(实现会自动补上结尾换行,见 lib.rs 第 700-705 行)
016_pure_addition_update_chunk纯新增 chunk(无-行):插入到文件末尾,实现见 lib.rs 第 741-751 行
019_unicode_simpleUnicode 内容

测试入口见 codex-rs/apply-patch/tests/suite/scenarios.rs 与 cli.rs。

10. 快速核对清单

写出补丁前,用这份清单对照模板与实现:

  • 首行*** Begin Patch、末行*** End Patch,二者之间至少一个文件操作;
  • 每个操作带正确 header:*** Add File:/*** Delete File:/*** Update File:;重命名在 Update 下紧跟*** Move to:
  • 新增文件内容每行+开头;hunk 行一律 /-/+前缀;
  • 默认 3 行前后上下文;3 行不够唯一时加@@ <类/函数>header,仍不够则多行@@逐级定位;相邻变更 3 行内不复制重复上下文;
  • 触及文件末尾可加*** End of File
  • 全部使用相对路径;
  • 调用形式为["apply_patch", "<patch>"](别名applypatch;heredoc 包裹可被宽松模式剥离),裸补丁直发会被显式拒绝;
  • 预期输出为Success. Updated the following files:+A/M/D列表;失败时注意已落盘的部分变更不会自动回滚。

这套"信封 + header + hunk"的设计,把"模型生成 diff"从自由文本约束为可流式解析(StreamingPatchParser)、可逐行定位(seek_sequence)、可审计(AppliedPatchDelta记录前后内容)的结构化操作,是 openinterpreter 这类编码代理能安全执行多文件编辑的底层基础。

【免费下载链接】openinterpreterA coding agent for open models like Kimi K3 and GLM 5.3项目地址: https://gitcode.com/GitHub_Trending/op/openinterpreter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询