一、有了评测尺子,为什么还不能直接改 Skill

第五篇把比较规则准备好了:同一份 generated-code-testing Skill 会面对固定任务,train 提供改写信号,validation 导航候选搜索, holdout 只在最后比较冻结基线与最终候选。现在看起来只差一句命令:把 Skill 交给优化器,让它写出更好的版本。

但优化器面对的不是一张可以随意涂改的白纸。一次 Agent 运行里同时有用户任务、Skill 说明、工具调用顺序、模型输出和评分规则。 如果这些内容都只是临时拼成一个字符串,评测器即使发现结果不好,也回答不了两个最基本的问题:究竟允许改哪一段,改完又怎样装回程序?

继续用同一个例子。旧 Skill 已经写明“修改 schema 后先生成代码,再运行测试”,但 Agent 仍可能漏掉“检查生成文件差异”这一步。 我们希望优化器改的是这份做事说明,而不是改用户提交的 schema,不是改测试命令的 Python 控制流,也不是为了得高分偷偷改 rubric。 因而,开始搜索前必须先把一次运行拆成四个边界:

固定输入输出:任务进来,结果出去
固定执行结构:程序按什么顺序调用模型和工具
允许搜索的参数:这份 Skill 对任务的说明
固定评价方式:同一道题怎样得到分数和失败原因

这四条边界解决的是不同问题。前两条保证候选仍然在做同一件事,第三条指出变化真正发生在哪里,第四条保证新旧版本仍用同一把尺子。 如果第三条没有显式接通,优化器可能只改到外围提示词,Skill 正文却纹丝不动;如果第四条跟着候选变化,分数再高也失去可比性。

这一篇只追一条证据链。 旧 Skill 怎样进入可优化参数,失败现场怎样变成下一版 instruction,候选怎样重新执行并被选择,胜出文字怎样写回文件, 最后又怎样经过约束、holdout 与人工审阅。任何一环只存在于 README、类名或注释里,都不能算已经闭合。

上一篇已经解释了题目、rubric 与三份数据的权限,本篇只在流程需要时引用它们。 下面先认识负责划定“程序与参数”边界的 DSPy,再看 GEPA 怎样利用轨迹和反馈改写参数;最后回到 Hermes Agent Self-Evolution 的最新固定快照,逐行核对目标设计与实际调用链之间还差什么。

二、在理解 GEPA 之前,先看 DSPy 让什么变得可优化

现在我们已经有题目和尺子,却还缺一个关键接口:优化器究竟可以改哪里?如果程序只是把一整段提示词拼成字符串后直接发给模型, 评测器即使知道结果不好,也不知道哪段文字属于任务说明、哪段属于输入,更不知道怎样把改写后的版本重新装回原程序。

DSPy解决的正是这个中间问题。它不是某一种搜索算法,而是一套语言模型编程框架: 先声明输入、输出和任务说明,再把一次或多次模型调用组成模块,最后用统一的样本与指标调用优化器。这样,“写提示词”就变成了“编译一个有参数的程序”。

在看类名之前,先把边界分成四格。前两格定义程序怎样工作,第三格才是优化器可以搜索的内容,最后一格只负责判断结果:

固定输入输出字段:Signature
固定执行结构:Module
可被搜索的内容:显式注册的 instructions / demonstrations
判断候选好坏:Example + metric

DSPy 不会任意改写 Python 控制流,也不会自动把任何字符串都当成可优化参数。只有被程序暴露出来的 instruction、示例等参数会进入搜索; 输入字段、Module 的调用结构和 metric 仍由开发者固定。后面所有源码都可以用“这一行属于哪一格”来读。

任务与 Skill 先经过 DSPy 的 Signature、Module 和 metric 组成可编译程序,再由 BootstrapFewShot、MIPROv2 或 GEPA 分别优化示例、指令与反思式指令
DSPy 先规定程序结构与可优化参数;BootstrapFewShot、MIPROv2 和 GEPA 再用不同方法搜索这些参数。

2.1 把这次生成代码任务装进 DSPy

继续使用 generated-code-testing Skill。我们希望程序接收 schema 变更与仓库状态,输出经过验证的修复,同时允许优化器替换“生成代码后应该怎样检查”这段说明。 要做到这一点,程序至少要把输入输出、调用方式、评测题和评分规则分开。DSPy 用四个对象承接这四项职责。

