多模态长期记忆不止于检索:从“找得到”到“算得出来”的落地架构
聊多模态长期记忆很多时候大家的第一反应都是“怎么把东西找回来”。图像找相似、视频捞片段、语音转文本之后再检索流程看起来很完整但真把系统扔到业务里用一个月你会发现最尴尬的问题根本不是“找不到”而是“算不出来”。用户想知道的不只是“哪段监控记录了那位顾客”而是“这位顾客过去三个月是不是越来越倾向晚上来”不只是“员工培训后有没有犯错视频”而是“培训后一周内的操作失误率到底降没降”。这种问题传统检索式记忆给不出答案。我去年做门店运营分析 Agent 时被这个问题卡了很久后来把记忆层整体重写项目代号叫 adammAdaptive Multi-Modal Decision Memory Module。这套东西的核心认知只有一句话多模态记忆不只要“找得到”还要“算得出来”。这篇就把 adamm 的架构思路、关键模块和我踩过的坑完整梳理一遍给正在做多模态 Agent、多模态记忆系统的朋友一个可直接参考的落地框架。1. 多模态记忆的尴尬检索能力很强分析能力为零1.1 传统“检索式记忆”只能给你素材不能给你答案大部分多模态记忆系统做的是这么一件事把视频、音频、图片、文本全部过一遍编码器得到 embedding存进向量库查询时用 query 的 embedding 去 Top-K 召回。这种范式在“找相似”场景下非常好用比如“有没有和这张商品图风格类似的陈列照片”——召回结果直接就是答案。可一旦问题变成分析型这套方式就失效了。用户问“过去三个月进店高峰期有没有变化”情感上我们希望系统直接给出“从晚上7点变成晚上8点整体后移约1小时建议关注夜宵档”。但向量检索返回的是 50 段高相似度的视频切片每段里都有顾客进店的画面却没有一段能代表“高峰期的整体变化”。检索把“证据”和“答案”混为一谈默认只要材料够了答案会自动浮现但实际并不会。这个问题在纯文本 RAG 里相对容易掩盖因为文本本身有概括性检索到几篇文档还能拼凑观点。多模态数据不一样一段视频包含大量瞬时信息截取哪一帧、截多长时间、和哪条音频对齐都会影响语义。检索模块如果只做相似度匹配根本没有能力对一堆异构片段做时间维度的聚合或者跨模态交叉验证。“找得到”是记忆的仓储能力“算得出来”才是记忆的分析能力两者缺一不可。1.2 长期记忆的真正价值不是存档而是建模长期记忆的价值曲线会随着时间推移发生变化。系统刚上线时每次检索都能命中最近几天的数据用户觉得“还挺聪明”等数据积累到几个月甚至一年用户开始期待系统能回答“照这个趋势发展下去下个月库存该怎么备”“哪个环节的异常频率明显升高”。这些问题的共同点是它们依赖时间轴上的统计规律、事件之间的共现关系、以及多模态信号互相印证后的结论。长期记忆如果只是“存了很多东西”那就和网盘没有本质区别。真正的长期记忆应该是一个不断更新的世界模型它要能把零散的多模态事件压缩成可查询的知识然后在需要时对这些知识进行聚合、比较、趋势推断最后输出可执行的分析结果。这也决定了记忆系统不能只有一个向量索引它还需要结构化属性层、事件模型、分析算子层以及一套把自然语言查询翻译成分析计划的能力。adamm 就是按这个思路设计的。2. 先把记忆分清楚情景、语义、程序性三层架构2.1 三层记忆到底各管什么设计 adamm 时我参考了认知科学里常见的记忆分类把系统记忆拆成三层情景记忆、语义记忆、程序性记忆。情景记忆Episodic Memory保存具体的多模态事件某年某月某日某分某位顾客在哪个货架前停留了多少秒收银台当时排队几个人环境声音里有没有促销广播。事件是带时间戳、带原始数据引用的“一次发生”强调时间、地点、人物、事件。语义记忆Semantic Memory保存从事件中抽象出来的概念知识比如“雨天客单价显著高于晴天”“周末下午 4 点后进店人数开始上升”“高毛利商品在试吃活动后销量提升约 18%”。语义记忆不带“某一次”的标签而是跨事件归纳后的结论。程序性记忆Procedural Memory保存可复用的分析方法与操作流程比如“遇到销售趋势类问题应该先按周聚合再做差分”“检测到客流量异常时要同步检查天气、促销活动、周边路况事件”。程序性记忆让系统知道“怎么算”。三层并不孤立。情景记忆是原材料语义记忆是加工产物程序性记忆是加工方法。传统多模态记忆系统往往只做了第一层甚至第一层都只做了一半。adamm 把三层都显式建模目的就是让“找得到”和“算得出来”各有归处检索主要落在情景记忆和语义记忆上而计算引擎依赖程序性记忆中的算子和编排规则。2.2 统一多模态事件格式MEV是整个系统的基础三层记忆要想协同工作底层数据结构必须先统一。我在 adamm 里定义了多模态事件格式Multi-modal Event VectorMEV所有输入数据在写入前都会被转成这种格式。一个典型事件长这样{ event_id: evt_10234, timestamp: 2025-11-03T14:32:00Z, modality: [video, audio, text], embedding: { video: [0.12, -0.04, 0.37], audio: [0.55, 0.21, -0.09], text: [0.47, -0.33, 0.08] }, entities: [customer_123, store_042, cashier_07], relations: [ {head: customer_123, type: browses, tail: product_88} ], stats: { person_count: 3, speech_emotion: neutral, weather: rain }, raw_ref: [ s3://store042/20251103/14_32.mp4, s3://store042/20251103/14_32.wav ] }用“事件”而不是“片段”作为原子单位是因为分析算子需要一个可对齐的最小语义单元。视频里截出来的 10 秒片段如果不绑定音频、时间戳、实体关系就无法回答“这个人在做什么”“当时环境如何”。MEV 把多模态信号绑定到同一个事件 ID 下后续做共现分析、时间聚合、跨模态交叉验证时能直接用 event_id 做 Join不用再纠结不同模态之间的对齐问题。2.3 写入与合并怎么防止长期记忆变成垃圾场记忆系统最怕的是“垃圾进、垃圾出”。多模态数据量大如果每个原始片段都无脑存进去系统很快就会变得又慢又臃肿。adamm 的写入流程分四步多模态编码视频抽帧后过 CLIP/ViT 编码音频切段后过 Whisper 编码文本直接过文本编码器。各模态 embedding 都归一化到同一维度。事件切分按场景切换点和滑动窗口把连续流切成候选事件。视频画面发生明显变化、音频静音/人声切换、文本语义转折都会触发新事件。质量筛选与去重对候选事件做质量打分纯噪声音频、黑屏视频、无实体关联的文本会被降权或丢弃。相似度超过阈值的事件自动合并保留信息量最丰富的一份并把冗余 raw_ref 追加到原事件的引用列表里。增量聚合标记为同类的高频事件定期触发语义记忆更新比如“连续三个工作日 14:00 到 15:00 客流量都高于均值”这条会被写入语义记忆层。这套流程跑稳之后情景记忆不会无限膨胀语义记忆又能持续沉淀分析引擎才有干净的数据可用。3. “找得到”层的正确姿势双通道索引加混合召回3.1 为什么单靠向量检索不够很多实现会直接拿 embedding 建向量索引查询时算余弦相似度。向量检索擅长处理“语义相似”但分析型查询通常带着很强的结构化约束“过去三个月”“周六下午”“下雨天”“客单价”。这些约束如果用纯向量表达模型会对“周六”“下雨”“客单价”这些概念做隐式编码但没法精确保证结果百分之百落在指定时间范围和天气条件下。结果就是检索返回了一些“语义上感觉对”但时间戳不对的事件分析算子一聚合数据就污染了。adamm 的做法是双通道索引一路向量通道负责语义相似度召回另一路结构化属性通道负责对时间、地点、实体类型、情感、天气、事件类型等可枚举字段做精确过滤。查询进入系统后先解析出结构化约束用属性通道把候选集合大幅缩小再在缩小后的集合里做向量相似度排序。打分融合用一个简单公式score alpha * cosine_sim(query_embedding, event_embedding) beta * attr_match_score(event, constraints)alpha 和 beta 可以在标注数据上用简单的网格搜索确定。我在早期项目里直接固定 alpha0.6、beta0.4效果已经不错。如果后续要更精细可以把两者拼成一个特征向量训练一个轻量级排序模型但没必要一上来就上复杂模型。3.2 双通道检索的日常效果对比检索方式适合场景典型问题纯向量检索找同款风格图片、相似语音片段无法精确限定时间和实体聚合时数据噪声大纯结构化查询“查 11 月 3 日 14:00 到 15:00 的门店视频”没有语义泛化能力换个说法就查不到双通道混合检索分析型问题某时间段内相似行为的趋势需要维护属性索引写入链路更复杂双通道混合召回并不会显著增加在线延迟因为属性过滤在向量检索之前完成向量检索只需要在过滤后的子集上搜索。真正需要注意的是属性字段的维护成本写入时如果没把 entities、stats 抽取准确属性通道再快也没用。3.3 保留原始模态数据永远不要只留 embedding有一个设计我踩过坑为了省存储早期版本只保存了事件 embedding 和断句文本原始视频和音频直接扔掉。结果分析引擎给出“该顾客在货架前停留时间异常长”的结论后用户要求查看原始视频取证系统拿不出来。这个教训很直接可执行分析必须可解释而可解释的最后一道防线是原始证据。所以 adamm 的 MEV 里强制保留 raw_ref原始媒体要么放对象存储要么存本地文件路径只把路径写进事件。冷数据可以压缩后存低成本存储但不能删。这样检索和分析产出的每个结论都能回溯到具体时间点的原始多模态数据。4. “算得出来”层的核心把查询翻译成可执行分析计划4.1 用户问题先转成分析意图而不是直接去搜用户不会按照 API 文档提问他们说的是自然语言。adamm 接到问题后第一步不是检索而是让一个大语言模型做意图解析把问题转换成结构化的分析计划。比如用户问“下雨天和晴天相比哪种天气客单价更高”解析结果大致是这样的{ intent: compare, target_metric: avg_order_value, group_by: [weather], filters: { event_type: purchase, time_range: all }, required_evidence: [purchase_event, weather_label] }解析过程我用了约束解码不让 LLM 自由发挥字段名而是给一段预先定义好的 JSON Schema只允许模型在 schema 里选择。这样能保证后续算子从记忆库里取数据时不会出现字段对不上。意图类型也就固定十几种trend、compare、anomaly、periodicity、co_occurrence、breakdown、top_k、correlation 等。4.2 记忆分析算子库每种问题都对应一个可复用算子分析计划确定后真正干活的是算子库。算子不是简单的 Python 函数每个算子都绑定了输入 schema、输出 schema、可解释模板和置信度评估方法。我在 adamm 里初期实现了这些算子trend对目标指标按时间聚合后做线性回归或移动平均输出变化方向和幅度。compare对对照组和实验组做均值比较并计算差异显著性。anomaly用统计阈值或轻量级隔离森林检测事件指标中的异常点。periodicity用自相关或快速傅里叶变换识别事件周期。co_occurrence统计两个实体或两类事件同时出现的频次和提升度。breakdown按指定维度时段、门店、商品类目拆解指标找出贡献最大的部分。算子执行时可以直接消费 MEV 中的 stats 字段和实体关系。比如 trend 算子会先把所有符合过滤条件的 purchase 事件按天聚合再在聚合序列上做平滑和差分最终得到“整体上升/下降每周环比变化百分之多少”这个结论。4.3 从分析计划到结果的执行流程整个计算链路用 Python 描述大概是这样的def execute_analysis(query, memory): plan parse_intent(query) operator resolve_operator(plan.intent) events memory.retrieve( filtersplan.filters, semanticplan.semantic_query, top_kplan.top_k ) if not enough_evidence(events, plan): return build_insufficient_payload(plan, missing_reason) result operator.compute(events, group_byplan.group_by, targetplan.target_metric) analysis_payload build_analysis_payload( summaryresult.summary, evidenceresult.evidence_event_ids, confidenceresult.confidence, actionplan_to_action(plan, result) ) return analysis_payload这里面有个容易忽略的地方检索出来的事件集合不一定够用来计算。比如用户问“最近三个月每天客单价趋势”但系统里只有两个月的 purchase 事件。如果强行算 trend出来的结论会误导人。所以执行算子前一定要做样本量检查我设了一个最小样本数阈值低于阈值就返回“当前数据量不足以支撑该分析”并说明还需要哪些数据。宁可让系统承认算不出来也不能编一个看似精准的结论。4.4 可执行分析的输出不能只是一段话“可执行”三个字决定了输出结构。adamm 的每个分析结果都包含三部分人类可读的摘要、结构化证据列表、可执行动作。{ summary: 下雨天客单价平均为 128 元晴天为 96 元雨天高出约 33%。, evidence: [ {event_id: evt_10234, role: purchase, weight: 0.82}, {event_id: evt_10289, role: weather_label, weight: 0.9} ], confidence: 0.87, action: { type: recommendation, params: { target: 雨天时段, suggestion: 安排高毛利商品试吃提高客单价 } } }结构化动作可以直接喂给下游业务系统。比如检测到连续三天晚间客流量异常上升系统可以自动生成一条“建议增加晚班人员”的工单。这才是把长期记忆从“能查”变成“能用”的关键一步。5. 一个完整落地案例门店运营多模态记忆系统5.1 数据接入摄像头图像、语音工单、POS 流水为了说清楚这套设计怎么跑通我拿门店运营场景举例。接入的数据主要有四类入口和货架摄像头视频流、店内麦克风采集的环境音与员工语音、客服语音工单、POS 系统的交易流水。摄像头图像经过检测模型得到“顾客出现/停留/拿取商品/走向收银台”等行为事件语音经过转写和情绪识别得到“顾客询问/员工引导/情绪异常”等事件POS 流水直接提供每一笔交易的金额、商品、时间。所有事件统一封装成 MEV 写入情景记忆层。天气数据作为环境标签在写入时关联到门店和时段不单独做成事件但会写进事件的 stats 字段。这样后续按天气分组时直接过滤 stats.weather 即可。5.2 用户提问“下雨天和晴天比哪种天气客单价更高”这个查询会走一遍完整的“解析-检索-计算-动作”链路。意图解析器把它转成 compare 分析计划target_metric 是 avg_order_valuegroup_by 是 weather。检索模块先在属性通道里过滤出 event_typepurchase 的事件再通过时间戳关联到天气标签得到两张事件集合雨天 purchase 事件和晴天 purchase 事件。compare 算子分别对两批事件计算客单价均值同时统计样本数量。如果雨天的有效 purchase 事件少于 100 笔系统会降低结论置信度。当样本量足够时它输出“雨天客单价更高高出约 33%”并附上参与计算的事件 ID 列表。这里我可以用伪 SQL 展现算子实际拿到数据后的聚合逻辑SELECT weather, SUM(order_amount) / COUNT(DISTINCT bill_id) AS avg_order_value FROM memory_events WHERE event_type purchase AND weather IS NOT NULL GROUP BY weather;算子并不直接操作底层数据库而是通过记忆访问层拿到过滤好的 MEV 集合再在内存里做聚合。上面的 SQL 只是展示“算子内部实际在算什么”帮助理解。5.3 分析结果如何变成门店动作得到“雨天客单价更高”的结论后动作层给出建议雨天客流通常较少但客单价反而高说明雨天进店的顾客购买意向更明确。这种情况下可以安排高毛利商品试吃或在雨天推送“雨天专属套餐”进一步拉高客单价。建议不是拍脑袋而是基于 co_occurrence 算子发现雨天事件常与“顾客咨询高毛利商品”事件同时出现。这个案例的价值在于用户没有要求“查一下雨天发生了哪些事件”他问的是“哪种天气客单价更高”。传统检索系统只能给他一堆雨天视频和晴天视频adamm 则能给出一个结论、一手证据、一条可执行建议。6. 工程实现里的坑以及把效果调稳的几个关键点6.1 显存只有 16G 时多模态编码怎么做多模态模型往往被当成显存杀手但实际落地并不需要把所有模态都塞进同一个超大模型。adamm 的多模态编码我在代码复现时彻底拆开视频帧用 CLIP ViT-B/16 编码音频用 Whisper-small 转写和向量化文本直接用 SentenceTransformer这几个开源小模型并在一起16G 显存完全跑得动。拆开之后还有一个好处每个模态可以异步处理。视频流抽帧是瓶颈我就先 1fps 抽帧检测到画面变化明显比如货架前人群聚集再插帧补充细节每个事件最多保留 8 帧原始画面超过的部分只留 embedding 和粗粒度描述文本。音频切段限制在 30 秒以内Whisper 转写时不生成时间戳对齐到字级别而是对齐到事件级大大减少计算量。如果想进一步榨干显存可以把编码器转成 ONNX 或 TensorRT用半精度跑。实测在同一条视频流上TensorRT 优化后编码吞吐能提升 40% 以上显存占用下降一半。关键原则是不要让多模态大模型做长期在线推理。离线把视频切好、抽好特征、写好离线描述在线只做轻量级检索和计算这样 16G 卡也能撑起一个小型门店群的多模态记忆系统。6.2 长期记忆膨胀与数据冷热分层只要业务连续跑记忆数据就会一直涨。我见过最夸张的情况是落地三个月后事件总量翻了几十倍向量检索延迟直线上升。解决方案不是无脑扩机器而是做数据冷热分层。热数据最近 30 天保留完整 embedding、完整 raw_ref、完整结构化字段检索实时走向量库。温数据30 天到 180 天embedding 降维保留raw_ref 保留原始码率但转存低频存储。冷数据180 天以上只保留语义记忆层自动生成的摘要事件原始情景记忆压缩成按月维度的统计摘要。如果后续需要回溯具体案例再从原始存储异步拉取。这套分层做下来向量库的热数据容量能控制在可接受范围内长期分析仍然可以跨全量记忆执行只是冷数据部分走语义记忆摘要而不是逐条事件。这样既保住了“找得到”也保住了“算得出来”的响应速度。6.3 防止“算出来”的内容是幻觉分析型输出比检索型输出更容易产生幻觉因为统计结论是加工过的用户不会一眼看出它是不是真的由事件支撑。adamm 从三个层面控制这个问题。第一所有结论必须携带证据事件 ID。没有证据链的 summary 不会被输出。第二置信度计算引入样本量惩罚。一个简单的置信度公式是confidence evidence_count / (evidence_count decay_base)decay_base 设为 50 左右样本不足 50 个事件时置信度很难超过 0.5系统会标记为低置信度结果。第三算子明确拒绝“外推”。趋势算子可以给出“过去 90 天周环比增长 5%”但不能自动预测“下个月还会增长 5%”。预测属于另一类任务需要额外的前置条件和更严格的验证集。踩过一次很深的坑门店系统上线第三周遇到连续暴雨天气顾客很少只有 30 笔雨天交易。系统照样输出“雨天客单价高 40%”的结论运营同事差点按照这个建议改了雨天促销策略。后来检查证据发现雨天样本只有 30 笔晴天样本有 3000 多笔均值差异根本不显著。从那以后minsample 阈值和置信度惩罚成了强制项宁可结论弱一点也不能给出夸张的误导。7. 如果让我从零复刻一套最小可行版本怎么搭7.1 组件选型和最简架构不一定要自研所有模块。如果要快速验证 adamm 的思路我会选一套现成组件搭最小可行版本SQLite 存储事件结构化字段和 raw_refMilvus 或 FAISS 做向量索引CLIP 做图像编码Whisper-small 做音频转写一个 chat 模型做意图解析和摘要生成再加一层几十行代码的分析算子库。这套组合在单台 16G 显存机器上就能跑通。最简架构可以按这个顺序搭先确定 MEV 数据结构和写入链路把真实业务数据灌进 SQLite再给事件建向量索引验证双通道检索能准确找回分析需要的候选集合然后实现两三个核心算子比如 trend 和 compare跑通一个完整查询最后接上动作输出。整个流程大概两周能出第一个可用版本。7.2 迭代路线先做好一件事再扩展算子很多团队一上来就追求“全模态、全算子”这是大忌。我建议第一版只做一件事让用户能用自然语言问“某个指标在某个维度下的变化趋势”。把这条链路做通做稳把证据回溯和置信度做扎实再去加异常检测、周期性分析、共现分析。每新增一个算子都要重新检查它对底层事件数据的要求最好提前预留一个“算子-证据需求”的映射表新增算子时照着表核对需要采集哪些新字段。程序性记忆层的价值在这一步体现得最明显。新增算子不是写一个孤立函数而是往程序性记忆里注册一套“方法”。下次遇到同类问题系统能自动关联到这个方法而不是每次都从零解析。7.3 我回头看这套系统的三点经验多模态记忆本身不是目的可执行分析才是。判断一个记忆系统好不好不该只看检索准召率而要看它能不能回答三类问题发生了什么、为什么会发生、下一步该做什么。如果系统只能回答“在哪”那它本质上还是一个带索引的数据库。另一个经验是分析引擎的边界一定要清晰。遇到样本不足、字段缺失、模态不完整系统应该大大方方说“算不了”并告诉用户缺什么数据。这种“拒绝能力”反而能建立信任。用户宁可听到一次拒绝也不愿意被一个看起来精确无比的数字骗五次。最后想说adamm 这套设计并不高深但它提醒我一件事长期记忆系统做到最后拼的不是模型参数有多大而是对业务问题的拆解能力、对证据链的执着以及把结论变成行动的执行力。如果你也在做类似的东西建议先把自己的记忆数据结构画清楚再谈用什么模型、什么向量库。数据结构稳了后面一切都会顺很多。