ローカルLLM 6モデルに「祝日入りカレンダーGAS」を書かせたら全滅した — 唯一動いた1本も祝日が平成初期だった
前回の記事では、RTX 3090(24GB)でQwen系6モデルの速度と日本語品質を実測した。今回はその続編として、実務コーディング課題を出して、生成物を本物のGoogle Apps Script環境で実行検証してみた。
課題はこれだ。
Googleスプレッドシートに年間カレンダーを生成するGASを書け。土日祝に背景色、祝日は「何の日か」(祝日名)も表示、実行するだけで動くこと。
社内ツールでよくある、地味で実用的なタスク。ポイントは日本の祝日をどう取得するかを指定しなかったこと。ここに各モデルの「知識」と「設計判断」が出る。
結論を先に書くと:
- 6モデル全滅。一発で正しいカレンダーを作れたモデルはゼロ
- 実行時エラーなしで完走したのは qwen3-coder:30b ただ1本。ただし生成されたカレンダーの祝日は約30年前の祝日法だった
- 正解ルート(Googleの祝日カレンダー参照)を選んだモデルはゼロ。全員が祝日をハードコードまたは自力計算して死んだ
実験環境
前回と同じ。RTX 3090(VRAM 24GB)+ Ollama 0.32.6、コンテキスト8192、temperature 0.3、思考モードオフ。プロンプトは全モデル共通で、生成は各1回のみ(リトライなし)。
結果一覧
| モデル | 生成時間 | tok/s | 実行 | 敗因 |
|---|---|---|---|---|
| qwen3:8b | 14.7秒 | 118.6 | ✗ 即死 | const変数に再代入でエラー。海の日を9月と主張 |
| qwen3:14b | 28.1秒 | 71.2 | △ 崩壊 | 月ブロックの行間隔が足りず全月が重なる。祝日判定が「日にち」のみで全月に誤爆 |
| qwen3.6:27b | 81.1秒 | 38.7 | ✗ 即死 | sheet.mergeAcross() — Rangeのメソッドをsheetに呼んでTypeError |
| qwen3.6:35b | 52.3秒 | 107.5 | △ 崩壊 | 日付カウンタを週1回しか進めず、同じ数字が1行に7個並ぶ |
| qwen3-coder:30b | 26.3秒 | 160.6 | ◎ 完走 | 祝日データが平成初期(成人の日1/15、天皇誕生日12/23) |
| qwen3:32b | 82.9秒 | 35.2 | ✗ ハング | 春分・秋分を天文計算で求めに行き、判定条件ミスで無限ループ |
各モデルの死に様
qwen3:8b — スタート地点で転ぶ
const startRow = 1;
// ...
startRow += 30; // TypeError: Assignment to constant variable
constへの再代入で1行も実行されずに死亡。祝日リストも4件だけで、なぜか海の日が9月10日にあった。
qwen3:14b — 全月が1月になる
祝日を holidays[day](日にちだけがキー)で判定するため、「1日」が祝日なら全部の月の1日が祝日になる。さらに月ブロックを6行間隔で配置するが、1つの月は8行必要なので、12ヶ月分が互いに上書きし合う。
qwen3.6:27b — 設計は一番賢く、APIで死ぬ
27bだけが「ユーザーが購読しているカレンダーから日本の祝日を探す」という正解に最も近いアプローチに到達した。第N月曜日の計算関数、春分・秋分の近似式まで自作していて、設計力は明らかに頭一つ抜けている。
だが実行すると:
sheet.mergeAcross(currentRow, 1, 7); // TypeError: mergeAcrossはRangeのメソッド
存在しないAPI呼び出しで即死。フォールバックの祝日リストも建国記念の日が2/23(正解は2/11)だったので、動いていても間違っていた。
qwen3.6:35b — 自覚のある確信犯
35bの出力には驚いた。自分の祝日データに自分で注釈を付けている。
'2-23': '建国記念の日(誤)', // 実際は11日
'1-13': '成人の日(仮)', // 実際は第2月曜(14日)
「間違っていると分かっているデータ」をそのまま出荷してくるスタイル。しかも日付カウンタを週に1回しか進めないバグで、カレンダーは同じ数字が横一列に並ぶ抽象アートになった。
qwen3:32b — 天文学で無限ループ
一番野心的だったのは前回最下位の32b。春分・秋分の日を太陽黄経の天文計算(ユリウス日→太陽位置)で求めに行った。ロマンはある。
だが秋分の判定条件が「赤経0度付近」になっていた。秋分点の赤経は180度なので、条件は永遠に成立せず無限ループ。GASの6分タイムアウトで強制終了される運命のコードだ。ちなみに計算した振替休日を返り値に含め忘れるバグもあり、天文学が全部無駄になっていた。
唯一の生還者を実際に動かす
qwen3-coder:30bのコードだけは、構文・API・レイアウト計算のすべてが正しかった。生成速度も26秒(160 tok/s)で最速。文句なしの優勝……に見えた。
実際にApps Scriptで実行してみた(検証はChromeの自動操作で行い、コード投入→OAuth承認→実行→シート確認まで自動化した)。

