使用Oceanbase从头开始创建Langchain替代方案
#ai #openai #database #chatgpt

最近,我一直在探索分布式关系数据库管理系统OceanBase的世界。当我在其extensive documentation中导航时,我意识到了手头的任务的大小 - 内容量的巨大量使得迅速有效地找到精确信息的挑战。

这种经历引发了一个主意。如果有一种简化此过程的方法怎么办?如果我们能够利用AI的力量在广阔的信息海上导航怎么办?因此,海底聊天机器人的聊天机器人的概念诞生了。

,但我想将这个想法进一步发展。为了使该项目更加有趣并加深了我对海底的理解,我决定将Oceanbase本身用作AI培训的矢量数据库。这种方法不仅可以为信息检索挑战提供一个实用的解决方案,而且还提供了一个独特的机会来从不同的角度探索海底的能力。

在这篇博客文章中,我将分享将这个想法栩栩如生的旅程。从将AI与Oceanbase集成到训练模型并创建聊天机器人,我们将探索一路上获得的挑战,解决方案和见解。无论您是AI爱好者,数据库专业人士还是对这两个领域的交集感兴趣,我都邀请您加入我的激动人心的探索。

TL:DR:

  • 该项目将AI与Oceanbase集成在一起,以创建一个文档聊天机器人,能够根据Oceanbase的文档和任何其他GitHub托管文档来回答用户查询。
  • 用户问题和文档文章被转换为矢量表示形式,以进行比较和回答。
  • 由于Oceanbase缺乏对向量数据类型的固有支持,将向量存储并以JSON的形式检索。
  • 逐步研究设置项目环境,训练模型和设置服务器的分步指南。
  • 该项目强调了海底的必要

Image description

对该项目的一些思考

启动该项目将Oceanbase与AI整合在一起是一项艰巨而又有意义的努力。这是一个探讨AI和数据库的复杂性的机会,这提供了丰富的学习经验。

通常,我们想使用AI的文档聊天机器人回答用户查询,并使用文档数据库中的信息来回答用户查询。它将用户问题转换为向量表示,并将其与预处理文档向量进行比较。最相关的文档是根据向量相似性确定的。然后,聊天机器人使用此文档生成上下文适当的答案,提供精确且相关的答案。

在聊天机器人可以根据给定文档回答问题之前,您需要使用相关知识来训练它。本质上,培训过程是将文档中的文章转换为嵌入的嵌入 - 捕获单词语义含义的文本的数值表示(向量)。然后将嵌入在该项目的数据库中存储在数据库中。

您可能会问为什么我不使用Langchain,这已经是已建立的解决方案。尽管Langchain是语言模型培训和部署的强大平台,但由于其期望和我们的特定要求,它并不是该项目的最佳选择。

Langchain假设基础数据库可以处理向量存储并执行相似性搜索。但是,我们使用的数据库Oceanbase并不直接支持向量数据类型。

此外,该项目的主要目的是探索Oceanbase作为AI培训的矢量数据库的潜力。在内部处理相似性搜索的Langchain将绕开探索海底的这些方面的机会。

由于海底没有直接支持向量商店,因此我们必须找到解决方法。但是向量本质上是一系列数字。我们可以在数据库中将其存储为JSON字符串或BLOB。当我们需要将记录与嵌入问题进行比较时,我们可以在下一步中将它们转回嵌入。

鉴于海碱并不固有地支持向量数据类型,因此每个嵌入需要转换为Oceanbase确实支持的数据类型,在这种情况下为JSON。因此,在将问题与文章进行比较时,需要将这些记录恢复到其原始嵌入形式。不幸的是,这种转换过程导致缺乏可伸缩性的系统较慢。

为了充分利用AI和机器学习的潜力,我强烈建议Oceanbase团队考虑纳入对矢量数据类型的支持。这不仅可以提高系统的性能和可扩展性,而且还将Oceanbase作为一种前瞻性的数据库解决方案,准备接受AI时代。我在他们的GitHub repo上创建了一个问题。如果您有兴趣进一步探索,请欢迎参加讨论。

该项目

使用Express.js来处理HTTP请求,该系统是在Node.js框架上构建的。续集是一种基于承诺的node.js orm,用于数据库架构迁移,播种和查询的任务。

该项目的结构是处理两个主要请求:用文章培训模型并提出问题。 “/火车”端点获取文章,将其转换为向量,并将其存储在数据库中。 “/ask”端点提到用户的问题,使用存储的向量找到最相关的文章,并使用OpenAI的API生成答案。

Image description

此项目可在GitHub上获得,供每个人访问和使用。这意味着您不仅可以利用这种AI驱动解决方案的功能来用于海底,还可以利用您可能拥有的任何其他文档集。请随意克隆存储库,探索代码,并将其用作培训您自己的文档聊天机器人的基础。这是使您的文档更具互动性和可访问的好方法。

入门

要从头开始设置项目环境,请执行以下步骤:

安装node.js :如果尚未安装,请从官方的Node.js website下载和安装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 as the ORM与海底数据库进行交互。 lodash软件包用于在相似性搜索过程中计算cosine similarity。当然,我们需要openai包来与Openai的API进行交互。我们将以Express作为服务器,将揭露问候和训练端点。

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

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

建立海底

要设置项目,您首先需要一个运行的海底群集。我写的这本article有有关如何建立海底的详细说明。

