私自身、個人開発の傍らで4つの技術ブログを自動投稿で回している関係で、無人で走るバッチがこれに引っかかったことがあります。題材として渡したコード断片やエラーメッセージのごく一部が DANGEROUS_CONTENT 寄りに判定され、生成が途中で止まる。人がいないので、翌朝ログを見るまで誰も気づきません。このメモは、そのとき「全カテゴリ OFF にして終わり」にせず、誤検知だけを拾って安全側に戻すために組んだ仕組みの記録です。
各候補とプロンプトフィードバックには safety_ratings が付き、要素ごとに category・probability(NEGLIGIBLE / LOW / MEDIUM / HIGH)・blocked(真偽)が入ります。これをそのまま構造化ログに落とし、カテゴリ別に集計します。
from collections import Counterdef extract_ratings(resp): """入力側・出力側の safety_ratings を平坦化して返す""" rows = [] pf = getattr(resp, "prompt_feedback", None) if pf and getattr(pf, "safety_ratings", None): for r in pf.safety_ratings: rows.append(("input", str(r.category), str(r.probability), bool(r.blocked))) for cand in (resp.candidates or []): for r in (cand.safety_ratings or []): rows.append(("output", str(r.category), str(r.probability), bool(r.blocked))) return rowsdef summarize(logged_rows): """蓄積した rows からカテゴリ別の誤検知傾向を出す""" blocked = Counter() medium_plus = Counter() for _side, cat, prob, was_blocked in logged_rows: if was_blocked: blocked[cat] += 1 if prob in ("MEDIUM", "HIGH"): medium_plus[cat] += 1 return blocked, medium_plus
ここで重要なのは、blocked の件数だけでなく MEDIUM 止まり(ブロックには至らないが境界に近い)の分布も一緒に見ることです。MEDIUM が特定カテゴリに偏って積み上がっているなら、そのカテゴリは「いまは耐えているが、入力がわずかに変われば落ちる」予備軍です。本番で突然ブロックが増える事故は、たいていこの予備軍が閾値をまたいだ瞬間に起きます。率で持っておくと、事故になる前に気づけます。
なお probability はあくまで安全方針上の確からしさであって、出力内容の正しさとは別物です。ここを取り違えて「LOW だから内容も安全」と読むと判断を誤ります。フィルタが見ているのは方針適合であって事実性ではありません。
ブロックが出たときに最も簡単なのは、4カテゴリすべてを OFF にすることです。ですが、これは本番では勧めません。理由は二つあります。一つは、無人運用ではモデルが本当に不適切な出力をしたときの最後の歯止めまで外れること。もう一つは、OFF(フィルタ完全無効)はそもそもキーや環境によって使えないことがあり、コードが環境差で壊れる温床になることです。
classify_block の戻り値(INPUT_BLOCKED / OUTPUT_BLOCKED / OK)を必ず構造化ログのフィールドに出し、カテゴリ別のブロック率と MEDIUM 率を週次で眺められるようにします。
OFF を使っている箇所があるなら、それが検証用に限定され本番経路に紛れていないかを確認します。本番経路では OFF を使わない設計を推奨します。
緩めた判断は測り直す
閾値をいじったら、いじりっぱなしにしないことも大切です。私の無人バッチでは、原因カテゴリを一段だけ緩めたことで、OUTPUT_BLOCKED の誤検知率がおおよそ4%から0.5%程度まで下がりました。一方で、緩めたカテゴリの HIGH レーティングが増えていないかは、同じログで継続して見ています。誤検知を1件減らす代わりに、本当に止めるべき出力を1件通してしまっては本末転倒だからです。