package memory import ( "context" ) // 本文件是「持久语义索引的一次性回填」:让既有条目也进入语义路径。 // // 写入侧(store_vector.go)保证**此后**每条记忆都被写进索引,但进程 // 启动前就已落库的 memory_items 没有对应向量。若不回填,语义路径 // 只覆盖新写入的少数条目,旧条目在索引里查不到 —— 对「配置了增强 // 就必须真生效」(AGENTS §9)而言仍是半个空壳。 // // 回填触发条件极窄:每进程首次需要语义打分时探测一次索引,**只在索 // 引确实为空**(一条向量都没有)时回填全部条目,之后永不重复。因此 // 它不会在正常运行的每次检索上花代价,也不会重复嵌入已入索引的向量。 // indexProbeText 是探测「索引是否为空」用的固定文本:它的向量内容 // 无关紧要(只想知道索引里有没有任何向量),取常量以保证确定性。 const indexProbeText = "probe" // indexBackfillBatch 是单次 Embed 调用喂给索引的条数上限,避免把 // 整库文本一次性交给远端 embedding API。 const indexBackfillBatch = 64 // maxBackfillItems 是单次回填处理的最大条目数。它给「一次启动就把 // 十万条历史记忆全量向量化」这种代价封顶;超出部分只记日志,不 // 阻塞检索(Store.List 最新在前,因此被截掉的是最旧的一批)。 const maxBackfillItems = 512 // ensureIndexSeeded 每进程最多执行一次:探测持久索引是否为空,为空 // 则把已落库的条目回填进去。未接入索引时是空操作;探测或嵌入失败 // 只记日志且不再重试(重试只会把同一条故障日志刷满每一次检索)。 func (r *Retriever) ensureIndexSeeded(ctx context.Context) { if r.vindex == nil { return } r.seedMu.Lock() defer r.seedMu.Unlock() if r.seeded { return } r.seeded = true probe, err := r.vindex.Embed(ctx, []string{indexProbeText}) if err != nil || len(probe) == 0 { r.log.Warn("memory: vector index probe failed, semantic index not seeded", "error", err) return } hits, err := r.vindex.Search(ctx, probe[0], 1) if err != nil { r.log.Warn("memory: vector index probe search failed, semantic index not seeded", "error", err) return } if len(hits) > 0 { // 索引里已有向量:它由写入侧或上一次运行填过,无需回填。 return } r.backfillIndex(ctx) } // backfillIndex 把已落库的条目向量化并写入索引。它分批 Embed,逐条 // Add;单条失败只记日志(其余条目继续),因此一次失败的 Add 不会让 // 整个回填白跑。按 maxBackfillItems 截断时记一条日志说明被跳过 // 的量,避免「配置了增强却只覆盖一部分」无人知晓。 func (r *Retriever) backfillIndex(ctx context.Context) { items, err := r.store.List(ctx) if err != nil { r.log.Warn("memory: list items for vector backfill failed", "error", err) return } if len(items) == 0 { return } total := len(items) if total > maxBackfillItems { r.log.Warn("memory: vector backfill truncated, oldest items left unindexed", "total", total, "limit", maxBackfillItems) items = items[:maxBackfillItems] } indexed, failed := 0, 0 for start := 0; start < len(items); start += indexBackfillBatch { end := min(start+indexBackfillBatch, len(items)) batch := items[start:end] texts := make([]string, len(batch)) for i, it := range batch { texts[i] = it.Content } vecs, err := r.vindex.Embed(ctx, texts) if err != nil { r.log.Warn("memory: vector backfill embed failed, seeding stopped", "indexed", indexed, "error", err) return } for i, v := range vecs { if i >= len(batch) { break } if err := r.vindex.Add(ctx, batch[i].ID, v); err != nil { failed++ r.log.Warn("memory: vector backfill add failed", "id", batch[i].ID, "error", err) continue } indexed++ } } r.log.Info("memory: vector index backfilled from persisted items", "indexed", indexed, "failed", failed, "total", total) }