Skip to content

evaluation: add auditable optimization regression loop - #262

Open
maludiem wants to merge 6 commits into
trpc-group:mainfrom
maludiem:feature/evaluation-optimization-loop
Open

evaluation: add auditable optimization regression loop#262
maludiem wants to merge 6 commits into
trpc-group:mainfrom
maludiem:feature/evaluation-optimization-loop

Conversation

@maludiem

Copy link
Copy Markdown

概述

实现 Evaluation + Optimization 自动回归闭环:执行 baseline 评测、失败归因、prompt 优化、候选验证和 gate 判定,并生成可审计的 JSON/Markdown 报告。

Fixes #91

主要改动

SDK 实现

  • _evaluation_optimization_pipeline.py
    • 使用 AgentEvaluator 评测训练集和验证集
    • 默认调用 AgentOptimizer,支持注入 fake optimizer
    • 生成逐 case delta,区分新增通过、失败、提升和退化
    • 检测训练集提升但验证集退化的过拟合候选
    • 仅在 gate 接受且显式配置后回写源 prompt
  • _evaluation_optimization_config.py
    • 配置验证集提升阈值、hard fail、关键 case、退化上限和成本预算
  • _evaluation_optimization_result.py
    • 定义 baseline、candidate、delta、gate、失败归因和审计模型
    • 输出 optimization_report.json 和中英文 optimization_report.md

示例

新增 examples/optimization/eval_optimize_loop/,包含:

  • 3 条训练 case 和 3 条验证 case
  • baseline system prompt
  • optimizer 与 gate 配置
  • fake model、fake judge 和 trace mode
  • pipeline 入口脚本
  • JSON/Markdown 示例报告
  • README 和设计说明

样例覆盖可优化成功、优化无效及优化后验证集退化三类情况,无需真实 API Key。

Gate 与审计

候选需要满足:

  • 验证集分数和通过率提升达到阈值
  • 不新增 hard fail
  • 关键 case 不发生超限退化
  • 验证集退化数量不超过上限
  • 总成本不超过预算
  • 不出现训练集提升但验证集退化的过拟合

报告记录每轮候选 prompt、逐 case 结果、失败归因、gate 检查、接受或拒绝理由、成本、耗时、随机种子和输入摘要。

Implement baseline evaluation, failure attribution, candidate regression, configurable acceptance gates, and audit reports.

Fixes trpc-group#91

RELEASE NOTES: Added an auditable evaluation and prompt optimization loop.
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

字段匹配。没问题。

我已经完成了深入审查。让我来撰写最终报告。

发现的问题

🚨 Critical

未发现 Critical 问题。

⚠️ Warning

  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:1054-1062_redact 仅按固定键名集合脱敏,无法覆盖 OptimizeModelOptions.extra_fields / generation_config 等自由结构中携带的凭证。

    • config_snapshot 会随审计报告落盘到 config.snapshot.json,而 _SECRET_KEYS 只匹配 api_key/apiKey/authorization/password/secret/token 这几个固定键;用户在 reflection_lm.extra_fields(如自定义 Authorizationx-api-keysk-... 等)里放入的密钥不会被脱敏,存在凭证泄露到审计产物的风险。建议对 extra_fields/generation_config 递归脱敏,或对 value 做模式匹配(如 sk-、Bearer 前缀)兜底。
  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py 全文件(及 _evaluation_optimization_config.py_evaluation_optimization_result.py):新增的回归闭环(gate 决策、失败分类 _classify_failure、逐 case delta _compare_snapshots、候选筛选 _candidate_specs/_select_candidate)在 tests/ 下无任何测试覆盖。

    • 这些是高风险判定逻辑(接受/拒绝、过拟合、关键 case 回归、hard fail 分类),且 tests/evaluation/ 目录已存在大量相邻优化器测试,却无对应 pipeline 测试。建议至少补充:baseline→candidate 全通过/全拒绝、overfit 拒绝、critical case 回归、max_candidates 截断保留 best、空 rounds 回退 baseline 等场景的回归测试。
  • examples/optimization/eval_optimize_loop/run_pipeline.py:57main() 默认输出目录 runs/latest.gitignore 仅忽略 runs/ 配合可工作,但脚本若被复制到其他位置或被 CI 直接调用,_HERE / "runs" / "latest".resolve() 在父目录不可写时会抛异常而无友好提示。

    • 影响较轻,但作为示例入口建议对 mkdir 失败给出明确错误。可视为可选。