拥有运行的海底实例后,下一步是创建一个表格,以存储文章及其相应的嵌入。该表格为“ article_vectors”,将具有以下列:

  • id:这是一个整数类型列,它将用作表的主要键。它是自动收入的,这意味着每个新条目将自动分配一个ID,该ID比上一个条目的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文档的数据填充此表,然后使用此数据训练我们的AI模型。

我们还需要在sequlize项目中定义文章向量模型。首先,运行sequelize init初始化续集环境。在生成的/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;
};

代码在我们的海基数据库中定义了article_vectors表的续集模型。续集是一个基于承诺的node.js orm,允许您使用高级API调用与SQL数据库进行交互。

关于文档的培训

培训过程是我们文档聊天机器人的核心。它涉及从GitHub存储库中获取海底文档,将内容转换为向量表示,并将这些向量存储在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 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文件。请注意,我添加了一个限制参数以限制获取的文件数量。这是因为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()

要进行续集和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的嵌入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()函数将生成的向量存储在海基数据库中。它使用quelize连接到数据库,在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);
    }
}

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

此培训过程可确保聊天机器人对海底文档有全面的了解,从而使其可以为用户查询提供准确且相关的答案。

在下一部分中,我们将讨论如何使用训练有素的模型回答用户查询。

您可能已经注意到,我们正在使用.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"

问文档

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

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

接下来,它从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);
    // 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;
}

cosineSimilarity()askDocs.js中的功能是文档检索过程的关键部分。它计算两个向量之间的余弦相似性,这是它们的相似程度的量度。

在此项目的上下文中,它用于确定用户问题的向量表示与数据库中文章的向量表示之间的相似性。与问题相似最高的文章被认为是最相关的。

该函数将两个向量作为输入。它计算两个向量的点产物和每个向量的大小。然后将余弦相似性计算为DOT产物除以两个幅度的乘积。

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);
}

该函数返回与问题最高的余弦相似性的文档。这是给定问题的最相关文档。

通过确定最相关的文档,askAI()函数在askDocs.js中为问题提供了答案。它使用OpenAI的API根据已确定的文档生成适当的上下文答案。

该函数将请求发送给API,并带有问题和相关文档。 API返回一个生成的答案,然后该功能返回。

此过程可确保聊天机器人为用户查询提供准确且相关的答案,使其成为任何从海底文档中寻求信息的人的宝贵工具。

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初始化的,这是Node.js的快速,未开放和最小化的Web框架。它被配置为使用express.json()中间件的JSON有效载荷解析传入请求。

const express = require('express');
const app = express();
const db = require('./models');
app.use(express.json());

服务器使用semelize与海底数据库建立了连接。 sync()函数在续集实例上调用,以确保所有定义的模型都与数据库同步。这包括创建必要的表,如果它们还不存在。

db.sequelize.sync().then((req) => {
    app.listen(3000, () => {
        console.log('Server running at port 3000...');
    });
});

训练终点

/train端点是触发训练过程的后路线。它期望包含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: 'Docs trained successfully.',
        });
    } catch (e) {
        next(e); // Pass the error to the error handling middleware
    }
});

如果培训过程成功,则端点会以成功消息做出响应。如果在此过程中发生错误,则将其传递给错误处理中间件。

端点的邮政请求将包含这样的请求主体:

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

您可以将存储库更改为您想要的其他GitHub托管文档。路径是目标内容的文件夹。在这种情况下,我使用了en-us,因为我只想培训英语文档。分支参数指示存储库的分支。在大多数情况下,您可以使用特定版本的主分支或分支。仅当存储库的文件太多时,才会使用限制参数,以使您的github api达到限制。

这就是培训成功的样子。

Image description

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

Image description

询问端点

/ask端点也是邮政路线。它期望包含用户问题的请求主体。这个问题传递给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响应。这是成功请求的一个示例。

Image description

正如我们所看到的,当我们询问聊天机器人时,在演示环境中安装Oceanbase的软件和硬件环境是什么,聊天机器人提供了一个article中所述的答案。

Image description

结论

这个项目代表了将AI与Oceanbase集成以创建文档聊天机器人的实验。尽管它证明了AI在增强文档的可用性方面的潜力,但重要的是要注意,这仍然是一种实验性设置,尚未准备好生产使用。当前的实现面临可伸缩性问题,因为需要将向量转换为JSON进行存储并重新进行矢量以进行比较。

为了充分利用AI和机器学习的潜力,我再次建议Oceanbase团队考虑纳入对矢量数据类型的支持。这不仅可以提高系统的性能和可扩展性,而且还将将Oceanbase视为一种前瞻性数据库解决方案,因为AI成为下一个大事。我已经创建了an issue in the OceanBase GitHub repo来讨论此建议,欢迎您加入对话。

该项目的代码可在GitHub上获得,供每个人访问和使用。这意味着您不仅可以利用这种AI驱动解决方案的功能来用于海底,还可以利用您可能拥有的任何其他文档集。随意克隆存储库,探索代码,并将其用作使用Oceanbase培训您自己的文档聊天机器人的基础。这是使您的文档更具互动性和可访问的好方法。

总而言之,尽管仍有工作要做,但该项目代表了将Oceanbase用作AI培训的矢量数据库的有希望的步骤。随着数据库的进一步开发和完善,我们可以期待在文档中找到答案与提出问题一样简单的时间。