feat(memory): T1 灰区模型判定 —— D3 入库前审查补上 LLM 那一半
主人 2026-10-07 裁定「可以做 T1」。此前 D3 只有纯规则 T0,凡灰区(相似但 互不包含)一律保守 create,真机上「同一事实换措辞」类重复结构性治不住 (清完 92 条字面级重复后,active 里含「喵喵七号」的仍有 101 条)。 - 插入点:T0 判定内核只多吐一份结构化 Gray(惰性收集,恒零模型调用); T1 挂在异步审查层 Review,同步入库闸不碰它 ⇒ 写入路径零模型调用是 **数据流结构**而不是承诺 - 只许回 merge / modify / keep + 一句理由;坏 JSON、非法枚举、编号越界 一律回落 create(绝不猜、绝不挑最近的一条) - 禁产新内容:模型只给「动作 + 候选编号 + 一句理由」,正文一律取自库里 既有的两行 ⇒ T1 结构性地不可能把幻觉写进记忆库 - 不越硬门与红线:灰区已滤掉跨角色/作用域/项目/只读来源与时效/数值/冲突/ 低置信;「规则否决一票否决」不可走样,T1 不提供翻案入口 - 有界:每次审查至多一次调用(灰区打包,上限 8 条),子超时 8s 且不超过 审查总预算;失败只改 reason、决定恒 create、零 UPDATE - 开关 t1Judge(放 $DATA_DIR/config/memory-tidy.json,零新 env 键),默认开; 模型出口用闭包晚绑定既有模型栈(零新客户端、零新依赖) - 留痕 reason 结构化(t1_merge / t1_modify / t1_keep / t1_reply_invalid / t1_unavailable),rule_version d3.1 → d3.2;模型的自由文本理由一律丢弃 - preinsert_config.go 为纯搬移(为守 500 行上限,无新逻辑)
This commit is contained in:
1 parent
5b8c7a3ef3
commit
1b4d0a552f
11 files changed
+1233
-91
No files matched your search
@@ -28,9 +28,11 @@ import (
|
||||
// 2. **失败即安全**:条目先落库 ⇒ 审查失败/超时/embedder 不可用时「记忆绝不
|
||||
// 丢」是结构性成立的,不靠回滚(§15 D3-5)。失败路径**只记 WARN、绝不
|
||||
// 改写任何一行**;
|
||||
// 3. **零 LLM**:T0 是纯规则(design §15 D3-2 T0),T1 默认关且**本切片未
|
||||
// 实现**(§15.2 开放点 1 = T1 默认关),因此「T1 关闭时零模型调用」是
|
||||
// 结构性成立 —— 本服务的依赖里根本没有模型适配器。
|
||||
// 3. **规则优先、模型兜底**:T0 是纯规则(design §15 D3-2 T0),**恒为零模型
|
||||
// 调用**;T1(灰区调一次轻量模型,§15.1 D3-2)默认**开**,但只在「T0 判不出
|
||||
// 结论且存在灰区候选」时才发出**至多一次**调用(preinsert_t1.go),并且
|
||||
// 要有模型出口才成立 —— 两者任一缺席即是结构性零模型调用。
|
||||
// **同步入库闸(preinsert_gate.go)不碰 T1**,写入路径因此仍是零模型调用。
|
||||
//
|
||||
// 代价(如实登记):极短窗口内库里会多一行待裁定的重复条目,随后被软合并
|
||||
// (status='merged' + merged_into,可 revert,绝不物理删除,design §8)。
|
||||
@@ -55,22 +57,15 @@ const (
|
||||
|
||||
// IngestRuleVersion 是入库前审查规则集的版本号,进每条审查留痕(§15 D3-8)。
|
||||
// **改判定规则必须同时升它**,否则新结论与旧留痕无法区分。
|
||||
const IngestRuleVersion = "d3.1"
|
||||
//
|
||||
// 版本史:
|
||||
// - `d3.1` 四态规则层(T0)+ 同步入库闸;
|
||||
// - `d3.2` 加入 **T1 灰区模型判定**(preinsert_t1.go,design/23 §15.1 D3-2):
|
||||
// 灰区(相似、互不包含、未被规则否决)在模型出口接上时会被改判成
|
||||
// merge/modify,因此 `reason` 上出现了 `t1_*` 族。
|
||||
const IngestRuleVersion = "d3.2"
|
||||
|
||||
// 默认值与审计口径。运算路径上不出现裸魔法数字(AGENTS §10)。
|
||||
const (
|
||||
// defaultReviewQueueSize 是有界审查队列长度:审查是「尽力而为」的旁路,
|
||||
// 队列满时宁可少审一条(记忆早已落库),也绝不阻塞写入方。
|
||||
defaultReviewQueueSize = 256
|
||||
// defaultReviewScanLimit 是每次审查在同一分区内最多比对的既有条目数。
|
||||
defaultReviewScanLimit = 200
|
||||
// defaultReviewTimeout 是单条审查的墙钟上限。
|
||||
defaultReviewTimeout = 20 * time.Second
|
||||
// reviewAuditWho / reviewAuditVerb 是审查留痕的审计口径:它是**系统任务**,
|
||||
// 不是任何角色(design §4.1 C 的同一理由)。
|
||||
reviewAuditWho = "system/memoryreview"
|
||||
reviewAuditVerb = "memory.review.ingest"
|
||||
)
|
||||
// 调优旋钮(ReviewConfig)与它们的默认值常量在 preinsert_config.go。
|
||||
|
||||
var (
|
||||
// ErrReviewDisabled 在审查被配置显式关闭却又被直接调用时返回。
|
||||
@@ -86,44 +81,7 @@ var (
|
||||
ErrReviewItemNotActive = errors.New("memory/review: item is no longer active")
|
||||
)
|
||||
|
||||
// ReviewConfig 携带入库前审查的调优旋钮。零值经 normalize 选取安全默认值。
|
||||
type ReviewConfig struct {
|
||||
// Enabled 是总开关。关闭时组合根不建、不接、不跑 —— 写入路径行为逐字
|
||||
// 回到本次改动之前。
|
||||
Enabled bool
|
||||
// QueueSize 是有界队列长度(≤0 用默认)。
|
||||
QueueSize int
|
||||
// ScanLimit 是单次审查的候选上限(≤0 用默认)。
|
||||
ScanLimit int
|
||||
// Timeout 是单条审查的墙钟上限(≤0 用默认)。
|
||||
Timeout time.Duration
|
||||
// Threshold 是候选召回的余弦门,**复用记忆整理的既有阈值**
|
||||
// (config.MemoryTidy.RecallThreshold,design §13 开放点 2 = 0.80):
|
||||
// 入库前审查与夜间整理必须用同一把尺子,否则同一对条目会有两个结论。
|
||||
// 非正用设计稿默认值。
|
||||
Threshold float64
|
||||
// DisableIngestGate 关闭**同步入库闸**(任务书规格 3 的开关)。
|
||||
//
|
||||
// 它是**反向**字段,且这是刻意的:bool 的零值必须落在「默认行为」上 ——
|
||||
// 同步闸本机制的默认取向(主人 2026-10-07「把记忆整理功能接到昔涟和记忆库
|
||||
// 中间做审查,由整理功能进行入库」),若写成正向的 EnableIngestGate,任何没
|
||||
// 显式配过的地方(含测试与组合根构造的一半配置)都会被读成「关掉」,与默认
|
||||
// 相反。要一键回到「只插不审」(= 本次改动之前的异步行为),写
|
||||
// `$DATA_DIR/config/memory-tidy.json` 的 `{"ingestGate": false}`(**零新
|
||||
// env 键**:`.env` 已冻结,design/13 §2.1)。
|
||||
DisableIngestGate bool
|
||||
// GateBudget 是同步入库闸的**总墙钟预算**(任务书规格 1:写入路径不许变慢)。
|
||||
// 非正用代码默认值 defaultReviewGateBudget。超预算一律 fail-open(照常插入 +
|
||||
// 留痕),绝不静默丢。
|
||||
GateBudget time.Duration
|
||||
// GateScanLimit 是同步入库闸的候选扫描上限。它比异步队列更紧(后者
|
||||
// ScanLimit 默认 200),因为它在**写入路径**上:非正用代码默认值
|
||||
// defaultReviewGateScanLimit,且最终还会被 cfg.ScanLimit 收紧取小。
|
||||
GateScanLimit int
|
||||
}
|
||||
|
||||
// ReviewDeps 是注入给入库前审查的依赖。Store 必需;其余可空并优雅降级。
|
||||
// 刻意**没有**模型适配器:T0 是零 LLM 判定,T1 未实现(§15.2 开放点 1)。
|
||||
type ReviewDeps struct {
|
||||
// Store 是记忆存储。必需。
|
||||
Store *Store
|
||||
@@ -140,6 +98,16 @@ type ReviewDeps struct {
|
||||
// 生产口径(Store.listReviewCandidates,分区过滤写在 SQL 里)。测试用它
|
||||
// 注入慢/坏的候选源,验证「预算有界」与「超时 fail-open」这两条。
|
||||
GateCandidates func(ctx context.Context, p tidyPartition, excludeID string, limit int) ([]MemoryItem, error)
|
||||
// Judge 是 **T1 灰区判定**的一次性模型调用口(design/23 §15.1 D3-2)。
|
||||
//
|
||||
// 为 nil 时 T1 **结构性关闭**(零模型调用)—— 这正是「T1 关闭 ⇒ 零模型调用」
|
||||
// 从承诺变成事实的那条线。它只取模型适配器的 Chat 一面(T1 不流式、不重试、
|
||||
// 不并发),注入的是**既有** model.Adapter 的调用口(组合根装配的就是模型栈
|
||||
// 本身,含按角色路由与主→备→本地降级),不新造任何 LLM 客户端。
|
||||
//
|
||||
// ⚠️ 它**只被异步审查层使用**(PreInsertReviewer.Review);同步入库闸
|
||||
// (preinsert_gate.go)的判定链里根本没有它,写入路径因此零模型调用。
|
||||
Judge T1Judge
|
||||
}
|
||||
|
||||
// IngestReview 是一条入库前审查留痕(design/23 §15 D3-8)。它**不含记忆正文**:
|
||||
@@ -203,29 +171,6 @@ func NewPreInsertReviewer(cfg ReviewConfig, deps ReviewDeps) (*PreInsertReviewer
|
||||
}, nil
|
||||
}
|
||||
|
||||
// normalizeReviewConfig 用设计稿默认值填充零值字段。
|
||||
func normalizeReviewConfig(cfg ReviewConfig) ReviewConfig {
|
||||
if cfg.QueueSize <= 0 {
|
||||
cfg.QueueSize = defaultReviewQueueSize
|
||||
}
|
||||
if cfg.ScanLimit <= 0 {
|
||||
cfg.ScanLimit = defaultReviewScanLimit
|
||||
}
|
||||
if cfg.Timeout <= 0 {
|
||||
cfg.Timeout = defaultReviewTimeout
|
||||
}
|
||||
if cfg.Threshold <= 0 {
|
||||
cfg.Threshold = defaultTidyRecallThreshold
|
||||
}
|
||||
if cfg.GateBudget <= 0 {
|
||||
cfg.GateBudget = defaultReviewGateBudget
|
||||
}
|
||||
if cfg.GateScanLimit <= 0 {
|
||||
cfg.GateScanLimit = defaultReviewGateScanLimit
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// gateEnabled 报告**同步入库闸**是否生效(任务书规格 3 的开关)。
|
||||
// 总开关关闭或闸被显式关掉时它都为 false,此时写入路径逐字回到「只插不审」。
|
||||
func (r *PreInsertReviewer) gateEnabled() bool {
|
||||
@@ -292,6 +237,12 @@ func (r *PreInsertReviewer) Start(ctx context.Context) {
|
||||
"sync_gate", r.gateEnabled(),
|
||||
"gate_budget", r.cfg.GateBudget.String(),
|
||||
"gate_scan_limit", r.gateScanLimit(),
|
||||
// T1 是否有模型出口:`t1_judge=false` 有两种成因(配置关掉 / 没接模型),
|
||||
// 分开写让「以为开着其实没接」在启动日志里就看得见。
|
||||
"t1_judge", r.t1Enabled(),
|
||||
"t1_off_by_config", r.cfg.DisableT1,
|
||||
"t1_timeout", r.t1Timeout().String(),
|
||||
"t1_max_gray", defaultT1MaxGray,
|
||||
"sweep_interval", DefaultIngestReviewGCInterval.String(),
|
||||
"retention", DefaultIngestReviewRetention.String(),
|
||||
"max_reviews", DefaultIngestReviewMaxRows)
|
||||
@@ -409,6 +360,10 @@ func (r *PreInsertReviewer) Review(ctx context.Context, item MemoryItem) (Ingest
|
||||
return IngestReview{}, fmt.Errorf("memory/review: %w", err)
|
||||
}
|
||||
verdict := judgeIngest(item, cands)
|
||||
// T1(灰区模型判定,design/23 §15.1 D3-2)**只挂在异步审查层**:规则层判不出
|
||||
// 结论且有灰区候选时,用至多一次模型调用改判。未启用 / 无出口 / 失败一律
|
||||
// 原样回落 create —— 它没有「改写记忆」的代码路径,因此失败绝不可能丢记忆。
|
||||
verdict = r.maybeJudgeT1(ctx, item, verdict)
|
||||
merged, err := r.applyVerdict(ctx, verdict, item, now)
|
||||
if err != nil {
|
||||
return IngestReview{}, fmt.Errorf("memory/review: apply: %w", err)
|
||||
|
||||
Reference in new issue
Block a user