【LLM全般】コンテキスト長オーバーエラーを解決!長文処理の実践的対処法5選

問題の概要:LLMのコンテキスト長制限を超えた場合のエラーと課題

大規模言語モデル(LLM)を利用する際、多くの開発者が直面する代表的な課題の一つが「コンテキスト長(Context Length)の制限」です。これは、モデルが一度に処理できる入力テキスト(プロンプト+生成テキスト)の最大トークン数を指します。この制限を超えると、以下のような問題が発生します。

具体的なエラー例と症状

# OpenAI API での典型的なエラーメッセージ
Error: This model's maximum context length is 4097 tokens. However, your messages resulted in 5000 tokens. Please reduce the length of the messages.

# LangChain を使用時のエラー例
openai.error.InvalidRequestError: This model's maximum context length is 16385 tokens. However, you requested 18000 tokens (1900 in the messages, 16100 in the completion). Please reduce the length of the messages or completion.

# ローカルLLM(Llama 2など)での症状
- 生成が途中で突然停止する
- 後半の入力内容が完全に無視される
- 意味のない文字列や繰り返しが出力される(デコーディングエラー)

これらのエラーは、長いドキュメントの要約、複数ファイルにわたるコード解析、長いチャット履歴の処理など、現実の多くのユースケースで発生します。単にエラーが返るだけでなく、モデルが警告なしに入力の後半部分を「サイレントに無視」する場合もあり、出力結果の信頼性を損なう重大な問題となります。

原因の解説:なぜコンテキスト長に制限があるのか?

コンテキスト長の制限は、主に以下の技術的・計算量的な理由に起因しています。

1. 計算コストの増大(二次的な複雑度)

Transformerアーキテクチャを基盤とする現代のLLMの多くは、注意機構(Attention Mechanism)を使用しています。特に、オリジナルの自己注意(Self-Attention)の計算複雑度は、入力シーケンス長Nに対してO(N²)です。つまり、コンテキスト長が2倍になると、必要な計算量とメモリ使用量は約4倍に膨れ上がります。これは、推論時間の増大とコスト上昇に直結します。

2. ハードウェアメモリ(VRAM)の制約

長いシーケンスを処理するためには、すべてのトークンのキー(Key)とバリュー(Value)をメモリに保持する必要があります。例えば、コンテキスト長32KトークンのモデルをFP16で実行する場合、注意機構のKVキャッシュだけでも数百MBから数GBのVRAMを消費します。これは、一般的なコンシューマー向けGPUのメモリ容量を容易に超える可能性があります。

3. モデル事前学習時の制約

モデルは特定のコンテキスト長で事前学習されています。この長さを超えるシーケンスを処理させようとすると、モデルがこれまで遭遇したことのない「位置」にトークンが配置されることになり、位置エンコーディング(Positional Encoding)が適切に機能せず、性能が大幅に低下する可能性があります。

解決方法:実践的ステップバイステップ対処法5選

ここでは、実際の開発現場で使える具体的な解決策を、実装例とともに紹介します。

方法1: 入力テキストの分割と要約(チャンキング)

長いドキュメントをモデルのコンテキスト長に収まるサイズの「チャンク」に分割し、それぞれを処理する古典的かつ効果的な方法です。

# Pythonによる簡単なテキスト分割の例
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 長いドキュメントを準備
long_document = "..." # 非常に長いテキスト

# テキストスプリッターの初期化
# chunk_size: 各チャンクの最大トークン数(モデルの制限より十分に小さい値に設定)
# chunk_overlap: チャンク間の重複部分(文脈の連続性を保つため)
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    length_function=len,
    separators=["nn", "n", "。", "、", " ", ""]
)

# ドキュメントを分割
chunks = text_splitter.split_text(long_document)
print(f"ドキュメントを {len(chunks)} 個のチャンクに分割しました。")

# 各チャンクを順番に処理
for i, chunk in enumerate(chunks):
    prompt = f"以下のテキストの要点をまとめてください:nn{chunk}"
    # LLM API呼び出し (例: OpenAI, Anthropicなど)
    # response = client.chat.completions.create(...)
    # 結果を保存または次の処理へ

方法2: Map-Reduceアプローチの適用

分割した各チャンクを並列または直列で処理(Map)し、その結果を統合(Reduce)する高度な手法です。LangChainなどのフレームワークがサポートしています。

# LangChainを用いたMap-Reduceの概要例
from langchain.chains import MapReduceDocumentsChain, ReduceDocumentsChain
from langchain.chains.llm import LLMChain
from langchain.prompts import PromptTemplate

# 1. Mapステップ:各チャンクを個別に要約
map_template = """以下のドキュメントの一部を要約してください:
{docs}
要約:"""
map_prompt = PromptTemplate.from_template(map_template)
map_chain = LLMChain(llm=llm, prompt=map_prompt)

# 2. Reduceステップ:複数の要約を一つに統合
reduce_template = """以下の複数の要約を統合して、元の長いドキュメント全体の一貫した要約を作成してください:
{doc_summaries}
統合された要約:"""
reduce_prompt = PromptTemplate.from_template(reduce_template)
reduce_chain = LLMChain(llm=llm, prompt=reduce_prompt)

# 3. チェーンを組み合わせる
combine_documents_chain = StuffDocumentsChain(
    llm_chain=reduce_chain,
    document_variable_name="doc_summaries"
)
reduce_documents_chain = ReduceDocumentsChain(
    combine_documents_chain=combine_documents_chain,
    collapse_documents_chain=combine_documents_chain,
    token_max=4000, # モデルのコンテキスト制限
)

# 4. Map-Reduceチェーンの作成と実行
map_reduce_chain = MapReduceDocumentsChain(
    llm_chain=map_chain,
    reduce_documents_chain=reduce_documents_chain,
    document_variable_name="docs"
)
# result = map_reduce_chain.run(split_docs)

