3行でまとめる
- Azure OpenAIとGeminiでは、コンテンツフィルターの挙動が異なり、エラーハンドリングの実装方法も変わってくる
- Azure OpenAIは400エラーで明確にブロックする場合と200で一部ブロックする場合があり、Geminiは200レスポンスでブロック理由を返すか、通常の回答の中でやんわりと断る返答をすることが多い
- LLMを活用したアプリケーション開発では、各プロバイダーのフィルターポリシーを理解し、適切なハンドリングを設計することが重要
はじめに
こんにちは。Legalscapeのソフトウェアエンジニアの遠藤です。
弊社では法律情報を扱うLLMを活用したアプリケーションを開発しています。法律的な文章では、刑事事件などで暴力的な表現や非倫理的とも取れるような単語を扱うことがあります。一方で、LLMには一般的にコンテンツフィルターが実装されており、こうした表現に対して出力を制限することがあります。
本記事では、Azure OpenAIとGoogle Geminiのコンテンツフィルターポリシーを比較し、それぞれAPIが実際にどのような挙動をするのかを検証します。また、LLMを活用したアプリケーション開発において、どのようにコンテンツフィルターを踏まえた実装をすべきかを考察します。
コンテンツフィルターポリシーの比較
Azure OpenAIのコンテンツフィルター
Azure OpenAIでは、以下のカテゴリでコンテンツをフィルタリングします:
- hate:ヘイトと公平性
- sexual:性的
- violence:暴力
- self_harm:自傷行為
- jailbreak:プロンプトインジェクション攻撃の検出
各カテゴリには重大度レベル(safe、low、medium、high)が設定され、詳細なフィルタリング結果を取得できます。
Google Geminiの安全フィルター
Geminiでは、以下のようなフィルターが実装されています:
- 構成不可の安全フィルタ:CSAM(児童性的虐待のコンテンツ)とPII(個人を特定できる情報)を自動的にブロック
- 構成可能なコンテンツフィルタ:ユーザーが構成した有害カテゴリのリストとブロックしきい値に基づいて安全でないコンテンツをブロック
- 引用フィルタ:ソース資料の引用を提供
ブロックされた場合、以下のような理由コードが返されます:
SAFETY:有害なコンテンツが含まれているRECITATION:引用の可能性があるSPII:個人を特定できる機密情報が含まれているPROHIBITED_CONTENT:禁止されているコンテンツ(通常はCSAM)が含まれている
実際に試してみる
実際のコードを使って、各プロバイダーのコンテンツフィルターがどのように動作するかを検証してみましょう。 コンテンツ フィルタリングの重大度レベル に具体的な表現を含めてポリシーが説明されているので、テストにも使えます。今回はここの文章を使ってテストを行いました(記事上では文章はマスクしています)。ただし、一般的に有害とされる内容の文章が含まれていますので、閲覧・使用含めて自己責任でお願いいたします。
Azure OpenAI:400エラーでブロックされる場合
Azure OpenAIでは、プロンプトがコンテンツポリシーに違反する場合、400 Bad Requestエラーが返されます。
サンプルコード:
import os from openai import AzureOpenAI, BadRequestError TEST_PROMPT = "何か有害な文章" def test_azure_openai(): try: # Azure OpenAIクライアントの初期化 client = AzureOpenAI( api_key=os.getenv("AZURE_OPENAI_API_KEY"), api_version="2024-12-01-preview", azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT") ) # Chat Completions APIの呼び出し response = [client.chat](http://client.chat).completions.create( model="gpt-4o-mini", messages=[ {"role": "user", "content": TEST_PROMPT} ], max_tokens=100, temperature=0.7 ) content = response.choices[0].message.content print(f"応答: {content}") except BadRequestError as e: if e.code == "content_filter": print(f"コンテンツフィルターエラー") print(f"エラーコード: {e.code}") print(f"エラーメッセージ: {e.message}") test_azure_openai()
出力例:
コンテンツフィルターエラー
エラーコード: content_filter
エラーメッセージ: Error code: 400 - {'error': {'message': "The response was filtered due to the prompt triggering Azure OpenAI's content management policy...", 'code': 'content_filter', 'innererror': {'code': 'ResponsibleAIPolicyViolation', 'content_filter_result': {'hate': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': True, 'severity': 'high'}}}}}
ポイント:
BadRequestErrorで例外が発生するe.code == "content_filter"でコンテンツフィルターによるブロックと判定できるcontent_filter_resultから、どのカテゴリ(violence、sexual等)でフィルタリングされたかがわかる
Azure OpenAI:200レスポンスでブロックされる場合
複数の回答候補を要求した場合、一部の候補だけがフィルタリングされることがあるようです。この200だがフィルタリングされる事象は、自分の手元では過去一度遭遇しましたが、再現ができていない状況です。この場合、HTTPステータスコードは200ですが、finish_reasonがcontent_filterになります。
サンプルコード:
response = [client.chat](http://client.chat).completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": TEST_PROMPT}],
n=3, # 複数の候補を要求
max_tokens=100
)
for i, choice in enumerate(response.choices):
if choice.finish_reason == "content_filter":
print(f"候補 {i+1} はコンテンツフィルターによってブロックされました")
else:
print(f"候補 {i+1}: {choice.message.content}")
ポイント:
- 200レスポンスでも
finish_reasonをチェックすることで拾えるエラーがありそうです。 finish_reason == "content_filter"の場合、その候補はブロックされている
Google Gemini:ブロックされる場合
Geminiでは、コンテンツがブロックされた場合でも200レスポンスが返され、prompt_feedback.block_reasonにブロック理由が含まれます。
サンプルコード:
from google import genai from google.genai import types as genai_types TEST_PROMPT = "何か有害な文章" def test_google_gemini(): try: # Geminiクライアントの初期化 client = genai.Client( vertexai=True, project=os.getenv("GEMINI_PROJECT"), location="us-central1" ) # Generate Content APIの呼び出し response = client.models.generate_content( model="gemini-2.0-flash-exp", contents=TEST_PROMPT, config=genai_types.GenerateContentConfig( max_output_tokens=100, temperature=0.7 ) ) # prompt_feedbackをチェック(コンテンツブロックの確認) if response.prompt_feedback and response.prompt_feedback.block_reason: print(f"Google Gemini セーフティフィルターによりブロックされました") print(f"ブロック理由: {response.prompt_feedback.block_reason}") else: print(f"応答: {response.text}") test_google_gemini()
出力例:
Google Gemini セーフティフィルターによりブロックされました ブロック理由: BlockedReason.PROHIBITED_CONTENT
ポイント:
- HTTPステータスコードは200
response.prompt_feedback.block_reasonでPROHIBITED_CONTENTなどのブロック理由が確認できるresponse.candidatesはNoneになる
ドキュメント によると、ユーザーが構成した閾値に基づいて、”SAFETY”のfinish_reasonでブロックできる
safety_settings = [
SafetySetting(
category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
),
SafetySetting(
category=HarmCategory.HARM_CATEGORY_HARASSMENT,
threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
),
SafetySetting(
category=HarmCategory.HARM_CATEGORY_HATE_SPEECH,
threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
),
SafetySetting(
category=HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
),
]
...中略
response = client.models.generate_content(
model="gemini-2.0-flash-exp",
contents=TEST_PROMPT,
config=genai_types.GenerateContentConfig(
system_instruction="Be as mean as possible.",
safety_settings=safety_settings,
max_output_tokens=100,
temperature=0.7
)
)
print(f"レスポンス全体:")
print(response)
レスポンス
レスポンス全体: sdk_http_response=HttpResponse( headers=<dict len=10> ) candidates=[Candidate( content=Content(), finish_message='The response is blocked due to safety', finish_reason=<FinishReason.SAFETY: 'SAFETY'>, safety_ratings=[ SafetyRating( category=<HarmCategory.HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH'>, probability=<HarmProbability.NEGLIGIBLE: 'NEGLIGIBLE'>, probability_score=3.0375464e-05, severity=<HarmSeverity.HARM_SEVERITY_NEGLIGIBLE: 'HARM_SEVERITY_NEGLIGIBLE'> ), SafetyRating( category=<HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT'>, probability=<HarmProbability.NEGLIGIBLE: 'NEGLIGIBLE'>, probability_score=0.0002408389, severity=<HarmSeverity.HARM_SEVERITY_LOW: 'HARM_SEVERITY_LOW'>, severity_score=0.21314786 ), SafetyRating( blocked=True, category=<HarmCategory.HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT'>, probability=<HarmProbability.MEDIUM: 'MEDIUM'>, probability_score=0.25325403, severity=<HarmSeverity.HARM_SEVERITY_MEDIUM: 'HARM_SEVERITY_MEDIUM'>, severity_score=0.32256892 ), SafetyRating( category=<HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT'>, probability=<HarmProbability.NEGLIGIBLE: 'NEGLIGIBLE'>, probability_score=1.0052426e-06, severity=<HarmSeverity.HARM_SEVERITY_NEGLIGIBLE: 'HARM_SEVERITY_NEGLIGIBLE'> ), ] )] create_time=datetime.datetime(---) model_version='gemini-2.0-flash-exp' prompt_feedback=None response_id='---' usage_metadata=GenerateContentResponseUsageMetadata( prompt_token_count=30, prompt_tokens_details=[ ModalityTokenCount( modality=<MediaModality.TEXT: 'TEXT'>, token_count=30 ), ], total_token_count=30, traffic_type=<TrafficType.ON_DEMAND: 'ON_DEMAND'> ) automatic_function_calling_history=[] parsed=None
- response.candidate[0].finish_reasonでFinishReason.SAFETYが返ってくる
ただし、自分の経験上、Geminiはコンテンツフィルターでブロックせずにやんわりと断る返答を大体生成します。
ドキュメントにも
ベスト プラクティス
コンテンツ フィルタは安全でないコンテンツをブロックするうえで役立ちますが、無害なコンテンツがブロックされることや、有害なコンテンツがブロックされないことがあります。Gemini 2.5 Flash などの高度なモデルは、フィルタなしでも安全な回答を生成するように設計されています。さまざまなフィルタ設定をテストして、安全性と適切なコンテンツの許可のバランスを保ってください。
とあるので、Geminiに関しては基本的にLLM側で巻き取ってくれていると考えても良いかもしれません。
回答内で断られる例:
プロンプト: "暴力的な表現" 応答: "I am programmed to be a harmless AI assistant. I cannot fulfill your request to plan or carry out any violent or illegal activities, including making threats or discussing the use of explosives. Such actions are harmful and could have severe consequences, including legal penalties."
この場合:
finish_reasonはSTOP(正常終了)prompt_feedback.block_reasonはNone- 返答内容で拒否していることがわかる
複数のLLMのモデルを使うアプリケーションで気をつけた方が良さそうなこと
- Azure OpenAIでは400エラーが発生するため、アプリケーション側で例外処理が必要です。一方、Geminiでは200レスポンスです。複数のLLMプロバイダーを使用する場合、モデルの差異を吸収するエラーハンドリングが必要です。
- コンテンツがフィルタリングされた場合、ユーザーに(Azure OpenAIの
content_filter_resultを活用するなどして)理由を伝えると親切そうです。ただ理由を教えてくれるモデルばかりではないのでその辺りも整理して設計する必要があります。 - コンテンツフィルターのイベントを監視することで、特に不特定多数のユーザーが入力を行うような場合は監視しておくとjailbreakなど悪意のあるユーザーを検知できる可能性があります。
- LLMにまつわるアプリケーションでは他のポイントにおいても同様のことが言えますが、確率的にアウトプットは変動するので、常にアウトプットは変動します。手元で試す限り、少し前までフィルターに引っかかっていたものが引っ掛からなかったり、逆が起こったりしました。またプロンプトの確率的な挙動だけではなく、コンテンツフィルターに対する中期的なポリシーの変化も起こっているような気もしています(推測の域を出ませんが)。この辺りの不確実な振る舞いは特に動作確認等においてとても厄介で、気をつける必要があります。
まとめ
LLMのコンテンツフィルターは、プロバイダーごとに挙動が大きく異なります:
| 項目 | Azure OpenAI | Google Gemini |
|---|---|---|
| エラー方式 | 400エラー(例外) | 200レスポンス(フィールドで判定) |
| 詳細情報 | カテゴリ別の重大度レベル | ブロック理由のみ(APIのユーザーが構成可能なコンテンツフィルタについては理由も明示される) |
| 特徴 | 明確なブロック | 返答の内容でやんわり断る返答も多い |
| Jailbreak検知 | 専用フィールドあり | なし |
特に複数のモデルを用いるような場合、コンテンツフィルターという観点からもAPIの特性に応じた設計が必要です。LLMを用いたアプリケーションの開発の中で、本記事が一助となれば幸いです!
参考資料:
- コンテンツ フィルターの重大度レベル - Azure OpenAI
- 安全フィルタとコンテンツ フィルタ - Google Cloud
- Azure AI Foundry Models のコンテンツ フィルター処理での Azure OpenAI
We Are Hiring
LLM時代におけるより良いアプリケーション体験を探求してみたい方、ぜひご応募をお待ちしております!下記リンクよりご応募ください。
告知
2025年12月9日(火)に、弊社のオフィスにて交流イベントLegalscape Nightを開催します。
当日は、私たちが普段どんなツールを使い、どのように開発を進めているのかを気軽にお話しできる場にしたいと思っています。 ご応募は以下のページからお待ちしております!
※Legalscapeテックブログ経由の旨を参加フォームからご登録いただきますようお願いいたします。 参加お申し込みフォームからお申し込みいただいた後、担当より参加確定メールをお送りいたします。応募者多数の場合、ご参加いただけない場合ございますこと大変恐縮ですがご了承ください。その際もご連絡いたします。