news 2026/10/21 23:21:56

agent-memory 召回路装配内收:CompositeStorage 工厂化装配 Recaller 的架构设计与循环依赖破解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
agent-memory 召回路装配内收:CompositeStorage 工厂化装配 Recaller 的架构设计与循环依赖破解
  • 人工智能
  • AI Agent
  • Agent 记忆
  • RAG
  • MCP 服务

【免费下载链接】agent-memory

Memory 是一款面向 AI 智能体的长期记忆模块,为运行在 openJiuwen 框架上的智能体提供记忆提取、存储、检索与迁移能力。

项目地址:https://gitcode.com/openJiuwen/agent-memory
点击查看免费下载

本文以 agent-memory 项目特性文档 F06-composite-recaller-assembly.md 为骨架,结合jiuwen_memory/storage与jiuwen_memory/retrieval的当前源码实现、默认装配配置与测试基线,完整讲解「多路 Recaller 召回路装配从 Retriever 工厂内收到 Storage/StoreManager 工厂」这一设计决策。读完你将掌握:七个*_recaller选择键与三个能力开关的配置语义、具名/匿名构建下循环依赖的双预注册破解方案、PipelineRetriever不再持有召回路后的委托模型,以及手工接线与构建期 fail-fast 的测试验证方式。

背景:统一 Storage 落地后的装配错位

在 F05-unified-storage-design 统一 Storage 落地之后,PipelineRetriever的生产装配仍按"旧世界"做事:retriever 工厂自己组装 keyword/vector/graph/L0/L1 各路 Recaller,再通过isinstance(storage, CompositeStorage)判断后调用bind_recallers把召回路塞回 storage。这种装配方式带来三个结构性问题:

  1. 责任错位:recall/get/rank 三条首选路径的执行都在 Storage 内,唯独召回路的组装留在 Retriever 工厂——CompositeStorage的能力开关(如vector_enabled)要由别人替它解释,能力声明与实际装配脱节。
  2. 非 Composite 实现被拖进无关概念:一体化 Storage 自带检索路径,根本不需要 Recaller;旧装配却仍先组装一整组 recaller,再发现无处可绑,白白浪费构建成本并造成概念污染。
  3. 构造签名被迫膨胀:PipelineRetriever构造签名被迫携带recallers参数,并在构造期做「无 storage 就用 unit_reader.kv 临时拼一个 CompositeStorage 再绑 recaller」的兼容逻辑,把一个装配期问题泄漏进运行期对象。

核心决策:五条设计主线

针对上述问题,F06 确立了五条相互咬合的决策,其最终形态在当前仓库代码中完整落地。

决策 1:召回路装配内收到 Storage 工厂

@StorageProducer.register("composite")的_build按配置组装 recaller。在 F07 拆分后,该装配入口位于 composite_store_manager.py 的from_config,召回路组装逻辑收敛在 composite_domain_store.py 的_assemble_recallers与for_manager:

  • 能力开关:vector_enabled/graph_enabled/layers_index_enabled,经config.get回退 globals,属于跨切面意图开关;
  • recaller 选择键:keyword_recaller/vector_recaller/graph_recaller/keyword_l0_recaller/keyword_l1_recaller/vector_l0_recaller/vector_l1_recaller,共七个,落在storage.default.params(F07 后进一步下移到domain_stores.default)。

这组键相应从defaults.py的retriever.default.params移到了 storage 侧,retriever段只保留本算子私有调参(over_fetch_factor、recall_max、rerank_max等)。默认装配下会同步组装出 7 路 recaller。

决策 2:构建期同步组装 + 双预注册打破循环

recaller builder 内部会StorageProducer.resolve(config)回取本 Storage 实例,而build_named没有「构建中」检测——如果装配期直接同步组装,会形成「装配 CompositeStorage → 组装 recaller → recaller 回取 Storage → 再建一个 CompositeStorage」的无限递归。破解方案是_build先把构建中的实例预注册进具名缓存再组装,且区分具名/匿名两条路径。

决策 3:PipelineRetriever 不再持有召回路

构造签名删除recallers参数与recallers属性;retrieve只按storage.preferred_retrieval_pipeline()委托recall/recall_and_get/retrieve。非 Composite 实现不再被拖进 recaller 概念。storage=None回退保留为CompositeStorage(kv=unit_reader.kv)(无召回路,仅供只走点读/短路路径的调用)。

决策 4:bind_recallers 保留为手工/测试接线口

recaller 构造需要 storage 实例(如KeywordRecaller(storage)),手工接线仍是「先建 storage、再建 recaller、再 bind」;绑定后惰性 loader 作废。重绑守卫不变:同一实例不允许绑定两套不同 recaller。

