Coding Agent 的知识也会漂移:用固定快照检查 Vercel Plugin 升级

0 阅读17分钟

Coding Agent 的知识也会漂移:用固定快照检查 Vercel Plugin 升级

插件升级以后,同一个项目突然收到不同的 API 建议,第一步该查模型,还是查插件?只看“已经安装 Vercel Plugin”无法回答。安装名称相同,内容可能不同;磁盘内容相同,宿主实际载入的部分也可能不同。

这篇文章解决一个具体问题:团队升级厂商维护的 Agent 知识时,怎样先确认升级了什么,再决定哪些项目需要回归。最小交付物是一份来源比对结果和一张任务证据表。这里的“知识依赖”是作者的工程类比,不是 Vercel 发布的包格式标准,也不是对模型输出确定性的承诺。

插件知识也会漂移

净增三项,实际迁移更大

1. 同一个插件,Skills 从 30 变成 33,却不只是增加三项

2026-10-14,本次重新读取官方仓库,把旧研究使用的提交 4f867228f69a48c4781ccf1bc5d2741af435cc97 与本次取到的提交 b97d74d07702e9fe4bd7ac5ef308bc9f65abc826 分别冻结。两个提交的 package.json 与插件 manifest 版本一致,分别为 0.45.1 和 0.54.1。固定链接可以直接核对:旧版清单、新版清单。

递归目录中,只统计 skills/<name>/SKILL.md 这一层,旧版有 30 项,新版有 33 项。排除更深的 upstream/SKILL.md,否则会把嵌套材料误计成一个新增可调用 Skill。净增加三项,由新增九项、删除六项构成;余下 24 项入口文件的 Git blob 标识也全部改变。文件标识变化只证明字节变化,不直接证明建议语义发生改变。

被移除的六个入口是 next-cache-components、next-forge、next-upgrade、nextjs、shadcn、turbopack。这比单纯的总数更能指导回归:旧项目如果依赖其中的入口,需要先检查知识来源迁移,再重跑相关任务。新版固定 README说明 Next.js 框架指导转向 Next.js 自身的版本匹配资料和工作流 Skills。不能继续把旧版“插件自带 Next.js Skills”的表述套到新版上。

两个版本的 manifest 都列出四个 commands 和三个 agents,声明列表未变;新版 hooks 清单新增 PostToolUse 事件。它们是文件声明的差异。这里没有执行这些 hooks,不据此宣称某宿主已经加载、调用或发送了某种事件。

这份差异首先适合形成回归候选,而不是升级风险分数。30 加上九项再减去六项等于 33,总数可以复核,但风险还取决于目标项目是否依赖被移除入口,以及共享文件究竟改了哪些建议。如果团队只保存总数量,就无法在建议突然变化时追溯是哪条知识入口迁移;如果只看到 24 个文件变化就重测所有业务,又会把格式调整和项目无关产品变更混进工作量。

差集应与项目使用范围关联。例如,旧项目曾依赖 next-cache-components 时,先确认现在应查哪份与目标 Next.js 版本相符的资料,再选择已有缓存行为验收任务;一个完全不使用队列的项目无需因为新增 queues 就增设队列测试。这里的例子是回归选择方式,没有在真实项目执行,也没有预设新版建议一定优于旧版。

被移除的六个知识入口

2. 来源、安装、载入、收益是四个不同的问题

问题最小证据缺少证据时能说什么
厂商发布了哪些内容官方固定提交、清单、目录和原始文件 SHA公开内容发生哪些变化
本机安装了什么安装来源、实际路径、实际文件摘要、宿主版本不能把远端清单当作本机安装结果
本次任务载入了什么对应会话的宿主日志、入口与内容标识、载入时间已安装不等于已载入
任务是否受益同任务、同项目、同验收条件的配对结果有更多 Skills 不等于成功率更高

这个区分决定排障顺序。远端升级后,本地缓存可能仍然指向旧提交;本地已更新后,新会话与恢复会话也可能处于不同载入条件;即使证实载入了新资料,模型仍可能引用不适合目标依赖版本的 API。不能用上一层的证据替下一层下结论。

官方文档描述默认会话启动行为只在空目录或检测到相应项目时启用,Skills 按需使用。这为“为什么安装后没有使用全部资料”提供机制解释,但仍不是本机每一条任务的执行记录。文档和 README 也是可变内容,本次保存了它们的原始字节与读取时间;固定提交是本文复算的依据,浮动页面仅作辅助说明。

来源摘要也有两个层次。保存下载文件的 SHA-256,可检测这份证据是否被改动;Git tree 中的 blob 标识,可比较仓库文件是否改变。两者目的不同。本文示例先核验保存文件的 SHA-256,再读取树中的 blob 标识,不把后者写成文件 SHA-256,更不把任何摘要当作发布者可信或宿主运行成功的证明。

