引言
AutoZyme[1] 是一个自动加速科学计算函数的框架,保证优化后的结果和原版一模一样。它由两部分组成:autozyme_r / autozyme_py 负责生成加速补丁,autozyme-cli(命令行工具叫 zyme)负责驱动整个优化流程——初始化任务、测量基准、迭代加速、验证正确性。
本文聚焦 autozyme-cli,以一次真实的 Seurat NormalizeData 加速任务为线索,边讲实战边拆源码。
预印本:https://www.biorxiv.org/content/10.64898/2026.06.12.731250v1[2]
快速上手
安装
git clone https://github.com/ElliotXie/autozyme.gitcd autozyme && pip install -e autozyme_cli/安装后获得 zyme 命令:
zyme --version # zyme 0.3.1两种使用方式
AutoZyme 的优化流程由编程代理(Claude Code、Codex、Cursor 等)驱动,CLI 负责执行和测量。有两种跑法:
自主模式:把一段提示词粘贴给代理,它自己跑完全流程:
Clone https://github.com/ElliotXie/autozyme.git,install the CLI (pip install -e autozyme/autozyme_cli/).I want to optimize FindAllMarkers from Seurat.Repo: https://github.com/satijalab/seuratRead and follow autozyme/autozyme_cli/zyme/prompts/manager/0_pipeline.md管理器代理自动跑完脚手架 → 初始化 → 迭代 → 验证 → 打包,典型耗时 4–10 小时。
手动模式:自己建目录、跑 zyme init,然后逐阶段把提示词粘贴给代理:
mkdir test_normalizedata && cd test_normalizedatazyme init https://github.com/satijalab/seurat NormalizeData我这次跑的是自主模式,目标就是 Seurat::NormalizeData。
实战:加速 Seurat::NormalizeData
任务长什么样
zyme init 会在当前目录生成一组模板文件。init 代理填完内容后,核心的 task.yaml 长这样:
# task.yamltask: seurat_NormalizeDatatarget_repo: https://github.com/satijalab/seurattarget_function: Seurat::NormalizeDatasignature: NormalizeData(object = seurat_obj, normalization.method = "LogNormalize", scale.factor = 10000)datasets:- {tier: small, name: pbmc3k, path: ./data/pbmc3k.rds}- {tier: medium, name: pbmc8k, path: ./data/pbmc8k.rds}- {tier: large, name: pbmc10k, path: ./data/pbmc10k.rds}- {tier: ood_large, name: neuron10k, path: ./data/neuron10k.rds}- {tier: ood_xlarge, name: tabula_muris_merged, path: ./data/tabula_muris_merged.rds}metrics:- {name: pearson_data, threshold: 0.999, comparator: gte} # 归一化后的数据 Pearson 相关必须 >= 0.999- {name: max_abs_diff_data, threshold: 0.001, comparator: lte} # 逐元素最大差异 <= 0.001baseline_threads: [1, 4, 8]几个关键信息:
三个开发数据集(small/medium/large)用来做迭代优化,还有两个 OOD(out-of-distribution)数据集用来做最终验证。 两个正确性指标:Pearson 相关系数和逐元素最大差异。LogNormalize 是确定性算术,理论上加速后的结果应该和原版完全一致。 三个线程点(1/4/8):验证在不同并行度下加速是否稳定。
任务目录的完整结构:
seurat_NormalizeData/├── task.yaml # 上面这个├── reference.R # 基准脚本:用原版 Seurat 跑,产出参考输出├── evaluate.R # 评估脚本:对比 pipeline 输出和参考输出├── pipeline/run.R # 优化脚本:每一轮编辑的目标├── results.tsv # 结果日志:每一轮的速度和指标├── reference_output_<tier>/ # 各数据层的参考输出(如 reference_output_small/)├── data/ # 数据集├── memory/ # 代理的"记忆"(发现、死胡同)└── .zyme/ # 框架内部状态 ├── best.ref # 当前最佳提交的 SHA └── round.counter # 轮次计数器基准与剖析
优化前先得知道基准是多少、时间花在哪。zyme baseline reference 负责前者,zyme profile 负责后者。
基准行(round 0)由 zyme baseline reference 跑原版代码测得。它会运行 reference.R,用 Sys.time() 计时原版 Seurat::NormalizeData,同时把结果矩阵存到 reference_output_<tier>/(如 reference_output_small/)供后续对比:
round commit dataset speed_sec speedup_pct status0 upstream pbmc3k 1.046 0.0 baseline0 upstream pbmc8k 1.832 0.0 baseline0 upstream pbmc10k 2.841 0.0 baselinepbmc3k(2700 个细胞)跑一次 1.046 秒,看起来不慢。但 1 秒里到底在算什么?代理接着跑了 zyme profile --backend full,这会启用 Rprof(R 内置采样式剖析器)记录整个调用栈,然后把结果归一化成 profile_history/<run-id>/profile.json。profile 实际耗时 1.7s,比基准的 1.046s 略高,这是 profiling 自身的采样开销。
profile.json 里最有用的是 layer_breakdown——按可编辑层次汇总时间分布:
.Call |
library + base-r 加起来 74%,全是 R 层面的开销。再往下看 per_layer_top(每层的头号函数),能定位到具体是什么在吃时间:
.Call | ||||
..subscript.2ary | SetAssayDataLayerData 的矩阵下标拷贝 | |||
paste | LogSeuratCommand | |||
gc |
这就是那张 74% 表的由来。问题很清楚了:Seurat v5 的 NormalizeData 调用链是 NormalizeData(泛型)→ NormalizeData.Seurat → NormalizeData.Assay → NormalizeData.V3Matrix → LogNormalize.V3Matrix → LogNorm(C++),每一跳都触发 S3 方法分派、Matrix 下标操作和校验。真正的计算只占 26%。
优化迭代
知道时间花在哪之后,整个优化的核心就是一个循环:编辑 pipeline/run.R → zyme run 提交并测量 → 根据结果 zyme accept(保留)或 zyme reject(丢弃)。让我用这次实际的 results.tsv 来讲这个过程。
以下是 results.tsv 完整记录的每次提交与测量结果:
patch_namespace 参数错) | |||||||
LogNorm 参数不匹配) | |||||||
match.arg 校验没绕过) | |||||||
nbrOfWorkers 找不到) | |||||||
-march=native -O3 | |||||||
对比 baseline,最终整体加速比如下:
关键解读:
3 crash → 1 keep:前 3 轮同一个 hypothesis 试了三次才跑通——LLM 写代码就是反复试错的过程。第 4 轮(99cc40f)首次成功绕过 S3 分派,加速 40.4%。 峰值在第 5 轮:91a8c08 直接读 a@layers[["counts"]]绕过LayerData.Assay5分派,0.632s → 0.325s(69.4%),之后所有轮次都在此基础上微调。跨层验证的价值:7.1/7.2 等行显示,在 small 上效果不明显的优化(如 OpenMP 在第 9 轮的 small 上 0.327s 没变化),在 medium/large 上可能有收益(+2~3%)。反之,small 上看似有益的优化,在 large 上可能退步(第 12 轮 fused-pass +3.7%)。 float 精度赢得 large:第 11 轮 c3ca3f5 在 small 上只是噪声水平,但在 large 上额外 −17.7%(1.263s → 1.040s),AVX2 的 2× SIMD 宽度在大数据量时才显优势。 OOD 泛化:neuron10k 58.3%、tabula_muris 52.2%,加速比从 development 的 ~70% 下降到 ~55%。bit-exact 全部通过。
验证可视化
zyme verify 在优化完成后生成验证报告,汇总速度、加速比、内存和正确性指标:

