Spring AI 结构化输出转换器实战:告别字符串解析,拥抱类型安全

Spring AI 结构化输出转换器实战:告别字符串解析,拥抱类型安全

📦 项目源码github.com/XiFYuW/spri...

引言

在使用大语言模型(LLM)开发应用时,我们经常会遇到这样的痛点:AI 返回的响应是纯文本字符串,需要手动解析才能提取有用的信息。这不仅繁琐,还容易出错,特别是当需要处理复杂的数据结构时。

Spring AI 结构化输出转换器(Structured Output Converter) 正是为了解决这一问题而生。它允许我们将 AI 的响应自动转换为 Java 对象(Bean、Map、List),实现类型安全的 AI 响应处理。

本文你将学到

  • 结构化输出转换器的核心概念与工作原理
  • BeanOutputConverterMapOutputConverterListOutputConverter 三种转换器的实战应用
  • 如何在响应式编程(Reactor)中优雅地使用转换器
  • 完整的项目实战与 API 测试示例

目录


一、项目概述与环境准备

1.1 技术栈

技术 版本 说明
Spring Boot 3.5.10 基础框架
Spring AI 1.1.0-SNAPSHOT AI 开发框架
Java 25 编程语言
Maven - 构建工具
WebFlux - 响应式 Web 框架

1.2 项目结构

bash 复制代码
phase-4/
├── src/main/java/org/example/
│   ├── SpringAiJcStart.java          # 启动类
│   ├── controller/
│   │   └── StructuredOutputController.java  # REST API 控制器
│   ├── service/
│   │   └── StructuredOutputService.java     # 业务逻辑服务
│   ├── entity/
│   │   ├── MovieActor.java           # 演员实体类
│   │   └── ProductInfo.java          # 产品信息实体类
│   └── exception/
│       ├── ChatException.java
│       ├── ErrorResponse.java
│       └── GlobalExceptionHandler.java
├── src/main/resources/
│   └── application.yml               # 配置文件
└── pom.xml                           # Maven 配置

建议:此处插入项目结构截图,展示 IDE 中的目录层级

1.3 核心依赖

xml 复制代码
<dependencies>
    <!-- Spring Boot WebFlux - 响应式编程支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    
    <!-- Spring Boot Web - 嵌入式 Netty -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- Spring AI OpenAI Starter - AI 模型支持 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
</dependencies>

1.4 配置文件

yaml 复制代码
spring:
  ai:
    openai:
      api-key: your-api-key-here
      base-url: https://ai.32zi.com  # 使用 32ai 代理服务
      chat:
        options:
          model: claude-3-7-sonnet-latest
    retry:
      max-attempts: 3
      backoff:
        initial-interval: 1000
        multiplier: 2
        max-interval: 10000
  server:
    port: 8080
    netty:
      connection-timeout: 2s

二、核心概念:什么是结构化输出转换器

2.1 传统方式的痛点

在没有结构化输出转换器之前,处理 AI 响应通常是这样的:

java 复制代码
// 传统方式:手动解析字符串
String response = chatClient.prompt("列出周杰伦的5部电影")
    .call()
    .content();

// 需要手动解析 JSON 字符串,容易出错
// 响应格式不稳定,可能需要复杂的正则表达式

2.2 结构化输出转换器的优势

Spring AI 结构化输出转换器 通过以下机制解决上述问题:

  1. JSON Schema 生成:自动根据目标类型生成 JSON Schema,指导 AI 模型输出符合预期的格式
  2. 自动类型转换:将 AI 的 JSON 响应自动映射到 Java 对象
  3. 类型安全:编译期类型检查,避免运行时类型错误
  4. 简化代码:无需手动解析,代码更简洁易维护

2.3 三种转换器对比

转换器 适用场景 输出类型 特点
BeanOutputConverter 固定结构的数据 Java Bean 自动生成 JSON Schema,强类型
MapOutputConverter 灵活/动态结构 Map<String, Object> 无需预定义类,灵活度高
ListOutputConverter 列表数据 List<String> 处理逗号分隔的列表

2.4 支持的 AI 模型

以下 AI 模型已测试支持列表、映射和 Bean 结构化输出:

模型厂商 具体模型/系列 说明
OpenAI GPT-4、GPT-4o、GPT-3.5-Turbo 原生支持结构化输出,JSON Schema 遵循度高
Anthropic Claude 3 系列(Opus、Sonnet、Haiku) 优秀的指令遵循能力,支持复杂嵌套结构
Azure OpenAI GPT-4、GPT-3.5-Turbo 企业级部署,与 OpenAI 接口兼容
Mistral AI Mistral Large、Medium、Small 欧洲领先模型,性价比优秀
Ollama Llama 3、Mistral、Gemma 等本地模型 本地部署,保护数据隐私
Vertex AI Gemini Pro、Gemini Ultra Google 云端模型,多模态能力强

提示 :不同模型对 JSON Schema 的遵循程度有所差异。对于复杂结构,推荐使用 Claude 3GPT-4 系列模型以获得最佳效果。

2.5 内置 JSON 模式配置

一些 AI 模型提供专门的配置选项来生成结构化(通常是 JSON)输出,这比单纯依赖提示词更可靠:

模型厂商 配置选项 说明 配置示例
OpenAI spring.ai.openai.chat.options.responseFormat 支持 JSON_OBJECT(保证有效 JSON)和 JSON_SCHEMA(保证符合提供的模式) {"type": "json_schema", "schema": {...}}
Azure OpenAI spring.ai.azure.openai.chat.options.responseFormat 设置为 {"type": "json_object"} 启用 JSON 模式 {"type": "json_object"}
Ollama spring.ai.ollama.chat.options.format 目前唯一接受的值是 json json
Mistral AI spring.ai.mistralai.chat.options.responseFormat 设置为 {"type": "json_object"} 启用 JSON 模式 {"type": "json_object"}
OpenAI 结构化输出配置示例
yaml 复制代码
spring:
  ai:
    openai:
      api-key: your-api-key
      chat:
        options:
          model: gpt-4o
          # 启用 JSON 模式,确保输出有效 JSON
          response-format:
            type: json_object
Ollama JSON 模式配置示例
yaml 复制代码
spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: llama3
          # 强制输出 JSON 格式
          format: json

重要提示

  • 使用内置 JSON 模式时,必须在提示词中明确说明要返回 JSON 格式
  • JSON_SCHEMA 模式比 JSON_OBJECT 更严格,能确保输出符合指定结构
  • 结合 BeanOutputConverter 使用时,内置 JSON 模式可以显著提高输出稳定性

三、三种转换器详解与实战

3.1 BeanOutputConverter - Java Bean 自动映射

BeanOutputConverter 是最常用的转换器,它可以将 AI 的 JSON 响应自动映射到预定义的 Java 类。

3.1.1 定义实体类

首先,我们需要定义一个 Java Bean 来接收转换后的数据:

java 复制代码
package org.example.entity;

import java.util.List;

/**
 * 电影演员实体类 - 用于演示 BeanOutputConverter
 */
public class MovieActor {

    private String actor;           // 演员姓名
    private List<String> movies;    // 电影列表
    private List<String> awards;    // 奖项列表

    // 必须提供无参构造器
    public MovieActor() {
    }

    public MovieActor(String actor, List<String> movies, List<String> awards) {
        this.actor = actor;
        this.movies = movies;
        this.awards = awards;
    }

    // Getters 和 Setters
    public String getActor() {
        return actor;
    }

    public void setActor(String actor) {
        this.actor = actor;
    }

    public List<String> getMovies() {
        return movies;
    }

    public void setMovies(List<String> movies) {
        this.movies = movies;
    }

    public List<String> getAwards() {
        return awards;
    }

    public void setAwards(List<String> awards) {
        this.awards = awards;
    }
}

关键点

  • 必须提供无参构造器,否则转换器无法实例化对象
  • 属性名应与 AI 返回的 JSON 字段名对应
  • 支持嵌套对象和集合类型
3.1.2 服务层实现
java 复制代码
@Service
public class StructuredOutputService {

    private final ChatClient chatClient;

    public StructuredOutputService(ChatModel chatModel) {
        this.chatClient = ChatClient.builder(chatModel).build();
    }

    /**
     * 使用 BeanOutputConverter 获取演员电影信息
     */
    public Mono<MovieActor> getActorMovies(String actorName) {
        return Mono.fromCallable(() -> {
            // 1. 创建 BeanOutputConverter,指定目标类型
            BeanOutputConverter<MovieActor> converter = 
                new BeanOutputConverter<>(MovieActor.class);

            // 2. 获取格式化指令(JSON Schema)
            String format = converter.getFormat();
            logger.debug("生成的格式指令: {}", format);

            // 3. 构建提示词模板,包含格式占位符
            String userPrompt = """
                    为演员 {actor} 生成电影作品信息。
                    包含该演员最著名的5部电影和获得的3个重要奖项。
                    {format}
                    """;

            // 4. 使用 PromptTemplate 构建提示词
            PromptTemplate promptTemplate = PromptTemplate.builder()
                    .template(userPrompt)
                    .variables(Map.of("actor", actorName, "format", format))
                    .build();

            Prompt prompt = new Prompt(promptTemplate.createMessage());

            // 5. 调用 AI 模型
            String response = chatClient.prompt(prompt)
                    .call()
                    .content();

            logger.debug("AI 原始响应: {}", response);

            // 6. 使用转换器将 JSON 响应转换为 MovieActor 对象
            return converter.convert(response);
        }).subscribeOn(Schedulers.boundedElastic());
    }
}

