구조화된 출력

·3분 읽기

원문: Koog Documentation — structured-output 이 글은 Koog 공식 문서의 structured-output 페이지를 한국어로 옮긴 번역본입니다. 문서 구조와 링크 의미를 유지하되, MkDocs 전용 UI 문법은 블로그에서 읽기 좋도록 정리했습니다.

구조화된 출력

소개

구조화된 출력 API는 LLM(대형 언어 모델)의 응답을 보장하는 방법을 제공합니다. 특정 데이터 구조를 준수합니다. 이는 자유 형식 텍스트가 아닌 예측 가능하고 올바른 형식의 데이터가 필요한 안정적인 AI 애플리케이션을 구축하는 데 중요합니다.

이 페이지에서는 이 API를 사용하여 데이터 구조를 정의하고, 스키마를 생성하고, LLM의 구조화된 응답을 요청합니다.

주요 구성 요소 및 개념

구조화된 출력 API는 다음과 같은 몇 가지 주요 구성 요소로 구성됩니다.

  1. 데이터 구조 정의: kotlinx.serialization 및 LLM 관련 주석으로 주석이 달린 Kotlin 데이터 클래스입니다.
  2. JSON 스키마 생성: Kotlin 데이터 클래스에서 JSON 스키마를 생성하는 도구입니다.
  3. 구조화된 LLM 요청: 정의된 구조를 준수하는 LLM의 응답을 요청하는 방법입니다.
  4. 응답 처리: 구조화된 응답을 처리하고 검증합니다.

데이터 구조 정의

Structured Output API를 사용하는 첫 번째 단계는 Kotlin 데이터 클래스를 사용하여 데이터 구조를 정의하는 것입니다.

기본 구조

1@Serializable2@SerialName("WeatherForecast")3@LLMDescription("Weather forecast for a given location")4data class WeatherForecast(5    @property:LLMDescription("Temperature in Celsius")6    val temperature: Int,7    @property:LLMDescription("Weather conditions (e.g., sunny, cloudy, rainy)")8    val conditions: String,9    @property:LLMDescription("Chance of precipitation in percentage")10    val precipitation: Int11)

주요 주석

  • @Serializable: kotlinx.serialization이 클래스와 작동하는 데 필요합니다.
  • @SerialName: 직렬화 중에 사용할 이름을 지정합니다.
  • @LLMDescription: LLM 클래스에 대한 설명을 제공합니다. 필드 주석의 경우 @property:LLMDescription을 사용하십시오.

지원되는 기능

API는 광범위한 데이터 구조 기능을 지원합니다.

중첩 클래스

