概览

我们将介绍一个如何设计和实现LLM驱动的聊天机器人的示例。这个聊天机器人将能够进行对话并记住以前的交互。

请注意,我们构建的聊天机器人只使用语言模型进行对话。您可能还在寻找其他几个相关概念:

  • 对话式RAG:在外部数据源上实现聊天机器人体验
  • 代理:构建可以执行操作的聊天机器人

本教程将介绍基础知识,对于这两个更高级的主题将会有所帮助。

概念

以下是我们将要使用的一些高级组件:

  • Chat Models : 聊天机器人界面是基于消息而不是原始文本的,因此最适合使用 Chat Models 而不是文本LLMs。
  • Prompt Templates : 简化了组装提示的过程,包括默认消息、用户输入、聊天历史和(可选)额外的检索上下文。
  • Chat History : 允许聊天机器人“记住”过去的交互,并在回答后续问题时考虑它们。
  • 使用 LangSmith 调试和跟踪应用程序。

我们将介绍如何将上述组件组合在一起,创建一个强大的对话式聊天机器人。

安装

参考前面文档: 《无限可能LangChain——开启大模型世界》 《无限可能LangChain——构建一个简单的LLM应用程序》

快速入门

首先,让我们直接使用模型。ChatModels 是LangChain 的“可运行”实例,这意味着它们提供了一个与之交互的标准接口。为了简单调用模型,我们可以将一个消息列表传递给 .invoke 方法。

from langchain_community.llms import Tongyi
from langchain_core.messages import HumanMessage

# 使用 Tongyi LLM,并设置温度为 1,代表模型会更加随机,但也会更加不确定
llm = Tongyi(temperature=1)

# 将一个消息列表传递给 .invoke 方法
result = llm.invoke([HumanMessage(content="你好,我是小明")])
print(result)

API Reference:HumanMessage

运行结果: image.png

但是模型本身没有任何状态概念。例如,如果您提出一个后续问题:

from langchain_community.llms import Tongyi
from langchain_core.messages import HumanMessage

# 使用 Tongyi LLM,并设置温度为 1,代表模型会更加随机,但也会更加不确定
llm = Tongyi(temperature=1)

# 将一个消息列表传递给 .invoke 方法
result = llm.invoke([HumanMessage(content="你好,我是小明")])
print(result)

# 模型本身没有任何状态记录,再次提问
result = llm.invoke([HumanMessage(content="我的姓名是什么?")])
print(result)

image.png

image.png

可以看出这时大模型完全没法正常对话,明明前一句告诉它我是小明,后一句它说我是李华。

我们可以看到它没有将之前的对话转化为上下文,并且无法回答这个问题。这样会导致糟糕的聊天机器人体验!

让我们来看一下 LangSmith 跟踪的示例 : 对话1 对话2

为了解决这个问题,我们需要将整个对话历史传递给模型。让我们看看当我们这样做时会发生什么:

from langchain_community.llms import Tongyi
from langchain_core.messages import HumanMessage,AIMessage

# 使用 Tongyi LLM,并设置温度为 1,代表模型会更加随机,但也会更加不确定
llm = Tongyi(temperature=1)

# 将全部消息列表传递给 .invoke 方法
result = llm.invoke([
                    HumanMessage(content="你好,我是小明"),
                    AIMessage(content="你好!很高兴认识你。你在想什么呢?"),
                    HumanMessage(content="我的姓名是什么?")
                ])
print(result)

API Reference:AIMessage

运行结果: image.png LangSmith 跟踪的示例:对话日志

现在我们可以看到我们得到了一个很好的回答!

这是支持聊天机器人进行对话互动能力的基本思想。那么我们如何最好地实现呢?

历史对话

我们可以使用一个消息历史类来封装我们的模型,使其具有状态。这将跟踪模型的输入和输出,并将它们存储在某个数据存储中。未来的交互将加载这些消息,并作为输入的一部分传递给链条。让我们看看如何使用它!

首先,让我们确保安装了langchain-community,因为我们将在其中使用一个集成来存储消息历史记录。

pip install langchain_community

之后,我们可以导入相关的类并设置我们的链,它将包装模型并添加这个消息历史记录。这里的一个关键部分是我们传递给 get_session_history 的函数。这个函数应该接受一个session_id并返回一个消息历史记录对象。这个session_id用于区分不同的对话,并应该作为调用新链条时的配置的一部分传递进去(我们将展示如何做到这一点)。

