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%를 보장한다. 즉
- 가중치가 품질을
- 마스크가 보증을
담당하는 이중 장치이다.