CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/Claude Code
Claude Code/2026-05-05上級

Claude Code SDK で本番品質の自律エージェントを作る7つの設計原則 — スタミナ・エラー回復・コスト管理の実装

Claude Code SDK で自律エージェントを本番運用するために必要な7つの設計原則。無限ループ防止・タイムアウト・コスト制御・エラー回復の実装パターンを解説します。

claude-code129sdk4自律エージェント4本番運用36設計パターン2

自律エージェントを「動かす」ことと「本番品質で動かし続ける」ことの間には、大きな溝があります。

Claude Code SDK を使って最初のエージェントを作るのは意外と簡単です。でも、それをスケジュール実行で毎日動かし続けると、必ず想定外の壊れ方をします。私は複数のコンテンツ自動化システムを本番で運用する中で、その壊れ方のパターンをほぼ出尽くしたと思っています。

ここではそこから蒸留した 7 つの設計原則と、その実装コードを公開します。「動くエージェント」から「壊れにくいエージェント」への距離を縮める手がかりになれば幸いです。

原則 1:ループ深度の上限を必ず設ける

Claude Code SDK の query() は内部でツール使用ループを自動で処理します。これは便利な反面、ループが終わらないケースを生みやすいです。

最も典型的な無限ループのパターンは「エージェントが同じツールを繰り返し呼び出す」です。エラーが出るたびに同じ修正を試み続け、いつまでも抜け出せない状況です。

import anthropic
 
def run_agent_with_depth_limit(
    prompt: str,
    max_turns: int = 20,
    model: str = "claude-sonnet-4-6"
) -> str:
    """ループ深度を制限したエージェント実行"""
    client = anthropic.Anthropic()
    
    messages = [{"role": "user", "content": prompt}]
    turn_count = 0
    
    while turn_count < max_turns:
        response = client.messages.create(
            model=model,
            max_tokens=4096,
            tools=TOOLS,
            messages=messages
        )
        
        turn_count += 1
        
        # ツール使用がなければ完了
        if response.stop_reason == "end_turn":
            return extract_text(response)
        
        # ツール呼び出しを実行してループ継続
        tool_results = execute_tools(response.content)
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": tool_results})
    
    # 上限到達:最後の応答を返す
    raise AgentLoopLimitError(f"Agent exceeded {max_turns} turns")

max_turns の適切な値はタスクの性質によりますが、私の実装では記事生成が 10〜15、コードレビューが 20〜30 を設定しています。上限に到達したら例外を投げてログに記録し、そのタスクをスキップして次に進む設計にしています。

原則 2:タイムアウトは 2 層で設ける

タイムアウトには「1 回の API 呼び出しのタイムアウト」と「エージェントループ全体のタイムアウト」の 2 層が必要です。API 単体には timeout パラメータで制御できますが、ループ全体は別途実装が必要です。

import asyncio
import signal
from contextlib import contextmanager
 
@contextmanager
def timeout_context(seconds: int):
    """シグナルベースのタイムアウト(Unix系OS向け)"""
    def handler(signum, frame):
        raise TimeoutError(f"Agent timed out after {seconds} seconds")
    
    old_handler = signal.signal(signal.SIGALRM, handler)
    signal.alarm(seconds)
    try:
        yield
    finally:
        signal.alarm(0)
        signal.signal(signal.SIGALRM, old_handler)
 
async def run_agent_with_timeout(
    prompt: str,
    timeout_seconds: int = 300  # 5分
) -> str:
    """全体タイムアウト付きエージェント実行"""
    try:
        with timeout_context(timeout_seconds):
            return await run_agent_core(prompt)
    except TimeoutError as e:
        log_timeout(prompt, timeout_seconds)
        raise AgentTimeoutError(str(e))

スケジュールタスクでは、1 タスクあたり 5〜10 分のタイムアウトを設定しています。これを超えるタスクは何らかの問題がある(ループ脱出できていない、API が応答しないなど)と判断して強制終了します。

原則 3:ツール呼び出しの重複を検知する

無限ループの 90% は「同じツールを同じ引数で繰り返し呼び出す」パターンです。これを検知するシンプルなガードが効果的です。

from collections import deque
from hashlib import md5
import json
 
class ToolCallDeduplicator:
    def __init__(self, window_size: int = 5):
        self.recent_calls = deque(maxlen=window_size)
    
    def is_duplicate(self, tool_name: str, tool_input: dict) -> bool:
        """直近 window_size 件と同じ呼び出しかチェック"""
        call_hash = md5(
            json.dumps({"name": tool_name, "input": tool_input}, sort_keys=True).encode()
        ).hexdigest()
        
        if call_hash in self.recent_calls:
            return True
        
        self.recent_calls.append(call_hash)
        return False
    
