把模型换版纳入数据库迁移式管理
文章指出模型别名漂移或版本升级可能改变输出结构,使下游系统在代码未变时出现故障。建议将逻辑任务与固定模型快照集中映射,并把配置纳入Git审查,按迁移方式管理升级。
AI 深度解读
模型升级只改一行配置,也可能把线上接口改坏。作者见过的典型故障是:把 gpt-4o 换成 gpt-4o-2024-08-06,CI 全绿,三天后下游却被 priority: null 弄崩了。提示词和业务代码都没动,变的是模型行为。
作者的主张很明确:把模型升级当成数据库结构迁移,先扩展兼容,再迁移流量,最后收掉旧逻辑。对用 AI 做信息提取、自动处理工单或调用工具的程序员,这篇最有用的地方,是把「换个模型试试」拆成一套能观测、能回退的发布流程。
为什么固定模型版本还不够
提供商通常同时提供浮动别名和带日期的快照。claude-3-5-sonnet-latest、-latest 或不带日期的模型系列名,都可能指向新的版本;日期快照能固定目标,但也会退役,并不是永久可用。
作者建议把版本集中到一个注册表,通过 extract-ticket、summarize-thread 这样的逻辑任务名查找模型,避免调用点各写一份。注册表进入 git,模型调整才有明确的审查记录和回退入口。
还要把模型、提示词哈希(prompt hash)和输出结构版本放在同一条记录里。原文的 ModelPin 用 task、model、schema_version、prompt_sha 表达这组关系,示例绑定的是 gpt-4o-2024-08-06、结构版本 2 和提示词哈希 9f3c1a。
原因很实际:回退模型,却继续使用新版提示词或解析器,不一定能恢复原来的行为。缓存也必须跟着这组配置走。
下面的缓存键函数把模型、结构版本、提示词哈希和输入一起计算,防止回退后继续命中新版本生成的答案:
def cache_key(pin: ModelPin, user_input: str) -> str:
# 缓存必须包含版本配置,不能只包含输入,否则回退后仍会读到被撤回模型生成的答案。
raw = f"{pin.model}|{pin.schema_version}|{pin.prompt_sha}|{user_input}"
return hashlib.sha256(raw.encode()).hexdigest()
要看的是 raw 那一行:即使用户输入相同,部署组合变化也会进入不同的缓存空间。这是原文代码节选,依赖其中定义的 ModelPin 和导入的 hashlib。
输出能解析成 JSON,为什么业务还会出错
如果代码执行 json.loads(resp.content) 后就把字典交给下游,模型输出实际上已经成了业务 API。
问题不只在 JSON 语法。枚举可能从 low|normal|high 增加一个 urgent;缺失值可能变成 null、"null"、"N/A" 或直接缺少字段;数字 3 可能变成字符串 "3"。Pydantic 的宽松模式可能自动转换类型,让变化暂时藏起来。
还有两种失败容易被重试掩盖:安全拒绝可能以 HTTP 200 和一段自然语言返回;输出达到 max_tokens 后可能被截断,看着接近合法 JSON,却并不完整。作者因此要求先检查 finish_reason == "stop",把 "length" 当错误处理,避免无限重试。
作者给出的兼容读取器,演示了如何把两种输出结构归一成同一个下游接口:
def read_ticket(resp_content: str, finish_reason: str) -> dict:
if finish_reason != "stop":
raise ValueError(f"non-stop finish: {finish_reason}") # "length" 表示被截断
payload = json.loads(resp_content)
# 扩展阶段:同时接受 v1 和 v2,记录实际结构,之后再收缩。
if payload.get("schema_version") == 2:
log.info("ticket_schema_seen", extra={"shape": "v2"})
return {"title": payload["summary"], "priority": payload["priority"]["level"]}
log.info("ticket_schema_seen", extra={"shape": "v1"})
return {"title": payload["title"], "priority": payload.get("priority", "normal")}
关键在 schema_version 分支:新版的 summary 和嵌套的 priority.level,被转换成旧接口认识的字段。发布顺序必须是先上线兼容读取器,再切换模型配置,等日志确认旧结构消失后才删除旧分支。
但这段代码只是迁移示例。payload.get("priority", "normal") 只能处理字段缺失,无法把显式的 null 转成 "normal";未知版本也会落进旧版分支。真正上线还需要补齐校验和异常处理。
怎么确认升级划算,又不让试跑产生真实操作
作者建议先跑影子流量(shadow traffic):抽取线上输入或回放历史输入,让候选模型并行处理,再比较结果。
样本量要提前定。想判断失败率是否变化了 2%,少量样本不足以下结论;看到一次结果不错就停,也会放大偶然性。原文没有提供所需样本数或具体计算方法。
成本则要按「解决一个任务」计算。新模型即使单 token 更便宜,如果输出 token 多了 40%,再叠加更多重试,最终仍可能更贵。应同时看任务成功率、重试率、输出 token 比例、结构违规率,以及每个已解决任务消耗的 token。用 LLM 当裁判时,还要先与人工标注集校准,报告一致率。
模型可以切回去,已经发出的邮件和退款不会自动撤销。
因此,候选模型应进入 outbox 或 dry-run 路径,影子流量不能真实写入。会产生副作用的工具调用要有幂等键,并按逻辑操作生成,不能每重试一次就换一个键;还需要独立于模型配置的停止开关,必要时冻结新任务。
Embedding 升级也有单独的迁移成本。不同代模型可能改变维度、归一化方式和邻域结构,即使维度相同,也不能据此认定向量可比。作者建议用 docs_v2_<model>_<dims> 给索引命名,回填期间双写,完成后切读,最后删除旧集合,并提前估算全量重新生成向量的成本。
我的判断
我认同把模型行为纳入接口契约,尤其是输出会进入工单、业务分支或工具执行链时。比起升级后观察几天,提前准备兼容读取器和失败指标,能让问题更容易定位。
但原文是一套工程经验,没有提供升级前后的对照实验。2% 和 40% 都是说明评估陷阱的例子,不能当成作者测出的收益或损失。
兼容也有边界。读懂旧结构,不代表应该接受任意新枚举、任意空值。能明确归一的差异可以兼容,无法判断的结果应该进入可观测的失败路径。直接拒绝不合法输出更简单,代价是迁移期间更容易中断任务,团队需要按业务容错能力取舍。
finish_reason == "stop" 同样应结合所用 API 理解,不能未经区分套到所有工具调用和响应类型。原文没有展开这些差异。
作者建议 CI 使用固定快照录制的 fixtures,能让契约测试稳定、可重复;不过,它只能检验已记录样本,无法证明提供商当前的线上行为。真实候选模型的影子评估仍然需要保留。
能拿来做什么
- 给现有 AI 调用建立统一注册表,把模型快照、提示词哈希和结构版本一起审查,并让缓存键包含这组配置。
- 对信息提取结果增加统一解析层,显式处理空值、未知枚举、拒绝和截断;需要改结构时,先发布兼容读取器。
- 升级前用预先确定的样本比较候选版本,报告每个成功任务的 token 消耗、重试率和结构违规率,别只看单价或响应速度。
- 如果 AI 会发邮件、创建工单或执行退款,先准备 dry-run、幂等键和停止开关,并分别写清模型回退与副作用修复流程。