决策 5:模块依赖方向不破

composite_storage.py只在_assemble_recallers函数体内惰性from jiuwen_memory.retrieval.recaller import RecallerProducer,模块层面 storage 仍不导入 retrieval。F07 E 组把 Recaller 整体移入 storage 数据面后,这条延迟导入连同它绕开的循环一起消失——这正是当前代码中Recaller契约与实现已经位于 recaller.py 的原因。

配置详解:七个选择键与三个能力开关

召回路装配的完整默认配置位于 defaults.py。F07 参数归位后,数据面的全部装配参数住在store_manager.<inst>.params.domain_stores.<name>,默认形状如下:

globals: schema_enabled: false vector_enabled: true graph_enabled: true rerank_enabled: true embedder_dim: 64 chunk_size: 120 store_manager: default # 全局唯一 StoreManager 指名 store_manager: default: target: composite params: domain_stores: default: # 数据面 default 必建;命名实例 overlay 在它之上 kv_store: default preferred_retrieval_pipeline: recall_get_rank keyword_recaller: keyword vector_recaller: vector graph_recaller: graph keyword_l0_recaller: keyword_l0 keyword_l1_recaller: keyword_l1 vector_l0_recaller: vector_l0 vector_l1_recaller: vector_l1

下表给出全部装配参数的语义:

参数取值/默认语义
kv_store端口名字符串,默认default数据面真源 KV 端口名,与其余消费方同一套选择键机制,运行期不硬编码"default"
preferred_retrieval_pipelinerecall_get_rank/recall_and_get_rank/retrieve数据面实例级稳定的首选检索路径,见 retrieval.py 的RetrievalPipeline枚举
keyword_recallerkeyword关键词/全文通道(L2 content),包一个FulltextStore
vector_recallervector向量语义通道(L2 content chunk),包一个VectorStore
graph_recallergraph图遍历通道:关键词种子 → 1 跳邻居候选
keyword_l0_recallerkeyword_l0预生成概要分表的全文倒排召回
keyword_l1_recallerkeyword_l1预生成片段分表的全文倒排召回
vector_l0_recallervector_l0概要整段向量分表召回
vector_l1_recallervector_l1片段整段向量分表召回

recaller 具名实例声明在recaller:顶层命名空间(target 与注册名在 F07 迁移后逐字不变,YAML 配置零迁移):

recaller: keyword: { target: keyword } keyword_l0: { target: keyword_l0 } keyword_l1: { target: keyword_l1 } vector: { target: vector, params: { min_similarity: 0.0 } } vector_l0: { target: vector_l0 } vector_l1: { target: vector_l1 } graph: { target: graph }

三个能力开关的生效逻辑(见_assemble_recallers):

  • vector_enabled=false时不装vector_recaller/vector_l0_recaller/vector_l1_recaller;
  • graph_enabled=false时不装graph_recaller;
  • layers_index_enabled=false时不装 L0/L1 四路(keyword_l0/l1、vector_l0/l1)。

注意一个易踩的静默退化陷阱:RecallerProducer.dep读的是config.params(直读不回退 globals),因此domain_stores的命名 entry 必须先 overlay 在defaultentry 之上再传入。若命名 entry 漏掉*_recaller键,dep会落到cls.build(default, {}, ctx)匿名新建一套不共享、params 为空的 recaller——装配不报错,读写悄悄用上两套实例,测试 test_named_domain_stores.py 中test_named_entry_shares_recaller_instance_with_default用对象身份(is)断言而非通道名来锁死该退化。

源码级原理:循环依赖的具名/匿名双预注册

Factory.build_named(见 factory.py)没有「构建中」检测:命中缓存即返回共享实例,未命中则新建并入缓存。这意味着装配期直接同步组装 recaller 时,recaller builder 内部的StorageProducer.resolve(config)会触发再建一个 CompositeStorage,形成递归。_assemble_recallers按构建形态拆成两条分支:

具名构建(config.name非空):CompositeStoreManager.from_config先StoreManagerProducer.put(config.name, manager)把构建中的实例预注册进具名缓存;recaller 命名空间下声明的具名实例(recaller.keyword等)带store_manager: <name>字符串引用,RecallerProducer.dep走build_named命中缓存,StoreManagerProducer.resolve在 builder 内回取成功,打破循环。

匿名构建(无config.name,如IndexBuilderProducer调StorageProducer.resolve落到第三分支构建):无缓存键,dep缺省会回落cls.build(default, {}, ctx),再次触发 recaller builder 用空 params 走StorageProducer.resolve落第三分支再建一个匿名 CompositeStorage → 递归。破解方案是用合成名__anon_store_manager_{id(storage)}__(id保证唯一)预注册本实例,改走RecallerProducer.build(target, {"store_manager": synthetic_name}, ctx)直接注入 storage 引用,让 builder 内resolve走第一分支(cls.dep)命中合成名缓存。当前实现见 composite_domain_store.py_assemble_recallers的synthetic_name分支。

