AI実践

エージェントハーネスの内部

Claude Codeのセッションログで解剖する
判断の自動化を成立させているのはモデルでなくハーネス。自分の jsonl と朝刊エージェントの実コードで、配線を開ける。

2026年6月 読了 約12分 エージェント設計に踏み込みたい技術者向け

この記事のポイント

  • 「手続きの自動化」を「判断の自動化」に変えているのはモデルではなく、その外側で回るハーネス(実行基盤)。本稿はその中身を5要素で解剖する
  • 自分のセッションログ29本を実測すると、ハーネスの形が裏側から透けて見える——cache_readが入力の611倍、tool_useはセッション中央値88回、最大414回
  • tool_useがtextブロックに漏れた19件は、LLMとハーネスのあいだに「整形プロトコル境界」がある証拠。エージェント性はモデルでなく整形と実行の側に宿る

SECTION 1

前編からの接続——モデルでなく、その外側を開ける

LLMは次の単語を選ぶ関数——前々回はそう書いた。入力を渡すと確率分布から1トークンを引く、それを繰り返す。モデル単体はあくまで1ショットの関数である。これを「判断の自動化」に化けさせているのは、関数の外側にいるハーネス(実行基盤)のほうだ。ループを回し、ツールを差し出し、結果を読み、文脈を剪定し、次の一手を組み立てる。エージェントらしさは、ほぼ全部そこで起きている。

前編 エージェントは「誰が」動かしているのか では、その分業を「自分はどの層に乗るか」という問いまで持っていった。本稿はそこから一段下りる。あの記事が「どの層に乗るか」だったなら、本稿はその層の中身を開ける。蓋を開けて、ループ・ツール・キャッシュ・サブエージェント・コンパクションがどう噛み合って「判断の連鎖」を成立させているのかを、配線レベルで見る。

エージェントの「使い方」を紹介する記事はもう溢れている。けれど、ハーネスの中身を一次資料で解剖した記事はまだ少ない。本稿は、自分の手元にしかない二つの一次資料で書く。

資料①: Claude Code のセッションログ——自分の ~/.claude/projects/.../jsonl。計測対象29本、tool_use 中央値88回/最大414回、cache_read 約10.9億トークンといった生の数字が取れる。

資料②: 自作の本番エージェント「朝刊エージェント」——TypeScript+AWS CDK。Pipeline の collect/compose、Structured Outputs、S3 経由の context 注入まで、コードと配線が全部自分のものだ。

読み方ガイドを2行だけ置いておく。縦軸に 5要素(Tool Use / Context / Prompt Caching / SubAgent / Loop制御)、横軸に 二つの一次資料 を取って、それぞれのマスを順に埋めていく構成にしてある。「概念 → 実例 → 実ログ/実コード」のリズムで読むと、どの章がどのマスを埋めにいっているかが見えるはずだ。

SECTION 2

手続きの自動化から、判断の自動化へ

これまでの自動化は、手順を全部書くことで成立してきた。シェルスクリプトもCI/CDも、人間が分岐と順番をあらかじめ書き切る世界だ。if の条件、for の範囲、失敗時のリトライ回数。「何をするか」を最後の一手まで決め打ちするのが、これまでの「自動化」だった。

エージェントが変えたのはここである。「何をするか」は書かない。書くのは 「何ができるか」=ツール群 だけだ。あとは「Webを調べてニュースを5本選んでメールにしろ」と目的を渡すと、LLMが状況を見ながら「次はこのツール、次はあのツール」と自分で組み立てる。手続きを書く側から、判断を委ねる側へ。エージェントとは、その委譲を成立させる仕組みのことだ。

