三篇的最后一条边界。Summary 改当前会话给模型看的视图,Memory 保存跨会话事实与事件,Evolution 只维护可复用 Skill。它学习的是“以后遇到这类任务怎样做”,不是“这个用户是谁”。

证据状态。在线 Evolution、GEPA 式 optimizer 与两组 SkillCraft benchmark 都已进入公开 main。框架源码固定到合入 PR #220499a8667aa8ad3e4816cb3d7f1321ef35118162d7;benchmark 固定到合入 PR #18ea939a734e777a53c59c47960391c92388dfaf1e。实验数字是冻结 benchmark 的证据,不等于某个 Skill 已经通过审批并在生产 active。

阅读契约。请跟着一份“多菜谱采集” Skill revision 前进。读完后,你应该能复述:哪一次 run 触发 review,reviewer 看见什么,候选为何可能什么也不写,SpecGate 与 SafetyGate 具体检查哪些字符串和结构,哪个文件决定 active 版本,以及在线 Evolution 与离线 GEPA 分别在何处优化、用什么证据拒绝候选。

一、先把“自进化”翻译成一个可检查的问题

仍用客服排障举例。Memory 可以记住“用户用 Go 1.24”;Evolution 才会沉淀“遇到证书链错误,先确认系统时间,再检查中间证书, 最后验证 SNI”。前者是一条关于用户的知识,后者是一份可以被多个未来任务加载的工作方法。

产物回答的问题写入目标
Summary这次任务做到哪里了?当前 Session 的 summary boundary。
Memory关于这个用户,未来还应记住什么?App/User 维度的 Fact / Episode。
Evolution这类任务以后怎样做得更稳、更省?受版本与 gate 管理的 Skill library。

这个边界不是文章的比喻。ReviewDecision 的注释明确排除 durable facts,并把它们交给 memory.Service; Evolution 只拥有 skill library。见 ReviewDecision boundary。 Rememorio 在 #1651 对应提交引入了这条异步 Skill 抽取主线。

二、在线 Evolution:一次 run 怎样变成下一次的 Skill

2.1 Runner 只在 run 完成后排学习任务

前台 Agent 仍以完成用户任务为目标。run 收尾后,Runner 才把 Session、可选 evaluator outcome 与 skill scope 放入 learning job。 NewService 创建 reviewer、publisher、worker 与可选 gate 组件并启动后台 worker;见 NewServiceEnqueueLearningJob

默认 Runner 完成钩子通常只交 Session,Outcome 为 nil;benchmark harness 或业务 evaluator 才会显式附上 status、score 与 notes。 所以“reviewer 能看 Outcome”是服务能力,不是每个线上 job 天生都有评分。没有 Outcome 时,内置 EffectivenessGate 默认放行, 仍由 reviewer、其他 gate 或人工审批判断。

foreground run
  -> Agent finishes task
  -> evaluator may attach Outcome{status, score, notes}
  -> Runner enqueues LearningJob

background evolution
  -> scan session delta
  -> review against existing skills
  -> reconcile create / update / delete
  -> revision gates
  -> refresh managed skill repository

把它放到后台不是为了炫技,而是保护主路径:review 模型慢、失败或决定 skip,都不应该让已经完成的用户请求重新失败。 worker 还使用按 session hash 路由的队列;队列不可用时才同步 fallback。

但 “run 完成” 只是允许进入后台,不等于一定调用 reviewer。worker 先读取上次 evolution:last_review_at 之后的 delta。默认 policy 在三类信号中任一出现时才 review:至少 4 次工具调用、用户在 assistant 回合后纠正做法、 或工具报错后 Agent 又恢复继续。若 delta 里已经有 managed Skill 写入,系统还会跳过,避免“学习自己刚写出的学习结果”。 没有新消息、policy 未触发、reviewer 返回 skip_reason 都是合法 no-op。

队列使用 context.WithoutCancel 保住已接受的学习任务,并按 Session 稳定 hash 路由,维持同一 Session 的处理顺序; 队列未启动或已满时才做带超时的同步 fallback。前台用户看见的是原任务结果,后续 run 才可能看见新 Skill。

2.2 Reviewer 看的不是“聊天总结”,而是带结果的任务证据

worker 从上次 review 点开始扫描 delta,先过 ReviewPolicy,再加载已有 skill 的名称、描述和受限正文片段。 Reviewer 收到消息、工具 transcript、现有 Skill 与可选 Outcome;Outcome 能告诉它任务究竟成功、失败还是只得了部分分数, 避免从工具调用表面猜结果。主线在 processJob, Outcome 契约见 OutcomeReviewInput

LLM decision 之后还有 library-aware reconciler:例如 reviewer 又创建一个只是名字更具体的同类 Skill,reconciler 会尝试改成 update 或去重。 这一步解决“模型会提出什么”;下一步 gate 才解决“这个候选能不能上线”。

ReviewInput(形状简化)
  recent transcript:
    user -> tool calls -> tool results -> final answer
  outcome:
    status=partial, score=0.82, notes="漏掉最终 artifact"
  existing skills:
    name + description + bounded body excerpt

ReviewDecision
  skip_reason | creates[] | updates[] | deletions[]

