qwen-code 异步记忆召回Async Memory Recall设计解析零阻塞的记忆预取与双消费点注入机制【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读本文深入解析 qwen-code一款运行在终端中的开源 AI 编码代理中托管自动记忆Managed Auto Memory的异步召回设计。该设计解决了一个具体而尖锐的工程问题当模型侧的记忆相关性选择relevance selector副查询在冷启动场景下耗时超过 1 秒时如何避免让每次用户查询UserQuery的主请求路径被记忆召回拖慢最多 2.5 秒。读完本文你将掌握触发后不等待fire-and-forget 双消费点注入UserQuery / ToolResult这一异步预取模式的数据结构、生命周期管理与清理策略并能对照仓库源码理解其从设计文档到生产实现的完整落地过程。一、问题背景一次记忆召回如何拖垮主请求延迟1.1 症状冷启动场景稳定命中 1 秒超时阈值在最初的设计中记忆相关性选择器 relevanceSelector.ts 使用AbortSignal.timeout(1_000)作为召回副查询的硬超时由 PR #3866 引入。问题在于首次会话冷启动first-session cold start时qwen3.5-flash 平均召回耗时约908 ms稳定逼近 1 秒阈值。同时resolveAutoMemoryWithDeadline中的外层2.5 秒截止时间意味着即使召回每次都失败每次 UserQuery 也最多会被阻塞 2.5 秒。1.2 根因主请求路径同步等待召回结果// 旧实现的核心问题伪代码示意 const relevantAutoMemoryPromise resolveAutoMemoryWithDeadline(...); // 主代理请求路径在这里 await 召回结果后才向模型发送请求 await relevantAutoMemoryPromise;主代理请求路径在向模型发送请求前await了召回结果召回副查询的任何延迟都会直接累加到用户可见的延迟上。这是典型的慢速旁路查询拖慢主链路问题。二、核心设计思想触发后不等待双消费点机会主义注入2.1 设计总览核心思路只有一句话在 UserQuery 时触发召回但绝不await它。召回结果在两个机会主义消费点被取用谁先触发谁生效UserQuery 消费点在turn.run()之前做同步的settledAt ! null检查。零等待——如果召回已结算就用它否则直接跳过不做任何阻塞。ToolResult 注入点在每个 ToolResult 回合做同样的检查。此时将记忆以system-reminder形式追加append到requestToSend中 functionResponse 部分之后在模型产出下一条响应之前为其提供记忆上下文。2.2 为什么 ToolResult 必须追加而非前置追加而非前置append, not prepend是硬性约束Qwen API 要求 functionResponse 必须紧跟在模型的 functionCall 之后。如果前置插入记忆提示就会破坏 functionCall/functionResponse 的配对关系。源码中已有的hasPendingToolCallIDE-context 跳过逻辑也遵循同一约束。该模式与上游 Claude Code 的做法一致query.ts中的startRelevantMemoryPrefetch/settledAt轮询。三、数据结构MemoryPrefetchHandle3.1 设计文档中的原始定义client.tstype MemoryPrefetchHandle { promise: PromiseRelevantAutoMemoryPromptResult; /** Set by promise.finally(). null until the promise settles. */ settledAt: number | null; /** True after memory has been injected — prevents double-inject. */ consumed: boolean; controller: AbortController; };四个字段各司其职字段类型作用promisePromiseRelevantAutoMemoryPromptResult召回副查询的 PromisesettledAtnumber \| null由promise.finally()写入的时间戳为null表示尚未结算consumedboolean记忆注入后置为true防止双消费点重复注入controllerAbortController召回副查询的中止控制器供清理路径使用3.2GeminiClient字段变更移除新增pendingRecallAbortController: AbortController \| undefinedpendingMemoryPrefetch: MemoryPrefetchHandle \| undefined从裸的 AbortController升级为携带结算状态与消费状态的完整句柄是本次重构的数据结构基础。3.3 演进源码中的实际形态设计落地后该类型在 client.ts 中演进出了更多字段以支撑后续的确定性快速结果fast result与遥测type MemoryPrefetchHandle { promise: PromiseRelevantAutoMemoryPromptResult; settledAt: number | null; result: RelevantAutoMemoryPromptResult | null; // 结算后缓存结果 consumed: boolean; terminalLogged: boolean; // 终态遥测防重复记录 firedAt: number; // 触发时间用于延迟计算 controller: AbortController; fastResultRef: MemoryFastResultBox; // 确定性结果的发布槽 fastDelivered: boolean; // 快速结果是否已注入 fastDeliveredPaths: Setstring; // 快速阶段已注入的路径集合 };其中fastResultRef使用盒子box而非普通字段是因为召回可能在句柄对象创建之前就通过回调发布了确定性结果详见下文 2026-08-08 演进章节。四、改动清单逐条拆解4.1client.ts— 删除resolveAutoMemoryWithDeadline该函数被整体删除由settledAt标志机制完全取代。4.2client.ts— UserQuery 触发路径用以下代码替换原先的resolveAutoMemoryWithDeadline调用// 在新 UserQuery 到达前中止上一次仍在飞行中的预取 // 防止用户在召回结算前再次输入时产生孤儿副查询。 this.pendingMemoryPrefetch?.controller.abort(); this.pendingMemoryPrefetch undefined; const controller new AbortController(); // 将调用方的 signal 桥接到预取控制器使用户中止 // Ctrl-C / Esc父回合时召回副查询也随之终止。 const onParentAbort () controller.abort(); if (signal.aborted) { controller.abort(); } else { signal.addEventListener(abort, onParentAbort, { once: true }); } const promise this.config .getMemoryManager() .recall(projectRoot, partToString(request), { config: this.config, excludedFilePaths: this.surfacedRelevantAutoMemoryPaths, abortSignal: controller.signal, }) .catch((error: unknown) { if (!(error instanceof DOMException error.name AbortError)) { debugLogger.warn(Managed auto-memory recall prefetch failed., error); } return EMPTY_RELEVANT_AUTO_MEMORY_RESULT; }); const handle: MemoryPrefetchHandle { promise, settledAt: null, consumed: false, controller, }; void promise.finally(() { handle.settledAt Date.now(); signal.removeEventListener(abort, onParentAbort); }); this.pendingMemoryPrefetch handle; // no await — continue immediately关键细节excludedFilePaths传入this.surfacedRelevantAutoMemoryPaths确保同一会话中已展示过的记忆文档不会被重复注入去重机制错误吞噬非中止类错误仅记录 warn 日志并返回EMPTY_RELEVANT_AUTO_MEMORY_RESULT兜底保证召回失败绝不影响主请求注释// no await — continue immediately是设计意图的代码级声明触发后立即继续。4.3client.ts— UserQuery 消费点替代原先的await relevantAutoMemoryPromiseconst prefetchHandle this.pendingMemoryPrefetch; if ( prefetchHandle prefetchHandle.settledAt ! null !prefetchHandle.consumed ) { prefetchHandle.consumed true; this.pendingMemoryPrefetch undefined; const result await prefetchHandle.promise; // already settled, returns immediately if (result.prompt) { // unshift, not push: keep memory at the front of systemReminders so // it leads the system-reminder block on UserQuery turns. (ToolResult // turns instead append to requestToSend to preserve functionCall / // functionResponse pairing — see below.) systemReminders.unshift(result.prompt); for (const doc of result.selectedDocs) { this.surfacedRelevantAutoMemoryPaths.add(doc.filePath); } } }注意这里的注入方式是unshift前置UserQuery 回合中记忆应位于 system-reminder 块的最前部起到引导作用这与 ToolResult 回合的追加方式形成鲜明对照。4.4client.ts— ToolResult 注入点新增在requestToSend组装完成后、turn.run()之前添加if (messageType SendMessageType.ToolResult) { const prefetchHandle this.pendingMemoryPrefetch; if ( prefetchHandle prefetchHandle.settledAt ! null !prefetchHandle.consumed ) { prefetchHandle.consumed true; this.pendingMemoryPrefetch undefined; const result await prefetchHandle.promise; if (result.prompt) { // Append (not prepend) so functionResponse parts stay first // and the models functionCall/functionResponse pairing // isnt broken on the native Gemini path. requestToSend [...requestToSend, result.prompt]; for (const doc of result.selectedDocs) { this.surfacedRelevantAutoMemoryPaths.add(doc.filePath); } } } }4.5client.ts— 清理路径句柄通过两套不同的机制释放5 个中止并清除点预取仍在飞行中丢弃引用前必须先中止控制器this.pendingMemoryPrefetch?.controller.abort(); this.pendingMemoryPrefetch undefined;对应站点resetChat()、MaxSessionTurns提前返回、boundedTurns0提前返回、SessionTokenLimitExceeded提前返回、Arena 控制信号提前返回。触发路径本身在新 UserQuery 到达而旧预取仍在飞行时也执行这一中止并替换操作。2 个仅清除点预取已结算且正在消费无需中止控制器只需丢弃引用prefetchHandle.consumed true; this.pendingMemoryPrefetch undefined;对应站点UserQuery 消费点、ToolResult 注入点。4.6relevanceSelector.ts— 移除 1 秒硬超时删除组合式AbortSignal.any([AbortSignal.timeout(1_000), callerAbortSignal])改为直接传递callerAbortSignal。超时治理从召回侧 1 秒硬切转移到调用侧句柄生命周期管理。五、行为对比表设计文档给出了重构前后五种典型场景的行为差异这是理解该设计价值的核心对照场景重构前重构后召回在模型准备前完成UserQuery 注入约 0 等待UserQuery 注入约 0 等待召回缓慢冷启动最多阻塞 2.5 秒跳过 UserQuery在首个 ToolResult 注入召回超时1 秒中止、空结果、无记忆无硬超时结算后随时注入无工具调用且召回缓慢最多阻塞 2.5 秒后跳过跳过 UserQuery无 ToolResult 机会——错过用户在召回结算前发送第 2 条消息第 2 次召回与第 1 个句柄竞争第 1 个句柄在新 UserQuery 触发新句柄时被中止其中无工具调用且召回缓慢场景是设计明确接受的取舍纯问答型回合若召回未结算且无 ToolResult 机会记忆会被丢弃。这一缺口正是后续 2026-08-08 演进要弥补的方向见第七节。六、源码落地从设计到实现的完整对照6.1 消费点调用链sendMessageStream中的实际接线设计中的四个入口在 client.ts 中分别对应四个方法设计概念源码方法说明UserQuery 触发beginManagedAutoMemoryRecall(query, signal)内部先cancelPendingMemoryPrefetch(new_query)清理旧句柄再安装新句柄UserQuery 消费consumeManagedAutoMemoryRecall(initial)传入INITIAL_MEMORY_RECALL_WAIT_MS有界等待预算ToolResult 注入consumeManagedAutoMemoryRecall(tool_result)等待预算为 0纯机会主义检查回合收尾finishManagedAutoMemoryRecall()以no_safe_delivery_point原因清理未消费句柄在sendMessageStream中beginManagedAutoMemoryRecall在 UserQuery/Cron 分支被调用client.tsconsumeManagedAutoMemoryRecall(initial)在主请求前调用consumeManagedAutoMemoryRecall(tool_result)在 ToolResult 回合调用finishManagedAutoMemoryRecall在回合收尾处调用。6.2 统一入口tryConsumeMemoryPrefetch设计文档中的两个消费点 防重复注入守卫在源码中收敛为一个统一方法tryConsumeMemoryPrefetchclient.ts注释明确说明其职责Centralises the consume-and-mark dance so the UserQuery and ToolResult inject sites cant drift on the guard logic. 将消费与标记动作集中化使 UserQuery 与 ToolResult 注入点不会在守卫逻辑上漂移。其核心守卫逻辑为句柄不存在或已消费则返回null否则等待预算上限而非固定成本预算到期后检查结算状态已结算则标记消费并返回结果未结算且存在确定性快速结果则注入快速结果。6.3 清理收敛cancelPendingMemoryPrefetch设计中的5 个中止并清除站点也被收敛为单一方法cancelPendingMemoryPrefetch(discardReason)client.ts它负责记录丢弃遥测、中止控制器、置空句柄引用。requestShutdown()、resetChat()以及各提前返回路径均调用它杜绝只清一半的悬挂引用。6.4 选择器侧的安全网30 秒兜底relevanceSelector.ts 中超时治理从1 秒硬超时调整为30 秒安全网abortSignal: callerAbortSignal ? AbortSignal.any([AbortSignal.timeout(30_000), callerAbortSignal]) : AbortSignal.timeout(30_000),源码注释解释了这个数字的由来正常召回约 1 秒30 秒远高于长尾分布不会重新引入本设计要消除的 1 秒超时回归它只会在模型 API 挂起网络分区、服务器停滞、无限重试且调用方从未中止时兜底触发。没有这个上限无调用方的调用会使用无信号的 AbortController 而无限运行。注意1 秒超时被移除但并非无限制——超时责任从选择器内部上移到调用方句柄的生命周期管理。6.5 测试验证行为契约被逐条固定client.test.ts 用可控制 PromisemockMemoryManager.recall.mockImplementation返回手动 resolve 的 Promise与假定时器vi.useFakeTimers()把设计的核心契约固化为测试召回永不结算时主请求恰好被保持初始召回预算client.test.ts假定时器钉住上限——预算到期瞬间请求继续执行且不带记忆召回在预算内结算时提前结束初始等待结算即提前退出不浪费剩余预算召回已结算时在 UserQuery 消费点注入召回在 UserQuery 之后结算时在首个 ToolResult 注入client.test.ts召回在 UserQuery 回合保持 pending结算后由 ToolResult 回合消费并断言召回 signal 未被误中止无工具回合以no_safe_delivery_point丢弃 pending 预取调用方 signal 中止时终止 pending 预取。这些测试精确对应设计文档行为对比表中的每一行是该设计可验证属性的最强证据。七、后续演进2026-08-08 Native Memory Recall Reliability设计文档顶部标注了重要的更新说明supersede 声明原设计中零等待的 UserQuery 消费点已被 2026-08-08 的 native-memory-recall-reliability 设计 取代。核心变化是引入 100 ms 有界初始等待INITIAL_MEMORY_RECALL_WAIT_MS 100见 client.tsUserQuery 消费点现在最多等待 100 ms。若召回在此预算内结算则随初始请求交付否则交付一个确定性快速结果deterministic fast result而非什么都不给同时保留 pending 的模型选择结果供 ToolResult 阶段以排除已交付文档的方式继续投递等待是上限而非固定成本等待在以下四者中谁先到谁结束——召回结算、确定性结果发布、取消、预算到期快速结果封顶 2 篇文档MAX_FAST_RECALL_DOCS因为其背后没有模型判断力telemetry 的phase与strategy正交phasefast/refined描述投递阶段strategynone/heuristic/model描述选择方法二者不可互相替代。这一演进直接补上了原设计无工具调用时错过的短板对纯问答回合无工具调用确定性快速结果能在预算内送达记忆使无工具回合的记忆投递率成为可测量的质量指标文档中记录的评估为 92.3%残余 7.7% 是语义匹配但无词法重叠的查询子集。八、边界与明确不做的事原设计文档明确列出了三个 Out of scope 项它们是刻意划定的边界不改变记忆注入格式不将注入从system-reminder改为tool-result附件形式Claude Code 风格不引入按会话的字节预算跳过门per-session byte budget skip gate不引入单词级提示跳过门single-word prompt skip gate。这三项留待未来设计本次重构严格聚焦于异步化召回 双消费点注入这一单一问题。九、总结一个可复用的异步旁路查询模式qwen-code 的异步记忆召回设计提供了一个极具复用价值的工程模式把旁路查询从主链路的await中解放出来——触发后不等待用句柄持有生命周期用settledAt标志实现零成本的机会主义消费——同步检查一次布尔值比任何竞态协调都简单可靠双消费点互补——UserQuery 前置unshift与 ToolResult 追加append各司其职前者保证引导性后者保证 functionCall/functionResponse 配对不被破坏清理路径分级——飞行中中止、已结算仅清除杜绝孤儿副查询与重复注入用测试钉住行为契约——可控制 Promise 与假定时器让永不结算预算内结算跨回合结算等极端场景都可复现验证。对照阅读 原始设计文档、实现代码、选择器实现、行为测试 与 后续演进设计可以完整还原这一模式从延迟痛点到生产验证的全链路。若你的项目同样存在旁路查询拖慢主请求的问题这套句柄 结算标志 机会主义消费 分级清理的组合拳值得直接借鉴。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
