Skip to content

feat: 新增评测-优化自动回归与提示词优化闭环示例 (#91)#205

Open
EricChan2028 wants to merge 1 commit into
trpc-group:mainfrom
EricChan2028:feat/91-eval-optimize-loop
Open

feat: 新增评测-优化自动回归与提示词优化闭环示例 (#91)#205
EricChan2028 wants to merge 1 commit into
trpc-group:mainfrom
EricChan2028:feat/91-eval-optimize-loop

Conversation

@EricChan2028

@EricChan2028 EricChan2028 commented Jul 19, 2026

Copy link
Copy Markdown

PR: feat: 新增评测-优化自动回归闭环示例 eval_optimize_loop (#91)

  • Branch / 分支: feat/91-eval-optimize-loop (worktree /home/seat1163/work/Tencent/issue-91)
  • Commit / 提交: 9471072bca6f4c36aac0083650c9b7b25ef2c062 (base: 099b571 = upstream default-branch tip, "feat: 支持动态创建子agent能力 (feat: 支持动态创建子agent能力 #109)")
  • Patch / 补丁: /home/seat1163/work/Tencent/docs/patches/issue-91/0001-feat-91.patch(已在空树上 git apply --check 验证可干净应用)
  • Diff: 1 个新增目录 examples/optimization/eval_optimize_loop/(43 files changed, +7185,全部新增);零 SDK 文件改动

中文

摘要

实现 issue #91 要求的「评测 → 失败归因 → prompt 优化 → 回归验证 → 产物审计」六阶段自动闭环,回答一个唯一的问题:优化器改出来的候选 prompt 到底值不值得接受? 全部代码位于新增示例目录 examples/optimization/eval_optimize_loop/,真跑 SDK 的 AgentEvaluator(LocalEvalService)与 AgentOptimizer(GEPA 反思循环),仅模型层通过 ModelRegistry 的 provider 正则路由替换为三个确定性 fake provider —— 不改一行 SDK、零 API Key、三场景全流程 < 3 分钟。交付 6 条评测 case(3 train + 3 val)、三个内置场景(优化成功 / 优化无效 / 优化后过拟合退化)、JSON+Markdown 双格式报告与契约校验器、65 个 pytest、中英双语 README 与 471 字方案设计说明。

动机

AgentOptimizer 能自动改出分数更高的 prompt,但「分数变高」≠「值得上线」:

  • 优化器黑盒 call_agent 模式下只见弱指标(SDK 硬性禁配轨迹/召回 metric),信号弱于验收套件;
  • 优化器的调参集若与训练集同源,它会过拟合而毫无察觉
  • 没有逐 case 对比,无法发现「提升是靠牺牲原本通过的关键 case 换来的」;
  • 没有审计产物,改出来的 prompt 无法进入生产评审。

本 PR 把这四个风险各自做成机制:完整验收套件独立复评、过拟合守卫(train↑∧val↓ 必拒)、保护 case + 新增 hard fail 闸门、SDK 原生 rounds/ 审计目录 + pipeline 快照。

设计

① baseline 评测           ② 失败归因            ③ 优化执行
train+val × 4 metric  →  6 类失败类型聚类   →  AgentOptimizer(GEPA)
(逐 case 分数/轨迹)      (每案给可读理由)      (优化 2 个 TargetPrompt 字段)
                                                      │
⑥ 审计落盘            ⑤ 接受策略             ④ 候选验证
报告 json+md      ←  六道可配置闸门      ←  独立 train/val 复评
每轮候选/成本/seed     (全过才接受)           逐 case delta 对比

关键决策(详见 DESIGN.mdREADME.md §7):

  1. 零 API Key 不改 SDK:judge / reflection LM 配置的 provider_name 会走 ModelRegistry.create_model("{provider}/{model}") 正则路由;注册 fake-agent / fake-judge / fake-reflection 三个确定性 provider 即可离线全跑(幂等注册,探测用固定串规避 resolve 的 lru_cache)。
  2. 指令 DSL 让离线优化「真实」:fake agent 解析 prompt 里的 <!-- directives: ... --> 注释块(output_format / unit_normalization / knowledge / memorize 四个开关),于是「改 prompt」在离线环境下产生真实的行为与分数差异,GEPA 反思循环端到端运转,而非 mock 分数。
  3. 优化器弱指标 vs 验收全指标的刻意信息差optimizer.json 只配响应精确匹配(黑盒模式 SDK 禁配轨迹/召回);pipeline 复评用 4-metric 验收套件(轨迹 + 精确响应 + rubric + 知识召回)——「优化器说变好了,还要独立复评过才算数」。
  4. 过拟合场景的机理构造(非造假数据):overfit 候选 = memorize: train_table 查表复读训练/调参集样本;优化器验证集换成泄漏 probe 集(其视角 0/3→3/3 严格提升);独立 val 复评 1/3→0/3。过拟合守卫 + 保护 case + 新增 hard fail 三门齐拒,拒绝理由按严重度点名根因闸门。
  5. 候选换入/换出永不污染源文件TargetPrompt.write_all(原子写)+ try/finally 快照还原;写回源文件需要 gate 通过 ∧ 显式 --apply 双条件。e2e 测试断言跑完后 prompts 与快照 byte 级一致。
  6. 归因规则通用化:六类失败归因只依赖框架 metric 结果的结构(调用名字多重集、JSON 可解析性、rubric 明细),不依赖本示例的 case 内容 —— 隐藏样本同样适用;未覆盖的 metric 失败按兜底映射,保证每个失败 case ≥1 条带证据的中文理由。
  7. 报告契约REQUIRED_TOP_LEVEL_KEYS + validate_report(pytest 与 CLI --check 共用同一实现),「失败必有理由」写进契约;配置层 extra="forbid",闸门键名拼写错误 fail-fast 而非静默回落默认值。

验收标准映射

# 验收标准 实现位置 测试
1 6 条公开 case 全部可运行并生成完整报告 run_pipeline.py(CLI 六阶段编排);data/train.evalset.json + data/val.evalset.json(3+3);loop_pipeline/report.py tests/test_pipeline_e2e.py::test_success_scenario_end_to_end(6 个 eval_id 全部在 baseline per_case,报告+审计产物逐项断言);test_report_contract
2 隐藏样本上接受/拒绝决策准确率 ≥ 80% loop_pipeline/gates.py 六道闸门只依赖 delta/成本/时长的通用结构,阈值全部在 pipeline.json 可配,与 case 内容零耦合 tests/test_gates.py::DECISION_MATRIX —— 12 组合成决策场景(接受 / 各类拒绝 / 开关组合)12/12 判定正确(100%)
3 「验证集退化但训练集提升」的过拟合必拒 gates.py::overfit_guard(train↑∧val↓)+ protected_cases + no_new_hard_fail;overfit 场景内置该结局 test_pipeline_e2e.py::test_overfit_scenario_rejected(train +0.667 / val −0.333 → REJECT);test_gates.py::test_overfit_reason_mentions_overfitting(理由点名「过拟合”)
4 失败归因分类准确率 ≥ 75%,每个失败 case ≥1 条可解释原因 loop_pipeline/attribution.py:6 类规则归因 + 「轨迹在上游」根因优先级 + metric 兜底映射;report.py::validate_report 把「失败必有理由」做进契约 tests/test_attribution.py::test_six_failure_types_classified —— 6 类 × 2 合成样本 12/12 全对(100%)test_fallback_guarantees_explanation_for_unknown_metrictest_every_failed_case_on_real_baseline_has_reason
5 fake model / trace mode 完整 pipeline ≤ 3 分钟 三个 fake provider(loop_agent/fake_models.py)+ trace 数据集(data/trace_baseline.evalset.jsonevalMode: "trace");报告内单场景进程内耗时 1.4–2.9s test_pipeline_e2e.py::test_all_scenarios_under_time_budget(三场景 <180s);test_trace_mode_baseline;独立验证时 env -u OPENAI_API_KEY ... 清空密钥重跑成功
6 报告含 baseline 分、candidate 分、逐 case delta、gate 决策、接受/拒绝理由 loop_pipeline/report.pyREQUIRED_TOP_LEVEL_KEYS 契约、build_report / validate_report / render_markdown;CLI --check 复用同一校验器 test_report_contract(三场景报告全字段断言);破坏性验证:删顶层字段 / 清空 failure_reasons → --check exit 1 并逐条报错

六阶段具体要求逐条覆盖

阶段要求 落点
① Baseline 评测(train+val、metric 分、pass/fail、失败原因、关键轨迹) loop_pipeline/evaluate.py::run_evalAgentEvaluator.get_executer)→ CaseEvalRecord(metric 分/状态/理由、rubric 明细、实际与期望工具轨迹与回答)
② 失败归因(题面点名的六类) loop_pipeline/attribution.py:wrong_tool_call / wrong_tool_args / knowledge_recall_miss / format_violation / llm_rubric_fail / final_answer_mismatch
③ 优化 ≥1 个 TargetPrompt loop_pipeline/optimize.py 真跑 AgentOptimizer.optimize(GEPA),同时优化 system_prompt + skill 两个字段(超出「至少一个」);update_source=False
④ 候选验证集重跑 + 逐 case 对比 loop_pipeline/regression.pyevaluate_candidate(write_all 换入 + finally 还原)+ compute_delta(new_pass / new_fail / score_up / score_down / unchanged)
⑤ 可配置 gate pipeline.json 六道闸门:提升双阈值 / 禁新增 hard fail / 保护 case / 过拟合守卫 / 成本预算 / 时长预算
⑥ 审计落盘 SDK 原生 optimize/rounds/round_*.jsonconfig.snapshot.json(含 seed)、run.log、baseline_prompts/、best_prompts/ + pipeline 侧 baseline/candidate 逐 case 记录、归因明细、配置快照;报告记录 best_prompts sha256
输入文件(题面标准名) data/train.evalset.jsondata/val.evalset.jsonoptimizer.jsonloop_agent/prompts/{system,skill}.md
输出 optimization_report.json(含 baseline / candidate / delta / gate decision / 归因统计)+ optimization_report.md(人话版结论)
300–500 字设计说明 DESIGN.md 正文 471 汉字(归因 / 接受策略 / 防过拟合 / 审计四主题齐备)+ English abstract

离线运行方式(无任何 API Key / 环境变量)

cd /home/seat1163/work/Tencent/issue-91

# 全部 65 个测试
~/.venvs/trpc91/bin/python -m pytest examples/optimization/eval_optimize_loop/tests -q

# 三场景端到端(success=ACCEPT, no_effect=REJECT, overfit=REJECT)
~/.venvs/trpc91/bin/python examples/optimization/eval_optimize_loop/run_pipeline.py --scenario all

# trace 模式(预录轨迹评测归因,不执行 agent)
~/.venvs/trpc91/bin/python examples/optimization/eval_optimize_loop/run_pipeline.py --baseline-from-trace

# 报告契约校验
~/.venvs/trpc91/bin/python examples/optimization/eval_optimize_loop/run_pipeline.py \
    --check examples/optimization/eval_optimize_loop/sample_output/success/optimization_report.json

# lint / format(repo 配置:flake8 max-line 120、yapf pep8/120)
~/.venvs/trpc91/bin/python -m flake8 examples/optimization/eval_optimize_loop --max-line-length=120
~/.venvs/trpc91/bin/python -m yapf --diff --recursive examples/optimization/eval_optimize_loop

样例产物路径

  • examples/optimization/eval_optimize_loop/sample_output/success/optimization_report.{json,md} — ACCEPT(val 1/3→3/3)
  • examples/optimization/eval_optimize_loop/sample_output/no_effect/optimization_report.{json,md} — REJECT(提升不足,全 unchanged)
  • examples/optimization/eval_optimize_loop/sample_output/overfit/optimization_report.{json,md} — REJECT(过拟合守卫,train +0.667 / val −0.333)

三份样例均为真实运行产物:独立验证时与全新重跑做结构化 diff,除时间戳/墙钟耗时字段外逐字段一致(确定性由 test_deterministic_reruns 固化)。


English

Summary

Implements the six-stage "evaluate → attribute failures → optimize prompts → regression-verify → audit" closed loop required by issue #91, answering one question: is the optimizer's candidate prompt actually worth accepting? Everything lives in the new example directory examples/optimization/eval_optimize_loop/. It genuinely runs the SDK's AgentEvaluator (LocalEvalService) and AgentOptimizer (GEPA reflection loop); only the model layer is swapped for three deterministic fake providers via ModelRegistry's provider-regex routing — zero SDK changes, zero API keys, all three scenarios end-to-end in well under 3 minutes. Ships 6 eval cases (3 train + 3 val), three built-in scenarios (successful optimization / no-effect / overfitting regression), JSON+Markdown reports with a contract validator, 59 pytest tests, bilingual READMEs, and a 471-character design note.

Motivation

A higher optimizer score does not mean a candidate is production-worthy: the optimizer only sees weak metrics in black-box call_agent mode (the SDK hard-rejects trajectory/recall metrics there); a leaked tuning set makes it overfit without noticing; without per-case deltas you cannot tell whether the gain came at the cost of previously passing critical cases; and without audit artifacts the change cannot pass production review. Each risk is turned into a mechanism: independent re-evaluation with the full acceptance metric suite, an overfit guard (train up AND val down → mandatory reject), protected-case and no-new-hard-fail gates, and SDK-native rounds/ audit output plus pipeline snapshots.

Design highlights

  1. Zero API keys without touching the SDK — judge/reflection LM configs support provider_name, which routes through ModelRegistry.create_model("{provider}/{model}"); three deterministic fake providers (fake-agent, fake-judge, fake-reflection) are registered idempotently.
  2. A directive DSL makes offline optimization real — the fake agent parses <!-- directives: ... --> blocks in the prompts, so rewriting a prompt genuinely changes behavior and scores; the GEPA reflection loop runs end-to-end instead of being mocked.
  3. Deliberate information gap — the optimizer sees only exact-response matching; the pipeline re-evaluates with the full 4-metric suite (trajectory + response + rubric + knowledge recall) on an independent validation set.
  4. Mechanistic overfit scenario — the overfit candidate memorizes train/probe samples via a lookup table while the optimizer is fed a leaked tuning set (its view: 0/3→3/3); independent re-eval shows val 1/3→0/3, and the overfit guard, protected cases, and no-new-hard-fail gates all fire, with the rejection reason naming the root-cause gate by severity.
  5. Source prompts are never polluted — candidate swap-in uses TargetPrompt.write_all with a try/finally snapshot restore; writing back requires gate pass AND an explicit --apply flag.
  6. Generalizable attribution — the six failure-type rules depend only on framework metric-result structure (tool-name multisets, JSON parseability, rubric details), not on this example's cases, with a fallback mapping guaranteeing at least one evidence-backed reason per failed case.
  7. Report contractREQUIRED_TOP_LEVEL_KEYS + validate_report shared by tests and the CLI --check flag; configs use extra="forbid" so misspelled gate keys fail fast instead of silently falling back to defaults.

Acceptance-criteria mapping

# Criterion Where satisfied Test
1 All 6 public cases runnable with complete reports run_pipeline.py; data/{train,val}.evalset.json; loop_pipeline/report.py test_success_scenario_end_to_end, test_report_contract
2 Accept/reject decision accuracy ≥ 80% on hidden samples loop_pipeline/gates.py — six gates depend only on generic delta/cost/duration structure; all thresholds configurable in pipeline.json test_gates.py::DECISION_MATRIX: 12 synthetic decision scenarios, 12/12 correct (100%)
3 Must reject overfit (val degrades while train improves) gates.py::overfit_guard + protected_cases + no_new_hard_fail; built-in overfit scenario test_overfit_scenario_rejected (train +0.667 / val −0.333 → REJECT); test_overfit_reason_mentions_overfitting
4 Attribution accuracy ≥ 75%, ≥1 explainable reason per failed case loop_pipeline/attribution.py (6 rule-based types + root-cause priority + fallback); enforced by validate_report test_six_failure_types_classified: 12/12 (100%); test_fallback_guarantees_explanation_for_unknown_metric; test_every_failed_case_on_real_baseline_has_reason
5 Full pipeline ≤ 3 min in fake/trace mode Three fake providers + data/trace_baseline.evalset.json (evalMode: "trace"); in-process per-scenario time 1.4–2.9s test_all_scenarios_under_time_budget (<180s); test_trace_mode_baseline; verified with API-key env vars explicitly unset
6 Report contains baseline/candidate scores, per-case delta, gate decision, accept/reject reason loop_pipeline/report.py contract + validator shared with CLI --check test_report_contract; destructive checks (missing fields / cleared failure_reasons → exit 1)

Run offline

cd /home/seat1163/work/Tencent/issue-91
~/.venvs/trpc91/bin/python -m pytest examples/optimization/eval_optimize_loop/tests -q   # 59 passed
~/.venvs/trpc91/bin/python examples/optimization/eval_optimize_loop/run_pipeline.py --scenario all

Sample outputs: examples/optimization/eval_optimize_loop/sample_output/{success,no_effect,overfit}/optimization_report.{json,md} — real run artifacts, deterministic (verified by structural diff against fresh reruns and by test_deterministic_reruns).

Notes for reviewers

  • Zero SDK files touched; the diff is a single new example directory (idiom reference: examples/evaluation, examples/optimization).
  • Package names loop_agent / loop_pipeline avoid sys.modules collisions with other examples (agent, pipeline) when pytest imports multiple examples in one process.
  • Hidden-sample accuracy (AC2/AC4) cannot be measured locally; the compliance mechanism is that gates and attribution rules depend only on framework result structure, and the publicly testable matrices score 12/12 (gates) and 12/12 (attribution), with all thresholds configurable.

🤖 Generated with Claude Code

评审后更新 / Post-review update(2026-07-19)

独立对抗式评审结论:0 个 major;6 个 minor 全部修复(其中第 2 项选择实做而非弱化文档):

  1. --check 现在 cwd 无关(相对路径先按 cwd 再按示例目录解析),文件缺失给友好报错而非裸 traceback。
  2. --baseline-from-trace 实做归因:对 trace 记录跑 cluster(),输出 trace_attribution.json —— 文档所述「评测与归因」现在名副其实。
  3. run_eval 异常捕获从宽 AssertionError 窄化为 _EvaluationCasesFailed(与 SDK 自身用法一致),真第三方断言失败不再被吞。
  4. _is_json_is_structured_json:仅 dict/list 计为结构化输出,纯标量期望('42'/'true')不再误判 format_violation。
  5. --apply --scenario all 改为全场景结束后统一写入,消除场景间污染(测试断言 no_effect/overfit 基线不受影响)。
  6. cost_budget 门在配置了 max_metric_calls 但预算不可用(None)时 fail-closed(detail:预算追踪不可用)。

测试:65/65 全绿(+6),flake8/yapf 均 clean。

(EN) Post-review update 2026-07-19: an independent adversarial review (6 parallel judges, execution-backed verification) found 0 majors; all 6 minors fixed (incl. implementing trace attribution rather than weakening docs); all items fixed as listed above, full suite green (pytest 65/65, flake8/yapf clean).

@github-actions

Copy link
Copy Markdown

CLA Assistant Lite bot:
Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


turrillcerza seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You can retrigger this bot by commenting recheck in this Pull Request

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@099b571). Learn more about missing BASE report.

Additional details and impacted files
@@            Coverage Diff             @@
##             main        #205   +/-   ##
==========================================
  Coverage        ?   87.90237%           
==========================================
  Files           ?         479           
  Lines           ?       44984           
  Branches        ?           0           
==========================================
  Hits            ?       39542           
  Misses          ?        5442           
  Partials        ?           0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

我已经了解足够多的上下文。让我仔细检查一下 evaluate_gatesbudget_used 的类型处理逻辑以及关于 int(budget_used) 的顾虑。

budget_used 字段是 Optional[int] 类型。gates 代码执行了 int(budget_used) —— 如果它本身已经是整数则没问题。我再检查一下 overfit 检查的边界情况:delta_val.pass_rate_delta < 0 是否严格正确?并且通过率差值(pass_rate_delta)等于 0 但分数下降的 overfit 防御场景——实际上这指的是 train 提升 + val 下降。没问题。

让我检查一个微妙的问题:在 evaluate_gates 中,overfit 防御使用 delta_val.pass_rate_delta < 0。但如果 val 通过率保持不变(0),而 train 提升且 val 分数下降,它是无法捕获到的。不过文档说明中只提到了通过率。这是设计使然。

让我核实一下 _protected_violations —— 只有当 change in ("new_fail", "score_down") 时才会标记。但保护用例可能根本不在 val 集合中 —— 这会被静默跳过。小问题。

现在让我检查一下 FakeAgentModel 流式传输的问题:_generate_async_impl 忽略了 stream 参数,但对于非流式传输,它只会生成一个响应 —— 这没问题。

让我检查一个潜在的 bug:在 run_optimization 中,update_source=False,但在 AgentOptimizer.optimize 内部,优化器在运行期间会写入源 prompt 文件(为了评估候选者),并在最后恢复。但是流水线并发地(或顺序地)在同一个 target 上运行 evaluate_candidate —— 实际上它们是顺序执行的。没问题。

关键问题:在 run_scenario 中,target = _build_target() 被构建一次,既用于 run_optimization(它通过 update_source=False 进行写入和恢复),也用于 evaluate_candidate。在优化之后,源文件被恢复为基准。然后 evaluate_candidate 进行快照并写入候选内容。没问题。

让我核实一下用于验证 budget_used 的测试 —— metric_calls_over_budget_reject 使用 budget_used=61,max=50 → 拒绝。很好。

现在,有一个真正令人担忧的问题:gates.py 第 137 行的 int(budget_used) —— 如果 budget_used 是一个浮点数或者 None。它是 Optional[int] 类型,所以如果是 None,calls_ok 会短路。没问题。但如果它是浮点数(理论上不应该发生),int() 会进行截断。问题不大。

让我检查一下 FakeJudgeModel 的 _verdict 中关于 "如果" 的处理 —— 它使用了第一个反引号 token 作为条件,但评估配置(eval config)中的 rubric 使用的是中文“如果”。匹配逻辑是 "如果" in rubric_text。评估配置中的 rubric 文本是“如果用户要求 JSON 输出...”。反引号 token 是 JSON, "result"。所以 condition=JSON,检查 JSON not in main_prompt。main_prompt 是用户查询。对于“把 3 公里换算成米,用 JSON 输出”,JSON 确实在其中。很好。

现在让我检查一个关于 knowledge_search 工具和 subset_matching: false 的潜在实际问题。让我看一个真正的问题:在 convert_distance 工具中,value 的类型是 float,但 memorize 表传递的是 int,且期望的调用使用的是 int。指标使用精确匹配。JSON 序列化 33.0 可能会不匹配。让我检查一下。

实际上,在 tool_trajectory 指标中,参数是精确匹配的。备忘录表使用 "value": 3 (int),期望调用也使用 "value": 3 (int)。而在 _tool_call_response 中,args_value = int(value) if value.is_integer() else value → int。匹配。很好。

让我检查一个具体问题:FakeAgentModel_generate_async_impl_first_user_text 用于查询检测,但在 _KM_RE 中,它匹配 [0-9]+ —— 对于“3 公里”,它捕获了“3”。但备忘录表的键需要精确的查询匹配,包括“用 JSON 输出”。_first_user_text 返回完整的用户文本。MEMORIZE_TABLE.get(query.strip())。查询如“把 3 公里换算成米,用 JSON 输出” —— 匹配。很好。

现在,关于并发计数器重置的一个真正担忧:FakeAgentModel.calls 是一个类属性,在运行中从不重置。在 run_scenario 中,calls_before 记录了增量。但在多个场景的 _amain 中,它们是按顺序运行的,且增量计算是正确的。很好。

但是等等 —— 在测试中,test_deterministic_reruns 再次运行成功场景,并比较稳定视图。假模型调用计数器是累积的,但 stable_view 并未包含调用计数。确定性视图排除了运行时/fake_model_calls。很好。

现在让我找出真正有影响的问题。让我更仔细地检查一下 evaluate_gatesint(budget_used) 的截断问题,以及当预算为 None 时的 calls_part 显示问题。

让我检查 knowledge_search 中一个值得标记的实际正确性问题:当 knowledge: off 时,简介会返回“{city}是一座很不错的城市。”。在过拟合场景中,备忘录表包含“介绍一下深圳”和“请介绍一下深圳”。但 val 包含“介绍一下杭州” —— 不在表中。很好。

现在让我检查 test_pipeline_e2e.pyall_scenarios fixture 的一个具体问题:它是模块作用域的,并在一个 asyncio.run 循环中运行所有 3 个场景。但是 FakeAgentModel.calls 等是类级别的,并且会累积 —— 测试 test_all_scenarios_under_time_budget 没问题。但有一个微妙的正确性问题:run_scenario 读取 calls_before 并计算运行时的增量。由于 fixture 顺序运行场景,每个场景的增量是正确的。

现在让我检查 --apply 路径是否存在真正的问题:if apply_on_accept and decision.accepted: await target.write_all(optimize_result.best_prompts)。这会修改源 prompt 文件。但没有回滚,并且随后的场景运行会读取被修改的 prompt!在 --scenario all --apply 中,成功场景被接受并写入源文件;然后 no_effect 场景从修改后的 prompt 开始(而不是基准)。这是一个真正的正确性/稳定性 bug。

让我核实一下 no_effect/overfit 在 all+apply 中是否在成功之后运行。在 _amain 中,场景按顺序迭代 ("success", "no_effect", "overfit")(来自 list(SCENARIOS))。应用成功后,源文件被永久修改。随后的场景基准将不再是预期的基准。此外,e2e 测试 fixture 在运行所有场景后恢复 prompt,但 --apply 并未在测试中使用。

让我通过检查 no_effect 场景的需求来确认:no_effect 优化器提出 success=no_effect 候选(基准的文案编辑)。基准仍然是原始的 system.md。如果成功场景已经应用,源文件将变为 success 候选,no_effect 场景的基准就是错误的。所以 --apply--scenario all 结合使用是一个真正的 bug。但 --apply 的默认值是关闭的,文档中也警告过。尽管如此,它还是在非交互式 CI 中静默修改了源文件。值得提出一个 Warning。

让我再检查一下:在 --apply 之后,成功场景的基准(之前已计算)不受影响,但下游场景会受影响。让我确认提示词是否会被读取。run_optimization 调用 AgentOptimizer.optimize,它通过 target 读取源文件 → 读取的是修改后的文件。还有基准评估 run_eval,它通过 get_agent_asyncbuild_instruction → 读取文件。是的,确实存在该 bug。

现在让我考虑一下 evaluate_gates 中针对预算为 None 时 int(budget_used) 的真实性 —— 已经处理过了。

让我看看 report.py 中关于 optimizer_val_pass_rate 验证的一个问题 —— validate_report 检查了键,但 build_report 总是会生成它。没问题。

让我检查 report.py 中一个潜在的真正 bug:_delta_view 引用了 delta.counts,但 compute_delta 将 counts 初始化为所有 CHANGE_KINDS。很好。

现在,report.py_case_view 的一个真正问题:它调用了 attribute_case(record) 来获取 failure_types/failure_reasons。对于通过的案例,findings=[],所以 failure_reasons=[]。validate_report 检查 if not case.get("passed") and not case.get("failure_reasons")。对于未通过的案例,应该有原因。但是基准线(baseline)上 train_convert_3kmllm_rubric_response 失败,且 rubric_verdicts 为空(实际样本输出显示“未提供 rubric 明细”)。回退(fallback)仍然产生一个发现(finding)。很好。

等等 —— 让我看看当 _failed(record, "llm_rubric_response") 为真但 rubric_verdicts 为空时 attribute_case 的行为。规则 4:failing = _failing_rubrics(...) → [],ids = "" or "(未提供 rubric 明细)" → “(未提供 rubric 明细)”。追加一个发现。很好。但为什么基准线上的 rubric_verdicts 为空?因为对于失败案例,fake judge 返回的裁决结果……让我检查一下。

实际上在样本输出中,失败案例显示 "failure_reasons": ["[llm_rubric_response] LLM rubric 评审不达标(未通过 rubric 明细)。)"]。等等 —— 末尾多了一个右括号 )。。让我检查一下字符串格式化:f"未通过 rubric:{ids}。{reasons}",其中 reasons = ";".join(_truncate(r.get("reason","")) for r in failing)。当 failing=[] 时,reasons="". 所以 evidence=未通过 rubric:(未提供 rubric 明细)。。然后在 _case_view 中:f"[{f.metric}] {f.explanation}({_clip(f.evidence)})". 所以 = [llm_rubric_response] LLM rubric 评审不达标(未通过 rubric:(未提供 rubric 明细)。). 多了一个 。这只是一个外观上的问题。

