Structured Outputs 실전
메뉴

AI Backend

Structured Outputs 실전

LLM 응답을 자유 텍스트가 아니라 JSON Schema 기반 API 계약으로 다루기 위해 Structured Outputs, validation, schema version, fallback을 설계하는 방법을 정리합니다.

Structured Outputs 실전 hero image
Markdown약 1959 tokens

모델 출력을 JSON Schema 계약으로 만들기

이 글에서는 Structured Outputs를 이용해 LLM 응답을 JSON Schema 기반 계약으로 다루는 방법을 정리합니다.

LLM 응답을 서비스에 붙일 때 가장 먼저 부딪히는 문제는 “답변이 그럴듯한가”가 아닙니다. “백엔드가 파싱할 수 있는가”입니다.

모델이 자유 텍스트로 답변하면 프론트엔드, DB, 후속 API가 안정적으로 사용할 수 없습니다. 특히 문서 요약, 정보 추출, 분류, 라우팅, 툴 호출 전처리처럼 구조화된 결과가 필요한 작업에서는 모델 출력을 계약으로 만들어야 합니다.

분석 기준일: 2026-05-12
실습 기준 환경: OpenAI API, Python, Pydantic, FastAPI
주요 참고자료: OpenAI Structured Outputs Docs, JSON Schema, Pydantic

핵심 요약

  • Structured Outputs는 모델 응답이 지정한 JSON Schema를 따르도록 만드는 기능이다.
  • LLM 응답은 서비스 내부에서 API response와 같은 계약으로 다뤄야 한다.
  • schema version을 두지 않으면 프롬프트 변경과 응답 구조 변경을 추적하기 어렵다.
  • validation 실패는 정상 운영 시나리오로 보고 retry/fallback을 설계해야 한다.
  • 모든 필드를 모델에게 맡기지 말고 서버가 계산할 필드는 서버에서 채워야 한다.

1. 왜 구조화된 출력이 필요한가

자유 텍스트 응답은 사람이 읽기에는 좋지만 시스템이 처리하기에는 어렵습니다.

예를 들어 모델에게 아래처럼 요청했다고 가정해보겠습니다.

# 예시입니다.이 문서의 핵심 내용을 요약하고, 출처와 신뢰도를 포함해줘.

모델은 다양한 형태로 답할 수 있습니다.

# 예시입니다.핵심은 Redis 캐시가 DB 부하를 줄인다는 점입니다. 출처는 Redis 공식문서입니다.

또는:

# 예시입니다.{  "summary": "Redis 캐시는 DB 부하를 줄인다",  "source": "Redis Docs"}

서비스는 이런 변동성을 싫어합니다. 프론트엔드는 citations 배열을 기대하는데 모델이 source 문자열을 주면 깨집니다.

2. JSON mode와 Structured Outputs의 차이

JSON mode는 유효한 JSON을 생성하는 데 도움을 줍니다. 하지만 특정 schema를 반드시 지키는 것과는 다릅니다.

Structured Outputs는 응답이 개발자가 정의한 JSON Schema를 따르도록 하는 데 초점을 둡니다.

구분JSON modeStructured Outputs
목표JSON 형식 출력JSON Schema 준수
필수 필드 보장약함강함
enum 제약약함schema 기반
API 계약 적합성제한적높음

실무적으로는 JSON mode보다 schema 기반 출력이 더 안전합니다.

3. 응답 schema 설계 원칙

좋은 schema는 모델에게 모든 것을 맡기지 않습니다.

원칙설명
필요한 필드만 둔다과한 필드는 실패율을 높인다
enum을 적극 사용한다후속 분기 안정성 확보
서버 계산 필드는 제외한다token usage, trace_id 등은 서버가 채운다
nullable을 신중히 쓴다null 허용은 후속 처리 복잡도 증가
version을 둔다schema 변경 추적

4. 문서 Q&A 응답 schema 예제

// 예시 JSON 구조입니다.{  "type": "object",  "properties": {    "answer": {      "type": "string",      "description": "사용자 질문에 대한 답변"    },    "confidence": {      "type": "string",      "enum": ["high", "medium", "low"]    },    "citations": {      "type": "array",      "items": {        "type": "object",        "properties": {          "document_id": { "type": "string" },          "chunk_id": { "type": "string" },          "quote": { "type": "string" }        },        "required": ["document_id", "chunk_id", "quote"]      }    },    "needs_human_review": {      "type": "boolean"    }  },  "required": ["answer", "confidence", "citations", "needs_human_review"],  "additionalProperties": false}

