我的机器人操作实验台被同一个问题卡了快两周:机械臂抓取、放杯子都准得离谱,但让它完成"把桌上的苹果放到红色托盘里,再把托盘放到厨房台面上"这种两段任务时,它经常做完第一步就愣住。后来发现问题不在控制精度,而在任务规划。这个项目围绕 Semantically Guided Task Planning 展开,核心思路是让 Large Language Models 去监督一个 Vision-Language-Action Model 的训练——用语言把长任务拆成有语义层次的子目标,再把这些子目标喂给 VLA 模型做动作学习。听起来不复杂,但真正做到"语义引导"和"监督"这两个词落地,中间全是坑。这篇文章把我从架构设计、数据管线、训练调试到端侧部署的完整过程都写下来,希望能帮你少走点弯路。
1. 内容整体设计与思路拆解
1.1 任务规划为何成为 VLA 模型的短板
VLA 模型的全称是 Vision-Language-Action Model,它做的事情看起来很统一:输入一张当前观测图像,加上一条语言指令,输出一组动作(末端位姿序列、关节角增量,或者离散的动作原语)。这类模型在单步操作任务上表现相当好,比如抓取指定物体、按下开关、推开门。因为它们本质上是在学习一个"条件策略",条件就是眼前的像素和耳朵边上的指令,输出就是下一步动作。
但问题出在长时任务上。你让机械臂完成"把苹果放进托盘、再把托盘放到台面"这样的任务,底层策略其实并不缺"放"的能力,缺的是"当前该放哪一个、放完之后下一件事是什么"这种规划信息。端到端 VLA 模型很难自己长出任务分解能力,原因很简单,训练数据里的动作序列是连续的,但语义事件(比如"苹果已放入托盘")没有被显式建模,模型学到的更多是"碰到什么样子的图像就往哪边挪",而不是"我现在处于哪个子任务阶段"。
我打个比方,端到端 VLA 像一个方向感极强但没地图的司机,指令给到哪里,它就能开到哪里;但没人告诉它途经几个路口、每个路口拐之前要确认什么标志,它就容易在长途线路上开岔。任务规划就是要解决这个"缺少地图"的问题。而语义引导,相当于在这张地图上标出带文字说明的里程碑,让模型每一步都知道自己踩在哪个点位上。
1.2 为什么选择 LLM 当"语义导师"
既然要在训练过程中给 VLA 模型补充任务规划级别的语义监督,谁来提供这个监督信号就非常关键。这里我们选择了大型语言模型担任"语义导师"角色,而不是自己写规则或者人工标注。原因有三层。
第一,任务分解本身高度依赖常识。拿"把苹果放到托盘再放到台面"来说,正确的子目标顺序是"放苹果→转移托盘→定位台面",而不是反过来。这种顺序约束可以写成专用规则,但换了任务就得重写。LLM 天然具备大规模文本里学到的常识和步骤推理能力,稍微设计一下 prompt 就能在不同任务之间泛化,不用为每个新任务去写分支逻辑。
第二,训练监督信号需要带有语义层级。直接让 LLM 输出"移动 12.5 厘米"没有意义,因为这不是任务规划,而是动作指令,而且这些低层动作很难迁移。我们需要的是"当前子目标描述"和"完成条件描述",比如"将苹果从桌面拿起""目标容器:红色托盘"。这些都属于语言语义层,恰好是 LLM 最擅长生成的内容。
第三,这套方案的投资回报率很高。实际部署时,LLM 不需要在每一步都参与运算,只需要在训练阶段生成监督标签,以及在推理阶段做粗粒度决策。也就是说,VLA 模型获得了一个"随时能给语义辅导的老师",但真正做实时动作执行的还是轻量级的 VLA 主干,这比在端侧硬跑一个超大推理模型要现实得多。
我一开始也犹豫过,要不要让 LLM 直接接管推理时的每一步决策,但后来发现没必要。把规划职责交还给一个专门的小型语言模型(SLM),把动作执行职责留给 VLA,两者分工干净,调试起来也轻松。
1.3 整体架构:把规划与执行拆成两层
这个项目的核心架构可以总结为一条链:任务输入 → 顶层语义规划器(LLM/SLM) → 子目标序列 → 低层 VLA 策略 → 动作执行 → 环境反馈。
顶层规划器不直接出电机指令,它只负责把用户的长指令转换成若干条带顺序的子目标文本。低层 VLA 策略负责接收当前观测图像和当前子目标,输出具体的动作。为了提高鲁棒性,我在中间加了一个很薄的"语义缓冲层",保存两个东西:当前正在执行的子目标下标,以及该子目标的历史观测摘要。这样一旦底层执行器发现连续几步都没有推进子目标,就可以把控制权交还给规划器,让它重新规划或者给出新的尝试方向。
这个架构里最关键的设计决策是:任务规划与任务执行解耦。传统一层式 VLA 把所有能力揉在一个模型里,出问题时分不清是哪一部分的锅。拆成两层之后,规划器出了问题,你直接看它输出的子目标序列是否合理;执行器出了问题,你看它给定子目标下的动作预测是否合理。定位 bug 的时间至少少一半。
同时解耦也带来了训练上的灵活性。规划器可以单独用文本数据集微调,VLA 主干可以保持大参数量的视觉骨干不动,只训练动作头。后面章节我会把每个环节的细节展开讲。
2. 核心细节解析与实操要点
2.1 语义样本生成:先让 LLM 当标注员
要让 VLA 模型在训练阶段就学会"按照子目标执行动作",首先得有带子目标标签的训练数据。我们的做法是让 LLM 先当标注员。具体来说,从仿真环境或者真实机器人上采一批成功的专家轨迹,每条轨迹包含一系列 (观测图像, 动作指令, 动作) 三元组。然后用 LLM 把这条轨迹切成若干语义段,为每个段生成当前状态描述、当前子目标文本和完成条件。
这里有个很容易踩的坑:直接拿原始文本指令去让 LLM 切分,它容易切出"手臂缓慢向前移动"这种毫无规划价值的句子。所以我在 prompt 里强制要求标注员只输出三种语义类型:物体状态(苹果在桌面上)、操作目标(将苹果放入红色托盘)、功能状态(苹果已就位)。这样切出来的子目标才是稳定的抽象层级,而不是对动作的低层描述。
还有一点是关于子目标命名的一致性。同一类任务,LLM 这一次写"放置苹果",下一次写"放下苹果",再下一次写"将苹果放到托盘里",虽然语义相同,但文本表达不一致会让 VLA 模型的学习变得困难。所以标注完以后还要加一步"子目标归一化":做一个轻量的规则映射加向量检索,把相近表达统一到同一套词表。这个步骤极其重要,它能大幅降低训练数据的噪声。
2.2 子目标表示:三种方案怎么选
训练 VLA 时,子目标最终要变成模型能消费的 token 或向量。我在项目里对比过三种表示方式,各有取舍。
第一种是自然句子表示,例如"拿起蓝色杯子"。优点是可读性和可调试性最强,人类可以肉眼确认规划器的输出是否合理,LLM 生成也最自然。缺点是句子表达多样性高,如果不做归一化,模型容易被同义表达干扰。
第二种是结构化谓词表示,例如 pick_up(agent=robot, target=blue_cup)。优点是非常适合程序解析,下游如果要做形式规划器(比如转成 PDDL),可以直接套。缺点是 LLM 生成这种形式化描述时容易出错,而且表达能力受限,一些语义含糊的状态很难用固定谓词写干净。
第三种是技能调用表示,例如 skill_grasp(target=blue_cup, mode=power)。这介于谓词和动作原语之间。优点是能直接在动作层面约束输出,减少规划器生成 VLA 处理不了的指令。缺点是技能库需要人工预定义,一旦遇到库外任务就失效。
三种方案对比下来,我实际项目里选的是混合方案:训练时以自然句子为主,同时让模型输出一个辅助的"技能候选词表",用来在推理阶段做动作可行性约束。简单说,规划器给出自然语言子目标,VLA 先解析这个文本,再用技能词表限制动作头的选择范围。这样既保留可读性和生成质量,又避免输出不可执行的动作。
2.3 语义指令注入 VLA 的几种做法
子目标文本不会自己钻进模型,需要考虑怎么把语义信息注入到 VLA 策略里。我试过三种做法,说下效果和取舍。
第一种是文本 prompt 拼接。把当前子目标文本、上一子目标的完成状态、全局任务描述拼成一段连续的指令,连到 VLA 的文本编码器里。这个做法改动最小,你的 VLA 主干如果是 CLIP 风格的文本编码器,直接拼进去就能用。缺点是有时候模型分不清当前子目标与全局任务的主次关系,所以我建议在 prompt 里用特殊标记把"当前目标"单独框出来,比如用<subgoal>...</subgoal>标签包住。
第二种是多模态特征融合。把子目标文本和视觉观测一起送进 transformer 层做 cross-attention,让语义信息在注意力层里和视觉特征交互。这个对模型结构改动比较大,但效果往往比 prompt 拼接好,因为语义信息不再只是初始化输入,而会参与每一层的信息交互。如果你的 VLA 主干是 transformer-based,可以试。
第三种是辅助语义损失。在训练时额外加一个"当前子目标预测"的头,让模型除了预测动作之外,还要根据视觉特征判断自己正在执行的子目标是什么。这相当于逼着视觉编码器去提取任务阶段相关的特征。推理阶段这个辅助头可以整个丢掉,不影响部署,只在训练时起正则化作用。
我的建议是,第一版先用 prompt 拼接加辅助语义损失,跑通之后再考虑改 cross-attention。少改结构,先验证思路,能省下大量时间。
2.4 损失函数:动作为主,语义为辅
模型训练时我用了一个多任务损失:主损失是动作模仿损失,采用离散动作分类交叉熵或者连续动作的 L2 损失,取决于你的 VLA 输出形式。在这个基础上叠加辅助语义损失,也就是让语义预测头用交叉熵预测当前子目标类别,或者用对比学习拉近视觉特征与子目标文本 embedding 之间的距离。
两个损失的权重我没有用默认的 1:1,而是把语义损失权重压得很低,大概 0.2 左右。原因很简单:主任务毕竟是动作生成,如果让语义监督信号权重过高,模型会倾向于把表征能力花在"描述当前状态"上,而忽略了"下一步动作推荐",最终动作精度反而下降。我这里给个经验值:动作损失权重 1.0,辅助语义损失 0.2,优化器用 AdamW,基础学习率 1e-4。
我实际实验里还对比过完全去掉辅助损失的情况。结果很有意思:在动作精度上两者差不多,但在长时任务累计成功率上,加辅助语义损失的版本明显更高,高出大概 15 个百分点。原因推测是辅助损失迫使模型显式编码了状态语义,一旦当前子目标切换,特征空间的跳变更加明确,动作策略更容易跟随语义切换而切换。
3. 实操过程与核心环节实现
3.1 数据管线:从轨迹到带语义标签的训练集
在仿真环境里,生成轨迹本身不是瓶颈,自动化 rollout 就可以拿到大量成功样本。真正的瓶颈在把轨迹切分并标注成"子目标化"的训练集。我的数据管线分三个阶段。
第一个阶段是轨迹切分。我不直接拿整条轨迹做训练,而是先检测轨迹中的"关键事件帧"。做法是看物体姿态变化率:如果某个物体的位置忽然发生明显位移,说明一个操作事件发生了,这就是语义段的边界。这一步用规则检测就够,不需要机器学习。检测出来的边界用于确认 LLM 输出的子目标分段是否和真实事件一致。
第二个阶段是 LLM 标注。把每个语义段内的观测摘要(物体位置、手爪状态、末端位姿轨迹摘要)和段前后的状态变化描述传给 LLM,让它输出该段的子目标文本、完成条件和失败条件。这里强调一点:我不把整条轨迹的原始数据全塞给 LLM,而是给一段压缩的摘要文本。模型上下文有限,原始 token 序列太长会稀释注意力,摘要反而让标注质量更高。
第三个阶段是数据清洗。做完标注后,建议人工抽检 10% 到 20% 的数据,重点检查子目标文本是否和执行行为一致。如果有标注错误,最好建立一个小型的脏数据规则库,比如"文本里有拿起,但动作轨迹未发生 z 轴变化"就标记为可疑。清洗后的数据进入训练集,脏数据可以直接丢弃,不要抱有侥幸心理。
3.2 规划器 Prompt 模板示例
顶层规划器用的 LLM 我选择了参数量在 8B 左右的开源模型,在端侧部署时再用量化压缩。系统 prompt 我调了很多版,下面这个版本实测最稳。
[角色] 你是一个室内操作机器人任务规划器。你只负责将用户的长任务指令拆解为有序的子目标。 [输入] 任务描述:{task} 当前已知可执行技能:{skill_list} 历史子目标摘要:{history} [要求] 1. 每个子目标必须描述一个语义可验证的状态变化。 2. 子目标必须从技能列表中选取可执行的能力。 3. 输出格式严格为 JSON,禁止输出任何其他文本。 [输出格式] { "subgoals": [ { "id": 1, "text": "抓取桌上的苹果", "verify_condition": "苹果在手爪中" }, { "id": 2, "text": "将苹果放入红色托盘", "verify_condition": "苹果在红色托盘内" } ] }这里面最关键的是 verify_condition。它给底层的语义缓冲层提供一个文本形式的验收条件,底层执行器每走几步就会用语言模型或者规则校验器检查这个条件是否满足。如果没有它,规划器输出的子目标序列就只是一串空话,机器根本不知道"这一步算不算完成"。
我在实际测试中还发现,在 prompt 里加入"历史子目标摘要"字段能显著抑制 LLM 规划器的重复输出问题。没有历史字段时,模型容易在第三次子目标重复第二次的内容;加了摘要之后,重复率下降非常明显。
3.3 训练配置与微调参数
VLA 主干的微调我用了 LoRA,单卡也能跑起来。下表是实测下来比较稳的配置:
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| 视觉编码器 | 冻结 | 预训练编码器语义已够用,降低显存占用 |
| 文本编码器 | 冻结 | 与视觉编码器保持一致,减少分布偏移 |
| LoRA rank | 32 | 动作头与语义头都插入 LoRA |
| 学习率 | 2e-4 | 超过 1e-3 会出现发散 |
| Batch size | 64 | 混合了正样本和负样本 |
| 语义损失权重 | 0.2 | 见 2.4 节,太高会伤动作精度 |
| 训练轮数 | 20 epoch | 早停在验证集动作损失最小处 |
冻结视觉编码器这个决定我最初有点犹豫,担心大参数骨干不更新会限制模型能力。实测下来,VLA 的视觉骨干是在互联网海量图文数据上预训练的,识别能力和通用性已经足够;真正缺的是视觉特征到动作输出的对齐,这部分靠训练动作头就能解决。冻结之后显存占用明显降低,训练速度提升近一倍,动作精度反而没有下降。
数据里的负样本也别忘了。我指的不是 NLP 里的负样本,而是"当前状态不满足子目标时应该停下等待"的训练样本。比如子目标是"抓取苹果",但当前观测里桌子上没有苹果,模型应该输出"等待/重新扫描"而不是胡乱抓取。这种样本采集起来容易,但让 VLA 学会"判断条件不满足时按兵不动"非常有效。加入负样本之后,我的部署场景里因为误动作导致的任务失败率降了四成。
3.4 推理执行:两级任务调度的落地逻辑
训练好模型之后,推理阶段的调度逻辑可以直接用一段简单的状态机描述。整个流程如下:
机器人收到用户任务,先把任务文本交给顶层规划器,规划器输出 JSON 格式的子目标序列。语义缓冲层初始化 current_index 为 0。VLA 模型开始执行时,输入当前观测图像和 subgoals[current_index].text,输出动作并执行。每执行完一步,缓冲层会做一个轻量校验:从当前观测中抽取关键状态(物体位置、手爪开合度),和当前子目标的 verify_condition 做匹配。如果匹配通过,current_index 加一,进入下一个子目标;如果连续 N 步没有匹配通过,就把控制权交还给顶层规划器,让规划器基于历史执行摘要重新生成后续子目标。
这个 N 我一般设成 15 到 20 步。设太短容易误判,比如抓取过程中物体滑动导致的暂时未成交会频繁触发重规划;设太长则会浪费执行时间,机器人卡在同一个错误的子目标上反复尝试。实际场景里可以自己调,但我测下来 15 步以下的误判率直线上升。
再说一个我踩过的坑:不要只在 VLA 里输入当前子目标文本,把全局任务文本也带上。否则当执行到第三个子目标时,模型容易忘记整个任务的终局是什么。全局任务文本的主要作用是让 VLA 能做出"长远规划"语义上的修正。比如它正在执行"把苹果放入托盘",同时知道全局目标是"把托盘搬到台面",那么它的姿态估计会更倾向于把苹果放稳而不是随手丢进去。
3.5 端侧部署:让 LLM 规划器跑在机器人身边
关于 on-device LLM 的部署和模型压缩,最近讨论度很高,我在这里把实际做法说细一点。端侧部署规划器的原因很直接:真正的机器人不可能每次都把图像和任务文本传到云端等一个超大型模型返回。延迟高、网络抖动影响大,而且在工厂这类环境里数据隐私很敏感。所以我最终把规划器模型压缩后部署在了机器人旁边的边缘计算设备上。
模型压缩我按顺序做了四步。第一步是知识蒸馏,用一个大参数量 LLM 作为教师,在任务规划数据集上蒸馏出一个 3B 规模左右的学生模型。这里数据集的覆盖面比模型参数量更关键,蒸馏出来的学生模型在常见任务上的规划成功率能达到教师模型的 95% 以上。第二步是量化,用 4-bit 量化把 3B 模型压到约 1.8GB 内存。这里要注意,量化之后要重新跑一遍规划器的输出测试,因为低比特量化可能会导致 JSON 输出的 token 概率分布变形,偶尔会出现不可解析的输出。第三步是结构剪枝,把注意力头里贡献最低的那部分去掉。我在这个项目里剪掉了 20% 的注意力头,跑测之后准确率没有显著下降。第四步是 KV cache 优化,因为规划器每次输出子目标序列长度不太长,但历史上会多次调用,所以缓存之前几轮的语音状态下文可以有效降低推理时延。
VLA 这边我做了一个更直接的轻量优化。动作头原本输出 256 维连续向量,我把它改成离散动作库上的分类输出,配合技能词表约束。这个改动让 VLA 模型变小了不少,又因为动作空间受限,推理稳定性反而更高。端侧部署不是简单塞进设备就完事,每一步都要回到数据上验证输出质量。
4. 常见问题与排查技巧实录
4.1 规划器输出与动作空间不对齐
这是我最开始做这个项目时遇到的头号问题。规划器信心满满地输出子目标"切好苹果",但实际上 VLA 的动作空间里根本没有"切"这个原语,只有"抓取/放置/移动/推开"四种。结果模型在推理阶段无论怎么解析文本,都映射不到可执行动作上。
解决这个问题得分两步走。第一步是缩窄规划器的输出范围,在 prompt 里强制传入当前技能列表,并明确要求每个子目标必须从列表中选择能力。第二步是在 VLA 输入端加一个动作可行性校验器,一旦文本偏离技能列表,就直接丢弃并触发重规划。经过这两步之后,规划器输出不可执行指令的比例从 40% 降到 5% 以内。别小看这个校验器,它只是一个薄的规则层,但能把整条链路的稳定性提升一个量级。
4.2 规划器幻觉导致计划不可执行
LLM 的幻觉在任务规划场景里同样存在。实际测试中,规划器有时会编造任务里不存在的物体,比如用户说"把苹果放到盘子",规划器子目标里却出现了"先拿起香蕉"这种乌龙。更隐蔽的是,它会预测出环境中根本不存在的物理状态,例如"苹果已在托盘内"。
我的排查思路有两个方向。一是输入侧约束,把环境中的实际物体清单传入 prompt,规定规划器只能引用清单里的物体,并在输出后再做一次字符串匹配,发现清单外的词就触发重采样。二是输出侧自洽性校验,同一任务让规划器采样三次,取子目标序列的交集或者投票结果。这个方案慢一点,但可靠。作者模式实测中,三次采样把所有候选子目标做并集,再用规则把不一致的地方合并,幻觉出现的概率显著下降。
另外,别忽略 verify_condition 的校验逻辑。如果校验条件本身写得太抽象,比如"苹果处理完毕",规则校验器根本没法判断。要把条件写成可计算的谓词,比如"苹果 x 坐标在托盘范围内且 z 轴高于托盘底面 2cm"。这类条件在仿真环境里很好抽取,在真实环境里也能通过粗略的姿态估计做到。
4.3 训练数据里有脏噪声怎么处理
数据噪声几乎没办法避免,尤其是当 LLM 自动生成子目标标注时,一定会有少量标签和实际动作不一致。脏数据不清理,模型学到的就不是"苹果放入托盘时应该放下",而是"苹果的某种状态可能和放这个动作有弱相关"。
我的方法分两层。第一层是规则过滤,基于事件边界检测的结果,把 LLM 标注的子目标和实际动作事件做交叉验证。例如标注写"抓起苹果",但轨迹数据显示手爪在接近苹果之前就已经闭合,那就是异常,直接丢弃。第二层是置信学习,用一个在干净子集上预训练的语义分类器,给整批数据打置信分,把低置信样本再交给人工复核。注意不要盲目丢弃低置信样本,因为有些只是文本表达比较特殊,并不是真的标签错误;人工复核这一步在项目启动阶段一定要做,等规则库成熟之后可以减少复核比例。
4.4 训练不收敛、过拟合与辅助损失失衡
我在训练初期遇到过一次非常典型的发散:前两个 epoch 的动作损失下降正常,第三个 epoch 突然飙升,然后一去不回。后来定位到原因是学习率偏高,LoRA 里低秩矩阵的更新幅度过大,导致视觉特征空间被大幅扰动。把学习率从 1e-3 调到 2e-4 之后,训练曲线立刻恢复正常。
过拟合的问题更多出现在数据量不够时。VLA 主干虽然冻结,但动作头和语义头本身有数千万参数,如果训练集只有几千条轨迹,后几个 epoch 验证损失就容易反弹。我的经验是观察到验证集动作损失开始上升时,不要急着加正则化,先看数据多样性够不够。如果场景只有一种桌面布局,最简单的做法是加仿真时域随机化,把物体位置、光照、背景颜色都做随机,比堆数据量有效得多。
辅助语义损失失衡这个问题,前面说过一次,但值得单独列出来。如果你发现模型的动作精度不高,但语义预测准确率却异常高,就说明语义损失权重太大了,模型的容量都被拿去维护语义表征,忽略了动作策略。这时候把权重调低,或者干脆在最后五轮训练里把语义损失关闭,让模型把动作头重新校准回来。
4.5 仿真跨真机的语义鸿沟
这个项目最初是在仿真环境里训练和验证的,转移到真实机器人时,我发现一个非常典型的现象:模型在仿真环境里执行子目标切换很流畅,到了真机上偶尔会出现"视觉上物体就在眼前,但模型判定子目标未完成"的僵局。原因是语义校验器在仿真里依赖精确的物体姿态,真机上物体检测有误差,导致最终状态判断不稳定。
我的解决方案有两个。第一,在子目标完成条件里加入时间累积判定,不要求单帧满足条件,而是连续 5 帧都满足才算完成。这能过滤掉检测器抖动带来的误判。第二,引入一个简单的重试机制,当前子目标未通过校验时,VLA 不是卡死原地,而是先做一个小幅度的"巡检动作"(比如左右移动 5 厘米重新观察),再重新预测动作。这个方法朴素但很有效,真实场景操作成功率提升了大概 20%。
另外,仿真到真机之间的视觉差异也是一个大问题。我在仿真阶段就特意加了相机噪声、光照变化和物体纹理随机化,而不是等到真机阶段再去微调。这一步要尽早做,越晚换到真机上,你越难分清任务失败是因为规划器还是因为视觉语义没对齐。
我在实际项目里最深的体会是,语义引导任务规划这条技术路线真正的难点不是"模型不够聪明",而是"工程链条太长"。每一个环节,数据标注、子目标表示、损失设计、推理调度、端侧压缩,单独看都不难,但任何一个地方没做好,最终成功率都会被拉垮。如果你也想做类似的项目,我的建议是先从一条固定场景的细分任务开始,把数据管线和语义校验逻辑打扎实,再用 LLM 换着花样去提高泛化,别一上来就挑战全场景。最后再分享一个小技巧:模型输出子目标序列时,一定把当前子目标文本单独框进特殊标记里,别和全局任务混在一起。这个细节最不起眼,但对底层执行效果影响却非常大。