7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コンテキストウィンドウってなんだ?〜LLMの「記憶の限界」を理解して最適なモデル選びと実装をする完全ガイド〜

7
Posted at

この記事の対象読者

  • LLMを使ったアプリケーションを開発している方
  • 「コンテキスト長が足りない」問題に直面したことがある方
  • 2026年のモデル選定で適切なコンテキストウィンドウを選びたい方

この記事で得られること

  • コンテキストウィンドウの正体: トークン化の仕組みから「なぜ上限があるのか」まで
  • 2026年主要モデル比較: 公称値だけでなく「実効性能」まで含めた実務的な判断基準
  • 長文処理の実装パターン: コンテキスト溢れを防ぐ3つの設計戦略とコード例

この記事で扱わないこと

  • Transformerアーキテクチャのself-attention計算の数学的詳細
  • ファインチューニングによるコンテキスト拡張
  • オンプレミス環境でのモデルデプロイ

1. コンテキストウィンドウとの出会い

「なんで途中から話が噛み合わなくなるんだ?」

私が最初にこの問題にぶつかったのは、Claude APIで長い技術仕様書を分析させようとしたときだ。前半で与えた指示を後半では完全に忘れている。まるで金魚と話しているような気分だった。

調べてみると原因は明快だった。コンテキストウィンドウの上限を超えていたのだ。AIは「全知全能の知性」のように見えるが、実は一度に覚えていられる情報量に厳格な上限がある。

コンテキストウィンドウを一言で表すなら、「AIの作業机の広さ」だ。机が広ければたくさんの書類を同時に広げて参照できるが、狭ければ新しい書類を置くたびに古い書類が落ちていく。そして重要なのは、机に載っているもの全てに対して料金が発生するということだ。

ここまでで、コンテキストウィンドウがどんなものか、なんとなくイメージできたでしょうか。次は、この記事で使う用語を整理しておきましょう。


2. 前提知識の確認

本題に入る前に、この記事で登場する用語を確認します。

2.1 トークンとは

LLMがテキストを処理する最小単位。英語では1単語がおおよそ1〜1.5トークン、日本語ではひらがな1文字が約1トークン、漢字1文字が約1〜2トークンになる。「コンテキストウィンドウ」の大きさはこのトークン数で測られる。

具体的な目安を表にすると、こうなる。

テキスト量 おおよそのトークン数
日本語1文字(ひらがな) 約1トークン
日本語1000文字 約700〜1,500トークン
英語の一般的な1ページ 約250〜300トークン
長編小説1冊(10万字) 約70,000〜150,000トークン
Qiita記事1本(平均的な長さ) 約2,000〜5,000トークン

2.2 入力トークンと出力トークンの違い

コンテキストウィンドウは「入力 + 出力」の合計で消費される。入力10万トークン + 出力5万トークン = 合計15万トークン使用。これが上限を超えると、モデルは応答できなくなるか、古い情報を「忘れる」。

さらに重要なのは料金体系だ。ほとんどのモデルで出力トークンは入力トークンの3〜10倍高い。Gemini 3.1 Proの場合、入力$2/1M に対して出力$12/1M、つまり出力は入力の6倍のコストだ。

2.3 コンテキストウィンドウと「記憶」の違い

よくある誤解だが、コンテキストウィンドウは「長期記憶」ではない。会話が終われば全てリセットされる。いわば「ワーキングメモリ(作業記憶)」であり、人間で言えば「今この瞬間、頭の中に置いておける情報量」に近い。前の会話を覚えているように見えるのは、会話履歴をまるごと入力トークンとして毎回送り直しているからだ。

2.4 Lost in the Middle問題とは

コンテキストウィンドウが大きくても、中間部分の情報は正確に取り出しにくいという現象。テストでは冒頭と末尾の情報は85〜95%の精度で取得できるのに対し、中間部分は76〜82%に低下する。公称100万トークンのモデルでも、全域で均一に「覚えている」わけではないのだ。

