0%

langchain4j 结构化输出支持

许多 LLM 和 LLM 提供商支持以结构化格式(通常是 JSON)生成输出。这些输出可以轻松映射到 Java 对象,并在应用程序的其他部分使用。

例如,假设我们有一个 Person 类:

1
2
record Person(String name, int age, double height, boolean married) {
}

我们的目标是像下面这样的非结构化文本中提取 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.

目前,根据 LLM 和 LLM 提供商的不同,有三种方式可以实现这一目标(从最可靠到最不可靠):

  1. JSON Schema
  2. Prompting + JSON Mode
  3. Prompting

1. JSON Schema

一些 LLM 提供商(目前包括 Amazon Bedrock、Azure OpenAI、Google AI Gemini、Mistral、Ollama 和 OpenAI)允许为所需输出指定 JSON Schema。您可以在这里的 “JSON Schema” 列中查看所有支持的 LLM 提供商。

当在请求中指定 JSON Schema 时,LLM 预期生成符合该 Schema 的输出。

请注意: JSON Schema 是在请求中作为专用属性指定给 LLM 提供商 API 的,不需要在提示中包含任何自由格式的指令(例如,在系统或用户消息中)。

LangChain4j 在低级 ChatModel API 和高级 AI Service API 中都支持 JSON Schema 功能。

1.1 在 ChatModel 中使用 JSON Schema

在低级 ChatModel API 中,可以使用与 LLM 提供商无关的 ResponseFormatJsonSchema 在创建 ChatRequest 时指定 JSON Schema:

1
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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();

ChatModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.logRequests(true)
.logResponses(true)
.build();
// 或
ChatModel chatModel = AzureOpenAiChatModel.builder()
.endpoint(System.getenv("AZURE_OPENAI_URL"))
.apiKey(System.getenv("AZURE_OPENAI_API_KEY"))
.deploymentName("gpt-4o-mini")
.logRequestsAndResponses(true)
.build();
// 或
ChatModel chatModel = GoogleAiGeminiChatModel.builder()
.apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY"))
.modelName("gemini-1.5-flash")
.logRequestsAndResponses(true)
.build();
// 或
ChatModel chatModel = OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("llama3.1")
.logRequests(true)
.logResponses(true)
.build();
// 或
ChatModel chatModel = MistralAiChatModel.builder()
.apiKey(System.getenv("MISTRAL_AI_API_KEY"))
.modelName("mistral-small-latest")
.logRequests(true)
.logResponses(true)
.build();
// 或
ChatModel chatModel = WatsonxChatModel.builder()
.baseUrl(System.getenv("WATSONX_URL"))
.projectId(System.getenv("WATSONX_PROJECT_ID"))
.apiKey(System.getenv("WATSONX_API_KEY"))
.modelName("ibm/granite-4-h-small")
.logRequests(true)
.logResponses(true)
.build();
// 或
ChatModel chatModel = BedrockChatModel.builder()
.modelId("us.anthropic.claude-haiku-4-5-20251001-v1:0")
.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]

注意事项:

  • [1] 在大多数情况下,根元素必须是 JsonObjectSchema 类型,但 Gemini 也允许 JsonEnumSchemaJsonArraySchema
  • [2] 必须明确指定必需的属性;否则,它们被视为可选的。

1.2 JSON Schema 元素类型

JSON Schema 的结构使用 JsonSchemaElement 接口定义,具有以下子类型:

JsonObjectSchema

JsonObjectSchema 表示具有嵌套属性的对象。它通常是 JsonSchema 的根元素。

有几种方法可以向 JsonObjectSchema 添加属性:

1
2
3
4
5
6
7
JsonObjectSchema schema = JsonObjectSchema.builder()
.addStringProperty("name")
.addIntegerProperty("age")
.addNumberProperty("height")
.addBooleanProperty("married")
.required("name", "age", "height", "married")
.build();

更多详细信息请参阅 JsonObjectSchema 的 Javadoc。

基本类型 Schema 示例

创建 JsonStringSchema 的示例:

1
2
3
JsonStringSchema schema = JsonStringSchema.builder()
.description("The person's name")
.build();

