28
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeのノウハウをサンプルコードで学ぶ ── 中級編(エージェント設計の考え方)

28
Last updated at Posted at 2026-04-21

はじめに

本記事は「Claude Codeのノウハウをサンプルコードで学ぶ」シリーズの中級編です。シリーズ全体は以下の4本構成になっています。

  1. 入門編 — Claude Codeの全体像とAIエージェントの仕組みを体感する
  2. 中級編(本記事) — エージェント設計の考え方を概念レベルで丁寧に解説する
  3. ハーネス設計8パターン編 — 設計パターンを動くTypeScriptで再現する
  4. エージェント実装深掘り編 — 統合エージェントの実装詳細を読み解く

入門編では「Claude Codeは考える部分(LLM)と実行する部分(ハーネス)の2層構造になっている」という骨組みと、内蔵ツール・拡張機能の地図をご紹介しました。本記事ではそこから一段踏み込んで、実際にエージェントを設計するときに頭の中にあるべき概念群を、図と具体例を中心に追いかけていきます。

この先のサンプルコード編(3本目・4本目)を読むときに「あの概念、中級編でやったやつだ」と思えることが本記事のゴールです。したがって、あえて具体的な実装コードは少なめに、絵と考え方を中心に進めます。

本記事で扱うテーマは、Claude Codeに限らずOpenAI AgentsLangChainなど、他のエージェントフレームワークにも共通します。一度身につけると他のツールを学ぶときにも効きます。

本記事で扱うテーマ

以下の15テーマを順に追いかけていきます。

  1. エージェントのメインループ
  2. Tool Use のメカニズム
  3. 会話プロトコルとメッセージ構造
  4. コンテキストウィンドウとトークン経済
  5. プロンプトキャッシュの基礎
  6. メモリ階層の発想
  7. 多層安全システム
  8. エラー回復の設計原則
  9. MCP(Model Context Protocol)設計の基礎
  10. フック設計
  11. スキル設計
  12. サブエージェントと並列化
  13. ストリーミング処理とレイテンシ最適化
  14. 観測可能性とロギング
  15. 中級者が次に学ぶべき設計パターン

それぞれ独立して読めるので、気になるテーマから飛び読みしていただいても構いません。

1. エージェントのメインループ

単発の質問と「エージェント」の違い

普段使っているChatGPTやClaudeは、基本的に「質問を送って1回だけ答えが返ってくる」形です。一方、エージェントは以下のように会話とツール実行を複数回往復します。

このループは人工知能研究の古典的な枠組みであるReAct(Reasoning + Acting)として知られています。2022年に論文"ReAct: Synergizing Reasoning and Acting in Language Models"で提案されたもので、「推論と行動を交互に行う」ことで、LLM単体では解けない複雑な問題に対処できる、という発想です。

実用的なエージェントでは何が違うのか

素朴なReActに対して、Claude Codeのような実用的なエージェントでは以下の強化が入っています。

要素 素朴なReAct 実用的なエージェント
ループ制御 while (終了条件なし) 状態マシン・maxTurns制限・強制中断
ツール呼び出し テキスト生成で "Action: ..." 構造化されたJSONブロック(Tool Use)
並列性 1ツールずつ直列 Read系は並列、Write系は直列
エラー処理 失敗したら終了 リトライ・段階的回復
コスト 無制限に消費 プロンプトキャッシュ・コンテキスト圧縮

状態マシンとしてのループ

素朴に while (継続) { 思考→実行 } と書くこともできますが、本番運用するエージェントはもう少し賢い状態遷移を持ちます。なぜかと言うと、途中で以下のような失敗が起きうるからです。

  • コンテキスト溢れ: 会話が長くなりすぎてモデルが受け取れる上限を超えた(413 Prompt too long)
  • 出力切れ: 応答の途中でトークン上限に達して途切れた(max_tokens_reached)
  • ツール失敗: コマンドが非ゼロ終了した、ファイルが見つからなかった、タイムアウトした
  • レート制限: API呼び出し頻度が上限を超えた(429 Too Many Requests)
  • ネットワーク瞬断: 一時的に接続が切れた

素朴なループではこれらは即エラーで落ちてしまいます。そこで、状態マシンとして設計し、各失敗に対して回復アクションを用意しておくのが定石です。

この状態遷移の具体的な実装はエージェント実装深掘り編で見ますが、いまは「エージェントのループには回復力が必要」という一般則だけ頭に入れておいていただければ十分です。

Plan-Act分離というバリエーション

ReActの発展形として、Plan(計画)とAct(行動)を完全に分離するパターンも増えています。Claude Codeのプランモードがその代表例です。

