合意は「同じ答え」でなく「同じ話題」を測っていた ― Phase 3 の合意エンジンを、作る前に計測して畳んだ話

次の大物になるはずだった「合意エンジン」

この分散基盤には、まだ作っていない目玉機能があった。Phase 3 の「合意エンジン」だ。同じ問いを複数の異なるモデルに解かせ、答えが一致すれば信頼して自動採用し、割れたら人間なり上位経路に回す ― いわゆる多数決・自己整合性(Self-Consistency)の発想である。第33回で「best-of の judge は自分の知らない正解を選べない」と分かった後、その代わりに据えようとしていたのが、この「合意を信頼度信号に使う」路線だった。

設計骨子はこうだった。R個の候補回答を bge-m3(埋め込みモデル)でベクトル化し、コサイン類似度でクラスタリングする。大きな塊にまとまれば「高合意=自動決定してよい」、バラければ「係争=エスカレーション」。judge を介さないので、judge の知識限界に縛られない ― はずだった。

だが、作る前に測ることにした。β(検索スコアによる自動ゲート)を実装せずに済ませたときと同じ判断だ。あのときは rag_probe_beta.py でスコア分布を測り、「被覆ありのスコアが無関係のスコアより低く出て逆転する=分離しない」と分かって棄却した。合意エンジンも同じ危険を抱えている。「合意スコアが本当に正しさと相関するのか」は、配線する前に数字で確かめられる。計測してから信じる

計測ハーネス ― consensus_probe.py

coordinator 本体には一切触らない、計測専用のスクリプトを書いた。やることは単純だ。固定した probe 集合を、学習出自の異なる2つの8Bに解かせ、両者の回答を bge-m3 で埋め込んでコサインを測る。probe は4カテゴリに分けた。

  • A_agree:Tier-A の事実質問(systemd の override.conf 位置、apt update と upgrade の違い、Python の逆順)。両者が正答し一致するはず=合意が効くべき場所。
  • B_correlated:Tier-B の言葉遊び(二羽「にわにはにわにわとりがいる」、早口言葉「すもももももももものうち」)。両8Bが同じ向きに外す相関誤差が起きうる、いちばん危険な場所。
  • dispute:正解が一意でない開放質問(分散システムで最も重要な原則)。competent な相違が出るはず。
  • general:合意フロアの sanity(日本の首都)。

候補生成は異系統8Bの Ollama 直叩き。.43 の elyza-jp-8b(Llama-JP 出自)と .196 の qwen3:8b(Qwen3 出自)だ。ここで正直に書いておくと、この 16GB のハードで意味のある「異系統2way」が成立するのは、.43 と .196 が両方健全なときだけだ。.43 は 8B を同時1つしか載せられず(第37回の num_ctx 実測のとおり)、.196 は best-effort で普通に落ちる。合意エンジンが常時回ることは、そもそもこのハードでは無い。「2台健全なときだけ日和見的に多様化する」が上限 ― これが opportunistic の実体だった。

途中で踏んだ穴:qwen3 が thinking で予算を食い潰す

初回 run で、肝心の B_correlated が測れなかった。qwen3 の回答が「Python で…」8字で切れ、二羽でも空応答になる。 FAILED: list index out of range も出た。埋め込みのバグを疑ったが、embed を単体で叩くと短文も535字も正常。真犯人は生成側だった ― qwen3:8b が <think> の思考に num_predict の予算を使い切り、本文が空になっていた。think:false を付けたら、同じ質問が8字から269字に回復した。空応答は候補から除外するガードも足して、v1.1 とした。ここでも「エラーの出た場所(embed)と真因の場所(生成)は違う」という、いつもの切り分けだった。

核心の結果 ― 合意スコアは「正しさ」を測っていなかった

