LLM WikiAccess-protected knowledge portal
← 스터디 홈
172편 · 약 12분

구조화 출력과 제약 디코딩: 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를 예로 들면:

  1. 현재 상태가 "객체 키를 기다리는 중"이면 " 로 시작하는 토큰만 허용
  2. 키가 완성된 후에는 : 를 허용
  3. 값 타입이 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는 이 두 문제를 각각 TagDispatchCross-Grammar Cache로 해결한다.

TagDispatch — 동적 문법 전환
에이전트 출력 스트림
<think>자유 텍스트</think>
<tool_call>JSON</tool_call>
↓ 태그 감지
자유 텍스트
문법
무제한
JSON Schema
문법
제약 마스크
↓ 닫는 태그에서 복원
원래 문법 상태로 복귀
Cross-Grammar Cache — 하위구조 재사용
주문 스키마
{ orderId: str, items: [...] }
계정 스키마
{ userId: str, email: str }
↓ 공통 서브구조 감지
캐시된 서브오토마톤
string 타입 마스크
integer 타입 마스크
array 패턴 마스크
↓ 조합만으로 새 스키마 처리
결과
컴파일 6× 빠름
런타임 near-zero 오버헤드
XGrammar-2 핵심 구성요소

추가로 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% 스키마 준수 보장
        }
    }
)

Anthropictool_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 대비 절대 수치는 워크로드에 따라 다름.


제약 디코딩을 피해야 할 경우

제약 디코딩이 항상 정답은 아니다.

  1. 창의적 텍스트 생성: 마스크가 문장 흐름을 제한해 품질이 떨어질 수 있다.
  2. 매우 복잡한 중첩 스키마: 상태 기계 크기가 기하급수적으로 커질 수 있다.
  3. 짧은 응답 + 낮은 재시도 비용: 단순 재시도가 더 빠를 수 있다.
  4. 다국어 혼합 텍스트: 토큰 경계와 문법 경계가 맞지 않아 예상치 못한 제약이 생길 수 있다.

확인 지표: 구조화 출력 도입 후 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