feat(memory): T1 加保守全局速率限流(防每天数百到上千次调用失控)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

主人 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 包。
This commit is contained in:
AskaEth committed 2026-10-07 16:25:54 +08:00
1 parent 987cefdf28
commit a684328b76
10 files changed
+782 -10

No files matched your search

@@ -24,6 +24,18 @@ const (
// 不是任何角色(design §4.1 C 的同一理由)。
reviewAuditWho = "system/memoryreview"
reviewAuditVerb = "memory.review.ingest"
// defaultT1PerWindow / defaultT1Window 是 T1 **全局限速**的默认额度
// (design/23 §15.4,主人 2026-10-07 裁定「加一层保守的全局速率限流」)。
//
// 推算依据(2026-10-07):现网节奏是每天 700–1000 条新记忆(≈25–42 条/小时),
// 而 T1 只在「T0 判不出结论**且**存在灰区候选」时才发调用 —— 每条新记忆至多换
// 一次调用。取 **120 次 / 60 分钟**,即在最忙的那一小时也留 ≈3–5 倍余量:
// 正常节奏下这个闸几乎碰不到;只有「召回/规则突然把大批条目判成灰区」这类
// 异常形态才会触顶,而那正是要拦的东西。超限不是错误:跳过 T1、回落 create、
// 留痕 `t1_throttled`,记忆一条不丢、一个字不改。
defaultT1PerWindow = 120
defaultT1Window = 60 * time.Minute
)
// ReviewConfig 携带入库前审查的调优旋钮。零值经 normalize 选取安全默认值。
@@ -73,6 +85,20 @@ type ReviewConfig struct {
// T1Timeout 是**单次** T1 模型调用的墙钟上限。非正用代码默认值
// defaultT1Timeout;最终还会被 cfg.Timeout 收紧取小(见 t1Timeout)。
T1Timeout time.Duration
// T1PerWindow 是 T1 的**全局**窗口额度(次)—— 不是每条各自的限制,而是
// 全进程共享的一个窗口:T1Window 内至多这么多次模型调用,超限即跳过 T1 并
// 回落 `create`(reason=`t1_throttled`,design/23 §15.4)。
//
// 非正用代码默认值 defaultT1PerWindow。**刻意没有「无上限」语义**:
// design/12 §N C.5 G3 登记过那条静默失败形态(未归一的 ≤0 直达滑窗 ⇒ 被读成
// 「无限制」,没有报错、没有 WARN);要完全不花钱请用
// `$DATA_DIR/config/memory-tidy.json` 的 `{"t1Judge": false}` 关掉 T1。
//
// 额度**只被真正的模型调用消耗**(闸放行才记账,被拒的那次不占),因此
// 「窗口过了就恢复」是结构性成立的。
T1PerWindow int
// T1Window 是 T1PerWindow 的时间窗。非正用代码默认值 defaultT1Window。
T1Window time.Duration
}
// normalizeReviewConfig 用设计稿默认值填充零值字段。
@@ -98,5 +124,14 @@ func normalizeReviewConfig(cfg ReviewConfig) ReviewConfig {
if cfg.T1Timeout <= 0 {
cfg.T1Timeout = defaultT1Timeout
}
// 🔴 限流的两项必须在这里回落(design/12 §N C.5 G3 的教训):闸是照着
// 「非正 = 恒放行」实现的,未归一的 ≤0 直达它就会把「回落默认」**静默**变成
// 「无上限」。因此 NewPreInsertReviewer 保证闸在 normalize **之后**构建。
if cfg.T1PerWindow <= 0 {
cfg.T1PerWindow = defaultT1PerWindow
}
if cfg.T1Window <= 0 {
cfg.T1Window = defaultT1Window
}
return cfg
}