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

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

TypeSafe の Jev というテキスト専用モデルを使って、書類の項目突合がどの程度の精度でできるか実測してみました。
2026.09.20

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

ある案件で、2つの書類に記載された同じ項目が意味的に一致しているかをチェックする機能を開発しました。
ここで厄介になるのが表記ゆれです。
例えば日付項目であれば、片方の書類には 2026/09/12、もう片方には 2026年9月12日 と書かれていたり、会社名が ACME CO., LTD.Acme Co., Ltd. で大文字小文字が異なっていたりします。
人が見れば同じだと分かりますが、文字列を単純比較するとすべて不一致になってしまいます。
かといって正規表現や自前の正規化ルールを書き始めると、元号や欧州式の小数点、法人格の略語表現などバリエーションが無数にあり、すべてを網羅するのは実質不可能です。

そのため、従来は Amazon Bedrock 経由で Claude モデルを呼び出し、意味的に一致しているかを判定していました。
しかし、大量の突合処理を回すとどうしても料金がかさんでしまいます。
そこで、TypeSafe の Jev が Claude モデルの代替として使えるのではないかと考えました。

Jev は文章を生成せず、型付きの質問に対して確率値だけを返すモデルです。
入力はテキストに限られますが、今回扱うのは文字列同士の比較ですので、Jev の得意な領域に収まります。

そこで本記事では、架空の書類項目107ケースを用意し、Jev がどれくらい正確に突合できるかを2026年9月20日に実測しました。
精度に加えて、レイテンシや消費トークン数も記録しています。
また、質問文を日本語で作成した場合と英語で作成した場合の両方を実行し、結果の差も比較しました。

1. 突合の前提条件

1.1 判定を2軸で持つ

実務における書類の突合では、書類や項目によって「1文字も違わず厳密に一致しているか(Exact)」で判断したい場合と、「表記が違っても同じ意味を指しているか(Semantic)」で評価したい場合があります。

そこで、Jev への質問を次の2本に分けました。

  • exact:文字列として1文字も違わないか

  • semantic:表記が違っても同じ値を指しているか

2026/09/122026年9月12日 であれば、exact は不一致、semantic は一致が期待する結果です。

もちろん、完全一致(exact)だけであれば、プログラム側で単純な文字列比較を行うほうが早くて確実です。
しかし、書類や項目ごとに「ここはプログラムで完全一致を判定し、あちらはモデルで意味的一致を判定する」といったロジックの切り分けを個別に作り込むと、コードやルールの管理が煩雑になります。
そのため、Jev を使って Exact と Semantic の両方をモデル側でまとめて判定させるとどうなるのかを検証したかった、というのがこの2軸を設けた理由です。

1.2 閾値は0.8

Jev の noul 型は、0から1の範囲で確率値を返します。
この確率値を一致・不一致の2値に分類する閾値は、exactsemantic のいずれも0.8に設定しました。

実際の現場でも、確信度が0.8を下回るものは要確認フラグを付けて人の目による確認に回す、という運用設計をよく見かけます。
表記ゆれを「同じ値である」と判定する処理は、誤った突合をすり抜けさせてしまうリスクがあります。
そのため、今回の検証でも現場の運用を意識し、0.8を下回るものを安全側に倒す基準として設定しました。

2. 環境