from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

store = {}


def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]


with_message_history = RunnableWithMessageHistory(model, get_session_history)

API Reference:ChatMessageHistory | BaseChatMessageHistory | RunnableWithMessageHistory

现在我们需要创建一个配置,每次传递给可运行的时候使用。这个配置包含的信息不是直接的输入的一部分,但仍然很有用。在这种情况下,我们想要包含一个session_id。它应该是这样的:

config = {"configurable": {"session_id": "abc2"}}

完整代码:

from langchain_community.llms import Tongyi
from langchain_core.messages import HumanMessage
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

store = {}

def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 使用 Tongyi LLM,并设置温度为 1,代表模型会更加随机,但也会更加不确定
llm = Tongyi(temperature=1)

# 历史消息
with_message_history = RunnableWithMessageHistory(llm, get_session_history)

# 对话1
config = {"configurable": {"session_id": "abc2"}}

# 将一个消息列表传递给 .invoke 方法
response1 = with_message_history.invoke([HumanMessage(content="你好,我是小明")], config=config)
print(response1)

# 模型本身没有任何状态记录,再次提问
response2 = with_message_history.invoke([HumanMessage(content="我的姓名是什么?")], config=config)
print(response2)

太棒了!我们的聊天机器人现在可以记住我们的信息了。但是笔者亲测,目前有报错:Error in RootListenersTracer.on_llm_end callback: KeyError('message')

官方文档也有人提出 issues: https://github.com/langchain-ai/langchain/issues/22060 期待后续解决。然后在下面的【提示模版】的完整代码就不会报该错误了,也算一种解法。

image.png

如果我们更改配置以引用不同的session_id,我们可以看到它会重新开始对话。

config = {"configurable": {"session_id": "abc3"}}

response = with_message_history.invoke(
    [HumanMessage(content="你的名字是什么")],
    config=config,
)

但是,我们总是可以回到最初的对话(因为我们将其持久化在数据库中)

config = {"configurable": {"session_id": "abc2"}}

response = with_message_history.invoke(
    [HumanMessage(content="你好,我是小强")],
    config=config,
)

这就是我们如何支持聊天机器人与许多用户进行对话!

目前,我们所做的只是在模型周围添加了一个简单的持久化层。我们可以通过添加提示模板来使其更复杂和个性化。

提示模板

提示模板有助于将原始用户信息转换为LLM可以处理的格式。在这种情况下,原始用户输入只是一个消息,我们将其传递给LLM。现在让我们稍微复杂一些。首先,让我们添加一个带有一些自定义指示的系统消息(但仍然以消息作为输入)。接下来,我们将添加除了消息之外的更多输入。

首先,让我们添加一个系统消息。为此,我们将创建一个ChatPromptTemplate。我们将利用MessagesPlaceholder 来传递所有的消息。

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# 提示模板
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "你是一个有用的助手。尽你所能回答所有问题。",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)

# 使用 Tongyi LLM,并设置温度为 1,代表模型会更加随机,但也会更加不确定
llm = Tongyi(temperature=1)

chain = prompt | llm

API Reference:ChatPromptTemplate | MessagesPlaceholder

请注意,这稍微改变了输入类型-我们不再传递一个消息列表,而是传递一个字典,其中包含一个名为messages的键,其值是一个消息列表。

# 将一个消息列表传递给 .invoke 方法
response1 = with_message_history.invoke({"messages": [HumanMessage(content="你好,我是小明")]}, config=config)
print(response1)

打印输出: image.png

现在我们可以将其与之前相同的消息历史对象包装在一起:

# 历史消息
with_message_history = RunnableWithMessageHistory(chain, get_session_history)

# 对话1
config = {"configurable": {"session_id": "abc2"}}

# 将一个消息列表传递给 .invoke 方法
response1 = with_message_history.invoke(
    {"messages": [HumanMessage(content="你好,我是小明")], "language":"english"}, 
    config=config
)
print(response1)

# 模型有历史聊天记录,再次提问
response2 = with_message_history.invoke(
    {"messages": [HumanMessage(content="我的姓名是什么?")], "language":"english"}, 
    config=config
)

完整代码:

from langchain_community.llms import Tongyi
from langchain_core.messages import HumanMessage
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# 提示模板
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "你是一个有用的助手。尽你所能回答所有问题。",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)

# 历史会话存储
store = {}

