边生成边执行:在 LLM 代码生成中隐藏执行延迟 Executing as You Generate: Hiding Execution Latency in LLM Code Generation
提出并行执行范式与 EAGER 框架,将 LLM 代码生成与执行流水线化以降低端到端延迟。
前置知识
自回归解码 (Autoregressive Decoding)
LLM 在生成代码时采用自回归方式,即从左到右逐个生成 token,每个新 token 的条件概率基于此前已生成的所有 token。这种解码方式意味着模型一旦输出某个 token,就不会回头修改它,输出流中每个前缀都是最终结果的一部分。
本文的核心观察依赖于自回归的「不可回写」特性:如果模型不会改写已生成的 token,那么每个完成的语句前缀就是最终程序的一部分,可以放心提前执行。
代码解释器 (Code Interpreter)
现代 LLM 系统(如 Claude Code Execution Tool、OpenAI Code Interpreter、CodeAct、Open Interpreter)会先让模型生成可执行脚本,再由 Python 解释器运行并把结果返回给用户或模型。脚本通常用于数据分析、可视化、计算等任务,是「代码即行动」(code-as-action) 范式的核心组件。
本文要优化的就是这种「生成—执行」工作流中存在的串行等待开销。
抽象语法树 (AST) 与 Python 顶层语句
Python 源码可以被解析为抽象语法树,AST 中的每个顶层节点对应一条完整的可执行语句(赋值、循环、函数调用等)。Python 解析器支持以 `incomplete_input=True` 模式在语句不完整时继续解析,这种增量解析能力是 AST 切分器的实现基础。
EAGER 的核心机制是用 AST 边界把 token 流切成可执行 chunk,必须理解 AST 节点与语句的对应关系才能看懂 chunker 的工作方式。
生产者-消费者流水线 (Producer-Consumer Pipeline)
经典的并发编程模式:生产者持续产出数据项放入缓冲区,消费者从缓冲区取出并处理。整体吞吐量由最慢的阶段决定,但如果用队列解耦并允许缓冲积压,可以让各阶段并行执行从而提高吞吐和降低延迟。
EAGER 把 LLM 视作 token 生产者、把执行器视作消费者,本文的延迟分析直接建立在三阶段流水线的临界路径模型之上。
Jupyter 风格的 REPL 会话
REPL (Read-Eval-Print Loop) 会话会维持一个持久的执行环境,所有变量绑定、import 语句、函数/类定义在多次调用之间都保留。Jupyter notebook 的 cell 机制就是这种模式,允许跨 cell 共享状态。
EAGER 的执行器在持久 REPL 会话中运行 chunk 序列,状态在 chunk 之间被保留,这与「单次解释器调用」的串行执行有本质区别。
研究动机
当前主流 LLM 代码解释器系统(Claude Code Execution Tool、OpenAI Code Interpreter、CodeAct、Open Interpreter)都采用「先生成、后执行」的串行工作流:模型先写完整个 Python 脚本,解释器再一次性运行它返回结果。这个流程造成两个明显的空闲窗口——生成期间解释器无事可做、执行期间模型停转等结果。在以 LLM 为核心的代理场景下,用户实际等待时间近似为 $T_{serial} \approx T_{FT} + L/v_{gen} + T_{setup}^{(full)} + T_{exe}^{(full)}$,即首 token 延迟、生成时间、一次性执行环境启动开销、完整脚本执行时间之和。已有增量执行方法(Open Interpreter 的 Jupyter cell 模式、EG-CFG 的逐行执行反馈)虽然把执行结果反馈给模型以改进代码质量,但本质仍是串行的:模型每生成几行就停下来等执行结果,总墙上时间仍包含全部生成加执行。本文把这种不必要的串行等待视为可被系统级调度消除的开销。
本文的目标是本文目标是形式化并实现「并行执行」范式——让代码执行的开始时间不晚于其对应 token 全部生成完成的时间,从而把执行时间尽可能隐藏在生成时间背后。具体量化目标包括:(1) 推导出并行执行相对于串行执行的延迟上界和下界,刻画速度提升 $S = T_{serial}/T_{parallel}$ 的理论极限;(2) 给出并行方案不劣于串行的判定条件;(3) 在真实 LLM 与多种执行环境下实证非重叠执行时间 (NEL) 与端到端延迟 (E2EL) 的节省幅度;(4) 探索早期错误中断对后续代码修复成功率的影响。
与已有工作不同的是,此前研究的关注点是「如何利用执行反馈改进代码正确性」——执行结果用于 steer 生成、rerank、self-debug;本文把执行反馈的角色反转为「如何让执行与生成时间重叠以缩短用户等待」。这是互补的视角:现有方法不影响生成时间而仅修改生成内容,本文不修改生成内容而仅修改代码何时运行。作者抓住的关键观察是:与人类开发者不同,LLM 从左到右单调输出 token,永不回头修改已写内容,因此每个语句前缀天然就是「最终程序」的一部分,没有理由等到全部写完再跑——这一观察在 LLM-based 代码生成的语境下是显然的,但之前没有人把它系统化地工程实现。
核心方法
EAGER 的核心直觉可以用一个比喻说明:把 LLM 想成一个「说话的同时就动手做的助手」——你说出第一句话(第一个完整语句),他立刻去执行;同时你继续说第二句,等你说完时他可能已经把你前面的话都做完了。技术上,这是一条三阶段流水线:LLM 作为 producer 自回归吐出 token,chunker 作为 detector 持续解析 token 流识别出可执行 chunk(完整的 Python 顶层语句),executor 作为 consumer 在持久 REPL 会话中立即执行这些 chunk。流水线用 queue 解耦三阶段,chunker 把检测到的 chunk 送入 pending queue,executor 用动态批处理把积压的 chunk 合并成一次调用以摊薄单次启动开销 $T_{setup}$。为了让这种重叠对安全,EAGER 维护一份静态 gating policy:把「外部副作用」、「时序敏感」、「低产出」、「动态执行入口」四类语句直接降级回串行路径执行,避免提前执行破坏语义。流水线还附带可选的「早期错误中断」机制:某个 chunk 抛出运行时错误时立刻 kill LLM 生成,把错误与已生成的部分代码返回给上游,让模型从失败点重新生成修复版本。
本文和已有方法最本质的区别在于对「生成—执行」耦合关系的处理视角。Open Interpreter/EG-CFG 等增量执行方法以「执行反馈驱动更好的代码」为目标,模型每生成几行就停下来等执行结果,相当于把流水线人工降级为同步;这种做法在提升代码正确性上有效,但代价是墙上时间等于「生成+执行」的全部。EAGER 的关键洞察是:自回归 LLM 输出的 token 流中每个前缀都已最终化,语句边界一旦确认就可以并发执行;执行结果不会被用来 steer 当前生成(已生成的 token 不可改),仅在 chunk 失败时终止生成以节省后续无效生成。这种「并行的、安全的、可中断的」设计完全不影响生成结果,仅通过调度把执行时间藏到生成时间后面。理论上的非劣性条件 $T_{setup}^{(full)} \geq \bar{\delta} + N \cdot T_{setup}$ 也明确给出:当累计每 chunk 开销不超过串行一次性启动开销时,并行方案永远不慢于串行。
方法步骤详情
EAGER 的运行分四个阶段:(1) **Producer(LLM 流式生成)**:通过 OpenRouter API 以 streaming 模式拉取 token,每个 token 到达后追加到代码缓冲区。生成速度记为 $v_{gen}$ tokens/s,首 token 延迟记为 $T_{FT}$。(2) **Chunker(AST 切分 + 预测性消歧)**:每收到新 token,chunker 尝试用 `ast.parse(buffer, mode='exec')` 把缓冲区解析成 AST。解析成功后判断顶层语句是否真正完整——大多数情况下语法完整即语义完整(例如 `print("hi")\n` 之后 newline 出现即说明该语句已结束),可直接切出 chunk;但函数定义等场景中,第一行函数体之后解析器已能给出合法 AST,下一行却可能继续填充该函数体,此时 chunker 启用「lookahead」策略:等下一个 token 到来后再决定当前语句是否真的闭合(如果下一个 token 开始新顶层语句或 dedent,则确认闭合)。(3) **Executor(持久 REPL + 动态批处理 + gating)**:chunk 进入 pending queue,executor 空闲时把当前所有 pending chunk 合并为一个 batch 提交给持久 Python REPL 子进程(Jupyter 内核或本地子进程),跨 chunk 保留 import、变量、函数定义。提交前过一遍静态 gating 检查:黑名单命中(os/subprocess/shutil/socket/multiprocessing、time/signal/threading、eval/exec/compile/__import__、纯定义类语句)则降级为串行执行;否则按并行路径提交。每个 chunk 的执行附加约 1 ms 的 per-call 开销 $T_{setup}$。(4) **Error 路径(早期中断,可选)**:chunk 抛 RuntimeError 时 executor 立刻调用 LLM 端点的 abort/stream-close 接口终止生成,把错误信息连同截至失败点的代码一并返回给调用方;调用方把错误附加到代码尾部触发修复循环。
技术新颖性
本文在概念、理论、工程三层都有明确新颖性。**概念层面**,首次把「生成—执行」的串行假设显式化并提出并行范式,与已有「执行反馈改进代码」路线正交。**理论层面**,给出三阶段流水线的闭式延迟公式 $T_{parallel} = \max_{1 \le i \le N}\left[ T_{FT} + \frac{\sum_{j=1}^{i} l_j}{v_{gen}} + \delta_i + \sum_{j=i}^{N}(T_{setup} + T_{exe,j})\right]$,并由此推导出非劣性条件 $T_{setup}^{(full)} \geq \bar{\delta} + N \cdot T_{setup}$、速度提升上界 $S \lesssim 1 + (T_{setup}^{(full)} + T_{exe}^{(full)})/(T_{FT} + L/v_{gen})$,以及在均匀 chunk 假设下三种 regime(R1 生成主导、R2 执行主导、R3 平衡)的临界 chunk 数 $N^* = (L/v_{gen} - T_{exe}^{(full)})/T_{setup}$。**工程层面**,AST 切分 + lookahead 消歧是新颖的:之前没有工作研究过在 LLM token 流上做边界无歧义、可证明保真的语句切分;gating policy 用静态 denylist 覆盖副作用/时序/动态/低产出四类语句,弥补了并行执行可能改变语义的隐患;早期错误中断与修复的耦合则是从工程中「意外发现」被形式化为可选特性,并在 RQ3 中被验证。
实验结果
**RQ1(模拟场景)**:Table I 报告了在 4 个 benchmark × 3 个环境 × 4 个 TPS 档位下的延迟节省。所有配置下非重叠执行时间 (NEL) 节省平均达到 89.8%,意味着绝大部分执行被生成隐藏;端到端 (E2EL) 节省与生成速度呈正相关,20 TPS 时仅 1–6%(生成占主导),200 TPS 时最高 34.9%(PandasPlotBench on Docker)。E2EL 随 TPS 上升而增大的趋势与理论 $S \lesssim 1 + (T_{setup}^{(full)} + T_{exe}^{(full)})/(T_{FT} + L/v_{gen})$ 吻合:分母中的 $L/v_{gen}$ 随 TPS 上升而缩小,使执行占比放大、节省更显著。**RQ2(真实 LLM)**:Table II 在 7 个 LLM × 4 个 benchmark 上验证模拟结论可迁移。无错误执行下,DeepSeek-V3.2/GPT-4o-mini 在 DABench 上 NEL 降至 1–2 ms(接近完全隐藏),Gemini-3.1-Flash-Lite 在 PandasPlotBench 上 E2EL 从 1440 ms 降至 903 ms(37.3% 节省);E2EL 节省最高 37.3% 与摘要数字一致。有错误执行下,早期中断让 NEL 多为 0 ms、E2EL 节省最高达 75.0%(Qwen3-Coder 在 DSBench 上从 13163 ms 降到 3289 ms),DeepSeek-R 在 PandasPlotBench 上 55.3% 节省。**RQ3(修复质量)**:Table III 比较从完整代码(Full)vs 截断代码(Partial)修复的成功率。三个数据类 benchmark(DABench/DSBench/PandasPlotBench)上一致偏向 Partial,幅度从 +2.1 到 +44.3 pp,Qwen3-Coder 与 Gemini-3.1 在 DABench 上 +32 pp,GPT-4o-mini 在 DSBench 上 +44.3 pp。例外是 GitChameleon:Partial 略输于 Full,原因是其任务是版本特定 API 完成,错误之后通常紧跟目标 API 用法,截断这段反而丢失关键上下文。**chunk 保真度**:在 Docker 环境下用所有 7 个 LLM 在 4 个 benchmark 上生成的程序做 chunk 重组,所有重组结果在字符级别与原始完全一致,证明 AST 切分不会丢/重/乱 token。
| 任务 | 指标 | 本文 | 基线 | 提升 |
|---|---|---|---|---|
| DABench 模拟(Docker 环境,20 TPS) | NEL 节省 / E2EL 节省 | 94.3% / 2.1% | 0% / 0%(串行) | NEL -94.3pp;E2EL -2.1pp |
| DABench 模拟(Docker 环境,200 TPS) | NEL 节省 / E2EL 节省 | 93.9% / 17.4% | 0% / 0% | NEL -93.9pp;E2EL -17.4pp |
| PandasPlotBench 模拟(Docker,200 TPS) | NEL 节省 / E2EL 节省 | 83.4% / 34.9% | 0% / 0% | 最高 E2EL 节省 34.9% |
| DABench 真实 LLM(DeepSeek-V3.2 on Docker) | NEL / E2EL(无错误) | 2 ms / 7879 ms | 590 ms / 8467 ms | NEL -99.7%,E2EL -6.9% |
| PandasPlotBench 真实 LLM(Gemini-3.1-Flash-Lite on Docker) | NEL / E2EL(无错误) | 215 ms / 903 ms | 753 ms / 1440 ms | E2EL -37.3%(无错误最大节省) |
| DSBench 真实 LLM(Qwen3-Coder on Docker,有错误) | NEL / E2EL(错误路径) | 342 ms / 3289 ms | 675 ms / 13163 ms | E2EL -75.0%(错误路径最大节省) |
| PandasPlotBench 修复(DeepSeek-R,Partial vs Full) | 错误解决率 | 94.7%(Partial) | 73.7%(Full) | +21.1 pp |
| DSBench 修复(GPT-4o-mini,Partial vs Full) | 错误解决率 | 76.6%(Partial) | 32.3%(Full) | +44.3 pp(全文最高) |
局限与改进
**作者承认的局限**:(1) 语言通用性——实现和评测只针对 Python,作者认为扩展到 JS/R 等解释型语言容易但编译型语言需要不同的 backend(增量编译/JIT),留作未来工作;(2) 任务范围——只针对单文件脚本,仓库级 agent(编译工程、跑测试集)落在执行主导 regime,节省有限,论文明确不针对该场景;(3) Docker 冷启动——990 个任务中有 46 个因虚拟环境冷启动导致执行时间异常高,被排除,剔除前后聚合差异 <1 pp;(4) GitChameleon 上早期中断反而有害,截断可能丢掉版本特定 API 上下文。**我自己的观察**:(1) gating policy 是「保守的 denylist」——任何无法静态归类为「位置不敏感」的语句都走串行,这意味着范围广泛的标准库 import(如 `os.path` 这种纯函数模块)会被误判为外部副作用,实际无副作用但仍走串行,影响理论最优 NEL;(2) 早期中断触发条件是「RuntimeError」——`AssertionError` 之外的非致命异常(如 `Warning`)可能错过中断机会,或者相反把语义异常但实际可恢复的错误强制中断;(3) 实验在 2 CPU 受限的容器里跑,对真实云端高并发部署下容器调度抖动的影响没有评估;(4) Token-Per-Second 是模拟而非真实 LLM 调速——真实 LLM 在不同 prompt 长度、不同 batch 下速度波动大,模拟结果可能略乐观。
独立分析的弱点
**独立分析的弱点**:(1) **gating 过于保守**——当前 denylist 是固定 denylist 加上「外部副作用语句降级串行」的两条规则,覆盖范围粗;改进方向是引入更细粒度的 AST-level effect analysis,例如追踪 `os.remove(path)` 的 `path` 是否受后续代码影响,若无依赖则仍可安全并行;同时允许 deployer 通过配置注入自定义 allow/deny 项。(2) **早期中断的语义保留**——`sys.exit()` 这类用户主动抛出的退出不会被识别为错误 chunk,但同时它会终结 REPL 会话,后续 chunk 无法在同一会话执行;改进方向是在 chunk 执行前静态扫描 `exit/quit/sys.exit` 关键字,把这种「终结性」chunk 标记为串行 + 后续 chunk 串行。(3) **chunk 大小分布不均**——LLM 生成的代码天然按语义块分布(一行 import、一段循环、一个完整函数),当前 AST 切分并不感知 chunk 大小分布,可能出现 N 很大但 $T_{exe,i}$ 都很小的退化情况,触发 $N \cdot T_{setup}$ 主导 regime;改进方向是根据历史 chunk 长度自适应合并小 chunk,或引入 token-budget-based 的切分。(4) **真实 LLM TPS 不可控**——Table I 是用「mock token stream at fixed TPS」模拟的,但真实 LLM 速度随 prompt 长度、温度、batch 变化;改进方向是把 20/50/100/200 TPS 替换为对真实 7 个 LLM 多次运行测得的速度分布,并在 RQ1/RQ2 中报告「在 P50 速度下 E2EL 节省多少」而非单一 TPS 数字。(5) **错误 chunk 的检测时延**——在 EAGER 中一个 chunk 必须等执行完成才能报错,但 `numpy` 这类大 import + 第一次矩阵运算的 chunk 可能耗时数百毫秒;改进方向是引入「first-failure-fast」机制,对高风险 chunk(涉及未 import 的标识符或重 I/O)在执行前先做轻量 dry-run 检查。
未来方向
**作者提出的**:(1) 扩展到编译型语言(增量编译 / JIT backend)以及 JS、R 等其他解释型语言;(2) 仓库级 agent 场景的扩展,本文 R2 regime 表明执行主导下节省有限但仍有理论空间。**基于结果可延伸的**:(1) **流式编程语言设计**——论文 §VIII 提到一个有意思的方向:让编程语言本身把「可流式执行」作为一等设计目标,例如显式的语句分隔符消除 chunk 边界歧义,或提供语言级 primitive 让运行时增量消费生成的代码。这对 AI 辅助编程生态有结构性影响。(2) **chunk-aware 的解码策略**——既然知道哪些语句是「低产出」的定义类,可以让 LLM 在生成这些 chunk 时降低采样温度以减小 $N$、摊薄 $N \cdot T_{setup}$;反过来对「高执行开销」chunk 加快生成让流水线更接近 R3 balanced regime。(3) **与其他执行反馈方法正交组合**——EAGER 只做调度不动语义,可以与 EG-CFG(执行反馈 steer 生成)、Self-Debugging(迭代修复)、Chain of Code(选择性 emulation)等代码质量方法叠加;下一步工作应系统化测量这种叠加的复合收益。(4) **跨会话的 chunk 缓存**——很多 LLM 调用生成的 import 块、辅助函数定义是重复的,可在 chunk 级别做 hash 缓存,跳过重复执行。(5) **在 agent loop 中的 per-turn 重叠**——多轮 agent 对话中每轮都要生成 + 执行,EAGER 的并行机制能否在多轮之间流水线化(例如第二轮的生成与第一轮的执行重叠)是值得探索的方向。
复现评估
**开源情况**:作者把 artifacts 放在 https://doi.org/10.6084/m9.figshare.31869469,包含 EAGER 框架代码、实验配置、chunking 验证脚本与 4 个 benchmark 的处理脚本。**数据集**:4 个公开 benchmark 均可独立获取(DSBench arXiv:2409.07703、DABench ICML'24、PandasPlotBench arXiv:2412.02764、GitChameleon arXiv:2507.12367),作者使用了 gold solution 或 DeepSeek-V3.2 生成的 runnable solution 作为 mock token stream。**算力需求**:实验在 Intel Xeon Platinum 8352V 单机 + 2 CPU 容器化环境上完成,无 GPU 训练需求;LLM 全部通过 OpenRouter API 调用(streaming mode),需要相应 API 配额;7 个 LLM 涵盖开源(DeepSeek-V3.2、MiMo-V2-Flash、Qwen3-Coder、DeepSeek-Reasoner)与商用(GPT-4o-mini、GPT-5.1-Codex-Mini、Gemini-3.1-Flash-Lite)模型。**复现难度**:中等偏低——核心算法(AST 切分 + 批处理 + gating + 中断)的实现约几百行 Python,理论部分只需照公式实现;难点在 (a) 与各 LLM 端点的 streaming + abort 协议对接(不同供应商实现差异大),(b) Docker 容器化与 CPU pinning 的精确时序测量。建议复现顺序:先复现 §V-A 的 chunk 重组保真度(无需 LLM),再复现 RQ1 的 mock token stream 实验(无需真实 LLM),最后才进入 RQ2/RQ3 的真实 LLM 实验。整体而言,有系统编程背景的工程师约 1–2 周可完整复现核心结果。
论文图表
图示对比串行与并行两种执行模式。串行模式下 LLM 完成全部生成($T_{gen}$)后才开始执行,executor 此前一直空闲;并行模式下 chunk 1、2、3 的执行与后续 token 生成时间重叠,只有最后一个 chunk($T_{parallel}$)落在生成窗口之外。
这是论文最核心的直觉图,一图讲清「为什么并行能省时间」以及 $T_{serial} \approx T_{gen} + T_{exec}$ vs $T_{parallel} \approx T_{gen} + T_{tail}$ 的关系;任何读论文的人必须先看懂这张图才能理解后续公式。
在 4 benchmark × 3 环境 × 4 TPS(20/50/100/200)共 48 个配置下报告 NEL 与 E2EL 节省百分比。NEL 全部配置平均 89.8%,E2EL 节省从 1.1%(DSBench 20 TPS)到 34.9%(PandasPlotBench 200 TPS)不等。
这是回答 RQ1 的核心数据表,量化了 EAGER 在不同生成速度与执行环境下的鲁棒性与节省趋势;E2EL 节省随 TPS 上升而放大的模式直接验证了 §II-B 的理论预测。
在 7 个 LLM × 4 个 benchmark × Docker 环境下分「无错误」与「有错误」两栏报告 Baseline 与 EAGER 的 NEL、E2EL。无错误下 NEL 多降至接近 0、E2EL 最高降 37.3%;有错误下 E2EL 最高降 75.0%。
这是回答 RQ2 的核心数据表,证明模拟场景的节省在真实 LLM 输出上同样成立;并且把「无错误路径」与「早期中断路径」的节省分开报告,揭示了中断机制在错误场景下的额外价值。
在 7 个 LLM × 4 benchmark 上对比 Full(修复时附完整代码 + 错误)和 Partial(修复时附截断到错误点的代码 + 错误)两种修复条件下的错误解决率。数据类 benchmark 上 Partial 一致优于 Full,幅度 +2.1 到 +44.3 pp;GitChameleon 上相反。
这是回答 RQ3 的核心数据表,也是论文最反直觉的发现——给模型「更少」上下文反而提升修复质量;揭示了「完整但失败的代码」对模型产生 anchor 效应这一此前未被讨论的现象。