A2 工具系统:模型怎么说清「改哪里」? 第 3 课
能不能从源头保证 patch 格式合法? 让格式从源头合法
在 prompt 里教格式,模型仍会写错:aider 榜上 diff 格式的「格式正确」最低只有 64.4%,Codex 的解析器也为模型放宽了规则。Codex 的做法是把 19 行 Lark 语法交给服务端,解码时就把「会让格式不合法」的 token 挡掉。它保证的只是语法:定位仍靠 seek_sequence,两处相同的行照样改到第一处。
❓ 本课一问: 第 2 课的引擎都在收拾「模型抄错了」的残局。Codex 走的是另一条路:让模型写一种专门的 patch 格式。可格式本身也会写错——能不能在模型生成的时候,就保证它写出来的一定合法?
这一课先不碰: patch 写对之后怎么在文件里定位(第 2 课的 seek_sequence);边生成边解析的流式 patch(Codex 的 streaming_parser.rs,A8 讲流式时再看);外部工具(第 4 课)。
从已有出发
A1 说过:模型只会生成文字,工具调用也是文字。本课项目的真实请求里能直接看到——pd-truncate-s1 第 1 轮模型回的那条调用,经 Qwen3 的 chat template 渲染后就是这样一段文本:
<tool_call>
{"name": "list_dir", "arguments": {"path": "."}}
</tool_call>
从已有出发 · 累计数据流 点已学环节看它回答什么
任务 贯穿任务 → ① 编排选择 A0 → 每一轮循环都经过 ② 循环 A1 → ③ 工具层本课 A2 → ④ 权限闸 A3 → ⑤ 沙箱 A4 → ⑥ 上下文管理 A5
→ ⑦ 记忆与会话 A6 → ⑧ 子 agent A7 → ⑨ 模型接入与计费 A8 → ⑩ 验证与评测 A9
服务端的解析器把 tool_call 标签里的 JSON 切出来,变成结构化的 tool_calls 字段。JSON 这种格式模型训练时见得多:本课项目迄今 26 次运行、139 次工具调用,参数解析失败 0 次(项目 trace 统计,单一模型、样本小)。
patch 不一样。Codex 的 apply_patch 要求模型写出下面这种格式——既不是 JSON,也不是 git 的 unified diff,而是 Codex 自己定义的:
*** Begin Patch
*** Update File: dateparse.py
@@ def parse_date(text):
- month, day, year = (int(g) for g in m.groups())
+ day, month, year = (int(g) for g in m.groups())
*** End Patch
先自己设计:在 prompt 里把格式教清楚
最直接的办法是写一份说明书放进 prompt。Codex 仓库里就保留着这样一份,75 行,给不支持语法约束的模型用:
① 朴素方案 ② 它在哪失败 ③ 改进方案 朴素方案 在 prompt 里教格式,模型照着写
apply_patch_tool_instructions.md(纯 prompt 版说明,共 75 行) codex@8cabf5a:codex-rs/prompts/templates/apply_patch_tool_instructions.md:1-24 需联网 Apache-2.0 1 ## `apply_patch`
2
3 Use the `apply_patch` shell command to edit files.
4 Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope:
5
6 *** Begin Patch
7 [ one or more file sections ]
8 *** End Patch
9
10 Within that envelope, you get a sequence of file operations.
11 You MUST include a header to specify the action you are taking.
12 Each operation starts with one of three headers:
13
14 *** Add File: <path> - create a new file. Every following line is a + line (the initial contents).
15 *** Delete File: <path> - remove an existing file. Nothing follows.
16 *** Update File: <path> - patch an existing file in place (optionally with a rename).
17
18 May be immediately followed by *** Move to: <new path> if you want to rename the file.
19 Then one or more “hunks”, each introduced by @@ (optionally followed by a hunk header).
20 Within a hunk each line starts with:
21
22 For instructions on [context_before] and [context_after]:
23 - By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
24 - If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
解析器为模型放宽规则 codex@8cabf5a:codex-rs/apply-patch/src/parser.rs:46-53 需联网 Apache-2.0 46
47 /// Currently, the only OpenAI model that knowingly requires lenient parsing is
48 /// gpt-4.1. While we could try to require everyone to pass in a strictness
49 /// param when invoking apply_patch, it is a pain to thread it through all of
50 /// the call sites, so we resign ourselves allowing lenient parsing for all
51 /// models. See [`ParseMode::Lenient`] for details on the exceptions we make for
52 /// gpt-4.1.
53 const PARSE_IN_STRICT_MODE: bool = false;
👆 带下划线的行可以点:点哪行,讲哪行。
改进方案 把语法交给解码器,生成时就挡掉不合法的 token
把格式写成一份语法 ,不是给模型看,而是交给服务端的解码器。模型每生成一个 token 之前,解码器先算出「哪些 token 接上去之后,文本仍然可能是一个合法 patch」,其余 token 的概率直接置零。于是最后吐出来的,一定 符合语法。
Codex 就是这么做的:apply_patch 不是 JSON 函数工具,而是一个 Freeform 工具,它的 format 字段是 type: grammar, syntax: lark,内容就是下面那 19 行语法。
✗ 失败点 说明写得再清楚,模型还是会写错:aider 榜上 47 个 diff 格式条目,「格式正确」的比例最低 64.4% ,中位数 94.2%,低于 95% 的有 26 个(官方自报)。Codex 自己的解析器也承认:为了 gpt-4.1,它对所有 模型都放宽了解析。
看它在哪失败 换成改进方案 重来一遍
同一个模型换一种更简单的格式,错得就少:aider 榜上同一个 Qwen2.5-Coder-32B-Instruct,whole 格式 99.6%,diff 格式 71.6%(第 1 课的图)。格式越复杂,靠 prompt 教就越不稳。
aider 排行榜:模型按哪种 edit 格式回复,「格式正确」的比例(每个点一个模型,69 个条目) 60% 70% 80% 90% 100% whole diff-fenced diff architect Gemini 2.0 Pro exp-02-05 · whole · 格式正确 100% · 两次尝试通过率 35.6% · 2025-02-25 gpt-4o-mini-2024-07-18 · whole · 格式正确 100% · 两次尝试通过率 3.6% · 2024-12-21 claude-3-5-sonnet-20241022 · diff · 格式正确 99.6% · 两次尝试通过率 51.6% · 2025-01-17 gpt-4o-2024-11-20 · diff · 格式正确 95.1% · 两次尝试通过率 18.2% · 2024-12-30 gpt-4o-2024-08-06 · diff · 格式正确 94.2% · 两次尝试通过率 23.1% · 2024-12-30 o1-2024-12-17 (high) · diff · 格式正确 91.5% · 两次尝试通过率 61.7% · 2024-12-21 DeepSeek Chat V2.5 · diff · 格式正确 92.9% · 两次尝试通过率 17.8% · 2024-12-21 claude-3-5-haiku-20241022 · diff · 格式正确 91.1% · 两次尝试通过率 28% · 2024-12-21 Qwen2.5-Coder-32B-Instruct · diff · 格式正确 71.6% · 两次尝试通过率 8% · 2024-12-22 o1-mini-2024-09-12 · whole · 格式正确 96.9% · 两次尝试通过率 32.9% · 2024-12-22 gemini-exp-1206 · whole · 格式正确 98.2% · 两次尝试通过率 38.2% · 2024-12-22 gemini-2.0-flash-exp · whole · 格式正确 100% · 两次尝试通过率 22.2% · 2024-12-22 yi-lightning · whole · 格式正确 92.9% · 两次尝试通过率 12.9% · 2024-12-23 DeepSeek Chat V3 (prev) · diff · 格式正确 98.7% · 两次尝试通过率 48.4% · 2024-12-25 Qwen2.5-Coder-32B-Instruct · whole · 格式正确 99.6% · 两次尝试通过率 16.4% · 2024-12-26 Codestral 25.01 · whole · 格式正确 100% · 两次尝试通过率 11.1% · 2025-01-13 DeepSeek R1 · diff · 格式正确 96.9% · 两次尝试通过率 56.9% · 2025-01-20 DeepSeek R1 + claude-3-5-sonnet-20241022 · architect · 格式正确 100% · 两次尝试通过率 64% · 2025-01-23 qwen-max-2025-01-25 · diff · 格式正确 90.2% · 两次尝试通过率 21.8% · 2025-01-28 o3-mini (medium) · diff · 格式正确 95.1% · 两次尝试通过率 53.8% · 2025-01-31 o3-mini (high) · diff · 格式正确 93.3% · 两次尝试通过率 60.4% · 2025-01-31 gemini-2.0-flash-thinking-exp-01-21 · diff · 格式正确 77.8% · 两次尝试通过率 18.2% · 2025-01-21 chatgpt-4o-latest (2025-02-15) · diff · 格式正确 93.3% · 两次尝试通过率 27.1% · 2025-02-15 claude-3-7-sonnet-20250219 (no thinking) · diff · 格式正确 93.3% · 两次尝试通过率 60.4% · 2025-02-24 claude-3-7-sonnet-20250219 (32k thinking tokens) · diff · 格式正确 97.8% · 两次尝试通过率 64.9% · 2025-02-24 gpt-4.5-preview · diff · 格式正确 97.3% · 两次尝试通过率 44.9% · 2025-02-27 QwQ-32B · diff · 格式正确 67.6% · 两次尝试通过率 20.9% · 2025-03-06 QwQ-32B + Qwen 2.5 Coder Instruct · architect · 格式正确 100% · 两次尝试通过率 26.2% · 2025-03-07 command-a-03-2025-quality · whole · 格式正确 99.6% · 两次尝试通过率 12% · 2025-03-14 gemma-3-27b-it · whole · 格式正确 100% · 两次尝试通过率 4.9% · 2025-03-15 DeepSeek V3 (0324) · diff · 格式正确 99.6% · 两次尝试通过率 55.1% · 2025-03-24 Gemini 2.5 Pro Preview 03-25 · diff-fenced · 格式正确 92.4% · 两次尝试通过率 72.9% · 2025-04-12 chatgpt-4o-latest (2025-03-29) · diff · 格式正确 64.4% · 两次尝试通过率 45.3% · 2025-03-29 Quasar Alpha · diff · 格式正确 98.2% · 两次尝试通过率 54.7% · 2025-04-04 Llama 4 Maverick · whole · 格式正确 99.1% · 两次尝试通过率 15.6% · 2025-04-06 Grok 3 Beta · diff · 格式正确 99.6% · 两次尝试通过率 53.3% · 2025-04-10 Grok 3 Mini Beta (low) · whole · 格式正确 100% · 两次尝试通过率 34.7% · 2025-04-10 Grok 3 Mini Beta (high) · whole · 格式正确 99.6% · 两次尝试通过率 49.3% · 2025-04-10 Optimus Alpha · diff · 格式正确 97.3% · 两次尝试通过率 52.9% · 2025-04-10 gpt-4.1 · diff · 格式正确 98.2% · 两次尝试通过率 52.4% · 2025-04-14 gpt-4.1-mini · diff · 格式正确 92.4% · 两次尝试通过率 32.4% · 2025-04-14 gpt-4.1-nano · whole · 格式正确 94.2% · 两次尝试通过率 8.9% · 2025-04-14 o4-mini (high) · diff · 格式正确 90.7% · 两次尝试通过率 72% · 2025-04-16 openhands-lm-32b-v0.1 · whole · 格式正确 95.1% · 两次尝试通过率 10.2% · 2025-04-19 gemini-2.5-flash-preview-04-17 (default) · diff · 格式正确 85.3% · 两次尝试通过率 47.1% · 2025-04-20 Gemini 2.5 Pro Preview 05-06 · diff-fenced · 格式正确 97.3% · 两次尝试通过率 76.9% · 2025-05-07 Qwen3 32B · diff · 格式正确 83.6% · 两次尝试通过率 40% · 2025-05-08 Qwen3 235B A22B diff, no think, Alibaba API · diff · 格式正确 92.9% · 两次尝试通过率 59.6% · 2025-05-09 claude-sonnet-4-20250514 (no thinking) · diff · 格式正确 98.2% · 两次尝试通过率 56.4% · 2025-05-24 claude-sonnet-4-20250514 (32k thinking) · diff · 格式正确 97.3% · 两次尝试通过率 61.3% · 2025-05-24 claude-opus-4-20250514 (no think) · diff · 格式正确 98.7% · 两次尝试通过率 70.7% · 2025-05-25 claude-opus-4-20250514 (32k thinking) · diff · 格式正确 97.3% · 两次尝试通过率 72% · 2025-05-25 gemini-2.5-flash-preview-05-20 (no think) · diff · 格式正确 93.8% · 两次尝试通过率 44% · 2025-05-26 gemini-2.5-flash-preview-05-20 (24k think) · diff · 格式正确 95.6% · 两次尝试通过率 55.1% · 2025-05-25 gemini-2.5-pro-preview-06-05 (default think) · diff-fenced · 格式正确 100% · 两次尝试通过率 79.1% · 2025-06-06 gemini-2.5-pro-preview-06-05 (32k think) · diff-fenced · 格式正确 99.6% · 两次尝试通过率 83.1% · 2025-06-06 DeepSeek R1 (0528) · diff · 格式正确 94.6% · 两次尝试通过率 71.4% · 2025-06-06 o3 (high) · diff · 格式正确 94.7% · 两次尝试通过率 81.3% · 2025-06-25 o3 · diff · 格式正确 93.8% · 两次尝试通过率 76.9% · 2025-06-25 o3 (high) + gpt-4.1 · architect · 格式正确 100% · 两次尝试通过率 78.2% · 2025-06-27 o3-pro (high) · diff · 格式正确 97.8% · 两次尝试通过率 84.9% · 2025-06-28 grok-4 (high) · diff · 格式正确 97.3% · 两次尝试通过率 79.6% · 2025-07-11 Kimi K2 · diff · 格式正确 92.9% · 两次尝试通过率 59.1% · 2025-07-17 gpt-oss-120b (high) · diff · 格式正确 79.1% · 两次尝试通过率 41.8% · 2025-08-06 gpt-5 (high) · diff · 格式正确 91.6% · 两次尝试通过率 88% · 2025-08-23 gpt-5 (medium) · diff · 格式正确 88.4% · 两次尝试通过率 86.7% · 2025-08-25 gpt-5 (low) · diff · 格式正确 86.7% · 两次尝试通过率 81.3% · 2025-08-25 DeepSeek-V3.2-Exp (Reasoner) · diff · 格式正确 97.3% · 两次尝试通过率 74.2% · 2025-10-03 DeepSeek-V3.2-Exp (Chat) · diff · 格式正确 98.2% · 两次尝试通过率 70.2% · 2025-10-03 71.6% 99.6%
格式 条目 最低 中位数 whole15 92.9% 99.6% diff-fenced4 92.4% 98.45% diff47 64.4% 94.2% architect3 100% 100%
高亮的一对是同一个模型 Qwen2.5-Coder-32B-Instruct 分别用 diff 与 whole 格式跑同一套题。数据:Aider-AI/aider 本地副本 @ cb6a152e 2025-10-04 11:14:08 +0200(aider/website/_data/polyglot_leaderboard.yml、edit_leaderboard.yml);字段 percent_cases_well_formed(线上页面列名「Correct edit format」,2025-11-20 更新,2026-09-26 核对可访问)。官方自报、单次运行;「格式正确」是 aider 自己的判定口径,不等于改对了代码。
改进:语法约束解码
Codex 把语法交给服务端的那几行:
apply_patch_spec.rs codex@8cabf5a:codex-rs/core/src/tools/handlers/apply_patch_spec.rs:5-28 需联网 Apache-2.0 5 const APPLY_PATCH_LARK_GRAMMAR: &str = include_str!("apply_patch.lark");
6
7 /// Returns a custom tool that can be used to edit files. Well-suited for GPT-5 models
8 /// https://platform.openai.com/docs/guides/function-calling#custom-tools
9 pub fn create_apply_patch_freeform_tool(include_environment_id: bool) -> ToolSpec {
10 let definition = if include_environment_id {
11 APPLY_PATCH_LARK_GRAMMAR.replace(
12 "start: begin_patch hunk+ end_patch",
13 "start: begin_patch environment_id? hunk+ end_patch\nenvironment_id: \"*** Environment ID: \" filename LF",
14 )
15 } else {
16 APPLY_PATCH_LARK_GRAMMAR.to_string()
17 };
18 ToolSpec::Freeform(FreeformTool {
19 name: "apply_patch".to_string(),
20 description: "The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.".to_string(),
21 defer_loading: None,
22 format: FreeformToolFormat {
23 r#type: "grammar".to_string(),
24 syntax: "lark".to_string(),
25 definition,
26 },
27 })
28 }
👆 带下划线的行可以点:点哪行,讲哪行。
OpenAI 的文档写明了服务端怎么用它 理论动机 证据等级 · 有理论动机 可证明 理论动机 实验验证 有数学或机制上的理由说明它可能 有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条 官方一手说明「支持 lark 与 regex 两种 CFG 语法」「采样由 LLGuidance 约束」;它是机制说明,不是效果评测。 出处 OpenAI Function calling 文档(2026-09-26 核对) ↗ 需联网 :自定义工具可以带一份上下文无关文法,目前支持 lark 与 regex 两种写法,采样过程由 LLGuidance 约束。
这不是 OpenAI 独有的思路。Claude 的工具定义可以设 strict: true,官方文档的说法是它「constraining the model’s token sampling to schema-valid outputs」,保证工具名与参数符合 JSON Schema 理论动机 证据等级 · 有理论动机 可证明 理论动机 实验验证 有数学或机制上的理由说明它可能 有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条 约束的是 JSON Schema,不是任意文法;同页写明每个请求最多 20 个 strict 工具等限制。 出处 Claude 文档:Strict tool use(2026-09-26 核对) ↗ 需联网 。Pi 从 0.86.0 起内置工具默认优先用这种 JSON Schema 约束采样(勘误表 P8,pin 31b513e3 之后的新版本)。
在浏览器里看「解码器看到的东西」
下面这个组件把那 19 行语法写成了一个按行推进的检查器:每输入一个字符,它回答「这还是不是某个合法 patch 的开头」,以及「下一个字符允许是什么」——后一个问题,正是约束解码每一步要回答的。
apply_patch 的 19 行 Lark 语法 · 逐字检查前缀
语法原文:openai/codex@8cabf5a apply_patch.lark(最新版移到 core/assets/tools/,内容逐字相同,见勘误表 X3)。本组件把语法写成按行推进的状态机;参照答案由 gen_a02_fixtures.py 把同一份语法机械转写成 Python 正则算出(本机未装 lark,python-re:按 apply_patch.lark 19 行逐条转写(完整串 + 前缀闭包))。它只检查「字符串属不属于这门语言」,不检查 patch 能不能在文件上找到位置——那是 seek_sequence 的事。
🎯 试三件事:① 先猜「模型写成了 unified diff」会在第几个字符被拒,再点它:第 1 个字符 - 就不行,此刻只允许 *;② 点「像解码器一样逐字回放」看合法 patch:每一步下面那排字符里,绿色的才是解码器允许采样的;③ 点「@@ 后面空着」和「Windows 换行」:前者卡在 @@ 之后必须至少一个字符,后者卡在 \r——语法里的换行只认 \n。
19 行语法本身也值得逐行读:
apply_patch.lark codex@8cabf5a:codex-rs/core/src/tools/handlers/apply_patch.lark:1-19 需联网 Apache-2.0 最新版已变化 · 勘误表 X3 1 start: begin_patch hunk+ end_patch
2 begin_patch: "*** Begin Patch" LF
3 end_patch: "*** End Patch" LF?
4
5 hunk: add_hunk | delete_hunk | update_hunk
6 add_hunk: "*** Add File: " filename LF add_line+
7 delete_hunk: "*** Delete File: " filename LF
8 update_hunk: "*** Update File: " filename LF change_move? change?
9
10 filename: /(.+)/
11 add_line: "+" /(.*)/ LF -> line
12
13 change_move: "*** Move to: " filename LF
14 change: (change_context | change_line)+ eof_line?
15 change_context: ("@@" | "@@ " /(.+)/) LF
16 change_line: ("+" | "-" | " ") /(.*)/ LF
17 eof_line: "*** End of File" LF
18
19 %import common.LF
👆 带下划线的行可以点:点哪行,讲哪行。
⚠ 常见误解:「用了约束解码,模型就更会改代码了。」 它只保证「写出来的是一个语法合法的 patch」。patch 在文件里落在哪 ,仍由第 2 课的 seek_sequence 决定:它按整行比较、不检查唯一、取第一处。第 2 课的真实用例在 Codex 上照样出问题——pd-truncate-s1 第 4 轮那行在文件里有两处,Codex 改到了第 31 行的 ISO 分支,写回测试 FAILED (failures=2);pd-unlimited-s3 第 4 轮的 old_string 没写缩进,Codex 在「忽略首尾空白」这一级接住,再把没缩进的新行原样写进去,结果是 IndentationError(Codex 一栏的输入是把 old/new 按行转写成 patch,见第 2 课的说明)。
🔍 Codex 为什么在 patch 里要求上下文行和 @@? 正因为 seek_sequence 取第一处。纯 prompt 版说明(上面 75 行那份)要求默认带上下 3 行上下文,3 行不够唯一时再用 @@ 写出所属的 class 或函数名;compute_replacements 会先用 @@ 那一行把搜索起点推到它后面(file_update.rs:94-107),再找变更块。也就是说,「唯一」这件事被交还给了模型:它要自己写够上下文。约束解码能保证 @@ 后面有文字,保证不了那段文字足够把位置钉死。
证据三问
问 本课的回答 ① 它解决什么问题? 自定义格式靠 prompt 教不稳:格式错了,harness 只能报错或宽松解析 ② 为什么可能有效? 每一步只允许「仍可能合法」的 token,最终输出必在语法之内 可证明 证据等级 · 数学可证明 可证明 理论动机 实验验证 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条 算法性质:若每步都把会使前缀不可行的 token 概率置零,且语法的每个可行前缀都能补全成完整句子,则生成结果一定属于该语法。本页组件对 12 条样例、86 个检查点与按语法转写的参照判定逐条一致。 出处 待补 ;它不改变模型「想改哪里」 ③ 实验上呢? 靠 prompt 时格式错误率不为零:aider 榜 diff 类格式「格式正确」最低 64.4% 实验验证 证据等级 · 实验验证 可证明 理论动机 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条 aider 自己的判定口径、单次运行。 出处 aider 排行榜(官方自报) ↗ 需联网 ;用约束解码之后格式错误率降到多少,本轮没有找到公开的对照数据,只有厂商的机制说明
演进地图:你在这里
模型怎么说清「改哪里」:每一代都被上一代的某个真数字逼出来
解决了 模型不用复述原文,只报行号,输出最短。
卡在 行号是「上一次读文件时」的快照:第一处修复只要多插 2 行,第二次按旧行号 41 改,就落在 if m: 上(第 1 课现算);CoreCoder README 的原话是「Line numbers are a trap」。
第一代 整文件覆写 write_file;aider 的 whole 格式 解决了 不需要定位,格式最不容易错:aider 同一个 Qwen2.5-Coder-32B,whole 格式合规 99.6% ,diff 格式只有 71.6% 。
卡在 只改 1 行,也要把 44 行的文件整个再写一遍:参数 487 token,片段替换只要 54 (9.0 倍,项目模型 tokenizer 实数);还会覆盖别人在此期间的改动。
解决了 按内容定位,不怕行号过期;0 处或多处都报错、文件不动,让模型自己补上下文。
卡在 真实 trace pd-truncate-s1:第 4 轮 old_string 在文件里 2 处、第 5 轮缩进多 4 格 0 处,两次报错多跑了两轮,花掉 8,481 token、109.5 秒;唯一也不等于对:pd-v2-unlimited-s1 第 7 轮的 old_string 恰好是已修好那一行的前半截,计数 = 1,于是 + 1 变成了 + 1 + 1。
解决了 模型复述时的小漂移不再白费一轮:上面第 5 轮那条缩进多 4 格的 edit,OpenCode 直接接住(LineTrimmed 级)。
卡在 接住 ≠ 改对:多转义一层的引号,OpenCode 的 EscapeNormalized 级照样接住,写回去的是 \",模块导入即 SyntaxError ;OpenCode 的 v2(packages/core)眼下只有精确匹配,「edit fuzzy parity」还列在待移植清单里。
解决了 语法由解码器保证,不再靠 prompt 教。对照:靠 prompt 教格式时,aider 榜上 47 个 diff 格式条目的「格式正确」比例最低只有 64.4% (官方自报)。
卡在 语法只管格式,定位仍是 seek_sequence:它不检查唯一、取第一处——两处相同的 return datetime.date(year, month, day) 会改到第 31 行 ISO 分支,test_parse_iso 跟着坏;整行比较还会把没写缩进的 new 行原样写进去,得到 IndentationError 。
解决了 一套协议接任意外部工具;同一个 harness 给 GPT 换上 apply_patch、给 Claude 用 edit。
卡在 接进来的每个工具每轮都要付上下文:chrome-devtools-mcp 的 30 个工具在项目模型的 chat template 里占 7,047 token,是 miniagent 自己 6 个工具(685)的 10.3 倍。
解决了 N 次「模型 → 工具 → 模型」往返压成一段脚本。
卡在 省的是往返,不省判断:真实 trace 里模型每一步都要看上一步的结果才知道下一步做什么(岔路课逐轮算)。
数字口径:token 数用项目录制模型 Qwen3-8B 的 tokenizer 与 chat template 算(与服务端 usage 逐轮核对一致);裁决与测试结果是原仓库代码实跑(a02-edit-fixtures.json,2026-09-26);trace 来自本课项目 miniagent 的真实运行(2026-09-26);aider 数字来自 aider 仓库的 polyglot_leaderboard.yml(官方自报);MCP 工具数来自本机实跑 tools/list。
一句话
📌 能用解码约束保证的格式,就别靠 prompt 教;约束只管格式,不管定位。
🚦 先停在这里: 分清两件事:「写得合法」可以由解码器保证,「改对地方」不行。
再深一层: 在检查器里自己写一个 patch,故意漏掉 *** End Patch,看状态停在「合法前缀,还没写完」——约束解码会一直不让模型结束,直到它补上。
下一个缺口
内置工具之外,外部工具怎么接进来? → 第 4 课:外部工具怎么接进来?MCP 放在哪一层?
到这里,工具都是 harness 自己写的:read、edit、bash……可用户还想接数据库、浏览器、公司内部的系统。第 4 课看外部工具怎么进来,以及每接一个工具,每一轮要多付多少上下文。
你可能会问
「为什么不干脆让 apply_patch 也用 JSON 参数?」 可以,把整个 patch 塞进一个字符串参数就行,本课项目的 edit_file 就是 JSON。代价是 patch 里的换行、引号都要转义(第 2 课「转义差异」那一题就是这么来的);Freeform 工具直接收原始文本,少一层转义。
「语法约束会不会让模型写得更慢、更笨?」 每一步多一次「哪些 token 允许」的计算,由服务端完成;对模型能力的影响,本轮没有找到公开数据,不下结论。
「Codex 的 patch 能一次改多个文件吗?」 能:一个 patch 里可以有多个 Add / Delete / Update hunk,改名是 Update 的一个可选行(*** Move to:,parser.rs:66-83)。