これらの用語が押さえられたら、コンテキストウィンドウの背景を見ていきましょう。


3. コンテキストウィンドウが進化した背景

3.1 初期のLLM — 2K〜4Kの世界

2022年のChatGPT(GPT-3.5)のコンテキストウィンドウは約4,096トークンだった。日本語で2,000〜3,000文字程度。長い文章を分析させようとすると、すぐに限界に達した。「もう少し前の話を覚えてて」が通じない時代だ。

3.2 128Kの衝撃

2023年末〜2024年にかけて、GPT-4 Turbo(128K)やClaude 3(200K)が登場。一気に数十倍のコンテキストウィンドウを手に入れた。これにより、書籍1冊分のテキストを一度に処理できるようになり、RAG(検索拡張生成)に頼らない「全文投入」というアプローチが現実的になった。

3.3 2026年 — 100万トークン時代

2026年現在、Gemini 3.1 Proは100万トークン、Llama 4 Scoutに至っては1,000万トークンを公称している。コードベース全体、論文100本、動画の書き起こし全文……かつては不可能だった規模のデータを一度に処理できる時代になった。

しかし「大きければいいのか?」という問いには、次のセクションで答える。

背景がわかったところで、基本的な仕組みを見ていきましょう。


4. 基本概念と仕組み

4.1 2026年2月 主要モデル コンテキストウィンドウ比較

注意: 公称値と実効性能は異なる。「200Kトークン対応」でも、実際に200K全域で均一な精度が出るとは限らない。

モデル 入力上限 出力上限 実効精度が維持される範囲 入力単価 ($/1M)
Gemini 3.1 Pro 1M 65K ネイティブマルチモーダル対応 $2.00
Claude Opus 4.6 200K(1Mベータ) 64K 200K以内で5%未満の精度劣化 $5.00
Claude Sonnet 4.6 200K(1Mベータ) 64K 安定性に定評 $3.00
GPT-5.3 Codex 400K 128K コーディングに特化 $1.75
GPT-5.2 400K 128K 汎用的な長文処理 $1.50
Llama 4 Scout 10M オープンソース最大 セルフホスト
Grok 2M リアルタイム検索統合 $2.00

4.2 「大きさ」より「使い方」が重要な理由

3つの現実を押さえておこう。

現実1: コストは線形に増加する

コンテキストに入れるトークンが増えれば、入力コストはそのまま増える。100万トークンをGemini 3.1 Proに入れると、入力だけで$2かかる。1日100回呼べば$200/日だ。

現実2: レイテンシも増加する

コンテキストが長くなるほど、最初のトークンが返ってくるまでの時間(TTFT: Time To First Token)が長くなる。ユーザー体験に直結する問題だ。

現実3: 中間部分は「忘れる」

先述の「Lost in the Middle」問題。100万トークン入れても、真ん中あたりの情報は正確に取り出せない可能性がある。重要な情報は冒頭か末尾に配置するのがベストプラクティスだ。

4.3 コンテキスト管理の3つの戦略

戦略 概要 最適なケース
全文投入 データを全部コンテキストに入れる データ < コンテキスト上限の50%
RAG 関連部分だけ検索して投入 大量のデータ、検索精度が重要
チャンク分割 データを分割して順番に処理 要約、翻訳など順序が重要なタスク

基本概念が理解できたところで、実際にコードを書いて動かしてみましょう。


5. 実践:コンテキストウィンドウを賢く使う

5.1 環境構築

# Gemini SDK(コンテキスト管理のデモ用)
pip install google-genai>=1.51.0

# トークンカウント用
pip install tiktoken

# APIキーの設定
export GEMINI_API_KEY="your-api-key-here"

5.2 環境別の設定ファイル

以下の3種類の設定ファイルを用意しました。コンテキスト管理戦略を環境ごとに定義しています。

開発環境用(config.yaml)