装配错误(选择键指向未注册实现、必填插件缺失等)在构建期 fail-fast 暴露,不拖到首次召回。这正是测试 test_storage_factory_wiring.py 中test_recaller_assembly_error_surfaces_at_build_time锁住的行为:把vector_recaller指向"nonexistent_impl"后,StoreManagerProducer.build_named("default", context)在构建期直接抛ValidationError。

PipelineRetriever 的委托模型

当前 pipeline_retriever.py 的构造签名已不含recallers,只收parser/fuser/discloser/unit_reader/reranker与检索调参,以及 keyword-only 的domain_store(F07 后 Retriever 运行期只持数据面)。_build工厂只做两件事:经StoreManagerProducer.resolve取全局 manager,再用manager.domain_store(resolve_name(config, "domain_store"))取数据面注入。

retrieve的召回段按self._domain.preferred_retrieval_pipeline()三选一:

首选路径行为
RECALL_GET_RANK先recall(多路并行召回 ScoredUnit)→ 再get物化真源 + 复核 → fuser 融合
RECALL_AND_GET_RANK数据面内一次完成recall_and_get,Retriever 侧复核后直接融合
RETRIEVE数据面内完成 recall + get + rank(传入 fuser),Retriever 只收已融合候选

多路并行与结果合并由 composite_domain_store.py 的recall负责:按channels过滤已装配 recaller,ThreadPoolExecutor并行提交,逐路捕获异常记入ChannelError,全路失败才抛StorageRetrievalError(部分成功语义)。每个 Recaller 对应一条召回通道,互不感知其他通道。

手工接线:bind_recallers 的测试与定制路径

bind_recallers仍是公开的手工/测试接线口(F06 决策 4 不变),装配路径改由for_manager内部调用。测试夹具 conftest.py 的make_world就是典型手工接线形态:

storage = CompositeStoreManager(kv=kv, vector=vector, fulltext=fulltext) domain_store = CompositeDomainStore( manager=storage, preferred_pipeline=RetrievalPipeline.RECALL_GET_RANK ) keyword = KeywordRecaller(storage) vector_recaller = VectorRecaller(storage) domain_store.bind_recallers([keyword, vector_recaller]) # 先建 storage → 再建 recaller → 再 bind storage.bind_domain_store(domain_store)

自定义召回路集成测试(如 test_pipeline_retriever.py 的test_default_config_attaches_l0_l1_recallers、test_index_contract.py)也走这条路径:手工构造CompositeStoreManager+CompositeDomainStore,bind_recallers注入故障注入型/固定候选型 recaller 验证通道级降级、并行性与 L0/L1 装配。

验证体系

F06 的验证覆盖在仓库中完整对应:

  • test_storage_factory_wiring.py:默认装配下retriever.storage is storage且storage.recallers同步组装出 7 路;globals 关闭三个能力开关后只剩 keyword 一路;选择键指向未注册实现时构建期抛ValidationError。
  • test_index_builder.py:test_fulltext_factory_*/test_vector_factory_*/test_unified_factory_*覆盖匿名 CompositeStorage 构建(经StorageProducer.resolve第三分支),合成名预注册打破循环、不触发递归。
  • test_storage_pipelines.py、test_recallers.py、test_pipeline_retriever.py(含keyword_recaller覆盖键移到 storage 段的用例)、test_index_contract.py 全部适配并通过;conftest.py的make_world改为 bind 手工接线。
  • test_named_domain_stores.py:domain_stores段此前全仓零覆盖,F07 补上 11 例,其中test_named_entry_shares_recaller_instance_with_default锁死「命名 entry 继承 default 的 recaller 实例」这一静默退化。

F06 文档记录的测试基线为.venv-wsl/bin/python -m pytest tests/unit tests/integration:1318 passed / 82 skipped;4 failed 均为 HEAD 既有问题(2 个 bge_m3 缺 torch;2 个 entity_linker 测试间干扰)。F07 演进后基线为pytest tests/unit/1978 passed / 8 skipped、pytest tests/integration/48 passed / 61 skipped。

拒绝的方案与取舍理由

