大数跨境

象信 · 系统一|(四)不知道就说不知道,是一项工程特性

象信 · 系统一|(四)不知道就说不知道,是一项工程特性 象信AI
2026-10-12
0
导读:象信 · 系统一

——《象信 · 系统一》系列之四

先看一个真实的设计难题。

手机银行 App 的智能助手,用户说一句话,可能是三件事之一:

  • 查余额
    ——只读操作,判断错了顶多显示错一个数字,用户自己会发现;
  • 确认一笔待处理的转账
    ——动钱,判断错了很麻烦;
  • 冻结银行卡
    ——用户可能正在被盗刷。

这三个动作,该用同一个置信度阈值吗?

用同一个,只有两种结果:要么动钱的操作太容易自动执行,要么查余额这种低风险操作也被卡去人工,体验糟得没人用。

答案是:置信度不是一个"准入开关",它是一个可以按风险定价的旋钮。

先说清楚:置信度是怎么算出来的

每个 Choice 和 Score 答案都带一个 confidence,0 到 1。

关键点:它不是模型额外"报告"的一个数,而是在返回之前,根据答案自己的 probabilities 算出来的。公式公开且固定:

同样是 0.70,2 个选项和 20 个选项的含金量差一倍

同样是 0.70,2 个选项和 20 个选项的含金量差一倍


类型
公式
含义
Choice (n × p_max − 1) / (n − 1)
n 为选项数,p_max 为最高概率
Score p_max
最高那一档的概率

第一次看这个 Choice 公式的人,几乎都会问同一个问题:为什么不直接用 p_max?

因为它有个陷阱。同样是 p_max = 0.70:

  • 两个选项
    时,瞎猜也有 0.50,0.70 只比瞎猜好一点 → 换算成置信度是 0.40
  • 二十个选项
    时,瞎猜只有 0.05,0.70 已经相当集中 → 换算成置信度约 0.68

同一个 0.70,含金量差了将近一倍。这个公式把"比均匀分布集中了多少"换算到 0–1,让 2 个选项的分类和 20 个选项的分类可以共用同一套阈值。

算一遍就清楚了。一个有 4 个选项的 Choice 返回:

json

{ "probabilities": { "物流": 0.70, "售后": 0.20, "账务": 0.10, "其他": 0.00 } }

text

confidence = (4 × 0.70 − 1) / (4 − 1) = 1.80 / 3 = 0.60

分布是均匀的,置信度就是 0;全部集中在一个选项上,置信度就是 1。 这就是它的全部含义。

顺便说一句:你不被这个定义锁死。完整的 probabilities 就在响应里,如果你更关心"前两名的差距",可以自己算:

python

probs = sorted(resp.answers["route"].probabilities.values(), reverse=True)

margin = probs[0] - probs[1]   # 第一名比第二名高多少

Noul 为什么没有置信度

Noul 的分布只有"是"和"否"两个结果,所以 noul 这一个数已经完整描述了它:

  • noul = 0.95
     意味着"否"的概率是 0.05,把握程度就体现在离 0.5 有多远;
  • 0.95 或 0.05:非常确定;
  • 0.70 或 0.30:倾向明显,但不确定;
  • 0.5 附近:两种可能差不多。

所以你不需要一个单独的 confidence,而是直接用两个阈值划出三个区间。

三个区间,三种行为

一个实用的起点:

区间
行为
高
自动执行。模型判断清晰,无需人工介入。
中
谨慎执行。答案合理但不确定:请用户确认、标记待复核,或者先补充信息再行动。
低
不执行。转人工、请用户澄清,或者交给另一个系统(比如推理更强但更慢的大模型)。

区间边界画在哪里,取决于风险。

阈值随风险变化:把风险偏好写进代码

回到开头那个银行 App。同一个模型、同一套问题,三种动作三种门槛:

同一个模型,三种动作,三种门槛

同一个模型,三种动作,三种门槛


python

UNSURE = 0.5          # 低于它:模型拿不准,不猜

CONFIRM_FREE = 0.9    # 高风险操作免确认所需的置信度

 

def handle(user_message: str, account_id: str) -> None:

    resp = client.system_one(

        state=user_message,

        questions={

            "action": Choice(

                instructions="用户想做什么?",

                criteria={

                    "check_balance": "查询账户余额或近期交易",

                    "approve_transfer": "确认或授权一笔待处理的转账",

                    "freeze_card": "挂失或冻结银行卡",

                    "support": "咨询问题或寻求帮助",

                },

            ),

        },

    )

    action = resp.answers["action"]

 

    if action.confidence < UNSURE:

        # 模型确实拿不准,不要猜

        route_to_human(user_message)

 

    elif action.choice == "check_balance":

        # 低风险:只读操作,展示错了也能挽回

        show_balance(account_id)

 

    elif action.choice == "freeze_card":

        # 冻结宁可错冻:漏掉一个真实挂失的代价更高

        freeze_card(account_id)

        notify_user(account_id, "已为您临时冻结银行卡,如非本人操作可一键解冻。")

 

    elif action.choice == "approve_transfer":

        if action.confidence >= CONFIRM_FREE:

            # 高风险、高置信:仍然走一次确认,但流程更短

            confirm_then_execute(account_id)

        else:

            # 高风险、中等置信:先让用户明确确认

            ask_user_to_confirm(account_id)

 

    else:

        route_to_support_bot(user_message)

