把高频小判断从大模型里拆出来:自建 200 条标注集,定阈值,留回退

2026-10-02 Skills成本工程实践分类路由

agent 里有一类调用特别浪费:不需要模型写东西,只需要它选一个。

客服工单该分给谁、这封邮件是不是投诉、要不要调搜索工具、下一步走 A 分支还是 B 分支。这些判断你最后只用得到”是/否”或者一个选项,但聊天模型会先生成一段话,你再解析回来——为一段注定被丢掉的文字按 token 付钱。

决策模型(decision model)就是冲着这个来的:给它一组你预先定义好的答案,它直接返回每个选项的概率。不生成文本,就不用为文本付费。

这篇讲怎么把它接进现有流程,而且要接得能撤回。

第一步:先证明这件事值得做

别急着换模型,先把你的调用分布摸出来。取最近 7 天的生产日志,按用途分个类,看有多少次调用属于”只需要一个选项”。

一个粗略的判断线:如果这类调用占到你总调用次数的 20% 以上,或者每月在这上面的花费超过你模型账单的 15%,就值得拆。

低于这个比例,先别动。改造本身的成本、多出来的运维面、两套模型的灰度逻辑,加起来不一定划算。

第二步:抽 200 条真实样本做标注集

这一步没有捷径,也不能用合成数据糊弄。

  • 从生产日志里随机抽 200 条,不要挑好做的。
  • 人工标一遍,标的是真实结果,不是”模型应该输出什么”。
  • 按真实分布分层。如果实际场景里”退款”占 5%,标注集里就别把它提到 30%。
  • 留 60 条做测试集,140 条用来定阈值。

200 条是个经验下限。少于这个数,阈值定出来不稳定;多于 500 条,边际收益就开始掉。

第三步:跑基线

下面这个脚本做的事:把 140 条样本批量发给决策端点,拿回概率分布,然后扫一遍阈值,找出每个阈值下”自动处理的比例”和”错误率”。

// scripts/decision-baseline.mjs
// 用法:node scripts/decision-baseline.mjs samples.jsonl
// samples.jsonl 每行:{"text":"...","label":"refund"}
import { readFileSync } from 'node:fs';

const ENDPOINT = process.env.DECISION_ENDPOINT; // 换成你选的服务端点
const KEY = process.env.DECISION_KEY;

const rows = readFileSync(process.argv[2], 'utf8')
  .split('\n').filter(Boolean).map(JSON.parse);

// 批量提问:一次请求里塞多条带名字的问题,字段名以各家文档为准
async function ask(batch) {
  const res = await fetch(ENDPOINT, {
    method: 'POST',
    headers: { 'content-type': 'application/json', authorization: `Bearer ${KEY}` },
    body: JSON.stringify({
      questions: batch.map(s => ({
        name: s.id,
        kind: 'choice',
        options: s.options,
        text: s.text,
      })),
    }),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return (await res.json()).results;
}

const results = [];
for (let i = 0; i < rows.length; i += 32) {
  results.push(...await ask(rows.slice(i, i + 32)));
}

// 扫阈值:confidence 低于阈值的走人工/回退
console.log('threshold\tauto%\terror%');
for (const t of [0.5, 0.6, 0.7, 0.8, 0.9, 0.95]) {
  let auto = 0, err = 0;
  for (const r of results) {
    const top = r.distribution[0];
    if (top.p < t) continue;          // 不够确定,交给回退
    auto++;
    if (top.value !== r.label) err++;
  }
  console.log([t, (auto / results.length * 100).toFixed(1), (err / Math.max(auto, 1) * 100).toFixed(1)].join('\t'));
}

跑出来是一张表。读法就一句:找到”自动处理比例 ×(1 − 错误率)“最高的那一档,再往右挪一格。

第四步:按赔多少钱定阈值,不要按准确率

这是整套方法里最容易被跳过的一步。

同样是 90% 准确率,“把退款工单错分成咨询”和”把投诉邮件错分成广告”,代价差两个量级。设阈值前先把每类错误的代价写成一个数:

错误类型单次代价样例
误放行高该退款的没退,客户流失
误拦截中正常工单进了人工队列,多花 5 分钟
路由错低转给隔壁组,对方再转回来

阈值应该定在”期望损失最小”的那一点,不是”准确率最高”的那一点。很多时候最优解是 0.85 而不是 0.95——后者自动处理的比例掉太多,省下的钱还抵不过人工成本。

第五步:留一条回退路径

低置信度的样本不要硬猜,回退给原来的大模型,或者直接进人工队列。这条路径必须在上线前就写好并测过。

回退的意义不只是兜底。它同时也是你的标注集自动扩容器——回退的样本人工标完之后,就成了下一轮的训练/校准数据。

怎么知道自己做对了

5 条,全过才算接完:

  1. 标注集必须是你自己的真实样本,随机抽,不能挑。 用合成数据或者”好做的样本”定出来的阈值,上线后一定会偏低。
  2. 60 条测试集一次都不能碰。 定阈值只用那 140 条。测试集只在最后验一次,用来报告真实错误率。
  3. 回退率要记进监控。 上线后如果回退率比基线高出 15 个百分点以上,说明样本分布漂移了,标注集要重抽,不是调阈值能解决的。
  4. 成本和准确率要同时报。 只报准确率是自欺欺人——把阈值拉到 0.99 能把错误率压到 1%,但 90% 的流量都走了回退,等于没省钱。两个指标必须成对出现在你的复盘表里。
  5. 保留一键切回原方案的能力。 用环境变量或者配置开关控制走哪条路,不要改代码。决策模型这个品类现在厂商进进出出,接口名字都不统一(有的叫 /v1/decisions,有的叫 /v1/systemone),能整段换掉比什么优化都重要。

几个坑

  • 别用公开榜单的分数当自己的预期。 厂商公布的准确率是在公开基准上测的,和你的业务分布两回事。85.71% 这种数字只有在你自己的 200 条上复现出来才有意义。
  • 批量接口有上限。 各家的批处理条数不同,超了会报错。上面的脚本按 32 条一批,换成你选的服务之前先查文档。
  • 限流会卡住批量任务。 决策模型给的单组织 QPS 通常不高,一次性给全量商品打分会被限流卡死。大批量任务要么排队,要么干脆自托管。
  • 别忘了输出也是要算钱的。 有些服务输出免费,有些照收。签之前把输入输出两条价格都看一遍。
  • 阈值不是设一次就完事。 业务变了、季节变了、上游数据变了,分布就漂。建议每季度用新样本重跑一次基线,把结果带日期存下来。

最后一句

这套东西真正的收益不是省钱,是把”判断”这件事从黑箱里拿出来变成可调的参数。省下的 token 费大概只占三成,另外七成来自终于知道自己的流程在哪些地方会判错——这个信息以前埋在日志里,没人去看。