0
0

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 に「なんか動かないんですけど」を分類させたら何が返ってくるか ― Go で判断特化 AI の境界を探ってみる

0
Posted at

はじめに

2026年9月、TypeSafe AI から Jev という AI モデルが公開されました。

ChatGPT のような一般的な LLM が文章を生成するのに対し、Jev は文章を一切生成しないモデルです。代わりに、与えられたテキストに対して事前定義された選択肢の中から判断を返すことに特化しています。1

例えば「商品が届いたのですが破損していました」という問い合わせに対して、billing / technical / shipping / account / other という候補を渡すと、

shipping    = 1.00
billing     = 0.00
other       = 0.00
confidence  = 1.00

のような判断結果と確率だけが返ってきます。文章による説明はありません。明確な入力では1位が確率をほぼ独占し、confidence も 1.00 に張り付きます。

では、「なんか動かないんですけど」のような曖昧な入力を投げたらどうなるのか? 今回は Go で Jev の API を叩いて、判断の境界を探ってみました。

なお、TypeSafe 本体は新規アカウントだとクレジット無しで API キーが発行できないため、今回は Qiita キャンペーン でも案内されている OpenRouter 経由で Jev を呼んでいます。2


Jev の概要

LLM との違い

一般的な LLM Jev
主目的 文章・コードの生成 判断
出力 文字列(自由) 型付き値(事前定義)
分類・ルーティング 可能(出力のパースが必要) 得意(型が固定)
確率 通常は直接扱わない 判断確率を利用可能
コスト モデルによる $0.042 / 1M input tokens

LLM = 考えて文章を書く AI
Jev = 曖昧な条件を判定する AI

と捉えると分かりやすいです。TypeSafe AI はこのタイプを System One Model と呼んでいます。

3種類の判断型

型 何をするか 出力
Choice 複数候補から1つ選ぶ 選ばれた候補 + confidence + 各候補の probabilities
Score 段階評価 段階の確率分布 + スコア(float, 0〜4)
Noul Yes/No の確率判定 0.0〜1.0

ハルシネーションしない?

TypeSafe AI は「Zero Hallucinations」と表現しています。1

ただしこれは正しい判断を保証するという意味ではありません。出力が事前定義された選択肢に固定されるため、super_darkness_dragon のような存在しない値は返しませんが、shipping を billing と誤判定する可能性はあります。


Go で Jev を叩く

セットアップ

OpenRouter でキーを発行し、環境変数を設定します。

export TYPESAFE_API_KEY="sk-or-v1-xxxxxxxx"          # OpenRouter のキー
export TYPESAFE_BASE_URL="https://openrouter.ai/api"  # OpenRouter 経由
export TYPESAFE_MODEL="~typesafe/jev-latest"           # 最新の Jev

mkdir jev-demo && cd jev-demo && go mod init jev-demo

エンドポイントは POST https://openrouter.ai/api/v1/systemone です。Go には公式 SDK がないため、net/http で直接叩きます。

コード

以下は呼び出しの骨格です。検証ではこれに加えて、応答の probabilities から確率分布と1位2位差を表示し、Score は *float64 で受けています。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

const defaultURL = "https://api.typesafe.ai/v1/systemone"

type Request struct {
	Model     string                 `json:"model"`
	State     string                 `json:"state"`
	Questions map[string]interface{} `json:"questions"`
}

type Response struct {
	Answers map[string]Answer `json:"answers"`
	Model   string            `json:"model"`
}

type Answer struct {
	Choice        string             `json:"choice,omitempty"`
	Confidence    float64            `json:"confidence,omitempty"`
	Noul          *float64           `json:"noul,omitempty"`
	Score         *float64           `json:"score,omitempty"`  // float。*int だと落ちる
	Probabilities map[string]float64 `json:"probabilities,omitempty"`
}

func apiURL() string {
	if u := os.Getenv("TYPESAFE_BASE_URL"); u != "" {
		return u + "/v1/systemone"
	}
	return defaultURL
}

func model() string {
	if m := os.Getenv("TYPESAFE_MODEL"); m != "" {
		return m
	}
	return "jev-latest"
}

func callJev(state string, questions map[string]interface{}) (*Response, error) {
	body, _ := json.Marshal(Request{
		Model:     model(),
		State:     state,
		Questions: questions,
	})

	req, _ := http.NewRequest("POST", apiURL(), bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TYPESAFE_API_KEY"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		respBody, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
	}

	var result Response
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, err
	}
	return &result, nil
}

