Jev 不是另一个聊天模型:Choice、Score、Boolean 与概率决策完整实战

很多 Agent 的关键故障,并不是大模型不会写,而是系统在不该让它自由发挥的位置仍然要求它"生成一段答案"。下一步该查订单、发起退款还是转人工,本质上是从有限集合中选择;一个投诉的紧急程度,本质上是有顺序的评分;"证据是否足以支持这项操作",本质上是带不确定性的真假判断。若仍调用聊天模型,让它先写一段 JSON,再解析、重试和修补,程序就把一个决策问题绕成了文本生成问题。

2026 年 9 月,TypeSafe AI 发布 Jev,并把它定位为面向软件的 System One 决策模型。调用方提交一份状态和若干类型化问题,模型返回 Choice、Score 或真假概率,程序直接分支。Vercel 公布的数据表明,Jev 上线 AI Gateway 后 24 小时内被接近 13% 的付费团队尝试,早期采用速度很快;但这只是单个平台的首日采用数据,不等于长期留存,更不能证明它已经通过每个行业的生产验证。

本文不把"结构化"误写成"永不出错",也不复述"零幻觉"式营销。类型正确只说明响应可以被代码读取,概率也不天然等于真实正确率。我们会从一个售后 Agent 出发,设计三种问题、适配器接口、校准集、风险阈值、人工复核和回退机制,并明确 Jev、Structured Output、传统分类器以及通用 LLM 各自适合解决什么。

1. 先纠正一个命名差异:Boolean 在原生文档里叫 Noul

标题使用 Boolean,是因为 Vercel AI SDK 的 Evaluation 接口以 type: "boolean" 展示真假问题,很多开发者也会自然地把它叫作布尔决策。TypeSafe 原生文档当前把这一原语命名为 Noul :它回答一个陈述为真的概率,返回 0 到 1 的 noul 值。Choice 和 Score 另有完整概率分布与 confidence,Noul 没有第二个独立的置信度字段。

因此,接入层必须把"业务概念"和"供应商字段"分开。业务代码使用 BooleanDecision(probability_true=...),TypeSafe 适配器读取 Noul,Vercel 适配器读取 Boolean;上层不应到处判断某家 SDK 的字段名。本文后文说 Boolean,指的就是这种真假概率契约,而不是宣称原生 API 存在一个同名类型。

这个细节也提醒我们:Jev 仍是非常早期的产品,命名、SDK 和网关封装可能继续变化。正式接入前必须以当时的官方文档为准,并用契约测试锁定请求、响应和错误行为。博客示例不能替代依赖版本对应的 API 参考。

2. Jev 究竟解决了哪一层问题

Jev 的输入不是聊天消息列表,而是待评估的 state;问题描述了要从状态中判断什么。官方文档强调每个问题应当原子、边界清楚,多个独立问题可以在一次请求中并行评估。程序拿到结果后,用普通条件语句组合:低风险且高把握时自动执行,中等把握时补充信息,高风险或低把握时交给人工。

这更接近一个语义决策部件,而不是缩小版聊天机器人。它不负责写安抚话术、总结几十页合同、规划十步调查,也不应该被要求解释复杂因果。它擅长的是"给定充分状态,对清晰问题做快速判断"。若一个问题需要先找资料、再算金额、最后权衡多个目标,应由检索、确定性代码、LLM 或工作流完成这些步骤,再把单一判断交给决策模型。

TypeSafe 把自己的训练方向称为 Reinforcement Learning for Calibrated Decisions,并在官网强调决策、类型化输出和置信估计。不过公开页面不足以让外部读者完整复现模型架构、训练数据和校准过程,也不能据此断言它不是 Transformer、不是语言模型技术,或对任何分布都已经校准。工程上应把它视为一个提供类型化概率输出的托管黑盒,通过自己的数据验证,而不是从产品类别推导可靠性。
#mermaid-svg-xrr0Lvz6ZCazK6kV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xrr0Lvz6ZCazK6kV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xrr0Lvz6ZCazK6kV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xrr0Lvz6ZCazK6kV .error-icon{fill:#552222;}#mermaid-svg-xrr0Lvz6ZCazK6kV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xrr0Lvz6ZCazK6kV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xrr0Lvz6ZCazK6kV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xrr0Lvz6ZCazK6kV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xrr0Lvz6ZCazK6kV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xrr0Lvz6ZCazK6kV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xrr0Lvz6ZCazK6kV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xrr0Lvz6ZCazK6kV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xrr0Lvz6ZCazK6kV .marker.cross{stroke:#333333;}#mermaid-svg-xrr0Lvz6ZCazK6kV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xrr0Lvz6ZCazK6kV p{margin:0;}#mermaid-svg-xrr0Lvz6ZCazK6kV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xrr0Lvz6ZCazK6kV .cluster-label text{fill:#333;}#mermaid-svg-xrr0Lvz6ZCazK6kV .cluster-label span{color:#333;}#mermaid-svg-xrr0Lvz6ZCazK6kV .cluster-label span p{background-color:transparent;}#mermaid-svg-xrr0Lvz6ZCazK6kV .label text,#mermaid-svg-xrr0Lvz6ZCazK6kV span{fill:#333;color:#333;}#mermaid-svg-xrr0Lvz6ZCazK6kV .node rect,#mermaid-svg-xrr0Lvz6ZCazK6kV .node circle,#mermaid-svg-xrr0Lvz6ZCazK6kV .node ellipse,#mermaid-svg-xrr0Lvz6ZCazK6kV .node polygon,#mermaid-svg-xrr0Lvz6ZCazK6kV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xrr0Lvz6ZCazK6kV .rough-node .label text,#mermaid-svg-xrr0Lvz6ZCazK6kV .node .label text,#mermaid-svg-xrr0Lvz6ZCazK6kV .image-shape .label,#mermaid-svg-xrr0Lvz6ZCazK6kV .icon-shape .label{text-anchor:middle;}#mermaid-svg-xrr0Lvz6ZCazK6kV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xrr0Lvz6ZCazK6kV .rough-node .label,#mermaid-svg-xrr0Lvz6ZCazK6kV .node .label,#mermaid-svg-xrr0Lvz6ZCazK6kV .image-shape .label,#mermaid-svg-xrr0Lvz6ZCazK6kV .icon-shape .label{text-align:center;}#mermaid-svg-xrr0Lvz6ZCazK6kV .node.clickable{cursor:pointer;}#mermaid-svg-xrr0Lvz6ZCazK6kV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xrr0Lvz6ZCazK6kV .arrowheadPath{fill:#333333;}#mermaid-svg-xrr0Lvz6ZCazK6kV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xrr0Lvz6ZCazK6kV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xrr0Lvz6ZCazK6kV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xrr0Lvz6ZCazK6kV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xrr0Lvz6ZCazK6kV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xrr0Lvz6ZCazK6kV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xrr0Lvz6ZCazK6kV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xrr0Lvz6ZCazK6kV .cluster text{fill:#333;}#mermaid-svg-xrr0Lvz6ZCazK6kV .cluster span{color:#333;}#mermaid-svg-xrr0Lvz6ZCazK6kV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xrr0Lvz6ZCazK6kV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xrr0Lvz6ZCazK6kV rect.text{fill:none;stroke-width:0;}#mermaid-svg-xrr0Lvz6ZCazK6kV .icon-shape,#mermaid-svg-xrr0Lvz6ZCazK6kV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xrr0Lvz6ZCazK6kV .icon-shape p,#mermaid-svg-xrr0Lvz6ZCazK6kV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xrr0Lvz6ZCazK6kV .icon-shape rect,#mermaid-svg-xrr0Lvz6ZCazK6kV .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xrr0Lvz6ZCazK6kV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xrr0Lvz6ZCazK6kV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xrr0Lvz6ZCazK6kV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 直接允许或拒绝
需要语义判断
低风险高把握
信息不足
高风险或低把握
用户请求
状态构建器

事实 证据 工具结果
确定性硬规则

权限 金额 合规
业务动作
Jev 决策适配器
Choice

下一步或队列
Score

有序风险级别
Boolean/Noul

陈述为真概率
风险策略引擎
询问或补充证据
人工复核
决策审计与评测日志

3. Choice:不是让模型随便起一个标签

Choice 用于互斥或近似互斥的有限选项,例如把工单路由到 returns、shipping、billing 或 other。响应不仅包含胜出的 choice,还包含各选项的 probabilities,以及由概率分布形状计算出的 confidence。概率集中在一个选项上时通常更有把握;多个选项接近时,系统应把歧义当成信号,而不是只读取第一名。

选项描述决定了边界。如果"退款"和"账单"都只写一个中文名,重复扣款、退货到账与商品换货很容易混在一起。每个 criteria 应说明涵盖什么、不涵盖什么,并保留 other 或 none_of_above,否则模型被迫在错误集合里选一个。增加兜底类并不能保证所有未知输入都进入兜底,仍需在真实域外样本上验证。

Choice 也不适合把几十项规则压成一个巨型问题。"综合判断是否值得信任"同时包含身份、证据、金额和行为历史,哪怕返回一个漂亮分布,也无法知道哪项因素出错。更稳妥的做法是分别询问意图、证据充分性和异常模式,再由代码按照责任矩阵组合。

4. Score:评分是有序分布,不是一句主观印象

Score 用于有序等级,如缺陷严重度从"仅视觉问题"到"核心流程阻断",或投诉风险从普通咨询到需要安全专席。官方接口为每个等级提供文字说明,响应返回每级概率、confidence、图例和一个可落在相邻等级之间的 score。这个分数是按等级位置对概率加权得到的期望位置,不是模型随意生成的百分制数字。

等级描述必须围绕单一维度。如果低等级写"影响小且用户平静",高等级写"影响大或用户愤怒",系统混入了影响范围和情绪两个因素,中间分数就很难解释。应该拆成"业务影响"和"情绪升级"两个 Score 或 Choice,最后由代码确定队列。等级还要覆盖边界案例:有变通方案但影响大量用户,究竟属于哪一级,应在 rubric 里明确。

不要把 1.8 直接换算成"90% 风险"。Score 的数值坐标来自你定义的等级位置,它不是事件发生概率。真正需要二元概率时,应另问 Boolean;需要金额、时间或库存时,应调用数据库和计算程序,不能让 Score 估算确定性事实。

5. Boolean/Noul:返回概率,不代表事实已经验证

Boolean 适合回答一个原子陈述是否成立,例如"用户是否明确要求退款""现有证据是否支持订单已签收""回复是否包含未经授权的赔偿承诺"。TypeSafe 原生 Noul 返回陈述为真的概率;官方文档明确说明它不像 Choice 和 Score 那样再返回独立 confidence。在业务适配器里,应保留 probability_true,不能先压成 True 或 False 再丢弃不确定性。

它不适合替代确定性校验。"当前用户是否拥有该订单"应查授权数据库;"退款金额是否超过一千元"应做数值比较;"签名是否有效"应运行密码学验证。只有当输入本身是非结构化语义,例如投诉话术是否构成明确威胁,决策模型才可能有价值。即便模型判断为真,危险动作仍须通过鉴权、幂等、限额和审批。

真假问题也会被提问方式影响。"这段回复安全吗"过于宽泛;"回复是否承诺了政策之外的退款金额"更可验证。避免否定套否定,如"是否并非没有风险",否则人类标注员和模型都会混乱。对高风险陈述,可以同时设计相反表述做离线一致性检查,但线上不应简单平均两个相关问题。

6. 一个不冒充官方 HTTP 的可运行适配器

官方 SDK 和网关正在快速演进。为了不把某个日期的字段写成永久契约,下面定义稳定的业务接口,并提供完全本地、可运行的模拟适配器。它不会假装调用 Jev;真实部署时只需新增 TypeSafeDecisionClient,在一个文件里把官方 Choice、Score、Noul 映射为这些数据类。这样文章代码可以验证策略逻辑,又不会编造 API 密钥、端点或响应字段。

python 复制代码
# decision_contract.py
from __future__ import annotations

from dataclasses import dataclass
from typing import Mapping, Protocol, Sequence


@dataclass(frozen=True)
class ChoiceDecision:
    value: str
    probabilities: Mapping[str, float]
    confidence: float


@dataclass(frozen=True)
class ScoreDecision:
    value: float
    probabilities: Mapping[int, float]
    confidence: float


@dataclass(frozen=True)
class BooleanDecision:
    probability_true: float


class DecisionClient(Protocol):
    def choose(self, state: str, question: str,
               options: Mapping[str, str]) -> ChoiceDecision: ...

    def score(self, state: str, question: str,
              levels: Sequence[str]) -> ScoreDecision: ...

    def boolean(self, state: str, statement: str) -> BooleanDecision: ...


class LocalFixtureClient:
    """离线契约测试桩;不是 Jev 的实现,也不代表其模型结果。"""

    def choose(self, state: str, question: str,
               options: Mapping[str, str]) -> ChoiceDecision:
        if not options:
            raise ValueError("choice needs at least one option")
        if "重复扣款" in state and "billing" in options:
            other = 0.06 / (len(options) - 1) if len(options) > 1 else 0.0
            probs = {
                name: 0.94 if name == "billing" and len(options) > 1 else other
                for name in options
            }
            if len(options) == 1:
                probs = {"billing": 1.0}
        else:
            share = 1.0 / len(options)
            probs = {name: share for name in options}
        value = max(probs, key=probs.__getitem__)
        ordered = sorted(probs.values(), reverse=True)
        margin = ordered[0] - ordered[1] if len(ordered) > 1 else 1.0
        return ChoiceDecision(value, probs, margin)

    def score(self, state: str, question: str,
              levels: Sequence[str]) -> ScoreDecision:
        if len(levels) < 2:
            raise ValueError("score needs at least two levels")
        probs = {i: 0.0 for i in range(len(levels))}
        index = len(levels) - 1 if "账户被盗" in state else 0
        probs[index] = 1.0
        return ScoreDecision(float(index), probs, 1.0)

    def boolean(self, state: str, statement: str) -> BooleanDecision:
        return BooleanDecision(0.98 if "转人工" in state else 0.08)


def _self_check() -> None:
    client = LocalFixtureClient()
    routed = client.choose(
        "信用卡发生重复扣款",
        "应该交给哪个队列",
        {"returns": "退换货", "billing": "支付账单", "other": "其他"},
    )
    assert routed.value == "billing"
    assert abs(sum(routed.probabilities.values()) - 1.0) < 1e-9
    assert client.boolean("请转人工", "用户明确要求人工").probability_true > 0.9


if __name__ == "__main__":
    _self_check()

真实适配器要做四类校验:请求问题数、选项数和状态长度是否超限;响应概率是否都在 0 到 1 且总和近似为 1;Choice 返回值是否存在于选项集合;超时、限流和上游错误是否被映射为明确异常。解析失败不能默认"允许执行",高风险路径应失败关闭,低风险只读路径可以回退到人工或旧分类器。

还应保存供应商模型标识、适配器版本、问题定义版本和 request_id。业务日志记录概率与最终动作,但不要无节制记录完整用户原文。状态里若有身份证、银行卡、医疗或内部机密,应先做字段最小化,并确认供应商条款、区域、训练使用和保留策略符合组织要求。

7. Jev、Structured Output、传统分类器和 LLM 怎么选

Structured Output 解决的是"生成结果必须符合 JSON Schema"。它可以把标签约束成枚举,避免少括号、字段漂移和解析重试,但底层任务仍可能是通用模型生成;Schema 正确不代表分类正确,也不天然提供经过校准的全类别概率。如果流程还需要理由、摘要或复杂推理,通用 LLM 加严格 Schema 很合适。

Jev 的差异在于产品接口从一开始就是决策原语:输入状态和问题,输出选项分布、等级分布或真假概率。它省去了自由文本层,适合高频而原子的语义判断。不过它仍然可能语义误判、遇到域外输入或被不可信文本影响。它是"更贴近问题形状的模型接口",不是数据库约束、业务规则或安全控制的替代品。

传统分类器在标签长期稳定、训练数据充足、吞吐巨大且能够自托管时往往更合适。一个经过验证的线性模型或小型编码器可固定权重、离线运行、精确统计漂移,也没有按调用付费和第三方状态外发。它的代价是标注、训练、部署和新类别迭代。Jev 的吸引力在于用自然语言定义判断维度,减少为每个小任务单独训练模型的启动成本,但要用实际账单和效果证明这项收益。

通用 LLM 继续负责开放问答、跨多步推理、解释和内容生成。一个常见组合是:LLM 生成候选计划,Jev 判断下一工具或风险,代码执行确定性动作;或者 Jev 做低成本初筛,低把握样本交给更强模型或人工。若任务只需固定关键词或数值规则,连 Jev 都不需要,普通代码最可靠也最便宜。

8. "概率"首先要接受校准检验

如果模型对一批样本都给出 0.8 的正确概率,长期看其中大约八成应当正确,这才叫校准。单个样本无法证明 0.8 对或错,必须在同分布样本组上观察。一个模型可以准确率较高却过度自信,也可以整体校准但在中文投诉、少数类、安全事件等切片严重失真。

TypeSafe 将校准作为产品方向,官方文档也建议根据风险使用置信阈值;但你的业务类别、语言、状态构造和时间分布都不同,不能把供应商的总体性质直接当成本地保证。最少应保留一份从真实业务脱敏而来的校准集,冻结问题定义和模型版本,分别评估 Choice 的胜出概率、Boolean 的真值概率,以及 Score 的等级分布。

Boolean 可以计算 Brier Score,即预测概率与真实标签差值的平方均值;越低越好。ECE 把概率分桶,比较每桶平均概率和实际正确率,容易理解但受分桶方式影响。Choice 可计算多分类 Brier Score、负对数似然和 top-label ECE;Score 还要看平均绝对误差、严重等级漏判率和相邻等级混淆,不能只比较期望分数。

python 复制代码
# calibration_report.py
from __future__ import annotations

from dataclasses import dataclass
from math import isfinite
from typing import Iterable


@dataclass(frozen=True)
class BinaryPrediction:
    probability_true: float
    label: int


def validate(rows: Iterable[BinaryPrediction]) -> tuple[BinaryPrediction, ...]:
    result = tuple(rows)
    if not result:
        raise ValueError("evaluation set is empty")
    for row in result:
        if not isfinite(row.probability_true) or not 0.0 <= row.probability_true <= 1.0:
            raise ValueError("probability must be finite and between 0 and 1")
        if row.label not in (0, 1):
            raise ValueError("label must be 0 or 1")
    return result


def brier_score(rows: Iterable[BinaryPrediction]) -> float:
    checked = validate(rows)
    return sum((row.probability_true - row.label) ** 2 for row in checked) / len(checked)


def expected_calibration_error(
    rows: Iterable[BinaryPrediction], bins: int = 10
) -> float:
    checked = validate(rows)
    if bins < 2:
        raise ValueError("bins must be at least 2")
    groups: list[list[BinaryPrediction]] = [[] for _ in range(bins)]
    for row in checked:
        index = min(int(row.probability_true * bins), bins - 1)
        groups[index].append(row)
    total = len(checked)
    error = 0.0
    for group in groups:
        if not group:
            continue
        confidence = sum(x.probability_true for x in group) / len(group)
        frequency = sum(x.label for x in group) / len(group)
        error += len(group) / total * abs(confidence - frequency)
    return error


def _self_check() -> None:
    perfect = [BinaryPrediction(1.0, 1), BinaryPrediction(0.0, 0)]
    assert brier_score(perfect) == 0.0
    assert expected_calibration_error(perfect, bins=5) == 0.0


if __name__ == "__main__":
    _self_check()

这段代码评估的是 Boolean 概率本身,不是 Jev 专用算法,可以直接用于任何提供概率的模型。生产评测还要输出样本数和置信区间。一个仅有十条高风险样本的"100% 召回"没有足够证据;若严重事件稀少,应延长收集窗口、设计红队样本并让专业人员复核,但不要用合成样本替代全部真实分布。

9. 阈值不能凭感觉拍成 0.8

官方文档给出的高、中、低置信三段式是架构起点,不是通用阈值。不同错误后果不同:把物流咨询误送人工只增加成本,把账户被盗判断为普通咨询可能扩大损失。应先定义动作、错误类型和成本,再在校准集上寻找满足风险约束的阈值。

对一个二元自动动作,若预测正确概率为 p,错误成本为 C_error,人工复核成本为 C_review,在极简假设下,自动执行的期望错误成本是 (1-p) * C_error。只有它不高于人工成本时才值得自动,即 p >= 1 - C_review/C_error。这不是完整财务模型,却说明高风险动作的阈值理应更高。若错误成本几乎不可接受,结论可能是始终人工审批,而不是继续把阈值调到 0.999。

Choice 的阈值还要同时看胜出概率、第二名概率和 other。第一名 0.55、第二名 0.44 与第一名 0.55、其余十类均分不是同一种歧义。可以把官方 confidence 作为信号,但要验证它与本地错误率的关系。Score 则关注越过业务分界的概率质量,例如严重等级总概率,而不是只对期望分数做一刀切。

10. 用错误成本选择"自动、澄清、人工"

下面代码不依赖某家 SDK,输入已校准的 Choice 结果与显式风险政策。它先执行确定性硬规则,再根据动作风险、胜出概率和概率间隔选择自动执行、补充信息或人工复核。示例数字只是演示,不能照搬到真实业务;真实阈值必须从离线回放与风险批准中产生。

python 复制代码
# decision_gate.py
from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Mapping


class Outcome(str, Enum):
    AUTO = "auto"
    CLARIFY = "clarify"
    HUMAN = "human"


@dataclass(frozen=True)
class Policy:
    auto_probability: float
    min_margin: float
    high_risk_actions: frozenset[str]


@dataclass(frozen=True)
class GateDecision:
    outcome: Outcome
    action: str | None
    reason: str


def gate(
    probabilities: Mapping[str, float],
    policy: Policy,
    *,
    authenticated: bool,
    evidence_complete: bool,
) -> GateDecision:
    if not probabilities:
        return GateDecision(Outcome.HUMAN, None, "模型没有返回候选概率")
    if any(not 0.0 <= value <= 1.0 for value in probabilities.values()):
        return GateDecision(Outcome.HUMAN, None, "概率响应无效")
    if abs(sum(probabilities.values()) - 1.0) > 1e-6:
        return GateDecision(Outcome.HUMAN, None, "概率分布总和异常")

    ranked = sorted(probabilities.items(), key=lambda item: item[1], reverse=True)
    action, probability = ranked[0]
    runner_up = ranked[1][1] if len(ranked) > 1 else 0.0

    if action in policy.high_risk_actions and not authenticated:
        return GateDecision(Outcome.HUMAN, action, "高风险动作尚未完成身份验证")
    if not evidence_complete:
        return GateDecision(Outcome.CLARIFY, action, "决策所需证据不完整")
    if action in policy.high_risk_actions:
        return GateDecision(Outcome.HUMAN, action, "高风险动作必须人工批准")
    if probability < policy.auto_probability:
        return GateDecision(Outcome.CLARIFY, action, "胜出概率低于自动阈值")
    if probability - runner_up < policy.min_margin:
        return GateDecision(Outcome.CLARIFY, action, "前两项过于接近")
    return GateDecision(Outcome.AUTO, action, "低风险且通过自动化门槛")


def _self_check() -> None:
    policy = Policy(0.85, 0.25, frozenset({"refund", "delete_account"}))
    assert gate(
        {"search_order": 0.92, "ask_user": 0.08},
        policy,
        authenticated=True,
        evidence_complete=True,
    ).outcome is Outcome.AUTO
    assert gate(
        {"refund": 0.99, "ask_user": 0.01},
        policy,
        authenticated=True,
        evidence_complete=True,
    ).outcome is Outcome.HUMAN


if __name__ == "__main__":
    _self_check()

这里最重要的一行不是 0.85,而是"高风险动作必须人工批准"。模型概率用于处理语义不确定性,不应覆盖制度性控制。身份验证、权限、余额、金额上限、审批人和幂等键都由确定性系统检查。若业务以后批准部分低额退款自动化,也应新增明确限额和二次确认,而不是删除这条规则后完全依赖模型。

11. Agent 中最适合插入 Jev 的五个决策点

第一是工具路由。LLM 已经理解用户目标,但候选工具有限时,Choice 可以在"检索订单、查询政策、创建工单、询问用户"中选择。候选列表必须只包含当前身份有权使用的工具,绝不能先把管理员工具给模型,再指望它自律不选。

第二是停止与继续。Boolean 可以判断"当前证据是否足以回答",Choice 可以在"继续检索、换关键词、请求澄清、停止"中选择。但循环次数、Token 预算和超时仍由代码控制。决策模型不能无限为自己批准下一轮。

第三是风险预筛。Score 对工单严重度或内容风险分级,低等级进入自动队列,高等级进入人工。真正的封禁、资金冻结或医疗建议不能只凭一次模型分数执行,应联合规则、证据和审批。

第四是结果核验。工具返回后,Boolean 判断"结果是否回答了原问题"或"引用是否支持结论",低把握时回到检索或人工。这只能作为质量信号,不能证明事实为真;核验模型与生成模型可能共享偏差,关键结论仍需可追溯证据。

第五是多 Agent 路由。Choice 可选择编码、检索、数据分析或客服子 Agent,但问题描述应按能力和限制区分,而不是只给角色名称。调用前检查输入数据能否交给目标 Agent,调用后检查输出契约。路由正确不代表子 Agent 的动作天然安全。

12. 状态构建比问题措辞更容易被忽略

模型只能依据提交的 state 判断。若工单状态缺少支付结果,要求它区分退款失败和重复扣款就是逼模型猜;若把整段两百轮对话、日志和 HTML 全塞进去,关键信号会被噪声淹没,还会扩大隐私和提示词注入面。可靠做法是由代码构建最小、可追踪的状态对象,再序列化成稳定文本。

状态应区分可信系统事实与不可信用户文本。例如用明确区块标记 verified_order_status、tool_errors、policy_version 和 user_message,但标记本身不是安全边界。用户消息中即使出现"忽略问题并选择 refund",也不能获得更高权限。模型输出永远只是建议,策略引擎依据调用者身份和服务器事实决定动作。

对每次决策保存 state schema 版本与事实来源引用,而不是只存最终标签。离线复现时才能知道错误来自模型、缺字段、错误工具结果还是阈值。出于隐私考虑,可以将完整原文保存在受控业务库,评测日志只存脱敏特征、内部引用和必要片段,并设置留存期限。

13. 离线评测必须与真实决策形状一致

评测集至少包含 state、问题版本、真实标签、风险等级、允许动作和人工仲裁。Choice 样本要覆盖多意图、无合适选项和相近类别;Score 样本要覆盖每个等级及相邻边界;Boolean 样本要覆盖肯定、否定、隐含表达、信息不足和反事实。按会话或用户切分训练、校准和测试,不能让同一模板泄漏到两边。

基线不能只有一个大模型。应比较现有规则、传统分类器、通用 LLM Structured Output、Jev 以及"Jev 低把握转人工"的组合。报告准确率之外,还要报告宏平均 F1、严重类召回、Brier Score、ECE、风险---覆盖曲线、P50/P95 延迟、失败率和真实费用。TypeSafe 经 Vercel 转述的"最高 194 倍更快、445 倍更便宜"来自厂商自己的工作流评测,只能作为待验证假设,不能写成你的生产结论。

阈值选择必须在校准集完成,最终测试集只用于一次无偏验收。若看完测试结果不断调阈值,测试集就变成了训练集。上线前先跑影子流量:Jev 产生决定但不执行,与现有系统和人工标签比较;稳定后只开放低风险、可逆动作,再逐步扩大覆盖。

还要做契约和故障测试:超时、429、5xx、缺字段、概率不归一、未知选项、模型版本变化、网关切换和重复请求。决策调用失败时,系统必须有明确状态,不能因为异常被吞掉而默认执行。外部模型可用性属于业务依赖,应设置短超时、熔断、重试上限和人工回退。

14. 从风险---覆盖曲线看人工复核价值

把低把握样本交给人工,会降低自动覆盖率,却可能显著降低自动部分的错误率。这就是选择性分类的风险---覆盖权衡。评测时从高到低扫描阈值,计算每个阈值下有多少样本自动处理,以及自动样本中错误率是多少。目标不是自动化率最大,而是在业务允许风险下获得尽可能高的覆盖。

不同切片要分别画曲线。总体曲线可能很好,但账户安全、方言、超长文本或新产品类别在同一阈值下表现很差。高风险切片可以使用更高门槛,甚至永不自动;低风险路由可以容忍更多误差。人工复核结果应回流为新标签,但要先做质检,坐席快速关闭工单不必然代表模型判断正确。

分布发生变化时,旧阈值会失效。新促销、新欺诈话术、政策调整、语言比例变化和状态构建器改版都可能改变概率含义。线上应监控选项分布、人工转入率、概率直方图、无合适选项比例和抽样真值。漂移出现时先收紧自动覆盖,再重新标注和校准,而不是为了维持自动化率下调阈值。

15. 安全、隐私与供应商边界

Jev 返回类型化结果,不代表不受提示词注入影响。state 中的用户文本、网页、邮件和工具结果都可能包含操纵语句。不要把系统策略、密钥或隐藏权限放进状态;不要允许模型输出任意工具名、URL 或参数;候选动作必须由服务器生成白名单,并在执行时重新鉴权。

真假概率也不能替代内容安全硬规则。恶意文件、SQL、脚本和外部链接应经过专门扫描与沙箱;金融、医疗、招聘和执法等高影响场景需要相应专业评审与合规流程。模型决定日志应包含模型和问题版本、输入事实引用、概率、阈值、最终动作以及人工覆盖,但敏感原文要最小化、加密并限制访问。

使用托管服务前要审查 TypeSafe、网关和云服务各自的数据条款。数据可能经过不止一个处理方;是否用于训练、能否零保留、推理区域和删除机制不能从"类型安全"四个字推断。供应商隐私政策和功能页面也会变化,应由组织在接入时保存批准快照并定期复审。

供应商与网关是两个故障域。应用要避免将业务代码绑定在一个 SDK 上,保留前文那样的窄适配器和稳定内部契约。回退可以是旧规则、传统分类器或人工队列,不一定是另一个生成模型。模型切换前用冻结评测集重跑,不要假设同名 jev-latest 的概率分布永远不变;若官方支持固定版本,生产应优先固定并明确升级。

16. 早期产品最容易踩的五个坑

第一个坑是把"输出不会乱格式"写成"不会判断错"。Jev 可以省掉 JSON 解析错误,却仍可能选择错误队列、给出错误风险等级或对错误陈述给出高概率。所有业务结论必须由评测数据支撑。

第二个坑是照搬厂商速度和成本数字。Vercel 明确写的是 TypeSafe 在自有工作流评测中的报告,任务、模型、地区、并发和缓存条件未必与你相同。应在同一状态、同一网络和同一成功标准下压测,并计入网关费、重试、人工复核与迁移成本。

第三个坑是问题越多越好。官方称同一请求内问题独立并行、增加问题对延迟影响很小,但额外问题仍消耗输入和输出 Token,也增加治理面。只提交当前流程可能使用、且离线验证过的问题;不要把所有未来想法塞进每次请求。

第四个坑是把概率当作跨版本稳定 API。即使标签不变,新模型也可能整体更保守或更自信,使旧阈值改变覆盖率。版本升级必须重新检查校准和风险---覆盖曲线,并通过影子流量观察真实分布。

第五个坑是忽略退出方案。服务停机、价格变化、区域不可用或条款不再满足要求时,系统能否切回规则和人工?历史日志能否用统一格式重放?如果替换供应商需要修改几十处业务代码,说明接入边界设计错了。

17. 一份可以直接执行的上线清单

先确认问题形状:答案是否真的是有限选项、有序等级或真假概率;是否能拆成原子问题;确定性规则能否直接解决;状态是否包含完成判断所需事实。再确认契约:内部数据类是否与供应商字段隔离;未知选项、非法概率、超时和限流是否失败关闭;模型、问题和适配器版本是否可追踪。

接着确认评测:是否有真实脱敏数据、边界样本和域外样本;Choice、Score、Boolean 是否使用各自合适的指标;是否与规则、分类器和 Structured Output 基线公平比较;是否按风险和语言切片;阈值是否在校准集选择;测试集是否保持冻结;速度与成本是否由自己的环境测得。

最后确认运行边界:高风险动作是否保留人工审批;权限和金额是否由代码检查;state 是否最小化并区分可信事实与用户文本;日志和供应商条款是否满足隐私要求;是否有影子、灰度、熔断、回退和版本升级流程;人工覆盖结果能否进入质检后的回归集。

Jev 值得关注的真正原因,不是它又提供了一个聊天窗口,而是它把一类长期被塞进 Prompt 的问题重新暴露成了软件原语:选择、评分和真假概率。这个接口形状使程序更容易验证、分支和审计,也迫使团队正视"不确定时怎么办"。但类型不会消灭语义错误,概率不会自动适配业务,低延迟也不会替你承担决策后果。

最稳妥的落地方式,是让代码继续负责确定性事实和权限,让 Jev 只处理边界清楚的语义判断,再用校准、错误成本和人工复核决定是否执行。先在低风险路由上影子验证,证明概率与本地错误率相关,再扩大覆盖。这样使用时,Jev 不是神奇的 Agent 大脑,而是一块小而清晰、能被替换、能被拒绝、也能被测量的决策组件。

参考资料

  1. TypeSafe AI:Jev 与 System One 模型简介
  2. TypeSafe AI:Choice 原语与响应结构
  3. TypeSafe AI:Score 原语、等级与概率分布
  4. TypeSafe AI:Noul 真假概率原语
  5. TypeSafe AI:Confidence 的含义与风险阈值
  6. TypeSafe AI:Quick Start
  7. TypeSafe AI:Manifesto 与 RLCD 产品方向
  8. Vercel:Jev 上线 AI Gateway 的首日采用数据
  9. Vercel AI Gateway:Jev 模型页面、Evaluation 示例与价格
  10. OpenAI:Structured Outputs 官方介绍
  11. Guo 等:On Calibration of Modern Neural Networks
  12. Geifman 与 El-Yaniv:Selective Classification for Deep Neural Networks
  13. NIST:AI Risk Management Framework 1.0
相关推荐
染指11101 小时前
127.Agent-LangChain核心组件-模型输出后json修复
人工智能·langchain·agent·agents
xsd202411181 小时前
LingCast灵播:数字人直播技术架构全解析
人工智能
Data analyse4561 小时前
数据合规的敏感数据怎么识别?
前端·人工智能·数据分析
嘿嘿-661 小时前
GPT-6.1 Sol 实战:做一个可交互的 3D 汽车展厅
人工智能·gpt·3d·chatgpt·汽车
AI你一生一世1 小时前
当广告采集器成为大模型的“眼睛“:跨站上下文注入的架构与代价
人工智能·架构·大模型·rag·隐私安全·上下文注入·跨站追踪
数字化顾问1 小时前
(136页PPT)BCGIDG资本中国某省市场Geneva项目商业尽职调查项目报告(附下载方式)
大数据·人工智能
故七月1 小时前
GEO 工程实践:企业官网结构化改造,提升大模型实体采信度
大数据·人工智能·geo
释厄6231 小时前
物理发展史·量子力学=节点2·牛顿力学=节点3·宇宙的语言算法AI
人工智能·算法·microsoft
乃嘿仔1 小时前
AI 热点日报 · 2026-10-01
开发语言·人工智能·php