- 人工智能
- AI Agent
- Agent 记忆
- RAG
- MCP 服务
【免费下载链接】agent-memory
Memory 是一款面向 AI 智能体的长期记忆模块,为运行在 openJiuwen 框架上的智能体提供记忆提取、存储、检索与迁移能力。
本文以 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。这种装配方式带来三个结构性问题:
- 责任错位:recall/get/rank 三条首选路径的执行都在 Storage 内,唯独召回路的组装留在 Retriever 工厂——
CompositeStorage的能力开关(如vector_enabled)要由别人替它解释,能力声明与实际装配脱节。 - 非 Composite 实现被拖进无关概念:一体化 Storage 自带检索路径,根本不需要 Recaller;旧装配却仍先组装一整组 recaller,再发现无处可绑,白白浪费构建成本并造成概念污染。
- 构造签名被迫膨胀:
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_pipeline | recall_get_rank/recall_and_get_rank/retrieve | 数据面实例级稳定的首选检索路径,见 retrieval.py 的RetrievalPipeline枚举 |
keyword_recaller | keyword | 关键词/全文通道(L2 content),包一个FulltextStore |
vector_recaller | vector | 向量语义通道(L2 content chunk),包一个VectorStore |
graph_recaller | graph | 图遍历通道:关键词种子 → 1 跳邻居候选 |
keyword_l0_recaller | keyword_l0 | 预生成概要分表的全文倒排召回 |
keyword_l1_recaller | keyword_l1 | 预生成片段分表的全文倒排召回 |
vector_l0_recaller | vector_l0 | 概要整段向量分表召回 |
vector_l1_recaller | vector_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 框架上的智能体提供记忆提取、存储、检索与迁移能力。
相关推荐
OpenStock 使用指南:免费实时行情 + 价格提醒,如何 10 分钟搭好盯盘流程
OpenStock 使用指南:免费实时行情 + 价格提醒,如何 10 分钟搭好盯盘流程 OpenStock 是一款开源股票工具,免费实时行情、板块热力图与自定义
金融科技前端后端AI 应用Anki-Android 模块化架构中的 `:anki-common`:打破循环依赖的共享层设计详解
Anki Android 模块化架构中的 :anki common :打破循环依赖的共享层设计详解 导读 本文以 Anki Android(AnkiDroid)
移动开发教育agent-memory 检索层实现规约深度解析:QueryParser / Recaller / Fuser / Discloser / Retriever 的装配、语义与取舍
agent memory 检索层实现规约深度解析:QueryParser / Recaller / Fuser / Discloser / Retriever
人工智能AI AgentAgent 记忆RAGMCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考