func main() {
	if len(os.Args) < 3 {
		fmt.Println("Usage: go run main.go <choice|score|noul> <text>")
		os.Exit(1)
	}

	text := os.Args[2]

	switch os.Args[1] {
	case "choice":
		resp, err := callJev(text, map[string]interface{}{
			"category": map[string]interface{}{
				"type":         "choice",
				"instructions": "Which department should handle this inquiry?",
				"criteria": map[string]string{
					"billing":   "Payment or invoice issues",
					"technical": "Product bugs or technical problems",
					"shipping":  "Delivery, damage, or logistics",
					"account":   "Account management or cancellation",
					"other":     "Anything else",
				},
			},
		})
		if err != nil {
			fmt.Println("Error:", err)
			return
		}
		ans := resp.Answers["category"]
		fmt.Printf("判定: %s\nconfidence: %.4f\n", ans.Choice, ans.Confidence)
		// 検証用: probabilities の出力は省略(完全版は後述)

	case "score":
		resp, err := callJev(text, map[string]interface{}{
			"severity": map[string]interface{}{
				"type":         "score",
				"instructions": "How severe is this incident?",
				"criteria":     []string{"trivial", "low", "medium", "high", "critical"},
			},
		})
		if err != nil {
			fmt.Println("Error:", err)
			return
		}
		fmt.Printf("重大度: %.2f/4\n", *resp.Answers["severity"].Score)

	case "noul":
		resp, err := callJev(text, map[string]interface{}{
			"destructive": map[string]interface{}{
				"type":         "noul",
				"instructions": "Is this SQL statement destructive?",
			},
		})
		if err != nil {
			fmt.Println("Error:", err)
			return
		}
		v := *resp.Answers["destructive"].Noul
		fmt.Printf("危険度: %.4f\n", v)
		switch {
		case v >= 0.90:
			fmt.Println("→ ブロック推奨")
		case v >= 0.60:
			fmt.Println("→ ユーザー確認推奨")
		default:
			fmt.Println("→ 実行可")
		}
	}
}

全体で約120行です。TYPESAFE_BASE_URL / TYPESAFE_MODEL を設定すれば、OpenRouter にも TypeSafe 直にもそのまま向きます。


まずは明確な入力で確認

Choice: 問い合わせ分類

候補 billing / technical / shipping / account / other に対して、明確な問い合わせ文を投げます。

$ go run main.go choice "商品が届いたのですが破損していました。交換できますか?"
判定: shipping
confidence: 1.0000
入力 判定 confidence
商品が届いたのですが破損していました。交換できますか? shipping 1.00
請求額が違うのですが確認できますか billing 1.00
API のレスポンスが遅くなっています technical 1.00

明確な入力なら期待通りで、confidence は 1.00 に張り付きます(分布も1位がほぼすべてを総取り)。

Score / Noul も同様

型 入力 結果
Score 全ユーザーがログインできない状態が30分続いている 3.84/4 (critical)
Noul DROP TABLE users; 0.99(ブロック推奨)
Noul SELECT * FROM users WHERE id = 1 0.02(実行可)

Score は 0〜4 の5段階(0=trivial 〜 4=critical)を float で返す点に注意(3.84 のように中間値になる)。

ここまでは予想通りです。では、入力を曖昧にしていきます。


曖昧な入力で confidence はどう変わるか

ここからが本題です。同じ Choice の候補に対して、段階的に曖昧な入力を投げて、confidence の変化を観察しました。

複数カテゴリにまたがる入力

1つの文に2つの話題が混在するケースです。

入力 判定 confidence 2位 1位2位差
商品が届いたのですが破損していました(※明確な入力) shipping 1.00 — 1.00
届いた商品が違うので返金してほしい shipping 0.70 billing 0.20 0.56
使い方がわからないので返品したい other 0.22 technical 0.35 0.03
アカウントを解約して、残りの料金を返してください account 0.68 billing 0.26 0.48

「使い方がわからないので返品したい」は other=0.38 / technical=0.35 / account=0.26 の三つ巴。confidence が低い(0.22)だけでなく、再実行すると1位が other↔technical で入れ替わる(後述の非決定性テストで確認)。判定そのものが不安定になる領域。

情報が不足している入力

主語や目的語が省略された、日本語によくある表現を投げます。

入力 判定 confidence 2位 1位2位差
なんか動かないんですけど technical 0.96 other 0.03 0.94
ちょっと困ってるんですが other 1.00 — 1.00
すみません other 1.00 — 1.00

予想外の結果: 情報が減るほど confidence が下がる…とはならなかった。情報ゼロに近い入力は迷うのではなく、自信を持って other に分類される(「すみません」は other=1.00)。confidence が下がるのは「複数の実カテゴリが競合するとき」であって、「情報が無いとき」ではない。