計画時点で副作用を出さないため、ユーザーが安心してレビューできるという利点があります。特に破壊的操作を含む大規模変更では、Plan-Act分離が事故防止に効きます。

2. Tool Use のメカニズム

「関数を呼ぶ」と言っても、直接は呼べない

LLMは自然言語を生成するモデルです。ファイルを読んだりコマンドを実行したりといった「関数呼び出し」は、LLM自身にはできません。

ではどうやってツールを使うのかと言うと、「ツール名と引数」をJSON形式のテキストとして出力するだけなのです。そのJSONを受け取ったホスト側(Claude Code等)が、実際に関数を呼び出して結果をLLMに戻します。

この「ツールを呼ぶように見えるが、実際はテキストの往復」という構造をTool Use(あるいはFunction Calling)と呼びます。Anthropic APIのドキュメントではTool Useのページで詳しく説明されています。

ツール定義の実際

LLMに渡すツール定義は、以下のようなJSONスキーマ形式です。

{
  "name": "Read",
  "description": "Read the contents of a file from disk",
  "input_schema": {
    "type": "object",
    "properties": {
      "file_path": {
        "type": "string",
        "description": "Absolute path to the file"
      }
    },
    "required": ["file_path"]
  }
}

LLMはこの定義を見て、自分がいま何ができるかを把握します。ツール定義が詳細で明確であるほど、LLMは適切に呼び分けます。逆に曖昧な定義だと、間違ったツールを呼んだり引数を間違えたりします。

ツールを統一インターフェースで扱う理由

Claude Codeには Read, Write, Edit, Bash, Grep, Glob, Task など多くのツールがありますが、すべて同じ形のインターフェースを守っています。

  • name — ツール名
  • description — LLMに見せる説明
  • inputSchema — 入力の型定義(Zod等のスキーマ)
  • call() — 実行本体
  • isReadOnly() — 読み取り専用か(並列実行の可否に関わる)
  • isConcurrencySafe() — 他ツールと同時実行してよいか
  • checkPermissions() — このツール固有の権限判定

すべてのツールがこの型を守ることで、メインループはツールを1つも知らないまま動けます。新しいツールを追加するときも、レジストリに登録するだけで済みます。これはソフトウェア設計における「開放閉鎖原則(Open-Closed Principle)」の良い実例です。

ツールの統一インターフェース設計は、ハーネス設計8パターン編の「ツールコントラクト」で詳しく扱います。

並列ツール実行の作法

read-onlyなツール(Read, Grep, Glob, WebFetch等)は並列で実行しても安全です。一方、write系ツール(Write, Edit, Bash等)は直列化しないと、同じファイルへの書き込みが競合します。

さらに、Anthropic APIには「tool_use_id に対応する tool_result を、直後のuserメッセージの先頭から順に並べる」という制約があります。並列で実行しても、レスポンス組み立て時には順序を揃える必要があるのです。

この「並列実行しつつ順序を守る」実装がOrderedDrainパターンとしてエージェント実装深掘り編に登場します。

3. 会話プロトコルとメッセージ構造

Anthropic Messages APIのメッセージ形式

Claude(LLM)とやり取りするときは、以下の形式のメッセージ配列を送ります。

{
  model: "claude-sonnet-4-6",
  system: "You are a helpful coding agent.",
  tools: [/* ツール定義 */],
  messages: [
    { role: "user", content: "READMEを要約して" },
    { role: "assistant", content: [
      { type: "text", text: "READMEを読みます" },
      { type: "tool_use", id: "toolu_01", name: "Read", input: { file_path: "README.md" } }
    ]},
    { role: "user", content: [
      { type: "tool_result", tool_use_id: "toolu_01", content: "# My Project\n..." }
    ]},
    { role: "assistant", content: "要約は以下の通りです: ..." }
  ]
}

ポイントは以下です。

  • systemプロンプトは全体の振る舞い方針(1回だけ)
  • messagesは交互: user → assistant → user → assistant ...
  • tool_useはassistant側のcontentブロックとして出現
  • tool_resultはその直後のuserメッセージのcontentブロックとして返す

tool_use_idtool_result の対応制約

これが特にハマりポイントです。並列で3つのツールを呼ぶと、assistantメッセージには tool_use が3つ並びます。これに対応する tool_result も、次のuserメッセージに3つ全て並べる必要があります。1つでも欠けるとAPIが400エラーを返します。

messages.N: `tool_use` ids were found without `tool_result` blocks
immediately after: toolu_XXX, toolu_YYY, ...

この制約のため、並列実行したツールの結果を取りこぼさず集めるOrderedDrainのような仕組みが必要になります。

ストリーミング形式のメッセージイベント

Claude APIは1回の応答でもストリーミング形式で複数のイベントに分解されて届きます。主要なイベント名は以下です。

