T-CREATOR

LangChain で幻覚が増える原因を切り分け:リトリーバ不整合/プロンプト漏れ/温度設定

LangChain で幻覚が増える原因を切り分け:リトリーバ不整合/プロンプト漏れ/温度設定

LangChain で RAG(Retrieval-Augmented Generation)を実装したのに、思ったより幻覚(Hallucination)が減らない……そんな経験はありませんか?実は、LangChain のパイプライン上には「幻覚を増やしてしまう落とし穴」がいくつも潜んでいます。

本記事では、LangChain における幻覚の主要な原因を リトリーバ不整合プロンプト漏れ温度設定 の 3 つに絞り、それぞれの発生メカニズムと具体的な検証・対策方法を解説します。これらを理解すれば、幻覚を大幅に減らし、信頼性の高い LLM アプリケーションを構築できるでしょう。

背景

LangChain と RAG の基本構造

LangChain は、LLM を活用したアプリケーション開発を効率化するフレームワークです。特に RAG パターンでは、以下のような流れで動作します。

mermaidflowchart LR
  query["ユーザークエリ"] -->|検索| retriever["Retriever<br/>(ベクトル検索)"]
  retriever -->|関連文書| context["Context 文書"]
  context -->|結合| prompt["Prompt Template"]
  query -->|質問| prompt
  prompt -->|生成| llm["LLM<br/>(GPT-4など)"]
  llm -->|回答| answer["最終回答"]

この図が示すように、RAG では Retriever がベクトルストアから関連文書を取得 し、それをプロンプトに埋め込んで LLM に渡します。この一連の流れが正しく機能すれば、LLM は外部知識を参照しながら正確な回答を生成できます。

しかし、各コンポーネントの設定ミスやデータ不整合があると、LLM が「知識を参照せず勝手に推測する」状態、すなわち幻覚が発生しやすくなるのです。

幻覚が発生しやすい 3 つのポイント

LangChain の RAG パイプラインにおいて、幻覚が増える主要な原因は以下の 3 つに集約されます。

#原因カテゴリ発生箇所主な影響
1リトリーバ不整合Retriever / ベクトルストア無関係な文書が取得され、LLM が誤った文脈で推測
2プロンプト漏れPrompt Templateコンテキストが LLM に渡らず、根拠なく回答
3温度設定LLM 設定高温度で創作的すぎる回答が生成される

これらは独立して発生する場合もあれば、複合的に重なることもあります。次章から、それぞれの原因を詳しく見ていきましょう。

課題

リトリーバ不整合:無関係な文書が取得される

問題の本質

Retriever は、クエリに対してベクトル類似度の高い文書を返します。しかし、埋め込みモデルとクエリの形式がミスマッチしていたり、チャンク分割が不適切だったりすると、意味的に無関係な文書が上位にランクインしてしまいます。

以下の図は、リトリーバ不整合が起きる典型的なパターンです。

mermaidflowchart TD
  query["クエリ: 「Next.jsの最新バージョンは?」"] -->|ベクトル化| embed["Embedding Model<br/>(text-embedding-ada-002)"]
  embed -->|類似検索| vectorstore["Vector Store<br/>(Pinecone/Chroma)"]
  vectorstore -->|Top-K取得| doc1["Doc 1: React 18の機能"]
  vectorstore -->|Top-K取得| doc2["Doc 2: Vercel の料金"]
  vectorstore -->|Top-K取得| doc3["Doc 3: Next.js 13 リリースノート"]
  doc1 & doc2 & doc3 -->|統合| context_bad["Context<br/>(無関係な文書が混入)"]
  context_bad -->|プロンプト化| llm_bad["LLM"]
  llm_bad -->|幻覚回答| answer_bad["「Next.jsの最新は14.2です」<br/>(根拠なし)"]

この例では、クエリは「最新バージョン」を尋ねているのに、Retriever が「React 18」や「料金」といった無関係な文書を返しています。LLM はこの不完全なコンテキストから推測し、存在しないバージョン番号を生成してしまうのです。

