构建一个聊天机器人
概述
我们将介绍如何设计和实现一个由 LLM 支持的聊天机器人。该聊天机器人能够进行对话并记住之前的交互。
请注意,我们构建的这个聊天机器人只会使用语言模型进行对话。还有其他几个相关概念,您可能正在寻找
本教程将涵盖基础知识,这对于这两个更高级的主题很有用,但如果您愿意,可以跳过直接进入那里。
设置
安装
要安装 LangChain,请运行
- npm
- yarn
- pnpm
npm i langchain
yarn add langchain
pnpm add langchain
有关更多详细信息,请参阅我们的 安装指南。
LangSmith
您使用 LangChain 构建的许多应用程序将包含多个步骤,并包含对 LLM 调用的多次调用。随着这些应用程序变得越来越复杂,能够检查链或代理内部究竟发生了什么变得至关重要。最好的方法是使用 LangSmith。
在您通过上面的链接注册后,请确保设置您的环境变量以开始记录跟踪
export LANGCHAIN_TRACING_V2="true"
export LANGCHAIN_API_KEY="..."
# Reduce tracing latency if you are not in a serverless environment
# export LANGCHAIN_CALLBACKS_BACKGROUND=true
快速入门
首先,让我们学习如何直接使用语言模型。LangChain 支持许多不同的语言模型,您可以互换使用它们 - 在下面选择您要使用的模型!
选择您的聊天模型
- OpenAI
- Anthropic
- FireworksAI
- MistralAI
- Groq
- VertexAI
安装依赖项
查看 本节了解有关安装集成包的一般说明.
- npm
- yarn
- pnpm
npm i @langchain/openai
yarn add @langchain/openai
pnpm add @langchain/openai
添加环境变量
OPENAI_API_KEY=your-api-key
实例化模型
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0
});
安装依赖项
查看 本节了解有关安装集成包的一般说明.
- npm
- yarn
- pnpm
npm i @langchain/anthropic
yarn add @langchain/anthropic
pnpm add @langchain/anthropic
添加环境变量
ANTHROPIC_API_KEY=your-api-key
实例化模型
import { ChatAnthropic } from "@langchain/anthropic";
const model = new ChatAnthropic({
model: "claude-3-5-sonnet-20240620",
temperature: 0
});
安装依赖项
查看 本节了解有关安装集成包的一般说明.
- npm
- yarn
- pnpm
npm i @langchain/community
yarn add @langchain/community
pnpm add @langchain/community
添加环境变量
FIREWORKS_API_KEY=your-api-key
实例化模型
import { ChatFireworks } from "@langchain/community/chat_models/fireworks";
const model = new ChatFireworks({
model: "accounts/fireworks/models/llama-v3p1-70b-instruct",
temperature: 0
});
安装依赖项
查看 本节了解有关安装集成包的一般说明.
- npm
- yarn
- pnpm
npm i @langchain/mistralai
yarn add @langchain/mistralai
pnpm add @langchain/mistralai
添加环境变量
MISTRAL_API_KEY=your-api-key
实例化模型
import { ChatMistralAI } from "@langchain/mistralai";
const model = new ChatMistralAI({
model: "mistral-large-latest",
temperature: 0
});
安装依赖项
查看 本节了解有关安装集成包的一般说明.
- npm
- yarn
- pnpm
npm i @langchain/groq
yarn add @langchain/groq
pnpm add @langchain/groq
添加环境变量
GROQ_API_KEY=your-api-key
实例化模型
import { ChatGroq } from "@langchain/groq";
const model = new ChatGroq({
model: "mixtral-8x7b-32768",
temperature: 0
});
安装依赖项
查看 本节了解有关安装集成包的一般说明.
- npm
- yarn
- pnpm
npm i @langchain/google-vertexai
yarn add @langchain/google-vertexai
pnpm add @langchain/google-vertexai
添加环境变量
GOOGLE_APPLICATION_CREDENTIALS=credentials.json
实例化模型
import { ChatVertexAI } from "@langchain/google-vertexai";
const model = new ChatVertexAI({
model: "gemini-1.5-flash",
temperature: 0
});
首先,让我们直接使用模型。ChatModel
是 LangChain “Runnable” 的实例,这意味着它们公开了一个用于与其交互的标准接口。要简单地调用模型,我们可以将消息列表传递给 .invoke
方法。
import { HumanMessage } from "@langchain/core/messages";
await model.invoke([new HumanMessage({ content: "Hi! I'm Bob" })]);
AIMessage {
"id": "chatcmpl-A64of8iD4GIFNSYlOaFHxPdCeyl9E",
"content": "Hi Bob! How can I assist you today?",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 10,
"promptTokens": 11,
"totalTokens": 21
},
"finish_reason": "stop"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 11,
"output_tokens": 10,
"total_tokens": 21
}
}
模型本身没有状态的概念。例如,如果您提出一个后续问题
await model.invoke([new HumanMessage({ content: "What's my name?" })]);
AIMessage {
"id": "chatcmpl-A64ogC7owxmPla3ggZERNCFZpVHSp",
"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. If you'd like to tell me your name, feel free!",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 39,
"promptTokens": 11,
"totalTokens": 50
},
"finish_reason": "stop"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 11,
"output_tokens": 39,
"total_tokens": 50
}
}
让我们看一下示例 LangSmith 跟踪
我们可以看到它没有将先前的对话轮次纳入上下文,也无法回答问题。这会导致糟糕的聊天机器人体验!
为了解决这个问题,我们需要将整个对话历史记录传递给模型。让我们看看这样做会发生什么
import { AIMessage } from "@langchain/core/messages";
await model.invoke([
new HumanMessage({ content: "Hi! I'm Bob" }),
new AIMessage({ content: "Hello Bob! How can I assist you today?" }),
new HumanMessage({ content: "What's my name?" }),
]);
AIMessage {
"id": "chatcmpl-A64ohhg3P4BuIiw8mUCLI3zYHNOvS",
"content": "Your name is Bob! How can I help you today, Bob?",
"additional_kwargs": {},
"response_metadata": {
"tokenUsage": {
"completionTokens": 14,
"promptTokens": 33,
"totalTokens": 47
},
"finish_reason": "stop"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 33,
"output_tokens": 14,
"total_tokens": 47
}
}
现在我们可以看到我们得到了一个很好的回应!
这是支撑聊天机器人进行对话交互的基本思想。那么我们如何最好地实现它呢?
消息历史记录
我们可以使用 Message History 类来包装我们的模型并使其具有状态。这将跟踪模型的输入和输出,并将它们存储在某个数据存储中。未来的交互将加载这些消息并将它们作为输入的一部分传递到链中。让我们看看如何使用它!
我们导入相关的类并设置我们的链,该链包装模型并添加此消息历史记录。这里一个关键部分是我们传入 getSessionHistory()
的函数。此函数应接收 sessionId
并返回一个 Message History 对象。此 sessionId
用于区分单独的对话,应在调用新链时作为配置的一部分传递。
我们还可以通过添加提示来帮助格式化来创建一个简单的链
// We use an ephemeral, in-memory chat history for this demo.
import { InMemoryChatMessageHistory } from "@langchain/core/chat_history";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
const messageHistories: Record<string, InMemoryChatMessageHistory> = {};
const prompt = ChatPromptTemplate.fromMessages([
[
"system",
`You are a helpful assistant who remembers all details the user shares with you.`,
],
["placeholder", "{chat_history}"],
["human", "{input}"],
]);
const chain = prompt.pipe(model);
const withMessageHistory = new RunnableWithMessageHistory({
runnable: chain,
getMessageHistory: async (sessionId) => {
if (messageHistories[sessionId] === undefined) {
messageHistories[sessionId] = new InMemoryChatMessageHistory();
}
return messageHistories[sessionId];
},
inputMessagesKey: "input",
historyMessagesKey: "chat_history",
});
现在我们需要创建一个 config
,每次都将其传递到可运行文件中。此配置包含不直接属于输入的信息,但仍然有用。在这种情况下,我们想包含一个 session_id
。它应该像这样
const config = {
configurable: {
sessionId: "abc2",
},
};
const response = await withMessageHistory.invoke(
{
input: "Hi! I'm Bob",
},
config
);
response.content;
"Hi Bob! How can I assist you today?"
const followupResponse = await withMessageHistory.invoke(
{
input: "What's my name?",
},
config
);
followupResponse.content;
"Your name is Bob. How can I help you today?"
太好了!我们的聊天机器人现在记住了关于我们的事情。如果我们将配置更改为引用不同的 session_id
,我们可以看到它会从头开始对话。
const config2 = {
configurable: {
sessionId: "abc3",
},
};
const response2 = await withMessageHistory.invoke(
{
input: "What's my name?",
},
config2
);
response2.content;
"I'm sorry, but I don't have your name. If you tell me, I'll remember it for our future conversations"... 1 more character
但是,我们始终可以回到原始对话(因为我们将其持久化到数据库中)
const config3 = {
configurable: {
sessionId: "abc2",
},
};
const response3 = await withMessageHistory.invoke(
{
input: "What's my name?",
},
config3
);
response3.content;
"Your name is Bob. What would you like to talk about?"
这就是我们如何支持聊天机器人与许多用户进行对话!
管理对话历史记录
构建聊天机器人时要理解的一个重要概念是如何管理对话历史记录。如果任其发展,消息列表将无限制地增长,并可能超出 LLM 的上下文窗口。因此,重要的是添加一个步骤来限制您传入的消息的大小。
重要的是,您需要在提示模板之前但加载 Message History 中的先前消息之后执行此操作。
我们可以通过在提示前面添加一个简单的步骤来修改 chat_history
键来实现这一点,然后将该新的链包装在 Message History 类中。首先,让我们定义一个函数来修改传入的消息。让我们使其选择 10 条最近的消息。然后,我们可以通过在开头添加它来创建一个新的链。
import type { BaseMessage } from "@langchain/core/messages";
import {
RunnablePassthrough,
RunnableSequence,
} from "@langchain/core/runnables";
type ChainInput = {
chat_history: BaseMessage[];
input: string;
};
const filterMessages = (input: ChainInput) => input.chat_history.slice(-10);
const chain2 = RunnableSequence.from<ChainInput>([
RunnablePassthrough.assign({
chat_history: filterMessages,
}),
prompt,
model,
]);
现在让我们试一试!如果我们创建一个超过 10 条消息的消息列表,我们可以看到它不再记得早期消息中的信息。
const messages = [
new HumanMessage({ content: "hi! I'm bob" }),
new AIMessage({ content: "hi!" }),
new HumanMessage({ content: "I like vanilla ice cream" }),
new AIMessage({ content: "nice" }),
new HumanMessage({ content: "whats 2 + 2" }),
new AIMessage({ content: "4" }),
new HumanMessage({ content: "thanks" }),
new AIMessage({ content: "No problem!" }),
new HumanMessage({ content: "having fun?" }),
new AIMessage({ content: "yes!" }),
new HumanMessage({ content: "That's great!" }),
new AIMessage({ content: "yes it is!" }),
];
const response4 = await chain2.invoke({
chat_history: messages,
input: "what's my name?",
});
response4.content;
"You haven't shared your name with me yet. What is it?"
但是,如果我们询问过去十条消息中的信息,它仍然会记得。
const response5 = await chain2.invoke({
chat_history: messages,
input: "what's my fav ice cream",
});
response5.content;
"Your favorite ice cream is vanilla!"
现在让我们将此链包装在 RunnableWithMessageHistory
构造函数中。为了演示,我们还将稍微修改我们的 getMessageHistory()
方法,使其始终以先前声明的 10 条消息列表开始新会话,以模拟多个对话回合。
const messageHistories2: Record<string, InMemoryChatMessageHistory> = {};
const withMessageHistory2 = new RunnableWithMessageHistory({
runnable: chain2,
getMessageHistory: async (sessionId) => {
if (messageHistories2[sessionId] === undefined) {
const messageHistory = new InMemoryChatMessageHistory();
await messageHistory.addMessages(messages);
messageHistories2[sessionId] = messageHistory;
}
return messageHistories2[sessionId];
},
inputMessagesKey: "input",
historyMessagesKey: "chat_history",
});
const config4 = {
configurable: {
sessionId: "abc4",
},
};
const response7 = await withMessageHistory2.invoke(
{
input: "whats my name?",
chat_history: [],
},
config4
);
response7.content;
"You haven't shared your name with me yet. What is it?"
现在聊天历史记录中有两条新消息。这意味着我们对话历史记录中曾经可访问的更多信息现在不可用了!
const response8 = await withMessageHistory2.invoke(
{
input: "whats my favorite ice cream?",
chat_history: [],
},
config4
);
response8.content;
"You haven't mentioned your favorite ice cream yet. What is it?"
如果您查看 LangSmith,您可以确切地了解 LangSmith 跟踪中幕后发生了什么 LangSmith 跟踪。导航到聊天模型调用以查看哪些消息被过滤掉了。
流式传输
现在我们已经有了功能齐全的聊天机器人。但是,对于聊天机器人应用程序来说,一个非常重要的 UX 考虑因素是流式传输。LLM 有时可能需要一段时间才能响应,因此为了改善用户体验,大多数应用程序会将生成的每个 token 流式传输回来。这允许用户看到进度。
实际上这非常容易做到!
所有链都公开了一个 .stream()
方法,使用消息历史记录的链也不例外。我们可以简单地使用该方法来获取流式响应。
const config5 = {
configurable: {
sessionId: "abc6",
},
};
const stream = await withMessageHistory2.stream(
{
input: "hi! I'm todd. tell me a joke",
chat_history: [],
},
config5
);
for await (const chunk of stream) {
console.log("|", chunk.content);
}
|
| Hi
| Todd
| !
| Here
| ’s
| a
| joke
| for
| you
| :
|
| Why
| did
| the
| scare
| crow
| win
| an
| award
| ?
|
| Because
| he
| was
| outstanding
| in
| his
| field
| !
|
下一步
现在您已经了解了如何在 LangChain 中创建聊天机器人的基础知识,以下是一些您可能感兴趣的更高级的教程
如果您想深入了解具体内容,一些值得关注的内容是
- 流式传输:流式传输对于聊天应用程序至关重要
- 如何添加消息历史记录:更深入地了解与消息历史记录相关的所有内容