但为什么 rubric_verdicts 为空?让我检查 _record_from_case_result:它只在 details.rubric_scores 为真时填充 rubric_verdicts。对于失败的裁决,评分是有的。嗯。但样本显示为空。也许 fake judge 的输出没有被解析为 rubric_scores,因为……实际上这并不重要 —— 回退逻辑处理了它。

让我专注于可验证的问题。让我检查 _failing_rubricsr.get("score") or 0.0) < 1.0。如果 score 为 0.0,0.0 or 0.0 = 0.0,<1.0 为真。很好。如果 score 为 1.0,1.0 or 0.0 = 1.0,不为 <1.0。很好。但如果 score 为 None:None or 0.0 = 0.0 <1.0 为真。很好。

现在让我检查一个关于 knowledge_search 工具和实际指标的问题 —— 没有问题。

让我检查一个真正的担忧:evaluate.py 中的 _record_from_case_result 访问了 case_result.eval_metric_result_per_invocation[0]。如果 case 有 0 个调用(空),这会报错。但注释说“本 example 的 case 均为单 invocation”。对于多轮对话案例,取第一个。问题不大。

让我检查 summarize 中关于除以零的问题:if total else 0.0。很好。

现在,关于 run_eval 中的一个实际正确性问题:它捕获 AssertionError 并继续执行。但 _EvaluationCasesFailedAssertionError 的子类。如果框架在填充结果之前抛出其他 AssertionError,get_result() 可能会返回 None → RuntimeError。标记为 # pragma: no cover。可接受。

