8番出口を作ろう

207 Views

August 28, 26

スライド概要

strandsの無限ループ問題を取り上げました

profile-image

SIerのデータサイエンティスト 2025 Japan AWS Jr.Champions

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

8 番出口 をつくろう

2.

やぎ 歩いてる人

3.

ご案内 Guide 異変を見逃さないこと Do not overlook any anomalies. 異変を見つけたら、すぐに引き返すこと If you find anomalies, turn around immediately. 異変が見つからなかったら、引き返さないこと If you don't find anomalies, do not turn back. 8番出口から外に出ること To go out from Exit 8.

4.

ValidationError → Retry ValidationError → Retry ValidationError → Retry ValidationError → Retry ValidationError → Retry

5.

どうやって出るんだ・・?

6.

Structured Output とは モデルの応答が 指定した JSON スキーマに準拠する ことを保証する機能 class Report(BaseModel): title: str score: int agent.structured_output(Report, “8番出口って何?”) report.score 「LLMの出力を、そのままプログラムで扱いたい」場面で使う

7.

どうやって JSON にする?

8.

2つの手法 A. 制約付きデコーディング ・・・生成中に縛る JSON Schema LLM 100% JSON になる JSON B. ツールベース ・・・生成後に検証する JSON Schema LLM 検証 失敗したら引き返す JSON

9.

2つの手法 縛る場所 方法 結果 制約 モデル 採用例 A. 制約付きデコーディング LLM の 生成中 スキーマを文法にコンパイル 100%スキーマ準拠 型だけ Claude4.6 など OpenAI / Bedrock B. ツールベース LLM の 生成後 JSON を Pydantic で検証 通らなければリトライ 値まで すべて Strands × Bedrock

10.

(A) 制約付きデコーディングの仕組み JSONスキーマを文法(CFG)にコンパイルし 各トークン生成時に無効なトークンのロジットを −∞ にしてマスクする {“score”: 8 1 “high” 🐝 ← 次の 1 token をえらぶ マスク後 2.1 8 1.4 1 3.8 -∞ 0.9 -∞ 2.1 1.4 CFG(文脈自由文法)とは、 言語構造を規則で定義する仕組み。 例:object → "{" members "}" 再帰を定義できるので、有限オート マトン(FSM)より表現が広く、 JSONを扱える。 確率が一番高くても、 無効なら選ばない

11.
[beta]
(B) ツールベースの仕組み

Pydanticモデルをツール仕様に変換し、ストリーム後にPydantic検証する。

1

2

class Report(BaseModel):

score: int = Field(ge=200, le=100)

Pydantic 型

{"toolSpec": {

"name": "Report",

"description": "IMPORTANT: This StructuredOutputTool should only be invoked

as the last and final tool before returning the completed

result to the caller. <description>...</description>",

"inputSchema": {"json": {

"properties": {"score": {"type": "integer", "minimum": 200, "maximum": 100}}

}}

}}

toolSpec を作る

12.

(B) ツールベースの仕組み Pydanticモデルをツール仕様に変換し、ストリーム後にPydantic検証する。 3 4 {"role": "assistant", "content": [{"toolUse": { "toolUseId": "tooluse_abc123", "name": "Report", "input": {"title": "8th_Report", "score": 188} }}]} LLM からの返答 toolUse.input を取り出す validated = Report(**tool_use["input"]) Pydantic で検証する

13.

なぜStrands は ツールベースなのか?

14.

なぜ Strands は ツールベースなのか? どのモデルでも一貫して、そのまま関数に渡せるJSON で出力したいから。 ① Pydantic の 表現力 A. 制約付きデコーディング 型・enum 数値制約 ge / le 文字列制約 max_length 再帰スキーマ カスタム検証 field_validator ◎ × × × × B. ツールベース ◎ ◎ ◎ ◎ ◎ ② 全モデルに対応 ツール呼出なので、全モデルで対応できる。 Claude Opus 5 でも対応可。 ③ 既存の仕組みに乗る Hooks・トレース・メトリクス・エラー処理 の既存基盤に載せられる。

15.

異変 みつかった?

16.

異変 200 ≦ X ≦ 100 ※200以上100以下 class Report(BaseModel): score: int = Field(ge=200, le=100)

17.

異変 200 ≦ X ≦ 100 ※200以上100以下 class Report(BaseModel): score: int = Field(ge=200, le=100) いつ引き返すんだ・・?

18.

いつ引き返す? 1 ↓ 3 class Report(BaseModel): score: int = Field(ge=200, le=100) Pydantic は 整合性チェックしない toolSpec を作って呼び出される {"role": "assistant", "content": [{"toolUse": { "toolUseId": "tooluse_abc123", "name": "Report", "input": {"title": "8th_Report", "score": 188} }}]} LLM からの返答 toolUse.input を取り出す

19.

いつ引き返す? 4 validated = Report(**tool_use["input"]) Pydantic で検証する 200 ≦ 188 ≦ 100 ここで初めてわかる assistant toolUse {"score": 188} user toolResult "Input should be less than or equal to 100"

20.
[beta]
引き返したあとは?

5
5

tool が呼び出されなかった場合

tool 利用も強制する

{"role": "user", "content": [{"text":

"You must format the previous response as structured output."}]}
validation エラー の場合
{"role": "user", "content": [{"toolResult": {

"toolUseId": "tooluse_abc123",

"status": "error",

"content": [{"text":

"Validation failed for Impossible. Please fix the following errors:\n- Field
'score': Input should be less than or equal to 100"}]

}}]}

リトライしていく!

21.

ようこそ 0番出口へ

22.
[beta]
異変1:リトライごとにトークンが大きくなる

6

[


]

{"role": "user", "content": [

{"text": "適当にレポートを作って"}]},


{"role": "assistant", "content": [

{"text": "申し訳ありませんが、お力になれません。利用可能なツールを

確認したところ…"}]},


{"role": "user", "content": [

{"text": "You must format the previous response as structured output."}]},


{"role": "assistant", "content": [

{"toolUse": {"name": "Impossible",

"input": {"title": "Unable to Create Report", "score": 200}}}]},


{"role": "user", "content": [

{"toolResult": {"status": "error", "content": [

{"text": "Validation failed for Impossible. Please fix the following errors:

- Field 'score': Input should be less than or equal to 100"}]}}]}


←

リトライ

23.

異変2:リトライごとにお金がかかる 6 リトライごとにモデルが呼ばれる JSON Schema LLM 検証 失敗したら引き返す 失敗しても「課金」される Bedrock と CloudWatchLogs がふえる JSON

24.
[beta]
異変3:リトライでエラーが出ない

6

{"role": "user", "content": [{"toolResult": {
 リトライは、エラーを出さずに繰り返す
"status": "error",

"content": [{"text": "Field 'score': Input should be less than or equal to 100"}]}}]}

6

result = agent(prompt, structured_output_model=Report, limits=Limits(turns=5))

↓ 失敗すると

result.structured_output is None

result.stop_reason
"limit_turns"

status: "error" は、 Python の例外ではない
limit を決めても None が返るだけ

リトライ でもエラーが出ない

自分で実装しないと

ランタイムが終わらない・・

25.

異変∞:リトライが終わらない ① リトライは2種類 発火条件 ツール未呼出 end_turn 検証落ち ValidError ③ 出口がない 回数 1回 上限なし ツール未呼出 → ツール利用を強制 {“any”: {}} ValidError だとリトライ ※ツールを呼ばずに逃げる出口がない ② リトライを数えてない structured_output_tool.py リトライ判断は モデル任せ ④ 止めるのはAWS リトライ上限なし → Bedrock を呼び続ける → ThrottlingException

26.

異変∞:リトライが終わらない 7 ①リトライごとにトークンが増えながら ②LLM呼出をする JSON Schema LLM 検証 ③リトライでエラーもでず、上限もない ④止まるのは、Bedrock の ThrottlingException JSON

27.

できる対策は?

28.
[beta]
対策:Hookで数えて、stop_event_loopをTrueにする

8

class ToolCallLimiter(HookProvider):

def __init__(self, max_calls: int = 3):

self.max_calls = max_calls

self.counts: dict[str, int] = {}


def register_hooks(self, registry: HookRegistry) -> None:

registry.add_callback(BeforeInvocationEvent, self.reset)

registry.add_callback(BeforeToolCallEvent, self.check)


def reset(self, event: BeforeInvocationEvent) -> None:

self.counts = {}


def check(self, event: BeforeToolCallEvent) -> None:

name = event.tool_use["name"]

self.counts[name] = self.counts.get(name, 0) + 1


if self.counts[name] > self.max_calls:

event.cancel_tool = f"Tool '{name}' invoked too many times."

event.invocation_state.setdefault(

"request_state", {})["stop_event_loop"] = True

29.

まとめ:Strands の構造化出力には、リトライ上限がない。 ① 書くとき 矛盾する制約を積まない 制約は送られるだけ。強制されない。 ② 走らせるとき Hook で数えて止める SDK は、回数を数えていない。 ③ 受け取るとき ④ 失敗したあと 履歴を戻す 失敗したゴミが、次に持ち越される。 None を見て raise 止めても、例外は飛ばない。

30.

お知らせ 本通路に、8番出口はございません。 ご利用のお客様は、 ご自身でお出口をおつくりください。

31.

8 番出口 をつくろう