LangChain 0.2 - 构建一个聊天机器人

LLM、AIGC、RAG 开发交流裙:377891973
本文翻译整理自:https://python.langchain.com/v0.2/docs/tutorials/chatbot/



一、说明

我们将通过一个示例来说明如何设计和实现由 LLM 支持的聊天机器人。该聊天机器人将能够进行对话并记住之前的交互。

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

  • 会话式 RAG:通过外部数据源实现聊天机器人体验
  • 代理:构建一个可以采取行动的聊天机器人

本教程将介绍对这两个更高级的主题有帮助的基础知识,但如果您愿意的话也可以直接跳到那里。


概念

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

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

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


二、设置


1、Jupyter Notebook

本指南(以及文档中的大多数其他指南)使用Jupyter 笔记本,并假设读者也使用 Jupyter 笔记本。 Jupyter 笔记本非常适合学习如何使用 LLM 系统,因为事情经常可能会出错(意外输出、API 关闭等),而在交互式环境中阅读指南是更好地理解它们的好方法。

本教程和其他教程可能最方便地在 Jupyter 笔记本中运行。有关如何安装的说明,请参阅此处。


2、安装

要安装 LangChain 运行:

pip install langchain

有关更多详细信息,请参阅我们的安装指南


3、LangSmith

您使用 LangChain 构建的许多应用程序将包含多个步骤以及多次调用 LLM 调用。

随着这些应用程序变得越来越复杂,能够检查链或代理内部到底发生了什么变得至关重要。最好的方法是使用LangSmith

在上面的链接注册后,请确保设置环境变量以开始记录跟踪:

export LANGCHAIN_TRACING_V2="true"
export LANGCHAIN_API_KEY="..."

或者,如果在笔记本中,您可以使用以下方式设置它们:

import getpass
import os

os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = getpass.getpass()

三、快速入门

首先,让我们学习如何单独使用语言模型。 LangChain支持多种不同的语言模型,您可以互换使用 - 在下面选择您想要使用的一种!

OpenAI, Anthropic, Google, Cohere, FireworksAI, MistralAI, TogetherAI

这里以 OpenAI 为例

pip install -qU langchain-openai

import getpass
import os

os.environ["OPENAI_API_KEY"] = getpass.getpass()

from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-3.5-turbo")

我们首先直接使用模型。ChatModel是 LangChain“Runnable”的实例,这意味着它们公开了一个用于与它们交互的标准接口。为了简单地调用模型,我们可以将消息列表传递给该.invoke方法。

from langchain_core.messages import HumanMessage

model.invoke([HumanMessage(content="Hi! I'm Bob")])


API参考:


AIMessage(content='Hello Bob! How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 12, 'total_tokens': 22}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_c2295e73ad', 'finish_reason': 'stop', 'logprobs': None}, id='run-be38de4a-ccef-4a48-bf82-4292510a8cbf-0')

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

model.invoke([HumanMessage(content="What's my name?")])

AIMessage(content="I'm sorry, as an AI assistant, I do not have the capability to know your name unless you provide it to me.", response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 12, 'total_tokens': 38}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_caf95bb1ae', 'finish_reason': 'stop', 'logprobs': None}, id='run-8d8a9d8b-dddb-48f1-b0ed-ce80ce5397d8-0')

让我们看一下LangSmith trace

可以看出,它没有将前面的对话转入上下文,无法回答问题。这会带来糟糕的聊天机器人体验!

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

from langchain_core.messages import AIMessage

model.invoke(
    [
        HumanMessage(content="Hi! I'm Bob"),
        AIMessage(content="Hello Bob! How can I assist you today?"),
        HumanMessage(content="What's my name?"),
    ]
)


API参考:

AIMessage(content='Your name is Bob.', response_metadata={'token_usage': {'completion_tokens': 5, 'prompt_tokens': 35, 'total_tokens': 40}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_c2295e73ad', 'finish_reason': 'stop', 'logprobs': None}, id='run-5692718a-5d29-4f84-bad1-a9819a6118f1-0')

现在我们可以看到我们得到了很好的回应!

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


四、消息历史

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

首先,让我们确保安装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参考:


我们现在需要创建一个config,每次传递到 可运行的对象。此配置包含的信息不是直接输入的一部分,但仍然有用。在本例中,我们想要包含一个session_id.这应该看起来像:

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

response = with_message_history.invoke(
    [HumanMessage(content="Hi! I'm Bob")],
    config=config,
)

response.content

'Hello Bob! How can I assist you today?'

response = with_message_history.invoke(
    [HumanMessage(content="What's my name?")],
    config=config,
)

response.content

'Your name is Bob.'

很好!我们的聊天机器人现在可以记住有关我们的事情。

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

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

response = with_message_history.invoke(
    [HumanMessage(content="What's my name?")],
    config=config,
)

response.content

"I'm sorry, I do not have the ability to know your name unless you tell me."

然而,我们总是可以回到原来的对话(因为我们将其保存在数据库中)

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

response = with_message_history.invoke(
    [HumanMessage(content="What's my name?")],
    config=config,
)

response.content

'Your name is Bob.'

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

现在,我们所做的就是在模型周围添加一个简单的持久层。我们可以通过添加提示模板来开始使内容变得更加复杂和个性化。


五、提示模板

提示模板有助于 将原始用户信息 转换为 LLM 可以使用的格式。在这种情况下,原始用户输入只是一条消息,我们将它传递给 LLM。

现在让我们让它更复杂一点。首先,让我们添加一条带有一些自定义指令的系统消息(但仍然将消息作为输入)。接下来,除了消息之外,我们还将添加更多输入。


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

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

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

chain = prompt | model


API参考:


请注意,这会稍微改变输入类型 - 我们现在不是传递消息列表,而是传递一个带有messages键的字典,其中包含消息列表。

response = chain.invoke({"messages": [HumanMessage(content="hi! I'm bob")]})

response.content

'Hello, Bob! How can I assist you today?'

我们现在可以将其包装在与之前相同的消息历史记录对象中

with_message_history = RunnableWithMessageHistory(chain, get_session_history)

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

response = with_message_history.invoke(
    [HumanMessage(content="Hi! I'm Jim")],
    config=config,
)

response.content

'Hello, Jim! How can I assist you today?'

response = with_message_history.invoke(
    [HumanMessage(content="What's my name?")],
    config=config,
)

response.content

'Your name is Jim. How can I assist you further, Jim?'

很好!现在让我们的提示变得更复杂一些。我们假设提示模板现在看起来像这样:

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

chain = prompt | model

请注意,我们已在提示中添加了新的 language 输入。我们现在可以调用该链 并传递我们选择的语言。

response = chain.invoke(
    {"messages": [HumanMessage(content="hi! I'm bob")], "language": "Spanish"}
)

response.content

'¡Hola Bob! ¿En qué puedo ayudarte hoy?'

现在让我们将这个更复杂的链 包装在消息历史记录类中。

这次,因为输入中有多个键,所以我们需要 指定正确的键 来保存聊天记录。

with_message_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="messages",
)

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

response = with_message_history.invoke(
    {"messages": [HumanMessage(content="hi! I'm todd")], "language": "Spanish"},
    config=config,
)

response.content

'¡Hola Todd! ¿En qué puedo ayudarte hoy?'

response = with_message_history.invoke(
    {"messages": [HumanMessage(content="whats my name?")], "language": "Spanish"},
    config=config,
)

response.content

'Tu nombre es Todd. ¿Hay algo más en lo que pueda ayudarte?'

为了帮助您了解内部发生的情况,请查看此 LangSmith trace


六、管理对话历史记录

构建聊天机器人时需要理解的一个重要概念是如何管理对话历史记录。

如果不进行管理,消息列表将无限增长,并可能溢出 LLM 的上下文窗口。

因此,添加一个步骤来限制您传入的消息的大小非常重要。

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

我们可以通过在提示前面添加一个简单的步骤来messages适当地修改密钥,然后将该新链包装在 Message History 类中来实现此目的。

首先,让我们定义一个函数来修改传入的消息。

让我们让它选择k最新的消息。然后我们可以通过在开头添加它来创建一个新链。

from langchain_core.runnables import RunnablePassthrough


def filter_messages(messages, k=10):
    return messages[-k:]


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

API参考:


现在我们就来尝试一下吧!如果我们创建一个长度超过 10 条消息的消息列表,我们可以看到它不再记住早期消息中的信息。

messages = [
    HumanMessage(content="hi! I'm bob"),
    AIMessage(content="hi!"),
    HumanMessage(content="I like vanilla ice cream"),
    AIMessage(content="nice"),
    HumanMessage(content="whats 2 + 2"),
    AIMessage(content="4"),
    HumanMessage(content="thanks"),
    AIMessage(content="no problem!"),
    HumanMessage(content="having fun?"),
    AIMessage(content="yes!"),
]

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

"I'm sorry, I don’t have access to your name. Can I help you with anything else?"

但如果我们询问最近十条消息中的信息,它仍然会记住它

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

'You mentioned that you like vanilla ice cream.'

现在让我们将其包含在消息历史记录中

with_message_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="messages",
)

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

response = with_message_history.invoke(
    {
        "messages": messages + [HumanMessage(content="whats my name?")],
        "language": "English",
    },
    config=config,
)

response.content

"I'm sorry, I don't know your name."

聊天记录中现在有两条新消息。这意味着我们的对话历史记录中过去可以访问的更多信息不再可用!

response = with_message_history.invoke(
    {
        "messages": [HumanMessage(content="whats my favorite ice cream?")],
        "language": "English",
    },
    config=config,
)

response.content

"I'm sorry, I don't know your favorite ice cream flavor."

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


七、流媒体

现在我们有了一个功能聊天机器人。

然而,聊天机器人应用程序的一个真正重要的用户体验考虑因素是流。

LLM 有时可能需要一段时间才能响应,因此为了改善用户体验,大多数应用程序所做的一件事就是在生成每个令牌时将其流回。这允许用户看到进度。

事实上,做到这一点非常简单!

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

config = {"configurable": {"session_id": "abc15"}}
for r in with_message_history.stream(
    {
        "messages": [HumanMessage(content="hi! I'm todd. tell me a joke")],
        "language": "English",
    },
    config=config,
):
    print(r.content, end="|")

|Sure|,| Todd|!| Here|'s| a| joke| for| you|:

|Why| don|'t| scientists| trust| atoms|?

|Because| they| make| up| everything|!||

八、下一步

现在您已经了解了如何在 LangChain 中创建聊天机器人的基础知识,您可能感兴趣的一些更高级的教程是:

  • 会话式 RAG:通过外部数据源实现聊天机器人体验
  • 代理:构建一个可以采取行动的聊天机器人

如果你想深入了解具体细节,以下是值得一看的内容:


2024-05-22(三)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

AI工程仔

请我喝杯伯爵奶茶~!

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

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

打赏作者

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

抵扣说明:

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

余额充值