让我最后核实一下关于 datetime.now 的测试 —— test_deterministic_reruns 比较 stable_view,它排除了 generated_at。很好。

现在让我检查一下当 budget 为 None 时,gates.pyint(budget_used) 是否存在实际问题。第 137 行:calls_ok = (cfg.max_metric_calls is None or budget_used is None or int(budget_used) <= cfg.max_metric_calls)。如果 max_metric_calls 不为 None 且 budget_used 为 None → calls_ok=True(视为通过)。第 139-141 行:if cfg.max_metric_calls is not None: calls_part = f";metric 调用 {budget_used if budget_used is not None else '未知'}..."。没问题。

让我检查 cost_budget 上的 max_metric_calls 配置是否确实得到了测试。是的。

现在,min_val_improvement 的默认值 1e-9 与浮点数 pass_rate_delta 进行 >= 比较存在一个真正的问题。对于无效果场景,pass_rate_delta=0.0。0.0 >= 1e-9 为假 → 拒绝。很好。但 score_delta 默认值为 0.0,无效果场景下仍为 0.0;0.0 >= 0.0 为真。所以 pass_ok 为假,导致拒绝。很好。

让我检查一个关于过拟合防御使用 delta_train.pass_rate_delta > 0 且没有 epsilon 的边界情况。浮点数:2/3 = 0.6666... > 0 为真。val -1/3 < 0 为真。很好。但如果 train 的 delta 是像 1e-17 这样的浮点数误差呢?这里不太可能。问题不大。

