Spring AI 结构化输出:JSON Mode 与 BeanOutputConverter
本文深入讲解 Spring AI 中结构化输出的多种方式,包括 JSON Mode、BeanOutputConverter、格式化输出等核心技术。
博客:
https://blog.csdn.net/badao_liumang_qizhi
一、结构化输出概述
1.1 为什么需要结构化输出?
文本输出:"价格是 100 元,数量是 5 个" → 需要正则解析
JSON 输出:{"price": 100, "quantity": 5} → 直接反序列化
Bean 输出:OrderItem 对象 → 直接类型安全使用
1.2 技术方案对比
| 方案 | 适用场景 | 类型安全 | 复杂度 |
|---|---|---|---|
| Prompt 引导 | 简单场景 | 否 | 低 |
| JSON Mode | OpenAI 兼容模型 | 否 | 低 |
| BeanOutputConverter | Spring AI 原生 | 是 | 中 |
| Tool Calling | 复杂结构提取 | 是 | 中 |
二、BeanOutputConverter 实现
2.1 基础用法
java
package com.example.ai.structured;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ResponseEntity;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StructuredOutputService {
private final ChatClient chatClient;
public StructuredOutputService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 输出为简单 Bean
*/
public ProductInfo extractProductInfo(String text) {
return chatClient.prompt()
.user("从以下文本中提取产品信息:" + text)
.call()
.entity(ProductInfo.class);
}
/**
* 输出为列表
*/
public List<OrderItem> extractOrderItems(String text) {
return chatClient.prompt()
.user("从订单文本中提取所有商品项:" + text)
.call()
.entity(new ParameterizedTypeReference<List<OrderItem>>() {});
}
/**
* 输出为复杂嵌套结构
*/
public OrderReport generateOrderReport(String orderText) {
return chatClient.prompt()
.system("""
你是一个订单分析助手。请分析订单信息,生成结构化报告。
输出格式必须包含:
- 订单基本信息
- 商品列表
- 费用明细
- 配送信息
""")
.user("订单内容:" + orderText)
.call()
.entity(OrderReport.class);
}
// 数据模型
public record ProductInfo(
String name,
String brand,
double price,
List<String> features
) {}
public record OrderItem(
String productName,
int quantity,
double unitPrice,
double subtotal
) {}
public record OrderReport(
String orderId,
String customerName,
String status,
List<OrderItem> items,
Pricing pricing,
Shipping shipping
) {}
public record Pricing(
double subtotal,
double discount,
double shippingFee,
double tax,
double total
) {}
public record Shipping(
String method,
String address,
String estimatedDelivery
) {}
}
2.2 自定义输出转换器
java
package com.example.ai.structured;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.OutputConverter;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Map;
/**
* 自定义结构输出转换器
*/
@Component
public class CustomStructuredConverter {
private final ChatModel chatModel;
private final ObjectMapper objectMapper;
public CustomStructuredConverter(ChatModel chatModel, ObjectMapper objectMapper) {
this.chatModel = chatModel;
this.objectMapper = objectMapper;
}
/**
* 使用 BeanOutputConverter 提取结构化数据
*/
public <T> T extractStructured(String text, Class<T> targetClass) {
// 获取目标类型的 JSON Schema
BeanOutputConverter<T> converter = new BeanOutputConverter<>(targetClass);
String format = converter.getFormat();
// 构建提示词,包含格式要求
String prompt = String.format("""
请从以下文本中提取信息并转换为结构化数据。
必须按照以下 JSON Schema 格式输出:
%s
文本内容:%s
只输出 JSON,不要包含任何解释。
""", format, text);
String response = chatModel.call(prompt);
// 解析 JSON 响应
return converter.convert(response);
}
/**
* 使用 JSON Mode(OpenAI 兼容)
*/
public Map<String, Object> extractAsJson(String text, String schema) {
String prompt = String.format("""
从以下文本中提取信息。
输出格式(必须严格遵循):
%s
文本:%s
""", schema, text);
String response = chatModel.call(new Prompt(prompt));
try {
return objectMapper.readValue(response,
objectMapper.getTypeFactory().constructMapType(Map.class, String.class, Object.class));
} catch (Exception e) {
throw new RuntimeException("解析 JSON 失败", e);
}
}
}
三、高级模式:多步骤提取
3.1 链式提取
java
package com.example.ai.structured;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
/**
* 多步骤结构化提取
*/
@Service
public class ChainExtractionService {
private final ChatClient chatClient;
public ChainExtractionService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 第一步:提取实体
*/
public DocumentEntities extractEntities(String document) {
return chatClient.prompt()
.system("从文档中提取所有实体信息(人名、地名、组织名等)")
.user(document)
.call()
.entity(DocumentEntities.class);
}
/**
* 第二步:基于实体提取关系
*/
public DocumentRelations extractRelations(String document, DocumentEntities entities) {
return chatClient.prompt()
.system("分析实体间的关系")
.user(String.format("文档:%s\n已识别实体:%s",
document, entities.toString()))
.call()
.entity(DocumentRelations.class);
}
/**
* 第三步:生成完整知识图谱
*/
public KnowledgeGraph buildKnowledgeGraph(String document) {
DocumentEntities entities = extractEntities(document);
DocumentRelations relations = extractRelations(document, entities);
return new KnowledgeGraph(entities, relations);
}
public record DocumentEntities(
List<String> persons,
List<String> organizations,
List<String> locations,
List<String> dates
) {}
public record DocumentRelations(
List<Relation> relations
) {
public record Relation(String source, String type, String target) {}
}
public record KnowledgeGraph(DocumentEntities entities, DocumentRelations relations) {}
}
3.2 验证与重试
java
package com.example.ai.structured;
import org.springframework.stereotype.Service;
/**
* 带验证的结构化输出
*/
@Service
public class ValidatedStructuredService {
private final ChatClient chatClient;
public ValidatedStructuredService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 带重试的结构化提取
*/
public <T> T extractWithRetry(String text, Class<T> targetClass,
java.util.function.Predicate<T> validator,
int maxRetries) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
T result = chatClient.prompt()
.user(buildPrompt(text))
.call()
.entity(targetClass);
// 验证结果
if (validator.test(result)) {
return result;
}
} catch (Exception e) {
if (attempt == maxRetries) {
throw new RuntimeException("结构化提取失败,已达最大重试次数", e);
}
}
}
throw new RuntimeException("结构化提取失败");
}
/**
* 带修正的提取
*/
public <T> T extractWithCorrection(String text, Class<T> targetClass) {
try {
return chatClient.prompt()
.user(buildPrompt(text))
.call()
.entity(targetClass);
} catch (Exception e) {
// 解析失败时,让 LLM 修正
return chatClient.prompt()
.system("请将以下文本修正为有效的 JSON 格式:")
.user("输入:" + text + "\n错误:" + e.getMessage())
.call()
.entity(targetClass);
}
}
private String buildPrompt(String text) {
return String.format("""
从以下文本中提取结构化数据并输出为 JSON 格式。
- 日期格式使用 ISO 8601(YYYY-MM-DD)
- 金额使用数字类型,不要包含货币符号
- 列表项使用数组格式
- 空值使用 null 而不是空字符串
文本:%s
""", text);
}
}
四、JSON Mode 高级用法
4.1 OpenAI JSON Mode
java
package com.example.ai.structured;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Service;
/**
* JSON Mode 输出服务
*/
@Service
public class JsonModeService {
private final ChatClient chatClient;
public JsonModeService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 使用 response_format 强制 JSON 输出
*/
public String getJsonResponse(String prompt) {
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
/**
* 批量提取
*/
public <T> List<T> batchExtract(List<String> texts, Class<T> targetClass) {
return texts.parallelStream()
.map(text -> chatClient.prompt()
.user("提取结构化数据:" + text)
.call()
.entity(targetClass))
.toList();
}
}
五、总结
| 技术 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| BeanOutputConverter | 类型安全、Spring 原生 | 依赖反射 | 标准 Java Bean |
| JSON Mode | 兼容性好 | 需要手动解析 | OpenAI 兼容 API |
| Tool Calling | 最可靠 | 需要定义工具 | 复杂嵌套结构 |
| Prompt 引导 | 简单直接 | 不可靠 | 快速原型 |
参考资源: