LLM WikiAccess-protected knowledge portal
← 스터디 홈
80편 · 약 18분

XGrammar-2: 에이전트 LLM의 동적 구조화 생성을 6배 빠르게 만드는 TagDispatch와 Cross-Grammar Cache

구조화 생성이 에이전트 환경에서 무너지는 이유

LLM이 JSON이나 함수 호출 결과를 반환하도록 제어하는 구조화 생성(structured generation)은 제약 디코딩(constrained decoding)으로 구현된다. 컨텍스트 자유 문법(Context-Free Grammar, CFG)을 EBNF 형식으로 정의하고, 각 토큰이 생성될 때 문법에 어긋나는 토큰을 걸러낸다.

단일 도구 호출, 단일 JSON 출력 — 정적 구조라면 잘 작동한다. 그런데 에이전트는 다르다.

에이전트가 여러 도구를 순차 호출하는 상황을 생각해 보자. 첫 번째 도구 호출의 결과를 보고 두 번째 도구를 선택하는 경우, 어떤 도구를 호출할지는 이전 출력이 결정한다. 이것을 인트라-요청 동적성(intra-request dynamism)이라고 부른다. 단순히 서로 다른 요청마다 다른 스키마가 적용되는 것이 아니라, 하나의 요청 안에서 구조가 도중에 바뀐다.

기존 구조화 생성 엔진의 두 가지 한계:

한계설명
인트라-요청 동적성 처리 불가하나의 요청 내에서 구조를 전환하려면 별도 로직이 필요. EBNF로 표현하면 매우 복잡해짐
그래마 간 재사용 없음100개의 요청이 모두 약간 다른 스키마를 쓰면 100번 컴파일. 공유 서브구조를 활용하지 못함

2026년 1월, MLC(Machine Learning Compilation) 팀은 이 두 문제를 정면으로 다룬 XGrammar-2를 발표했다. 같은 해 5월 ACM CAIS 2026 컨퍼런스에서 정식 발표됐다.


기존 구조화 생성 엔진의 작동 방식

XGrammar-2가 무엇을 개선했는지 이해하려면, 먼저 기존 엔진이 어떻게 동작하는지 알아야 한다.

토큰 마스크 캐시

LLM은 각 스텝마다 어휘(vocabulary) 전체에 대한 로짓(logit)을 계산한다. GPT-4 계열 모델의 어휘 크기는 수만 개에 달한다. 구조화 생성은 그중에서 현재 문법 상태에서 합법적인 토큰들만 남기고 나머지를 마스킹(-∞)한다.

어휘 전체(예: 128,000 토큰) 
     ↓
현재 문법 상태에 맞는 토큰 집합 계산
     ↓
토큰 마스크 생성 (0/1 비트맵)
     ↓
로짓에 적용 → 샘플링

이 마스크를 매 스텝 계산하면 추론 속도가 크게 저하된다. 기존 XGrammar(v1)는 마스크를 미리 컴파일해 캐시에 저장해 이 문제를 줄였다. 그러나 에이전트 환경에서는 어떤 구조가 나올지 사전에 알 수 없어 컴파일 비용이 폭증한다.


XGrammar-2의 두 핵심 아이디어

1. TagDispatch: 인트라-요청 동적성 처리

TagDispatch는 EBNF에 새로운 내장 문법 구조(intrinsic)를 추가한다. 하나의 요청 안에서 태그를 기반으로 자유 텍스트 모드와 구조화 모드 사이를 전환한다.

Dispatching 모드
자유 텍스트 생성 중
태그 감지
예: <tool:search>
Dispatched 모드
search 스키마 강제 적용
↓ (호출 완료 후)
Dispatching 모드
다시 자유 텍스트
태그 감지
예: <tool:calculator>
Dispatched 모드
calculator 스키마 강제 적용
TagDispatch 동작 흐름

TagDispatch는 (tag, grammar) 쌍의 목록과 종료 문자열 집합으로 파라미터화된다. 디코딩은 dispatching 모드로 시작한다. 어떤 태그가 일치하면 해당 서브 그래마로 전환되어 dispatched 모드에서 제약 생성이 이루어진다. 종료 문자열이 나오면 다시 dispatching 모드로 돌아간다.

이 구조를 EBNF로 직접 표현하는 것과 무엇이 다른가? EBNF로 표현하면 "임의의 비태그 텍스트를 허용하면서 여러 태그를 인식하고 각 태그를 다른 서브 그래마로 라우팅"하는 단일 복잡한 문법이 필요하다. 이 문법은 컴파일이 어렵고 상태 폭발(state explosion)을 일으킨다. TagDispatch는 이 패턴을 엔진 수준에서 직접 지원해 컴파일 복잡도를 줄인다.

2. Cross-Grammar Cache: 인터-요청 서브구조 재사용

에이전트 서버에는 수백 가지 도구가 등록된다. 각 요청마다 사용 가능한 도구 목록이 다르지만, 서로 다른 그래마들 사이에는 공통 서브구조가 많다.

예를 들어 HTTP API를 호출하는 도구들은 모두 headers 객체와 url 문자열 필드를 갖는다. 이 서브구조의 토큰 마스크는 어떤 도구 그래마에서든 동일하다.

Cross-Grammar Cache는 서브구조 수준에서 마스크를 캐시한다. 새로운 그래마가 컴파일될 때 이미 캐시된 서브구조가 있으면 재사용한다.

요청 A 그래마
headers: Object
url: String
tool_a_specific: String
요청 B 그래마
headers: Object
url: String
tool_b_specific: Integer
↓ 공통 서브구조 식별
Cross-Grammar Cache
headers:Object 마스크, url:String 마스크 — 한 번만 컴파일, 양쪽 재사용
Cross-Grammar Cache 동작

이 기법은 그래마 간 재사용만이 아니라 단일 그래마 안에서 반복되는 서브구조에도 적용된다. 예를 들어 배열 원소 타입이 반복되면 해당 원소 마스크를 한 번만 컴파일하고 재사용한다.


세 가지 추가 최적화

JIT(Just-In-Time) 컴파일

기존 방식은 요청이 오기 전에 스키마 전체를 사전 컴파일한다. XGrammar-2는 실제로 사용되는 상태만 그때그때 컴파일한다. 에이전트 출력에서 대부분의 분기는 실행되지 않는다. JIT는 실행되지 않는 분기의 컴파일 비용을 없앤다.

Ablation 결과: JIT만으로 전처리 시간이 8.1배 단축됐다.

Earley 기반 적응형 토큰 마스크 캐시

XGrammar-2는 Earley 파싱 알고리즘을 기반으로 마스크 캐시를 구성한다. 정규 토큰 마스크 경계(컨텍스트 의존/독립 경계)를 더 정교하게 계산해, 컨텍스트 의존 토큰 처리 비용을 줄인다.

반복 상태 압축(Repetition State Compression)

JSON 배열처럼 동일한 패턴이 수백 번 반복되면 상태 공간이 선형으로 증가한다. 반복 상태 압축은 반복 구조를 접혀(folded) 처리한다.

Ablation 결과: 롱테일 케이스(복잡한 반복 문법)에서 전처리 시간 99.6배 단축.


성능 수치

Ablation 종합:

최적화전처리 시간 단축마스크 생성 단축
JIT 컴파일8.1×
Cross-Grammar Cache2.2×
반복 상태 압축 (롱테일)99.6×2.6×

전체 효과: XGrammar-2는 기존 최신 구조화 생성 엔진(XGrammar-1 포함) 대비 도구 호출 컴파일 속도 6배 이상 개선, 엔드-투-엔드 추론 지연에 거의 제로(near-zero) 오버헤드.


서빙 프레임워크 통합

XGrammar-2는 vLLM, SGLang, TRT-LLM 등 주요 LLM 서빙 프레임워크에 통합됐다. 인터페이스는 간단하다. 마스크 모듈이 로짓을 가로채고, dispatching 상태에 따라 적절한 마스크를 적용한 뒤 샘플러로 돌려보낸다:

# XGrammar-2 마스크 콜백 인터페이스
def mask_callback(logits, grammar_state):
    mask = grammar_state.get_token_mask()
    return logits + mask  # -inf 마스킹

TagDispatch를 사용하는 에이전트 설정 예:

grammar = xgrammar.TagDispatch([
    ("<tool:search>", search_schema),
    ("<tool:calculator>", calculator_schema),
    ("<tool:write_file>", write_file_schema),
], stop_strings=["</tool>"])

이 문법 하나로 요청 내에서 세 가지 도구 호출을 자유롭게 전환할 수 있다.


에이전트 추론 파이프라인에서의 위치

에이전트 시스템 프롬프트 + 도구 목록
LLM 포워드 패스 → 로짓 생성
XGrammar-2 마스크 적용
TagDispatch 모드 확인 → 적절한 마스크 로드 또는 Cross-Grammar Cache 히트
구조적으로 유효한 토큰 샘플링
자유 텍스트
마스크 없음
도구 호출 JSON
TagDispatch 마스크
다음 도구 인자
Cross-Grammar 재사용
XGrammar-2가 에이전트 추론에서 위치하는 곳

중요한 포인트: 구조화 생성은 출력의 형식적 정확성을 보장하지만 의미론적 정확성은 보장하지 않는다. LLM이 search 도구를 호출하는 JSON을 완벽히 유효하게 생성해도, 그 검색 질의가 의도에 맞는지는 별개의 문제다.


운영 고려사항

캐시 관리: Cross-Grammar Cache는 메모리를 소비한다. 도구 수가 수천 개에 달하는 대규모 에이전트 시스템에서는 캐시 크기를 제한하고 LRU 정책을 적용해야 한다.

스키마 버전 관리: 도구 스키마가 변경되면 해당 서브구조의 캐시가 무효화된다. 빈번한 스키마 변경은 캐시 효율을 떨어뜨린다.

fallback 처리: TagDispatch가 알려지지 않은 태그를 만나면 어떻게 할지 명시해야 한다. 에러를 발생시킬지, 자유 텍스트 모드를 유지할지는 시스템 설계 결정이다.

XGrammar-1 → 2 마이그레이션: XGrammar-2는 XGrammar-1의 API와 하위 호환된다. 단순 JSON 스키마 제약만 쓰는 경우 코드 변경 없이 성능 향상을 얻을 수 있다. TagDispatch는 에이전트 도구 호출처럼 동적 구조가 필요한 경우에 추가로 도입한다.


한계와 Open Question

복잡한 CFG와 컴파일 비용: TagDispatch와 Cross-Grammar Cache가 평균 케이스를 크게 개선했지만, 매우 복잡한 재귀 문법이나 순환 구조는 여전히 컴파일 비용이 클 수 있다. (반복 상태 압축이 이를 일부 완화하지만 완전 해결은 아님)

의미론적 정확성: 구조적으로 유효한 출력이 의미상 올바른 도구 인자를 담는다는 보장이 없다. 구조화 생성은 파싱 실패를 줄이지만 환각(hallucination)을 없애지 않는다.

토크나이저 의존성: 토큰 마스크는 특정 토크나이저에 의존한다. 모델이 바뀌면 캐시를 재구성해야 한다.

멀티모달 출력: 텍스트+코드+구조화 데이터가 섞인 멀티모달 에이전트 출력에서 TagDispatch의 적용 범위가 어디까지인지는 Open question으로 남아 있다.


정리

에이전트 LLM에서 구조화 생성의 문제는 단순히 "JSON을 강제한다"는 수준을 넘어선다. 하나의 요청 안에서 구조가 바뀌고, 수백 가지 도구가 각기 다른 스키마를 요구하며, 이를 낮은 지연으로 처리해야 한다.

XGrammar-2는 두 가지 핵심 아이디어로 이를 다룬다. TagDispatch는 단일 요청 안에서 자유 텍스트와 구조화 모드 전환을 문법 수준에서 표현한다. Cross-Grammar Cache는 그래마들 사이의 공통 서브구조를 캐시해 반복 컴파일 비용을 없앤다. JIT 컴파일과 반복 상태 압축이 이를 보완한다.

결과: 도구 호출 컴파일 속도 6배 이상, 엔드-투-엔드 오버헤드 거의 제로.

에이전트 LLM 서버를 운영한다면, XGrammar-2의 vLLM 및 SGLang 통합이 이미 기본 경로로 들어와 있다. 추가 설정 없이 혜택을 받거나, TagDispatch API를 직접 사용해 복잡한 멀티-도구 에이전트를 더 안정적으로 만들 수 있다.


References