イベント名 意味
message_start メッセージ開始
content_block_start コンテンツブロック(text, tool_useなど)開始
content_block_delta ブロックの差分(text_delta, input_json_deltaなど)
content_block_stop ブロック完了
message_delta メッセージ全体の差分(stop_reason等)
message_stop メッセージ完了

注意点として、tool_useinputcontent_block_start 時点では**空({})**で、input_json_delta イベントが順次届き、content_block_stop で完成します。この仕様を知らずにcontent_block_startinputを取り出そうとすると空になる、というのが実装時の定番バグです。

4. コンテキストウィンドウとトークン経済

コンテキストウィンドウとは

LLMが一度に覚えていられる情報の上限を指します。Claudeの場合、モデルにより以下のようになっています。

モデル コンテキストウィンドウ
Claude Opus 4.7 最大1Mトークン
Claude Sonnet 4.6 最大1Mトークン
Claude Haiku 4.5 最大200Kトークン

1トークンは英語では約4文字、日本語では1〜2文字に相当します。200Kトークンは小説1冊分ほどの情報量です。

なぜトークン管理が重要か

AIエージェントを本格的に動かすと、このトークン予算との付き合い方が設計の中心テーマになります。理由は3つあります。

  1. 上限を超えるとエラーになる — ハードリミット
  2. トークン量に比例して課金される — お財布の問題
  3. 長すぎると応答品質が下がる — 後半の指示が効きにくくなる現象("lost-in-the-middle")

Lost-in-the-middle 現象

LLMは長いコンテキストの先頭と末尾は覚えていやすく、中盤は忘れやすいという性質を持ちます。Liu et al., 2023 のLost-in-the-Middle論文で実証されました。

つまり、重要な指示をコンテキストの先頭か末尾に置くのが実務上の定石です。CLAUDE.mdをシステムプロンプト先頭に置き、最新ユーザー指示は末尾に来るのも、この原則に沿った設計です。

コンテキストを圧縮する3つのレベル

実務では、圧縮手法を段階的に重ねるのが定石です。

軽いものから試して、足りなかったら次の段階に進むという構造になっています。各手法の特徴を比べます。

手法 コスト 失う情報 復元可能性
microCompact ゼロ(regex) 60分以上前のRead/Grep結果 再実行で復元可
contextCollapse ゼロ(固定文字列化) 会話前半の詳細 不可(サマリのみ残す)
autoCompact LLM呼び出し1回 全履歴の細部 不可(要約のみ残る)

Claude Codeでは /compact スラッシュコマンドでautoCompactを手動発動できます。明示的に「ここで履歴を圧縮したい」という節目に使うのが効果的です。

5. プロンプトキャッシュの基礎知識

同じプレフィックスを何度も送らない

Anthropic APIにはプロンプトキャッシュという機能があります。一言でいうと、「さっきと同じ部分は再計算しなくていいよ」とAPIに教えてあげる仕組みです。

エージェントはターンが進むたびに、同じシステムプロンプトやツール定義を繰り返し送信します。この部分を明示的にキャッシュ対象としてマークしておくと、2回目以降のリクエストは以下のようなコストになります。

項目 通常 キャッシュヒット時
入力トークン単価 1倍 0.1倍(90%削減)
cache write(5分TTL) 1.25倍
cache write(1時間TTL) 2倍

初回書き込み時だけ少し高くなりますが、その後のキャッシュ読み取りが圧倒的に安いため、同じプロンプトで繰り返し呼ぶ用途ではほぼ必ず元が取れます。

キャッシュを有効化するには

APIリクエスト時に、キャッシュしたいブロックの末尾に cache_control: { type: 'ephemeral' } を付けます。

{
  system: [
    {
      type: "text",
      text: "You are a helpful agent...\n[長い指示]",
      cache_control: { type: "ephemeral" }
    }
  ],
  messages: [...]
}

これで、このブロックとそれより前のプレフィックスがキャッシュ対象になります。

キャッシュを壊さないためのコツ

ここでひとつ注意点があります。キャッシュは「リクエストのプレフィックスが同一」かつ「cache_control でマークされたブロックまで」が対象です。したがって、システムプロンプトの頭側に変化する情報を入れると、毎回キャッシュが壊れます。

変化する情報(タイムスタンプ・リクエストIDなど)は、キャッシュ境界の後ろ側に置くのが原則です。一見些細なことですが、長時間走るエージェントでは数倍のコスト差につながります。

キャッシュブレイクの検出

「キャッシュが効いていると思っていたが、実は効いていなかった」という事態は、コストが跳ね上がって初めて気付くケースが多いです。対策として、usage.cache_read_input_tokens の数値を毎リクエスト記録し、大幅にドロップした時点で原因を特定できる仕組みを持つのが実務の知恵です。

