python
import os, json
from typing import Sequence
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import message_to_dict, messages_from_dict, BaseMessage
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableWithMessageHistory
# message_to_dict:单个消息对象(BaseMessage类实例) -> 字典
# messages_from_dict:[字典、字典...] -> [消息、消息...]
# AIMessage、HumanMessage、SystemMessage 都是BaseMessage的子类
class FileChatMessageHistory(BaseChatMessageHistory):
def __init__(self, session_id, storage_path):
self.session_id = session_id # 会话id
self.storage_path = storage_path # 不同会话id的存储文件,所在的文件夹路径
# 完整的文件路径
self.file_path = os.path.join(self.storage_path, self.session_id)
# 确保文件夹是存在的
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
def add_messages(self, messages: Sequence[BaseMessage]) -> None:
# Sequence序列 类似list、tuple
all_messages = list(self.messages) # 已有的消息列表
all_messages.extend(messages) # 新的和已有的融合成一个list
# 将数据同步写入到本地文件中
# 类对象写入文件 -> 一堆二进制
# 为了方便,可以将BaseMessage消息转为字典(借助json模块以json字符串写入文件)
# 官方message_to_dict:单个消息对象(BaseMessage类实例) -> 字典
# new_messages = []
# for message in all_messages:
# d = message_to_dict(message)
# new_messages.append(d)
new_messages = [message_to_dict(message) for message in all_messages]
# 将数据写入文件
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(new_messages, f)
@property # @property装饰器将messages方法变成成员属性用
def messages(self) -> list[BaseMessage]:
# 当前文件内: list[字典]
try:
with open(self.file_path, "r", encoding="utf-8") as f:
messages_data = json.load(f) # 返回值就是:list[字典]
return messages_from_dict(messages_data)
except FileNotFoundError:
return []
def clear(self) -> None:
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump([], f)
model = ChatTongyi(model="qwen3-max")
# prompt = PromptTemplate.from_template(
# "你需要根据会话历史回应用户问题。对话历史:{chat_history},用户提问:{input},请回答"
# )
prompt = ChatPromptTemplate.from_messages(
[
("system", "你需要根据会话历史回应用户问题。对话历史:"),
MessagesPlaceholder("chat_history"),
("human", "请回答如下问题:{input}")
]
)
str_parser = StrOutputParser()
def print_prompt(full_prompt):
print("="*20, full_prompt.to_string(), "="*20)
return full_prompt
base_chain = prompt | print_prompt | model | str_parser
def get_history(session_id):
return FileChatMessageHistory(session_id, "./chat_history")
# 创建一个新的链,对原有链增强功能:自动附加历史消息
conversation_chain = RunnableWithMessageHistory(
base_chain, # 被增强的原有chain
get_history, # 通过会话id获取InMemoryChatMessageHistory类对象
input_messages_key="input", # 表示用户输入在模板中的占位符
history_messages_key="chat_history" # 表示用户输入在模板中的占位符
)
if __name__ == '__main__':
# 固定格式,添加LangChain的配置,为当前程序配置所属的session_id
session_config = {
"configurable": {
"session_id": "user_001"
}
}
# res = conversation_chain.invoke({"input": "小明有2个猫"}, session_config)
# print("第1次执行:", res)
#
# res = conversation_chain.invoke({"input": "小刚有1只狗"}, session_config)
# print("第2次执行:", res)
res = conversation_chain.invoke({"input": "总共有几个宠物"}, session_config)
print("第3次执行:", res)
这段代码复用了前一个例子的核心逻辑,但将历史存储从内存(InMemoryChatMessageHistory)升级为文件持久化(FileChatMessageHistory),使对话历史能够在程序重启后依然保留。
1. 核心变化:自定义 FileChatMessageHistory
python
class FileChatMessageHistory(BaseChatMessageHistory):
def __init__(self, session_id, storage_path):
self.session_id = session_id
self.storage_path = storage_path
self.file_path = os.path.join(self.storage_path, self.session_id)
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
- 每个会话的历史被保存为 单独的一个文件 ,文件名即为
session_id,存放在storage_path目录下。 - 这样,即使程序退出,下次启动时仍能加载之前的历史消息。
关键方法:
add_messages:将新消息追加到已有历史中,并使用message_to_dict将BaseMessage对象转成字典,再以 JSON 格式写入文件。messages(属性) :从文件读取 JSON 数据,用messages_from_dict还原为BaseMessage对象列表。clear:清空文件(重置历史)。
2. 链的构建与记忆增强
python
def get_history(session_id):
return FileChatMessageHistory(session_id, "./chat_history")
conversation_chain = RunnableWithMessageHistory(
base_chain,
get_history,
input_messages_key="input",
history_messages_key="chat_history"
)
- 与之前唯一不同的是
get_history返回的是FileChatMessageHistory实例,而非内存存储。 - 其余部分(提示模板、模型、解析器)完全一致。
3. 执行情况分析(重点)
python
if __name__ == '__main__':
session_config = {"configurable": {"session_id": "user_001"}}
# 前两次调用被注释掉了
res = conversation_chain.invoke({"input": "总共有几个宠物"}, session_config)
print("第3次执行:", res)
分两种场景:
场景一:程序首次运行(历史文件不存在)
-
FileChatMessageHistory.messages读取文件时抛出FileNotFoundError,返回空列表[]。 -
因此
chat_history占位符为空。 -
模型收到的提示只有系统消息和当前用户问题,没有历史记录。
-
结果 :模型无法知道"宠物数量"的上下文,会回复类似:
您还没有告诉我任何宠物的信息,请问您有多少只宠物呢?
场景二:之前已经运行过前两次调用(历史文件已存在)
-
文件
./chat_history/user_001中已保存了两次对话记录:- 用户:小明有2个猫
- 助手:(记录确认)
- 用户:小刚有1只狗
- 助手:(记录确认)
-
第三次调用时,历史被完整加载,模型能从中提取出"2 只猫 + 1 只狗"。
-
结果 :模型回答:
总共有 3 只宠物。
4. 关键技术总结
| 技术点 | 说明 |
|---|---|
自定义 BaseChatMessageHistory |
通过继承基类,可以轻松实现任何后端存储(文件、Redis、MySQL 等)。 |
message_to_dict / messages_from_dict |
LangChain 提供的序列化工具,确保消息对象的完整还原(包含角色、内容、额外元数据)。 |
| 文件持久化 | 将历史保存为 JSON 文件,方便调试和迁移,适合单机开发环境。 |
@property 装饰器 |
将 messages 方法变成属性访问,保持与 InMemoryChatMessageHistory 一致的接口。 |
| 健壮性处理 | messages 方法中捕获 FileNotFoundError 并返回空列表,避免程序崩溃。 |
5. 面试拓展提问
Q:如果生产环境,你会把历史存在哪里?
A :文件存储不适合高并发,我会使用 Redis (RedisChatMessageHistory)或 PostgreSQL (配合 AsyncPostgresChatMessageHistory),通过 langchain_community.chat_message_histories 中的官方实现,只需更换 get_history 返回的对象即可,业务代码零改动。
Q:FileChatMessageHistory 存在并发写入问题吗?
A:是的,多线程/多进程同时写入同一文件会导致数据损坏。在单机单进程环境下可行;若需并发,应加锁或改用支持并发的数据库后端。