"Return JSON only"만으로는 JSON을 강제하지 않습니다. 실제로 JSON 을 강제하는 방법은 무엇인가요?
요약
LLM에게 'JSON만 반환하라'고 지시하는 것은 단순히 다음 토큰에 대한 확률 분포를 이동시키는 것에 불과하며, 완벽한 JSON 출력을 보장하지 못합니다. 프로덕션 환경에서 파싱 실패가 발생할 위험이 있습니다. 진정으로 JSON 출력을 강제하려면 프롬프트 엔지니어링이 아닌 '제약 디코딩(Constrained Decoding)'이라는 추론 단계의 메커니즘을 사용해야 합니다. 이는 특정 스키마를 따르지 않는 토큰의 확률을 0으로 설정하여 모델이 해당 토큰을 생성하는 것을 원천적으로 불가능하게 만듭니다.
핵심 포인트
- 단순한 프롬프트 지시('JSON만 반환')는 출력 분포에 영향을 줄 뿐, 강제적인 보장을 제공하지 않습니다. 이는 '소프트' 접근 방식입니다.
- 진정한 JSON 출력을 강제하는 방법은 추론 단계에서 작동하는 '제약 디코딩(Constrained Decoding)'을 사용하는 것입니다. 이는 토큰 수준의 스키마 유효성 검사를 통해 불가능한 토큰의 확률을 0으로 만듭니다.
- 주요 구현 기술로는 Outlines 라이브러리, llama.cpp의 --grammar-file, 그리고 OpenAI의 structured outputs 기능 등이 있습니다.
- LLM 출력을 구조화된 데이터로 사용할 때는 파싱 실패를 '1차 이벤트'로 취급하고 절대 침묵적으로 None을 다운스트림으로 흘려보내서는 안 됩니다.
- 구조화된 출력에 의존하는 중요한 결정(점수, 라우팅 등)에는 반드시 제약 디코딩과 같은 하드한 메커니즘을 사용해야 합니다.
파이프라인에 평가용 LLM(강화학습)이 있습니다. "JSON 만 반환하세요. 서론이나 설명은 없으세요. JSON 객체만."라고 지시했습니다. 테스트 환경에서는 잘 작동합니다. 스테이지 환경에서도 잘 작동합니다. 그런데 프로덕션 환경에서는 다음과 같이 반환합니다:
Sure! Here's my evaluation of the response: { "score" : 4 , "reason" : "The answer is mostly correct but..." }
json.loads()가 에러를 발생시킵니다. 파이프라인이 아무것도 잡지 않습니다. 다운스트림 코드는 None 을 받고 계속 실행됩니다. 다음 200 회 요청에 걸쳐 평가 점수가 조용히 잘못됩니다. 누군가가 이를 발견하기 전에. 이 모델이 거북한 것이었나요? 아닙니다. JSON 출력으로 실제로 강제할 수 있는 방법이 있었나요? 예 — 하지만 그것은 프롬프트가 아닙니다. 실제 메커니즘을 보여드리겠습니다.
"Return JSON only" 가 실제로 무엇을 하는지
프롬프트에 형식 지시문을 작성할 때, 당신은 정확히 한 일을 합니다: 다음 토큰에 대한 확률 분포를 이동시킵니다. 모델은 훈련 과정에서 그런 표현식이 { 와 잘-formed JSON 바디로 이어지는 수백만 개의 예제를 보았습니다. 당신의 지시는 그 패턴을 컨텍스트에 강하게 로드합니다. JSON 형태의 토큰에 대한 확률 질량은 크게 증가합니다 — 잘 튜닝된 모델에서는 95–99% 의 경우 유효한 JSON 을 얻습니다. 그러나 확률은 확실하지 않습니다. 디코딩 단계마다, 모델은 출력 분포에 따라 다음 토큰을 선택합니다. 온도가 0 인 경우, argmax 를 선택 — 단일 최고 확률 토큰을 결정적으로 선택합니다. 온도가 0 보다 높은 경우, 샘플링을 수행하므로 낮은 확률 토큰도 선택될 수 있습니다. 어느 경우든, 지시는 그 분포를 형성할 뿐이며, 결과를 제거하지는 않습니다. 서론 문구인 "Sure! Here's the evaluation:" 은 첫 단계에서 매우 작지만 0 이 아닌 확률을 가집니다. 컨텍스트의 어떤 것 — 긴 시스템 프롬프트, 대화형 톤이 있는 입력, 도움이 되는 소리를 내도록 Fine-tuned 된 모델 — 이 그 확률을 약간이라도 높인다면, 서론을 얻고 파싱 실패가 발생합니다.
결정론적 디코딩은 위험을 줄이지만 제거하지는 않습니다: 첫 단계에서 최고 확률 토큰이 genuinely 서론 토큰이라면 여전히 그것을 얻습니다. 이는 지시 따름입니다. 그것은 부드러운 메커니즘입니다. 확실한 보장이 없습니다.
실제로 JSON 을 강제하는 것: 제약 디코딩 (Constrained Decoding)
다른 메커니즘이 있습니다: 제약 디코딩 (또는 구조화된 생성 또는 문법 안내 샘플링). 이는 프롬프트 레이어에서 작동하지 않습니다. 추론 레이어에서 작동합니다 — 샘플링이 발생하기 전에. 이것이 어떻게 작동하는지:
디코딩 단계마다, 시스템은 현재 부분 출력과 문법이나 스키마를 비교합니다. 해당 파싱 상태에서 출력을 무효로 만드는 토큰의 logit 은 음수 무한도로 설정됩니다 — 확률 0 이 됩니다. 모델은 그 토큰을 생성할 수 없습니다. 불가능합니다.
기초 논문은 Willard & Louf (2023), Efficient Guided Generation for Large Language Models 입니다. 그들은 JSON 스키마를 유한 상태 머신으로 컴파일하고, 디코딩 단계마다 보기를 마스킹하는 방법을 보여줍니다. 토큰당 O(1) 시간.
이 마지막 부분이 중요합니다: 접근법은 의미 있는 지연 오버헤드 없이 프로덕션에서 사용할 수 있을 정도로 빠릅니다.
오늘날 이 방법은 다음과 같이 구현됩니다:
Outlines — 논문의 저자들이 만든 참조 라이브러리
llama.cpp 를 통해 --grammar-file (GBNF 문법 형식)
OpenAI structured outputs (response_format: { type: "json_schema", json_schema: {...} }) — OpenAI 의 문서에서는 이 것을 토큰 수준의 스키마 강제, 모든 비거부 호출에서 스키마 유효한 출력을 생성한다고 설명합니다.
참고하세요.
필터: 안전 거절 또는 콘텐츠 필터가 스키마를 따르지 않는 (non-schema) 응답을 반환할 수도 있습니다. 따라서 경계 코드 (boundary code) 는 이 경우를 명시적으로 처리해야 합니다. 소프트 프롬팅 (soft prompting) 과의 차이는 양적이지 않고 범주적입니다. 지시어 수행은 분포 이동입니다. 제약된 디코딩은硬性 배제입니다.
소프트 vs 하드: 최소 코드 비교
소프트 접근법 — 대부분의 파이프라인이 사용하는 방식:
import json
response = client.chat.completions.create(
model=
'output reliability comes from constrained decoding, not prompt engineering' 이라는 점을 강조합니다. 그 차이가 중요한 이유는, 기본 모델 자체를 교체할 때 이를 고려하기 시작하는 순간부터입니다. 구조화된 LLM 출력에 작용하는 모든 파이프라인을 위한 세 가지 규칙이 있습니다.
-
신뢰의 경계마다 항상 검증하세요. LLM 출력이 구조화된 데이터로 코드에 들어가는 모든 지점은 신뢰의 경계입니다. 파싱 실패를 1 차 이벤트 (first-class event) 로 취급하세요 — 이를 기록하고, 경고하며, 크게 처리해야 하며 — 절대 downstream 으로 None 이 침묵적으로 흐르도록 허용하지 마세요.
-
출력이 부하를 지탱할 때 제한된 디코딩을 사용하세요. 점수, 라우팅 결정, 또는 분류가 구조화된 출력에 의존한다면, 제한된 엔드포인트 또는 라이브러리를 사용하세요. 1~5% 범위에서의 소프트 프롬프트 실패는 다단계 파이프라인에서 더 복잡하게 됩니다. 고립 상태에서 2%의 오류율을 가진 판사는 평가 체인에서 10 회 실행될 때 훨씬 더 자주 오류를 범합니다.
-
프롬프트 지시문은 계속 유지하세요. 제한된 디코딩이 있어도 프롬프트에 형식 지시문을 작성하세요. 이는 출력 품질을 개선하고, 코드를 읽는 모든 사람을 위한 의도 문서 역할을 합니다. 하지만 이를 모델의 힌트로, 기술적 계약으로 취급하지 마세요. 스키마 강제성이 계약입니다.
실제 교훈 (The real lesson)은 파이프라인이 모델이 신뢰할 수 없었기 때문에 깨진 것이 아닙니다. 프롬프트 지시문이 타입 제약과 동등하다고 가정하여 시스템이 설계되었기 때문입니다. 그것은 아닙니다. 프롬프트 지시물은 통계적 유도입니다. 디코딩 시간에 강제된 문법은 보장입니다.
구조화된 LLM 출력이 이를 작용하는 코드에 공급되는 순간 — 점수 시스템, 에이전트 라우터, 도구 호출 파서, 추출 파이프라인 등 — 두 가지 중 하나를 필요로 합니다. 유도는 충분하지 않습니다.
'back to my system' 섹션의 코드는 구조화된 AI 엔지니어링 프로그램 동안 구축된 실제 LLM 판사 파이프라인에서 비롯되었습니다. 설명된 실패는 프로덕션 환경에서 발생했습니다.
출처 (Sources):
Willard, B. & Louf, R. (2023). Efficient Guided Generation for Large Language Models. arXiv:2307.09702. https://arxiv.org/abs/2307.09702
OpenAI. Structured Outputs — Platform Documentation. https://platform.openai.com/docs/guides/structured-outputs
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기