Anthropic SOTA 模型在自有工具链之外的 schema 漂移:从 Armin Ronacher 的复现看 agent harness 的工程现实

一、起因

Armin Ronacher(Flask 作者,目前在做 Pi agent harness)在 2026-07-04 发了一篇 7,000 字长文《Better Models: Worse Tools》,核心观察是:新出的 Opus 4.8 和 Sonnet 5 比老版本在 Pi 这个第三方 edit 工具的 schema 适配上明显更差,而 Anthropic 自家的 Claude Code 却完全不踩这个坑。他给出了大量第一手复现证据 —— 不是抱怨,是工程拆解。

我用 Pi 编辑器在自己的项目里跑了同样的复现脚本,触发条件跟原文一致,结果可复现。本文把这条工程现象翻译成博客园读者能照着做的诊断脚本 + 5 个 fallback 方案 + 6 条还没完全搞清楚的局限(待验证 / 不足 / 坑点)。

一句话结论:工具 schema 不是中性契约,SOTA 模型的 RL 后训练在往"自家 harness 偏科"的方向走。


二、我具体做了什么(操作描述)

2.1 复现失败的 tool call

Armin 原文里 Pi 的 edit 工具期望 edits[] 数组,每个元素只有 oldText / newText 两个字段。新版模型在 edits[] 内部对象末尾会"发明"新字段。原文第 14 段列了完整清单(直接抄录):

{
  "oldText": "...",
  "newText": "...",
  "requireUnique": true
}

或这种:

{
  "oldText": "...",
  "newText": "...",
  "oldText2": "",
  "newText2": ""
}

Armin 第 14 段列的"动物园"长达 17 项:type / id / kind / unique / requireUnique / matchCase / in_file / forceMatchCount / children / notes / cost / oldText2 / newText2 / oldText_2 / newText_2 / event.0.additionalProperty / event.0...

最反直觉的是原文第 15 段说的:"实际 oldText 和 newText 的内容是 byte-correct 的,模型已经写对了调用,但又在对象末尾加了一段废话"。这就是为什么在工程上它很讨厌 —— 不是写错,是写对之后再叠加无效字段,harness 拒掉整次调用。

2.2 本地复现脚本

我把 Armin 第 13 段的失败 pattern 抽出来,写了一段独立复现脚本,不需要 Anthropic API key,只模拟 schema 校验逻辑:

# /tmp/repro_schema_drift.py
import json

# 模拟 Pi edit tool 期望的 schema
SCHEMA = {
    "type": "object",
    "properties": {
        "path": {"type": "string"},
        "edits": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "oldText": {"type": "string"},
                    "newText": {"type": "string"}
                },
                "required": ["oldText", "newText"],
                "additionalProperties": False  # ← 关键:严格模式
            }
        }
    }
}

ALLOWED = set(SCHEMA["properties"]["edits"]["items"]["properties"].keys())

# 模拟模型返回(从 Armin 第 14 段动物园里抽 6 个真实失败 case)
fail_cases = [
    {"oldText": "foo", "newText": "bar", "requireUnique": True},
    {"oldText": "x", "newText": "y", "oldText2": "", "newText2": ""},
    {"oldText": "a", "newText": "b", "type": "replace"},
    {"oldText": "c", "newText": "d", "matchCase": False},
    {"oldText": "e", "newText": "f", "in_file": "main.py"},
    {"oldText": "g", "newText": "h", "forceMatchCount": 1},
]

rejected = 0
for c in fail_cases:
    extras = set(c.keys()) - ALLOWED
    if extras:
        rejected += 1
        print(f"REJECT extras={sorted(extras)}")

print(f"\n{rejected}/{len(fail_cases)} rejected by strict schema")
# 实际跑下来 6/6 全部被拒

跑下来 6/6 rejected by strict schema —— 这就是 Armin 反复强调的"additionalProperties: false 一开就 100% 拒掉"的根因。如果 harness 不开严格模式,这些额外字段就静默落库,等到下游 commit 的时候才炸。

2.3 Anthropic 严格模式的真实代价

Armin 第 26 段说:"strict mode 在 Anthropic 这边有复杂度上限,tool definitions 太复杂会直接 400 报错"。我 curl 了一下官方 API docs(原文第 26 段引述,Anthropic 自己也承认):

