Structured Outputs
# 结构化输出
“结构化输出”一词具有多重含义,可能指代以下两种情况:
- LLM 生成结构化格式输出的通用能力(这是本页内容所涉及的)
- OpenAI 的结构化输出 (opens new window)功能,适用于响应格式和工具(函数调用)。
许多 LLM 和 LLM 提供商支持生成结构化格式的输出,通常是 JSON 格式。这些输出可以轻松映射到 Java 对象,并在应用程序的其他部分使用。
例如,假设我们有一个 Person
类:
record Person(String name, int age, double height, boolean married) {
}
2
我们想从以下非结构化文本中提取一个 Person
对象:
John is 42 years old and lives an independent life.
He stands 1.75 meters tall and carries himself with confidence.
Currently unmarried, he enjoys the freedom to focus on his personal goals and interests.
2
3
根据 LLM 和提供商的不同,目前有四种方法可以实现这一目标(按可靠性排序):
# JSON Schema
一些 LLM 提供商(目前包括 OpenAI、Google AI Gemini 和 Ollama)允许为期望的输出指定 JSON schema (opens new window)。您可以在此处 (opens new window)查看所有支持 JSON Schema 的 LLM 提供商。
当在请求中指定 JSON schema 时,LLM 需要生成符合该 schema 的输出。
请注意,JSON schema 是通过请求中专用的属性指定的,而不需要在提示(例如系统或用户消息)中包含任何自由格式的指令。
LangChain4j 在低级 ChatLanguageModel
API 和高级 AI 服务 API 中都支持 JSON Schema 功能。
# 在 ChatLanguageModel
中使用 JSON Schema
在低级的 ChatLanguageModel
API 中,可以使用与 LLM 提供商无关的 ResponseFormat
和 JsonSchema
来指定 JSON schema,创建 ChatRequest
:
ResponseFormat responseFormat = ResponseFormat.builder()
.type(JSON) // 类型可以是 TEXT(默认)或 JSON
.jsonSchema(JsonSchema.builder()
.name("Person") // OpenAI 要求指定 schema 名称
.rootElement(JsonObjectSchema.builder() // 见 [1] 下文
.addStringProperty("name")
.addIntegerProperty("age")
.addNumberProperty("height")
.addBooleanProperty("married")
.required("name", "age", "height", "married") // 见 [2] 下文
.build())
.build())
.build();
UserMessage userMessage = UserMessage.from("""
John is 42 years old and lives an independent life.
He stands 1.75 meters tall and carries himself with confidence.
Currently unmarried, he enjoys the freedom to focus on his personal goals and interests.
""");
ChatRequest chatRequest = ChatRequest.builder()
.responseFormat(responseFormat)
.messages(userMessage)
.build();
ChatLanguageModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.logRequests(true)
.logResponses(true)
.build();
// 或
ChatLanguageModel chatModel = GoogleAiGeminiChatModel.builder()
.apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY"))
.modelName("gemini-1.5-flash")
.logRequestsAndResponses(true)
.build();
// 或
ChatLanguageModel chatModel = OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("llama3.1")
.logRequests(true)
.logResponses(true)
.build();
ChatResponse chatResponse = chatModel.chat(chatRequest);
String output = chatResponse.aiMessage().text();
System.out.println(output); // {"name":"John","age":42,"height":1.75,"married":false}
Person person = new ObjectMapper().readValue(output, Person.class);
System.out.println(person); // Person[name=John, age=42, height=1.75, married=false]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
注意:
- [1] - 在大多数情况下,根元素必须是
JsonObjectSchema
类型,然而 Gemini 也支持JsonEnumSchema
和JsonArraySchema
。 - [2] - 必填属性必须明确指定,否则视为可选属性。
JSON schema 的结构通过 JsonSchemaElement
接口定义,其子类型如下:
JsonObjectSchema
- 用于对象类型。JsonStringSchema
- 用于String
、char
/Character
类型。JsonIntegerSchema
- 用于int
/Integer
、long
/Long
、BigInteger
类型。JsonNumberSchema
- 用于float
/Float
、double
/Double
、BigDecimal
类型。JsonBooleanSchema
- 用于boolean
/Boolean
类型。JsonEnumSchema
- 用于enum
类型。JsonArraySchema
- 用于数组和集合(例如List
、Set
)。JsonReferenceSchema
- 支持递归(例如,Person
类有一个Set<Person> children
字段)。JsonAnyOfSchema
- 支持多态(例如,Shape
可以是Circle
或Rectangle
)。
# JsonObjectSchema
JsonObjectSchema
代表一个具有嵌套属性的对象,通常是 JSON schema 的根元素。
可以通过以下几种方式将属性添加到 JsonObjectSchema
中:
- 使用
properties(Map<String, JsonSchemaElement> properties)
方法一次性添加所有属性:
JsonSchemaElement citySchema = JsonStringSchema.builder()
.description("The city for which the weather forecast should be returned")
.build();
JsonSchemaElement temperatureUnitSchema = JsonEnumSchema.builder()
.enumValues("CELSIUS", "FAHRENHEIT")
.build();
Map<String, JsonSchemaElement> properties = Map.of(
"city", citySchema,
"temperatureUnit", temperatureUnitSchema
);
JsonSchemaElement rootElement = JsonObjectSchema.builder()
.properties(properties)
.required("city") // 必填属性需要明确指定
.build();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 使用
addProperty(String name, JsonSchemaElement jsonSchemaElement)
方法逐个添加属性:
JsonSchemaElement rootElement = JsonObjectSchema.builder()
.addProperty("city", citySchema)
.addProperty("temperatureUnit", temperatureUnitSchema)
.required("city")
.build();
2
3
4
5
- 使用
add{Type}Property(String name)
或add{Type}Property(String name, String description)
方法逐个添加属性:
JsonSchemaElement rootElement = JsonObjectSchema.builder()
.addStringProperty("city", "The city for which the weather forecast should be returned")
.addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
.required("city")
.build();
2
3
4
5
更多细节请参阅 JsonObjectSchema (opens new window) 的 Javadoc。
# JsonStringSchema
创建 JsonStringSchema
的示例:
JsonSchemaElement stringSchema = JsonStringSchema.builder()
.description("The name of the person")
.build();
2
3
# JsonIntegerSchema
创建 JsonIntegerSchema
的示例:
JsonSchemaElement integerSchema = JsonIntegerSchema.builder()
.description("The age of the person")
.build();
2
3
# JsonNumberSchema
创建 JsonNumberSchema
的示例:
JsonSchemaElement numberSchema = JsonNumberSchema.builder()
.description("The height of the person")
.build();
2
3
# JsonBooleanSchema
创建 JsonBooleanSchema
的示例:
JsonSchemaElement booleanSchema = JsonBooleanSchema.builder()
.description("Is the person married?")
.build();
2
3
# JsonEnumSchema
创建 JsonEnumSchema
的示例:
JsonSchemaElement enumSchema = JsonEnumSchema.builder()
.description("Marital status of the person")
.enumValues(List.of("SINGLE", "MARRIED", "DIVORCED"))
.build();
2
3
4
# JsonArraySchema
创建 JsonArraySchema
来定义字符串数组的示例:
JsonSchemaElement itemSchema = JsonStringSchema.builder()
.description("The name of the person")
.build();
JsonSchemaElement arraySchema = JsonArraySchema.builder()
.description("All names of the people found in the text")
.items(itemSchema)
.build();
2
3
4
5
6
7
8
# JsonReferenceSchema
JsonReferenceSchema
可以用于支持递归:
String reference = "person"; // reference 应在 schema 中唯一
JsonObjectSchema jsonObjectSchema = JsonObjectSchema.builder()
.addStringProperty("name")
.addProperty("children", JsonArraySchema.builder()
.items(JsonReferenceSchema.builder()
.reference(reference)
.build())
.build())
.required("name", "children")
.definitions(Map.of(reference, JsonObjectSchema.builder()
.addStringProperty("name")
.addProperty("children", JsonArraySchema.builder()
.items(JsonReferenceSchema.builder()
.reference(reference)
.build())
.build())
.required("name", "children")
.build()))
.build();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
JsonReferenceSchema
当前仅由 OpenAI 支持。
# JsonAnyOfSchema
JsonAnyOfSchema
可用于支持多态:
JsonSchemaElement circleSchema = JsonObjectSchema.builder()
.addNumberProperty("radius")
.build();
JsonSchemaElement rectangleSchema = JsonObjectSchema.builder()
.addNumberProperty("width")
.addNumberProperty("height")
.build();
JsonSchemaElement shapeSchema = JsonAnyOfSchema.builder()
.anyOf(circleSchema, rectangleSchema)
.build();
JsonSchema jsonSchema = JsonSchema.builder()
.name("Shapes")
.rootElement(JsonObjectSchema.builder()
.addProperty("shapes", JsonArraySchema.builder()
.items(shapeSchema)
.build())
.required(List.of("shapes"))
.build())
.build();
ResponseFormat responseFormat = ResponseFormat.builder()
.type(ResponseFormatType.JSON)
.jsonSchema(jsonSchema)
.build();
UserMessage userMessage = UserMessage.from("""
Extract information from the following text:
1. A circle with a radius of 5
2. A rectangle with a width of 10 and a height of 20
""");
ChatRequest chatRequest = ChatRequest.builder()
.messages(userMessage)
.responseFormat(responseFormat)
.build();
ChatResponse chatResponse = model.chat(chatRequest);
System.out.println(chatResponse.aiMessage().text()); // {"shapes":[{"radius":5},{"width":10,"height":20}]}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
JsonAnyOfSchema
当前仅由 OpenAI 支持。
# 添加描述
所有的 JsonSchemaElement
子类型(除 JsonReferenceSchema
外)都有一个 description
属性。 如果 LLM 没有提供期望的输出,可以提供描述,以向 LLM 提供更多的指令和正确输出的示例,例如:
JsonSchemaElement stringSchema = JsonStringSchema.builder()
.description("The name of the person, for example: John Doe")
.build();
2
3
# 限制
使用 JSON Schema 与 ChatLanguageModel
时,有一些限制:
- 它仅与支持的 OpenAI、Google AI Gemini 和 Ollama 模型兼容。
- 它在 OpenAI 的流式模式 (opens new window)下尚不支持。 对于 Google AI Gemini 和 Ollama,可以在创建/构建模型时通过
responseSchema(...)
指定 JSON Schema。 JsonReferenceSchema
和JsonAnyOfSchema
当前仅由 OpenAI 支持。
# 使用 JSON Schema 与 AI 服务
当使用 AI 服务 (opens new window) 时,可以更轻松地实现相同的功能,且代码更简洁:
interface PersonExtractor {
Person extractPersonFrom(String text);
}
ChatLanguageModel chatModel = OpenAiChatModel.builder() // 参见 [1] 下文
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.responseFormat("json_schema") // 参见 [2] 下文
.strictJsonSchema(true) // 参见 [2] 下文
.logRequests(true)
.logResponses(true)
.build();
// 或者
ChatLanguageModel chatModel = GoogleAiGeminiChatModel.builder() // 参见 [1] 下文
.apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY"))
.modelName("gemini-1.5-flash")
.responseFormat(ResponseFormat.JSON) // 参见 [3] 下文
.logRequestsAndResponses(true)
.build();
// 或者
ChatLanguageModel chatModel = OllamaChatModel.builder() // 参见 [1] 下文
.baseUrl("http://localhost:11434")
.modelName("llama3.1")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // 参见 [4] 下文
.logRequests(true)
.logResponses(true)
.build();
PersonExtractor personExtractor = AiServices.create(PersonExtractor.class, chatModel); // 参见 [1] 下文
String text = """
John is 42 years old and lives an independent life.
He stands 1.75 meters tall and carries himself with confidence.
Currently unmarried, he enjoys the freedom to focus on his personal goals and interests.
""";
Person person = personExtractor.extractPersonFrom(text);
System.out.println(person); // Person[name=John, age=42, height=1.75, married=false]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
注意:
- [1] - 在 Quarkus 或 Spring Boot 应用程序中,无需显式创建
ChatLanguageModel
和 AI 服务, 这些 bean 会自动创建。更多信息: for Quarkus (opens new window), for Spring Boot (opens new window)。 - [2] - 这要求启用 OpenAI 的 JSON Schema 功能,更多细节请见 这里 (opens new window)。
- [3] - 这要求启用 Google AI Gemini (opens new window) 的 JSON Schema 功能。
- [4] - 这要求启用 Ollama (opens new window) 的 JSON Schema 功能。
当满足以下所有条件时:
- AI 服务方法返回一个 POJO
- 使用的
ChatLanguageModel
支持 (opens new window) JSON Schema 功能 - 在使用的
ChatLanguageModel
上启用了 JSON Schema 功能
则 ResponseFormat
将根据指定的返回类型自动生成带有 JsonSchema
的响应。
确保在配置 ChatLanguageModel
时显式启用 JSON Schema 功能, 因为它默认是禁用的。
生成的 JsonSchema
的 name
是返回类型的简单名称(getClass().getSimpleName()
), 在本例中为:Person
。
一旦 LLM 响应,输出会被解析成对象并从 AI 服务方法返回。
虽然我们正在逐步迁移到 Jackson,但 AI 服务中仍使用 Gson 来解析输出, 因此 POJO 上的 Jackson 注解将无效。
你可以在以下位置找到许多支持的用例示例: 这里 (opens new window) 和 这里 (opens new window)。
# 添加描述
如果 LLM 没有提供期望的输出,可以使用 @Description
注解类和字段,向 LLM 提供更多指令和正确输出的示例,例如:
@Description("a person")
record Person(@Description("person's first and last name, for example: John Doe") String name,
@Description("person's age, for example: 42") int age,
@Description("person's height in meters, for example: 1.78") double height,
@Description("is person married or not, for example: false") boolean married) {
}
2
3
4
5
6
# 限制
在使用 JSON Schema 与 AI 服务时,有一些限制:
- 仅支持 OpenAI、Google AI Gemini 和 Ollama 模型。
- 配置
ChatLanguageModel
时需要显式启用对 JSON Schema 的支持。 - 在流式模式 (opens new window)下不支持。
- 当前仅当返回类型是单个 POJO 或
Result<POJO>
时有效。 如果需要其他类型(例如List<POJO>
、enum
等),请将这些类型包装成 POJO。 我们正在努力 (opens new window)尽快支持更多的返回类型。 - POJO 可以包含:
- 标量/简单类型(例如
String
、int
/Integer
、double
/Double
、boolean
/Boolean
等) enum
- 嵌套 POJO
List<T>
、Set<T>
和T[]
,其中T
是标量、enum
或 POJO
- 标量/简单类型(例如
- 所有字段和子字段在生成的
JsonSchema
中默认标记为required
,当前没有方法将其设置为可选。 - 当 LLM 不支持 JSON Schema 功能,或者该功能未启用,或者返回类型不是 POJO 时,AI 服务将回退到提示方式 (opens new window)。
- 递归当前仅由 OpenAI 支持。
- 多态尚未支持。返回的 POJO 及其嵌套的 POJO 必须是具体类;接口或抽象类不支持。
# 工具(功能调用)
此方法假设(错误)地使用工具 (opens new window)来生成结构化输出。 请求中指定一个工具,工具参数描述了期望的输出结构。 一旦 LLM 返回一个包含 ToolExecutionRequest
的 AiMessage
, ToolExecutionRequest.arguments()
中的 JSON 字符串将被解析为 POJO。
更多信息即将发布。
同时,请阅读这一部分 (opens new window)和这篇文章 (opens new window)。
# 提示 + JSON 模式
更多信息即将发布。
同时,请阅读这一部分 (opens new window)和这篇文章 (opens new window)。
# 提示
在使用提示时,必须在系统或用户消息中以自由文本的形式指定期望输出的格式,并希望 LLM 遵守此格式。此方法不太可靠。 如果 LLM 和 LLM 提供商支持上述方法,最好使用这些方法。
更多信息即将发布。
同时,请阅读这一部分 (opens new window)和这篇文章 (opens new window)。