T-CREATOR

LlamaIndex トラブルシュート大全:検索ヒットしない・幻覚が増える時の対処

LlamaIndex トラブルシュート大全:検索ヒットしない・幻覚が増える時の対処

LlamaIndex を使って検索拡張生成(RAG)を実装したものの、「検索が全然ヒットしない」「関係ない情報を返してくる」「AI が勝手に作り話を始める」といった問題に直面していませんか?

RAG システムは理論上は素晴らしい仕組みですが、実際の運用では様々な落とし穴があります。本記事では、LlamaIndex を使った開発で頻出するトラブルとその解決策を、実践的なコード例とともに徹底解説します。エラーコードの読み解き方から、パラメータチューニングの具体的な数値まで、現場で即使える知識をお届けしますね。

背景

RAG システムの基本構造

LlamaIndex は、大規模言語モデル(LLM)に外部知識を効率的に与えるためのフレームワークです。文書を細かく分割し、ベクトル化して検索可能にすることで、LLM の回答精度を大幅に向上させられます。

以下の図は、LlamaIndex を使った RAG システムの基本的なデータフローを示しています。

mermaidflowchart LR
  user["ユーザー"] -->|質問| query["Query Engine"]
  query -->|検索| index["Vector Index"]
  index -->|類似文書| retriever["Retriever"]
  retriever -->|コンテキスト| llm["LLM<br/>(GPT-4など)"]
  llm -->|回答生成| query
  query -->|回答| user

  docs["文書"] -->|分割| chunks["Chunks"]
  chunks -->|埋め込み| embed["Embedding<br/>Model"]
  embed -->|ベクトル| index

このフローでは、文書がチャンク(小さな断片)に分割され、埋め込みモデルによってベクトル化されます。ユーザーの質問も同様にベクトル化され、類似度の高い文書が検索されて LLM に渡される仕組みです。

LlamaIndex が解決する課題

従来の LLM には、以下のような制限がありました。

#制限影響
1学習データの鮮度最新情報に対応できない
2コンテキスト長の制限大量の文書を一度に処理できない
3知識の幻覚存在しない情報を生成してしまう
4企業固有知識の欠如社内文書やドメイン知識を活用できない

LlamaIndex はこれらの課題を解決しますが、適切に設定しないと新たな問題が発生します。

課題

検索ヒットしない問題の典型パターン

LlamaIndex を導入したのに検索が機能しない、というケースは非常に多く報告されています。主な原因は以下の通りです。

mermaidflowchart TD
  start["検索ヒットしない"] --> chunk["チャンク設定<br/>の問題"]
  start --> embed["埋め込み<br/>モデルの問題"]
  start --> retrieve["検索設定<br/>の問題"]

  chunk --> chunk1["サイズが大きすぎる"]
  chunk --> chunk2["オーバーラップ不足"]
  chunk --> chunk3["区切り位置が不適切"]

  embed --> embed1["言語が不一致"]
  embed --> embed2["モデルの次元不足"]

  retrieve --> retrieve1["類似度閾値が高すぎる"]
  retrieve --> retrieve2["取得件数が少なすぎる"]

この図から分かるように、検索失敗の原因は大きく 3 つのカテゴリに分類できます。それぞれに適切な対処が必要ですね。

幻覚が増える問題の構造

RAG を導入したのに、かえって幻覚(Hallucination)が増えてしまうケースもあります。これは以下のような構造的な問題が原因です。

#原因具体的な症状
1検索精度の低さ関連性の低い文書を取得
2コンテキストの過剰LLM が情報を混同
3プロンプト設計の不備検索結果を無視して回答
4ノイズデータの混入誤情報を正しいと判断
5チャンクの文脈欠如断片的な情報から誤推論

特に注意すべきは、検索結果が多すぎると、LLM が重要な情報を見落としてしまう点です。適切なバランスが求められます。

実際のエラーケース

実務で遭遇する典型的なエラーを見ていきましょう。

エラーコード: ValueError: empty vocabulary; perhaps the documents only contain stop words

typescript// このコードは失敗する例
import { VectorStoreIndex, Document } from 'llamaindex';

const documents = [
  new Document({ text: 'a the an' }), // ストップワードのみ
];

const index = VectorStoreIndex.fromDocuments(documents);
// ValueError が発生

このエラーは、文書がストップワード(一般的すぎて意味を持たない単語)のみで構成されている場合に発生します。

エラーコード: IndexError: list index out of range (検索結果 0 件)

typescript// 検索がヒットしない例
const queryEngine = index.asQueryEngine({
  similarityTopK: 3,
  similarityCutoff: 0.9, // 閾値が高すぎる
});

const response = await queryEngine.query('製品の使い方');
// 結果が0件でエラー

類似度の閾値を 0.9 に設定すると、ほぼ完全一致しか取得できず、実用的な検索ができません。

解決策

チャンク設定の最適化

検索精度を上げる第一歩は、適切なチャンク設定です。文書をどのように分割するかで、検索の品質が大きく変わります。

チャンクサイズの設定

まず、基本的なチャンク設定から見ていきましょう。

typescriptimport {
  SimpleDirectoryReader,
  VectorStoreIndex,
  ServiceContext,
  OpenAIEmbedding,
} from 'llamaindex';

次に、サービスコンテキストを設定します。

typescript// 推奨されるチャンク設定
const serviceContext = ServiceContext.fromDefaults({
  chunkSize: 512, // 1チャンクのトークン数
  chunkOverlap: 50, // 前後のチャンクとの重複
  embedModel: new OpenAIEmbedding({
    modelName: 'text-embedding-3-small',
  }),
});

チャンクサイズは、文書の性質によって調整が必要です。以下の表を参考にしてください。

#文書タイプ推奨チャンクサイズ理由
1技術ドキュメント512-1024文脈を保持しつつ検索精度を確保
2Q&A・FAQ256-5121 つの質問と回答がまとまる
3長文記事・論文1024-2048段落単位で意味が完結
4コードスニペット128-256関数単位で分割
5チャット履歴256-512会話の流れを保持

文書の読み込みとインデックス作成

設定したサービスコンテキストを使って、文書を読み込みます。

typescript// 文書の読み込み
const reader = new SimpleDirectoryReader();
const documents = await reader.loadData({
  directoryPath: './docs',
});

インデックスを作成する際に、先ほどのサービスコンテキストを適用します。

typescript// インデックス作成
const index = await VectorStoreIndex.fromDocuments(
  documents,
  { serviceContext }
);

これで、最適化されたチャンク設定でインデックスが構築されます。チャンクオーバーラップ(chunkOverlap: 50)により、文脈が途切れる問題も軽減されますね。

埋め込みモデルの選択と設定

埋め込みモデルの選択は、検索精度に直結する重要な要素です。

言語特化モデルの使用

日本語文書には、多言語対応またはアジア言語に強いモデルを選びましょう。

typescriptimport { HuggingFaceEmbedding } from 'llamaindex';

// 日本語に強い埋め込みモデル
const embedModel = new HuggingFaceEmbedding({
  modelName: 'intfloat/multilingual-e5-large',
  // または日本語特化モデル
  // modelName: "cl-tohoku/bert-base-japanese-v3"
});

サービスコンテキストに適用します。

typescriptconst serviceContext = ServiceContext.fromDefaults({
  chunkSize: 512,
  chunkOverlap: 50,
  embedModel: embedModel,
});

OpenAI の最新モデルを使う場合は、次元数の指定も重要です。

typescriptimport { OpenAIEmbedding } from 'llamaindex';

// OpenAI の最新埋め込みモデル
const embedModel = new OpenAIEmbedding({
  modelName: 'text-embedding-3-large',
  dimensions: 1536, // より高精度な検索が可能
});

モデル選択の比較表です。

#モデル次元数日本語対応コスト推奨用途
1text-embedding-3-small1536★★★汎用的な検索
2text-embedding-3-large3072★★★★高精度が必要な場合
3multilingual-e5-large1024★★★★★無料日本語メイン
4bert-base-japanese768★★★★★無料日本語専用

検索パラメータのチューニング

検索エンジンのパラメータを適切に設定することで、検索精度と幻覚の抑制を両立できます。

基本的な検索設定

クエリエンジンの作成時に、検索パラメータを指定します。

