从零开始使用OceanBase创建一个Langchain的替代方案

使用OceanBase创建Langchain的替代方案

最近,我一直在探索OceanBase这个分布式关系型数据库管理系统的世界。在浏览其广泛的文档时,我意识到任务的巨大——庞大的内容量使得快速高效地找到精确信息变得具有挑战性。

这个经历引发了一个想法。如果有一种方法能简化这个过程呢?如果我们能利用人工智能的力量来浏览这片广阔的信息海洋呢?于是,OceanBase文档聊天机器人的概念诞生了。

但我想进一步发展这个想法。为了使项目更有趣,并加深我对OceanBase的理解,我决定将OceanBase本身作为AI训练的向量数据库。这种方法不仅能够提供一个实际的解决方案来应对信息检索的挑战,还提供了一个从不同角度探索OceanBase能力的独特机会。

在这篇博文中,我将分享将这个想法付诸实践的过程。从将AI与OceanBase集成到训练模型和创建聊天机器人,我们将探索沿途遇到的挑战、解决方案和所获得的见解。无论你是一位AI爱好者、数据库专业人士,还是对这两个领域的交叉感兴趣的人,我邀请你加入我一起进行这个激动人心的探索。

TL:DR:

  • 该项目将AI与OceanBase集成,创建了一个文件聊天机器人,能够根据OceanBase的文档和任何其他托管在GitHub上的文档回答用户查询。
  • 用户的问题和文档文章被转化为向量表示进行比较和生成答案。
  • 由于OceanBase本身缺乏对向量数据类型的支持,向量被存储和检索为JSON。
  • 深入了解如何设置项目环境、训练模型和设置服务器的逐步指南。
  • 该项目强调了OceanBase需要整合对向量数据类型的支持,以充分利用AI和机器学习的潜力。

项目的一些反思

着手将OceanBase与AI集成的这个项目是一个具有挑战性但值得的努力。它是一个深入研究AI和数据库细节的机会,提供了丰富的学习经验。

一般来说,我们想要创建的文件聊天机器人使用AI来根据文档数据库中的信息回答用户的问题。它将用户的问题转化为向量表示,并将其与预处理过的文档向量进行比较。基于向量相似性,确定最相关的文档。然后,聊天机器人使用该文档生成一个上下文恰当的答案,提供精确和相关的回复。

在聊天机器人能够根据给定的文档回答问题之前,你需要用相关知识“训练”它。训练过程的本质是将文档中的文章转化为嵌入——捕捉单词的语义含义的文本的数值表示(向量)。这些嵌入然后在项目中的OceanBase数据库中存储。

你可能会问为什么我不使用Langchain,它已经是一个成熟的解决方案了。虽然Langchain是一个强大的语言模型训练和部署平台,但由于其期望和我们的具体要求,它并不是这个项目的最佳选择。

Langchain假设底层数据库能够处理向量存储和相似性搜索。然而,我们使用的数据库OceanBase并不直接支持向量数据类型。

此外,该项目的关键目标是探索OceanBase作为用于AI训练的向量数据库的潜力。使用Langchain内部处理相似性搜索的方式将绕过深入研究OceanBase这些方面的机会。

由于OceanBase不直接支持向量存储,我们必须找到一种解决方法。但向量本质上是一个数字数组。我们可以将其存储为数据库中的JSON字符串或blob。当我们需要将记录与问题嵌入进行比较时,我们只需在下一步将其转换回嵌入形式。

鉴于OceanBase并不直接支持向量数据类型,每个嵌入都需要转换为OceanBase支持的数据类型,例如JSON。因此,在将问题与文章进行比较时,这些记录需要恢复为其原始嵌入形式。不幸的是,这个转换过程会导致系统变慢且缺乏可扩展性。

为了充分发挥AI和机器学习的潜力,我强烈建议OceanBase团队考虑整合对向量数据类型的支持。这不仅会提升系统的性能和可扩展性,也会使OceanBase成为一个面向未来、准备拥抱AI时代的数据库解决方案。我在他们的GitHub仓库上提出了一个问题。如果你对进一步探索感兴趣,欢迎加入讨论。

项目

该系统是基于Node.js框架构建的,使用Express.js处理HTTP请求。使用基于Promise的Node.js ORM Sequelize来处理数据库模式迁移、种子数据和查询任务。

项目结构用于处理两个主要请求:使用文章训练模型和提问问题。’/train’端点获取文章,将其转换为向量,并将其存储在数据库中。’/ask’端点接收用户的问题,使用存储的向量找到最相关的文章,并使用OpenAI的API生成答案。

该项目在GitHub上可供所有人访问和利用。这意味着您不仅可以为OceanBase,还可以为任何其他可能存在的文档集合利用这个基于AI的解决方案的强大功能。请随意克隆存储库,探索代码,并将其用作训练自己的文档聊天机器人的基础。这是使您的文档更加互动和可访问的绝佳方式。

开始

要从头开始设置项目环境,请按照以下步骤进行操作:

安装Node.js:如果尚未安装,请从官方Node.js网站下载并安装Node.js。

创建新项目:打开终端,导航到所需的目录,并使用命令’mkdir oceanbase-vector’创建一个新项目。使用’cd oceanbase-vector’命令进入新项目目录。

初始化项目:运行’npm init -y’命令初始化一个新的Node.js项目。该命令将使用默认值创建一个新的’package.json’文件。

安装依赖项:安装项目所需的必要依赖项。使用以下命令安装所有必要的软件包:

npm install dotenv express lodash mysql2 openai sequelize --save

我们将使用Sequelize作为与OceanBase数据库进行交互的ORM。Lodash软件包用于计算相似性搜索过程中的余弦相似度。当然,我们需要’openai’软件包与OpenAI的API进行交互。我们将使用express作为服务器,该服务器将公开ask和train端点。

接下来,使用’npm install sequelize-cli –save-dev’命令安装开发依赖项。

现在,我们的项目设置完成,准备开始构建每个模块。

设置OceanBase

要设置项目,首先需要运行中的OceanBase集群。我写的这篇文章详细介绍了如何设置OceanBase。

一旦您有一个正在运行的OceanBase实例,下一步是创建一个用于存储文章及其对应嵌入的表。这个名为’article_vectors’的表将具有以下列:

  • id:这是一个整数类型的列,将用作表的主键。它是自动递增的,这意味着每个新条目都会自动分配一个比前一个条目的ID大1的ID。
  • content_vector:这是一个JSON类型的列,将存储文章的向量表示。每个向量将被转换为JSON对象以进行存储。
  • content:这是一个文本类型的列,将存储文章的实际内容。

要创建’article_vectors’表,可以使用以下SQL命令:

CREATE TABLE article_vectors (
    id INT AUTO_INCREMENT,
    content_vector JSON,
    content TEXT CHARACTER SET 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.jsembedding.js

获取文档

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 (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文件。请注意,我添加了一个限制参数来限制获取的文件数量。这是因为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);
    });
}

转换并存储向量

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) {
    // 使用OpenAI创建文章嵌入
    try {
        const result = await openai.createEmbedding({
            model: 'text-embedding-ada-002',
            input: article,
        });
        // 获取嵌入向量
        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('连接成功。');
        const articleVector = await ArticleVector.create({
            content_vector: await embedArticle(article),
            content: article,
        });
        console.log('创建新的文章向量:', articleVector.id);
    } catch (err) {
        console.error('错误:', err);
    }
}

要启动训练过程,只需使用适当的参数调用trainDocs.js中的articleEmbedding()函数。此函数获取文档,将其转换为向量,并将这些向量存储在数据库中。

这个训练过程确保聊天机器人全面理解OceanBase文档,使其能够对用户查询提供准确和相关的答案。

在下一节中,我们将讨论如何使用训练好的模型来回答用户查询。

如您可能已经注意到的,我们在项目中使用.env文件来存储敏感信息。以下是一个示例.env文件:

GITHUB_TOKEN="从https://docs.github.com/en/rest/guides/getting-started-with-the-rest-api?apiVersion=2022-11-28获取您的Github令牌"
OPENAI_KEY ="YOUR_OPENAI_KEY"
DOC_OWNER="OceanBase,您可以将其更改为其他产品的名称,以便聊天机器人知道它正在处理的产品"
MODEL="支持gpt-4和gpt-3.5-turbo"

