AIコーディング 2026.06.07

ChatGPT APIで長文を送信する方法|トークン上限超過エラーの対処と分割戦略【2026年版】

タグ:ChatGPT API / トークン上限 / エラー対処 / テキスト分割 / API実装

ChatGPT APIのトークン上限とは

ChatGPT APIを使うときに最もよくぶつかる問題が「トークン上限を超えた」というエラーです。具体的には「Max tokens exceeded」や「This model’s maximum context length is X tokens」「context length exceeded」といったエラーが発生します。トークンとは、APIが理解できる最小の文字単位のことで、日本語の場合、おおよそ1〜2文字で1トークンとして数えられます。英語ではより効率的で、1語がおおむね1トークン程度です。

モデルごとにトークン上限(context window)が決まっており、2026年時点での主なモデルは以下の通りです。

モデル最大トークン数用途
gpt-4o(最新)128,000マルチモーダル対応
gpt-4o mini128,000軽量・低コスト
gpt-4 Turbo128,000高速・高精度バランス
gpt-3.5-turbo16,384軽量・低コスト

なお、上限はAPIドキュメントで定期的に更新されるため、最新の仕様はOpenAI公式ドキュメントでご確認ください。

この上限は入力と出力を合わせた合計なので、非常に長いテキストを送信すると簡単に超えてしまいます。上限を超えると、APIはHTTP 400エラーを返し、「This model’s maximum context length is … tokens」というメッセージを表示します。

実務では、ユーザーからの質問に長い文書を添付してもらったり、データベースから大量のテキストを取得して処理したりする場面が頻繁にあります。こうしたケースでAPIが「上限超過」エラーを返すと、処理が止まってしまい、ユーザー体験が著しく低下します。また、複数ターンにわたる対話型アプリケーションでは、過去の会話履歴をすべてAPIへ送信する必要があるため、やり取りが増えるたびにトークン消費が積み上がっていく点にも注意が必要です。

なぜトークン上限超過が起きるのか

トークン上限超過の原因は大きく3つあります。

1. 入力テキストが大きすぎる

送信するテキスト(質問や文書)が上限を超えているケースです。例えば、数十ページのPDF文書を丸ごと送ったり、数千行のログファイルを一度に処理しようとしたりすると起きます。RAGシステムで、ベクトル検索で取得した複数のドキュメントをすべてプロンプトに含める場合も、テキスト量が爆発的に増えることがあります。

なお、日本語は英語よりもトークン効率が悪く、同じ文字数でも約2〜3倍のトークンを消費します。1トークンはおおよそ英語で4文字に相当しますが、日本語は1文字が約1.3トークンです。つまり、日本語テキストが10,000文字あれば、おおよそ13,000トークンを消費します。日本語アプリケーションでは特に注意が必要です。

2. 会話履歴が溜まっている

APIを繰り返し呼び出す会話形式で使う場合、過去のやり取りがすべてコンテキストに含まれます。長い会話をしていると、古いメッセージも新しいメッセージもすべてトークン数にカウントされるため、知らないうちに上限に近づいていることがあります。LangChainなどのフレームワークを使用した場合、チャット履歴が明示的に管理されず、背景でメモリが蓄積されることもあります。

3. システムプロンプト+ユーザー入力+max_tokensの合計が上限を超える

APIリクエストはシステムプロンプト(指示内容)・ユーザー入力(質問や処理対象のテキスト)・max_tokens(回答の最大長を指定したパラメータ)で構成されます。これら3つの合計がコンテキストウィンドウを超えると、エラーが発生します。詳細な指示や例示を含むシステムプロンプトは、リクエストが多いほどトークンを消費するため注意が必要です。

また、多くの開発者が見落とすのが、システムメッセージ(role: "system")や改行・インデントのトークンです。JSONやPythonコードをプロンプトに含める場合、改行やインデントもトークンとしてカウントされます。

トークン数を事前に計算する方法

トークン上限に達する前に、送信前のテキストのトークン数を計算することが重要です。OpenAIは公式に tiktoken というPythonライブラリを提供しており、これを使って正確にトークン数を数えられます。

まだインストールしていなければ、以下のコマンドで導入してください。

pip install tiktoken

動作確認バージョン: tiktoken 0.5.0 以上(2026年時点)

import tiktoken

# 使いたいモデルのエンコーディングを取得
encoding = tiktoken.encoding_for_model("gpt-4o")

# トークン数を計算
text = "これはサンプルテキストです。ChatGPT APIを使う際のトークン管理について説明します。"
tokens = encoding.encode(text)
token_count = len(tokens)

