目錄
1、Ollama 的下載配置 與 DeepSeek 的本地部署流程
1.1 下載安裝 Ollama
1.2 搜索模型并進行本地部署
2、基于 SpringAI 調用 Ollama 模型
2.1 基于OpenAI 的接口規范(其他模型基本遵循)
2.2 在 IDEA?中進行創建?SpringAI 項目并調用 DS 模型
3、基于 SpringAI 實現會話記憶功能
4、基于 SpringAI 實現會話歷史功能
需求一:需要進行展示歷史會話的列表信息
需求二:點擊會話列表,展示對應的歷史會話記錄信息
📜后端代碼地址:
MySpringAI: 基于 SpringAI + 大模型 實現基本的對話功能(只提供后端)
https://gitee.com/MIMIDeK/my-spring-ai.git
1、Ollama 的下載配置 與 DeepSeek 的本地部署流程
很多云平臺都提供了一鍵部署大模型的功能,這里不再贅述(阿里云百煉平臺、火山方舟-火山引擎等)
官網地址如下:
Ollamahttps://ollama.com/
1.1 下載安裝 Ollama
首先,我們需要下載一個Ollama的客戶端,在官網提供了各種不同版本的Ollama,大家可以根據自己的需要下載
注意:
Ollama默認安裝目錄是C盤的用戶目錄,如果不希望安裝在C盤的話(其實C盤如果足夠大放C盤也沒事),就不能直接雙擊安裝了,需要通過命令行安裝
在 OllamaSetup.exe 所在目錄打開cmd命令行,然后命令如下:
OllamaSetup.exe /DIR=你要安裝的目錄位置
安裝完成后,需要進行環境變量的配置:
OLLAMA_MODELS=你想要保存模型的目錄
1.2 搜索模型并進行本地部署
Ollama 是一個模型管理工具和平臺,它提供了很多國內外常見的模型,我們可以在其官網上搜索自己需要的模型,這里我們部署的是 DeepSeek R1 模型(我選擇的是 7b,根據需求進行選擇):
其中,Ollama 的命令很像 Docker 的,選擇好對應的模型后,在控制臺中進行運行命令:
ollama run deepseek-r1:7b
首次下載的話會有些慢,耐性等待即可
以下是本地下載部署完成后,進行對話的場景,ctrl + d 進行退出與 DeepSeek 的對話:
2、基于 SpringAI 調用 Ollama 模型
2.1 基于OpenAI 的接口規范(其他模型基本遵循)
目前大多數大模型都遵循 OpenAI 的接口規范,是基于Http協議的接口;因此請求路徑、參數、返回值信息都是類似的,可能會有一些小的差別,具體需要查看大模型的官方API文檔
以下是基于 Python 的代碼示范:
# Please install OpenAI SDK first: `pip3 install openai`from openai import OpenAI# 1.初始化OpenAI客戶端,要指定兩個參數:api_key、base_url
client = OpenAI(api_key="<DeepSeek API Key>", base_url="https://api.deepseek.com")# 2.發送http請求到大模型,參數比較多
response = client.chat.completions.create(model="deepseek-chat", # 2.1.選擇要訪問的模型messages=[ # 2.2.發送給大模型的消息{"role": "system", "content": "You are a helpful assistant"},{"role": "user", "content": "Hello"},],stream=False # 2.3.是否以流式返回結果
)print(response.choices[0].message.content)
接口說明
- 請求方式):通常是POST,因為要傳遞JSON風格的參數
- 請求路徑):與平臺有關
- DeepSeek官方平臺:https://api.deepseek.com
- 阿里云百煉平臺:https://dashscope.aliyuncs.com/compatible-mode/v1
- 本地ollama部署的模型:http://localhost:11434
- 安全校驗):開放平臺?都需要提供 API_KEY 來校驗權限,本地 ollama 則不需要
- 請求參數)(這里只列舉常用的):
- model:要訪問的模型名稱
- messages:發送給大模型的消息,是一個數組
- stream:true,代表響應結果流式返回;false,代表響應結果一次性返回,但需要等待
- temperature:取值范圍[0:2),代表大模型生成結果的隨機性,越小隨機性越低。DeepSeek-R1不支持
- 這里請求參數中的 messages 是一個消息數組,其中包含兩個屬性):
- role:消息對應的角色
- content:消息內容
2.2 在 IDEA?中進行創建?SpringAI 項目并調用 DS 模型
以下是創建項目的基本依賴需求(基于SpringBoot3、JDK17):
.yaml 配置:
這里是基于 Ollama 的(默認端口是11434)
這里是基于 openAI 的,進行調用開放平臺的 API,需要輸入 API-KEY(后面就不具體寫明了,這里主要是基于 Ollama 的調用演示)
Config 配置類:(固定寫法)
@Configuration
public class CommonConfig {@Beanpublic ChatClient chatClient(OllamaChatModel model) {return ChatClient.builder(model).build();}
}
Controller 控制響應類:(這里是以流式的方式進行響應)
@RestController
@RequestMapping("/ai")
public class ChatController {@Resourceprivate ChatClient chatClient;// 由于以流式方式調用模型,展示的數據會出現亂碼,需要進行規定編碼格式@GetMapping(value = "/chat", produces = "text/html; charset=utf-8")public Flux<String> chat(String msg) {return chatClient.prompt().user(msg).stream().content();}
}
進行頁面的調用(若非流式訪問,則需要等待 DS 生成完畢,才會出現響應信息):
注意,進行接口的調用時,需要啟動 ollama 中的模型
🔖也可以加上日志的控制臺打印輸出,方便調試
.yml 中的配置:
然后在 Config 配置類中添加這一行代碼,即可開啟日志的打印:
3、基于 SpringAI 實現會話記憶功能
大致步驟:
定義會話存儲方式(默認基于Map集合) ?? 配置會話記憶(關聯上下文) ?? 添加會話ID(保證每個會話獨立)
這里使用默認方式,由源碼可知是基于 Map 直接存入內存中的;當然也可以重寫 ChatMemory,比如存入到 Redis 中做緩存之類的(根據業務需求)
然后將 "存儲方式" 配置到 "會話的上下文" 中,以處理會話的內容
運行結果:
之前的msg提問信息是:現在有15個蘋果,6個人應該怎么分才好
我現在直接問它x個人應該怎么分,很明顯它會進行關聯之前的會話,根據上下文,來進行作答
存在問題:
然而,以上會話的處理會比較混亂,不管是誰來進行提問,模型都會自動關聯所有的會話上下文來進行回答,這顯然是不符合需求的;這時就需要與前端進行交接,即傳遞對應的會話 ID 過來進行處理,以實現用戶的每個獨立會話內容互不干擾
由前端響應接口可知,不同的會話對應著不同的會話ID
這時需要在 Controller 接口中添加以下配置,來接收存儲不同會話下的ID
4、基于 SpringAI 實現會話歷史功能
存在問題:目前,只要頁面一刷新,之前的會話列表和對話記錄都會被清除,不會被保留展示
前言:以下的 type 參數可根據具體需求進行更換,chatId 建議作保留以記錄唯一會話
需求一:需要進行展示歷史會話的列表信息
以下是對應的 UI 頁面,以及對應的接口地址(模擬):
以下是 自定義 的 方法接口 與 實現類,主要用于 保存 和 獲取 會話信息
public interface ChatHistoryService {/*** 保存會話記錄(這里的 type 可根據需求進行更改,chatId 建議保留以作為會話的唯一ID)*/void save(String type, String chatId);/*** 獲取會話列表(這里的 type 可根據需求進行更改)*/List<String> getChatIds(String type);
}
@Service
public class ChatHistoryServiceImpl implements ChatHistoryService {// 使用 map 集合進行存儲private final Map<String, List<String>> chatHistoryMap = new HashMap<>();/*** 保存會話記錄(這里的 type 可根據需求進行更改,chatId 建議保留以作為會話的唯一ID)*/@Overridepublic void save(String type, String chatId) {// 1.判斷當前是否存在對應的 type 類型,若不存在則新增if (! chatHistoryMap.containsKey(type)) {chatHistoryMap.put(type, new ArrayList<>());}// 2.判斷當前的 type 對應的 會話集合ID 是否含有當前的會話IDList<String> chatIds = chatHistoryMap.get(type);if (chatIds.contains(chatId)) {return;}chatIds.add(chatId);}/*** 獲取會話列表信息(這里的 type 可根據需求進行更改)*/@Overridepublic List<String> getChatIds(String type) {List<String> chatIds = chatHistoryMap.get(type);return chatIds == null ? new ArrayList<>() : chatIds;}
}
每次用戶在進行模型對話的時候,都需要將會話ID做保存(這里的?key?根據自己的業務需求來定,比如使用?用戶ID?來進行拼接等,這里先寫死)
然后獲取當前對應?key 的會話 ID 集合,進行會話列表信息展示
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {@Resourceprivate ChatHistoryService chatHistoryService;/*** 獲取歷史會話列表信息(這里的 type 可根據需求進行更改)*/@RequestMapping("/{type}")public List<String> getChatIds(@PathVariable("type") String type) {return chatHistoryService.getChatIds(type);}
}
需求二:點擊會話列表,展示對應的歷史會話記錄信息
像以下UI所示,隨便點擊其中一個會話列表,對應的歷史會話記錄依然存在,并進行全部展示
接口地址(模擬):
會話的存儲是在?ChatMemory 類中進行的,所以需要跟進源碼進行查看
主要還是這個 get 方法,來進行獲取對應會話的對話記錄
然后 返回值 Message 類型,內部有繼承以及實現的類
其中的 Content 代表著對話記錄的內容,MessageType 代表著當前對話記錄的類型
以上結論:由于需要滿足當前接口的返回值,所以需要創建一個?DTO?來進行滿足
@Data
@NoArgsConstructor
public class MessageDTO implements Serializable {/*** 角色類型*/private String role;/*** 對應的內容信息*/private String content;public MessageDTO(Message message) {// 判斷當前會話的類型switch (message.getMessageType()) {case USER:this.role = "user";break;case ASSISTANT:this.role = "assistant";break;case SYSTEM:this.role = "system";break;case TOOL:this.role = "tool";break;default:this.role = "";break;}this.content = message.getText();}@Serialprivate static final long serialVersionUID = 1L;
}
接下來實現 “查詢對話記錄” 接口,與前端交互,即可實現展現歷史對話記錄
/*** 查詢會話記錄詳情(這里的 type 可根據需求進行更改,chatId 建議保留以作為會話的唯一ID)*/@GetMapping("/{type}/{chatId}")public List<MessageDTO> getChatHistory(@PathVariable("type") String type, @PathVariable("chatId") String chatId) {// 這里的展示記錄條數最大值規定,直接使用的 int 型最大值,具體自己根據需求規定List<Message> messages = chatMemory.get(chatId, Integer.MAX_VALUE);if (messages == null) {return List.of();}// 進行封裝返回類型return messages.stream().map(new Function<Message, MessageDTO>() {@Overridepublic MessageDTO apply(Message message) {return new MessageDTO(message);}}).toList();}