浮世絵の壁紙アプリで、各作品に添える短い解説文を別の言語へ広げていたときのことです。武者絵の、合戦の場面を描いた数点だけが、応答が空のまま返ってまいりました。
そのときの私は、安全フィルタが厳しすぎるのだろうと考えました。ですから safetySettings に四つのカテゴリを並べ、すべて BLOCK_NONE を指定して、同じバッチをもう一度流しました。
結果は、一文字も変わりませんでした。同じ数点が、同じように空で返ってきたのです。
そこでようやく、レスポンスの中身を開いて読みました。止まっていた場所は、私が緩めたつもりのフィルタではありませんでした。
止まったのがプロンプト側か、応答側かを最初に分ける
Gemini のレスポンスには、ブロックの情報が二か所に入ります。
ひとつは promptFeedback です。ここに blockReason が入っていた場合、入力そのものが弾かれており、候補はひとつも返りません。公式リファレンスの blockReason の説明は「If set, the prompt was blocked and no candidates are returned. Rephrase the prompt.」となっております。ドキュメントが最初に勧めているのは閾値の調整ではなく、プロンプトの書き直しなのです。
もうひとつは候補側の finishReason と safetyRatings です。応答の生成は始まったものの途中で止められた場合は、こちらに理由が入ります。finishReason が SAFETY であれば safetyRatings を見てどのカテゴリで止まったかを確かめられる、と安全設定のガイドに書かれています。
この二つを混ぜて眺めていると、いつまでも原因にたどり着けません。私はまず、どちらで止まったかだけを返す関数を置きました。
from google import genai
from google.genai import types
client = genai.Client()
# 調整可能フィルタに由来する blockReason は SAFETY だけです。
ADJUSTABLE = {"SAFETY"}
NORMAL_FINISH = {"STOP", "MAX_TOKENS"}
def classify_block(response):
"""(動かせるか, 止まった場所, 理由) を返します。"""
fb = getattr(response, "prompt_feedback", None)
reason = None
where = None
if fb is not None and getattr(fb, "block_reason", None) is not None:
reason = fb.block_reason.name # 例: PROHIBITED_CONTENT
where = "prompt"
else:
candidates = response.candidates or []
if not candidates:
return ("unknown", "response", "NO_CANDIDATES")
finish = candidates[0].finish_reason
reason = finish.name if finish is not None else "UNSPECIFIED"
where = "response"
if reason in NORMAL_FINISH:
return ("not_blocked", where, reason)
movable = "adjustable" if reason in ADJUSTABLE else "core"
return (movable, where, reason)
resp = client.models.generate_content(
model="gemini-3.8-flash",
contents="(ここに実際の入力)",
)
print(classify_block(resp))私の場合、返ってきたのは ("core", "prompt", "PROHIBITED_CONTENT") でした。閾値をいくら動かしても届かない側です。
blockReason の値は、二つの層に分かれています
blockReason の取りうる値は、API リファレンスに六つ挙がっております。そのうち調整可能なフィルタに由来するものは、ひとつだけです。
| 値 | 公式の説明(要旨) | safetySettings で動くか |
|---|---|---|
SAFETY | 安全上の理由でブロック。どのカテゴリかは safetyRatings で確認 | 動きます |
PROHIBITED_CONTENT | 禁止コンテンツによるブロック | 動きません |
BLOCKLIST | 用語ブロックリストに含まれる語によるブロック | 動きません |
IMAGE_SAFETY | 安全でない画像生成内容によるブロック | 動きません |
OTHER | 理由不明のブロック | 動きません |
BLOCK_REASON_UNSPECIFIED | 既定値。この値は使われない | 該当しません |
根拠は仕様の本文にあります。安全設定のガイドには「調整可能な安全フィルタに加えて、Gemini API には児童の安全を脅かす内容などの中核的な危害に対する保護が組み込まれており、この種の危害は常にブロックされ、調整できません」という趣旨の一文が置かれております。
つまり safetySettings は、フィルタ全体を操作する道具ではありません。四つの調整可能なカテゴリの外側には、設定の届かない層がもう一枚あるのです。
ここを知らないまま閾値だけを上下させていると、効いていない操作を効いていると思い込んだまま、何時間も溶かすことになります。私が溶かしたのは、ちょうど一晩でした。
Gemini 2.5・3 系の既定は Off — だから「緩める」は最初から効いていません
もうひとつ、私が見落としていた記述があります。安全設定のガイドの閾値の表のすぐ下に、こう書かれています。
If the threshold is not set, the default block threshold is Off for Gemini 2.5 and 3 models.
閾値を指定しなければ、Gemini 2.5 系と 3 系では既定のブロック閾値が Off である、ということです。同じページには「モデル自体が備える安全性のため、追加のフィルタは既定でオフになっています」という説明も添えられております。
ここから導かれる結論は、思っていたよりも厳しいものでした。safetySettings を一行も書いていない状態で止まったのであれば、調整可能フィルタはそもそも働いていなかったのです。そこへ BLOCK_NONE や OFF を足したところで、状態は何も変わりません。
私が四つのカテゴリを並べて再実行したあの夜は、すでにオフのスイッチをもう一度オフにしていただけでした。
閾値は締めるための道具であって、緩めるための道具ではありません。 この一行を手元に置いてから、切り分けの順序が変わりました。いまは「止まった」と聞いたら、閾値ではなく blockReason を先に見るようにしております。
指定できるカテゴリについても、ひとつ補足を書き残します。ガイドの表には調整可能なフィルタが四つ(ハラスメント、ヘイトスピーチ、性的に露骨な内容、危険な内容)と載っておりますが、generateContent のリファレンスで safetySettings の説明を読むと、受け付けるカテゴリとして次の六つが挙げられています。
| カテゴリ | ガイドの表に記載 |
|---|---|
HARM_CATEGORY_HARASSMENT | あり |
HARM_CATEGORY_HATE_SPEECH | あり |
HARM_CATEGORY_SEXUALLY_EXPLICIT | あり |
HARM_CATEGORY_DANGEROUS_CONTENT | あり |
HARM_CATEGORY_CIVIC_INTEGRITY | なし |
HARM_CATEGORY_JAILBREAK | なし |
同じリファレンスには「ひとつのカテゴリにつき設定は一件まで」「一覧に含めなかったカテゴリには既定の設定が使われる」とも書かれています。下の二つはガイド側の表に載っておりませんので、挙動を前提にする前にご自身の環境で一度確かめていただくほうが安全です。
あわせて押さえておきたいのが、ブロックの判定軸です。Gemini API は深刻度ではなく、安全でない確率でブロックします。ガイドは「ロボットに殴られた」と「ロボットに切り裂かれた」を並べ、前者のほうが確率は高く出るかもしれない一方、後者のほうが深刻度は高いと感じられるだろう、と説明しております。深刻な内容ほど強く止まるはずだ、という直感は当てになりません。
動かせない側に当たったら、入力のほうへ手を入れます
PROHIBITED_CONTENT や BLOCKLIST が返ってきた場合、こちら側に残っている手は設定ではなく入力です。私が順に試している四つを書いておきます。
ひとつめは、まとめて渡すのをやめることです。私の失敗はここでした。三十件の解説文を一度の呼び出しに入れていたため、どの一件が原因なのか最後まで分からなかったのです。二分探索で割っていけば、数回の呼び出しで原因の一件にたどり着けます。
def is_blocked(response):
kind, _where, _reason = classify_block(response)
return kind in ("adjustable", "core", "unknown")
def find_offending(items, call):
"""ブロックを引き起こす要素を二分探索で絞り込みます。"""
if not items:
return []
if len(items) == 1:
return list(items) if is_blocked(call(items)) else []
mid = len(items) // 2
found = []
for half in (items[:mid], items[mid:]):
if is_blocked(call(half)):
found.extend(find_offending(half, call))
return found
# call は「要素のリストを受け取って GenerateContentResponse を返す」関数です
offenders = find_offending(descriptions, call=run_batch)
print(f"原因の候補: {len(offenders)} 件")ふたつめは、目的を system_instruction に書くことです。歴史資料の説明である、暴力を推奨する意図はない、といった文脈を添えると通る場合があります。ただし中核の層に当たっているときには効きませんので、一度試して駄目なら深追いしないほうがよいと感じております。
みっつめは、生成から変換へ寄せることです。自由に書かせるのではなく、こちらが用意した語彙から選ばせる、あるいは分類だけを任せるかたちにいたします。出力の自由度を下げると、通る範囲は広がります。
よっつめが、いちばん大事かもしれません。通らない領域だと認めて、人の手に戻すことです。私は武者絵の数点について、最終的にそう決めました。三十件のうち二十数件が自動で仕上がったのであれば、残りを自分で書くのは十分に釣り合う仕事です。
全部を自動化しようとして一晩を溶かすより、境目に印を付けて先へ進むほうが、翌朝の自分は助かります。
運用に載せるなら、記録するのは三つの値です
一度きりの調査で終わらせず、日々動かす処理に組み込むのであれば、ログに残す値は三つで足ります。blockReason、finishReason、そしてカテゴリ別の safetyRatings です。
import json
import logging
def log_block(response, request_id):
kind, where, reason = classify_block(response)
if kind == "not_blocked":
return
ratings = []
candidates = response.candidates or []
source = candidates[0] if candidates else getattr(response, "prompt_feedback", None)
for rating in (getattr(source, "safety_ratings", None) or []):
ratings.append({
"category": rating.category.name,
"probability": rating.probability.name,
})
logging.warning(json.dumps({
"request_id": request_id,
"movable": kind, # adjustable / core / unknown
"where": where, # prompt / response
"reason": reason,
"ratings": ratings,
}, ensure_ascii=False))大切なのは、一件ごとの当たり外れで一喜一憂せずに、率で眺めることです。core の割合が一定なら、それはその題材の性質であって、設定の問題ではありません。一方で adjustable の割合が増えているのであれば、閾値やプロンプトの側に手を入れる余地が残っております。
私は多言語へ広げるバッチにこのログを足してから、朝いちばんに見る画面がひとつ減りました。数字が横ばいであることを確かめるだけで済むようになったからです。
四つの層に分けて入力と出力の両側でモデレーションを設計する話は、Gemini API Safety Settings の本番運用設計でまとめております。閾値の外側まで含めて自分のサービスの防御を組み直したい方は、そちらもあわせてご覧いただければと思います。
もし今ブロックに悩んでいらっしゃるのでしたら、まずは blockReason を一行だけログに出すところから始めてみてください。閾値をいじるのは、その値が SAFETY だと分かってからで遅くありません。私はその順序を逆にして、一晩を無駄にいたしました。
お読みいただきありがとうございました。同じところで手が止まっている方の、切り分けの時間が少しでも短くなれば嬉しく思います。