主人 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 行上限,无新逻辑)
82 lines
3.3 KiB
Go
82 lines
3.3 KiB
Go
package memory
|
||
|
||
import "fmt"
|
||
|
||
// 本文件是入库前审查(design/23 §15.1 D3)的**观测面**:进程内计数与审计出口。
|
||
// 它从 preinsert.go 拆出来只为守 AGENTS §10 的文件行数上限(≤500 行)与
|
||
// 「模块按职责拆源文件」那条规范 —— 口径逐字未变。
|
||
//
|
||
// 为什么要**分列**闸与异步队列的计数:同步入库闸(写路径上当场拍板)与异步
|
||
// 审查队列(后台补审)是两件事,把它们加成一个数会让「闸拦下了多少重复」与
|
||
// 「事后补审了多少」不可分辨。分列之后,面板/日志能分别读到两条路径的量。
|
||
|
||
// ReviewStats 是审查的观测计数(进程内累计,供日志/测试断言)。
|
||
type ReviewStats struct {
|
||
Reviewed int
|
||
Skipped int
|
||
Merged int
|
||
Modified int
|
||
Created int
|
||
Dropped int
|
||
Failed int
|
||
// GateChecked 是**同步入库闸**实际做了一次判定的条数(跳过闸的条目不计)。
|
||
GateChecked int
|
||
// GateAbsorbed 是闸当场拦下并落成 `merged` 的条数(真重复,含 modify 的
|
||
// 「旧行被吸收」)。
|
||
GateAbsorbed int
|
||
// GateFailed 是闸因预算/候选源不可用而 fail-open 的次数(照常插入 + 留痕)。
|
||
GateFailed int
|
||
// T1Called 是 T1 实际发出过**模型调用**的次数。每次审查至多 +1
|
||
// (defaultT1MaxGray 只是把多条灰区候选打包进同一次 prompt,不多发请求)。
|
||
T1Called int
|
||
// T1Decided 是 T1 真的定下结论(merge/modify/keep)的次数。它与 T1Called
|
||
// 的差值就是「调了但没用上」(非法回复 / 超时 / 报错回落 create)——
|
||
// 这个差值本身是可观测信号,因此两个数分开计。
|
||
T1Decided int
|
||
}
|
||
|
||
// bump 在互斥下修改计数(工作协程、手动 Review 与同步闸都可能调用)。
|
||
func (r *PreInsertReviewer) bump(apply func(*ReviewStats)) {
|
||
r.statsMu.Lock()
|
||
defer r.statsMu.Unlock()
|
||
apply(&r.stats)
|
||
}
|
||
|
||
// recordDecision 累计观测计数(含降级计数,便于运维判读)。
|
||
func (r *PreInsertReviewer) recordDecision(rev IngestReview) {
|
||
r.bump(func(s *ReviewStats) {
|
||
s.Reviewed++
|
||
switch rev.Decision {
|
||
case IngestSkip:
|
||
s.Skipped++
|
||
case IngestMerge:
|
||
s.Merged++
|
||
case IngestModify:
|
||
s.Modified++
|
||
default:
|
||
s.Created++
|
||
}
|
||
})
|
||
}
|
||
|
||
// auditWith 写一条审计。verb 让「同步闸当场拦下的」与「异步审查事后合并的」
|
||
// 在审计链上可分(前者 memory.ingest.gate,后者 memory.review.ingest)。
|
||
// 它**只带决定与 id,绝不带任何记忆正文**。
|
||
func (r *PreInsertReviewer) auditWith(verb string, rev IngestReview) {
|
||
result := fmt.Sprintf("decision=%s reason=%s item=%s target=%s degraded=%t score=%.3f",
|
||
rev.Decision, rev.Reason, rev.ItemID, rev.TargetID, rev.Degraded, rev.Score)
|
||
if r.deps.Audit == nil {
|
||
r.log.Debug("memory ingest review audit (no sink configured)", "what", verb, "result", result)
|
||
return
|
||
}
|
||
r.deps.Audit(reviewAuditWho, verb, result)
|
||
}
|
||
|
||
// reviewStatsLabel 把计数渲染成一条稳定日志串。
|
||
func reviewStatsLabel(s ReviewStats) string {
|
||
return fmt.Sprintf("reviewed=%d skip=%d merge=%d modify=%d create=%d dropped=%d failed=%d "+
|
||
"gate_checked=%d gate_absorbed=%d gate_failed=%d t1_called=%d t1_decided=%d",
|
||
s.Reviewed, s.Skipped, s.Merged, s.Modified, s.Created, s.Dropped, s.Failed,
|
||
s.GateChecked, s.GateAbsorbed, s.GateFailed, s.T1Called, s.T1Decided)
|
||
}
|