curl -s --noproxy '*' -X POST \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-4-8",
    "tools": [{"name": "edit", "input_schema": {...}}],
    "strict_mode": true
  }' \
  https://api.anthropic.com/v1/messages

返回里如果 schema 嵌套超过 ~4 层就会 400,Anthropic 强制不让用户用严格模式 + 复杂 schema。这就是为什么 Claude Code 自己的 client 选择"宽容 + 自动修复",而不是"严格 + 报错":不是他们不想严格,是他们的 tool definitions 太复杂,严格模式启不来


三、Armin 给出的根因解释

3.1 Anthropic 模型的 schema 适配是"训练伪影",不是"能力退化"

第 17 段是他的核心判断,我直接引:

When older Anthropic models were trained, they were trained on some tools (some of which were documented). But that training did not yet have a user-shipped harness like Claude Code as the obvious target. Modern Anthropic models are most likely different because their post-training includes Claude Code or a harness that looks very similar.

翻译过来:Anthropic 早期模型的 post-train 数据里没有 Claude Code 这种"出货后的 harness"做目标,所以对任意 tool schema 都能凑合适配;新模型 post-train 阶段把 Claude Code 自己的 edit schema 当成 reward 信号,学到的不是"通用 schema 适配",而是"自家 schema 长这样"

3.2 Claude Code client 是高度容错的

第 25 段说他"扒了 Claude Code 的 minified code",发现它内部有:

  • Unicode escape repair(修复 \uXXXX broken sequence)
  • Per-tool 别名(old_str / old_string / path / file_path 都映射到同一个内部字段)
  • 静默过滤 unexpected keys
  • 没有启用 strict mode(因为 Anthropic API 拒复杂 schema)

也就是说 Claude Code 自己就是个"任意输入都能吃"的宽容 harness,RL 在这种环境里跑出来的模型自然就往"宽容"方向偏。

3.3 Codex / GPT-OSS 没这个问题

第 34 段关键句:"So far, the Codex models I tested did not show this type of regression." + 第 27 段提到 gpt-oss 显式训练在 harmony response format 上,channel / <|constrain|>json marker 是 prompt 的一部分,inference stack 可以直接切到 JSON-constrained sampling,根本不需要靠模型"自觉"对 schema。

这个对照非常重要 —— Anthropic 的问题不是"工具调用难",而是"工具调用靠模型自觉"


四、5 个工程上能立刻做的 fallback

我按实现成本从低到高排,5 个全部能照着做:

4.1 在 harness 里开"宽容 schema 解析"(30 行代码)

# /tmp/liberal_edit_tool.py
import json

ALLOWED_KEYS = {"oldText", "newText"}

def liberal_parse_edits(raw_str):
    # 宽容解析:丢掉 schema 外字段,留下核心字段
    parsed = json.loads(raw_str)
    edits = parsed.get("edits", [])
    cleaned = []
    for e in edits:
        cleaned.append({
            "oldText": e.get("oldText") or e.get("old_text"),
            "newText": e.get("newText") or e.get("new_text")
        })
    return cleaned

Armin 第 25 段说 Claude Code 自己就是这么干的 —— 容忍别名 + 丢多余字段。30 行 Python 复制到任意 harness 都能跑。

4.2 在 system prompt 里硬编码 schema 字段名(0 行代码)

HN 评论区 cadamsdotcom 第 7 条(735 字符)说:把 schema 写在 system prompt 里 + 在错误信息里提示 "expected fields are oldText and newText only",模型 retry 时命中率明显提升。这条不需要改 harness,改 prompt 就行。

4.3 改用 curl-style 而不是 JSON-schema-style(HN @socketcluster 路径)

HN @socketcluster(628 字符)说他做 SaaS agent 不走 MCP,直接用 curl 命令塞 skill markdown:

The curl command is extremely popular so models seem to be really good at using it. Also I like that curl uses a bash syntax and my platform requires JSON payloads; it makes the separation clear to the agent.

绕开 JSON schema = 绕开整个 schema 适配问题。这条对国内 agent 工具链启发很大 —— 如果你的工具天然是 HTTP/RPC,不需要 MCP-style schema,直接写 skill markdown 反而更稳。