項目 内容
モデル jev-latest(レスポンスは jev-1.13.0 に解決)
エンドポイント POST /v1/systemone(ホストは api.typesafe.ai
実行環境 macOS 26.6.2、Python 3.14.6、標準ライブラリのみ
実行方法 逐次(並列なし)
ケース数 107
実行日 2026年9月20日

API の仕様は公式リファレンスに沿っています。
リクエストパラメータは state(判断材料となるデータ)、modelquestions(型付き質問のマップ)の3つのみです。
レスポンスには質問と同じキーで判定結果が返り、usage にトークン数が記録されます。

引用元: API reference | TypeSafe Docs

レイテンシを正確に測定するため、並列実行は行っていません。

3. 実行スクリプトと実行方法

3.1 テストデータの形式

テストデータは1ケース1行の JSONL 形式で用意しました。

cases.jsonl:

{"id": "2-1", "category": "date", "a": "2026/09/12", "b": "2026年9月12日", "expect_exact": false, "expect_semantic": true, "note": "日本語表記"}
{"id": "3-9", "category": "number", "a": "500 KG", "b": "1102.31 LBS", "expect_exact": false, "expect_semantic": true, "note": "単位換算が必要"}
{"id": "4-6", "category": "party", "a": "ACME TRADING CO.", "b": "ACNE TRADING CO.", "expect_exact": false, "expect_semantic": false, "note": "一字違いの別社"}

ab が突合対象の2つの値であり、expect_exactexpect_semantic にそれぞれの正解ラベルを設定しています。

107ケース全件の具体的な内容は、4章の一覧表 に記載しているデータそのものです。

3.2 スクリプト

外部ライブラリへの依存はなく、Python の標準ライブラリのみで動作します。

run_eval.py:

run_eval.py の全文(クリックすると展開します)
#!/usr/bin/env python3
"""JEV(TypeSafe AI System One)で横突合の精度・レイテンシ・トークンを測る。

各ケースについて値A・値Bを state として渡し、noul(yes/no確率)で
「完全一致か」「意味として同じ値か」を同時に聞く。1リクエストで両方answerが返る。

API: POST https://api.typesafe.ai/v1/systemone
     https://docs.typesafe.ai/api.md
"""

from __future__ import annotations

import argparse
import json
import os
import statistics
import sys
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor

ENDPOINT = "https://api.typesafe.ai/v1/systemone"
DEFAULT_MODEL = "jev-latest"

# 入力トークンのみ課金。出力トークンは無料
USD_PER_INPUT_MTOK = 0.042

# state のキー名。質問文と同じ言語に揃える。
STATE_KEYS = {"ja": ("値A", "値B"), "en": ("value_a", "value_b")}

# 質問文の言語だけを変えた2セット。中身の意味は揃えてある。
QUESTIONS = {
    "ja": {
        "exact": {
            "type": "noul",
            "instructions": (
                "値Aと値Bは、文字列として完全に同一か。"
                "空白・記号・大文字小文字・全角半角の違いも「同一ではない」と扱う。"
            ),
            "criteria": {
                "true": "1文字も違わず完全に同じ",
                "false": "1文字でも違う",
            },
        },
        "semantic": {
            "type": "noul",
            "instructions": (
                "値Aと値Bは、表記が違っていても同じ値を指しているか。"
                "これは2つの書類の同じ項目を突合している場面での判断である。"
                "片方が明細の羅列で、もう片方が合計値の場合は、"
                "明細をすべて足し合わせてから合計値と比べること。"
            ),
            "criteria": {
                "true": ("書式・言語・単位・略記の違いだけで、指している値は同じ。"
                         "または明細の合計が合計値と一致する"),
                "false": ("指している値そのものが違う、または片方にしか値がない。"
                          "または明細の合計が合計値と一致しない"),
            },
        },
    },
    "en": {
        "exact": {
            "type": "noul",
            "instructions": (
                "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."
            ),
            "criteria": {
                "true": "Identical character for character.",
                "false": "They differ by at least one character.",
            },
        },
        "semantic": {
            "type": "noul",
            "instructions": (
                "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."
            ),
            "criteria": {
                "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."
                ),
            },
        },
    },
}

def build_questions(lang: str) -> dict:
    """1リクエストで聞く質問セットを組み立てる。"""
    src = QUESTIONS[lang]
    return {"exact": src["exact"], "semantic": src["semantic"]}

def call_jev(case: dict, questions: dict, api_key: str, model: str,
             timeout: float, lang: str) -> dict:
    """1ケースをJEVに投げ、応答とレイテンシを返す。"""
    key_a, key_b = STATE_KEYS[lang]
    body = json.dumps({
        "state": {key_a: case["a"], key_b: case["b"]},
        "model": model,
        "questions": questions,
    }, ensure_ascii=False).encode("utf-8")

    req = urllib.request.Request(
        ENDPOINT,
        data=body,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        method="POST",
    )

    started = time.perf_counter()
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            payload = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="replace")[:500]
        return {"error": f"HTTP {exc.code}: {detail}",
                "latency_ms": (time.perf_counter() - started) * 1000}
    except Exception as exc:  # ネットワーク断・タイムアウト
        return {"error": f"{type(exc).__name__}: {exc}",
                "latency_ms": (time.perf_counter() - started) * 1000}

    return {"payload": payload,
            "latency_ms": (time.perf_counter() - started) * 1000}

