35.RAG開發(fā)-22-Memory長期會話記憶

memory 長期會話記憶

FileChatMessageHistory類實(shí)現(xiàn),核心思路:

  • 基于文件存儲會話記錄,以session_id為文件名,不同session_id有不同文件存儲消息

繼承BaseChatMessageHistory實(shí)現(xiàn)如下3個(gè)方法:

  • add_messages:同步模式,添加消息
  • messages:同步模式,獲取消息
  • clear: 同步模式,清除消息
# 導(dǎo)入必要的庫和模塊
import json  # 用于JSON序列化和反序列化
import os    # 用于文件和目錄操作
from typing import Sequence  # 用于類型注解

# 從LangChain核心模塊導(dǎo)入必要的組件
from langchain_core.messages import (
    messages_from_dict,  # 將字典列表轉(zhuǎn)換為消息對象列表
    message_to_dict,     # 將消息對象轉(zhuǎn)換為字典
    BaseMessage         # 消息基類
)
from langchain_core.chat_history import BaseChatMessageHistory  # 聊天歷史基類
from langchain_core.runnables.history import RunnableWithMessageHistory  # 帶消息歷史的可運(yùn)行對象
from langchain_community.chat_models.tongyi import ChatTongyi  # 通義千問模型
from langchain_core.output_parsers import StrOutputParser  # 字符串輸出解析器
from langchain_core.prompts import MessagesPlaceholder, ChatPromptTemplate  # 提示模板相關(guān)


# 消息轉(zhuǎn)換函數(shù)說明:
# message_to_dict:單個(gè)消息對象(BaseMessage類實(shí)例)→ 字典
# messages_from_dict:[字典、字典...] → [消息、消息...]
# AIMessage、HumanMessage、SystemMessage都是BaseMessage的子類


class FileChatMessageHistory(BaseChatMessageHistory):
    """
    基于文件的聊天消息歷史記錄類
    
    該類繼承自BaseChatMessageHistory,實(shí)現(xiàn)了基于文件系統(tǒng)的消息歷史存儲
    每個(gè)會話的消息歷史會被存儲到對應(yīng)的文件中,支持消息的添加、獲取和清空操作
    """
    
    def __init__(self, storage_path: str, session_id: str) -> None:
        """
        初始化FileChatMessageHistory實(shí)例
        
        Args:
            storage_path: 存儲路徑,用于存放會話歷史文件
            session_id: 會話ID,用于唯一標(biāo)識一個(gè)會話
        """
        self.storage_path = storage_path
        self.session_id = session_id

        # 構(gòu)建文件路徑:存儲路徑 + 會話ID
        self.file_path = os.path.join(self.storage_path, self.session_id)

        # 確保存儲路徑存在,如果不存在則創(chuàng)建
        os.makedirs(os.path.dirname(self.file_path), exist_ok=True)

    @property
    def messages(self) -> list[BaseMessage]:
        """
        獲取當(dāng)前會話的所有消息列表
        
        Returns:
            list[BaseMessage]: 消息對象列表,如果文件不存在則返回空列表
        """
        try:
            # 打開文件并讀取消息數(shù)據(jù)
            with open(
                self.file_path,
                "r",
                encoding="utf-8"
            ) as f:
                messages_data = json.load(f)
                # 將字典列表轉(zhuǎn)換為消息對象列表
                return messages_from_dict(messages_data)
        except FileNotFoundError:
            # 如果文件不存在,返回空列表
            return []

    def add_messages(self, messages: Sequence[BaseMessage]) -> None:
        """
        添加消息到歷史記錄
        
        Args:
            messages: 要添加的消息序列
        """
        # 獲取當(dāng)前消息列表
        all_messages = list(self.messages)
        # 添加新消息
        all_messages.extend(messages)

        # 序列化消息:將消息對象列表轉(zhuǎn)換為字典列表
        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)

    def clear(self) -> None:
        """
        清空消息歷史記錄
        """
        # 寫入空列表,清空文件內(nèi)容
        with open(self.file_path, "w", encoding="utf-8") as f:
            json.dump([], f)


