做机器人操作的朋友应该都有过这种体验一个简单的“把红色方块放到篮子里”写传统控制代码可能要一整天感知模块、坐标转换、轨迹规划、抓取判定各写一版最后还要为了某个边界情况反复调参。后来看到VLM视觉语言模型在图片问答上的表现我就在想既然模型能看懂画面里的物体和空间关系能不能让机器人直接“看着画面、听着指令”把活干了Show-Harness就是围绕这个想法搭的一套框架核心只有一句话用单个VLM智能体完成从视觉理解到动作决策的整条链路不再为每个任务单独写控制逻辑。这篇文章会把这套方案的架构、复现步骤和我在实测中踩过的坑完整讲清楚适合正在做机器人操作研究、准备入门具身智能或者想用低成本方式验证VLM控制效果的工程师。1. 为什么敢用一个VLM包办机器人操纵1.1 传统流程的瓶颈在哪里经典的机器人操作流程基本是模块串联视觉模块先识别目标物体输出像素坐标坐标变换模块把像素坐标转到机械臂基座坐标系轨迹规划模块负责生成一条无碰撞路径底层控制器再去执行这条路径。听起来很顺但每一环都在累积误差。我印象最深的一次是做一个抓取项目。目标检测模型在测试集上准确率挺高但换了一个光源方向框的位置偏了4到5个像素。就这么点误差换算到机器人坐标系里已经超过了夹爪的容差抓取直接落空。后面检查发现手眼标定矩阵也有将近2毫米的偏差。一个模块一个问题调到最后甚至不知道该信谁。这种模块化方案的另一个痛点是任务边界特别脆。你要让机器人“把红色方块放到篮子里”就得单独训练一个红色方块检测器还要定义什么算“放到篮子里”篮子被遮挡怎么办红色方块被其他物体压住怎么办。每个新任务都要新增规则规则之间还会打架。说到底传统流程把大量精力花在了把真实世界“翻译”成预定义状态上。1.2 VLM恰好补上了什么VLM能补上的恰恰是这种“翻译”能力。它不是在识别一个固定类别而是在用海量图文预训练中学到的世界常识来理解画面这是杯子那是篮子杯子在篮子左边红色方块离机械臂更近。这些对人类来说顺理成章的判断传统视觉方案要专门建模。从技术上拆开看VLM一般由一个视觉编码器和一个语言模型组成。视觉编码器把图像切成类似token的patch映射到语言模型能处理的向量空间然后语言模型根据任务指令预测输出。所以它天然具备两个能力一是理解图像里的物体和空间关系二是把理解结果组织成文本或结构化指令。Show-Harness实际用起来也是一样。我只需要把机械臂末端的相机画面传进去再问一句“请把红色方块放到篮子里”它就能输出一步动作。这种交互方式等于把“写死规则”变成了“用自然语言描述目标”对非专业开发者友好得多。1.3 适用边界它不是万能控制器但我得先把丑话说在前面VLM不是万能的更不能替代底层伺服控制。它擅长的是任务级决策识别场景、判断目标、规划大致动作序列。真正执行时关节伺服、动力学控制、力控制依然要靠传统控制器完成。我在实测中碰到的典型场景是VLM能准确说出“现在应该往左移动2厘米”但如果让它生成机械臂每个关节的力矩值那就完全失控了。它没有动力学模型也不会计算惯性补偿。所以Show-Harness的正确打开方式是VLM输出高层动作意图底层控制器负责平滑执行。两个层级各干各的系统才稳。这个边界想清楚之后很多问题就不会纠结了。比如VLM一个动作推理要1到3秒用来做实时力控肯定不行但用来做“观察-决策-执行”的离散步骤时间完全够用。2. Show-Harness的架构拆解从像素到关节指令的完整链路2.1 感知输入怎么设计Show-Harness的输入端不复杂核心是RGB图像加一段任务描述。如果机械臂上有深度相机我也会把关键目标的深度值提取出来换算成文字放进提示词里而不是把深度图直接塞给VLM。原因很简单大多数开源VLM对深度图的先验几乎为零你给它一张深度图它很容易当成普通灰度图理解反而产生误导。相机视角需要固定。我早期测试时把顶置相机换成了45度斜视角VLM给出的目标坐标立刻乱掉。后来我在提示词里明确标注了“frontview”和“topview”两个视角并让模型只基于frontview输出动作效果才稳定。如果你只有一个相机尽量固定安装不要频繁动它。图像分辨率也不是越高越好。模型输入是一串patch分辨率太高会占掉大量上下文窗口留给动作生成的token就少了。我一般把相机画面缩放到640x480重点区域可以再做一次中心裁剪。这样推理速度快也不容易丢失关键信息。2.2 决策层VLM如何输出动作VLM输出动作的方式决定了整套系统的可靠程度。最省事的做法是让它输出自然语言“向左移动两厘米然后放下夹爪。”但自然语言解析起来很痛苦同一个意思它能换一百种说法你总不能每种都写一个正则。Show-Harness的做法是强制输出JSON格式的动作原语。我在提示词里给它定义一个结构模型只会输出这样一个JSON{ action: move, delta_position: [0.0, -0.02, 0.01], gripper: open, speed: 0.2 }这里的action是动作类型可以是move、pick、place、waitdelta_position是相对于当前末端位置的平移量gripper控制夹爪speed限制执行速度。用JSON的好处是解析简单而且利用语言模型的指令遵循能力正确率可以做到95%以上。如果模型输出了非法JSON常见情况是多了尾逗号或者中文字符我不会让程序崩溃而是重新采样一次。大概两三次内都能拿到合法结构。2.3 动作翻译与底层控制器VLM输出JSON只是第一步要让机械臂真正动起来还需要一个动作翻译层。在我的实现里这个翻译层做了三件事坐标变换、轨迹插值、安全校验。坐标变换最关键。如果VLM输出的是绝对坐标它必须知道坐标系原点在哪、每个轴怎么定义但语言模型对绝对数字的感知很弱让它直接回归[0.42, 0.31, 0.15]这种坐标误差很大。所以我改成了输出相对位移让模型判断“向左2厘米、向上1厘米”实际目标位置由程序计算出来。VLM对相对方向的理解远比对绝对坐标的理解靠谱。轨迹插值和安全校验则完全是工程问题。计算得到目标位置后我会用MoveIt或自定义的直线插补生成一段平滑轨迹再把轨迹点交给底层速度控制器。安全校验是为了防止VLM突发奇想输出一个大跳跃动作超出工作空间。可见决策层可以交给模型但执行层必须握在程序员手里。2.4 为什么强调“只需一个智能体”最近很多框架喜欢做多智能体编排一个模型负责规划一个模型负责感知一个模型负责执行中间再用一个调度器串联。听起来很灵活实际跑起来经常崩。每多一个模型就多一层token传输、多一份上下文不一致的风险。Show-Harness选择单智能体的理由很朴素所有信息都放到同一个上下文窗口里感知和决策天然共享同一个视觉表示。模型在生成动作时既能看到图像patch也能看到任务指令和失败反馈不需要额外通信。单智能体也有明显的代价就是单点故障。一旦模型理解错了场景整个任务就错了没有其他模型来纠偏。所以我在后面会讲一个很重要的机制失败反馈闭环。用环境反馈把错误信息再喂给同一个智能体让它自己修正这才是单智能体方案能活下去的关键。3. 从零复现一套可运行的VLM操纵环境3.1 运行环境与模型选型如果你也想上手试我建议先在仿真环境里跑通再考虑真机。我目前在用的组合是robosuite仿真器加一款开源VLM硬件是一张RTX 4090显存24GB。表里这几类模型都可以考虑模型参数量显存占用参考适用场景Qwen2-VL-7B7B约16GB通用视觉语言理解中文支持好文本动作输出LLaVA-1.6-7B7B约14GB轻量方便做LoRA微调OpenVLA7B约24GB以上机器人预训练输出动作token不是传统VLM商业API模型不确定零本地显存快速验证但反馈延迟和成本要留意我的默认选择是Qwen2-VL-7B因为它对中文指令和结构化JSON输出的支持都不错量化之后显存占用可以压到10GB左右一张消费级显卡就能跑。如果你要做精细调优LLaVA系列更容易做LoRA微调。系统依赖大概这样Ubuntu 22.04、Python 3.10、PyTorch 2.x、transformers、robosuite。仿真器不需要太高的显卡要求CPU也能跑但VLM推理必须GPU。3.2 核心代码骨架下面是一个简化版的核心循环去掉了一堆异常处理但整体结构足够看清Show-Harness是怎么运转的import json import torch from transformers import AutoProcessor, Qwen2VLForConditionalGeneration import robosuite as suite from robosuite.controllers import load_controller_config # 1. 加载VLM model_id Qwen/Qwen2-VL-7B-Instruct processor AutoProcessor.from_pretrained(model_id) model Qwen2VLForConditionalGeneration.from_pretrained( model_id, device_mapauto, torch_dtypetorch.float16 ) # 2. 创建仿真环境 config load_controller_config(default_controllerJOINT_VELOCITY) env suite.make( Lift, robotsUR5e, grippersRobotiq85Gripper, has_rendererFalse, has_offscreen_rendererTrue, use_camera_obsTrue, camera_namesfrontview, camera_heights480, camera_widths640, controller_configsconfig, ) def build_prompt(task, image, history): return { system: 你是一个机械臂操作员。 只按给定JSON格式输出下一步动作不要解释。, user: f当前相机画面image\n任务{task}\n f历史反馈{history}\n 可选动作move/pick/place/wait。 输出格式{\action\:\...\, \delta_position\:[dx,dy,dz], \gripper\:\open/close\}, } def parse_action(raw_text): text raw_text.strip().replace(json, ).replace(, ) return json.loads(text) obs env.reset() history task 把红色方块放到篮子里 for step in range(30): image obs[frontview_image] inputs processor.build_inputs( build_prompt(task, image, history), images[image], return_tensorspt ).to(model.device) output model.generate( **inputs, max_new_tokens128, do_sampleTrue, top_p0.9, temperature0.6, ) raw_text processor.batch_decode(output, skip_special_tokensTrue)[0] action parse_action(raw_text) # 3. 把JSON动作翻译成环境可执行的低层指令 env_action action_to_env_action(env, action) obs, reward, done, info env.step(env_action) # 4. 用简单的视觉判断任务是否完成 if check_success(obs, task): print(任务完成) break这里的action_to_env_action是最容易被忽略的部分delta_position是相对于当前夹爪位置的笛卡尔平移量但robosuite的底层控制器接收的往往是关节速度或目标末端位置所以需要先经过逆运动学换算。仿真器里可以直接调控制器接口真机则要经过ROS和MoveIt。3.3 仿真器和真实机器人的接入差异仿真环境最大的好处是状态完全可观测你能直接拿到夹爪是否闭合、物体是否被拿起。但这也掩盖了真实世界的一堆麻烦相机畸变、标定误差、物体摆放偏差、控制延迟。我搬真机之后遇到的第一件事就是仿真里抓得稳稳的方块真机上每次都偏几毫米。原因就是仿真里的相机内参和手眼矩阵都是理想值而真机的一连串标定误差叠在一起直接让VLM输出的相对位移失效。后来我重新做了手眼标定又在每次任务开始前加了一步“对刀尖”校准成功率才回来。建议是先把仿真成功率跑到80%以上再上真机不要一上来就用真机调VLM失败原因会复杂到根本没法归因。4. 任务提示词与交互闭环让模型稳定干活的关键4.1 提示词里必须写清楚的信息VLM不是程序不会自动明白你的坐标系、动作空间和禁止事项。提示词里少写一个约束模型就可能放飞一次。我总结下来提示词至少要包含五部分角色设定、任务目标、坐标系定义、可用动作列表、输出格式。我常用的模板长这样系统提示词 你是一个机械臂操作员控制一台UR5e机械臂。 机械臂当前末端位置定义为原点x轴向前y轴向左z轴向上。 你只能使用动作原语move、pick、place、wait。 move表示移动到相对位置pick表示闭合夹爪尝试抓取 place表示打开夹爪放下物体wait表示等待。 只输出一个JSON对象不要输出任何解释。 用户提示词 当前相机画面image 任务把红色方块放到篮子里。 如果上一次动作失败反馈是夹爪闭合但未夹到物体请调整位置重试。 请给出下一步动作。这套模板看起来啰嗦但每句话都有用。坐标系定义能帮模型理解delta_position的方向动作列表限制了模型的行为空间输出格式约束让解析不再头疼。我遇到的大部分格式错误都是因为少了其中某一段。4.2 失败重试闭环VLM单次预测不可能永远正确但错误不一定是致命的只要你告诉它错了它常常能自己纠正。这就是失败反馈闭环存在的意义。具体做法是每次执行完动作后先判断是否成功。成功可以靠两种方式判断一种是视觉前后对比抓取前后各截一张图看物体有没有离开桌面另一种是读取夹爪状态和末端高度判断夹爪内是否有障碍物。把判断结果拼接成文本作为历史反馈加进下一轮提示词。比如模型第一次输出“向左移动2厘米”执行后夹爪闭合却什么都没夹到。我会把“夹爪未检测到物体物体可能还在原位置建议向上移动0.5厘米后重新靠近”这段反馈放进上下文再让模型生成下一步。实测这套闭环大概能把任务成功率提高三成。模型更像是“看着结果慢慢摸索”而不是一次性蒙一个动作。4.3 采样次数与置信度判断很多教程默认用贪心解码但VLM做动作生成时贪心解码容易陷入重复或死板的输出。我建议使用带采样的解码同一个画面生成多个候选动作再挑一个最合理的执行。Show-Harness里我通常设置n5次采样。如果五次里四次都是同一个动作那这个动作大概率可信如果动作五花八门说明模型对当前场景很困惑这时候我会选择等待一步或者让模型重新表述任务。你甚至可以在JSON里加一个confidence字段让模型自己给动作打个分虽然这个分数并不是真正的概率但它在排序候选项时确实有用。采样也会带来一个副作用偶尔有一两次采样会输出离谱动作。所以无论采样几次动作校验都不能省。最大位移超限、目标位置在工作空间之外、夹爪指令冲突这些都要在解析层拦住。4.4 少量示例的作用如果模型对JSON输出的格式总出错或者频繁选择错误动作最快解决方法是给它一两条few-shot示例。示例不是让它背答案而是让它理解“期望的输出长什么样”“哪些行为是禁止的”。我会在提示词末尾补一段示例1 用户把杯子推到桌子右边。 助手{action: move, delta_position: [0.0, -0.05, 0.0], gripper: open, speed: 0.2}示例不宜多一到两条足够多了反而会占用上下文窗口拖慢推理速度。如果换成新的机械臂或者新任务记得把示例里的坐标系和动作语义一并更新否则模型会模仿错误示例。5. 实测翻车现场与调优经验5.1 相机位置一变就失灵我最早测试时用的是顶置相机效果不错物体识别率高相对位移判断也很准。某天想把相机挪到机械臂侧面结果同一个任务的成功率从八成直接掉到三成。原因不复杂VLM对空间关系的理解本质上是二维像素层面的它看到“方块在图像左侧”并不意味着知道“方块在机器人坐标系的哪个方向”。这个问题我最后用两个办法解决。一是固定相机安装不做随意调整这样模型面对的观察分布不会漂移。二是在提示词里同时提供顶置视角和侧视角两张图让模型交叉验证空间关系。加了一个视角后成功率基本恢复。5.2 动作抖动与安全限位连续执行时VLM输出的相对位移不稳定。比如模型第一次输出向左2厘米第二次可能输出向左1.5厘米第三次又变成向上0.3厘米末端执行器在这种交替指令下会明显抖动。直接执行不仅难看还有撞到工件的风险。我给执行层加了两层保护。第一层是滑动平均用一个长度为5的窗口对每一维位移做平滑第二层是硬限位任何单步位移超过0.05米就截断到0.05米任何目标位置超出机械臂工作空间的直接丢弃。这样牺牲了一点动作灵活度换来的却是稳定性和安全性。尤其注意在仿真里撞一下只是画面难看在真机上可能就是夹爪和工件受损。安全限位不是可选项是必选项。5.3 长指令多步骤任务怎么拆VLM的上下文窗口虽然大但让它一次性输出一整条长任务的每步动作大概率会丢步骤。比如“把红色方块放到篮子里再把篮子推到桌子边缘”它常常只做完第一步就停了。Show-Harness的做法不是硬撑而是把长任务切成多轮短任务。每一轮只让VLM做一个动作但用户提示词里维护一个“已完成步骤”列表。模型看到“已完成物体已经放入篮子当前目标把篮子从当前位置推到桌子边缘”就能继续干活而不会重复。这个“已完成步骤”列表本质上就是最轻量的记忆机制不需要额外的状态存储只靠文本拼接就能实现而且效果显著。5.4 一份实测踩坑清单现象可能原因常规解法JSON解析失败模型输出了尾逗号或中文标点做字符串清理或重采样2至3次模型声称抓取成功但实际没抓到视觉幻觉缺少成功验证增加夹爪状态和图像前后对比检查推理延迟高任务卡顿图像分辨率太大上下文过长缩到640x480减少few-shot示例换场景后成功率骤降过度依赖旧场景先验重新设计提示词补充新场景示例连续动作抖动采样随机性叠加滑动平均加最大步长限制坐标方向反了坐标系定义没写清在提示词里明确x、y、z轴方向和原点这些坑几乎每个做VLM控制的人都会遇到。建议你把自己的失败案例也记录下来攒成一张自己的避坑清单比任何现成教程都更有价值。6. 这类方案能走多远几个值得继续投入的方向6.1 数据回流与微调通用VLM确实能开箱做不少任务但如果你要长期在一个固定场景里运行微调是绕不开的。Show-Harness的执行日志本身就是一个数据金矿每一步的相机图像、任务指令、模型输出、执行结果全部可以被整理成训练样本。我在项目中期收集了大概几百条成功轨迹用LoRA对VLM做了一次轻量微调格式错误率明显下降对特定工具的识别也更稳定。这里要提醒一句微调数据必须包含失败样本否则模型会变成“只报喜不报忧”的嘴硬选手明明没抓到也说自己抓到了。6.2 多模态记忆与长期任务单智能体方案的局限在于没有长时记忆。模型不会记得十分钟前它已经完成了哪些步骤除非你把历史信息写进提示词。这个用文本列表就能解决但任务一旦复杂到几十步文本列表会变得很长挤占图像token的空间。我目前在做的一个改造是把已完成步骤摘要化只保留关键状态比如“物体在夹爪中”“篮子位置已移动”而不是完整对话历史。这就像你做事前会先在便利贴上记一个待办清单一样模型的便利贴就是那段摘要文本。6.3 从单机械臂到移动操作从固定机械臂扩展到移动底盘加机械臂是一个自然的方向。固定机械臂只有一个操作空间VLM只需要关心当前工作台移动操作则要额外处理全局导航和局部避障。如果只靠一个VLM硬扛全局路径规划效果会很差因为导航需要的是稠密里程计和地图信息这不是VLM擅长的。我个人建议保留移动底盘原本的导航方案让VLM专注于机械臂抓取和放置决策。导航到一个目标点后再启用VLM操作两套系统各司其职比硬塞一个模型里更可靠。我在实际运行中最深的一点体会是Show-Harness这类方案把机器人开发的调试重心从“写控制代码”变成了“设计提示词和反馈机制”。你花在观察模型为什么犯错上的时间会远远超过写代码的时间而收获也恰恰在这里每一次失败反馈都在帮同一个智能体变得更好。拿到一个新任务我现在会先跑五十次统计成功率和失败模式再针对最常见的失败场景修改提示词。这套循环走顺之后你手里就真的多了一个能跟着自然语言指挥干活的机器人。