从0打造编码Agent 实验 实验 ≈ 30 分钟 页内实验 ×1
A2 工具系统:模型怎么说清「改哪里」? 实验课

同一批真实 edit,五个真引擎各判成什么?改完测试过不过?

五个真引擎跑真实 edit
把本课项目 trace 里的全部 edit_file 调用(T8)同时交给 learn-claude-code、CoreCoder、Pi、OpenCode、Codex 五个引擎的浏览器移植,再和当时 miniagent 的真实结果并排;每一格都附上「写回任务仓后实跑测试」的结果,页面加载时先与原仓库代码实跑的结果逐条比对。
🧪

这一课做什么:前四课用构造的「坑」讲清了每个引擎的规则。这一课只用真实语料:本课项目 miniagent 在 18 次运行里一共发出 33 次 edit_file,去重后 14 条不同的(改前文件 + old_string + new_string)。每一条同时交给五个引擎,看裁决,再看改完之后测试过不过。

从已有出发 · 累计数据流 点已学环节看它回答什么
每一轮循环都经过

实验

实验 · 五个真 edit 引擎 × T8 真实语料(Qwen3-8B · 2026-09-26)
实验 · T8 真实 edit 语料 × 六个真引擎 正在用浏览器移植重跑 fixture…

口径:引擎逻辑移植自各 pin 源码(卡片上的链接),期望输出由原仓库代码实跑得到(Python 3.12 / Node 26 / rustc 1.97,脚本 src/data/precomputed/gen_a02_fixtures.py,生成于 2026-09-26);T8 语料来自本课项目真实运行(模型 Qwen/Qwen3-8B-MLX-4bit@383413e,录制于 2026-09-26);「改后测试」是把引擎的结果写回任务仓副本、实跑 python -m unittest 的结果,浏览器里改动过的自定义输入不跑测试。Codex 一栏:apply_patch 以整行为单位,这里把 old/new 各按换行切成一个无 @@ 的 update chunk 交给它——这是构造的转写,不是模型写的 patch。

怎么读这张表

🎯

试三件事:① 找到出自 pd-truncate-s1 第 4 轮的那一行:learn-claude-code 与 Codex 都「改成功了」,可测试里多挂了一个 test_parse_iso——改到了 ISO 分支;② 找到 pd-v2-unlimited-s1 第 7 轮:它的改前文件测试是全过的,四个按子串匹配的引擎都照改,改完反而挂了,只有 Codex 拒绝;③ 勾上「同时显示构造坑」,对比合计行:没有一个引擎在所有行上都对。

读数

合计行里的几个数(真实语料部分):

引擎改了拒绝改后改坏
learn-claude-code s021313
CoreCoder v0.7.01222
Pi1222
OpenCode1312
Codex(整行转写)1224
本课项目 miniagent1222

「改坏」的判定:写回后出现语法或缩进错误,或者出现了改前文件里没有的失败测试。「拒绝」不算错——被拒的 edit 文件不动,模型会收到报错再试一次(代价是一轮,第 2 课算过)。

⚠

别从这张表读出「哪个引擎最好」。语料只有 14 条、单一模型、单一任务 。它能说明的是:每一种设计都在某一条真实 edit 上付出了代价——只查存在会改错位置,严格唯一会多花轮次,子串匹配会接受半行,整行匹配会丢缩进。选哪条路,是在这些代价之间取舍,这正是里程碑 M2 要你拍板的决策。

自己动手

  1. 在项目里多跑几次任务(或换一个模型),trace 会多出新的 edit_file;
  2. 运行 python3 src/data/precomputed/gen_a02_fixtures.py,脚本用原仓库代码重算全部期望输出和测试结果;
  3. 刷新本页,新语料会出现在表里,右上角的一致性检查会覆盖它们。

生成脚本不改写任何算法:它从只读的 pin 副本按行号抽取原文来跑——OpenCode 与 Pi 用 Node 直接执行 TypeScript 原文,CoreCoder 按包结构加载原模块、learn-claude-code 抽出 run_edit 原函数,Codex 用 rustc 编译 seek_sequence.rs 原文件;写回任务仓副本后再跑测试。浏览器里的移植只在页面上用,它和脚本的结果逐条比对。

演进地图:你在这里

这张表同时覆盖了演进图的三代:唯一片段、容错匹配、语法约束 patch 的定位。

模型怎么说清「改哪里」:每一代都被上一代的某个真数字逼出来
  1. 朴素 按行号改 「把第 a–b 行换成……」
    解决了模型不用复述原文,只报行号,输出最短。
    卡在行号是「上一次读文件时」的快照:第一处修复只要多插 2 行,第二次按旧行号 41 改,就落在 if m: 上(第 1 课现算);CoreCoder README 的原话是「Line numbers are a trap」。
  2. 第一代 整文件覆写 write_file;aider 的 whole 格式
    解决了不需要定位,格式最不容易错:aider 同一个 Qwen2.5-Coder-32B,whole 格式合规 99.6%,diff 格式只有 71.6%。
    卡在只改 1 行,也要把 44 行的文件整个再写一遍:参数 487 token,片段替换只要 54(9.0 倍,项目模型 tokenizer 实数);还会覆盖别人在此期间的改动。
  3. 第二代 唯一片段替换(search-replace) CoreCoder、本课项目 miniagent 你在这里
    解决了按内容定位,不怕行号过期;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。
  4. 第三代 容错匹配:归一化与 9 级 replacer Pi、OpenCode 你在这里
    解决了模型复述时的小漂移不再白费一轮:上面第 5 轮那条缩进多 4 格的 edit,OpenCode 直接接住(LineTrimmed 级)。
    卡在接住 ≠ 改对:多转义一层的引号,OpenCode 的 EscapeNormalized 级照样接住,写回去的是 \",模块导入即 SyntaxError;OpenCode 的 v2(packages/core)眼下只有精确匹配,「edit fuzzy parity」还列在待移植清单里。
  5. 第四代 语法约束解码的 patch Codex apply_patch(19 行 Lark 语法);strict 工具 你在这里
    解决了语法由解码器保证,不再靠 prompt 教。对照:靠 prompt 教格式时,aider 榜上 47 个 diff 格式条目的「格式正确」比例最低只有 64.4%(官方自报)。
    卡在语法只管格式,定位仍是 seek_sequence:它不检查唯一、取第一处——两处相同的 return datetime.date(year, month, day) 会改到第 31 行 ISO 分支,test_parse_iso 跟着坏;整行比较还会把没写缩进的 new 行原样写进去,得到 IndentationError。
  6. 工具从哪来 工具集随模型变,外部工具经 MCP 接入 OpenCode registry.ts;MCP 2026-07-28
    解决了一套协议接任意外部工具;同一个 harness 给 GPT 换上 apply_patch、给 Claude 用 edit。
    卡在接进来的每个工具每轮都要付上下文:chrome-devtools-mcp 的 30 个工具在项目模型的 chat template 里占 7,047 token,是 miniagent 自己 6 个工具(685)的 10.3 倍。
  7. 岔路 Code Mode:让模型写脚本调工具 Codex exec / wait;OpenCode codemode
    解决了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。

一句话

📌
同一条 edit,五个引擎给出三种裁决;只有跑过测试才知道谁对。