手っ取り早い答え: 検索拡張生成(RAG)は、2つのアイデアを組み合わせたものです。まず、ドキュメントを一度だけ埋め込み、得られたベクトルを保存します。次に、質問のたびにその質問を埋め込み、意味的に最も近いチャンクを取得して、それらをコンテキストとしてLLMに渡します。こうすることで、回答はモデルの学習記憶ではなく、あなたのデータから生成されます。sgcWebSocketsでは、このパイプライン全体が3つのコンポーネントで構成されます。テキストをベクトルに変換する TsgcAIOpenAIEmbeddings、それらを保存・検索する TsgcAIDatabaseVectorFile または TsgcAIDatabaseVectorPinecone、そして最終的な根拠に基づいた回答を書く TsgcHTTP_API_OpenAI です。
汎用的なチャットモデルは世界について多くを知っていますが、あなたの製品マニュアル、サポートチケット、あるいは前四半期の社内レポートについては何も知りません。それらについて尋ねると、回答を拒否するか、もっと悪ければもっともらしい何かをでっち上げます。RAGは、何も再学習せずにこれを解決します。モデルはそのままにしておき、質問時にあなた自身のコーパスから適切な一節を与えるのです。以下に、実際のコンポーネント名を使って、Delphiでのループ全体を最初から最後まで示します。
RAGが実際に行うこと
埋め込みとは、テキストの意味を捉えた数値のリストです。同じトピックに関する2つの一節は、キーワードを1つも共有していなくても、その数値空間内で近い位置に配置されます。ベクトルデータベースはそれらの数値を保存し、クエリベクトルが与えられると、類似度でランク付けされた最も近いエントリを返します。RAGはこれらをつなぎ合わせます。
| 段階 | 何が起こるか | sgcWebSocketsの要素 |
|---|---|---|
| 1. 取り込み(一度だけ) | ドキュメントをチャンクに分割し、各チャンクを埋め込み、ベクトルを保存する | TsgcAIOpenAIEmbeddings.CreateEmbeddingsFromFile |
| 2. 保存 | ベクトルをローカルファイルまたはクラウドインデックスに保持する | TsgcAIDatabaseVectorFile · TsgcAIDatabaseVectorPinecone |
| 3. 取得(質問ごと) | 質問を埋め込み、最も近いチャンクを見つける | GetEmbedding → QueryData |
| 4. 回答 | チャンクをプロンプトに入れ、モデルに尋ねる | TsgcHTTP_API_OpenAI._CreateChatCompletion |
ステップ1と2は、データが変更されたときに実行します。ステップ3と4は、ユーザーの質問のたびに実行します。それぞれを作っていきましょう。
ステップ1 — ドキュメントを埋め込む
TsgcAIOpenAIEmbeddings を作成し、OpenAIキーを与え、その Database プロパティをベクトルストアに向け、CreateEmbeddingsFromFile を呼び出します。この1回の呼び出しでファイルを読み込み、チャンクに分割し(EmbeddingsOptions.ChunkSize で制御)、各チャンクを埋め込み、BeginAddData / AddData / EndAddData のシーケンスを通じてベクトルをストアに書き込んでくれます。
uses
sgcAI, sgcAI_OpenAI_Embeddings,
sgcAI_DB_Vector, sgcAI_DB_Vector_File, sgcAI_DB_Vector_Pinecone;
var
Embeddings: TsgcAIOpenAIEmbeddings;
DBFile: TsgcAIDatabaseVectorFile;
begin
Embeddings := TsgcAIOpenAIEmbeddings.Create(nil);
Embeddings.OpenAIOptions.ApiKey := 'sk-...';
// local, file-based vector store
DBFile := TsgcAIDatabaseVectorFile.Create(nil);
DBFile.VectorFileOptions.InputFilename := 'corpus.sgcif';
DBFile.VectorFileOptions.VectorFilename := 'corpus.sgcvf';
Embeddings.Database := DBFile;
Embeddings.CreateEmbeddingsFromFile('docs.txt');
end;
これが取り込みステップのすべてです。一度実行するか、ドキュメントが変更されるたびに実行します。デフォルトの埋め込みモデルは text-embedding-3-small です。別のモデルが必要な場合は EmbeddingsOptions.Model から変更してください。詳細は Embeddingsコンポーネントのページにあります。
ステップ2 — ベクトルの保存場所を選ぶ
どちらのバックエンドも同じ基底コンポーネント TsgcAIDatabaseVector を継承しているため、相互に置き換え可能です。一方を他方に入れ替えても、取り込みコードやクエリコードは変わりません。唯一の違いは、データがどこに置かれるかだけです。
デスクトップアプリ、オフラインツール、あるいは小規模なコーパスには、TsgcAIDatabaseVectorFile が外部サービスなしですべてをローカルファイルに保持します。インデックスが大きい、プロセスやユーザー間で共有する必要がある、あるいは1台のマシンを超えてスケールしなければならない場合は、TsgcAIDatabaseVectorPinecone に切り替えます。これはマネージドのPinecone REST APIを通じて、すべてのチャンクをupsertします。
var
DBPinecone: TsgcAIDatabaseVectorPinecone;
begin
DBPinecone := TsgcAIDatabaseVectorPinecone.Create(nil);
DBPinecone.PineconeOptions.ApiKey := 'pc-...';
DBPinecone.PineconeIndexOptions.IndexName := 'sgc-embeddings';
Embeddings.Database := DBPinecone;
Embeddings.CreateEmbeddingsFromFile('docs.txt');
end;
取り込みの行がステップ1とまったく同じであることに注目してください。それが共有基底クラスの要点です。ファイルバックエンドについてはベクトルデータベースのページを、クラウドについてはPineconeのページをご覧ください。
ステップ3 — 取得して回答する
次は質問ごとの経路です。ユーザーの質問を埋め込み、保存された最も近いチャンクを1回の呼び出しで見つけます。GetEmbedding はテキストを埋め込み、それをデータベースの QueryData に通して、あなたのコーパスから最も関連性の高い一節を返します。それらの一節があなたのコンテキストです。それらを質問と連結し、全体をチャットモデルに送信します。
var
Question, Context, Prompt, Answer: string;
OpenAI: TsgcHTTP_API_OpenAI;
begin
Question := 'How do I enable the WatchDog reconnect?';
// retrieve the closest chunks from your own data
Context := Embeddings.GetEmbedding(Question, '');
// build a grounded prompt
Prompt :=
'Answer the question using only the context below.' + sLineBreak +
'If the context does not contain the answer, say you do not know.' +
sLineBreak + sLineBreak +
'Context:' + sLineBreak + Context + sLineBreak + sLineBreak +
'Question: ' + Question;
// ask the model
OpenAI := TsgcHTTP_API_OpenAI.Create(nil);
OpenAI.OpenAIOptions.ApiKey := 'sk-...';
Answer := OpenAI._CreateChatCompletion('gpt-4o-mini', Prompt);
Memo1.Lines.Text := Answer;
end;
これがRAGです。モデルは学習中にあなたのドキュメントを一度も見ていませんが、それでもそこから回答します。なぜなら、リクエスト時に関連する一節をモデルの目の前に置いたからです。コーパスを変更すれば、それに合わせて回答も変わり、ファインチューニングは一切不要です。コンテキストが空のときに回答を拒否するよう指示することが、モデルに推測させず誠実に保つ鍵です。
ローカルでもクラウドでも、同じコード
繰り返す価値のある点が1つあります。ファイルストアとPineconeの選択は可逆です。TsgcAIDatabaseVectorFile と TsgcAIDatabaseVectorPinecone は TsgcAIDatabaseVector 基底を共有しているため、ローカルファイルに対してプロトタイプを作り(インフラ不要、オフラインで動作)、後で Embeddings.Database に割り当てるコンポーネントを入れ替えるだけでPineconeに移行できます。取り込みコードやクエリコードは何も変わりません。最後のLLMについても同じです。TsgcHTTP_API_OpenAI の _CreateChatCompletion は、最終的な回答を書くのに別のモデルを使いたい場合、AnthropicやGeminiのコンポーネントに入れ替えることができます。
チャンク分割と品質についての注記
取得の品質は、ドキュメントをどう分割するかに依存します。小さなチャンクはマッチをより正確にしますが、コンテキストを失う可能性があります。大きなチャンクはコンテキストを保ちますが、マッチを薄めてしまいます。EmbeddingsOptions.ChunkSize が CreateEmbeddingsFromFile でこれを制御するので、あなたの素材に合わせて調整する価値があります。より細かく制御するには、CreateEmbeddings で個々の文字列を埋め込み、取り込む前にチャンクを自分で形作ることもできます。
はじめに
3つのコンポーネントはすべてsgcWebSocketsに付属しています。無料トライアルを入手し、埋め込みとベクトルストアのコンポーネントをフォームに配置し、テキストファイルに向ければ、100行を大きく下回るコードで動作するRAGループが手に入ります。AI & LLM コンポーネントハブでAI構築ブロックの全セットをご覧ください。
これを自前のコーパスに適用することについてご質問は? お問い合わせください — コードを書いた本人たちから返信が届きます。