def judge(case: dict, result: dict, thresholds: dict[str, float]) -> dict:
    """応答を期待値と突き合わせて1行分のレコードにする。"""
    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"]
    answers = payload.get("answers", {})
    usage = payload.get("usage") or {}

    row["model"] = payload.get("model")
    row["input_tokens"] = usage.get("input_tokens")
    row["output_tokens"] = usage.get("output_tokens")

    for key in ("exact", "semantic"):
        prob = answers.get(key, {}).get("noul")
        expected = case[f"expect_{key}"]
        got = None if prob is None else prob >= thresholds[key]
        row[f"{key}_prob"] = prob
        row[f"{key}_threshold"] = thresholds[key]
        row[f"{key}_got"] = got
        row[f"{key}_expected"] = expected
        # 期待値 None は正解が決まらないケースなので正答率から外す
        row[f"{key}_ok"] = None if expected is None else (got == expected)

    return row

def summarize(rows: list[dict]) -> 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"ケース数: {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 "-"
        th = ok_rows[0].get(f"{key}_threshold") if ok_rows else None
        print(f"{key:9s} 正答 {hit}/{len(scored)} ({rate})  [閾値 {th}]")

    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 not scored:
            continue
        hit = sum(1 for r in scored if r["semantic_ok"])
        print(f"{cat:12s} {hit}/{len(scored)} ({hit / len(scored):.0%})")

    for key in ("exact", "semantic"):
        misses = [r for r in ok_rows if r.get(f"{key}_ok") is False]
        if not misses:
            continue
        print(f"\n--- {key} を外したケース ---")
        for r in misses:
            print(f"{r['id']:5s} {r['note']}  期待={r[f'{key}_expected']} "
                  f"(p={r[f'{key}_prob']:.3f})")

    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}")

        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)
        print("\n--- トークン ---")
        print(f"入力 合計 {tin} (平均 {tin / len(ok_rows):.0f}/件) / 出力 合計 {tout}")
        print(f"概算コスト: ${tin / 1_000_000 * USD_PER_INPUT_MTOK:.6f} "
              f"(入力 ${USD_PER_INPUT_MTOK}/1Mtok、出力は課金対象外)")

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

def main() -> int:
    ap = argparse.ArgumentParser(description="JEVで横突合の精度・速度・トークンを測る")
    ap.add_argument("--cases", default="cases.jsonl")
    ap.add_argument("--out", help="デフォルトは results_<lang>.jsonl")
    ap.add_argument("--lang", choices=("ja", "en"), default="en",
                    help="質問文とstateキーの言語")
    ap.add_argument("--model", default=DEFAULT_MODEL)
    ap.add_argument("--threshold-exact", type=float, default=0.8,
                    help="exactのnoul確率を一致とみなす閾値")
    ap.add_argument("--threshold-semantic", type=float, default=0.8,
                    help="semanticのnoul確率を一致とみなす閾値")
    ap.add_argument("--concurrency", type=int, default=1,
                    help="並列数。レイテンシを正しく測るなら1のまま")
    ap.add_argument("--timeout", type=float, default=30.0)
    args = ap.parse_args()
    out_path = args.out or f"results_{args.lang}.jsonl"

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

    questions = build_questions(args.lang)

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

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

    thresholds = {"exact": args.threshold_exact,
                  "semantic": args.threshold_semantic}

    run = lambda c: judge(  # noqa: E731
        c, call_jev(c, questions, api_key, args.model, args.timeout, args.lang),
        thresholds)

    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)
    print(f"\n全体所要: {wall:.1f}s ({wall / len(rows):.2f}s/件)")
    print(f"明細: {out_path}")
    return 0

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

3.3 実行

API キーを環境変数に設定します。

export TYPESAFE_API_KEY="<YOUR_API_KEY>"

スクリプトとテストデータを配置したディレクトリに移動し、質問文を英語に指定してスクリプトを実行します。

python3 run_eval.py --lang en

日本語版を実行する場合も、出力先が自動的に results_ja.jsonl へ切り替わるため、英語版の結果ファイルを上書きする心配はありません。

python3 run_eval.py --lang ja

実行すると、以下のようなサマリが出力されます。

# 出力例
107 ケースを jev-latest に投げます (質問文 en / 並列 1)...

=== 結果サマリ ===
ケース数: 107  成功: 107  失敗: 0
exact     正答 106/107 (99.1%)  [閾値 0.8]
semantic  正答 96/106 (90.6%)  [閾値 0.8]

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