F06 明确记录了被否定的备选方案,理解它们有助于把握内收设计的分寸:

  • 装配期同步构建(仅put预注册):具名路径可行,但匿名构建仍会经 recaller builder 回取storage.default递归构建默认实例。最终采纳时补上合成名预注册 + 直接RecallerProducer.build注入 storage 引用覆盖匿名路径。
  • 首次召回时惰性物化:能彻底回避循环,但装配错误推迟到首次召回才暴露,违背「配置错误尽早暴露」原则;且增加首次召回延迟与状态管理复杂度。
  • 保留 retriever 侧装配、仅按isinstance跳过非 Composite:责任依然错位,开关仍由 retriever 工厂替 storage 解释,治标不治本。
  • 让 recaller 不再依赖 Storage(改注入裸端口):可彻底消除循环,但要改全部 recaller 的构造契约与测试,且违背「上层握共享 Storage 入口、勿构造期握死裸端口」的本地约束(RoutingStorage 惰性代理依赖这一点)。
  • 召回路用命名空间扫描替代显式选择键:失去按配置裁剪/替换单路的能力,且会把未接线入库的具名实例也拉进来。

已知遗留与后续演进(F07)

F06 的已知遗留当前依旧成立:

  • recaller 装配的运行时错误(如距离型度量向量库被VectorRecaller在首次召回时拒绝、向量库连接不通等)仍推迟到首次召回才暴露;装配期 fail-fast 只覆盖选择键指向未注册实现、必填插件缺失等配置类错误,运行时错误依赖冒烟/健康检查覆盖。注:距离型度量的拒绝实际发生在构造期——vector_recaller.py 在__init__中检查score_higher_is_better()并抛ValidationError。
  • 匿名构建用id(storage)派生合成名预注册进_instances缓存,装配后该缓存条目无人再引用(recaller 已持直接引用),属无害死条目;Factory.reset_all在每次装配前清空。
  • 用户配置迁移:写在retriever.*.params的*_recaller覆盖键不再生效,需迁移到storage.*.params(F07 后进一步迁至domain_stores.<name>);globals 里的能力开关不受影响。

后续演进见 F07-storage-manager-domain-store-split.md:本文的召回装配链路整体保留(仍由 manager 工厂末尾调_assemble_recallers),仅符号随拆分更名(CompositeStorage→CompositeStoreManager/CompositeDomainStore、StorageProducer.resolve→StoreManagerProducer.resolve、合成名__anon_storage_{id}__→__anon_store_manager_{id}__)。F07 E 组进一步把Recaller契约与实现移入storage/domain_store_impl/,_assemble_recallers的调用点下沉进CompositeDomainStore.for_manager,本文决策 5 的惰性导入随之删除。值得强调的是,F06 拒绝方案里「让 recaller 不再依赖 Storage(改注入裸端口)」始终未被采纳——E 组走的是相反方向:不是把 recaller 从 storage 摘出去,而是把它收进来。

  • 人工智能
  • AI Agent
  • Agent 记忆
  • RAG
  • MCP 服务

【免费下载链接】agent-memory

Memory 是一款面向 AI 智能体的长期记忆模块,为运行在 openJiuwen 框架上的智能体提供记忆提取、存储、检索与迁移能力。

项目地址:https://gitcode.com/openJiuwen/agent-memory
点击查看免费下载

相关推荐

上一篇:Aptos Core 二进制发布与 cargo-binstall 集成指南:让用户免编译安装预编译二进制
下一篇:Epic Stack 导入路径方案演进:用 package.json 的 "imports" 字段配置 前缀路径别名

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 11:37:45

MXNet图像数据生成:从目录结构到record文件的完整工程实践

1. 为什么“mxnet 图像训练数据生成”不是个简单配置问题&#xff0c;而是一整套数据工程闭环你打开MXNet文档&#xff0c;翻到mx.io.ImageRecordIter或gluon.data.vision.transforms那几页&#xff0c;照着示例改了两行路径、调了三个参数&#xff0c;跑起来报错&#xff1a;O…

作者头像 李华
网站建设 2026/10/21 11:36:03

OpenPose核心解读:PAF如何解决多人姿态估计的关键点关联难题

姿态估计这两年其实已经进入“柴米油盐”阶段了&#xff0c;大部分做视觉的工程师手里都有现成的人体关键点方案&#xff0c;但回到那篇让多人实时2D姿态估计真正落地的工作&#xff0c;几乎所有人绕不开同一个名字&#xff1a;OpenPose。我最早接触它的时候&#xff0c;脑子里…

作者头像 李华
网站建设 2026/10/22 7:02:14

C++引用深入解析:本质、与指针的区别及避坑实践

1. 引用的本质&#xff1a;它不是指针的语法糖&#xff0c;而是对象的第二个名字很多从C语言转过来学C的朋友&#xff0c;在第一次接触引用时&#xff0c;都会产生一种感觉&#xff1a;这不就是带解引用的指针吗&#xff1f;我的看法是——理解可以这样起步&#xff0c;但如果停…

作者头像 李华