driftmon
后端漂移主动监测:冻结探针组 / 五通道签名 / 行为模板 / 灰度告警(纯 Node,零运行时依赖)
- Stars
- 1
- Language
- JavaScript
- Created
- Oct 19, 2026
- Updated
- Oct 20, 2026
Introduction
driftmon · 后端漂移监测
一句话:在不依赖供应商声明的前提下,持续监测「我调用的模型后端有没有悄悄变」。
当前状态: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)。
目录
- 为什么做这个
- 环境要求
- DSH 版本声明
- 快速开始
- 配置
- 已知检查点模板
- T 通道官方参照
- 告警合并到展示层
- 数据与目录约定
- 怎么读这套文档
- 与插件仓库的关系
- 边界
- 开发与测试
- 当前状态与已知限制
- 贡献
- 安全
- 变更记录
- 许可证
为什么做这个
起因(2026-10-17 的实测):把上游从 deepseek-official 切到 opencode-go 之后发现两件事——
- 新上游根本不发布
system_fingerprint,官方那条"后端配置指纹"的通道直接消失; - 即使回到有指纹的官方端点,那个字段也只是供应商的一句承诺——它说没变就没变,你没有任何反证。
于是问题变成:能不能只靠自己观测到的机械行为,判断后端有没有变?
这件事的价值不限于 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.mdE10。 换端点 / 换模型 / 换探针组必须重跑 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-unicode | 18 / 17 / 17 / 39 / 16 | 18 / 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/只 importnode:内建模块;不引入第三方运行时依赖。 - 原始层 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/必须 bumpprobeSetVersion,并重建基线/模板;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-recipe0.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