# config.yaml - 開発環境用
context:
  model: "gemini-3.1-pro-preview"
  max_input_tokens: 100000      # 開発時はコスト抑制で10万に制限
  max_output_tokens: 8192
  strategy: "full_context"       # 開発時は全文投入で手軽に

safety:
  warn_threshold_percent: 70     # 70%消費で警告
  hard_limit_percent: 90         # 90%でエラー

logging:
  level: "DEBUG"
  log_token_usage: true

本番環境用(config.production.yaml)

# config.production.yaml - 本番環境用
context:
  model: "gemini-3.1-pro-preview"
  max_input_tokens: 500000       # 本番は50万まで許容
  max_output_tokens: 65536
  strategy: "rag"                # 本番はRAGでコスト最適化

safety:
  warn_threshold_percent: 60
  hard_limit_percent: 80

cost_control:
  monthly_budget_usd: 1000
  per_request_limit_tokens: 200000

logging:
  level: "INFO"
  log_token_usage: true

テスト環境用(config.test.yaml)

# config.test.yaml - CI/CD用
context:
  model: "gemini-3.1-pro-preview"
  max_input_tokens: 10000       # テストは1万で十分
  max_output_tokens: 1024
  strategy: "chunk"             # テストはチャンク分割

safety:
  warn_threshold_percent: 80
  hard_limit_percent: 95

logging:
  level: "WARNING"
  log_token_usage: false

5.3 トークン数の計測と管理

"""
コンテキストウィンドウ管理ユーティリティ
実行方法: python context_manager.py
前提条件: pip install google-genai>=1.51.0 tiktoken
"""
import tiktoken


class ContextWindowManager:
    """コンテキストウィンドウの使用量を管理するクラス"""

    # 2026年2月時点の主要モデルのコンテキスト上限
    MODEL_LIMITS = {
        "gemini-3.1-pro-preview": {"input": 1_000_000, "output": 65_536},
        "claude-opus-4-6":        {"input": 200_000, "output": 64_000},
        "claude-sonnet-4-6":      {"input": 200_000, "output": 64_000},
        "gpt-5.3-codex":          {"input": 400_000, "output": 128_000},
        "gpt-5.2":                {"input": 400_000, "output": 128_000},
    }

    def __init__(self, model: str = "gemini-3.1-pro-preview"):
        self.model = model
        self.limits = self.MODEL_LIMITS.get(model, {"input": 128_000, "output": 4_096})

    def count_tokens(self, text: str) -> int:
        """テキストのトークン数を概算"""
        # 日本語テキストの簡易推定
        # 実運用ではモデル固有のトークナイザーを使うべき
        jp_chars = sum(1 for c in text if ord(c) > 0x3000)
        en_words = len(text.split()) - jp_chars // 2
        return int(jp_chars * 1.2 + en_words * 1.3)

    def check_fits(self, text: str, reserved_output: int = 4096) -> dict:
        """テキストがコンテキストウィンドウに収まるか確認"""
        input_tokens = self.count_tokens(text)
        available = self.limits["input"] - reserved_output
        fits = input_tokens <= available
        usage_percent = (input_tokens / available) * 100

        return {
            "fits": fits,
            "input_tokens": input_tokens,
            "available_tokens": available,
            "usage_percent": round(usage_percent, 1),
            "remaining_tokens": available - input_tokens,
        }

    def estimate_cost(self, input_tokens: int, output_tokens: int) -> dict:
        """コストを概算"""
        # Gemini 3.1 Proの料金
        PRICES = {
            "gemini-3.1-pro-preview": {"input": 2.0, "output": 12.0},
            "claude-opus-4-6":        {"input": 5.0, "output": 25.0},
            "claude-sonnet-4-6":      {"input": 3.0, "output": 15.0},
            "gpt-5.3-codex":          {"input": 1.75, "output": 14.0},
        }
        prices = PRICES.get(self.model, {"input": 2.0, "output": 12.0})

        input_cost = (input_tokens / 1_000_000) * prices["input"]
        output_cost = (output_tokens / 1_000_000) * prices["output"]

        return {
            "input_cost": round(input_cost, 4),
            "output_cost": round(output_cost, 4),
            "total_cost": round(input_cost + output_cost, 4),
        }


