はじめに
自宅の RTX 5090 (32GB) で ollama を回し、opencode から使っている。デフォルトモデルには 長らく dense な Qwen3.6 27B を据えていたが、コーディング特化の MoE モデル Ornith 1.0 35B(35B/A3B)を試したら実測が良すぎた──単発のコード生成は品質同等以上で 約3倍速い(246 tok/s vs 71 tok/s)。即デフォルトを差し替えた。
この記事は、その判断が2日で覆るまでの記録だ。先に結論を言う。 MoE の速さの源泉と、エージェント作業で壊れる原因は、同じものだった。
1日目: ベンチではなく実測で選んだ(はずだった)
ベンチマークは基本自己申告なので、手元の実タスクで横並びにした。 仕様からの関数実装(独立採点14ケース)、仕込んだバグの修正、シェルスクリプトのレビュー。
| タスク | Ornith 35B (MoE) | Qwen3.6 27B (dense) |
|---|---|---|
| 仕様から実装(14ケース) | 14/14・22s | 14/14・58s |
| 仕込みバグ修正 | 正解・24s | 正解・55s |
| スクリプトレビュー | 9指摘・19s | 6指摘・96s |
単発なら文句なしだ。レビューに至っては指摘数で dense に勝っている。 「コーディングは MoE、これで決まり」──この時点ではそう見えた。
2日目午前: 一般タスクでほころびが出る
デフォルトにした以上、調べ物のような一般タスクも通る。そこで妙な答えが返り始めた。
「次の祝日まで何日?」と聞くと、検索はする。だが検索結果のスニペットだけを見て、 存在しない祝日をでっち上げた。「8月1日はこどもの日の振替休日です」──支離滅裂である。
プロンプト規律(本文を読むまで答えるな、日付は必ずツールで確認しろ、日数計算は
シェルの date でやれ)を足すと挙動は矯正されたが、それでもページを誤読して
祝日の日付を間違えた。規律で減らせるが、消えない。
同じ質問を dense の Qwen3.6 に投げると、現在日時をツールで取り、祝日一覧のページ本文を 読み、日数計算を Python で書いて実行し、全問正解した。この時点で嫌な予感はしていた。
2日目午後: 機械判定つきの移植実験
疑いを白黒つけるため、実験を設計した。RFC 6902 (JSON Patch) の Rust 移植だ。 題材選定には理由がある:
- 仕様も参照実装も既にある──モデルに「判断」させる余地が最小
- 公式の conformance suite(言語中立の JSON、112ケース)がある──完了判定を機械にできる
フェーズを T0(テストランナー作成)→ T1(JSON Pointer)→ T2(add/remove/replace)→
T3(move/copy/test)に割り、各フェーズの完了条件を「cargo test が
total=112 pass=N fail=0 を出力すること」に固定した。
モデルの「できました」は一切信用しない。 判定はテスト出力だけ。
結果: Ornith 0勝5敗
5セッション走らせて、Ornith は一つのフェーズも閉じられなかった。壊れ方が興味深い。
- 依存を追加し忘れてコンパイル不能のまま「完了」
- 矛盾した assert 入りのテストランナーを書いた(
filenameが A かつ B であることを 要求する──呼べば必ず panic する)。ついでに tests/ の中に別の cargo プロジェクトを 勝手に init していた - 238行目のコメントの途中で切れたファイルを書いた。切れた箇所には 「let me reconcile:」と思考の漏出が残っていた
- 正しい TODO リストを自分で作り、1項目目の途中でセッションを離脱した
op_add/op_removeの呼び出しだけ書いて、関数本体を書かずに終了した
共通項がある。どれも一見もっともらしい成果物と完了報告を残すことだ。 テスト出力を確認する運用にしていなければ、5回とも「完了」として通っていた。
dense は全フェーズ完走した
同じフェーズを Qwen3.6 に渡すと、遅いなりに全部閉じた。ランナーは一発で正しく書き、 112ケースの集計を出し、最終的に conformance 全ケース green (pass=108 / fail=0 / skipped=4 は公式の無効化分)まで到達した。
dense 側のコツは一つだけ──セッションに収まる粒度までタスクを刻むこと。 71 tok/s + 常時 reasoning は遅いので、診断と修正を同じセッションに入れると時間切れになる。 診断は上流でやり、確定した事実を渡して修正だけさせると速い。
なぜこうなるのか: 速さの源泉と壊れ方の原因は同じ
Ornith がバカなわけではない。単発のレビューでは dense に勝っていたのを思い出してほしい。 総パラメータでも Ornith 35B > Qwen3.6 27B で、負けた方がむしろ大きい。
MoE の 3 倍速は「1トークンあたり active 3B しか働かせない」ことから来る。 そしてその節約が、ターンをまたいで状態を保持する力を削っている──というのが 5敗の並びから引いた推定だ。壊れた5件は全部 「自分がさっき何をしたかを踏まえて次を決める」場面だ──直前の編集を配線する、 作った TODO を消化する、書きかけの関数を書き終える。
つまり境界は「コーディングか一般か」ではない。 「1発で閉じるか、状態を持ち越すか」だ。検索→即答は通る。 検索→抽出→計算は落ちる。生成→配線→テストも落ちる。
落ち着いた構成
- デフォルト = Qwen3.6 27B (dense)。多段でも壊れない方を標準にする。 「多段だと気づかず速いモデルに投げて黙って壊される」という一番厄介な失敗モードが デフォルトごと消える
- Ornith は名前つきの速度レーンに降格。opencode のカスタムエージェントで 「ツール1〜2発で閉じるタスク専用。積み上げが要ると分かったら降りろ」と 焼き込み、単発生成とデバイス制御だけ任せる
- 完了判定は機械に固定。テスト・状態の読み返し・件数固定の集計行。 今回の実験でモデルの完了報告を信用した回数はゼロで、それで初めて成立した
ベンチの点数でも tok/s でもなく、「どこで壊れるか」を自分のタスクで測るのが ローカルLLM選びの本体だと思う。3倍速は惜しい。だが完走しない速さは速さではない。