Skip to content

[bug] DSH 0.2.1 Step 1 读不到当轮用户输入 + queryVectorCache 首步必 miss + injectCandidates/fuseRecall 的 RRF 排序被覆盖 #617

Closed

Description

@xueyue33

环境与版本

  • @modusensus/dsh-mneme: 0.12.0
  • @deepseek-ai/dsh: 0.2.1-rc.2 (@deepseek-ai/dsh-agent-loop, @deepseek-ai/dsh-system-prompt, @deepseek-ai/dsh-session)

问题描述

在 DSH 0.2.1-rc.2 下实测排查发现自动注入(inject.js / service.js:injectCandidates)与工具检索(service.js:fuseRecall)存在四个环环相扣的召回缺陷,导致云端向量与 RRF 融合在首步及常规检索中无法按预期生效:

1. DSH 0.2.1 下 lastUserQuery(ctx) 在 Step 1 读不到当轮新输入,且 queryVectorCache 在 Step 1 恒为 undefined(src/inject.js:47-74, 298-309, 366-368)

  • 根因 A(事件落盘时序变化):在 @deepseek-ai/[email protected] 中,用户发出的消息先通过 ReactLoopInbox.splice 记为 event.type === "agent/inbox/spliced"(位于 event.data.inserted),在 preStep() 中由 this.inbox.claim() 认领并触发 systemPrompt.assemble()(第 679 行)及 step() 中的 systemPrompt.refreshContext()(第 843 行);直到第 865 行才真正执行 this.session.append("user/message", message)。
    因此 lastUserQuery(ctx) 仅遍历 event.type === "user/message" 时,在首轮 Step 1 恒返回 "",在第 $N$ 轮($N \ge 2$)的 Step 1 恒返回第 $N-1$ 轮的旧消息。
  • 根因 B(同步渲染与异步预取竞态):prefetchQueryVector(query) 发起异步 service.embedQuery(query),紧接着下一行同步执行 const queryVector = queryVectorCache.get(query),对于每轮新问题在 Step 1 必然 Cache Miss(若单步直接回答完毕不调工具,则整轮对话 100% 走不到向量检索)。

2. injectCandidates 中 Vector 与 BM25 互斥,且被下方 selectiveInjectEnabled 二次覆盖(src/service.js:1788-1835, 1930-1944)

  • 互斥问题:src/service.js:1819 使用 if (!semanticItems.length) 包裹 bm25Recall(q, poolSize),一旦向量路返回结果(即使余弦相似度很低),BM25 词法路被完全跳过,无法实现 Vector $\oplus$ BM25 双路 RRF 融合。
  • 先截断后过滤与软加权顺序:src/service.js:1790 调用 vectorIndex.search(queryVector, { limit: poolSize, threshold: 0 }) 先按 poolSize(默认 10)截断,再过滤 archived / forgotten / importance >= threshold,若前 10 名中含有低重要度条目或跨作用域(foreign)条目,过滤与软加权后有效候选名额直接缩水。
  • 下游覆盖问题:即便在 :1788-1835 将 vecItems 与 bm25Items 按 RRF (k = 60) 融合进 candidates,下方 :1930-1944 的 selectiveInjectEnabled !== false(默认开启)会用纯向量余弦相似度 sim.get(m.id) 对 candidates 全量重排,把刚由 RRF 顶上来的 BM25 命中项全部压到第 maxItems..poolSize-1 位,随后在 :2037(general.length >= maxItems)被截断丢弃。

3. fuseRecall 在 mode === "auto" 下用 SQL LIKE 强行覆盖 RRF 排序(src/service.js:931-936)

  • src/service.js:931-936:
    if (recipe !== "blend" && mode === "auto" && keyword.length) {
      const head = keyword.slice(0, lim);
      const seen = new Set(head.map((m) => m.id));
      const tail = merged.filter((m) => !seen.has(m.id)).slice(0, Math.max(0, lim - head.length));
      merged = head.concat(tail);
    }
  • 当用户显式配置 recallFusion: "rrf"(或 "minmax")时,memory_search 工具默认 mode = "auto",上述代码会推翻刚刚算好的四路 RRF 融合排序,把粗暴的 SQL LIKE 子串匹配(keyword.slice(0, lim))强行插队到最前面。

建议修复方案与边界防护(已通过全部 186 项相关单测与对抗回归)

  1. src/inject.js:
    • 识别尾部未落盘 agent/inbox/spliced 且防止 Rewind / Cancel 穿透:
      由于 agent/inbox/spliced 不在 session.surface.nodes(visibleSeqs)中,逆序遍历 events 时需维护 allowPendingSplice = true:一旦遇到任何 user/message(无论是否在 visibleSeqs 中)、turn/end 或 outcome === "canceled" / removedCount > 0 的撤回 splice,立即置 allowPendingSplice = false,从而只读取当前轮次尚未落盘的尾部新消息,绝不泄漏已撤回(Rewind)或已取消(Cancel)的历史轮次。
    • 异步预热与定时器/失败冷却管理:
      将 prefetchQueryVector(query) 改为返回 Promise(用 inflightPrefetches = new Map() 去重,并对空向量/失败查询设置 failedPrefetchAt 15s 冷却避免单步重复请求);在 awaitPrefetchWithTimeout 的 finally 中调用 clearTimeout 防止定时器泄漏。
    • 带会话状态指纹(eventsLen + visibleSize)与容量上限的 liveStepQuery:
      在 agent/inbox/claimed 与 agent/pre-step 中仅对显式新消息记录 { query, eventsLen, visibleSize, at }(若 decision.kind !== "enter" 则立即清除);一旦后续 events.length 或 visibleSeqs.size 发生变化(如消息已落盘为 user/message 或发生 Rewind),自动失效并交回 lastUserQuery(ctx)。
  2. src/service.js (injectCandidates):
    • vectorIndex.search 与 bm25Recall 采用 Math.max(poolSize * 4, 40) over-fetch 过滤门控并完成 softScopeActive 排序后再截取 poolSize;
    • 同时收集 vecItems 与 bm25Items,按 RRF (k = 60) 计算 rrfScores(并在双路同时非空时将 merged 的语义池容量设为 poolSize * 2 防止候选提前被挤出);
    • 在 selectiveInjectEnabled 的 scopedSim(m) 中,将归一化 RRF 分 normRrf = rrf * ((RRF_K + 1) / 2) 与真实余弦相似度 s 按单调连续尺度 (0.5 * (s ?? 0) + 0.5 * normRrf) * scopeMultOf(m) 融合(未向量化条目取 normRrf * scopeMultOf(m)),避免 scopeMultOf 双重惩罚与尺度碰撞。
  3. src/service.js (fuseRecall):
    • 移除 :931-936 在 recipe !== "blend" && mode === "auto" 下用 keyword.slice(0, lim) 覆盖 merged 的逻辑。

Activity

  1. added
    bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)
    on Nov 9, 2026
  2. moved this from 📋 Backlog to 🔨 In Progress in dsh-mneme 任务看板on Nov 9, 2026
  3. moved this from 🔨 In Progress to ✅ Done in dsh-mneme 任务看板on Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions