Files
Cyrene_v2/internal/runtime/memory/preinsert_stats.go
T
AskaEth 1b4d0a552f
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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 行上限,无新逻辑)
2026-10-07 14:10:33 +08:00

82 lines
3.3 KiB
Go
Raw 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 "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)
}