## 为什么删
执行计划在实施完结后**会变成误导性读物**:它记录的是"当时打算做什么",
而读者会读成"现在是什么"。
这在本仓已经造成真实误读 —— `prod-migration-checklist.md` 写着
「状态:HologramAgent 已停机」,那是一次性迁移(2026-10-04)当场的状态,
但照着读会以为服务现在还停着。
## 删了 7 份,删前提取三类内容
| 删掉的文档 | 行数 |
|---|---|
| `memory-restructure-plan.md` | 168 |
| `memory-blockflow-repair-plan.md` | 450 |
| `memory-usability-plan.md` | 309 |
| `scene-memory-fix-plan.md` | 271 |
| `toolcall-parallel-execution-plan.md` | 578 |
| `prod-migration-checklist.md` | 157 |
| `entities-retirement-plan.md` | 118 |
**提取去向**:
1. **教训 → 新建 `engineering-lessons.md`**
这些是它们唯一还有长期价值的部分。汇总成一篇,并按形态归类:
- 判据类(5 条):结构判据不能替代质量判据 / 测「接线之后」≠测「有没有接线」/
验证通过≠架构正确 / fixture 照臆测字段编 / 先改代码再写测试
- 接线类(3 条):机制≠接线 / 依赖图是唯一保证顺序的地方 / 单数改了复数漏了
- 权限与生命周期(3 条):「接上了」有三层 / 以调用方名字为键的表要清理 /
提权口子:让调用方声明自己是谁
- 流程类(3 条):长任务纪律 / 数字要现场取 / 计划不进仓库
文末点出它们共同的形态:**某种"看起来对了"的信号与"真的对了"之间存在缺口。**
2. **实测数据 → 并入 `toolcall-contract-and-sequence-design.md` 附录 A**
那是新旧内核的对比压测(288 条输入 × 两版),核心结论值得留:
```
场景 旧版中位/工具数 新版中位/工具数
slowbatch8 0.220s / 0 个 0.409s / 8 个
```
★ **旧版"更快"是假的** —— 它的 `openai.lua` 缺 `stream_index` 透传,
多个分片并到槽 0 ⇒ 每个工具报参数错误而一个都没真跑。
这是「只看耗时会被骗」的典型。
另附 stage 锁告警的完整归因(不是内核缺陷,成因在插件进程侧运行时)。
3. **未完成的项 → `future-plan.md` §六**
尤其 `entities-retirement-plan` 那条:方案是"三表全退",但 2026-10-31
实测生产库 `entities 1292 / relations 978 / sentences 66` 行**仍在用**。
这个事实不能随文档一起消失。
## 保留两份(它们不是计划,是依据)
- `legacy-table-retirement.md` —— 被 **4 处生产代码注释**引用(`graph.go` ×2、
`commit_blocks_test.go`、`probe_prod_test.go`),是退役判断的依据
- `interrupt-subsystem-split-plan.md` —— 探测事实被 `interrupt_invariant_test.go`
引用;拆分本身确实未实施
## 索引重写
`docs/zh/README.md` 现在是「四类文档」的规则说明:
| 类型 | 回答什么 | 命名 |
|---|---|---|
| 设计文档 | 为什么这样设计 | `*-design.md` |
| 操作手册 | 怎么做 | `*-runbook.md` |
| 教训 | 踩过什么坑 | `engineering-lessons.md` |
| 未做登记 | 还剩什么没做 | `future-plan.md` |
并明确写「**不放执行计划**」及例外条件。
## 顺带修掉的引用
删除后 4 处引用指向不存在的文件:
- `deploy-runbook.md` → 改指 `toolcall-contract-and-sequence-design.md`
- SDK 仓 `guide/stream-tool-call-index.md` / `guide/parallel-tool-declaration.md`
→ 同样改指(这两个在 SDK 仓,随后单独提交)
## 验证
- docs/zh 27 篇,索引里列的全部存在、无遗漏
- 全仓无指向已删文档的引用(代码 + 文档)
- 代码块全部配平
- go build 通过
6.7 KiB
工程教训
这个仓库踩过的坑,以及它们共同的形态。写在这里是因为它们比实现细节更耐用 —— 代码会改,缺陷形态会重复出现。
来源:这些条目原本散落在各份执行计划(
memory-*-plan.md、toolcall-parallel-execution-plan.md等)里。计划本身已随实施完结而删除, 但教训被提出来了 —— 那是它们唯一还有价值的部分。建立于 2026-10-31。
一、判据类(最贵的教训,占了一半)
1.1 「结构判据」不能替代「质量判据」
症状:16 条判据、6 个变异全部通过,而功能完全不可用。
当时的场景:记忆检索改造。判据验的是「字段非空」「条数正确」「不 panic」, 而实际检索结果是:
查询 "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):怎么做
附:这些教训的共同点
回头看,几乎每一条都是同一个形态的变体:
某种"看起来对了"的信号,与"真的对了"之间存在缺口。
- 判据全绿 ≠ 功能可用
- 机制存在 ≠ 接线生效
- 层号碰巧对 ≠ 依赖声明正确
- 单数路径对 ≠ 复数路径也对
- 名字相同 ≠ 是同一个东西
- 计划写完 ≠ 事情做完
所以写判据、写文档、写代码时,都要问一句:我现在拿到的这个信号, 在什么情况下会是假的?