Spring AI Alibaba Agent 结构化输出(Structured Output)完整指南

Spring AI Alibaba Agent 结构化输出(Structured Output)完整指南

基于 Spring AI Alibaba 1.1.2.0,实现 Agent 按标准 JSON 格式返回数据,支持 Java POJO 自动映射,提升程序化消费效率


一、概述

结构化输出是 Agent 框架中一项重要能力,它允许 Agent 以固定的、可预测的格式(如 JSON)返回数据,而不是自然语言的自由文本。这极大地简化了应用程序对 Agent 输出的解析和处理,尤其适用于数据提取、系统集成、API 对接等场景。

Spring AI Alibaba 通过 ReactAgent.Builder 提供了两种简洁的方式来实现结构化输出,同时框架自动处理 JSON Schema 的生成与注入,开发者只需关注业务逻辑。

本文将通过一个完整的可运行示例,从零开始演示如何配置和使用结构化输出,并特别强调在实际开发中必须处理的异常情况。


二、核心概念

2.1 什么是结构化输出?

结构化输出是指 Agent 按照预定义的格式(通常是 JSON)返回数据,该格式可直接映射为 Java 对象(POJO)。与传统的自然语言响应相比,结构化输出具有以下优势:

对比维度 自然语言输出 结构化输出
输出格式 自由文本 固定 JSON 结构
解析方式 正则/字符串处理 直接 JSON 反序列化
类型安全 强(编译期校验)
应用场景 聊天问答 数据提取、系统集成

典型场景 :从一段描述中提取联系人信息(姓名、邮箱、电话),Agent 直接返回 {"name":"张三","email":"zhangsan@example.com","phone":"123456"}

2.2 实现方式

Spring AI Alibaba 提供两种实现方式:

方法 说明 推荐度
.outputType(Class<?> type) 传入 Java 类,框架自动生成 JSON Schema ⭐⭐⭐⭐⭐ 强烈推荐
.outputSchema(String schema) 手动传入 JSON Schema 字符串 仅特殊场景

推荐理由outputType 利用 Java 类型信息自动生成 Schema,维护成本低,且编译期即可发现类型错误。

2.3 工作原理

  1. 调用前 :框架通过 BeanOutputConverter 根据 POJO 或手动 Schema 生成 JSON 格式指令,追加到用户消息后。
  2. 调用中:模型按指令生成符合 Schema 的 JSON 字符串。
  3. 调用后 :返回的 AssistantMessage.getText() 即为 JSON 字符串,开发者可反序列化为 POJO。

模型兼容性:对于支持原生结构化输出的模型(如 DashScopeChatModel),框架优先使用原生能力;否则回退到 ToolCall 策略。


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

三、完整示例项目

3.1 项目结构

复制代码
spring-ai-structured-output-demo/
├── pom.xml
├── src/main/
│   ├── java/com/example/ai/
│   │   ├── SpringAiStructuredOutputDemoApplication.java
│   │   ├── config/
│   │   │   └── AgentConfig.java
│   │   ├── controller/
│   │   │   └── StructuredOutputController.java
│   │   ├── service/
│   │   │   └── StructuredOutputService.java
│   │   └── model/
│   │       ├── ContactInfo.java
│   │       └── ProductReview.java
│   └── resources/
│       └── application.yml

3.2 依赖配置(pom.xml)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>
    <groupId>com.example.ai</groupId>
    <artifactId>spring-ai-structured-output-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>17</java.version>
        <spring-ai-alibaba.version>1.1.2.0</spring-ai-alibaba.version>
        <jackson.version>2.17.2</jackson.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.fasterxml.jackson</groupId>
                <artifactId>jackson-bom</artifactId>
                <version>${jackson.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring AI Alibaba Agent Framework -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-agent-framework</artifactId>
            <version>${spring-ai-alibaba.version}</version>
        </dependency>

        <!-- DashScope Starter -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
            <version>${spring-ai-alibaba.version}</version>
        </dependency>

        <!-- Jackson 相关 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

注意 :必须管理 Jackson 版本,避免因版本不一致导致 NoSuchMethodError

3.3 配置文件(application.yml)

yaml 复制代码
server:
  port: 885

spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}
    chat:
      options:
        model: deepseek-v4-flash   # 或 qwen-max

logging:
  level:
    com.alibaba.cloud.ai: debug
    com.example.ai: debug

3.4 输出 POJO 定义

简单 POJO(联系人信息)
java 复制代码
package com.example.ai.model;

public class ContactInfo {
    private String name;
    private String email;
    private String phone;

    // 无参构造器(Jackson 反序列化必需)
    public ContactInfo() {}

    // 带参构造器(便于测试)
    public ContactInfo(String name, String email, String phone) {
        this.name = name;
        this.email = email;
        this.phone = phone;
    }

    // Getters & Setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }

    @Override
    public String toString() {
        return "ContactInfo{name='" + name + "', email='" + email + "', phone='" + phone + "'}";
    }
}
复杂嵌套 POJO(商品评价)
java 复制代码
package com.example.ai.model;

import java.util.Arrays;

public class ProductReview {
    private int rating;
    private String sentiment;          // "positive", "neutral", "negative"
    private String[] keyPoints;
    private ReviewDetails details;

    public ProductReview() {}

    // Getters & Setters 省略,请参考前文

    public static class ReviewDetails {
        private String[] pros;
        private String[] cons;
        private String summary;

        // 无参构造器和 getter/setter 省略
    }

    @Override
    public String toString() {
        return "ProductReview{rating=" + rating + ", sentiment='" + sentiment + 
               "', keyPoints=" + Arrays.toString(keyPoints) + ", details=" + details + "}";
    }
}

3.5 Agent 配置类(AgentConfig.java

java 复制代码
package com.example.ai.config;

import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import com.example.ai.model.ContactInfo;
import com.example.ai.model.ProductReview;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AgentConfig {

    /**
     * 方式一:使用 outputType(推荐)
     */
    @Bean
    public ReactAgent contactAgent(ChatModel chatModel) {
        return ReactAgent.builder()
                .name("contact_extractor")
                .model(chatModel)
                .outputType(ContactInfo.class)
                .saver(new MemorySaver())
                .build();
    }

    /**
     * 方式二:使用 outputSchema(高级自定义)
     * 使用 BeanOutputConverter 从 POJO 自动生成 Schema
     */
    @Bean
    public ReactAgent reviewAgent(ChatModel chatModel) {
        org.springframework.ai.converter.BeanOutputConverter<ProductReview> converter =
                new org.springframework.ai.converter.BeanOutputConverter<>(ProductReview.class);
        String schema = converter.getFormat();

        return ReactAgent.builder()
                .name("review_analyzer")
                .model(chatModel)
                .outputSchema(schema)
                .saver(new MemorySaver())
                .build();
    }
}

3.6 Service 层(含异常处理)

关键点reactAgent.call(...) 可能抛出 GraphRunnerException,必须捕获并进行妥善处理。

java 复制代码
package com.example.ai.service;

import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.exception.GraphRunnerException;
import com.example.ai.model.ContactInfo;
import com.example.ai.model.ProductReview;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.stereotype.Service;

@Service
public class StructuredOutputService {

    private final ReactAgent contactAgent;
    private final ReactAgent reviewAgent;
    private final ObjectMapper objectMapper;

    public StructuredOutputService(ReactAgent contactAgent,
                                   ReactAgent reviewAgent,
                                   ObjectMapper objectMapper) {
        this.contactAgent = contactAgent;
        this.reviewAgent = reviewAgent;
        this.objectMapper = objectMapper;
    }

    /**
     * 提取联系人信息(使用 outputType)
     */
    public ContactInfo extractContact(String text, String sessionId) {
        RunnableConfig config = RunnableConfig.builder()
                .threadId(sessionId)
                .build();

        AssistantMessage response = null;
        try {
            response = contactAgent.call(text, config);
        } catch (GraphRunnerException e) {
            // 捕获 Agent 执行异常,转换为业务异常
            throw new RuntimeException("Agent 执行失败: " + e.getMessage(), e);
        }

        String json = response.getText();
        System.out.println("原始 JSON 响应: " + json);

        try {
            return objectMapper.readValue(json, ContactInfo.class);
        } catch (Exception e) {
            throw new RuntimeException("解析结构化输出失败,原始 JSON: " + json, e);
        }
    }

    /**
     * 分析商品评价(使用 outputSchema)
     */
    public ProductReview analyzeReview(String reviewText, String sessionId) {
        RunnableConfig config = RunnableConfig.builder()
                .threadId(sessionId)
                .build();

        String prompt = "请分析以下商品评价,提取评分、情感倾向、关键点和详细优缺点:\n" + reviewText;
        AssistantMessage response = null;
        try {
            response = reviewAgent.call(prompt, config);
        } catch (GraphRunnerException e) {
            throw new RuntimeException("Agent 执行失败: " + e.getMessage(), e);
        }

        String json = response.getText();
        System.out.println("原始 JSON 响应: " + json);

        try {
            return objectMapper.readValue(json, ProductReview.class);
        } catch (Exception e) {
            throw new RuntimeException("解析结构化输出失败,原始 JSON: " + json, e);
        }
    }

    /**
     * 直接获取原始 JSON 字符串(不反序列化)
     */
    public String extractContactRaw(String text, String sessionId) {
        RunnableConfig config = RunnableConfig.builder()
                .threadId(sessionId)
                .build();

        AssistantMessage response = null;
        try {
            response = contactAgent.call(text, config);
        } catch (GraphRunnerException e) {
            throw new RuntimeException("Agent 执行失败: " + e.getMessage(), e);
        }
        return response.getText();
    }
}

为什么要捕获 GraphRunnerException

ReactAgent.call() 方法在 Agent 执行过程中可能因多种原因失败(如模型调用超时、工具执行错误、状态保存失败等),如果不捕获,异常将向上传播导致接口返回 500。通过捕获并转换为业务异常,可以更好地控制错误响应。


3.7 Controller 层

java 复制代码
package com.example.ai.controller;

import com.example.ai.model.ContactInfo;
import com.example.ai.model.ProductReview;
import com.example.ai.service.StructuredOutputService;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/structured")
public class StructuredOutputController {

    private final StructuredOutputService service;

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

    @PostMapping("/contact")
    public Map<String, Object> extractContact(
            @RequestParam String text,
            @RequestParam(required = false, defaultValue = "default") String sessionId) {
        ContactInfo contact = service.extractContact(text, sessionId);
        return Map.of("success", true, "data", contact, "sessionId", sessionId);
    }

    @PostMapping("/contact/raw")
    public Map<String, Object> extractContactRaw(
            @RequestParam String text,
            @RequestParam(required = false, defaultValue = "default") String sessionId) {
        String json = service.extractContactRaw(text, sessionId);
        return Map.of("success", true, "json", json, "sessionId", sessionId);
    }

    @PostMapping("/review")
    public Map<String, Object> analyzeReview(
            @RequestParam String reviewText,
            @RequestParam(required = false, defaultValue = "default") String sessionId) {
        ProductReview review = service.analyzeReview(reviewText, sessionId);
        return Map.of("success", true, "data", review, "sessionId", sessionId);
    }
}

3.8 启动类

java 复制代码
package com.example.ai;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StructuredOutputDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(StructuredOutputDemoApplication.class, args);
    }
}

