4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jev完全入門ガイド

4
Posted at

〜圧倒的な速さと低コストで、AIの判断をソフトウェアに組み込む〜


はじめに

Jevとは何か? 一言でいえば、Jevは「文章を書かないAI」です。

Jevは、AIスタートアップ企業のTypesafe AIが開発した、System One モデルと呼ばれる新しいカテゴリーのAIモデルです。ChatGPTのような一般的な生成AIが「人間が読むための文章」を作ることを目的としているのに対し、Jevは文章をいっさい生成しません。その代わりに、みなさんが渡した「state(評価してほしい対象)」と「questions(知りたいこと)」にもとづいて、あらかじめ型が決まった判断——選択肢のうちどれか、点数、あるいはYes/Noの確率——を、そのままプログラムが使える形で返してくれます。

たとえば「この問い合わせは請求・技術・営業のどれに振り分けるべきか」をJevに尋ねると、返ってくるのは説明文ではなく、"billing" のような値そのものと、その確からしさを表す確率です。返信文を読んで必要な部分を抜き出す、という手間がまるごと不要になるのです。この「文章ではなく、型のついた判断を返す」という一点が、JevとこれまでのAIとの最大の違いです。詳しい仕組みは、この後の第1章でじっくり説明していきます。

まず結論からお伝えします。筆者が考えるJevの最大の魅力は、なんといっても「圧倒的な速さ」と「圧倒的なコストの安さ」です。

Typesafe AIが公開しているベンチマークによれば、Jevは同種の判断タスクにおいて、最先端のLLM(いわゆるフロンティアモデル)と比べておよそ40倍から200倍という桁違いの速度で応答を返します。1回のリクエストにかかる時間は、目安としておよそ70ミリ秒から500ミリ秒程度とされており、人間がまばたきをする間もなく処理が完了してしまう速さです。

コストの面でも同様に大きな差があります。Jevの利用料金は、入力100万トークンあたりわずか0.042ドル(しかも出力トークンは無料)という水準に設定されています。Typesafe AIは、これによって主要な大規模LLMと比べて数百倍単位のコスト削減が可能になる、とアピールしています。

さらにJevは、あらかじめ定義した型(スキーマ)の外側にある値を、原理的に返すことができない設計になっています。つまり、「本来ありえないはずの分類名を答えてしまう」「形式が崩れた出力をしてしまう」といった、生成AI特有のいわゆる「ハルシネーション(もっともらしい誤りの生成)」が、構造上起こりえないのです。