💡 Suggestion

总结

整体实现严谨,核心 gate/分类/delta 逻辑与示例产物自洽,示例可正确产出 ACCEPT/REJECT 决策。存在两个建议修复项:审计配置脱敏对自由结构字段不完整(凭证泄露风险),以及新增的高风险判定逻辑完全缺少单元测试;无必须修复的 Critical 问题。

测试建议

  • 补充 pipeline 回归测试:覆盖 candidate 全通过接受、overfit 拒绝、critical case 退化拒绝、max_candidates 截断时 best 仍被保留、optimize_result.rounds 为空时回退 baseline 五条路径。
  • 补充 _redact 测试:构造含 extra_fields={"Authorization": "Bearer ..."} 的配置,断言 config.snapshot.json 中该值被脱敏。

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.94163% with 39 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@86b5f7d). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...dk/evaluation/_evaluation_optimization_pipeline.py 92.16102% 37 Missing ⚠️
..._sdk/evaluation/_evaluation_optimization_config.py 98.14815% 1 Missing ⚠️
..._sdk/evaluation/_evaluation_optimization_result.py 99.58333% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main        #262   +/-   ##
==========================================
  Coverage        ?   88.30070%           
==========================================
  Files           ?         485           
  Lines           ?       45960           
  Branches        ?           0           
==========================================
  Hits            ?       40583           
  Misses          ?        5377           
  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.

Redact nested provider credentials from audit artifacts and cover candidate limiting, empty optimizer results, and report generation.

Updates trpc-group#91

RELEASE NOTES: NONE
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

已导出。很好。

现在让我重新考虑一个我想要标记的真实问题:_file_artifact 使用 resolved.read_bytes() 读取文件以获取 sha256。对于 prompt 输入,它使用的是内容(而非路径)。但是 _input_audit 使用 sha256 读取实际的配置/训练/验证文件——没问题。

但有一点值得注意:_prompt_input_audit 对内容进行哈希处理,但通过 _portable_path 报告路径。对于回调源,它将路径设为 "<callback>"。没问题。

让我重新考虑安全性:临时配置文件 optimizer_config_path 包含了可能存在的真实 API key,并且在运行期间于临时目录(系统临时目录)中以默认权限存在。tempfile.NamedTemporaryFile 创建时使用了 delete=False,并在 finally 中被删除。该文件具有标准的 0600 权限吗?实际上,Python 的 NamedTemporaryFile 在 POSIX 上使用 O_EXCL 创建文件,默认模式为 0600。所以它是安全的。不是问题。

让我重新考虑脱敏(redaction)的完整性:_is_sensitive_key 会去除非字母数字字符并转为小写。因此 "api_key""apikey" ∈ set。"x-api-key""xapikey" ∈ set。很好。"Authorization""authorization"。很好。但是带有键名 credentialsprivate_keyapi_key_id 的配置呢——这些不在集合中。generation_config.credential 在测试中映射为键 "credential""credential" 不在集合中,但测试期望它能被脱敏。让我检查一下:测试中 generation_config 包含 "credential": "sk-provider-secret-123"。键 "credential" 去除非字母数字后为 "credential" —— 不在敏感键集合中。因此,它仅通过值模式 "sk-..." 被脱敏。测试断言 "sk-provider-secret-123" not in snapshot。值模式 r"^\s*sk-[A-Za-z0-9_-]{8,}\s*$" 可以匹配。所以它被脱敏了。很好,测试通过。

