1. 当机器人只会“过目即忘”视觉—语言—动作模型的记忆之痛先聊个实际场景。你让一个装了视觉—语言—动作模型VLA的机械臂去“把桌上那个红色杯子放到盘子里”第一次它做得很好第二次换个位置它又跟没见过似的重新摸索一遍。更气人的是你上午教它“瓶盖要顺时针拧开”下午再问它它已经忘了瓶盖这回事。这不是模型笨而是绝大多数VLA模型天生就没有“记忆”这个模块——它们的推理过程就是把当前帧图像和当前指令塞进一个上下文窗口输出一段动作序列然后整个系统就“失忆”了。MEMMulti-scale Embodied Memory多尺度具身记忆要解决的就是这件事给VLA模型装上一套分层级、可读写、能跨时间检索的记忆系统。这个概念不是某个具体开源项目的名字而是最近具身智能论文里逐渐收敛出来的一类架构思路——把记忆分成工作记忆、情景记忆、语义记忆三个尺度分别对应“当前动作”“当前任务”“长期经验”三种时间粒度再通过检索机制把相关内容动态注入VLA模型的推理过程。这篇文章我会从设计思路、架构拆解、代码实现到调参踩坑完整梳理一遍如何给VLA模型装上MEM模块。适合正在做机器人操控、VLA微调或者具身智能系统集成的朋友也适合刚接触这个方向、想知道“记忆到底怎么和策略网络捏在一起”的入门读者。全程没有晦涩的数学推导都是我实际跑实验时用的方案和教训。2. 多尺度到底在“尺”什么三种时间粒度与记忆架构对应关系理解了MEM的核心目标紧接着的问题就是什么叫“多尺度”很多刚接触这个方向的人第一反应是“图像多尺度”以为是把同一帧图片缩小放大做金字塔。其实不是这里的尺度指的是时间粒度和抽象粒度是记忆内容本身的层次。2.1 工作记忆一个动作里的“手忙脚乱”工作记忆对应的是“当前时刻往前推几秒钟”这个时间窗口。它解决的是VLA模型在单步执行时的状态连续性问题。机械臂在抓取一个移动的物体时光靠当前一帧图像是不够的——物体在动你的夹爪也在动必须知道物体过去半秒的运动趋势才能预测抓取点。这就是工作记忆的作用保留最近N帧的视觉特征和动作历史让模型在预测下一秒的动作时有足够的时序上下文。很多实际项目会把这一层做成一个环形缓冲区ring buffer只存最近5到10帧满了就覆盖最旧的。实现非常简单但它却是整个记忆系统里被调用最频繁的一层延迟要求最高任何检索逻辑都不应该在这一层做——直接按时间顺序拼接就好。2.2 情景记忆从“拧开瓶盖”到“我上次拧过这个瓶子”情景记忆是MEM真正拉开差距的地方。它记录的是“某次具体任务执行过程中发生了什么”时间粒度从几十秒到几小时不等。比如让机器人执行“把桌上的五个积木按颜色分类摆放”这个任务整个流程可能持续三分钟。期间机器人需要记住哪个积木已经放过、哪个颜色的区域已经摆满、上次试图抓取蓝色积木时为什么失败了滑了一下。这些信息如果丢到工作记忆里早就被覆盖了如果丢到语义记忆里又太具体、不具备泛化价值。情景记忆就是为这类“任务内状态跟踪”设计的。2.3 语义记忆把经验沉淀成“世界常识”语义记忆是最高层、最抽象的记忆时间粒度可以跨越天、周甚至月。它保存的是一般性的经验规则和技能知识比如“塑料瓶盖通常需要先按压再旋转”“门把手往下压才能开门”“陶瓷杯子比塑料杯子重抓取力要大一档”。这些知识从哪儿来一部分可以靠预训练时灌入另一部分靠对情景记忆做周期性归纳——比如机器人一星期内成功拧开了20个不同瓶子语义记忆层就该自动提炼出一条“拧瓶盖”的通用技能描述。这条描述不是某个具体任务的照片而是一个高度抽象的特征向量配合上自然语言描述共同构成语义记忆条目。这三层记忆是层层递进、互相配合的关系记忆类型时间粒度典型内容存储方式检索方式工作记忆秒级最近帧图像特征、最近动作环形缓冲区时间顺序直接拼接情景记忆分钟级当前任务的子目标、中间状态内存向量库与当前状态做相似度检索语义记忆天/周级技能规则、物体交互常识持久化向量库任务级检索按指令匹配这个分层设计的巧妙之处在于它不是把一堆记忆胡乱扔进一个大池子而是严格按“当前动作—当前任务—长期经验”把记忆的使用频率和生命周期做了切分让每一层都能用最简单的方式管理自己的读写。3. MEM整体框架与核心模块逐层拆解架构分层说清楚之后接下来落实到具体实现。MEM模块插在VLA模型的什么位置、每个子模块干什么事这是做工程实现时第一个要明确的问题。3.1 记忆编码多模态输入如何变成“可检索的向量”记忆不是拿原始图像和文本直接存的那样既占空间又没法做相似度检索。所有记忆条目在写入之前都要过一道编码器转成固定维度的特征向量。我实验里最常用的做法是直接复用VLA模型自带的视觉编码器和文本编码器把当前帧图像过一遍视觉编码器得到图像特征把任务描述或者子目标描述过一遍文本编码器得到文本特征然后拼接或者加权求和得到一条记忆嵌入memory embedding。这里有个容易踩的坑有些图像特征直接拼接文本特征维度会不一致。我的做法是各取特征后先各自过一个线性投影层统一映射到相同的维度比如768维再拼接或相加。投影层参数是可以通过对比学习一起训练的训练目标简单说就是“同一个记忆内容的不同表达方式向量距离要近”。3.2 记忆检索任务来了该翻哪本笔记检索是整个MEM的核心直接决定记忆内容能不能用上。我的实现里用的是双向检索机制而不是只查一层。具体来说当VLA模型处理新的指令和当前帧时系统会同时去情景记忆和语义记忆里检索相关内容。情景记忆的检索键是“当前视觉特征最近动作历史”的拼接向量语义记忆的检索键是“任务级文本指令”的编码向量。相似度计算我用的是余弦相似度加温度系数缩放。经验值是温度系数设在0.05到0.1之间时检索结果的区分度最好设太大检索结果全变成“谁都像”设太小又变成“谁都不匹配”。检索返回的结果不是直接丢给模型而是要过一个“相关性过滤”步骤低于阈值的记忆直接丢弃否则噪声记忆会严重干扰VLA模型的动作预测。我的经验阈值范围是0.6到0.75具体看数据分布需要跑一次验证集统计。3.3 记忆写入与更新什么该记、什么该忘写入策略是MEM里最容易被忽视却最影响长期效果的部分。激进地写会把记忆库塞满噪声保守地写又会让模型“学不到新东西”。我的写入策略分两条线第一情景记忆层在每一个任务节点自动写入。比如机器人完成一个子目标或者检测到状态发生显著变化夹爪开合状态翻转、物体位置坐标变化超过阈值就写入一条新记忆。写入内容包含当前视觉特征、动作描述、状态变化结果。第二定期对情景记忆做聚类生成语义记忆。我每个训练日结束时会对当天所有情景记忆做一次K-Means聚类每个聚类中心如果和已有语义记忆条目的相似度低于某个阈值就生成一条新的语义记忆。这个“由具体经验归纳出抽象规则”的过程是MEM能够持续进化的关键。遗忘机制也很重要没有遗忘的记忆库最终会变成一本塞满废话的日记。我的做法是给每条记忆加一个“访问次数”计数器和“最后访问时间”定期清理那些长时间未被检索到的冷记忆。清理优先级是情景记忆优先于语义记忆访问次数少的优先于访问次数多的。3.4 与VLA主干的高效集成两路注入法记忆模块单独说完了下一步是怎么和VLA模型捏在一起。直接用大语言模型的方式把检索到的记忆当成纯文本拼进上下文这种做法简单但效果一般——因为视觉记忆本身不是文本能完整描述的拼进去只会增大输入长度、拖慢推理。我用的是双路注入情景记忆的检索结果以特征向量形式直接加到VLA模型的视觉特征序列后面作为额外的transformer输入token语义记忆的检索结果则转成自然语言描述记忆条目里本身就存了文本描述字段拼接到指令文本后面。这种做法的好处是情景记忆保留了完整的视觉信息语义记忆又兼顾了语言模型的文本理解能力两类记忆互不干扰而且不会把上下文窗口撑爆。4. 实操从零搭建一个MEM原型理论部分差不多了到动手环节。以下是我在项目里实际跑通的实现方案代码结构尽量精简重点在于让你看清记忆模块和VLA推理循环是怎么衔接的。4.1 环境与依赖Python 3.10PyTorch 2.1transformers加载VLA模型的文本部分timm加载视觉骨干faiss-cpu或faiss-gpu向量检索库大规模记忆库必须用FAISS千万条级别的暴力检索速度扛不住numpy、scikit-learn聚类用4.2 记忆库的核心数据结构class MemoryBank: def __init__(self, dim768, max_size100000): self.dim dim self.max_size max_size # 情景记忆可动态增长的向量列表 self.episodic_vectors [] self.episodic_text [] # 每条情景记忆的文本描述 self.episodic_access_time [] # 记录最后访问时间 self.episodic_access_count [] # 语义记忆同样管理方式但生命周期更长 self.semantic_vectors [] self.semantic_text [] self.semantic_access_time [] self.semantic_access_count [] # FAISS索引用于高效检索 self.episodic_index None self.semantic_index None def add_episodic(self, vector, text): # 情景记忆写入 self.episodic_vectors.append(vector) self.episodic_text.append(text) self.episodic_access_time.append(time.time()) self.episodic_access_count.append(1) # 如果超过最大容量触发清理 if len(self.episodic_vectors) self.max_size: self._evict_oldest(ratio0.3) self._rebuild_episodic_index()这个结构最关键的是FAISS索引的维护。每次新增记忆就重建索引会非常慢所以我实际是每写入50条才重建一次检索的时候如果索引还没建好就直接暴力计算。4.3 VLA推理循环中的记忆读写代码骨架def vla_inference_step(model, obs, instruction, memory_bank, cfg): # 1. 提取当前观测特征 visual_feat model.visual_encoder(obs) # 形状 [1, seq_len, dim] # 2. 工作记忆维护最近N帧的缓冲区直接拼接进视觉特征 working_memory_buffer.append(visual_feat) if len(working_memory_buffer) 5: working_memory_buffer.pop(0) augmented_visual torch.cat([*working_memory_buffer], dim1) # 3. 情景记忆检索用当前视觉特征最近动作 做检索键 query_episodic torch.cat([visual_feat.mean(dim1), recent_action_embedding], dim-1) episodic_hits memory_bank.retrieve_episodic(query_episodic, top_k8) # 4. 语义记忆检索用任务级指令文本做检索键 instr_feat model.text_encoder(instruction) semantic_hits memory_bank.retrieve_semantic(instr_feat, top_k4) # 5. 注入VLA模型 # 情景记忆特征直接拼进视觉token序列 augmented_visual torch.cat([augmented_visual, episodic_hits], dim1) # 语义记忆文本拼到指令后面 augmented_instruction instruction .join(semantic_hits_text) # 6. 模型前向 action model.policy_head(augmented_visual, augmented_instruction) # 7. 执行动作后把结果写入情景记忆 memory_bank.add_episodic( vectortorch.cat([visual_feat.mean(dim1), action_embedding]), textfObserved state, performed action, outcome: {task_state} ) return action这段代码是整个MEM集成的最小闭环。动作预测之前做检索动作执行之后做写入每一轮都在持续更新记忆库。4.4 关键超参数与调参心得我踩了好几次坑才把下面这几个参数调到能用的状态直接列出来参数推荐值调参说明情景记忆检索top_k816太少信息不足太多噪声明显增多语义记忆检索top_k36语义记忆比较抽象3-4条足够相关性过滤阈值0.60.75低于0.6会让大量无关记忆混进来检索温度系数0.050.1控制相似度分布的锐利程度情景记忆清理阈值30%以上容量超过90%才清理会显著拖慢检索速度语义记忆聚类周期每个训练日结束太频繁产生冗余太久则记忆不完整有一个参数组合上的经验情景检索的top_k要跟相关性过滤阈值配合着调。如果阈值设在0.6top_k设到16经常出现一半以上的检索结果被过滤掉阈值提到0.7top_k反而可以设到12保留的有效记忆更多。总体上宁可让检索结果少而精也不要让它多而杂。5. 踩坑实录与排查技巧这部分是我跑了两个月实验攒下来的实战问题集很多问题论文里压根不会告诉你。5.1 检索失效为什么模型记忆里明明有却调不出来第一种典型表现是记忆库里明明存了类似场景的成功经验但测试时模型完全没反应。排查思路是先看检索日志如果发现检索结果相关性分数普遍很低问题出在查询向量和记忆向量的分布不一致。我遇到这种情况根因是视觉编码器在训练和推理时图像预处理方式不同训练用了随机裁剪推理用了缩放导致同一场景的特征差异很大。解决办法是统一预处理逻辑并定期用最新数据的特征分布对记忆库向量做一次归一化校准。校准公式很简单就是逐维度减均值、除标准差用滑动窗口统计。5.2 记忆污染旧经验影响新任务的动作决策比检索不到更麻烦的是“检索到但不该用”。比如上次抓的是易碎陶瓷杯这次抓的是金属保温杯记忆系统却把“轻轻地、小力抓取”这条旧经验调了出来导致夹爪闭合不够、金属杯滑落。这是记忆污染问题本质是相关性过滤阈值偏低把“部分相似但关键属性不同”的记忆放行了。我的排查方法是在记忆条目里增加关键属性标签比如物体材质、重量等级、摩擦系数。检索时除了算特征相似度还要比对关键属性的一致性通过两个分数的加权作为最终相关性分数。5.3 显存爆炸与延迟问题记忆模块最大的工程性问题是资源开销。FAISS索引在千万级向量下本身占几个GB的显存再加上VLA模型的权重很容易把一张24GB的卡跑爆。我最终采用的方案是“显存-内存混合存储”热记忆最近写入的放显存冷记忆放CPU内存检索时先用低维度的近似索引在CPU做召回再把Top-100的结果搬到显存做精排。这个方案把单次推理延迟从128ms降到了92ms显存占用下降了约40%。另一个能明显省显存的做法是压缩记忆向量维度从768维压缩到256维检索精度损失很小但显存占用直接降到三分之一。5.4 遗忘频率怎么调记得太少会忘记得太多会乱遗忘清理的频率是个需要反复实验的平衡点。清理太激进好经验被误删模型能力退化清理太保守记忆库膨胀检索噪声上升推理变慢。我的经验是采用分层清理策略情景记忆层每500次写入触发一次清理保留最近最活跃的70%语义记忆层每7天检查一次清理60天内未被访问过的条目。这个节奏在我的任务集上效果最好。如果你处理的任务类型差异很大可以适当把情景记忆的保留比例调低到50%给新经验腾出空间。5.5 多任务场景下记忆相互干扰的排查方法当机器人执行的不止一个任务而是“倒水、叠衣服、摆积木”三种任务来回切换时记忆系统容易出现一种尴尬情况当前在做A任务检索出来的记忆却混着B任务和C任务的内容。我排查后发现根源在语义记忆的检索键设计。统一用任务指令文本编码做检索键不同任务的语义向量在向量空间里离得不够远检索时天然会互相干扰。后来我改成“任务描述关键物体描述”双编码组合作为检索键效果好了很多。比如“叠衣服”这个任务检索键从“叠衣服”变成“叠一件蓝色T恤叠衣服任务”检索结果里别的任务的内容基本消失。6. 一些也许不严谨但确实有用的实操经验最后按我个人经验聊几句。MEM这套架构真正落地到实际项目中最有价值的地方不是某一层记忆本身而是“多尺度分层”这个思考方式。最开始我在一个桌面操作任务里只加了情景记忆效果提升明显但波动很大后来补上工作记忆稳定性提高了再后来加上语义记忆的周期性归纳跨任务的泛化能力才真正上去。每一层补上去都有立竿见影的作用缺了哪一层都能感觉到短板。有个小技巧值得单独提一下在你调试检索相关性阈值时不要凭感觉选数字。我的做法是收集50条“正确”记忆和50条“干扰”记忆分别计算它们与测试查询的相似度分布观察两个分布的重叠区域。阈值选在重叠区域下端基本就是最优区间附近。这个方法能帮你把阈值选择从玄学变成统计学。另外一个容易被忽略的点是记忆条目的文本描述质量。很多实现里文本描述只是象征性地存了一句话比如“成功完成一次抓取”这对语义记忆归纳毫无帮助。我后来的做法是让描述尽可能结构化包含“场景操作结果”三段例如“红色桌面抓取蓝色积木成功放入绿色盒子”。这种格式化的描述在聚类和语义检索时明显更容易对齐。MEM多尺度具身记忆的框架到这里已经完整覆盖了从理论设计到代码实现再到工程调试都是我在真实项目里一步步验证过的。如果你的VLA模型正在面临长时任务做不稳、同类场景换位置就失灵的问题值得花一个周期把这套记忆系统装上去试试。