SJ blog
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 に正確な文脈を与える」ことです。品質に最も影響するのは:

  1. チャンキング戦略: 意味の区切りで分割する
  2. Embedding モデルの選択: 言語・用途に合わせる
  3. 検索の精度: コサイン類似度 or ハイブリッド検索

参考: pgvector / LangChain RAG ガイド