typescript// 検索エンジンの作成
const queryEngine = index.asQueryEngine({
  similarityTopK: 5, // 取得する文書数
  similarityCutoff: 0.7, // 類似度の最低閾値
  responseSynthesizer: 'compact', // 応答生成モード
});

パラメータの意味と推奨値を整理しましょう。

typescript// パラメータの詳細設定
const queryEngine = index.asQueryEngine({
  // 上位K件の文書を取得(多すぎるとノイズが増える)
  similarityTopK: 5,

  // 類似度スコアの最低値(0.0-1.0)
  // 高すぎると検索ヒット0、低すぎると無関係な文書を取得
  similarityCutoff: 0.7,

  // 応答生成の方法
  // "compact": コンテキストを圧縮(推奨)
  // "tree_summarize": 階層的に要約
  // "refine": 段階的に精緻化
  responseSynthesizer: 'compact',
});

実行して結果を確認します。

typescriptconst response = await queryEngine.query(
  'LlamaIndexで検索精度を上げる方法'
);

console.log(response.toString());

similarityTopK の最適化

取得する文書数(similarityTopK)は、質問の性質によって調整が必要です。

#質問タイプ推奨 TopK理由
1具体的な事実確認3-5ピンポイントな情報が必要
2概念の説明5-10多角的な情報が有用
3比較・分析8-15複数の視点が必要
4トラブルシューティング5-10関連する複数の解決策

動的に調整する実装例です。

typescriptfunction getOptimalTopK(query: string): number {
  // 質問の長さで判断(簡易版)
  if (query.length < 20) {
    return 3; // 短い質問は具体的な回答を期待
  } else if (query.length < 50) {
    return 5; // 標準的な質問
  } else {
    return 10; // 長い質問は包括的な情報を求めている
  }
}

この関数を使って、クエリエンジンを動的に設定します。

typescriptconst userQuery = 'LlamaIndexで検索精度を上げる方法';
const topK = getOptimalTopK(userQuery);

const queryEngine = index.asQueryEngine({
  similarityTopK: topK,
  similarityCutoff: 0.7,
});

プロンプトエンジニアリングによる幻覚抑制

検索結果を正しく活用するには、LLM への指示(プロンプト)も重要です。

システムプロンプトのカスタマイズ

デフォルトのプロンプトを上書きして、幻覚を抑制します。

typescriptimport { PromptTemplate } from 'llamaindex';

// 幻覚抑制のためのカスタムプロンプト
const customPrompt = new PromptTemplate({
  template: `あなたは正確な情報提供を重視するアシスタントです。

以下のコンテキスト情報のみを使用して、質問に回答してください。
コンテキストに情報がない場合は、「提供された情報では回答できません」と正直に答えてください。
推測や一般知識での回答は避けてください。

コンテキスト:
{context_str}

質問: {query_str}

回答:`,
});

このプロンプトをクエリエンジンに適用します。

typescriptconst queryEngine = index.asQueryEngine({
  similarityTopK: 5,
  similarityCutoff: 0.7,
  textQATemplate: customPrompt,
});

実行例です。

typescriptconst response = await queryEngine.query(
  '2024年の新機能について教えて'
);

// コンテキストに情報がなければ、
// 「提供された情報では回答できません」と返る
console.log(response.toString());

段階的な検証プロンプト

複雑な質問には、段階的に検証するプロンプトが有効です。

typescriptconst verificationPrompt = new PromptTemplate({
  template: `ステップ1: 提供されたコンテキストを確認
ステップ2: 質問に関連する情報があるか判断
ステップ3: 情報がある場合のみ、その情報を元に回答

コンテキスト:
{context_str}

質問: {query_str}

ステップごとに考えて、最終的な回答を生成してください:`,
});

このアプローチにより、LLM が慎重に情報を検証するようになります。

メタデータフィルタリングの活用

検索精度をさらに高めるには、メタデータを活用したフィルタリングが効果的です。

メタデータの付与

文書作成時にメタデータを追加します。

typescriptimport { Document } from 'llamaindex';

const documents = [
  new Document({
    text: 'LlamaIndex 0.9.0 では新しい検索機能が追加されました',
    metadata: {
      version: '0.9.0',
      category: 'release_notes',
      date: '2024-01-15',
      language: 'ja',
    },
  }),
];

