LangChain4j从入门到精通-11-结构化输出
本文是《LangChain4j从入门到精通》系列的第十一篇,深入解析了如何利用LangChain4j框架实现大模型结构化输出,将非文本数据自动转换为规范的Java对象。文章详细对比了三种实现方案:JSON Schema(最可靠,支持Azure OpenAI、Gemini等主流模型)、提示词+JSON模式(平衡方案)和纯提示词(基础方案),并通过完整代码示例演示了从POJO、枚举到集合类型的自动映射技巧。针对生产环境需求,特别介绍了JSON Schema的高级配置,包括递归结构、多态支持、字段校验规则及描述信息优化等实战经验。掌握结构化输出技术,能让开发者轻松实现从文本提取到数据解析的端到端自动化,大幅提升AI应用的数据处理效率与准确性。
#Java #大模型开发 #LangChain4j #结构化输出 #JSONSchema
:::注意
术语"结构化输出"具有多重含义,可能指代以下两种情况:
- LLM以结构化格式生成输出的总体能力(这是我们在本页介绍的内容)
- 结构化输出 OpenAI 的特性,适用于响应格式和工具(函数调用)。
:::
许多大型语言模型(LLM)及其服务提供商支持以结构化格式(通常是JSON)生成输出。这些输出可以轻松映射到Java对象,并在应用程序的其他部分使用。
例如,假设我们有一个Person类:
java
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)及其提供商的不同,有三种实现方式(按可靠性从高到低排序):
JSON模式
一些大型语言模型提供商(目前包括Azure OpenAI、Google AI Gemini、Mistral、Ollama和OpenAI)允许
指定 JSON模式 为了更理想的输出.
当请求中指定了JSON模式时,LLM应生成符合该模式的输出。
:::注意
请注意,JSON 模式是在向LLM提供商的API发送请求时通过专用属性指定的,无需在提示中包含任何自由格式的指令(例如在系统或用户消息中)。
:::
LangChain4j在低级别的ChatModelAPI和高级别的AI服务API中都支持JSON Schema功能。
将JSON Schema与ChatModel结合使用
在底层 ChatModelAPI 中,创建 ChatRequest时可以使用与LLM提供商无关的 ResponseFormat和 JsonSchema来指定 JSON 模式。
java
ResponseFormat responseFormat = ResponseFormat.builder()
.type(JSON) // type can be either TEXT (default) or JSON
.jsonSchema(JsonSchema.builder()
.name("Person") // OpenAI requires specifying the name for the schema
.rootElement(JsonObjectSchema.builder() // see [1] below
.addStringProperty("name")
.addIntegerProperty("age")
.addNumberProperty("height")
.addBooleanProperty("married")
.required("name", "age", "height", "married") // see [2] below
.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();
// OR
ChatModel chatModel = AzureOpenAiChatModel.builder()
.endpoint(System.getenv("AZURE_OPENAI_URL"))
.apiKey(System.getenv("AZURE_OPENAI_API_KEY"))
.deploymentName("gpt-4o-mini")
.logRequestsAndResponses(true)
.build();
// OR
ChatModel chatModel = GoogleAiGeminiChatModel.builder()
.apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY"))
.modelName("gemini-1.5-flash")
.logRequestsAndResponses(true)
.build();
// OR
ChatModel chatModel = OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("llama3.1")
.logRequests(true)
.logResponses(true)
.build();
// OR
ChatModel chatModel = MistralAiChatModel.builder()
.apiKey(System.getenv("MISTRAL_AI_API_KEY"))
.modelName("mistral-small-latest")
.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类型, 然而 * Azure OpenAI、Mistral、Ollama、OpenAI 和 OpenAI 官方也允许将 JsonRawSchema作为根元素 * Gemini 还允许将 JsonEnumSchema和 JsonArraySchema作为根元素
JSON模式的结构使用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).JsonNullSchema- 支持可空类型.JsonRawSchema- 使用您自定义的完整JSON模式
JsonObjectSchema
JsonObjectSchema表示一个具有嵌套属性的对象。 它通常是 JsonSchema的根元素。
有几种方法可以向JsonObjectSchema添加属性:
- 你可以使用 properties(
Map<String, JsonSchemaElement> properties)方法一次性添加所有属性:
java
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()
.addProperties(properties)
.required("city") // required properties should be specified explicitly
.build();
- 您可以使用
addProperty(String name, JsonSchemaElement jsonSchemaElement)方法逐个添加属性:
java
JsonSchemaElement rootElement = JsonObjectSchema.builder()
.addProperty("city", citySchema)
.addProperty("temperatureUnit", temperatureUnitSchema)
.required("city")
.build();
- 您可以使用
add{Type}Property(String name)或add{Type}Property(String name, String description)方法逐个添加属性:
java
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();
请参考Javadoc中的说明
JsonObjectSchema
JsonStringSchema
创建JsonStringSchema的示例:
java
JsonSchemaElement stringSchema = JsonStringSchema.builder()
.description("The name of the person")
.build();
JsonIntegerSchema
创建 JsonIntegerSchema的示例
java
JsonSchemaElement integerSchema = JsonIntegerSchema.builder()
.description("The age of the person")
.build();
JsonNumberSchema
创建 JsonNumberSchema的示例:
java
JsonSchemaElement numberSchema = JsonNumberSchema.builder()
.description("The height of the person")
.build();
JsonBooleanSchema
创建JsonBooleanSchema的示例:
java
JsonSchemaElement booleanSchema = JsonBooleanSchema.builder()
.description("Is the person married?")
.build();
JsonEnumSchema
创建JsonEnumSchema的示例:
java
JsonSchemaElement enumSchema = JsonEnumSchema.builder()
.description("Marital status of the person")
.enumValues(List.of("SINGLE", "MARRIED", "DIVORCED"))
.build();
JsonArraySchema
创建 JsonArraySchema以定义字符串数组的示例:
java
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();
JsonReferenceSchema
JsonReferenceSchema可用于支持递归:
java
String reference = "person"; // reference should be unique withing the 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();
:::注意
目前只有Azure OpenAI、Mistral和OpenAI支持JsonReferenceSchema。
:::
JsonAnyOfSchema
JsonAnyOfSchema可用于支持多态性:
java
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}]}
:::注意
目前只有OpenAI、Azure OpenAI和Google AI Gemini支持JsonAnyOfSchema。
:::
JsonRawSchema
从现有模式字符串创建 JsonRawSchema的示例:
java
var rawSchema = """
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"city": {
"type": "string"
}
},
"required": ["city"],
"additionalProperties": false
}
""";
JsonRawSchema schema = JsonRawSchema.from(rawSchema);
添加描述
除了 JsonReferenceSchema之外,所有 JsonSchemaElement子类型都有一个 description属性。 如果大型语言模型(LLM)未能生成预期输出,可以通过提供描述来向模型传递更多指令和正确输出的示例,例如:
java
JsonSchemaElement stringSchema = JsonStringSchema.builder()
.description("The name of the person, for example: John Doe")
.build();
限制
在使用JSON Schema与ChatModel时,存在以下限制:
- 仅适用于受支持的Azure OpenAI、Google AI Gemini、Mistral、Ollama和OpenAI模型。
- 目前OpenAI的流式模式尚不支持该功能。 对于Google AI Gemini、Mistral和Ollama,可以在创建/构建模型时通过responseSchema(...)指定JSON Schema。
- 目前只有Azure OpenAI、Mistral和OpenAI支持JsonReferenceSchema和JsonAnyOfSchema。
在AI服务中使用JSON Schema
使用AI服务时,可以更轻松地用更少的代码实现同样的效果:
java
interface PersonExtractor {
Person extractPersonFrom(String text);
}
ChatModel chatModel = OpenAiChatModel.builder() // see [1] below
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // see [2] below
.strictJsonSchema(true) // see [2] below
.logRequests(true)
.logResponses(true)
.build();
// OR
ChatModel chatModel = AzureOpenAiChatModel.builder() // see [1] below
.endpoint(System.getenv("AZURE_OPENAI_URL"))
.apiKey(System.getenv("AZURE_OPENAI_API_KEY"))
.deploymentName("gpt-4o-mini")
.strictJsonSchema(true)
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // see [3] below
.logRequestsAndResponses(true)
.build();
// OR
ChatModel chatModel = GoogleAiGeminiChatModel.builder() // see [1] below
.apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY"))
.modelName("gemini-1.5-flash")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // see [4] below
.logRequestsAndResponses(true)
.build();
// OR
ChatModel chatModel = OllamaChatModel.builder() // see [1] below
.baseUrl("http://localhost:11434")
.modelName("llama3.1")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // see [5] below
.logRequests(true)
.logResponses(true)
.build();
// OR
ChatModel chatModel = MistralAiChatModel.builder()
.apiKey(System.getenv("MISTRAL_AI_API_KEY"))
.modelName("mistral-small-latest")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // see [6] below
.logRequests(true)
.logResponses(true)
.build();
PersonExtractor personExtractor = AiServices.create(PersonExtractor.class, chatModel); // see [1] below
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]
注意:
-
1\] - 在Quarkus或Spring Boot应用中,无需显式创建ChatModel和AI服务,这些Bean会自动生成。更多信息请参考: [Quarkus](https://docs.quarkiverse.io/quarkus-langchain4j/dev/ai-services.html), [Spring Boot](https://docs.langchain4j.dev/tutorials/spring-boot-integration#spring-boot-starter-for-declarative-ai-services).
-
AI服务方法返回一个POJO
-
使用的 ChatModel 支持 JSON Schema 功能
-
所使用的 ChatModel已启用 JSON Schema 功能
然后会根据指定的返回类型自动生成带有 JsonSchema的 ResponseFormat。
:::注意
在配置 ChatModel时,请务必明确启用 JSON Schema 功能, 因为该功能默认是禁用的。
:::
生成的 JsonSchema的 name是返回类型的简单名称(getClass().getSimpleName()),在本例中为:"Person"。一旦LLM响应,输出将被解析为一个对象并从AI服务方法中返回。 你可以找到许多支持的使用案例示例。
必填和选填
默认情况下,生成的 JsonSchema中的所有字段和子字段都被视为 可选。
这是因为当大语言模型(LLMs)缺乏足够信息时,往往会虚构内容并用合成数据填充字段 (例如:当姓名缺失时使用"John Doe")。
请注意,对于基本类型的可选字段(例如 int、boolean等),如果大语言模型未提供值,它们将被初始化为默认值(例如 int默认为 0,boolean默认为 false等)
请注意,即使在严格模式下(strictJsonSchema(true)),可选的 enum字段仍可能被填入虚构值。
要将字段设为必填,可以使用注解 @JsonProperty(required = true):
java
record Person(@JsonProperty(required = true) String name, String surname) {
}
interface PersonExtractor {
Person extractPersonFrom(String text);
}
请注意,当与工具一起使用时,默认情况下所有字段和子字段均被视为必填项。
添加描述
如果大型语言模型(LLM)未能生成预期的输出结果,可以通过为类和字段添加@Description注解来为其提供更多指令和正确输出的示例,例如:
java
@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) {
}
请注意,在枚举值上使用 @Description注解 不会产生任何效果,也 不会包含在生成的 JSON 模式中。
java
enum Priority {
@Description("Critical issues such as payment gateway failures or security breaches.") // this is ignored
CRITICAL,
@Description("High-priority issues like major feature malfunctions or widespread outages.") // this is ignored
HIGH,
@Description("Low-priority issues such as minor bugs or cosmetic problems.") // this is ignored
LOW
}
限制
在使用JSON Schema与AI服务时存在以下限制:
- 仅支持特定的Azure OpenAI、Google AI Gemini、Mistral、Ollama和OpenAI模型。
- 需在配置ChatModel时显式启用JSON Schema支持功能。
- 无法在流式传输模式下使用。
- 并非所有类型都受支持,可查看支持的类型列表。
- POJO可包含:
- 标量/简单类型(如String、int/Integer、double/Double、boolean/Boolean等)
- enum枚举类型
- 嵌套POJO
List<T>、Set<T>及T[](其中T为标量、枚举或POJO)
- 目前仅Azure OpenAI、Mistral和OpenAI支持递归结构。 暂不支持多态,返回的POJO及其嵌套POJO必须是具体类,不支持接口或抽象类。
- 当LLM不支持JSON Schema功能、未启用该功能或类型不受支持时,AI服务将回退至提示词方式。
提示词 + JSON模式
更多信息即将发布。在此期间,请阅读本节内容
文章.
提示词
在使用提示词(这是默认选项,除非启用了对JSON模式的支持)时,AI服务会自动生成格式说明,并将其附加到UserMessage的末尾,
指示LLM应以何种格式进行响应。 在方法返回之前,AI服务会将LLM的输出解析为所需的类型
:::注意
这种方法相当不可靠。如果LLM和LLM提供商支持上述方法,最好使用那些方法
:::
Supported Types
| 类型 | JSON Schema | 提示词 |
|---|---|---|
POJO |
✅ | ✅ |
List<POJO>, Set<POJO> |
✅ | ❌ |
Enum |
✅ | ✅ |
List<Enum>, Set<Enum> |
✅ | ✅ |
List<String>, Set<String> |
✅ | ✅ |
boolean, Boolean |
✅ | ✅ |
int, Integer |
✅ | ✅ |
long, Long |
✅ | ✅ |
float, Float |
✅ | ✅ |
double, Double |
✅ | ✅ |
byte, Byte |
❌ | ✅ |
short, Short |
❌ | ✅ |
BigInteger |
❌ | ✅ |
BigDecimal |
❌ | ✅ |
Date |
❌ | ✅ |
LocalDate |
❌ | ✅ |
LocalTime |
❌ | ✅ |
LocalDateTime |
❌ | ✅ |
Map<?, ?> |
❌ | ✅ |
几个例子:
java
record Person(String firstName, String lastName) {}
enum Sentiment {
POSITIVE, NEGATIVE, NEUTRAL
}
interface Assistant {
Person extractPersonFrom(String text);
Set<Person> extractPeopleFrom(String text);
Sentiment extractSentimentFrom(String text);
List<Sentiment> extractSentimentsFrom(String text);
List<String> generateOutline(String topic);
boolean isSentimentPositive(String text);
Integer extractNumberOfPeopleMentionedIn(String text);
}