文章目录
- [1. 创建 BeanOutputConverter](#1. 创建 BeanOutputConverter)
-
- [1.1 构造方法](#1.1 构造方法)
- [1.2 创建 Cleaner](#1.2 创建 Cleaner)
- [1.3 自动生成 JSON Schema](#1.3 自动生成 JSON Schema)
- [2. 执行请求](#2. 执行请求)
-
- [2.1 请求上下文](#2.1 请求上下文)
- [2.2 原生模式](#2.2 原生模式)
- [2.3 非原生模式](#2.3 非原生模式)
- [3. 输出转换](#3. 输出转换)
-
- [3.1 提取文本内容](#3.1 提取文本内容)
- [3.2 清洗](#3.2 清洗)
- [3.3 转换](#3.3 转换)
1. 创建 BeanOutputConverter
1.1 构造方法
entity 方法会自动创建 BeanOutputConverter:
java
@Override
@Nullable
public <T> T entity(Class<T> type) {
Assert.notNull(type, "type cannot be null");
// 1. 根据传入的 Class 自动创建 BeanOutputConverter
var outputConverter = new BeanOutputConverter<>(type);
// 2. 进入核心执行方法
return doSingleWithBeanOutputConverter(outputConverter);
}
BeanOutputConverter 构造方法中会初始化各种属性:
java
private BeanOutputConverter(Type type, ObjectMapper objectMapper, ResponseTextCleaner textCleaner) {
this.logger = LoggerFactory.getLogger(BeanOutputConverter.class);
Objects.requireNonNull(type, "Type cannot be null;");
this.type = type;
this.objectMapper = objectMapper != null ? objectMapper : this.getObjectMapper();
this.textCleaner = textCleaner != null ? textCleaner : createDefaultTextCleaner();
this.generateSchema();
}
对象信息如下:

1.2 创建 Cleaner
构造方法中会初始化多个清理器,用于在解析 LLM 响应文本之前,对文本进行清理预处理,处理不同 AI 模型的各种响应格式和模式。
WhitespaceCleaner:去除文本首尾空白字符ThinkingTagCleaner:移除Markdown代码块格式MarkdownCodeBlockCleaner:移除AI模型的"思考标签"(reasoning tokens)
java
private static ResponseTextCleaner createDefaultTextCleaner() {
return CompositeResponseTextCleaner.builder()
.addCleaner(new WhitespaceCleaner())
.addCleaner(new ThinkingTagCleaner())
.addCleaner(new MarkdownCodeBlockCleaner())
.addCleaner(new WhitespaceCleaner()) // Final trim after all cleanups
.build();
}
1.3 自动生成 JSON Schema
还会自动生成 JSON Schema :
java
/**
* Generates the JSON schema for the target type.
* 为目标 Java 类型生成 JSON Schema。
*/
private void generateSchema() {
// 创建 Jackson 模块,配置:
// - RESPECT_JSONPROPERTY_REQUIRED: 尊重 @JsonProperty(required=true) 注解
// - RESPECT_JSONPROPERTY_ORDER: 尊重 @JsonPropertyOrder 注解的字段顺序
JacksonModule jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED,
JacksonOption.RESPECT_JSONPROPERTY_ORDER);
// 构建 Schema 生成器配置:
// - DRAFT_2020_12: 使用 JSON Schema 2020-12 版本
// - PLAIN_JSON: 生成纯 JSON 格式(不含 Java 类型信息)
// - FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT: 默认禁止额外属性(additionalProperties: false)
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(
com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12,
com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON)
.with(jacksonModule)
.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT);
// 将所有字段标记为 required(必填)
configBuilder.forFields().withRequiredCheck(f -> true);
// 如果 Kotlin 反射存在,添加 Kotlin 模块支持(处理 Kotlin data class 等)
if (KotlinDetector.isKotlinReflectPresent()) {
configBuilder.with(new KotlinModule());
}
// 构建 Schema 生成器并生成 JSON Schema
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonNode = generator.generateSchema(this.type);
// 后置处理:允许子类自定义 Schema
postProcessSchema(jsonNode);
// 格式化输出:使用美化打印,确保可读性
ObjectWriter objectWriter = this.objectMapper.writer(new DefaultPrettyPrinter()
.withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator())));
// 将 JsonNode 转换为 JSON 字符串
try {
this.jsonSchema = objectWriter.writeValueAsString(jsonNode);
}
catch (JsonProcessingException e) {
logger.error("Could not pretty print json schema for jsonNode: {}", jsonNode);
throw new RuntimeException("Could not pretty print json schema for " + this.type, e);
}
}
生成关键配置说明:
| 配置项 | 作用 |
|---|---|
| RESPECT_JSONPROPERTY_REQUIRED | 读取 @JsonProperty(required=true) 注解 |
| RESPECT_JSONPROPERTY_ORDER | 读取 @JsonPropertyOrder 注解控制字段顺序 |
| DRAFT_2020_12 | 使用最新 JSON Schema 规范版本 |
| PLAIN_JSON | 生成纯 JSON,不含 Java 类型元数据 |
| FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT | 设置 additionalProperties: false |
| withRequiredCheck(f -> true) | 所有字段默认必填 |
2. 执行请求
2.1 请求上下文
在 doSingleWithBeanOutputConverter 方法中,会先判断当前转换器中是否包含 Format 格式化指令,有则添加到上下文中:
java
// ====================== 步骤1:设置格式提示(兜底方案:所有模型都能用)======================
if (StringUtils.hasText(outputConverter.getFormat())) {
// 把转换器生成的格式指令(如 JSON、Schema)放入请求上下文
// 作用:框架会自动将 {format} 拼接到 Prompt 末尾
this.request.context()
.put(ChatClientAttributes.OUTPUT_FORMAT.getKey(),
outputConverter.getFormat());
}
BeanOutputConverter 默认的指令如下:
java
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
在 getFormat() 方法中,默认也会将 JsonSchema 也拼接到一起:
java
@Override
public String getFormat() {
String template = """
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```%s```
""";
return String.format(template, this.jsonSchema);
}
最终 spring.ai.chat.client.output.format 属性其实包含了格式指令、 输出 JsonSchema :
json
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"contractName" : {
"type" : "string"
},
"contractNo" : {
"type" : "string"
},
"signDate" : {
"type" : "string"
},
"partyA" : {
"type" : "string"
},
"partyB" : {
"type" : "string"
},
"amount" : {
"type" : "number"
},
"paymentMethod" : {
"type" : "string"
},
"remark" : {
"type" : "string"
},
"validUntil" : {
"type" : "string"
}
},
"required" : [ "contractName", "contractNo", "signDate", "partyA", "partyB", "amount", "paymentMethod", "remark", "validUntil" ],
"additionalProperties" : false
}```
接着判断 STRUCTURED_OUTPUT_NATIVE 属性,有则添加 spring.ai.chat.client.structured.output.schema :
java
// ====================== 步骤2:如果开启原生结构化输出 → 注入 JSON Schema ====================
// 判断条件:
// 1. 上下文开启了 NATIVE 模式(ENABLE_NATIVE_STRUCTURED_OUTPUT)
// 2. 转换器必须是 BeanOutputConverter(只有它能生成 JSON Schema)
if (this.request.context().containsKey(ChatClientAttributes.STRUCTURED_OUTPUT_NATIVE.getKey())
&& outputConverter instanceof BeanOutputConverter beanOutputConverter) {
// 将自动生成的 JSON Schema 放入上下文
// 作用:传递给模型客户端(如 OpenAI),调用模型原生 Structured Output API
this.request.context()
.put(ChatClientAttributes.STRUCTURED_OUTPUT_SCHEMA.getKey(),
beanOutputConverter.getJsonSchema());
}
BeanOutputConverter 默认生成的 Schme 如下:
json
{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"contractName" : {
"type" : "string"
},
"contractNo" : {
"type" : "string"
},
"signDate" : {
"type" : "string"
},
"partyA" : {
"type" : "string"
},
"partyB" : {
"type" : "string"
},
"amount" : {
"type" : "number"
},
"paymentMethod" : {
"type" : "string"
},
"remark" : {
"type" : "string"
},
"validUntil" : {
"type" : "string"
}
},
"required" : [ "contractName", "contractNo", "signDate", "partyA", "partyB", "amount", "paymentMethod", "remark", "validUntil" ],
"additionalProperties" : false
}
最终请求上下文对象对下:

2.2 原生模式
设置好请求上下文后,进入到调用 AI 模型阶段:
java
// ====================== 步骤3:调用 AI 模型 ====================
// 执行真正的模型调用
var chatResponse = doGetObservableChatClientResponse(this.request).chatResponse();
在 ChatModelCallAdvisor 中,如果同时满足以下三个条件,会将 OutputSchema 配置到 ChatOptions 中:
context中设置了STRUCTURED_OUTPUT_NATIVE标志(用户通过advisor启用)outputSchema不为空(由BeanOutputConverter从目标Java类生成)ChatOptions实现了StructuredOutputChatOptions接口(模型支持原生结构化输出)
因为我们使用的是 ZhiPuAiChatOptions 并没有实现 StructuredOutputChatOptions 接口:
java
public class ZhiPuAiChatOptions implements ToolCallingChatOptions {
所以,这里不会在 ChatOptions 中配置 JSON Schema :
java
if (chatClientRequest.context().containsKey(ChatClientAttributes.STRUCTURED_OUTPUT_NATIVE.getKey())
&& StringUtils.hasText(outputSchema) && chatClientRequest.prompt()
.getOptions() instanceof StructuredOutputChatOptions structuredOutputChatOptions) {
// 将 JSON Schema 设置到模型的原生结构化输出配置中。
// 模型 API 将使用此 schema 来保证输出格式符合要求。
structuredOutputChatOptions.setOutputSchema(outputSchema);
// 返回原始请求,不修改 prompt。
// prompt 保持干净------不会追加任何格式指令文本。
return chatClientRequest;
}
// .............
2.3 非原生模式
继续执行,通过 prompt 文本指令引导模型输出指定格式,而非使用模型的原生 API :
java
Prompt augmentedPrompt = chatClientRequest.prompt()
.augmentUserMessage(userMessage -> userMessage.mutate()
.text(userMessage.getText() + System.lineSeparator() + outputFormat)
.build());
return ChatClientRequest.builder()
.prompt(augmentedPrompt)
.context(Map.copyOf(chatClientRequest.context()))
.build();
此时,提示词内容如下:
json
ChatClientRequest[prompt=Prompt{messages=[UserMessage{content='2026年度办公设备采购合同
合同编号:HT20260313
甲方:北京科技有限公司
乙方:上海办公设备销售有限公司
甲乙双方经友好协商,就办公设备采购事宜达成如下协议:
第一条 采购内容
甲方向乙方采购办公设备一批,具体型号及数量详见附件清单。
第二条 合同金额
合同总金额为人民币贰万伍仟捌佰元整(¥25,800.00元)。
第三条 付款方式
合同签订后分3期支付:首付款30%于签约时支付,二期款40%于设备到货时支付,尾款30%于验收合格后支付。
第四条 交货期限
乙方须于2026年04月01日前完成全部设备交付及安装调试。
第五条 质量保证
乙方保证所供设备为全新正品,符合国家质量标准,提供一年免费保修服务。
第六条 违约责任
任何一方违约,应向守约方支付合同金额10%的违约金。
第七条 本合同一式两份,双方各执一份,自签订之日起生效,有效期至2027年03月12日。
甲方(盖章): 北京科技有限公司
乙方(盖章): 上海办公设备销售有限公司
签订日期: 2026年03月13日
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"contractName" : {
"type" : "string"
},
"contractNo" : {
"type" : "string"
},
"signDate" : {
"type" : "string"
},
"partyA" : {
"type" : "string"
},
"partyB" : {
"type" : "string"
},
"amount" : {
"type" : "number"
},
"paymentMethod" : {
"type" : "string"
},
"remark" : {
"type" : "string"
},
"validUntil" : {
"type" : "string"
}
},
"required" : [ "contractName", "contractNo", "signDate", "partyA", "partyB", "amount", "paymentMethod", "remark", "validUntil" ],
"additionalProperties" : false
}```
', metadata={messageType=USER}, messageType=USER}], modelOptions=ZhiPuAiChatOptions: {"response_format":{"type":"json_object"}}}, context={spring.ai.chat.client.structured.output.native=true, spring.ai.chat.client.structured.output.schema={
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"contractName" : {
"type" : "string"
},
"contractNo" : {
"type" : "string"
},
"signDate" : {
"type" : "string"
},
"partyA" : {
"type" : "string"
},
"partyB" : {
"type" : "string"
},
"amount" : {
"type" : "number"
},
"paymentMethod" : {
"type" : "string"
},
"remark" : {
"type" : "string"
},
"validUntil" : {
"type" : "string"
}
},
"required" : [ "contractName", "contractNo", "signDate", "partyA", "partyB", "amount", "paymentMethod", "remark", "validUntil" ],
"additionalProperties" : false
}, spring.ai.chat.client.output.format=Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"contractName" : {
"type" : "string"
},
"contractNo" : {
"type" : "string"
},
"signDate" : {
"type" : "string"
},
"partyA" : {
"type" : "string"
},
"partyB" : {
"type" : "string"
},
"amount" : {
"type" : "number"
},
"paymentMethod" : {
"type" : "string"
},
"remark" : {
"type" : "string"
},
"validUntil" : {
"type" : "string"
}
},
"required" : [ "contractName", "contractNo", "signDate", "partyA", "partyB", "amount", "paymentMethod", "remark", "validUntil" ],
"additionalProperties" : false
}```
}]
3. 输出转换
3.1 提取文本内容
在模型回复后,返回模型响应对象 ChatClientResponse :
java
var stringResponse = getContentFromChatResponse(chatResponse);
if (stringResponse == null) {
return null;
}

然后从 ChatResponse 中提取文本内容:
java
/**
* 从 ChatResponse 中提取文本内容。
*
* <p>提取路径:ChatResponse → Generation → AssistantMessage → text
*
* <pre>
* ChatResponse
* └─ results: List<Generation>
* └─ Generation
* └─ output: AssistantMessage
* └─ text: "响应文本内容"
* </pre>
*
* @param chatResponse 聊天响应对象,可为 null
* @return 响应文本内容,如果响应为空或没有内容则返回 null
*/
@Nullable
private static String getContentFromChatResponse(@Nullable ChatResponse chatResponse) {
return Optional.ofNullable(chatResponse)
.map(ChatResponse::getResult)
.map(Generation::getOutput)
.map(AbstractMessage::getText)
.orElse(null);
}
提取到的文本内容去下
java
{"contractName":"2026年度办公设备采购合同","contractNo":"HT20260313","signDate":"2026年03月13日","partyA":"北京科技有限公司","partyB":"上海办公设备销售有限公司","amount":25800,"paymentMethod":"合同签订后分3期支付:首付款30%于签约时支付,二期款40%于设备到货时支付,尾款30%于验收合格后支付。","remark":"乙方须于2026年04月01日前完成全部设备交付及安装调试。乙方保证所供设备为全新正品,符合国家质量标准,提供一年免费保修服务。任何一方违约,应向守约方支付合同金额10%的违约金。","validUntil":"2027年03月12日"}
3.2 清洗
接着调用输出转换器并返回:
java
return outputConverter.convert(stringResponse);
第一步会先进行清洗:
java
@Override
public T convert(@NonNull String text) {
try {
// Clean the text using the configured text cleaner
text = this.textCleaner.clean(text);
return (T) this.objectMapper.readValue(text, this.objectMapper.constructType(this.type));
}
catch (JsonProcessingException e) {
logger.error(SENSITIVE_DATA_MARKER,
"Could not parse the given text to the desired target type: \"{}\" into {}", text, this.type);
throw new RuntimeException(e);
}
}
默认的清理器有:

它们都实现了 ResponseTextCleaner 接口:
java
@FunctionalInterface
public interface ResponseTextCleaner {
/**
* Clean the given text by removing unwanted patterns, tags, or formatting.
* @param text the raw text from LLM response
* @return the cleaned text ready for parsing
*/
String clean(String text);
}
3.3 转换
最后调用 ObjectMapper 将文本转换为 Java 对象:
java
return (T) this.objectMapper.readValue(text, this.objectMapper.constructType(this.type));