이 schema는 답변, 신뢰도, 출처, 검토 필요 여부를 명확히 분리합니다.

5. Validation과 fallback

Structured Outputs를 사용해도 백엔드 validation은 필요합니다. 외부 시스템의 출력은 항상 검증해야 합니다.

# 예시 코드입니다.from pydantic import BaseModelfrom typing import Literal # 이 선언은 예시 흐름을 보여줍니다.class Citation(BaseModel):    document_id: str    chunk_id: str    quote: str # 이 선언은 예시 흐름을 보여줍니다.class AnswerOutput(BaseModel):    answer: str    confidence: Literal["high", "medium", "low"]    citations: list[Citation]    needs_human_review: bool

실패 시나리오:

# 예시입니다.1. 모델 응답 schema validation 실패2. 한 번 재시도3. 그래도 실패하면 fallback 응답 반환4. eval/debug dataset에 실패 케이스 저장

Fallback 예시:

// 예시 JSON 구조입니다.{  "answer": "답변을 안정적으로 생성하지 못했습니다. 질문을 조금 더 구체적으로 입력해 주세요.",  "confidence": "low",  "citations": [],  "needs_human_review": true}

6. Schema versioning

응답 구조는 시간이 지나며 바뀝니다. 따라서 schema version이 필요합니다.

# 예시입니다.answer.v1answer.v2classification.v1extraction.receipt.v1

schema version은 prompt version과 함께 기록해야 합니다.

기록 항목이유
schema_version응답 구조 추적
prompt_version프롬프트 변경 추적
model모델별 품질 비교
validation_result실패율 분석

7. DB 저장과 API 응답 분리

모델 출력 schema와 API 응답 schema를 완전히 같게 만들 필요는 없습니다.

모델 출력:

// 예시 JSON 구조입니다.{  "answer": "...",  "confidence": "high",  "citations": []}

API 응답:

// 예시 JSON 구조입니다.{  "answer_id": "ans_123",  "answer": "...",  "confidence": "high",  "citations": [],  "usage": {    "input_tokens": 1000,    "output_tokens": 300  },  "trace_id": "0af..."}

usage, trace_id, answer_id는 서버가 붙이는 값입니다.

8. 실무 리스크

리스크대응
schema가 너무 복잡함작은 schema부터 시작
enum이 현실을 못 담음unknown 또는 needs_review 설계
모델이 출처를 꾸며냄citation은 검색 결과 chunk_id에서만 선택하도록 제한
schema 변경 시 클라이언트 깨짐versioning과 migration
validation 실패를 장애로만 봄정상 운영 지표로 수집

9. 실무 체크리스트

# 예시입니다.[ ] 모델 출력 schema와 API 응답 schema를 구분했는가?[ ] required 필드가 명확한가?[ ] additionalProperties를 제한했는가?[ ] enum 값이 후속 로직과 연결되는가?[ ] schema_version을 저장하는가?[ ] validation 실패율을 metric으로 남기는가?[ ] 실패 시 retry/fallback 정책이 있는가?[ ] citation을 모델이 임의 생성하지 못하게 했는가?

실패 사례: JSON은 받았지만 계약은 없었던 경우

Structured Outputs를 쓴다고 해서 곧바로 안정적인 API 계약이 생기지는 않습니다. 가장 흔한 실패는 schema를 만들었지만 version, validation, fallback 정책이 없는 경우입니다. 모델은 JSON을 반환하고 parser도 통과하지만, downstream service가 기대한 의미와 다를 수 있습니다. 예를 들어 risk: "medium"이 어떤 threshold를 뜻하는지 문서화되지 않으면 UI, DB, alert rule이 서로 다르게 해석합니다.

또 다른 문제는 모든 필드를 모델에게 맡기는 것입니다. createdAt, sourceId, confidenceScore, needsReview 같은 값은 일부는 서버가 계산해야 합니다. 모델이 채운 timestamp나 내부 id를 그대로 믿으면 데이터 정합성이 깨집니다. Structured Outputs는 출력 형식을 제어하는 도구이지, business rule 검증을 대체하는 도구가 아닙니다.

구현 예시: schema version과 서버 계산 필드

