ゼロから作るコーディングエージェント【第5回】ローカルLLMを一級市民にする。16GBのGPU2枚でWebシステムを自走で作らせるまで

RTX A5000(24GB)1枚で始めて、ローカルモデル7種類が全滅。RTX A4000(16GB)2枚にQwen3.6-35B-A3BをAWQ 4bitで載せ、fp8のKVキャッシュと98kの文脈で、認証つきWebシステムを3本連続93.8〜96.5点で作らせるまでの構成と落とし穴。モデルの癖、量子化の劣化、文脈長、壊れたツール呼び出しの直し方を、測った数字で書きます。

ゼロから作るコーディングエージェント【第5回】ローカルLLMを一級市民にする。16GBのGPU2枚でWebシステムを自走で作らせるまで

こんにちは!

この連載の実験は、すべて手元のGPUで動くローカルLLMでやっています。

最初に用意したのはRTX A5000(24GB)のカード1枚でした。

そこに載るモデルを7種類試して、認証つきのタスク管理Webを作らせた結果は、7種類すべて失格でした。

起動しないものを書き続ける。禁止した外部パッケージを入れる。データベースのAPIを毎回間違える。出力が同じ単語の繰り返しに崩れる。

結論から言うと、いま弊社の手元で「Webシステムをゼロから自走で作れる」ローカル構成は1つ確定しています。

RTX A4000(16GB)のカード2枚にQwen3.6-35B-A3B(MoE、活性3B)をAWQ 4bitで載せ、KVキャッシュを8bitにして98kの文脈を確保する構成です。

同じ課題を3本走らせて94.9、93.8、96.5点。ただし、この構成が確定した時点で、ハーネス側の欠陥を64件直していました。モデルを変えるだけでは届きませんでした。

図1 16GBのカード2枚で98kの文脈を載せる(作図: Qualiteg)
図1 16GBのカード2枚で98kの文脈を載せる(作図: Qualiteg)

この記事は連載「ゼロから作るコーディングエージェント」の第5回です。今回はローカルLLMの話に絞って、モデルの癖、GPUメモリ、文脈長、壊れたツール呼び出しの直し方を、測った数字で書きます。

回テーマ
第1回「最後までやりきる」が難しい理由。ターンを回す係と止める係を分ける
第2回完了は「作った」ではなく「動いた」で決める。完了の門と押し戻しの書き方
第3回「止まらなかった」は「進んだ」ではない。空転を数え、思考は切らずに上限だけ置く
第4回自走の規律は人がいない場面のもの。人がいるなら止まって聞く
第5回(今回)ローカルLLMを一級市民にする。16GB×2でWebシステムを作らせるまで
第6回評価をどう作るか。動くものを作らせて自動採点し、採点器そのものを疑う
第7回記録と回帰。全部イベントログから見つかる。そして依存0で作る

なぜ、小さなモデルと小さなGPUなのか

先に、なぜこの組み合わせなのかを書きます。弊社がこのエージェントで狙っているのは、手元の1台で、Webシステムを最後まで作りきるコーディングエージェントです。大きなクラウドモデルで動くものを作るだけなら、第1回から第4回の話で足ります。この回で扱うのは、その上で「小さなモデルと小さなGPUでも、同じことがちゃんとできる」ようにする、という課題です。

理由は3つあります。

1つ目は、コードを外に出せない現場があることです。顧客のソースコードや、契約で社外に送れないデータを扱う開発では、クラウドのAPIにそのまま渡せません。社内のワークステーションで完結するエージェントなら、この問題がそもそも起きません。

2つ目は、費用の形です。自走するエージェントは、1つの課題で40〜144ターン回り、そのたびに長くなった履歴を丸ごと入力に載せます。あたりまえっちゃ、あたりまえですが、従量課金のAPIでは、この繰り返しがそのまま料金に積み上がります。手元のGPUなら、機材は買った時点の固定費で、走らせるたびの料金はありません。しかも小型GPUなので個人でも手が届きやすいです。

3つ目は、小さいモデルで動かすとハーネスの弱点が全部見えることです。賢いモデルは、ハーネスの穴を自分の判断で埋めてしまうので、欠陥が隠れます。小さなモデルは埋めてくれません。この回で出てくる「ハーネスの欠陥を64件直した」の多くは、小さなモデルが落ちたことで見つかったものです。小さなモデルで見つけた欠陥の直しは、大きなモデルを載せるときにも無駄になりません。完了の判定、空転の検出、ツール呼び出しの検証は、モデルの賢さとは別に要る仕組みだからです。

