REPL的实现以及Agent的REPL-Plan模式
REPL(读取-求值-打印循环)是一种交互式编程环境,它接收单个用户输入,执行这些输入,并立即返回结果。它充当对话式界面,用于快速原型设计、调试和实验,允许开发人员在不运行完整程序的情况下测试代码片段。常见的例子包括 Python shell、Node.js 和 IDE 控制台。
OpenAI Agents SDK中对REPL的实现:
REPL 实用工具
该 SDK 提供 run_demo_loop,可在终端中直接对智能体行为进行快速、交互式测试。
import asyncio
from agents import Agent, run_demo_loop
async def main() -> None:
agent = Agent(name="Assistant", instructions="You are a helpful assistant.")
await run_demo_loop(agent)
if __name__ == "__main__":
asyncio.run(main())
run_demo_loop 会在循环中提示输入用户输入,并在轮次之间保留对话历史。默认情况下,它会在模型生成输出的同时进行流式传输。运行上面的示例后,run_demo_loop 会启动一个交互式聊天会话。它会持续请求你的输入,在轮次之间记住完整的对话历史(因此你的智能体知道已经讨论过什么),并在生成回复的同时将智能体的响应实时流式传输给你。
要结束此聊天会话,只需输入 quit 或 exit(然后按回车),或使用键盘快捷键 Ctrl-D。
引申:REPL-Plan
研究问题
大型语言模型(LLM)在复杂、长时程(long-horizon)规划任务中表现不稳定,即使采用现有的“代码增强”(code-augmented)方法,仍然存在几个难以克服的根本性缺陷。具体来说,文章明确指出了当前代码增强规划方法的三大痛点:
- 模糊(fuzzy)子问题难以用纯代码可靠解决:很多真实任务包含大量非结构化、需要语言理解或主观判断的部分(如“挑一个最符合用户偏好的商品”“解读复杂的网页内容”),这些部分无法被精确地写成无歧义的函数,LLM一旦写错就全盘崩溃。
- 必须“自底向上”(bottom-up)规划的僵硬性:传统写代码的方式要求先把所有子过程、所有可能的case都预先定义好(先写函数 → 再写主流程 → 还要写各种异常处理),这对LLM来说几乎不可能一次写对,稍微复杂一点的任务就容易前功尽弃。
- 代码天然的“一次写入、难以动态纠错”特性:哪怕是人类程序员写代码也经常bug百出,LLM一次性生成整段代码的概率更是灾难级,一旦执行出错,整个计划就断了。
人类程序员真正写复杂程序时,从来不是“一口气写完整个脚本”,而是开一个REPL(交互式编程壳/Notebook),一句一句敲、立刻看到结果、错了马上改、需要递归调试函数时就再开一个REPL来试。
文章提出的REPL-Plan就是把这种“人类写原型/调试代码的真实工作流”直接搬给LLM用:
- 让LLM在真正的REPL环境里一行一行输出代码,执行后立刻能看到结果/报错,然后它自己接着改(动态纠错)
- 当遇到“模糊”或复杂子任务时,不是硬写函数,而是直接“spawn”一个子REPL,把子任务扔进去,让另一个(或同一个)LLM调用去解决,再把结果answer回来(顶向下+递归
- 变量、循环、条件判断、函数调用全部保留,所以表达力完全不打折扣(code-expressive)
相关研究
文章针对下面的方法做了详细的讨论,总结相关的优缺点
| 方法类别 | 代表性工作 | 优点 | 缺点 |
|---|---|---|---|
| 纯文本逐步推理 + Action (Think step by step / ReAct 类) |
ReAct ,Reflexion, ADaPT,Zero-shot CoT 等 |
实现简单、少样本/零样本有效 显著提升短时程常识推理 能交替产生推理轨迹和行动 |
长时程任务极易幻觉(hallucination) 短期决策错误频发 无法处理需要精确循环/变量/控制流的复杂逻辑 在ALFWorld无memory下仅53.7%,真实网页任务仅17.6% |
| 纯文本子任务分解/分治 (Text-based Decomposition) |
THREAD ← 最强基线 Tree of Thoughts |
支持top-down递归分解 高度动态,能根据环境反馈调整计划 在ALFWorld达到95.5%,简单WebShop也76.3% |
完全缺乏代码表达力(no code-expressivity) 遇到需要循环、变量、长观察解析的复杂逻辑时彻底崩溃 WebShop k=10仅21%,真实网页复杂任务0.0% |
| 纯代码生成 (Code-as-Planning) |
包含:(1)伪代码自结构,不执行;(2)写可执行代码直接运行 | 充分利用代码控制流(if/loop/function) 对需要精确逻辑的任务理论上表达力最强 |
三大致命缺陷: 1. “模糊”(fuzzy)子任务无法可靠编码(如主观判断、解析非结构观察) 2. 必须bottom-up规划(先写完所有函数和case),LLM几乎不可能一次写对 3. 代码bug极难动态修复,一次生成整段代码,出错就全盘崩溃 |
| 需要External Memory / Reflection的混合方法 | Reflexion, AdaPlanner, RAP, AutoGuide, LATS, TDAG 等 | 通过记忆/多轮反思在部分任务上有提升 在ALFWorld有memory时可达76-85% |
依赖外部记忆,计算成本高 去掉memory后性能大幅下降 本质上还是受限于纯文本或半代码方式,无法同时拥有“动态纠错+完整代码表达力” |
文章指出现有方法只能在“动态性”与“代码表达力”之间二选一,而REPL-Plan是第一个真正把两者都拿满的方案——既能像程序员写Notebook一样动态试错、递归spawn子REPL处理模糊任务,又保留完整的代码控制流,因此在所有长时程+复杂任务中全面碾压。
解决方案

REPL-Plan原理
REPL-Plan的核心思想是模仿人类开发者使用交互式编程环境(如Python的REPL或Jupyter Notebook)的方式来解决复杂问题。其原理建立在以下几个关键点上:
- 代码可表达性:与传统仅输出文本动作(如“点击链接”)的智能体不同,REPL-Plan允许LLM生成任意的代码语句(如变量赋值、循环、条件判断)。这使它能以更紧凑、更强大的方式管理状态和逻辑。
- 动态交互与状态管理:系统维护一个持续的“REPL”会话,其中包含了全局和局部变量状态。LLM每生成一行代码,系统会立即执行并返回结果(如打印输出、错误信息)。这使得LLM可以基于上一步的结果来决定下一步的代码,实现了真正的交互式规划。
- 递归任务分解与抽象:这是REPL-Plan最关键的创新。当LLM在代码中调用一个未定义的函数(如
check_requirements())时,系统不会报错,而是会动态生成一个子LLM-REPL来专门处理这个子任务。
如上图所示,这种机制实现了自上而下的任务分解。主REPL负责高层规划流程,而将模糊、复杂或可复用的子任务(如“解析物品”、“判断匹配度”)委托给子REPL。子任务完成后,通过 answer() 函数将结果返回给父REPL,这与函数调用的栈机制非常相似。
REPL-Plan的实现可以看作一个状态机,其核心实现逻辑和关键组件:
1. 关键数据结构:LLM-REPL函数
每个LLM-REPL可以被视为一个类(LLMREPLFunction)的实例,它包含:
task: 该REPL要解决的任务描述。history: 该REPL中所有已执行的代码及其输出的历史记录。locals: 一个字典,包含当前REPL的局部变量状态和预定义的原语函数。
2. 核心执行循环
系统的核心是一个循环,其简化版的数学描述可以表示为一系列的状态转换:设 𝑆𝑡=(𝐻𝑡,𝐿𝑡) 表示在时间步 𝑡 的系统状态,其中:𝐻𝑡 是到时间 𝑡 为止的代码和执行历史。𝐿𝑡 是当前的局部变量环境。系统的演进过程如下:
- 代码生成:LLM基于当前任务 task 和历史 𝐻𝑡 生成下一段代码 𝑐𝑡+1。 𝑐𝑡+1=LLM(task,𝐻𝑡)
- 代码执行与状态更新:系统尝试在环境 𝐿𝑡 中执行代码 𝑐𝑡+1。
- 情况A:成功执行:代码正确运行,产生新的变量状态 𝐿𝑡+1 和输出 𝑜𝑡+1。 (𝐿𝑡+1,𝑜𝑡+1)=Execute(𝑐𝑡+1,𝐿𝑡) 历史被更新:𝐻𝑡+1=𝐻𝑡∪{(𝑐𝑡+1,𝑜𝑡+1)}
- 情况B:遇到未定义函数(NameError):这是递归分解的触发点。假设未定义的函数名为 𝑓。
- 系统会暂停当前执行,并创建一个新的子REPL状态 𝑆sub0。
- 系统查询LLM以获得子任务 tasksub 的描述:tasksub=LLM(𝑓,𝐻𝑡)
- 然后,系统开始在子状态 𝑆sub 上运行独立的REPL循环,直到子REPL调用
answer(a)。此时,返回值 𝑎 被传递给父REPL,并用于定义函数 𝑓。 - 父REPL从中断处继续执行,现在 𝑓 已被定义为一个能返回 𝑎 的函数。
- 循环:重复步骤1和2,直到主任务完成。
3. 预定义原语函数
这些是注入到每个LLM-REPL环境中的特殊函数,是实现与环境交互和递归分解的基础:
act(a): 在环境中执行动作 𝑎,并获取新的观察。这是规划与环境的接口。get_obs(): 返回当前的环境观察(通常是一个长字符串)。args: 触发子LLM-REPL的生成。在底层,这通过触发一个NameError并按照上述情况B处理来实现。get_args(): 在子REPL中调用,用于接收父REPL传递过来的参数。answer(a): 在子REPL中调用,将结果 𝑎 返回给父REPL,并终止子REPL的执行。

伪代码
4.总结
REPL-Plan的原理并非依赖于某个神秘的数学突破,而是源于一个强大且实用的工程框架。它将大语言模型的代码生成能力与交互式编程范式的强大功能(如即时反馈、状态管理和递归抽象)巧妙地结合在一起。其“数学精髓”体现在递归算法、状态机模型以及与LLM的条件概率交互中,从而实现了对复杂、长视野规划任务的动态、稳健且可表达的解码。
实验
这篇论文通过在三个具有挑战性的语言交互环境中进行系统实验,全面评估了REPL-Plan的性能。实验旨在测试智能体在长视野、需推理、需处理模糊信息的任务中的能力。选用的环境和基线对比如下:
| 实验环境 | 任务描述 | 核心挑战 | 对比的基线方法 |
|---|---|---|---|
| ALFWorld | 文本模拟家庭环境中的任务(如:在厨房找到苹果并加热) | 具身推理、长序列动作规划 | ReAct, Reflexion, AdaPlanner, RAP, AutoGuide, THREAD |
| WebShop (k=3) | 在模拟电商网站根据自然语言描述搜索并购买商品 | 理解网页语义、比较商品属性 | 同上,包括 THREAD |
| WebShop (k=10) | 同上,但每页显示10个商品,策略更复杂(如:搜索两次,从Top-20中选最佳) | 处理更多选项、需要循环和复杂逻辑 | THREAD, REPL-Plan |
| 真实世界网页任务 |
如果这篇文章帮助到了你,你可以请作者喝一杯咖啡

浙公网安备 33010602011771号