type ExtractionV1 = {  schemaVersion: "customer_intent.v1";  intent: "refund" | "upgrade" | "bug_report" | "unknown";  evidence: string[];  modelConfidence: number;}; type StoredExtraction = ExtractionV1 & {  requestId: string;  validatedAt: string;  needsReview: boolean;};

모델은 ExtractionV1만 생성합니다. 서버는 requestId, validatedAt, needsReview를 계산합니다. 특히 needsReview는 모델 confidence만 보지 않고 사용자 등급, 민감 키워드, 이전 실패 이력 같은 서버 정보를 함께 봐야 합니다.

비교표: 출력 형식과 운영 계약

항목Structured Outputs가 도와주는 것서버가 계속 해야 하는 것
JSON 형식schema에 맞는 응답 생성파싱 실패와 재시도 처리
enum허용 값 제한값의 business 의미 정의
required field필드 누락 감소신뢰할 수 없는 값 재계산
schema구조 고정version migration과 호환성

이 표를 기준으로 보면 Structured Outputs는 validation layer의 앞단입니다. 백엔드는 여전히 외부 입력을 검증해야 하고, 저장 전에는 business invariant를 확인해야 합니다. 그래야 모델 교체나 prompt 변경이 downstream 장애로 바로 이어지지 않습니다.

실무 적용 예시: 문의 분류 결과 저장

고객 문의를 refund, billing, bug, account 같은 intent로 분류하는 기능을 만든다고 해봅시다. 모델 출력 schema는 작게 유지하는 편이 좋습니다.

{  "schemaVersion": "ticket_intent.v1",  "intent": "billing",  "evidence": ["invoice number", "charged twice"],  "confidence": "medium"}

여기서 서버는 requestId, receivedAt, customerId, needsHumanReview를 별도로 붙입니다. 특히 needsHumanReview는 모델 confidence 하나로 결정하지 않습니다. 환불, 개인정보, 결제 실패처럼 민감한 intent는 confidence가 높아도 검토 대상으로 보낼 수 있습니다.

실패 사례는 schema를 너무 빨리 크게 만드는 것입니다. 처음부터 department, assignee, refundAmount, legalRisk, replyDraft까지 한 번에 받으면 모델은 형식은 맞춰도 business 의미를 섞기 쉽습니다. 분류 schema와 답변 초안 schema를 분리하면 실패 범위가 줄어듭니다. 분류가 틀려도 답변 생성은 시작하지 않을 수 있고, 답변 초안이 실패해도 분류 로그는 남길 수 있습니다.

비교 기준은 단순합니다. Structured Outputs가 보장하는 것은 "응답이 schema 모양에 가깝다"는 점입니다. 서비스가 보장해야 하는 것은 "그 값이 현재 사용자, 권한, DB 상태, 정책에 맞다"는 점입니다. 이 둘을 분리해야 JSON을 받았다는 이유만으로 잘못된 후속 API가 실행되는 사고를 막을 수 있습니다.

10. Q&A

Q1. Structured Outputs를 쓰면 validation 코드를 안 써도 되나요?

아닙니다. 백엔드에서는 여전히 validation을 해야 합니다. 외부 시스템 결과를 신뢰하지 않는 것이 기본입니다.

Q2. 모든 LLM 응답을 JSON으로 만들어야 하나요?

아닙니다. 사용자에게 보여줄 최종 답변은 Markdown일 수 있습니다. 다만 시스템이 후속 처리해야 하는 값은 구조화하는 것이 좋습니다.

Q3. schema가 너무 커지면 어떻게 하나요?

작게 나누는 편이 좋습니다. 추출, 분류, 답변, 검토 필요 여부를 하나의 거대한 schema에 넣기보다 단계별 schema로 분리합니다.

11. 참고자료와 불확실성

참고자료

불확실성

  • 지원 모델과 API 세부 파라미터는 시점에 따라 바뀔 수 있습니다.
  • 복잡한 schema의 성능과 실패율은 실제 테스트가 필요합니다.

Create model output as a JSON Schema contract

In this article, we summarize how to use Structured Outputs to handle LLM responses as JSON Schema-based contracts.

The first question you encounter when attaching an LLM response to a service is not “Is the answer plausible?” “Can the backend parse it?”

If your model answers in free text, your frontend, DB, and subsequent APIs won't be able to use it reliably. In particular, tasks that require structured results, such as document summarization, information extraction, classification, routing, and tool call preprocessing, require model outputs to be contracts.

