ai
A
信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
RAG(検索拡張生成)をゼロから実装する
RAGの仕組みから実装まで解説。テキストのチャンキング・Embeddingによるベクトル化・類似検索・LLMへのコンテキスト注入まで、Node.jsの実例コードで紹介します。
一言結論
RAGの品質はチャンキング戦略とEmbeddingモデルの選択で大きく左右され、適切なオーバーラップと上位k件のコンテキスト注入を丁寧に設計することが「LLMが知らない情報を正確に答えさせる」ための核心だ。
RAG とは
RAG(Retrieval-Augmented Generation)は、LLM の知識を外部ドキュメントで補強する手法です。
問題: LLM は学習データにない情報を知らない(社内文書・最新情報など)
解決策:
1. ドキュメントを事前にベクトル化してDBに保存
2. 質問と関連するドキュメントを検索
3. 関連ドキュメントをコンテキストに加えて LLM に渡す
RAG の全体フロー
【事前処理】
ドキュメント → チャンキング → Embedding → ベクトルDB
【質問時】
質問 → Embedding → ベクトルDB で類似検索 → 上位N件取得
→ LLM に「質問 + 関連ドキュメント」を渡す → 回答生成
ステップ1: テキストのチャンキング
長いドキュメントをLLMに渡せるサイズに分割します。
function chunkText(text: string, chunkSize = 500, overlap = 50): string[] {
const chunks: string[] = [];
let start = 0;
while (start < text.length) {
const end = Math.min(start + chunkSize, text.length);
chunks.push(text.slice(start, end));
start += chunkSize - overlap; // 前後をオーバーラップさせる
}
return chunks;
}
const doc = "長い技術文書...";
const chunks = chunkText(doc, 500, 50);
ステップ2: Embedding でベクトル化
import OpenAI from "openai";
const openai = new OpenAI();
async function embed(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text,
});
return response.data[0].embedding;
}
// 全チャンクをベクトル化
const embeddings = await Promise.all(
chunks.map(async (chunk, i) => ({
id: i,
text: chunk,
vector: await embed(chunk),
}))
);
ステップ3: ベクトルDBへの保存
シンプルな実装(インメモリ)
// 小規模なら配列でもOK
const vectorStore: Array<{ id: number; text: string; vector: number[] }> = [];
function cosineSimilarity(a: number[], b: number[]): number {
const dot = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
const normA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
const normB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
return dot / (normA * normB);
}
function search(queryVector: number[], topK = 3) {
return vectorStore
.map((item) => ({
...item,
score: cosineSimilarity(queryVector, item.vector),
}))
.sort((a, b) => b.score - a.score)
.slice(0, topK);
}
本格的なベクトルDB
pgvector — PostgreSQL 拡張(既存 DB に追加できる)
Qdrant — 高性能・Rust 製
Pinecone — マネージドサービス
Chroma — 開発向け、ローカル利用に便利
Weaviate — フル機能のベクトルDB
-- pgvector の使用例
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536) -- text-embedding-3-small の次元数
);
-- 最も類似したドキュメントを取得
SELECT content, 1 - (embedding <=> '[0.1, 0.2, ...]'::vector) AS similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 5;
ステップ4: RAG パイプラインの統合
async function ragAnswer(question: string): Promise<string> {
// 1. 質問をベクトル化
const queryVector = await embed(question);
// 2. 関連ドキュメントを検索
const relevantDocs = search(queryVector, 3);
// 3. コンテキストを作成
const context = relevantDocs
.map((doc, i) => `[${i + 1}] ${doc.text}`)
.join("\n\n");
// 4. LLM に質問 + コンテキストを渡す
const response = await openai.chat.completions.create({
model: "gpt-5.4-mini",
messages: [
{
role: "system",
content: "以下の参考情報のみを使って質問に答えてください。情報がない場合は「わかりません」と答えてください。",
},
{
role: "user",
content: `参考情報:\n${context}\n\n質問: ${question}`,
},
],
});
return response.choices[0].message.content ?? "";
}
// 使用例
const answer = await ragAnswer("TypeScript の satisfies 演算子とは何ですか?");
チャンキング戦略の改善
// 文単位でチャンクする(より自然な分割)
function sentenceChunk(text: string, maxChunkSize = 500): string[] {
const sentences = text.match(/[^。!?.!?]+[。!?.!?]/g) ?? [];
const chunks: string[] = [];
let current = "";
for (const sentence of sentences) {
if (current.length + sentence.length > maxChunkSize && current) {
chunks.push(current.trim());
current = sentence;
} else {
current += sentence;
}
}
if (current) chunks.push(current.trim());
return chunks;
}
まとめ
RAG の核心は「LLM に正確な文脈を与える」ことです。品質に最も影響するのは:
- チャンキング戦略: 意味の区切りで分割する
- Embedding モデルの選択: 言語・用途に合わせる
- 検索の精度: コサイン類似度 or ハイブリッド検索
参考: pgvector / LangChain RAG ガイド