SpyBara
Go Premium

agent-sdk/structured-outputs.md 2026-11-09 23:02 UTC to 2026-11-10 21:57 UTC

This page contains 118 additions and 95 deletions.

2026
Tue 10 21:57

에이전트에서 구조화된 출력 얻기

JSON Schema, Zod 또는 Pydantic을 사용하여 에이전트 워크플로우에서 검증된 JSON을 반환합니다. 다중 턴 도구 사용 후 타입 안전 구조화된 데이터를 얻습니다.

구조화된 출력을 사용하면 에이전트에서 반환받을 데이터의 정확한 형태를 정의할 수 있습니다. 에이전트는 작업을 완료하기 위해 필요한 모든 도구를 사용할 수 있으며, 마지막에는 여전히 스키마와 일치하는 검증된 JSON을 얻습니다. 필요한 구조에 대한 JSON Schema를 정의하면, SDK가 출력을 검증하고 불일치 시 다시 프롬프트합니다. 재시도 제한 내에서 검증이 성공하지 않으면 구조화된 데이터 대신 오류가 발생합니다. 오류 처리를 참조하세요.

완전한 타입 안전성을 위해 Zod(TypeScript) 또는 Pydantic(Python)을 사용하여 스키마를 정의하고 강타입 객체를 반환받습니다.

구조화된 출력을 사용하는 이유

에이전트는 기본적으로 자유 형식 텍스트를 반환합니다. 이는 채팅에는 적합하지만 출력을 프로그래밍 방식으로 사용해야 하는 경우에는 적합하지 않습니다. 구조화된 출력을 사용하면 애플리케이션 로직, 데이터베이스 또는 UI 컴포넌트에 직접 전달할 수 있는 타입이 지정된 데이터를 얻을 수 있습니다.

에이전트가 웹을 검색하여 레시피를 가져오는 레시피 앱을 예로 들어 보겠습니다. 구조화된 출력이 없으면 직접 파싱해야 하는 자유 형식 텍스트를 받게 됩니다. 구조화된 출력을 사용하면 원하는 형태를 정의하고 앱에서 바로 사용할 수 있는 타입이 지정된 데이터를 받을 수 있습니다.

[accordion] 구조화된 출력 없이

Here's a classic chocolate chip cookie recipe!

**Chocolate Chip Cookies**
Prep time: 15 minutes | Cook time: 10 minutes

Ingredients:
- 2 1/4 cups all-purpose flour
- 1 cup butter, softened
...

이를 앱에서 사용하려면 제목을 파싱하고, "15 minutes"를 숫자로 변환하고, 재료와 조리 방법을 분리하고, 응답마다 일관되지 않은 형식을 처리해야 합니다.

[accordion] 구조화된 출력 사용

{
  "name": "Chocolate Chip Cookies",
  "prep_time_minutes": 15,
  "cook_time_minutes": 10,
  "ingredients": [
    { "item": "all-purpose flour", "amount": 2.25, "unit": "cups" },
    { "item": "butter, softened", "amount": 1, "unit": "cup" }
    // ...
  ],
  "steps": ["Preheat oven to 375°F", "Cream butter and sugar" /* ... */]
}

UI에서 바로 사용할 수 있는 타입이 지정된 데이터입니다.

빠른 시작

구조화된 출력을 사용하려면 원하는 데이터의 형태를 설명하는 JSON Schema를 정의한 다음, outputFormat 옵션(TypeScript) 또는 output_format 옵션(Python)을 통해 query()에 전달합니다. 에이전트가 작업을 마치면 결과 메시지에 스키마와 일치하는 검증된 데이터가 담긴 structured_output 필드가 포함됩니다.

이 페이지의 예제를 실행하기 전에 빠른 시작을 따라 Claude Agent SDK를 설치하세요. 아래 예제는 에이전트에게 Anthropic을 조사하여 회사 이름, 설립 연도, 본사 위치를 구조화된 출력으로 반환하도록 요청합니다.

import { query } from "@anthropic-ai/claude-agent-sdk";

// 반환받을 데이터의 형태를 정의합니다
const schema = {
  type: "object",
  properties: {
    company_name: { type: "string" },
    founded_year: { type: "number" },
    headquarters: { type: "string" }
  },
  required: ["company_name"]
};