Analysis base date: 2026-05-12 Practice standard environment: OpenAI API, Python, Pydantic, FastAPI Main reference materials: OpenAI Structured Outputs Docs, JSON Schema, Pydantic

Key takeaways

  • Structured Outputs is a function that makes model responses follow the specified JSON Schema.
  • LLM responses must be treated as contracts like API responses within the service.
  • Without a schema version, it is difficult to track changes in prompts and response structures.
  • Validation failure should be viewed as a normal operating scenario and a retry/fallback should be designed.
  • Instead of leaving all fields to the model, the fields to be calculated by the server should be filled in by the server.

1. Why Structured Outputs?

Free text responses are great for humans to read, but difficult for systems to process.

For example, let's say you made the following request to the model:

# This is an example.Summarize the main content of this document and include sources and credibility.

The model can answer in many different ways.

# This is an example.The key point is that Redis cache reduces DB load. The source is the Redis official document.

or:

# This is an example.{"summary": "Redis cache reduces DB load","source": "Redis Docs"}

Services hate this volatility. The frontend expects an array ofcitations, but if the model gives it the stringsource, it breaks.

2. Difference between JSON mode and Structured Outputs

JSON mode helps you generate valid JSON. However, this is different from necessarily adhering to a specific schema.

Structured Outputs focuses on ensuring that the response follows the JSON Schema defined by the developer.

divisionJSON modeStructured Outputs
targetJSON format outputJSON Schema compliance
Ensure required fieldsweaknessstrong
enum constraintsweaknessschema based
API contract compliancelimitedheight

In practice, schema-based output is safer than JSON mode.

3. Response schema design principles

A good schema doesn't leave everything to the model.

principleexplanation
Leave only the fields you needExcessive fields increase the failure rate
Actively use enumsEnsure stability in subsequent quarters
Exclude server calculated fieldsToken usage, trace_id, etc. are filled in by the server.
Use nullable carefullyNullability increases subsequent processing complexity
put versionSchema change tracking

4. Document Q&A response schema example

// This is an example JSON structure.{  "type": "object",  "properties": {    "answer": {      "type": "string",      "description": "Answers to user questions"    },    "confidence": {      "type": "string",      "enum": ["high", "medium", "low"]    },    "citations": {      "type": "array",      "items": {        "type": "object",        "properties": {          "document_id": { "type": "string" },          "chunk_id": { "type": "string" },          "quote": { "type": "string" }        },        "required": ["document_id", "chunk_id", "quote"]      }    },    "needs_human_review": {      "type": "boolean"    }  },  "required": ["answer", "confidence", "citations", "needs_human_review"],  "additionalProperties": false}

This schema clearly separates answers, their credibility, source, and whether they require review.

5. Validation and fallback

Even when using Structured Outputs, backend validation is required. Output from external systems should always be verified.

# This is example code.from pydantic import BaseModelfrom typing import Literal # This declaration shows an example flow.class Citation(BaseModel):    document_id: str    chunk_id: str    quote: str # This declaration shows an example flow.class AnswerOutput(BaseModel):    answer: str    confidence: Literal["high", "medium", "low"]    citations: list[Citation]    needs_human_review: bool

Failure Scenario:

# This is an example.1. Model response schema validation failed2. Retry once3. If it still fails, return a fallback response4. Save failure cases in eval/debug dataset

Fallback example:

// This is an example JSON structure.{  "answer": "Could not reliably generate answers. Please be a little more specific with your question.",  "confidence": "low",  "citations": [],  "needs_human_review": true}

6. Schema versioning

Response structures change over time. Therefore, schema version is required.

# This is an example.answer.v1answer.v2classification.v1extraction.receipt.v1

The schema version must be recorded along with the prompt version.

record entryreason
schema_versionResponse structure tracking
prompt_versionTrack prompt changes
modelQuality comparison by model
validation_resultFailure rate analysis

7. Separate DB storage and API response

There is no need to make the model output schema and API response schema exactly the same.

Model output:

// This is an example JSON structure.{  "answer": "...",  "confidence": "high",  "citations": []}

API response:

// This is an example JSON structure.{  "answer_id": "ans_123",  "answer": "...",  "confidence": "high",  "citations": [],  "usage": {    "input_tokens": 1000,    "output_tokens": 300  },  "trace_id": "0af..."}