def execute_tools_with_dedup(tool_calls: list, dedup: ToolCallDeduplicator) -> list:
    results = []
    for call in tool_calls:
        if dedup.is_duplicate(call.name, call.input):
            # 重複検知:エラーメッセージをフィードバックして方向転換を促す
            results.append({
                "type": "tool_result",
                "tool_use_id": call.id,
                "content": "Error: This exact tool call was already attempted. Please try a different approach.",
                "is_error": True
            })
        else:
            results.append(execute_single_tool(call))
    return results

このガードを導入してから、無限ループによるタスク失敗が約 85% 減りました。単純ですが効果は大きいです。

原則 4:コスト予算をエージェントレベルで管理する

API 全体のコスト管理はできていても、「このエージェント実行で使っていいコスト上限」をエージェントレベルで管理しているケースは少ないです。しかし、本番運用では必要です。

from dataclasses import dataclass, field
 
@dataclass
class CostBudget:
    max_dollars: float
    spent_dollars: float = 0.0
    
    # Sonnet 4.6 料金(MTok単位)
    INPUT_RATE = 3.0 / 1_000_000
    OUTPUT_RATE = 15.0 / 1_000_000
    THINKING_RATE = 15.0 / 1_000_000
    
    def record_usage(self, usage: anthropic.types.Usage) -> None:
        self.spent_dollars += (
            usage.input_tokens * self.INPUT_RATE +
            usage.output_tokens * self.OUTPUT_RATE
        )
    
    @property
    def is_exceeded(self) -> bool:
        return self.spent_dollars >= self.max_dollars
    
    @property
    def remaining(self) -> float:
        return max(0, self.max_dollars - self.spent_dollars)
 
def run_agent_with_budget(prompt: str, budget: CostBudget) -> str:
    client = anthropic.Anthropic()
    messages = [{"role": "user", "content": prompt}]
    
    while True:
        if budget.is_exceeded:
            raise BudgetExceededError(
                f"Budget exceeded: ${budget.spent_dollars:.4f} / ${budget.max_dollars}"
            )
        
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=4096,
            messages=messages
        )
        
        budget.record_usage(response.usage)
        
        if response.stop_reason == "end_turn":
            return extract_text(response)
        
        # ループ継続処理...

タスクごとの予算は $0.05〜$0.20 に設定しています。これを超えるタスクはスキップしてログに記録します。月末に予算超過ログをレビューして、コストがかかりすぎているタスクを特定するのに役立てています。

原則 5:エラーを「回復可能」と「回復不可能」に分類する

エラーハンドリングで犯しがちな失敗は「すべてのエラーを同じように扱う」ことです。API の一時的な過負荷(529)とプロンプトの問題(400)では対処法が全く違います。

import anthropic
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
 
class RecoverableError(Exception):
    """リトライで回復できる可能性があるエラー"""
    pass
 
class FatalError(Exception):
    """リトライしても意味がないエラー"""
    pass
 
def classify_api_error(e: anthropic.APIError) -> None:
    """APIエラーを分類して適切な例外を投げる"""
    if isinstance(e, anthropic.RateLimitError):
        raise RecoverableError(f"Rate limit: {e}")
    elif isinstance(e, anthropic.InternalServerError):
        raise RecoverableError(f"Server error (529): {e}")
    elif isinstance(e, anthropic.BadRequestError):
        raise FatalError(f"Bad request (400) - prompt issue: {e}")
    elif isinstance(e, anthropic.AuthenticationError):
        raise FatalError(f"Auth failed (401) - check API key: {e}")
    else:
        raise RecoverableError(f"Unknown error: {e}")
 
@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=4, max=60),
    retry=retry_if_exception_type(RecoverableError)
)
def call_api_with_retry(params: dict) -> anthropic.Message:
    client = anthropic.Anthropic()
    try:
        return client.messages.create(**params)
    except anthropic.APIError as e:
        classify_api_error(e)

FatalError は即座に上位に伝播させてタスクをスキップ、RecoverableError は指数バックオフでリトライします。この分類だけで、不要なリトライによる無駄なコストが大幅に減りました。

原則 6:実行ログを構造化してデバッグ可能にする

スケジュール実行のエージェントは「いつ・何が・なぜ失敗したか」が分かりにくくなります。ログを構造化してフィルタ可能にしておくことが後のデバッグを大幅に楽にします。

import json
import logging
from datetime import datetime, timezone
from typing import Any
 
def create_structured_logger(task_id: str) -> logging.Logger:
    logger = logging.getLogger(task_id)
    
    class StructuredFormatter(logging.Formatter):
        def format(self, record: logging.LogRecord) -> str:
            log_data = {
                "ts": datetime.now(timezone.utc).isoformat(),
                "task_id": task_id,
                "level": record.levelname,
                "msg": record.getMessage(),
            }
            if hasattr(record, "extra"):
                log_data.update(record.extra)
            return json.dumps(log_data, ensure_ascii=False)
    
    handler = logging.StreamHandler()
    handler.setFormatter(StructuredFormatter())
    logger.addHandler(handler)
    return logger
 
