ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从BM25到RRF向量融合:agent-memory本地排序检索的代码实现原理

从BM25到RRF向量融合:agent-memory本地排序检索的代码实现原理 从BM25到RRF向量融合agent-memory本地排序检索的代码实现原理【免费下载链接】agent-memoryLong-term memory runtime for AI agents — plain Markdown as the source of truth, local ranked retrieval, and an independent sleep-time Manage layer. Claude Code and Codex share one store. No API key.项目地址: https://gitcode.com/gh_mirrors/age/agent-memoryagent-memory 是一个面向 AI Agent 的本地长期记忆运行时以纯 Markdown 文件为唯一事实来源用 SQLite 建立可重建的索引在不联网、不要 API key 的前提下完成BM25 关键词检索与可选的向量融合排序RRF。本文将带你读懂它从 BM25 到 RRF 向量融合的完整排序链路——这也是大多数本地检索引擎的核心难题。1️⃣ 先搞懂问题为什么记忆检索要两条腿走路BM25 是最经典的词频-逆文档频率排序算法擅长精确词命中向量检索则擅长语义相近但用词不同。只用 BM25会漏掉同义表达只用向量则慢且对专有名词不敏感。agent-memory 的取舍很务实见 README.mdBM25 是基线低延迟、纯本地、默认开启向量是可选插件装上才启用且两路结果用RRFReciprocal Rank Fusion倒数排名融合合并任何一条路失效都有兜底最坏情况 agent 还能lsgrep直接翻目录树。整条读路径不调用任何模型、不跨网络索引本身是删了也能重建的缓存database.py 第一行注释就写明了这一点。2️⃣ 第一路BM25 检索FTS5 分块分块给索引一个检索面但召回单位仍是整文件记忆写入时chunking.py 会把每条记忆切分一段abstract 块一句话摘要正文按 Markdown 标题切分成若干body 块每块带 heading anchor 锚点。注意一个精妙的设计分块只为索引服务召回返回的单位始终是整个 Markdown 文件。agent 拿到的是路径和锚点再按需mem read name --level outline逐层深入——每深入一层成本递增一个数量级每一层都是可以停下的地方。FTS5 倒排索引与bm25()排名索引建在 SQLite 的 FTS5 虚拟表上database.py#L45-L62chunks表存当前有效记忆的块默认检索面history表存已失效记忆的块只有--as-of时间旅行查询才会读它。查询时search_index.py 先把查询拆成小写字母数字 token用OR连接逐词精确匹配再交给 FTS5 内置的bm25()函数打分SELECT name, kind, anchor, heading, bm25(chunks) AS rank FROM chunks WHERE chunks MATCH ? ORDER BY rank LIMIT ?BM25 分数越小负得越多表示越相关代码里取relevance -rank翻转成越大越相关返回最多pool条候选search_index.py#L87-L126。3️⃣ 第二路本地向量召回可选在 store 的config.toml中打开vector_enabled true并安装可选依赖后vector_index.py 登场嵌入模型默认BAAI/bge-small-en-v1.5通过 FastEmbed/ONNX 在本地跑不依赖任何云端 APIembeddings.py存储每个块的向量以BLOB形式存进 SQLite 的vector_chunks表随文件内容哈希增量更新检索暴力精确余弦brute-force cosine——逐块算 query 向量与块向量的余弦相似度降序取前pool条vector_index.py#L75-L96。没有向量数据库、没有近似最近邻ANN黑盒数据量是记忆条数级别精确搜索足够快且可解释、可重建。4️⃣ 核心RRF 倒数排名融合怎么合并两路结果两路各自返回排序后的候选列表但 BM25 分数和余弦分数的量纲完全不同不能直接相加。RRF 的妙处在于它只看名次不看分数每个候选的融合分 它出现在各路结果中的1 / (K 名次)之和其中K 60。实现见 recall.py 的fuse_candidatesK 60经验常数让头部名次1、2、3…之间的差距远大于尾部同时避免第 1 名一家独大——这正是 RRF 论文选择的值按块身份去重以(name, kind, anchor, heading)四元组为键同一块在单路结果里重复出现只计一次归一化到 0..1两路都命中第 1 名时理论满分是2 / (601)用它作除数融合分relevance就落在稳定的 0~1 区间可和后继的权重、新鲜度因子相乘稳定排序分数相同时按身份元组排序结果可复现。一句话总结 RRF两个评委各排各的名次名次靠前的加成大两路都靠前的是真答案。5️⃣ 最终排序相关性 × 权重 × 新鲜度融合完只是候选池。Recall 主流程 还做三步① 资格过滤先于相关性。作用域scope目录前缀、有效期valid_from/invalid_at先筛一遍——该不该出现比有多相关更优先。② 摘要块加权。每份文件取得分最高的块但命中 abstract 块的相关性乘2.0命中 body 块乘1.0config.py#L51-L52——一句话摘要命中的记忆通常更值得展示。③ 新鲜度指数衰减。最终分 relevance × weight × recency其中recency 0.5 ^ (距今天数 / 180)半衰期 180 天更新半年前的记忆新鲜度剩一半再久也保底 0.25recall.py#L216-L221。这样旧但重要的记忆不会被永久沉底而刚更新的内容会浮上来。取前limit条默认 8并把命中写入访问日志——日志是后续 Manage 层按价值遗忘的依据。6️⃣ 实测数据召回率与延迟的权衡README 给出了固定 120 条查询验收集上的量化结果指标仅 BM25BM25 向量 RRFRecall579.0%86.6%7.6pp中位检索延迟5.1ms139.2ms代价很直白向量嵌入 暴力余弦把中位延迟抬高了约 27 倍换来 7.6 个百分点的头部召回率。因此 agent-memory 把向量检索定位为可选增强而非默认——小库低延迟用 BM25 就够库大、查询口语化再打开。7️⃣ 如何动手开启 RRF 向量融合三步即可体验完整链路完整说明见 README.md 的 Optional vector recall index 一节uv sync --extra vector安装嵌入依赖在 store 的config.toml中设置[index] vector_enabled truemem --store /path/to/store rebuild重建索引再mem recall 你的问题。想只走最快的 BM25 基线保持vector_enabled false即可读路径零额外开销。8️⃣ 小结agent-memory 的检索栈可以浓缩为四句话BM25/FTS5 打底倒排索引 bm25()排名毫秒级、纯本地向量是插件FastEmbed 本地嵌入 SQLite BLOB 精确余弦没有黑盒RRF 只融合名次K60、按块身份去重、归一化到 0~1量纲问题自然消失业务规则最后出场资格 → 摘要加权 → 180 天半衰期新鲜度分数 相关性 × 权重 × 新鲜度。所有索引都是可rm -rf的缓存Markdown 文件才是记忆本身——这套从 BM25 到 RRF 向量融合的实现正是既要检索精度、又保持可读可迁移的一个务实范本。【免费下载链接】agent-memoryLong-term memory runtime for AI agents — plain Markdown as the source of truth, local ranked retrieval, and an independent sleep-time Manage layer. Claude Code and Codex share one store. No API key.项目地址: https://gitcode.com/gh_mirrors/age/agent-memory创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表