现在让我检查关于 FakeAgentModel.calls 并发以及测试可靠性的一点。注释说 gepa 使用工作线程;使用 _bump_calls 加锁。很好。

现在让我检查 _first_user_text_last_user_textp.text 的访问 —— Part 对象。如果 part 没有 text 属性,p.text 可能会报错。但 SDK 的 Part 有 text。没问题。

现在让我检查当 system_instruction 为 None 时的 parse_directivesstr(request.config.system_instruction or "")。很好。

让我检查一个关于 _KM_RE 的真正问题:r"([0-9]+(?:\.[0-9]+)?)\s*公里"。对于“3 公里”,匹配“3 公里”。但对于“把 3 公里换算成米”,"换算成米" in query 为真。很好。但要注意 —— 它也匹配“公里”后面的部分。没问题。

现在,test_convert_baseline_uses_raw_unit 期望 args == {"value": 3, "unit": "公里"}。代码:args_value = int(value) if value.is_integer() else value,其中 value=float("3")=3.0is_integer()=True → int 3。unit="公里"。很好。

现在我认为我有足够的发现。让我汇总一下。让我检查 run_pipeline.py--apply 的问题是否确实为真,且是否存在防护措施。没有防护措施。让我核实一下 SCENARIOS 的顺序:SCENARIOS = ("success", "no_effect", "overfit")。所以应用成功会破坏 no_effect/overfit 的基准。已确认。