結果がこれだ。

一見まともに見える。だがよく見ると成人の日が1月15日にある。ハッピーマンデー制度(2000年)以前の祝日法だ。天皇誕生日は2月に存在せず、平成の12月23日に置かれている。つまり、エラーなく動く唯一のコードは、約30年前の日本の祝日を出力する。
これは前回記事で見た「billion→億」の誤変換と同根の問題だと思う。学習データには新旧の祝日法が混在していて、モデルはどれが現行法か区別できない。日本ローカルの制度知識は、ローカルLLMの一番の弱点だ。
正解はこう書く
祝日をハードコードした時点で負けだ。GASにはCalendarAppがあり、Googleが管理する日本の祝日カレンダーを直接参照できる。振替休日も国民の休日も、Google側の計算をそのまま使える。
const HOLIDAY_CALENDAR_ID = 'ja.japanese#[email protected]';
function fetchHolidays(year) {
const cal = CalendarApp.getCalendarById(HOLIDAY_CALENDAR_ID);
const events = cal.getEvents(new Date(year, 0, 1), new Date(year + 1, 0, 1));
const map = {};
const tz = Session.getScriptTimeZone();
for (const ev of events) {
const title = ev.getTitle();
if (!HOLIDAY_NAME_PATTERN.test(title)) continue; // 後述
map[Utilities.formatDate(ev.getStartTime(), tz, 'yyyy-MM-dd')] = title;
}
return map;
}
この方式で書いた参照実装を実行した結果がこちら。祝日18件(法定祝日+振替休日)が祝日名付きで正しく入る。

ただし実際にやってみて、ハマりどころが2つあった。
- このカレンダーは「行事」も含む。節分、雛祭り、銀行休業日まで返ってくる(2026年は27件)。イベントのdescriptionは空で判別できないため、法定祝日名の正規表現ホワイトリストでフィルタする必要がある
- 祝日のみを返す
ja.japanese.official#holiday@...というIDも存在するが、GASからは使えない。未購読だとgetCalendarByIdがnullを返し、subscribeToCalendarは "Action not allowed" で弾かれる
この2つはWeb上にもほぼ情報がなく、モデルが知らないのも無理はない。ただ、だからこそ「実行して確かめる」工程が省けない。
まとめ — ローカルLLMコーディングの現在地
- 「それらしいコード」と「動くコード」の間には壁があり、「動くコード」と「正しいコード」の間にはもう1枚壁がある。6モデル中、1枚目の壁を越えたのは1本、2枚目を越えたのはゼロ
- コーディング用途ならqwen3-coder:30bが頭一つ抜けている。速度3倍で唯一完走。ただし出力の事実性は別問題
- 日本ローカル知識(祝日法、制度、慣習)はローカルLLMの鬼門。この領域はコードレビューではなく実行結果の検証で潰すしかない
- LLMの生成コードは必ず実行して検証する。今回それを自動化した(Puppeteer + CDPでApps Scriptを操作)が、この仕組み自体も面白いテーマなので、また別の記事で書く
道具として見れば、160 tok/sで26秒でこの水準のコードが電気代だけで出てくるのは十分実用的だ。ただし「祝日」のような一見簡単で実は深いドメインでは、人間側が正解ルートを知っているかどうかで結果が決まる。ローカルLLMは、あなたの知らないことは、だいたい間違える。