不整合が起きる主なケース

#不整合パターン具体例結果
1埋め込みモデルの言語ミスマッチ英語モデルで日本語クエリを検索意味的に無関係な文書が上位
2チャンク分割が大きすぎる1 文書 = 5000 トークン複数トピックが混在し、関連度が低下
3メタデータフィルタ未使用古い文書と新しい文書が混在時系列が狂った情報を取得
4Top-K が少なすぎるk=1 で取得唯一の文書が外れた場合、幻覚が激増

プロンプト漏れ:コンテキストが LLM に渡らない

問題の本質

Retriever が正しく文書を取得しても、Prompt Template の設計ミスによってコンテキストが LLM に渡らないケースがあります。これを「プロンプト漏れ」と呼びます。

典型的なパターンは以下の 2 つです。

  1. 変数名の typo や未定義{context} を埋め込むべき箇所に {content} と書いてしまう
  2. テンプレート構造のミス:条件分岐で context が空文字列になるパス

以下は、プロンプト漏れが発生する処理フローです。

mermaidflowchart TD
  retriever["Retriever"] -->|取得成功| docs["関連文書 3 件"]
  docs -->|結合処理| join["join_documents()"]
  join -->|文字列化| context_str["context 変数<br/>「Doc1: ...\\nDoc2: ...」"]
  context_str -->|テンプレート適用| template["PromptTemplate<br/>「Answer: {content}」<br/>(typo!)"]
  template -->|変数未定義| prompt_final["最終プロンプト<br/>「Answer: {content}」<br/>(context が消失)"]
  prompt_final -->|送信| llm["LLM"]
  llm -->|根拠なし| hallucination["幻覚回答"]

この図では、Retriever が正常に動作しているにもかかわらず、Prompt Template で {context} ではなく {content} と typo したため、最終的に LLM にコンテキストが渡らず、幻覚が発生しています。

プロンプト漏れの主なパターン

#パターン原因検出方法
1変数名 typo{context}{content}テンプレート文字列を検索
2条件分岐で空文字列if docs else "" で context が消失ログ出力で確認
3フォーマット関数のバグカスタム関数内で context を返し忘れユニットテスト
4複数テンプレートの混在Chain 内で異なるテンプレートが上書きデバッグプリント

温度設定:LLM が創作的すぎる回答を生成

問題の本質

LLM の temperature(温度) パラメータは、生成テキストのランダム性を制御します。温度が高いほど創作的・多様な回答が生成されますが、事実ベースの RAG では高温度が幻覚を増やす原因になります。

温度範囲挙動RAG での適性
0.0 - 0.3決定的・一貫性高い★★★ 推奨
0.4 - 0.7バランス型★★☆ 条件付き可
0.8 - 1.0創作的・多様★☆☆ 非推奨
1.1 以上非常にランダム☆☆☆ 幻覚が激増

温度が高いと、LLM は「コンテキストに書かれていない情報」を補完しようとし、結果的に存在しない事実を生成してしまいます。

温度設定ミスの典型例

以下は、温度設定が幻覚に与える影響を示した比較です。

温度 0.0(推奨):コンテキストに忠実な回答

plaintextQ: Next.js 13 の主な新機能は?
Context: "Next.js 13 では App Router が導入され、React Server Components がサポートされました。"

A: Next.js 13 の主な新機能は App Router と React Server Components です。

温度 1.2(非推奨):創作的な幻覚

plaintextQ: Next.js 13 の主な新機能は?
Context: "Next.js 13 では App Router が導入され、React Server Components がサポートされました。"

A: Next.js 13 では App Router、React Server Components、さらに自動画像最適化 2.0 や新しい CSS モジュールシステムが追加されました。
(※ 後半は幻覚)

温度を上げると、LLM は「それっぽい情報」を補完しようとし、実際には存在しない機能を追加してしまうのです。