def demo():
    """デモ: テキストがコンテキストウィンドウに収まるか確認"""
    manager = ContextWindowManager("gemini-3.1-pro-preview")

    texts = {
        "短い質問": "Pythonでソートする方法を教えてください。",
        "中程度の文書": "" * 10_000,  # 約1万文字
        "大きな文書": "" * 500_000,   # 約50万文字
    }

    for name, text in texts.items():
        result = manager.check_fits(text)
        cost = manager.estimate_cost(result["input_tokens"], 2000)

        status = "" if result["fits"] else ""
        print(f"{status} {name}")
        print(f"   トークン数: {result['input_tokens']:,}")
        print(f"   使用率: {result['usage_percent']}%")
        print(f"   推定コスト: ${cost['total_cost']}")
        print()


if __name__ == "__main__":
    print("=" * 50)
    print("コンテキストウィンドウ管理デモ")
    print(f"モデル: gemini-3.1-pro-preview(上限: 1,000,000トークン)")
    print("=" * 50)
    print()
    demo()

5.4 チャンク分割による長文処理

"""
コンテキスト溢れを防ぐチャンク分割処理
長い文書を安全に分割してLLMに処理させる
"""
from google import genai
from google.genai import types


def split_into_chunks(text: str, max_tokens: int = 50_000, overlap: int = 2_000) -> list[str]:
    """テキストをオーバーラップ付きでチャンク分割"""
    # 簡易的に1文字≒1トークンとして分割(日本語)
    chunks = []
    start = 0
    while start < len(text):
        end = min(start + max_tokens, len(text))
        chunks.append(text[start:end])
        start = end - overlap  # オーバーラップで文脈を維持
        if start >= len(text):
            break
    return chunks


def summarize_long_document(document: str) -> str:
    """長い文書をチャンク分割して要約"""
    client = genai.Client()
    chunks = split_into_chunks(document, max_tokens=50_000)

    print(f"文書を {len(chunks)} チャンクに分割")

    # 各チャンクを要約
    chunk_summaries = []
    for i, chunk in enumerate(chunks):
        response = client.models.generate_content(
            model="gemini-3.1-pro-preview",
            contents=f"以下のテキストを500文字以内で要約してください:\n\n{chunk}",
            config=types.GenerateContentConfig(
                thinking_config=types.ThinkingConfig(thinking_level="LOW"),
                max_output_tokens=1024,
            ),
        )
        chunk_summaries.append(response.text)
        print(f"  チャンク {i + 1}/{len(chunks)} 完了")

    # 要約を統合
    combined = "\n\n".join(chunk_summaries)
    response = client.models.generate_content(
        model="gemini-3.1-pro-preview",
        contents=f"以下の複数の要約を統合し、全体の要約を作成してください:\n\n{combined}",
        config=types.GenerateContentConfig(
            thinking_config=types.ThinkingConfig(thinking_level="MEDIUM"),
            max_output_tokens=2048,
        ),
    )

    return response.text


if __name__ == "__main__":
    # 長い文書のサンプル
    long_doc = "これは非常に長い技術文書です。" * 20_000  # 約30万文字
    print(f"文書の長さ: {len(long_doc):,} 文字")
    summary = summarize_long_document(long_doc)
    print(f"\n最終要約:\n{summary}")

5.5 コンテキスト使用量の可視化

"""
会話のコンテキスト使用量をリアルタイムで表示する
"""
from google import genai
from google.genai import types


