厂商会悄悄改发布页上的数字:测试团队该把结论锚在自己能重跑的那一份上

举报
霍格沃兹测试学社 发表于 2026/09/29 18:20:52 2026/09/29
【摘要】 本文揭示AI模型评测中厂商发布页数字“悄然变动”的行业风险,指出依赖截图引用的脆弱性。提出“留痕+复现”双轨实践:用哈希快照捕获变更,以可复现评测替代被动采信。强调差异即信号,需优先对齐口径而非质疑真假。核心主张:测试结论必须锚定在自己能重跑、可追溯、有置信区间的证据上。

选型材料还剩最后一页。你上周从厂商发布页截下来的那张图,被贴进了三份评审文档,其中一个数字撑起了整个『为什么选这版模型』的论证。今天临下班你想把同一块区域再截一次,补进复盘稿——页面还是那一页,URL 一个字符没变,那个数字变了。

你的第一反应是怀疑自己截错了图,于是去翻截图目录。截图在,可它只能证明『那天屏幕上出现过那个数』,证明不了那天页面上挂的就是它,更证明不了它今天为什么变了。

九月初有一条外媒报道,把这件事从『你多疑』变成了『行业问题』。据《财富》Fortune 2026-09-04 报道,OpenAI 在其 GPT-6『Astra』模型发布中被指悄悄上调了部分评测指标,并在发布之后持续改动其他指标,模型博客的公开发布出现了罕见延迟,引发对厂商自报数字可信度的质疑。被改的到底是哪几项、前后各是多少,本文不写——中文自媒体的转述渲染也一律不采信;同样地,本文不替这个模型下任何跑分结论。

值得写的只有一件事:我们引用外部数字的方式,默认了一个正在被拆掉的前提。

拆掉的是『数字固定且可追溯』这个前提

接口测试时代我们有一条几乎不用想的纪律:文档说的不算,抓包算。理由是文档会过期、会写错、会和产品实际行为不一致,而一次请求的报文是可以钉住、可以复放的。

到了选型和验收阶段,这条纪律莫名其妙地松了。厂商发布页给你的那个数,我们通常只做三件事:截图、抄进 PPT、写成结论句。它没有版本,没有抓取时间,没有口径注记,没有任何一样东西能让你在下个季度回答『当时依据的是什么』。它比接口文档更脆弱——文档至少不会当天改,而发布页会。

Fortune 那条报道里被质疑的,其实不是某个数字的高低,而是『一个被广泛引用的数字,居然可以在没有变更记录的情况下改动』。这对测试团队是直接命中:你拿它做选型论证、做验收依据、做上线评审的证据链,而证据链最上游那一环是别人家一个可以随时移动的数。

正确的姿势不是信或不信——那是一句没有工程含量的表态。是把这个结论从『抄来的』迁移到『自己那份能重跑的』上面。迁移有三个动作。

数字会动的三种方式,和各自对应的动作

会动的方式 它实际在做什么 留痕动作 复现动作
发布后继续改 页面上那个数被换掉,URL 不变、没有更新说明 抓一次留一条:快照哈希 + 抓取时间戳 + 当时声称值落 jsonl,再抓就比对,值变即告警 你自己的那份评测独立跑,把厂商值只当对照列,不进你的判定式
挑口径改 数没变,口径变了:数据集换成更窄的子集、允许重试、单轮不测多轮 除数字外,把口径那一行小字一起留痕(哪怕只是一句话),哈希变了指向口径动过 复现时把口径显式写成参数:数据集、温度、重试次数、是否多轮,一项一项扫
延迟披露 关键指标晚于发布给,或先给概览再补细节,评审窗口内你拿不到全貌 留痕记录『当时能拿到什么』,评审结论注明信息截止时间 决策分两级:没有你复现数据的那一项标成待验证,不因概览数字放行

第三行最容易被忽略,因为它不涉及任何欺骗,纯粹是节奏问题——但测试团队恰恰是最该在意节奏的一群:你的门禁不能建在一份『还在路上』的材料上。

留痕:引用一个外部数字,等于签一张会过期的支票

# claim_ledger.py —— 外部数字留痕器:抓一次留一条哈希,值变了当场告警(纯标准库)
import hashlib, json, time
from pathlib import Path

LEDGER = Path("claims.jsonl")

def page_hash(text):
    return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]

def latest(url):
    last = None
    if not LEDGER.exists():
        return None
    for line in LEDGER.read_text(encoding="utf-8").splitlines():
        row = json.loads(line)
        if row["url"] == url:
            last = row
    return last

