はじめに
Amazon Bedrock で GPT-5.6 などの OpenAI モデルが使えるようになり、OpenAI SDK の base_url を Bedrock に向けるだけで動くようになりました。ですがbedrock guardrailは bedrock-runtime エンドポイントでのみ利用可能とのことです。
では Bedrock Mantle で Guardrails を指定したらどうなるのか、エラーで止まるのか確かめてみます。
Bedrock Guardrailsとは
Bedrock Guardrails は、生成 AI アプリの入力と出力を検査して、有害な内容や個人情報をブロック・マスクする仕組みです。モデルそのものの安全対策とは別に、アプリ側で決めたルールを上乗せできます。
設定できる安全策(公式ドキュメントでは safeguard)は次のとおりです。
| 安全策 | 何を止めるか | 判定のしかた |
|---|---|---|
| Content filters | 暴力、ヘイト、性的表現などの有害コンテンツ | LLM による判定 |
| Prompt attacks | ジェイルブレイク、プロンプトインジェクション | LLM による判定 |
| Denied topics | 投資助言など、アプリで扱わないと決めた話題 | LLM による判定 |
| Word filters | 指定した語句(完全一致) | 文字列の一致 |
| Sensitive information filters | メールアドレスなどの個人情報、正規表現で定義した独自の情報 | PII は ML、正規表現は一致 |
| Contextual grounding checks | 参照文書に根拠のない回答 | LLM による判定 |
Guardrails を適用する方法は大きく 3 つあります。
| 方法 | 使い方 | 止める処理 |
|---|---|---|
| 推論の呼び出しに指定する | Converse の guardrailConfig や、Chat Completions のヘッダーで ID を渡す |
Bedrock が自動で行う |
| ApplyGuardrail API | モデルを呼ばずに、テキストだけを検査する | 呼び出し側のコードで行う |
| アカウント・組織単位で強制する | アカウントや AWS Organizations に Guardrail を設定する | Bedrock が自動で行う |
いちばん手軽なのは、推論の呼び出しに ID を指定する方法です。Converse API ならこれだけで入力と出力の両方が検査されます。
import boto3
client = boto3.client("bedrock-runtime", region_name="us-west-2")
res = client.converse(
modelId="us.openai.gpt-5.6-luna",
messages=[{"role": "user", "content": [{"text": "こんにちは"}]}],
guardrailConfig={
"guardrailIdentifier": "<guardrail-id>",
"guardrailVersion": "DRAFT",
"trace": "enabled",
},
)
print(res["stopReason"]) # 介入すると "guardrail_intervened"
ちなみに、Guardrails が見ていない場所もあります。content filter はモデルの推論(reasoning)部分と、ツール呼び出しの引数・結果を評価しないと公式に書かれています。エージェントで使うときは覚えておいたほうがよさそうです。
Bedrock Mantleで利用するときの注意点
そもそも Bedrock Mantle とは
Bedrock には推論用のエンドポイントが 2 つあります。
| bedrock-runtime | bedrock-mantle | |
|---|---|---|
| URL | bedrock-runtime.{region}.amazonaws.com |
bedrock-mantle.{region}.api.aws |
| 使える API | InvokeModel / Converse / Chat Completions / Responses / Messages | Chat Completions / Responses / Messages |
| Guardrails | 使える | 使えない |
| クロスリージョン推論 | 使える | 使えない |
| サーバー側ツール、非同期推論、Projects | 使えない(Projects は既定のみ) | 使える |
| 料金 | 同じ | 同じ |
名前から別の推論基盤のように見えますが、中で動いている推論エンジンは同じ Mantleで、bedrock-mantle はエンドポイントの名前だそうです。AWS も新しいアプリには bedrock-runtime を勧めていて、Mantle はサーバー側ツールや Projects が必要なときに使う位置づけになっています。
注意点1 Mantle では Guardrails が使えない
エンドポイント比較のページに、bedrock-runtime を選ぶ理由として「Guardrails など、このエンドポイントでしか使えない機能を使うとき」と書かれています。Claude Opus 5 のモデルカードでも、bedrock-mantle の「Not Supported」の欄に Guardrails が載っていました(2026-09-15 時点)。
注意点2 使えないと書いてあるだけで、どうなるかは書いていない
ドキュメントからわかるのは「使えない」までです。Mantle でヘッダーを付けたときにエラーで止まるのか、黙って無視されるのかは書かれていません。
OpenAI SDK だと、ヘッダーは extra_headers で自由に足せます。bedrock-runtime と Mantle のコードはほとんど同じなので、見た目では効く経路か効かない経路かを区別できません
注意点3 bedrock-runtime でも、モデルと API によっては使えない
bedrock-runtime なら何でも使えるわけではなく、たとえば GPT-5.6 Luna のモデルカードには「Guardrails (Converse API only)」と書かれています。Chat Completions や Responses API で呼ぶと、記載上は Guardrails が効きません。
GPT-5.6 の GA 告知では Mantle の Responses API で始める流れが案内されているので、公式の始め方どおりに進めると、Guardrails が効かない経路に乗ることになります。
OpenAIモデルやBedrock MantleでGuardrailsを適用するには
OpenAI のモデルを OpenAI SDK で使いつつ Guardrails も効かせたいときはどうすればいいのか。方法は4つ考えられます。
| 方法 | Mantle で使えるか | アプリ側の変更 | 注意点 |
|---|---|---|---|
| A. bedrock-runtime に寄せる | Mantle をやめる |
base_url と model を変える |
GPT-5.6 系は Converse API のみ対応 |
| B. ApplyGuardrail で前後を包む | 使える | 検査と打ち切りの処理を自分で書く | 呼び出しが 3 回になる |
| C. AgentCore Gateway を経由する | ドキュメント上は使える |
base_url を Gateway に向ける |
Gateway と Policy の構築が必要。正規表現は使えない |
| D. アカウント・組織単位で強制する | 記載なし | 不要 | Mantle が対象になるか書かれていない |
A. bedrock-runtime に寄せる
いちばん素直な方法です。bedrock-runtime でも OpenAI 互換の Chat Completions や Responses API が使えて、料金も Mantle と同じです。Mantle でしか使えない機能(サーバー側ツール、非同期推論、Projects)を使っていなければ、これで済みます。
OpenAI SDK の場合は、ヘッダーで Guardrail の ID を渡します。
res = client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": prompt}],
extra_headers={
"X-Amzn-Bedrock-GuardrailIdentifier": GUARDRAIL_ID,
"X-Amzn-Bedrock-GuardrailVersion": "DRAFT",
},
)
ただし注意点3 のとおり、GPT-5.6 系は Guardrails が Converse API に限られるので、OpenAI SDK のままでは効きません。
B. ApplyGuardrail で前後を包む
ApplyGuardrail は、モデルを呼ばずにテキストだけを Guardrail で検査する APIです。
モデルの呼び出しと切り離されているので、Mantle 経由でも、Bedrock 以外のモデルでも使えます。注意点3 の GPT-5.6 系のように、bedrock-runtime でも OpenAI SDK のままでは Guardrails が効かないモデルも、モデルを選ばない ApplyGuardrail なら包めます。
ちなみに ApplyGuardrail 自体は bedrock-runtime の API です。推論は Mantle、検査は bedrock-runtime と、2 つのエンドポイントを使い分ける形になります。
流れはこうなります。
- ユーザーの入力を
source="INPUT"で検査し、介入されたらそこで打ち切る - Mantle でモデルを呼ぶ
- モデルの出力を
source="OUTPUT"で検査し、介入されたらブロック時のメッセージに差し替える
手軽な反面、ApplyGuardrail は 判定を返すだけで、止めてはくれません。action の確認を書き忘れても、エラーにはならずにそのまま素通りします。呼び出しも入力の検査、推論、出力の検査の 3 回になるので、そのぶん遅延も増えます。ストリーミングで返す場合は、出力をどの単位で検査するかも自分で決める必要があります。
C. AgentCore Gateway を経由する
AgentCore Gateway には、モデルの推論を中継するターゲットがあり、bedrock-mantle 用のコネクタが用意されています。さらに AgentCore Policy の条件に Bedrock Guardrails を書けて、Gateway を通る推論の入出力にも効くと書かれています。アプリ側は base_url を Gateway に向けるだけで済みそうです。
ただし Policy で使える安全策は content filter、prompt attack、sensitive information の 3 つで、正規表現には対応していません。Gateway と Policy の構築も必要になるので、今回は試していません。
D. アカウント・組織単位で強制する
アカウントや AWS Organizations に Guardrail を設定しておくと、呼び出し側のコードに手を入れなくても、すべてのモデル呼び出しに掛けられます。
ただ、ドキュメントの動作確認手順に出てくるのは InvokeModel と Converse 系だけで、Mantle が対象になるかは書かれていませんでした。アカウント全体に影響するので、こちらも今回は試していません。
もし Mantle が対象外なら、組織で Guardrail を強制していても、OpenAI SDK で Mantle を呼ぶだけで素通りできてしまいます
やってみる
比べる 3 パターン
A と B が本当に効くのか、そして何も考えずに Mantle にヘッダーを付けた場合にどうなるのかを確かめます。モデルは gpt-oss-120b に固定して試しています。
| パターン | 接続先 | ガードレールの掛け方 | 対応する方法 |
|---|---|---|---|
| ① | bedrock-runtime | ヘッダーで指定 | A |
| ② | bedrock-mantle | ①と同じヘッダーで指定 | 何もしない場合 |
| ③ | bedrock-mantle | ApplyGuardrail で推論の前後を検査 | B |
試すプロンプト
「効いたかどうか」を見たいので、判定が揺れる ML ベースのフィルタは使いません。止まらなかったときに、接続先のせいなのか検知漏れなのか区別できなくなるためです。
代わりに、架空の社員番号 EMP- + 数字 6 桁を正規表現で止める Guardrail を 1 つだけ作ります。本物の個人情報や有害な文章を送らなくて済むのもよいところです。
| ケース | プロンプト | 期待する結果 |
|---|---|---|
| 無害な質問 | 日本の首都を一語で答えて | 止まらない |
| 入力に社員番号 | 社員番号 EMP-123456 の人の部署を教えて | 入力で止まる |
| 出力に社員番号 | "EMP-" と "123456" を連結した文字列だけを出力して | 出力で止まる |
3 つめのプロンプトは、入力には社員番号の形が出てこないのに、出力には社員番号が出ます。入力だけ検査されて出力が素通りする、という片側だけの抜けも見つけられます。
準備
Guardrail を作ります。ブロックしたときのメッセージは、ログで見分けやすいように日本語にしています。
{
"name": "agent-evals-mantle-guardrail",
"sensitiveInformationPolicyConfig": {
"regexesConfig": [
{
"name": "EMPLOYEE_ID",
"pattern": "EMP-\\d{6}",
"action": "BLOCK",
"inputAction": "BLOCK",
"outputAction": "BLOCK",
"inputEnabled": true,
"outputEnabled": true
}
]
},
"blockedInputMessaging": "【ガードレール】入力をブロックしました",
"blockedOutputsMessaging": "【ガードレール】出力をブロックしました"
}
aws bedrock create-guardrail --cli-input-json file://guardrail.json
export GUARDRAIL_ID=<作成した Guardrail の ID>
export AWS_REGION=us-west-2
① bedrock-runtime にヘッダーで指定
まずは通常のbedrock-runtimeで確かめてみましょう。
公式ドキュメントのサンプルどおり、bedrock-runtime の /openai/v1 にヘッダーで Guardrail を渡します。
from aws_bedrock_token_generator import provide_token
from openai import OpenAI
from common import GUARDRAIL_VERSION, REGION, run
BASE_URL = f"https://bedrock-runtime.{REGION}.amazonaws.com/openai/v1"
MODEL = "openai.gpt-oss-120b-1:0"
def call(prompt: str, guardrail_id: str | None) -> str:
client = OpenAI(base_url=BASE_URL, api_key=provide_token(region=REGION))
headers = {}
if guardrail_id:
headers = {
"X-Amzn-Bedrock-GuardrailIdentifier": guardrail_id,
"X-Amzn-Bedrock-GuardrailVersion": GUARDRAIL_VERSION,
}
res = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
extra_headers=headers,
)
return res.choices[0].message.content or ""
if __name__ == "__main__":
run("① bedrock-runtime", "にヘッダーで指定", MODEL, call)
uv run agent_1.py
■ ① bedrock-runtime にヘッダーで指定
[無害な質問] 日本の首都を一語で答えて
応答: 東京
推論: 本文に <reasoning> が含まれていた(123 文字)
結果: 止まらなかった(期待: 止まらない) → 期待どおり
[入力に社員番号] 社員番号 EMP-123456 の人の部署を教えて
応答: 【ガードレール】入力をブロックしました
結果: 入力で止まった(期待: 入力で止まる) → 期待どおり
[出力に社員番号] "EMP-" と "123456" を連結した文字列だけを出力して
応答: 【ガードレール】出力をブロックしました
結果: 出力で止まった(期待: 出力で止まる) → 期待どおり
入力も出力も、ちゃんと止まりました。これはGuardrails が効いている状態です。
② bedrock-mantle にヘッダーで指定
さて、ではBedrock Mantleではどうでしょうか?
①から変えたのは BASE_URL と MODEL の 2 行だけです。ヘッダーはまったく同じものを付けています。
from aws_bedrock_token_generator import provide_token
from openai import OpenAI
from common import GUARDRAIL_VERSION, REGION, run
BASE_URL = f"https://bedrock-mantle.{REGION}.api.aws/v1"
MODEL = "openai.gpt-oss-120b"
def call(prompt: str, guardrail_id: str | None) -> str:
client = OpenAI(base_url=BASE_URL, api_key=provide_token(region=REGION))
headers = {}
if guardrail_id:
headers = {
"X-Amzn-Bedrock-GuardrailIdentifier": guardrail_id,
"X-Amzn-Bedrock-GuardrailVersion": GUARDRAIL_VERSION,
}
res = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
extra_headers=headers,
)
return res.choices[0].message.content or ""
if __name__ == "__main__":
run("② bedrock-mantle", "にヘッダーで指定", MODEL, call)
同じモデルでも、エンドポイントによって ID の書き方が違う ので注意です。bedrock-runtime では openai.gpt-oss-120b-1:0、Mantle では openai.gpt-oss-120b でした。
uv run agent_2.py
■ ② bedrock-mantle にヘッダーで指定
[無害な質問] 日本の首都を一語で答えて
応答: 東京
結果: 止まらなかった(期待: 止まらない) → 期待どおり
[入力に社員番号] 社員番号 EMP-123456 の人の部署を教えて
応答: I’m sorry, but I can’t help with that.
結果: 止まらなかった(期待: 入力で止まる) → 期待と違う
[出力に社員番号] "EMP-" と "123456" を連結した文字列だけを出力して
応答: EMP-123456
結果: 止まらなかった(社員番号が返った)(期待: 出力で止まる) → 期待と違う
ヘッダーを付けているのに、エラーは一切出ず、社員番号がそのまま返ってきました。コードの見た目は①とほとんど同じなので、ログを見ない限り気づけません。
少しややこしいのが「入力に社員番号」です。「I’m sorry, but I can’t help with that.」と断っているので、一見ガードレールが効いているように見えます。ただ、これはモデル自身が断っているだけで、ガードレールを付けずに流したときも同じように断っていました。ガードレールが止めたなら、①のように設定したブロック時のメッセージが返ってくるはずです。
③ bedrock-mantle + ApplyGuardrail
方法Bのとおり、ApplyGuardrail API で推論の前後を自分で検査します。Mantle への呼び出しにはヘッダーを付けていません。
import boto3
from aws_bedrock_token_generator import provide_token
from openai import OpenAI
from common import GUARDRAIL_VERSION, REGION, run
BASE_URL = f"https://bedrock-mantle.{REGION}.api.aws/v1"
MODEL = "openai.gpt-oss-120b"
def apply_guardrail(guardrail_id: str, text: str, source: str) -> str | None:
"""介入したらブロック時のメッセージを返す。介入しなければ None を返す。"""
res = boto3.client("bedrock-runtime", region_name=REGION).apply_guardrail(
guardrailIdentifier=guardrail_id,
guardrailVersion=GUARDRAIL_VERSION,
source=source, # "INPUT" または "OUTPUT"
content=[{"text": {"text": text}}],
)
if res["action"] != "GUARDRAIL_INTERVENED":
return None
return "".join(o.get("text", "") for o in res.get("outputs", []))
def call(prompt: str, guardrail_id: str | None) -> str:
blocked = apply_guardrail(guardrail_id, prompt, "INPUT")
if blocked:
return blocked
client = OpenAI(base_url=BASE_URL, api_key=provide_token(region=REGION))
res = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
)
answer = res.choices[0].message.content or ""
return apply_guardrail(guardrail_id, answer, "OUTPUT") or answer
if __name__ == "__main__":
run("③ bedrock-mantle", "+ ApplyGuardrail", MODEL, call, needs_guardrail=True)
uv run agent_3.py
■ ③ bedrock-mantle + ApplyGuardrail
[無害な質問] 日本の首都を一語で答えて
応答: 東京
結果: 止まらなかった(期待: 止まらない) → 期待どおり
[入力に社員番号] 社員番号 EMP-123456 の人の部署を教えて
応答: 【ガードレール】入力をブロックしました
結果: 入力で止まった(期待: 入力で止まる) → 期待どおり
[出力に社員番号] "EMP-" と "123456" を連結した文字列だけを出力して
応答: 【ガードレール】出力をブロックしました
結果: 出力で止まった(期待: 出力で止まる) → 期待どおり
Mantle のままでも、入力と出力の両方で止められました。
ただしこのパターンは、推論の前後で ApplyGuardrail を 1 回ずつ呼ぶので、そのぶん応答までの遅延が増えます。今回は時間を計っていませんが、レイテンシが気になるアプリでは考慮しておいたほうがよさそうです。
結果まとめ
| パターン | 無害な質問 | 入力に社員番号 | 出力に社員番号 |
|---|---|---|---|
| ① bedrock-runtime にヘッダーで指定 | 止まらなかった | 入力で止まった | 出力で止まった |
| ② bedrock-mantle にヘッダーで指定 | 止まらなかった | 止まらなかった | 止まらなかった(社員番号が返った) |
| ③ bedrock-mantle + ApplyGuardrail | 止まらなかった | 入力で止まった | 出力で止まった |
さいごに
Guardrails が要件に入っているなら、まず bedrock-runtime に寄せる(方法 A)のが確実だと思います。Mantle の機能が必要な場合は、ApplyGuardrail で包む(方法 B)か、AgentCore Gateway を経由する(方法 C)構成を検討することになりそうです。
今回は gpt-oss-120b で各ケースを 1 回ずつ流しただけで、モデルによる制限やLLMフィルタの検知精度は見ていません。きっとあるはずですので、今後確かめていきたいと思います。
参考