结构化输出
"结构化输出"这个术语有多重含义,可以指两件事:
- LLM 生成结构化格式输出的一般能力(这是我们在本页中介绍的内容)
- OpenAI 的结构化输出功能, 适用于响应格式和工具(函数调用)。
许多 LLM 和 LLM 提供商支持以结构化格式(通常是 JSON)生 成输出。 这些输出可以轻松映射到 Java 对象,并在应用程序的其他部分使用。
例如,假设我们有一个 Person
类:
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 提供商的不同,有三种方式可以实现这一目标 (从最可靠到最不可靠):
JSON Schema
一些 LLM 提供商(目前包括 Azure OpenAI、Google AI Gemini、Mistral、Ollama 和 OpenAI)允许 为所需输出指定 JSON schema。 您可以在"JSON Schema"列中查看所有支持的 LLM 提供商 这里。
当在请求中指定 JSON schema 时,LLM 预期生成符合该 schema 的输出。
请注意,JSON schema 是在请求中作为专用属性指定给 LLM 提供商的 API 的, 不需要在提示中包含任何自由格式的指令(例如,在系统或用户消息中)。
LangChain4j 在低级 ChatLanguageModel
API 和高级 AI 服务 API 中都支持 JSON Schema 功能。
使用 ChatLanguageModel
的 JSON Schema
在低级 ChatLanguageModel
API 中,可以使用与 LLM 提供商无关的 ResponseFormat
和 JsonSchema
在创建 ChatRequest
时指定 JSON schema:
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 = AzureOpenAiChatModel.builder()
.endpoint(System.getenv("AZURE_OPENAI_URL"))
.apiKey(System.getenv("AZURE_OPENAI_API_KEY"))
.deploymentName("gpt-4o-mini")
.logRequestsAndResponses(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();
// 或
ChatLanguageModel 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
类型, 但 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
)。JsonNullSchema
- 支持可空类型。
JsonObjectSchema
JsonObjectSchema
表示具有嵌套属性的对象。
它通常是 JsonSchema
的根元素。
有几种方法可以向 JsonObjectSchema
添加属性:
- 您可以使用
properties(Map<String, JsonSchemaElement> properties)
方法一次添加所有属性:
JsonSchemaElement citySchema = JsonStringSchema.builder()
.description("应返回天气预报的城市")
.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") // 必需的属性应明确指定
.build();
- 您可以使用
addProperty(String name, JsonSchemaElement jsonSchemaElement)
方法单独添加属性:
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)
方法之一单独添加属性:
JsonSchemaElement rootElement = JsonObjectSchema.builder()
.addStringProperty("city", "应返回天气预报的城市")
.addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
.required("city")
.build();
更多详细信息请参阅 JsonObjectSchema 的 Javadoc。
JsonStringSchema
创建 JsonStringSchema
的示例:
JsonSchemaElement stringSchema = JsonStringSchema.builder()
.description("人的名字")
.build();
JsonIntegerSchema
创建 JsonIntegerSchema
的示例:
JsonSchemaElement integerSchema = JsonIntegerSchema.builder()
.description("人的年龄")
.build();
JsonNumberSchema
创建 JsonNumberSchema
的示例:
JsonSchemaElement numberSchema = JsonNumberSchema.builder()
.description("人的身高(米)")
.build();
JsonBooleanSchema
创建 JsonBooleanSchema
的示例:
JsonSchemaElement booleanSchema = JsonBooleanSchema.builder()
.description("人是否已婚?")
.build();
JsonEnumSchema
创建 JsonEnumSchema
的示例:
JsonSchemaElement enumSchema = JsonEnumSchema.builder()
.description("人的婚姻状况")
.enumValues(List.of("SINGLE", "MARRIED", "DIVORCED"))
.build();
JsonArraySchema
创建 JsonArraySchema
以定义字符串数组:
JsonSchemaElement itemSchema = JsonStringSchema.builder()
.description("人的名字")
.build();
JsonSchemaElement arraySchema = JsonArraySchema.builder()
.description("文本中找到的所有人的名字")
.items(itemSchema)
.build();
JsonReferenceSchema
JsonReferenceSchema
可以用于支持递归:
String reference = "person"; // reference 应在模式中唯一
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();
JsonReferenceSchema
目前仅由 Azure OpenAI、Mistral 和 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}]}
JsonAnyOfSchema
目前仅由 OpenAI 和 Azure OpenAI 支持。
添加描述
所有 JsonSchemaElement
子 类型,除了 JsonReferenceSchema
,都有 description
属性。
如果 LLM 没有提供所需的输出,可以提供描述
以向 LLM 提供更多指令和正确输出的示例,例如:
JsonSchemaElement stringSchema = JsonStringSchema.builder()
.description("人的名字,例如:John Doe")
.build();
限制
使用 ChatLanguageModel
的 JSON Schema 时,有一些限制:
- 它仅适用于支持的 Azure OpenAI、Google AI Gemini、Mistral、Ollama 和 OpenAI 模型。
- 它在流式模式下不工作。
- 对于 Google AI Gemini、Mistral 和 Ollama,JSON Schema 可以通过
responseSchema(...)
在创建/构建模型时指定。 JsonReferenceSchema
和JsonAnyOfSchema
目前仅由 Azure OpenAI、Mistral 和 OpenAI 支持。
使用 AI 服务的 JSON Schema
在高级 AI 服务 API 中,JSON Schema 功能是自动处理的。 您只需定义一个返回 POJO 的 AI 服务方法:
interface PersonExtractor {
@SystemMessage("你是一个专门从文本中提取人物信息的助手。")
Person extractPersonFrom(String text);
}
ChatLanguageModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.responseFormat(ResponseFormat.builder().type(JSON).build()) // 见下面的 [2]
.logRequests(true)
.logResponses(true)
.build();
// 或
ChatLanguageModel chatModel = AzureOpenAiChatModel.builder()
.endpoint(System.getenv("AZURE_OPENAI_URL"))
.apiKey(System.getenv("AZURE_OPENAI_API_KEY"))
.deploymentName("gpt-4o-mini")
.responseFormat(ResponseFormat.builder().type(JSON).build()) // 见下面的 [3]
.logRequestsAndResponses(true)
.build();
// 或
ChatLanguageModel chatModel = GoogleAiGeminiChatModel.builder()
.apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY"))
.modelName("gemini-1.5-flash")
.responseFormat(ResponseFormat.builder().type(JSON).build()) // 见下面的 [4]
.logRequestsAndResponses(true)
.build();
// 或
ChatLanguageModel chatModel = OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("llama3.1")
.responseFormat(ResponseFormat.builder().type(JSON).build()) // 见下面的 [5]
.logRequests(true)
.logResponses(true)
.build();
// 或
ChatLanguageModel chatModel = MistralAiChatModel.builder()
.apiKey(System.getenv("MISTRAL_AI_API_KEY"))
.modelName("mistral-small-latest")
.responseFormat(ResponseFormat.builder().type(JSON).build()) // 见下面的 [6]
.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]
注意:
- [1] - 在 Quarkus 或 Spring Boot 应用程序中,无需显式创建
ChatLanguageModel
和 AI 服务, 因为这些 bean 是自动创建的。更多信息: 对于 Quarkus, 对于 Spring Boot。 - [2] - 这是启用 OpenAI 的 JSON Schema 功能所必需的,详见此处。
- [3] - 这是启用 Azure OpenAI 的 JSON Schema 功能所必需的。
- [4] - 这是启用 Google AI Gemini 的 JSON Schema 功能所必需的。
- [5] - 这是启用 Ollama 的 JSON Schema 功能所必需的。
- [6] - 这是启用 Mistral 的 JSON Schema 功能所必需的。
当满足以下所有条件时:
- AI 服务方法返回 POJO
- 使用的
ChatLanguageModel
支持 JSON Schema 功能 - 在使用的
ChatLanguageModel
上启用了 JSON Schema 功能
那么将根据指定的返回类型自动生成带有 JsonSchema
的 ResponseFormat
。
确保在配置 ChatLanguageModel
时显式启用 JSON Schema 功能,
因为它默认是禁用的。
生成的 JsonSchema
的 name
是返回类型的简单名称(getClass().getSimpleName()
),
在本例中是:"Person"。
一旦 LLM 响应,输出将被解析为对象并从 AI 服务方法返回。
必需和可选
默认情况下,生成的 JsonSchema
中的所有字段和子字段都被视为可选的。
这是因为 LLM 倾向于产生幻觉,当缺乏足够信息时,会用合成数据填充字段(例如,当名字缺失时使用"John Doe")。
请注意,具有原始类型的可选字段(例如,int
、boolean
等)
如果 LLM 没有为它们提供值,将使用默认值初始化(例如,int
为 0
,boolean
为 false
等)。
请注意,当严格模式开启(strictJsonSchema(true)
)时,可选的 enum
字段仍然可能被填充幻觉值。
要使字段成为必需的,您可以使用 @JsonProperty(required = true)
注解它:
record Person(@JsonProperty(required = true) String name, String surname) {
}
interface PersonExtractor {
Person extractPersonFrom(String text);
}
请注意,当与工具一起使用时, 所有字段和子字段默认都被视为必需的。
添加描述
如果 LLM 没有提供所需的输出, 可以使用 @Description
注解类和字段,
为 LLM 提供更多指令和正确输出的示例,例如:
@Description("一个人")
record Person(@Description("人的名和姓,例如:John Doe") String name,
@Description("人的年龄,例如:42") int age,
@Description("人的身高(米),例如:1.78") double height,
@Description("人是否已婚,例如:false") boolean married) {
}
请注意,放在 enum
值上的 @Description
没有效果,不会包含在生成的 JSON schema 中:
enum Priority {
@Description("关键问题,如支付网关故障或安全漏洞。") // 这会被忽略
CRITICAL,
@Description("高优先级问题,如主要功能故障或广泛的服务中断。") // 这会被忽略
HIGH,
@Description("低优先级问题,如小错误或外观问题。") // 这会被忽略
LOW
}
限制
使用 AI 服务的 JSON Schema 时,有一些限制:
- 它只适用于支持的 Azure OpenAI、Google AI Gemini、Mistral、Ollama 和 OpenAI 模型。
- 需要在配置
ChatLanguageModel
时显式启用 JSON Schema 支持。 - 它在流式模式下不工作。
- 并非所有类型都受支持。请参阅这里的支持类型列表。
- POJO 可以包含:
- 标量/简单类型(例如,
String
、int
/Integer
、double
/Double
、boolean
/Boolean
等) enum
- 嵌套 POJO
List<T>
、Set<T>
和T[]
,其中T
是标量、enum
或 POJO
- 标量/简单类型(例如,
- 递归目前仅由 Azure OpenAI、Mistral 和 OpenAI 支持。
- 多态性尚不支持。返回的 POJO 及其嵌套 POJO 必须是具体类; 不支持接口或抽象类。
- 当 LLM 不支持 JSON Schema 功能,或未启用,或类型不受支持时, AI 服务将回退到提示。
提示 + JSON 模式
提示
使用提示时(这是默认选择,除非启用了 JSON schema 支持),
AI 服务将自动生成格式指令并将其附加到 UserMessage
的末尾,
指示 LLM 应以何种格式响应。
在方法返回之前,AI 服务将 LLM 的输出解析为所需类型。
您可以通过启用日志记录观察附加的指令。
这种方法相当不可靠。 如果 LLM 和 LLM 提供商支持上述方法,最好使用那些方法。
支持的类型
类型 | 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<?, ?> | ❌ | ✅ |
几个示例:
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);
}