枢纽
机器人Tabletop manipulator
上真机
实时
输入模态vision,language

Code as Policies:让 LLM 写代码,而不是写「计划」

Jacky Liang, Wenlong Huang, Fei Xia 等(Robotics at Google)。ICRA 2023 / arXiv:2209.07753v4 (2023-05)。 项目页 · 全开源 · foundation_modelsLLM × 机器人

🧠 一句话心智模型:与其让 LLM 输出一串「自然语言计划」("先抓杯子,再放到碗上"),不如让它直接写一段 Python 代码——代码可以 if/else 判断感知、可以 for 循环、可以调 NumPy 算几何,还能调你提供的 pick_place() 原语。语言计划只能「讲故事」,代码能「算数 + 执行」,这就是质的差别。

🎯 为什么重要:自然语言是「粘合剂」,代码才是「真策略」

在 CaP 之前,LLM 接机器人主要有两种范式:

SayCan / Inner Monologue 范式:LLM 输出一段自然语言步骤("1. 走到冰箱 2. 打开 3. 拿可乐…"),再用价值函数或文本解析把每步对应到预训练技能。
瓶颈:自然语言没有「数值」。说「向左 0.3 米」简单,但说「把红色块每隔 5 厘米排成一行」就崩了——LLM 没法在自然语言里精确表达算术、几何、循环。

CaP 的洞察:代码本身就是一种「带数值、带逻辑、带库函数」的策略表示。LLM 已经在海量 GitHub 代码上训练过,写机器人代码只是把它的能力换个出口——无需任何微调、无需任何机器人数据。

自然语言指令 "把所有方块放到 空的碗里, 动作快一点" Codex LLM (few-shot prompt Hints + Examples) 输出: Python 代码 生成代码 block_names = detect("blocks") for b in bowl_names: if is_empty(b): eb = b objs = [eb] + block_names stack_objects(objs, speed=0.2)
图 1:Code as Policies 的核心循环——把自然语言指令当作「注释」,让代码 LLM 写出对应的 Python,调用 perception/control API 完成任务。

💡 核心思想:用「层级代码生成」把模糊指令变精确程序

CaP 的方法分两层。低层是few-shot prompting:给 LLM 一些「注释→代码」范例,让它照葫芦画瓢。高层是层级代码生成(Hierarchical Code-Gen)——这是 CaP 真正的「招」:

高层 LMP 读指令 → 生成 主策略代码 AST 解析 找出代码里 未定义的函数 函数生成 LMP 递归写出每个 缺失函数 函数库 每定义一个 就被记下复用 递归(depth-first),直到无未定义函数 例:高层代码调用 get_objs_bigger_than_area_th()(未实现)→ 触发 LMP 写这个函数 → 它内部又调 get_obj_bbox_area() → 再写…
图 2:层级代码生成。LLM 只需写「主程序 + 函数签名」,缺失的实现会被另一个 LMP 自动补全。这让 prompt 可以更简洁、代码更易读、生成更准。

这种设计有个意外收益:因为函数名本身就是「文档」(如 get_objs_bigger_than_area_th),LLM 能从命名推断语义(「bbox_xyxy 一定是 [x1,y1,x2,y2]」)。等于把「chain-of-thought」从自然语言搬到了函数式编程里。

🛠️ 三个关键设计:Hints / Examples / Safety

设计心智为什么必要
① Hints(导入 + 类型)prompt 开头先 import numpy as np; from utils import get_pos, put_first_on_second告诉 LLM「你能调哪些 API」——API 名字就是最强的 grounding 信号
② Examples(few-shot)给数个「注释→代码」范例(论文附录里每个 prompt 含 2–3 个,覆盖不同任务模式)示范如何把模糊语言("a bit to the left")映射成具体数值(+ [-0.1, 0])
③ 安全 exec()禁止 __ 变量、importexec/eval;用受限 globals 调 exec()LLM 生成的代码要在真机上跑,必须防注入、防逃逸
🔮 直觉:为什么「写代码」比「写计划」强?
三个杀手锏:
(1) 精确数值:"faster" → speed=0.2,由 LLM 从训练分布里取合理值;
(2) 算术 + 几何:「排成 20cm 长的线」→ np.linspace(0, 0.2, n),免费继承 NumPy/Shapely;
(3) 反馈控制while not detect("apple"): move_right()——一行代码就是一个 reactive policy,自然语言做不到。

