【Spring AI 0基礎教程】1、基礎篇 環境搭建 - 智能天氣預報助手

基礎篇 | 環境搭建 - 智能天氣預報助手

一、什么是 Spring AI

Spring AI (https://spring.io/projects/spring-ai)]是 Spring 官方于 2023 年推出的 AI 應用開發框架,它如同 AI 世界的"Spring 生態連接器",致力于簡化開發集成了 AI 功能的應用程序。它主要解決兩大核心問題:

  1. 統一接口:消除不同 AI 服務(如 OpenAI、智譜 AI、DeepSeek 等)的 API 差異,允許用戶靈活地在多個模型之間切換。
  2. 生態整合:將 AI 能力無縫融入 Spring 技術棧(如 Spring Boot、Spring MVC)。

在這里插入圖片描述

如上圖所示,Spring AI 成為了連接企業數據以及 API 和生成式 AI 的橋梁。

?? 核心功能矩陣

Spring AI 具備的能力如下:

它具備以下特點:

  1. 即插即用:通過更換 application.yml 配置即可切換大模型供應商。
  2. 響應式支持:原生集成 Reactor Streams,支持背壓控制的流式響應。
  3. 擴展性強:自定義 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 實現:

    1. 語音識別
      • 集成 Whisper 模型,將用戶語音轉為文本。
      • 支持實時流式傳輸(降低延遲)。
    2. 自然語言處理(NLP)
      • 使用 Spring AI 的對話模型解析用戶意圖,生成響應文本。
      • 情感分析:識別用戶情緒(憤怒/焦慮),觸發人工坐席接管。
    3. 語音合成(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 webspring 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-SNAPSHOT1.0.0-M61.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);}}

我們來解讀一下代碼:


  1. 類定義與注解

    @RestController
    public class WeatherController {

  • @RestController
    • Spring MVC 注解,表示該類是一個 RESTful 控制器,所有方法默認返回 JSON/XML 數據而非視圖頁面。
    • 等價于 @Controller + @ResponseBody 的組合。

  1. 依賴注入

    private final ZhiPuAiChatModel chatModel;

    public WeatherController(ZhiPuAiChatModel chatModel) {this.chatModel = chatModel;}

  • 構造函數注入:Spring 推薦的方式,保證依賴不可變(final 修飾符),避免空指針異常。
  • ZhiPuAiChatModel:Spring AI 的組件,封裝了與智譜 AI 模型的交互邏輯(如 API 調用、參數處理)。

  1. 系統提示詞定義

    private static final String SYSTEM_PROMPT = “”"
    你是一個專業的天氣預報機器人,擅長:
    1. 解答天氣相關的問題
    2. 提供天氣預報建議
    // … 其他提示 …
    “”";

  • 關鍵作用:
    • 角色定義:明確 AI 的領域邊界(只處理天氣問題)。
    • 安全控制:當用戶提問非天氣問題時,觸發禮貌拒絕邏輯。
    • 風格控制:確保回復的專業性和友好性。

  1. 接口實現

    return this.chatModel.call(prompt);

  • chatModel.call() 內部機制:

    1. 認證:自動添加智譜 API 密鑰(通常通過 ZhiPuAiChatModel 配置類設置)。
    2. HTTP 調用:向智譜 API 端點(如 https://api.zhipu.ai/v4/chat/completions)發送 POST 請求。
    3. 參數封裝:將 prompt 包裝為模型所需的 JSON 格式,例如:

    {“model”: “glm-4”,“messages”: [{“role”: “user”, “content”: “北京今天天氣…”}],“temperature”: 0.7}

    1. 響應解析:提取智譜 API 返回結果中的 content 字段。

整體流程示意圖

]在這里插入圖片描述

后端代碼測試

運行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 架構在處理流式響應時存在以下局限:

  1. 同步阻塞模型導致資源利用率低,每個請求會獨占一個線程直至響應完成(通常需要 3-5 秒),當并發量達到 Tomcat 默認的 200 線程上限時,后續請求將進入等待隊列,而此時 CPU 利用率往往不足 30%。
  2. 難以實現真正的實時數據推送,傳統 MVC 基于 HTTP1.1 的請求-響應模式,要實現類似 ChatGPT 的字詞逐句返回,只能通過定時輪詢(Polling)或長輪詢(Long-Polling)等非實時方案,造成至少 300-500ms 的延遲。
  3. 高并發場景下性能瓶頸明顯,在 10,000 QPS 的高并發場景下,同步模型會產生超過 16GB 的線程堆內存開銷,而 WebFlux 的響應式模型僅需 4 個 EventLoop 線程即可處理相同負載,內存消耗降低 80%。

Spring WebFlux 核心概念

  1. 與傳統 Spring MVC 對比
  • 非阻塞 I/O:使用少量線程處理高并發請求(Tomcat 默認約 200 線程 vs Netty 約 2-4 個 EventLoop 線程)
  • 響應式 Endpoints:支持返回 Mono/Flux 類型,自動處理響應式流式輸出
  1. 響應式編程,基于 Reactive Streams 規范
  • 異步非阻塞數據處理:無需等待單個操作完成即可處理其他任務(對比傳統同步阻塞式處理)
  • 背壓(Backpressure):消費者控制數據流速的機制,防止生產者發送數據過快導致內存溢出
  • 事件驅動:通過事件回調(而非輪詢)實現數據流處理,適合高并發場景
  1. 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):


