前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯10183
  • GPT 系列旧语言能力提升,迁移评测受关注
  • 用 AWS 注册表实现 Agent 动态工具发现
  • 用 X.509 为多 Agent 建立可验证身份
  • Eon 用统一 MCP 网关简化多云排障
  • 给企业Agent搭建带权限的统一语义层
  • AWS 智能体跨库查询如何落实用户级权限
  • 提示注入检测为何被同义改写绕过
  • Synapse 打通编程助手的记忆与配置
  • vLLM 与 SGLang 新增 TPU 原生后端
  • 给水印检测加门控,减少误选与修复风险
  • 用本地语音组件搭建可打断的 AI 助手
  • Claude Code 误删复盘:命令钩子的盲区
  • Mac 跨应用文本助手如何读取界面
  • 用 AI 编程智能体清理电视广告与预装应用
  • DeepSeek开放生产推理框架DeepInfer
  • 京东大促期间暂停外部安全测试
  • Coral NPU补齐矩阵单元与开源编译链
  • 向量检索与知识图谱如何协同支撑 RAG
  • 让编程助手接入浏览器完成调试闭环
  • 先验证分类器,再让 AI 排查运行轨迹
  • 录制模型响应,隔离评测代码与模型变动
  • 审查 AI 生成 PR,先核对任务边界
  • 用有效吞吐压测确定模型接口并发上限
  • Agent 改一个字段,如何证明操作正确
  • 用多任务决策模型自动分流 CVE 漏洞
  • 逐步核算 Token,定位 Agent 的结构性浪费
  • Agent 生成 SQL,如何控制重跑漂移
  • 审核超时后的任务释放与计费隔离
  • DeepSeek 开源大规模推理框架 DeepInfer
  • DeepInfer 开源,公开大规模推理部署能力
  • Cloudflare预览低价结构化抽取模型
  • Headroom 在本地压缩智能体上下文
  • 智能体改记忆,如何验证没有误伤
  • llama-server 工具控制与停止条件现兼容问题
  • 模型快十倍,智能体为何只快一点
  • 用决策模型做评测,先调分类阈值
  • 编码 Agent 实测:成本之外看交付细节
  • Mercury 3 发布:主打高速生成与百万上下文
  • 用 Node 拆解向量池化与检索质量
  • 补齐 Oracle 方言约束,轻量模型首答率跃升
  • 拆解 Claude Code 的完整智能体循环
  • 邮件 Agent 重试前先校验退信
  • 用信号队列约束 Agent 等待邮件
  • 用 Cloudflare 搭建 Agent 收信中枢
  • 一次字符串替换如何让线上文章翻倍
  • AI 搭项目失败,先确认代码是否拉取成功
  • 检索匹配再高,也救不了过期知识库
  • 本地模型帮你给常用命令起快捷名
  • MCP 无状态改版涉及哪些迁移工作
  • 检查 Supabase 服务端鉴权的信任来源
  • 流式 AI 接口延迟该如何拆分测量
  • 已加载 51 / 10183
8.0
热点
AI SCORE
技术实践2026-10-20 18:18

录制模型响应,隔离评测代码与模型变动

dev.to · AI#回归测试#模型评估#提示词
Editor brief · 编辑速览

文章将真实模型响应按输入指纹保存,回放时只读取匹配记录,缺失则明确报错。通过限制网络调用仅发生在录制阶段,可以单独检查评测逻辑变化,减少模型波动对回归判断的干扰。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

一套 prompt 测试套件里,有三个不断变化的部分:prompt 文本、grader,以及模型快照。只要其中任何一个发生变化,整体通过率就可能跟着变。Replay cassette 通过把模型输出固定保存在磁盘上,消除了这种歧义,让你可以只重跑实际修改过的部分。要让这套机制成立,规则必须足够严格:网络请求只能出现在一条代码路径中,也就是录制路径;其余所有路径,要么读取按哈希寻址的文件,要么明确报错。下面介绍的是一套花一个下午就能搭起来的 harness,以及它会在哪些情况下误导你。