核心流程解析

  1. 创建转换器new BeanOutputConverter<>(MovieActor.class) 会根据 MovieActor 类生成对应的 JSON Schema
  2. 获取格式指令converter.getFormat() 返回 JSON Schema,用于指导 AI 输出格式
  3. 构建提示词 :将 {format} 占位符替换为实际的 Schema 指令
  4. 调用 AI:发送提示词并获取响应
  5. 类型转换converter.convert(response) 自动将 JSON 字符串转为 Java 对象
3.1.3 控制器 API
java 复制代码
@RestController
@RequestMapping("/api/structured")
public class StructuredOutputController {

    private final StructuredOutputService structuredOutputService;

    public StructuredOutputController(StructuredOutputService structuredOutputService) {
        this.structuredOutputService = structuredOutputService;
    }

    /**
     * 获取演员电影信息 - BeanOutputConverter 示例
     */
    @GetMapping("/actor")
    public Mono<ResponseEntity<MovieActor>> getActorMovies(
            @RequestParam String actorName) {
        
        return structuredOutputService.getActorMovies(actorName)
                .map(ResponseEntity::ok);
    }
}
3.1.4 测试示例
bash 复制代码
# 请求
curl "http://localhost:8080/api/structured/actor?actorName=成龙"

# 响应
{
    "actor": "成龙",
    "movies": [
        "警察故事",
        "醉拳",
        "尖峰时刻",
        "红番区",
        "A计划"
    ],
    "awards": [
        "奥斯卡终身成就奖",
        "金马奖最佳男主角",
        "香港电影金像奖"
    ]
}

3.2 MapOutputConverter - 灵活的键值对结构

MapOutputConverter 适用于数据结构不固定或需要动态解析的场景,它将 AI 响应转换为 Map<String, Object>

3.2.1 服务层实现
java 复制代码
/**
 * 使用 MapOutputConverter 获取灵活的键值对数据
 */
public Mono<Map<String, Object>> analyzeTopic(String topic) {
    return Mono.fromCallable(() -> {
        // 创建 MapOutputConverter
        MapOutputConverter converter = new MapOutputConverter();

        String format = converter.getFormat();

        String userPrompt = """
                分析以下主题,并以键值对形式返回相关信息:
                主题:{topic}
                
                请返回以下信息(JSON格式):
                - 定义(definition)
                - 重要性(importance)
                - 相关概念(relatedConcepts,数组形式)
                - 应用场景(applications,数组形式)
                
                {format}
                """;

        PromptTemplate promptTemplate = PromptTemplate.builder()
                .template(userPrompt)
                .variables(Map.of("topic", topic, "format", format))
                .build();

        Prompt prompt = new Prompt(promptTemplate.createMessage());

        String response = chatClient.prompt(prompt)
                .call()
                .content();

        // 转换为 Map
        return converter.convert(response);
    }).subscribeOn(Schedulers.boundedElastic());
}
3.2.2 控制器 API
java 复制代码
/**
 * 分析主题 - MapOutputConverter 示例
 */
@GetMapping("/topic")
public Mono<ResponseEntity<Map<String, Object>>> analyzeTopic(
        @RequestParam String topic) {
    
    return structuredOutputService.analyzeTopic(topic)
            .map(ResponseEntity::ok);
}
3.2.3 测试示例
bash 复制代码
# 请求
curl "http://localhost:8080/api/structured/topic?topic=人工智能"

# 响应
{
    "definition": "人工智能是计算机科学的一个分支,致力于创造能够模拟人类智能的系统",
    "importance": "AI 正在改变各行各业,从医疗诊断到自动驾驶,具有巨大的经济和社会影响",
    "relatedConcepts": ["机器学习", "深度学习", "神经网络", "自然语言处理"],
    "applications": ["智能助手", "图像识别", "推荐系统", "自动驾驶"]
}

适用场景

  • 数据结构动态变化
  • 快速原型开发,不想定义实体类
  • 需要灵活处理不同格式的响应

3.3 ListOutputConverter - 列表数据处理

ListOutputConverter 专门用于处理逗号分隔的列表数据,将 AI 响应转换为 List<String>

3.3.1 服务层实现
java 复制代码
/**
 * 使用 ListOutputConverter 获取列表数据
 */
public Mono<List<String>> getSuggestions(String category, int count) {
    return Mono.fromCallable(() -> {
        // 创建 ListOutputConverter,需要传入 ConversionService
        ListOutputConverter converter = 
            new ListOutputConverter(new DefaultConversionService());

        String format = converter.getFormat();

        String userPrompt = """
                列出 {count} 个关于 {category} 的建议。
                请以逗号分隔的列表形式返回。
                {format}
                """;

        PromptTemplate promptTemplate = PromptTemplate.builder()
                .template(userPrompt)
                .variables(Map.of(
                    "count", String.valueOf(count), 
                    "category", category, 
                    "format", format))
                .build();

        Prompt prompt = new Prompt(promptTemplate.createMessage());

        String response = chatClient.prompt(prompt)
                .call()
                .content();

        // 转换为 List
        return converter.convert(response);
    }).subscribeOn(Schedulers.boundedElastic());
}
3.3.2 控制器 API
java 复制代码
/**
 * 获取建议列表 - ListOutputConverter 示例
 */
@GetMapping("/suggestions")
public Mono<ResponseEntity<List<String>>> getSuggestions(
        @RequestParam String category,
        @RequestParam(defaultValue = "5") int count) {
    
    return structuredOutputService.getSuggestions(category, count)
            .map(ResponseEntity::ok);
}
3.3.3 测试示例
bash 复制代码
# 请求
curl "http://localhost:8080/api/structured/suggestions?category=Java学习资源&count=5"

# 响应
[
    "《Effective Java》书籍",
    "Spring 官方文档",
    "Baeldung 教程网站",
    "LeetCode 算法练习",
    "GitHub 开源项目实践"
]

四、项目结构详解

4.1 实体类设计

java 复制代码
// ProductInfo.java - 产品信息实体
public class ProductInfo {
    private String name;        // 产品名称
    private String description; // 产品描述
    private double price;       // 价格
    private String category;    // 类别
    private int stock;          // 库存
    
    // 必须有无参构造器
    public ProductInfo() {}
    
    // Getters and Setters...
}

4.2 异常处理

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgumentException(
            IllegalArgumentException ex, 
            ServerWebExchange exchange) {
        
        log.warn("参数错误: {}", ex.getMessage());
        
        ErrorResponse error = ErrorResponse.of(
                HttpStatus.BAD_REQUEST.value(),
                HttpStatus.BAD_REQUEST.getReasonPhrase(),
                ex.getMessage(),
                exchange.getRequest().getPath().value()
        );
        
        return ResponseEntity.badRequest().body(error);
    }
}

4.3 响应式编程处理

本项目使用 Project Reactor 进行响应式编程:

java 复制代码
public Mono<MovieActor> getActorMovies(String actorName) {
    return Mono.fromCallable(() -> {
        // 同步代码块
        BeanOutputConverter<MovieActor> converter = 
            new BeanOutputConverter<>(MovieActor.class);
        // ... 业务逻辑
        return converter.convert(response);
    })
    .subscribeOn(Schedulers.boundedElastic()); // 在弹性线程池执行
}

为什么使用 Schedulers.boundedElastic()

  • AI 调用是阻塞 I/O 操作
  • boundedElastic() 提供了有界的弹性线程池
  • 防止阻塞事件循环线程,保持应用响应性

五、API 测试与效果展示

5.1 完整 API 列表

端点 方法 参数 说明
/api/structured/actor GET actorName 获取演员电影信息(Bean)
/api/structured/product POST description 生成产品信息(Bean)
/api/structured/topic GET topic 分析主题(Map)
/api/structured/compare GET product1, product2 产品对比(Map)
/api/structured/suggestions GET category, count 获取建议列表(List)
/api/structured/keywords POST text, count 提取关键词(List)
/api/structured/steps GET task 获取任务步骤(List)

5.2 测试示例

建议:此处插入 Postman 或 curl 测试截图,展示 API 调用过程和响应结果

bash 复制代码
# 1. 测试 BeanOutputConverter
curl "http://localhost:8080/api/structured/actor?actorName=周星驰"

# 2. 测试 MapOutputConverter
curl "http://localhost:8080/api/structured/topic?topic=微服务架构"

# 3. 测试 ListOutputConverter
curl "http://localhost:8080/api/structured/suggestions?category=编程语言&count=3"

# 4. POST 请求测试
curl -X POST "http://localhost:8080/api/structured/product" \
  -H "Content-Type: application/json" \
  -d '{"description": "一款适合程序员的机械键盘"}'

六、避坑指南与最佳实践

6.1 常见问题与解决方案

问题 1:实体类缺少无参构造器
go 复制代码
错误:Cannot construct instance of `org.example.entity.MovieActor`

解决方案 :确保所有用于 BeanOutputConverter 的类都有无参构造器:

java 复制代码
public class MovieActor {
    // 必须提供!
    public MovieActor() {
    }
}
问题 2:AI 响应格式不符合预期

解决方案

  1. 检查 converter.getFormat() 是否正确插入到提示词中
  2. 在提示词中明确指定输出格式要求
  3. 使用更强大的模型(如 GPT-4、Claude 3.5+)
问题 3:响应式编程中的线程阻塞

解决方案 :始终使用 subscribeOn(Schedulers.boundedElastic()) 包装阻塞操作:

java 复制代码
return Mono.fromCallable(() -> {
    // 阻塞操作
    return result;
}).subscribeOn(Schedulers.boundedElastic());

6.2 最佳实践

  1. 实体类设计

    • 始终提供无参构造器
    • 使用包装类型(Integer 而非 int)避免默认值问题
    • 添加字段验证注解(如 @NotNull
  2. 提示词工程

    • 在提示词中明确说明期望的输出格式
    • 提供示例(Few-shot)帮助 AI 理解要求
    • 使用 {format} 占位符插入 JSON Schema
  3. 错误处理

    • 添加全局异常处理器
    • 记录 AI 原始响应便于调试
    • 实现重试机制(Spring AI 已内置)
  4. 性能优化

    • 使用响应式编程处理并发请求
    • 考虑缓存频繁查询的结果
    • 合理设置超时时间

七、总结与扩展思考

7.1 核心要点回顾

本文详细介绍了 Spring AI 结构化输出转换器的三种实现:

  • BeanOutputConverter:将 AI 响应映射到 Java Bean,适合固定结构的数据
  • MapOutputConverter:转换为灵活的 Map 结构,适合动态数据
  • ListOutputConverter:处理逗号分隔的列表数据

通过使用这些转换器,我们可以:

  • 告别繁琐的字符串解析
  • 实现类型安全的 AI 响应处理
  • 大幅提升开发效率和代码可维护性

7.2 扩展思考

基于本项目,你可以进一步探索:

  1. 复杂嵌套对象 :尝试定义包含嵌套对象的实体类,如 Order 包含 List<OrderItem>
  2. 自定义转换器 :实现 StructuredOutputConverter 接口,创建特定领域的转换器
  3. 流式响应 :结合 ChatClient 的流式 API,实现实时结构化输出
  4. 多模型对比:测试不同 AI 模型对结构化输出的支持程度
  5. 前端集成:开发一个可视化界面,动态展示不同类型的结构化输出

7.3 参考资料


标签建议

  • CSDN:JavaSpring BootSpring AI人工智能实战
  • 稀土掘金:后端Spring AIAI 应用开发Java

💰 为什么选择 32ai?

低至 0.56 : 1 比率 🔗 快速访问 : 点击访问 --- 直连、无需魔法。

欢迎在评论区交流讨论!

原创声明:本文为原创教程,转载请注明出处

相关推荐
机器之心9 小时前
AI发布首个全球科学家社区爆火,硅谷投资圈:科技研究领域的「谷歌地图」来了!
人工智能·openai
机器之心10 小时前
1美元Token撬动4800美元收益!AI挑战百万美元级基准,最赚钱的Agent出现了
人工智能·openai
量子位10 小时前
只要1分钟!电脑装满血龙虾,现在跟下载APP似的
aigc·openai
量子位10 小时前
给龙虾定MBTI、发工牌,还让龙虾偷技能…打工人得适应新环境了
openai·ai编程
Java水解12 小时前
微服务架构下Spring Session与Redis分布式会话实战全解析
后端·spring
洛小豆13 小时前
我用 AI 当主力,三天撸了一个跨平台的所见即所得 Markdown 编辑器
openai·ai编程
树上有只程序猿15 小时前
OpenClaw虽香,但不是人人都养得起“小龙虾
前端·openai
树獭叔叔15 小时前
OpenClaw Plugins 与 Hooks 系统:让 AI 助手无限可能
后端·aigc·openai
Mintopia16 小时前
OpenClaw在日常开发中的应用实践与全场景解析
人工智能·openai·ai编程