mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
feat(memory): 媒体 GC 与描述生成两条后台循环
补齐媒体记忆的最后两块:容量上限真正生效,描述文本成为持久语义记忆。
## mediaGCLoop:让容量上限不再形同虚设
CAS 的 GC 只在被显式调用时执行,Put 路径不触发它。此前配置项
core.memory.media.max_mb 注册了却没有任何调用方——一次 see_video 抽 10 帧,
帧本身在工具结果被 Prune 后就没人引用了,若无人清理会一直堆在磁盘上。
现在按 gc_interval(默认 6h)周期调 GC(gc_min_age)。两个不变量:
- 有引用的内容永不删除,即使超容量(宁可超限也不断引用)
- gc_min_age(默认 1h)保护刚 Put 还没来得及 AddRef 的项——它们
refcount 也是 0
## mediaDescribeLoop:描述才是能活过 GC 的那部分
blob 会被容量 GC 淘汰,而描述留在 media 表里,并经 mediaSummaryForEvent
写进 L0 事件、随归档进 L2 文档、经蒸馏进 L3 图库。于是「那张紫蓝红三色
带图」在原始字节早已被清掉之后仍然可被检索到。
复用既有的视觉回退链(resolveModalFallback + chatModalFallbackBatch),
不新造一套模型调用。
三个刻意的决定:
- **走后台而非入库时同步**:视觉模型一次调用生产实测 9.6s。放在对话
路径上会让每张图都给回复加十几秒,而描述的价值是几个月后还能检索到,
不是这一轮——这一轮模型本来就直接看着图。
- **逐条而非批量**:批量拿回来是一整段文字,无法可靠切分回各自的
digest(模型未必按序号输出,也可能把两张图合并成一句)。宁可多几次
往返也要保证「描述 ↔ digest」的对应关系确定。
- **默认关闭**(describe_on_ingest=false):它消耗视觉模型配额。开启后
每 30s 最多处理 4 条,不跟对话抢额度。
失败处理分三类:
- 网络抖动/配额 → 不标记,下轮重试
- 空回复 → 视作失败(上游剥离媒体时通常回空,与 modalfallback 同理)
- 不可描述(kind=other、blob 已丢失)→ 标记 described_by=unsupported/
content-missing,退出队列
## 顺带修掉 Pending 的一个真缺陷
测试写出来才发现:Pending 原先只看 `description = ''`,于是被标记为
described_by=unsupported 但 description 仍空的项**每轮都会被重新取出来
重试**,永久占着 LIMIT 的名额,真正需要描述的新项永远轮不到。
改为同时要求 described_by 也为空。
这是「先写断言再看它是否成立」抓到的——原本我以为标记一下就够了。
## 测试
medialoop_test.go 7 例:两条循环在禁用时立即返回(nil store / 零间隔 /
describe 关闭三种形态,不留空转 goroutine)、GC 清孤儿保留有引用项、
minAge 保护新项、无可用源时不误标记、不可描述大类被标记后退出队列。
media_test.go 补 1 例专测 Pending 的排除逻辑。
全仓 go build / go vet / go test 通过,SDK 冻结 diff = 0。
This commit is contained in:
@ -438,6 +438,13 @@ func (s *Store) Search(query string, kind Kind, limit int) ([]*Item, error) {
|
||||
}
|
||||
|
||||
// Pending 返回尚无描述的媒体,供后台描述任务消费。
|
||||
// Pending 返回尚无描述的媒体,供后台描述任务消费。
|
||||
//
|
||||
// 不只看 description 为空,还要求 described_by 也为空。
|
||||
// 因为“已尝试但无法描述”的项(如 kind=other 的二进制、blob 已丢失)
|
||||
// 会被标记为 described_by=unsupported/content-missing 而 description 仍为空——
|
||||
// 若只看 description,这些项每轮都会被取出来重试,永远卡在队列头部,
|
||||
// 真正需要描述的新项永远轮不到(LIMIT 只取前 N 条)。
|
||||
func (s *Store) Pending(limit int) ([]*Item, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
@ -447,7 +454,8 @@ func (s *Store) Pending(limit int) ([]*Item, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT digest, kind, mime, size, width, height, origin_path, tool,
|
||||
description, described_by, ref_count, first_seen, last_seen
|
||||
FROM media WHERE COALESCE(description,'') = ''
|
||||
FROM media
|
||||
WHERE COALESCE(description,'') = '' AND COALESCE(described_by,'') = ''
|
||||
ORDER BY last_seen DESC LIMIT ?`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -475,3 +475,39 @@ func TestReopen_PersistsAcrossRestart(t *testing.T) {
|
||||
t.Fatalf("内容应持久化: %v / %q", err, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPending_ExcludesAttemptedButUndescribable(t *testing.T) {
|
||||
// 「已尝试但无法描述」的项必须退出待描述队列。
|
||||
//
|
||||
// 这些项被标记为 described_by=unsupported/content-missing 而 description
|
||||
// 仍为空。若 Pending 只看 description,它们每轮都会被取出来重试、
|
||||
// 永久占着 LIMIT 的名额,真正需要描述的新项永远轮不到。
|
||||
s := newTestStore(t, 0)
|
||||
|
||||
fresh, _ := s.Put([]byte("needs-describe"), Item{MIME: "image/png"})
|
||||
unsupported, _ := s.Put([]byte("cannot-describe"), Item{MIME: "application/octet-stream"})
|
||||
described, _ := s.Put([]byte("已描述"), Item{MIME: "image/png"})
|
||||
|
||||
// 标记「尝试过但不支持」:description 空,described_by 非空
|
||||
if err := s.Describe(unsupported, "", "unsupported"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Describe(described, "一张图", "visionllm"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pending, err := s.Pending(10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pending) != 1 {
|
||||
var names []string
|
||||
for _, p := range pending {
|
||||
names = append(names, shortDigest(p.Digest))
|
||||
}
|
||||
t.Fatalf("应只剩 1 条待描述,实际 %d 条: %v", len(pending), names)
|
||||
}
|
||||
if pending[0].Digest != fresh {
|
||||
t.Fatalf("待描述的应是未处理项,实际 %s", shortDigest(pending[0].Digest))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user