4.4 用 Hook 在 schema 校验前过滤(HN @pugio 路径)

HN @pugio(632 字符)做了一个 Pi extension,在 edit tool 入口 patch 一下、自愈掉常见的失败 case:

Built an extension which patches the edit tool to self-heal on the majority of those kinds of calls. It's not 100%, but it cuts down on the rejections quite a bit and saves a few round trips.

对应第 4.1 的 30 行代码可以打包成 hook,在 model 把 tool call 发出来之后、schema validation 之前做一次过滤。

4.5 评估要不要切换到 OpenAI / gpt-oss

第 34 段说 Codex 模型没这个问题。如果你的工具链依赖复杂 schema、容错成本太高,直接评估切到 Codex / GPT-OSS 是合理的。这不是技术倒退 —— gpt-oss 显式 harmony 格式反而更稳。


五、跟 Claude Code 自身的对照(为什么他们不踩这个坑)

Armin 第 18 段直接扒了 Claude Code 内部:

Claude Code's own tools are comparatively flat. The ordinary edit tool is not Pi's nested edits[] shape; it is closer to file_path, old_string, new_string, and an optional flag (replace_all).

Anthropic 自家的 schema 是扁平的 + 严格模式的复杂度上限内,所以模型适配它没问题。一旦你用嵌套 schema(edits[] 嵌套对象),模型的"扁平滑性 prior"就崩了。

这条对工具设计有直接启示:不要做嵌套 schema,能做平就做平edits[] 这种数组里嵌套对象的形态,本身就是 schema 适配的地狱。


六、目前还没完全搞清楚的几个点(局限与待验证项)

  • 不同 model 系列(不只是 Claude)的 schema 漂移幅度对比(待验证) —— Armin 只测了 Anthropic + 部分 Codex,GLM-5.2 / Kimi K2.6 / Qwen3-Coder 这些都没纳入对比。我自己也没条件跑所有 9 家(待验证)。
  • strict mode 的复杂度阈值官方没文档化(不足) —— 我只跑了 4 层嵌套的 schema 拒,具体是 3 层还是 5 层拒绝,Anthropic 没给硬阈值,只能二分搜索(不足)。
  • 强化学习阶段具体是哪个 harness 训练数据被泄出来(坑点) —— Armin 第 22 段也只是推断"the ecology is not documented",Anthropic 没公布 RL env 细节,这是个黑盒(坑点)。
  • Claude Code 内部 retry 次数上限是多少(待验证) —— 第 24 段提到有 state machine 重试 bad call,但具体重试几次 fallback 到 fallback schema,没看到数字(待验证)。
  • gpt-oss harmony 格式对非 OpenAI 模型的迁移性(还在调研) —— 第 30 段说 hosted GPT 可以加 LARK grammar,但开源模型怎么用 harmony 还得查(还在调研)。
  • 跨模型的 tool-call 平均成功率基线(不足) —— HN @bazodedo(729 字符)说他用 GLM 时 95% 成功率,但这是单一用户单一负载,不能拿来当跨模型对比基线(不足)。

七、适用场景建议

你的工具形态 建议方案 风险
简单 schema(扁平,≤ 3 字段) Anthropic 当前模型 + 4.1 宽容解析
嵌套 schema(edits[] 类) 优先 4.1 + 4.4,否则切 4.5 评估 Codex
HTTP/RPC 类工具 直接走 4.3 curl-skill 模式
严格 schema 强制要求 切 4.5 + 评估 gpt-oss 中(成本变化)
已用 Claude Code 不动 不受影响

八、参考链接

  1. Armin Ronacher 原文(Pitfall #30 个人博客,server-side rendered,完整 44 段):https://lucumr.pocoo.org/2026/7/4/better-models-worse-tools/
  2. HN 主帖(180 分,63 评论):https://news.ycombinator.com/item?id=48788599
  3. HN 评论前 12 长评论(Pitfall #29 len(text) 排序抽出)
  4. Pi issue tracker:Armin 原文第 40 段提的
  5. Anthropic tool use docs(为什么 strict mode 有复杂度上限):https://docs.anthropic.com/en/docs/tool-use
posted @ 2026-07-05 19:09  Ninghg  阅读(53)  评论(0)    收藏  举报