Spring AI 统一结构化返回:ChatModel / ChatClient 两种实现方式

在Spring AI开发中,大模型默认返回自然语言文本,混杂多余描述、代码块标记,业务代码无法直接反序列化使用。本文基于Spring AI官方规范,详解结构化输出(Structured Output)核心概念、两套实现方案、技术选型对比及高频踩坑解决方案,适配日常业务开发与项目落地。

一、结构化输出(Structured Output)核心概念

1.1 官方定义

引用 Spring AI 官方定义:

如果您想从 LLM 接收结构化输出,Structured Output 可以协助将 ChatModel/ChatClient 方法的返回类型从 String 更改为其他类型。

LLM 生成结构化输出的能力对于依赖可靠解析输出值的下游应用程序非常重要。开发人员希望快速将 AI 模型的结果转换为可以传递给其他应用程序函数和方法的数据类型,例如 JSON、XML 或 Java 类。Spring AI 结构化输出转换器可自动将 LLM 原始文本输出转为标准化结构化格式。

1.2 传统开发业务痛点

不做结构化约束时,大模型返回的文本通常混杂自然语言描述,并非纯净结构化数据,示例如下:

复制代码
好的,这是结果:
{
    "title":"SpringAI",
    "desc":"Java AI开发框架"
}

这种格式存在严重问题:前端、后端业务代码无法直接反序列化,需要手动编写正则清洗文本,开发效率低、容错性差,极易出现解析异常。

1.3 结构化输出核心目标

强制约束大模型输出规则,让模型稳定输出标准结构化数据(JSON),由 Spring AI 框架自动完成 原始文本 → Java 实体对象 的转换,彻底规避手动清洗数据的问题。

1.4 Spring AI 两大实现技术路线

  • **底层API:**ChatModel + OutputParser,灵活性最高,支持多角色模板、外部文件Prompt,适配复杂场景
  • **高层DSL:**ChatClient 流式调用 call().entity(),代码极简,内置封装转换器,适合快速开发

二、前置准备:定义统一接收POJO

本文所有案例统一使用该实体类接收结构化JSON结果,简化代码冗余,基于Lombok简化开发:

复制代码
import lombok.Data;

/**
 * AI结构化输出统一接收实体
 */
@Data
public class ArticleDTO {
    // 文章标题
    private String title;
    // 文章简介
    private String description;
    // 文章作者
    private String author;
}

三、方案一:底层 ChatModel + JsonOutputParser(标准企业方案)

JsonOutputParser 是Spring AI官方核心结构化输出转换器,也是复杂项目首选方案。核心具备两大能力:

  1. **getFormatInstructions():**自动生成JSON格式约束提示词,自动注入Prompt
  2. **parse():**接收模型原始文本,自动解析转换为对应Java实体类

完整可运行代码

复制代码
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.parser.JsonOutputParser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;

@RestController
public class StructuredOutputController {

    private final ChatModel chatModel;

    // 构造器注入,统一Spring AI开发规范
    public StructuredOutputController(ChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @GetMapping("/structured/model-parser")
    public ArticleDTO structuredByModel(String topic) {
        // 1. 创建结构化输出转换器,绑定接收实体类
        JsonOutputParser<ArticleDTO> parser = new JsonOutputParser<>(ArticleDTO.class);

        // 2. 定义系统角色模板:基础输出约束 + 自动注入格式规范
        SystemPromptTemplate sysTemplate = new SystemPromptTemplate("
                禁止输出任何前言、解释、markdown代码块标记。
                {format_instructions}
                ");
        // 注入框架自动生成的JSON格式约束
        Message sysMsg = sysTemplate.createMessage(Map.of("format_instructions", parser.getFormatInstructions()));

        // 3. 定义用户角色模板,动态接收业务参数
        PromptTemplate userTemplate = new PromptTemplate("生成一篇关于{topic}的短文信息");
        Message userMsg = userTemplate.createMessage(Map.of("topic", topic));

        // 4. 组装多角色Prompt消息
        Prompt prompt = new Prompt(List.of(sysMsg, userMsg));

        // 5. 调用大模型,自动解析文本为Java实体
        String rawResult = chatModel.call(prompt).getResult().getOutput().getText();
        return parser.parse(rawResult);
    }
}

适用场景:需要手动管理SystemPrompt、外部txt提示词文件、复杂多角色消息编排的场景;与PromptTemplate、多消息角色体系完全无缝衔接,是中大型复杂AI项目的标准方案。

四、方案二:高层 ChatClient DSL 流式写法(简洁开发首选)

ChatClient 高层DSL内部已封装结构化转换器,提供 call().entity(Class<T>) 极简API,底层依旧复用JsonOutputParser,无需手动创建解析器,代码极度简洁,适合快速开发、接口原型搭建。

4.1 基础字符串User写法

适合固定提示词、无复杂参数渲染的简单场景:

复制代码
@GetMapping("/structured/client-simple")
public ArticleDTO structuredClientSimple(String topic) {
    return chatClient.prompt()
            // 系统级输出约束
            .system("禁止输出多余文字,不要```json代码块标记")
            // 固定用户提示词
            .user("生成一篇关于" + topic + "的短文信息")
            .call()
            // 自动结构化解析为实体
            .entity(ArticleDTO.class);
}

4.2 Consumer模板写法(重点推荐)

支持 {key} 占位符模板 + .param() 动态绑定参数,无需手动new PromptTemplate,兼顾简洁性与动态性,是轻量级业务接口最优写法。

Lambda表达式写法(日常开发主流)

复制代码
@GetMapping("/structured/client-consumer")
public ArticleDTO structuredClientConsumer(String topic) {
    return chatClient.prompt()
            .system("禁止输出多余文字,不要```json代码块标记")
            // Lambda实现Consumer,动态模板传参
            .user(spec -> spec
                    .text("生成一篇关于{topic}的短文信息")
                    .param("topic", topic)
            )
            .call()
            .entity(ArticleDTO.class);
}

匿名内部类写法(原理学习参考)

Lambda语法糖脱糖后的原生写法,兼容所有Java版本,仅用于理解底层原理,项目中统一使用Lambda写法:

复制代码
@GetMapping("/structured/client-anonymous")
public ArticleDTO structuredClientAnonymous(String topic) {
    return chatClient.prompt()
            .system("禁止输出多余文字,不要```json代码块标记")
            // 完整匿名内部类实现Consumer接口
            .user(new Consumer<ChatClient.PromptUserSpec>() {
                @Override
                public void accept(ChatClient.PromptUserSpec spec) {
                    spec.text("生成一篇关于{topic}的短文信息")
                        .param("topic", topic);
                }
            })
            .call()
            .entity(ArticleDTO.class);
}

Consumer核心原理:Consumer是Java8函数式接口,仅包含一个抽象方法,支持Lambda简化写法:

复制代码
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

**开发规范:**项目统一使用Lambda表达式,代码简洁可读性高;匿名内部类仅用于原理学习。

五、两套API核心选型对比

两套方案底层核心一致,均基于Spring AI OutputParser,仅封装层级不同,可根据项目复杂度选型:

开发方式 核心API 优点 局限 适用场景
底层原始API ChatModel + JsonOutputParser 高度可控,支持外部文件模板、灵活组装多角色消息,适配复杂提示词工程 代码量偏多,需手动处理解析逻辑 中大型项目、提示词复杂、多角色消息编排场景
高层流式DSL ChatClient + call().entity() 代码极简,内置结构化转换器,开发效率极高 外部txt模板编排不够直观 小型业务接口、快速原型开发、简单AI功能

**底层原理:**chatClient.prompt().call().entity() 底层会自动创建JsonOutputParser,与底层API本质一致。

六、开发高频踩坑解决方案(必看)

6.1 问题:模型返回Markdown JSON代码块,解析报错

**现象:**大模型返回内容携带```json标记,导致实体解析失败:

复制代码
```json
{"title":"xxx","description":"xxx","author":"xxx"}
```

**解决方案1(优先):**在系统提示词中强制约束输出规则:

复制代码
禁止使用markdown代码块包裹返回内容,只输出纯净JSON字符串。

**解决方案2(兜底工具):**封装公共文本清洗工具类,兼容异常场景:

复制代码
/**
 * 清洗大模型返回的Markdown JSON标记
 */
public static String cleanJsonMarkdown(String text) {
    return text.replaceAll("```json", "")
            .replaceAll("```", "")
            .trim();
}

6.2 问题:字段缺失、格式错乱导致解析异常

**问题现象:**模型返回字段缺失、JSON格式错误,抛出ParseException,程序直接报错。

**解决方案:**所有结构化解析逻辑必须捕获异常,打印原始返回文本,方便快速定位问题:

复制代码
try {
    return parser.parse(rawResult);
} catch (ParseException e) {
    // 打印模型原始返回内容,精准排查格式问题
    log.error("结构化输出解析失败,原始返回:{}", rawResult, e);
    throw new RuntimeException("模型返回格式异常,请重试");
}

6.3 占位符大小写严格匹配

模板中的占位符名称(如{topic})与param、Map传入的key名称必须大小写完全一致,否则占位符无法渲染,导致提示词失效、输出结果异常。

七、总结

  1. 结构化输出的核心价值是标准化模型返回值,实现AI结果与业务代码的无缝对接,彻底告别手动文本清洗;
  2. 复杂项目、自定义提示词工程优先使用 ChatModel + JsonOutputParser,灵活性拉满;
  3. 简单接口、快速开发优先使用 ChatClient DSL + Lambda模板传参,代码简洁高效;
  4. 开发必须做好输出约束、异常捕获、文本兜底清洗,保证接口稳定性。
相关推荐
Mr. zhihao44 分钟前
深度解析:为什么Java序列化需要搭配ByteArrayOutputStream?IO装饰器模式的精妙设计
java·开发语言·装饰器模式
vx-程序开发1 小时前
springboot旅游推介平台---附源码24175
java·spring boot·python·spring cloud·eclipse·django·idea
艾莉丝努力练剑1 小时前
【QT:解决问题】Qt5Core.dll:无法定位程序输入点
java·开发语言·qt·学习·面试
小蒜学长2 小时前
“喵汪联盟”宠物领养系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·宠物
程序员黑豆2 小时前
Java字符串常量池完全指南:原理、intern()方法与性能优化最佳实践
java·前端·ai编程
AI人工智能+电脑小能手2 小时前
大白话说Java设计模式-14-适配器模式(业务实战篇)
java·设计模式·适配器模式·系统兼容·多渠道对接
xbgRS5 小时前
springboot的自动装配
java·spring boot
嘻哈∠※12 小时前
0061基于 SpringBoot 的投稿与稿件处理系统设计与实现
java·spring boot·后端
白狐_79813 小时前
408数据结构第8章:排序②——性质对比秒杀、场景选择与外部排序
java·数据结构·算法