軽量ローカルLLM5モデルガチ比較!実際に実行して初めてわかったコード生成の落とし穴

軽量ローカルLLM5モデルガチ比較!実際に実行して初めてわかったコード生成の落とし穴

きっかけ:軽量モデルって結局どこまで使えるの? うちの自動記事生成パイプラインでは、誤字脱字チェックみたいな「軽めだけど頻度の高いタスク」に、わざわざ20B超えの大きいモデルを使う場面があるんだよね。でも正直、そこまで賢さが要らないタスクなら、もっと軽量なモデルに任せて高速化したいよねって前々から思ってた。 というわけで、単体のB580(VRAM 12GB)に余裕で収まるサイズの軽量モデルを5つ集めて、日本語生成・翻訳・コーディングの3タスクでガチンコ比較してみた。今回は「なんとなく良さそう」で終わらせず、コーディング課題は実際にコードを実行して検証するところまでやってる。 対象モデルとテスト内容 モデル 実体 Qwen3.8-4B-Distill hf.co/empero-ai/Qwen3.8-4B-Distill-GGUF:Q4_K_M Phi-4-mini-instruct phi4-mini(Ollama公式) Qwen3-4B hf.co/Qwen/Qwen3-4B-GGUF:Q4_K_M(Thinking系) Llama-3.2-3B-uncensored hf.co/bartowski/Llama-3.2-3B-Instruct-uncensored-GGUF:Q4_K_M Gemma4-E2B-Alignment-Study hf.co/mradermacher/gemma-4-E2B-Alignment-Study-GGUF:Q4_K_M テスト項目は4つ。①「画像生成AIの問題点」について400字程度の日本語生成、②固定の日本語3文を英訳、③「テキストファイルから最頻出単語TOP5を表示するプログラム」を書かせて実際に実行、④生成速度とGPU/VRAM使用率の計測。 ちなみに候補に挙がっていたモデルのうち2つは、検証中に「そもそも実体が怪しい」ことが判明して除外してる。mondk/GGUF.chatgpt-gpt-gpt5.1-thinkingは名前と裏腹にOpenAIのGPT-5.1では全くなく、Qwen3-4B-Thinking-2507を「悪意のあるプロンプトも拒否せず説明する」方向にファインチューンした別物だった。名前だけで判断せずちゃんと中身を確認するの、大事だなと。 生成速度:Qwen3-4Bだけ突出して遅い理由 グラフにするとQwen3-4Bのコーディングタスクだけ突き抜けて時間がかかってるのが一目瞭然。これ、単純に生成が遅いわけじゃなくて「事故」なんだよね。 Qwen3-4Bは"Thinking"系のモデルで、Ollama呼び出し時にthink=Falseを指定したにもかかわらず、<think>...</think>形式の内部思考(しかも英語の長い独り言)がそのまま出力に漏れ続けた。日本語生成・翻訳・コーディングの全タスクで発生していて、特にコーディングタスクでは104秒のうち大半がこの可視化された思考過程で占められてる。コミュニティ配布のGGUF量子化モデルだと、OllamaのthinkAPIパラメータが必ずしも効かないケースがある、というのは地味に重要な学びだった。 それを除けば、Phi-4-miniとLlama-3.2-3B-uncensoredが速度的には優秀。ただし後述するけど、速いなりの理由があった。 VRAM使用量:ほぼモデルサイズに比例 初回計測では全モデルVRAM使用量が0と表示されて「あれ?」ってなったんだけど、これはWindowsの「GPU Process Memory」カウンターが、Ollama(Vulkanバックエンド)のプロセス単位の使用量を正しく報告できない制約が原因だった。プロセス非依存の「GPU Adapter Memory」カウンター(タスクマネージャーと同じ系統)に切り替えて再計測してる。 結果は見ての通り、ほぼモデルサイズに比例。最大のQwen3.8-4B-Distillで約8GB、最小のGemma4-E2Bで約2.5〜2.9GB。全モデルとも単体のB580(12GB)に十分収まるサイズで、この点は候補全モデルで問題なし。 コーディング課題:見た目は動きそうでも実行すると壊れるやつが2つ ここが一番面白かったところ。テストデータ(51語、正解: the:10, quick:9, fox:5, is:5, dog:4)で全モデルのコードを実際に実行してみたら、5モデル中2モデルが実質的に破綻してた。 モデル 実行結果 問題点 Qwen3.8-4B-Distill 正解と完全一致 なし Phi-4-mini 無出力(終了コード0) 関数を定義しただけで呼び出し部分がコメントアウトされたまま Qwen3-4B 正解と完全一致 潜在バグあり(今回は顕在化せず) Llama-3.2-3B-uncensored FileNotFoundErrorで即クラッシュ ファイルパスを引数で受け取らず'path_to_your_file.txt'とハードコード Gemma4-E2B-Alignment-Study 正解と完全一致 存在しないファイルでも適切にエラーメッセージを表示、最も堅牢 Phi-4-miniのコードは、パッと見ただけだと「関数もちゃんと定義されてるし動きそう」に見えるんだよね。でも実際に実行すると、呼び出し部分が丸ごとコメントアウトされたままで何も起きない。終了コードも0なので、自動テストで「エラーが出なかったからOK」みたいな雑な判定をしてたら見逃してたと思う。Llama-3.2-3Bの方はもっと直接的で、ファイルパスを引数で受け取る仕様のはずが決め打ちのパス文字列になっていて、渡したテストファイルが存在せず即クラッシュ。 有名な提供元(Microsoft/Phi、bartowski)のモデルだからといって、コード生成品質が保証されるわけじゃないんだな、というのが実行検証してみて初めてわかったこと。コードの目視確認だけだと、この手の「表面上はそれらしいけど実運用不可能」なバグは普通に見逃す。 言語混入は「たまたま」じゃなく構造的な弱点だった 日本語生成タスクは同じプロンプトで2回実施したんだけど、Qwen3.8-4B-DistillとLlama-3.2-3B-uncensoredは2回とも言語混入系のミスを起こした。1回目はQwen3.8-4B-Distillが「多起」という誤変換、2回目は同じ文の別の箇所に韓国語の単語(제시)が混入。Llama-3.2-3B-uncensoredの方は、1回目が翻訳での1文丸ごと脱落、2回目は簡体字・繁体字由来の漢字(「自杀」「剽竊」など)が日本語文中に混入していた。 サンプリングで出力が毎回変わるモデルで、違う種類のミスが2回とも出るってことは、単発の運の悪さじゃなくて、モデルや量子化の構造的な弱点だと考えた方が良さそう。日本語主体で使うなら、この2モデルは要注意。 結論 総合最良はGemma4-E2B-Alignment-Study — 内容の充実度・翻訳精度・コードの堅牢性のすべてでトップ。速度も45.7秒と許容範囲内 Qwen3.8-4B-Distillが僅差で優秀 — 全項目で合格点、最速クラス。ただし言語混入のリスクは頭に入れておきたい Phi-4-mini・Llama-3.2-3B-uncensoredは要注意 — 速度は魅力的だけど、コーディングタスクで実質的に破綻。有名どころのモデルでも過信は禁物 Qwen3-4B(Thinking系)は現状のOllama連携では扱いづらい — 内部思考の漏洩で速度が3〜10倍に膨張。専用のパース処理無しでは採用しにくい 「実際に実行して検証する」ことの重要性を再確認 — コードの目視確認だけでは見抜けない不具合が、5モデル中2モデルで見つかった 軽量モデルだからといって侮れない結果と、逆に「有名だから安心」でもない結果が両方出た検証だった。次はこのGemma4-E2Bを、うちのパイプラインの軽量タスク(誤字脱字チェックとか)に実際に組み込んで、体感速度がどれくらい変わるか試してみたい。

2026年9月7日 · 1 min
誤字脱字チェックが記事のリンクを壊していた話 — ローカルLLM自動生成パイプラインのデバッグ記録

誤字脱字チェックが記事のリンクを壊していた話 — ローカルLLM自動生成パイプラインのデバッグ記録

きっかけ:自動生成した記事のリンクが、いつの間にか切れていた うちの検証ノート(もう一つのブログ)は、ローカルLLMで楽天商品の比較記事を自動生成して、そのまま公開キューに流すパイプラインで回してるんだけど、ある日ふと「前に公開した記事、アフィリエイトリンクが切れてない?」って気づいたんだよね。 最初は「まあ生成AIだしたまに誤字くらい出るか」くらいに思ってたんだけど、よく見たら誤字どころじゃなかった。見出しの中の商品スペックが「両耳用」→「両極用」になってたり、対応機種が「PS5」→「PS2」になってたり。極めつけは、アフィリエイトリンクのドメインが hb.afl.rakuten.co.jp から hb.agfl.rakuten.co.jp に変わってて、見事にリンク切れ。これはさすがに「たまたま」で片付けられないやつだった。 犯人は「誤字脱字チェック」そのものだった パイプラインの流れとしては、こんな感じ。 ローカルLLM(Gemma4-26B)で商品紹介文を生成 コード側のテンプレートで見出し・画像タグ・アフィリエイトリンクを組み立ててMarkdownをレンダリング 「誤字脱字が残ってないか最終チェックして」と、レンダリング済みの全文をもう一度LLMに投げる LLMが返した「修正後の全文」をそのまま採用 犯人はステップ3・4だった。チェック前後のMarkdownを実際にdiffしてみたら、見出しの中の固有名詞は化けてるのに、その数行下にある画像altタグの同じ文字列は無事、みたいな状態がいくつも見つかったんだよね。 - ...装着タイプ:両耳用 接続方式:ワイヤード(有線)... + ...装着タイプ:両極用 接続方式:ワイヤード(有効)... プロンプトには「見出し・リンクのURL・文言は一切変更しない」ってちゃんと書いてあるんだけど、量子化した26Bモデルに数千字のMarkdownを丸ごと「作文として書き直させる」と、この手の指示は確率的に無視されることがある、というのが今回学んだこと。長文を一字一句そのまま再生成させるタスクって、地味に難易度が高いんだなと。 直し方:そもそもLLMにリンクを見せない プロンプトの書き方をどう工夫しても「絶対に守らせる」のは無理筋だなと判断して、方針を変えた。見出し・画像タグ・アフィリエイトリンクは元々コード側のテンプレートで組み立てていて、LLMが生成しているのは商品紹介文などの地の文だけ。だったら、チェックもレンダリング前の地の文だけに対してかければ、LLMはリンクの存在すら知らずに済む。 # Before: レンダリング済みの全文をまるごとチェックに通す markdown_text = render_roundup_article(state, content) markdown_text = apply_typo_check_local(markdown_text, model=CHECK_MODEL) # After: 導入文・まとめ・各商品紹介文を個別にチェックしてから組み立てる content["intro"] = apply_typo_check_local(content["intro"], model=CHECK_MODEL) content["conclusion"] = apply_typo_check_local(content["conclusion"], model=CHECK_MODEL) for product in content["products"]: product["write_up"] = apply_typo_check_local(product["write_up"], model=CHECK_MODEL) markdown_text = render_roundup_article(state, content) # ここで初めてリンク等を組み込む 地の文だけの短いチェックを何回かに分けて呼ぶ形に変えたので、LLMがURLや見出しの文字列を目にすること自体が無くなった。「プロンプトで頑張って指示する」から「そもそも壊せない構造にする」への転換、という話。ちなみに副産物として、1回の巨大なチェックが409〜748秒かかっていたのが、短い文章を複数回チェックする方式に変えたことで合計118秒まで縮んだ。 おまけ:画像生成を並走させたら今度はGPUがハングした ついでの話だけど、このパイプラインは本文生成(GPU0/1をまたぐ26Bモデル)と、サムネイル画像生成(GPU1固定のStable Diffusion系)を同じマシンで動かしてる。せっかくだから両方を並走させて時間短縮したいよね、と思って組んだんだけど、これがまた沼だった。 Ollamaは一度使ったモデルを、既定で数分間VRAMに乗せっぱなしにする(keep_alive)。26Bモデルは18GB近くあって2枚のGPUにまたがってロードされるので、本文生成が終わった直後にサムネイル生成を始めると、GPU1がOllamaの26Bモデルでほぼ埋まったまま。この状態でPyTorch側(diffusers)がGPU1に新規メモリを確保しようとすると、メモリ不足エラーにすらならず、無期限にハングするという厄介な挙動を実機で確認した。torch/SYCL側とOllama/Vulkan側という、別々のGPUドライバスタックが同じデバイスの空き容量を取り合った時の相性問題っぽい。 対策は「Ollamaにモデルを明示的にアンロードさせてから画像生成を始める」なんだけど、ここにも罠があった。keep_alive: 0 を送ると done_reason: "unload" という応答は返ってくるものの、実際のVRAM解放は数秒〜数十秒遅れることがある。応答を信じてすぐ画像生成を投げると、また同じようにハングしてしまう。なので今は、アンロード要求を送った後に /api/ps をポーリングして、実際にモデルがリストから消えたことを確認してから画像生成ジョブを投げるようにしている。地味だけど、これでハングは再現しなくなった。 まとめ 「プロンプトで禁止事項を書く」だけでは、長文の丸ごと再生成タスクにおいて量子化モデルの逸脱を防ぎきれない。壊されたくない部分は、そもそもLLMの入力に含めない構造にするのが一番確実 複数のGPUドライバスタック(今回はOllama/VulkanとPyTorch/SYCL)を同じマシンで並走させる時は、「APIの応答」と「実際のリソース状態」がズレることがある。信じるのは状態そのものを確認した結果だけにする 自動化パイプラインを組んでると、こういう「一見ちゃんと動いてるように見えて、実は細かいところが少しずつ壊れている」系のバグに時々遭遇するんだけど、今回は実際にdiffを取って原因を特定できたのが収穫だった。同じような構成でハマってる人の参考になれば。

2026年9月7日 · 1 min