Jev Recipe / 棄答と誠実な境界
Jev で棄答を実装:正解が「わからない」のとき
目の前の state から答えられない質問には、どの信頼度しきい値も効きません。型付き Choice に is_answerable Noul と missing_information Choice を組み合わせ、欠けている情報を名指す構造化された「わからない」を返させ、自動化を回答可能性と信頼度の 2 軸でゲートします。
棄答コントラクトを構築する 6 ステップ
{
"answer": {
"type": "choice",
"instructions": "state に含まれる情報だけを使って答える",
"criteria": {
"billing": "支払い・請求・返金",
"technical": "製品の不具合",
"sales": "購入・アップグレードの相談"
}
},
"is_answerable": {
"type": "noul",
"instructions": "state は、ちょうど 1 つの選択肢を選ぶのに十分な情報を含んでいますか——追加データではなく、推測でもなく?"
},
"missing_information": {
"type": "choice",
"instructions": "is_answerable が no の場合:欠けているのはどの 1 つですか?",
"criteria": {
"none": "state は十分",
"a_date": "日付または期限",
"an_amount": "金額または残高",
"an_identity": "人物またはアカウントの特定",
"a_policy_rule": "適用されるポリシーまたはバージョン",
"external_state": "別システムのライブデータ"
}
}
}6 つの質問、1 つの回答可能性ゲート
以下の各質問は、棄答コントラクト(型付き回答・較正済み信頼度・is_answerable Noul)付きで evaluate エンドポイントを通過済みです。回答可能性ゲートを動かし、レーンの再計算を確認してください。危険な行は自信のある行です。注文 #9012 は、state には存在し得ない日付に信頼度 0.88 を示しています。信頼度は「どれだけ確信しているか」であって、「答えが state に存在するか」ではないのです。
| 質問と state | 回答と信頼度 | is_answerable | レーン |
|---|---|---|---|
注文 #5521 はポリシー v3 で返金対象ですか? q_101 · 注文明細 + 返品期間の日付 + ポリシーイベント | 対象0.96 | 0.98 | 回答 |
どのチームの案件か:billing / technical / sales? q_102 · 話題が明確なチケット本文一式 | billing0.97 | 0.95 | 回答 |
この注文の前に顧客は住所を変更しましたか? q_103 · タイムスタンプ付き住所が 2 つあるプロファイル | true0.94 | 0.91 | 回答 |
注文 #9012 はいつ出荷されますか? q_104 · 注文明細のみ——在庫なし、配送業者 SLA なし | 2026-10-140.88 | 0.31 | 棄答信頼度だけなら自動実行されていた回答 |
欠落:在庫状態 + 配送業者 SLA テーブル | |||
アカウント #4183 はなぜ解約したのですか? q_105 · 利用メトリクスのみ——調査も会話もなし | 価格感応度0.71 | 0.22 | 棄答 |
欠落:顧客の直接発言——メトリクスは何が起きたかを示すだけで「なぜ」は示さない | |||
契約 #77 は次四半期に更新されますか? q_106 · 利用トレンド——更新結果は未来の情報 | true0.66 | 0.41 | 棄答 |
欠落:結果はまだ存在しない——あるのは意図のシグナルだけ | |||
デモデータのみ——6 件の模擬質問。レーンはレシピ本文と同じ回答可能性ルールで再計算されます。
棄答の挙動:型付きコントラクト vs プロンプトで「わからない」vs 信頼度だけのゲート
実装コード:棄答コントラクトの TypeScript / Python
import os
import requests
resp = requests.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"},
json={
"model": "gpt-4o-mini",
"messages": [{
"role": "user",
"content": (
"注文 #9012 はいつ出荷されますか?以下のデータのみ使用してください。"
"データに答えがない場合は、一字一句違わず I DON'T KNOW と返してください\n"
"データ:{'lines': [...], 'channel': 'web'}"
),
}],
},
timeout=30,
)
text = resp.json()["choices"][0]["message"]["content"]
if "I DON'T KNOW" in text:
...
# 見えない失敗:言い方を「遅くともいつまでに」に変えたり、口調を柔らかくしたり、
# データを長く貼ったりすると、同じ state が「10 月 14 日に出荷」と返してきます——
# 流暢で、反証不可能で、根拠のある答えと区別がつきません。ゲートしていたのは散文で、
# 散文にはしきい値がありません。import requests
JEV_ENDPOINT = "https://api.typesafe.ai/v1/jev/evaluate"
ANSWERABILITY_GATE = 0.60 # is_answerable がこれ未満 -> 棄答
AUTO_CONFIDENCE = 0.85 # 回答の信頼度がこれ未満 -> レビュー
QUESTIONS = {
"answer": {
"type": "choice",
"instructions": "state に含まれる情報だけを使って答える",
"criteria": {
"billing": "支払い・請求・返金",
"technical": "製品の不具合",
"sales": "購入・アップグレードの相談",
},
},
"is_answerable": {
"type": "noul",
"instructions": "state は、ちょうど 1 つの選択肢を選ぶのに十分な情報を含んでいますか——追加データではなく、推測でもなく?",
},
"missing_information": {
"type": "choice",
"instructions": "is_answerable が no の場合:欠けているのはどの 1 つですか?",
"criteria": {
"none": "state は十分",
"a_date": "日付または期限",
"an_amount": "金額または残高",
"an_identity": "人物またはアカウントの特定",
"a_policy_rule": "適用されるポリシーまたはバージョン",
"external_state": "別システムのライブデータ",
},
},
}
def decide_or_abstain(state: dict) -> dict:
resp = requests.post(
JEV_ENDPOINT,
json={"state": state, "questions": QUESTIONS},
timeout=5,
)
resp.raise_for_status()
data = resp.json()
knows = data["is_answerable"]
if not knows["answer"] or knows["confidence"] < ANSWERABILITY_GATE:
# 型付き棄答:ワークフローは欠落キーでルーティングし、
# このオブジェクトは記録・集計・リプレイ可能です。
return {
"abstained": True,
"missing": data["missing_information"]["answer"],
"is_answerable": knows["confidence"],
}
answer = data["answer"]
lane = "auto" if answer["confidence"] >= AUTO_CONFIDENCE else "review"
return {
"abstained": False,
"answer": answer["answer"],
"confidence": answer["confidence"],
"lane": lane,
}import OpenAI from "openai";
const openai = new OpenAI();
export async function whenDoesItShip(orderData: object) {
const resp = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "user",
content: `この注文はいつ出荷されますか?以下のデータのみ使用。データに答えがない場合は、一字一句違わず I DON'T KNOW と返してください\nデータ:${JSON.stringify(orderData)}`,
},
],
});
const text = resp.choices[0].message.content ?? "";
// ゲートしているのは散文です。質問の言い換え、柔らかい口調、長いデータ貼り付けで
// 拒否挙動は変わります——そしてこの戻り値の型には、根拠のある日付と
// 自信のある推測を区別するものが何もありません。
return { text, refused: text.includes("I DON'T KNOW") };
}const JEV_ENDPOINT = "https://api.typesafe.ai/v1/jev/evaluate";
const ANSWERABILITY_GATE = 0.6; // is_answerable が未満 -> 棄答
const AUTO_CONFIDENCE = 0.85; // 回答の信頼度が未満 -> レビュー
const QUESTIONS = {
answer: {
type: "choice",
instructions: "state に含まれる情報だけを使って答える",
criteria: {
billing: "支払い・請求・返金",
technical: "製品の不具合",
sales: "購入・アップグレードの相談",
},
},
is_answerable: {
type: "noul",
instructions: "state は、ちょうど 1 つの選択肢を選ぶのに十分な情報を含んでいますか——追加データではなく、推測でもなく?",
},
missing_information: {
type: "choice",
instructions: "is_answerable が no の場合:欠けているのはどの 1 つですか?",
criteria: {
none: "state は十分",
a_date: "日付または期限",
an_amount: "金額または残高",
an_identity: "人物またはアカウントの特定",
a_policy_rule: "適用されるポリシーまたはバージョン",
external_state: "別システムのライブデータ",
},
},
} as const;
export type Outcome =
| { abstained: true; missing: string; is_answerable: number }
| {
abstained: false;
answer: string;
confidence: number;
lane: "auto" | "review";
};
export async function decideOrAbstain(state: object): Promise<Outcome> {
const resp = await fetch(JEV_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state, questions: QUESTIONS }),
});
if (!resp.ok) throw new Error("Jev evaluate failed");
const data = await resp.json();
const knows = data.is_answerable as { answer: boolean; confidence: number };
if (!knows.answer || knows.confidence < ANSWERABILITY_GATE) {
return {
abstained: true,
missing: (data.missing_information as { answer: string }).answer,
is_answerable: knows.confidence,
};
}
const answer = data.answer as { answer: string; confidence: number };
return {
abstained: false,
answer: answer.answer,
confidence: answer.confidence,
lane: answer.confidence >= AUTO_CONFIDENCE ? "auto" : "review",
};
}
// 1 回の呼び出し、3 つの型付き読み取り。棄答ブランチは欠落キーを持ち運ぶので、
// ワークフローはまさにその情報を収集しに行けます。判断モデルの棄答 FAQ
判断モデルの棄答(abstention)とは何ですか?
入力 state に答えが含まれていないとき、モデルが推測するのではなく、明示的で型付きの「わからない」——欠けている情報の名指し付き——を返すことです。このページで構築するコントラクトでは、棄答は is_answerable Noul としきい値で決まるレーンであり、棄答オブジェクトは missing_information キーを持つため、ワークフローは欠けているものを正確に収集しに行けます。棄答は判断品質における証拠の軸です。信頼度しきい値は確実性の軸であり、本番システムは両方でゲートします。
Choice に「unknown」選択肢を足せばいいのでは?
3 つの理由で推奨しません。選択肢分布が汚染されます——すべての実際の回答がキャッチオールの選択肢と競合し、選択肢ごとの確率が同じ意味を持たなくなります。シグナルの帰属が不能になります——後から「証拠の欠落」と「criteria の書き方の問題」を区別できません。そしてレバーがありません——統合された選択肢は過剰棄答と過剰確信を別々に扱えませんが、独立した Noul なら各誤り方向に実測率と専用のゲートを与えられます。
信頼度しきい値だけで足りるのでは?
足りません——信頼度と回答可能性は違う仕方で失敗します。信頼度は較正されています。0.62 とは、モデルが証拠を見た上で 62% の確信があるという意味です。棄答が担うのは、どんなに確信が高くても根拠にならないケースです。出荷日は state にない、解約理由は記録されていない、更新はまだ起きていない。このページが引用する監査と評価はどちらも、モデルがまさにそうした state で高信頼度を維持することを示しています。だからこそゲートは最初に is_answerable を読み、次に信頼度を読みます。
棄答は Jev にネイティブに組み込まれていますか?
Jev はプリミティブを提供します——型付き回答、較正済み信頼度、Noul 質問タイプ、1 パスの並列質問——そして棄答レーンはその上の 5 行のアプリケーションコードです。判断の隣に is_answerable を問い、ゲートし、missing_information でルーティングする。切り替える「IDK モード」は存在せず、それは双方向に誠実です。何もあなたの背後で勝手に棄答せず、システムが行うすべての棄答は、あなたがルールを書きテストできるものです。
回答可能性のしきい値はどう決めますか?
信頼度しきい値と同じ方法です。実測した誤りコストから決めます。回答可能な state と明確に回答不能な state の両方を含むラベル付きセットを作り、is_answerable ゲートを掃引し、2 つの曲線を読みます——誤棄答(人の作業の浪費)とハルシネーション(高価な方向)。2 つのコスト曲線の交点がゲートです。スキーマ変更後には掃引をやり直してください。測定方法論は較正ガイドが示しています。
判断モデルは実際のところ、上手に棄答できるのですか?
モデルによって大きく異なりますし、測定可能です。このページが引用する 5 モデル評価(2026 年 10 月、18 メッセージ、探索的)では、ホスト型 Jev は回答不能 6 ケースすべてで棄答した一方、ローカル小型モデルはどれも少なくとも 1 つを推測で答えました——Clef Flash 9B の棄答は 6 件中 1 件。これは限界ページにある、より広範な過信エビデンスと整合します。ベンダーの数字を信頼する前に(私たちのものも含め)、自分のドメインから取った回答不能 state でテストしてください。