메뉴
HN
Hacker News 24일 전

더 똑똑해진 모델, 오히려 툴 호출은 더 불안해졌다

IMP
7/10
핵심 요약

최근 출시된 최고 성능(SOTA)의 AI 모델들이 도구(Tool) 호출 시 스키마에 없는 필드를 지어내어 오류가 발생하는 현상이 늘었습니다. 이는 LLM이 도구를 호출할 때 텍스트 기반의 제한된 규칙을 따르는 과정에서 발생하는 한계로, 실무 에이전트 개발 시 모델 버전 업그레이드가 항상 안정적인 도구 사용으로 이어지지 않음을 시사합니다.

번역된 본문

원문 제목: 더 나은 모델: 더 나쁜 도구 (Better Models: Worse Tools) 출처: Armin Ronacher의 블로그 (2026년 7월 4일)

지난 이틀 동안 아주 이상한 'Pi(Pi)' 관련 이슈 때문에 깊은 토끼굴(rabbit hole)에 빠졌습니다. 결론부터 말씀드리면, 최신 Claude 모델들이 Pi의 편집(edit) 도구를 호출할 때 중첩된 edits[] 배열 안에 추가적이고 허구의 필드(invented fields)를 넣어서 보내는 것입니다. Haiku 같은 소형 모델이 아니라 Opus 4.8에서 이런 일이 일어납니다. 편집 내용 자체는 보통 정확하지만, 모델이 스키마에 없는 키(key)를 만들어내기 때문에 인자가 스키마와 일치하지 않아 Pi가 도구 호출을 거부하고 재시도를 요청하는 상황이 발생합니다.

모델이 가끔 잘못된 형식의 도구 호출을 내보내는 것 자체는 그리 놀라운 일이 아닙니다. 특히 소형 모델들에서 흔히 발생하니까요. 하지만 제가 놀란 이유는 이 문제가 Anthropic의 최신 모델들에서 더 심해진다는 점입니다. 구형 모델에서는 전혀 발생하지 않았지만, Opus 4.8과 Sonnet 5 모두 이러한 현상을 보여줍니다. 즉, 해당 제품군의 최고 수준(SOTA) 모델들이 이 특정 도구 스키마를 다루는 데 있어서는 이전 세대 모델들보다 오히려 더 서툴다는 뜻입니다.

참고로 Fable에 대해 궁금해하실 수 있는데, 이 모델은 의도적으로 테스트하지 않았습니다. Fable이 내부적으로 실행하는 분류기(classifier)가 모를 조용히 Opus로 강등시킬 수도 있을 것 같아서입니다.

도구 호출은 곧 텍스트다 LLM 도구 호출의 내부 동작 방식을 자세히 살펴보지 않으셨다면, 알아두어야 할 핵심은 도구 호출이 마법 같은 것이 아니며 꽤 단순한 대역 내 신호(in-band signalling) 방식을 사용한다는 점입니다. 모델은 대화 내용, 시스템 프롬프트, 사용 가능한 도구 목록을 전달받습니다. 그러면 서버는 이를 특수한 마커 토큰이 포함된 커다란 하나의 프롬프트로 변환합니다. 모델은 이 형식의 예시로 학습되고 강화(reinforce)되었기 때문에, 텍스트 생성 도중 특정 시점에 API나 클라이언트가 "이 인자들로 이 도구를 호출해"라고 해석할 수 있는 결과물을 내보내게 됩니다.

예를 들어 파일 편집 도구의 경우, 의도된 호출 페이로드는 대략 다음과 같을 것입니다: { "path": "some/file.py", "edits": [ { "oldText": "text to replace", "newText": "replacement text" } ] } 그러면 연동 프로그램(harness)이 인자를 검증하고 편집을 수행한 뒤, 그 결과를 다시 모델에 피드백합니다. 검증에 실패하면 모델은 에러를 마주하고 보통 다시 시도하게 됩니다.

Anthropic 모델에서 이 형식이 정확히 어떻게 구성되는지는 알려져 있지 않지만, 일부 사람들이 "ANTML" 마커를 찾아냈으며 이들이 때때로 공개된 대화 내용에 유출되기도 합니다. 제가 알기로 위의 호출은 모델에서 다음과 같이 직렬화(serialized)되어 출력됩니다:

antml:function_calls <antml:invoke name="edit"> <antml:parameter name="path">some/file.py <antml:parameter name="edits">[{ "oldText": "text to replace", "newText": "replacement text" }]

여기서 짚고 넘어가야 할 점은, 이 형태가 XML처럼 보이긴 하지만 실제로는 진짜 XML이 아니라는 것입니다. 이는 단지 토큰화(tokenize)하고 학습시키기에 편리하다고 판단하여 고안된 형태일 뿐입니다. 또 다른 특징은, 단순한 최상위 문자열 매개변수는 인라인으로 표현되지만 객체 배열은 JSON 직렬화를 통해 구현된다는 점입니다. 이것이 정확히 이렇게 동작한다고 확신할 수는 없지만, 몇 가지 정황을 볼 때 크게 다르지는 않을 것입니다. 이 부분은 뒤에서 중요해집니다.

모델이 이런 구조를 생성하도록 만드는 방법에는 크게 두 가지가 있습니다:

  1. 모델에게 스키마와 일치하는 유효한 JSON을 생성하라고 요청한 후, 나중에 검증하는 방법.
  2. 샘플러(sampler)를 제약(constrain)하여, 애초에 잘못된 JSON이나 스키마 형식 위반을 방지하는 방법.

두 번째 접근법은 보통 문법 인식(grammar-aware) 또는 제약 디코딩(constrained decoding)이라고 불립니다. 샘플러는 문법을 위반할 토큰들을 마스킹하여 배제합니다. 만약 모델이 현재 JSON 객체 내부를 생성 중이고 스키마 상으로 oldText와 newText만 허용된다면, 샘플러는 모델이 "in_file"이나 "type" 같은 것을 생성하지 못하도록 막을 수 있습니다. 문법 인식 디코딩은 JSON 구문이 유효하도록 강제하는 데 사용될 수 있을 뿐만 아니라, 특정 enum 값이나 키를 강제하는 데에도 사용될 수 있습니다. 어떠한 형태의 제약도 없다면, 모델은 단순히 학습된 규칙을 따라가려고만 시도할 뿐입니다.

실패의 원인 Pi의 편집 도구는 한 번의 호출로 여러 개의 정확한 문자열 교체를 지원합니다. 그렇기 때문에 인자에 edits 배열이 포함되는 것입니다.

원문 보기
원문 보기 (영어)
Armin Ronacher 's Thoughts and Writings blog archive projects travel talks about Better Models: Worse Tools written on July 04, 2026 A very strange Pi issue sent me down a rabbit hole over the last two days. The short version is that newer Claude models sometimes call Pi's edit tool with extra, invented fields in the nested edits[] array. And not Haiku or some small model: Opus 4.8. The edit itself is usually correct but the arguments do not match the schema as the model invents made-up keys and Pi thus rejects the tool call and asks to try again. That alone is not too surprising as models emit malformed tool calls sometimes. Particularly small ones. What surprised me is that this is getting worse with newer Anthropic models as both Opus 4.8 and Sonnet 5 show it but none of the older models. In other words, the SOTA models of the family are worse at this specific tool schema than their older siblings. In case you are curious about Fable: I intentionally did not test it because I was not sure if the classifiers they are running might downgrade me to Opus silently. Tool Calls Are Text If you have not spent too much time looking at LLM tool calling internals, the important thing to understand is that tool calls are not magic and use some rather crude in-band signalling. The model receives a transcript, a system prompt and a list of available tools. The server munches that into a large prompt with special marker tokens. Because the model was trained and reinforced on examples of that format, at some point during generation it emits something that the API or client interprets as "call this tool with these arguments". For a file edit tool, the intended invocation payload might say something like this: { "path" : "some/file.py" , "edits" : [ { "oldText" : "text to replace" , "newText" : "replacement text" } ] } A harness then validates the arguments, performs the edit, and feeds the result back into the model. If validation fails, the model sees an error and usually tries again. How exactly that formatting happens is not known for the Anthropic models, but some people have gotten out "ANTML" markers and they at times do leak also into public communications. To the best of my knowledge, the call above would come out serialized like this from the model: <antml:function_calls> <antml:invoke name= "edit" > <antml:parameter name= "path" > some/file.py </antml:parameter> <antml:parameter name= "edits" > [ { "oldText": "text to replace", "newText": "replacement text" } ] </antml:parameter> </antml:invoke> </antml:function_calls> An important thing to note here is that this thing, while looking like XML, is not really XML. It's just a thing they found convenient to tokenize and train on. The other thing to note is that a basic top-level string parameter appears in-line whereas an array of objects is implemented via JSON serialization. While I'm not entirely sure that this is how it works, there are some indications that this is not too far off. This will become relevant later. There are two very different ways to make the model produce a structure like this: You can ask the model to produce valid JSON matching a schema and then validate it afterwards. You can constrain the sampler so that invalid JSON, or even invalid schema shapes, cannot be sampled in the first place. The second approach is what people usually refer to as grammar-aware or constrained decoding. The sampler masks out tokens that would violate the grammar. If the model is currently inside a JSON object and the schema says only oldText and newText are allowed, the sampler can prevent it from emitting "in_file" or "type" . Grammar-aware decoding can be used both to constrain something to be syntactically valid JSON and also to enforce specific enum values or keys. Without any form of constraints the model is merely following a learned convention. The Failure Pi's edit tool supports multiple exact string replacements in one call. That is why the arguments contain an edits array. In the failing cases the model produces entries like this: { "oldText" : "..." , "newText" : "..." , "requireUnique" : true } or this: { "oldText" : "..." , "newText" : "..." , "oldText2" : "" , "newText2" : "" } Across repeated trials I saw a whole zoo of invented trailing keys: type , id , kind , unique , requireUnique , matchCase , in_file , forceMatchCount , children , notes , cost , oldText2 , newText2 , oldText_2 , newText_2 , and even an event.0.additionalProperties key inside the edit object itself. The most annoying part is that the actual oldText and newText payloads were byte-correct in the invalid calls I inspected. The model had in fact produced the right invocation but then added nonsense at the end of the object. The failure is also heavily context-dependent. A fresh single-turn prompt like "edit this file" did not reproduce it at all for me. An agentic history where the model had read files, diagnosed a problem and then composed a multi-line edit could reproduce it. And more annoyingly, not all transcripts will show that behavior. In fact, I needed Petr Baudis &#8216;s transcripts to reproduce this for me at all! In that user's session continuing the session caused Opus 4.8 to fail around 20% of the time. Stripping thinking blocks from history reduced the failure rate by half. Turning on strict tool invocation eliminated it in my runs. Why It's Getting Worse My strongest hypothesis is that this is not random deterioration but a training artifact. 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. The model learns what a successful tool call looks like in that environment. It also learns what mistakes are tolerated by that environment. 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 ). Looking at Claude Code's client is very instructive: it contains retry paths for malformed tool use, parameter aliases, type coercions, Unicode repairs and filtering of unknown keys. In other words, Anthropic's own client appears to expect and accept a fair amount of slop and repairs it, mostly silently. If reinforcement learning happens in a harness like that, or a simulation of one, then slightly malformed tool calls can still complete the task and receive reward. The harness fully absorbs the error and there is little gradient against inventing an alias, adding a stray field or using a nearby parameter name. Worse, the model may become very strongly adapted to the canonical Claude Code edit tool shape. A different harness can present a tool with the same semantic intent but a different schema. Such a tool can increasingly be off-distribution. The better-trained model might actually fight you harder because its prior is stronger. This is not too surprising, but it is a change from how this was a few months ago. When Opus 4.5 launched, it adapted to other edit tools exceptionally well. In fact, I was pretty convinced that we're on a good path where the models are more likely to adapt to any sort of tool shape that comes around for as long as the instructions are good. Now I'm somewhat worried about the track we're on here. Alternative tool schemas might not just be unfamiliar. They might be implicitly punished by post-training that optimizes for one particular, forgiving tool ecology. And that ecology is not documented. While there is a text editor tool that is documented, you will see that this format is in fact not followed by Claude Code. What Claude Code does internally (which is a closed-source harness) is hidden from you. The Slop Harness Claude Code is obviously closed-source but we can look a