先用 Signature 声明这次语言调用的表格:输入栏是 schema 变更与失败现场,输出栏是修复说明和验证结果, instructions 写“运行真实生成命令、检查生成差异、再执行目标测试”。 如果我们要优化 Skill,这段 Skill 说明就必须被适配成可替换的 instructions,而不能只作为一段普通输入跟代码一起传进去。

再用 Module 决定怎样执行这张表格,例如交给 ChainOfThought 做一次预测。“schema 已变但生成物过期”这道题被装成 Example:仓库状态进入输入栏,评分说明随样本保留。最后,metric 读取模型结果和评分说明,返回分数; GEPA 这类需要反思的优化器还会拿到文字反馈。四样东西合在一起,优化器才既知道“可以改哪段说明”,又知道“改完怎样比较”。

当前 Skill
-> Signature.instructions
-> 允许替换的任务说明

评测题 -> Example
Module -> 运行一次生成代码修复
metric -> 给出分数与失败原因
compile -> 返回新程序

现在再把四个源码名压缩成表格,就不需要靠表格猜它们之间的顺序了:

角色在生成代码例子里是什么接住哪项职责
Signature输入是 schema 变更与失败现场,输出是修复和验证结果,instructions 承载 Skill声明输入、输出与可替换说明
ModuleChainOfThought 执行这份 Signature固定调用顺序与控制流程
Example一道生成代码故障题及其 rubric给编译器可重复的比较对象
metric检查是否重生成、查看差异并跑对测试告诉优化器什么叫更好

其中最重要的边界是“什么会被优化”。DSPy 把任务说明放在 Signature.instructions 上;优化器沿着 Module 的 predictor 树寻找这些参数。 官方源码明确区分了 predictor 这类可发现的 Parameter 与普通 Python 属性:前者会进入优化器视野,后者不会自动进入。这个契约见 Signature instructionsModule 参数发现规则

所以 compile() 不是“让模型自己变聪明”的魔法调用。它接收一个结构已经明确的 student program、训练样本和 metric,返回一份参数经过选择或改写的程序副本; 模型权重可以完全不变。后面审计 Hermes 时,我们也会沿着这条边界追问:Skill 正文到底是 predictor 的 instruction,还是一次普通输入?

2.2 三个优化器改变的不是同一件事

有了这个框架,我们就能按“想从失败中学到什么”选择办法。先看最简单的情况:旧 Skill 的说明基本正确,只是模型缺少可模仿的成功案例。 BootstrapFewShot 会运行训练题,把达到评分要求的执行轨迹留下来,作为 demonstrations 放回 predictor。它主要增加示例,不负责系统地重写整段任务说明。

如果示例和说明都可能有问题,可以再扩大搜索空间。MIPROv2 会提出多组 instruction 和 few-shot 候选, 然后用验证分数寻找更好的组合。它能同时调整“怎么说”和“给哪些示例”,代价是需要评测更多组合。

如果我们不仅知道分数低,还保存了“运行了生成器,却没有检查生成差异”这样的失败现场,就可以让 GEPA 读取 trace 和文字反馈, 先解释为什么失败,再重写 predictor instructions。三种方法都接收 DSPy 程序和评测集,但改变的东西与使用的学习信号不同。 下面的表是刚才三条路径的速查:

优化器主要改变什么它从评测中怎样学习
BootstrapFewShot挑选并加入 demonstrations保留达到 metric 要求的成功执行轨迹
MIPROv2候选 instruction 与 demonstrations 的组合提出多个候选,再用验证分数搜索组合
GEPApredictor 的自然语言 instructions读取轨迹和文字反馈,反思失败后生成新候选

BootstrapFewShot 的官方实现把合格轨迹装进 predictor 的 demos;MIPROv2 同时提出 instruction 与 few-shot 候选并搜索组合,分别见 BootstrapFewShotMIPROv2 候选生成。 GEPA 也会改 instruction,但它不只根据“哪个候选分高”做选择,而是让语言模型阅读失败现场后写出下一版。现在我们才有足够的铺垫理解它为什么不同。

三、GEPA 的关键不是“遗传”,而是带着失败原因改提示词