解決策

リトリーバ不整合の対策

埋め込みモデルとクエリの言語統一

日本語クエリを扱う場合、日本語対応の埋め込みモデルを使用しましょう。

typescriptimport { OpenAIEmbeddings } from '@langchain/openai';

// 日本語対応の埋め込みモデルを使用
const embeddings = new OpenAIEmbeddings({
  modelName: 'text-embedding-3-small', // 多言語対応
  dimensions: 1536,
});

OpenAI の text-embedding-3-smalltext-embedding-3-large は多言語対応で、日本語クエリでも高精度な検索が可能です。

チャンク分割の最適化

大きすぎるチャンクは複数トピックが混在し、小さすぎると文脈が失われます。500〜1000 トークン、オーバーラップ 50〜100 トークンが目安です。

typescriptimport { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';

// 適切なチャンクサイズでドキュメントを分割
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 800, // トークン数(約 600 文字)
  chunkOverlap: 100, // 前後の文脈を保持
  separators: ['\n\n', '\n', '。', '、', ' ', ''],
});

const docs = await splitter.splitDocuments(rawDocuments);

日本語では句点()や改行(\n)を区切りに使うと自然な分割ができます。

メタデータフィルタの活用

ベクトル検索に加えて、メタデータ(日付・カテゴリなど)でフィルタすると精度が向上します。

typescriptimport { PineconeStore } from '@langchain/pinecone';

// Pinecone ストアから最新文書のみを検索
const retriever = vectorStore.asRetriever({
  k: 5,
  filter: {
    category: 'nextjs',
    publishedAt: { $gte: '2024-01-01' }, // 2024年以降のみ
  },
});

これにより、古い情報や無関係なカテゴリが除外され、幻覚のリスクが減ります。

Top-K の調整とリランキング

k=3〜5 を基本とし、必要に応じて Reranker(再ランキング) を導入します。

