✨ 复制成功!

以文会友,打造好学人设!

📋 已复制到剪贴板:

借力打力:调用开源VLA模型的基础API

创建时间:2026-08-10 更新时间:2026-08-11 阅读次数:1069 次

上一节,我们理解了具身大模型的概念。RT-2把互联网知识装进了机器人的身体,Octo让这一切变成了开源现实。但概念理解得再透彻,也比不上亲手让一个VLA模型跑起来。

这一节,我们要动手。不训练任何模型,不标注任何数据,直接调用开源VLA模型的预训练权重。 你只需要写几十行代码,发送一句“拿起红色的积木”,然后看着仿真环境里的机械臂自主拆解指令、规划动作、完成抓取。

这不是科幻。这是你打开电脑就能跑通的现实。

什么是VLA模型?

在正式动手之前,先锁定一个核心概念。上一节我们提到了VLA——Vision-Language-Action,视觉-语言-动作模型。这三个词精确地定义了它的输入输出边界:

  • 输入:视觉观测(RGB图像) + 语言指令(自然语言文本)
  • 输出:动作指令(关节角度、夹爪开合、末端位移)

VLA模型内部是一个巨大的神经网络,通常基于Transformer架构。它接受当前场景的图像和人类给出的指令,然后直接输出下一步该做什么动作。感知、理解、规划、控制,全部封装在一个模型中。

用数学语言表达,VLA模型学习了一个映射函数 $\pi$:

$$ a_t = \pi(o_t, l) $$

其中 $o_t$ 是时刻 $t$ 的视觉观测(一张RGB图像),$l$ 是语言指令(一个文本字符串),$a_t$ 是模型输出的动作向量。这个动作向量通常包含末端执行器的位移增量 $(\Delta x, \Delta y, \Delta z)$、旋转增量 $(\Delta r_x, \Delta r_y, \Delta r_z)$ 以及夹爪的开合指令 $g \in [0, 1]$。

有些模型会输出多个未来时刻的动作序列 $a_t, a_{t+1}, ..., a_{t+H}$,形成一个短期的动作轨迹。这被称为动作分块,目的是让动作更加平滑连贯。

选择你的开源VLA模型

目前适合初学者快速上手的开源VLA模型主要有两个选择:

Octo(伯克利出品):纯Python接口,HuggingFace上有预训练权重,支持多种机器人平台,文档友好,社区活跃。缺点是推理速度对CPU不太友好,建议有GPU。

OpenVLA(斯坦福出品):同样开源,模型规模更小,推理更快,对消费级GPU更加友好。7B参数的版本在RTX 3060上就能流畅运行。

本节我们以Octo为例进行讲解,因为它的API设计最为简洁。如果你手头的硬件条件有限,可以将Octo替换为OpenVLA,整体代码逻辑几乎不变,只改模型加载的几行代码。

环境准备:安装Octo

首先创建一个新的Python环境(推荐Python 3.10),安装依赖:

pip install torch jax jaxlib flax transformers numpy opencv-python
pip install octo-models

Octo的预训练权重托管在HuggingFace上。首次运行时,它会自动从HuggingFace下载权重文件,大小约为几百MB,请确保网络通畅。

最小的可运行示例:一行指令,一个动作

我们先写一个最简单的测试脚本,确认环境配置正确。这个脚本不涉及仿真,只加载Octo模型,输入一张随机图像和一句指令,让模型输出一个动作向量。

import numpy as np
from octo.model.octo_model import OctoModel

# 加载预训练模型(首次运行会自动下载权重)
print("正在加载Octo模型...")
model = OctoModel.load_pretrained("octo-base")
print("模型加载完成!")

# 构造一个虚拟的观测(用随机噪声模拟一张图像)
# 实际使用时,这里应该是从仿真环境或相机获取的真实图像
dummy_image = np.random.randint(0, 255, (256, 256, 3), dtype=np.uint8)

# 构造一条语言指令
instruction = "pick up the red block"

# 将观测打包成模型所需的格式
observation = {
    "image_primary": dummy_image,
    "timestep_pad_mask": np.ones((1,), dtype=np.float32),
}

# 运行推理
# 将语言指令也作为输入传给模型
task = model.create_tasks(texts=[instruction])
action = model.sample_actions(
    observation,
    task,
    rng=np.random.default_rng(0),
)

print(f"指令: {instruction}")
print(f"输出动作向量形状: {action.shape}")
print(f"输出动作向量: {action[0]}")

如果一切正常,你会看到类似这样的输出:

正在加载Octo模型...
模型加载完成!
指令: pick up the red block
输出动作向量形状: (1, 7)
输出动作向量: [[ 0.0023 -0.0015  0.0089  0.0001 -0.0003  0.0002  0.9845]]