實際應用場景
  1. 客服機器人
    • 設置 topP=0.9 + 溫度=0.8
    • 確保回答專業(不跳出預設話術),同時帶點自然感
  2. 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 實現從模型推理到瀏覽器逐字輸出的端到端非阻塞傳輸,既避免了傳統同步請求的內存壓力,又天然支持高并發場景下的實時交互,使開發者無需關注復雜的數據流控制,即可快速構建具備"人類思考節奏感"的智能應用。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/92422.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/92422.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/92422.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

深入淺出MyBatis緩存:如何讓數據庫交互飛起來

深入淺出MyBatis緩存&#xff1a;如何讓數據庫交互飛起來你是否遇到過這樣的場景&#xff1a;系統在高并發下響應緩慢&#xff0c;數據庫監控顯示CPU飆升&#xff0c;日志里充斥著大量重復SQL&#xff1f;作為開發者&#xff0c;我曾親眼目睹一個簡單的配置查詢拖垮整個系統。今…

【計算機考研(408)- 數據結構】緒論

緒論 基本概念&#xff08;理解即可&#xff09; 數據是信息的載體&#xff0c;是描述客觀事物屬性的數、字符及所有能輸入到計算機中并被計算機程序識別 和處理的符號的集合。數據是計算機程序加工的原料。&#xff08;For Example : 聲音/圖像/字符串等&#xff09; 數據元…

嵌入式學習-土堆PyTorch(9)-day25

進入尾聲&#xff0c;一個完整的模型訓練 &#xff0c;點亮的第一個led#自己注釋版 import torch import torchvision.datasets from torch import nn from torch.utils.tensorboard import SummaryWriter import time # from model import * from torch.utils.data import Dat…

Java變量詳解:局部變量、成員變量、類變量區別及使用場景

作為Java開發者&#xff0c;深入理解不同變量的特性是寫出高質量代碼的基礎。本文將為你全面解析三種核心變量類型&#xff0c;并通過實戰案例展示它們的正確使用方式。一、變量類型概覽 1. 局部變量&#xff08;Local Variable&#xff09; 定義&#xff1a;在方法、構造方法或…

【收集電腦信息】collect_info.sh

收集電腦信息 collect_info.sh #!/bin/bashoutput"info.txt" > "$output"# 1. OS Version echo " 操作系統名稱及版本 " >> "$output" lsb_release -d | cut -f2- >> "$output" echo -e "\n" >…

服務器清理空間--主要是conda環境清理和刪除

1.查看空間情況 (base) zhouy24RL-DSlab:~/zhouy24Files$ df -h Filesystem Size Used Avail Use% Mounted on udev 252G 0 252G 0% /dev tmpfs 51G 4.9M 51G 1% /run /dev/nvme0n1p3 1.9T 1.7T 42G 98% / tmpfs 252G …

UE5多人MOBA+GAS 26、為角色添加每秒回血回藍(番外:添加到UI上)

文章目錄添加生命值和藍量的狀態標簽創建無限GE并應用監聽添加和去除標簽每秒回復配上UI添加生命值和藍量的狀態標簽 添加新的標簽 CRUNCH_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Stats_Health_Full)CRUNCH_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Stats_Health_Empty)CRUNCH_API U…

MetaGPT源碼剖析(三):多智能體系統的 “智能角色“ 核心實現——Role類

每一篇文章都短小精悍&#xff0c;不啰嗦。今天我們來深入剖析Role類的代碼實現。在多智能體協作系統中&#xff0c;Role&#xff08;角色&#xff09;就像現實世界中的 "員工"&#xff0c;是執行具體任務、參與協作的基本單位。這段代碼是 MetaGPT 框架的核心&#…

【項目經驗】小智ai MCP學習筆記