现在才轮到 GEPA。论文把目标写得很清楚:模型权重保持不变,搜索的是自然语言提示词。每轮从候选池里选一个父代,拿一小批样本执行,收集轨迹、分数和文字反馈, 再让反思模型解释失败原因并提出新的提示词。新候选经过评测后回到池里,循环继续。

继续用生成代码题,一轮反思不是“让模型自由复盘”,而是下面这条可以重新执行的链:

候选 P1 处理 schema 变更后的测试失败
-> 运行了生成器,却没有检查生成文件差异,得分低
-> 反馈指出:无法证明变更只来自预期 schema,也没有发现意外生成物
-> 反思器把“生成后先检查 diff”写成明确步骤
-> 得到候选 P2
-> P1 与 P2 在同一小批题上重新执行
-> 只有实际改善的候选才回到候选池

因此下面的图应按“执行、诊断、修改、复测”阅读。文字反馈负责提出改动方向,重新执行负责证明这次改动是否真的有效; 少了最后一步,反思只是一段听起来合理的自我解释。

GEPA 从候选提示词出发执行小批量任务,读取轨迹与反馈,反思失败原因,生成新候选并放入帕累托候选池
分数告诉系统哪里不好,轨迹和文字反馈才让反思器判断应该改哪一段提示词。

3.1 轨迹让“错了”变成“错在哪里”

只有一个 0.4 分,反思模型不知道问题来自工具选择、顺序、遗漏约束,还是输出格式。执行轨迹把中间决策暴露出来;评估器再补充文字反馈, 例如“调用了正确工具,但在拿到空结果后没有执行备用搜索”。GEPA 的 reflective mutation 正是读取这类上下文后,针对失败模式重写提示词。 论文的算法与反馈接口见 GEPA v2 第 3 节

在独立 GEPA 实现里,这个失败现场不是一段随意拼接的日志,而是 adapter 必须返回的逐题批次。引擎只要求输出、分数和轨迹一一对齐; 轨迹内部是什么,由接入方解释。这个边界让 GEPA 可以优化 Agent、RAG 或别的文字组件,而不必理解它们各自的运行时。

Candidate = dict[str, str]

@dataclass
class EvaluationBatch:
    outputs: list[RolloutOutput]
    scores: list[float]
    trajectories: list[Trajectory] | None = None

def evaluate(
    batch,
    candidate: Candidate,
    capture_traces: bool = False,
) -> EvaluationBatch: ...

EvaluationBatchscores[i] 回答第 i 道题有多好,trajectories[i] 保存这道题为何得到这个结果。 反思阶段再由 make_reflective_dataset 把轨迹压成可操作反馈。没有这条对应关系,“反思”就会退化成只看总分猜原因。

3.2 为什么不永远选择总分第一

一个候选可能擅长发现生成物过期,另一个擅长识别不该提交的生成文件,第三个能稳定选择正确的测试范围。只保留平均分最高的版本,会过早丢掉这些局部强项。 GEPA 维护按样本表现形成的帕累托候选集合:先抽一道题,再从这道题上的强候选中选择父代。这样,仍有独特优势的候选可以继续繁殖,而不是被一个暂时的总冠军清空。

这也解释了论文名称里的 Pareto。它不是为了让术语更复杂,而是在有限 rollout 预算下保护搜索多样性。 论文消融实验显示,只从“当前最好”变异会弱于帕累托选择;但候选合并也不是永远有利,组件之间存在依赖时可能反而破坏组合。

四、先走完一次进化,再看框架怎样接线

到这里,我们已经分别认识了基线、评测样本、rollout、trace、候选、DSPy 和 GEPA。但零件都认识,不等于流程已经连起来。 先继续用那份生成代码 Skill,把“旧版怎样成为可交付新版”从头到尾走一遍;然后再看三个框架怎样为这条流程传递数据。

