Skip to main content
Open In ColabOpen on GitHub

构建一个聊天机器人

note

本教程之前使用了RunnableWithMessageHistory抽象。您可以在v0.2文档中访问该版本的文档。

截至LangChain的v0.3版本发布,我们建议LangChain用户利用LangGraph持久化memory集成到新的LangChain应用中。

如果你的代码已经依赖于RunnableWithMessageHistoryBaseChatMessageHistory,你不需要做任何更改。我们不打算在不久的将来弃用此功能,因为它适用于简单的聊天应用程序,并且任何使用RunnableWithMessageHistory的代码将继续按预期工作。

请参阅如何迁移到LangGraph Memory了解更多详情。

概述

我们将通过一个示例来讲解如何设计和实现一个由LLM驱动的聊天机器人。 这个聊天机器人将能够进行对话,并记住与聊天模型的先前交互。

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

  • Conversational RAG: 启用基于外部数据源的聊天机器人体验
  • Agents: 构建一个可以采取行动的聊天机器人

本教程将涵盖基础知识,这些内容对那两个更高级的主题会有所帮助,但如果您选择直接跳到那里,也请随意。

设置

Jupyter 笔记本

本指南(以及文档中的大多数其他指南)使用Jupyter notebooks,并假设读者也是如此。Jupyter notebooks 非常适合学习如何使用 LLM 系统,因为经常会出现问题(意外的输出、API 宕机等),在交互式环境中浏览指南是更好地理解它们的好方法。

本教程及其他教程或许在Jupyter笔记本中运行最为方便。有关如何安装的说明,请参见这里

安装

本教程我们将需要langchain-corelanggraph。本指南要求langgraph >= 0.2.28

pip install langchain-core langgraph>0.2.27

更多详情,请参阅我们的安装指南

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

if not os.environ.get("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter API key for OpenAI: ")

from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini")

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

from langchain_core.messages import HumanMessage

model.invoke([HumanMessage(content="Hi! I'm Bob")])
API Reference:HumanMessage
AIMessage(content='Hi Bob! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 11, 'total_tokens': 21, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-5211544f-da9f-4325-8b8e-b3d92b2fc71a-0', usage_metadata={'input_tokens': 11, 'output_tokens': 10, 'total_tokens': 21, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})

模型本身没有任何状态的概念。例如,如果你问一个后续问题:

model.invoke([HumanMessage(content="What's my name?")])
AIMessage(content="I'm sorry, but I don't have access to personal information about users unless it has been shared with me in the course of our conversation. How can I assist you today?", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 11, 'total_tokens': 45, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-a2d13a18-7022-4784-b54f-f85c097d1075-0', usage_metadata={'input_tokens': 11, 'output_tokens': 34, 'total_tokens': 45, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})

让我们看一下这个例子 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?"),
]
)
API Reference:AIMessage
AIMessage(content='Your name is Bob! How can I help you today, Bob?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 33, 'total_tokens': 47, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-34bcccb3-446e-42f2-b1de-52c09936c02c-0', usage_metadata={'input_tokens': 33, 'output_tokens': 14, 'total_tokens': 47, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})

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

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

消息持久化

LangGraph 实现了一个内置的持久化层,使其非常适合支持多次对话轮次的聊天应用。

将我们的聊天模型封装在一个最小的LangGraph应用程序中,使我们能够自动保存消息历史记录,从而简化多轮应用程序的开发。

LangGraph 自带一个简单的内存检查点,我们在下面使用它。有关更多详细信息,包括如何使用不同的持久化后端(例如 SQLite 或 Postgres),请参阅其 文档

from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, MessagesState, StateGraph

# Define a new graph
workflow = StateGraph(state_schema=MessagesState)


# Define the function that calls the model
def call_model(state: MessagesState):
response = model.invoke(state["messages"])
return {"messages": response}


# Define the (single) node in the graph
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)

# Add memory
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
API Reference:MemorySaver | StateGraph

