「思っていたより長い応答が返ってきて Cloud Run の上限を超えた」「JSON が途中で切れて壊れた」「箇条書きで止めたかったのに余計な解説まで生成された」— Gemini API を本番で動かしていると、出力長まわりの細かい問題が地味に効いてきます。私自身、最初にぶつかったのは「同じプロンプトでも応答長が日によって倍くらい違う」という現象でした。原因は思考トークン(thinking)と出力トークンを混同していたことだったのですが、当時は気付くまで半日溶かしてしまいました。
ここでは maxOutputTokens と stopSequences という二つのパラメータを使い分けて、出力長を精密にコントロールする方法を整理します。両方とも公式ドキュメントには簡潔な説明しかありませんが、実際に組み合わせると挙動の理解が必要になる場面が多いので、本番で詰まりやすいポイントまで含めて解説していきます。
maxOutputTokens は「最大値」であって「希望値」ではありません
まず誤解しやすい点ですが、maxOutputTokens は応答に含まれる出力トークン数の上限を指定するものであり、「だいたいこれくらいの長さで返してほしい」という希望ではありません。Gemini はこの上限に到達した時点で生成を強制終了するため、運悪く文末記号の前で切れると応答が中途半端になります。
# Python SDK 例
from google import genai
from google.genai import types
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-pro",
contents="次の議事録を300文字程度で要約してください。\n---\n(議事録本文)",
config=types.GenerateContentConfig(
max_output_tokens=400, # 出力の上限。300文字目安なら少し余裕を持たせる
temperature=0.3,
),
)
print(response.text)
print("finish_reason:", response.candidates[0].finish_reason)ここで重要なのは finish_reason を必ず確認することです。STOP であれば自然終了、MAX_TOKENS であれば上限切り捨てなので、後者なら設定値を見直します。本番運用では MAX_TOKENS の発生率をログに残して監視すると、コスト最適化と品質改善の両方に役立ちます。
「文字数」ではなく「トークン数」で考える
日本語の場合、おおむね 1 文字 = 1〜2 トークンと見ておくと感覚が合います。300 文字で要約させたいなら、max_output_tokens は 400〜500 くらいに設定するのが安全です。英語なら 1 単語が 1.3 トークン程度なので、500 ワードなら 700 程度。厳密に測るなら client.models.count_tokens() を併用してください。
# 想定プロンプトでのトークン消費を事前計測
count = client.models.count_tokens(
model="gemini-2.5-pro",
contents="次の議事録を300文字程度で要約してください。",
)
print(f"input_tokens: {count.total_tokens}")
# → 入力トークン数が把握できれば、合計コストの見積もりも正確になるGemini 2.5 / 3 系では thinking トークンが別枠です
ここが一番ハマりやすいポイントです。Gemini 2.5 Pro や Gemini 3 系では、応答前に内部で思考(thinking)トークンを消費しますが、これは maxOutputTokens に含まれない枠で課金されます。つまり「出力上限を 500 にしたのに、課金トークンは 3,000 を超えていた」ということが普通に起こります。コスト感を見積もる際は thinking 側の予算も合わせて考える必要があり、詳しくは Gemini 2.5 thinking budget の最適化ガイド で解説しています。
stopSequences は「ここで終わって」と教えるためのもの
stopSequences は、生成中にこの文字列が出現したら即座に応答を終わらせる、という指示です。最大 5 つまで指定でき、配列で渡します。
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="次の3つの単語で英作文を作ってください: cat, river, morning\n出力は1文だけにしてください。",
config=types.GenerateContentConfig(
stop_sequences=["\n\n", "###", "解説:"],
max_output_tokens=200,
temperature=0.7,
),
)
print(response.text)この例では「2 行空き」「###」「解説:」のいずれかが現れた瞬間に止まります。Gemini は親切心から「以下、解説します」と続けがちなのですが、それを stopSequences で抑え込めるのです。
出力に含まれない、という重要な仕様
stopSequences に指定した文字列自体は応答テキストに含まれません。たとえば stop_sequences=["</answer>"] と設定して <answer>...</answer> のような構造を生成させた場合、終端タグは応答に含まれない点に注意してください。後段でパースする際に「終端タグがあるはず」と仮定したコードを書くとバグります。私は実際にこれで後処理が壊れたことがあります。
JSON モードと組み合わせるときの注意
response_mime_type="application/json" で構造化出力を取らせる場合、stopSequences に } のような JSON 構文の一部を入れてしまうと中身が空のオブジェクトで止まります。構造化出力を扱うなら stopSequences ではなく Pydantic スキーマの responseSchema を使うほうが安全です。
実践パターン:両者を組み合わせて出力を狙い通りに整える
ここからが本題です。実運用では両者を組み合わせて、安全弁と意図表現の二重構成にするのが効果的です。
パターン 1: 短文要約をきっちり止める
def summarize_strict(text: str, max_chars: int = 200) -> dict:
"""指定文字数±20%で要約を返す。超過時は finish_reason で警告を返す。"""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=f"次の文章を{max_chars}文字以内で要約してください。要約のみを返し、前置きは不要です。\n---\n{text}",
config=types.GenerateContentConfig(
max_output_tokens=int(max_chars * 1.5), # 安全マージン
stop_sequences=["\n\n要約", "\n\n以上", "###"],
temperature=0.2,
),
)
finish = response.candidates[0].finish_reason.name
return {
"text": response.text.strip(),
"truncated": finish == "MAX_TOKENS",
"finish_reason": finish,
}
result = summarize_strict("(長い記事本文)", max_chars=200)
if result["truncated"]:
print("⚠️ 上限到達。プロンプトの指示文字数を見直してください。")
print(result["text"])パターン 2: 構造化された短い回答を抽出する
「カテゴリ: ◯◯」のような決まった形式の出力を取るときは、出力末尾を stopSequences で切るとパースが楽になります。
PROMPT = """次の問い合わせを以下の形式で分類してください。
形式:
カテゴリ: <技術|請求|営業|その他>
理由: <30文字以内>
---
問い合わせ: {query}"""
def classify(query: str) -> dict:
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=PROMPT.format(query=query),
config=types.GenerateContentConfig(
max_output_tokens=80,
stop_sequences=["\n---", "\n\n"],
temperature=0.0,
),
)
out = response.text
cat = out.split("カテゴリ:")[-1].split("\n")[0].strip()
reason = out.split("理由:")[-1].strip() if "理由:" in out else ""
return {"category": cat, "reason": reason}
print(classify("ログインできません"))
# → {'category': '技術', 'reason': 'ログイン関連の不具合'}このように stopSequences をパース起点として活用すると、後段の処理が安定します。期待通りに止まらないケースでは、まずプロンプトで「フォーマット以外の出力をしないでください」と明示し、それでも止まらない場合に stopSequences を追加するのが順序として安全です。
パターン 3: チームで共有する SDK ラッパーには防御的なデフォルトを置く
SDK の薄いラッパーをチーム内向けに作る場合、最悪のケースを書く呼び出し元が暴走しないように防御的なデフォルトを設定しておきます。
DEFAULTS = {
"max_output_tokens": 1024,
"temperature": 0.4,
"stop_sequences": [],
}
def call_gemini(prompt: str, **overrides) -> dict:
cfg = {**DEFAULTS, **overrides}
response = client.models.generate_content(
model=cfg.pop("model", "gemini-2.5-flash"),
contents=prompt,
config=types.GenerateContentConfig(**cfg),
)
return {
"text": response.text,
"finish_reason": response.candidates[0].finish_reason.name,
"input_tokens": response.usage_metadata.prompt_token_count,
"output_tokens": response.usage_metadata.candidates_token_count,
"thinking_tokens": getattr(response.usage_metadata, "thoughts_token_count", 0),
}usage_metadata をテキストと一緒に返すことで、呼び出し元が個別に計測コードを足さなくてもコスト追跡ができます。Gemini 2.5/3 系の thinking トークン数も忘れずに含めておくと、後で分析するときに迷子になりません。
ハマりやすい落とし穴
実装中によくある失敗を 3 つだけ挙げておきます。
- ストリーミングと併用したときの体感差: ストリーミングでは
stopSequencesヒット時に即座にストリームが終了するため、UI 側で「途中で文末が来た」演出が必要になります。詳しい制御は ストリーミング応答のチャンク制御 を参照してください。 - 長過ぎる入力で出力枠が圧迫される: モデルの総コンテキスト上限は入力+出力+thinking の合計です。100 万トークン入力したら出力余地は実質ゼロになります。長文要約では入力を分割するか、Flash 系で先行要約してから Pro に渡す二段構えが有効です。
- finish_reason の取りこぼし: 失敗時に
MAX_TOKENSを握りつぶしてしまうと「なぜか応答が短い日がある」というバグになります。応答が短く感じたらまずfinish_reasonを確認するのが鉄則です。途中切れの根本対策は 応答が途中で切れる問題のトラブルシューティング にまとめてあります。
全体を振り返って — 次にやってみてほしいこと
maxOutputTokens は安全弁、stopSequences は意図表現、と役割を分けて考えると整理しやすいです。本日の記事を試すなら、まず手元のもっとも使用頻度が高い API 呼び出しに finish_reason のロギングを足してみてください。1 週間ほど運用ログを眺めるだけで、MAX_TOKENS で切れているリクエストの割合が見えます。そこを起点に上限値を調整していくと、コストと品質のバランスが一気に取りやすくなります。
LLM API を本番に載せ続ける開発者として、本書の「指示の粒度」と「出力の予測可能性」の章は何度か読み返しています。