一言メモ:ここで挙げた具体的な数値は、いずれもTypesafe AI自身が公表しているベンチマーク結果にもとづくものであり、まだ第三者機関による独立した検証がすべて済んでいるわけではありません。とはいえ、モデルの設計思想そのもの(後述する「型付きの判断を返す」という仕組み)を踏まえると、応答速度とコストの両面で、従来型のLLMとは一線を画す性能が出ることには、十分な理由があると筆者は考えています。数値は日々更新されていく可能性があるため、正確な最新情報は、必ず公式サイト(https://docs.typesafe.ai/)でご確認ください。

なぜ、これほどまでの速度とコストの違いが生まれるのでしょうか。その理由を理解するために、まずは「AI」という言葉から、みなさんと一緒に整理していきましょう。

みなさんは、「AI」と聞くとどんなものを思い浮かべるでしょうか。

おそらく多くの方が、チャット画面に質問を打ち込むと、文章で答えを返してくれるAI——いわゆる「生成AI」や「チャットボット」を思い浮かべると思います。ChatGPTのようなサービスがまさにそれで、こうしたAIは「LLM(大規模言語モデル)」と呼ばれる技術をベースにしています。

LLMは、人間が読んで理解できる自然な文章を生成することに長けています。ブログ記事を書いてもらったり、難しい概念をわかりやすく説明してもらったり、プログラムのコードを書いてもらったりと、実にさまざまな用途に使えます。

ところが、いざ「AIをソフトウェアの部品として組み込みたい」と考えたときに、LLMには少し困った性質があります。それは、LLMが本来「人間が読むための文章」を作るように設計されているという点です。

たとえば、あるカスタマーサポートのチケット(問い合わせメッセージ)を見て「このチケットは "billing"(請求関連)、"technical"(技術的な問題)、"sales"(営業関連)のどれに振り分けるべきか」をプログラムで自動判定したいとします。LLMにこの判定をやらせようとすると、次のようなステップを踏む必要が出てきます。

  1. 「以下のチケットをbilling/technical/salesのいずれかに分類してください」というプロンプト(指示文)を作る
  2. LLMに送信する
  3. LLMが「このチケットは billing に分類するのが適切だと考えられます。なぜなら……」といった文章を返してくる
  4. プログラム側で、その文章の中から実際に使いたい単語(この場合は "billing" という単語)を抜き出す(パースする)処理を書く
  5. LLMの出力形式が微妙に変わってしまったり、余計な前置きがついてしまったりすると、パース処理が壊れる

この「文章を生成させて、それを無理やりプログラムが扱える形に変換する」という作業には、地味ながら大きなコストがかかります。出力のブレを吸収するための例外処理を書いたり、プロンプトの言い回しを何度も調整したり、たまにLLMが分類名を微妙に言い換えてしまって処理が失敗したり——こうした苦労を経験したことがある方も多いのではないでしょうか。

この課題に対して、まったく違うアプローチで応えようとしているのが、これからみなさんと一緒に学んでいく Typesafe AI という会社が提供する Jev(ジェブ) というAIモデルです。

Jevは、文章を生成するのではなく、あらかじめ型(データの形)が決まった「判断」そのものを返してくれます。「billingかtechnicalかsalesか」という質問を送れば、その3つの選択肢のうちどれか一つが、確率つきの構造化されたデータとして返ってくるのです。文章を生成させてパースするという回りくどいプロセスが、まるごと不要になります。

そして、これこそが冒頭で述べた「圧倒的な速さ」と「圧倒的な安さ」の正体でもあります。一般的なLLMは、文章を一語一語(正確には「トークン」という単位ごとに)順番に生成していくため、答えが長くなるほど処理に時間もコストもかかります。一方でJevは、そもそも長い文章を生成する必要がなく、あらかじめ定義された選択肢や数値を、一度の並列処理でまとめて算出するだけで済みます。「文章を書く」という重い作業をまるごと省略している、と考えるとイメージしやすいかもしれません。この設計の違いこそが、Jevと従来のLLMとを分ける、最も本質的なポイントなのです。

このガイドでは、Python未経験に近い方でも読み進められるように、コード例を豊富に用意しながら、Jevというモデルの考え方、基本的な使い方、そして実際の業務で使えるような設計パターンまでを、3部構成でじっくりと解説していきます。

第1部となる本パートでは、まず「なぜJevのような仕組みが必要なのか」という背景を丁寧に押さえたうえで、開発環境の準備、最初のAPI呼び出し、そしてJevの心臓部である「State(状態)」と「Primitives(プリミティブ)」という2つの基本概念について、実際に手を動かしながら学んでいきます。

それでは、さっそく始めていきましょう。


第1章:Jevとは何か——「System One モデル」という新しい考え方

1-1. LLMとJevの違いをもう少し詳しく見てみる

前置きで触れたとおり、Jevは文章を生成するAIではありません。Typesafe AIはJevのことを、**「System One モデル」**という独自のカテゴリーに分類しています。

「System One」という名前は、心理学者のダニエル・カーネマンが書いた有名な書籍『ファスト&スロー』の中に出てくる概念に由来しています。この本では、人間の思考には2つのモードがあるとされています。

  • System 1(速い思考):直感的で、瞬時に、ほとんど意識せずに働く思考。たとえば「このメッセージ、なんだか急いでいそうだな」と一瞬で感じ取るような判断。
  • System 2(遅い思考):じっくりと時間をかけて、意識的に、論理的に検討する思考。たとえば複雑な数式を紙に書いて解くような作業。

Jevが目指しているのは、まさにこの「System 1」的な判断を、ソフトウェアの中で高速かつ大量に再現することです。「このメッセージは緊急性を伝えているだろうか?」「この顧客はどのくらい苛立っているだろうか?」——こういった、経験のある人間なら数秒で下せるような直感的な判断を、プログラムから呼び出せる関数のような形で提供してくれるのがJevなのです。

公式ドキュメントでは、この点についてこんな趣旨のことが説明されています。Jevは、テキストで表現された入力(=これから説明する「state」)に対して問いかけを行い、型のついた答えと、その答えに対する確率(どれくらいそれらしいか)をそのまま返す、と。返信文を作ったり、理由を長々と説明したりすることはしません。

1-2. なぜ「型」が大事なのか

みなさんがプログラミングを学んだことがあるなら、「型」という言葉には馴染みがあるかもしれません。まだの方のために簡単に説明すると、「型」とはデータの種類や形を表すものです。

たとえば「今日の気温」を表す変数があったとして、それが 25.4 という数値(float型)で表されるのか、"暖かい" という文字列(string型)で表されるのかによって、プログラムでの扱い方がまったく変わってきます。数値であれば「30度を超えたら警告を出す」といった比較演算が簡単にできますが、文字列だと「"暖かい" は "暑い" より高いのか低いのか」をプログラムが自動では判断できません。

LLMに「このメッセージは緊急性が高いですか?」と尋ねると、返ってくるのはたいてい「はい、このメッセージは緊急性が高いと考えられます。理由は……」といった自由な文章です。これは人間には読みやすいのですが、プログラムから見ると「型が定まっていないデータ」です。この中から「緊急かどうか」という情報(boolean、つまりTrue/Falseのような値)を取り出すには、テキスト解析が必要になります。

一方でJevに同じ質問をすると、0.999 のような 0から1の間の数値(「はい」である確率)が直接返ってきます。この数値はプログラムの中で即座に if 確信度 > 0.5: のような条件分岐に使えます。文章を読み解く必要がまったくありません。

この「型が保証されている」という性質こそが、Jevのドキュメントの随所に出てくる「type-safe(型安全)」という考え方の核心です。Typesafe AIという社名自体が、この思想をそのまま表していると言ってよいでしょう。

1-3. Jevが今のところ扱えるデータ

ここで一つ、大事な制約について触れておきます。本ガイド執筆時点(2026年9月)において、Jevが受け付けられる入力はテキストのみです。

具体的には、次の3つの形式のいずれかを入力として渡せます。

  • 文字列(string)
  • JSONオブジェクト
  • テキストの配列(array)

画像や音声、動画といったマルチモーダルな入力には、今のところ対応していません。公式ドキュメントには「(まだ)」という表現とともにこの制約が明記されており、将来的な拡張の可能性がにじませてありますが、現時点ではテキストベースの判断に用途が絞られている、と理解しておいてください。

とはいえ、テキストで表現できる情報は非常に幅広いものです。カスタマーサポートのメッセージ、レビューコメント、契約書の条文、ログファイルの中身、JSON形式のアプリケーション状態——これらはすべてJevの入力として扱うことができます。本ガイドを通して、その幅広さを実感していただけるはずです。

1-4. 「原子的な質問」という考え方——本ガイドで最も大切な概念

ここで、公式ドキュメントの随所で強調されている、非常に重要な考え方を紹介します。それは「質問は、できるだけ小さく、具体的で、原子的(atomic)なものにする」という原則です。

Jevのようなモデルにうまく力を発揮してもらうためには、1つの質問で1つの明確な判断だけを問うことが大切です。「知識のある人が、適切な文脈さえあれば数秒でパッと答えられるような、直感的な良し悪しの判断(gut-check judgment)」——公式ドキュメントはこのようなイメージで質問を設計することを勧めています。

たとえば、あるスタートアップの事業計画書(ピッチ)を評価したいとします。このとき、

  • 「このピッチは投資する価値がありますか?」

という一つの大きな質問を投げるのは、あまり良い設計ではありません。なぜなら、この判断は「市場規模は十分か」「技術的に実現可能か」「競合と比べて差別化できているか」といった、複数の独立した要因の組み合わせによって成り立っているからです。長い推論や、複数の観点を頭の中で同時にバランスさせる必要がある質問は、Jevのような「速い思考」を担うモデルには向いていません。

そこで公式ドキュメントが提案しているのが、次のようなアプローチです。

  • 「この事業の市場規模は十分に大きいか?」
  • 「この技術は現実的に実現可能か?」
  • 「競合他社と比べて、明確な差別化ポイントがあるか?」

というように、判断材料となる要因を一つひとつ独立した小さな質問に分解し、それぞれをJevに問いかけます。そして、それぞれの答え(数値)を受け取ったあとで、プログラムのコード側で重み付けをしながら組み合わせ、最終的な評価スコアを算出するのです。

この設計には大きなメリットがあります。もし「市場規模よりも技術的実現性を重視したい」というように優先順位を変えたくなった場合、プロンプトの文章をあれこれ書き直す必要はなく、コード側の重み係数の数字を変えるだけで済むのです。これは、AIの振る舞いをソフトウェアのロジックとして明示的にコントロールできるということを意味しています。

この「大きな判断を分解し、小さな質問の組み合わせとしてコードで合成する」という考え方は、本ガイドのあらゆる場面で繰り返し登場します。ぜひこの段階で、頭の片隅にしっかりと置いておいてください。


第2章:開発環境を整えよう

それでは、実際に手を動かしてJevを使う準備を進めていきましょう。この章では、Python環境へのSDK(ソフトウェア開発キット)のインストールと、APIキーの取得方法を解説します。

2-1. Python SDKのインストール

Typesafe AIは、Python向けの公式SDKとして typesafe-sdk というパッケージを提供しています。このSDKを使うことで、HTTPリクエストの詳細を意識することなく、Pythonの関数呼び出しとしてJevを利用できます。

まず、前提条件として Python 3.10以上が必要です。ご自身の環境のPythonバージョンは、ターミナル(コマンドプロンプトやターミナルアプリ)で次のコマンドを実行すると確認できます。

python --version

実行結果(例):

Python 3.11.6

3.10以上であることを確認できたら、SDKをインストールしましょう。Pythonのパッケージ管理でよく使われる pip を利用する場合は、次のコマンドを実行します。

pip install typesafe-sdk

もし、近年人気が高まっている高速なパッケージマネージャー uv を使っている場合は、次のように書きます。

uv add typesafe-sdk

どちらの方法でも構いませんが、本ガイドでは以降、標準的な pip を前提にコードを進めます。インストールが正しく完了したかどうかは、Pythonの対話環境(インタラクティブシェル)で次のように確認できます。

python -c "import typesafe_sdk; print(typesafe_sdk.__name__)"

実行結果(例):

typesafe_sdk

エラーが出ずにパッケージ名が表示されれば、インストールは成功です。

2-2. APIキーを取得しよう

Jevを呼び出すには、Typesafe AIが発行する「APIキー」という認証情報が必要です。これは、みなさんが誰であるかを識別し、利用状況を管理するための鍵のようなものです。

APIキーは、Typesafe AIのコンソール画面(管理用のWebサイト)から取得します。公式ドキュメントでは、APIキーの発行ページとして console.typesafe.ai のドメイン配下にある設定画面が案内されています。アカウントを作成し、その画面でキーを新規発行してください。

APIキーを取得したら、それをコードの中に直接書き込むのではなく、環境変数として設定することが強く推奨されています。環境変数として設定しておけば、うっかりコードをGitHubなどに公開してしまったときにキーが漏洩してしまう事故を防ぎやすくなりますし、SDKも自動的にその環境変数を読み取ってくれます。

macOSやLinuxのターミナルであれば、次のように設定します。

export TYPESAFE_API_KEY="ここに取得したAPIキーを貼り付けます"

Windowsのコマンドプロンプトであれば、次のようになります。

set TYPESAFE_API_KEY=ここに取得したAPIキーを貼り付けます

PowerShellを使っている場合は、次のように設定します。

$env:TYPESAFE_API_KEY="ここに取得したAPIキーを貼り付けます"

この環境変数 TYPESAFE_API_KEY こそが、Python SDKがAPIキーを自動で読み込む際に参照する変数名です。この名前を変更してしまうとSDKが認証情報を見つけられなくなるため、正確に入力するようにしてください。

豆知識:もし、まずはコードを書く前に手っ取り早く動作を試してみたいという方は、Typesafe AIが用意している「Playground(プレイグラウンド)」というブラウザ上の試用画面を使う方法もあります。テキストを貼り付けて質問を追加するだけで、コードを1行も書かずにJevの挙動を体験できます。本ガイドではコードでの実装を中心に解説していきますが、雰囲気をつかみたい場合はこうした画面から触ってみるのもよい方法です。


第3章:はじめてのJev呼び出し

準備が整ったところで、いよいよ実際にJevを呼び出してみましょう。

3-1. もっとも小さいコード例

まずは、ごく短いコードで、Jevがどんな働きをするのか体感してみます。次のコードは、あるカスタマーサポートのメッセージを渡して、「このメッセージは緊急性を伝えているか?」という一つの質問を投げかけるものです。

from typesafe_sdk import Noul, TypeSafeClient

# クライアントを作成する
# (環境変数 TYPESAFE_API_KEY からAPIキーが自動的に読み込まれます)
client = TypeSafeClient()

# 評価したい文章(state)
message = "さっき頼んだお弁当がまだ届きません。配達員さんにも連絡が取れなくて、お昼休みが終わってしまいそうです。"

# Jevに問いかける
response = client.system_one(
    state=message,
    questions={
        "is_urgent": Noul(
            instructions="このメッセージは緊急性を伝えていますか?",
        ),
    },
)

# 結果を表示する
print(response.answers["is_urgent"].noul)

実行結果(例):

0.97

たったこれだけのコードで、「このメッセージが緊急かどうか」という判断が、0.97(=かなり高い確率で緊急である)という一つの数値として返ってきました。この数値をどう解釈すればよいかは、後ほど詳しく説明しますが、直感的には「1に近いほど『はい』に近く、0に近いほど『いいえ』に近い」と考えて差し支えありません。

このコードの中で登場した要素を、一つずつ分解して見ていきましょう。

  • TypeSafeClient():Jevと通信するための「クライアント」というオブジェクトを作成しています。これは以降、何度もAPIを呼び出す際の窓口になります。
  • client.system_one(...):これが実際にJevへリクエストを送る中心的なメソッドです。「system_one」という名前は、第1章で説明した「System One モデル」に由来しています。
  • state=message:評価してほしい対象(今回はサポートメッセージの文章)を渡しています。
  • questions={...}:どんな質問をしたいかを、辞書(Pythonのdict型)の形で指定しています。キーである "is_urgent" は、後で答えを受け取るときに使う名前で、Jev自身には送られない、あくまでプログラム側の識別子です。
  • Noul(instructions="..."):Yes/No形式の質問を表す Noul というクラスを使い、instructions に具体的な質問文を書いています。
  • response.answers["is_urgent"].noul:返ってきたレスポンスから、"is_urgent" という名前で登録した質問の答えを取り出し、その .noul 属性(0から1の確率値)を参照しています。

この構造——「state(評価対象)」と「questions(質問)」を渡して、「answers(答え)」を受け取る——は、Jevを使うすべての場面で共通する、基本の「型」です。この型さえ理解してしまえば、あとは応用の連続になります。

3-2. APIを直接呼び出す方法(HTTPリクエスト)

先ほどはPython SDKを使いましたが、Jevは標準的なWeb APIとしても提供されているため、SDKを使わずに直接HTTPリクエストを送ることもできます。これは、Python以外の言語からJevを使いたい場合や、SDKの内部で何が起きているのかを理解したい場合に役立ちます。

エンドポイント(APIの宛先URL)は次のとおりです。

POST https://api.typesafe.ai/v1/systemone

リクエストを送るときは、Authorization ヘッダーに Bearer <APIキー> という形式で認証情報を含め、Content-Type ヘッダーには application/json を指定します。

コマンドラインからAPIを試すときによく使われる curl コマンドを使うと、次のようになります。

curl -X POST https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "さっき頼んだお弁当がまだ届きません。配達員さんにも連絡が取れなくて、お昼休みが終わってしまいそうです。",
    "model": "jev-latest",
    "questions": {
      "is_urgent": {
        "type": "noul",
        "instructions": "このメッセージは緊急性を伝えていますか?"
      }
    }
  }'

