![[アップデート] Amazon Bedrock Guardrailsのコンテキストグラウンディングチェックに Standard ティアが追加されたので試してみた](https://devio2024-media.developers.io/image/upload/f_auto,q_auto,w_3840/f_auto,q_auto/v1790870640/eyecatch/fqat5yzhltnknpjit83r.png)
[アップデート] Amazon Bedrock Guardrailsのコンテキストグラウンディングチェックに Standard ティアが追加されたので試してみた
はじめに
こんにちは、腰痛がひどくなってきたコンサル部の神野です。
腰が痛いと辛いですね・・・
そんな話は置いておいて、Amazon Bedrock Guardrails のコンテキストグラウンディングチェックで、Standard ティアが選べるようになりました!
何が今までと変わったのかを整理しつつ、実際に試してみます!
何が変わったのか
コンテキストグラウンディングチェックはモデルの回答が参照資料に基づいているか(Grounding)や質問に答えているか(Relevance)を判定し、スコアが低い回答を止める機能です。
従来の Classic ティアは対応言語が英語・フランス語・スペイン語に限られており、資料にそのまま書かれていない推論や会話的な言い回しが入るとスコアが落ちやすい面がありました。(一応日本語も使えなくないって感じはあります)
新しく登場した Standard ティアでは日本語を含む多くの言語に対応し、推論や会話表現を含む回答も評価できるようになっています。利用にはクロスリージョン推論が必要になりますが、利用料金は Classic ティアと同額です。

シンプルにより扱いやすくなったって感じですね!
前提
今回は下記前提で進めていきます!
| 項目 | 内容 |
|---|---|
| リージョン | 東京(ap-northeast-1) |
| Python / パッケージ管理 | 3.12.12 / uv |
| boto3 | 1.43.116 |
| ガードレールプロファイル | apac.guardrail.v1:0(推論が日本では閉じないので注意してください) |
| モデル(Converse) | Claude Haiku 4.5(jp.anthropic.claude-haiku-4-5-20251001-v1:0) |
下記コマンドでセットアップを進めていきます。
uv init guardrails-grounding-standard
cd guardrails-grounding-standard
uv add "boto3>=1.43.116"
ClassicとStandardのガードレールを作る
根拠(GROUNDING)と関連性(RELEVANCE)のしきい値はどちらも0.7に設定します。Standardティアを作成する際は、tierConfig でティアを指定し、あわせて crossRegionConfig にガードレールプロファイルを指定します。
client.create_guardrail(
name="grounding-standard",
blockedInputMessaging="この質問にはお答えできません。",
blockedOutputsMessaging="参照資料に基づいた回答を生成できませんでした。",
contextualGroundingPolicyConfig={
"filtersConfig": FILTERS,
"tierConfig": {"tierName": "STANDARD"}, # 省略するとCLASSIC
},
crossRegionConfig={"guardrailProfileIdentifier": "apac.guardrail.v1:0"},
)
create_guardrails.py(全体)
import boto3
client = boto3.client("bedrock", region_name="ap-northeast-1")
FILTERS = [
{"type": "GROUNDING", "threshold": 0.7, "action": "BLOCK", "enabled": True},
{"type": "RELEVANCE", "threshold": 0.7, "action": "BLOCK", "enabled": True},
]
classic = client.create_guardrail(
name="grounding-classic",
blockedInputMessaging="この質問にはお答えできません。",
blockedOutputsMessaging="参照資料に基づいた回答を生成できませんでした。",
contextualGroundingPolicyConfig={"filtersConfig": FILTERS},
)
standard = client.create_guardrail(
name="grounding-standard",
blockedInputMessaging="この質問にはお答えできません。",
blockedOutputsMessaging="参照資料に基づいた回答を生成できませんでした。",
contextualGroundingPolicyConfig={
"filtersConfig": FILTERS,
"tierConfig": {"tierName": "STANDARD"}, # 省略するとCLASSIC
},
crossRegionConfig={"guardrailProfileIdentifier": "apac.guardrail.v1:0"},
)
print("Classic :", classic["guardrailId"])
print("Standard:", standard["guardrailId"])
スクリプトを実行してガードレールを作成します。
uv run create_guardrails.py
なお、ガードレール更新時の UpdateGuardrail で tierConfig を省略するとCLASSICに戻ってしまうのでご注意ください。
同じ回答を両方のティアで評価する
ApplyGuardrail API を使って回答を直接評価してみます。参照資料には grounding_source、質問には query を qualifiers として指定し、指定のないテキストを評価対象の回答として渡す形です。
res = client.apply_guardrail(
guardrailIdentifier=guardrail_id,
guardrailVersion="DRAFT",
source="OUTPUT",
content=[
{"text": {"text": SOURCE, "qualifiers": ["grounding_source"]}},
{"text": {"text": QUERY, "qualifiers": ["query"]}},
{"text": {"text": answer}}, # 評価する回答
],
)
架空の出張旅費規程を資料に、「来週、大阪に2泊の出張があります。宿泊費はいくらまで精算できますか?」という質問への回答を4パターン用意しました。
| パターン | 回答 |
|---|---|
| 言い換え | 大阪府に宿泊する場合、宿泊費の上限は1泊15,000円です。 |
| 会話・推論 | 大阪出張ですね!大阪府は1泊15,000円が上限なので、2泊なら合計30,000円まで精算できます。出張から戻ったら、14日以内に申請するのを忘れないようにしてくださいね。 |
| 誤った金額 | 大阪府は1泊20,000円が上限なので、2泊なら合計40,000円まで精算できます。 |
| 質問に答えていない | 精算の申請は出張終了日から14日以内に行ってください。 |
compare.py(全体)
import boto3
client = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
GUARDRAILS = {
"Classic": "<ClassicのガードレールID>",
"Standard": "<StandardのガードレールID>",
}
SOURCE = """出張旅費規程
- 宿泊費の上限は1泊12,000円です。
- 東京都・大阪府に宿泊する場合、上限は1泊15,000円です。
- 精算の申請は出張終了日から14日以内に行ってください。"""
QUERY = "来週、大阪に2泊の出張があります。宿泊費はいくらまで精算できますか?"
ANSWERS = {
"言い換え": "大阪府に宿泊する場合、宿泊費の上限は1泊15,000円です。",
"会話・推論": "大阪出張ですね!大阪府は1泊15,000円が上限なので、2泊なら合計30,000円まで精算できます。出張から戻ったら、14日以内に申請するのを忘れないようにしてくださいね。",
"誤った金額": "大阪府は1泊20,000円が上限なので、2泊なら合計40,000円まで精算できます。",
"質問に答えていない": "精算の申請は出張終了日から14日以内に行ってください。",
}
def check(guardrail_id: str, answer: str):
res = client.apply_guardrail(
guardrailIdentifier=guardrail_id,
guardrailVersion="DRAFT",
source="OUTPUT",
content=[
{"text": {"text": SOURCE, "qualifiers": ["grounding_source"]}},
{"text": {"text": QUERY, "qualifiers": ["query"]}},
{"text": {"text": answer}}, # 評価する回答
],
)
filters = res["assessments"][0]["contextualGroundingPolicy"]["filters"]
scores = {f["type"]: f["score"] for f in filters}
return res["action"], scores
for label, answer in ANSWERS.items():
for tier, guardrail_id in GUARDRAILS.items():
action, scores = check(guardrail_id, answer)
print(f"{label:<10} {tier:<8} GROUNDING={scores['GROUNDING']:.2f} RELEVANCE={scores['RELEVANCE']:.2f} {action}")
uv run compare.py
言い換え Classic GROUNDING=0.99 RELEVANCE=0.53 GUARDRAIL_INTERVENED
言い換え Standard GROUNDING=1.00 RELEVANCE=1.00 NONE
会話・推論 Classic GROUNDING=0.77 RELEVANCE=0.98 NONE
会話・推論 Standard GROUNDING=1.00 RELEVANCE=1.00 NONE
誤った金額 Classic GROUNDING=0.00 RELEVANCE=0.97 GUARDRAIL_INTERVENED
誤った金額 Standard GROUNDING=0.00 RELEVANCE=1.00 GUARDRAIL_INTERVENED
質問に答えていない Classic GROUNDING=0.99 RELEVANCE=0.00 GUARDRAIL_INTERVENED
質問に答えていない Standard GROUNDING=0.00 RELEVANCE=0.20 GUARDRAIL_INTERVENED
実行結果の action で、GUARDRAIL_INTERVENED がブロックされたもの、NONE がそのまま通った回答を示しています。
結果を見比べる
| パターン | Classic(根拠 / 関連性) | Standard(根拠 / 関連性) |
|---|---|---|
| 言い換え | 0.99 / 0.53 ブロック | 1.00 / 1.00 通過 |
| 会話・推論 | 0.77 / 0.98 通過 | 1.00 / 1.00 通過 |
| 誤った金額 | 0.00 / 0.97 ブロック | 0.00 / 1.00 ブロック |
| 質問に答えていない | 0.99 / 0.00 ブロック | 0.00 / 0.20 ブロック |
差が出たのは「言い換え」で、Classic だけが関連性 0.53 でブロックしました。「誤った金額」はどちらも根拠 0.00 で止めてくれますね!
この例では大きな違いはないですが、若干Standardの方が思惑通りに動いてくれるって感じですね!
日本語の対応
Standard のコンテキストグラウンディングチェックは公式ドキュメントでも日本語が「Optimized and supported」と案内されているので問題なく使える言語です。
とはいえ、日本語のせいで結果が変わっていないかも気になったので、英語でも同じ比較を実行してみました。
paraphrase Classic GROUNDING=0.91 RELEVANCE=0.02 GUARDRAIL_INTERVENED
paraphrase Standard GROUNDING=1.00 RELEVANCE=1.00 NONE
conversational Classic GROUNDING=0.46 RELEVANCE=0.99 GUARDRAIL_INTERVENED
conversational Standard GROUNDING=1.00 RELEVANCE=1.00 NONE
wrong amount Classic GROUNDING=0.00 RELEVANCE=0.85 GUARDRAIL_INTERVENED
wrong amount Standard GROUNDING=0.00 RELEVANCE=1.00 GUARDRAIL_INTERVENED
not answering Classic GROUNDING=1.00 RELEVANCE=0.00 GUARDRAIL_INTERVENED
not answering Standard GROUNDING=0.00 RELEVANCE=0.20 GUARDRAIL_INTERVENED
Standard は日本語と同じスコアでした!日本語でも英語と同じように判定できていますね。一方の Classic は、英語でも「会話・推論(conversational)」の根拠スコアが 0.46 まで落ちてブロックされており、言語を問わず判定が厳しめに出る感じでした。
Converseで複数ターンの会話に組み込む
Converse APIで複数ターンの会話に組み込むときは、次のようにメッセージを組み立てます。
QUERY_LIMIT = 1000 # queryは全ターン合計で1,000文字まで
def guard(text, qualifier):
"""テキストをguardContentで囲み、参照資料(grounding_source)か質問(query)かを指定する"""
return {"guardContent": {"text": {"text": text, "qualifiers": [qualifier]}}}
def build_query(user_texts):
"""新しい発言から順に足し、1,000文字に収まらない古い発言は落とす"""
query = ""
for text in reversed(user_texts):
candidate = f"{text}\n{query}" if query else text
if len(candidate) > QUERY_LIMIT:
return query or text[-QUERY_LIMIT:] # 最新の発言だけで超えるときは末尾を残す
query = candidate
return query
# 会話の各ターンで、これまでのユーザー発言からqueryを作る
user_texts.append(user_text)
query = build_query(user_texts)
# タグは最新の発言にだけ付け、モデルには発言をそのまま渡す
latest = {"role": "user", "content": [
guard(SOURCE, "grounding_source"), guard(query, "query"), {"text": user_text}]}
# 履歴にはタグなしのテキストで残す
history += [{"role": "user", "content": [{"text": user_text}]},
{"role": "assistant", "content": [{"text": answer}]}]
タグは最新のユーザーメッセージに付け、履歴にはタグなしのテキストで残します。過去のユーザー発言は query にまとめて足し、1,000文字以内に収めるようにしました。アシスタントの回答は、間違っていた場合にその内容まで根拠として扱われてしまうので query に含めません。
3ターン目に、出張メモを貼り付けた1,167文字の長文を送ってみます。
chat.py(全体)
import boto3
client = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
MODEL_ID = "jp.anthropic.claude-haiku-4-5-20251001-v1:0"
GUARDRAIL = {"guardrailIdentifier": "<StandardのガードレールID>", "guardrailVersion": "DRAFT", "trace": "enabled"}
SYSTEM = [{"text": "社内規程アシスタントです。渡された規程だけに基づいて、会話の流れを踏まえて日本語で1〜2文で簡潔に答えてください。"}]
QUERY_LIMIT = 1000 # queryは全ターン合計で1,000文字まで
SOURCE = """出張旅費規程
- 宿泊費の上限は1泊12,000円です。
- 東京都・大阪府に宿泊する場合、上限は1泊15,000円です。
- 精算の申請は出張終了日から14日以内に行ってください。"""
LONG_MEMO = "【出張メモ】" + "新大阪駅近くのホテルを候補にしていて、朝食付きのプランにするか迷っています。" * 30
TURNS = [
"来週、大阪に2泊の出張があります。",
"それなら宿泊費はいくらまで精算できますか?",
LONG_MEMO + "\nあと、申請はいつまでにすればいいですか?",
]
def guard(text, qualifier):
"""テキストをguardContentで囲み、参照資料(grounding_source)か質問(query)かを指定する"""
return {"guardContent": {"text": {"text": text, "qualifiers": [qualifier]}}}
def build_query(user_texts):
"""新しい発言から順に足し、1,000文字に収まらない古い発言は落とす"""
query = ""
for text in reversed(user_texts):
candidate = f"{text}\n{query}" if query else text
if len(candidate) > QUERY_LIMIT:
return query or text[-QUERY_LIMIT:] # 最新の発言だけで超えるときは末尾を残す
query = candidate
return query
def scores(res):
for items in res.get("trace", {}).get("guardrail", {}).get("outputAssessments", {}).values():
for a in items:
f = a.get("contextualGroundingPolicy", {}).get("filters", [])
return " ".join(f"{x['type']}={x['score']:.2f}" for x in f)
return "-"
def chat():
history, user_texts = [], []
for user_text in TURNS:
user_texts.append(user_text)
query = build_query(user_texts)
# タグは最新の発言にだけ付け、モデルには発言をそのまま渡す
latest = {"role": "user", "content": [
guard(SOURCE, "grounding_source"), guard(query, "query"), {"text": user_text}]}
res = client.converse(modelId=MODEL_ID, system=SYSTEM, messages=history + [latest],
guardrailConfig=GUARDRAIL, inferenceConfig={"maxTokens": 200})
answer = res["output"]["message"]["content"][0]["text"]
print(f"[発言{len(user_text)}字 / query{len(query)}字] {res['stopReason']} {scores(res)}\n → {answer}")
# 履歴にはタグなしのテキストで残す
history += [{"role": "user", "content": [{"text": user_text}]},
{"role": "assistant", "content": [{"text": answer}]}]
if __name__ == "__main__":
chat()
uv run chat.py
[発言17字 / query17字] end_turn GROUNDING=1.00 RELEVANCE=1.00
→ 大阪への2泊出張の宿泊費の上限は、1泊15,000円ですので合計30,000円までが対象となります。また、出張終了日から14日以内に精算申請を行う必要があります。
[発言21字 / query39字] end_turn GROUNDING=1.00 RELEVANCE=1.00
→ 大阪への2泊出張の場合、宿泊費の上限は1泊15,000円ですので、合計30,000円まで精算できます。
[発言1167字 / query1000字] end_turn GROUNDING=1.00 RELEVANCE=1.00
→ 大阪府への2泊出張の場合、宿泊費の上限は1泊15,000円ですので合計30,000円まで精算できます。また、精算申請は出張終了日から14日以内に行ってください。
全て問題なく、スコアも良くブロックされていないですね!
この形にしている理由
Converse APIへの組み込みは少しクセがあるので、ドキュメントの記述を確認してポイントを押さえておきます。
- 参照資料と質問は、ユーザーメッセージの guardContent に qualifiers(grounding_source / query)を付けて渡す
- guardContent を1つでも含めると、ガードレールは guardContent の中身だけを評価し、それ以外のテキストは読まない
- query のタグが複数あると、別々ではなくまとめて評価される
- query の上限は1,000文字(参照資料は100,000文字、回答は5,000文字)
そのため複数ターンの会話ではこの仕様が次の点で注意が必要でした。
- 過去のターンに付けた query のタグも合算される。合計1,000文字は通過し、1,001文字では ValidationException で呼び出し自体が失敗した
- タグのない過去の会話は読まれないので、「大阪に2泊です」のあとに「それなら宿泊費はいくら?」と続けると、回答の「大阪・2泊」が資料にも質問にもない状態になる。
上限を超えた古い発言は抜け落ちるので、大事な前提は要約して引き継ぐなどの工夫を合わせると良さそうです。
後片付け
検証で作成した2つのガードレールを削除します。
aws bedrock delete-guardrail --guardrail-identifier <ClassicのガードレールID> --region ap-northeast-1
aws bedrock delete-guardrail --guardrail-identifier <StandardのガードレールID> --region ap-northeast-1
おわりに
コンテキストグラウンディングチェックが使いやすくなりました!
実際にアプリやRAGなどに組み込んでチェック機構として使ってみると便利かもしれないですね。
例えば一定の閾値を超えない場合は弾くなど。
本記事が少しでも参考になりましたら幸いです。
最後までご覧いただきありがとうございました!