这次比对的范围是入口文件和声明清单。SKILL.md 可能继续引用 references、生态图或其他材料,而这些依赖内容的变化没有在当前脚本里逐项计算。即使某入口 blob 相同,也不能推出它依赖的所有参考资料相同;需要查某个实际任务时,应再补该入口引用文件和宿主实际选择的内容摘要。这样的扩展应由任务需要触发,而不是让一个有限目录例子承担全量知识闭包的保证。

摘要还能解释两个容易混淆的失败。下载副本被改变时,SHA-256 不符表示当前输入已偏离保存证据,应先恢复或重新取证;两版入口 blob 不同时,只说明固定仓库对象不同,还需要读具体 diff。前者阻止错误数据进入统计,后者帮助确定回归范围。两者都不回答模型究竟用了哪一段内容,更不能替代项目验收。

目录不完整,就停止计数

3. 一个能离线复算的最小示例

下载时已经保存两版 package.json、.claude-plugin/plugin.json、hooks/hooks.json 和 GitHub 递归 tree 响应。source-metadata.json 记录每份原始文件的 URL、读取时间、HTTP 状态、字节数和 SHA-256。下面的 Python 程序只读取这些文件;它不会安装插件、执行 hook 或连接模型服务。

完整附件中的文件名使用 baseline- 和 candidate- 前缀。复制代码时,将代码与这些输入放在同一目录,再运行 python3 snapshot_compare.py。这里没有把 manifest 当作 Skills 的全量清单:Skills 来自树中的直接入口,commands 和 agents 来自 manifest。

"""Compare saved official source snapshots; do not install or invoke a plugin."""
from pathlib import Path
import hashlib
import json

ROOT = Path(__file__).resolve().parent
META = json.loads((ROOT / "source-metadata.json").read_text())
SOURCES = {row["file"]: row for row in META["sources"]}


def load(name):
    raw = (ROOT / name).read_bytes()
    if hashlib.sha256(raw).hexdigest() != SOURCES[name]["sha256"]:
        raise ValueError("source SHA mismatch: " + name)
    return json.loads(raw)


def skills(tree):
    if tree.get("truncated") is not False:
        raise ValueError("incomplete Git tree")
    result = {}
    for item in tree["tree"]:
        parts = item["path"].split("/")
        if item["type"] == "blob" and len(parts) == 3 and parts[0] == "skills" and parts[2] == "SKILL.md":
            result[parts[1]] = item["sha"]
    return result


def snapshot(label):
    package = load(label + "-package.json")
    plugin = load(label + "-.claude-plugin_plugin.json")
    if package["version"] != plugin["version"]:
        raise ValueError("package and plugin versions disagree")
    return {"version": package["version"], "skills": skills(load(label + "-tree.json")),
            "commands": plugin["commands"], "agents": plugin["agents"],
            "hook_events": sorted(load(label + "-hooks_hooks.json")["hooks"])}


if __name__ == "__main__":
    assert skills({"truncated": False, "tree": [
        {"path": "skills/demo/upstream/SKILL.md", "type": "blob", "sha": "nested"},
        {"path": "skills/demo/SKILL.md", "type": "blob", "sha": "direct"}]}) == {"demo": "direct"}
    try:
        skills({"truncated": True, "tree": []})
    except ValueError:
        pass
    else:
        raise AssertionError("truncated tree must fail")
    old, new = snapshot("baseline"), snapshot("candidate")
    a, b = old["skills"], new["skills"]
    result = {"baseline_commit": META["baseline_commit"], "candidate_commit": META["candidate_commit"],
              "versions": [old["version"], new["version"]], "direct_skill_counts": [len(a), len(b)],
              "added_skills": sorted(b.keys() - a.keys()), "removed_skills": sorted(a.keys() - b.keys()),
              "changed_shared_skills": sorted(k for k in a.keys() & b.keys() if a[k] != b[k]),
              "commands_equal": old["commands"] == new["commands"],
              "agents_equal": old["agents"] == new["agents"],
              "added_hook_events": sorted(set(new["hook_events"]) - set(old["hook_events"])),
              "evidence_scope": "saved_source_comparison_only",
              "installed": "not_checked", "runtime_loaded": "not_checked", "task_gain": "not_evaluated"}
    print(json.dumps(result, ensure_ascii=False, indent=2))

验证环境为 Python 3.11.6,仅用标准库。实际运行退出码为 0,stderr 为空;命令、UTC 时间、操作系统、脚本和输出 SHA 另存于运行记录。原始输出如下:

{
  "baseline_commit": "4f867228f69a48c4781ccf1bc5d2741af435cc97",
  "candidate_commit": "b97d74d07702e9fe4bd7ac5ef308bc9f65abc826",
  "versions": [
    "0.45.1",
    "0.54.1"
  ],
  "direct_skill_counts": [
    30,
    33
  ],
  "added_skills": [
    "access-protected-vercel-deployment",
    "build-agents",
    "create-a-backend",
    "custom-metrics",
    "domains",
    "flags-sdk",
    "is-agentic",
    "queues",
    "vercel-services"
  ],
  "removed_skills": [
    "next-cache-components",
    "next-forge",
    "next-upgrade",
    "nextjs",
    "shadcn",
    "turbopack"
  ],
  "changed_shared_skills": [
    "ai-gateway",
    "ai-sdk",
    "auth",
    "bootstrap",
    "cdn-caching",
    "chat-sdk",
    "deployments-cicd",
    "env-vars",
    "eve",
    "knowledge-update",
    "marketplace",
    "microfrontends",
    "react-best-practices",
    "routing-middleware",
    "runtime-cache",
    "vercel-agent",
    "vercel-cli",
    "vercel-connect",
    "vercel-firewall",
    "vercel-functions",
    "vercel-sandbox",
    "vercel-storage",
    "verification",
    "workflow"
  ],
  "commands_equal": true,
  "agents_equal": true,
  "added_hook_events": [
    "PostToolUse"
  ],
  "evidence_scope": "saved_source_comparison_only",
  "installed": "not_checked",
  "runtime_loaded": "not_checked",
  "task_gain": "not_evaluated"
}

输出中的 not_checked 和 not_evaluated 是证据边界,不是漏填成功结果。这次实际验证的是保存文件一致性、目录统计和清单差异。程序也用两个断言检查统计边界:嵌套 upstream 文件不计数,截断的 Git tree 必须拒绝。一次正常退出不证明宿主加载成功,也不证明任何生产项目受益。

如果输入文件被改动,程序应在读取时因 SHA 不符退出;如果 GitHub tree 标记 truncated,不应输出貌似完整的数量。把来源错误当作“没有差异”,会使整个回归选择从错误起点出发。反过来,如果文件只是格式调整导致 blob 改变,应检查具体差异,不能直接宣布行为回归。

要独立复算,可以直接使用随稿 examples 目录中的附件。八个 JSON 输入分别是两版 package、plugin manifest、hooks 和 tree;另外保存两版 README 与本次文档快照,供追溯说明。文件名虽然把仓库中的斜杠改成了下划线,但原始文件字节未改,来源清单仍保留原 URL 和 SHA。先把目录完整保留,再在该目录运行程序,避免只复制代码后因缺输入而报错。

若自行重新下载,也应沿清单里的固定提交地址取文件,重新计算实际字节摘要。递归 tree 的目录项可以一致,而传输 JSON 的缩进或空白不同;这种情况下,新的下载 SHA 与旧清单不同是预期可能性,不能直接把新下载覆盖旧附件继续声称逐字复现。应保留另一份清单,先核对目录项和版本,再明确是原始字节复现还是同一来源对象的语义复算。

下面输出来自已保存官方输入的真实本地运行,显示 30/33、九项新增、六项删除和 24 项共享入口变更。原始运行用 CPython 3.11.6,2026-10-14 11:03:57 UTC 开始,退出 0;本轮平台附件在同一 Python 版本再次执行也得到逐字一致的输出,退出 0、stderr 为空。它没有触发安装、遥测、hook 或模型任务,得到的成功记录只属于来源比对程序。

4. 先根据差异选择任务,再做配对回归

来源比对回答“需要看哪里”,任务评测回答“是否接受升级”。删除的 Next.js 相关入口可触发旧项目的版本适配任务;新增后台或队列知识可触发已有业务确实使用的任务。没有使用相应能力的项目无需为了凑覆盖率测试所有入口。变更文件很多时,先按目标项目涉及的产品、依赖和历史失败案例筛选,不把文件数量等同风险评分。

建议为每个任务固定项目提交、锁文件、模型配置、宿主版本、Prompt、允许使用的网络和工具、验收脚本。比较旧知识和新知识时,除插件快照外保持其他条件相同;把模型或框架也同时升级,就无法把结果差异归给知识更新。无插件条件可以作为额外基线,不能代替旧、新插件的直接对照。

一次运行成功应按任务定义。例如,“在目标项目增加缓存失效逻辑,并通过指定测试”是可验收动作;“代码更现代”只是主观评价。记录首次验收是否通过、最终是否通过、修复轮数、使用了哪些过时或不存在的 API、无关改动、输入输出与工具成本。某项缺少日志时保留未知,不把最终通过倒填成首次通过。