print(f"テキスト: {text}")
print(f"トークン数: {token_count}")
print(f"デコード確認: {encoding.decode(tokens)}")

実際にAPIを呼び出す際は、プロンプト全体(システムメッセージ+ユーザー入力)と回答用に確保するトークン数の合計が上限を超えないよう、以下のようにメッセージ全体を計算することが重要です。

import tiktoken

encoding = tiktoken.encoding_for_model("gpt-4o")

messages = [
    {"role": "system", "content": "あなたは日本語の翻訳家です。"},
    {"role": "user", "content": "以下の英文を日本語に訳してください: 'The quick brown fox jumps over the lazy dog.'"},
]

# トークン数を計算(メッセージ枠のオーバーヘッドも含める)
total_tokens = 0
for msg in messages:
    total_tokens += 4  # メッセージ枠のオーバーヘッド
    total_tokens += len(encoding.encode(msg["content"]))

# 回答用に200トークン確保したい場合
max_response_tokens = 200
required_tokens = total_tokens + max_response_tokens

print(f"プロンプト: {total_tokens} トークン")
print(f"回答用: {max_response_tokens} トークン")
print(f"合計: {required_tokens} トークン")

# gpt-4oの上限(128,000)に収まるか確認
model_limit = 128000
if required_tokens > model_limit:
    print(f"⚠️  超過! {required_tokens - model_limit} トークン削減が必要です。")
else:
    print(f"✓ OK(余裕: {model_limit - required_tokens} トークン)")

なお、モデルを gpt-3.5-turbo から gpt-4o に変更した場合など、モデル間でトークン数の計算方法が異なるため、切り替えの際は tiktoken.encoding_for_model() に正確なモデル名を渡して再計算してください。

長文テキストを送信する3つの戦略

トークン上限を超えないようにするには、テキストを分割して複数のリクエストに分ける方法が一般的です。

戦略1: テキストを固定サイズで分割

最もシンプルな方法は、テキストを一定のトークン数ごとに分割することです。例えば、3,000トークンごとに区切り、複数回に分けてAPIに送信します。

import tiktoken

def split_text_by_tokens(text, max_tokens=3000):
    """テキストを指定トークン数で分割"""
    encoding = tiktoken.encoding_for_model("gpt-4o")
    tokens = encoding.encode(text)
    
    chunks = []
    current_chunk = []
    current_count = 0
    
    for token in tokens:
        current_chunk.append(token)
        current_count += 1
        
        if current_count >= max_tokens:
            # チャンクをテキストにデコード
            chunk_text = encoding.decode(current_chunk)
            chunks.append(chunk_text)
            current_chunk = []
            current_count = 0
    
    # 残りのトークンをチャンクに追加
    if current_chunk:
        chunk_text = encoding.decode(current_chunk)
        chunks.append(chunk_text)
    
    return chunks

# 使用例
long_text = "非常に長いテキスト..."
chunks = split_text_by_tokens(long_text, max_tokens=3000)

for i, chunk in enumerate(chunks):
    print(f"\n--- チャンク {i+1} ---")
    print(chunk)

この方法なら、どのサイズのモデルでも対応できます。ただし、テキストが意味のある単位(段落や文)で分割されないため、チャンクの途中で文が切れることがあります。

この方法の利点:

  • 実装が単純
  • メモリ効率が良い
  • 同期処理で結果を順序通り取得できる

欠点:

  • 複数回のAPI呼び出しが発生(料金が増加)
  • チャンク間の文脈が失われる可能性

戦略2: 段落や改行で区切る(スライディングウィンドウ)

テキストの意味を損なわないように、段落や改行単位で分割する方法です。LangChainの RecursiveCharacterTextSplitter を使えば、段落→文→単語の順序で自動的に分割することもできます。また、チャンク間で一定のトークンを重複させることで文脈の連続性を保つ「スライディングウィンドウ」方式も有効です。

import tiktoken

def split_text_by_paragraphs(text, max_tokens=3000):
    """段落ごとにテキストを分割"""
    encoding = tiktoken.encoding_for_model("gpt-4o")
    
    # 段落で分割(2つ以上の改行を区切り文字とする)
    paragraphs = text.split("\n\n")
    
    chunks = []
    current_chunk = ""
    
    for paragraph in paragraphs:
        test_text = current_chunk + paragraph + "\n\n"
        token_count = len(encoding.encode(test_text))
        
        if token_count <= max_tokens:
            current_chunk = test_text
        else:
            # 現在のチャンクを保存
            if current_chunk:
                chunks.append(current_chunk.strip())
            # 新しいチャンクを開始
            current_chunk = paragraph + "\n\n"
    
    # 最後のチャンクを追加
    if current_chunk:
        chunks.append(current_chunk.strip())
    
    return chunks

