在本教程中,您将学习如何使用 LangChain
和 Redis
构建一个 GenAI 聊天机器人。您还将学习如何使用 OpenAI
的语言模型来生成对用户查询的响应,以及如何使用 Redis 来存储和检索数据。
以下是涵盖的内容
生成式 AI,也称为 GenAI,是人工智能的一个类别,专门针对基于预先存在的数据创建新内容。它可以生成各种内容类型,包括文本、图像、视频、声音、代码、3D 设计和其他媒体格式。与专注于分析和解释现有数据的传统 AI 模型不同,GenAI 模型从现有数据中学习,然后利用其知识生成完全新颖的东西。
LangChain 是一个用于构建语言模型应用的创新库。它提供了一种结构化的方式来组合不同的组件,例如语言模型(例如 OpenAI 的模型)、存储解决方案(例如 Redis)和自定义逻辑。这种模块化方法促进了复杂 AI 应用的创建,包括聊天机器人。
OpenAI 提供先进的语言模型,例如 GPT-3,这些模型以其理解和生成类似人类的文本的能力彻底改变了该领域。这些模型构成了许多现代 AI 应用(包括聊天机器人)的基础。
以下命令用于克隆本教程中使用的应用的源代码
git clone --branch v9.2.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions
让我们看一下演示应用的架构
products service
: 处理从数据库查询产品并将其返回到前端orders service
: 处理验证和创建订单order history service
: 处理查询客户的订单历史记录payments service
: 处理订单的付款处理api gateway
: 将服务统一到单个端点下mongodb/ postgresql
: 作为写入优化的数据库,用于存储订单、订单历史记录、产品等您无需在演示应用中使用 MongoDB/ Postgresql 作为写入优化的数据库;您可以使用其他 prisma 支持的数据库 。这只是一个示例。
电子商务微服务应用包含一个前端,使用 Next.js 和 TailwindCSS 构建。应用后端使用 Node.js。数据存储在 Redis 和 MongoDB 或 PostgreSQL 中,使用 Prisma。以下是展示电子商务应用前端的屏幕截图。
仪表板: 显示产品列表,并具有不同的搜索功能,可以在设置页面配置。
设置: 通过点击仪表板右上角的齿轮图标访问。在这里控制搜索栏、聊天机器人可见性和其他功能。
仪表板(语义文本搜索): 配置为语义文本搜索,搜索栏支持自然语言查询。例如: "纯棉蓝色衬衫"。
仪表板(语义图像查询): 配置为语义图像摘要搜索,搜索栏支持基于图像的查询。例如: "左胸 Nike 标识"。
聊天机器人: 位于页面右下角,协助产品搜索和详细视图。
在聊天中选择产品会在仪表板上显示其详细信息。
购物车: 将产品添加到购物车,然后使用 "立即购买" 按钮结账。
订单历史记录: 购买后,顶部导航栏中的 "订单" 链接将显示订单状态和历史记录。
管理面板: 通过顶部导航中的 "admin" 链接访问。显示购买统计信息和趋势产品。
1> 创建独立问题: 使用 OpenAI 的语言模型创建一个独立问题。
独立问题仅仅是将问题简化为表达信息请求所需的最小单词数量。
//Example
userQuestion =
"I'm thinking of buying one of your T-shirts but I need to know what your returns policy is as some T-shirts just don't fit me and I don't want to waste money.";
//semanticMeaning of above question
standAloneQuestion = 'What is your return policy?';
2> 为问题创建嵌入: 创建问题后, OpenAI
的语言模型会为问题生成一个嵌入。
3> 在 Redis 向量存储中找到最近匹配项: 然后使用嵌入来查询 Redis
向量存储。系统将在存储的向量中搜索与问题嵌入最接近的匹配项
4> 获取答案: 使用用户的初始问题、向量存储中的最近匹配项以及对话记忆, OpenAI
的语言模型会生成一个答案。然后将此答案提供给用户。
注意: 系统会维护一个对话记忆,用于跟踪正在进行的对话的上下文。该记忆对于确保对话的连续性和相关性至关重要。
5> 用户收到答案: 答案将发送回用户,完成交互循环。对话记忆会使用此最新交换来更新,以告知未来的响应。
假设用户的原始问题如下
我在找手表,你能推荐一些适合正式场合并且价格低于 50 美元的款式吗?
OpenAI 转换后的独立问题如下
你推荐什么手表适合正式场合,价格低于 50 美元?
在 Redis 上进行向量相似性搜索后,我们获得以下相似产品
similarProducts = [
{
pageContent: ` Product details are as follows:
productId: 11005.
productDisplayName: Puma Men Visor 3HD Black Watch.
price: 5495 ...`,
metadata: { productId: '11005' },
},
{
pageContent: ` Product details are as follows:
productId: 11006.
productDisplayName: Puma Men Race Luminous Black Chronograph Watch.
price: 7795 ... `,
metadata: { productId: '11006' },
},
];
使用以上上下文和之前的聊天记录(如果有的话),最终的 OpenAI 响应如下
answer = `I recommend two watches for formal occasions with a price under $50.
First, we have the <a href="/?productId=11005">Puma Men Visor 3HD Black Watch</a> priced at $54.95. This watch features a heavy-duty design with a stylish dial and chunky casing, giving it a tough appearance - perfect for navigating the urban jungle. It has a square dial shape and a 32 mm case diameter. The watch comes with a 2-year warranty and is water-resistant up to 50 meters.
Second, we have the <a href="/?productId=11006">Puma Men Race Luminous Black Chronograph Watch</a> priced at $77.95. This watch also features a heavy-duty design with a stylish dial and chunky casing. It has a round dial shape and a 40 mm case diameter. The watch comes with a 2-year warranty and is water-resistant up to 50 meters.
Both these watches from Puma are perfect for formal occasions and are priced under $50. I hope this helps, and please let me know if you have any other questions!`;
注册一个 OpenAI 帐户 以获取将在演示中使用的 API 密钥(在 .env 文件中添加 OPEN_AI_API_KEY 变量)。您也可以参考 OpenAI API 文档 以了解更多信息。
以下命令用于克隆本教程中使用的应用的源代码
git clone --branch v9.2.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions
在本教程中,让我们考虑一个简化的电子商务环境。 products
提供的 JSON 提供了我们将要操作的 AI 搜索功能的简介。
const products = [
{
productId: '11000',
price: 3995,
productDisplayName: 'Puma Men Slick 3HD Yellow Black Watches',
variantName: 'Slick 3HD Yellow',
brandName: 'Puma',
ageGroup: 'Adults-Men',
gender: 'Men',
displayCategories: 'Accessories',
masterCategory_typeName: 'Accessories',
subCategory_typeName: 'Watches',
styleImages_default_imageURL:
'http://host.docker.internal:8080/images/11000.jpg',
productDescriptors_description_value:
'<p style="text-align: justify;">Stylish and comfortable, ...',
stockQty: 25,
},
//...
];
以下是将 products 数据作为 OpenAI 嵌入到 Redis 的示例代码。
import { Document } from 'langchain/document';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { RedisVectorStore } from 'langchain/vectorstores/redis';
/**
* Adds OpenAI embeddings to Redis for the given products.
*
* @param _products - An array of (ecommerce) products.
* @param _redisClient - The Redis client used to connect to the Redis server.
* @param _openAIApiKey - The API key for accessing the OpenAI service.
*/
const addOpenAIEmbeddingsToRedis = async (
_products,
_redisClient,
_openAIApiKey,
) => {
if (_products?.length > 0 && _redisClient && _openAIApiKey) {
// Check if the data is already seeded
const existingKeys = await _redisClient.keys('openAIProducts:*');
if (existingKeys.length > 0) {
console.log('seeding openAIEmbeddings skipped !');
return;
}
const vectorDocs: Document[] = [];
// Create a document for each product
for (let product of _products) {
let doc = new Document({
metadata: {
productId: product.productId,
},
pageContent: ` Product details are as follows:
productId: ${product.productId}.
productDisplayName: ${product.productDisplayName}.
price: ${product.price}.
variantName: ${product.variantName}.
brandName: ${product.brandName}.
ageGroup: ${product.ageGroup}.
gender: ${product.gender}.
productColors: ${product.productColors}
Category: ${product.displayCategories}, ${product.masterCategory_typeName} - ${product.subCategory_typeName}
productDescription: ${product.productDescriptors_description_value}`,
});
vectorDocs.push(doc);
}
// Create a new OpenAIEmbeddings instance
const embeddings = new OpenAIEmbeddings({
openAIApiKey: _openAIApiKey,
});
// Add the documents to the RedisVectorStore
const vectorStore = await RedisVectorStore.fromDocuments(
vectorDocs,
embeddings,
{
redisClient: _redisClient,
indexName: 'openAIProductsIdx',
keyPrefix: 'openAIProducts:',
},
);
console.log('seeding OpenAIEmbeddings completed');
}
};
您可以在 RedisInsight 中观察 OpenAIProducts JSON。
下载 RedisInsight 以可视化地浏览您的 Redis 数据或在工作台使用原始 Redis 命令。
将产品数据作为 OpenAI 嵌入到 Redis 后,我们可以创建一个 chatbot
API 来回答用户问题并推荐产品。
请求
POST https://localhost:3000/products/chatBot
{
"userMessage":"I am looking for a watch, Can you recommend anything for formal occasions with price under 50 dollars?"
}
响应
{
"data": "I recommend two watches for formal occasions with a price under $50.
First, we have the <a href='/?productId=11005'>Puma Men Visor 3HD Black Watch</a> priced at $54.95. This watch features a heavy-duty design with a stylish dial and chunky casing, giving it a tough appearance - perfect for navigating the urban jungle. It has a square dial shape and a 32 mm case diameter. The watch comes with a 2-year warranty and is water-resistant up to 50 meters.
Second, we have the <a href='/?productId=11006'>Puma Men Race Luminous Black Chronograph Watch</a> priced at $77.95. This watch also features a heavy-duty design with a stylish dial and chunky casing. It has a round dial shape and a 40 mm case diameter. The watch comes with a 2-year warranty and is water-resistant up to 50 meters.
Both these watches from Puma are perfect for formal occasions and are priced under $50. I hope this helps, and please let me know if you have any other questions!",
"error": null,
"auth": "SES_54f211db-50a7-45df-8067-c3dc4272beb2"
}
当您发出请求时,它会通过 API 网关传递到 products
服务。最终,它会调用 chatBotMessage
函数,该函数如下所示
import {
ChatOpenAI,
ChatOpenAICallOptions,
} from 'langchain/chat_models/openai';
import { PromptTemplate } from 'langchain/prompts';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { RedisVectorStore } from 'langchain/vectorstores/redis';
import { StringOutputParser } from 'langchain/schema/output_parser';
import { Document } from 'langchain/document';
let llm: ChatOpenAI<ChatOpenAICallOptions>;
const chatBotMessage = async (
_userMessage: string,
_sessionId: string,
_openAIApiKey: string,
) => {
const CHAT_BOT_LOG = 'CHAT_BOT_LOG_STREAM';
const redisWrapperInst = getRedis();
// Add user message to chat history
const chatHistoryName = 'chatHistory:' + _sessionId;
redisWrapperInst.addItemToList(
chatHistoryName,
'userMessage: ' + _userMessage,
);
// add log
addMessageToStream(
{ name: 'originalQuestion', comments: _userMessage },
CHAT_BOT_LOG,
);
// (1) Create a standalone question
const standaloneQuestion = await convertToStandAloneQuestion(
_userMessage,
_sessionId,
_openAIApiKey,
);
// add log
addMessageToStream(
{ name: 'standaloneQuestion', comments: standaloneQuestion },
CHAT_BOT_LOG,
);
// (2) Get similar products from Redis
const similarProducts = await getSimilarProductsByVSS(
standaloneQuestion,
_openAIApiKey,
);
if (similarProducts?.length) {
// add log
addMessageToStream(
{ name: 'similarProducts', comments: JSON.stringify(similarProducts) },
CHAT_BOT_LOG,
);
}
// Combine the product details into a single document
const productDetails = combineVectorDocuments(similarProducts);
console.log('productDetails:', productDetails);
// (3) Get answer from OpenAI
const answer = await convertToAnswer(
_userMessage,
standaloneQuestion,
productDetails,
_sessionId,
_openAIApiKey,
);
// add log
addMessageToStream({ name: 'answer', comments: answer }, CHAT_BOT_LOG);
// Add answer to chat history
redisWrapperInst.addItemToList(
chatHistoryName,
'openAIMessage(You): ' + answer,
);
return answer;
};
以下函数使用 openAI
将 userMessage 转换为 standaloneQuestion。
// (1) Create a standalone question
const convertToStandAloneQuestion = async (
_userQuestion: string,
_sessionId: string,
_openAIApiKey: string,
) => {
const llm = getOpenAIInstance(_openAIApiKey);
const chatHistory = await getChatHistory(_sessionId);
const standaloneQuestionTemplate = `Given some conversation history (if any) and a question, convert it to a standalone question.
***********************************************************
conversation history:
${chatHistory}
***********************************************************
question: {question}
standalone question:`;
const standaloneQuestionPrompt = PromptTemplate.fromTemplate(
standaloneQuestionTemplate,
);
const chain = standaloneQuestionPrompt
.pipe(llm)
.pipe(new StringOutputParser());
const response = await chain.invoke({
question: _userQuestion,
});
return response;
};
const getOpenAIInstance = (_openAIApiKey: string) => {
if (!llm) {
llm = new ChatOpenAI({
openAIApiKey: _openAIApiKey,
});
}
return llm;
};
const getChatHistory = async (_sessionId: string, _separator?: string) => {
let chatHistory = '';
if (!_separator) {
_separator = '\n\n';
}
if (_sessionId) {
const redisWrapperInst = getRedis();
const chatHistoryName = 'chatHistory:' + _sessionId;
const items = await redisWrapperInst.getAllItemsFromList(chatHistoryName);
if (items?.length) {
chatHistory = items.join(_separator);
}
}
return chatHistory;
};
const combineVectorDocuments = (
_vectorDocs: Document[],
_separator?: string,
) => {
if (!_separator) {
_separator = '\n\n --------------------- \n\n';
}
return _vectorDocs.map((doc) => doc.pageContent).join(_separator);
};
以下函数使用 Redis
查找 standaloneQuestion 的类似产品。
// (2) Get similar products from Redis
const getSimilarProductsByVSS = async (
_standAloneQuestion: string,
_openAIApiKey: string,
) => {
const client = getNodeRedisClient();
const embeddings = new OpenAIEmbeddings({
openAIApiKey: _openAIApiKey,
});
const vectorStore = new RedisVectorStore(embeddings, {
redisClient: client,
indexName: 'openAIProductsIdx',
keyPrefix: 'openAIProducts:',
});
const KNN = 3;
/* Simple standalone search in the vector DB */
const vectorDocs = await vectorStore.similaritySearch(
_standAloneQuestion,
KNN,
);
return vectorDocs;
};
以下函数使用 openAI
将 standaloneQuestion、Redis 中的类似产品和其他上下文转换为人类可理解的答案。
// (3) Get answer from OpenAI
const convertToAnswer = async (
_originalQuestion: string,
_standAloneQuestion: string,
_productDetails: string,
_sessionId: string,
_openAIApiKey: string,
) => {
const llm = getOpenAIInstance(_openAIApiKey);
const chatHistory = await getChatHistory(_sessionId);
const answerTemplate = `
Please assume the persona of a retail shopping assistant for this conversation.
Use a friendly tone, and assume the target audience are normal people looking for a product in a ecommerce website.
***********************************************************
${
chatHistory
? `
Conversation history between user and you is :
${chatHistory}
`
: ''
}
***********************************************************
OriginalQuestion of user is : {originalQuestion}
***********************************************************
converted stand alone question is : {standAloneQuestion}
***********************************************************
resulting details of products for the stand alone question are :
{productDetails}
Note : Different product details are separated by "---------------------" (if any)
***********************************************************
Answer the question based on the context provided and the conversation history.
If you don't know the answer, please direct the questioner to email help@redis.com. Don't try to suggest any product out of context as it may not be in the store.
Let the answer include product display name, price and optional other details based on question asked.
Let the product display name be a link like <a href="/?productId="> productDisplayName </a>
so that user can click on it and go to the product page with help of productId.
answer: `;
const answerPrompt = PromptTemplate.fromTemplate(answerTemplate);
const chain = answerPrompt.pipe(llm).pipe(new StringOutputParser());
const response = await chain.invoke({
originalQuestion: _originalQuestion,
standAloneQuestion: _standAloneQuestion,
productDetails: _productDetails,
});
return response;
};
您可以在 RedisInsight 中观察聊天历史记录和中间聊天日志。
下载 RedisInsight 以可视化地浏览您的 Redis 数据或在工作台使用原始 Redis 命令。
使用 LangChain 和 Redis 构建 GenAI 聊天机器人涉及将高级 AI 模型与高效的存储解决方案集成。本教程涵盖了开发能够处理电子商务查询的聊天机器人所需的基本步骤和代码。借助这些工具,您可以为各种应用程序创建响应迅速、智能的聊天机器人。