4.1 旧版怎样走到可交付候选

  1. 冻结起点。保存旧 Skill、模型配置、评测题来源和数据切分。后面即使产生很多候选,基线也不能跟着变化。
  2. 跑出基线。让旧版完成训练集和验证集里的任务,保存每题输出、执行轨迹、分数和文字反馈。现在系统知道旧版平均多好,也知道它具体错在哪里。
  3. 生成候选。优化器选择一个父候选,抽取一小批失败样本,让反思模型阅读 Skill、轨迹和反馈,再提出一版有明确针对性的改写。
  4. 重新执行。新版完成同样的题目。结构、长度或安全约束不合格就立即淘汰;合格版本才进入验证比较。
  5. 循环搜索。验证分数和逐题表现决定候选是否留在池里、下一轮从谁继续改。预算耗尽或提升停滞时,搜索停止。
  6. 只打开一次留出集。选定候选后,才让它和旧版面对搜索期间从未见过的题。没有稳定提升,就不进入交付。
  7. 交给人审阅。系统展示 Skill diff、基线与候选指标、失败样本和已知局限。人可以接受、拒绝或继续修改;运行中的 Skill 不会被优化器直接覆盖。

这条路线里有三个不同身份:旧版是比较基准,候选是尚未证明的实验品,交付版是通过留出比较和人工审阅后才获得的身份。 “生成了一份更顺眼的文字”只走到第三步,不等于完成自进化。

4.2 适配层把三种表示翻译给彼此

完整路线清楚后,框架接线就容易理解了。Hermes 手里的是一份 Skill 文件;DSPy 希望把任务包装成可重复调用的 program,并把可优化说明放在 predictor instructions; GEPA 看到的则是一组有名字的文字组件,以及“怎样执行组件、怎样返回逐题分数和轨迹”的接口。三者说的不是同一种数据形状,因此中间需要 adapter 做翻译。

GEPA 的 独立官方实现 把候选交给 adapter 执行,再收集逐题分数、轨迹和反思材料;它本身并不知道 DSPy 的 Signature 或 Hermes 的 SKILL.mddspy.GEPA 则负责 DSPy 这一侧的翻译:遍历 student program 的 predictors,把 pred.signature.instructions 组成初始候选, 运行模块并捕获 trace,最后把 GEPA 选出的文字装回新程序。对应实现见 dspy.GEPA.compileGEPA adapter 契约

DSPy 的翻译可以缩成下面四行。第一行决定搜索空间:只有 predictor 的 signature.instructions 被放进初始候选。 adapter 负责把候选跑成轨迹与分数,GEPA 负责搜索,最后再由 adapter 把胜出文字装回程序。

seed_candidate = {
    name: pred.signature.instructions
    for name, pred in student.named_predictors()
}
result = optimize(seed_candidate=seed_candidate, adapter=adapter, ...)
new_program = adapter.build_program(result.best_candidate)

这段 dspy.GEPA.compile 也给了我们一把很具体的审计尺:想证明某段文字会进化,就必须能指出它在哪一步进入 seed_candidate,又在哪一步从 best_candidate 被写回。

适配层决定“到底改了什么”。 独立 GEPA 可以通过自定义 adapter 优化任意文字组件;但标准 dspy.GEPA 路径从 DSPy predictor instructions 读取种子候选。 如果 Skill 正文只作为普通输入传进 forward,它不会因为参加了同一次执行就自动成为可改写参数。

4.3 Hermes 入口怎样表达这条目标路线

Hermes Agent Self-Evolution 的 CLI 把目标流程写成十个阶段:找到 Skill、准备数据、检查基线、配置 DSPy、运行 GEPA、取出新版、检查约束、比较留出集、展示结果、保存输出。 主入口在 evolve_skill.py

为了让同一份 Skill 可以反复做同一道题,仓库用 SkillModule 把它包成 DSPy 模块:每次 forward 把 Skill 正文和任务输入交给 ChainOfThought,再返回模型输出。换句话说,它把“某个 Skill 做某道题”变成一项可以重复运行、评分和比较的函数。实现见 SkillModule

class TaskWithSkill(dspy.Signature):
    skill_instructions: str = dspy.InputField()
    task_input: str = dspy.InputField()
    output: str = dspy.OutputField()

def __init__(self, skill_text):
    self.skill_text = skill_text
    self.predictor = dspy.ChainOfThought(self.TaskWithSkill)

def forward(self, task_input):
    return self.predictor(
        skill_instructions=self.skill_text,
        task_input=task_input,
    )

这段包装已经完成“可重复执行”,却也暴露出下一节要检查的边界:self.predictor 是 DSPy 能发现的 predictor, self.skill_text 则是普通 Python 属性;forward 又把它放进 InputField。能参与一次运行,不自动等于会进入刚才的 seed_candidate

