AI・機械学習

コードベースはコンテキストウィンドウに収まる?トークン数を事前に計測して賢くトリミングする方法

「とりあえずリポジトリ全体をAIに貼り付けよう!」と思ったことはありませんか?🤔

実はこれ、かなり危険な操作なんですよね。コンテキストウィンドウの上限を超えると、AIは先頭のファイルを黙って切り捨てて回答します。エラーが出るならまだしも、「一部しか見ていない状態で答えが返ってくる」というのが一番怖いパターンです。

今回は「貼り付ける前にトークン数を計測して、必要なら削ぎ落とす」という実践的なアプローチをご紹介します 💡

そもそもコンテキストウィンドウって何?

token counting AI
token counting AI / Photo by Nancho via Pexels

イメージとしては「AIが一度に読める原稿用紙の枚数」です。GPT-4oなら128,000トークン、Claude 3.5 Sonnetなら200,000トークンといった上限があります。1トークンはざっくり英語で4文字、日本語だと1〜2文字が目安です。

ソースコードはコメントや変数名など英数字が多いので、日本語テキストよりはトークン消費が少なめです。ただ大規模なリポジトリになると、すぐに数十万トークンを超えることも珍しくありません。

モデル名コンテキストウィンドウ上限
GPT-4o128,000トークン
Claude 3.5 Sonnet200,000トークン
Gemini 1.5 Pro1,000,000トークン

上限を超えても「エラー」にならないモデルが多く、気づかないまま不完全な回答を受け取ってしまうことがあります。だからこそ、送る前に自分でトークン数を把握しておくことが大切です。

Step 1: APIを使わずにトークン数を推定する

ネットワーク通信なしでもかなり正確に推定できます。Pythonの tiktoken ライブラリを使えば、OpenAI系モデルのトークン数をローカルで計算可能です 🛠️

# pip install tiktoken
import tiktoken
import os

def estimate_tokens(directory: str, model: str = "gpt-4o") -> dict:
    """
    指定ディレクトリ以下のソースファイルのトークン数を計測する
    戻り値: {ファイルパス: トークン数} の辞書
    """
    enc = tiktoken.encoding_for_model(model)
    results = {}

    # 対象とする拡張子(必要に応じて追加)
    TARGET_EXTS = {".py", ".js", ".ts", ".jsx", ".tsx",
                   ".java", ".cpp", ".c", ".h", ".md", ".txt"}

    for root, dirs, files in os.walk(directory):
        # .git や node_modules は除外
        dirs[:] = [d for d in dirs if d not in {".git", "node_modules", "__pycache__", ".venv"}]

        for fname in files:
            ext = os.path.splitext(fname)[1]
            if ext not in TARGET_EXTS:
                continue

            fpath = os.path.join(root, fname)
            try:
                with open(fpath, "r", encoding="utf-8", errors="ignore") as f:
                    text = f.read()
                tokens = enc.encode(text)
                results[fpath] = len(tokens)
            except Exception as e:
                print(f"[SKIP] {fpath}: {e}")

    return results


if __name__ == "__main__":
    import sys
    target_dir = sys.argv[1] if len(sys.argv) > 1 else "."
    result = estimate_tokens(target_dir)

    total = sum(result.values())
    print(f"\n{'ファイル':<50} {'トークン数':>10}")
    print("-" * 62)
    for path, count in sorted(result.items(), key=lambda x: -x[1]):
        print(f"{path:<50} {count:>10,}")
    print("-" * 62)
    print(f"{'合計':<50} {total:>10,}")

実行例(カレントディレクトリを計測する場合):

python estimate_tokens.py ./my_project

出力はトークン数の多い順にソートされるので、「どのファイルが一番重いか」が一目でわかります。

Step 2: 上限に対して何%使っているか確認する

合計トークン数がわかったら、使いたいモデルのコンテキストウィンドウに対して何%を消費しているか計算してみましょう。

MODEL_LIMITS = {
    "gpt-4o": 128_000,
    "gpt-4o-mini": 128_000,
    "claude-3-5-sonnet": 200_000,
    "gemini-1-5-pro": 1_000_000,
}

def check_usage(total_tokens: int, model: str = "gpt-4o") -> None:
    limit = MODEL_LIMITS.get(model, 128_000)
    usage_pct = total_tokens / limit * 100

    print(f"\n=== トークン使用状況 ===")
    print(f"モデル         : {model}")
    print(f"合計トークン数 : {total_tokens:,}")
    print(f"上限           : {limit:,}")
    print(f"使用率         : {usage_pct:.1f}%")

    if usage_pct >= 100:
        print("⚠️  上限超過! トリミングが必要です")
    elif usage_pct >= 80:
        print("🔶 80%以上。プロンプト文を加えるとギリギリになる可能性があります")
    else:
        print("✅ 余裕あり")


