一、前言
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
最后:如果文章對你有幫助,別忘了點贊支持一下,謝謝~