三篇的最后一条边界。Summary 改当前会话给模型看的视图,Memory 保存跨会话事实与事件,Evolution 只维护可复用 Skill。它学习的是“以后遇到这类任务怎样做”,不是“这个用户是谁”。
证据状态。在线 Evolution、GEPA 式 optimizer 与两组 SkillCraft benchmark 都已进入公开 main。框架源码固定到合入 PR #2204 的 99a8667aa8ad3e4816cb3d7f1321ef35118162d7;benchmark 固定到合入 PR #18 的 ea939a734e777a53c59c47960391c92388dfaf1e。实验数字是冻结 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;见
NewService 与 EnqueueLearningJob。
默认 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 契约见 Outcome 与 ReviewInput。
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;
状态落盘与最终决定分别见 runHumanGate
和 ApprovalService.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 只是告警,不是隔离。实现顺序见
processRevision
与 publishRevision。
// 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 / DeleteSkill | publishRevision 返回 false,不切 pointer。 | CandidateStore 可能已有 revision,live Skill 未变;按 revision 状态幂等重试 publisher。 |
| 发布后的 revision 状态回写 | 第二次 WriteRevision 的错误被忽略,继续归档和切 pointer。 | live 正文已变,但 CandidateStore 仍可能显示 pending;用 publisher、pointer 与 revision 三方对账。 |
| 归档旧 active | publisher 已成功后才执行;失败会 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) | Baseline | Evolution | 变化 |
|---|---|---|---|
| Pass rate | 84.4% | 87.8% | +3.3pp |
| End-to-end tokens/task | 272,653 | 183,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 | 谁能看 | 只允许做什么 |
|---|---|---|
| Feedback | Evaluator + reflector | 暴露失败轨迹,产生 mutation。 |
| Validation | Evaluator 与候选选择器 | 固定 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 branch、
promotion 与 submit
和 submitRevision。
六、GEPA Benchmark:发现、冻结确认、运行回放分三关
6.1 同一 SkillCraft,换成三阶段证据
| 阶段 | 数据用途 | 它可以做出的决定 |
|---|---|---|
| Search | feedback 反思,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 Skill | Holdout 质量 95.50% → 98.35%;pass 100% 不变;tokens -6.57%。 | 保留为 runtime replay 候选 |
| 通用 Recipe 效率 mutation | Validation tokens -10.35%;但 holdout pass 100% → 87.5%,质量 95.50% → 83.41%。 | 从实验候选池淘汰 |
| World Bank 效率 mutation | Holdout 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 delta | Quality delta | E2E token delta | 决定 |
|---|---|---|---|---|
| Recipe(有 overlay) | 0.00pp | +0.32pp | -14.75% | 实验支持提交 |
| World Bank(有 overlay) | 0.00pp | 0.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) | Baseline | Evolution | Optimized Evolution |
|---|---|---|---|
| Pass rate | 97.78% | 97.78% | 98.89% |
| Official quality | 95.98% | 95.96% | 97.21% |
| E2E tokens/task | 305,240 | 352,971 | 362,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、触发时机、回收路径和实验问题。