理想阶段必须留下的证据失败时应该怎样
冻结基线原始 Skill、源码版本、模型配置不能悄悄换基线继续比较
准备评测题目来源、rubric、切分和种子题目质量不足就停止优化
反思变异父候选、样本、轨迹、反馈、新候选可定位哪次变异导致退化
候选门控结构、长度、测试、基准结果任一硬约束失败就淘汰
留出比较基线与候选在同一未见集合上的结果无提升就不交付
人工交付diff、指标、已知局限、可回滚版本不直接覆盖运行中 Skill

这张表把 4.1 的完整路线压缩成源码审计标尺。一个仓库可以同时拥有所有模块文件,却还没有把它们接成这条证据链; 判断“实现了没有”,要看值是否真的从入口流到门控和交付,而不是只看类名是否存在。

五、当前源码走到哪里:原型已经成形,闭环还没有

回到 Hermes 仓库时,要把“文件存在”“进入主流程”和“目标闭环已证明”分开。固定快照里的状态可以先压成三层:

状态当前可以确认的内容
已经进入主流程读取 Skill、选择一种数据来源、构造 DSPy 样本、尝试优化、做留出比较并保存输出
已有代码但没有接入目标路径丰富的 LLM Judge、测试函数和 PR 配置存在,却没有成为本次候选搜索与交付的实际门
目标路线尚未证明GEPA 真正改写 Skill 正文、每个候选经过硬门控、通过后自动形成可审阅 PR
当前源码已经有读取 Skill、构建评测集、运行优化器和留出集比较的原型,但结构校验、测试和交付尚未形成端到端闭环
README 画的是目标架构;判断现状要沿着参数、返回值和实际调用逐步追踪。

先说已经具备的部分:仓库能发现和解析 Skill,能生成或导入评测数据,能构造 DSPy 模块,能调用优化器,能在留出集上比较基线与候选,也能保存指标与前后版本。 这些不是空白路线图,而是一套有单元测试覆盖的数据和编排骨架。

但在固定快照 0a929e3 上,几处关键接线决定了它还不能被称为完整交付闭环:

5.1 被优化的 Skill 文本还没有明确注册为 GEPA 参数

SkillModule 把正文保存在普通 Python 属性 self.skill_text,forward 时再把它作为 skill_instructions 输入字段传给 predictor。 主流程却假设 optimized_module.skill_text 已经装着 GEPA 改写后的正文。当前源码没有展示把这个普通属性注册为 DSPy 可优化提示词参数的桥接, 因此仅凭这条调用链,不能确认 GEPA 会改写 Skill 本身,而不是只优化 predictor 内部提示。

baseline_module = SkillModule(skill["body"])
optimized_module = optimizer.compile(baseline_module, ...)

# 主流程从普通属性取“进化后的正文”
evolved_body = optimized_module.skill_text

这里缺的不是一次赋值,而是双向映射:优化前要把 Skill 正文注册成 predictor instruction,优化后再从胜出 instruction 还原成 SKILL.md 正文。当前 主流程 只展示了包装与读取,没有展示这两次翻译。

5.2 依赖没有锁定上限,GEPA 的构造参数也对不上

仓库声明的依赖是 dspy>=3.0.0,没有锁定上限;主流程却调用 dspy.GEPA(metric=skill_fitness_metric, max_steps=iterations)。在本篇固定的最新 DSPy 快照里,GEPA 没有 max_steps, 而是要求在 automax_full_evalsmax_metric_calls 中恰好选择一个预算, 并提供 reflection_lm 或自定义 instruction_proposer。 对照 Hermes 依赖声明实际调用最新 DSPy 构造接口即可确认。

# Hermes 调用
dspy.GEPA(
    metric=skill_fitness_metric,
    max_steps=iterations,
)

# 最新 DSPy 快照的关键参数
def __init__(
    metric,
    *,
    auto=None,
    max_full_evals=None,
    max_metric_calls=None,
    reflection_lm=None,
    instruction_proposer=None,
): ...

这不是参数改个名字就结束:三个预算参数必须恰好选择一个,反思模型与自定义 proposer 也必须至少提供一个。 当前调用既没有合法预算,也没有告诉 GEPA 由谁阅读失败现场并提出新 instruction。

