一文教会你用 LangChain 构建一个聊天机器人

这篇文章我们将通过一个示例来设计和实现一个基于大型语言模型的聊天机器人, 这个聊天机器人将能够进行对话并记住之前的互动。本篇文章基于阿里云的通义千问模型作为语言模型,如果还没阅读过我们以下系列文章的,可以前往阅读。

系列文章:

  1. 一文教会你用 LangChain 快速构建大模型应用

  2. 一文教会你使用 LangServe 构建大模型服务

  3. 一文教会你使用 LangSmith 搭建 AI 应用监测平台

1、模型本身没有状态

模型本身是没有任何状态概念的,如下所示,我们先告诉模型:“你好,我的名字叫小明!”,然后再问:"我的名字叫什么?" 。模型是没有办法回答的,因为模型没有将之前的对话轮次作为上下文,因此无法回答问题。 这会导致糟糕的聊天机器人体验!

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

model = Tongyi()

result1 = model.invoke([HumanMessage(content="你好,我的名字叫小明!")])
print("回答1:", result1)
# 回答1: 你好,小明!很高兴认识你。你可以叫我Qwen,我是阿里云推出的一种超大规模语言模型。有什么我可以帮助你的吗?

result2 = model.invoke([HumanMessage(content="我的名字叫什么?")])
print("回答2:", result2)
# 回答2: 您好!我是一个AI助手,如果您没有告诉我您的名字,我是无法知道的。您可以告诉我您的名字吗?我很乐意认识您!

为了解决模型本身没有状态这个问题,我们需要将整个对话历史传递给模型,如下所示,我们将看到模型能给我们一个很好的回应!这是支撑聊天机器人进行对话交互的基本理念。

from langchain_core.messages import AIMessage

model.invoke(
    [
        HumanMessage(content="你好,我的名字叫小明!"),
        AIMessage(content="你好,小明!很高兴认识你。有什么我可以帮助你的吗?"),
        HumanMessage(content="我的名字叫什么?"),
    ]
)
# 你的名字叫小明。
2、历史对话

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

我们通过定义一个 get_session_history 函数,它接受一个 session_id 并返回一个消息历史对象。这个 session_id 用于区分不同的对话,并应作为配置的一部分在调用新链时传入,如下所示

from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
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)

我们现在需要创建一个 config,每次都传递给可运行的部分,其包含一个 session_id,如下所示:

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

response = with_message_history.invoke(
    [HumanMessage(content="你好,我的名字叫小明!")],
    config=config,
)
print(response)


response = with_message_history.invoke(
    [HumanMessage(content="我的名字叫什么?")],
    config=config,
)
print(response)

输出结果如下所示,很遗憾,这段示例代码报错了,并且结果并不是我们预期的!官方文档也有人提出 issues: https://github.com/langchain-ai/langchain/issues/22060 期待后续解决。然后在下面的【提示模版】的完整代码就不会报该错误了,所以我们在这里先暂时略过!

3、提示词模版

提示词模板帮助将原始用户信息转换为大型语言模型可以处理的格式。在这种情况下,原始用户输入只是一个消息,我们将其传递给大型语言模型。首先,让我们添加一个带有一些自定义指令的系统消息(但仍然将消息作为输入)。

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

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
model = Tongyi()

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

# 历史消息
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)

# -------输出--------
# 你好,小明!很高兴认识你。有什么我可以帮助你的吗?

接着,我们分别使用相同的 session_id 和 不相同的 session_id 来询问语言模型,如下所示,我们看到使用相同的 session_id 是会记住先前的对话状态的,也就达到了我们原先的期望。

# 对话1
config = {"configurable": {"session_id": "abc2"}}
response2 = with_message_history.invoke({"messages":[HumanMessage(content="我的姓名是什么?")]}, config=config)
print(response2)
# --------回答---------
# 你好,小明!根据你之前的介绍,你的姓名是小明。如果有其他问题或者需要帮助的地方,请告诉我哦!

# 对话2
config2 = {"configurable": {"session_id": "abc3"}}
response2 = with_message_history.invoke({"messages":[HumanMessage(content="我的姓名是什么?")]}, config=config2)
print(response2)
# --------回答---------
# 您好!我是一个AI助手,除非您之前告诉我您的名字,否则我是不知道您的姓名的。如果您愿意分享,可以告诉我,但请记得在公共平台上分享个人信息时要谨慎。如果您只是出于好奇或有其他问题,我很乐意帮助您!
4、管理对话历史

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

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

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

如下所示,如果我们设置 k=20 则模型助手能记住我的名字叫小明,如果设置 k=10 则模型助手无法则不知道我的名字叫什么。

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

# 获取会话历史
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(
    chain,
    get_session_history,
    input_messages_key="messages",
)

# 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
)

messages = [
    SystemMessage(content="你是一个非常棒的助手!"),
    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": messages + [HumanMessage(content="我的姓名是什么?")]}, 
    config=config
)
print(response)

5、流式处理

现在我们有了一个功能齐全的聊天机器人。然而,对于聊天机器人应用程序来说,一个非常重要的用户体验考虑是流式处理。大型语言模型有时可能需要一段时间才能响应,因此为了改善用户体验,大多数应用程序所做的一件事是随着每个令牌的生成流回。这样用户就可以看到进度。

实际上,这非常简单!所有链都暴露一个.stream方法,使用消息历史的链也不例外。我们可以简单地使用该方法获取流式响应。

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

# 模型有历史聊天记录,再次提问
for r in  with_message_history.stream(
    {"messages":   [HumanMessage(content="请讲一个超过5句话的笑话给我听")]}, 
    config=config
):
    pprint.pprint(r,  width=200)

通过以上各章节,我们详细介绍了如何基于 LangChain 构建一个聊天机器人。希望这篇文章对您有所帮助!如果您有任何问题或需要更多示例,请留言。


如果你喜欢本文,欢迎点赞,并且关注我们的微信公众号:Python技术极客,我们会持续更新分享 Python 开发编程、数据分析、数据挖掘、AI 人工智能、网络爬虫等技术文章!让大家在Python 技术领域持续精进提升,成为更好的自己!

添加作者微信(coder_0101),拉你进入行业技术交流群,进行技术交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coder_风逝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值