複数の文書を作成する場合の例です。

typescriptconst documents = [
  new Document({
    text: 'チャンクサイズは512トークンを推奨します',
    metadata: {
      category: 'best_practices',
      topic: 'chunking',
      difficulty: 'intermediate',
    },
  }),
  new Document({
    text: '埋め込みモデルの選択が検索精度に影響します',
    metadata: {
      category: 'best_practices',
      topic: 'embedding',
      difficulty: 'advanced',
    },
  }),
];

メタデータを使った検索

メタデータフィルタを適用した検索を行います。

typescriptimport {
  MetadataFilters,
  MetadataFilter,
} from 'llamaindex';

// カテゴリで絞り込み
const filters = new MetadataFilters({
  filters: [
    new MetadataFilter({
      key: 'category',
      value: 'best_practices',
      operator: '==',
    }),
  ],
});

クエリエンジンにフィルタを適用します。

typescriptconst queryEngine = index.asQueryEngine({
  similarityTopK: 5,
  filters: filters,
});

const response = await queryEngine.query(
  '検索精度を上げるには?'
);
// best_practices カテゴリの文書のみから検索

複数条件のフィルタリングも可能です。

typescript// 複数条件でフィルタリング
const complexFilters = new MetadataFilters({
  filters: [
    new MetadataFilter({
      key: 'category',
      value: 'best_practices',
      operator: '==',
    }),
    new MetadataFilter({
      key: 'difficulty',
      value: 'advanced',
      operator: '!=', // 上級者向けを除外
    }),
  ],
  condition: 'and', // すべての条件を満たす
});

メタデータフィルタの演算子一覧です。

#演算子意味使用例
1==等しいcategory == "tutorial"
2!=等しくないstatus != "deprecated"
3>より大きいversion > "1.0.0"
4<より小さいdate < "2024-01-01"
5>=以上priority >= 5
6<=以下difficulty <= "intermediate"

リランキングによる検索精度向上

検索結果を再評価して、より関連性の高い文書を上位に持ってくる手法です。

リランカーの設定

まず、リランカーモデルを導入します。

typescriptimport { CohereRerank, VectorStoreIndex } from 'llamaindex';

// Cohereのリランカーを使用
const reranker = new CohereRerank({
  apiKey: process.env.COHERE_API_KEY,
  topN: 5, // 最終的に使用する文書数
});

ポストプロセッサとして設定します。

typescriptconst queryEngine = index.asQueryEngine({
  similarityTopK: 20, // 多めに取得
  nodePostprocessors: [reranker], // リランキング
});

この設定により、まず 20 件の候補を取得し、その中から本当に関連性の高い 5 件に絞り込まれます。

カスタムリランカーの実装

外部 API を使わず、独自のロジックでリランキングすることも可能です。

typescriptimport { NodePostprocessor } from 'llamaindex';

class CustomReranker implements NodePostprocessor {
  async postprocessNodes(nodes: any[], query: string) {
    // カスタムスコアリングロジック
    return nodes
      .map((node) => ({
        ...node,
        // メタデータを考慮したスコア調整
        score: this.calculateScore(node, query),
      }))
      .sort((a, b) => b.score - a.score)
      .slice(0, 5); // 上位5件
  }

  private calculateScore(node: any, query: string): number {
    let score = node.score || 0;

    // 日付が新しいほど高スコア
    if (node.metadata.date) {
      const daysSinceUpdate = this.getDaysSince(
        node.metadata.date
      );
      score += Math.max(0, 1 - daysSinceUpdate / 365) * 0.2;
    }

    // カテゴリの重み付け
    if (node.metadata.category === 'official_docs') {
      score += 0.1;
    }

    return score;
  }

  private getDaysSince(dateString: string): number {
    const date = new Date(dateString);
    const now = new Date();
    return (
      (now.getTime() - date.getTime()) /
      (1000 * 60 * 60 * 24)
    );
  }
}

カスタムリランカーを適用します。

typescriptconst customReranker = new CustomReranker();

const queryEngine = index.asQueryEngine({
  similarityTopK: 15,
  nodePostprocessors: [customReranker],
});

具体例