def chat_with_context_monitor():
    """コンテキスト使用量を監視しながらチャット"""
    client = genai.Client()
    history = []
    context_limit = 1_000_000

    print("=" * 50)
    print("コンテキストモニター付きチャット")
    print("'quit' で終了")
    print("=" * 50)

    while True:
        user_input = input("\nあなた: ")
        if user_input.lower() == "quit":
            break

        history.append({"role": "user", "content": user_input})

        # 全履歴をテキスト化してトークン概算
        all_text = " ".join(msg["content"] for msg in history)
        estimated_tokens = len(all_text)  # 簡易推定

        # コンテキスト使用率を表示
        usage_percent = (estimated_tokens / context_limit) * 100
        bar_length = 30
        filled = int(bar_length * usage_percent / 100)
        bar = "" * filled + "" * (bar_length - filled)

        if usage_percent > 80:
            color_note = "  ⚠️ 80%超過 — 古い会話の要約を検討してください"
        elif usage_percent > 50:
            color_note = "  📊 50%超過"
        else:
            color_note = ""

        print(f"  コンテキスト: [{bar}] {usage_percent:.1f}%{color_note}")

        # API呼び出し
        response = client.models.generate_content(
            model="gemini-3.1-pro-preview",
            contents=[
                types.Content(role=msg["role"], parts=[types.Part(text=msg["content"])])
                for msg in history
            ],
            config=types.GenerateContentConfig(
                thinking_config=types.ThinkingConfig(thinking_level="LOW"),
                max_output_tokens=2048,
            ),
        )

        assistant_reply = response.text
        history.append({"role": "model", "content": assistant_reply})
        print(f"\nAI: {assistant_reply}")


if __name__ == "__main__":
    chat_with_context_monitor()

5.6 実行結果

$ python context_manager.py
==================================================
コンテキストウィンドウ管理デモ
モデル: gemini-3.1-pro-preview(上限: 1,000,000トークン)
==================================================

✅ 短い質問
   トークン数: 22
   使用率: 0.0%
   推定コスト: $0.024

✅ 中程度の文書
   トークン数: 12,000
   使用率: 1.2%
   推定コスト: $0.048

✅ 大きな文書
   トークン数: 600,000
   使用率: 60.2%
   推定コスト: $1.224

5.7 よくあるエラーと対処法

エラー/症状 原因 対処法
context_length_exceeded 入力+出力がモデル上限を超過 入力を削減するか、max_output_tokens を下げる
応答が途中で切れる max_output_tokens が小さすぎる 出力上限を増やす(最大65,536)
後半の質問に正しく答えない Lost in the Middle問題 重要な指示を冒頭か末尾に配置
コストが想定より高い 会話履歴の蓄積 古い履歴を要約して圧縮、または定期的にリセット
レスポンスが遅い 大量のコンテキスト チャンク分割 or RAG戦略に切り替え
日本語のトークン数が読めない 日英でトークン効率が異なる モデル固有のトークナイザーで正確に計測

5.8 環境診断スクリプト

#!/usr/bin/env python3
"""
コンテキストウィンドウ環境診断スクリプト
実行方法: python check_context_env.py
"""
import sys
import os


