【学习LangChain】02. 记忆(Memory)

Andy Yang
22 min read3 days ago

--

Messages:消息

上面调用LLM的方式,都是通过向invoke函数传入一个string参数,实现text-in, text-out(文本输入,文本输出),这是比较传统、老式的completion接口;现在LLM主推的API接口,都是所谓的Chat Completion Models方式,实现messages in, message out(消息输入,消息输出)。直接看例子🌰:

from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage

load_dotenv()

# LLM style: text in, text out
model = ChatOpenAI(model="gpt-4o")
result = model.invoke('What is 81 divided by 9?')
print("\n== LLM style: text in, text out ==\n")
print(result)

# SystemMessage:
# Message for priming AI behavior, usually passed in as the first of a sequenc of input messages.
# HumanMessagse:
# Message from a human to the AI model.
messages = [
SystemMessage(content="Solve the following math problems"),
HumanMessage(content="What is 81 divided by 9?"),
]

result = model.invoke(messages)
print("\n== Chat Model style: messages in, message out ==\n")
print(result)
== LLM style: text in, text out ==

content='81 divided by 9 equals 9.' response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 16, 'total_tokens': 25}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'stop', 'logprobs': None} id='run-9ca2ec2d-1dbf-4c29-a7c8-490a282ed9dd-0' usage_metadata={'input_tokens': 16, 'output_tokens': 9, 'total_tokens': 25}

== Chat Model style: messages in, message out ==

content="81 divided by 9 is 9. \n\nHere's the calculation:\n\n\\[ 81 \\div 9 = 9 \\]" response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 25, 'total_tokens': 51}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None} id='run-0389a581-4878-484b-827c-d59b50fd41e2-0' usage_metadata={'input_tokens': 25, 'output_tokens': 26, 'total_tokens': 51}
type(result)
langchain_core.messages.ai.AIMessage

在Chat Model的调用中,用户传入的参数是一个消息列表,支持的消息类型包括:

  • SystemMessage:系统消息,用来设定AI的行为,一般放在消息列表的第一项。
  • AIMessage:AI消息,用来表示模型的输出。上面例子中的result就是一个AIMessage,它会包含一个response_metadata成员,来说明token的消耗、模型名称、停止原因等等。
  • HumanMessage:人类消息,用来表示用户的输入。LLM方式调用直接传入invoke的string,就会被LangChain包装成一个HumanMessage。
  • FunctionMessage:在function call中会用到。
  • ToolMessage:在tool call中会用到。

消息列表可以很好地支持Few Shot(通过举例,提升LLM的准确性,减少幻觉)的Prompt技巧:

from langchain_core.messages import AIMessage, HumanMessage, SystemMessage

# Few shot prompt messages
messages = [
SystemMessage(content="Solve the following math problems"),
HumanMessage(content="What is 81 divided by 9?"),
AIMessage(content="81 divided by 9 is 9."),
HumanMessage(content="What is 10 times 5?"),
]

# Invoke the model with messages
result = model.invoke(messages)
print(f"Answer from AI: {result.content}")
Answer from AI: 10 times 5 is 50.

在后面的编码中,我们会采用Messages这种更主流的API方式。之前的例子,用户和LLM的对话都是一次性的;使用Messages,我们可以方便地实现对话消息历史记录,从而增加Chat Model的记忆功能。

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.schema import AIMessage, HumanMessage, SystemMessage

# Load environment variables from .env
load_dotenv()

# Create a ChatOpenAI model
model = ChatOpenAI(model="gpt-3.5-turbo")


chat_history = [] # Use a list to store messages

# Set an initial system message (optional)
system_message = SystemMessage(content="You are a helpful AI assistant.")
chat_history.append(system_message) # Add system message to chat history

# Chat loop
while True:
query = input("You: ")
if query.lower() == "exit":
break
chat_history.append(HumanMessage(content=query)) # Add user message

# Get AI response using history
result = model.invoke(chat_history)
response = result.content
chat_history.append(AIMessage(content=response)) # Add AI message

print(f"AI: {response}")