另外,Hermes 的 metric 只接受三个参数,而最新 DSPy 会在构造时检查五参数接口,随后为单个 predictor 传入 goldpred、完整 trace、predictor 名称和 predictor 子轨迹五项。 配置中的 optimizer_model 虽然被打印出来,也没有作为 reflection LM 交给 GEPA。 因此按本篇最新依赖快照运行时,这条 GEPA 路径会在真正开始反思之前失败;宽泛异常捕获随后把运行切到 MIPROv2。

5.3 结构校验拿到的是正文,却要求它带 frontmatter

load_skill 已经把 frontmatter 与 body 分开。主流程把 skill["body"]evolved_body 送入 validate_all; 但 _check_skill_structure 又要求输入以 --- 开头,并在前 500 字符里包含 name:description:。 对当前调用来说,这个条件天然失败。相关检查在 validate_all_check_skill_structure

# 调用方传入 body
validator.validate_all(skill["body"], "skill")
validator.validate_all(evolved_body, "skill", ...)

# 校验方却按完整文件检查
has_frontmatter = text.strip().startswith("---")
has_name = "name:" in text[:500] if has_frontmatter else False
has_description = "description:" in text[:500] if has_frontmatter else False

因而基线和候选即使正文完全合法,也会在同一处失败。修复方向只有两个:要么传入重组后的完整 Skill 文件,要么让“正文约束”和“frontmatter 约束”接收各自的数据,而不是让一个函数猜调用方传来的究竟是哪一层。

5.4 丰富的 LLM Judge 存在,但优化器实际拿到的是关键词重合度

fitness.py 定义了 correctness、procedure following、conciseness 和文字反馈组成的 LLMJudge;这正接近 GEPA 需要的可解释反馈。 可是传入 dspy.GEPA(metric=...)skill_fitness_metric 只计算 expected behavior 与输出的单词重合比例,并返回一个浮点数。 两条路径见 LLMJudgeskill_fitness_metric

expected_words = set(expected.lower().split())
output_words = set(agent_output.lower().split())

overlap = len(expected_words & output_words) / len(expected_words)
score = 0.3 + (0.7 * overlap)
return min(1.0, max(0.0, score))

这几行才是搜索实际看见的目标。关键词代理指标速度快,却会奖励“重复 rubric 里的词”,不一定奖励正确完成任务;同时,丰富的失败原因没有通过这条 metric 返回给优化器。 这不是调权重能解决的小问题,而是当前搜索目标与论文方法之间的接口差异。

5.5 测试门、逐候选约束和 PR 交付仍是断开的

ConstraintValidatorrun_test_suite,配置也有 run_pytestcreate_pr;但主流程没有调用测试函数,也没有创建 PR。 约束只在优化完成后检查最终提取出的正文,并没有进入 GEPA 的每个候选评测。成功路径最终只是把文件与 metrics.json 写进 output/。 测试函数可见于 run_test_suite, 输出路径则在 保存阶段

5.6 异常回退会把“GEPA 不可用”和“GEPA 执行失败”混在一起

主流程用一个宽泛的 except Exception 捕获 GEPA compile 的任何错误,然后回退到 MIPROv2。 这能提高演示时的可运行性,却可能把真正的接线错误伪装成“当前 DSPy 版本没有 GEPA”。严谨的进化运行应该只对已知能力缺失回退,其余异常保留失败现场。

这不是说仓库没有价值。 数据导入、样本结构、Skill 包装、留出比较和约束类都提供了可继续建设的骨架。准确的状态描述应是:Phase 1 原型已出现,端到端的 GEPA Skill 改写、硬门控和人工交付尚未由当前调用链证明。

六、怎样读“提升 39.5%”:先问实验真正改了什么

仓库的 Phase 1 报告记录了一次 arxiv Skill 实验:使用 MiniMax M2.5、7 个合成样本中的 3 个训练样本与 2 个验证样本,优化器是 DSPy BootstrapFewShot, 评分是关键词重合度。第一个验证样本从 0.408 到 0.569,报告为 +39.5%;第二个从 0.374 到 0.374,平均从 0.391 到 0.472,即 +20.7%。 配置、指标和结果写在 generate_report.py

