Jev Recipe / 弃答与诚实边界

用 Jev 做决策弃答:教会 AI 说「我不知道」

有些问题靠眼前的 state 根本答不出来,任何置信度阈值都救不了。把一个类型化 Choice 与 is_answerable Noul、missing_information Choice 配成一份契约,让模型返回一个结构化的「我不知道」——并明确指出缺的是什么,再让自动化同时门控在可答性与置信度两条轴上。

广告

构建弃答契约的 6 个步骤

01先给两种失败模式命名,再写任何闸门。「不确定」是校准问题:模型看到了证据但信心不足——在校准数据上调出来的置信度阈值就是解法。「不可答」是证据问题:state 里根本没有答案,任何阈值都无能为力——问模型订单 #9012 什么时候发货,只给它订单行、不给库存和承运商数据,它照样会编一个看起来很自信的日期。第三方审计已经抓到过决策模型对不可知答案报 30%+ 置信度的案例;本页上线前一天发布的一份五模型独立横评发现了同样的模式:每个本地小模型都至少硬答过一道任何 state 都撑不起来的题。弃答就是针对第二种失败模式的类型化答案:契约本身说出「这个问题在这里没有答案」。
02写弃答契约:答案保持干净,另加两个问题。直觉做法是在现有 Choice 里加一个「unknown」选项——不要这么做:它会污染选项分布(每个真实答案从此都要和一个大杂烩竞争),信号无法归因(事后分不清「缺证据」还是「criteria 写得差」),而且没有独立可调的杠杆。正确做法是答案 Choice 只保留真实结果,另加 is_answerable 这个 Noul(「state 是否包含选出唯一选项所需的全部信息——不是多给点数据,不是猜」),再加 missing_information 这个 Choice,枚举会缺席的信息类型:日期、金额、身份、政策规则、外部系统状态。一次 evaluate 调用返回全部三个:它会给的答案、state 到底撑不撑得起回答、以及撑不起时该去补什么。
03先门控可答性,再门控置信度。这两个数字回答的是不同的问题,永远不要合并成一个阈值:置信度低于校准线转人工复核——那是校准决策;is_answerable 低于它的线触发弃答——这是证据决策。危险的象限是高置信度配低可答性——没有承运商数据的 state 给出的 0.88 发货日期,因为只看置信度的闸门恰恰会自动执行这一行。只要 is_answerable 跌破闸门就弃答,无论答案看起来多好,并返回一个类型化对象:abstained 为 true、missing_information 键、可答性读数。规则只有五行代码,却是「我不知道」和「带置信度分数的幻觉」之间的区别。
04让弃答可执行。走进死胡同的弃答只是一句客气的报错。因为契约指明了缺失的是什么,弃答就能派发工作:缺日期把流程引向订单系统,缺政策规则引向合规 wiki,缺身份信息引向一封澄清邮件——这和置信度门控降级链处理不确定答案用的是同一个「精确补这个」模式,只是矛头从确定性转向了证据。实践中团队把 missing_information 当路由键用:做哈希、做计数,弃答率里占大头的那几类就是你的数据模型 TODO 清单——按阻塞自动化的频次排好序。
05用不可答的 state 测试弃答车道。构建一组你的 state 明确无法回答的问题——未来结果、你从不收集的字段、别的系统里的实时值——和常规标注集放在一起,分别测量两个错误方向:对可答的问题弃答(浪费人力)与对不可答的问题作答(产生幻觉),两者互为代价。本页引用的独立横评把 18 条客户消息跑过五个决策模型:五个在十二条明确请求上几乎全对,弃答才是全部差距所在——托管 Jev 对全部六条不可答案例都弃了答,本地小模型则大多硬答了下去(Clef Flash 9B 六条只弃了一条)。可答性阈值的调法和置信度阈值一模一样:用你自己 state 上的实测错误成本,不靠感觉。
06把弃答当一等公民决策来记录。当「我不知道」改变了接下来发生的事——一个人领到任务、一封邮件发给客户、一张工单进队列——它就是一个决策,理应带着和其他决策相同的字段进审计追踪:契约版本、可答性与置信度读数、车道、missing_information 键。弃答记录特别经得起时间:下个季度缺失的数据补进 state 之后,把本季度的弃答记录在新契约下重放,就能精确看到这次数据补全修好了哪些案例。而 schema 变更后弃答率上升不是回归——是契约在告诉你:新问题正在触达撑不起它们的 state。
schema / 答案·可答性·缺失信息契约
{
  "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": "另一个系统里的实时数据"
    }
  }
}
把一个 state 里没有答案的问题发过去,观察 is_answerable 走低而置信度依旧居高不下。

