mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 09:17:31 +00:00
fix(memory): 回填事件必须照常归档 —— 纠正 NoMemory 语义被错误扩大到存储层
用户指出:doc 召回部分需要回填记忆,因为 doc 召回后原文在 doc 层已经删除了。
## 我上一轮的错误(PR #22)
把「工具输出不进记忆」与「回填事件不归档」塞进了同一个 NoMemory 标记:
doc_query → Consume(L2 副本删除)→ 回填 L1(标记 NoMemory)
→ L1 裁掉它 → Prune 跳过归档(错误实现)
→ 三层都不在 ⇒ 内容永久丢失
Consume 已经把 L2 副本删掉,**L1 是唯一副本**。裁掉时不归档 = 丢数据。
这不是自激防护,这是把数据丢掉当成防护。
## 正确区分(两件事不能共用一个标记)
| 对象 | 进计算层 | 进归档 | 理由 |
|---|---|---|---|
| doc_query 的**工具返回值**(正文摘录) | ❌ | ❌ | 它是事件的摘录副本,再向量化 = 重复注入 ⇒ 自激 |
| **回填的事件本身**(原文) | ❌ | ✅ **必须** | L2 副本已删,L1 是唯一副本;不归档即丢失 |
## 修法
ContextEvent.NoMemory 语义定为「**不参与计算,照常归档**」,
与 ToolDef.NoMemory 一致:只影响计算层,不影响存储层。
自激的入口在"重新计算向量"那一侧(textForVector 返回空),
不在"归档"这一侧 —— 删掉 Prune 里的跳过逻辑。
## 判据(改写了那条断言错误行为的判据)
TestSelfExcite_RestoredEventsNotReArchived 现在断言三件事:
1. 回填事件带 NoMemory 标记且未被向量化(不参与计算)
2. 前提成立:L2 里确实已无该事件(Consume 删掉了)
3. ★ 被裁掉后必须能落回 L2(否则内容永久丢失)
变异验证:
- Prune 重新跳过 NoMemory ⇒ 红,报「三层都不在,内容永久丢失」
- restoreDocEvents 不置 NoMemory ⇒ 红(会重复加权)
## 验证
- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
This commit is contained in:
@ -35,18 +35,19 @@ type ContextEvent struct {
|
||||
ToolsUsed []string `json:"tools_used,omitempty"`
|
||||
ToolResults []ToolResultItem `json:"tool_results,omitempty"`
|
||||
|
||||
// NoMemory 标记这条事件**不参与记忆计算与再归档**。
|
||||
// NoMemory 标记这条事件**不参与记忆计算**(不向量化、不提关键词、不蒸馏),
|
||||
// 但**照常参与归档**(离开 L1 时会写回 L2)。
|
||||
//
|
||||
// ★ 为 doc_query 的回填而设(2026-10-22)。回填是把 L2 的历史事件
|
||||
// 按原时间戳放回 L1 —— 它们**已经**在记忆体系里了。若不加标记:
|
||||
// ★ 与 ToolDef.NoMemory 语义一致:只影响计算层,不影响存储层。
|
||||
//
|
||||
// ① 计算层:它们会被重新向量化、参与 Prune 打分 ⇒ 因为与当前输入
|
||||
// 相关而倾向被保留,挤掉真正的新事件;
|
||||
// ② 归档层:Prune 会把它们**再次**归档回 L2 ⇒ 下次 doc_query
|
||||
// 又召回同一批 ⇒ 块在 L1↔L2 之间反复搬迁,自激震荡。
|
||||
// ★★ 为什么不能顺手把归档也关掉(2026-10-22 踩过的坑):
|
||||
//
|
||||
// 语义与 ToolDef.NoMemory / ChannelDef.NoMemory 一致:
|
||||
// **原文保留在上下文里可读**,只是不进计算、不再产生新记忆。
|
||||
// doc_query 的 Consume 已经**从 L2 删除**了原文。若回填事件在 L1
|
||||
// 被裁掉时又不归档,它就**三层都不在**了 —— 内容被永久丢弃。
|
||||
// L1 是它最后的落脚点,必须能落回去。
|
||||
//
|
||||
// 所以"回填事件不再归档"是错的。它确实该重新归档(那才是迁移闭环),
|
||||
// 该抑制的只是"重新计算向量/再次进蒸馏"—— 那才是自激的入口。
|
||||
NoMemory bool `json:"no_memory,omitempty"`
|
||||
// --- 原生多模态记忆 ---
|
||||
// 一等记忆块:块本身随事件在层间迁移,身份不变,不建引用计数。
|
||||
@ -231,7 +232,9 @@ func (c *RelevanceContext) load() {
|
||||
|
||||
func textForVector(evt *ContextEvent, toolDefLookup func(name string) *sdk.ToolDef, channelDefLookup func(name string) (sdk.ChannelDef, bool)) string {
|
||||
// 事件自带 NoMemory(doc_query 回填的历史事件):不进计算层。
|
||||
// 原文仍在上下文里可读,只是不参与向量化/关键词/蒸馏。
|
||||
//
|
||||
// 为什么回填事件不参与计算:它们的内容刚刚在 L2 里被向量化过一次,
|
||||
// 再算一次只会让"同一内容"在向量空间里被重复加权。但它们**照常归档**。
|
||||
if evt.NoMemory {
|
||||
return ""
|
||||
}
|
||||
@ -538,24 +541,14 @@ func (c *RelevanceContext) PruneWithProtected(currentInput string, topK int, doc
|
||||
|
||||
archived := 0
|
||||
if docStore != nil && len(archive) > 0 {
|
||||
// ★ 回填事件(NoMemory)**不再归档回 L2**。
|
||||
// ★ 回填事件(NoMemory)**照常归档** —— 这是数据不丢的关键。
|
||||
//
|
||||
// 这是自激震荡的根:doc_query 把事件从 L2 取到 L1,Prune 又把它们
|
||||
// 归档回 L2 —— 下一轮 doc_query 再召回同一批,块在两层间反复搬迁。
|
||||
// 它们本来就在记忆体系里(来源可能是 L2 或 L3),无需重新落库。
|
||||
filtered := archive[:0]
|
||||
skippedNoMemory := 0
|
||||
for _, s := range archive {
|
||||
if s.event != nil && s.event.NoMemory {
|
||||
skippedNoMemory++
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, s)
|
||||
}
|
||||
archive = filtered
|
||||
if skippedNoMemory > 0 {
|
||||
log.Printf("[agent] prune: 跳过 %d 条回填事件(NoMemory,它们在记忆体系里已有归属)", skippedNoMemory)
|
||||
}
|
||||
// 曾经在这里跳过它们(理由是"防自激"),那是错的:
|
||||
// doc_query 的 Consume 已经把 L2 副本删掉了,L1 是唯一副本。
|
||||
// 跳过归档 = 裁掉即丢失(L1、L2、L3 三层都不在)。
|
||||
//
|
||||
// 自激的入口不在"归档"这一侧,而在"重新计算向量"那一侧 ——
|
||||
// 那已经由 textForVector 里 `if evt.NoMemory { return "" }` 挡住。
|
||||
}
|
||||
if docStore != nil && len(archive) > 0 {
|
||||
entries := make([]document.ContextEntry, len(archive))
|
||||
|
||||
Reference in New Issue
Block a user