文章目录
- [1. 前言](#1. 前言)
- [2. 单个对象](#2. 单个对象)
-
- [2.1 定义 Java 对象](#2.1 定义 Java 对象)
- [2.2 开启原生结构化输出](#2.2 开启原生结构化输出)
-
- [2.2.1 模型参数](#2.2.1 模型参数)
- [2.2.2 ENABLE_NATIVE_STRUCTURED_OUTPUT 属性](#2.2.2 ENABLE_NATIVE_STRUCTURED_OUTPUT 属性)
- [2.3 调用 entity 方法](#2.3 调用 entity 方法)
- [3. 泛型集合类型](#3. 泛型集合类型)
-
- [3.1 List](#3.1 List)
- [3.2 Map](#3.2 Map)
- [4. responseEntity()](#4. responseEntity())
- [5. 自定义 StructuredOutputConverter](#5. 自定义 StructuredOutputConverter)
1. 前言
在之前我们提到过,想要实现结构化输出需要:
- 向提示词追加格式要求指令
- 将模型输出转换为结构化类型实例
分为两种模式:
- 非原生模式 :追加格式指令文本、
Schema要用户提示词中 - 原生模式 :需要模型支持原生结构化输出,通过
ChatOptions传递Schema,更加可靠稳定
接下来,我们使用 Spring AI 提供的结构化输出功能,进行案例演示。
2. 单个对象
演示需求 :提取合同中的关键信息为 Java 对象。
2.1 定义 Java 对象
定义合同信息 record 类:
java
/**
* 合同信息实体类(适配 Spring AI 原生结构化输出)
*/
public record ContractInfo(
/**
* 合同编号(必填,格式:HT开头+8位数字)
*/
@JsonProperty("contractNo") // 指定 JSON 字段名(确保和大模型输出映射)
String contractNo,
/**
* 合同名称(必填)
*/
@JsonProperty("contractName")
String contractName,
/**
* 签订日期(必填,ISO-8601格式:yyyy-MM-dd)
*/
@JsonProperty("signDate")
String signDate,
/**
* 甲方名称(必填)
*/
@JsonProperty("partyA")
String partyA,
/**
* 乙方名称(必填)
*/
@JsonProperty("partyB")
String partyB,
/**
* 合同金额(必填,单位:元,保留2位小数)
*/
@JsonProperty("amount")
BigDecimal amount, // 用BigDecimal避免浮点精度问题,比Double更适合金额
/**
* 付款方式(可选,仅支持:一次性付清/分3期/分12期/月结)
*/
@JsonProperty(value = "paymentMethod", required = false)
String paymentMethod,
/**
* 合同备注(可选)
*/
@JsonProperty(value = "remark", required = false)
String remark,
/**
* 有效期至(可选,ISO-8601格式:yyyy-MM-dd)
*/
@JsonProperty(value = "validUntil", required = false)
String validUntil) {
}
支持通过 @JsonPropertyOrder 注解指定属性的精确顺序,例如:
java
// 在 AI 模型输出内容中,contractName 始终在最前面
@JsonPropertyOrder({"contractName", "contractNo"})
record ActorsFilms(String actor, List<String> movies) {}
注意事项:
- 使用
BeanOutputConverter时生效 - 适用于
record类和普通Java类。
2.2 开启原生结构化输出
2.2.1 模型参数
对于支持原生结构化输出的 AI 模型,需要配置输出格式化模式,相比基于提示词的方式更可靠。
注意 :智谱 AI 只支持配置 response-format,实际并非完整的原生结构化输出。
可以在 application.yml 中全局默认配置:
yml
spring:
application:
name: ai-chat-demo
ai:
# 智谱 GLM 配置
zhipuai:
api-key: f9d9e7c26bd24406ad # 替换为你的智谱 API Key
base-url: https://open.bigmodel.cn/api/paas # 可选,默认值可省略
chat:
options:
model: glm-4.7 # 智谱模型名称(如 glm-4、glm-4-flash 等)
response-format: json_object # text
也可以在构建 ChatClient 时全局配置:
java
ZhiPuAiChatOptions chatOptions = ZhiPuAiChatOptions.builder()
.responseFormat(ZhiPuAiApi.ChatCompletionRequest.ResponseFormat.jsonObject())
.build();
ChatClient client = ChatClient.builder(zhiPuAiChatModel)
.defaultOptions(chatOptions)
.build();
不过,一般全局默认应该是默认的 text 文件输出,在运行时配置 json 输出:
java
// 动态设置 response-format
String result = client.prompt()
.user("你的问题")
.options(ZhiPuAiChatOptions.builder()
.responseFormat(ZhiPuAiApi.ChatCompletionRequest.ResponseFormat.jsonObject()) // 或 ResponseFormat.text()
.build())
.call()
.content();
2.2.2 ENABLE_NATIVE_STRUCTURED_OUTPUT 属性
需要设置 ENABLE_NATIVE_STRUCTURED_OUTPUT 属性,用于自动生成 output.schema 。
ChatClient 可配置以下属性:
java
public enum ChatClientAttributes {
//@formatter:off
// 格式指令文本,追加到用户 prompt,告诉模型输出格式
OUTPUT_FORMAT("spring.ai.chat.client.output.format"),
// JSON Schema 字串,从 Java 类生成,传给模型原生 API
STRUCTURED_OUTPUT_SCHEMA("spring.ai.chat.client.structured.output.schema"),
// 原生模式开关,启用后 schema 直接传模型 API 而非注入 prompt
STRUCTURED_OUTPUT_NATIVE("spring.ai.chat.client.structured.output.native");
//@formatter:on
private final String key;
ChatClientAttributes(String key) {
this.key = key;
}
public String getKey() {
return this.key;
}
}
配置如下:
java
ChatClient.CallResponseSpec callResponseSpec = client
.prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.user(userInput)
.call();
2.3 调用 entity 方法
直接调用 entity 方法将输出转换为合同信息类:
java
String userInput = """
2026年度办公设备采购合同
合同编号:HT20260313
甲方:北京科技有限公司
乙方:上海办公设备销售有限公司
甲乙双方经友好协商,就办公设备采购事宜达成如下协议:
第一条 采购内容
甲方向乙方采购办公设备一批,具体型号及数量详见附件清单。
第二条 合同金额
合同总金额为人民币贰万伍仟捌佰元整(¥25,800.00元)。
第三条 付款方式
合同签订后分3期支付:首付款30%于签约时支付,二期款40%于设备到货时支付,尾款30%于验收合格后支付。
第四条 交货期限
乙方须于2026年04月01日前完成全部设备交付及安装调试。
第五条 质量保证
乙方保证所供设备为全新正品,符合国家质量标准,提供一年免费保修服务。
第六条 违约责任
任何一方违约,应向守约方支付合同金额10%的违约金。
第七条 本合同一式两份,双方各执一份,自签订之日起生效,有效期至2027年03月12日。
甲方(盖章): 北京科技有限公司
乙方(盖章): 上海办公设备销售有限公司
签订日期: 2026年03月13日
""";
ContractInfo contractInfo = client
.prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.user(userInput)
.call()
// 直接映射为ContractInfo实体(无需手动解析JSON)
.entity(ContractInfo.class);
运行后,可以看到在模型请求中包含了格式要求指令 以及自动生成的输出 JSON Schema :

打印信息中可以看到成功被转换为 Java 对象:

3. 泛型集合类型
3.1 List
对于泛型集合类型,需要使用 Spring Framework 提供的 ParameterizedTypeReference ,解决 Java 泛型类型擦除问题。
入口方法:
java
@Override
@Nullable
public <T> T entity(ParameterizedTypeReference<T> type) {
Assert.notNull(type, "type cannot be null");
return doSingleWithBeanOutputConverter(new BeanOutputConverter<>(type));
}
在 BeanOutputConverter 会保留类型:

简单示例:
java
List<ContractInfo> contractInfoList = client
.prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.user(userInput)
.call()
.entity(new ParameterizedTypeReference<List<ContractInfo>>() {
});
生成的 schema :
java
{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "array",
"items" : {
"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.2 Map
Map 类型也是一样,简单示例:
java
Map<String, Object> objectMap = client
.prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.user(userInput)
.call()
.entity(new ParameterizedTypeReference<Map<String, Object>>() {
});
生成的 schema :
java
{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"additionalProperties" : false
}
4. responseEntity()
responseEntity() 可以返回结构化的 ResponseEntity ,适用于需要同时访问结构化实体和响应元数据,双返回值:
response (R):原始ChatResponse,包含所有元数据entity (E):转换后的目标类型实体
java
public record ResponseEntity<R, E>(@Nullable R response, @Nullable E entity) {
@Nullable
public R getResponse() {
return this.response;
}
@Nullable
public E getEntity() {
return this.entity;
}
}
三个重载方法:
java
@Override
public <T> ResponseEntity<ChatResponse, T> responseEntity(Class<T> type) {
Assert.notNull(type, "type cannot be null");
return doResponseEntity(new BeanOutputConverter<>(type));
}
@Override
public <T> ResponseEntity<ChatResponse, T> responseEntity(ParameterizedTypeReference<T> type) {
Assert.notNull(type, "type cannot be null");
return doResponseEntity(new BeanOutputConverter<>(type));
}
@Override
public <T> ResponseEntity<ChatResponse, T> responseEntity(
StructuredOutputConverter<T> structuredOutputConverter) {
Assert.notNull(structuredOutputConverter, "structuredOutputConverter cannot be null");
return doResponseEntity(structuredOutputConverter);
}
使用方式和执行逻辑和 entity 方法基本一致,只是返回值多了一个 ChatResponse :
java
/**
* 执行结构化输出转换的核心方法,将 LLM 响应转换为目标类型。
*
* <p>处理流程:
* <ol>
* <li>将格式指令存入 context,供后续 Advisor 使用</li>
* <li>如果是原生结构化输出模式,将 JSON Schema 存入 context</li>
* <li>调用模型获取响应</li>
* <li>使用转换器将响应文本转换为目标类型</li>
* </ol>
*
* @param outputConverter 结构化输出转换器,包含格式指令和转换逻辑
* @param <T> 目标类型
* @return 包含 ChatResponse 和转换后实体的 ResponseEntity
*/
protected <T> ResponseEntity<ChatResponse, T> doResponseEntity(StructuredOutputConverter<T> outputConverter) {
Assert.notNull(outputConverter, "structuredOutputConverter cannot be null");
// 将格式指令存入 context,ChatModelCallAdvisor 会将其注入 prompt 或 ChatOptions
this.request.context().put(ChatClientAttributes.OUTPUT_FORMAT.getKey(), outputConverter.getFormat());
// 原生结构化输出模式:将 JSON Schema 存入 context
// ChatModelCallAdvisor 会将其设置到 StructuredOutputChatOptions 中
if (Boolean.TRUE.equals(this.request.context().get(ChatClientAttributes.STRUCTURED_OUTPUT_NATIVE.getKey()))
&& outputConverter instanceof BeanOutputConverter beanOutputConverter) {
this.request.context()
.put(ChatClientAttributes.STRUCTURED_OUTPUT_SCHEMA.getKey(), beanOutputConverter.getJsonSchema());
}
// 执行请求并获取响应
var chatResponse = doGetObservableChatClientResponse(this.request).chatResponse();
// 提取响应文本内容
var responseContent = getContentFromChatResponse(chatResponse);
if (responseContent == null) {
return new ResponseEntity<>(chatResponse, null);
}
// 将响应文本转换为目标类型
T entity = outputConverter.convert(responseContent);
return new ResponseEntity<>(chatResponse, entity);
}
简单示例:
java
ResponseEntity<ChatResponse, ContractInfo> chatResponseContractInfoResponseEntity = client
.prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.user(userInput)
.call()
.responseEntity(ContractInfo.class);
返回结果:

5. 自定义 StructuredOutputConverter
我们可以自定义实现 StructuredOutputConverter ,以适配更多的场景,比如:
- 实现中文的格式指令
- 支持更多的
schema
简单示例:
java
public class ChineseBeanOutputConverter<T> implements StructuredOutputConverter<T> {
private final Type type;
private final ObjectMapper objectMapper;
private final String jsonSchema;
private final ResponseTextCleaner textCleaner;
public ChineseBeanOutputConverter(Class<T> clazz) {
this(ParameterizedTypeReference.forType(clazz), new ObjectMapper(), null);
}
public ChineseBeanOutputConverter(Class<T> clazz, ObjectMapper objectMapper) {
this(ParameterizedTypeReference.forType(clazz), objectMapper, null);
}
public ChineseBeanOutputConverter(Class<T> clazz, ObjectMapper objectMapper, ResponseTextCleaner textCleaner) {
this(ParameterizedTypeReference.forType(clazz), objectMapper, textCleaner);
}
public ChineseBeanOutputConverter(ParameterizedTypeReference<T> typeRef) {
this(typeRef, new ObjectMapper(), null);
}
public ChineseBeanOutputConverter(ParameterizedTypeReference<T> typeRef, ObjectMapper objectMapper, ResponseTextCleaner textCleaner) {
this.type = typeRef.getType();
this.objectMapper = objectMapper;
this.textCleaner = textCleaner;
this.jsonSchema = generateSchema();
}
/**
* 生成 JSON Schema
*/
private String generateSchema() {
JacksonModule jacksonModule = new JacksonModule(
JacksonOption.RESPECT_JSONPROPERTY_REQUIRED,
JacksonOption.RESPECT_JSONPROPERTY_ORDER);
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);
// 所有字段必填
configBuilder.forFields().withRequiredCheck(f -> true);
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonNode = generator.generateSchema(this.type);
return jsonNode.toPrettyString();
}
/**
* 中文格式指令
*/
@Override
public String getFormat() {
String template = """
你的回答必须是 JSON 格式。
不要包含任何解释说明,只提供符合 RFC8259 标准的 JSON 响应,严格按照以下格式输出,不得有任何偏差。
不要在回答中包含 markdown 代码块标记。
请移除输出中的 ```json markdown 标记。
以下是你的输出必须遵循的 JSON Schema:
```%s```
""";
return String.format(template, this.jsonSchema);
}
@SuppressWarnings("unchecked")
@Override
public T convert(String text) {
try {
// 如果有文本清理器,先清理文本
if (this.textCleaner != null) {
text = this.textCleaner.clean(text);
}
return (T) this.objectMapper.readValue(text, this.objectMapper.constructType(this.type));
} catch (JsonProcessingException e) {
throw new RuntimeException("无法将文本转换为目标类型: " + this.type, e);
}
}
public String getJsonSchema() {
return this.jsonSchema;
}
}
使用示例:
java
// 使用自定义转换器
ChineseBeanOutputConverter<ContractInfo> converter = new ChineseBeanOutputConverter<>(ContractInfo.class);
ContractInfo contractInfo = client
.prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.user(userInput)
.call()
.entity(converter);
生成的 schema :
