JSON을 안정적으로 출력하는 법 — 4층 출력 방어선

4층 출력 방어선: 프롬프트 Few-shot으로 기반 다지기 → 인터페이스 네이티브 능력(JSON Mode / Function Calling) → 밑바탕의 제약 디코딩(Token 단위 감시 + 문법 트리 제약) → 코드 검증과 에러 되먹임 재시도.

1층: 프롬프트로 기반 다지기

  • 방식: 구호만 외치지 말 것(「JSON으로 출력해 주세요」). 완벽한 입력/출력 쌍 예시를 한두 개 직접 줄 것
  • few-shot 예: # 작업: 이름과 나이 추출
  • 원리: 대모델의 모방 능력이 매우 강해서 구체적 샘플이 있으면 크게 벗어나지 않는다
  • 한계: 이것만으로는 턱없이 부족하다 — Few-shot은 확률을 높일 뿐 어떤 보장도 제공하지 않는다

2층: 모델 네이티브 능력

{"response_format": {"type": "json_object"}}

3층: 밑바탕의 비법 — 제약 디코딩

  • 전제: 반드시 모델을 로컬에서 직접 돌려야 한다(디코딩 과정에 개입할 수 있어야 함)
  • 메커니즘: 모델이 글자를 뱉을 때(토큰 단위 생성) 코드로 지켜보다가, 문법에 맞지 않는 내용을 출력하려 하면 그 글자의 생성 확률을 즉시 0으로 만든다
  • 예시: { "name" : John — 이미 합법적으로 생성된 key는 유지하고 비합법 후속은 차단

4층: 폴백 사고 — 검증과 재시도

  • 철칙: 앞에서 아무리 잘했어도 반드시 폴백이 있어야 한다
  • 방식: 결과를 받으면 즉시 코드로 검사·검증. 괄호 누락, 타입 오류 등은 에러 정보를 그대로 되먹여 모델이 스스로 고쳐 다시 출력하게 한다

핵심 통찰

  1. 「확률적 이어쓰기」가 모든 포맷 불안정 문제의 제1원인이다 — LLM에는 「구조를 출력한다」는 개념이 없고 다음 토큰을 샘플링할 뿐이다. 이 점을 이해하면 프롬프트에 쓴 어떤 요구도 확률을 높일 뿐 보장을 제공하지 못하는 이유를 알 수 있다.
  2. 모델의 「예의」가 포맷 킬러다 — RLHF로 훈련된 대화 습관(접두 인사말, 접미 설명, 마크다운 코드 펜스)이야말로 구조화 출력의 최대 오염원이다. 「정렬 목표」와 「공학 목표」가 충돌하는 전형적 사례다.
  3. 방어선은 「보장 강도」 순으로 정렬해야지 「구현 난이도」 순으로 정렬하면 안 된다 — 프롬프트는 확률급, 인터페이스 파라미터는 프로세스급, 제약 디코딩은 문법급, 코드 검증은 결과급이다. 강도가 점증하며 뒤로 갈수록 결정론에 가까워진다.
  4. 제약 디코딩은 유일하게 문법급 보장을 제공하는 수단이지만 배포 전제가 있다 — 샘플링 과정에 개입할 수 있어야 하므로 모델 가중치/추론 스택을 직접 보유해야 한다. 즉 클라우드 API를 쓸 때 3층 「밑바탕 차단」은 손댈 수 없다.
  5. 폴백 층의 가치는 「더 강해서」가 아니라 「무조건적이어서」다 — 앞의 세 층은 모델 버전, 파라미터 지원도, 배포 형태에 따라 무력화될 수 있고, 「코드 검증 + 에러 되먹임 재시도」만이 어떤 형태에서도 쓸 수 있다. 그래서 유일하게 생략할 수 없는 층이다.
  6. 에러 되먹임(error feedback loop)은 LLM을 자가 치유 가능한 컴포넌트로 만든다 — 파서의 에러 원문을 모델에게 되던지는 것이, 사람이 규칙을 짜서 JSON을 고치는 것보다 범용적이다. 에러 정보 자체가 고품질의 수정 힌트이기 때문이다.
  7. 다층 방어는 이식 가능한 공학 패러다임이다 — 「소프트 유도 → 하드 제약 → 밑바탕 차단 → 사후 폴백」이라는 계층 사고는 JSON뿐 아니라 도구 호출 파라미터, 콘텐츠 안전, 길이 통제 등 「모델을 말 듣게 하는」 모든 장면에 똑같이 적용된다.

엔지니어링 실전 Tips

  • Few-shot 예시는 「그대로 복사 가능」해야 한다: 예시 출력은 최종적으로 기대하는 맨 JSON이어야 하며, 예시에 어떤 설명 문구도 넣지 말 것. 그렇지 않으면 모델이 그대로 따라 배운다.
  • 순수 JSON Mode보다 Function Calling을 우선할 것: JSON Mode는 「합법적인 JSON임」만 보장하지만, Function Calling은 필드명과 필드 타입(name: string / age: number)까지 제약하므로 후속 검증이 한 겹 줄어든다.
  • 제약 디코딩은 자체 추론 스택에서만 가능하다: 클라우드 API 호출 시 이 층은 아예 부재하므로, 아키텍처 리뷰 때 배포 형태를 미리 확인하고 손에 넣을 수 없는 능력 위에 신뢰성 방안을 세우지 말 것.
  • 검증 층은 두 종류의 에러를 구분해야 한다: 구조 오류(괄호 누락)는 바로 재시도해도 되지만, 의미 오류(값을 잘못 추출)는 재시도가 무효이므로 사람이나 규칙 폴백으로 가야 한다.
  • 되먹임 시 원본 에러 텍스트를 함께 돌려보낼 것. 모델은 Expecting ',' delimiter: line 1 column 24 같은 정보에 대한 수정 성공률이 막연한 「포맷이 틀렸다」보다 확연히 높다.
  • 재시도에는 상한과 강등이 있어야 한다: 무한 재시도는 비용과 지연을 폭발시킨다. N회를 넘으면 계속 토큰을 태우지 말고 구조화된 실패 객체를 반환할 것.
  • 2층/3층을 넣었다고 4층을 지우지 말 것: 폴백 층은 모델 형태와 무관한 유일한 보험이다.