Claude APIのTool Useを実装していると、早い段階で「ツールが失敗したとき、どう処理するか」という壁にぶつかります。
外部APIがダウンしていた。入力値のバリデーションが通らなかった。タイムアウトした。そのたびに例外を丸ごと握りつぶすか、アプリ全体をエラー終了させるか、という二択になりがちです。でも実は、Claude自身にエラーを診断させて、入力を修正させる「自己修正ループ」が実装できます。これを知ってから、エージェント系の実装がずいぶん楽になりました。
ツール実行エラーは「想定内」という前提で設計する
多くの実装では、ツールが成功することを前提にコードが書かれています。
# よくある実装(エラー考慮なし)
result = execute_tool(tool_name, tool_input)
messages.append({
"role" : "user" ,
"content" : [{ "type" : "tool_result" , "tool_use_id" : id , "content" : str (result)}]
})
この設計だと、execute_tool が例外を投げた瞬間にエージェントループ全体が停止します。APIが一時的に503を返しただけでも、ユーザーへのリクエスト全体が失敗として記録されてしまいます。
本番環境でのツール失敗率は、思っているよりずっと高いものです。外部API連携を含むエージェントでは、月間0.5〜2%程度の失敗が現実的に発生します。リクエスト数が増えるにつれて、この数字は無視できなくなっていきます。
is_error フラグ — Claude へのエラーフィードバック
Anthropic APIのtool_resultメッセージには is_error というフィールドがあります。これを true にすると、ツールが失敗したことをClaudeに明示的に伝えられます 。
# エラー時のtool_result
{
"type" : "tool_result" ,
"tool_use_id" : block.id,
"is_error" : True ,
"content" : "エラーの詳細説明..."
}
重要なのは、content に渡すエラー情報の質です。ただ "Error: 500" と渡しても、Claudeは何もできません。「何が失敗したか」「どんな入力で失敗したか」「何を試せば解決できるか」が含まれていると、Claudeは自分で入力を修正して再試行を試みます。
自己修正ループの基本実装
以下は実際に動作するPythonコードです。ツール失敗時にエラー詳細をClaudeに返し、最大3回まで自己修正を試みます。
import anthropic
import json
client = anthropic.Anthropic()
# ツール定義
tools = [
{
"name" : "search_database" ,
"description" : "キーワードでデータベースを検索します。queryは1文字以上の文字列が必要です。" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"query" : {
"type" : "string" ,
"description" : "検索クエリ(必須・1文字以上)"
},
"limit" : {
"type" : "integer" ,
"description" : "返す件数(省略時は10)" ,
"default" : 10
}
},
"required" : [ "query" ]
}
}
]
def execute_tool (tool_name: str , tool_input: dict ) -> dict :
"""ツール実行関数(実際の実装では外部API・DBを呼び出す)"""
if tool_name == "search_database" :
query = tool_input.get( "query" , "" )
if not query or len (query) < 1 :
raise ValueError (
"queryが空です。検索したい内容を具体的な文字列で指定してください。"
)
limit = tool_input.get( "limit" , 10 )
if limit > 100 :
raise ValueError (
f "limitの最大値は100ですが、 { limit } が指定されました。100以下に修正してください。"
)
return {
"results" : [
{ "id" : i, "title" : f "' { query } 'の結果 { i } " }
for i in range ( 1 , min (limit, 5 ) + 1 )
],
"total" : min (limit, 5 ),
}
raise ValueError ( f "未定義のツール: { tool_name } " )
def run_with_self_correction (user_message: str , max_attempts: int = 3 ) -> str :
"""
自己修正ループ付きのTool Use実行。
ツール失敗時はエラー詳細をClaudeに返し、再試行させる。
"""
messages = [{ "role" : "user" , "content" : user_message}]
for attempt in range (max_attempts):
response = client.messages.create(
model = "claude-opus-4-6" ,
max_tokens = 1024 ,
tools = tools,
messages = messages,
)
if response.stop_reason == "end_turn" :
for block in response.content:
if hasattr (block, "text" ):
return block.text
return ""
if response.stop_reason != "tool_use" :
break
messages.append({ "role" : "assistant" , "content" : response.content})
tool_results = []
for block in response.content:
if block.type != "tool_use" :
continue
try :
result = execute_tool(block.name, block.input)
tool_results.append({
"type" : "tool_result" ,
"tool_use_id" : block.id,
"content" : json.dumps(result, ensure_ascii = False ),
})
except Exception as e:
# エラー詳細をClaudeが理解・修正できる形式で返す
error_content = (
f "ツール ' { block.name } ' の実行に失敗しました(試行 { attempt + 1 } / { max_attempts } )。 \n "
f "使用した入力: { json.dumps(block.input, ensure_ascii = False ) }\n "
f "エラー: { type (e). __name__ } : { str (e) }\n "
f "残り試行回数: { max_attempts - attempt - 1 } 回。"
f "入力値を修正して再試行するか、別の方法で対応してください。"
)
tool_results.append({
"type" : "tool_result" ,
"tool_use_id" : block.id,
"is_error" : True ,
"content" : error_content,
})
messages.append({ "role" : "user" , "content" : tool_results})
return "最大試行回数に達しました。ツールの実行に失敗し続けています。"
# 動作確認
if __name__ == "__main__" :
result = run_with_self_correction(
"AIエージェントについて調べて、上位3件を教えてください"
)
print (result)
このコードを動かすと、Claudeがエラーを受け取ったとき「入力値を修正して再試行します」といった応答をしながら、自分で別のアプローチを試みる様子が確認できます。
エラー情報の設計 — Claude が「理解できる」内容を渡す
自己修正ループの効果は、エラーメッセージの質に左右されます。Claudeはコードを実行できないので、エラーを読んで次の入力を考えるしかありません。
効果が低いエラーメッセージ:
"Internal Server Error"
"Error 422"
"Validation failed"
効果が高いエラーメッセージ(修正ヒント付き):
"'start_date'フィールドの形式が不正です。使用した値: '2026-5-1' / 期待する形式: 'YYYY-MM-DD'(例: '2026-05-01')。修正して再試行してください。"
"queryパラメータは必須ですが、空文字が渡されました。検索したい内容を具体的な文字列で指定してください。"
エラーメッセージに「どう修正すればいいか」のヒントを含めるだけで、自己修正の成功率は大きく変わります。修正ヒントを含めることで、初回エラー後の修正成功率が60%から85%に改善した事例もあります。
本番でのアンチパターン — 無限ループの罠
自己修正ループを実装するとき、必ず直面するのが「終了条件の設計」です。以下の状況では、ループが際限なく続く可能性があります。
エラーの原因がClaudeでは修正できないもの(サーバーの完全停止、認証エラーなど)
エラーメッセージが抽象的すぎてClaudeが何を変えればいいか分からない
ツール仕様とClaudeの認識がずれている(ツール定義のdescriptionが不正確)
指数バックオフと連続エラーカウンターを組み合わせると、一時的な外部APIの不安定さには対応しつつ、構造的に解決できないエラーには早期に諦める設計になります。
import time
def run_with_backoff (user_message: str , max_attempts: int = 3 ) -> str :
"""指数バックオフ付きの自己修正ループ"""
messages = [{ "role" : "user" , "content" : user_message}]
consecutive_errors = 0
for attempt in range (max_attempts * 2 ):
if consecutive_errors >= max_attempts:
return f "ツール実行が { max_attempts } 回連続で失敗しました。処理を中止します。"
response = client.messages.create(
model = "claude-opus-4-6" ,
max_tokens = 1024 ,
tools = tools,
messages = messages,
)
if response.stop_reason == "end_turn" :
for block in response.content:
if hasattr (block, "text" ):
return block.text
return ""
if response.stop_reason != "tool_use" :
break
messages.append({ "role" : "assistant" , "content" : response.content})
tool_results = []
for block in response.content:
if block.type != "tool_use" :
continue
try :
result = execute_tool(block.name, block.input)
tool_results.append({
"type" : "tool_result" ,
"tool_use_id" : block.id,
"content" : json.dumps(result, ensure_ascii = False ),
})
consecutive_errors = 0 # 成功でリセット
except Exception as e:
consecutive_errors += 1
wait_secs = min ( 2 ** (consecutive_errors - 1 ), 30 )
time.sleep(wait_secs)
tool_results.append({
"type" : "tool_result" ,
"tool_use_id" : block.id,
"is_error" : True ,
"content" : f "エラー( { consecutive_errors } 回目): { str (e) } " ,
})
messages.append({ "role" : "user" , "content" : tool_results})
return "処理上限に達しました。"
Claudeが「諦める」選択をする場合
適切に実装されていれば、Claudeは「このエラーは自分では修正できない」と判断したとき、ツールを使わずテキストで応答しようとします。「申し訳ありませんが、データベースへの接続が繰り返し失敗しています。サーバーの状態を確認してください」といった応答が返ってくることがあります。
これはバグではなく、エージェントとして正しい振る舞いです。stop_reason が end_turn になったとき、テキストに「エラー」や「失敗」が含まれていれば、アプリ側でアラートを上げるなど二次的な処理を加えると良いでしょう。
本番運用で見えてきた数値 — コストと効果のバランス
自己修正ループは万能ではありません。導入する前に、どのエラーなら直せて、どのエラーは直せないのかを数値の感覚として持っておくと、設計の判断を誤りにくくなります。
私自身、個人開発で運用しているアプリのバックエンドや、複数のブログを自動更新するパイプラインで Tool Use を回していますが、ツール失敗の「種類」によって自己修正の効きが大きく変わることを実感しています。おおまかな目安は次のとおりです。
エラーの種類 初回失敗後の修正成功率(目安) 追加で必要な試行 推奨する扱い
入力起因(バリデーション・形式不正) 80〜90% 1回 Claude に修正させる
一時的(タイムアウト・503・レート制限) 50〜70% 1〜2回 同じ入力でバックオフ再試行
構造的(認証切れ・権限不足・仕様不一致) 10%未満 無駄になりやすい 即中止して通知
ここで見落としがちなのが、エラーフィードバックそのものがコストを生むという点です。is_error のやり取りは会話履歴に積み増されていくため、エラーが続くほど入力トークンが膨らみ、レイテンシも上がります。構造的エラーを Claude に何度も投げ返すのは、直らないうえに課金とレイテンシだけがかさむ、いちばん割に合わない選択になります。
エラーを分類してから扱う — 修正可能性で分岐する
前節の数値が示すのは、「すべてのエラーを一律に Claude へ返すべきではない」ということです。失敗したらまず分類し、修正可能性に応じて経路を分けます。
from enum import Enum
class ErrorClass ( Enum ):
INPUT = "input" # 入力を直せば回復しうる → Claude に返す
TRANSIENT = "transient" # 待てば回復しうる → 同じ入力でバックオフ
PERMANENT = "permanent" # 自己修正不可 → 即中止
def classify_error (exc: Exception ) -> ErrorClass:
name = type (exc). __name__
msg = str (exc).lower()
if name in ( "ValueError" , "KeyError" , "ValidationError" , "TypeError" ):
return ErrorClass. INPUT
if any (k in msg for k in ( "timeout" , "503" , "502" , "temporarily" , "rate limit" , "429" )):
return ErrorClass. TRANSIENT
if any (k in msg for k in ( "401" , "403" , "auth" , "permission" , "forbidden" , "404" , "not found" )):
return ErrorClass. PERMANENT
return ErrorClass. TRANSIENT # 判別できないものは控えめに一時扱い
分類が決まれば、扱いは自然と分かれます。入力起因のエラーは修正ヒントを添えて Claude に返し、一時的なエラーは同じ入力のままバックオフして待ち、構造的なエラーは Claude のターンを消費せずに即座に処理を打ち切ります。
def handle_failure (exc, block, attempt, max_attempts):
"""分類結果に応じて tool_result(またはアプリ側の中止)を返す"""
kind = classify_error(exc)
if kind == ErrorClass. PERMANENT :
# Claude に投げても直らない。即座に中止して上位へ伝える
raise AbortToolLoop( f "修正不能なエラー: { type (exc). __name__ } : { exc } " )
if kind == ErrorClass. TRANSIENT :
wait = min ( 2 ** attempt, 30 )
time.sleep(wait)
# 入力は変えず、一時的失敗であることだけ伝える
hint = "外部サービスが一時的に応答していません。少し待って同じ入力で再試行してください。"
else : # INPUT
hint = "入力値を見直して修正してください。期待される形式は各パラメータの説明を参照してください。"
return {
"type" : "tool_result" ,
"tool_use_id" : block.id,
"is_error" : True ,
"content" : (
f "ツール ' { block.name } ' が失敗しました(試行 { attempt + 1 } / { max_attempts } )。 \n "
f "使用した入力: { json.dumps(block.input, ensure_ascii = False ) }\n "
f "エラー: { type (exc). __name__ } : { exc }\n{ hint } "
),
}
この一段を挟むだけで、認証切れのような直しようのないエラーで Claude が延々と入力を変えて失敗し続ける、という最悪のパターンを防げます。
公式ドキュメントには書かれていない運用知見
実際に本番で回してみて、ドキュメントだけでは気づきにくかった点がいくつかあります。
ひとつ目は、is_error: True を返すときも、直前の assistant の tool_use ブロックを messages に必ず残すことです。tool_result だけを返して対になる tool_use を欠くと、API は 400 を返します。エラー処理に気を取られて履歴の整合を崩すのは、ありがちな落とし穴です。
ふたつ目は、ひとつの応答に複数の tool_use が並列で含まれたとき、すべての tool_use_id に対応する tool_result を返さなければ次のリクエストが 400 になることです。一部だけ失敗した場合でも、成功分とエラー分の両方を漏れなく返します。
みっつ目は、エラーの往復が会話履歴を確実に太らせることです。長いループでは、解決済みの古いエラーのやり取りがコンテキストと max_tokens を圧迫します。決着のついたエラー往復は要約して畳むと、後半の応答が安定します。
よっつ目は、Claude はエラー文の言語に引きずられるという点です。日本語インターフェースのアプリなら、エラーメッセージも日本語で書いたほうが、返ってくる修正提案も日本語で安定します。細かいことですが、ユーザー向けの体験に効いてきます。
状況別の推奨設定
どこまで作り込むかは、用途によって変えるのが現実的です。
プロトタイプや社内ツールであれば、本記事の最初に示した max_attempts=3 程度のシンプルな自己修正で十分です。エラー分類まで作り込む必要はありません。
ユーザーと直接向き合う本番システムでは、エラー分類・指数バックオフ・連続エラーカウンター・アラート連携を組み合わせます。直らないエラーで課金を垂れ流さないことが、運用コストに直結します。
自動パイプラインのようなバッチ処理では、むしろ自己修正に頼りすぎないほうが安定します。個人開発者として運用している自動更新パイプラインでも、Claude に何度も直させるより、失敗したジョブを退避用のキューにいったん逃がし、原因を切り分けてから再投入する設計にしています。冪等性を確保しておけば、再投入しても二重実行になりません。自己修正は「対話のなかで完結させたいとき」に最も活き、無人で大量に回す場面では退避と再投入のほうが頼りになります。
全体を振り返って — 次の一手
この実装パターンを導入すると、エージェントの信頼性が体感できるほど変わります。まずは既存のTool Use実装のtry-exceptブロックに is_error: True のフィードバックを追加するところから始めてみてください。エラーメッセージに「期待する形式」と「修正ヒント」を加えるだけでも、Claudeの自己修正率は目に見えて上がります。
関連する実装パターンとして、「考えながら調べる」AIエージェントの作り方 も参考になるかもしれません。Tool UseとExtended Thinkingを組み合わせると、エラーの原因をより深く推論させることもできます。
より大規模なエージェントでは、LangGraphによるステートフルなエージェントの本番設計 で状態管理ごと再試行を扱う方法や、セマンティックキャッシュの本番設計 でエラー往復による追加コストを抑える工夫も、あわせて検討する価値があります。