这7个数字代表了一个完整的动作指令。前三个是末端执行器的位移增量 $(\Delta x, \Delta y, \Delta z)$,中间三个是旋转增量,最后一个通常表示夹爪的开合程度(接近1表示闭合,接近0表示张开)。

模型“认为”该往哪个方向移动,该不该闭合夹爪——这一切决策都封装在这7个数字里,完全由神经网络自动生成。

接入仿真:让模型操控真实的虚拟机器人

跑通随机图像的测试只是第一步。真正的重头戏是把Octo接入我们熟悉的仿真环境,让模型根据仿真相机拍到的真实画面,控制虚拟机械臂完成抓取任务。

我们需要一个简单的桥接脚本,完成以下闭环:

  1. 从仿真环境获取当前相机图像
  2. 将图像和语言指令喂给Octo模型
  3. Octo输出动作向量
  4. 将动作向量转化为仿真环境中机械臂的控制指令
  5. 步进仿真,回到第1步

以下是完整的桥接脚本 octo_sim_bridge.py

import numpy as np
import mujoco
import mujoco.viewer
import cv2
from octo.model.octo_model import OctoModel

# ==================== 加载Octo模型 ====================
print("正在加载Octo模型...")
model = OctoModel.load_pretrained("octo-base")
print("模型加载完成!")

# ==================== 加载仿真环境 ====================
# 使用一个预置的桌面操作场景
xml_path = "tabletop_manipulation.xml"
mj_model = mujoco.MjModel.from_xml_path(xml_path)
mj_data = mujoco.MjData(mj_model)

# 获取关键元素的ID
# 假设场景中有一个红色积木叫 "red_block"
block_body_id = mujoco.mj_name2id(
    mj_model, mujoco.mjtObj.mjOBJ_BODY, "red_block"
)
# 机械臂末端执行器的body名称
gripper_body_id = mujoco.mj_name2id(
    mj_model, mujoco.mjtObj.mjOBJ_BODY, "gripper"
)

# 仿真参数
timestep = mj_model.opt.timestep

def get_camera_image():
    """从仿真相机获取RGB图像"""
    renderer = mujoco.Renderer(mj_model, 256, 256)
    renderer.update_scene(mj_data, camera=0)
    image = renderer.render()
    renderer.close()
    return image  # shape: (256, 256, 3)

def apply_action(action_vector):
    """
    将Octo输出的动作向量应用到仿真环境。
    action_vector: 长度为7的数组 [dx, dy, dz, drx, dry, drz, grasp]
    """
    # 前三个是末端位移增量
    dx, dy, dz = action_vector[:3]
    # 最后一个是夹爪指令
    grasp = action_vector[-1]

    # 获取当前末端执行器的位置
    gripper_pos = mj_data.body_xpos[gripper_body_id]

    # 更新末端目标位置(在世界坐标系中)
    new_target_pos = gripper_pos + np.array([dx, dy, dz])

    # 通过逆运动学求解关节角度(简化处理:直接使用位置控制)
    # 实际工程中这里需要完整的IK求解器
    # 我们使用MuJoCo内置的IK功能
    jacobian = np.zeros((3, mj_model.nv))
    mujoco.mj_jacBody(mj_model, mj_data, jacobian, None, gripper_body_id)

    # 简化的速度控制:雅可比伪逆
    jacobian_pinv = np.linalg.pinv(jacobian[:3, :])
    joint_velocities = jacobian_pinv @ np.array([dx, dy, dz]) / timestep

    mj_data.qvel[:] = joint_velocities

    # 夹爪控制:grasp > 0.5 表示闭合
    for i in range(mj_model.nu):
        if "gripper" in mj_model.actuator(i).name.lower():
            mj_data.ctrl[i] = 255 if grasp > 0.5 else 0

# ==================== 主控制循环 ====================
instruction = "pick up the red block"
task = model.create_tasks(texts=[instruction])

print(f"指令: {instruction}")
print("开始视觉-语言-动作闭环控制...")

step_count = 0
max_steps = 500

with mujoco.viewer.launch_passive(mj_model, mj_data) as viewer:
    while viewer.is_running() and step_count < max_steps:
        # 第1步:获取当前相机图像
        image = get_camera_image()

        # 第2步:准备模型输入
        observation = {
            "image_primary": image,
            "timestep_pad_mask": np.ones((1,), dtype=np.float32),
        }

        # 第3步:调用Octo推理
        action = model.sample_actions(
            observation,
            task,
            rng=np.random.default_rng(step_count),
        )
        action_vector = action[0]  # 取出batch中的第一个样本

        # 第4步:应用动作到仿真环境
        apply_action(action_vector)

        # 第5步:步进仿真
        mujoco.mj_step(mj_model, mj_data)
        viewer.sync()

        # 打印状态
        if step_count % 50 == 0:
            block_pos = mj_data.body_xpos[block_body_id]
            gripper_pos = mj_data.body_xpos[gripper_body_id]
            distance = np.linalg.norm(block_pos - gripper_pos)
            print(f"步数 {step_count} | "
                  f"末端位置: ({gripper_pos[0]:.2f}, {gripper_pos[1]:.2f}, {gripper_pos[2]:.2f}) | "
                  f"距目标: {distance:.3f}m | "
                  f"夹爪: {'闭合' if action_vector[-1] > 0.5 else '张开'}")

        step_count += 1

print(f"控制循环结束,共执行 {step_count} 步")

运行这个脚本,你会看到仿真窗口弹出,机械臂开始自主运动。它可能一开始有些犹豫,动作幅度很小,然后逐渐靠近红色积木,最终夹爪闭合——在没有一行手工规则的情况下,机器人靠一个神经网络完成了“理解指令-规划路径-执行抓取”的全过程。

模型推理的实时可视化

为了更直观地理解VLA模型的行为,我们可以添加一个简单的可视化窗口,将当前相机图像、模型输出的动作向量、以及执行状态实时显示出来:

# 在主循环中添加以下可视化代码
def visualize_inference(image, action_vector, step_count, status_text):
    """可视化推理过程"""
    # 在图像上标注动作方向
    h, w = image.shape[:2]
    dx, dy = action_vector[0] * 50, action_vector[1] * 50  # 放大以便可视化
    center = (w // 2, h // 2)
    end_point = (int(center[0] + dx), int(center[1] - dy))

    viz_image = image.copy()
    cv2.arrowedLine(viz_image, center, end_point, (0, 255, 0), 2, tipLength=0.3)

    # 标注夹爪状态
    grasp_status = "GRASP" if action_vector[-1] > 0.5 else "OPEN"
    cv2.putText(viz_image, f"Action: {grasp_status}", (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
    cv2.putText(viz_image, f"Step: {step_count}", (10, 60),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
    cv2.putText(viz_image, status_text, (10, 90),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 1)

    cv2.imshow("VLA Inference", cv2.cvtColor(viz_image, cv2.COLOR_RGB2BGR))
    cv2.waitKey(1)

图像上的绿色箭头会告诉你模型“认为”下一步该往哪个方向移动,角落的文字告诉你夹爪是开还是合。这些视觉线索能帮你建立对VLA模型行为的直觉——它什么时候在探索,什么时候在接近目标,什么时候决策已定准备抓取。

换一个指令试试看

一个好的VLA模型应该能理解不同的自然语言指令。尝试修改 instruction 变量的值,看看模型的行为会如何变化:

instruction = "move the blue cube to the left"
instruction = "push the green button"
instruction = "open the drawer"

你可能会发现,对于某些指令模型表现良好,对于另一些则犹豫不决甚至乱动。这是因为Octo的训练数据并非覆盖所有可能的任务。它在常见操作(抓取、移动、放置)上泛化较好,在罕见操作上可能出现不确定的输出。这种“灵与不灵”的边界,正是当前具身大模型研究的核心挑战之一。

小结:你刚刚做了什么?

停下手指,回顾这一节。

你没有训练任何模型。你没有标注任何数据。你只是加载了开源社区训练好的模型权重,编写了一个闭环控制脚本,然后看着仿真环境里的机械臂根据你输入的自然语言指令自主完成了动作。

这不是玩具。这就是2024年具身智能研究的标准工作流。全球各地的实验室每天都在做类似的事情:加载预训练模型,接入仿真或真实机器人,用少量数据微调,验证泛化能力。你今天跑通的这套流程,和顶会论文里的实验范式没有本质区别。

更重要的是,你亲自体验了VLA模型的运作方式。图像进,文本进,动作出。感知、理解、规划、控制——封装在一个模型调用里。它不完美,有时会犹豫,有时会出错,但它代表了一种全新的范式:机器人不需要为每一个任务手写规则,它可以像人类一样,看着场景,听指令,然后直接行动。

在下一节,我们将在这个基础上更进一步——不只是调用预训练模型,而是用你自己的数据去微调它,让一个通用底座模型学会你的专属技能。你将扮演机器人老师的角色,用几十条示教数据,教会它一个全新的操作任务。

本教程共21节,当前为第17节!
本教程最新修订时间为:2026-08-19 23:29:19

📌 面试天下网:一款服务于大一新生的口袋书,让大家在无聊的公共课上可以学习大模型技术!
📌 网站公告:【悬赏 200 元/篇】寻找“大模型面试战场”的一手回忆录
📌 网站公告:【大模型/Agent面试陪练1V1指导】正式启动......

评论专区

评论加载中...