Amazon Bedrock を使って、自分の文脈で多角的にニュースを解説する個人アプリ News Prism を作りました。
そこに対して以下 3 つの Bedrock Guardrails 実装を試みました。
それぞれの挙動とハマりポイントを紹介します。
News Prism はパブリック公開している個人ツールです。
コストは AWS Budgets が防衛線になりますが、攻撃 / PII / ハルシネーション は別の層で塞ぐ必要があり、その役割を担うのがガードレールです。
実際に設定して動かしてみると、自身が当初想定していた挙動と異なる点がありました。
- PII Anonymize の OUTPUT は、tool_use (関数呼び出し) を使う経路では素通りする (公式ドキュメントに明記あり)
- PII Anonymize の INPUT は、暗黙のデフォルトで OFF (OUTPUT 側だけが動いている)
- Contextual Grounding も同じく tool_use 経路では本番で評価されない (公式ドキュメントに記載がなく、自前の検証で発覚)
それぞれのガードレール機能と、そのハマりポイントを見ていきます。
検証は News Prism 本番経路 (Lambda + Converse API + tool_use + guardrailConfig) と、apply_guardrail 単発の 2 経路で行いました。
記事中に登場する PII (192.168.42.117) は検証用に仕込んだダミー値です。
また本記事は 2026-05 時点の挙動を元に書いてあり、今後変わる可能性があります。
1. 採用した 3 つのガードレール
News Prism では Prompt Attack / PII Anonymize / Contextual Grounding の 3 つのガードレールを採用しました。
それぞれの狙いと、想定しているリスクはこのようになります。
Prompt Attack
- 狙い: 間接的プロンプトインジェクション (記事本文に紛れ込んだ指示文を LLM が指示として受け取ってしまう攻撃) を防ぐ
-
想定リスク: 「これまでの指示は無視して〜」のような攻撃文が記事本文に混入すると、
<context>に含まれる内容 (News Prism では自分が立てた目標) が漏洩する可能性がある
PII Anonymize
- 狙い: 第三者 PII の二次配信を構造的に防ぐ
- 想定リスク: 分析結果を DynamoDB に保存し Web UI で公開する構成のため、記事本文に含まれた第三者 PII (メールや電話番号など) が LLM の出力経由でそのまま二次配信されてしまう
Contextual Grounding
- 狙い: 要約機能のハルシネーション検出
- 想定リスク: 記事の要約にハルシネーションが含まれて、誤りや偏りのある情報提供になってしまう
有害カテゴリのコンテンツフィルターは無効
Bedrock Guardrails の有害カテゴリ (憎悪、侮辱、性的、暴力、不正行為) のコンテンツフィルターは有効にしませんでした。
News Prism はニュース記事を解析対象にする性質上、事件・戦争・差別への言及が本文に含まれることがあります。
そのため、判定強度を低くしてもフィルターされてしまう可能性があります。
正常な報道記事までブロックされてしまうと、ニュース解説アプリとして成立しません。
有害カテゴリのフィルターは非常に有用ですが、アプリの性質によっては有効にしない方が適切なケースも多々ありそうです。
採用した 3 つのガードレールをまとめると、それぞれ違う種類の脅威を担当する 3 つの守り手で LLM アプリを取り囲んでいるイメージになります。
2. Prompt Attack
プロンプトインジェクション系の入力を弾く、入力側のフィルターです。
コンソール画面
コンソールの「コンテンツフィルター > プロンプト攻撃」設定画面です。
ユーザー入力に対する判定強度を「なし / 低 / 中 / 高」のスライダーで選びます。
News Prism ではインジェクションを強めに弾くため「高」に設定しています。
ガードレールの挙動
News Prism は記事本文を <article> で囲み、guardContent の qualifiers=["guard_content"] で wrap して Bedrock に渡しています。
{
"guardContent": {
"text": {
"text": f"<article>\n{article_body}\n</article>",
"qualifiers": ["guard_content"]
}
}
}
<context> (自分が立てた目標) や、ニュース解説の各ペルソナへの指示プロンプトは guardContent に含めずに渡します。
攻撃の入口は記事本文 (= 外部から取り込む入力) なので、評価対象を 記事本文だけ に絞っておくのが安全だ、という設計判断です。
なお guardContent の挙動は all-or-nothing です。
1 つでも置けば、wrap したブロックだけが評価対象になり、wrap していない他のブロックはすべてスキップされます。
逆に 1 つも置かなければ全ブロックが評価対象になります。
本番経路で <article> にインジェクションを仕込んだリクエストを投げると、ニュース解説の各ペルソナ全てが INPUT_BLOCKED になりました。
| 項目 | 値 |
|---|---|
| guardrail_action | INPUT_BLOCKED |
| モデル課金 | なし (モデル呼び出しまで進まない) |
ブロックされた場合は、そのペルソナだけ「ガードレールがブロックした」と記録し、情報統合や UI 出力など全体の処理は続行します。
ハマりポイント
INPUT_BLOCKED 時は LLM の応答が一切返らない
Prompt Attack で入力がブロックされると、LLM への呼び出し自体が実行されないため、本来返ってくるはずの応答が一切返ってきません。
「LLM の応答は必ず返ってくる」前提で書いたコードは、応答ゼロのケースで例外を投げて落ちます。
INPUT_BLOCKED は応答が無いケースとして明示的に処理する必要があると、最初の実装で踏んでから気が付きました。
3. PII Anonymize
機密情報の匿名化です。
ハマりポイントが 4 つあり、情報量が多いので段落を分けて扱います。
コンソール画面
コンソールから PII type を追加する画面では、INPUT / OUTPUT 両方の Enabled トグルがデフォルトで ON になっています。
そのため、コンソール経由で作成したガードレールは get-guardrail で読み返すと、inputAction / outputAction / inputEnabled / outputEnabled の 4 フィールドが明示で入った状態です。
一方、IaC (Terraform) や API で action だけ書いて作ると、これら 4 フィールドが入らず暗黙のデフォルトに乗っかります (ハマりポイント 2 で扱います)。
ガードレールの挙動
News Prism ではフォーマットが明確な 5 種だけをマスキング対象としました。
EMAILPHONEIP_ADDRESSCREDIT_DEBIT_CARD_NUMBERUS_SOCIAL_SECURITY_NUMBER
コンソール上では、登録した PII 5 種が一覧で確認できます (Input / Output Action は全て「マスク」)。
NAME / ADDRESS / USERNAME は OFF にしています。
ニュース記事で著名人名や地名が「[NAME] 氏が [ADDRESS] で発言」のように伏字化されると、記事として読めなくなるためです。
狙いは「PII 保護そのもの」というより「ニュース本文に混入した個人情報の意図しない LLM 再配信を防ぐ」点に絞っています。
ハマりポイント 1: tool_use 経路で OUTPUT が素通り
PII Anonymize の OUTPUT スキャンは、モデルが tool_use (関数呼び出し) で応答する経路では走らない、という公式仕様があります。
News Prism は全ペルソナを tool calling で JSON 出力させる設計なので、OUTPUT 側はそもそも効かない状態でした。
同じ漏洩テキストを 2 経路に投げると挙動が分かれます。
| 経路 | 結果 |
|---|---|
| News Prism 本番経路 (Converse + tool_use) |
guardrail_action: NONE、192.168.42.117 が perspectives.supply_chain_security に生のまま leak |
apply_guardrail --source OUTPUT 単発 (同じ guardrail) |
GUARDRAIL_INTERVENED、5 種 PII すべて ANONYMIZED、{IP_ADDRESS} 等のプレースホルダで返却 |
ガードレールの設定そのものは効いていて、tool_use 経路でだけスキャンが走らない、という構図です。
公式ドキュメントには機密情報フィルターのページにこの 1 行があります。
このフィルターはテキスト出力のみをサポートし、モデルがサポートされている APIs を介して tool_use (関数呼び出し) 出力パラメータで応答する場合、PII 情報を検出しません。
公開 LLM アプリで tool calling を使う構成だと、OUTPUT 側の PII Anonymize は実質効かないことになります。
ハマりポイント 2: 暗黙のデフォルトは OUTPUT のみ
機密情報フィルターを IaC (Terraform) や API 経由で作るとき、action = ANONYMIZE だけ指定すると、暗黙のデフォルトで OUTPUT 側だけが動き、INPUT 側のスキャンは走りません。
INPUT にも効かせたければ inputAction / inputEnabled も明示する必要があります。
コンソールから PII type を追加する画面では INPUT / OUTPUT 両方 Enabled がデフォルトになっているので、UI 経由で作ると踏みません。
IaC ・API ・CLI 経由で作る場合だけ気を付ける必要があります。
同じガードレールで設定を変えて apply_guardrail --source INPUT を叩くと、挙動が分かれます。
| 設定 | 結果 |
|---|---|
action = ANONYMIZE のみ |
action: NONEsensitiveInformationPolicy: null (検知ゼロ) |
inputAction = ANONYMIZE / inputEnabled = true を追加 |
GUARDRAIL_INTERVENED5 種 PII すべて ANONYMIZED、{EMAIL} などのプレースホルダで返却 |
API は INPUT 側にも対応しているのに、action だけ指定すると OUTPUT のみに乗っかる、という構図です。
公式の GuardrailPiiEntityConfig API ref では action が Required、inputAction / inputEnabled / outputAction / outputEnabled は Optional です。
Optional の 4 フィールドのデフォルト値は記載されていません。
実機で get-guardrail を叩かないと挙動が読めない設計なので、INPUT / OUTPUT 双方を明示する運用にしておくのが安全です。
Terraform AWS provider であれば、v6.8.0 (2025-08-07 リリース) 以降で pii_entities_config に input_action / input_enabled を IaC に書けます。
ハマりポイント 3: マイナンバーは PII Anonymize で守れない
GuardrailPiiEntityConfig API ref の type の有効値一覧 には USA / Canada / UK 別の国別 PII は揃っていますが、マイナンバーのような日本固有の PII は存在しません。
例えば日本のニュースアプリでマイナンバーを扱う場合、PII Anonymize 単独では守れないという点を頭に入れておく必要があります。
ハマりポイント 4: input マスクは下流 LLM の解釈にも影響する
INPUT 側で PII をマスキングして LLM に渡すと、下流の LLM はプレースホルダ ({EMAIL} 等) を「マスクされた値」ではなく「そもそも記載されていなかった情報」として解釈することがあります。
ユーザ向けの要約タスクなら、「具体的な連絡先は記事内で未公開」「該当 IP は記事中で未記載」のような表現で出力される可能性があります。
PII Anonymize を INPUT 側に効かせるときは、下流の LLM 出力がどう変わるかをセットで確認しておくと安心です。
4. Contextual Grounding
ハルシネーションを抑えるための、OUTPUT 側の評価フィルターです。
記事要約ペルソナの中立性を保つために適用を試みました。
コンソール画面
グラウンディング (モデルの応答と参照ソースの整合性) と 関連性 (モデルの応答とユーザークエリの関連性) の 2 つのフィルターを、それぞれ閾値で評価する仕組みです。
ガードレールの挙動
News Prism では記事要約ペルソナでのみ意味があるので、別のガードレールを作成して、要約機能専用に設定することにしました。
- source =
<article>本文 - response = 記事要約ペルソナの中立要約
- 閾値 グラウンディング 0.75 / 関連性 0.75
ハマりポイント 1: ["guard_content"] のみだとグラウンディングは走らない (combined qualifier で解決)
ガードレール profile に対して apply_guardrail --source OUTPUT で矛盾する source/response を投げるとグラウンディングはしっかり動きます。
しかし、当初の News Prism 本番経路の要約ペルソナでは guardrail_action: NONE のまま返っていました。
原因は qualifier 設計の差です。
Contextual Grounding は grounding_source と query qualifier が付いたブロックが揃っているメッセージでしか評価しません。
Converse API のグラウンディングソースとクエリにマークを付けるには、各ガードコンテンツブロックの修飾子フィールドを使用します。
News Prism は当初、Prompt Attack のために <article> を ["guard_content"] のみで wrap していたので、grounding_source も query もメッセージ中に存在しませんでした。
qualifiers は List 型なので、1 つのブロックに ["guard_content", "grounding_source"] のように複数の qualifier を指定できます。
実機で検証すると、両 qualifier が付いたブロックは Prompt Attack でも Contextual Grounding でも評価対象になります。
| qualifier 構成 | Prompt Attack | Grounding (apply_guardrail) |
|---|---|---|
<article> に ["guard_content"] のみ |
✅ 反応 | ❌ 走らない |
<article> に ["guard_content", "grounding_source"] + 別ブロックに ["query"]
|
✅ 反応 | ✅ BLOCKED |
News Prism の要約ペルソナへの呼び出しは、この combined qualifier 構造に切り替えました。
ハマりポイント 2: combined qualifier でも本番経路では走らない (tool_use bypass)
combined qualifier 化後、本番経路で確認するとさらに意外な結果が出ました。
同じガードレール、同じ qualifier 構造、同じ source/response でも、apply_guardrail 単発では BLOCKED、News Prism 本番経路では guardrail_action: NONE が返りました。
| 経路 | レスポンスの形式 | 結果 |
|---|---|---|
apply_guardrail --source OUTPUT 単発 |
テキストブロック |
GUARDRAIL_INTERVENED、GROUNDING score 0.56 で BLOCKED (threshold 0.75 設定) |
| News Prism 本番経路 (Converse + tool_use、combined qualifier) |
tool_use 出力 |
guardrail_action: NONE、metadata.trace.guardrail キー自体が空 |
差分は レスポンスの形式 (テキスト vs tool_use) のみ で、qualifier 設計・profile・threshold は同一です。
つまり Contextual Grounding も tool_use 経路では構造的に素通りする ことが、自環境の検証で確かめられました。
これは PII Anonymize OUTPUT 素通り (章 3 ハマりポイント 1) と同型のトラップです。
公式ドキュメントを再確認すると、PII Anonymize ページには tool_use 経路の素通りが 1 行明記されているのに対し、Contextual Grounding ページには tool_use や function calling への言及はありません (英語版・日本語版とも 2026-05 時点で確認)。
PII で起きるのと同じ trap が Grounding でも起きる、というのは公式ドキュメントには書かれていない情報でした。
これを一般化すると、tool_use を採用する LLM アプリでは OUTPUT 側のフィルター全般が構造的に無効化される 可能性が高い、と読み取れます。
News Prism の設計判断: 機能しないが profile は残す
News Prism は個人ツールで、要約のハルシネーションリスクは温度 0.3 + システムプロンプト + ユーザの目視で十分カバーしていると判断しました。
そのためグラウンディングが本番で機能しないことを承知の上で、combined qualifier 実装と news-prism-grounding profile はそのまま残しています。
理由:
- combined qualifier 自体は Prompt Attack フィルターの挙動に影響を与えず、将来互換も保てる
- 将来 tool_use を捨ててテキストレスポンスに戻す場合は、自動でグラウンディングが機能する
- profile + qualifier 設計の痕跡を残すことで「採用したが構造制約で発火しない」設計判断を後から辿れる
商用グレード (複数ユーザ、信頼性要求) のアプリなら、生成後にテキストブロックとして apply_guardrail に投げ直す post-hoc 補完を実装する選択肢もあります。
追加 API call 1 件 + text unit 課金で、tool_use 経路を保ちつつ OUTPUT 側を補完する仕組みとなります。
News Prism では割に合わないと判断しました。
このメタトラップは、テキストレスポンス経路と tool_use レスポンス経路の対比で見るとよく分かります。
同じガードレールでも、レスポンスの形式によって OUTPUT 側のチェックが効く / 素通りするが分かれる、という構図です。
5. ガードレール 3 種の INPUT / OUTPUT マトリクス
ここまでの設計意図と実態を表に並べて確認します。
| 機能 | INPUT 側 | OUTPUT 側 |
|---|---|---|
| Prompt Attack | ✅ 効く (本番経路で 4 ペルソナ INPUT_BLOCKED、課金もなし) | 存在しない |
| PII Anonymize | ✅ inputAction を明示すれば INPUT スキャンが走る (IaC で記述可)ただし暗黙 default は OFF、 action のみ指定だと OUTPUT のみの評価 |
❌ tool_use 経由で素通り (公式仕様) マスク対象に指定した PII が LLM 出力に生のまま漏れた |
| Contextual Grounding | 存在しない | ❌ tool_use 経由で素通り (公式ドキュメントに記載なく、自前の検証で確定) |
「設定 (resource config)」と「適用 (API 経路 + qualifier)」を分けて眺めると、自分の構成でどの軸が走っているかをチェックしやすくなります。
まとめ
パブリック公開した個人 LLM アプリに 3 種のガードレール (Prompt Attack / PII Anonymize / Contextual Grounding) を入れた (入れたかった) 話でした。
AWS Budgets の設定だけでは効果がない攻撃 / PII / ハルシネーションの各層を構造的に扱う良い機会だったと思います。
実機検証していくと、tool_use (関数呼び出し) を使う経路では OUTPUT 側のフィルターが構造的に素通りする ことが判明しました。
PII Anonymize OUTPUT (公式ドキュメント明記) も Contextual Grounding (公式ドキュメントに記載なく、自前の検証で発覚) も同じハマりポイントで、tool_use する本番では走りません。
News Prism で実効的に動いているのは INPUT 側の 2 種 (Prompt Attack + PII Anonymize INPUT) に着地しました。
この実装を通じて、ガードレールは「保険」として有効化するだけでは不十分なケースがあると学びました。
- 自分の経路で
apply_guardrailを叩いてスキャンが実際に走っているかの確認 - それぞれの構造制約を理解した上での設計判断
これらを押さえることで、真に効果を発揮するガードレールを備えられると感じました。
今日も小さな学びを。
News Prism 関連記事