try {
  for await (const message of query({
    prompt: "Research Anthropic and provide key company information",
    options: {
      outputFormat: {
        type: "json_schema",
        schema: schema
      }
    }
  })) {
    // 결과 메시지에는 검증된 데이터가 담긴 structured_output이 포함됩니다
    if (message.type === "result" && message.subtype === "success" && !message.is_error && message.structured_output) {
      console.log(message.structured_output);
      // { company_name: "Anthropic", founded_year: 2021, headquarters: "San Francisco, CA" }
    }
  }
} catch (error) {
  // 단일 실행 query()는 error_max_structured_output_retries와 같은
  // 오류 결과를 반환한 후 예외를 발생시킵니다. 오류 처리 섹션을 참조하세요.
  console.error(`Session ended with an error: ${error}`);
}
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage

# 반환받을 데이터의 형태를 정의합니다
schema = {
    "type": "object",
    "properties": {
        "company_name": {"type": "string"},
        "founded_year": {"type": "number"},
        "headquarters": {"type": "string"},
    },
    "required": ["company_name"],
}


async def main():
    try:
        async for message in query(
            prompt="Research Anthropic and provide key company information",
            options=ClaudeAgentOptions(
                output_format={"type": "json_schema", "schema": schema}
            ),
        ):
            # 결과 메시지에는 검증된 데이터가 담긴 structured_output이 포함됩니다
            if (
                isinstance(message, ResultMessage)
                and not message.is_error
                and message.structured_output
            ):
                print(message.structured_output)
                # {'company_name': 'Anthropic', 'founded_year': 2021, 'headquarters': 'San Francisco, CA'}
    except Exception as error:
        # 단일 실행 query()는 error_max_structured_output_retries와 같은
        # 오류 결과를 반환한 후 예외를 발생시킵니다. 오류 처리 섹션을 참조하세요.
        print(f"Session ended with an error: {error}")


asyncio.run(main())

Zod와 Pydantic을 사용한 타입 안전 스키마

JSON Schema를 직접 작성하는 대신 Zod(TypeScript) 또는 Pydantic(Python)을 사용하여 스키마를 정의할 수 있습니다. 이러한 라이브러리는 JSON Schema를 자동으로 생성해 주며, 응답을 완전히 타입이 지정된 객체로 파싱하여 자동 완성과 타입 검사를 활용하면서 코드베이스 전체에서 사용할 수 있게 해 줍니다.

아래 예제는 요약, 단계 목록(각 단계에 복잡도 수준 포함), 잠재적 위험 요소로 구성된 기능 구현 계획의 스키마를 정의합니다. 에이전트는 기능을 계획하고 타입이 지정된 FeaturePlan 객체를 반환합니다. 이후 완전한 타입 안전성을 유지하면서 plan.summary와 같은 속성에 접근하고 plan.steps를 순회할 수 있습니다.

SDK는 JSON Schema draft-07로 스키마를 검증하므로, 더 최신 버전을 선언하는 스키마는 거부됩니다. Zod는 기본적으로 draft 2020-12를 대상으로 하므로, 스키마를 변환할 때 target: "draft-7"을 전달해야 합니다.

import { z } from "zod";
import { query } from "@anthropic-ai/claude-agent-sdk";

// Zod로 스키마 정의
const FeaturePlan = z.object({
  feature_name: z.string(),
  summary: z.string(),
  steps: z.array(
    z.object({
      step_number: z.number(),
      description: z.string(),
      estimated_complexity: z.enum(["low", "medium", "high"])
    })
  ),
  risks: z.array(z.string())
});

type FeaturePlan = z.infer<typeof FeaturePlan>;

// SDK가 요구하는 draft-07 대상을 사용하여 JSON Schema로 변환
const schema = z.toJSONSchema(FeaturePlan, { target: "draft-7" });

// query에서 사용
try {
  for await (const message of query({
    prompt:
      "Plan how to add dark mode support to a React app. Break it into implementation steps.",
    options: {
      outputFormat: {
        type: "json_schema",
        schema: schema
      }
    }
  })) {
    if (message.type === "result" && message.subtype === "success" && !message.is_error && message.structured_output) {
      // 검증 후 완전히 타입이 지정된 결과 가져오기
      const parsed = FeaturePlan.safeParse(message.structured_output);
      if (parsed.success) {
        const plan: FeaturePlan = parsed.data;
        console.log(`Feature: ${plan.feature_name}`);
        console.log(`Summary: ${plan.summary}`);
        plan.steps.forEach((step) => {
          console.log(`${step.step_number}. [${step.estimated_complexity}] ${step.description}`);
        });
      }
    }
  }
} catch (error) {
  // 단일 실행 query()는 error_max_structured_output_retries와 같은 오류 결과를
  // 반환한 후 예외를 발생시킵니다. 오류 처리 섹션을 참조하세요.
  console.error(`Session ended with an error: ${error}`);
}
import asyncio
from pydantic import BaseModel
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


