mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
## 背景 上一次文档校订(b9b5ac0)之后又有两次代码提交(5194a96 / 8293282), 把 future-plan §1.1 整节变成了错的 —— 它还写着「档位没有生效点」 和「在 prepareInputTask 里加一次 activateSecurityProfile 调用」, 而后者正是被后来的收敛**否定掉的方案**。 ## 新增 docs/zh/README.md(文档索引) 32 篇文档此前混着三种时态(当时实测 / 历史推演 / 设计规划),外观相同。 索引给出**状态分类与可信度规则**: | 状态 | 能不能当事实用 | |---|---| | 已实现并部署 | ✅ 直接信 | | 部分实现 | ⚠️ 只信标了"生效"的部分 | | 设计 / 未实现 | ❌ 只能当设计意图 | | 归档说明 | ❌ 不要改、也不要信 | 并写死**判据名可信度规则**:文档里的 `TestXxx` 不保证还存在。 给了核对命令,以及当前基线(27 条缺失的分布 —— 18 条属"设计未实现"类 文档的规划名、7 条是 §2.7.3 有意保留的历史对照表左栏、2 条属视频能力 未纳入契约)。判定标准:**在"已实现"类文档正文里找不到才是缺陷**。 ## 重写 future-plan §1.1(已结案) 原节写的是错的方案。改成记录**真正的缺陷分三层**: ``` 第 3 层(表面):activateSecurityProfile 零调用方 第 2 层:agent/core 在替 security 做策略决策 (自己抠 security_profile / 自己选判定链 / 自己写 Kind:"resident" 声明主体类别 —— 提权口子) 第 1 层(根因):security 缺少「输入接入点」 ``` 并补上「每 agent 一个实例」的最终形态(共享注册表 + per-agent 执行视图, sessionProfile / longtimeProfile 两层,优先级 session > longtime 本身是安全语义)。 §〇「机制 ≠ 接线」表格里档位那一条从 🔴 改为 ✅。 ## 各文档状态行 此前 14 篇缺状态行(runbook / 实测报告 / 计划类)。逐篇补上**符合实际内容** 的声明,而不是统一套一句。特别地: - `commondev-design.md` 标题本就写「设计 · 未实现」→ 归到"设计"类 - `live-run-evidence-2026-10-04.md` → 明确标注"历史证据快照,不要用它的 结论覆盖现在的实现判断" - `legacy-issues.md` → 写明它与 future-plan 的分工(前者收已知问题, 后者收能力缺失与零调用方) ## 正文里两处判据名漂移 - `TestBootCtxCloseAllIsReverseOrder` → 实际是 `TestKernelStateCloseAllIsReverseOrder`(`BootCtx` 改名时判据跟着改的) - `TestNilTypedBusSafety` → 标注为"规划名,仓库中不存在" ## 新增 §2.7.5 模块产物的实例化粒度 依赖图保证"设施已就绪",但没说该有几个实例 —— 这是独立决策。 记下 security 的两个答案:全局基线共享(否则插件声明对部分 agent 不生效)、 轮次状态 per-agent。并给出给未来新模块的两条判断标准。 ## 安全文档 状态头补「架构形态」段(防火墙定位、per-agent 视图、两层档位); 接线状态表里 `ActivateProfile` 从 🔴 改为 ✅(2026-10-31 接通)。 ## README 文档列表加索引与 future-plan 入口 —— 此前从 README 找不到 docs/zh 的任何指引。 ## 核对 - docs/zh 32 篇全部有状态行 - 索引里列的文档全部存在,无遗漏 - 分类与各文档自述一致(逐篇抽查) - go build 仍通过(纯文档改动)
804 lines
31 KiB
Markdown
804 lines
31 KiB
Markdown
# 生产召回验证:探针 PASS 但结论是假的
|
||
|
||
> **状态:实测报告(结论有效,日期见文内)。** 记录了「探针通过≠能力成立」
|
||
> 这一类失败的具体形态,是判据设计的反面教材。
|
||
> 文末 3 条遗留 sentence→块 边仍开放,已索引到 [`future-plan.md`](./future-plan.md) §六。
|
||
|
||
## 二十五、生产规模验证:**探针 PASS 了,但结论是假的**
|
||
|
||
在生产库快照(1294 entities / 980 relations / 66 sentences / 98 blocks)上
|
||
跑全维度探针,输出:
|
||
|
||
```
|
||
快照规模: 98 块(带向量 97, 99%)
|
||
[casual] 插件目录 ✘ | | | | | | |
|
||
[overwrite] 值班分机 ✘ | | | | | | |
|
||
[abstention] 不存在的面板 ✓ | | | | | | |
|
||
合计 0/0
|
||
--- PASS: TestProbe_生产规模召回 (4.29s)
|
||
```
|
||
|
||
★ **`--- PASS` 而 `合计 0/0`** —— Go 测试框架不把「探针全挂」当失败,
|
||
因为断言只记日志。这正是 memory 里那条判据不可达教训的又一次复现,
|
||
而且这次更隐蔽:**它连 `合计` 都打了 0/0,说明分母是 0**。
|
||
|
||
### 根因:生产库的块是**图像块**,不是文本块
|
||
|
||
```
|
||
memory_blocks: 98 条,modality 全是 image,text_content 非空 0/98
|
||
sentences: 66 条,text 非空 66/66 ← 真正的文本在这
|
||
entities: 1294 条
|
||
```
|
||
|
||
`text_content` 空是**正确的** —— 图像块本来就没有文本。
|
||
而探针判据读的是 `h.Block.Text` ⇒ 全部为空 ⇒ 全部判失败。
|
||
|
||
★ 所以探针**没坏**,它测的是 block 召回,而生产侧的 block 是 98 个
|
||
图像块 —— **判据描述的状态在生产库上不存在**。
|
||
|
||
### 生产侧的正确顺序(还没做)
|
||
|
||
1. `homed-graph-migrate` 1294 entities → 带文本+向量的 block
|
||
2. `homed-graph-distill` 66 sentences → 拆分块
|
||
3. **然后**本探针才有意义
|
||
|
||
### 生产库未被触碰(已核验)
|
||
|
||
```
|
||
生产: inode=16684491 1994752 字节 修改 2026-10-03 05:21:22
|
||
快照: inode=6815957 修改 2026-10-03 22:22:08
|
||
```
|
||
|
||
字节数、mtime、inode 与开始时一致。用 `sqlite3 .backup` 而非 `cp`
|
||
(库在写,`cp` 拿不到一致快照)。
|
||
|
||
### ★ 一个必须记的教训:探针自己会「PASS」
|
||
|
||
**探针全挂 ≠ 测试失败**。`t.Logf` 不改变退出码。
|
||
今后所有探针必须有一条**硬断言**,例如:
|
||
|
||
```go
|
||
if totP == 0 || totT == 0 {
|
||
t.Fatalf("探针未产生任何有效判定(分母为 %d)—— 前提不满足,不是通过", totT)
|
||
}
|
||
```
|
||
|
||
否则「0/0」会被打印成 PASS,**比红更危险** —— 红会让人去查,绿不会。
|
||
|
||
## 二十六、第二个探针缺陷:**期望值是编造的**
|
||
|
||
迁移后(1294 文本块)重跑探针前,先核对了每个 `want` 值是否真的在库里:
|
||
|
||
```
|
||
4324 → 0 块 老周 → 0 块
|
||
9090 → 0 块 8080 → 0 块
|
||
plugins → 7 块 graph.db → 1 块
|
||
```
|
||
|
||
★ **六个 want 值里四个在生产库中根本不存在** —— 我是从 ha-c 测试库
|
||
抄过来的。探针会全判失败。
|
||
|
||
### 这与「假 PASS」是同一类错误的两面
|
||
|
||
```
|
||
假 PASS 什么都没判定,却报成功 (分母为 0)
|
||
编造期望 判据描述的状态在库里不存在 (期望值是抄来的)
|
||
```
|
||
|
||
★ 两者都是**判据与实际数据脱节**,只是方向相反:
|
||
一个是判据要求太多(0 条判定也算过),一个是判据要求的东西不存在。
|
||
|
||
⇒ 教训:**探针的期望值必须从被测库里取,不能从另一个库或记忆里抄。**
|
||
核对动作应该是探针的前置步骤,而不是跑完看分数才发现。
|
||
|
||
### 已按生产库真实数据重写(10 条)
|
||
|
||
```
|
||
casual 脚本路径 ${HA_DATA}、工具链 plugindev、
|
||
公网地址 101.201.37.155、本机端口 13010
|
||
coexist 并存端口 13010/13011 不该互斥
|
||
confusable 13011 与 13010 别答错对象
|
||
abstention grafana / 容灾演练 / kafka —— 库里没有,不该编造
|
||
```
|
||
|
||
★ 生产库的端口事实是 **13010/13011 这类并存的**(不是新旧覆盖),
|
||
所以 `overwrite` 维度在生产数据上**不适用** —— 改成了验证「不误判」。
|
||
强行套用 ha-c 的覆盖场景就是又一次「判据不可达」。
|
||
|
||
## 二十七、生产快照迁移结果
|
||
|
||
```
|
||
块 98 → 1392 (+1294 实体块,全部带向量)
|
||
块边 97 → 2350 (+980 关系边)
|
||
耗时 104 秒
|
||
自动快照 prod.db.bak-20261003-222649
|
||
带文本块 1294/1392(另 98 个是 image 块,无文本是正确的)
|
||
```
|
||
|
||
### 顺带修的报告口径缺陷
|
||
|
||
```
|
||
报告 块边 966 ← legacyStats → COUNT(*) WHERE status='active'
|
||
实际 块边 980 ← MigrateLegacyTextEntities → ORDER BY id(不过滤)
|
||
```
|
||
|
||
用户会把「预计产出」当承诺,**差 14 条就说明它不可信**。
|
||
已让 `Introspect` 数全表与迁移同口径,并保留真实不确定性:
|
||
「孤儿关系会被跳过,实际可能略少」。
|
||
|
||
## ★ 一条操作纪律(我自己又犯了)
|
||
|
||
蒸馏用 `| tail -12` 管道跑,**`tail` 要等进程结束才吐**,
|
||
于是 66 条 × 28 秒的活儿看起来「零输出、进程 0% CPU」,
|
||
我误判成卡死。
|
||
|
||
代码里明明有进度输出(每 10 条或 20 秒一行),
|
||
而 memory 里那条约束写得很清楚:**长任务必须实时写日志,不能用全缓冲 tail 管道**。
|
||
|
||
⇒ 第几次了:**约束记住了,动手时没查**。
|
||
正确做法:直接重定向到文件(`> file.log`),不用管道。
|
||
|
||
## 二十八、生产规模召回:**3/10,且原因不是判据**
|
||
|
||
硬断言生效(`--- FAIL` 而非假 PASS),10 条探针的真实结果:
|
||
|
||
```
|
||
[casual] 脚本路径 ✘ [casual] 插件工具链 ✓
|
||
[casual] 公网地址 ✘ [casual] 本机端口 ✘
|
||
[coexist] 并存端口 ✘
|
||
[confusable] 13010与13011 ✓
|
||
[abstention] 三个不存在的 ✘✘✘ ← 全部编造
|
||
合计 0/0(分母 bug:coexist 维度名不在列表里)
|
||
```
|
||
|
||
★ 全部 want 值已核对**确实在库里**(1~11 块命中):
|
||
`${HA_DATA}` 11 块、`plugindev` 1、`101.201.37.155` 1、
|
||
`13010` 2、`13011` 2 —— 这次不是编造期望,是真的召回不到。
|
||
|
||
### 根因:分数挤成一团,没有区分度
|
||
|
||
```
|
||
查询「本机 13010 端口对应什么」→ 8 命中
|
||
1. 0.9006 本机 443 按 SNI 透传到 192.168.2.106:3080
|
||
2. 0.8712 ACP回环调用自身12001
|
||
3. 0.8611 MAR/MDR与ALU不直通必须经CPU内部总线
|
||
4. 0.8583 本地网关8081
|
||
5. 0.8488 CPU总线与MAR/MDR/ALU连接问题
|
||
6. 0.8462 CPU总线问题记忆缺失待补
|
||
7. 0.8410 防呆已实测生效
|
||
8. 0.8405 无工具插件属正常
|
||
★ 目标块「13010/13011 而非 12011」连 top2000 都没进
|
||
```
|
||
|
||
★ **全部 8 条挤在 0.84~0.90**。目标块含精确串 `13010`,
|
||
词法上是最强的信号,向量却给不出任何区分 —— 这是
|
||
**chineseclip 在长尾中文实体名上的失效**,不是中心化没做对。
|
||
|
||
### ★ 为什么 ha-c 的 7/7 不能推广
|
||
|
||
```
|
||
ha-c 448 块,运维域高度集中(排期/容量预警/告警规则…)
|
||
查询与块的词汇重叠高 → 7/7
|
||
|
||
生产 1391 块,横跨全部业务域(AgentMail/CPU总线/GUI/gateway…)
|
||
查询落在长尾 → 3/10
|
||
```
|
||
|
||
**7/7 是小库 + 同质分布的产物,不是「召回质量好」的证据。**
|
||
这正是 memory 里那条「金标准必须覆盖输入分布而不只是输出形态」的
|
||
又一次复现 —— 我拿了小库的结论当大库的预期。
|
||
|
||
### abstention 3/3 全失败 = 全编造
|
||
|
||
```
|
||
查询「grafana 监控面板的端口是多少」→ 召回 CPU总线 / MAR-MDR / 本机443…
|
||
查询「kafka 的 broker 地址是什么」 → 召回 read_thread / sdk站 / 跨实现校验…
|
||
```
|
||
|
||
★ 库里没有任何相关块,但向量仍给出 0.84+ 的高分 ⇒ **纯编造**。
|
||
这是比误召回更严重的一类失效:**系统对「不知道」毫无表达能力**。
|
||
|
||
## 结论:生产规模下召回不可用
|
||
|
||
| | ha-c(448块) | 生产快照(1391块) |
|
||
|---|---|---|
|
||
| casual | 3/3 | 2/4 |
|
||
| overwrite | 2/2 | 不适用(数据是并存非覆盖) |
|
||
| confusable | 2/2 | 1/1 |
|
||
| **abstention** | 0/0(无此维度) | **0/3(全编造)** |
|
||
| 合计 | 7/7 | **3/10** |
|
||
|
||
⇒ 在把召回接回生产前,必须先解决两件事:
|
||
1. **区分度**:分数挤在 0.84~0.90,长尾实体名召不回
|
||
2. **拒答能力**:0/3 编造 —— 系统需要「相似度不够就说没有」
|
||
|
||
第 2 条更紧急:它不是召回不准,而是**会给用户错误的答案**。
|
||
|
||
## 二十九、区分度问题的三层根因
|
||
|
||
### 第一层:中心向量生产路径无调用者(已修 `1255ea2`)
|
||
|
||
`graph_centroid` 表在生产快照里**根本不存在**。`RebuildCentroid` 有实现、
|
||
有测试、有持久化、有召回侧消费,**唯独没有生产调用者** ——
|
||
ha-c 那一行是手工测试时留下的。
|
||
|
||
建中心后实测:
|
||
|
||
均值向量长度 0.8041,平均两两余弦 0.6466,anomalous=true
|
||
样本 1391,维度 512
|
||
|
||
★ **平均两两余弦 0.6466** 解释了「分数挤在 0.84~0.90」:
|
||
任意两个块的相似度基线就有 0.65。
|
||
|
||
### 第二层:向量对「精确数字串」不敏感(这是真问题)
|
||
|
||
中心化后重测,同一条查询:
|
||
|
||
查询「本机 13010 端口对应什么」
|
||
召回 「13000端口」(7字符) ← 进了 top8
|
||
「本地网关8081」(8字符)
|
||
没召回「13010/13011 而非 12011」(20字符) ← 含**精确串 13010**
|
||
|
||
★ **短文本反而召回了,含精确串的反而召不回** ——
|
||
chineseclip 的句向量把 `13010` 这个 token 稀释在 20 字里,
|
||
而 7 字的「13000端口」几乎全是有效信号。
|
||
|
||
⇒ 这不是各向异性能解决的。**中心化改不了「词法强信号被稀释」。**
|
||
|
||
### 第三层:没有符号路兜底
|
||
|
||
```
|
||
词法 LIKE '%13010%' → 2 块命中(正确)
|
||
向量 top8 → 0 块命中(含 13010 的都不在)
|
||
```
|
||
|
||
而 `memory_recall` 现在是**「块命中优先、符号结果 fallback」**,
|
||
不是真正的向量+符号融合排序:
|
||
块一旦命中(同域块总是能命中一些),符号路就没有机会参与排序。
|
||
|
||
⇒ **当前架构下,向量的弱会让符号路完全没有补位机会。**
|
||
|
||
## 三十、修正后的真实分数:2/9
|
||
|
||
```
|
||
维度 casual 1/4 有缺口
|
||
维度 coexist 0/1 有缺口 ← 连「不误判」都做不到
|
||
维度 confusable 1/1 OK
|
||
维度 abstention 0/3 ★ 编造
|
||
合计 2/9
|
||
```
|
||
|
||
★ 之前观察到的「合计 0/0」是**第三个 bug**:map 值拷贝没写回 ——
|
||
`st := byDim[k]; st[1]++` 不写回,`byDim` 永远是零值,
|
||
汇总时全部 `continue`。我误判成「前提不满足」。
|
||
|
||
★ 这个 bug 藏了两次重跑。教训:**「0/0」有两种可能** ——
|
||
真的没判定(前提不满足),或**判定了但没记进去**(代码 bug)。
|
||
我一开始只想到前者。
|
||
|
||
## 结论:需要向量+符号融合,不是修向量
|
||
|
||
| 层次 | 状态 |
|
||
|---|---|
|
||
| 各向异性 | ✅ 已修(中心化补上调用者) |
|
||
| 精确串稀释 | ❌ 向量模型固有限制 |
|
||
| 符号兜底 | ❌ 架构上是 fallback 而非融合 |
|
||
|
||
⇒ 单纯调向量(换模型、调中心化、调 topK)**解决不了**这一层。
|
||
必须做**融合排序**:把词法命中作为一路信号,与向量分数加权/串并,
|
||
而不是「块命中就跳过符号路」。
|
||
|
||
## 三十一、拒答能否靠阈值?**测出来「可分」,但不可信**
|
||
|
||
这是决定 abstention 怎么做的关键测量:
|
||
**真实查询(库里有)与编造查询(库里没有)的 top1 分数能否分开?**
|
||
若能分 → 加阈值即可;若重叠 → 阈值必然误伤,必须靠符号路。
|
||
|
||
```
|
||
真实查询 top1
|
||
脚本路径改到哪个目录了 0.6731
|
||
从零开发 QQ 插件用什么工具链 0.6390 ← 最低
|
||
agentmail 公网访问地址是什么 0.8041
|
||
本机 13010 端口对应什么 0.6435
|
||
|
||
编造查询 top1
|
||
grafana 监控面板的端口是多少 0.4320 ← 最低
|
||
谁负责数据库容灾演练 0.5231
|
||
kafka 的 broker 地址是什么 0.5890
|
||
redis 集群的主从复制配置在哪 0.5923 ← 最高
|
||
|
||
真实最低 0.6390 ┃ 编造最高 0.5923
|
||
⇒ 间隔 0.047,阈值 0.6157
|
||
```
|
||
|
||
### ★ 但这个「可分」不可信
|
||
|
||
```
|
||
4 个真实、4 个编造,各压在阈值一侧
|
||
真实最低与编造最高的间隔只有 0.047
|
||
```
|
||
|
||
★ 0.6390 和 0.5923 分别是「4 个里最差的那个」。
|
||
只要任一侧多一个样本,间隔就可能变成负数。
|
||
|
||
**n=4 的可分性不是可分性。** 这正是 memory 里那条
|
||
「金标准必须覆盖输入分布」的又一形态 ——
|
||
**4 个编造样本不能代表「用户会问什么库里没有的东西」**。
|
||
|
||
真实使用里「库里没有」的查询分布远不止这 4 个:
|
||
`sentry 告警怎么接`、`postgres 主库地址`、`张伟负责哪个模块`……
|
||
每一个都可能落在 0.60 以上(`kafka broker` 就已经 0.5890)。
|
||
|
||
⇒ **不能据此加阈值上线。** 需要的是:
|
||
1. 扩充编造样本到 50+ 条(覆盖多种「看起来合理但库里没有」的形态)
|
||
2. 且必须测量 **ROC / 假阳率**,而不是「4/4 分开了」
|
||
|
||
## 顺带确认:符号路优先级的设计意图是对的,但没生效
|
||
|
||
`toolcall.go:236` 的注释写着:
|
||
|
||
> 端口号(8328)、分机号(4324)这类纯数字串向量天然弱,
|
||
> 而它们恰是本项目最常问的。两条路都跑,块向量在前,符号路兜底。
|
||
|
||
★ 设计意图**完全正确**,而实测证明**没生效** ——
|
||
因为「块向量在前」意味着块一旦有命中(非空字符串)就直接 return,
|
||
符号路的 `RecallSorted` 根本没被调用。
|
||
|
||
⇒ 要做的不是改设计,而是让它真正融合:
|
||
两路都跑,按**统一分数**重排,而不是「谁先返回听谁的」。
|
||
|
||
## 三十二、融合召回接入:2/9 → 3/9,收益范围明确
|
||
|
||
### 接入时改的两处(都不是"顺手")
|
||
|
||
**1. MinScore 从 0.5 改成 0**
|
||
|
||
融合分数的**量纲变了**:
|
||
|
||
```
|
||
纯向量 余弦序,阈值 0.5 是实测得出的(无关句也能到 0.83)
|
||
融合 精确串命中 = 1.0(布尔置顶)
|
||
向量加权 = 0.7 × 余弦 ⇒ 典型 0.35~0.45
|
||
```
|
||
|
||
若沿用 0.5:**精确串命中的 1.0 留下,所有纯向量候选被全筛掉** ——
|
||
反而丢掉向量侧的有效召回。筛选改由融合内部按路处理。
|
||
|
||
**2. 输出标签从 modality 改为召回来源**
|
||
|
||
```
|
||
[exact] 精确串命中(分数统一显示 1.00,它是布尔信号不是强度信号)
|
||
[symbol] 仅符号命中(向量没召回)
|
||
[text] 普通向量命中
|
||
```
|
||
|
||
模型需要知道**为什么**这条被召回 —— 这也是日后排查误召回的唯一线索。
|
||
|
||
### 结果
|
||
|
||
```
|
||
维度 casual 2/4 coexist 0/1 confusable 1/1 abstention 0/3
|
||
合计 3/9(纯向量基线 2/9)
|
||
```
|
||
|
||
★ 归因统计直接证明融合在起作用:
|
||
|
||
```
|
||
[casual] 本机端口 ✓ 13010/13011 而非 12011 [+符号 2: 精确2]
|
||
[confusable] 13010与13011 ✓ 13010/13011 而非 12011 [+符号 2: 精确2]
|
||
```
|
||
|
||
`13010/13011 而非 12011` 现在**排第一** ——
|
||
而纯向量下它**连 top2000 都进不去**。
|
||
|
||
### ★ 但收益范围必须说清楚
|
||
|
||
融合只救了**「查询里含精确数字串/版本串」**这一类:
|
||
|
||
| 探针 | 查询含精确串 | 融合是否有效 |
|
||
|---|---|---|
|
||
| 本机 13010 端口 | ✓ | ✅ 救回并置顶 |
|
||
| 13010 与 13011 | ✓ | ✅ 救回并置顶 |
|
||
| 脚本路径改到哪个目录 | ✗ | ❌ 无改善 |
|
||
| agentmail 公网访问地址 | ✗ | ❌ 无改善 |
|
||
| 本机服务监听哪些端口 | ✗ | ❌ 无改善(无串可提) |
|
||
|
||
★ **「脚本路径」「公网地址」这两个失败不是符号路能解决的** ——
|
||
查询里没有可提取的符号(`${HA_DATA}`、`101.201.37.155`
|
||
只存在于**答案**里,不在查询里)。这类「同义改写」问题
|
||
符号路天然无能,只能靠更强的向量。
|
||
|
||
⇒ **融合解决的是「精确串被稀释」,不是「语义不匹配」。** 3/9 里的 3 分
|
||
全部来自精确串类探针,一点也不来自语义类。
|
||
|
||
### abstention 仍是 0/3 —— 且融合帮不上
|
||
|
||
```
|
||
「grafana 监控面板的端口是多少」 库里没有 grafana,查询也无精确串
|
||
「谁负责数据库容灾演练」 同上
|
||
「kafka 的 broker 地址是什么」 同上(但 `kafka` 是拉丁词,能提出来)
|
||
```
|
||
|
||
★ 融合**不可能解决编造** —— 编造查询的共同特征正是
|
||
「**库里没有、查询里也没有对应符号**」。
|
||
符号路对这类查询天然给 0 分,与向量路同样无能为力。
|
||
|
||
⇒ **编造问题需要一个独立的第三种机制**(如「top1 分数 + 跨度」
|
||
的联合判据,或「库里是否存在该符号」的显式检查),
|
||
不是继续调融合能解决的。
|
||
|
||
## 三十三、方案 A 退场的决定性验证(2026-10-04)
|
||
|
||
从生产库重新 `.backup` 出干净快照(`fresh.db`),用**新代码**
|
||
(块→块路径,5a70452/0586f6b)跑完整迁移:
|
||
|
||
```
|
||
迁移前:entities=1294 relations=980 sentences=66 blocks=98 block_edges=97
|
||
|
||
迁移完成(100.8s)
|
||
句子 0,块 1294,边 959(跳过孤儿 0,去重边 21,无向量 0)
|
||
迁移后:块 2686,块边 2350
|
||
已重建中心向量(样本 1391,均值范数 0.8041,anomalous=true)
|
||
```
|
||
|
||
### 退场判据逐条核对
|
||
|
||
| 判据 | 结果 |
|
||
|---|---|
|
||
| `sentences` 表未被写入 | ✅ 迁移前后都是 **66** |
|
||
| contains 边为块→块 | ✅ **1294** 条 |
|
||
| 原句块已生成 | ✅ `source=sentence` **1294** 块 |
|
||
| 迁移块 | ✅ `source=legacy-entity` **1294** 块 |
|
||
| 去重边被显式报出 | ✅ `去重边 21`(dd2c996 的修复生效) |
|
||
| 中心自动重建 | ✅ 迁移后立即重建,无需人工 |
|
||
|
||
块总数 98 → 2686:98 图像块 + 1294 迁移块 + 1294 原句块。
|
||
|
||
### ★ 3 条遗留 sentence→块 边(非迁移产生)
|
||
|
||
```
|
||
source_id 1427 / 1428 / 1429 → blk_1789…
|
||
```
|
||
|
||
迁移前 `block_edges=97`,其中 3 条已是 sentence→块 形态 ——
|
||
是**生产库原有的历史数据**(旧 `sentences` 表的行号做端点),
|
||
迁移不处理它们(它只负责 entities/relations 的转换)。
|
||
|
||
⇒ 存量清理时这 3 条也要一并处理,否则 sentences 表删掉后它们会悬空。
|
||
|
||
### 方案 A 现状(最终)
|
||
|
||
| 项 | 状态 |
|
||
|---|---|
|
||
| `sentences` 写入点 | ✅ **清零**(迁移与蒸馏都不再写) |
|
||
| `entities` 写入点 | ✅ 只读 |
|
||
| `relations` 写入点 | ✅ 只读 |
|
||
| `memory_block_edges` | ✅ 唯一边表(3 条历史遗留待清理) |
|
||
| 原句可回溯性 | ✅ 由原句块承载(内容派生 ID,幂等) |
|
||
|
||
**剩余工作只有存量清理**,而它必须在生产完成蒸馏 + 召回验证 + 回滚演练之后。
|
||
|
||
## 三十四、生产蒸馏试跑:11% 可拆率是数据事实,不是代码问题
|
||
|
||
用新代码(块→块路径)在 `fresh.db` 上试跑 5 条:
|
||
|
||
```
|
||
待处理实体 5 条;现有块 2686(带向量 1391)
|
||
拆解结果:5 条实体 → 0 个字段块;零字段 5 条,报错 0 条
|
||
```
|
||
|
||
**零字段 5/5 —— 先核实是模型问题还是数据问题。**
|
||
|
||
### 抽样的 5 条实体名
|
||
|
||
```
|
||
#读取并处理-homeagent-来信
|
||
(136, 62, -122) 平原水岸
|
||
+08:00的加号在query中解码成空格,服务端RFC3339解析失败后静默退回默认窗口
|
||
+365天会算成2027-09-04,差11天
|
||
/api/sources、/api/keys、/api/status
|
||
```
|
||
|
||
**这些确实没有可拆的 (主语,维度,值)** —— 路径列表、坐标、单个日期。
|
||
零字段在这里是**正确结果**。
|
||
|
||
### 量化两个库的差异(决定产出上限)
|
||
|
||
```
|
||
生产 fresh 429 条实体,含明确可拆结构 47 条 (11%)
|
||
测试 ha-c 146 条实体,含明确可拆结构 121 条 (83%)
|
||
```
|
||
|
||
★ **差 7.5 倍。** ha-c 的产出(260 个拆分块、7/7 端到端)建立在
|
||
83% 可拆率上;生产库只有 11%。
|
||
|
||
⇒ 全量 429 条跑完,预期字段块约 **47 条左右**(不是 429 条,
|
||
也不是 ha-c 那样的几百条)。
|
||
|
||
### 这意味着什么
|
||
|
||
生产侧蒸馏的**收益远小于测试库**。而召回质量的第一限制因素
|
||
不是拆分算法(71% 字段级 F1 已达标),而是**数据里有没有结构可拆**。
|
||
|
||
⇒ 「生产迁移后召回能到 7/7」这个预期**不成立**。
|
||
7/7 是 83% 可拆率 + 448 块的产物;生产是 11% + 2686 块。
|
||
|
||
★ 这又一次印证「金标准必须覆盖输入分布」——
|
||
我在 ha-c 上验证的一切(融合、仲裁、拒答、拆分)都建立在
|
||
**输入分布严重不同**的前提上。
|
||
|
||
## 三十五、生产蒸馏产出为 0 —— 根因是已知的 Ollama bug
|
||
|
||
跑 60 条(副本 `dist60.db`):
|
||
|
||
```
|
||
[60/60] commit f91b27a,2026-09-03 … 用时837s
|
||
拆解结果:60 条实体 → 0 个字段块;零字段 60 条,报错 0 条
|
||
块总数 2686 → 2686
|
||
```
|
||
|
||
**我预估是「最多 57 个」(11% 可拆率 × 429 条)。实际是 0。**
|
||
⇒ 上一条的预期偏乐观了,而**偏差的根因不是数据质量,是工具链。**
|
||
|
||
### 直接复现
|
||
|
||
同一条输入连发三次,只换提示词:
|
||
|
||
```
|
||
任务A(拆三元组) 118s → "anter\n\n</think>\n\n要将句子「commit f91b27a...」拆成三元组,
|
||
首先要理解句子的结构和内容。\n\n### 分析句子:..."
|
||
任务B(判断疑问句) 15s → "no"
|
||
任务A(重复) 83s → "根据输入句子「commit f91b27a...」,可以将其拆分为三元组:
|
||
\n- **字段名**:commit \n- **字段值**:f91b27a ..."
|
||
```
|
||
|
||
★ 两个问题同时存在:
|
||
|
||
1. **`think=False` 没有生效** —— 输出里带着 `</think>`,
|
||
说明模型仍在输出推理链
|
||
2. **推理文本把 JSON 挤出窗口** —— `num_predict=300` 全被 `### 分析句子:`
|
||
这类前缀吃掉,JSON 部分根本没生成完
|
||
|
||
⇒ 抽取器收到的是**推理文本而非 JSON**,解析失败 ⇒ 零字段。
|
||
而 14 秒/条的耗时证明 LLM **确实被调用了**,不是没跑。
|
||
|
||
### 这是本会话第三次撞上同一个 bug
|
||
|
||
`docs/zh/triple-extract-attention.md` 里已记:
|
||
|
||
> ①「模型随机性(差 40%)」实际是 ollama schema 污染
|
||
> `label3.py` 使用逐个 `}` 枚举解析 JSON,避免贪婪匹配导致 `Extra data`
|
||
|
||
而**蒸馏命令没做同样的防御** —— 它复用了带 schema 的抽取器,
|
||
于是踩进同一个坑。
|
||
|
||
### 结论修正
|
||
|
||
| 我先说的 | 实际 |
|
||
|---|---|
|
||
| 「生产可拆率 11%,产出约 57 个」 | 产出 **0**,与可拆率无关,是 JSON 没生成出来 |
|
||
| 「全量 429 条约 111 分钟」 | **现在不该跑** —— 跑完也是 0 |
|
||
|
||
⇒ **先修 JSON 生成,再谈产出量。** 一个 60 条就能测的修复,
|
||
比 111 分钟的全量跑重要得多。
|
||
|
||
### 修复方向(三选一,未实测)
|
||
|
||
1. **关掉 think 真正生效** —— 查 ollama 0.31.1 的正确参数
|
||
(可能需要 `chat_template_kwargs: {"enable_thinking": false}`)
|
||
2. **加大 num_predict** —— 但 300 都不够说明推理链极长,
|
||
关掉 think 才是根治
|
||
3. **换模型** —— `qwen3-vl:2b` 之前测过批处理返回空;
|
||
`qwen3.8:27b-iq2m` 未测过单条 JSON
|
||
|
||
## 三十六、蒸馏产出 0 的根因:**Ollama schema 污染**(再次复现)
|
||
|
||
### 先排除两个错误假设
|
||
|
||
**假设 1「think 没关掉」—— 不成立。** 三种参数都试过:
|
||
|
||
```
|
||
think:false 66s think残留=False
|
||
think + enable_thinking:false 68s think残留=False
|
||
options.num_think:0 69s think残留=False
|
||
```
|
||
|
||
输出里都没有 `</think>`。⇒ `think` 已经关掉了,**推理链不是根因**。
|
||
|
||
**假设 2「schema 双重编码」—— 不成立,而且是我的验证脚本有 bug。**
|
||
|
||
```
|
||
HTTP 500 invalid format: "\\\"{\\\"type\\\": ... ← 我把 json.dumps 的结果当 format 值传
|
||
```
|
||
|
||
Go 侧的 `body.Format = json.RawMessage(s)` 是**正确**的(嵌入原样输出,
|
||
不会二次编码)。⇒ 生产代码没问题,**我的 Python 测法错了**。
|
||
|
||
★ 这是本会话第四次「先归因后核实」,而且这次我差点把 bug 归到生产代码上
|
||
—— 只因为「schema 看起来可疑」。
|
||
|
||
### 真正的根因:schema 污染(同一个 bug 第四次复现)
|
||
|
||
修正测试脚本(`format` 直接传对象,不 `dumps`)后:
|
||
|
||
```
|
||
任务A(拆三元组) 34s → {"fields":[{"name":"commit","value":"f91b27a"},
|
||
{"name":"date","value":"2026-09-03"},
|
||
{"name":"status","value":"已推送"}]}
|
||
任务B(判断疑问句) 28s → { "fields": [ { "name": "text",
|
||
"value": "commit f91b27a,2026-09-03 已推送" } ] }
|
||
任务A(重复) 17s → 与任务A第一次**逐字节相同**
|
||
```
|
||
|
||
★ 两个症状同时出现:
|
||
|
||
1. **任务 B 完全不同的任务,返回了同一 schema 结构** ——
|
||
问「是不是疑问句」却得到 `fields[{name,value}]`
|
||
2. **同输入逐字节相同** —— 不是随机,是**污染后的确定性输出**
|
||
|
||
⇒ 抽取器拿到的是 `{"fields":[{"name":"text","value":"整句话"}]}` 这种
|
||
**无意义的字段名 + 整句当值**,全部被「值 ≤12 字 + 值须原样出现 + 属性名对得上」
|
||
三道闸门拦掉 ⇒ **零字段**。
|
||
|
||
### 这个 bug 的性质
|
||
|
||
它不是「模型能力不足」,是 **ollama 0.31.1 的 grammar 缓存缺陷**:
|
||
schema 被上一次请求「粘」到下一次。表现随**调用顺序**而变:
|
||
|
||
```
|
||
先 schema 请求 → 污染后续所有请求
|
||
先无 schema 请求 → 输出较正常(记忆里 label3.py 的观察)
|
||
```
|
||
|
||
### 修复方向(按优先级)
|
||
|
||
| | 方案 | 成本 | 风险 |
|
||
|---|---|---|---|
|
||
| A | **每请求唯一 nonce**(在 prompt 里加随机串,字段名也随之变化) | 低 | JSON 可能截断 |
|
||
| B | **不用 schema**,改用 `<record>` 分隔的纯文本协议 + 宽松解析 | 低 | 解析容错要求高 |
|
||
| C | 升级/换掉 ollama | 高 | 影响其他任务 |
|
||
|
||
★ 记忆里 `label3.py` 用的是**方案 A 的变体**(nonce),
|
||
且已记录它的两个副作用(JSON 截断、`<record>` 碎片循环)。
|
||
蒸馏侧应该复用那套防御,而不是重新踩一遍。
|
||
|
||
### 一个必要的判据
|
||
|
||
蒸馏目前的产出统计是「零字段 N 条」——**它无法区分**:
|
||
|
||
```
|
||
真的没有可拆字段(数据事实)
|
||
LLM 返回了垃圾(工具链故障)
|
||
```
|
||
|
||
⇒ 必须先让 `homed-graph-distill` **打印前 3 条的原始 LLM 输出**。
|
||
代码里本来就有这个打印(`i < 8` 时打印字段),但因为零字段所以什么都不显示
|
||
—— **判据能显示 0,是因为它只看得到 0。**
|
||
|
||
## 三十七、★ 根因第三次修正:**不是 ollama,不是 schema,是无主语**
|
||
|
||
### 我连续三次猜错了
|
||
|
||
| 我的判断 | 验证方式 | 结果 |
|
||
|---|---|---|
|
||
| 「think 没关掉,推理链挤出 JSON」 | 三种参数实测(think:false / enable_thinking:false / num_think:0) | **都不成立**,输出里没有 `</think>` |
|
||
| 「schema 污染导致返回垃圾字段」 | 用**同样 schema + 同样 prompt** 直接调 ollama | **不成立**,返回 `{"fields":[{"name":"commit","value":"f91b27a"},…]}` 三个正确字段 |
|
||
| 「字段被三道闸门拦掉」 | 读代码 | **第三道之后还有第五道闸门** |
|
||
|
||
### 真正的根因
|
||
|
||
```go
|
||
// split.go:248
|
||
// 无主语时写不出 subject,直接跳过 —— 宁可少记也不写悬空属性。
|
||
if len(subjects) == 0 {
|
||
continue
|
||
}
|
||
```
|
||
|
||
`deriveSubjects(record)` 返回空 ⇒ **全部字段被丢弃**。
|
||
|
||
实测哪些实体没有主语:
|
||
|
||
```
|
||
commit f91b27a,2026-09-03 已推送 0 主语 → 字段全丢
|
||
127.0.0.1:12100(lan=false…) 0 主语 → 字段全丢
|
||
/api/sources、/api/keys、/api/status 0 主语 → 字段全丢
|
||
第114批周二凌晨1点·停机4分… 1 主语 → ✓
|
||
值班室分机号 4324,值班 老周 1 主语 → ✓
|
||
```
|
||
|
||
★ LLM **完全做对了**(3 个字段全对),却被 Go 侧的
|
||
「无主语不写悬空属性」规则丢掉。
|
||
|
||
⇒ 「零字段 60 条」的真相:**不是模型不行,也不是数据没得拆,
|
||
而是主语提取在无主语句式上失效**。
|
||
|
||
### 三个诊断缺口叠加,让这个根因被掩盖了三轮
|
||
|
||
```
|
||
① 命令只在 payload != nil 时打印 → 零字段时什么都不显示
|
||
② 报错与零字段混在一个统计里 → 看不出「谁在丢弃」
|
||
③ 没有主语计数的读数 → 字段凭空消失
|
||
```
|
||
|
||
⇒ 这就是「判据的观测面决定你能看见什么」的三个层次。
|
||
**修复第①层(无条件打印)之后,才看见「合法 JSON 但零字段」;
|
||
修复第②层之后,才有机会去查闸门;缺第③层至今。**
|
||
|
||
### 修复方向
|
||
|
||
| 方案 | 说明 |
|
||
|---|---|
|
||
| A | 无主语时用**属性型主语**(`62eba81` 已有 `DeriveSubjects` 的属性型受控别名) |
|
||
| B | 无主语时退化成**键值对块**(`<维度>=<值>`,无主语) |
|
||
| C | 放宽闸门,接受悬空属性 | ❌ 会污染图谱,与设计意图冲突 |
|
||
|
||
⇒ **A 与 B 都可行,且 B 更通用**(键值块不需要主语,
|
||
而 block-only 架构本来就允许块文本为 `<维度>=<值>`)。
|
||
但两者都要先有判据钉住「哪些句式被判为无主语」,
|
||
否则又是一轮「改了但不知道有没有变好」。
|
||
|
||
## 三十八、主语缺失率 99% —— 蒸馏方案在生产侧不成立
|
||
|
||
补上第③层判据(主语计数),在生产库 429 条实体上实测:
|
||
|
||
```
|
||
总计 429 条:有主语 5 条(1%),无主语 424 条(99%)
|
||
|
||
形态分布
|
||
命令/commit 2 条 无主语 100%
|
||
符号开头(符号学) 2 条 无主语 100%
|
||
路径/URL 38 条 无主语 100%
|
||
其他 387 条 无主语 98.7%
|
||
```
|
||
|
||
### 而 ha-c 的主力句式在生产库一条都没有
|
||
|
||
```
|
||
生产库含「第N批」 0 条
|
||
生产库含「分机」 0 条
|
||
生产库含「端口」 11 条
|
||
```
|
||
|
||
★ ha-c 的 7/7 完全建立在「第114批…」「值班室分机号 4324」这类
|
||
**有明确主语锚点**的句式上。生产库 99% 没有锚点。
|
||
|
||
### 三层结论串起来
|
||
|
||
```
|
||
① LLM 拆得对 3 个字段全对(实测)
|
||
② 第五道闸门丢弃 无主语 → 全部 continue
|
||
③ 99% 的生产实体无主语 ⇒ 放宽闸门也救不回多少
|
||
```
|
||
|
||
⇒ **放宽闸门(方案 B:键值块)最多救回 1% 的实体**。
|
||
因为 99% 的实体压根没有主语可写,键值块也写不出 `<主语>|<维度>`。
|
||
|
||
⇒ 生产侧的蒸馏路径**整体不成立**,不是"修一下 bug 就能跑"。
|
||
|
||
### 正确的方向
|
||
|
||
既然 99% 的生产实体是「无主语的通用事实」,那它们**本来就应该**
|
||
以整句块形态存在(`legacy-entity` 迁移块已经是这样),
|
||
而**不该**被强行拆成三元组。
|
||
|
||
⇒ 生产侧记忆的正确形态是:
|
||
|
||
整句块(可召回)+ 少量键值块(11 条「端口」类的那些)
|
||
|
||
⇒ 蒸馏命令应该**只对有主语的实体跑**,其余跳过并明确报告:
|
||
|
||
待处理 429 条 → 有主语 5 条,实际处理 5 条
|
||
|
||
### 这个发现的元价值
|
||
|
||
今天我在生产侧反复撞到的不是"bug",而是**假设错误**:
|
||
|
||
「生产库迁移后召回能到 7/7」 —— 错,7/7 是 83% 可拆率 + 主语锚点的产物
|
||
「可拆率 11%,产出约 57 个」 —— 错,实际是 0(第五道闸门)
|
||
「先修 bug 再跑全量 429 条」 —— 错,99% 无主语,跑完也是 0
|
||
|
||
⇒ **而每一个错误的起点,都是拿 ha-c 的结论直接套生产。**
|
||
判据、变体、实验都在 ha-c 上做,只把探针搬到了生产 ——
|
||
探针搬过去了,**输入分布的差异没被当成一等变量**。
|