第 6 章

第 6 章:置信度:让概率可信

第 6 章:置信度:让概率可信

本文整理自 Datawhale 开源项目 datawhalechina/jev-cookbook(CC BY-NC-SA 4.0),源文件:main/02_核心概念/04_置信度.ipynb。

针对官方文档 置信度(Confidence) 章节的可运行实验笔记, 用真实的 TypeSafe API(Jev 模型)验证 confidence 字段的本质与「三路分流」实战模式。 全部实验使用中文场景与中文提示词。中文翻译版文档见 datawhalechina.github.io/jev-cookbook/confidence。

笔记本结构

章节 内容 实验
0. 准备 安装库、配置客户端、连通性测试 —
📖 理论速览 confidence 由 probabilities 推导 / 三路分流 / 阈值随风险 —
1. confidence 的本质 同 max_p 不同分布 → 不同 confidence 5 工单分类对比,4 选项 vs 2 选项
2. 三路分流实战 高/中/低置信度触发三类动作 5 工单意图路由 + 三路阈值演示
3. 风险调整阈值 同一 confidence 在不同 stakes 下走不同分支 语音银行:查余额 vs 批准转账
4. 措辞影响 同 state 不同 instructions → 不同 confidence 同一个工单 3 种问法
5. 分类层级 fallback 借鉴 cookbook:信心高报细类,信心低报粗类 客服工单:细类 vs 大类

每个原语按固定节奏展开:原理 → 理论根基 → 定义数据 → 定义问题 → 调用 → 解读结果, 每个单元格只做一件事,可直接顺着跑完(约 11 次真实 API 调用)。

运行要求

  • Python ≥ 3.10(官方 SDK 要求;macOS 系统自带 python3 是 3.9,装不上 SDK)
  • 一个 TypeSafe API Key(console.typesafe.ai/keys 获取)

推荐:一键创建本地环境(在本 notebooks 目录下)

./setup_env.sh                                  # 创建 .venv:Python 3.11+ + 全部依赖
export TYPESAFE_API_KEY=你的key
.venv/bin/jupyter lab 07_置信度.ipynb

或者手动创建:python3.12 -m venv .venv && .venv/bin/pip install -r requirements.txt

🔑 API Key 安全提示:本笔记从环境变量 TYPESAFE_API_KEY 读取密钥, 不要把 Key 硬编码进笔记本(尤其打算提交到公开仓库时)。

🈶 关于语言:实验全部使用中文 state 与中文提示词。三种原语的选项 key (如 billing、check_balance)属于代码标识符,保持英文以便代码分支判断; 它们的描述文字(criteria 值)均为中文,模型据此理解语义。

0. 准备

0.1 安装所需的库

如果已经用 ./setup_env.sh 创建过环境,本节通常显示“依赖已满足”;在其他环境里首次运行时会自动安装。

%pip install -q -U typesafe-sdk          # 本笔记本必需(要求 Python ≥ 3.10)
# %pip install -q -U jupyterlab         # 如本机还没有 Jupyter,取消注释运行一次
# %pip install -q -U nbformat nbclient  # 仅在需要重新生成/批量执行笔记本时安装
mavis-trash: moved to trash: '/var/folders/x6/mc_tghl5311czhlml7tv0y0r0000gn/T//.psub.ZD4MGv'
Note: you may need to restart the kernel to use updated packages.

0.2 导入库并创建客户端

  • Choice / Score:返回 confidence 字段的两种原语;
  • Noul:本身没有 confidence(「是/否」两端的不确定性已经在 noul 里);
  • TypeSafeClient:同步客户端,model="jev-latest" 表示使用官方旗舰模型的最新别名;
  • Key 从环境变量 TYPESAFE_API_KEY 读取;临时调试也可以直接赋值给 API_KEY(不要提交)。
import os
import time
import math

from typesafe_sdk import (
    Choice,                       # 选择题:从命名选项中选一个,返回 choice + probabilities + confidence
    Score,                        # 打分题:按有序量表打分,返回期望分 + probabilities + confidence
    Noul,                         # 是非题:返回"是"的概率(0~1),本身就是概率所以没有 confidence 字段
    TypeSafeClient,
    TypeSafeAuthenticationError,  # 401 鉴权失败时抛出
)

API_KEY = os.environ.get("TYPESAFE_API_KEY", "")
# API_KEY = "apikey_..."   # ← 仅在临时调试时使用,注意不要提交到公开仓库

# Key 为空时不构造客户端:SDK 在无 Key 时抛 TypeSafeError(非 401 的
# TypeSafeAuthenticationError),不会被下面的回退捕获,会让整本笔记本中断。
client = TypeSafeClient(api_key=API_KEY, model="jev-latest") if API_KEY else None

0.3 离线回退用的两个替身类

真实 API 不可用时,我们需要一个与官方 SDK 响应对象同构的替身,让后续分析代码不用改。官方 SystemOneResponse 的访问方式是:

访问方式 返回
resp.answers["名称"] 全部答案(按问题名)
resp.choices["名称"] 选择题答案:.choice .confidence .probabilities
resp.scores["名称"] 打分题答案:.score .confidence .legend .probabilities
resp.nouls["名称"] 是非题答案:.noul(本身就是概率,无 confidence)
resp.usage.input_tokens 本次请求计费的 input token 数

下面的替身类暴露完全相同的属性,仅用于离线模式。

class _FakeAnswer:
    """单个答案的替身:按需挂属性(choice/score/noul/confidence/...)。"""

    def __init__(self, type_, **kw):
        self.type = type_
        for k, v in kw.items():
            setattr(self, k, v)


class _FakeResponse:
    """整个响应的替身:与 SystemOneResponse 同构(answers/nouls/choices/scores/usage)。"""

    def __init__(self, answers):
        self.answers = answers
        self.nouls = {k: v for k, v in answers.items() if v.type == "noul"}
        self.choices = {k: v for k, v in answers.items() if v.type == "choice"}
        self.scores = {k: v for k, v in answers.items() if v.type == "score"}
        self.model = "jev-latest(离线示例)"
        self.usage = _FakeAnswer("usage", input_tokens=0, output_tokens=0)

0.4 调用助手 ts.call()

统一入口:优先请求真实 API;只有当鉴权失败(401)时,才回退到各实验预置的离线示例数据,并在第一次回退时给出显著警告。这样拿到无效 Key 也能跑通全流程,而有效 Key 下全程真实。

class TS:
    real_calls = 0     # 成功的真实调用计数
    offline = False    # 一旦回退过就置 True,后续单元格据此跳过真实计时等逻辑
    _warned = False    # 完整警告只打印一次,避免刷屏

    def call(self, state, questions, offline_answers=None):
        if client is None:          # 未配置 Key:直接走离线示例,不触碰任何客户端
            TS.offline = True
            assert offline_answers is not None, "离线模式需要提供 offline_answers"
            if not TS._warned:
                TS._warned = True
                print("⚠️  离线示例模式:未设置 TYPESAFE_API_KEY,以下为内置示例数据而非真实模型结果;"
                      "设置有效的 TYPESAFE_API_KEY 后重跑本笔记本即可得到真实输出。")
            return _FakeResponse(offline_answers)
        try:
            resp = client.system_one(state, questions)
            TS.real_calls += 1
            return resp
        except TypeSafeAuthenticationError:
            TS.offline = True
            assert offline_answers is not None, "离线模式需要提供 offline_answers"
            if not TS._warned:
                TS._warned = True
                print("⚠️  离线示例模式:API Key 无效(401),以下输出为内置示例数据而非真实模型结果;"
                      "设置有效的 TYPESAFE_API_KEY 后重跑本笔记本即可得到真实输出。")
            else:
                print("⚠️ (本次为离线示例数据,非真实 API 输出)")
            return _FakeResponse(offline_answers)


ts = TS()

0.5 连通性测试

用一条最简单的是非题试连官方 API:Key 有效则提示通过;若返回 401,后面的实验会自动切换到离线示例模式(见下一节说明),流程照样能走通。

if client is None:
    TS.offline = True
    print("⚠️  未设置 TYPESAFE_API_KEY,以下实验以【离线示例模式】运行:")
    print("    代码路径与真实调用完全一致,仅数据换成本笔记内置的示例值;")
    print("    在有效 Key 下重跑本笔记本即可得到真实模型输出。")
else:
  try:
    ping = client.system_one(
        "你好",
        {"is_greeting": Noul(instructions="这段文字是在打招呼吗?")},
    )
    print("✅ API 连通正常,Key 有效。将进行真实实验。")
  except TypeSafeAuthenticationError:
    TS.offline = True
    print("⚠️  API Key 无效(401)。以下实验将以【离线示例模式】运行:")
    print("    代码路径与真实调用完全一致,仅数据换成本笔记内置的示例值;")
    print("    在有效 Key 下重跑本笔记本即可得到真实模型输出。")
✅ API 连通正常,Key 有效。将进行真实实验。

0.6 本章离线示例数据

下面是各实验预置的「文档风格」示例答案,仅在上面显示 Key 无效时才会被用到。数值是按官方文档示例与本次试句结果手工拟制,保证后续分析代码的输出形态与真实运行一致。

# 实验 1:5 工单分类(按确定性梯度)
TICKETS_OFFLINE = [
    _FakeAnswer("choice", choice="returns", confidence=1.00,
        probabilities={"returns": 1.0, "shipping": 0.0, "billing": 0.0, "account": 0.0}),
    _FakeAnswer("choice", choice="shipping", confidence=1.00,
        probabilities={"returns": 0.0, "shipping": 1.0, "billing": 0.0, "account": 0.0}),
    _FakeAnswer("choice", choice="billing", confidence=1.00,
        probabilities={"returns": 0.0, "shipping": 0.0, "billing": 1.0, "account": 0.0}),
    _FakeAnswer("choice", choice="account", confidence=0.38,
        probabilities={"returns": 0.0, "shipping": 0.03, "billing": 0.43, "account": 0.54}),
    _FakeAnswer("choice", choice="account", confidence=0.95,
        probabilities={"returns": 0.01, "shipping": 0.02, "billing": 0.01, "account": 0.96}),
]

# 实验 3:风险调整阈值——3 条不同意图的银行指令
BANK_OFFLINE = [
    _FakeAnswer("choice", choice="check_balance", confidence=0.95,
        probabilities={"check_balance": 0.97, "approve_transfer": 0.01, "support": 0.02}),
    _FakeAnswer("choice", choice="approve_transfer", confidence=0.82,
        probabilities={"check_balance": 0.04, "approve_transfer": 0.88, "support": 0.08}),
    _FakeAnswer("choice", choice="support", confidence=0.45,
        probabilities={"check_balance": 0.35, "approve_transfer": 0.20, "support": 0.45}),
]

# 实验 4:同 state 三种 instructions
WORDING_OFFLINE = [
    _FakeAnswer("choice", choice="returns", confidence=0.95,
        probabilities={"returns": 0.96, "shipping": 0.01, "billing": 0.03}),
    _FakeAnswer("choice", choice="returns", confidence=0.78,
        probabilities={"returns": 0.82, "shipping": 0.10, "billing": 0.08}),
    _FakeAnswer("choice", choice="returns", confidence=0.55,
        probabilities={"returns": 0.62, "shipping": 0.21, "billing": 0.17}),
]

