Skip to content
LangChain4j 文档版本 1.22.0 源快照 1fc4ebe3 更新于 2026-10-14

结构化输出

NOTE

"结构化输出"(Structured Outputs)一词含义 overloaded,可能指两件事:

  • LLM 以结构化格式生成输出的通用能力(本页所涵盖的就是这个)
  • OpenAI 的 Structured Outputs 特性, 它同时适用于响应格式与工具(函数调用)。

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

举例来说,假设我们有一个 Person 类:

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

我们的目标是从一个描述虚构角色的非结构化文本中抽取一个 Person 对象:

Eldwin Brightblade is 412 years old and serves as court wizard in the kingdom of Aelyria.
He stands 1.65 meters tall and is known for his flowing white beard.
Currently unmarried, he devotes his time to studying ancient runes.

目前,取决于 LLM 与 LLM 提供者,有三种方式可达成此事(从最可靠到最不可靠):

JSON Schema

一些 LLM 提供者(目前为 Amazon Bedrock、Azure OpenAI、Google AI Gemini、Mistral、Ollama 与 OpenAI)允许为期望的输出指定 JSON schema。你可以在这里的"JSON Schema"列查看所有受支持的 LLM 提供者。

当请求中指定了一个 JSON schema 时,LLM 应会生成一个遵循该 schema 的输出。

NOTE

请注意,JSON schema 是在对 LLM 提供者 API 的请求中一个专门的属性里指定的,并不需要在提示词中包含任何自由形式的指令(例如在系统或用户消息中)。

LangChain4j 在低层 ChatModel API 与高层 AI 服务 API 中都支持 JSON Schema 特性。

结合 ChatModel 使用 JSON Schema

在低层 ChatModel API 中,JSON schema 可以在创建 ChatRequest 时,用与 LLM 提供者无关的 ResponseFormat 与 JsonSchema 来指定:

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("""
        Eldwin Brightblade is 412 years old and serves as court wizard in the kingdom of Aelyria.
        He stands 1.65 meters tall and is known for his flowing white beard.
        Currently unmarried, he devotes his time to studying ancient runes.
        """);

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();
// OR
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();
// OR
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":"Eldwin Brightblade","age":412,"height":1.65,"married":false}

Person person = new ObjectMapper().readValue(output, Person.class);
System.out.println(person); // Person[name=Eldwin Brightblade, age=412, height=1.65, married=false]

请注意:

  • [1] - 在多数情况下,根元素必须是 JsonObjectSchema 类型, 不过:
    • Amazon Bedrock、Azure OpenAI、Mistral、Ollama、OpenAI 与 OpenAI Official 也允许 JsonRawSchema 作为根元素
    • 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 添加属性:

  1. 你可以用 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();
  1. 你可以用 addProperty(String name, JsonSchemaElement jsonSchemaElement) 方法逐个添加属性:
java
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) 之一逐个添加属性:
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();

更多细节请参考 JsonObjectSchema 的 Javadoc。

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();

NOTE

JsonReferenceSchema 目前仅由 Azure OpenAI、Mistral 与 OpenAI 支持。

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}]}

NOTE

JsonAnyOfSchema 目前仅由 OpenAI、Azure OpenAI 与 Google AI Gemini 支持。

JsonRawSchema

从一个已有的 schema 字符串创建 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);

NOTE

JsonRawSchema 目前仅由 Amazon Bedrock、Azure OpenAI、Mistral、Ollama、OpenAI、OpenAI Official 与 Google AI Gemini 支持。就 Google AI Gemini 而言,参见 Response JSON Schema 中的示例。

添加描述

所有 JsonSchemaElement 子类型,除 JsonReferenceSchema 外,都有一个 description 属性。若 LLM 没有提供期望的输出,可以提供描述来给 LLM 更多指令与正确输出的示例,例如:

java
JsonSchemaElement stringSchema = JsonStringSchema.builder()
        .description("The name of the person, for example: John Doe")
        .build();

限制

当结合 ChatModel 使用 JSON Schema 时,有一些限制:

  • 它只对受支持的 Amazon Bedrock、Azure OpenAI、Google AI Gemini、Mistral、Ollama 与 OpenAI 模型有效。
  • 它对 OpenAI 尚不适用于流式模式。 对于 Google AI Gemini、Mistral 与 Ollama,JSON Schema 可以在创建/构建模型时通过 responseSchema(...) 指定。
  • JsonReferenceSchema 与 JsonAnyOfSchema 目前仅由 Azure OpenAI、Mistral 与 OpenAI 支持。

结合 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
         .strictJsonSchema(true) // see [6] below
         .logRequests(true)
         .logResponses(true)
         .build();
// OR
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")
        .supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // see [7] below
        .logRequests(true)
        .logResponses(true)
        .build();
