package memory import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory/vector" ) // 本文件是「持久语义索引的写入侧」:把记忆条目真正喂进向量索引。 // // 背景(审计 P0-3):vec_items 表与 Index.Add 早已实现,但生产代码 // 从未把任何条目写进索引 —— 索引恒空,而检索一见 vindex 非 nil 就 // 只查它,语义检索因此在线上恒为 0 分,还遮住了可用的内存余弦回退。 // 这直接触碰 design/03 §3「选增强则向量检索路径真实生效」、design/04 // §7「配置后必须真实走语义路径(非空壳)」与 AGENTS §9 的红线。 // // 接线方式:装配方(cmd/cyrene/wire_runtime.go)把索引交给检索器 // (memory.WithVectorIndex),NewRetriever 随即把它接到同一个 store 的 // 写入路径(attachVectorIndex),因此「写记忆」与「写向量」不再可能 // 只发生一半,也无需装配方多记一步。 // attachVectorIndex 把持久语义索引接到该 store 的写入路径:此后每条 // 经 Add 落库的条目都会被向量化并 upsert 进索引。nil 索引是空操作 // (保持「未配置索引」的语义)。它可被并发调用,last-writer-wins。 func (s *Store) attachVectorIndex(idx vector.Index) { if s == nil || idx == nil { return } s.vecMu.Lock() defer s.vecMu.Unlock() s.vec = idx } // vectorIndex 返回当前接入的持久语义索引,未接入时为 nil。 func (s *Store) vectorIndex() vector.Index { if s == nil { return nil } s.vecMu.RLock() defer s.vecMu.RUnlock() return s.vec } // indexItem 把已落库的 item 向量化并写入持久索引(写入侧闭环)。 // 未接入索引、索引报错或向量数不符时都只记日志:条目本身已经 // 在关系表里,语义增强的失败绝不使写入失败,检索侧另有内存余弦 // 回退。索引文本取 item.Content —— 与内存语义回退所用的文本一致, // 使「索引作答」与「回退作答」在同一份输入上可比。 func (s *Store) indexItem(ctx context.Context, item MemoryItem) { idx := s.vectorIndex() if idx == nil { return } vecs, err := idx.Embed(ctx, []string{item.Content}) if err != nil { s.log.Warn("memory: vector embed for index failed, item left unindexed", "id", item.ID, "error", err) return } if len(vecs) == 0 { s.log.Warn("memory: vector embed returned no vector, item left unindexed", "id", item.ID) return } if err := idx.Add(ctx, item.ID, vecs[0]); err != nil { s.log.Warn("memory: vector index add failed, item left unindexed", "id", item.ID, "error", err) } }