skip to content
barorin&

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以上が必要です。

Terminal window
pip install typesafe-sdk

APIキーは環境変数に入れます。

Terminal window
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.0
print(res.model) # jev-1.13.0
print(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.94
print(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.28
print(s.confidence) # 0.58
print(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つに集約されます。

  1. 答えの形を先に決める(Noul / Choice / Score)
  2. 必要な質問を1回のリクエストにまとめる
  3. 返ってきた確率としきい値の判断は、自分のコード側に置く

3つ目が特に大事です。Jevが返すのは判断の材料であって、結論ではありません。confidence が低いものを人に回すのか、そのまま流すのかを決めるのは、モデルではなくこちらの設計です。

計算と日付をコードに残し、テキストの意味だけをJevに渡す。この線引きができていれば、扱いやすいモデルだと思います。

次は、これを会計・監査の実務データに当てはめた記事を書く予定です。

この記事を書いた人

barorinのプロフィール画像

barorinCPA & Engineer

会計とITの二足のわらじで働く公認会計士です。Pythonを中心に、会計・監査の実務で使えるコードや、Ubuntu・Docker等の設定メモなどを書いています。