機材を「16GBのカード2枚」に置いたのも同じ理由です。データセンター用のGPUではなく、机の下のワークステーションに挿せるカードの範囲で、何ができて何ができないかを確かめたい。

RTX A4000は1スロットで、電源も冷却も普通のPCで足りるカードです。その2枚で、認証つきのWebシステムを人が見ていなくても作りきれる構成を、この回では1つ出します。

1. 「一級市民」とは、中核が接続先の語彙を知らないことです

まず設計の話を1つだけ。

ローカルLLMを載せるとき、いちばん最初に決めたのは、エージェントの中核が特定の接続先の語彙を知らないことでした。中核が扱うのは「本文」「ツール呼び出し」「ツールの結果」の3種類の正規化されたブロックだけ。クラウドのAPIも、ローカルの推論サーバも、それぞれのアダプタが自分のワイヤーフォーマットへ双方向に変換します。

(擬似コード)中核とアダプタの境界
core  ──▶ ModelClient(抽象)
             ├─ クラウドA用アダプタ
             ├─ OpenAI互換API用アダプタ   ← ローカルの推論サーバはここ
             ├─ クラウドB用アダプタ
             └─ テスト用の偽アダプタ(ネットワーク不要)

新しい接続先を足すときは、アダプタを1クラス追加するだけ。中核とツールは1行も変えない。変えたくなったら、抽象が漏れています。

なぜこれを最初に決めたか。ローカルモデルは、クラウドのモデルより「壊れた応答」を返すことが多いからです。JSONが途中で切れる。思考が本文に混ざる。ツール呼び出しの引数の順番が違う。こういった癖への対処をアダプタに閉じ込めておかないと、中核が特定のモデルの癖で汚れていきます。

2. 試したローカルモデルと、1つの対照

同じ課題(認証つきタスク管理Web、API12本、SQLite、画面、テスト)で走らせた結果です。最初にRTX A5000(24GB)1枚で試した7種類は、すべて失格でした。表に載せた35B-A3B以外の6種類と、表に無い旧世代のQwen3-30B-A3B(禁止した外部パッケージを入れる癖が直りませんでした)です。そのうち27B、Coder版、Gemma 4はRTX A4000×2でも走らせ直し、本命の35B-A3Bは16GB×2で初めて試したモデルです。表には両方の構成の結果を載せています。

図2 同じ課題での、ローカルモデル別の得点(作図: Qualiteg)
図2 同じ課題での、ローカルモデル別の得点(作図: Qualiteg)
モデル量子化/形GPU文脈得点1ターン一言
Qwen3.6-35B-A3BAWQ 4bit/MoE16GB×298k94.9 / 93.8 / 96.525〜36秒本命。仕様の細部まで読む
Qwen3.6-27Bint4/Dense16GB×298k75.554秒合格するが遅い
Qwen3-Coder-30B-A3BAWQ 4bit/MoE16GB×2、24GB98k、32k失格×914〜24秒速いが、DBのAPIを毎回間違える
Gemma 4 26B-A4BAWQ 4bit/MoE24GB、16GB×232k、98k9 / 11.5不定起動しないものを書き続ける
Devstral-Small-2 24BAWQ 4bit/Dense24GB32k失格不定出力が同じ語の繰り返しに崩れる
Qwen3-14BAWQ/Dense(思考型)24GB32k失格不定思考が履歴の93%を占めた
Qwen3-4B-Instruct無し24GB32k失格不定起動と画面はできる。APIは0
対照 クラウドの小型有料モデルAPI無しAPI側89.5未計測33ターンで完了。欠陥修正前の古いハーネス

対照のクラウドモデルについて1つ注意があります。この89.5点は、ハーネスの欠陥を16件直した段階の古いハーネスでの数字です。同じ古いハーネスでローカル勢は、APIの配点35点のうち最高でも6.4点で全滅していました。ローカルの35Bの3本(94.9 / 93.8 / 96.5)は、欠陥を64件まで直した段階のハーネスでの数字です。その後も直し続けて累計105件になった最新のハーネスでは、同じ課題で98.3点でした。版が違うので、モデルの優劣の比較にはなりません。読み取れるのは「賢いモデルを載せるだけでは届かない」ことのほうです。