typescriptimport { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression';
import { CohereRerank } from '@langchain/cohere';

// Cohere Rerank で上位結果を再評価
const reranker = new CohereRerank({
  apiKey: process.env.COHERE_API_KEY,
  topN: 3, // 最終的に 3 件に絞る
});

const compressor = new ContextualCompressionRetriever({
  baseRetriever: retriever,
  baseCompressor: reranker,
});

const relevantDocs = await compressor.getRelevantDocuments(
  'Next.js の最新バージョンは?'
);

Reranker を使うと、ベクトル検索の結果をさらに精査し、真に関連性の高い文書だけを LLM に渡せます。

プロンプト漏れの対策

テンプレート変数の検証

Prompt Template 内で 使用する変数がすべて定義されているか を確認しましょう。

typescriptimport { PromptTemplate } from '@langchain/core/prompts';

// 正しいテンプレート定義
const template = PromptTemplate.fromTemplate(`
以下のコンテキストを参照して質問に回答してください。

コンテキスト:
{context}

質問: {question}

回答:`);

// 変数の確認
console.log(template.inputVariables); // ['context', 'question']

inputVariables プロパティで使用変数を確認できます。typo があればここで検出できます。

コンテキスト結合の明示的処理

Retriever の出力を Prompt Template に渡す際、明示的に結合処理を行いましょう。

typescriptimport { RunnableSequence } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';

// ドキュメントを文字列に結合する関数
const formatDocs = (docs: Document[]) => {
  return docs
    .map((doc, i) => `[文書${i + 1}]\n${doc.pageContent}`)
    .join('\n\n');
};

// Chain の構築
const chain = RunnableSequence.from([
  {
    context: retriever.pipe(formatDocs), // 明示的に結合
    question: (input) => input.question,
  },
  template,
  llm,
  new StringOutputParser(),
]);

formatDocs 関数で各文書に番号を付け、改行で結合しています。これにより、コンテキストが確実に LLM に渡ります。

デバッグログの挿入

本番環境では難しいですが、開発時は 中間変数をログ出力 して、コンテキストが正しく渡っているか確認しましょう。

typescriptimport { RunnableLambda } from '@langchain/core/runnables';

// デバッグ用の Lambda を挿入
const debugLogger = new RunnableLambda({
  func: (input) => {
    console.log('=== Debug: Prompt Input ===');
    console.log(
      'Context:',
      input.context?.substring(0, 200)
    ); // 冒頭 200 文字
    console.log('Question:', input.question);
    return input;
  },
});

const chain = RunnableSequence.from([
  {
    context: retriever.pipe(formatDocs),
    question: (input) => input.question,
  },
  debugLogger, // ← デバッグ用
  template,
  llm,
  new StringOutputParser(),
]);

この方法で、Prompt Template に渡される直前のデータを確認できます。

ユニットテストの導入

Prompt Template のレンダリング結果を Jest などでテスト しましょう。

typescriptimport { PromptTemplate } from '@langchain/core/prompts';

describe('PromptTemplate', () => {
  it('should include context and question', async () => {
    const template = PromptTemplate.fromTemplate(`
コンテキスト: {context}
質問: {question}
`);

    const result = await template.format({
      context: 'Next.js 14 がリリースされました。',
      question: '最新バージョンは?',
    });

    expect(result).toContain('Next.js 14');
    expect(result).toContain('最新バージョンは?');
  });
});

このテストにより、テンプレートが期待通りに変数を展開しているかを自動検証できます。

温度設定の最適化

RAG では温度 0.0〜0.3 を推奨

事実ベースの回答が求められる RAG では、温度を低く設定しましょう。

typescriptimport { ChatOpenAI } from '@langchain/openai';

// 温度を 0.0 に設定(決定的な回答)
const llm = new ChatOpenAI({
  modelName: 'gpt-4-turbo-preview',
  temperature: 0.0, // 幻覚を最小化
  maxTokens: 500,
});

温度 0.0 では、同じ入力に対して常に同じ出力が得られ、幻覚のリスクが大幅に減ります。

ユースケース別の温度設定

ユースケース推奨温度理由
FAQ・ドキュメント検索0.0 - 0.1事実を正確に返す必要がある
技術サポート0.1 - 0.3多少の言い回し変化は許容
要約・翻訳0.2 - 0.4表現の多様性が必要
ブレインストーミング0.7 - 1.0創作性が求められる

RAG で幻覚を避けたい場合、0.3 以下が安全圏です。

温度とトップ P(top_p)の併用

temperature だけでなく、top_p(nucleus sampling) も併用すると、より安定した出力が得られます。

typescriptconst llm = new ChatOpenAI({
  modelName: 'gpt-4-turbo-preview',
  temperature: 0.2,
  topP: 0.9, // 上位 90% の確率トークンから選択
  maxTokens: 500,
});

top_p=0.9 では、累積確率が 90% になるまでのトークンのみを候補とするため、極端に低確率な(幻覚的な)トークンが排除されます。

動的な温度調整

クエリの種類に応じて、実行時に温度を変更することも可能です。

typescriptconst answerQuestion = async (
  question: string,
  isFact: boolean
) => {
  const llm = new ChatOpenAI({
    modelName: 'gpt-4-turbo-preview',
    temperature: isFact ? 0.0 : 0.7, // 事実系は低温度
  });

  const chain = RunnableSequence.from([
    {
      context: retriever.pipe(formatDocs),
      question: () => question,
    },
    template,
    llm,
    new StringOutputParser(),
  ]);

  return await chain.invoke({ question });
};

事実確認系のクエリには temperature=0.0、創作系には 0.7 を使い分けられます。

具体例

統合デバッグ:3 つの原因を一度に確認

ここでは、リトリーバ不整合プロンプト漏れ温度設定を一括で検証する TypeScript コードを示します。

プロジェクトセットアップ

まず、必要なパッケージをインストールします。

bashyarn add langchain @langchain/openai @langchain/pinecone pinecone-client dotenv
yarn add -D @types/node typescript ts-node

環境変数の設定

.env ファイルに API キーを記載します。

plaintextOPENAI_API_KEY=sk-xxxxxxxxxxxxxx
PINECONE_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PINECONE_ENVIRONMENT=us-east-1
PINECONE_INDEX_NAME=langchain-demo

Retriever のセットアップと検証

Pinecone を使ったベクトルストアを構築し、取得文書を検証します。

typescriptimport { OpenAIEmbeddings } from '@langchain/openai';
import { PineconeStore } from '@langchain/pinecone';
import { Pinecone } from '@pinecone-database/pinecone';
import dotenv from 'dotenv';

dotenv.config();

// Pinecone クライアントの初期化
const pinecone = new Pinecone({
  apiKey: process.env.PINECONE_API_KEY!,
});

const index = pinecone.Index(
  process.env.PINECONE_INDEX_NAME!
);

次に、埋め込みモデルを設定します。

typescript// 日本語対応の埋め込みモデル
const embeddings = new OpenAIEmbeddings({
  modelName: 'text-embedding-3-small',
});

ベクトルストアを作成し、Retriever を構築します。

typescript// ベクトルストアの作成
const vectorStore = await PineconeStore.fromExistingIndex(
  embeddings,
  {
    pineconeIndex: index,
    namespace: 'nextjs-docs', // 名前空間でフィルタ
  }
);

// Retriever の設定
const retriever = vectorStore.asRetriever({
  k: 5,
  filter: {
    category: 'nextjs',
    version: { $gte: '13.0' }, // バージョン 13 以上
  },
});

取得した文書を検証する関数を作成します。

typescript// 取得文書の検証関数
const validateRetrieval = async (query: string) => {
  console.log(`\n=== クエリ: ${query} ===`);

  const docs = await retriever.getRelevantDocuments(query);

  console.log(`取得文書数: ${docs.length}`);

  docs.forEach((doc, i) => {
    console.log(`\n[文書${i + 1}]`);
    console.log(
      `内容: ${doc.pageContent.substring(0, 150)}...`
    );
    console.log(`メタデータ:`, doc.metadata);
  });

  return docs;
};

この関数を実行すると、Retriever が 無関係な文書を取得していないかを確認できます。

Prompt Template の検証

次に、Prompt Template が正しく変数を展開しているかを確認します。

typescriptimport { PromptTemplate } from '@langchain/core/prompts';
import { Document } from '@langchain/core/documents';

// ドキュメント結合関数
const formatDocs = (docs: Document[]) => {
  return docs
    .map((doc, i) => `[文書${i + 1}]\n${doc.pageContent}`)
    .join('\n\n');
};

// Prompt Template の定義
const template = PromptTemplate.fromTemplate(`
あなたは技術サポート担当です。以下のコンテキストを参照して、質問に正確に回答してください。
コンテキストに情報がない場合は「情報がありません」と答えてください。

コンテキスト:
{context}

質問: {question}

回答:`);

テンプレートの変数を確認します。

typescript// 変数の確認
console.log('テンプレート変数:', template.inputVariables);
// => ['context', 'question']

実際にフォーマットして、変数が正しく展開されるかテストします。

typescript// フォーマットテスト
const testFormat = async () => {
  const testDocs = [
    new Document({
      pageContent:
        'Next.js 14 が 2024 年 1 月にリリースされました。',
    }),
  ];

  const formatted = await template.format({
    context: formatDocs(testDocs),
    question: 'Next.js の最新バージョンは?',
  });

  console.log('\n=== フォーマット結果 ===');
  console.log(formatted);

  // 期待する文字列が含まれているか確認
  if (formatted.includes('Next.js 14')) {
    console.log('✓ コンテキストが正しく展開されています');
  } else {
    console.error('✗ コンテキストが漏れています');
  }
};

await testFormat();

これにより、プロンプト漏れを事前に検出できます。

温度設定の比較実験

同じクエリに対して、異なる温度設定で複数回実行し、幻覚の発生率を比較します。

typescriptimport { ChatOpenAI } from '@langchain/openai';
import { RunnableSequence } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';

// 温度別に LLM を実行
const compareTemperatures = async (question: string) => {
  const temperatures = [0.0, 0.5, 1.0];

  for (const temp of temperatures) {
    console.log(`\n=== 温度: ${temp} ===`);

    const llm = new ChatOpenAI({
      modelName: 'gpt-4-turbo-preview',
      temperature: temp,
      maxTokens: 300,
    });

    const chain = RunnableSequence.from([
      {
        context: retriever.pipe(formatDocs),
        question: () => question,
      },
      template,
      llm,
      new StringOutputParser(),
    ]);

    const result = await chain.invoke({ question });
    console.log(result);
  }
};

実行例を示します。

typescriptawait compareTemperatures(
  'Next.js 14 の主な新機能は何ですか?'
);

出力例(温度 0.0)

plaintext=== 温度: 0.0 ===
Next.js 14 の主な新機能は、Turbopack の安定化、Server Actions の正式サポート、Partial Prerendering のプレビューです。

出力例(温度 1.0)

plaintext=== 温度: 1.0 ===
Next.js 14 では Turbopack、Server Actions、Partial Prerendering に加えて、新しい画像最適化エンジンや CSS-in-JS のパフォーマンス改善が含まれています。
(※ 後半は幻覚の可能性)

温度が高いと、コンテキストに書かれていない情報(画像最適化エンジンなど)が追加されることがわかります。

統合実行スクリプト

最後に、すべての検証を一括実行するスクリプトを作成します。

typescript// main.ts
const main = async () => {
  console.log('=== LangChain 幻覚デバッグツール ===\n');

  // 1. Retriever の検証
  console.log('【ステップ 1】 Retriever 検証');
  await validateRetrieval('Next.js 14 の新機能');

  // 2. Prompt Template の検証
  console.log('\n【ステップ 2】 Prompt Template 検証');
  await testFormat();

  // 3. 温度設定の比較
  console.log('\n【ステップ 3】 温度設定比較');
  await compareTemperatures(
    'Next.js 14 の主な新機能は何ですか?'
  );

  console.log('\n=== デバッグ完了 ===');
};

main().catch(console.error);

実行方法は以下の通りです。

bashyarn ts-node main.ts

このスクリプトを実行すると、リトリーバ不整合プロンプト漏れ温度設定の 3 つを一度に検証でき、幻覚の原因を特定できます。

図で理解できる要点

  • Retriever 検証:取得文書の内容とメタデータをログ出力し、無関係な文書が混入していないか確認
  • Prompt Template 検証:変数の展開結果を目視確認し、コンテキストが漏れていないかチェック
  • 温度比較:同じクエリで複数温度を試し、幻覚の発生率を定量的に比較

まとめ

LangChain で RAG を実装しても幻覚が減らない場合、その原因は リトリーバ不整合プロンプト漏れ温度設定 のいずれか(または複数)にあることがほとんどです。

本記事で紹介した検証方法を使えば、以下のことが可能になります。

#対策内容効果
1埋め込みモデルとチャンク分割の最適化無関係な文書の混入を防ぐ
2Prompt Template の変数検証コンテキスト漏れを事前に検出
3温度設定の最適化(0.0〜0.3)幻覚的な補完を抑制
4メタデータフィルタと Reranker の導入検索精度を大幅向上
5デバッグログとユニットテストの活用問題箇所を迅速に特定

これらを組み合わせることで、LangChain の RAG パイプラインを大幅に改善し、信頼性の高い LLM アプリケーションを構築できるでしょう。幻覚に悩んでいる方は、ぜひ本記事の手法を試してみてください。

関連リンク