ELLMER 论文精读:GPT-4、RAG、视觉与力反馈如何驱动机器人完成复杂任务

论文:Embodied large language models enable robots to complete complex tasks in unpredictable environments
期刊:Nature Machine Intelligence, 2025, 7: 592–601
项目代码:https://github.com/ruaridhmon/ELLMER
DOI:10.1038/s42256-025-01005-x

文章目录

一、论文解决了什么问题?

1.1 从一句话到一连串真实动作

论文给机器人的指令并不是“向右移动 10 cm,再把夹爪关闭”,而是一句非常抽象的话:

我有点累,朋友很快要来吃蛋糕。请帮我准备一杯热饮,并在盘子上画一个随机动物。

对人来说,这句话很容易理解;对机器人来说,却至少包含以下问题:

  • “热饮”具体应该做什么?
  • 杯子、咖啡、勺子和水壶分别在哪里?
  • 如果杯子不在桌面上,是否要打开抽屉?
  • 应该先舀咖啡还是先倒水?
  • 杯子被人移动以后,机械臂是否还沿原轨迹运动?
  • 水壶倾斜到什么程度、倒出多少水以后应该停止?
  • 如何把生成的动物图案转换为机械臂轨迹?

这种由多个相互依赖步骤组成的任务称为 long-horizon task(长时序任务)。后面的动作依赖前面的结果,例如机器人只有找到并放好杯子,才能继续倒咖啡和倒水。

1.2 ELLMER 的技术定位

ELLMER 不是一个从图像和语言直接输出关节控制量的端到端模型,也不是典型的 Vision-Language-Action model(视觉—语言—动作模型,VLA)

它更准确的定位是一个 modular robotic framework(模块化机器人框架)

GPT-4:理解目标、拆分任务、组合技能
RAG:从知识库中检索可用技能和代码示例
运动原语:实现抓取、倒水、开抽屉等具体动作
ROS:连接机械臂、夹爪、相机和力传感器
视觉与力觉:检测实际执行状态并进行反馈修正

因此,这篇论文最重要的贡献不是提出一种全新的底层控制算法,而是把语言推理、知识检索、机器人技能和多传感器反馈连接成一条完整链路。

形象理解:把 ELLMER 想成一家机器人餐厅。
GPT-4 像店长,负责听懂顾客想吃什么并安排工作;RAG 知识库像菜谱和员工手册,告诉店长有哪些菜可以做、步骤是什么;运动原语像已经训练好的厨师,每个人会完成抓杯子、舀咖啡或倒水等具体动作;ROS 像餐厅里的对讲系统,负责把指令传给不同员工;相机和力传感器则像厨师的眼睛和手感,帮助机器人发现杯子被移动、工具碰到桌面或水已经倒够了。


二、论文图 1:ELLMER 总体框架详细解析

在这里插入图片描述

论文图 1 是理解 ELLMER 的核心。图中的蓝色虚线把系统分为两个层次:

  • 虚线上方:high-level reasoning(高层推理)
  • 虚线下方:low-level sensorimotor control(低层感知运动控制)

整套系统可以概括为:

用户指令
→ GPT-4 理解并拆分任务
→ RAG 检索相关机器人技能
→ GPT-4 生成任务级 Python 代码
→ 控制器执行代码并调用运动原语
→ 机械臂与真实环境交互
→ 视觉、力觉和机器人状态反馈
→ 控制器持续修正速度与轨迹

2.1 Query:用户输入的自然语言任务

图最上方的 Query 表示用户输入。

用户通常只描述最终目标,并不会告诉机器人具体控制方法。例如“制作一杯咖啡”没有说明:

  • 杯子在哪里;
  • 抽屉怎样打开;
  • 抓取姿态是什么;
  • 应调用哪个函数;
  • 倒水多少克后停止。

因此,Query 首先被送入 Transformer,由 GPT-4 将自然语言目标转换成机器人可以处理的任务结构。

2.2 Transformer / LLM:理解和拆分任务

图中央的 Transformer 代表 GPT-4,内部的 LLMlarge language model(大语言模型)

它主要承担三项工作:

  1. 理解用户的真实意图;
  2. 把复杂任务拆解为多个子任务;
  3. 根据知识库中的函数和示例生成任务级代码。

论文用一组符号表示任务分解:

L T = { L 1 , L 2 , … , L N } L_T=\{L_1,L_2,\ldots,L_N\} LT={L1,L2,,LN}

其中:

  • L T L_T LT 表示完整任务;
  • L 1 L_1 L1 L N L_N LN 表示不同子任务。

制作咖啡可以被拆为:

寻找杯子
→ 必要时打开抽屉
→ 抓取并放置杯子
→ 获取勺子
→ 舀取咖啡
→ 将咖啡倒入杯中
→ 获取水壶
→ 向杯中倒水

子任务之间存在 dependency(依赖关系)。如果没有找到杯子,系统就不能直接进入倒水阶段,而应先搜索其他位置或打开抽屉。

可以用条件关系表示这种任务分支:

P ( L 2 A , L 2 B ∣ L 1 ) P(L_{2A},L_{2B}\mid L_1) P(L2A,L2BL1)

它表示完成子任务 L 1 L_1 L1后,系统根据当前执行结果选择进入 L 2 A L_{2A} L2A L 2 B L_{2B} L2B

初学者可以把 GPT-4 理解为“任务经理”:它负责安排做事顺序,但并不亲自计算每个电机怎样转动。

再举一个生活中的例子:
当你对导航软件说“带我去最近的咖啡店”时,软件会把目标拆成“选择咖啡店、规划道路、在路口转弯”等步骤,但真正让汽车转向的是方向盘和底盘控制器。ELLMER 中的 GPT-4 类似导航软件,机械臂控制器则类似汽车底盘。

2.3 Database:机器人技能知识库

图右侧的 Databasecurated knowledge base(精选知识库)

这里存储的不是普通百科知识,而是机器人真正能用到的内容:

  • 已实现的函数名称;
  • 函数参数;
  • 动作调用示例;
  • 多个动作如何组合;
  • 哪些步骤需要视觉;
  • 哪些步骤需要力反馈;
  • 已知的安全条件。

例如,知识库会告诉 GPT-4 系统中存在类似下面的技能:

get_spoon(self)
scoop(self)
empty_in_cup(self, "white mug")
get_kettle(self)
track_and_pour(self, "white mug", amount_to_pour=100)
drawing(self, "bird")

GPT-4 与知识库之间的箭头表示 retrieval-augmented generation(检索增强生成,RAG)

当前任务
→ 检索最相关的函数和示例
→ 把检索结果放进 Prompt
→ GPT-4 基于真实接口生成代码

RAG 主要降低 hallucination(幻觉)。没有知识库时,模型可能生成一个看起来合理、但项目中并不存在的函数;RAG 会尽量让模型使用真实可调用的 API。

形象理解:RAG 相当于“开卷考试”。
如果只让 GPT-4 凭记忆写机器人代码,它可能把函数名记错;加入 RAG 后,它会先翻阅项目提供的“函数说明书”,再根据说明书作答。RAG 并不会让机器人凭空获得新技能,它只是帮助模型更准确地找到并组合已经存在的技能。

2.4 Code:GPT-4 生成什么代码?

图左上角的 Code 表示 GPT-4 生成的 Python 程序。

它通常只负责编排技能顺序,例如:

get_spoon(self)
go_to_coffee(self)
scoop(self)
empty_in_cup(self, "white mug")
return_spoon(self)

get_kettle(self)
track_and_pour(
    self,
    "white mug",
    amount_to_pour=100
)
return_kettle(self)

这段代码没有直接输出机械臂七个关节的角度,也没有以几十赫兹的频率计算电机命令。它属于 task-level program(任务级程序)

底层的 joint-level control(关节级控制)、速度控制和反馈判断已经封装在运动原语中。

形象理解:
GPT-4 写出的代码更像一张“待办清单”,例如“拿勺子、舀咖啡、倒进杯子”;运动原语才是每一项工作的详细操作手册,例如机械臂应该向哪个方向移动、什么时候闭合夹爪、检测到多大接触力后停止。

2.5 控制器 A A A:把代码变成真实动作

图中的圆形模块 A A A表示动作执行和机器人控制模块。公开源码中,这一层主要对应:

  • robot_brain.py
  • action_execution.py
  • utilities.py
  • 机械臂与夹爪驱动
  • ROS 视觉和力觉节点

例如 GPT-4 调用:

track_and_pour(
    self,
    "white mug",
    amount_to_pour=100
)

这个简单函数在底层需要展开为:

  1. 通知视觉节点检测白色杯子;
  2. 等待视觉节点返回杯子的三维坐标;
  3. 控制水壶移动到杯口上方;
  4. 如果杯子被移动,重新计算运动方向;
  5. 倾斜水壶;
  6. 根据力传感器估算已经倒出的质量;
  7. 达到目标质量后停止;
  8. 将水壶恢复到竖直姿态。

所以,控制器 A A A 是大语言模型与实体机器人之间的桥梁。

2.6 控制信号与闭环动作

从控制器向下的符号 λ 可以理解为控制参数或控制命令。论文用下面的概念式表示机器人动作:

a = f ( F , V ) a=f(F,V) a=f(F,V)

其中:

  • a a a:机器人实际执行的动作;
  • F F F:force feedback(力反馈);
  • V V V:visual feedback(视觉反馈)。

这并不是一个具体的神经网络公式,而是在强调:机器人动作不是只由预先生成的代码决定,还会根据传感器结果实时改变。

这种控制方式叫 closed-loop control(闭环控制)

执行动作
→ 读取传感器
→ 计算误差
→ 修正下一步动作

与之相对的是 open-loop control(开环控制):机器人只执行预设轨迹,不关心物体是否被移动、是否已经发生接触。

形象理解:闭环控制就像倒车入库。
开环控制相当于闭着眼睛按照记忆打方向盘;闭环控制则是一边观察后视镜,一边不断调整方向。ELLMER 也是一边运动,一边读取视觉和力觉,再决定下一步怎样修正。

2.7 三类反馈状态

2.7.1 视觉状态

论文把视觉状态写为:

S visual = [ p 0 q 0 ] + W visual S_{\text{visual}} = \begin{bmatrix} p^0 \\ q^0 \end{bmatrix} + W_{\text{visual}} Svisual=[p0q0]+Wvisual

其中:

  • p 0 p^0 p0:视觉估计的位置;
  • q 0 q^0 q0:视觉估计的姿态;
  • W v i s u a l W_{visual} Wvisual:视觉噪声。

视觉结果不是绝对准确的,误差可能来自:

  • 目标被遮挡;
  • 深度相机噪声;
  • 分割边界错误;
  • 相机标定误差;
  • 机械臂末端挡住相机;
  • 视觉计算延迟。
2.7.2 机器人自身状态

机器人状态可表示为:

S feedback = [ χ 1 χ 2 ] + W angle S_{\text{feedback}} = \begin{bmatrix} \chi_1 \\ \chi_2 \end{bmatrix} + W_{\text{angle}} Sfeedback=[χ1χ2]+Wangle

图中的 χ 1 χ_1 χ1 χ 2 χ_2 χ2 只是简化符号,用于表示关节角、末端位置和机械臂姿态等本体状态。Kinova Gen3 实际上是七自由度机械臂,并不是只有两个关节。

2.7.3 力与力矩状态

力反馈表示为:

S force = [ f τ ] + W force S_{\text{force}} = \begin{bmatrix} f \\ \tau \end{bmatrix} + W_{\text{force}} Sforce=[fτ]+Wforce

其中:

  • f f f:三轴力;
  • τ τ τ:三轴力矩;
  • W f o r c e W_{force} Wforce:力传感器噪声。

力反馈可以帮助判断:

  • 杯子是否接触桌面;
  • 抽屉是否被卡住;
  • 水壶已经倒出多少水;
  • 用户是否接住递出的物体;
  • 笔尖是否接触盘子。

2.8 当前速度、修正速度和轨迹

图中红色点附近的速度写为:

v = ( v 1 , v 2 , v 3 ) v=(v_1,v_2,v_3) v=(v1,v2,v3)

它表示当前末端的速度命令。绿色点 v ′ v' v 表示根据最新反馈修正后的命令或目标。

例如用户把杯子向右移动:

视觉模块得到杯子的新坐标
→ 控制器重新计算位置误差
→ 原速度命令 v 被更新
→ 机械臂沿新的轨迹追踪杯子

右下角的 Trajectories 表示机器人运动轨迹会随着扰动和反馈不断更新,而不是从开始到结束只沿同一条固定路线运动。

图 1 的核心不是公式本身,而是一个分层闭环:GPT-4 负责“想和安排”,运动原语负责“执行”,视觉与力觉负责“发现偏差并纠正”。


三、关键技术与源码原理

3.1 GPT-4、RAG 与代码传输

3.1.1 RAG 到底在检索什么?

在普通问答系统中,RAG 经常检索文档知识;在 ELLMER 中,它检索的是机器人技能。

一个标准向量检索过程可以描述为:

  1. 把用户查询编码成 embedding(嵌入向量)
  2. 把知识库片段也编码成向量;
  3. 计算查询与各片段之间的相似度;
  4. 选出最相关的 Top-k 片段;
  5. 把这些片段加入 Prompt;
  6. 让 GPT-4 生成代码。

余弦相似度常写为:

sim ⁡ ( q , s i ) = q ⊤ s i ∥ q ∥ ∥ s i ∥ \operatorname{sim}(q, s_i) = \frac{q^\top s_i}{\|q\| \|s_i\|} sim(q,si)=q∥∥siqsi

其中:

  • q q q:用户查询的向量;
  • s i s_i si:第 i 个知识片段的向量;
  • 相似度越大,说明片段与当前任务越相关。

需要注意,论文框架支持多种检索方法;实际演示采用了自定义 GPT 的 Knowledge 功能管理 Markdown 知识库,项目仓库同时提供了 Haystack 示例。因此,ELLMER 的重点是“可检索技能库”这一设计,而不是绑定某一种向量数据库。

3.1.2 RAG 的作用与不能证明的事情

RAG 能提高代码与技能库的一致性,例如:

  • 减少不存在的函数;
  • 减少错误的参数格式;
  • 提供合理的动作顺序;
  • 方便持续加入新技能。

论文使用 faithfulness(忠实度) 评估生成内容是否忠于知识库。

但是要注意:

faithfulness 提高,不等于机器人任务成功率以同样幅度提高。

它只能说明生成计划更符合参考知识,不能直接证明抓取成功率、倒水精度或整套任务成功率已经提高。

3.1.3 Flask 动作队列

GPT-4 生成代码后,通过 Flask 服务传给机器人。其逻辑可以简化为:

actions_queue = queue.Queue()

@app.route("/send_action", methods=["POST"])
def receive_action():
    action = request.json
    actions_queue.put(action)
    return jsonify({"status": "success"})

@app.route("/get_action", methods=["GET"])
def give_action():
    action = actions_queue.get(timeout=30)
    return jsonify(action)

这里的 Flask 不负责机械臂控制,只承担通信:

GPT 发送代码
→ Queue 临时保存
→ robot_brain.py 定期请求
→ 获取待执行动作
3.1.4 受限命名空间与安全边界

action_execution.py 会构造受限命名空间,只暴露允许调用的技能:

restricted_globals = {
    "self": self,
    "get_spoon": get_spoon,
    "scoop": scoop,
    "pour": pour,
    "drawing": drawing,
}

exec(code, restricted_globals)

这种方式属于 allowlist(白名单):模型只有在白名单中的函数可以直接使用。

但它不应被理解为真正严格的 security sandbox(安全沙箱)。动态执行 exec() 仍存在风险,安全性还取决于:

  • 是否移除了危险的内置函数;
  • 暴露对象能否间接访问文件或网络;
  • 超时后能否真正停止执行线程;
  • 异常时机械臂能否立即停止;
  • 是否使用独立进程或容器隔离。

因此,受限命名空间是一层工程保护,但不是生产级安全隔离的完整替代。


3.2 视觉模块:从文字类别到三维坐标

在这里插入图片描述

视觉处理链路为:

当前任务需要寻找的物体名称
→ Grounding DINO 生成目标框
→ NMS 去除重复框
→ MobileSAM 生成像素级掩膜
→ 深度图恢复三维点云
→ 坐标转换到机器人基座
→ 发布目标三维位置

把整条视觉链路想成“找人并告诉司机具体位置”。
Grounding DINO 先在人群照片中框出“穿白衣服的人”;MobileSAM 像剪刀一样把这个人从背景中精确剪出来;深度相机再测量这个人离相机有多远;最后通过坐标标定,把“他在相机右前方两米”翻译成“他在机器人基座坐标系中的哪个位置”。

从图中的检测结果可以看到,每个目标框上方都包含“物体类别 + 数字”,例如:

  • White mug 0.70
  • Black kettle 0.79
  • Hand 0.53

这里的数字是目标检测模型给出的 confidence score(置信度分数),用于表示图像区域与文本类别之间的匹配程度。

可以把置信度表示为:

s ∈ [ 0 , 1 ] s∈[0,1] s[0,1]

数值越大,表示 Grounding DINO 越认为该区域符合输入的文本类别。例如:

  • White mug 0.70:模型认为紫色框中的区域与“白色杯子”这一类别具有较高的匹配度;
  • Black kettle 0.79:模型对黑色水壶的检测结果更有把握;
  • Hand 0.53:模型识别出了人的手,但匹配程度低于前两个物体。

不过,0.70 不能简单理解为“这个物体有 70% 的概率一定是杯子”。它更准确地表示模型内部计算出的图像区域与文本提示之间的相似或匹配分数。这种分数通常没有经过严格的概率校准,因此主要用于:

  1. 判断是否保留检测框;
  2. 比较多个候选检测结果;
  3. 选择同一类别中置信度最高的目标。

系统会设置一个 confidence threshold(置信度阈值)。只有分数超过阈值的检测结果才会继续进入后面的分割与三维定位过程:

图像区域
→ Grounding DINO 计算置信度
→ 低于阈值:删除检测框
→ 高于阈值:交给 MobileSAM 分割

图中不同颜色的矩形是 bounding box(边界框),表示模型判断目标大致位于哪个图像区域;覆盖在物体上的半透明颜色是 segmentation mask(分割掩膜),表示模型进一步判断哪些像素真正属于该物体。

因此,这张图同时展示了两步结果:

Grounding DINO:找到物体大致在哪里
MobileSAM:从背景中精确分离物体像素
3.2.1 第一步:动态设置检测类别

机器人不会始终检测环境中的所有物体,而是根据当前任务更新类别。

例如当前需要找杯子:

classes = ["white mug"]

之后需要找水壶:

classes = ["black kettle"]

这样做可以让视觉模块聚焦于当前子任务。

3.2.2 第二步:Grounding DINO 检测目标框

Grounding DINO 支持 open-vocabulary detection(开放词汇检测)

传统检测器通常只能识别训练时固定的类别;开放词汇检测器可以根据自然语言类别寻找目标,例如:

  • white mug
  • black kettle
  • drawer handle

模型输出的是 bounding box(边界框) 和置信度。

如果多个检测框高度重叠,系统会用 non-maximum suppression(非极大值抑制,NMS) 去掉重复框,只保留最可信的检测。

3.2.3 第三步:MobileSAM 进行实例分割

检测框只能告诉系统“目标大致在这个矩形中”,矩形内仍包含桌面和其他背景。

因此,系统把检测框交给 MobileSAM,生成 segmentation mask(分割掩膜),也就是目标物体对应的像素区域。

分割的作用是:

边界框:包含目标 + 一部分背景
分割掩膜:尽量只保留目标像素

这对后面的三维定位非常重要,否则背景深度会污染目标位置估计。

一个简单例子:
假设杯子的检测框中同时包含杯子和后面的墙。如果直接对整个矩形区域求深度平均,得到的位置可能落在杯子与墙之间。分割掩膜相当于先把杯子从图片上“抠出来”,再只计算杯子像素的深度。

3.2.4 第四步:深度图生成点云

Azure Kinect 同时提供彩色图和深度图。每个深度像素都可以根据相机内参转换为三维坐标。

像素点到相机坐标系的转换可写为:

X = ( u − c x ) Z f x X=\frac{(u-c_x)Z}{f_x} X=fx(ucx)Z

Y = ( v − c y ) Z f y Y=\frac{(v-c_y)Z}{f_y} Y=fy(vcy)Z

Z = D ( u , v ) Z=D(u,v) Z=D(u,v)

其中:

  • u , v u, v u,v:图像像素坐标;
  • D ( u , v ) D(u,v) D(u,v):对应的深度值;
  • f x , f y f_x, f_y fx,fy:相机焦距参数;
  • c x , c y c_x, c_y cx,cy:主点坐标;
  • X , Y , Z X, Y, Z X,Y,Z:相机坐标系中的三维点。

所有目标像素转换后形成 point cloud(三维点云)

形象理解:
普通照片像一张平面的彩色纸,点云则像用成千上万个小点搭出的三维模型。每个点不仅有左右和上下位置,还记录了离相机多远。

3.2.5 第五步:相机坐标转换到机器人基座

相机知道的是“物体相对于相机的位置”,机械臂需要的是“物体相对于机器人基座的位置”。

论文使用 AprilTag 进行外参标定:

P R = T A R ( T C A P C ) P_R=T_{AR}\left(T_{CA}P_C\right) PR=TAR(TCAPC)

其中:

  • P C P_C PC:目标在相机坐标系中的坐标;
  • T C A T_{CA} TCA:相机坐标系到 AprilTag 坐标系的变换;
  • T A R T_{AR} TAR:AprilTag 坐标系到机器人基座的变换;
  • P R P_R PR:目标在机器人基座坐标系中的坐标。

可以把 AprilTag 理解为相机和机械臂共同认识的“坐标参照物”。

3.2.6 第六步:提取目标中心位置

公开代码主要对分割区域内的三维点求平均:

p ˉ = 1 N ∑ i = 1 N p i \bar{p} = \frac{1}{N} \sum_{i=1}^{N} p_i pˉ=N1i=1Npi

对应的简化代码是:

target_position = object_points.mean(axis=0)

这意味着视觉模块主要给出目标中心的三维位置:

[x, y, z]

它并没有自动解决完整的六自由度抓取位姿:

[x, y, z, roll, pitch, yaw]

所以要区分四个概念:

技术环节 回答的问题
Object detection 图中有没有杯子,杯子大致在哪里?
Segmentation 哪些像素属于杯子?
3D localization 杯子的三维中心在哪里?
Grasp planning 夹爪从哪个方向、以什么姿态抓杯子?

ELLMER 的视觉模块主要完成前三项;抓取方向和偏移量仍大量依赖运动原语中的预设规则和 affordance(可供性) 先验。

3.2.7 视觉更新速度与能力边界

论文相机采样率为 30 fps,但最终目标位置更新频率约为三分之一赫兹,也就是大约每 3 秒更新一次。

因此:

相机以 30 fps 获取图像
≠
机器人以 30 Hz 获得新的目标坐标

Grounding DINO、SAM、点云生成和坐标计算需要较长时间,所以系统适合相对缓慢的环境变化,不属于高速目标追踪系统。

论文还报告:

  • 实验条件下白色杯子的识别率达到 100%;
  • 遮挡 20%~30% 时约为 90%;
  • 遮挡 80%~90% 时下降到约 20%。

这里的识别率不是抓取成功率,也不是三维定位精度。检测准确、定位准确和最终抓取成功是三个不同问题。


3.3 力反馈:接触、抽屉与倒水

在这里插入图片描述

论文使用 ATI 六轴力/力矩传感器,可以测量:

  • 三个方向的力;
  • 三个方向的力矩。

形象理解:力传感器就是机器人的“手感”。
人在放杯子时,不需要一直盯着杯底,只要手上感到桌面的反作用力,就知道杯子已经落桌;机器人也利用类似的力变化判断接触是否发生。

3.3.1 为什么要标定力传感器?

传感器安装在机械臂末端,会受到夹爪、工具和自身重量影响。

如果不标定,传感器读数中会混有:

  • 夹爪重力;
  • 水壶本身的重量;
  • 工具安装偏差;
  • 传感器零漂;
  • 不同姿态下的重力投影。

项目 README 给出了分姿态归零的标定方法,目的就是让后面的变化量更接近真实外力。

3.3.2 为什么要做坐标变换?

传感器测得的是末端局部坐标系中的力。机械臂旋转以后,传感器的 x、y、z 轴也随之旋转。

因此需要把局部力转换到机器人基座坐标系:

F global = T end-effector → base F local F_{\text{global}} = T_{\text{end-effector} \to \text{base}} F_{\text{local}} Fglobal=Tend-effectorbaseFlocal

其中 R R R 是旋转矩阵。

这样,无论水壶处于什么姿态,系统都能在统一的全局坐标中解释“竖直方向的支撑力”。

形象理解:
手机无论横着拿还是竖着拿,屏幕坐标的“上方”都会变化;但现实世界中的重力方向不会变化。力坐标变换就是把传感器自身的方向,换算成机器人共同使用的世界方向。

3.3.3 不同动作使用不同力信息

ELLMER 并不是把视觉和力觉一起输入一个统一神经网络,而是采用 task-dependent sensor fusion(面向任务的传感器融合)

不同技能在不同阶段使用最合适的反馈:

技能 主要反馈
寻找杯子 视觉
移动到杯口上方 视觉 + 末端位姿
放置杯子 接触力
打开抽屉 力和力矩
倒水量估计 重量变化
绘图接触 竖直方向力

也就是说,它更像一套工程化的多传感器闭环,而不是端到端学习的多模态融合网络。

3.3.4 倒水质量估计

在慢速操作下,论文使用 quasi-static assumption(准静态假设)

F u p ≈ m g F_{\mathrm{up}}\approx mg Fupmg

水壶中水的质量减少,会引起支撑力变化:

Δ F u p ≈ Δ m g \Delta F_{\mathrm{up}}\approx \Delta m g ΔFupΔmg

因此:

Δ m ≈ ∣ Δ F u p ∣ g \Delta m \approx \frac{\left|\Delta F_{\mathrm{up}}\right|}{g} ΔmgΔFup

转换为克以后,简化代码类似:

poured_mass = (
    abs(current_force - initial_force)
    * 1000
    / 9.81
)

poured_mass 达到目标值,机器人停止倾斜水壶。

形象理解:这像在厨房秤上倒面粉。
你不需要看每一粒面粉,只要观察容器重量变化,就能知道已经倒了多少。ELLMER 反过来观察水壶变轻了多少,从而估计已经流出去多少水。

这里控制的是“累计倒出质量达到阈值”,不是建立精确的瞬时流量模型。它没有直接学习:

水壶倾角
→ 液体流速
→ 瞬时流量

当动作速度变快时,传感器读数还会受到惯性、液体晃动和质心移动影响,准静态假设就会逐渐失效。

3.3.5 开抽屉中的 PID 思想

打开抽屉时,机器人需要根据当前力和力矩修正速度。源码中使用了 PID 控制思想。

误差为:

e ( t ) = r ( t ) − y ( t ) e(t)=r(t)-y(t) e(t)=r(t)y(t)

其中:

  • r ( t ) r(t) r(t):期望受力或目标状态;
  • y ( t ) y(t) y(t):当前测量值。

PID 输出为:

u ( t ) = K p e ( t ) + K i ∫ 0 t e ( τ ) d τ + K d d e ( t ) d t u(t) = K_p e(t) + K_i \int_{0}^{t} e(\tau) d\tau + K_d \frac{de(t)}{dt} u(t)=Kpe(t)+Ki0te(τ)dτ+Kddtde(t)

  • 比例项快速响应当前误差;
  • 积分项消除长期偏差;
  • 微分项抑制变化过快和振荡。

控制器再对输出速度进行限幅,避免机械臂因瞬时力变化产生过大的动作。


3.4 运动原语、ROS 与安全控制

3.4.1 什么是运动原语?

Motion primitive(运动原语) 是一个经过封装的可复用机器人技能,例如:

go_to_position()
take_object()
put_down_object()
open_door()
scoop()
pour()
drawing()

一个运动原语通常包含:

  • 输入参数;
  • 速度控制;
  • 传感器反馈;
  • 完成条件;
  • 超时条件;
  • 力或速度限制;
  • 失败后的停止逻辑。

因此,GPT-4 不需要每次都重新编写底层控制,只需选择和组合已有技能。

形象理解:运动原语像乐高积木。
“抓取”“放置”“倒水”都是已经搭好的积木块。GPT-4 的工作是决定怎样组合这些积木,而不是重新制造每一个齿轮和连接件。

3.4.2 通用技能与场景专用代码

需要特别注意:项目中的技能并不全是完全通用的。

一部分函数是较通用的接口,例如移动到目标位置、抓取和倒水;另一部分包含:

  • 固定抽屉坐标;
  • 固定水壶抓取姿态;
  • 固定盘子位置;
  • 固定动作持续时间;
  • 针对当前实验台设计的偏移量。

因此应准确理解论文所说的 scalable(可扩展)

ELLMER 的架构和知识库可以继续加入新技能,但当前运动原语并不能不加修改地迁移到任意厨房或任意机械臂。

此外,论文假设低层控制器已经处理 obstacle avoidance(避障)。ELLMER 本身的重点不是重新解决完整的路径规划与避障问题。

3.4.3 ROS 如何连接各模块?

项目主要启动:

roslaunch kortex_driver kortex_driver.launch
roslaunch kortex_examples run_vision.launch
roslaunch kortex_examples run_force.launch
roslaunch kortex_examples run_brain.launch

对应关系为:

模块 作用
Kortex driver 连接 Kinova 机械臂
Gripper node 控制 Robotiq 夹爪
Vision node 检测、分割、点云和坐标发布
Force node 力/力矩读取、标定和坐标变换
Brain node 获取 LLM 代码并调度执行

不同模块通过 topic(话题)service(服务) 和消息进行通信。

形象理解:ROS 像一个机器人内部的群聊系统。
相机节点在群里发布“杯子坐标”,力传感器节点发布“当前受力”,主控制节点订阅这些消息,再向机械臂驱动发送速度命令。每个节点只负责自己的工作,不需要知道整个系统的所有细节。

3.4.4 安全限制为什么必须放在底层?

论文把最大速度、最大力和工作空间边界写入底层运动控制,而不是让 GPT-4 决定。

论文报告的限制包括:

  • 线速度约束;
  • 角速度约束;
  • 最大末端力 20 N;
  • 预定义的三维工作空间。

其原则是:

LLM 负责灵活性,确定性的控制器负责不可违反的安全边界。

不过,公开代码、知识库说明和论文中的部分限速数值并不完全一致,复现时必须以自己测试验证后的底层参数为准,而不能只复制某一个文件中的默认值。


3.5 从 DALL-E 图像到机械臂绘图

在这里插入图片描述

绘图任务的技术链路为:

用户给出图案类别
→ DALL-E 生成黑色剪影
→ 图像灰度化和二值化
→ OpenCV 提取轮廓
→ 简化轮廓点
→ 缩放到真实空间
→ 机械臂沿轨迹运动
→ 力反馈判断笔尖接触

形象理解:
这个过程类似把一张卡通图变成“儿童连点画”。系统先找出图案最外侧的边线,再把密集的像素边界压缩成一串关键点,最后让机械臂像连点一样依次经过这些位置。

3.5.1 为什么生成剪影图?

系统提示 DALL-E 生成:

  • 白色背景;
  • 黑色剪影;
  • 无阴影;
  • 无额外物体。

原因是这种图像容易二值化,也容易提取外轮廓。

3.5.2 OpenCV 轮廓提取

核心过程可以简化为:

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

_, binary = cv2.threshold(
    gray,
    127,
    255,
    cv2.THRESH_BINARY_INV
)

contours, _ = cv2.findContours(
    binary,
    cv2.RETR_EXTERNAL,
    cv2.CHAIN_APPROX_SIMPLE
)

largest_contour = max(
    contours,
    key=cv2.contourArea
)

系统使用 RETR_EXTERNAL 获取外轮廓,并选择面积最大的轮廓。

这意味着:

  • 内部纹理通常不会被画出来;
  • 多个分离区域可能只保留最大的一个;
  • 图案越复杂,轨迹越容易失真。
3.5.3 轮廓简化与轨迹缩放

像素轮廓点过多会导致机械臂轨迹冗长,因此使用 Douglas–Peucker 算法简化轮廓。

之后把图像坐标缩放到盘子所在的真实坐标范围。机械臂先移动到第一个轨迹点上方,再缓慢下降,通过力反馈检测笔尖是否已经接触盘面,最后沿二维轮廓运动。

当前源码中盘子位置和部分偏移仍较固定,因此该功能展示的是“生成图像可以转成机器人轨迹”,而不是已经解决任意平面、任意位置的通用绘图问题。


四、实验过程与结果

4.1 实验平台

论文使用:

  • Kinova Gen3 七自由度机械臂;
  • Robotiq 2F-140 夹爪;
  • Azure Kinect DK 深度相机;
  • ATI 六轴力/力矩传感器;
  • Ubuntu 20.04 与 ROS Noetic;
  • NVIDIA RTX 2080。

4.2 完整任务

在这里插入图片描述

机器人最终连续完成:

打开抽屉
→ 抓取并放置杯子
→ 舀取咖啡
→ 将咖啡倒入杯中
→ 追踪杯子并倒水
→ 获取画笔
→ 生成轮廓
→ 在盘子上绘图

4.3 主要结果

视觉结果
  • 实验条件下,白色杯子的识别率达到 100%;
  • 遮挡 20%~30% 时,识别成功率约为 90%;
  • 遮挡 80%~90% 时,成功率下降到约 20%;
  • 目标坐标更新频率约为三分之一赫兹。
RAG 结果

作者用 80 条任务查询评估 faithfulness(忠实度)

模型 无 RAG 使用 RAG
GPT-4 0.74 0.88
GPT-3.5-turbo 0.78 0.86
Zephyr-7B-beta 0.37 0.44

结果说明 RAG 使生成内容更符合技能知识库,但该指标不是实体任务成功率。

倒水结果

低速倒水时,论文报告每倒出 100 g 水,误差约为 5.4 g。速度升高以后,惯性和液体晃动使误差明显增加。

完整任务结果

在这里插入图片描述

机器人成功完成了一次完整的咖啡制作和盘子绘图流程,说明该框架能够把语言推理、技能检索、多传感器反馈和真实机械臂执行连接起来。

但论文没有报告大量重复实验中的:

  • 完整任务成功率;
  • 每个子任务的成功率;
  • 平均完成时间;
  • 置信区间;
  • 大规模端到端消融结果。

因此,这项实验更适合作为 proof of concept(概念验证),而不是已经充分验证的通用机器人系统。


五、总结与技术边界

5.1 这篇论文真正证明了什么?

ELLMER 证明了一条可行的系统路线:

LLM 高层推理
+ RAG 技能检索
+ 任务级代码
+ 运动原语
+ ROS 实时执行
+ 视觉和力觉反馈

它说明 GPT-4 可以理解较抽象的人类指令,并组织已有机器人技能完成一项较长的任务。

ELLMER 更准确的评价是:

一个结构清晰、具有代表性的具身 LLM 机器人系统原型,而不是已经成熟的通用家庭机器人。


六、如何复现 ELLMER?

6.1 先确定你想复现到什么程度

“复现论文”并不一定意味着第一天就买齐机械臂、相机和力传感器。对初学者,更合理的方式是把复现分成三个等级。

等级 目标 是否需要真实机械臂
Level 1 复现 RAG 检索与机器人计划生成 不需要
Level 2 复现视觉检测、分割、点云和 ROS 消息链路 可以不需要
Level 3 使用 Kinova、夹爪、相机和力传感器完成完整实体任务 需要

建议按照:

先让模型正确“说出怎么做”
→ 再让视觉模块正确“看见物体”
→ 再手动测试单个机器人技能
→ 最后才连接 GPT-4 自动执行

不要一开始就让 LLM 控制完整机械臂。这样一旦出错,很难判断问题来自语言模型、网络、视觉、坐标标定,还是底层控制。


6.2 官方资料与项目目录

项目仓库:

https://github.com/ruaridhmon/ELLMER

论文演示视频:

https://www.youtube.com/watch?v=WiLEw9Zu2MA

代码中的主要目录如下:

ELLMER/
├── llm_robot_planning/
│   ├── custom_gpt_examples/       # 自定义 GPT、知识库、Action 与 Flask 服务
│   └── haystack_rag_pipelines/    # 开源 RAG 示例与评估代码
├── ros_kortex/                    # Kinova 驱动与 ELLMER ROS 代码
├── netft_rdt_driver/              # ATI 力/力矩传感器接口
├── robotiq_2finger_grippers/      # Robotiq 夹爪驱动
└── data/                          # 实验数据

初学者阅读源码时,可以先看:

README.md
llm_robot_planning/custom_gpt_examples/README.MD
ros_kortex/kortex_examples/src/ELLMER/

6.3 硬件与软件环境

论文使用的主要硬件包括:

硬件 作用
Kinova Gen3 七自由度机械臂 执行动作
Robotiq 2F-140 夹爪 抓取杯子、勺子和水壶
ATI 网络力/力矩传感器 测量接触力与重量变化
Azure Kinect DK 获取彩色图和深度图
NVIDIA RTX 2080 运行视觉模型
AprilTag 标定相机与机器人坐标关系

官方仓库说明该系统测试于:

Ubuntu 20.04
ROS Noetic
Python 3
NVIDIA RTX 2080

需要特别注意,ROS Noetic 已经是较旧的 ROS 1 技术栈。为了忠实复现论文,建议使用一台独立电脑、双系统或固定版本的虚拟机/镜像,不要随意升级 Python、CUDA、Conan 和视觉依赖。


6.4 安装基础环境和下载代码

首先安装 ROS Noetic,并初始化 rosdep。完成 ROS 安装后,按照官方仓库的方式建立 Catkin 工作空间:

sudo apt update
sudo apt install python3 python3-pip git

sudo python3 -m pip install conan==1.59

conan config set general.revisions_enabled=1
conan profile new default --detect > /dev/null
conan profile update settings.compiler.libcxx=libstdc++11 default

mkdir -p ~/ellmer_ws/src
cd ~/ellmer_ws/src

git clone https://github.com/ruaridhmon/ELLMER.git

cd ~/ellmer_ws
rosdep install --from-paths src --ignore-src -y

这里必须使用 Conan 1.x。项目 README 明确使用:

conan==1.59

Conan 2.x 的命令和包管理方式不同,可能导致 Kortex 依赖无法正常构建。


6.5 安装视觉模块

进入项目自带的 Grounded-Segment-Anything 目录,并参考其官方安装说明安装依赖:

cd ~/ellmer_ws/src/ELLMER/ros_kortex/third_party/Grounded-Segment-Anything

常见的核心安装步骤包括:

python3 -m pip install -e segment_anything
python3 -m pip install --no-build-isolation -e GroundingDINO

python3 -m pip install \
    opencv-python \
    pycocotools \
    matplotlib \
    open3d

还需要根据项目代码中配置的模型类型,下载对应的 Grounding DINO 与 SAM/MobileSAM 权重,并把权重路径写入视觉脚本。

注意: Grounded-Segment-Anything、PyTorch、CUDA 和编译器版本很容易产生兼容问题。先单独运行官方图片 Demo,确认“文本输入 → 检测框 → 分割掩膜”能够成功,再接入 ROS。不要在视觉模型尚未运行成功时直接启动完整 ELLMER。


6.6 编译 ROS 工作空间

完成依赖安装后:

cd ~/ellmer_ws

catkin_make
source devel/setup.bash

为了每次打开终端自动加载:

echo "source ~/ellmer_ws/devel/setup.bash" >> ~/.bashrc
source ~/.bashrc

项目 README 还提醒,需要检查 Python 脚本开头的 Shebang:

#!/usr/bin/env python3

它必须指向实际使用的 Python 环境,否则 ROS 可能调用错误的 Python 解释器。


6.7 先验证机械臂和夹爪

在连接 LLM 之前,先只启动 Kinova 驱动:

roslaunch kortex_driver kortex_driver.launch

检查:

rostopic list
rosnode list

确认能够看到机械臂相关节点和状态话题。

接下来单独测试:

  1. 机械臂是否能够到达 home 位姿;
  2. 是否能够发送一个非常小的速度命令;
  3. 夹爪是否能够打开和关闭;
  4. 急停按钮是否有效。

第一次测试时应清空机械臂周围空间,并把速度限制得很低。不要直接运行咖啡制作任务。


6.8 相机内参与相机—机器人外参标定

6.8.1 获取相机内参

相机内参告诉系统:一个图像像素应该怎样转换成相机坐标系中的射线。

启动 Azure Kinect 后可以查询:

rostopic list | grep camera_info
rostopic echo /depth_to_rgb/camera_info

将真实相机的:

fx, fy, cx, cy

更新到视觉代码中。不能直接复制论文作者的内参,因为每台相机和分辨率配置可能不同。

6.8.2 使用 AprilTag 标定外参

官方项目使用 tagStandard41h12 AprilTag,并打印在 A3 纸上以获得更稳定的检测。

运行项目提供的标定 Launch:

roslaunch run_calibration.launch

标定的目标是求出:

相机坐标系
→ AprilTag 坐标系
→ 机器人基座坐标系

之间的变换矩阵。

形象理解: 相机和机器人原本使用两张不同的地图。AprilTag 是两张地图上都能找到的地标,通过这个共同地标,就能把“杯子在相机右边”翻译成“机械臂应该移动到基座坐标中的哪个位置”。

标定后,先放置一个静止杯子,检查视觉发布的坐标是否与人工测量大致一致。若机械臂总是朝错误方向移动,最常见原因不是 GPT-4,而是坐标轴方向、变换矩阵顺序或单位出现错误。


6.9 标定力/力矩传感器

官方 README 给出的标定步骤是:

  1. 将机械臂移动到 home 位姿;
  2. 对全部轴归零:
rosservice call /netft/zero_axis "axis: 'all'"
  1. 绕传感器 z 轴旋转 90 度,使 x 轴与地面平行;
  2. 再对 x 轴归零:
rosservice call /netft/zero_axis "axis: 'x'"

这样做是为了降低工具重力对不同姿态下读数的影响。

标定以后建议做三个简单测试:

不接触物体:读数应接近零
轻推末端:对应方向的力应明显变化
拿起已知质量物体:竖直力变化应大致符合 mg

如果方向相反,需要检查传感器坐标系与机器人基座坐标系之间的旋转矩阵。


6.10 验证视觉模块

启动视觉节点:

roslaunch kortex_examples run_vision.launch

然后按照下面的顺序验证:

  1. 输入单一类别,例如 white mug
  2. 查看图像上是否出现正确检测框;
  3. 查看分割掩膜是否只覆盖杯子;
  4. 检查点云是否与物体重合;
  5. 检查 target_coordinates 是否持续发布三维坐标;
  6. 手动移动杯子,观察坐标是否更新。

此时不要急着控制机械臂。视觉结果稳定后,再让机械臂移动到目标上方,而不是直接抓取。


6.11 单独验证每个运动原语

完整任务由多个运动原语组成。建议按照风险从低到高逐一测试:

go_to_position
→ 打开/关闭夹爪
→ take_object
→ put_down_object
→ scoop
→ open_door
→ track_and_pour
→ drawing

每测试一个技能,都应记录:

  • 输入参数;
  • 实际终点;
  • 是否触发超时;
  • 是否超过工作空间;
  • 传感器读数是否合理;
  • 失败时是否安全停止。

项目中存在固定坐标和固定偏移,因此必须根据自己的桌面、杯子、水壶和抽屉重新修改参数。


6.12 配置 Custom GPT 与服务器

官方仓库在:

llm_robot_planning/custom_gpt_examples/

中提供了四个关键文件:

custom_knowledge.md
custom_gpt_instructions.md
custom_gpt_action_schema.md
app.py

按照官方说明,配置过程为:

  1. custom_knowledge.md 上传到自定义 GPT 的 Knowledge;
  2. custom_gpt_instructions.md 放入 GPT Instructions;
  3. 添加 custom_gpt_action_schema.md
  4. 将 Schema 中的服务器地址改成自己的公开地址;
  5. 创建 GPT;
  6. 在服务器上运行 app.py

服务器可以部署在 AWS,也可以部署在其他平台。关键要求是 GPT 能够访问它,因此普通的:

http://localhost:端口

通常不能直接被云端 GPT 访问。需要使用公开可访问的 HTTPS 地址,并根据当前平台要求配置认证与 Action。

服务器端可以尝试:

cd ~/ellmer_ws/src/ELLMER/llm_robot_planning/custom_gpt_examples
python3 app.py

启动后先用 curl 或 Postman 测试:

send_action 是否能接收 JSON
get_action 是否能返回队列中的动作

只有通信测试通过后,才让 robot_brain.py 连接该服务器。


6.13 按正确顺序启动完整系统

官方 README 给出的启动顺序是:

roslaunch kortex_driver kortex_driver.launch
roslaunch kortex_examples run_vision.launch
roslaunch kortex_examples run_force.launch
roslaunch kortex_examples run_brain.launch

分别对应:

机械臂与夹爪
→ 视觉模块
→ 力反馈模块
→ LLM 代码接收与任务执行

同时确保 Flask/AWS 服务器已经运行。

可以把复现成功分成以下里程碑:

里程碑 成功标准
1 GPT 能根据知识库生成真实存在的函数
2 Flask 能收发 run_code 动作
3 视觉能发布正确的杯子坐标
4 力传感器能反映接触和重量变化
5 单个运动原语可以稳定执行
6 GPT 代码可以调用一个低风险技能
7 多个技能可以按顺序完成
8 完成长时序咖啡任务

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