AI エージェント開発実践 #1 エージェントループを堅牢にする
LLM アプリ開発 第10回で、はじめてエージェントを作りました。複数のツールと目標を与えると、Claude が自分で順序を決めて仕事を終えるループでした。あのループは動きますが、実際のサービスに組み込むには穴があります。このシリーズではその穴を一つずつ埋めて、長い作業を任せても崩れないエージェントを作ります。
シリーズは全7回です。ループの堅牢化(第1回)、ツール設計(第2回)、計画と自己修正(第3回)、コンテキスト管理(第4回)、サブエージェント(第5回)、MCP サーバー製作(第6回)を経て、最終の第7回でイシュートリアージエージェントを完成させます。モデルは、エージェントのように複数の段階を自分で判断する作業に最も強い claude-opus-4-8 を使います。
最小ループの穴 #
第10回のループを見直すと、応答の stop_reason は end_turn かツール呼び出しのどちらかだと仮定しています。
if response.stop_reason == "end_turn":
return next(b.text for b in response.content if b.type == "text")
# そうでなければツールを実行して続ける実際には他のケースがあります。応答が max_tokens にかかって途中で切れることもありますし、安全上の理由で refusal が返ることもあります。さらにツール関数の中で例外が起きると、ループ全体が落ちます。エージェントは一度回って終わるコードではなく、何十回も繰り返すコードなので、一周あたり 1% の事故もループ全体で見ればよく起きる出来事になります。
stop_reason をすべて処理する #
ループが出会いうる stop_reason を整理するとこうなります。
| stop_reason | 意味 | ループですること |
|---|---|---|
tool_use | ツールを呼びたい | ツールを実行し、結果を返して続ける |
end_turn | 答えを終えた | 最終テキストを返して終了 |
max_tokens | 出力上限にかかって切れた | 上限を増やして再試行するか、エラーとして処理 |
refusal | 安全上の理由で拒否した | 同じリクエストを繰り返さず終了 |
分岐をコードに移すとこうなります。
if response.stop_reason == "end_turn":
return final_text(response)
if response.stop_reason == "max_tokens":
raise RuntimeError("応答が途中で切れました。max_tokens を増やしてください。")
if response.stop_reason == "refusal":
return "リクエストを処理できません。"
# 残るのは tool_use。ツールを実行してループを続ける。max_tokens をエラーとして処理するのには理由があります。切れた応答をそのまま会話に積んで回し続けると、Claude は切れた自分の言葉を引き継いで、だんだんおかしな方向へ進みます。切れたという事実を知るほうが、中途半端に続けるよりましです。
ツールのエラーを結果として返す #
ツール関数で例外が出るとループが落ちるのがデフォルトの動作です。より良い方法は、例外を捕まえて、エラーもツールの結果として返すことです。tool_result に is_error の印を付けると、Claude がエラーを読んで別の方法を試します。
def execute_tool(block) -> dict:
try:
result = run_tool(block.name, block.input)
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
}
except Exception as e:
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": f"ツールの実行に失敗しました: {e}",
"is_error": True,
}たとえばファイル読み取りツールが「ファイルがありません」というエラー結果を受け取ると、Claude は一覧取得ツールでパスを確認し直すといった形で自力で復旧します。例外で落ちていたら起きなかったことです。
一時的な API エラーは SDK がリトライします #
ループが長くなると、その間にネットワーク障害や一時的な過負荷(429、5xx)に遭う確率も上がります。これは自分で実装する必要がありません。Anthropic SDK がデフォルトで2回まで指数バックオフのリトライを行い、回数は max_retries で調整します。
client = anthropic.Anthropic(max_retries=4)完成したループ #
ここまでの内容を合わせたループです。どのツールをどの入力で呼んだかを記録するログも入れました。エージェントがおかしな動きをしたとき、原因を探すにはこのログが事実上唯一の手がかりです。
import logging
import anthropic
logger = logging.getLogger("agent")
client = anthropic.Anthropic(max_retries=4)
def run_agent(goal: str, max_steps: int = 20) -> str:
messages = [{"role": "user", "content": goal}]
for step in range(max_steps):
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
return next(b.text for b in response.content if b.type == "text")
if response.stop_reason == "max_tokens":
raise RuntimeError("応答が max_tokens にかかって切れました。")
if response.stop_reason == "refusal":
return "リクエストを処理できません。"
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
logger.info("step=%d tool=%s input=%s", step, block.name, block.input)
tool_results.append(execute_tool(block))
messages.append({"role": "user", "content": tool_results})
raise RuntimeError(f"{max_steps}ステップ以内に作業を終えられませんでした。")第10回からの変更点を挙げると、stop_reason の4種類をすべて処理し、ツールの例外はループを落とす代わりに is_error の結果として返し、最大ステップ超過を黙って流さずエラーで知らせます。構造は同じですが、どんな状況でも「なぜ止まったか」が分かるループになりました。
もう一つ、Claude は一つの応答で複数のツールを同時に呼ぶことがあります。上のコードのように response.content の tool_use ブロックをすべて巡回し、ブロックごとに一つずつ tool_result を作って一つのメッセージで返さなければなりません。一つでも欠けると、API が次のリクエストを拒否します。
よくつまずくところ #
- 切れた応答をそのまま続ける —
max_tokensで切れた応答を会話に積んで回し続けると、出力がだんだん崩れます。切れたらエラーとして処理し、上限を増やします。 - tool_result の数が合わない — 並列呼び出しで一部のツールの結果だけ返すと、「tool_use_id に対応する結果がない」という 400 エラーが出ます。エラーになったツールも
is_errorの結果で埋めて数を合わせます。 - ログなしで回す — エージェントが見当違いの結論を出したとき、ログがなければどの段階でずれたのか知るすべがありません。ツール名と入力だけでも記録します。
まとめ #
今回は、最小のエージェントループを実戦レベルに引き上げました。
stop_reasonはtool_useとend_turnのほかにmax_tokens、refusalまですべて分岐します。- ツールの例外はループを落とす代わりに
is_errorの結果として返し、Claude に自力で復旧させます。 - 一時的な API エラーは SDK の
max_retriesに任せ、ツール呼び出しのログを残します。
ループが堅牢になったので、次はその上で動くツールの番です。次回の「AI エージェント開発実践 #2 良いツールを設計する」では、同じループでもエージェントの性能を一変させるツール設計を扱います。