実行結果(例、JSON形式のレスポンス):

{
  "model": "jev-1.13.0",
  "answers": {
    "is_urgent": {
      "type": "noul",
      "noul": 0.97
    }
  },
  "usage": {
    "input_tokens": 84,
    "output_tokens": 9
  }
}

このJSONの中身を見ると、先ほどPython SDKで確認した 0.97 という値が、answers.is_urgent.noul という場所にそのまま格納されていることがわかります。実はPython SDKは、この生のHTTP通信を裏側で行い、その結果をPythonのオブジェクトとして扱いやすい形に変換してくれているだけなのです。仕組みを理解しておくと、思わぬエラーに遭遇したときにも落ち着いて対処できるようになります。

なお、model フィールドに指定している "jev-latest" という文字列は、「そのときどきの最新版のJevモデル」を指すエイリアス(別名)です。レスポンスの model フィールドには、実際に応答した具体的なバージョン(このガイド執筆時点では jev-1.13.0)が記載されます。本番システムで挙動を安定させたい場合は、あえてこの具体的なバージョン名を指定する、という選択肢もあります。

3-3. 複数の種類の質問を同時に投げる

Jevの大きな特徴の一つに、「1回のリクエストで、複数の質問を同時に、しかも異なる種類(型)で問いかけられる」という点があります。先ほどの例では Noul(Yes/No判定)を1つだけ使いましたが、これから紹介する Choice(選択肢から1つ選ぶ)や Score(レベルで採点する)といった別の種類の質問も、同じリクエストの中に混在させることができます。