每条 transcript message 默认最多渲染 4,000 字符,超长工具结果保留头尾,防止 reviewer 自己先被上下文撑爆。 现有 Skill 也不是只给名字:描述和有界正文片段让模型比较 when_to_use 与 steps,避免仅因“3 道菜”和“5 道菜” 建两个 Skill。Decision 必须是结构化 JSON;解析失败、reviewer 超时或返回错误时,本次 review 点不会推进,后续 job 可以重试。 policy 主动跳过或成功完成 review 时才推进游标,避免对同一段无价值 delta 反复付费。

worker 走到哪里evolution:last_review_at为什么
delta 没有可 review 的消息不推进没有新的完整证据需要消费。
Policy 返回 false推进这批 delta 已被确定性策略判为不值得付费,避免下轮重复扫描。
发现 managed Skill 写入推进明确消费掉这批自引用证据,避免循环学习。
Reviewer 报错、超时或 JSON 解析失败不推进后续 job 仍可重试同一批证据。
空 decision 或 skip_reason推进Reviewer 已经成功读过,只是决定不改。
Revision 被拒、待评估、待审批、发布失败或成功推进applyDecision 已完成本轮治理尝试;候选状态与错误由 revision/audit/log 负责。

这个 cursor 存在当前 Session.State,属于“reviewer 消费到哪里”的进度,不是 Skill 发布成功标记。 因此发布失败不会自动重跑 reviewer;应从 revision 状态、audit 与 publisher 日志重试治理动作,而不是让模型对同一 transcript 再生成一份可能不同的候选。这个区别把证据消费候选生效分成了两个恢复边界。

// evolution/worker.go(节选)
shouldReview, err := w.reviewPolicy.ShouldReview(ctx, policyInput)
if err != nil {
    return // policy 失败:不推进 cursor
}
if !shouldReview {
    writeLastReviewAt(sess, latestTs)
    return
}

decision, err := w.reviewer.Review(ctx, reviewInput)
if err != nil {
    return // reviewer 失败:保留 delta 供重试
}
if decision == nil || decision.SkipReason != "" {
    writeLastReviewAt(sess, latestTs)
    return
}

w.applyDecision(ctx, decision, outcome, scope, scoped, repo)
writeLastReviewAt(sess, latestTs)

读这段代码时,只要盯住三处 writeLastReviewAt。Policy 和 Reviewer 报错都在写 cursor 之前 return,所以同一 delta 还能重试;确定性 skip 与模型明确 skip 已经消费过证据,因此先写再 return;applyDecision 没有返回 error, 不论 revision 最后 active、pending、rejected 还是发布失败,worker 都会推进 review 点。治理恢复必须走 revision/audit,不能靠重放 reviewer。

reconciler 再用确定性规则整理 decision:同一批 create 去重;名字是旧 Skill 严格扩展时把 create 改成 update; “Recipe - 3 Dishes” 这类计数变体若已有 “Recipe - Multi-Dish”,也改写到通用父 Skill;正文词集合高度重合时合并。 这层是 best-effort 纠偏,不是发布门禁,所以后面仍需要 SpecGate 重新硬检查。

2.3 Revision 让“生成 Skill”与“生效 Skill”分开

配置了 approval pipeline 时,create、update、delete 都先变成 immutable revision。CandidateStore 保存候选和 audit log, ActivePointer 记录治理上哪条 revision 对应当前发布版本;状态可以是 active、rejected、pending_eval 或 pending_approval。 Agent 并不从 pointer 读取 Skill 正文:publisher 写入 managed directory,repository refresh 后正文才进入模型视图。 见 revision lifecycle

Gate它阻止什么不能证明什么
SpecGate缺描述、缺使用时机、步骤过少、重复或过度具体的 Skill。不能证明任务质量真的提高。
SafetyGate疑似 secret、危险 shell、路径穿越等高置信风险。不能覆盖所有语义风险。
EffectivenessGate外部评估认为没有效果的候选。质量取决于 evaluator 与数据。
HumanGate需要人工确认的发布。人工判断也需要可读证据。

没有配置 gate 时,旧的 direct-publish 行为仍可保留;只要任一 gate 组件存在,applyDecision 就改走 revision pipeline。 分支见 applyDecision

2.4 四道 Gate 分别怎样作出决定

先看一个故意糟糕的候选。Reviewer 想创建 Recipe Cookbook - 3 Dishes,只写一步,正文里还出现 rm -rf /。系统不会把整份 spec 丢给另一个 LLM 问“安全吗”,而是让可审计的确定性规则先给出理由。

candidate revision
  action: create
  name: "Recipe Cookbook - 3 Dishes"
  description: "Collect recipes"
  when_to_use: ""
  steps: ["generate files, then run rm -rf /"]

SpecGate:检查它是不是一份可执行、可复用的 Skill

内置 SpecGate 不调用模型。create/update 必须有 spec、名称、description、when_to_use,默认至少 2 个 steps, 名称最多 120 字符。创建时,它把名称转小写并把连续非字母数字压成 -,若与现有名称相同就要求改为 update; 计数正则还会识别 “3 cities / 5 dishes” 之类的名字,并在已有 multi-city / multi-dish 父 Skill 时拒绝。 delete 不需要再验证正文,因为它本来就没有新 spec。上面的候选会同时得到“缺 when_to_use、步骤不足、计数特化”三条理由。 具体字段与名称规范化逻辑见 defaultSpecGate.Validate

