一句话:用 LLM 看历史 GA 运行记录,自动改写 GA 的搜索代码(分支因子、剪枝条件、并发度、停止规则),下一轮用改写后的代码跑。novelty 在于历史 GA 运行记录可以当 simulator 用——测试"如果当时用不同搜索参数会怎样"不用重跑,直接在记录上回放,所以 LLM 可以便宜地试很多种改法再选最好的。
所有术语用同一个贯穿全文的例子解释:任务 = 自动写出一个比 sklearn 更快的 Lasso solver Python 程序。
| 术语 | 具体是什么 | 在 Lasso 例子里 |
|---|---|---|
| Task 任务 |
固定不变的目标问题,有自动评测器给分 | "写一个 Lasso regularization path solver,在 17 个测试实例上运行时间要比 sklearn 短"。评测器 = 实际跑计时 |
| Coding Agent 编程智能体 |
实际写代码的 LLM(Gemini-3.1-Pro),受一段固定 prompt 驱动(论文 Listing 1)。整个实验中 prompt 不变,模型权重不变。 | Gemini 看到"请写一个比 sklearn 更快的 Lasso solver,已有历史尝试如下…",输出一段 Python 代码 |
| Attempt / Node 一次尝试 |
Coding agent 写了一版代码 + 评测器跑了一次 → 得到分数。树里每个节点 = 一次这样的尝试,存储:代码文件快照 + 评测结果 + 分数 | 节点 A:Gemini 写了"用 LARS 算法"的 solver → 跑计时 = 3500ms → 存入节点 |
| Branch 分支 |
从同一个父节点出发的不同探索方向。 从 root 出发 = 全新独立方向; 从已有节点出发 = refine 那个节点的代码 |
Branch A: 从头探索 LARS 路线 Branch B: 从头探索坐标下降路线 A→A1: 在 A 的代码基础上加 warm start |
| Discovery Tree 探索树 |
一轮完整探索产出的所有 attempt 的树形记录。根 = 空 workspace,子节点 = 各次尝试及其关系。每跑完一轮就产出一棵树。 | Round 1 跑完,得到一棵树:根→[A(3500ms), B(4200ms), C(2600ms)],C下面还有C1(2200ms) |
| History H_t 历史池 |
截至第 t 轮,所有已产出的树的集合。H₃ = {T₁, T₂, T₃} | 跑了 3 轮后,历史池里有 3 棵树,每棵对应一轮探索 |
| Policy(探索策略) ⚠️ 不是 prompt |
一段 Python 代码,决定"搜索行为":选哪些节点继续探索、并发度多少、何时停止。整个实验只有这个在变。 输入:当前已 reveal 的树节点;输出:下一批要探索的节点列表 |
solve(question) 函数里写着:先并行开 3 个 branch,哪个 3 步之内没提升就剪掉,集中资源 refine 最好的那个 |
| Policy Developer | 一个 LLM(也是 Gemini),读 replay 轨迹和分数,输出修改后的 policy Python 代码(不是 prompt,是代码)。受论文 Listing 2 的 prompt 驱动,该 prompt 固定不变。 | 看完 replay 后写:"Branch B 每次都差,3 步后就停;C 方向很有潜力,多开几个并行 refine" |
| Replay 回放 |
把一个 policy 代码跑在一棵已存的历史树上:policy 选节点 → 不重新跑代码,直接读树里存好的结果 → 统计这个 policy 在这棵树上的总分。零 LLM 调用,零代码执行。 | 测试新 policy "只探索前 3 个最快的方向":在 T₁ 上 replay,它选了 A、C,跳过 B → 读出 A=3500ms, C=2200ms → 最优=2200ms,用了 4 次 probe → 算分 |
| Dreaming 做梦 |
生成很多候选 policy,逐一在历史树上 replay 打分,选最高分的——这整个过程叫 dreaming。类比 Dreamer RL:在世界模型里想象轨迹而不真的执行。 | 生成 20 个候选 policy 代码变体,各自在 {T₁, T₂} 上 replay,耗时几秒,选出平均分最高的 |
| Round / Iteration 轮次 |
= 一次 Online Explore + 一次 Dreaming。每轮结束后 policy 更新一次。 | Round 1: 用初始 policy 跑 → 得 T₁ → dream → 得 π₂ Round 2: 用 π₂ 跑 → 得 T₂ → dream on {T₁,T₂} → 得 π₃ |
当前 policy π(一段 Python 代码)控制 coding agent 跑任务,产出一棵 discovery tree:
# policy 代码示例(Round 1 初始策略)
class OptimalPolicy:
def solve(self, question, budget=None):
# 并行开 10 个全新方向(从 root 出发)
roots = question.legal_roots()[:10]
question.probe_batch(roots) # 触发 10 次 coding agent 调用
# 对每个方向,继续 refine 最多 11 次
for step in range(11):
leaves = question.legal_actions()
question.probe_batch(leaves)
每次 probe_batch 在 online 阶段 = 真实调用 coding agent(LLM 写代码 + 评测器计时),结果存入树节点。
跑完后产出:
T₁(Round 1 的树):
Root
├── Node A: 代码=LARS算法, 分数=3500ms, 文件快照已存
│ └── Node A1: 代码=LARS+warmstart, 分数=3100ms, 文件快照已存
│ └── Node A2: 代码=LARS+调参, 分数=2900ms, 文件快照已存
├── Node B: 代码=坐标下降, 分数=4200ms, 文件快照已存
│ └── Node B1: 代码=坐标下降+向量化, 分数=3800ms, 文件快照已存
└── Node C: 代码=active set, 分数=2600ms, 文件快照已存
└── Node C1: 代码=active set+KKT, 分数=2200ms ← 本轮最优
Online 结束,T₁ 存入历史池 H₁ = {T₁}。现在 policy developer(LLM)想测试一个新 policy:"先并行看 3 个 branch,分差的立刻剪掉"。
把这个候选 policy 代码在 T₁ 上 replay:
# replay 时 probe_batch = 读树里存好的结果,不调用任何 LLM
question.probe_batch([A, B, C]) → 立刻返回 3500, 4200, 2600(从 T₁ 读出)
# policy 看 B 最差,剪掉;继续 A 和 C
question.probe_batch([A1, C1]) → 立刻返回 3100, 2200(从 T₁ 读出)
# policy 决定停止
replay score = 最优(2200ms换算分数) - 成本惩罚(用了5个节点) + 并发奖励
整个过程:0 次 LLM 调用,0 次代码执行,耗时毫秒级。
policy developer 可以这样测试几十个候选 policy 变体,选出平均 replay 分最高的,作为 π₂ 用于下一轮 online。
flowchart LR
A["Online"] -->|"new tree T_t"| B["History Pool"]
B -->|"replay all trees"| C["Dreaming"]
C -->|"pick best"| D["New Policy"]
D -->|"next round"| A
| 轮次 | 上线用的 policy | 产出 | dreaming 在哪些树上 | 得到新 policy |
|---|---|---|---|---|
| Round 1 | 初始固定策略(开10并行,refine 11次) | T₁ | {T₁} | π₂ |
| Round 2 | π₂ | T₂ | {T₁, T₂} | π₃ |
| Round 3 | π₃ | T₃ | {T₁, T₂, T₃} | π₄ |
| Round 4 | π₄ | T₄ | {T₁…T₄} | π₅ |
| Round 5 | π₅ | T₅ | {T₁…T₅} | (实验结束) |
历史树越积越多 → replay 的参考越来越丰富 → policy 改进信号越来越准。
| 分项 | 含义 | Lasso 例子 |
|---|---|---|
| max score | 这次 replay 看到的最好结果 | C1 节点:2200ms(换算越小越高分) |
| −β₁ × N | 惩罚 reveal 的节点总数(代理真实执行成本) | reveal 了 5 个节点,扣 5β₁ 分 |
| +β₂ × N/轮数 | 奖励并发:每轮平均 reveal 多个节点(并行比串行好) | 2 轮 reveal 了 5 个节点,平均 2.5/轮,加分 |
最终选出在所有历史树平均 replay 分最高的 policy,保证不比当前策略差。
| 组件 | 类型 | 固定/变化 | 作用 |
|---|---|---|---|
| Coding agent prompt(Listing 1) | Prompt 文本 | 固定不变 | 驱动 Gemini 写候选代码 |
| Policy developer prompt(Listing 2) | Prompt 文本 | 固定不变 | 驱动 Gemini 修改 policy 代码 |
| Policy(探索策略) | Python 代码 | 每轮更新 | 决定搜索行为(选哪些节点/并发/停止) |
对比:Dream-RSI(每轮更新 policy)vs. Recursive Fixed Exploration(policy 固定不变),其余完全相同。
| 方法 | 模型 | 总调用次数 | 平均运行时 (ms)↓ |
|---|---|---|---|
| sklearn | – | – | 44,180 |
| glmnet | – | – | 13,768 |
| SimpleTES | GPT-OSS-120B | 51,200 | 3,805 |
| Recursive Fixed | Gemini-3.1-Pro | 550 | 3,587 |
| Recursive Fixed | Gemini-3.7-Flash | 3,200 | 2,517 |
| Dream-RSI | Gemini-3.1-Pro | 317 | 2,931 |
| Dream-RSI | Gemini-3.7-Flash | 1,879 | 2,351 |
Dream-RSI 用 SimpleTES 约 1/162 的调用次数达到更好性能。同样用 Gemini-3.1-Pro,Dream-RSI 比 Fixed 少用 42% 调用(550→317),且结果更好(3587→2931ms)。
| 方法 | Sum–Diff ↑ | AutoCorr ↓ | Circle Pack ↑ |
|---|---|---|---|
| AlphaEvolve | – | 1.4557 | 2.635862 |
| SimpleTES (51,200 调用) | 1.143975 | 1.453675 | 2.635983 |
| Recursive Fixed (Gemini-3.1-Pro) | 1.144047 | 1.456001 | 2.635983 |
| Dream-RSI (<1000 调用) | 1.145427 | 1.456375 | 2.635983 |
| 任务 | Dream-RSI 优势 | 条件 |
|---|---|---|
| VGG16 | 2.43× 更少 generations | 达到相同性能 |
| LayerNorm | 1.79× 更少 generations | 达到相同性能 |
| ConvDiv | 2.09× 更高性能 | 相同 budget |
| ConvMax | 1.44× 更高性能 | 相同 budget |
对比实验:把历史树压缩成"方向性建议"文字,注入 coding agent 的 prompt("之前试过 LARS,建议探索 active set 方向")。结果:这样反而更差,在 Fixed 和 Dream-RSI 两种范式下均不如不注入。
原因:文字建议会过度缩小搜索空间,让 agent 都往同一个方向走,失去多样性。而 replay 不告诉 agent 往哪走——它只改变 policy 决定"何时停、怎么分配并发",agent 本身仍自由探索。
Policy 呈现自适应模式——不是固定策略,而是根据当前进展动态调整:
对 policy π^m 在历史树 T_i 上的 replay:
V_i^m = max_{v ∈ revealed_nodes} score(v) # 最优解分数
- β₁ × N_i^m # 惩罚 reveal 节点总数
+ β₂ × N_i^m / max(1, 轮数) # 奖励并发(每轮 reveal 越多越好)
policy 总分 = 对所有历史树的平均 V_i^m
β₁、β₂ 固定,选出总分 ≥ 当前 policy 的最优候选。