UNIX哲学を思い出してほしい。small sharp tools を作り、テキストでつなぐ。grep | sort | uniq -c | sort -rn のように、独立した小さな道具を標準入出力で連結する思想だ。エージェントもまったく同じ系譜にある。違いは、パイプの接続役が人間(あるいは固定スクリプト)からLLMに変わったこと、それだけだ。だからこそ次の命題が成り立つ——エージェントの限界は、ツール設計の限界である。LLMがどれだけ賢くなろうと、与えられた道具の外側にはみ出して動くことはできない。本稿の主筋として、これを置いておく。

ハーネスの最小構造は3形態に分けられる。同じ図にまとめるとこうなる。

[A] シングルショット
   user ──▶ [LLM] ──▶ answer        ※ツール無し、1ターンで終わる

[B] エージェントループ(基本形)
   user ──▶ [LLM] ─┬─▶ tool_use ──▶ [Tool] ──▶ tool_result ─┐
                   │                                          │
                   └◀─────────── loop (max_iterations) ◀──────┘
                              │
                              └─▶ final answer

[C] Orchestrator + SubAgents(多層)
   user ──▶ [Orchestrator LLM]
              ├──▶ [SubAgent: 収集] ──▶ tools...
              ├──▶ [SubAgent: 編集] ──▶ tools...
              └──▶ [SubAgent: 配信] ──▶ tools...
                     ↑ 各SubAgentの内側に [B] が入れ子で回る

[B] のループの内側で起きているのが、Planning(次に何をすべきか考える)→ Tool selection(手持ちの道具からどれを使うか選ぶ)→ Self-correction(結果を見て次の一手を修正する)の反省サイクルだ。失敗してもループの中で気付き、軌道を直す。逆に言えば、この3点のどれかが弱いと、ハーネスは途端に暴走するか、停止する。

次章では、この [B] のループと [C] のオーケストレーションを5つの要素に分解する。縦軸を立ててから、第4章で朝刊エージェント、第5章でセッションログという二つの一次資料に重ねていく。

SECTION 3

ハーネスを5つに分解する

ハーネスは「LLMの周りに張った足場」の総称だが、解像度を上げないと議論が滑る。本稿では以降ずっと、次の5要素で切る。各要素は単独で意味を持ち、どれか1つ抜けるとエージェントは別の壊れ方をする。

1. Tool Use / Function Calling

「何ができるか」だけ与えて「何をするか」をLLMに委ねる仕組み。これがないと、エージェントは外界に触れられない純粋な文章生成器に戻る。ファイルを読むことも、Webを叩くことも、メールを送ることもできない。判断の自動化はここから始まる。

2. Context Compaction

長くなった会話履歴を要約・剪定して、有限のコンテキスト窓に収め直す機構。これがないと、長期セッションは窓を超えた瞬間に古い前提を忘れるか、肥大化したまま劣化する。Claude Codeは自動 compaction を持ち、それでも限界が来ると人間が /compact を打つ。朝刊エージェントは履歴をモデル内に持たず、S3 を外付け記憶として渡す。内側で要約するか、外側に逃がすか

3. Prompt Caching

不変な前文を先頭に固定して、KVキャッシュを毎ターン再利用する仕組み。これがないと、長いシステムプロンプトを毎回フルで再計算する羽目になり、コストとレイテンシが線形に積み上がる。どれくらい効くかは第5章で実数を開ける。一方で、朝刊エージェントのようなバッチ便ではキャッシュが乗らず、見送りが正解になることもある。

4. SubAgent / Multi-Agent

Orchestrator が役割特化のワーカーを呼び出す構造。これがないと、1つのモデルに「収集も編集も判断も」を詰め込むことになり、プロンプトが肥大し責務が混ざる。朝刊エージェントは Pipeline クラスが collect/compose を司り、Claude Code は Agent ツールで明示的に subagent を launch する。

5. ループ制御

max_iterations / timeout / 並列 worktree などの「止め方」と「散らし方」。これがないと、tool_use ループは平気で暴走する。上限を切らなければコストは爆発し、無限ループは静かにバジェットを焼く。「止め方」を握っている側がループの主導権を握る

