LangChain 讓 LLM 帶上記憶
最近兩年,我們見識(shí)了“百模大戰(zhàn)”,領(lǐng)略到了大型語言模型(LLM)的風(fēng)采,但它們也存在一個(gè)顯著的缺陷:沒有記憶。
在對話中,無法記住上下文的 LLM 常常會(huì)讓用戶感到困擾。本文探討如何利用 LangChain,快速為 LLM 添加記憶能力,提升對話體驗(yàn)。
LangChain 是 LLM 應(yīng)用開發(fā)領(lǐng)域的最大社區(qū)和最重要的框架。
一、LLM 固有缺陷,沒有記憶
當(dāng)前的 LLM 非常智能,在理解和生成自然語言方面表現(xiàn)優(yōu)異,但是有一個(gè)顯著的缺陷:沒有記憶。
LLM 的本質(zhì)是基于統(tǒng)計(jì)和概率來生成文本,對于每次請求,它們都將上下文視為獨(dú)立事件。這意味著當(dāng)你與 LLM 進(jìn)行對話時(shí),它不會(huì)記住你之前說過的話,這就導(dǎo)致了 LLM 有時(shí)表現(xiàn)得不夠智能。
這種“無記憶”屬性使得 LLM 無法在長期對話中有效跟蹤上下文,也無法積累歷史信息。比如,當(dāng)你在聊天過程中提到一個(gè)人名,后續(xù)再次提及該人時(shí),LLM 可能會(huì)忘記你之前的描述。
本著發(fā)現(xiàn)問題解決問題的原則,既然沒有記憶,那就給 LLM 裝上記憶吧。
二、記憶組件的原理
1.沒有記憶的煩惱
當(dāng)我們與 LLM 聊天時(shí),它們無法記住上下文信息,比如下圖的示例:
2.原理
如果將已有信息放入到 memory 中,每次跟 LLM 對話時(shí),把已有的信息丟給 LLM,那么 LLM 就能夠正確回答,見如下示例:
目前業(yè)內(nèi)解決 LLM 記憶問題就是采用了類似上圖的方案,即:將每次的對話記錄再次丟入到 Prompt 里,這樣 LLM 每次對話時(shí),就擁有了之前的歷史對話信息。
但如果每次對話,都需要自己手動(dòng)將本次對話信息繼續(xù)加入到history信息中,那未免太繁瑣。有沒有輕松一些的方式呢?有,LangChain!LangChain 對記憶組件做了高度封裝,開箱即用。
3.長期記憶和短期記憶
在解決 LLM 的記憶問題時(shí),有兩種記憶方案,長期記憶和短期記憶。
- 短期記憶:基于內(nèi)存的存儲(chǔ),容量有限,用于存儲(chǔ)臨時(shí)對話內(nèi)容。
- 長期記憶:基于硬盤或者外部數(shù)據(jù)庫等方式,容量較大,用于存儲(chǔ)需要持久的信息。
三、LangChain 讓 LLM 記住上下文
LangChain 提供了靈活的內(nèi)存組件工具來幫助開發(fā)者為 LLM 添加記憶能力。
1.單獨(dú)用 ConversationBufferMemory 做短期記憶
Langchain 提供了 ConversationBufferMemory 類,可以用來存儲(chǔ)和管理對話。
ConversationBufferMemory 包含input變量和output變量,input代表人類輸入,output代表 AI 輸出。
每次往ConversationBufferMemory組件里存入對話信息時(shí),都會(huì)存儲(chǔ)到history的變量里。
2.利用 MessagesPlaceholder 手動(dòng)添加 history
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(return_messages=True)
memory.load_memory_variables({})
memory.save_context({"input": "我的名字叫張三"}, {"output": "你好,張三"})
memory.load_memory_variables({})
memory.save_context({"input": "我是一名 IT 程序員"}, {"output": "好的,我知道了"})
memory.load_memory_variables({})
from langchain.prompts import ChatPromptTemplate
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一個(gè)樂于助人的助手。"),
MessagesPlaceholder(variable_name="history"),
("human", "{user_input}"),
]
)
chain = prompt | model
user_input = "你知道我的名字嗎?"
history = memory.load_memory_variables({})["history"]
chain.invoke({"user_input": user_input, "history": history})
user_input = "中國最高的山是什么山?"
res = chain.invoke({"user_input": user_input, "history": history})
memory.save_context({"input": user_input}, {"output": res.content})
res = chain.invoke({"user_input": "我們聊得最后一個(gè)問題是什么?", "history": history})
執(zhí)行結(jié)果如下:
3.利用 ConversationChain 自動(dòng)添加 history
我們利用 LangChain 的ConversationChain對話鏈,自動(dòng)添加history的方式添加臨時(shí)記憶,無需手動(dòng)添加。一個(gè)鏈實(shí)際上就是將一部分繁瑣的小功能做了高度封裝,這樣多個(gè)鏈就可以組合形成易用的強(qiáng)大功能。這里鏈的優(yōu)勢一下子就體現(xiàn)出來了:
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
memory = ConversationBufferMemory(return_messages=True)
chain = ConversationChain(llm=model, memory=memory)
res = chain.invoke({"input": "你好,我的名字是張三,我是一名程序員。"})
res['response']
res = chain.invoke({"input":"南京是哪個(gè)?。?})
res['response']
res = chain.invoke({"input":"我告訴過你我的名字,是什么?,我的職業(yè)是什么?"})
res['response']
執(zhí)行結(jié)果如下,可以看到利用ConversationChain對話鏈,可以讓 LLM 快速擁有記憶:
4. 對話鏈結(jié)合 PromptTemplate 和 MessagesPlaceholder
在 Langchain 中,MessagesPlaceholder是一個(gè)占位符,用于在對話模板中動(dòng)態(tài)插入上下文信息。它可以幫助我們靈活地管理對話內(nèi)容,確保 LLM 能夠使用最上下文來生成響應(yīng)。
采用ConversationChain對話鏈結(jié)合PromptTemplate和MessagesPlaceholder,幾行代碼就可以輕松讓 LLM 擁有短時(shí)記憶。
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一個(gè)愛撒嬌的女助手,喜歡用可愛的語氣回答問題。"),
MessagesPlaceholder(variable_name="history"),
("human", "{input}"),
]
)
memory = ConversationBufferMemory(return_messages=True)
chain = ConversationChain(llm=model, memory=memory, prompt=prompt)
res = chain.invoke({"input": "今天你好,我的名字是張三,我是你的老板"})
res['response']
res = chain.invoke({"input": "幫我安排一場今天晚上的高規(guī)格的晚飯"})
res['response']
res = chain.invoke({"input": "你還記得我叫什么名字嗎?"})
res['response']
四、使用長期記憶
短期記憶在會(huì)話關(guān)閉或者服務(wù)器重啟后,就會(huì)丟失。如果想長期記住對話信息,只能采用長期記憶組件。
LangChain 支持多種長期記憶組件,比如Elasticsearch、MongoDB、Redis等,下面以Redis為例,演示如何使用長期記憶。
from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="gpt-3.5-turbo",
openai_api_key="sk-xxxxxxxxxxxxxxxxxxx",
openai_api_base="https://api.aigc369.com/v1",
)
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一個(gè)擅長{ability}的助手"),
MessagesPlaceholder(variable_name="history"),
("human", "{question}"),
]
)
chain = prompt | model
chain_with_history = RunnableWithMessageHistory(
chain,
# 使用redis存儲(chǔ)聊天記錄
lambda session_id: RedisChatMessageHistory(
session_id, url="redis://10.20.1.10:6379/3"
),
input_messages_key="question",
history_messages_key="history",
)
# 每次調(diào)用都會(huì)保存聊天記錄,需要有對應(yīng)的session_id
chain_with_history.invoke(
{"ability": "物理", "question": "地球到月球的距離是多少?"},
config={"configurable": {"session_id": "baily_question"}},
)
chain_with_history.invoke(
{"ability": "物理", "question": "地球到太陽的距離是多少?"},
config={"configurable": {"session_id": "baily_question"}},
)
chain_with_history.invoke(
{"ability": "物理", "question": "地球到他倆之間誰更近"},
config={"configurable": {"session_id": "baily_question"}},
)
LLM 的回答如下,同時(shí)關(guān)閉 session 后,直接再次提問最后一個(gè)問題,LLM 仍然能給出正確答案。
只要configurable配置的session_id能對應(yīng)上,LLM 就能給出正確答案。
然后,繼續(xù)查看redis存儲(chǔ)的數(shù)據(jù),可以看到數(shù)據(jù)在 redis 中是以 list的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)的。
五、總結(jié)
本文介紹了 LLM 缺乏記憶功能的固有缺陷,以及記憶組件的原理,還討論了如何利用 LangChain 給 LLM 裝上記憶組件,讓 LLM 能夠在對話中更好地保持上下文。希望對你有幫助!