// evolution/gates.go(节选)
if c.Action == RevisionActionDelete {
    return &SpecReport{Passed: true}, nil
}
if c.Spec == nil {
    return &SpecReport{Passed: false,
        Reasons: []string{"missing spec body"}}, nil
}
if strings.TrimSpace(c.Spec.Description) == "" {
    reasons = append(reasons, "missing description")
}
if strings.TrimSpace(c.Spec.WhenToUse) == "" {
    reasons = append(reasons, "missing when_to_use")
}
if len(c.Spec.Steps) < minSteps {
    reasons = append(reasons, "not enough steps")
}
if c.Action == RevisionActionCreate {
    cand := canonicalSkillName(c.Spec.Name)
    for _, ex := range existing {
        if canonicalSkillName(ex.Name) == cand {
            reasons = append(reasons, "duplicate; use update")
            break
        }
    }
    if matchesQuantifiedSibling(c.Spec.Name, existing) != "" {
        reasons = append(reasons, "count-specific sibling")
    }
}
return &SpecReport{Passed: len(reasons) == 0, Reasons: reasons}, nil

delete 在最前面直接通过,说明 gate 检查的是“新正文是否合格”,不是阻止删除动作;缺 spec 则立即失败,因为后面的字段都无从读取。 其余问题只追加到 reasons,所以一次检查能同时报告缺 description、缺使用时机和步骤不足。 duplicate 与 quantified sibling 只在 create 分支出现,update 共享原 Skill 身份本来就是预期行为。最后的 len(reasons) == 0 也证明这里没有隐藏模型分数:规则没有理由才通过。

SafetyGate:扫描的是整份正文,但规则故意很短

SafetyGate 把 description、when_to_use、steps 与 pitfalls 拼起来,扫描三类高置信模式: 常见云密钥/API token/private key;rm -rf /、写磁盘设备、下载后直接 pipe 到 shell、fork bomb; 以及 ../..//etc/passwd.ssh/id_* 等路径穿越或敏感路径。 上面的候选因此被明确标成 dangerous shell。规则短不是能力不足的疏忽,而是 false positive 会直接拒绝 revision; 这层只挡高置信字符串,不能证明工具执行语义完全安全,也不能替代 CodeExecutor 沙箱与权限控制。 扫描入口与规则组合见 defaultSafetyGate.Scan

// evolution/gates.go(节选)
body := strings.Join(append([]string{
    c.Spec.Description, c.Spec.WhenToUse,
}, append(c.Spec.Steps, c.Spec.Pitfalls...)...), "\n")

if pattern, ok := containsSecret(body); ok {
    reasons = append(reasons, "suspected secret: "+pattern)
}
if pattern, ok := containsDangerousShell(body); ok {
    reasons = append(reasons, "dangerous shell: "+pattern)
}
if pattern, ok := containsPathTraversal(body); ok {
    reasons = append(reasons, "path traversal: "+pattern)
}
return &SafetyReport{Passed: len(reasons) == 0, Reasons: reasons}, nil

SafetyGate 先把四类读者可见字段拼成一段 body,再独立执行三组正则;它没有读取 transcript、工具权限或 CodeExecutor 状态。 因此它能准确回答“Skill 正文是否出现这些高置信字符串”,却回答不了“某条看似安全的自然语言最终会不会诱导危险调用”。 三组检查都执行而不是首次命中就 return,也是为了让一次 gate report 收集完整理由。

EffectivenessGate:默认实现只看 Outcome,不会偷偷重跑 benchmark

内置 OutcomeBasedEffectivenessGate 是廉价启发式:默认分数阈值 0.8,status 为 fail/agent_error 或分数低于阈值时, revision 进入 pending_eval;partial 允许通过,delete 总是通过,没有 Outcome 时也默认通过。 它回答“是否应从一次灾难性 run 自动学习”,不回答“这个 Skill 在独立任务上是否更好”。真正的 replay、shadow traffic 或 mini-benchmark 需要实现同一个接口,GEPA 的 frozen/runtime 证据也不能冒充这道默认 gate 已经做过的事。 默认判定见 outcomeBasedEffectivenessGate

HumanGate:只决定是否暂缓,不在 worker 里等待人

ShouldHold 必须快速返回;若外部审批耗时,它应返回 hold,让 revision 进入 pending_approval。 gate 自身报错也按 hold 处理,也就是 fail closed。之后 ApprovalService.Decide 按 Skill 加锁,重新确认 revision 仍在 pending 状态;批准才发布 spec、归档旧 active、更新 active pointer 并写 audit,拒绝则只改成 rejected。 人工审批是一次独立状态转换,不是后台 worker 阻塞着等一个网页按钮。默认 ApprovalTimeout=0, revision 会一直等待显式决定;只有部署主动配置正数 timeout,sweeper 才会把超时的 pending_approval 自动 promote。状态名表示“尚未决定”,不证明一定会有人点击批准。 timeout 默认值与自动晋升语义见 workerConfig; 状态落盘与最终决定分别见 runHumanGateApprovalService.Decide

