feat(memory): 记忆 P2(项目索引物化 / decay 回写 / 作用域写规则 / 静默吞点 + loop 级只读红线测试)
This commit is contained in:
1 parent
9b903a89bc
commit
509dc412eb
12 files changed
+660
-55
No files matched your search
@@ -0,0 +1,86 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// decayWritebackKey 是「是否允许把检索命中的衰减值物化回写」的 context 键。
|
||||
// 默认允许;只读上下文(群聊 / 陌生人,design/03 §5「能查不能写」)经
|
||||
// WithoutDecayWriteback 关掉它,使受限检索对记忆库零写入。
|
||||
type decayWritebackKey struct{}
|
||||
|
||||
// WithoutDecayWriteback 返回一个禁用衰减物化回写的 context:受限(只读)
|
||||
// 检索必须用它,避免在「查记忆只读不写」的群聊 / 陌生人上下文里改动记忆行。
|
||||
func WithoutDecayWriteback(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, decayWritebackKey{}, false)
|
||||
}
|
||||
|
||||
// decayWritebackEnabled 报告 ctx 是否允许衰减物化回写(未显式关闭时为 true)。
|
||||
func decayWritebackEnabled(ctx context.Context) bool {
|
||||
v, ok := ctx.Value(decayWritebackKey{}).(bool)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// decayRow 是一条待物化的衰减记录:命中条目 id 与其当前衰减值。
|
||||
type decayRow struct {
|
||||
id string
|
||||
decay float64
|
||||
}
|
||||
|
||||
// persistDecay 把本轮检索**真正命中**(relevance > 0)的条目的衰减值
|
||||
// 物化回写 decay_weight,并把它们的 updated_at 推进到 now。它是 design/03
|
||||
// §2 与裁决 Q3(2026-10-04)的落地:「updated_at 即衰减源 —— 读时按它计算
|
||||
// 衰减;检索命中时把结果物化回写 decay_weight」「decay_weight 列语义=
|
||||
// 上次物化的衰减值」。一次命中即一次「使用」,因此同时刷新 updated_at,使
|
||||
// 下一次读取从同一基准继续衰减,而不会把已物化的值再乘一遍(避免双重衰减)。
|
||||
//
|
||||
// query 为空时的纯新近度排序不是命中,故不物化;只读上下文
|
||||
// (WithoutDecayWriteback)也不物化,以守住「群聊/陌生人不写记忆」。
|
||||
//
|
||||
// 物化失败绝不使检索失败:decay_weight 只是排序/展示用的派生列(design §2
|
||||
// 「不作为真值源」),失败只记一条带来源的告警。
|
||||
func (r *Retriever) persistDecay(ctx context.Context, results []Result, relevance map[string]float64, now time.Time) {
|
||||
if !decayWritebackEnabled(ctx) {
|
||||
return
|
||||
}
|
||||
rows := make([]decayRow, 0, len(results))
|
||||
for _, res := range results {
|
||||
if relevance[res.Item.ID] <= 0 {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, decayRow{id: res.Item.ID, decay: r.decayFactor(res.Item, now)})
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return
|
||||
}
|
||||
if err := r.store.writeDecay(ctx, rows, now); err != nil {
|
||||
r.log.Warn("memory: decay weight writeback failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// writeDecay 在单个事务里把每条命中的 decay_weight 更新为其物化值,并把
|
||||
// updated_at 推进到 at(命中即使用)。批量、参数化的 UPDATE,绝不拼接 SQL;
|
||||
// 任一条失败即整批回滚,保持「要么都物化、要么都不动」。
|
||||
func (s *Store) writeDecay(ctx context.Context, rows []decayRow, at time.Time) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("memory: begin decay writeback tx: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
for _, row := range rows {
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`UPDATE memory_items SET decay_weight = ?, updated_at = ? WHERE id = ?`,
|
||||
row.decay, formatTime(at), row.id); err != nil {
|
||||
return fmt.Errorf("memory: update decay weight %q: %w", row.id, err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("memory: commit decay writeback tx: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user