mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 17:27:31 +00:00
feat(memory): L2 与 L1 同构 —— 事件列表承载原文,正文内联在块里
块化重构(memory-restructure-plan)换了底层数据结构,但 L1↔L2 的流动没跟着改,
导致这条链出现结构性失效。本次按设计原文修复。
## 断点(生产实测)
**① L2 只存渲染视图,事件本体丢失**
ContextToDoc 把每条事件拍平成一行:truncate(Response,100)、truncate(工具输出,200)。
生产库 1000 篇归档文档实测:
- response 段恰好 103 字符(=100+"..."):311/2094 = 15.1%
- 工具输出段恰好 203 字符:7593/13261
- ToolsUsed 完全不存
⇒ 原文在落库那一刻就不存在了,召回再全也拿不回来。
**② L2 里没有文本块,与 L1 不同构**
L2 块分布 image 437 / audio 7 / text 0 —— 文本全靠 Content 这个视图。
**③ 召回造合成事件,时间戳用归档时刻**
doc_query 用 d.CreatedAt 造一条 cold_storage 事件、正文取 Content 再截 2000 字、
且完全不回填 Blocks。块自带 created_at(捕获时刻),实测与 doc.created_at
差近两小时,却没人用。
**④ 块在 Consume 时两层同时消失**
Consume 从内存与磁盘同时删除文档,而块的字节只存在于那个 .json 里。
生产 L2 有 444 个块,其中 396 个不在 L3(约 248MB)—— 一旦被命中即永久丢失。
## 修法(按设计,不是按我的推断)
设计前提(写进 ARCHITECTURE 与计划文档):
· 记忆块是唯一物理载体,层是持有关系
· L1 与 L2 结构同构,唯一区别是「是否直接投喂给模型」
· 正文(含多媒体正文)内联在块里;事件只是块的容器
· 任何一层都不压缩(视图不是记忆)
· 内核不得硬编码任何插件行为
1. Doc 增加 Events []DocEvent(与 ContextEvent 同构),成为文档主体
2. 正文各自成块:Input 一块、Response 一块(BlockText,Text 即正文)
3. restoreDocEvents 逐条按原始时间戳迁回 L1,块随事件迁回
4. Content 保留为渲染视图(contentFromEvents 生成),不再是原文载体
5. 蒸馏 docToTriples 改用 documentOriginalText:从块读原文,不再用视图
6. 删掉 ARCHITECTURE 里「过滤 agentcli/terminal 来源」——那是内核硬编码
插件名,改由插件经 NoMemory/Cleaner 声明
## 顺带修掉的真实缺陷
simpleHash 是 O(n²) 且哈希串长度正比于内容:
for _, c := range s { h += fmt.Sprintf("%x", c) }
压力测试实测 1.25MB 正文归档耗时 **270 秒**;生产实测 content_hash
是 content 的 2.2 倍(52072 字 → 114491 字)。
换成 sha256 前 16 字节:**270s → 0.52s(520 倍)**,定长 32 hex。
## 存量
按拍板不迁移,等系统自然冲刷。降级路径:无 Events 的旧文档仍按旧行为召回
(整篇渲成一条事件),不伪造成「有原始事件」。生产副本实测 1104 篇旧文档
全部正常加载。
## 判据(16 条,全部有变异自证)
document/blockflow_test.go(11 条):正文完整性、文本块粒度、逐条保留、
视图定位、幂等、存量兼容、原文可用性、hash 线性、3 条压力
core/recalldoc_test.go(5 条):逐条回填、原始时间戳、块迁回、
回填不截断、存量降级、批量压力
变异全部被抓:
· 落库 truncate(100) → ArchivePreservesFullText(实测 len=303/原 10215)
· Input/Response 合成一块 → InputAndResponseAreSeparateTextBlocks(实际 1)
· 召回用 d.CreatedAt → RestoresEachEventAtOriginalTimestamp(回填 1/期望 3)
· 召回不回填 Blocks → MigratesBlocksBackToContext
· 召回再截 2000 字 → TextIsNotTruncatedOnTheWayBack(2003/原 18020)
· 恢复 O(n²) hash → HashIsLinearAndFixedLength(超时 >300s)
## 验证
- go build ./... 通过
- go test ./... -count=1:58 包 0 失败
- go test -race ./internal/memory/document/ ./internal/agent/core/:通过
- 生产数据副本实际启动:1104 篇文档加载正常、0 崩溃、干净退出
- 副本 e2e:新归档 2 事件 / 4 文本块、正文 3207 字完整回读、时间戳精确
## 不改
- 公开 SDK 契约(TextEvent / DocMemoryAPI / InsertWithMedia 已等价)
- 存量文档(等自然冲刷)
- 渲染层截断常数(独立议题,避免与本次修复互相掩盖)
This commit is contained in:
@ -1037,24 +1037,33 @@ func (a *Agent) executeDocTool(tc agentAPI.ToolCall) string {
|
||||
if len(d.Tags) > 0 {
|
||||
parts = append(parts, " 标签: "+strings.Join(d.Tags, ", "))
|
||||
}
|
||||
content := d.Content
|
||||
if len(content) > 2000 {
|
||||
content = content[:2000] + "..."
|
||||
// ★★ 召回 = 把事件(连同它持有的块)迁回 L1,不是造一条新事件
|
||||
//
|
||||
// 设计原文(ARCHITECTURE「记忆流转」② Document 层):
|
||||
//
|
||||
// doc_query → Consume(读取并删除)
|
||||
// → 逐条 context.Append{Timestamp: 原始时间戳, Source: 原来源}
|
||||
//
|
||||
// 此前实现用 d.CreatedAt(归档时刻)造一条 cold_storage 事件、
|
||||
// 正文取 d.Content(渲染视图)再截 2000 字、块完全没回填 ——
|
||||
// 三处都偏离:时间错位、正文二次截断、块两层消失。
|
||||
if n := a.restoreDocEvents(d); n == 0 {
|
||||
// 存量文档(无 Events)降级:整篇渲成一个事件,保持既有行为。
|
||||
// 不伪造成「有原始事件」——那时归档确实只有渲染视图。
|
||||
content := d.Content
|
||||
if labels := a.blockLabelsForDoc(d); labels != "" {
|
||||
content = content + "\n关联媒体: " + labels
|
||||
}
|
||||
a.context.InsertByTimestamp(ContextEvent{
|
||||
Timestamp: d.CreatedAt,
|
||||
Source: "cold_storage",
|
||||
Input: fmt.Sprintf("加载文档记忆: %s", query),
|
||||
Response: content,
|
||||
})
|
||||
}
|
||||
// 媒体块标签单独一行进冷存事件:正文可能被上面的 2000 字截断,
|
||||
// 截掉之后模型就不知道这篇文档带过图。
|
||||
if labels := a.blockLabelsForDoc(d); labels != "" {
|
||||
content = content + "\n关联媒体: " + labels
|
||||
}
|
||||
a.context.InsertByTimestamp(ContextEvent{
|
||||
Timestamp: d.CreatedAt,
|
||||
Source: "cold_storage",
|
||||
Input: fmt.Sprintf("加载文档记忆: %s", query),
|
||||
Response: content,
|
||||
})
|
||||
refs = append(refs, fmt.Sprintf("#%d(%s)", i+1, d.Summary))
|
||||
}
|
||||
return fmt.Sprintf("已加载 %d 篇文档记忆: %s\n(完整内容参见对话时序中 cold_storage 事件)",
|
||||
return fmt.Sprintf("已加载 %d 篇文档记忆: %s\n(正文已按原时间戳回填到对话时序,直接读时序即可)",
|
||||
len(docs), strings.Join(refs, ", "))
|
||||
|
||||
case "doc_commit":
|
||||
@ -1262,3 +1271,56 @@ func (a *Agent) currentFingerprint() string {
|
||||
}
|
||||
return a.multimodalSpace.Fingerprint()
|
||||
}
|
||||
|
||||
// restoreDocEvents 把一篇 L2 文档的原始事件**逐条按原时间戳迁回 L1**。
|
||||
//
|
||||
// 返回回填的事件条数;0 表示这篇文档没有可回填的原始事件(存量文档)。
|
||||
//
|
||||
// ## 依据
|
||||
//
|
||||
// ARCHITECTURE「记忆流转」② Document 层的读取分支:
|
||||
//
|
||||
// doc_query → Consume(读取并删除)
|
||||
// → 逐条 context.Append{Timestamp: 原始时间戳, Source: 原来源}
|
||||
// → 文档以原始时间戳写入 context 时间线, 从 docStore 删除
|
||||
//
|
||||
// L1 与 L2 结构同构,召回是**迁移**:事件连同它持有的块从 L2 换到 L1,
|
||||
// 块 ID 与向量全程不变(不重建、不重算)。
|
||||
//
|
||||
// ## 块为什么要一起回填
|
||||
//
|
||||
// Consume 已把文档从 L2 删掉,而块的字节只存在于那篇文档里。
|
||||
// 不回填块 = 块两层都不在 = 永久丢内容(生产实测这一敞口约 248MB)。
|
||||
func (a *Agent) restoreDocEvents(d *document.Doc) int {
|
||||
if a == nil || a.context == nil || d == nil || !d.HasEvents() {
|
||||
return 0
|
||||
}
|
||||
|
||||
n, blocks := 0, 0
|
||||
for _, ev := range d.Events {
|
||||
input, response, held := ev.InOut()
|
||||
a.context.InsertByTimestamp(ContextEvent{
|
||||
Timestamp: ev.Timestamp, // ★ 原始时间戳:回到事件发生的位置
|
||||
Source: ev.Source,
|
||||
Input: input,
|
||||
Response: response,
|
||||
ToolsUsed: append([]string(nil), ev.ToolsUsed...),
|
||||
ToolResults: func() []ToolResultItem {
|
||||
var out []ToolResultItem
|
||||
for _, tr := range ev.ToolResults {
|
||||
out = append(out, ToolResultItem{Name: tr.Name, Output: tr.Output})
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
Blocks: append([]memory.MemoryBlock(nil), held...),
|
||||
})
|
||||
n++
|
||||
blocks += len(held)
|
||||
}
|
||||
|
||||
log.Printf("[agent] doc→context: %s 逐条回填 %d 条原始事件(%s → %s),块 %d 个迁回 L1",
|
||||
d.ID, n,
|
||||
d.Events[0].Timestamp.Format("01-02 15:04:05"),
|
||||
d.Events[len(d.Events)-1].Timestamp.Format("01-02 15:04:05"), blocks)
|
||||
return n
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user