以降の章は、この5要素を縦軸に、朝刊エージェント(自分が運用している本番)と Claude Code のセッションログ(自分の手元の一次資料)を横軸に置いて解剖していく。抽象だけで終わらせず、それぞれに実コードか実ログを必ず1つ添える。

SECTION 4

実装例——朝刊エージェントを5要素にマップする

前提を3行でまとめる。構成は EventBridge Scheduler → Lambda(収集)→ S3 → Lambda(送信)→ Amazon SES の片道パイプラインで、朝 6:30 JST/夕 18:00 JST の 1日2便のバッチ。情報源は Webのみ。実装は TypeScript(@anthropic-ai/sdk + AWS CDK)。全体図は 朝刊エージェントの現在地 に置いてあるので、ここでは5要素にどう割り付けたかだけを書く。

EventBridge ─▶ Lambda(collect) ─▶ S3 ─▶ Lambda(compose+send) ─▶ SES
                 │                          │
                 └─ WebAgent (haiku)        └─ ComposerAgent (haiku)

Tool Use——並列はコード側、LLMは判断だけ

webFetchTool はあるが、tool_use ループは回していない。URLリストを取ってきて並列で叩く部分は TypeScript 側でやり、LLM には集まったテキストを渡して「トピック分類とスコアリング」だけをさせる。出力は Structured Outputs(JSON schema 強制)で型を縛る。LLMに「次にどのツールを呼ぶか」を委ねるのをやめた瞬間に、ループの暴走と O(n²) の課金が消える(このコスト面の意思決定は 1回$0.20の朝刊が$0.02になるまで で深掘り済み)。

// ※ 実装の要点を抜粋・簡略化(src/agents/webAgent.ts)
const summaryResponse = await this.client.messages.create({
  model: 'claude-haiku-4-5-20251001',
  max_tokens: 8192,
  output_config: { format: buildSummaryFormat(input.config.topics) },
  // ... system / messages
});

Context管理——外付けS3に逃がす

Lambda はステートレスだから、記憶は S3 に持たせる。editorialContext.ts が前回配信(朝刊→夕刊/前日夕刊→今朝)の picks を次回プロンプトへ差し込み、deliveredHistory.ts が過去7日の配信済みURLを除外リストとして注入する。Claude Code が会話履歴を自動 compaction で詰めるのに対し、こちらはバッチ便なのでセッションそのものが存在しない。「便と便のあいだを橋渡しする外部記憶」を自前で組む必要があった。

Prompt Caching——見送り

これは 明示的に見送っている。理由は単純で、1日2便のバッチは TTL 5分に乗らないし、cache_write の 1.25倍コストが回収できる回数の呼び出しが発生しない。Claude Code のように1セッションで cache_read が億単位に積み上がる利用形態とは前提が違う。判断の経緯は morning-agent-cost に書いた。

SubAgent / Multi-Agent——オーケストレーターはコード

src/orchestrator/pipeline.tsPipeline クラスが司令塔。collect フェーズは Promise.allSettled で並列、compose フェーズは前段の結果を context として渡す直列。Orchestratorパターンを採るが、指揮するのはLLMではなくコード

// src/orchestrator/pipeline.ts より抜粋
const settledResults = await Promise.allSettled(
  this.collectors.map((agent) => agent.run(input))
);
const successCount = collectResults.filter((r) => !r.error).length;
if (successCount === 0) {
  throw new Error('[Pipeline] All collectors failed. Aborting pipeline.');
}

1つ落ちても続行、全滅したらabort。この「決定的なコードがループを握る」設計は、第3章で見た Claude Code の自律ループとは正反対のスタンスだ。新聞は毎日定刻に出すべきもので、LLMに「いま発行すべきか」を判断させる必要はない。

ループ制御——そもそも回さない

ここが一番大きい設計判断。tool_use ループ自体をやめ、LLM呼び出しは原則1ステップに畳んだ。並列フェッチはコードで、選定はLLMで、配信判断はまたコードで。max_iterations を悩む前にループを消す、というのも立派な解だ。

