记住,不要重读:用于令牌高效自主实验的有状态ReAct智能体

arXiv cs.LG 论文

摘要

本文提出使用LangGraph中的有状态ReAct智能体取代无状态自动研究模式,将每次迭代的令牌成本从O(n)降低至O(1),在超参数调优和代码优化基准测试中实现了52-90%的令牌减少。

arXiv:2606.14945v1 公告类型:新 摘要:自动研究模式通过让大语言模型(LLM)迭代修改代码来优化目标指标,从而实现自主实验。然而,其无状态设计每次迭代都从头重建实验上下文,导致每次迭代的令牌成本为O(n),总成本为O(n²)。本研究将该模式重新构思为使用LangGraph构建的有状态ReAct智能体,其中类型化的持久状态通过工具调用接口在迭代之间传递实验历史。评估了两个基准测试:超参数调优(15次迭代,每次迭代观察数据较小)和代码性能优化(40次迭代,每次迭代观察数据较大,包含完整源代码和基准测试结果)。在超参数调优上,有状态智能体消耗的令牌减少了90%(2,492对比24,465)。在代码优化上,有状态智能体消耗的令牌减少了52%(627K对比1,275K),同时两项任务的优化质量相当。令牌的减少是结构性的:无状态智能体每次迭代以O(n)成本重新读取全部历史,而有状态智能体在固定大小的对话窗口内以O(1)成本运行。本文详细描述了该架构,足够使从业者为其自身工作流程实现一个状态化的自动研究智能体。
查看原文
查看缓存全文

缓存时间: 2026/06/16 11:36

# 记住,不要重读:面向令牌高效自主实验的有状态ReAct智能体
来源:https://arxiv.org/html/2606.14945

###### 摘要

自主研究模式通过让大语言模型(LLM)迭代修改代码以优化目标指标,实现了自主实验。然而,其无状态设计导致每次迭代都需要从头重建实验上下文,造成每次迭代的O(n)令牌成本以及总计O(n²)的成本。本文将此模式重构为使用LangGraph的有状态ReAct智能体,其中类型化的持久化状态通过工具调用接口跨迭代携带实验历史。我们对两个基准任务进行了评估:超参数调优(15次迭代,每次迭代观测数据量小)和代码性能优化(40次迭代,每次迭代观测数据量大,包含完整源代码和基准测试结果)。在超参数调优任务上,有状态智能体消耗的令牌减少了90%(2,492 vs. 24,465)。在代码优化任务上,有状态智能体消耗的令牌减少了52%(627K vs. 1,275K),同时两项任务的优化质量相当。令牌的减少是结构性的:无状态智能体每次迭代以O(n)成本重读完整历史,而有状态智能体在固定大小的对话窗口内以O(1)成本运行。本文以足够的细节描述了该架构,使实践者能够为自己的工作流实现有状态自主研究智能体。

## 1 引言

机器学习研究本质上是迭代的:研究者提出假设,实现代码更改,运行实验,分析结果,并决定下一步尝试什么。这个循环在收敛之前会重复数十次到数百次。虽然单个实验的计算成本已经降低,但实验之间的人类推理仍然是主要瓶颈。