# 使い方
result = estimate_tokens("./my_project")
total = sum(result.values())
check_usage(total, model="gpt-4o")

⚠️ 注意:コンテキストウィンドウにはプロンプト文(質問文)やシステムメッセージも含まれます。実際にはコードベース以外で数百〜数千トークン消費するので、余裕を持って70〜80%以内に収めるのがベストです。

Step 3: 優先度でファイルをトリミングする

上限を超えそうなら、重要度の低いファイルを除外してから貼り付けましょう。以下のスクリプトは「指定したトークン上限に収まるように、ファイルを優先度順に選択する」ものです。

def trim_to_limit(
    token_map: dict,
    max_tokens: int,
    priority_keywords: list = None
) -> list:
    """
    max_tokens に収まるよう、優先度の高いファイルを選んで返す

    priority_keywords: ファイルパスにこの文字列が含まれると優先される
    例: ["main", "core", "service", "model"]
    """
    if priority_keywords is None:
        priority_keywords = ["main", "core", "service", "model", "util"]

    def priority_score(path: str) -> int:
        """パスに優先キーワードが含まれるほどスコアが高い"""
        path_lower = path.lower()
        return sum(1 for kw in priority_keywords if kw in path_lower)

    # (優先スコア降順, トークン数昇順) でソート
    sorted_files = sorted(
        token_map.items(),
        key=lambda x: (-priority_score(x[0]), x[1])
    )

    selected = []
    used = 0
    skipped = []

    for path, count in sorted_files:
        if used + count <= max_tokens:
            selected.append(path)
            used += count
        else:
            skipped.append((path, count))

    print(f"\n✅ 選択: {len(selected)}ファイル / {used:,}トークン")
    print(f"❌ 除外: {len(skipped)}ファイル")
    if skipped:
        print("  除外されたファイル(上位5件):")
        for p, c in skipped[:5]:
            print(f"    {p}  ({c:,} tokens)")

    return selected


# 使い方例
result = estimate_tokens("./my_project")
selected_files = trim_to_limit(
    result,
    max_tokens=90_000,  # 上限の約70%を目安に
    priority_keywords=["main", "service", "model", "api"]
)

Step 4: 選択したファイルを1つのテキストにまとめる

選択されたファイル一覧ができたら、AIに渡す用のテキストをまとめて生成しましょう。