询问文档

训练过程完成后,聊天机器人准备好回答用户的查询。这由/embeddings/askDocs.js文件处理,它使用存储的向量来查找给定问题的最相关文档,然后使用OpenAI的API生成答案。

askDocs.js中的getSimilarDoc()函数负责查找给定问题的最相关文档。它首先使用embedding.js中的embedArticle()函数将问题转换为向量。

然后,它从OceanBase数据库的article_vectors表中检索所有存储的向量。接下来,它计算问题向量与每个存储向量之间的余弦相似度。余弦相似度是衡量两个向量相似程度的指标,非常适合这个任务。

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);
    // 获取article_vector表中的所有行
    const vectors = await ArticleVector.findAll();
    sequelize.close();
    // console.log(vectors[0]);

    // 为每个向量计算余弦相似度
    const embeddedQuestion = await embedArticle(question);

    // 为每个向量计算余弦相似度
    // 为每个向量计算余弦相似度
    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);
        }
    }
}

设置API服务器

现在我们已经设置好了训练和询问函数,我们可以从API端点调用它们。Express.js服务器是我们应用程序的主要入口点。它公开了两个端点,/train/ask,分别处理模型的训练和回答用户查询。这里描述的代码将位于index.js文件中。

服务器使用Express.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...');
    });
});

训练端点

/train端点是一个POST路由,触发训练过程。它期望请求体包含GitHub存储库的详细信息(repopathbranchlimit)。这些详细信息将传递给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: '文档训练成功。',
        });
    } catch (e) {
        next(e); // 将错误传递给错误处理中间件
    }
});

如果训练过程成功,端点将返回一个成功的消息。如果在过程中发生错误,错误将传递给错误处理中间件。

向端点发送的POST请求将包含以下请求体:

{
  "repo":"oceanbase/oceanbase-doc",
  "path":"en-US",
  "branch": "V4.1.0",
  "limit": 1000
}

你可以将仓库更改为你想要的其他托管在 GitHub 上的文档。路径是目标内容的文件夹。在这种情况下,我使用了“en-US”,因为我只想训练 OceanBase 的英文文档。分支参数表示仓库的分支。在大多数情况下,你可以使用主分支或某个版本的分支。limit 参数仅在一个仓库有太多文件时使用,以避免超过 GitHub API 的限制。

如果训练成功,它的效果如下所示。

在 OceanBase 数据库中,我们可以看到所有记录及其嵌入:

问答端点

/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); // 将错误传递给错误处理中间件
    }
});

端点将返回生成的答案和最相关文档的 ID。这是一个成功请求的示例。

如我们所见,当我们询问聊天机器人在演示环境中安装 OceanBase 的软件和硬件环境时,聊天机器人提供了本文中所述的答案。

结论

这个项目代表了将AI与OceanBase集成以创建文档聊天机器人的实验。虽然它展示了AI在提升文档可用性方面的潜力,但需要注意的是,这仍然是一个实验性设置,尚未准备好用于生产。当前的实现面临可扩展性问题,因为需要将向量转换为JSON进行存储,并在比较时再转换回向量。

为了充分利用AI和机器学习的潜力,我再次建议OceanBase团队考虑增加对向量数据类型的支持。这不仅将增强系统的性能和可扩展性,还将使OceanBase成为一个具有前瞻性的数据库解决方案,因为AI成为下一个大趋势。我在OceanBase的GitHub仓库中创建了一个问题来讨论这个提案,欢迎你加入对话。

这个项目的代码在GitHub上可供所有人访问和利用。这意味着你不仅可以为OceanBase利用这个基于AI的解决方案的强大功能,还可以为任何其他文档集合利用它。请随意克隆存储库,探索代码,并将其用作使用OceanBase训练您自己的文档聊天机器人的基础。这是使您的文档更具互动性和可访问性的绝佳方式。

总之,虽然还有工作要做,但这个项目代表了将OceanBase作为用于AI训练的向量数据库的一个有希望的步骤。随着数据库的进一步开发和完善,我们可以期待一个时代,通过问一个问题就能轻松地在文档中找到答案。