← Back to home@Mizumic

driftmon

后端漂移主动监测:冻结探针组 / 五通道签名 / 行为模板 / 灰度告警(纯 Node,零运行时依赖)

Stars
1
Language
JavaScript
Created
Oct 19, 2026
Updated
Oct 20, 2026

Introduction

driftmon · 后端漂移监测

一句话:在不依赖供应商声明的前提下,持续监测「我调用的模型后端有没有悄悄变」。

License: MIT CI Node.js Tests Phase

当前状态:Phase 0/1 骨架已实现(采集 / 签名 / 基线 / 判定 / 报告 + 离线测试); Phase 0 噪声标定已完成(2026-10-20):官方 flash 1017 同一天 30 轮 / 240 批 / 8640 请求 / 0 错误(892k token,2h07m),noise-floor.json 的 verdict=calibrated、五通道全部可用, 判定阈值已按噪声底校准(L drop 0.10、S ratio 2.5;见 docs/EVIDENCE.md E10)。 2026-10-20 起有了已知检查点行为模板(data/templates/,官方 flash 1017 已固化), 作为灰度检测与「无指纹网关反推检查点」的参考物(尚未接进 judge)。 同日加入 T 通道官方参照:用官方 deepseek-recipe + v41 tokenizer 离线算出期望 T 向量并入库, flash 1017 模板的 T 向量 [18,17,17,39,16] 已与官方参考实现逐项一致(见 docs/TOKENIZER-REFERENCE.md 与 E11)。

目录

为什么做这个

起因(2026-10-17 的实测):把上游从 deepseek-official 切到 opencode-go 之后发现两件事——

  1. 新上游根本不发布 system_fingerprint,官方那条"后端配置指纹"的通道直接消失;
  2. 即使回到有指纹的官方端点,那个字段也只是供应商的一句承诺——它说没变就没变,你没有任何反证。

于是问题变成:能不能只靠自己观测到的机械行为,判断后端有没有变?

这件事的价值不限于 OpenCode Go:任何中转站、网关、公司内部代理都可能悄悄换后端 (OpenRouter / OrcaRouter / 各类聚合服务,换后端是常态),而客户端只有一句"我们没变"。

环境要求

项要求说明
Node.js>= 20纯 Node 实现,零运行时依赖(只用内建模块)
API key(采集时才需要)环境变量例如 OPENCODE_API_KEY / DEEPSEEK_API_KEY;只走环境变量,不写进配置文件
被动层插件(可选)dsh-system-fingerprint只读它的产物做交叉引用;不装也能采集/判定
开发机重建 T 参照(可选)Python 3.10+ + deepseek-recipe==0.2.0只给 scripts/build-tokenizer-reference.py 用;不进入运行时,日常跑 reference check 不需要 Python

DSH 版本声明

driftmon 不依赖 DSH 版本:它是独立 CLI(纯 Node、零运行时依赖),不 import DSH 代码, 不装插件也能采集/判定。唯一与 DSH 生态的耦合是插件产物契约:

项声明
与 DSH 的关系无代码/进程依赖;只在「联动展示」时只读 dsh-system-fingerprint 的 endpoints.json / config.json(schema v1)
当前验证组合DSH 0.2.0-rc.2 + 插件 0.3.0(2026-10-20)
契约不兼容时缺失 / 损坏 / 版本不认识 → 只降级告警,不崩、不阻断采集与判定(见 docs/DEPENDENCY.md)
DSH 升级后先确认插件仍是 0.3.x 且产物契约仍是 v1;插件 bump 契约版本时,driftmon 需同步升级消费逻辑
Node 版本>= 20(不随 DSH 版本变化)

快速开始

让 DSH agent 安装:agent 请按 docs/AGENT-INSTALL.md 执行 (符号初始化 → 离线验证 → 配置 → 验证 → 正式工作流 → 回滚,含成本与安全边界)。 机器相关字段(${DSH_HOME}、${WATCH_FILE} 等)统一由 docs/SYMBOLS.md 的符号映射表定义,本机取值放 .local/symbols.env(gitignored)。

git clone https://github.com/Mizumic/driftmon.git
cd driftmon
npm test                                  # 77 项离线测试,不联网
node bin/driftmon.mjs status              # 配置 / 探针组哈希 / 插件依赖 / 数据现状 / T 参照

