Files
Cyrene_v2/internal/runtime/memory/store_vector.go
T
AskaEth fbad02899a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(memory,loop,capacity): 打通记忆链三条空壳(P0-3 / P0-4 / P0-17,审计 §4.3)
- 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
2026-10-04 14:20:22 +08:00

69 lines
2.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}