この具体的な実装はハーネス設計8パターン編の「プロンプトキャッシュ最適化」で扱います。キャッシュが壊れた瞬間を検出するCacheBreakDetectorも登場します。

6. メモリ階層の発想

「何もかも覚える」は破綻する

エージェントが1時間走ると、会話とツール結果は数万トークンに膨れ上がります。これを毎ターンそのままLLMに渡していたら、コストとコンテキストウィンドウが両方とも破綻します。

人間の記憶も似たような構造になっていて、短期記憶・作業記憶・長期記憶に分かれています。エージェントも同じように、情報を寿命別に3層に分けて管理するのが一般的です。

寿命 用途
短期 現在のターン ツール出力・思考 直前のgrep結果
セッション 現在のセッション 進行中のタスクの要約 「認証機能を実装中」
長期 プロジェクト横断 設計方針・規約 CLAUDE.md のルール

ポインタ指向という工夫

セッションメモリや長期メモリをプロンプトに直接埋め込むと、結局トークンを食います。そこで実用的なエージェントでは、「ここにメモリファイルがあるよ」というポインタ(ファイル参照)だけをプロンプトに入れ、本文はモデルが必要なときだけReadツールで取りに行く、という設計が取られています。

こうすると、長大なメモリを持っていても普段は数百トークンで済み、必要に応じてモデルが取りに行く構造になります。

非同期更新という発想

セッションメモリは、メインの会話を邪魔しない形で非同期に更新するのが理想です。Claude CodeのAuto memory機能は、~/.claude/projects/<project>/memory/ 配下のMarkdownファイルを、Claude自身が必要に応じて読み書きする形で実装されています。

自作エージェントで同じ発想を取り入れる場合、「メインの会話とは別にバックグラウンドでサブエージェントを動かし、会話履歴からキー情報を抽出してファイルに書き戻す」という設計も取りうる選択肢です。ユーザーの体感速度を落とさずメモリを最新に保てるため、非同期処理のパターンとして覚えておくと応用が効きます。

7. 多層安全システム

なぜ1層では足りないのか

「危険なコマンドをブロックする関数を1つ作ればよいのでは」と考えたくなります。実際、シンプルなケースではそれで十分です。ただし現実のエージェントは以下のような要求に同時に応える必要があります。

  • ユーザーが事前に許可したツールは聞き返さない
  • ユーザーが事前に禁止したツールは絶対に実行しない
  • .git/ のような重要パスへの書き込みは、たとえ許可モードでも確認する
  • 新しく追加されたツールにも自動で一貫した振る舞いをさせる
  • プラグイン・MCP経由のツールも同じ規則に従わせる

これらを1つの巨大なif文に押し込めると、読めないコードになります。そこで、複数のチェック関数を順に呼んで最初に確定した判定を採用するというパイプライン設計が採られます。

重要なのは順序です。Denyルールは常に最初に評価されるので、「絶対に実行してはいけない操作」をここに入れておけば、他の許可設定の影響を受けません。こうした優先順位の固定が、多層設計の核心です。

bypass モードの正しい理解

Claude Codeには --dangerously-skip-permissions(いわゆる bypass モード)があり、これは権限プロンプトをスキップするモードです。ただし公式ドキュメントによれば、bypass でも保護パス(.git/, .claude/, シェル設定など)への書き込みだけは例外的に確認が入ります。

「bypassだから何でも通る」ではなく、「壊滅的な事故は最後までガードする」という設計思想です。自分でエージェントを作る場合も、このポリシーは真似する価値があります。

サンドボックスとプロセス分離

多層安全の発展形として、ツール実行をサンドボックス内に隔離するアプローチがあります。

  • macOSの sandbox-exec
  • Linuxの seccomp, bubblewrap
  • 仮想マシン内実行
  • Docker コンテナ内実行

Claude Codeには簡易版サンドボックス機能が順次実装されつつあり、特にBashツールはネットワーク遮断・書き込み範囲限定のサンドボックスで実行するオプションがあります。クライアント側のアプリ制御だけでなく、OS機構まで動員して守るのが本格派です。

8. エラー回復の設計原則

失敗を前提に設計する

新人の方にぜひ押さえてほしいのは、**「エラーは起きる前提で設計する」**という姿勢です。エージェント運用では、以下のような失敗が日常的に発生します。

  • ネットワーク瞬断でAPIリクエストが失敗する
  • モデルが不正なJSONを返してきてパースに失敗する
  • ツールが予期しないエラーで落ちる
  • プロセスがクラッシュして会話履歴が消える
  • レート制限に引っかかる