# 使用例
long_text = """
第1章 序論
これは序論です。...

第2章 方法
方法についてです。...

第3章 結果
結果は以下の通りです。...
"""

chunks = split_text_by_paragraphs(long_text, max_tokens=3000)
print(f"分割されたチャンク数: {len(chunks)}")

この方法は、元のテキストの構造を保ちながら分割できるため、より自然な処理が可能です。ただし、文脈をより厳密に保持したい場合は、チャンク間でオーバーラップを持たせる方法も検討してください。

def split_text_with_overlap(text, chunk_size=3000, overlap=500, model="gpt-4o"):
    """重複を持たせながらテキストを分割する関数"""
    encoding = tiktoken.encoding_for_model(model)
    tokens = encoding.encode(text)
    
    chunks = []
    start = 0
    
    while start < len(tokens):
        end = start + chunk_size
        chunk_tokens = tokens[start:end]
        chunk_text = encoding.decode(chunk_tokens)
        chunks.append(chunk_text)
        
        # 次のチャンクは重複を考慮して開始
        start = end - overlap
    
    return chunks

戦略3: API呼び出しごとに段階的に処理(要約→追加質問パターン)

複数のチャンクを別々に処理するのではなく、段階的にAPIを呼び出し、前の結果を次のリクエストに含める方法です。例えば、長い文書を処理するときに「まず第1部分を要約して」「その要約と第2部分を合わせて分析して」というように進めます。複数の質問がある場合は、最初のAPI呼び出しで長文を要約させてから、その要約に対して個別の質問を投げると後続のコストを削減できます。

from openai import OpenAI

def process_long_document_step_by_step(chunks, task="要約"):
    """チャンクを段階的に処理"""
    client = OpenAI()
    
    accumulated_result = ""
    
    for i, chunk in enumerate(chunks):
        # プロンプトを組み立て
        if i == 0:
            # 最初のチャンク
            prompt = f"以下のテキストを{task}してください:\n\n{chunk}"
        else:
            # 2回目以降は前の結果を含める
            prompt = f"これまでの{task}:\n{accumulated_result}\n\n新しいテキスト:\n{chunk}\n\nこれまでの{task}と新しいテキストを合わせて、更新された{task}を提供してください。"
        
        # APIを呼び出し
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "user", "content": prompt}
            ]
        )
        
        accumulated_result = response.choices[0].message.content
        print(f"チャンク {i+1} 処理完了")
    
    return accumulated_result

# 使用例
long_text = "非常に長いテキスト..."
chunks = split_text_by_paragraphs(long_text, max_tokens=2000)
final_summary = process_long_document_step_by_step(chunks, task="要約")
print("\n最終要約:")
print(final_summary)

この方法の利点:

  • 各ステップでAPIに送信するテキスト量が少なくなるため、上限超過のリスクが低い
  • 複数の質問を効率的に処理できる
  • 要約によりコスト最適化が可能

欠点:

  • APIを複数回呼び出すため、料金とレスポンス時間が増加
  • 要約の過程で情報が失われる可能性

会話メモリ管理の3つのパターン

対話型アプリケーションでは、長文テキストの分割に加えて、会話履歴そのものを管理する「メモリ戦略」が重要です。

パターン1: 直近N件の会話を保持

最も簡単な方法は、会話履歴から直近のやり取りだけを抽出し、古いやり取りを削除することです。

from openai import OpenAI

client = OpenAI()

class SimpleMemoryChat:
    def __init__(self, max_history=10):
        # 保持する会話ペアの最大数
        self.max_history = max_history
        self.conversation = []
    
    def add_message(self, role, content):
        """メッセージを会話履歴に追加"""
        self.conversation.append({"role": role, "content": content})
        
        # 古いやり取りを削除(max_history個を超えた場合)
        if len(self.conversation) > self.max_history * 2:
            self.conversation = self.conversation[-self.max_history * 2:]
    
    def chat(self, user_input):
        """ユーザー入力を受け取り、APIに送信"""
        self.add_message("user", user_input)
        
        # APIへリクエスト送信
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=self.conversation,
            temperature=0.7
        )
        
        assistant_message = response.choices[0].message.content
        self.add_message("assistant", assistant_message)
        
        return assistant_message

# 使用例
chat = SimpleMemoryChat(max_history=10)
print(chat.chat("こんにちは。今日の天気について教えてください"))
print(chat.chat("明日はどうですか?"))