创建 JsonIntegerSchema 的示例:

1
2
3
JsonIntegerSchema schema = JsonIntegerSchema.builder()
.description("The person's age")
.build();

创建 JsonNumberSchema 的示例:

1
2
3
JsonNumberSchema schema = JsonNumberSchema.builder()
.description("The person's height in meters")
.build();

创建 JsonBooleanSchema 的示例:

1
2
3
JsonBooleanSchema schema = JsonBooleanSchema.builder()
.description("Whether the person is married")
.build();

创建 JsonEnumSchema 的示例:

1
2
3
JsonEnumSchema schema = JsonEnumSchema.builder()
.enumValues(List.of("SINGLE", "MARRIED", "DIVORCED"))
.build();

创建 JsonArraySchema 以定义字符串数组的示例:

1
2
3
JsonArraySchema schema = JsonArraySchema.builder()
.items(JsonStringSchema.builder().build())
.build();

JsonReferenceSchema(递归支持)

JsonReferenceSchema 可以用于支持递归:

1
2
3
4
// 定义递归引用,例如树形结构
JsonReferenceSchema referenceSchema = JsonReferenceSchema.builder()
.reference("#/defs/TreeNode")
.build();

注意: JsonReferenceSchema 目前仅由 Azure OpenAI、Mistral 和 OpenAI 支持。

JsonAnyOfSchema(多态支持)

JsonAnyOfSchema 可以用于支持多态性:

1
2
3
4
5
6
7
8
9
10
11
12
JsonAnyOfSchema schema = JsonAnyOfSchema.builder()
.anyOf(List.of(
JsonObjectSchema.builder()
.addStringProperty("type")
.addStringProperty("carBrand")
.build(),
JsonObjectSchema.builder()
.addStringProperty("type")
.addStringProperty("bikeBrand")
.build()
))
.build();

注意: JsonAnyOfSchema 目前仅由 OpenAI 和 Azure OpenAI 支持。

使用 description 提供额外指令

所有 JsonSchemaElement 子类型(除了 JsonReferenceSchema)都有 description 属性。如果 LLM 没有提供所需的输出,可以提供描述以向 LLM 提供更多指令和正确输出的示例:

1
2
3
4
5
6
7
JsonObjectSchema schema = JsonObjectSchema.builder()
.addStringProperty("name", "人名,例如:张三、李四")
.addIntegerProperty("age", "年龄,必须是正整数")
.addNumberProperty("height", "身高,单位为米,例如 1.75")
.addBooleanProperty("married", "是否已结婚")
.required("name", "age")
.build();

1.3 使用 ChatModel 时 JSON Schema 的限制

使用 ChatModel 的 JSON Schema 时,有以下限制:

  • 并非所有 LLM 都支持 ResponseFormatType.JSON 或结构化输出。如果您使用不支持的模型,可能会忽略 responseFormat 参数,导致返回纯文本输出。
  • Schema 的 Token 开销:传递 JsonSchema 时,整个 Schema 定义会作为系统上下文的一部分发送给 LLM。深度嵌套和冗长描述的复杂 Schema 会增加输入 Token 的使用量。

2. 在 AI Service 中使用 JSON Schema

在高级 AI Service API 中,JSON Schema 功能是自动处理的。您只需定义一个返回 POJO 的 AI Service 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface PersonExtractor {
Person extractPerson(String text);
}

PersonExtractor extractor = AiServices.create(PersonExtractor.class, chatModel);

Person person = extractor.extractPerson("""
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.
""");

System.out.println(person); // Person[name=John, age=42, height=1.75, married=false]

注意事项:

当满足以下所有条件时:

  1. 方法返回类型是一个 POJO(普通的 Java 对象,如 record、class 等)
  2. 返回类型不是 StringResult<T>TokenStream 等内置类型
  3. 底层 ChatModel 支持 JSON Schema

