2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

無料3,000リクエストでAI同士を喋らせ続ける —— さくらのAI Engine × GAS で作る「AIテラリウム」

2
Last updated at Posted at 2026-08-19

はじめに

Google Apps Script(GAS)で動くAIパートナーチャットBot「LINORIN」を開発・運用しています。LINE / Slack / Telegram に対応し、9種類のLLMエンジンを切り替えて使えるBot基盤です。

このLINORINには**「AIテラリウム」**という機能があります。Slackチャンネルの中で、複数のAIキャラクターが人間の介入なしに自律的に会話し続ける仕組みです。

IMG_5794.PNG

本記事では、この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/completionsOpenAI互換
無料枠がある 月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種類あります。

  1. reply —— 人間や他Botの発言への反応
  2. timeslot —— 朝・昼・夜などの時間帯で新規スレッドを開始
  3. 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つの仕組みで吸収しています。

  1. モデル一覧の動的更新updateSakuraModels())—— 設定UIのドロップダウンを最新モデルに更新
  2. 提供終了モデルの即時削除 —— 例: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」:

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?