Xでシェア

Writing / 生成AI

N° 21

そのLLM API、本当に指定したモデル?「ランダムな数字」だけでAIの正体を見抜く研究

「ランダムな数字」などの短い回答を繰り返し集め、その分布からLLMの系統や同一性を推定する行動フィンガープリント研究を解説します。論文の結果、Jensen–Shannon divergence、Node.jsによる簡易実装、API監視への応用と限界まで扱います。

APIのモデル名に claudegpt と書かれていれば、本当にそのモデルが動いているのでしょうか。

AI本人に「あなたは本当にClaudeですか?」と聞いても、確認にはなりません。モデルは、システムプロンプトや学習データに従って、指定された名前をいくらでも名乗れるからです。

ところが、「1から100までの数字をランダムに1つ答えて」という、ごく普通の質問を繰り返すだけで、APIの向こう側にいるAIの輪郭が見えてくる可能性があります。

2026年7月11日に公開されたプレプリント『One Token Is Enough: Fingerprinting and Verifying Large Language Models from Single-Token Output Distributions』は、この偏りをLLMの「行動フィンガープリント」として利用しました。

論文の図1では、同じ質問を30回したところ、GPT-4oは42、37、57などに回答が分散し、Claude Sonnet 5は47、Llama 3.3は53に集中しました。Qwen3-Maxは30回すべて42を答えています。

最初に、誤解しやすい点を解いておきましょう。

「1トークンで判別」とは、1回の質問だけでモデル名を特定できるという意味ではありません。1回あたりの回答が1トークン程度で済み、その短い回答を多数集めて分布として照合する手法です。

モデル名は、APIに書かれたただの文字列

アプリからLLM APIを見ると、送信するのはモデル名とプロンプトで、返ってくるのはテキストです。通常、利用者はモデルの重み、内部のロジット、推論サーバーへアクセスできません。

公式APIだけを使う場合でも、モデルの更新や内部ルーティングを完全に観測できるとは限りません。APIアグリゲーター、リセラー、複数の推論プロバイダーを挟む構成では、経路はさらに複雑になります。

論文が想定するのは、安価なモデルへの置き換え、強い量子化、古いチェックポイントへのロールバックといった可能性です。ただし、実際のサービスが不正をしていると一律に主張しているわけではありません。問題は、モデル名という文字列を利用者側から直接検証する手段が少ないことです。

これはAIコーディングにも関係します。同じモデル名なのに、ある日から修正能力が落ちる。通常の経路では成功するのに、フォールバック先ではテストが通らない。サイレントアップデート後に、以前のプロンプトが再現しなくなる。原因はモデルだけとは限りませんが、観測可能なシグナルがなければ切り分けもできません。

この論文を30秒で説明すると

研究者は、165モデル、19のモデルファミリー、53の配信プロバイダーを対象に実験しました。質問は10種類。英語、ロシア語、中国語、アラビア語の4言語で実行したため、合計40個の「質問×言語」条件になります。

収集した回答は326,047件。入力は2,330万トークン、出力は116万トークンで、総費用は34.44ドル、1モデルあたり平均0.21ドルでした。

質問は、数字、文字、単語、色、動物、都市、コイントスなど、長い説明を必要としないものです。回答を正規化してヒストグラムを作り、その分布をモデルの指紋として比較しました。論文だけでなく、データセット収集・分析ソフトウェアも公開されています。

なぜAIは「ランダムな数字」をランダムに選べないのか

LLMは、「ランダム」という概念を理解して内部で公平な100面ダイスを振っているわけではありません。直前までの文章を基に、次に出力するトークンの確率分布を計算し、デコード設定に従って候補を選びます。

その確率分布には、さまざまな履歴が混ざっています。

  • 学習データで数字や単語が登場した頻度
  • 人間が「ランダムらしい」と感じやすい数字
  • 候補を何トークンに分割するかというトークナイザーの設計
  • Instruction Tuningや選好最適化の影響
  • 蒸留や量子化による確率分布の変化
  • temperatureなどのデコード設定
  • 推論サーバー側の実装差
  • 言語や文化による選び方の偏り

「好きな数字を1つ選んで」と100人に聞いても、回答は完全には均等になりません。学校、国、世代が違えば、選ばれやすい数字も少し変わるでしょう。LLMでも、学習経歴や追加調整の違いが「選び癖」として残ります。

temperatureを上げれば出力の多様性は増えますが、元の確率分布が均等になるわけではありません。42の確率がほかの数字より高いなら、繰り返しサンプリングしたときにも42が多く現れます。その偏りこそが、今回の鑑定材料です。

研究者はAIの指紋をどう採取したのか

10種類の質問を用意する

論文の質問セットは次のとおりです。

  1. 1〜100のランダムな数字
  2. 1〜10のランダムな数字
  3. 好きな数字
  4. ランダムな文字
  5. ランダムな単語
  6. ランダムな色
  7. 好きな色
  8. ランダムな動物
  9. ランダムな都市
  10. コイントス

閉じた回答空間を持つ数字やコインと、候補が多い単語や都市を混ぜています。さらに4言語を使うことで、モデルの異なる部分に残った事前分布を観測します。

同じ質問を何度も繰り返す

通常価格のモデルでは、各条件をtemperature 1.0で30回、temperature 0で3回実行しました。高価なモデルではtemperature 1.0を15回に減らしています。

1回の回答だけでは、偶然なのかモデルの癖なのかを区別できません。30回の「42」も、1つの回答ではなく「42が30回中何回現れたか」という頻度に変換して初めて指紋になります。

回答を正規化する

APIからは、同じ意味でも次のような表記が返る可能性があります。

plaintext
Blue
blue
blue.
“blue”

研究では、大文字・小文字、句読点、Unicode、数字体系、言語固有の表記を正規化しました。無効回答、空回答、拒否、reasoningが混入した回答も記録し、指紋へ入れるべきでないものを分離しています。

結果1――AIにはモデルごとの「好きな答え」がある

回答分布は、想像以上に偏っていました。有効な6,572条件における最頻回答の占有率は中央値71%。条件ごとのエントロピーの中央値は1.00ビットでした。

1〜100から均等に数字を選べば、理論上のエントロピーは約6.64ビットです。1ビットという値は、多くの条件で回答が少数の候補へ強く集中していたことを示します。

同じモデルのサンプルを半分ずつに分けたとき、条件単位のJensen–Shannon距離の中央値は0.075でした。異なるモデル同士では0.489です。つまり、同じモデルから採った2つの指紋は近く、別モデルの指紋は離れる傾向が確認されました。

重要なのは、特定の数字そのものではありません。「42を答えたからQwen」と単発で決めることはできません。複数の質問、言語、反復から得た分布全体を比較します。

結果2――回答の癖から「モデルの家系」まで見えてくる

回答分布の近いモデルを並べると、GPT、Qwen、Mistral、Claude、Llamaなど、同じ系統のモデルが近くに集まる傾向がありました。

最も近い既知モデルのファミリーを採用するleave-one-out方式では、163モデルに対するファミリー分類の正解率が59.5%でした。モデル数を考慮したランダム基準は18.4%なので、約3.2倍です。

59.5%は、未知のモデル名を確実に当てられる精度ではありません。大規模な追加学習によってベースモデルの癖が上書きされることもあります。それでも、数字や色を選ばせただけで、モデルの家系が半数以上当たること自体が興味深い結果です。

結果3――約100回の短い質問でモデルを本人確認できる

研究では、信頼できる配信環境から取得した参照指紋と、監査対象エンドポイントの指紋を照合しました。

flowchart LR
    accTitle: LLMモデルを行動指紋で照合する流れ
    accDescr: 信頼できる参照環境と監査対象APIへ同じ短い質問を繰り返し、それぞれの回答分布を作成してJensen–Shannon距離を計算し、閾値と比較して差を判定する。
    A[信頼できる参照モデル] --> C[同じ短い質問を反復]
    B[監査対象API] --> C
    C --> D[参照回答の分布]
    C --> E[監査回答の分布]
    D --> F[Jensen–Shannon距離]
    E --> F
    F --> G[閾値と比較]
    G --> H[一致または要調査]

