---
title: 8番出口を作ろう
tags: 
author: [やぎ](https://docswell.com/user/7707589)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/2EVVKQ99EQ.jpg?width=480
description: strandsの無限ループ問題を取り上げました
published: August 28, 26
canonical: https://docswell.com/s/7707589/K27E8R-2026-08-28-233839
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/2EVVKQ99EQ.jpg)

8
番出口
をつくろう


# Page. 2

![Page Image](https://bcdn.docswell.com/page/57GLQWZ4EL.jpg)

やぎ
歩いてる人


# Page. 3

![Page Image](https://bcdn.docswell.com/page/4EQYK3LRJP.jpg)

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


# Page. 4

![Page Image](https://bcdn.docswell.com/page/KJ4WP1DQ71.jpg)

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


# Page. 5

![Page Image](https://bcdn.docswell.com/page/LE1Y9GZL7G.jpg)

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


# Page. 6

![Page Image](https://bcdn.docswell.com/page/GEWGQK9QJ2.jpg)

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


# Page. 7

![Page Image](https://bcdn.docswell.com/page/47ZLWZ93J3.jpg)

どうやって JSON にする？


# Page. 8

![Page Image](https://bcdn.docswell.com/page/YJ6WGZKZJV.jpg)

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


# Page. 9

![Page Image](https://bcdn.docswell.com/page/GJ5MVWP9J4.jpg)

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


# Page. 10

![Page Image](https://bcdn.docswell.com/page/9E293Q657R.jpg)

(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 → &quot;{&quot; members &quot;}&quot; 
再帰を定義できるので、有限オート
マトン（FSM）より表現が広く、
JSONを扱える。
確率が一番高くても、
無効なら選ばない


# Page. 11

![Page Image](https://bcdn.docswell.com/page/D7Y41W96EM.jpg)

(B) ツールベースの仕組み
Pydanticモデルをツール仕様に変換し、ストリーム後にPydantic検証する。
1
2
class Report(BaseModel):
score: int = Field(ge=200, le=100)
Pydantic 型
{&quot;toolSpec&quot;: {
&quot;name&quot;: &quot;Report&quot;,
&quot;description&quot;: &quot;IMPORTANT: This StructuredOutputTool should only be invoked
as the last and final tool before returning the completed
result to the caller. &lt;description&gt;...&lt;/description&gt;&quot;,
&quot;inputSchema&quot;: {&quot;json&quot;: {
&quot;properties&quot;: {&quot;score&quot;: {&quot;type&quot;: &quot;integer&quot;, &quot;minimum&quot;: 200, &quot;maximum&quot;: 100}}
}}
}}
toolSpec を作る


# Page. 12

![Page Image](https://bcdn.docswell.com/page/VENY29D1J8.jpg)

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


# Page. 13

![Page Image](https://bcdn.docswell.com/page/Y79P12GYE3.jpg)

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


# Page. 14

![Page Image](https://bcdn.docswell.com/page/G78D45VK7D.jpg)

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


# Page. 15

![Page Image](https://bcdn.docswell.com/page/L7LM6Y5PJR.jpg)

異変
みつかった？


# Page. 16

![Page Image](https://bcdn.docswell.com/page/4EMY5N32EW.jpg)

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


# Page. 17

![Page Image](https://bcdn.docswell.com/page/PER93DQ5J9.jpg)

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


# Page. 18

![Page Image](https://bcdn.docswell.com/page/P7XQ415XEX.jpg)

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


# Page. 19

![Page Image](https://bcdn.docswell.com/page/37K9Z2697D.jpg)

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


# Page. 20

![Page Image](https://bcdn.docswell.com/page/LJ3WD469J5.jpg)

引き返したあとは？
5
5
tool が呼び出されなかった場合
tool 利用も強制する
{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: [{&quot;text&quot;:
&quot;You must format the previous response as structured output.&quot;}]}
validation エラー の場合
{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: [{&quot;toolResult&quot;: {
&quot;toolUseId&quot;: &quot;tooluse_abc123&quot;,
&quot;status&quot;: &quot;error&quot;,
&quot;content&quot;: [{&quot;text&quot;:
&quot;Validation failed for Impossible. Please fix the following errors:\n- Field
&#039;score&#039;: Input should be less than or equal to 100&quot;}]
}}]}
リトライしていく！


# Page. 21

![Page Image](https://bcdn.docswell.com/page/8JDKPQV8EG.jpg)

ようこそ
0番出口へ


# Page. 22

![Page Image](https://bcdn.docswell.com/page/VEPKNLVW78.jpg)

異変１：リトライごとにトークンが大きくなる
6
[
]
{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: [
{&quot;text&quot;: &quot;適当にレポートを作って&quot;}]},
{&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: [
{&quot;text&quot;: &quot;申し訳ありませんが、お力になれません。利用可能なツールを
確認したところ…&quot;}]},
{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: [
{&quot;text&quot;: &quot;You must format the previous response as structured output.&quot;}]},
{&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: [
{&quot;toolUse&quot;: {&quot;name&quot;: &quot;Impossible&quot;,
&quot;input&quot;: {&quot;title&quot;: &quot;Unable to Create Report&quot;, &quot;score&quot;: 200}}}]},
{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: [
{&quot;toolResult&quot;: {&quot;status&quot;: &quot;error&quot;, &quot;content&quot;: [
{&quot;text&quot;: &quot;Validation failed for Impossible. Please fix the following errors:
- Field &#039;score&#039;: Input should be less than or equal to 100&quot;}]}}]}
←
リトライ


# Page. 23

![Page Image](https://bcdn.docswell.com/page/27VVKQZ97Q.jpg)

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


# Page. 24

![Page Image](https://bcdn.docswell.com/page/5JGLQW447L.jpg)

異変３：リトライでエラーが出ない
6
{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: [{&quot;toolResult&quot;: {
 リトライは、エラーを出さずに繰り返す
&quot;status&quot;: &quot;error&quot;,
&quot;content&quot;: [{&quot;text&quot;: &quot;Field &#039;score&#039;: Input should be less than or equal to 100&quot;}]}}]}
6
result = agent(prompt, structured_output_model=Report, limits=Limits(turns=5))
↓ 失敗すると
result.structured_output is None
result.stop_reason
&quot;limit_turns&quot;
status: &quot;error&quot; は、 Python の例外ではない
limit を決めても None が返るだけ
リトライ でもエラーが出ない
自分で実装しないと
ランタイムが終わらない・・


# Page. 25

![Page Image](https://bcdn.docswell.com/page/47QYK3GREP.jpg)

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


# Page. 26

![Page Image](https://bcdn.docswell.com/page/KE4WP15QJ1.jpg)

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


# Page. 27

![Page Image](https://bcdn.docswell.com/page/L71Y9GWLJG.jpg)

できる対策は？


# Page. 28

![Page Image](https://bcdn.docswell.com/page/G7WGQK6QE2.jpg)

対策：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) -&gt; None:
registry.add_callback(BeforeInvocationEvent, self.reset)
registry.add_callback(BeforeToolCallEvent, self.check)
def reset(self, event: BeforeInvocationEvent) -&gt; None:
self.counts = {}
def check(self, event: BeforeToolCallEvent) -&gt; None:
name = event.tool_use[&quot;name&quot;]
self.counts[name] = self.counts.get(name, 0) + 1
if self.counts[name] &gt; self.max_calls:
event.cancel_tool = f&quot;Tool &#039;{name}&#039; invoked too many times.&quot;
event.invocation_state.setdefault(
&quot;request_state&quot;, {})[&quot;stop_event_loop&quot;] = True


# Page. 29

![Page Image](https://bcdn.docswell.com/page/4JZLWZY3E3.jpg)

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


# Page. 30

![Page Image](https://bcdn.docswell.com/page/YE6WGZDZEV.jpg)

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


# Page. 31

![Page Image](https://bcdn.docswell.com/page/GE5MVW39E4.jpg)

8
番出口
をつくろう


