一、準備環(huán)境
Spring AI 最低支持 Spring Boot 3.4.x + JDK 17 以上版本。如果在使用 Spring Boot 2.x + JDK 8,需要先升級環(huán)境,可參考Spring Boot 2.7 + JDK 8 升級至 Spring Boot 3.4.13 + JDK 17 手冊。
二、本地安裝大模型
本地安裝大模型,可參考本地部署DeepSeek大模型,文章里安裝的是deepseek-r1:7b大模型,不支持 tool 工具。

我們需要安裝一個新的模型,這里我們使用 qwen3.5 大模型
ollama run qwen3.5
Ollama提供了兩個主要的API端點,它們在功能和協(xié)議兼容性上有所區(qū)別。原生接口是 /api/generate,它使用 prompt 字段,并且不支持 messages 這種對話列表格式。
| 特性 | /api/generate (Ollama原生接口) | /v1/completions (OpenAI兼容接口) |
|---|---|---|
| 協(xié)議標準 | Ollama自定義協(xié)議 | 遵循OpenAI API規(guī)范 |
| 兼容性 | 適用于Ollama專屬功能 | 便于與現(xiàn)有OpenAI生態(tài)工具(如LangChain)集成 |
| 上下文管理 | 需顯式傳遞context字段維護多輪對話狀態(tài) | 通常通過messages數(shù)組傳遞歷史對話 |
| 流式響應 | 默認支持,通過stream: true啟用 | 通過stream: true參數(shù)啟用 |
| 適用場景 | 需要深度控制模型參數(shù)、低延遲流式輸出 | 項目從云端OpenAI遷移至本地Ollama,希望最小化代碼改動 |
注意:Ollama 的 OpenAI 兼容端點是 http://localhost:11434/v1/,SDK 會自動拼接出完整的http://localhost:11434/v1/completions。
三、實戰(zhàn)案例:實現(xiàn)智能客服 AI 助手
2.1 創(chuàng)建Maven工程
使用IntelliJ IDEA創(chuàng)建一個Maven工程,完成工程初始化。
2.2 引入核心依賴
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.devpotato</groupId>
<artifactId>chat-service</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
<packaging>jar</packaging>
<name>ChatService</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>2.15.0</jackson.version>
<spring-ai.version>1.0.0-M1</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- spring-boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI -->
<!-- Source: https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-ollama -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama</artifactId>
<version>${spring-ai.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<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>
</project>
2.3 解決跨域問題
由于前端頁面與后端服務可能存在跨域(CORS)問題,導致前端無法正常調用后端API,因此需要添加跨域過濾器。創(chuàng)建MyFilter類,實現(xiàn)Filter接口,具體代碼如下:
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 解決Chrome等瀏覽器訪問本地服務的跨域問題
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 允許所有域名跨域訪問(生產環(huán)境建議指定具體域名,提升安全性)
httpResponse.setHeader("Access-Control-Allow-Origin", "*");
// 允許的請求方式
httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
// 允許的請求頭
httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type");
// 繼續(xù)執(zhí)行過濾鏈
chain.doFilter(request, response);
}
}
2.4 工程配置文件
在src/main/resources目錄下創(chuàng)建application.yml文件,配置服務端口(默認8080,可根據(jù)需求修改):
server:
port: 8080
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
options:
model: qwen3.5 # 本地部署的大模型
2.5 核心對話控制器
package org.devpotato.ollama;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/chat")
public class OllamaChatController {
/**
* 系統(tǒng)提示詞
*/
private static final String SYSTEM_PROMPT = "作為一個專業(yè)的電商售后支持客服,你具備深厚的問題解決能力。請針對用戶提出的問題,提供詳細的解答步驟或有效的解決方案,并且考慮到用戶的水平可能有所不同,請盡可能地簡化語言。";
@Autowired
private OllamaChatModel chatModel;
@RequestMapping(path = "", method = RequestMethod.POST)
public String chat(@RequestBody Map<String, Object> map) {
String message = map.get("message").toString();
// 構建帶系統(tǒng)提示詞的Prompt
String fullMessage = SYSTEM_PROMPT + "\n\n用戶提問:" + message;
Prompt prompt = new Prompt(fullMessage);
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getContent();
}
}
四、前端頁面搭建(簡易版)
前端采用簡單的HTML+JavaScript搭建聊天界面,實現(xiàn)用戶輸入提問、展示客服回復的功能。創(chuàng)建index.html文件,代碼如下(可直接在瀏覽器中打開使用):
<!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;
font-family: 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f7fa;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.chat-container {
width: 100%;
max-width: 800px;
height: 80vh;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
background-color: #409eff;
color: #fff;
padding: 15px 20px;
display: flex;
align-items: center;
gap: 10px;
}
.chat-header img {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #fff;
}
.chat-header h2 {
font-size: 18px;
font-weight: 600;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: #f9f9f9;
}
.message {
margin-bottom: 15px;
max-width: 70%;
display: flex;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user-message {
margin-left: auto;
flex-direction: row-reverse;
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
margin: 0 8px;
flex-shrink: 0;
}
.user-message .message-content {
background-color: #409eff;
color: #fff;
border-radius: 10px 10px 0 10px;
}
.bot-message .message-content {
background-color: #fff;
color: #333;
border-radius: 10px 10px 10px 0;
border: 1px solid #eee;
}
.message-content {
padding: 10px 15px;
word-wrap: break-word;
line-height: 1.4;
}
/* 快捷按鈕區(qū)域樣式 */
.quick-buttons {
padding: 10px 15px;
border-top: 1px solid #eee;
background-color: #fafafa;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.quick-button {
padding: 6px 15px;
background-color: #e8f4ff;
color: #409eff;
border: 1px solid #d1e9ff;
border-radius: 20px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.quick-button:hover {
background-color: #409eff;
color: #fff;
border-color: #409eff;
}
.chat-input {
display: flex;
padding: 15px;
border-top: 1px solid #eee;
background-color: #fff;
}
#message-input {
flex: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 25px;
outline: none;
font-size: 14px;
resize: none;
height: 45px;
max-height: 120px;
}
#message-input:focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
#send-button {
margin-left: 10px;
padding: 0 20px;
background-color: #409eff;
color: #fff;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
#send-button:hover {
background-color: #337ecc;
}
#send-button:disabled {
background-color: #b3d8ff;
cursor: not-allowed;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-hint {
text-align: center;
color: #999;
padding: 50px 0;
font-size: 14px;
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMjAiIGZpbGw9IiM0MDllZmYiLz4KPHBhdGggZD0iTTE1IDI1QzE1IDI3LjcxIDE2Ljk5IDI5IDE5IDI5QzIxLjAxIDI5IDIzIDI3LjcxIDIzIDI1QzIzIDIyLjc5IDIxLjAxIDIxIDE5IDIxQzE2Ljk5IDIxIDE1IDIyLjc5IDE1IDI1WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==" alt="客服圖標">
<h2>在線客服中心</h2>
</div>
<div class="chat-messages" id="chat-messages">
<div class="empty-hint" id="empty-hint">歡迎咨詢,我是您的專屬客服??</div>
</div>
<!-- 新增快捷按鈕區(qū)域 -->
<div class="quick-buttons" id="quick-buttons">
<div class="quick-button" onclick="sendQuickMessage('查看訂單')">查看訂單</div>
<div class="quick-button" onclick="sendQuickMessage('查看物流')">查看物流</div>
<div class="quick-button" onclick="sendQuickMessage('申請退款')">申請退款</div>
<div class="quick-button" onclick="sendQuickMessage('修改收貨地址')">修改收貨地址</div>
<div class="quick-button" onclick="sendQuickMessage('商品質量問題')">商品質量問題</div>
</div>
<div class="chat-input">
<textarea id="message-input" placeholder="請輸入您想咨詢的問題..." onkeydown="if(event.keyCode===13&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea>
<button id="send-button" onclick="sendMessage()">發(fā)送</button>
</div>
</div>
<script>
// 獲取DOM元素
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const chatMessages = document.getElementById('chat-messages');
const emptyHint = document.getElementById('empty-hint');
// 頭像URL(使用base64編碼的SVG,也可以替換為實際圖片URL)
const BOT_AVATAR = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTgiIGN5PSIxOCIgcj0iMTgiIGZpbGw9IiM0MDllZmYiLz4KPHBhdGggZD0iTTEzIDIyQzEzIDI0LjIxIDE0Ljk5IDI2IDE3IDI2QzE5LjAxIDI2IDIxIDI0LjIxIDIxIDIyQzIxIDE5Ljc5IDE5LjAxIDE4IDE3IDE4QzE0Ljk5IDE4IDEzIDE5Ljc5IDEzIDIyWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==';
const USER_AVATAR = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTgiIGN5PSIxOCIgcj0iMTgiIGZpbGw9IiNmZmYwMDAiLz4KPHBhdGggZD0iTTEyIDIwQzEyIDIyLjcxIDEzLjk5IDI1IDE2IDI1QzE4LjAxIDI1IDIwIDIyLjcxIDIwIDIwQzIwIDE3Ljc5IDE4LjAxIDE1IDE2IDE1QzEzLjk5IDE1IDEyIDE3Ljc5IDEyIDIwWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTIwIDI5QzIwIDI5IDE3IDMwIDE3IDMwQzE0IDMwIDEyIDI5IDEyIDI5QzEyIDI5IDEyIDI3IDEyIDI3QzEyIDI3IDE0IDI2IDE2IDI2QzE4IDI2IDIwIDI3IDIwIDI3QzIwIDI3IDIwIDI5IDIwIDI5WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==';
// 自動調整輸入框高度
messageInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight > 45 ? this.scrollHeight : 45) + 'px';
});
// 快捷消息發(fā)送函數(shù)
function sendQuickMessage(message) {
// 將快捷消息填入輸入框
messageInput.value = message;
messageInput.style.height = 'auto';
messageInput.style.height = (messageInput.scrollHeight > 45 ? messageInput.scrollHeight : 45) + 'px';
// 自動發(fā)送該消息
sendMessage();
}
// 發(fā)送消息函數(shù)
async function sendMessage() {
const message = messageInput.value.trim();
// 驗證輸入內容
if (!message) {
alert('請輸入咨詢內容!');
return;
}
// 禁用發(fā)送按鈕和輸入框
sendButton.disabled = true;
messageInput.disabled = true;
try {
// 隱藏空提示
emptyHint.style.display = 'none';
// 添加用戶消息到聊天窗口
addMessageToChat(message, 'user');
// 清空輸入框并恢復高度
messageInput.value = '';
messageInput.style.height = '45px';
// 添加加載狀態(tài)
const loadingId = addLoadingMessage();
// 調用后端接口
const response = await fetch('http://127.0.0.1:8080/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message })
});
// 移除加載狀態(tài)
removeLoadingMessage(loadingId);
// 處理響應
if (response.ok) {
const data = await response.text();
// 添加客服回復到聊天窗口
addMessageToChat(data, 'bot');
} else {
addMessageToChat('抱歉,服務器暫時無法響應,請稍后再試!', 'bot');
console.error('接口請求失敗:', response.status);
}
} catch (error) {
// 移除加載狀態(tài)
const loadingElements = document.querySelectorAll('.loading-message');
loadingElements.forEach(el => el.remove());
addMessageToChat('網(wǎng)絡錯誤,請檢查您的網(wǎng)絡連接!', 'bot');
console.error('請求出錯:', error);
} finally {
// 恢復發(fā)送按鈕和輸入框
sendButton.disabled = false;
messageInput.disabled = false;
messageInput.focus();
// 滾動到最新消息
scrollToBottom();
}
}
// 添加消息到聊天窗口
function addMessageToChat(content, sender) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}-message`;
// 創(chuàng)建頭像元素
const avatarImg = document.createElement('img');
avatarImg.className = 'message-avatar';
avatarImg.src = sender === 'user' ? USER_AVATAR : BOT_AVATAR;
avatarImg.alt = sender === 'user' ? '用戶頭像' : '客服頭像';
// 創(chuàng)建消息內容元素
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = content;
// 組裝消息元素
messageDiv.appendChild(avatarImg);
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
// 滾動到最新消息
scrollToBottom();
}
// 添加加載中的消息
function addLoadingMessage() {
const loadingId = 'loading-' + Date.now();
const loadingDiv = document.createElement('div');
loadingDiv.id = loadingId;
loadingDiv.className = 'message bot-message loading-message';
// 創(chuàng)建客服頭像
const avatarImg = document.createElement('img');
avatarImg.className = 'message-avatar';
avatarImg.src = BOT_AVATAR;
avatarImg.alt = '客服頭像';
// 創(chuàng)建加載內容
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = '<div class="loading"></div>';
// 組裝加載消息
loadingDiv.appendChild(avatarImg);
loadingDiv.appendChild(contentDiv);
chatMessages.appendChild(loadingDiv);
scrollToBottom();
return loadingId;
}
// 移除加載中的消息
function removeLoadingMessage(loadingId) {
const loadingDiv = document.getElementById(loadingId);
if (loadingDiv) {
loadingDiv.remove();
}
}
// 滾動到聊天底部
function scrollToBottom() {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// 監(jiān)聽輸入框回車事件(兼容)
messageInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight > 45 ? this.scrollHeight : 45) + 'px';
});
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
</script>
</body>
</html>
五、測試