def check_environment():
    """環境チェック + モデル別コンテキスト情報"""
    issues = []
    info = []

    # Python確認
    if sys.version_info < (3, 9):
        issues.append(f"Python 3.9以上が必要(現在: {sys.version}")
    else:
        info.append(f"Python: {sys.version.split()[0]}")

    # SDK確認
    try:
        import google.genai
        info.append(f"google-genai: {getattr(google.genai, '__version__', '不明')}")
    except ImportError:
        issues.append("google-genai 未インストール")

    # tiktoken確認
    try:
        import tiktoken
        info.append("tiktoken: インストール済み")
    except ImportError:
        issues.append("tiktoken 未インストール → pip install tiktoken")

    # APIキー
    if os.environ.get("GEMINI_API_KEY"):
        info.append("GEMINI_API_KEY: 設定済み")
    else:
        issues.append("GEMINI_API_KEY: 未設定")

    # モデル比較表示
    print("=" * 60)
    print("2026年2月 主要LLM コンテキストウィンドウ一覧")
    print("=" * 60)
    print()
    print(f"  {'モデル':<25} {'入力上限':>12} {'出力上限':>10} {'$/1M入力':>10}")
    print(f"  {'-' * 25} {'-' * 12} {'-' * 10} {'-' * 10}")

    models = [
        ("Gemini 3.1 Pro", "1,000,000", "65,536", "$2.00"),
        ("Claude Opus 4.6", "200,000", "64,000", "$5.00"),
        ("Claude Sonnet 4.6", "200,000", "64,000", "$3.00"),
        ("GPT-5.3 Codex", "400,000", "128,000", "$1.75"),
        ("GPT-5.2", "400,000", "128,000", "$1.50"),
        ("Llama 4 Scout", "10,000,000", "", "セルフホスト"),
        ("Grok", "2,000,000", "", "$2.00"),
    ]

    for name, inp, out, price in models:
        print(f"  {name:<25} {inp:>12} {out:>10} {price:>10}")

    print()

    # 結果表示
    print("=" * 60)
    print("環境診断結果")
    print("=" * 60)

    if info:
        print("\n[情報]")
        for i in info:
            print(f"{i}")

    if issues:
        print("\n[問題]")
        for issue in issues:
            print(f"{issue}")
    else:
        print("\n🎉 環境は正常です!")


if __name__ == "__main__":
    check_environment()

実装方法がわかったので、次は具体的なユースケースを見ていきます。


6. ユースケース別ガイド

6.1 ユースケース1: 大規模コードベースの全文投入

想定読者: リポジトリ全体をAIに読ませてリファクタリングしたい開発者

推奨構成: Gemini 3.1 Pro(100万トークン)+ 全文投入戦略

なぜこのモデルか: 100万トークンのコンテキストウィンドウにより、数万行規模のコードベースを分割せずに投入できる。入力コストも$2/1Mトークンと安価。

サンプルコード:

"""
コードベース全文投入 → アーキテクチャ分析
"""
from pathlib import Path
from google import genai
from google.genai import types


def load_codebase(project_dir: str, max_tokens: int = 500_000) -> str:
    """プロジェクトのソースコードを1つのテキストに結合"""
    extensions = {".py", ".ts", ".js", ".yaml", ".json", ".toml"}
    ignore_dirs = {"node_modules", ".git", "__pycache__", ".venv", "dist"}

    files_text = []
    total_chars = 0

    for path in sorted(Path(project_dir).rglob("*")):
        if any(d in path.parts for d in ignore_dirs):
            continue
        if path.suffix not in extensions:
            continue

        try:
            content = path.read_text(encoding="utf-8")
            header = f"\n{'='*60}\n# FILE: {path.relative_to(project_dir)}\n{'='*60}\n"
            files_text.append(header + content)
            total_chars += len(content)

            if total_chars > max_tokens:  # 簡易的な上限チェック
                print(f"  ⚠️ {max_tokens:,}文字を超えたため読み込み停止")
                break
        except (UnicodeDecodeError, PermissionError):
            continue

    print(f"  読み込みファイル数: {len(files_text)}")
    print(f"  合計文字数: {total_chars:,}")
    return "\n".join(files_text)


def analyze_architecture(codebase: str) -> str:
    """コードベースのアーキテクチャを分析"""
    client = genai.Client()

    response = client.models.generate_content(
        model="gemini-3.1-pro-preview",
        contents=f"""以下のコードベース全体を分析し、レポートを作成してください:

1. 全体アーキテクチャの概要図(テキスト形式)
2. モジュール間の依存関係
3. SOLID原則の遵守状況
4. 改善提案(優先度順)

{codebase}""",
        config=types.GenerateContentConfig(
            thinking_config=types.ThinkingConfig(thinking_level="HIGH"),
            max_output_tokens=16384,
        ),
    )
    return response.text


if __name__ == "__main__":
    code = load_codebase("./my-project")
    report = analyze_architecture(code)
    print(report)