これらに対する向き合い方は、以下の3原則にまとめられます。

原則1: Write-Ahead(先に書いてから進む)

クラッシュに備えるなら、重要な情報は何かを呼ぶ前にディスクに書く。データベースのWAL(Write-Ahead Logging)と同じ発想です。

Claude Codeのセッション永続化は、この原則で設計されています。ユーザーの発話を受け取ったら、LLM APIを呼ぶ前にJSONL形式でディスクに書く。これにより、APIコール中にクラッシュしても会話は残ります。

原則2: Idempotent(冪等性)

リトライ可能にするためには、副作用のある処理でも2回実行して問題ないように設計するのが理想です。たとえば以下のようなパターンです。

  • ファイル作成は「既存ならスキップ」ではなく「存在チェック後に上書き」
  • HTTP POSTは冪等キーを付けて二重実行を検出
  • DB書き込みは INSERT ON CONFLICT を使う

原則3: Graceful Degradation(徐々に品質を落とす)

コンテキストが溢れたからといってプロセス全体を落とすのではなく、段階的に情報を圧縮して処理を続ける。先ほどの圧縮パイプラインはこの原則の具体例です。

指数バックオフ

APIレート制限に遭遇したときの基本戦略は指数バックオフです。

async function callWithBackoff(fn: () => Promise<any>, maxRetries = 5) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn()
    } catch (err) {
      if (err.status !== 429) throw err
      const wait = Math.min(1000 * 2 ** i, 30000) + Math.random() * 500
      await new Promise(r => setTimeout(r, wait))
    }
  }
  throw new Error('Max retries exceeded')
}

待ち時間を倍々にし、さらにジッター(ランダム要素)を足すことで、複数クライアントが同時にリトライする「thundering herd」を防ぎます。

いざという時に効く、自分の目で読む習慣

エラーが起きたときの対処は、多くの場面でAIに任せてしまって構いません。実際、ここ最近のモデルはスタックトレースを受け取っただけで原因を言い当てることも珍しくなく、開発規模やチーム体制によっては人が介在する場面を極力減らしていく方向も十分に現実的です。

ただ、AIだけでは解けない問題や、解いた振りで終わってしまうケースも依然として残っています。そうした「いざという時」に立ち返れる手札として、以下の習慣を持っておくと役に立ちます。

  • スタックトレースを最後まで読む
  • ログのタイムスタンプから時系列を組み立てる
  • 再現手順を1つずつ書き出してから修正に入る
  • 修正後、同じ状況を意図的に再現してテストする

普段は全部やる必要はありません。AIに聞いて5分で終わるならそれでいい場面もたくさんあります。大事なのは問題が解決しない時に、自分の目でログや差分を読み解ける選択肢を持っておくことです。

AIは日進月歩で進化しており、「どこまでAIに任せるか」の境界線も毎年のように変わっていきます。自分自身の考え方やAIとの関わり方も、その進化に合わせて柔軟に変化させていくのが、長く使える姿勢だと思います。

9. MCP(Model Context Protocol)設計の基礎

MCPとは何か

MCPは2024年末にAnthropicが公開した、AIエージェントに外部データソースをつなぐ標準プロトコルです。公式仕様はmodelcontextprotocol.ioで公開されており、USBのように「ホストとサーバーの規格を統一して、つなぎ替えを自由にする」という狙いがあります。

MCPサーバーは3種類のリソースを提供します。

種類 用途
Tools LLMが呼び出せる関数 github.create_issue
Resources 参照可能な静的データ schema://database/users
Prompts 再利用可能なプロンプトテンプレート review_pr_template

なぜMCPが重要か

エージェントは、接続できる外部サービスの数が多いほど応用範囲が広がります。MCPが登場する前は、各エージェントごとにSlack連携・GitHub連携・DB連携を別々に実装する必要がありました。

MCPという共通規格ができたことで、以下の利点が生まれました。

  • エコシステムの再利用: GitHub公式MCPを作れば、Claude CodeもCursorもClineも使える
  • セキュリティ境界の明確化: サーバーごとに権限・認証を独立管理できる
  • 開発効率: ホスト側はMCPの規格だけ守れば、個別連携コードが不要

自作MCPサーバーの勘所

自作する場合の設計ポイントをいくつか挙げます。

  1. ツール定義のdescriptionを具体的に: LLMはdescription頼みで呼び分けるので、曖昧だと誤作動する
  2. エラー時の戻り値を構造化: 単にthrowするのではなく、is_errorフラグ付きJSONで返す
  3. 認証情報の扱いに注意: 環境変数経由とし、絶対にリポジトリに混入させない
  4. レスポンスサイズを意識: 数MBの結果を返すとコンテキストが一撃で溢れる

