结构化输出
许多大型语言模型(LLMs)和 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 在低级 ChatModel
API 和高级 AI Service API 中都支持 JSON Schema 功能。
在 ChatModel
中使用 JSON Schema
在低级 ChatModel
API 中,可以在创建 ChatRequest
时使用与 LLM 提供商无关的 ResponseFormat
和 JsonSchema
来指定 JSON Schema:
ResponseFormat responseFormat = ResponseFormat.builder()
.type(JSON) // type 可以是 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();
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
- 用于支持可空类型。JsonRawSchema
- 用于使用自定义的完整定义的 JSON Schema。
JsonObjectSchema
JsonObjectSchema
表示一个带有嵌套属性的对象。它通常是 JsonSchema
的根元素。
有几种方法可以向 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()
.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", "The city for which the weather forecast should be returned")
.addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
.required("city")
.build();
请参阅 JsonObjectSchema
的 Javadoc 以获取更多详细信息。
JsonStringSchema
创建一个 JsonStringSchema
的示例:
JsonSchemaElement stringSchema = JsonStringSchema.builder()
.description("The name of the person")
.build();
JsonIntegerSchema
创建一个 JsonIntegerSchema
的示例:
JsonSchemaElement integerSchema = JsonIntegerSchema.builder()
.description("The age of the person")
.build();
JsonNumberSchema
创建一个 JsonNumberSchema
的示例:
JsonSchemaElement numberSchema = JsonNumberSchema.builder()
.description("The height of the person")
.build();
JsonBooleanSchema
创建一个 JsonBooleanSchema
的示例:
JsonSchemaElement booleanSchema = JsonBooleanSchema.builder()
.description("Is the person married?")
.build();
JsonEnumSchema
创建一个 JsonEnumSchema
的示例:
JsonSchemaElement enumSchema = JsonEnumSchema.builder()
.description("Marital status of the person")
.enumValues(List.of("SINGLE", "MARRIED", "DIVORCED"))
.build();
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();
JsonReferenceSchema
JsonReferenceSchema
可用于支持递归:
String reference = "person"; // 引用在 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();
注
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 支持。
JsonRawSchema
从现有 Schema 字符串创建 JsonRawSchema
的示例:
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);
注
JsonRawSchema
目前仅受 Azure OpenAI、Mistral、Ollama、OpenAI 和 OpenAI Official 支持。
添加描述
所有 JsonSchemaElement
子类型,除了 JsonReferenceSchema
,都有一个 description
属性。如果 LLM 没有提供所需的输出,可以提供描述以向 LLM 提供更多指令和正确输出的示例,例如:
JsonSchemaElement stringSchema = JsonStringSchema.builder()
.description("The name of the person, for example: John Doe")
.build();
限制
在 ChatModel
中使用 JSON Schema 时,存在一些限制:
- 它仅适用于支持的 Azure OpenAI、Google AI Gemini、Mistral、Ollama 和 OpenAI 模型。
- 对于 OpenAI,它尚不适用于流式模式。对于 Google AI Gemini、Mistral 和 Ollama,可以在创建/构建模型时通过
responseSchema(...)
指定 JSON Schema。 JsonReferenceSchema
和JsonAnyOfSchema
目前仅受 Azure OpenAI、Mistral 和 OpenAI 支持。
在 AI Services 中使用 JSON Schema
在使用 AI Services 时,可以更轻松地以更少的代码实现相同的目标:
interface PersonExtractor {
Person extractPersonFrom(String text);
}
ChatModel chatModel = OpenAiChatModel.builder() // 见 [1]
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // 见 [2]
.strictJsonSchema(true) // 见 [2]
.logRequests(true)
.logResponses(true)
.build();
// 或者
ChatModel chatModel = AzureOpenAiChatModel.builder() // 见 [1]
.endpoint(System.getenv("AZURE_OPENAI_URL"))
.apiKey(System.getenv("AZURE_OPENAI_API_KEY"))
.deploymentName("gpt-4o-mini")
.strictJsonSchema(true)
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // 见 [3]
.logRequestsAndResponses(true)
.build();
// 或者
ChatModel chatModel = GoogleAiGeminiChatModel.builder() // 见 [1]
.apiKey(System.getenv("GOOGLE_AI_GEMINI_API_KEY"))
.modelName("gemini-1.5-flash")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // 见 [4]
.logRequestsAndResponses(true)
.build();
// 或者
ChatModel chatModel = OllamaChatModel.builder() // 见 [1]
.baseUrl("http://localhost:11434")
.modelName("llama3.1")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // 见 [5]
.logRequests(true)
.logResponses(true)
.build();
// 或者
ChatModel chatModel = MistralAiChatModel.builder()
.apiKey(System.getenv("MISTRAL_AI_API_KEY"))
.modelName("mistral-small-latest")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // 见 [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 应用程序中,无需显式创建
ChatModel
和 AI Service,因为这些 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 Service 方法返回一个 POJO
- 所使用的
ChatModel
支持 JSON Schema 功能 - 在所使用的
ChatModel
上启用了 JSON Schema 功能
ResponseFormat
和 JsonSchema
将根据指定的返回类型自动生成。
注
请确保在配置 ChatModel
时显式启用 JSON Schema 功能,因为它默认是禁用的。
生成的 JsonSchema
的 name
是返回类型的简单名称(getClass().getSimpleName()
),在本例中为:“Person”。
一旦 LLM 响应,输出将被解析为对象并从 AI Service 方法返回。
必需和可选
默认情况下,生成的 JsonSchema
中的所有字段和子字段都被视为**可选**。这是因为当 LLM 缺乏足够的信息时,它倾向于虚构并用合成数据填充字段(例如,当名称缺失时使用“John Doe”)。
注
请注意,如果 LLM 未为可选的原始类型字段(例如 int
、boolean
等)提供值,它们将被初始化为默认值(例如 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("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) {
}
注
请注意,放置在 enum
值上的 @Description
没有效果 且**不会**包含在生成的 JSON Schema 中:
enum Priority {
@Description("Critical issues such as payment gateway failures or security breaches.") // 这将被忽略
CRITICAL,
@Description("High-priority issues like major feature malfunctions or widespread outages.") // 这将被忽略
HIGH,
@Description("Low-priority issues such as minor bugs or cosmetic problems.") // 这将被忽略
LOW
}
限制
在 AI Services 中使用 JSON Schema 时,存在一些限制:
- 它仅适用于支持的 Azure OpenAI、Google AI Gemini、Mistral、Ollama 和 OpenAI 模型。
- JSON Schema 的支持需要在配置
ChatModel
时显式启用。 - 它不适用于流式模式。
- 并非所有类型都受支持。请参阅这里支持的类型列表。
- 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 Service 将退回到提示。
提示 + JSON 模式
提示
当使用提示时(这是默认选择,除非启用了对 JSON Schema 的支持),AI Service 将自动生成格式指令并将其附加到 UserMessage
的末尾,以指示 LLM 应该以何种格式响应。在方法返回之前,AI Service 会将 LLM 的输出解析为所需的类型。
您可以通过启用日志记录来观察附加的指令。
注
这种方法相当不可靠。如果 LLM 和 LLM 提供商支持上述方法,最好使用它们。
支持的类型
类型 | JSON Schema | Prompting |
---|---|---|
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);
}