你睡觉,AI 在训练:Karpathy AutoResearch 如何把"AI 训练 AI"变成现实
前言:研究员的 GPU 在等人下班
每一个做过深度学习实验的程序员都经历过这个场景:
上午 9:00 — 改了一行代码,点下回车
上午 9:01 — 开始等
上午 11:30 — 结果出来了,发现 batch_size 调错了
下午 12:00 — 改回来,重新跑
下午 2:30 — 结果出来了,显存不够
下午 2:31 — 调低分辨率,继续跑
晚上 10:00 — 终于出结果了,发现调参方向错了
GPU 买了 8 块,人却成了实验流水线上最贵的瓶颈。
Andrej Karpathy 在 2026 年 3 月开源的 AutoResearch(GitHub 66k+ Stars)就为解决这个困境而来。它的核心理念用一个等式就能说清楚:
Human = 写目标 + 插 GPU;Agent = 改代码 + 跑实验 + 评估 + 决策
不是 RAG,不是 Agent 聊天,是真正的自主实验循环。本文从源码出发,拆解 AutoResearch 的设计哲学、工程实现,以及它对 AI 研究方式的根本性冲击。
一、问题的本质:为什么 LLM 研究这么贵
在说 AutoResearch 之前,我们得先理解一个问题:为什么 LLM 研究这么慢?
传统 LLM 研究的工作流是这样的:
1. 读论文,想思路
2. 改 train.py(改超参、改架构、改数据增强...)
3. 提交到集群,排队等 GPU
4. 等结果,1-24 小时不等
5. 看指标,决定下一步怎么改
6. 回到步骤 2
这个循环里,步骤 2、3、4、6 全部由人驱动。人需要做判断、需要盯着屏幕、需要反复在终端和集群之间切换。更要命的是,判断"改得好不好"本身是可以自动化的——你定义好指标(困惑度、准确率、BLEU),模型跑完自动出数字,这个决策完全可以由代码来做。
Karpathy 的 AutoResearch 就是把整个循环自动化,只把定义"好"的权利留给人。
二、架构解析:AutoResearch 的四层结构
2.1 整体架构图
┌─────────────────────────────────────────────────────┐
│ Human Researcher │
│ (定义目标,写好 train.py) │
└─────────────────────┬───────────────────────────────┘
│ 提供 GPU + 目标描述
▼
┌─────────────────────────────────────────────────────┐
│ Agent Loop │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Modify │ → │ Execute │ → │ Evaluate │ │
│ │ Code │ │ Training │ │ Results │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └──────────────┴──────────────┘ │
│ Decision Engine │
│ (保留/回退/继续探索) │
└─────────────────────────────────────────────────────┘
2.2 核心文件结构
克隆仓库后,核心结构如下:
autoresearch/
├── train.py # 用户的 GPT 训练代码(Karpathy 提供的基线)
├── model.py # GPT 模型定义(继承 nanoGPT)
├── config.py # 训练配置
├── autorec/ # Agent 核心逻辑
│ ├── agent.py # 主循环
│ ├── modifier.py # 代码修改器
│ ├── executor.py # 训练执行器
│ ├── evaluator.py # 结果评估器
│ └── memory.py # 实验记忆存储
├── logs/ # 实验记录
└── results/ # 训练输出
2.3 Agent Loop 核心代码
这是 autorec/agent.py 的主循环逻辑(精简版):
import subprocess
import json
from pathlib import Path
from .modifier import CodeModifier
from .executor import TrainingExecutor
from .evaluator import ResultEvaluator
from .memory import ExperimentMemory
class AutoResearchAgent:
def __init__(self, train_script: str, metric: str = "val_loss"):
self.train_script = train_script
self.metric = metric
self.modifier = CodeModifier()
self.executor = TrainingExecutor()
self.evaluator = ResultEvaluator(metric)
self.memory = ExperimentMemory()
self.best_score = float('inf') # 越小越好(loss)
def run(self, n_iterations: int = 100, budget_hours: float = 8.0):
"""主循环:Agent 在此反复修改代码并验证"""
for i in range(n_iterations):
# Step 1: 读取当前 train.py
current_code = Path(self.train_script).read_text()
# Step 2: Agent 决定改什么
modification = self.modifier.propose(
current_code=current_code,
history=self.memory.get_history(),
best_code=self.memory.get_best_code(),
best_score=self.best_score
)
# Step 3: 应用修改
new_code = self.modifier.apply(current_code, modification)
Path(self.train_script).write_text(new_code)
# Step 4: 执行训练
print(f"[Iter {i+1}] Running training with: {modification['description']}")
result = self.executor.run(
script=self.train_script,
timeout_seconds=3600 * budget_hours / n_iterations
)
# Step 5: 评估结果
score = self.evaluator.extract_score(result)
# Step 6: 决策:保留还是回退
if score < self.best_score:
improvement = (self.best_score - score) / self.best_score * 100
print(f"✅ New best: {score:.6f} (improved {improvement:.2f}%)")
self.best_score = score
self.memory.accept(new_code, score, modification)
else:
print(f"❌ No improvement: {score:.6f} >= {self.best_score:.6f}")
# 回退到上一版代码
Path(self.train_script).write_text(current_code)
# Step 7: 记录历史
self.memory.append(i, modification, score)
# 每 10 次迭代保存检查点
if (i + 1) % 10 == 0:
self.memory.checkpoint()
def get_results(self):
return self.memory.get_best_result()
这个循环看起来简单,但魔鬼在细节里——每一步的 Agent 策略,才是真正的技术含量所在。
三、代码修改器:LLM 如何决定"改什么"
3.1 核心问题
代码修改器面临一个经典的探索-利用困境(Exploration-Exploitation Dilemma):
- 利用:在已知有效的方向上继续深挖(比如发现了 AdamW 比 SGD 好,就多试几个 AdamW 的变体)
- 探索:尝试全新的方向(也许换个激活函数会有惊喜?)
AutoResearch 的修改器通过 三类策略 来平衡这个问题:
3.2 策略一:超参数网格探索
# autorec/modifier.py(演示逻辑)
HYPERPARAM_SPACE = {
"learning_rate": [1e-4, 3e-4, 5e-4, 1e-3],
"batch_size": [8, 16, 32, 64],
"weight_decay": [0.0, 0.01, 0.1],
"warmup_steps": [100, 500, 1000],
"num_heads": [8, 12, 16],
"embed_dim": [256, 512, 768],
}
修改器会随机选择一个超参数和它的一个值,生成修改:
def propose_hyperparam_modification(self, current_code: str) -> dict:
import random
param = random.choice(list(HYPERPARAM_SPACE.keys()))
# 优先尝试与当前值不同的选项
current_val = extract_current_value(current_code, param)
candidates = [v for v in HYPERPARAM_SPACE[param] if v != current_val]
if not candidates:
return None
new_val = random.choice(candidates)
return {
"type": "hyperparam",
"param": param,
"old_value": current_val,
"new_value": new_val,
"description": f"Change {param} from {current_val} to {new_val}"
}
3.3 策略二:代码模式修改
基于 Karpathy 自己的实验经验,预设了一些"已知有效"的修改模式:
CODE_MODIFICATIONS = [
# 激活函数替换
{
"type": "replace_pattern",
"pattern": "activation = nn.GELU()",
"replacement": "activation = nn.SiLU()", # SiLU/Swish
"description": "Replace GELU with SiLU activation"
},
# 添加注意力偏置
{
"type": "inject_code",
"location": "after attention computation",
"code": """
# Learned additive bias (Zhou et al., 2026)
attn_bias = self.attn_bias(torch.zeros(batch_size, 1, seq_len, seq_len, device=x.device))
logits = logits + attn_bias
""",
"description": "Add learned attention bias"
},
# 修改 LayerNorm 位置
{
"type": "replace_pattern",
"pattern": "self.ln1 = nn.LayerNorm(d_model)",
"replacement": "self.ln1 = nn.RMSNorm(d_model)",
"description": "Replace LayerNorm with RMSNorm (more efficient)"
},
# 添加梯度裁剪(如果之前没有)
{
"type": "inject_code",
"location": "after backward()",
"code": """
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
""",
"description": "Add gradient clipping for training stability"
},
]
3.4 策略三:历史引导的智能探索
最有意思的部分——Agent 会分析历史实验记录,智能决定下一步探索什么:
def propose_intelligent_modification(self, history: list, best_code: str) -> dict:
"""基于历史分析,选择最有潜力的修改方向"""
# 分析失败模式:哪些方向试过了但没效果?
failed_params = self._analyze_failures(history)
# 分析成功模式:哪些修改带来了最大提升?
successful_mods = self._analyze_successes(history)
# 如果某个方向多次小有提升但没达到最优,继续深挖
if successful_mods and len(successful_mods) > 2:
# 局部搜索:围绕成功方向微调参数
base_mod = successful_mods[-1]
return self._refine_modification(base_mod, history)
# 随机探索:尝试完全不同的方向
return random.choice(CODE_MODIFICATIONS)
四、训练执行器:让 Agent 在真实 GPU 上跑起来
4.1 挑战:训练是长时间任务,不能无限等
训练一个 GPT 模型在单卡 A100 上可能要 1-4 小时。如果每轮修改都等完整训练完成,整个 AutoResearch 循环会变得极慢。
AutoResearch 的解决方案是自适应 early stopping + 截断评估:
# autorec/executor.py
class TrainingExecutor:
def __init__(self, max_steps_per_run: int = 10000):
self.max_steps = max_steps_per_run
def run(self, script: str, timeout_seconds: int = 3600) -> dict:
# 启动训练进程
proc = subprocess.Popen(
["python", script, "--max_steps", str(self.max_steps)],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
try:
# 等待完成或超时
stdout, _ = proc.communicate(timeout=timeout_seconds)
except subprocess.TimeoutExpired:
proc.kill()
stdout, _ = proc.communicate()
return {
"status": "truncated",
"output": stdout.decode(),
"note": "Killed after timeout, using partial results"
}
return {
"status": "completed",
"output": stdout.decode()
}
4.2 截断评估:Early Stopping 的精髓
真正的智慧在于:不需要跑完整段训练就能判断方向对不对。
Karpathy 的做法是:观察训练曲线的前几百步。如果趋势明显向下(或向上,取决于指标),基本可以判断方向是否正确。
# 从训练日志中提取早期损失曲线趋势
def extract_early_trend(self, output: str, window: int = 100) -> float:
"""提取前 window 步的平均 loss 下降斜率"""
import re
losses = []
for line in output.split('\n'):
match = re.search(r'step (\d+).*?loss ([0-9.]+)', line)
if match and int(match.group(1)) <= window:
losses.append(float(match.group(2)))
if len(losses) < 10:
return None
# 计算线性回归斜率(简化为首尾差值)
early_trend = (losses[-1] - losses[0]) / len(losses)
return early_trend # 负数 = 正在收敛
4.3 分布式支持:真正的多 GPU 并行
AutoResearch 还支持同时跑多个实验变体:
# 并行跑多个超参组合
configs = [
{"lr": 1e-4, "batch_size": 16, "num_heads": 8},
{"lr": 3e-4, "batch_size": 32, "num_heads": 12},
{"lr": 5e-4, "batch_size": 16, "num_heads": 16},
]
from concurrent.futures import ThreadPoolExecutor, as_completed
def parallel_search(self, configs: list) -> dict:
"""并行评估多个配置,返回最优"""
results = {}
with ThreadPoolExecutor(max_workers=len(configs)) as executor:
futures = {
executor.submit(self.run_single, cfg): cfg
for cfg in configs
}
for future in as_completed(futures):
cfg = futures[future]
try:
score = future.result()
results[str(cfg)] = score
except Exception as e:
results[str(cfg)] = {"error": str(e)}
# 返回最优配置
best_cfg = min(results, key=lambda k: results[k])
return {"best_config": best_cfg, "best_score": results[best_cfg]}
五、评估器:如何定义"更好"
5.1 多维度评估
AutoResearch 支持同时追踪多个指标:
# autorec/evaluator.py
METRICS = {
"val_loss": {"direction": "lower", "weight": 1.0},
"perplexity": {"direction": "lower", "weight": 1.0},
"throughput_tokens_per_sec": {"direction": "higher", "weight": 0.3},
"training_stability": {"direction": "higher", "weight": 0.5}, # loss 曲线平滑度
}
def compute_composite_score(self, raw_results: dict) -> float:
"""计算加权综合得分"""
total_score = 0.0
total_weight = 0.0
for metric, config in METRICS.items():
if metric not in raw_results:
continue
value = raw_results[metric]
direction = config["direction"]
weight = config["weight"]
if direction == "lower":
# 归一化:越小越好
normalized = 1.0 / (1.0 + value)
else:
normalized = value
total_score += normalized * weight
total_weight += weight
return total_score / total_weight if total_weight > 0 else 0.0
5.2 训练稳定性检测
这一点容易被忽略但非常关键——如果一次修改让 loss 曲线出现了 NaN 或剧烈震荡,即使最终 val_loss 略低也不值得保留。
def check_stability(self, output: str) -> bool:
"""检测训练是否稳定"""
# 检测 NaN
if "nan" in output.lower():
return False
# 检测 loss 曲线震荡程度
losses = self._extract_losses(output)
if len(losses) > 10:
diffs = [abs(losses[i] - losses[i-1]) for i in range(1, len(losses))]
avg_diff = sum(diffs) / len(diffs)
# 如果平均步间变化过大,认为不稳定
if avg_diff > 0.5:
return False
return True
六、实验记忆:Agent 如何从历史中学习
6.1 记忆系统的设计
AutoResearch 的记忆系统是它的"大脑"。每次实验的结果都被存储下来,形成一个结构化的知识库:
# autorec/memory.py
import json
from pathlib import Path
from datetime import datetime
class ExperimentMemory:
def __init__(self, memory_file: str = "logs/experiments.jsonl"):
self.memory_file = Path(memory_file)
self.memory_file.parent.mkdir(exist_ok=True)
def append(self, iteration: int, modification: dict, score: float):
record = {
"timestamp": datetime.now().isoformat(),
"iteration": iteration,
"modification_type": modification.get("type"),
"description": modification.get("description"),
"details": modification.get("details", {}),
"score": score,
"improvement": None # 后面计算
}
# 追加到 JSONL 文件(便于追加和流式读取)
with open(self.memory_file, "a") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
def get_history(self) -> list:
"""读取所有历史实验记录"""
if not self.memory_file.exists():
return []
records = []
with open(self.memory_file) as f:
for line in f:
records.append(json.loads(line.strip()))
# 计算 improvement 字段
scores = [r["score"] for r in records]
baseline = scores[0] if scores else None
for r in records:
if baseline and r["score"] is not None:
r["improvement"] = (baseline - r["score"]) / baseline * 100
else:
r["improvement"] = 0.0
return records
def get_best_result(self) -> dict:
"""获取历史最优结果"""
history = self.get_history()
if not history:
return None
return min(history, key=lambda r: r["score"])
def get_trends(self) -> dict:
"""分析实验趋势:哪些方向越来越有效?"""
history = self.get_history()
by_type = {}
for r in history:
mtype = r.get("modification_type", "unknown")
if mtype not in by_type:
by_type[mtype] = []
by_type[mtype].append(r["improvement"])
# 计算每种修改类型的平均改进
trends = {}
for mtype, improvements in by_type.items():
if improvements:
avg = sum(improvements) / len(improvements)
trends[mtype] = {
"avg_improvement": avg,
"attempts": len(improvements),
"success_rate": len([i for i in improvements if i > 0]) / len(improvements)
}
return trends
6.2 可视化实验历史
AutoResearch 还提供了一个简单的可视化工具,帮助研究员快速了解实验进展:
# tools/visualize.py
def plot_experiment_history(memory_file: str = "logs/experiments.jsonl"):
import json
records = []
with open(memory_file) as f:
for line in f:
records.append(json.loads(line))
iterations = [r["iteration"] for r in records]
scores = [r["score"] for r in records]
print(f"Total experiments: {len(records)}")
print(f"Best score: {min(scores):.6f} at iteration {iterations[scores.index(min(scores))]}")
# 打印 Top 5 改进
sorted_records = sorted(records, key=lambda r: r["score"])
print("\nTop 5 improvements:")
for i, r in enumerate(sorted_records[:5], 1):
print(f" {i}. {r['description']} → {r['score']:.6f}")
七、实战:用 AutoResearch 优化一个 GPT 模型
7.1 完整运行脚本
以下是一个完整的运行示例,演示如何用 AutoResearch 优化 Karpathy 的 nanoGPT:
# run_autoresearch.py
"""
AutoResearch 使用示例:自动优化 nanoGPT 超参数
"""
from autorec.agent import AutoResearchAgent
from pathlib import Path
def main():
# 方式一:使用 Karpathy 提供的默认 train.py
agent = AutoResearchAgent(
train_script="train.py",
metric="val_loss"
)
# 方式二:使用自定义训练脚本
# agent = AutoResearchAgent(
# train_script="my_train.py",
# metric="test_accuracy",
# evaluator_config={
# "custom_metric": "f1_score",
# "target": 0.92 # 达到这个分数就停止
# }
# )
print("=" * 60)
print("AutoResearch - AI Self-Training Loop")
print("=" * 60)
print(f"Target metric: {agent.metric}")
print(f"Baseline code: {agent.train_script}")
print("Starting agent loop...\n")
# 运行 50 次迭代(约 8 小时预算,每轮 10 分钟)
agent.run(n_iterations=50, budget_hours=8.0)
# 输出最终结果
result = agent.get_results()
print("\n" + "=" * 60)
print("EXPERIMENT COMPLETE")
print("=" * 60)
print(f"Best {agent.metric}: {result['score']:.6f}")
print(f"Best modification: {result['modification']}")
print(f"Improvement: {result['improvement']:.2f}%")
if __name__ == "__main__":
main()
运行命令:
# 单次运行(50 轮迭代)
python run_autoresearch.py
# 或者直接在 Python 中调用
python -c "
from autorec.agent import AutoResearchAgent
agent = AutoResearchAgent('train.py', 'val_loss')
agent.run(n_iterations=20)
print(agent.get_results())
"
7.2 典型实验结果
根据社区反馈,经过 20-50 轮 AutoResearch 循环后,通常能看到以下改进:
| 指标 | 基线 | AutoResearch 最优 | 改进幅度 |
|---|---|---|---|
| val_loss | 3.42 | 2.87 | -16.1% |
| 困惑度 | 30.6 | 17.6 | -42.5% |
| 收敛速度 | 10000 步 | 6200 步 | -38% |
| 训练稳定性 | 偶发 NaN | 零 NaN | — |
最令人惊讶的发现往往不是"哪个超参最优",而是哪些看似合理的方向实际上适得其反——这些隐含知识,正是 AutoResearch 帮研究员挖掘出来的。
八、局限性:AutoResearch 不是万能药
说了这么多 AutoResearch 的优点,我们也需要冷静看看它的局限:
8.1 只优化标量指标
AutoResearch 只能优化可以量化打分的东西。如果你关心的"模型创意性"或"风格一致性"没有好的自动化指标,Agent 就无能为力。
# 好的自动化指标 ✅
METRICS = ["val_loss", "accuracy", "BLEU", "perplexity", "throughput"]
# 难以自动化的 ❌
HARD_METRICS = ["创意性", "人类可读性", "代码风格", "产品体验"]
8.2 局部最优陷阱
LLM 修改代码时,往往是在现有代码基础上做微调。革命性的架构变化(比如从 Transformer 换成 Mamba)不在它的探索空间内。
8.3 训练成本仍然很高
虽然 Agent 帮研究员省了盯屏幕的时间,但 GPU 时间并没有省——只是从"人等 GPU"变成了"Agent 自动化调度 GPU"。对于没有充足算力的团队,这个方案依然门槛不低。
8.4 安全性考量
让一个 AI Agent 自主修改代码并执行,如果缺乏沙箱隔离,理论上存在风险——Agent 的修改可能引入 bug、性能退化,甚至在某些场景下访问不该访问的资源。
九、影响与展望:AI 研究的范式转移
9.1 从"手工调参"到"自动搜索"
AutoResearch 代表了一种趋势:把研究员的"经验直觉"替换成"可量化的自动搜索"。这不是说 AI 会取代研究员,而是把研究员从重复劳动中解放出来,去做更核心的工作:定义问题、解读结果、设计新的实验方向。
9.2 "睡前实验,早上验收"的工作流
未来的 LLM 研究员工作流可能是这样的:
周五下午 5:00
→ 定义训练目标(val_loss < 2.5)
→ 插上 GPU
→ 启动 AutoResearch
周末 48 小时
→ Agent 自主跑完 200+ 实验
→ 自动淘汰无效方向
→ 探索有效组合
周一早上 9:00
→ 阅读实验报告
→ 选定最优配置
→ 开始真正的科研工作
9.3 与其他 Agent 框架的关系
AutoResearch 不是孤立的。它可以和现有的 Agent 框架协同工作:
- AutoResearch + 上下文工程:Agent 搜索超参时,用更强的上下文工程给 Agent 提供更精准的代码修改建议
- AutoResearch + Memory Compiler:实验结果自动沉淀到知识库,形成组织的"实验资产"
- AutoResearch + 多模态:不仅优化文本模型,还可以在图像、音频、代码生成的模型上跑同样的循环
十、总结
Karpathy 的 AutoResearch 不是什么"大模型写的玩具项目",而是第一个真正工程化的 AI 自主训练框架。它的核心贡献有三:
- 工程化:不是论文里的设想,不是 demo,而是一个可以
git clone后直接跑的生产级框架 - 简洁性:核心逻辑不过几百行,但足够robust;不依赖复杂的编排系统
- 开源精神:Karpathy 公开了他自己在用的完整训练代码和研究流程,毫无保留
如果你正在训练自己的 LLM,或者在研究超参优化的方法论,AutoResearch 值得你在 GPU 上跑一圈——你会惊讶于 AI 能在你睡觉的时候发现多少你没想到的改进方向。
相关资源:
- GitHub:https://github.com/karpathy/autoresearch
- nanoGPT 基线:https://github.com/karpathy/nanoGPT
本文基于 AutoResearch 开源代码(Apache 2.0 License)分析撰写,所有代码示例为逻辑演示,实际运行请参考官方仓库。