感情・緊急度だけの入力

内容がなく、感情や緊急度だけが伝わる入力です。

入力 判定 confidence 2位 1位2位差
至急対応お願いします!!! other 0.99 technical 0.01 0.98
もういい加減にしてください。何回言えばわかるんですか other 0.99 account 0.01 0.98
ありがとうございました。とても助かりました other 1.00 — 1.00

感情(緊急・怒り・感謝)はカテゴリ判定をほとんど引っ張らず、内容が無ければ高い confidence で other に落ちる。感情の強さと内容カテゴリは独立した軸であることが観測できた。

日本語のぼかし表現

敬語・指示語・文末省略など、日本語特有の曖昧さを持つ入力です。

入力 判定 confidence 2位 1位2位差
お手数ですが、先日の件について確認いただけますでしょうか other 1.00 — 1.00
例の件、どうなりました? other 1.00 — 1.00
商品届いたんですけど、なんか違くて… shipping 0.45 other 0.40 0.16

「商品届いたんですけど、なんか違くて…」は口語でも shipping=0.56 を拾えた(ただし other=0.40 と僅差)。一方「先日の件」「例の件」のような指示語だけの入力は内容が無いため other=1.00 に落ちる。口語の崩れには耐性があるが、文脈依存の指示語は(当然ながら)判定材料にならない。

全体の傾向

全テストケースを並べると、はっきりした傾向が出ました。

明確な入力:      confidence 1.00          1位2位差 1.00     (断定)
2カテゴリ混在:   confidence 0.22〜0.73    1位2位差 0.03〜0.62(ここだけ低い)
情報不足:        confidence 0.96〜1.00    1位2位差 0.94〜1.00(自信を持って other)
感情のみ:        confidence 0.99〜1.00    1位2位差 0.98〜1.00(自信を持って other)
ぼかし表現:      confidence 0.45〜1.00    1位2位差 0.16〜1.00(指示語は other)

当初は「曖昧にするほど confidence が下がる」と予想していました。実際はそうではなく、confidence が下がったのは「複数の実カテゴリが競合する2カテゴリ混在」だけでした。情報不足・感情のみ・ぼかし表現は、迷うどころか高い confidence で other に分類されます。

つまり Jev の confidence は「入力の曖昧さ」ではなく「選択肢間の競合の強さ」を反映していると考えるのが正確です。


Noul の境界: SQL の微妙なケース

Noul(破壊的操作の判定)でも、明確なケース以外を試しました。

入力 危険度 判定 備考
SELECT * FROM users WHERE id = 1 0.02 実行可 ベースライン(安全)
DROP TABLE users; 0.99 ブロック推奨 ベースライン(危険)
TRUNCATE TABLE users; 0.98 ブロック推奨 DROP とほぼ同等に危険と判定
ALTER TABLE users ADD COLUMN email VARCHAR(255); 0.07 実行可 DDL でも非破壊なら低リスク
UPDATE users SET deleted_at = NOW() 0.79 ユーザー確認 WHERE 無し全件更新として警戒
UPDATE users SET name = 'test' 0.82 ユーザー確認 WHERE 無し全件更新
UPDATE users SET name = 'test' WHERE id = 1 0.59 実行可 WHERE 追加で危険度が低下

確認できたこと:

  • 「DDL だから危険」ではない。TRUNCATE=0.98 と警戒する一方、ALTER ADD COLUMN=0.07。構文カテゴリではなく「破壊性」を見ている。
  • WHERE の有無が効く。同じ UPDATE users SET name='test' でも、WHERE 無し=0.82、WHERE id=1 付き=0.59。全件か1行かを危険度に反映している。

同じ入力を5回投げると、結論が変わる

ここまでの表はすべて1回のスナップショットです。Jev は非決定的で、同一入力でも呼び出しごとに値が揺れます。

大半のケースでは揺れは小さく、ラベルが変わることはありません。しかし閾値の境界では実害が出ます。実際に境界付近のケースを各5回呼んで確認しました。

入力 5回の観測 何が困るか
使い方がわからないので返品したい(B2) 1位が other↔technical で入れ替わる。conf 0.21–0.30、差 0.02–0.13 部署ルーティングの行き先が毎回変わる
UPDATE ... WHERE id = 1(N7) noul 0.54–0.61。閾値 0.60 をまたぐ 「実行可」と「ユーザー確認」が呼ぶたびに変わる
商品届いたんですけど、なんか違くて…(E3) shipping で安定。conf 0.44–0.50 判定は安定。確信度だけ変動
UPDATE ... name='test'(N6) noul 0.80–0.83 常に「確認」帯。閾値から離れていれば安定

