专题篇章 · 拆开一只生产级 Coding Agent

从文件变更到混合排序

追踪 FTS、向量检索、时间衰减和 MMR 重排组成的记忆召回流水线

本页解决的问题

先给结论

「从文件变更到混合排序」要解决的关键问题是什么?

追踪 FTS、向量检索、时间衰减和 MMR 重排组成的记忆召回流水线

判断标准

让这个结论先证明自己值得留下。 把这一页当成决策工具,而不是需要背下来的定义。把概念连到一个真实任务、一个可观察结果,以及一个能改变你判断的失败上。

下一步

写下一个问题:试完这个方法后,你能用什么证据回答它?

常见误区

结论听起来很完整,却没有检查最关键的假设。

课程目标按真实顺序复述 sync-on-search、FTS、embedding、KNN、合并加权与 MMR,并能说明 embedding 失败和 MMR 关闭时各自发生什么。
核心视觉 · 完整检索路径
Watcher 脏路径create · modify · remove sync-on-searchreindex_file / delete_path 用户查询query FTS5 BM25始终可用 · 关键词候选 Embedding + KNN可用时走 sqlite-vec embedding 失败FTS-only 合并与排序decay × source weight × access boostMMR 可选,随后 truncate SearchResultmax_results
教学化结构图:失败分支回到 FTS-only;MMR 是 opt-in,默认不会重排。
流水线中的真实机制
01 · SYNC

查询前同步

MemoryFileWatcher 累积变化的 Markdown 路径。backend 在 search 开始时重新索引新增或修改文件,并删除已移除文件的旧 chunk。

02 · FTS

BM25 候选

先执行普通 FTS,再补充 global 与 workspace 来源查询,降低 session 数量过多造成的挤出。

03 · VECTOR

可选 KNN

仅在 sqlite-vec 与 provider 可用时嵌入 query。embedding 报错会记录 warning,并传入 None 继续 FTS-only。

04 · SCORE

归一化与合并

BM25 分数与向量 L2 距离分别归一化。双路命中时按权重合并,同时保证结果不低于该 chunk 的 FTS 分数。

05 · WEIGHT

时间与来源

session 按半衰期指数衰减,global 与 workspace 视为 evergreen。随后乘 source weight 与适度的 access boost。

06 · DIVERSITY

可选 MMR

开启后按相关性与 snippet 的 Jaccard 差异做贪心重排。最后截断到 max_results

两个容易误读的开关

Embedding 失败

向量路径停止,FTS 结果仍进入 hybrid_search_merge。页面或调用方无需把 embedding 故障当成整次搜索失败。

fallback = FTS-only

MMR 默认状态

MmrConfig::default() 设置 enabled: falselambda: 0.7。0.7 只在显式开启 MMR 后生效。

enabled = false
真实源码证据
crates/codegen/xai-grok-memory/src/search.rs · 第 146 至 190 行节选
pub async fn hybrid_search(
    index: &MemoryIndex,
    embedding_provider: Option<&dyn EmbeddingProvider>,
    query: &str,
    config: &MemorySearchConfig,
) -> Result<Vec<SearchResult>, Box<dyn std::error::Error>> {
    let candidate_limit = config.max_results * 3;
    let mut fts_results =
        index.search_fts(query, candidate_limit).unwrap_or_default();
    /* 补充 evergreen FTS 候选的源码在此处 */

    let vec_available = index.vec_available();
    let query_embedding = if vec_available {
        if let Some(provider) = embedding_provider {
            match provider.embed_batch(&[query]).await {
                Ok(embeddings) if !embeddings.is_empty() =>
                    Some(embeddings.into_iter().next().unwrap()),
                Ok(_) => None,
                Err(e) => {
                    tracing::warn!(error = %e,
                        "embedding query failed, falling back to FTS-only");
                    None
                }
            }
        } else { None }
    } else { None };

    hybrid_search_merge(index, fts_results, query_embedding.as_deref(), config)
}
crates/codegen/xai-grok-memory/src/backend.rs:search() 中执行 watcher 同步与查询 crates/codegen/xai-grok-memory/src/watcher.rs:MemoryFileWatcher crates/codegen/xai-grok-memory/src/mmr.rs:mmr_rerank crates/codegen/xai-grok-config-types/src/memory.rs:MmrConfig 默认值
源码快照说明:依据本地仓库 grok-build-main,核对日期 2026-07-17。代码块保留真实函数与分支,唯一的折叠处已用注释说明;流程图明确标为教学化结构图。
课堂练习
06

