AIコーディング 2026.05.06

LangChain × ベクトルDB で RAG システムを実装|5ステップで自社データAIを構築

タグ:RAG / LangChain / 生成AI / ベクトルDB / Python

RAGパイプラインで何ができるか

質問応答システムを自分たちのデータで作ることができます。例えば、会社の提案書や過去の案件資料から自動で情報を引き出して、生成AIに答えさせるような使い方です。

RAGは「データ取得と質問回答を組み合わせた仕組み」という意味です。ユーザーが質問すると、関連する資料を自動で探して、その内容をふまえた回答が返ってきます。社内マニュアルを読み込ませたり、顧客の問い合わせに自動で対応したり、膨大なドキュメントから必要な情報だけを抽出したりするときに活躍します。

従来は「生成AIに質問を投げて、学習データから回答を生成する」という流れでしたが、RAGを使うと「質問に関連する資料を先に見つけて、その資料の内容を参考に回答を作る」という流れになります。最新データへの対応や、企業秘密の保持、回答の正確さが大きく改善されます。

なお、Claude CodeのようなAIコーディングアシスタントでRAGを実装する際に特に多く発生するのが「コンテキストウィンドウ超過エラー」です。チャット履歴を保持しながら外部ドキュメントを検索するシステムでは、検索結果・チャット履歴・システムプロンプトが累積してモデルの最大トークン数を超えてしまうことがあります。本記事ではこの問題への対処も含めて解説します。

準備するもの

実装に必要なのは、Pythonの開発環境(バージョン3.10以上推奨)と、下記のライブラリです:

  • LangChain:質問応答の流れを組み立てるためのツール
  • ベクトルDB(FAISS、Pinecone、Chromaなど):資料を数値に変換して保存・検索するデータベース
  • OpenAI APIキーまたはAnthropic APIキーなどに相当するAPI:テキストを数値に変換したり(埋め込み)、質問に答えたりするAI機能
  • テキスト処理用ライブラリ:PDFやテキストファイルから情報を読み込む

実装の段階に応じて、Pythonのパッケージマネージャー(pip)でインストールを進めます。なお、Node.jsのエコシステムが充実した現在では、JavaScriptエンジニアもネイティブに実装できるようになっており、既存のWebアプリへの統合や、バックエンド・フロントエンド・AI処理を同じ言語で管理したい場合はNode.jsでの実装も有力な選択肢です。

手順

1. LangChainの基本構造を理解する(所要時間:15分)

RAGパイプラインは大きく3つの工程に分かれます。

第一段階:資料の準備 会社の資料やWebサイトなど、回答の根拠にしたいデータを集めます。テキストファイル、PDF、データベースなど、形式は自由です。

第二段階:埋め込み処理(ベクトル化) 資料の内容を「ベクトル」という数値のセットに変換して、ベクトルDBに保存します。この処理によって、後で「この質問に似た資料は何か」という検索が高速で実行できるようになります。

第三段階:質問応答 ユーザーが質問をすると、その質問もベクトル化されて、保存されている資料と比較されます。最も関連性の高い資料を見つけ出し、その内容を生成AIに与えて、質問に対する答えを作成します。

LangChainはこの3段階全体を管理するためのフレームワークです。なお、LlamaIndexはドキュメント検索に特化しており、RAGの検索パートにおいて最適化されています。LangChainは柔軟なパイプライン設計に強く、両者の選択は実装内容によって決まります。

2. ライブラリをインストールする(所要時間:10分)

Pythonの場合、ターミナルやコマンドプロンプトを開いて、下記を実行します:

pip install langchain langchain-openai faiss-cpu openai chromadb anthropic python-dotenv

Node.jsの場合は、下記を実行します:

npm install langchain @langchain/openai faiss-node dotenv axios
  • langchain:RAGの構築に必要なコア機能
  • @langchain/openai:OpenAI APIとの連携
  • faiss-node:ベクター検索(NPMで提供されているバイナリを使用)
  • dotenv.envファイルから環境変数を読み込み

3. テキストデータを読み込む(所要時間:20分)

Pythonの場合、コードで使う資料を読み込みます。例えば、テキストファイルの場合:

with open('company_manual.txt', 'r', encoding='utf-8') as f:
    documents = f.read()

複数のファイルがあれば、ループで処理します。PDFを使う場合は、別のライブラリ(例:PyPDFLoader)で読み込むことになります。文字列から直接Documentオブジェクトを作成したい場合は以下のようにします:

from langchain.schema import Document

doc_text = "これはサンプルドキュメントです。LangChainで処理します。"
document = Document(page_content=doc_text, metadata={"source": "manual"})
docs = [document]

Node.jsの場合、ドキュメントをLangChain形式のオブジェクトとして扱います:

const documents = [
  {
    text: "社内マニュアルの内容...",
    source: "manual.md",
  },
  // 複数ドキュメントも配列で管理
];

4. テキストを小分けにする(所要時間:15分)

長いテキストをそのまま使うと、検索が効率的になりません。適切なサイズに分割します。

Pythonの場合、LangChainの「テキストスプリッター」を使う方法が一般的です:

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
)
docs = splitter.split_text(documents)

Node.jsの場合は、テキストを手動でチャンク化する方法が実用的です:

function chunkDocument(text, chunkSize = 500, overlap = 50) {
  const chunks = [];

  for (let i = 0; i < text.length; i += chunkSize - overlap) {
    chunks.push(text.substring(i, i + chunkSize));
  }

  return chunks;
}

chunk_size は1回の分割で何文字にするか、chunk_overlap は分割したテキスト同士がどのくらい重複するかを指定します。重複があると、境界線の情報が失われにくくなります。

5. ベクトルDBを初期化する(所要時間:20分)

読み込んだテキストを数値に変換して、保存します。

Pythonの場合(FAISS)

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS

embeddings = OpenAIEmbeddings(api_key='your-api-key')
vectorstore = FAISS.from_texts(docs, embeddings)

ここで初めてOpenAI APIが呼ばれます。テキストが数値(ベクトル)に変換されて、FAISS内に保存されます。Chromaを使う場合はChroma.from_documents(docs, embeddings)で同様に作成できます。

Node.jsの場合

const { FaissStore } = require("@langchain/community/vectorstores/faiss");
const { OpenAIEmbeddings } = require("@langchain/openai");
require("dotenv").config();

const embeddings = new OpenAIEmbeddings({
  openAIApiKey: process.env.OPENAI_API_KEY,
  modelName: "text-embedding-3-small", // 小規模モデル(安価)
});

const docs = documents.map((doc, index) => ({
  pageContent: doc.text,
  metadata: { id: index, source: doc.source || "unknown" },
}));

const vectorStore = await FaissStore.fromDocuments(docs, embeddings);
await vectorStore.save("./faiss_store"); // ディスクに永続化

text-embedding-3-smallはOpenAIの軽量埋め込みモデルで、1536次元のベクトルを出力します。saveloadでインデックスをディスクに永続化することで、毎回ベクトル化を繰り返す手間を省けます。

6. 質問応答チェーンを構築する(所要時間:25分)

Pythonの場合、RAGでメモリ機能も含めた会話システムを作る際にはConversationalRetrievalChainを使うのが現在の標準的な方法です。まず、LangChainにはOpenAIを操作するクラスが2種類あります。

  • OpenAIクラス:単純な質問応答向け(テキスト補完API)
  • ChatOpenAIクラス:複数ターンの会話履歴を処理できる(チャットAPI)

RAGやメモリを使う場合はChatOpenAIを選ぶことを推奨します:

from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain

llm = ChatOpenAI(model_name='gpt-3.5-turbo', temperature=0.7, api_key='your-api-key')

memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True
)

retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
qa_chain = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=retriever,
    memory=memory,
    return_source_documents=True
)

memory_keyは必ず"chat_history"とすることに注意してください。別の名前にするとメモリが機能しないことがあります。

Node.jsの場合ChatOpenAIとプロンプトテンプレートを組み合わせて回答を生成します:

const { ChatOpenAI } = require("@langchain/openai");
const { ChatPromptTemplate } = require("@langchain/core/prompts");

const promptTemplate = ChatPromptTemplate.fromTemplate(
  `以下のドキュメントを参照して、質問に日本語で答えてください。

【参照ドキュメント】
{context}

【質問】
{question}

【回答】
回答は簡潔で、参照ドキュメントの内容に基づいてください。`
);

const model = new ChatOpenAI({
  openAIApiKey: process.env.OPENAI_API_KEY,
  modelName: "gpt-4o-mini", // 軽量で高速なモデル
  temperature: 0.3, // 低めに設定(より確定的な回答)
});

const chain = promptTemplate.pipe(model);

シンプルなメモリなしの実装が必要なら、Pythonでは従来のRetrievalQAも使えます:

from langchain.chains import RetrievalQA
qa_chain = RetrievalQA.from_chain_type(llm=llm, retriever=vectorstore.as_retriever())

LangChainは頻繁に更新されているため、インポートエラーが出た場合は公式ドキュメントまたはStackOverflowで最新のインポート方法を確認してください。

7. 質問を投げて回答を得る(所要時間:10分)

Pythonの場合

# ConversationalRetrievalChainを使う場合
query = '会社の新入社員向けトレーニング期間は何日ですか?'
result = qa_chain({"question": query})
print(result['answer'])
print(result['source_documents'])  # 参照されたドキュメントも確認できる

複数の質問を連続して実行すると、メモリに保存された会話履歴がコンテキストとして引き継がれます。

Node.jsの場合

async function queryRAG(userQuestion) {
  // 関連ドキュメントを検索
  const results = await vectorStore.similaritySearch(userQuestion, 3);
  const context = results.map((doc, i) => `[文書${i + 1}]\n${doc.pageContent}`).join("\n\n");

  // 回答を生成
  const response = await chain.invoke({
    context: context,
    question: userQuestion,
  });

  return {
    answer: response.content,
    sources: results.map((doc) => doc.metadata),
  };
}

つまずきやすいところ

APIキーの設定ミス

OpenAI APIキーが正しくセットされていないと、埋め込み処理も質問応答も動きません。キーは環境変数として設定するか、コード内で明示的に指定します。本番環境ではキーをソースコードに直接書かないよう注意してください。Node.jsの場合は.envファイルにOPENAI_API_KEY=sk-xxxxの形式で記述し、dotenvで読み込みます。

インポートエラー

LangChainは頻繁に更新されており、ライブラリの構造が変わることがあります。「ImportError: cannot import name ‘RetrievalQA’」というエラーが出た場合は、LangChainのバージョンを確認し、公式ドキュメントまたはStackOverflowで最新のインポート方法を調べてください。

メモリ機能がない・機能しない

基本的なRetrievalQAには、会話の履歴を覚える機能がありません。ユーザーが前の質問に基づいて「それについてもっと詳しく教えて」と聞いても、新しい質問として扱われてしまいます。この問題はConversationalRetrievalChainConversationBufferMemoryの組み合わせで解決できます。

メモリを設定しているのに機能しない場合は、memory_keyの名前が"chat_history"になっているか、return_messages=Trueが設定されているかを確認してください。長い会話でメモリが肥大化する場合は、ConversationSummaryMemory(履歴をLLMが自動要約)やConversationTokenBufferMemory(トークン数で管理)への切り替えを検討します。

コンテキストウィンドウの超過

チャット履歴・検索結果・システムプロンプトが累積してモデルの最大トークン数を超えると、エラーが発生します。放置すると次のような弊害が生じます:

  • エージェントの無限ループ化:エラー時の再試行ロジックが無限に動作し続ける
  • API使用量の爆増:トークン超過エラーの試行が繰り返されて無駄なコストが発生
  • ユーザー体験の悪化:タイムアウトやランタイムエラーでサービスが停止

対処方法としては、検索結果を5件程度に制限し、チャット履歴は直近のターン分のみ保持する(ConversationBufferWindowMemory)か、古い履歴を要約する(ConversationSummaryMemory)方法が有効です。Anthropic SDKの公式トークンカウント機能を使って実際の使用量を把握することも重要です:

from anthropic import Anthropic

def count_tokens_accurate(text: str) -> int:
    client = Anthropic()
    response = client.messages.count_tokens(
        model="claude-3-sonnet-20240229",
        messages=[{"role": "user", "content": text}]
    )
    return response.input_tokens

Node.jsでトークン数を制限する場合は、取得するドキュメント数を絞る方法が実用的です:

async function retrieveWithTokenLimit(query, topK = 3, maxTokens = 2000) {
  const results = await vectorStore.similaritySearch(query, topK);

  let totalTokens = 0;
  const filtered = [];

  for (const doc of results) {
    const docTokens = Math.ceil(doc.pageContent.length / 4); // 粗い推定
    if (totalTokens + docTokens > maxTokens) break;
    totalTokens += docTokens;
    filtered.push(doc);
  }

  return filtered;
}

FAISSのバイナリエラー(Node.js固有)

Node.jsバージョンとfaiss-nodeのビルド済みバイナリが一致していない場合に発生します。

rm -rf node_modules/faiss-node
npm install faiss-node --build-from-source

または、小規模用途であればメモリベクターストアに切り替えることも検討してください:

const { MemoryVectorStore } = require("langchain/vectorstores/memory");
const vectorStore = await MemoryVectorStore.fromDocuments(docs, embeddings);

カスタムプロンプトが反映されない

LangChainのチェーンは内部にデフォルトプロンプトを持っており、単にpromptパラメータを渡すだけでは機能しないことがあります。独自の指示を追加したい場合は、LLMChainStuffDocumentsChainを組み合わせて低レベルAPIで直接チェーンを構築する必要があります。

チャット履歴の不具合

会話形式のRAGシステムを作る場合、チャット履歴の管理がうまくいかないことがあります。回答が不正確になったり、文脈がぐちゃぐちゃになったりするのは、多くの場合メモリの設定か、プロンプト(生成AIへの指示文)の書き方の問題です。

評価の難しさ

RAGパイプラインが「ちゃんと動いているか」を測るのは思った以上に難しいです。回答の正確さ、資料の検索精度、ユーザー満足度など、複数の指標を組み合わせて判断する必要があります。グラフデータベース(Graph RAG)を使ったり、複数の指標を組み合わせたりする方法が業界で検討されています。

開発者向け:Claude APIでのRAG生成部分の実装

検索で取得したドキュメントをClaude Sonnetの長いコンテキストに渡して回答生成する例です。

import anthropic

client = anthropic.Anthropic()

def generate_answer_with_rag(question: str, retrieved_docs: list[str]) -> str:
    """検索結果をコンテキストに含めてClaude Sonnetで回答生成"""
    context = "\n\n---\n\n".join(retrieved_docs)

    prompt = f"""以下の参考資料だけを根拠に質問に回答してください。
資料に記載がない場合は「資料からは判断できません」と答えてください。

【参考資料】
{context}

【質問】
{question}"""

    response = client.messages.create(
        model="claude-sonnet-4-5",  # 200Kコンテキストで多めの検索結果も一度に処理可能
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}]
    )
    return response.content[0].text

# 使用例(ベクトルDBでの検索結果を想定)
retrieved = [
    "当社の返品ポリシー:購入後30日以内であれば返品可能です。",
    "送料について:1万円以上のご注文は送料無料です。"
]

answer = generate_answer_with_rag("返品はいつまでできますか?", retrieved)
print(answer)

Claude Sonnet 4.5の200Kトークンコンテキストを活かせば、検索結果を絞り込みすぎず、多めのドキュメントをそのままコンテキストに含めて回答精度を上げる設計も可能です。


慣れてきたら試したいこと

メモリ機能を追加・強化する

基本的なRAGでは毎回独立した質問として処理されますが、ConversationalRetrievalChainにメモリを追加することで会話形式にできます。過去の質問と回答を保持させることで、より自然な対話が可能になります。

長い会話ではメモリが肥大化するため、ConversationSummaryMemoryで要約保存する方法も有効です。ただし、要約処理にAPIコストがかかる点に注意してください:

from langchain.memory import ConversationSummaryMemory

summary_memory = ConversationSummaryMemory(
    llm=llm,
    memory_key="chat_history",
    return_messages=True
)

