qwen-code 原生记忆召回可靠性设计:确定性快速通道与多语言打分器的实现解析
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
qwen-code(Qwen Code)是一款运行在终端中的开源 AI 编程代理,其"原生记忆(auto-memory)"能力会在用户每次提问时异步地检索已保存的记忆文档,并把相关内容注入提示词。本文基于 2026-08-08 原生记忆召回可靠性设计,完整剖析该模块的一次关键可靠性重构:为什么原来"先等 100ms、拿不到就放弃"的召回策略会在无工具调用的轮次上彻底失效,如何通过"单一召回生命周期 + 模型主导选择 + 确定性快速交付"的架构把无工具轮次的首轮交付率提升到 92.3%,以及支撑这一切的 Unicode 多语言打分器与三层验证体系。读完本文,你将掌握该设计的决策脉络、核心常量与代码路径、遥测语义以及其已知边界,可直接对照源码继续深入。
问题背景:异步召回在无工具轮次上的"安全交付点"缺失
qwen-code 的记忆召回(managed-memory recall)是异步的:每次用户提问时,召回流程与模型生成并行启动。原始实现采用zero-wait consume(零等待消费),即首轮提示词组装时立即尝试取召回结果。问题在于:
- 召回包含一次**模型选择器(model selector)**调用,这是一个网络侧查询,其终止上限(abort ceiling)高达 30 秒;
- 零等待意味着"有用的选择结果"几乎必然赶不上第一轮提示词;
- 如果这一轮次没有工具调用(tool-free turn),那么后续唯一的安全注入点 ToolResult 永远不会出现,召回结果只能被丢弃。
这类无工具轮次恰恰是用户级记忆最该生效的场景——不查仓库、直接依赖上下文回答的短问题。于是第一次修复引入了一个固定 100ms 初始预算,但实测证明它远远不够:只要存在Config(正常情况总是存在),召回就会等待模型选择器,而选择器是网络往返,预算被往返时间耗尽,而不是被它原本针对的调度抖动耗尽。预算超时后交付流程落到 ToolResult 注入点,无工具轮次永远到不了那里,结果依旧为零。
与此同时,模型选择器的失败回退(fallback)存在两个独立的正确性问题:它只对ASCII 文本做分词(tokenize),并且即使没有任何词法匹配,也会给每个非空文档一个正分数。前者让西里尔文、希腊文、阿拉伯文、带变音拉丁文等脚本完全无法参与检索,后者让无结果的查询也拿到一堆无关文档。
核心决策:单一生命周期 + 模型主导选择 + 确定性快速交付
设计文档给出的结论是:保留单一召回生命周期与"模型为主"的选择方式,但在它前面增加一个确定性的交付阶段——而不是 RFC #7040 最初提出的两阶段共享扫描(Fast/Refined)架构。具体决策如下:
- 100ms 初始等待是"上限(ceiling)"而非"固定开销":等待在以下四种情况中最先发生者结束——召回完成、确定性结果发布、取消、预算耗尽。
- 预算内完成的召回结果直接注入首轮提示词。
- 预算耗尽但确定性候选通过找到了相关文档时,交付这份有界结果而不是什么都不给。
selectModelCandidateDocuments本就要计算词法排序的候选以构建模型清单,快速结果复用这些候选,不产生额外的扫描或 I/O。它被限制在最多2 篇文档(MAX_FAST_RECALL_DOCS),远低于 5 篇的提示词上限,因为这条路径背后没有模型判断。 - 快速交付后召回继续挂起(pending),这样模型选择的结果仍能在同轮次的 ToolResult 交付点落地。
- 从后续交付中排除快速阶段已交付的文档,用剩余文档重建提示词。两个结果来自同一次扫描,选择器从未把快速文档视为"已排除",因此它合法地重新选择它们。当所有被选文档都已交付过时,记录
already_delivered;当选择器一篇文档都没返回时,记录no_relevant_results。 - 初始预算耗尽不中止召回。
- 保留既有取消路径与"恰好一次(exactly-once)"的终止遥测:被取消的轮次不交付任何快速结果。
100ms 预算保持为内部常量,遵循 RFC #7040 的方向——由基准测试决定的小型内部预算,不暴露为公开配置;后续是否调整由遥测数据说了算。
预算为何是"上限":决定权在扫描,不在选择器
快速结果只有在召回完成"枚举、读取、解析记忆树"之后才能发布——而本次设计移除了召回的 200 文档上限,扫描成本随记忆树增长。recall-scan-latency.test.ts 针对真实的临时记忆树,测量从召回调用到快速结果发布的墙钟时间:
| 主题数 | 中位数 | 占 100ms 预算比例 |
|---|---|---|
| 200 | ~29 ms | ~29% |
| 500 | ~70 ms | ~70% |
| 1000 | ~130 ms | ~130% |
由此得出两个结论(二者都无法从确定性的打分成本看出——那只有微秒级):
第一,对任何能在时限内扫描完的记忆树——即普通情况,用户通常持有几十个主题而非几百个——快速结果在预算耗尽前很久就已就绪。剩余时间的等待对象是一个本文档已经假设会错过预算的模型选择器,因此这种等待几乎等于在每个用户轮次上白白增加延迟。等待因此在快速结果发布时就结束。
代码并没有让这一点显而易见,文档直接点明:在首轮,只要确定性打分器匹配到任何东西,交付的就是快速结果——无论选择器有多快。onFastResult在召回发出选择器请求之前就已发布,因此等待结束时召回必然尚未 settle,"优先已 settle 的召回"分支只会在不存在快速结果时走到(没有Config,或没有词法匹配)。这是有意取舍而非疏漏:生产环境中的模型侧查询不可能在 100ms 上限内完成,在两个结果之间做仲裁等于每个轮次都花掉剩余预算去赢一场不会发生的赛跑。文档用回环(loopback)选择器 15ms 落地的本地验证确认了这一行为及其边界:快速结果被交付、模型的选择被丢弃——而改造前同一轮次什么都不交付。选择器的判断仍然到达模型,但发生在 ToolResult 交付点,且已排除快速文档。
第二,在足够慢的机器上,仅扫描就超过上限。上表是保守测量;在更快硬件上的独立运行记录为 9ms / 21ms / 46ms(同样三种规模),全部落在预算内。因此"交叉点"是机器的属性而非固定的主题数——大致在约一千个主题到"永远不会"之间,取决于 I/O 速度。越过交叉点后,一轮会花光整个预算仍然什么都不交付,这比被替代的零等待行为更糟。提前结束等待并不能修复这种情况,只能约束它并消除其他所有情况下的开销。真正的修复是持久化目录(persistent catalog),按 2026-08-09 有界记忆召回候选设计 所述,这超出本文档范围。
MAX_RELEVANT_DOCS按交付计,而非按轮次计
MAX_RELEVANT_DOCS = 5约束的是单次提示词,不是整个轮次。一个轮次如果快速阶段交付 2 篇、随后在 ToolResult 再交付 5 篇快速阶段未包含的文档,模型面前会出现7 篇文档。去重只移除重复项,并不减少总和。
这是放弃"组合快速/精炼预算核算"的有意后果——RFC #7040 原本把它规定为跨两阶段"补齐到五篇"的上限。保持组合上限意味着在交付路径上携带跨阶段文档预算,这正是本文档为避免重复注入(duplicate-injection)风险而拒绝的簿记方式。两个提示词各自有界、每篇文档正文仍被截断到MAX_DOC_BODY_CHARS、快速阶段上限为 2,因此最坏情况有界且很小——只是不是 5 而已。若聚合上限将来确需硬顶,廉价做法是把limit - fastDeliveredPaths.size作为精炼阶段的 limit 传入,而不是重新引入第二套预算。
为什么放弃原始 Fast/Refined 架构
RFC #7040 规定从一次共享扫描产出两个结果:精炼阶段排除已交付的快速文档,并补齐到组合五篇上限。该设计要提供的交付保证是值得保留的,但其机制不值得:第二条选择路径需要自己的扫描管线、自己的预算核算和跨阶段文档簿记——而正是这种簿记成为 RFC 自身警告过的重复注入类 bug 的来源。复用选择器本来就要打分的候选,只需一个额外回调 + 一个排除集合就拿到了同样的保证。这一对比直观地体现在 recall.ts 的onFastResult实现中:回调在selectModelCandidateDocuments计算完候选、阻塞于选择器往返之前发布确定性候选,fallbackDocs已经过词法排序并滤除了 active-tool 噪声。
遥测语义:phase与strategy正交
phase是交付阶段(delivery stage):fast表示预算耗尽时注入的确定性结果,refined表示模型选择的结果。strategy是选择方法(selection method):none、heuristic或model。
二者不冗余、互不包含。fast交付总是heuristic,但refined交付正常是model、在选择器失败并运行回退时是heuristic。仅凭strategy推断交付阶段,会把"确定性结果先到"与"模型选择器坏了"这两种截然不同的情况悄悄合并。在 client.ts 的消费路径中可以看到phase: 'fast'的遥测写入,而strategy来自快速结果的fast.strategy。
确定性打分器:同时服务快速路径与失败回退的多语言升级
打分器现在同时服务两条路径(快速路径 + 选择器失败回退),因此被系统性改进:
- Unicode NFKC 归一化查询与文档文本;
- 保留至少三个非 CJK 字母/记号/数字的连续段为整体 token。基于
\p{L}而非[a-z0-9],使西里尔文、希腊文、阿拉伯文、带变音拉丁文都能产出 token 而非一无所获。CJK逐字符排除而非依赖交替顺序,因为\p{L}也匹配汉字,一个拉丁起始的连续段会吞掉后面的 CJK,把abc漢字变成一个 token; - 对汉字(Han)、平假名(Hiragana)、片假名(Katakana)、谚文(Hangul)连续段生成Unicode 码点 bigram;
- 忽略孤立的 CJK 字符;
- 限制回退查询 token 数但保留两端(对应
MAX_HEURISTIC_QUERY_TOKENS = 64,见 recall.ts); - 只对能进入提示词的正文窗口打分(正文先截断到
MAX_DOC_BODY_CHARS = 1200字符,见 recall.ts 与 scoreDocument); - 必须先有标题/描述/正文的词法匹配,才应用类型加成;
- 标题与描述的 token 匹配权重大于正文(标题 +4、描述 +3、正文 +1);
- 平分按"新旧程度(recency)→ 输入顺序"打破,绝不按文档类型。按字母序的类型比较会把
feedback排在project之前、reference排在user之前,而MAX_FAST_RECALL_DOCS只取前两篇——类型决胜会系统性地把用户级记忆从快速结果中挤掉,而这恰恰是快速路径存在的意义。输入顺序作为最终键保留了"项目优先于用户"的优先级,因为召回把项目文档拼接在用户文档之前(见 recall.ts 的拼接逻辑)。
上述打分逻辑在 selectRelevantAutoMemoryDocuments 中实现,其排序键是score降序后接mtimeMs降序——注释明确说明不是按文档类型决胜。
非目标:明确不做的事
- 不做第二次扫描、第二个选择器或独立的 fast/refined 预算核算;
- 不提供公开的召回时序或检索模式配置;
- 不引入新的分词器或检索依赖;
- 不改变记忆写入、作用域、抽取、DREAM、Forget 或压缩;
- 不移除共享扫描器对非召回调用方的 200 文档上限。只有召回使用 2026-08-09 有界记忆召回候选设计 描述的有界广谱候选方案。
验证体系:质量、交付、延迟三层独立测量
设计文档明确区分了三件必须分开测量的事,对应三个测试文件:
- 召回质量:recall-eval.test.ts 在 51 个用例、25 篇文档的标注语料上评测,同时用线上打分器和一份冻结的改造前打分器打分,使"无回归"可复现而非仅靠断言。测试打印语料规模与盲选随机打分器在该语料上的 Recall@5(25 篇中取 5,即 20%),并有一条测试将该下限维持在 25% 以下,而实测结果远高于它——小语料会美化任何设计,下限的存在让头条数字可信。
- 交付:recall-delivery-eval.test.ts 回答一个独立问题——"选对的文档是否真的在安全交付点到达了模型"。一次正确但从未交付的选择毫无价值。它实测确定性打分延迟、各设计交付的文档集合、快速与精炼集合在去重规则下的重叠;建模(而非实测)选择器延迟,因为网络往返无法在单元测试中计时——结果按 40/250/600/1500/3000ms 五个延迟场景分别报告。结构化结论——"选择器慢于预算时,单路径设计下无工具轮次没有任何交付点"——在预算之上的每个场景都成立。测试还断言:快速路径在所有场景对词法可答查询实现 100% 工具自由首轮交付与 100% 任意交付,重复交付率恒为 0,无结果查询在两种设计下都保持静默。
- 扫描延迟:recall-scan-latency.test.ts 面向真实临时记忆树测量"召回调用 → 快速回调"的墙钟时间,并把模型选择器 mock 成挂起的网络往返。断言故意宽松(CI 共享、时序依赖机器),打印出的表格才是值得读的产物;被断言的是结构声明:在用户可达的记忆树规模上,快速结果远在预算之内落地。语料本体位于 packages/core/src/memory/fixtures/auto-memory-recall-eval.json。
完整验收清单(节选,全部可在上述测试与 recall.test.ts 中找到对应断言):
- 预算内 settle 的召回在首轮交付;预算耗尽但有确定性候选时交付有界结果、召回保持存活等待 ToolResult 交付;
- 后续交付绝不重复快速阶段已发送的文档;
- 取消结束有界等待并阻止过期交付;快速结果不跨查询边界;
- 标注集覆盖中文、英文、日文、韩文、混合文本、NFKC 归一化、仅正文匹配、无结果查询、与文档无共享 token 但可答的查询、以及 ASCII 与 CJK 之外的字母脚本(西里尔文、希腊文、带变音拉丁文);
- 初始等待在确定性结果发布时立即结束、不跑满上限;无可交付内容时等待仍跑满上限然后无记忆继续;
- active-tool 别名集每次召回派生一次而非每篇文档派生一次(对应 createActiveToolUsageFilter 的提升);
- 平分按新旧而非文档类型打破,用户文档不会被同分的 feedback/project/reference 文档挤出两篇快速结果;
- 全部文档均被快速阶段交付过的结果,无论在哪里被丢弃都记录为
already_delivered——工具自由轮次交付了一切就不该被计入no_safe_delivery_point桶;部分重叠仍计入,因为快速集合之外的文档确实没有交付点; - 长 CJK 查询保持有界打分并保留查询两端;active-tool 噪声过滤在确定性候选路径上保持不变;模型选择器失败回退仍触发且最多返回五篇文档。
已知限制:诚实标注的边界
设计文档以"已知限制"小节明确划出边界,这些在引用该设计时不应被忽略:
- 交付评测建模而非实测选择器延迟;超过预算的每个场景下结构声明成立。
- 快速结果背后没有模型判断,最多两篇文档以约束"选错"的成本;在无工具轮次上选择器永不落地时,排序不佳的快速文档就是模型看到的内容。
- 打分基于子串:查询 token 可能匹配进更长的词("owner" 匹配 "ownership")。评测语料如实记录了一个这样的用例而非隐藏它。
- 快速路径关闭的是时序缺口,不是匹配缺口。与文档无共享 token 的查询不会产生确定性结果,因此提出该问题的无工具轮次仍以"无交付"结束;只有模型选择器能服务这类查询,而无工具轮次上它永不落地。语料中的
semantic-no-lexical切片直接度量这一点——线上打分器与冻结的旧打分器在该切片上 Recall@5 均为 0%,说明"要求词法匹配"并未制造这个缺口,但确实让快速路径在此保持静默。这就是头条工具自由交付率是92.3% 而非 100%的原因:剩余 7.7% 恰好是这一切片。 - I/O 足够慢时记忆树扫描超过初始上限,轮次花光预算仍无交付;交叉点取决于机器(较慢机器约一千个主题,较快机器上未触达)。在快速结果上结束等待只能约束而不能消除此问题;真正的修复是持久化目录,超出范围。
- 首轮上确定性打分器命中时,模型选择器的判断不被使用(无论其延迟如何),它在 ToolResult 才到达模型。
- 无词分隔符且不在 CJK 集合内的脚本(泰文、高棉文、老挝文)现在能产出一个 token(此前是零个),但该 token 是整个连续段——这不是分词,此类查询通常仍然匹配不到东西。
- 召回可以看到共享 200 文档扫描器上限之外的更旧文档,但包括 Forget 在内的非召回调用方保留有上限的扫描器(Forget 已被 issue #9378 移到无上限扫描器,并对模型选择提示词施加按作用域上界,使得各作用域中的字面匹配优先到达模型;排在该上界之后的匹配不论字面还是语义都不会被提供给模型,仍可能被漏掉)。Indexer、Status 与 Extraction 保持有上限。
源码路径速查
| 关注点 | 位置 |
|---|---|
| 设计文档本体 | docs/design/2026-08-08-native-memory-recall-reliability.md |
| 关联的有界候选设计 | docs/design/2026-08-09-bounded-memory-recall-candidates.md |
召回实现(常量、打分器、onFastResult、resolveRelevantAutoMemoryPromptForQuery) | packages/core/src/memory/recall.ts |
初始预算与消费编排(INITIAL_MEMORY_RECALL_WAIT_MS、beginManagedAutoMemoryRecall、tryConsumeMemoryPrefetch) | packages/core/src/core/client.ts / packages/core/src/core/client.ts |
| 扫描延迟测量 | packages/core/src/memory/recall-scan-latency.test.ts |
| 交付评测 | packages/core/src/memory/recall-delivery-eval.test.ts |
| 质量评测(含冻结旧打分器) | packages/core/src/memory/recall-eval.test.ts |
| 行为契约测试 | packages/core/src/memory/recall.test.ts |
| 评测语料 | packages/core/src/memory/fixtures/auto-memory-recall-eval.json |
这套"单一生命周期 + 确定性快速交付 + 模型主导精炼"的组合,以最小机制(一个回调、一个排除集合、一个内部预算常量)解决了无工具轮次上记忆交付的时序缺口,并用三层测试把"选择正确"与"及时到达"两个问题分别钉死——这是值得在同类记忆系统中复用的设计范式。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考