docs: 架构节改成参考文档写法

上一版还是论证体:四个小标题里三个是否定式断言(「上下文**不是**截断+摘要」
「三层记忆是迁移,**不是**复制」「记忆按场面分,**不按**会话」),
每段都以一句评论收尾。段落字数 62–128,几乎等长 —— 这是最明显的机器痕迹。

改成参考文档:

## 小标题换成名词

   上下文不是「截断 + 摘要」  →  上下文窗口
   三层记忆是迁移,不是复制    →  三层记忆
   记忆按场面分,不按会话      →  场面

标题说明这节讲什么,不下判断。判断该由正文的数据支撑,不该写在标题里。

## 删掉与假想敌的对比

「常见做法是把超窗的对话裁掉或让模型自己摘要,两者的共同问题是丢什么由位置决定」
「普通 agent 通常一条会话一条历史,换个话题就失忆」

换成直接说我们怎么做的。读者不需要先被告知别人怎么错。

## 段落长度拉开

之前 62–128 字(极差 66)。现在每节先给一行文件路径,正文长短不一:

    32 字  `internal/agent/core/context.go`
   127 字  这条约束来自一次实测。50k 窗口……
    51 字  N 默认 10,实际取值受预算约束:……
   129 字  场面由五个特征构成……

极差 98 字。技术文档本来就是「一行路径 + 一段解释 + 一行数字」这种不均匀节奏。

## 去掉两处破折号

「没有『取出即删除』的路径 —— 删除排在确认之后」
「不吸收整轮指纹 —— 吸收了会……」

改成句号。全文破折号归零。

## 补上实现位置

原来只讲设计没给代码位置,改成每节标文件路径:
`internal/agent/core/context.go`、`internal/memory/coldcontext/`、
`internal/memory/scene_emerge.go`、`internal/agent/core/stages.go`。

## 核对

文中数字逐条对源码:`preemptPromotionCap = 2`、`minSceneEvidence = 2`、
`FindColdContexts(72*time.Hour, 2)` 均一致。两版 9 个标题对齐,无死链。
This commit is contained in:
JianFeeeee
2026-10-31 11:42:21 +08:00
parent e542bc1643
commit bd9dd9e75f
2 changed files with 76 additions and 88 deletions

View File

@ -21,7 +21,7 @@ API 密钥在 WebUI(`http://localhost:8080`)里填。需要 Linux。
```text
homed(内核)
LLM 编排 · 三层记忆 · 检索 · 调度
LLM 编排 · 记忆 · 检索 · 调度
│
┌──────────────┼──────────────┐
│ │ │
@ -34,56 +34,51 @@ API 密钥在 WebUI(`http://localhost:8080`)里填。需要 Linux。
WebUI · QQ · 命令行 · 文件 · 浏览器
```
内核不发 HTTP、不读文件。所有接触外部世界的能力都在插件里,插件崩溃不影响内核,
重启退避 1s / 2s / 3s,5 分钟内第 4 次崩溃停止自动重启等人工介入。
内核进程做编排、记忆、检索;HTTP、文件、设备都由插件负责(`internal/plugin`)。
插件崩溃后内核重启它,退避 1s / 2s / 3s;5 分钟内第 4 次崩溃停止自动重启,
等人工介入(`cmd/homed/kmod_runtime.go`)。
### 上下文不是「截断 + 摘要」
### 上下文窗口
常见做法是把超窗的对话裁掉或让模型自己摘要,两者的共同问题是**丢什么由位置决定**。
这里换成了按相关度取舍:用当前输入给每条历史事件打分,低分事件下沉到冷上下文层。
`internal/agent/core/context.go`
打分用稠密向量余弦(与媒体、冷上下文共享同一向量空间),模型未就绪时退化到 TF-IDF。
裁剪时无条件保住最近 N 条,但 N 本身受预算约束:
事件进窗口时算两套向量:稠密向量与媒体、冷上下文共用同一空间;词向量用 TF-IDF
作退路。裁剪时用当前输入给每条历史事件打分,保留 topK,最近 N 条不参与打分直接保留。
```go
// 保护条数不超过预算能装下的条数
budgetCap = ContextTokens / avgEventTokens
pCount = min(配置值, budgetCap)
```
N 默认 10,实际取值受预算约束:`min(配置值, ContextTokens / 单条平均 token)`。
这条约束来自一次实测:50k 窗口下单条事件约 5800 token(工具回灌型),预算约 40000,
「钉住 10 条」本身就装不下,于是每轮裁剪都撞保护下限,两轮后预算耗尽,任务直接终止。
召回率从 44% 掉到 22%。现在窗口越小越自动收紧,1M 窗口下则能用满配置值。
这条约束来自一次实测。50k 窗口、单条事件平均 5800 token、预算 40000,固定保留 10 条
要占 58k,比预算本身还大。于是每轮裁剪都能达标却清不出空间,两轮后预算耗尽、任务终止,
召回率从 44% 降到 22%。现在小窗口会自动收紧,1M 窗口下用满配置值。
被裁掉的事件不是丢弃,是迁移:事件连同它持有的媒体块一起进冷上下文层,块的 ID 不变、
只换持有层。回灌时用 `coldcontext_query` 取回,两阶段(先 Peek 确认、再 Remove),
不用「先删后灌」——那会在回灌失败时永久丢数据。
被裁掉的事件连同它持有的媒体块写入冷上下文层,块 ID 不变,只换持有层。
取回用 `coldcontext_query`:先 `Peek` 确认,再 `Remove` 源副本。
没有「取出即删除」的路径,删除排在确认之后。
### 三层记忆是迁移,不是复制
### 三层记忆
```text
L1 上下文 Context 相关性裁剪,最近 N 条恒受保护
↓ 低分事件下沉(带媒体块)
L2 冷上下文 Cold Context 稠密检索,≤2 次访问且 72h 未访问则继续下沉
↓ 蒸馏成三元组
L3 稳定记忆 Stable 实体关系 + 场面索引,支持按场面召回
```
`internal/agent/core/context.go` · `internal/memory/coldcontext/` · `internal/memory/graph.go`
块在任一层只有一份持有者,不靠引用计数保活。媒体不是附件:图片、音频是图里的一等节点,
有自己的向量,和文本在同一空间里检索。
| 层 | 持有物 | 进模型的方式 | 冷化 |
|---|---|---|---|
| L1 上下文 | 事件 + 块 | 直接进 system prompt | 相关性低分下沉 |
| L2 冷上下文 | 事件 + 块(与 L1 同构) | 召回后插回 L1 时序 | ≤2 次访问且 72h 未读,蒸馏 |
| L3 稳定记忆 | 三元组,节点即块 | 召回后注入 | 不流出 |
### 记忆按场面分,不按会话
一块在任一层只有一个持有者,没有引用计数。图片和音频是 L2/L3 里的块,
有自己的向量,与文本在同一空间检索。
普通 agent 通常一条会话一条历史,换个话题就失忆,或者全塞进一个上下文里互相干扰。
这里按「可观察的场所指纹」分:通道、对话对象、工具、话题、时段,权重 1.0/1.0/0.8/0.4/0.2。
### 场面
同一指纹出现到第 2 次才建场景(`minSceneEvidence = 2`),只发生一次的不算。
归属阈值 0.5、唤起阈值 0.35,唤起比归属宽松——想不起来是损失,多想一条只是多几行上下文。
场景每次重现强度 +1,久不重现按半衰期衰减。
`internal/memory/scene_emerge.go`
两个来源并行:**声明**(插件在注入点说明"这是哪个场面")与**涌现**(指纹自己聚类)。
声明场景不吸收整轮指纹,否则它会以接近 1.0 的相似度吃掉后续所有同类轮次,
被动路再也长不出更细的场面。
场面由五个特征构成,权重各不相同:通道 1.0、对话对象 1.0、工具 0.8、话题 0.4、时段 0.2。
同一指纹出现两次才建场面(`minSceneEvidence = 2`),一次性的交互不建。
归属阈值 0.5,唤起阈值 0.35;重现时强度 +1,久不重现按半衰期衰减。
两个入口并行:插件在注入点声明(如 `chan:qq/peer:group_1`),或者指纹自行聚类。
声明场景只从键本身解析特征,不吸收整轮指纹。吸收之后它会以接近 1.0 的相似度
匹配后续所有同类轮次,自行聚类那条路就长不出更细的场面。
### 七个阶段
@ -99,8 +94,8 @@ L3 稳定记忆 Stable 实体关系 + 场面索引,支持按场面召
| `before_output` | 最终文本就绪 | 格式适配 |
| `after_output` | 已发送 | 统计 |
`post_action → [before_toolcall → 执行 → after_toolcall] → post_action` 是内循环,
退出条件:模型不再调工具、全部被拒、或达到上限。
内循环是 `post_action → [before_toolcall → 执行 → after_toolcall] → post_action`,
模型不再调工具、全部被拒、或达到上限时退出。
## 其余