def print_prompt(full_prompt):
    """
    打印完整的提示信息
    
    Args:
        full_prompt: 完整的提示對象
    
    Returns:
        full_prompt: 原樣返回提示對象,用于鏈?zhǔn)秸{(diào)用
    """
    # 打印提示信息,前后用等號包圍以增強(qiáng)可讀性
    print("="*20, full_prompt.to_string(), "="*20)
    return full_prompt


# 初始化通義千問模型
model = ChatTongyi(model="qwen3-max")

# 定義提示模板,包含對話歷史和用戶輸入
# 注釋掉的是使用PromptTemplate的方式
# prompt = PromptTemplate.from_template(
#     "你需要根據(jù)對話歷史回應(yīng)用戶問題,對話歷史:{chat_history}\n用戶當(dāng)前輸入:{input}, 請給出回應(yīng)"
# )

# 使用ChatPromptTemplate構(gòu)建提示模板
prompt = ChatPromptTemplate.from_messages([
    ("system", "你需要根據(jù)對話歷史回應(yīng)用戶問題,對話歷史:"),
    MessagesPlaceholder(variable_name="chat_history"),  # 消息歷史占位符
    ("human", "請回答如下問題:{input}"),  # 用戶輸入占位符
])

# 構(gòu)建基礎(chǔ)鏈:提示模板 → 打印提示 → 模型 → 輸出解析
base_chain = prompt | print_prompt | model | StrOutputParser()

# 會話歷史存儲字典(當(dāng)前未使用,保留以備擴(kuò)展)
chat_history_store = {}


def get_history(session_id):
    """
    獲取指定會話ID的歷史記錄對象
    
    Args:
        session_id: 會話ID
    
    Returns:
        FileChatMessageHistory: 會話歷史對象
    """
    return FileChatMessageHistory("./chat_history", session_id)


# 創(chuàng)建帶有消息歷史功能的對話鏈
conversation_chain = RunnableWithMessageHistory(
    base_chain,          # 基礎(chǔ)鏈對象
    get_history,         # 獲取歷史會話的函數(shù)
    input_messages_key="input",       # 輸入消息的鍵名
    history_messages_key="chat_history"  # 歷史消息的鍵名
)


if __name__ == "__main__":
    """
    主函數(shù),測試對話鏈的會話記憶功能
    """
    # 固定格式,添加LangChain的配置,為當(dāng)前程序配置所屬的session_id
    session_config = {"configurable": {"session_id": "user_001"}}
    
    # # 測試對話:詢問小明的貓
    # res = conversation_chain.invoke({"input": "小明有一只貓"}, config=session_config)
    # print("第一次執(zhí)行:", res)
    
    # # 測試對話:詢問小剛的狗
    # res = conversation_chain.invoke({"input": "小剛有兩只狗"}, config=session_config)
    # print("第二次執(zhí)行:", res)
    
    # 測試對話:基于歷史詢問總共有幾只寵物
    res = conversation_chain.invoke({"input": "共有幾只寵物?"}, config=session_config)
    print("第三次執(zhí)行:", res)

==================== System: 你需要根據(jù)對話歷史回應(yīng)用戶問題,對話歷史:
Human: 請回答如下問題:小明有一只貓 ====================
第一次執(zhí)行: 小明有一只貓。請問您想了解關(guān)于這只貓的更多信息,還是有其他問題需要解答呢?
==================== System: 你需要根據(jù)對話歷史回應(yīng)用戶問題,對話歷史:
Human: 小明有一只貓
AI: 小明有一只貓。請問您想了解關(guān)于這只貓的更多信息,還是有其他問題需要解答呢?
Human: 請回答如下問題:小剛有兩只狗 ====================
第二次執(zhí)行: 小剛有兩只狗。  
如果您有進(jìn)一步的問題,比如關(guān)于小剛的狗、小明的貓,或者他們之間的關(guān)系等,請隨時(shí)告訴我!
==================== System: 你需要根據(jù)對話歷史回應(yīng)用戶問題,對話歷史:
Human: 小明有一只貓
AI: 小明有一只貓。請問您想了解關(guān)于這只貓的更多信息,還是有其他問題需要解答呢?
Human: 小剛有兩只狗
AI: 小剛有兩只狗。
如果您有進(jìn)一步的問題,比如關(guān)于小剛的狗、小明的貓,或者他們之間的關(guān)系等,請隨時(shí)告訴我!
Human: 請回答如下問題:共有幾只寵物? ====================
第三次執(zhí)行: 小明有1只貓,小剛有2只狗。  
所以他們一共有:
1 + 2 = **3只寵物**。

