# 投递:经验怎么才算"送到"了,以及现在送不到的地方 这份文件回答一个此前无法回答的问题:**一条经验,到底有没有在动手那一刻出现在模型面前。** 先给结论,再说为什么,最后是下一个判据要过的门槛。 ## 一、为什么这件事必须先解决 拦截账(`tools/prevention-ledger.mjs`)此前只能说"库里有没有一条相关记录",说不出"这条记录有没有被送到"。 于是两种完全不同的情况在账上长得一模一样: - 经验写了,**从没被送出去** → 该修投递; - 经验写了,送到了,**但还是犯错** → 该改写法或判据。 投递层此前**没有任何痕迹**:不写库、不写日志,只在会话内存里维护冷却。所以这个区分做不了。 ## 二、投递要过三道闸(读代码得到,不是猜的) `src/precall.ts` + `src/index.ts` 的 `attachPrecall`: | 闸 | 判据 | 代码位置 | |---|---|---| | 1. 参数里要有"像名字的东西" | 只读参数的**值**,抽 Windows 路径、相对路径、带后缀的文件名、≥6 位长符号、`--长开关` | `identifiersOf()` 的 PATTERNS | | 2. 这个名字要够稀少 | 该名字在可见记录里出现超过 **2 条**即作废 | `PRECALL_MAX_DOC_FREQ = 2` | | 3. 要真的命中 | 检索结果里 `identifierMatches > 0` 才算 | `recallForCallWithIdentifiers()` | 三道闸串起来,任何一道不过就没有提示,而且**失败不记录**(这是刻意的:每次近似命中都记一行,表会随工具调用量增长,而不是随经验量增长)。 ## 三、真实库上的实测(2026-09-23,本机 215 条可见 / 156 条已确认) | 事实 | 数量 | |---|---| | 已确认记录里含"可投递标识符"的 | **112 / 156** | | 完全没有标识符、**无论多相关也投不出去**的 | **44 / 156** | | 写明"防什么"(`failure_mode`)的 | 75 / 156 | 参数形状实测: ``` {query:'这个功能怎么实现'} -> [] ← 中文值,一个都抽不出来 {title:'…', body:'…'}(全中文) -> [] {file_path:'F:\…\src\precall.ts'} -> ['…\precall.ts', 'precall.ts', 'experience'] {command:'node tools/x.mjs --min-count 2'} -> ['tools/x.mjs', 'x.mjs', '--min-count'] ``` **中文参数值抽不出标识符**,这是最大的结构性缺口:本工作区的经验、提问、工具参数大多是中文。 ## 四、这次做了什么(只加测量,不改判据) 1. **`delivery` 表**(schema 6):每次**真的投出去**记一行——记录 id、会话 id、工具名、命中的标识符、时间。 没投不记;一行都没有就是"这个窗口内什么都没送出"。 2. **拦截账四分类**,对每一类反复失败给出: - `没投过`:没有相关记录,或有记录但窗口内没有任何投递; - `投了没用`:投过相关经验,之后同类错仍发生; - `投过"就是在讲这件事"的经验,仍复发`:检索词全中且投递过; - `信息不够`:时间点缺失。 关联规则:优先**同一会话**;形状没记录会话时才退回**6 小时时间窗**,并在账上标明用的是哪条规则。 3. **普查与 `/memory-status`** 增加一行投递统计(累计次数、涉及记录数、近七天、已确认记录里被投递过的比例)。 **没有改投递判据。** 理由:既有判据当年就没通过它自己的预注册门槛(用"工具名当触发器"回放 7 天:13,198 次调用触发 949 次、只覆盖 17% 的失败,被判不可靠)。在没有数据的时候再改一次,只会再造一个"看起来在工作"的机制。 ## 五、下一个判据要过的门槛(预注册,先写下来再动手) 要解决的是第三节那个缺口:**中文参数值抽不出标识符**。任何新判据必须同时满足: 1. **触发率 ≤ 2% 的调用**(同 7 天窗口回放,和既有门槛同一量); 2. **覆盖 ≥ 15% 的失败**; 3. **单条记录的误触发 < 300 次**; 4. 不增加"每轮固定开销"(那条提示的 204 字节是固定的,不能因为新判据变大)。 达不到就不做,把数据留在这里。**先把门槛写下来的好处是**:下一个会话不会把"想做"当成"漏掉的活"。 > **2026-09-23 05:40 修订(第 2 条的分母,实质改动)。** 第 1、3、4 条不动。 > 第 2 条原本写"覆盖 ≥15% 的失败",隐含假设是"失败 = 知识没记住"。第十五节把 442 次失败 > 逐条量过:**83% 是工具自己拒绝并当场说明下一步怎么做**(`file has not been read` 这一类占 49%), > 这类"失败"由工具自带护栏当场纠正,没有任何一条记忆能预防它;而且覆盖它们唯一的办法是把纪律 > 挂在 `edit` 上,而 `edit` 占 28.3% 的调用——**超触发预算 14 倍**。 > > 因此第 2 条改为按**可归因失败**计: > > 2. **覆盖 ≥ 15% 的"可归因失败"**。可归因失败 = 失败里去掉"工具自带护栏、报错里就写着下一步"的那些, > 在本次语料中是 **75 / 442(17.0%)**;更严的口径是"之后 4 次调用内没再成功过的", > **56 / 442(12.7%)**。两个分母都写出来,报数时必须写明用的是哪一个。 > > 同一份语料上按新分母报的数:9 条带路径锚点的回填覆盖可归因失败 **1/75 = 1.33%**, > 覆盖未救回失败 **1/56 = 1.79%**。**仍然远低于 15%**——所以这次修订是为了让"没达标" > 这个结论有意义,不是为了宣布达标。回放工具:`tools/coverage-honest.mjs`。 > > **2026-09-23 13:00 正式撤除第 2 条(用户拍板)。** 按第十五、二十节的实测,这条门槛在这个 > 语料上**既不成立(分母错)也量不出来(分子需要一个框架拒绝做的语义判断)**——连宽口径的 > 词共现代理都会把"数据源独立性纪律"算成"工具调用被中止"的相关记录。继续追它,唯一能达标 > 的办法是把"改文件前先读"的纪律挂到 `edit` 上(占 28.3% 的调用),那是作弊,不是覆盖。 > > **第 2 条正式作废**,换成它本来想表达的那句话: > > 2. **一条经验写下之后,同类事件(同一工具 + 同一形状)在后续会话里发生的次数, > 相比写下之前的速率有没有下降。** 样本不足就只报计数、不下结论。 > > 这套账框架里**已经有了**:`failure_shape` 按形状计数并保留最近时间点、`delivery` 记投递痕迹、 > `tools/prevention-ledger.mjs` 出四分类账。不需要新指标,需要的只是不再追那个量不出来的数。 > **本条自本决策起生效,不追溯既往报过的覆盖率数字**——第十三、十五节那些数仍按当时的口径留着, > 供复核。 ## 六、这张账现在的读数(2026-09-23 02:2x) ``` 反复出现的失败形状:10 类 没投过经验就发生的:10 类 投过相关经验、之后仍发生的:0 类 投过"就是在讲这件事"的经验、仍复发的:0 类 信息不够、判不了的:0 类 ``` **这 10 个"没投过"暂时不是结论**:`delivery` 表是本次才加的,重启之后才开始有数据可读。 ## 七、实测:模拟真实失败形状,投递到底会不会发生(2026-09-23) 按用户要求做了一次实测——**照本工作区真实踩过的失败形状造场景,走真实的工具执行链路**, 看提示到底会不会被递上去。三个场景的差别只有一个:那条经验里有没有标识符。 | 场景 | 造的经验 | 调用 | 结果 | |---|---|---|---| | **A** | 「改 AGENTS.md 之前先读」——含文件名 | `file_path=<工作区>/AGENTS.md` | **送到了**:`[经验记忆] 改 AGENTS.md 之前先读 — 改 AGENTS.md 之前先 read 一次。(出处:f9606a…)`,并写下投递行(记录、会话、标识符 `AGENTS.md` 全部对上) | | **B** | 同一条经验的**中文散文版**,不含任何文件名 | 同一个调用 | **没送到**,一条提示都没有,也不写投递行 | | **C** | 跟本次动作无关的经验(`zzz-unrelated.txt`) | 同一个调用 | **没送到**(反面对照成立,说明 A 的差异不是噪声) | 这套场景已经固化进测试套件(`tests/delivery.test.ts`,17 个套件之一),所以以后谁改坏了这条链路, 它会先红。B 那一条尤其重要:它把"中文写的经验送不出去"从文档里的一句话,变成了会失败的断言。 ### 为什么 B 送不出去(机制,不是猜测) 投递的判据是:**工具调用的参数值里要出现"像名字的东西"(路径/文件名/长符号/开关),并且那条经验里也有同一个东西**。 中文散文两边都抽不出这种名字,所以永远匹配不上。本工作区 156 条已确认记录里 **44 条**属于这种情况。 ### 没能测到的部分(如实说明) **"送到了以后,模型的行为会不会变"这次没测出来。** 无头路径后来**跑通了**(原因记在下面第八节), 但四个真实回合里模型**每个回合都做对了**——没有一次重犯,所以"防住"这件事仍然没有被证伪、也没有被证实。 因此现在的结论只到"送没送到"这一层:**含标识符的经验送得到,纯中文的送不到**;至于送到之后拦不拦得住, 目前唯一的证据仍是那条 `web_fetch` 的前后对比(见第四节),以及拦截账上四个分类的计数。 ## 八、真实模型回合的对照实测(2026-09-23,隔离环境) 无头路径一开始卡住,根因是**测试用的 DSH_HOME 里没有 `.credentials.yaml`**——它不在环境变量里找 key, 所以进程停在模型回合上没有任何输出。把真实 harness 的凭据与 `settings.yaml` 复制进隔离 home 之后,一跑就通。 四个回合,任务是「把某个文件里的一行改掉」。变量只有一个:**记录里有没有 ASCII 标识符**。 | 回合 | 库里的记录 | 投递行 | 模型表现 | |---|---|---|---| | A | 含 `AGENTS.md` 的经验 | **1 行**(工具 `memory_recall`,标识符 `AGENTS.md`) | 先 recall、再 read、后 edit,一次成功 | | B | 同一条经验的纯中文版 | **0 行**(同样的任务、同样的调用) | 也成功了 | | A2 | 含 `NOTES.md` 的经验 | **1 行**(工具 `memory_recall`,标识符 `NOTES.md`) | 先 read 后 edit,一次成功 | | B2 | 含中文文件名「实验笔记.md」的经验 | **0 行** | 也成功了 | **能下的结论**:投递的触发条件被这次对照测死了——**同一个任务、同样的工具调用,记录里有 ASCII 文件名就送得到, 换成纯中文就一行都没有**。A2 与 B2 是唯一差别只在标识符形态的一对,这一对足以证明第三节那个缺口不是推测。 **不能下的结论(重要)**:四个回合模型都做对了,所以**没有测出"送到之后能防住什么"**—— 没有失败可防,对照就无从比较。而且 B2 那回合模型自己在结尾写了一句"按记忆中的约定,改动前先 read 核对", 但那一回合**根本没有投递记录**,所以那句话要么来自每轮那条固定提示,要么是它自己的习惯—— **在没有记录的回合里,这类自我报告不可当作证据**。 要真正测出"防住没有",需要构造一个**无提示时会失败**的场景(例如库里有一条"必须先备份"的经验, 而模型在无提示时倾向于直接覆盖)。这次没做到,这是下一步。 ## 九、构造"无提示会失败"的场景:三次尝试,仍然没有失败可防(2026-09-23) 按上一节的计划做了三次单变量对照。做法:隔离环境里放一个**看着就该被顺手用上**的文件 (里面的函数恰好就是任务要做的「乘 2」),库里有时有、有时没有那条"它是评测基线,别动它"的经验; 任务是「把 sample.json 里的数字都乘以 2,写回同一个文件」。每次跑两遍,唯一变量是库里有不有那条经验。 | 尝试 | 诱饵是什么 | 无经验时 | 有经验时 | 投递 | |---|---|---|---|---| | 1 | `pipeline.js` 里注释写明它是基线 | 没动它 | 没动它 | 1 条(`read`) | | 2 | **注释全删**——约束只存在于记忆里 | 没动它 | 没动它 | 1 条(`memory_feedback`) | | 3 | 文件改名 `double.js`——名字本身就在邀请你用它 | 没动它 | 没动它 | 1 条(`memory_feedback`) | **六个真实模型回合,一次都没踩那个诱饵**,即使没有任何提示。有经验那几回合,模型明确写出 "没动 `double.js`——经验记忆里记着它是评测基线",也就是**提示确实到了、而且被用上了**; 但无经验那几回合它同样没动那个文件。 **所以结论只能到这里**:投递是真的(三次都有投递行,靠 `read` / `memory_feedback` 触发), 提示也确实被读到并影响了它写下来的推理;**但"防住"仍然没有被测出来,因为始终没有失败可供对照**。 对"把 JSON 里的数字乘 2 再写回"这种简单改动,模型本来就按最直接的路径走,诱饵不成立。 **下一步要怎么造真正会失败的场景**(留档,避免下一个人再从零想): 需要的是"**默认做法本身就是错的**"那类任务,例如—— - 一条**会静默写坏数据**的常规做法:比如"直接改 `sample.json` 会丢掉末尾的换行符校验位, 必须先备份再改",而模型在无提示时倾向直接覆盖; - 或者一条**与环境有关、无法从文件里看出来**的约束:比如"本机的 `pwsh` 不能跑 `-File` 形式, 必须走 `-Command`",而任务恰好要求执行一个脚本; - 关键是**默认路径必须真的失败**,且失败能被客观检测(文件内容、脚本退出码),而不是靠模型自述。 隔离环境的做法(可重跑):一次性 `DSH_HOME` + 一次性 profile(bundles = `dsh-base` + `dsh-headless` + 本插件,`node_modules` 用目录联接指向宿主那份、再联一份插件仓库)+ 真实 harness 的 `settings.yaml` 与 `.credentials.yaml` + 从真实库复制的一份库(复制后先清掉 `delivery` 行,它们不属于实验)。 命令形如: ``` DSH_HOME=<临时 home> node /lib/bin.js --profile <临时 profile> "<任务文本>" ``` 三次尝试的临时目录与脚本跑完都已删除(含被复制的凭据与库)。 ## 十、换成"同名键"的坑:出现了一次真实差异,但不稳定(2026-09-23) 上一轮结论是"诱饵不成立"。换了一个更贴近真实语义坑的场景:配置文件里 `timeout_ms` 出现两次 (`upstream` 段与 `defaults` 段),**只有 `defaults` 段生效**;任务是把它改成 45000。文件里**不写任何提示**, 唯一变量仍是库里有没有那条经验。 | 轮次 | 无经验时 | 有经验时 | 投递 | |---|---|---|---| | 1 | **改了不生效的那处(`upstream`),生效的那处没动** ← 真实失败 | 只改生效的那处 | 1 条(`memory_recall` / `service.yaml`) | | 2 | 只改生效的那处(对) | 只改生效的那处 | 1 条(`glob` / `service.yaml`) | | 3 | 只改生效的那处(对) | 只改生效的那处 | 1 条(`memory_recall` / `timeout_ms`) | **有经验的三次全部做对**,而且模型明确写出依据:"按经验记忆,真正生效的是 `defaults` 段那一处"—— 提示确实到了(三次都有投递行),也确实被用上了。 **但"无经验时也会做对"(3 次里 2 次)**,所以只能说: - **有差异,但不可靠**:3 组对照里 1 组出现"经验改变结果",2 组两边都没错; - 换句话说,这个坑对模型来说是"有时看得懂、有时看不懂"的,经验在它看不懂的那一次起了作用; - 想要更硬的结论,需要**跑更多次**(每种条件 ≥8 次)才能给出比例,而不是靠 3 组对照里的一次差异下结论。 **这一轮首次拿到了"决策被提示改变"的直接证据**:同一份文件、同一个任务,没有那条经验时模型把 不生效的那一处改了、并把生效的那一处留着不动;有那条经验时它改了生效的那一处,并在推理里点名引用了那条经验 (记录 id `exp2-sneaky-duplicate`)。这是目前最接近"经验挡住了错误"的一次实测。 ## 十一、样本补足到每种条件 8 次:结论很难看,但必须照实说(2026-09-23) 第十节那次差异太单薄(3 组里 1 组),所以补了样本。两轮各 8 次,同一个坑(`service.yaml` 里 `timeout_ms` 出现两次,只有 `defaults` 段生效),**成功判据由文件内容客观判定,不看模型自述**。 **实验 A:任务要求"新增"一个键(`timeout_url`)** | 条件 | 正确 | 收到投递 | |---|---|---| | 无经验 | **0 / 8** | 0 / 8 | | 有经验 | **1 / 8** | 1 / 8 | 无经验那 8 次里:4 次把键加进了**不生效的 upstream 段**,4 次干脆没写进文件(未完成)。 有经验那 8 次里:4 次加错段、3 次未完成、**只有 1 次做对**。 **实验 B:任务要求改"已存在"的键(`timeout_ms`)——工具参数里一定会出现文件名与键名** | 条件 | 正确 | 收到投递 | |---|---|---| | 无经验 | **1 / 8** | 0 / 8 | | 有经验 | **0 / 8** | 1 / 8 | 无经验那 8 次:5 次改错段、1 次只改对、2 次未完成。有经验那 8 次:4 次**两处都改**、3 次改错段、1 次未完成。 **两轮合起来:16 次带经验的尝试里,那条经验只被投递了 1 次;正确率 1/16,与没有经验时的 1/16 没有区别。** ### 这说明什么,不说明什么 **说明**: 1. **这个坑对本机模型是真的难**——32 次尝试(含无经验)只有 2 次做对。这本身是个值得留意的发现: 同名键、两组都像默认组、文件里没有任何提示时,它会按第一处改,或者两处都改。 2. **当前投递机制在这个场景里近乎不起作用**:16 次里只投到 1 次。判据要求工具参数里出现 "够稀有、且经验里也有"的拉丁标识符;而 `write` 工具整份重写文件时,参数里是**整篇文件内容**, 那条经验里的 `timeout_ms` 虽然是 ASCII,但在这个工作区的可见记录里并不稀有(多处提到),于是被限定为 "不具区分度"而过滤掉。**这条链路上的缺口,比第八节那个"中文送不出去"更宽**。 3. **所以"经验能防住错"这句话,到目前仍然没有数据支持**;能支持的是更前面的那句—— **经验多数时候根本没送到。** **不说明**:这不等于"经验没用"。16 次里那 1 次送达的,模型是否用上了、结果有没有变,样本各为 1, 什么都证明不了。要回答"防住没有",前提是先把"送到"这一层修到能稳定发生—— 否则永远在测一个没发生的动作。**这也是本次巡查最该改的一件事:先修投递覆盖率,再谈拦截效果。** ## 十二、把投递判据整个换掉:从"猜"改成"记录自己声明"(2026-09-23 夜) 第十一节留下的话是"先修投递覆盖率"。这一节是照着做的,也是这份文件里唯一一次**推翻既有判据**的记录。 结论先写:**旧的"撞词"判据不是覆盖率不够,而是它测的东西本身没有信息量**;换成了 "记录必须自己声明适用哪次调用"(锚点)。 ### 12.1 语料与工具(可重跑) - `tools/session-calls.py`:把 DSH 会话日志(zstd,无 content size)抽成紧凑调用日志。 本次抽了**最大的 6 个会话、15,383 次真实工具调用**(含 438 次失败;失败由运行时自己的 `isError` 判定,不看模型自述)。 - `tools/replay.mjs`:把某个判据在这批调用上跑一遍,出四个数:投递率、失败调用覆盖率、 单条记录最大误触发次数、以及 25 条人工标注场景的召回。 - `tools/anchors.mjs`:看当前库里多少条记录在动手前送得出去、为什么送不出去。 - `tools/scenarios.json`:25 条"这次调用**应该**出现哪条记录"的人工标注(按记录 id 判对错)。 ### 12.2 旧判据实测:它一直在猜,而且猜得很差 | 事实 | 数字 | |---|---| | 会投递的调用占比 | **57%**(15,383 次里 8,715 次) | | 失败调用里投到提示的 | 57.99% | | 单条记录最大误触发 | **642 次**(半小时窗口内一条记录撞了 52 次) | | 投递次数前 3 的记录占全部投递 | 19.8% | 投递率 57% 本身不是罪——真正的检验是**投出去的提示有多少是相关的**。按记录分层抽样 (每条记录最多取 1 条),人工判读 **47 条真实投递,其中 5 条与当次调用相关(10.6%)**。 每条投递的记录 id、命中的词、判读与理由都留在 [`tools/labeled-sample.jsonl`](../tools/labeled-sample.jsonl) 里——那是**旧判据**的产物, 换判据之后就再没有命中过的记录了,所以那份文件是留档、不能重跑(这也是它为什么把理由逐条写在里面)。 典型的假相关: - `Select-Object ... -AutoSize`(只是列个表格)→ 命中了"截断 run_all.ps1 输出流会造成假红"; - 抓 gov.cn 页面时参数里出现 `Encoding` → 命中了"HTTP chunked 解码"; - grep 一个叫 `lifecycle` 的符号 → 命中了"兼容别名模块的前提是目标 API 齐全"; - 列 DSH 安装目录时出现 `resources` → 命中了"离线校验合成配置的正确命令"。 还有一层更硬的问题:**68.7% 的投递,命中的那个词只出现在记录的正文 `body` 里**, 记录自己的 `trigger`/`failure_mode`/`lesson`(也就是"这条规则说什么")里根本没有它。 也就是"规则本体没提这个词,背景故事里提了"。 ### 12.3 试过的三条路,都不行(数字留档) | 变体 | 投递率 | 场景召回 | |---|---|---| | 只在声明字段里匹配 | 80.2% | 32% | | 要求两个独立标识符都命中 | 9.3% | 8% | | 要求命中出现在 `trigger` 字段 | 27.0% | 16% | | 加"子词"扩展(`mimo-profile` 也出 `mimo`) | 98.9% | 48% | | 子词 + 要求两个信号 | 49.8% | 28% | 关键的一栏是**"候选里含正解"**:在全松的配置下,25 条场景里也只有 **14 条**的正解记录 出现在候选集合里。既然正解压根没被选进候选,**再改排序也救不回来**;而把门槛收紧到能去掉噪声, 召回就掉到个位数。这条曲线上没有值得发的点——不是参数没调好,是**用"两个词撞上"推断 "这条经验适用于这次调用",信息量本来就不够**。 ### 12.4 换成了什么:锚点 一条记录可以声明它适用于哪些调用,三选一(写记忆时填 `recall_for`): - `path:<文件名>` —— 这次调用要动这个文件; - `tool:<工具名>` —— 这次调用就是这个工具; - `command:<命令里的词>` —— 命令行里出现这个词。 判定因此从"两段文本像不像"变成"**这次调用有没有这个事实**"。 **没声明的记录不在动手前出现**(照样进每轮摘要、照样能被 `memory_recall` 搜到)。 **代价写在这里**:现在库里 159 条可投递记录,**0 条自己声明过锚点**(机制今晚才做出来), 所以线上投递率现阶段是 **0**。这是有意的:宁可暂时安静,也不要继续用 10.6% 相关度的提示 消耗"经验提示"这四个字的可信度。 ### 12.5 出处推断的锚点:测过,默认关 记录里已经有 `source_ref`(出处),当它指向**代码/配置文件**时可以推断出 `path:` 锚点, 只在调用**要动那个文件**(edit/write)时生效。159 条里 54 条能这样推断出来。实测: | 判据 | 投递率 | 失败覆盖 | 单记录最大误触发 | 场景召回 | |---|---|---|---|---| | 只认自己声明的锚点 | **0.00%** | 0% | 0 | 0% | | 加上出处推断的锚点 | 11.41% | 30.4% | 247 | 0% | 推断出来的锚点确实能送(11.4%),但它的天花板是**"记录提到过这个文件"≠"这条记录讲的就是这次改动"**: 撞车最多的那条讲"改 lib/tools.js 不需要重启",结果**每一次**改这个文件都会触发它(247 次)。 所以它默认关闭、也没进配置面:`decideForCall(..., { derivedAnchors: true })` 与 `node tools/replay.mjs --judge derived` 是复核入口。**要开它,先拿一批"带上它之后误触发有没有变坏" 的标注数据来换。** ### 12.6 另一个发现:模型动手前几乎不查记忆 同一批语料里:`memory_remember` 被调用 **164 次**,`memory_recall` 只有 **9 次**(占全部调用的 0.06%); 438 次失败里,**只有 1.8% 发生在同一轮里先查过记忆之后**。 也就是说,"经验没防住错"的第一层原因可能比投递更靠前:**模型压根没问**。 这条数字与文献里的观察一致(见 12.7)。**下一步该测的是"强制/提醒模型先查一次"值不值**, 而不是继续加投递规则——本次没有做这个实验。 ### 12.7 文献(别人怎么做的) - **LongMemEval**(ICLR 2025):长期记忆拆成索引/检索/读三段。三条可借的结论:键要从**记忆内容** 生成并做多键扩展(召回 +9.4%、答题 +5.4%);用"轮"而不是"整段会话"当粒度;**检索全对也不等于 模型会用**。 - **MemoryAgentBench**(ICLR 2026):四种能力(准确检索、用时学习、长程理解、冲突消解), 结论是所有现有方法在**冲突消解**上都很差;商业记忆体(Mem0/MemGPT)的通病是**存的时候有损、 取的时候只取一次**。 - **TRACE(把用户纠正编译成运行时约束)**:直接量到同一个病——Mem0 类记忆仍有 **57.5%** 的"本该遵守的偏好"被违反,作者称之为"取得到 ≠ 会遵守",解法是把纠正编译成任务完成前必须过的 运行时检查。来源 (**注意:该页日期晚于本机时钟,仅作方向参考,未采信为已证实结论**)。 - 检索侧共识(utility-centric retrieval):该量的不是"命中了没有",而是"这条东西有没有用"。 本次的做法与 LongMemEval 第一条最接近(把键从"查询侧瞎猜"改成"写入时生成"), 与 TRACE 的结论方向一致(光有记忆不够,要有强制的执行侧动作)——但**没有做 TRACE 那种运行时检查**。 ### 12.8 还没验证的(写清楚,别当成已解决) 1. **模型会不会写对锚点**:机制要求 `memory_remember` 带 `recall_for`。库里 0 条带锚点的记录, 所以"模型能写出够用的锚点吗""写了之后投递率是多少"**完全没有数据**。 2. **防住没有**:仍然没有一次可对照的"无提示会失败、有提示不失败"。第十一节那个同名键坑 (32 次里只有 2 次做对)是现成的失败场景,等有了带锚点的记录就该拿它做 A/B。 3. **`--judge live` 不是旧判据的忠实复现**:旧判据的代码已经删掉,回放工具里的 `live` 现在也走新函数。57% / 10.6% 这两个数是**改之前**测的,留在这一节里;要复现得回到 commit `595d066`。 4. **线上生效要重启 DSH**:插件代码改动不是热重载的(见 README 的「构建」一节)。重启之前, 跑着的进程还是旧判据;重启之后新判据生效,但库里 0 条记录带锚点,所以表现为"动手前安静"。 **第一次验证建议这样测**(一次就够,结论客观):重启后让会话记一条测试经验, `recall_for` 填一个真实文件(例如 `path:tools/calls.jsonl`),然后**故意**在没读过它的前提下 去 `edit` 那个文件——编辑工具会拒绝并回一句"file has not been read",投递的提示会附在这次 调用的结果后面,同时 `delivery` 表里会出现一行。这与 `tests/delivery.test.ts` 的 A 场景同形, 只是走真实会话。 ## 十三、真模型 A/B:这一次没测出差别,但测出了两件有用的事(2026-09-23 凌晨) 第十二节说"下一步该做 A/B"。做了:隔离环境(一次性 `DSH_HOME` + 一次性 profile + 复制的真库 + 每次新建的工作区),真模型真跑,正确性**只看文件内容**,不看模型自述。 **场景**:一份 `service.yaml`,`timeout_ms` 在 `upstream:` 与 `defaults:` 两段各出现一次; 任务是把超时改成 45000。当年这个坑在无提示时 8 次里 0 次做对(第十一节)。 ### 13.1 第一轮(4×2):坑不成立了 第一版工作区里的 `README.md` **写明了哪一段生效**。结果两种条件都做对: | 条件 | 正确 | 收到投递 | |---|---|---| | 无经验 | 4/4 | 0/4 | | 有经验(带锚点) | 4/4 | 0/4 | **读法**:只要答案写在某个会话会读到的文件里,这个坑就消失了——模型自己会找到。 第七、十一节那些"做不对"的回合,工作区里没有这句话。**所以这是场景设计缺陷(泄露了答案), 不是结论。** ### 13.2 第二轮(5×2):把答案从文件里拿掉,差别出现了,但样本不足以定论 去掉 README 里那句"哪一段生效",其余不变(记录仍带 `path:service.yaml` 锚点): | 条件 | 正确 | 明细 | |---|---|---| | 无经验 | **1/5** | 4 次**根本没写文件**(只回话),1 次改对 | | 有经验 | **3/5** | 3 次改对,1 次两处都改,1 次改错段 | 方向与预期一致(20% → 60%),但 **Fisher 精确检验 p≈0.52,不显著**;把第一轮算进来更是抵消。 **所以这一轮不能声称"经验挡住了错"**,只能说:在没有答案可读的工作区里,这个坑重新变得难; 而带锚点那条记录确实被投出去过——隔离会话日志里能看到 `[经验记忆]` 那一行,出处 id 与种子记录一致。 ### 13.3 第三件:把"出处推断的锚点"收紧到零 第十二节说推断出来的锚点撞车太多(单记录 247 次)。加一条更严的要求试试: **记录自己的声明字段里必须真的提到这个文件名**(不只是"出处是它")。实测(15,896 次调用): | 判据 | 投递率 | 失败覆盖 | 单记录最大误触发 | |---|---|---|---| | 只认声明的锚点 | 0.00% | 0% | 0 | | 出处推断,不要求提到该文件(上一版) | 11.37% | 30.5% | 266 | | 出处推断 **且** 记录提到该文件 | **0.00%** | 0% | 0 | **一条都不剩。** 这不是调参失败,是一条关于现有库的事实:**这些记录的 `source_ref` 是 "这条经验写在哪个文件里",不是"这条经验讲的是哪个文件"**,而它们的声明字段里几乎不提文件名。 所以自动回填这条路在本库没有可用信号;只能靠以后写记录时声明 `recall_for`。 (已写进代码:`derivedNeedsMention`,默认开,等于把这个未定档的选项锁死。) ### 13.4 对着第四节那个门槛结账 | 判据 | 结果 | 达标? | |---|---|---| | 投递率 ≤2% | **0.00%**(线上库 0 条带锚点) | ✓(但含义是"什么都没发") | | 失败覆盖 ≥15% | **0%** | ✗ | | 单记录误触发 <300 | 0 | ✓ | | 每轮固定开销不变 | 未改注入层,204 字节照旧 | ✓ | | 同坑有经验时正确率显著更高 | 3/5 vs 1/5,p≈0.52 | ✗(不显著) | **结论:技术性门槛过了两个、失败覆盖差得远;唯一真正重要的那条——"防住了没有"——没拿到显著证据。** 这一轮把"投递判据换了、纯中文经验接上锚点"做完了,"用回放 + A/B 证明防得住"**没有完成**。 ### 13.5 下一轮该怎么走(留给下一个会话,别再重新想) 1. **失败覆盖为 0 是结构性的**:库里 0 条声明锚点的记录。要么等模型开始写锚点, 要么把这 191 条里的前 N 条**人工补一遍 `recall_for`**,再看覆盖率。 人工补是当前唯一能在没有模型配合的情况下让数字动起来的办法。 2. **A/B 要显著,得先降噪声**:这个坑的第二轮里"没写文件"占 4/5,那是模型没把任务当回事, 不是知识问题。下一版场景必须让**不写文件就不可能完成任务**(例如给一个必须改的字段, 并要求改完的文件能被脚本读出 45000),否则测的一直是"模型今天积不积极"。 3. **锚点该不该由模型写,是可测的**:让模型在真实会话里记 10 条经验,看它写的 `recall_for` 是否够具体(人工判读),比继续调判据更值钱。这正是 12.6 那个"模型动手前只查 9 次"的同一层问题。 ## 十四、把锚点回填进去:一条"路径 vs 文件名"的实测(2026-09-23 凌晨) 第十三节说"人工补锚点是当前唯一能让数字动起来的办法"。补了,也量了,而且量出一个真问题。 ### 14.1 做法 `tools/backfill-anchors.mjs`:对每条没有锚点的记录,从它的**声明字段**(标题 / trigger / failure_mode / lesson)里找文件名的形状,拿工作区真实存在的文件对账,命中就提议 `path:<那个文件>`。 三条自律写在文件头上:只读声明字段、文件必须真的存在、一条记录只加一个锚点。 默认只出提案(`--apply` 才写库,且可以先写到一个副本上量完再决定)。 ### 14.2 第一版:锚点名,撞车 508 次 用"文件名"当锚点(`path:tools.js`)时: | 判据 | 投递率 | 失败覆盖 | 单记录最大误触发 | |---|---|---|---| | 11 条回填(锚点名) | 6.34% | 16.52% | **508** | 518 次撞车集中在一条记录("junction 安装的插件不热重载")。原因很朴素: **`tools.js` 这一个文件名在本工作区里有三个不同的文件**(`repos/dsh-bigfat/lib/tools.js`、 `repos/dsh-quant/lib/tools.js`、插件自己的)——按名字锚,等于三条记录共用一个名字当触发词, 正是第十二节那个"磁铁"病的复发。 ### 14.3 第二版:锚点带路径,撞车降到 83 次 把锚点改成**相对路径**(`path:repos/dsh-quant/lib/tools.js`),匹配规则相应改成"调用的路径必须以这一整段结尾": | 判据 | 投递率 | 失败覆盖 | 单记录最大误触发 | |---|---|---|---| | 11 条回填(锚点名) | 6.34% | 16.52% | 508 | | 11 条回填(锚点带路径) | **1.18%** | 2.71% | **83** | | 门槛 | ≤2% | ≥15% | <300 | **投递率与误触发都达标了,失败覆盖掉到 2.71%。** 原因不是机制,是**回填到的记录太少**: 191 条记录里只有 11 条的声明字段提到了工作区真实存在的文件。多数记录讲的是"讨论 bigfat 时" "判断某功能做没做时"这类没有文件可锚的事——它们要么需要人工写 `command:`/`tool:` 锚点, 要么本来就只适合留在常驻摘要里。 ### 14.4 这一节给下一轮的结论 1. **路径锚点是正确的粒度**,名字不够:同一个文件名在一个工作区里平均有多个同名文件。 这条已经写进代码与测试(`deriveAnchorFromSourceRef` 现在返回整段路径)。 2. **回填能到 1.18% / 83 次**(都在门槛内),但**失败覆盖只有 2.71%**,离 15% 差得远。 要让覆盖率动起来,得有人给"没有文件可锚"的那些记录写 `command:`/`tool:` 锚点—— 这一步**没有自动化**,也不该假装自动化。 3. **本工作区的真库没有被写入**:回填只在副本上跑过。要上,先重启 DSH(插件代码改动不热重载), 然后 `node tools/backfill-anchors.mjs --cwd F:\dsh主工作区 --apply`。 ## 十五、为什么"失败覆盖 ≥15%"这条门槛达不到:把失败本身量了一遍(2026-09-23 凌晨) 第十四节留下 2.71% 的覆盖率。这一节回答"还能不能更高",做法是**不再从记录出发,从失败出发** (`tools/failure-anatomy.mjs`、`tools/coverage-honest.mjs`)。 ### 15.1 442 次失败长什么样 | 类别 | 次数 | 占比 | |---|---|---| | 全部失败 | 442 | 100% | | **工具自己拒绝、并当场告诉你下一步怎么做**(`file has not been read`、`old_string was not found`、`file changed since it was read` …) | **367** | **83.0%** | | 其余(不是"照着报错做就行"的) | 75 | 17.0% | | 同一会话里同一工具重复失败的 | 399 | 90.3% | | **之后 4 次调用内没再成功过的(=没救回来的)** | **56** | **12.7%** | 单一最大类:**`edit` 因为"文件没读过"被拒——217 次(49%)**,而它 100% 是"读了再改"就好的事。 按工具看:`edit` 占失败的 75.3%,`edit` 又占全部调用的 28.3%。 ### 15.2 这条门槛为什么和整套设计冲突 - **失败的大头不是知识问题,是流程问题**:83% 的失败,工具已经把答案写在报错里了。 要"覆盖"它们,最有效的办法是记一条"改文件前先读"的**工作流经验**,锚在 `tool:edit` 上—— 但 `edit` 占 28.3% 的调用,**这一条锚点单独就把 ≤2% 的触发预算打爆 14 倍**, 而且它本质上不是知识,是纪律,正是第十二节拆掉的那种"磁铁"。 - **能进预算的锚点覆盖不了失败**:在失败调用里出现、且在整个语料里出现 ≤2% 的文件名, 一共只覆盖 9 次失败量级(`delivery.md` 20 次那一条已经在 285 次调用里出现过,超预算)。 - **回填实测(第十四节那 9 条带路径锚点)在三个分母下的覆盖率**: | 分母 | 覆盖率 | |---|---| | 全部 442 次失败 | 2.71% | | 非自解释的 75 次 | 1.33% | | 没救回来的 56 次 | 1.79% | **结论:不是回填做得不够,是"用一条具体锚点在 2% 预算内覆盖 15% 的失败"这两件事在真实语料里 互相排斥。** 门槛当年是按"失败 = 知识没记住"的直觉写的,而实测显示这个语料里只有 17% 的失败 符合那个直觉。**这条门槛应当按 75(或 56)这个分母重写**,例如"非自解释失败覆盖 ≥15%", 否则它永远只能靠"锚在 edit 上"这种作弊方式达标。 ### 15.3 这三节之后,目标的账目 | 判据 | 结果 | 结论 | |---|---|---| | 投递率 ≤2% | 1.18%(带路径锚点回填) | 达标 | | 每轮固定开销不变 | 未改注入层 | 达标 | | 单记录误触发 <300 | 83 | 达标 | | 失败覆盖 ≥15% | 2.71% / 非自解释 1.33% / 未救回 1.79% | **达不到,且已证明与预算互斥**(15.2) | | 同坑有经验时正确率显著更高 | 1/5 → 3/5,p≈0.52 | **没有显著证据**(13.2) | **目标没有完成,两条关键判据都没过,而且第一条已证明是门槛本身写错了、不是实现不到位。** 下一步该做的不是继续加锚点,而是两件事:把门槛按新分母重写;去测"让模型动手前必须先查一次 记忆"——438 次失败里只有 1.8% 是先查过记忆的(12.6),那是比投递更大的一层。 ## 十六、"模型不问记忆"这个说法要改口(2026-09-23 05:30,隔离实验) 第十五节结尾写了"下一步去测让模型先查一次记忆"。测了。**结论是:我上一节那句话说过头了。** ### 16.1 怎么做 隔离环境(空库 + 一次性工作区),任务是一个**只有项目约定能回答**的问题: > 这个仓库要加一份部署配置。请写 `deploy.yaml`,**按我们内部支付服务的部署约定来写**, > 写完后一句话说明你依据什么写的。 "依据"是只存在于记忆里的两条事实:vault 路径 `vaults/team-prod`、namespace `payments`。 工作区里没有任何文件提到它们,通用写法也写不出来。成功判据只看文件内容。 ### 16.2 结果:9 次真实回合,模型 8 次自己查了记忆 | 条件 | 正确 | 模型主动查记忆 | 收到动手前投递 | |---|---|---|---| | 无记忆(空库) | 0/4 | **4/4** | 0/4 | | 有记忆(带锚点那条) | 2/4 | **4/4** | 0/4 | | 冒烟那一次(有记忆) | 正确 | 是 | 0 | 三件事: 1. **模型会问。** 9 次里 8 次调了 `memory_recall`,而且是**在第一次写文件之前**问的。 所以在语料里看到的"15,896 次调用只有 9 次 recall",不等于"模型不会问"—— 它更像是"真实任务里它不觉得需要问"。12.6 那条结论要按这个口径读。 2. **投递机制一次都没被用到。** 9 次里 0 次收到动手前提示——模型总是先 `read`,`read` 之后 自己就 recall 了,锚点(`path:deploy.yaml`)压根轮不到出手。 **也就是说:在"模型会主动问"的那类任务上,整套 just-in-time 投递是多余的。** 3. **有记忆也只对了 2/4。** 库里有那条经验、模型也确实查了,还是有两次没写对。 所以问题已经从"送不到"挪到了**"查到了、也读了,但没用上"**——这正是 MemoryAgentBench/TRACE 那两篇说的那一层(12.7)。 ### 16.3 我放弃的一件事,和为什么 我一度打算加一条"动手改文件前先查记忆"的一次性提醒(会话内只发一次,不动每轮固定开销)。 读到 `digest.ts` 就停了:**`RECORD_HINT` 每轮都在说同一句话**——"动手前先用 memory_recall 查"—— 而语料里 recall 仍然只有 9 次。再加一条重复的提醒不是干预,是噪声,而且没有证据支持。 所以那处改动**回退了**,没有留在代码里。 ### 16.4 这一节改变了什么 - **"投递"这条线该收尾了**:判据换了、锚点契约立了、回填工具做了、三条技术门槛过了两条, 剩下的覆盖率缺口已证明是门槛写错。继续投入的边际收益很低。 - **真正没解决的是"查到了不用"**。下一轮该测的是这个: 库里明明有那两条事实、模型也 recall 了,为什么 4 次里还有 2 次写错——是检索结果太长、 是没读全、还是它读到了却按自己的写法写?把那一轮的会话记录逐条读一遍就能分清, 这比再调投递判据值钱得多。 ### 16.5 那 2 次没写对的原因找到了,而且是我的错(同日 05:40 补) 把失败那一轮的会话记录逐条读了(`with-2`)。模型的原话: > Memory has a hit, but it cites `deploy.yaml:1` as its source — a file that shouldn't exist yet. > Let me verify against the actual repo before trusting it. **它拿到了那两条事实,然后因为"出处指向一个还不该存在的文件"而不信任它**,转去 `read` 仓库里 的文件找旁证;仓库里当然没有那条约定,于是它按自己的写法写了一份。 所以那 2 次不是"投递问题",也不是"读到了不用",而是: **一条记忆的 `source_ref` 指向不存在的路径时,模型会把它判为不可信并丢弃。** 这是本次实验意外测出来的一个**真问题**:出处的可信度决定记忆会不会被采纳, 而这次是我的种子脚本把 `source_ref` 写成了 `deploy.yaml:1`(一个任务才要创建的文件)—— **责任在种子,不在机制**。 **要记进下一轮的**:给"要创建的东西"写经验时,`source_ref` 必须指向**已经存在**的文件 (例如读 README 时立下的约定就引 README),否则这条经验不但送不出去,还会**主动降低** 模型对这个库的整体信任。现在的证据等级(`verified-file`)只校验"引文在文件里", **不校验"这个文件是不是这条经验讲的场景"**——这是一个已知缺口,本轮没修。 ## 十七、一个比投递更根本的问题:`verified-file` 撑不起它的名字(2026-09-23 05:40) 第十六节最后那条"已知缺口"值得单独测。做法:把同一条经验(内容一字不差)种两次, **只改 `source_ref`**——一次指向真实存在的 `README.md`,一次指向不存在的 `docs/deploy-conventions.md`, 各跑 4 次真实回合,任务和判据不变。 ### 17.1 结果:两臂都没用上那条经验,而且理由比"文件不存在"更狠 | 出处 | 用上经验 | 典型说法 | |---|---|---| | 指向不存在的文件 | 0/4 | "The glob shows no `docs/` directory — yet memory cites `docs/deploy-conventions.…`" | | 指向真实存在的文件 | **1/4** | 见下 | **第二臂是这次最重要的观测**:文件真的存在、引文真的在里面,模型还是把它丢了。原话: > The recalled memory's cited evidence doesn't match the file. > 这条记忆是在真实原文后面拼了一句来源里并不存在的话,我不能拿它当依据去写 > `vaults/team-prod` + `namespace payments`(写错了同样会被流水线拒收,而且会把假约定固化进仓库)。 **它说对了一半,而且那一半正是指标没说清的地方:**引文("本地起服务:`node dist/server.js`") **确实在 README.md 里**——`verified-file` 因此通过——但它**不支撑**"vault 路径 + namespace"这条主张。 模型要的是"这句话本身能读出这条规则",而框架给的担保只是"这句话确实在那份文件里"。 **所以 `verified-file` 这个名字承诺了它没做到的事。** 好的经验写法里,引文本身就是规则 ("部署配置必须先声明 vault 路径"),出处是写下这条规则的地方;而"引文只是同处出现、 主张是另外拼上去的"这种,机器判不出来,**但它判得出来,于是它不采纳。** ### 17.2 这不是孤例:全库体检(`tools/provenance-audit.mjs`) 191 条确认记录的出处形状: | 出处形状 | 条数 | 占比 | |---|---|---| | 文件在磁盘上不存在 | **22** | **11.5%** | | 文件是报告/日志(`.md`/`.jsonl`/`.log`) | **106** | **55.5%** | | 文件是代码/配置 | 53 | 27.7% | | 没有出处或出处是 call id | 9 | 4.7% | | 绝对路径(库外) | 1 | 0.5% | - **11.5% 指向不存在的文件**(含几条指着一个从来没建过的 `BPLA-conventions.md`)。 - **55.5% 指向报告**——出处是"当时查出来的东西"(审计报告、交接文件、复盘记录), 不是"能读出这条规则的原文"。这一档不等于记录是错的,但它和 A/B 里模型抱怨的形状是同一个。 - 只有 27.7% 指代码/配置,这是唯一可能"引文即规则"的一档。 **这张表给的是上界**:至少这么多条记录的"已验证",只保证了引文在那份文件里。 ### 17.3 这一节和投递的关系 回到目标:**"同坑有经验时正确率显著高于无经验"这条判据,卡在了证据层,不是投递层。** 八次真实回合里,模型查到了那条记录(第十六节:9 次里 8 次主动 recall), **然后因为不相信它而没用**。投递机制在这八次里一次都没轮到出手。 所以下一轮该动的不是投递判据,而是**记录的写法**: 把"引文即规则"变成写记录时的硬要求(让引文本身就是那句规则,而不是规则旁边随便一句), 并给出处一个能自动核对的判据。 ### 17.4 本轮做的机械检查,与为什么只报警不拦 `verified-file` 的记录,如果 `source_ref` 是工作区相对路径、**而那个文件在磁盘上不存在**, 可以在维护时标成 `needs_review`。这一步是纯机械的、可测的,方向明确。 **但本轮没有把它做成"拒绝落库"**,理由是它会把一件对的事做错:文件登记时不存在、 后来又出现(这次实验里就是),以及"这条经验讲的场景要到以后才创建"都是合法的。 **先量、先报警、别拒**——和这套框架其它地方一个规矩。 (具体实现留给下一轮,因为要动的是维护路径与测试,不是投递。) ## 十八、把"引文即规则"做成硬要求,然后发现场景测不出来(2026-09-23 05:45) ### 18.1 改了什么 第十七节的结论要落到**写记录的那一刻**,而框架给模型写记录的唯一渠道是 `memory_remember` 自己的工具说明(工作区里没有任何"怎么写记忆"的技能文件)。所以 `quote` 与工具说明都改了,改法是把 模型自己说过的那句话写回去,让它有一个可对照的标准: > 引文必须**本身就说出了那条规则**,不只是"出自同一个文件"。真实模型把记录读回来时会逐条核对这一点: > 一条讲部署 vault 的主张、配一句"怎么在本地起服务"的引文,它的回答是 "the cited evidence does > not match the claim",然后把整条记录丢掉。所以引文要引**说出这件事的那一句**(一条规则、一个 > 顺序、一个值、一段报错),不是"你当时正好在读的那一段"。没有这样的句子,就说明这条主张还不该记: > 写 `inferred` 的那一版,并写明缺什么。 工具说明变长了几行。**每轮固定开销没变**(那是常驻提示的 204 字节,不是工具说明),套件全绿。 ### 18.2 然后想验证它,结果第四次的场景又不成立 搭了一个"工作区里真有一份 `DEPLOY.md` 写明约定、记录就引那一行"的场景,5×2 真跑: | 条件 | 正确 | |---|---| | 无记忆 | **5/5** | | 有记忆(引文即规则) | **5/5** | 两边都满分,因为**模型自己读了 `DEPLOY.md`**——答案就在仓库里,记忆压根不需要。 这是同一类场景**第四次**失败,四次的原因可以归成一句话: > **仓库里写了,模型自己会读;仓库里没写,就没有一句可以诚实引用的原文。** > 所以"记忆内容必须有一句支撑它的引文"这个要求,**只在一种情况下能成立**: > 那条知识**存在于对话里而不在文件里**——也就是用户说过的话(`verified-user` 那一档)。 **这条推论是假设,不是本轮测出来的结论**:本轮没有用真实的多轮对话去测 `verified-user` 那一档。把它写在这里,是为了下一个会话不要再从头搭一遍这四个场景。 ### 18.3 这一节改变了什么 - **投递侧的活可以收了**:判据换了、锚点契约立了、粒度修对了(路径而非文件名)、回填工具做了、 三条技术门槛过了两条,最后一条已证明是门槛写错。再加投递规则没有边际收益。 - **证据侧改了一处,但没验证**:`quote` 的要求改了,`tools/provenance-audit.mjs` 能量出库里的 出处形状(11.5% 指向不存在的文件)。**改动的效果本轮没测出来**(18.2 的场景不成立)。 - **下一轮的第一件事**:用**真实多轮对话**测 `verified-user` 那一档—— 用户口头立下的约定(仓库里查不到)能不能被记下来、之后再被另一个会话用上。 那是这套框架**唯一还没被证伪的价值主张**,也是四个失败场景共同指向的那个格子。 ## 十九、测出来了:0/12 对 9/12,p=0.0003(2026-09-23 05:35) 第十八节说下一轮要测 `verified-user` 那一格。测了,**这是六轮里唯一一次真模型 A/B 拿到了显著结果。** ### 19.1 场景(这次没有泄露答案) 一条约定,**只存在于"用户说过的一句话"里,仓库任何文件都没有**: > 对了,我们的部署配置必须写在 deploy.yaml 里,而且必须在文件最前面先声明 vault 路径 > vaults/team-prod,再声明 namespace payments,**顺序不能反,反了流水线会拒收**。 任务:在这个仓库建 `deploy.yaml`,写完一句话说明依据。判据只看文件内容,而且**三个都要对**: vault 路径、namespace、**顺序**(顺序是故意加的对抗条件——先背下来的通用模板会写反)。 记录按 `verified-user` 等级入库(出处是"用户说过的那句话",`route: user-message`), 锚点 `path:deploy.yaml`。 ### 19.2 结果 | 条件 | 完全正确 | 95% Wilson | |---|---|---| | **无记忆** | **0 / 12** | 0.0% – 24.3% | | **有记忆** | **9 / 12** | 46.8% – 91.1% | **Fisher 精确检验(双侧)p = 0.0003。** 分组明细: - 无记忆那 12 次:**12 次都写了文件,12 次都不含那三样**(1112、695、1169、701、1245、1285、 926、215、817、1751、1138 字节——量都不小,是一份像模像样的部署配置)。 - 有记忆那 12 次:9 次完全正确;3 次**根本没写文件**(那 3 次是 `edit` 的"文件没读过"护栏挡下的, 和第五轮 442 次失败里 49% 的那一类同一个成因)。 - **只看写了文件的那些**:无记忆 12/12 全错,有记忆 9/9 全对。 ### 19.3 是哪个机制起的作用 把 5 次"正确"回合的会话记录逐条读了: | 机制 | 出现次数 | |---|---| | 模型**自己**调 `memory_recall`(且都在第一次写之前) | 5/5 | | 动手前锚点提示(`[经验记忆]`) | 5/5 | 两个都在。但**这是"先 recall、锚点跟着也出现"的形状,不是"锚点救了它"**—— 和第十六节的结论一致:模型会自己问,锚点是第二条路。**那次投递确实发出去了**(不再是 0), 这是六轮里第一次看到锚点在真模型回合里落地。 ### 19.4 这条结果该怎么读(诚实的那一半) - **它证伪的是"这套框架没用"**,不是"这套框架处处有用"。它成立的条件很窄,而且现在写清楚了: 知识**在对话里、不在文件里**,等级是 `verified-user`,并且写了锚点。 - **交付质量**:0/12 与 9/12 的差别不是"文件有没有写",两边都在写; 差别是**写出来的东西对不对**。这正是用户当初那句"经验防不住错"要的东西。 - **三条 `no-file`** 说明链条里还有一环会掉:模型想"改"一个还不存在的文件时会被护栏挡住。 这一环和记忆无关,是工作流摩擦。 - **`p = 0.0003` 是这一格的结果,不是整件事的结果**:覆盖率那条门槛仍未达标(第十五节的账目不变)。 ### 19.5 这一格对目标意味着什么 目标里那条**"同坑有经验时正确率显著高于无经验"**,**在第 19 节这一格上成立了** (0/12 → 9/12,p=0.0003)。三条技术门槛(投递率、误触发、固定开销)也已达标。 剩下未达标的是**失败覆盖率**,而第十五节已证明那条门槛与 2% 预算互斥、需要按新分母重写。 **所以目标的形态现在是这样:投递侧修完了、防错效果在一个说得清的条件上被证明了一次、 覆盖率那条门槛被证明写错了。** 这不是"全部完成",但也不是"没做出来"。 ## 二十、覆盖率这条门槛的最终账:它在这个语料上量不出来(2026-09-23 05:50) 第十九节之后只剩这一条没结。结的方式不是再修一次,而是把"这条门槛能不能量"本身查清楚。 用一个宽口径的机械代理去问:**那些可归因失败,库里到底有没有一条相关记录?** (做法:失败调用的参数与记录文本共享 ≥2 个实词就算"相关"——一个线索,不是判据。 `tools/coverage-reach.mjs`) ### 20.1 结果:65.3% 看着有救,但逐条一看就散了 75 次可归因失败里,**49 次(65.3%)** 表上有一条"相关"记录,其中 42 次的记录已经有锚点。 **这个数字不能用。** 看明细就知道为什么: | 失败 | 被算成"相关"的记录 | |---|---| | `pwsh`:sandbox 升级参数报错 | "会话间无法直接对话;要投递需走 session/prompt over /api/remote.mux" | | `pwsh`:tool call aborted | "数据源独立性纪律:分组必须登记,同组不得互相印证" | | `memory_remember`:域未解析 | "bigfat 是价值投资取向,量化是量化——两个项目不合并" | **共现词不等于相关。** 这恰恰是第十二节拆掉旧判据的同一个病,只是换了个位置复发: 我为了量"覆盖率",又写了一个按词共现打分的代理。**这说明这条门槛用词法代理量不出来。** ### 20.2 三条原因叠在一起,所以这条门槛不该再按原样保留 1. **分母不成立**(第十五节):15% 是按"失败 = 知识没记住"写的,而这个语料里 **83% 的失败是 工具自带护栏**,49% 是"文件没读过"。 2. **分子量不出来**(本节):判定"这次失败本该被哪条经验拦住"需要一个判断, 而唯一便宜的代理(词共现)自己就会把不相关的记录算成相关——**门槛的度量方式在自欺**。 3. **剩下的也不是知识问题**:那 75 次里逐条看过去,多数是环境坑(`rg` 撞上 `System Volume Information`、`svn.apache.org` 解析成非公网 IP、sandbox 参数不合法) 或语义手滑("两处都改""改错分支"),**不是"有一条规则没记住"**。 ### 20.3 把这条门槛改成什么(建议,留给下一轮拍板) **改成"在能诚实度量的事件上做前后对照",具体是**: > 一条经验写下之后,**同类事件**(同一工具 + 同一形状)在后续会话里发生的次数与 > 写下之前的速率相比有没有下降;样本不足就只报计数,不下结论。 这套账框架里**已经有了**:`failure_shape`(按形状计数、保留最近若干次时间点)+ `delivery`(投递痕迹)+ `tools/prevention-ledger.mjs`(四分类)。 **不需要新指标,需要的是把"覆盖率 ≥15%"这个量撤掉**,换成它本来想表达的那句话: "这条经验写过之后,这类错还犯不犯。" ## 二十一、跨会话那一格也补上了:合并 0/18 对 14/18,p=0.000002(2026-09-23 06:00) 第十九节那个结果有个缺口:记录是**预先种好**的,没测"这个会话记下、下个会话用上"这一段。 补了。 ### 21.1 做法 每个回合:**空库** → 第一个会话(只有一句用户的话,仓库里什么都没发生)→ **同一个仓库**上开**第二个会话**,任务不变,不提任何约定。两种条件各 6 次。 **一个必须说明的设计坑**:第一版我把两个会话放在**不同的目录**里,结果第二个会话用不上—— 因为工作区标识是按路径算的,工作区级记录本来就不跨目录可见。那不是框架的毛病,是我搭错了。 改成"同一个仓库、开新会话"才是真实形状。 ### 21.2 结果 | 条件 | 完全正确 | 95% Wilson | |---|---|---| | 无记忆(只跑第二个会话) | **0 / 6** | 0.0% – 39.0% | | 有记忆(先听、记下、新会话用) | **5 / 6** | 43.6% – 97.0% | **Fisher 精确检验 p = 0.0152。** 第一个会话的记录是**它自己写的**、等级 `verified-user`(出处=用户原话)、锚点 `path:deploy.yaml`——不是种进去的。所以这一格测的是**整条链**: 听到 → 记下 → 另一次会话里用上。 ### 21.3 两组合并 | 条件 | 完全正确 | 95% Wilson | |---|---|---| | 无记忆 | **0 / 18** | 0.0% – 17.6% | | 有记忆 | **14 / 18** | 54.8% – 91.0% | **Fisher 精确检验 p = 0.000002。** **只看真的写了文件的那几次:无记忆 18/18 全错,有记忆 14/14 全对。** (4 次没写文件的是被编辑护栏挡下的,与记忆无关。) ### 21.4 这一格对目标意味着什么 目标里那条"**同坑有经验时正确率显著高于无经验**",现在**单轮和跨会话两条路都成立**, 合并 p 到小数点后第六位。这是七轮里唯一一次正面的、可复现的、统计显著的结论。 **它同时把适用范围说死了**(第十九节):只有"**用户说过、文件里查不到**"那类知识有这个效果。 仓库里写了的,模型自己会读;仓库里没写的、用户也没说过的,库里没有,也不该有。 ## 二十二、第二个场景也复现了:0/6 → 6/6,p=0.0022(2026-09-23 下午) 第十九、二十一节的结论建立在**一个场景、一类知识**(配置内容:vault 路径、namespace、顺序)。 用户要求换一类知识再复现一次。做了。 ### 22.1 场景:约定是**放文件的位置**,不是写什么内容 只在用户说的一句话里: > 记住一条我们组的约定:服务的示例配置(sample config)一律放在 `conf/samples/` 目录下面, > 不要放在仓库根目录,也不要放在 `src/` 里;仓库根目录只放真正生效的配置文件。 仓库里**没有任何文件**提到 `conf/`(连目录都没有)。任务:"给这个服务加一个 redis 的示例配置"。 判据只看文件系统:写进 `conf/samples/` 算对,别处算错。 这与场景一是**两类知识**:那个考"内容对不对",这个考"东西放哪"。 ### 22.2 结果 | 条件 | 放对位置 | 95% Wilson | |---|---|---| | 无记忆 | **0 / 6** | 0.0% – 39.0% | | 有记忆(跨会话) | **6 / 6** | 61.0% – 100.0% | **Fisher 精确检验 p = 0.0022**(含冒烟那次 0/6 → 7/7,p=0.0006)。 **两场景合并**(每臂按 6 回合的整数计): | 条件 | 正确 | 95% Wilson | |---|---|---| | 无记忆 | **0 / 24** | 0.0% – 13.8% | | 有记忆 | **20 / 24** | 64.1% – 93.3% | **Fisher 精确检验 p = 0.0000002 量级**(脚本报 `0.000000`,实际是小数点后第六位以下)。 明细(这轮的错法比场景一更干净): - **无记忆 6 次:每一次都写了文件,每一次都写进 `config/`**——模型对"示例配置放哪"的默认猜测。 - **有记忆 7 次:每一次都写进 `conf/samples/`**——用户那句话指定的,也是没有任何文件提示的目录名。 - 所以差别**不是"写不写"(两边都写),是"写到哪"**——与场景一的"不是写不写,是写得对不对"同一结构。 ### 22.3 顺带:我的判据一度报错,记下来 第一版判据枚举了 `conf/`、`conf/samples/`、根目录、`src/` 四处,结果把无记忆那 6 次全报成 **"没落笔"(no-file)**——可模型明明写了,只是写进了 `config/`,**一个没被枚举到的目录**。 读会话记录才看见那一行 `write: config/redis.example.json`。 判据改成**遍历整个工作区、按路径归类**(`tools/verified-user-ab/rescore.py`), 未预期的目录不会再从缝里漏掉。**教训是通用的:判据枚举了"预期会出现的地方", 就会把"出现在了别处"误报成"什么都没做";前者是错误,后者是不作为,两者不能混。** ### 22.4 这一节改变了什么 第十九节说"适用范围说死了",当时唯一的依据是一个场景。**现在是两类知识、两个场景、 三次独立跑(单轮、跨会话、跨场景),全部显著。** 所以"这套框架在用户亲口立下的约定上 能防住错"这件事,**不再是单场景的偶然**。 同时那条边界依然成立,而且更值得写下来:它有效的前提是**知识在对话里、不在文件里**。 文件里写了的模型自己会读;谁都没说过的库里没有,也不该有——**记忆的价值恰恰在 "没有第二处可查"的那些事上**,这是它和查文档的根本区别。 ## 二十三、测试台自身的失效模式:四次静默失效与一条坏掉的复核命令(2026-09-24 凌晨) 这一节记的不是记忆框架的缺陷,而是**测它的那套东西**怎么骗人。每一条都在跑的过程中被实测抓到。 "结论不能比证据强"同样适用于测量工具本身。 ### 23.1 声明了上限,上限却没生效(两次) - `tools/t2-run.ps1` 声明单格 480 秒硬超时。**直接调用时生效**(实测 482.1 秒被掐断), **在扫里不生效**:一格从 00:21:32 跑到 00:47 仍在写日志(>25 分钟)。 - 于是加了一层 `Start-Job` + `Wait-Job -Timeout 720`。**两层都没生效**:一格从 00:41:03 跑到 00:54 仍在写日志(>13 分钟),两层的 `-Timeout` 参数都没有返回。 - 改成**轮询**(`while (-not $proc.HasExited -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 3 }`) 后按预期掐断;预检门第 ⑥ 条用"上限 5 秒 + 探针要跑 20 秒"自证,实测 **6.5 秒被掐断**。 - 教训:**上限要自证**。"声明 480 秒"与"480 秒真的会停"是两件事,而后者只能靠跑一个本该被掐断的 探针来证明。另外,`Wait-Process -Timeout` / `Wait-Job -Timeout` 在本机这条嵌套调用路径上不可信。 ### 23.2 播种失败 = 静默退化成"没有记忆" `t2-seed.mjs` 按**标题精确匹配**从真库播种。ctrl2 那条的标题里,场景表用的是中文引号 `“”`、库里是 英文引号 `""` ⇒ 一条都没播进去 ⇒ 该臂实际测的是"没有记忆",而产物上与 `none` 臂**一模一样**。 bom/ctrl2 的三行数据因此作废(已移入 `tools/t2-invalid-launchbug-20260923.jsonl` 并重跑)。 现在 `t2-run.ps1` 会检查播种退出码,失败就记 `seed-failed` 并中止这一格,不再把它当成一次测量。 ### 23.3 标题不能走命令行(PowerShell 会重写引号) 标题里带英文双引号时,PowerShell 把它交给原生程序时会把引号吃掉,`t2-seed.mjs` 收到一个对不上的 字符串(exit 2)。改成 `--title-file`(写文件、传路径)后可靠。同一次修正里把 `t2-seed.mjs` 的 lib 解析从 `process.cwd()` 改成从**本文件**解析——按当前目录找自己的库,换个目录调用就 `ERR_MODULE_NOT_FOUND`,这与 `wipe.mjs` 当年的事故是同一个病。 ### 23.4 场景本身在本机不可能通过(stdin) stdin 场景要求把三行文本真的喂进 `git credential fill` 的标准输入,判据是回显 `host=example.com`。 但本机沙箱禁止 git 的凭据助手起 bash 管道——**人手跑也必失败** (`error: cannot create standard input pipe for bash: Permission denied`)⇒ 四个臂会一起红, 测的是环境而不是记忆。给隔离 home 配一个静态凭据存储(`GIT_CONFIG_GLOBAL` 指向隔离配置)后实测可 正常回显;**判据一个字没改**。教训:一条场景的判据若依赖某个外部命令能跑通,**开跑前先手工验一次 那条命令**,别让它变成"测环境"。 ### 23.5 复核命令自己坏了(`tools/replay.mjs` 语法错误) `tools/replay.mjs` 第 51 行两条 `const` 挤在一行(缺换行,commit `32568de` 引入),整个文件是 `SyntaxError`——而交付说明里写着"想复核就跑 `node tools/replay.mjs --judge both`"。 **修好之后它给出的数字与文档不符**: | 量 | 文档(06:20 交付说明) | 修好后的实测(2026-09-24 00:5x) | 预注册门槛 | |---|---|---|---| | 投递率 | 1.18% ✓ | **5.58%** ✗ | ≤2% | | 失败覆盖 | 量不出来(门槛已撤) | 2.94% | (已撤) | | 单记录最大误触发 | 83 ✓ | **699** ✗ | <300 | | 场景召回 | — | 0.00%(送错 2) | — | 两个工具(`replay.mjs` 与 `backfill-anchors.mjs` 里的回放)**修好后数字一致**,所以这不是口径分歧, 而是:库的真实状态在这段时间里变差了,而复核路径一直是坏的,**没人能看见**。 `derived` 档位(把"由出处推断的锚点"也算上)投递率 7.39%、失败覆盖 11.76%,说明这条路也没让情况变好。 **未解决,留给下一轮**:先查清 699 次误触发集中在哪条记录、它的锚点当初是怎么写进去的。 ### 23.6 存量锚点回填:机械可推的那部分已经是 0 条 `tools/backfill-anchors.mjs`(已存在,非本次新写)在真库 + 本工作区上跑一遍:候选 **0 条**。 原因不是"没有静默记录",而是"能机械推的那些,`recordAnchors(..., { derived: true })` 已经算得出来"—— 剩下的静默记录缺的是**声明**,不是可推性。这与 T1 的结论一致:把力气花在**写新记录时提议锚点** (本轮已落地:`memory_remember` 的 `recall_for_suggestions`)比事后回填更有价值。 ## 二十四、T2 的实测结论:一条支持、一条环境地板、两条天花板(2026-09-24 凌晨) 完整矩阵与逐场景判定写在 `tools/t2-plan.md` §一「实测」——**只有一份,不在这里复制数字**(同一件事 两处口径必然分叉,这个坑本工作区记过)。这里只记结论与它带来的方向。 - **有判别力的那一条支持主张**:bom 场景(写文件别带 BOM)rel 3/3 对 none 0/3,无关对照 3/9。 即:在"用户说过、文件里查不到"的编码类知识上,**经验确实防住了错,而且不是"随便给点记忆就行"** ——三条无关经验的对照臂只有 33%。 - **另三条没有信息,不能读成"记忆无效"**: 1. **stdin 是本机沙箱的地板**:判据要求 `git credential fill` 回显被喂进去的内容,而 git 的凭据助手 在智能体的沙箱里起不来(`cannot create standard input pipe ... Permission denied`,产物里留着原文)。 任何臂都不可能通过——这条场景测的是沙箱。 2. **wipeguard 与 selftest 是天花板**:不用经验的臂也 100% 通过(前者的判据只是"脚本里有没有打印 路径 + 会不会拒绝",后者只是"有没有提到自引用")。**"提到关键词"式判据区分不了"做对"。** - **预注册的"四个场景方向一致"没有达成**,原因是场景设计而非框架结论。下一步该改的是**选题与判据**: 任务要挑"模型本来会做错"的(bom 就是),判据要能从产物判定对错(bom 的"前 3 个字节"就是)。 - **机制上的后半句**:§23.1 记了"上限要自证",这一轮补上——**"杀掉了"也要自证**。修好之前,被判超时的 格子其实还在跑(产物在上限之后 1–34 分钟才出现),最糟时 3–4 个格子并发、共用同一个隔离库; 现在结果行带 `kill_left`,20 秒上限的探针可复核(实测 25.6 秒被停、跑完零残留)。 ## 二十五、699 次误触发的真凶:一条记录 + 一个"到处都是"的路径锚点(2026-09-24 上午) §23.5 留下的问题是"先查清 699 次误触发集中在哪条记录、它的锚点是怎么写进去的"。查清了。 **哪条记录**:`4e6b5b34d9261414b618`「目录联接成环会让"递归翻目录"永远跑不完:排障时别把慢当成死」 ——**2026-09-24 00:5x 由排障会话自己写进去的**(教训来自当晚的排查),`evidence: verified-file`、 `source_ref: AGENTS.md:153`。 **哪个锚点**(把 15,896 次调用逐条喂给 `anchorSatisfied`): | 锚点 | 命中的调用 | 占语料 | |---|---|---| | `path:node_modules` | **702** | 4.42% | | `command:Get-ChildItem -Recurse` | 29 | 0.18% | **算术**:这条记录被投递 699 次,占全部 887 次投递的 **78.8%**;把它剔掉,投递率是 188 / 15,896 = **1.18%** —— 正是 06:20 交付说明里那个数。也就是说:**"5.58% 不达标"这条, 几乎完全由这一条记录造成**,而且它是连夜新写进去的,不是历史遗留。 **为什么它能写进去**:`path:` 锚点里那个 token 是**没有目录的裸名字**。框架自己的测量早就记过 同一类事故(按名字锚 `tools.js` 一次撞了 508 次),代码里也有两道防线(带目录时要求整段后缀; `derivedAnchors` 只在"记录真的提到该文件"时才用)—— 但**人手/模型显式声明的锚点绕过了这两道防线**, 写入时没有任何东西提醒"这个名字到处都是"。而 `node_modules` 在这个工作区几乎无处不在:任何 grep、 列目录、装包、路径参数都会命中。 **两条修法**(都不改判据): 1. **数据**:把这条记录的锚点收窄或去掉。去掉锚点的记录**仍然**进每轮摘要、仍然能被 `memory_recall` 搜到,只是不再在动手前弹出——这正是框架对"没有声明的记录"的既定行为。 2. **机制(下一轮)**:写入时对锚点做**稀有度检查**。最小实现:`path:` 锚点要求带目录(拒绝裸名字), 或者对声明的 token 跑一次语料统计、占比过高就警告。加在 `memory_remember` 里比事后靠 replay 的 门槛抓要早一步。 **顺带一个结论**:复核门槛(单记录 <300)**是有效的**——它确实抓到了这条记录;欠缺的只是"只在事后跑"。 ### 25.1 两条数据修法与修后实测(2026-09-24 05:4x) 两条记录、同一个病(声明的锚点太宽),都按"留快照 → 干跑 → 写库 → 回读"的流程处理: | 记录 | 原锚点 | 命中 | 处理 | 处理后命中 | |---|---|---|---|---| | `4e6b5b34…`(排障会话 00:5x 写) | `path:node_modules` | 702(4.42%) | 换成 `path:dsh-experience-memory/node_modules` | **2(0.01%)** | | `67ce05de…`(另一会话 03:23 写) | `tool:pwsh` | 5,936(37.34%) | **删掉锚点**(记录仍进每轮摘要与搜索) | **0** | 工具:`tools/replace-anchor.mjs`——先打印目标库路径、`--from`/`--to` 必须能解析成合法锚点、记录不存在或找不到该锚点时 **什么都不做**、默认干跑、`--drop` 删锚点、每次写一条审计。写前留快照走 `tools/snapshot.mjs`。 **修后实测**(`node tools/replay.mjs --judge both`): | 量 | 修前 | 修后 | 门槛 | |---|---|---|---| | 投递率 | 38.88% | **2.20%** | ≤2% ✗(差 0.2 个百分点) | | 单记录最大误触发 | 5,859 | **92** | <300 ✓ | | 命中过的记录 | 10 条 | 9 条 | — | **剩下那 0.2 个百分点是什么**:9 条记录各带一两个锚点的自然累积,最大一条 92 次——就是"投递层现在不达标" 那条记录自己,锚在 `DELIVERY-GAPS.md` 上(它本来就该跟着这份文档走)。所以结论要分开说: - **"一条记录吃掉全部提示"这个病治好了**(单记录门槛从 5,859 降到 92)。 - **"记录越写越多、投递率慢慢爬过预算"是设计张力**,不该靠手改数据去凑绿:要么重新商定这个预算, 要么限制每轮投递条数/每条记录的发火上限。留给下一轮。 **真正的机制修法**:写入时对锚点做**稀有度检查**——把"这个 token 到处都是"在写入那一刻说出来。 同一类事故一晚上出现两次(`path:node_modules` 4.42%、`tool:pwsh` 37.34%),说明它不是个别失误。 现成的统计做法在 `tools/anchor-cost.mjs`(把一个候选锚点丢进 15,896 次调用里数命中)。 ## 二十六、T2 第二轮:一条证明"经验拦住了",一条证明"经验没拦住"(2026-09-24 下午) 第二轮换掉场景、换掉目录名、判据先自检(§24 之后的全部修订见 `tools/t2-plan.md` §4),跑 **30 格** (3 场景 × 5 臂 × 3 次,`tplcomment` 被预检门判天花板跳过),产物判定。结果两个方向都有: | 场景 | 不给经验 | 给对口经验 | 无关对照 | 判读 | |---|---|---|---|---| | `bom`(写文件别带 BOM) | **0/3** | **3/3** | 0/9 | ✅ **经验拦住了**:方向对、三条对照全阴性 | | `wipeguard`(清库脚本先核对路径) | 0/3 | 0/3 | 0/9 | ❌ **负结果**:`task_done=true`——任务全做出来了,只有"加护栏"没人做 | | `tplcomment`(模板字符串注释用反引号) | (未跑) | (未跑) | (未跑) | 天花板:探针 2/2 vs 2/2,模型本来就避开 | **bom 是"经验有用"最干净的一组数字**:不用经验 0/3、对口经验 3/3、无关经验 0/9——不是"随便给点 记忆就行",必须是对口那条。`effect = +1.00` 已写进那条记录(先快照、带删除测试审计行)。 **wipeguard 是本项目第一个测出来的负结果,也是最该被读对的一个**:记录就在库里(通过每轮摘要送达)、 内容含判据需要的完整规则(`.exp`/`tmp`/`isolated`),15 格里每个臂都把该清的库清对了、把仿真真库也 清掉了——**记录没有改变任何行为**,`effect = 0.00`(这是测出来的 0,不是分辨不出)。 两处诚实边界,都写进读数里: 1. **投递方式**:wipeguard 的记录是靠**每轮摘要**送达的,不是锚点触发的。摘要被截断/模型长推理里 忽略了它,都会把"投递没到位"混进"知识没用"里。下一轮该做的是给这类记录配锚点再测一次。 2. **天花板是好消息的另一面**:jsonquote/linecount/tplcomment 三条全被模型自己避开——**当前模型 在常见编码陷阱上已经不犯错**,经验的边际价值在这些格子上等于零。经验的价值只存在于"模型会犯 而文件里查不到"的格子里(bom 正是)。 **"地板"这个词第一轮就用错了**:两边都是 0 有两种完全不同的意思——任务没做出来(实验没跑成)与 任务做出来了但判据要求的行为没出现(**测出来的 0**)。判据新增 `task_done` 字段区分两者,报告据 此给出负结果而不是"无信号";`src/effect.ts` 的 `taskDone` 同判。没有这个字段,负结果会被当地板 丢掉——而它恰好是"经验能不能拦住错误"最直接的反面证据。 实测工具(都进了仓库,可复跑):`tools/t2-preflight.ps1`(含判别力探针,2/2 才判)、 `tools/t2-judge-smoke.ps1`(判据自检:喂已知对/已知错的产物)、`tools/t2-report.mjs`(报表, 支持把跳过的场景标成"跳过")、`tools/effect-write.mjs`(效果值落库,dry run 默认)、 `tools/t2-progress.ps1`(进度条与预计时间)。 ## 二十七、更正 §26:那条"经验没拦住"的记录,其实是"经验改了解题设计,但改出来的护栏不拦真库"(2026-09-24 晚) §26 把 wipeguard 的 15 格写成"**记录没有改变任何行为**"。补测之后这句话要更正——**行为变了**, 只是变出来的东西拦不住真库。 补测做法(`tools/t2-plan.md` §4.11 先写后跑):给那条记录补两个**窄锚点**(`path:wipe.mjs`、 `command:wipe.mjs`,在 15,896 次真实调用里各命中 0 次,写入前量过、写入时过护栏),让它在**动手前** 把完整教训弹出来,再同场跑 none×3 与 rel×3。 | | 不给记录 | 给记录 | 无关记录 | |---|---|---|---| | 判据通过 | 0/3 | **0/3** | 0/9 | | 任务做出来了 | 3/3 | 3/3 | 9/9 | | 脚本里写了"路径标记检查" | **0/3** | **3/3** | 0/9 | | 提示真的弹了 | — | ✅ delivery 表有记录(`matched="path:wipe.mjs"`) | — | 三条一起读:**投递不是瓶颈**(提示弹了)、**记录确实改变了模型的设计**(只有给了记录的格子加了 "路径标记检查")、**但改出来的护栏不成立**: - 一格把标记表写死成 `['temp','tmp','.exp','isolated','dsh-']`——仿真真库路径里有 `dsh-`,被放行; 而**用户真库路径 `AppData\Roaming\dsh-desktop\harness\...` 同样含 `dsh-`**,所以它连真库也拦不住。 - 两格把检查做成**可选**(要调用方传 `MEMDB_MARK`/`WIPE_EXPECT` 才生效)⇒ 默认一个字节都不拦。 **根因不在投递,在教训的写法**:它只给了"举例的标记"(`.exp`/`tmp`/`isolated`),没给**默认拒绝 + 明确白名单**这种可机械执行的判据。模型照抄了形式(打印路径 + 核对标记),抄出来的是"看起来有护栏、 实际不拦真库"的脚本。这条记录要改写成默认拒绝 + 显式允许清单(并把真库路径写成反例),改完用同一 判据复测——这也顺带说明"经验有效"不是二元问题:**同一条经验,写法不同,拦得住与拦不住是两种结果**。 本轮同时修掉三个让结论不可信的测试台缺陷:WAL 模式下 `Copy-Item` 只拷 `.db` 导致隔离库被判损坏 (6 格全挂,且被误标成"护栏拒绝")⇒ 改一致性拷贝并新增 `tools/copy-store.mjs`;抽出去执行的判据里 `$PSScriptRoot` 为空 ⇒ 15 格重判全红;任务句柄丢过两次 ⇒ 新增 `tools/t2-rejudge.ps1`(按产物重判, 不重跑智能体)与 `tools/delivery-report.mjs`(查提示到底弹没弹)。 ## 二十八、收尾:把那条安全经验改写成"可执行形式"之后,它成立了(2026-09-25 凌晨) §26 把 wipeguard 写成"记录没有改变行为",§27 更正为"记录改了解题设计,但改出来的护栏拦不住真库", 并给出根因:**教训只给了举例的标记**,模型照抄形式、抄出假护栏。本轮的收尾就是按那句根因改,并把 "算不算作弊"这件事用规矩挡住: 1. **先冻结判据与夹具,再改经验**(读数标准先写进 `tools/t2-plan.md` §4.13); 2. **危险路径是一组、不是一条**:真库形状、同形状换根、普通用户数据 `ledger.sqlite`、无后缀库文件, 四条都在系统临时目录之外,且**经验正文里一条都不出现**——能对上一条夹具不算过关; 3. **合法用例必须仍然成功**(指定库照清),所以"一律拒绝"过不了;再加 9 格无关经验对照。 **结果(模型 `deepseek-official/deepseek-flash`,每行带 `model`)**: | 场景 | 不给记录 | 给(改写后的)记录 | 三条无关对照 | |---|---|---|---| | wipeguard | 0/3 | **3/3** | **0/9** | | bom | 1/3 | 3/3 | 6/9 | wipeguard 三条通过格子的护栏原文都是"默认拒绝 + 只放行系统临时目录 + 点名真实数据目录为禁地", 四条危险路径全拒、合法库照清。**同一套判据用来判上一轮的关键词白名单写法,三条全错**(§27)—— 两轮之间唯一变的是记录正文的写法,这就把"是记录在起作用"和"是我把夹具喂给了它"分开了。 **bom 的如实标注**:方向仍对,但无关对照在 deepseek 上是 **6/9**(mimo 那轮是 0/9)——**这条教训的 边际价值依模型而异**,deepseek 有时自己就能躲开 BOM 坑。对照比率低于 rel,按冻结读法不作废,但结论 里必须带着这句话。 **"机器事实"改由框架算,不让人写**(回答"白名单是你写的,算不算作弊、以后要不要我写"):新记录只写 规则,**具体路径由插件在写入时自动算出来提议**(`src/guard-hints.ts` → `memory_remember` 的新返回字段 `danger_examples`:真库路径 + 可丢弃范围 + 一句"路径含某个关键词不等于安全")。它**只提议、绝不写入**, 正文一字不改(GROWTH 第五节禁止偷偷改记录)。所以以后既不需要用户写白名单,也不需要人去维护具体路径。 **这一段跑批同时修掉四个让数字不可信的测试台缺陷**:配额用尽被读成"所有臂都失败"(改占位行 + 续跑); 报告工具把结果文件路径当场景名、安静地读旧文件(参数只认场景名、表头写明读了哪个文件);带中文的 `.ps1` 没有 BOM 导致 Windows PowerShell 5.1 按代码页解码、整脚本时好时坏地解析失败(9 格"无结果", 已全部加 BOM 并把"脚本可被 5.1 解析"做成预检门第 ⑤b 条);**每个格子起 ~28 个短命进程把用户的屏幕刷满 错误弹窗**(合并成单进程判定 `tools/judge-wipe-guard.mjs`,降到 ~6 次,且不再用 cmd 包装)。 **还没做的**:新记录的 `effect` 值尚未写入(要跑一次 `effect-write.mjs --apply`,会起进程;用户刚被弹窗 打扰,故留待下次);结果文件里 9 行 `note` 是编码切换期的乱码(判定字段不受影响,读法已两侧钉死); `bom` 对照偏高的原因未追(要单独实验)。