先月、個人開発で運営しているアプリの AdMob 月次レポートを Gemini に集計させていたとき、静かにヒヤリとしました。「eCPM の加重平均は 312.4 円です」とモデルが書いてきたのですが、念のため自分で電卓を叩いたら 287.9 円だったのです。Code Execution を有効にしていたので、モデルは裏で実際に Python を走らせていました。ところが、その走らせた結果と、返ってきた文章の中の数値がずれていたのです。
Code Execution は「コードを実行する機能」だと理解されがちですが、本番で使ううえで重要なのはそこではありません。実際に実行された結果と、それを説明する自然文が、別々のパートとして返ってくる という構造です。この二つは一致するとは限りません。ここを分けて扱わないと、せっかくサンドボックスで正確に計算していても、最後の一歩でモデルが書き写した幻覚の数値を掴んでしまいます。
以下では、その二つを分離し、実行結果だけを唯一の真実として抽出し、文章側の数値と突き合わせて弾く検証ゲートを実装します。数値が業務判断に直結する場面、たとえば料金計算やレポート集計に Code Execution を使っている方に向けた内容です。
なぜモデルの文章を信頼できないのか
Code Execution を有効にすると、モデルは「コードを書く → 実行する → 結果を見て考える → 自然文で答える」という流れをたどります。このとき API のレスポンスには、少なくとも三種類のパートが混在します。
パート 中身 信頼度
executable_code モデルが生成した Python コード 実行された保証はある
code_execution_result サンドボックスが実際に返した標準出力と結果コード 唯一の真実
text 結果を要約した自然文 幻覚しうる
問題は最後の text です。モデルは実行結果を見たうえで文章を組み立てますが、その転記の段で桁を落としたり、途中式の値と最終値を取り違えたりします。私が遭遇したずれも、実行結果には正しい 287.9 が出ていたのに、文章では別の中間計算の値が最終値として書かれていた、というものでした。
つまり 実行された値は正しかったのに、それを日本語にする段で壊れていた わけです。ここを見抜くには、文章を読むのをやめて、code_execution_result だけを機械的に取り出す必要があります。
レスポンスの3パートを分離する
まず基本の受け取り方です。google-genai SDK では、code_execution ツールを渡すだけで機能が有効になります。返ってきた parts を型ごとに仕分けます。
from google import genai
from google.genai import types
client = genai.Client( api_key = "YOUR_API_KEY" )
response = client.models.generate_content(
model = "gemini-2.5-pro" ,
contents = (
"次の日次eCPM(円)の加重平均を、impressionsを重みとして求めてください。"
"day1: eCPM=280.0, impressions=12000 / "
"day2: eCPM=305.0, impressions=8000 / "
"day3: eCPM=290.0, impressions=15000"
),
config = types.GenerateContentConfig(
tools = [types.Tool( code_execution = types.ToolCodeExecution())],
),
)
# パートを型ごとに仕分ける
executed_code = [] # モデルが書いたコード
execution_outputs = [] # サンドボックスの実出力(真実)
prose = [] # 自然文(要約)
for part in response.candidates[ 0 ].content.parts:
if part.executable_code is not None :
executed_code.append(part.executable_code.code)
elif part.code_execution_result is not None :
execution_outputs.append(part.code_execution_result)
elif part.text is not None :
prose.append(part.text)
print ( "実行された出力:" , [r.output for r in execution_outputs])
# 実行された出力: ['288.42105263157896\n']
print ( "自然文:" , "" .join(prose)[: 60 ])
# 自然文: 加重平均eCPMは約288.4円です。...
ここで大切なのは、code_execution_result.output が サンドボックスの標準出力そのもの だという点です。モデルの解釈が一切入っていません。この値だけを下流に流すのが基本方針になります。
なお、パートの順序は「コード → 実行結果 → 文章」とは限らず、モデルが試行錯誤すると executable_code と code_execution_result が交互に何度も現れます。最後の実行結果が最終値とは限らないので、単純に「最後の一個」を取るのではなく、後述のように意図した最終計算を明示的に取り出す設計にします。
実行結果を構造化して取り出す
output は文字列の標準出力なので、そのままでは数値として扱えません。素朴に正規表現で数字を拾うと、途中式に現れた別の数字を誤って掴みます。ここは モデルに最終値を機械可読な形で print させる のが堅実です。プロンプトで出力フォーマットを固定します。
PROMPT_SUFFIX = (
"計算の最後に、最終結果だけを次の形式で1行だけ出力してください。"
"他の行にこの形式を使わないこと: RESULT_JSON={ \" weighted_ecpm \" : <値>}"
)
response = client.models.generate_content(
model = "gemini-2.5-pro" ,
contents = user_prompt + " \n " + PROMPT_SUFFIX ,
config = types.GenerateContentConfig(
tools = [types.Tool( code_execution = types.ToolCodeExecution())],
),
)
import json, re
def extract_executed_value (parts, key):
"""code_execution_result の標準出力から RESULT_JSON 行を拾い、
指定キーの値を返す。見つからなければ None。"""
marker = re.compile( r 'RESULT_JSON= ( \{ . * \} ) ' )
found = None
for part in parts:
if part.code_execution_result is None :
continue
for line in part.code_execution_result.output.splitlines():
m = marker.search(line)
if m:
try :
found = json.loads(m.group( 1 )).get(key)
except json.JSONDecodeError:
continue
return found # 複数回実行された場合は最後の RESULT_JSON を採用
executed = extract_executed_value(response.candidates[ 0 ].content.parts, "weighted_ecpm" )
print ( "実行された最終値:" , executed)
# 実行された最終値: 288.42105263157896
この extract_executed_value が返す値だけが、業務ロジックに入ってよい数字です。文章側の「約288.4円」は表示用の説明に過ぎず、判断には使いません。私自身、Dolice のレポート集計ではこの一次情報/表示情報の切り分けを徹底してから、数値のズレで悩むことがなくなりました。
OUTCOME を分類してリカバリを打つ
code_execution_result には outcome があり、実行が成功したかどうかを表します。ここを見ずに output だけ読むと、失敗して空になった出力を「結果ゼロ」と誤読する という厄介なバグを踏みます。
outcome 意味 打つべき手
OUTCOME_OK 正常終了 output を採用
OUTCOME_FAILED 実行時例外 output のトレースバックを見て再試行、またはプロンプト修正
OUTCOME_DEADLINE_EXCEEDED タイムアウト 入力を分割・軽量化して再実行
outcome を見たうえで、成功した実行結果からのみ値を採る関数にまとめます。
def resolve_execution (parts, key):
"""成功した実行結果からのみ最終値を採る。
失敗・タイムアウトは理由付きで返し、静かに握りつぶさない。"""
marker = re.compile( r 'RESULT_JSON= ( \{ . * \} ) ' )
ok_value = None
failures = []
for part in parts:
r = part.code_execution_result
if r is None :
continue
outcome = getattr (r.outcome, "name" , str (r.outcome))
if outcome != "OUTCOME_OK" :
failures.append((outcome, r.output[: 200 ]))
continue
for line in r.output.splitlines():
m = marker.search(line)
if m:
try :
ok_value = json.loads(m.group( 1 )).get(key)
except json.JSONDecodeError:
pass
if ok_value is None :
return { "status" : "no_value" , "failures" : failures}
return { "status" : "ok" , "value" : ok_value, "failures" : failures}
result = resolve_execution(response.candidates[ 0 ].content.parts, "weighted_ecpm" )
print (result)
# {'status': 'ok', 'value': 288.42105263157896, 'failures': []}
failures を捨てずに返しているのがポイントです。本番では、成功していても途中で一度失敗している実行はプロンプトが不安定なサインなので、監視ログに残して後で見直します。空応答の切り分け全般については、Gemini API の空応答を finish_reason から逆引きする診断フロー で扱った考え方がそのまま応用できます。
実行結果と文章を突き合わせる検証ゲート
ここが本題です。実行結果を真実として持ったうえで、モデルの文章がその真実と矛盾していないか を機械的にチェックします。矛盾していれば、その応答は表示に出さず弾きます。文章の数値がずれているということは、モデルが結果を正しく理解できていない兆候であり、そのまま読者に見せるべきではないからです。
def numbers_in_text (text):
"""文章中の数値(カンマ・小数対応)を集合で返す。"""
raw = re.findall( r '- ? \d[\d , ] * \. ? \d * ' , text)
vals = set ()
for r in raw:
try :
vals.add( float (r.replace( "," , "" )))
except ValueError :
pass
return vals
def verify_prose (executed_value, prose_text, rel_tol = 0.01 ):
"""文章中に、実行結果と相対誤差1%以内で一致する数値が
含まれているかを確認する。無ければ矛盾とみなす。"""
for v in numbers_in_text(prose_text):
if executed_value == 0 :
if v == 0 :
return True
elif abs (v - executed_value) / abs (executed_value) <= rel_tol:
return True
return False
executed_value = result[ "value" ]
prose_text = "" .join(prose)
if verify_prose(executed_value, prose_text):
final_answer = prose_text # 文章も実行結果と整合している
trusted_number = executed_value # 判断に使う数字は必ず実行結果
else :
# 文章がずれている → 表示を差し替える
final_answer = f "加重平均eCPMは { executed_value :.1f } 円です(自動整形)。"
trusted_number = executed_value
# 監視: モデルの転記ずれを記録
print ( "⚠ prose mismatch:" , prose_text[: 80 ])
print ( "表示:" , final_answer)
print ( "判断に使う値:" , trusted_number)
相対誤差で判定しているのは、文章側が四捨五入で「288.4」と書くことを許容するためです。桁を落とした丸めは許し、別の数を書いていたら弾く という線引きになります。私の AdMob のケースでは、文章に 312.4 しか含まれず実行結果の 287.9 と 1% 以内で一致する数値が無かったため、このゲートで確実に検出できました。
この「一次データを唯一の真実とし、表示や説明はそこから導出する」という考え方は、Code Execution に限らずデータを扱うシステム全般で効いてきます。設計の背景を掘り下げるなら、データ指向アプリケーションデザイン(Martin Kleppmann) の「信頼性」の章が、真実の源をどこに置くかという観点で参考になります。
本番で踏みやすい落とし穴
実装してみると、いくつか静かに間違えやすい点があります。
複数の実行結果パートを取り違える。 モデルが試行錯誤すると、途中で一度計算し、やり直してもう一度計算する、ということが起きます。素朴に最後の output を取ると、それが「最終回答のための計算」とは限りません。だからこそ前述のように RESULT_JSON= のような明示マーカーで最終値を特定し、順序依存を断ち切ります。
stdout に出ていない値を期待する。 サンドボックスは基本的に標準出力で結果を返します。print していない変数の値は取れません。「最後の式の値が返る」対話環境の感覚で書くと空振りするので、最終値は必ず print させます。
浮動小数の丸めで照合が過敏になる。 集計に割り算が入ると、実行結果と文章の丸め位置がずれます。完全一致で照合すると常に弾いてしまうため、相対誤差ベースにするのが実務的です。逆に、通貨のように丸めルールが決まっているなら、実行側で丸めまで済ませてから RESULT_JSON に載せると、表示と一次値の一致が取りやすくなります。
コストの見落とし。 Code Execution はコード実行分のトークンも消費します。検証のために毎回実行させるなら、リクエスト単位でトークンを記録しておくと後で効きます。この計上の仕方はGemini API の usageMetadata で本番コストを記録する実装パターン にまとめてあります。機能そのものの基本的な使い方をまず押さえたい場合は、Gemini Code Execution API にコードを書かせて実行する解説 から入ると流れがつかめます。
まとめ
Code Execution を数値の集計に使うなら、今日からひとつだけ変えてみてください。判断に使う数字を、モデルの文章からではなく code_execution_result.output から取り出す ようにすることです。表示用の文章はそのままでも構いませんが、業務ロジックに渡す値だけは実行結果を一次情報として通す。この一本の線を引くだけで、転記の幻覚に足をすくわれる事故はほぼ消えます。
私自身、レポート集計を自動化する中で「便利だから任せる」と「結果を検証する」は別の作業なのだと痛感しました。実行結果と文章を分けて扱う小さなゲートを一枚挟むだけで、自動化の安心感がずいぶん変わります。まだ模索の途中ですが、同じように数値を AI に任せている方の助けになれば嬉しいです。お読みいただきありがとうございました。