class Step(BaseModel):
    step_number: int
    description: str
    estimated_complexity: str  # 'low', 'medium', 'high'


class FeaturePlan(BaseModel):
    feature_name: str
    summary: str
    steps: list[Step]
    risks: list[str]


async def main():
    try:
        async for message in query(
            prompt="Plan how to add dark mode support to a React app. Break it into implementation steps.",
            options=ClaudeAgentOptions(
                output_format={
                    "type": "json_schema",
                    "schema": FeaturePlan.model_json_schema(),
                }
            ),
        ):
            if (
                isinstance(message, ResultMessage)
                and not message.is_error
                and message.structured_output
            ):
                # 검증 후 완전히 타입이 지정된 결과 가져오기
                plan = FeaturePlan.model_validate(message.structured_output)
                print(f"Feature: {plan.feature_name}")
                print(f"Summary: {plan.summary}")
                for step in plan.steps:
                    print(
                        f"{step.step_number}. [{step.estimated_complexity}] {step.description}"
                    )
    except Exception as error:
        # 단일 실행 query()는 error_max_structured_output_retries와 같은 오류 결과를
        # 반환한 후 예외를 발생시킵니다. 오류 처리 섹션을 참조하세요.
        print(f"Session ended with an error: {error}")


asyncio.run(main())

출력 형식 구성

outputFormat(TypeScript) 또는 output_format(Python) 옵션은 다음을 포함하는 객체를 허용합니다:

  • type: 구조화된 출력의 경우 "json_schema"로 설정
  • schema: 출력 구조를 정의하는 JSON Schema 객체입니다. Zod 스키마에서 z.toJSONSchema(schema, { target: "draft-7" })를 사용하거나 Pydantic 모델에서 .model_json_schema()를 사용하여 생성할 수 있습니다.

SDK는 모든 기본 타입(object, array, string, number, boolean, null), enum, const, required, 중첩 객체 및 $ref 정의를 포함한 표준 JSON Schema 기능을 지원합니다. 지원되는 기능 및 제한사항의 전체 목록은 JSON Schema 제한사항을 참조하세요.

유효한 JSON Schema가 아닌 스키마는 문제를 명시하는 오류와 함께 시작 시 실행에 실패합니다. v2.1.205 이전에는 유효하지 않은 스키마가 자동으로 무시되었고 에이전트가 구조화되지 않은 텍스트를 반환했습니다.

"format": "email"과 같은 format 키워드는 주석으로 허용되며 SDK의 유효성 검사기에 의해 적용되지 않습니다. v2.1.205 이전에는 format을 포함하는 모든 스키마가 유효하지 않은 것으로 처리되었습니다.

예제: TODO 추적 에이전트

이 예제는 구조화된 출력이 다단계 도구 사용과 함께 어떻게 작동하는지 보여 줍니다. 에이전트는 코드베이스에서 TODO 주석을 찾은 다음, 각 주석에 대한 git blame 정보를 조회해야 합니다. 에이전트는 사용할 도구(검색에는 Grep, git 명령 실행에는 Bash)를 자율적으로 결정하고, 그 결과를 하나의 구조화된 응답으로 결합합니다.

모든 파일에서 git blame 정보를 사용할 수 있는 것은 아니므로 스키마에는 선택적 필드(author 및 date)가 포함되어 있습니다. 에이전트는 찾을 수 있는 정보를 채우고 나머지는 생략합니다.

import { query } from "@anthropic-ai/claude-agent-sdk";

// TODO 추출을 위한 구조 정의
const todoSchema = {
  type: "object",
  properties: {
    todos: {
      type: "array",
      items: {
        type: "object",
        properties: {
          text: { type: "string" },
          file: { type: "string" },
          line: { type: "number" },
          author: { type: "string" },
          date: { type: "string" }
        },
        required: ["text", "file", "line"]
      }
    },
    total_count: { type: "number" }
  },
  required: ["todos", "total_count"]
};

