一、前言
Spring AI 提供跨 AI 供应商(如 OpenAI、Hugging Face 等)的一致性 API, 通过分装的ChatModel或ChatClient即可轻松调动LLM进行流式或非流式对话。
本专栏主要围绕着通过OpenAI兼容接口调用各种大语言模型展开学习(因为大部分模型都兼容OpenAI方式调用接口),本篇文章目标:探索Spring AI的结构化输出,即将 LLM 的非结构化文本输出转换为结构化数据(如 Java 对象、Map、List 或 JSON)
二、转化器Converter概述
2.1 核心作用
在调用 LLM 之前,转换器将格式指令附加到提示中,为模型提供明确的指导,以生成所需的输出结构。这些指令充当蓝图,塑造模型的响应以符合指定的格式。
在 LLM 调用后,转换器将模型的输出文本转换为结构化类型的实例。此转换过程涉及解析原始文本输出并将其映射到相应的结构化数据表示形式,例如 JSON、XML 或特定领域的数据结构。
2.2 数据流向
PS : 转换器有点类似Java中的AOP(拦截器)的作用,在调用LLM前注入结构化指令,在调用LLM后将非结构化文本后转换成指定的结构化输出!
2.3 三大核心转换器对比
Spring AI通过三大核心转换器实现将LLM的响应响应转成特定的结构化输出!
三、代码
3.1 项目依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.better</groupId><artifactId>spring-ai-parent</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><modules><module>models/chat/chat-openai-deepseek</module></modules><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.5</version><relativePath/></parent><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-openai</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.57</version></dependency></dependencies><!--Spring AI模块的依赖版本管理--><dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><!--正式生产版本:1.0.0 GA(2025年5月20日发布)是首个稳定且支持生产环境的版本,ChatClient 成为官方推荐的核心 API--><version>1.0.0</version> <!-- GA 版本 --><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement></project>
3.2 配置yml
server:port: 8321
spring:ai:openai:base-url: https://api.deepseek.comapi-key: ${OPENAI_API_KEY}chat:options:model: deepseek-chat # 可选模型:deepseek-chat/deepseek-reasonertemperature: 0.6 # 响应随机性控制,默认值
3.3 Bean对象转换器
@GetMapping("/chat/bean")BookInfo chatBeanOutput(String author) {var userPromptTemplate = """告诉我作者{author}最有名的书名.""";BookInfo bookInfo = chatClient.prompt().user(userSpec -> userSpec.text(userPromptTemplate).param("author", author)).call().entity(BookInfo.class);log.info("LLM响应结果:{}", JSONObject.toJSONString(bookInfo));return bookInfo;}
执行结果:
3.4 Map转换器
@GetMapping("/chat/map")Map<String,Object> chatMapOutput() {Map<String, Object> result = chatClient.prompt().user(u -> u.text("Provide me a List of {subject}").param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'")).call().entity(new ParameterizedTypeReference<Map<String, Object>>() {});log.info("LLM响应结果:{}", JSONObject.toJSONString(result));return result;}
执行结果:
3.5 List转换器
@GetMapping("/chat/list")List<String> chatListOutput() {List<String> flavors = chatClient.prompt().user(u -> u.text("List five {subject}").param("subject", "李笑来写的书")).call().entity(new ListOutputConverter(new DefaultConversionService()));log.info("LLM响应结果:{}", JSONObject.toJSONString(flavors));return flavors;}
执行结果:
3.6 完整代码
package com.better.springai.structured_output;import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;
import java.util.Map;/*** 高级封装ChatClient*/
@RestController
@Slf4j
class StructuredOutputController {private final ChatClient chatClient;StructuredOutputController(ChatClient.Builder chatClientBuilder) {this.chatClient = chatClientBuilder.build();}/*** 结构化输出-转成Bean对象* @param author* return com.better.springai.structured_output.BookInfo* @author luchuyan* @time 2025/7/23 8:33**/@GetMapping("/chat/bean")BookInfo chatBeanOutput(String author) {var userPromptTemplate = """告诉我作者{author}最有名的书名.""";BookInfo bookInfo = chatClient.prompt().user(userSpec -> userSpec.text(userPromptTemplate).param("author", author)).call().entity(BookInfo.class);log.info("LLM响应结果:{}", JSONObject.toJSONString(bookInfo));return bookInfo;}/*** 结构化输出-转成Map* return java.util.Map<java.lang.String,java.lang.Object>* @author luchuyan* @time 2025/7/23 8:34**/@GetMapping("/chat/map")Map<String,Object> chatMapOutput() {Map<String, Object> result = chatClient.prompt().user(u -> u.text("Provide me a List of {subject}").param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'")).call().entity(new ParameterizedTypeReference<Map<String, Object>>() {});log.info("LLM响应结果:{}", JSONObject.toJSONString(result));return result;}/*** 结构化输出-转成List* return java.util.List<java.lang.String>* @author luchuyan* @time 2025/7/23 8:34**/@GetMapping("/chat/list")List<String> chatListOutput() {List<String> flavors = chatClient.prompt().user(u -> u.text("List five {subject}").param("subject", "李笑来写的书")).call().entity(new ListOutputConverter(new DefaultConversionService()));log.info("LLM响应结果:{}", JSONObject.toJSONString(flavors));return flavors;}}
3.7 一定能结构化输出吗?
能不能转成指定的结构化输出主要还要看LLM的能力,如调用DeepSeek的推理模型一般就比调用非推理模型的效果好些,较大参数的模型比较少的好些 (如Qwen32B比Qwen7B好些~)。
在我们的业务中调用本地部署的Qwen2-7B模型,尽管在Prompt中要求模型按JSON结构化输出,但往往不能如期所愿(非完整JSON格式导致解析时异常,如缺少了花括弧、带了一些特殊字符等),我们的一个解决方案是再调一次腾讯元的混元模型进行修复,通过混元模型基本都能修复成正常的JSON!
四、参考资料
4.1 Spring AI官网文档
- Structured Output Converter :: Spring AI Reference
最后:如果文章对你有帮助,别忘了点赞支持一下,谢谢~