次のコードでは、先ほどと同じ配達に関する問い合わせメッセージに対して、「担当部署の判定(Choice)」「顧客の苛立ち度合い(Score)」「緊急性の有無(Noul)」という3種類の質問を、まとめて1回のリクエストで問いかけています。

from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

client = TypeSafeClient()

message = "さっき頼んだお弁当がまだ届きません。配達員さんにも連絡が取れなくて、お昼休みが終わってしまいそうです。"

response = client.system_one(
    state=message,
    questions={
        "department": Choice(
            instructions="このメッセージはどの部署が対応すべきですか",
            criteria={
                "delivery": "配達状況や配達員に関する問題",
                "order": "注文内容や店舗側の準備に関する問題",
                "payment": "支払いや請求に関する問い合わせ",
            },
        ),
        "frustration": Score(
            instructions="この顧客はどの程度苛立っていますか",
            criteria=[
                "冷静で、事実を淡々と述べているだけ",
                "苛立ってはいるが、まだ礼儀正しい",
                "非常に怒っており、強い言葉を使っている",
            ],
        ),
        "is_urgent": Noul(
            instructions="このメッセージは緊急性を伝えていますか",
        ),
    },
)

print("担当部署:", response.answers["department"].choice)
print("苛立ち度:", response.answers["frustration"].score)
print("緊急性  :", response.answers["is_urgent"].noul)

実行結果(例):

担当部署: delivery
苛立ち度: 0.72
緊急性  : 0.97

たった1回のAPI呼び出しで、3つの異なる観点からの判断が、すべて型のついたデータとして返ってきました。それぞれの答えの意味は次章以降で詳しく解説しますが、ここで押さえておいてほしいのは、質問をいくつ増やしても、応答にかかる時間はほとんど変わらないという性質です。

これは、それぞれの質問が互いに独立して、並列に評価されているためです。ある質問の答えが、別の質問の答えに影響を与えることはありません。この「独立性」は、あとで説明する「文脈汚染(コンテキストロット)を防ぐ」という観点からも、非常に重要な特徴です。


第4章:State(状態)を正しく理解する

ここからは、Jevを使いこなすための土台となる2つの概念——「State」と「Primitives」——について、じっくりと掘り下げていきます。まずは「State」からです。

4-1. Stateとは「判断材料」のこと

Jevに送るリクエストには、必ず state というフィールドが登場します。これは、Jevに評価してもらいたい「対象」そのものを表す情報です。

公式ドキュメントでは、Stateのことを「専門家パネルに判断を仰ぐ前に、その専門家たちに提示する資料」にたとえています。これはとてもわかりやすい比喩だと筆者は思います。裁判の陪審員に証拠資料を提示するように、Jevというモデルに「この情報をもとに判断してください」と手渡す材料が、Stateなのです。

Stateには、大きく分けて3つの形式が使えます。

形式 どんなときに便利か 具体例
文字列(string) 単純な1つのメッセージや文章 "カードが二重に請求されました。"
オブジェクト(JSON) 名前つきの複数のフィールドや、関連するレコード、アプリの状態などをまとめて渡したいとき {"message": "...", "order_id": "A-104"}
配列(array) メッセージの列や、複数のレコードを順序つきで渡したいとき ["こんにちは", "私の顧客番号はTS1337です。", "カードが二重に請求されました。"]

多くの実践的な場面では、オブジェクト形式を使うことになります。オブジェクトを使うメリットは、各情報に意味のわかりやすい名前(フィールド名)をつけられる点です。単に文章を並べるよりも、情報同士の関係が明確になります。

4-2. オブジェクト形式のStateを使ってみる

実際に、少し込み入った例で見てみましょう。あるカスタマーサポートの会話と、それに関連する注文情報、返金ポリシーをまとめてStateとして渡すケースです。

from typesafe_sdk import Noul, TypeSafeClient

client = TypeSafeClient()

# 複数の情報をオブジェクト(辞書)としてまとめる
state = {
    "ticket": {
        "subject": "二重請求について",
        "messages": [
            {"from": "customer", "text": "注文A-104について二重に請求されています。重複分を返金してください。"},
            {"from": "support", "text": "現在、請求内容を確認しております。"},
        ],
    },
    "order": {
        "id": "A-104",
        "charges": [
            {"amount_usd": 49, "status": "captured"},
            {"amount_usd": 49, "status": "captured"},
        ],
    },
    "refund_policy": "二重請求が確認された場合、返金の対象となります。",
}

response = client.system_one(
    state=state,
    questions={
        "refund_requested": Noul(
            instructions="`ticket.messages` の中で、顧客は返金を求めていますか?",
        ),
        "policy_supports_refund": Noul(
            instructions=(
                "`refund_policy` は、`order.charges` の内容を踏まえたとき、"
                "顧客が求めている返金を支持する内容になっていますか?"
            ),
        ),
    },
)

print("返金要求の有無      :", response.answers["refund_requested"].noul)
print("ポリシー上の返金可否 :", response.answers["policy_supports_refund"].noul)

実行結果(例):

返金要求の有無      : 0.98
ポリシー上の返金可否 : 0.95

