feat(memory): 记忆 P2(项目索引物化 / decay 回写 / 作用域写规则 / 静默吞点 + loop 级只读红线测试)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
AskaEth committed 2026-10-05 11:47:39 +08:00
1 parent 9b903a89bc
commit 509dc412eb
12 files changed
+660 -55

No files matched your search

+86
View File
@@ -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
}