2-1. 本命の35B-A3Bは、仕様書を読みます

他のモデルが落とす status の既定値やクエリの並び順まで実装します。3本目は、APIの確認22項目がすべて通りました。

ただし弱点もあります。同じTODOを11ターン保存し直す。27KBを1回で書こうとして切られる。READMEを書き忘れる。ターン数は40〜144とばらつきますが、得点は平均から±1.5以内に収まります(第3回)。

2-2. Coder版は「速いが知らない」

Coder版は1.5〜2倍速い(14〜24秒/ターン)のですが、Node.js標準のSQLiteモジュールのAPIを9本すべてで間違え(open という関数は無く、正しくは DatabaseSync クラス)、動くものになりませんでした。知識の欠落はハーネスでは埋まりません。

ただし、課題の形を変えると別の顔が出ます。既存コードの不具合修正なら12ターン、35秒で92.5点。既存コードへの機能追加は20ターン、70秒で96.7点。CLIのゼロ構築は70点。背景処理つきサーバは24.3点(ワーカーが一度も動かないまま完了報告)。

課題の形で4段階に分かれる(Web 0/背景処理 24/CLI 70/修正と機能追加 92〜97)。1つの課題の点数で「使える」「使えない」を言うのは危険で、弱いモデルでも「構造が既にある仕事」なら実用になります。この話は第6回で「課題の種類を増やす」として書きます。

2-3. 量子化の劣化は「モデルが悪い」に見えます

Devstralの4bit版は「Let me create files are files as files are files...」と同じ語の繰り返しに崩れました。空転33%という数字だけ見ると能力不足に見えますが、原因は量子化と長い出力の相性です。同じモデルの別の量子化では24GBで重みだけで枠を使い切り、起動できませんでした。

第3回で書いたとおり、空転が30%を超えるランの低得点を、モデルの能力のせいにしてはいけません。量子化の版を変えるだけで結果が変わることがあります。

3. 16GB×2で98kの文脈を載せる

本命の構成です(冒頭の図1)。

モデルはcyankiwiのAWQ 4bit版です。Qwen公式の量子化版はBF16とFP8だけで4bitは無く、16GB×2に載せるにはサードパーティの量子化版を使うことになります。cyankiwiはQwenやGemmaのAWQ版を継続して公開している量子化の専門アカウントで、NVIDIAのJetson AI Labの手順でも同じモデルが使われています。重みはsafetensors形式で配られていて、読み込みに独自のPythonコードは要りません。

# 推論サーバ(vLLM 0.28)の起動。16GB × 2、WSL2 上の Ubuntu
export NCCL_P2P_DISABLE=1 NCCL_SHM_DISABLE=1 NCCL_CUMEM_ENABLE=0 NCCL_IB_DISABLE=1
vllm serve cyankiwi/Qwen3.6-35B-A3B-AWQ-4bit \
  --tensor-parallel-size 2 --kv-cache-dtype fp8 \
  --max-model-len 98304 --gpu-memory-utilization 0.92 \
  --max-num-batched-tokens 2048 --max-num-seqs 4 \
  --compilation-config '{"cudagraph_capture_sizes":[1,2,4]}' \
  --reasoning-parser qwen3 --tool-call-parser qwen3_coder --enable-auto-tool-choice

重みを4bitにして2枚に分け(テンソル並列)、KVキャッシュを8bitにして、文脈を98,304トークン確保しています。ここまでに踏んだ落とし穴を表にします。

落とし穴何が起きるか対処
--enforce-eager(CUDAグラフを切る)6.8トークン/秒、63秒/ターン。実用外CUDAグラフを絞って有効化。同時実行4、捕捉サイズ1/2/4。グラフ用メモリ0.51GiBで収まり30〜39トークン/秒
メモリ利用率 0.95起動できない(空き14.77/16GiB)0.92
0.92 と既定のCUDAグラフ「KV用メモリ無し」上の絞り込み
文脈長 131072KVに3.0GiB必要で載らない98304(KV 2.42GiB)
PyPI既定のホイール(cu130)手元のドライバ(CUDA 12.8)で "driver too old"cu129版のホイールを指定
WSL2でテンソル並列NCCLが止まるNCCL_P2P_DISABLE=1 NCCL_SHM_DISABLE=1 NCCL_CUMEM_ENABLE=0 NCCL_IB_DISABLE=1
TritonのJITヘッダが無いpython3-dev
背景で起こすnohup がWSLのセッション終了で止まるWindows側からプロセスを作って前景を保持

