LangChain 进阶历史对话管理

自动历史管理

前面的示例将消息显式地传递给链。这是一种完全可接受的方法,但确实需要外部管理新消息。LangChain还包括一个名为RunnableWithMessageHistory的包裹器,能够自动处理这个过程。

为了展示其工作原理,我们稍微修改上面的提示,增加一个最终输入变量,该变量在聊天历史记录之后填充一个HumanMessage模板。这意味着我们将需要一个chat_history参数,该参数包含当前消息之前的所有消息,而不是所有消息。



from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
import os

os.environ["OPENAI_API_KEY"] = "not empty"

chat = ChatOpenAI(model="qwen1.5-32b-chat-32k", temperature="0",openai_api_base='http://127.0.0.1:9997/v1')

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "You are a helpful assistant. Answer all questions to the best of your ability.",
        ),
        MessagesPlaceholder(variable_name="chat_history"),
        ("human", "{input}"),
    ]
)

chain = prompt | chat

我们将最新的输入传递到这里的对话,让RunnableWithMessageHistory类来包装我们的链,并将该输入变量附加到聊天记录中。

接下来,让我们声明我们包装后的链:

from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain.memory import ChatMessageHistory

demo_ephemeral_chat_history_for_chain = ChatMessageHistory()

chain_with_message_history = RunnableWithMessageHistory(
    chain,
    lambda session_id: demo_ephemeral_chat_history_for_chain,
    input_messages_key="input",
    history_messages_key="chat_history",
)

此类除了我们想要包装的链之外,还接受几个参数:

  1. 一个工厂函数,它返回给定会话ID的消息历史记录。这样,您的链就可以通过加载不同对话的不同消息来同时处理多个用户。
  2. 一个 input_messages_key,用于指定输入的哪个部分应该在聊天历史中被跟踪和存储。在此示例中,我们希望跟踪作为输入传递的字符串。
  3. 一个 history_messages_key,用于指定以前的消息应如何注入到提示中。我们的提示中有一个名为 chat_history 的 MessagesPlaceholder,因此我们指定此属性以匹配。
  4. (对于有多个输出的链)一个 output_messages_key,指定要将哪个输出存储为历史记录。这是 input_messages_key 的反向。

我们可以像往常一样调用这个新链,增加一个可配置字段来指定传递给工厂函数的特定 session_id。这在演示中未使用,但在实际的链中,您会希望返回与传递的会话对应的聊天历史记录。

chain_with_message_history.invoke(
    {"input": "Translate this sentence from English to French: I love programming."},
    {"configurable": {"session_id": "unused"}},
)
Parent run ad0848e5-75f1-456f-9567-be6069e64bd2 not found for run e55eb095-0db5-48d0-81cf-4394858a30f7. Treating as a root run.





AIMessage(content="J'aime programmer.", response_metadata={'token_usage': {'completion_tokens': 6, 'prompt_tokens': 41, 'total_tokens': 47}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-506154fb-49bc-42d4-b7bd-edad6bccf857-0')
chain_with_message_history.invoke(
    {"input": "What did I just ask you?"}, {"configurable": {"session_id": "unused"}}
)
Parent run 886f51d5-53d1-4ad4-9a75-692ea38a9d36 not found for run c258fa0e-690e-4644-8188-e3fc5b328f13. Treating as a root run.





AIMessage(content='You asked me to translate the sentence "I love programming" from English to French.', response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 63, 'total_tokens': 81}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-fba286a0-af0a-4b44-9638-2d6bbed4d5f2-0')
demo_ephemeral_chat_history_for_chain
InMemoryChatMessageHistory(messages=[HumanMessage(content='Translate this sentence from English to French: I love programming.'), AIMessage(content="J'aime programmer.", response_metadata={'token_usage': {'completion_tokens': 6, 'prompt_tokens': 41, 'total_tokens': 47}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-506154fb-49bc-42d4-b7bd-edad6bccf857-0'), HumanMessage(content='What did I just ask you?'), AIMessage(content='You asked me to translate the sentence "I love programming" from English to French.', response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 63, 'total_tokens': 81}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-fba286a0-af0a-4b44-9638-2d6bbed4d5f2-0')])

修改聊天记录

修改存储的聊天消息可以帮助您的聊天机器人处理各种情况。以下是一些例子:

修剪消息

大型语言模型和聊天模型具有有限的上下文窗口,即使您没有直接达到限制,您可能也希望限制模型需要处理的干扰量。一个解决方案是仅加载和存储最近的n条消息。让我们使用一个带有一些预加载消息的示例历史记录:

demo_ephemeral_chat_history = ChatMessageHistory()

demo_ephemeral_chat_history.add_user_message("Hey there! I'm Nemo.")
demo_ephemeral_chat_history.add_ai_message("Hello!")
demo_ephemeral_chat_history.add_user_message("How are you today?")
demo_ephemeral_chat_history.add_ai_message("Fine thanks!")

demo_ephemeral_chat_history.messages
[HumanMessage(content="Hey there! I'm Nemo."),
 AIMessage(content='Hello!'),
 HumanMessage(content='How are you today?'),
 AIMessage(content='Fine thanks!')]

让我们使用上述声明的RunnableWithMessageHistory链中的这段消息历史记录:

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "You are a helpful assistant. Answer all questions to the best of your ability.",
        ),
        MessagesPlaceholder(variable_name="chat_history"),
        ("human", "{input}"),
    ]
)