MCPサーバー開発の実践は、MCP公式ドキュメントや関連する SDK(Python / TypeScript)が充実しています。興味が深まったらそちらを参照してみてください。

10. フック設計

フックとは何か

フック(Hooks)は、Claude Codeのライフサイクルイベントに対して自動実行されるスクリプトを登録する仕組みです。.claude/settings.json には「イベント → マッチャ(matcher)→ hooks 配列」の3階層で書きます。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "node .claude/hooks/validate.js" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "node .claude/hooks/telemetry.js" }
        ]
      }
    ]
  }
}

matcher にはツール名やパターンを指定でき、「このツールのときだけ走る」という絞り込みができます。フックはstdinでJSON入力を受け取り、stdoutでJSONを返す契約になっていて、任意の言語で書けるため、シェルスクリプトでもPythonでもGoでも使えます。

主要なイベント

フックは多数のライフサイクルイベントに対して登録できます。代表的なものを列挙します(全容は公式ドキュメントを参照)。

イベント タイミング 代表ユースケース
PreToolUse ツール実行直前 危険コマンドブロック・入力検証
PostToolUse ツール実行直後 ログ記録・自動フォーマッタ
UserPromptSubmit ユーザー入力直後 追加コンテキスト注入
Stop メインエージェントの応答終了時 完了通知・サマリー生成
SubagentStop サブエージェント応答終了時 サブエージェント結果の記録
SessionStart セッション開始時 前回の続きを復元
SessionEnd セッション終了時 終了時の後片付け
PreCompact / PostCompact 履歴圧縮の前後 圧縮前の状態保存
Notification 通知発火時 外部通知への橋渡し

設計原則

フックを書くときに意識したい原則です。

  1. 適度に素早く返す: デフォルトの実行制限は60秒で、コマンドごとに個別設定もできます。長い処理はバックグラウンド化するのが基本です
  2. 冪等に: PostToolUseは複数回呼ばれる前提で設計
  3. 失敗は寛容に: フック内エラーでClaude Code本体が落ちないよう try-catch で包む
  4. 出力を構造化: 決まったスキーマでJSONを返す

実例: rm -rf ブロック

Bashコマンドに rm -rf / が含まれていたらブロックする、というシンプルなフック例です。

#!/usr/bin/env node
// .claude/hooks/block-dangerous.js
let input = ''
process.stdin.on('data', chunk => input += chunk)
process.stdin.on('end', () => {
  const data = JSON.parse(input)
  if (data.name === 'Bash' && /rm\s+-rf\s+\//.test(data.input?.command ?? '')) {
    console.log(JSON.stringify({
      decision: 'block',
      systemMessage: 'rm -rf / is blocked by hook policy'
    }))
  } else {
    console.log('{}')
  }
})

これをPreToolUseに登録しておくと、どれだけClaudeが暴走しても致命的操作はここで止まります。多層安全の「第2の防壁」として機能します。

11. スキル設計

スキルとは何か

スキル(Skills)は、MarkdownファイルでClaude Codeの振る舞いを拡張する仕組みです。SKILL.md というファイルに手順やルールを書いておくと、Claude Codeはそのスキル名を検知したときに内容を自動で読み込みます。

---
name: qiita-writing
description: Qiita記事の執筆ワークフローとスタイルガイド
---

## フロントマター
記事は以下のフロントマターで始める:
...

## 文体規則
- です/ます調で統一
- 1文60文字以内
...

なぜMarkdownで拡張するのか

スキルの特徴は、コードを一行も書かずに振る舞いを拡張できることです。

  • デザイナー・テクニカルライターでも書ける
  • バージョン管理がシンプル(diffが読みやすい)
  • 他のツール(GitHub Actions、CI)でも流用できる

プログラミング経験の浅いメンバーでも、自分の業務知識をスキル化できるのが大きな価値です。

スキルの構造

典型的なスキルディレクトリは以下のような構造です。

.claude/skills/qiita-writing/
├── SKILL.md            # メインの指示書
├── references/         # 参考資料
│   └── checklist.md
└── templates/          # テンプレート
    └── article.md

SKILL.mdから相対パスで他のファイルを参照でき、必要に応じてClaudeがReadで取得します。すべてプロンプトに詰め込むわけではないため、コンテキストを圧迫しません。

スキル化の判断基準

すべての知識をスキル化する必要はありません。スキル化が効く領域は以下です。

  • 繰り返し参照される定型ワークフロー(記事執筆・PRレビュー・コミット規約)
  • ドメイン固有のルール(社内コーディング規約・設計パターン)
  • 外部ツールの使い方(特定CLIの操作手順)