四、测试与验证

4.1 启动应用

确保环境变量 DASHSCOPE_API_KEY 已设置,运行:

bash 复制代码
mvn spring-boot:run

4.2 测试联系人提取(返回 POJO)

bash 复制代码
curl -X POST "http://localhost:885/api/structured/contact?text=从以下信息提取联系方式:王五,wangwu@outlook.com,+86 139-9999-8888&sessionId=test01"

预期响应

json 复制代码
{
  "success": true,
  "data": {
    "name": "王五",
    "email": "wangwu@outlook.com",
    "phone": "+86 139-9999-8888"
  },
  "sessionId": "test01"
}

4.3 测试原始 JSON 输出

bash 复制代码
curl -X POST "http://localhost:885/api/structured/contact/raw?text=赵六,zhaoliu@163.com,010-88886666&sessionId=test01"

预期响应

json 复制代码
{
  "success": true,
  "json": "{\"name\":\"赵六\",\"email\":\"zhaoliu@163.com\",\"phone\":\"010-88886666\"}",
  "sessionId": "test01"
}

4.4 测试商品评价分析

bash 复制代码
curl -X POST "http://localhost:885/api/structured/review?reviewText=这款耳机音质不错,降噪效果好,但佩戴舒适度一般,价格略高。&sessionId=test01"

预期响应(结构示例):

json 复制代码
{
  "success": true,
  "data": {
    "rating": 4,
    "sentiment": "positive",
    "keyPoints": ["音质不错", "降噪效果好", "佩戴舒适度一般", "价格略高"],
    "details": {
      "pros": ["音质不错", "降噪效果好"],
      "cons": ["佩戴舒适度一般", "价格略高"],
      "summary": "整体满意,舒适度和价格有改进空间"
    }
  },
  "sessionId": "test01"
}

4.5 查看日志

控制台会输出 DEBUG 信息,包括 Agent 追加的 Schema 指令以及模型返回的原始 JSON,便于调试。


五、异常处理最佳实践

在 Service 层捕获 GraphRunnerException 并转换为业务异常,可以避免底层异常暴露给前端,同时便于统一错误处理。

异常类型 处理方式
GraphRunnerException 记录日志,转换为业务异常(如 RuntimeException
JsonProcessingException 记录原始 JSON,抛出解析异常
其他未预期异常 记录错误信息,返回通用错误码

建议在 Controller 层使用 @ControllerAdvice 统一处理异常,返回友好的错误 JSON。


六、选择 outputType 还是 outputSchema

场景 推荐方式 原因
标准业务对象(联系人、订单) outputType 类型安全,维护成本低
复杂嵌套对象 outputType 自动生成完整 Schema
需要自定义字段描述 outputSchema 可精细控制每个字段
快速原型开发 outputType 只需定义 POJO
无法定义 Java 类型(纯动态 JSON) outputSchema 灵活

结论 :绝大多数情况使用 outputType 即可。


七、总结

  • 结构化输出:让 Agent 按固定 JSON 格式返回数据,方便程序化消费。
  • 两种方式outputType(推荐)和 outputSchema
  • 核心机制BeanOutputConverter 自动生成 JSON Schema 并注入到提示中。
  • 异常处理 :务必捕获 GraphRunnerException,确保应用健壮性。
  • 测试验证:通过 REST API 直观验证结构化输出效果。

通过本文,你已经掌握了如何在 Spring AI Alibaba 中使用结构化输出,并能够应用到实际项目中,提升 Agent 与业务系统的集成效率。


参考资源