usage,trace_id, andanswer_idare values ​​added by the server.

8. Practice risks

riskreact
Schema is too complexStart with a small schema
enum does not contain realityunknown or needs_review design
Model fabricates originCitations are limited to selection only by search result chunk_id
Client crashes when changing schemaversioning and migration
View validation failure only as a failureCollected as normal operating indicators

9. Practical checklist

# This is an example.[ ] Did you distinguish between model output schema and API response schema?[ ] Are the required fields clear?[ ] Have you restricted additionalProperties?[ ] Are enum values ​​connected to subsequent logic?[ ] Do you save schema_version?[ ] Is the validation failure rate recorded as a metric?[ ] Is there a retry/fallback policy in case of failure?[ ] Did you prevent the model from randomly generating citations?

Failure case: JSON received but no contract

Using Structured Outputs does not immediately create a stable API contract. The most common failure is when you create a schema but don't have a version, validation, or fallback policy. The model returns JSON, which the parser also passes, but the meaning may not be what the downstream service expects. For example, if it is not documented what thresholdrisk: "medium"means, the UI, DB, and alert rule interpret it differently.

Another problem is leaving all the fields to the model. Some values, such ascreatedAt,sourceId,confidenceScore, andneedsReview, must be calculated by the server. If you trust the timestamp or internal ID filled in by the model, data consistency will be broken. Structured Outputs is a tool for controlling output format, not a replacement for business rule validation.

Example implementation: schema version and server calculated fields

type ExtractionV1 = {  schemaVersion: "customer_intent.v1";  intent: "refund" | "upgrade" | "bug_report" | "unknown";  evidence: string[];  modelConfidence: number;}; type StoredExtraction = ExtractionV1 & {  requestId: string;  validatedAt: string;  needsReview: boolean;};

The model only producesExtractionV1. The server calculatesrequestId,validatedAt,needsReview. In particular,needsReviewmust not only look at model confidence, but also look at server information such as user rating, sensitive keywords, and previous failure history.

Comparison table: output formats and operating agreements

itemWhat Structured Outputs can help you withWhat the server must continue to do
JSON formatGenerate response matching schemaHandling parsing failures and retries
enumAllowed value limitsDefine the business meaning of the value
required fieldReduce missing fieldsRecalculate Untrusted Values
schemastructural fixationCompatibility with version migration

Based on this table, Structured Outputs are at the front of the validation layer. The backend still needs to validate external input and check for business invariants before saving. This ensures that model replacement or prompt changes do not directly lead to downstream failures.

Practical application example: storing inquiry classification results

Let's say you want to create a function that classifies customer inquiries into intents such as refund, billing, bug, and account. It is best to keep the model output schema small.

{  "schemaVersion": "ticket_intent.v1",  "intent": "billing",  "evidence": ["invoice number", "charged twice"],  "confidence": "medium"}

Here, the server separately addsrequestId,receivedAt,customerId, andneedsHumanReview. In particular,needsHumanReviewis not determined by model confidence alone. Sensitive intents such as refunds, personal information, and payment failures can be sent for review even if the confidence level is high.

A failure is to make your schema too large too quickly. If you receivedepartment,assignee,refundAmount,legalRisk, andreplyDraftat once from the beginning, it is easy to mix up the business meaning even if the model format matches. Separating the classification schema and the answer draft schema reduces the scope for failure. Even if the classification is incorrect, answer generation may not start, and even if the answer draft fails, a classification log may be left.

The comparison criteria are simple. What Structured Outputs guarantees is that “the response is close to the schema shape.” What the service must guarantee is that “the value matches the current user, permissions, DB state, and policy.” Separating the two prevents you from running the wrong subsequent API just because you received JSON.

10. Q&A

Q1. If I use Structured Outputs, can I avoid using validation code?

no. Validation still needs to be done in the backend. It is fundamental not to trust external system results.

Q2. Should all LLM responses be JSON?

no. The final answer you show your users might be Markdown. However, it is a good idea to structure the values ​​that the system must subsequently process.

Q3. What if my schema gets too big?

It's better to break it down into smaller pieces. Separate the extraction, classification, response, and review needs into step-by-step schemas rather than putting them into one large schema.

11. References and uncertainty

References

uncertainty

  • Supported models and detailed API parameters may change over time.
  • Performance and failure rates of complex schemas require real-world testing.

댓글

GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.

TOP