LLMモデルを行動指紋で照合する流れ

40条件すべてを使った検証では、AUC 0.971、EER 7.3%でした。8条件ではEER 10.6%で、各条件15回なら120クエリです。1条件だけではEER 23.3%でした。

EERは、本物を別物と判断する率と、別物を本物と判断する率が等しくなる地点のエラー率です。値が低いほど見分けやすいものの、7.3%は電子署名のような確実性ではありません。運用上の異常シグナルとしては有望ですが、単独でモデル真正性を証明するものではないと理解する必要があります。

なお、タイトルの「約100回」は8条件×15回という評価設定を丸めた表現です。簡易実験で4条件を30回ずつ実行しても、同じ120リクエストになりますが、論文と同じ精度が得られるわけではありません。

APIの向こう側で見つかった“気になる違い”

研究では、独自の主力モデルとして提供されていたあるエンドポイントが、オープンウェイトのQwenモデルと統計的に区別できないほど近い回答分布を示したと報告されています。

また、同じモデルを提供している34組のプロバイダー比較のうち、10組は、異なるモデルに匹敵するほど大きな分布差を示しました。一方、プロバイダーをまたいだ検証全体ではAUC 0.880で、指紋の多くは配信経路が変わっても残っています。

ここで「別モデルへのすり替えが証明された」と断定してはいけません。モデルの更新、許可された量子化、異なるデコード実装、キャッシュ、配信設定などでも差は生じます。論文も、観測結果を不正意図の主張ではなく、配信された分布の統計的な差として扱っています。

Jensen–Shannon divergenceをざっくり理解する

例えば、次の2つの分布を考えます。

plaintext
モデルA: 42=60%, 47=20%, 53=20%
モデルB: 42=55%, 47=25%, 53=20%
モデルC:  7=80%, 13=20%

AとBはかなり似ています。Cは回答候補から大きく異なります。

Jensen–Shannon divergence(JSD)は、2つの確率分布の違いを測る指標です。底を2とする対数を使えば0〜1に収まり、0なら同一、1に近いほど離れています。AからBを測ってもBからAを測っても同じ値になります。片方にしか存在しない回答候補があっても計算できます。

1回の回答同士を比べるのではなく、頻度を確率へ変換してから分布同士を比べるのがポイントです。

Node.jsで「AIの指紋採取」を試してみる

次は、OpenAI互換のChat Completions APIを想定した簡易実験です。論文の完全な再現実装ではありません。目的は、回答分布の偏りとモデル間の距離を手元で体験することです。

ファイル構成は次の3つです。

  • collect.js: 4条件を30回ずつ呼び出す
  • normalize.js: 回答を正規化して頻度を集計する
  • compare.js: 2つの結果から平均JSDを計算する

回答を収集する

collect.jsjavascript
import { writeFile } from "node:fs/promises";

const baseUrl = process.env.LLM_BASE_URL;
const apiKey = process.env.LLM_API_KEY;
const model = process.env.LLM_MODEL;
const output = process.argv[2] ?? "results.json";

if (!baseUrl || !apiKey || !model) {
  throw new Error("Set LLM_BASE_URL, LLM_API_KEY, and LLM_MODEL");
}

const prompts = [
  { id: "number-100", text: "Name one random integer from 1 to 100. Answer with the value only." },
  { id: "color", text: "Name one random color. Answer with one word only." },
  { id: "animal", text: "Name one random animal. Answer with one word only." },
  { id: "coin", text: "Flip a coin. Answer with Heads or Tails only." }
];

const repetitions = 30;

async function ask(prompt) {
  const response = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
    method: "POST",
    headers: {
      "authorization": `Bearer ${apiKey}`,
      "content-type": "application/json"
    },
    body: JSON.stringify({
      model,
      temperature: 1,
      max_tokens: 16,
      messages: [{ role: "user", content: prompt }]
    })
  });

  if (!response.ok) {
    throw new Error(`API ${response.status}: ${await response.text()}`);
  }

  const data = await response.json();
  return {
    answer: data.choices?.[0]?.message?.content ?? "",
    usage: data.usage ?? null,
    responseModel: data.model ?? null,
    collectedAt: new Date().toISOString()
  };
}