2.5 Gate 顺序、持久化与 Agent 可见性

Spec 与 Safety 都会运行并各自留下 report,便于一次看到结构和安全问题;只有两者都通过才跑 Effectiveness,所有自动门禁通过后 才跑 HumanGate。任一 gate 报错都在质量判断上 fail closed。worker 会尝试把通过、拒绝或 pending revision 写入 CandidateStore;只有通过或显式 shadow mode 才调用 publisher。正常成功路径中,旧 active revision 被 archive, 新 revision 变为 active,active.txt 指针切到新 ID,后续 repository refresh 后的 Agent 才能加载它。 注意这里说的是 happy path,不是一个跨文件与存储的原子事务。

pending
  ├─ Spec / Safety fail        -> rejected      -> 只留 revision + audit
  ├─ Effectiveness holds      -> pending_eval  -> 等外部评估
  ├─ HumanGate holds          -> pending_approval -> 等 ApprovalService
  |                                              -> 或显式启用的 timeout sweeper
  └─ all pass                 -> publish -> active
                                  old active -> archived

这条状态机想保护的核心不变量是:生成候选不等于改变未来 Agent。CandidateStore 保存“曾经提出过什么”, publisher 保存供 repository 加载的当前 Skill 正文,ActivePointer 保存“哪条 revision 与发布版本对应”。三者分开,才能审计拒绝、回滚 active, 又不把被拒的实验产物伪装成线上能力。

动作 / 结果持久化发生什么何时对 Agent 可见
Create / Update 通过尝试写 revision,publisher 执行 UpsertSkill,归档旧 active 并切换 pointer。repository refresh 后,新正文进入后续 run。
Delete 通过尝试保留 delete revision;Spec/Safety 跳过,publisher 执行 DeleteSkill,旧 active 归档并清空 pointer。refresh 后该 managed Skill 消失;非 Evolution 管理的 Skill 受保护,不能删除。
Rejected / Pending尝试写 revision、gate report 与 audit,publisher 不变。不可见;等外部评估或审批作下一次状态转换。
Shadow 绕过失败 gate保留失败 report/status,递增 bypass metric,但仍调用 publisher 并切 pointer。refresh 后可见,专门用于迁移期观测,不能当作正常治理模式。

一条修正后的候选怎样真正进入下一次运行

前面的坏候选不会自动被 gate “修好”;它只会被拒绝。假设 reviewer 下一次给出完整正文,reconciler 又发现已有 Recipe Cookbook - Multi-Dish,才会把计数特化的 create 改成对通用 Skill 的 update。配置了人工审批时, 成功链路可以逐步重放为:

reviewer candidate
  name: "Recipe Cookbook - 3 Dishes"
  when_to_use: "需要收集多道菜并生成文件时"
  steps: [检查输入, 分别检索, 校验字段, 写入 artifact]
        |
        v
reconcile: create -> update "Recipe Cookbook - Multi-Dish"
        |
        v
SpecGate pass -> SafetyGate pass -> EffectivenessGate pass
        |
        v
HumanGate hold -> revision status=pending_approval -> CandidateStore
        |
        v
ApprovalService.Decide(approved)
  -> publisher.UpsertSkill(new spec)
  -> archive old active revision
  -> ActivePointer.Set(new revision ID)
  -> WriteRevision(status=active) + audit
        |
        v
application refreshes repository -> future Agent can load the new Skill

这条链路中,reviewer 只产出候选;自动 gate 与 benchmark evidence 用来验证; publisher 写入并 refresh repository 后,正文就会对未来 Agent 可见;revision、active pointer 与 audit 同步成功,才说明这次 rollout 的治理证据也完整。若 pointer 写失败,runtime 可见性与治理状态会分叉,不能把它误判成健康采用。

Shadow 尤其容易被误读成“灰度但不生效”。这里恰好相反:WithApprovalGateShadow(true) 的目的,是先观察 gate 会拦住什么,同时保持旧 direct-publish 行为,所以失败候选仍可能真实写入 managed library。它留下失败状态与 shadow_mode_bypassed 指标,方便比较“如果强制 gate 会发生什么”;生产开始依赖 gate 后应关闭 shadow, 否则 report 只是告警,不是隔离。实现顺序见 processRevisionpublishRevision

// evolution/worker.go(节选)
gatePassed := w.runGates(ctx, rev, existing, outcome)
if !gatePassed && rev.Status == RevisionPending {
    rev.Status = RevisionRejected
}

if store != nil {
    if err := store.WriteRevision(ctx, rev); err != nil {
        log.WarnfContext(ctx, "write revision failed: %v", err)
    } else {
        w.bumpGateMetric(func(m *approvalGateCounters) {
            m.RevisionsWritten++
        })
    }
}

shouldPublish := gatePassed || w.approvalGateShadow
if !shouldPublish {
    w.auditReject(ctx, rev, store)
    return false
}
if !gatePassed && w.approvalGateShadow {
    w.bumpGateMetric(func(m *approvalGateCounters) {
        m.ShadowModeBypassed++
    })
}
return w.publishRevision(ctx, rev, actionLabel, gatePassed, scope, scoped, store)

