1. 引入
Spring 官?推出的?個穩定版??智能(AI)集成框架. 旨在幫助 Java/Spring 開發者更便捷地在企業級應?中集成 AI 能? (如?語?模型、機器學習、向量數據庫、圖像?成等)。
它主要提供了以下功能:
? ?持主要的AI模型提供商, ?如 Anthropic、OpenAI、Microsoft、Amazon、Google 和 Ollama, 此外它?持的模型種類也?常多, ?如: 聊天模型, 嵌?模型, 圖像模型, ?頻模型, 內容審核等.
? 跨 AI 提供商的可移植 API ?持. ?持聊天 (Chat) , ?本到圖像 (text-to-image) 和嵌?(Embedding) 模型的統?接?, 同時提供同步和流式 API 選項. ?持訪問模型特定功能.
總之, Spring AI 為 ?AI專家的開發者也能快速調??語?模型, 提供了構建 AI 應?的基礎抽象層, 允許開發?員通過極少的代碼修改即可輕松替換組件, 簡化集成了AI功能的應?程序開發。
spring AI 官網 鏈接
2. 術語介紹
2.1 模型
模型是人工智能系統的核心組件,通過算法和大量數據訓練而成,能夠執行特定任務,如預測、分類或生成內容,?如ChatGPT、DeepSeek、通義千問等等. 每種模型能?不同, 適合的任務也不同。
我們給模型的叫 輸入;模型返回給我們的叫 輸出。
Spring AI 的能讓我們在 Java/Spring 應?中, 能更方便地:① 選擇不同的 模型 ;
② 構造和發送 輸? (Prompt) ;③接收并處理 輸出 (AI 的響應) 。
2.2 LLM
LLM 即 Large Language Model 大語言模型,也稱?型語?模型。是??智能模型中專?處理?本的?種類型, 屬于語?模型的范疇。
特點是:規模龐?, 參數數量通常在數百億到萬億級別。
以下是幾個代表模型:
? GPT-5(OpenAI)
? ?持 128K?上下?(128K token的上下文長度大約可以支持128,000個漢字) , 在多輪復雜推理、創意寫作中表現突出
? DeepSeek R1(深度求索)
? 開源, 專注于邏輯推理與數學求解, ?持128K?上下?和多語? (20+語?) , 在科技領域表現突出
? Qwen2.5-72B-Instruct (阿?巴巴)
? 通義千問開源模型家族重要成員, 擅?代碼?成結構化數據 (如JSON) 處理??扮演對話等, 尤其適合企業級復雜任務, ?持包括中?英?法語等29種語?
? Gemini 2.5 Pro (Google)
? 多模態融合標桿 , ?持圖像/代碼/?本混合輸?, 適合跨模態任務 (如圖??成、技術?檔解析)
2.3 提示詞
提示詞是用戶或系統提供給大語言模型 (LLM) 的指令或文本 ,用于引導模型生成特定輸出。
用戶提示詞:我們使用 ai 工具時,發給 它 的文字,文件等就屬于 用戶提示詞。
系統提示詞:一般是開發者預設的,它 對 ai 進行了行為預設,劃定邊界等,像你使用 ai 時,它告訴你問題暫時不能回答之類的,就是受到了系統提示詞的規范。
2.4 詞元
詞元(即 Token)是自然語言處理(NLP)中的基本單位,指文本分割后的最小語義單元。使文本被拆解為模型可理解的離散單元.。
詞元具體形式取決于分詞策略,可以是單詞、子詞(Subword)、字符或符號。例如,英文句子“I love NLP”可能被拆分為詞元 [“I”, “love”, “NLP”],而中文句子“我喜歡NLP”可能被拆分為 [“我”, “喜歡”, “NLP”]。不同模型的分詞策略不同, 同一個詞在不同模型中可能被拆分成不同詞元。
前面的 模型的上下文 (如128K) 指的就是詞元數量限制, 此外,如果充值使用 AI 的 API,就會知道收費通常按詞元數計費。 詞元數越多, 計算耗時和內存占用越?,因此在使用時, 應盡量避免冗余詞(如請, 謝謝) ,這樣不僅能省錢,還可以給 AI 減負。
3. Spring AI 快速入門
3.1 環境要求
JDK 要至少 JDK17 或以上版本。
Spring Boot 版本至少 3.2 以上。
IDEA 使用 專業版。
3.2 條件準備
3.2.1 deepseek api 申請
DeepSeek 網址
進入 API 開放平臺,先進行充值(1 塊錢一般就夠用了)。
然后在 API keys 中進行 API 申請。
注意:這里要記住 API keys,因為它只在創建時顯示,后面都不顯示了。如果,手太快點了關閉,沒有記住那就再申請一個。
此外,別人如果知道了你的 API key 也是可以使用的,所有要妥善報存。
3.2.2 阿里云百煉平臺API-KEY 申請
阿里云百煉平臺 API 申請,
按照網頁說明進行申請,新人有 百萬 token 的免費額度
3.3 基于 deepseek 的簡單使用
Spring AI 為OpenAI及兼容的API服務(如DeepSeek)設計了Starter spring-ai-openaispring-boot-starter , 用于快速集成大語?模型能力到 Spring Boot 應用中.
核心價值包括:
? 簡化配置:自動動封裝 OpenAI API 的請求/響應等邏輯
? 統?接口:提供 ChatClient 等標準化接口, 支持無縫切換不同模型提供商
? Spring ?態集成:與 Spring Boot 的?動配置、依賴注?等特性深度整合
3.3.1 項目創建
Spring AI 中
3.3.2 配置文件設置
這里用的 .yml 進行配置,不是 .properties 。
spring:ai:openai:api-key: sk-your-key #你申請的 api keybase-url: https://api.deepseek.comchat:options:model: deepseek-chat #使用的 deepseek 模型temperature: 0.7
#在AI模型的temperature參數中,值的大小直接影響生成文本的特性:
#較低值(如0.1-0.3):輸出更加確定性和保守,傾向于選擇最高概率的詞匯,適合需要準確性和一致性的場景
#中等值(如0.4-0.7):在創造性和可靠性之間取得平衡,會產生適度變化的輸出
#較高值(如0.8-1.0):輸出更加隨機和創造性,適合需要多樣性和新穎性的場景
#您當前設置的0.7屬于中等偏高,會使模型輸出有一定創造性但不會過于隨機
3.3.3 ChatModel 使用
package com.example.saidemo;import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/dp")
public class DPChat {@AutowiredOpenAiChatModel openAiChatModel;@RequestMapping("/chat")public String chat(String message){//call() 調用AIreturn openAiChatModel.call(message);}
}
前面說過,deepseek 的 API 是兼容的,這里使用 OpenAiChatModel
。
使用示例:
3.3.4 ChatClient 使用
ChatModel 屬于底層接口,而 ChatClient 則是高階接口,它基于 ChatModel 進行了再次封裝。ChatClient 使用鏈式調用,可讀性更強。
簡單對話
@RestController
@RequestMapping("/dp")
public class DPChat {private final ChatClient chatClient;public DPChat(ChatClient.Builder chatClientBuilder) {this.chatClient = chatClientBuilder.build();}@RequestMapping("/chat2")public String chat2(String message){return chatClient.prompt().user(message).call().content();}
}
ChatClient
不支持使用用 @Autowired
進行直接注入,需要調用 ChatClient.Builder
下的 build()
才可以。
使用示例:
角色預設
為了方便后續使用,我們將 ChatClient 單獨定義。
package com.example.saidemo;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class ChatClientConfig {@Beanpublic ChatClient chatClient(ChatClient.Builder chatClientBuilder){return chatClientBuilder.build();}
}
這樣后面就可以直接使用 @Autowired
進行注入。
回顧前文,我們知道系統提示詞能夠對模型進行預設,為它指定規范。所以,這里的角色預設就是設置 系統提示詞。
操作如下:
package com.example.saidemo;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class ChatClientConfig {@Beanpublic ChatClient chatClient(ChatClient.Builder chatClientBuilder){return chatClientBuilder.defaultSystem("你叫小明,你是一個不專業的寫信者").build();}
}
使用 defaultSystem()
進行設置。
使用示例:
結構化輸出
就是當你輸入提示詞后, AI 根據你所規定的結構,進行回答。
首先,我們使用JDK16提供的新關鍵詞 record
來定義?個實體類
record Recipe(String dish, List<String> ingredients) {}
通過 entity()
?法將模型輸出轉為自定義實體,(模型會自動匹配并填充)。
package com.example.saidemo;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController
@RequestMapping("/dp")
public class DPChat {@AutowiredChatClient chatClient;record Recipe(String dish, List<String> ingredients) {}@RequestMapping("/chat3")public String chat1(String message){Recipe recipe = chatClient.prompt().user(String.format("請根據用戶的描述,生成一個菜譜。用戶的描述是:%s", message)).call().entity(Recipe.class);return recipe.toString();}
}
使用示例:
流式輸出
運行之前的代碼,你會發現和常用的 AI 工具不同,它是直接將完整結果發送過來,這就導致用戶等待時間過長。想要和使用的 AI 工具的輸出效果相同,就要采用流式輸出,逐步生成內容而非一次性返回完整結果。
package com.example.saidemo;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;import java.util.List;@RestController
@RequestMapping("/dp")
public class DPChat {@AutowiredChatClient chatClient;@RequestMapping("/chat4")public Flux<String> chat1(String message){return chatClient.prompt().user(String.format("請根據用戶的描述,生成一個菜譜。用戶的描述是:%s", message)).stream().content();}
}
直接運行,你會發現輸出結果為亂碼:
這是因為沒有對輸出結果的類型進行設置,對 @RequestMapping("/chat4")
進行如下修改,就能夠正確輸出了。
@RequestMapping(value = "/chat4" ,produces = "text/html;charset=utf-8")
運行結果:
(流式效果不好展示,這里只展示最終效果,感興趣的可以自己試一試)
打印日志
Spring AI 借助Advisors 來實現日志打印的功能.。
Advisors 是基于 AOP思想實現的, 在具體實現上進行了領域適配. 其設計核心借鑒了Spring AOP 的攔截機制, 各個Advisor以鏈式結構運?,序列中的每個Advisor都有機會對傳入的請求和傳出的響應進行處理。 這種鏈式處理機制確保了每個Advisor可以在請求和響應流中添加自己的邏輯, 從而實現更靈活和可定制的功能。如下圖:
主要應用場景:
? 敏感詞過濾
? 建?聊天歷史
? 對話上下?管理
可以直接在Config 中添加,那么后續使用 ChatClient 都會有。
package com.example.saidemo;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class ChatClientConfig {@Beanpublic ChatClient chatClient(ChatClient.Builder chatClientBuilder){return chatClientBuilder.defaultSystem("你叫小明,你是一個不專業的寫信者").defaultAdvisors(new SimpleLoggerAdvisor()).build();}
}
同樣,也可以在單個接口中添加,僅對這一個起作用:
@RestController
@RequestMapping("/dp")
public class DPChat {@AutowiredChatClient chatClient;@RequestMapping(value = "/chat4" ,produces = "text/html;charset=utf-8")public Flux<String> chat1(String message){return chatClient.prompt().user(String.format("請根據用戶的描述,生成一個菜譜。用戶的描述是:%s", message)).advisors(new SimpleLoggerAdvisor()).stream().content();}
}
3.3.5 ChatModel 拓展
Ctrl + 左鍵
進入之前代碼中的 call()
方法,會發現它調用了 參數類型為 Prompt
的 call()
方法。
接下來使用 Prompt
來使用 call()
方法。
簡單使用
@RequestMapping("/chatByPrompt")public String chatByPrompt (String msg) {Prompt prompt = new Prompt(msg);ChatResponse response = chatModel.call(prompt);return response.getResult().getOutput().getText();}
角色預設
@RequestMapping("/role")public String role(String msg) {SystemMessage systemMessage = new SystemMessage("你叫dp,是對話ai機器人");UserMessage userMessage = new UserMessage(msg);Prompt prompt = new Prompt(userMessage,systemMessage);ChatResponse response = chatModel.call(prompt);return response.getResult().getOutput().getText();}
流式響應
@RequestMapping(value = "/stream" ,produces = "text/html;charset=utf-8" )public Flux<String> stream (String msg) {Prompt prompt = new Prompt(msg);Flux<ChatResponse> response = chatModel.stream(prompt);return response.map(x->x.getResult().getOutput().getText());}
.map(x->x.getResult().getOutput() .getText())
方法,用于操作流中元素。
基于代碼可知,chatModel.stream(prompt);
的返回類型為 Flux<ChatResponse>
。你可以理解為 response (流)
是一輛列車,是由很多節組成的連續一長串,這每一節都是 類型為 ChatResponse
的元素。
map
方法則是操作每一節,并返回生成新的 流,且不改變原流。這里 x 就代指 response
中的元素(也可以使用其他字母來代指),x 經過 getResult().getOutput().getText()
就變成了 String 類型,所以新的流就是 Flux<String>
類型。
3.3.6 Client & Model 對比
維度 | ChatModel | ChatClient |
---|---|---|
交互?式 | ?動構建Prompt, 解析響應 | 鏈式API, ?動封裝請求與響應 |
結構化輸出 | ?動解析?本 | ?持 .entity(Class) ?動映射POJO |
擴展能? | 依賴外部組件 | 內置Advisor機制, 提供更?級的功能, 如提供上下?記憶, RAG等功能 |
適合場景 | 適合需要精細控制模型參數的場景, ?如模型實驗, 參數調優等定制需求 | 適合快速構建AI服務, 如帶記憶的客服系統 |
3.4 流式編程 - 資料了解
HTTP 協議本身設計為無狀態的 請求-響應模式,嚴格來說, 是無法做到服務器主動推送消息到客戶端, 但通過Server-Sent Events (服務器發送事件, 簡稱SSE)技術可實現流式傳輸,允許服務器主動向瀏覽器推送數據流。
服務器會告訴客戶端,接下來要發送的是流消息(streaming), 這時客戶端不會關閉連接,會?直等待服務器發送過來新的數據流。
SSE(Server-Sent Events)是?種基于 HTTP 的輕量級實時通信協議,瀏覽器通過內置的
EventSource API接收并處理這些實時事件。
特點:
? 基于 HTTP 協議
復?標準了 HTTP/HTTPS 協議,?需額外端?或協議,兼容性好且易于部署 。
? 單向通信機制
SSE 僅支持服務器向客戶端的單向數據推送,客戶端通過普通 HTTP 請求建?連接后,服務器可持續發送數據流,但客戶端無法通過同?連接向服務器發送數據。
? 自動重連機制
?持斷線重連,連接中斷時,瀏覽器會?動嘗試重新連接(?持 retry 字段指定重連間隔)
? 自定義消息類型
客戶端發起請求后,服務器保持連接開放, 響應頭設置 Content-Type: text/event-stream
,標識為事件流格式, 持續推送事件流。
數據格式
每?次發送的消息,由若干個message組成, 每個message之間由 \n\n 分隔, 每個message內部由若干行組成, 每?行都是如下格式:
[field]: value\n
前端代碼:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>SSE</title>
</head>
<body><div id="sse"></div>
</body>
<script>let eventSource = new EventSource('/sse/data')eventSource.onmessage = function (event) {document.getElementById('sse').innerHTML = event.data;}// eventSource.addEventListener('foo', (event) => {// document.getElementById('sse').innerHTML = event.data;// })// eventSource.addEventListener('end', (event) => {// eventSource.close()// })
</script>
</html>
EventSource('/sse/data')
中填寫的是后端接口。
后端代碼:
package com.example.saidemo;import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;import java.awt.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.Date;@Slf4j
@RequestMapping("/sse")
@RestController
public class SseController {@RequestMapping("/data")public void data(HttpServletResponse response) throws IOException, InterruptedException {log.info("返回響應: data");response.setContentType("text/event-stream;charset=utf-8");PrintWriter writer = response.getWriter();for (int i = 0; i < 10; i++) {String s = "data: " + new Date() + "\n\n";writer.write(s);writer.flush();Thread.sleep(1000L);}}@RequestMapping("/retry")public void retry(HttpServletResponse response) throws IOException, InterruptedException {log.info("返回響應: retry");response.setContentType("text/event-stream;charset=utf-8");PrintWriter writer = response.getWriter();String s = "retry: 2000\n";s += "data: " + new Date() + "\n\n";writer.write(s);writer.flush();}@RequestMapping("/event")public void event(HttpServletResponse response) throws IOException, InterruptedException {log.info("返回響應: event");response.setContentType("text/event-stream;charset=utf-8");PrintWriter writer = response.getWriter();for (int i = 0; i < 10; i++) {String s = "event: foo\n"; //自定義事件,對應前端注釋掉的代碼s += "data: " + new Date() + "\n\n";writer.write(s);writer.flush();Thread.sleep(1000L);}}@RequestMapping("/end")public void end(HttpServletResponse response) throws IOException, InterruptedException {log.info("返回響應: event");response.setContentType("text/event-stream;charset=utf-8");PrintWriter writer = response.getWriter();for (int i = 0; i < 10; i++) {String s = "event: foo\n"; //自定義事件,對應前端注釋掉的代碼s += "data: " + new Date() + "\n\n";writer.write(s);writer.flush();Thread.sleep(1000L);}writer.write("event: end\ndata: EOF\n\n");}@RequestMapping(value = "/stream",produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<String> stream() {return Flux.interval(Duration.ofSeconds(1)) //發送間隔.map(s->new Date().toString()); }
}
自動重連的體現:
運行后,訪問前端頁面,發現數字一直在改變。但實際后端 /sse/data
接口中只定義發送 10 次。這是因為當該接口的完整邏輯執行完后,進行了自動重連。
3.5 基于 Spring AI Alibaba 的簡單使用
3.5.1 項目創建
項目創建時,添加的依賴:
3.5.2 配置文件設置
pom
文件的配置:
<dependencyManagement><dependencies><dependency><groupId>com.alibaba.cloud.ai</groupId><artifactId>spring-ai-alibaba-bom</artifactId><version>1.0.0.2</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement><dependencies><dependency><groupId>com.alibaba.cloud.ai</groupId><artifactId>spring-ai-alibaba-starter-dashscope</artifactId></dependency>
</dependencies>
.yml
文件配置:
spring:ai:dashscope:api-key: #你在百煉平臺申請的 API KEYchat:options:model: qwen-vl-max-latest #模型名稱multi-model: true #是否啟?多模型
3.5.3 ChatModel 使用
由于使用類似,不同的是 這里使用 ChatModel
類,不再使用 OpenAiChatModel
。
所以,不再細說,只進行代碼展示和必要說明。
package com.example.salidemo;import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/ali")
public class AliController {@Autowiredprivate ChatModel chatModel;@RequestMapping("/chat")public String chat(String message){return chatModel.call(message);}
}
使用示例:
3.5.4 ChatClient 使用
直接使用
package com.example.salidemo;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;import java.util.List;@RestController
@RequestMapping("/chat")
public class ChatController {private ChatClient chatClient;public ChatController(ChatClient.Builder chatClientBuilder) {this.chatClient = chatClientBuilder.build();}@RequestMapping("/chat")public String chat(String message){return chatClient.prompt().user(message).call().content();}
}
角色預設
這里同樣將 ChatClient
單獨遷出,并使用 @Bean
注解進行注入。
單獨類:
package com.example.salidemo;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class ChatConfiguration {@Beanpublic ChatClient chatClient(ChatClient.Builder chatClientBuilder){return chatClientBuilder.defaultSystem("你是一個專業的演員,在每次回復前加上{word}").build();}
}
這里的 {word}
就相當于占位符,在實際使用時可進行替換,否則 原樣輸出。像這樣:
@RequestMapping("/word")public String word(String message,String word){return chatClient.prompt().user(message).system(sp->sp.param("word",word)).call().content();}
system(sp->sp.param("word",word))
替換操作。sp 是Lambda表達式的參數名稱,可以自由命名,但必須與后續調用保持一致。
如果有多個參數,使用方法如下:
//鏈式調用param方法:
.system(sp -> sp.param("word1", word1).param("word2", word2).param("word3", word3))//使用Map傳遞多個參數:
Map<String, Object> params = new HashMap<>();
params.put("word1", word1);
params.put("word2", word2);
.system(sp -> sp.params(params))//使用對象封裝參數(推薦):
record WordParams(String word1, String word2) {}
.system(sp -> sp.entity(new WordParams(word1, word2)))
之前的 基于 deepseek 的ChatClient 同樣適用。
結構化輸出
record ActorFilms(String actor, List<String> films){}@RequestMapping("/entify")public String entify(String actor){return chatClient.prompt().user(String.format("幫我生成%S參演的作品",actor)).call().entity(ActorFilms.class).toString();}
流式輸出
@RequestMapping(value = "/stream",produces = "text/html;charset=utf-8")public Flux<String> stream(String message){return this.chatClient.prompt().user(message).stream().content();}
3.5.5 多模態的使用。
多模態性指模型同時理解和處理文本、圖像、音頻及其他數據格式等多源信息的能力。
這里展示 處理圖片的能力。
@RestController
@RequestMapping("/multi")
public class MultiController {private final ChatClient chatClient;public MultiController(ChatClient.Builder chatClientBuilder) {this.chatClient = chatClientBuilder.build();}@RequestMapping("/image")public String image(String prompt) throws URISyntaxException, MalformedURLException {String url = "https://tse4-mm.cn.bing.net/th/id/OIP-C.TCq0ItOzbTzC11dudmqMrgHaLJ?w=195&h=294&c=7&r=0&o=5&dpr=1.5&pid=1.7";//網上隨便找的網圖List<Media> mediaList = List.of(new Media(MimeTypeUtils.IMAGE_JPEG, new URI(url).toURL().toURI()));//用戶提示詞UserMessage userMessage = UserMessage.builder().text(prompt).media(mediaList).build();//調用模型String response = chatClient.prompt(new Prompt(userMessage)).call().content();return response;}
}