實(shí)現(xiàn)自定義 FileChatMessageHistory:會話歷史持久化到本地文件

核心內(nèi)容

  • 通過自定義 FileChatMessageHistory 類,將聊天會話歷史持久化到本地 JSON 文件。
  • 解決程序重啟后歷史記錄丟失的問題。

一、核心概念對比

聊天歷史存儲方式對比

特性 InMemoryChatMessageHistory (臨時(shí)記憶) FileChatMessageHistory (長期記憶)
存儲位置 內(nèi)存 本地文件 (JSON格式)
生命周期 程序重啟后丟失 永久保存,重啟不丟失
適用場景 測試、開發(fā)調(diào)試 生產(chǎn)環(huán)境、需要持久化的應(yīng)用
實(shí)現(xiàn)方式 LangChain 內(nèi)置類 需繼承 BaseChatMessageHistory 自定義實(shí)現(xiàn)

核心區(qū)別

  • 臨時(shí)記憶:速度快但易失,適合開發(fā)調(diào)試。
  • 長期記憶:數(shù)據(jù)持久化,適合用戶對話歷史的保存。
概念 功能
BaseChatMessageHistory LangChain 聊天歷史記錄的基類,定義接口
messages_to_dict() / messages_from_dict() LangChain 提供的消息序列化與反序列化工具
RunnableWithMessageHistory LangChain 封裝類,自動管理歷史記錄的注入與保存
session_id 會話唯一標(biāo)識,用于區(qū)分不同用戶或會話的歷史記錄

二、代碼實(shí)現(xiàn):自定義 FileChatMessageHistory

1. 導(dǎo)入必要的模塊

import json
import os
from typing import List, Sequence
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import BaseMessage, messages_from_dict, messages_to_dict

說明

  • BaseChatMessageHistory:所有聊天歷史記錄類的基類。
  • messages_from_dict / messages_to_dict:消息序列化與反序列化的工具函數(shù)。

2. 實(shí)現(xiàn)自定義類

class FileChatMessageHistory(BaseChatMessageHistory):
    def __init__(self, file_path: str):
        self.file_path = file_path

    @property
    def messages(self) -> List[BaseMessage]:
        """獲取歷史消息列表"""
        # 如果文件不存在,返回空列表
        if not os.path.exists(self.file_path):
            return []
        # 讀取文件內(nèi)容
        with open(self.file_path, 'r', encoding='utf-8') as f:
            messages_data = json.load(f)
        # 將字典列表轉(zhuǎn)換為消息對象列表
        return messages_from_dict(messages_data)

    def add_messages(self, messages: Sequence[BaseMessage]) -> None:
        """添加消息到歷史記錄"""
        # 獲取現(xiàn)有消息
        existing_messages = self.messages
        # 合并新消息
        all_messages = existing_messages + list(messages)
        # 將消息對象列表轉(zhuǎn)換為可序列化的字典列表
        messages_data = messages_to_dict(all_messages)
        # 寫入文件
        with open(self.file_path, 'w', encoding='utf-8') as f:
            json.dump(messages_data, f, ensure_ascii=False, indent=4)

    def clear(self) -> None:
        """清空歷史記錄"""
        # 直接寫入空列表即可
        with open(self.file_path, 'w', encoding='utf-8') as f:
            json.dump([], f)

關(guān)鍵點(diǎn)解析

  • messages 屬性:使用 @property 裝飾器,訪問時(shí)自動觸發(fā)讀取文件邏輯。
  • add_messages 方法:必須實(shí)現(xiàn),RunnableWithMessageHistory 會自動調(diào)用此方法保存新對話。
  • 序列化邏輯
    • 存儲時(shí):BaseMessage 對象 → dict → JSON 字符串。
    • 讀取時(shí):JSON 字符串 → dictBaseMessage 對象。

三、代碼實(shí)現(xiàn):歷史記錄管理函數(shù)

1. 定義存儲路徑與緩存字典

# 定義歷史記錄存儲目錄
history_dir = "chat_history"
os.makedirs(history_dir, exist_ok=True)

# 用于緩存歷史記錄實(shí)例的字典
history_cache = {}