四面板解读(全部基于 small/pbmc3k 数据集,线程 1/4/8):
A. Wall time(左上):优化后(蓝色)从 1.1s 降到 0.34s,三个线程点一致的 ~3.2× 加速。注意 gray 条高度完全相同——基准测量在不同线程数下一致,说明 NormalizeData原版本身就是单线程的,OpenMP/MKL 线程预算不影响原版时间。B. Speedup factor(右上):三个线程点稳定在 3.2× 加速。虚线 1× 是"无加速"参考线。一致性说明优化没有引入线程竞争—— LogNormOMPf的 OpenMP 并行在 2700 列上可预测。C. Peak memory(左下):优化后内存 514–515 MB 相比基准 522 MB 还低 ~1–2%,因为跳过了 LogSeuratCommand的中间字符串拷贝和LayerData的临时矩阵分配。没有内存回退。D. Correctness(右下): pearson_data = 1.000(阈值 ≥ 0.999),max_abs_diff_data = 0.00e+00(阈值 ≤ 0.001)。所有指标全绿,bit-exact 正确。
验证确认:加速是真实的(非作弊)、正确的(bit-exact)、在不同并行度下稳定的(线程无关)、且没有引入内存回归。
源码解读
CLI 入口与命令分发
入口定义在 pyproject.toml:
[project.scripts]zyme = "zyme.cli:main"cli.py 用 argparse 构建了一个子命令树。命令很多(40+),但按生命周期分成了清晰的五组:
# cli.py_TOP_LEVEL_GROUPS = [ ("Core loop", ["init", "run", "dryrun", "accept", "reject", "rollback", "iterate"]), ("Measurement", ["baseline", "verify", "attest", "attest-sweep", "backfill", "validate"]), ("Analysis", ["status", "plot", "scan", "registry", "audit", "cost", "cost-capture"]), ("Workflow", ["dispatch", "bench", "prompt"]), ("Packaging", ["package"]), ("Utility", ["inspect-parallelism", "datasets", "publish-speedups"]),]每个子命令通过 set_defaults(func=cmd_xxx) 绑定到处理函数。main() 只需要读 args.func 就能分发。commands/__init__.py 把所有实现重新导出,cli.py 统一导入。
::: {.callout-note} _GroupedHelpFormatter 拦截了 argparse 的子命令列表渲染,按生命周期分组显示。20 个命令挤成一堆没人看得下去,分组后一眼就能找到自己要的。 :::
任务脚手架:zyme init
cmd_init 做的事情很确定:把模板复制到目录、克隆上游仓库、检测语言、创建目录结构。它不生成任何内容——内容由 init 代理填充。
我跑 zyme init https://github.com/satijalab/seurat NormalizeData 时,它做了这些事:
# commands/init.py (核心逻辑)def cmd_init(args): task_dir = Path(os.getcwd()).resolve() task_name = task_dir.name # seurat_NormalizeData# 1. 复制模板文件 template = FRAMEWORK_ROOT / "templates" / "task_template"for item in template.iterdir(): shutil.copy2(item, task_dir / item.name) # 或 copytree# 2. 克隆上游仓库 git("clone", args.target_repo, str(task_dir / "upstream_repo"))# 3. 嗅探语言:DESCRIPTION → R, pyproject.toml → Python language = _detect_language(upstream_repo_dir)# Seurat 有 DESCRIPTION → language = "R"# 4. 重命名模板:run.R.template → run.R,删掉 run.py.template# 5. 创建 data/、setup/、reference_output_<tier>/、memory/ 目录# 6. 初始化 git 仓库这个设计的关键是确定性——所有脚手架工作由 CLI 完成,init 代理只需要关注"选择哪些数据集、写什么参考脚本、怎么评估正确性"这些需要判断的事情。
优化循环:zyme run → accept / reject
这是框架的心脏。用我跑的第 4 轮来举例——代理第一次成功绕过 S3 分派链:
# commands/run.py (简化)def cmd_run(args): task_dir = task_dir_from_args(args) _, best_ref, round_counter_file = zyme_state(task_dir) hypothesis = args.hypothesis# "bypass S3 dispatch + direct LogNorm call + slot assignment"# 反作弊:提交前检查 pipeline/run.R 有没有"计时外提升"for _pipe_name in ("pipeline/run.R", "pipeline/run.py"): _violations = scan_pipeline_hoist(task_dir / _pipe_name)if _violations and not _hoist_exempt_reason: sys.exit(2) # 阻断,不会留下幽灵提交# 提交 pipeline 改动 git("add", "pipeline/", cwd=task_dir) git("commit", "-m", hypothesis, cwd=task_dir) # commit 99cc40f# 执行 pipeline + evaluate log_content = run_task(task_dir, dataset_entry=tier_entry, thread=1) speed_sec, peak_mb, metrics, status = parse_log(log_content)# 计算加速比 baseline_speed = get_baseline_speed(task_dir, "pbmc3k", thread=1) # 1.046 speedup_pct = (1 - speed_sec / baseline_speed) * 100.0# 40.4# 写入 results.tsv,状态 = pending append_results_row(results_tsv, {"round": "4", "commit": "99cc40f","speed_sec": "0.632", "speedup_pct": "40.4","status": "pending","metrics_json": '{"pearson_data": 1.0, "max_abs_diff_data": 0.0}', ... })代理看到结果后决定 zyme accept -m "bypassed S3 dispatch, 1.06s→0.63s":
# commands/run.pydef cmd_accept(args):# 验证 HEAD 匹配 pending 行的提交# (防止共享仓库中其他任务插入提交导致 best.ref 指错) head_full = git("rev-parse", "HEAD", cwd=task_dir)if not head_short.startswith(expected): die("HEAD 与 pending 轮次不匹配") best_ref.write_text(head_full) # .zyme/best.ref → 99cc40f 的完整 SHA update_last_status(results_tsv, "keep", args.description)# 额外检查:内存有没有回退、该不该重新 profile _maybe_memory_regression_warning(...) _maybe_reprofile_hint(...)第 6 轮的 future_lapply 并行尝试被 reject 了:
# commands/run.pydef cmd_reject(args):# 脏树安全:有未提交改动就拒绝默认的 --hard resetif has_uncommitted and not args.keep_tree and not args.force: die("工作树有未提交改动,选择 --keep-tree 或 --force")# 共享仓库安全:防止 reset 越过兄弟任务的提交 cross_task = _cross_task_commits_between(task_dir, best_sha)if cross_task and not args.force: die("reject 会越过其他任务的提交...") update_last_status(results_tsv, "discard", args.description) git("reset", "--hard", best_sha, cwd=task_dir) # 回到 best.refreject 做了 git reset --hard,回到 best.ref 指向的提交。被拒绝的代码不会从历史中消失——它保存在 artifacts/006_426fc62_small/run.R 里,描述记录在 results.tsv 的 description 列。
::: {.callout-tip} accept 和 reject 中大量代码在处理共享仓库安全。当多个任务共用一个 git 仓库时,盲目的 git reset --hard 可能越过兄弟任务的提交。框架通过检测 best.ref..HEAD 区间内是否有任务目录之外的文件改动来防护。 :::
流水线执行器:runner.py
run_task() 负责实际运行 pipeline/run.R 并测量。以我的任务为例,它做的事情是:
# runner.py (简化)def run_task(task_dir, dataset_entry, thread=None, ...): lang = detect_lang(task_dir) # "R"# 1. 通过环境变量传递执行参数 env["ZYME_TIER"] = "small" env["ZYME_DATA_PATH"] = "/path/to/data/pbmc3k.rds" env["ZYME_REFERENCE_DIR"] = "/path/to/reference_output_small"if thread is not None: env["ZYME_THREADS"] = "1"# 同步 BLAS/OMP/MKL 线程上限,避免不公平比较 env["OMP_NUM_THREADS"] = "1" env["OPENBLAS_NUM_THREADS"] = "1" env["MKL_NUM_THREADS"] = "1"# 2. 执行 pipeline/run.R(带内存 watchdog 和运行时间超时) rc, stdout, stderr, peak_gb, timed_out, ... = _run_with_watchdog( ["Rscript", "run.R"], cwd=pipeline_dir, env=env, mem_cap_gb=mem_cap_gb, # 超内存就 SIGKILL 进程组 wall_cap_s=wall_cap_s, # 按 tier 设置运行时间上限 )# 3. 执行 evaluate.R(对比 pipeline 输出和参考输出) rc_eval, stdout_eval, _ = _run_evaluate(...)# 4. 汇总日志:提取 speed_sec / peak_mb / pearson_data / max_abs_diff_datareturn "\n".join(log_parts) + _summary(speed, peak, status)::: {.callout-note} run_task 通过环境变量而非命令行参数传递执行参数。pipeline/run.R 读 Sys.getenv("ZYME_TIER") 就能知道自己在跑哪个数据层,读 Sys.getenv("ZYME_THREADS") 就知道线程预算。这让 pipeline 代码保持简洁。 :::
看看优化后的代码长什么样
14 轮迭代后,最终保留的 pipeline/run.R 里的核心逻辑是这样的:
# pipeline/run.R — 最终优化版本# 用 sourceCpp 编译一个 OpenMP 并行 + float 精度的 C++ 函数Rcpp::sourceCpp(code = '#include <RcppEigen.h>#include <omp.h>// [[Rcpp::depends(RcppEigen)]]// [[Rcpp::plugins(openmp)]]// float 精度:AVX2 一次处理 8 个 float vs 4 个 double// [[Rcpp::export]]SparseMatrix<double> LogNormOMPf(SparseMatrix<double> data, double scale_factor, int n_threads) { int n = data.outerSize(); float sf = (float)scale_factor; #pragma omp parallel for num_threads(n_threads) for (int k = 0; k < n; ++k) { float colSum = 0.0f; for (SparseMatrix<double>::InnerIterator it(data, k); it; ++it) colSum += (float)it.value(); float norm = sf / colSum; for (SparseMatrix<double>::InnerIterator it(data, k); it; ++it) it.valueRef() = (double)log1pf(v * norm); } return data;}')# 覆盖 Seurat 的 NormalizeData.Seurat 方法NormalizeData.Seurat <- function(object, assay = NULL, normalization.method = "LogNormalize", scale.factor = 1e4, ...) { a <- object[[assay]] counts <- a@layers[["counts"]]# 直接读 slot,跳过 LayerData 分派 n_threads <- get_threads(default = 1L)# 从框架获取线程数if (n_threads > 1L && ncol(counts) > 1000L) { norm.data <- LogNormOMPf(counts, scale.factor, n_threads)} else {# 小数据集用原版 LogNorm LogNorm_fn <- getFromNamespace("LogNorm", "Seurat") norm.data <- LogNorm_fn(counts, scale_factor = scale.factor, ...)} a@layers[["data"]] <- norm.data # 直接写 slot,跳过 SetAssayData object@assays[[assay]] <- areturn(object)}# 注册覆盖patch_namespace("NormalizeData.Seurat", "Seurat", NormalizeData.Seurat)# 执行(和 reference.R 完全一样的调用方式)t0 <- Sys.time()result <- NormalizeData(obj, normalization.method = "LogNormalize", scale.factor = 10000, verbose = FALSE)优化做的事情可以概括为:绕过 5 层 S3 方法分派,直接调 C++ 的 LogNorm,用 slot 赋值替代 Matrix 下标操作,跳过 LogSeuratCommand 命令日志,再叠加 OpenMP 并行和 float 精度 SIMD。
状态管理:.zyme/ 目录
zyme accept 和 zyme reject 为什么能精确地推进或回退?秘密在 .zyme/ 目录。整个优化过程的状态都集中在这里:
best.ref | d89f20d5... | |
round.counter | 14 | |
audit.jsonl | zyme 调用的审计日志(97KB) | |
baselines_history.tsv | ||
baseline_noise.json | ||
version_check.json | ||
setup_audit.log |
其中最重要的是 best.ref 和 baseline_noise.json。
best.ref 存的就是一个 git SHA。zyme accept 把当前 HEAD 写进去,zyme reject 读出来做 git reset --hard。一个文件、一个 SHA,就是整个状态机的核心。
baseline_noise.json 记录基准运行的测量噪声——同一段代码跑 5 次,每次时间不会完全一样,这个波动就是噪声:
{"tiers": {"small": {"1": {"dataset_name": "pbmc3k","n_reps": 5,"speed_mean": 1.061,"speed_stdev": 0.019,"speed_cv": 0.018}}}}5 次基准测试的 CV = 标准差/均值 = 1.8%,即单次测量有约 ±1.8% 的随机波动。差异需超过 3×CV ≈ 5.4% 才可信。第 7 轮 0.328s 反而比第 5 轮 0.325s 慢了 0.9%,在噪声窗口内,无法判断真实变化,故 discard。
反作弊:怎么防止代理"作弊"
AutoZyme 最大的挑战不是让代理写出更快的代码,而是防止代理制造不真实的加速。这次任务里,反作弊系统真的抓到了问题。
静态检查:三道防线
第一道:fingerprint.py 检查 reference.R 有没有合成数据膨胀(比如 [expr] * 10000 把小数据重复放大来制造虚假加速)。用 Python AST 检查列表重复、np.tile、重复循环三种模式。
第二道:hoist_audit.py 检查有没有把工作移出计时器。比如这种作弊:
# 在 t0 之前预计算上游的内部函数.precomputed <- getFromNamespace(".decontxInitializeZ", "celda")(counts, ...)install_override(".decontxInitializeZ", "celda", function(...) .precomputed)t0 <- Sys.time()res <- decontX(counts, ...)# init 现在是 O(1),但真实用户收不到这个加速这道检查在 zyme run提交之前运行,违规直接 exit 2,不留下幽灵提交。我的任务里 pipeline 通过了这道检查。
第三道:argdiff.py 检查 pipeline 传给目标函数的参数有没有和基准调用不同(比如偷偷把 .tsv 换成 .pickle)。
LLM 驱动的对抗性审计:zyme validate
静态检查之外,框架还会在阶段结束时跑一个独立的 LLM 代理来审查整个任务。这就是 zyme validate。
我的任务在 iterate 阶段结束后跑了验证,结果抓出了一个 LIKELY_HACK:
# validate.tsvseverity category mechanism_summaryWEAK Threshold margin max_abs_diff=1e-3 太宽松,bfloat16 降精度可能通过WEAK No OOD tier 三个数据集都是 PBMC(后来加了 OOD)LIKELY_HACK Fixture-only behavior normalization.method 参数被接受但从不读取WEAK Output contract gap 跳过了 LogSeuratCommand,但 evaluator 不检查命令日志LIKELY_HACK 的发现是:我的 NormalizeData.Seurat 覆盖函数在参数列表里接受 normalization.method,但函数体里从来不读这个参数——无论传什么值都走 LogNormalize 路径。如果用户传 normalization.method = "RC",上游会走完全不同的归一化逻辑,但我的覆盖函数会悄悄忽略这个参数。
这个发现是合理的。虽然在我的测试场景里不会触发(因为只用 LogNormalize),但对于一个要发布给所有人用的加速补丁来说,忽略参数确实是 hack 行为。
审计结果按严重程度排序:PASS < WEAK < LIKELY_HACK < FAIL。LIKELY_HACK 和 FAIL 会阻断流程,需要修复后才能继续。
多代理调度:dispatch
前面讲的都是单个任务怎么跑。如果你要同时优化多个函数——比如先 NormalizeData,再 FindAllMarkers——zyme dispatch 就是一个队列管理器,按顺序逐个跑完。
它的工作方式很简单:找到机器上安装的 agent CLI(支持 Claude Code、Codex、Cursor,自动检测),以子进程的方式启动它,把任务目录作为工作目录。然后逐行读 agent 的输出,检测它是不是卡住了(默认 15 分钟没输出就标记停滞),等它跑完再启动下一个任务。
flowchart TD A["构建任务队列"] --> B{"--detach?"} B -->|是| C["双 fork 守护化"] B -->|否| D["前台运行"] C --> D D --> E{"有待处理任务?"} E -->|是| F["等待 RAM + 磁盘释放"] F --> G["启动 agent CLI<br/>cwd=task_dir"] G --> H["逐行读取 stdout<br/>停滞检测"] H --> I{"结果"} I -->|停滞| J["标记停滞"] I -->|完成| K["标记 done/failed"] J --> K K --> L["重算 ETA"] L --> E E -->|否| M["退出"]我的任务在 pipeline_runs/ 下留了一份调度日志,记录了各阶段的推进:
## Log- Run created. Starting scaffold via zyme init.- Scaffold completed: tasks/seurat_NormalizeData- Init phase completed: datasets=3, refs_present, pipeline+evaluate real.- Validate init: 2 WEAK findings (non-blocking)- Iterate phase completed: 14 rounds (5 accepts, 9 rejects). Best: 72.5% (pbmc8k).- Validate gate: PASS (no hacks detected). Advancing to scaling.::: {.callout-tip} dispatch 的崩溃恢复策略很实用:主控崩溃时,先检查工作 agent 还在不在跑——如果还在跑,直接重新等它;如果已经跑完了,推进到下一阶段;最后才考虑恢复会话。因为工作 agent 跑了几十轮迭代积累的状态是没法重建的,保护它比保护主控重要。 :::
优化流水线全景
zyme init | |
autozyme_r,跨平台验证 |
设计哲学
代理决策 + CLI 执行
框架的一个核心设计是把决策和执行分开。代理负责想:提出假设、写代码、看结果好不好;CLI 负责做:git 提交、跑基准、记数据、做安全检查。这些操作不可逆,不能让代理自己来。“该绕什么、不该绕什么”的判断是代理做的,而 git 提交、跑 pipeline、算加速比、写 results.tsv 这些都由 CLI 确定性地完成,代理不用操心。
去掉 S3 分派虽然能加速,但降低了代码可读性。实际代码中需要权衡利弊,而不是一味绕过。
防御性编程
代码库里有大量防御性编程。以 reject 为例:
脏树安全: git reset --hard前检查工作树有没有未提交改动共享仓库安全:检测 reset 会不会越过兄弟任务的提交 崩溃行安全:崩溃行不携带 speed/metrics 数据,防止 speed_sec=0被误算成 100% 加速
这些边界情况看似琐碎,但它们防止的是"加速看起来很好、实际是 bug"这类最难发现的问题。
记忆系统
这次任务让我印象深刻的是 memory/ 目录的设计。代理在迭代过程中把发现写到 discoveries.md,下次会话开始时先读这个文件,跳过重复的探索成本。
比如代理发现"LogNorm 没有从 Seurat 命名空间导出,需要 getFromNamespace"这个坑后,把它记在 discoveries.md 里。如果会话中断重开,新的代理不需要重新踩这个坑。
dead_ends.md 记录被证伪的方向("future_lapply 并行在 2700 个细胞上因 fork 开销而更慢"),防止后续轮次重复尝试。
参考资料
[1] https://github.com/ElliotXie/autozyme
[2] https://www.biorxiv.org/content/10.64898/2026.06.12.731250v1

夜雨聆风