029、VLM在机器人视觉问答中的应用:从场景理解到任务决策
昨天半夜在实验室调一个抓取demo,机械臂死活认不出桌面上的红色马克杯——不是识别不到,是它把杯子和旁边的红色胶带卷搞混了。我盯着屏幕上的CLIP相似度分数看了十分钟,突然意识到问题不在视觉编码器,而在prompt设计。这个场景太典型了,值得单独写一篇。
从"看到什么"到"该做什么"的鸿沟
传统视觉系统输出的是检测框、分割掩码、类别标签,这些是"名词"。但机器人执行任务需要的是"动词+名词+约束条件"——比如"用两指夹爪从侧面捏住杯柄,施加2N力,抬升15cm"。VLM(视觉语言模型)的介入让这个跨越成为可能,但前提是你得知道怎么跟它对话。
我见过太多人直接把VLM当黑盒用,输入一张图加一句"what should I do?",然后期待输出一个可执行的轨迹。现实是模型会给你一段充满幻觉的散文。关键区别在于:VLM不是规划器,它是感知到决策之间的翻译器。你要做的是设计好翻译的中间格式。
场景理解:别让模型猜你问什么
先解决最基础的问题——让VLM准确描述场景。这里有个反直觉的坑:你给的指令越"自然",模型越容易出错。比如"描述这个场景"这种开放性问题,VLM会给你一段包含背景噪音的冗长描述,而机器人真正需要的可能只是"桌面上有三个物体:红色马克杯(中心坐标320,240),银色金属罐,蓝色海绵"。
我的做法是强制结构化输出。用JSON schema约束回答格式,每个物体包含类别、位置、颜色、姿态估计、可抓取性判断。这里踩过坑:一开始我让模型直接输出坐标,结果它经常把像素坐标和归一化坐标搞混。后来我改成让它输出"物体在图像中的相对位置(左/中/右,上/中/下)",再用传统视觉方法精确定位。混合方案比纯VLM方案稳定得多。
另一个关键点是上下文注入。单张图对VLM来说信息量太大,它会平均分配注意力。我习惯先做一次粗检测(用YOLO或者Grounding DINO),把候选区域裁剪出来,再让VLM针对每个区域做细粒度分析。这样既保留了VLM的语义理解能力,又避免了它在杂乱背景中迷失。别这样写:直接整图输入然后期望模型自己找到重点,除非你的场景极其干净。
任务决策:把VLM输出变成机器人动作
当VLM理解了场景,下一步是让它参与决策。这里我推荐分层设计:高层用VLM做任务分解和物体选择,低层用传统规划器或RL策略执行具体动作。
举个例子,任务指令是"把红色杯子放到蓝色托盘里"。VLM需要回答三个问题:哪个是红色杯子?哪个是蓝色托盘?当前状态离目标状态差几步?第一个问题通过视觉 grounding 解决,第二个类似,第三个需要模型理解空间关系和操作顺序。
我在实际代码里是这样做的:先让VLM输出一个结构化动作序列,比如[{"action": "pick", "target": "red_mug", "grasp_point": "handle", "approach_vector": [0, -1, 0]}, {"action": "place", "target": "blue_tray", "position": "center"}]。然后每个动作映射到具体的运动规划器。这里有个重要经验:不要指望VLM直接输出关节角度或末端坐标,它没有那个精度。VLM负责语义决策,数值计算交给确定性算法。
代码实现:一个最小可用的VQA机器人模块
下面是我在项目里实际用的一个简化版本,去掉了一些工程细节,保留核心逻辑。
importjsonimporttorchfromtransformersimportAutoProcessor,AutoModelForVisionText2TextfromPILimportImage# 这里踩过坑:不同版本的transformers对VLM的支持差异很大# 我用的是llava-1.5-7b,如果你用更新的模型,注意调整prompt格式model_id="llava-hf/llava-1.5-7b-hf"processor=AutoProcessor.from_pretrained(model_id)model=AutoModelForVisionText2Text.from_pretrained(model_id,torch_dtype=torch.float16,device_map="auto")# 关键:定义输出格式,用few-shot示例引导模型SYSTEM_PROMPT="""You are a robot perception assistant. Given an image and a task instruction, output a JSON object with: - "objects": list of detected objects, each with "name", "bbox" (relative coordinates 0-1), "graspable" (bool) - "plan": list of action steps, each with "action" (pick/place/push), "target", "constraints" Example: Input: "Move the apple to the green bowl" Output: {"objects": [{"name": "apple", "bbox": [0.2, 0.3, 0.4, 0.5], "graspable": true}, {"name": "green_bowl", "bbox": [0.6, 0.7, 0.8, 0.9], "graspable": false}], "plan": [{"action": "pick", "target": "apple", "constraints": "top-down grasp"}, {"action": "place", "target": "green_bowl", "constraints": "center"}]} """defvlm_scene_understanding(image_path,task_instruction):image=Image.open(image_path)# 构建对话格式,注意llava需要特殊的chat template# 别这样写:直接拼接字符串,llava对格式敏感,必须用processor的chat templatemessages=[{"role":"system","content":SYSTEM_PROMPT},{"role":"user","content":f"Task:{task_instruction}. Analyze the scene and output JSON."}]prompt=processor.apply_chat_template(messages,add_generation_prompt=True)inputs=processor(text=prompt,images=image,return_tensors="pt").to(model.device,torch.float16)# 生成参数调优经验:temperature别太高,0.1-0.3之间# max_new_tokens根据输出复杂度调整,我一般给512output=model.generate(**inputs,max_new_tokens=512,temperature=0.2,do_sample=False,# 这里用贪心解码,减少随机性pad_token_id=processor.tokenizer.pad_token_id)response=processor.decode(output[0],skip_special_tokens=True)# 提取JSON部分,模型有时会输出额外文本try:# 找到第一个{和最后一个}之间的内容start=response.find('{')end=response.rfind('}')+1json_str=response[start:end]result=json.loads(json_str)returnresultexceptjson.JSONDecodeError:# 这里踩过坑:模型偶尔会输出不合法JSON,需要重试或降级处理print(f"JSON parse failed. Raw response:{response}")returnNone# 使用示例if__name__=="__main__":result=vlm_scene_understanding("table_scene.jpg","Pick up the red mug and place it on the saucer")ifresult:print("Detected objects:",result["objects"])print("Action plan:",result["plan"])这段代码在真实机器人上跑的时候,我发现几个问题值得注意。第一,VLM的推理速度很慢,7B模型在A100上也要几百毫秒,在Jetson上可能要几秒。所以不能每帧都调用,我通常只在任务开始时调用一次,或者当场景发生显著变化时(用帧差检测触发)。第二,输出中的bbox是相对坐标,需要乘以图像尺寸得到像素坐标,再经过相机标定转到机器人坐标系。
实验与调优:那些让你怀疑人生的时刻
我在一个桌面抓取任务上对比了不同方案。纯视觉方法(YOLO+规则)在固定场景下准确率95%,但换一个背景就掉到60%。VLM方案在多样场景下能保持85%左右,但偶尔会犯低级错误——比如把阴影当成物体。
调优过程中最有效的三个手段:一是few-shot示例的质量,我手工标注了20个典型场景,覆盖不同光照、遮挡、物体组合,模型表现提升明显;二是输出格式约束,用正则表达式强制JSON结构,减少解析失败;三是置信度阈值,当VLM输出"graspable: false"时,不要硬抓,触发重试或人工介入。
还有一个容易被忽视的点:VLM对语言指令的措辞很敏感。我测试过"pick up the mug"和"grasp the mug",前者更容易触发正确的抓取姿态。这可能是因为训练数据中"pick up"更常与完整操作关联。所以我在系统里加了一个指令规范化模块,把用户输入映射到一组预定义的动作模板上。
落地经验:别把VLM当万能钥匙
最后说点实在的。VLM在机器人视觉问答中的定位应该是"语义增强器"而不是"唯一感知源"。我的建议是构建一个混合感知管线:传统视觉方法负责快速、精确的几何信息提取,VLM负责处理模糊语义、开放词汇、复杂关系推理。两者通过一个简单的仲裁机制结合——当传统方法置信度高时直接用,低时交给VLM兜底。
另外,模型选择上别盲目追新。7B级别的VLM在边缘设备上勉强可用,13B以上基本只能云端推理。如果你的机器人需要实时响应,考虑蒸馏一个小模型专门做场景理解,或者用API调用云端VLM但加上本地缓存。
调试VLM时最痛苦的是不确定性——同样的输入,两次推理可能给出不同结果。我的经验是固定随机种子、关闭采样、增加温度惩罚,把输出确定性提到最高。如果业务允许,还可以做多数投票,跑三次取一致结果。
这篇先写到这里。下一期我打算聊聊怎么用VLM做失败检测——就是机器人执行完动作后,让VLM判断"任务是否真的完成了"。这个场景比任务规划更实用,也更容易踩坑。有问题欢迎评论区交流,我尽量回复。