跳到主要内容

结构化输出

备注

"结构化输出"这个术语有多重含义,可以指两件事:

  • 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 提供商无关的 ResponseFormatJsonSchema 在创建 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 也允许 JsonEnumSchemaJsonArraySchema
  • [2] - 必须明确指定必需的属性;否则,它们被视为可选的。

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

  • JsonObjectSchema - 用于对象类型。
  • JsonStringSchema - 用于 Stringchar/Character 类型。
  • JsonIntegerSchema - 用于 int/Integerlong/LongBigInteger 类型。
  • JsonNumberSchema - 用于 float/Floatdouble/DoubleBigDecimal 类型。
  • JsonBooleanSchema - 用于 boolean/Boolean 类型。
  • JsonEnumSchema - 用于 enum 类型。
  • JsonArraySchema - 用于数组和集合(例如,ListSet)。
  • JsonReferenceSchema - 支持递归(例如,Person 有一个 Set<Person> children 字段)。
  • JsonAnyOfSchema - 支持多态性(例如,Shape 可以是 CircleRectangle)。
  • JsonNullSchema - 支持可空类型。

JsonObjectSchema

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

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

  1. 您可以使用 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();
  1. 您可以使用 addProperty(String name, JsonSchemaElement jsonSchemaElement) 方法单独添加属性:
JsonSchemaElement rootElement = JsonObjectSchema.builder()
.addProperty("city", citySchema)
.addProperty("temperatureUnit", temperatureUnitSchema)
.required("city")
.build();
  1. 您可以使用 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(...) 在创建/构建模型时指定。
  • JsonReferenceSchemaJsonAnyOfSchema 目前仅由 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 功能

那么将根据指定的返回类型自动生成带有 JsonSchemaResponseFormat

备注

确保在配置 ChatLanguageModel 时显式启用 JSON Schema 功能, 因为它默认是禁用的。

生成的 JsonSchemaname 是返回类型的简单名称(getClass().getSimpleName()), 在本例中是:"Person"。

一旦 LLM 响应,输出将被解析为对象并从 AI 服务方法返回。

您可以在这里这里找到许多支持的用例示例。

必需和可选

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

备注

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

备注

请注意,当严格模式开启(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 可以包含:
    • 标量/简单类型(例如,Stringint/Integerdouble/Doubleboolean/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);
}

相关教程