逆に、1回しか使わない知識や、コードで書いた方がシンプルな処理はスキルには向きません。

12. サブエージェントと並列化

サブエージェントとは

サブエージェントは、メインのClaude Codeから子プロセス的に呼び出される別のClaudeインスタンスです。Taskツールで起動します。

> 認証周りとAPI周りを同時に調査して。
● Task(agent=Explore, prompt="認証周りを調査...")
● Task(agent=Explore, prompt="API周りを調査...")

Claude Codeは2つのサブエージェントを並列起動し、それぞれが独立したコンテキストで調査を実行、最後にメインが結果を統合します。

なぜサブエージェントを使うか

利点は大きく3つあります。

1. コンテキスト節約

「大量のファイルをgrep→読解→要約」を全部メインでやると、grep結果の生データがメインのコンテキストを埋め尽くします。サブエージェントに委譲すれば、メインには要約結果だけ返ってくるので節約できます。

2. 並列性

複数の独立した調査を同時に進められます。直列なら30秒かかる作業が、並列なら10秒で終わる、といった時短効果があります。

3. 専門化

サブエージェントは専門用途ごとに独自のシステムプロンプトを持てます。Claude Codeでは、以下のようなビルトインエージェントが用意されています。

エージェント名 用途
Explore 高速な探索・検索タスク
general-purpose 汎用の多段調査
Plan 計画立案専用

このほか、コードレビューやセキュリティ監査といった用途向けには、エージェントではなくスラッシュコマンドとして /review/security-review が提供されています。プロジェクト固有のエージェントは .claude/agents/ にMarkdownで追加することもできます。

並列化の勘所

サブエージェントを並列起動するときは、以下に注意します。

  • 状態の共有は最小限に: 共有状態があると並列化のメリットが消える
  • 出力フォーマットを指定: 統合しやすい形で返してもらう
  • 1タスク1エージェント: 1つのサブエージェントに複数のタスクを詰め込まない
  • 結果の要約を依頼: 生データではなく要約を返させる

13. ストリーミング処理とレイテンシ最適化

ストリーミングの本質

LLM APIはストリーミング形式で応答を返します。これを最初から最後まで待ってから処理するのと、流れてくる途中から処理し始めるのでは、ユーザー体験が大きく変わります。

Claude CodeはLLMが応答を流している最中にツール実行を始めることで、ユーザー体感のレイテンシを大幅に削減しています。この「ストリーミング中ツール起動」のパターンは、エージェント実装深掘り編で詳しく扱います。

レイテンシ最適化のポイント

エージェントの「返答が速い」に効くのは以下の4点です。

  1. ストリーミング応答を使う: 最初のトークンが出るまでの時間(TTFT)が短い
  2. ツールを並列で起動する: 直列化は必要最小限にする
  3. プロンプトキャッシュを活用する: cache read はレイテンシも大幅に短い
  4. モデルを適材適所に: Haikuは速くて安いので軽作業に積極的に使う

実測データと体感

OpenTelemetryでClaude Codeの内部を計測すると、実態がよくわかります。

  • ファイル1読み込み: 平均 50ms
  • Bash簡易コマンド: 平均 200ms
  • WebFetch: 平均 800ms(SPAは秒オーダー)
  • LLMのTTFT(time to first token): Sonnetで 400〜800ms

並列化が効くのは「同じオーダーの処理を複数走らせるとき」です。1つだけ重いタスクがあると、並列化しても全体はその重い1つに律速されます。

14. 観測可能性とロギング

なぜ観測可能性が重要か

AIエージェントは、従来のWebアプリケーション以上に**「何をしているか見えにくい」**存在です。ブラックボックスの中で複数ターンのLLM呼び出し・ツール実行が飛び交うため、問題が起きたときに原因特定が難しいのです。

この状況を打開するのが、**observability(観測可能性)**のアプローチです。以下の3本柱があります。

役割 Claude Codeでの実装
Logs イベントを時系列で記録 JSONLセッションファイル
Metrics 数値を時系列で集計 OpenTelemetryメトリクス
Traces 呼び出しチェーンを追跡 OpenTelemetryトレース

OpenTelemetry対応

Claude Codeは、OpenTelemetryへの出力をサポートしています。環境変数で設定します。最小構成は以下のようなイメージです(詳細は公式の監視ドキュメントを参照)。

# 有効化
export CLAUDE_CODE_ENABLE_TELEMETRY=1

# エクスポータの種別(metrics / logs / traces それぞれ設定)
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp

# OTLP接続先
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

Jaeger・Tempo・Datadog・New Relic等の受信側に送ると、以下が可視化できます。

  • どのツールがどれだけ時間を使っているか
  • LLM呼び出しの入出力トークン数と課金
  • セッション全体の処理時間内訳
  • エラー発生箇所と頻度