# 使用例
logger = create_structured_logger("article-generation-2026-05-05")
logger.info("Agent started", extra={
    "model": "claude-sonnet-4-6",
    "task_type": "article_generation",
    "budget_usd": 0.10
})
logger.error("Tool call failed", extra={
    "tool": "web_search",
    "error": "timeout",
    "turn": 7
})

JSON 構造化ログにすることで、jq や CloudWatch Logs Insights でのフィルタリングが簡単になります。「先週のコスト超過タスクを一覧する」「特定ツールのエラー率を計算する」といった分析が即座にできます。

原則 7:グレースフルデグレードを設計する

最も重要かもしれない原則です。エージェントが失敗したとき、何もしないのではなく「品質は下がっても何かを返す」設計にします。

コンテンツ生成パイプラインでの実装例を示します。

from enum import Enum
 
class DegradationLevel(Enum):
    FULL = "full"       # フル機能(拡張思考 + 高品質プロンプト)
    REDUCED = "reduced" # 機能縮小(拡張思考なし + 簡略プロンプト)
    MINIMAL = "minimal" # 最小(Haiku + 最短プロンプト)
    SKIP = "skip"       # スキップ(次回に持ち越し)
 
def run_with_graceful_degradation(
    task: dict,
    max_retries: int = 3
) -> dict:
    level = DegradationLevel.FULL
    
    for attempt in range(max_retries):
        try:
            if level == DegradationLevel.FULL:
                return run_full_agent(task)
            elif level == DegradationLevel.REDUCED:
                return run_reduced_agent(task)
            elif level == DegradationLevel.MINIMAL:
                return run_minimal_agent(task)
            else:
                return {"status": "skipped", "task_id": task["id"]}
                
        except (AgentLoopLimitError, BudgetExceededError):
            # 上位品質モードが失敗したら次のレベルへ降格
            if level == DegradationLevel.FULL:
                level = DegradationLevel.REDUCED
            elif level == DegradationLevel.REDUCED:
                level = DegradationLevel.MINIMAL
            else:
                level = DegradationLevel.SKIP
                
        except FatalError:
            # 致命的エラーはスキップ
            return {"status": "fatal_error", "task_id": task["id"]}
    
    return {"status": "skipped", "task_id": task["id"]}

この設計により、スケジュール実行の成功率が「0か100か」から「品質のグラデーション」に変わりました。フル機能が失敗しても最低限の出力は得られます。

実装チェックリスト

本番デプロイ前に確認すべき項目をまとめます。

ループ深度の上限は設定されているかどうか、全体タイムアウトは設定されているかどうか、ツール呼び出し重複検知は実装されているかどうか、コスト予算はエージェントレベルで管理されているかどうか、エラーを回復可能・回復不可で分類しているかどうか、ログは構造化 JSON で出力されているかどうか、グレースフルデグレードは設計されているかどうかという 7 点です。

全体を振り返って:本番品質は「壊れ方の設計」から

エージェントの本番品質は、正常系の完成度だけでは決まりません。「いつ、どのように壊れるか」を設計することで、壊れても影響を最小化できます。

7 つの原則はどれも個別に実装できます。まずは最も問題になりやすい「ループ深度の上限」と「タイムアウト」の 2 つから始めるのがお勧めです。その 2 つだけで、スケジュール実行の信頼性は大幅に上がります。

自律エージェントを本番で動かし続けることは、一度作って終わりではありません。壊れ方を知り、設計に組み込み、観察し続けることが必要です。それが「本番品質」への道だと感じています。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Claude Code2026-07-09
「ゲートに合格した原稿」と「公開された原稿」は同じものか — 検証と公開のあいだに開く隙間
無人パイプラインでは、品質ゲートが見たファイルと実際に公開されるファイルが別物になることがあります。ダイジェスト付きの合格証を発行し、公開直前に照合する設計を実装コードと実測値つきでまとめました。
Claude Code2026-04-17
Claude Code SDK でカスタム自律エージェントを実装する — 公式 CLI を超えた独自ループ設計の実践
Claude Code SDK の内部構造を理解し、公式 CLI では制御しきれない自律エージェントループを Python/TypeScript で実装します。ツール許可・エラー回復・ストリーミングに加え、課金変更後のコスト計測とモデル棚卸しまで、実運用で使っているパターンを共有します。
Claude Code2026-07-17
テーブルが「… 2,847 more rows」で切れていた朝 — 描画上限とトークン計上を切り離して、ツール出力を設計し直す
Claude Code 2.1.209 で 200 行超の markdown テーブルは先頭 200 行+残り件数の表示になりました。省略されるのは描画だけで、モデルは全行を受け取ります。この差を測り、ツール出力を集約型へ組み替える設計をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →