データベースを叩いて 8,000 行返ってきたとき、あるいは Web ページをスクレイプして 200KB の HTML が返ってきたとき、その全文をそのまま Claude のツール返り値として渡すと何が起きるか。私自身、個人開発で運営している6サイトの自動投稿パイプラインで、最初に大きく踏んだのがこの落とし穴でした。返り値 1 件あたり 60,000 トークンを超えるツールがループの中で3〜4回呼ばれ、context window はあっという間に飽和し、最終的なアウトプットの品質まで巻き添えで悪化したのです。
ツール返り値の圧縮は、エージェントを長く動かすほど効いてくる地味だが致命的な設計領域です。Anthropic の公式ドキュメントは「ツール定義の書き方」までは丁寧に書かれていますが、返り値の側をどう絞り込むかは実装者の裁量に任されています。ここでは、私が運用する自動投稿エージェント・コンテンツ監査エージェント・収益分析エージェントで実装している3層の圧縮アーキテクチャを、TypeScript のコード例とともに整理していきます。
巨大ツール返り値が引き起こす3つの劣化
「context window が大きくなったから生で渡しても大丈夫」という考え方は、コスト面でも品質面でも誤りです。具体的には次の3つが同時に悪化します。
- トークンコストの直接的な増加: Claude Sonnet 4.6 で 60,000 トークン × 4 ツール呼び出し = 240,000 トークン分の入力が毎ターン乗ります。$3/M トークンとして 1 リクエスト $0.72。1 日 200 リクエスト動かす自動投稿パイプラインなら月 $4,300 規模の純粋な無駄が発生します
- 指示追従性能の低下: 入力長が伸びるほど、システムプロンプトや指示の影響力が薄まる傾向があります。私の運用では、ツール返り値合計が 30,000 トークンを超えたあたりからスタイルガイド違反率が体感で 2 倍ほど増える印象です
- キャッシュ効率の崩壊: prompt caching を効かせている場合、ツール返り値が毎回変動する性質を持つため、cache hit 率がツール呼び出し回数に応じて急速に下がっていきます
整理すると、ツール返り値の圧縮は「コスト最適化」だけの問題ではなく、エージェント全体の信頼性に関わる設計レベルの判断だということです。
圧縮の3層モデル
私が実装で使っている分類は次の3層です。上の層ほど安全(情報損失が少ない)、下の層ほど強力(圧縮率が高い)。実運用ではツールごとに3層から選び、必要なら複数を組み合わせます。
第1層: スキーマプロジェクション(必要なフィールドだけ返す)
最も情報損失の少ない圧縮方法は、そもそも不要なフィールドを返さないことです。例えば Stripe の Charge オブジェクトは 50 を超えるフィールドを持ちますが、エージェントが消費分析のために必要なのは id, amount, currency, created, description の5つだけ、ということが珍しくありません。
// Claude に渡す前にプロジェクション
async function searchCharges(query: SearchQuery) {
const charges = await stripe.charges.search({
query: query.text,
limit: 100,
});
// 50+フィールド × 100件 = 数十万トークン → 5フィールド × 100件 = 約1万トークン
return {
data: charges.data.map((c) => ({
id: c.id,
amount: c.amount,
currency: c.currency,
created: c.created,
description: c.description,
})),
has_more: charges.has_more,
next_cursor: charges.data[charges.data.length - 1]?.id,
};
}経験上、プロジェクションだけで 70〜90% のトークン削減が達成できることが多いです。特に外部 API ラッパー型の MCP サーバーを書くときは、「Claude が判断に使うフィールド」と「人間がダッシュボードで見たいフィールド」を区別する設計を意識すると、自然にこのレイヤーに到達します。
第2層: サマリ生成(Haiku に要約させてから渡す)
スキーマレベルでは絞れない、自由テキストの返り値が来るツールがあります。Web 検索、ファイル読み込み、ログ取得などがこれに当たります。ここでは Haiku に要約させてから Sonnet/Opus のエージェントループに渡すカスケード構成が効きます。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function fetchAndSummarize(url: string, focus: string) {
const html = await fetch(url).then((r) => r.text());
const text = stripHtml(html).slice(0, 50_000); // 約 25,000 トークン分
// 5,000トークン分の入力 → 400トークン分のサマリにダウンサイズ
const summary = await client.messages.create({
model: "claude-haiku-4-5-20251001",
max_tokens: 400,
system: `次のWebページを要約してください。要約観点: ${focus}。
事実のみ。推測や評価は含めない。重要な数値は必ず残す。`,
messages: [{ role: "user", content: text }],
});
return {
url,
focus,
summary: summary.content[0].type === "text" ? summary.content[0].text : "",
original_length: text.length,
};
}このパターンで重要なのは、要約観点 (focus) を呼び出し側のツールパラメータに含めることです。Claude 自身が「自分が今何を知りたいか」をツール呼び出し時に宣言する設計にしておくと、Haiku の要約が呼び出し意図に沿った内容になり、再質問のターンが減ります。
ちなみにこの「上位モデルが下位モデルに圧縮させる」発想は、私のアプリ事業で 2014 年から AdMob の最適化に使っていたメディエーション設計と発想が似ています。高単価リクエストは限られた頻度で呼び、安価な処理で先にスクリーニングして本命に回す、という構造です。Claude のエージェント設計でも同じ経済合理性が働きます。
第3層: 参照ハンドル渡し(中身は渡さず、ID だけ返す)
データそのものを文脈に積む必要がない場面では、ツール返り値を「保存しました。ID は xxxx です」だけにして、Claude が必要なときに別ツールで参照する設計が最強です。
const blobStore = new Map<string, unknown>();
async function executeQuery(sql: string) {
const rows = await db.query(sql);
const handle = `query-${crypto.randomUUID()}`;
blobStore.set(handle, rows);
// 数千行を返さず、ハンドルとサンプル3件・行数だけ返す
return {
handle,
row_count: rows.length,
sample: rows.slice(0, 3),
columns: rows[0] ? Object.keys(rows[0]) : [],
note: "use `query_result_aggregate` or `query_result_filter` with the handle to drill down",
};
}
async function queryResultAggregate(handle: string, groupBy: string, agg: string) {
const rows = (blobStore.get(handle) ?? []) as Record<string, unknown>[];
// group by + 集計を実行してから返す
return aggregate(rows, groupBy, agg);
}参照ハンドルパターンは、Claude を「データを直接見るのではなく、集計指示を出すアナリストとして使う」発想です。本番運用では blobStore を Cloudflare KV や Durable Objects に置き換え、TTL を 1 時間程度に設定すると安定します。