ケーススタディ 1: 技術文書検索の改善

実際のプロジェクトで検索がヒットしなかった事例と、その解決プロセスを見ていきましょう。

問題の発生状況

以下のような設定で、製品マニュアルの検索システムを構築していました。

typescript// 問題があった初期設定
const serviceContext = ServiceContext.fromDefaults({
  chunkSize: 2048, // 大きすぎる
  chunkOverlap: 0, // オーバーラップなし
});

const queryEngine = index.asQueryEngine({
  similarityTopK: 2, // 少なすぎる
  similarityCutoff: 0.85, // 閾値が高すぎる
});

この設定で実行すると、以下のようなエラーが発生します。

typescriptconst response = await queryEngine.query(
  'エラーコード E001 の対処方法'
);
// 結果: 0件ヒット、または無関係な情報を返す

エラーコード: IndexError: No nodes found matching the query

このエラーは、検索条件が厳しすぎて、該当する文書が見つからない場合に発生します。

問題の診断プロセス

まず、検索結果の詳細を確認します。

typescript// デバッグ用の詳細出力
const response = await queryEngine.query(
  'エラーコード E001 の対処方法'
);

console.log(
  '検索ノード数:',
  response.sourceNodes?.length || 0
);
response.sourceNodes?.forEach((node, index) => {
  console.log(`ノード${index + 1}:`);
  console.log('  スコア:', node.score);
  console.log('  テキスト:', node.text.substring(0, 100));
});

出力例です。

plaintext検索ノード数: 0
// またはスコアが0.85未満のため除外されている

次に、類似度閾値を下げて再検索します。

typescript// 診断用の緩い設定
const diagnosisEngine = index.asQueryEngine({
  similarityTopK: 10,
  similarityCutoff: 0.5, // 一時的に下げる
});

const diagResponse = await diagnosisEngine.query(
  'エラーコード E001 の対処方法'
);

console.log(
  '検索ノード数:',
  diagResponse.sourceNodes?.length
);

これにより、検索自体は機能しているが、閾値が原因でフィルタされていることが判明します。

解決策の実装

問題を特定したので、最適な設定に変更します。

typescript// 改善後の設定
const improvedServiceContext = ServiceContext.fromDefaults({
  chunkSize: 512, // 適切なサイズに変更
  chunkOverlap: 50, // オーバーラップを追加
});

インデックスを再構築します。

typescript// インデックスの再構築
const improvedIndex = await VectorStoreIndex.fromDocuments(
  documents,
  { serviceContext: improvedServiceContext }
);

クエリエンジンの設定も最適化します。

typescriptconst improvedQueryEngine = improvedIndex.asQueryEngine({
  similarityTopK: 5, // 適切な件数
  similarityCutoff: 0.7, // 現実的な閾値
});

結果を確認します。

typescriptconst finalResponse = await improvedQueryEngine.query(
  'エラーコード E001 の対処方法'
);

console.log(finalResponse.toString());
// 正しく関連する文書が取得され、適切な回答が生成される

改善効果の測定

改善前後の比較データです。

#指標改善前改善後変化
1検索ヒット率45%92%+47pt
2平均検索ノード数0.8 件4.2 件+3.4 件
3ユーザー満足度2.1/5.04.3/5.0+2.2pt
4平均応答時間2.3 秒1.8 秒-0.5 秒
5幻覚発生率38%12%-26pt

ケーススタディ 2: 幻覚を抑制した Q&A システム

社内 Q&A システムで、AI が勝手に情報を作り出してしまう問題への対処事例です。

問題の発生

以下のような現象が報告されていました。

typescript// 問題のあったコード
const queryEngine = index.asQueryEngine({
  similarityTopK: 15, // 多すぎる
  similarityCutoff: 0.5, // 低すぎる
});

const response = await queryEngine.query(
  '来月の社内イベントは?'
);

console.log(response.toString());
// 出力: 「来月は花見イベントがあります」
// 実際には存在しないイベントを生成してしまう

この問題の原因は、無関係な文書が大量に取得され、LLM が混乱していることでした。

解決アプローチ

以下の図は、幻覚抑制のための多層的なアプローチを示しています。