1@Serializable2@SerialName("WeatherForecast")3data class WeatherForecast(4    // Other fields5    @property:LLMDescription("Coordinates of the location")6    val latLon: LatLon7) {8    @Serializable9    @SerialName("LatLon")10    data class LatLon(11        @property:LLMDescription("Latitude of the location")12        val lat: Double,13        @property:LLMDescription("Longitude of the location")14        val lon: Double15    )16}

컬렉션(목록 및 지도)

1@Serializable2@SerialName("WeatherForecast")3data class WeatherForecast(4    // Other fields5    @property:LLMDescription("List of news articles")6    val news: List<WeatherNews>,7    @property:LLMDescription("Map of weather sources")8    val sources: Map<String, WeatherSource>9)

열거형

1@Serializable2@SerialName("Pollution")3enum class Pollution { Low, Medium, High }

봉인된 클래스를 사용한 다형성

1@Serializable2@SerialName("WeatherAlert")3sealed class WeatherAlert {4    abstract val severity: Severity5    abstract val message: String67    @Serializable8    @SerialName("Severity")9    enum class Severity { Low, Moderate, Severe, Extreme }1011    @Serializable12    @SerialName("StormAlert")13    data class StormAlert(14        override val severity: Severity,15        override val message: String,16        @property:LLMDescription("Wind speed in km/h")17        val windSpeed: Double18    ) : WeatherAlert()1920    @Serializable21    @SerialName("FloodAlert")22    data class FloodAlert(23        override val severity: Severity,24        override val message: String,25        @property:LLMDescription("Expected rainfall in mm")26        val expectedRainfall: Double27    ) : WeatherAlert()28}

예시 제공

LLM이 예상 형식을 이해하는 데 도움이 되는 예를 제공할 수 있습니다.

1val exampleForecasts = listOf(2  WeatherForecast(3    news = listOf(WeatherNews(0.0), WeatherNews(5.0)),4    sources = mutableMapOf(5      "openweathermap" to WeatherSource(Url("https://api.openweathermap.org/data/2.5/weather")),6      "googleweather" to WeatherSource(Url("https://weather.google.com"))7    )8    // Other fields9  ),10  WeatherForecast(11    news = listOf(WeatherNews(25.0), WeatherNews(35.0)),12    sources = mutableMapOf(13      "openweathermap" to WeatherSource(Url("https://api.openweathermap.org/data/2.5/weather")),14      "googleweather" to WeatherSource(Url("https://weather.google.com"))15    )16  )17)18

구조화된 응답 요청

Koog에서 구조화된 출력을 사용할 수 있는 세 가지 주요 레이어가 있습니다.

  1. 프롬프트 실행기 레이어: 프롬프트 실행기를 사용하여 직접 LLM 호출하기
  2. 에이전트 LLM 컨텍스트 레이어: 대화 컨텍스트를 위해 에이전트 세션 내에서 사용
  3. 노드 레이어: 구조화된 출력 기능을 갖춘 재사용 가능한 에이전트 노드 생성

레이어 1: 프롬프트 실행자

프롬프트 실행기 계층은 구조화된 LLM 호출을 수행하는 가장 직접적인 방법을 제공합니다. 단일 독립형 요청에는 executeStructured 메서드를 사용합니다.

이 메서드는 프롬프트를 실행하고 다음을 통해 응답이 적절하게 구성되었는지 확인합니다.

  • model capabilities을 기반으로 최상의 구조화된 출력 접근 방식을 자동으로 선택합니다.
  • 필요할 때 구조화된 출력 지침을 원래 프롬프트에 삽입
  • 가능한 경우 기본 구조화된 출력 지원 사용
  • 선택적으로 구문 분석 실패 시 보조 LLM을 통해 자동 오류 수정 제공(fixingParser 매개변수를 통해)

다음은 executeStructured 방법을 사용하는 예입니다.

1// Define a simple, single-provider prompt executor2val promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_KEY"))34// Make an LLM call that returns a structured response5val structuredResponse = promptExecutor.executeStructured<WeatherForecast>(6        // Define the prompt (both system and user messages)7        prompt = prompt("structured-data") {8            system(9                """10                You are a weather forecasting assistant.11                When asked for a weather forecast, provide a realistic but fictional forecast.12                """.trimIndent()13            )14            user(15              "What is the weather forecast for Amsterdam?"16            )17        },18        // Define the main model that will execute the request19        model = OpenAIModels.Chat.GPT4oMini,20        // Optional: provide examples to help the model understand the format21        examples = exampleForecasts,22        // Optional: provide a fixing parser for error correction23        fixingParser = StructureFixingParser(24            model = OpenAIModels.Chat.GPT4o,25            retries = 326        )27    )

executeStructured 메서드는 다음 인수를 사용합니다.

이름 데이터 유형 필수의 기본 설명
prompt 즉각적인 실행할 프롬프트입니다. 자세한 내용은 Prompts을 참조하세요.
model LL모델 프롬프트를 실행할 메인 모델입니다.
examples 목록 아니요 emptyList() 모델이 예상 형식을 이해하는 데 도움이 되는 선택적 예제 목록입니다.
fixingParser 구조고정파서? 아니요 null 구문 분석 오류를 지능적으로 수정하기 위해 보조 LLM을 사용하여 잘못된 응답을 처리하는 선택적 구문 분석기입니다. 제공되면 오류 수정을 통해 실패한 구문 분석을 자동으로 재시도합니다.

이 메서드는 성공적으로 구문 분석된 구조화된 데이터 또는 오류가 포함된 Result<StructuredResponse<T>>를 반환합니다.

계층 2: 에이전트 LLM 컨텍스트

에이전트 LLM 컨텍스트 계층을 사용하면 에이전트 세션 내에서 구조화된 응답을 요청할 수 있습니다. 이는 흐름의 특정 지점에서 구조화된 데이터가 필요한 대화형 에이전트를 구축하는 데 유용합니다.

에이전트 기반 상호 작용을 위해 writeSession 내에서 requestLLMStructured 방법을 사용합니다.

1val structuredResponse = llm.writeSession {2    requestLLMStructured<WeatherForecast>(3        examples = exampleForecasts,4        fixingParser = StructureFixingParser(5            model = OpenAIModels.Chat.GPT4o,6            retries = 37        )8    )9}

fixingParser 매개변수는 잘못된 JSON 응답에 대한 자동 오류 수정 기능을 제공합니다. 구문 분석이 실패하면 보조 LLM을 사용하여 지정된 재시도 횟수까지 응답을 지능적으로 수정합니다.

StructureFixingParser 매개변수:

  • model: LLModel - 잘못된 JSON 출력을 수정하는 데 사용되는 LLM
  • retries: Int - 최대 수정 시도 횟수(기본값: 3)
  • prompt - 수정 프로세스를 위한 선택적 사용자 정의 프롬프트 기능(기본값은 내장 수정 프롬프트로 설정됨)

수정 프로세스는 구문 분석 오류를 반복적으로 보조 모델에 전달합니다. 보조 모델은 원본 데이터를 보존하고 최소한의 변경을 하면서 JSON을 수정하려고 시도합니다.

상담원 전략과 통합

구조화된 데이터 처리를 에이전트 전략에 통합할 수 있습니다.

1val agentStrategy = strategy("weather-forecast") {2    val setup by nodeLLMRequest()34    val getStructuredForecast by node<Message.Response, String> { _ ->5        val structuredResponse = llm.writeSession {6            requestLLMStructured<WeatherForecast>(7                fixingParser = StructureFixingParser(8                    model = OpenAIModels.Chat.GPT4o,9                    retries = 310                )11            )12        }1314        """15        Response structure:16        $structuredResponse17        """.trimIndent()18    }1920    edge(nodeStart forwardTo setup)21    edge(setup forwardTo getStructuredForecast)22    edge(getStructuredForecast forwardTo nodeFinish)23}

레이어 3: 노드 레이어

노드 계층은 에이전트 워크플로의 구조화된 출력에 대해 최고 수준의 추상화를 제공합니다. 구조화된 데이터를 처리하는 재사용 가능한 에이전트 노드를 생성하려면 nodeLLMRequestStructured을 사용하십시오.

그러면 다음과 같은 에이전트 노드가 생성됩니다.

  • String 입력 허용(사용자 메시지)
  • LLM 프롬프트에 메시지를 추가합니다.
  • LLM에서 구조화된 출력을 요청합니다.
  • 반품 Result<StructuredResponse<MyStruct>>

노드 레이어 예시

1val agentStrategy = strategy("weather-forecast") {2    val setup by node<Unit, String> { _ ->3        "Please provide a weather forecast for Amsterdam"4    }5    6    // Create a structured output node using delegate syntax7    val getWeatherForecast by nodeLLMRequestStructured<WeatherForecast>(8        name = "forecast-node",9        examples = exampleForecasts,10        fixingParser = StructureFixingParser(11            model = OpenAIModels.Chat.GPT4o,12            retries = 313        )14    )15    16    val processResult by node<Result<StructuredResponse<WeatherForecast>>, String> { result ->17        when {18            result.isSuccess -> {19                val forecast = result.getOrNull()?.data20                "Weather forecast: $forecast"21            }22            result.isFailure -> {23                "Failed to get structured forecast: ${result.exceptionOrNull()?.message}"24            }25            else -> "Unknown result state"26        }27    }2829    edge(nodeStart forwardTo setup)30    edge(setup forwardTo getWeatherForecast)31    edge(getWeatherForecast forwardTo processResult)32    edge(processResult forwardTo nodeFinish)33}

전체 코드 샘플

다음은 구조화된 출력 API를 사용하는 전체 예입니다.

1// Note: Import statements are omitted for brevity2@Serializable3@SerialName("SimpleWeatherForecast")4@LLMDescription("Simple weather forecast for a location")5data class SimpleWeatherForecast(6    @property:LLMDescription("Location name")7    val location: String,8    @property:LLMDescription("Temperature in Celsius")9    val temperature: Int,10    @property:LLMDescription("Weather conditions (e.g., sunny, cloudy, rainy)")11    val conditions: String12)1314val token = System.getenv("OPENAI_KEY") ?: error("Environment variable OPENAI_KEY is not set")1516fun main(): Unit = runBlocking {17    // Create sample forecasts18    val exampleForecasts = listOf(19        SimpleWeatherForecast(20            location = "New York",21            temperature = 25,22            conditions = "Sunny"23        ),24        SimpleWeatherForecast(25            location = "London",26            temperature = 18,27            conditions = "Cloudy"28        )29    )3031    // Generate JSON Schema32    val forecastStructure = JsonStructure.create<SimpleWeatherForecast>(33        schemaGenerator = BasicJsonSchemaGenerator.Default,34        examples = exampleForecasts35    )3637    // Define the agent strategy38    val agentStrategy = strategy("weather-forecast") {39        val setup by nodeLLMRequest()40  41        val getStructuredForecast by node<Message.Response, String> { _ ->42            val structuredResponse = llm.writeSession {43                requestLLMStructured<SimpleWeatherForecast>()44            }45  46            """47            Response structure:48            $structuredResponse49            """.trimIndent()50        }51  52        edge(nodeStart forwardTo setup)53        edge(setup forwardTo getStructuredForecast)54        edge(getStructuredForecast forwardTo nodeFinish)55    }565758    // Configure and run the agent59    val agentConfig = AIAgentConfig(60        prompt = prompt("weather-forecast-prompt") {61            system(62                """63                You are a weather forecasting assistant.64                When asked for a weather forecast, provide a realistic but fictional forecast.65                """.trimIndent()66            )67        },68        model = OpenAIModels.Chat.GPT4o,69        maxAgentIterations = 570    )7172    val runner = AIAgent(73        promptExecutor = simpleOpenAIExecutor(token),74        toolRegistry = ToolRegistry.EMPTY,75        strategy = agentStrategy,76        agentConfig = agentConfig77    )7879    runner.run("Get weather forecast for Paris")80}

고급 사용법

위의 예는 모델 기능을 기반으로 최상의 구조화된 출력 접근 방식을 자동으로 선택하는 단순화된 API를 보여줍니다. 구조화된 출력 프로세스를 더 효과적으로 제어하려면 수동 스키마 생성 및 공급자별 구성이 포함된 고급 API를 사용할 수 있습니다.

수동 스키마 생성 및 구성

자동 스키마 생성에 의존하는 대신 JsonStructure.create을 사용하여 명시적으로 스키마를 생성하고 StructuredOutput 클래스를 통해 구조화된 출력 동작을 수동으로 구성할 수 있습니다.

주요 차이점은 examplesfixingParser과 같은 간단한 매개변수를 전달하는 대신 다음을 세밀하게 제어할 수 있는 StructuredRequestConfig 개체를 생성한다는 것입니다.

  • 스키마 생성: 특정 생성기 선택(표준, 기본 또는 공급자별)
  • 출력 모드: 기본 구조화된 출력 지원 및 수동 프롬프트
  • 공급업체 매핑: 다양한 LLM 제공업체에 대한 다양한 구성
  • 대체 전략: 공급자별 구성을 사용할 수 없는 경우의 기본 동작
1// Create different schema structures with different generators2val genericStructure = JsonStructure.create<WeatherForecast>(3    schemaGenerator = StandardJsonSchemaGenerator,4    examples = exampleForecasts5)67val openAiStructure = JsonStructure.create<WeatherForecast>(8    schemaGenerator = OpenAIBasicJsonSchemaGenerator,9    examples = exampleForecasts10)1112val anthropicStructure = JsonStructure.create<WeatherForecast>(13    schemaGenerator = AnthropicBasicJsonSchemaGenerator,14    examples = exampleForecasts15)1617val promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_KEY"))1819// The advanced API uses StructuredRequestConfig instead of simple parameters20val structuredResponse = promptExecutor.executeStructured(21    prompt = prompt("structured-data") {22        system("You are a weather forecasting assistant.")23        user("What is the weather forecast for Amsterdam?")24    },25    model = OpenAIModels.Chat.GPT4oMini,26    config = StructuredRequestConfig(27        byProvider = mapOf(28            LLMProvider.OpenAI to StructuredRequest.Native(openAiStructure),29            LLMProvider.Anthropic to StructuredRequest.Native(anthropicStructure),30        ),31        default = StructuredRequest.Manual(genericStructure)32    ),33    fixingParser = StructureFixingParser(34        model = AnthropicModels.Haiku_4_5,35        retries = 236    )37)

스키마 생성기

필요에 따라 다양한 스키마 생성기를 사용할 수 있습니다.

  • StandardJsonSchemaGenerator: 다형성, 정의 및 재귀 참조를 지원하는 전체 JSON 스키마
  • BasicJsonSchemaGenerator: 다형성을 지원하지 않는 단순화된 스키마, 더 많은 모델과 호환 가능
  • 제공자별 생성기: 특정 LLM 제공자(OpenAI, Anthropic, Google 등)에 최적화된 스키마

모든 계층에서의 사용량

고급 구성은 API의 세 가지 계층 모두에서 일관되게 작동합니다. 메소드 이름은 동일하게 유지되며 매개변수만 간단한 인수에서 고급 StructuredRequestConfig로 변경됩니다.

  • 신속 실행자: executeStructured(prompt, model, config: StructuredRequestConfig<T>)
  • 에이전트 LLM 컨텍스트: requestLLMStructured(config: StructuredRequestConfig<T>)
  • 노드 레이어: nodeLLMRequestStructured(config: StructuredRequestConfig<T>)

단순화된 API(examplesfixingParser 매개변수만 사용)는 대부분의 사용 사례에 권장되는 반면 고급 API는 필요할 경우 추가 제어 기능을 제공합니다.

모범 사례

  1. 명확한 설명 사용: LLM이 예상 데이터를 이해하는 데 도움이 되도록 @LLMDescription 주석을 사용하여 명확하고 자세한 설명을 제공합니다.

  2. 예제 제공: LLM을 안내하는 유효한 데이터 구조의 예를 포함합니다.

  3. 순조롭게 오류 처리: LLM이 유효한 구조를 생성하지 못하는 경우를 처리하기 위해 적절한 오류 처리를 구현합니다.

  4. 적절한 스키마 유형 사용: 귀하의 필요와 사용 중인 LLM의 기능에 따라 적절한 스키마 형식과 유형을 선택하십시오.

  5. 다양한 모델로 테스트: 서로 다른 LLM은 구조화된 형식을 따르는 다양한 능력을 가질 수 있으므로 가능하면 여러 모델로 테스트하세요.

  6. 간단하게 시작: 간단한 구조로 시작하고 필요에 따라 점차적으로 복잡성을 추가합니다.

  7. 다형성을 신중하게 사용하세요: API는 봉인된 클래스를 사용하여 다형성을 지원하지만 LLM이 올바르게 처리하는 것이 더 어려울 수 있다는 점에 유의하세요.