
这两年具身智能的热度大家有目共睹但真正把 VLM视觉语言模型接到机械臂上、跑通过完整闭环的人都知道中间那层“语义鸿沟”有多难跨。你让大模型描述桌面有什么东西它说得头头是道你让它给出“抓取红色杯子”的末端坐标和关节角它十有八九在胡编。Show-Harness 的核心思路就是在这个鸿沟上架一座桥不要求 VLM 直接输出机器人的底层运动参数而是通过一层语义动作接口让模型只负责“做什么”和“对哪个物体做”剩下的“怎么做、动到什么位置”交给确定性算法去解。这篇文章就把这套接口怎么设计、底层怎么接、仿真到实机需要注意哪些坑完整拆开讲一遍。无论你是做 ROS 机械臂开发、具身智能研究还是刚入门想在 Gazebo 里搭一个视觉抓取demo这套思路都值得参考。1. 为什么要给 VLM 装一道“语义动作闸门”1.1 先想清楚VLM 到底擅长什么不擅长什么先说个经常被初学者忽略的事实VLM 是语言模型和视觉编码器的组合它的强项在“语义理解”和“视觉描述”而不是“精确数值计算”。你把一张桌面图片丢给它它能告诉你图里有红色杯子、蓝色盒子、黄色香蕉还能大概判断“红色杯子在蓝色盒子的左边”。但这些判断是分布式的、模糊的模型内部并没有一个相机标定矩阵也没有手眼变换关系。可机械臂控制恰好是反过来的。底层控制器需要的是精确到毫米、甚至亚毫米级的空间坐标或者精确到弧度的关节角。VLM 输出的数字天生不靠谱原因有两个一是它本来就不是回归模型你让它输出x0.432它是在 token 预测而不是在解方程二是它对空间的感知是相对粗粒度的让它估计“杯子离机械臂底座 30 厘米”它可能给你 35 厘米这种误差放在抓取任务里就直接导致夹爪落空。打个比方这就像你让一位只看过菜谱的实习生去代替老师傅报盐的克数他能告诉你“少许”“半勺”但你说“请用精密天平称出 2.37 克”他一定翻车。VLM 就是那个实习生它适合做“语义决策”不适合做“精密测量”。1.2 直接让 VLM 输出坐标和关节角会踩哪些坑我见过不少项目上来就让 VLM 直接输出抓取点的三维坐标还让模型自己规划一串关节角度。听起来很酷但实际跑起来几乎每条路都是坑。第一个坑是精度漂移。VLM 输出浮点数要走 tokenizer模型在预测小数点后几位时本身就带随机性。你今天测同一个场景它可能给你x0.4318明天给你x0.4427这 1 厘米左右的波动可能把原本对准的夹爪直接带偏。如果是实机带相机标定误差、DH 参数误差末端的实际偏差还会更大。第二个坑是坐标来源错位。VLM 看到的是图像像素坐标而机械臂需要的是基座坐标系或者相机坐标系下的三维坐标。要想把像素坐标变换到机器人的工作空间需要相机内参、深度图、手眼标定矩阵这一整套流程本身就有误差叠加。让 VLM 做这个转换等于让一个不识数的店员帮你算账算错是常态算对是偶然。第三个坑是 token 浪费和幻觉。VLM 在生成一长串动作序列时容易“编”出不存在的步骤。比如你让它连续执行“拿起杯子放到盒子里再拿起来”它可能输出一个自定义的奇怪动作甚至中途把物体名字搞混。而且长序列输出会让推理时间变长、成本变高调试的时候你都分不清问题是出在模型还是出在控制。所以结论很简单VLM 应该被用在对的地方——做视觉语义理解、做任务分解、做物体识别而不是做矢量计算。1.3 语义动作接口到底是什么语义动作接口Semantic Action Interface本质上是一层中间表示它位于 VLM 的输出和运动执行器之间。它的形式通常是结构化的、类似“动作原语”的指令比如{action: pick, target: red_cup}这条 JSON 表达的意思非常清楚执行“抓取”这个动作目标物体是名为red_cup的实体。你看里面根本没有坐标没有关节角没有笛卡尔路径。VLM 只需要告诉我动作类型和目标物体的符号标识就已经完成了它最擅长的工作。至于red_cup当前在哪儿、要以什么姿态抓取、夹爪开合多少、机械臂末端要走什么路径这些都由下游的场景解析器和运动规划器来完成。这层“语义到运动”的翻译才是 Show-Harness 真正要解决的核心问题。这种设计带来的好处是显而易见的VLM 的输出变得可校验、可解释、可追溯。你可以随时记录pick red_cup这条指令也可以增加规则来过滤掉模型输出的非法动作。而底层运动规划模块依然用的是 MoveIt、OMPL、笛卡尔路径这类成熟的确定性工具不会因为模型的随机性而抖动。2. Show-Harness 接口设计动作原语、参数与约束2.1 接口到底长什么样既然要把语义动作作为 VLM 和机器人之间的“通用语言”那接口的格式定义就非常关键。我建议使用 JSON 作为载体不要用自由文本。原因很简单JSON 天然支持结构化校验、序列化、日志记录也方便后续做下游解析。一套基础的语义动作 JSON 大概长这样{ action: pick, target: red_cup, approach_offset: 0.12, max_force: 30.0 }其中action是动作名称target是物体的语义 IDapproach_offset是抓取前接近距离max_force是最大夹持力。这四条里面action和target是必须有的approach_offset和max_force是不是要暴露给 VLM取决于你的任务复杂度。再举个例子place动作会稍微复杂一点{ action: place, object: red_cup, target: box_b, orientation: upright }这里object是当前夹持的物体target是放置目标区域orientation是放置时的姿态约束。还有一类常见的动作是push常用于操作阀门、推滑块{ action: push, target: valve, direction: clockwise, angle_deg: 45 }这些例子虽然简单但已经覆盖了语义动作接口的核心理念动作名称负责定义“行为类型”参数负责定义“必要的语义条件”而不是具体的运动数据。2.2 动作原语怎么分层设计在实际项目中我不会把所有东西都塞进一个 JSON 里让 VLM 全权处理。更靠谱的做法是分三层第一层是高层语义动作原语对应着一组机器人能执行的宏操作比如pick、place、push、pour、slide、tap。每个动作原语背后其实都绑定了一套底层的确定性执行模板。第二层是语义参数层负责表达任务的约束条件比如目标物体 ID、目标区域 ID、运动方向、旋转角度等。这一层只涉及语义概念不涉及具体的坐标数值。第三层是执行层完全交给底层的运动规划和控制模块。例如pick动作模板展开后会依次执行移动到预抓取点、调整末端姿态、直线下降、闭合夹爪、抬升。这其中的每个中间点坐标都由场景解析器和运动规划器实时计算VLM 完全不需要看到。这种分层设计最大的好处是可以把“易错”的部分从 VLM 身上剥离。VLM 只做选择题不做事无巨细的指令生成模型输出的失败率自然会下降。2.3 让 VLM 填的参数为什么必须“少而精”很多第一次接触这个思路的人会忍不住在 JSON 里加很多参数比如末端速度、加速度、路径类型、避障开关、力控阈值……结果模型一次推理要输出八九个字段性能直线下降。这不是模型智商不够而是任务难度呈指数增长。每多一个数值参数模型就需要在视觉特征和数值之间建立联想这对一个基于 token 的语言模型来说是超高难度宿命。实测下来三个参数以内的动作成功率尚可一旦超过五个字段模型输出的非法 JSON、越界数值、字段遗漏概率明显上升。我的经验是接口只暴露“必选参数”和“极少数必要的可选参数”其余全部走底层默认值。比如pick动作默认预抓取距离 0.12 米、默认夹爪速度 50%、默认夹持力 30N这些都不需要 VLM 操心。只有类似“是否要避开某个物体”“是不是要额外旋转末端”这种任务级语义才值得让模型输出。2.4 如何让 VLM 稳定输出符合 schema 的 JSON接口定好了下一个问题就是怎么让 VLM 每次都乖乖按格式输出。直接丢一段提示词让它自由发挥十次里有三次会给你加料。所以需要在解析端加约束在推理端做引导。最简单有效的方式是在提示词里给出几个 JSON 示例并在系统提示中明确要求“只输出一个 JSON 对象不要任何解释和多余文字”。实际操作中还可以在后端用 Pydantic 或 JSON Schema 做严格校验解析失败就让模型重新生成一次。如果再进一步可以在解码层面限制输出 token只允许输出符合语法结构的 JSON 分支。目前有一些工具比如 outlines、jsonformer 可以做这种“结构化解码”配合本地部署的开源 VLM 效果很好。进阶一点的方案是直接用 LLaMA-Factory 这类工具对开源 VLM 做微调把“视觉输入 任务描述 → 语义动作 JSON”的训练样本做成数据集让模型在参数层面就学会这种接口格式。实测下来即使只有几百上千条高质量样本模型输出结构的稳定性也会比纯靠 prompt 好不少。3. 从语义动作到机械臂运动底层实现的关键环节3.1 整体 pipeline 拆解VLM 只当“指挥官”如果只用一句话概括这条链路那就是VLM 负责透明决策确定性模块负责精确执行。整体流程是这样的视觉信息进入 VLMVLM 结合任务指令输出语义动作 JSON这个 JSON 被动作解析器接收校验合法性后传递给场景解析器场景解析器通过目标检测或者实例分割把red_cup这样的符号标识转换成当前环境中的三维坐标和抓取姿态随后运动规划器比如 MoveIt基于这些坐标在机械臂的运动学约束下规划出一条可行的轨迹最后控制器执行轨迹完成抓取或放置。这里最关键的一点是VLM 在整个链路中并不直接接触坐标。它只输出“目标叫什么名字”而“目标在哪里”由场景解析器回答。这样就把“语义识别”和“空间计算”彻底解耦了两边都能各司其职。3.2 坐标从哪来场景解析器不是 VLM很多人会问那red_cup到底怎么变成一个三维坐标实际工程里场景解析器通常由视觉检测模块和坐标变换模块组成。先做目标检测或分割比如用 YOLO、SAM 或者 Grounding DINO 把图像中的物体识别出来得到物体在图像上的 2D 包围框或者分割掩码。然后结合深度图取物体中心点或者掩码质心对应的深度值换算到相机坐标系下的 XYZ。注意如果用的是深度相机这一步最关键的是深度图的去噪和对齐尤其是反光物体、透明物体和暗色物体会产生深度空洞。拿到相机坐标系下的坐标后还需要通过手眼标定矩阵把它转换到机械臂的基坐标系。这个标定矩阵的精度直接影响最终抓取效果。绝大多数“机械臂偏差”问题其实根源就在这段变换链上某个外参标定偏了 2 度传到末端可能就偏了两三厘米。如果是在仿真环境里事情会简单一些因为相机坐标系和机械臂基坐标系可以由世界坐标系统一给出不需要额外标定这也是我建议新手先跑仿真的原因之一。3.3 与 ROS2 / MoveIt / Gazebo 的对接底层运动规划这个模块几乎绕不开 ROS 生态。我自己目前用比较多的是 Ubuntu 24.04 搭配 ROS2 Jazzy再装 Gazebo Harmonic 做仿真验证。机械臂方面UR5e 和 Panda 这两个型号都有比较成熟的 URDF 模型和 Gazebo 插件适合用来做视觉抓取仿真。URDF 模型配置这块建议用 MoveIt Setup Assistant 生成机器人描述包重点检查几个部分规划组是否覆盖了手臂的完整关节链碰撞免碰撞矩阵ACM是否配置正确末端执行器夹爪或吸盘是否定义了单独的规划组。很多人在仿真里轨迹规划一直失败原因往往是 ACM 没配好或者规划组少选了一个关节。ROS2 层面还需要通过gazebo_ros2_control把 MoveIt 的轨迹指令接到 Gazebo 仿真模型上。URDF 里需要配置好ros2_control标签声明每个关节的硬件接口类型这样ros2_control才能正常发布关节状态、接收关节位置指令。如果你用的是真实机械臂比如 UR 系列可以通过ur_ros2_driver或直接发 URScript 来执行关节目标。这里有个小提醒不同厂家的机械臂URDF 中关节轴的朝向定义不一样JAKA 这类国产机械臂在 DH 参数和旋转顺序上尤其容易出现坑经常表现为模型里关节角正常实际机械臂却“掰成了奇怪姿势”。遇到这种情况优先核对各个关节的零位和旋转方向不要急着改算法。3.4 执行参数怎么定几个关键的计算和默认值当场景解析器给出抓取点的位置和姿态之后仍有一些参数需要规划主要包括抓取姿态、TCP 偏移、接近距离和速度限制。先讲抓取姿态。对于水平桌面上的杯子抓取姿态通常是末端 Z 轴朝下和物体表面法向量对齐。如果物体是倾斜的比如一个斜放的盒子需要用实例分割得到的点云计算表面法向量再把末端姿态调整到与法向量一致。这一步在仿真里可以考虑用网格模型的法向量在实机上更推荐用点云主成分分析PCA估算。然后讲 TCP 偏移。夹爪的指尖点不一定和机械臂法兰中心重合这个偏移通常要单独标定。如果你在 MoveIt 里给末端执行器添加了tool0到gripper_tip的固定变换那么规划出来的位置就是指尖的位置否则就是法兰中心的位置抓取时会发现夹爪中心总差一截。接近距离的作用是防止机械臂在下降过程中撞到目标物体或桌面。我的习惯是设置一个安全值先从上方 20 厘米的预抓取点直线下降在接近目标物体 5 到 8 厘米处停下再缓慢下降直到夹爪包住物体。整个接近过程的速度比例限制在 0.1 到 0.3 之间不要开满速否则即便夹住了也会把物体撞飞。4. 实操从零搭一个 Show-Harness 最小闭环4.1 仿真环境搭建为什么先用 Gazebo我强烈建议所有第一次接触这套思路的人先在仿真里跑通再考虑实机。原因有三一是安全机械臂失控不会砸坏东西或伤人二是可重复每次环境状态都可以完全复位方便调试三是成本低你不需要等实体机械臂到位就能把算法逻辑全部验证一遍。具体搭配上Ubuntu 24.04 下安装 ROS2 Jazzy、Gazebo Harmonic 和 MoveIt 是比较顺的一条路。机械臂我建议选 Panda 或 UR5e它们的 URDF 模型在开源社区里维护得比较好。安装好之后先启动 Gazebo 加载机械臂模型再启动 MoveIt 的 demo 节点手动拖拽目标位姿让规划器生成一条轨迹确认环境基本可用。这一步别急着接 VLM先把“运动规划闭环”跑通后面接入语义动作时你才能判断问题出在哪一层。4.2 语义动作解析器怎么写语义动作解析器的职责很简单接收 VLM 返回的 JSON做结构校验和字段提取把语义动作转换成下游可以执行的数据结构。用 Python 写的话我会直接用 Pydantic 定义模型解析失败就触发一次重试from typing import Literal, Optional from pydantic import BaseModel class SemanticAction(BaseModel): action: Literal[pick, place, push, pour, move] target: str object: Optional[str] None direction: Optional[str] None angle_deg: Optional[float] None approach_offset: Optional[float] 0.12这样VLM 输出{action:pick,target:red_cup}后Pydantic 会自动补上approach_offset的默认值。如果模型输出了未知动作名校验会直接报错你可以选择让 VLM 重新生成也可以走一个默认的 fallback 策略比如回退到move动作。4.3 场景解析器要做什么场景解析器是整个系统中最重要的“翻译官”它把red_cup变成机械臂能用的坐标。基于 YOLO 或者 Grounding DINO 实现目标检测再用点云分割获取每个物体的中心点和尺寸信息是比较常见的方案。在 Gazebo 仿真里你还可以直接从仿真接口读取物体的真实位姿然后加一点人为噪声来模拟检测误差这样既能调试算法也能测试系统对误差的鲁棒性。坐标变换部分如果仿真里相机固定在机械臂末端或者桌面上方记得在 TF 树里确认好坐标系之间的变换关系。我是习惯把所有坐标统一到base_link坐标系再喂给 MoveIt这样后续规划时不需要再反复切换坐标系能少踩很多坑。4.4 接入 VLM 并跑通一次完整抓取环境搭好、解析器写好后下一步就是把 VLM 接进来。如果你本机有显卡可以本地部署一个 Qwen-VL 这类开源模型如果机器配置一般调用云端 API 也完全可行只要在提示词里约束输出 JSON 格式即可。我常用的一份提示词大概长这样你是机器人控制接口你只能输出一个JSON。当前桌面上有物体red_cup, blue_box, yellow_banana。 任务把红色杯子放到蓝色盒子里。 输出格式{action:pick,target:...}VLM 输出{action:pick,target:red_cup}后系统依次调用场景解析器、MoveIt 规划器、夹爪控制。等pick完成后再调用一次 VLM让它输出place动作。这里我建议一次只让 VLM 做一个动作不要让它一口气输出整个任务序列长序列的稳定性远不如短序列。整个闭环跑通之后你会对“语义动作接口”的设计有更直观的体感VLM 的输出会被“锁”在接口的框架里底层规划器怎么动VLM 一概不关心但最终机械臂又确实执行了它想表达的任务。5. 实战中的常见问题与排查技巧5.1 “VLM 说抓到了但夹爪空了”以及所有定位失败这是做视觉抓取最经典的翻车现场。VLM 正确识别出了red_cup动作解析也正常场景解析器返回了一个坐标机械臂也动了但夹爪合上之后什么都没有。这类问题的排查顺序很重要。先看 VLM 输出是不是选错了目标再看检测框中心是不是偏了。如果物体本身被遮挡或者环境光导致深度图出现空洞点云质心很容易偏移半个物体宽度。最容易让人忽略的是检测到的物体中心不一定等于可抓取中心。比如一个圆底杯子2D 检测框中心可能落在杯壁侧面而不是杯身的轴心线附近。我习惯的做法是在运行时把场景解析器的结果可视化也就是在图像上把检测框、抓取点、深度值都画出来录一段视频回放。很多时候一眼就能看出问题出在“检测框在但深度不对”还是“深度对但方向不对”。5.2 机械臂偏差轨迹规划对了末端却偏了几厘米“机械臂偏差”是另一个高频词。表现形式通常有两种第一种是仿真里一切正常实机上末端位置总是系统性偏移几厘米第二种是规划器计算出的运动学解就不对机械臂动起来姿态看起来很奇怪。第一类问题优先怀疑标定。手眼标定矩阵不准确、TCP 标定偏差、相机安装松动任何一个环节出问题都会表现为末端漂移。建议先用示教器把机械臂末端移动到标记点然后在机器人系统里对比实际位置和模型算出的位置能快速判断是标定问题还是运动学参数问题。第二类问题优先检查 URDF 的关节轴定义和 DH 参数。每个关节的正方向定义不一样模型里的关节角范围也可能和真实机械臂不一致。尤其是 JAKA 这类机械臂其 DH 参数和旋转顺序不同于常见的 UR 系列直接照搬 UR 的配置很容易让规划器给出“看似合理但实际不可行”的解。5.3 动作顺序混乱、VLM 输出幻觉如果你发现 VLM 在同一个任务里忽前忽后一会儿说先抓杯子一会儿说要先推挡块甚至自己造出grab、lift_up这种接口中根本不存在的动作名那大概率是提示词没有约束住它。解决思路有两个方向。一是把任务节奏控制权从 VLM 手里拿回来由状态机或者脚本控制任务推进每一步调用一次 VLM 并限定可动作范围。二是用一个更“封闭”的提示词集合把所有可选动作显式列出来严格要求模型只能从中选择。如果实机场景特别固定终极方案是用 LLaMA-Factory 这类微调框架针对自己的语义动作接口做一个小型微调数据集。不需要多大我实测 2000 条左右的人机标注样本就足以让开源模型在目标场景下稳定输出接口定义的 JSON 动作。因为这种模式的本质是“让模型背诵一种输出模板”难度远低于让它学会写代码。5.4 Token 爆炸、延迟高、成本高还有一个经常被忽略的工程问题是推理延迟和 token 成本。很多人把桌面图像原图直接丢给 VLM一张 1080p 的图转成 base64再附带一大段长 prompt一次推理可能耗时好几秒费用还不低。我的经验是进入 VLM 之前先把图像降到 512x512 左右减少视觉 token 数量让 VLM 只输出 JSON不要说理由把固定的物体列表和坐标信息放在解析器里维护不要在 prompt 里塞完整语义地图。这样处理后延迟和成本通常能降一半以上。下面这张表是我的常用问题排查速查现象可能原因排查方法VLM 输出格式乱七八糟prompt 约束不够、模型版本弱增加 Few-shot 示例、加 JSON Schema 校验、考虑微调动作序列顺序错乱长序列生成不稳定改成状态机控制一次只让 VLM 出一个动作抓取落空目标中心估计偏、深度图空洞可视化检测框和抓取点、用点云质心、增加接近确认末端系统性偏移手眼标定或 TCP 标定不准对标标记点、重新标定、检查 TF 树仿真正常实机乱动URDF 关节定义不对对比示教器读数检查 DH 参数和关节方向推理很慢图像分辨率过高、prompt 太长图像降采样、精简 prompt、只让模型输出短 JSON5.5 关于“一次只做一步”的碎碎念最后分享一个我踩过好几次坑之后得出的习惯无论 VLM 能力多强我都不会让它一口气输出一整套长序列动作。原因很简单长序列的“幻觉率”是随步数累积的第一步错后面全部白搭。与其赌模型的全局规划能力不如把任务拆成“感知 - 决策 - 执行 - 确认”的小循环每一步都由语义动作接口把关由底层确认执行结果再进入下一步。这样既降低了模型能力要求又让整个系统在一个一个的闭环中变得可控、可调试。如果你正准备在自己的机械臂项目里引入 VLM我个人建议的路子是先用仿真搭出最小闭环把语义动作接口的格式定死再去换更强的 VLM甚至微调自己的模型等视觉定位和运动规划都稳定了再考虑迁移到实机。这套顺序能帮你把“模型没选对”和“代码不对”两类问题彻底分开。顺着这个方向往下做你迟早能跑出一个像 Show-Harness 这样既能让 VLM 听懂人话又能让机械臂痛快干活的系统。