但如果值不是 Bearer/sk 模式呢?那么 "credential" 键就不会被脱敏。这是一个覆盖范围的缺口,但可能不是关键问题,因为存在覆盖最常见情况的值模式。我考虑将其标记为关于脱敏覆盖范围的改进建议,但测试和已知情况已经覆盖。可能值得提出一个建议。

让我完成最终评估。我认为没有关键问题。让我形成最终结论。

让我再次核实一件事——_apply_gate 成本检查中 total_cost_usd <= gate.max_total_cost_usd + _EPSILON 的比较。没问题。

让我重新考虑关于 _select_candidate 的问题,当存在已接受的轮次,但选中的轮次已接受,而 report.gate_decision = selected.gate_decision(已接受)。然后 update_source 会基于 report.gate_decision.accepted 进行写回。正确。

还有一个:evaluation_case_runs 的成本计算在所有评估的候选中重复计算了基准情况。基准情况各计算一次,然后针对每个候选,将 train+validation.case_run_count 添加进去。这是按设计进行的(基准 + N 个候选评估)。审计结果显示 evaluationCaseRuns: 18 = 3 个基准训练 + 3 个基准验证 + 2 个候选 × (3+3) = 18。正确。

我很有信心。没有关键问题。让我写出审查意见。

发现的问题

🚨 Critical

无。

⚠️ Warning

  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:4581-4587:4622-4623finally 中直接 await target_prompt.write_all(baseline_prompts) 恢复源 prompt,在任务被取消(CancelledError)或 write_all 自身抛错时,该 await 会被中断或再次抛出,导致源 prompt 可能停留在某个候选 prompt 而未恢复成 baseline。建议对恢复写入做 asyncio.shield 或捕获并记录恢复失败,至少保证 unlink 与后续落盘不被跳过。

  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:4822-4832:当 best_prompts 与已有某轮 candidate 不同时,会以 max(round)+1 作为新的 round 号追加,且 _select_candidate-record.round 作为 tie-breaker 偏好更小 round。虽然 best 通常也会被 gate 接受,但若 best 候选未被 gate 接受、而其它轮被接受,-round 仍可能让这个「并不存在的优化器轮次」优先被选中,导致 report.candidate.round 报告一个优化器从未产出的轮次号,审计语义有误导风险。建议 best 规格复用其原始轮号或单独标注来源,不参与 round tie-break。

💡 Suggestion

  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:5452-5468_redact 仅按固定 key 名集合与 Bearer/sk- 值模式脱敏,未覆盖 credentialprivate_keyapi_key_id 等常见敏感键名;若配置中以非 Bearer/sk- 形式存放密钥,仍会落入 config.snapshot.json / 报告。可扩充敏感键名白名单以增强审计脱敏稳健性。

总结

整体实现严谨,安全脱敏、原子写盘、baseline 恢复和 gate 逻辑均有对应测试覆盖,未发现阻塞级别问题。两处 Warning 分别涉及取消场景下源 prompt 恢复的中断风险和 best 候选 round 号语义问题,建议修复。

测试建议

  • 补充一个用例:在 optimizer_runner 或 candidate 评估阶段注入 CancelledError,断言 finally 能将源 prompt 恢复为 baseline(或明确记录恢复失败)。
  • 补充一个用例:best_prompts 与所有轮次均不同且 best 未通过 gate、其它轮通过 gate 时,验证 report.candidate.round 与选中语义符合预期。

Match the repository YAPF output for the evaluation optimization configuration and pipeline.

Updates trpc-group#91

RELEASE NOTES: NONE
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

I'll skip the shell and reason analytically. Let me verify a key concern: in the redaction test, generation_config.credential = "sk-provider-secret-123" — this value matches the sk- pattern so it's redacted by value pattern. And nested[0]["x-api-key"] key normalizes to xapikey which IS in the set. And Authorizationauthorization IS in set. So the test passes. Good.

But note credential (key) normalizes to credential which is NOT in the sensitive set — only saved by the value pattern matching sk-.... If someone put a non-sk-/non-Bearer secret there, it would leak. That's a real but minor gap.

