介绍
本教程将指导你创建一个可以和你的文档聊天的机器人。我们将利用 LangChain 进行自然语言处理、文档处理以及矢量数据库进行高效数据检索。技术栈包括 Vite 进行项目设置、React 进行前端开发、TailwindCSS 进行样式设计、OpenAI 生成响应以及 Supabase 作为矢量数据库。该指南旨在通过介绍如何从头开始使用 LangChain 构建聊天机器人,并将涵盖如何有效地使用 LangChain 开发聊天机器人。通过本教程,您将学会如何使用 LangChain 构建一个 LLM RAG 聊天机器人。
完整项目代码可在 GitHub 上找到。
适合人群
本教程适用于以下开发人员:
- 希望使用自己的文档构建 AI 驱动的应用程序。
- 寻求将矢量数据库和 AI 模型集成到项目中。
内容概述
- 使用 Vite 和 TailwindCSS 设置新项目。
- 将 LangChain 与 OpenAI 集成以实现 AI 功能。
- 使用 Supabase 作为矢量数据库存储和检索文档数据。
- 构建聊天机器人的前端组件。
- 创建文档处理和 AI 响应生成的实用工具。
架构
准备工作
在 Supabase 上创建一个矢量存储,请参考此 GitHub Repo。
步骤指南
步骤1:使用 Vite 初始化项目
创建一个新的 Vite 项目:
npm create vite@latest ai-assistant -- --template react
cd ai-assistant
步骤2:安装必要的依赖
安装 TailwindCSS 及其他依赖:
npm install tailwindcss postcss autoprefixer react-router-dom @langchain/core @langchain/openai @supabase/supabase-js
步骤3:配置 TailwindCSS
初始化 TailwindCSS:
npx tailwindcss init -p
配置 tailwind.config.js:
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
在 src/index.css 中添加 Tailwind 指令:
@tailwind base;
@tailwind components;
@tailwind utilities;
步骤4:配置环境变量
在项目根目录下创建 .env 文件,并添加您的 API 密钥:
VITE_SUPABASE_BASE_URL=your-supabase-url
VITE_SUPABASE_API_KEY=your-supabase-api-key
VITE_OPENAI_API_KEY=your-openai-api-key
VITE_OPENAI_BASE_URL=https://api.openai.com/v1
步骤5:项目目录结构
将您的项目文件组织如下:
src/
├── components/
│ ├── Avatar.jsx
│ ├── Chat.jsx
│ ├── ChatBox.jsx
│ ├── Header.jsx
│ └── Message.jsx
├── utils/
│ ├── chain.js
│ ├── combineDocuments.js
│ ├── formatConvHistory.js
│ └── retriever.js
├── App.jsx
├── main.jsx
├── index.css
└── custom.css
步骤6:主入口文件
src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import './custom.css'; // 自定义 CSS
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
main.jsx 文件通过将 App 组件渲染到根元素中来设置 React 应用程序。它还导入了主要样式,包括 TailwindCSS 和任何自定义 CSS。
步骤7:创建组件
App 组件
src/App.jsx
import Chat from './components/Chat'
function App() {
return (
<Chat />
)
}
export default App
App 组件作为应用程序的根组件,它简单地渲染包含主要功能的 Chat 组件。
Chat 组件
src/components/Chat.jsx
import { useState, useEffect } from 'react';
import Header from './Header';
import Message from './Message';
import ChatBox from './ChatBox';
import robotAvatar from '../assets/robot-avatar.png';
import userAvatar from '../assets/profile.jpg';
import { chain } from '../utils/chain';
import { formatConvHistory } from '../utils/formatConvHistory';
function Chat() {
const [messages, setMessages] = useState(() => {
// 从本地存储中检索消息(如果有)
const savedMessages = localStorage.getItem('messages');
return savedMessages ? JSON.parse(savedMessages) : [];
});
const [isLoading, setIsLoading] = useState(false);
// 每当消息发生变化时,将其保存到本地存储中
useEffect(() => {
localStorage.setItem('messages', JSON.stringify(messages));
}, [messages]);
// 处理用户发送的新消息
const handleNewMessage = async (text) => {
const newMessage = {
time: new Date().toLocaleTimeString(),
text,
avatarSrc: userAvatar,
avatarAlt: "用户头像",
position: "left",
isRobot: false,
};
setMessages((prevMessages) => [...prevMessages, newMessage]);
const updatedMessages = [...messages, newMessage];
setIsLoading(true);
try {
// 调用 LangChain API 获取 AI 的响应
const response = await chain.invoke({
question: text,
conv_history: formatConvHistory(updatedMessages.map(msg => msg.text)),
});
const aiMessage = {
time: new Date().toLocaleTimeString(),
text: response,
avatarSrc: robotAvatar,
avatarAlt: "机器人头像",
position: "right",
isRobot: true,
};
setMessages((prevMessages) => [...prevMessages, aiMessage]);
} catch (error) {
console.error("获取 AI 响应时出错:", error);
} finally {
setIsLoading(false);
}
};
return (
<main className="font-merriweather px-10 py-8 mx-auto w-full bg-sky-950 max-w-[480px] h-screen">
<Header
mainIconSrc={robotAvatar}
mainIconAlt="主图标"
title="AI-Assistant"
/>
<div id="chatbot-conversation-container" className="flex flex-col gap-y-2 mt-4">
{messages.map((message, index) => (
<Message
key={index}
time={message.time}
text={message.text}
avatarSrc={message.avatarSrc}
avatarAlt={message.avatarAlt}
position={message.position}
isRobot={message.isRobot}
/>
))}
</div>
<div className="mt-auto mb-4">
<ChatBox
label="在想什么?"
buttonText="询问"
onSubmit={handleNewMessage}
isLoading={isLoading}
/>
</div>
</main>
);
}
export default Chat;
Chat组件是 AI 助手的核心,处理消息状态和交互逻辑。useState用于初始化和管理messages状态,该状态保存聊天消息,以及isLoading状态,指示消息是否正在处理。useEffect钩子确保每当messages状态发生变化时,将消息保存到本地存储。这允许聊天历史在页面重新加载时保持。handleNewMessage函数处理用户发送的新消息。它创建一个新的消息对象,使用新消息更新状态,并将消息发送到 LangChain API 以获取响应。- 然后将 AI 响应添加到消息状态中,使用用户和 AI 消息更新聊天界面。
return语句定义了聊天界面的布局,包括标题、消息列表和输入框。Header组件显示聊天标题和主图标,而Message组件渲染聊天历史中的每条消息。ChatBox组件提供用于发送新消息的输入字段和按钮。
Header 组件
src/components/Header.jsx
import React from "react";
import PropTypes from "prop-types";
import Avatar from "./Avatar";
function Header({ mainIconSrc, mainIconAlt, title }) {
return (
<header className="flex flex-col mx-auto items-center grow shrink-0 basis-0 w-fit">
<Avatar src={mainIconSrc} alt={mainIconAlt} className="aspect-square w-[
129px]" />
<h1 className="mt-3 text-4xl text-center text-stone-200">{title}</h1>
</header>
);
}
Header.propTypes = {
mainIconSrc: PropTypes.string.isRequired,
mainIconAlt: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};
export default Header;
Header 组件显示聊天应用程序的标题和主图标。
Message 组件
src/components/Message.jsx
import PropTypes from "prop-types";
import Avatar from "./Avatar";
const avatarStyles = "rounded-full shrink-0 self-end aspect-square w-[47px]";
const messageTimeStyles = "self-center mt-5 text-xs font-bold leading-4 text-slate-600";
function Message({ text, time, avatarSrc, avatarAlt, position, isRobot }) {
const messageTextStyles = `text-sm leading-4 rounded-xl p-3 mt-2 ${isRobot ? "bg-sky-50 black-800" : "bg-cyan-800 text-white"
} font-merriweather`; // 应用 Merriweather 字体
return (
<section className={`flex gap-2 ${position === 'left' ? 'justify-start' : 'justify-end'} items-center`}>
{position === 'left' && <Avatar src={avatarSrc} alt={avatarAlt} className={avatarStyles} />}
<div className="flex flex-col grow shrink-0 basis-0 w-fit">
{time && <time className={messageTimeStyles}>{time}</time>}
<p className={messageTextStyles}>{text}</p>
</div>
{position === 'right' && <Avatar src={avatarSrc} alt={avatarAlt} className={avatarStyles} />}
</section>
);
}
Message.propTypes = {
text: PropTypes.string.isRequired,
time: PropTypes.string,
avatarSrc: PropTypes.string.isRequired,
avatarAlt: PropTypes.string.isRequired,
position: PropTypes.oneOf(['left', 'right']).isRequired,
isRobot: PropTypes.bool.isRequired,
};
export default Message;
Message组件在聊天界面中显示单个消息。- 它使用
Avatar组件显示消息发送者的头像。 messageTextStyles用于样式化消息文本,不同的样式用于用户和机器人消息。
ChatBox 组件
src/components/ChatBox.jsx
import { useState } from "react";
import PropTypes from "prop-types";
const inputStyles = "w-full px-4 pt-4 pb-4 mt-2 text-base leading-5 bg-sky-900 rounded-xl border-4 border-solid shadow-sm border-slate-600 text-gray-100 resize-none";
const buttonStyles = "w-full px-6 py-3 mt-3 text-2xl font-bold text-center text-white whitespace-nowrap rounded-xl bg-slate-600 hover:bg-slate-700 hover:translate-y-0.5 focus:outline-none ";
const buttonDisabledStyles = "w-full px-6 py-3 mt-3 text-2xl font-bold text-center text-white whitespace-nowrap rounded-xl bg-slate-600 opacity-50 cursor-not-allowed";
function ChatBox({ label, buttonText, onSubmit, isLoading }) {
const [message, setMessage] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
if (message.trim() && !isLoading) {
onSubmit(message);
setMessage('');
}
};
return (
<section className="flex flex-col mt-4">
<form onSubmit={handleSubmit}>
<label htmlFor="chatInput" className="sr-only">{label}</label>
<input
id="chatInput"
placeholder={label}
className={inputStyles}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button
type="submit"
className={isLoading ? buttonDisabledStyles : buttonStyles}
disabled={isLoading}
>
{buttonText}
</button>
</form>
</section>
);
}
ChatBox.propTypes = {
label: PropTypes.string.isRequired,
buttonText: PropTypes.string.isRequired,
onSubmit: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
};
export default ChatBox;
ChatBox组件提供一个输入框和一个按钮,供用户发送消息。- 它使用
useState管理消息输入状态。 handleSubmit函数处理表单提交,使用消息内容调用onSubmit函数。
Avatar 组件
src/components/Avatar.jsx
import React from "react";
import PropTypes from "prop-types";
function Avatar({ src, alt, className }) {
return <img loading="lazy" src={src} alt={alt} className={className} />;
}
Avatar.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
className: PropTypes.string,
};
export default Avatar;
Avatar 组件渲染头像图片。
步骤8:实用工具函数
链实用工具
src/utils/chain.js
import {
RunnablePassthrough,
RunnableSequence,
} from "@langchain/core/runnables";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts"
import { retriever } from './retriever';
import { combineDocuments } from './combineDocuments';
const openAIApiKey = import.meta.env.VITE_OPENAI_API_KEY;
const openAIUrl = import.meta.env.VITE_OPENAI_BASE_URL;
const llm = new ChatOpenAI({
apiKey: openAIApiKey,
configuration: {
baseURL: openAIUrl,
}
});
// 一个保存提示短语的字符串
const standaloneQuestionTemplate = `Given some conversation history (if any) and a question,
convert the question to a standalone question.
conversation history: {conv_history}
question: {question}
standalone question:`;
// 使用 PromptTemplate 和 fromTemplate 方法创建的提示
const standaloneQuestionPrompt = PromptTemplate.fromTemplate(standaloneQuestionTemplate);
const answerTemplate = `You are a helpful and enthusiastic support bot who can answer a given question based on
the context provided and the conversation history. Try to find the answer in the context. If the answer is not given
in the context, find the answer in the conversation history if possible. If you really don't know the answer,
say "I'm sorry, I don't know the answer to that." And direct the questioner to email help@example.com.
Don't try to make up an answer. Always speak as if you were chatting to a friend.
context: {context}
conversation history: {conv_history}
question: {question}
answer: `;
const answerPrompt = PromptTemplate.fromTemplate(answerTemplate);
// 使用模型的 standaloneQuestionPrompt 并 PIPE
const standaloneQuestionChain = standaloneQuestionPrompt
.pipe(llm)
.pipe(new StringOutputParser());
const retrieverChain = RunnableSequence.from([
prevResult => prevResult.standalone_question,
retriever,
combineDocuments,
]);
const answerChain = answerPrompt
.pipe(llm)
.pipe(new StringOutputParser());
const logConvHistory = async (input) => {
console.log('Conversation History:', input.conv_history);
return input;
}
const chain = RunnableSequence.from([
{
standalone_question: standaloneQuestionChain,
original_input: new RunnablePassthrough(),
},
{
context: retrieverChain,
question: ({ original_input }) => original_input.question,
conv_history: ({ original_input }) => original_input.conv_history,
},
logConvHistory,
answerChain,
]);
export { chain };
以下是流程图
该文件设置了一系列操作,以使用 LangChain、OpenAI 和检索器处理用户查询并检索适当的响应。
StandaloneQuestionChain:负责将用户的查询(可能依赖于先前对话历史的上下文)转换为一个独立的问题,可以在没有任何上下文的情况下理解。RetrieverChain:用于从矢量数据库中检索与StandaloneQuestionChain生成的独立问题相关的文档。LogConvHistory:用于打印对话历史到控制台的简单日志记录功能。这有助于调试和理解对话的流向以及它如何影响生成的响应。AnswerChain:负责根据检索到的上下文和对话历史生成对用户问题的响应。
合并文档实用工具
src/utils/combineDocuments.js
export function combineDocuments(docs) {
return docs.map((doc) => doc.pageContent).join('\n\n');
}
此实用函数将多个文档合并为一个字符串。这对于向 AI 模型提供统一的上下文非常有用。
格式化对话历史实用工具
src
/utils/formatConvHistory.js
export function formatConvHistory(messages) {
return messages.map((message, i) => {
if (i % 2 === 0) {
return `人类: ${message}`
} else {
return `AI: ${message}`
}
}).join('\n')
}
此实用函数将 messages 数组格式化为人类和 AI 之间的对话。
检索实用工具
src/utils/retriever.js
import { SupabaseVectorStore } from "@langchain/community/vectorstores/supabase";
import { OpenAIEmbeddings } from "@langchain/openai";
import { createClient } from '@supabase/supabase-js';
const sbUrl = import.meta.env.VITE_SUPABASE_BASE_URL;
const sbApiKey = import.meta.env.VITE_SUPABASE_API_KEY;
const openAIApiKey = import.meta.env.VITE_OPENAI_API_KEY;
const openAIUrl = import.meta.env.VITE_OPENAI_BASE_URL;
const client = createClient(sbUrl, sbApiKey);
const embeddings = new OpenAIEmbeddings({
apiKey: openAIApiKey,
configuration: {
baseURL: openAIUrl
}
});
const vectorStore = new SupabaseVectorStore(embeddings, {
client,
tableName: 'personal_infos',
queryName: 'match_personal_infos',
});
const retriever = vectorStore.asRetriever();
export { retriever };
该实用工具使用 Supabase 和 OpenAI 嵌入设置了一个检索器。
SupabaseVectorStore:使用 Supabase 初始化矢量存储。OpenAIEmbeddings:使用 OpenAI 创建嵌入。- 检索器用于根据用户的查询获取相关文档。
运行应用程序
-
启动开发服务器:
npm run dev -
访问应用程序: 在浏览器中访问
http://localhost:5173。
结论
在本教程中,您学习了如何使用 LangChain、OpenAI 和 Supabase 构建 AI 助手。我们介绍了 Vite 项目的设置、TailwindCSS 的集成以及 React 组件和实用函数的创建。您现在拥有一个功能齐全的 AI 助手,它可以与用户交互并基于对话历史和上下文提供响应。