はじめに
GMOコネクトの永田です。
半年前、ローカルLLMにGoのテストを書かせようとして失敗し、「大きいモデルを載せられるマシンがあれば勝てるはず😇」と書いて締めました。
その後ご縁があってDGX Spark(統合メモリ128GB)が手に入り、ローカルLLMもこの半年で増えたので、同じお題で再挑戦しました。
やってみると、合否を分けたのはマシンの大きさではありませんでした(とはいえ、毎回安定して通すには大きさも効きました)。なお今回の記事は、あくまでGoのある1つのテスト生成課題に限った話で、特定のモデルやハードの優劣を決める記事ではありません。ローカルLLMの選び方を考え直すきっかけになった、その記録です。
- 半年前の記事
先にまとめ
同じお題(後述の addLogic の単体テスト生成)を、ローカル6モデル(gpt-oss:20b はMacとDGXの両方で)+クラウドのGemini 3ティアで、共通のエージェント手順に通した結果です。
| モデル | 環境 | 雑な指示 | 丁寧な指示 | 生成速度 (tok/s) | 1件の所要時間 |
|---|---|---|---|---|---|
| qwen3-coder:30b | Mac 36GB(当時の機材) | 不合格 | 不合格 | 約69 | 約70秒 |
| qwen3.5:27b | DGX 128GB | 不合格(go testはOKだがaddLogic単体でない) | 不合格 | 約12 | 約220秒 |
| glm-4.7-flash | DGX 128GB | 不合格 | 不合格 | 約58 | 約220秒 |
| nemotron-3-super:120b | DGX 128GB | 不合格 | 不合格 | 約21 | 約600秒 |
| gpt-oss:20b | Mac 36GB | 9/10合格 | 10/10合格 | 約69 | 約64秒 |
| gpt-oss:20b | DGX 128GB | 5/10合格(4件は中身が別物) | 5/9合格 | 約59 | 約47秒 |
| gpt-oss:120b | DGX 128GB | 10/10合格 | 10/10合格 | 約42 | 約24秒 |
| Gemini 3.1 Pro | クラウド | 合格 | 合格 | — | 約14秒 |
| Gemini 3.5 Flash | クラウド | 合格 | 合格 | — | 約12秒 |
| Gemini 3.1 Flash-lite | クラウド | 合格 | 合格 | — | 約5秒 |
gpt-oss の各行は、temp=0でも実行ごとに結果が揺れたため、10回ずつ試した合格率です(合格=go testが通り、かつ addLogic を直接検証できているもの)。他のローカル行とクラウド行は少数回の結果で、クラウドは安定しているとみなしています。生成速度(tok/s)と「1件の所要時間」は雑promptの中央値です。「1件の所要時間」は、合格なら完成まで・不合格なら3回試して諦めるまでの時間で、モデルの読み込みや自己修正の回数で前後します(おもしろいことに、1トークンの速さは gpt-oss:120b の方が遅いのに、一発で通るぶん総時間はいちばん短くなりました)。クラウドはネットワーク往復やサーバ側の待ち時間を含む応答時間で、ローカルのdecode速度とは測り方が異なるため、生成速度(tok/s)は「—」としています。いずれも計測時の環境に左右される参考値で、特にクラウドの所要時間は手元の回線速度の影響を受けます。
結果から見えてきたことです。
- 当時の「30分」の主因はハードではなく、古いソフト+当時のエージェント(Cline)が上限なく回っていたことらしい
- 合否を分けたのはサイズより「タスクに合うモデルか」。gpt-oss系は通り、大きい
nemotron-3-super:120bでも通らなかった - ただし同じ「合う系統」でも、毎回安定して通るか(頑健性)は別物。10回ずつ試すと
gpt-oss:120bは10/10で一発合格、小さいgpt-oss:20bは当たり外れが大きかった - 同じモデルファイルでも、動かすマシンで結果が違って見えた(試行回数が限られるので断定はできないが)。スペック表やモデル名だけで決めず、実タスク・実機でのPoCを
- テストが「緑(合格)」でも、目的の関数を検証していないことがある。雑な指示で出やすく、丁寧に指示すると減った
- クラウドは一番安いティアでも難なく通った。費用対効果はクラウド、機密性はローカル、と軸が別
お題:半年前にあきらめたテスト生成
題材は当時と同じで、Go製Webフレームワーク Fiber の関数です。*fiber.Ctx を引数に取る addLogic の単体テストを書かせます。
// main logic for adding two numbers
func addLogic(c *fiber.Ctx, a int, b int) error {
result := a + b
return c.SendString(strconv.Itoa(result))
}
*fiber.Ctx は自前で素直に生成できないため、単体テストでどう用意するかが悩みどころです。Stack Overflowでも質問が立っています。
書き方は1つではありません。実際に今回通ったテストは、大きく2通りに分かれました。どちらも addLogic 自体を動かしている点は共通です。
-
app.AcquireCtx(&fasthttp.RequestCtx{})で*fiber.Ctxを用意し、addLogic(ctx, a, b)を直接呼ぶ(今回いちばん多かった形) - テスト用のルートを1本立て、そのハンドラの中で
addLogicを呼び、app.Testでレスポンス本文を検証する
逆に、既存のHTTPハンドラ addHandler(クエリを読んで addLogic を呼ぶ入口)をそのままテストしたり、a + b を別途計算して検算するだけだと、go test は通っても、テスト対象は addLogic そのものになりません。今回はこの「テストが addLogic を直接動かしているか」を合否とは別に必ず確認しました。
当時の指示文(雑な指示)はこの一行だけです。今回もそのまま使いました。
addLogicのUnitTest Codeを作成
検証のしかた
共通のエージェント手順に通す
モデルごとに使うツールが違うと比較になりません。そこで、Claude Codeのような「計画→go testを実行→エラーを読んで自己修正」という流れを、最小限のPythonスクリプトにして共通化しました。エンジン(モデル)だけ差し替えて、同じ手順で回しています。
OpenCode のような既製のエージェントCLI(ローカルのOllamaからクラウドまで対応)を使う手もありました。ただ既製ツールは、それ自体が独自のシステムプロンプトやツール群、リトライの作り込みを持っていて、その層が結果を左右します。これだと「モデルの差」と「ツール側の作り込みの差」が混ざってしまいます。今回はプロンプト・温度・自己修正の上限(3回)・合否の判定を全モデルでそろえ、半年前の素朴な手順に合わせたかったので、あえて中身を見渡せる自作スクリプトにしました。
- 1件あたり最大3回まで自己修正(当時の「手動で3回まで再指示」に合わせた)
- temperature=0(出力のばらつきを抑える設定)
- 指示は2種類: 雑な指示(前述の一行)と、丁寧な指示(「
addLogicを直接呼ぶこと」「fiber.Ctxを用意すること」などを明記)
評価は4点で見ました。go test が通るか、addLogic を実際に呼んでいるか、生成速度(tok/s=1秒あたりに生成する語の数)、自己修正の回数です。
環境とソフトの更新
3つの環境で回しました。
- Mac(M4 Max・メモリ36GB):半年前と同じ機材
- DGX Spark(GB10 Grace Blackwell・統合メモリ128GB、実際に使えるのは約119GB、メモリ帯域273GB/s)
- クラウド(Geminiの各ティア)
検証前に、推論まわりのソフトを最新化しました。MacとDGXの両方をOllama 0.30.8にそろえ、llama.cppとvLLMも更新しています。こうすると、少なくとも「推論ソフトが古い/バージョンが違う」という要因は外せます(チップもバックエンドも違うので、MacとDGXの差をメモリ容量だけに絞れるわけではありません)。
結果から分かったこと
その1:遅さの主因は、ハードではなく「当時の道具立て」だった
当時のローカルLLMは「30分かかってファンがうなる」状態でした。ところが今は、同じMac(36GB)で、1回の生成は数十秒、3回の自己修正まで通しても1分前後で返ってきます。
ただ、当時と今では道具立ても違います。半年前は Cline(VS Code上のAIエージェント)+ Ollama + qwen という構成で、エージェントが自己修正を延々と繰り返していた可能性があります。今回は自己修正を3回で打ち切る最小ハーネスなので、「同じやり方でソフトだけ新しくした」きれいな比較ではありません。
それでも一つ言えるのは、当時の遅さの主因はメモリ不足やハードの非力さではなかった、ということです。古い推論ソフトか、上限なく回り続けるエージェントか——どちらがどれだけ効いたかは今となっては切り分けられませんが、いずれにせよ大きいマシンを買わずに済む話でした。
その2:合否を分けたのは、サイズより「相性」だった
「メモリを増やして大きいモデルを載せれば書けるようになるのでは」という当時の見立てを、ここで直接ためせました。結果は、モデルの大きさでは決まりませんでした。
- 通ったのは
gpt-oss:20bとgpt-oss:120b - 通らなかったのは
qwen3-coder:30b、qwen3.5:27b、glm-4.7-flash、そしてパラメータの大きいnemotron-3-super:120b
たとえば当時と同じ qwen3-coder:30b は、存在しないAPI(fiber.NewContext など)を呼ぼうとして、3回の自己修正でも直りませんでした。
./addlogic_test.go:18:13: app.NewContext undefined (type *fiber.App has no field or method NewContext)
大きい nemotron-3-super:120b でも通らなかったので、「大きくすれば良くなる」わけではありません。新しい・大きい・メーカー純正・コーディング向けの名前、といった見た目の手がかりは、このお題の合否とは一致しませんでした。決め手はサイズではなく、タスクとモデルの相性でした。
その3:同じ「相性の良い系統」でも、毎回通るかは分かれた
ここが今回いちばんの発見でした。合う系統の gpt-oss でも、各モデルを10回ずつ回すと、頑健性(毎回安定して通るか)がはっきり分かれたのです。
-
gpt-oss:120b(DGX)は雑な指示でも10回中10回、しかも一発(自己修正なし)で合格。最も安定していました - 小さい
gpt-oss:20bは当たり外れが大きく、Macでは雑9/10・丁寧10/10、DGXの雑では5/10(うち4回は後述の「中身が別物」)と、環境や指示で大きく振れました
「サイズより相性」は今回も当てはまりますが、同じ相性の良い系統の中では、大きい gpt-oss:120b が最も安定していました。これは65GBあり、36GB Macには載りません。安定して一発で通るモデルの置き場所として、128GBの統合メモリが効いた、というのが正直なところです。
一方、小さい gpt-oss:20b も、当時の36GB Macなら(特に丁寧な指示で10/10と)十分に通せます。半年前も、大きなマシンを待つより、このタスクに合うモデルを選ぶだけで通せた可能性は高いです。ただし当たり外れの波はあるので、確実を狙うほど大きいモデルが安心、という二段構えでした。
もう一つ気になったのは、同じ gpt-oss:20b(モデルファイルは中身まで同一)でも、MacとDGXで合格率が違って見えたことです。ただ各10回なので、これだけで「マシンで決まる」と断定はできません。それでも、temp=0でも実行環境(推論バックエンド)が違えば出力は揺れうるので、同じモデル名でも、使う予定の実機で一度通してみる価値はある、と感じました。
その4:テストが「緑」でも中身が別物のことがある
qwen3.5:27b は雑な指示で go test が通りました。中身を見ると、addLogic を直接呼ばず、既存のHTTPハンドラ addHandler を登録してエンドポイント越しに叩く形でした。これは動くテストで、addHandler の中で addLogic も実行されるので、足し算が壊れていれば気づけます。テスト単体としては及第点です。
ただ今回のお題は「addLogic の単体テスト」で、難所は「*fiber.Ctx をどう用意して addLogic を直接呼ぶか」でした。エンドポイント越しのテストは、クエリ解釈やルーティングまで巻き込んだ広めのテストで、その難所を回避しています。目的の関数を単体で検証できてはいないので、この検証では不合格としました。
合否だけ見ると「addLogic をテストできた」と取り違えるところです。
// 通ってはいるが addLogic を直接呼んでいない例
app.Get("/add", addHandler)
req := httptest.NewRequest("GET", "/add?a=...&b=...", nil)
resp, err := app.Test(req)
しかもこれは qwen3.5 だけの癖ではありませんでした。雑な指示では、合格率の高かった gpt-oss:20b(DGX)でも10回中4回が同じパターンに陥っていました。一方、丁寧な指示(「addLogic を直接呼ぶこと」と明記)に変えると、この別物テストは0回に。指示を一行具体的にするだけで避けられた、ということです。
評価のときは「合格/不合格」だけでなく「何をテストしたか」を見る必要があります。今回 addLogic を直接呼んだかを別軸で確認したのは、こうした「緑だが狙いと違う」を拾うためでした。
その5:思考モードの相性でスクリプトが空振りする
glm-4.7-flash は、思考(thinking)を有効にすると、同じコードブロックを延々と生成し続けてトークン上限まで止まらないことがありました。思考を切ると今度は構文が崩れる、という具合で、今回のお題ではかみ合いませんでした。
モデル単体ではなく「モデル+推論サーバの設定」の組み合わせで挙動が変わるので、ここもPoCで実際に動かして確かめるしかない部分です。
その6:クラウドは一番安いティアでも通った
比較のために置いたクラウド(Gemini)は、上位のProだけでなく、Flash、さらに一番安いFlash-liteまで、すべて合格しました。Flash-liteは一発では決めず、go test の結果を見て3回の自己修正で到達しましたが、それでも1件およそ5秒です。
ここから言えるのは、エージェントの自己修正ループが、軽いモデルの不足をある程度埋めてくれるということです。賢いモデルは一発、軽いモデルは数回試す、どちらも数秒で終わります。今回くらいの難易度なら、安いクラウドティアで十分に間に合いました。
では、DGXや大きいモデルは要らないのか
そうは思いません。今回むしろ、最も安定して一発で通したのは、128GBにこそ載る gpt-oss:120b でした。用途次第で出番は十分にあります。
- 機密データを外に出せない用途では、そもそもクラウドが選べません。社内のチケットやドキュメントを扱う検証では、ローカルで完結できることが効きます(Python+Ollamaで16,000チケットからレビュー観点を自動生成した話)
- 安定して一発で通したい用途では、今回の
gpt-oss:120bのように、大きめのモデルを1台に載せられること自体が価値になります。大量バッチや長いコンテキストを回すときも同様です - 逆に、最新の大規模なオープンモデル(例:DeepSeek V4系)は、Ollamaではクラウド扱いになっていて、手元にダウンロードして動かすのが難しくなりつつあります。「ローカルで動かせる範囲」と「最前線のモデル」が少しずつ離れてきている印象です
費用対効果ならクラウド、機密性や占有ならローカル、と軸が別なので、タスクごとに選び分けるのが現実的でした。
まとめ
半年前の「大きいモデルを載せれば勝てる」という見立てを、実機で確かめた結論です。あくまでGoのある1つのテスト生成課題に限った話として読んでください。
- 当時の遅さの主因はハードではなく、古い推論ソフトと当時のエージェントのループだったとみられる
- 合否を分けたのはサイズより相性。gpt-oss系は通り、大きい
nemotron-3-super:120bでも通らなかった - ただし同じ相性でも、毎回安定して通るか(頑健性)は別。10回試すと
gpt-oss:120bは10/10で一発、小さいgpt-oss:20bは当たり外れ。確実を狙うほど128GB級が効いた - 同じモデルでもマシンで結果が違って見えた(少数回なので確証ではない)。スペック表やモデル名だけでは決めきれず、実タスク・実機でのPoCを
- 「緑のテスト」は中身まで確認する。雑な指示で別物が出やすく、丁寧にすると減った
- 今回の難易度なら安いクラウドティアでも十分。機密や占有が要るときにローカルが効く、と使い分け
次は OpenCode のような本物のエージェントCLIに各モデルをそのままつないで、実際の開発での使い勝手も見てみたいと思っています。
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。