def bundle_files(file_paths: list, base_dir: str = ".") -> str:
    """
    ファイル一覧を読み込んで、AIに渡しやすい形式の文字列にまとめる
    """
    chunks = []
    for path in file_paths:
        rel_path = os.path.relpath(path, base_dir)
        try:
            with open(path, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
            chunks.append(f"### {rel_path}\n```\n{content}\n```")
        except Exception as e:
            chunks.append(f"### {rel_path}\n[読み込みエラー: {e}]")

    return "\n\n".join(chunks)


# 使い方例
bundled = bundle_files(selected_files, base_dir="./my_project")

# クリップボードにコピーしたい場合(macOS)
# import subprocess
# subprocess.run("pbcopy", input=bundled.encode("utf-8"))

print(bundled[:500])  # 先頭500文字だけプレビュー

出力されたテキストをそのままAIのチャット画面に貼り付ければOKです。ファイル名がヘッダーとして付くので、AIも「どのファイルのコードか」を把握しやすくなります。

全部まとめた完全版スクリプト

ここまでの処理を1ファイルにまとめたものがこちらです。コマンドライン引数でディレクトリ・モデル・上限を指定できます。

# token_trimmer.py
# 使い方: python token_trimmer.py ./my_project --model gpt-4o --limit 90000

import os
import sys
import argparse
import tiktoken

MODEL_LIMITS = {
    "gpt-4o": 128_000,
    "gpt-4o-mini": 128_000,
    "claude-3-5-sonnet": 200_000,
}

TARGET_EXTS = {".py", ".js", ".ts", ".jsx", ".tsx",
               ".java", ".cpp", ".c", ".h", ".md", ".txt"}

EXCLUDE_DIRS = {".git", "node_modules", "__pycache__", ".venv", "dist", "build"}


def estimate_tokens(directory: str, model: str = "gpt-4o") -> dict:
    enc = tiktoken.encoding_for_model(model)
    results = {}
    for root, dirs, files in os.walk(directory):
        dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
        for fname in files:
            if os.path.splitext(fname)[1] not in TARGET_EXTS:
                continue
            fpath = os.path.join(root, fname)
            try:
                with open(fpath, "r", encoding="utf-8", errors="ignore") as f:
                    text = f.read()
                results[fpath] = len(enc.encode(text))
            except Exception:
                pass
    return results


def trim_to_limit(token_map: dict, max_tokens: int,
                  priority_keywords: list = None) -> list:
    if priority_keywords is None:
        priority_keywords = ["main", "core", "service", "model", "api", "util"]

    def score(path):
        p = path.lower()
        return sum(1 for kw in priority_keywords if kw in p)

    sorted_files = sorted(token_map.items(), key=lambda x: (-score(x[0]), x[1]))
    selected, used = [], 0
    for path, count in sorted_files:
        if used + count <= max_tokens:
            selected.append(path)
            used += count
    return selected, used


def bundle_files(file_paths: list, base_dir: str = ".") -> str:
    chunks = []
    for path in file_paths:
        rel = os.path.relpath(path, base_dir)
        try:
            with open(path, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
            chunks.append(f"### {rel}\n```\n{content}\n```")
        except Exception as e:
            chunks.append(f"### {rel}\n[Error: {e}]")
    return "\n\n".join(chunks)


def main():
    parser = argparse.ArgumentParser(description="コードベースのトークン数を計測してトリミングする")
    parser.add_argument("directory", help="対象ディレクトリ")
    parser.add_argument("--model", default="gpt-4o", choices=list(MODEL_LIMITS.keys()))
    parser.add_argument("--limit", type=int, default=None, help="使用トークン上限(省略時はモデル上限の70%)")
    parser.add_argument("--output", default="bundled_code.txt", help="出力ファイル名")
    args = parser.parse_args()

    model_limit = MODEL_LIMITS[args.model]
    max_tokens = args.limit or int(model_limit * 0.7)

    print(f"📁 対象: {args.directory}")
    print(f"🤖 モデル: {args.model} (上限: {model_limit:,} tokens)")
    print(f"🎯 使用上限: {max_tokens:,} tokens")
    print()

    print("⏳ トークン数を計測中...")
    token_map = estimate_tokens(args.directory, args.model)
    total = sum(token_map.values())
    print(f"   合計: {total:,} tokens / {len(token_map)} ファイル")

    selected, used = trim_to_limit(token_map, max_tokens)
    print(f"\n✅ 選択: {len(selected)} ファイル / {used:,} tokens")
    print(f"❌ 除外: {len(token_map) - len(selected)} ファイル")

    bundled = bundle_files(selected, base_dir=args.directory)
    with open(args.output, "w", encoding="utf-8") as f:
        f.write(bundled)
    print(f"\n💾 出力完了: {args.output}")


if __name__ == "__main__":
    main()

実行するとこんな感じの出力になります:

📁 対象: ./my_project
🤖 モデル: gpt-4o (上限: 128,000 tokens)
🎯 使用上限: 89,600 tokens

⏳ トークン数を計測中...
   合計: 142,380 tokens / 87 ファイル

✅ 選択: 61 ファイル / 88,942 tokens
❌ 除外: 26 ファイル

💾 出力完了: bundled_code.txt

Claude・Geminiに使うには?(tiktoken以外の選択肢)

tiktoken はOpenAI公式のライブラリなので、他のモデルでは厳密には異なるトークナイザーが使われています。とはいえ、英数字主体のソースコードであれば誤差は5〜10%程度なので、上限に対して70〜80%を目安にしておけば実用上は問題ありません。

より正確に計測したい場合は以下の方法もあります:

  • Anthropic(Claude):APIの usage フィールドで実際のトークン数を確認できます
  • Google(Gemini)generativelanguage APIの countTokens エンドポイントを利用できます
  • 汎用的な推定:文字数 ÷ 3(日本語)または 文字数 ÷ 4(英語)でざっくり計算するのも手軽です

まとめ:「見えない切り捨て」を防ぐために

今回のポイントをまとめます。

  • コンテキストウィンドウを超えると、AIはエラーを出さずに静かに先頭を切り捨てる
  • tiktoken を使えばローカルでトークン数を事前計測できる
  • 上限の70〜80%を目安にして、プロンプト文の分も余裕を残す
  • 優先度キーワードを使って、重要なファイルを自動選択できる
  • まとめたテキストをAIに渡すことで、確実に全体を見てもらえる

「なんかAIの回答がズレてるな…」と感じたら、まずトークン数を疑ってみてください。貼り付け前の一手間が、AIコーディングの質を大きく変えてくれますよ 🚀

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る
実践Claude Code入門―現場で活用するためのAIコーディングの思考法

もしも

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

AIコーディングの現場活用法を学ぶ一冊

Amazonで見る
Python Web開発実践入門 ―― FastAPIによるWebAPI開発と非同期処理

もしも

Python Web開発実践入門 ―― FastAPIによるWebAPI開発と非同期処理

FastAPIでWebAPI開発を実践的に学ぶ

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です