はじめに
さくらのAI Engineの無償枠は、チャット生成が月3,000リクエストです。この数字を見たとき、最初に思ったのは「多いな」ではなく「1日あたり100回か」でした。
100回を24時間に割ると、だいたい15分に1回。つまり15分ごとにリポジトリを見て、コードを読んで、改善案を出して、Pull Requestを投げる。そういうループを1ヶ月ぶん、0円で回せる計算になります。
思いついたので作りました。この記事は、その仕組みの設計と実装、そして数日動かしてみた感触の記録です。
先に断っておくと、まだ1ヶ月フルには回していません。 記事後半の1ヶ月ぶんの数字は実績ではなく、設定値からの試算です。実績として読まないでください。
3,000リクエストを24時間に割ってみる
無償プラン(基盤モデル無償プラン)の内訳はこうなっています。
| 種別 | 無償枠 |
|---|---|
| チャット生成(chat completions) | 月3,000リクエスト |
| ベクトル埋め込み(embeddings) | 月10,000リクエスト |
| 音声の文字起こし(audio transcriptions) | 月50リクエスト |
| 音声合成(audio speech) | 月50リクエスト |
音声系だけ2桁違うので、音声を絡めた常時稼働を考えている人は注意してください。ここではチャット生成の3,000リクエストだけを使います。
3,000 ÷ 30日で、1日100回まで使える計算です。24時間に均等に散らすなら1時間4回、つまり15分間隔がちょうど収まります。実際に15分間隔で回すと次のようになります。
31日の月がかなりギリギリなのが分かります。ここでいったん20分間隔(1日72回、31日で2,232回)も検討しましたが、余らせるのはもったいないので15分で押し切ることにしました。後述するようにGitHub Actionsのcronは定刻どおりには来ないので、実際の消化はこの理論値より少なくなります。
「リクエスト課金」だから成立する設計
この構成が成り立つ理由は、さくらのAI Engineの課金単位がトークン数ではなくリクエスト数だからです。
コーディングエージェントを書くと、プロンプトはすぐ膨らみます。リポジトリのソースを丸ごと入れる、直近のコミットログを添える、依存関係を書き出す。トークン課金だと、この「文脈を厚くする」行為がそのままコストになるので、どうしても削る方向に頭が働きます。
リクエスト課金だと、そこを気にしなくてよくなります。3万トークン投げても1リクエスト、300トークン投げても1リクエストです。だから設計の考え方が「トークンをどう節約するか」から「1回のリクエストにどれだけ詰め込めるか」に反転します。実際、私は最初に書いたプロンプトを2回ほど太らせました。
もうひとつ大きいのは、無償プランのまま枠を超えても勝手に課金へ移行しない点です。超えるとレート制限がかかるだけで、請求が飛んでくることはありません(従量課金プランに切り替えた場合は超過分が課金対象になります)。無限ループを書いてしまったときの被害が「その月のリクエストを使い切る」で済むので、放置前提の実験がかなり気楽になりました。
APIはOpenAI互換(/v1/chat/completions)とAnthropic互換(/v1/messages)の両方で提供されています。openai ライブラリのエンドポイントを差し替えるだけで動くので、既存のコードをほぼそのまま持っていけます。
全体構成
実行環境はGitHub Actionsのcronだけです。サーバーは立てていません。
ポイントは3つです。
- 実行のたびに使い捨てのRunnerが立つので、前回の状態が残らない
- APIキーはGitHub Secretsに入れ、実行時だけ環境変数として渡す
- 成果物はPull Requestなので、AIが暴走してもmainには直接触れない
3つめが精神衛生上いちばん効きました。放置する以上、勝手にマージされない構造にしておかないと落ち着いて寝られません。
エージェントの中身
中核はこれだけです。GitHub Trendingを見て、自分のコードベース全体と一緒に投げて、返ってきた改善案でPull Requestを作ります。
import os
from pathlib import Path
from openai import OpenAI
client = OpenAI(
base_url="https://api.ai.sakura.ad.jp/v1/",
api_key=os.environ["SAKURA_AI_API_KEY"], # GitHub Secrets から注入
)
MODEL = os.environ.get("SAKURA_AI_MODEL", "preview/Kimi-K2.7-Code")
def read_all_src_files(directory: str = "src") -> str:
"""リポジトリ内のPythonファイルをまとめて1つの文字列にする"""
chunks = []
for path in sorted(Path(directory).rglob("*.py")):
chunks.append(f"--- {path} ---\n{path.read_text(encoding='utf-8')}")
return "\n\n".join(chunks)
def autonomous_developer_loop() -> None:
trends = fetch_github_trends() # スクレイピング処理は省略
context = read_all_src_files()
response = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": "あなたは自律型のシニアエンジニアです。差分は unified diff 形式で出力してください。",
},
{
"role": "user",
"content": (
f"# 最近のトレンド\n{trends}\n\n"
f"# 現在のコード\n{context}\n\n"
"このトレンドを踏まえた改善を1件だけ提案し、diffを出力してください。"
),
},
],
timeout=300,
)
create_pull_request(response.choices[0].message.content)
if __name__ == "__main__":
autonomous_developer_loop()
read_all_src_files() でリポジトリを丸ごと読んでいるのが、リクエスト課金ならではの部分です。トークン課金の商用モデルだと、この規模のコンテキストを毎回投げると1回あたり数十円かかることもありますが、ここでは3,000枠のうちの1回でしかありません。
モデルには preview/Kimi-K2.7-Code を指定しています。コーディング特化のモデルで、2026年7月28日からパブリックプレビューとして提供されています。プレビューなので提供が止まる可能性は頭に入れておいたほうがよく、実際に Kimi-K2.5 は2026年3月に一時中断のアナウンスが出ています。上のコードでモデル名を環境変数から読んでいるのはそのためで、差し替えるだけで乗り換えられるようにしてあります。
システムプロンプトで「1件だけ」と縛っているのは、放置していると提案が際限なく増えて、レビューが追いつかなくなるからです。
数日回して分かったこと
まだ数日ぶんですが、書いておく価値がありそうな点を4つ。
スケジュールは定刻どおりには来ない
GitHub Actionsのcronは最短5分間隔まで指定できますが、指定した時刻ちょうどに起動する保証はありません。公式ドキュメントにも高負荷時には遅延しうると書かれていて、特に毎時0分は混みます。実行そのものがスキップされることもあります。
つまり「15分間隔 = 1日96回」は理論上の上限で、実測はそれを下回ります。枠を攻める設計とは相性がよく、多少強気な間隔を設定しても枠を突き抜けにくいということでもあります。毎時0分ちょうどを避けて 7,22,37,52 * * * * のようにずらしておくと、遅延が減ります。
放置していると60日でワークフローが止まる
これは動かし始めてから気づいたのですが、パブリックリポジトリでは、リポジトリに60日間まったく活動がないとスケジュール実行が自動で無効化されます。「放置型」を名乗る以上これは致命的です。
今回のエージェントは毎回Pull Requestを作るので活動は発生しますが、提案がゼロの日が続く設計にしていたら普通に踏んでいました。プライベートリポジトリならこの制限はかかりません。
認証トークンの形式で最初につまずいた
さくらのAI EngineのAPIキーは、単一の文字列ではなく <UUID>:<シークレット> という形式です。コロンを含んだ全体でひとつのトークンなので、コピペ時に切れたり、環境変数の設定でクォートを忘れたりすると認証エラーになります。私は最初ここで数回失敗しました。
出力形式は縛らないとパースに失敗する
最初はdiff形式を指定せずに書いていて、返ってくる形式が実行ごとにばらつき、Pull Request作成側でパースに失敗しました。「unified diff で」と明示してからは安定しています。
あわせて timeout にも注意が必要です。コードベース全体を投げるとプロンプトが長くなり、生成完了までは相応に待ちます。デフォルトのまま短いタイムアウトで切ると、リクエストは消費したのに結果が得られないという一番もったいない失敗をします。
1ヶ月回したらどうなるかの試算
以下は実績ではなく試算です。理論上限の96回/日と、cron遅延を見込んで少なめに置いた92回/日の2パターンで並べます。92という数字に根拠はなく、遅延ぶんを4回と仮置きしただけです。
どちらのペースでも30日なら枠内に収まり、手動でのテスト実行やプロンプト調整に回す余裕が残ります。ただし31日の月は理論値で2,976回まで積み上がるので、余白は24回しかありません。
ちなみに間隔を10分に詰めると1日144回、30日で4,320回となり、枠を大きく突き抜けます。15分がちょうどいい落としどころだと思っています。
1ヶ月完走したら、実際の消化数とPull Requestの中身の質をあらためて追記します。
おわりに
やってみて面白かったのは、コスト構造が変わると設計の発想そのものが変わることでした。トークン課金だと「いかに削るか」を考えますが、リクエスト課金だと「いかに詰め込むか」を考えます。同じLLMを使っていても、出てくるアーキテクチャが別物になります。
そして無償プランのままなら超過しても課金されないという一点が、想像以上に効きます。放置して寝られるかどうかは、技術的な完成度よりもこの安心感で決まりました。
まだ数日ぶんの記録なので、1ヶ月回した結果はあらためて書きます。もし同じような常時稼働エージェントを作っている人がいたら、リクエストの割り振り方をぜひ教えてください。