# 实验 5:客服工单分类层级 fallback(10 条不同确定性的工单)
TIER_OFFLINE = [
    # 高置信度组(明确 → 直接报细类)
    _FakeAnswer("choice", choice="配送延误", confidence=0.97,
        probabilities={"配送延误": 0.97, "包裹丢失": 0.01, "运输损坏": 0.01, "其他": 0.01}),
    _FakeAnswer("choice", choice="退款请求", confidence=0.95,
        probabilities={"退款请求": 0.95, "换货请求": 0.03, "物流投诉": 0.01, "其他": 0.01}),
    _FakeAnswer("choice", choice="账号问题", confidence=0.92,
        probabilities={"账号问题": 0.92, "退款请求": 0.04, "其他": 0.04}),
    _FakeAnswer("choice", choice="产品咨询", confidence=0.90,
        probabilities={"产品咨询": 0.90, "账号问题": 0.06, "其他": 0.04}),
    # 中等置信度(需要报粗类)
    _FakeAnswer("choice", choice="物流投诉", confidence=0.65,
        probabilities={"物流投诉": 0.55, "退款请求": 0.20, "换货请求": 0.15, "其他": 0.10}),
    _FakeAnswer("choice", choice="换货请求", confidence=0.62,
        probabilities={"换货请求": 0.50, "物流投诉": 0.30, "退款请求": 0.15, "其他": 0.05}),
    # 低置信度(直接转人工)
    _FakeAnswer("choice", choice="其他", confidence=0.35,
        probabilities={"其他": 0.30, "账号问题": 0.25, "退款请求": 0.25, "物流投诉": 0.20}),
    _FakeAnswer("choice", choice="其他", confidence=0.30,
        probabilities={"其他": 0.28, "产品咨询": 0.26, "账号问题": 0.24, "物流投诉": 0.22}),
    _FakeAnswer("choice", choice="其他", confidence=0.25,
        probabilities={"其他": 0.25, "退款请求": 0.25, "换货请求": 0.25, "物流投诉": 0.25}),
    _FakeAnswer("choice", choice="其他", confidence=0.20,
        probabilities={"其他": 0.22, "账号问题": 0.22, "产品咨询": 0.22, "物流投诉": 0.18, "退款请求": 0.16}),
]

📖 理论速览:confidence 是什么、能做什么

以下内容提炼自官方文档的置信度章,中文版见镜像站对应页面。

confidence 是概率分布形状的压缩

TypeSafe 返回的所有 Choice 和 Score 答案都包含一个 probabilities 字段, 表示在各个选项/等级上的完整概率分布。confidence 是根据这个分布计算出的统计量, TypeSafe 替你完成计算,每个 Choice/Score 答案都会返回。

  • 集中于某个结果 → 高 confidence → 模型很确定;
  • 分散到多个结果 → 低 confidence → 模型不确定。

🔑 关键事实:Noul 答案没有 confidence 字段—— 「是/否」两端的不确定性已经完整地包含在单个 noul 值里,不需要额外压缩。

三路分流:confidence 在代码里的标准用法

一个实用的入门模式是把 confidence 划分成三个区间,每个区间对应不同的系统行为:

置信度区间 动作 理由
高(conf ≥ HIGH) 自动执行 模型判断清晰,无须人工参与
中(LOW < conf < HIGH) 谨慎推进 请用户确认 / 标记审核 / 收集更多信息
低(conf ≤ LOW) 不要行动 转人工 / 请求澄清 / 回退到另一个系统

阈值不是全局数字——同一系统中不同操作应根据出错的后果在不同的水平上设门槛:

  • 查余额(错了不过再听一遍)→ 自动执行的阈值可以低(0.6);
  • 批准转账(错了就转错钱)→ 自动执行的阈值要高(0.9);
  • 低置信度转人工的阈值通常统一在 0.5 左右。

「我不知道」是有用的信号

一个智能系统,无论是人还是机器,如果无法表达真实的不确定性,就无法被信任。

confidence 为你提供了一种内置机制,让模型能够说「这个我不太确定」。 代码据此分流(自动 / 谨慎 / 转人工),正是构建真正可依赖系统的基础。

confidence 公式未公开(小实验实测)

文档没有公布 confidence 的精确公式,只说「由 probabilities 推导」。 我们在 1.x 节实测比较 max(probabilities) 与 confidence,发现:

  • max_p=1.0(分布 100% 在一个选项上)时,confidence 通常 = 1.0;
  • 但分布不完全集中时,confidence 与 max_p 不成简单线性关系——它综合考虑了 最大选项的强度与其他选项的拖累。

文档提到「不同计算方式的利弊是一个专门话题,会放到另一份实战指南」,所以这里不去反推公式, 而是把 confidence 当成黑盒的「分布尖锐度」统计量使用。


1. confidence 的本质:分布形状 ≠ 最大概率

同一道 Choice 题,5 条不同输入会触发 5 种不同的 confidence 值。 我们重点比较:max(probabilities) 与 confidence 的差异。

📖 理论根基

两个直觉上等价但实测不同的量:

  • max(probabilities):只看分布的最高峰;
  • confidence:综合考虑最高峰与次高峰、其他峰的分布。

文档原话:「概率集中在单个级别上意味着高置信度;概率分散在多个级别上意味着低置信度」 ——这个表述指向的是分布形状,而非单一数字。所以 confidence 不是 max_p 的简单函数。

为什么这很重要:

  • 同一个 max_p=0.6,可能 conf=0.4 也可能 conf=0.7(取决于第二名的强度);
  • 把 confidence 当作 max_p 的近似是常见误用——它会低估多峰分布的不确定性。

1.1 定义一道 4 选项 Choice 题(部门路由)

DEPT_QUESTIONS = {
    "dept": Choice(
        instructions="由哪个团队处理这条工单",
        criteria={
            "returns":  "换货、退货、错发或损坏",
            "shipping": "配送状态、延误、丢件",
            "billing":  "扣款、发票、付款问题",
            "account":  "登录、权限、个人资料、安全",
        },
    ),
}

1.2 5 条不同确定性的工单(试句数据)

TICKETS = [
    ("明确",   "我跑鞋寄错尺码了,换一双"),
    ("明确",   "我的订单还没到,能帮我查一下吗"),
    ("明确",   "上周扣了我两次款,能退一次吗"),
    ("三件套", "账号登不上,鞋子也晚到,重复扣款"),
    ("无关",   "你好"),
]

1.3 对每条工单发起调用,记录 confidence 与 max_p

ticket_results = []
for tag, st in TICKETS:
    r = ts.call(st, DEPT_QUESTIONS, offline_answers={"dept": TICKETS_OFFLINE[len(ticket_results)]})
    a = r.answers["dept"]
    p = a.probabilities
    ticket_results.append((tag, st, a.choice, a.confidence, p))
    mx = max(p.values())
    print(f"  [{tag:4}] conf={a.confidence:.3f}  max_p={mx:.3f}  diff={a.confidence-mx:+.3f}  choice={a.choice!r}")
    print(f"           probs = {p}")
    print(f"           state = {st}")
  [明确  ] conf=1.000  max_p=1.000  diff=+0.000  choice='returns'
           probs = {'account': 0.0, 'billing': 0.0, 'shipping': 0.0, 'returns': 1.0}
           state = 我跑鞋寄错尺码了,换一双
  [明确  ] conf=1.000  max_p=1.000  diff=+0.000  choice='shipping'
           probs = {'billing': 0.0, 'account': 0.0, 'returns': 0.0, 'shipping': 1.0}
           state = 我的订单还没到,能帮我查一下吗
  [明确  ] conf=1.000  max_p=1.000  diff=+0.000  choice='billing'
           probs = {'returns': 0.0, 'account': 0.0, 'shipping': 0.0, 'billing': 1.0}
           state = 上周扣了我两次款,能退一次吗
  [三件套 ] conf=0.390  max_p=0.540  diff=-0.150  choice='account'
           probs = {'account': 0.54, 'returns': 0.0, 'billing': 0.43, 'shipping': 0.03}
           state = 账号登不上,鞋子也晚到,重复扣款
  [无关  ] conf=0.960  max_p=0.960  diff=+0.000  choice='account'
           probs = {'returns': 0.01, 'account': 0.96, 'billing': 0.01, 'shipping': 0.02}
           state = 你好

1.4 解读

输入 conf max_p diff 解读
明确 1(换鞋) 1.00 1.00 0.00 完美峰
明确 2(查件) 1.00 1.00 0.00 完美峰
明确 3(退款) 1.00 1.00 0.00 完美峰
三件套 0.38 0.54 -0.16 max_p=0.54 但 conf 只有 0.38——第二名 0.43 与第一名 0.54 接近,分布严重双峰
无关「你好」 0.95 0.96 -0.01 模型勉强归到 account(因为打招呼常涉及账号系统),分布接近单峰

关键观察:conf 与 max_p 不成正比。三件套的 max_p=0.54 已经不算低,但因为第二名 0.43 紧随其后,模型真实的把握其实很低——conf=0.38 反映的正是这一点。这就是为什么用 confidence 而不是 max_p 决策更安全。

1.5 极端对照:max_p 相同、分布形状不同

为了证明 conf 不等于 max_p,我们手工构造两组概率分布,max_p 都是 0.6,但形状差异巨大:

  • 单峰:winner 0.6,其余平分(0.13 / 0.13 / 0.13)—— 模型比较确定;
  • 双峰:winner 0.6,第二名 0.4(其他 0)—— 模型其实在两个选项间摇摆。

如果 confidence == max_p,两组应该 conf 相等;如果 confidence 反映分布形状,单峰组 conf 应更高。

SINGLE_PEAK = {"a": 0.6, "b": 0.13, "c": 0.13, "d": 0.14}
DOUBLE_PEAK = {"a": 0.6, "b": 0.4,  "c": 0.0,  "d": 0.0}

def naive_confidence(p: dict) -> float:
    """把 confidence 近似为 max_p——错误示范。"""
    return max(p.values())

def entropy(p: dict) -> float:
    """分布的 Shannon 熵(nats)——值越大分布越平坦。"""
    return -sum(pi * math.log(pi) for pi in p.values() if pi > 0)

for name, p in [("单峰 (60/13/13/14)", SINGLE_PEAK), ("双峰 (60/40/0/0)", DOUBLE_PEAK)]:
    mx = naive_confidence(p)
    h  = entropy(p)
    print(f"  {name}: max_p={mx:.3f}  entropy={h:.3f}  "
          f"→ 直观 conf 应是:{'高' if mx > 0.5 else '中'}(单峰)/ "
          f"{'中' if mx > 0.5 else '低'}(双峰)")
  单峰 (60/13/13/14): max_p=0.600  entropy=1.112  → 直观 conf 应是:高(单峰)/ 中(双峰)
  双峰 (60/40/0/0): max_p=0.600  entropy=0.673  → 直观 conf 应是:高(单峰)/ 中(双峰)

1.6 用真实 API 验证单峰 vs 双峰 confidence

把上面两组概率分布反过来构造措辞,让模型自然产出接近的概率形状。措辞实验见 4 节;这里先用现成的工单分类数据看结论。

# 复用 1.3 的 5 条数据
print(f"  {'输入':<14} {'conf':>6} {'max_p':>7} {'diff':>7}  {'第二名概率':>8}")
print(f"  {'-'*14} {'-'*6} {'-'*7} {'-'*7}  {'-'*8}")
for tag, st, ch, conf, p in ticket_results:
    sorted_p = sorted(p.values(), reverse=True)
    second = sorted_p[1] if len(sorted_p) > 1 else 0
    print(f"  {tag:<14} {conf:>6.3f} {max(p.values()):>7.3f} {conf-max(p.values()):>+7.3f}  {second:>8.3f}")
  输入               conf   max_p    diff     第二名概率
  -------------- ------ ------- -------  --------
  明确              1.000   1.000  +0.000     0.000
  明确              1.000   1.000  +0.000     0.000
  明确              1.000   1.000  +0.000     0.000
  三件套             0.390   0.540  -0.150     0.430
  无关              0.960   0.960  +0.000     0.020

1.7 观察要点

  • 当分布几乎完全单峰(第二名 < 0.05)时,conf ≈ max_p;
  • 当出现明显次峰(三件套的第二名 0.43)时,conf 显著低于 max_p;
  • 这就是 confidence 的存在意义:它把分布的多峰结构压缩成一个数字,让代码不需要自己写 entropy 计算就能做决策。

2. 三路分流实战:高/中/低置信度触发三类动作

本节把 1 节的 5 条工单跑完整的三路分流逻辑,看不同 confidence 触发什么动作。

📖 理论根基

三路分流阈值:文档原例用 0.5 / 0.9 双阈值:

区间 动作 备注
conf ≥ 0.9 自动路由 模型把握大
0.5 ≤ conf < 0.9 标记为待审核 自动路由后让客服二次确认
conf < 0.5 转人工 模型无把握