2. 定義獲取歷史記錄的函數(shù)

def get_session_history(session_id: str) -> BaseChatMessageHistory:
    """
    根據(jù) session_id 獲取對應(yīng)的歷史記錄實(shí)例
    """
    # 構(gòu)建文件路徑:chat_history/{session_id}.json
    file_path = os.path.join(history_dir, f"{session_id}.json")
    # 如果該 session_id 的實(shí)例未創(chuàng)建,則創(chuàng)建并緩存
    if session_id not in history_cache:
        history_cache[session_id] = FileChatMessageHistory(file_path)
    return history_cache[session_id]

說明

  • session_id:會話唯一標(biāo)識符(如用戶ID、會話ID)。
  • history_cache:緩存字典,避免重復(fù)創(chuàng)建對象。

四、代碼實(shí)現(xiàn):構(gòu)建帶長期記憶的對話鏈

1. 初始化模型與提示詞模板

from langchain_community.chat_models import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory

# 初始化模型
model = ChatTongyi(model="qwen-plus")

# 定義提示詞模板
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一個(gè)樂于助人的AI助手。"),
    MessagesPlaceholder(variable_name="history"),  # 歷史消息占位符
    ("human="{input}")                                  # 用戶輸入占位符
])

關(guān)鍵點(diǎn)

  • MessagesPlaceholder:必須使用此占位符,LangChain 會自動填充歷史消息。
  • 變量名:historyinput 需保持一致。

2. 構(gòu)建基礎(chǔ)鏈與包裝鏈

# 構(gòu)建基礎(chǔ)鏈(提示詞 → 模型)
base_chain = prompt | model

# 包裝為帶歷史記錄的鏈
chain_with_history = RunnableWithMessageHistory(
    base_chain,
    get_session_history,         # 歷史記錄獲取函數(shù)
    input_messages_key="input",  # 輸入消息的鍵名
    history_messages_key="history"  # 歷史消息的鍵名
)

參數(shù)詳解

參數(shù) 說明
base_chain 被包裝的原始 LangChain 鏈
get_session_history 指定如何獲取歷史記錄對象
input_messages_key 用戶輸入在字典中的 key
history_messages_key 歷史消息在提示詞模板中的 key

五、代碼實(shí)現(xiàn):測試長期記憶效果

1. 第一輪對話

# 配置 session_id
config = {"configurable": {"session_id": "user_001"}}

# 發(fā)起對話
response1 = chain_with_history.invoke(
    {"input": "你好,我是小明,我喜歡打籃球。"},
    config=config
)
print(f"AI回復(fù): {response1.content}")

后臺動作

  1. LangChain 調(diào)用 get_session_history("user_001")
  2. 讀取文件 chat_history/user_001.json(此時(shí)為空)
  3. 將用戶輸入填入模板,調(diào)用模型
  4. 將用戶消息和 AI 回復(fù)寫入文件

2. 第二輪對話(驗(yàn)證記憶)

response2 = chain_with_history.invoke(
    {"input": "我剛才說我喜歡什么運(yùn)動?"},
    config=config
)
print(f"AI回復(fù): {response2.content}")
# 輸出: 你剛才說你喜歡打籃球。

3. 驗(yàn)證程序重啟后的記憶

# 模擬程序重啟:清空緩存,重新創(chuàng)建對象
history_cache.clear()

# 再次提問,依然能記住之前的對話
response3 = chain_with_history.invoke(
    {"input": "我是誰?"},
    config=config
)
print(f"AI回復(fù): {response3.content}")
# 輸出: 你是小明。

六、總結(jié)

要點(diǎn) 說明
核心邏輯 繼承 BaseChatMessageHistory 實(shí)現(xiàn)自定義存儲后端
關(guān)鍵組件 RunnableWithMessageHistory 自動化歷史記錄的注入與保存
實(shí)現(xiàn)流程 1. 定義存儲后端類
2. 定義獲取歷史函數(shù)
3. 構(gòu)建 RunnableWithMessageHistory
4. 調(diào)用時(shí)傳入 session_id
生產(chǎn)擴(kuò)展 將文件存儲替換為 Redis、MySQL 等數(shù)據(jù)庫,只需修改 FileChatMessageHistory 的實(shí)現(xiàn)邏輯
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容