最後に運用面を一つだけ。runArchive.ts が実行ごとに収集ソース・採用記事・トピック構成を S3 に保存していて、これが評価ハーネスの原料になっている。重複率・カテゴリ精度・見出し忠実性をどう測っているかは AIの出力品質を「なんとなく」で語らない を参照してほしい。

SECTION 5

ハーネスが漏れた瞬間——セッションログで解剖する

自分が今動かしている Claude Code は、ユーザーとアシスタントの全メッセージ、tool_use の引数と結果、トークン使用量、システムリマインダーまで、全部が ~/.claude/projects/.../jsonl に残る。論文や公式ドキュメントを読む代わりに、これをそのまま開けば、ハーネスの形が裏側から透けて見える。ここでは自分の手元の29セッションを集計した実測値を出す。

指標
tool_use 回数 / セッション最小 2 / 中央値 88 / p75 154 / 最大 414
総アシスタント応答6,165(対ユーザー 3,441)
総 output tokens10.4M
総 cache_read tokens1.09B
cache_read / 生 input 比約 611 倍
ツール上位Bash 776 / Edit 758 / preview_eval 492 / Read 429
system-reminder 出現56 回
ユーザー /compact 手動実行4 回
text 内に <invoke 漏れ19 回

ここから「ハーネスが漏れた瞬間」を4つ、観測事実として短く解剖する。

1) prompt caching が効いている証拠

cache_read が生 input の約 611倍。これは、ハーネスが毎ターン同じ前文(system プロンプト・既読ファイル・規約・直近の会話履歴)を先頭に積み直し、KV キャッシュからほぼタダで取り戻している、ということに他ならない。長セッションでも会話が破綻せず、しかも課金額が爆発しないのは、この裏側の積み直し戦略のおかげである。モデル単体の能力ではなく、ハーネス側の運用設計がコストとレイテンシを決めている

2) system-reminder の常時注入

ユーザー側メッセージのなかに <system-reminder> が 56回 現れた。これは「ハーネスが LLM に注意書きを差し込む通信路」が会話の表に出ないかたちで常時開いていることを意味する。タスク管理の催促、利用可能スキルの案内、Auto Mode の挙動切替、CLAUDE.md の再注入——いずれも、ユーザーが書いたメッセージに見えるが、実体はハーネスが封入している。会話のうしろから運営側が割り込めるメタ通信路、と思えばいい。

3) /compact の手動介入が4回

561 メッセージ目、784 メッセージ目で /compact が走っていた。Context Compaction は普段は自動で回るが、長セッションになると「自動の判断より、自分の意図で切りたい」瞬間が来る。そのとき人間がハーネスを直接叩く。自動と手動の両刀で記憶の総量を絞っているわけだ。

4) tool_use が text に漏れた19件

ここがいちばん示唆的だ。assistant の text ブロックの中に、こんなものが素のまま吐かれていた。

call
<invoke name="Edit">
<parameter name="file_path">/Users/.../design-guidelines.md</parameter>
...

本来なら、これは jsonl 上では tool_use ブロックとして構造化され、ハーネスが Edit を実際に走らせる。ところが整形が外れて文字列のまま text に流れ出ると、ハーネスはそれを関数呼び出しに昇格させられず、結果として実行されない。モデルはたいてい次のターンですぐ気付き、普通に Edit を呼び直していた。

この19件が静かに語っているのは、LLMとハーネスのあいだにはプロトコル境界があり、その整形が外れた瞬間、ツールは「ただのテキスト」に戻るということだ。前編で書いた「エージェント性はモデルでなくハーネスに宿る」が、自分のログから事故のかたちで顔を出した瞬間である。