def record(url, snapshot, claimed, caliber="", clock=time.time):
    """snapshot:那一页当时的正文文本(真实项目里落对象存储,这里用字符串占位)。
    claimed:你从页面上抄下来的那个数;caliber:页面上那行口径原话。"""
    row = {"url": url, "ts": round(clock(), 3), "sha": page_hash(snapshot),
           "claimed": claimed, "caliber": caliber}
    prev = latest(url)
    with LEDGER.open("a", encoding="utf-8") as f:
        f.write(json.dumps(row, ensure_ascii=False) + "\n")
    if prev is None:
        return {"alert": "NEW", "row": row}
    if prev["claimed"] != row["claimed"]:
        return {"alert": "VALUE_CHANGED", "from": prev, "to": row}      # 数字被改了
    if prev["sha"] != row["sha"]:
        return {"alert": "PAGE_CHANGED", "from": prev, "to": row}       # 数没变,措辞/口径动了
    return {"alert": "UNCHANGED", "row": row}

if __name__ == "__main__":
    URL = "https://vendor.example.com/models/frontier-6"
    week1 = "Frontier-6 任务解决率 91.0%(评测允许最多 4 次重试,单轮会话)"
    week2 = "Frontier-6 任务解决率 98.5%(评测允许最多 4 次重试,单轮会话)"
    print(record(URL, week1, 91.0, caliber="允许重试/单轮", clock=lambda: 1000.0)["alert"])
    r = record(URL, week2, 98.5, caliber="允许重试/单轮", clock=lambda: 1000.0)
    print(r["alert"], r["from"]["claimed"], "->", r["to"]["claimed"])

为什么分两个告警等级:VALUE_CHANGED 是数字被改,PAGE_CHANGED 是数字没改但页面动过——后者往往更阴,因为口径那行小字通常就藏在这里动。踩过的坑有三个。一是只存截图:截图不可比对,你没法拿两张 PNG 做 diff 并生成一条报警;截图可以留作人证,但机器能判的是哈希。二是把整页 HTML 拿去哈希:导航栏、推荐位、时间戳每天都在变,哈希天天报红,两周后没人再看这个告警——只对你引用那一段文本做哈希。三是留痕器不带口径字段:只留了数字,等你半年后想搞清楚『这个 91% 是单轮还是多轮、允不允许重试』,页面上那行字早改过三轮了。

留痕不解决对错,它只保证一件事:数字变了你能发现,并且能拿出『当时是哪个值、什么口径、几点抓的』。这就是评审现场最贵的一句话。

复现:报你自己的数

# reproduce.py —— 固定数据集 + 固定 seed + 不变量断言:报你自己的那个数
import random
from statistics import mean

TASKS = [{"id": f"t{i:02d}", "ctx": "long" if i % 3 == 0 else "short",
          "must_cite": i % 2 == 0} for i in range(60)]

def fake_agent(task, seed, temperature, retries):
    """本地替身:上下文越长越容易掉事实,温度越高越飘,重试能救回一部分。
    通过 = 任务完成 且 该引用的都引用了。断的是属性,不断措辞。"""
    rng = random.Random(f"{task['id']}-{seed}")
    base = 0.90 - (0.28 if task["ctx"] == "long" else 0.0) - (0.09 if temperature > 0.6 else 0.0)
    ok, attempt = rng.random() < base, 0
    while not ok and attempt < retries:
        attempt += 1
        ok = random.Random(f"{task['id']}-{seed}-r{attempt}").random() < min(0.99, base + 0.30)
    cited = random.Random(f"{task['id']}-{seed}-cite").random() < 0.99
    return ok and (cited or not task["must_cite"])

def run(temperature=0.2, retries=0, repeats=5):
    return [int(fake_agent(t, rep, temperature, retries))
            for rep in range(repeats) for t in TASKS]

def rate_with_ci(flags, n_boot=2000, alpha=0.05, seed=7):
    rng = random.Random(seed)
    boots = sorted(mean(rng.choices(flags, k=len(flags))) for _ in range(n_boot))
    return mean(flags), boots[int(len(boots) * alpha / 2)], boots[int(len(boots) * (1 - alpha / 2))]

if __name__ == "__main__":
    VENDOR = 98.5                     # 留痕器里厂商发布页当时挂着的那个值
    for temp, retries, tag in [(0.2, 0, "线上口径 低温/不重试"),
                               (1.0, 0, "高温/不重试"),
                               (0.2, 4, "低温/允许 4 次重试")]:
        p, lo, hi = rate_with_ci(run(temperature=temp, retries=retries))
        gap = p * 100 - VENDOR
        flag = "  ← 差异超 5pt,先查口径,别急着吵谁对" if abs(gap) > 5 else "  ← 口径对上了"
        print(f"{tag}  我的复现 {p*100:.1f}% [{lo*100:.1f}, {hi*100:.1f}]"
              f"  vs 厂商 {VENDOR}%  差异 {gap:+.1f}pt{flag}")

跑起来是三行:线上口径下我的复现 83.0%(区间 78.7 到 87.0),高温下 73.0%,而一旦允许 4 次重试,复现值抬到 99.3%,和厂商那个 98.5 已经落在同一量级里了。

