Files
HomeAgent/docs/zh/engineering-lessons.md
JianFeeeee a402bfd777 docs: 删除 7 份已完结的执行计划,把教训与数据提出来
## 为什么删

执行计划在实施完结后**会变成误导性读物**:它记录的是"当时打算做什么",
而读者会读成"现在是什么"。

这在本仓已经造成真实误读 —— `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 通过
2026-10-31 08:50:39 +08:00

180 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 工程教训
> 这个仓库踩过的坑,以及它们共同的形态。**写在这里是因为它们比实现细节更耐用** ——
> 代码会改,缺陷形态会重复出现。
>
> 来源:这些条目原本散落在各份执行计划(`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`):怎么做
---
## 附:这些教训的共同点
回头看,几乎每一条都是同一个形态的变体:
> **某种"看起来对了"的信号,与"真的对了"之间存在缺口。**
- 判据全绿 ≠ 功能可用
- 机制存在 ≠ 接线生效
- 层号碰巧对 ≠ 依赖声明正确
- 单数路径对 ≠ 复数路径也对
- 名字相同 ≠ 是同一个东西
- 计划写完 ≠ 事情做完
**所以写判据、写文档、写代码时,都要问一句:我现在拿到的这个信号,
在什么情况下会是假的?**