主人 2026-10-07 裁定:按最优解加全局限流。 - 新增 t1RateGate(滑动窗口、可注入时钟,~70 行);默认 120 次 / 60 分钟(现网 700-1000 条新记忆/天 ≈ 25-42/小时, 取 120 留约 3-5 倍余量) - 过闸摆在「t1Enabled + 有灰区候选」两个守卫之后、发请求之前 ⇒ 不调模型就不占额度;放行才记账 - 超限语义与既有失败路径同源:跳过 T1、决定仍是 create、零改写, 只多一个 reason=t1_throttled 进留痕 + ReviewStats.T1Throttled 计数 - 配置键 t1PerWindow / t1WindowSec 放 $DATA_DIR/config/memory-tidy.json (零新 env 键,启动期读一次);坏配置回落默认 + WARN,不启动失败 - 每窗口只记一条 WARN(超限是预期行为,一天几百条会淹掉真故障) - IngestRuleVersion 仍为 d3.2:判定规则集与解析契约没变,变的只是 额度状态,reason 列已可分辨(升版会有 §17.6 记过的幂等键重复行副作用) 自研而非复用 session.NewRateLimiter 的理由(design/12 §N C.5 自己登记过 这条取舍):既有件 Allow() 内部写死 time.Now(),没有时钟注入缝, 而本切片必须能注入可控时钟;且复用会把 channel 层概念拖进 memory 包。
87 lines
3.7 KiB
Go
87 lines
3.7 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
|
||
// T1Throttled 是**全局限速**命中、因而跳过 T1 的次数(design/23 §15.4,
|
||
// reason=`t1_throttled`)。它**不**计入 T1Called(一次模型调用都没发),
|
||
// 也不计入 T1Decided(没定任何结论)——「想调但被闸拦下」是与「调了没用上」
|
||
// 不同的一件事,分开计才看得出额度是不是成了瓶颈。
|
||
T1Throttled 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 t1_throttled=%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, s.T1Throttled)
|
||
}
|