本节摘要:你签合同选中的是模型 A 的能力,但模型即服务的履约物是"名字叫 A 的那个服务端点"——权重可以换、量化可以降、请求可以被路由,而名字不用改。本节先给案例底账(保守口径):2026-09 社区出现"模型探测器"类开源项目(约 159 star,社区)讨论服务方悄悄更换/降级模型;美国也有针对头部实验室的相关民事诉讼——均为指控,尚无裁判结论,本书不做断案。重心放在不依赖任何个案结论的判定方法:一致性判定卡把一轮观测归入六种类型——Match(一致)、Suspicious(可疑)、Downgrade(疑似降级)、Rerouted(疑似路由变化)、Unlisted(未声明版本)、Invalid(判定无效)——每种类型有明确判据与动作,配判定脚本;判定纪律四条(Invalid 优先、时间窗对齐、证据留档、缺席审判禁止);最后把一致性要求写进合同与流程。从此"感觉它变笨了"不再是一句牢骚,而是一张可交给服务方的判定卡。
阅读完本节,你应当能够:
选型报告里写的是"选定模型 A",工程上真正接入的是 api.provider.com/v1/chat 加一个模型名字符串。从名字到答案之间,服务方至少有三个可以随时动而客户端无感知的开关:
| 通道 | 服务方能做什么 | 你的损失 |
|---|---|---|
| 换权重 | 名字不变,底层换成旧版/小杯/蒸馏版 | 质量下滑,账单没变 |
| 降量化 | 部署精度下调(如 fp16 → int8)省算力 | 边缘题变差,风格大体还在 |
| 改路由 | 按负载/按题目难度把部分请求发给别的模型 | 体验时好时坏,难复现 |
动机也不必都是恶意的:容量压力下的临时切换、灰度发布的分层放量、成本优化的静默上线——每一项在服务方内部都是"运维",在你这边都是"模型变了"。这与 1.1 的利益结构一脉相承:当"模型稳定"没有写进你付钱买的东西里,它就不是默认交付物。
按本书数字属性纪律,这个话题的公开底账记两笔,均不构成结论:
⚠️ 措辞纪律:在裁判结论出现之前,任何"某家确实偷偷换了模型"的断言都是转述指控。你能严谨说出的最强命题是:"我的探针在 X 时间窗内观测到统计显著的行为漂移"——这句话本身就是完整的证据,不需要给服务方定罪就能采取行动。
把观测定性的关键,是先承认**"变没变"不是一个二值问题**——行为漂移有好几种成因与含义。判定卡把一轮观测(题数、探针自检、锚点分变化、风格指纹、双簇信号、元数据变化六个字段,取法见 5.2)归入六种类型:
| 类型 | 中文 | 判据(满足其一) | 动作 |
|---|---|---|---|
| Match | 一致 | 全部信号在噪声带内 | 留档,进入下轮 |
| Suspicious | 可疑 | 有异常信号但不足以定性 | 加密观测一轮,暂不结论 |
| Downgrade | 疑似降级 | 锚点分显著下滑,风格大体还在 | 正式沟通,出示证据包 |
| Rerouted | 疑似路由变化 | 回答分裂成两簇(部分像原模型) | 按题分桶复查,出示两簇样本 |
| Unlisted | 未声明版本 | 元数据变化/版本口径查不明 | 要求确认当前快照与公告 |
| Invalid | 判定无效 | 探针自检失败、题数不足 | 先修探针,再谈模型 |
判定脚本(把优先级固化成代码——团队对同一观测必然得出同一张卡):
# identity_card.py(纯标准库,可直接运行) """模型一致性判定卡:把一轮观测归入六种判定类型之一,并给出对应动作。 观测字段(来自 5.2 的自查流水线): sample_n 本轮探针题数(少于 15 不足以判定) probe_ok 探针自检是否通过(对照题、网络、配额都正常) quality_delta 锚点均分变化(百分点,正数=变好) style_shift 风格指纹是否越限(长度分布/口头禅明显变化) bimodal 回答是否分裂成两簇(部分像原模型、部分像别的) metadata_changed 服务端 model/版本元数据是否发生变化 """ def classify(o): if not o["probe_ok"] or o["sample_n"] < 15: return "Invalid", "判定无效:先修探针(对照题/网络/配额),再谈模型" q, s = o["quality_delta"], o["style_shift"] if abs(q) <= 2 and not s and not o["bimodal"] and not o["metadata_changed"]: return "Match", "一致:无需动作,本轮记录留档" if o["bimodal"]: return "Rerouted", "疑似路由变化:按题分桶复查,向服务方出示两簇样本" if q <= -5: return "Downgrade", "疑似降级:启动正式沟通,出示锚点分与区间" if o["metadata_changed"]: return "Unlisted", "未声明版本:要求服务方确认当前快照与变更公告" return "Suspicious", "可疑:加密观测一轮(题量翻倍、双时间窗),暂不结论" if __name__ == "__main__": cases = [ # 示意:三种典型观测 {"sample_n": 20, "probe_ok": True, "quality_delta": 0.5, "style_shift": False, "bimodal": False, "metadata_changed": False}, {"sample_n": 20, "probe_ok": True, "quality_delta": -7.0, "style_shift": False, "bimodal": True, "metadata_changed": False}, {"sample_n": 10, "probe_ok": True, "quality_delta": -9.0, "style_shift": True, "bimodal": False, "metadata_changed": True}, ] for i, c in enumerate(cases, 1): t, act = classify(c) print(f"观测{i}: 锚点分变化 {c['quality_delta']:+.1f}pt,题数 {c['sample_n']}," f"风格越限={c['style_shift']},双簇={c['bimodal']},元数据变化={c['metadata_changed']}") print(f" -> 判定卡 [{t}] {act}\n")
一次运行的真实输出(实测,示意输入):
观测1: 锚点分变化 +0.5pt,题数 20,风格越限=False,双簇=False,元数据变化=False -> 判定卡 [Match] 一致:无需动作,本轮记录留档 观测2: 锚点分变化 -7.0pt,题数 20,风格越限=False,双簇=True,元数据变化=False -> 判定卡 [Rerouted] 疑似路由变化:按题分桶复查,向服务方出示两簇样本 观测3: 锚点分变化 -9.0pt,题数 10,风格越限=True,双簇=False,元数据变化=True -> 判定卡 [Invalid] 判定无效:先修探针(对照题/网络/配额),再谈模型
注意观测 3 的教学含义:信号最吓人(分掉 9 个点、风格越限、元数据也变了),但题数只有 10——证据不足时,最响的警报恰恰最可能是探针自己的问题。阈值(±2pt 噪声带、-5pt 判降级、15 题下限)是示意起点,应按 5.2 的基线重跑波动校准。
💡 为什么要六类而不是"变/没变"两类:因为动作不同。Rerouted 要按题分桶(找路由边界),Downgrade 要看锚点分区间(是否超噪声),Unlisted 只是要一纸确认。分类的目的是让下一步动作没有歧义。
判型是事后的,治理是事前的。三件事在接入前做完(治理动作,不是本书的法律建议):
第 7 章选型报告的"风险与预案"区块会直接引用这三项。
定性靠判定卡,但判定卡的六个字段要有数据来源——基线怎么留、探针怎么选、比对怎么做,是 5.2 的任务。顺带说一句:那套基线顺手也是第 6 章盲测的起点,一份功夫两份收益。