Now let me finalize the review. Let me double-check one correctness concern in _candidate_specs: when best_prompts equals an existing round's prompts, it's deduped (fingerprint in seen). Good. When truncating to max_candidates, if best is already in selected, it's kept. The logic at line 4836-4850 looks correct.

Let me verify the concern about max_validation_regressions gate: validation_regressions = newly_failed + regressed. The regressed group in _compare_snapshots includes cases with negative score delta even if still passing. Combined with overfitting. This is reasonable.

One real concern: _select_candidate uses pool = accepted or rounds — when no candidates accepted, it selects from ALL rounds by score, and the top-level gate_decision.accepted comes from selected.gate_decision. But report.gate_decision is set to selected.gate_decision, so if no round accepted, top-level accepted=False. Correct.

Let me check the _apply_gate overfitting: train_delta.score_delta > _EPSILON and (validation score delta < threshold OR validation_regressions). Round 1 in example: train delta 0.667, validation score delta 0.0 < 0.3 → overfitting True. Correct.

Now, let me verify the new_hard_fail check more carefully for a subtle issue: it flags candidate_case.hard_fail and not baseline.hard_fail. But hard_fail is only set when not passed AND (case in hard_fail_case_ids OR category in hard_fail_categories). In the example round 1, val_system_prompt_safety fails with category final_response_mismatch (not in hard_fail_categories which are tool/argument/execution)... but the report shows hardFail=true for that case. That's because it's in hard_fail_case_ids. OK consistent.

I have enough to write the review.

发现的问题

🚨 Critical

  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:5291-5294new_hard_fail 判定在 baseline 缺失该 case 时会误报

    • 当候选侧出现某 case 为 hard fail、但 baseline 中该 case 不存在时,baseline_by_id.get(case_id)None,表达式 not (None and ...) 恒为 True,会把"新增 case"也算作 new hard fail 并直接拒绝候选。结合 _compare_snapshots 里 baseline/candidate case 集合可能不一致(added/removed),这会把正常的 case 集合变化误判为 hard fail 回归。建议显式区分"baseline 不存在该 case"与"baseline 存在但非 hard fail"两种情形,前者不应计入 new hard fail。
  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:5296-5307:critical case 在 baseline 缺失时无条件判为回归

    • if before is None or after is None: critical_regressions.append(case_id) 会让任何在 baseline 或候选侧缺失的 critical case 直接判定为回归并拒绝。当数据集 case 集合变动(trace 回放、跨版本 evalset)时,配置里仍引用旧 case id 会直接阻塞所有候选。建议对缺失 case 给出更明确的语义(如仅在候选侧缺失才视为回归),或在 baseline 缺失时不计入回归。

⚠️ Warning

  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:4544-4547,4711:脱敏仅依赖固定 key 名与两个 value 正则,存在泄露面

    • _SENSITIVE_KEY_NAMES 未覆盖 credential/credentials/access_key/private_key 等常见命名;_SENSITIVE_VALUE_PATTERNS 仅匹配 Bearer sk- 前缀。测试中 generation_config.credential 仅因值恰好是 sk-... 才被脱敏,若放入非前缀形态的密钥会直接写入审计快照。建议补齐常见敏感 key 名,或对配置中的已知密钥字段强制脱敏而非依赖值匹配。
  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:4564-4585:优化器返回非 OptimizeResult 时在 finally 之后才校验,资源已恢复但仍会抛 TypeError

    • runner(...)finally 会先恢复 baseline 并清理临时配置,随后才做 isinstance 校验。虽然行为正确,但 finallyoptimizer_config_path.unlink(missing_ok=True)_redact_optimizer_config_snapshot 若本身抛异常会掩盖优化器原始错误。建议在 finally 内对清理操作做 try/except 吞掉次要错误,避免掩盖主错误。
  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:5076-5081failure_category_overrides 会覆盖执行类错误分类

    • 当 case 配置了 override 且未通过时,categories = {override} 直接丢弃了由 _classify_failure 推断出的 execution_error/tool_call_error 等类别。这会让 hard_fail_categories 检查失效——一个本应因 execution_error 触发 hard fail 的 case 被 override 改写后不再被判为 hard fail,存在绕过 gate 的风险。建议 override 与推断分类取并集,或在 hard fail 判定后再应用 override。
  • examples/optimization/eval_optimize_loop/optimizer.json:386-393:示例配置含可能误导真实接入的字段

    • reflection_lm 使用 api_key: "not-used"base_url: "http://127.0.0.1/unused",README 又说明真实模式需配置模型;开发者复制该配置进入真实模式时容易沿用占位值导致优化器静默失败。建议在示例 JSON 中以注释或 README 显式标注这些为 fake 模式占位值、真实接入必须替换。

