同じコードをローカル LLM に 51 回監査させたら、多数決は「正しさ」より「しつこさ」を選んでいた

結論

  1. 温度 0・seed 固定・GPU 固定なら、指摘集合の実行間一致(Jaccard)は 1.00。seed を 6 通り変えても出力はバイト単位で同じ=温度 0 では seed は効かない。
  2. 温度 0.7 にすると同じコードでも指摘がばらつく。7B の Jaccard 0.52(行ズレを 1〜2 行許すと 0.80)、1.5B は 0.07。不一致の大半は「同じバグを違う行に指す」ドリフト。
  3. 多数決が残すのは「毎回出る指摘」であって「正しい指摘」ではない。本物のバグでも指す行が割れると過半に届かず落ちる。安全網のつもりが安全網にならない。
同じコードを 51 回レビュー(Jaccard=指摘の重なり)温度 01.00温度 0.7・7B0.52(行ズレ許容 0.80)温度 0.7・1.5B0.07多数決が残すもの毎回同じ行に出る指摘 → 残る(eval 33 票・shell=True 28 票)本物のバグでも指す行が割れる → 過半に届かず落ちる

前提

  • 誰向け: 「1 回では不安だから AI レビューを何回か回して、多数決を取ればいい」と考えている人。
  • 使ったもの: Qwen2.5-Coder 1.5B / 7B(Apache-2.0)を Ollama 0.31.1 で実行。NVIDIA RTX 4060 Ti(CUDA)、量子化 Q4_K_M。
  • 対象コード 4 種: target_a(欠陥 7 件)、clean(欠陥なし・偽陽性の基準)、decoy(危険に見えるが実害の低い構造)、easy(誰でも分かる欠陥 6 件)。正解はコードの外の gt.csv に事前登録し、コード内にもプロンプト内にもヒントを書かない。
  • 反復: 温度 0.7 で N=51(run ごとに seed 0〜50)。温度 0 の基準は N=11。
  • 「同じ指摘」の定義: (行番号, カテゴリ) の複合キー。自然文の言い換えは無視する。埋め込みの類似度は使わない(12 行目の null チェック漏れと 45 行目のそれを同一視するため)。

手順

  1. 出力は Ollama の structured outputs(JSON スキーマ強制)。各指摘は {line_number, category, severity, description}、カテゴリは列挙型。
  2. 温度 0 条件は seed 固定+top_k=1, top_p=1.0 を明示(貪欲デコード)。温度 0.7 条件は run ごとに seed を変える。
  3. 生成(非決定)は著者が 1 回だけ回してログを凍結。集計(決定的)は誰でも同じログから再現できる二層構成。
  4. 集計コードとは別に、生ログから主要数値を独立に再計算する検算スクリプトを用意し、ALL PASS を確認してから公開。

うまくいかなかったこと

踏んだ誤りは 5 つ。③は測定器そのもののバグで、この記事でいちばん役に立つ部分です。

  1. 解答がプロンプトに漏れていた。 欠陥行に # DEFECT: ... とコメントを書いたまま監査対象に渡していた。recall が高くて「良いベンチだ」と喜んだが、モデルはコメントを読んでいただけ。削除したら recall は崩落し、ばらつきは深まった。正解はテスト対象から見えない場所に置く。
  2. CPU だと思っていたら 100% GPU だった。 スレッド数(1 vs 4)を変数にしようとしたが、ollama ps が 100% GPU。num_thread は CPU 推論でしか効かない。
  3. 集計器に、直したそばからバグを入れた。 クラスタリングの補助関数を 1 行に「最適化」したとき、リストを append してから同じ参照を clear する古典的なミューテーションバグを埋め込み、多数決の真陽性・偽陽性のカウントを静かに壊した。気づけたのは、ある指摘数を手で数え直したから。
  4. 自分が「偽陽性」と呼んだものは本物のスメルだった。 decoyshell=Trueeval は Bandit 等が実際に警告する。多数決を生き残った「偽陽性」は、むしろ一貫して正しい指摘だった可能性がある。だから主張は「無害/有害」でなく「しつこさ」で立てた。
  5. 測った本人の記憶が結果を捏造した。 原稿の初期版に「md5 への警告も多数決を生き残った」と書いたが、ログでは 7B・decoy で md5 系の票は 51 回中 0 回(生き残ったのは eval 33 票・shell=True 28 票だけ)。印象で数字が頭の中にできあがっていた。

数値

条件 指摘集合の実行間一致(Jaccard)
温度 0・seed 固定・GPU 固定(4 条件すべて) 1.00(実行間で変動なし)
温度 0.7・target_a・7B 0.52(行ズレ 1〜2 行を許すと 0.80)
温度 0.7・target_a・1.5B 0.07
  • 温度 0 で seed を 6 通り変えても出力はバイト単位で一致。
  • target_a・per-run の recall は 7B 0.31、1.5B 0.05。7B は precision 0.98(出せば当たる)。1.5B は 51 回でファイル 32 行中 25 行を叩く多弁かつ低 recall だったため、主張は 7B で立て、1.5B は対照群。
  • 多数決を生き残った decoy の指摘: eval 33 票・shell=True 28 票・md5 0 票(51 回中)。

まとめ

固定すれば決定的、揺らせば揺れる。そして多数決が見ているのは指摘の「顕著さ」であって「正しさ」ではありません。AI レビューを検証に使うなら、回数を増やすより、正解を先に外へ置き、集計器を自分でも疑い、数値を独立に再計算する側の規律が要ります。

元の記事(手順の全文・データ・再現手順): Zenn(技術版・凍結ログと集計コード)・2026-07-02

AI の出力を検証の合否基準で確かめる設計は Works で受けています

この記事での AI の利用

監査役のモデルはローカル LLM(Qwen2.5-Coder 1.5B / 7B・Ollama)。実験ハーネスと集計コードの下書きに生成 AI を使った。正解ラベル(gt.csv)はコードの外に人間が事前登録し、本文の数値はすべて集計コードとは独立に生ログから再計算して一致を確認した。文章は人間が決めた。