fix(memory): embedding 批上限与超时同源 —— 不再「客户端先放弃、服务端继续算」
M9(EMBRECALL)。故障机制(M6 + 本次复测):客户端 5s 单次超时先放弃,服务端仍在 算那一批 ⇒ 本机 llama.cpp 被自己顶住 ⇒ 后续每个请求(含写向量索引的单条请求) 排队到自己的超时 ⇒ 召回全线降级。 - embedding/budget.go(新):把「一次请求的耗时上界 ≤ 单次超时的一半」写成不变式, 批上限(条数 4 / 字符 480)与 5s 超时由**同一个不等式**解出;实测依据与复跑命令 写进注释(空闲态摊销曲线 1→0.19s / 8→1.1s / 12→1.3s / 64→8.5s,约 50ms 固定 + 2.7~3.9ms/字符,n≥32 起有数倍放大 ⇒ 余量取一半)。 - embedding/api.go:Embed 按同一对上限**自己切片**(不变式与调用方无关);冷启动 首次调用单独给 warmupTimeout,拿到第一个成功响应后立刻回常态超时。 - memory/tidy_config.go:tidyEmbedBatch = embedding.DefaultBatchLimit(同源), 并加编译期断言 —— 调回 64 会直接编译失败(constant -60 overflows uint)。 - memory/tidy.go | tidy_recall.go | tidy_dryrun.go:recall_mode 改报**实际发生** 的来源(""=本轮没候选可比 / embedding / lexical / mixed),新增 recall_degraded_partitions;试算轮 RecallMode 同口径(M6 medium finding)。 - 测试:embedding 侧用「实测曲线 + 会睡够时间的假客户端」钉住两态 (批内 < 超时 ⇒ 不降级;旧配对 64 条 ⇒ 必然超时),并断言 abandoned == 0; memory 侧钉住降级/健康/没候选三态与试算轮的如实上报。 不改 preinsert*、不改 .env、不动判定规则与任何阈值。
This commit is contained in:
1 parent
68ba85389b
commit
59672720bd
9 files changed
+836
-45
No files matched your search
@@ -119,12 +119,17 @@ func (t *Tidier) RunOnce(ctx context.Context) (TidyStats, error) {
|
||||
return stats, err
|
||||
}
|
||||
stats.RunID = runID
|
||||
// ledger 是**本轮实际发生**的召回来源记账口(不是配置里那一种):生产轮可以
|
||||
// 多分区混合(有的走语义、有的降级),因此汇总行只能由实测记录推出。
|
||||
ledger := &recallLedger{}
|
||||
|
||||
failures := t.scanPartitions(ctx, partitions, &stats, tidyRoundPolicy{
|
||||
sink: func(ctx context.Context, cands []TidyCandidate, stats *TidyStats) {
|
||||
t.persistCandidates(ctx, runID, cands, stats)
|
||||
},
|
||||
recall: t.fallbackRecall,
|
||||
recall: func(ctx context.Context, p tidyPartition, items []MemoryItem, stats *TidyStats, budget tidyBudget) ([]tidyPair, error) {
|
||||
return t.fallbackRecall(ctx, p, items, stats, budget, ledger)
|
||||
},
|
||||
})
|
||||
|
||||
// 轮末自动落地(2026-10-07 主人口径):规则层判为高置信的待定候选直接落地,
|
||||
@@ -150,7 +155,7 @@ func (t *Tidier) RunOnce(ctx context.Context) (TidyStats, error) {
|
||||
failures = append(failures, err)
|
||||
}
|
||||
|
||||
t.logRun(stats, run)
|
||||
t.logRun(stats, run, ledger)
|
||||
t.auditAutoApply(auto)
|
||||
|
||||
switch {
|
||||
@@ -165,7 +170,14 @@ func (t *Tidier) RunOnce(ctx context.Context) (TidyStats, error) {
|
||||
|
||||
// logRun 输出本轮的可观测口径:扫描 N / 候选 M / 建议 K / 否决原因分布。
|
||||
// 它带 source(memory/tidy),因此这条汇总永远可回溯到整理子系统。
|
||||
func (t *Tidier) logRun(stats TidyStats, run TidyRun) {
|
||||
//
|
||||
// ledger 给出的是**本轮实际发生**的召回来源(不是配置态)。这一条是 M6 2026-10-13
|
||||
// 的 medium finding 的落点:旧实现按「有没有配 embedder」写 recall_mode,全量降级
|
||||
// 在汇总行里被写成 embedding。现在:
|
||||
// - recall_mode = "":没有任何分区真正算过相似度(没有候选可比);
|
||||
// - recall_mode = lexical ∧ recall_degraded_partitions > 0:语义不可用、降级了;
|
||||
// - recall_mode = mixed:多分区里一部分走了语义、一部分降级 —— 不再被抹平。
|
||||
func (t *Tidier) logRun(stats TidyStats, run TidyRun, ledger *recallLedger) {
|
||||
fields := []any{
|
||||
"run_id", run.ID,
|
||||
"rule_version", run.RuleVersion,
|
||||
@@ -181,7 +193,8 @@ func (t *Tidier) logRun(stats TidyStats, run TidyRun) {
|
||||
"kept", stats.Kept,
|
||||
"errors", run.Errors,
|
||||
"duration_ms", run.FinishedAt.Sub(run.StartedAt).Milliseconds(),
|
||||
"recall_mode", tidyRecallMode(t.deps.Embedder != nil),
|
||||
"recall_mode", ledger.label(),
|
||||
"recall_degraded_partitions", ledger.degradedPartitions(),
|
||||
"auto_apply", t.cfg.autoApplyEnabled(),
|
||||
"auto_applied", stats.AutoApplied,
|
||||
"auto_merge", stats.AutoMerged,
|
||||
@@ -232,11 +245,15 @@ func (t *Tidier) scanPartitions(ctx context.Context, partitions []tidyPartition,
|
||||
// fallbackRecall 是**生产轮**的召回:相似度来源不可用时降级到零依赖的词项
|
||||
// 余弦(离线/无 embedding 配置时可跑,宁可比语义路径更保守),并记一条 WARN。
|
||||
// 两个来源**不可混用阈值**:阈值只有一个(design §13 开放点 2 的 0.80)。
|
||||
func (t *Tidier) fallbackRecall(ctx context.Context, p tidyPartition, items []MemoryItem, stats *TidyStats, budget tidyBudget) ([]tidyPair, error) {
|
||||
pairs, err := t.recallPairs(ctx, p, items, stats, budget)
|
||||
//
|
||||
// 降级这一笔**必须记进 ledger**:汇总行/面板上的召回来源只能由实测记录推出,
|
||||
// 否则「全量降级」会被报成「向量正常」(M6 2026-10-13 的 medium finding)。
|
||||
func (t *Tidier) fallbackRecall(ctx context.Context, p tidyPartition, items []MemoryItem, stats *TidyStats, budget tidyBudget, ledger *recallLedger) ([]tidyPair, error) {
|
||||
pairs, err := t.recallPairs(ctx, p, items, stats, budget, ledger)
|
||||
if err == nil {
|
||||
return pairs, nil
|
||||
}
|
||||
ledger.noteLexical(true)
|
||||
t.log.Warn("memory tidy embedding recall unavailable, falling back to lexical",
|
||||
"partition", p.label(), "error", err)
|
||||
return t.collectPairs(p, items, t.cfg.RecallThreshold,
|
||||
|
||||
Reference in new issue
Block a user