第 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 None0.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.0201.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 → 自动 → account2.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 质量门)。