const results = [];
for (const prompt of prompts) {
  for (let run = 0; run < repetitions; run += 1) {
    try {
      results.push({ promptId: prompt.id, run, ...(await ask(prompt.text)) });
    } catch (error) {
      results.push({ promptId: prompt.id, run, error: String(error) });
    }
  }
}

await writeFile(output, JSON.stringify({ model, prompts, repetitions, results }, null, 2));
console.log(`Saved ${results.length} attempts to ${output}`);

max_tokens を1ではなく16にしているのは、数字や引用符がプロバイダーごとに異なる方法でトークン化され、途中で切れるのを避けるためです。実際の出力トークン数は usage で確認してください。APIによっては max_tokens ではなく max_completion_tokens が必要です。

回答を正規化する

normalize.jsjavascript
export function normalizeAnswer(value) {
  return value
    .normalize("NFKC")
    .trim()
    .toLowerCase()
    .replace(/^["'“”‘’「『]+|["'“”‘’」』]+$/g, "")
    .replace(/[.!?,;:。!?、,;:]+$/g, "")
    .trim();
}

export function histograms(document) {
  const output = {};

  for (const row of document.results) {
    if (row.error) continue;
    const answer = normalizeAnswer(row.answer);
    if (!answer) continue;

    output[row.promptId] ??= {};
    output[row.promptId][answer] = (output[row.promptId][answer] ?? 0) + 1;
  }

  return output;
}

本格的な監査では、数字表記、色の同義語、言語固有の表現、拒否回答をより厳密に分類する必要があります。意味の異なる回答まで統合すると、かえって指紋を壊します。

2つの指紋を比較する

compare.jsjavascript
import { readFile } from "node:fs/promises";
import { histograms } from "./normalize.js";

const [leftPath, rightPath] = process.argv.slice(2);
if (!leftPath || !rightPath) {
  throw new Error("Usage: node compare.js reference.json audit.json");
}

const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));

function probabilities(counts, keys) {
  const total = Object.values(counts).reduce((sum, value) => sum + value, 0);
  return keys.map((key) => (counts[key] ?? 0) / total);
}

function kl(p, q) {
  return p.reduce((sum, value, index) => {
    if (value === 0) return sum;
    return sum + value * Math.log2(value / q[index]);
  }, 0);
}

function jsd(left, right) {
  const keys = [...new Set([...Object.keys(left), ...Object.keys(right)])];
  const p = probabilities(left, keys);
  const q = probabilities(right, keys);
  const midpoint = p.map((value, index) => (value + q[index]) / 2);
  return (kl(p, midpoint) + kl(q, midpoint)) / 2;
}

const left = histograms(await readJson(leftPath));
const right = histograms(await readJson(rightPath));
const promptIds = Object.keys(left).filter((id) => right[id]);
const distances = promptIds.map((id) => ({ id, jsd: jsd(left[id], right[id]) }));
const meanJsd = distances.reduce((sum, row) => sum + row.jsd, 0) / distances.length;

console.table(distances);
console.log({ meanJsd });

参照モデルと監査対象モデルをそれぞれ収集し、次のように比較します。

bash
node collect.js reference.json
node collect.js audit.json
node compare.js reference.json audit.json

同じモデルを2回収集した結果とも比較してください。その距離が、自分の環境における通常の揺らぎです。別モデルとの距離が大きくても、論文の閾値をそのまま流用して「偽物」と判定してはいけません。

AIコーディングや個人開発でどう使えるか

モデルドリフトの検知

信頼できる環境で参照分布を保存し、毎週同じ条件を実行します。過去の通常範囲よりJSDが急に大きくなれば、モデル更新や配信構成の変化を調べるきっかけになります。

フォールバック先の比較

通常は公式API、障害時は別プロバイダーへ切り替える構成なら、両経路の指紋を比較できます。指紋が同一である必要はありませんが、切り替え時の品質変化を説明するメタデータになります。

