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」といったエラーが発生します。トークンとは、APIが理解できる最小の文字単位のことで、日本語の場合、おおよそ1〜2文字で1トークンとして数えられます。英語ではより効率的で、1語がおおむね1トークン程度です。

モデルごとにトークン上限が決まっており、2026年時点では gpt-4o および gpt-4o mini が128,000トークン、gpt-4 Turbo が128,000トークン、gpt-3.5-turbo が16,384トークンが上限です。この上限は入力と出力を合わせた合計なので、非常に長いテキストを送信すると簡単に超えてしまいます。

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

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

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

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

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

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

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

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

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

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

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

import tiktoken

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

# トークン数を計算
text = "ここにあなたのテキストを入れてください。"
tokens = encoding.encode(text)
token_count = len(tokens)

print(f"トークン数: {token_count}")

このコードを実行すれば、実際のトークン数がわかります。事前に計算しておくことで、上限に達する前に対応できます。

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

pip install tiktoken

長文テキストを送信する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)

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

戦略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)}")

この方法は、元のテキストの構造を保ちながら分割できるため、より自然な処理が可能です。

戦略3: API呼び出しごとに段階的に処理

複数のチャンクを別々に処理するのではなく、段階的にAPIを呼び出し、前の結果を次のリクエストに含める方法です。例えば、長い文書を処理するときに「まず第1部分を要約して」「その要約と第2部分を合わせて分析して」というように進めます。

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 ライブラリのインストールが必要です。

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

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"length" だった場合、回答がトークン上限で切られたことを意味します。この場合、続きを取得するには、これまでのやり取りに「続きを書いてください」という新しいメッセージを追加します。

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", "content": "続きをお願いします"})
        else:
            break
    
    return full_response

# 使用例
messages = [
    {"role": "system", "content": "あなたは有用なアシスタントです"},
    {"role": "user", "content": "次の物語を続けてください: 昔々、ある森に..."}
]

full_response = get_complete_response(messages)
print(full_response)

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

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

ポイントは、新しいリクエストでも過去のメッセージをすべて含めることです。そうすることで、モデルが文脈を理解した上で、自然な続きを書くことができます。

ただし、このやり方でも新しいリクエストでトークン数が増えるため、非常に長い会話の場合は、古いメッセージを削除するなどの工夫が必要になる場合があります。

つまずきやすいポイントと解決策

チャンク分割後に重複や欠落が起きる

テキストを分割するときに、チャンク境界で文字が重複したり、逆に落ちたりすることがあります。特に、トークンレベルで分割した場合、日本語が文字化けすることもあります。

解決策: 段落や文で分割する戦略2を使い、「テキストを確認してから送信する」というチェックステップを入れましょう。

# チャンク分割後に合計トークン数を確認
total_tokens = sum(len(encoding.encode(chunk)) for chunk in chunks)
original_tokens = len(encoding.encode(original_text))
print(f"元のテキスト: {original_tokens} トークン")
print(f"分割後の合計: {total_tokens} トークン")

複数のAPIコール実行時にレート制限に達する

短時間に大量のAPIコールをすると、OpenAIのレート制限(Rate limit exceeded)に引っかかります。

解決策: APIコール間に遅延を入れる、またはバッチ処理APIを使う。

import time
from openai import OpenAI

client = OpenAI()

for chunk in chunks:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": chunk}]
    )
    
    print(response.choices[0].message.content)
    
    # 次のリクエストまで1秒待機
    time.sleep(1)

トークン数の計算が実際と異なる

tiktoken でカウントしたトークン数と、APIが実際にカウントしたトークン数がズレることがあります。特に、システムプロンプトやメッセージ構造(rolecontent など)もトークン数に含まれるため、純粋なテキストだけで計算すると少なく見積もってしまいます。また、tiktoken のバージョンによってはトークン化の仕様が異なる可能性もあるため、バージョンの確認も行いましょう。

解決策: テキストだけでなく、メッセージ全体のトークン数を計算する関数を使う。

