最近,我一直在探索 OceanBase,这是一个分布式关系数据库管理系统。在阅读其广泛的文档时,我意识到手头这项任务的巨大挑战——庞大的内容量使得快速高效地找到准确的信息变得困难。
这个经历激发了一个灵感。如果我们能够简化这个信息获取的过程会怎么样?如果我们可以利用人工智能的力量来在这片广阔的信息海洋中导航呢?于是, OceanBase 文档聊天机器人的概念诞生了。
但我想进一步推动这个想法。为了使这个项目更有趣,也为了加深我对 OceanBase 的理解,我决定使用 OceanBase 作为 AI 训练的向量数据库。这种方法不仅可以提供一个实际的解决方案来应对信息检索的挑战,还提供了一个独特的机会从不同的角度探索 OceanBase 的能力。
在这篇文章中,我将分享将这个想法付诸实践的过程。从将 AI 与 OceanBase 集成到训练模型和创建聊天机器人,我们将探讨遇到的挑战、解决方案和所获得的见解。无论你是 AI 爱好者、数据库专业人士,还是对这两个领域的交叉感兴趣,都欢迎你和我一起踏上这场令人兴奋的探索之旅。
TL:DR:
该项目将人工智能与 OceanBase 集成,创建了一个文档聊天机器人,能够根据 OceanBase 的文档和其他托管在 GitHub 上的文档回答用户的查询。
用户的问题和文档文章被转化为向量表示,用于比较和生成答案。
由于 OceanBase 本身不支持向量数据类型,向量以 JSON 格式存储和检索。
深入了解如何设置项目环境、训练模型和设置服务器的逐步指南。
关于项目的一些思考
着手这个将 OceanBase 与 AI 集成的项目是一项具有挑战性但有益的尝试。这是一个深入研究 AI 和数据库的机会,为我提供了丰富的学习经验。
一般来说,我们想要创建的文档聊天机器人利用 AI 从文档数据库中回答用户的查询。它将用户的问题转化为向量表示,并将其与预处理的文档向量进行比较。根据向量的相似性,确定最相关的文档。然后,聊天机器人使用这个文档生成一个上下文相关的答案,提供准确和相关的回复。
在聊天机器人能够基于给定的文档回答问题之前,你需要用相关知识对其进行“训练”。训练的过程本质上是将文档中的文章转化为嵌入(embeddings)——文本的数值表示(向量),捕捉词汇的语义含义。这些嵌入随后存储在数据库中,而在这个项目中,我们使用的是 OceanBase。
你可能会问为什么不使用已经成熟的解决方案 Langchain。虽然 Langchain 是一个强大的语言模型训练和部署平台,但由于其预期和我们具体的要求,它并不是这个项目的最佳选择。
Langchain 假设底层数据库能够处理向量存储和执行相似性搜索。然而,我们使用的数据库 OceanBase 并不直接支持向量数据类型。
此外,该项目的主要目标是探索 OceanBase 作为向量数据库在 AI 训练方面的潜力。使用内部处理相似性搜索的 Langchain 将绕过深入探索 OceanBase 这些方面的机会。
由于 OceanBase 并不直接支持向量存储,我们需要找到一个变通的方法。但是向量本质上是一个数字数组,我们可以将其存储为数据库中的 JSON 字符串或二进制对象(blob)。当我们需要将记录与问题的嵌入进行比较时,我们只需在下一步将其转换回嵌入形式。
鉴于 OceanBase 本身不支持向量数据类型,需要将每个嵌入转换为 OceanBase 支持的数据类型,本例中为 JSON。因此,当将问题与文章进行比较时,这些记录需要还原为它们的原始嵌入形式。需要注意的是,这个转换过程导致系统变慢,并且缺乏可扩展性。
为了充分发挥 AI 和机器学习的潜力,我强烈建议 OceanBase 团队考虑整合对向量数据类型的支持。这不仅会增强系统的性能和可扩展性,还会将 OceanBase 定位为一个前瞻性的数据库解决方案,准备迎接 AI 时代。如果你对这个问题感兴趣,欢迎到这个 GitHub Issue 来讨论。
关于本项目
该系统基于 Node.js 框架构建,使用 Express.js 处理 HTTP 请求。Sequelize 是一个基于 Promise 的 Node.js ORM,用于数据库模式迁移、种子数据填充和查询任务。
该项目的结构处理两个主要请求:使用文档进行模型训练和提问。"/train"端点获取文档,将其转化为向量,并将其存储在数据库中。"/ask"端点接收用户的问题,使用存储的向量找到最相关的文档,并使用 OpenAI 的 API 生成答案。
这个项目在 GitHub 上供所有人访问和利用。这意味着你不仅可以利用这个基于 AI 的解决方案来处理 OceanBase 文档,还可以处理任意其他的项目文档。你可以随意克隆存储库、探索代码,并将其用作训练自己的文档聊天机器人的基础。
入门指南
要从头开始设置项目环境,请按照以下步骤进行操作:
1、安装 Node.js:如果尚未安装,请从官方 Node.js 网站下载并安装 Node.js。
2、创建新项目:打开终端,导航到你希望的目录,并使用命令 mkdir oceanbase-vector 创建一个新项目。使用 cd oceanbase-vector 命令进入新项目目录。
3、初始化项目:通过运行 npm init -y 来初始化一个新的 Node.js 项目。该命令将创建一个带有默认值的新 package.json 文件。
4、安装依赖项:安装项目所需的依赖项。使用以下命令安装所有必要的包:
npm install dotenv express lodash mysql2 openai sequelize --save
我们将使用 Sequelize 作为与 OceanBase 数据库进行交互的 ORM。lodash 包用于计算相似性搜索过程中的余弦相似度(cosine similarity)。当然,我们还需要 openai 包来与 OpenAI 的 API 进行交互。我们将使用 Express 作为服务器,它将公开 ask 和 train 端点。
接下来,使用 npm install sequelize-cli --save-dev 安装开发依赖项。
现在,项目设置完成,准备开始构建每个模块。
设置 OceanBase
要设置项目,首先需要一个正在运行的 OceanBase 集群。我撰写了一篇详细介绍如何设置 OceanBase 的文章。(详情点击:在Node.js中使用OceanBase:使用Sequelize和Express构建CRM)
一旦你有一个正在运行的 OceanBase 实例,下一步就是创建一个用于存储文章及其对应嵌入的表。这个名为 article_vectors 的表将具有以下列:
1、 id:这是一个整数类型的列,将作为表的主键。它是自增的,这意味着每个新条目将自动被分配一个 I D,该 ID 比前一个条目的 ID 大 1。
2、 content_vector:这是一个 JSON 类型的列,将存储文章的向量表示。每个向量将被转换为 JSON 对象进行存储。
3、 content:这是一个文本类型的列,将存储文章的实际内容。
要创建 article_vectors 表,你可以使用以下 SQL 命令:
CREATETABLE article_vectors (
id INT AUTO_INCREMENT,
content_vector JSON,
content TEXT CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
PRIMARY KEY (id)
);
现在,我们在 OceanBase 数据库中有一个准备好的表,用于存储文章及其向量表示。在接下来的部分中,我们将使用 OceanBase 文档中的数据填充这个表,然后使用这些数据来训练我们的 AI 模型。
我们还需要在 Sequlize 项目中定义 ArticleVector 模型。首先,运行 sequelize init 来初始化 sequelize 环境。在生成的 /models 文件夹中,创建一个名为 ArticleVector.js 的新文件。
const { DataTypes, Model } = require('sequelize');
class ArticleVector extends Model {}
module.exports = (sequelize) => {
ArticleVector.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
content_vector: {
type: DataTypes.JSON,
},
content: {
type: DataTypes.TEXT,
},
},
{
sequelize,
modelName: 'ArticleVector',
tableName: 'article_vectors',
timestamps: false,
},
{
sequelize,
modelName: 'ArticleVector',
tableName: 'content',
timestamps: false,
}
);
return ArticleVector;
};
这是对 OceanBase 数据库中 article_vectors 表的 Sequelize 模型的定义代码。Sequelize 是一个基于 Promise 的 Node.js ORM,允许调用高级 API 与 SQL 数据库进行交互。
文档训练
训练过程是文档聊天机器人的核心步骤。它涉及从 GitHub 仓库获取 OceanBase 文档,将内容转换为向量表示,并将这些向量存储在 OceanBase 数据库中。这个过程由两个主要的脚本 trainDocs.js 和 embedding.js 来处理。
1、获取文档
trainDocs.js 脚本从 GitHub 仓库中获取 OceanBase 文档的内容。它使用 GitHub API 访问存储库并检索所有的 .md 文件。该脚本被设计为递归地获取目录的内容,以确保无论文档文件在存储库中的位置如何,都能被检索到。
脚本还获取每个文档文件的内容。它向文件的下载 URL 发送请求,并将返回的内容存储供进一步处理。
async function fetchRepoContents(
repo,
path = '',
branch = 'main',
limit = 100
) {
const url = `https://api.github.com/repos/${repo}/contents/${path}?ref=${branch}`;
const accessToken = process.env.GITHUB_TOKEN;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
});
const data = await response.json();
// If the path is a directory, recursively fetch the contents
if (Array.isArray(data)) {
let count = 0;
const files = [];
for (const item of data) {
if (count >= limit) {
break;
}
if (item.type === 'dir') {
const dirFiles = await fetchRepoContents(
repo,
item.path,
branch,
limit - count
);
files.push(...dirFiles);
count += dirFiles.length;
} else if (item.type === 'file' && item.name.endsWith('.md')) {
files.push(item);
count++;
}
}
return files;
}
return [];
}
/**
* @param {RequestInfo | URL} url
*/
async function fetchFileContent(url) {
const response = await fetch(url);
const content = await response.text();
return content;
}
现在我们可以编写处理嵌入过程的主要函数。该函数将使用我们定义的 GitHub 获取函数从给定的存储库获取所有的 Markdown 文件。请注意,我添加了一个 limit 参数来限制获取的文件数量。这是因为 GitHub API 对每小时可以进行的请求有限制。如果仓库包含数千个文件,可能会达到限制。
async function articleEmbedding(repo, path = '', branch, limit) {
const contents = await fetchRepoContents(repo, path, branch, limit);
console.log(contents.length);
contents.forEach(async (item) => {
const content = await fetchFileContent(item.download_url);
await storeEmbedding(content);
});
}
2、转换和存储向量
embedding.js 脚本负责将获取的文档转换为向量表示,并将这些向量存储在 OceanBase 数据库中。这是通过使用 embedArticle( ) 和 storeEmbedding( ) 这两个主要函数来完成的。
为了使 Sequelize 和 OpenAI 正常工作,我们需要对它们进行初始化设置。
const { Sequelize } = require('sequelize');
const dotenv = require('dotenv');
dotenv.config();
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const { Configuration, OpenAIApi } = require('openai');
const configuration = new Configuration({
apiKey: process.env.OPENAI_KEY,
});
const openai = new OpenAIApi(configuration);
embedArticle( ) 函数使用 OpenAI 的 Embedding API 将文章内容转换为向量。它向 API 发送包含文章内容的请求,并获取生成的向量。
async function embedArticle(article) {
// Create article embeddings using OpenAI
try {
const result = await openai.createEmbedding({
model: 'text-embedding-ada-002',
input: article,
});
// get the embedding
const embedding = result.data.data[0].embedding;
return embedding;
} catch (error) {
if (error.response) {
console.log(error.response.status);
console.log(error.response.data);
} else {
console.log(error.message);
}
}
}
storeEmbedding( ) 函数接受生成的向量,并将其存储在 OceanBase 数据库中。它使用 Sequelize 连接到数据库,在 article_vectors 表中创建一条新记录,并将向量和原始文章内容存储在相应的列中。
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(
config.database,
config.username,
config.password,
config
);
}
const ArticleVectorModel = require('../models/ArticleVector');
const ArticleVector = ArticleVectorModel(sequelize);
async function storeEmbedding(article) {
try {
await sequelize.authenticate();
console.log('Connection has been established successfully.');
const articleVector = await ArticleVector.create({
content_vector: await embedArticle(article),
content: article,
});
console.log('New article vector created:', articleVector.id);
} catch (err) {
console.error('Error:', err);
}
}
要启动训练过程,只需在 trainDocs.js 中使用适当的参数调用 articleEmbedding( ) 函数即可。该函数获取文档,将其转换为向量,并将这些向量存储在数据库中。
这个训练过程确保聊天机器人对 OceanBase 文档有全面的理解,使其能够对用户的查询提供准确和相关的答案。
在接下来的部分,我们将讨论如何使用训练好的模型来回答用户的查询。
你可能已经注意到,我们在项目中使用了一个 .env 文件来存储敏感信息。以下是一个你可以使用的示例 .env 文件:
GITHUB_TOKEN="Get your Github token from https://docs.github.com/en/rest/guides/getting-started-with-the-rest-api?apiVersion=2022-11-28"
OPENAI_KEY ="YOUR_OPENAI_KEY"
DOC_OWNER="OceanBase, you can change this to another product's name so that the chatbot knows which product it is dealing with"
MODEL="gpt-4 and gpt-3.5-turbo are supported"
3、询问文档
一旦训练过程完成,聊天机器人就可以回答用户的查询了。这由 /embeddings/askDocs.js 文件处理,它使用存储的向量来找到与给定问题最相关的文档,然后使用 OpenAI 的 API 生成答案。
askDocs.js 中的 getSimilarDoc( ) 函数负责找到与给定问题最相关的文档。它首先使用 embedding.js 中的 embedArticle( ) 函数将问题转换为一个向量。
接下来,它从 OceanBase 数据库的 article_vectors 表中检索所有存储的向量。然后,它计算问题向量与每个存储向量之间的余弦相似度(cosine similarity)。余弦相似度是衡量两个向量相似程度的指标,非常适合这个任务。
async function getSimilarDoc(question) {
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(
config.database,
config.username,
config.password,
config
);
}
const ArticleVector = ArticleVectorModel(sequelize);
// get all rows in article_vector table
const vectors = await ArticleVector.findAll();
sequelize.close();
// console.log(vectors[0]);
// Calculate cosine similarity for each vector
const embeddedQuestion = await embedArticle(question);
// Calculate cosine similarity for each vector
// Calculate cosine similarity for each vector
const similarities = vectors.map((vector) => {
const similarity = cosineSimilarity(
embeddedQuestion,
vector.content_vector
);
return {
id: vector.id,
content: vector.content,
similarity: similarity,
};
});
const mostSimilarDoc = _.orderBy(similarities, ['similarity'], ['desc'])[0];
// console.log(mostSimilarDoc);
return mostSimilarDoc;
}
askDocs.js 中的 cosineSimilarity( ) 函数是文档检索过程的关键部分。它计算两个向量之间的余弦相似度,这是衡量它们相似程度的指标。
在这个项目的背景中,它用于确定用户问题的向量表示与数据库中文章的向量表示之间的相似度。与问题具有最高余弦相似度的文章被认为是最相关的。
该函数接受两个向量作为输入。它计算两个向量的点积和各自的模。然后,余弦相似度被计算为点积除以两个模的乘积。
function cosineSimilarity(vecA, vecB) {
const dotProduct = _.sum(_.zipWith(vecA, vecB, (a, b) => a * b));
const magnitudeA = Math.sqrt(_.sum(_.map(vecA, (a) => Math.pow(a, 2))));
const magnitudeB = Math.sqrt(_.sum(_.map(vecB, (b) => Math.pow(b, 2))));
if (magnitudeA === 0 || magnitudeB === 0) {
return 0;
}
return dotProduct / (magnitudeA * magnitudeB);
}
该函数返回与问题具有最高余弦相似度的文档。这是给定问题的最相关文档。
在确定了最相关的文档之后,askDocs.js 中的 askAI( ) 函数生成问题的答案。它使用 OpenAI 的 API 基于确定的文档生成一个上下文相关的答案。
该函数向 API 发送包含问题和相关文档的请求。API 返回一个生成的答案,然后函数将该答案返回。
这个过程确保聊天机器人为用户查询提供准确和相关的答案,使其对任何想要从 OceanBase 文档中获取信息的人来说都是有价值工具。
async function askAI(question) {
const mostSimilarDoc = await getSimilarDoc(question);
const context = mostSimilarDoc.content;
try {
const result = await openai.createChatCompletion({
model: process.env.MODEL,
messages: [
{
role: 'system',
content: `You are ${
process.env.DOC_OWNER || 'a'
} documentation assistant. You will be provided a reference docs article and a question, please answer the question based on the article provided. Please don't make up anything.`,
},
{
role: 'user',
content: `Here is an article related to the question: \n ${context} \n Answer this question: \n ${question}`,
},
],
});
// get the answer
const answer = result.data.choices[0].message.content;
console.log(answer);
return {
answer: answer,
docId: mostSimilarDoc.id,
};
} catch (error) {
if (error.response) {
console.log(error.response.status);
console.log(error.response.data);
} else {
console.log(error.message);
}
}
}
4、设置API服务器
现在我们已经设置好了训练和询问函数,我们可以从 API 端点调用它们。Express.js 服务器是我们应用程序的主要入口点。它公开了两个端点,/train 和 /ask,分别用于处理模型的训练和回答用户的查询。下面描述的代码将位于 index.js 文件中。
服务器使用 Express.js 进行初始化,它是一个快速、无偏见和极简的 Node.js 网络框架。它使用 express.json( ) 中间件配置为解析带有 JSON 负载的传入请求。
const express = require('express');
const app = express();
const db = require('./models');
app.use(express.json());
服务器使用 Sequelize 与 OceanBase 数据库建立连接。在 Sequelize 实例上调用 sync( ) 函数,以确保所有定义的模型与数据库同步。这包括在表尚不存在的情况下创建必要的表。
db.sequelize.sync().then((req) => {
app.listen(3000, () => {
console.log('Server running at port 3000...');
});
});
5、训练端点
/train 端点是一个 POST 路由,用于触发训练过程。它期望请求体中包含 GitHub 存储库的详细信息(repo、path、branch 和 limit)。这些详细信息将传递给 articleEmbedding( ) 函数,该函数获取文档,将其转换为向量,并将这些向量存储在数据库中。
app.post('/train', async (req, res, next) => {
try {
await articleEmbedding(
req.body.repo,
req.body.path,
req.body.branch,
req.body.limit
);
res.json({
status: 'Success',
message: 'Docs trained successfully.',
});
} catch (e) {
next(e); // Pass the error to the error handling middleware
}
});
如果训练过程成功,端点将响应一个成功消息。如果在过程中发生错误,将其传递给错误处理中间件。
向端点发送的 POST 请求将包含如下的请求体:
{
"repo":"oceanbase/oceanbase-doc",
"path":"en-US",
"branch": "V4.1.0",
"limit": 1000
}
你可以将仓库更改为任意托管在 GitHub 上的文档。路径是目标内容所在的文件夹。在这种情况下,我使用了" en-US ",因为我只想训练 OceanBase 的英文文档。分支参数指示存储库的分支。在大多数情况下,你可以使用主分支或特定版本的分支。如果仓库有太多文件,以至于 GitHub API 达到了限制,则可以使用 limit 参数。
如果训练成功,它的样子如下所示:
在 OceanBase 数据库中,我们可以看到所有记录及其嵌入向量:
6、询问端点
/ask 端点也是一个 POST 路由。它期望请求体中包含用户的问题。这个问题将传递给 askAI( ) 函数,该函数找到最相关的文档并使用 OpenAI 的 API 生成答案。
app.post('/ask', async (req, res, next) => {
try {
const answer = await askAI(req.body.question);
res.json({
status: 'Success',
answer: answer.answer,
docId: answer.docId,
});
} catch (e) {
next(e); // Pass the error to the error handling middleware
}
});
端点回复包含生成的答案和最相关文档的 ID。这是一个成功请求的示例。
正如我们所看到的,当我们询问聊天机器人在演示环境中安装 OceanBase 所需的软件和硬件环境时,聊天机器人提供了一篇文章中提到的答案。
结论
这个项目代表了在 OceanBase 中集成 AI 以创建文档聊天机器人的实验。虽然它展示了 AI 在增强文档可用性方面的潜力,但需要注意的是,这仍然是一个实验性的设置,还没有准备好用于生产环境。当前的执行面临可扩展性问题,因为需要将向量转换为 JSON 进行存储,然后再将其转换回向量进行比较。
这个项目的代码全部放在 GitHub 上,所有人均可访问和使用。尽管还有许多工作要做,但这个项目代表了将 OceanBase 用作 AI 训练的向量数据库的一种尝试。随着数据库的进一步开发和完善,我们可以期待在文档中查找答案就像提问一样简单的时代到来。