三个值得注意的地方:

  1. freeze_card 的门槛反而最低。
     因为它"做了可以撤销,不做可能损失更大"。这不是模型在权衡,是你在代码里表达风险偏好。
  2. approve_transfer 即使在最高置信度下也要确认一次
    ,只是流程更短。动钱的操作,再确定也让人点一下。
  3. UNSURE 这条线是硬的。
     模型明确说没把握时,不许猜——这是整段代码里唯一一个跟业务无关的规则。

模型只负责如实报告它有多确定,风险偏好完全由你的代码表达。 这是整个设计里最舒服的一点:想改策略,改常量,不用动模型。

校准是"一群预测"的性质,不是一个答案的保证

象信经过训练,它的概率是校准的:在所有它给出约 0.8 的答案里,大约 80% 是对的。

这句话有三层意思,缺一层就容易误用:

  • 校准是对一批预测的统计性质。
     它不保证任何单个答案正确。0.95 的答案仍然可能错。
  • 校准依赖数据分布。
     在我们的评测集上校准良好,不代表在你的业务数据上同样好——领域术语、行文风格、中英混排程度都会影响。
  • 所以阈值必须用你自己的数据来定。

第三条最容易被跳过,但它决定系统能不能上线。

阈值怎么定:分桶看一眼

做法不复杂:拿一两百条已知正确答案的样本跑一遍,按置信度分桶,看每个桶的准确率。

python

from collections import defaultdict

 

# samples: [(state, 正确答案), ...],resps: 对应的响应

buckets = defaultdict(lambda: [0, 0])  # 置信度桶 → [答对数, 总数]

for (state, truth), resp in zip(samples, resps):

    ans = resp.answers["action"]

    b = min(int(ans.confidence * 10), 9) / 10   # 0.0, 0.1, …, 0.9

    buckets[b][0] += int(ans.choice == truth)

    buckets[b][1] += 1

 

for b in sorted(buckets):

    right, total = buckets[b]

    print(f"置信度 ≥ {b:.1f}: 准确率 {right / total:.0%}({total} 条)")

然后找"准确率达到业务要求"的那条线:

  • 你要求自动处理准确率 ≥ 98%?找到 98% 对应的最低置信度当阈值;
  • 那条线以上能覆盖多少流量?如果只覆盖 5%,说明要么模型不合适,要么问题拆得不够细。

这个"阈值—覆盖率—准确率"的三角,才是上线时要做的那个决定。

官方那个预解析取值的实测就很有代表性:阈值 0.60 时自动通过 108 个全部正确,剩 26 个转人工——覆盖率 80% 左右,准确率 100%。这比"全部自动处理、准确率 95%"要好用得多,因为人工队列的规模是可控的。

两个容易踩的工程细节

1. 近似确定,不是逐位确定

象信不采样,同样的请求会得到几乎相同的结果。但推理用低精度浮点并与其他请求批量计算,所以同一个请求重复调用时,中间区间的概率会小幅波动。接近 0 或 1 的值(如 0.99)则非常稳定。

实测示意(同一请求重复 10 次):

问题
最小值
最大值
是否紧急(接近饱和)
0.99
0.99
是否为功能建议(中间区间)
0.80
0.83

所以:

  • 单元测试里不要写assert noul == 0.81,改成 assert noul > 0.7;
  • 不要把阈值恰好设在你观察到的某个值上
    (看到 0.62 就把阈值设成 0.62),要留余量;
  • 落在阈值附近的样本,本来就应该按"不确定"处理。

2. 调好阈值后,固定模型版本

xiangxin-latest 这个别名会随新版本发布而移动。你已经针对某个版本调好的阈值,在新版本上不一定成立。

python

# 阈值调好之后

resp = client.system_one(state=..., questions=..., model="xiangxin-2.0.0")

响应里的 model 字段总是报告实际作答的版本号,建议写进日志——将来追溯"这个结果出自哪个版本"时,你会需要它。

今天就试一次

如果你现在有任何一个"靠模型判断 + 靠人抽查"的流程,可以这样开始:

  1. 给答案加上阈值分支。
     先不管阈值是否最优,先把"高 / 中 / 低"三档的行为写出来;
  2. 跑 100 条带标注的数据。
     用上面那段分桶代码,看每个桶的准确率;
  3. 改阈值,看人工队列的变化。
     你会很快找到一个"覆盖率够高、准确率也够用"的点;
  4. 把阈值集中放在一个文件里。
     这是最需要人审阅的部分。

最后一条经验:先假设你的阈值是错的,然后去证明它。 文档里的数字都是示意,包括这篇文章里的。

下一篇:《让 DeepSeek 教会它,别让 DeepSeek 次次来》——聊聊怎么把高频判断"固化"下来。


动手入口

  • 领免费额度(注册送 ¥5):console.xiangxinai.cn
  • 置信度详解:confidence
  • 置信度门控路由:patterns/confidence-routing
  • Noul 怎么用两个阈值:Noul 是非题
  • 哪些地方会抖:已知短板 · 近似确定

数据说明:分桶示例与阈值区间为方法演示,不是效果承诺;请用你自己的标注数据确定阈值。概率抖动幅度来自官方实测,具体数值随任务与版本变化。

【声明】内容源于网络
0
0
象信AI
让人放心把真实工作交给AI
内容 147
粉丝 0
象信AI 让人放心把真实工作交给AI
总阅读2.4k
粉丝0
内容147