import tiktoken

def count_message_tokens(messages, model="gpt-4o"):
    """メッセージ全体のトークン数を計算"""
    encoding = tiktoken.encoding_for_model(model)
    
    token_count = 0
    
    # メッセージごとにトークンを計算
    for message in messages:
        # ロールとコンテンツを含める
        token_count += len(encoding.encode(message["role"]))
        token_count += len(encoding.encode(message["content"]))
        # メッセージのオーバーヘッド(約4トークン)
        token_count += 4
    
    # プロンプト全体のオーバーヘッド(約2トークン)
    token_count += 2
    
    return token_count

# 使用例
messages = [
    {"role": "system", "content": "あなたは日本語アシスタントです。"},
    {"role": "user", "content": "長いテキスト..."}
]

total = count_message_tokens(messages)
print(f"メッセージ全体のトークン数: {total}")

max_tokensの設定ミス

モデルのコンテキストウィンドウを無視してmax_tokensを大きく設定しすぎると、エラーになります。

# ❌ NG:コンテキストウィンドウが小さいモデルでは超過する
response = client.chat.completions.create(
    model="gpt-3.5-turbo",  # 最大16,384トークン
    messages=[...],
    max_tokens=10000  # 危険
)

# ✅ 正解:モデルの上限を考慮
response = client.chat.completions.create(
    model="gpt-4o",  # 最大128,000トークン
    messages=[...],
    max_tokens=10000  # 安全
)

実装時の注意点

ChatGPT APIを本番環境で使う場合、以下の点に注意してください。

メモリ戦略の選択基準: アプリケーションの要件に応じて、適切なメモリ戦略を選びましょう。

  • 短期の対話のみ:シンプルなメモリ戦略(直近N件保持)で十分
  • ユーザー情報の長期保持が必要:メモリドック手法を採用
  • 精密な管理が必要:トークン数を監視する方法を使用

実装時のチェックリスト:

  1. 事前のトークン数計算tiktokenを使ってテキスト量を把握する
  2. max_tokensの設定:回答に必要な長さに応じて、適切な値を設定する(推奨:500〜2000)
  3. エラーハンドリング:「finish_reason == length」やAPIErrorをキャッチする処理を用意する
  4. 分割戦略の選択:テキストを分割する場合は、チャンク間の文脈損失を考慮する
  5. コスト管理:チャンク分割により複数回のAPI呼び出しが発生することを予算に含める
  6. トークン数のログ記録:本番環境では、すべてのAPIリクエストのトークン数をログに記録し、問題の早期発見に役立てる

システムプロンプトの活用: メモリドックや会話の制約を明確にするため、システムプロンプトを工夫しましょう。

system_prompt = """
あなたは親切で有用なアシスタントです。

【ユーザーの基本情報】
名前:Taro
職業:ソフトウェアエンジニア
関心事:Python、APIセキュリティ

【対話のルール】
- ユーザーの前の発言内容を覚えておく
- 矛盾した回答をしない
- 不確実な情報は「わかりません」と答える
"""

APIキーの管理: APIキーは絶対にコード内に書かず、環境変数から読み込むようにします。

import os
from openai import OpenAI

api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=api_key)

エラーハンドリング: トークン上限超過だけでなく、ネットワークエラーや認証エラーに対応する必要があります。

from openai import OpenAI, APIError, RateLimitError

client = OpenAI()

try:
    response = client.chat.completions.create(
        model="gpt-

---

## あわせて読みたい

- [Claude Codeのトークン消費を98%削減する方法【MCP活用+コンテキスト最適化】](/code/claude-code-token-consumption-reduction/)
- [Claude Codeのトークン削減方法【94%コスト削減の5ステップ】](/code/token-reduction-with-claude-code-94-cost-savings-guide/)
- [Claude Codeで修正回数を減らす7つのプロンプトテクニック|精度を上げる質問構造](/code/boost-claude-code-context-skills/)

参考ソース