いちばん効いたのは1行目です。メモリが足りなくてCUDAグラフを切ると、速度が5分の1になります。切るのではなく、同時実行数と捕捉サイズを絞って有効にしたまま載せる。この違いで、1ターンが63秒から30秒前後になりました。

WSL2でのNCCLの問題は、以前に別の記事で詳しく書いています(関連記事を参照)。

4. 文脈長は「載る」だけでなく「循環を起こさない」ために効きます

同じ27Bのモデルを、24GB1枚で16kの文脈と、16GB×2で98kの文脈で走らせました。

図3 短い文脈は読み直しの循環を生む(作図: Qualiteg)
図3 短い文脈は読み直しの循環を生む(作図: Qualiteg)

16kでは、4つのファイルを48回ずつ読み直す圧縮の循環に入って打ち切り。98kでは75.5点で合格しました。

仕組みは第3回で書いたとおりです。読んだ結果が窓に溜まり、圧縮で落ち、落ちたのでまた読む。32kの24GB単機では300ターンで圧縮が6〜54回、98kでは0〜5回でした。文脈長は「モデルが載る」だけの話ではなく、読み直しの循環を起こさない長さかどうかの話です。

もう1つ、地味な落とし穴を書きます。推論サーバが98,304で待ち受けているのに、ハーネス側がモデルのカタログにある32,768を使っていました。3分の1で圧縮が始まります。文脈長はカタログではなく、サーバの /v1/models が返す max_model_len を見て、起動時に差し替えるようにしました。出力枠も窓に合わせて広げます。

5. 思考モデルの落とし穴。比較の前に、同じ扱いか確かめる

思考(reasoning)を出す型のモデルを載せたときの話です。

推論サーバは、--reasoning-parser を指定したときだけ、思考を本文とは別の欄に分離して返します。指定が無いと <think> タグが本文に混ざって届きます。最初の版はそれを知らず、思考が履歴の93%を占めていました。モデルの応答のほとんどが思考で、それが全部履歴に積まれ、次のターンの入力になっていたわけです。

サーバの設定に依存しないよう、ハーネス側で本文から <think> を剥がす層を入れました。

ここで大事なのは、比較する前に、比較する両者が同じ扱いを受けているか確かめることです。この件では、思考モデルだけが不利になっていました。「14Bは弱い」という結論は、ハーネスの欠陥が作った結論でした。

6. 壊れたツール呼び出しは、引数の形では判定できません

ローカルモデルとクラウドモデルで、いちばん扱いが違ったのがここです。

図4 出力上限で切れた呼び出しは「引数不正」に化ける(作図: Qualiteg)
図4 出力上限で切れた呼び出しは「引数不正」に化ける(作図: Qualiteg)

あるモデルは、ファイル書き込みの引数で content(本文)を先に、file_path(パス)を後ろに出す癖がありました。出力上限(当時4096)で途中で切られると、パスが落ちます。ここまでは想定内です。

想定外だったのは、推論サーバのパーサが切れたJSONを閉じて返すことでした。ハーネスには、形の整った「パス無し」の引数として届きます。ハーネスは「引数不正」と差し戻し、モデルは同じ大きさの書き込みを18回繰り返しました。切られた事実が、どこにも残っていなかったからです。

対処は、引数の形ではなく finish_reason と出力トークン数から「応答全体が上限に当たったか」を判定すること。当たっていれば、書き込み系のツールは書かずに「分割せよ、短い引数を先に」と返します。

ここで1つ、決めたことがあります。値の途中で切れた引数は修復しません。閉じて成功扱いにすれば動いたように見えますが、切れた本文をファイルに書き込むことになり、中身が捏造されます。壊れたものは壊れたと返して、モデルにやり直させます。

ローカルモデル向けに効いた他の対処も2つ書いておきます。

編集ツールは、置換の照合を「完全一致 → 空白の違いを許容 → 前後のアンカーで探す」の順にフォールバックさせています。ローカルモデルはインデントを揺らすので、完全一致だけだと編集の失敗率が跳ね上がります。