这次实验不是 GEPA 实验,也没有重写 Skill。BootstrapFewShot 选择表现较好的执行轨迹作为 few-shot demonstrations,给模块增加示例。 因此它能支持的窄结论是:在一个很小的合成集合和代理指标上,DSPy 包装后的模块通过示例增强得到更高分。它不能证明 GEPA 已经接通,更不能证明改写后的 Hermes Skill 在真实任务上稳定提升。

证据它实际测试了什么不能外推成什么
GEPA 论文六类任务上的提示词进化方法与消融Hermes 集成已经获得同样收益
Phase 1 报告一个 Skill、极小合成集、BootstrapFewShot、关键词指标GEPA 已跑通或 Skill 正文已被改写
当前源码数据、编排、比较和约束原型测试、PR 与逐候选门控已经自动闭环

GEPA 论文自己的结果更强:在六项任务上,相比 GRPO 平均高约 6%、最高约 20%,并报告最多 35 倍更少的 rollout;相对 MIPROv2 平均也有超过 10% 的优势。 这些数字说明方法值得研究,但评价对象是论文基准,不是这份 Hermes 原型。把两个证据层级分开,反而能看清项目下一步真正需要补什么。

七、要把原型变成可信闭环,还缺哪几步

补齐路线不需要发明更多名词,只要让前面的证据链一环不漏地传下去:

  1. 固定 artifact。记录源仓库 commit、Skill 哈希、模型与数据配置,保证基线可重放。
  2. 把 Skill 正文变成真正可优化的提示词参数。用 DSPy 明确支持的 instruction / signature 机制接入,而不是依赖普通属性被隐式改写。
  3. 让 metric 返回正确目标。优先使用可执行检查;开放任务再用 rubric judge,并把可操作的文字反馈交给 GEPA。
  4. 每个候选先过硬约束。完整 frontmatter、长度增长、测试和缓存边界都应在候选进入池前检查。
  5. 把异常留在正确层。GEPA 失败就保留失败,不用另一个优化器掩盖;回退必须是显式运行模式。
  6. 最后只打开一次留出集。同时报告逐题结果、关键旧任务是否退化、置信区间或重复运行,避免一个样本的百分比制造确定感。
  7. 检查改进的代价。更高分不能靠不可接受的模型成本、token、工具调用或时延换来;这些预算也要和基线并排报告。
  8. 交付是 diff,不是覆盖。输出候选、基线、指标、失败案例和 PR,让人决定是否进入 Hermes Agent。
  9. 回到运行时仍要等下一轮。通过评测的 Skill 在安全边界刷新,不能在当前会话中途改变稳定提示前缀。
未见题上稳定提升
+ 关键旧能力没有退化
+ 成本与时延仍在预算内
+ 数据、配置和候选可以重放
+ 旧版本可以回滚
= 有资格交给人决定是否交付

做完这些,离线搜索部分才真正闭合:运行时从真实任务里发现值得沉淀的经验,离线流程把某个 Skill 当成有版本的 artifact 做实验, 人工批准后,新版本再在未来会话生效;新的失败继续成为下一轮评测线索,而不是当场改写自身。下一篇会继续追问:候选进入人工审阅前, Hermes 自己怎样收集本地验证证据,又怎样避免在没有新鲜通过证据时草率结束任务。

八、结论:自进化不是不断重写,而是不断补强证据

回到开头的问题。Agent 把“先运行生成器”写进 Skill,并不等于它已经变好;只有当旧版和新版面对同一套独立任务,新版在正确指标上更稳定, 又没有破坏结构、测试和运行边界,这次改写才有资格被称为改进。

运行时负责发现经验;
评测集负责固定问题;
轨迹与反馈负责解释失败;
候选池负责保留多种强项;
约束与留出集负责阻止自我说服;
人工审阅负责决定是否交付。

Hermes Agent 给出了第一只时钟:回答之后,经验怎样进入长期状态。GEPA 给出了第二只时钟的方法:在不训练模型权重的前提下,怎样让提示词候选经历执行、反思和选择。 Hermes Agent Self-Evolution 则把两者之间的工程接口摆到了桌面上。它当前最有价值的地方,不是宣告自进化已经完成,而是让我们能沿着源码准确看到下一段路。

参考源码与论文