このパターンは実装が簡単ですが、古い会話情報が失われるため、長期的な文脈を保つことができません。

パターン2: 「メモリドック」による会話要約戦略

注目を集めている手法が、「メモリドック」(Memory Doc)です。これは、対話の中で重要な情報を別に記録しておき、古い会話履歴を削除する際も、このメモに基づいて文脈を保つという方法です。

メモリドックには、以下のような情報を記録します。

  • ユーザーの基本情報(名前、職業、関心事など)
  • 過去のやり取りから抽出した重要な事実
  • ユーザーの好み、制約、目標
from openai import OpenAI

client = OpenAI()

class MemoryDocChat:
    def __init__(self, max_history=10):
        self.max_history = max_history
        self.conversation = []
        # メモリドックをシステムメッセージとして管理
        self.memory_doc = ""
    
    def update_memory_doc(self):
        """
        会話履歴からメモリドックを更新
        実装簡略化のため、ここでは基本的なパターンのみ示す
        """
        # 実際の運用では、定期的にAPIを呼び出してメモリドックを要約更新する
        pass
    
    def get_system_prompt(self):
        """
        システムプロンプトにメモリドックを含める
        """
        base_prompt = "あなたは親切で有用なアシスタントです。"
        if self.memory_doc:
            base_prompt += f"\n\n【ユーザーについての既知情報】\n{self.memory_doc}"
        return base_prompt
    
    def chat(self, user_input):
        """ユーザー入力を受け取り、APIに送信"""
        self.conversation.append({"role": "user", "content": user_input})
        
        # 会話が長くなった場合、古いやり取りを削除
        if len(self.conversation) > self.max_history * 2:
            # 削除前に重要情報をメモリドックに反映させる
            self.update_memory_doc()
            self.conversation = self.conversation[-self.max_history * 2:]
        
        # システムプロンプトにメモリドックを含める
        messages = [
            {"role": "system", "content": self.get_system_prompt()}
        ] + self.conversation
        
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            temperature=0.7
        )
        
        assistant_message = response.choices[0].message.content
        self.conversation.append({"role": "assistant", "content": assistant_message})
        
        return assistant_message

# 使用例
chat = MemoryDocChat(max_history=10)
print(chat.chat("私の名前はTaroです。Python開発者です"))
print(chat.chat("最近、APIの実装について学んでいます"))

メモリドック手法のメリットは、古い会話を削除しながらも、ユーザーの重要な特性や過去の決定を保持できることです。メモリドックは以下のタイミングで更新することを推奨します。

  • 会話が一定数(例:20ターン)に達したとき
  • ユーザーが明示的に情報を提供したとき(「私の名前は〇〇です」など)

パターン3: トークン数を監視しながら管理する方法

より正確に実装するには、各メッセージのトークン数を計算し、上限に達する前に古い履歴を削除する方法があります。

from openai import OpenAI
import tiktoken

client = OpenAI()

class TokenAwareChat:
    def __init__(self, model="gpt-4o", max_tokens=3000):
        self.model = model
        self.max_tokens = max_tokens
        self.conversation = []
        self.encoding = tiktoken.encoding_for_model(model)
    
    def count_tokens(self, text):
        """テキストのトークン数を計算"""
        return len(self.encoding.encode(text))
    
    def get_total_tokens(self):
        """会話全体のトークン数を計算"""
        total = 0
        for msg in self.conversation:
            total += self.count_tokens(msg["content"])
        return total
    
    def trim_conversation(self):
        """トークン数が上限を超えた場合、古いやり取りを削除"""
        while self.get_total_tokens() > self.max_tokens and len(self.conversation) > 2:
            # 最初の2件(ユーザーとアシスタント)を削除
            self.conversation = self.conversation[2:]
    
    def chat(self, user_input):
        """ユーザー入力を受け取り、APIに送信"""
        self.conversation.append({"role": "user", "content": user_input})
        
        # トークン数を確認し、必要に応じて古い履歴を削除
        self.trim_conversation()
        
        response = client.chat.completions.create(
            model=self.model,
            messages=self.conversation,
            temperature=0.7
        )
        
        assistant_message = response.choices[0].message.content
        self.conversation.append({"role": "assistant", "content": assistant_message})
        
        return assistant_message

# 使用例
chat = TokenAwareChat(model="gpt-4o", max_tokens=3000)
print(chat.chat("こんにちは"))
print(f"現在のトークン数: {chat.get_total_tokens()}")

このアプローチは最も正確ですが、tiktoken ライブラリのインストールが必要です。

テキストを圧縮・削減する方法

分割以外にも、テキスト自体を短くする工夫があります。

