このエラーに遭遇した方へ
Gemini 2.5 Pro 以降の Thinking モードでマルチターンの Function Calling を実装している方なら、こんなメッセージに足を引っ張られた経験があるのではないでしょうか。
google.api_core.exceptions.InvalidArgument: 400 ...
Thoughts must be present in conversation history when using thinking signature.
公式ドキュメントには「thought_signature を会話履歴に含めて返してください」と一行書かれているだけで、どこをどう直せばよいのか掴みにくい部分です。私自身もエージェントの本番化作業中、ツール呼び出しの2ターン目で突然これに当たり、原因の切り分けに半日溶かしました。
エラーが出る本当の原因
Gemini 2.5/3.x の Thinking モデルは、Function Calling の最中に内部的な「思考」を行い、その結果を parts[].thought_signature(Base64 エンコードされた不透明トークン)として応答に含めます。このトークンには、モデルが次のターンで応答を継続するための内部状態が暗号化されて入っています。
マルチターン会話で次のリクエストを送るときに、この thought_signature を会話履歴の中の関数呼び出しパーツに紐付けたまま API へ送り返す必要があります。逆に言えば、そのまま渡せば良いだけなのですが、以下の場面で「うっかり剥がす」事故が起きます。
function_callだけを取り出して履歴を再構築している- 自前で
Contentオブジェクトをdictから組み立て直して送信している - レスポンスを JSON で別言語の SDK に渡し、不要そうなフィールドを
nullで削っている - LangChain など外部ラッパーが古いバージョンで
partsを再加工している
つまり「履歴を加工しすぎた」ことが原因です。これを最初に頭に入れておくと、調査の方向が定まります。
最短の修正:応答 Content をそのまま append する
google-genai Python SDK での正しい実装は、たった一行の意識で十分です。
import os
from google import genai
from google.genai import types
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
tools = [{
"function_declarations": [{
"name": "get_weather",
"description": "都市の天気を取得します",
"parameters": {
"type": "OBJECT",
"properties": {"city": {"type": "STRING"}},
"required": ["city"],
},
}]
}]
config = types.GenerateContentConfig(
tools=tools,
thinking_config=types.ThinkingConfig(thinking_budget=1024),
)
# 1ターン目
contents = [types.Content(
role="user",
parts=[types.Part.from_text(text="東京の天気を調べて要点だけ教えてください")],
)]
res1 = client.models.generate_content(
model="gemini-2.5-pro", contents=contents, config=config,
)
# ⭐ ここがキモ:応答の content をそのまま履歴に追加する
contents.append(res1.candidates[0].content)
# function_call を実行して結果を返す
fc = res1.candidates[0].content.parts[0].function_call
tool_result = {"city": fc.args["city"], "temp_c": 18, "summary": "晴れ"}
contents.append(types.Content(
role="user",
parts=[types.Part.from_function_response(
name=fc.name, response=tool_result,
)],
))
# 2ターン目(thought_signature が保持されているので成功する)
res2 = client.models.generate_content(
model="gemini-2.5-pro", contents=contents, config=config,
)
print(res2.text)
# 期待出力: "東京は晴れ、気温は18度です。" のような最終回答ポイントは contents.append(res1.candidates[0].content) の一行です。応答の Content オブジェクトを分解せずまるごと履歴に渡すことで、parts[].thought_signature が自動的に保持されます。
やりがちな間違いと修正パターン
逆に、以下のように書くと確実にエラーが再現します。
# Bad: function_call だけを抜き出して再構築している
fc = res1.candidates[0].content.parts[0].function_call
contents.append(types.Content(
role="model",
parts=[types.Part(function_call=fc)], # thought_signature が消える
))
# → 次のリクエストで Thoughts must be present ... が出る修正は単純で、応答全体をそのまま使うだけです。
# Good
contents.append(res1.candidates[0].content)履歴を JSON にシリアライズしてキャッシュや Redis に保存する設計を取っている場合も、同じ罠にハマります。Pydantic の model_dump_json(exclude_none=True) などで None を一律落としていると、thought_signature 以外でも壊れる場合があります。私は exclude_none=False を使うか、保持すべきフィールドを明示的に列挙する設計に切り替えました。再出現したらまずシリアライズ層を疑うのが早いです。
ストリーミングで詰まったときの集約処理
generate_content_stream でストリームを受け取る構成では、最後のチャンクだけを履歴に積むとほぼ確実に再発します。全チャンクのパーツを集約して最終 Content を組み立ててください。
final_parts = []
function_call = None
for chunk in client.models.generate_content_stream(
model="gemini-2.5-pro", contents=contents, config=config,
):
if not chunk.candidates or not chunk.candidates[0].content.parts:
continue
for part in chunk.candidates[0].content.parts:
final_parts.append(part) # thought_signature を含むパーツも漏らさず集める
if part.function_call:
function_call = part.function_call
contents.append(types.Content(role="model", parts=final_parts))Gemini 3.x では thought_signature が応答の途中チャンクに入ってくるケースが増えており、最後のチャンクだけ拾うと欠落します。集約処理を抜いた瞬間に再発するので、ストリーミング実装を疑うときはここを最初に確認してください。
REST API を直接叩いている場合の落とし穴
Curl や fetch で generateContent を呼んでいる方も、考え方は同じです。レスポンスの candidates[0].content をそのまま次回リクエストの contents 配列の末尾に積みます。
# 1ターン目の結果から content を取り出して次のリクエストに混ぜる
RESP1=$(curl -s -H "x-goog-api-key: $GEMINI_API_KEY" \
-H "Content-Type: application/json" \
-d @req1.json \
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent")
# jq で content をそのまま抜き、次の contents に追加する
MODEL_TURN=$(echo "$RESP1" | jq '.candidates[0].content')
jq --argjson m "$MODEL_TURN" '.contents += [$m] | .contents += [{
"role":"user",
"parts":[{"functionResponse":{"name":"get_weather","response":{"temp_c":18}}}]
}]' req1.json > req2.jsonここで jq 'del(.candidates[0].content.parts[].thoughtSignature)' のように削ってしまうと、まさに今回のエラーが顔を出します。生 JSON を扱う時こそ thoughtSignature フィールドの存在を意識してください。
それでも消えないときのチェックリスト
最後に、上記のいずれを試してもエラーが残る場合のチェックポイントを並べておきます。
- SDK のバージョンが古くないか(
google-genai>=1.0.0推奨。google-generativeaiの旧 SDK は EOL) thinking_config.thinking_budget=0になっていないか(0 だと thought_signature 自体が発行されないため、履歴に入っていなくてもエラーは出ませんが Thinking の恩恵も消えます)- LangChain / LlamaIndex のラッパー側で
partsを再構築していないか(両者とも最新版で thought_signature 保持に対応済み。古いバージョンは要更新) - Vertex AI と Google AI Studio で履歴を相互流用していないか(
thought_signatureは発行元エンドポイントでのみ有効で、混在すると署名検証で弾かれます) - リトライ層で同じリクエストを別ターンとして扱っていないか(リトライ時は contents をそのまま再送するのが原則)
最後の Vertex AI / Studio 切り替えは意外な落とし穴です。開発中に Studio で取った会話履歴をそのまま本番の Vertex AI に流すと弾かれます。履歴は同一エンドポイントで完結させるのが安全です。
次の一手
このエラー、結局のところ「履歴を素直に渡せば消える」のが本質です。応答の content をそのまま contents 配列に append するという一行さえ守れば、9 割の事例は解消します。残りの 1 割はストリーミング集約とエンドポイント混在の 2 点を順番に潰してください。
Function Calling 自体で他にも詰まっている箇所がある方は、症状別に整理した Gemini Function Calling が機能しない原因と解決策 も合わせて読むと早く片付くかもしれません。ツール呼び出しが無限ループに陥る別系統の問題は Gemini API でツール呼び出しが無限ループする時の対処 で扱っています。