chain = prompt | chat

chain_with_message_history = RunnableWithMessageHistory(
    chain,
    lambda session_id: demo_ephemeral_chat_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)

chain_with_message_history.invoke(
    {"input": "What's my name?"},
    {"configurable": {"session_id": "unused"}},
)
Parent run ee3a1347-5491-480f-baf2-0083437a53cb not found for run 2ccf1c84-41d1-4d43-bd20-435e758a92b3. Treating as a root run.





AIMessage(content='Your name is Nemo.', response_metadata={'token_usage': {'completion_tokens': 7, 'prompt_tokens': 72, 'total_tokens': 79}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-341d0aaf-67e6-4e10-a4d2-67062031cdf3-0')

我们可以看到链条记住了预加载的名称。

但是假设我们的上下文窗口非常小,我们希望将传递给链条的消息数量修剪到仅最近的两条。我们可以使用clear方法删除消息并将它们重新添加到历史记录中。我们不必这样做,但让我们将此方法放在链条的前面,以确保它始终被调用:

from langchain_core.runnables import RunnablePassthrough

# 当消息大于两条时,清空旧历史记录,然后把切片旧历史记录的备份添加到旧历史记录变量中
def trim_messages(chain_input):
    stored_messages = demo_ephemeral_chat_history.messages
    if len(stored_messages) <= 2:
        return False

    demo_ephemeral_chat_history.clear()

    for message in stored_messages[-2:]:
        demo_ephemeral_chat_history.add_message(message)

    return True


chain_with_trimming = (
    RunnablePassthrough.assign(messages_trimmed=trim_messages)
    | chain_with_message_history
)

让我们调用这个新链并稍后检查消息:

chain_with_trimming.invoke(
    {"input": "Where does P. Sherman live?"},
    {"configurable": {"session_id": "unused"}},
)
Parent run fb2a6eda-cd48-40de-a364-f260041da762 not found for run b6d8b077-4448-4268-a3bc-858503c6f208. Treating as a root run.





AIMessage(content='P. Sherman lives in the fictional city of Sydney, Australia, in the underwater world of the Great Barrier Reef. He is the main character in the 2003 Pixar animated film "Finding Nemo," and his full address is 42 Wallaby Way, Sydney.', response_metadata={'token_usage': {'completion_tokens': 58, 'prompt_tokens': 57, 'total_tokens': 115}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-7679d7f6-6b9a-48c3-9f07-a6983c25e797-0')
demo_ephemeral_chat_history.messages

[HumanMessage(content="What's my name?"),
 AIMessage(content='Your name is Nemo.', response_metadata={'token_usage': {'completion_tokens': 7, 'prompt_tokens': 72, 'total_tokens': 79}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-341d0aaf-67e6-4e10-a4d2-67062031cdf3-0'),
 HumanMessage(content='Where does P. Sherman live?'),
 AIMessage(content='P. Sherman lives in the fictional city of Sydney, Australia, in the underwater world of the Great Barrier Reef. He is the main character in the 2003 Pixar animated film "Finding Nemo," and his full address is 42 Wallaby Way, Sydney.', response_metadata={'token_usage': {'completion_tokens': 58, 'prompt_tokens': 57, 'total_tokens': 115}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-7679d7f6-6b9a-48c3-9f07-a6983c25e797-0')]

我们可以看到,我们的历史删除了两条最古老的消息,同时仍在最后添加了最近的对话。下次调用该链时,将再次调用trim_mailings,并且只将最近的两条消息传递给模型。在这种情况下,这意味着模型在下次调用时会忘记我们给它命名的名称:

chain_with_trimming.invoke(
    {"input": "What is my name?"},
    {"configurable": {"session_id": "unused"}},
)
Parent run c1f79b96-555c-4263-bb89-1306982a1cb7 not found for run 355c774d-7ed8-4879-ad8a-5970911db504. Treating as a root run.





AIMessage(content="I'm sorry, but as an AI language model, I don't have access to personal information like your name. Only you know your name, or if someone has told it to me in our previous conversation, I would not remember it for privacy reasons. If you'd like, you can tell me your name, and I'll be happy to address you by it.", response_metadata={'token_usage': {'completion_tokens': 75, 'prompt_tokens': 108, 'total_tokens': 183}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-a9804ef3-10ac-4c9a-8a83-88414515a3b8-0')
demo_ephemeral_chat_history.messages


[HumanMessage(content='Where does P. Sherman live?'),
 AIMessage(content='P. Sherman lives in the fictional city of Sydney, Australia, in the underwater world of the Great Barrier Reef. He is the main character in the 2003 Pixar animated film "Finding Nemo," and his full address is 42 Wallaby Way, Sydney.', response_metadata={'token_usage': {'completion_tokens': 58, 'prompt_tokens': 57, 'total_tokens': 115}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-7679d7f6-6b9a-48c3-9f07-a6983c25e797-0'),
 HumanMessage(content='What is my name?'),
 AIMessage(content="I'm sorry, but as an AI language model, I don't have access to personal information like your name. Only you know your name, or if someone has told it to me in our previous conversation, I would not remember it for privacy reasons. If you'd like, you can tell me your name, and I'll be happy to address you by it.", response_metadata={'token_usage': {'completion_tokens': 75, 'prompt_tokens': 108, 'total_tokens': 183}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-a9804ef3-10ac-4c9a-8a83-88414515a3b8-0')]

摘要记忆

我们也可以以其他方式使用相同的模式。例如,我们可以在调用我们的链之前使用额外的LLM调用来生成对话摘要。让我们重新创建我们的聊天历史记录和聊天机器人链:

demo_ephemeral_chat_history = ChatMessageHistory()

demo_ephemeral_chat_history.add_user_message("我是BigLee ,你好")
demo_ephemeral_chat_history.add_ai_message("Hello!")
demo_ephemeral_chat_history.add_user_message("你吃饭了吗?")
demo_ephemeral_chat_history.add_ai_message("作为AI模型,我不会吃饭")

demo_ephemeral_chat_history.messages# 
[HumanMessage(content='我是BigLee ,你好'),
 AIMessage(content='Hello!'),
 HumanMessage(content='你吃饭了吗?'),
 AIMessage(content='作为AI模型,我不会吃饭')]

我们将稍微修改提示,让LLM知道将收到精简摘要而不是聊天历史记录:

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "You are a helpful assistant. Answer all questions to the best of your ability. The provided chat history includes facts about the user you are speaking with.",
        ),
        MessagesPlaceholder(variable_name="chat_history"),
        ("user", "{input}"),
    ]
)

chain = prompt | chat

chain_with_message_history = RunnableWithMessageHistory(
    chain,
    lambda session_id: demo_ephemeral_chat_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)

现在,让我们创建一个函数,将之前的交互提炼成摘要。我们也可以将这个添加到链的前面

def summarize_messages(chain_input):
    stored_messages = demo_ephemeral_chat_history.messages
    if len(stored_messages) == 0:
        return False
    summarization_prompt = ChatPromptTemplate.from_messages(
        [
            MessagesPlaceholder(variable_name="chat_history"),
            (
                "user",
                "Distill the above chat messages into a single summary message. Include as many specific details as you can.",
            ),
        ]
    )
    summarization_chain = summarization_prompt | chat

    summary_message = summarization_chain.invoke({"chat_history": stored_messages})

    demo_ephemeral_chat_history.clear()

    demo_ephemeral_chat_history.add_message(summary_message)

    return True


chain_with_summarization = (
    RunnablePassthrough.assign(messages_summarized=summarize_messages)
    | chain_with_message_history
)

让我们看看它是否记得我们给它起的名字:

chain_with_summarization.invoke(
    {"input": "我叫什么?"},
    {"configurable": {"session_id": "unused"}},
)
Parent run d1f228cc-e496-48e6-83b4-f573bcdda4fb not found for run 6af95504-045b-46cc-8d94-f60e6e5da12f. Treating as a root run.





AIMessage(content='您没有直接提到您的名字,您在对话中自称为"BigLee"。如果您想告诉我您的真实名字,我很乐意称呼您。', response_metadata={'token_usage': {'completion_tokens': 31, 'prompt_tokens': 106, 'total_tokens': 137}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-0116bec1-c38c-44f1-945f-fc71bc8ebc72-0')
demo_ephemeral_chat_history.messages


[AIMessage(content="User BigLee greeted and asked if I had eaten, to which I replied that, as an AI model, I don't consume food.", response_metadata={'token_usage': {'completion_tokens': 29, 'prompt_tokens': 78, 'total_tokens': 107}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-75f8db97-b0cc-420a-83c3-b5a8725a6196-0'),
 HumanMessage(content='What did I say my name was?'),
 AIMessage(content='In the provided chat history, you didn\'t explicitly mention your name. You greeted me as "BigLee," so I\'ve been addressing you as BigLee. If you would like to share your actual name, please feel free to do so.', response_metadata={'token_usage': {'completion_tokens': 50, 'prompt_tokens': 84, 'total_tokens': 134}, 'model_name': 'qwen1.5-32b-chat-32k', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-3978c887-ae2e-41ff-b331-bae06eca4207-0')]

posted @ 2024-05-15 17:55    阅读(582)  评论(0编辑  收藏  举报