那么将根据指定的返回类型自动生成带有 JsonSchemaResponseFormat。确保在配置 ChatModel显式启用 JSON Schema 功能,因为它默认是禁用的。生成的 JsonSchemaname 是返回类型的简单名称(getClass().getSimpleName()),在上例中为:"Person",一旦 LLM 响应,输出将被解析为对象并从 AI Service 方法返回。

2.1 可选字段与必需字段

默认情况下,生成的 JsonSchema 中的所有字段和子字段都被视为可选的。这是因为 LLM 倾向于产生幻觉,当缺乏足够信息时,会用合成数据填充字段(例如,当名字缺失时使用 “John Doe”)。

请注意: 具有原始类型的可选字段(例如 intboolean 等)如果 LLM 没有为它们提供值,将使用默认值初始化(int0booleanfalse 等)。

请注意: 当严格模式开启(strictJsonSchema(true))时,可选的 enum 字段仍然可能被填充幻觉值。

要使字段成为必需的,您可以使用 @JsonProperty(required = true) 注解它:

1
2
3
4
5
6
record Person(
@JsonProperty(required = true) String name,
@JsonProperty(required = true) int age,
double height,
boolean married
) {}

请注意: 当与工具(Tools)一起使用时,所有字段和子字段默认都被视为必需的

2.2 使用 @Description 提供额外指令

如果 LLM 没有提供所需的输出,可以使用 @Description 注解类和字段,为 LLM 提供更多指令和正确输出的示例:

1
2
3
4
5
6
7
@Description("从文本中提取人物信息")
record Person(
@Description("姓名,例如:张三、李四") String name,
@Description("年龄,必须是正整数") int age,
@Description("身高,单位为米,例如 1.75") double height,
@Description("是否已结婚") boolean married
) {}

请注意: 放在 enum 值上的 @Description 没有效果,不会包含在生成的 JSON Schema 中。

2.3 使用 AI Service 时 JSON Schema 的限制

使用 AI Service 的 JSON Schema 时,有以下限制:

  • 某些高级 JsonSchemaElement 子类型(如 JsonReferenceSchemaJsonAnyOfSchema)在 AI Service 自动生成 Schema 时可能不被支持,需要手动使用 ChatModel API。

3. Prompting + JSON Mode

如果 LLM 提供商支持 JSON Mode(即 ResponseFormatType.JSON)但不支持 JSON Schema,可以使用这种方式。

在这种方式下,您将 ResponseFormat 的类型设置为 JSON,但不提供 JsonSchema

1
2
3
4
5
6
7
8
ResponseFormat responseFormat = ResponseFormat.builder()
.type(JSON)
.build();

ChatRequest chatRequest = ChatRequest.builder()
.responseFormat(responseFormat)
.messages(userMessage)
.build();

这种方式保证输出是有效的 JSON 语法(不会缺少括号),但不能保证特定的字段名称。AI 可能返回 cost 而不是 price。仅适用于动态数据探索场景。


4. Prompting(纯提示)

使用提示时(这是默认选择,除非启用了 JSON Schema 支持),AI Service 将自动生成格式指令并将其附加到 UserMessage 的末尾,指示 LLM 应以何种格式响应。在方法返回之前,AI Service 将 LLM 的输出解析为所需类型。

您可以通过启用日志记录来观察附加的指令:

1
2
3
4
5
6
ChatModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.logRequests(true)
.logResponses(true)
.build();

这种方式相当不可靠。如果 LLM 和 LLM 提供商支持上述更可靠的方法,最好使用那些方法。

几个示例:

1
2
3
4
interface PersonExtractor {
@UserMessage("从以下文本中提取个人信息,返回 JSON 格式:{{it}}")
Person extractPerson(String text);
}

总结:三种方式对比

方式 可靠性 说明
JSON Schema ⭐⭐⭐ 最高 LLM 严格按照 Schema 输出,字段名和类型都有保证
Prompting + JSON Mode ⭐⭐ 中等 保证输出是有效 JSON,但不保证字段名称
Prompting ⭐ 最低 仅靠提示指令引导,LLM 可能不遵守格式要求

建议: 如果您的 LLM 提供商支持 JSON Schema,请优先使用它。这是目前最可靠、精确度最高的结构化输出实现方式。