6.2 ユースケース2: 長い会話履歴の効率的な管理

想定読者: チャットボットの会話が長くなるとコストが爆発する問題を抱える開発者

推奨構成: 履歴の自動要約 + スライディングウィンドウ

サンプルコード:

"""
会話履歴の自動圧縮(コンテキスト溢れ防止)
"""
from google import genai
from google.genai import types


class SmartChatHistory:
    """コンテキスト上限を意識した会話履歴管理"""

    def __init__(self, max_history_tokens: int = 50_000):
        self.messages: list[dict] = []
        self.summary: str = ""
        self.max_tokens = max_history_tokens

    def _estimate_tokens(self) -> int:
        """現在の履歴のトークン数を概算"""
        all_text = self.summary + " ".join(m["content"] for m in self.messages)
        return len(all_text)  # 日本語: おおよそ1文字≒1トークン

    def _compress_history(self):
        """古い会話を要約して圧縮"""
        if len(self.messages) < 6:
            return

        client = genai.Client()

        # 古い半分を要約
        half = len(self.messages) // 2
        old_messages = self.messages[:half]
        old_text = "\n".join(
            f"{m['role']}: {m['content']}" for m in old_messages
        )

        response = client.models.generate_content(
            model="gemini-3.1-pro-preview",
            contents=f"以下の会話履歴を200文字以内で要約してください:\n\n{old_text}",
            config=types.GenerateContentConfig(
                thinking_config=types.ThinkingConfig(thinking_level="LOW"),
                max_output_tokens=512,
            ),
        )

        self.summary = f"[過去の会話の要約: {response.text}]"
        self.messages = self.messages[half:]
        print(f"  📦 会話履歴を圧縮({half}メッセージ → 要約)")

    def add_message(self, role: str, content: str):
        """メッセージを追加(必要に応じて圧縮)"""
        self.messages.append({"role": role, "content": content})

        if self._estimate_tokens() > self.max_tokens:
            self._compress_history()

    def get_context(self) -> list[dict]:
        """API送信用のコンテキストを取得"""
        context = []
        if self.summary:
            context.append({"role": "user", "content": self.summary})
        context.extend(self.messages)
        return context


# 使用例
if __name__ == "__main__":
    history = SmartChatHistory(max_history_tokens=5_000)

    messages = [
        "Pythonの基本を教えて",
        "変数の型について詳しく",
        "リスト内包表記の使い方は?",
        "辞書型の操作方法を教えて",
        "クラスの定義方法は?",
        "継承について教えて",
        "デコレータってなに?",
        "非同期処理の書き方は?",
    ]

    for msg in messages:
        history.add_message("user", msg)
        history.add_message("model", f"{msg}への回答...)" * 50)

    ctx = history.get_context()
    print(f"\n最終コンテキスト: {len(ctx)} メッセージ")

6.3 ユースケース3: RAGとコンテキストウィンドウの組み合わせ

想定読者: 大量のドキュメントからAIに回答させたいが、全文投入ではコストが高すぎる開発者

推奨構成: ベクトル検索(RAG)で関連部分だけをコンテキストに投入

サンプルコード:

"""
簡易RAG: 関連チャンクだけをコンテキストに投入
"""
from google import genai
from google.genai import types


def simple_rag(documents: list[str], query: str, top_k: int = 3) -> str:
    """キーワードベースの簡易RAG(ベクトルDB不要版)"""
    client = genai.Client()

    # Step 1: クエリに関連するチャンクを選択(簡易キーワードマッチ)
    scored = []
    query_words = set(query.lower().split())
    for i, doc in enumerate(documents):
        doc_words = set(doc.lower().split())
        overlap = len(query_words & doc_words)
        scored.append((overlap, i, doc))

    scored.sort(reverse=True)
    relevant = [doc for _, _, doc in scored[:top_k]]

    # Step 2: 選択されたチャンクだけをコンテキストに投入
    context = "\n\n---\n\n".join(relevant)
    total_tokens = len(context)
    print(f"  関連チャンク: {top_k}/{len(documents)} 個を選択")
    print(f"  投入トークン: 約{total_tokens:,}(全文の{total_tokens * 100 // sum(len(d) for d in documents)}%)")

    response = client.models.generate_content(
        model="gemini-3.1-pro-preview",
        contents=f"""以下の参考情報をもとに質問に回答してください。
参考情報にない内容は「情報がありません」と回答してください。

【参考情報】
{context}

【質問】
{query}""",
        config=types.GenerateContentConfig(
            thinking_config=types.ThinkingConfig(thinking_level="MEDIUM"),
            max_output_tokens=2048,
        ),
    )

    return response.text