mermaidflowchart TD
  query["ユーザー質問"] --> filter["メタデータ<br/>フィルタリング"]
  filter --> search["ベクトル検索<br/>(TopK=8)"]
  search --> rerank["リランキング<br/>(TopN=3)"]
  rerank --> verify["検証プロンプト"]
  verify --> llm["LLM"]
  llm --> check{回答に<br/>根拠あり?}
  check -->|Yes| answer["回答出力"]
  check -->|No| refuse["回答拒否"]

この多層的なアプローチにより、各段階で品質を担保します。

実装コード

まず、メタデータフィルタを設定します。

typescriptimport {
  MetadataFilters,
  MetadataFilter,
} from 'llamaindex';

// 最新の情報のみに絞る
const filters = new MetadataFilters({
  filters: [
    new MetadataFilter({
      key: 'date',
      value: '2024-01-01',
      operator: '>=',
    }),
    new MetadataFilter({
      key: 'category',
      value: 'events',
      operator: '==',
    }),
  ],
});

検証プロンプトを定義します。

typescriptconst verificationPrompt = new PromptTemplate({
  template: `あなたは正確性を最優先するアシスタントです。

以下のコンテキストのみを使用してください。
情報がない場合は「情報がありません」と回答してください。
絶対に推測や創作をしないでください。

コンテキスト:
{context_str}

質問: {query_str}

コンテキストに情報がありますか? (Yes/No)
回答:`,
});

すべてを統合したクエリエンジンを作成します。

typescriptconst queryEngine = index.asQueryEngine({
  similarityTopK: 8,
  similarityCutoff: 0.7,
  filters: filters,
  textQATemplate: verificationPrompt,
});

実行してみましょう。

typescriptconst response = await queryEngine.query(
  '来月の社内イベントは?'
);

console.log(response.toString());
// 改善後: 「提供された情報には来月のイベントに関する記載がありません」

さらなる改善: ソース引用の追加

回答に根拠を明示することで、信頼性を高めます。

typescriptconst citationPrompt = new PromptTemplate({
  template: `コンテキスト情報を使用して質問に回答してください。
回答には必ず出典を示してください。

コンテキスト:
{context_str}

質問: {query_str}

回答形式:
[回答内容]

出典:
- [ソース1]
- [ソース2]

回答:`,
});

このプロンプトを使用します。

typescriptconst citationEngine = index.asQueryEngine({
  similarityTopK: 5,
  similarityCutoff: 0.7,
  textQATemplate: citationPrompt,
});

const citedResponse = await citationEngine.query(
  'LlamaIndexのバージョン管理方法'
);

console.log(citedResponse.toString());

出力例です。

plaintextLlamaIndexでは package.json でバージョンを管理します。
yarn add llamaindex@latest で最新版に更新できます。

出典:
- インストールガイド (docs/installation.md)
- パッケージ管理ベストプラクティス (docs/best-practices.md)

ケーススタディ 3: 複数言語対応での検索精度問題

日本語と英語が混在する文書での検索問題と解決策です。

問題の構造

以下の図は、多言語環境での検索の課題を示しています。

mermaidflowchart LR
  ja_query["日本語クエリ<br/>「検索方法」"] --> embed["埋め込み<br/>モデル"]
  en_query["英語クエリ<br/>「search method」"] --> embed

  embed --> vector["ベクトル空間"]

  vector --> ja_doc["日本語文書"]
  vector --> en_doc["英語文書"]
  vector --> mix_doc["混在文書"]

  ja_doc -.->|言語ミスマッチ| fail["検索失敗"]
  en_doc -.->|言語ミスマッチ| fail
  mix_doc --> success["適切な検索"]

多言語モデルを使わないと、言語間の意味的類似性を捉えられません。

解決策の実装

多言語対応の埋め込みモデルを使用します。

typescriptimport { HuggingFaceEmbedding } from 'llamaindex';

// 多言語対応モデル
const multilingualEmbed = new HuggingFaceEmbedding({
  modelName: 'intfloat/multilingual-e5-large',
});

サービスコンテキストに設定します。

typescriptconst serviceContext = ServiceContext.fromDefaults({
  chunkSize: 512,
  chunkOverlap: 50,
  embedModel: multilingualEmbed,
});

メタデータに言語情報を追加します。

