Spring AI 结构化输出:JSON Mode 与 BeanOutputConverter

Spring AI 结构化输出:JSON Mode 与 BeanOutputConverter

本文深入讲解 Spring AI 中结构化输出的多种方式,包括 JSON Mode、BeanOutputConverter、格式化输出等核心技术。


博客:

https://blog.csdn.net/badao_liumang_qizhi

一、结构化输出概述

1.1 为什么需要结构化输出?

复制代码
文本输出:"价格是 100 元,数量是 5 个"  → 需要正则解析
JSON 输出:{"price": 100, "quantity": 5} → 直接反序列化
Bean 输出:OrderItem 对象               → 直接类型安全使用

1.2 技术方案对比

方案 适用场景 类型安全 复杂度
Prompt 引导 简单场景
JSON Mode OpenAI 兼容模型
BeanOutputConverter Spring AI 原生
Tool Calling 复杂结构提取

二、BeanOutputConverter 实现

2.1 基础用法

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

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ResponseEntity;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class StructuredOutputService {

    private final ChatClient chatClient;

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

    /**
     * 输出为简单 Bean
     */
    public ProductInfo extractProductInfo(String text) {
        return chatClient.prompt()
                .user("从以下文本中提取产品信息:" + text)
                .call()
                .entity(ProductInfo.class);
    }

    /**
     * 输出为列表
     */
    public List<OrderItem> extractOrderItems(String text) {
        return chatClient.prompt()
                .user("从订单文本中提取所有商品项:" + text)
                .call()
                .entity(new ParameterizedTypeReference<List<OrderItem>>() {});
    }

    /**
     * 输出为复杂嵌套结构
     */
    public OrderReport generateOrderReport(String orderText) {
        return chatClient.prompt()
                .system("""
                    你是一个订单分析助手。请分析订单信息,生成结构化报告。
                    输出格式必须包含:
                    - 订单基本信息
                    - 商品列表
                    - 费用明细
                    - 配送信息
                    """)
                .user("订单内容:" + orderText)
                .call()
                .entity(OrderReport.class);
    }

    // 数据模型
    public record ProductInfo(
        String name,
        String brand,
        double price,
        List<String> features
    ) {}

    public record OrderItem(
        String productName,
        int quantity,
        double unitPrice,
        double subtotal
    ) {}

    public record OrderReport(
        String orderId,
        String customerName,
        String status,
        List<OrderItem> items,
        Pricing pricing,
        Shipping shipping
    ) {}

    public record Pricing(
        double subtotal,
        double discount,
        double shippingFee,
        double tax,
        double total
    ) {}

    public record Shipping(
        String method,
        String address,
        String estimatedDelivery
    ) {}
}

2.2 自定义输出转换器

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

import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.OutputConverter;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.Map;

/**
 * 自定义结构输出转换器
 */
@Component
public class CustomStructuredConverter {

    private final ChatModel chatModel;
    private final ObjectMapper objectMapper;

    public CustomStructuredConverter(ChatModel chatModel, ObjectMapper objectMapper) {
        this.chatModel = chatModel;
        this.objectMapper = objectMapper;
    }

    /**
     * 使用 BeanOutputConverter 提取结构化数据
     */
    public <T> T extractStructured(String text, Class<T> targetClass) {
        // 获取目标类型的 JSON Schema
        BeanOutputConverter<T> converter = new BeanOutputConverter<>(targetClass);
        String format = converter.getFormat();

        // 构建提示词,包含格式要求
        String prompt = String.format("""
            请从以下文本中提取信息并转换为结构化数据。
            
            必须按照以下 JSON Schema 格式输出:
            %s
            
            文本内容:%s
            
            只输出 JSON,不要包含任何解释。
            """, format, text);

        String response = chatModel.call(prompt);

        // 解析 JSON 响应
        return converter.convert(response);
    }

    /**
     * 使用 JSON Mode(OpenAI 兼容)
     */
    public Map<String, Object> extractAsJson(String text, String schema) {
        String prompt = String.format("""
            从以下文本中提取信息。
            
            输出格式(必须严格遵循):
            %s
            
            文本:%s
            """, schema, text);

        String response = chatModel.call(new Prompt(prompt));
        
        try {
            return objectMapper.readValue(response, 
                objectMapper.getTypeFactory().constructMapType(Map.class, String.class, Object.class));
        } catch (Exception e) {
            throw new RuntimeException("解析 JSON 失败", e);
        }
    }
}

三、高级模式:多步骤提取

3.1 链式提取

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

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

/**
 * 多步骤结构化提取
 */
@Service
public class ChainExtractionService {

    private final ChatClient chatClient;

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

    /**
     * 第一步:提取实体
     */
    public DocumentEntities extractEntities(String document) {
        return chatClient.prompt()
                .system("从文档中提取所有实体信息(人名、地名、组织名等)")
                .user(document)
                .call()
                .entity(DocumentEntities.class);
    }

    /**
     * 第二步:基于实体提取关系
     */
    public DocumentRelations extractRelations(String document, DocumentEntities entities) {
        return chatClient.prompt()
                .system("分析实体间的关系")
                .user(String.format("文档:%s\n已识别实体:%s", 
                    document, entities.toString()))
                .call()
                .entity(DocumentRelations.class);
    }

    /**
     * 第三步:生成完整知识图谱
     */
    public KnowledgeGraph buildKnowledgeGraph(String document) {
        DocumentEntities entities = extractEntities(document);
        DocumentRelations relations = extractRelations(document, entities);

        return new KnowledgeGraph(entities, relations);
    }

    public record DocumentEntities(
        List<String> persons,
        List<String> organizations,
        List<String> locations,
        List<String> dates
    ) {}

    public record DocumentRelations(
        List<Relation> relations
    ) {
        public record Relation(String source, String type, String target) {}
    }

    public record KnowledgeGraph(DocumentEntities entities, DocumentRelations relations) {}
}

3.2 验证与重试

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

import org.springframework.stereotype.Service;

/**
 * 带验证的结构化输出
 */
@Service
public class ValidatedStructuredService {

    private final ChatClient chatClient;

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

    /**
     * 带重试的结构化提取
     */
    public <T> T extractWithRetry(String text, Class<T> targetClass, 
                                   java.util.function.Predicate<T> validator,
                                   int maxRetries) {
        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                T result = chatClient.prompt()
                        .user(buildPrompt(text))
                        .call()
                        .entity(targetClass);

                // 验证结果
                if (validator.test(result)) {
                    return result;
                }
            } catch (Exception e) {
                if (attempt == maxRetries) {
                    throw new RuntimeException("结构化提取失败,已达最大重试次数", e);
                }
            }
        }
        throw new RuntimeException("结构化提取失败");
    }

    /**
     * 带修正的提取
     */
    public <T> T extractWithCorrection(String text, Class<T> targetClass) {
        try {
            return chatClient.prompt()
                    .user(buildPrompt(text))
                    .call()
                    .entity(targetClass);
        } catch (Exception e) {
            // 解析失败时,让 LLM 修正
            return chatClient.prompt()
                    .system("请将以下文本修正为有效的 JSON 格式:")
                    .user("输入:" + text + "\n错误:" + e.getMessage())
                    .call()
                    .entity(targetClass);
        }
    }

    private String buildPrompt(String text) {
        return String.format("""
            从以下文本中提取结构化数据并输出为 JSON 格式。
            
            - 日期格式使用 ISO 8601(YYYY-MM-DD)
            - 金额使用数字类型,不要包含货币符号
            - 列表项使用数组格式
            - 空值使用 null 而不是空字符串

            文本:%s
            """, text);
    }
}

四、JSON Mode 高级用法

4.1 OpenAI JSON Mode

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

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Service;

/**
 * JSON Mode 输出服务
 */
@Service
public class JsonModeService {

    private final ChatClient chatClient;

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

    /**
     * 使用 response_format 强制 JSON 输出
     */
    public String getJsonResponse(String prompt) {
        return chatClient.prompt()
                .user(prompt)
                .call()
                .content();
    }

    /**
     * 批量提取
     */
    public <T> List<T> batchExtract(List<String> texts, Class<T> targetClass) {
        return texts.parallelStream()
                .map(text -> chatClient.prompt()
                        .user("提取结构化数据:" + text)
                        .call()
                        .entity(targetClass))
                .toList();
    }
}

五、总结

技术 优点 缺点 推荐场景
BeanOutputConverter 类型安全、Spring 原生 依赖反射 标准 Java Bean
JSON Mode 兼容性好 需要手动解析 OpenAI 兼容 API
Tool Calling 最可靠 需要定义工具 复杂嵌套结构
Prompt 引导 简单直接 不可靠 快速原型

参考资源:

相关推荐
G***技16 分钟前
IB3-771嵌入式主板6 TOPS真实算力怎么用:从PyTorch到RKNN的量化落地
人工智能·嵌入式硬件
cxr82817 分钟前
涌现与坍塌在AI自然语义分析与生成中的层级化因果约束
人工智能·智能体·认知框架
nanawinona19 分钟前
先跑通小流程,再让 AI 和 Python 承接复杂量化
人工智能·python
beiju19 分钟前
别拿生产账号给 Agent 实习:从 Anthropic 事故看 Agent Staging
人工智能
用户52746756142119 分钟前
模型退役不是换个 ID:Agent 迁移最容易丢的是岗位能力
人工智能
奈斯先生Vector23 分钟前
本地图片识别怎么接入多模态 AI?用 Python API 理解 GPT-4o Vision 的真实工作流
开发语言·人工智能·windows·python·网络协议·http·aigc
国科安芯23 分钟前
卫星电源管理系统中高可靠MCU的功耗特性与电源监控功能分析
人工智能·单片机·嵌入式硬件·mcu·安全·电源管理系统·抗辐射
空堂与归24 分钟前
处理序列数据问题:用循环神经网络RNN建模时序依赖
人工智能
AI的探索之旅24 分钟前
97 个 OpenCV 实例(十五):几何校正,透视变换 + ECC 对齐
人工智能·opencv·计算机视觉