// 에이전트는 Grep으로 TODO를 찾고, Bash로 git blame 정보를 가져옵니다
try {
  for await (const message of query({
    prompt: "Find all TODO comments in this codebase and identify who added them",
    options: {
      outputFormat: {
        type: "json_schema",
        schema: todoSchema
      }
    }
  })) {
    if (message.type === "result" && message.subtype === "success" && !message.is_error && message.structured_output) {
      const data = message.structured_output as { total_count: number; todos: Array<{ file: string; line: number; text: string; author?: string; date?: string }> };
      console.log(`Found ${data.total_count} TODOs`);
      data.todos.forEach((todo) => {
        console.log(`${todo.file}:${todo.line} - ${todo.text}`);
        if (todo.author) {
          console.log(`  Added by ${todo.author} on ${todo.date}`);
        }
      });
    }
  }
} catch (error) {
  // 단일 실행 query()는 error_max_structured_output_retries와 같은 오류 결과를
  // 반환한 후 예외를 발생시킵니다. 오류 처리 섹션을 참조하세요.
  console.error(`Session ended with an error: ${error}`);
}
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage

# TODO 추출을 위한 구조 정의
todo_schema = {
    "type": "object",
    "properties": {
        "todos": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "text": {"type": "string"},
                    "file": {"type": "string"},
                    "line": {"type": "number"},
                    "author": {"type": "string"},
                    "date": {"type": "string"},
                },
                "required": ["text", "file", "line"],
            },
        },
        "total_count": {"type": "number"},
    },
    "required": ["todos", "total_count"],
}


async def main():
    # 에이전트는 Grep으로 TODO를 찾고, Bash로 git blame 정보를 가져옵니다
    try:
        async for message in query(
            prompt="Find all TODO comments in this codebase and identify who added them",
            options=ClaudeAgentOptions(
                output_format={"type": "json_schema", "schema": todo_schema}
            ),
        ):
            if (
                isinstance(message, ResultMessage)
                and not message.is_error
                and message.structured_output
            ):
                data = message.structured_output
                print(f"Found {data['total_count']} TODOs")
                for todo in data["todos"]:
                    print(f"{todo['file']}:{todo['line']} - {todo['text']}")
                    if "author" in todo:
                        print(f"  Added by {todo['author']} on {todo['date']}")
    except Exception as error:
        # 단일 실행 query()는 error_max_structured_output_retries와 같은 오류 결과를
        # 반환한 후 예외를 발생시킵니다. 오류 처리 섹션을 참조하세요.
        print(f"Session ended with an error: {error}")


asyncio.run(main())

StructuredOutput 도구 호출 인식하기

TypeScript에서 outputFormat을 설정하거나 Python에서 output_format을 설정하면, 메시지 스트림에 직접 정의한 적 없는 StructuredOutput이라는 이름의 도구 호출이 포함됩니다. Claude Code는 이 도구를 세션에 추가하며, 에이전트는 이 도구를 호출하여 답변을 반환합니다. system init 메시지는 이 도구를 tools 배열에 나열하며, 각 호출은 두 개의 블록으로 전달됩니다.

  • StructuredOutput이라는 이름의 tool_use 블록으로, 입력값은 에이전트가 스키마에 맞춰 생성한 객체입니다
  • Structured output provided successfully라는 내용의 tool_result 블록

해당 객체가 스키마와 일치하지 않으면 tool_result에는 대신 is_error: true가 포함되고 에이전트가 StructuredOutput을 다시 호출하므로, 하나의 쿼리에 대해 스트림에 여러 호출이 포함될 수 있습니다. 검증된 데이터는 tool_use 블록이 아닌 result 메시지의 structured_output에서 읽어야 하며, 모든 시도가 실패했을 때 받게 되는 결과는 오류 처리를 참조하세요.

애플리케이션에서 도구 호출을 표시하거나 로그에 기록하는 경우, 이 메커니즘이 에이전트가 선택한 단계처럼 보이지 않도록 StructuredOutput이라는 이름의 호출은 건너뛰세요. tool_result 블록에는 도구 이름이 없고 응답 대상 호출의 tool_use_id만 포함되므로, 이를 건너뛴 각 호출의 id와 대조하세요.

오류 처리