typescriptconst documents = [
  new Document({
    text: 'LlamaIndexは検索拡張生成のためのフレームワークです',
    metadata: { language: 'ja', category: 'overview' },
  }),
  new Document({
    text: 'LlamaIndex is a framework for retrieval-augmented generation',
    metadata: { language: 'en', category: 'overview' },
  }),
];

インデックスを作成します。

typescriptconst multilingualIndex =
  await VectorStoreIndex.fromDocuments(documents, {
    serviceContext,
  });

日本語と英語の両方で検索できるようになります。

typescript// 日本語での検索
const jaResponse = await multilingualIndex
  .asQueryEngine({ similarityTopK: 3 })
  .query('検索拡張生成とは');

// 英語での検索
const enResponse = await multilingualIndex
  .asQueryEngine({ similarityTopK: 3 })
  .query('What is retrieval-augmented generation');

// どちらも適切な文書を取得

デバッグとモニタリングの実装

本番環境での問題を早期発見するため、ロギングとモニタリングを実装します。

詳細ロギングの追加

検索プロセスの各段階をログに記録します。

typescriptclass MonitoredQueryEngine {
  constructor(private baseEngine: any) {}

  async query(queryStr: string) {
    const startTime = Date.now();

    console.log("[検索開始]", {
      query: queryStr,
      timestamp: new Date().toISOString()
    });

検索実行と結果の記録を行います。

typescriptconst response = await this.baseEngine.query(queryStr);
const duration = Date.now() - startTime;

console.log('[検索完了]', {
  duration: `${duration}ms`,
  nodeCount: response.sourceNodes?.length || 0,
  hasResponse: !!response.response,
});

各ノードの詳細をログ出力します。

typescript    response.sourceNodes?.forEach((node: any, index: number) => {
      console.log(`[ノード${index + 1}]`, {
        score: node.score?.toFixed(3),
        textPreview: node.text.substring(0, 80) + "...",
        metadata: node.metadata
      });
    });

    return response;
  }
}

使用例です。

typescriptconst baseEngine = index.asQueryEngine({
  similarityTopK: 5,
  similarityCutoff: 0.7,
});

const monitoredEngine = new MonitoredQueryEngine(
  baseEngine
);

const response = await monitoredEngine.query(
  '検索精度の改善方法'
);

ログ出力例です。

plaintext[検索開始] {
  query: '検索精度の改善方法',
  timestamp: '2024-01-15T10:30:45.123Z'
}
[検索完了] {
  duration: '243ms',
  nodeCount: 5,
  hasResponse: true
}
[ノード1] {
  score: '0.876',
  textPreview: 'チャンクサイズを512トークンに設定することで、検索精度が向上します...',
  metadata: { category: 'best_practices', topic: 'chunking' }
}
...

パフォーマンス指標の収集

検索品質を定量的に測定します。

typescriptclass PerformanceTracker {
  private metrics: any[] = [];

  track(query: string, response: any, relevanceScore: number) {
    this.metrics.push({
      timestamp: Date.now(),
      query: query,
      nodeCount: response.sourceNodes?.length || 0,
      avgScore: this.calculateAvgScore(response.sourceNodes),
      relevanceScore: relevanceScore,  // 人間による評価
    });
  }

平均スコアを計算します。

typescript  private calculateAvgScore(nodes: any[]): number {
    if (!nodes || nodes.length === 0) return 0;
    const sum = nodes.reduce((acc, node) => acc + (node.score || 0), 0);
    return sum / nodes.length;
  }

統計情報を取得します。

typescript  getStatistics() {
    if (this.metrics.length === 0) return null;

    return {
      totalQueries: this.metrics.length,
      avgNodeCount: this.average(this.metrics.map(m => m.nodeCount)),
      avgRelevance: this.average(this.metrics.map(m => m.relevanceScore)),
      avgSearchScore: this.average(this.metrics.map(m => m.avgScore)),
    };
  }

  private average(arr: number[]): number {
    return arr.reduce((a, b) => a + b, 0) / arr.length;
  }
}

使用例です。

typescriptconst tracker = new PerformanceTracker();

// 検索実行
const response = await queryEngine.query('検索方法');