// OR
ChatModel chatModel = BedrockChatModel.builder()
        .modelId("us.anthropic.claude-haiku-4-5-20251001-v1:0")
        .supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // see [8] below
        .logRequests(true)
        .logResponses(true)
        .build();

PersonExtractor personExtractor = AiServices.create(PersonExtractor.class, chatModel); // see [1] below

String text = """
        Eldwin Brightblade is 412 years old and serves as court wizard in the kingdom of Aelyria.
        He stands 1.65 meters tall and is known for his flowing white beard.
        Currently unmarried, he devotes his time to studying ancient runes.
        """;

Person person = personExtractor.extractPersonFrom(text);

System.out.println(person); // Person[name=Eldwin Brightblade, age=412, height=1.65, married=false]

请注意:

  • [1] - 在一个 Quarkus 或 Spring Boot 应用中,无需显式创建 ChatModel 与 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 特性是必需的。
  • [7] - 这对启用 watsonx.ai 的 JSON Schema 特性是必需的。
  • [8] - 这对启用 Amazon Bedrock 的 JSON Schema 特性是必需的。

当以下所有条件都满足时:

  • AI 服务方法返回一个 POJO
  • 所使用的 ChatModel 支持 JSON Schema 特性
  • JSON Schema 特性在所采用的 ChatModel 上已启用

那么带 JsonSchema 的 ResponseFormat 会基于所指定的返回类型自动生成。

NOTE

请在配置 ChatModel 时确保显式启用 JSON Schema 特性,因为它默认禁用。

所生成的 JsonSchema 的 name 是返回类型的一个简单名称(getClass().getSimpleName()),在本例中即:"Person"。

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

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

必填与可选

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

NOTE

请注意,带原始类型的可选字段(例如 int、boolean 等),当 LLM 未为其提供值时,会被初始化为默认值(例如 int 为 0、boolean 为 false 等)。

NOTE

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

要让某字段必填,你可以用 @JsonProperty(required = true) 注解它:

java
record Person(@JsonProperty(required = true) String name, String surname) {
}

interface PersonExtractor {
    
    Person extractPersonFrom(String text);
}

NOTE

请注意,当与工具一起使用时,所有字段与子字段默认都被视为**必填**。

添加描述

若 LLM 没有提供期望的输出,类和字段可以用 @Description 注解,来给 LLM 更多指令与正确输出的示例,例如:

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) {
}

NOTE

请注意,放在某个 enum 值上的 @Description 不起作用,且**不会**被包含在所生成的 JSON schema 中:

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
}

多态类型

AI 服务方法可以返回多态类型——一个其具体子类型由 LLM 在运行时决定的基类型。支持两种形式:

  • 密封接口与密封类 —— 无需注解;子类型通过 Class.getPermittedSubclasses() 发现。
  • 普通的抽象类与接口 —— 必须用 Jackson 的 @JsonSubTypes 显式声明其子类型。

多态返回类型适用于类型本身、集合(List<T>、Set<T>)、嵌套在其他 POJO 内的字段,以及子类型把基类型作为一个字段包含在内的递归层次结构(例如 BinaryOp(left: ExpressionNode, right: ExpressionNode),其中 ExpressionNode 是密封基)。

每个子类型都会添加一个判别属性(默认为 "type"),以便 LLM 能表达它产出的是哪个具体类型;随后解析器会自动分派到正确的子类型。

密封接口与类——无需注解:

java
sealed interface Animal permits Dog, Cat {}

record Dog(String name, String breed) implements Animal {}

record Cat(String name, boolean indoor) implements Animal {}

interface AnimalExtractor {

    Animal extractAnimalFrom(String text);
}

LLM 会看到一个对 Dog 与 Cat 使用 anyOf 的 schema,每个都被约束在 type 属性中输出其简单类名。给定:

Rex is a Labrador.

LLM 会产出 {"value":{"type":"Dog","name":"Rex","breed":"Labrador"}},它会被解析回一个 Dog 实例。

NOTE

由于许多 LLM 提供者不支持根处带 anyOf 的 JSON schema,该 schema 会把多态选择包在一个 value 属性下(集合则为 values)。这个包装是实现细节——你的 AI 服务方法仍返回被解包后的子类型。

多态类型的集合:

java
interface AnimalsExtractor {

    List<Animal> extractAnimalsFrom(String text);
}

另一个 POJO 内的多态字段:

java
record Owner(String name, Animal pet) {}

interface OwnerExtractor {

    Owner extractOwnerFrom(String text);
}

Jackson 的 @JsonSubTypes / @JsonTypeInfo** 也受支持,它们让你把线路名称与 Java 类名解耦:

java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Square.class, name = "square"),
    @JsonSubTypes.Type(value = Circle.class, name = "circle")
})
interface Shape {}

class Square implements Shape { double side; }

class Circle implements Shape { double radius; }