E3 と N6 のように、閾値から離れていれば揺れても結論は変わりません。しかし B2 と N7 は、ルーティング先や実行可否が呼ぶたびに反転します。

「確率が返る」ことと「毎回同じ値が返る」ことは別です。閾値の境界ではマージンを設ける(例: 0.55〜0.65 は一律「人間確認」に倒す)か、複数回サンプリングして多数決を取るヒステリシス設計が要ります。


発見と考察

confidence は「曖昧さ」ではなく「選択肢の競合」を測っている

当初の仮説は「入力が曖昧になるほど confidence が下がる」でした。実測ではこれは半分外れでした。

  • 情報ゼロの「すみません」「助けて」→ other=1.00(迷っていない)
  • 複数カテゴリが競合する「使い方がわからないので返品したい」→ other=0.38 / technical=0.35(confidence 0.22)

confidence が落ちるのは「情報が少ないとき」ではなく「複数の実カテゴリが拮抗するとき」でした。したがって、

if ans.Confidence < 0.60 {
    escalateToHuman(inquiry) // 競合していて判断がつかない → 人間へ
} else {
    routeTo(ans.Choice)
}

というエスカレーション設計は成立しますが、これが拾うのは「カテゴリ競合ケース」であって、「情報が無いケース」は高 confidence の other として素通りします。other の多発を別途モニタリングすることで、情報不足のケースもカバーする必要があります。

Noul は構文ではなく「破壊性」を見ている

TRUNCATE=0.98 と警戒する一方、同じ DDL でも ALTER ADD COLUMN=0.07。さらに WHERE の有無で UPDATE の危険度が 0.82→0.59 に下がります。キーワードマッチではなく、操作が実際にデータを壊すかどうかを意味的に判定できている点は、ルールベースの正規表現では書きづらい挙動です。

感情と内容は別の軸

「もういい加減にしてください」のような怒りの入力は、感情に引っ張られず other=0.99 と判定されました。感情の強さはカテゴリ判定にほとんど影響しません。

これは、カテゴリ分類(Choice)と緊急度判定(Score / Noul)を別の質問として同時に投げる設計が有効であることを示唆しています。Jev は1回のリクエストで複数の質問を評価できるため、3

{
  "questions": {
    "category": { "type": "choice", ... },
    "is_urgent": { "type": "noul", "instructions": "Is this message urgent?" }
  }
}

のように、内容と緊急度を同時に判定する使い方が自然です。

API の型に注意

検証中に踏んだ罠として、Score が *int だとデコードで落ちる点があります。API は 3.84 のような float を返すため、*float64 で受ける必要があります。また confidence と各候補の probabilities は別々のフィールドで返り、1位の確率と confidence は一致しません。


まとめ

Jev に明確な入力を渡せば期待通りの判定が返ります。これ自体は多くの記事で紹介されている通りです。

今回面白かったのは、入力を曖昧にしたときの confidence の変化と、同じ入力でも結論が変わる非決定性です。

発見 設計への示唆
confidence が下がるのは「カテゴリ競合」時だけ。情報ゼロは高 confidence で other に落ちる 低 confidence でエスカレーション + other 多発のモニタリングを組み合わせる
同じ入力でも閾値の境界では結論が変わる(N7: 0.54〜0.61 で 0.60 をまたぐ) 境界にマージン or 複数回サンプリングでヒステリシスを入れる
Noul は構文でなく破壊性を見る(TRUNCATE 0.98 vs ALTER 0.07、WHERE 有無で 0.82→0.59) 正規表現より頑健な危険操作フィルタに使える
感情と内容は独立した軸(怒りでも other=0.99) Choice と Noul/Score を同一リクエストで組み合わせるのが有効
Score は 0〜4 の float / confidence と probabilities は別物 クライアントの型は float で受ける(int だとクラッシュ)

Jev を「ルールベースでは書きづらいが、LLM を呼ぶほどでもない判断処理」に使うなら、confidence を活用した設計と非決定性への備えが鍵になりそうです。

confidence が高い → 自動処理
confidence が低い → 人間に回す
閾値の際 → マージンを持たせる


参考

  1. TypeSafe AI, Introducing System One Models & Jev, 2026-09-15. Jev の System One Model としての位置付け、型付き確率判断、価格等。 ↩ ↩2

  2. OpenRouter, Jev Documentation, https://openrouter.ai/docs/guides/community/jev ― OpenRouter 経由の利用方法とエンドポイント。 ↩

  3. Jev では Choice / Score / Noul などの型付き質問を同じ state に対して同一リクエストで評価できる。 ↩

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?