0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

話題のJevをローカルLLMで再現、機密情報の見逃し20件がしきい値だけで2件に

0
Last updated at Posted at 2026-09-28

「本番DBのパスワードは Kx9!mQ2#vL7p です」という引き継ぎメモ(の英語版)を、ローカルのqwen3.5:4bに見せて「機密情報は含まれますか?」と聞きました。返ってきた確率は「いいえ」が69%でした。パスワードそのものを見せて、7割の自信で「何も入っていません」。門番としてはなかなかの度胸です。

鎧を着た門番が腕を組んで得意げに立ち、その横を、金色の鍵が大きく描かれた紙を掲げた配達人が素通りしていく

先日、TypeSafe AIのJevについて、公開情報だけで解説記事を書きました。Jevは文章を生成せず、質問への答えを型と確率で返すモデルで、その記事の最後に私は「機密かどうかの判定はクラウドのJevに送る前に、ローカルで済ませるべき」と書いています。判定のために本文を外へ送ったら、ゲートの意味がありません。

「ローカルで済ませるべき」と書いておいて自分では作っていないのは据わりが悪いので、RTX 4070(12GB)のPCで、Jevのオープン実装とOllamaだけで組んだ判定を比べました。冒頭の69%は、そのOllama側の数字です。ただ、この門番はクビにせずに済みました。しきい値を手元のデータで決め直すだけで、英語の機密20件の見逃しは2件まで減ります。

Jevのオープン実装は24件あった

Jevの公開から2週間ほどで、「Jevを模したオープン実装」がGitHubにいくつも出ています。からあげさんのまとめ記事とawesome-jev、GitHub検索で見つかった24件について、READMEとソースを読みました。確率の出し方で分けると、だいたい次の3種類です。

確率の出し方 例 4070 12GBで
選択肢トークンのlogitを読む rizzo-flow、SemIf-OpenJev 4B級なら動く
判定用のheadを学習した小型モデル Laya、ollaya CPUでも動くものがある
LLMに確率をJSONで書かせる localjev 動くが、確率はモデルの自己申告

3つ目はLLMが「0.87」と書いた数字をそのまま確率として扱う方式です。テストの前に本人へ「何点取れそう?」と聞いて、その答えを点数にするのに近いので、本物の確率が返るというJevの売りから外れます。今回は候補から外しました。

選んだのは次の2つです。

  • rizzo-flow: llama.cppのCUDA版で、4BのオープンモデルSpark-X2.5を判定向けに微調整したものを動かします。JevとAPIが同じ形(/v1/systemone)で、1回のリクエストで複数の質問に答えます。Q8_0で約5.6GiBなので、ほかのLLMと同じGPUに同居できます
  • Ollama + logprobs(自作): 追加のインストールはありません。手元のqwen3.5に1文字のラベルで答えさせ、そのトークンの確率を読みます

依頼をモデルに振り分ける設計図。0段目のローカル機密ゲート、1段目でJevにタスクの性質を聞き、2段目の自分の表でフロンティアかローカルLLMに回す

前の記事で描いた、振り分けの設計図です。今回ローカルで作れるか測るのは、左端の機密ゲート(0段目)と、タスクの種別判定の2つです。図の1段目では「推論の難しさ」なども聞く設計にしていますが、難しさは人によって正解が変わるので、今回は正解がはっきり決まる種別の5択に置き換えました。Jevの仕組みやLLM as a judgeとの違いは、前の記事に図付きで書いています。

試したこと

データセット

依頼文を60件作り、日本語版と英語版を用意しました。

  • 種別(5択): 翻訳 / 要約 / コード作成 / 質問への回答 / 文章作成を各12件
  • 機密(yes/no): 本文に実際の認証情報や個人の連絡先があるかどうか。ありが20件、なしが40件です
  • なしの40件のうち10件は、「APIキーをGitHubに誤ってpushしたら何をすべき?」のように話題としてだけ出るものです。キーワードに反応するだけの判定を落とすために入れました
  • 秘密情報は、パスワード、AWSキー、Slack Webhook、カード番号、マイナンバー、SSH秘密鍵、住所と氏名などです。値はすべて架空です

「難しいタスクか」のように正解が人によって変わる質問は入れていません。

Ollamaで確率を読む

Ollamaはv0.12.11からlogprobsを返します。num_predict: 1で1トークンだけ出させ、上位の候補からYとNの確率を拾います。

body = {
    "model": model, "stream": False, "think": False,
    "logprobs": True, "top_logprobs": 20,
    "options": {"temperature": 0, "num_predict": 1},
    "messages": [{"role": "user", "content": prompt}],
}
d = post("http://<GPU PC>:11434/api/chat", body)
top = d["logprobs"][0]["top_logprobs"]

raw = {lab: 0.0 for lab in labels}          # labels = ["Y", "N"] or list("ABCDE")
for t in top:
    tok = t["token"].strip()
    if tok in raw:
        raw[tok] += math.exp(t["logprob"])
