版本、评估与回滚
前置为预算篇与 Python 集合。一次 AI 应用发布可能没有改代码,却改了提示词、模型、schema、工具或索引。仅记录 Git 提交无法重建这次行为。用发布清单把这些版本绑定为一个不可变集合,任务轨迹保存清单 ID。
从总分拆到失败切片
分类任务100条中95条正确,可能把5条高风险退款全错分。总体准确率掩盖了业务风险。分别保留普通、歧义、拒答、权限、长输入和工具失败样本。训练/调提示使用开发集,发布比较使用未参与优化的保留集;若反复根据保留集修改,它也变成开发数据,需补新的盲测样本。
下面实验只验证发布门槛计算,不声称测过真实模型。两个候选在相同输入上比较;安全项一旦失败,平均分再高也不能通过。
cases = [
{'id': 'normal', 'critical': False, 'old': True, 'new': True},
{'id': 'ambiguous', 'critical': False, 'old': False, 'new': True},
{'id': 'cross-tenant', 'critical': True, 'old': True, 'new': False},
]
def gate(rows):
if not rows or len({r['id'] for r in rows}) != len(rows):
raise ValueError('empty or duplicate evaluation')
regressions = [r['id'] for r in rows if r['critical'] and not r['new']]
old = sum(r['old'] for r in rows) / len(rows)
new = sum(r['new'] for r in rows) / len(rows)
return {'release': not regressions and new >= old,
'old': old, 'new': new, 'blocked': regressions}
result = gate(cases)
assert result['release'] is False
assert result['blocked'] == ['cross-tenant']
assert result['old'] == result['new']
print('equal average, blocked release')保存为 release.py,输出 equal average, blocked release。同样的平均值代表不同错误分布。任务完成率、正确引用、成本与延迟分别统计;模型裁判需要与人工标注校准,不能自己生成答案再自己宣布正确。小数据集的比例波动很大,不能把一次提升当显著改进。
灰度保持身份稳定
先影子运行:用真实请求的脱敏副本评估候选,禁止候选执行写工具。再对稳定用户分桶灰度,让同一会话保持同一清单,避免半程切换模型或 schema。设置错误、成本、超时及质量反馈阈值;回滚将新任务切回旧清单,正在执行的任务继续用原清单,或显式进入兼容恢复流程。
事件 schema 的旧版本必须能读。例如 v1 用 text,v2 用 answer,在读入边界适配为内部字段,保留原版本以审计;不要直接重写历史事件。工具幂等键也不能因换版本而意外重置,否则旧任务恢复会重复执行。
运行日志也有数据边界
trace 保存任务 ID、发布清单、阶段、耗时、token 用量、错误类别和工具审批结果即可支撑大多数定位。默认不把完整提示、密钥或用户原文写到共享日志;需要抽样排错时按权限、保留期与脱敏规则控制。脱敏字段名不能保证字段值安全,异常消息也可能包含原始输入。
部署从单进程起步:当任务超出请求寿命,加入队列与可查询状态;当下游限流独立,分开 worker;当数据必须驻留本地,选择满足边界的模型服务。服务拆分是对状态、吞吐和责任的响应,不能仅凭“AI应用”三个字决定微服务。
练习
编写 read_answer(event):v1 必须读取字符串 text,v2 读取字符串 answer;未知版本和字段类型错误都拒绝。验证空字符串允许,数字不允许,并证明输入事件没有被修改。解释为何迁移历史记录前必须保留旧读取器。