SpringAI 2.0 结构化输出:JSON Schema 验证与 POJO 强类型映射
前言
在企业级应用中,AI 模型的输出往往是下游系统的输入。如果 AI 返回的是无结构的纯文本,我们需要额外的解析和验证逻辑,这不仅增加了系统的复杂度,还引入了类型安全的风险。
Spring AI 2.0 引入了一套强大的结构化输出机制,通过 JSON Schema 验证和 POJO 强类型映射,让我们能够将 AI 的输出直接映射为 Java 对象。本文将从架构师的视角,深入探讨如何在实战中应用这些特性,构建类型安全的 AI 响应处理流程。
一、结构化输出的架构价值
1.1 传统方式的痛点
在没有结构化输出的情况下,我们通常是这样处理 AI 响应的:
java
String response = chatModel.call("提取用户信息:姓名张三,年龄30,邮箱zhangsan@example.com");
// 需要手动解析 JSON 字符串
JsonNode json = objectMapper.readTree(response);
String name = json.get("name").asText();
int age = json.get("age").asInt();
String email = json.get("email").asText();
这种方式存在几个问题:
- 类型不安全: 字符串解析在运行时才发现错误
- 冗余代码: 每个响应都需要编写解析逻辑
- 验证缺失: AI 可能返回不符合预期的格式
- 维护成本高: 响应结构变化需要修改多处代码
1.2 SpringAI 的解决方案
SpringAI 通过以下三个核心组件解决了上述问题:
- StructuredOutputConverter: 负责将 JSON 字符串转换为 Java 对象
- FormatProvider: 生成 JSON Schema 格式提示
- StructuredOutputChatOptions: 配置原生 JSON Schema 支持
核心接口定义如下:
java
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {}
public interface FormatProvider {
String getFormat();
}
这个设计非常简洁:
- Converter: 定义如何从字符串转换为目标类型
- FormatProvider: 生成格式提示词,告诉 AI 应该返回什么格式的数据
二、核心接口与实现
2.1 StructuredOutputConverter 接口
StructuredOutputConverter 是结构化输出的核心接口,它组合了两个功能:
java
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
// Converter 接口方法
T convert(String source);
// FormatProvider 接口方法
String getFormat();
}
设计意图:
- Converter: 负责实际的类型转换,将 JSON 字符串反序列化为 Java 对象
- FormatProvider: 负责生成格式提示词,包含 JSON Schema 定义
2.2 内置转换器实现
SpringAI 提供了几个常用的转换器实现:
BeanOutputConverter
将 JSON 转换为指定的 Java Bean:
java
BeanOutputConverter<ActorsFilms> converter =
new BeanOutputConverter<>(ActorsFilms.class);
MapOutputConverter
将 JSON 转换为 Map<String, Object>:
java
MapOutputConverter converter = new MapOutputConverter();
Map<String, Object> result = converter.convert(jsonString);
ListOutputConverter
将 JSON 数组转换为 List:
java
ListOutputConverter converter = new ListOutputConverter();
List<String> result = converter.convert(jsonString);
2.3 嵌套 POJO 定义
对于复杂的数据结构,我们可以定义嵌套的 POJO 类:
java
// 电影信息
public class Film {
private String title;
private int year;
private String genre;
private double rating;
// 构造器、getter、setter
}
// 演员及其电影作品
public class ActorsFilms {
private String actor;
private List<Film> films;
// 构造器、getter、setter
}
三、高级 API:ChatClient 集成
3.1 基础用法
ChatClient 提供了最简洁的结构化输出 API:
java
ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
.user("生成 Tom Hanks 和 Bill Murray 的5部电影作品集")
.call()
.entity(ActorsFilms.class);
这个示例展示了高级 API 的三个关键点:
- 类型推导: 自动将返回结果转换为指定的 POJO 类型
- 格式注入: 自动生成 JSON Schema 并注入到提示词中
- 结果转换: 自动将 AI 返回的 JSON 转换为 Java 对象
3.2 泛型集合支持
对于集合类型,我们需要使用 ParameterizedTypeReference:
java
List<ActorsFilms> actorsFilmsList = ChatClient.create(chatModel).prompt()
.user("生成5位演员的电影作品集,每位演员包含3部电影")
.call()
.entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});
ParameterizedTypeReference 是 Spring 提供的工具类,用于处理泛型类型信息。这是因为 Java 的类型擦除机制导致运行时无法直接获取泛型的实际类型。
3.3 完整示例
下面是一个完整的示例,展示如何使用 ChatClient 生成结构化数据:
java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.core.ParameterizedTypeReference;
public class StructuredOutputExample {
private final ChatClient chatClient;
public StructuredOutputExample(ChatClient chatClient) {
this.chatClient = ChatClient.create(chatModel);
}
public List<MovieReview> generateReviews(String movieTitle) {
return chatClient.prompt()
.user("为电影《" + movieTitle + "》生成3条影评,每条包含评分和评语")
.call()
.entity(new ParameterizedTypeReference<List<MovieReview>>() {});
}
public static void main(String[] args) {
ChatModel chatModel = new OpenAiChatModel(...);
StructuredOutputExample example = new StructuredOutputExample(chatModel);
List<MovieReview> reviews = example.generateReviews("肖申克的救赎");
reviews.forEach(review -> {
System.out.println("评分: " + review.getRating());
System.out.println("评语: " + review.getComment());
});
}
}
// 影评 POJO
public class MovieReview {
private String reviewer;
private int rating; // 1-10分
private String comment;
private LocalDate reviewDate;
// 构造器、getter、setter
}
四、低级 API:精细控制转换流程
4.1 手动控制流程
低级 API 让我们可以完全控制转换流程,适合需要精细化的场景:
java
public class LowLevelApiExample {
private final ChatModel chatModel;
private final ObjectMapper objectMapper;
public ActorsFilms generateStructuredOutput(String actorName) {
// 1. 创建转换器
BeanOutputConverter<ActorsFilms> outputConverter =
new BeanOutputConverter<>(ActorsFilms.class);
// 2. 获取 JSON Schema 格式
String format = outputConverter.getFormat();
// 3. 构建提示词模板
String template = """
请为演员 %s 生成5部电影作品。
%s
""".formatted(actorName, format);
// 4. 创建提示
Prompt prompt = new Prompt(template);
// 5. 调用模型
Generation generation = chatModel.call(prompt).getResult();
// 6. 转换响应
ActorsFilms actorsFilms = outputConverter.convert(
generation.getOutput().getContent()
);
return actorsFilms;
}
}
4.2 流程图解
低级 API 的执行流程如下:
1. 创建转换器
└─> BeanOutputConverter<ActorsFilms>
└─> 分析 POJO 类结构
└─> 生成 JSON Schema
2. 获取格式提示
└─> getFormat()
└─> 返回 JSON Schema 字符串
3. 构建提示词
└─> 用户指令 + JSON Schema
└─> "生成演员作品... {json_schema}"
4. 调用 AI 模型
└─> chatModel.call(prompt)
└─> 模型根据 Schema 生成 JSON
5. 转换响应
└─> outputConverter.convert(jsonString)
└─> JSON 字符串 -> POJO 对象
└─> Jackson 反序列化
4.3 错误处理机制
在低级 API 中,我们可以添加详细的错误处理:
java
public ActorsFilms generateWithErrorHandling(String actorName) {
BeanOutputConverter<ActorsFilms> converter =
new BeanOutputConverter<>(ActorsFilms.class);
try {
String format = converter.getFormat();
String prompt = String.format("生成 %s 的电影作品\n%s", actorName, format);
Generation generation = chatModel.call(new Prompt(prompt)).getResult();
String response = generation.getOutput().getContent();
// 验证响应是否为空
if (response == null || response.trim().isEmpty()) {
throw new IllegalStateException("AI 返回了空响应");
}
// 尝试转换
ActorsFilms result = converter.convert(response);
// 验证转换结果
if (result.getFilms() == null || result.getFilms().isEmpty()) {
throw new IllegalStateException("生成的电影列表为空");
}
return result;
} catch (JsonProcessingException e) {
// JSON 解析失败
log.error("JSON 解析失败: {}", e.getMessage());
throw new RuntimeException("无法解析 AI 响应", e);
} catch (Exception e) {
// 其他错误
log.error("生成结构化输出时发生错误: {}", e.getMessage(), e);
throw new RuntimeException("生成失败", e);
}
}
五、错误处理与回退策略
5.1 常见错误类型
在使用结构化输出时,可能会遇到以下错误:
- JSON 格式错误: AI 返回的不是有效的 JSON
- Schema 不匹配: 返回的数据结构不符合 POJO 定义
- 类型转换失败: 字段类型不匹配(如字符串无法转换为整数)
- 空字段处理: 必填字段为空或缺失
5.2 重试机制
对于 transient 错误,我们可以实现自动重试:
java
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.support.RetryTemplate;
public class RetryableStructuredOutput {
private final ChatModel chatModel;
private final RetryTemplate retryTemplate;
public ActorsFilms generateWithRetry(String actorName) {
return retryTemplate.execute(context -> {
BeanOutputConverter<ActorsFilms> converter =
new BeanOutputConverter<>(ActorsFilms.class);
String format = converter.getFormat();
String prompt = String.format("生成 %s 的电影作品\n%s", actorName, format);
Generation generation = chatModel.call(new Prompt(prompt)).getResult();
return converter.convert(generation.getOutput().getContent());
});
}
// 配置重试策略
@Bean
public RetryTemplate retryTemplate() {
return RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(1000, 2, 5000)
.retryOn(JsonProcessingException.class)
.build();
}
}
5.3 回退策略
当结构化输出失败时,我们可以提供回退方案:
java
public class FallbackStructuredOutput {
private final ChatModel chatModel;
private final StructuredOutputCache cache;
public ActorsFilms generateWithFallback(String actorName) {
try {
// 尝试从缓存获取
ActorsFilms cached = cache.get(actorName);
if (cached != null) {
log.info("从缓存获取数据: {}", actorName);
return cached;
}
// 尝试生成结构化输出
return generateStructuredOutput(actorName);
} catch (Exception e) {
log.warn("结构化输出失败,使用回退策略: {}", e.getMessage());
// 回退到手动解析
return fallbackToManualParsing(actorName);
}
}
private ActorsFilms fallbackToManualParsing(String actorName) {
String response = chatModel.call("请简要列出 " + actorName + " 的5部电影");
// 手动提取信息
ActorsFilms result = new ActorsFilms();
result.setActor(actorName);
// 简单的文本解析逻辑
List<Film> films = Arrays.stream(response.split("\n"))
.filter(line -> !line.trim().isEmpty())
.map(this::parseFilmFromText)
.collect(Collectors.toList());
result.setFilms(films);
// 缓存结果
cache.put(actorName, result);
return result;
}
}
5.4 多级回退策略
更健壮的实现应该包含多级回退:
java
public ActorsFilms generateWithMultiLevelFallback(String actorName) {
// 第一级: 尝试结构化输出
try {
return generateStructuredOutput(actorName);
} catch (Exception e) {
log.warn("结构化输出失败,进入二级回退");
}
// 第二级: 尝试带宽松 Schema 的输出
try {
return generateWithLenientSchema(actorName);
} catch (Exception e) {
log.warn("宽松 Schema 输出失败,进入三级回退");
}
// 第三级: 手动解析文本
try {
return fallbackToManualParsing(actorName);
} catch (Exception e) {
log.warn("手动解析失败,进入四级回退");
}
// 第四级: 返回空结果或默认值
log.error("所有策略均失败,返回默认结果");
return createEmptyResult(actorName);
}
六、复杂嵌套结构处理
6.1 多层嵌套 POJO
对于复杂的数据结构,我们可以定义多层嵌套的 POJO:
java
// 电影公司
public class ProductionCompany {
private String name;
private String country;
private int establishedYear;
// getter、setter
}
// 电影详细信息
public class DetailedFilm {
private String title;
private int year;
private String genre;
private double rating;
private String director;
private List<String> cast;
private String plot;
private ProductionCompany productionCompany;
private int boxOffice; // 票房(百万美元)
// getter、setter
}
// 完整的演员信息
public class ComprehensiveActorProfile {
private String name;
private LocalDate birthDate;
private String nationality;
private String biography;
private List<Awards> awards;
private List<DetailedFilm> filmography;
// getter、setter
}
// 奖项信息
public class Awards {
private String awardName;
private String category;
private int year;
private String movie;
// getter、setter
}
6.2 生成复杂嵌套数据
使用 ChatClient 生成复杂的嵌套结构:
java
public ComprehensiveActorProfile generateComprehensiveProfile(String actorName) {
String prompt = String.format("""
请为演员 %s 生成一份完整的个人资料,包括:
1. 基本信息(姓名、出生日期、国籍、生平简介)
2. 获奖记录(至少3个奖项)
3. 代表作品(至少5部详细电影信息,每部包括导演、演员表、剧情、制片公司、票房)
""", actorName);
return ChatClient.create(chatModel).prompt()
.user(prompt)
.call()
.entity(ComprehensiveActorProfile.class);
}
6.3 泛型集合的高级用法
处理包含泛型的复杂集合:
java
// 电影目录,包含不同类型的电影
public class MovieCatalog {
private List<FeatureFilm> featureFilms; // 故事片
private List<Documentary> documentaries; // 纪录片
private List<ShortFilm> shortFilms; // 短片
// getter、setter
}
// 使用泛型方式生成
public class GenericStructuredOutput {
public <T> List<T> generateList(
String prompt,
Class<T> elementType,
int expectedSize
) {
String enhancedPrompt = String.format(
"%s\n请生成 %d 个 %s 类型的实例",
prompt, expectedSize, elementType.getSimpleName()
);
return ChatClient.create(chatModel).prompt()
.user(enhancedPrompt)
.call()
.entity(new ParameterizedTypeReference<List<T>>() {});
}
// 使用示例
public MovieCatalog generateCatalog(String studioName) {
MovieCatalog catalog = new MovieCatalog();
catalog.setFeatureFilms(generateList(
"为电影公司 %s 生成故事片",
FeatureFilm.class,
5
));
catalog.setDocumentaries(generateList(
"为电影公司 %s 生成纪录片",
Documentary.class,
3
));
catalog.setShortFilms(generateList(
"为电影公司 %s 生成短片",
ShortFilm.class,
4
));
return catalog;
}
}
6.4 自定义序列化器
对于特殊的字段类型,我们可以自定义序列化逻辑:
java
public class CustomDeserializer extends JsonDeserializer<LocalDate> {
@Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
String dateStr = p.getValueAsString();
// 尝试多种日期格式
DateTimeFormatter[] formatters = {
DateTimeFormatter.ISO_LOCAL_DATE,
DateTimeFormatter.ofPattern("yyyy年MM月dd日"),
DateTimeFormatter.ofPattern("yyyy-MM-dd"),
DateTimeFormatter.ofPattern("yyyy/MM/dd")
};
for (DateTimeFormatter formatter : formatters) {
try {
return LocalDate.parse(dateStr, formatter);
} catch (Exception e) {
// 继续尝试下一个格式
}
}
throw new IOException("无法解析日期: " + dateStr);
}
}
// 在 POJO 中使用自定义反序列化器
public class Film {
private String title;
@JsonDeserialize(using = CustomDeserializer.class)
private LocalDate releaseDate;
// getter、setter
}
七、原生 JSON Schema 支持
7.1 StructuredOutputChatOptions 接口
Spring AI 2.0 引入了 StructuredOutputChatOptions 接口,用于配置原生的 JSON Schema 支持:
java
public interface StructuredOutputChatOptions extends ChatOptions {
String getResponseFormat();
}
支持的模型:
- OpenAI: 通过 response_format 参数
- Mistral AI: 原生 JSON 模式验证
- Ollama: 通过实现 StructuredOutputChatOptions 接口
7.2 Mistral AI 原生支持
Mistral AI 在 Spring AI 2.0.0-M2 中增加了原生 JSON 模式验证:
java
@Configuration
public class MistralConfig {
@Bean
public ChatModel mistralChatModel() {
return new MistralAiChatModel(
MistralAiApi.builder()
.apiKey(System.getenv("MISTRAL_API_KEY"))
.build(),
MistralAiChatOptions.builder()
.model("mistral-large-latest")
.responseFormat("json") // 启用 JSON 模式
.temperature(0.7)
.build()
);
}
}
7.3 Ollama 原生支持
Ollama 在 Spring AI 2.0 中实现了 StructuredOutputChatOptions 接口:
java
@Configuration
public class OllamaConfig {
@Bean
public ChatModel ollamaChatModel() {
return new OllamaChatModel(
OllamaApi.builder()
.baseUrl("http://localhost:11434")
.build(),
OllamaChatOptions.builder()
.model("llama3.2")
.format("json") // 启用 JSON 格式
.temperature(0.7)
.build()
);
}
}
7.4 原生支持的优势
使用原生 JSON Schema 支持有以下优势:
- 更高的准确性: 模型直接在生成时进行验证
- 更少的 token 消耗: 不需要在提示词中包含完整的 JSON Schema
- 更快的响应: 减少了后处理时间
- 更好的兼容性: 直接使用模型的原生能力
八、实战案例:电影推荐系统
8.1 场景描述
构建一个智能电影推荐系统,根据用户的偏好生成个性化的电影推荐列表。系统需要:
- 分析用户偏好(类型、年代、导演等)
- 生成符合偏好的推荐列表
- 为每部电影提供详细的推荐理由
- 支持多种推荐策略
8.2 数据模型
java
// 用户偏好
public class UserPreference {
private List<String> genres; // 喜欢的类型
private List<Integer> decades; // 喜欢的年代
private List<String> favoriteDirectors; // 喜欢的导演
private double minRating; // 最低评分
private boolean includeClassics; // 是否包含经典电影
// getter、setter
}
// 电影推荐
public class MovieRecommendation {
private String title;
private int year;
private String genre;
private String director;
private double rating;
private String reason; // 推荐理由
private List<String> reasons; // 推荐原因列表
private double matchScore; // 匹配度分数
// getter、setter
}
// 推荐结果
public class RecommendationResult {
private UserPreference preference;
private List<MovieRecommendation> recommendations;
private String summary;
private int totalRecommendations;
private LocalDate generatedDate;
// getter、setter
}
8.3 推荐服务实现
java
@Service
public class MovieRecommendationService {
private final ChatClient chatClient;
private final RecommendationCache cache;
public RecommendationResult generateRecommendations(UserPreference preference) {
// 构建提示词
String prompt = buildRecommendationPrompt(preference);
// 生成推荐
RecommendationResult result = chatClient.prompt()
.user(prompt)
.call()
.entity(RecommendationResult.class);
// 后处理:计算匹配分数
result.getRecommendations().forEach(rec -> {
rec.setMatchScore(calculateMatchScore(rec, preference));
});
// 排序:按匹配分数降序
result.getRecommendations().sort(
Comparator.comparing(MovieRecommendation::getMatchScore).reversed()
);
// 缓存结果
cache.put(preference, result);
return result;
}
private String buildRecommendationPrompt(UserPreference preference) {
return String.format("""
请根据以下用户偏好生成电影推荐列表:
偏好类型: %s
偏好年代: %s
喜欢的导演: %s
最低评分: %.1f
是否包含经典电影: %s
要求:
1. 推荐不少于5部、不超过10部电影
2. 为每部电影提供详细的推荐理由(至少3条)
3. 匹配度分数范围为0-100
4. 提供推荐摘要
""",
String.join(", ", preference.getGenres()),
formatDecades(preference.getDecades()),
String.join(", ", preference.getFavoriteDirectors()),
preference.getMinRating(),
preference.isIncludeClassics() ? "是" : "否"
);
}
private double calculateMatchScore(MovieRecommendation rec, UserPreference pref) {
double score = 0.0;
// 类型匹配(40分)
if (pref.getGenres().contains(rec.getGenre())) {
score += 40;
}
// 年代匹配(30分)
if (isInPreferredDecades(rec.getYear(), pref.getDecades())) {
score += 30;
}
// 导演匹配(20分)
if (pref.getFavoriteDirectors().contains(rec.getDirector())) {
score += 20;
}
// 评分匹配(10分)
if (rec.getRating() >= pref.getMinRating()) {
score += 10 * (rec.getRating() / 10.0);
}
return Math.min(score, 100);
}
private boolean isInPreferredDecades(int year, List<Integer> decades) {
int decade = (year / 10) * 10;
return decades.contains(decade);
}
private String formatDecades(List<Integer> decades) {
return decades.stream()
.map(d -> d + "年代")
.collect(Collectors.joining(", "));
}
}
8.4 控制器实现
java
@RestController
@RequestMapping("/api/recommendations")
public class RecommendationController {
private final MovieRecommendationService recommendationService;
@PostMapping("/generate")
public ResponseEntity<RecommendationResult> generateRecommendations(
@RequestBody UserPreference preference
) {
try {
RecommendationResult result = recommendationService
.generateRecommendations(preference);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("生成推荐失败", e);
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/example")
public ResponseEntity<UserPreference> getExamplePreference() {
UserPreference preference = new UserPreference();
preference.setGenres(Arrays.asList("剧情", "科幻", "犯罪"));
preference.setDecades(Arrays.asList(1990, 2000, 2010));
preference.setFavoriteDirectors(Arrays.asList("诺兰", "克里斯托弗·诺兰"));
preference.setMinRating(7.5);
preference.setIncludeClassics(true);
return ResponseEntity.ok(preference);
}
}
8.5 完整工作流程
1. 用户提交偏好
└─> UserPreference 对象
└─> 类型、年代、导演等
2. 构建提示词
└─> 将偏好转换为自然语言描述
└─> 加入格式要求
3. 调用 AI 模型
└─> ChatClient 调用
└─> 模型生成结构化推荐
4. 后处理
└─> 计算匹配分数
└─> 按分数排序
5. 返回结果
└─> RecommendationResult 对象
└─> 包含推荐列表和摘要
九、性能优化与最佳实践
9.1 缓存策略
对于重复的查询,我们可以实现缓存机制:
java
@Service
public class StructuredOutputCache {
private final Cache<String, Object> cache;
private final int maxEntries;
private final Duration ttl;
public StructuredOutputCache(int maxEntries, Duration ttl) {
this.cache = Caffeine.newBuilder()
.maximumSize(maxEntries)
.expireAfterWrite(ttl)
.build();
this.maxEntries = maxEntries;
this.ttl = ttl;
}
@SuppressWarnings("unchecked")
public <T> T get(String key, Class<T> type) {
Object value = cache.getIfPresent(key);
return value != null && type.isInstance(value) ? (T) value : null;
}
public void put(String key, Object value) {
cache.put(key, value);
}
public void invalidate(String key) {
cache.invalidate(key);
}
public void clear() {
cache.invalidateAll();
}
}
// 在服务中使用缓存
public class CachedStructuredOutputService {
private final ChatModel chatModel;
private final StructuredOutputCache cache;
public ActorsFilms generateWithCache(String actorName) {
String cacheKey = "actors:" + actorName;
// 尝试从缓存获取
ActorsFilms cached = cache.get(cacheKey, ActorsFilms.class);
if (cached != null) {
log.info("从缓存获取: {}", actorName);
return cached;
}
// 生成新的结果
ActorsFilms result = ChatClient.create(chatModel).prompt()
.user("生成 " + actorName + " 的电影作品")
.call()
.entity(ActorsFilms.class);
// 缓存结果
cache.put(cacheKey, result);
return result;
}
}
9.2 批量处理
对于需要生成多个结构化输出的场景,我们可以实现批量处理:
java
@Service
public class BatchStructuredOutputService {
private final ChatModel chatModel;
private final ExecutorService executor;
public List<ActorsFilms> generateBatch(List<String> actorNames) {
// 使用并行流处理
return actorNames.parallelStream()
.map(this::generateSingle)
.collect(Collectors.toList());
}
private ActorsFilms generateSingle(String actorName) {
return ChatClient.create(chatModel).prompt()
.user("生成 " + actorName + " 的电影作品")
.call()
.entity(ActorsFilms.class);
}
// 或者使用 CompletableFuture 进行更精细的控制
public CompletableFuture<List<ActorsFilms>> generateBatchAsync(
List<String> actorNames
) {
List<CompletableFuture<ActorsFilms>> futures = actorNames.stream()
.map(actor -> CompletableFuture.supplyAsync(
() -> generateSingle(actor),
executor
))
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList())
);
}
}
9.3 Schema 优化
为了减少 token 消耗,我们可以优化 JSON Schema:
java
public class OptimizedSchemaConverter {
private final BeanOutputConverter<ActorsFilms> converter;
public String getOptimizedFormat() {
String originalSchema = converter.getFormat();
// 移除不必要的描述信息
String optimized = originalSchema
.replaceAll("\"description\":\\s*\"[^\"]*\",?", "")
.replaceAll(",\\s*}", "}")
.replaceAll(",\\s*]", "]");
return optimized;
}
}
9.4 错误监控
实现完善的错误监控和日志记录:
java
@Aspect
@Component
public class StructuredOutputMonitor {
private final MeterRegistry meterRegistry;
@Around("@annotation(org.springframework.ai.chat.client.ChatClient)")
public Object monitorStructuredOutput(ProceedingJoinPoint joinPoint)
throws Throwable {
String methodName = joinPoint.getSignature().getName();
Timer.Sample sample = Timer.start(meterRegistry);
try {
Object result = joinPoint.proceed();
// 记录成功指标
meterRegistry.counter("structured.output.success",
"method", methodName
).increment();
sample.stop(Timer.builder("structured.output.duration")
.tag("method", methodName)
.tag("status", "success")
.register(meterRegistry));
return result;
} catch (Exception e) {
// 记录失败指标
meterRegistry.counter("structured.output.error",
"method", methodName,
"error", e.getClass().getSimpleName()
).increment();
sample.stop(Timer.builder("structured.output.duration")
.tag("method", methodName)
.tag("status", "error")
.register(meterRegistry));
throw e;
}
}
}
十、Spring AI 2.0.0-M2 新特性
10.1 Mistral AI 原生 JSON 模式验证
Mistral AI 在 2.0.0-M2 版本中增加了原生 JSON 模式验证支持:
java
@Configuration
public class MistralNativeJsonConfig {
@Bean
public ChatModel mistralNativeJsonModel() {
return new MistralAiChatModel(
MistralAiApi.builder()
.apiKey(System.getenv("MISTRAL_API_KEY"))
.build(),
MistralAiChatOptions.builder()
.model("mistral-large-latest")
.responseFormat("{ \"type\": \"json_object\" }")
.build()
);
}
}
10.2 Ollama 原生支持
Ollama 实现了 StructuredOutputChatOptions 接口:
java
@Configuration
public class OllamaNativeJsonConfig {
@Bean
public ChatModel ollamaNativeJsonModel() {
return new OllamaChatModel(
OllamaApi.builder()
.baseUrl("http://localhost:11434")
.build(),
OllamaChatOptions.builder()
.model("llama3.2")
.format("json")
.build()
);
}
}
10.3 嵌入模型可配置参数
嵌入模型增加了可配置的 dimensions 参数:
java
@Configuration
public class EmbeddingModelConfig {
@Bean
public EmbeddingModel embeddingModel() {
return new OpenAiEmbeddingModel(
OpenAiApi.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.build(),
OpenAiEmbeddingOptions.builder()
.model("text-embedding-3-small")
.dimensions(1536) // 可配置的维度
.build()
);
}
}
十一、常见问题与解决方案
11.1 JSON 解析失败
问题: AI 返回的 JSON 无法解析
解决方案:
java
public class RobustJsonParser {
private final ObjectMapper objectMapper;
public <T> T parseRobustly(String json, Class<T> type) throws Exception {
// 尝试直接解析
try {
return objectMapper.readValue(json, type);
} catch (JsonProcessingException e) {
// 尝试提取 JSON 片段
String cleaned = extractJsonFragment(json);
return objectMapper.readValue(cleaned, type);
}
}
private String extractJsonFragment(String text) {
// 使用正则表达式提取 JSON 片段
Pattern pattern = Pattern.compile("\\{[^}]*\\}|\\[[^\\]]*\\]");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
return matcher.group();
}
return text;
}
}
11.2 字段为 null
问题: 某些字段总是返回 null
解决方案:
java
// 在提示词中明确要求字段
public class FieldRequirement {
public String getPromptWithRequirements() {
return """
请生成用户信息,必须包含以下所有字段:
- name: 姓名(字符串,不能为空)
- age: 年龄(整数,必须大于0)
- email: 邮箱(字符串,格式正确)
- address: 地址(字符串,可以是详细地址或城市)
所有字段都是必填的,不能省略。
""";
}
}
11.3 数组大小不符合预期
问题: 生成的数组大小不符合要求
解决方案:
java
public class ArraySizeControl {
public String getPromptWithSizeControl(int expectedSize) {
return String.format("""
请生成包含恰好 %d 个元素的列表。
不要生成更多或更少的元素。
每个元素都必须完整且有效。
""", expectedSize);
}
// 后处理验证
public <T> List<T> ensureSize(List<T> list, int expectedSize) {
if (list.size() != expectedSize) {
throw new IllegalStateException(
String.format("期望 %d 个元素,实际得到 %d 个",
expectedSize, list.size())
);
}
return list;
}
}
11.4 性能问题
问题: 结构化输出响应较慢
解决方案:
- 使用原生 JSON 模式: 减少后处理时间
- 优化 Schema: 减少不必要的字段
- 启用缓存: 缓存常见查询
- 批量处理: 合并多个请求
- 降低温度值: 减少随机性,提高一致性
java
@Configuration
public class PerformanceOptimizationConfig {
@Bean
public ChatModel optimizedChatModel() {
return new OpenAiChatModel(
OpenAiApi.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.build(),
OpenAiChatOptions.builder()
.model("gpt-4o")
.temperature(0.3) // 降低温度值
.maxTokens(2000) // 限制输出长度
.build()
);
}
}
十二、完整示例:智能表单处理系统
12.1 场景描述
构建一个智能表单处理系统,能够自动从用户提交的自由文本中提取结构化信息,并将其填充到预定义的表单中。
12.2 数据模型
java
// 用户信息表单
public class UserRegistrationForm {
private String fullName;
private String email;
private String phone;
private LocalDate birthDate;
private String address;
private String city;
private String postalCode;
private String country;
private List<String> interests;
private Map<String, String> customFields;
// getter、setter
}
// 提取结果
public class ExtractionResult {
private UserRegistrationForm form;
private double confidence; // 提取置信度
private List<String> warnings; // 警告信息
private Map<String, String> extractedFields; // 原始字段映射
// getter、setter
}
12.3 服务实现
java
@Service
public class IntelligentFormProcessor {
private final ChatClient chatClient;
private final FormValidator validator;
public ExtractionResult extractFromText(String text) {
// 构建提取提示词
String prompt = buildExtractionPrompt(text);
// 生成提取结果
ExtractionResult result = chatClient.prompt()
.user(prompt)
.call()
.entity(ExtractionResult.class);
// 验证表单
List<String> validationErrors = validator.validate(result.getForm());
if (!validationErrors.isEmpty()) {
result.getWarnings().addAll(validationErrors);
// 尝试修复
fixErrors(result, validationErrors);
}
return result;
}
private String buildExtractionPrompt(String text) {
return String.format("""
请从以下文本中提取用户注册信息:
文本内容:
%s
提取要求:
1. 提取所有可识别的字段
2. 对于不明确的字段,请标记为警告
3. 计算提取置信度(0-1)
4. 记录原始字段映射
5. 如果字段缺失,可以留空但要在警告中说明
字段说明:
- fullName: 完整姓名
- email: 电子邮箱
- phone: 电话号码
- birthDate: 出生日期(ISO格式)
- address: 详细地址
- city: 城市
- postalCode: 邮政编码
- country: 国家
- interests: 兴趣爱好列表
- customFields: 其他自定义字段
""", text);
}
private void fixErrors(ExtractionResult result, List<String> errors) {
// 实现自动修复逻辑
// 例如: 格式化日期、规范化电话号码等
}
}
12.4 控制器实现
java
@RestController
@RequestMapping("/api/forms")
public class FormProcessingController {
private final IntelligentFormProcessor processor;
@PostMapping("/extract")
public ResponseEntity<ExtractionResult> extract(
@RequestBody String text
) {
try {
ExtractionResult result = processor.extractFromText(text);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("表单提取失败", e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/validate")
public ResponseEntity<ValidationResult> validate(
@RequestBody UserRegistrationForm form
) {
ValidationResult result = validator.validate(form);
return ResponseEntity.ok(result);
}
}
总结
SpringAI 2.0 的结构化输出机制为我们提供了一套完整的解决方案,用于处理 AI 模型的类型安全输出。通过本文的深入探讨,我们学习了:
- 核心接口: StructuredOutputConverter、FormatProvider
- API 层级: ChatClient 高级 API 和低级 API 的使用
- 错误处理: 重试机制、回退策略、多级回退
- 复杂场景: 嵌套 POJO、泛型集合、自定义序列化
- 原生支持: Mistral AI、Ollama 的 JSON Schema 原生验证
- 性能优化: 缓存策略、批量处理、Schema 优化
- 实战应用: 电影推荐系统、智能表单处理
结构化输出不仅提高了代码的类型安全性,还大大简化了 AI 响应的处理逻辑。在实际项目中,我们应该根据具体需求选择合适的 API 和策略,构建健壮、高效、可维护的 AI 应用。
随着 SpringAI 的不断发展,结构化输出的能力将会越来越强大。作为架构师,我们需要持续关注新特性,并将其应用到实际项目中,为业务创造更大的价值。
参考资源
本文代码基于 Spring AI 2.0.0-M2 版本编写,实际使用时请根据具体版本进行调整。