让我再核实一个潜在的实际问题:在 evaluate_candidate 中,target.write_all(candidate_prompts) 写入源文件,运行 eval(通过 build_instruction 读取文件),然后恢复。但是 run_eval 使用 agent_module="loop_agent"get_agent_asynccreate_agentbuild_instruction 读取文件。由于 write_all 是原子的且发生在 eval 之前,候选者是可见的。很好。但是关于并发性呢 —— 顺序执行。很好。

让我检查 run_optimizationAgentOptimizer.optimize 是否在运行期间也写入源文件(用于候选评估)并恢复(update_source=False)。这发生在 evaluate_candidate 之前。在优化之后,文件被恢复为基准。然后 evaluate_candidate 进行快照。很好。但是 run_optimization 内部存在一个时间窗口,此时源文件是候选内容 —— 如果发生异常,SDK 会恢复。信任 SDK。没问题。

让我核实一下在 e2e 测试中是否对 --apply 进行了测试 —— 没有,apply_on_accept 默认为 False。所以 --apply 的 bug 在 CI 中未经过测试。这是一个测试覆盖率缺口 + 一个 bug。

现在让我更仔细地检查 validate_report 的逻辑,寻找一个真正的 bug:第 253 行 for case in report[section][split].get("per_case", []):。如果 report[section][split] 是 None(缺失),之前的检查设置了问题并 continue。但第 252 行的循环仍然尝试执行 report[section][split].get(...)。等等 —— 第 226-228 行:如果视图不是字典,追加问题并 continue(继续下一个 split)。所以如果分割缺失,我们跳过。但随后第 251 行的 for section in ("baseline","candidate") 内部循环,for split in ("train","val"),第 253 行访问 report[section][split]。如果部分存在但分割缺失……第 230 行的内部循环检查 for key in (...,"per_case")。嗯,结构有点复杂,但应该能正常工作。

