Jev入門|型付きの判断を返すSystem Oneモデルの基本
/ 19 min read
Table of Contents
はじめに
テキストを見て「どちらか」を決める作業は、実務のあちこちにあります。
- 摘要欄を読んで、その支出がどの費用区分かを決める
- 問い合わせメールを読んで、担当部署に振り分ける
- 添付ファイルの中身を見て、契約書か請求書かを判定する
- 記述が具体的かどうかを見て、追加の確認が要るかを決める
こういう判定を普通のLLMに投げると、返ってくるのは文章です。JSONで返すよう指示しても、キーが増えたり減ったり、数値のはずの欄に「高」と入っていたりします。 受け取った側のコードは、毎回それを検証してから使うことになります。そしてどれくらい確信を持って答えたのかが分かりません。
Jevは、この用途に絞られたモデルです。答えの形を先に宣言しておくと、その形の中の値と確率しか返ってきません。文章は返しません。
この記事では、Jevの基本的な使い方をPythonで確認します。会計・監査の業務にどう当てはめるかは別記事に分けました。
Jevとは何か
Jevは、TypeSafeが「System One」と呼ぶクラスの最初のモデルです。名前は経済学者のウィリアム・スタンレー・ジェヴォンズから取られています。
System Oneモデルは、文章を書くためではなく、ソフトウェアがそのまま使える判断を返すために作られています。状態(state)と型付きの質問(questions)を送ると、型付きの答えと確率が返ります。
| 通常のLLM | System One(Jev) | |
|---|---|---|
| 返すもの | 自由な文章 | 宣言した型の値と確率 |
| スキーマ外の出力 | 起こりうる | 構造的に発生しない |
| 確信度 | 分からない | confidence / 確率分布として返る |
| 得意なこと | 説明・生成・コード | 分類・採点・真偽判定 |
| 入力 | テキスト・画像・音声など | テキストのみ |
用途が違うので、置き換えの関係ではありません。判断はJev、説明の生成は別のモデルという分け方になります。
セットアップ
Python 3.10以上が必要です。
pip install typesafe-sdkAPIキーは環境変数に入れます。
export TYPESAFE_API_KEY="sk-..."最小のコードはこれだけです。
from typesafe_sdk import Noul, TypeSafeClient
with TypeSafeClient() as client: res = client.system_one( state="請求書の金額が契約書と違います。至急確認をお願いします。", questions={"is_urgent": Noul(instructions="送り手は急ぎだと伝えている")}, )
print(res.answers["is_urgent"].noul) # 0.0〜1.0print(res.model) # jev-1.13.0print(res.usage.input_tokens)TypeSafeClient() は環境変数 TYPESAFE_API_KEY を読みます。設定されていないと、リクエストを送る前の生成時点で落ちます。
TypeSafeError: No API key was provided. Pass api_key or set the TYPESAFE_API_KEY environment variable.モデルはデフォルトで jev-latest(現時点では jev-1.13.0)が使われます。
3つの質問型
Jevに投げられる質問は3種類だけです。これ以外はありません。
| 型 | 聞くこと | 返るもの |
|---|---|---|
Noul |
これは正しいか(はい/いいえ) | noul(はいの確率) |
Choice |
どれか(順序のない選択) | choice / probabilities / confidence |
Score |
どの段階か(順序のあるレベル) | score / legend / probabilities / confidence |
Noul — はい/いいえ
from typesafe_sdk import Noul
Noul(instructions="この文章には個人情報が含まれている")返るのは noul という数値ひとつです。
print(res.answers["has_pii"].noul) # 0.93ここが最初に間違えやすいところです。noul は「程度」ではなく「はいである確率」です。
- 1.0に近い … 強い「はい」
- 0.0に近い … 強い「いいえ」
- 0.5前後 … はいといいえが同じくらい。つまり判断がついていない
「0.5だから中くらい」と読むと間違えます。程度を測りたいときは Score を使います。
Choice — 順序のない選択
選択肢を辞書で渡します。キーが選択肢の名前、値がその説明です。
from typesafe_sdk import Choice, TypeSafeClient
with TypeSafeClient() as client: res = client.system_one( state={"摘要": "懇親会 一式", "勘定科目": "交際費"}, questions={ "expense_type": Choice( instructions="この支出の費用区分はどれか", criteria={ "meeting": "社内打合せの飲食", "entertainment": "取引先の接待・贈答", "other": "判断できない", }, ) }, )
a = res.answers["expense_type"]print(a.choice) # 'entertainment'print(a.confidence) # 0.94print(a.probabilities) # {'meeting': 0.02, 'entertainment': 0.96, 'other': 0.02}choice は最も確率の高い選択肢の名前です。probabilities には全選択肢の確率が入り、合計は1になります。
選択肢は255個まで指定できます。**入力が選択肢に収まらない可能性があるなら、other や none を必ず入れておきます。**入れないと、どれかに無理やり寄せた答えが返ります。
Score — 順序のあるレベル
criteria は配列です。低いほうから順に並べます。2〜10段階まで指定できます。
from typesafe_sdk import Score
Score( instructions="摘要が、第三者が取引内容を特定できる程度に具体的か", criteria=[ "内容がまったく特定できない", "費目は分かるが相手先や目的が不明", "相手先・目的が具体的に書かれている", ],)返り値はこうなります。
s = res.answers["detail"]print(s.score) # 0.28print(s.confidence) # 0.58print(s.probabilities) # {0: 0.72, 1: 0.28, 2: 0.0}print(s.legend) # {0: '内容がまったく特定できない', 1: ..., 2: ...}score が小数になるのは、各レベルの番号を確率で加重平均した値だからです。上の例なら 0×0.72 + 1×0.28 + 2×0.0 = 0.28 です。「0.28番目のレベル」という意味ではなく、0寄りの分布だと読みます。
レベルの説明を書くときのコツは3つです。
- 程度を表す言葉ではなく、具体的な状況を書く(「やや不十分」ではなく「相手先が書かれていない」)
- 説明の中に数字を入れない(Jevは数値の扱いが苦手です。後述します)
- 1つの質問で測る軸は1つにする
使い分け
| 聞きたいこと | 型 |
|---|---|
| 該当するか、しないか | Noul |
| 決まった選択肢のどれか | Choice |
| 低い〜高いのどのあたりか | Score |
Noul と Score の混同に注意します。「この候補者のスキルは高いか」を Noul で聞くと、返るのは**スキルの高さではなく「高いと言えるかどうかの確率」**です。程度を測るなら Score です。
confidence は確率ではない
Choice と Score には confidence が付きます。これは確率とは別のものです。
confidence は、確率分布がどれだけ1か所に集まっているかを0〜1で表した統計量です。選択肢が n 個のとき、最大確率を p とすると次の式になります。
confidence = (n × p − 1) / (n − 1)1つの選択肢に全部乗っていれば1.0、均等に散らばっていれば0です。つまり confidence は、**答えの中身ではなく「答えが割れていないか」**を示します。
これを使うと、処理を3段階に分けられます。
a = res.answers["expense_type"]
if a.confidence < 0.5: to_human_review(record) # 判断が割れている。人が見るelif a.confidence < 0.9: apply_with_flag(a.choice) # 採用するが、印を付けておくelse: apply(a.choice) # そのまま処理するしきい値は、間違えたときの損害の大きさで決めます。読むだけの処理なら低くてよく、取り消しにくい処理ほど高くします。
1回のリクエストに質問をまとめる
Jevは、同じ状態に対する複数の質問を並列に評価します。質問を増やしても応答時間はほとんど変わりません。課金対象は入力トークンだけです。
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
with TypeSafeClient() as client: res = client.system_one( state={"摘要": "懇親会 一式", "勘定科目": "交際費", "計上部門": "営業部"}, questions={ "expense_type": Choice( instructions="この支出の費用区分はどれか", criteria={ "meeting": "社内打合せの飲食", "entertainment": "取引先の接待・贈答", "other": "判断できない", }, ), "detail": Score( instructions="摘要が、第三者が取引内容を特定できる程度に具体的か", criteria=[ "内容がまったく特定できない", "費目は分かるが相手先や目的が不明", "相手先・目的が具体的に書かれている", ], ), "private_doubt": Noul( instructions="個人的な支出が混ざっている疑いを説明する必要がある", ), }, )
for name, ans in res.answers.items(): print(name, ans.type)ここで効いてくるのが、使うかどうか分からない質問も先に聞いておくという考え方です。「接待かどうかを判定してから、接待なら相手先が書かれているかを聞く」と2往復になります。両方まとめて聞いて、要らなかった答えは捨てればいいという割り切りです。往復が1回で済みます。
料金と制限
| 項目 | 値 |
|---|---|
| 現行モデル | jev-1.13.0(別名 jev-latest) |
| 料金 | 入力100万トークンあたり $0.042 |
| 出力トークン | 無料 |
| コンテキスト | 1リクエスト合計 64k トークン |
| うち state + 最長の質問 | 32k トークン |
| レート制限 | 250,000 トークン/秒、1,200 リクエスト/分 |
| 入力形式 | テキストのみ(文字列・JSON・文字列配列) |
| 言語 | 英語が主。他言語も通るが精度は落ちる |
画像・音声・動画は未対応です。また、**顧客ごとのファインチューニングはありません。**調整できるのは instructions と criteria の書き方だけです。
不得意なことを先に知っておく
公式ドキュメントには、Jev 1.13が苦手とする処理が明記されています。使う前にここを読んでおくと事故が減ります。
| 苦手なこと | 内容 |
|---|---|
| 計算 | 電卓ではない。数を数えられない。金額の大小比較も当てにできない |
| 日付の比較 | 日付を順序のある量ではなく文字列として読む。前後関係や経過日数は不正確 |
| 文字どおりに読む | 書いた質問に答える。意図を汲まない。否定や限定はそのまま解釈される |
| 間接的な推論 | 二重否定や多段の推論で精度が落ちる |
| 無関係な情報の多いstate | 関係ない記述が判断を引っ張る |
| 敵対的な入力 | データを疑わない。埋め込まれた指示に従ってしまうことがある |
| 指示と基準の矛盾 | instructions と criteria が食い違うと混乱する |
| 構造的な前提 | P(はい) + P(いいえ) = 1 は保証されない。しきい値は型をまたいで流用できない |
| 文章生成 | 生成用に訓練されていない。連鎖させても遅いだけ |
「文字どおりに読む」も実務では効いてきます。instructions に曖昧な表現を残すと、こちらの意図ではなく書いた文のとおりに判定されます。書いた質問がそのまま仕様になると考えて書きます。
エラー処理とリトライ
SDKには例外クラスとリトライ方針が用意されています。
from typesafe_sdk import ( RetryPolicy, TypeSafeAPIError, TypeSafeAuthenticationError, TypeSafeRateLimitError, TypeSafeClient,)
retry = RetryPolicy(max_retries=4, backoff_initial=0.5, backoff_max=8.0)
with TypeSafeClient(timeout=20.0) as client: try: res = client.system_one(state=..., questions=..., retry=retry) except TypeSafeRateLimitError: ... # レート超過。時間をおく except TypeSafeAuthenticationError: ... # APIキーが不正 except TypeSafeAPIError as e: ... # その他のAPIエラーRetryPolicy はデフォルトで max_retries=2、指数バックオフ、Retry-After ヘッダを尊重する設定になっています。タイムアウトのデフォルトは10秒です。
例外は TypeSafeError を頂点に、TypeSafeAPIError → 各ステータス別の順で継承されています。まとめて受けたいときは TypeSafeError で捕まえます。
おわりに
Jevの使い方は、結局のところ次の3つに集約されます。
- 答えの形を先に決める(
Noul/Choice/Score) - 必要な質問を1回のリクエストにまとめる
- 返ってきた確率としきい値の判断は、自分のコード側に置く
3つ目が特に大事です。Jevが返すのは判断の材料であって、結論ではありません。confidence が低いものを人に回すのか、そのまま流すのかを決めるのは、モデルではなくこちらの設計です。
計算と日付をコードに残し、テキストの意味だけをJevに渡す。この線引きができていれば、扱いやすいモデルだと思います。
次は、これを会計・監査の実務データに当てはめた記事を書く予定です。
