让任何制品自己训练自己:我做了一个通用自进化引擎
上个月我在调一个 Prompt,改了大概十几版。每次改完我都觉得”这次应该行了”,跑几个测试,有的好了有的坏了,然后继续改。改到第八版的时候我已经不记得第三版为什么被丢弃了,也说不清第六版和第八版到底哪个更好。
这种感觉你应该很熟悉——手工调优就是这样,靠直觉,靠记忆,靠运气。
那段时间我正好在看 Karpathy 的 autoresearch,突然想到一个事:他让 AI 自己迭代训练代码,每次改一点,跑指标,好了留下坏了回滚。这个 Loop 的本质跟我手工调 Prompt 是同一件事——只不过他用程序跑,我用脑子跑。
然后我又看到 Stanford 的 Meta-Harness 论文,讲的是怎么让 AI 诊断另一个 AI 的失败。再加上 Anthropic 的 skill-creator 提供了现成的评测引擎。
三个东西拼在一起,我觉得有戏。
所以我做了一个 self-evolution Skill——不只是给 Skill 用,而是让任何可评测的制品(Prompt、代码、文档、配置)都能丢进去跑自进化 Loop。制品提供数据和指标,Loop 负责试错和保留,人只需要定义什么叫”好”。
三根柱子
先把我的灵感来源说清楚,不然后面的设计会看不懂。
Karpathy 的 autoresearch 给了 Loop 的骨架。
今年 3 月 Karpathy 发了一个 630 行的 Python 脚本。干一件事:让 AI Agent 迭代优化一段 LLM 训练代码。每次改一点,训练 5 分钟,看指标,好了留下,坏了回滚。两天跑了 700 个实验,找到 20 个优化,性能提升 19%。
后来 Udit Goenka 把这个思路泛化成了 Claude Code Skill,提炼出 5 条原则:one metric、constrained scope、fast verification、automatic rollback、git as memory。
我看到这个的时候想的是:这不就是我手工调 Prompt 在干的事吗?只不过他用程序跑 Loop,我用脑子跑。
Stanford 的 Meta-Harness 给了诊断的大脑。
这篇 Paper 解决的是一个很具体的问题:你让 AI 去优化另一个 AI 的时候,该给它看什么?
答案是完整的原始执行轨迹,而不是分数摘要。
他们的消融实验显示,只给分数相比给完整 Trace,效果差了 44%。类比一下:你让一个医生看完整的病例、化验报告、影像,他能给出好诊断。你把这些压缩成 300 字摘要,他只能靠猜。
这一点对我触动很大。我之前调 Prompt 的时候,知道某个 Case 挂了,但不知道具体挂在哪一步、为什么挂。如果能拿到完整的执行 Trace,诊断的精度会完全不一样。
Anthropic 的 skill-creator 给了评测引擎的底座。
它提供了 quick_validate 做结构检查、grader 做逐条打分、comparator 做 A/B 盲审,还能自动生成 GT(Ground Truth)测试用例。评测这件事最烦的是从零搭,有现成的底座可以直接用。
三个东西各自解决一个问题:autoresearch 管循环、Meta-Harness 管诊断、skill-creator 管评测。我要做的是把它们接成一条能跑起来的闭环——而且不限定在某一种制品上。
为什么不绑死在某一种制品上
我一开始想过只做 Prompt 优化器。但拆到底层就发现,自进化 Loop 需要的输入其实只有三个:
- Artifact(制品)——被改的东西
- Ground Truth(GT)——测试用例,定义什么叫”好”
- Execution Method(执行方法)——怎么从制品产出输出,供评测
Prompt 满足这三个条件,Skill 也满足,代码也满足,甚至一份创业 BP 也满足——只要你能定义什么叫”好”。
所以我没有做”Prompt 优化器”或者”Skill 优化器”,而是定义了五种制品类型,每种有不同的执行方法:
| 类型 | 制品是什么 | 怎么执行 |
|---|---|---|
prompt |
系统提示词、few-shot 模板 | 发给 LLM,捕获回复 |
skill |
SKILL.md + references + scripts | 加载 Skill 跑 Claude |
code |
源代码文件 | 跑测试或 shell 命令 |
idea |
商业文档、设计方案 | LLM 直接评估文档本身 |
config |
YAML、JSON、.env 配置 | 应用配置后检查系统行为 |
这个分类很关键,因为不同类型的制品,Mutation 策略完全不一样。改一个 Prompt 的关键词是 Layer 1(表面层),改它的指令逻辑是 Layer 2(核心层),把它拆成 chain-of-thought 多步骤是 Layer 3(架构层)。但改代码的 Layer 1 是变量名和常量,Layer 2 是函数实现和算法,Layer 3 是模块结构和接口。
泛化不是把 “Skill” 改成 “Artifact” 这么简单。每一层的 Mutation 边界、评测方式、安全检查都要重新定义。
核心机制:怎么跑一次自进化
整个流程分 Phase 0 的一次性准备 + 每轮迭代 8 个阶段。我把关键部分拆开说。
Phase 0 做的事比你想的多。
不只是建目录。它要分析制品类型、检查 Git 状态(没 Git 就自动 init)、准备 GT 数据并做三路切分(Dev 70% / Holdout 20% / Regression 10%),然后跑一次 Baseline 评测,最后生成一份 evolve_plan.md——包含评测策略、门控阈值、起始 Mutation 层。这份 Plan 决定了后面所有轮次的行为。
没有这个 Plan,Loop 就是盲跑。
每轮迭代的 8 个阶段:
Phase 1: Review → 读记忆:results.tsv、experiments.jsonl、上轮 Trace
Phase 2: Ideate → 基于 Trace 证据提出 ONE 个原子化改动
Phase 3: Modify → 执行改动,git diff --stat 检查原子性
Phase 4: Commit → 先 commit 再验证——保留审计轨迹
Phase 5: Verify → 三层评测流水线
Phase 6: Gate → 5 维 AND 门控 → KEEP 或 DISCARD
Phase 7: Log → 写 results.tsv + experiments.jsonl + per-case Trace
Phase 8: Loop → 继续 / 升层 / 停止
这里有几个我觉得值得展开说的设计决策。
Phase 2 的硬规矩:没有 Trace 证据不许动手。
每个改动提案必须引用具体的 Trace 文件,写成这种格式:
“Case 12 失败了,因为 Agent 把离职问题路由到了邮箱分类。Trace 在
traces/iteration-4/case-12.md,显示模型在第 3 步选择了错误的索引分支。如果我在root_index.md加一个易混淆提示,输出应该从’邮箱’变成’通讯录’。“
没有这种级别的具体证据,就不许改。这不是形式主义——这是防止”感觉这里可以优化”这种 vibe-based mutation。LLM 特别擅长给你一个看起来合理但实际没有根据的改动建议,不加约束的话,很多轮次会浪费在这种”感觉对”的修改上。
Phase 4 的反直觉操作:先 Commit 再验证。
为什么不是验证通过了再 Commit?因为 Git 是安全网。每个 Mutation 都有 Commit 记录,即使最后被 Revert 了,审计轨迹也在。你可以回头看第 7 轮的改动到底改了什么、为什么被丢弃。这个信息在后面的 Phase 1 Review 阶段会用到。
Phase 2 还有一个原子性测试: 描述这个改动的时候如果需要用”和”字,就该拆成两轮。改完之后 git diff --stat 如果超过 5 个文件,大概率不是原子的。
三层评测:便宜的先跑,贵的后跑
评测不是一刀切地”跑一遍测试”。我把它分成三层,成本递增:
L1 Quick Gate(秒级,每轮都跑)。 纯程序检查,不调 LLM。检查制品结构是否完整、安全扫描(硬编码密钥、危险删除命令、硬编码用户路径,一共 14 条规则)、随机抽 3 个 GT Case 验结构。两条 Critical 规则(危险删除和硬编码密钥)不过直接阻断——坏的迭代成本压到最低。
L1 挂了就不跑 L2。这很重要。如果每次都跑完整评测再告诉你结构就有问题,Token 烧得冤。
L2 Dev Eval(分钟级,每轮都跑)。 拿全量 Dev 集逐条跑。8 种 Assertion 类型,6 种程序直接判(contains、not_contains、regex、file_exists、json_valid、script),2 种需要 LLM 做 YES/NO 分类(llm_judge、fact_coverage)。结果写入 per-case JSON,供下一轮诊断用。
L3 Strict Eval(约 10 分钟,条件触发)。 只在三种情况下跑:每 N 轮自动触发、Dev pass_rate 超过阈值、层晋升之前。它跑的是 Holdout 集——优化器从来没见过的数据。如果 Holdout pass_rate 比 Dev 低 15% 以上,说明过拟合了。
Holdout 集的存在是从 ML 训练直接搬过来的。Dev 集上分数涨了不代表制品真的变好了,可能只是在”背答案”。Holdout 是你留的一手,用来验证泛化能力。
5 维 AND 门控:为什么不用加权求和
每一轮改动都要过 5 道关,全部 PASS 才保留,任何一个 NO 就 git revert HEAD:
| 维度 | 问题 | 检查方式 |
|---|---|---|
| 结构 | L1 过了吗? | L1 结果 |
| 进步 | pass_rate >= 历史最佳? | 对比 results.tsv |
| 回归 | 有没有之前过了现在挂了的 Case? | Diff per-case 结果 |
| 成本 | Token 和时间在 Baseline 2 倍以内? | 时间数据 |
| 安全 | 有没有新的安全违规? | L1 安全扫描 |
为什么是 AND 不是加权求和?
加权求和的问题是:质量涨了 10% 但 Token 消耗翻倍?加权求和可能给你 PASS。pass_rate 涨了 5% 但有 2 个 Case 回归?加权求和也可能给你 PASS。
AND 逻辑不会。每个维度必须独立通过。没有”拿一个维度的高分补另一个维度的低分”这种操作。
这个决定在实践中被证明是对的。最常见的陷阱就是:改了一个地方修好了 3 个 Case,但悄悄搞坏了 1 个。加权求和会把这种改动放过去,AND 门控会拦住。
回归维度有一个噪声处理:如果恰好只有 1 个 Case 回归,同时其他几个 Case 改善了,会把那个回归的 Case 跑 3 次。2/3 通过就算 LLM 噪声,不算真回归。2/3 失败才算真回归。这是因为 LLM 评测本身有随机性——同一个 Skill 状态、同一份 GT,Claude 跑 4 次 pass_rate 可以在 0.79 到 0.92 之间漂移。
分层 Mutation:从最便宜的地方开始改
这个设计类比的是 ML 里的学习率调度——先大幅调整便宜参数,再精细调整贵参数。
三层:
Layer 1(表面层) 是最便宜、风险最低的改动。对 Prompt 来说是措辞和格式,对代码来说是变量名和常量值,对文档来说是标题和用词。改这些不太可能搞坏什么。
Layer 2(核心层) 是主要逻辑。Prompt 的指令逻辑、代码的函数实现、文档的论证结构。中等成本,中等风险。
Layer 3(架构层) 是最贵的。Prompt 拆成多步 chain-of-thought、代码改模块结构、文档重新定位目标受众。高风险,但有时候不动架构就是改不动了。
规矩:从 Layer 1 开始,在当前层连续 K 次 DISCARD(默认 K=3)才升到下一层。不准跨层——一次迭代只能改一层的东西。如果三层都试过都没改善,迭代结束。
这里有一个实际观察:大部分改善发生在 Layer 1 和 Layer 2。Layer 3 的改动风险太高,成功率明显低于前两层。但有时候问题确实出在架构上,Layer 1 和 2 怎么改都改不动,升到 Layer 3 一刀切反而解决了。
工程落地:三个脚本撑起评测
设计想清楚了,落地的时候我决定不硬依赖任何外部框架。skill-creator 很好用,但我不想把整个系统绑死在上面——万一用户要优化的是一段 Python 代码,装一套 skill-creator 就显得多余了。
所以我写了三个独立的 Python 脚本:
evaluate_assertions.py 处理 6 种程序化 Assertion(contains、not_contains、regex、file_exists、json_valid、script)。LLM 类型的(llm_judge、fact_coverage)标记为 SKIPPED_LLM_REQUIRED,交给上层处理。输入一个输出文件和一组 Assertion JSON,返回逐条结果。
structural_check.py 做 L1 的结构和安全检查。14 条安全规则扫描(AWS Key、GitHub Token、硬编码密码、rm -rf /、DROP TABLE 之类),按制品类型做针对性检查——Skill 查 YAML frontmatter,代码做 syntax check,通用的查文件是否存在、是否为空、是否是二进制。
results_tracker.py 管理 results.tsv 和 experiments.jsonl 两个日志文件。TSV 是给人看的概览,JSONL 是给 Phase 1 Review 读的结构化记忆。支持 init、log、summary、best 四个子命令。
GT 格式我做了兼容——如果用户已经有 skill-creator 的 evals.json,直接能用。同时扩展了 [contains]、[regex]、[script_check] 等前缀语法,在一个 expectations 数组里混用程序化和 LLM 评测。
Trace 的处理也值得说一句。我没有把完整 Trace 塞进 Prompt——那可能是几万 Token 的执行记录。Phase 2 的 Proposer 只会拿到 Trace 文件的路径,需要哪个自己去读。这是 Meta-Harness 思路的工程化:给地图,不给全文。
踩过的坑和还没解决的问题
LLM 评测的噪声。 这个问题是我跑前几轮就撞上的。同一份制品、同一套 GT,跑 3 次结果能差出好几个百分点。你改了一行文本,pass_rate 从 0.85 变成 0.87——这是你的功劳还是 LLM 随机波动?分不清。我的处理方式是:优先用程序化 Assertion(contains、regex 是确定性的),LLM 评测的 Assertion 跑 3 次取多数票,Temperature 固定 0。不完美,但比裸跑好。
GT 质量是天花板。 这是真的。GT 标注本身有问题的话,无论跑多少轮都修不好那个 Case。我加了一条规则:如果一个 Case 连续 5 轮在所有层都没被修复,先怀疑 GT 而不是制品。标记为”不可修”,排除出所有评测集。
成本。 有些制品类型(比如 skill 需要启动 Claude 进程)每个 Case 的执行成本不低。跑一轮完整的 L2 Eval 如果有 30 个 Case,光 Token 就不少。L3 的 Holdout + Regression + A/B 对比跑一次大概 10 分钟。跑个十几轮下来,钱包是能感受到的。这不是免费的午餐。
前几轮需要人盯。 前 3-5 轮最好看一眼。不是不信任系统,是 experiments.jsonl 里的记忆还不够厚。Proposer 在没有足够历史数据的时候,有时候会提出重复的改动方向,或者在错误的层级上尝试。等跑了 5 轮以上,成功和失败的 Pattern 积累起来了,后面的迭代方向会准很多。
Idea 类型的评测特别难。 技术类制品(代码、配置)有硬指标,script Assertion 跑出来就是过或不过。但 idea 类型——比如一份商业计划书——几乎全靠 llm_judge 和 fact_coverage,噪声大很多。而且有一个”Assertion Gaming”的问题:LLM 为了过 fact_coverage 会加一句凑数的话,但内容质量并没有真正提升。我在 Ideate 阶段加了一条要求——“Depth over assertion-gaming”,每个 Mutation 要让制品实质性变强,不是加最少的内容去翻转一个 Assertion。但说实话,这条规则执行起来还是靠 LLM 的判断力,不算完全可靠。
这个工具让我改变了一个看法
以前我觉得 Prompt Engineering 是门手艺。靠经验,靠直觉,靠反复试。
现在我觉得它更像训练——有数据、有指标、有反馈循环。你定义好 GT,剩下的事情让 Loop 去跑。你不需要猜”这个改动是不是好了”,门控函数会告诉你。你不需要手动回滚”那个改坏了的版本”,git revert HEAD 自动执行。
这个转变对我来说挺大的。
但我也想说一句诚实的话:这个系统不会替你想清楚”好的制品应该长什么样”。GT 是你写的,指标是你定的,制品类型是你选的。它只是帮你在你定义的搜索空间里,比你自己更系统地试错。如果你的 GT 本身就偏了,它会非常忠实地朝着错误的方向迭代。
该想清楚的事情,还是得你自己想。