에이전트가 스키마와 일치하는 유효한 JSON을 생성할 수 없으면 구조화된 출력 생성이 실패할 수 있습니다. 이는 일반적으로 스키마가 작업에 비해 너무 복잡하거나, 작업 자체가 모호하거나, 에이전트가 유효성 검사 오류를 수정하려다 재시도 한도에 도달할 때 발생합니다. 유효성 검사 실패 없이 발생할 수도 있습니다. 모델 폴백이 이미 완료된 출력을 스트림 도중에 철회할 수 있으며, 이를 대체하는 재시도가 없으면 실행은 동일한 오류로 종료됩니다. 스키마를 디버깅하기 전에 오류 결과 메시지의 errors 목록을 확인하여 두 원인을 구분하십시오.

결과 메시지의 subtype을 확인합니다.

Subtype 의미
success 실행이 오류 subtype 없이 종료되었습니다
error_max_structured_output_retries 여러 번 시도한 후에도 유효한 출력이 남지 않았습니다(유효성 검사 실패 또는 성공한 재시도가 없는 모델 폴백 철회)

success subtype만으로는 실행이 답변을 생성했다는 의미가 아닙니다. 결과의 is_error가 true이거나 structured_output이 없는 경우 해당 결과를 실패로 처리하십시오.

아래 예제는 이러한 검사를 적용합니다.

import { query } from "@anthropic-ai/claude-agent-sdk";

const contactSchema = {
  type: "object",
  properties: {
    name: { type: "string" },
    email: { type: "string" }
  },
  required: ["name"]
};

try {
  for await (const msg of query({
    prompt: "Extract contact info from the document",
    options: {
      outputFormat: {
        type: "json_schema",
        schema: contactSchema
      }
    }
  })) {
    if (msg.type === "result") {
      if (msg.subtype === "success" && !msg.is_error && msg.structured_output) {
        // 유효성 검사를 통과한 출력을 사용합니다
        console.log(msg.structured_output);
      } else if (msg.subtype === "error_max_structured_output_retries") {
        console.error("Could not produce valid output");
      } else {
        console.error("Run ended with an error or without a structured output");
      }
    }
  }
} catch (error) {
  // 단일 실행 query()는 오류 결과를 생성한 후 예외를 발생시킵니다. 실패가
  // 오류 결과였다면 위의 오류 subtype 분기가 이미 실행되었으며,
  // 연결 또는 프로세스 실패는 결과 메시지를 생성하지 않습니다.
  console.log(`Session ended with an error: ${error}`);
}
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage

contact_schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "email": {"type": "string"},
    },
    "required": ["name"],
}


async def main():
    try:
        async for message in query(
            prompt="Extract contact info from the document",
            options=ClaudeAgentOptions(
                output_format={"type": "json_schema", "schema": contact_schema}
            ),
        ):
            if isinstance(message, ResultMessage):
                if (
                    message.subtype == "success"
                    and not message.is_error
                    and message.structured_output
                ):
                    # 유효성 검사를 통과한 출력을 사용합니다
                    print(message.structured_output)
                elif message.subtype == "error_max_structured_output_retries":
                    print("Could not produce valid output")
                else:
                    print("Run ended with an error or without a structured output")
    except Exception as error:
        # 단일 실행 query()는 오류 결과를 생성한 후 예외를 발생시킵니다. 실패가
        # 오류 결과였다면 위의 오류 subtype 분기가 이미 실행되었으며,
        # 연결 또는 프로세스 실패는 결과 메시지를 생성하지 않습니다.
        print(f"Session ended with an error: {error}")


asyncio.run(main())

오류를 방지하기 위한 팁:

  • 스키마를 간결하게 유지하십시오. 필수 필드가 많고 깊게 중첩된 스키마는 충족하기가 더 어렵습니다. 단순하게 시작하고 필요에 따라 복잡성을 추가하십시오.
  • 스키마를 작업에 맞추십시오. 작업에 스키마가 요구하는 모든 정보가 없을 수 있다면 해당 필드를 선택 사항으로 만드십시오.
  • 명확한 프롬프트를 사용하십시오. 모호한 프롬프트는 에이전트가 어떤 출력을 생성해야 하는지 파악하기 어렵게 만듭니다.

관련 리소스

  • JSON Schema 문서: 중첩 객체, 배열, 열거형 및 검증 제약 조건이 있는 복잡한 스키마를 정의하기 위한 JSON Schema 구문 학습
  • API 구조화된 출력: 도구 사용 없이 단일 턴 요청에 대해 Claude API와 함께 직접 구조화된 출력 사용
  • 사용자 정의 도구: 구조화된 출력을 반환하기 전에 실행 중에 호출할 에이전트 사용자 정의 도구 제공