把高频小判断从大模型里拆出来:自建 200 条标注集,定阈值,留回退
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 条,全过才算接完:
- 标注集必须是你自己的真实样本,随机抽,不能挑。 用合成数据或者”好做的样本”定出来的阈值,上线后一定会偏低。
- 60 条测试集一次都不能碰。 定阈值只用那 140 条。测试集只在最后验一次,用来报告真实错误率。
- 回退率要记进监控。 上线后如果回退率比基线高出 15 个百分点以上,说明样本分布漂移了,标注集要重抽,不是调阈值能解决的。
- 成本和准确率要同时报。 只报准确率是自欺欺人——把阈值拉到 0.99 能把错误率压到 1%,但 90% 的流量都走了回退,等于没省钱。两个指标必须成对出现在你的复盘表里。
- 保留一键切回原方案的能力。 用环境变量或者配置开关控制走哪条路,不要改代码。决策模型这个品类现在厂商进进出出,接口名字都不统一(有的叫
/v1/decisions,有的叫/v1/systemone),能整段换掉比什么优化都重要。
几个坑
- 别用公开榜单的分数当自己的预期。 厂商公布的准确率是在公开基准上测的,和你的业务分布两回事。85.71% 这种数字只有在你自己的 200 条上复现出来才有意义。
- 批量接口有上限。 各家的批处理条数不同,超了会报错。上面的脚本按 32 条一批,换成你选的服务之前先查文档。
- 限流会卡住批量任务。 决策模型给的单组织 QPS 通常不高,一次性给全量商品打分会被限流卡死。大批量任务要么排队,要么干脆自托管。
- 别忘了输出也是要算钱的。 有些服务输出免费,有些照收。签之前把输入输出两条价格都看一遍。
- 阈值不是设一次就完事。 业务变了、季节变了、上游数据变了,分布就漂。建议每季度用新样本重跑一次基线,把结果带日期存下来。
最后一句
这套东西真正的收益不是省钱,是把”判断”这件事从黑箱里拿出来变成可调的参数。省下的 token 费大概只占三成,另外七成来自终于知道自己的流程在哪些地方会判错——这个信息以前埋在日志里,没人去看。