print("---- Message History ----")
print(chat_history)
You:  Andy是一个喜欢编程、喜欢写作的工程师,你认识他吗?
AI: 抱歉,作为一个AI助手,我不认识任何具体的个人。不过,我可以帮助您了解关于编程、写作以及工程领域的信息。如果您有任何问题,都可以随时问我哦!
You:  81÷9=
AI: 81 ÷ 9 = 9
You:  Andy的爱好是什么?
AI: 根据您之前提到的信息,Andy的爱好是编程和写作。这些爱好表明他对技术和创意有着浓厚的兴趣。如果您还想了解更多关于编程或写作方面的信息,可以随时向我提问。我将竭诚为您提供帮助!
You:  exit
---- Message History ----
[SystemMessage(content='You are a helpful AI assistant.'), HumanMessage(content='Andy是一个喜欢编程、喜欢写作的工程师,你认识他吗?'), AIMessage(content='抱歉,作为一个AI助手,我不认识任何具体的个人。不过,我可以帮助您了解关于编程、写作以及工程领域的信息。如果您有任何问题,都可以随时问我哦!'), HumanMessage(content='81÷9= '), AIMessage(content='81 ÷ 9 = 9'), HumanMessage(content='Andy的爱好是什么?'), AIMessage(content='根据您之前提到的信息,Andy的爱好是编程和写作。这些爱好表明他对技术和创意有着浓厚的兴趣。如果您还想了解更多关于编程或写作方面的信息,可以随时向我提问。我将竭诚为您提供帮助!')]

在上面的例子中,我们手动维护了一个chat_history列表,将角色设定的SystemMessage、每轮对话的用户输入HumanMessage以及LLM的回复AIMessage都加入chat_history中,作为每次对话的消息列表,从而实现了AI聊天的记忆功能,让它记住了Andy的爱好。

Memory:记忆

内存

上面的聊天记录是手动维护的,LangChain提供了一系列内置方便存储和读取聊天记录的功能供开发者使用。下面是一个使用内存作为存储的例子:

from langchain_openai.chat_models import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory

model = ChatOpenAI(model='gpt-3.5-turbo')
prompt = ChatPromptTemplate.from_messages(
[
MessagesPlaceholder(variable_name="history"),
("human", "{input}"),
]
)
runnable = prompt | model


store = {}


def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]

def print_session_history(session_id: str) -> None:
text = f"Message History (session_id={session_id})"
print(f"\n----{text}----")
print(get_session_history(session_id))
print(f"----{'-' * len(text)}----")

with_message_history = RunnableWithMessageHistory(
runnable,
get_session_history,
input_messages_key="input",
history_messages_key="history",
)

session_id = 'abc123'
print_session_history(session_id)
result = with_message_history.invoke(
{"input": "我是Andy, 喜欢编程和写作。"},
config={"configurable": {"session_id": session_id}},
)
print(f"AI: {result.content}")


print_session_history(session_id)
result =with_message_history.invoke(
{"input": "我是谁?爱好是什么?"},
config={"configurable": {"session_id": session_id}},
)
print(f"AI: {result.content}")

session_id = 'abc456'
print_session_history(session_id)
result = with_message_history.invoke(
{"input": "我是谁?爱好是什么?"},
config={"configurable": {"session_id": session_id}},
)
print(f"AI: {result.content}")
----Message History (session_id=abc123)----

-------------------------------------------
AI: 很高兴认识你,Andy!编程和写作是非常有趣和创造性的爱好。你喜欢哪种编程语言?在写作方面,你更倾向于写小说还是技术文章呢?希望我们可以互相交流和分享我们的兴趣!如果有任何问题或者想讨论的话题,都可以随时告诉我。😊

----Message History (session_id=abc123)----
Human: 我是Andy, 喜欢编程和写作。
AI: 很高兴认识你,Andy!编程和写作是非常有趣和创造性的爱好。你喜欢哪种编程语言?在写作方面,你更倾向于写小说还是技术文章呢?希望我们可以互相交流和分享我们的兴趣!如果有任何问题或者想讨论的话题,都可以随时告诉我。😊
-------------------------------------------
AI: 您是Andy,您的爱好是编程和写作。您可以随时告诉我您想要讨论的话题,我会尽力和您进行交流和互动。如果有任何问题或者需要帮助的地方,也欢迎随时告诉我。让我们一起分享和探讨各种有趣的主题吧!😊

----Message History (session_id=abc456)----

-------------------------------------------
AI: 抱歉,我无法回答这个问题,因为我不了解你的身份和爱好。您可以告诉我更多关于自己,让我更好地了解您。

上面例子中使用LangChain提供的RunnableWithMessageHistory、ChatMessageHistory来自动管理聊天对话,并通过实现一个get_session_history函数来实现在不同session之间的对话切换。在session_id=abc123的对话中,AI记住了我的名字和爱好;当切换到session_id=abc456的对话时,这个新聊天的历史就是空的。

sqlite 数据库