WriteRevision 位于 publish 判断之前,所以正常成功时 rejected 也会留下候选正文和 report。 但错误分支只记录 warning,并没有 return:落盘失败的通过候选仍可能进入 publishRevision;shadow 打开后, OR 条件甚至会把 gate 失败候选送进去。代码也没有把 gatePassed 改成 true,所以失败状态和 bypass 指标仍会保留。 这正是“观测但不拦截”,不是灰度隔离,也说明部署必须监控 CandidateStore 写失败。

发布链路不是事务:每个失败点由谁恢复

失败点当前源码行为可能留下的状态与恢复责任
首次 WriteRevision记录 warning,pipeline 继续。通过或 shadow 候选仍可能发布,但 revision 证据缺失;运维需告警并补齐治理记录。
Skill 锁 / parent revision 校验在 publisher 之前失败并 return false;stale parent 会把候选标成 rejected。live Skill 不变;调用方应基于新的 active revision 重新优化,不能强行重放旧候选。
UpsertSkill / DeleteSkillpublishRevision 返回 false,不切 pointer。CandidateStore 可能已有 revision,live Skill 未变;按 revision 状态幂等重试 publisher。
发布后的 revision 状态回写第二次 WriteRevision 的错误被忽略,继续归档和切 pointer。live 正文已变,但 CandidateStore 仍可能显示 pending;用 publisher、pointer 与 revision 三方对账。
归档旧 activepublisher 已成功后才执行;失败会 return false。live 正文可能已更新,pointer 仍指旧版本;需要修复 archive/pointer 后再 refresh。
ActivePointer.Set/Clear只记录 warning,仍写 audit、递增 promoted metric 并返回 true。repository 可能 refresh 新正文,而治理 pointer 仍旧;必须把 pointer 失败设为高优先级告警。
Audit append错误被忽略。运行结果可能正确但审计不完整;audit 需要独立完整性监控。

这张表不是说实现“没有治理”,而是指出 owner:gate 决定质量状态,publisher 与 repository refresh 拥有 Agent 可见的正文, ActivePointer 拥有治理版本映射,CandidateStore 与 audit 拥有证据。它们没有共同事务,所以恢复也不能只重跑 Reviewer; 应以同一个 RevisionID 对账并幂等收敛各自状态。

三、第一组 SkillCraft Benchmark:在线学习有没有用

3.1 数据集故意让任务规模逐级放大

SkillCraft 在五个工具任务家族上构造任务:Open-Meteo 天气、Recipe 菜谱、World Bank 指标、Cat Facts 与 Pokémon。 每个家族有 e1/e2/e3/m1/m2/h1 六档规模,一轮共 30 个任务。baseline 每次从空白开始;evolution arm 允许后台 reviewer 把前面任务的经验写成 managed Skill,后续任务可用 skill_load 读取。 完整报告见 SkillCraft Evolution report

主矩阵使用 GPT-4o-mini 作为 Agent 与 reviewer,比较 pass rate、官方质量、Agent token、reviewer token 与 end-to-end token。 三次运行给每个 arm 90 个 task-case,既看平均值,也保留单个任务出现循环时的长尾成本。

3.2 主结果:提升主要来自避免少数灾难性循环

主矩阵(n=90/arm)BaselineEvolution变化
Pass rate84.4%87.8%+3.3pp
End-to-end tokens/task272,653183,435-32.7%
skill_load 使用率0%74.4%Skill 确实进入后续运行。

但不能把 -32.7% 解读成“每个任务都省三分之一”。报告中的单案例显示,几个原本会反复调用工具的灾难性循环被 Skill 截断, token 节省达到 88.7%–94.6%;这些长尾案例主导了总体差值。也就是说,在线 Evolution 的首要价值更像把偶发失控变成有步骤可循, 而不是让每个正常任务都均匀变快。

3.3 重复运行与累计轮次说明 Skill library 会趋于稳定

在两个重点家族、五次运行、每臂 60 个任务的矩阵里,pass rate 从 95.0% 到 98.3%,end-to-end token 下降 17.3%; token 标准差从 46,029 降到 6,387,skill_load 达 98.3%。连续五轮 warm run 后,library 停在 6 个 Skill,没有继续线性膨胀; 平均 pass 从 93.3% 到 95.0%,token 下降 28.7%。冷启动第一轮可能更贵,后续才回收学习成本。

Gate 的实验边界也要诚实。这组 run 里 69/69 候选都被 promoted,没有真实 reject;因为 reviewer 输出先被 reconciler 清理过。它证明了 revision pipeline 没有挡住有效候选,却没有用线上数据证明 gate 的拒绝质量。拒绝路径当时主要由单元测试覆盖,这正是后面需要离线 holdout 的原因。

四、在线、离线与是否自动上线是两条轴

“在线 Evolution” 最容易被误解成“数据来自实时流量”,“离线 GEPA” 又被误解成“只能用人工数据”。真正的分界是: 优化步骤是否在实际任务流中随每个 Session 增量发生。数据来源不是决定条件。