--- レイテンシ(ms) ---
平均 702 / 中央値 709 / p95 872 / 最小 537 / 最大 1001

--- トークン ---
入力 合計 54508 (平均 509/件) / 出力 合計 3852
概算コスト: $0.002289 (入力 $0.042/1Mtok、出力は課金対象外)

全体所要: 75.1s (0.70s/件)
明細: results_en.jsonl

出力された results_*.jsonl には、Jev が返した確率値がそのまま記録されます。

4. テストデータと結果(全107ケース)

表内の exact および semantic の列は正解ラベルです。
ja確率(exact / semantic)en確率(exact / semantic) の列には Jev が返した数値を、完全一致確率(exact)/ 意味的一致確率(semantic)の順で記載しています。
太字 で示した数値は、閾値0.8の判定で正解と一致しなかった結果です。

また、文字列内に含まれる不可視文字については、 を半角スペース、 を全角スペース、 を改行、 をタブとして表記しています。

4.1 完全同一(9件)

A B exact semantic ja確率(exact / semantic) en確率(exact / semantic)
INV-2026-0912 INV-2026-0912 一致 一致 0.98 / 0.96 0.99 / 0.98
1,250.00 1,250.00 一致 一致 0.97 / 0.97 0.98 / 0.98
ACME TRADING CO., LTD. ACME TRADING CO., LTD. 一致 一致 0.98 / 0.96 0.99 / 0.98
長文240字(末尾 ORIGIN JAPAN 長文240字(末尾 ORIGIN JAPAN 一致 一致 0.95 / 0.98 0.98 / 0.99
STEEL PIPE⏎SEAMLESS⏎50MM STEEL PIPE⏎SEAMLESS⏎50MM 一致 一致 0.93 / 0.96 0.98 / 0.98
ACME␠TRADING ACME␠TRADING 一致 一致 0.95 / 0.94 0.98 / 0.96
DOC-O0O12345 DOC-O0O12345 一致 一致 0.97 / 0.94 0.99 / 0.98
␣1,250.00␣ ␣1,250.00␣ 一致 一致 0.90 / 0.97 0.92 / 0.98
Sep 12, 2026 Sep 12, 2026 一致 一致 0.97 / 0.96 0.99 / 0.98

4.2 日付(13件)

A B exact semantic ja確率(exact / semantic) en確率(exact / semantic)
2026/09/12 2026年9月12日 不一致 一致 0.02 / 0.97 0.02 / 0.98
12-SEP-2026 2026-09-12 不一致 一致 0.02 / 0.97 0.02 / 0.98
Sep 12, 2026 12/09/2026 不一致 一致 0.02 / 0.69 0.02 / 0.71
令和8年9月12日 2026-09-12 不一致 一致 0.02 / 0.94 0.02 / 0.94
03/04/2026 2026-04-03 不一致 一致 0.02 / 0.89 0.02 / 0.77
03/04/2026 2026-03-04 不一致 対象外 0.02 / 0.94 0.02 / 0.94
2026/09/12 2026/09/13 不一致 不一致 0.02 / 0.04 0.01 / 0.03
2026/09/12 2025/09/12 不一致 不一致 0.02 / 0.11 0.01 / 0.03
2026年9月12日 2026年9月12日 一致 一致 0.97 / 0.96 0.99 / 0.98
12-SEP-2026 12-SEP-2026 一致 一致 0.98 / 0.97 0.99 / 0.98
令和8年9月12日 令和8年9月12日 一致 一致 0.98 / 0.97 0.99 / 0.99
2026/09/12 2026/12/09 不一致 不一致 0.01 / 0.04 0.01 / 0.02
令和8年9月12日 令和7年9月12日 不一致 不一致 0.02 / 0.03 0.02 / 0.02

なお、03/04/20262026-03-04 の組み合わせは、月/日形式(MM/DD)と解釈すれば一致、日/月形式(DD/MM)と解釈すれば不一致となり、文脈なしでは正解が一意に定まりません。
そのため、このケースは正答率の集計対象から除外しています。

4.3 数値・金額(18件)

A B exact semantic ja確率(exact / semantic) en確率(exact / semantic)
1,250.00 1250 不一致 一致 0.02 / 0.96 0.01 / 0.97
USD 1,250.00 $1,250.00 不一致 一致 0.02 / 0.97 0.02 / 0.98
1.250,00 1,250.00 不一致 一致 0.03 / 0.86 0.03 / 0.89
1250.0 1250.5 不一致 不一致 0.02 / 0.09 0.02 / 0.08
12,500 1,250 不一致 不一致 0.01 / 0.06 0.01 / 0.06
1,250.00 1,250.00 USD 不一致 不一致 0.02 / 0.88 0.02 / 0.96
USD 1,250 JPY 1,250 不一致 不一致 0.01 / 0.06 0.02 / 0.05
500 KGS 500.000 KG 不一致 一致 0.02 / 0.93 0.02 / 0.96
500 KG 1102.31 LBS 不一致 一致 0.01 / 0.88 0.01 / 0.96
10 CTNS 10 CARTONS 不一致 一致 0.02 / 0.95 0.02 / 0.98
USD 1,250.00 USD 1,250.00 一致 一致 0.96 / 0.97 0.98 / 0.99
1.250,00 1.250,00 一致 一致 0.95 / 0.96 0.98 / 0.98
500 KGS 500 KGS 一致 一致 0.94 / 0.95 0.98 / 0.98
1,250.00 USD 1,250.00 USD 一致 一致 0.96 / 0.97 0.98 / 0.99
1,250.00 1,205.00 不一致 不一致 0.01 / 0.06 0.02 / 0.05
500 KG 500 LBS 不一致 不一致 0.02 / 0.05 0.02 / 0.03
10 CTNS 10 PALLETS 不一致 不一致 0.01 / 0.24 0.02 / 0.16
500.00 KG 50.00 KG 不一致 不一致 0.01 / 0.03 0.01 / 0.03

500 KG1102.31 LBS の比較は、単位換算まで踏み込んで同一とみなす方針で正解ラベルを設定しています。
もしシステム要件として単位換算を許容しない運用であれば、正解ラベルの期待値は逆になります。

4.4 取引先名(15件)

A B exact semantic ja確率(exact / semantic) en確率(exact / semantic)
ACME CO., LTD. Acme Co., Ltd. 不一致 一致 0.03 / 0.91 0.02 / 0.94
ACME CO.,LTD. ACME CO., LTD. 不一致 一致 0.05 / 0.93 0.06 / 0.95
GLOBEX CORPORATION GLOBEX CORP. 不一致 一致 0.02 / 0.86 0.02 / 0.94
株式会社サンプル商事 サンプル商事株式会社 不一致 一致 0.03 / 0.78 0.03 / 0.75
ACME ACME 不一致 一致 0.03 / 0.92 0.02 / 0.94
ACME TRADING CO. ACNE TRADING CO. 不一致 不一致 0.02 / 0.18 0.02 / 0.18
Initech Inc. Initech Incorporated 不一致 一致 0.02 / 0.88 0.02 / 0.93
株式会社サンプル商事 株式会社サンプル商事 一致 一致 0.98 / 0.95 0.99 / 0.98
ACME ACME 一致 一致 0.96 / 0.91 0.98 / 0.94
GLOBEX CORP. GLOBEX CORP. 一致 一致 0.98 / 0.95 0.99 / 0.98
Initech Incorporated Initech Incorporated 一致 一致 0.98 / 0.95 0.99 / 0.98
ACME TRADING CO., LTD. ACME SHIPPING CO., LTD. 不一致 不一致 0.02 / 0.12 0.02 / 0.11
GLOBEX CORP. GLOBEX HOLDINGS CORP. 不一致 不一致 0.02 / 0.32 0.02 / 0.29
株式会社サンプル商事 株式会社サンプル物産 不一致 不一致 0.02 / 0.18 0.02 / 0.11
ACME CO., LTD. ACME CO., LTD. (Taiwan Branch) 不一致 不一致 0.02 / 0.21 0.02 / 0.25

4.5 住所(11件)

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

4.6 品名(10件)

A B exact semantic ja確率(exact / semantic) en確率(exact / semantic)
STEEL PIPE SEAMLESS 50MM SEAMLESS STEEL PIPE 50MM 不一致 一致 0.02 / 0.94 0.02 / 0.97
STEEL PIPE⏎SEAMLESS⏎50MM STEEL PIPE SEAMLESS 50MM 不一致 一致 0.02 / 0.95 0.03 / 0.96
STEEL PIPE (50MM) STEEL PIPE 50MM 不一致 一致 0.02 / 0.93 0.02 / 0.96
STEEL PIPE 50MM STEEL PIPE 60MM 不一致 不一致 0.02 / 0.03 0.01 / 0.03
長文(末尾 ORIGIN JAPAN 長文(末尾 ORIGIN KOREA 不一致 不一致 0.02 / 0.07 0.01 / 0.06
STEEL PIPE (50MM) STEEL PIPE (50MM) 一致 一致 0.97 / 0.96 0.99 / 0.98
長文(末尾 ORIGIN KOREA 長文(末尾 ORIGIN KOREA 一致 一致 0.95 / 0.98 0.99 / 0.99
STEEL PIPE SEAMLESS 50MM STEEL PIPE WELDED 50MM 不一致 不一致 0.01 / 0.10 0.01 / 0.09
STEEL PIPE 50MM BLACK STEEL PIPE 50MM GALVANIZED 不一致 不一致 0.01 / 0.09 0.01 / 0.09
SEAMLESS STEEL PIPE 50MM x 6000MM SEAMLESS STEEL PIPE 50MM x 6600MM 不一致 不一致 0.02 / 0.05 0.02 / 0.04

4.7 OCR誤読(8件)

A B exact semantic ja確率(exact / semantic) en確率(exact / semantic)
INVO1CE(数字1) INVOICE 不一致 不一致 0.07 / 0.57 0.04 / 0.57
DOC-O012345(英字O) DOC-0012345(数字0) 不一致 不一致 0.33 / 0.69 0.54 / 0.71
1,25O.00(英字O) 1,250.00 不一致 不一致 0.03 / 0.75 0.04 / 0.76
ACMl(小文字L) ACME 不一致 不一致 0.02 / 0.38 0.02 / 0.42
DOC-O012345 DOC-O012345 一致 一致 0.99 / 0.95 0.99 / 0.98
1,25O.00 1,25O.00 一致 一致 0.95 / 0.85 0.97 / 0.91
INVO1CE INVO1CE 一致 一致 0.98 / 0.89 0.99 / 0.88
DOC-2026 DOC-2028 不一致 不一致 0.02 / 0.08 0.01 / 0.04

OCR の誤読パターンについては、「文字が1文字でも異なっていれば別の値である」という方針で正解ラベルを設定しています。

4.8 欠損(7件)

A B exact semantic ja確率(exact / semantic) en確率(exact / semantic)
(空) (空) 一致 一致 0.85 / 0.20 0.97 / 0.21
(空) 1,250.00 不一致 不一致 0.01 / 0.05 0.01 / 0.03
N/A (空) 不一致 一致 0.02 / 0.15 0.03 / 0.11
- 該当なし 不一致 一致 0.02 / 0.22 0.02 / 0.43
N/A N/A 一致 一致 0.95 / 0.22 0.98 / 0.23
該当なし 該当なし 一致 一致 0.97 / 0.61 0.98 / 0.88
N/A 0 不一致 不一致 0.02 / 0.12 0.02 / 0.11

4.9 集計(8件)

A B exact semantic ja確率(exact / semantic) en確率(exact / semantic)
合計 500 KG 明細: 200 KG / 200 KG / 100 KG 不一致 一致 0.01 / 0.98 0.01 / 0.98
合計 500 KG 明細: 200 KG / 200 KG / 50 KG 不一致 不一致 0.01 / 0.91 0.01 / 0.89
10 CTNS CTN No.1-10 不一致 一致 0.01 / 0.90 0.02 / 0.93
1 箱 管理番号 ABCD1234567 (1箱) 不一致 一致 0.01 / 0.88 0.01 / 0.91
明細: 200 KG / 200 KG / 100 KG 明細: 200 KG / 200 KG / 100 KG 一致 一致 0.97 / 0.97 0.98 / 0.98
CTN No.1-10 CTN No.1-10 一致 一致 0.97 / 0.94 0.99 / 0.98
10 CTNS CTN No.1-9 不一致 不一致 0.01 / 0.25 0.01 / 0.22
合計 500 KG 明細: 500 KG / 500 KG 不一致 不一致 0.02 / 0.77 0.02 / 0.73

4.10 見えない差(8件)

A B exact semantic ja確率(exact / semantic) en確率(exact / semantic)
INV-2026-0912 INV-2026-0912␣ 不一致 一致 0.03 / 0.94 0.02 / 0.96
ACME␣␣CO., LTD. ACME CO., LTD. 不一致 一致 0.15 / 0.90 0.13 / 0.93
␣1,250.00 1,250.00 不一致 一致 0.05 / 0.96 0.03 / 0.98
ACME␠CO., LTD. ACME CO., LTD. 不一致 一致 0.03 / 0.93 0.03 / 0.94
1,250.00⏎ 1,250.00 不一致 一致 0.92 / 0.97 0.94 / 0.98
INV-2026-0912␣ INV-2026-0912␣ 一致 一致 0.78 / 0.96 0.93 / 0.98
ACME␠CO., LTD. ACME␠CO., LTD. 一致 一致 0.92 / 0.94 0.95 / 0.97
STEEL PIPE→SEAMLESS STEEL PIPE→SEAMLESS 一致 一致 0.95 / 0.91 0.98 / 0.97

5. 精度・レイテンシ・トークン

5.1 全体の数字

指標 日本語の質問文 英語の質問文
exact 正答 105/107(98.1%) 106/107(99.1%)
semantic 正答 97/106(91.5%) 96/106(90.6%)
レイテンシ 中央値 約0.69秒(686ms) 約0.71秒(709ms)
レイテンシ p95 約0.87秒(869ms) 約0.87秒(872ms)
レイテンシ 最大 約1.63秒(1633ms) 約1.00秒(1001ms)
入力トークン(平均) 607 509
入力トークン(合計) 64,994 54,508
出力トークン(合計) 3,852 3,852
コスト $0.0027(約0.41円) $0.0023(約0.35円)
107ケースの所要 75.9秒 75.1秒

※レイテンシの p95 とは、全リクエストを応答時間が短い順に並べたとき、下から95%目(上位5%の遅いリクエストを除いたライン)に位置する応答時間のことです。「大半(95%)のリクエストがこの秒数以内に完了した」という安定性の目安を示しています。

コストは、入力トークンの実測値に 100 万トークンあたり $0.042 の単価を掛けて算出した概算値です(1ドル=150円換算)。
Jev の課金体系では、出力トークンは課金対象外となっています。

引用元: Jev | Vercel AI Gateway

5.2 カテゴリ別の semantic 正答

カテゴリ 日本語 英語
完全同一 9/9 9/9
日付 11/12 10/12
数値・金額 17/18 17/18
取引先名 14/15 14/15
住所 11/11 10/11
品名 10/10 10/10
OCR誤読 8/8 8/8
欠損 2/7 3/7
集計 7/8 7/8
見えない差 8/8 8/8

欠損カテゴリのみ正答率が突出して低い結果となりましたが、それ以外のカテゴリでは概ね9割以上の正答率を維持しています。

6. 検証結果からわかったこと

6.1 表記ゆれの吸収と別物の識別は実用域

元号から西暦への変換(令和8年9月12日2026-09-12)や欧州式の小数点表記(1.250,001,250.00)、法人格の略称(GLOBEX CORPORATIONGLOBEX CORP.)、英語住所と日本語住所の突合など、高い確信度で一致と判定されました。自前で複雑な正規化ルールを作り込む手間に比べれば、この領域を Jev に任せる実用性は十分にあります。

また、異なるデータを「別物」として見抜く精度も安定しており、別物データ37件中29件で確率は0.32以下に沈みました。明らかに違うデータにはっきり低い確率が返るため、実運用にも組み込みやすい挙動です。

6.2 不一致を「一致」と誤認した割合と、グレーゾーンの挙動

突合システムの実運用で最も困るのは、「実際には不一致なのに、高確率で一致と嘘をついて自動通過してしまう(偽陽性)」ケースです。

今回の検証で「不一致」と定義した37件のうち、閾値0.8以上の高確率を出して一致と誤認してしまったのは以下の2件だけでした。

  • わずかな計算違い:合計 500 KG明細: 200 KG / 200 KG / 50 KG(明細計450 KG なのに確率0.89〜0.91で一致)

  • 通貨単位の有無:1,250.001,250.00 USD(通貨指定なしを米ドルとみなして確率0.88〜0.96で一致)

この2件を除けば、別物データはしっかりと低い確率(大半が0.32以下)で弾かれています。

一方で、一致を期待したものの閾値0.8を下回ったケース(前株・後株、日付形式、欠損値)は、モデルのミスというよりも人間が見ても判断が分かれるグレーゾーンでした。

  • 取引先名(前株と後株):株式会社サンプル商事サンプル商事株式会社 は確率0.75〜0.78にとどまりました。実務上、前株と後株で別法人として登記されているケースは珍しくないため、安易に高確率を出さず低めに見積もったのは妥当な挙動といえます。

  • 日付の順序(MM/DD か DD/MM か):Sep 12, 202612/09/2026(確率0.69〜0.71)や、03/04/20262026-04-03(確率0.77)は0.8を下回りました。文脈なしでは3月4日とも4月3日とも解釈できるため、確信度が上がらないのは自然です。

  • 欠損値の扱い:N/A 同士(確率0.22)や空欄同士(確率0.20)の比較では、semantic の確率が極端に低くなりました。「実体の値が存在しないもの同士を、同一の実体と言えるか」という解釈の分かれ目であり、曖昧なものを確実に弾いてくれたとも捉えられます。

このように、Jev は確信を持てるものには0.9以上の高スコアを返し、判断がつかないグレーゾーンには0.6〜0.7台の低スコアを出してくれます。
「わからないものはわからない」と正直に確率を下げてくれるため、0.8を閾値にしておけば、自信のない項目を確実に拾い上げて人の目による確認へ回す業務フローが非常に組みやすいと感じました。

6.3 実務で本当に注意すべき弱点

一方で、実務への組み込みにおいて明確な注意が必要なポイントも浮き彫りになりました。

  • 僅差の計算ミスを見逃す:合計値 500 KG に対し、明細計が 450 KG(200+200+50 KG)という僅差のケースでは、プロンプトで計算指示を与えても確率0.89を返し、一致と誤認してしまいました。

  • 言語によって確率に差が出る:日本語プロンプトと英語プロンプトで、同一データに対する確率値に開きが出るケースが見られました(例: 該当なし 同士は英語で0.88、日本語で0.61)。英語のほうがトークン消費が約13%安く済みますが、判定の出方は言語ごとに確認しておく必要があります。

  • 単位の有無を勝手に同一視する:1,250.001,250.00 USD を比較した際、確率0.88〜0.96で一致とみなされました。今回のように「片方にだけ単位や通貨記号が付いていて、もう片方にはない」場合、モデルが親切に補完して同一視してしまいます。「片方にしか単位がない場合は同一とみなさない」「単位が完全に異なる場合は注意すること」といった前提ルールをプロンプトに書いておいた方がよかったと感じました。

なお、末尾に改行コードが含まれるケース(1,250.00⏎1,250.00)は完全一致判定で一致(確率0.92〜0.94)となりましたが、末尾改行の有無は実務上同じ意味を持つことが多いため、運用上の影響は軽微です。

7. 実運用に向けた対策

上記の検証結果や弱点をふまえると、実業務に組み込む際は以下の2点を設計に組み込むのが安全です。

  1. 計算が必要なものは人手確認に回すか外で解く:明細の足し算や集計値の突き合わせは、プログラム側であらかじめ合算した確定値同士を Jev に渡すか、そもそも「比較に計算が必要か」を判定させて該当した場合は自動処理を通さず人間の確認へ回す運用設計にします。Jev のドキュメントを読んでも計算に弱いことが記載されています。

  2. 単位の有無や差異はプロンプトで縛る:単位が完全に異なる場合は気をつけることや、片方にだけ単位があって片方にない場合は注意して確信度を下げることなど、単位に関するルールをプロンプトで事前に明示しておきます。

8. まとめ

架空の書類項目107ケースで検証した結果、Jev は日付・数値・社名・住所などの表記ゆれを0.89以上の高確率で安定して吸収し、異なる項目は0.32以下で確実に弾く実用性を示しました。処理速度は中央値で約0.7秒、全107件のコストは約0.35〜0.41円(約$0.002)と極めて安価です。

Jev の強みは、自信のない曖昧なデータに対して正直に低い確率を出し、「人の目で確認すべきもの」を自然に浮き彫りにしてくれる点にあります。一方で、計算が求められる処理は見逃してしまう弱点もあるため、すべてを丸投げするのではなく役割分担が重要です。

足し算や欠損値の判定は手前のプログラムで済ませ、Jev には「表記ゆれの自動解消」と「判断に迷うグレーゾーンの洗い出し」を任せる。この分担でシステムを組むのが、最も安全で効果的な使い方であるように思えます。

参考

この記事をシェアする

DevelopersIO 2026

関連記事