LLMのトークン制限と戦う – コンテキスト管理の実践テクニック

LLMを本格的に使い始めると必ず直面するのがトークン制限の壁だ。「長いコードを渡したら途中で切れた」「会話が長くなると前の内容を忘れられた」という経験は誰でもある。実践で試してきた対処法をまとめる。

トークンの基礎知識

まず数字を把握しておく。

| モデル | コンテキスト長 | 目安の文字数(日本語) |

|——–|————–|———————|

| GPT-4o | 128K tokens | 約96,000文字 |

| Claude 3.7 Sonnet | 200K tokens | 約150,000文字 |

| Gemini 1.5 Pro | 1M tokens | 約750,000文字 |

日本語は英語と比べてトークン効率が低く、同じ内容でも英語の1.5〜2倍のトークンを使うことが多い。

トークン消費の概算:

  • コード1行 ≈ 5〜15トークン
  • 日本語1文字 ≈ 1〜3トークン
  • 英語1単語 ≈ 1〜2トークン
  • テクニック1: 必要な部分だけを渡す

    よくやりがちな失敗が「全ファイルをそのまま渡す」こと。

    # NG: ファイル全体を渡す
    with open("large_module.py", "r") as f:
        content = f.read()
    prompt = f"このコードを修正してください:\n{content}"
    
    # OK: 関連する部分だけを渡す
    relevant_section = extract_relevant_section("large_module.py", start_line=120, end_line=180)
    prompt = f"以下の関数を修正してください(large_module.py 120-180行):\n{relevant_section}"

    「修正したい関数だけ渡す」「エラーが出ているクラスだけ渡す」という絞り込みをするだけでトークンを大幅に節約できる。

    テクニック2: コンテキストを要約する

    長い会話を続けていると、古いやり取りがトークンを圧迫する。重要な情報は要約して引き継ぐ。

    def summarize_conversation(conversation: list[dict]) -> str:
        """会話履歴を要約する"""
        client = anthropic.Anthropic()
    
        conv_text = "\n".join(
            f"{msg['role']}: {msg['content'][:200]}"  # 各発言は200文字まで
            for msg in conversation
        )
    
        message = client.messages.create(
            model="claude-3-haiku-20240307",  # 要約には安い・速いモデルを使う
            max_tokens=256,
            messages=[{
                "role": "user",
                "content": f"以下の会話を3行以内で要約してください:\n{conv_text}"
            }]
        )
    
        return message.content[0].text
    
    
    # 会話が長くなったら要約して新しいセッションに持ち込む
    if len(conversation) > 20:
        summary = summarize_conversation(conversation)
        conversation = [
            {"role": "user", "content": f"前回の会話の要約: {summary}\n続きをお願いします。"}
        ]

    テクニック3: 不要な情報を除去する

    コードを渡すとき、コメントやドキュメントストリングを削除するだけでトークンを30〜50%削減できることがある。

    import ast
    import textwrap
    
    def strip_docstrings(source_code: str) -> str:
        """Pythonコードからdocstringを除去する"""
        try:
            tree = ast.parse(source_code)
        except SyntaxError:
            return source_code  # パースできない場合はそのまま返す
    
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Module)):
                if (node.body and isinstance(node.body[0], ast.Expr) and
                        isinstance(node.body[0].value, ast.Constant)):
                    node.body.pop(0)  # docstringを削除
    
        return ast.unparse(tree)
    
    
    def remove_comments(code: str) -> str:
        """コードからコメントを除去する(シンプルな実装)"""
        lines = []
        for line in code.split("\n"):
            # インラインコメントを除去(文字列内の#は対象外にしたい場合は正規表現が必要)
            if "#" in line:
                stripped = line.split("#")[0].rstrip()
                if stripped:
                    lines.append(stripped)
            else:
                lines.append(line)
        return "\n".join(lines)

    ただし、コメントが意図の説明として重要な場合は除去しない。「バグを修正してほしい」用途ではコメントを残すのが良いこともある。

    テクニック4: 階層的なコンテキスト管理

    大きなプロジェクトを扱う場合、「全体の概要」と「詳細」を分けて管理する。

    PROJECT_OVERVIEW = """
    プロジェクト: WordPress自動投稿システム
    言語: Python 3.12
    主要ファイル:
    - scripts/utils/wp_client.py: WordPress REST APIクライアント
    - scripts/posting/post.py: 記事投稿エンジン
    - scripts/posting/converter.py: Markdown→HTML変換
    .envから認証情報を読み込む(WP_URL, WP_USER, WP_APP_PASSWORD)
    """
    
    def ask_with_context(question: str, specific_code: str = "") -> str:
        """プロジェクト概要を含めて質問する"""
        context = PROJECT_OVERVIEW
        if specific_code:
            context += f"\n\n対象コード:\n```python\n{specific_code}\n```"
    
        prompt = f"{context}\n\n質問: {question}"
        # ... APIを呼ぶ

    毎回プロジェクト全体を渡すのではなく、「プロジェクトの概要(固定)」と「今回の質問に必要なコード(可変)」を組み合わせる。

    テクニック5: 出力トークンも節約する

    入力だけでなく出力のトークンもコストになる。不要な説明を省かせるよう明示する。

    # 説明が多いと出力トークンが増える
    system_bad = "コードをレビューして、問題点を詳しく説明してください。"
    
    # 問題点だけ出力させれば出力トークンが減る
    system_good = """コードをレビューしてください。
    以下の形式のみで返してください(説明不要):
    [CRITICAL] 問題の説明
    [MAJOR] 問題の説明
    [MINOR] 問題の説明
    問題なければ: OK"""

    特に自動化パイプラインでLLMを使う場合、出力形式を厳密に指定して余分なテキストを排除するだけでコストを大幅に削減できる。

    トークン使用量を事前に計算する

    渡す前にトークン数を確認しておくと安心。

    import anthropic
    
    def count_tokens(text: str) -> int:
        """テキストのトークン数を計算する"""
        client = anthropic.Anthropic()
        response = client.messages.count_tokens(
            model="claude-3-7-sonnet-20250219",
            messages=[{"role": "user", "content": text}]
        )
        return response.input_tokens
    
    # 渡す前に確認
    code = open("large_file.py").read()
    token_count = count_tokens(code)
    print(f"トークン数: {token_count:,}")  # 例: 15,234
    if token_count > 50000:
        print("警告: 大量のトークンを消費します")

    まとめ

    LLMのトークン制限と戦うための実践テクニック5つ:

    1. 必要な部分だけを渡す – 関連コードを絞り込む

    2. コンテキストを要約する – 長い会話は要約して引き継ぐ

    3. 不要な情報を除去する – docstringやコメントを状況に応じて削除

    4. 階層的なコンテキスト管理 – 概要(固定)と詳細(可変)を分ける

    5. 出力トークンも節約する – 出力形式を厳密に指定する

    コンテキスト管理を意識するだけで、同じAPIコストでできることが増える。大きなコードベースを扱うほど、この意識が重要になってくる。