我们现在需要创建一个config,每次我们都会将其传递给可运行对象。这个配置包含的信息不是直接输入的一部分,但仍然有用。在这种情况下,我们希望包含一个thread_id。这应该看起来像:

config = {"configurable": {"thread_id": "abc123"}}

这使我们能够支持单个应用程序的多个对话线程,这是当您的应用程序有多个用户时的常见需求。

然后我们可以调用应用程序:

query = "Hi! I'm Bob."

input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print() # output contains all messages in state
================================== Ai Message ==================================

Hi Bob! How can I assist you today?
query = "What's my name?"

input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================

Your name is Bob! How can I help you today, Bob?

太好了!我们的聊天机器人现在可以记住我们的事情。如果我们更改配置以引用不同的thread_id,我们可以看到它重新开始对话。

config = {"configurable": {"thread_id": "abc234"}}

input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================

I'm sorry, but I don't have access to personal information about you unless you've shared it in this conversation. How can I assist you today?

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

config = {"configurable": {"thread_id": "abc123"}}

input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================

Your name is Bob. What would you like to discuss today?

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

tip

为了支持异步操作,将call_model节点更新为异步函数,并在调用应用程序时使用.ainvoke

# Async function for node:
async def call_model(state: MessagesState):
response = await model.ainvoke(state["messages"])
return {"messages": response}


# Define graph as before:
workflow = StateGraph(state_schema=MessagesState)
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
app = workflow.compile(checkpointer=MemorySaver())

# Async invocation:
output = await app.ainvoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()

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

提示模板

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

要添加系统消息,我们将创建一个ChatPromptTemplate。我们将使用MessagesPlaceholder来传递所有消息。

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt_template = ChatPromptTemplate.from_messages(
[
(
"system",
"You talk like a pirate. Answer all questions to the best of your ability.",
),
MessagesPlaceholder(variable_name="messages"),
]
)

我们现在可以更新我们的应用程序以包含此模板:

workflow = StateGraph(state_schema=MessagesState)


def call_model(state: MessagesState):
prompt = prompt_template.invoke(state)
response = model.invoke(prompt)
return {"messages": response}


workflow.add_edge(START, "model")
workflow.add_node("model", call_model)

memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

我们以相同的方式调用应用程序:

config = {"configurable": {"thread_id": "abc345"}}
query = "Hi! I'm Jim."

input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================

Ahoy there, Jim! What brings ye to these waters today? Be ye seekin' treasure, knowledge, or perhaps a good tale from the high seas? Arrr!
query = "What is my name?"

input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================

Ye be called Jim, matey! A fine name fer a swashbuckler such as yerself! What else can I do fer ye? Arrr!

太棒了!现在让我们让我们的提示稍微复杂一点。假设提示模板现在看起来像这样:

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

请注意,我们已经在提示中添加了一个新的language输入。我们的应用程序现在有两个参数——输入messageslanguage。我们应该更新应用程序的状态以反映这一点:

from typing import Sequence

from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
from typing_extensions import Annotated, TypedDict