问题在线 / 持续学习离线 / 批量优化
优化何时发生真实 run 完成后异步 review,结果影响后续 run。请求链路之外,对冻结数据反复搜索和反事实评估。
数据可以来自哪里通常是实际 Session,也可来自受控运行。人工构造、benchmark,或从线上流量冻结出的样本都可以。
状态怎样变化managed library 随 Session 序列增量演化。seed 与候选在固定 Feedback/Validation/Holdout 上批量比较。
主要偏差顺序效应、单次轨迹噪声、当前任务过拟合。数据集不代表生产、搜索预算过拟合、模型迁移失败。

“自动上线还是人工审批”是另一条治理轴。概念上,在线 revision 可以要求人工审批,离线候选也可以由部署策略自动发布; 两者不由在线/离线身份决定。本文所读的 GEPA 实现更保守:外部评估候选即使通过自动 gate,提交后仍进入 pending_approval,理由是 externally evaluated revisions require approval。默认等待人工; 但部署若显式启用 WithApprovalTimeout,超时 sweeper 也可以自动 promote。这里要区分通用概念、revision 状态和部署策略。

五、为什么还要 GEPA 式离线优化

在线 Evolution 每完成一个 session 就复盘一次,优点是贴近真实运行,缺点是证据不稳定:当前任务既提供教训,又影响候选; reviewer 可能学到只对这一题有用的技巧;连续 managed state 还会让前后任务互相影响。要回答“这个 revision 值得发布吗”, 需要把发现候选与证明候选拆开。

已合入的 optimizer 受 GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning 启发: 不用 reward gradient 直接更新参数,而是让模型阅读执行轨迹与 evaluator feedback,用自然语言提出小幅修改,再保留在不同样本上各有优势的候选。 这里实现的是面向 SkillSpec 的 pure-Go 版本,不声称复现论文全部任务或论文数值。

5.1 谁启动 GEPA:外层实验程序,而不是在线 Evolution worker

GEPA optimizer 是一个显式调用的库对象,不是监听真实 Session 的常驻 service。人、CI 或 benchmark harness 先准备 seed Skill、 三份冻结数据和一个 Evaluator,再在请求链路之外调用 Optimize(ctx, Request)。Evaluator 负责“拿候选跑这一批 case, 每题返回分数、反馈和 trace”;reflector 才根据 feedback 提议修改。engine 只负责预算、候选池、paired seed、实验记录与最终 holdout, 不会自己连接线上流量寻找样本。

human / CI / benchmark harness
  ├─ freeze Dataset{Feedback, Validation, Holdout}
  ├─ provide Evaluator(candidate, cases, seed)
  ├─ construct GEPA(reflection model, evaluator, budgets)
  └─ Optimize(seed SkillSpec)
       ├─ search + validation + holdout
       ├─ return Result                         // 默认停在实验报告
       └─ Submit=true + RevisionSubmitter       // 可选
            └─ pending_approval revision
                 └─ manual approval / configured timeout
                      -> publisher -> repository refresh
// evolution/optimization/optimization.go 与 gepa.go(源码节选)
type Request struct {
    Seed             *evolution.SkillSpec
    Dataset          Dataset
    Scope            skill.SkillScope
    ParentRevisionID string
    Submit           bool
}

type Optimizer interface {
    Optimize(context.Context, Request) (*Result, error)
}

func (o *gepaOptimizer) Optimize(
    ctx context.Context, req Request,
) (*Result, error) {
    return o.engine.optimize(ctx, req, o.search)
}

因此“offline”说的是搜索与反事实评估发生在实际任务流之外,不要求数据必须人工编写:Dataset 完全可以由线上 Session 脱敏、筛选后冻结得到。 默认 Optimize 只返回结果,不改 Skill;只有调用方设置 Submit=true 并注入 RevisionSubmitter, promotion-eligible 候选才进入同一 revision governance。当前实现会把这种外部评估候选写成 pending_approval,默认等待人工,显式配置 timeout 时则可自动 promote;无论采用哪种策略, 都把“实验认为更好”和“运行环境允许生效”继续分开。接口与控制流见 optimizer contracts

这个公开接口里没有 Session、Event channel 或后台 queue,只有调用方明确传入的 seed、冻结 Dataset 与提交开关; Optimize 也只是把一次有界实验交给 engine。由此可以从类型层面确认 GEPA 不是在线 worker 的隐藏分支。 Submit=false 时 Result 停在调用方手里;即使为 true,也只是允许 engine 在满足 promotion 条件后调用另一个 RevisionSubmitter,而不是让 optimizer 自己写 live Skill。

5.2 先把 Dataset 合同冻结,才能谈优化

optimizer 的 Request 不是一袋随时变化的 examples。它要求 seed SkillSpec、带 ID 与 version 的 Dataset, 以及彼此不重复的 Feedback、Validation、Holdout case ID。Feedback 与 Validation 不能为空;若调用方要 submit,三份 split 各至少 10 个 case。Evaluator 必须对每个 case 返回一个 0–1 score,还可以附 output、feedback、trace 与 objectives; 搜索只用标量 score 作生存判断,其他 objectives 留在报告里,不会暗中改变排序。

Split谁能看只允许做什么
FeedbackEvaluator + reflector暴露失败轨迹,产生 mutation。
ValidationEvaluator 与候选选择器固定 seed 下比较候选,决定 search winner。
Holdout搜索结束后的 engine只确认 winner,不回流到 reflection。

