Spring AI结构化输出
1. 核心概念与设计思想
1.1 什么是结构化输出
Spring AI 结构化输出是一种类型安全的 AI 交互范式。它通过向 LLM 注入格式指令,强制模型输出符合特定 Schema 的内容,再由框架自动将字符串转换为 Java 对象,实现了从 "字符串拼接与解析" 到 "面向对象编程" 的跨越。
1.2 核心接口契约
Spring AI 定义了两个职责单一的核心接口,共同构成结构化输出的基础:
// 格式提供者:生成AI能够理解并严格遵守的输出格式说明 public interface FormatProvider { String getFormat(); } // 结构化输出转换器:组合"格式生成"与"字符串转对象"能力 public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider { }接口职责分离原则:
Converter<String, T>:定义从字符串到目标类型 T 的反序列化逻辑FormatProvider:定义如何生成 AI 可执行的格式指令(通常是 JSON Schema)StructuredOutputConverter:组合两者,形成完整的 "指令 - 生成 - 解析" 闭环
1.3 底层工作原理
Spring AI 内部会自动执行以下 4 个步骤,全程对开发者透明:
- Schema 生成:根据目标 Java 类自动生成 JSON Schema 格式说明
- 提示词注入:将格式说明注入到提示词的
{format}占位符 - 模型调用:发送包含格式要求的完整提示词给 LLM
- 自动反序列化:将 LLM 返回的字符串转换为强类型 Java 对象
2. 快速上手:恋爱报告实战
2.1 第一步:定义结构化输出类
使用 Java 16 + 的Record特性(推荐)定义不可变的数据载体,这是结构化输出的最佳实践:
/** * 恋爱报告结构化输出类 * 字段名会直接影响AI生成内容的准确性,请使用清晰易懂的命名 */ public record LoveReport( String title, // 报告标题(要求:{用户名}的恋爱报告) List<String> s // 恋爱建议列表(每条建议为一个字符串元素) ) {}2.2 第二步:实现结构化输出调用
使用 Spring AI 的ChatClient流式 API,一行代码完成从对话到对象的转换:
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.ChatMemoryAdvisor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service public class LoveConsultationService { private final ChatClient chatClient; // 基础系统提示词 private static final String SYSTEM_PROMPT = "你是一位专业、温和的恋爱咨询师,擅长提供客观、可执行的恋爱建议。"; // 构造注入ChatClient.Builder(Spring Boot自动配置) public LoveConsultationService(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder.build(); } /** * 与AI对话并生成结构化的恋爱报告 * @param userMessage 用户输入的恋爱问题 * @param chatId 会话唯一标识,用于实现对话记忆 * @return 强类型的LoveReport对象 */ public LoveReport doChatWithReport(String userMessage, String chatId) { LoveReport loveReport = chatClient .prompt() // 系统提示词:明确业务要求+格式要求 .system(SYSTEM_PROMPT + "每次对话后必须生成恋爱结果,标题严格为'用户名的恋爱报告',内容为建议列表。") .user(userMessage) // 配置对话记忆:指定会话ID和历史消息检索条数 .advisors(spec -> spec .param(ChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) .param(ChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) .call() // 核心魔法:自动将AI输出转换为LoveReport对象 .entity(LoveReport.class); log.info("成功生成恋爱报告: {}", loveReport); return loveReport; } }3. 核心 API 详解
3.1 ChatClient.entity () 方法
entity(Class<T> entityType)是 Spring AI 结构化输出的核心入口,它内部封装了所有复杂逻辑:
- 自动创建对应类型的
StructuredOutputConverter实例 - 生成该类型的完整 JSON Schema
- 将 Schema 注入到提示词的适当位置
- 调用 LLM 并捕获输出
- 执行反序列化并返回强类型对象
支持的类型范围:
- 基本类型:
String、Integer、Long、Boolean、Double等 - 集合类型:
List<T>、Set<T>、Map<K, V> - 自定义 POJO/Record(支持嵌套)
- 枚举类型
- 泛型类型(需使用
ParameterizedTypeReference)
3.2 内置转换器
Spring AI 提供了多种开箱即用的转换器:
| 转换器 | 用途 | 特点 |
|---|---|---|
JacksonStructuredOutputConverter | JSON 格式转换 | 默认使用,基于 Jackson,支持所有 Java 类型 |
BeanOutputConverter | Bean 属性映射 | 基于 BeanUtils,兼容性好 |
EnumOutputConverter | 枚举类型转换 | 自动处理枚举值的大小写和别名 |
ListOutputConverter | 列表类型转换 | 专门优化列表输出的格式指令 |
4. 高级用法:自定义转换器
当内置转换器无法满足需求时,可以实现自定义的StructuredOutputConverter:
import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.ai.converter.StructuredOutputConverter; import org.springframework.core.convert.ConversionException; public class CustomLoveReportConverter implements StructuredOutputConverter<LoveReport> { private final ObjectMapper objectMapper; public CustomLoveReportConverter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public LoveReport convert(String source) { try { // 自定义解析逻辑:可以处理AI输出中的额外文本 String json = extractJsonFromText(source); return objectMapper.readValue(json, LoveReport.class); } catch (Exception e) { throw new ConversionException("无法转换为LoveReport对象", e); } } @Override public String getFormat() { // 自定义格式说明:比自动生成的更简洁、更符合业务需求 return """ 请严格按照以下JSON格式输出,不要添加任何其他内容: { "title": "报告标题,格式为'用户名的恋爱报告'", "s": ["建议1", "建议2", "建议3"] } """; } // 从AI输出的文本中提取JSON部分 private String extractJsonFromText(String text) { int start = text.indexOf('{'); int end = text.lastIndexOf('}'); if (start == -1 || end == -1 || start >= end) { throw new IllegalArgumentException("未找到有效的JSON内容"); } return text.substring(start, end + 1); } }使用自定义转换器:
public LoveReport doChatWithCustomConverter(String userMessage, String chatId) { CustomLoveReportConverter converter = new CustomLoveReportConverter(objectMapper); return chatClient .prompt() .system(SYSTEM_PROMPT) .user(userMessage) .call() .entity(converter); // 使用自定义转换器 }