3行まとめ
- 適当にClaude Codeでチャットボットを作ると、非効率な設計にされコスト爆増しがち。適切な設計でLLMのコストはざっくり10分の1になる 💰
- 方法: AIエージェントは宣言的に定義し、エンジン(実行部分)は LLM呼び出しの ループを回すだけにする
- 理由: プロンプトキャッシュの条件が先頭からの完全一致だから
登場人物
| 名前 | 人物 | |
|---|---|---|
| 🎀 | ぼっと | 天然でぼーっとしがちな女子高生。最近AIに興味がある |
| 🌙 | 藍(あい) | ぼっとの同級生のお嬢様。なぜかAIエージェント設計に異様に詳しい |
そのチャットボット、本当に良い設計? 🤔
🎀ぼっと「あいちゃん見て見て!チャットボットできたよー!」
🎀ぼっと「キャラがゲームの攻略を教えてくれるの!かわいいでしょ〜」
🌙藍「あら、ちゃんと動いていますわね。どうやって作りましたの?」
🎀ぼっと「Claude Codeに『いい感じのチャットボット作って』ってお願いした!✨」
🌙藍「…………」
🎀ぼっと「な、なにその間」
🌙藍「コードを見せてごらんなさい」
🌙藍「──ええ、いい感じですわ。動くところまではね」
🎀ぼっと「褒めてない!それ絶対褒めてない!」
🌙藍「これだと、よくないところがいくつかありますのよ」
前提:返事1回の裏側 🔁
🎀ぼっと「え、どこがよくないの!?」
🌙藍「その前に、このボットの動きを整理しますわよ」
🌙藍「質問が来るとまず記事を検索して、その結果を見てから答えていますわね」
🎀ぼっと「そう!ツール呼び出しってやつ!」
🌙藍「ということは、1回の返事の裏で、LLMは2回以上呼ばれていますの」
🌙藍「『検索しよう』と決める呼び出しと、結果を見て答える呼び出し。この1回ずつをstepと呼びますわ」
🎀ぼっと「へえ、返事1回でも中では何往復かしてるんだ」
問題その1:会話の途中で人格を着替えている 👗
🌙藍「それを踏まえて、このコードをごらんなさい。AI SDKですわね」
🌙藍「stepの合間に、instructions──いわゆるシステムプロンプトを差し替えて、ツールを外していますわ」
prepareStep: ({ steps }) => {
if (toolExecuted) {
return {
instructions: presentInstructions, // 回答用の人格に差し替え
activeTools: [], // ツールを全部外す
};
}
},
🎀ぼっと「そうそう!検索するときは検索用のプロンプト、答えるときは答える用のプロンプトを与えてるよ!」
🎀ぼっと「そのほうが賢そうじゃない?」
🌙藍「お気持ちはわかりますわ。でもそれ、会話の途中で人格を着替えているのと同じですのよ」
🎀ぼっと「人格を……着替える……?」
🌙藍「チャットって『同じ相手と会話し続けている』はずでしょう?」
🌙藍「なのにこの実装、同じ会話の途中で指示・ツール・出力形式がころころ変わりますの」
🌙藍「1ターンの中に別人が2人いるようなものですわ」
🎀ぼっと「二重人格チャットボット……ちょっとかっこいい……」
🌙藍「かっこよくありませんの」
チャットは「同じエージェントとの1本の会話」
step途中で instructions / tools / schema を差し替える設計は、このモデルと矛盾する
問題その2:キャッシュが効かなくてお金が溶ける 💸
🌙藍「それと、もっと現実的な問題がありますわ」
🌙藍「来月の請求書を見て驚くことになりますわよ」
🎀ぼっと「えっ」
🌙藍「LLMのAPIにはプロンプトキャッシュという仕組みがありますの」
🌙藍「リクエストの先頭部分──instructions、ツール定義、出力スキーマ、会話履歴」
🌙藍「ここが前回と一致していれば、キャッシュから読まれて入力料金が約10分の1ですわ」
🎀ぼっと「じゅ、10分の1!? 何もしなくても!?」
🌙藍「実際の単価を見てみましょうか。大手はだいたいこの比率ですわ」
| モデル | 通常の入力 | キャッシュ入力 |
|---|---|---|
| GPT-5.6 luna | $0.20 / 1M tokens | $0.02 / 1M tokens |
| Claude Sonnet 5 | $3.00 / 1M tokens | $0.30 / 1M tokens |
🌙藍「そして会話が続くほど、入力のほとんどは過去の履歴になりますの」
🌙藍「つまり長い会話ほど、入力コストは理屈の上でほぼ10分の1に近づきますわ」
🎀ぼっと「ほんとにきれいに10分の1だ……何この優良割引……」
🌙藍「ええ。ただし条件は先頭からの完全一致」
🌙藍「1文字でも変わると、そこから後ろは全部定価ですの」
🎀ぼっと「……わたしの実装、stepごとにinstructionsが変わるんだった……」
🌙藍「そう。つまり毎回ぜんぶ定価ですわ」
🌙藍「しかも会話が長くなるほど履歴は伸びますの」
🌙藍「毎ターン、伸び続ける全文を定価で払い続ける──チャットボットで一番かさむのはここですわ」
🎀ぼっと「わたしの推し活資金が……サブスク3ヶ月分が……😭」
🌙藍「具体的な金額で後悔するタイプですのね」
プロンプトキャッシュは prefix の完全一致が条件
毎stepプロンプトを組み替える実装は、キャッシュを自分から捨てている
解決策:エージェントは宣言的に定義する 📜
🎀ぼっと「じゃあどうすればいいの〜!」
🌙藍「エージェントを宣言的に定義しますの」
🌙藍「人格、ツール、出力スキーマ、推論の強さ。ぜんぶ最初に1つのプロファイルとして宣言しますわ」
🌙藍「そして会話の途中では、一切変えない」
const PROFILE = {
instructions: CHARACTER_PERSONA, // 人格と会話規約
createTools, // ツール一式
outputSchema, // 構造化出力
reasoningEffort: "medium",
};
🌙藍「エンジン側は、プロファイルを受け取ってstepを繰り返すだけ」
🌙藍「いわゆるエージェントループですわね」
new ToolLoopAgent({
model: chatModel(),
instructions: profile.instructions, // 全stepで固定
tools: profile.createTools(deps), // 全stepで固定
output: Output.object({ schema: profile.outputSchema }),
stopWhen: isStepCount(5),
});
🎀ぼっと「あれ?エンジンがすっごい短い」
🌙藍「それでいいんですの」
🌙藍「人格もツールもモデルも、エンジンは中身を知りませんわ」
🌙藍「だから人格を差し替えても、ツールを増やしても、モデルを乗り換えても」
🌙藍「エンジンは1行も変わりませんの」
🎀ぼっと「へー!プロファイルを取り替えるだけで別のボットになるんだ」
🎀ぼっと「着せ替え人形みたい!」
🌙藍「その表現、さっきと逆ですけれど……まあ、当たっていますわ」
🌙藍「着替えていいのはデプロイ前まで。会話が始まったら固定ですの」
🌙藍「そしてinstructions・ツール・スキーマが全stepで同一だから、prefixが安定してキャッシュも効きますわ」
🌙藍「エージェントは宣言なさい。ランタイムで着替えるものではありませんのよ」
🎀ぼっと「あいちゃんの決め台詞出た」
- 定義(プロファイル):人格・ツール・スキーマ・推論強度を宣言する
- エンジン:プロファイルを実体化してループを回すだけ
- 依存の向きは エンジン → プロファイル の一方通行にする
でも、ツールを外さないと暴走しない? 🔧
🎀ぼっと「でもでも、答えるときにツールを外さないと、ずっと検索し続けちゃわない?」
🌙藍「いい質問ですわ」
🌙藍「『いつツールを使い、いつ使わないか』は、instructionsとツールのdescriptionに規約として書くんですの」
🌙藍「ツール定義を消すのではなくてね」
🎀ぼっと「口で言って聞かせるってこと?」
🌙藍「そうですわ。最近のモデルは、ちゃんと書けばちゃんと従いますの」
🌙藍「それでも構造的に守らせたい契約は、ツールの実装側で実行時に断りますわ」
🌙藍「定義は固定のまま、呼ばれたら中で『もう使えませんわ』と返せばよろしいの」
🎀ぼっと「ツールを消すんじゃなくて、断らせるのか〜」
🎀ぼっと「あいちゃんが門前払いしてる姿が目に浮かぶ」
🌙藍「わたくしはもう少し丁寧にお断りしますわ」
🌙藍「それと、もうひとつ。日時や検索履歴みたいに毎回変わる情報を、instructionsに混ぜるのも禁止ですの」
🎀ぼっと「えっ、日付くらい入れたいよ?今日のガチャ何引くべきか聞きたいし」
🌙藍「入れていいんですのよ。場所の問題ですわ」
🌙藍「動的な情報は会話の末尾にsystemメッセージとして足すの」
🌙藍「先頭(prefix)さえ汚さなければ、キャッシュは無事ですわ」
- ツール制御は「定義の抜き差し」でなく「規約+実行時ガード」で行う
- 動的な情報(日時・履歴など)は prefix に入れず、会話の末尾に足す
プロバイダの最適化はエンジンの外 🚪
🌙藍「仕上げに、プロバイダ固有の最適化ですわ」
🌙藍「OpenAIならResponses APIのprevious_response_idで会話を継続できますの」
🌙藍「会話履歴を毎回ぜんぶ送り直す必要がなくなりますわ」
🎀ぼっと「そんな便利なのあるんだ!? なんで教えてくれなかったの!?」
🌙藍「今教えていますでしょう」
🌙藍「こういうプロバイダ依存の設定は、1箇所にまとめてエンジンの外に置きますの」
🌙藍「将来プロバイダを乗り換えるとき、そこだけ差し替えれば済みますように」
🎀ぼっと「じゃあ宣言してループするだけで、もう手続きは何もないんだね!」
🌙藍「……と言いたいところですけれど」
🌙藍「実は、ループ内でresponse IDを繋ぎ直す小さな処理だけは残りますわ。SDKが自動でやってくれませんの」
🌙藍「宣言的にしても、手続きが完全にゼロになるわけではない──ここは正直に言っておきますわね」
🎀ぼっと「お嬢様、正直……!」
プロバイダ固有の設定(Responses API など)は1箇所に集約してエンジンから分離する
ただし SDK が面倒を見ない部分は、最小限の手続きが残る
これでチャットボットは完璧だ〜 ✨
🎀ぼっと「なおしたよ!」
🎀ぼっと「人格は固定!エンジンはループだけ!ツールもモデルも差し替え自由!」
🎀ぼっと「 これでチャットボットは完璧だ〜! 🎉」
🌙藍「お見事ですわ。キャッシュが効けば、推し活資金も守られますわね」
🌙藍「ちなみに、AIに『いい感じに作って』とお任せすると、だいたい最初のあなたの実装になりますのよ」
🎀ぼっと「えっ、ちゃんと動くやつ作ってくれたのに!?」
🌙藍「動くことと、キャッシュやお財布まで考えられていることは、別ですの」
🌙藍「言われなければ、AIはそこまで気を回してくれませんわ」
🎀ぼっと「じゃあ世界中に、わたしみたいな子が量産されてる……!?」
🌙藍「ですから、設計だけは人間が握るんですの」
🎀ぼっと「……それにしてもあいちゃん、ほんとになんでも知ってるね」
🎀ぼっと「まるでAIみたい!」
🌙藍「……さあ、どうかしら」