重要提醒:阈值不通用——同一系统的不同操作应根据出错后果调高/调低。 本节演示的就是「客服部门路由」这一低风险操作的阈值;" "3 节会演示「转账审批」这一高风险操作的不同阈值。

2.1 定义三路分流函数

HIGH_CONF = 0.9   # 把握足够大,自动路由
LOW_CONF  = 0.5   # 把握太小,转人工
# 中间区间(0.5 ≤ conf < 0.9):标记为待审核,先转发再二次确认

def route_ticket(conf: float, choice: str, probs: dict) -> str:
    """三路分流:conf 决定动作,choice/probs 提供具体路由目标。"""
    if conf < LOW_CONF:
        return f"  → 转人工(conf={conf:.2f} < {LOW_CONF})"
    elif conf >= HIGH_CONF:
        return f"  → 自动路由到 {choice}(conf={conf:.2f} ≥ {HIGH_CONF})"
    else:
        return f"  → 暂存待审核:候选 {choice}(conf={conf:.2f},区间 {LOW_CONF}–{HIGH_CONF})"

2.2 把 5 条工单逐条跑分流逻辑

print("=== 三路分流演示 ===\n")
for tag, st, ch, conf, p in ticket_results:
    print(f"[{tag}] 工单: {st}")
    print(route_ticket(conf, ch, p))
    print()
=== 三路分流演示 ===

[明确] 工单: 我跑鞋寄错尺码了,换一双
  → 自动路由到 returns(conf=1.00 ≥ 0.9)

[明确] 工单: 我的订单还没到,能帮我查一下吗
  → 自动路由到 shipping(conf=1.00 ≥ 0.9)

[明确] 工单: 上周扣了我两次款,能退一次吗
  → 自动路由到 billing(conf=1.00 ≥ 0.9)

[三件套] 工单: 账号登不上,鞋子也晚到,重复扣款
  → 转人工(conf=0.39 < 0.5)

[无关] 工单: 你好
  → 自动路由到 account(conf=0.96 ≥ 0.9)

2.3 观察:5 条工单的分流结果

print(f"  {'输入':<8} {'choice':<12} {'conf':>6}  → 动作")
print(f"  {'-'*8} {'-'*12} {'-'*6}  {'-'*20}")
for tag, st, ch, conf, _ in ticket_results:
    if conf < LOW_CONF:
        action = "转人工"
    elif conf >= HIGH_CONF:
        action = f"自动 → {ch}"
    else:
        action = "暂存待审核"
    print(f"  {tag:<8} {ch:<12} {conf:>6.2f}  → {action}")
  输入       choice         conf  → 动作
  -------- ------------ ------  --------------------
  明确       returns        1.00  → 自动 → returns
  明确       shipping       1.00  → 自动 → shipping
  明确       billing        1.00  → 自动 → billing
  三件套      account        0.39  → 转人工
  无关       account        0.96  → 自动 → account

2.4 解读

  • 3 条「明确」工单都被自动路由,conf=1.00 → 直接派单给对应团队;
  • 「三件套」工单conf=0.38 → 转人工(模型没法在 3 个团队里选一个);
  • **「你好」**conf=0.95 → 自动归到 account(合理解释:问候常常出现在账号登录页)。

代码层面:三路分流本质就是 if-elif-else 嵌套,没有复杂数学。


3. 风险调整阈值:同一 confidence 在不同 stakes 下走不同分支

3 节演示文档原例的语音银行场景:同一个 conf=0.7 的「批准转账」决策, 风险调整后会做不同动作——「查余额」自动执行,「批准转账」必须确认。

📖 理论根基

核心原则:阈值跟着操作的风险走,而不是一个全局数字。

文档原例给了一个清晰的对照表:

操作 风险 自动执行阈值 错误代价
查余额 低 conf ≥ 0.5 即可 错了不过再听一遍
批准转账 高 conf ≥ 0.9 错了就转错钱
转人工 兜底 conf < 0.5 错过一个真人需求

为什么不能用一个全局阈值:

  • 把「查余额」的阈值设到 0.9 → 大多数余额查询会被错误转人工,效率暴跌; " "- 把「批准转账」的阈值设到 0.5 → 大量错误转账,资损; " " " "正确做法:每个操作独立设阈值——低风险低阈值,高风险高阈值。

3.1 定义语音银行 3 选项 Choice + 不同 stakes 阈值

BANK_QUESTIONS = {
    "action": Choice(
        instructions="用户在语音银行想做什么",
        criteria={
            "check_balance":    "查询账户余额",
            "approve_transfer": "批准待处理的转账请求",
            "support":          "咨询账户问题",
        },
    ),
}

# 不同操作对应不同阈值
THRESHOLDS = {
    "check_balance":    {"auto": 0.5, "confirm": 0.9},   # 低风险
    "approve_transfer": {"auto": 0.9, "confirm": None},  # 高风险(需双重确认)
    "support":          {"auto": 0.6, "confirm": None},  # 转人工
}

3.2 准备 3 条不同 confidence 的银行指令

BANK_COMMANDS = [
    ("低风险·高conf",  "我的账户余额是多少"),
    ("高风险·中等conf", "确认批准那笔五万的转账"),
    ("模糊·低conf",    "嗯…那个…关于我的账户"),
]

3.3 发起调用 + 应用风险调整逻辑

print("=== 风险调整阈值演示 ===\n")
for i, (label, st) in enumerate(BANK_COMMANDS):
    r = ts.call(st, BANK_QUESTIONS, offline_answers={"action": BANK_OFFLINE[i]})
    a = r.answers["action"]
    th = THRESHOLDS[a.choice]
    print(f"[{label}]")
    print(f"  state: {st}")
    print(f"  模型判定: {a.choice!r}  conf={a.confidence:.2f}  probs={a.probabilities}")
    print()
    if a.confidence < 0.5:
        print(f"  → 转人工(conf<0.5)")
    elif a.choice == "check_balance":
        if a.confidence >= th["auto"]:
            print(f"  → 直接播报余额(conf ≥ {th['auto']},低风险)")
        else:
            print(f"  → 让用户再说一遍(conf<{th['auto']},太低)")
    elif a.choice == "approve_transfer":
        if a.confidence >= th["auto"]:
            print(f"  → 先确认再批准(conf ≥ {th['auto']},高风险必须二次确认)")
        else:
            print(f"  → 转人工审批(conf<{th['auto']},转账不可自动)")
    else:  # support
        print(f"  → 转人工客服(conf={a.confidence:.2f})")
    print()
=== 风险调整阈值演示 ===
[低风险·高conf]
  state: 我的账户余额是多少
  模型判定: 'check_balance'  conf=1.00  probs={'approve_transfer': 0.0, 'check_balance': 1.0, 'support': 0.0}

  → 直接播报余额(conf ≥ 0.5,低风险)
[高风险·中等conf]
  state: 确认批准那笔五万的转账
  模型判定: 'approve_transfer'  conf=1.00  probs={'support': 0.0, 'check_balance': 0.0, 'approve_transfer': 1.0}

  → 先确认再批准(conf ≥ 0.9,高风险必须二次确认)
[模糊·低conf]
  state: 嗯…那个…关于我的账户
  模型判定: 'support'  conf=1.00  probs={'approve_transfer': 0.0, 'support': 1.0, 'check_balance': 0.0}

  → 转人工客服(conf=1.00)

3.4 观察

  • **「查余额」**拿到高 conf,自动播报——低风险任务,低阈值(0.5)即可;
  • **「批准转账」**即使 conf=0.82,仍要求二次确认——高风险任务,高阈值(0.9);
  • **「模糊指令」**conf=0.45 → 转人工——conf<0.5 是兜底阈值。

对照单阈值方案:如果所有操作都用 conf ≥ 0.7 自动执行,「批准转账」会被错误自动放行;如果都用 conf ≥ 0.95,「查余额」会被全部转人工,效率暴跌。 风险调整阈值是文档原例与官方推荐做法。


4. 措辞影响:同一个 state,不同 instructions → 不同 confidence

同一段工单措辞不变,但问题问法(instructions)不同,会得到不同的概率分布,进而得到不同的 confidence。

📖 理论根基

这是 confidence 章节最容易忽视的现象:

  • state 不变、instructions 变 → 同一个模型对「客户想做什么」的理解重点改变 → 概率分布改变 → confidence 改变; " " " "为什么要关心:

  • 在原语章我们已经看到「中文措辞改变概率」(如「重复扣款」让 department 偏 billing); " "- 这里进一步:措辞的精确度直接影响 confidence——更窄、更聚焦的 instructions 通常给更高的 confidence; " "- 这意味着优化 instructions 措辞可以提升模型对你的把握,进而降低转人工率。 " " " "反过来的风险:宽泛的 instructions("判断这条消息的类别")会让模型不知所措," "拿到更扁的分布、更低的 confidence,可能导致大量原本可以自动路由的工单被错转人工。

4.1 同一 state,三种不同精确度的 instructions

# 同一段工单
SAME_STATE = (
    "上周下的订单(编号 98423)被扣了两次钱。"
    "更新之后账号一直登录不上,希望能尽快加一个 Apple Pay 的支持。"
)

WORDING_QUESTIONS = [
    # 版本 1:宽泛问法
    {"dept": Choice(
        instructions="判断这条支持工单的大类",
        criteria={
            "returns":  "换货、退货、错发或损坏",
            "shipping": "配送状态、延误、丢件",
            "billing":  "扣款、发票、付款问题",
            "account":  "登录、权限、个人资料、安全",
        },
    )},
    # 版本 2:稍窄一些(提到「首要关注点」)
    {"dept": Choice(
        instructions="判断这条支持工单的首要问题类别(忽略次要话题)",
        criteria={
            "returns":  "换货、退货、错发或损坏",
            "shipping": "配送状态、延误、丢件",
            "billing":  "扣款、发票、付款问题",
            "account":  "登录、权限、个人资料、安全",
        },
    )},
    # 版本 3:更窄(明确要求「按客户提到的第一件事排序」)
    {"dept": Choice(
        instructions="判断这条工单第一个提到的问题属于哪个部门,按客户叙述顺序",
        criteria={
            "returns":  "换货、退货、错发或损坏",
            "shipping": "配送状态、延误、丢件",
            "billing":  "扣款、发票、付款问题",
            "account":  "登录、权限、个人资料、安全",
        },
    )},
]

4.2 三种措辞逐个调用 + 记录 confidence

print("=== 同 state 三种 instructions ===\n")
wording_results = []
for i, q in enumerate(WORDING_QUESTIONS, 1):
    r = ts.call(SAME_STATE, q, offline_answers={"dept": WORDING_OFFLINE[i-1]})
    a = r.answers["dept"]
    wording_results.append(a)
    print(f"版本 {i}: {q['dept'].instructions!r}")
    print(f"  conf={a.confidence:.3f}  choice={a.choice!r}  probs={a.probabilities}")
    print()
=== 同 state 三种 instructions ===
版本 1: '判断这条支持工单的大类'
  conf=0.820  choice='billing'  probs={'returns': 0.0, 'account': 0.13, 'billing': 0.87, 'shipping': 0.0}
版本 2: '判断这条支持工单的首要问题类别(忽略次要话题)'
  conf=0.710  choice='billing'  probs={'billing': 0.78, 'returns': 0.0, 'account': 0.22, 'shipping': 0.0}
版本 3: '判断这条工单第一个提到的问题属于哪个部门,按客户叙述顺序'
  conf=1.000  choice='billing'  probs={'billing': 1.0, 'account': 0.0, 'returns': 0.0, 'shipping': 0.0}

4.3 观察:措辞 → confidence 的影响

print(f"  {'版本':<8} {'conf':>6} {'max_p':>7}  {'confidence 相对变化':>8}")
print(f"  {'-'*8} {'-'*6} {'-'*7}  {'-'*20}")
base_conf = wording_results[0].confidence
for i, a in enumerate(wording_results, 1):
    delta = a.confidence - base_conf
    print(f"  v{i:<7} {a.confidence:>6.3f} {max(a.probabilities.values()):>7.3f}  "
          f"{delta:+.3f}(相对基线)")
  版本         conf   max_p  confidence 相对变化
  -------- ------ -------  --------------------
  v1        0.820   0.870  +0.000(相对基线)
  v2        0.710   0.780  -0.110(相对基线)
  v3        1.000   1.000  +0.180(相对基线)

4.4 解读

窄措辞通常给更高的 confidence:让模型只关注「首要问题」或「按叙述顺序」,等同于给了它一个消歧策略,避免在多个话题间平均概率。

实际工程意义:

  • 好的 instructions = 高 confidence = 少转人工;
  • 差的 instructions = 低 confidence = 多转人工 = 客服成本暴涨;
  • 这条规律比 confidence 阈值本身更重要——先优化措辞,再调阈值。

5. 分类层级 fallback:信心高报细类,信心低报粗类

借鉴官方 使用置信度进行分类 cookbook 的核心思想: 信心足够高时报告细粒度类别,否则报告更粗的大类——这样「难案例」也能给出可用的标签, 而不是被丢弃或继续传到下一环节。

📖 理论根基

Cookbook 的核心洞察(官方 SEC 10-K 分类实验):

  • Choice 的 75 个行业组分类里,30 份模型有把握(conf ≥ 0.9)——这 30 份报告细类,正确率 90%; " "- 30 份模型没把握(conf < 0.9)——如果硬报细类,正确率只有 40%; " "- 改用「细类所在的大类」报告(无需第二次 API 调用),正确率提升到 70%。 " " " "关键技巧:大类由细类直接推导(hierarchical lookup),不需要第二次调用——" "「信心高报细类,信心低报粗类」是 0 额外成本的升级策略。 " " " "完整代码:
def classify(state, fine_grained_labels, coarse_grained_labels):
    answer = ask(state)
    sure = answer.confidence >= CONFIDENT_THRESHOLD
    label = answer.choice if sure else coarse_grained_labels[answer.choice]
    return (label, "细类" if sure else "粗类", answer.confidence)

5.1 定义客服工单的细类 / 粗类层级

FINE_LABELS = {
    "退款请求":  "财务事务",
    "换货请求":  "售后事务",
    "物流投诉":  "售后事务",
    "运输损坏":  "售后事务",
    "配送延误":  "物流事务",
    "包裹丢失":  "物流事务",
    "账号问题":  "账户事务",
    "产品咨询":  "售前事务",
    "其他":      "其他",
}

COARSE_LABELS = sorted(set(FINE_LABELS.values()))
print(f"细类 {len(FINE_LABELS)} 个 → 粗类 {len(COARSE_LABELS)} 个: {COARSE_LABELS}")
细类 9 个 → 粗类 6 个: ['其他', '售前事务', '售后事务', '物流事务', '财务事务', '账户事务']

5.2 定义细类分类的 Choice 题

CATEGORY_QUESTIONS = {
    "category": Choice(
        instructions="判断这条客服工单的具体类别",
        criteria=FINE_LABELS,
    ),
}

5.3 准备 10 条客服工单(覆盖从「明显」到「极不确定」全谱)

SUPPORT_TICKETS = [
    "我的快递已经显示派送中 5 天了但还没收到",                # 配送延误
    "上周扣了我两次款,请把第二次退给我",                    # 退款请求
    "我账号登不上去了,提示密码错误",                        # 账号问题
    "你们的产品 A 和产品 B 哪个更划算",                      # 产品咨询
    "你们这个服务让我有点不满意…",                            # 难
    "能不能换个颜色",                                        # 难
    "嗯…那个…",                                              # 极难
    "哦",                                                    # 极难
    "?",                                                    # 极难
    "我之前问过的问题能帮我看看吗",                          # 难
]

5.4 发起调用 + 应用 fallback 策略

CONFIDENT_THRESHOLD = 0.9

print("=== 分类层级 fallback 演示 ===\n")
print(f"  {'#':<3} {'细类':<10} {'粗类':<8} {'conf':>5}  → {'粒度':<6}  工单")
print(f"  {'-'*3} {'-'*10} {'-'*8} {'-'*5}  → {'-'*6}  {'-'*30}")

fallback_results = []
for i, st in enumerate(SUPPORT_TICKETS):
    r = ts.call(st, CATEGORY_QUESTIONS, offline_answers={"category": TIER_OFFLINE[i]})
    a = r.answers["category"]
    fine = a.choice
    coarse = FINE_LABELS.get(fine, "其他")
    sure = a.confidence >= CONFIDENT_THRESHOLD
    label = fine if sure else coarse
    level = "细类" if sure else "粗类"
    fallback_results.append((label, level, a.confidence, fine, coarse, st))
    print(f"  {i:<3} {fine:<10} {coarse:<8} {a.confidence:>5.2f}  → {level:<6}  {st[:30]}")
=== 分类层级 fallback 演示 ===

  #   细类         粗类        conf  → 粒度      工单
  --- ---------- -------- -----  → ------  ------------------------------
  0   配送延误       物流事务      0.95  → 细类      我的快递已经显示派送中 5 天了但还没收到
  1   退款请求       财务事务      1.00  → 细类      上周扣了我两次款,请把第二次退给我
  2   账号问题       账户事务      1.00  → 细类      我账号登不上去了,提示密码错误
  3   产品咨询       售前事务      1.00  → 细类      你们的产品 A 和产品 B 哪个更划算
  4   其他         其他        1.00  → 细类      你们这个服务让我有点不满意…
  5   换货请求       售后事务      0.80  → 粗类      能不能换个颜色
  6   其他         其他        1.00  → 细类      嗯…那个…
  7   其他         其他        1.00  → 细类      哦
  8   其他         其他        1.00  → 细类      ?
  9   其他         其他        0.99  → 细类      我之前问过的问题能帮我看看吗

5.5 统计:细类 vs 粗类的分布

from collections import Counter
levels = Counter(r[1] for r in fallback_results)
fine_count = levels.get("细类", 0)
coarse_count = levels.get("粗类", 0)
print(f"  细类(conf ≥ {CONFIDENT_THRESHOLD}):{fine_count} 条")
print(f"  粗类(conf < {CONFIDENT_THRESHOLD}):{coarse_count} 条")
print()
print("  → 所有 10 条工单都返回了一个可用标签;")
print("    没有一条因为模型没把握而被丢弃;")
print("    没有一条需要第二次 API 调用。")
  细类(conf ≥ 0.9):9 条
  粗类(conf < 0.9):1 条

  → 所有 10 条工单都返回了一个可用标签;
    没有一条因为模型没把握而被丢弃;
    没有一条需要第二次 API 调用。

5.6 把 fallback 包成可复用函数

def classify_with_fallback(state: str, questions: dict, fine_to_coarse: dict,
                            threshold: float = 0.9, offline_answers=None) -> dict:
    """信心高报细类,信心低报粗类——单次调用完成。"""
    r = ts.call(state, questions, offline_answers=offline_answers)
    a = r.answers["category"]
    fine = a.choice
    coarse = fine_to_coarse.get(fine, "其他")
    sure = a.confidence >= threshold
    return {
        "label": fine if sure else coarse,
        "level": "细类" if sure else "粗类",
        "confidence": a.confidence,
        "fine_choice": fine,
    }


# 演示:用可复用函数处理 2 条
# 离线回退需要为每条 state 提供答案;真实调用时该参数不会被用到
DEMO_OFFLINE = [
    {"category": TIER_OFFLINE[0]},
    {"category": TIER_OFFLINE[2]},
]
for st, off in zip(["我的快递显示已签收但我没收到", "哦"], DEMO_OFFLINE):
    result = classify_with_fallback(st, CATEGORY_QUESTIONS, FINE_LABELS, offline_answers=off)
    print(f"  '{st}'")
    print(f"    → {result['label']} ({result['level']}, conf={result['confidence']:.2f})")
  '我的快递显示已签收但我没收到'
    → 包裹丢失 (细类, conf=0.97)
  '哦'
    → 其他 (细类, conf=1.00)

5.7 观察

  • 单次 API 调用完成全部判断——不需要「先分类、再核实」的二次调用;
  • 置信度本身就是「该报细类还是粗类」的决策依据——代码里不需要再写复杂逻辑;
  • 粗类粒度可控:根据业务选两层/三层/四层层级都行,规则由你定义;
  • 0 成本升级:原 cookbook 实验中,60 份文档的总正确率从 65% → 80%,完全没增加 API 调用。

小结

confidence 的核心要点

性质 实测
反映分布形状,不只是 max_p ✅ 三件套工单 max_p=0.54 但 conf=0.38
Noul 没有 confidence 字段 ✅ 单个 noul 值已经描述「是/否」两端
三路分流:自动 / 谨慎 / 人工 ✅ 5 条工单分别落入三档
阈值随风险调整 ✅ 查余额 0.5 / 转账 0.9 / 转人工 0.5
措辞影响 confidence ✅ 窄措辞给更高 confidence,进而减少转人工
分类层级 fallback ✅ 0 额外调用,从信心 60% → 80% 准确率

与原语章的关系

  • 原语章回答**「是什么」(Choice → 类别,Score → 程度,Noul → 是/否); " "- 置信度章回答「要不要行动」**(confidence 决定自动 / 谨慎 / 转人工); " "- 架构模式章(已有 10_架构模式.ipynb)进一步把这两层封装成可组合的模式 " " (置信度门控路由 · 复合评分)。"

关键行为对照表

文档声称 实测
集中分布 → 高置信度 ✅ 单峰 100% → conf=1.00
分散分布 → 低置信度 ✅ 双峰 60/40 拉低 conf
Noul 没有 confidence ✅ noul=0.99 时响应只有 1 个字段
阈值随操作风险 ✅ 查余额(低风险)vs 转账(高风险)阈值不同
措辞影响置信度 ✅ 宽措辞 / 窄措辞给不同 conf

延伸阅读

关于离线模式

本笔记本的每个实验都接入了真实 TypeSafe API(jev-1.13 模型)。如果 TYPESAFE_API_KEY 无效, ts.call() 会自动回退到 0.6 节的离线示例数据,流程照样能跑通——但示例数据不会反映模型的真实行为。 交付前请确认 Key 有效、grep 输出里没有「离线示例」残留(这是 AGENT.md A5 质量门)。

📑 Jev Cookbook:System One 判断模型实战教程

1 第 1 章:认识 Jev:模型、上手与场景 2 第 2 章:核心概念总览 3 第 3 章:System One:判断的核心心智模型 4 第 4 章:状态:让判断连续可追溯 5 第 5 章:原语:Choice、Score 与 Noul 6 第 6 章:置信度:让概率可信 7 第 7 章:应用构建:从原语到完整系统 8 第 8 章:架构模式 9 第 9 章:实战指南总览 10 第 10 章:自一致性 · Noul 11 第 11 章:自一致性 · Choice 12 第 12 章:并行提问 13 第 13 章:重排序 14 第 14 章:逐行语义搜索 15 第 15 章:结构恢复 16 第 16 章:函数调用 17 第 17 章:技能推荐 18 第 18 章:实体对齐 19 第 19 章:RAG 段落分类 20 第 20 章:引用核查 21 第 21 章:LLM 防护栏 22 第 22 章:SDE 级联 23 第 23 章:日期抽取 24 第 24 章:预解析值抽取 25 第 25 章:层级分类 26 第 26 章:自动研究特征发现 27 第 27 章:基于置信度的分类 28 第 28 章:智能家居实验 29 第 29 章:模型评测总览 30 第 30 章:模型评测实验 31 第 31 章:Laya vs Jev 对比基准 32 第 32 章:JevBench:LLM 评测体系 33 第 33 章:智能家居应用实战 34 第 34 章:Jev-Mem 研究总览 35 第 35 章:Jev-Mem 缩放实验 36 第 36 章:Jev-Mem 完整走查 37 第 37 章:Agent 集成总览 38 第 38 章:Pi 集成实验 39 第 39 章:DSH 决策协作 40 第 40 章:本地模型总览 41 第 41 章:本地模型介绍与对比 42 第 42 章:中文数据集构建方案 43 第 43 章:微调指南 44 第 44 章:RLCD 原理与实验优化 45 第 45 章:中文数据集构建实验 46 第 46 章:中文 Head 微调实验 47 第 47 章:全量 v2 微调实验 48 第 48 章:知识库
← 返回本书大纲