AFlow 的名字里挂着 MCTS。真读
scripts/optimizer_utils/data_utils.py,你会发现它没有树、没有 UCB、没有 N(node)/Q(node) 那套结构——所谓「MCTS 选择」是排序取前 K → softmax + 均匀混合 → 按概率抽一个当父节点,一共 30 行 numpy。
这不是贬义。承认这一点之后,AFlow 反而更好懂:它把一件事做对了——当你手里有一个跑得起、验得动、数值化的评分器,你就可以拿一个 LLM 当”改写算子”,把 workflow(agent 的编排代码)当”状态”,让搜索循环替你把 workflow 结构和 prompt 一起改。ICLR 2025 Oral 的论文(arXiv:2410.10762)挂的实验数字很硬:六个基准平均比手工设计高 5.7 个百分点,且能让小模型追平 GPT-4o、开销只有它的 4.55%——前提是任务能被数值验收。
我这篇不重复论文里”我们搜到了更好的 workflow”这条主线,而是走进 FoundationAgents/AFlow 的源码,回答三个问题:这套东西到底在搜什么、朴素做法会先在哪里崩、代码默认值和论文默认值为什么不一样。
名词速查
| 术语 | 一句话解释 |
|---|---|
| workflow | 一段 Python 代码,里面写好「模型先做 A、再做 B、投票、返回」的编排。AFlow 的状态就是这段代码。 |
| operator | 预先封装的 LLM 调用块,Generate/Review/Ensemble 一类。搜索空间里的积木。 |
| optimizer | 拿一个 LLM 当”改写者”,读旧 workflow + 历史 experience + 分数,吐出一份改写方案。 |
| evaluator | 跑当前 workflow 打分(HumanEval 的 pass@1、MATH 的准确率之类);是数值化的奖励信号。 |
| experience | 历史记录,主要用途只有一个:同一父节点上,同样的修改不许再提第二次。 |
| soft mixed probability | 把 top-K workflow 按分数 softmax,再和 1/n 均匀分布混合,用来抽父节点。相当于ε-greedy的连续版。 |
如果对「可验证奖励 / 验证器 / 环境」这一层没底,先读《大模型多试几次,为什么反而选错了?》——那篇讲验收器不完整会怎样,本篇讲验收器完整时能撬动什么。
根本约束:这个形状只能这么长
写下一句可反驳的话:AFlow 之所以长成「LLM 当改写算子 + workflow 是代码 + 数值 evaluator 当奖励」这个形状,是因为搜索循环要闭合,必须同时满足三件事——奖励便宜、奖励密集、奖励可读。
- 便宜:一轮 evaluate 里代码要跑 5 次取均值(
optimizer.py里validation_rounds: 5),如果每次都要人评,几百轮预算根本烧不起。 - 密集:不能只给一个终态 pass/fail,否则 LLM 改一处 prompt 都没法比较”这次改得对不对”。基准里选 HumanEval/MATH/GSM8K 都是每题一个 0/1,聚合成百分比就是密集的连续信号。
- 可读:得能取 top-K、能算 softmax、能挑父节点。分数必须能排序、能加权。
三条同时满足,才有资格做这种”用 LLM 搜 LLM 用法”的循环。如果奖励只满足其中一条会怎样?
- 只便宜、不密集(终态 pass/fail、稀疏):搜索退化成撞大运,混合概率里的 softmax 项没有区分度——去下面「所有 workflow 都 70 分附近」那组数字看结果。
- 只密集、不便宜(人工评分连续):轮次卡在预算而不是收敛。
- 只可读、不密集(比如 LLM-as-a-Judge 打分,但方差比信号还大):分数排序不稳,top-K 每轮换人,experience 缓存全废。
这就是为什么 AFlow 的六个基准都是有标准答案的题——它需要的不是”能评”,是”能廉价、密集、稳定地评”。开放式任务(论文附录 F 提了一下)不在主战场。
崩点:如果把中间层拆掉,最朴素的做法会怎样? 换成”让 LLM 直接从零写一个完整 workflow、跑一次 evaluator、看分数决定要不要留”——这就是 ADAS 的基本盘。论文 Table 1 里 ADAS 在六个基准的平均分是 67.2%,AFlow 是 80.3%,差了 13 个百分点。差在哪里?差在 AFlow 的每一步改动都是从某个已知能跑的父节点出发、只改一处、且 experience 里禁止重复提同一改动。这三件事联合起来把搜索空间从”全体合法程序”压到”有前缀有邻域的可增量修改集合”。
共同祖先:拆开看,AFlow 的每个部件都有更老的名字——
- LLM 当改写算子 ≈ 神经架构搜索(NAS)里的 evolutionary mutation,只不过 mutator 从遗传算子换成了自然语言 prompt。
- top-K + softmax 混合 ≈ ε-greedy 的连续版本,或者 Boltzmann exploration。
- experience 缓存 ≈ tabu search 的 tabu list。
- workflow-as-code ≈ DSPy 那条路线:prompt 和结构写成同一段可执行代码,一起编译一起打分。
叫它 MCTS 是给论文加光环。真正的 MCTS 有 tree policy(UCB1 之类)+ tree traversal + 反向传播沿路径更新,AFlow 只有 root-level 的”选一个高分父节点、改一次、评估、加进 pool”。这不影响它工作得很好——只影响你读代码时的心理预期。
主循环:一张表读完
不动源码语义地压缩 scripts/optimizer.py 的 _optimize_graph:
# 伪代码,对应 scripts/optimizer.py 的 _optimize_graph;异常/文件 IO 已省略
async def _optimize_graph(self):
top_rounds = self.data_utils.get_top_rounds(self.sample) # 取历史前 K
parent = self.data_utils.select_round(top_rounds) # softmax 混合抽父
prompt, graph = read_graph_files(parent["round"]) # 加载父 workflow
experience = load_experience_for(parent["round"]) # 加载兄弟修改记录
while True:
response = await optimize_llm( # LLM 当"改写算子"
build_optimize_prompt(experience, parent["score"],
graph, prompt, operator_desc))
if check_modification(experience, response["modification"], parent["round"]):
break # 不与已有修改重复才通过
write_graph_files(self.round + 1, response) # 保存新 workflow
avg = await evaluate_graph(self.round + 1, validation_n=5) # 跑 5 次取均值
update_experience(succeed = (avg > parent["score"])) # experience 里落一条
return avg
一共四步——选父 / LLM 改一次 / 跑 5 次评分 / 落 experience——外面套一个最多 20 轮的循环(max_rounds=20)。所谓的”tree-structured experience”落在磁盘上,succeed = bool(avg_score > experience["before"])——这就是全部的反向传播(experience_utils.py:91-95)。
MCTS 选择:手算一次 softmax 混合
这套框架里最有意思、也是最容易被论文包装夸大的地方,是 data_utils.py:78-108 的 _compute_probabilities。全文如下(略去防御性检查):
# scripts/optimizer_utils/data_utils.py
DEFAULT_ALPHA = 0.2 # softmax 温度
DEFAULT_LAMBDA = 0.3 # 均匀混合权重
def _compute_probabilities(self, scores, alpha=0.2, lambda_=0.3):
uniform_prob = np.full(n, 1.0 / n) # 均匀分布
shifted = scores - scores.max() # 减去最大值防溢出
exp_weights = np.exp(alpha * shifted) # Boltzmann
score_prob = exp_weights / exp_weights.sum() # 归一化 softmax
mixed_prob = lambda_ * uniform_prob + (1-lambda_) * score_prob
return mixed_prob
给一个具体例子。假设 top-4 workflow 的分数是 [0.75, 0.72, 0.70, 0.68](代码里会 * 100 变成百分点),我用 Python 标准库亲手跑了两组默认值——代码默认 α=0.2 λ=0.3,论文正文报告 α=0.4 λ=0.2(论文数值来自 arXiv HTML 版核对,我未通读 PDF 原文;代码默认直接读自源码常量)——结果如下:
| workflow 分数 | softmax(α=0.2) | mixed(λ=0.3,代码默认) | mixed(λ=0.2,α=0.4,论文正文) |
|---|---|---|---|
| 75 | 0.4623 | 0.3986 | 0.5843 |
| 72 | 0.2537 | 0.2526 | 0.2109 |
| 70 | 0.1701 | 0.1940 | 0.1223 |
| 68 | 0.1140 | 0.1548 | 0.0825 |
同一个搜索池,代码默认让最好那个 workflow 只拿 40% 的抽签权重、最差的也有 15%;论文默认让最好那个直接吸走 58%、最差的只剩 8%。方向刚好相反:代码默认更 explore,论文默认更 exploit。 这不是谁写错了,是两组默认值分别对”探索/收敛”做了不同下注——但如果你只读论文正文、不看代码常量就复现,你会得到一个搜索行为明显不同的系统。
再看一个极端场景:如果搜索池分数都差不多,softmax 会退化成什么?
| workflow 分数 | softmax(α=0.2) | mixed(λ=0.3) |
|---|---|---|
| 71 | 0.2893 | 0.2775 |
| 70 | 0.2369 | 0.2408 |
| 70 | 0.2369 | 0.2408 |
| 70 | 0.2369 | 0.2408 |
第一名的溢价只剩 3.7 个百分点——这就是 evaluator 信号变糊时搜索退化成随机走的样子。 也是”根本约束里为什么要奖励密集”的直接表现。
可复现的小实验(Python 标准库、无需模型、5 秒跑完):
import math
def compute_probs(scores, alpha, lam):
n = len(scores)
uniform = [1.0/n] * n
m = max(scores)
exp_w = [math.exp(alpha * (s - m)) for s in scores]
sum_w = sum(exp_w)
softmax = [w/sum_w for w in exp_w]
mixed = [lam*u + (1-lam)*p for u, p in zip(uniform, softmax)]
return softmax, mixed
for alpha, lam, label in [(0.2, 0.3, "code default"),
(0.4, 0.2, "paper text")]:
sp, mp = compute_probs([75, 72, 70, 68], alpha, lam)
print(label, "top pick prob =", round(mp[0], 4))
想验证代码-论文默认值到底哪个更好?把上面的 α、λ 网格化跑一遍 AFlow 的 HumanEval 单个基准(run.py --dataset HumanEval --initial_round 1 --max_rounds 10),看 20 轮内的收敛曲线——这是论文没做的消融,也是这段代码里最值得再挖的一处。
七个 operator:不是七种能力,是七个 prompt 模板
论文口径是七个 operator,源码里 scripts/operators.py 一共 400 行不到,我把它们按”实际做的事”归了三类:
| 类别 | operator | 源码位置 | 一句话本质 |
|---|---|---|---|
| 产出 | Custom / AnswerGenerate / CustomCodeGenerate | operators.py:89/99/109 | 就是一次 LLM 调用,带模板。Format (:340) 是它的输出整形版。 |
| 筛选 | ScEnsemble / MdEnsemble | operators.py:119/370 | 对多个候选做 Self-Consistency 投票;ScEnsemble 让 LLM 直接选一个字母 A/B/C。 |
| 反馈闭环 | Review / Revise / Test / Programmer | operators.py:350/360/267/188 | Review 出评语、Revise 按评语改、Test 跑代码验证、Programmer 是 Generate + Test 的循环体。 |
读懂一件事就够:这些 operator 不是硬编码的图结构,它们只是”LLM 优化器修改 workflow 代码时可以参考的现成积木”。论文里做过一次消融——去掉 operator,AFlow 仍能拿到 93.1%(HumanEval),因为 LLM 自己会在 workflow 代码里长出 ensemble-like 的结构。operator 库的作用是减少搜索步数,不是提供不可替代的能力。
这一点意味着,如果你要把 AFlow 迁移到你的领域,operator 库不是护城河——你只要有 evaluator,就可以从空 operator 起手。护城河是 evaluator。
论文数字:五分钟能不能追上手工
论文 Table 1(六个基准,GPT-4o mini 为执行模型,Claude-3.5-Sonnet 为优化模型):
| Method | HotpotQA | DROP | HumanEval | MBPP | GSM8K | MATH | Avg |
|---|---|---|---|---|---|---|---|
| IO 单次调用 | 68.1 | 68.3 | 87.0 | 71.8 | 92.7 | 48.6 | 72.8 |
| CoT + Self-Consistency | 68.9 | 78.8 | 91.6 | 73.6 | 92.7 | 50.4 | 76.0 |
| ADAS(从零搜 workflow) | 64.5 | 76.6 | 82.4 | 53.4 | 90.8 | 35.4 | 67.2 |
| AFlow | 73.5 | 80.6 | 94.7 | 83.4 | 93.5 | 56.2 | 80.3 |
我未复现过这张表;数字都来自论文原文,我从 arXiv HTML 版核对过。可以看出两件事:
- AFlow 平均比 CoT SC 高 4.3 个百分点、比 ADAS 高 13.1 个百分点——同一个执行模型 GPT-4o mini,用不同的 workflow 编排能拉出 13 分的差距。这是 workflow engineering 本身的可优化空间。
- MBPP 那一列差得最猛(AFlow 83.4 vs ADAS 53.4)——因为 MBPP 是”程序合成+隐藏测试”,能吃到 Test operator 的全部红利。任务和 operator 库匹配度越高,AFlow 相对朴素基线的领先越大。
论文另一处数字:让 GPT-4o mini 用 AFlow 搜出的 workflow 打,能在 HumanEval 上追平 GPT-4o 的表现,成本降到 4.55%。这是”结构性红利抵参数量红利”最直接的一次量化。
适用边界:这套结构什么时候不好用
我倾向于(这是我的判断,第二档证据,但由前面的根本约束支撑)AFlow 在以下四种情况会打折甚至失效:
- evaluator 靠 LLM-as-a-Judge:如果打分器方差比信号还大,softmax 项区分不出来,退化成随机走。见前面「所有 workflow 都 70 分附近」那组数字。
- 任务终态是主观质量(写作、创意、对话拟人度):没有 pass@k 这种数值,evaluator 缺席。论文附录 F 讨论过,但那不是主战场。
- 搜索预算 < 20 轮:AFlow 的
max_rounds=20默认是有道理的——太少几轮 experience 没建起来,混合概率里的 softmax 没锚点。 - workflow 边界跨系统(比如要跨微服务、跨数据库操作):AFlow 的 workflow 是一段 Python 代码,副作用范围仅限于 LLM 调用和本地代码执行。你要搜的东西一旦包含”调外部服务、有真实副作用”,这套评估循环就跑不动了。
小结
- 根本约束:搜索循环闭合需要奖励同时便宜、密集、可读;缺一个,AFlow 就退化。
- 崩点:换成朴素的”从零写 workflow + 跑评分”(ADAS),六基准平均掉 13 分——因为丢了父节点、experience 和混合选择这三件事。
- 共同祖先:LLM 改写算子 = evolutionary NAS 的 mutator;top-K 混合抽签 = Boltzmann exploration;experience 缓存 = tabu list;workflow-as-code = DSPy 那条编译路线。别被”MCTS”这个名字带偏——它不是一棵树。
- 可带走的动作:如果你的领域有数值 evaluator,AFlow 是一个 20 轮起步、只改一处 prompt/结构、拿 5 次均值当奖励的最小可运行搜索器。护城河不在 operator 库,在 evaluator。
常见的三种误读
比起总结,这里更适合列一个纠正表——AFlow 是我最近读过的最容易被误传的项目之一:
| 常见误读 | 实际情况 |
|---|---|
| ”AFlow 里有 MCTS,所以有树搜索、UCB、反向传播” | 没有树。选父节点就是排序取前 K 后 softmax + 均匀混合抽一个;「反向传播」是往磁盘写一行 succeed = avg > before。 |
| “AFlow 让 agent 自己生成 agent,是通用 workflow 优化器” | 只搜可数值验收的任务。开放式任务在附录 F 简短讨论,不是主战场。 |
| “operator 库是 AFlow 的核心资产” | 论文消融过:去掉 operator,HumanEval 上仍 93.1%。护城河是 evaluator 和 5 次取均值的评估流水线,operator 只是缩短搜索。 |
下一步(5 分钟内可动手):git clone https://github.com/FoundationAgents/AFlow,直接读 scripts/optimizer_utils/data_utils.py 从第 61 行到第 108 行——不到 50 行代码。读完再回头看论文里的 “Monte Carlo Tree Search” 那段,你会对”论文措辞和代码实现之间的距离”有一次校准。
参考来源
论文
- AFlow: Automating Agentic Workflow Generation(arXiv:2410.10762,ICLR 2025 Oral)——本文主要论据。数字来自论文原文,我通过 arXiv HTML 版核对,未通读 PDF。
- ADAS: Automated Design of Agentic Systems(Table 1 的对照基线,从零搜 workflow)。
- DSPy / TextGrad——同类”用 LLM 编译 LLM 用法”路线的两条前辈。
- Self-Consistency Improves Chain of Thought Reasoning——
ScEnsembleoperator 引用的原始出处。
源码
- FoundationAgents/AFlow — 本文所有源码定位均来自 main 分支,具体到:
scripts/optimizer.py、scripts/optimizer_utils/data_utils.py:16-108、scripts/optimizer_utils/experience_utils.py:69-95、scripts/operators.py:46-400。
本站相关
- 大模型多试几次,为什么反而选错了?从可验证奖励到环境工程 — 讲验收器不完整会怎样,是本篇根本约束的反面案例。
- 环境工程的非零梯度 — evaluator/环境为什么是资产。
- 不教,只给激励:DeepSeek-R1 — RLVR 那条路线的起点,本文是它的”推理时表亲”。
- Agent 工程决策地图 — AFlow 落在这张图的哪一格。
- RouteLLM 路由深读 — 同样是”分数当锚点做选择”的另一个变体,只是选择的对象换成了模型。