class State(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
language: str


workflow = StateGraph(state_schema=State)


def call_model(state: State):
prompt = prompt_template.invoke(state)
response = model.invoke(prompt)
return {"messages": [response]}


workflow.add_edge(START, "model")
workflow.add_node("model", call_model)

memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
API Reference:BaseMessage | add_messages
config = {"configurable": {"thread_id": "abc456"}}
query = "Hi! I'm Bob."
language = "Spanish"

input_messages = [HumanMessage(query)]
output = app.invoke(
{"messages": input_messages, "language": language},
config,
)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================

¡Hola, Bob! ¿Cómo puedo ayudarte hoy?

请注意,整个状态都会被持久化,因此如果不需要更改,我们可以省略像 language 这样的参数:

query = "What is my name?"

input_messages = [HumanMessage(query)]
output = app.invoke(
{"messages": input_messages},
config,
)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================

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

为了帮助您理解内部发生了什么,请查看这个LangSmith跟踪

管理对话历史

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

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

我们可以通过在提示前添加一个简单的步骤来适当地修改messages键,然后将这个新链包装在消息历史类中。

LangChain 提供了一些内置的帮助工具来管理消息列表。在这种情况下,我们将使用trim_messages帮助工具来减少我们发送给模型的消息数量。该修剪器允许我们指定要保留的令牌数量,以及其他参数,例如是否始终保留系统消息以及是否允许部分消息:

from langchain_core.messages import SystemMessage, trim_messages

trimmer = trim_messages(
max_tokens=65,
strategy="last",
token_counter=model,
include_system=True,
allow_partial=False,
start_on="human",
)

messages = [
SystemMessage(content="you're a good assistant"),
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!"),
]

trimmer.invoke(messages)
[SystemMessage(content="you're a good assistant", additional_kwargs={}, response_metadata={}),
HumanMessage(content='whats 2 + 2', additional_kwargs={}, response_metadata={}),
AIMessage(content='4', additional_kwargs={}, response_metadata={}),
HumanMessage(content='thanks', additional_kwargs={}, response_metadata={}),
AIMessage(content='no problem!', additional_kwargs={}, response_metadata={}),
HumanMessage(content='having fun?', additional_kwargs={}, response_metadata={}),
AIMessage(content='yes!', additional_kwargs={}, response_metadata={})]

要在我们的链中使用它,我们只需要在将messages输入传递给我们的提示之前运行修剪器。

workflow = StateGraph(state_schema=State)


def call_model(state: State):
trimmed_messages = trimmer.invoke(state["messages"])
prompt = prompt_template.invoke(
{"messages": trimmed_messages, "language": state["language"]}
)
response = model.invoke(prompt)
return {"messages": [response]}


workflow.add_edge(START, "model")
workflow.add_node("model", call_model)

memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

现在如果我们尝试询问模型我们的名字,它不会知道,因为我们删除了聊天历史的那部分:

config = {"configurable": {"thread_id": "abc567"}}
query = "What is my name?"
language = "English"

input_messages = messages + [HumanMessage(query)]
output = app.invoke(
{"messages": input_messages, "language": language},
config,
)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================

I don't know your name. You haven't told me yet!

但如果我们询问最近几条消息中的信息,它会记得:

config = {"configurable": {"thread_id": "abc678"}}
query = "What math problem did I ask?"
language = "English"

input_messages = messages + [HumanMessage(query)]
output = app.invoke(
{"messages": input_messages, "language": language},
config,
)
output["messages"][-1].pretty_print()
================================== Ai Message ==================================

You asked what 2 + 2 equals.

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

流处理

现在我们有了一个功能正常的聊天机器人。然而,对于聊天机器人应用程序来说,一个非常重要的用户体验考虑是流式传输。大型语言模型有时可能需要一些时间来响应,因此为了改善用户体验,大多数应用程序会做的一件事是随着每个标记的生成而流式返回。这使用户能够看到进度。

实际上,这超级容易做到!

默认情况下,我们的LangGraph应用程序中的.stream会流式传输应用程序步骤——在这种情况下,是模型响应的单个步骤。设置stream_mode="messages"允许我们流式传输输出令牌:

config = {"configurable": {"thread_id": "abc789"}}
query = "Hi I'm Todd, please tell me a joke."
language = "English"

input_messages = [HumanMessage(query)]
for chunk, metadata in app.stream(
{"messages": input_messages, "language": language},
config,
stream_mode="messages",
):
if isinstance(chunk, AIMessage): # Filter to just model responses
print(chunk.content, end="|")
|Hi| Todd|!| Here|’s| a| joke| for| you|:

|Why| don|’t| skeleton|s| fight| each| other|?

|Because| they| don|’t| have| the| guts|!||

下一步

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

  • Conversational RAG: 启用基于外部数据源的聊天机器人体验
  • Agents: 构建一个可以采取行动的聊天机器人

如果你想深入了解具体细节,一些值得查看的内容包括:


这个页面有帮助吗?