推演一次降级查询

假设 watcher 发现一个文件被修改,query embedding 随后失败,MMR 保持默认配置。请按顺序写出索引更新、候选生成、加权排序与最终截断,并标出没有发生的两个步骤。

Takeaway:记忆检索具备可降级与可同步两条关键保障。FTS 始终提供基础候选,向量搜索按可用性增强;时间衰减和来源权重调整排序;MMR 需要显式开启;watcher 让外部 Markdown 修改在下一次查询前进入索引。

「核心视觉 · 完整检索路径」为什么能找到相关内容

「MemoryFileWatcher 累积变化的 Markdown 路径。backend 在 search 开始时重新索引新增或修改文件,并删除已移除文件的旧 chunk」把检索问题从“把资料存起来”推进到“怎样找到真正相关的资料”。这一步决定了 RAG、推荐和以图搜图最后交给模型的输入质量。

相似度不是答案,召回之后还要核对

在「先执行普通 FTS,再补充 global 与 workspace 来源查询,降低 session 数量过多造成的挤出」对应的流程里,Embedding 负责把对象放到可比较的语义空间,近邻索引负责减少搜索范围,最终的回答仍然依赖召回片段是否覆盖问题、距离指标是否合适,以及内容有没有过期。

先区分找得到和找得准

把「假设 watcher 发现一个文件被修改,query embedding 随后失败,MMR 保持默认配置。请按顺序写出索引更新、候选生成、加权排序与最终截断,并标出没有发生的两个步骤」落成一次小测试:准备几条有明确答案的查询,记录召回的相关性、遗漏和无关结果,再决定是否需要换切分方式、索引或重排。

从「核心视觉 · 完整检索路径」走到「查询前同步」

「核心视觉 · 完整检索路径」先把问题落在「Watcher 脏路径 create · modify · remove sync-on-search reindex_file / delete_path 用户查询 query FTS5 BM25 始终可用 · 关键词候选 Embedding + KNN 可用时走 sqlite-vec embedding 失败 FTS-only 合并与排序 decay × source weight × access boost…」上;到了「查询前同步」,讨论继续推进到「MemoryFileWatcher 累积变化的 Markdown 路径。backend 在 search 开始时重新索引新增或修改文件,并删除已移除文件的旧 chunk」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。

把这条判断带到下一个场景

检索系统也可以这样看:先确定什么算相关,再观察召回是否覆盖问题,最后检查排序、切分和时效性有没有把真正有用的内容挤出去。

  • 「核心视觉 · 完整检索路径」:Watcher 脏路径 create · modify · remove sync-on-search reindex_file / delete_path 用户查询 query FTS5 BM25 始终可用 · 关键词候选 Embedding + KNN 可用时走 sqlite-vec embedding 失败 FTS-only 合并与排序 decay × source weight × access boost…
  • 「查询前同步」:MemoryFileWatcher 累积变化的 Markdown 路径。backend 在 search 开始时重新索引新增或修改文件,并删除已移除文件的旧 chunk
  • 「推演一次降级查询」:假设 watcher 发现一个文件被修改,query embedding 随后失败,MMR 保持默认配置。请按顺序写出索引更新、候选生成、加权排序与最终截断,并标出没有发生的两个步骤

最后的「推演一次降级查询」把讨论落到「假设 watcher 发现一个文件被修改,query embedding 随后失败,MMR 保持默认配置。请按顺序写出索引更新、候选生成、加权排序与最终截断,并标出没有发生的两个步骤」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。

标记为已学完 阅读进度会自动记录
← 上一篇下一篇 →

继续阅读

同一条线上的下一篇。

文章讨论

读到这里,留下一个判断。

把刚想明白的地方、还没想通的问题,留给下一位一起学习的人。

正在讨论 从文件变更到混合排序 拆开一只生产级 Coding Agent
3条讨论文章讨论 · 与共学社区同步
在共学社区查看
AM
Asha Morgan内容编辑
观点实践记录

我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。

文章讨论7 有帮助
LH
Lin Harper独立开发者
观点观点

读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。

文章讨论5 有帮助
KM
Kiki Moore产品运营
问题问题

如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。

文章讨论4 有帮助