ツールの数は19個に絞りました。参考にした実装には42個ありましたが、小さなモデルは選択肢が多いと選択そのものを誤ります。

まとめ。モデルとハーネスの両方が要る

見ること測った結果
GPUメモリ16GB×2で、4bitの35B-A3B + fp8 KV + 98k文脈。CUDAグラフは切らずに絞る
文脈長16kでは読み直しの循環、98kで合格。サーバに聞いて差し替える
モデルの形MoE 35B(活性3B)が本命。Dense 27Bは合格するが2倍遅い。Coder版は知識の欠落で失格
量子化版によって同じ語の繰り返しに崩れる。空転30%超の低得点はモデルのせいにしない
思考モデルパーサの指定が無いと思考が本文に混ざる。ハーネス側で剥がす
ツール呼び出し出力上限で切れた呼び出しは形が整って届く。上限に当たったかで判定し、切れた引数は修復しない
課題の形同じモデルでもWeb 0/背景処理 24/CLI 70/修正と追加 92〜97

「賢いモデルを載せれば動く」でもなく、「ハーネスを鍛えれば弱いモデルでも動く」でもなく、両方が要りました。16GB×2の構成が確定したのは、ハーネスの欠陥を64件直した後です。

次回は、この連載の数字をすべて出している評価装置の話です。動くものを作らせて自動採点する仕組み、5種類の課題を3本ずつ走らせる合否の基準、そして採点器そのものが3日間ずっと壊れていた話を書きます。

それでは、また次回、お会いしましょう!

関連記事

Read more

Claudeのリセット権(上限のリセット)とは?Fableリセット・週次リセットで戻るものとChatGPTとの違い

Claudeのリセット権(上限のリセット)とは?Fableリセット・週次リセットで戻るものとChatGPTとの違い

Claude Fable 5.2 のリリース記念で配られたリセット権(上限のリセット)をご紹介します。使った利用枠を定期リセットを待たずに戻せる権利で、Fable リセットで戻るのは Fable で使った分です。週次リセットとの違い、もらえる件数、プランごとの期限、注意点、ChatGPT のリセットとの違いを、公式ヘルプと実際の画面をもとに整理します。

By Qualiteg プロダクト開発部
ゼロから作るコーディングエージェント【第6回】評価をどう作るか。動くものを作らせて自動採点し、採点器そのものを疑う

ゼロから作るコーディングエージェント【第6回】評価をどう作るか。動くものを作らせて自動採点し、採点器そのものを疑う

採点器の「テスト」10点が3日間ずっと0/0でした。点数の表を見ているだけでは気づけません。仕様書を渡してWebシステムを作らせ、起動して叩いて採点する評価装置の作り方、5種類×3本の合否基準、対照実験を先にやる理由、自作でない課題で「テストだけが決めている契約」を先に洗う話を、測った数字で書きます。

By Qualiteg プロダクト開発部
大企業のAIセキュリティを支える基盤技術 - 今こそ理解するActive Directory 第7回 セキュリティとベストプラクティス - 本番環境での考慮事項

大企業のAIセキュリティを支える基盤技術 - 今こそ理解するActive Directory 第7回 セキュリティとベストプラクティス - 本番環境での考慮事項

こんにちは、今回はシリーズ第7回セキュリティとベストプラクティス - 本番環境での考慮事項 について解説いたします! 連載の構成 * 第1章:基本概念の理解 - Active DirectoryとKerberos/NTLM認証の基礎 * 第2章:ドメイン環境の構築 - 検証環境の構築手順 * 第3章:クライアントとサーバーのドメイン参加 - ドメイン参加の詳細手順 * 第4章:プロキシサーバーと統合Windows認証 * 第5章:ブラウザ設定と認証 - 各ブラウザでの設定方法 * 第6章:トラブルシューティング - よくある問題と解決方法 * 【★今回です★】第7章:セキュリティとベストプラクティス - 本番環境での考慮事項 * 第8章:実践的な構成例 - AIセキュリティツールとの統合事例 第7章:セキュリティとベストプラクティス 第 7 回は、ここまでの回で作った認証基盤を「本番で運用できる状態」にするための話です。 前半の 7.1

By Qualiteg コンサルティング