方法3: 重要な部分の選択的抽出(Relevance Extraction)

ユーザーのクエリに関連する部分のみをコンテキストとして抽出する方法です。まず、全体から関連部分を検索・抽出し、その部分だけをLLMに渡します。

# 埋め込み(Embedding)を使った関連部分抽出の概念例
import openai
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

def extract_relevant_context(full_text, query, max_tokens=3000):
    # 1. 全文を段落や文単位で分割
    paragraphs = full_text.split('nn')
    
    # 2. クエリと各段落の埋め込みベクトルを取得
    query_embedding = get_embedding(query)
    para_embeddings = [get_embedding(p) for p in paragraphs]
    
    # 3. コサイン類似度を計算
    similarities = [cosine_similarity([query_embedding], [emb])[0][0] for emb in para_embeddings]
    
    # 4. 類似度が高い順に段落を選択し、トークン数制限内に収める
    selected_indices = np.argsort(similarities)[::-1]
    selected_text = ""
    token_count = 0
    
    for idx in selected_indices:
        para = paragraphs[idx]
        para_tokens = estimate_token_count(para) # トークン数を概算
        if token_count + para_tokens <= max_tokens:
            selected_text += para + "nn"
            token_count += para_tokens
        else:
            break
    
    return selected_text

# 抽出した関連コンテキストのみをプロンプトに使用
relevant_part = extract_relevant_context(long_document, user_query)
prompt = f"以下の関連文脈に基づいて質問に答えてください:nn{relevant_part}nn質問: {user_query}"

方法4: コンテキスト拡張をサポートするモデル・技術の利用

コンテキスト長の長いモデルを選択したり、制限を拡張する技術を利用します。

# コンテキスト長の長いモデルをAPIで指定する例(OpenAI)
# GPT-4 Turbo は 128Kトークンまでサポート
from openai import OpenAI
client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4-turbo", # または "gpt-4-32k", "claude-3-opus-20240229" (200K)
    messages=[{"role": "user", "content": long_prompt}],
    max_tokens=4000
)

# ローカルモデルでコンテキスト拡張技術を利用する例
# transformersライブラリで、RoPEの位置補間(Position Interpolation)を適用
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

model_name = "meta-llama/Llama-2-7b-chat-hf"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16)

# 注意: 実際のコンテキスト拡張には、モデルの修正とファインチューニングが必要な場合が多い
# 例: NTK-aware補間、YaRN、RoPEの周波数調整など

方法5: プロンプト設計の最適化

無駄なトークンを削減し、同じコンテキスト長でより多くの情報を効率的に伝えます。

# 非効率なプロンプト例(冗長)
prompt_bad = """
こんにちは、AIアシスタントさん。お願いがあります。私は今、ある長い技術文書を読んでいて、その内容を理解したいと思っています。その文書はソフトウェアアーキテクチャに関するもので、マイクロサービスとモノリシックアーキテクチャの比較について書かれています。もし可能でしたら、その文書の主要なポイントを、箇条書きで、できれば5つから7つくらいにまとめて、わかりやすく要約していただけないでしょうか?よろしくお願いいたします。

文書:
{document}
"""

# 効率的なプロンプト例(簡潔で指示が明確)
prompt_good = """
以下の技術文書の要点を5〜7箇条で要約せよ:
{document}
"""

# チャット履歴を圧縮する例
# 過去の長い会話を、LLM自身に要約させてコンテキストとして使用する
compression_prompt = """
以下のこれまでの会話履歴を、今後会話を続ける上で必要な情報を残しつつ、可能な限り簡潔に要約してください。
会話履歴:
{chat_history}
要約:
"""
# 要約された履歴を次の会話のシステムメッセージやコンテキストとして使用

まとめ・補足情報

LLMのコンテキスト長制限は根本的な技術的制約ですが、今回紹介した5つの実践的アプローチを組み合わせることで、ほとんどの長文処理タスクに対応できるようになります。

選択の指針

  • 単純な要約タスク → 「方法1: チャンキング」が最もシンプルで効果的。
  • 高精度が求められる分析タスク → 「方法2: Map-Reduce」で文脈のロスを最小化。
  • QAシステムや検索 → 「方法3: 関連部分抽出」が効率的。
  • 予算に余裕があり、シンプルに解決したい → 「方法4: 長いコンテキストモデル」を利用。
  • あらゆる場合に適用可能な基本対策 → 「方法5: プロンプト最適化」を常に心がける。

将来の展望

現在、コンテキスト長を効率的に拡張する研究が活発に行われています。Transformerの計算複雑度を線形(O(N))に近づけるMambaRWKVなどのアーキテクチャ、RoPEの拡張手法、メモリ効率の良い注意機構など、技術の進歩によりこの問題は緩和されていくでしょう。しかし、当面の間は、本記事で紹介したような「ソフトウェア的解決策」と「ハードウェア的解決策(長いコンテキストモデル)」を適切に使い分けることが、実用的なLLMアプリケーション開発の鍵となります。

最後に、実際のトークン数は単純な文字数カウントとは異なることに注意してください。日本語は英語に比べて1文字あたりのトークン数が多くなる傾向があります。開発時には、tiktoken(OpenAIモデル用)やモデル固有のトークナイザーを使って正確なトークン数を計測することを強くお勧めします。

# トークン数の正確な計測例 (OpenAI)
import tiktoken

encoding = tiktoken.encoding_for_model("gpt-4")
text = "これはテスト用の日本語の文章です。"
tokens = encoding.encode(text)
print(f"トークン数: {len(tokens)}")
print(f"トークンID: {tokens}")
この記事は役に立ちましたか?