ここで注目してほしいのは、instructions(質問文)の中に、バッククォート(`)で囲んだ `ticket.messages` や `order.charges` のような表記が登場している点です。これは、Stateの中の特定の部分(フィールド)を指し示すための書き方で、ドット区切りやインデックス(配列の何番目か)を使って、深くネストした構造の中の情報でもピンポイントで指定できます。

このように、質問の中でStateの特定部分を明示的に参照することで、「Stateのどの情報にもとづいて判断してほしいのか」がJevにとって明確になり、より精度の高い答えが期待できるようになります。

4-3. 内容と判断を分離するという設計思想

State(内容)とQuestions(判断基準)は、明確に役割が分かれています。Stateは「事実」や「裏付けとなる情報」を持ち、Questionsは「その事実にもとづいて、何を判断すべきか」を定義します。

この分離には大きな利点があります。同じStateに対して、目的の異なる複数の質問を自由に組み合わせられるのです。先ほどの返金リクエストの例で言えば、同じState(チケット・注文・ポリシー)に対して、「返金要求があるか」「ポリシー上支持されるか」に加えて、「顧客の苛立ち度はどれくらいか」「二重請求の証拠は明確か」といった質問を、いくらでも追加で問いかけることができます。

この「内容と判断の分離」という考え方は、ソフトウェア設計における「関心の分離(Separation of Concerns)」という古典的な原則にも通じるものがあります。Stateの組み立て方と、Questionsの設計方法を、それぞれ独立して洗練させていくことができるのです。


第5章:Primitives概論——3つの質問の型を使い分ける

いよいよ、Jevの中核をなす「Primitives(プリミティブ)」について学んでいきます。ここまでのコード例でも既に Choice、Score、Noul という3つの言葉が登場してきましたが、この章ではそれぞれの位置づけと使い分けの基準を、体系的に整理します。

5-1. プリミティブとは何か

「プリミティブ」という言葉は、プログラミングの世界では「それ以上分解できない、基本的な構成要素」という意味で使われます。整数や文字列、真偽値(boolean)といった基本的なデータ型を「プリミティブ型」と呼ぶのを、聞いたことがある方もいるかもしれません。

Jevにおけるプリミティブも、これと似た発想です。Jevが扱える「質問の種類」は、次の3つの基本形に集約されています。

種類 何を問うものか 返ってくるもの
Choice(チョイス) 決められた選択肢の中から、どれか一つを選ぶ choice(選ばれた選択肢)、probabilities(各選択肢の確率)、confidence(確信度)
Score(スコア) 順序のあるレベル(段階)のうち、どこに位置するかを採点する score(採点結果)、legend(レベルの説明)、probabilities(各レベルの確率)、confidence(確信度)
Noul(ヌール) ある文がYesかNoか、その確率を問う noul(Yesである確率、0〜1)

この3つのプリミティブは、ソフトウェアにおけるプリミティブと同じように、モジュール的で、組み合わせ可能で、構造化されていて、信頼性が高く、高速という性質を持っています。それぞれが異なる型の答えを返すため、プログラムのロジックの中で、その型に応じた適切な扱い方(比較、分岐、ソートなど)ができるのです。

5-2. どのプリミティブを選べばよいか——判断のフローチャート的な考え方

3つのプリミティブのうち、どれを使うべきかを考えるときは、次のような視点で整理すると迷いにくくなります。

Choiceを選ぶべきとき

答えが、あらかじめ決まった、順序のない選択肢の集合の中から一つに定まる場合です。「どのチームが対応すべきか」「どのプログラミング言語で書かれたコードか」「どの商品カテゴリーに属するか」といった質問がこれにあたります。

ここで注意したいのは、選択肢のリストがすべての可能性を網羅していないおそれがある場合には、"other"(その他)や "none of the above"(当てはまるものなし)といった選択肢をあらかじめ追加しておくことです。そうしないと、本来は当てはまらないはずの入力に対しても、リストの中から無理に一つを選ばせてしまうことになりかねません。

Scoreを選ぶべきとき

答えが、説明可能な「スペクトル(連続的な尺度)」上のどこかに位置づけられる場合です。「バグの深刻度はどれくらいか」「顧客満足度はどの段階か」「Pythonの経験レベルはどの程度か」といった、程度や強さを測りたい質問に向いています。

Noulを選ぶべきとき

答えが明確なYes/Noであり、しかもその「確率」そのものが有用な情報になる場合です。「返金を要求しているか」「メッセージに個人情報が含まれているか」といった、二択の判定に向いています。

ここで一つ、公式ドキュメントが強調している注意点があります。それは、「Yes/Noの二択に見えても、実際には程度を測りたい場合はScoreを使うべきだ」ということです。

たとえば、「この候補者はPythonが得意か?」という質問を考えてみましょう。これをNoulで問うと、0.5 という答えが返ってきたとき、それは「得意か苦手かが五分五分である」という意味にしかなりません。「そこそこ得意」という中間的なスキルレベルを表しているわけではないのです。もし「未経験」「多少触ったことがある」「日常的に使っている」「深い専門知識を持つ」といった段階的なスキルレベルを知りたいのであれば、それはNoulではなくScoreの出番です。

この使い分けの感覚は、実際に手を動かしながら身につけていくのが一番の近道です。次章以降、それぞれのプリミティブについて、さらに詳しいコード例とともに解説していきます。

5-3. 質問IDは、あくまでプログラムのための名前

コード例の中で、questions={"department": Choice(...), ...} のように、質問に "department" のような名前(キー)をつけていることに気づいたでしょうか。この名前のことを、本ガイドでは「質問ID」と呼びます。

ここで大切な注意点があります。この質問IDは、Jevというモデル自体には送信されません。 あくまでプログラム側が、あとで response.answers["department"] のようにして答えを取り出すために使う、目印のようなものです。

したがって、たとえ質問IDが "department" のようにわかりやすい名前であったとしても、それだけでJevが「部署を判定すればよいのだな」と理解してくれるわけではありません。実際に何を判断してほしいのかは、必ず instructions フィールドに、完全な文章として明示的に書く必要があります。この点は、初めて使う方がつまずきやすいポイントなので、ぜひ覚えておいてください。

5-4. 質問はまとめて投げるのが基本

第3章のコード例でも見たとおり、Jevでは同じStateに対する複数の質問を、1回のリクエストにまとめて送ることができます。これは単なる利便性の話ではなく、Jevを効果的に使ううえでの基本戦略です。

公式ドキュメントでは、「使う可能性のある質問は、まとめて全部投げてしまってよい」という考え方(投機的な質問、Speculative fan-outと呼ばれる手法)も紹介されています。たとえば、入力の種類によっては意味を持たない質問が混ざっていたとしても、まとめて問いかけておき、実際に使う答えだけをあとからコードで選び取る、というやり方です。

ただし、1回のリクエストに含められる情報量には上限があります。目安として、Stateと質問をあわせたトークン数(AIがテキストを処理する際の単位)が、おおよそ32,000トークン程度(英語のテキストで換算するとおよそ15万文字相当)までとされています。日本語の場合はこの目安が多少変わってくる可能性がありますが、「無制限に詰め込めるわけではない」という点は意識しておくとよいでしょう。

注意点:コーディングを自動化するAIエージェントにJevを組み込ませようとすると、「1回のAPI呼び出しにつき1つの質問だけを送る」という、やや非効率なコードを書いてしまいがちだと公式ドキュメントは指摘しています。このガイドを読んでいるみなさんは、ぜひ「関連する質問はまとめて1回のリクエストに詰め込む」という発想を、意識的に持つようにしてください。


第6章:Choiceプリミティブを使いこなす

この章では、3つのプリミティブの中でも特に頻繁に使うことになる「Choice」について、より深く掘り下げます。

6-1. Choiceの基本構造

Choiceの質問は、次の3つの要素から構成されます。

  • type:常に文字列 "choice"(Python SDKでは Choice クラスを使うため、この指定は自動的に行われます)
  • instructions:何を選んでほしいのかを説明する質問文
  • criteria:選択肢の一覧。キーが選択肢の名前、値がその選択肢の説明という、辞書(マップ)の形式で指定します

返ってくる答えには、次の3つの情報が含まれます。

  • choice:最も確率の高かった選択肢の名前
  • probabilities:すべての選択肢それぞれについての確率(合計するとちょうど1になります)
  • confidence:確率分布の「偏り具合」から算出される、0から1の確信度

6-2. Choiceの実践コード

商品の返品リクエストを、担当部署に振り分ける例で見てみましょう。

from typesafe_sdk import Choice, TypeSafeClient

client = TypeSafeClient()

response = client.system_one(
    state="注文したワイヤレスイヤホンが、注文と違う色(黒ではなく白)で届きました。黒に交換してもらえますか?",
    questions={
        "department": Choice(
            instructions="このメッセージはどの部署が対応すべきですか?",
            criteria={
                "returns": "交換・返金、誤った商品や破損した商品に関する問題",
                "shipping": "配送状況、遅延、荷物の紛失に関する問題",
                "billing": "請求、請求書、支払いに関する問題",
            },
        ),
    },
)

answer = response.answers["department"]
print("選ばれた選択肢:", answer.choice)
print("確信度        :", answer.confidence)
print("各選択肢の確率:", answer.probabilities)

実行結果(例):

選ばれた選択肢: returns
確信度        : 0.98
各選択肢の確率: {'shipping': 0.01, 'returns': 0.98, 'billing': 0.01}

今回のメッセージは「色違いによる交換」という要件が非常に明確だったため、returns という選択肢に確率が大きく集中し、確信度も高い値になりました。

6-3. 選択肢の上限と「その他」の重要性

Choiceで指定できる選択肢の数には上限があり、1つの質問につき最大255個までという仕様になっています。実務上、これだけの数の選択肢を使う場面はそれほど多くないと思いますが、大規模な分類タスクを設計する際には頭の片隅に入れておくとよいでしょう。

また先ほども触れましたが、選択肢のリストが入力のすべてのパターンを網羅できるとは限らない場合、必ず "other" や "none_of_the_above" のような「逃げ道」の選択肢を用意しておくことが推奨されています。これを怠ると、本来はどの選択肢にも当てはまらないはずの入力が、無理やりどれかの選択肢に振り分けられてしまい、誤判定の原因になります。

6-4. 複数のChoice質問をまとめて使う実践的な例

ここで、実際のカスタマーサポート業務を想定した、より本格的な例を見てみましょう。1件のチケットに対して「担当部署」「返品理由」「配送上の問題があるか」「希望する解決方法」「文面のトーン」という、5つの質問をまとめて投げかけます。

from typesafe_sdk import Choice, TypeSafeClient

client = TypeSafeClient()

TRIAGE_QUESTIONS = {
    "department": Choice(
        instructions="このチケットはどの部署が対応すべきですか?",
        criteria={
            "returns": "交換・返金に関する問題",
            "shipping": "配送状況や遅延に関する問題",
            "billing": "請求や支払いに関する問題",
        },
    ),
    "return_reason": Choice(
        instructions="返品・交換が求められている場合、その理由は何ですか?",
        criteria={
            "wrong_size": "サイズや型番が注文と異なっていた",
            "defective": "商品に不具合や破損があった",
            "changed_mind": "単に気が変わった、不要になった",
            "not_applicable": "返品・交換の要求ではない",
        },
    ),
    "shipping_issue": Choice(
        instructions="配送に関する問題が発生している場合、その種類は何ですか?",
        criteria={
            "delayed": "予定より配送が遅れている",
            "lost": "荷物が行方不明になっている",
            "damaged_in_transit": "配送中に商品が破損した",
            "not_applicable": "配送に関する問題ではない",
        },
    ),
    "requested_resolution": Choice(
        instructions="顧客が最終的に望んでいる対応は何ですか?",
        criteria={
            "refund": "返金してほしい",
            "replacement": "同じ商品または代替品との交換を希望",
            "information": "状況の説明や情報だけがほしい",
        },
    ),
    "tone": Choice(
        instructions="このメッセージの文面のトーンはどれに近いですか?",
        criteria={
            "calm": None,
            "frustrated": None,
            "angry": None,
        },
    ),
}


def triage(ticket_text: str) -> dict:
    """チケットを分析し、各質問への答えをまとめた辞書を返す。"""
    response = client.system_one(state=ticket_text, questions=TRIAGE_QUESTIONS)
    answers = response.answers

    result = {
        "department": answers["department"].choice,
        "department_confidence": answers["department"].confidence,
        "return_reason": answers["return_reason"].choice,
        "requested_resolution": answers["requested_resolution"].choice,
        "tone": answers["tone"].choice,
    }

    # 配送問題が「該当あり」で、かつある程度の確率がある場合は、
    # 投機的に問いかけていた shipping_issue の答えも採用する
    shipping_probs = answers["shipping_issue"].probabilities
    not_applicable_prob = shipping_probs.get("not_applicable", 1.0)
    if not_applicable_prob < 0.5:
        result["shipping_issue"] = answers["shipping_issue"].choice

    return result


ticket = "注文したランニングシューズが違うサイズで届きました。サイズ10に交換できますか?配送自体は予定通りでした。"
result = triage(ticket)

for key, value in result.items():
    print(f"{key}: {value}")

実行結果(例):

department: returns
department_confidence: 0.97
return_reason: wrong_size
requested_resolution: replacement
tone: calm

この例で注目してほしいのは、tone という質問の criteria において、各選択肢の説明を None(説明なし)にしている点です。「calm(冷静)」「frustrated(苛立っている)」「angry(怒っている)」といった選択肢は、名前そのものが意味を十分に伝えているため、追加の説明を省略しても問題ないケースがあることを示しています。

また、shipping_issue という質問については、今回のメッセージには配送上の問題が含まれていなかったため、shipping_issue.probabilities の中の "not_applicable"(該当なし)の確率が高くなり、コード側の条件分岐によって結果からは除外されています。これはまさに、「投機的にいくつかの質問を混ぜて投げておき、実際に必要な答えだけをあとからコードで選び取る」という、先ほど紹介した設計パターンの実例です。

6-5. 似た選択肢を混同してしまうときの対処法——構造化されたinstructionsとcriteria

選択肢の意味が似通っていて、Jevが混同しやすい場合には、instructions や criteria の値を、単なる文字列ではなく**JSONオブジェクト(構造化されたデータ)**として渡すという高度なテクニックがあります。

たとえば、「返金ポリシーに関する質問(return_policy)」と「実際の返金処理の状況に関する質問(return_status)」という、名前も内容も紛らわしい2つの選択肢を区別したい場合を考えてみます。

from typesafe_sdk import Choice, TypeSafeClient

client = TypeSafeClient()

response = client.system_one(
    state="返品ポリシーについて教えてください。まだ返品の申請はしていません。",
    questions={
        "topic": Choice(
            instructions="このメッセージが主に尋ねているトピックは何ですか?",
            criteria={
                "return_policy": {
                    "what": "返品や交換に関する一般的なルールや条件についての質問",
                    "not_for": "すでに申請済みの返品の進捗状況についての質問は含まない",
                    "examples": [
                        "返品できる期間はどれくらいですか?",
                        "未開封でなくても返品できますか?",
                    ],
                },
                "return_status": {
                    "what": "既に申請した返品や交換の、現在の処理状況についての質問",
                    "not_for": "まだ申請していない返品についての一般的なルールの質問は含まない",
                    "examples": [
                        "先週申請した返品はいつ処理されますか?",
                        "返金はまだ届いていません。進捗を教えてください。",
                    ],
                },
            },
        ),
    },
)

answer = response.answers["topic"]
print("トピック:", answer.choice)
print("確信度  :", answer.confidence)

実行結果(例):

トピック: return_policy
確信度  : 1.0

このように、選択肢ごとに「何についての選択肢か(what)」「何については当てはまらないか(not_for)」「具体例(examples)」といった複数の観点を構造化して与えることで、意味が近い選択肢同士でも高い精度で区別できるようになります。what や not_for といったキー名自体はAPI側で決められた予約語ではなく、みなさんが自由に名づけてよいものです。わかりやすい名前を選んで、判断材料を整理してあげることが大切です。


第7章:Scoreプリミティブを使いこなす

続いて、「程度」や「レベル」を測定するためのプリミティブ、Scoreについて学びます。

7-1. Scoreの基本構造

Scoreの質問も、Choiceと似た構造を持っていますが、criteria の与え方が異なります。

  • type:常に文字列 "score"
  • instructions:何を採点してほしいのかを説明する質問文
  • criteria:低いレベルから高いレベルへ、**順序つきの配列(リスト)**として与えるレベルの説明。最低2つ、最大10個のレベルを指定できます

それぞれのレベルには、配列の中での位置(0から始まる番号)が自動的に割り振られます。たとえば3つのレベルを渡した場合、それぞれ0、1、2という番号が対応します。

返ってくる答えには、次の情報が含まれます。

  • score:採点結果。これは必ずしも整数のレベル番号ぴったりになるとは限らず、レベルとレベルの「間」に位置するような小数値になることもあります
  • legend:レベル番号と、そのレベルの説明文の対応表
  • probabilities:各レベルに対する確率
  • confidence:確信度

7-2. Scoreの実践コード

バグ報告の深刻度を、3段階のレベルで採点する例を見てみましょう。

from typesafe_sdk import Score, TypeSafeClient

client = TypeSafeClient()

bug_report = (
    "検索結果を並び替えると、一覧が真っ白になってしまいます。"
    "ページを再読み込みすれば元に戻りますが、毎回この手順を踏まないといけません。"
)

response = client.system_one(
    state=bug_report,
    questions={
        "bug_severity": Score(
            instructions="報告されている問題の深刻度はどの程度ですか?",
            criteria=[
                "見た目上の問題に過ぎず、機能面への影響はない",
                "機能が壊れている、または劣化しているが、回避策が存在する",
                "処理をブロックする問題で、回避策が存在しない",
            ],
        ),
    },
)

answer = response.answers["bug_severity"]
print("採点結果  :", answer.score)
print("確信度    :", answer.confidence)
print("凡例      :", answer.legend)
print("各レベルの確率:", answer.probabilities)

実行結果(例):

採点結果  : 1.2
確信度    : 0.61
凡例      : {0: '見た目上の問題に過ぎず、機能面への影響はない', 1: '機能が壊れている、または劣化しているが、回避策が存在する', 2: '処理をブロックする問題で、回避策が存在しない'}
各レベルの確率: {0: 0.03, 1: 0.74, 2: 0.23}

ここで面白いのは、採点結果が 1.2 という、整数ではない中間的な値になっている点です。これは、probabilities に示されている各レベルの確率分布から、確率で重みづけした平均値として計算されています(0×0.03 + 1×0.74 + 2×0.23 = 1.2)。つまり score は、単に「一番確率が高いレベル」を答えているのではなく、確率分布全体を反映した連続的な数値になっているのです。この性質のおかげで、「レベル1とレベル2の中間くらいの深刻さだ」という、より繊細なニュアンスをプログラムで扱うことができます。

7-3. レベルの説明文の書き方——良い例と悪い例

Scoreプリミティブの精度を左右する最大のポイントは、criteria に渡すレベルの説明文をどれだけ具体的に書けるかにかかっています。

良くない例として、単に度合いを表す言葉だけを並べたケースを考えてみましょう。

# あまり良くない例:抽象的な度合いの表現だけを使っている
bad_criteria = [
    "軽度な問題",
    "中程度の問題",
    "深刻な問題",
]

このような表現は、「軽度」「中程度」「深刻」の境界がどこにあるのか曖昧で、判断のブレを招きやすくなります。それよりも、先ほどの例のように「機能が壊れているが回避策があるかどうか」「処理をブロックするかどうか」といった、具体的な状況で表現する方が、はるかに安定した結果が得られます。

さらに悪い例として、レベルの説明を単なる番号のラベルにしてしまうケースもあります。

# 非常に良くない例:番号をそのままラベルにしている
very_bad_criteria = ["0", "1", "2"]

このような書き方をすると、Jevはそれぞれのレベルが実際に何を意味しているのかをまったく判断できなくなり、精度が大きく低下してしまいます(公式ドキュメントの実例でも、このような書き方にすると採点結果の確信度が大幅に下がることが示されています)。レベルの説明文は、必ず「そのレベルに該当する状況」を、具体的な言葉で記述するようにしてください。

7-4. Scoreは1つの軸だけを測る

もう一つ大切な注意点として、1つのScore質問では、1つの軸(次元)だけを測定するようにしてください。

たとえば「この候補者は、時間に正確で、賢くて、経験豊富か?」というような複数の性質をまとめて1つのScoreで測ろうとするのは、良い設計ではありません。これは「時間厳守」「知性」「経験」という3つの異なる軸を、無理やり1つの尺度に押し込めようとしているためです。

このような場合は、Choiceのときと同様に、それぞれの軸を独立したScore質問に分解し、あとでコード側で組み合わせるようにしましょう。

7-5. 複数のScoreを合成して優先度を算出する実践例

最後に、公式ドキュメントで紹介されている、複数のScoreの答えを正規化してから重み付き合成する、実践的なパターンを紹介します。バグ報告の「深刻度」「顧客の苛立ち度」「報告内容の質」という3つの観点をそれぞれScoreで採点し、それらを合成して「対応優先度」という単一の指標を算出します。

from typesafe_sdk import Score, TypeSafeClient

client = TypeSafeClient()

TRIAGE_QUESTIONS = {
    "severity": Score(
        instructions="報告された問題の深刻度はどの程度ですか?",
        criteria=[
            "見た目上の問題のみ",
            "機能が壊れているが回避策がある",
            "処理をブロックし、回避策がない",
        ],
    ),
    "frustration": Score(
        instructions="この報告者はどの程度苛立っていますか?",
        criteria=[
            "冷静で中立的",
            "懸念はあるが礼儀正しい",
            "非常に怒っている、または強い言葉を使っている",
        ],
    ),
    "report_quality": Score(
        instructions="この報告は、再現手順などの情報がどの程度明確ですか?",
        criteria=[
            "情報が乏しく、再現方法がわからない",
            "ある程度の情報はあるが不十分",
            "再現手順や環境情報が明確に記載されている",
        ],
    ),
}


def normalized(answers: dict, question_id: str) -> float:
    """スコアを、そのレベル数に応じて0〜1の範囲に正規化する。"""
    top_level = len(TRIAGE_QUESTIONS[question_id].criteria) - 1
    return answers[question_id].score / top_level


def priority(bug_report: str) -> float:
    """複数のScoreの答えを重み付き合成して、優先度を算出する。"""
    response = client.system_one(state=bug_report, questions=TRIAGE_QUESTIONS)
    answers = response.answers

    severity = normalized(answers, "severity")
    frustration = normalized(answers, "frustration")
    report_quality = normalized(answers, "report_quality")

    # 深刻度を最も重視し、苛立ち度、報告の質の順に重みを下げる
    return 0.6 * severity + 0.3 * frustration + 0.1 * report_quality


report = (
    "エクスポートボタンを押すと設定画面がクラッシュします。"
    "何度も繰り返し発生しており、非常に困っています。手順はメモしてあります。"
)

score = priority(report)
print(f"対応優先度: {score:.2f}")

実行結果(例):

対応優先度: 0.69

このコードのポイントは、normalized という関数の中で、各Scoreの結果(score)を、そのレベル数の最大値(len(criteria) - 1)で割ることによって、「0から1の範囲」に統一している点です。深刻度のレベルが3段階(0〜2)であっても、苛立ち度が5段階(0〜4)であっても、この正規化を行うことで、異なるスケールのScoreを公平に比較・合成できるようになります。

そのうえで、priority 関数では、深刻度に60%、苛立ち度に30%、報告の質に10%という重みをつけて合成しています。この重みの割合こそが、まさに第1章で紹介した「プロンプトを書き換えるのではなく、コード側の係数を変えることで優先順位を調整する」という考え方を、そのまま体現したものです。「もっと苛立ち度を重視したい」と思ったら、0.3 の数字を大きくするだけで、Jevへの質問そのものは一切変更せずに、システムの振る舞いを調整できます。


第8章:Noulプリミティブを使いこなす

最後のプリミティブ、Noulについて解説します。

8-1. Noulの基本構造

Noulは、3つのプリミティブの中で最もシンプルな構造を持っています。

  • type:常に文字列 "noul"
  • instructions:Yes/Noで答えられる質問、または文(statement)
  • criteria(任意):true(Yesの場合の説明)と false(Noの場合の説明)を書くことで、境界があいまいなケースでも意味を固定できる、省略可能な項目

返ってくる答えは、noul というただ一つの数値(0〜1)だけです。ChoiceやScoreと違って、Noulには confidence(確信度)というフィールドがありません。 これは、Noulの場合、noul の値そのものがすでに「Yesである確率」を表しており、その値自体が不確実性の情報を内包しているためです。

8-2. instructionsは「質問文」ではなく「文(statement)」として書くこともできる

ここまでのコード例では、instructions を疑問文(「〜ですか?」)の形で書いてきましたが、Noulの場合は、疑問文ではなく**平叙文(言い切りの文)**として書くスタイルもよく使われます。

from typesafe_sdk import Noul, TypeSafeClient

client = TypeSafeClient()

response = client.system_one(
    state="至急ご対応ください。売上に影響が出ています。",
    questions={
        # 疑問形のスタイル
        "is_urgent_question_style": Noul(
            instructions="このメッセージは緊急性を伝えていますか?",
        ),
        # 言い切り(statement)形式のスタイル
        "is_urgent_statement_style": Noul(
            instructions="このメッセージは緊急性や時間的な切迫感を伝えている。",
        ),
    },
)

print(response.answers["is_urgent_question_style"].noul)
print(response.answers["is_urgent_statement_style"].noul)

実行結果(例):

0.998
0.997

どちらのスタイルで書いても、意味さえ明確であれば結果に大きな違いは生まれません。読みやすさや、みなさんのコードのスタイルに合わせて、好きな方を選んでいただいて構いません。

8-3. criteriaで境界を明確にする

曖昧になりがちなYes/No判定には、criteria を使って true と false それぞれの意味を明示的に定義しておくと、精度が安定します。

次の例では、「メッセージが認証情報(パスワードやアカウント情報など)を要求しているかどうか」という、フィッシング詐欺の検知にも関わるような、境界がデリケートな判定を扱っています。

from typesafe_sdk import Noul, TypeSafeClient

client = TypeSafeClient()

message = (
    "セキュリティ強化のため、アカウントの確認が必要です。"
    "下記のリンクからログインし、パスワードを再設定してください。"
)

response = client.system_one(
    state=message,
    questions={
        "requests_credentials": Noul(
            instructions="このメッセージは、パスワードやログイン情報など、認証情報の入力や提供を求めていますか?",
            criteria={
                "true": (
                    "パスワードの入力、ログイン、アカウント確認のための"
                    "個人情報の提供を明示的または暗示的に求めている"
                ),
                "false": (
                    "認証情報の提供は求めておらず、"
                    "一般的な情報提供や別の内容についての連絡である"
                ),
            },
        ),
    },
)

print(response.answers["requests_credentials"].noul)

実行結果(例):

0.96

このように criteria を明確に定義しておくことで、「このメッセージは認証情報を求めていると言えるのか、それとも単なる注意喚起に過ぎないのか」といった、判断が割れやすいグレーゾーンのケースでも、一貫性のある結果が得られやすくなります。

8-4. Noulの値を、プログラムのif文にどう落とし込むか

最後に、Noulの数値を実際のプログラムロジックに組み込む、シンプルな例を見てみましょう。

from typesafe_sdk import Noul, TypeSafeClient

client = TypeSafeClient()

def contains_refund_request(message: str) -> bool:
    """メッセージに返金要求が含まれているかどうかを、booleanとして判定する。"""
    response = client.system_one(
        state=message,
        questions={
            "refund_requested": Noul(
                instructions="このメッセージは返金を要求していますか?",
            ),
        },
    )
    noul_value = response.answers["refund_requested"].noul
    # 0.5を境界(閾値)として、Yes/Noに変換する
    return noul_value > 0.5


message = "先週購入した商品が不良品だったので、返金してもらえますか?"
if contains_refund_request(message):
    print("→ 返金対応フローへ進みます")
else:
    print("→ 通常の問い合わせ対応フローへ進みます")

実行結果(例):

→ 返金対応フローへ進みます

ここでは noul_value > 0.5 というシンプルな比較で、確率値を「はい/いいえ」の判定に変換しています。ただし、この 0.5 という境界値は絶対的なものではありません。次のパート(第2部)で詳しく解説する「confidence(確信度)」の考え方と組み合わせることで、より賢く、リスクに応じた閾値の設定ができるようになります。この話題は、第2部の中心テーマの一つとして詳しく扱います。


まとめ

ここまで、Jevというモデルの根本的な考え方から、開発環境の準備、そして3つのプリミティブ(Choice、Score、Noul)の使い方までを、コード例を交えながら一通り解説してきました。

最後に、学んだ内容を振り返っておきましょう。

  • LLM(生成AI)は文章を生成するために設計されているが、Jevは構造化された判断そのものを返す「System One モデル」である
  • Jevは、大きく曖昧な判断を一度に問うのではなく、小さく原子的な質問に分解し、その答えをコード側で合成するという設計思想にもとづいている
  • State(評価対象)と questions(質問)をJevに渡すと、answers(型のついた答え)が返ってくる、というのが基本の構造である
  • State(内容)とQuestions(判断基準)は明確に役割が分かれており、同じStateに対して複数の質問を自由に組み合わせられる
  • 3つのプリミティブには、それぞれ明確な使いどころがある:
    • Choice:順序のない選択肢から1つを選ぶ(例:担当部署の振り分け)
    • Score:説明可能なスペクトル上の位置を採点する(例:深刻度、満足度)
    • Noul:Yes/Noの確率を問う(例:緊急性の有無、返金要求の有無)
  • 複数の質問は、1回のリクエストにまとめて投げることができ、質問を増やしても応答時間はほとんど変わらない

続きは以下をご覧ください。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?