判别值的解析顺序:

  1. 基类型上的 @JsonSubTypes.Type(name = "...")
  2. 子类型上的 @JsonTypeName
  3. Class.getSimpleName()(默认)

因此,当你不想在基类型上声明线路名时,@JsonTypeName 是一种设置它的便捷方式:

java
sealed interface Bird permits Eagle, Sparrow {}

@JsonTypeName("bird_eagle")
record Eagle(double wingspanMeters) implements Bird {}

@JsonTypeName("bird_sparrow")
record Sparrow(boolean migratory) implements Bird {}

LLM 会把 "bird_eagle" / "bird_sparrow" 当作判别值来看到,而不是简单类名("Eagle"/"Sparrow")。

受支持的 @JsonTypeInfo 配置:

属性受支持的值
useId.NAME、Id.SIMPLE_NAME
includeAs.PROPERTY(默认)、As.EXISTING_PROPERTY
property任何显式值;为空时默认为 "@type"
defaultImpl任何具体子类——当 LLM 的判别缺失或未知时使用
visibletrue 会把判别字段保留在反序列化后的 bean 上(并绕过字段冲突检查)

其他任何取值(例如 Id.CLASS、As.WRAPPER_OBJECT)会在 schema 生成时以 UnsupportedFeatureException 拒绝。

用 defaultImpl 容忍幻觉:

java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, defaultImpl = UnknownTool.class)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Hammer.class, name = "hammer"),
    @JsonSubTypes.Type(value = Wrench.class, name = "wrench")
})
interface Tool {}

若 LLM 产出一个未知的判别(例如 "saw")或完全省略它,解析器会返回一个 UnknownTool 而不是失败,从而你的代码能检测并处理这个幻觉。

添加描述:

你可以通过用 @Description 注解基类型和/或子类型来引导 LLM。基类型的描述会附加到 anyOf 元素上,而每个子类型的描述会附加到它各自的选项上:

java
@Description("A pet that lives in your home")
sealed interface Pet permits Hamster, Parrot {}

@Description("A small caged rodent kept as a pet")
record Hamster(String name, double weightGrams) implements Pet {}

@Description("A talking bird that can mimic human speech")
record Parrot(String name, int vocabulary) implements Pet {}

当省略 @Description 时,描述会回退到简单类名(例如 "Hamster"),因此 LLM 对每个选项仍有一个标签。

递归多态类型:

一个其子类型把它作为字段包含在内的多态基同样可行:

java
sealed interface ExpressionNode permits Literal, BinaryOp {}

record Literal(int value) implements ExpressionNode {}

record BinaryOp(String operator, ExpressionNode left, ExpressionNode right) implements ExpressionNode {}

递归多态 schema 需要一个支持 $ref / $defs 的模型(目前为 Azure OpenAI、Mistral 与 OpenAI)。

判别字段冲突:

若某子类型声明了一个与判别同名的字段(例如一个仅在密封基上的 type 字段),schema 生成会带着清晰的提示失败。修复选项:

  • 重命名字段,或
  • 用 @JsonTypeInfo(property = "...") 选一个不同的判别名,或
  • 若该字段是子类型中有意为之的一部分,设置 @JsonTypeInfo(visible = true),或
  • 当子类型上的字段是判别的权威来源时,使用 @JsonTypeInfo(include = As.EXISTING_PROPERTY)。

限制

当结合 AI 服务使用 JSON Schema 时,有一些限制:

  • 它只对受支持的 Amazon Bedrock、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 是标量、enum 或 POJO
    • 多态类型(密封接口/类,或用 Jackson @JsonSubTypes 注解的类型)
  • 递归目前仅由 Azure OpenAI、Mistral 与 OpenAI 支持。
  • 多态类型需要一个在 JSON schema 中支持 anyOf 的 LLM。
  • 当 LLM 不支持 JSON Schema 特性、或它未启用、或类型不受支持时,AI 服务会回退到提示词。

提示词 + JSON 模式

More info is coming soon. In the meantime, please read this section and this article.

提示词

当使用提示词时(这是默认选择,除非启用了对 JSON schema 的支持),AI 服务会自动生成格式指令,并把它们追加到 UserMessage 末尾,指明 LLM 应以其响应的格式。在方法返回之前,AI 服务会把 LLM 的输出解析为期望的类型。

你可以通过启用日志来观察被追加的指令。

NOTE

这种方式相当不可靠。若 LLM 与 LLM 提供者支持上面描述的那些方法,最好使用那些。

受支持的类型

类型JSON Schema提示词
POJO✅✅
List<POJO>、Set<POJO>✅❌
Enum✅✅
List<Enum>、Set<Enum>✅✅
List<String>、Set<String>✅✅
多态(密封 / @JsonSubTypes),含 List/Set✅❌
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);
}

相关教程

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。