LangChain的History的使用#

History是LangChain提供了一个保存对话历史的功能,LangChain的社区有很多的实现,他们都是基于BaseChatMessageHistory子类来拓展的,LangChain在langchain_core包中只实现了InMemoryChatMessageHistory。 社区提供的如下:

LangChain中memory和History的主要区别是:

  • Memory是用于在Chain的执行过程中存储和加载状态信息的抽象接口。

  • History是用于存储和管理对话历史消息的抽象接口。

  • memory是短时间的记忆,History是长时间的记忆。

InMemoryChatMessageHistory#

在内存中记录的对话历史

from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
import langchain

langchain.debug=True

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "你是一个ai助手,基于下面的对话历史来回答问题"),
        ("placeholder", "{chat_history}"),
        ("human", "{input}"),
    ]
)

history = InMemoryChatMessageHistory()

chain = prompt | ChatOpenAI() | StrOutputParser()

wrapped_chain = RunnableWithMessageHistory(chain, lambda x: history)
res = wrapped_chain.invoke(
{"input": "你是谁"},
config={"configurable": {"session_id": "42"}})
wrapped_chain.invoke({"input": "我刚才问你什么了?"},config={"configurable": {"session_id": "42"}})
history.messages

RedisChatMessageHistory#

数据存放在redis中,需要引入redis,需要在本地先启动redis RedisChatMessageHistory在构建的时候需要链接信息,默认是redis://localhost:6379/0,key的前缀是message_store,存储的数据类型是list

from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
import langchain
langchain.debug=True

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "你是一个ai助手,基于下面的对话历史来回答问题"),
        ("placeholder", "{chat_history}"),
        ("human", "{input}"),
    ]
)
history = RedisChatMessageHistory(session_id="42")
history.clear()
history.messages
chain = prompt | ChatOpenAI() | StrOutputParser()

wrapped_chain = RunnableWithMessageHistory(chain, lambda x: history)
res = wrapped_chain.invoke(
{"input": "你是谁"},
config={"configurable": {"session_id": "42"}})
history.messages
wrapped_chain.invoke({"input": "我刚才问你什么了?"},config={"configurable": {"session_id": "42"}})
history.messages
history.redis_client.type(history.key)

与此类似的有MongoDBChatMessageHistoryElasticsearchChatMessageHistory,SQLChatMessageHistorySQLChatMessageHistory是将数据存储在sql数据库中。

Memory和History同时使用#

Memory中使用History#

Memory中底层实现就是History,可以在构造Memory的时候指定chat_Memory,默认是InMemoryChatMessageHistory

from langchain.memory import ConversationBufferWindowMemory
from langchain.chains import ConversationChain
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

template = """
你是一个ai助手,叫做小李,你需要根据对话历史来回答用户问题.
Chat history: {history}
Question: {input}
"""

prompt = ChatPromptTemplate.from_template(template)

history = RedisChatMessageHistory(session_id="42")
history.clear()
memory = ConversationBufferWindowMemory(chat_memory=history,k=2)

chain = ConversationChain(
    llm=ChatOpenAI(),
    memory=memory,
    prompt=prompt,
)

chain.invoke({"input":"你是谁?"})
chain.invoke({"input":"你知道1+1等于?"})

redis中看数据 key存在了,数据已经存在了,在进件多轮对话看redis中结果

chain.invoke({"input":"你知道1+2等于?"})
chain.invoke({"input":"你知道1+3于?"})
chain.invoke({"input":"我问你的第一个问题是什么?"})

redis中数据如下: 怎么看起来不是预期的,预期是保存4条对话记录,也就是2轮对话。但是redis中全部都保存起来了。从上面的执行过程中可以看到,确实传递给llm的是最后两轮对话,是预期的。翻找代码发现 Memory中对对话历史做处理是不影响History的,History会记录完整的对话历史,Memory会对History中的message做加工 ,具体可以看langchain.memory.chat_memory.BaseChatMemory.save_context

上面的例子是Memory中套入了History,并且使用的是老的LangChain的写法,不是LCEL。 LCEL使用的是RunnableWithMessageHistory来包装chain,他需要返回的是一个BaseChatMessageHistory的子类,看起来Memory这一套并不适用,需要自己在包装一下,在返回History的时候使用Memory来处理一下。

History中使用Memory(使用LCEL)#

不兼容,需要手动做兼容 原因如下

  1. 在Memory的实现中,是通过History的add_message的方式来完成的。这个接口的入参就一个 message,但是Memory对外暴漏的接口是save_context,每次都要求传递两个参数,input,output,这里就不能兼容,需要有一种方式来判断本次添加的message是那种,在我的代码中我是通过switch来做的。

  2. 需要处理数据首次加载的问题,先从Memory中获取数据,如果没有,在从History中获取,最后数据回灌到History。

下面的代码没有跑动

import json
import logging
from typing import List, Optional

from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import (
    BaseMessage,
    message_to_dict,
    messages_from_dict,
)

from langchain_community.utilities.redis import get_client

logger = logging.getLogger(__name__)


class RedisConversationBufferHistory(RedisChatMessageHistory):
    """Chat message history stored in a Redis database. use ConversationBufferMemory"""

    def __init__(
        self,
        **kwargs
    ):
        super().__init__(**kwargs)
        self.memory = ConversationBufferWindowMemory(k=2)
        self.switch = True
        

    def change_switch(self):
        self.switch = !self.switch
        
    def is_open_switch(self):
        return self.switch

    @property
    def messages(self) -> List[BaseMessage]:
        return self.memory.buffer_as_messages

    def add_message(self, message: BaseMessage) -> None:
        """Append the message to the record in Redis"""
        if len(message.content) > 0:
            self.redis_client.lpush(self.key, json.dumps(message_to_dict(message)))
        if self.ttl:
            self.redis_client.expire(self.key, self.ttl)
        
        if self.is_open_switch():
            self.memory.save_context(message,"")
        else:
            self.memory.save_context("",message)
        

    def clear(self) -> None:
        super().clear()
        self.memory.clear()


from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
import langchain
langchain.debug=True

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "你是一个ai助手,基于下面的对话历史来回答问题"),
        ("placeholder", "{chat_history}"),
        ("human", "{input}"),
    ]
)
chain = prompt | ChatOpenAI() | StrOutputParser()

history = RedisConversationBufferHistory(session_id="43")
history.clear()
wrapped_chain = RunnableWithMessageHistory(chain,lambda x: history)
res = wrapped_chain.invoke(
{"input": "你是谁"},
config={"configurable": {"session_id": "43"}})
res = wrapped_chain.invoke(
{"input": "hello"},
config={"configurable": {"session_id": "42"}})

end end end