1. 為什么需要內存?
大型語言模型(LLM)本身是無狀態的。這意味著每次你向 LLM 發送一個請求(Prompt),它都會獨立處理這個請求,完全不記得之前任何的交互。這在構建一次性問答應用時沒問題,但在需要多輪對話(比如聊天機器人、智能客服)的場景中,LLM 需要“記住”之前的對話內容,才能理解上下文并做出連貫的回復。
內存就是 LangChain 解決這個問題的方案。它允許你在 LLM 應用中注入和管理對話歷史,讓 LLM 具備“長期”記憶的能力。
簡單來講就是存儲歷史記錄,然后在每次使用 LLM 時,不僅傳入當前輸入,還有歷史記錄
2. 內存的基本工作原理
在 LangChain 中,內存通常扮演著連接用戶輸入和 LLM 提示的橋梁。
- 保存歷史:每次用戶和 LLM 進行交互后,內存組件都會捕獲并存儲這次對話的輸入和輸出。
- 加載歷史:在下一次 LLM 調用之前,內存會根據需要從其存儲中加載相關的對話歷史。
- 注入提示:加載的對話歷史會被格式化并注入到 LLM 的提示(Prompt)中,作為上下文的一部分。這樣,LLM 就能“看到”之前的對話內容,從而理解當前問題的背景。
3. 最簡單的內存:對話緩沖區(ConversationBufferMemory)
ConversationBufferMemory
是 LangChain 中最基礎也是最常用的內存類型。它非常簡單粗暴:記住所有對話的原始文本。它會將完整的對話歷史原封不動地存儲起來,并在每次調用時將所有歷史添加到提示中。
# memory_buffer_example.pyfrom langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import os# --- 配置部分 ---
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"if not os.getenv("OPENAI_API_KEY"):print("錯誤:請設置環境變量 OPENAI_API_KEY 或在代碼中取消注釋并設置您的密鑰。")exit()print("--- ConversationBufferMemory 示例開始 ---")# 1. 定義 LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)# 2. 定義內存
# memory_key 是在 Prompt 中用來引用對話歷史的變量名
# return_messages=True 表示內存返回的是消息對象列表,而不是單個字符串
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
print("已創建 ConversationBufferMemory。")# 3. 定義帶有歷史占位符的 Prompt
# MessagesPlaceholder 用于告訴 Prompt 在這里插入消息列表 (chat_history)
prompt = ChatPromptTemplate.from_messages([("system", "你是一個友好的AI助手,擅長進行多輪對話。"),MessagesPlaceholder(variable_name="chat_history"), # 聊天歷史的占位符("human", "{input}") # 當前用戶輸入
])
print("已創建包含 chat_history 占位符的 Prompt。")# 4. 創建 ConversationChain
# ConversationChain 是 LangChain 提供的一個預構建的鏈,專為處理對話而設計
# 它會自動處理內存的注入和更新
conversation = ConversationChain(llm=llm,memory=memory,prompt=prompt,verbose=True # 打印詳細日志,可以看到內存注入到 Prompt 的過程
)
print("已創建 ConversationChain。")# 5. 進行多輪對話
print("\n--- 開始對話 ---")# 第一輪
print("\n用戶: 你好,我叫小明。你叫什么名字?")
response1 = conversation.invoke({"input": "你好,我叫小明。你叫什么名字?"})
print(f"AI: {response1['response']}")# 第二輪
print("\n用戶: 很高興認識你!我之前告訴你我叫什么名字了?")
response2 = conversation.invoke({"input": "很高興認識你!我之前告訴你我叫什么名字了?"})
print(f"AI: {response2['response']}")# 第三輪
print("\n用戶: 幫我寫一句關于編程的詩。")
response3 = conversation.invoke({"input": "幫我寫一句關于編程的詩。"})
print(f"AI: {response3['response']}")print("\n--- 對話結束 ---")
print("\n--- ConversationBufferMemory 示例結束 ---")
ConversationBufferMemory
將所有交互都作為HumanMessage
和AIMessage
存儲起來。MessagesPlaceholder
是一個非常關鍵的組件,它告訴 LangChain 在構建最終發送給 LLM 的提示時,應該將chat_history
這個變量的內容(即內存中的消息列表)插入到這里。ConversationChain
是一個便利的鏈,它自動處理了內存的讀寫,簡化了對話應用的構建。
4. 限制歷史長度:對話緩沖區窗口內存(ConversationBufferWindowMemory)
ConversationBufferMemory
的一個缺點是,如果對話很長,內存中的歷史會不斷增長,導致每次發送給 LLM 的提示越來越長,最終可能超出 LLM 的上下文窗口限制(Context Window Limit),并增加 API 成本。
ConversationBufferWindowMemory
解決了這個問題,它只記住最近 N 輪的對話。
# memory_window_example.pyfrom langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferWindowMemory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import os# --- 配置部分 ---
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"if not os.getenv("OPENAI_API_KEY"):print("錯誤:請設置環境變量 OPENAI_API_KEY 或在代碼中取消注釋并設置您的密鑰。")exit()print("--- ConversationBufferWindowMemory 示例開始 ---")llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)# 定義窗口大小為 2,表示只記住最近 2 輪(4條消息:2用戶+2AI)對話
memory = ConversationBufferWindowMemory(memory_key="chat_history", return_messages=True, k=2)
print("已創建 ConversationBufferWindowMemory,窗口大小 k=2。")prompt = ChatPromptTemplate.from_messages([("system", "你是一個友好的AI助手,只記得最近的對話。"),MessagesPlaceholder(variable_name="chat_history"),("human", "{input}")
])conversation = ConversationChain(llm=llm,memory=memory,prompt=prompt,verbose=True
)
print("已創建 ConversationChain。")# 5. 進行多輪對話,觀察內存如何截斷
print("\n--- 開始對話 ---")# 第一輪
print("\n用戶: 我喜歡吃蘋果。")
response1 = conversation.invoke({"input": "我喜歡吃蘋果。"})
print(f"AI: {response1['response']}")# 第二輪
print("\n用戶: 你呢?你喜歡什么水果?")
response2 = conversation.invoke({"input": "你呢?你喜歡什么水果?"})
print(f"AI: {response2['response']}")# 第三輪 - 此時第一輪對話(用戶說“我喜歡吃蘋果”)應該被“遺忘”
print("\n用戶: 我之前喜歡什么水果來著?")
response3 = conversation.invoke({"input": "我之前喜歡什么水果來著?"})
print(f"AI: {response3['response']}") # AI可能無法準確回答,因為它忘記了第一輪# 第四輪 - 此時第二輪對話應該被遺忘
print("\n用戶: 你呢?你剛才喜歡什么水果?")
response4 = conversation.invoke({"input": "你呢?你剛才喜歡什么水果?"})
print(f"AI: {response4['response']}")print("\n--- 對話結束 ---")
print("\n--- ConversationBufferWindowMemory 示例結束 ---")
k=2
參數控制了窗口大小。這意味著內存將只保留最近的 2 輪完整的對話(即 2 條用戶消息和 2 條 AI 消息)。- 你會發現,在第三輪對話中,模型可能無法回憶起第一輪用戶提到的“蘋果”,因為它已經超出了窗口范圍。
5. 總結歷史:對話摘要內存(ConversationSummaryMemory)
ConversationBufferWindowMemory
雖然限制了歷史長度,但可能會丟失早期對話的關鍵信息。ConversationSummaryMemory
旨在解決這個問題:它不直接存儲所有歷史,而是使用一個 LLM 對對話歷史進行摘要,然后將這個摘要作為上下文提供給主 LLM。
這樣,無論對話多長,每次傳遞給 LLM 的都是一個簡潔的摘要,既能保持上下文,又不會超出令牌限制。
# memory_summary_example.pyfrom langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationSummaryMemory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import os# --- 配置部分 ---
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"if not os.getenv("OPENAI_API_KEY"):print("錯誤:請設置環境變量 OPENAI_API_KEY 或在代碼中取消注釋并設置您的密鑰。")exit()print("--- ConversationSummaryMemory 示例開始 ---")# 定義一個用于生成摘要的 LLM
# 摘要LLM可以是一個更小的、成本更低的模型,或者與主LLM相同
summary_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
main_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)# 1. 定義內存
# memory_key 和 return_messages 類似之前的內存
# llm 參數指定了用于生成摘要的 LLM
memory = ConversationSummaryMemory(llm=summary_llm, memory_key="chat_history", return_messages=True)
print("已創建 ConversationSummaryMemory。")prompt = ChatPromptTemplate.from_messages([("system", "你是一個友好的AI助手,能記住所有對話的重要摘要。"),MessagesPlaceholder(variable_name="chat_history"), # 這里傳入的是摘要("human", "{input}")
])conversation = ConversationChain(llm=main_llm,memory=memory,prompt=prompt,verbose=True
)
print("已創建 ConversationChain。")# 5. 進行多輪對話,觀察內存如何進行摘要
print("\n--- 開始對話 ---")# 第一輪
print("\n用戶: 我的項目遇到了一個問題,需要你的幫助。我正在開發一個Python腳本,它需要處理大量文本數據。")
response1 = conversation.invoke({"input": "我的項目遇到了一個問題,需要你的幫助。我正在開發一個Python腳本,它需要處理大量文本數據。"})
print(f"AI: {response1['response']}")# 第二輪
print("\n用戶: 具體來說,我需要對文本進行分詞和詞性標注。你有什么建議的庫嗎?")
response2 = conversation.invoke({"input": "具體來說,我需要對文本進行分詞和詞性標注。你有什么建議的庫嗎?"})
print(f"AI: {response2['response']}")# 第三輪
print("\n用戶: 好的,我會試試 NLTK 和 SpaCy。你認為哪個更適合大型項目?")
response3 = conversation.invoke({"input": "好的,我會試試 NLTK 和 SpaCy。你認為哪個更適合大型項目?"})
print(f"AI: {response3['response']}")# 第四輪 - 此時內存會包含前三輪的摘要
print("\n用戶: 好的,謝謝你的建議。我的項目主要是關于中文文本處理。")
response4 = conversation.invoke({"input": "好的,謝謝你的建議。我的項目主要是關于中文文本處理。"})
print(f"AI: {response4['response']}")print("\n--- 對話結束 ---")
# 打印最終的摘要內容
print("\n當前內存中的摘要:")
print(memory.buffer)print("\n--- ConversationSummaryMemory 示例結束 ---")
說明:
ConversationSummaryMemory
需要一個llm
參數來執行摘要任務。這個llm
可以是與主 LLM 相同的模型,也可以是專門為摘要優化的模型。- 通過
verbose=True
觀察輸出,你會發現每次調用時,LLM 接收到的chat_history
變量會是一個不斷更新的摘要字符串,而不是原始的完整消息列表。
6. 其他常用內存類型
LangChain 還提供了其他更高級或更具體用途的內存類型:
ConversationSummaryBufferMemory
: 結合了ConversationBufferWindowMemory
和ConversationSummaryMemory
的特點。它會保留最近 N 輪的完整對話,而將 N 輪之前的對話進行摘要。這樣可以在短期內提供精確上下文,同時長期保持摘要記憶。VectorStoreRetrieverMemory
: 這種內存將對話歷史存儲在向量數據庫中。當需要回憶信息時,它會像 RAG 那樣,根據當前查詢在向量數據庫中檢索最相關的歷史片段,而不是全部或摘要。這對于需要記憶超長對話或從大量歷史中檢索特定信息非常有用。EntityMemory
: 專注于從對話中識別和記憶特定的實體(如人名、地點、概念),并將其存儲在一個結構化(通常是 JSON)的知識圖中。當對話中再次提到這些實體時,LLM 可以直接引用其存儲的信息。
7. 選擇合適的內存策略
ConversationBufferMemory
: 適用于短對話、簡單場景或調試。ConversationBufferWindowMemory
: 適用于需要限制上下文長度、但仍需保持一定近期對話完整性的場景。ConversationSummaryMemory
: 適用于長對話,需要保持核心上下文但又不能超出 LLM 上下文窗口的場景。ConversationSummaryBufferMemory
: 結合了短期精確記憶和長期摘要記憶的優點。VectorStoreRetrieverMemory
: 適用于需要從海量、復雜對話歷史中智能檢索相關片段的場景,或構建具備“知識庫”的聊天機器人。EntityMemory
: 適用于需要記憶和跟蹤特定實體信息(如客戶檔案、產品屬性)的對話場景。