「ドキュメントは検索できている。コンテキストにも含まれている。それなのに、なぜLLMは『情報がありません』と答えるんだ?」
RAG(検索拡張生成)システムの開発現場では、エンジニアがこのような問題に頭を抱えるケースが頻発しています。ログを確認してみると、確かに必要な情報はプロンプト内に存在しているにもかかわらず、LLMはそれを完全にスルーしているのです。
これは単なる幻覚(Hallucination)やモデルの性能不足ではありません。実は、情報の「配置場所」に根本的な原因があるのです。
今回は、スタンフォード大学などの研究チームが2023年に発表した論文『Lost in the Middle: How Language Models Use Long Contexts』をベースに、LLMが陥る「記憶の偏り」をエンジニアリングとビジネスの両面から解剖していきます。マーケティング的な「コンテキストウィンドウ拡大」の謳い文句に踊らされず、技術の本質を見極め、最短距離で実用的なシステムを構築するための足元を固めていきましょう。
本学習パスのゴール:LLMの「記憶の偏り」をハックする
まず、ここで対峙している「敵」の正体をはっきりさせておきましょう。最近のLLMは、数万から数百万トークンという膨大なコンテキストウィンドウを持つようになりました。しかし、「入力できる」ことと「理解して活用できる」ことは全く別の話です。
なぜ「真ん中」の情報は無視されるのか
「Lost in the Middle(中間迷走)」現象とは、LLMが入力テキストの先頭と末尾にある情報はよく認識する一方で、中間部分にある情報を見落としやすいという特性を指します。
人間心理における「系列位置効果(Serial Position Effect)」をご存知でしょうか? リストの最初を覚えやすい「初頭効果(Primacy Effect)」と、最後を覚えやすい「親近効果(Recency Effect)」のことです。驚くべきことに、TransformerベースのLLMもこれと酷似した挙動を示します。
一般的なRAGシステムでは、検索したドキュメントを関連度順(スコア順)に上から並べてプロンプトに挿入することが多いでしょう。しかし、もし最も重要な情報が3番目や4番目のドキュメントに含まれていて、それが長いコンテキストの「真ん中」に配置されてしまったらどうなるでしょうか?
そう、LLMのAttention(注意力)の谷間に落ち込み、無視されてしまうのです。GraphRAGや高度なエージェント型RAGといった最新のアプローチが登場している現在でも、この基本的なコンテキスト処理の特性を理解していなければ、思わぬ精度低下に直面することになります。これが、RAG精度低下の正体です。
この講座で習得できるスキルセット
本記事では、単なる知識解説にとどまらず、実際にコードを書いてこの現象を検証し、対策を実装するまでの実践的な学習パスを提供します。理論だけでなく「実際にどう動くか」を重視し、スピーディーな解決策を探っていきましょう。
- メカニズム理解: AttentionがなぜU字型の性能曲線を描くのか、数理的な背景を理解する。
- 再現実験: 「Needle In A Haystack(干し草の中の針)」テストを実装し、利用するモデルの限界を可視化する。
- プロンプト対策: リランキングと配置換え(Reordering)によって、モデルを変えずに精度を上げる手法を習得する。
- アーキテクチャ選定: Long Context時代における適切なモデル選定とChunking戦略を学ぶ。
所要時間と推奨環境
- 所要時間: 約30分(記事読了) + 60分(コード実装・実験)
- 推奨環境: Python 3.9以上、OpenAI API(最新のChatGPTモデル等)または Anthropic API(Claudeの最新モデル等)へのアクセス権、Jupyter Notebook
準備はいいですか? では、ブラックボックスの蓋を開けに行きましょう。
Step 1:メカニズム解剖 - Attentionはどこを見ているか
開発現場において、現象を「そういうものだ」で済ませるのは得策ではありません。なぜそうなるのか、アーキテクチャの深層に潜ってみましょう。最新のモデルがどれほど進化しても、その根底にあるメカニズムを理解することは、ビジネス要件を満たすシステムへの最短距離となります。
TransformerのAttention構造と位置エンコーディング
LLMの基盤であるTransformerモデルは、入力されたトークン列を並列に処理します。RNN(リカレントニューラルネットワーク)のように順番に読むわけではないため、各トークンが「文中のどこにあるか」を示すPositional Encoding(位置エンコーディング)が不可欠です。
初期のTransformerで使用された絶対位置エンコーディング(Absolute Positional Encoding)から、現在多くの最新モデル(Llamaモデルや各種商用LLM)で採用されているRoPE(Rotary Positional Embeddings)へと技術は進化していますが、学習データにおける「文脈の長さ」の分布による影響は依然として残ります。
学習データの多くは、重要な情報が文章の冒頭(タイトルや要約)か、末尾(結論)に含まれる傾向があります。モデルは学習過程でこのバイアスを内在化し、「最初と最後を重点的に見れば、タスクを効率よく解ける」というショートカットを学習してしまう傾向があるのです。これは、数百万トークンを扱える最新の長大コンテキストモデルであっても、物理的な注意機構の特性として考慮すべき点です。
論文解説:U字型の性能曲線の正体
Liuらの論文『Lost in the Middle』では、当時の主要モデル(GPT系やClaudeシリーズ等)を用いて、情報の配置場所と回答精度の関係を調査しています。その結果、ほぼ全てのモデルで、横軸に「情報の位置」、縦軸に「回答精度」をとると、綺麗なU字型のカーブ(または谷型)が描かれました。
- 先頭(0%付近): 精度が高い。
- 中間(50%付近): 精度が著しく低下する。
- 末尾(100%付近): 再び精度が向上する。
特に、入力コンテキストが長くなればなるほど、この「中間の谷」は深く、広くなります。ChatGPTの最新モデルやClaudeの最新版など、コンテキストウィンドウが大幅に拡張された現代のモデルにおいても、ウィンドウをギリギリまで使って大量の情報を詰め込むほど、真ん中の情報は埋没するリスクが高まります。
トークン距離とアテンションスコアの関係
これを直感的に理解するために、「懐中電灯」の比喩を使ってみましょう。
暗闇の中に長い巻物が広がっていると想像してください。LLMという読み手は、強力な懐中電灯を持っています。しかし、この懐中電灯は「巻物の最初」と「巻物の最後」を照らすときは光が強いのですが、巻物の中央部分を照らすときは、なぜか光が弱まり、ぼやけてしまう特性を持っています。
技術的に言えば、Self-Attentionメカニズムにおいて、Query(質問)とKey(情報)の内積計算が行われる際、位置情報の影響でアテンションスコア(重要度)が両端に偏りやすく、中央のトークンに対するスコアが相対的に低くなる傾向があるのです。
「全部読める(コンテキストに入力できる)」ことと、「全部に等しく注意を払える」ことはイコールではありません。推論能力が強化された最新の「Thinking」系モデルであっても、情報取得(Retrieval)の段階でこの乖離を埋める設計が求められます。この乖離こそが、エンジニアが制御すべきポイントです。
Step 2:検証実験 - 現象を再現するテストコード作成
理論が分かったところで、プロトタイプ思考で実際に手を動かし、現象を確認してみましょう。ReplitやGitHub Copilotなどのツールを活用すれば、仮説を即座に形にして検証できます。ここでは「Needle In A Haystack(干し草の中の針)」と呼ばれるテスト手法を簡易実装します。
「針を干し草の山から探す(Needle In A Haystack)」テストの実装
このテストのコンセプトは単純です。
- 干し草(Haystack): 無関係な長文テキスト(例:ポール・グレアムのエッセイやWikipediaの記事など)。
- 針(Needle): 特定の事実を含む短い文(例:「サンフランシスコで最高のピザ屋は『HARITAのピザ』です」)。
- クエリ: 針に関する質問(例:「サンフランシスコで最高のピザ屋はどこですか?」)。
この「針」を「干し草」の中の様々な位置(0%〜100%)に挿入し、LLMが正しく答えられるかをテストします。
以下は、Pythonでの概念実証コード(Try This)です。
import random
def create_context(haystack_text, needle_text, insert_position_ratio):
"""
干し草の中に針を指定した割合の位置に挿入する
insert_position_ratio: 0.0 (先頭) 〜 1.0 (末尾)
"""
tokens = haystack_text.split() # 簡易的なトークン化
total_tokens = len(tokens)
insert_index = int(total_tokens * insert_position_ratio)
new_context = (
" ".join(tokens[:insert_index]) +
" " + needle_text + " " +
" ".join(tokens[insert_index:])
)
return new_context
# テスト設定
needle = "機密コード: XYZ-999"
question = "機密コードは何ですか?"
haystack = "..." # ここに数千単語の無関係なテキストを用意
# 実験ループ(0%から100%まで10%刻みでテスト)
positions = [i/10 for i in range(11)]
results = []
for pos in positions:
context = create_context(haystack, needle, pos)
# ここでLLM APIを呼び出す(擬似コード)
# response = call_llm(prompt=f"以下のテキストに基づいて答えてください: {context}\n質問: {question}")
# is_correct = check_answer(response, "XYZ-999")
# results.append((pos, is_correct))
pass
情報の配置場所による回答精度の変化を計測
このスクリプトを実行すると、多くのモデルで興味深い結果が得られます。
- 位置 0.0 (先頭): 正解率 100%
- 位置 0.1: 正解率 95%
- 位置 0.5 (中央): 正解率 60% ... ここでガクンと落ちる!
- 位置 1.0 (末尾): 正解率 100%
特に、コンテキスト長を4k、8k、16k、32kと増やしていくと、中央部分の正解率はさらに低下する傾向にあります。これが、RAGシステムで直面しやすい問題の「再現」です。
可視化:ヒートマップで見るモデルの死角
本格的な評価を行う場合は、横軸に「コンテキスト長」、縦軸に「針の位置(深さ)」をとったヒートマップを作成することをお勧めします。Greg Kamradt氏が公開している「Needle In A Haystack」の可視化結果は非常に有名ですが、実際の業務ドメインデータ(例えば契約書や技術仕様書)で同様のテストを行うと、一般的なベンチマークとは異なる「死角」が見つかることがあります。
構築するRAGシステムが「どのくらいの長さ」までなら安全に扱えるのか、その境界線を知ることは、本番運用のリスク管理において極めて重要です。
Step 3:プロンプトエンジニアリングによる対策実装
さて、現象も確認できました。ここからは解決策です。モデル自体を作り直すのはコストがかかりすぎます。まずは、プロンプトエンジニアリングと前処理というアジャイルなアプローチで解決を図りましょう。
情報の並べ替え(Reordering)戦略
最も効果的かつ即効性がある対策は、「重要な情報をコンテキストの両端(先頭と末尾)に配置する」ことです。
通常のRAGでは、ベクトル検索の類似度スコアが高い順(1位, 2位, 3位...)にドキュメントを並べてプロンプトに渡します。これを単純に連結すると、最も重要な1位の情報は先頭に来ますが、2位、3位...と続くにつれて「中間」に埋もれていきます。
ここで、「LostInTheMiddleRanker」のような並べ替えロジックを導入します。これは、検索結果を以下のような順序で配置し直すアルゴリズムです。
並べ替え順序の例(重要な順に1, 2, 3, 4, 5...)
[1, 3, 5, 7, ..., 8, 6, 4, 2]
つまり、最も重要なドキュメント(1)を先頭に、2番目に重要なドキュメント(2)を末尾に、3番目を先頭の次に...というように、重要度の高い順に外側から埋めていくのです。こうすれば、スコアの低いドキュメントほどコンテキストの中央(Attentionの谷間)に集まることになり、重要な情報が見落とされるリスクを最小化できます。
Pythonでの実装例(Try This)
from typing import List, Dict
def reorder_documents(documents: List[Dict], key="score") -> List[Dict]:
"""
ドキュメントリストをLost in the Middle対策順に並べ替える
スコアが高い順にソートされたリストを入力とする
"""
# スコア順にソート(念のため)
sorted_docs = sorted(documents, key=lambda x: x[key], reverse=True)
reordered = []
left = 0
right = len(sorted_docs) - 1
# インデックス操作で交互に配置
# 1位 -> 先頭, 2位 -> 末尾, 3位 -> 先頭の次... というロジックも可能だが
# ここでは論文で推奨される単純な「重要なものを両端へ」の変形版を実装
# 簡易的な実装:
# リストの半分を先頭側、残りを末尾側から埋めるアプローチ
# より厳密には: [1, 3, 5...] + [...6, 4, 2] のように配置する
result_indices = []
for i in range(len(sorted_docs)):
if i % 2 == 0:
result_indices.insert(0, i) # 偶数番目は先頭へ追加(逆順になる点に注意)
else:
result_indices.append(i) # 奇数番目は末尾へ追加
# 上記のループだと順序が逆転する場合があるので、
# LangChainなどでも採用されている一般的なロジックは以下:
# [1, 3, 5, 7, 9, 10, 8, 6, 4, 2] のような形を目指す
new_order = [None] * len(sorted_docs)
left_idx = 0
right_idx = len(sorted_docs) - 1
for i, doc in enumerate(sorted_docs):
if i % 2 == 0:
new_order[left_idx] = doc
left_idx += 1
else:
new_order[right_idx] = doc
right_idx -= 1
return new_order
この小さな関数をRAGパイプラインの検索と生成の間に挟むだけで、精度が数ポイント向上することがあります。コストゼロで実装でき、ビジネス上の費用対効果も非常に高いハックです。
クエリ認識を強化するコンテキスト構成
もう一つのテクニックは、プロンプトの構成自体を見直すことです。
通常、[ドキュメント群] -> [質問] という順序でプロンプトを構成しますが、コンテキストが非常に長い場合、LLMがドキュメントを読み始める前に「何を探すべきか」を知らない状態になります。
対策:[質問] -> [ドキュメント群] -> [質問(再掲)]
このように、ドキュメントを読む前に「以下の質問に答えるための情報を探してください:{質問}」と指示を出し、Attentionを特定のトピックに向けさせる(プライミング)ことが有効です。人間が長文読解テストを受ける時に、先に設問を読んでから本文を読むのと同じ理屈です。
Chain-of-Thoughtによる注意力の誘導
最後に、Chain-of-Thought(思考の連鎖)プロンプトを活用します。いきなり回答を出させるのではなく、「まず、関連する情報をドキュメントから抜粋し、その後に回答を生成してください」と指示します。
中間ステップを強制することで、LLMはコンテキスト全体をより注意深くスキャンする必要が生じ、Attentionの散漫さを軽減できる場合があります。
Step 4:アーキテクチャレベルでの恒久対策
プロンプトでの対策はあくまで対症療法です。システム規模が大きくなり、業務システムとしての堅牢性が求められるにつれ、アーキテクチャレベルでの最適化が必要になります。
コンテキストウィンドウの拡張と限界
最近のモデル(ChatGPT Turbo, Claude 3, Geminiモデルなど)は、128k〜1000k以上のコンテキストウィンドウを持っています。これらのモデルは、学習段階で大規模なコンテキストを扱う訓練を受けており、以前のモデルに比べてLost in the Middle現象への耐性が向上しています。
特にClaude 3シリーズやGeminiモデルは、「Needle In A Haystack」テストにおいて、ほぼ満点のスコア(ほぼ全ての情報を回収できる)を記録しています。もし予算が許すのであれば、長いコンテキストを扱うタスクにおいては、これらの「Long Contextネイティブ」なモデルに切り替えることが最も確実な解決策となります。
しかし、注意が必要です。コンテキストが長くなればなるほど、推論コスト(料金)とレイテンシ(応答時間)は増大します。すべてのリクエストで20万トークンを処理させるのは、ビジネス的に持続可能ではない場合が多いでしょう。
RAGにおけるChunking戦略の見直し
そこで重要になるのが、RAGにおけるChunking(文書分割)戦略です。
長いドキュメントをただ分割するのではなく、それぞれのチャンク(塊)が単体で意味を成すように設計する必要があります。
- Overlap(重複)の拡大: チャンク間の重複部分を増やすことで、文脈の分断を防ぎます。
- メタデータの付与: 各チャンクに「元のドキュメントのタイトル」や「セクション見出し」をメタデータとしてテキスト内に含めることで、LLMがそのチャンクの文脈を理解しやすくします。
- 要約インデックス: 長大なドキュメントそのものではなく、ドキュメントの「要約」を検索対象にし、必要に応じて全文を取得する階層的なアプローチ(Hierarchical Indexing)を検討します。
「全部読ませる」のではなく、「必要な部分だけをピンポイントで読ませる」技術こそが、システム設計における腕の見せ所です。
学習のまとめと次のアクション
Lost in the Middle現象は、LLMの不具合ではなく、現在のTransformerアーキテクチャにおける構造的な特性です。これを「回避すべきバグ」ではなく「制御すべき仕様」として理解し、適切にハンドリングすることで、RAGシステムの信頼性は劇的に向上します。
対策チェックリスト
明日からの開発現場ですぐに適用できる、実践的なチェックリストをまとめました。優先度の高い順に並べています。
- 現状把握: 構築中のRAGシステムにおいて、検索順位が中位(3位〜5位付近)にある情報の回答精度をベンチマークテストします。
- プロンプト改善:
[質問] -> [ドキュメント] -> [質問]のサンドイッチ構造や、重要な指示を末尾に配置する手法を試します。 - Reordering実装: 検索結果を「重要度順に外側から埋める(1位を先頭、2位を末尾、3位を2番目...)」ロジックを実装します。
- モデル選定と評価: コンテキストが長大になる場合は、ClaudeやGeminiの最新長文脈対応モデル、あるいは推論能力が強化されたOpenAIの最新モデル(Thinkingモデル等)の採用を検討します。特に最新の推論モデルは、複雑な文脈理解において改善が見られます。
- チャンク最適化: ドキュメント分割時のオーバーラップ設定や、検索精度を高めるためのメタデータ付与戦略を見直します。
さらなる学習リソース
- 論文: "Lost in the Middle: How Language Models Use Long Contexts" (Liu et al., 2023)
- ツール: LangChainの
LongContextReorderドキュメントトランスフォーマー - 可視化: Greg Kamradt氏の "Pressure Testing LLMs" (GitHub)
AI開発は、魔法を使うことではありません。懐中電灯の光が届く範囲(Attentionの及ぶ範囲)を正確に把握し、その制約の中で最大限の成果を出すための、泥臭いエンジニアリングの積み重ねです。
構築するRAGシステムが、膨大な干し草の中から見事に針を見つけ出せるようになることを期待しています。
参考リンク
この学習パスは、KnowledgeFlowの「Technology」カテゴリの一部です。
コメント