저는 open-weight 모델을 사용할 때, 품질 벤치마크가 나타내는 수준에 전혀 미치지 못하는 결과를 보게 됩니다.
도구 호출(tool call) 정확도는 open-weight 모델을 서비스하는 데 중요한 부분입니다. 도구 호출이 정확하지 않으면, 모델은 무의미한 컨텍스트를 쌓게 됩니다. 이는 낮은 품질의 결과로 이어지며, 특히 다중 턴 세션이 흔한 에이전트 워크로드(agentic workloads)에서 두드러집니다.
안타깝게도 많은 제공업체와 널리 사용되는 추론 엔진들은 도구 호출 정확도 테스트를 완전히 통과하지 못합니다. 예를 들어, 저희가 vLLM 버전 0.28로 Kimi K2.6을 서비스했을 때, 예상 도구 호출 스키마(tool call schema)는 92%의 경우에만 얻을 수 있었습니다. 즉, 시도된 25번의 도구 호출 중 2번은 실패한 것입니다.
다행히 vLLM이 최근 constrained decoding(제약 디코딩) 지원 기능을 추가했습니다. 이 기술은 스키마 준수(schema-adherence)를 보장하는 방법입니다. 이 기능은 이후 vLLM 0.31에서 서버 측 플래그로 출시되었습니다. 이를 활성화함으로써, 최신 Kimi 및 DeepSeek 모델을 사용하여 100%의 스키마 정확도를 얻을 수 있습니다.
본 블로그 게시물에서는 먼저 도구 호출 스키마 정확도와 이것이 에이전트 워크로드 서비스를 하는 데 왜 중요한지 설명합니다. 그런 다음 제약 디코딩(constrained decoding)에 대해 논하고, 이것이 어떻게 스키마 정확도 문제를 해결하는지 이야기하겠습니다.
LLM에게 외부 도구 호출을 가능하게 하려면 두 단계를 거쳐야 합니다: (1) AI 모델에게 외부에 사용 가능한 도구 호출의 존재를 알리고, (2) 구조화된 도구 호출을 출력하는 방법에 대한 지침을 제공하는 것입니다. 예를 들어, OpenCode가 Kimi에 연결될 때, OpenCode는 사용 가능한 도구를 설명하는 JSON 배열을 Kimi에게 보냅니다. 이 목록 중 하나는 웹 페이지 내용을 가져오는 webfetch입니다:
[
{
"type": "function",
"function": {
...
}
]
이후 추론 엔진은 LLM이 도구 선언을 인식하도록 학습된 방식과 일치시키기 위해 이 JSON 설명을 LLM 전용 토큰으로 감싸서 LLM에 전달합니다. Kimi를 서비스할 때의 모습은 다음과 같습니다:
<|im_system|>tool_declare<|im_middle|>[{"type":"function","function":{...}, ...]<|im_end|>
JSON 객체는 도구(또는 위의 JSON에서 '함수')를 설명하며, 도구의 매개변수, 그 유형, 필수 매개변수 여부 등을 포함합니다. 이것을 도구의 *스키마 (schema)*라고 합니다. 에이전트 하네스(agent harness)가 LLM에 의해 생성된 도구 호출을 성공적으로 실행하려면, 해당 호출은 스키마에 명시된 규칙을 따라야 합니다.
그런 다음, 사용자가 에이전트 하네스를 통해 웹 페이지를 가져오도록 요청할 때, LLM은 다음과 같은 토큰을 출력할 수 있습니다:
I'll fetch that page. <|tool_calls_section_begin|><|tool_call_begin|>functions.webfetch:0<|tool_call_argument_begin|>{"url": "https://ubicloud.com", "format": "markdown"}<|tool_call_end|><|tool_calls_section_end|>
이 토큰들은 추론 서버의 파서(parser)에 의해 구문 분석되어, LLM 전용 토큰을 추출하고 호출을 다음과 같은 JSON 객체로 변환합니다:
{
"role": "assistant",
"content": "I'll fetch that page.",
...
}
이것은 다시 하네스(예시에서 OpenCode)로 전송되며, 하네스는 웹fetch 도구를 실행하고 그 결과를 모델에게 되돌려 보냅니다.
도구 호출이 성공하려면, 해당 호출은 도구의 스키마를 준수해야 합니다.
도구 호출에 대한 간단한 접근 방식은 모델이 자유롭게 도구 호출을 생성하도록 허용하고, 모델의 훈련만으로도 올바른 호출을 생성하는 데 충분하다고 신뢰하는 것입니다. 이는 우리가 모델이 구문적으로 정확한 산문을 생성할 것이라고 신뢰하는 것과 유사합니다.
간단한 접근 방식은 현대 모델들이 구조화된 패턴을 따르는 데 능숙하기 때문에 대부분의 경우 작동합니다. 그러나 "대부분의 경우" 성공한다는 것은 여전히 많은 도구 호출이 실패할 수 있음을 의미하며, 이는 오류 메시지로 인한 컨텍스트 윈도우 오염(context window pollution), 낮은 모델 성능, 그리고 에이전트가 호출을 한 번 이상 재시도해야 하므로 증가된 비용을 초래합니다.
Kimi를 사용할 때 이러한 경험을 피하기 위해, Moonshot AI는 K2VV ToolCall 벤치마크를 발표했습니다. 이 벤치마크는 제공업체가 기본적으로 유효한 도구 호출만 생성하는 원래의 동작을 포함하여, 자신들이 Kimi를 동일한 품질로 서비스하고 있는지 검증할 수 있게 합니다.
K2VV ToolCall의 작업 부하는 LLM이 도구 호출을 생성할 때와, 생성된 도구 호출이 구문적으로 정확한지(즉, 제공된 스키마를 준수하는지)를 평가하기 위해 선택된 2000개의 요청으로 구성됩니다. 이 벤치마크는 두 가지 지표인 *트리거 유사성(trigger similarity)*과 *스키마 정확도(schema accuracy)*를 정량화하여 모델의 품질을 분석할 수 있도록 합니다. 트리거 유사성은 서비스가 도구 호출을 실행하기로 결정하는 시점과, 그 결정이 Moonshot AI 자체 서비스의 결정과 얼마나 유사한지를 고려합니다. 스키마 정확도는 도구의 스키마를 준수하며 성공적으로 호출될 수 있는 도구 호출의 비율을 정량화합니다. 예를 들어, 도구 호출 정확도가 50%라면, 모델이 생성한 도구 호출 중 절반은 잘못된 형식(malformed)입니다.
스키마 정확도는 일반적으로 언어 모델의 확률적 특성 때문에 낮은 경우가 많습니다. 모델은 가능도(likelihood)를 기반으로 토큰을 생성합니다. 이는 컴파일러나 타입 체커를 실행하는 것이 아닙니다. 명확한 프롬프트가 주어지더라도 여전히 잘못된 형식의 도구 호출을 생성할 수 있습니다. 이는 예를 들어, 모델의 학습 데이터와 현재 추론 하네스(inference harness) 간에 불일치가 발생했기 때문일 수 있습니다. 모델들은 자체적인 도구, 스키마 및 규칙을 가진 많은 도구 호출 데이터셋으로 훈련됩니다. 그 결과, 모델은 현재 프로덕션 환경에는 존재하지 않더라도 학습 과정에서 본 도구나 필드 이름을 생성할 수 있습니다. 예를 들어, 사용 가능한 도구가 web_search일 때 search_web을 생성하거나, 활성 스키마가 location을 기대할 때 city_name을 사용할 수 있습니다.
LLM은 시퀀스에서 다음 토큰이 될 확률을 각 토큰에 대해 계산합니다. 그런 다음 가중 샘플링(weighted sample)을 수행하여 다음 토큰을 선택합니다. 제약 디코딩(Constrained decoding)은 모델이 구문적으로 유효하지 않은 도구 호출을 초래하는 토큰을 샘플링하는 것을 방지함으로써 잘못된 형식의 도구 호출을 방지합니다. 제약 디코딩을 사용할 때는, 모델이 자유롭게 토큰을 샘플링하고 나중에 결과를 검증하는 대신, 추론 엔진이 단계별로 생성을 안내합니다. 각 단계에서 유효한 토큰만 허용됩니다.
허용되는 토큰은 제공된 도구 스키마(tool schema)에 의해 결정됩니다. vLLM은 XGrammar 라이브러리의 *구조적 태그(structural tags)*를 사용하여 이를 구현합니다. 아래 코드 조각은 Kimi K2.6에서 XGrammar가 어떻게 제약 디코딩(constrained decoding)을 구현하는지 발췌한 것입니다. 이는 호출 시작 지점, 인덱스가 나타나는 위치, 인수가 시작되는 위치, 그리고 스키마 제약 JSON이 생성되어야 하는 위치를 포함하여 도구 호출의 유효 구조를 정의합니다.
# Kimi K2.6 tool call syntax:
# <|tool_call_begin|>{function.name}:{index}
# <|tool_call_argument_begin|>{arguments}
...
전체 구현은 여기에서 확인할 수 있습니다.
이 정의를 통해 생성 과정은 유효한 경로를 따라야 합니다:
도구 호출 시작 → 도구 이름 → 도구 호출 인덱스 → 인수 시작 → 스키마 제약 JSON → 도구 호출 종료
유효하지 않은 경로는 생성 중에 차단됩니다. 예를 들어, 생성이 도구 호출 인덱스 단계에 도달하면, 엔진은 샘플링을 정수 토큰(integer tokens)으로 제한하고 나머지 모든 것을 마스크 처리합니다. XGrammar의 논문에서 재현된 아래 그림은 유효하지 않은 토큰이 어떻게 마스크되는지 좋은 시각화를 제공합니다.
출처: Dong, Yixin 외. "XGrammar: 대규모 언어 모델을 위한 유연하고 효율적인 구조화된 생성 엔진(Flexible and efficient structured generation engine for large language models)." Proceedings of Machine Learning and Systems 7 (2025). [link]
이는 스키마 정확도를 최선의 노력(best-effort) 접근 방식에서 런타임 보장(runtime guarantee)으로 변화시킵니다. 모델은 여전히 어떤 토큰을 생성할지 결정하지만, 이제 그 토큰은 제한적이고 구문적으로 올바른 집합에서 선택됩니다.
주목할 만한 점은, 이러한 런타임 강제 적용이 무시할 수 있는 성능 오버헤드(negligible performance overhead)만 추가한다는 것입니다. 각 단계에 대한 토큰 마스크를 미리 계산하고 생성 중에 GPU에서 효율적으로 적용함으로써, XGrammar는 상당한 성능 저하 없이 정확한 도구 호출을 보장합니다.
Moonshot AI는 제약 디코딩(constrained decoding)을 활성화할 것을 권장하며 자체 API도 그렇게 작동합니다. 따라서 저희는 vLLM의 포크 버전에서 처음에 제약 디코딩을 추가했습니다. 이후 vLLM이 v0.25 버전에서 이 기능을 추가했을 때, 저희는 Kimi와 DeepSeek에 대해 상위(upstream) 버전을 사용하는 것으로 전환했습니다. 불행하게도 얼마 지나지 않아 도구 호출 정확도 점수가 91%까지 떨어지는 것을 발견했습니다!
문제 분석 후, vLLM의 제약 디코딩 구현 자체는 여전히 올바르다는 것을 확인했습니다. 대신, 저희는 vLLM이 제약 디코딩을 명시적으로 활성화하지 않은 요청에 대한 기본 동작 방식을 변경했다는 것을 발견했습니다. 이는 주로 vLLM이 광범위한 모델을 지원하도록 설계되었기 때문입니다. 제약 디코딩을 지원하는 것은 모델별(per-model) 작업이며, 문제는 세 가지 유형으로 나뉩니다:
[생략된 목록/다이어그램 내용]
이러한 문제들 때문에 vLLM 유지 관리자들은 제약 디코딩이 활성화된 후 여러 모델에서 문제가 발생한다는 보고를 받았습니다. 이로 인해 vLLM과 같은 모델에 구애받지 않는 서빙 엔진(model-agnostic serving engine)에게는 기본적으로 제약 디코딩을 비활성화하는 것이 더 안전한 옵션이 되었습니다. 나중에 vLLM은 사용자들이 배포 환경에서 도구 호출의 엄격성(tool-call strictness)을 활성화할 수 있도록 --tool-strict-level 플래그를 추가했습니다.
하지만 Ubicloud의 초점은 vLLM과는 다릅니다. 저희는 사용자들이 퍼스트 파티 제공업체로부터 기대하는 품질로 Kimi와 DeepSeek 모델을 서비스하는 데 중점을 둡니다. 이러한 이유로, 저희는 제약 디코딩을 활성화하고 K2VV ToolCall 벤치마크를 다시 실행했습니다. 모든 서비스된 모델 버전에서 100%의 도구 호출 스키마 정확도를 검증한 후, 이 변경 사항을 프로덕션에 배포했습니다. 이후 저희는 이러한 학습 내용을 공유하기 위해 이 블로그 게시물을 작성하게 되었습니다.
LLM을 사용할 때 관찰되는 품질은 모델 자체에만 기반하는 것이 아닙니다. 추론 엔진(inference engine) 또한 중요한 역할을 합니다. 추론 엔진과 그 설정을 변경하면 모델의 출력 품질이 달라집니다.
Kimi K2.6과 DeepSeek V4 Flash를 서비스할 때, 도구 호출(tool-call) 스키마의 부정확성으로 인해 눈에 띄는 품질 저하가 관찰되었습니다. 저희는 이 문제를 해결하기 위해 먼저 자체 vLLM 포크(fork)를 만들고, 이후 vLLM에서 새로운 제약 디코딩(constrained decoding) 기능을 활성화했습니다. 이를 통해 K2VV ToolCall 벤치마크에서 도구 호출 정확도를 92%에서 100%로 향상시키는 데 도움이 되었습니다.
본 블로그 게시물에서 저희는 도구 호출 및 제약 디코딩에 대한 경험을 공유했습니다. 이것이 흥미롭거나 모델 서비스와 관련하여 실제 사례(war stories)를 공유하고 싶으시다면, 저희 지원 이메일로 연락 주시기 바랍니다.