TL;DR
- 運用中のシフト作成アプリ ShiftMaster LM の AI 呼び出しを、DeepSeek から さくらのAI Engine (gpt-oss-120b) に移行した
- さくらのAI Engine は OpenAI 互換なので、移行はエンドポイント URL とモデル名を足すだけで済んだ (差分は実質数行)
- さくらは 月 3,000 リクエストまで無料 (超過分はトークン課金)。1 リクエストに詰め込むほど無料枠の消費を節約できるので、「スタッフ全員分の希望テキストを 1 リクエストにまとめて解析」するフルバッチ化を実装した
- 最初は失敗が出たが、
-
max_tokensを大きくする - 入力を JSONL に構造化する
の 2 手で 39 ケース中 39 ケース = 正解率 100% を達成
-
- 同じ 39 人分の希望が、従来「39 リクエスト・計 77 秒・正解率 92%」だったのが、「1 リクエスト・22 秒・正解率100%」に。バッチ化で 1 ユーザー (部署) あたり月数リクエスト以下に収まり、現状のユーザー数なら余裕で無料枠内
背景: なぜ乗り換えたのか
ShiftMaster LM ( https://shiftmaster.emuyn.net ) は、弊社 EMUYN LLC が提供する AI シフト自動作成・管理システムです。キャッチコピーは「公平に、スマートに。シフト管理を AI で革新する」。飲食・医療・介護・小売といったシフト制の現場、とりわけ24 時間体制で早番/遅番/夜勤/オンコールが入り混じる職場の、あの憂鬱な「シフト作成の重労働」を丸ごと解消します。
主な特徴はこんな感じです。
- 🤖 AI シフト自動生成: 人員・スキル・希望・法令をまとめて考慮し、一発でたたき台を生成
- ⚖️ 公平性エンジン: 夜勤や NG 日の「譲ってくれた実績」を自動で記録し、次回に反映。だから納得感がある
- 📝 自然言語での希望入力: 「10 日は子供の行事で休みたい、できれば午前だけ入れます」をそのまま書けば AI が読み取る (← 本記事で改善したのがまさにこの部分)
- ✏️ グリッドエディタ: 微調整はマウスでサクサク
- 📱 QR ログイン: スタッフはアプリのインストール不要
- 🏥 複数病棟/拠点対応・📊 勤務実績 CSV 出力
しかも 初期導入費用ゼロ・カスタム開発費ゼロ、3 ヶ月は機能制限なしで無料。価格も Small(〜30 名)¥9,900/年からと、この手のツールとしては思い切った激安設定にしています。より多くの現場に使ってもらいたいからです。
ただ、この「激安で広く使ってもらう」路線には裏があります。価格が安いぶん、AI 呼び出しのコストがそのまま採算を左右するのです。だからこそ、AI コストの圧縮は私たちにとって死活問題でした。ここからが本題です。
このアプリには 2 種類の AI 呼び出しがあります。
-
希望解析 (analyze-request): スタッフが自由記述した「10 日は子供の行事で休みたい、できれば午前中だけ入れます」のような文章を、構造化データ (
{type, date, priority}の配列) にパースする - その他: シフトの説明生成、ローカルルール解析、ヘルプチャットなど
これらを DeepSeek で回していたのですが、DeepSeek が大幅値上げをアナウンスしました。詳細は未定でしたが、「特定プロバイダーにロックインされているのは危険だ」と痛感します。
そんなときに登場したのが さくらのAI Engine でした。
- OpenAI 互換 / Anthropic 互換の両インターフェースを提供
- 月 3,000 リクエストまで無料
- 無料枠を超えた分はトークン課金になる
この「無料枠がリクエスト数で数えられる」点が決定的でした。10 人分を 10 リクエストで呼べば無料枠を 10 消費しますが、1 リクエストにまとめれば消費はたったの 1。つまり詰め込むほど無料枠を長く使えるわけです (枠を超えたぶんはトークン課金)。「全員分を 1 リクエストで処理する」という設計が、無料枠の数え方とキレイに噛み合いました。
移行はほぼ「設定を足すだけ」だった
ShiftMaster LM の AI クライアントは、もともと複数プロバイダーを切り替えられる作りにしてありました。OpenAI 互換プロバイダーは、エンドポイント URL とデフォルトモデル名を登録するだけで使えます。
// api/lib/AiClient.php
private const DEFAULT_MODELS = [
'sakura' => 'gpt-oss-120b', // ← 追加
'anthropic' => 'claude-haiku-4-5',
'openai' => 'gpt-4o-mini',
'deepseek' => 'deepseek-v4-flash',
// ...
];
private const OPENAI_COMPAT_URLS = [
'sakura' => 'https://api.ai.sakura.ad.jp/v1', // ← 追加
'openai' => 'https://api.openai.com/v1',
'deepseek' => 'https://api.deepseek.com/v1',
// ...
];
認証は OpenAI と同じく Authorization: Bearer <APIキー> 形式なので、既存の OpenAI 互換クライアントがそのまま流用できます。
「どのプロバイダーを使うか」は環境変数 AI_PROVIDER で一元管理し、ここを書き換えるだけでプロバイダーを切り替えられるようにしました。
モデル選定: gpt-oss-120b vs llm-jp-3.1
さくらのAI Engine には複数モデルがあります。実際にどれが使えるのか、アプリと同じテストハーネス(後述)にかけて実測しました。フィクスチャは実運用に近い 39 ケースの希望テキストです。
| モデル | 方式 | 正解率 | 備考 |
|---|---|---|---|
gpt-oss-120b |
単発 (1 人 1 リクエスト) | 36/39 (92%) | |
gpt-oss-120b |
バッチ (JSONL・調整前) | 35〜37/39 (90〜95%) | |
gpt-oss-120b |
バッチ (JSONL・調整後) | 39/39 (100%) | 約 22 秒/1 リクエスト |
llm-jp-3.1-8x13b |
単発 | 28/39 (72%) | |
llm-jp-3.1-8x13b |
バッチ | 0/39 (0%) | context 4096 で HTTP 400 |
日本語特化モデルの llm-jp-3.1 に期待したのですが、バッチ処理では全滅しました。理由はエラーメッセージに明快に出ています。
API returned HTTP 400: {"error":{"message":
"'max_tokens' or 'max_completion_tokens' is too large: 8000.
This model's maximum context length is 4096 tokens ..."}}
コンテキスト長が 4096 トークンしかなく、複数人分をまとめた入力+出力が収まりません。まとめ処理をするならコンテキスト長の大きいモデルが必須、という当たり前だけど見落としがちな教訓でした。結果、コンテキストが大きく精度も高い gpt-oss-120b を採用しました。
⚠️ 落とし穴メモ: gpt-oss-120b は reasoning 系モデルですが、
reasoning_contentは返ってきません。またmax_tokensが小さいと途中で出力が切れて JSON が壊れます。まとめ処理ではmax_tokensを大きめ (今回は解析 16384 / 正規化 8192) に取るのが安定の鍵でした。「エラーが出たら、まずmax_tokensとコンテキスト長を疑え」です。
フルバッチ化: 全員分を 1 リクエストに
無料枠(リクエスト数)を最大限に活かすため、希望解析をフルバッチ化しました。従来は「スタッフ 1 人 = 1 リクエスト」でしたが、部署全員分を 1 リクエストにまとめて投げます。
処理の流れはこうです。
- 未解析の希望テキストを集める (最大 50 人/バッチ)
- 表記ゆれを整える正規化バッチ (1 リクエスト)
- 希望解析バッチ (1 リクエスト)
- 結果をパース・検証して各スタッフの
parsedDataに保存
50 人でも 1〜2 リクエストで完結します。実際、1 ユーザー(部署)あたりの消費は月 10 リクエスト以下。無料枠 3,000 リクエスト/月に対して、現状のユーザー数ならまったく問題なく無料枠に収まります。
Before → After: 39 リクエスト・計 77 秒 → 1 リクエスト・22 秒
同じ 39 人分の希望テキストで、単発方式とバッチ方式を計測しました。
| リクエスト数 | 所要時間 (合計) | 正解率 | |
|---|---|---|---|
| 従来 (1 人 1 リクエスト) | 39 | 77 秒 | 92% |
| バッチ (JSONL) | 1 | 22 秒 | 100% |
無料枠がリクエスト数で数えられるさくらでは、リクエスト数が 39 → 1 になるのがそのまま無料枠の節約に直結します。時間も速くなり、正解率まで上がりました。「まとめると枠に優しくて速くて正確」という、三拍子そろった結果です。
失敗率が高い → JSONL 入力で劇的改善
最初のバッチ実装では、こんな感じの素朴な区切りで複数人分を渡していました。
--- 1 人目 ---
10日は子供の行事で休みたい、できれば午前中だけ入れます
--- 2 人目 ---
...
これだと 90〜95% 止まり。人数が増えると「何人目の結果か」の対応がズレたり、区切りをまたいで解釈が混線したりします。
そこで、入力を JSON/JSONL に構造化することを試しました。1 行 1 レコードの JSONL で index を明示します。
入力 (JSONL):
{"index":0,"text":"6月10日は休みたいです。"}
{"index":1,"text":"できれば早番希望、水曜は遅番だと助かります"}
{"index":2,"text":"特になし"}
出力 (JSON):
{"results":[
{"index":0,"items":[{"type":"off","date":"2026-06-10","priority":"want"}],"confidence":0.95},
{"index":1,"items":[...],"confidence":0.9},
{"index":2,"items":[],"confidence":1.0}
]}
index で入力と出力が明示的に紐づくため、順序ズレや取りこぼしがなくなり、検証側でも「index が全部揃っているか」を機械的にチェックできます。
結果は劇的でした。
39 ケース中 39 ケース = 正解率 100%、しかも 1 リクエスト・22 秒。
区切り文字のプロンプトから JSONL に変えただけで、DeepSeek 時代のベースライン以上の品質を、さくらの無料枠で達成できたわけです。**「まとめ処理の入力は JSONL にせよ」**これが今回いちばんの実践知でした。
無料枠を超えたら? 多段フォールバックで止まらない
無料枠 3,000 リクエストを超えたらどうなるのでしょうか。無料契約ではエラーになります。有料契約では、超過してもエラーにはならず、静かに課金が始まります。とはいえ本番運用では、レート制限 (429)・一時的な障害 (5xx)・混雑などでAI 呼び出しがエラーを返す場面はゼロにできません。そこで、業務が止まらないように多段フォールバックを用意しています。
- 正規化バッチが失敗 → 原文のまま解析へ続行(表記ゆれ吸収を諦めるだけ)
- 解析バッチが 429/5xx/quota で失敗 → 別プロバイダーへ自動フェイルオーバー。さくら → DeepSeek → Anthropic… と、API キーが設定されている次のプロバイダーで 1 回リトライする
- それでも全滅 → 1 人ずつの単発解析にフォールバック(確実性優先)
- AI が完全に使えない → 正規表現ベースの簡易パースに落とす(AI ゼロでも
parsedDataは埋まる)
ポイントは 2 番です。プロバイダーを環境変数 AI_PROVIDER と fallbackChain で抽象化してあるので、さくらが一時的にコケても DeepSeek や Anthropic が引き継ぐ。しかも 400/401 のような「別プロバイダーでも直らない決定的エラー」ではフェイルオーバーせず(無駄な課金リクエストを撃たない)、429/5xx/403(quota)/タイムアウトといった一過性・枠超過のエラーだけを次に回します。
「無料枠でコストを抑える」ことと「単一プロバイダーに依存しない」ことを両立させる設計です。値上げにも障害にも、慌てなくて済みます。
なぜ「一発で 100%」にこだわるのか: テストハーネスの話
ここまで平然と「正解率」を測っていましたが、これができるのは ShiftMaster LM が AI 解析専用の評価ハーネス (scripts/ai_eval) を用意しているからです。
- 実運用に近い希望テキストのフィクスチャ(正解ラベル付き)
- モデル・プロバイダー・プロンプトを差し替えて一括実行するランナー
- 「must_have がちゃんと must で取れているか」まで見るスコアラー
- 失敗ケースを抽出し、AI に分類・改善提案させるプロンプトチューニングのループ
だからこそ「DeepSeek → さくら」「単発 → バッチ」「素朴区切り → JSONL」といった変更のたびに、体感ではなく数値で回帰チェックができます。プロバイダー乗り換えを 1 日で判断・実行できたのは、この土台があったからです。
シフト作成という業務は、1 か所の取りこぼしが「誰かが休めない/現場が回らない」に直結します。だから ShiftMaster LM は「AI になんとなく投げる」のではなく、テストで裏を取りながら一発で質の高い出力を出すことに徹底的にこだわっています。希望を書けば、締切ボタンひとつで解析からスコアリングまで走り、あとはワンクリックでシフト表が組み上がる。その裏側には、こうした地味な評価基盤が効いています。
ShiftMaster LM に興味を持たれた方はこちら → https://shiftmaster.emuyn.net
まとめ
- さくらのAI Engine は OpenAI 互換なので移行が本当にラク。URL とモデル名を足すだけだった
- リクエスト数で数える無料枠 × フルバッチ化の相性が抜群。1 リクエストに詰め込むほど無料枠が長持ちし、3,000 req/月で十分回る(超過分はトークン課金)
- まとめ処理では コンテキスト長の大きいモデル (gpt-oss-120b) を選び、
max_tokensを大きく取る - 入力を JSONL に構造化すると、件数が増えても破綻せず正解率 100% に到達した
- 多段フォールバック(プロバイダーフェイルオーバー → 単発 → 正規表現)で、枠超過や障害でも業務が止まらない
- そして、こうした判断を支えるのはテストハーネス。数値で裏を取れる環境が、乗り換えのスピードと品質を両立させる
「値上げに振り回されず、無料枠で高品質」を実現できた良い移行でした。さくらのAI Engine、まとめ処理系のワークロードとは特に好相性だと思いました。お勧めです。
