用输出校验驱动大小模型分级调用
文章用约 70 行 Python 演示先调用低价模型,输出未通过代码校验时再升级到强模型,适配 OpenAI 兼容接口。示例以工单分类和字段提取为场景,强调输出必须具备可校验的约束。
AI 深度解读
给客服工单分类、从邮件里提取字段,这些任务真的需要每次都调用昂贵模型吗?作者给出的方案很直接:先让便宜模型回答,用代码检查结果,只有没过关才升级到强模型。约 70 行 Python,就能搭出一条简单的模型路由。
但省钱只是其中一部分。这个方案是否可靠,取决于你能否识别那些「格式完全正确,内容却错了」的回答。对正在做 AI 分类、信息提取或结构化输出的团队,这比直接照搬代码更值得看。
便宜模型先上,关键是答案能不能验
原文用客服工单演示:输入一段自由文本,输出包含 category、priority、order_id 和 confidence 的 JSON。
其中,分类只能取 billing、delivery、refund、technical、other;优先级限定为 low、normal、high;订单号必须符合 ^[A-Z]-\d{5}$,或者为 null;置信度则是模型自己报告的 0 到 1 之间的数字。
这些限制让输出具备了检查条件。代码可以确定某个分类是否越界、订单号格式是否正确,却无法仅靠这些规则判断:用户明明在申请退款,模型为什么给了 delivery。
能验证格式,是模型路由的起点;能验证业务含义,才决定它能走多远。
作者通过 OpenAI SDK 接入兼容端点,使用 LLM_BASE_URL、LLM_API_KEY 配置连接。CHEAP_MODEL 默认是 deepseek/deepseek-v4-flash,STRONG_MODEL 默认是 anthropic/claude-sonnet-5,名称采用作者所在网关的 provider/model 格式。
请求调用 client.chat.completions.create,设置 temperature=0、max_tokens=200 和 response_format={"type": "json_object"}。任务只需要标签,限制输出长度既减少无关解释,也限制输出费用。作者同时提醒,不同模型和供应商对 JSON 模式的支持不同,仍然需要自己的校验器。
下面这段决定了哪些结果必须被拦住:
def validate(raw: str):
try:
d = json.loads(raw)
except (json.JSONDecodeError, TypeError):
return None, "not json"
if d.get("category") not in CATEGORIES:
return None, "bad category"
if d.get("priority") not in PRIORITIES:
return None, "bad priority"
oid = d.get("order_id")
if oid is not None and not (isinstance(oid, str) and ORDER_RE.match(oid)):
return None, "bad order_id"
conf = d.get("confidence")
if not isinstance(conf, (int, float)) or not 0 <= conf <= 1:
return None, "bad confidence"
return d, None
要看清 d.get("category") 这一行的边界:它检查分类是否在允许集合中,不检查分类是否符合工单内容。
升级逻辑很短,置信度却不能直接相信
校验器后面接的是顺序尝试:便宜模型先回答,失败后再调用强模型;两次都失败,返回空结果供后续处理。
def label(ticket: str, min_conf: float = 0.7):
log = []
for model in (CHEAP, STRONG):
raw, usage, secs = call(model, ticket)
data, err = validate(raw)
if err is None and model == CHEAP and data["confidence"] < min_conf:
err = f"low confidence {data['confidence']}"
log.append({"model": model, "error": err, "secs": round(secs, 2),
"in": usage.prompt_tokens, "out": usage.completion_tokens})
if err is None:
return data, log
return None, log # 两次都失败:转入人工队列
重点是包含 model == CHEAP 的判断:默认 min_conf=0.7 只约束便宜模型。强模型只要通过校验,就会被接受,即使它自报的置信度很低。
这是作者选择的策略,并非可靠性的保证。自报置信度只是模型对自己答案的评价,不是经过校准的正确率。作者认为它在实践中能拦下一些猜测,但没有提供样本量、准确率或阈值对照结果。
因此,作者建议先拿几百条真实工单,同时运行两个模型并保存答案,暂时不要切换线上路由。观察便宜模型通过校验后,与强模型有多少分歧,再画出准确率与自报置信度的关系;如果两者没有关系,就去掉置信度门槛。
日志必须记录每次尝试,包括模型、错误原因、耗时,以及 usage.prompt_tokens、usage.completion_tokens。只保存最终答案,会丢掉判断升级率所需的信息。
省钱要算两次调用,还要算等待时间
设便宜调用的平均成本为 c,强模型为 s,升级比例为 p。路由后的单次请求期望成本是:
c + p * s
一直使用强模型的成本是 s,所以省钱条件为:
p < 1 - c / s
升级请求已经花过一次便宜模型的钱,不能只统计最终使用的强模型。
作者引用了某俄罗斯网关在 2026 年 10 月 23 日的目录价,单位为卢布/百万 token,输入、输出分别为:DeepSeek V4 Flash 11.5 / 22.5,Gemini 3.7 Flash 51.5 / 257,Claude Sonnet 5 274.5 / 1,370.5。按文中的短分类调用估算,最便宜与最强模型的价差超过 20x,成本盈亏平衡对应的升级率超过 95%。
这些是特定网关的报价,不能直接作为自己的预算。需要代入实际供应商价格和真实输入、输出用量。
更大的约束是延迟和错误。升级请求要串行等待两轮调用;便宜模型答错却通过校验的请求,则会直接到达用户。
作者主张从分歧里补业务规则:订单号是否存在于数据库,退款工单是否缺少订单号,优先级是否与「urgent」等词冲突。这些规则比继续往系统提示词里加段落更容易检查和维护,但仍要符合自己的业务定义。
我的判断
我认同它把注意力放在校验器和日志上。对于标签、字段提取、可试运行的 SQL、可编译的代码,规则能筛掉一部分明确错误,也能帮助团队找到小模型的薄弱环节。
不过,原文展示的是实现模式和成本推导,没有给出上线一周后的真实升级率、错误率或延迟对照。「置信度能拦住猜测」「收紧校验优于调整提示词」都应视为经验建议。
还有一个评估问题:作者把两个模型的分歧称为静默错误,但强模型也可能错。分歧只能定位待复核样本;两个模型一致,也不能证明答案正确。要计算准确率,仍需要可信的标注或业务结果。
代码也只是骨架。合法 JSON 可能是数组或 null,直接调用 d.get 会出错;Python 的布尔值也会通过这里的数字类型判断。接口超时、限流等异常没有进入回退流程,末尾的人工队列也只有注释,尚未实现。
对于客户回复、开放式摘要,作者建议按任务类型预选模型并离线评估,我认为这个边界合理。编译通过只能证明代码满足部分语法约束,JSON 合法也只能证明结构过关,不能替代任务质量评估。
能拿来做什么
- 从工单分类、邮件字段提取这类结构清晰的任务开始,先写出允许值、字段类型和业务约束,再判断是否适合加入模型路由。
- 在现有 Node 服务中用几百条真实请求做离线对照,保存两份答案和调用记录,并人工复核关键分歧,避免直接把强模型答案当作标准答案。
- 上线前用实际 token 用量计算
c、s、p,同时观察升级请求的耗时,确认节省费用后仍能满足接口响应要求。 - 如果把模式用于 AI 编程,把编译或类型检查当作一道检查,再配合能验证需求的测试;同时明确两次失败后的人工处理入口。