ツール分布にも一言だけ触れておく。上位に preview_eval 492 / preview_screenshot 258 と、ブラウザ駆動系が大量に並んでいる。これはフロントエンド作業中心のセッションが多かったからで、同じハーネスでも、タスクによって使うツール集合は大きく偏る。Bash と Edit を主軸にしたコーディングのセッションと、preview 系で画面を回しながらの調整セッションとでは、外から見れば同じ Claude Code でも、内側の「手の動き」はかなり違う。

未検証

長セッションでの応答の鈍化、いわゆる品質劣化は、ログ上のメタ指標としては観測できなかった。token usage の推移には残るが、「鈍化」と判定するための基準を持っていないので、jsonl の表からはまだ取れていない。次の機会に評価ハーネスの軸を切り直したい。

SECTION 6

限界とリスク——ツール面の落とし穴

ハーネスを自分で組むと、便利さの裏側でハーネス特有の事故を自分で踏むことになる。たくさんあるが、現実に効くのは3本だ。

1. コスト爆発——tool_use ループは O(n²)

tool_use のループは、毎ターン「これまでの全履歴 + 新しい tool_result」をモデルに送り直す。会話が伸びるほど1ターンあたりの入力トークンが膨らみ、ループ全体では入力量が O(n²) で効いてくる。max_iterationstimeout を握っていないと、数十〜数百リクエスト分のコストが積み上がり得る。朝刊エージェントで実際に効いた打ち手の詳細は morning-agent-cost に譲り、本稿では「上限を握る側に立て」とだけ書いておく。

2. ツール面のプロンプトインジェクション

入力面の Lethal Trifecta は プロンプトインジェクションを徹底的に調べた で扱った。本稿はツール側から刺さる4つに絞る。

防御は4階層で重ねる。

  1. 最小権限: ツールに渡す権限・スコープを絞る。読み取りと送信は同じエージェントに同居させない。
  2. サンドボックス: 実行環境を隔離し、副作用の届く範囲を物理的に区切る。
  3. 出力検証: ツールの戻り値・description の差分を検証し、Rug Pull / Shadowing を検出する。
  4. スキャン: mcp-scan などで MCP サーバの description を継続スキャンする。

Claude Code 側であれば、Claude Code hooks で防御設計する で hooks を足せる。

3. 長コンテキスト劣化——自動 Compaction の盲点

自動 Compaction は便利だ。だが、要約で落ちるのは多くの場合「なぜこの判断をしたか」の文脈で、これは後から復元できない。実セッションでも、長くなった会話で自分は /compact を手で打って整理し直している(実測4回)。どの時点で圧縮を切るか、どの判断ログを残すか——このメタな判断は、まだハーネスではなく人間の仕事だ。

SECTION 7

まとめ——次は計測する

ハーネスを開けて中身を眺めれば、エージェント性がどこに宿っているかは案外あっさり透ける。判断の自動化を支えているのはモデル本体ではなく、その周囲に組まれた配管のほうだ、というのが本稿で繰り返した話だった。最後に、縦軸×横軸の早見表で締める。

5要素 朝刊エージェントの実装 Claude Codeのログから見えたもの
Tool Use並列フェッチはコード、LLMは分類だけ。Structured Outputsで型を縛るBash/Edit/preview_eval が支配。textに <invoke 漏れ 19件
Context管理S3に外付け(editorialContext / deliveredHistory)自動 compaction + 手動 /compact 4回
Prompt Cachingバッチ便のため見送りcache_read が生 input の約611倍
SubAgentPipelineクラスが orchestrator、collect並列/compose直列Agentツールで explicit launch、添付13件
ループ制御そもそも回さない。LLMは原則1ステップtool_use 中央値88 / 最大414回 / セッション

ここまでは構造の解剖だった。次は計測である。Observability——具体的には Langfuse と OpenTelemetry + CloudWatch でトレースを取り、朝刊エージェントと Claude Code の両方で同じ目線で並べる、という回を予定している。「中央値88回・最大414回の tool_use」みたいな数字を、本番運用にも同じ粒度で持ち込む話だ。