도구 호출과 오픈소스 모델의 M×N 문제
클로즈드 소스 AI 모델에서는 매끄러웠던 도구 호출(Tool calling) 기능이 오픈소스 모델에서는 파편화된 문제를 다룹니다. 각 모델(GPT, DeepSeek, GLM 등)마다 도구 호출을 인코딩하는 형식이 다르기 때문입니다. 다양한 애플리케이션(M)과 모델(N)이 증가함에 따라 포맷 파싱과 문법 적용을 위한 M×N의 개발 부담이 기하급수적으로 커지는 것이 핵심 문제입니다.
클로즈드소스 모델의 도구 호출(Tool calling)은 매끄럽습니다. 함수 목록을 API에 전달하면, 모델이 이를 호출하고 구조화된 JSON을 반환받습니다. 이때 전송 형식(wire format)은 사용자에게 노출되지 않습니다. 하지만 오픈소스 모델로 넘어오면, 도구 호출이 엔진이 이해해야 하는 전송 형식에 의존한다는 사실을 깨닫게 됩니다. 엔진이 특정 모델의 형식을 아직 지원하지 않는다면, 출력이 깨져서 나옵니다. 인수(arguments) 안에 추론 토큰이 섞여 있거나, 잘못된 JSON 형식이거나, 도구 호출 자체가 누락될 수 있습니다. 그러면 누군가 지원할 때까지 기다리거나, 직접 파서(parser)를 작성해야 합니다.
'모델 지원'이 실제로 의미하는 것 모든 모델 패밀리는 도구 호출을 서로 다르게 인코딩합니다. 다음은 함수 search(query="GPU")를 호출하는 동일한 시맨틱 작업이 세 가지 다른 전송 형식으로 표현된 모습입니다:
gpt-oss (Harmony): <|channel|>commentary to=functions.search <|constrain|>json<|message|> {"query": "GPU"} <|call|>
DeepSeek: <|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>search '''json {"query": "GPU"} ''' <|tool▁call▁end|><|tool▁calls▁end|>
GLM5: ünserschied search unserschied query unserschied GPU unserschied /search unserschied (역주: 원문에서 GLM5 포맷은 특수 태그 쌍으로 처리됨)
동일한 작업이지만 호환되지 않는 전송 형식입니다. 토큰 어휘(vocabulary), 경계 마커(boundary markers), 인수 직렬화(serialization) 방식이 모두 다릅니다. 생성된 도구 호출을 깔끔한 JSON 객체 배열로 반환하려면, 모델의 원본 출력을 다시 파싱하여 깔끔한 API 응답으로 만들어야 합니다.
실제로 M개의 애플리케이션(vLLM, SGLang, TensorRT-LLM, transformers 등)은 지원하고자 하는 각 N개의 모델에 대해 커스텀 파서를 작성해야 합니다. 그리고 이것은 구현 부담의 절반에 불과합니다.
문제의 전개 속도 Gemma 4는 이와 관련된 어려움을 잘 보여줍니다. <|channel> 추론 토큰은 파서가 처리하기 전에 디코더에 의해 제거됩니다(vLLM #38855). 추론 내용이 도구 호출 인수로 새어 나갈 수도 있습니다(vLLM PR #39027). 이 모델의 비표준 형식은 충분히 달랐기 때문에, llama.cpp는 기존의 범용 자동 파서를 포기하고 전용 구현을 만들어야 했습니다(llama.cpp PR #21418). 이는 모델 훈련 시점에 결정된 형식 선택이 파서 버그로 표면화되는 것입니다.
범용 파서의 역부족 자연스러운 대응책은 모든 형식을 처리할 수 있을 만큼 범용적인 파서를 구축하는 것입니다. 모든 엔진이 이를 시도했습니다. '특수 토큰을 찾아 그 사이의 JSON을 추출하라'와 같은 합리적인 휴리스틱은 일부 형식에서는 충분히 잘 작동합니다. 하지만 Harmony는 to= 속성과 함께 <|channel|>을 통해 라우팅하고, GLM5는 아예 JSON 대신 태그 쌍으로 인수를 직렬화합니다.
이것이 근본적인 문제입니다. 전송 형식은 모델 훈련 시점에 결정되는 사항이며, 이를 공통된 규약으로 제한할 어떠한 장치도 없습니다. 가능한 형식의 공간이 무한히 열려 있기 때문에, 범용 파서는 아직 만들어지지도 않은 설계 선택을 예측하려고 시도하는 것과 같습니다. 그래서 범용 파서가 일반적인 경우에는 도움이 되지만, 어려운 버그가 존재하는 '모델별 예외 상황(per-model tail)'을 제거하지 못하는 것입니다. 여기에는 인수로 새어 나가는 추론 토큰, 파서가 처리하기 전에 특수 토큰을 제거해 버리는 디코더, 콘텐츠와 충돌하는 생성 종료 신호 등의 문제가 포함됩니다.
이러한 모델별 형식 지식은 결과를 파싱할 때뿐만 아니라 텍스트 생성 중에도 필요합니다. 바로 이 지점에서 문법 엔진(Grammar engines)이 등장합니다.
부재한 관심사의 분리 새로운 모델이 출시되면 두 곳의 독립적인 곳에서 작업이 이루어집니다. Outlines, XGrammar, 그리고 llama.cpp의 문법 지원과 같은 문법 엔진은 생성 중에 제약을 어디에 적용할지 알아야 합니다. 어떤 토큰이 도구 호출의 봉투(envelope)를 나타내는지, 그 안에서 구조화된 생성을 언제 활성화할지, 그 외부에서 언제 모델을 제약 없이 둘지를 결정해야 합니다.
vLLM, SGLang, TensorRT-LLM, transformers 내부의 출력 파서는 그 반대의 작업을 수행해야 합니다. 원본 생성 텍스트를 가져와 도구 호출을 깔끔한 API 응답으로 추출해야 하며, 이를 위해 동일한 형식 지식을 역방향으로 필요로 합니다.
이들은 서로 다른 팀이고, 다른 코드베이스를 사용하며, 다른 릴리스 주기를 가집니다. 하지만 이들에게 필요한 모델별 지식은 동일합니다. 즉, 어떤 토큰이 경계를 나타내는지, 인수가 어떻게 직렬화되는지, 추론 토큰이 어디에 나타날 수 있는지 등입니다. 오늘날 각 팀은 혼란스러운 상황에서 이를 독립적으로 역엔지니어링하고 있습니다.