這一期的目標是讓原本的旅遊景點與天氣生成器,增加記憶功能,讓 AI 能夠記住多個使用者的對話內容,不會搞錯。
在上一篇文章中,我示範了製作旅遊景點與天氣生成器,讓大型語言模型根據提示詞生成旅遊建議與當地天氣概況。
不過這個生成器無法記得使用者上次說了什麼,如果使用者詢問北京的景點與天氣概況,當語言模型回覆後,又緊接著詢問此城市有什麼知名美食。則語言模型不會知道使用者指的是北京。
LangChain 提供了不同的記憶模組,可以提升大型語言模型的連慣性與上下文理解能力,簡單地說就是讓模型能夠記得你們之間的對話。
LangChain 記憶模組的各種變形
LangChain 的記憶模組有不同的變化形式,我們不需要記住模組的名字,但是要知道有哪些場景可以運用。
全部內容都要記住
如果我們需要讓語言模型記住我們之間的所有對話,可以使用 ConversationBufferMemory
模組。
我們不可能讓語言模型無限制地記住我們之間所有對話,沒有這麼好的事,畢竟語言模型最大上下文是有限制的。
以OpenAI 的 GPT-4o 模型為例,是 128,000 tokens ,一個中文字元不一定對應一個 token ,但就姑且當作是吧,那就大概10幾萬字。
這些 tokens 還不包括使用者輸入的,也包括語言模型吐出來給你的,每次 API 呼叫時,整體的 token 使用量計算方式為:
總 token 數 = 輸入 tokens + 模型回應 tokens
假設第一次對話合計2000字,第二次對話又有2000字,兩次對話加起來就是4000字,以此類推。有可能你在進行到第50輪對話時,語言模型效能會越來越慢,達到上限就會回傳錯誤訊息。
所以為了避免模型出錯,就必須手動加上程式,例如定期檢查 memory.load_memory_variables({})
,確認 tokens 數。設定一個上限,只要一到上限就清理記憶,然後讓使用者重啟對話。
另一個簡單的作法是改用 ConversationBufferWindowMemory
模組,例如 memory = ConversationBufferWindowMemory(k=10)
。意思是只記住10輪之間的所有對話。
記得重點就好
其實我們不一定需要記住所有細節,人與人之間聊天也不會記得所有事,都是記得大概就好。
我們可以採取兩種方法,
ConversationSummaryMemory
:讓語言模型把對話歷史濃縮成摘要,適合用在客服聊天機器人。ConversationEntityMemory
:追蹤對話中提及的特定「實體」(Entities),例如人物、地點、公司等,如果需要記住使用者訊息,提供更好的個人化體驗,很適合用這個。
多用戶使用情境
上述的模組都適用於單一用戶情境,例如你要開發一個體適能追蹤系統,讓 AI 提供飲食與運動建議。使用者輸入的資料都會儲存在本地端,例如在手機上,不會上傳到雲端。
如果你沒打算把使用者輸入的資訊儲存在本地端,例如是基於瀏覽器的客服聊天機器人,這就叫多用戶使用情境。
客服聊天機器人在伺服器上接受多個使用者的詢問,因此需要記住每一個使用者,各自的對話內容。這些記憶資料都會放在伺服器上,而不是放在使用者的電腦或手機。
在多用戶使用情境底下,我們還是可以使用上述的模組,但是要另外設計用戶識別機制。
或者,我們可以採用 RunnableWithMessageHistory
模組,它使用 session_id
當作用戶識別機制,它的作法如下:
將現有的工作鏈包裝起來,也就是處理使用者的輸入
提供獲取歷史記錄的函數(get_message_history),透過用戶的
session_id
來檢索對話記錄,對話記錄可以來自資料庫。指定輸入訊息的鍵名(input_messages_key),在執行時,
RunnableWithMessageHistory
會從傳入的參數中,根據使用者輸入的input
內容,傳遞給retrieval_chain
。指定歷史訊息的鍵名(history_messages_key),系統根據
history_messages_key
找到歷史記錄,然後跟使用輸入內容一起提供給retrieval_chain
,確保回應是基於完整的對話上下文。
chain_with_history = RunnableWithMessageHistory(
main_branch, # 要包裝的原始 chain
get_message_history, # 用於獲取歷史記錄的函數
input_messages_key="input", # 輸入訊息在參數中的鍵名
history_messages_key="chat_history" # 歷史訊息在參數中的鍵名
)
通常還會手動建立用戶訊息歷史管理系統來搭配,用來傳入上面的 RunnableWithMessageHistory
。
# 用於儲存每個用戶的對話歷史
user_message_histories = {}
def get_message_history(user_id: str) -> ChatMessageHistory:
"""取得特定用戶的訊息歷史記錄,如果不存在則建立新的"""
if user_id not in user_message_histories:
user_message_histories[user_id] = ChatMessageHistory()
return user_message_histories[user_id]
關於怎麼讓 AI 產生記憶,你只要記得這些用法就可以了,其他部分在開發時,讓 Windsurf 等 AI 工具來幫你實現。
記得在實際的專案開發時,還要另外搭配使用者帳號,因為那才是RunnableWithMessageHistory
真正需要的 session_id
。
我將上一期的旅遊景點生成器,增加了RunnableWithMessageHistory
,並且示範了兩個不同使用者如何進行對話。如果有需要讀程式碼,請見LangChain奇幻旅程筆記(7):電子報 EP-9 示範案例 。
之前我開發到一半的武俠小說生成器,其實很適合用今天提到的ConversationEntityMemory
,來記錄每個角色的狀態。
我這個武俠小說生成器,是把金庸小說儲存在向量資料庫內,使用檢索增強生成(Retrieval-Augmented Generation, RAG)的方式來取出資料,讓語言模型學習後生成小說內容。
在下一期電子報,我們就來談談 RAG 系統怎麼實作。