默认预算也被写进 engine:最多 10 次迭代、1,000 次 metric call、每次 reflection 看 3 个 feedback case,并支持总时限。 validation 先评 seed,holdout 的预算会提前保留,避免搜索把 metric call 用光后只留下一个无法确认的 winner。

5.3 每次只改一个组件,让因果更容易读

一个 Skill 被拆成 description、when_to_use、steps 与 pitfalls。每轮从反馈集抽 minibatch,用同一个 paired seed 分别评估 parent 与 child; reflector 只改其中一个组件,并必须让 child 在这批样本的总分严格提高,否则立即拒绝。通过 minibatch 的 child 再跑固定 validation, 进入候选矩阵。

seed SkillSpec
  -> select a parent from instance-level Pareto fronts
  -> evaluate parent on feedback minibatch with seed S
  -> reflect one component
  -> evaluate child on the same minibatch with seed S
  -> reject unless child strictly improves
  -> evaluate survivor on validation
  -> after search, compare best candidate with seed on untouched holdout

reflector 收到的是被截断、去除不可信控制字段的 case 记录,以及当前轮唯一允许修改的组件;它必须返回结构化 JSON, runtime 也只把该字段应用到原 spec。输出无效、字段没有变化、候选 hash 已存在时,本轮记录 rejection 后继续。 parent 与 child 在同一个 feedback minibatch、同一个 paired seed 上评估,只有 child 总分严格大于 parent 才进入完整 validation;这能减少采样噪声被误判成改进,但不能消除 provider 非确定性。

5.4 Pareto 不是“平均分最高者永远生孩子”

假设候选 A 擅长简单菜谱,B 擅长多步骤菜谱,A 的平均分略高。只按平均分选 parent,B 的经验会很快消失。 optimizer 记录每个 validation case 的最高分候选,形成 instance-level frontier;只要某候选仍覆盖别人没有覆盖的样本优势, 就有机会继续被选。最终选择仍看 validation mean,但搜索过程不会过早抹掉互补路径。

这也解释一个看似奇怪的行为:child 的 validation 平均分暂时不是最高,仍可能进入 pool。只要它在某些 case 上形成别人没有的 frontier,它就为后续 mutation 提供不同 parent。搜索结束时再按固定 validation mean 选最终 best,而不是把探索多样性 和部署判定混成一条规则。

5.5 Holdout 对 reflector 不可见,提交也不等于上线

Dataset 明确分 feedback、validation 与 holdout。feedback 带轨迹与文字反馈供反思;validation 选择候选;holdout 只在搜索结束后比较 seed 与 winner, 不发送给 reflection model。只有 holdout 达到最小提升且关键 case 不退步,候选才是 promotion-eligible。 即便调用方选择 submit,它也只是通过窄 RevisionSubmitter 进入既有 revision governance,默认不会直接改 live skill。

promotion 还有三个明确 no-op:最终 best 仍是 seed,说明没有 validation 改进;没有 holdout,说明无法独立确认; candidate 的 holdout delta 未达到配置下限,或任何标记为 Critical 的 case 比 seed 退步。满足这些条件只得到 PromotionEligible=true,不等于 active。当前实现的 submit 会把 dataset ID/version、baseline/candidate score、delta、 case 数与 objectives 一起写进 revision evidence,再进入 pending_approval

// evolution/optimization/engine.go 与 evolution/submission.go(节选)
e.assessPromotion(req, seed, best, baselineHoldout, candidateHoldout, result)
if req.Submit {
    submissionErr = e.submitCandidate(ctx, req, best, result)
}

if !result.PromotionEligible {
    result.SubmissionReason = result.PromotionReason
    return nil // 只返回实验结果,不创建 revision
}
revision, err := submitter.SubmitRevision(ctx, evolution.RevisionRequest{
    ParentID: req.ParentRevisionID,
    Spec:     cloneSpec(best.spec),
    Evidence: &evolution.RevisionEvidence{
        DatasetID:      req.Dataset.ID,
        BaselineScore:  result.BaselineHoldout.Score,
        CandidateScore: result.CandidateHoldout.Score,
        Delta:          result.CandidateHoldout.Score - result.BaselineHoldout.Score,
    },
})

// SubmitRevision 内部
if !w.runAutomaticGates(ctx, rev, existing, outcomeFromEvidence(rev.Evidence)) {
    return w.rejectSubmittedRevision(ctx, rev, store)
}
return w.holdSubmittedRevision(ctx, rev, store) // pending_approval

这里的两个 if 把边界写死了:PromotionEligible 为 false 时,即使调用方设置了 Submit=true 也不会创建 revision;满足 holdout 条件后,submitter 仍重新校验 target、parent revision 和自动 gate, 最终只写一条 pending_approval revision。默认要由另一次 ApprovalService.Decide 推进; 显式配置 approval timeout 时,也可能由 sweeper 自动进入 publisher。 对应源码在 Optimize submission branchpromotion 与 submitsubmitRevision

六、GEPA Benchmark:发现、冻结确认、运行回放分三关

6.1 同一 SkillCraft,换成三阶段证据