# 获取会话历史
def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 使用 Tongyi LLM,并设置温度为 1,代表模型会更加随机,但也会更加不确定
llm = Tongyi(temperature=1)

# 构建链式调用
chain = prompt | llm

# 历史消息
with_message_history = RunnableWithMessageHistory(chain, get_session_history)

# 对话1
config = {"configurable": {"session_id": "abc2"}}

# 将一个消息列表传递给 .invoke 方法
response1 = with_message_history.invoke({"messages": [HumanMessage(content="你好,我是小明")]}, config=config)
print(response1)

# 模型有历史聊天记录,再次提问
response2 = with_message_history.invoke({"messages":[HumanMessage(content="我的姓名是什么?")]}, config=config)
print(response2)

LangSmith 跟踪的示例:对话日志 image.png

太棒了!现在让我们把提示变得更复杂一点。让我们假设提示模板现在看起来像这样:

# 提示模板
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "你是一个有用的助手。尽你所能用{language}回答所有问题。",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)

# 使用 Tongyi LLM,并设置温度为 1,代表模型会更加随机,但也会更加不确定
llm = Tongyi(temperature=1)

# 构建链式调用
chain = prompt | llm

请注意,我们在提示符中添加了一个新的 language 输入。我们现在可以调用链并传入我们选择的语言。 并且让我们将这个更复杂的链包装在一个消息历史类中。这次,因为输入中有多个键,我们需要指定正确的键来保存聊天记录。

from langchain_community.llms import Tongyi
from langchain_core.messages import HumanMessage
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# 提示模板
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "你是一个有用的助手。尽你所能用{language}回答所有问题。",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)

# 历史会话存储
store = {}

# 获取会话历史
def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 使用 Tongyi LLM,并设置温度为 1,代表模型会更加随机,但也会更加不确定
llm = Tongyi(temperature=1)

# 构建链式调用
chain = prompt | llm

# 历史消息
with_message_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="messages",
)

# 对话1
config = {"configurable": {"session_id": "abc2"}}

# 将一个消息列表传递给 .invoke 方法
response1 = with_message_history.invoke(
    {"messages": [HumanMessage(content="你好,我是小明")], "language":"english"}, 
    config=config
)
print(response1)

# 模型有历史聊天记录,再次提问
response2 = with_message_history.invoke(
    {"messages": [HumanMessage(content="我的姓名是什么?")], "language":"english"}, 
    config=config
)
print(response2)

为了帮助您了解内部发生了什么,请查看LangSmith跟踪(第二句回复又变回了中文,呵呵) image.png

管理对话历史

构建聊天机器人时要理解的一个重要概念是如何管理对话历史。如果不加管理,消息列表将无限增长,并可能溢出LLM的上下文窗口。因此,添加一个限制您传入的消息大小的步骤非常重要。

重要的是,您将希望在提示模板之前但在从消息历史记录加载以前的消息之后执行此操作。

我们可以通过在提示符前面添加一个简单的步骤来适当地修改messages键,然后将新链包装在消息历史类中。首先,让我们定义一个函数来修改传入的消息。让我们使它选择k最近的消息。然后我们可以通过在开始时添加它来创建一个新链。

from langchain_core.runnables import RunnablePassthrough


# k=10 则无法记得姓名是什么,k=20 则可以记得
def filter_messages(messages, k=20):
    return messages[-k:]


chain = (
    RunnablePassthrough.assign(messages=lambda x: filter_messages(x["messages"]))
    | prompt
    | llm
)

API Reference:RunnablePassthrough

现在让我们试试看!如果我们创建一个超过10条消息的消息列表,我们可以看到它不再记得早期消息中的信息。

messagesList = [
    HumanMessage(content="你好,我是小明"),
    AIMessage(content="你好!"),
    HumanMessage(content="我喜欢香草冰淇淋"),
    AIMessage(content="好的"),
    HumanMessage(content="2 + 2 等于几"),
    AIMessage(content="4"),
    HumanMessage(content="谢谢"),
    AIMessage(content="不用谢!"),
    HumanMessage(content="正玩得开心么?"),
    AIMessage(content="是的!"),
]
# 模型有历史聊天记录,再次提问
response = with_message_history.invoke(
    {"messages": messagesList + [HumanMessage(content="我的姓名是什么?")], "language":"english"}, 
    config=config
)
print(response)

打印输出: image.png

