Files
Cyrene_v2/internal/runtime/memory/preinsert_config.go
T
AskaEth a684328b76
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(memory): T1 加保守全局速率限流(防每天数百到上千次调用失控)
主人 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 包。
2026-10-07 16:25:54 +08:00

138 lines
7.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 "time"
// 本文件是 D3「入库前审查」(design/23 §15.1 D3)的**调优面**:默认值常量、
// ReviewConfig 的旋钮与它们的零值归一化。它从 preinsert.go 拆出来只为守
// AGENTS §10 的文件行数上限(≤500 行)与「模块按职责拆源文件」那条规范 ——
// 口径逐字未变。
//
// 边界:本文件只管「旋钮长什么样 + 零值取什么默认值」,不认识服务本体
// (PreInsertReviewer 的队列/工作协程在 preinsert.go),也不认识任何开关的
// 运维来源(env 与 `$DATA_DIR/config/memory-tidy.json` 都在组合根)。
// 默认值与审计口径。运算路径上不出现裸魔法数字(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"
// 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 选取安全默认值。
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
// DisableT1 关闭 **T1 灰区模型判定**(design/23 §15.1 D3-2:灰区调一次轻量
// 模型,只许回 merge/modify/keep + 理由、禁产新内容)。
//
// 它是**反向**字段,理由与 DisableIngestGate 逐字相同:bool 的零值必须落在
// 「默认行为」上,而 T1 的代码默认值是**开**(主人 2026-10-07:要的就是「真的
// 走记忆管理会话」,T0 仍是第一道零成本闸)。若写成正向的 EnableT1,任何没
// 显式配过的地方(含测试与组合根构造的一半配置)都会读成「关掉」,与默认相反。
// 要一键回到纯规则(零模型调用),写 `$DATA_DIR/config/memory-tidy.json` 的
// `{"t1Judge": false}`(**零新 env 键**:`.env` 已冻结,design/13 §2.1)。
DisableT1 bool
// 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 用设计稿默认值填充零值字段。
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
}
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
}