跳到主要内容

构建聊天机器人

先决条件

本指南假设您熟悉以下概念:

  • 聊天模型
  • 提示模板
  • 聊天历史记录

本指南需要 langgraph >= 0.2.28。

笔记

本教程之前使用 RunnableWithMessageHistory 构建了一个聊天机器人。您可以在 v0.2 文档中访问此版本的教程。

LangGraph 实现比 RunnableWithMessageHistory 具有许多优势,包括能够持久保存应用程序状态的任意组件(而不仅仅是消息)。

概览

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

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

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

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

设置

Jupyter 笔记本

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

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

安装

在本教程中,我们需要@langchain/core 和 langgraph

pnpm add @langchain/core @langchain/langgraph uuid

LangSmith

您使用 LangChain 构建的许多应用程序将包含多个步骤,并多次调用 LLM 调用。随着这些应用程序变得越来越复杂,能够检查链或代理内部究竟发生了什么变得至关重要。最好的方法是使用 LangSmith。

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

process.env.LANGCHAIN_TRACING_V2 = "true";
process.env.LANGCHAIN_API_KEY = "...";

快速开始

实例化模型

import { ChatOpenAI } from "@langchain/openai";

const llm = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0
});

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

await llm.invoke([{ role: "user", content: "Hi im bob" }]);
AIMessage {
"id": "chatcmpl-ABUXeSO4JQpxO96lj7iudUptJ6nfW",
"content": "Hi Bob! How can I assist you today?",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 10,
"promptTokens": 10,
"totalTokens": 20
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 10,
"output_tokens": 10,
"total_tokens": 20
}
}

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

await llm.invoke([{ role: "user", content: "Whats my name" }]);
AIMessage {
"id": "chatcmpl-ABUXe1Zih4gMe3XgotWL83xeWub2h",
"content": "I'm sorry, but I don't have access to personal information about individuals unless it has been shared with me during our conversation. If you'd like to tell me your name, feel free to do so!",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 39,
"promptTokens": 10,
"totalTokens": 49
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 10,
"output_tokens": 39,
"total_tokens": 49
}
}

让我们看一下 LangSmith 跟踪示例

我们可以看到,它没有将之前的对话转变为上下文,也无法回答问题。这给聊天机器人带来了糟糕的体验!

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

await llm.invoke([
{ role: "user", content: "Hi! I'm Bob" },
{ role: "assistant", content: "Hello Bob! How can I assist you today?" },
{ role: "user", content: "What's my name?" },
]);
AIMessage {
"id": "chatcmpl-ABUXfX4Fnp247rOxyPlBUYMQgahj2",
"content": "Your name is Bob! How can I help you today?",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 12,
"promptTokens": 33,
"totalTokens": 45
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 33,
"output_tokens": 12,
"total_tokens": 45
}
}

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

这是聊天机器人对话式互动能力的基本理念。那么我们如何才能最好地实现这一点呢?

消息持久化

LangGraph 实现了内置持久层,使其成为支持多轮对话的聊天应用程序的理想选择。

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

LangGraph 附带一个简单的内存检查点,我们将在下面使用它。

import {
START,
END,
MessagesAnnotation,
StateGraph,
MemorySaver,
} from "@langchain/langgraph";

// Define the function that calls the model
const callModel = async (state: typeof MessagesAnnotation.State) => {
const response = await llm.invoke(state.messages);
return { messages: response };
};

// Define a new graph
const workflow = new StateGraph(MessagesAnnotation)
// Define the node and edge
.addNode("model", callModel)
.addEdge(START, "model")
.addEdge("model", END);

// Add memory
const memory = new MemorySaver();
const app = workflow.compile({ checkpointer: memory });

现在我们需要创建一个配置,每次将其传递给可运行程序。此配置包含不直接属于输入但仍然有用的信息。在本例中,我们希望包含一个 thread_id。它应该如下所示:

import { v4 as uuidv4 } from "uuid";

const config = { configurable: { thread_id: uuidv4() } };

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

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

