LLM과 JSON 출력
요새는 agentic LLM이니 tool calling이니 이런 것들이 프론티어이지만 LLM 초기에는 JSON(또는 모종의 구조화된 출력)을 내뱉게 하는 것도 힘들었다.
- 규칙성 없는 순수 next-token 샘플링이었음
- 잘못된 JSON이 나와도 기계적인 검증 절차 없이 그냥 출력됨
- 오타 하나로 전체가 파싱 오류가 나는 일이 빈번했음
- "질문 → JSON 답" 데이터가 많이 없었음
Sure! Here's your JSON:같은 서두를 붙이는 경우가 많아, 이를 제거하기도 어려웠음- 출력 JSON이 길어질수록 instruction following 능력이 저하됐었음
- BPE 토큰 경계가 JSON 문법 단위와 어긋났음
- 예를 들어
{",key,":와 같은 식으로 토큰이 쪼개져, 모델이 JSON 구조를 학습하지 못함 - 이 어긋남 자체는 지금도 존재하며, 현대 엔진이 토큰-문법 정렬을 별도로 처리하는 이유임
- 예를 들어
이를 우회하기 위한 여러 편법들이 존재했었다.
- 각 key 별로 value를 단답형으로 질문하게 해서 코드 레벨에서 JSON을 엮어냄
- 우선 답변을 하게 한 뒤, JSON을 regex로 감지해내거나, 최대한 살릴 수 있는 부분을 살려내서 사용함
- LLM이 답변하기 더 쉬운 YAML이나 TOML 형태로 답변하게 한 뒤 추출해서 변환함
- 잘 나올 때까지 통째로 재생성... (output-level rejection)
이를 근본적으로 해결하려는 연구는 두 갈래로 진행됐다.
- 모델 레벨(가중치). 스키마 예제와 tool calling 데이터를 포스트트레이닝(행동 교정)에 넣어 JSON을 더 잘 출력하게 만드는 것.
- 디코딩 레벨(인퍼런스 엔진). 애초에 잘못된 토큰이 나올 수 없게 샘플링을 막는 것.
디코딩 레벨의 시조는 사실 LLM 붐 이전인 PICARD(2021)로, SQL 생성용이었다. 토큰을 하나 뽑은 후 파서로 검사하고, 불합격이면 기각하고 다시 뽑는 token-level rejection 방식이다.
토큰 하나 생성
토큰 하나 생성
이후 LLM과 구조화된 출력의 필요가 폭증하자, 2022–2023년에 도구들이 쏟아졌다. 예를 들어 Jsonformer는 중괄호, 키, 따옴표 같은 구조 토큰은 코드가 직접 삽입하고 LLM은 value 빈칸만 생성하게 했다 (구조 부분은 아예 생성을 안 시킴). 그러나 결과적으로 다음 방식으로 정리되었다.
Constrained Decoding
문법을 유한 상태 기계로 보고, 각 상태에서 허용되는 토큰 집합 테이블을 생성 시작 전에 미리 컴파일해 둔다. 런타임에는 매 스텝 토큰을 전수 검사하는 대신 테이블 조회만 한다.
- 모델의 포워드 패스
- 트랜스포머가 어텐션과 MLP를 계산해 로짓(다음 토큰 후보들 각각에 대한 점수)을 출력한다.
- GPU에서 실행되며, constrained decoding이 전혀 건들지 않는다.
- 인퍼런스 엔진의 로짓 처리
- 포워드 패스와 샘플링 사이에서, 현재 JSON이 어느 상태까지 출력됐는지 추적하고, 사전 컴파일된 테이블에서 해당 상태의 마스크를 조회해 JSON을 '고장낼' 후보들의 로짓에 −∞를 가산한다.
- 상태 추적과 마스크 산출은 CPU의 문법 엔진이 담당하고, 마스크는 GPU 위의 로짓 텐서에 적용된다.
- 인퍼런스 엔진의 샘플링
- 살아남은 로짓을 softmax 처리하여 다음 토큰을 최종 선정한다.
즉 토큰을 골라놓고 적법한지 검사하는 게 아니라, 적법한 후보만 남겨놓고 고르는 것. 모델은 자신이 제약받고 있다는 사실조차 모른다.
상용 API는 여기에 모델 레벨을 결합했다. OpenAI 자료에 따르면 gpt-4o-2024-08-06을 포스트트레이닝에서 복잡한 스키마 예제로 훈련해 스키마 준수율을 93%까지 높였고, 남은 갭은 로짓 처리 레이어(constrained decoding)로 100%를 보장한다. 즉
- 가중치가 품질을
- 마스크가 보증을
담당하는 이중 장치이다.