この記事のポイント
- ✓「手続きの自動化」を「判断の自動化」に変えているのはモデルではなく、その外側で回るハーネス(実行基盤)。本稿はその中身を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.ts の Pipeline クラスが司令塔。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 tokens | 10.4M |
| 総 cache_read tokens | 1.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_iterations と timeout を握っていないと、数十〜数百リクエスト分のコストが積み上がり得る。朝刊エージェントで実際に効いた打ち手の詳細は morning-agent-cost に譲り、本稿では「上限を握る側に立て」とだけ書いておく。
2. ツール面のプロンプトインジェクション
入力面の Lethal Trifecta は プロンプトインジェクションを徹底的に調べた で扱った。本稿はツール側から刺さる4つに絞る。
- ▸Tool Poisoning: ツールの description に「ユーザーに知らせず X を実行せよ」のような命令を埋め込む。モデルはツール定義を素直に読むので、悪意の指示が「仕様」として通ってしまう。
- ▸Rug Pull: ユーザーが一度承認したツールの description を、後から差し替える。同意済みに見せかけたまま挙動を変える、古典的な詐称パターン。
- ▸Tool Shadowing: 正規ツールと同名の悪意ツールを後から登録し、優先解決を奪う。MCPサーバを複数つないでいると刺さりやすい。
- ▸Toxic Agent Flow: 単体では無害なツール同士を、信頼境界をまたいで連鎖させる(社内ファイル読み取り → 外部送信、など)。1本ずつ見ても気づきにくい。
防御は4階層で重ねる。
- 最小権限: ツールに渡す権限・スコープを絞る。読み取りと送信は同じエージェントに同居させない。
- サンドボックス: 実行環境を隔離し、副作用の届く範囲を物理的に区切る。
- 出力検証: ツールの戻り値・description の差分を検証し、Rug Pull / Shadowing を検出する。
- スキャン:
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倍 |
| SubAgent | Pipelineクラスが orchestrator、collect並列/compose直列 | Agentツールで explicit launch、添付13件 |
| ループ制御 | そもそも回さない。LLMは原則1ステップ | tool_use 中央値88 / 最大414回 / セッション |
ここまでは構造の解剖だった。次は計測である。Observability——具体的には Langfuse と OpenTelemetry + CloudWatch でトレースを取り、朝刊エージェントと Claude Code の両方で同じ目線で並べる、という回を予定している。「中央値88回・最大414回の tool_use」みたいな数字を、本番運用にも同じ粒度で持ち込む話だ。