理論 1、什么是MCP MCP(Model Context Protocol&#xff0c;模型上下文協議)是一種開放式協議&#xff0c;它實現了LLM與各種工具的調用。使LLM從對話、生成式AI變成了擁有調用三方工具的AI。用官方的比喻&#xff0c;MCP就是USB-C接口&#xff0c;只要實現了這個接口&#x…

Matlab學習筆記:矩陣基礎

MATLAB學習筆記:矩陣基礎 作為MATLAB的核心,矩陣是處理數據的基礎工具。矩陣本質上是一個二維數組,由行和列組成,用于存儲和操作數值數據。在本節中,我將詳細講解矩陣的所有知識點,包括創建、索引、運算、函數等,確保內容通俗易懂。我會在關鍵地方添加MATLAB代碼示例,…

技術演進中的開發沉思-38 MFC系列:關于打印

打印程序也是MFC開發中不能忽視的一個環節&#xff0c;現在做打印開發so easy。但當年做打印開發還是挺麻煩。在當年的桌面程序里就像拼圖的最后一塊&#xff0c;看著簡單&#xff0c;實則要把屏幕上的像素世界&#xff0c;準確映射到打印機的物理紙張上。而MFC 的打印機制就像…

Apache Ignite 長事務終止機制

這段內容講的是 Apache Ignite 中長事務終止機制&#xff08;Long Running Transactions Termination&#xff09;&#xff0c;特別是關于分區映射交換&#xff08;Partition Map Exchange&#xff09;與事務超時設置&#xff08;Transaction Timeout&#xff09;之間的關系。下…

網絡編程---TCP協議

TCP協議基礎知識TCP&#xff08;Transmission Control Protocol&#xff0c;傳輸控制協議&#xff09;是互聯網核心協議之一&#xff0c;位于傳輸層&#xff08;OSI第4層&#xff09;&#xff0c;為應用層提供可靠的、面向連接的、基于字節流的數據傳輸服務。它與IP協議共同構成…

K 近鄰算法(K-Nearest Neighbors, KNN)詳解及案例

K近鄰算法&#xff08;K-Nearest Neighbors, KNN&#xff09;詳解及案例 一、基本原理 K近鄰算法是一種監督學習算法&#xff0c;核心思想是“物以類聚&#xff0c;人以群分”&#xff1a;對于一個新樣本&#xff0c;通過計算它與訓練集中所有樣本的“距離”&#xff0c;找出距…

深入理解 Redis 集群化看門狗機制:原理、實踐與風險

在分布式系統中&#xff0c;我們常常需要執行一些關鍵任務&#xff0c;這些任務要么必須成功執行&#xff0c;要么失敗后需要明確的狀態&#xff08;如回滾&#xff09;&#xff0c;并且它們的執行時間可能難以精確預測。如何確保這些任務不會被意外中斷&#xff0c;或者在長時…

Python機器學習:從零基礎到項目實戰

目錄第一部分&#xff1a;思想與基石——萬法歸宗&#xff0c;筑基問道第1章&#xff1a;初探智慧之境——機器學習世界觀1.1 何為學習&#xff1f;從人類學習到機器智能1.2 機器學習的“前世今生”&#xff1a;一部思想與技術的演進史1.3 為何是Python&#xff1f;——數據科學…

數據庫:庫的操作

1&#xff1a;查看所有數據庫SHOW DATABASES;2&#xff1a;創建數據庫CREATE DATABASE [ IF NOT EXISTS ] 數據庫名 [ CHARACTER SET 字符集編碼 | COLLATE 字符集校驗規則 | ENCRYPTION { Y | N } ];[]&#xff1a;可寫可不寫{}&#xff1a;必選一個|&#xff1a;n 選 1ENCR…

AngularJS 動畫

AngularJS 動畫 引言 AngularJS 是一個流行的JavaScript框架,它為開發者提供了一種構建動態Web應用的方式。在AngularJS中,動畫是一個強大的功能,可以幫助我們創建出更加生動和引人注目的用戶界面。本文將詳細介紹AngularJS動畫的原理、用法以及最佳實踐。 AngularJS 動畫…

SonarQube 代碼分析工具

??親愛的技術愛好者們,熱烈歡迎來到 Kant2048 的博客!我是 Thomas Kant,很開心能在CSDN上與你們相遇~?? 本博客的精華專欄: 【自動化測試】 【測試經驗】 【人工智能】 【Python】 ??全面掌握 SonarQube:企業代碼質量保障的利器 ?? 在當今 DevOps 流水線中,代碼…

vmware vsphere esxi6.5 使用工具導出鏡像

注&#xff1a;為什么使用這個工具&#xff0c;我這邊主要因為esxi6.5自身bug導致web導出鏡像會失敗一、下載VMware-ovftool到本地系統&#xff08;根據你的操作系統版本到官網下載安裝&#xff0c;此處略&#xff09;以下內容默認將VMware-ovftool安裝到windows 本地系統為例。…