問題の概要:LLMのコンテキスト長制限を超えた場合のエラー
大規模言語モデル(LLM)を活用する際、多くの開発者が直面する代表的な課題の一つが「コンテキスト長(Context Length)の制限」です。これは、モデルが一度に処理できる入力テキスト(プロンプト + 生成するテキスト)の最大トークン数を指します。この制限を超えると、以下のような具体的なエラーが発生します。
代表的なエラーメッセージ例
# OpenAI API (GPT-4, GPT-3.5-turbo) の場合
Error: This model's maximum context length is 8192 tokens. However, your messages resulted in 10500 tokens. Please reduce the length of the messages.
# Anthropic Claude API の場合
Error: Request too large: Request must be less than 100000 tokens. Your request was 120000 tokens.
# ローカルLLM (Llama 2, Mistral等) を transformers ライブラリで使用する場合
RuntimeError: The size of tensor a (15000) must match the size of tensor b (8192) at non-singleton dimension 1
# LangChain を使用している場合
langchain.schema.output_parser.OutputParserException: Could not parse LLM output: ... (コンテキスト不足による不完全な出力)
これらのエラーは、長いドキュメントの要約、複数ファイルにわたるコード分析、長いチャット履歴の処理など、現実の多くのユースケースで発生します。エラーが発生すると、APIコールは完全に失敗するか、モデルが入力の後半部分を無視して不正確な回答を生成する「コンテキストの切り捨て」が起こります。
原因の解説:なぜコンテキスト長に制限があるのか?
コンテキスト長の制限は、主に以下の技術的・計算量的な理由に起因しています。
1. 計算コストとメモリ使用量(O(n²)問題)
Transformerアーキテクチャを基盤とする現代のLLMは、自己注意機構(Self-Attention)を使用しています。この機構の計算量は入力シーケンス長の二乗(O(n²))に比例して増加します。つまり、コンテキスト長を2倍にすると、必要な計算リソースとメモリは約4倍になります。これは、応答時間の遅延とコストの急増を招きます。
2. モデルアーキテクチャの事前定義
多くのモデルは、訓練段階で固定された最大シーケンス長(例:Llama 2は4096トークン、GPT-4は8192または32768トークン)に対して最適化されています。推論時にこの長さを超えると、モデルが対応する位置埋め込み(Positional Encoding)を持たないため、性能が著しく低下したり、エラーが発生したりします。
3. トークン化のオーバーヘッド
「トークン」は単語やサブワードに相当するモデルの処理単位です。英語では1トークン≒0.75単語ですが、日本語などの言語ではより多くのトークンを消費する傾向があります。ユーザーが文字数で考えているテキストが、予想以上に多くのトークンに変換され、制限を超えてしまうことがよくあります。
解決方法:5つの実践的アプローチ
コンテキスト長制限に対処するには、単純に入力を切り詰める以外にも、いくつかの戦略があります。以下に、実装難易度と効果のバランスが取れた5つの方法を紹介します。
方法1: 入力テキストの戦略的切り捨てと要約
最も基本的な方法です。全ての情報を渡すのではなく、最も関連性の高い部分のみを選択してプロンプトに含めます。
# Python による簡単な実装例 (文字数ベースの簡易版)
def truncate_text_by_importance(text, max_tokens_estimate, keywords):
"""
text: 入力テキスト
max_tokens_estimate: 目標トークン数(概算)
keywords: 重要なキーワードのリスト
"""
# キーワード周辺の文を優先的に抽出(簡易実装)
sentences = text.split('。')
scored_sentences = []
for sent in sentences:
score = sum([sent.count(keyword) for keyword in keywords])
scored_sentences.append((score, sent))
# スコアの高い順にソート
scored_sentences.sort(reverse=True, key=lambda x: x[0])
# トークン数が上限に達するまで結合
result_text = ""
estimated_length = 0
for score, sent in scored_sentences:
sent_with_period = sent + '。'
if estimated_length + len(sent_with_period) <= max_tokens_estimate * 0.75: # 文字数で概算
result_text += sent_with_period
estimated_length += len(sent_with_period)
else:
break
return result_text
# 使用例
long_document = "非常に長いドキュメントのテキスト..."
important_keywords = ["エラー", "解決", "設定", "API"]
truncated = truncate_text_by_importance(long_document, 3000, important_keywords)
print(f"要約後: {len(truncated)} 文字")
方法2: MapReduce(分割統治)アプローチ
長いドキュメントを小さなチャンクに分割し、それぞれを個別に処理した後、結果を統合します。要約タスクに特に有効です。
# LangChain を利用した MapReduce の概念実装例
from langchain import OpenAI, PromptTemplate
from langchain.chains.summarize import load_summarize_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 1. テキストをチャンクに分割
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=2000, # 各チャンクのサイズ(文字数)
chunk_overlap=200, # チャンク間のオーバーラップ(文脈の連続性を保つ)
length_function=len,
)
texts = text_splitter.split_text(very_long_document)
# 2. 各チャンクを要約する「Map」プロンプト
map_prompt = """
以下のテキストを簡潔に要約してください:
{text}
要約:
"""
map_prompt_template = PromptTemplate(template=map_prompt, input_variables=["text"])
# 3. 部分要約を統合する「Reduce」プロンプト
combine_prompt = """
以下の複数の要約を統合し、全体として一貫性のある最終要約を作成してください:
{text}
最終要約:
"""
combine_prompt_template = PromptTemplate(template=combine_prompt, input_variables=["text"])
# 4. チェーンの実行(実際にはLLMの初期化等が必要)
# chain = load_summarize_chain(llm, chain_type="map_reduce",
# map_prompt=map_prompt_template,
# combine_prompt=combine_prompt_template)
# summary = chain.run(texts)
方法3: 階層的ナビゲーションと検索(RAGの基本)
Retrieval-Augmented Generation(RAG)の考え方を用います。全てのテキストをプロンプトに入れるのではなく、質問に関連する部分だけをベクトル検索で取得し、コンテキストとして提供します。
# 簡易版RAGの流れ(コードスケッチ)
# 1. ドキュメントをチャンクに分割し、埋め込みベクトルを生成して保存
# 2. ユーザークエリの埋め込みベクトルを計算
# 3. コサイン類似度等で最も関連性の高いチャンクを検索(例: 上位3件)
# 4. 検索されたチャンクのみをコンテキストとしてLLMに渡す
# 疑似コード例
relevant_chunks = vector_store.similarity_search(user_query, k=3)
context = "nn".join([chunk.page_content for chunk in relevant_chunks])
final_prompt = f"""
以下のコンテキスト情報に基づいて質問に答えてください。
コンテキスト:
{context}
質問: {user_query}
回答:
"""
# final_prompt をLLMに送信
この方法では、ベクトルデータベース(ChromaDB, Pinecone, FAISS等)と埋め込みモデル(OpenAI embeddings, sentence-transformers等)のセットアップが必要になります。
方法4: コンテキストの圧縮を指示するプロンプトエンジニアリング
モデル自身に、与えられた長いコンテキストから重要な要素を抽出・保持するよう指示する方法です。
# 長いチャット履歴を圧縮するプロンプト例
compress_prompt = """
これから会話の履歴を送ります。あなたはこの履歴から、会話の流れ、決定事項、重要な事実関係を失わないようにしながら、可能な限りコンパクトに要約してください。
圧縮された履歴は、未来のあなた(または別のAI)が会話を適切に継続できるだけの情報を含んでいる必要があります。
### 会話履歴 ###
{chat_history}
### 圧縮された会話履歴 ###
"""
# 圧縮された履歴を次の会話のシステムメッセージ等に埋め込んで使用
方法5: 長いコンテキストをネイティブにサポートするモデル・手法の採用
根本的な解決策として、コンテキスト長の長いモデルや技術を選択します。
- モデルの選択: GPT-4-128K (128,000トークン)、Claude-3 (200,000トークン)、Mistral Large (32,000トークン)など、大きなコンテキスト長をサポートするモデルを利用する。
- アーキテクチャの選択: ローカルで実行する場合、コンテキスト長を拡張する技術を実装したモデルを選ぶ。例: YaRN (Yet another RoPE extensioN method) や NTK-aware scaling を適用したLlama 2の派生モデル。これらは、微調整なしで最大8倍程度のコンテキスト長への推論を可能にします。
# transformersライブラリで、長いコンテキスト対応のモデルをロードする例(概念)
# from transformers import AutoModelForCausalLM, AutoTokenizer
# model_name = "NousResearch/Yarn-Llama-2-7b-64k" # YaRN適用済みモデルの例
# model = AutoModelForCausalLM.from_pretrained(model_name)
# tokenizer = AutoTokenizer.from_pretrained(model_name)
# 推論時は通常通りだが、長いシーケンスを入力可能
# inputs = tokenizer(long_text, return_tensors="pt", truncation=True, max_length=64000)
まとめ・補足情報
LLMのコンテキスト長制限は避けて通れない課題ですが、適切な戦略を選ぶことで効果的に対処できます。
選択ガイド
- コストと実装容易性を優先: 方法1(戦略的切り捨て)または方法4(プロンプト圧縮)。
- 精度と網羅性を優先: 方法2(MapReduce)または方法3(RAG)。特にQ&AタスクではRAGが強力。
- 将来性と根本解決を優先: 方法5(長コンテキストモデルの採用)。APIコストやインフラ要件とのバランスを考慮。
重要な注意点
1. トークン数の正確なカウント: 実際のトークン数は使用するモデルのトークナイザーによって異なります。計画段階でライブラリ(`tiktoken` for OpenAI, `transformers` for オープンソースモデル)を使って正確にカウントする習慣をつけましょう。
2. コンテキストウィンドウの消費: プロンプトだけでなく、モデルが生成する回答(出力)もコンテキストウィンドウを消費します。`max_tokens`パラメータで出力長を適切に制御してください。
3. パフォーマンストレードオフ: コンテキストが長くなると、単純に処理コストが増えるだけでなく、モデルが関連情報を見つけづらくなり、回答の精度が低下する「中間層問題」が発生する可能性もあります。常に必要十分なコンテキストを提供する設計を心がけましょう。
本記事で紹介した手法を組み合わせることで、例えば「RAGで関連部分を検索→重要なチャンクをプロンプト圧縮→長コンテキストモデルに投入」といった高度なワークフローも構築可能です。プロジェクトの要件とリソースに合わせて、最適な対処法を選択してください。