はじめに
Google Apps Script(GAS)で動くAIパートナーチャットBot「LINORIN」を開発・運用しています。LINE / Slack / Telegram に対応し、9種類のLLMエンジンを切り替えて使えるBot基盤です。
このLINORINには**「AIテラリウム」**という機能があります。Slackチャンネルの中で、複数のAIキャラクターが人間の介入なしに自律的に会話し続ける仕組みです。
本記事では、このAIテラリウムのLLMバックエンドとして**「さくらのAI Engine」**を使い、無料枠3,000リクエスト/月を「制御された使い倒し」で消費する設計を紹介します。
「3,000リクエストでどこまでアプリを作れるか試してみた」歓迎、とのことなので、本記事はその実践記です。AI同士の会話はリクエストが自己増殖するため、ガンガン使いつつも枠を溶かさない設計が肝になります。
さくらのAI Engine を選んだ理由
AIテラリウムのような機能に求める条件と、さくらのAI Engine の特徴がきれいに一致しました。
| 求める条件 | さくらのAI Engine |
|---|---|
| OpenAI互換(既存実設計を使い回せる) |
https://api.ai.sakura.ad.jp/v1/chat/completions は OpenAI互換
|
| 無料枠がある | 月3,000リクエスト無料 |
| 複数モデルを使い比べたい |
gpt-oss-120b / llm-jp-3.1-8x13b / preview/Kimi-K2.7-Code / preview/Qwen3.6-35B-A3B / preview/gemma-4-31b-it 等 |
| 「トークン」ではなく回数で管理したい | 無償枠はリクエスト数ベース(コントロールパネルで残数確認可) |
AIテラリウムとは
Slackの専用チャンネルに、ゲストAI Bot(各自トークンを持つ)と人間が同居します。
人間: 今日なんか疲れたなぁ
Bot A(Kimi系): お疲れ様。そんな日は雑談に付き合う係だよ
Bot B(Qwen系): 疲労の種類にもよる。身体的?精神的?
Bot A: なんかあった?
Bot C(gemma系): (突然)そういえば昔「疲れた」の語源で盛り上がったことが…
発火トリガーは3種類あります。
- reply —— 人間や他Botの発言への反応
- timeslot —— 朝・昼・夜などの時間帯で新規スレッドを開始
- spontaneous —— 沈黙が続いたときの自発的な発言
ホストBot(LINORIN本体)はトリガー管理だけを行い、発言自体は各ゲストBotのSlackトークンで投稿されます。
アーキテクチャ(GAS縛りの設計)
GASは6分/実行の制約があるため、すべてキュー駆動で設計しています。
scheduledEveryMinute() [1分トリガー]
└─ _terrariumScheduledCheck()
├─ _pollTerrariumChannel() ← Slack API ポーリング
└─ _maybeEnqueueSpontaneous() ← 自発発話の抽選
terrarium_queue シート [status=queued]
└─ _processTerrariumQueue()
├─ type=reply → 応答生成
├─ type=timeslot → 時間帯スレッド
└─ type=spontaneous → 自発スレッド
└─ _callTerrariumBotLLM(bot) → さくらのAI Engine
データはすべてスプレッドシート(terrarium_queue / terrarium_logs / terrarium_threads)。外部DB不要で、GAS単体で完結します。
実装①:さくらのAI Engine 呼び出し(OpenAI互換)
LINORINは9エンジン(Gemini / ChatGPT / Claude / Grok / GLM / DeepSeek / OpenRouter / ローカル / さくら)を同一インターフェースで扱います。さくらのAI Engine の実装は実質これだけです。
function callSakura(message, model, imageBase64, userId, temperature) {
try {
const apiKey = CONFIG["SAKURA_ACCESS_TOKEN"] || CONFIG["SAKURA_API_KEY"] || "";
const useModel = model || CONFIG["SAKURA_MODEL"];
if (!apiKey) return { error: true, type: "system", status: "NO_KEY" };
if (!useModel) return { error: true, type: "system", status: "NO_MODEL" };
const payload = {
model: useModel,
messages: [
{ role: "system", content: message.system },
{ role: "user", content: message.user }
],
max_tokens: _getMaxTokens_(),
temperature: temperature || 0.7,
stream: false
};
const res = UrlFetchApp.fetch(
"https://api.ai.sakura.ad.jp/v1/chat/completions",
{
method: "post",
contentType: "application/json",
headers: {
"Authorization": "Bearer " + apiKey,
"Accept": "application/json"
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
}
);
const status = res.getResponseCode();
const json = JSON.parse(res.getContentText() || "{}");
if (json.error) {
Logger.log("Sakura API error [" + status + "]: " + JSON.stringify(json.error));
return { error: true, type: "api", status, apiMessage: json.error.message || "" };
}
const text = json.choices?.[0]?.message?.content || "";
return { error: false, text, model: useModel };
} catch (e) {
Logger.log("Sakura system error: " + e);
return { error: true, type: "system" };
}
}
OpenAI互換なので messages / temperature / max_tokens をそのまま渡せます。既存のOpenAIクライアント実装からの移植は、エンドポイントとキーを変えるだけで済みました。
実装②:Botごとに別モデルを割り当てる(使い倒しの核心)
テラリウムのゲストBotは、設定でエンジンとモデルをBotごとに指定できます。
terrarium_bot_{name,inst,emoji,engine,model,token}_01..05
これを受けて、BotごとにLLMをディスパッチします。
function _callTerrariumBotLLM(bot, systemPrompt) {
const engine = (bot.engine || "").toLowerCase();
const model = bot.model || "";
const temp = (typeof _computeFinalTemperature_ === "function")
? _computeFinalTemperature_() : 0.9;
const merged = { system: systemPrompt, user: "次の発言をしてください。" };
// Botごとの指定 → 未指定なら SUB_LLM_ENGINE / LLM_ENGINE にフォールバック
const targetEngine = engine || (
(_getConf()["SUB_LLM_ENGINE"] || _getConf()["LLM_ENGINE"] || "gemini")
).toLowerCase();
const c = _getConf();
if (targetEngine === "sakura" && typeof callSakura === "function")
return callSakura(merged, model || c["SAKURA_MODEL"], null, null, temp);
if (targetEngine === "gemini" && typeof callGemini === "function")
return callGemini(merged, model || c["GEMINI_MODEL"], null, null, temp);
// ... 他エンジン同様
return (typeof callSubLLM === "function")
? callSubLLM(systemPrompt, TERRARIUM_MAX_TOKENS)
: { error: true, text: "" };
}
ポイントは「Bot AはKimi、Bot BはQwen、Bot Cはgemma」という混成構成ができることです。さくらのAI Engine の複数モデルを並列に晒すことで、
- モデルごとのキャラの立ちはっきりした違い(会話が単調にならない)
- 無償枠3,000リクエストを多様なモデルに分散して消費できる
という効果が得られます。実際、同じプロンプトでもKimi系とgemma系とでは口調も反応の切れ方も別物になり、モデルの個性がそのままキャラの個性になります。
実装③:リクエストを自己増殖させない「連鎖確率」
AI同士の会話で一番危険なのは無限対話ループです。Bot Aの発言にBot Bが反応し、それにBot Aが反応し……で、あっという間に無料枠が溶けます。
テラリウムでは連鎖の深さごとに発言確率を落とすことで、会話を自然に収束させます。
const TERRARIUM_STREAK_PROB = [1.0, 1.0, 0.95, 0.85, 0.7, 0.5, 0.3];
- 1〜2発言目:必ず反応(会話が始まる)
- 3発言目:95% … 以下漸減
- 7発言目以降:30%で自然沈黙
人間の会話がそうであるように、盛り上がった話題は長く続き、そうでなければ早く沈黙する。これで「使い倒す」と「溶かす」のバランスを取っています。
さらに保険を2つ:
const TERRARIUM_MAX_TOKENS = 300; // 1発言のトークン上限
// 時間バケット制限(CacheService)
const TERRARIUM_HOURLY_BUCKET_KEY = "_terrarium_hourly_bucket_";
const TERRARIUM_HOURLY_COUNT_KEY = "_terrarium_hourly_count_";
1時間あたりの投稿数に上限を設け、夜間に暴走しても朝起きたら枠が枯渇していた、を防止します。
実装④:会話が単調にならない工夫
Botに渡すシステムプロンプトには、こういう「スパイス」を入れています。
function _getSpice() {
return "時々予想外の発言をして会話に意外な展開をもたらしてよい。突飛な連想や脱線も歓迎。人間が発言したら特に強く反応せよ。";
}
温度(temperature)を0.9と高めに設定しているのも同じ理由です。自律会話のエンタメ性を担保するには、多少の脱線を許容する方が面白くなります。
モデル変遷への追従
さくらのAI Engine は提供モデルの入れ替わりが定期的に発生しています。LINORINではこの追従を2つの仕組みで吸収しています。
-
モデル一覧の動的更新(
updateSakuraModels())—— 設定UIのドロップダウンを最新モデルに更新 -
提供終了モデルの即時削除 —— 例:
Qwen3-Coder-480B-A35B-Instruct-FP8は2026-07-21で提供終了 → リリース履歴に残して即削除(preview/Kimi-K2.7-Code等を追加)
実際の変遷:Kimi-K2.5 → K2.6 → K2.7-Code、Qwen3-Coder系の終了とQwen3.6系への置き換え。API互換性が保たれているため、モデル差し替えは設定値の変更だけで済みました。これもOpenAI互換の恩恵です。
運用してみて
- リクエスト消費の実感:テラリウムを有効にしていると、対話の連鎖が予想以上に伸びることがあり、3,000リクエストが「無限にある」感覚ではなく「設計して消費する資源」として認識されるようになりました。連鎖確率の導入前は、1日で想定の数倍が消えることもありました(これが本設計の直接の動機です)
- モデルの個性:モデル選択がキャラ設計の一部になっています
- llm-jp-3.1 の存在意義:国産モデルを会話に混ぜられるのはこの基盤ならではです
まとめ
| テーマ | 設計 |
|---|---|
| 無料枠の「制御された使い倒し」 | 連鎖確率による収束 + 時間バケット制限 + max_tokens 300 |
| 複数モデルの並列利用 | Per-bot の engine/model 指定で混成構成 |
| モデル入れ替わりへの追従 | OpenAI互換により設定値変更のみで対応 |
| 実行基盤 | GAS(スプレッドシート駆動、外部DB不要) |
「AI同士を喋らせる」は、リクエストが自己増殖する最も贅沢なAIの使い方だと思います。さくらのAI Engine は「超過しても自動課金されない」ため、この種の実験に最適でした。3,000リクエストを「消費を恐れず、しかし設計して使う」—— そのお供に、ぜひ。
サンプル実装はAIチャットBot「LINORIN」で公開しています:
自分から話しかけてくれるAIチャットBot基盤「LINORIN」:
- 完全版: https://note.com/nou_yakareta/m/mb0c5401f132f
- フリー版(GitHub): https://github.com/smalltomatowater-boop/LINORIN-free
※フリー版にはテラリウム機能を実装していません - 公式: https://www.linorin.jp
- 完全版: https://note.com/nou_yakareta/m/mb0c5401f132f
- フリー版(GitHub): https://github.com/smalltomatowater-boop/LINORIN-free
※フリー版にはテラリウム機能を実装していません - 公式: https://www.linorin.jp
