はじめに
商品マスタで「商品名からカテゴリを付ける」処理を、正規表現・LLM2種・Jevの4手法に同じ100件を通し、正解ラベルを手で付けて採点しました。結論は次の3行です。
- 正答率は正規表現55%、Opus 5とgpt-5.6-lunaが93%、Jevが85%。Jevは正答率ではLLMに8ポイント負けた
- それでもJevを選ぶ理由は確信度にある。確信度1.00の43件は98%的中、0.50未満の12件は33%しか当たらない。当たらない行をJev自身が申告してくる
- 確信度0.5でしきい値を切ると、自動処理に回した88件の的中率は92.0%まで上がる。人が見るのは12件で済む
対象読者: 商品名・品名・問い合わせ文などの短いテキストを、キーワードのルール表で分類している人。
この記事ではJevの基本的な使い方やプリミティブの解説はしません。すでに解説記事が多く出ているので、そちらに譲ります。ここで扱うのは「既存のルールベース処理を置き換えたら何が変わるか」だけです。
参考文献
検証の設計
データ
Open Food Facts から「日本の商品」として登録されているものを取得し、かな・カタカナ・CJK漢字のいずれかを含む商品名だけを残して、乱数シードを固定して100件を抽出しました。母集団は489件です。
この絞り方だと漢字だけの中国語名も通ります。実際に「好侍百梦多咖喱」(ハウス バーモントカレー)が混ざりました。言語判定まではしていません。
有志が登録したデータなので、実際の商品マスタと同じくらい汚れています。抽出された100件にはこういう名前が入っていました。
| 商品名 | 何が厄介か |
|---|---|
| 生茶 | 種別を示す語が「茶」しかなく、ブランド名で完結している |
| 超熟 6枚スライス | 食パンだが「パン」の字が無い |
| 淡麗 グリーンラベル〈生〉 | ビールだが酒を示す語が無い |
| マルエフ | 同上。しかも略称 |
| 好侍百梦多咖喱 | 中国語表記のカレールウ |
| グリーンティー | 実体は抹茶アイス |
| 究極の水ゼリー | 「水」が入るが菓子 |
| ホワイトサワー ノンアルコール | 「サワー」が入るが酒ではない |
商品マスタを触ったことがある人なら、この時点で嫌な予感がすると思います。
カテゴリ定義
飲料・酒類・菓子デザート・乳製品・パンシリアル穀類・調味料油・加工食品惣菜・その他の8つを定義しました。定義文は1つのPythonモジュールに置いてあります。
LLMとJevにはこの定義文をそのまま渡しています。正規表現だけは定義文を機械的に使えないので、同じ定義を読みながらキーワード表を手で書きました。
揃えたのは「同じ100商品」と「同じカテゴリ定義」の2つだけです。呼び出し形式は揃っていません。LLMには10商品ぶんの一覧と出力形式の指示を加えたプロンプトを送り、Jevには1商品を state に、カテゴリ定義を choice の criteria に入れて送っています。
境界は事前に決めています。牛乳そのものは乳製品、缶コーヒーや乳飲料は飲料、アイスは菓子デザートです。この線引きを曖昧にすると後の突き合わせが成立しません。
4つの手法
| 手法 | 実行方法 |
|---|---|
| 正規表現 | キーワード表を上から順に評価し、最初に当たったカテゴリを採用 |
| Opus 5 |
claude -p --model opus を10件ずつのバッチで10回 |
| gpt-5.6-luna |
codex exec -m gpt-5.6-luna を同じバッチ構成で10回 |
| Jev |
POST https://api.typesafe.ai/v1/systemone を1商品につき1回、計100回 |
正規表現はわざと弱くしていません。酒類・乳製品・飲料・菓子などに思いつく限りのキーワードを並べ、7カテゴリぶんのルールを書いています。
Jevだけ1件ずつにしたのは、この検証でそう決めたからです。Jevは1リクエストにつき state は1つですが、questions は複数送れます。複数の商品を構造化して state に詰め、商品ごとに質問を立てればバッチ化もできます。今回はLLM側のバッチと条件が混ざるのを避けて、1商品1リクエストに揃えました。
正解ラベル
100件すべてに手でカテゴリを付けました。判定結果を見てから付けると引きずられるので、4手法の出力を横に並べたExcelで、定義シートを見ながら1行ずつ埋めています。この記事の正答率はすべてこのラベルを正としたものです。
正答率は正規表現55%、LLM93%、Jev85%
誤りの内訳は「その他と答えて外した」ぶんと「別カテゴリに誤爆した」ぶんに分けています。3列の合計が100件です。
| 手法 | 正解 | 「その他」と答えて外した | 別カテゴリに誤爆 |
|---|---|---|---|
| 正規表現 | 55 | 32 | 13 |
| Opus 5 | 93 | 0 | 7 |
| gpt-5.6-luna | 93 | 2 | 5 |
| Jev | 85 | 4 | 11 |
正規表現が「その他」と答えたのは36件で、うち4件は本当に「その他」が正解だったため、外したのは32件です。
正直に書くと、始める前は「判断特化モデルなんだからJevが一番当たるだろう」と思っていました。外れました。 LLM2種が93%で並び、Jevは85%です。8ポイントの差は100件では小さくありません。
一方で正規表現の55%は予想以上にひどい数字でした。半分近くを外しています。
Jevの確信度は実際に当たり外れと対応していた
ここからがJevを使う理由です。Jevは choice で選んだカテゴリと一緒に confidence と全選択肢の確率分布を返します。確信度の帯ごとに的中率を出すとこうなりました。
| 確信度 | 件数 | 的中率 |
|---|---|---|
| 1.00(0.999以上) | 43 | 98% |
| 0.90以上1.00未満 | 22 | 91% |
| 0.70以上0.90未満 | 17 | 82% |
| 0.50以上0.70未満 | 6 | 83% |
| 0.50未満 | 12 | 33% |
0.50〜0.70と0.70〜0.90がほぼ並んでいる以外は、確信度が下がるほど的中率も下がります。とくに0.50未満の12件は3分の1しか当たっていません。この100件では、Jevが外す行は低い確信度に寄っていました。
実際の誤り15件のうち、確信度0.50未満だったものが8件を占めます。たとえば北海道の焼き菓子「元祖 山親爺」は確信度0.15で酒類と答えていました。外していますが、0.15という数字が「これは信用するな」と言っています。
逆に確信度1.00で外したのは1件だけで、「カロリーメイト ブロック チョコレート味」でした。これは後述の境界ケースです。
ここは同じ100件の中で見た相関であって、別のデータで同じ傾向が出る保証はありません。しきい値を実務で決めるなら、自分のデータで同じ表を作り直す必要があります。
しきい値を切ると自動処理の精度が上がる
確信度でふるいにかけたときの数字です。
| しきい値 | 自動処理する件数 | その的中率 | 人が見る件数 |
|---|---|---|---|
| 0.5以上 | 88 | 92.0% | 12 |
| 0.7以上 | 82 | 92.7% | 18 |
| 0.9以上 | 65 | 95.4% | 35 |
Jev全体の正答率は85%ですが、確信度0.5で切るだけで自動処理分は92.0%に上がります。人が見るのは12件だけです。0.9まで上げれば95.4%ですが、人が見る件数は35件に増えます。このトレードオフを数字で置けるのが大きい。
LLMは93%と単体では強いものの、今回のCLI経由の出力には確信度がありません。どの7件が間違っているか分からないので、93%を受け入れて全件流すか、全件を人が見るかになります。
ただしこれはLLMの限界ではなく、今回の測り方の問題です。OpenAI互換APIの logprobs を使えばトークンの確率は取れますし、プロンプトで自己申告の確信度を出させることもできます。前者は「選択肢の確率」そのものではなく、後者は較正されている保証がありません。同じ土俵で比べるならここを揃えて測り直す必要があります。Jevの利点は、確率を返すことが仕様として最初から入っている点です。
一方で今回の正規表現実装には「自信がない」という出力そのものが無いので、間違いを間違いとして検出する手段がありません。ルールごとに信頼度を持たせる作りにすれば近いことはできますが、そのスコアが較正されているかは別途測る必要があります。
今回の検証の限界
数字をそのまま信じないでほしい点が3つあります。
1. 100件は少ない。 正答率の差が8ポイントあっても、100件では偶然の幅に収まる可能性があります。差を主張するなら件数を増やす必要があります。
2. 食品に偏っている。 Open Food Factsは食品データベースなので、日用品や生鮮が入っていません。実際のPOSマスタはもっと雑多です。
3. 各手法とも1回しか流していない。 LLMは温度パラメータを指定していないので、同じプロンプトでも実行のたびに結果が変わり得ます。Jevについても、同じ入力を繰り返して結果が一致するかは確認していません。決定的に見えるという印象だけで、この検証では裏が取れていません。確実に決定的なのは正規表現だけです。
リクエストで引っかかった2つのところ
criteria の形が質問タイプによって違う
Jevの questions は配列ではなくオブジェクトで、キーが質問IDになります。ここまでは公式ドキュメントどおりなのですが、criteria の形が質問タイプごとに違います。
# choice: 選択肢名をキーにしたマップ
"criteria": {"飲料": "そのまま飲む液体。...", "酒類": "アルコールを含む飲料。..."}
# score: 配列。順にレベル0から割り当てられる
"criteria": ["まったく当てはまらない", "少し当てはまる", "強く当てはまる"]
# noul: true / false をキーにしたオブジェクト
"criteria": {"true": "...", "false": "..."}
自分は最初 choice にも配列を渡してエラーを踏みました。3つとも「criteria」という同じ名前なので、1つ覚えると他も同じだと思い込みます。
カテゴリ定義をそのままマップとして渡せるのは、実装としてはきれいでした。Pythonの辞書をそのままJSONに載せるだけで済みます。
subprocess から CLI を呼ぶと標準入力で止まる
これはJevの話ではなく、比較対象のLLMを回すときに踏んだものです。codex exec をPythonの subprocess.run から呼んだら、10分待っても返ってきませんでした。
シェルで同じコマンドを < /dev/null 付きで直接叩くと5秒で終わります。そこで標準入力を疑いました。
subprocess.run(cmd, capture_output=True, text=True, stdin=subprocess.DEVNULL)
stdin=subprocess.DEVNULL を足したら通りました。ただしプロンプトは位置引数で渡しているので、CLIが標準入力を読む理由はコード上ありません。「標準入力待ちだった」と断定はできず、原因は特定できていないというのが正確なところです。
CLIをバッチから叩くときは、読む理由が無くても stdin を塞いでおくのが安全だと学びました。タイムアウトまで何も出力されないので、原因にたどり着くのに時間を使います。
まとめ
商品名のカテゴリ判定という、小売の現場でよくある処理に4手法を当てて採点しました。
正規表現は55%でした。誤りの内訳は、名前に種別語が無くて手が出ない取りこぼしが32件、文字列は当たるが商品が違う誤爆が13件です。後者はルールの優先順位を入れ替えても別の商品が壊れるので、根本的に解けません。
Jevは85%で、LLMの93%に負けました。判断特化モデルだから精度で勝つ、という期待は外れています。それでもJevに価値があるのは、**確信度1.00の43件が98%、0.50未満の12件が33%**という形で、外す行が低い確信度に寄っていたからです。確信度0.5で切れば自動処理88件が92.0%になり、人が見るのは12件で済みます。
正答率という1つの数字だけで選ぶとLLMになります。運用まで含めて設計するなら話が変わります。