const input = [
{
role: "user",
content: "Hi! I'm Bob.",
},
];
const output = await app.invoke({ messages: input }, config);
// The output contains all messages in the state.
// This will long the last message in the conversation.
console.log(output.messages[output.messages.length - 1]);
AIMessage {
"id": "chatcmpl-ABUXfjqCno78CGXCHoAgamqXG1pnZ",
"content": "Hi Bob! How can I assist you today?",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 10,
"promptTokens": 12,
"totalTokens": 22
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 12,
"output_tokens": 10,
"total_tokens": 22
}
}
const input2 = [
{
role: "user",
content: "What's my name?",
},
];
const output2 = await app.invoke({ messages: input2 }, config);
console.log(output2.messages[output2.messages.length - 1]);
AIMessage {
"id": "chatcmpl-ABUXgzHFHk4KsaNmDJyvflHq4JY2L",
"content": "Your name is Bob! How can I help you today, Bob?",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 14,
"promptTokens": 34,
"totalTokens": 48
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 34,
"output_tokens": 14,
"total_tokens": 48
}
}

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

const config2 = { configurable: { thread_id: uuidv4() } };
const input3 = [
{
role: "user",
content: "What's my name?",
},
];
const output3 = await app.invoke({ messages: input3 }, config2);
console.log(output3.messages[output3.messages.length - 1]);
AIMessage {
"id": "chatcmpl-ABUXhT4EVx8mGgmKXJ1s132qEluxR",
"content": "I'm sorry, but I don’t have access to personal data about individuals unless it has been shared in the course of our conversation. Therefore, I don't know your name. How can I assist you today?",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 41,
"promptTokens": 11,
"totalTokens": 52
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 11,
"output_tokens": 41,
"total_tokens": 52
}
}

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

const output4 = await app.invoke({ messages: input2 }, config);
console.log(output4.messages[output4.messages.length - 1]);
AIMessage {
"id": "chatcmpl-ABUXhZmtzvV3kqKig47xxhKEnvVfH",
"content": "Your name is Bob! If there's anything else you'd like to talk about or ask, feel free!",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 20,
"promptTokens": 60,
"totalTokens": 80
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 60,
"output_tokens": 20,
"total_tokens": 80
}
}

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

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

prompt 模板

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

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

import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";

const prompt = ChatPromptTemplate.fromMessages([
[
"system",
"You talk like a pirate. Answer all questions to the best of your ability.",
],
new MessagesPlaceholder("messages"),
]);

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

import {
START,
END,
MessagesAnnotation,
StateGraph,
MemorySaver,
} from "@langchain/langgraph";

// Define the function that calls the model
const callModel2 = async (state: typeof MessagesAnnotation.State) => {
const chain = prompt.pipe(llm);
const response = await chain.invoke(state);
// Update message history with response:
return { messages: [response] };
};

// Define a new graph
const workflow2 = new StateGraph(MessagesAnnotation)
// Define the (single) node in the graph
.addNode("model", callModel2)
.addEdge(START, "model")
.addEdge("model", END);

// Add memory
const app2 = workflow2.compile({ checkpointer: new MemorySaver() });

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

const config3 = { configurable: { thread_id: uuidv4() } };
const input4 = [
{
role: "user",
content: "Hi! I'm Jim.",
},
];
const output5 = await app2.invoke({ messages: input4 }, config3);
console.log(output5.messages[output5.messages.length - 1]);
AIMessage {
"id": "chatcmpl-ABUXio2Vy1YNRDiFdKKEyN3Yw1B9I",
"content": "Ahoy, Jim! What brings ye to these treacherous waters today? Speak up, matey!",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 22,
"promptTokens": 32,
"totalTokens": 54
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 32,
"output_tokens": 22,
"total_tokens": 54
}
}
const input5 = [
{
role: "user",
content: "What is my name?",
},
];
const output6 = await app2.invoke({ messages: input5 }, config3);
console.log(output6.messages[output6.messages.length - 1]);
AIMessage {
"id": "chatcmpl-ABUXjZNHiT5g7eTf52auWGXDUUcDs",
"content": "Ye be callin' yerself Jim, if me memory serves me right! Arrr, what else can I do fer ye, matey?",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 31,
"promptTokens": 67,
"totalTokens": 98
},
"finish_reason": "stop",
"system_fingerprint": "fp_3a215618e8"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 67,
"output_tokens": 31,
"total_tokens": 98
}
}