字段用途本次是否已有
source_commit / source_file_sha256重建公开来源快照有
installed_path / installed_digest定位宿主实际安装内容未检查
session_id / loaded_entry / loaded_digest关联一次具体载入未检查
project_commit / dependency_lock_digest固定任务适用版本待选真实任务
prompt_digest / model / host_version固定其他实验条件待执行
first_pass / final_pass / repair_count / cost比较验收和代价未评测

需要重复运行多少次,应随任务随机性、成本和需要判断的差异确定;本文不把“十次”当作统计保证。少量本地回放只适合检查验收链路,不能支持稳定成功率提升。若要报告提升,还需展示分母、失败样本、重复运行协议及波动,而不是只给挑选的成功案例。

配对记录应按任务逐行保存,不能把不同难度的任务混成一个平均成功率。例如缓存失效任务可以固定变更前项目提交与已有测试,旧、新知识分别从相同工作状态开始。记录生成结果与测试失败内容,然后按同一修复预算推进,才能比较首次通过、最终通过和修复代价。将旧条件失败后已修好的文件直接交给新条件,会把人工修复带来的优势错误记到知识升级上。

如果关心 API 建议质量,要同时定义判定对象和分母。过时 API 指不适用目标依赖版本的输出调用;不存在的 API 需要在固定依赖或相应资料里核实;构建通过与测试通过要各自记录。只输出文字建议的任务可能不适合统计构建通过率,只有存在该验收动作的任务才能进入对应分母。所有无法核对的调用保留待定,不能为凑比例把它们填成正确。

这里的指标定义是候选观察口径,不是本次已获得的任务结果。选择哪些指标应服从任务验收,不要求为运行这个来源比对示例构建任何业务项目。

5. 出现冲突时,按目标依赖和实际载入证据排查

模型建议使用新 API,但项目锁文件仍是旧版时,先核对目标依赖,再找建议对应的资料入口。若宿主没有可读的载入日志,至少应保存安装路径与入口文件摘要,并明确“本次实际载入未证实”。这比看到插件名称就宣称新资料覆盖旧知识更准确。

仓库约定、用户要求与厂商建议冲突时,应在具体宿主内验证指令处理方式。本文不建立跨宿主通用优先级规则,也不把插件中的自然语言内容当成不可覆盖的程序约束。任务验收必须独立于这些建议:例如限定依赖版本、限定修改文件范围、用测试核对行为。对于无法观察的裁剪、上下文截断或动态检索,保留未知项,并避免将其推测为失败根因。

更新操作还涉及缓存和恢复会话。保存旧提交的材料不等于宿主支持原生按提交安装;团队应确认具体工具的安装接口、更新方式及缓存行为。更新后建立新会话并记录版本,是可以尝试的排查动作,不能保证所有宿主都因此清空所有旧上下文。需要回退时,应先确认实际安装内容和项目状态能否恢复,避免只回退 manifest 而遗漏知识入口。

遥测也要按具体版本审阅。新版 README 的描述比本文读取到的浮动文档更细;字段范围、禁用选项、传输端点和宿主支持情况应由团队核对实际版本。本文没有运行遥测,不把文档声明当成本机网络行为证明,也不把某个调用事件当成内容全文载入或模型采纳的证明。

针对恢复会话的排查,也应先把观察范围写清。旧会话继续给出旧建议,可能与它保留的上下文有关,也可能与本地安装未更新、目标依赖不变或模型自身判断有关。先比较实际文件,再取对应会话的可读证据;如果宿主只提供一个插件调用名称,就把证明停在入口调用这一层,不推断全文载入或建议被采纳。下一步可以在可恢复的项目状态下建立新会话做对照,但不要将新会话本身写成刷新成功的证据。

以下是作者建议的检查顺序。Install、Runtime Loading、Evaluation 分别需要独立记录;Rollback、Pin、Disable 只是待具体宿主接口支持后才可采用的处置选择。这一顺序不能证明这些步骤已执行,也不保证宿主提供任意提交锁定安装。

6. 接受升级的条件应落到项目验收

这份对照得到一个有限但有用的结论:同名 Vercel Plugin 的两版公开内容确实发生了入口迁移,净数量变化掩盖了新增、删除和共享文件变化。它足以帮助团队选出需要回归的任务,却不足以回答新插件是否让 Agent 更可靠。

小团队可以先记录固定提交、实际安装内容和少数关键任务,不必先建设复杂知识管理平台。当项目的历史任务能在相同条件下验收、新增入口的适用任务经过验证、已知回归有回退办法,再决定推广。不能证明实际载入时,就把结论停在安装或来源层。

把 Agent 知识像依赖一样维护,最实用的动作是让“插件更新了”变成可复算的文件变化,让“任务变好了”变成独立验收结果。两份证据都保留,排障时才能判断问题是来源迁移、安装漂移、会话载入,还是任务本身的失败。

哪些证据还没有获得

参考资料