跳到主要内容
预算花不满,因为采样循环没有跨步计划

预算花不满,因为采样循环没有跨步计划

陈渊
陈渊

· 阅读约 4 分钟

一个 agent 手里有预算,为什么没把预算铺到不同基座、不同后训练方法的比较上?arXiv:2611.03551 那篇用 15 个模型、八套 coding-agent 配置跑企业 text-to-SQL 的交付阶段,最后只有 25% 的 run 把验收条件全过完。我盯着这个数字,没有先怪模型能力,先想到 loop 的结构。论文把这条列成瓶颈之一——underused compute budgets / insufficient comparison across base models and post-training methods——但它没接着往下挖。这篇我们只挖这一层:写一个最小能跑的 agent loop,再给它加一个跨步计划,跑一遍看差别。

第一步,搭最朴素的采样循环。管它是哪个框架封的,最里面都长这样:

from collections import Counter

ACTIONS = [("base_model", "A"), ("base_model", "B"), ("base_model", "C"),
           ("post_train", "sft"), ("post_train", "lora")]

def execute(action):
    # 模拟一次训练配方执行:只返回这一步覆盖到的维度
    return {"covered": action[0]}

class NaiveAgent:
    def __init__(self, budget):
        self.budget = budget
        self.trace = []

    def propose(self):
        if not self.trace:
            return ACTIONS[0]
        # 没有计划,只从近期上下文里挑一个最常出现的动作
        counts = Counter(a for a, _ in self.trace)
        return counts.most_common(1)[0][0]

    def run(self):
        while self.budget > 0:
            a = self.propose()
            r = execute(a)
            self.trace.append((a, r))
            self.budget -= 1
        return self.trace

for a, r in NaiveAgent(6).run():
    print(a, r["covered"])

跑出来是这样:

('base_model', 'A') base_model
('base_model', 'A') base_model
('base_model', 'A') base_model
('base_model', 'A') base_model
('base_model', 'A') base_model
('base_model', 'A') base_model

预算 6 个单位全花了,但全花在同一个动作上,post_train 维度一次都没碰到。这个版本还不够狠:真实 loop 里 propose 不是 Counter,多半是 LLM 按 P(action|context) 采样。采样温度调低,它更会重复局部看起来成功的动作;调高也只是乱跳,不会自动去补没跑过的格子。两种情况都指向同一个结构问题——loop 的每一步没有“还剩哪些组合没跑”的状态。它只有上下文,上下文里没有跨步计划,propose 想负责全局搜索也拿不到全局信息。

第二步,把缺的东西补上:一个独立于上下文的跨步计划。

BASES = ["A", "B", "C"]
POSTS = ["sft", "lora"]

class PlannedAgent:
    def __init__(self, budget):
        self.budget = budget
        self.trace = []
        self.remaining = [(b, p) for b in BASES for p in POSTS]

    def propose(self):
        return self.remaining.pop(0) if self.remaining else None

    def run(self):
        while self.budget > 0:
            step = self.propose()
            if step is None:
                break
            self.trace.append(step)
            self.budget -= 1
        return self.trace

for action in PlannedAgent(6).run():
    print(action)

输出:

('A', 'sft')
('A', 'lora')
('B', 'sft')
('B', 'lora')
('C', 'sft')
('C', 'lora')

同样 6 个预算单位,这个 loop 把所有六个格子都踩了一遍。差别不在模型,在 self.plan:一个显式的、独立于上下文的跨步状态。真实的后训练配方比这个格子复杂得多——数据、步数、硬件、验证集——但计划的结构一样:先把要比较的维度做成笛卡尔积,每走一步就划掉一个还没覆盖的格子,直到预算跑完或格子全被覆盖。没有这个结构,预算花不出去不是错觉,是控制流里没有地方可以花。

到这一步就清楚了:所谓“交付阶段只有 25% 的 run 满足所有 acceptance criteria”,很可能不全是模型执行能力不行。八套 coding-agent 配置,如果每套的 loop 都缺这个跨步计划,它们会在少数看起来能过的配置上耗掉预算,却没跑够足以比较不同基座/不同后训练方法的组合;到头来验收标准一全量查,就漏。论文作者在补救建议里写的那条 explicit experiment-environment specification,覆盖 hardware、budget scope、production checks,正是这个计划结构在评测侧的样子。他们没把它叫 agent loop,但往回对,它恰好补的是我们上面 self.plan 缺的那个位置。

有人可能会说,给 agent 加个“先列出所有组合”的 prompt 不就行了。那是把计划交给采样,仍然不牢靠。LLM 可以在第一步输出一个漂亮的计划文本,但后面的执行 loop 如果不把计划变成每个 step 能查的硬状态,计划就只是上下文里一段话,过了几十步就被新的执行结果稀释掉,proposal 又会回到局部采样。真正的跨步计划必须是一个独立的数据结构,不是一段 prompt。

这里顺便纠正一个容易想岔的地方:预算用不满不一定是物理 GPU 闲着。上面的 NaiveAgent 预算也是花完的,但它花在了重复动作上。论文里那条瓶颈写的是 underused compute budgets,我一开始也以为是指有的 run 提前停了、机器空转;再看八套配置只有 25% 通过验收这个数字,更可能的是预算被低信息量的动作占满,真正该花的对照实验没排上。严格说,“用不满”应该是“没用在刀刃上”。这个区别决定修法不一样——如果只是机器空转,加个重试就完了;如果是计划缺失,得改 loop 的结构。

想再深一层,有一个入口可以直接动手:把上面 PlannedAgent 的 self.remaining 换成一个真实的后训练配方,比如固定数据、固定 eval,只换 base model 和 post-training method,然后给每个格子加一条 acceptance criteria,看 coverage 从多少爬到多少。爬不动的地方就是计划粒度太粗,不够覆盖生产检查。翻开源码这步,等人把 forward-deployed agent 的 harness 放出来再对照;没放出来之前,上面这两个最小 loop 就是机制的骨架。这块地我据过,往下还有的是。

陈渊
陈渊

据守底层,挑「会用却说不出为什么」的 CS 机制从第一性原理挖到底。

查看主页 →