LLM 구조화 출력, JSON 스키마, tool_use | AI 응답에서 파싱 에러 없이 데이터를 뽑으려면?
JSON mode만 쓰면 마크다운 코드 블록과 설명 문장이 섞여 파싱 에러가 난다. OpenAI structured outputs(json_schema + strict), Anthropic Claude tool_use 강제, Google Gemini response_schema로 스키마를 고정하는 방법과 각각 실패하는 케이스를 API 실사용 기준으로 비교한다. 자동 포스팅 파이프라인에서 안정적으로 쓰는 실무 패턴 포함.
LLM에 "JSON으로 줘"라고 하면 마크다운 코드 블록, 주석, 설명 문장이 섞여서 나옵니다. 저는 12개 사이트의 자동 포스팅 파이프라인에서 이 문제를 여러 번 겪었습니다. JSON 파싱 에러가 날 때마다 재시도 로직을 짜는 것보다, 처음부터 구조화 출력(structured outputs)으로 고정하는 게 훨씬 안정적입니다.
이 글은 OpenAI, Anthropic Claude, Google Gemini에서 구조화 출력을 쓰는 방법과 각 접근의 차이, 실제 파싱 에러가 나는 케이스를 정리합니다. JSON mode, structured outputs, tool_use(function calling)의 차이도 포함합니다.
세 방식의 차이 | JSON mode, structured outputs, tool_use
LLM에서 구조화된 데이터를 받는 방식은 크게 세 가지입니다.
JSON mode는 모델에게 "JSON을 출력해라"라고 지시합니다. JSON 문자열이 나오기는 하지만 스키마를 강제하지 않아서 필드가 빠지거나 타입이 다를 수 있습니다. 가장 느슨한 방식입니다.
Structured outputs(OpenAI 기준)는 JSON 스키마를 정확히 정의하면 모델이 그 스키마를 반드시 따르도록 보장합니다. strict 옵션이 있으면 스키마 이외의 필드가 절대 나오지 않습니다.
Tool use / function calling은 원래 외부 함수를 모델이 호출하도록 만든 기능이지만, 구조화 출력 용도로 쓰기도 합니다. 특정 도구를 반드시 호출하도록 강제하면 해당 함수의 파라미터 스키마에 맞는 JSON이 나옵니다. Anthropic Claude에서 가장 안정적인 방식입니다.
방식
스키마 보장
OpenAI
Claude
Gemini
JSON mode
약함 (JSON 형식만)
json_object
프롬프트 지시
json MIME 타입
Structured outputs
강함 (스키마 100%)
json_schema
-
response_schema
Tool use / Function calling
강함
tool_choice 강제
tool_use 강제
function_calling
세 방식 모두 JSON을 받지만 스키마 보장 수준이 다르다
OpenAI structured outputs 쓰는 방법
OpenAI의 gpt-4o, gpt-4o-mini는 structured outputs를 지원합니다. response_format에 json_schema를 넘기고 strict: true를 주면 스키마를 벗어난 출력이 나오지 않습니다.
tool_choice: { type: 'tool', name: '...' }로 특정 도구를 강제 호출하면 클로드가 반드시 그 도구의 스키마에 맞는 데이터를 냅니다. 결과값이 이미 파싱된 JavaScript 객체로 오기 때문에 JSON.parse가 필요 없습니다. 저는 자동 포스팅 파이프라인에서 이 방식이 가장 안정적이었습니다.
tool_use 결과는 이미 파싱된 객체라서 별도 JSON.parse 없이 바로 쓸 수 있다
파싱 에러가 나는 케이스들
⚠️ JSON mode에서 마크다운이 섞이는 경우 | response_format: { type: "json_object" }는 JSON 형식을 유도하지만 강제하지는 않습니다. 오래된 모델이나 프롬프트에 따라 마크다운 코드 블록이나 설명 문장이 앞뒤에 붙을 수 있습니다. 이 방식을 쓴다면 파싱 전에 코드 블록 제거 로직을 항상 넣어야 합니다.
⚠️ strict 모드에서 additionalProperties 누락 | OpenAI structured outputs의 strict 모드는 중첩된 모든 객체에 additionalProperties: false가 있어야 합니다. 이게 빠지면 API 호출 자체가 에러를 냅니다. 중첩 객체가 많은 복잡한 스키마에서 자주 빠뜨리는 케이스입니다.
⚠️ 큰 배열이 잘리는 경우 | 태그를 50개 이상 뽑아달라거나 항목이 많은 배열을 요청하면 max_tokens 한도 때문에 중간에 잘릴 수 있습니다. 잘린 JSON은 파싱 에러가 납니다. 배열 아이템 수를 프롬프트에서 제한하거나 max_tokens를 충분히 설정하세요.
스키마를 강제하지 않으면 다음 요청에서 필드가 빠지거나 형식이 달라질 수 있다
안정적으로 쓰는 실무 패턴
자동화 파이프라인에서 LLM 구조화 출력을 안정적으로 쓰려면 몇 가지 패턴이 도움이 됩니다.
파싱 실패 시 재시도 로직 추가 | structured outputs를 써도 네트워크 오류나 모델 오류가 날 수 있습니다. JSON.parse가 실패하면 같은 프롬프트로 최대 2~3번 재시도합니다.
Zod로 런타임 타입 검증 | 구조화 출력을 받아도 Zod 스키마로 한 번 더 검증하면 TypeScript 타입이 자동 추론되고, 예상 밖 값이 들어올 때 에러를 잡을 수 있습니다. zod-to-json-schema 패키지로 Zod 스키마를 LLM 스키마로 변환할 수 있습니다.
배열은 최대 크기 제한 | 프롬프트에 "5개 이내"라고 명시하거나 스키마에 maxItems를 설정합니다. 무한정 배열은 토큰 낭비이고 잘릴 위험이 있습니다.
필수 필드와 선택 필드 구분 | 항상 있어야 하는 것만 required에 넣습니다. optional 필드를 required에 넣으면 없는 정보를 모델이 만들어내는 hallucination 위험이 있습니다.
자주 묻는 것들
JSON mode와 structured outputs의 차이가 뭔가?
JSON mode는 모델에게 JSON 형식으로 출력하라고 지시할 뿐, 특정 스키마를 보장하지 않습니다. Structured outputs는 정의한 스키마를 100% 따르도록 모델 레벨에서 강제합니다. 안정성이 필요하면 structured outputs나 tool_use를 쓰세요.
중첩 객체나 배열 스키마도 strict 모드가 지원하나?
지원합니다. 단, 중첩된 모든 객체에 additionalProperties: false를 넣어야 합니다. 배열 아이템 스키마에도 같은 규칙이 적용됩니다. 이 조건을 만족하면 어떤 구조든 strict 모드를 쓸 수 있습니다.
Zod 스키마를 LLM 스키마로도 쓸 수 있나?
직접 쓸 수는 없지만 변환 라이브러리가 있습니다. zod-to-json-schema 패키지로 Zod 스키마를 JSON 스키마로 변환하면 OpenAI structured outputs에 넘길 수 있습니다. Anthropic SDK의 zodResponseFormat 헬퍼도 비슷한 역할을 합니다.
Gemini에서 구조화 출력은 어떻게 쓰나?
Google AI SDK에서 generationConfig에 responseMimeType: "application/json"과 responseSchema를 넘깁니다. OpenAI JSON 스키마와 비슷한 형식으로 스키마를 정의합니다. 단 strict 모드가 없어서 optional 필드가 빠지는 경우가 있습니다. Zod 검증 로직을 추가하는 게 안전합니다.
프롬프트에 JSON 예시를 주면 더 안정적이지 않나?
예시를 주면 모델이 형식을 더 잘 따르는 경향이 있지만, 구조화 출력 API 자체가 더 강력한 보장입니다. 예시만 주는 방식은 모델 버전이 바뀌거나 프롬프트가 길어지면 형식이 흔들릴 수 있습니다. 예시 제공과 structured outputs를 함께 쓰면 더 안정적입니다.