セッションログの活用

Claude Codeは各セッションを ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl として記録しています(encoded-cwd は作業ディレクトリパスをエンコードしたもの)。このファイルには会話の全トレースが入っているため、以下の用途に使えます。

  • セッション復元(claude --resume
  • 問題の再現とデバッグ
  • 成功パターンの抽出(「うまくいったセッション」を教師データに)
  • 監査・コンプライアンス対応

メトリクスで見るべき数字

本番運用で追いかけるべきメトリクスの例を挙げます。

メトリクス 見方
ターン数の分布 1タスクあたりの平均ターン、maxTurns到達率
キャッシュヒット率 cache_read / (cache_read + input_tokens)
ツール失敗率 is_error=trueの割合、ツール別
レート制限頻度 429エラー発生回数
平均レイテンシ TTFT、ツール実行時間、トータル

ユーザー側での観察

個人利用レベルでは、以下のシンプルな習慣で「見える化」ができます。

  • /cost でトークンとコストを定期チェック
  • /status で現在の設定・モデル・認証を確認
  • 異常に長い処理のときはCtrl+Cでキャンセルして /compact を挟む
  • セッション終了後に .claude/projects/ を覗いてみる

15. 中級者が次に学ぶべき設計パターン

ここまでで、エージェント設計の基礎となる14テーマをご紹介しました。これらの概念を、動くサンプルコードとして実際に組み立てるのが、シリーズの後半2本です。

ハーネス設計8パターン編

ハーネス設計8パターン編では、本記事で登場した以下の概念を1ファイル完結のTypeScriptサンプルで再現します。

  • ハーネスパラダイム(§7で触れた多層防御の基礎)
  • ツールコントラクト(§2で触れた統一インターフェース)
  • クエリエンジン(§1の状態マシンループ)
  • パーミッションシステム(§7の多層判定)
  • コンテキストエントロピー管理(§6のメモリ階層)
  • プロンプトキャッシュ最適化(§5のキャッシュ)
  • その他2パターン

それぞれ10〜50行程度の小さなサンプルで、APIキー不要で動きます。

エージェント実装深掘り編

エージェント実装深掘り編では、上記の概念を統合した1つのエージェントを動かしながら、実装詳細を読み解きます。

  • ストリーミングレスポンスとツール並列実行の両立(§13の実装)
  • OrderedDrain による順序保証(§2の並列制約の実現)
  • 多段圧縮の閾値設計(§4の実装)
  • キャッシュブレイクの検出(§5の実装)
  • ライフサイクルフックの契約(§10の実装)
  • Write-Ahead JSONL 永続化(§8の実装)

こちらは実API(Anthropic公式API またはAzure Anthropic)でも動かせる構成です。

両方のサンプルコードは以下のGitHubリポジトリで公開しています。

まとめ

本記事でお伝えしたかったことを、15点にまとめます。

  1. エージェントのループは単なるwhileではなく、回復力のある状態マシンとして設計する
  2. LLMのツール呼び出しはJSONテキストの往復であり、ホスト側が実際の関数呼び出しを担う
  3. tool_use_idtool_result の対応制約があり、欠落があればAPIが400エラーを返す
  4. トークン経済を意識しないと、エージェントはすぐに破綻する
  5. Lost-in-the-middleを避けるため、重要な指示は先頭か末尾に置く
  6. プロンプトキャッシュを正しく使うと、運用コストが大きく下がる
  7. メモリ階層(短期・セッション・長期)とポインタ指向で、長時間実行にも耐える
  8. 安全性は多層の優先順位付き判定で守る(bypass でも壊滅的操作は確認)
  9. エラー回復はWrite-Ahead・冪等性・段階的劣化の3原則で設計する
  10. MCPは外部サービス連携の標準プロトコル、USBのように規格統一する
  11. フックはライフサイクルイベントに対する自動化の差込口(5種類)
  12. スキルはMarkdownで振る舞いを拡張、非エンジニアでも書ける
  13. サブエージェントでコンテキスト節約・並列化・専門化ができる
  14. ストリーミング処理中にツール起動することでレイテンシを下げる
  15. 観測可能性(logs, metrics, traces)なしでは本番運用は難しい

どれか1つでも「あ、これは自分の言葉で説明できる」となっていれば、中級編としての役目は果たしたかなと思います。

次のサンプルコード編では、これらの概念が具体的なコードに落ちたときにどう表現されるかを、一緒に追いかけていきます。新人の頃からこの「概念 → コード」の往復を意識しておくと、どんなフレームワークに出会っても応用が効くようになります。


参考リンク

前の記事・次の記事

28
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?