一个系统最初用某个模型跑分类任务,后来因为成本上涨要换成更便宜的小模型。评估阶段只看了一件事:准确率掉了两个百分点,能不能接受。结论是能接受,于是准备切换。真正开始改的时候,发现提示词里有一堆为该模型调过的措辞、输出格式的解析依赖它的某种习惯、错误重试策略按它的超时特性设计——改动量远超预期。
模型不是可替换的组件,它更像一个行为特性未知的外部服务:同样的接口,行为不同。所以"换模型"这件事的成本,主要不在接口层,而在那些围绕旧模型行为建立的隐性适配。
1. 切换的三种范围
| 范围 | 改动 | 风险 |
|---|---|---|
| 单点替换(同一提示词换模型) | 只改配置 | 中(行为差异未测) |
| 适配替换(改提示与解析) | 提示词、解析、重试 | 高 |
| 架构替换(改流程与分工) | 拆成多步、换调用结构 | 最高 |
判断自己在做哪一种,有个很直接的检查:把模型名换成另一个字符串,看有多少处需要改。只有一处(配置),是第一种;超过三处,说明适配已经分散在代码里。
多数团队以为自己在做第一种,实际上做的是第二种——因为提示词、解析代码、重试参数都是围绕旧模型写的,而它们散落在代码里,不在"模型配置"这个可见的位置。
2. 提示词里的隐性绑定
| 绑定形态 | 例子 |
|---|---|
| 措辞习惯 | "请用简洁的短句回答"(某模型需要,另一个会过度省略) |
| 格式诱导 | "只输出 JSON,不要有其它文字"(某模型能遵守,另一个会加前缀) |
| 示例数量 | 给三个示例才稳定(换模型可能一个就够,或更多) |
| 分隔符偏好 | 用 ### 分段 |
| 拒绝触发词 | 某些措辞会触发额外审查 |
第二项最容易造成线上故障:解析器假设"输出是纯 JSON",而新模型在 JSON 前加了一句"好的",整个链路解析失败。这类失败在测试集上可能只出现一两次,上线后放大到几个百分点。
这三项验证可以写成一个小脚本,跑两百次调用统计分布:
// 换模型前的分布验证:格式、长度、失败率
for i in $(seq 200); do
curl -s "$API" -d "{\"prompt\": \"$PROMPT\"}" | jq -r '.text' >> out.txt
done
grep -c '^{' out.txt // 纯 JSON 的行数
awk '{print length}' out.txt | sort -n | awk '{a[NR]=$1} END {print "p50:",a[int(NR/2)],"p99:",a[int(NR*0.99)]}'
grep -c '^$' out.txt // 空输出数量
// 换模型前必须验证的三件事
1. 输出格式的稳定性:连续 200 次调用里,纯 JSON 的比例是多少
2. 长度分布:输出长度的分布是否与旧模型接近(影响下游截断)
3. 失败模式:超时、截断、空输出的比例分别是多少
3. 重试与超时参数需要重估
| 参数 | 旧模型下合理 | 换模型后可能失效 |
|---|---|---|
| 超时 | 按旧模型的 P99 设 | 新模型更慢则频繁超时 |
| 重试次数 | 按旧模型的抖动设 | 新模型限流更严则无意义 |
| 并发上限 | 按旧模型配额设 | 新模型配额更小 |
| 退避策略 | 按旧模型的错误码设 | 错误码含义可能不同 |
第一项最实际:超时值是围绕旧模型的响应分布设的,换模型后这个分布变了,超时值就变成了随机丢弃请求的装置。
// 换模型时参数要重新标定,而不是沿用
model:
name: "MODEL_NEW"
timeout_ms: 12000 // 按新模型 P99 × 1.3 重算,不要沿用旧值
max_concurrency: 8 // 按新模型配额上限的 70%
retry:
max_attempts: 2
backoff_ms: [300, 900] // 按新模型的限流响应调整
4. 成本账要按"任务"算而不是按"token"算
| 计价方式 | 容易忽略的部分 |
|---|---|
| 按 token | 重试与失败调用的 token 也算钱 |
| 按任务 | 需要成功率才能折算 |
| 按成功任务 | 最接近真实成本 |
第三种最有用:同样完成一个任务,不同模型需要的步数与重试次数不同,按 token 单价比较会得出错误结论。一个单价更低但需要两次重试的模型,实际成本可能更高。
// 单任务成本的三项
成本 = (首次调用 token 成本 + 重试 token 成本 + 工具/检索成本) / 成功率
// 只比较第一项,会系统性低估"不稳定但便宜"的模型
切换时间点的选择也有讲究:不要与其它变更同期做。模型切换会同时改变延迟、格式与成本三个维度,如果同时上线了别的功能,任何异常都无法归因。
5. 灰度切换的方式
| 方式 | 作用 | 风险 |
|---|---|---|
| 按流量比例分流 | 对比两模型表现 | 需要双份配额 |
| 按任务类型分流 | 低风险任务先切 | 效果代表性有限 |
| 影子调用(只记录不采用) | 收集差异样本 | 成本翻倍 |
| 直接切换 | 快 | 无退路 |
影子调用还有一个副产品:差异样本本身就是回归测试集。把影子期间发现的典型差异保存下来,下次换模型时直接用这套样本做验收,比重新设计测试集可靠得多。
第三种在"不确定差异在哪"时最有用:两个模型都调用,只用旧模型的输出,同时记录两者的差异。跑几天就能拿到真实的差异分布,而不是靠离线测试集。
6. 排错:五种"换了模型效果变差"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 解析失败率上升 | 输出格式不稳定 | 加宽容解析或强格式约束 |
| 延迟变高 | 超时与并发参数未重估 | 按新模型分布重设 |
| 成本没降 | 重试次数增加 | 按成功任务成本重算 |
| 某些任务变好、某些变差 | 任务类型差异 | 按类型分流 |
| 输出内容风格变了 | 提示词绑定旧模型 | 重写提示并复测 |
分类型评估的最小做法是给每个子类单独设阈值,而不是共用一个总分:
// 子类评估表:只有全部子类达标才允许切换
category old_acc new_acc threshold pass
退款咨询 0.92 0.90 ≥0.88 yes
物流查询 0.88 0.94 ≥0.88 yes
投诉升级 0.85 0.71 ≥0.85 NO ← 平均被掩盖的下降
整体 0.89 0.88
第四项常被忽略:整体准确率没变,不代表每个子类都没变。某个子类可能大幅改善,另一个大幅下降,平均下来互相抵消。所以切换评估要按子类看,而不只看总分。
7. 边界:什么情况可以简单替换
| 场景 | 为什么 | 做法 |
|---|---|---|
| 结构化抽取(格式约束强) | 行为差异小 | 直接替换 + 抽样验证 |
| 内部试验、无外部用户 | 失败代价低 | 直接换 |
| 已有严格输出校验与重试 | 差异被兜住 | 换后观察错误率 |
| 任务极简(分类、路由) | 对模型能力要求低 | 用小模型,成本优先 |
还有一条与组织有关的边界:切换决策应当由"承担失败后果的人"签字。评估者往往看到的是准确率,而承担后果的是客服、是业务方。让后者确认"这个子类的下降我能接受",比让前者解释平均分更有意义。
判断口径一句话:围绕旧模型的行为,我写了多少适配代码。几乎没有,替换是配置问题;写了不少,替换就是一次改造项目。
8. 自测
- 为什么说模型"不是可替换的组件"?成本主要落在哪一层?
- 提示词里的隐性绑定有哪五种形态?哪一项最容易造成线上故障?
- 超时值为什么必须在换模型后重算?沿用的后果是什么?
- 为什么按 token 单价比较会系统性低估某些模型?单任务成本的三项是什么?
- 影子调用解决了什么评估问题?它的代价是什么?
- "整体准确率没变"为什么可能掩盖严重问题?评估应该按什么维度看?