💡 Suggestion

  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:5117-5144_classify_failure 基于 metric 名与 reason 子串匹配,分支顺序耦合度高(如 "knowledge" in metric_name 优先于 tool 判定),后续新增 metric 类型时易误归类。可考虑将映射规则表化或允许配置显式 category 映射,降低顺序耦合带来的维护成本。

总结

整体实现结构清晰、测试覆盖了 gate 各分支与脱敏路径,但存在 2 个 Critical 的边界判定问题(new_hard_fail 与 critical case 在 case 集合不一致时会误拒绝候选)以及脱敏覆盖面不足、override 覆盖 hard fail 类别等 Warning,建议在合并前修复 Critical 项。

测试建议

  • 补充 case 集合不一致场景:候选侧出现 baseline 不存在的 case(含 hard fail)与 critical case 在一侧缺失时,验证 gate 不误报回归。
  • 补充脱敏测试:在 generation_config.credential 等非 sk-/Bearer 形态字段放入真实样态密钥,断言其被脱敏。

maludiem added 3 commits July 30, 2026 15:05
Apply the repository YAPF style to the evaluation optimization pipeline and report renderer, and keep the evaluation run count compatible with flake8.

Updates trpc-group#91

RELEASE NOTES: NONE
Handle mismatched evaluation case sets without false hard-fail regressions, preserve safety classifications, protect optimizer errors during cleanup, and expand audit credential redaction.

Updates trpc-group#91

RELEASE NOTES: NONE
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查指令说明“不要修改任何文件”,因此跳过 review.md 的写入。我直接在回复中提供审查结论。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/evaluation/_evaluation_optimization_pipeline.py:176-179evaluation_config.snapshot.json 未做敏感信息脱敏
    • 该处用 config.evaluate.model_dump_json(...) 直接落盘评估配置,未像同文件中 config.snapshot.json_evaluation_optimization_pipeline.py:366_redact)和 optimizer/config.snapshot.json_evaluation_optimization_pipeline.py:584_redact)那样脱敏。EvalConfig.user_simulator_config 类型为 Optional[Any],而 BaseUserSimulatorConfig 设置了 extra="allow",允许调用方在其中嵌套任意 LLM 配置(如 api_key/base_url)。一旦凭证放进用户模拟器配置,就会以明文持久化到输出目录,绕过本 PR 已建立的脱敏边界。建议对该快照同样执行 _redact(config.evaluate.model_dump(mode="json", by_alias=True)) 后再写盘。

💡 Suggestion

总结

整体实现稳健:脱敏、原子写盘、gate 判定、回滚与清理错误传播逻辑完整且有测试覆盖,未发现 Critical 级阻塞问题。主要风险是 evaluation_config.snapshot.json 这一处快照漏过了脱敏,建议合入前补上以保持脱敏边界一致。

测试建议

  • 建议补充一条测试:在 user_simulator_config 中嵌入形如 {"api_key": "..."} 的凭证,断言 evaluation_config.snapshot.json 落盘内容已被 ***REDACTED***,覆盖上述 Warning 场景。

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.

构建 Evaluation + Optimization 的自动回归与提示词优化闭环

2 participants