结构化输出
简介
结构化输出 API 提供了一种方式,确保大语言模型 (LLM) 的响应符合特定的数据结构。 这对于构建可靠的 AI 应用程序至关重要,因为在这类程序中,您需要可预测、格式良好的数据,而非自由格式的文本。
本页面将说明如何使用此 API 定义数据结构、生成架构 (schema) 以及向 LLM 请求结构化响应。
关键组件与概念
结构化输出 API 由几个关键组件组成:
- 数据结构定义:使用 kotlinx.serialization 和 LLM 特定注解进行标注的 Kotlin 数据类。
- JSON 架构生成:从 Kotlin 数据类生成 JSON 架构的工具。
- 结构化 LLM 请求:向 LLM 请求符合定义结构的响应的方法。
- 响应处理:处理和验证结构化响应。
定义数据结构
使用结构化输出 API 的第一步是使用 Kotlin 数据类定义您的数据结构。
基础结构
@Serializable
@SerialName("WeatherForecast")
@LLMDescription("给定地点的天气预报")
data class WeatherForecast(
@property:LLMDescription("摄氏温度")
val temperature: Int,
@property:LLMDescription("天气状况(例如:晴天、多云、下雨)")
val conditions: String,
@property:LLMDescription("降水概率百分比")
val precipitation: Int
)关键注解
@Serializable:kotlinx.serialization 处理该类所必需的。@SerialName:指定序列化时使用的名称。@LLMDescription:为 LLM 提供类的描述。对于字段注解,请使用@property:LLMDescription。
支持的功能
该 API 支持广泛的数据结构功能:
嵌套类
@Serializable
@SerialName("WeatherForecast")
data class WeatherForecast(
// 其他字段
@property:LLMDescription("地点的坐标")
val latLon: LatLon
) {
@Serializable
@SerialName("LatLon")
data class LatLon(
@property:LLMDescription("地点的纬度")
val lat: Double,
@property:LLMDescription("地点的经度")
val lon: Double
)
}集合(列表和映射)
@Serializable
@SerialName("WeatherForecast")
data class WeatherForecast(
// 其他字段
@property:LLMDescription("新闻文章列表")
val news: List<WeatherNews>,
@property:LLMDescription("天气源映射")
val sources: Map<String, WeatherSource>
)枚举
@Serializable
@SerialName("Pollution")
enum class Pollution { Low, Medium, High }使用密封类的多态
@Serializable
@SerialName("WeatherAlert")
sealed class WeatherAlert {
abstract val severity: Severity
abstract val message: String
@Serializable
@SerialName("Severity")
enum class Severity { Low, Moderate, Severe, Extreme }
@Serializable
@SerialName("StormAlert")
data class StormAlert(
override val severity: Severity,
override val message: String,
@property:LLMDescription("风速,单位 km/h")
val windSpeed: Double
) : WeatherAlert()
@Serializable
@SerialName("FloodAlert")
data class FloodAlert(
override val severity: Severity,
override val message: String,
@property:LLMDescription("预期降雨量,单位 mm")
val expectedRainfall: Double
) : WeatherAlert()
}提供示例
您可以提供示例来帮助 LLM 理解预期的格式:
val exampleForecasts = listOf(
WeatherForecast(
news = listOf(WeatherNews(0.0), WeatherNews(5.0)),
sources = mutableMapOf(
"openweathermap" to WeatherSource(Url("https://api.openweathermap.org/data/2.5/weather")),
"googleweather" to WeatherSource(Url("https://weather.google.com"))
)
// 其他字段
),
WeatherForecast(
news = listOf(WeatherNews(25.0), WeatherNews(35.0)),
sources = mutableMapOf(
"openweathermap" to WeatherSource(Url("https://api.openweathermap.org/data/2.5/weather")),
"googleweather" to WeatherSource(Url("https://weather.google.com"))
)
)
)请求结构化响应
在 Koog 中,您可以在三个主要层级使用结构化输出:
- 提示词执行器层:使用提示词执行器直接进行 LLM 调用
- 智能体 LLM 上下文层:在智能体会话中用于对话上下文
- 节点层:创建具有结构化输出能力的可复用智能体节点
第 1 层:提示词执行器
提示词执行器层提供了进行结构化 LLM 调用最直接的方式。对于单个独立的请求,请使用 executeStructured 方法:
此方法执行提示词并确保响应结构正确,其方式包括:
- 根据模型能力自动选择最佳的结构化输出方案
- 必要时在原始提示词中注入结构化输出指令
- 在可用时使用原生结构化输出支持
- 可选地,当解析失败时,通过辅助 LLM 提供自动错误修正(通过
fixingParser参数)
以下是使用 executeStructured 方法的示例:
// 定义一个简单的单供应商提示词执行器
val promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_KEY"))
// 进行返回结构化响应的 LLM 调用
val structuredResponse = promptExecutor.executeStructured<WeatherForecast>(
// 定义提示词(包括系统消息和用户消息)
prompt = prompt("structured-data") {
system(
"""
你是一个天气预报助手。
当被问及天气预报时,请提供一个真实但虚构的预报。
""".trimIndent()
)
user(
"阿姆斯特丹的天气预报是什么?"
)
},
// 定义执行请求的主模型
model = OpenAIModels.Chat.GPT4oMini,
// 可选:提供示例以帮助模型理解格式
examples = exampleForecasts,
// 可选:提供修复解析器用于错误修正
fixingParser = StructureFixingParser(
model = OpenAIModels.Chat.GPT4o,
retries = 3
)
)executeStructured 方法接受以下参数:
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
prompt | Prompt | 是 | 要执行的提示词。有关更多信息,请参阅提示词。 | |
model | LLModel | 是 | 执行提示词的主模型。 | |
examples | List<T> | 否 | emptyList() | 可选的示例列表,帮助模型理解预期的格式。 |
fixingParser | StructureFixingParser? | 否 | null | 可选的解析器,通过使用辅助 LLM 智能修复解析错误来处理格式错误的响应。提供后,将自动对解析失败的响应进行带错误修正的重试。 |
该方法返回一个 Result<StructuredResponse<T>>,其中包含成功解析的结构化数据或错误。
第 2 层:智能体 LLM 上下文
智能体 LLM 上下文层允许您在智能体会话中请求结构化响应。这对于构建在流程中特定点需要结构化数据的对话智能体非常有用。
在 writeSession 中使用 requestLLMStructured 方法进行基于智能体的交互:
val structuredResponse = llm.writeSession {
requestLLMStructured<WeatherForecast>(
examples = exampleForecasts,
fixingParser = StructureFixingParser(
model = OpenAIModels.Chat.GPT4o,
retries = 3
)
)
}fixingParser 参数为格式错误的 JSON 响应提供自动错误修正。当解析失败时,它会使用辅助 LLM 智能地修复响应,直到达到指定的重试次数。
StructureFixingParser 参数:
model: LLModel- 用于修复格式错误 JSON 输出的 LLMretries: Int- 最大修复尝试次数(默认值:3)prompt- 可选的自定义提示词函数,用于修复过程(默认为内置的修复提示词)
修复过程会迭代地将解析错误传递给辅助模型,该模型会尝试纠正 JSON,同时保留原始数据并进行最小限度的更改。
与智能体策略集成
您可以将结构化数据处理集成到您的智能体策略中:
val agentStrategy = strategy<String, String>("weather-forecast") {
val setup by nodeLLMRequest()
val getStructuredForecast by node<Message.Assistant, String> { _ ->
val structuredResponse = llm.writeSession {
requestLLMStructured<WeatherForecast>(
fixingParser = StructureFixingParser(
model = OpenAIModels.Chat.GPT4o,
retries = 3
)
)
}
"""
响应结构:
$structuredResponse
""".trimIndent()
}
edge(nodeStart forwardTo setup)
edge(setup forwardTo getStructuredForecast)
edge(getStructuredForecast forwardTo nodeFinish)
}第 3 层:节点层
节点层为智能体工作流中的结构化输出提供了最高级别的抽象。使用 nodeLLMRequestStructured 创建处理结构化数据的可复用智能体节点。
这会创建一个智能体节点,该节点:
- 接收
String输入(用户消息) - 将消息附加到 LLM 提示词
- 向 LLM 请求结构化输出
- 返回
Result<StructuredResponse<MyStruct>>
节点层示例
val agentStrategy = strategy<Unit, String>("weather-forecast") {
val setup by node<Unit, String> { _ ->
"请提供阿姆斯特丹的天气预报"
}
// 使用委托语法创建结构化输出节点
val getWeatherForecast by nodeLLMRequestStructured<WeatherForecast>(
name = "forecast-node",
examples = exampleForecasts,
fixingParser = StructureFixingParser(
model = OpenAIModels.Chat.GPT4o,
retries = 3
)
)
val processResult by node<Result<StructuredResponse<WeatherForecast>>, String> { result ->
when {
result.isSuccess -> {
val forecast = result.getOrNull()?.data
"天气预报:$forecast"
}
result.isFailure -> {
"获取结构化预报失败:${result.exceptionOrNull()?.message}"
}
else -> "未知结果状态"
}
}
edge(nodeStart forwardTo setup)
edge(setup forwardTo getWeatherForecast)
edge(getWeatherForecast forwardTo processResult)
edge(processResult forwardTo nodeFinish)
}完整代码示例
以下是使用结构化输出 API 的完整示例:
// 注意:为了简洁,省略了导入语句
@Serializable
@SerialName("SimpleWeatherForecast")
@LLMDescription("地点的简单天气预报")
data class SimpleWeatherForecast(
@property:LLMDescription("地点名称")
val location: String,
@property:LLMDescription("摄氏温度")
val temperature: Int,
@property:LLMDescription("天气状况(例如:晴天、多云、下雨)")
val conditions: String
)
val token = System.getenv("OPENAI_KEY") ?: error("未设置环境变量 OPENAI_KEY")
fun main(): Unit = runBlocking {
// 创建示例预报
val exampleForecasts = listOf(
SimpleWeatherForecast(
location = "纽约",
temperature = 25,
conditions = "晴天"
),
SimpleWeatherForecast(
location = "伦敦",
temperature = 18,
conditions = "多云"
)
)
// 生成 JSON 架构
val forecastStructure = JsonStructure.create<SimpleWeatherForecast>(
schemaGenerator = BasicJsonSchemaGenerator.Default,
examples = exampleForecasts
)
// 定义智能体策略
val agentStrategy = strategy<String, String>("weather-forecast") {
val setup by nodeLLMRequest()
val getStructuredForecast by node<Message.Assistant, String> { _ ->
val structuredResponse = llm.writeSession {
requestLLMStructured<SimpleWeatherForecast>()
}
"""
响应结构:
$structuredResponse
""".trimIndent()
}
edge(nodeStart forwardTo setup)
edge(setup forwardTo getStructuredForecast)
edge(getStructuredForecast forwardTo nodeFinish)
}
// 配置并运行智能体
val agentConfig = AIAgentConfig(
prompt = prompt("weather-forecast-prompt") {
system(
"""
你是一个天气预报助手。
当被问及天气预报时,请提供一个真实但虚构的预报。
""".trimIndent()
)
},
model = OpenAIModels.Chat.GPT4o,
maxAgentIterations = 5
)
val runner = AIAgent(
promptExecutor = simpleOpenAIExecutor(token),
toolRegistry = ToolRegistry.EMPTY,
strategy = agentStrategy,
agentConfig = agentConfig
)
runner.run("获取巴黎的天气预报")
}高级用法
上述示例展示了根据模型能力自动选择最佳结构化输出方案的简化 API。 为了更好地控制结构化输出过程,您可以使用带有手动架构创建和供应商特定配置的高级 API。
手动架构创建与配置
除了依赖自动架构生成外,您还可以使用 JsonStructure.create 显式创建架构,并通过 StructuredOutput 类手动配置结构化输出行为。
关键区别在于,您不再传递简单的参数(如 examples 和 fixingParser),而是创建一个 StructuredRequestConfig 对象,以便对以下各项进行精细控制:
- 架构生成:选择特定的生成器(标准版、基础版或供应商特定版)
- 输出模式:原生结构化输出支持对比手动提示
- 供应商映射:针对不同的 LLM 供应商使用不同的配置
- 回退策略:当供应商特定配置不可用时的默认行为
// 使用不同的生成器创建不同的架构结构
val genericStructure = JsonStructure.create<WeatherForecast>(
schemaGenerator = StandardJsonSchemaGenerator,
examples = exampleForecasts
)
val openAiStructure = JsonStructure.create<WeatherForecast>(
schemaGenerator = OpenAIBasicJsonSchemaGenerator,
examples = exampleForecasts
)
val anthropicStructure = JsonStructure.create<WeatherForecast>(
schemaGenerator = AnthropicBasicJsonSchemaGenerator,
examples = exampleForecasts
)
val promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_KEY"))
// 高级 API 使用 StructuredRequestConfig 而非简单参数
val structuredResponse = promptExecutor.executeStructured(
prompt = prompt("structured-data") {
system("你是一个天气预报助手。")
user("阿姆斯特丹的天气预报是什么?")
},
model = OpenAIModels.Chat.GPT4oMini,
config = StructuredRequestConfig(
byProvider = mapOf(
LLMProvider.OpenAI to StructuredRequest.Native(openAiStructure),
LLMProvider.Anthropic to StructuredRequest.Native(anthropicStructure),
),
default = StructuredRequest.Manual(genericStructure)
),
fixingParser = StructureFixingParser(
model = AnthropicModels.Haiku_4_5,
retries = 2
)
)架构生成器
根据您的需求,可以使用不同的架构生成器:
- 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(仅使用 examples 和 fixingParser 参数),而高级 API 则在需要额外控制时提供支持。
最佳做法
使用清晰的描述:使用
@LLMDescription注解提供清晰详细的描述,帮助 LLM 理解预期数据。提供示例:包含有效数据结构的示例以引导 LLM。
优雅处理错误:实现适当的错误处理,以应对 LLM 可能无法生成有效结构的情况。
使用合适的架构类型:根据您的需求和所使用的 LLM 的能力,选择合适的架构格式和类型。
使用不同模型进行测试:不同的 LLM 遵循结构化格式的能力各不相同,因此尽可能使用多个模型进行测试。
从简单开始:先从简单的结构开始,逐步根据需要增加复杂性。
谨慎使用多态:虽然 API 支持使用密封类的多态,但请注意,LLM 正确处理多态可能更具挑战性。