
LangChain构建一个聊天机器人
我们将通过一个示例来说明如何设计和实现由 LLM 驱动的聊天机器人。该聊天机器人将能够进行对话并记住之前的交互。请注意,我们构建的这个聊天机器人将仅使用语言模型进行对话。本教程将涵盖对这两个更高级主题有帮助的基础知识,但如果您选择,请随意直接跳到那里。
概述
我们将通过一个示例来说明如何设计和实现由 LLM 驱动的聊天机器人。该聊天机器人将能够进行对话并记住之前的交互。
请注意,我们构建的这个聊天机器人将仅使用语言模型进行对话。您可能正在寻找其他几个相关概念:
- 会话式 RAG:通过外部数据源实现聊天机器人体验
- 代理:构建可以采取行动的聊天机器人
本教程将涵盖对这两个更高级主题有帮助的基础知识,但如果您选择,请随意直接跳到那里。
概念
以下是我们将使用的一些高级组件:
- Chat Models 。聊天机器人界面基于消息而不是原始文本,因此最适合聊天模型而不是文本LLMs。
- Prompt Templates ,它简化了组合默认消息、用户输入、聊天历史记录和(可选)附加检索上下文的提示的过程。
- Chat History ,它允许聊天机器人“记住”过去的交互,并在回答后续问题时考虑它们。
- 使用 LangSmith 调试和跟踪您的应用程序
我们将介绍如何将上述组件组合在一起以创建强大的对话聊天机器人。
#设置
Jupyter 笔记本
本指南(以及文档中的大多数其他指南)使用 Jupyter 笔记本,并假设读者也使用 Jupyter 笔记本。 Jupyter 笔记本非常适合学习如何使用 LLM 系统,因为经常会出现问题(意外输出、API 关闭等),在交互式环境中阅读指南是更好地理解它们的好方法。
安装
要安装 LangChain 运行:
pip install langchain
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支持多种不同的语言模型,您可以互换使用 - 在下面选择您想要使用的一种!
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“Runnables”的实例,这意味着它们公开了一个标准接口来与之交互。为了简单地调用模型,我们可以将消息列表传递给 .invoke 方法。
from langchain_core.messages import HumanMessage
model.invoke([HumanMessage(content="Hi! I'm Bob")])
该模型本身没有任何状态概念。例如,如果您提出后续问题:
model.invoke([HumanMessage(content="What's my name?")])
让我们看一下 LangSmith 跟踪示例
可以看出,它没有把前面的对话转入上下文,无法回答问题。这会带来糟糕的聊天机器人体验!
为了解决这个问题,我们需要将整个对话历史记录传递到模型中。让我们看看这样做时会发生什么:
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?"),
]
)
现在我们可以看到我们得到了很好的回应!
这是支撑聊天机器人对话交互能力的基本思想。那么我们如何最好地实现这一点呢?
消息历史
我们可以使用消息历史记录类来包装我们的模型并使其具有状态。这将跟踪模型的输入和输出,并将它们存储在某个数据存储中。未来的交互将加载这些消息并将它们作为输入的一部分传递到链中。让我们看看如何使用它!
首先,让我们确保安装 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)
我们现在需要创建一个 config ,每次都将其传递到可运行对象中。此配置包含的信息不是直接输入的一部分,但仍然有用。在本例中,我们想要包含 session_id 。这应该看起来像:
config = {"configurable": {"session_id": "abc2"}}
response = with_message_history.invoke(
[HumanMessage(content="Hi! I'm Bob")],
config=config,
)
response.content
response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
config=config,
)
response.content
伟大的!我们的聊天机器人现在可以记住有关我们的事情。如果我们更改配置以引用不同的 session_id ,我们可以看到它重新开始对话。
config = {"configurable": {"session_id": "abc3"}}
response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
config=config,
)
response.content
然而,我们总是可以回到原来的对话(因为我们将其保存在数据库中)
config = {"configurable": {"session_id": "abc2"}}
response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
config=config,
)
response.content
这就是我们如何支持聊天机器人与许多用户进行对话!
现在,我们所做的就是在模型周围添加一个简单的持久层。我们可以通过添加提示模板来开始使内容变得更加复杂和个性化。
提示模板
提示模板有助于将原始用户信息转换为 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
请注意,这会稍微改变输入类型 - 我们现在不是传递消息列表,而是传递带有 messages 键的字典,其中包含消息列表。
response = chain.invoke({"messages": [HumanMessage(content="hi! I'm bob")]})
response.content
我们现在可以将其包装在与之前相同的 Messages History 对象中
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
response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
config=config,
)
response.content
惊人的!现在让我们的提示变得更复杂一些。我们假设提示模板现在看起来像这样:
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
现在让我们将这个更复杂的链包装在消息历史记录类中。这次,因为输入中有多个键,所以我们需要指定正确的键来保存聊天记录。
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
response = with_message_history.invoke(
{"messages": [HumanMessage(content="whats my name?")], "language": "Spanish"},
config=config,
)
response.content
为了帮助您了解内部发生的情况,请查看此 LangSmith 跟踪
管理对话历史记录
构建聊天机器人时需要理解的一个重要概念是如何管理对话历史记录。如果不进行管理,消息列表将无限增长,并可能溢出 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
)
现在我们就来尝试一下吧!如果我们创建一个长度超过 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
但如果我们询问最近十条消息中的信息,它仍然会记住它
response = chain.invoke(
{
"messages": messages + [HumanMessage(content="what's my fav ice cream")],
"language": "English",
}
)
response.content
现在让我们将其包含在消息历史记录中
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
聊天记录中现在有两条新消息。这意味着我们的对话历史记录中过去可以访问的更多信息不再可用!
response = with_message_history.invoke(
{
"messages": [HumanMessage(content="whats my favorite ice cream?")],
"language": "English",
},
config=config,
)
response.content
如果你看一下 LangSmith,你可以准确地看到 LangSmith 跟踪中到底发生了什么
流式传输
现在我们有了一个功能聊天机器人。然而,聊天机器人应用程序的一个真正重要的用户体验考虑因素是流。 LLMs 有时可能需要一段时间才能响应,因此为了改善用户体验,大多数应用程序所做的一件事就是在生成每个令牌时将其流回。这允许用户看到进度。
事实上,做到这一点非常简单!
所有链都公开 .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="|")
更多推荐
所有评论(0)