基礎篇 | 環境搭建 - 智能天氣預報助手
一、什么是 Spring AI
Spring AI
(https://spring.io/projects/spring-ai)]是 Spring 官方于 2023 年推出的 AI 應用開發框架,它如同 AI 世界的"Spring 生態連接器",致力于簡化開發集成了 AI 功能的應用程序。它主要解決兩大核心問題:
- 統一接口:消除不同 AI 服務(如 OpenAI、智譜 AI、DeepSeek 等)的 API 差異,允許用戶靈活地在多個模型之間切換。
- 生態整合:將 AI 能力無縫融入 Spring 技術棧(如 Spring Boot、Spring MVC)。
如上圖所示,Spring AI 成為了連接企業數據以及 API 和生成式 AI 的橋梁。
?? 核心功能矩陣
Spring AI 具備的能力如下:
它具備以下特點:
- 即插即用:通過更換
application.yml
配置即可切換大模型供應商。 - 響應式支持:原生集成
Reactor Streams
,支持背壓控制的流式響應。 - 擴展性強:自定義
ChatClient
實現可接入任意 AI 服務。
??Spring AI 能做什么?
那么 Spring AI 到底能做什么呢?這里我給出幾個常見案例,在咱們這套課程中也會帶著大家完成這些案例:
案例 1:智能客服系統
-
場景:集成自然語言處理的客服機器人,處理用戶咨詢、訂單查詢和售后問題。
-
AI 價值:
- 減少人工客服成本,24/7 響應客戶需求。
- 通過意圖識別提升問題解決效率(準確率可達 85%+)。
-
Spring AI 實現:
使用 Spring AI 的對話模型接口(如 OpenAI 或 智譜 AI),快速構建企業級對話流。
案例 2:智能數據分析平臺
-
場景:自動分析企業銷售數據、用戶行為日志,生成可視化報告和預測建議。
-
AI 價值:
- 通過時間序列預測優化庫存管理(降低 20%-30% 滯銷風險)。
- 實時異常檢測(如金融反欺詐)。
-
Spring AI 實現:
結合 Spring AI 和 MCP 組件,對數據庫進行訪問,實現數據預處理與預測 API。
案例 3:自動化文檔處理
-
場景:合同、發票的自動分類、關鍵詞提取和合規性審查。
-
AI 價值:
- 節省 90% 人工文檔處理時間。
- 通過 OCR 實現非結構化數據標準化。
-
Spring AI 實現:
調用 Spring AI 的多模態接口,實現文檔解析流水線。
案例 4:智能營銷內容生成
-
場景:自動化生成廣告文案、社交媒體推文、郵件營銷內容,支持多語言適配。
-
AI 價值:
- 縮短 70% 的創意內容生產周期。
- 通過 A/B 測試數據反饋優化生成策略(點擊率提升 15%-25%)。
-
Spring AI 實現:
調用 大模型接口,結合企業品牌風格指南定制生成規則。
案例 5:語音客服系統
-
場景:通過語音交互處理用戶來電(如銀行催收、快遞查詢、政務熱線),支持多語言、方言識別和情感分析。
-
AI 價值:
- 成本降低:替代 60% 以上重復性語音服務(如賬單查詢)。
- 效率提升:語音響應速度 <1 秒(傳統 IVR 需 5-10 秒菜單導航)。
-
Spring AI 實現:
- 語音識別
- 集成 Whisper 模型,將用戶語音轉為文本。
- 支持實時流式傳輸(降低延遲)。
- 自然語言處理(NLP)
- 使用 Spring AI 的對話模型解析用戶意圖,生成響應文本。
- 情感分析:識別用戶情緒(憤怒/焦慮),觸發人工坐席接管。
- 語音合成(TTS)
- 調用 AI 模型生成擬人化語音反饋。
- 語音識別
?? 學習資源指引
以下是 Spring AI 相關的學習資源:
同學們,了解了以上內容,我們就可以開始準備學習 Spring AI 啦,先別急著埋頭苦干,下面這幾個重點得拿小本本記好咯!
其一,Spring AI 不是大模型的 “替身” !它自己沒有那種直接生成智能內容的超能力,但別小瞧它,它可是個超厲害的 “連接大師”,專門負責牽線搭橋,把咱們手頭現有的各種牛哄哄的模型給串起來,讓它們一起為咱的項目服務,超給力!
其二,Spring AI 特別 “隨性”,絕不綁死在一家廠商身上。現在市面上云供應商那么多,它倒好,直接搞出個統一 API,就像一把萬能鑰匙,不管哪家云供應商的門都能開。咱用它的時候,完全可以根據心情、項目需求,自由切換不同廠商的資源,根本不用擔心被 “套牢”。比如最近超火的 DeepSeek,符合 OpenAI 接口規范的話我們可以實現秒級接入,如果不符合我們自己編寫一個接入模塊,也能實現快速接入。
最后,學習 Spring AI 有個小門檻,得有點 Java 和 Spring 基礎。雖說它已經幫咱們把 AI 集成的路給鋪平了不少,讓整個過程簡單了許多,但基礎不牢,地動山搖!Spring 的那些核心概念,咱們還是得穩穩拿捏住,這樣才能在后續玩 Spring AI 的時候,一路開掛,輕松應對各種難題,做出超炫的成果!
二、快速入門
效果展示
先來看一下本章要完成的案例:智能天氣預報助手。該助手借助 Spring AI 的特性結合人工智能技術,為用戶提供準確、便捷且個性化的天氣信息服務,可廣泛應用于日常生活提醒、出行規劃、農業生產參考等多個場景。
環境搭建
我們一起來體驗一下 Spring AI 的魅力,首先來創建項目:
使用 IDEA 創建 spring 項目,注意官方要求版本號為 3.2.x 和 3.3.x。本教程中我們使用 maven 來作為項目管理工具。
勾選 spring web
和 spring reactive web
選項,分別支持 mvc模式
和 webflux模式
訪問:
創建完項目之后,我們來添加依賴。首先添加 snapshot 需要的依賴庫,如果已經發布正式版則不需要此步驟:
<repositories><repository><id>spring-milestones</id><name>Spring Milestones</name><url>https://repo.spring.io/milestone</url><snapshots><enabled>false</enabled></snapshots></repository><repository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><releases><enabled>false</enabled></releases></repository>
</repositories>
接下來添加 spring-ai 所有的 bom,用來鎖定依賴版本,目前可選版本有 1.0.0-SNAPSHOT
和 1.0.0-M6
。1.0.0-M6
中包含一些未正式發布的特性,這里我們先使用 spring-ai-bom
:
<dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>1.0.0-SNAPSHOT</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
最后添加對應大模型的依賴,Spring AI 支持的大模型有很多,在官網上有詳細的對比,有興趣的同學們可以詳細去看下。今天我們選擇的是國產的智譜大模型,不需要科學上網就可以使用。首先去智譜的官網進行注冊,然后申請一個 API KEY。將這個 Key 復制一下,一會兒需要配置到項目中。
接下來我們引入對應的依賴:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
</dependency>
我們將剛才智譜 AI 的 API KEY 配置到配置文件 srcmain esources
中,默認的配置文件為 properties 文件,可讀性和可配置性都不加,將它刪除,重新創建一個 application.yml
文件。將如下內容配置到文件中:
spring:ai:zhipuai:api-key: ${ZHIPU_API_KEY}
這里千萬不要把 API KEY 直接寫入到配置文件中,如果后續提交到 github 等 git 倉庫中,API KEY 相當于就暴露出去了,非常不安全。所以這里我們使用占位符,在啟動命令的環境變量中去配置內容:
如果沒有這選項,就先運行一下 SpringBoot Application。點擊修改選項:
選擇環境變量:
填入如下內容:
ZHIPU_API_KEY=你的API KEY
點擊保存。
后端代碼編寫
好了,前置工作都已經完成了,接下來我們來編寫后端代碼。創建 WeatherController
用來處理前端發送的請求:
@RestController
@RequestMapping("/weather")
public class WeatherController {// 注入智譜AI聊天模型private final ZhiPuAiChatModel chatModel;// 系統提示詞,定義機器人的角色和行為private static final String _SYSTEM_PROMPT _= """你是一個專業的天氣預報機器人,擅長:1. 解答天氣相關的問題2. 提供天氣預報建議3. 解釋天氣現象4. 提供合適的穿衣建議5. 分析天氣對出行的影響請始終以專業、友好的口吻回答問題。如果問題與天氣無關,請禮貌地提醒用戶你是一個天氣預報助手。""";public WeatherController(ZhiPuAiChatModel chatModel) {this.chatModel = chatModel;}_/**_
_ * 生成單次天氣相關回復_
_ *_
_ * @param message 用戶輸入的消息_
_ * @return 包含AI回復的Map_
_ */_
_ _@GetMapping("/generate")public String generate(@RequestParam(value = "message") String message) {// 構建提示詞,加入系統角色定義String prompt = _SYSTEM_PROMPT _+ "
用戶問題:" + message;return this.chatModel.call(prompt);}}
我們來解讀一下代碼:
-
類定義與注解
@RestController
public class WeatherController {
@RestController
:- Spring MVC 注解,表示該類是一個 RESTful 控制器,所有方法默認返回 JSON/XML 數據而非視圖頁面。
- 等價于
@Controller
+@ResponseBody
的組合。
-
依賴注入
private final ZhiPuAiChatModel chatModel;
public WeatherController(ZhiPuAiChatModel chatModel) {this.chatModel = chatModel;}
- 構造函數注入:Spring 推薦的方式,保證依賴不可變(
final
修飾符),避免空指針異常。 ZhiPuAiChatModel
:Spring AI 的組件,封裝了與智譜 AI 模型的交互邏輯(如 API 調用、參數處理)。
-
系統提示詞定義
private static final String SYSTEM_PROMPT = “”"
你是一個專業的天氣預報機器人,擅長:
1. 解答天氣相關的問題
2. 提供天氣預報建議
// … 其他提示 …
“”";
- 關鍵作用:
- 角色定義:明確 AI 的領域邊界(只處理天氣問題)。
- 安全控制:當用戶提問非天氣問題時,觸發禮貌拒絕邏輯。
- 風格控制:確保回復的專業性和友好性。
-
接口實現
return this.chatModel.call(prompt);
-
chatModel.call()
內部機制:- 認證:自動添加智譜 API 密鑰(通常通過
ZhiPuAiChatModel
配置類設置)。 - HTTP 調用:向智譜 API 端點(如
https://api.zhipu.ai/v4/chat/completions
)發送 POST 請求。 - 參數封裝:將
prompt
包裝為模型所需的 JSON 格式,例如:
{“model”: “glm-4”,“messages”: [{“role”: “user”, “content”: “北京今天天氣…”}],“temperature”: 0.7}
- 響應解析:提取智譜 API 返回結果中的
content
字段。
- 認證:自動添加智譜 API 密鑰(通常通過
整體流程示意圖
]
后端代碼測試
運行spring boot服務之后,調用接口進行測試:
這個時候我們會遇到一個錯誤:
{"timestamp":"2025-02-05T07:25:47.090+00:00","status":500,"error":"Internal Server Error","path":"/ai/generate"}
這說明我們已經將請求發送給了智譜大模型,但是由于 Spring AI 配置的默認大模型是收費的,同時 api-key 綁定的賬戶沒有充值,所以無法成功調用。我們可以選擇使用免費的模型進行測試:
修改一下配置
spring:ai:zhipuai:api-key: ${ZHIPU_API_KEY}chat:options:model: GLM-4-Flash
使用 GLM-4-Flash 這個免費模型,再次進行測試:
成功返回了結果,恭喜你完成了后端代碼的編寫,是不是非常簡單?當然這里如果我們使用的是免費模型,可能數據方面會有一定的準確性問題,后續我們可以使用 Tool Functions 進行優化,也可以替換為付費模型。
前端代碼編寫
前端代碼我這里給出一個案例,同學們可以直接拿去使用:
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>智能天氣預報助手</title><style>_/* 全局樣式 */_* {margin: 0;padding: 0;box-sizing: border-box;}body {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;line-height: 1.6;color: #333;background: linear-gradient(120deg, #89f7fe 0%, #66a6ff 100%);}_/* 聊天容器 */_.chat-container {max-width: 800px;margin: 20px auto;padding: 20px;height: calc(100vh - 40px);display: flex;flex-direction: column;background-color: rgba(255, 255, 255, 0.95);border-radius: 12px;box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);}_/* 頭部標題 */_.chat-header {text-align: center;padding: 20px 0;margin-bottom: 20px;border-bottom: 1px solid #eee;position: relative;}.chat-header h1 {color: #1a73e8;font-size: 24px;margin-bottom: 10px;}.chat-header p {color: #666;font-size: 14px;}_/* 切換按鈕 */_.switch-mode {position: absolute;right: 20px;top: 20px;padding: 8px 16px;background-color: #1a73e8;color: white;border: none;border-radius: 20px;cursor: pointer;font-size: 14px;transition: all 0.3s;}.switch-mode:hover {background-color: #1557b0;transform: translateY(-2px);}_/* 消息區域 */_.messages-container {flex: 1;overflow-y: auto;margin-bottom: 20px;padding: 20px;background-color: rgba(255, 255, 255, 0.8);border-radius: 8px;}_/* 消息樣式 */_.message {margin-bottom: 20px;padding: 15px;border-radius: 8px;max-width: 80%;}.user-message {background-color: #e3f2fd;margin-left: auto;color: #1565c0;}.assistant-message {background-color: #f5f5f5;margin-right: auto;color: #333;}_/* 輸入區域 */_.input-container {position: relative;padding: 20px;background-color: rgba(255, 255, 255, 0.9);border-radius: 8px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);}#message-input {width: 100%;padding: 12px;border: 2px solid #e0e0e0;border-radius: 8px;resize: none;height: 50px;font-size: 16px;transition: border-color 0.3s;}#message-input:focus {border-color: #1a73e8;outline: none;}#send-button {position: absolute;right: 30px;bottom: 30px;padding: 8px 20px;background-color: #1a73e8;color: white;border: none;border-radius: 20px;cursor: pointer;transition: all 0.3s;}#send-button:hover {background-color: #1557b0;transform: translateY(-2px);}#send-button:disabled {background-color: #cccccc;cursor: not-allowed;transform: none;}_/* 示例問題區域 */_.example-questions {margin-top: 10px;padding: 10px;display: flex;flex-wrap: wrap;gap: 10px;}.example-question {background-color: #e3f2fd;color: #1565c0;padding: 8px 16px;border-radius: 16px;font-size: 14px;cursor: pointer;transition: all 0.3s;}.example-question:hover {background-color: #1a73e8;color: white;}_/* 打字動畫 */_.typing {display: inline-block;margin-left: 4px;}.typing span {display: inline-block;width: 6px;height: 6px;background-color: #666;border-radius: 50%;margin: 0 2px;animation: typing 1s infinite;}.typing span:nth-child(2) {animation-delay: 0.2s;}.typing span:nth-child(3) {animation-delay: 0.4s;}@keyframes _typing_ {0%,100% {transform: translateY(0);}50% {transform: translateY(-4px);}}</style>
</head><body><div class="chat-container"><div class="chat-header"><h1>??? 智能天氣預報助手</h1><p>我可以為您提供天氣預報、穿衣建議和出行建議</p><button class="switch-mode" onclick="window.location.href='stream.html'">切換到流式版</button></div><div class="messages-container" id="messages">_<!-- 歡迎消息 -->_<div class="message assistant-message">您好!我是您的智能天氣預報助手。您可以詢問我任何關于天氣的問題,比如:</div></div><div class="example-questions"><div class="example-question" onclick="askExample(this)">北京今天天氣怎么樣?</div><div class="example-question" onclick="askExample(this)">今天適合戶外運動嗎?</div><div class="example-question" onclick="askExample(this)">明天要出門,需要帶傘嗎?</div><div class="example-question" onclick="askExample(this)">最近三天的天氣預報</div></div><div class="input-container"><textarea id="message-input" placeholder="請輸入您的天氣相關問題..." rows="1"onkeydown="if(event.keyCode === 13 && !event.shiftKey) { event.preventDefault(); sendMessage(); }"></textarea><button id="send-button" onclick="sendMessage()">發送</button></div></div><script>_// DOM 元素_const messagesContainer = document.getElementById('messages');const messageInput = document.getElementById('message-input');const sendButton = document.getElementById('send-button');_// 示例問題點擊處理_function **askExample**(_element_) {messageInput.value = _element_.textContent;sendMessage();}_// 工具函數:創建消息元素_function **createMessageElement**(_content_, _isUser_) {const messageDiv = document.createElement('div');messageDiv.className = `message ${_isUser_ ? 'user-message' : 'assistant-message'}`;messageDiv.textContent = _content_;return messageDiv;}_// 創建打字動畫元素_function **createTypingIndicator**() {const typingDiv = document.createElement('div');typingDiv.className = 'message assistant-message';typingDiv.innerHTML = '正在查詢天氣信息<div class="typing"><span></span><span></span><span></span></div>';return typingDiv;}_// 發送消息_async function **sendMessage**() {const message = messageInput.value.trim();if (!message) return;_// 禁用輸入和發送按鈕_messageInput.disabled = true;sendButton.disabled = true;_// 顯示用戶消息_messagesContainer.appendChild(createMessageElement(message, true));messageInput.value = '';_// 顯示打字動畫_const typingIndicator = createTypingIndicator();messagesContainer.appendChild(typingIndicator);messagesContainer.scrollTop = messagesContainer.scrollHeight;try {_// 修改API調用地址_const response = await fetch(`/weather/generate?message=${encodeURIComponent(message)}`);const data = await response.text();_// 移除打字動畫_typingIndicator.remove();_// 創建并添加助手消息_const assistantMessage = createMessageElement(data, false);messagesContainer.appendChild(assistantMessage);messagesContainer.scrollTop = messagesContainer.scrollHeight;} catch (error) {console.error('API調用錯誤:', error);const errorMessage = document.createElement('div');errorMessage.className = 'message assistant-message';errorMessage.textContent = '抱歉,發生了一些錯誤,請稍后重試。';messagesContainer.appendChild(errorMessage);} finally {_// 重新啟用輸入和發送按鈕_messageInput.disabled = false;sendButton.disabled = false;messageInput.focus();}}_// 頁面加載完成后聚焦到輸入框_window.onload = () => {messageInput.focus();};</script>
</body></html>
這里邊最關鍵的代碼如下,我們逐行添加注釋讓大家更容易理解:
/*** 核心消息發送處理函數* 實現了以下核心功能:* 1. 用戶消息的發送和顯示* 2. 輸入控件的狀態管理* 3. 加載動畫的顯示和隱藏* 4. 與后端API的通信* 5. 響應消息的展示* 6. 錯誤處理機制*/
async function sendMessage() {// 獲取并清理用戶輸入const message = messageInput.value.trim();if (!message) return; // 空消息直接返回// 【第一步:UI狀態管理】// 禁用輸入控件,防止重復發送messageInput.disabled = true;sendButton.disabled = true;// 【第二步:顯示用戶消息】// 創建并添加用戶消息到消息容器messagesContainer.appendChild(createMessageElement(message, true));messageInput.value = ''; // 清空輸入框// 【第三步:顯示加載狀態】// 添加打字動畫,提供視覺反饋const typingIndicator = createTypingIndicator();messagesContainer.appendChild(typingIndicator);// 自動滾動到底部messagesContainer.scrollTop = messagesContainer.scrollHeight;try {// 【第四步:API通信】// 調用后端API獲取天氣響應const response = await fetch(`/weather/generate?message=${encodeURIComponent(message)}`);const data = await response.text();// 【第五步:更新UI】// 移除加載動畫typingIndicator.remove();// 顯示AI助手的響應消息const assistantMessage = createMessageElement(data, false);messagesContainer.appendChild(assistantMessage);// 確保新消息可見messagesContainer.scrollTop = messagesContainer.scrollHeight;} catch (error) {// 【第六步:錯誤處理】console.error('API調用錯誤:', error);// 顯示友好的錯誤提示const errorMessage = document.createElement('div');errorMessage.className = 'message assistant-message';errorMessage.textContent = '抱歉,發生了一些錯誤,請稍后重試。';messagesContainer.appendChild(errorMessage);} finally {// 【第七步:狀態恢復】// 重新啟用輸入控件messageInput.disabled = false;sendButton.disabled = false;messageInput.focus(); // 將焦點返回到輸入框}
}/*** 輔助函數:創建消息元素* @param {string} content - 消息內容* @param {boolean} isUser - 是否為用戶消息* @returns {HTMLElement} 返回格式化的消息DOM元素*/
function createMessageElement(content, isUser) {const messageDiv = document.createElement('div');// 根據消息類型設置不同的樣式messageDiv.className = `message ${isUser ? 'user-message' : 'assistant-message'}`;messageDiv.textContent = content;return messageDiv;
}/*** 輔助函數:創建加載動畫* @returns {HTMLElement} 返回包含加載動畫的DOM元素*/
function createTypingIndicator() {const typingDiv = document.createElement('div');typingDiv.className = 'message assistant-message';// 使用三個點實現打字動畫效果typingDiv.innerHTML = '正在查詢天氣信息<div class="typing"><span></span><span></span><span></span></div>';return typingDiv;
}
最后我們訪問一下頁面地址:
http://localhost:8080/weather.html
同學們會發現一個問題,這個頁面中 AI 返回的結果,好像沒有一開始打字機的效果啊?其實這里是因為我們使用的是 MVC
機制,后端會將數據完整的返回到前端,接下來我們就改在已有代碼,使用 WebFlux
方式將數據以流式響應返回,實現打字機的效果。
三、打字機效果實現
使用傳統 Spring MVC 同步阻塞式的處理方式,只能在后端處理完請求之后返回整個響應,而使用 Spring WebFlux
這種異步非阻塞數據處理方式,可以將數據逐步返回前端避免前端處于長期等待的狀態。我們來看下他們在瀏覽器上的區別。
傳統 Spring MVC:
可以看到數據是整個返回的,瀏覽器只能等到數據返回之后,才能顯示對應的數據。
Spring WebFlux:
圖片過大,無法展示抱歉
這里通過 HTTP EventSource(也稱為 Server-Sent Events,SSE)技術實現了逐步的數據返回。這是一種在 Web 應用中實現服務器向客戶端推送實時數據的技術。它基于 HTTP 協議,允許服務器將更新發送到客戶端,而無需客戶端頻繁地輪詢服務器來獲取新數據。
協議特征
- MIME 類型:
text/event-stream
- 消息格式:
data: 消息內容
(注意雙換行符結尾) - 斷線處理:客戶端自動嘗試重新連接(默認 3 秒重試間隔)
為什么要使用 Spring Webflux 呢?這是因為傳統 Spring MVC 架構在處理流式響應時存在以下局限:
- 同步阻塞模型導致資源利用率低,每個請求會獨占一個線程直至響應完成(通常需要 3-5 秒),當并發量達到 Tomcat 默認的 200 線程上限時,后續請求將進入等待隊列,而此時 CPU 利用率往往不足 30%。
- 難以實現真正的實時數據推送,傳統 MVC 基于 HTTP1.1 的請求-響應模式,要實現類似 ChatGPT 的字詞逐句返回,只能通過定時輪詢(Polling)或長輪詢(Long-Polling)等非實時方案,造成至少 300-500ms 的延遲。
- 高并發場景下性能瓶頸明顯,在 10,000 QPS 的高并發場景下,同步模型會產生超過 16GB 的線程堆內存開銷,而 WebFlux 的響應式模型僅需 4 個 EventLoop 線程即可處理相同負載,內存消耗降低 80%。
Spring WebFlux 核心概念
- 與傳統 Spring MVC 對比
- 非阻塞 I/O:使用少量線程處理高并發請求(Tomcat 默認約 200 線程 vs Netty 約 2-4 個 EventLoop 線程)
- 響應式 Endpoints:支持返回 Mono/Flux 類型,自動處理響應式流式輸出
- 響應式編程,基于 Reactive Streams 規范
- 異步非阻塞數據處理:無需等待單個操作完成即可處理其他任務(對比傳統同步阻塞式處理)
- 背壓(Backpressure):消費者控制數據流速的機制,防止生產者發送數據過快導致內存溢出
- 事件驅動:通過事件回調(而非輪詢)實現數據流處理,適合高并發場景
-
Reactor 核心類型
Mono // 表示包含0或1個元素的異步序列(例如單個HTTP請求響應)
Flux // 表示包含0到N個元素的異步序列(例如實時數據流、SSE推送)
后端具體寫法:
@GetMapping(value = "/typing", produces = MediaType._TEXT_EVENT_STREAM_VALUE_)
public Flux<String> typingEffect(@RequestParam String content) {return Flux._fromStream_(Arrays._stream_(content.split(""))) // 將字符串拆分為字符流.delayElements(Duration._ofMillis_(50)) // 每個元素延遲50ms(控制打字速度).scan(new StringBuilder(), StringBuilder::append) // 累積字符.map(StringBuilder::toString); // 轉換為字符串序列
}
produces = MediaType.TEXT_EVENT_STREAM_VALUE
表示該方法返回的響應內容類型是 text/event-stream
,這是一種服務器端事件流(Server-Sent Events, SSE)的格式,允許服務器持續向客戶端發送更新,非常適合實現實時數據的推送,在這個場景中用于模擬逐字顯示的打字效果。
前端核心代碼:
_// 建立新的 SSE 連接_eventSource = new EventSource(`/weather/typing?content=${encodeURIComponent(message)}`);_// 處理消息事件_eventSource.onmessage = function (_event_) {//添加數據responseText += _event_.data;};_// 處理錯誤_eventSource.onerror = function (_error_) {console.error('SSE錯誤:', _error_);//關閉連接eventSource.close();};
實現打字機效果
學會 Spring Flux
基礎用法之后,在這個案例中實現打字機效果就不難了,我們增加一個接口:
_/**_
_ * 生成流式天氣相關回復_
_ *_
_ * @param message 用戶輸入的消息_
_ * @return AI回復的流式響應_
_ */_
@GetMapping(value = "/generateStream",produces = MediaType._TEXT_EVENT_STREAM_VALUE_)
public Flux<String> generateStream(@RequestParam(value = "message") String message) {// 構建提示詞,加入系統角色定義String promptText = _SYSTEM_PROMPT _+ "
用戶問題:" + message;var prompt = new Prompt(new UserMessage(promptText));Flux<ChatResponse> stream = this.chatModel.stream(prompt);return stream.map(e -> e.getResult().getOutput().getText());
}
然后修改下前端頁面:
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>智能天氣預報助手 - 流式響應版</title><style>_/* 全局樣式 */_* {margin: 0;padding: 0;box-sizing: border-box;}body {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;line-height: 1.6;color: #333;background: linear-gradient(120deg, #89f7fe 0%, #66a6ff 100%);}_/* 聊天容器 */_.chat-container {max-width: 800px;margin: 20px auto;padding: 20px;height: calc(100vh - 40px);display: flex;flex-direction: column;background-color: rgba(255, 255, 255, 0.95);border-radius: 12px;box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);}_/* 頭部標題 */_.chat-header {text-align: center;padding: 20px 0;margin-bottom: 20px;border-bottom: 1px solid #eee;position: relative;}.chat-header h1 {color: #1a73e8;font-size: 24px;margin-bottom: 10px;}.chat-header p {color: #666;font-size: 14px;}_/* 切換按鈕 */_.switch-mode {position: absolute;right: 20px;top: 20px;padding: 8px 16px;background-color: #1a73e8;color: white;border: none;border-radius: 20px;cursor: pointer;font-size: 14px;transition: all 0.3s;}.switch-mode:hover {background-color: #1557b0;transform: translateY(-2px);}_/* 消息區域 */_.messages-container {flex: 1;overflow-y: auto;margin-bottom: 20px;padding: 20px;background-color: rgba(255, 255, 255, 0.8);border-radius: 8px;}_/* 消息樣式 */_.message {margin-bottom: 20px;padding: 15px;border-radius: 8px;max-width: 80%;white-space: pre-wrap;word-wrap: break-word;}.user-message {background-color: #e3f2fd;margin-left: auto;color: #1565c0;}.assistant-message {background-color: #f5f5f5;margin-right: auto;color: #333;}_/* 輸入區域 */_.input-container {position: relative;padding: 20px;background-color: rgba(255, 255, 255, 0.9);border-radius: 8px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);}#message-input {width: 100%;padding: 12px;border: 2px solid #e0e0e0;border-radius: 8px;resize: none;height: 50px;font-size: 16px;transition: border-color 0.3s;}#message-input:focus {border-color: #1a73e8;outline: none;}#send-button {position: absolute;right: 30px;bottom: 30px;padding: 8px 20px;background-color: #1a73e8;color: white;border: none;border-radius: 20px;cursor: pointer;transition: all 0.3s;}#send-button:hover {background-color: #1557b0;transform: translateY(-2px);}#send-button:disabled {background-color: #cccccc;cursor: not-allowed;transform: none;}_/* 示例問題區域 */_.example-questions {margin-top: 10px;padding: 10px;display: flex;flex-wrap: wrap;gap: 10px;}.example-question {background-color: #e3f2fd;color: #1565c0;padding: 8px 16px;border-radius: 16px;font-size: 14px;cursor: pointer;transition: all 0.3s;}.example-question:hover {background-color: #1a73e8;color: white;}_/* 打字動畫 */_.typing {display: inline-block;margin-left: 4px;}.typing span {display: inline-block;width: 6px;height: 6px;background-color: #666;border-radius: 50%;margin: 0 2px;animation: typing 1s infinite;}.typing span:nth-child(2) {animation-delay: 0.2s;}.typing span:nth-child(3) {animation-delay: 0.4s;}@keyframes _typing_ {0%,100% {transform: translateY(0);}50% {transform: translateY(-4px);}}</style>
</head><body><div class="chat-container"><div class="chat-header"><h1>??? 智能天氣預報助手</h1><p>我可以為您提供天氣預報、穿衣建議和出行建議</p><button class="switch-mode" onclick="window.location.href='weather.html'">切換到普通版</button></div><div class="messages-container" id="messages">_<!-- 歡迎消息 -->_<div class="message assistant-message">您好!我是您的智能天氣預報助手(流式響應版)。您可以詢問我任何關于天氣的問題,比如:</div></div><div class="example-questions"><div class="example-question" onclick="askExample(this)">北京今天天氣怎么樣?</div><div class="example-question" onclick="askExample(this)">今天適合戶外運動嗎?</div><div class="example-question" onclick="askExample(this)">明天要出門,需要帶傘嗎?</div><div class="example-question" onclick="askExample(this)">最近三天的天氣預報</div></div><div class="input-container"><textarea id="message-input" placeholder="請輸入您的天氣相關問題..." rows="1"onkeydown="if(event.keyCode === 13 && !event.shiftKey) { event.preventDefault(); sendMessage(); }"></textarea><button id="send-button" onclick="sendMessage()">發送</button></div></div><script>_// DOM 元素_const messagesContainer = document.getElementById('messages');const messageInput = document.getElementById('message-input');const sendButton = document.getElementById('send-button');_// 示例問題點擊處理_function **askExample**(_element_) {messageInput.value = _element_.textContent;sendMessage();}_// 工具函數:創建消息元素_function **createMessageElement**(_content_, _isUser_) {const messageDiv = document.createElement('div');messageDiv.className = `message ${_isUser_ ? 'user-message' : 'assistant-message'}`;messageDiv.textContent = _content_;return messageDiv;}_// 創建打字動畫元素_function **createTypingIndicator**() {const typingDiv = document.createElement('div');typingDiv.className = 'message assistant-message';typingDiv.innerHTML = '正在查詢天氣信息<div class="typing"><span></span><span></span><span></span></div>';return typingDiv;}_// 發送消息_async function **sendMessage**() {const message = messageInput.value.trim();if (!message) return;_// 禁用輸入和發送按鈕_messageInput.disabled = true;sendButton.disabled = true;_// 顯示用戶消息_messagesContainer.appendChild(createMessageElement(message, true));messageInput.value = '';_// 顯示打字動畫_const typingIndicator = createTypingIndicator();messagesContainer.appendChild(typingIndicator);messagesContainer.scrollTop = messagesContainer.scrollHeight;try {_// 創建新的助手消息容器_const assistantMessage = document.createElement('div');assistantMessage.className = 'message assistant-message';_// 創建 EventSource_const eventSource = new EventSource(`/weather/generateStream?message=${encodeURIComponent(message)}`);_// 移除打字動畫并添加消息容器_typingIndicator.remove();messagesContainer.appendChild(assistantMessage);_// 處理消息事件_eventSource.onmessage = function (_event_) {assistantMessage.textContent += _event_.data;messagesContainer.scrollTop = messagesContainer.scrollHeight;};_// 處理錯誤_eventSource.onerror = function (_error_) {console.error('EventSource錯誤:', _error_);eventSource.close();if (!assistantMessage.textContent) {assistantMessage.textContent = '抱歉,發生了一些錯誤,請稍后重試。';}_// 重新啟用輸入和發送按鈕_messageInput.disabled = false;sendButton.disabled = false;messageInput.focus();};_// 處理完成_eventSource.addEventListener('complete', function (_event_) {eventSource.close();messageInput.disabled = false;sendButton.disabled = false;messageInput.focus();});} catch (error) {console.error('API調用錯誤:', error);const errorMessage = document.createElement('div');errorMessage.className = 'message assistant-message';errorMessage.textContent = '抱歉,發生了一些錯誤,請稍后重試。';messagesContainer.appendChild(errorMessage);_// 重新啟用輸入和發送按鈕_messageInput.disabled = false;sendButton.disabled = false;messageInput.focus();}}_// 頁面加載完成后聚焦到輸入框_window.onload = () => {messageInput.focus();};</script>
</body></html>
可以看到核心代碼中,使用了 EventSource
將每次收到的數據進行拼接,展示到頁面上。
四、常用參數說明
Spring AI
針對 智譜AI
提供了許多實用的參數對大模型的使用進行靈活的配置,所有的參數列表可以通過
官方網站獲取。這里我們列舉幾個較為常用的配置:
屬性
描述
默認值
spring.ai.zhipuai.chat.enabled
是否啟用智譜 AI 聊天模型
true
spring.ai.zhipuai.chat.base-url
如果智譜的base url(接口的基礎地址)變了,就需要調整這個參數
[open.bigmodel.cn/api/paas](https://open.bigmodel.cn/api/paas)
spring.ai.zhipuai.chat.api-key
智譜AI提供的api key
spring.ai.zhipuai.chat.options.model
智譜AI聊天模型
GLM-3-Turbo
spring.ai.zhipuai.chat.options.maxTokens
在整個聊天完成過程中token的最大數量。在與大模型聊天過程中,通過此參數可以限制輸入和大模型返回的token總長度。
無
spring.ai.zhipuai.chat.options.temperature
temperature溫度,是大模型中一個很重要的概念:
溫度低 = “嚴謹模式” 溫度高 = “創意模式”
0.7
spring.ai.zhipuai.chat.options.topP
**topP 就像給模型的"詞庫篩選器"**,控制它輸出內容時優先選擇哪些高頻詞
1
溫度和 topP 到底有什么區別?
舉例子來對比一下溫度和 topP。
溫度:
當溫度=0.5 時,AI 寫詩只會用經典押韻格式;
當溫度=2.0 時,AI 可能把"月亮"寫成"會飛的咸蛋黃"
這個參數本質是在"穩定性"和"創造性"之間找平衡,溫度越低越像教科書,溫度越高越像科幻小說。
topP:
想象你在點外賣,模型要推薦菜品:
此時就算溫度很高,只要 topP 壓得低(比如 0.9),模型實際只能從"前三熱門"里選,不會跳出廚房小白的常規選項。
差異對比
為什么同時用這兩個參數?
就像開車時既要控制油門(溫度)又要控制方向盤(topP):
實際應用場景
- 客服機器人:
- 設置
topP=0.9
+溫度=0.8
- 確保回答專業(不跳出預設話術),同時帶點自然感
- 設置
- AI 寫小說:
- 設置
topP=0.95
+溫度=1.2
- 允許較多創意組合,但剔除完全不符合邏輯的段落
- 設置
總結:溫度決定"敢不敢想",topP 決定"能不能選"。兩者配合使用,才能在穩定性和創造性間找到平衡點。
參考寫法:
spring:ai:zhipuai:chat:enabled: true # 是否啟用智譜AI聊天模型base-url: open.bigmodel.cn/api/paas # 接口基礎地址(按需修改)api-key: your_api_key_here # ?? 必須替換為你自己的API密鑰options:model: GLM-3-Turbo # 模型名稱(固定使用智譜指定模型)maxTokens: 2048 # ?? 建議顯式設置最大token數(例如2048)temperature: 0.7 # 溫度值(嚴謹模式)topP: 1 # topP值(高頻詞篩選閾值)
五、總結
通過簡潔的流式 API 無縫對接大語言模型,將 AI 生成的長文本自動轉換為響應式數據流,配合 WebFlux 實現從模型推理到瀏覽器逐字輸出的端到端非阻塞傳輸,既避免了傳統同步請求的內存壓力,又天然支持高并發場景下的實時交互,使開發者無需關注復雜的數據流控制,即可快速構建具備"人類思考節奏感"的智能應用。