if __name__ == "__main__":
    # ドキュメントのサンプル(実運用ではDBから取得)
    docs = [
        "Python 3.12ではf-string内でバックスラッシュが使えるようになった。",
        "TypeScriptの型推論はVersion 5.0で大幅に改善された。",
        "Rustのborrow checkerは所有権システムの中核を成す。",
        "Pythonのasyncioは非同期I/Oを実現するための標準ライブラリ。",
        "Go言語のgoroutineは軽量スレッドとして並行処理を実現する。",
    ]
    answer = simple_rag(docs, "Pythonの新機能について教えて")
    print(f"\n回答: {answer}")

ユースケースを把握できたところで、この先の学習パスを確認しましょう。


7. 学習ロードマップ

この記事を読んだ後、次のステップとして以下をおすすめします。

初級者向け(まずはここから)

  1. OpenAI Tokenizer で実際にテキストがどうトークン化されるか体験
  2. 自分がよく使うLLMのコンテキスト上限と料金を確認
  3. 短い会話で usage_metadata のトークン数を確認してみる

中級者向け(実践に進む)

  1. 会話履歴の要約・圧縮を実装してコスト最適化
  2. RAG(検索拡張生成)を導入し、必要な情報だけをコンテキストに投入
  3. Gemini Context Caching で繰り返し入力のコストを75%削減

上級者向け(さらに深く)

  1. Lost in the Middle問題を検証し、プロンプト内の情報配置を最適化
  2. コンテキストウィンドウ全域でのリトリーバル精度をベンチマーク
  3. マルチモーダル入力(画像+テキスト)時のトークン消費パターンを分析

8. まとめ

この記事では、コンテキストウィンドウについて以下を解説しました:

  1. 「AIの作業机」としての直感的理解 — トークン化の仕組み、入出力の区別、コストとの関係
  2. 2026年モデル比較 — Gemini 100万、Claude 200K、GPT-5 400K、Llama 4 1000万の使い分け
  3. 3つの実装戦略 — 全文投入、チャンク分割、RAGのコード例と使い分けの判断基準

私の所感

コンテキストウィンドウは、「大きければ大きいほどいい」とは限らない。100万トークンのモデルにQiita記事1本(約3,000トークン)を投入しても、99.7%のスペースは無駄になるだけでなく、料金は発生しないが「大きなモデルの方が高い」という相関がある。

最も実務的な教訓は、自分のユースケースに必要なコンテキストサイズを正確に見積もることだ。10万文字の技術仕様書を処理するなら100万トークンが必要。日常的なQ&Aなら128Kで十分。そして大半のユースケースでは、RAGやチャンク分割を使えば、巨大なコンテキストウィンドウに頼らなくても問題は解決できる。

「Lost in the Middle」問題があるかぎり、コンテキストを大きくするだけでは品質は上がらない。重要な情報の配置、不要な情報の除外、定期的な履歴圧縮——こうした「コンテキストエンジニアリング」こそが、これからのLLMアプリケーション開発で差がつくスキルだと思う。


参考文献


この記事が参考になったら、いいね・ストックをお願いします!
他にもAI/ML関連の記事を書いています。よければフォローもお願いします。

Xでも技術情報を発信しています → https://x.com/geneLab_999

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?