mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 17:27:31 +00:00
docs: 架构节写清上下文管理与记忆的实际做法
## 补上真正的差异点 之前架构节只有一张图和七阶段表,把最核心的上下文管理省略了。 用 codegraph 查了 `RelevanceContext` / `PruneWithProtected` / `coldcontext.Store` / `block_recall`,确认实际做法,逐条对照常见 agent: ### 上下文:按相关度取舍,不是截断或摘要 常见做法是超窗裁掉旧对话,或让模型自己摘要 —— 共同问题是**丢什么由位置决定**。 这里是给每条历史事件按当前输入打分,低分下沉到文档层。 打分主路径是稠密余弦(与媒体、文档同一向量空间),模型未就绪退到 TF-IDF。 关键在 `effectiveProtectedCount`:保护条数受预算约束。 ```go budgetCap = ContextTokens / avgEventTokens pCount = min(配置值, budgetCap) ``` 源码里记着这条约束的来历:50k 窗口下单条事件约 5800 token(工具回灌型), 预算 40000,「钉住 10 条」本身就装不下 —— 每轮裁剪都撞保护下限, 两轮后预算耗尽,任务直接终止,召回率从 44% 掉到 22%。 ### 三层是迁移,不是复制 `ColdDoc` 的注释写得很清楚:「块的真实归属在 Events[i].Blocks 里」, 块在任一层只有一个持有者,不靠引用计数保活。 回灌用 `Peek` + 确认 + `Remove` 两阶段,源码明确标注「**禁止先删后灌** —— 删除必须排在确认之后,回灌失败即永久丢数据」。 ### 场面:两条路并行,且互不吞并 声明场景**不吸收整轮指纹**(`EnsureScene` 的注释):否则它会以接近 1.0 的相似度 吃掉后续所有同类轮次,被动聚类再也长不出更细的场面。 这条边界是踩出来的,不是设计时想到的。 ## 开头相应收缩 原来开头四段「**粗体断言** + 三行解释」与架构节重复。改成一句介绍 + 安装 + 三段话点出差异,细节留给架构节。 两版 163 / 191 行,9 个标题对齐,代码块 10/10 配对,无死链。
This commit is contained in:
76
README.md
76
README.md
@ -13,22 +13,9 @@ make build build-cli
|
|||||||
|
|
||||||
API 密钥在 WebUI(`http://localhost:8080`)里填。需要 Linux。
|
API 密钥在 WebUI(`http://localhost:8080`)里填。需要 Linux。
|
||||||
|
|
||||||
## 几个设计
|
跟常见 agent 框架的主要差别在上下文管理:不是超窗就截断或让模型摘要,
|
||||||
|
而是按相关度取舍,低分的下沉成文档、再蒸馏进图数据库。另外两处不太一样的是
|
||||||
**启动顺序在编译期定序。** 20 个内核模块各自声明 `Requires`/`Provides`,构建时算出 6 层
|
启动顺序在编译期定序,以及插件按设施就绪提前启动(详见下方架构)。
|
||||||
依赖图写进 `depgraph.bin`,digest 用 `-ldflags` 注入二进制。运行期只读图,不重算。
|
|
||||||
图与二进制不配套则拒绝启动。
|
|
||||||
|
|
||||||
**插件按设施就绪提前启动。** 声明 `requires_facilities: [config]` 的插件在第 1 层完成时就起,
|
|
||||||
不必等第 4 层 agent。实测省 35 秒,那是 agent 层做 dense backfill 750 条事件、重建索引的时间。
|
|
||||||
|
|
||||||
**两级输入、四级中断。** 排队输入无级别,任何中断可打断。中断带 L1–L4,高抢占低并将现场
|
|
||||||
压栈待恢复,同级不抢占。L4 内核独占,插件声明会被夹到 L3。抢占计数可提级,上限两档
|
|
||||||
(`preemptPromotionCap = 2`),避免低优先级任务长期得不到执行。
|
|
||||||
|
|
||||||
**记忆按场面分。** 指纹由通道、对话对象、工具、话题、时段构成,权重 1.0/1.0/0.8/0.4/0.2。
|
|
||||||
同一指纹出现到第 2 次才建场景(`minSceneEvidence = 2`)。归属阈值 0.5、唤起阈值 0.35,
|
|
||||||
唤起比归属宽松:想不起来是损失,多想一条只是多几行上下文。
|
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
@ -39,7 +26,7 @@ API 密钥在 WebUI(`http://localhost:8080`)里填。需要 Linux。
|
|||||||
┌──────────────┼──────────────┐
|
┌──────────────┼──────────────┐
|
||||||
│ │ │
|
│ │ │
|
||||||
stdio RPC 共享内存 事件环
|
stdio RPC 共享内存 事件环
|
||||||
控制 大块数据 通知
|
控制 大块数据 通知
|
||||||
│ │ │
|
│ │ │
|
||||||
└──────────────┼──────────────┘
|
└──────────────┼──────────────┘
|
||||||
│
|
│
|
||||||
@ -50,7 +37,57 @@ API 密钥在 WebUI(`http://localhost:8080`)里填。需要 Linux。
|
|||||||
内核不发 HTTP、不读文件。所有接触外部世界的能力都在插件里,插件崩溃不影响内核,
|
内核不发 HTTP、不读文件。所有接触外部世界的能力都在插件里,插件崩溃不影响内核,
|
||||||
重启退避 1s / 2s / 3s,5 分钟内第 4 次崩溃停止自动重启等人工介入。
|
重启退避 1s / 2s / 3s,5 分钟内第 4 次崩溃停止自动重启等人工介入。
|
||||||
|
|
||||||
**消息进入后的七个阶段**(`internal/agent/core/stages.go`):
|
### 上下文不是「截断 + 摘要」
|
||||||
|
|
||||||
|
常见做法是把超窗的对话裁掉或让模型自己摘要,两者的共同问题是**丢什么由位置决定**。
|
||||||
|
这里换成了按相关度取舍:用当前输入给每条历史事件打分,低分的下沉到文档层。
|
||||||
|
|
||||||
|
打分用稠密向量余弦(与媒体、文档共享同一向量空间),模型未就绪时退化到 TF-IDF。
|
||||||
|
裁剪时无条件保住最近 N 条,但 N 本身受预算约束:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 保护条数不超过预算能装下的条数
|
||||||
|
budgetCap = ContextTokens / avgEventTokens
|
||||||
|
pCount = min(配置值, budgetCap)
|
||||||
|
```
|
||||||
|
|
||||||
|
这条约束来自一次实测:50k 窗口下单条事件约 5800 token(工具回灌型),预算约 40000,
|
||||||
|
「钉住 10 条」本身就装不下,于是每轮裁剪都撞保护下限,两轮后预算耗尽,任务直接终止。
|
||||||
|
召回率从 44% 掉到 22%。现在窗口越小越自动收紧,1M 窗口下则能用满配置值。
|
||||||
|
|
||||||
|
被裁掉的事件不是丢弃,是迁移:事件连同它持有的媒体块一起进文档层,块的 ID 不变、
|
||||||
|
只换持有层。回灌时用 `coldcontext_query` 取回,两阶段(先 Peek 确认、再 Remove),
|
||||||
|
不用「先删后灌」——那会在回灌失败时永久丢数据。
|
||||||
|
|
||||||
|
### 三层记忆是迁移,不是复制
|
||||||
|
|
||||||
|
```
|
||||||
|
L0 上下文 相关性裁剪,最近 N 条恒受保护
|
||||||
|
↓ 低分事件下沉(带媒体块)
|
||||||
|
L1 文档 稠密检索,访问计数 ≤2 且久未访问则继续下沉
|
||||||
|
↓ 蒸馏成三元组
|
||||||
|
L2 图 实体关系 + 场面索引,支持按场面召回
|
||||||
|
```
|
||||||
|
|
||||||
|
块在任一层只有一份持有者,不靠引用计数保活。媒体不是附件:图片、音频是图里的一等节点,
|
||||||
|
有自己的向量,和文本在同一空间里检索。
|
||||||
|
|
||||||
|
### 记忆按场面分,不按会话
|
||||||
|
|
||||||
|
普通 agent 通常一条会话一条历史,换个话题就失忆,或者全塞进一个上下文里互相干扰。
|
||||||
|
这里按「可观察的场所指纹」分:通道、对话对象、工具、话题、时段,权重 1.0/1.0/0.8/0.4/0.2。
|
||||||
|
|
||||||
|
同一指纹出现到第 2 次才建场景(`minSceneEvidence = 2`),只发生一次的不算。
|
||||||
|
归属阈值 0.5、唤起阈值 0.35,唤起比归属宽松——想不起来是损失,多想一条只是多几行上下文。
|
||||||
|
场景每次重现强度 +1,久不重现按半衰期衰减。
|
||||||
|
|
||||||
|
两个来源并行:**声明**(插件在注入点说明"这是哪个场面")与**涌现**(指纹自己聚类)。
|
||||||
|
声明场景不吸收整轮指纹,否则它会以接近 1.0 的相似度吃掉后续所有同类轮次,
|
||||||
|
被动路再也长不出更细的场面。
|
||||||
|
|
||||||
|
### 七个阶段
|
||||||
|
|
||||||
|
`internal/agent/core/stages.go`
|
||||||
|
|
||||||
| 阶段 | 时机 | 插件可做 |
|
| 阶段 | 时机 | 插件可做 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@ -65,9 +102,6 @@ API 密钥在 WebUI(`http://localhost:8080`)里填。需要 Linux。
|
|||||||
`post_action → [before_toolcall → 执行 → after_toolcall] → post_action` 是内循环,
|
`post_action → [before_toolcall → 执行 → after_toolcall] → post_action` 是内循环,
|
||||||
退出条件:模型不再调工具、全部被拒、或达到上限。
|
退出条件:模型不再调工具、全部被拒、或达到上限。
|
||||||
|
|
||||||
**三层记忆**:上下文(最近 10 条恒受保护)→ 文档(冷数据下沉)→ 图数据库(蒸馏成三元组)。
|
|
||||||
媒体是图里的一等节点,不是附件。
|
|
||||||
|
|
||||||
## 其余
|
## 其余
|
||||||
|
|
||||||
- **通道**:WebUI、命令行、QQ 群与私聊、A2A、IDE 里的 ACP
|
- **通道**:WebUI、命令行、QQ 群与私聊、A2A、IDE 里的 ACP
|
||||||
|
|||||||
98
README_EN.md
98
README_EN.md
@ -13,28 +13,10 @@ make build build-cli
|
|||||||
|
|
||||||
API keys go in the web UI at `http://localhost:8080`. Requires Linux.
|
API keys go in the web UI at `http://localhost:8080`. Requires Linux.
|
||||||
|
|
||||||
## Design notes
|
The main difference from typical agent frameworks is context handling: instead of truncating
|
||||||
|
on overflow or asking the model to summarize, it scores history by relevance and sinks the
|
||||||
**Startup order is resolved at build time.** Twenty kernel modules declare `Requires`/`Provides`.
|
low-scoring events into documents, then distills those into a graph. Two other things differ:
|
||||||
The build computes a 6-layer graph into `depgraph.bin` and injects its digest into the binary via
|
startup order is resolved at build time, and plugins launch as soon as their facilities are ready.
|
||||||
`-ldflags`. At runtime the kernel reads the graph rather than recomputing it. A mismatch between
|
|
||||||
graph and binary refuses startup.
|
|
||||||
|
|
||||||
**Plugins launch as soon as their facilities are ready.** A plugin declaring
|
|
||||||
`requires_facilities: [config]` starts when layer 1 completes instead of waiting for the agent
|
|
||||||
layer at layer 4. That saves a measured 35 seconds, which the agent layer spends on dense
|
|
||||||
backfill of 750 events and reindexing.
|
|
||||||
|
|
||||||
**Two input classes, four interrupt levels.** Queued input has no level, so any interrupt can
|
|
||||||
preempt it. Interrupts carry L1–L4: higher preempts lower and pushes the preempted state onto a
|
|
||||||
stack for later resume, same level never preempts. L4 is kernel-only; plugins declaring it get
|
|
||||||
clamped to L3. Preemption count can raise a level by at most two steps
|
|
||||||
(`preemptPromotionCap = 2`), so low-priority work can't starve indefinitely.
|
|
||||||
|
|
||||||
**Memory is partitioned by scene.** A fingerprint is built from channel, partner, tools, topic,
|
|
||||||
and time of day, weighted 1.0/1.0/0.8/0.4/0.2. A scene exists only after the second matching
|
|
||||||
fingerprint (`minSceneEvidence = 2`). Join threshold 0.5, recall 0.35 — recall is looser because
|
|
||||||
failing to recall loses information while an extra scene costs a few lines of context.
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@ -53,11 +35,70 @@ failing to recall loses information while an extra scene costs a few lines of co
|
|||||||
web UI · QQ · CLI · files · browser
|
web UI · QQ · CLI · files · browser
|
||||||
```
|
```
|
||||||
|
|
||||||
The kernel makes no HTTP calls and reads no files. Everything that touches the outside world is a
|
The kernel makes no HTTP calls and reads no files. Everything touching the outside world is a
|
||||||
plugin. A plugin crash leaves the kernel running; restart backs off 1s / 2s / 3s, and the 4th
|
plugin. A plugin crash leaves the kernel running; restarts back off 1s / 2s / 3s, and the 4th
|
||||||
crash within 5 minutes stops automatic restarts for human intervention.
|
crash within 5 minutes stops automatic restarting for human intervention.
|
||||||
|
|
||||||
**Seven stages after a message arrives** (`internal/agent/core/stages.go`):
|
### Context is not "truncate and summarize"
|
||||||
|
|
||||||
|
The usual approach drops old turns or asks the model to summarize them. Both share a flaw: what
|
||||||
|
gets lost is decided by position. Here each historical event is scored against the current input,
|
||||||
|
and low scorers sink into the document layer.
|
||||||
|
|
||||||
|
Scoring uses dense cosine similarity in the same vector space as media and documents, falling
|
||||||
|
back to TF-IDF when the model isn't loaded. The most recent N events are always kept, but N is
|
||||||
|
itself bounded by budget:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// protected count cannot exceed what the budget can hold
|
||||||
|
budgetCap = ContextTokens / avgEventTokens
|
||||||
|
pCount = min(configured, budgetCap)
|
||||||
|
```
|
||||||
|
|
||||||
|
That bound came from a measurement: with a 50k window, one event averaged 5800 tokens (tool
|
||||||
|
results), while the budget was 40000, so "pin 10 events" didn't fit. Every prune hit the
|
||||||
|
protection floor, the budget was exhausted after two rounds, and the task terminated. Recall
|
||||||
|
dropped from 44% to 22%. Smaller windows now tighten automatically, and a 1M window can use the
|
||||||
|
full configured value.
|
||||||
|
|
||||||
|
Pruned events aren't discarded, they migrate: the event and the media blocks it holds move into
|
||||||
|
the document layer with block IDs unchanged, only the holding layer changes. Retrieval uses
|
||||||
|
`coldcontext_query` in two phases (Peek to confirm, then Remove) rather than delete-then-refill,
|
||||||
|
which would lose data permanently if the refill failed.
|
||||||
|
|
||||||
|
### Three layers migrate, they don't copy
|
||||||
|
|
||||||
|
```
|
||||||
|
L0 context relevance pruning, last N always protected
|
||||||
|
↓ low scorers sink (with media blocks)
|
||||||
|
L1 documents dense retrieval; access count ≤2 and stale means sink further
|
||||||
|
↓ distilled into triples
|
||||||
|
L2 graph entities, relations, scene index; recall by scene
|
||||||
|
```
|
||||||
|
|
||||||
|
A block has exactly one holder at any layer, with no reference counting to keep it alive. Media
|
||||||
|
are not attachments: images and audio are first-class graph nodes with their own vectors,
|
||||||
|
searchable in the same space as text.
|
||||||
|
|
||||||
|
### Memory is partitioned by scene, not by session
|
||||||
|
|
||||||
|
Typical agents keep one history per session, so switching topics loses context or crams
|
||||||
|
everything into one window. Here memory is partitioned by observable scene fingerprint: channel,
|
||||||
|
conversation partner, tools, topic, time of day, weighted 1.0/1.0/0.8/0.4/0.2.
|
||||||
|
|
||||||
|
A scene comes into being only on the second matching fingerprint (`minSceneEvidence = 2`).
|
||||||
|
Join threshold 0.5, recall 0.35 — recall is looser because failing to recall loses information
|
||||||
|
while an extra scene costs a few lines of context. Each recurrence adds to scene strength;
|
||||||
|
scenes that stop recurring decay by half-life.
|
||||||
|
|
||||||
|
Two paths run in parallel: **declared** (a plugin states at the injection point which scene this
|
||||||
|
is) and **emergent** (fingerprints cluster on their own). Declared scenes deliberately don't
|
||||||
|
absorb the full fingerprint, otherwise they'd score near 1.0 against every later turn and the
|
||||||
|
emergent path could never grow finer-grained scenes.
|
||||||
|
|
||||||
|
### Seven stages
|
||||||
|
|
||||||
|
`internal/agent/core/stages.go`
|
||||||
|
|
||||||
| Stage | When | Plugins can |
|
| Stage | When | Plugins can |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@ -67,14 +108,11 @@ crash within 5 minutes stops automatic restarts for human intervention.
|
|||||||
| `before_toolcall` | before one tool runs | audit, deny, rewrite args |
|
| `before_toolcall` | before one tool runs | audit, deny, rewrite args |
|
||||||
| `after_toolcall` | after one tool runs | redact, sort results |
|
| `after_toolcall` | after one tool runs | redact, sort results |
|
||||||
| `before_output` | final text ready | format adaptation |
|
| `before_output` | final text ready | format adaptation |
|
||||||
| `after_output` | already sent | statistics |
|
| `after_output` | after sending | statistics |
|
||||||
|
|
||||||
`post_action → [before_toolcall → run → after_toolcall] → post_action` is the inner loop. It exits
|
`post_action → [before_toolcall → run → after_toolcall] → post_action` is the inner loop. It exits
|
||||||
when the model stops calling tools, all calls are denied, or the limit is reached.
|
when the model stops calling tools, all calls are denied, or the limit is reached.
|
||||||
|
|
||||||
**Three memory layers**: context (last 10 always protected) → documents (cold data sinks) → graph
|
|
||||||
database (distilled into triples). Media are first-class graph nodes, not attachments.
|
|
||||||
|
|
||||||
## Everything else
|
## Everything else
|
||||||
|
|
||||||
- **Channels**: web UI, CLI, QQ groups and DMs, A2A, ACP in your IDE
|
- **Channels**: web UI, CLI, QQ groups and DMs, A2A, ACP in your IDE
|
||||||
|
|||||||
Reference in New Issue
Block a user