如何打造一个自进化的 Agent:逐行拆解 autoresearch
「怎么做一个能自我进化的 Agent?」
我最初想到的答案和大多数人一样:让它改自己的 prompt,让它反思自己的输出,再给它配一个 critic 模型打分。这套东西能跑,在短任务上也确实能看到提升。
但它有一个藏得很深的前提:得有一个能判断「变好了」的裁判。当裁判是 Agent 自己时,这个前提就塌了。模型对自己的输出有系统性偏好——它改出来的方案,它自己也更认可。于是你会得到一个稳定「进步」的日志,和一个并没有变好的系统。
所以真正的问题不是「怎么让它改自己」,而是**「改完之后,凭什么说它变好了,以及改坏了怎么回去」**。
karpathy 今年开源的 autoresearch 把这两个问题回答得很干净。它的目标不是让 Agent 变聪明,而是让 Agent 的每一次尝试都变得便宜、可比、可回滚。整件事的聪明之处几乎全在约束设计上,不在模型能力上。
下面我用这个仓库的原文和行号,把它的控制机制拆开。行号对应当前 master(228791f),文中出现的数字来自仓库 README 的示例输出,不是我的实测结果。
一、自进化需要三个零件,缺一个就会退化
先把「自进化」这个词拆成可以检查的东西。一个能持续变好的系统,至少要有三样:
| 零件 | 作用 | 缺了会怎样 | autoresearch 里对应 |
|---|---|---|---|
| 变异 | 产生候选改动 | 只能重复自己,没有新状态可探索 | Agent 改 train.py |
| 适应度 | 判断哪个改动更好 | 无法淘汰,谱系一路腐化 | val_bpb |
| 遗传 | 保存好状态、丢弃坏状态 | 每次从零开始,或带着坏状态前进 | git 分支 + git reset |
大部分「自进化 Agent」的教程只认真做了第一个。它们教你怎么让模型改 prompt、怎么加反思步骤,但适应度那一栏是空的——或者更糟,适应度就是模型自己的判断。遗传那一栏也是空的,改完就地覆盖,没有回滚点。
结果就是:系统在动,但不是在进化。进化需要选择压力,而选择压力需要一个外部的、不可被优化对象篡改的裁判。
autoresearch 的设计,本质上是把这三栏都填满,而且每一栏都刻意做得很小。
二、为什么被「编程」的是 Markdown,不是代码
这是整个项目最反直觉的地方,也是 README 里被放在正中间的一句话。
README.md:7
The core idea is that you’re not touching any of the Python files like you normally would as a researcher. Instead, you are programming the
program.mdMarkdown files that provide context to the AI agents and set up your autonomous research org.
翻译过来:核心想法是,你不再像传统研究者那样去改 Python 文件;你编程的对象是 program.md 这些 Markdown,用它们给 Agent 提供上下文、搭建你的自主研究组织。
学到什么:仓库里有两层被编辑的东西,而且编辑者不是同一个人。
README.md:14
train.py— the single file the agent edits. … This file is edited and iterated on by the agent.
README.md:15
program.md— baseline instructions for one agent. Point your agent here and let it go. This file is edited and iterated on by the human.
train.py 归 Agent 改,program.md 归人改。这是一个两层结构:Agent 在里层做实验,人在外层改「怎么做实验」的规则。
为什么有效:因为它把「改进研究策略」的迭代成本压到了接近零。如果你把研究策略写进 Python——比如写一个专门的 scheduler 类来安排实验顺序——那每次想调整策略,你都要改代码、跑测试、担心引入 bug。而改 Markdown 只需要改几行自然语言,下一次 Agent 读进去就生效,没有编译、没有回归测试、没有接口兼容问题。
README 对这个文件的定位说得很直接:
README.md:50
The
program.mdfile is essentially a super lightweight “skill”.
一个「超轻量级的 skill」。这个类比是准确的:skill 的本质就是「用自然语言封装一套针对特定任务的策略」,只不过这里的 skill 不是给一次对话用的,而是给一个无限循环用的。
而 program.md 自己对自己的描述,只有一行:
program.md:3
This is an experiment to have the LLM do its own research.
这句话里没有任何技术细节,但它定了一个很关键的语气:这是一个实验,不是产品。所以后面所有的规则都可以被当作「当前版本的假设」来改,而不是必须遵守的规范。这句话在给整份文档定可修改性。
三、md 里到底要写什么,Agent 才会去改代码
program.md 不是一份需求文档,它更像一份给新员工的交接说明:哪些事你能做、哪些不能做、入口在哪、做完怎么汇报。
它用三个层次把动作空间框住。
第一层:允许改什么
program.md:26
- Modify
train.py— this is the only file you edit. Everything is fair game: model architecture, optimizer, hyperparameters, training loop, batch size, model size, etc.
只能改 train.py,但里面的东西随便动:架构、优化器、超参、训练循环、batch、模型大小。
学到什么:注意这句话的结构——先给一个极窄的边界(一个文件),再在边界内给一个极大的自由度(everything is fair game)。这是控制 Agent 的通用手法:收窄范围,放开强度。如果反过来(允许改多个文件,但每处只许微调),Agent 会花大量时间在文件之间跳转,而且 diff 无法审查。
README.md:63 补了理由:
Single file to modify. The agent only touches
train.py. This keeps the scope manageable and diffs reviewable.
「scope 可控、diff 可审查」。后半句更重要——可审查性是这套系统能被人信任的前提。如果 Agent 一晚上改了 20 个文件,你早上根本没法判断它做了什么。
第二层:绝对不能改什么
program.md:29
- Modify
prepare.py. It is read-only. It contains the fixed evaluation, data loading, tokenizer, and training constants (time budget, sequence length, etc).
program.md:30
- Install new packages or add dependencies. You can only use what’s already in
pyproject.toml.
program.md:31
- Modify the evaluation harness. The
evaluate_bpbfunction inprepare.pyis the ground truth metric.
三条禁令,一条比一条狠。不能改 prepare.py,不能装新包,不能碰评测。最后一句直接把 evaluate_bpb 定义成 “the ground truth metric”(真值指标)。
为什么有效:这三条堵住了三条不同的作弊路径。
不能改 prepare.py,堵的是「改数据预处理让任务变简单」;不能装新包,堵的是「引入外部预训练权重或现成组件,把研究变成调库」;不能碰评测,堵的是「直接改考卷」。
这里有个细节值得停一下:这三条禁令在技术上是软的。Agent 有完整的文件写权限,README 甚至建议你「disable all permissions」。它想违反随时可以。所以真正的约束不是这几行字,而是 program.md:102 那条——不提交 results.tsv,让所有实验记录留在工作区,你事后 git log + git diff 一查就知道它动过什么。
约束靠的不是不可违反,而是不可隐藏。
第三层:入口坐标
这一层不在 program.md 里,在 train.py 里。
train.py:429
# Hyperparameters (edit these directly, no CLI flags needed)
紧接着是 ASPECT_RATIO、TOTAL_BATCH_SIZE、DEPTH、DEVICE_BATCH_SIZE 这一排常量。
学到什么:这是一句写给 Agent 看的注释。它同时说了两件事——超参在哪(就在下面),以及不要改成 CLI 参数(no CLI flags needed)。
为什么有效:这是我最想强调的一个细节。如果没有这句话,一个能力不错的 Agent 很可能会做一件「看起来很专业」的事:把超参抽成 argparse,加一个 --lr 参数,再加个 config 文件。从软件工程角度这完全正确,但对这个任务来说是纯粹的负担——它增加了几十行代码、引入了配置解析的失败面、而且让「改一个超参」这个动作的 diff 从 1 行变成 5 行。
Agent 的漂移,很多时候不是它做错了,是它用了一个正确但不适用的工程习惯。 program.md 的任务就是把这类习惯提前关掉。
四、适应度:为什么必须是一个不可伪造的标量
现在到了最关键的一栏。
program.md:33
The goal is simple: get the lowest val_bpb. Since the time budget is fixed, you don’t need to worry about training time — it’s always 5 minutes. Everything is fair game: change the architecture, the optimizer, the hyperparameters, the batch size, the model size. The only constraint is that the code runs without crashing and finishes within the time budget.
目标只有一句:把 val_bpb 压到最低。唯一的约束是代码能跑通、不超时。
这个指标本身在 prepare.py 里定义,而且文件顶部就被标了红:
prepare.py:340
# Evaluation (DO NOT CHANGE — this is the fixed metric)
它的语义写在函数的 docstring 里:
prepare.py:344-351
1
2
3
4
5
6
7
8 def evaluate_bpb(model, tokenizer, batch_size):
"""
Bits per byte (BPB): vocab size-independent evaluation metric.
Sums per-token cross-entropy (in nats), sums target byte lengths,
then converts nats/byte to bits/byte. Special tokens (byte length 0)
are excluded from both sums.
Uses fixed MAX_SEQ_LEN so results are comparable across configs.
"""
学到什么:bpb 是「每字节的编码代价」,本质上是压缩率。它先把每个 token 的交叉熵(单位 nats)加起来,再把目标 token 的字节长度加起来,然后相除,最后把 nats 换算成 bits。
为什么不用 loss,要用 bpb? 因为 bpb 除以了字节数,所以与词表大小无关。Agent 如果把 vocab_size 从 8192 改成 4096,per-token loss 会明显变化,但 bpb 仍然可比。docstring 最后一句把这个意图说透了:Uses fixed MAX_SEQ_LEN so results are comparable across configs——用固定序列长度,让不同配置的结果可比。
这是一个很容易被忽略的设计决策。如果指标是「验证集 loss」,那么 Agent 会很快发现一条捷径:把词表调小、把 batch 调小,loss 曲线就会好看。这不一定是提升,可能只是把问题变简单了。bpb 和词表解耦之后,这条捷径被堵住了——你改词表,bpb 不会因为分词粒度变化而虚假下降。
更深一层的理由:单标量是「淘汰」这个动作能成立的前提。如果指标是向量(既要 bpb 低,又要显存小,又要代码简洁),那「这次改动到底算不算更好」就变成了一个需要人判断的偏序问题,Agent 无法自主决策,循环立刻断掉。
README 把这条设计的好处写得很清楚:
README.md:17
The metric is val_bpb (validation bits per byte) — lower is better, and vocab-size-independent so architectural changes are fairly compared.
「让架构改动被公平比较」。这就是单标量的价值:它不是最好的指标,它是唯一能让不同改动互相比较的指标。
但显存是软约束,这里有个分寸
program.md:35
VRAM is a soft constraint. Some increase is acceptable for meaningful val_bpb gains, but it should not blow up dramatically.
显存是软约束,为了有意义的 bpb 提升可以接受一些增长,但不能暴涨。
学到什么:这是唯一一个非二值的判断。注意它的措辞——不是「显存不得超过 X GB」,而是「不能 blow up dramatically」。这个模糊度是刻意的:如果把显存写成硬上限,Agent 会把大量精力花在「压显存」而不是「降 bpb」上,而显存本来只是次要成本。
这里也埋了一个风险:模糊的约束意味着 Agent 需要自己判断什么叫「dramatically」。这类判断在无人监督下会逐渐松动——今天加 2GB 算合理,明天加 5GB 也「有意义的提升」。软约束的成本,就是你放弃了这一维的控制权。
五、遗传:为什么状态必须活在 Agent 的上下文之外
Agent 的上下文是不可靠的。它会被截断,会遗忘,会把自己的历史记错。所以真实状态必须放在外面。
program.md 的循环体只有 9 步,其中第 7 到第 9 步是全部的选择逻辑:
program.md:102
- Record the results in the tsv (NOTE: do not commit the results.tsv file, leave it untracked by git)
program.md:103
- If val_bpb improved (lower), you “advance” the branch, keeping the git commit
program.md:104
- If val_bpb is equal or worse, you git reset back to where you started
学到什么:选择规则只有两条分支,而且是严格二元的——更好就前进,持平或更差就回滚。没有「差不多就留着看看」,没有「这次不行但方向对」。
为什么有效:因为二元判断是可自动化执行的最小决策。任何需要权衡的规则,都会在无人监督下被 Agent 重新解释。
但真正值得看的是第 102 行那个括号:不提交 results.tsv,让它不被 git 跟踪。
这句话看起来像个排版细节,其实是个很精确的设计。想一下:如果 results.tsv 被 git 跟踪,那么第 104 行的 git reset 会把它一起回滚——Agent 的实验记忆会随着每次失败被抹掉。跑十次失败之后,它会发现自己回到了起点,什么都不记得。
所以这里必须把两种状态分开:
| 状态类型 | 存哪 | 谁管 |
|---|---|---|
| 代码状态 | git 分支 | commit / reset |
| 实验记忆 | 工作区文件 results.tsv |
只追加,永不回滚 |
这是一个很细但很关键的一刀:git 管「我现在是什么」,文件管「我试过什么」。 混在一起,回滚就会变成失忆。
而 results.tsv 的格式也是被规定死的:
program.md:71
commit val_bpb memory_gb status description
五个字段:提交哈希、指标、显存、状态(keep / discard / crash)、一句话描述。
program.md:66
When an experiment is done, log it to
results.tsv(tab-separated, NOT comma-separated — commas break in descriptions).
用制表符而不是逗号,因为描述里会有逗号。这个细节说明作者预设了 Agent 会写自然语言描述——记录不是给机器看的,是给下一次的自己看的。
这个文件后来成了 analysis.ipynb 的唯一输入:它读 tsv,画出「每次 keep 之后 val_bpb 如何下台阶」的曲线。也就是说,Agent 的实验日志,直接变成了人类评估这次自主研究质量的证据。这一点比它看起来重要:它意味着 Agent 的每一步都留下了可审计的痕迹,而不是只在最后交给你一个「我变好了」的结论。
六、反馈带宽:为什么只让两个数字回流
这一节是我认为整个项目最精妙的地方。
program.md:99
- Run the experiment:
uv run train.py > run.log 2>&1(redirect everything — do NOT use tee or let output flood your context)
program.md:100
- Read out the results:
grep "^val_bpb:\|^peak_vram_mb:" run.log
学到什么:跑训练时,所有输出重定向进文件,不用 tee,理由写在括号里——不要让输出淹没你的上下文。跑完之后,只 grep 两个字段:val_bpb 和 peak_vram_mb。
一次 5 分钟的训练会打印几千行日志,里面有 step、loss、学习率倍率、每步耗时、tok/sec、MFU、epoch 进度。这些全都留在 run.log 里,一个都不进上下文。
为什么有效:这不是为了省 token,是为了防止一种很具体的失败——幻觉式因果推断。
如果 Agent 读到完整 loss 曲线,它几乎必然会开始解释它:「loss 在 3000 步后趋于平缓,说明 warmdown 比例设得太早」「第 800 步有个尖峰,可能是 batch 太大」。这些解释听起来专业,但建立在单次运行的噪声上,而且一旦它把某个解释当成结论,下一步的改动就会基于一个不存在的因果关系。
更隐蔽的问题是上下文稀释。program.md 里的禁令(不能改 prepare.py、不能装包)在上下文的开头。如果中间塞进几千行日志,这些早期约束的权重会被摊薄,Agent 会开始做一些「它明明被告知不能做」的事。
所以压缩带宽的真正作用,是把 Agent 从「解释者」降级为「变异算子」。它不需要理解为什么变好,它只需要知道有没有变好。这是刻意的降智,换的是稳定性。
但崩溃时它又能看日志,这里有个分级
program.md:101
- If the grep output is empty, the run crashed. Run
tail -n 50 run.logto read the Python stack trace and attempt a fix. If you can’t get things to work after more than a few attempts, give up.
只有当 grep 输出为空(也就是没跑出结果、确实崩了)时,才允许 tail -n 50 看堆栈。
学到什么:日志的读取权限是分级的。正常路径只给两个数,异常路径才展开 50 行堆栈,而且限定 50 行、限定「试几次不行就放弃」。
为什么有效:因为「需要更多信息」这件事,必须由一个客观条件触发(grep 为空),而不是由 Agent 自己觉得「我想多了解一点」触发。如果让 Agent 自主决定要不要读日志,它几乎总会读——读信息在直觉上永远是「更谨慎」的选择,但在长循环里,它是上下文腐化的主要来源。
给 Agent 的信息权限,要按「是否异常」来分,而不是按「是否有用」来分。 后者它自己判断不了。
七、反熵:唯一一条阻止「越改越烂」的规则
program.md:37
Simplicity criterion: All else being equal, simpler is better. A small improvement that adds ugly complexity is not worth it. Conversely, removing something and getting equal or better results is a great outcome — that’s a simplification win. When evaluating whether to keep a change, weigh the complexity cost against the improvement magnitude. A 0.001 val_bpb improvement that adds 20 lines of hacky code? Probably not worth it. A 0.001 val_bpb improvement from deleting code? Definitely keep. An improvement of ~0 but much simpler code? Keep.
这段值得整段读。它说的是:同等条件下越简单越好;加 20 行 hack 代码换 0.001 的提升,大概不值得;删掉代码还能拿到 0.001 的提升,一定保留;提升接近 0 但代码简单很多,也保留。
学到什么:这是整个 program.md 里唯一一条「反熵」规则。其他规则都在说「怎么改」,只有这条在说「什么改动不该被接受」。
为什么有效:因为 Agent 的默认漂移方向是加东西。加一个模块、加一个 trick、加一段注释——这些动作在语言模型的先验里是「努力」的表现,而且在噪声范围内几乎总能带来一点点提升(改得越多,撞上好运气的概率越高)。如果没有这条规则,谱系会单调地变复杂:每次 keep 都留下几十行代码,一百次实验之后,train.py 会变成一个没人看得懂的怪物。
这条规则的作用,相当于给搜索空间加了一个朝向低复杂度的梯度。它让「删除」也能得分,于是简化不再需要额外的动机。
但要注意它的成本:它把「简洁」这个主观判断交给了 Agent。什么叫 “ugly complexity”、什么叫 “hacky code”,没有客观标准。这条规则的效力,完全取决于 Agent 的判断力——这是整个设计里最依赖模型能力的一处,也是最容易在换模型后退化的一处。
八、无人值守:为什么必须写 NEVER STOP
program.md:112
NEVER STOP: Once the experiment loop has begun (after the initial setup), do NOT pause to ask the human if you should continue. Do NOT ask “should I keep going?” or “is this a good stopping point?”. The human might be asleep, or gone from a computer and expects you to continue working indefinitely until you are manually stopped. You are autonomous. If you run out of ideas, think harder — read papers referenced in the code, re-read the in-scope files for new angles, try combining previous near-misses, try more radical architectural changes. The loop runs until the human interrupts you, period.
学到什么:这条禁令防的不是「偷懒」,而是礼貌。
一个训练良好的助手,在被交予一个长任务时,会倾向于在关键节点请示:「我已经跑了 12 个实验,要不要继续?」从对话体验看这是对的,但在无人值守场景下,请示就等于停止,停止就等于任务失败。人可能在睡觉。
为什么有效:它把「人类确认」这个环节从循环里彻底删掉了。
但代价很大,而且必须说清楚:删掉人类确认,意味着 program.md 里任何模糊地带,Agent 都会自己填。你在文档里没写清楚的地方,它会用预训练里的通用直觉补上——而那个直觉,很可能和你的意图不一致。
所以 NEVER STOP 不是一条独立的规则,它是对前面所有规则的压力测试。只有在「允许什么、禁止什么、怎么判断、失败了怎么办」都写死之后,你才敢写 NEVER STOP。反过来说,如果你的 md 里还有大量需要人来判断的空白,加这条就是给自己埋雷。
最后一句也值得注意:If you run out of ideas, think harder——后面给了三个具体的「想更深」的方向:读代码里引用的论文、重读在范围内的文件找新角度、把之前差一点的方案组合起来。它在把「想不出办法」这个状态,转成一组可执行的动作,而不是允许 Agent 停下来。
九、5 分钟:一个被低估的设计
program.md:23
Each experiment runs on a single GPU. The training script runs for a fixed time budget of 5 minutes (wall clock training time, excluding startup/compilation). You launch it simply as:
uv run train.py.
每次实验固定跑 5 分钟墙钟时间,不含启动和编译。
学到什么:这是一个把「算力」从变量变成常量的动作。
为什么有效:如果让 Agent 自己决定训练多久,它会一路训到收敛——因为那确实能拿到更低的 bpb。但这样一来,实验之间就不可比了:A 方案训了 5 分钟,B 方案训了 50 分钟,你不能说 B 更好,你只能说 B 用了更多卡时。整个循环退化成「谁肯烧算力谁赢」。
固定 5 分钟之后,模型多大、batch 多大、架构怎么改,全部进入同一个竞技场。被优化的东西从「绝对效果」变成了「单位算力下的效率」,而后者才是研究真正该优化的东西。
README 把这个决策的两面都写了,这是我很欣赏的一点:
README.md:64
There are two upsides of this design decision. First, this makes experiments directly comparable regardless of what the agent changes (model size, batch size, architecture, etc). Second, this means that autoresearch will find the most optimal model for your platform in that time budget. The downside is that your runs (and results) become not comparable to other people running on other compute platforms.
两个好处:实验可比;能针对你的平台找到该预算下的最优模型。一个坏处:结果不能跨平台比较。
学到什么:这是「交还选择权」的写法——不假装这个设计只有优点。而这个坏处是真实的:你在 H100 上跑出来的最优架构,换到 A100 上可能完全不成立,因为瓶颈从算力变成了显存带宽。
固定预算还有一个副产品:可预测的吞吐。README 算过,约 12 次/小时,睡一觉约 100 次。这个数字本身是设计目标的一部分——它让「一晚 100 次实验」成为一个可以规划的资源,而不是一个模糊的期望。
十、失败也要分类,否则循环会卡死
program.md:108
Timeout: Each experiment should take ~5 minutes total (+ a few seconds for startup and eval overhead). If a run exceeds 10 minutes, kill it and treat it as a failure (discard and revert).
program.md:110
Crashes: If a run crashes (OOM, or a bug, or etc.), use your judgment: If it’s something dumb and easy to fix (e.g. a typo, a missing import), fix it and re-run. If the idea itself is fundamentally broken, just skip it, log “crash” as the status in the tsv, and move on.
超 10 分钟直接 kill 当失败;崩溃分两类——typo、缺 import 这种修一下重跑,思路本身错了就记 crash 跳过。
学到什么:失败不是一种状态,是两类。「修一下能好的」和「方向错了的」,处理方式完全不同。如果不区分,Agent 会陷入无限修 bug 的循环——这是长循环 Agent 最常见的死法。
为什么有效:因为这两类失败的期望收益完全不同。修 typo 的收益是确定的(改完就能跑),继续修一个方向性错误的方案,收益是负的(浪费的不只是时间,还有上下文)。
train.py 内部还有一道更早的闸门:
train.py:569-570
1
2 # Fast fail: abort if loss is exploding or NaN
if math.isnan(train_loss_f) or train_loss_f > 100:
学到什么:不等 5 分钟跑完,一旦 loss 变 NaN 或超过 100,立刻 exit(1)。
为什么有效:这是把「失败检测」提前到了最早可能的时刻。如果不这样做,一个已经发散的训练会继续跑满 5 分钟,然后交给你一个巨大的 bpb——你不仅浪费了 5 分钟,还得到一条误导性的记录。快速失败的价值不在于省时间,而在于不让坏数据进入统计。
十一、它其实不试图防止漂移
到这里可以把所有零件拼起来了。而拼完之后你会发现,这套系统有一个和直觉相反的哲学。
它没有漂移检测器。
没有模块在检查「Agent 是不是偏离了原始目标」,没有 critic 在评估「这次改动是否符合研究伦理」,没有一致性检查。它只有淘汰机制。
漂移的后果会表现为 val_bpb 不降,于是被 git reset 掉。你不需要知道它漂了,你只需要让漂移的结果无法通过筛选。
这是进化思路和设计思路的根本差别:
- 设计思路:写更详细的 md、加更多约束、加反思环节、加 critic,试图让 Agent 每一步都正确。这条路会无限膨胀,而且永远有漏洞——你写的每一条约束,都在制造一个新的模糊地带。
- 进化思路:允许它乱试,只保证三件事——每步代价固定(5 分钟)、状态可精确回滚(git)、适应度不可伪造(只读评测)。然后让时间筛选。
这套东西能成立,靠的不是 Agent 的自我一致性,而是选择压力。
代价也很清楚:你放弃了可解释性。分支上留下的是一串「不知道为什么有效」的改动,其中不少是偶然发现。README 开头那段玩笑——“code is now a self-modifying binary that has grown beyond human comprehension”——其实点到了真实问题,不只是段子。
十二、这套机制的失效边界
如果你想把这三个零件迁移到别的任务,先看清它在什么条件下会失灵。
第一,单标量是前提,不是简化。
一旦目标变成多目标(既要效果好,又要延迟低,又要可解释),就没有单一适应度。「淘汰」这个动作失去定义——你无法判断一次改动是「变好了一点」还是「在另一维上变差了」。整个机制立刻瘫痪。
这是它无法直接搬到真实科研的根本原因:真实科研的目标是多元的,而且会随着发现而改变。
第二,短视进化。
5 分钟窗口内不显效的改动会被永久淘汰。一个需要更长训练才能体现价值的架构创新,在这里是负分。进化只认「立刻有用」,它不会为一个可能三年后才成立的假设保留火种。
第三,噪声污染谱系。
单 seed、单次运行,val_bpb 的抖动可能和真实提升同量级。一次假阳性 keep 会让后续所有实验建立在噪声上——而且它会一路 keep 下去,因为分支只前进不回退。
analysis.ipynb 里有一行很说明问题的筛选:below = valid[valid["val_bpb"] <= baseline_bpb + 0.0005]。图里只画落在基线附近 0.0005 之内的点,keep 是绿色实心,discard 是浅灰小点。你去看那张图会发现,绿点和灰点几乎挤在同一条带里,纵轴上分不开。
这就是这套系统最实际的风险:它不会告诉你,你保留了哪些运气。
要缓解,就得在 program.md 里加一条「候选改动跑两次取均值」,代价是实验吞吐减半。这就是一个真实的权衡,没有免费答案。
第四,md 不能自指。
默认 program.md 由人改,Agent 改不了。如果放开,让 Agent 也能改自己的 program.md,那就是真正的自指进化——也是「目标漂移」最危险的形态,因为它可以自己把约束改松:把「不能改 prepare.py」改成「尽量不改 prepare.py」,把「只用已有依赖」改成「谨慎引入依赖」。
这是整个项目里最有研究价值、也最该设护栏的接口。
十三、迁移:把三个零件变成一张检查表
如果你的任务不是训练模型,这套东西还能不能用?我的判断是:能用,但只在满足三个条件时能用。
- 有一个可以自动计算的标量指标。 不是「代码质量好不好」,而是「测试通过率」「端到端延迟 P99」「每月云账单」。必须是算出来的,不是评出来的。
- 每次尝试的代价可控且固定。 不能是「有时候 1 分钟,有时候 3 小时」。固定预算才能让实验可比。
- 状态可以精确回滚。 最好是代码;如果涉及数据库、外部 API 副作用,回滚就变成一件需要设计的事,难度会上升一个量级。
三条都满足,就可以照搬这套骨架:
| 要素 | 要回答的问题 | autoresearch 的答案 |
|---|---|---|
| 变异 | 谁产生候选改动,改哪里 | Agent 改 train.py 的超参区 |
| 适应度 | 用什么算分,谁不能碰 | evaluate_bpb,在只读文件里 |
| 遗传 | 好的存哪,坏的怎么退 | git 分支 + reset |
| 记忆 | 试过什么,怎么不被回滚 | results.tsv,不入 git |
| 带宽 | 什么信息回流给 Agent | 只有 val_bpb 和显存两个数 |
| 反熵 | 怎么防止越改越复杂 | simplicity criterion |
| 无人值守 | 什么情况下允许停下来 | 只在被人类打断时停 |
三条里只要缺一条,我的建议是先别做自进化。缺指标,你会得到自我感觉良好的系统;缺固定预算,你会得到不可比的实验;缺回滚,你会在某个早上发现所有东西都坏了而且回不去。
十四、最后:一个可带走的判断
回到最初那个问题。
「怎么打造一个自进化的 Agent」——答案不是让它更会改自己,而是给它一个它伪造不了的分数,和一条它能退回去的路。
前者让它无法自欺,后者让它的失败不致命。有了这两样,哪怕变异算子很笨(一个只会改超参的循环),系统也会慢慢变好;缺了这两样,哪怕模型再强,你得到的也只是一段看起来很努力的活动记录。
如果只让我留一句话:自进化不是「让 Agent 修改自己」,是「让修改这件事可以被淘汰」。
留一道题给你。假设你把这套东西搬到一个「修 bug」的任务上:变异是 Agent 改代码,遗传是 git,适应度用「测试通过数」。现在加入一个条件——这个仓库的测试有 3 个是长期失败的(flaky),任何人跑都会随机挂。请问:
- 你的适应度函数该怎么改,才能不让 Agent 把「避开 flaky 测试」当成主要策略?
- 如果 Agent 提交了一个「让 flaky 测试通过」的改动,但它其实是把断言改松了,你的哪一层机制能拦住它?
能答清第二问,基本就理解了这套设计的重心在哪一层。
参考资料
- karpathy/autoresearch。本文所有引文与行号来自该仓库 master 分支(commit
228791f)的README.md、program.md、train.py、prepare.py。 - karpathy/nanochat。
train.py是其单卡精简版,包含 Muon + AdamW 混合优化器的完整实现。



