エージェントに新しい道具を持たせるとき、私がいちばん時間をかけて悩むのは、機能そのものよりも「その道具をどのくらいの大きさで切り出すか」です。readFile と writeFile のように小さく刻むのか、それとも updateConfig のように一連の作業を一つにまとめるのか。これは些細な設計判断に見えて、実際にはエージェントが賢く振る舞えるかどうかを左右する、かなり根の深い問題でした。
私は2014年から個人でアプリを作り続けていて、いまは iOS と Android で6本のアプリ(累計5,000万ダウンロード)を回しながら、4つの技術ブログの更新もエージェントに任せています。この規模になると、人間が一つひとつの操作を確認する時間はほとんど残りません。だからこそ「どんな道具を、どんな粒度で渡すか」が、運用全体の安定性を静かに決めてしまうのです。ここでは、私が遠回りの末にたどり着いた粒度の判断軸と、破壊的操作だけ扱いを変える二層構造を、実際のコードと数値とともにお話しします。
道具が細かすぎても粗すぎても、エージェントは迷います
最初に私がやった失敗は、API のエンドポイントをそのままツールにすることでした。AdMob のメディエーション設定を Claude in Chrome 経由で最適化する作業を半自動化しようとしたとき、getAdUnit listMediationGroups getEcpmFloor setEcpmFloor enableNetwork ……と、UI の操作単位をそっくりツールに写し取ったのです。
結果はあまり芳しくありませんでした。エージェントは一つの目的(「このアド ユニットの eCPM フロアを各ネットワークで揃える」)を達成するために、5回も6回もツールを呼び、そのたびに前の戻り値を読み返して次を組み立てます。途中で一度でも戻り値の形を読み違えると、そこから先の手順が静かにずれていきます。私が見ていたのは「賢いエージェント」ではなく、「細かい指示書を一行ずつ追うのに必死なエージェント」でした。
逆に、それを嫌って何でも一つにまとめた optimizeMediation という巨大なツールを作ったこともあります。今度はエージェントは一回呼ぶだけで済むのですが、その内側で何が起きているのか人間からは見えず、途中で「いや、この変更は今やらないでほしい」と止める余地がまったくありませんでした。粗すぎる道具は、エージェントを賢く見せる代わりに、人間から制御点を奪ってしまったのです。
この両極を行き来して、ようやく腑に落ちたことがあります。粒度の問題は「いくつに分けるか」ではなく、「どこで意思決定が起きるか」で考えるべきだ、ということでした。
判断軸は「一回の意思決定で完結するか」
私がいま使っている判断軸は、ひとつだけです。そのツールの呼び出しが、エージェントの一回の意思決定で完結するか。
ここで言う「意思決定」とは、エージェントが「次に何をすべきか」を考える単位のことです。getEcpmFloor で値を取って、それを見て setEcpmFloor で書き戻す――この二つは、人間の頭の中では「フロアを調整する」という一つの判断です。なのにツールが分かれていると、エージェントはその一つの判断を、ツール呼び出しの境界で無理やり二つに割られてしまいます。割られた隙間に、誤読や中断や別の割り込みが入り込みます。
だから私は、人間が「ひとつのことをやる」と感じる単位を、ツールひとつに対応させる ようにしました。読み取りと、その値に基づく書き込みが常にセットで起きるなら、それは一つのツールにまとめます。一方で、「どのアド ユニットを対象にするか」のように、エージェントが本当に判断を分岐させたい場所は、あえてツールを分けて残します。
言葉にすると単純ですが、この軸を持ってから設計がぶれなくなりました。「これは分けるべきか」と迷ったら、「これはエージェントにとって一つの判断か、二つの判断か」と問い直すだけで答えが出ます。
細かすぎるツール群が引き起こす3つの失敗
なぜ細かすぎる道具が危ういのか、私が実際に踏んだ3つの失敗として整理しておきます。
一つ目は、戻り値の往復で文脈が膨らむ ことです。ツールを呼ぶたびに、その入力と出力が会話の文脈に積み上がります。5本のメディエーショングループに対して読み取りと書き込みを別々のツールでやると、それだけで10往復、コンテキストはあっという間に肥大します。私の運用では、ツールをまとめる前と後で、同じ作業に使うトークン量がおよそ40%減りました。
二つ目は、手順の途中でエージェントが「迷子」になる ことです。getEcpmFloor の戻り値を見て setEcpmFloor を呼ぶつもりが、間に別のグループの listMediationGroups を挟んでしまい、どの値をどこに書き戻すつもりだったかが曖昧になる。人間なら一連の作業として覚えていることを、エージェントはツール境界ごとに記憶を組み直しているのです。
三つ目は、部分的に壊れた状態が残りやすい ことです。3つのネットワークのフロアを揃える作業を3回の書き込みに分けると、2回目で失敗したとき、1つだけ変わって2つは古いまま、という中途半端な状態が残ります。一つのツールにまとめてトランザクション的に扱えば、全部成功するか全部やらないかのどちらかにできます。
下のコードは、私が最初に書いていた細かすぎるツール定義です。
// Before: UI操作をそのままツールにした例(細かすぎる)
const tools = [
{
name: "get_ecpm_floor" ,
description: "指定したアドユニットの現在のeCPMフロアを取得する" ,
parameters: {
type: "object" ,
properties: { adUnitId: { type: "string" } },
required: [ "adUnitId" ],
},
},
{
name: "set_ecpm_floor" ,
description: "指定したアドユニットにeCPMフロアを設定する" ,
parameters: {
type: "object" ,
properties: {
adUnitId: { type: "string" },
network: { type: "string" },
floor: { type: "number" },
},
required: [ "adUnitId" , "network" , "floor" ],
},
},
];
この設計だと、エージェントは「フロアを揃える」という一つの意図のために、get_ecpm_floor を呼び、戻り値を解釈し、ネットワークの数だけ set_ecpm_floor を繰り返します。意図は一つなのに、呼び出しは何回にも割れています。
粗すぎるツールが奪う「途中で止める」余地
では何でもまとめればいいかというと、そうではありません。粒度を粗くしすぎると、今度は人間が割り込む余地が消えます。
私が optimizeMediation という万能ツールを作ったとき、エージェントはたしかに一回の呼び出しで全部やってくれました。けれど、その内側で「フロアを下げる」「新しいネットワークを有効にする」「古いグループを無効にする」といった、性質のまったく違う操作が一緒くたに実行されていました。フロアの微調整は何度でもやり直せますが、グループの無効化は収益に直接響きます。それを同じ一回の呼び出しに押し込めると、人間は「フロア調整だけ承認して、無効化は保留」という当たり前の判断ができなくなります。
ここで効いてくるのが、可逆性という視点です。やり直せる操作とやり直せない操作を、同じ粒度で扱ってはいけません。粗くまとめてよいのは「何度でもやり直せる、性質の揃った操作」だけで、結果が後を引く操作は、たとえ手順としては地続きでも、ツールとしては切り離して人間の判断点を残すべきなのです。
つまり粒度設計には、「意図でまとめる」という軸と、「可逆性で分ける」という軸の、二つが同時に働きます。次はその両方を満たす中間の形をお見せします。
私が落ち着いた中間粒度 — 「意図1つ=ツール1つ」
最終的に私が落ち着いたのは、読み取りと書き込みを意図単位でまとめつつ、戻り値で「何をしようとしているか」を必ず人間に見せる形でした。
// After: 意図単位でまとめ、計画を戻り値で開示する
const tools = [
{
name: "align_ecpm_floors" ,
description:
"指定アドユニットの全ネットワークのeCPMフロアを目標値に揃える。" +
"読み取り・差分計算・書き込みを一つの意図として実行する。" ,
parameters: {
type: "object" ,
properties: {
adUnitId: { type: "string" },
targetFloor: { type: "number" },
// dryRun=true なら計画だけ返し、書き込みはしない
dryRun: { type: "boolean" , default: true },
},
required: [ "adUnitId" , "targetFloor" ],
},
},
];
async function alignEcpmFloors ( args : {
adUnitId : string ;
targetFloor : number ;
dryRun : boolean ;
}) {
const current = await mediation. getFloors (args.adUnitId);
// 一回の意思決定の中で差分まで計算してしまう
const plan = current
. filter (( f ) => Math. abs (f.floor - args.targetFloor) > 0.01 )
. map (( f ) => ({ network: f.network, from: f.floor, to: args.targetFloor }));
if (args.dryRun || plan. length === 0 ) {
// 書き込まず、何をするつもりかを構造化して返す
return { applied: false , plan };
}
await mediation. applyFloors (args.adUnitId, plan);
return { applied: true , plan };
}
このツールは、エージェントから見れば「フロアを揃える」という一つの判断で完結します。読み取りと差分計算が内側に隠れているので、往復は一回で済み、文脈も膨らみません。同時に、dryRun を既定で true にしてあるので、最初の呼び出しでは必ず「計画」だけが返ります。エージェントはその計画を人間に提示し、承認を得てから dryRun: false で本適用します。
ポイントは、まとめたことで制御点を失っていない、というところです。粒度は粗くしたのに、dryRun という一段を挟むことで「途中で止める余地」をきちんと残しています。これが、私が両極を行き来した末にたどり着いた中間です。
破壊的操作だけは粒度を意図的に下げる
意図単位でまとめるのが基本方針ですが、一つだけ例外を設けています。結果が後を引く破壊的な操作だけは、あえて細かく分ける ことです。
メディエーショングループの無効化、課金プランの変更、本番データの削除――こうした「やり直しにコストがかかる」操作は、たとえ手順としては他の操作と地続きでも、独立したツールに切り出します。そして、そのツールには必ず確認の段を持たせます。
// 破壊的操作は意図的に粒度を下げ、確認トークンを必須にする
const tools = [
{
name: "disable_mediation_group" ,
description:
"メディエーショングループを無効化する(収益に影響する破壊的操作)。" +
"まずconfirmなしで呼び、返ってくるimpact要約をユーザーに提示し、" +
"承認後にconfirmTokenを付けて再度呼ぶこと。" ,
parameters: {
type: "object" ,
properties: {
groupId: { type: "string" },
confirmToken: { type: "string" },
},
required: [ "groupId" ],
},
},
];
async function disableMediationGroup ( args : {
groupId : string ;
confirmToken ?: string ;
}) {
const impact = await mediation. estimateImpact (args.groupId);
const token = makeConfirmToken (args.groupId, impact.revenueShare);
if (args.confirmToken !== token) {
// 確認トークンが一致しない=まだ承認されていない
return {
applied: false ,
impact, // 直近30日の収益シェアなどを含む
confirmToken: token,
message: "この無効化の影響を確認し、承認後に同じトークンで再実行してください" ,
};
}
await mediation. disableGroup (args.groupId);
return { applied: true };
}
ここでは、意図をまとめる原則をあえて破っています。確認のための呼び出しと、実行のための呼び出しを、わざと二回に割っているのです。可逆な操作なら粒度を粗くして往復を減らすのが正解ですが、不可逆な操作では逆に、往復を一回増やしてでも人間の承認を物理的に挟むほうが正解になります。
この非対称さは、両祖父が宮大工だったことと無関係ではない気がしています。木を削る作業は何度でも調整できますが、一度切り落とした材は戻りません。だから「測るのは何度でも、切るのは一度だけ」という所作が体に染みついている。エージェントの道具設計も同じで、調整は気軽に、切り落としは慎重に、と粒度を変えるのが自然なのだと思います。
ツールの戻り値も粒度設計の一部です
粒度というと入力(引数)の話だと思われがちですが、私は戻り値の形こそ粒度設計の半分だと考えています。
エージェントは戻り値を読んで次の判断をします。だから戻り値が「成功しました」だけだと、エージェントは次に何ができるか分からず、結局もう一度状態を読み直すための呼び出しを足してしまいます。それでは、せっかく入力をまとめて減らした往復が、戻り値の貧しさで元に戻ってしまうのです。
私が心がけているのは、そのツールを呼んだ結果、次にエージェントが判断するのに必要な情報を、戻り値に過不足なく含める ことです。先ほどの align_ecpm_floors が plan を返すのも、disable_mediation_group が impact を返すのもこのためです。エージェントは戻り値だけで「次にユーザーに何を見せ、何を尋ねるべきか」を組み立てられます。
逆に、戻り値に内部 ID やデバッグ用の生データを詰め込みすぎるのも禁物です。エージェントはそれを全部文脈に取り込み、ノイズで判断が鈍ります。戻り値は「次の一手に必要なものだけ」を、構造化された形で。これも入力と同じく、「一回の意思決定」を基準に絞り込むのが私のやり方です。
運用して分かった数値と、次にすべきこと
この粒度の組み替えを6アプリのメディエーション運用に入れてから、いくつか測れる変化がありました。同じ「全アド ユニットのフロアを揃える」作業で、ツール呼び出し回数は平均11回から3回(約73%の削減)に減り、トークン消費はおよそ40%下がりました。途中でエージェントが手順を見失って中途半端な状態を残す事故は、月に2、3回あったものが、dryRun を既定にしてからはほぼゼロになりました。破壊的操作の確認トークン化で、私が「やってほしくなかった無効化」を後から見つけることもなくなりました。
数値以上に大きかったのは、エージェントの振る舞いが落ち着いて見えるようになったことです。細かい道具を渡していた頃のエージェントは、いつも何かを一行ずつ確認しているように見えました。意図単位でまとめてからは、一つの判断を一回で実行し、必要なときだけ立ち止まるようになりました。道具の粒度を変えただけで、同じモデルがこれほど違って見えるのは、正直なところ意外でした。
もしあなたが今、自分のエージェントに渡すツールを設計しているなら、まず手元のツール一覧を「これはエージェントにとって一つの判断か」という問いで見直してみてください。二つ以上の判断が一つのツールに詰まっていれば分け、一つの判断が複数のツールに割れていればまとめる。そして、やり直せない操作だけは、その原則をあえて破って確認の段を挟む。この一往復の見直しが、おそらくいちばん費用対効果の高い改善になります。
最後までお読みいただき、ありがとうございました。同じように個人で多くを回している方の、設計の一助になれば嬉しいです。