export OPENCODE_API_KEY=...               # key 只走环境变量,不写配置文件
node bin/driftmon.mjs collect --sessions 1 --only T,L,G,S   # 冒烟:partial 批次,不进判定
node bin/driftmon.mjs collect --sessions 8                  # 一轮巡检(≈4.7k token/会话)

node bin/driftmon.mjs baseline build      # 滚动窗口 → data/baselines(按端点分组)
node bin/driftmon.mjs baseline noise-floor  # Phase 0 噪声底 → data/baselines/noise-floor.json
node bin/driftmon.mjs judge run           # 同端点内部比基线 → results/alerts
node bin/driftmon.mjs report              # 人可读报告 → results/reports

# 已知检查点的行为模板(官方端点用 system_fingerprint 认定检查点后固化;见 data/templates/)
node bin/driftmon.mjs template build --fingerprint <官方 system_fingerprint> --name "flash 1017"
node bin/driftmon.mjs template check          # 最新窗口对模板比较 → results/watch/gray-watch.json

# T 通道官方参照(离线工件;不联网、不花 token;见 docs/TOKENIZER-REFERENCE.md)
node bin/driftmon.mjs reference check     # 实测 T 向量 vs 官方参考 → results/reference/tokenizer-check.json
node scripts/check-upstream.mjs           # 上游 deepseek-recipe / DeepInfer 是否有更新(可选)

配置

配置文件:复制 config.example.json 为 driftmon/config.json(gitignored)。 官方端点采集另见 config.official-deepseek.example.json。

关键约定:

  • key 永远走环境变量:配置里只写 apiKeyEnv(环境变量名),不写明文。
  • 被动层存储目录解析顺序:config.json 的 plugin.dshHome → 环境变量 DSH_HOME → 默认 <driftmon>/../home(仅当与 DSH 树并排放置时成立)。换机器时写一份 config.json 或 export DSH_HOME=/path/to/dsh/home 即可。
  • 所有判定阈值在 judge.thresholds / gray.thresholds,默认值已按 Phase 0 噪声底 (官方 flash 1017,30 轮 / 240 批)校准:lMaxTopKJaccardDrop=0.10、sTtftRatio=2.5, G/C 与 gray.* 复核后保留;依据与回放数据见 docs/EVIDENCE.md E10。 换端点 / 换模型 / 换探针组必须重跑 Phase 0,不得沿用这套阈值。
  • 探针组冻结在 probes/probe-set.json(probeSetVersion + probeSetHash);换探针组必须重采基线、重建模板。

已知检查点模板(灰度检测的参考物)

官方端点的 system_fingerprint 是「检查点」的权威标识;把同一检查点在冻结探针组下的行为签名 (T/L/G/C/S)固化成模板,作为以后比较的参考物:

  • 官方灰度检测:指纹没变、但行为通道相对模板偏移或出现双峰 → 疑似灰度/静默变更; T/C/S 偏移更像配置/服务栈变化,L top-k / G 偏移更像权重变化。
  • 无指纹网关反推:网关不返回指纹时,用行为签名对模板挑最接近的检查点。

已固化:data/templates/checkpoint-flash_1017.json(2026-10-20 官方端点 5 轮完整巡检 = 40 批 / 1440 样本 / 0 错误;T 向量 [18,17,17,39,16] 40/40 一致、G 失败 3/14 且集合 40/40 一致、 L top-k 中位 0.819、C 命中 0.889、TTFT 中位 117ms;L 的 logprob 数值退化为 0,只用 top-k 集合)。 template check 用最近一轮对模板比较 → results/watch/gray-watch.json(当前 stable)。 重建方法与限制见 data/templates/README.md。

边界:模板对比(template check)已经能产出 gray-candidate/checkpoint-change 等告警; 判定阈值已按 Phase 0 噪声底校准(L 0.10 / S 2.5,见 E10),灰度阈值(gray.thresholds) 复核后保留默认值。灰度是否可测取决于它是否改变这些可测量分布。

T 通道官方参照(tokenizer reference)