正誤を自動ラベルする正規表現マーカー(二羽なら「二羽/2羽」に到達しているか、すももなら「すもも」を割らずに保持しているか)を各 probe に仕込み、「合意スコア × 正誤」を1枚の表にした(v1.2)。judge は信用しない設計なので、正誤の物差しは人間が定義したマーカーだ。出てきた表がこれである。

  id   cat            pair_cos  verdict
  A1   A_agree        0.9187   split          ← 高合意なのに片方誤り
  A2   A_agree        0.8806   both_correct
  A3   A_agree        0.9645   both_correct
  B1   B_correlated   0.8773   both_wrong     ← 高合意なのに両方誤り(二羽)
  B2   B_correlated   0.7918   both_correct   ← 両方正解なのに最低スコア
  D1   dispute        0.7638   n/a
  G1   general        1.0000   both_correct

  🔴 高合意(≥0.80)なのに both_correct でない probe: ['A1', 'B1']

いちばん見たかった行は B1 だ。二羽で elyza も qwen3 も数詞「二羽」に到達せず(両者とも誤り=both_wrong)、それでいてコサインは 0.877。A_agree の平均 0.921 とほとんど同じ帯にいる。もし「高合意→合意採用」を実装していたら、この誤答を、judge にも人間にも通さず、自動で「正解」として出していた。bge-m3 は「両者とも二羽・言葉遊びについて語っている」という話題の一致を 0.877 と測っただけで、答えの中身が正しいかは一切見ていない。

合意スコアは「同じ答えか」ではなく「同じ話題か」を測っていた。 これが今回の一行結論だ。

補強もそろっている。A1(systemd)は Tier-A の事実質問なのに、片方が .service.d に触れず外して split になったが、コサインは 0.919。事実を外しても高合意に出る。そして皮肉なことに、両者とも正解した B2(すもも)が 0.792 で全 probe 中最低だった ― 正しく答えても、表現の差でコサインは開く。この標本では、合意スコアと正しさは相関していないどころか、軽くを向いていた。both_wrong(0.877) も both_correct(0.792〜1.0) も 0.79〜1.0 の同じ帯に潰れていて、閾値では分離できない。β のスコア逆転と、寸分違わぬ構図だった。

Phase 3 の判断 ― 合意採用は棄却、係争検知に格下げ

結論はこうなった。

「高合意→合意採用(judge 不要)」は棄却。 高合意が正しさを保証しない以上、これを自動採用に使えば二羽型の誤答を素通しする。β に続く、2件目の「配線前の棄却」だ。

合意(正しくは非合意)は「明確な低合意→係争検知」の粗いフィルタにだけ格下げ。 ただし dispute(0.764) と、両者正解の B2(0.792) が近接しているので、係争とみなす閾値は 0.75 未満くらいまで下げないと、正解ケースを巻き込んでしまう。実質「めったに焚かない安全弁」程度の役割しか残らない。

Tier-B の本線は、合意ではなく Phase 2 の権威ルーティング(sarashina)+RAG のままだ。これは第37回で実装済みの経路で、今回の計測は「合意エンジンに Tier-B を任せてはいけない」を確定させ、既存の設計をむしろ補強した。best-of も、judge が正解を選べず(第33回)合意も正しさを測れない今、Tier-A の事故フィルタ止まり、という位置づけは変わらない。

負の結論を、配線前に得ることの価値

正直に言えば、これは「Phase 3 の合意エンジンは、このハードとこの埋め込みモデルでは主役になれない」という負の結論だ。次の大物になるはずだった機能が、着工前に畳まれた。

でも、これが計測先行の値打ちだと思う。もし measure せずに合意採用を実装していたら、二羽型の問いで誤答を自動で「正解」として配信する基盤が、静かに出来上がっていた。しかもそれは「複数モデルが合意したのだから正しいはずだ」という、いちばんもっともらしい顔をして紛れ込む。第33回で judge の順位反転を、今回で合意スコアの話題一致を ― 「似ている」は「正しい」ではないという同じ落とし穴を、角度を変えて2度踏まずに済んだ。

類似度は意味の近さを測るのに長けているが、事実の正しさは測れない。埋め込みも judge も多数決も、そこは代われない。正しさの非代替性を埋めるのは、やはり「その答えを知っているモデルへ routing する」しかない ― 第37回で sarashina に通り道を作ったのは、遠回りではなく本線だった、と計測が言っている。次の大物を1つ失った代わりに、既にある経路が本線だと確信できた回だった。