【TypeSafe】出力を絞ればLLMも速くなる? Gemini 3.8 Flash と Jev で同じ107ケースの速度・コストを比較してみた

【TypeSafe】出力を絞ればLLMも速くなる? Gemini 3.8 Flash と Jev で同じ107ケースの速度・コストを比較してみた

Jev との比較から見えた、生成型LLMの意外な弱点。出力を極限まで絞ってもなお、速度とコストで大きな差が出た理由を、107ケースの検証結果から探ります。
2026.09.21

こんにちは、けーまです。

先日、TypeSafe の Jev で書類項目の突合を試す記事を書きました。

【TypeSafe】Jev で書類項目の突合はどこまでできるか?精度・速度・コストを測ってみた | DevelopersIO

Jev は文章を生成せず、質問に対して確率値だけを返すモデルです。
107ケースを投げて中央値が約0.7秒、合計で約0.35円という結果を見て、私はふと思いました。
「これ、速くて安いのは単に文章を生成していないからでは?」
もしそうなら、普通の生成型 LLM でも出力を数トークンに絞れば、同じくらいの速度と料金で動くはずです。

そこで今回は、前回とまったく同じ107ケースを Gemini 3.8 Flash に解かせて、精度・速度・コストを比較してみました(検証日は2026年9月20日です)。

結論から言うと、私の予想は見事に外れました。
「確率を返すだけなら普通の LLM で十分では」と考えている方の参考になれば幸いです。

1. 「生成しないから速い」は本当か?

生成型 LLM の応答時間は、大きく分けると2つの要素で決まります。
入力を読み込んで最初の1トークンを返すまでの時間(TTFT)と、そこから1トークンずつ出力を生成していく時間です。
長い文章を書かせると後者の時間がかさみます。

前回の検証で Jev の出力は1ケースあたりわずか36トークンでした。
それなら Gemini にも {"exact": 0.95, "semantic": 0.2} のような十数トークンだけを返させれば、出力生成にかかる時間はほぼゼロにできるはずです。
残るのは「入力を読む時間」だけなので、Jev といい勝負になるのではないか、というのが今回の仮説です。

比較相手に Gemini 3.8 Flash を選んだのは、Artificial Analysis で最もスピードが速いとされていたからです。
https://artificialanalysis.ai/

2. 条件のそろえ方

2.1 前回とそろえたもの

公平に比較できるよう、前回の記事と以下の条件をそろえました。

項目 内容
テストデータ 前回と同じ cases.jsonl(107ケース、すべて架空の値)
質問文 前回の英語版と同じ文言。前回のスクリプトから import して使う
判定の軸 exact(1文字も違わないか)と semantic(表記が違っても同じ値か)の2つ
閾値 どちらも0.8
実行方法 逐次(並列なし)

テストデータの中身や2つの判定軸の考え方は前回の記事に全件載せています。
Jev 側の数値は、前回の記事の英語版の結果をそのまま使っています。

2.2 Gemini 側で工夫したこと

出力を絞るため、Gemini には structured output(返してほしい JSON のスキーマを指定すると、その形式だけで出力してくれる機能)を使いました。
これで出力を exactsemantic の2項目だけに固定できます。

確率は、モデル自身に0〜1の数値を出力させています。今回の Gemini が返す確率は「モデルがテキストとして出力した自己申告の値」になります。

もう1つの注意点は thinking(回答前の内部推論)の扱いです。
出力を短く絞っても、裏で長く考え込まれてしまっては速度が出ません。
しかし公式ドキュメントによると、Gemini 3.8 Flash が受け付ける thinking レベルは low・medium・high の3段階のみで、最小の minimal を指定するとエラーになってしまいます。

Supported (low, medium, high)
Note: minimal is not supported and returns an error.

引用元: モデル情報: Gemini 3.8 Flash | Google AI for Developers

つまり、thinking を完全にオフにはできません。
今回は指定できる中で最小の low に設定し、裏で実際に何トークン思考したかを記録することにしました。

2.3 環境

