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