# 工程教训 > 这个仓库踩过的坑,以及它们共同的形态。**写在这里是因为它们比实现细节更耐用** —— > 代码会改,缺陷形态会重复出现。 > > 来源:这些条目原本散落在各份执行计划(`memory-*-plan.md`、 > `toolcall-parallel-execution-plan.md` 等)里。计划本身已随实施完结而删除, > 但教训被提出来了 —— 那是它们唯一还有价值的部分。 > > 建立于 2026-10-31。 --- ## 一、判据类(最贵的教训,占了一半) ### 1.1 「结构判据」不能替代「质量判据」 **症状**:16 条判据、6 个变异**全部通过**,而功能完全不可用。 **当时的场景**:记忆检索改造。判据验的是「字段非空」「条数正确」「不 panic」, 而实际检索结果是: ```text 查询 "doc 层召回是怎么设计的" → score=0.7834 查询 "hmap 插件安装验签" → score=0.7922 查询 "QQ 头像" → score=0.7601 ``` **分数全挤在 0.75~0.79,区分度 ≈ 0,命中内容与查询毫无关系。** ⇒ 「全绿」证明的只是**我实现了我以为的东西**,不是它可用。 **判据要问**:这条判据在"功能完全失效"时会绿吗?会绿就不是质量判据。 ### 1.2 判据测「接线之后」≠ 测「有没有接线」 这是本仓出现频率最高的缺陷形态(已攒够 5 例,见 `future-plan.md` §〇)。 两处实测: | 判据 | 它做了什么 | 为什么永远绿 | | --- | --- | --- | | `security_toolgate_test.go` | 手动调 `a.SetSecurity(rt)` 再断言拦截 | **判据自己把缺失的接线补上了** | | `retention_test.go` | 手写 `month_2026-01.tar.gz` 再断言被删 | 从没跑过 `compressMonthly` 的**真实产物**(那产出的是 `month_2026-27`,解析必失败) | 两者都全绿,两个功能在生产里都完全没生效。 **判据要问**:**这条判据在生产装配下也会绿吗?** ### 1.3 「验证通过」≠「架构正确」 **当时的场景**:`630d3f4` 的 16 三元组端到端验证全部通过。 但那次验证的是**拆分质量**(能不能把句子拆成主谓宾), 不是**正确的落库形态**(拆出来的三元组该以什么结构存)。 ⇒ 验证通过之后才发现,整个蒸馏路线方向错了,要整体否决。 **判据要问**:我验证的,是不是我真正想保证的那件事? ### 1.4 fixture 照臆测字段编 本仓 T23 教训:判据用的 fixture 是 **手写臆测的字段名**,而真实结构体字段不同 ⇒ 判据测的是一个不存在的契约。 ⇒ 判据的输入应当来自**真实产物**,不是人脑里的想象。 ### 1.5 先改代码再写测试 原本记在记忆块流修复计划里(该计划已随实施完结删除):先改代码再补测试, 测试就只会证明「我改的是我以为的」。 ⇒ 这个顺序本身会让判据失去独立价值。 --- ## 二、接线类 ### 2.1 机制 ≠ 接线 见 `future-plan.md` §〇。一句话总结: **策略表存在、判定函数正确、日志里一条拒绝记录都没有** —— 看起来像「没人违规」。 判断方法:`codegraph_callers(符号名)`。0 个生产调用方就是没接线。 ### 2.2 依赖图是唯一保证顺序的地方 `agent` 模块读了 `ks.Security` 却没在 `Requires()` 里声明 `FacSecurity`。 「恰好」能用(security 在第 1 层、agent 在第 4 层),但那是**偶然**: 一旦 security 加了自己的依赖被推后,就会静默拿到 nil。 ⇒ 读了谁的字段,就必须声明对谁的依赖。**不要靠层号碰巧对。** ### 2.3 单数路径改了,复数路径漏了 `StopRTChild`(单数)注销入站登记,`StopRTChildren`(复数)漏了。 残留随 create/destroy 单调累积。 ⇒ 写这类对称函数时,判据要**同时**钉两条路径。本仓已有判据专门做这件事。 --- ## 三、权限与生命周期类 ### 3.1 「接上了」有三种层次,容易停在第二层 | 层次 | 内容 | | --- | --- | | 1 | 机制存在(`CheckTool` / `ScanContent` 都写了) | | 2 | 接到了执行链(`securityDecisionFor` 真的调它) | | 3 | **模型正确** | 第三层的典型错误:把「子 agent 继承父的权限」当成「父为子声明策略」。 两者都"接上了",但权限模型完全不同 —— 前者意味着子等于父,后者意味着子更窄。 **判断标准**:**这条策略是谁的意图?** 权限的意图只能来自委派者(父), 不能来自被委派者(子),也不能来自内核默认(全局)。 ### 3.2 以「调用方给的名字」为键的表,必须在生命周期结束时清理 两个实例: - `rtClosedTranscripts[id]` —— 以模型给的名字为键,全仓 0 处删除。 结果:同名重建的 rtchild 读到上一通电话的原文 - `scopesByAgent[agentID]` 的 `longtimeProfile` —— 不释放则同名重建的 agent 继承上一代的约束 ⇒ 键来自外部(模型、调用方)就不能假设它唯一。 ### 3.3 提权口子:让调用方声明自己是谁 `Kind: "resident"` 由 agent 自己写 —— 它同时意味着「我是内部主体」 (豁免 HardPrivateTools)和「我不受收窄」。调用方说自己是内部就拿到豁免。 ⇒ 类别必须由安全模块从**事实**(谁在调、有没有父)推出,不能由调用方声明。 --- ## 四、流程类 ### 4.1 长任务纪律 导出、回填等内存密集任务**绝不与测试并行**(同一会话里踩了三次 OOM)。 ### 4.2 数字类断言要现场取,不要凭记忆 文档里写「17 模块 6 层」,实际是 **20 模块**。给读者的是错的坐标。 ⇒ 数字必须附取数命令(如 `go run ./cmd/homed-depgraph -print`)。 ### 4.3 计划文档不要进仓库 执行计划会在实施完结后变成**误导性读物**:它记录的是"当时打算做什么", 而读者会读成"现在是什么"。 本仓删掉了 9 份已完结的计划(`memory-*-plan.md` 等),保留: - **设计文档**(`*-design.md`):为什么这样设计,有长期价值 - **教训**(本文):踩过什么坑 - **未做登记**(`future-plan.md`):还剩什么没做 - **操作手册**(`*-runbook.md`):怎么做 --- ## 附:这些教训的共同点 回头看,几乎每一条都是同一个形态的变体: > **某种"看起来对了"的信号,与"真的对了"之间存在缺口。** - 判据全绿 ≠ 功能可用 - 机制存在 ≠ 接线生效 - 层号碰巧对 ≠ 依赖声明正确 - 单数路径对 ≠ 复数路径也对 - 名字相同 ≠ 是同一个东西 - 计划写完 ≠ 事情做完 **所以写判据、写文档、写代码时,都要问一句:我现在拿到的这个信号, 在什么情况下会是假的?**