T 通道原来只能「跟自己的历史基线比」:如果第一次采集时链路就已经被换过,就永远发现不了。 2026-10-20 官方开源 deepseek-recipe(生产级 Token 编解码组件,官方称其处理逻辑与 DeepSeek 官方 API 完整对齐)后,我们可以离线算出冻结 探针组的期望 prompt_tokens,作为绝对参照。

已固化:data/reference/official-tokenizer-v41.json(deepseek-recipe 0.2.0 + 自带 v41 tokenizer, tokenizer 内容由 SHA-256 锁定)。实测 5 条 T 探针全部精确对上:

探针官方参考实现期望官方端点实测(E9/E10,240/240 批)
t-ascii / t-cjk / t-rare / t-special / t-unicode18 / 17 / 17 / 39 / 1618 / 17 / 17 / 39 / 16

即 flash 1017 模板的 T 向量有官方实现背书,不只是「我们自己观测到它稳定」。

该结论已经固化进模板工件:data/templates/checkpoint-flash_1017.json 的 officialReference 块记录期望向量、参考实现版本与 vectorMatch=true;template build 会自动写入, template check 会显示「模板记录 / 当前窗口」两行核验状态(不改变 verdict 与告警 schema)。

node bin/driftmon.mjs reference check   # 实测窗口 vs 官方参考 → results/reference/tokenizer-check.json

match 表示 tokenizer / prompt 模板 / 请求渲染路径与官方参考一致(不代表权重没变); mismatch 会给出逐探针差异,按 docs/TOKENIZER-REFERENCE.md §3 排查(tokenizer 换版 / 模板变化 / 网关改写 prompt),不要直接当模型漂移。 工件生成、刷新纪律与上游更新关注见同一份文档;上游更新可用 node scripts/check-upstream.mjs 检查。

告警合并到展示层

template check 写 results/watch/gray-watch.json(通用 schema v1,不绑定具体宿主)。 dsh-system-fingerprint 可选配置 externalAlertFile 指向它:warn/critical 时提醒合并进指纹徽标(⚠ + 警示色),详情进悬浮卡片。 等级映射:checkpoint-change(指纹变化)→ critical(徽标红色 ⚠); gray-candidate / config-change → warn(黄色 ⚠);infra-change → info(不变色,仅悬浮卡); stable → ok。插件不感知 driftmon;文件缺失/损坏静默降级。这个文件属于可选展示契约, 不属于两个仓库必需的文件契约(后者见 docs/DEPENDENCY.md)。

数据与目录约定(四层分离)

driftmon/
├── README.md              ← 你在这里:定位、怎么读
├── bin/driftmon.mjs       ← CLI 入口
├── config.example.json    ← 配置样例(config.json 不入库)
├── config.official-deepseek.example.json  ← 官方 DeepSeek 端点采集/模板配置样例
├── package.json
├── docs/                  ← ① 文档层:设计与前置知识(人写、可评审)
│   ├── DESIGN.md          ← 设计文档:目标、边界、架构、判定规则、开放问题
│   ├── PREREQUISITES.md   ← 前置知识:实测得到的硬事实与陷阱(写实现前必须读)
│   ├── DATA-MODEL.md      ← 四层数据结构的字段定义与不变量
│   ├── TOKENIZER-REFERENCE.md ← T 通道官方参照:工件、重建流程、上游更新关注
│   ├── DEPENDENCY.md      ← 与 dsh-system-fingerprint 的两仓库依赖契约
│   └── ROADMAP.md         ← 阶段划分与验收标准
├── probes/                ← ② 配置层:探针组的定义(数据,不是代码)
│   ├── probe-set.json     ← 冻结探针组(T/L/G/C/S 五组共 22 条探针 / 36 请求)
│   ├── cache-prefix.txt   ← C 组固定长前缀(字节由 promptSha256 锁定)
│   ├── glitch-tokens.json ← G 组 token 清单与来源/置信度
│   └── NOTES.md           ← 冻结规则、来源、已知缺口、冒烟实测
├── src/                   ← ③ 代码层:采集 / 签名 / 基线 / 判定 / 报告(纯 Node,无外部依赖)
├── data/                  ← ④ 原始数据层:append-only,不删不改
│   ├── raw/               ← 每次采样的原始输出(数值摘要 + 端点溯源 + 官方 system_fingerprint;不存 prompt / 响应正文)
│   ├── baselines/         ← 滚动基线:每通道的分布统计
│   ├── templates/         ← 已知检查点行为模板(派生参考:T/L/G/C/S 签名;从 raw 重建)
│   └── reference/         ← 官方 tokenizer 参考工件(入库;离线算出的期望 T 向量,人工评审后更新)
├── results/               ← ⑤ 判定层:结论(人可读报告 + 机器可读告警)
│   ├── reports/
│   ├── alerts/
│   ├── watch/             ← 行为告警(模板对比 → gray-watch.json,可被展示层读取)
│   └── reference/         ← T 通道核验结果(tokenizer-check.json,可重建)
├── scripts/               ← 采集/标定/核验/上游检查脚本(含 dev-only Python 参考工件生成器)
└── test/                  ← 离线测试(node:test;桩 fetch,不联网)