📊 结果:在 unseen 任务上完胜端到端方法

评测任务/数据关键数字
通用代码生成HumanEval (OpenAI)层级 Codex (davinci) Greedy 53.0% / P@100 95.7%(当时 SOTA)
机器人代码生成RoboCodeGen (37 题,本文新提)层级 Codex davinci 95%(Flat 81%);GPT-3 6.7B 仅 5%
桌面操作(seen 属性)Long-HorizonCaP 97.2%(CLIPort 78.8%,NL Planner 86.4%)
桌面操作(unseen 属性)Long-HorizonCaP 97.6%(CLIPort 暴跌至 36.8%)
桌面操作(unseen 任务+属性)Spatial-GeometricCaP 62.0%(CLIPort 几乎为 0.01%)
真机演示UR5e / Everyday Robots形状绘制、桌面操作、厨房移动操作——零训练、few-shot 即可
关键洞察:CaP 最震撼的不是 seen 任务赢了几个点,而是在「unseen 任务+unseen 属性」上仍能跑 62%,而监督学习的 CLIPort 直接掉到 0。这说明 LLM 的「语义组合能力」给了机器人一种免训练的泛化——只要新任务能用已有 API 组合出来,就能做。

🌐 在领域中的位置

CLIPort(端到端语言条件策略) SayCan / Inner Monologue(LLM 出语言计划) Code as Policies(LLM 出代码)⭐ Voxposer / ChatGPT for Robotics / Eureka VLA 模型(RT-2 / π0)把「代码」内化到权重

CaP 是「LLM 接机器人的中间时代」的代表作——在 VLA 出现之前,它把 LLM 当作「外挂的策略生成器」,靠 prompt 完成零训练部署。它给后人留了两个遗产:(1) 「写代码」是比「写计划」更强的策略表示,(2) RoboCodeGen 这个评测集。后续 Voxposer 把这个思路推广到 3D 场景,Eureka 把它推到奖励函数设计,而 VLA 则是把这种能力蒸馏进模型权重——但范式起点都是 CaP。

❓ 常见误区

CaP 是端到端学习吗?

不是。CaP 不训练任何模型——它只是 prompt 一个预训练好的 Codex。所有「学习」都发生在 LLM 预训练阶段(在海量代码上)。论文里所有真机演示都是 zero-training,靠 few-shot prompt + 已有 API。

那它算「策略」吗?策略不是应该把 observation 映射到 action?

算,但「策略」的层级更高。CaP 生成的代码本身就是一个策略——它调用 detect() 读感知,用 if/while 做反馈,调 set_velocity() 出动作。它是一个由 LLM 即时合成、由 Python 解释器执行的策略。论文 §III-D 专门讨论了这种「policy code」的层级定位。

它有什么硬伤?

论文自己列了三条:(1) 受感知 API 表达力限制——没法说「这条路更 bumpy」;(2) 受控制原语覆盖限制——没给 grasp_cube() 就做不了抓取;(3) 指令长度/复杂度上去后会崩——LLM 长上下文推理仍不稳。更根本地,它没有「学习」能力:同样的指令永远生成同样的代码,不会从失败里改进。

为什么后来的 VLA 取代了它?

因为 VLA 把「写代码」这个能力内化了。RT-2 / π0 直接用神经网络把「观察+指令」映射到动作,不需要中间那层 Python 解释器。优势是端到端可微、可以微调。但 CaP 的「可解释、可组合、免训练」优势,VLA 至今仍难完全替代——很多 robot-code 工作今天仍在用它做 long-horizon 规划。