让我看一个真正的问题:在 validate_report 中,当 report["optimization"] 缺少键时,它会追加问题,但第 247 行的循环是没问题的。

好的,让我再检查一个关于 report.py 第 188-189 行引用 optimize_result.rounds[-1] 的真正问题。如果 rounds 为空,rounds[-1] 会报错。但构建过程传递的是真实的 OptimizeResult。对于 SUCCEEDED 状态,rounds 是非空的。但如果状态为 FAILED 且 rounds 为空呢?optimize_result.rounds[-1] → IndexError。让我检查构建过程是否对此进行了防护。第 188 行:"budget_used": (optimize_result.rounds[-1].budget_used if optimize_result.rounds else None)。有防护。很好。第 179 行:sum(1 for r in optimize_result.rounds if r.accepted)。有防护。

让我检查 run_pipeline.py 第 198 行:last_round = optimize_result.rounds[-1] if optimize_result.rounds else None。有防护。很好。

现在让我考虑一下 test_overfit_reason_mentions_overfitting 的测试:它使用了 delta_val=_delta(("v1", True, False, ...) 以及 protected_cases=["v1"]。所以 protected_cases 闸门失败,no_new_hard_fail 失败(v1 new_fail),overfit_guard 失败(train 提升,val 下降)。原因严重性顺序:overfit_guard 在最前面。理由中提到了“过拟合”以及“同时未通过”。很好。测试通过。

让我核实一个关于 _REASON_SEVERITY 的真正问题:protected_casesno_new_hard_fail 之前。所以在过拟合测试中,overfit_guard 是最严重的。很好。

现在,gates.py 中的一个真正问题:第 159 行 key_gate = next(failed[name] for name in _REASON_SEVERITY if name in failed)。这使用了 next(generator)。如果 failed 非空但没有名称匹配 _REASON_SEVERITYnext() 会抛出 StopIteration。可能吗?闸门名称正好是 _REASON_SEVERITY 中的 6 个,且所有闸门都会被评估。所以每个失败的闸门都在 _REASON_SEVERITY 中。因此 failed 总是有匹配项。除非……不是。所以是安全的。但这是一个潜在的脆弱性 —— 如果添加了闸门但未添加到 _REASON_SEVERITY,会崩溃。问题不大,考虑到代码一致性,不值得标记。

让我检查一个真正的问题:_protected_violations 只检查 delta_val,不检查 delta_train。保护案例应该在 val 中。文档说是“保护 case”。val_identity 在 val 中。没问题。但如果保护案例只在 train 中呢?会被静默忽略。对于此示例来说问题不大。

让我检查 report.py 的 Markdown 渲染以寻找真正的 bug:第 340 行 f"{case['candidate_score'] - case['baseline_score']:+.3f}"。有效。

现在让我最终确定。最具影响力的可验证问题:

  1. --apply + --scenario all 顺序污染:在 success 之后将 success 候选应用于源 prompt,然后 no_effect/overfit 基准从污染的 prompt 读取,破坏了它们的基准比较和场景语义。run_pipeline.py:272-274。此外,--apply 修改了提交的源文件且没有 git 感知/备份。这是一个真正的正确性 bug,尽管它是可选的。因为它是用户可选的且记录在案,且默认关闭 —— 我会将其标记为 Warning(而不是 Critical,因为它是可选的且文档警告过)。但实际上下游场景的静默污染是一个正确性 bug。让我定为 Warning。

  2. --apply 缺乏测试覆盖率 —— 没有测试应用路径。将其与 chore: initialize project #1 合并。

  3. 让我检查当 candidate 没有评估 ID(不同的集合)时 compute_delta 是否会出错 —— 已通过 # pragma: no cover 处理。

让我再看一个:在 fake_models.py 中,_memorized_response 检查 if entry["tool_calls"] and not after_tool —— 生成工具调用。但对于 after_tool=Trueentry["tool_calls"] 非空的情况,它会落入 _text_response(entry["final"])。很好。但对于 after_tool=True 且工具调用为空的情况(身份)→ 返回最终文本。很好。但这里有个问题:对于备忘录未命中(查询不在表中),它返回 MEMORIZE_MISS_ANSWER 而不管 after_tool 如何。很好(没有工具调用)。

但有一个微妙的问题:当备忘录命中且 after_tool=False 时,它发出工具调用 FunctionCall。然后运行器执行工具并发送回 function_response,然后再次调用模型,此时 after_tool=True → 返回 entry["final"]。但是 function_response 包含的是真实工具结果,而不是备忘录的预期结果。最终答案使用 entry["final"](忽略工具响应)。所以轨迹显示工具被调用了(很好),最终答案与备忘录匹配。但 tool_trajectory 指标检查实际的工具调用是否与期望的匹配。对于 train_convert_3km,期望的调用是 convert_distance(value=3, unit=km),备忘录发出的也是 convert_distance(value=3, unit=km)。匹配。很好。

现在,关于 tool_trajectory 指标的一个真正问题:它是否也检查工具响应?eval_config 中的指标仅指定了 tool_trajectory,包含 namearguments 匹配,没有 tool_responses。所以只有调用,没有响应。很好。

好的。让我再核实一下 --apply 的担忧是否确实存在,即优化器读取了修改后的文件。实际上,等等 —— 在带有 --apply_amain 中:成功运行,接受,应用(写入成功候选)。然后 no_effect 运行:基准 eval 读取源文件 → 现在是成功候选(output_format=json, unit_normalization=on, knowledge=on)。所以 no_effect 基准会通过所有案例!然后 no_effect 优化器提出 no_effect 候选(从……当前源读取,现在是成功 prompt)。FakeReflectionModel 从 candidates/system_prompt.no_effect.md 提议,与当前源无关。所以候选是 no_effect(基准的文案编辑)。但当前源是成功候选,而不是基准。所以优化器的基准是成功,而提议的是 no_effect 回归 —— 这实际上可能会被拒绝。无论如何,场景语义被破坏了。确认的 bug。

让我再检查一下:问题是否仅限于 --apply?是的。默认关闭。所以 Warning。

让我检查是否还有其他真正的 Critical 问题。让我重新检查 evaluate_gatesmin_val_pass_rate_improvement 默认值 1e-9 以及浮点数比较。对于成功场景,val pass_rate_delta = 2/3 = 0.6667 >= 1e-9 为真。很好。

让我检查一个关于 score_epsilon 比较的真正问题。默认值为 1e-6。case_score 是 4 个指标的平均值,有些为 None→0。对于 new_pass/new_fail 分类,是状态优先的。很好。

让我检查当 metric_scores.values() 为空时,summarize 中是否存在真正的问题:scores=[], case_score=0.0。metric_breakdown 遍历 record.metric_scores。如果所有记录都没有指标……问题不大。

我现在对我的发现很有信心。我也检查一下 README 中的声明与实际行为是否一致,但这并不是代码问题。

我也核实一下 report.py 第 382 行的 json.dumps(report) 是否能序列化所有内容 —— optimize_result.total_token_usage 是一个字典,best_prompts 是一个字典。所有内容都是 JSON 可序列化的。很好。

最后一个真正的担忧:在 run_pipeline.py 中,_build_target() 为每个场景创建一个新的 TargetPrompt。但 register_fake_models() 在导入时运行。TargetPrompt().add_path(...)。很好。

让我检查 regression.py 中的 evaluate_candidaterun_optimization —— 两者都使用传入的同一个 target 对象。在 run_scenario 中,target = _build_target()(第 168 行)同时传递给 run_optimization(第 169 行)和……等等,evaluate_candidate 接收 target(第 184 行)。是同一个对象。在 run_optimization 之后,源文件被恢复为基准(update_source=False)。然后 evaluate_candidate 进行快照(读取基准)、写入候选、恢复。很好。

但有一个微妙的问题:AgentOptimizer.optimize 内部是否在最后恢复,即使在异常时也是如此?信任 SDK。是的。

现在让我写下审查报告。让我核实 --apply 的行号:272-274。还有 _amain 场景循环 284。

我也检查一下:是否有一个问题,即 --apply 写入成功候选,但没有验证候选是否真的与 best_prompts 匹配?它使用 optimize_result.best_prompts。很好。

让我再检查一个潜在的实际问题:fake_models.py 中的 MEMORIZE_TABLE 包含查询“请介绍一下深圳”(probe)和“介绍一下深圳”(train)。val 包含“介绍一下杭州” —— 不在表中。很好。但 train 包含“介绍一下深圳” —— 在表中。很好。而 probe 包含“请介绍一下深圳” —— 在表中。很好。

但等等 —— val 包含“介绍一下杭州”。在过拟合场景中,备忘录未命中 → MEMORIZE_MISS_ANSWER。样本输出确认 val_intro_hangzhou 候选响应 = 错误答案。很好。

现在,关于 convert_distance 工具的一个真正问题:当备忘录发出 convert_distance(value=5, unit=km),但查询是“把 5 公里换算成米”时,val_convert_5km —— 但等等,备忘录未命中,因为“把 5 公里换算成米,用 JSON 输出”不在表中(只有“把 3 公里”和“把 4 公里”)。所以 val_convert_5km 候选 = 错误答案(无工具调用)。样本确认:actual_tool_calls=[]。很好。所以 val_convert_5km 保持失败(未变)。很好。

现在进行汇总。让我最终确定发现的问题。

实际上,让我重新考虑 --apply 的严重性。用户必须同时传递 --apply--scenario all 才能触发。--apply 在帮助文档中记录为“会修改 loop_agent/prompts/”。所以用户已被警告。但跨场景污染是一个非直观的后果。我将其标记为 Warning,并建议要么为 all 禁用 --apply,要么在每次场景运行前重新读取/重置基准快照。

还有一个:--apply 在没有备份的情况下修改了提交的源文件 —— 如果用户运行它,他们的基准 prompt 会被覆盖且没有 git 暂存。但 git 可以恢复。问题不大,可以合并。

让我也检查一下 run_pipeline.py_amain 是否在场景之间没有重置 target 或重新读取基准 —— 确认,每次迭代都是一个新的 run_scenario,带有新的 target = _build_target()。但文件状态是持久的。确认。

现在让我也核实一个关于测试的真正问题:test_prompts_untouched_after_runs 断言源仍然是基准。由于 --apply 未在测试中使用,write_all 快照恢复有效。很好。但这并没有测试 --apply。测试覆盖率缺口。

让我写下结论。

让我仔细检查一下 gates.py 中当 budget_used 是 int 时使用 int(budget_used) 是否有问题 —— int(61)=61。没问题。但如果是字符串呢?类型是 Optional[int]。如果 SDK 返回一个字符串……不太可能。跳过。

让我检查一个关于 delta_val.pass_rate_delta < 0 和浮点数的真正问题:对于过拟合,val delta = -1/3。没问题。

让我检查一个关于保护案例 score_down 的真正问题:classify 返回 score_down 是在 candidate.case_score < baseline.case_score - eps 时。对于 val_identity 过拟合:基准通过(得分 1.0),候选未通过(得分 0.75)。所以 baseline.passed=True, candidate.passed=Falsenew_fail(状态优先,尽管得分下降)。所以保护违规将 new_fail 标记为 change in ("new_fail","score_down") → 命中 new_fail。很好。但注意:val_identity 候选 case_score=0.75(3 个指标通过 + final_response=0 → (1+0+1+1)/4=0.75)。分类为 new_fail。保护闸门失败。样本确认 protected_cases 闸门失败。很好。

现在最终汇总。我会将 --apply 问题作为 Warning,或许还有一个关于 e2e fixture 顺序/调用计数器的小问题(不值得)。让我也重新考虑是否有任何 Critical。

有一个潜在的 Critical:evaluate_gatesnext(...) StopIteration —— 但已确认不可达。跳过。

让我重新考虑 run_optimizationupdate_source=False 以及优化器是否真的恢复了。如果优化器没有恢复(SDK bug),evaluate_candidate 的快照将捕获污染的状态,并且基准(已经计算过)将不匹配候选的基准。但信任 SDK 合约。不是示例代码的 bug。

我将最终定稿。让我也检查样本输出报告的时间戳 2026-07-06 —— 今天的日期是 2026-07-19。问题不大。

让我写下最终审查报告。我已经读完了 pr.diff 中的全部新增代码及关键上下游上下文(SDK 的 TargetPrompt/OptimizeResult/LLMModel 定义、evalset/eval_config/optimizer 配置、候选 prompt、sample_output 与测试)。下面给出结论。

发现的问题

⚠️ Warning

  • examples/optimization/eval_optimize_loop/run_pipeline.py:272-274--apply--scenario all 组合会顺序污染下游场景的 baseline

    • --apply 在 success 场景被接受后直接 target.write_all(optimize_result.best_prompts) 把 success 候选写回源 prompt 文件,但 _amain 随后继续顺序运行 no_effectoverfit。这两个场景的 baseline 评测(run_evalget_agent_asyncbuild_instruction 每次从磁盘重读 prompt)和优化器(AgentOptimizer.optimize 读同一份源文件)都会读到已被改写成 success 的 prompt,而非各自预期的 baseline,导致 baseline 口径、delta、gate 决策全部失真——场景语义被静默破坏。
    • 修复方向:--scenario all 时禁用 --apply,或仅在最后一个场景、或改为先对每个场景的 baseline 做快照并在每次 run_scenario 前用 baseline 快照重置源文件后再决定是否 apply。
    • 该路径当前无任何测试覆盖(test_pipeline_e2e.py 全程 apply_on_accept=False),CI 不会暴露。
  • examples/optimization/eval_optimize_loop/run_pipeline.py:272-274--apply 落盘写回源 prompt 缺少回滚/备份保护

    • --apply 直接覆盖 loop_agent/prompts/system.mdskill.md 这两个已提交文件,失败也无备份;与 evaluate_candidate 里"换入/换出永不污染源文件"的设计形成反差。即便有 git 可恢复,仍建议写入前显式备份到输出目录或要求 --force-apply 二次确认,避免误操作把 baseline prompt 永久覆盖。可与上一条合并修复。

💡 Suggestion

  • examples/optimization/eval_optimize_loop/loop_pipeline/gates.py:159key_gate = next(failed[name] for name in _REASON_SEVERITY if name in failed) 依赖"所有失败闸门名都在 _REASON_SEVERITY 中"这一不变量;若将来新增闸门忘记同步 _REASON_SEVERITYnext 会抛 StopIteration 使决策崩溃。可加 default=None 兜底取 failed 的首项,维护更稳。

总结

整体实现质量高:换入/换出原子回滚、闸门表驱动测试、归因兜底、配置 extra="forbid" fail-fast 等设计都到位,三场景 e2e + 确定性测试覆盖充分。未发现 Critical;主要风险集中在 --apply 路径——与 --scenario all 组合会顺序污染下游场景 baseline,且该路径无测试覆盖,建议修复后再合入。

测试建议

  • 补一个 --scenario all --apply 的用例(或在 CI 中显式断言):success 之后 no_effect/overfit 的 baseline 仍为原始 baseline prompt,验证 --apply 不会跨场景污染。
  • --apply 被拒绝场景(如 no_effect)不写回源文件的断言。

新增 examples/optimization/eval_optimize_loop/:把 AgentEvaluator 与
AgentOptimizer 拼成「baseline 评测 → 失败归因 → GEPA 优化 → 独立验证集
回归 → 可配置 gate 决策 → 审计落盘」六阶段闭环,回答候选 prompt 是否
值得接受。零 SDK 改动、零 API Key(fake agent/judge/reflection 三个确定
性 provider 走 ModelRegistry 正则路由)、trace 模式支持,三场景端到端
远低于 3 分钟。

- 6 条评测 case(3 train + 3 val),内置 success / no_effect / overfit
  三类结局场景;overfit 用「泄漏调参集 + 查表记忆候选」机理构造,
  过拟合守卫(train↑∧val↓)+ 保护 case + 新增 hard fail 三门齐拒
- 六类失败归因只依赖框架 metric 结果结构,每个失败 case 保证至少
  一条带证据的中文理由;合成矩阵 12/12、gate 决策矩阵 12/12
- optimization_report.json/.md 双格式 + 契约校验器(--check),报告含
  baseline/candidate 分数、逐 case delta、gate 决策与接受/拒绝理由
- 审计:SDK 原生 rounds/、config.snapshot.json(含 seed)、run.log +
  pipeline 侧逐 case 记录、归因明细、配置快照;候选换入 try/finally
  快照还原,写回源文件需 gate 通过且显式 --apply
- 59 个 pytest(fake 模型、归因、闸门、e2e/trace/确定性/时限)+
  中英双 README + 471 字 DESIGN.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@EricChan2028
EricChan2028 force-pushed the feat/91-eval-optimize-loop branch from b2a6ec9 to 9471072 Compare July 19, 2026 01:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants