구조화 출력과 제약 디코딩: XGrammar-2로 LLM 응답을 JSON Schema에 신뢰 가능하게 고정하는 원리
LLM 출력이 프로덕션에서 깨지는 이유
LLM은 자유 형식 텍스트를 잘 생성한다. 문제는 "JSON을 출력해"라고 지시해도 괄호가 빠지거나, 키 이름이 바뀌거나, 값 타입이 틀리는 일이 실제 서비스에서 꾸준히 발생한다는 것이다.
# 운영에서 실제로 발생하는 파싱 실패 패턴
response = llm.generate("사용자 데이터를 JSON으로 반환해")
# 출력 예:
# {"name": "김민준", age: 28, "email" "[email protected]"}
# ↑ age 따옴표 없음 ↑ 콜론 빠짐
json.loads(response) # JSONDecodeError프롬프트 엔지니어링으로 어느 정도 완화할 수 있지만, 재시도 로직, 포맷 검증, 예외 처리를 모두 직접 구현해야 한다. 에이전트 파이프라인에서는 이 실패가 연쇄 오류로 번진다.
제약 디코딩(constrained decoding)은 다른 접근이다. LLM이 토큰을 고를 때마다 현재 문법 상태에서 허용된 토큰만 선택 가능하도록 어휘 마스크를 씌운다. 프롬프트를 바꾸는 게 아니라 디코딩 과정 자체를 제어한다.
토큰 마스킹: 제약 디코딩의 작동 원리
디코딩의 각 스텝에서 LLM은 어휘 전체(보통 32K~128K 토큰)에 대한 로짓을 계산한다. 제약 디코딩은 이 로짓에 이진 마스크를 씌운다.
스텝 t에서의 제약 디코딩:
로짓 벡터 마스크 마스킹된 로짓
[0.8, 0.3, ...] × [1, 0, ...] = [0.8, -inf, ...]
↑ 허용 토큰 ↑ 금지 토큰 ↑ softmax 후 확률=0마스크는 문법 오토마톤의 현재 상태에서 결정된다. JSON Schema를 예로 들면:
- 현재 상태가 "객체 키를 기다리는 중"이면
"로 시작하는 토큰만 허용 - 키가 완성된 후에는
:를 허용 - 값 타입이
integer로 지정됐으면 숫자 토큰만 허용
이 마스크를 어떻게 빠르게 계산하느냐가 구현체들 간의 핵심 차이다.
XGrammar: 어휘를 두 범주로 나눈다
2024년 발표된 XGrammar(arXiv:2411.15100)는 이 문제의 병목을 분석해 어휘 토큰을 두 범주로 구분했다.
| 범주 | 설명 | 처리 시점 |
|---|---|---|
| 컨텍스트 독립 토큰 | 문법 어디서든 항상 허용되거나 항상 금지 | 오프라인 사전 계산 |
| 컨텍스트 의존 토큰 | 현재 문법 상태에 따라 허용 여부가 달라짐 | 런타임 계산 |
대부분의 토큰은 컨텍스트 독립적이다. JSON 문법 기준으로 "hello" 같은 일반 문자열 토큰은 문자열 값 위치에서 언제나 허용된다. 반면 } 나 , 같은 구조 토큰은 현재 파싱 깊이와 상태에 따라 달라진다.
이 분리 덕분에 XGrammar는 대부분의 마스크 계산을 모델 로드 시점에 끝내고, 런타임에는 소수의 컨텍스트 의존 토큰만 처리한다. 결과는 토큰당 40μs 미만의 오버헤드다. 2026년 3월 기준으로 vLLM, SGLang, TensorRT-LLM 세 주요 서빙 프레임워크의 기본 구조화 출력 백엔드가 됐다.
XGrammar-2: 에이전트 워크로드의 새 문제
에이전트가 본격적으로 배포되면서 기존 XGrammar가 다루지 못하는 패턴이 드러났다(arXiv:2601.04426, ACM CAIS 2026).
문제 1: 구조가 중간에 바뀐다
에이전트 출력은 단일 스키마가 아니다. <think> 블록은 자유 텍스트, <tool_call> 블록은 JSON, <result> 블록은 또 다른 포맷일 수 있다. 토큰 하나를 볼 때마다 어떤 문법을 적용할지 달라진다.
문제 2: 요청마다 스키마가 다르다
사용자 A는 주문 스키마, 사용자 B는 계정 스키마를 요청한다. 각 스키마를 처음부터 컴파일하면 지연이 쌓인다.
XGrammar-2는 이 두 문제를 각각 TagDispatch와 Cross-Grammar Cache로 해결한다.
<think>자유 텍스트</think>
<tool_call>JSON</tool_call>
문법
무제한
문법
제약 마스크
{ orderId: str, items: [...] }
{ userId: str, email: str }
string 타입 마스크
integer 타입 마스크
array 패턴 마스크
런타임 near-zero 오버헤드
추가로 XGrammar-2는 Earley 알고리즘 기반의 적응형 토큰 마스크 캐시와 JIT 컴파일을 도입해 컴파일 비용을 선행 지연 없이 분산시킨다.
프로덕션 통합: vLLM, SGLang, API
vLLM
from vllm import LLM, SamplingParams
llm = LLM(model="Qwen/Qwen3-8B")
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"email": {"type": "string"}
},
"required": ["name", "age", "email"]
}
# xgrammar가 기본 백엔드 (2026년 3월 이후)
outputs = llm.generate(
"사용자 프로필을 JSON으로 반환해: 이름 김민준, 28세",
SamplingParams(
temperature=0.0,
guided_decoding={"json": schema}
)
)기본 백엔드 변경: --guided-decoding-backend outlines 로 Outlines 선택 가능.
SGLang
import sglang as sgl
@sgl.function
def extract_profile(s, text):
s += sgl.user(f"{text}")
s += sgl.assistant(
sgl.gen("profile", json_schema=schema, max_tokens=200)
)
# SGLang은 반복 스키마에서 vLLM 대비 약 3× 빠름
# (Grammar 캐시 워밍업 후 기준)OpenAI 호환 API
OpenAI response_format 파라미터는 2024년 8월 Structured Outputs 기능으로 도입됐다.
from openai import OpenAI
client = OpenAI()
completion = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "사용자 프로필 JSON 반환"}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "user_profile",
"schema": schema,
"strict": True # 100% 스키마 준수 보장
}
}
)Anthropic은 tool_choice: {type: "tool", name: "..."} 를 통해 특정 도구 호출 포맷을 강제하는 방식으로 구조화 출력을 지원한다. 직접 JSON Schema 제약 디코딩 API는 2026년 9월 기준 미지원.
백엔드별 성능과 선택 기준
| 백엔드 | 오버헤드 | 장점 | 적합한 경우 |
|---|---|---|---|
| XGrammar (단독) | <40μs/토큰 | 낮은 레이턴시, 생산 안정성 | 단순 JSON/regex 스키마 |
| XGrammar-2 | <40μs/토큰 + 6× 빠른 컴파일 | 동적 스키마 전환, 캐시 재사용 | 에이전트 파이프라인, 다중 스키마 |
| Outlines | 수ms/토큰 | 유연한 정규식, CFG 지원 | 복잡한 문법, 비JSON 포맷 |
| WGRAMMAR | 최대 250× 빠름* | 사전 지식 활용, 정규 연산 | 반복 패턴이 강한 도메인별 포맷 |
\* naive 구현 대비. 프로덕션 XGrammar 대비 절대 수치는 워크로드에 따라 다름.
제약 디코딩을 피해야 할 경우
제약 디코딩이 항상 정답은 아니다.
- 창의적 텍스트 생성: 마스크가 문장 흐름을 제한해 품질이 떨어질 수 있다.
- 매우 복잡한 중첩 스키마: 상태 기계 크기가 기하급수적으로 커질 수 있다.
- 짧은 응답 + 낮은 재시도 비용: 단순 재시도가 더 빠를 수 있다.
- 다국어 혼합 텍스트: 토큰 경계와 문법 경계가 맞지 않아 예상치 못한 제약이 생길 수 있다.
확인 지표: 구조화 출력 도입 후 guided_decoding_latency_ms (vLLM 메트릭)를 모니터링해 오버헤드가 실제로 무시 가능한지 검증한다. 복잡한 스키마에서는 컴파일 비용이 첫 요청 TTFT에 나타날 수 있다.
제약 디코딩이 보장하지 않는 것
중요한 점: 제약 디코딩은 문법적 올바름을 보장하지 의미적 정확성은 보장하지 않는다.
// 이것은 JSON Schema를 통과하지만 의미적으로 잘못됨
{
"name": "김민준",
"age": -999,
"email": "not_an_email"
}age가 음수가 아니어야 한다는 제약(minimum: 0)을 스키마에 포함시키면 마스킹으로 걸러낼 수 있다. 이메일 형식은 정규식 패턴으로 추가 가능하다. 핵심은 비즈니스 규칙을 스키마에 명시적으로 인코딩하는 것이다.
요약
구조화 출력은 프롬프트 지시에서 디코딩 제어로 방법론을 전환한다. XGrammar가 어휘 분리로 오버헤드 문제를 해결했고, XGrammar-2는 에이전트 워크로드의 동적 패턴까지 수용했다. 2026년 현재 vLLM, SGLang, TensorRT-LLM은 모두 XGrammar를 기본 백엔드로 채택했으며, 에이전트 파이프라인에서는 XGrammar-2의 TagDispatch와 Cross-Grammar Cache가 핵심 운영 도구가 되고 있다.
JSON Schema를 제약으로 쓸 때는 비즈니스 규칙(범위, 형식, 열거형)을 스키마에 최대한 인코딩하는 것이 재시도 비용을 줄이는 가장 직접적인 방법이다.
References
- XGrammar 원논문 (arXiv:2411.15100): https://arxiv.org/abs/2411.15100
- XGrammar-2 (arXiv:2601.04426, ACM CAIS 2026): https://arxiv.org/abs/2601.04426
- XGrammar-2 ACM CAIS 2026 발표 페이지: https://www.caisconf.org/program/2026/papers/xgrammar-dynamic-and-efficient-structured-generation-engine-for-agentic-llms/
- XGrammar 공식 문서 (0.2.5): https://xgrammar.mlc.ai/docs/start/constrained_decoding.html
- WGRAMMAR (arXiv:2507.16768): https://arxiv.org/abs/2507.16768
- Pre³: Deterministic PDA for Faster Structured LLM Generation (arXiv:2506.03887): https://arxiv.org/abs/2506.03887
- vLLM vs SGLang 2026 — Structured Output 성능 비교: https://www.spheron.network/blog/vllm-vs-sglang-2026/
- OpenAI Structured Outputs 공식 문서: https://platform.openai.com/docs/guides/structured-outputs