内容由GPT-6 Astra生成。
本文基于 OMP coding-agent 当前源码与文档整理。重点是区分模型输出的 wire contract、OMP 的解析/校验/落盘链路,以及不同模型为什么可能使用不同 edit mode。
总览
OMP 有两个文件修改工具:
edit:对已有源码做结构化或局部修改,有多种可切换的输入协议。write:创建文件或完整覆盖文件内容,不是edit的一个 mode。
默认 edit mode 是 hashline,但实际 mode 还会受到模型变体、环境变量和配置影响。
# 显式固定协议
edit:
mode: apply_patch
modelVariants: {}
PI_EDIT_VARIANT=apply_patch \
PI_STRICT_EDIT_MODE=1 \
/root/.local/bin/omp
PI_STRICT_EDIT_MODE=1 禁用内置的模型专用 fallback;它不会覆盖显式的 PI_EDIT_VARIANT 或 edit.modelVariants。
write:完整内容写入
核心参数:
{
"path": "src/main.py",
"content": "完整文件内容\n"
}
适用于:
- 创建新文件;
- 明确要求整体重写文件;
- 某些 archive/SQLite 内部资源操作。
OMP 的 write prompt 建议:修改已有源码时优先使用 edit,避免完整覆盖导致未提及的内容丢失。
优点:schema 简单、新文件自然、容易实现完整文件 diff UI。
风险:覆盖范围是整个文件;模型必须提供完整最终内容;绕过 OMP 原生执行会绕过 LSP writethrough、诊断、formatter 和 file-write fallback 等机制。
edit mode
当前 EditMode 枚举为:
export type EditMode = "replace" | "patch" | "hashline" | "apply_patch" | "sloppy";
resolveEditMode() 的选择顺序:
1. edit.modelVariants 中匹配当前模型的 mode
2. PI_EDIT_VARIANT
3. edit.mode
4. 默认 hashline
例如:
edit:
mode: hashline
modelVariants:
claude: apply_patch
gpt-5: replace
这不是让同一个调用同时接受多个 schema,而是按当前模型选择一个有效 edit tool 协议。
没有 PI_STRICT_EDIT_MODE=1 时,默认 hashline 可能因为模型兼容性规则自动切换为 sloppy。当前源码将这些模型类别视为不适合 hashline:
kimimimodeepseek的flashfamilystepfun
因此不能只根据“配置没有写 mode”推断实际 schema。
replace:字符串查找替换
{
"path": "src/main.py",
"old_string": "return items",
"new_string": "return sorted(items)",
"replace_all": false
}
replace_all: false 通常替换一个匹配;replace_all: true 替换所有匹配。OMP 还可以通过 fuzzy matching 容忍高置信度的空白差异:
edit:
mode: replace
fuzzyMatch: true
fuzzyThreshold: 0.95
环境变量:PI_EDIT_FUZZY、PI_EDIT_FUZZY_THRESHOLD。
适合:小而明确、目标字符串唯一的修改,以及不能稳定生成复杂 patch 的模型。
不适合:同一文件多处不相邻修改、跨文件移动、需要重排代码的任务。模型必须准确复制 old_string,复杂修改通常会被拆成多个 tool call。
patch:结构化 JSON patch
{
"path": "src/main.py",
"edits": [
{
"op": "update",
"diff": "@@ -1,3 +1,3 @@\n old\n-old line\n+new line\n"
}
]
}
字段:
{
op?: "create" | "delete" | "update";
rename?: string;
diff?: string;
}
它适合单文件多 hunk,比 replace 灵活;但顶层主要围绕一个 path,多文件表达能力不如 apply_patch。
apply_patch:Codex 风格多文件 patch
schema:
{
"input": "*** Begin Patch\n...\n*** End Patch"
}
示例:
*** Begin Patch
*** Update File: src/main.py
@@ -1,3 +1,3 @@
def greet():
- return "hello"
+ return "hello, world"
*** Add File: tests/test_main.py
+def test_greet():
+ assert True
*** Delete File: obsolete.py
*** End Patch
能力:
- 一次修改多个文件;
- 一个文件包含多个 hunk;
- Add、Update、Delete、Move/rename;
- 由 OMP 原生 parser、fuzzy matcher 和执行器处理。
适合能稳定生成结构化文本的强模型,以及经常需要多文件修改的 coding agent。
apply_patch 不是任意 unified diff 的别名。必须有完整的 *** Begin Patch / *** End Patch envelope,文件 header、上下文行和操作语义都必须满足当前 OMP parser。
hashline:版本化行锚定
[src/example.ts#1A2B]
PUT 4.=4:
+const value = 2;
#1A2B 是四位十六进制 snapshot tag,来自此前的 read、grep 或成功 edit 结果。
主要操作:
PUT N.=M: 替换原始快照中的 N..M 行
PUT N*: 替换从 N 开始的 tree-sitter 语法块
PUT <N: 在 N 行前插入
PUT >N: 在 N 行后插入
PUT >$: 文件尾追加
CUT N.=M 删除并放入匿名寄存器
CUT N.=M @name 删除并存入具名寄存器
PUT <N @name 粘贴具名寄存器
REM 删除文件
MV DEST 移动/重命名
hashline 的安全能力包括:
- snapshot tag 校验版本;
- seen-line guard 限制模型只能编辑已展示内容;
- stale tag 安全恢复;
- tree-sitter block anchor;
- clipboard/register 跨 section 搬运代码;
- 多 section 预解析和冲突检查。
代价是模型必须持续维护最新 tag、原始行号、展示范围、block opener、body 语法和 register 生命周期。常见失败包括 stale tag、编辑未展示行、错误 block 边界、统一 diff 污染、范围重叠和 no-op。
适合安全优先、需要版本校验和最小上下文暴露的 agent。不适合经常忘记更新 tag 或不稳定复制严格结构化 anchor 的模型。
sloppy:兼容性编辑格式
典型形式:
*** Edit File: src/main.py
*** Find
old text
*** Replace
new text
sloppy 的目标是兼容宽松编辑文本,而不是提供 hashline 的版本安全语义。它适合兼容旧式文本编辑输出;如果需要可预测行为,通常应显式选择 replace 或 apply_patch。
五种 mode 对比
| Mode | 多文件 | 多 hunk | Create/Delete/Move | 版本校验 | 主要优点 |
|---|---|---|---|---|---|
replace | 弱 | 弱 | 否 | 否 | schema 最简单 |
patch | 有限 | 是 | 是 | 否 | 单文件结构化修改 |
apply_patch | 是 | 是 | 是 | 否 | 多文件和操作 envelope |
hashline | 是 | 是 | 是 | 是 | 快照、seen-line、block、register |
sloppy | 取决于输入 | 取决于输入 | 有限 | 否 | 兼容宽松文本 |
write 不属于上表,因为它是完整内容写入工具。
不同基模如何选择
强模型:apply_patch
推荐:
edit:
mode: apply_patch
modelVariants: {}
它能一次表达多文件、多 hunk、创建、删除和移动,适合生成 diff UI,也避免反复复制长 old_string。
中等模型:replace 或 patch
无法稳定生成复杂 envelope 时使用:
edit:
mode: replace
它适合小而明确的局部修改。如果模型能生成基本 unified hunk,但多文件需求不高,可以选 patch。
安全优先:hashline
如果首要目标是防止编辑过时内容、限制模型只能修改已读行,并需要 block/register,应使用:
edit:
mode: hashline
兼容旧模型:sloppy
只有在模型对严格 schema 持续失败,且接受较弱结构安全时才考虑 sloppy。它不是更强的 apply_patch。
不同模型在预训练期间接触的edit工具不同
“不同模型家族熟悉的编辑接口不一样”是code agent的一个核心设计前提。其依据主要来自各个模型厂商的官方工具文档、开源实现。Pi为适配它们做了不少工程实践。
DeepSeek 系列 → str_replace_editor
DeepSeek 官方在其 deepseek-harness 项目中提供了独立的 str_replace_editor 工具包,其 README 明确指出这是为了提供“Claude Code 风格”的文件编辑能力。
- 官方来源:DeepSeek 官方发布的
@deepseek-ai/dsh-tool-str-replace-editornpm 包,其 schema 提供view、create、str_replace、insert四个命令,操作绝对路径。 - 训练证据:第三方分析指出,DeepSeek 的官方 minimal preset 将
bash和str_replace_editor的组合称为“exact RL prompt and schemas”,表明该模型在训练/评测中大量接触这套工具组合。 - Pi 的适配:Pi 的扩展
@kennyfrc/pi-str-replace-editor仅对 DeepSeek 系列模型启用,并会替换掉内置的read/edit/write工具,以提供“模型被训练过的工具”。
GPT / Codex 系列 → apply_patch
OpenAI 为 GPT 和 Codex 模型提供了结构化的 apply_patch 工具,官方描述其“适用于多文件或多 hunk 编辑,在这些场景下单一的 edit 调用会非常脆弱”。
- 官方文档:OpenClaw 的文档明确说明
apply_patch默认对 OpenAI 和 OpenAI Codex 模型可用。 - 训练证据:OpenAI 的 Codex 仓库中有一个 PR,专门指示模型使用
apply_patch进行单文件编辑。Simon Willison 也指出,OpenAI 曾谈论过他们的模型是如何被训练来有效使用该工具的。 - Pi 的适配:Pi 的扩展
pi-model-suitable-tools会为 GPT/Codex 模型提供 Codex 风格的shell_command和apply_patch,其apply_patch使用 Codex 风格的补丁语法。
Claude 系列 → str_replace_based_edit_tool
Anthropic 官方定义了一个名为 str_replace_based_edit_tool 的文本编辑器工具,通过 Messages API 提供给 Claude,支持 view、str_replace、create、insert 命令。
- 官方文档:Anthropic 平台文档详细说明了该工具的使用方式,并指出
max_characters参数仅与text_editor_20250728及更高版本兼容。 - 训练证据:Simon Willison 的博客指出,较新的 Claude 模型(如 Opus 4.8、Sonnet 5)反而比旧模型更容易错误调用 Pi 的自定义编辑工具。他引用 Armin 的理论认为,这是因为这些新模型被专门训练(很可能通过强化学习)以更好地使用 Claude Code 内置的编辑工具,导致它们对第三方 harness 的自定义 schema 更不适应。
- Pi 的适配:
pi-model-suitable-tools会为 Claude 模型提供 Claude Code 风格的别名(如Read、Edit、Write),这些别名会委托给 Pi 的内置工具。
💡 总结
这些来源共同指向一个事实:主流模型厂商都在围绕自家编码代理(Claude Code、Codex)的编辑接口对其模型进行训练或微调。因此,第三方代理工具为了获得最佳的模型表现,最理性的策略就是按模型家族路由到它们各自最熟悉的编辑协议。这不仅是 Pi 的设计,也是 Cursor 等工具在“为不同模型定制 harness”时所遵循的原则。