六个问题,一道可答性闸门

下面每个问题都已经带着弃答契约跑过 evaluate 端点:类型化答案、校准置信度,以及一个 is_answerable Noul。拖动可答性闸门,看车道实时重算。危险的行恰恰是自信的那些:订单 #9012 对一个 state 里根本不可能包含的日期给出了 0.88 的置信度——置信度衡量的是「有多确定」,不是「答案是否存在于 state 中」。

正常作答 · 3弃答 · 31 · 仅看置信度就会自动执行的回答
问题与 state答案与置信度is_answerable车道

订单 #5521 符合 v3 版退款政策吗?

q_101 · 订单行 + 退货窗口日期 + 政策事件

符合0.960.98正常作答

该工单归哪个团队:账务 / 技术 / 销售?

q_102 · 主题清晰的完整工单文本

账务0.970.95正常作答

客户在这笔订单前改过地址吗?

q_103 · 带两个时间戳地址的客户档案

true0.940.91正常作答

订单 #9012 什么时候发货?

q_104 · 仅订单行——无库存、无承运商 SLA

2026-10-140.880.31弃答仅看置信度就会自动执行的回答

缺失:库存状态 + 承运商 SLA 表

客户 #4183 为什么流失?

q_105 · 仅使用度指标——无调研、无沟通记录

价格敏感0.710.22弃答

缺失:任何客户直接陈述——指标只说明发生了什么,不说明为什么

合同 #77 下季度会续约吗?

q_106 · 使用趋势——续约结果属于未来信息

true0.660.41弃答

缺失:结果尚不存在——存在的只有意向信号

仅演示数据——六条模拟问题。车道按与文案相同的可答性规则实时重算。

弃答行为:类型化契约 vs 提示词「说不知道」 vs 仅置信度门控

METRIC
Jev
提示词求它说不知道 / 仅置信度门控
「我不知道」住在哪里
契约里的一个 Noul——is_answerable 每次调用都返回概率,你门不门控它都在
一句模型可能说也可能不说的话;你解析散文,祈祷拒绝出现
弃答带回了什么
缺失的东西,而且是类型化的:missing_information 指明该去补哪个日期、金额、身份或政策规则
一个没有结构的拒绝——后续问题全靠人从措辞里猜
能不能调
可答性阈值和其他闸门一样:误弃答率与幻觉率都能在标注集上实测
改提示词(「只在……的时候才说不知道」)让拒绝行为漂移不定,下一批数据接着翻车
自信的幻觉
哪怕置信度很高也会被可答性轴拦下——两个读数各自独立门控
无药可救:流利的错误答案和流利的正确答案在没有第二信号时长得一模一样
弃答之后发生什么
missing_information 是路由键——工作流精确补齐那一块,弃答本身作为决策入日志
聊天日志里一条走进死胡同的拒绝:没有可路由的字段,没有可调的阈值,没有可重放的东西
独立证据
五模型独立横评(2026 年 10 月,n=18 探索性)总分 18/18、六条不可答案例全部弃答;RLCD 校准置信度 + 公开 ECE 方法论
LLM 弃答双向失准——AbstentionBench 的存在本身就是因为:靠提示词求出来的拒绝并不跟随可答性

生产代码:TypeScript 与 Python 双语言弃答契约

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 日发货」——
# 流利、无法证伪、和有根据的答案无法区分。你门控的是散文,
# 而散文没有阈值。
python / jev:弃答契约与闸门
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,
    }
typescript / 基线:解析散文式拒绝
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") };
}
typescript / jev:类型化弃答对象
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 测试。

继续扩展可信决策工具箱