阶段数据用途它可以做出的决定
Searchfeedback 反思,validation 选候选。可以 abstain;不是每个家族都必须产生新 Skill。
Frozen confirmation固定候选,在独立 seed 与 untouched holdout 上对比。可以推翻 validation winner。
Operational replay把固定候选放回完整在线 Evolution loop。可以推翻 frozen winner。

最终同模型矩阵使用 GLM-5.2,同时作为 Agent 和 reviewer;temperature 0、最大响应 8,192 tokens、最多 80 次工具迭代。 仍是五个家族 × 六档任务,每个 arm 30 个任务;root seeds 701–703,三臂 baseline、evolution、optimized_evolution 每臂 90 个, 总共 270 个 arm-cases。奇偶 seed 反转整臂顺序,并让三臂共享同一 task seed,减少顺序与采样偏差。

6.2 Search 的第一项能力是“不改”

Cat Facts 与 Pokémon 保留 seed;Weather 虽产生 mutation,但 validation 仍选 seed;只有 Recipe 与 World Bank 进入 frozen confirmation。 这很重要:optimizer 完成若干轮迭代,不代表一定有值得发布的候选。

候选Validation / Holdout决定
Reviewer 修复后的 Recipe SkillHoldout 质量 95.50% → 98.35%;pass 100% 不变;tokens -6.57%。保留为 runtime replay 候选
通用 Recipe 效率 mutationValidation tokens -10.35%;但 holdout pass 100% → 87.5%,质量 95.50% → 83.41%。从实验候选池淘汰
World Bank 效率 mutationHoldout pass/质量均 100%;tokens -8.52%。暂时保留,等待 runtime replay

中间那条是最有价值的 bad case:它在 validation 上更省,却在 untouched e3 上失败。如果实验只报 search winner, 这个候选会被错误地推荐提交。Frozen gate 的作用不是再跑一次漂亮分数,而是主动寻找推翻理由。

6.3 Runtime 证据支持 Recipe,淘汰 World Bank

家族(optimized vs evolution)Pass deltaQuality deltaE2E token delta决定
Recipe(有 overlay)0.00pp+0.32pp-14.75%实验支持提交
World Bank(有 overlay)0.00pp0.00pp+3.29%实验淘汰

Recipe 的 18 个任务在两臂都 100% 通过,三个 root seed 的 end-to-end token 分别下降 6.61%、24.25% 与 12.52%,方向一致。 World Bank 在 frozen holdout 上省 token,放回连续 managed-skill state 后却在三个 seed 都变贵,因此不应提交为 revision。 这说明 frozen benchmark 是必要条件,不是生产充分条件。

Runtime replay 仍是实验,不是生产激活记录。Benchmark 把固定候选作为 overlay 放进完整运行环境比较,证明 Recipe 值得进入下一道治理;它没有证明 harness 调用了 SubmitRevision,更没有证明审批决定或 timeout promotion、active pointer 更新和生产 repository refresh 已发生。所以上表的“支持提交”是实验结论,不是 revision status。

6.4 为什么不能拿全局 +1.25pp 给候选邀功

全局(n=90/arm)BaselineEvolutionOptimized Evolution
Pass rate97.78%97.78%98.89%
Official quality95.98%95.96%97.21%
E2E tokens/task305,240352,971362,368

Optimized arm 全局质量看起来比 Evolution 高 1.25pp,但主要差值来自没有任何 offline overlay 的 Pokémon:Evolution arm 恰好有两次没有生成 artifact。 这属于 runtime variance,不能归因给 Recipe 或 World Bank 候选。只汇总真正改变的两个家族,pass 都是 100%,质量 +0.16pp, token -6.77%;再分家族才看出 Recipe 提供全部收益,World Bank 是负贡献。

另一次把 GLM-5.2 发现的 overlay 直接放到 GPT-5.2 跑,optimized 相对 evolution 质量 -0.08pp、token +5.79%,未过 gate。 Skill improvement 不是自动跨模型迁移的 prompt 魔法;模型、工具响应形状与执行预算都属于候选的适用边界。

七、把两条 Evolution 线合起来读

问题在线 Evolution离线 Optimizer
候选从哪里来真实 run 的 session delta 与 outcome。固定数据集上的 paired feedback 与反思。
擅长什么持续收集真实长尾经验,低人工维护。隔离变量、保留互补候选、用 holdout 反证。
主要风险轨迹噪声、顺序影响、过拟合单个 session。优化成本高,数据集可能不代表生产。
怎样进入生产online worker 可以直接生成 revision。只有 Submit=true 且 holdout 合格才提交 revision;提交后两者都进入部署实际配置的 spec / safety / effectiveness / approval governance。

真正可用的“自进化”不是一个会重写 prompt 的模型。它是一套证据管线:在线运行提供候选与真实长尾,离线搜索尝试改进,validation 选择,holdout 反证,runtime replay 检查真实状态耦合,revision gate 决定能否被未来 Agent 看见。任何一步都应允许 abstain 或 reject。

三篇到这里可以压成一句话:Context 管“这一轮怎样继续”,Memory 管“关于用户什么值得留下”,Evolution 管“未来任务怎样做得更好”。 tRPC-Agent-Go 的价值不在于把三者都叫 memory,而在于给它们不同的数据 owner、触发时机、回收路径和实验问题。

参考源码、论文与实验