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
@@ -52,11 +52,15 @@ type TidyDryRun struct {
|
||||
// 「完整的建议」展示)。
|
||||
func (t *Tidier) DryRun(ctx context.Context) (TidyDryRun, error) {
|
||||
started := t.now()
|
||||
out := TidyDryRun{RecallMode: tidyRecallMode(t.deps.Embedder != nil)}
|
||||
out := TidyDryRun{}
|
||||
stats, partitions, err := t.beginRound(ctx)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
// ledger 是**本轮实际发生**的召回来源:试算会写进 RecallMode 返回给面板。
|
||||
// 旧实现按「有没有配 embedder」写(tidyRecallMode(deps.Embedder != nil)),
|
||||
// 那会让面板把「向量不可用」读成「向量正常」(M6 2026-10-13 的 medium finding)。
|
||||
ledger := &recallLedger{}
|
||||
|
||||
// 收集出口:闭包只在 scanPartitions 的**串行**循环里被调用,因此无需加锁。
|
||||
collected := make([]TidyCandidate, 0)
|
||||
@@ -64,12 +68,17 @@ func (t *Tidier) DryRun(ctx context.Context) (TidyDryRun, error) {
|
||||
sink: func(_ context.Context, cands []TidyCandidate, _ *TidyStats) {
|
||||
collected = append(collected, cands...)
|
||||
},
|
||||
recall: t.strictRecall,
|
||||
recall: func(ctx context.Context, p tidyPartition, items []MemoryItem, stats *TidyStats, budget tidyBudget) ([]tidyPair, error) {
|
||||
return t.strictRecall(ctx, p, items, stats, budget, ledger)
|
||||
},
|
||||
}
|
||||
failures := t.scanPartitions(ctx, partitions, &stats, policy)
|
||||
|
||||
out.Stats = stats
|
||||
out.Candidates = collected
|
||||
// 如实上报**实际发生**的来源:空串 = 没有任何分区真正算过相似度(「没候选」,
|
||||
// 与「向量不可用」不是一回事)。
|
||||
out.RecallMode = ledger.label()
|
||||
out.DurationMS = t.now().Sub(started).Milliseconds()
|
||||
if len(failures) > 0 {
|
||||
return out, fmt.Errorf("memory/tidy: dry-run finished with errors: %w", errors.Join(failures...))
|
||||
@@ -80,8 +89,8 @@ func (t *Tidier) DryRun(ctx context.Context) (TidyDryRun, error) {
|
||||
|
||||
// strictRecall 是**试算轮**的召回:语义召回不可用时**不**降级,如实上报。
|
||||
// 日志只记分区与错误,绝不带记忆正文(候选预览里已含正文)。
|
||||
func (t *Tidier) strictRecall(ctx context.Context, p tidyPartition, items []MemoryItem, stats *TidyStats, budget tidyBudget) ([]tidyPair, error) {
|
||||
pairs, err := t.recallPairs(ctx, p, items, stats, budget)
|
||||
func (t *Tidier) strictRecall(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 {
|
||||
t.log.Warn("memory tidy dry-run recall unavailable",
|
||||
"partition", p.label(), "items", len(items), "error", err)
|
||||
|
||||
Reference in new issue
Block a user