这几年机器人学习领域最绕不开的一个词大概就是 VLA。Vision-Language-Action把视觉、语言、动作三个模态揉进同一个模型里让机器人听一句人话就能在真实环境里干活。但源码读多了你会发现真正拉开差距的往往不是“多模态 prompt 怎么拼”而是训练目标怎么定。有一类工作做得特别激进除了预测动作还把整个场景“重建”出来这类模型也被叫成重建式 VLA。能拿下顶会最佳论文说明这套源码不只在刷榜工程完成度通常也很高。我前后把它完整读过两遍也照着思路自己复现过一版中间的坑不少。这篇我打算从源码角度把重建式 VLA 的架构选型、数据管线、重建头实现、训练细节和推理过程全部拆开讲顺着代码逻辑走一遍。适合两类人看一类是已经在跑策略学习、想搞清楚 VLA 内部到底发生了什么的人另一类是准备自己训练机器人基座模型、但担心上来就被各种细节卡住的开发者。我不讲虚的尽量把每个模块为什么这么设计都说清楚。1. 重建式VLA到底在解决什么问题1.1 难点为什么不在模型而在数据的“信息密度”很多人第一次接触 VLA 时会以为难点在模型结构。实际上模型结构只要照着成熟的视觉语言模型改一改就能用最麻烦的是数据。机器人领域和 NLP、CV 不一样数据里动作标签经常有噪声语言指令也存在歧义比如“把杯子放到桌上”这句话并没有说放在桌子的哪个具体位置也没说目标是哪个颜色相近的杯子。过去主流做法是直接把动作作为文本 token 输出让 VLM 预测一串类似“move to coordinate (x, y)”的文本。这么做有一个隐含风险模型只看文本动作标签很容易学会“抄答案”而不是学会理解场景。只要换一个背景、换一个光照策略就会崩。原因很简单——文本动作本身就是人类事后标注的低维信息信息密度太低模型根本没有被逼着去注意物体形状、边缘、空间位置这些细节。重建式 VLA 的思路不同。它在训练时额外要求模型把当前场景、甚至未来几帧场景重建出来。相当于你带一个新员工不只要他记住“把杯子放回桌子”这句话还要他先仔细辨认桌子长什么样、杯子抓在哪个位置、路径上有没有障碍然后再把动作决定写下来。信息的使用方式完全不一样。这也是源码里最值得关注的第一点它不是在原来 VLM 后面简单挂一个动作 head而是通过重建目标倒逼视觉编码器保存更丰富的空间细节。源码里大量细节包括 tokenizer 的选择、重建头的输入输出、损失函数权重都是围绕这个目标服务的。1.2 重建目标为什么能改变策略学习的上限从优化角度来看纯行为克隆的损失函数通常长这样loss_action MSE(action_pred, action_gt)这个损失在动作标签准确的前提下没问题但它的梯度信息非常稀疏。一张 256×256 的图最后只得到一个 7 维动作向量中间绝大部分像素其实没有参与监督。模型自然倾向于只关注少数显著区域一旦场景出现变化就翻车。重建式 VLA 在损失函数里加了一项loss loss_action lambda * loss_reconstruction重建损失提供的是稠密梯度。每个像素、每个 latent token 都可能对模型产生监督模型被迫理解“杯子是圆形的”“把手在杯子侧面”“桌子台面是平的”这些底层结构。它不依赖动作标签的质量能直接从图像本身吸收可迁移的视觉语义。源码里最常见的做法不是直接重建 RGB 像素而是重建离散视觉 token 或低维 latent。原因是纯像素重建训练不稳定、计算开销高而且容易产生模糊结果。通过一个预训练的 VQ tokenizer把图像压缩成离散 token 序列再让模型去预测这些 token既保留了空间信息又把任务变成一个更规整的分类问题。在真实复现中这种重建分支对模型的泛化提升非常明显。我做过一组对比实验同样一个模型去掉重建头之后在已见过的训练场景里表现几乎一样但一旦换到新场景、新物体位置带重建头的模型成功率高出不少。原因是它学会了“场景几何”而不是单纯记住了动作映射表。2. 源码整体设计与模块拆解2.1 架构总览一个视觉语言骨干两个输出头那套开源实现的整体架构并不复杂概括下来就是一个视觉语言骨干两个输出头。视觉语言骨干负责把图像和文本指令编码成语义一致的隐状态。图像先被视觉编码器转成 patch embedding文本指令经过分词和 embedding 后拼接成同一个 token 序列。这个序列送入多层 Transformer 骨干做交叉建模。骨干的输出隐藏状态同时被两个 head 使用动作头Action Head从序列中特定位置取 token 状态回归出连续动作向量或分类出离散动作 token。重建头Reconstruction Head把隐藏状态映射回视觉 token 空间重建当前帧或未来帧的 latent 表示。这个双头结构很关键。如果没有重建头模型只在动作监督下工作骨干很容易退化成“一个只认颜色和粗糙位置的特征提取器”。有了重建头骨干除了要回答“该干什么”还要回答“场景到底长什么样”。这就相当于多任务学习视觉语言骨干被迫学习一个更完整、更通用的中间表征。我在源码里注意到一个细节两个 head 不是简单并联而是共用了最后一层 Transformer 的隐藏状态。这意味着重建任务和动作任务在底层是真正被联合处理的不是各学各的。重建任务提供的梯度会直接影响骨干参数这就是它和“先训练一个视觉编码器、再冻结做策略”的最大区别。后者只把重建当成离线前置完全起不到联合正则化的作用。2.2 数据管线从一堆原始数据集到可训练样本源码里最反直觉的部分往往不在模型定义而在数据管线。一个真正可用的 VLA 训练管线需要处理的数据种类太多了单臂操作、双臂操作、移动操作、仿真数据、遥操作数据。它们动作维度不同、相机视角不同、指令粒度不同直接拼在一起会让模型学疯。源码的做法是先做统一的中间格式。每条样本包含这样几个字段观测图像序列通常是最近 3 到 6 帧按时间顺序堆叠用于感知物体运动和当前状态。语言指令统一模板化最简单就是“把 [目标物体] 放到 [目标位置]”。动作序列连续动作向量归一化到固定范围。时间戳和相机参数记录动作对应的时间点、相机内参外参用于后续视角对齐。数据混合的时候源码没有简单随机拼接而是用了“任务级加权采样”。高难度任务、多样化场景的样本会被提高采样权重单一重复的简单样本会被主动降权。这个细节直接影响模型在复杂任务上的表现我在复现时一开始没有注意导致模型在简单场景上很准一到长 horizon 任务就彻底迷路。还有一个细节是动作分块Action Chunking。源码不是每一步预测一个动作而是每来一次观测直接预测未来 16 步动作序列。这么做的好处有二一是让动作轨迹更平滑避免高频抖动二是减少了策略对于“观测噪声”的敏感度。训练时这 16 步动作要对应样本里连续 16 个时间步所以数据管线里会有专门的 chunk 切片逻辑不能随便跳过帧。2.3 重建头设计像素级重建和隐空间重建的取舍重建头是整个源码里最值得琢磨的部分它决定了模型能不能真正理解场景。像素级重建最直观把图像送进解码器输出重建后的 RGB 图用 MSE 或 L1 做损失。但实际做的时候会发现两个问题。第一个是计算量太大在 VLM 骨干后面再挂一个像素级解码器显存直接多出几个 GB第二个是像素损失容易产生模糊结果模型特别容易学出一个“平均图像”对细节完全无感。真机部署时这种模糊反馈在特征空间里会产生误导。源码里更常见的做法是隐空间重建分三步用训练集训练一个离散视觉 tokenizer把 256×256 图像压缩成若干离散 token。在 VLA 训练时把真实图像通过 tokenizer 编码得到目标 token 序列。重建头输出每个位置的 token 概率分布用交叉熵损失和真实 token 序列做监督。这样重建任务从“预测像素”变成“预测离散 token 类别”模型要准确回答这个 patch 属于哪个语义类别比预测颜色更抽象也更符合策略学习的需求。这个思路跟视觉语言模型中用图像 tokenizer 做对齐的做法一脉相承。选择隐空间重建还有一个额外好处可以和未来预测结合起来。如果训练样本里包含未来帧重建目标不恢复当前帧而是恢复未来 1 到 3 帧的 latent模型就变成了一个小型世界模型。它必须估计出“物体下一步会以什么状态出现在什么位置”。这对长程任务特别有价值。源码里把“重建当前场景”和“重建未来场景”做成可配置选项我在实验时发现未来重建对动作成功率的提升比当前重建明显很多。3. 核心实现与训练细节3.1 从配置文件到训练脚本拿到源码第一步不是读模型代码而是先看配置目录。这套实现的配置组织得很清楚基本可以分成三个部分模型配置、数据配置、训练配置。目录结构类似下面这样- configs/ - train.yaml - model/vla.yaml - model/backbone.yaml - data/open_source_mix.yaml - vla/ - model.py - heads/ - action_head.py - reconstruction_head.py - tokenizer/ - vq_tokenizer.py - datasets/ - base_dataset.py - mixed_dataset.py - trainer.py - inference.pytrain.yaml是整个流程的入口决定模型用哪个 backbone、数据用哪个 mix、损失权重是多少。model.py负责把 backbone、动作头、重建头拼起来mixed_dataset.py负责把多份数据集统一成训练样本。这个层次划分对复现很友好改动某个部分不需要牵动全局。训练入口代码通常长这样model ReconstructiveVLA( backbone_cfgcfg.model.backbone, action_dimcfg.model.action_dim, chunk_sizecfg.model.chunk_size, reconstruction_head_cfgcfg.model.reconstruction_head, )虽然只是寥寥几行但背后有一个容易忽略的点chunk_size被放在模型配置里而不是数据配置里。这个设计不是随意的。chunk size 决定动作头输出多少个未来动作它影响重建头的对齐方式也影响推理策略所以它本质上是模型结构的一部分不是数据预处理的参数。我自己第一版把 chunk size 放在数据配置里结果数据切分和模型输出对不上排查了很久才发现是配置层级的问题。3.2 损失计算、梯度裁剪与优化器配置模型前向的核心部分简化后可以写成这样class ReconstructiveVLA(nn.Module): def __init__(self, encoder, decoder, action_head, recon_head): super().__init__() self.encoder encoder self.decoder decoder self.action_head action_head self.recon_head recon_head def forward(self, image, instruction, actionNone, visual_token_targetNone): # 1. 图像和指令一起编码 hidden self.encoder(image, instruction) # 2. 动作头读取动作 token 位置的隐状态 action_pred self.action_head(hidden) # 3. 重建头把隐状态映射回视觉 token 空间 recon_logits self.recon_head(hidden) if self.training: loss_action F.mse_loss(action_pred, action) loss_recon F.cross_entropy( recon_logits.permute(0, 2, 1), visual_token_target, ignore_index-100 ) return loss_action self.lambda_recon * loss_recon return action_pred真实的训练循环会在这个基础上加不少东西。首先是梯度裁剪源码里普遍会把 grad norm 限制在 1.0 左右。VLA 是视觉语言骨干和策略头的联合训练两个任务梯度方向经常不完全一致不裁剪的话经常训练两三千步就出现 loss 突刺。其次是优化器很多实现直接沿用了 LLM 的 AdamW warmup cosine decay 配置。我的经验是 warmup 至少要给 1000 步千万不能省。VLM 骨干加载预训练权重之后如果一开始就用大学习率激活全参数重建分支的随机初始化会使总损失剧烈震荡。还有一个小但非常重要的细节EMA指数移动平均策略。源码里动作头和重建头通常各自保留一份 EMA 参数副本在验证和推理时用 EMA 参数而不是用实时参数。这样做对稳定策略输出很有帮助相当于把训练过程中震动的权重做了一个平滑。我在复现时不加 EMA实机测试时动作轨迹明显比加 EMA 要“毛躁”。3.3 推理时策略怎么把“动作”抠出来训练和推理的一个关键差异在推理阶段模型不再有重建目标。重建头只在训练时参与推理时把它从计算图里摘掉。源码在写推理逻辑时通常会有类似model.eval()加inference_mode的双重包装避免重建头白白消耗显存和计算时间。推理阶段真正的核心问题是怎么从骨干输出里抽取动作。如果动作头是连续回归式直接取动作 token 对应位置的隐藏状态过一个多层感知机输出连续向量就行。如果动作是离散式则需要从隐藏状态生成动作 token再通过逆映射得到具体数值解码时还需要在动作 token 集合上做一遍 softmax 采样。源码里一般会单独列一份逆归一化配置把训练前算好的均值、标准差、最大值最小值存下来推理时做逆变换。这个步骤看着简单却是最容易出 bug 的地方。我踩过的一个坑是训练用的动作归一化统计来自全部数据集但推理时没有加载这份统计直接把模型输出发到底层控制器。结果就是模型动作看起来合理但幅度全部偏大机器人在真实环境里“甩手”。正确做法是把这些统计参数序列化保存推理时和模型权重一起加载。如果是连续动作头还有一个采样策略问题。最简单的方式是直接用均值输出、不做采样这样动作最稳定。但代价是有时会陷入局部平庸比如多峰任务里均值动作会打在两条可行轨迹之间。更稳的做法是预测 K 个候选动作在推理时用某种打分函数选最优的一个。源码里通常用轨迹成本函数、重建误差或者简单的动作自洽性打分来选。3.4 多卡训练、混合精度与显存策略VLA 模型规模通常不小骨干参数几百亿级别的也有但开源复现一般用几十亿或几亿规模的模型。不管哪种多卡训练都是避不开的。源码里常用的是 FSDP 或 ZeRO把模型参数、梯度和优化器状态切到多卡上。用 FSDP 时有个配置要特别注意重建头通常不参与 FSDP 的参数切分因为重建头本身不大切分反而增加通信开销。混合精度方面大部分实现用 bf16。bf16 的指数范围和 fp32 一样训练起来比 fp16 稳定很多不需要手动做 loss scaling。但要注意批量归一化或者某些自定义算子对精度比较敏感我发现重建头的交叉熵损失在 bf16 下容易出现数值不稳定给它单独保留 fp32 计算会更稳妥。还有一个很多人容易忽略的加速技巧离线缓存视觉编码器的输出。如果视觉编码器在训练时是冻结的那么同一张图像每次迭代都会被重复编码非常浪费算力。源码里很多实现会提前把图像编码成视觉 embedding 存到磁盘或内存训练时直接加载 embedding而不是加载原始图像。这样可以省下大量视觉编码时间。代价是数据增强受限一旦冻结视觉编码器就不能在训练时做在线 color jitter 这类增强。我在复现时用了离线缓存训练速度差不多快了 1.5 到 2 倍所以这是一个非常值得投入的优化。4. 复现过程中最常见的坑与排查4.1 显存直接爆掉第一次按默认配置跑这套源码十有八九会碰见显存溢出。原因往往不是骨干模型太大而是重建头和长序列同时存在。图像经过视觉编码器后 token 数量本来就多如果重建头要在每个 token 位置输出一份分类 logits那 logits 的量级会非常夸张。排查顺序建议是这样的先关掉重建头只看动作头的显存占用如果这个占用你还能接受那问题出在重建头。解决手段有几个可以叠加使用降低重建目标的空间分辨率比如让重建头只恢复 32×32 的 latent而不是 128×128。对重建头使用 block-wise 计算分块计算 logits减少一次性显存峰值。启用梯度检查点用少量计算换显存。如果骨干够大还可以直接用 LoRA 之类的参数高效方案冻结大部分骨干参数。我在复现时按这个顺序操作显存从爆掉降到大概占了原来的六成。不要一上来就换小模型那样会把实验结论也一起带偏。4.2 两个损失为什么总是打架重建损失和动作损失在训练初期经常出现一边快速下降、一边原地不动的情况。这不是代码 bug而是两个任务的学习难度和梯度尺度差异太大。动作损失是低维回归量级通常非常小重建损失是大量 token 的分类损失累加之后数值会高出好几个量级。源码一般会给lambda_recon一个固定权重但这个权重在不同数据集上不是普适的。我建议训练前先不看最终 loss而是分别打印两个独立 loss 的初始值。然后根据初始值选 lambda让两者在数量级上差不多。比如动作 loss 初始 1.2重建 loss 初始 0.8lambda 可以设 1.5 左右。如果重建 loss 初始就比动作 loss 大十倍那 lambda 反而要小于 1不要无脑调大重建权重。还有一种更细腻的做法是给重建损失加一个梯度阻塞前几百步只更新动作头和相关骨干重建头先什么都不干等动作主任务稳住了再放开。这样能避免两个任务在初期抢梯度。源码里未必会写但实测下来确实管用。4.3 机器人动作在实机上“发抖”模型在仿真里跑得好好的一上真机就抖这是很典型的问题。通常不是模型本身错乱了而是推理输出不稳定。VLA 推理频率一般不会太高几赫兹到十几赫兹都有但底层控制器往往需要更高频的指令。如果每次推理输出之间出现微小跳变底层执行器就会表现出震颤。我的经验是几件事一起做使用动作分块推理时输出未来 16 步但只取第一帧动作下发后面 15 帧作为平滑缓冲。对动作头输出做 EMA 平滑公式大概是action_smooth alpha * action_pred (1 - alpha) * action_smooth。限制单步动作的最大变化量超过阈值就按阈值截断。还有一个容易忽略的因素是观测堆叠。我只用单帧图像输入的时候抖动最明显后来把最近到 3 到 4 帧堆叠起来模型对瞬时噪声不那么敏感了动作稳定度明显提升。图像帧率不要随意调整源码里的频率预设是一个固定值改变帧率等价于改变动作的速度语义。4.4 指令和视觉对不上模型看字不看图如果训练数据里的指令和图像内容匹配度不高模型会学到很偷懒的策略只根据语言指令做平均动作完全忽略图像。表现就是换一个场景、换一个物体颜色模型照样按旧动作执行。排查时可以做一个简单的 checker固定同一句指令换不同背景的图片做推理。如果输出动作完全不变基本可以确认模型被语言指令主导了。修复手段主要是数据清洗和指令模板统一。源码里对指令文本有很严格的模板要求不鼓励在指令里写多余信息。比如“把红色的杯子放到托盘上”模型默认只需要关注“红色杯子”和“托盘”其他描述词都会干扰特征对齐。如果数据本身没有问题还有可能是重建头把大部分语义责任揽过去了导致视觉语言骨干在文本侧过度学习。这时候可以尝试降低重建头的层数或者砍掉一部分重建 token 数量逼迫模型更依赖原始多模态输入。这个调参方向在实际排查中经常有效。5. 复现经验留档与后续改造建议关于损失权重我最终选择的是让重建损失占总损失的三成左右这个配置在中等规模数据下收敛最快但每个数据集都要重新标定。如果你在复现时发现训练 loss 下降很快、评测却不行大概率就是动作任务被重建任务过度压制了。如果你是准备把重建式 VLA 用到自己的机器人项目里我的建议是先别急着从头训练。挑一个开源预训练好的视觉语言骨干冻结住只在上面训练重建头和动作头先把 pipeline 跑通。等整体训练、部署、实机的闭环稳定了再考虑解冻骨干做全参数微调。这样能把变量控制到最小排查问题也更容易定位。另外一个小技巧在训练早期只看重建头输出的可视化结果不看动作指标。如果重建结果在训练集上都模糊不清说明骨干没有学到足够好的中间表征这时候做任何策略优化都是在沙地上盖楼。重建可视化是判断模型是否“理解”场景的最快标尺。我个人在踩过这么多坑之后如果只能留一条经验那就是重建式 VLA 的价值不在那个可爱的重建分支本身而在于它迫使整个模型去学习场景的结构化表示。源码里的一切细节数据对齐、tokenizer 选择、损失配比、EMA 平滑都是在为这个目标服务。真正读懂这条主线后面无论换骨干还是换下游任务你都知道该动哪里。