mass = sum(raw.values())                    # 候補ラベルに乗った確率の合計
probs = {k: v / mass for k, v in raw.items()}

注意点が2つあります。top_logprobsの上限は20で、欲張って30を指定したら「must be between 0 and 20」と断られました。また、vLLMのallowed_token_idsのように出力を候補の文字だけに絞る機能は見当たらず、候補以外のトークンに確率が逃げる可能性があります。今回は候補ラベルに乗った確率の合計が全件で0.97以上あったので、取りこぼしは問題になりませんでした。

Ollama方式は、1問ごとに1リクエストです。機密と種別の2問なら2回呼びます。

rizzo-flowのセットアップ

READMEの手順どおり、4行で立ちました。動かしたのはWindows上のWSL2のUbuntuです。READMEが検証済みとしているのはWindows 10のネイティブ環境だけなので動かないことも覚悟していましたが、引っかかるところはありませんでした。身構えた分だけ拍子抜けしました。

git clone https://github.com/Rizzo-AI-Academy/rizzo-flow && cd rizzo-flow
uv sync --locked
uv run rizzo download          # llama.cpp(CUDA版) + 4B Q8_0 モデル 約5GB
uv run rizzo serve --device cuda --host 0.0.0.0

呼び出しはJevと同じ形で、2問を1回で聞けます。

{
  "state": "本番DBのパスワードは Kx9!mQ2#vL7p です。",
  "model": "rizzo-latest",
  "questions": {
    "confidential": {"type": "noul", "instructions": "実際の機密情報が書かれていますか? ..."},
    "task": {"type": "choice", "instructions": "この依頼はどの種類ですか?",
             "criteria": {"translate": "翻訳", "summarize": "要約", "code": "コード作成",
                          "question": "質問への回答", "write": "文章作成"}}
  }
}

起動すると、nvidia-smiのVRAM使用量は約5.7GiB増えました。

結果: 速いのはrizzo-flow、並び順が良いのはqwen

しきい値は0.5に固定しています。つまり、機密の確率が0.5以上なら「機密あり」と判定します。

実装 言語 機密 正解率 見逃し/20 誤検知/40 AUROC 種別 正解率 1件(ms)
qwen3.5:4b 日本語 85.0% 9 0 1.000 100% 595
qwen3.5:4b 英語 66.7% 20 0 0.999 83.3% 593
qwen3.5:9b 日本語 95.0% 2 1 0.996 96.7% 675
qwen3.5:9b 英語 95.0% 3 0 0.999 95.0% 672
rizzo-flow 4B 日本語 86.7% 6 2 0.956 95.0% 187
rizzo-flow 4B 英語 90.0% 5 1 0.984 100% 173

1件あたりの時間は、別PCからTailscale越しに2問を聞いた往復の中央値です。rizzo-flowのサーバー側の推論時間だけなら、中央値は160msでした。

表から読めることは3つあります。

  • 種別の5択は、どれでも足ります。 83〜100%です。4B級で十分でした
  • 速さはrizzo-flowが上です。 2問を1回で聞けるので約180msです。Ollama方式は2回呼ぶので約600msかかります。エージェントの毎ステップに挟むなら、この差は効きます
  • 機密の判定は、見逃しの数だけ見ると全部だめに見えます。 qwen3.5:4bは英語で20件すべてを見逃しました

ただ、AUROCの列を見てください。4bは1.000です。

4Bは確率を信じると全部見逃す、しきい値を決めると化ける

AUROCは、機密ありの依頼に機密なしより高い確率を付けられているか、という並び順だけの指標です。1.000は、機密ありの20件すべてが、機密なしの40件すべてより高い確率になっているという意味です。

つまりqwen3.5:4bは区別はできていて、答え全体が「いいえ」寄りに偏っているだけです。分かっているのに手を挙げない、慎重すぎる新人のような振る舞いです。英語の機密20件では「はい」の確率が最大でも0.47だったので、しきい値0.5では1件も引っかかりません。

そこで、手元のデータでしきい値を決め直しました。60件を5分割し、4つ分で「見逃しが0になる最大のしきい値」を決めて、残り1つで試します。これを5回繰り返した合計です。

実装 言語 0.5固定: 見逃し 0.5固定: 誤検知 較正後: 見逃し 較正後: 誤検知
qwen3.5:4b 日本語 9 0 1 0
qwen3.5:4b 英語 20 0 2 1
qwen3.5:9b 日本語 2 1 1 1
qwen3.5:9b 英語 3 0 2 1
rizzo-flow 4B 日本語 6 2 2 8
rizzo-flow 4B 英語 5 1 1 3

決まったしきい値は、4bの日本語で0.21、英語で0.04です。英語では確率4%を超えたら機密として扱うわけで、人間なら心配性を通り越しています。それでも結果は見逃し2件、誤検知1件でした。このモデルにとっては、4%が「はい」の言い方だったようです。

