跳至主要内容

构建聊天机器人

先决条件

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

本指南需要 langgraph >= 0.2.28

注意

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

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

概述

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

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

  • 对话式 RAG: 在外部数据源上启用聊天机器人体验
  • 代理: 构建一个能够执行操作的聊天机器人

本教程将涵盖基础知识,这对于这两个更高级的主题会有所帮助,但如果您愿意,可以跳过直接前往这两个主题。

设置

Jupyter Notebook

本指南(以及文档中的大多数其他指南)使用 Jupyter 笔记本,并假设读者也是如此。Jupyter 笔记本非常适合学习如何使用 LLM 系统,因为很多时候事情可能会出错(意外输出、API 宕机等),在交互式环境中逐步完成指南是更好地理解它们的绝佳方式。

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

安装

对于本教程,我们需要 @langchain/corelanggraph

yarn add @langchain/core @langchain/langgraph uuid

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

LangSmith

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

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

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

快速入门

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

选择您的聊天模型

安装依赖项

yarn add @langchain/openai 

添加环境变量

OPENAI_API_KEY=your-api-key

实例化模型

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

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

让我们首先直接使用模型。ChatModel是 LangChain “运行器”的实例,这意味着它们公开了一个用于与它们交互的标准接口。要简单地调用模型,我们可以将消息列表传递到.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 });

现在我们需要创建一个config,每次都将其传递给运行器。此配置包含不属于输入本身的信息,但仍然有用。在本例中,我们希望包含一个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
}
}

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

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

提示模板

提示模板有助于将原始用户的信息转换为 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
}
}

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

const prompt2 = ChatPromptTemplate.fromMessages([
[
"system",
"You are a helpful assistant. Answer all questions to the best of your ability in {language}.",
],
new MessagesPlaceholder("messages"),
]);

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

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

// Define the State
const GraphAnnotation = Annotation.Root({
...MessagesAnnotation.spec,
language: Annotation<string>(),
});

// Define the function that calls the model
const callModel3 = async (state: typeof GraphAnnotation.State) => {
const chain = prompt2.pipe(llm);
const response = await chain.invoke(state);
return { messages: [response] };
};

const workflow3 = new StateGraph(GraphAnnotation)
.addNode("model", callModel3)
.addEdge(START, "model")
.addEdge("model", END);

const app3 = workflow3.compile({ checkpointer: new MemorySaver() });
const config4 = { configurable: { thread_id: uuidv4() } };
const input6 = {
messages: [
{
role: "user",
content: "Hi im bob",
},
],
language: "Spanish",
};
const output7 = await app3.invoke(input6, config4);
console.log(output7.messages[output7.messages.length - 1]);
AIMessage {
"id": "chatcmpl-ABUXkq2ZV9xmOBSM2iJbYSn8Epvqa",
"content": "¡Hola, Bob! ¿En qué puedo ayudarte hoy?",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 12,
"promptTokens": 32,
"totalTokens": 44
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 32,
"output_tokens": 12,
"total_tokens": 44
}
}

请注意,整个状态都是持久化的,因此如果不需要更改,我们可以省略诸如language之类的参数

const input7 = {
messages: [
{
role: "user",
content: "What is my name?",
},
],
};
const output8 = await app3.invoke(input7, config4);
console.log(output8.messages[output8.messages.length - 1]);
AIMessage {
"id": "chatcmpl-ABUXk9Ccr1dhmA9lZ1VmZ998PFyJF",
"content": "Tu nombre es Bob. ¿Hay algo más en lo que te pueda ayudar?",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 16,
"promptTokens": 57,
"totalTokens": 73
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 57,
"output_tokens": 16,
"total_tokens": 73
}
}

为了帮助您了解内部发生的事情,请查看此 LangSmith 跟踪

管理对话历史记录

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

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

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

LangChain 提供了一些用于管理消息列表的内置助手。在本例中,我们将使用trimMessages助手来减少发送给模型的消息数量。修剪器使我们能够指定要保留的标记数,以及其他参数,例如是否要始终保留系统消息以及是否允许部分消息

import {
SystemMessage,
HumanMessage,
AIMessage,
trimMessages,
} from "@langchain/core/messages";

const trimmer = trimMessages({
maxTokens: 10,
strategy: "last",
tokenCounter: (msgs) => msgs.length,
includeSystem: true,
allowPartial: false,
startOn: "human",
});

const messages = [
new SystemMessage("you're a good assistant"),
new HumanMessage("hi! I'm bob"),
new AIMessage("hi!"),
new HumanMessage("I like vanilla ice cream"),
new AIMessage("nice"),
new HumanMessage("whats 2 + 2"),
new AIMessage("4"),
new HumanMessage("thanks"),
new AIMessage("no problem!"),
new HumanMessage("having fun?"),
new AIMessage("yes!"),
];

await trimmer.invoke(messages);
[
SystemMessage {
"content": "you're a good assistant",
"additional_kwargs": {},
"response_metadata": {}
},
HumanMessage {
"content": "I like vanilla ice cream",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"content": "nice",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
},
HumanMessage {
"content": "whats 2 + 2",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"content": "4",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
},
HumanMessage {
"content": "thanks",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"content": "no problem!",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
},
HumanMessage {
"content": "having fun?",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"content": "yes!",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
}
]

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

const callModel4 = async (state: typeof GraphAnnotation.State) => {
const chain = prompt2.pipe(llm);
const trimmedMessage = await trimmer.invoke(state.messages);
const response = await chain.invoke({
messages: trimmedMessage,
language: state.language,
});
return { messages: [response] };
};

const workflow4 = new StateGraph(GraphAnnotation)
.addNode("model", callModel4)
.addEdge(START, "model")
.addEdge("model", END);

const app4 = workflow4.compile({ checkpointer: new MemorySaver() });

现在,如果我们尝试询问模型我们的姓名,它将不知道,因为我们修剪了聊天历史记录的那一部分

const config5 = { configurable: { thread_id: uuidv4() } };
const input8 = {
messages: [...messages, new HumanMessage("What is my name?")],
language: "English",
};

const output9 = await app4.invoke(input8, config5);
console.log(output9.messages[output9.messages.length - 1]);
AIMessage {
"id": "chatcmpl-ABUdCOvzRAvgoxd2sf93oGKQfA9vh",
"content": "I don’t know your name, but I’d be happy to learn it if you’d like to share!",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 22,
"promptTokens": 97,
"totalTokens": 119
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 97,
"output_tokens": 22,
"total_tokens": 119
}
}

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

const config6 = { configurable: { thread_id: uuidv4() } };
const input9 = {
messages: [...messages, new HumanMessage("What math problem did I ask?")],
language: "English",
};

const output10 = await app4.invoke(input9, config6);
console.log(output10.messages[output10.messages.length - 1]);
AIMessage {
"id": "chatcmpl-ABUdChq5JOMhcFA1dB7PvCHLyliwM",
"content": "You asked for the solution to the math problem \"what's 2 + 2,\" and I answered that it equals 4.",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 27,
"promptTokens": 99,
"totalTokens": 126
},
"finish_reason": "stop",
"system_fingerprint": "fp_1bb46167f9"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 99,
"output_tokens": 27,
"total_tokens": 126
}
}

如果您查看 LangSmith,您可以在LangSmith 跟踪中看到幕后到底发生了什么。

后续步骤

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

  • 对话式 RAG: 在外部数据源上启用聊天机器人体验
  • 代理: 构建一个能够执行操作的聊天机器人

如果您想更深入地了解具体细节,以下是一些值得查看的内容


此页面对您有帮助吗?


您也可以留下详细的反馈 在 GitHub 上.