方法1: 不要な情報を事前削除

import re

def preprocess_text(text):
    """テキストから不要な部分を除去"""
    
    # 複数の空行を1つに統一
    text = re.sub(r'\n\n+', '\n\n', text)
    
    # HTMLタグを削除
    text = re.sub(r'<[^>]+>', '', text)
    
    # URLを簡略化
    text = re.sub(r'https?://[^\s]+', '[URL]', text)
    
    # 重複する空白を削除
    text = re.sub(r' +', ' ', text)
    
    return text.strip()

sample = """
<p>これはサンプルです。</p>

詳細は https://example.com を参照してください。

より詳しくは   https://example.com/page  をご覧ください。
"""

cleaned = preprocess_text(sample)
print(cleaned)

出力:

これはサンプルです。

詳細は [URL] を参照してください。

より詳しくは [URL] をご覧ください。

方法2: 要点のみを抽出してから送信

最初のリクエストで「このテキストから重要な部分を箇条書きで5個抽出してください」と指示し、そのサマリーを次のリクエストで利用する手法もあります。

from openai import OpenAI

client = OpenAI()

def extract_key_points(long_text, num_points=5):
    """テキストから重要なポイントを抽出"""
    
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "user",
                "content": f"""以下のテキストから最も重要な{num_points}個のポイントを箇条書きで抽出してください。
各ポイントは50文字以内で簡潔にしてください。

テキスト:
{long_text}"""
            }
        ],
        max_tokens=500,
        temperature=0.5
    )
    
    return response.choices[0].message.content

# 使用例
key_points = extract_key_points(long_document_text)
print(key_points)

このアプローチは、2ステップになるため処理時間は増えますが、トークン効率が大幅に改善されます。

回答が途中で切れた場合の対処法

APIが返す回答が上限に近づくと、文が途中で切れることがあります。その場合、finish_reason というフィールドで理由を確認できます。

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "user", "content": "長い質問..."}
    ]
)

print(f"終了理由: {response.choices[0].finish_reason}")
print(f"回答: {response.choices[0].message.content}")

finish_reason の種類は以下の通りです。

  • stop:通常の終了
  • length:トークン数上限により途中で終了
  • function_call:関数呼び出しが発生(関数呼び出し機能を使用する場合)

finish_reason"length" だった場合、回答がトークン上限で切られたことを意味します。max_tokens を増やし、その分プロンプトを削減するか、続きを取得するために「続きを書いてください」という新しいメッセージを追加します。

from openai import OpenAI

client = OpenAI()

def get_complete_response(messages, model="gpt-4o"):
    """完全な応答を取得するまで、APIへのリクエストを繰り返す"""
    full_response = ""
    
    while True:
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            temperature=0.7
        )
        
        content = response.choices[0].message.content
        full_response += content
        
        # finish_reason が "length" の場合、応答が途中で切れている
        if response.choices[0].finish_reason == "length":
            # 応答を会話履歴に追加し、続きを取得するようAIに促す
            messages.append({"role": "assistant", "content": content})
            messages.append({"role": "user

---

## 参考:Claude APIでの長文事前チェック実装

Claude APIの`count_tokens`を使い、送信前にコンテキスト超過を検知する実装です。

```python
import anthropic

client = anthropic.Anthropic()
MAX_CONTEXT = 200_000

def check_and_split_if_needed(long_text: str, chunk_size: int = 150_000) -> list[str]:
    """長文を送信前にチェックし、必要なら分割"""
    count_result = client.messages.count_tokens(
        model="claude-sonnet-4-5",
        messages=[{"role": "user", "content": long_text}]
    )

    if count_result.input_tokens <= MAX_CONTEXT * 0.8:
        return [long_text]  # 分割不要

    # 文字数ベースで概算分割(正確なトークン数境界ではない点に注意)
    chunks = []
    for i in range(0, len(long_text), chunk_size):
        chunks.append(long_text[i:i + chunk_size])
    return chunks

def process_long_document(document: str) -> str:
    chunks = check_and_split_if_needed(document)
    summaries = []

    for i, chunk in enumerate(chunks):
        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=1024,
            messages=[{"role": "user", "content": f"以下の文書パート{i+1}を要約してください:\n{chunk}"}]
        )
        summaries.append(response.content[0].text)

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

# 使用例
result = process_long_document("非常に長い文書テキスト..." * 10000)
print(result[:200])

Claude Sonnet 4.5の200Kトークンという広いコンテキストでも、限界を超える文書を扱う場合は同様の事前チェックと分割戦略が必要です。


あわせて読みたい

参考ソース