Karpathy[1 (https://arxiv.org/html/2606.14945#bib.bib1)] 展示了LLM可以自主地闭合这个循环。*自主研究(autoresearch)*模式将实验分解为三个文件:固定的数据流水线 (prepare.py)、可变的训练脚本 (train.py) 和自然语言的研究指令 (program.md)。LLM读取指令,修改训练脚本,执行实验,观察指标,然后重复。Karpathy在其最初的演示中,这个系统在两天内运行了大约700次实验,并在无需人工干预的情况下发现了20个独立的训练优化方法。

这种模式的优雅之处在于其简单性:LLM与机器学习系统的唯一接口是代码修改,唯一的反馈信号是目标指标。然而,这种简单性引入了一个结构性低效。每次LLM调用都是*无状态*的——智能体必须在每次迭代时通过重读累积在program.md文件中的完整结果历史来重建实验上下文。随着实验次数的增长,提示词呈线性增长,消耗令牌来重新传输智能体已经处理过的信息。这不是一个提示工程问题;Huang等人[10 (https://arxiv.org/html/2606.14945#bib.bib10)] 表明LLM无法在交互之间内部维护瞬态状态,这使得外部状态管理成为架构上的必需。

本文中,自主研究模式被重构为使用LangGraph的**有状态ReAct智能体**[2 (https://arxiv.org/html/2606.14945#bib.bib2)]。该智能体通过工具调用而非单一巨大的提示词与机器学习系统交互,并且实验历史、策略推理和收敛跟踪在类型化的状态图中跨迭代持久化。关键的结构性优势在于每次迭代的令牌成本从O(n)降至O(1),从而无需截断或总结提示词即可实现任意长的实验序列。

## 2 相关工作

### 2.1 面向机器学习研究的LLM智能体

自主研究模式属于一个不断增长的自主式AI系统家族,这些系统优先考虑易于实现和灵活性,而非可证明的最优搜索。与操作预定义参数化搜索空间的经典AutoML不同,LLM引导的实验将搜索空间本身视为可变的——智能体可以修改任意代码,而非从固定配置集中选择。

有几个系统探索了这一空间。MLAgentBench[6 (https://arxiv.org/html/2606.14945#bib.bib6)] 提供了一个用于评估机器学习智能体的13任务基准;AI Scientist[4 (https://arxiv.org/html/2606.14945#bib.bib4),5 (https://arxiv.org/html/2606.14945#bib.bib5)] 将范围扩展到完整的论文生成;AIDE[7 (https://arxiv.org/html/2606.14945#bib.bib7)] 将机器学习工程框架化为代码变体上的树搜索;AgentHPO[8 (https://arxiv.org/html/2606.14945#bib.bib8)] 将LLM智能体应用于超参数优化,在12个任务上达到了人类水平的表现;Agent Laboratory[9 (https://arxiv.org/html/2606.14945#bib.bib9)] 展示了一个多智能体流水线,成本降低了84%。自主研究模式[1 (https://arxiv.org/html/2606.14945#bib.bib1)] 采用了最小化的方法——单个LLM迭代编辑训练脚本——但其无状态设计限制了在较长实验序列上的效率。本文的工作保留了这种最小化方法,同时增加了持久化状态。

### 2.2 LLM智能体中的推理与记忆

ReAct[2 (https://arxiv.org/html/2606.14945#bib.bib2)] 将推理轨迹与工具操作交错进行。Reflexion[3 (https://arxiv.org/html/2606.14945#bib.bib3)] 通过显式的自我反思和情景记忆扩展了这一点。Huang等人[10 (https://arxiv.org/html/2606.14945#bib.bib10)] 表明LLM无法在交互之间维护瞬态状态,这促使了外部记忆的使用。MemGPT[11 (https://arxiv.org/html/2606.14945#bib.bib11)] 通过受操作系统启发的记忆层次结构解决了这个问题;Voyager[12 (https://arxiv.org/html/2606.14945#bib.bib12)] 为具身智能体构建了技能库;CoALA[13 (https://arxiv.org/html/2606.14945#bib.bib13)] 提供了智能体记忆类型的分类法。本文的工作为迭代实验实例化了这些原则:图状态作为工作记忆,实验历史作为情景记忆,领域约束作为语义记忆,通过类型化的状态转换而非LLM驱动的分页进行管理。

## 3 方法

### 3.1 架构

该智能体实现为一个具有三种节点类型的LangGraph状态图 (图1 (https://arxiv.org/html/2606.14945#S3.F1))。

参见图注
图1: ReAct智能体状态图。`reason` 调用LLM;如果产生工具调用,控制权传递给 `tools` 并返回。否则,`check` 评估收敛性。如果未收敛,控制权循环回 `reason`。
`REASON` 使用当前状态上下文调用LLM (Claude Haiku 4.5)。模型按照ReAct模式[2 (https://arxiv.org/html/2606.14945#bib.bib2)] 生成推理轨迹和工具调用。
`TOOLS` 确定性地执行工具调用并返回观察结果。
`CHECK` 在无需LLM参与的情况下评估收敛标准。条件边根据LLM是否产生工具调用(转到 `Tools`)或最终响应(转到 `Check`)进行路由。

该图使用 `langgraph.graph.StateGraph` 构建,带有条件边:

```python
graph = StateGraph(AgentState)

graph.add_node("reason", reason_node)
graph.add_node("tools", ToolNode(TOOLS))
graph.add_node("check", check_node)

graph.set_entry_point("reason")

graph.add_conditional_edges(
    "reason",
    lambda s: "tools" if has_tool_calls(s)
              else "check",
)

graph.add_edge("tools", "reason")

graph.add_conditional_edges(
    "check",
    lambda s: "end" if s["status"] != "running"
              else "reason",
)
```

### 3.2 状态模式

智能体状态定义为一个包含六个字段的类型化字典:

```python
class AgentState(TypedDict):
    messages: list
    experiment_history: list
    current_best: dict
    iteration: int
    train_py_content: str
    status: str
```

此状态在所有迭代中持久化。与无状态设计(LLM必须在每一步从结果表中重建上下文)不同,有状态智能体向前携带其实验历史、当前最佳结果和推理轨迹。`messages` 字段由一个20条消息的滑动窗口限制,确保每次迭代的输入成本不会随实验次数增长而增加。

一个 `ExperimentState` 辅助类管理历史记录,并在每一步向LLM提供紧凑的摘要:

```python
class ExperimentState:
    def __init__(self):
        self.history: list[dict] = []
        self.best_f1: float = 0.0
        self.best_params: dict = {}
        self.best_iteration: int = -1
        self.iteration: int = 0
        self.tried_configs: set = set()
        self.strategy_notes: list[str] = []

    def summary(self) -> str:
        """用于LLM的紧凑状态摘要。"""
        lines = [
            f"Iteration: {self.iteration}/{MAX}",
            f"Best F1: {self.best_f1}",
        ]
        recent = self.history[-5:]
        for h in recent:
            lines.append(
                f"Iter {h['iteration']}:"
                f"F1={h['f1']},"
                f"params={json.dumps(h['params'])}"
            )
        return "\n".join(lines)
```

`summary()` 方法使得O(1)成本成为可能:无论运行了多少次实验,LLM只接收最近5次的结果以及聚合统计信息。完整历史在状态中仍然可访问,但不会进入提示词。

### 3.3 工具与防护栏

智能体通过两个工具访问机器学习系统:`get_experiment_history`(查询过去的结果)和 `run_experiment`(使用指定的超参数进行训练,返回指标)。遵循自主研究原则,即数据流水线必须不可变,智能体只能修改超参数,不能修改数据加载或评估代码。

在生产变体中(见第5节 (https://arxiv.org/html/2606.14945#S5)),还提供了其他工具:`get_current_train_py` 读取当前训练笔记本,`modify_train_py` 上传修改后的版本并带有防护栏(必须保留评估和日志记录调用),`submit_training_job` 触发Databricks作业并轮询直至完成,以及 `query_results` 对结果表执行只读SQL查询。

### 3.4 收敛

智能体在满足以下三个条件中的任何一个时终止:(1) 目标指标超过用户定义的阈值,(2) 迭代预算耗尽,或 (3) LLM 认为进一步改进不太可能。

## 4 实验

使用两个基准任务来评估有状态架构在不同每次迭代观察数据大小下的表现。两者都使用通过Databricks模型服务端点 (databricks-claude-haiku-4-5) 提供的Claude Haiku 4.5,3个随机种子 (42, 123, 456),以及相同的无状态 vs. 有状态比较结构。令牌计数通过 tiktoken (cl100k_base) 估算。所有实验均在Databricks无服务器计算上运行。

### 4.1 任务1:超参数调优

第一个任务在UCI Covertype数据集[14 (https://arxiv.org/html/2606.14945#bib.bib14)](来自加州大学欧文分校机器学习知识库——一个具有54个特征的7类森林覆盖分类任务,子采样至20,000个实例)上优化XGBoost超参数。每次迭代产生一个小型观察数据:一个JSON超参数配置(约 ~200 个令牌)和三个标量指标 (F1、精确率、召回率)。预算为15次迭代。

此任务代表**小观察数据规模**:无状态智能体每次迭代的提示词增长约每个实验 ~200 令牌,因此总成本增长为 ∑_{i=1}^{n} 200i = O(n²),但常数较小。

#### 无状态运行器。

在每次迭代中,运行器构建一个包含*完整*实验历史(每次迭代的参数和指标)以及当前最佳分数的新提示词。此提示词作为单条消息发送给LLM;解析响应以获取JSON超参数配置。

#### 有状态运行器。

有状态运行器在所有迭代中维护一个 `ExperimentState` 对象和一个持久化对话。在每一步,LLM只接收一个紧凑的摘要(当前迭代、最佳分数、最近5次实验、策略注释),而不是完整历史。消息窗口被修剪为最近20条消息。

表1: 任务1:超参数调优结果(15次迭代,3个种子的均值)。F1差异在噪声范围内;令牌减少是主要发现。
参见图注
图2: 任务1:UCI Covertype上的超参数调优(15次迭代,3个种子)。(a) 每次迭代输入令牌成本:无状态线性增长;有状态保持在O(1)恒定。(b) 随迭代次数变化的累计最佳宏观F1。(c) 总令牌消耗:减少了9.8倍。(d) 挂钟时间比较。

在小观察数据规模下,有状态智能体实现了**9.8倍的令牌减少**(表1 (https://arxiv.org/html/2606.14945#S4.T1))。两个智能体都达到了相当的F1分数;观察到的差异 (0.779 vs. 0.764) 在种子方差范围内,并非宣称的贡献。无状态智能体的每次迭代输入令牌成本从第1次的约 ~700 线性增长到第15次的约 ~1,900(图2 (https://arxiv.org/html/2606.14945#S4.F2)a),而有状态智能体保持在每次迭代约 ~100 令牌不变。

### 4.2 任务2:代码性能优化

第二个任务针对一个故意效率低下的Python数据处理函数(100行,处理10,000条员工记录)优化执行速度。该函数包含冒泡排序、循环内的字符串拼接、手动字典累加以及其他反模式,LLM可以系统地将这些替换为Pythonic结构(`collections.Counter`、`sorted()`、列表推导式、f-string)。每次迭代产生一个**大型**观察数据:完整的修改后源代码(约 ~1,500–3,000 令牌)加上基准测试结果(中位执行时间、加速比、正确性检查)。预算为40次迭代。

此任务代表**大观察数据规模**:无状态智能体每次迭代的提示词增长约每个实验 ~2,000–4,000 令牌。经过40次迭代,在第40次迭代时发送的累积历史超过80,000令牌——接近较小模型的上下文窗口限制。

#### 无状态运行器。

在每次迭代中,运行器构建一个包含*完整*优化历史的新提示词:每个先前的代码版本、其基准测试时间、加速比因子和正确性状态。提示词遵循以下结构:

```
完整优化历史:
===迭代0 (时间=8.14ms, 加速比=1.00x)===
代码:
def process_records(records):
    filtered = []
    for r in records:
        if r["active"] == True and r["years"] >= 3:
            ...
基准测试:
执行时间:8.140 ms (20次运行的中位数)
基线时间:8.140 ms
加速比:1.00x
===迭代1 (时间=4.55ms, 加速比=1.79x)===
代码:
def process_records(records):
    filtered = [r for r in records
                if r["active"] and r["years"] >= 3]
    ...
基准测试:
执行时间:4.550 ms
加速比:1.79x
...
(所有前39次迭代)
提出下一个优化版本。
```

到第40次迭代时,此历史包含约 ~40 个完整代码列表,每次调用的输入超过80,000令牌。解析响应以获取包含修改后函数的 `CODE:` 块,并在沙盒化的 `exec()` 调用中执行。函数必须返回正确的输出结构(对照所需键进行验证),然后才能记录其计时。

#### 有状态运行器。

有状态运行器在所有迭代中维护一个 `CodeState` 对象和一个持久化对话(`messages` 列表)。`CodeState` 跟踪如下:

```python
class CodeState:
    def __init__(self):
        self.history = []
        self.best_time_ms = float("inf")
        self.best_code = ""
        self.best_iteration = -1
        self.iteration = 0
        self.strategy_notes = []
        self.failed_approaches = []
```

在每一步,LLM接收一个紧凑摘要。

相似文章