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:
JianFeeeee
2026-10-31 11:30:18 +08:00
parent 5f570075a3
commit 152dc680ad
2 changed files with 123 additions and 51 deletions

View File

@ -13,28 +13,10 @@ make build build-cli
API keys go in the web UI at `http://localhost:8080`. Requires Linux.
## Design notes
**Startup order is resolved at build time.** Twenty kernel modules declare `Requires`/`Provides`.
The build computes a 6-layer graph into `depgraph.bin` and injects its digest into the binary via
`-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.
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
low-scoring events into documents, then distills those into a graph. Two other things differ:
startup order is resolved at build time, and plugins launch as soon as their facilities are ready.
## 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
```
The kernel makes no HTTP calls and reads no files. Everything that touches the outside world is a
plugin. A plugin crash leaves the kernel running; restart backs off 1s / 2s / 3s, and the 4th
crash within 5 minutes stops automatic restarts for human intervention.
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; restarts back off 1s / 2s / 3s, and the 4th
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 |
|---|---|---|
@ -67,14 +108,11 @@ crash within 5 minutes stops automatic restarts for human intervention.
| `before_toolcall` | before one tool runs | audit, deny, rewrite args |
| `after_toolcall` | after one tool runs | redact, sort results |
| `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
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
- **Channels**: web UI, CLI, QQ groups and DMs, A2A, ACP in your IDE