让 LLM 返回类型安全的 Java 对象(POJO、Record、Enum),而非无结构的 String。本章深入解析 JSON Schema 模式、JsonSchemaElement 体系、多态类型支持、以及三种输出方式的可靠性对比。
7.1 为什么结构化输出很重要?
csharp
❌ 自由文本输出:
LLM: "姓名:张三,年龄:30,身高:1.78,已婚"
→ 需要手写正则/字符串解析,脆弱且不可靠
✅ 结构化输出:
LLM: {"name":"张三","age":30,"height":1.78,"married":true}
→ 直接反序列化为 Person record,编译期类型安全
7.2 三种输出方式(可靠性从高到低)
| 方式 | 可靠性 | Provider 支持 | 配置复杂度 |
|---|---|---|---|
| JSON Schema | ⭐⭐⭐⭐⭐ | OpenAI, Gemini, Mistral, Ollama, Bedrock, Azure | 低(AI Services 自动) |
| Prompting + JSON Mode | ⭐⭐⭐ | 部分 Provider | 中(需配置 responseFormat) |
| Prompting | ⭐⭐ | 所有 | 低(默认,但不推荐) |
方式3:Prompting(默认,不可靠)
java
// AI Services 默认行为:在用户消息末尾追加格式指令
interface PersonExtractor {
Person extractPersonFrom(String text);
}
// 自动追加: "You must answer strictly in the following JSON format: {...}"
// ⚠️ LLM 有时不遵循指令,输出包含额外解释文字
方式2:JSON Mode(Provider 特定配置)
java
// OpenAI 旧模型
OpenAiChatModel.builder()
.responseFormat("json_object")
.build();
// Google Gemini
GoogleAiGeminiChatModel.builder()
.responseFormat(ResponseFormat.JSON)
.build();
// Ollama
OllamaChatModel.builder()
.responseFormat(ResponseFormat.JSON)
.build();
方式1:JSON Schema(最可靠,推荐)
java
ChatModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) // 显式启用
.strictJsonSchema(true) // 严格模式
.build();
interface PersonExtractor {
Person extractPersonFrom(String text);
}
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
Person person = extractor.extractPersonFrom("张三,30岁,身高1米78,已婚");
// 框架自动:生成 JSON Schema → 发给 API → 解析返回
触发条件(必须全部满足):
- 方法返回 POJO
ChatModel支持 JSON Schema(需要通过supportedCapabilities开启)- AI Service 支持该功能
7.3 Provider JSON Schema 配置速查
java
// OpenAI / Azure OpenAI
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
.strictJsonSchema(true)
// Google AI Gemini / Vertex AI Gemini
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
// Mistral AI
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
.strictJsonSchema(true)
// Ollama
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
// Amazon Bedrock
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
7.4 支持的返回类型
java
interface Assistant {
// === 基本类型 ===
boolean isPositive(String text);
int extractAge(String text);
double extractHeight(String text);
// === 枚举 ===
Sentiment analyzeSentiment(String text); // enum Sentiment { POSITIVE, NEGATIVE, NEUTRAL }
// === POJO / Record ===
Person extractPerson(String text);
// === 集合类型 ===
List<String> generateOutline(String topic);
List<Person> extractPeople(String text); // JSON Schema 支持
Set<Person> extractUniquePeople(String text); // JSON Schema 支持
List<Sentiment> analyzeSentiments(String text);
// === 多态类型 ===
Animal identifyAnimal(String description); // sealed interface Animal permits Dog, Cat
// === 日期类型(仅 Prompting 模式) ===
LocalDate extractDate(String text);
LocalDateTime extractDateTime(String text);
}
JSON Schema 与 Prompting 的支持矩阵
| 类型 | JSON Schema | Prompting |
|---|---|---|
POJO |
✅ | ✅ |
List<POJO>, Set<POJO> |
✅ | ❌ |
Enum |
✅ | ✅ |
List<Enum>, Set<Enum> |
✅ | ✅ |
List<String>, Set<String> |
✅ | ✅ |
| 多态类型(含 List/Set) | ✅ | ❌ |
| 基本类型(int, boolean, ...) | ✅ | ✅ |
BigInteger, BigDecimal |
✅ | ✅ |
Date, LocalDate, LocalTime, LocalDateTime |
❌ | ✅ |
Map<?, ?> |
❌ | ✅ |
7.5 @Description 注解
为 Schema 添加人类可读的描述,让 LLM 更准确理解字段含义:
java
@Description("a person with basic identity information")
record Person(
@Description("person's full name, e.g. 'John Doe'")
String name,
@Description("person's age in years, e.g. 42")
int age,
@Description("person's height in meters, e.g. 1.78")
double height,
@Description("whether the person is married, e.g. false")
boolean married
) {}
⚠️
@Description在 枚举值 上无效(不会写入 JSON Schema)。
7.6 Required vs Optional 字段
AI Services 默认:全部 Optional
java
record Person(String name, int age, double height, boolean married) {}
// 生成的 JSON Schema: required = [] (所有字段都是可选的)
// 原因: LLM 在缺少信息时会"编造"值,全 optional 比编造更好
副作用:
int→ 默认0boolean→ 默认false- 对象类型 →
null
⚠️ optional 的 enum 字段在严格模式下仍可能被 LLM 编造出不在枚举列表中的值。
用 @JsonProperty 标记 Required
java
record Person(
@JsonProperty(required = true) String name, // 必填
String surname // 可选
) {}
工具方法中的默认:全部 Required
java
// @Tool 方法中,所有参数默认 required=true
@Tool
void getWeather(
@P String city, // required
@P(required = false) TemperatureUnit unit // 显式设为 optional
) {}
7.7 低层 API:手动构建 JsonSchema
当 AI Services 的自动生成不够灵活时,手动控制:
JsonSchemaElement 层次结构
arduino
JsonSchemaElement (接口)
├── JsonObjectSchema --- 对象类型(通常作为 rootElement)
├── JsonStringSchema --- String, char, Character
├── JsonIntegerSchema --- int, long, BigInteger
├── JsonNumberSchema --- float, double, BigDecimal
├── JsonBooleanSchema --- boolean, Boolean
├── JsonEnumSchema --- enum 类型
├── JsonArraySchema --- List, Set, 数组
├── JsonReferenceSchema --- 递归引用(树形结构)
├── JsonAnyOfSchema --- 多态类型
├── JsonNullSchema --- nullable
└── JsonRawSchema --- 自定义原始 JSON Schema
完整示例
java
// 构建 Person Schema
JsonObjectSchema personSchema = JsonObjectSchema.builder()
.addStringProperty("name", "person's full name")
.addIntegerProperty("age", "age in years")
.addNumberProperty("height", "height in meters")
.addBooleanProperty("married", "marital status")
.required("name", "age", "height", "married")
.build();
// 封装为 ResponseFormat
ResponseFormat responseFormat = ResponseFormat.builder()
.type(ResponseFormatType.JSON)
.jsonSchema(JsonSchema.builder()
.name("Person") // OpenAI 要求指定 schema name
.rootElement(personSchema)
.build())
.build();
// 用于 ChatRequest
ChatRequest request = ChatRequest.builder()
.messages(UserMessage.from("提取以下文本中的人物信息:..."))
.responseFormat(responseFormat)
.build();
ChatResponse response = model.chat(request);
Person person = new ObjectMapper().readValue(
response.aiMessage().text(), Person.class);
枚举 Schema
java
JsonEnumSchema enumSchema = JsonEnumSchema.builder()
.description("Marital status of the person")
.enumValues(List.of("SINGLE", "MARRIED", "DIVORCED"))
.build();
数组 Schema
java
JsonArraySchema arraySchema = JsonArraySchema.builder()
.description("All names of the people found in the text")
.items(JsonStringSchema.builder().build())
.build();
7.8 多态类型(Polymorphic Types)
密封接口(推荐,零注解)
java
sealed interface Animal permits Dog, Cat {}
record Dog(String name, String breed) implements Animal {}
record Cat(String name, boolean indoor) implements Animal {}
interface AnimalIdentifier {
Animal identifyFrom(String description);
}
// "Rex is a Labrador." → LLM 返回 {"type":"Dog","name":"Rex","breed":"Labrador"}
// 框架自动反序列化为 Dog 实例
Jackson 注解(需要自定义判别器名)
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 {}
判别器名称解析顺序:
@JsonSubTypes.Type(name = "...")@JsonTypeName注解Class.getSimpleName()(默认)
defaultImpl:对抗 LLM 幻觉
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 {
String use();
}
record UnknownTool() implements Tool {
public String use() {
return "Unknown tool --- cannot operate";
}
}
// LLM 编造 "saw" → 反序列化为 UnknownTool 而非抛异常
多态字段碰撞检查
如果子类型中有与判别器同名的字段,框架会在 Schema 生成时抛出清晰的错误消息:
scala
Field 'type' in subtype Dog conflicts with discriminator property.
To fix this, either rename the field, change the discriminator property name via @JsonTypeInfo,
or set visible=true to keep the discriminator on the bean.
7.9 递归类型(树形结构)
java
record TreeNode(
String name,
List<TreeNode> children // 递归引用
) {}
// 框架自动生成: 使用 JsonReferenceSchema 和 $defs
⚠️ 递归类型目前只被 Azure OpenAI、Mistral、OpenAI 支持。
7.10 JsonRawSchema:完全自定义
当需求超出框架 Schema DSL 的能力:
java
String rawSchema = """
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名" },
"date": { "type": "string", "format": "date" }
},
"required": ["city"],
"additionalProperties": false
}
""";
JsonRawSchema schema = JsonRawSchema.from(rawSchema);
7.11 JSON Mode vs Tools(Function Calling)
两者都涉及 JSON,但用途完全不同:
| 维度 | JSON Mode / Structured Outputs | Tools / Function Calling |
|---|---|---|
| 目的 | 从非结构化文本中提取结构化数据 | 让 LLM 触发真实世界的操作 |
| 调用方式 | 每次都是 responseFormat |
LLM 决定是否调用、调用哪个 |
| 典型场景 | 简历解析、发票提取、情感分析 | 查数据库、调 API、发邮件、计算 |
| Schema 角色 | 定义输出格式 | 定义工具输入参数格式 |
| 循环 | 单次调用 | 可能多轮(执行→反馈→再推理) |
7.12 最佳实践
| 建议 | 说明 |
|---|---|
| 优先 JSON Schema | Prompting 不可靠,尽量避免 |
| 字段全 Optional | 让 LLM 省略不确定的字段,比编造值更好 |
| 加 @Description | 描述的质量直接影响提取的准确度 |
| 严格模式 | OpenAI/Azure/Mistral 开启 strictJsonSchema(true) |
| defaultImpl | 多态类型永远提供默认实现,对抗 LLM 幻觉 |
| 测试驱动 | 对不同输入写集成测试,验证提取准确性 |