《Spring AI 实战系列 入门篇》第 4 篇

0 阅读4分钟

Spring AI 实战系列 | 第 4 篇

结构化输出:AI 结果映射为 POJO

系列说明:本文为《Spring AI 实战系列 入门篇》第 4 篇

前置知识:完成第 1-3 篇

预计阅读时间:12 分钟


📖 目录

  1. 为什么需要结构化输出?
  2. Spring AI 的解决方案
  3. BeanOutputConverter 用法
  4. Native Structured Output
  5. 泛型集合处理
  6. 实战案例
  7. 系列预告

一、为什么需要结构化输出?

1.1 AI 返回的是字符串

❌ 问题:AI 返回的是字符串,不是数据结构

用户:生成一个用户信息
AI:{"name": "张三", "age": 25, "email": "zhangsan@example.com"}

这看起来像 JSON,但 AI 返回的是:
String json = '{\"name\": \"张三\", ...}';  // 只是一段字符串!

1.2 手动解析的痛苦

// ❌ 传统方式:字符串解析
String response = chatClient.prompt()
    .user("生成用户信息")
    .call()
    .content();  // 返回的是字符串!

// 手动解析 JSON
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(response, User.class);  // 可能失败!

1.3 结构化输出的价值

✅ 目标:直接获得 Java 对象

User user = chatClient.prompt()
    .user("生成用户信息")
    .call()
    .entity(User.class);  // 直接返回 User 对象!

二、Spring AI 的解决方案

2.1 两种方案

方案说明适用场景
BeanOutputConverterSpring AI 自动转换通用方案
Native Structured Output模型原生支持OpenAI 等

2.2 BeanOutputConverter 原理

┌─────────────────────────────────────────┐
│  BeanOutputConverter 转换流程            │
├─────────────────────────────────────────┤
│                                         │
│  1. 获取 Java 类定义                     │
│     User { name, age, email }           │
│           ↓                             │
│  2. 生成 JSON Schema                    │
│     {"type": "object", "properties":...} │
│           ↓                             │
│  3. 构建 Prompt(含 Schema)              │
│     "请返回 JSON,格式如下..."            │
│           ↓                             │
│  4. AI 返回字符串                        │
│     '{"name": "张三"...}'               │
│           ↓                             │
│  5. 解析为 Java 对象                     │
│     User(name="张三", age=25, ...)       │
│                                         │
└─────────────────────────────────────────┘

三、BeanOutputConverter 用法

3.1 基础用法

定义 POJO:

public record User(
    String name,
    int age,
    String email
) {}

使用 .entity():

@RestController
public class OutputController {

    private final ChatClient chatClient;

    public OutputController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping("/user")
    public User generateUser() {
        return chatClient.prompt()
            .user("生成一个用户信息,包含姓名、年龄、邮箱")
            .call()
            .entity(User.class);
    }
}

3.2 带格式控制的转换器

@GetMapping("/recipe")
public Recipe getRecipe() {
    // 创建转换器
    BeanOutputConverter<Recipe> converter = new BeanOutputConverter<>(Recipe.class);
    
    // 获取期望的格式字符串
    String format = converter.getFormat();
    
    // 调用 AI,传入格式要求
    Recipe recipe = chatClient.prompt()
        .user(u -> u
            .text("请生成一个菜谱,格式如下:\n{format}")
            .param("format", format))
        .call()
        .entity(converter);
    
    return recipe;
}

3.3 常用 POJO 示例

// 菜谱
public record Recipe(
    String name,
    List<String> ingredients,
    List<String> steps,
    int cookingTime,
    String difficulty
) {}

// 电影信息
public record Movie(
    String title,
    String director,
    int year,
    double rating,
    List<String> genres
) {}

// 产品信息
public record Product(
    String name,
    String description,
    double price,
    String currency,
    List<String> tags
) {}

四、Native Structured Output

4.1 什么是 Native?

部分 AI 模型(如 OpenAI GPT-4、Anthropic Claude)原生支持结构化输出,直接返回 JSON。

// ✅ 使用原生结构化输出
ActorFilms films = chatClient.prompt()
    .advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)  // 启用原生
    .user("生成一个演员和其代表作品")
    .call()
    .entity(ActorFilms.class);

4.2 启用原生结构化输出

// 方式1:全局启用
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
    .build();

