iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
生成式 AI

使用 Spring AI 打造企業 RAG 知識庫系列 第 18

Day18 - Spring AI 官方記憶

  • 分享至 

  • xImage
  •  

動手寫程式的機會越來越少了

https://ithelp.ithome.com.tw/upload/images/20240818/20161290r2Eu5opGWt.png
使用 Spring Boot 開發程式真的很快,透過 Lombok 不用寫 Getter / Setter / Constructor,現在連記憶儲存的類別都不讓我們動手,在不努力點就真的要被 AI 取代了XD

Spring AI 從 1.0.0 開始提供了 ChatMemory 介面,目前有一個 InMemoryChatMemory 實作類別,下面是原始程式碼,除了記錄歷史訊息外,透過 ChatID 還能區分不同對話

InMemoryChatMemory 原始碼

public class InMemoryChatMemory implements ChatMemory {
	Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();
	@Override
	public void add(String conversationId, List<Message> messages) {
		this.conversationHistory.putIfAbsent(conversationId, new ArrayList<>());
		this.conversationHistory.get(conversationId).addAll(messages);
	}
	@Override
	public List<Message> get(String conversationId, int lastN) {
		List<Message> all = this.conversationHistory.get(conversationId);
		return all != null ? all.stream().skip(Math.max(0, all.size() - lastN)).toList() : List.of();
	}
	@Override
	public void clear(String conversationId) {
		this.conversationHistory.remove(conversationId);
	}
}

研究其程式碼可發現內部使用 ConcurrentHashMap<String, List<Message>> 儲存資料,每個 Chat ID 就能有獨立的記憶,而 ConcurrentHashMap線程安全無阻塞存取 的特性,非常適合多人同時使用

取得資料的 get 方法除了 ChatID 外還多了 lastN 參數,透過 stream 的 skip 方法可篩選出最近 lastN 筆資料

最後還提供了 clear 方法可用來清除某個 ChatID 資料

程式碼實作

接下來開始改寫昨天的程式,將土炮記憶改為原裝的 InMemoryChatMemory
因為之後程式開始複雜,凱文大叔將複雜的邏輯獨立到 Service 元件處理

@RequiredArgsConstructor
@Service
public class ChatService {
	private final ChatClient chatClient;
	private ChatMemory chatMemory = new InMemoryChatMemory();
	
	public String chat(String chatId, String userMessage) {
		chatMemory.add(chatId, new UserMessage(userMessage)); //存對話時多了chatId
		return this.chatClient.prompt()
	            .messages(chatMemory.get(chatId, 30)) //取對話時多了chatId與筆數
	            .call()
	            .content();
	}
}

原本的 Controller 專心處理轉派

@RestController
@RequiredArgsConstructor
public class AiClientController {
	private final ChatService chatService;
	
	@GetMapping("/memchat")
	public String chat(@RequestParam String chatId, @RequestParam String prompt) {
		return chatService.chat(chatId, prompt);
	}
}

功能驗收

接著測試兩個主要功能

  1. 當 ChatID 不同時記憶是否隔離
  2. 超過取得資料的 lastN 時,是否還有記憶

測試1: 切換 ChatID,確認不同 ID 是否有獨立記憶

ChatID = 2
提問1: 我是凱文大叔
https://ithelp.ithome.com.tw/upload/images/20240818/20161290TTt4t17LY2.png
提問2: 我是誰
https://ithelp.ithome.com.tw/upload/images/20240818/20161290sf7iUG1Cv5.png
ChatID = 2
提問3: 我是誰
https://ithelp.ithome.com.tw/upload/images/20240818/20161290eAVjSG3vVI.png

測試結果可以看出在 ChatID = 1 時,能記住我是凱文大叔,一但切到 ChatID = 2 又是另一段記憶

測試2: 測試 lastN 的效果

這次將記憶數量改成2 .messages(chatMemory.get(chatId, 2)),確認第三次發問時能否有之前的記憶
提問1: 我是凱文大叔
https://ithelp.ithome.com.tw/upload/images/20240818/201612900YmPX3kzvt.png
提問2: 金魚的記憶有幾秒?
https://ithelp.ithome.com.tw/upload/images/20240818/201612908HZETqO0E0.png
提問3: 還記得我是誰嗎?
https://ithelp.ithome.com.tw/upload/images/20240818/20161290W4B0cZUyJI.png

記憶上限的參數也有作用
通常歷史訊息的數量可設為 20~30 左右,超過的話除了對 AI 的回答沒幫助外,也容易超過每次傳訊的 Token 限制,當然越多 Token 花的錢也越多,有效控制歷史訊息數量就非常重要了

回顧:

今日學到甚麼?

  1. Spring AI 提供的 InMemoryChatMemory 原始碼解讀
  2. InMemoryChatMemory 的紀錄與取用方式
  3. 測試不同 Chat ID 與超過上限的對話結果

Source Code:

程式碼下載: https://github.com/kevintsai1202/SpringBoot-AI-Day18.git


認識凱文大叔

凱文大叔使用 Java 開發程式超過 20 年,對於 Java 生態非常熟悉,曾使用反射機制開發 ETL 框架,對 Spring 背後的原理非常清楚,目前以 Spring Boot 作為後端開發框架,前端使用 React 搭配 Ant Design
下班之餘在 Amazing Talker 擔任程式語言講師,並獲得學員的一致好評

最近剛成立一個粉絲專頁-凱文大叔教你寫程式 歡迎大家多追蹤,我會不定期分享實用的知識以及程式開發技巧

想討論 Spring 的 Java 開發人員可以加入 FB 討論區 Spring Boot Developer Taiwan

我是凱文大叔,歡迎一起加入學習程式的行列


上一篇
Day17 - 讓 AI 記住對話
下一篇
Day19 - Spring AI的鏈式增強器
系列文
使用 Spring AI 打造企業 RAG 知識庫21
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言