quirkbench

ローカル LLM のを測る。

総合スコアで順位をつけるのではなく、そのモデルが何を得意とし、何を苦手とし、どう失敗するかの輪郭を描きます。対象は Ollama で動くモデルだけ。

課金もレート制限も無いからこそ、同じ問題を seed を変えて何度も回し、ばらつきそのものを癖として測れます。


01

見つかったこと — 日本語で JSON を頼むと、キー名まで訳される

name / age / occupation をキーに持つ JSON を日本語で頼んだとき、6 モデル中 5 モデルがキー名を日本語に訳しました。値は正しく、訳してはいけない識別子まで訳しています。

日本語で JSON を頼んだときにキー名を訳した生成の数 6 モデルそれぞれについて、seed 5 本のうち何本でキー名が日本語に訳されたかを示した図。qwen2.5:1.5b だけが 0 本。 MODEL SEED 1〜5 — 訳した生成 llama3.2:1b 5 / 5 llama3.2:3b 5 / 5 qwen2.5:0.5b 4 / 5 qwen2.5:1.5b 0 / 5 qwen2.5:3b 1 / 5 qwen2.5:7b 5 / 5 これは 1 ケースの内訳です。次元をまたいだ合計ではなく、モデルの優劣でもありません。 同じモデルでも、別の日本語 × 形式制約のケースでは勝敗が入れ替わります。
訳さないのは qwen2.5:1.5b だけ。いちばん大きい qwen2.5:7b は 5 本すべてで訳しました。

これは「日本語が苦手」ではありません。qwen2.5:7b を例に取ると、同じ課題の英語版は 5 本すべて正解で、コード生成の日英差はありませんでした。壊れているのは日本語そのものではなく、日本語 × 形式制約の組み合わせです。

そして量子化を替えても動きませんでした。llama3.2:1bQ8_0 から Q4_K_M に差し替えて測り直しても、6 モデルの内訳は同じままでした。


02

測らないもの

先に書きます。ここを誤解したまま数字を読まれるのが一番まずいからです。


03

測る次元

次元問い方採点
instruct形式制約つきの依頼(schema・列挙値・件数)schema 検証 × 期待値の一致
code-gen関数を書かせるサンドボックスで実行する
code-fix壊れた関数を直させるサンドボックスで実行する
code-readバグのある行番号を答えさせる正規化してから完全一致
extract文章から JSON を作らせるキー単位の一致(構造の門は置かない)
reason答えが一意な数値問題最後の数値を採り、許容差で比較
longctx長文に事実を埋めて問う(針の位置は 3 通り)答えを含むか
ideatek 個の案を出させる制約内多様性(上記)

reason が「最後の数値」を採るのは、素の数値だけを正解にすると測るのが算数ではなく指示追従になるからです。指示追従は instruct が測る次元なので、両方に入れると同じものを二重に数えてしまいます。


04

どう測るか — 生成と採点を分ける

全次元を複数モデルで回すと数時間かかります。そのため生成・採点・集計を別のコマンドに分け、採点は生成を書き換えません。

生成・採点・集計の 3 段構成 qb run が生成を書き、qb score が採点を追記し、qb report が集計する。採点は生成を書き換えない。 qb run モデルを回して出力を書く runs/<run>/ responses.jsonl qb score 採点を追記する 生成は書き換えない — 期待値を直したら採点だけ回し直せる qb report 輪郭を出す Ctrl-C で止めても、同じ run 名で再実行すれば続きから再開します
期待値や schema を直したら qb score をもう一度回すだけで済み、数時間かけた生成はそのまま残ります。

05

分かっている限界

設計上の限界を先に書きます。直っていないものも、測って出しています。

脱線ゲートの順序が逆転している お題との埋め込み類似度で足切りしているが、屑の案がまともな店名より高い値を取っており、閾値をどこに置いても分離できない。 案とお題の埋め込み類似度 — 高いほど「お題に沿っている」と判定される 0.26 0.36 閾値 0.35 グラウンズ 0.2782 すずらん亭 0.3196 8番 ← 屑 0.3234 屑がまともな店名より高い。順序が逆転しているので、閾値をどこに置いても分離できません。
原因は短い固有名詞の埋め込みが不安定で、類似度が意味ではなく文長と形式に引きずられること。この足切りは ideate のスコアを動かし、あるモデルの順位を 2 つ変えています(推定の下限)。直っていませんが、どれだけ効いているかは qb gate-audit が測ります。
ideate のスコアは 3 つの積で、1 つでも 0 なら 0 に落ちる valid_rate と coverage と diversity の積。案が 1 件以下まで削られると多様性が 0 になり、スコアは比例減ではなく崖のように 0 に落ちる。 valid_rate 件数を守ったか × coverage 観点を含むか × diversity 案が散っているか 案が 1 件以下まで削られると 0 積なので、どれか 1 つが 0 ならスコアは 0 になります。比例して減るのではなく、崖のように落ちます。 実測では、ある課題の 2 モデルで 6 割の生成がこの崖に落ちました。どの生成が落ちたかは qb report に出ます。
支離滅裂な出力ほど案同士が散らばるので、多様性だけを見ると壊れたモデルが最高得点を取ります。積にしてあるのはそのためです。
num_ctx が足りないと、ollama は警告せずにプロンプトを切り詰める 7821 文字のプロンプトを num_ctx 4096 に投げると 2050 トークンしか入らず、モデルは事実を渡されないまま答える。 投げたプロンプト — 7,821 文字 実際に入った分 — 2,050 トークン ここから先は黙って捨てられる モデルの答え —「文中に記述はありません」 そのまま採点すると「長文脈で事実を保持できない」という結論が出ますが、実際には事実を渡していません。
ollama はエラーも警告も出しません。そのため qb lint-cases が静的に、qb run が実測値で二重に止めます。

06

使い方

Python 3.11 以上と uv、動いている Ollama、測りたいモデルが ollama pull 済みであることが要ります。

# 埋め込みモデルを入れる(ideate 次元の採点で使う)
ollama pull bge-m3
uv sync

# 生成を実行する
uv run qb run --models qwen2.5:0.5b,qwen2.5:1.5b --repeats 5

# 採点して、輪郭を出す
uv run qb score --run main
uv run qb report --run main

中断してかまいません。Ctrl-C で止めても、同じ --run 名で再実行すれば続きから再開します。


07

続きを読む