前言#
RAG(Retrieval-Augmented Generation)について説明する前に、現在使用しているこれらのモデル、つまり LLM の欠点について議論します:
- 答えがない場合に虚偽の情報を提供する。(でたらめを言う)
- ユーザーが特定の現在の応答を必要とする際に、古いまたは一般的な情報を提供する。(モデルにはトレーニングの締切日があるため)
- 非権威的なソースから応答を生成する。(トレーニングの内容がインターネットから来ているため、不正確な内容が含まれる可能性がある)
- 用語の混乱により、異なるトレーニングソースが同じ用語を使用して異なる事柄について話すため、不正確な応答が生じる。
もう一つ重要で現実的な理由があります —— 私たち自身にはいくつかのプライベートデータがあり、市場に出回っているいくつかのモデルは私たちのデータでトレーニングされていないため、私たちのデータセットに対して質問することは不可能です。
そこで RAG(Retrieval-Augmented Generation)が登場しました —— これは、大規模言語モデル(LLM)とのインタラクションを変更することで、モデルがユーザーのクエリに応じて指定されたドキュメントセットを参照し、これらの情報を優先的に使用するようにします。これにより、LLM は特定の分野や最新の情報を使用できるようになります。ユースケースには、チャットボットが企業内部のデータにアクセスすることや、権威あるソースからの事実情報のみを提供することが含まれます。
流程#
- まず、私たちの知識データの一部をベクトルモデルを使用してベクトルに変換し、それをベクトルデータベースに保存する必要があります。
- 残りはユーザーの入力です。その後、ユーザーのデータをベクトルに変換し、ユーザーが入力したベクトルを使用して現在のベクトルデータベースを検索(Retrieval)し、類似度に基づいて最も近い数件のデータを見つけます。次に、大モデルを使用して検索されたデータソースとユーザーのニーズに関する質問を組み合わせ(Augmented)、大モデルに入力して生成(Generation)します。こうして —— 完全な RAG が完成します。
コード実装#
フロントエンド開発者として最も馴染みのあるのは JavaScript です。ちょうど市場で有名なlangchainには js のバージョンがあり、フロントエンド開発が AI 開発に入るのがより簡単になりました。余計なことは言わず、今すぐ始めましょう!
簡単な使用法#
次に、コードを通じて RAG の使用方法を示します。簡単のために、LangChain の JavaScript バージョンを使用し、ローカルで llama3:3b モデルを使用します。必要に応じて他のモデルに置き換えることができます。
import { ChatOllama } from "@langchain/ollama";
import { StringOutputParser } from '@langchain/core/output_parsers'
const llm = new ChatOllama({
model: 'llama3.2:3b-instruct-fp16',
temperature: 0, // 生成内容のランダム性を制御
});
const res = await llm.invoke('こんにちは');
// 出力テキストをフォーマット
const parser = new StringOutputParser();
const parsed = await parser.invoke(res);
console.log(parsed);
// 出力:こんにちは!私はあなたのAIアシスタントです。私たちの会話にようこそ。何かお手伝いできることはありますか?
ドキュメントの読み込み#
私たちの目的は自分のデータに対して強化検索を行うことなので、最初のステップはドキュメントを読み込むことです。langchain はさまざまなドキュメントを読み込む方法を提供しています(ドキュメントの読み込み)。基本的に市場に出回っているすべてのドキュメントが含まれています。興味がある方はぜひご覧ください。今回は Web からリモートでドキュメントを読み込みます。
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
const loader = new CheerioWebBaseLoader("https://goodcheng.wang/web3", {
selector: 'article' // 読み込む内容の領域を指定
});
const doc = await loader.load();
ドキュメントの分割#
なぜドキュメントを分割する必要があるのでしょうか?直接全体のドキュメントを埋め込む(embedding)ことは簡単に見えますが、実際には効率的ではありません。理由は以下の通りです:
- メモリと計算の制限:大規模なドキュメント(例えば、全書籍や長い報告書など)は通常非常に大きく、直接全体のドキュメントを埋め込むと、計算リソースとメモリの消費が非常に高くなります。明らかな問題があります —— 私たちが使用するモデルはそんなに大きな入力をサポートしていません。
- コンテキストウィンドウの制限:現在のほとんどの自然言語処理(NLP)モデルは、テキストを処理する際に一定のコンテキストウィンドウサイズの制限があります。つまり、モデルは一定数のトークンしか処理できません。テキストがそのウィンドウサイズを超えると、超えた部分は切り捨てられたり無視されたりし、大量の情報が失われます。したがって、ドキュメントを小さなブロックに分割し、各ブロックの長さをモデルのコンテキスト制限に適応させることで、モデルが埋め込みを生成する際に入力のコンテキスト情報を十分に活用できるようにします。
- 検索とマッチング精度の向上:ドキュメントを分割することで、情報検索の精度も向上します。全体のドキュメントを埋め込んだ場合、特定の質問を照会すると、埋め込みがあまりにも多くの無関係な情報を含む可能性があり、マッチングが不正確になります。逆に、ドキュメントを分割し、各セクションを埋め込むことで、照会時に問題に最も関連する段落のみを検索でき、結果がより正確になります。
- その他
ここでは、テキストを 500 文字ごとに分割し、隣接する 100 文字を「ブロック」として扱います。興味がある方は、このウェブサイトで自分で試してみてくださいText-splitter demo。
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
const splitter = new RecursiveCharacterTextSplitter({
chunkOverlap: 100, // 隣接ブロックの重複文字数、連続性を強化
chunkSize: 500, // 各ブロックの文字数
});
const allSplits = await splitter.splitDocuments(doc);
console.log(allSplits[0]); // 分割された最初のブロックを印刷
コンテンツを埋め込み、ベクトルデータベースに保存#
次に、分割されたドキュメントの内容を埋め込み、ベクトルデータベースに保存します。この例では、メモリ内のベクトルデータベースを使用しますが、実際のアプリケーションでは他のストレージソリューションに置き換えることができます。VectorStore
import { OllamaEmbeddings } from "@langchain/ollama";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
const embeddings = new OllamaEmbeddings({
model: "mxbai-embed-large", // デフォルト値
baseUrl: "http://localhost:11434", // デフォルト値
});
const vectorStore = await MemoryVectorStore.fromDocuments(allSplits, embeddings)
const vectorStoreRetriever = vectorStore.asRetriever({
k: 5,
searchType: 'similarity',
})
const find = await vectorStoreRetriever.invoke("web3.0とは何ですか")
console.log(find)
RAG チェーンに統合#
最後に、上記のすべてのステップを統合して完全な処理チェーンを作成します。まず、生成されたチェーンのプロンプトテンプレートを作成します:
import {ChatPromptTemplate} from '@langchain/core/prompts'
const ragPrompt = ChatPromptTemplate.fromMessages([
[
'human',
`あなたは質問応答タスクのアシスタントです。以下の検索されたコンテキストを使用して質問に答えてください。答えがわからない場合は、わからないと言ってください。最大三文を使用し、答えは簡潔にしてください。
質問: {question}
コンテキスト: {context}
答え:`
]
])
次に、RunnableSequence を使用してプロセスをリンクします:
import { formatDocumentsAsString } from "langchain/util/document";
import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
const runnableRagChain = RunnableSequence.from([
{
context: vectorStoreRetriever.pipe(formatDocumentsAsString), // 検索されたコンテキスト
question: new RunnablePassthrough(), // ユーザーの質問
},
ragPrompt, // 生成されたプロンプト
llm, // 大規模言語モデル
new StringOutputParser(), // 生成された結果を解析
]);
const res = await runnableRagChain.invoke('web2.0の代表的なウェブサイトは何ですか?');
console.log(res);
// 出力:FacebookとTwitterはWeb2.0の著名なウェブサイトです。
結果はなかなか良いです~
まとめ#
RAG(Retrieval-Augmented Generation)を通じて、私たちは既存の大規模言語モデルの回答の正確性を向上させることができ、特に最新または特定の分野のデータが必要なシナリオで効果を発揮します。本記事では、LangChain の JavaScript 実装を通じて、ドキュメントの読み込み、分割、埋め込みから最終生成までの全プロセスを示しました。