存在内存中的聊天记录在程序结束之后就没有了;为了保留聊天记录,需要将历史数据存放到外部的存储中,比如文件、数据库等。LangChain提供了不同的组件加以支持。下面是用嵌入式数据库sqlite作为聊天记录存储的例子:

from langchain_community.chat_message_histories import SQLChatMessageHistory

chat_message_history = SQLChatMessageHistory(
session_id="test_session_id", connection="sqlite:///chat_history.db"
)

chat_message_history.add_user_message("Hello")
chat_message_history.add_ai_message("Hi")
chat_message_history.messages
[HumanMessage(content='Hello'), AIMessage(content='Hi')]

可以通过下面的函数来获取并打印某个session的聊天历史:

def get_session_history_from_sqlite(session_id: str) -> BaseChatMessageHistory:
return SQLChatMessageHistory(
session_id=session_id, connection_string="sqlite:///chat_history.db"
)

def print_session_history_from_sqlite(session_id: str) -> None:
text = f"Message History (session_id={session_id})"
print(f"\n----{text}----")
print(get_session_history_from_sqlite(session_id))
print(f"----{'-' * len(text)}----")

print_session_history_from_sqlite('test_session_id')
----Message History (session_id=test_session_id)----
Human: Hello
AI: Hi
----------------------------------------------------
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful assistant."),
MessagesPlaceholder(variable_name="history"),
("human", "{question}"),
]
)

chain = prompt | ChatOpenAI()

chain_with_history = RunnableWithMessageHistory(
chain,
lambda session_id: SQLChatMessageHistory(
session_id=session_id, connection_string="sqlite:///chat_history.db"
),
input_messages_key="question",
history_messages_key="history",
)

session_id = "chat_his_1"
config = {"configurable": {"session_id": session_id}}


print_session_history_from_sqlite(session_id)
result = chain_with_history.invoke({"question": "Hi! 你好,我是Andy,我喜欢编程和写作"}, config=config)
print(f"AI: {result.content}")

print_session_history_from_sqlite(session_id)
result = chain_with_history.invoke({"question": "81÷9=?"}, config=config)
print(f"AI: {result.content}")

print_session_history_from_sqlite(session_id)
result = chain_with_history.invoke({"question": "你还记得我吗?"}, config=config)
print(f"AI: {result.content}")
----Message History (session_id=chat_his_1)----

-----------------------------------------------
AI: 你好 Andy!很高兴认识你。编程和写作都是很有趣的爱好!你有任何关于编程或写作方面的问题需要帮助吗?我会尽力回答你的问题。

----Message History (session_id=chat_his_1)----
Human: Hi! 你好,我是Andy,我喜欢编程和写作
AI: 你好 Andy!很高兴认识你。编程和写作都是很有趣的爱好!你有任何关于编程或写作方面的问题需要帮助吗?我会尽力回答你的问题。
-----------------------------------------------
AI: 81 ÷ 9 = 9.

----Message History (session_id=chat_his_1)----
Human: Hi! 你好,我是Andy,我喜欢编程和写作
AI: 你好 Andy!很高兴认识你。编程和写作都是很有趣的爱好!你有任何关于编程或写作方面的问题需要帮助吗?我会尽力回答你的问题。
Human: 81÷9=?
AI: 81 ÷ 9 = 9.
-----------------------------------------------
AI: 是的,我记得你是Andy,喜欢编程和写作。有什么我可以帮忙的吗?

我们可以通过sqliteonlinesqlime等网站在线查看写入到chat_history.db中的聊天记录数据。下图是通过sqlime查看的结果:

可见,聊天结果是写入到数据表message_store中,其中有id、session_id和message等三个字段。

其他存储

除了sqlite,LangChain还提供了各种Memory类供存储聊天历史,常见的包括:

  • Google Firestore
  • Postgres
  • Redis
  • MongoDB
  • Neo4j
  • 。。。

你可以根据自己的需要,自行选定。

用Memory来管理聊天历史的系统框图,如下:

其中的Runnable部分,与之前的ChatOpenAI略有不同,是一个如下的代码片段:

prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful assistant."),
MessagesPlaceholder(variable_name="history"),
("human", "{question}"),
]
)

chain = prompt | ChatOpenAI()

chain_with_history = RunnableWithMessageHistory(
chain,
lambda session_id: SQLChatMessageHistory(
session_id=session_id, connection_string="sqlite:///chat_history.db"
),
input_messages_key="question",
history_messages_key="history",
)

这就引出来接下来要探索的ChatPromptTemplate和LangChain Expression Language (LCEL) 。

【未完待续】

--

--