文章目录
- 一、前言
- [二、JSON Schema](#二、JSON Schema)
-
- [1. ChatModel](#1. ChatModel)
-
- [1.1 基本使用](#1.1 基本使用)
- [1.2 注意事项](#1.2 注意事项)
- [2. Ai Service](#2. Ai Service)
-
- [2.1 基本使用](#2.1 基本使用)
- [2.1 高级特性](#2.1 高级特性)
-
- [2.1.1 Required and Optional](#2.1.1 Required and Optional)
- [2.2.2 Adding Description](#2.2.2 Adding Description)
- [2.3 Limitations](#2.3 Limitations)
- [三、Prompting + JSON Mode](#三、Prompting + JSON Mode)
- 四、Prompting
- 五、参考内容
一、前言
本系列内容来源于 LangChain4J 官网,内容稍作改删,仅做个人笔记使用。
本系列使用 LangChain4J 版本:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.8.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
本系列完整代码地址 :langchain4j-hwl
大语言模型(LLM)及其服务商普遍支持生成 JSON 格式的结构化输出,这类输出可便捷地映射为 Java 对象,进而在应用程序的其他模块中复用。
例如,假设我们有一个Person类:
java
record Person(String name, int age, double height, boolean married) {
}
我们需要从以下非结构化文本中提取出上述 Person 对象对应的信息:
java
约翰今年42岁,过着独立的生活。他身高1.75米,举止自信。目前未婚,他享有专注于个人目标和兴趣的自由。
针对上述需求,根据所使用的 LLM 及服务商不同,实现这一目标主要有三种方式:
- JSON Schema(JSON 模式)
- Prompting + JSON Mode(提示词 + JSON 模式)
- Prompting(仅提示词)
下面我们一一来看。
二、JSON Schema
JSON Schema 就是给 JSON 数据写的「数据说明书 + 校验规则」,用来规定一段 JSON 必须长什么样、有哪些字段、字段是什么类型。
而部分大语言模型(LLM)服务商(当前涵盖 Amazon Bedrock、Azure OpenAI、Google AI Gemini、Mistral、Ollama 和 OpenAI)支持为目标输出指定 JSON Schema(JSON 模式)。
- 支持该功能的服务商可在 文档 中的 JSON Mode 列中查询。
- JSON模式是在向LLM提供商的API发出的请求中的一个专用属性中指定,不需要在提示词(例如,在系统消息或用户消息中)包含任何自由格式的指令。
LangChain4j在 low-level 的 ChatModel API 和 high-level 的 AI Service API 中均支持JSON模式功能,下面我们具体来看。
1. ChatModel
1.1 基本使用
在使用 ChatModel API 时,我们可以在创建 ChatRequest 时通过 LLM 无关的 ResponseFormat 和 JsonSchema 来指定JSON模式,如下:
java
// json 对应对象
public record Person(String name, int age, double height, boolean married) {
}
@SneakyThrows
@Override
public String chatByChatModel(String userMessage) {
ResponseFormat responseFormat = ResponseFormat.builder()
// 设置模型返回类型
.type(ResponseFormatType.JSON)
// 构建 jsonSchema
.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();
// 构建请求
ChatRequest chatRequest = ChatRequest.builder()
.responseFormat(responseFormat)
.messages(UserMessage.from(userMessage))
.build();
// 发起请求
ChatResponse chatResponse = chatModel.chat(chatRequest);
// 打印响应内容
String output = chatResponse.aiMessage().text();
// {"name":"约翰","age":42,"height":1.75,"married":false}
System.out.println(output);
Person person = new ObjectMapper().readValue(output, Person.class);
// Person[name=约翰, age=42, height=1.75, married=false]
System.out.println(person);
return person.toString();
}
1.2 注意事项
JSON Schema 使用中有几个注意点, 如下:
-
JSON Schema 的根元素默认必须是
JsonObjectSchema(对应 Java 中的对象/实体类,比如Person类);- Amazon Bedrock、Azure OpenAI 等主流服务商,支持
JsonRawSchema作为根元素(允许直接传入自定义的完整 JSON Schema 字符串,无需用 LangChain4j 内置类型构建); - Google Gemini 额外支持
JsonEnumSchema(枚举类型)、JsonArraySchema(数组/集合类型)作为根元素(比如直接让 LLM 输出一个枚举数组,无需包裹在对象里)。
- Amazon Bedrock、Azure OpenAI 等主流服务商,支持
-
所有需要强制返回的属性(比如
Person中的name),必须通过required()显式声明;未声明的属性会被视为「可选」,LLM 可能不返回,Java 解析时会用默认值填充(比如
int类型默认 0)。 -
LangChain4j 把 JSON 类型和 Java 类型做了一一映射,每个子类型对应特定的 Java 数据类型,核心用途如下:
子类型 核心作用 JsonObjectSchema对应 Java 实体类/记录(如 Person),支持嵌套属性(比如Person里包含Address对象)JsonStringSchema、JsonIntegerSchema、JsonNumberSchema、JsonBooleanSchema映射 Java 基础类型/包装类(如 String、int、double、boolean)JsonEnumSchema映射 Java 枚举类型(如 Sentiment枚举:POSITIVE/NEGATIVE)JsonArraySchema映射 Java 集合/数组(如 List<String>、Set<Person>)JsonReferenceSchema支持递归结构(比如「人物」包含「子女列表」,子女又是「人物」类型) JsonAnyOfSchema支持多态(比如「形状」可以是「圆形」或「矩形」,两种不同结构) JsonNullSchema支持可空类型(允许属性值为 null)JsonRawSchema自定义完整 JSON Schema(适配特殊场景,无需遵循 LangChain4j 内置类型规则)
2. Ai Service
2.1 基本使用
Ai Service 可以更方便的实现 JSON 功能,如下:
java
@Bean
@Primary
public OpenAiChatModel openAiChatModel() {
return OpenAiChatModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
// 启用JSON Schema
.supportedCapabilities(Capability.RESPONSE_FORMAT_JSON_SCHEMA)
.modelName("gpt-4o-mini")
.build();
}
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel")
public interface PersonExtractor {
/**
* 从文本中提取Person对象
*
* @param text 包含Person信息的文本
* @return 提取到的Person对象
*/
StructuredLlmServiceImpl.Person extractPersonFrom(String text);
}
...
// 调用
@Override
public String chatByAiService(String userMessage) {
return personExtractor.extractPersonFrom(userMessage).toString();
}
注 : 官方提供更多的模型使用示例,每个模型的使用方式可能各有不同,需要因模型而异。
2.1 高级特性
JSON Schema 存在一些高级特性,下面我们具体来看。
2.1.1 Required and Optional
LangChain4j 把 JSON Schema 所有字段默认设为「可选」,本质是规避 LLM 的「幻觉问题」:
- 当 LLM 从文本中提取信息时,如果某个字段没有足够的上下文信息,不会强行编造虚假数据,而是直接忽略该字段; 如若文本只有「John」没有姓氏,LLM 不会虚构「John Doe」,只会返回 {"name":"John"},而非 {"name":"John","surname":"Doe"}。
需要注意的是:
- 对于 int、boolean 等基本类型的可选字段,即使 LLM 未返回该字段值,也会按 Java 规则填充默认值:
- 如:int → 0、boolean → false、double → 0.0 等;
- 举例:若 Person 有 int age 字段且为可选,文本无年龄信息时,返回 {"name":"John","age":0}。
- 开启 strictJsonSchema(true)(严格模式)本是为了强制 LLM 遵循 Schema 规则,但对可选的枚举字段仍有漏洞:
- LLM 可能无视枚举限定值(比如枚举是 ["MAN","WOMAN"],却返回 "UNKNOWN");
- 原因:枚举的可选性让 LLM 仍有「自由发挥」空间,严格模式无法完全约束。
- 如果想要一个字段为必填项,可以使用
@JsonProperty(required = true)对其进行注解:
java
record Person(@JsonProperty(required = true) String name, String surname) {
}
interface PersonExtractor {
Person extractPersonFrom(String text);
}
- 当 JSON Schema 用于「工具调用(Function Calling)」时,规则完全相反:
- 所有字段默认「必填」,LLM 必须提取所有字段值(哪怕需要编造),确保工具能拿到完整参数;
- 这是因为工具调用对参数完整性要求高(比如调用「查询用户信息」工具,必须传 name 才能执行)。
2.2.2 Adding Description
当 LLM 生成的结构化输出不符合预期时(比如字段格式错误、取值不符合要求),可以给定义结构化数据的类(如 Person 记录类)和字段添加 @Description 注解:
- 对类的注解(如
@Description("a person")):告诉 LLM 这个类整体代表什么含义; - 对字段的注解(如
@Description("person's age, for example: 42")):明确字段的含义、格式、示例值,引导 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) {
}
这些注解会被融入给 LLM 的提示词或 JSON Schema 中,提升输出准确性。
@Description 注解仅对类/记录类 、类的字段 有效,直接标注在枚举(enum)的枚举值上时(如 Priority 中的 CRITICAL/HIGH/LOW):
- LangChain4j 会忽略这些注解,不会将其加入生成的 JSON Schema;
- 也不会把这些描述传递给 LLM,无法通过这种方式引导 LLM 理解枚举值的含义。
如下:
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
}
2.3 Limitations
JSON Schema + AI Services 方案的存在一些使用局限,具体来说如下:
-
仅能在 Amazon Bedrock、Azure OpenAI、Google AI Gemini、Mistral、Ollama、OpenAI 这些特定模型上使用,其他 LLM 模型不支持该特性。
-
配置
ChatModel时必须显式开启 JSON Schema 支持(默认关闭),否则该功能不会生效。 -
如果你的场景需要 LLM 流式返回结果(比如实时输出内容),JSON Schema 方案无法使用,只能用其他方式。
-
数据类型有严格限制 : 支持的 POJO 内容:仅包含标量类型(String/int/boolean 等)、枚举、嵌套 POJO、以及元素为上述类型的集合/数组(List/Set/数组)。同时不支持的如下特性:
- 递归仅 Azure OpenAI/Mistral/OpenAI 能用;
- 完全不支持多态(不能用接口/抽象类,必须是具体的实体类);
- 部分基础类型(如 byte/short/BigInteger 等)也不兼容 JSON Schema(但提示词方式支持)。
-
只要满足「模型不支持、功能未启用、类型不兼容」任一条件,AI Service 会自动放弃 JSON Schema,转而使用「提示词引导 LLM 输出」的方式(这种方式可靠性更低)。
三、Prompting + JSON Mode
官方还没给出具体内容
四、Prompting
Prompting (提示词) 方式是 LangChain4j 实现结构化输出的默认方案,只有当你显式启用「JSON Schema 功能」时,才会优先使用更可靠的 JSON Schema 方式,否则自动走提示词方式。
当未启用 JSON Schema 功能时,AI 服务会默认采用「提示词」方式:
- 自动生成格式指令(比如要求 LLM 返回 JSON 格式),并把这些指令追加到用户消息(UserMessage)末尾;
- LLM 按提示返回内容后,AI 服务会将返回结果解析成开发者期望的 Java 类型(如 Person 类、枚举等)。
这种纯靠提示词约束格式的方式可靠性低------LLM 可能忽略提示、返回格式不规范的内容(比如 JSON 语法错误),导致解析失败。因此文档明确建议:如果 LLM/服务商支持 JSON Schema 等更严谨的方式,优先使用前者。
下面是一些模型的特性支持情况
| 类型分类 | JSON Schema 支持情况 | Prompting 支持情况 | 典型场景 |
|---|---|---|---|
| 对象及对象集合 | |||
单个 POJO(如 Person) |
✅ | ✅ | 提取单条结构化数据(如用户信息) |
POJO 集合(List<Person>) |
✅ | ❌ | 提取多条对象数据(如多用户列表) |
| 枚举及枚举集合 | |||
单个枚举(如 Sentiment) |
✅ | ✅ | 分类结果(如情感倾向) |
枚举集合(List<Sentiment>) |
✅ | ✅ | 多分类结果(如多段文本情感) |
| 基础数据类型 | |||
常用类型(int/double/boolean 等) |
✅ | ✅ | 提取数值、布尔值(如年龄、婚姻状态) |
特殊类型(byte/BigInteger/short) |
❌ | ✅ | 处理高精度数值、字节数据 |
| 时间与键值对类型 | |||
时间类型(Date/LocalDate 等) |
❌ | ✅ | 提取时间信息(如生日、创建时间) |
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);
}
最终的使用建议如下:
- 若场景是「提取 POJO/枚举/常用基础类型」(如从文本中提取用户信息、分类标签):优先用 JSON Schema(可靠性更高,且能自动映射为 Java 对象)。
- 若场景涉及「时间类型、
Map、BigInteger等特殊类型」:只能用 Prompting,但需额外通过提示词约束格式(如明确要求时间格式为"yyyy-MM-dd"),并做好结果校验。 - 若需「提取 POJO 集合」(如多用户列表):只能用 JSON Schema(Prompting 不支持)。