Cassette 是按输入索引的录制记录,不是 mock

大多数团队首先想到的是 mock:手写一些看起来合理的假响应,结果不到一周就与真实情况脱节。Cassette 不一样,它来自一次真实调用,并通过生成这次输出的完整输入指纹来寻址。只要改动 prompt 中的一个词,指纹就会变化,旧记录就无法悄无声息地回答新问题。正是这一点,让缓存变成了回归检测器。

# cassette.py
import hashlib, json, os, time

DIR = os.environ.get("CASSETTE_DIR", "cassettes")

class CassetteMiss(Exception):
    """Raised in replay mode when no tape matches the input fingerprint."""

def fingerprint(prompt, params):
    blob = json.dumps({"prompt": prompt, "params": params}, sort_keys=True)
    return hashlib.sha256(blob.encode("utf-8")).hexdigest()[:16]

def get(prompt, params, produce, mode="record"):
    key = fingerprint(prompt, params)
    path = os.path.join(DIR, key + ".json")
    if os.path.exists(path):
        with open(path) as fh:
            return json.load(fh), True          # hit
    if mode == "replay":
        raise CassetteMiss(key)                 # never fall through to network
    tape = produce(prompt, params)
    tape.update({"key": key, "params": params,
                 "recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())})
    os.makedirs(DIR, exist_ok=True)
    with open(path, "w") as fh:
        json.dump(tape, fh, indent=2, sort_keys=True)
    return tape, False                          # miss, now recorded

这里值得专门说一下 params 参数,因为大家往往就是在这里不知不觉破坏了确定性。Temperature、seed、max tokens、system prompt,以及任何工具选择标志,都应该纳入指纹,即便服务商声称这些参数不起作用。只要一个参数可能改变输出文本,就应该放进 key;即便它不会改变文本,存下来也不过多占几个字节。

只有录制路径可以访问网络

下面的 runner 读取 JSONL 用例文件,向 cassette 层索取录制记录,再把录制的文本交给指定名称的 grader。注意,grader 是通过名称从注册表中查找的,而不是存进 JSON,因为函数无法序列化;一个无法查看 diff 的 grader,也就无法审计。还要注意,回放未命中时会以退出码 2 结束。这与断言失败是不同的信号,也应该通知不同的负责人。

# harness/run.py
import json, os, sys
from cassette import get, CassetteMiss
from client import call_model          # the only module that opens a socket

MODE = os.environ.get("CASSETTE_MODE", "replay")
GRADERS = {
    "json_shape": lambda text, exp: _parses(text) and set(exp["keys"]) <= set(_parses(text)),
    "contains":   lambda text, exp: exp["needle"].lower() in text.lower(),
    "refusal":    lambda text, exp: text.strip().lower().startswith(exp["prefix"]),
}

def run(suite_path):
    hits = misses = 0
    for line in open(suite_path):
        if not line.strip():
            continue
        case = json.loads(line)
        try:
            tape, hit = get(case["prompt"], case.get("params", {}),
                            produce=call_model, mode=MODE)
        except CassetteMiss as exc:
            print("CASSETTE_MISS", exc, file=sys.stderr)
            return 2
        hits += hit
        misses += not hit
        verdict = GRADERS[case["grader"]](tape["text"], case.get("expect", {}))
        print(json.dumps({"id": case["id"], "hit": hit, "verdict": verdict,
                          "finish": tape.get("finish_reason")}))
    total = hits + misses
    print(f"hit_rate={hits/total:.2f}", file=sys.stderr)
    return 0 if hits + misses else 1

if __name__ == "__main__":
    sys.exit(run(sys.argv[1]))

运行两次,就能看明白这套设计的意义。第一次用 CASSETTE_MODE=record 执行,为真实调用付费并写入录制记录;第二次用 CASSETTE_MODE=replay 执行,几毫秒就能结束,而且可以证明没有发出任何网络请求。如果你的 CI 无法在禁用网络的情况下成功完成回放,就说明你还没有真正把 harness 变化与模型变化分开,通过率仍然只是一个无法区分变化来源的数字。

$ CASSETTE_MODE=record python -m harness.run cases/canary.jsonl   # needs a model
$ CASSETTE_MODE=replay python -m harness.run cases/golden.jsonl   # offline, exit 2 on miss
$ git diff --stat cassettes/ | tail -1                            # which tapes moved

无论付费还是免费额度,都用来录制,别用来回放

在这套设计中,录制是成本高的那一半:每个新用例、每次 prompt 修改,都会导致缓存未命中,需要一次真实生成;回放则完全不需要。这种不对称性,恰好适合 MonkeyCode 提供的免费模型访问和免费服务器选项,这也是这套工作流与该产品的结合点。利益披露:本文是作为 MonkeyCode 产品推广活动的一部分撰写的。实际操作时,可以把录制任务放在免费服务器实例上,调用免费模型档位;回放任务则留在运行测试套件的机器上。

要让评估依据保持可靠,因为免费档位本质上属于推广资源,可能随时调整,且不另行通知。应到项目页面核对当前的模型列表、额度和服务器规格,不要把博客文章里的假设写死。应该把 cassette 目录视为能长期保留的资产,而不是依赖某个服务商。即便一个免费模型下线了,你的录制记录仍然可以回放,只有刷新记录的任务需要重新接入服务。

Canary 用例应该选在决策边界附近,而不是中间地带

均匀采样会把大部分预算浪费在那些结果几乎不可能翻转的用例上:一个得分 0.9、判定阈值为 0.5 的用例,几乎无论怎么修改 prompt 都还能通过。真正容易变化的是那些靠近决策边界的用例,所以应该根据得分与阈值的距离来挑选 canary 用例,而不是按类别挑选。这个函数只要五行,通常就能把 canary 套件的录制成本降低一个数量级。

def boundary_band(rows, band=0.10):
    """rows: [{"id", "score", "threshold"}]; keep cases that can actually flip."""
    return [r for r in rows if abs(r["score"] - r["threshold"]) <= band]

先对一批新样本执行边界区间筛选,录制筛选出的用例,然后把它们冻结下来。提交 prompt 修改后,先回放这批冻结的边界用例;如果它们的判定结果都没有翻转,这轮低成本回归检查就完成了,完整套件可以等到夜间再跑。如果确实出现了翻转,你手里就有修改前后的 cassette,可以直接比较两个文件的 diff,不必各执一词。

判断回放何时不适用的决策表

回放无法告诉你什么,以及哪些人应该跳过它

回放能证明的是,面对一组固定输出,你的 harness 保持稳定。它无法证明当前模型的任何情况,因为你刚停止录制,模型就可能已经发生变化。一旦用录制记录替代 socket,延迟、token 用量统计,以及负载下的拒绝行为,就都无法再从运行结果中观察到。因此,绝不能把回放结果当作性能测量。Grader 仍然是最薄弱的一环:即使它已经悄悄偏离人工标注,cassette 套件也照样可能判定它一切正常。

如果你还没有一套稳定的用例集,就先别采用这种方法,因为 cassette 基础设施只会放大糟糕套件的问题,无法修好它。如果你真正关心的唯一问题是新模型是否更好,也应该跳过它,因为回答这个问题需要新样本和配对比较,而不是冻结的文本。任何要求证明每一次独立输出来源的场景,也都不适合这种方法,因为回放记录是缓存产物,不是一次新的决策。

如果你想先试试录制这部分,又不想配置任何基础设施,MonkeyCode 的免费模型访问和免费服务器选项,足以录制第一批几百个边界用例。把 cassettes/ 目录与 harness 一起提交,让这些录制记录逐渐积累,成为团队手中成本最低的回归证据。

如果还想采取进一步行动,可以考虑屏蔽此人和/或举报滥用行为。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
先验证分类器,再让 AI 排查运行轨迹
下一篇
审查 AI 生成 PR,先核对任务边界