注:本目录树用 ①–⑤ 数五个目录层;docs/DATA-MODEL.md 里的"层①②③④"是数据语义层 (原始 / 签名 / 基线 / 判定),其中签名层是纯函数派生、刻意不给独立目录。两套编号含义不同,不要混读。

为什么要分这么细:这个窗口在端点登记表上踩过一次坑——第一版采集器是"每次重建", 重跑一次就把上次扫描的成果冲掉了(那轮新发现 17 个、登记表总数 19)。教训是原始观测、派生摘要、判定结论必须分开存: 前者不可再生,后者随时可重算。driftmon 把这条当硬约束。

数据不入库:data/raw/*.jsonl、data/baselines/*.json、results/alerts|watch|reports|reference/* 都是本机生成物(见 .gitignore),随用随弃、可重算;入库的是探针定义、模板、官方 tokenizer 参考工件、文档与代码。data/raw/2026-10-17-transcripts/ 是早期人工转录,用于追溯实测结论的来源,不是机器产物。

怎么读这套文档

你想干什么读哪份
让 DSH agent 安装/升级本仓库docs/AGENT-INSTALL.md(agent 流程:符号 → 验证 → 配置 → 工作流 → 回滚)
本机路径等字段怎么落地docs/SYMBOLS.md(符号映射表;真实值放 .local/symbols.env,gitignored)
跑 / 复核 Phase 0 噪声标定docs/PHASE0-PLAN.md(runbook + 2026-10-20 已完成记录:成本/时间、采集脚本、noise-floor、阈值 review)
决定要不要做、做成什么样docs/DESIGN.md
动手改代码之前docs/PREREQUISITES.md(必读,全是实测结论和坑)
想知道文档里的数字是哪来的docs/EVIDENCE.md(证据索引:脚本 / 产物 / 复现命令 / 可信度;E11 = T 向量的官方参照)
想核对 T 通道是否还和官方 tokenizer/模板一致docs/TOKENIZER-REFERENCE.md(工件、reference check、重建与刷新纪律、上游更新关注)
想知道两个仓库怎么放、接口是什么docs/DEPENDENCY.md
设计数据结构 / 改字段docs/DATA-MODEL.md
想知道先做什么、做到什么算完成docs/ROADMAP.md
动探针之前probes/NOTES.md(冻结规则 + 来源 + 已知缺口)
想看第一手观测data/raw/2026-10-17-transcripts/(转录,非机器产物,用前请重跑复核)

与插件仓库的关系

本项目是两个独立仓库:

仓库角色
Mizumic/dsh-system-fingerprint被动层(DSH 插件):拦截真实响应、探测 system_fingerprint、网关溯源、端点登记表
Mizumic/driftmon(本仓库)主动层:定时探针组、签名、基线、漂移判定、报告、行为模板与告警

两个仓库互不 import 代码,只靠只读文件契约相连:driftmon 只读插件的 $DSH_HOME/storages/system-fingerprint/endpoints.json(v1,必需)与 config.json(v1,可选), 缺失/损坏/版本不认识都只降级告警。契约细节见 docs/DEPENDENCY.md。

边界(写在最前面,防止这个项目被期待成别的东西)

  • 它是观测,不是断言:只能告诉你「某个可测量的东西变了」,说不出「变成了什么」。
  • 供应商不改指纹又只换权重、不换 tokenizer 时,各通道都很弱,此时只有统计置信度。
  • T 官方参照只覆盖 T 通道:reference check 的 match 只说明 tokenizer / prompt 模板与官方参考 一致,不代表权重没变;权重漂移看 judge / template check 的 L/G 通道。
  • 不做内容质量评测:任务正确性、风格、工具调用能力属于另一件事,不在这里。
  • 不存 prompt / 响应正文:只存数值摘要与(脱敏后的)响应头。
  • 主动监测不默认常驻:插件不会自动跑它;要采就显式跑 CLI(cron / systemd / 手动)。

开发与测试

npm test                     # 77 项离线测试(node --test;桩 fetch,不联网)
npm run status               # 等价于 node bin/driftmon.mjs status
npm run upstream:check       # 检查上游 deepseek-recipe / DeepInfer 是否有更新(联网,可选)
node bin/driftmon.mjs probe-set hash   # 查看冻结探针组哈希

约定:

  • 纯 Node、零依赖:src/ 只 import node: 内建模块;不引入第三方运行时依赖。
  • 原始层 append-only:采集只追加 data/raw/*.jsonl,不修改历史;派生层(baseline / template / alert / report)随时可从原始层重算。
  • 证据先落盘:任何产生文档数字的探测都必须由脚本写出文件,禁止「只 print」和一次性内联脚本 (见 docs/EVIDENCE.md)。
  • T 参照工件只经人工评审更新:data/reference/official-tokenizer-v41.json 由 scripts/build-tokenizer-reference.py 生成候选、人工 review 后替换;运行时只读,不联网。
  • 探针组冻结:改 probes/ 必须 bump probeSetVersion,并重建基线/模板;probeSetHash 不一致时拒绝比较。
  • key 不进代码/配置/日志:只从环境变量读。

当前状态与已知限制

  • Phase 0 已完成(2026-10-20):官方 flash 1017 同一天 30 轮 / 240 批 / 8640 请求 / 0 错误, data/baselines/noise-floor.json 的 verdict=calibrated、五通道可用;judge 阈值已按噪声底校准 (L drop 0.10、S ratio 2.5)。阈值只对该端点/模型/探针组有效;drift-confirmed 在跨端点回放 验收(Phase 2)之前仍只用于结构验证,不能当作对外结论。
  • template check 的灰度阈值(config.gray.thresholds)经 Phase 0 复核后保留默认值 (噪声底未显示需要调整的证据);gray-candidate 只是「疑似」,需要连续复现。
  • L 通道存在小时级非平稳性:同指纹下 top-k Jaccard 在 0.786↔0.923 模式间漂移(G/C/T 恒定), 已按滚动基线计入噪声阈值,跨天/跨时段表现需复核(见 docs/EVIDENCE.md 待补)。
  • 行为模板 ≠ 版本识别:模板只能回答「行为是否偏离已知检查点」,不能证明具体版本。
  • T 通道官方参照(2026-10-20):data/reference/official-tokenizer-v41.json 的期望 T 向量 [18,17,17,39,16] 与 deepseek-recipe 0.2.0 + v41 tokenizer 的离线计算结果逐项一致, flash 1017 模板的 T 向量有官方实现背书(E11);reference check 可用于发现 tokenizer / prompt 模板 / 请求渲染路径的变化。工件刷新需要人工 review,上游更新用 npm run upstream:check 检查。
  • 真实网关单会话冒烟已跑通;完整 5 轮官方巡检数据见 docs/EVIDENCE.md(E9), Phase 0 的 30 轮标定原始层在 data/raw/2026-10-20.jsonl(gitignored,240 批)。

贡献

欢迎 issue / PR。开始前请读 CONTRIBUTING.md:开发流程、 测试要求、探针/数据层硬约束。行为准则见 CODE_OF_CONDUCT.md。

安全

本项目处理 API key 与模型响应证据:key 只走环境变量;不存 prompt / 响应正文。 安全问题请不要开公开 issue,按 SECURITY.md 的渠道私下报告。

变更记录

见 CHANGELOG.md。版本号遵循 SemVer。

许可证

MIT © 2026 Mizumic