LangChain 進階歷史對話管理

發表於2024-05-15

自動歷史管理

前面的示例將訊息顯式地傳遞給鏈。這是一種完全可接受的方法,但確實需要外部管理新訊息。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')]

相關文章