カスタムプロンプトを用意することで、生成AIにどんな役割を果たすか明確に指示することもできます。例えば「あなたは会社のサポート担当者です。ユーザーの質問には資料を参考に、丁寧に答えてください」というような指示を追加します。

メモリ戦略を用途に応じて選択する

LangChainが提供するメモリクラスはそれぞれ異なるコンテキスト管理戦略を取ります:

  • ConversationBufferWindowMemory:直近Nターンのみ保持(コンテキスト超過防止に最も効果的)
  • ConversationSummaryMemory:古い履歴は要約して保持(中程度の安全性)
  • ConversationEntityMemory:人物・組織・日付などのエンティティを抽出して保存

本番環境ではConversationBufferWindowMemoryで直近5ターン程度に絞るのがバランスの取れた選択です。

エージェントのループ防止設定を入れる

エージェントでRAGを利用する際は、再試行ロジックが無限ループになるのを防ぐため、最大反復回数を明示的に制限します:

from langchain.agents import initialize_agent, AgentType

agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
    max_iterations=5,
    early_stopping_method="force",  # 最大反復数に達したら強制終了
)

リトライ処理には指数バックオフ(試行ごとに待機時間を増やす)を入れて、上限回数(2〜3回程度)を設けるようにしてください。

検索パラメータを調整する

as_retriever()メソッドで検索の精度を細かく制御できます:

# 取得するドキュメント数を増やす
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# スコアに基づいてフィルタリング
retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"score_threshold": 0.7, "k": 10}
)

Node.jsの場合はスコア付き検索で同様のフィルタリングができます:

const results = await vectorStore.similaritySearchWithScore(query, 5);
const filtered = results
  .filter(([_, score]) => score >= 0.7)
  .map(([doc, score]) => ({ content: doc.pageContent, score }));

スコア閾値を設定することで、関連性の低いドキュメントを除外しつつ、不必要なトークン消費を抑えることができます。

検索結果のキャッシュで効率化する

同じクエリが繰り返される場合、検索結果をキャッシュすることでトークン消費とAPIコストを削減できます。Node.jsではnode-cacheなどを活用する方法が実用的です:

const NodeCache = require("node-cache");
const cache = new NodeCache({ stdTTL: 3600 }); // 1時間

async function queryRAGWithCache(question) {
  const cacheKey = question.toLowerCase();

  if (cache.has(cacheKey)) {
    return cache.get(cacheKey);
  }

  const result = await generateAnswerWithRAG(question);
  cache.set(cacheKey, result);

  return result;
}

クエリをハッシュ化してキャッシュキーとし、有効期限付きで保存する方法も実用的です。

トークンコストを把握・最適化する

OpenAI APIはトークン単位で課金されます。主な目安は以下の通りです:

  • text-embedding-3-small:1000トークンあたり$0.02
  • gpt-4o-mini:入力$0.15、出力$0.60(1Mトークンあたり)

大量のドキュメント処理の場合、バッチ処理でコストを削減できます:

async function batchEmbedWithRateLimit(texts, batchSize = 10, delayMs = 1000) {
  const results = [];

  for (let i = 0; i < texts.length; i += batchSize) {
    const batch = texts.slice(i, i + batchSize);
    const embedded = await embeddings.embedDocuments(batch);
    results.push(...embedded);

    if (i + batchSize < texts.length) {
      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }

  return results;
}

複数の情報源を統合する

最初は1つのテキストファイルやデータベースだけかもしれませんが、複数のシステムや資料から同時に情報を引き出すようにカスタマイズできます。PDFファイル・Webページ・データベースから自動的にドキュメントを抽出する仕組みも実現できます。

回答の評価を自動化する

生成される回答の質を自動で測定する仕組みを入れることで、パイプラインの改善箇所を見つけやすくなります。複数の評価方法を並行して走らせるアプローチもあります。

Node.jsでの完全実装

Pythonでの実装が一般的ですが、Node.js 18以上の環境でRAGパイプラインを完全に動かすことも可能です。JavaScriptを得意とする環境や、既存のWebアプリに組み込みたい場合、Node.jsによる実装は学習コストなしに導入できる有力な選択肢です。


あわせて読みたい

参考ソース