AIコーディングエージェントの再現性確認

コード生成の成功率、テスト通過率、レイテンシと一緒に指紋距離を保存します。性能低下と指紋変化が同じ時期に発生すれば、プロンプトやリポジトリだけでなく、モデル経路も調査対象にできます。

ローカルLLMの比較

同じベースモデルについて、Q4とQ8、異なるGGUF、OllamaとLM Studio、別の推論エンジンを比較できます。量子化やデコード実装が回答分布へどの程度影響するかを観察する実験になります。

CIでの定期チェック

定期ジョブで指紋を採取し、距離が自分で設定した警告範囲を超えた場合だけSlackやメールへ通知できます。ただし、最初から固定の判定閾値を置くのではなく、複数回のベースライン、時刻差、プロバイダー差を記録して通常範囲を作るべきです。

この研究で分からないこと

  1. 暗号学的な証明ではない。 モデルの重みが完全に同一であることは証明できません。
  2. 信頼できる参照先が必要。 本人確認には、公式APIなどから事前に採取した参照指紋が必要です。
  3. モデル更新で指紋が変わる。 論文は短期間の安定性を調べていますが、数か月単位のドリフトは未検証です。
  4. 1つのアグリゲーターを使った研究である。 手法自体はほかのAPIにも使えますが、エコシステム上の観測結果をそのまま一般化できません。
  5. 非表示reasoningを必須とするモデルは対象外。 推論後の回答は、直接の1トークン生成とは異なる生成過程になるため除外されました。
  6. 異常は不正を意味しない。 更新、量子化、蒸留、キャッシュ、デコード設定でも差が発生します。
  7. プロンプトへの適応耐性は未確定。 論文は言い換え可能な日常質問を採用していますが、言い換え不変性の専用実験は今後の課題です。
  8. 短い回答にも入力コストはある。 出力は小さくても、多数回のリクエスト、入力トークン、レート制限を考慮する必要があります。

したがって、この手法は電子署名ではなく、煙探知機に近いものです。警報が鳴ったら火災と断定するのではなく、配信ログ、品質評価、バージョン情報と合わせて原因を調べます。

まとめ――AIにも、隠しきれない「選び方の癖」がある

LLMの短い回答にも、トークナイザー、学習データ、追加学習、量子化、蒸留、デコード実装の影響が残ります。

1回だけ「42」と答えても、そこからモデル名は分かりません。しかし、数字、色、動物、都市などへの回答を100回前後集めると、小さな偏りが分布になり、モデルを見分ける手がかりになります。

この行動フィンガープリントは、LLM APIのモデル判別を確実にする魔法ではありません。それでも、これまで利用者から見えにくかったモデルドリフトや配信経路の違いを、安価なブラックボックス監査で観測できる可能性を示しました。

APIの向こう側にいるAIは、自分の名前を自由に名乗れます。しかし、何を選びやすいかという癖まで完全に隠すのは、もう少し難しいようです。

よくある質問

1トークンだけでモデルを判別できるのですか?

いいえ。1回あたりの出力を1トークン程度に抑えられるという意味です。判別には、複数条件で多数回集めた回答分布が必要です。

モデル本人に名前を聞くのでは駄目ですか?

自己申告はシステムプロンプトや学習内容で変更できるため、技術的な証明にはなりません。

temperature 0の方が比較しやすくありませんか?

temperature 0では回答が固定されやすい一方、同じモデルでも配信プロバイダーごとに異なる固定回答になるケースが観測されました。論文ではtemperature 1.0の分布を主要な指紋として使っています。

この方法で偽物のAPIを断定できますか?

できません。参照分布との統計的な違いを示すだけで、差の原因はモデル更新、量子化、配信設定など複数考えられます。

自分のアプリにも導入できますか?

モデルドリフト監視やプロバイダー比較の補助シグナルとして利用できます。自分の環境で複数回ベースラインを取得し、品質テストや運用ログと組み合わせてください。

次のステップ

ほかの実装記事を探す

Writing一覧から、現在の課題に近い記事を探せます。

記事一覧へ戻る