Jev を実際の仕組みに組み込むとき、公式が示している設計パターンは4つです。共通する考え方は、判断は小さく分けて Jev に、組み合わせと分岐はコードに、です。
| パターン | 一言で | 使いどころ |
|---|---|---|
| 投機的ファンアウト | 要りそうな質問を全部、1回で聞く | 後続の処理が前の答えで枝分かれする |
| 確信度ゲート | 迷いの小さい判断だけを自動で通し、残りは確認や人に回す | 間違えると困る操作がある |
| 合成スコア | 観点ごとに採点し、重みはコードで掛ける | 複数の基準で順位をつけたい |
| 意図ルーティング | 先に Jev で振り分け、適した処理に渡す | 高価な LLM や人に回す前の受付 |
1. 投機的ファンアウト — 全部まとめて1回で聞く
「まず種類を聞き、不具合だったら深刻度を聞く」と2回呼ぶ代わりに、深刻度も最初から聞いてしまい、不具合でなければその答えを捨てます。
const state = {
ticket: 'CSV をアップロードすると毎回 500 エラーになります。昨日のリリース以降です。月末の請求処理が止まっていて困っています。',
};
const questions = {
category: {
type: 'choice',
instructions: 'What kind of ticket is `ticket`?',
criteria: { bug: 'Something is broken', billing: 'Payments or invoices', howto: 'How to use a feature', other: null },
},
severity: {
type: 'score',
instructions: 'If `ticket` reports a bug, how severe is it?',
criteria: ['Cosmetic', 'Workaround exists', 'Blocking; no workaround'],
},
frustration: { type: 'score', instructions: 'How frustrated is the customer?', criteria: ['Calm', 'Frustrated', 'Very angry'] },
mentions_deadline: { type: 'noul', instructions: 'Does `ticket` mention a deadline or time pressure?' },
};
実行結果(2026-09-21):
| 質問 | 答え |
|---|---|
category | bug(confidence 1) |
severity | 2 = Blocking(confidence 0.99) |
frustration | 1.01 = Frustrated |
mentions_deadline | 0.90 |
if (a.category.choice === 'bug' && a.severity.score >= 1.5) pageOnCall();
// bug でなければ severity は見ない。frustration は種類によらず使える
質問を足すコストは小さい。 同じ state で、1問だけのときと4問のときを比べました。
| 質問の数 | 応答時間 | 入力トークン |
|---|---|---|
| 1問 | 673ms | 403 |
| 4問 | 694ms | 515 |
1回ずつの計測です。2回に分けて呼ぶと、通信の往復が1回増え、state のぶんの入力トークンも2回数えられます。まとめれば、増えるのは質問のぶんのトークンだけです。応答時間がどれだけ変わるかは通信の状況によるので、自分の環境で測ってください。
注意点は2つ。
- 各質問は独立に評価されます。
severityは「categoryが bug だった場合」という前提を知りません。だからinstructionsに「Ifticketreports a bug」と条件を書いています - 上限はリクエスト全体で 64k トークンです。質問を数百個積むときは 料金と制限 を確認します
2. 確信度ゲート — 迷いの小さい判断だけを自動で通す
答え(choice)は「どれだと判断したか」、確信度(confidence)は「その判断で確率がどれだけ1つに集中していたか」です。確信度は、その操作をしてよい権限があることも、答えが正しいことも保証しません。 迷いの小さい判断だけを自動で通すための目安として使い、操作の危なさに応じて通す確信度を変えます。 本人確認や権限の確認は、別にコードで行います。
const a = res.answers.intent; // 'check_balance' | 'transfer' | 'support'
if (a.confidence < 0.6) return routeToHuman(); // どの操作でも、迷っているなら人へ
switch (a.choice) {
case 'check_balance':
return showBalance(); // 読むだけ。間違えても取り返せる
case 'transfer':
return a.confidence > 0.9 ? confirmThenExecute() : askUserToConfirm();
case 'support':
return openSupport();
}
しきい値の数字は用途ごとに決めるものです。決め方と、同じ入力でも値が 0.01〜0.02 揺れる点は 確率と confidence の読み方 にまとめました。
3. 合成スコア — 観点ごとに採点し、重みはコードで
「この候補者は良いか」のような総合評価を1つの質問にすると、何が効いたのか分からず、基準を変えるたびに質問文を書き直すことになります。観点ごとに Score を取り、重みづけはコードで行います。
const levels = ['No evidence', 'Some evidence', 'Strong evidence'];
const questions = {
backend: { type: 'score', instructions: 'Evidence of backend engineering depth in `resume`', criteria: levels },
leadership: { type: 'score', instructions: 'Evidence of people leadership in `resume`', criteria: levels },
oss: { type: 'score', instructions: 'Evidence of open source contribution in `resume`', criteria: levels },
};
resume に「バックエンド歴6年、Go と PostgreSQL で決済基盤を設計・運用。直近2年はテックリードとして5名を率い、採用面接と 1on1 を担当。OSS への貢献は少しだけ。」を渡した結果:
| 観点 | score(0〜2) |
|---|---|
| backend | 1.72 |
| leadership | 1.91 |
| oss | 0.99 |
type Dimension = 'backend' | 'leadership' | 'oss';
// 上の表の値。実際のコードでは res.answers[k].score から取る
const scores: Record<Dimension, number> = { backend: 1.72, leadership: 1.91, oss: 0.99 };
const weights: Record<'seniorIC' | 'manager', Record<Dimension, number>> = {
seniorIC: { backend: 0.6, leadership: 0.1, oss: 0.3 },
manager: { backend: 0.3, leadership: 0.6, oss: 0.1 },
};
const MAX_LEVEL = 2; // 3段階なので 0〜2
const total = (w: Record<Dimension, number>): number =>
(Object.keys(w) as Dimension[]).reduce((sum, k) => sum + w[k] * (scores[k] / MAX_LEVEL), 0);
console.log(total(weights.seniorIC).toFixed(2)); // 0.76
console.log(total(weights.manager).toFixed(2)); // 0.88
同じ採点結果から、役割ごとの点数が追加の API 呼び出しなしで出ます。採用方針が変わったら、直すのはコードの係数だけです。
使い方の注意:
- 合計点は、同じ基準で採点した候補どうしの順位づけに使う。公式は、Score の値を精密な物差しとして扱うことを避けるよう書いています(Jev が苦手なこと)
- 段階の説明は具体的に。
'Some evidence'のような抽象的な段階は例を簡単にするためのもので、本番では「何が書かれていればその段階か」を書きます - 人の採否のような重い判断を、この点数だけで自動決定しない
4. 意図ルーティング — 先に振り分けてから、適した処理へ
すべての問い合わせを高価な LLM に通す必要はありません。Jev を受付に置き、内容に応じて渡す先を変えます。
| 振り分け先 | 例 |
|---|---|
| 決まった処理(データベースの参照など) | 「残高を教えて」「注文の状況は?」 |
| 専門の LLM | 込み入った技術的な質問、文章の作成 |
| 人 | 苦情、解約、確信度が低いもの |
const questions = {
intent: {
type: 'choice',
instructions: 'What is the customer asking for in `message`?',
criteria: {
order_status: 'Where is my order / delivery date',
how_to: 'How to use a product feature',
complaint: 'Expresses dissatisfaction or asks for compensation',
other: null,
},
},
complexity: {
type: 'score',
instructions: 'How complex is the request in `message`?',
criteria: ['Answerable with a single lookup', 'Needs explanation', 'Needs investigation across systems'],
},
};
意図(Choice)と複雑さ(Score)を1回で聞くのはファンアウト、intent.confidence が低ければ人に回すのは確信度ゲートです。4つのパターンは、このように重ねて使います。
このルーティングの例は公式の構成を日本語向けに書き直したもので、このサイトでは実行していません(実行したのは 1 と 3 です)。
共通の注意
otherを入れる。 Choice は必ずどれかを選びます。想定外の入力の逃げ道が無いと、無理に当てはめます- 日本語の入力では先に測る。 公式は、英語以外では精度が下がると明記しています。振り分けの正解が分かっているデータで、誤って振り分けた割合と、人に回った割合を見ます
- Jev に向かない判断を混ぜない。 金額の計算、日付の前後、件数の集計はコードで行います
質問の書き方は 3つの質問タイプ、state の作り方は state の設計 を参照してください。
よくある質問
使わないかもしれない質問まで送るのは無駄ではありませんか?
ほとんど無駄になりません。state は1回しか読まれず、質問は並列に評価されます。手元の計測では、質問を1問から4問に増やしても応答時間は 673ms から 694ms、入力は 403 トークンから 515 トークンに増えただけでした。2回に分けて呼ぶと、通信の往復が1回増え、state のぶんの入力トークンも2回数えられます。
合成スコアの合計点は、絶対的な評価として使えますか?
使わないほうが安全です。公式は、Score の値から元の数値を逆算するような使い方は数値の校正が弱いので避けるよう書いています。合成スコアは、同じ基準で採点した候補どうしの順位づけに使うのが向いています。
4つのパターンは組み合わせられますか?
組み合わせて使うのが普通です。意図ルーティングで振り分け、その判定に確信度ゲートを掛け、振り分けに必要な質問はファンアウトで1回にまとめる、という形になります。
出典・参考リンク
- 一次情報 Patterns
- 一次情報 Speculative fan-out
- 一次情報 Confidence-gated routing
- 一次情報 Composite scoring
- 一次情報 Intent routing