在本教程中,您将学习如何使用 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 。以下是电商应用前端的截图。
仪表板: 显示商品列表,具有不同的搜索功能,可在设置页面配置。
设置: 点击仪表板右上角的齿轮图标即可访问。在此处控制搜索栏、聊天机器人可见性及其他功能。
仪表板(语义文本搜索): 配置为语义文本搜索后,搜索栏支持自然语言查询。示例:“纯棉蓝色衬衫。”
仪表板(基于图像的语义查询): 配置为语义图像摘要搜索后,搜索栏支持基于图像的查询。示例:“左胸耐克标志。”
聊天机器人: 位于页面右下角,辅助商品搜索和详细查看。
在聊天中选择商品会在仪表板上显示其详细信息。
购物车: 将商品添加到购物车并使用“立即购买”按钮结账。
订单历史: 购买后,顶部导航栏中的“订单”链接会显示订单状态和历史记录。
管理面板: 通过顶部导航栏中的“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 http://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 [email protected]. 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 模型与高效的存储解决方案集成。本教程涵盖了开发能够处理电商查询的聊天机器人所需的基本步骤和代码。借助这些工具,您可以为各种应用创建响应迅速、智能的聊天机器人。