推测性编程工具调用(12分钟阅读)

TLDR AI 工具

摘要

本文提出推测性编程工具调用(sPTC),一种优化AI框架中工具调用的技术。它受CPU和大语言模型中推测执行的启发,通过重叠执行与令牌生成来降低延迟。

推测性编程工具调用(sPTC)通过在令牌生成期间预启动工具调用来优化递归语言模型,从而减少高延迟工具和上下文生成带来的延迟。该方法类似于即时编译器,允许并行执行非阻塞工具调用,可提供1至1.2倍的运行时加速。sPTC在受内存限制的本地大语言模型和高吞吐量服务系统中尤为实用,它通过将计算与执行时间重叠,为框架(如RLMs)中复杂的程序执行带来显著的性能提升。
查看原文
查看缓存全文

缓存时间: 2026/08/25 15:47

# 推测性编程工具调用 来源:https://alexzhang13.github.io/blog/2026/spec-ptc/ 图1\. 推测性编程工具调用(sPTC)示例 模型端 — 流式输出 使用推测:解码执行预处理子调用 ×6判断执行基线答案 · 7/7声明 不使用推测 · 相同程序答案c1c2c3c4c5c6判断执行崩溃 — 2.4倍,无异步 时间t 相关代码见此处:https://github.com/alexzhang13/spec-ptc 从我之前的文章(https://alexzhang13.github.io/blog/2026/harness/)和对递归语言模型(RLMs)(https://arxiv.org/abs/2512.24601)的研究来看,这并不令人意外:我认为(1)REPL中的代码是系统唯一需要的“工具”;(2)所有其他工具都应该是该代码工具中的函数。当代码成为系统的主要动作空间时,你需要开始考虑将这些专用工具调用与生成和执行的代码进行重叠。 受CPU中的推测执行(https://en.wikipedia.org/wiki/Speculative_execution)和LLM中的推测解码(https://arxiv.org/abs/2211.17192)启发,我在运行时设计中提出的一个相对简单且有用的技巧是**推测性编程工具调用**(**sPTC**),即在运行时仍在生成令牌时,我们推测并预启动来自部分生成的REPL调用的工具调用,而不是等待它完成整个生成。特别是,当感兴趣的工具是子LLM或子代理调用时(例如在RLM中),这尤其有用。如果最终生成的REPL确实调用了这些工具,它们将立即返回推测调用中缓存的输出。 一个关键观察是,LLM工具(如子代理或搜索API)通常延迟较高,通常是依赖代码执行作为动作的运行时的瓶颈。此外,主上下文的实际生成也常常是一个显著的延迟瓶颈,阻碍了中间调用的发生。 在本文的其余部分,我将主要讨论相对于RLM的推理时间节省,但请注意,这通常适用于生成代码的运行时(即CodeAct类或“代码模式”运行时)。节省时间的两个明显领域是: 1. **在令牌流式传输过程中重叠已生成的工具调用。** 大多数运行时设计会等待模型生成整个生成过程完成后再执行工具。这种设计可能是JSON风格工具调用的后果,因为那里这不是真正的瓶颈。由于主上下文的生成通常很慢,这代表了可以削减的大量时间,尤其是对于*思考*时间很长的模型。 2. **作为REPL调用的JIT编译器。** 即使没有启用流式传输,一个明显的优化是许多REPL程序包含非阻塞的工具调用——例如,两个独立的子代理调用,代码没有编写为异步,但仍然可以并行运行。**sPTC**充当一个*非常原始*的JIT编译器来防止这种情况,并且可能在不同的语言和REPL设计中进一步改进。 字符串字面量输入的子代理、具有变量依赖关系的子代理,以及具有变量依赖关系的子代理循环被推测并与主上下文生成重叠的并排示例。 对于本地运行的LLM(一次运行一个或少量聊天实例),您的推理引擎通常在解码主上下文时高度受内存限制,而推测可以帮助增加算术强度。对于高吞吐量服务系统(例如,如果您使用前沿实验室或模型路由器API),批量请求被抽象到各种可能不相交的服务引擎中,因此增益纯粹来自主上下文的计算重叠,或与慢速REPL调用的执行时间重叠。 ## RLM的一些对比数据 我将首先突出一些基本的运行时数据,尽管您可能可以推断出运行时的收益。实际实现细节在代码库中相当简单且易于理解,我将在本文的后半部分讨论。 您应该记住,很难估计确切的速度提升,因为它高度依赖于工具的延迟、生成的令牌数量、服务引擎的负载以及运行时实际做出的选择。我们可以隔离一些组件(例如,固定程序)来突出那些不需要基准测试就能理解的明显情况,但最简单的例子就是复制一个已知的设置和运行时,并对多次样本的运行时间取平均值。 我们在原论文中的OOLONG(trec-coarse,132k)(https://arxiv.org/abs/2511.02817)和OOLONG-Pairs(32k)(https://huggingface.co/datasets/mit-oasys/oolong-pairs)上运行以下实验,使用配备8xH100 80GB的节点运行vLLM服务器。我们使用`Qwen3-30B-A3B-Instruct-0527`测试了两种情况,一种是标准温度0.7,另一种是温度0.0以控制这些RLM运行的方差,其速度高度依赖于它所采取的轨迹。每个实验运行5次。我们报告子调用和轮次的平均数量,以相对了解每个模型的操作情况,并报告在4个并发运行和同一服务引擎上8个并发运行的设置下的数据。 推测性PTC与基线RLM在OOLONG和OOLONG-Pairs上的对比RLM的速度提升通常在1-1.2倍左右。我们还测试了更“确定性”的LM程序套件,这些程序中我们可以观察到代码库中假设或实际的运行时速度提升,但在此省略,因为它们过于具体。 ## 设计推测性PTC方法 代码(https://github.com/alexzhang13/spec-ptc/)相当短且可读,但下面我会谈谈设计理念。 高层设计是让REPL中导入的工具调用有一个钩子,可以在需要时提前调用并替换为缓存输出。我们希望一个库和前端契约允许我们定义(1)哪些工具应该被推测,哪些不应该(例如,我们可能希望子LLM调用被推测,但子RLM调用成本太高);以及(2)一个机制,用于推测那些输入依赖于先前在REPL调用中计算的内存中变量的工具调用,而不仅仅是字面量。 从高层看,我们想要的契约类型是围绕我们希望被推测的函数(例如RLM中的子调用`llm_query()`)的钩子,以便我们可以在解析时异步运行它,将其保存到某个全局工具输出存储中,并将其视为一个承诺(promise),当它在REPL中实际被调用时从这个存储中获取。我们还需要区分多个相同工具调用的调用,特别是当它们是非确定性的(像子调用)时。 ``` @spec.tool(speculatable=True, pure=True) def tool(...) -> OutputType: ... # 推测版本 def tool_spec(...): promise = launch(tool(...)) register_speculation(promise) # 在真实REPL中运行的钩子版本 def tool_real(...): if exists(promise, ID(...)): return promise else: return tool(...) ``` 上面的契约让我们能够定义一个“影子”命名空间,在LLM输出被解析时调用这些修改后的工具调用,并将它们作为未来对象(futures)保存在某个存储中,供真实工具调用。这给了我们下面相对简单的逻辑: ``` real_ns = {**locals, real_tools} shadow_ns = replace_tools(real_ns) # 在LLM流式输出时推测 while not LLM.done: code += LLM.next_tokens() parse_and_peek(code, shadow_ns) # 排队但不执行代码 parse_and_speculate(code, shadow_ns) # 重新运行影子REPL # 真实工具现在路由到承诺工具 exec(code, real_ns) ``` **用于推测的影子执行。** 推测的简单情况是当您可以解析工具调用并从令牌中直接推断输入时,即当输入是字面量时。更棘手的情况是当工具调用嵌入在条件或循环逻辑中,或者输入依赖于REPL调用中先前计算的内存中的变量时。在前一种情况下,我们可能无法先验知道条件是否满足,或者循环运行多长时间。在后一种情况下,我们不确定计算变量输入是否*非纯*并且会修改某些外部状态,意味着我们无法提前计算。 解决这个问题有许多潜在方法,可能涉及一整条研究路线,但为了简单起见,我选择维护我们主代码REPL的一个深拷贝分支,我们将其标记为*影子REPL*,它即时执行部分REPL。为防止不必要的副作用,大多数外部库和函数如`open`被标记为“不安全”,任何使用这些函数且有输入依赖的可推测工具都不会被推测。 **注意**:我故意选择不使用部分“推测器”执行器作为真实REPL执行器,因为模型生成的整个REPL可能包含容易出错的代码或不完整的工具调用。在这些情况下,我们不希望推测器实际修改REPL状态,我们希望将整个REPL单元视为计算单元。 ## 什么可以被推测并“提前运行” 对于到底什么可以被推测、你想要多激进,以及运行部分代码的整体安全性,有一些考量。我们通常关心理解(1)我们是否有足够的信息来提前运行工具调用;(2)我们能否判断该工具调用是否会实际执行,特别是在条件语句周围。 在PTC中,我们可以通过工具调用的**输入**(如果是非确定性的,还包括出现次数)来唯一索引工具调用。在流式传输期间,如果每个工具调用的输入在没有代码执行的情况下是已知的(即所有输入都是字面量,如字符串字面量),我们可以立即异步调用此工具调用。更常见的情况是输入依赖于其他变量,其中一些我们可以*安全地*计算,而另一些则不能。对于相同的工具调用,例如在几个子代理上计算多数票,我们不希望单个推测工具调用路由到每个副本,因此我们需要跟踪相同工具调用的唯一实例,除非我们知道调用是确定性的。 我们可以看看以下几种情况,了解当前哪些会被*推测*,哪些不会,以提供一些直觉: **情况1:字面量。** 字符串、整数或其他字面量可以立即解析并转换为工具调用,即使没有逐行影子执行。 ``` title = llm_query("为《奥德赛》起一个标题") # 解析 blurb = llm_query("为《奥德赛》写一句简介") # 解析 print(title, blurb) ``` **情况2:输入依赖。** 当涉及输入依赖时,只要所有输入是*安全的*(即纯函数,无副作用),就可以进行推测。此外,依赖于被推测的依赖项的工具将等待依赖项先计算,然后即使LLM仍在流式传输也会执行。 ``` a = llm_query("分流:" + doc) # 解析然后执行 c = llm_query("摘要:" + str(a)) # 解析,等待a,然后执行 print(c) if len(doc) > 10_000: # 将会求值,如果安全则进行推测 extra = llm_query("同时给出大纲:" + doc) ``` **情况3:可窥探和不可窥探的依赖。** 在流式传输的任何步骤中,我们都有影子REPL的工作命名空间。当解析出一个完整的工具时,有些情况下即使依赖项是内存中的变量而不是字面量,我们也可以推测输入。 ``` def gist(t): return llm_query("一句话要点:" + t) # 不可窥探 parts = [gist(c) for c in chunks] side = llm_query("为以下内容起一个随机标题:", chunks[0]) # 可窥探 print(side, parts) ``` **情况4:被阻塞的推测调用。** 在尝试推测时,有一个允许列表的关键词和函数调用可用于计算输入的依赖项。我们还可以指定我们确实不想指定为纯函数的新工具。任何具有被阻塞的依赖项的可推测工具都不会被推测。 ``` a = llm_query("分流:" + doc) # 被推测 notes = open("/tmp/scratch.txt").read() # 被阻塞 b = llm_query("用笔记注释:" + notes) # 被阻塞 c = llm_query("摘要:" + a) # a完成后推测 ``` 还有更多情况,理想情况下我们会为此编写一个完整的伪编译器。我也怀疑不同的运行时设计会出现各种权衡,但到目前为止,这还没有被深入优化。 ### 推测性PTC的开销 与这个技巧的其他部分一样,推测性PTC的额外开销在某种程度上取决于具体的实现和推理引擎的设置。对于此实现,在运行时开销上,由于推测器廉价地解析并检查它是否相信可以对部分生成的REPL进行推测,因此开销可以忽略不计。在内存方面,我们创建了运行时代码REPL的深拷贝,这相对于实际REPL变量的分配内存来说通常是廉价的,因为我们往往没有太多大的可变对象。 最坏的情况是当工具的服务引擎因许多并发且可能额外的推测请求而堵塞,这可以通过推测的激进程度以及这些工具的排队方式来控制。 在LLM解码级别(即流式输出)的推测算法,对于较旧的工具调用设计已经有过一些探索,尽管不广泛。很可能的情况是,对于标准工具调用,这些技术不是那么有用,或者开销不值得边际延迟改进。 Conveyor(Xu等人,2024)(https://arxiv.org/abs/2406.00059)允许用户定义部分执行机会,例如一行代码,在解码期间进行解析。在推测性交互代理(Hooper等人,2026)(https://arxiv.org/abs/2605.13360)中,他们正式将Conveyor中提出的系统定义为*推测性工具调用*,主要通过将更现代模型的长思考链与调用的工具调用重叠来减少首令牌时间(TTFT)。异步函数调用(AsyncFC)(Feng等人,2026)(https://arxiv.org/abs/2605.15077)则认为工具调用通常以阻塞方式实现,并定义了一个基于未来对象的异步包装器的契约;然而,这有不能与原始运行时轨迹1:1对应的风险。 在sPTC的情况下,我之所以认为在更复杂的程序中为工具添加推测性更有用,是因为程序本身的运行时未知。对于标准工具调用,当LLM完成生成足够令牌以完全指定工具调用时,可能没有多少令牌需要生成。在PTC情况下,关于实际推测什么以及如何推测,有更多的考虑,因为代码执行使实际的工具调用模式显著复杂化,留下了更多的重叠空间。 ## 结束语 推测性编程工具调用是一个自然的技巧,源于编程工具调用本身比传统工具调用本身具有更大的重叠潜力。我想澄清的是,除了与运行时中根LLM的流式令牌生成重叠外,长远来看,真正的价值将来自于更聪明的JIT编译技巧,将PTC设置中的工具调用与实际的REPL执行本身重叠,随着运行时生成更复杂的程序,这可能会变得更加昂贵。 推测性PTC的实现方式可能有很多,但我发现这种相对简单的实现已经足够在RLM等设置中获得显著的速度提升。推测的本质是尽早启动,即使是在流式传输和部分代码执行期间,这与代码作为主要动作空间完美契合。随着工具调用模式变得更加复杂(特别是在代码执行中),推测的收益可能会继续增加。

相似文章

提升工具使用智能体速度的推测性宏提交

arXiv cs.AI

本文介绍了推测性宏提交(SMC),这是一个两层智能体系统,通过推测性地执行未来动作链并在匹配时提交它们来减少工具使用LLM智能体的延迟,从而实现比顺序执行更快的速度。

OoO-Spec:用于快速工具调用的无序语义推测

arXiv cs.CL

介绍了 OoO-Spec,一种通过小型辅助模型以无序方式计算语义槽位来加速 LLM 工具调用的方法,相比自回归解码实现了最高 5.34 倍的加速,并在多个目标和基准测试中超越了现有的草稿模型方法。

ProgAgent:通过程序化训练学习工具编排

Hugging Face Daily Papers

ProgAgent 提出一个完整的程序化工具调用(PTC)训练框架,通过可验证的长时程任务合成、轻量级 PTC 运行时和两阶段训练配方,显著优于同规模基线,在 LOCA-bench 64k token 状态下达 30.7% 通过率而基线为零。