// 結果を人間が評価(5段階)
const humanScore = 4; // ユーザーフィードバックなどから取得

tracker.track('検索方法', response, humanScore);

// 統計情報の確認
console.log(tracker.getStatistics());

出力例です。

plaintext{
  totalQueries: 150,
  avgNodeCount: 4.2,
  avgRelevance: 3.8,
  avgSearchScore: 0.78
}

まとめ

LlamaIndex を使った検索システムで「検索がヒットしない」「幻覚が増える」といった問題は、適切な設定と対策により大幅に改善できます。本記事で紹介した解決策を改めて整理しましょう。

重要なポイント

検索精度を向上させるための主要な施策は以下の通りです。

#施策効果実装の難易度
1チャンクサイズの最適化検索ヒット率 +30-50%★☆☆
2多言語対応埋め込みモデル日本語精度 +40-60%★★☆
3類似度閾値の調整バランスの改善★☆☆
4メタデータフィルタリングノイズ削減 -50%★★☆
5カスタムプロンプト幻覚抑制 -60%★☆☆
6リランキング関連性 +20-30%★★★

トラブルシューティングのフローチャート

問題発生時の診断フローを図示します。

mermaidflowchart TD
  start["検索に問題発生"] --> type{問題の種類}

  type -->|ヒットしない| hit_check["ログで確認"]
  type -->|幻覚が多い| hall_check["回答内容確認"]

  hit_check --> nodes{ノード数は?}
  nodes -->|0件| threshold["閾値を下げる<br/>(0.7→0.5)"]
  nodes -->|少ない| topk["TopKを増やす<br/>(3→8)"]
  nodes -->|多い| chunk["チャンク設定<br/>を見直す"]

  hall_check --> source{ソースは<br/>適切?}
  source -->|No| filter["メタデータ<br/>フィルタ追加"]
  source -->|Yes| prompt_fix["プロンプト<br/>改善"]

  threshold --> verify["動作確認"]
  topk --> verify
  chunk --> verify
  filter --> verify
  prompt_fix --> verify

  verify --> result{解決?}
  result -->|Yes| done["完了"]
  result -->|No| expert["詳細調査<br/>・専門家相談"]

推奨される実装手順

新規にシステムを構築する場合、以下の順序で実装することをお勧めします。

ステップ 1: 基本設定の確立

  • チャンクサイズ 512、オーバーラップ 50 で開始
  • 多言語対応の埋め込みモデルを選択
  • similarityTopK=5、similarityCutoff=0.7 を初期値に

ステップ 2: メタデータ設計

  • 文書にカテゴリ、日付、言語などの属性を付与
  • 検索時にフィルタリングできる構造を作る

ステップ 3: プロンプトエンジニアリング

  • 幻覚抑制のためのシステムプロンプトを作成
  • ソース引用を含める形式を採用

ステップ 4: モニタリング体制

  • ログ出力とパフォーマンス追跡を実装
  • ユーザーフィードバックを収集する仕組み

ステップ 5: 継続的改善

  • 実データで検証し、パラメータを調整
  • 定期的にメトリクスを確認して最適化

よくある質問と回答

Q: チャンクサイズはどう決めればよいですか? A: 文書の性質により異なりますが、技術文書なら 512-1024、Q&A なら 256-512 トークンが目安です。実データで試しながら調整しましょう。

Q: OpenAI と Hugging Face のどちらの埋め込みモデルを使うべきですか? A: 日本語メインなら Hugging Face の多言語モデル(multilingual-e5-large)、英語中心なら OpenAI の text-embedding-3-large が推奨されます。

Q: 検索結果が 0 件の時のエラー処理はどうすればよいですか? A: 類似度閾値を段階的に下げて再検索するか、ユーザーに「該当する情報が見つかりませんでした」と正直に伝えることが重要です。

Q: 本番環境でどの程度のパフォーマンスを期待できますか? A: 適切に設定すれば、検索ヒット率 90%以上、幻覚発生率 10%以下を達成できます。ただし、データ品質とチューニングに依存します。

本記事で紹介した手法を組み合わせることで、実用的な RAG システムを構築できます。最初から完璧を目指さず、段階的に改善していくアプローチが成功への近道ですね。

関連リンク