Jev Recipe / 弃答与诚实边界
用 Jev 做决策弃答:教会 AI 说「我不知道」
有些问题靠眼前的 state 根本答不出来,任何置信度阈值都救不了。把一个类型化 Choice 与 is_answerable Noul、missing_information Choice 配成一份契约,让模型返回一个结构化的「我不知道」——并明确指出缺的是什么,再让自动化同时门控在可答性与置信度两条轴上。
构建弃答契约的 6 个步骤
{
"answer": {
"type": "choice",
"instructions": "只使用 state 里已有的信息作答",
"criteria": {
"billing": "付款、发票或退款",
"technical": "产品故障",
"sales": "购买或升级咨询"
}
},
"is_answerable": {
"type": "noul",
"instructions": "state 是否包含选出唯一选项所需的全部信息——不是多给点数据,不是猜?"
},
"missing_information": {
"type": "choice",
"instructions": "若 is_answerable 为否:缺失的究竟是哪一样?",
"criteria": {
"none": "state 信息充分",
"a_date": "一个日期或截止时间",
"an_amount": "一个金额或余额",
"an_identity": "某个人或账户是谁",
"a_policy_rule": "适用哪条政策或哪个版本",
"external_state": "另一个系统里的实时数据"
}
}
}六个问题,一道可答性闸门
下面每个问题都已经带着弃答契约跑过 evaluate 端点:类型化答案、校准置信度,以及一个 is_answerable Noul。拖动可答性闸门,看车道实时重算。危险的行恰恰是自信的那些:订单 #9012 对一个 state 里根本不可能包含的日期给出了 0.88 的置信度——置信度衡量的是「有多确定」,不是「答案是否存在于 state 中」。
| 问题与 state | 答案与置信度 | is_answerable | 车道 |
|---|---|---|---|
订单 #5521 符合 v3 版退款政策吗? q_101 · 订单行 + 退货窗口日期 + 政策事件 | 符合0.96 | 0.98 | 正常作答 |
该工单归哪个团队:账务 / 技术 / 销售? q_102 · 主题清晰的完整工单文本 | 账务0.97 | 0.95 | 正常作答 |
客户在这笔订单前改过地址吗? q_103 · 带两个时间戳地址的客户档案 | 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 | 弃答 |
缺失:结果尚不存在——存在的只有意向信号 | |||
仅演示数据——六条模拟问题。车道按与文案相同的可答性规则实时重算。
弃答行为:类型化契约 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 是否包含选出唯一选项所需的全部信息——不是多给点数据,不是猜?",
},
"missing_information": {
"type": "choice",
"instructions": "若 is_answerable 为否:缺失的究竟是哪一样?",
"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 是否包含选出唯一选项所需的全部信息——不是多给点数据,不是猜?",
},
missing_information: {
type: "choice",
instructions: "若 is_answerable 为否:缺失的究竟是哪一样?",
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",
};
}
// 一次调用,三个类型化读数。弃答分支带着缺失键,
// 工作流可以精确去补那一样东西。决策模型弃答常见问题
什么是决策模型弃答(abstention)?
模型返回一个显式的、类型化的「我不知道」——并指明缺失的是什么——而不是在输入 state 不包含答案时硬猜。在本页构建的契约里,弃答是由 is_answerable Noul 加上你的阈值决定的一条车道,弃答对象携带 missing_information 键,工作流可以照着去精确补齐缺席的那块信息。它是决策质量的证据轴;置信度阈值是确定性轴,生产系统两条轴都门控。
为什么不直接在 Choice 里加一个「unknown」选项?
三个理由。选项分布会被污染——每个真实答案从此都要和大杂烩竞争,每个选项的概率不再具有原来的含义。信号无法归因——事后你分不清「模型缺证据」和「criteria 写得差」。没有可调的杠杆——合并进选项的阈值无法对过度弃答和过度自信区别对待,而一个独立的 Noul 让每个错误方向都有自己的实测率和自己的闸门。
置信度阈值不是已经覆盖了吗?
没有——置信度和可答性的失败方式不同。置信度是校准过的:0.62 的意思是模型看到了证据、有 62% 的把握。弃答覆盖的是任何把握都不该存在的场景:发货日期不在 state 里、流失原因从没被记录过、续约还没有发生。本页引用的审计和横评都表明,模型恰恰在这些 state 上保持高置信——所以闸门先读 is_answerable、再读置信度。
Jev 原生支持弃答吗?
Jev 提供原语——类型化答案、校准置信度、Noul 题型、单次前向并行多问——弃答车道是在这之上的五行应用代码:在决策问题旁边加上 is_answerable、设闸门、按 missing_information 路由。没有一个可以打开的「IDK 模式」,这对双方都诚实:没有东西会在你背后偷偷弃答,而你系统里的每一次弃答都是你写了规则、可以测试的弃答。
可答性阈值怎么定?
和定置信度阈值的方法一样:从实测错误成本出发。构建一个同时包含可答与明确不可答 state 的标注集,扫 is_answerable 闸门,读两条曲线——误弃答(浪费人力)和幻觉(贵的那个方向)。两条成本曲线的交点就是你的闸门。schema 变更后重跑一遍;校准指南里有完整的测量方法论。
决策模型的弃答能力实际表现如何?
模型之间差异巨大,而且可测量。本页引用的五模型横评(2026 年 10 月,18 条消息,探索性)发现托管 Jev 对全部六条不可答案例都弃了答,每个本地小模型都至少硬答了一条——Clef Flash 9B 六条只弃一条。这与诚实边界页收录的更广泛的过度自信证据一致。在信任任何厂商数字之前(包括我们的),先用你自己领域的不可答 state 测试。