Node.jsでLangChainとMomento Vector Indexを使ってQ&AシステムのRAGパイプラインを構築する方法

ベクトルインデックスとLangChainがあれば、質問応答システムを作るのは簡単だ。

注:このチュートリアルでは、TypeScript(Node.js)とPythonの両方でコード例を示します。これはNode.jsのチュートリアルです。Python版はこちら!

このブログポストでは、Momento Vector IndexとLangChainを応用して、ドキュメントを使って質問に対する人間が読める回答を生成できるRAGパイプラインを作成する方法を探ります。まず、LangChainを使ってドキュメントを読み込んで管理する方法を説明します。次に、ドキュメントをチャンクに分割し、Momento Vector Indexでインデックスを作成します。最後に、より自然な回答を生成するために、GPT 3.5とインデックスを接続します。このガイドでは、ベクターインデックスの構築と活用に必要な技術的ステップを提供し、ベクター検索とデータ検索の機能を強調します。

一般的なベクターインデックスについては、Momentoの開発ドキュメントLangChainチャットボットに関するビデオをご覧ください。

前提条件

例に従うには、Node.jsをインストールする必要があります。インストールしたら、簡単なプロジェクトを作成し、TypeScriptをインストールします:

npm init -y
npm install --save-dev typescript
npx tsc --init
touch example.ts

後で、TypeScriptをJavaScriptにコンパイルし、結果のファイルを実行することで、プロジェクトを実行できます:

npx tsc && node example.js

プロジェクトがセットアップされたら、Momento Vector IndexとLangChainを使い始めるために必要な依存関係を以下に示します:

npm install @gomomento/sdk langchain @langchain/openai cheerio

これが、これから編集するファイルで、使用するインポートも含まれています:

import {CheerioWebBaseLoader} from "langchain/document_loaders/web/cheerio";
import {TokenTextSplitter} from "langchain/text_splitter";
import {CredentialProvider, PreviewVectorIndexClient, VectorIndexConfigurations} from "@gomomento/sdk";
import {MomentoVectorIndex} from "@langchain/community/vectorstores/momento_vector_index";
import {ChatOpenAI, OpenAIEmbeddings} from "@langchain/openai";
import {sleep} from "langchain/util/time";
import {RetrievalQAChain} from "langchain/chains";

async function main() {

}

main().catch(console.error);

すべてのサンプル・コードはmainメソッドに追加されます。

APIキー

APIキーMomento Vector Indexを使用するには、スーパーユーザーAPIキーが必要です。APIキーの生成方法は開発者向けドキュメントをご覧ください。

OpenAI API Keyも必要で、これはOpenAIから入手できます。

これらのAPIキーをMOMENTO_API_KEYOPENAI_API_KEY環境変数に書き込めば、LangChainとMomentoが自動的にそれらを見つけてくれます。

データの読み込み

この例では、ウィキペディアのニンジンのページをインデックスします。LangChainはウィキペディアローダーを提供していないので、より一般的なCheerio web loader:を使っています。LangChainはWikipediaローダーを提供していないので、より一般的なCheerioウェブローダーを使います:

const loader = new CheerioWebBaseLoader(
    "https://en.wikipedia.org/w/api.php?action=query&format=json&titles=Carrot&prop=extracts&explaintext"
);

const docs = await loader.load();

const pageJson = JSON.parse(docs[0].pageContent);
const firstPageKey = Object.keys(pageJson.query.pages)[0];
docs[0].pageContent = pageJson.query.pages[firstPageKey].extract;
console.log(docs[0].pageContent.length)
console.log(docs[0].pageContent.substring(0, 500));

21534.

ニンジン(Daucus carota subsp. sativus)は根菜で、一般的にはオレンジ色をしているが、紫、黒、赤、白、黄色などの品種もあり、いずれもヨーロッパと南西アジア原産の野生のニンジンDaucus carotaを家畜化したものである。原産地はおそらくペルシャで、もともとは葉と種子のために栽培されていた。最もよく食べられているのは根の部分だが、茎や葉も食べられている。この例では、ウィキペディアのニンジンのページをインデックス化する。LangChainはウィキペディアローダーを提供していないので、より一般的なCheerio web loader:を使っています。LangChainはWikipediaローダーを提供していないので、より一般的なCheerioウェブローダーを使います:

ウィキペディアのURLに引数を追加して、ページのプレーンテキストJSONを取得し、それをローダーで取得します。ドキュメントを取得した後、ページの本文だけに切り詰めます。あなたのドキュメントやウェブサイトには、おそらく独自のカスタムロジックが必要でしょう。

Split

これで一つの大きなドキュメントができました。これをベクターとして埋め込み、ベクター・インデックスに追加すると、どのようなクエリに対してもドキュメント全体が返されることになります。有用な答えを得るためには、ページの最も関連性の高い部分だけが返されるように、チャンクに分割する必要があります。チャンクサイズの選択とオーバーラップはこのドキュメントの範囲外ですが、より深く知りたい方はこちらの論文を参考にしてください。

const splitter = new TokenTextSplitter({
    encodingName: "gpt2",
    chunkSize: 200,
    chunkOverlap: 0,
});

const splitDocs = await splitter.splitDocuments(docs);
console.log(splitDocs.length);

25

TokenTextSplitterは、ドキュメントを指定された長さのチャンクに分割しようとします。OpenAIのモデルが使っているのと同じアルゴリズムであるバイトペアエンコーディングを使って、ドキュメントをトークン化します。この方法で作成されたトークンは長さが固定されていないため、チャンクにはわずかに異なる数の文字が含まれる可能性があります。目安としては、トークンは平均して4文字程度です。

Index

OpenAIを使ってチャンクをベクターに埋め込み、Momentoにアップロードします。インデックスがまだ存在しない場合は、インデックスも作成します。OpenAIのレート制限エラーに遭遇した場合は、OpenAIEmbeddingschunk_sizeオプションを試してみてください。

Query

これで検索可能なベクトル・インデックスができました!このようにクエリーできます:

response = await vectorStore.similaritySearch("Are all carrots orange?");
console.log(response[0]);

10世紀、あるいはそれ以前かもしれない。現代まで残っている東洋ニンジンの標本は、紫色か黄色が一般的で、根が枝分かれしていることが多い。西洋」ニンジンは17世紀にオランダで生まれた。オレンジ色をしていることから、オレンジ家とオランダ独立闘争の象徴としてオランダで広まったという俗説があるが、その根拠はほとんどない。オレンジ色は、これらの品種に含まれる豊富なカロテンに起因する。西洋ニンジンの品種は、一般的に根の形によって分類される。一般的には以下の4種類である: シャンテネー・キャロット。根は他の品種より短いが、葉は旺盛で胴回りが大きく、肩幅が広く、先端が鈍く丸みを帯びている。貯蔵性に優れ、芯は淡い色をしており、主に加工用に使われる。

見ての通り、最も関連性の高いチャンクを見つけて返します。質問した内容が正しい情報とベクトル空間で十分に類似していれば、有用な答えが得られます。その情報をより人間が読みやすい形式に変えたい場合は、LangChainを使ってチャンクとクエリをOpenAIのLLMに送り、より自然な答えを生成することができます:

const llm = new ChatOpenAI({
    modelName: "gpt-3.5-turbo",
    temperature: 0,
});
const qaChain = RetrievalQAChain.fromLLM(llm, vectorStore.asRetriever());

これでMomento Vector IndexをGPT 3.5にチェーンし、次のようなクエリを実行できるようになりました:

llmResult = await qaChain.invoke({"query": "Are all carrots orange?"})
console.log(llmResult);

いや、すべてのニンジンがオレンジ色というわけではない。ニンジンには紫、黒、赤、白、黄色の品種もある。これらは家宝品種であり、野生のニンジンを家畜化したものである。

これで、結果には必要な情報だけが含まれるようになりました。もちろん、GPT 3.5はすでにニンジンについて知っていると思いますが、この方法でどんな情報も人間が読める結果に変換することができます。フローが完成すると、LangChainがどのように複数のデータソースをつなげて有用な答えに変換しているかがわかります。もちろん、GPT3.5はすでにニンジンについて知っている可能性が高いが、どんな情報でもこの方法で人間が読める結果に変換することができます。このフローが完成すると、LangChainがどのように複数のデータソースをつなげて、有用な答えに変換しているかがわかります。

この時点で、データの保存とクエリに使用できるインデックスができ、ベクトル検索をサポートするあらゆるチェーンに接続できます。ドキュメントに基づいて質問に答えたり、チャットボットのメッセージ履歴を保存したり、ベクターを使って想像できることは何でもできます。

LangChainを使わずにMomento Vector Indexを直接使う方法は、このブログ記事Momentoのドキュメントをご覧ください。