- P0-3 持久向量索引**不再空壳**:Store.Add 落库后真做 Embed+Add(失败只记日志、绝不让记忆写入失败); NewRetriever 拿到 WithVectorIndex 时**自动接上写入路径**(无需改组合根);索引无命中 → WARN + **回退内存余弦**; 新增每进程一次的**回填**(索引确实为空时把已落库条目向量化,分批 64、上限 512) - P0-4 工作记忆**按会话接线**:回合落定时写入(owner 会话 + 带会话 id 的窗口,LRU 上限 64), 组装上下文时按会话读取 ⇒ 前导块真的出现;原单窗口 API 语义不变 - P0-17 L4 学习回环**真回写**:新增 capacity.Judge(只跑判定、不施人格覆盖/不发事件/不改状态); loop 在回合落定"只写 owner 会话 + 跳过子代理分支",写 ScopeUser(L4 的读域)+ 结构化溯源 - 顺带红线加固:意图记录是主人**原话**的机器可读形态 ⇒ **绝不注入群聊/陌生人块**(design/03 §5) - 复核:三条各自的包 build/vet/test 绿;「真生效」断言含生产构造路径(vector.New + 真实 SQLite 后端 + n-gram embedder)、跑完一轮窗口非空、L4 回写后能唤醒一个自身无强信号的表达; **5 条变异反证**(其中两条专门打「受限会话零写入」与「群聊不泄露主人原话」)均已还原 - 未做(登记):Index.Search 无 ID 过滤(客户端按候选集过滤,极端情况走回退);每条记忆一次 Embed
69 lines
2.6 KiB
Go
69 lines
2.6 KiB
Go
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)
|
||
}
|
||
}
|