claim-verify-wscv0.7.0
borui1757-cmd/claim-verify-wsc
dsh plugin --profile web add github:borui1757-cmd/claim-verify-wsc#b3479d47ede7acd3f550cf721cf20802d7421b49
介绍
Verify what AI agents claim they did — against real OS state, with signed receipts
标签
概述
claim-verify-wsc
不给答案打分,去问真值。 Score nothing. Verify against reality.
让 agent 能碰生产环境——因为每一步都能被核对,且留下可举证的回执。 Let agents touch production: every step is checkable, and every step leaves an attestation.
不是给模型加一个更聪明的嘴,是给它发一张可以碰生产的许可证。
三分钟看懂:它测出了什么
执行类幻觉(说做了、其实没做)可以压到 0,但有两个条件。
| 干预 | 结果 | 复现 |
|---|---|---|
| 把核验结论放进工具结果(而非事后消息) | 无保留谎报 20/20 → 0/20 | ✅ 两次独立 |
只报状态(size)不给差异 | 无帮助(与不报相同) | ✅ 两次 |
报"与意图的差"(mismatch + actualBytes/expectedBytes) | 谎报 20/20 → 0/20 | ✅ |
| 事前接种(任务前描述失败模式) | 明确判失败 0/20 → 13/20(带措辞指令 20/20) | 单次 |
| 讲道理 / 加强措辞 / 强制二元 | 无效 | ✅ |
基准(bench/,真实磁盘真值,DeepSeek,n=4/格)
| 场景 | 不告诉它 | 告诉它差异 |
|---|---|---|
| 截断写入 | 谎报 4/4 | 谎报 0/4 |
| 同长替换(只有哈希能发现) | 谎报 4/4 | 谎报 0/4 |
| 幽灵删除(返回 ok、文件仍在) | 谎报 4/4 | 仍 4/4 ⚠️ 布尔字段无效 |
三条结论:
- 核验必须作为"观测"返回(工具结果),事后劝告无效;
- 必须报"与意图的差",只报状态等于没报;
- 规则分任务类型:写入类认
mismatch,删除类必须改ok或给明确文字。
复现全部数字:npm test(55 个测试,零网络零模型)+ node bench/run.mjs。
框架无关:核心是纯 Node、零第三方依赖;DSH 只是其中一个适配器(lib/index.js 的 apply),
CLI 和程序化 API 在 DSH 之外也能直接用。名字里的 wsc 是作者署名。
⚠️ 实验版(0.5.0-experimental.1):默认 observe —— 只写日志,不追加说明、不打断对话。 真实对话里的真阳性样本还是个位数,"误报率"目前只有回放数据 + 26 条构造评测,没有大规模独立标注。 想让它开口,配annotate: true;想先看它在你这台机器上抓到什么,读~/.dsh/dsh-claim-verify/hits.jsonl。
一个挂在 agent 输出钩子上的断言核验器:把回答里「能核验的断言」抽出来,向本机要真值;对不上就标出来(可选:触发升级重答)。
和市面上主流方案的区别:默认层不用第二个模型当裁判。默认开启的核验全部是程序真值(端口探活、existsSync、日历、算式、目录计数),零模型调用;句子级模型核验是可选层,默认关闭。
它和 LLM 裁判是互补,不是替代(2026-11-05 实测)
同一批 26 条样本、同一份事实清单,两种方法各跑一遍:
| 断言域 | 本插件(程序真值) | deepseek-chat 当裁判 |
|---|---|---|
| 机械事实(端口/路径/日期/算式/计数)18 条,其中 10 条是错的 | P 100% / R 100% / F1 100%0 次模型调用 | P 100% / R 40% / F1 57% |
| 叙事结论("我改好了"/"服务停了"/"全部成功")8 条,其中 5 条是错的 | R 0% | P 100% / R 100% |
所以:它不替代 LLM 裁判,只补「机械事实」这一类。强模型漏掉的 6 条全是硬事实——18080 端口、不存在的路径、12 月 6 日、1873×4=7000、1024/8=64、21 个 html 页面。
成本对照:整批 26 条,程序真值 0 次模型调用;deepseek-chat 裁判 26 次调用、约 21 秒。
复现:node eval/compare.mjs --judge=deepseek,完整输出见 eval/results-2026-11-05.md。
样本用构造法生成(真值现场确认),共 26 条;样本量还小,不能外推到"超过大厂"。
安装(实验版)
普通的 DSH bundle 插件,装法和其它本地插件一样:
- 把本目录放到任意位置(例如
<workspace>/plugins/claim-verify-wsc) - 在 profile 的
package.json里加两处:
"dependencies": { "claim-verify-wsc": "link:<插件绝对路径>" },
"dsh": { "profile": { "bundles": [ /* ... */ "claim-verify-wsc" ] } }
- 手工建
node_modules/claim-verify-wsc→ 指向插件目录的 junction / 软链 (不要对正在运行的 profile 跑pnpm install,这条我们踩过坑) - 重启宿主
验证:~/.dsh/dsh-claim-verify/loads.jsonl 里出现 "event":"loaded" 就说明加载成功。
为什么需要它
agent 的假话有一半是「可核验的」:它说端口是 64164、文件在某个路径、今天周几、987×13 等于多少。这些不需要判断力,只需要去问一次真实系统。
但绝大多数防护方案做的是:
- 再叫一个模型判好坏(LLM-as-judge / RAGAS faithfulness)—— 贵、慢、自己也会编
- 只判有没有引用来源(attribution)—— 有来源不等于说对了
- 只在发布前跑回归(offline eval)—— 拦不住运行时现编
这个插件走第四条路:能核验的就去核验,核验不了的不装作知道。
核验什么(可扩展)
| 类别 | 抽取信号 | 真值来源 |
|---|---|---|
| 端口 | 127.0.0.1:8271、「端口是 8271」+ 在跑/监听类断言词 | TCP connect 探活 |
| 路径 | C:... / ~/... + 在/是/配置类上下文 | fs.existsSync |
| 进程 PID | 「PID 71144 还在跑 / 已经不在了」 | process.kill(pid, 0)(EPERM 也算存在) |
| 文件行数 | 「x.log 一共 65 行」 | 读文件数换行 |
| 文件大小 | 「x.log 大小 20 KB」 | fs.statSync().size,2% 容差 |
| 文件哈希 | 「sha256 前 8 位是 a1b2c3d4」 | 现算 sha256 前缀比对 |
| 依赖版本 | 「claim-verify-wsc@0.2.0」「x 的版本是 1.2.3」 | 读 node_modules/<name>/package.json |
| 内存余量 | 「可用内存 12 GB」 | os.freemem(),25% 容差 |
| 磁盘余量 | 「C 盘剩余 500 GB」 | fs.statfsSync(),25% 容差(无子进程) |
| 服务状态 | 「服务 Spooler 正在运行 / 已经停止」 | spawnSync('sc', ['query', name]) |
| 注册表值 | 「HKLM…ServicesSpooler 的 Start 是 2」 | spawnSync('reg', ['query', key, '/v', name]) |
| 计划任务 | 「计划任务 X 状态是就绪」 | spawnSync('schtasks', ['/query','/tn',name,'/fo','list']) |
需要 spawn 的三类有硬预算:单次 400ms 超时、每轮最多 3 次、同名真值 20 秒缓存。
实测(本机,spawnSync 不经 cmd shell):sc 13ms / reg 12ms / schtasks 30ms;
对照 powershell Get-Service 429ms —— 任何"顺手用 PowerShell 查一下"的实现都是错的。
踩坑记录(2026-11-05):这些系统工具在中文 Windows 上输出的是 OEM 代码页(GBK/936), 按 UTF-8 解码会把「模式: 就绪」变成乱码,正则永远匹配不上。现在统一用new TextDecoder('gbk')解码(英文系统下对 ASCII 恒等)。另外schtasks的运行状态在 「模式」 栏而不是「状态」栏——「计划任务状态: 已启用」是另一个字段,且必须锚定行首, 否则「登录状态: 只使用交互方式」会被误当成任务状态。
| 时间 | 「现在时间是 05:30」 | new Date(),±5 分钟 |
| 日期/星期 | 「今天是 11 月 5 日 / 星期三」 | 本地日历 |
| 算式 | 「1873 乘以 4 等于 7000」 | 实际计算 |
| 计数 | 「一共 9 个 html 页面 / N 个按钮」 | 目录读取、DOM 计数 |
| 模型 | provider/model + 当前/本会话类断言 | 账单记录里本会话最后一条 |
| 金额 | 「今天花了 ¥12」 | 账单汇总,带容差 |
设计原则:取不到真值就放过(unknown),绝不猜;疑问句、举例、假设一律不抽。
四种用法
A. 输出钩子(本仓库的形态)
ctx.on('llm/stream', (options, next) => {
// …收全文,在 finish 前追加核验结论
});
对不上的断言追加一段说明(不覆盖原文,改写会破坏流式与工具调用交错):
【核验】本条回复有 1 处断言与实际不符(程序刚跑的,不是模型判断):
- 端口 64164 → DSH 实际端口是 3080
B. 级联触发器(已实现,默认关闭)
把「真值核验失败」当作升级重路由的触发条件:
便宜模型出答案 → 真值核验 → 对不上 → 下一次请求换强模型 → 再核验一次
现有级联(cascade)几乎都用模型自评/置信度/第二个模型打分来触发。 用「本机真值对不上」触发的,我们在公开项目里还没见到——同类项目(writeguard、 verifier-agent 等)做的是事后报告或拦截,不拿核验结果去驱动重路由。
为什么挂在 agent/request 而不是 llm/stream:后者的签名是
(options, next) => AsyncIterable,而 next 是 () => AsyncIterable——不接受参数,
第二次调用只会用同一个已准备好的调用再请求同一个模型,换不了 provider/model;
而且下游插件(比如计费)会被重复触发。agent/request 的契约允许监听器直接返回改过的
{ provider, model },这才是升级的落点。
语义边界(别夸大):它提升的是下一次请求所用的模型,不是把这一轮的回答推倒重来。 真正的"重答同一句"要在 agent loop 层面重放,那是另一个量级的改动。
config:
cascadeEnabled: true
cascadeProvider: deepseek-account
cascadeModel: deepseek-flash
cascadeMax: 2 # 单会话最多升级几次,防循环
规则:只有硬不符(非低置信类)才触发;已经在目标模型上就不再升;升级记录写
escalations.jsonl,用来统计"升级之后是否真的更准"。
C. 命令行(在 DSH 之外也能用)
node bin/claim-verify.mjs "服务 Spooler 已经停止" # 人读
node bin/claim-verify.mjs --json "可用内存 99 GB" # 机器读
echo "端口 3080 在跑" | node bin/claim-verify.mjs # stdin
有硬报时退出码为 1,可以直接塞进 CI 当门禁。
D. 程序化 API(接任何框架)
import { verifyText, renderNote } from 'claim-verify-wsc';
const { claims, checked, bad } = await verifyText(modelOutput, { sessionId: 'x' });
DSH 只是其中一个适配器。 真正跟 DSH 绑定的只有三处:apply(ctx, config)、
ctx.on('llm/stream')、以及 cordis 的 bundle 注册;抽取层(lib/index.js 的
extractClaims)和真值层(lib/host.js、lib/extra.js)是纯 Node、零第三方依赖,
可以直接接到 OpenAI 代理中间件、LangChain callback、MCP server 或任何有"输出钩子"的地方。
@deepseek-ai/schemastery 只在 lib/config.js 里用,且是动态加载——没有它也能跑。
默认 observe,不是默认拦截
发布版默认 annotate: false——只写日志不追加。原因很直白:误报比不报更烦人。
我们自己的实测数据(2026-11-05 一晚,~/.dsh/dsh-claim-verify/hits.jsonl):14 次核验、35 条断言命中,其中真错只有 5 条(全是端口:说 64164/8271 而实际 3080),其余 30 条是误报——
- Markdown 链接尾部的
>被当成路径的一部分(...index.js>) - 配置值
perConversationCny: 0.01被当成「今天花了 0.01」 - 单价
¥1/M、¥4/M被当成花费 notes/00-ideas.md、qq-bridge/config.json被当成provider/model
精度不到 15%,所以默认必须是 observe。 现在的策略是置信分层:
| 层 | 类别 | 行为 |
|---|---|---|
| 高置信 | 端口 / 路径 / 时间 / 日期 / 算式 | 核验并报告(实测 5/5 命中全对) |
| 低置信 | 金额 / 模型 / 计数 | 只记日志,不报告、不触发升级(实测 30/30 全错) |
七条防误报规则(已修):
- 路径排除
<>与引号,尾部标点清洗 - 金额只认「今天/累计」口径,且排除上限/单价/配置上下文
- 模型名排除文件扩展名与目录前缀(
plugins/、notes/这类),占位符(provider/model)过滤 - 取不到真值一律放过
- 句内作用域:断言词必须与候选值同句;假设句(比如/如果/可能)、问句整句放过
- 极性判定(NegEx 精简版):句内扫否定线索词(没有/不是/未/不在…)+ 24 字作用域, 伪否定(不仅/不但/不得不)先抹掉;否定断言不是丢弃,而是期望值取反核验—— 「18080 没有服务在监听」探活为 false 才算对,「3080 明明在监听却说没有」照样报
- URL 里的 host:port 不算端口断言(
127.0.0.1:18080/authorize?...是登录链接,不是监听状态)
规则 6 的依据:NegEx 的「线索词 + 作用域」是临床 NLP 的标准做法,词表 191 条含 11 条伪否定 (NegEx);「抽取要 precision-first、宁可少抽也别抽错」 与 FActScore 对命题分解的经验一致(FActScore, EMNLP 2023)。
第二轮实测(2026-11-05 07:00,加完规则 5–7):拿 hits.jsonl 里 25 条历史误报原文回放,
仍会抽出断言的只剩 3 条 —— 其中 2 条是刻意写的错断言(__no_such_file__.md,本就该报),
1 条是被正确识别成否定式的 18080。误报从 25/25 降到 0/25。
看日志攒够样本,再开拦截。 这条比任何宣传语都重要:一个误报率 85% 的核验器,比不装更糟。
配置
- id: claim-verify-wsc
name: claim-verify-wsc
config:
enable: true
annotate: false # 默认只记不拦;要看到【核验】说明改成 true
budgetMs: 1500 # 核验预算,超时放过
portTimeoutMs: 300
moneyTolerance: 0.3
pagesDir: '' # 可选:数 html 页面的目录,不配就跳过计数类
buttonsFile: '' # 可选:数按钮的 html 文件,不配就跳过
日志:~/.dsh/dsh-claim-verify/hits.jsonl(每次命中)、loads.jsonl(加载探针,用来确认插件真被宿主加载)。
日志目录名沿用了旧包名,为的是不改动历史数据位置。
局限(写清楚,不吹)
- 只覆盖可核验类断言。「为什么这个模型不聪明」「这个方案好不好」这类没有真值源,它不碰。
- 真值源要接:账单路径、服务端口、目录位置都是环境相关的,本仓库带的是 DSH 本机默认值。
- 追加的说明会进入下一轮上下文,模型可能引用它(有意为之)。
- 不是评测替代品:它管运行时,回归测试管发布前。
自测
node test/dry.mjs # 抽取 + 核验 + 拼接(注入假真值,不联网)16 个用例
node test/hook.mjs # 端到端:假流 → finish 前追加
node test/policy.mjs # 置信分层 + 解析修复(防止再退回 85% 误报)
node test/resolve.mjs # profile 侧 junction 能否解析到插件
node test/replay.mjs # 拿 hits.jsonl 里的真实误报原文回放,量误报率
评测(eval/)—— 「最强」只能靠数字说
test/ 管回归,eval/ 管指标。两者别混。
eval/不随 npm 包发布(corpus.jsonl含真实会话片段),只在仓库里跑; 发布出去的是lib/ + bin/ + test/ + 文档。npm test在任何环境都能跑。
node eval/gold.mjs # 金集:真值由构造决定,能同时算 precision 与 recall
node eval/corpus.mjs 200 # 真实语料:从会话原始记录抽 200 条回复,统计覆盖率 + 待标注清单
node eval/score.mjs # 用 corpus.jsonl 的 label 算真实语料 P/R
首轮结果(2026-11-05,57 条金集用例)
| 口径 | precision | recall |
|---|---|---|
| 对用户可见的报告(端口/路径/时间/日期/算式/进程/文件属性/依赖版本/内存磁盘/服务/注册表/计划任务) | 1.000 | 0.850 |
| 含只记日志的低置信类(模型/金额/计数) | 1.000 | 1.000 |
recall 的 3 条缺口全部来自「故意不报」的低置信类(金额/模型/计数口径歧义大),不是抽不到。
真实语料(200 条回复,稳定抽样):35 句含可核验断言、41 条断言;硬报 10 条。
逐条看:date/time 是状态过期(当时 02:24、现在 07:21);port 那几条是
「用 adb 连 127.0.0.1:16384」这类目标地址,不是「现在在监听」;剩下一部分是路径后来被删。
没有一条指向抽取层的结构性错误——结构性错误在上一轮已经被榨干了(见下)。
抽样必须稳定:早期用等距抽样,每加一条新会话下标全平移,三次跑出三批不同样本, 指标根本没法比较。现在改成按正文哈希排序取前 N,同一样本可复现。
方法论警告:历史文本必须按当时的事实判定,不能拿现在的系统状态当准。eval/corpus.mjs的核验结果只用来找抽取层 bug,不能当 precision 用—— 正是这样抓到并修掉了 8 个真 bug:C:Program Files被空格截断、#L29-L30行号锚点 被当成路径、祈使句「删 X」被当成断言「X 存在」、今天是星期三。(7 字)被长度保护整句跳过、 「C:x不在工作区里」被读成「C:x不存在」(否定作用域越界)、 带序号指令被当成端口断言、附加疑问句「…这个页面吧」被当成断言、 中文引号“…”后面的解释被卷进路径值;另外补了「adbd 没有运行起来 / → 不通」 这类端口否定线索、元文本(引用原句讨论语法)、命令行(adb connect …)三类放过规则。
与同类方案的位置
一、已有的同类项目(截至 2026-11-05)
这个方向不是我们首创。 下面这些都在做「核验 agent 的断言 / 完成声明」,按接近程度排:
| 项目 | 它做什么(按其自述) | 与我们的差别 |
|---|---|---|
| writeguard | 校验 coding agent 声称对文件系统做的改动;哈希基准、零依赖、框架无关 | 只覆盖文件系统;我们是 14 类本机断言 + 否定极性 |
| verifier-agent | 监视 Pi coding agent,把每条断言对照文件系统真实状态核验 | 绑定单一 agent;我们是通用输出钩子 + 可独立调用的 API |
| completion-checker | 对本地 agent 的任务声明做独立的文件系统核验 | 同上,且同样只到文件系统 |
| rashomon | 独立记录每次工具调用,标记「总结说测试全过、但其实失败」 | 它靠旁听工具调用推断;我们靠读输出正文抽取断言,不依赖 agent 框架的内部事件 |
| sentinel | 拦截工具调用,声明结果对照独立证据(diff / 执行轨迹 / 隐藏测试) | 它是完整 runtime 中间件;我们是单点插件,只做一件事 |
| Veract | local-first 可验证 agent runtime,「runtime 应该证明它完成了」 | 同上,runtime 级方案;我们可挂在任何有输出钩子的框架上 |
共同点:都认为「agent 不该自己宣布成功」。这一点我们不宣称原创。
需要说清楚的是:上面这些目前多是 0–1 star 的个人项目,没有成熟实现和社区。 但「没有成熟实现」不等于「我们更好」,只是目前还没人把它做扎实。
二、我们不一样的三点(都有代码支撑)
- 覆盖面:同类几乎只做文件系统。我们做 14 类,含端口探活、Windows 服务状态、 注册表、计划任务、进程 PID、内存磁盘——这些要
sc/reg/schtasks或 SCM API, 不是fs.readFile能覆盖的。 - 否定极性:「
127.0.0.1:18080本机没有服务在监听」不能当成「在监听」去核验。 我们用 NegEx 的线索词 + 作用域处理,并把否定断言期望值取反。同类里没见到这一层。 - 中文句式:同类都是英文句式。中文的否定(没有/不是/未)、附加疑问句(…吧)、 祈使句(删 X)都要单独处理,我们踩过的 8 个真 bug 里有一半属于这类。
另外一个差异:我们只追加说明,不拦截也不改写——改写会破坏流式与工具调用交错。
三、广义对照
| 方案 | 判定依据 | 额外成本 | 会自己编吗 |
|---|---|---|---|
| LLM-as-judge / RAGAS faithfulness | 另一个模型 | 一次模型调用 | 会 |
| Guardrails validators | 规则/检索 | 低 | 部分 |
| 发布前回归(hallu-suite 之类) | 固定题集 | 无 | 不会,但拦不住运行时 |
| 本插件 | 本机真值 | 无模型调用 | 不会 |
MIT. 只为解决一件事:让 agent 在说「端口是 64164」之前,先去问一句。
星级评价
基本信息
插件信息来自公开 GitHub 仓库与 SkillHub,版权归原作者所有;安装前请自行审查仓库代码。