Repository navigation
Closed
Labels
bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)功能异常、崩溃、数据错误(agent 诊断 → 人工确认)
Description
环境与版本
@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 融合排序,把粗暴的 SQLLIKE子串匹配(keyword.slice(0, lim))强行插队到最前面。
建议修复方案与边界防护(已通过全部 186 项相关单测与对抗回归)
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()去重,并对空向量/失败查询设置failedPrefetchAt15s 冷却避免单步重复请求);在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)。
- 识别尾部未落盘
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双重惩罚与尺度碰撞。
src/service.js(fuseRecall):- 移除
:931-936在recipe !== "blend" && mode === "auto"下用keyword.slice(0, lim)覆盖merged的逻辑。
- 移除
Metadata
Metadata
Assignees
Labels
bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)功能异常、崩溃、数据错误(agent 诊断 → 人工确认)
Activity
fix(recall): Step 1 读到当轮用户输入,向量与 BM25 双路 RRF 融合(#617) (#620)