ProgPrompt:别让 LLM「自由发挥」,让它写 Python 代码来计划机器人任务
🎯 为什么重要:LLM 进机器人规划的两种「翻车」方式
2022 年 LLM 开始被用于机器人高层规划,但有两种主流路线都翻车:
- SayCan 路线(打分):枚举所有 admissible actions,让 LLM 打分挑下一个。问题:动作空间组合爆炸时枚举不动;每个 step 都要 query LLM,慢。
- Huang [2] 路线(自由文本生成):直接让 LLM 写出整段动作描述。问题:可能写出机器人不会做的动作(「拿出微波炉里的碗」),或用到场景里没有的物体;并且没有环境反馈,开环执行容易崩。
ProgPrompt 的洞察:LLM 同时是「世界知识库」和「代码生成器」——前者负责常识,后者负责约束。把两件事合起来:让 LLM 写一段 Python 程序来表达计划,用 Python 的语法结构天然约束输出。
💡 核心思想:把「任务规划」翻译成「Python 代码补全」
ProgPrompt 的 prompt 长这样(论文 Fig. 2/3 简化):
# 1. 告诉 LLM 有哪些动作(机器人能力) from actions import walk, find, grab, open, close, putin, switchon, switchoff, ... # 2. 告诉 LLM 场景里有哪些物体(situated state) objects = ['salmon', 'microwave', 'plate', 'fridge', 'bookshelf', ...] # 3. 给 3 个示例任务(few-shot) def throw_away_lime(): # 0: find lime find('lime') # 1: grab lime assert('close' to 'lime') else: find('lime') grab('lime') ... # 4. 让 LLM 补全下一个任务(answer search) def microwave_salmon(): # 0: walk to kitchen walk('kitchen') # 1: find microwave find('microwave') ...
这个结构里有四个关键编程语言特性同时被利用:
🛠️ 程序结构的四个特性(全做了消融)
| 特性 | 形式 | 解决的问题 |
|---|---|---|
| ① import 声明 | from actions import walk, grab, open, ... | LLM 不会输出「机器人不会做的动作」(比如「拿出」)。换机器人时只需换 import 列表 |
| ② 物体列表 | objects = ['salmon', 'microwave', ...] | LLM 不会用到场景里没有的物体。situated awareness 不靠训练,靠 prompt 注入 |
| ③ 自然语言注释 | # 1: grab salmon 在 grab('salmon') 前 | 把任务拆成子目标,类似 chain-of-thought;防止 LLM 输出重复、发散、不连贯的动作序列 |
| ④ assert + else 恢复 | assert('close' to 'salmon') else: find('salmon') | 闭环环境反馈:执行前检查前置条件,不满足就走恢复分支。失败时不用整个计划推倒重来 |
因为代码有语法约束:函数名必须在 import 列表里、参数必须是 objects 列表中的字符串、注释和函数体要对得上。LLM 在「补全代码」任务上已经被海量 GitHub 数据训练过,它天然遵循这些约束。而自由文本生成的 LLM 输出虽然语义通顺,但没有任何机制阻止它「说人话但做不了」。
📊 关键结果(论文 Table I / III / IV)
| 实验 | 对比 | 关键数字(SR↑ / Exec↑ / GCR↑) |
|---|---|---|
| Table I · VirtualHome 主结果 | Huang et al. [2](GPT3) | 0.00 / 0.45 / 0.21 |
| LangPrompt(GPT3,自然语言 prompt) | 0.00 / 0.36 / 0.42 | |
| ProgPrompt + GPT3(3 注释 + 3 反馈) | 0.34 / 0.84 / 0.65 | |
| ProgPrompt + DaVinci(2 示例,prompt 限制) | 0.22 / 0.60 / 0.46 | |
| ProgPrompt + Codex(代码预训练,总最佳) | 0.40 / 0.90 / 0.72 | |
| Table I · 消融(GPT3) | 3 注释 + 7 反馈 | 0.28 / 0.82 / 0.56(反馈太多反而降) |
| 7 注释 + 3 反馈 | 0.30 / 0.65 / 0.58 | |
| 7 注释 + 7 反馈 | 0.18 / 0.68 / 0.42(overload) | |
| 注释过多(行 5 vs 3:3→7 注释) | SR 0.34 → 0.30;GCR 0.65 → 0.58 | |
| Table III · 新场景泛化 | ENV-0(原场景) | SR 0.34 / GCR 0.65 |
| ENV-1 / ENV-2(未见场景) | SR 0.56 / 0.56;GCR 0.81 / 0.72(新场景反而更好) | |
| Table IV · Franka 真机 | 桌面 pick-and-place(含干扰物) | Plan SR 几乎全 1.0;Exec 始终 = 1;唯一失败是 sort 任务把 soup can 错认为 bottle |
(1) LangPrompt 完全失败(SR=0),ProgPrompt 在同一个 GPT3 上 SR=0.34——差距完全来自 prompt 结构,模型不变。
(2) Codex 比 GPT3 更好(0.40 vs 0.34),证明「代码预训练」和「代码风格 prompt」是双向匹配的。
(3) 注释 + 反馈都不是越多越好:3+3 最佳,7+7 反而 SR 掉到 0.18——LLM context 容量有限,信息密度比信息量更重要。
🌐 在领域中的位置
ProgPrompt 是「LLM as code generator for robots」这条线的开山之作之一(与同期的 Socratic Models [3] 并列)。它建立了「用编程语言结构约束 LLM 输出」的范式,之后的 Code as Policies(Liang 2023)把这个思路推到「LLM 直接生成 Python 调用 perception / control API」,Voxposer 则进一步生成 3D 空间中的 value maps。这条线最终汇入 VLA 大模型(RT-2、π0)的「动作 token 化」思路。
❓ 常见误区
ProgPrompt 是不是就是「让 LLM 写 Python」那么简单?
形式上是的,但价值在于「用哪种 Python 结构约束 LLM」。论文做了系统消融——import / object list / comments / assert-else 四件套,每一件都贡献正向。把注释从 3 条加到 7 条,SR 从 0.34 掉到 0.30(信息过载);反馈过多同理。所以不是「写 Python」牛,而是「用 Python 的特定结构」牛。
assert + else 真的能闭环纠错吗?真机上似乎没用?
对,真机实验中 ProgPrompt 没用反馈机制(Table IV 的 SR 是 Plan SR,假设 pick-and-place 成功)。原因是真机状态追踪不够稳定,assert 无法可靠检查。这是 ProgPrompt 的诚实局限——仿真里 assert 闭环很好,真机要靠感知系统先到位。
为什么不用更大的 LLM、不用 chain-of-thought?
论文用的是 2022 年的 GPT-3 / Codex,那会儿 CoT 还刚出来。但 ProgPrompt 的注释本质上就是一种 CoT——只不过「子目标 + 代码」比「自由思考 + 答案」结构化更强。论文 Table I 也专门验证了注释(即 CoT)的功效。
和 Code as Policies 比有什么区别?
ProgPrompt 生成的是高层任务计划(动作是 `find`、`grab`、`putin` 这种符号 API),Code as Policies 生成的是含感知/控制 API 调用的代码(比如直接调 `get_object_position`、`move_to`)。前者依赖环境提供 primitive,后者更接近端到端。ProgPrompt 是「LLM 做高层规划」,Code as Policies 是「LLM 做中低层控制」。