項目 内容
モデル gemini-3.8-flash
エンドポイント POST /v1beta/models/gemini-3.8-flash:generateContent(ホストは generativelanguage.googleapis.com
料金枠 Gemini API の無料枠
thinking レベル low
実行環境 macOS 26.6.2、Python 3.14.6、標準ライブラリのみ
ケース数 107
実行日 2026年9月20日

なお、前回の Jev は1リクエストごとに接続を張り直していましたが、Gemini 側は HTTP 接続を使い回す設定(--keepalive)で測定しました。
接続ごとの TLS ハンドシェイクが省ける分、Gemini 側に有利な測定条件になっています。

3. スクリプトと実行

3.1 ファイルの配置

前回の記事で使った run_eval.pycases.jsonl があるディレクトリに、今回の run_eval_gemini.py を追加します。
質問文を前回のスクリプトから import しているため、必ず同じディレクトリに配置してください。

<作業ディレクトリ>/
├── cases.jsonl          # 前回の記事のテストデータ
├── run_eval.py          # 前回の記事のスクリプト
└── run_eval_gemini.py   # 今回追加するスクリプト

3.2 スクリプト

前回と同様、サードパーティ製ライブラリは使わず Python の標準ライブラリだけで動くようにしています。

run_eval_gemini.py:

run_eval_gemini.py の全文(クリックすると展開します)
#!/usr/bin/env python3
"""生成型 LLM(Gemini)で、JEV と全く同じ横突合ケースを測る。

確かめたいのは「JEV が速くて安いのは文章を生成しないからで、生成型 LLM でも
出力を極小に絞れば同じ速度・料金になるのでは」という仮説。

条件を揃えるために次のようにしている。
- ケースは JEV 側の cases.jsonl をそのまま読む(コピーしない)
- 質問文も JEV 側 run_eval.py の QUESTIONS を import して同じ文言を使う
- JEV 側と同じく逐次実行でレイテンシを測る。接続は既定で1件ずつ新規、--keepalive で使い回し
- 出力は structured output(JSON スキーマ強制)で2項目だけに絞る

API: POST https://generativelanguage.googleapis.com/v1beta/models/<model>:generateContent
     https://ai.google.dev/api/generate-content

標準ライブラリだけで動く。
"""

from __future__ import annotations

import argparse
import http.client
import json
import os
import statistics
import sys
import threading
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

# JEV 側の run_eval.py と cases.jsonl を探す。同じディレクトリに置いてあればそれを、
# 無ければ隣の jev-cross-match-validation/ を使う。
HERE = Path(__file__).resolve().parent
JEV_DIR = HERE if (HERE / "run_eval.py").exists() else HERE.parent / "jev-cross-match-validation"
sys.path.insert(0, str(JEV_DIR))
from run_eval import QUESTIONS, STATE_KEYS  # noqa: E402  JEV と同じ質問文を使う

HOST = "generativelanguage.googleapis.com"
ENDPOINT_PATH = "/v1beta/models/{model}:generateContent"
ENDPOINT = f"https://{HOST}{ENDPOINT_PATH}"
DEFAULT_MODEL = "gemini-3.8-flash"

# USD / 1M tokens(Standard、2026-09 時点の公開価格)。出力単価は thinking トークンにもかかる。
# https://ai.google.dev/gemini-api/docs/pricing
PRICES = {
    "gemini-3.8-flash": (0.75, 3.75),       # 2027-01-01 から 1.50 / 7.50
    "gemini-3.5-flash-lite": (0.30, 2.50),
}

# モデルごとに指定できる最小の thinking level。
# gemini-3.8-flash は minimal 非対応(400 エラー)で、thinking を完全には切れない。
MIN_THINKING = {
    "gemini-3.8-flash": "low",
    "gemini-3.5-flash-lite": "minimal",
    # Gemma 4 の thinking は on/off の2値で、API 上は minimal が off にあたる。
    "gemma-4-26b-a4b-it": "minimal",
    "gemma-4-31b-it": "minimal",
}

RETRY_STATUS = {429, 500, 503}

def build_prompt(case: dict, lang: str) -> tuple[str, str]:
    """JEV の questions / state と同じ情報を、system と user の2つの文字列にする。"""
    src = QUESTIONS[lang]
    key_a, key_b = STATE_KEYS[lang]
    lines = []
    for key in ("exact", "semantic"):
        q = src[key]
        lines.append(f"[{key}] {q['instructions']}")
        lines.append(f"  true: {q['criteria']['true']}")
        lines.append(f"  false: {q['criteria']['false']}")
    state = json.dumps({key_a: case["a"], key_b: case["b"]}, ensure_ascii=False)
    return "\n".join(lines), state

def build_schema(mode: str) -> dict:
    """bool は true/false だけ、prob は JEV の noul に合わせて 0〜1 の確率を返させる。"""
    if mode == "bool":
        prop = {"type": "BOOLEAN"}
    else:
        prop = {"type": "NUMBER", "minimum": 0, "maximum": 1}
    return {
        "type": "OBJECT",
        "properties": {"exact": prop, "semantic": prop},
        "required": ["exact", "semantic"],
        "propertyOrdering": ["exact", "semantic"],
    }

_local = threading.local()

def post_fresh(body: bytes, api_key: str, args) -> tuple[int, str]:
    """JEV 側と同じ測り方。1件ごとに新規接続するので TLS ハンドシェイクが毎回乗る。"""
    req = urllib.request.Request(
        ENDPOINT.format(model=args.model), data=body, method="POST",
        headers={"x-goog-api-key": api_key, "Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=args.timeout) as resp:
            return resp.status, resp.read().decode("utf-8")
    except urllib.error.HTTPError as exc:
        return exc.code, exc.read().decode("utf-8", errors="replace")

def post_keepalive(body: bytes, api_key: str, args) -> tuple[int, str]:
    """接続を使い回す。実運用のサーバーはこちらに近く、ハンドシェイク分だけ速い。"""
    for retry in (False, True):
        conn = getattr(_local, "conn", None)
        if conn is None:
            conn = _local.conn = http.client.HTTPSConnection(HOST, timeout=args.timeout)
        try:
            conn.request("POST", ENDPOINT_PATH.format(model=args.model), body=body,
                         headers={"x-goog-api-key": api_key,
                                  "Content-Type": "application/json"})
            resp = conn.getresponse()
            return resp.status, resp.read().decode("utf-8", errors="replace")
        except (http.client.HTTPException, ConnectionError):
            # サーバー側がアイドル接続を切っていた場合は1回だけ張り直す
            conn.close()
            _local.conn = None
            if retry:
                raise
    raise AssertionError("unreachable")

def call_gemini(case: dict, api_key: str, args) -> dict:
    """1ケースを投げ、応答とレイテンシを返す。レイテンシは成功した試行の分だけ。"""
    system, state = build_prompt(case, args.lang)
    if args.mode == "prob":
        system += ("\nAnswer each question with the probability (0 to 1) that it is true."
                   if args.lang == "en" else
                   "\n各質問について、true である確率を 0〜1 の数値で答える。")

    body = json.dumps({
        "systemInstruction": {"parts": [{"text": system}]},
        "contents": [{"role": "user", "parts": [{"text": state}]}],
        "generationConfig": {
            "responseMimeType": "application/json",
            "responseSchema": build_schema(args.mode),
            "thinkingConfig": {"thinkingLevel": args.thinking_level},
        },
    }, ensure_ascii=False).encode("utf-8")

    for attempt in range(4):
        started = time.perf_counter()
        try:
            status, raw = (post_keepalive if args.keepalive else post_fresh)(body, api_key, args)
        except Exception as exc:  # ネットワーク断・タイムアウト
            return {"error": f"{type(exc).__name__}: {exc}",
                    "latency_ms": (time.perf_counter() - started) * 1000}
        latency_ms = (time.perf_counter() - started) * 1000
        if status == 200:
            return {"payload": json.loads(raw), "retries": attempt,
                    "latency_ms": latency_ms}
        if status in RETRY_STATUS and attempt < 3:
            time.sleep(2 ** attempt)
            continue
        return {"error": f"HTTP {status}: {raw[:500]}", "latency_ms": latency_ms}
    raise AssertionError("unreachable")

def judge(case: dict, result: dict, args) -> dict:
    """応答を期待値と突き合わせる。行の形は JEV 側の results_*.jsonl に揃える。"""
    row = {
        "id": case["id"],
        "category": case["category"],
        "note": case["note"],
        "latency_ms": round(result["latency_ms"], 1),
    }
    if "error" in result:
        row["error"] = result["error"]
        return row

    payload = result["payload"]
    usage = payload.get("usageMetadata") or {}
    row["model"] = payload.get("modelVersion") or args.model
    row["retries"] = result["retries"]
    row["input_tokens"] = usage.get("promptTokenCount")
    row["output_tokens"] = usage.get("candidatesTokenCount")
    row["thinking_tokens"] = usage.get("thoughtsTokenCount") or 0

    try:
        parts = payload["candidates"][0]["content"]["parts"]
        text = "".join(p.get("text", "") for p in parts if not p.get("thought"))
        answers = json.loads(text)
    except (KeyError, IndexError, json.JSONDecodeError) as exc:
        row["error"] = f"応答を解釈できない: {type(exc).__name__}: {json.dumps(payload, ensure_ascii=False)[:300]}"
        return row

    thresholds = {"exact": args.threshold_exact, "semantic": args.threshold_semantic}
    for key in ("exact", "semantic"):
        value = answers.get(key)
        expected = case[f"expect_{key}"]
        if args.mode == "bool":
            prob, got = None, value
        else:
            prob = value
            got = None if value is None else value >= thresholds[key]
        row[f"{key}_prob"] = prob
        row[f"{key}_threshold"] = thresholds[key] if args.mode == "prob" else None
        row[f"{key}_got"] = got
        row[f"{key}_expected"] = expected
        row[f"{key}_ok"] = None if expected is None else (got == expected)
    return row

def summarize(rows: list[dict], args) -> None:
    errors = [r for r in rows if "error" in r]
    ok_rows = [r for r in rows if "error" not in r]

    print("\n=== 結果サマリ ===")
    print(f"モデル: {args.model} / thinking {args.thinking_level} / 出力 {args.mode}")
    print(f"ケース数: {len(rows)}  成功: {len(ok_rows)}  失敗: {len(errors)}")

    for key in ("exact", "semantic"):
        scored = [r for r in ok_rows if r.get(f"{key}_ok") is not None]
        hit = sum(1 for r in scored if r[f"{key}_ok"])
        rate = f"{hit / len(scored):.1%}" if scored else "-"
        print(f"{key:9s} 正答 {hit}/{len(scored)} ({rate})")

    print("\n--- カテゴリ別(semantic) ---")
    for cat in sorted({r["category"] for r in ok_rows}):
        scored = [r for r in ok_rows
                  if r["category"] == cat and r.get("semantic_ok") is not None]
        if scored:
            hit = sum(1 for r in scored if r["semantic_ok"])
            print(f"{cat:12s} {hit}/{len(scored)} ({hit / len(scored):.0%})")

    misses = [r for r in ok_rows if r.get("semantic_ok") is False]
    if misses:
        print("\n--- semantic を外したケース ---")
        for r in misses:
            print(f"{r['id']:5s} {r['note']}  期待={r['semantic_expected']} "
                  f"実際={r['semantic_got']} (p={r['semantic_prob']})")

    if ok_rows:
        lat = sorted(r["latency_ms"] for r in ok_rows)
        p = lambda q: lat[min(int(len(lat) * q), len(lat) - 1)]  # noqa: E731
        print("\n--- レイテンシ(ms) ---")
        print(f"平均 {statistics.mean(lat):.0f} / 中央値 {p(0.5):.0f} / "
              f"p95 {p(0.95):.0f} / 最小 {lat[0]:.0f} / 最大 {lat[-1]:.0f}")

        n = len(ok_rows)
        tin = sum(r.get("input_tokens") or 0 for r in ok_rows)
        tout = sum(r.get("output_tokens") or 0 for r in ok_rows)
        tthink = sum(r.get("thinking_tokens") or 0 for r in ok_rows)
        print("\n--- トークン ---")
        print(f"入力 合計 {tin} (平均 {tin / n:.0f}/件) / 出力 合計 {tout} (平均 {tout / n:.1f}/件) / "
              f"thinking 合計 {tthink} (平均 {tthink / n:.1f}/件)")
        price_in = args.price_in if args.price_in is not None else PRICES.get(args.model, (None, None))[0]
        price_out = args.price_out if args.price_out is not None else PRICES.get(args.model, (None, None))[1]
        if price_in is None or price_out is None:
            print("概算コスト: 単価が未登録。--price-in / --price-out で指定する")
        else:
            cost = tin / 1e6 * price_in + (tout + tthink) / 1e6 * price_out
            print(f"概算コスト: ${cost:.6f} (入力 ${price_in} / 出力+thinking ${price_out} per 1Mtok)")

    for r in errors:
        print(f"[ERROR] {r['id']}: {r['error']}", file=sys.stderr)

def main() -> int:
    ap = argparse.ArgumentParser(description="Gemini で JEV と同じ横突合ケースを測る")
    ap.add_argument("--cases", default=str(JEV_DIR / "cases.jsonl"))
    ap.add_argument("--out", help="既定は results_<model>_<mode>_<lang>.jsonl")
    ap.add_argument("--lang", choices=("ja", "en"), default="en")
    ap.add_argument("--model", default=DEFAULT_MODEL)
    ap.add_argument("--mode", choices=("bool", "prob"), default="bool",
                    help="bool は true/false だけ返させる。prob は 0〜1 の確率を返させて閾値で判定")
    ap.add_argument("--thinking-level", choices=("minimal", "low", "medium", "high"),
                    help="既定はモデルが受け付ける最小値")
    ap.add_argument("--threshold-exact", type=float, default=0.8)
    ap.add_argument("--threshold-semantic", type=float, default=0.8)
    ap.add_argument("--price-in", type=float, help="USD / 1M 入力トークン")
    ap.add_argument("--price-out", type=float, help="USD / 1M 出力トークン")
    ap.add_argument("--concurrency", type=int, default=1,
                    help="並列数。レイテンシを正しく測るなら1のまま")
    ap.add_argument("--keepalive", action="store_true",
                    help="HTTP 接続を使い回す。JEV 側の測り方(毎回新規接続)とは条件が変わる")
    ap.add_argument("--limit", type=int, help="先頭 N 件だけ実行(疎通確認用)")
    ap.add_argument("--timeout", type=float, default=60.0)
    args = ap.parse_args()
    if args.thinking_level is None:
        args.thinking_level = MIN_THINKING.get(args.model, "low")
    suffix = "_keepalive" if args.keepalive else ""
    out_path = args.out or f"results_{args.model}_{args.mode}_{args.lang}{suffix}.jsonl"

    with open(args.cases, encoding="utf-8") as f:
        cases = [json.loads(line) for line in f if line.strip()]
    if args.limit:
        cases = cases[:args.limit]

    api_key = os.environ.get("GEMINI_API_KEY")
    if not api_key:
        print("GEMINI_API_KEY が未設定です。export してください。", file=sys.stderr)
        return 1

    print(f"{len(cases)} ケースを {args.model} に投げます (質問文 {args.lang} / "
          f"thinking {args.thinking_level} / 出力 {args.mode} / 並列 {args.concurrency})...",
          file=sys.stderr)

    run = lambda c: judge(c, call_gemini(c, api_key, args), args)  # noqa: E731

    wall = time.perf_counter()
    if args.concurrency > 1:
        with ThreadPoolExecutor(max_workers=args.concurrency) as pool:
            rows = list(pool.map(run, cases))
    else:
        rows = [run(c) for c in cases]
    wall = time.perf_counter() - wall

    with open(out_path, "w", encoding="utf-8") as f:
        for row in rows:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")

    summarize(rows, args)
    print(f"\n全体所要: {wall:.1f}s ({wall / len(rows):.2f}s/件)")
    print(f"明細: {out_path}")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

Gemini に渡しているプロンプトは、前回の質問文を並べたシステムプロンプトと、比較したい2つの値を入れた JSON のみです。

# システムプロンプト(確率モード)
[exact] Are value_a and value_b identical as strings? Any difference in whitespace, punctuation, letter case, or full-width versus half-width characters counts as not identical.
  true: Identical character for character.
  false: They differ by at least one character.
[semantic] Do value_a and value_b refer to the same underlying value, even when they are written differently? This is a cross-check of the same field on two business documents. When one side lists itemized values and the other states a total, add the items up and compare that sum against the stated total.
  true: Only the format, language, unit, or abbreviation differs and the underlying value is the same, or the itemized values add up to the stated total.
  false: The underlying value itself differs, one side has no value, or the itemized values do not add up to the stated total.
Answer each question with the probability (0 to 1) that it is true.

# ユーザーメッセージ
{"value_a": "INV-2026-0912", "value_b": "INV-2026-0912"}

3.3 実行

API キーは Google AI Studio で発行して環境変数にセットします。
課金アカウントを紐付けていないプロジェクトのキーを使えば、そのまま無料枠で動作します。

export GEMINI_API_KEY="<YOUR_API_KEY>"

確率を返させるモードにし、接続を使い回すオプションをつけて実行します。

python3 run_eval_gemini.py --mode prob --keepalive
# 出力例
107 ケースを gemini-3.8-flash に投げます (質問文 en / thinking low / 出力 prob / 並列 1)...

=== 結果サマリ ===
モデル: gemini-3.8-flash / thinking low / 出力 prob
ケース数: 107  成功: 107  失敗: 0
exact     正答 107/107 (100.0%)
semantic  正答 101/106 (95.3%)

--- カテゴリ別(semantic) ---
address      10/11 (91%)
aggregate    7/8 (88%)
baseline     9/9 (100%)
date         12/12 (100%)
description  10/10 (100%)
missing      7/7 (100%)
number       17/18 (94%)
ocr          7/8 (88%)
party        14/15 (93%)
whitespace   8/8 (100%)

--- semantic を外したケース ---
3-6   片方に通貨の記載がなく、同じ金額と断定できない  期待=False 実際=True (p=1.0)
4-4   前株後株(方針依存)  期待=True 実際=False (p=0)
5-3   粒度違い。市名だけと都道府県つきは同じ住所とは言い切れない  期待=False 実際=True (p=0.95)
7-3   数値中の O 誤読。文字が違えば別の値  期待=False 実際=True (p=1)
9-4   個数が一致  期待=True 実際=False (p=0.2)

--- レイテンシ(ms) ---
平均 1877 / 中央値 1635 / p95 3380 / 最小 1147 / 最大 5607

--- トークン ---
入力 合計 26330 (平均 246/件) / 出力 合計 1653 (平均 15.4/件) / thinking 合計 4532 (平均 42.4/件)
概算コスト: $0.042941 (入力 $0.75 / 出力+thinking $3.75 per 1Mtok)

全体所要: 200.8s (1.88s/件)
明細: results_gemini-3.8-flash_prob_en_keepalive.jsonl

4. 結果:精度は Gemini、速度とコストは Jev

4.1 全体の数字

指標 Jev(前回の英語版) Gemini 3.8 Flash
exact 正答 106/107(99.1%) 107/107(100.0%)
semantic 正答 96/106(90.6%) 101/106(95.3%)
レイテンシ 中央値 約0.71秒(709ms) 約1.64秒(1635ms)
レイテンシ p95 約0.87秒(872ms) 約3.38秒(3380ms)
レイテンシ 最大 約1.00秒(1001ms) 約5.61秒(5607ms)
入力トークン(平均) 509 246
出力トークン(平均) 36(課金対象外) 15.4
thinking トークン(平均) なし 42.4
コスト $0.0023(約0.35円) $0.0429(約6.4円、有料枠換算)
107ケースの所要 75.1秒 200.8秒

コストは1ドル=150円で換算しています。
Gemini 3.8 Flash の単価は、2026年12月31日までの導入価格(入力が100万トークンあたり $0.75、出力が $3.75)で計算しました。
thinking トークンにも出力単価が適用されます。
なお、2027年1月1日以降はどちらの単価も2倍になる予定です。

引用元: 料金: Gemini Developer API pricing | Google AI for Developers

4.2 出力を絞っても、2倍以上遅かった

中央値のレイテンシは Jev の約2.3倍でした。
「thinking が入ったせいで遅くなったのでは?」と思い、thinking が0トークンだった57件だけを抜き出して集計してみましたが、それでも中央値は1616ms でした。
この57件において、Gemini が返した出力は十数トークンにすぎません。

つまり、遅さの主な原因はトークンの生成量ではなく、入力を読み込んで最初の1トークンを返すまでの処理時間にあります。
「出力を生成しないから速い」という理屈だけでは、Jev の圧倒的な速さは説明できませんでした。
出力を極限まで削って Jev 並みの速度を出す、という狙いは、 Gemini 3.8 Flash では実現できませんでした。

また、p95 が3秒を超えて大きく跳ねたのは thinking の影響です。
low を指定していても、107件中50件で thinking が発生し、その50件では平均91トークンほど裏で推論を行っていました。
「確率は何%か」と聞かれると、モデル側もじっくり考え込んでしまうようです。

4.3 料金の差は、そもそもの入力の単価と出力の単価で決まった

コスト面では約19倍の差がつきました。
面白いのは、平均の入力トークン数自体は Gemini のほうが半分以下で済んでいる点です。
同じ質問文でも、Jev は型付きの質問を JSON スキーマで渡す必要があるためトークンが増えやすく、Gemini は素直なテキストとして渡せるため少なく抑えられたと考えられます。

それでもコストが逆転しない理由は2つあります。
1つ目は入力トークンの単価で、約18倍の開きがあります($0.042 と $0.75)。
入力の料金だけで比べても、Gemini は Jev の約8.6倍でした。
2つ目は出力側の料金です。
Jev は出力トークンは無料ですが、Gemini は出力と thinking の両方に $3.75 の単価がかかります。
Gemini のコストの内訳は入力が46%、出力が14%、thinking が40%で、出力側だけで半分を超えていました。

出力を十数トークンまで絞っても、自分では切れない thinking がその3倍近いトークンを使い、入力の5倍の単価で課金されます。

4.4 Gemini の確率は、0か1に張り付いた

個人的に一番の発見だったのがこの点です。
semantic の確率の分布を3つの帯に分けてみました。

確率の帯 Jev Gemini 3.8 Flash
0.8以上(一致と判定) 66件 71件
0.2超〜0.8未満(迷っている) 14件 1件
0.2以下(不一致と判定) 27件 35件

Gemini が「判断に迷った」ケースは、107件中なんと1件だけでした。
semantic では64件が1.0ぴったり、29件が0.0ぴったりです。
exact に至っては、107件すべてが0.0か1.0のどちらかに振り切れていました。

正答率が高いうちはこれでも問題ありません。
しかし、困るのは間違えたときです。
Gemini が不正解だった5件の出力確率は 1.0、1.0、0.95、0.0、0.2 で、どれも自信満々に間違えています。
「閾値0.8で迷っているものは人間の目視チェックに回す」というフローを組んでも、この5件は高い確率で素通りするか、逆に問答無用で弾かれてしまいます。

前回の記事で紹介したように、Jev は判断がつかないケースに対して0.6〜0.7前後の絶妙な確率を返してくれます。
確率が「テキストとして出力された自己申告値」なのか、「モデル内部の計算結果そのもの」なのかという違いが、ここにはっきりと表れています。
確率値を運用のしきい値やゲートとして使いたい場合は、Jev の確率のほうがはるかに実用的です。

ケースごとの確率は、次の5章に全件載せています。

5. テストデータと確率(全107ケース)

107ケースそれぞれについて、2つのモデルが返した確率を並べます。
表内の exact および semantic の列は正解ラベルです。
JevGemini の列は、完全一致確率(exact)/ 意味的一致確率(semantic)の順で記載しています。
Jev の数値は前回の記事の英語版と同じものです。
太字 で示した数値は、閾値0.8の判定で正解と一致しなかった結果です。

文字列内の不可視文字は、前回と同じく を半角スペース、 を全角スペース、 を改行、 をタブとして表記しています。
正解ラベルの決め方は前回の記事の4章に書いたとおりです。

5.1 完全同一(9件)

A B exact semantic Jev(exact / semantic) Gemini(exact / semantic)
INV-2026-0912 INV-2026-0912 一致 一致 0.99 / 0.98 1.00 / 1.00
1,250.00 1,250.00 一致 一致 0.98 / 0.98 1.00 / 1.00
ACME TRADING CO., LTD. ACME TRADING CO., LTD. 一致 一致 0.99 / 0.98 1.00 / 1.00
長文240字(末尾 ORIGIN JAPAN 長文240字(末尾 ORIGIN JAPAN 一致 一致 0.98 / 0.99 1.00 / 1.00
STEEL PIPE⏎SEAMLESS⏎50MM STEEL PIPE⏎SEAMLESS⏎50MM 一致 一致 0.98 / 0.98 1.00 / 1.00
ACME␠TRADING ACME␠TRADING 一致 一致 0.98 / 0.96 1.00 / 1.00
DOC-O0O12345 DOC-O0O12345 一致 一致 0.99 / 0.98 1.00 / 1.00
␣1,250.00␣ ␣1,250.00␣ 一致 一致 0.92 / 0.98 1.00 / 1.00
Sep 12, 2026 Sep 12, 2026 一致 一致 0.99 / 0.98 1.00 / 1.00

5.2 日付(13件)

A B exact semantic Jev(exact / semantic) Gemini(exact / semantic)
2026/09/12 2026年9月12日 不一致 一致 0.02 / 0.98 0.00 / 1.00
12-SEP-2026 2026-09-12 不一致 一致 0.02 / 0.98 0.00 / 1.00
Sep 12, 2026 12/09/2026 不一致 一致 0.02 / 0.71 0.00 / 0.95
令和8年9月12日 2026-09-12 不一致 一致 0.02 / 0.94 0.00 / 1.00
03/04/2026 2026-04-03 不一致 一致 0.02 / 0.77 0.00 / 0.80
03/04/2026 2026-03-04 不一致 対象外 0.02 / 0.94 0.00 / 0.85
2026/09/12 2026/09/13 不一致 不一致 0.01 / 0.03 0.00 / 0.00
2026/09/12 2025/09/12 不一致 不一致 0.01 / 0.03 0.00 / 0.00
2026年9月12日 2026年9月12日 一致 一致 0.99 / 0.98 1.00 / 1.00
12-SEP-2026 12-SEP-2026 一致 一致 0.99 / 0.98 1.00 / 1.00
令和8年9月12日 令和8年9月12日 一致 一致 0.99 / 0.99 1.00 / 1.00
2026/09/12 2026/12/09 不一致 不一致 0.01 / 0.02 0.00 / 0.00
令和8年9月12日 令和7年9月12日 不一致 不一致 0.02 / 0.02 0.00 / 0.00

03/04/20262026-03-04 の組み合わせは、前回と同じく正解が一意に定まらないため、正答率の集計対象から除外しています。

Sep 12, 202612/09/2026 は、Jev が0.71、Gemini が0.95でした。
12/09 は12月9日とも9月12日とも読めるため、Jev は確率を下げています。
Gemini は184トークン考えたうえで、一致と判断しました。

5.3 数値・金額(18件)

A B exact semantic Jev(exact / semantic) Gemini(exact / semantic)
1,250.00 1250 不一致 一致 0.01 / 0.97 0.00 / 1.00
USD 1,250.00 $1,250.00 不一致 一致 0.02 / 0.98 0.00 / 1.00
1.250,00 1,250.00 不一致 一致 0.03 / 0.89 0.00 / 1.00
1250.0 1250.5 不一致 不一致 0.02 / 0.08 0.00 / 0.00
12,500 1,250 不一致 不一致 0.01 / 0.06 0.00 / 0.00
1,250.00 1,250.00 USD 不一致 不一致 0.02 / 0.96 0.00 / 1.00
USD 1,250 JPY 1,250 不一致 不一致 0.02 / 0.05 0.00 / 0.00
500 KGS 500.000 KG 不一致 一致 0.02 / 0.96 0.00 / 1.00
500 KG 1102.31 LBS 不一致 一致 0.01 / 0.96 0.00 / 1.00
10 CTNS 10 CARTONS 不一致 一致 0.02 / 0.98 0.00 / 1.00
USD 1,250.00 USD 1,250.00 一致 一致 0.98 / 0.99 1.00 / 1.00
1.250,00 1.250,00 一致 一致 0.98 / 0.98 1.00 / 1.00
500 KGS 500 KGS 一致 一致 0.98 / 0.98 1.00 / 1.00
1,250.00 USD 1,250.00 USD 一致 一致 0.98 / 0.99 1.00 / 1.00
1,250.00 1,205.00 不一致 不一致 0.02 / 0.05 0.00 / 0.00
500 KG 500 LBS 不一致 不一致 0.02 / 0.03 0.00 / 0.00
10 CTNS 10 PALLETS 不一致 不一致 0.02 / 0.16 0.00 / 0.00
500.00 KG 50.00 KG 不一致 不一致 0.01 / 0.03 0.00 / 0.00

1,250.001,250.00 USD は、Jev が0.96、Gemini が1.00で、どちらも一致と誤認しました。
片方にしか通貨の記載がないケースは、前回の記事で触れた弱点が Gemini にもそのまま当てはまっています。

5.4 取引先名(15件)

A B exact semantic Jev(exact / semantic) Gemini(exact / semantic)
ACME CO., LTD. Acme Co., Ltd. 不一致 一致 0.02 / 0.94 0.00 / 1.00
ACME CO.,LTD. ACME CO., LTD. 不一致 一致 0.06 / 0.95 0.00 / 1.00
GLOBEX CORPORATION GLOBEX CORP. 不一致 一致 0.02 / 0.94 0.00 / 1.00
株式会社サンプル商事 サンプル商事株式会社 不一致 一致 0.03 / 0.75 0.00 / 0.00
ACME ACME 不一致 一致 0.02 / 0.94 0.00 / 1.00
ACME TRADING CO. ACNE TRADING CO. 不一致 不一致 0.02 / 0.18 0.00 / 0.00
Initech Inc. Initech Incorporated 不一致 一致 0.02 / 0.93 0.00 / 1.00
株式会社サンプル商事 株式会社サンプル商事 一致 一致 0.99 / 0.98 1.00 / 1.00
ACME ACME 一致 一致 0.98 / 0.94 1.00 / 1.00
GLOBEX CORP. GLOBEX CORP. 一致 一致 0.99 / 0.98 1.00 / 1.00
Initech Incorporated Initech Incorporated 一致 一致 0.99 / 0.98 1.00 / 1.00
ACME TRADING CO., LTD. ACME SHIPPING CO., LTD. 不一致 不一致 0.02 / 0.11 0.00 / 0.00
GLOBEX CORP. GLOBEX HOLDINGS CORP. 不一致 不一致 0.02 / 0.29 0.00 / 0.10
株式会社サンプル商事 株式会社サンプル物産 不一致 不一致 0.02 / 0.11 0.00 / 0.00
ACME CO., LTD. ACME CO., LTD. (Taiwan Branch) 不一致 不一致 0.02 / 0.25 0.00 / 0.20

前株と後株(株式会社サンプル商事サンプル商事株式会社)は両方とも不正解でしたが、外し方が違いました。
Jev は0.75で「たぶん同じだが自信はない」、Gemini は244トークン考えて0.00で「別法人だ」と断定しています。
前回も書いたとおり、ここは業務ルール次第でどちらにも転ぶグレーゾーンです。
グレーなものはグレーと返してくれるほうが、実務の運用には組み込みやすいと感じました。

5.5 住所(11件)

A B exact semantic Jev(exact / semantic) Gemini(exact / semantic)
TOKYO, JAPAN Tokyo Japan 不一致 一致 0.02 / 0.97 0.00 / 1.00
JP JAPAN 不一致 一致 0.02 / 0.96 0.00 / 1.00
OSAKA Osaka-shi, Osaka 不一致 不一致 0.01 / 0.80 0.00 / 0.95
1-1-1 Sample-cho, Chuo-ku, Tokyo 〒100-0000 東京都中央区サンプル町1-1-1 不一致 一致 0.01 / 0.95 0.00 / 0.95
SAPPORO SENDAI 不一致 不一致 0.01 / 0.03 0.00 / 0.00
〒100-0000 東京都中央区サンプル町1-1-1 〒100-0000 東京都中央区サンプル町1-1-1 一致 一致 0.98 / 0.99 1.00 / 1.00
TOKYO, JAPAN TOKYO, JAPAN 一致 一致 0.99 / 0.98 1.00 / 1.00
JP JP 一致 一致 0.99 / 0.96 1.00 / 1.00
1-1-1 Sample-cho, Chuo-ku, Tokyo 1-1-2 Sample-cho, Chuo-ku, Tokyo 不一致 不一致 0.02 / 0.04 0.00 / 0.00
〒100-0000 東京都中央区サンプル町1-1-1 〒101-0000 東京都千代田区サンプル町1-1-1 不一致 不一致 0.02 / 0.05 0.00 / 0.00
CHUO-KU, TOKYO CHUO-KU, OSAKA 不一致 不一致 0.02 / 0.03 0.00 / 0.00

5.6 品名(10件)

A B exact semantic Jev(exact / semantic) Gemini(exact / semantic)
STEEL PIPE SEAMLESS 50MM SEAMLESS STEEL PIPE 50MM 不一致 一致 0.02 / 0.97 0.00 / 1.00
STEEL PIPE⏎SEAMLESS⏎50MM STEEL PIPE SEAMLESS 50MM 不一致 一致 0.03 / 0.96 0.00 / 1.00
STEEL PIPE (50MM) STEEL PIPE 50MM 不一致 一致 0.02 / 0.96 0.00 / 1.00
STEEL PIPE 50MM STEEL PIPE 60MM 不一致 不一致 0.01 / 0.03 0.00 / 0.00
長文(末尾 ORIGIN JAPAN 長文(末尾 ORIGIN KOREA 不一致 不一致 0.01 / 0.06 0.00 / 0.00
STEEL PIPE (50MM) STEEL PIPE (50MM) 一致 一致 0.99 / 0.98 1.00 / 1.00
長文(末尾 ORIGIN KOREA 長文(末尾 ORIGIN KOREA 一致 一致 0.99 / 0.99 1.00 / 1.00
STEEL PIPE SEAMLESS 50MM STEEL PIPE WELDED 50MM 不一致 不一致 0.01 / 0.09 0.00 / 0.00
STEEL PIPE 50MM BLACK STEEL PIPE 50MM GALVANIZED 不一致 不一致 0.01 / 0.09 0.00 / 0.00
SEAMLESS STEEL PIPE 50MM x 6000MM SEAMLESS STEEL PIPE 50MM x 6600MM 不一致 不一致 0.02 / 0.04 0.00 / 0.00

5.7 OCR誤読(8件)

A B exact semantic Jev(exact / semantic) Gemini(exact / semantic)
INVO1CE(数字1) INVOICE 不一致 不一致 0.04 / 0.57 0.00 / 0.05
DOC-O012345(英字O) DOC-0012345(数字0) 不一致 不一致 0.54 / 0.71 0.00 / 0.15
1,25O.00(英字O) 1,250.00 不一致 不一致 0.04 / 0.76 0.00 / 1.00
ACMl(小文字L) ACME 不一致 不一致 0.02 / 0.42 0.00 / 0.00
DOC-O012345 DOC-O012345 一致 一致 0.99 / 0.98 1.00 / 1.00
1,25O.00 1,25O.00 一致 一致 0.97 / 0.91 1.00 / 1.00
INVO1CE INVO1CE 一致 一致 0.99 / 0.88 1.00 / 1.00
DOC-2026 DOC-2028 不一致 不一致 0.01 / 0.04 0.00 / 0.00

1,25O.00(ゼロの位置に英字の O)と 1,250.00 は、Gemini が96トークン考えたうえで確率1.00の一致と答えました。
OCR の誤読を親切に読み替えてしまった形です。
突合の目的は「書類に書かれた値の違いを検知すること」なので、これが見逃しにつながってしまいます。
Jev も0.76と高めでしたが、閾値0.8を下回ったため人の確認に回せます。

5.8 欠損(7件)

A B exact semantic Jev(exact / semantic) Gemini(exact / semantic)
(空) (空) 一致 一致 0.97 / 0.21 1.00 / 1.00
(空) 1,250.00 不一致 不一致 0.01 / 0.03 0.00 / 0.00
N/A (空) 不一致 一致 0.03 / 0.11 0.00 / 0.80
- 該当なし 不一致 一致 0.02 / 0.43 0.00 / 1.00
N/A N/A 一致 一致 0.98 / 0.23 1.00 / 1.00
該当なし 該当なし 一致 一致 0.98 / 0.88 1.00 / 1.00
N/A 0 不一致 不一致 0.02 / 0.11 0.00 / 0.10

正答数だけを見ると、Jev が3/7、Gemini が7/7で、Gemini の圧勝に見えます。
ただ、このカテゴリは「値が無いもの同士を、同じ値とみなしてよいか」という、人によって判断が分かれる部分です。
今回は正解ラベルを「一致」としましたが、業務によっては「不一致」や「要確認」とするほうが自然な場面もあります。
Gemini は空欄同士の比較に304トークンも考えたうえで、1.00と言い切りました。
Jev は外した4件で0.11〜0.43という低めの確率を返しており、閾値0.8の運用なら人の確認に回せます。
判断が分かれるケースを断定せずに人へ回せるという点では、このカテゴリにおける Jev の挙動のほうが扱いやすいと考えています。
なお、semantic の正答数の差は Jev が96/106、Gemini が101/106ですが、欠損の7件を除くと93/99と94/99になり、ほぼ差はありません。

5.9 集計(8件)

A B exact semantic Jev(exact / semantic) Gemini(exact / semantic)
合計 500 KG 明細: 200 KG / 200 KG / 100 KG 不一致 一致 0.01 / 0.98 0.00 / 1.00
合計 500 KG 明細: 200 KG / 200 KG / 50 KG 不一致 不一致 0.01 / 0.89 0.00 / 0.00
10 CTNS CTN No.1-10 不一致 一致 0.02 / 0.93 0.00 / 0.95
1 箱 管理番号 ABCD1234567 (1箱) 不一致 一致 0.01 / 0.91 0.00 / 0.20
明細: 200 KG / 200 KG / 100 KG 明細: 200 KG / 200 KG / 100 KG 一致 一致 0.98 / 0.98 1.00 / 1.00
CTN No.1-10 CTN No.1-10 一致 一致 0.99 / 0.98 1.00 / 1.00
10 CTNS CTN No.1-9 不一致 不一致 0.01 / 0.22 0.00 / 0.00
合計 500 KG 明細: 500 KG / 500 KG 不一致 不一致 0.02 / 0.73 0.00 / 0.50

明細の合計が450 KG なのに500 KG と書かれているケースは、Jev が0.89で一致と誤認したのに対し、Gemini は0.00で不一致と正しく判定しました。
このケースでは thinking に100トークン使っており、裏で足し算を行ったものと思われます。
一方で 1 箱管理番号 ABCD1234567 (1箱) は、Jev が0.91で正解し、Gemini は0.20で外しましたが意味的に一緒かというと不一致な気もするのでGeminiが正しいような気がします。

5.10 見えない差(8件)

A B exact semantic Jev(exact / semantic) Gemini(exact / semantic)
INV-2026-0912 INV-2026-0912␣ 不一致 一致 0.02 / 0.96 0.00 / 1.00
ACME␣␣CO., LTD. ACME CO., LTD. 不一致 一致 0.13 / 0.93 0.00 / 1.00
␣1,250.00 1,250.00 不一致 一致 0.03 / 0.98 0.00 / 1.00
ACME␠CO., LTD. ACME CO., LTD. 不一致 一致 0.03 / 0.94 0.00 / 1.00
1,250.00⏎ 1,250.00 不一致 一致 0.94 / 0.98 0.00 / 1.00
INV-2026-0912␣ INV-2026-0912␣ 一致 一致 0.93 / 0.98 1.00 / 1.00
ACME␠CO., LTD. ACME␠CO., LTD. 一致 一致 0.95 / 0.97 1.00 / 1.00
STEEL PIPE→SEAMLESS STEEL PIPE→SEAMLESS 一致 一致 0.98 / 0.97 1.00 / 1.00

6. まとめ

「Jev が速いのはテキスト生成をしないからで、LLM も出力を絞れば同等になるはず」という私の仮説は外れでした。
出力を十数トークンまで絞った Gemini 3.8 Flash でも、中央値で約2.3倍遅く、有料枠換算のコストは約19倍かかりました。
レイテンシの差は最初の入力を読み込む段階で生じ、コストの差は入力トークンの単価差や出力・thinking への課金有無によって開いています。
semantic の正答率は Gemini のほうが高く出たものの、差の大半は判断の分かれる欠損カテゴリによるもので、それ以外のカテゴリではほぼ同等でした。

両者の使い分けは次のように整理できそうです。

  1. 件数が多く、速度とコストがシビアな突合は Jev に任せる(算出された確率をそのまま「人による確認に回すか」の判定基準に使う)

  2. 明細の合計計算のように推論が必要な項目は生成型 LLM に任せるか、前回触れたように前段のプログラム側で計算しておく

  3. 生成型 LLM に確率を出力させても値が0か1に偏りやすいため、確信度判定としては過信しない

補足:DiffusionGemma を Jev のように使う取り組み

今回は Gemini 3.8 Flash で検証しましたが、Jev と同じような使い方ができるモデルは今後も増えていきそうです。
Google Gemma の公式アカウントでも、DiffusionGemma を Jev のように動かす試みが紹介されていました。
DiffusionGemma は、トークンを1つずつ順番に出力するのではなく、全体を一括で生成していく拡散モデルです。
ポストによると、選択肢の判定を1回の並列処理で済ませられ、DGX Spark 上では約0.2秒で結果が返ってくるとのことでした。
この仕組みは vLLM へのプルリクエストとして公開されています。
2026年9月21日時点ではまだマージされておらず、プロトタイプの段階です。

引用元: ポスト: "DiffusionGemma as Jev" | Google Gemma(X)

引用元: プルリクエスト: structured generation mode for DiffusionGemma model (Jev-like) | vllm-project/vllm

Jev はテキスト入力のみに対応していますが、DiffusionGemma は Gemma 4 の画像認識能力を引き継いでおり、画像を含めた判定にも利用できるそうです。

Inherits Gemma 4's spatial vision capabilities for complex visual and text decisions.

私自身はまだ試せていませんが、こちらも動かせるようになるのがとても楽しみです。

参考


AI白書2026 配布中

クラスメソッドが独自に行なったAI診断調査をもとに、企業のAI活用の現在地を調査レポートとしてまとめました。企業規模別の活用度傾向に加え、規模を超えてAI活用を進める企業に共通する取り組みまで、自社の現在地を捉えるためのヒントにぜひ。

AI白書2026

無料でダウンロードする

この記事をシェアする

DevelopersIO 2026

関連記事