但如果我们询问最近10条信息中的信息,它仍然记得。

response = chain.invoke(
    {
        "messages": messages + [HumanMessage(content="what's my fav ice cream")],
        "language": "English",
    }
)
response.content

打印输出: image.png

现在让我们将其包装在消息历史记录中,完整代码:

from langchain_community.llms import Tongyi
from langchain_core.messages import HumanMessage,AIMessage
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough

# k=10 则无法记得姓名是什么,k=20 则可以记得
def filter_messages(messages, k=10):
    return messages[-k:]


# 提示模板
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "你是一个有用的助手。尽你所能用{language}回答所有问题。",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)

# 历史会话存储
store = {}

# 获取会话历史
def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 使用 Tongyi LLM,并设置温度为 1,代表模型会更加随机,但也会更加不确定
llm = Tongyi(temperature=1)

# 构建链式调用
chain = (
    RunnablePassthrough.assign(messages=lambda x: filter_messages(x["messages"]))
    | prompt
    | llm
)

# 历史消息
with_message_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="messages",
)

# 对话1
config = {"configurable": {"session_id": "abc2"}}

messagesList = [
    HumanMessage(content="你好,我是小明"),
    AIMessage(content="你好!"),
    HumanMessage(content="我喜欢香草冰淇淋"),
    AIMessage(content="好的"),
    HumanMessage(content="2 + 2 等于几"),
    AIMessage(content="4"),
    HumanMessage(content="谢谢"),
    AIMessage(content="不用谢!"),
    HumanMessage(content="正玩得开心么?"),
    AIMessage(content="是的!"),
]

# 模型有历史聊天记录,再次提问
response1 = with_message_history.invoke(
    {"messages": messagesList + [HumanMessage(content="我的姓名是什么?")], "language":"english"}, 
    config=config
)
print(response1)

response1 = with_message_history.invoke(
    {"messages": messagesList + [HumanMessage(content="我喜欢的冰淇淋是什么?")], "language":"english"}, 
    config=config
)
print(response1)

如果你看一下LangSmith,你可以在LangSmith跟踪中看到到底发生了什么 LangSmith trace

image.png

现在我们有了一个功能聊天机器人。然而,聊天机器人应用程序的一个真正重要的用户体验考虑是流式传输。LLM有时需要一段时间才能响应,因此为了改善用户体验,大多数应用程序都会在生成每个令牌时流式传输。这允许用户看到进度。

这其实超级容易做到!

所有链都公开了一个.stream方法,使用消息历史记录的链也不例外。我们可以简单地使用该方法来获取流响应。

from langchain_community.llms import Tongyi
from langchain_core.messages import HumanMessage,AIMessage
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough

# k=10 只保留最近的10条消息
def filter_messages(messages, k=10):
    return messages[-k:]


# 提示模板
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "你是一个有用的助手。尽你所能用回答所有问题。",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)

# 历史会话存储
store = {}

# 获取会话历史
def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 使用 Tongyi LLM,并设置温度为 1,代表模型会更加随机,但也会更加不确定
llm = Tongyi(temperature=1)

# 构建链式调用
chain = (
    RunnablePassthrough.assign(messages=lambda x: filter_messages(x["messages"]))
    | prompt
    | llm
)

# 历史消息
with_message_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="messages",
)

# 对话1
config = {"configurable": {"session_id": "abc2"}}

# 模型有历史聊天记录,再次提问
for r in  with_message_history.stream(
    {"messages":   [HumanMessage(content="请讲一个极简的笑话给我听")]}, 
    config=config
):print(r, end="|")

image.png

如果你看一下LangSmith,你可以在LangSmith跟踪中看到到底发生了什么 LangSmith trace image.png

小结

本节我们学习了 Chat ModelsPrompt TemplatesChat History ,是非常有趣和值得去扩展的,快去实践一下吧,创造自己的聊天机器人搭子。

后续学习 如果你想深入了解细节,一些值得学习的内容是:

  • 对话式RAG:通过外部数据源启用聊天机器人体验
  • 代理:构建一个可以采取行动的聊天机器人
  • 流媒体:流媒体对于聊天应用程序至关重要
  • 如何添加消息历史:深入了解与消息历史相关的所有内容

欢迎关注微信公众号【千练极客】,尽享更多干货文章! qrcode_for_gh_e39063348296_258.jpg

本文由博客一文多发平台 OpenWrite 发布!