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两点补充事实:
- 补丁信封支持可选的
*** Environment ID: <id>行,解析结果保存在ApplyPatchArgs.environment_id(见 codex-rs/apply-patch/src/lib.rs),用于多执行环境场景下标明补丁应作用于哪个环境; - 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" 列出的三条铁律:
- 必须带 header:每个文件操作都要写明意图(Add / Delete / Update),不能只贴 diff 行;
- 新建文件也要
+前缀:即使是*** Add File创建的初始内容,每一行都要以+开头; - 路径只能是相对路径,绝不使用绝对路径:
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-command、cmd /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 Patch→The first line of the patch must be '*** Begin Patch'; - 末行不是
*** End Patch→The 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:
- 若 chunk 带
change_context(即@@ header),先用序列搜索函数(codex-rs/apply-patch/src/seek_sequence.rs)从"上一个 chunk 结束位置"开始向前找到该上下文行,找不到即报Failed to find context '...' in <path>; - 然后在上下文之后的范围内,对
old_lines做逐行精确匹配;若直接匹配失败且old_lines以空行结尾(代表被替换区域的终止换行符),会去掉末尾空行重试一次,以稳妥处理触及文件末尾的修改; - 每个成功定位的 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_simple | Unicode 内容 |
测试入口见 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),仅供参考