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

87 lines
3.4 KiB
Go
Raw Permalink 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"
"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
}