Interactions API の previous_interaction_id で会話状態をサーバーに預けると、毎回履歴を送らずに済みます。ところが途中で入った巨大なツール出力を後から刈り込めず、以降のターンが重量を引きずる落とし穴があります。サーバー側状態と手元での刈り込みを状況で使い分ける分岐設計を、動くPythonの薄いラッパーとともに整理します。
この記事は、その分岐点の設計についての記録です。会話状態をサーバーに預けるべきか、手元に置いて刈り込めるようにしておくべきか。Interactions API の previous_interaction_id が省いてくれるものと、代わりに預けてしまうものを切り分け、状況で使い分けるための薄いラッパーを、動くコードとともに残しておきます。
previous_interaction_id が省くもの、預けるもの
Interactions API では、直前の応答が返す interaction の ID を次のリクエストの previous_interaction_id に渡すと、それまでの会話文脈をサーバー側が保持したまま続きを処理してくれます。手元で messages の配列を積み上げて毎回丸ごと送り直す、あの反復から解放される仕組みです。
以下は、その判断を一箇所に閉じ込めた薄いラッパーです。呼び出し側は会話 ID を渡すだけで、連鎖を継ぐか切り直すかを意識せずに済みます。API のメソッド名や引数は提供形態で変わり得るため、実際の呼び出しは公式ドキュメントで確認してください。ここで示したいのは、その一段上の「刈り込む判断をどこに置くか」の設計です。
import refrom dataclasses import dataclass, field# 1ターンのツール出力がこの推定トークン数を超えたら、# 連鎖に載せ続けず要約へ差し替える判断に入るHEAVY_TURN_TOKEN_THRESHOLD = 4000def estimate_tokens(text: str) -> int: """日本語混在テキストの粗いトークン見積もり。 厳密なトークナイザではなく、閾値判定に足る近似で十分。 英数字は約4文字/トークン、それ以外(主に日本語)は約1.6文字/トークンとして合算する。""" ascii_chars = len(re.findall(r"[\x00-\x7F]", text)) other_chars = len(text) - ascii_chars return int(ascii_chars / 4 + other_chars / 1.6)@dataclassclass Conversation: """会話ごとの状態。server_interaction_id が生きていれば連鎖を継ぐ。 切り直した場合はここに手元要約(carryover)を蓄える。""" server_interaction_id: str | None = None carryover: list[str] = field(default_factory=list) # 手元に残す短い要約def summarize_heavy_output(raw: str) -> str: """重い出力を、次ターン以降に引きずる価値のある最小限へ畳む。 実際にはここに軽量モデルの1回呼び出しやルールベース抽出を置く。 要点は「何が確定したか」だけを残し、原文は捨てること。""" head = raw.strip().splitlines()[0][:80] if raw.strip() else "" return f"[前ターンのツール出力を要約] {head} … 詳細は破棄済み"def send_turn(conv: Conversation, user_text: str, tool_output: str | None, call_model) -> tuple[str, Conversation]: """1ターンを送る。call_model は実際の Interactions API 呼び出しを包む関数で、 (prompt_parts, previous_interaction_id) を受け取り (reply_text, interaction_id) を返す。""" heavy = tool_output is not None and \ estimate_tokens(tool_output) > HEAVY_TURN_TOKEN_THRESHOLD if heavy or conv.server_interaction_id is None: # 連鎖を切り直す: これまでの carryover を手元で先頭に積み、 # 重いツール出力はモデルに渡す前に要約へ差し替える parts: list[str] = list(conv.carryover) if tool_output is not None: folded = summarize_heavy_output(tool_output) if heavy else tool_output parts.append(folded) if heavy: conv.carryover.append(folded) # 次回以降に引きずるのは要約だけ parts.append(user_text) reply, new_id = call_model(prompt_parts=parts, previous_interaction_id=None) else: # happy path: サーバー側連鎖に素直に継ぐ。手元は空でよい parts = [tool_output, user_text] if tool_output else [user_text] reply, new_id = call_model(prompt_parts=parts, previous_interaction_id=conv.server_interaction_id) conv.server_interaction_id = new_id return reply, conv
7 月 6 日から Interactions API の対応呼び出しの実行ログが AI Studio のダッシュボードで確認できるようになりました。各実行ステップと消費が追えるので、まずはここが一次情報になります。ただしダッシュボードのログは保持期間やサンプリングの制約があり、これ自体を課金監査の正本にはできません。