这段输出才是本篇真正想给的东西。它没有证明厂商造假——它只证明了另一件更要紧的事:同一套代码、同一批任务,仅仅改变『是否允许重试』这一个口径参数,就能在你的数据集上重现出那个数量级的数字。差异的第一解释往往不是谁在骗谁,而是你俩测的压根不是同一件事。

两个设计点值得抄。第一,通过判定不写『答案对不对』,写『任务完成且该引用的都引用了』这种属性断言:措辞每次都不同,属性必须每次都在。第二,rate_with_ci 里的区间比点估计值钱——60 个任务跑 5 遍共 300 次,区间还有将近 8 个百分点宽,这时候任何『比厂商低了 3 个点』的结论都是噪声;先加样本,再加结论。踩过的坑是固定种子只固定了模型调用、忘了固定数据集顺序:换台机器跑出另一个数,然后团队花两天怀疑模型侧有变更。

差异即信号:对不上时先查这三件事

你的复现值和留痕里的厂商值不一致时,最没用的反应是立刻判定谁对谁错。差异本身就是这次评测最有价值的产出,它按顺序指向三个可查项。

先查数据集口径:他那 60 项和你那 60 项是不是同一类难度、同一个业务域、同一批语言。这一步最常见,也最不需要技术——只需要有人把两边的样本列表并排摊开看一次。

再查采样配置:温度、是否 top-p、上下文窗口长度上限、单轮还是多轮。发布页那行小字(就是你留痕器里 caliber 字段存的那句)通常在这里救你。

最后查重试与超时策略:允许重试几次、失败是否重投。这一项单独就能造出十几个百分点,而它几乎从不写进摘要。

三项都查完仍对不上,你才有资格讨论『是不是他那个数有问题』。这条顺序值得写进评审话术:不是不信厂商,是任何数字都要先过一遍口径对齐——包括厂商的,也包括你自己上周那个。

对照项 抄发布页 自己复现 + 留痕
选型风险 结论建在别人可随时移动的数上,改动无通知、无记录 风险变成可观测项:哈希比对不通过就报警,选型依据有一版可查
可追责性 出事只能回看截图,截图证不了当时页面上挂的是哪个口径 每条外部数字都有时间戳 + 快照哈希 + 声称值,能定位到哪天哪版
回归可比性 每半年重跑一次选型,数字对不上却分不清是模型变了还是他改了数 你的数据集、seed、断言固定,跨版本真正可比;漂移可归因
跨团队信任 评审时五个团队对同一张截图做五种解读 现场并排两列数:你复现的、他声称的,外加区间与样本量

image.png

接进 CI:把外部数字降级成一列参考值

留痕器挂在两个地方:新增引用时人工执行一次,以及每周定时无差重抓全部在册 URL——定时那条才是重点,因为改动从来不挑你评审的那天。抓完把 VALUE_CHANGED / PAGE_CHANGED 两类告警直接开成工单,附前后两版快照。

复现任务挂在发布流水线上,产出一行三值(点估计、置信下界、样本量)与一列外部对照值。关键纪律是:外部值只打印,不参与门禁判定式。门禁只看你自己那一份——这是把『结论锚在自己能重跑的那一份上』这句话落到代码里的唯一写法。

第三件事是给每个引用建立到期提醒。发布页数字要定期重抓,你自家数据集要定期换血(跑了两年的样本集会失去区分度),两条时间线一起管,评审文档里的每个数才都有『依据截至某日』这句兜底话。

image.png

这套做法管到哪里为止

四条边界。第一,Fortune 那条报道是标题层核实,本文只引用『被指上调、发布后持续改动、博客延迟』这三点定性事实,具体指标名与前后数值本文不知也不猜;这件事的争议本身还没有权威结论,别把它当成『厂商必然造假』的证据。第二,复现的前提是你有可重跑的本地评测集和一个稳定可寻址的被测端点:如果你的智能体依赖真实第三方数据源,两次跑的不是同一个世界,任何复现值都不可比,先做数据快照再谈复现。第三,样本量不够时差异分析会骗人——上面那个 300 次运行的区间还宽近 8 个点,你现在的评测集如果只有 30 条样本,先别急着讨论谁对谁错。第四,也要坦白:本文留痕器的告警阈值、复现骨架里替身模型的概率参数、以及那组 83.0 / 73.0 / 99.3 的演示数字,全是本地构造与算术结果,接进你们项目的第一件事是按真实数据集与真实口径重算,别把这三个数抄进 PPT。

结语:明天上午能做的一个动作

打开你最近一份选型或验收文档,把里面所有来自外部的数字挑出来——通常三到五个。给它们各建一条留痕记录(URL、抓取时间、页面文本哈希、声称值、口径原话),然后挑其中最关键的那一个,用你自己的任务集跑一遍复现,把两个数和你的置信区间并排贴回文档。

那一刻你会发现,文档里真正撑得住的结论,比你想的少;而这也正是它第一次变得可以防守。

别人的数字是参考值,你重跑得出来的才是证据——测试团队的结论,从来只该锚在能重跑的那一份上。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。