// 方式2:单次调用启用
chatClient.prompt()
    .advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
    .user("生成数据")
    .call()
    .entity(MyClass.class);

五、泛型集合处理

5.1 返回 List

// 定义类型
ParameterizedTypeReference<List<User>> typeRef = new ParameterizedTypeReference<>() {};

// 调用
List<User> users = chatClient.prompt()
    .user("生成5个用户信息")
    .call()
    .entity(typeRef);

5.2 返回 Map<String, T>

ParameterizedTypeReference<Map<String, Integer>> typeRef = 
    new ParameterizedTypeReference<>() {};

Map<String, Integer> scores = chatClient.prompt()
    .user("统计每个科目的分数:数学95,语文88,英语92")
    .call()
    .entity(typeRef);

5.3 流式响应 + 结构化输出

// 需要先收集流,再转换
BeanOutputConverter<List<Movie>> converter = 
    new BeanOutputConverter<>(new ParameterizedTypeReference<>() {});

Flux<String> flux = chatClient.prompt()
    .user("推荐5部科幻电影")
    .stream()
    .content();

String content = flux.collectList().block()
    .stream().collect(Collectors.joining());

List<Movie> movies = converter.convert(content);

六、实战案例

6.1 案例:智能数据分析助手

public record SalesReport(
    String region,
    double totalRevenue,
    int orderCount,
    double averageOrderValue,
    List<ProductPerformance> topProducts
) {}

public record ProductPerformance(
    String productName,
    int quantity,
    double revenue
) {}

@RestController
@RequestMapping("/analytics")
public class AnalyticsController {

    private final ChatClient chatClient;

    public AnalyticsController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping("/report")
    public SalesReport getReport(
            @RequestParam String region,
            @RequestParam String period
    ) {
        BeanOutputConverter<SalesReport> converter = 
            new BeanOutputConverter<>(SalesReport.class);
        
        String prompt = """
            分析以下销售数据,返回 JSON 格式:
            
            地区:%s
            时间段:%s
            
            返回格式:
            %s
            
            注意:
            - totalRevenue 是总销售额
            - orderCount 是订单数量
            - averageOrderValue 是平均订单金额
            - topProducts 是前3个畅销产品
            """.formatted(region, period, converter.getFormat());
        
        return chatClient.prompt()
            .user(prompt)
            .call()
            .entity(converter);
    }
}

6.2 案例:表单数据提取

public record ExtractedFormData(
    String fullName,
    String email,
    String phone,
    String address,
    String dateOfBirth
) {}

@Service
public class FormExtractionService {

    private final ChatClient chatClient;

    public FormExtractionService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    /**
     * 从文本中提取表单数据
     */
    public ExtractedFormData extractFromText(String text) {
        BeanOutputConverter<ExtractedFormData> converter = 
            new BeanOutputConverter<>(ExtractedFormData.class);
        
        return chatClient.prompt()
            .user(u -> u
                .text("从以下文本中提取表单信息:\n\n{text}\n\n返回 JSON:\n{format}")
                .param("text", text)
                .param("format", converter.getFormat()))
            .call()
            .entity(converter);
    }
}

七、系列预告

本篇小结

  • ✅ 理解了为什么需要结构化输出
  • ✅ 掌握了 BeanOutputConverter 用法
  • ✅ 学会了泛型集合处理

完整系列

篇目内容状态
第 1 篇核心概念 + 快速上手✅ 已完成
第 2 篇Tool Calling + 工具调用✅ 已完成
第 3 篇VectorStore + RAG✅ 已完成
第 4 篇结构化输出✅ 本文
第 5 篇Advisors 中间件🔜 下篇
第 6 篇国产模型集成🔜

下篇预告

第 5 篇:Advisors - 自定义 AI 中间件

  • Advisors 原理与执行链
  • 自定义日志记录器
  • 对话记忆实现

📚 参考资料

  1. Spring AI Structured Output 文档

  2. BeanOutputConverter API

    • org.springframework.ai.support. BeanOutputConverter

📌 引用说明:本文核心概念与技术描述参考自 Spring AI 官方文档(docs.spring.io/spring-ai/r… Structured Output Converter 章节。


关注公众号「AI日撰」,点击菜单「获取源码」获取完整代码(Gitee 仓库)。


系列:《Spring AI 实战系列 入门篇》第 4 篇(共 6 篇)