rizzo-flowは逆の動きをしました。並び順(AUROC 0.956)がqwenより悪いので、見逃しを減らそうとしきい値を下げると、誤検知が増えます。日本語では、機密なし40件のうち8件をゲートで止めることになりました。慎重すぎる新人の次は、翻訳の依頼まで止める門番です。

Jevの独立検証でも、公開ベンチマークではよく較正されている確率が、分布の外のデータでは大きくずれると報告されています。今回の4bはその縮図に見えます。確率は自分のデータでしきい値を決めてから使うものだと前の記事に書きましたが、それを手元の数字で確かめる形になりました。

rizzo-flowが見逃したのは「形の決まった秘密」

rizzo-flowが日本語で見逃した6件は次のとおりです。

依頼(抜粋) p(機密)
送付先は東京都世田谷区…山田花子様です 0.47
クレジットカード 4539 1488 0343 6467 で二重請求 0.16
Webhookは https://hooks.slack.com/services/T04Q… 0.15
このトークンはまだ有効ですか? xoxb-5521… 0.42
マイナンバー 3847 2915 6603 を会社に提出するとき 0.12
-----BEGIN OPENSSH PRIVATE KEY----- b3Bl… 0.09

住所の1件を除くと、どれも形が決まった秘密です。カード番号やSlackのトークン、秘密鍵のヘッダは、gitleaksのようなツールが持っている正規表現のルールで、確率を持ち出すまでもなく確実に拾えます。-----BEGIN OPENSSH PRIVATE KEY-----という、名札を付けて歩いてくる秘密を0.09で見送るのは、モデルに聞く相手を間違えているということです。

仕組みとしてはこう組むのが筋だと考えています。

  1. 正規表現(決定論): カード番号、APIキー、トークン、秘密鍵など、形の決まったもの
  2. ローカルの判定モデル: 住所、氏名、社内の数字など、形の決まらないもの。しきい値は手元のデータで決める
  3. Jevやクラウドの判定: 1と2を通ったものだけ

2を1より手前に置くと、モデルが苦手なものをモデルに聞くことになります。なお、この3段の構成そのものはまだ試していません。今回の結果から考えた設計です。

数字を使うときの注意

この結果を手元の判断に使う前に、次の条件を確認してください。

  • 機密ありは20件です。 見逃しが1件違うだけで5ポイント動くので、実装同士の小さな差は誤差の範囲です。AUROCや決まったしきい値も、傾向として見てください
  • データとラベルは私が作ったものです。 定義は本文に書いたとおりで、第三者の確認は通していません。自分の業務の依頼文では、結果が変わる前提で測り直してください
  • Jev本体は呼んでいません。 「Jevより良い/悪い」の比較には使えません
  • rizzo-flowは既定の設定だけです。 較正機能(--calibration)、1.7B、Q4、ベース重みは試していません。較正機能を使えば、見逃しと誤検知の関係は変わりえます
  • プロンプトは方式ごとに1種類です。 言い回しを変えると、特にしきい値は動きます

まとめ

Jevのオープン実装をRTX 4070で動かし、Ollamaのlogprobsで組んだ判定と比べました。

  • rizzo-flowは5.6GiBほどで動き、2問を約180msで返しました。Jevと同じAPIの形で、同居させやすいです
  • 機密ゲートとしては、qwen3.5:4bのほうが確率の並び順が良い結果でした。ただし、確率を0.5で切ると英語で全件を見逃します
  • しきい値を手元の40件ほどで決めると、4bの見逃しは20件から2件に減りました
  • 形の決まった秘密は、モデルより正規表現のほうが確実です

Jevのような「確率を返す判定」で私がいちばん手応えを感じたのは、確率が返ることそのものより、その確率を自分の40件で較正できるところでした。GPUを回して最後に行き着いたのが「しきい値は0.5とは限らない」という統計の教科書の1ページ目だったのは、少し笑えます。

JevとLLM as a judgeの違い、なぜ公開2週間で話題になったのか、独立検証で見つかった弱点は、こちらの記事にまとめています。

Jevとは?話題の理由5つとLLM判定との差
kenimoto.dev

皆さんは、LLMに渡す前の機密チェックに何を置いていますか? 正規表現だけ、モデルだけ、それとも組み合わせでしょうか。

次の一歩

『ローカルLLMで足りる仕事、Claude Codeに任せる仕事』の表紙。線路の分岐の写真に「依頼の96%は、ローカルに回せなかった。でも、1ステップずつに下ろすと、半分が回せた。」の帯

この記事の判定を、Claude Code の作業全体に広げたのが Kindle 本『ローカルLLMで足りる仕事、Claude Codeに任せる仕事』です。依頼を1ステップずつに分け、ローカルLLMで足りる部分をルールと確率で振り分けます。本記事の機密ゲートとしきい値は第8〜10章で扱い、日本の個人情報を拾う正規表現と、正規表現では拾えない「名札のない機密」まで測っています。付属コードは local-step-router で公開しています。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?