|
| 1 | +# Issue #91 安全评测优化闭环设计 |
| 2 | + |
| 3 | +## 背景 |
| 4 | + |
| 5 | +PR #119 已经提供 fake 评测、候选 prompt、gate 和 JSON/Markdown 报告,但已发布提交中没有一条路径同时完成真实 baseline 评测、候选逐 case 复评、完整 gate 和 gate 后回写。fake model 还会读取 expectation、split、protected 等评测真值,因此现有测试不能证明隐藏样本决策准确率。SDK 模式把 `--update-source` 直接传给 `AgentOptimizer`,候选可能在 wrapper gate 拒绝前写入源 prompt。 |
| 6 | + |
| 7 | +本设计的边界是通过 Issue #91 的验收并安全合并。与验收无关的通用框架重构不纳入本次修改。 |
| 8 | + |
| 9 | +## 目标 |
| 10 | + |
| 11 | +1. fake 和 SDK 共用同一条“评测—归因—优化—复评—gate—审计—可选回写”流程。 |
| 12 | +2. SDK 使用真实 `AgentEvaluator`、`AgentOptimizer` 和 `TargetPrompt`,但外部模型调用仍可在 CI 中替换为确定性实现。 |
| 13 | +3. gate 只消费完整、可核验的逐 case 结果;不再允许 `cases=[]` 或 `partial_applied` 的候选被选中。 |
| 14 | +4. 源 prompt 只在候选通过完整 gate 且用户显式指定 `--update-source` 后写回。 |
| 15 | +5. 提供可执行证据证明隐藏决策准确率、失败归因准确率、过拟合拒绝和三分钟性能要求。 |
| 16 | +6. 报告、输入哈希、耗时、成本和运行产物可以复核,不泄露贡献者机器的绝对路径。 |
| 17 | + |
| 18 | +## 非目标 |
| 19 | + |
| 20 | +- 不修改 `AgentEvaluator`、`AgentOptimizer` 或 GEPA 的公共 API。 |
| 21 | +- 不引入新的远程服务、数据库或长期任务系统。 |
| 22 | +- 不要求 CI 使用真实 API key。 |
| 23 | +- 不把示例扩展成通用生产部署平台。 |
| 24 | + |
| 25 | +## 选定方案 |
| 26 | + |
| 27 | +采用统一闭环编排器。fake 与 SDK 仅负责各自的 evaluate/optimize 适配,候选选择、gate、审计和写回语义由共享 pipeline 实现。 |
| 28 | + |
| 29 | +分别修补两条路径虽然改动较少,但会保留两套不同的 gate 和报告语义;完全 SDK-first 会放大重构范围。统一编排器在验收覆盖、维护成本和合并风险之间更平衡,并可直接承接当前工作树中已有的 SDK EvalSet 与 AgentEvaluator 适配工作。 |
| 30 | + |
| 31 | +## 组件与文件职责 |
| 32 | + |
| 33 | +### `run_pipeline.py` |
| 34 | + |
| 35 | +保留 CLI、路径解析和同步兼容入口。新增异步核心入口的薄包装:同步 `run_pipeline()` 在没有活动事件循环时调用 `asyncio.run(run_pipeline_async(...))`;异步调用者直接使用 `run_pipeline_async()`。该文件不再实现候选循环、SDK 报告拼装或写回逻辑。 |
| 36 | + |
| 37 | +### `eval_loop/pipeline.py` |
| 38 | + |
| 39 | +新增共享编排器,负责: |
| 40 | + |
| 41 | +1. 冻结输入与 prompt 快照; |
| 42 | +2. baseline train/validation 评测; |
| 43 | +3. 失败归因; |
| 44 | +4. 调用 optimizer backend; |
| 45 | +5. 对每个候选重新执行 train/validation 评测; |
| 46 | +6. 计算逐 case delta 和完整 gate; |
| 47 | +7. 选择候选; |
| 48 | +8. 写审计产物; |
| 49 | +9. 在允许时执行安全回写。 |
| 50 | + |
| 51 | +### `eval_loop/backends.py` |
| 52 | + |
| 53 | +backend 统一暴露异步接口: |
| 54 | + |
| 55 | +```python |
| 56 | +class EvaluationBackend(Protocol): |
| 57 | + async def evaluate( |
| 58 | + self, |
| 59 | + *, |
| 60 | + prompt_id: str, |
| 61 | + prompts: dict[str, str], |
| 62 | + dataset_path: Path, |
| 63 | + split: str, |
| 64 | + trace: bool, |
| 65 | + artifact_dir: Path, |
| 66 | + ) -> EvalResult: ... |
| 67 | + |
| 68 | + |
| 69 | +class OptimizationBackend(Protocol): |
| 70 | + async def optimize( |
| 71 | + self, |
| 72 | + *, |
| 73 | + baseline_prompts: dict[str, str], |
| 74 | + baseline_train: EvalResult, |
| 75 | + failure_summary: dict[str, object], |
| 76 | + train_path: Path, |
| 77 | + validation_path: Path, |
| 78 | + config_path: Path, |
| 79 | + artifact_dir: Path, |
| 80 | + ) -> OptimizationResult: ... |
| 81 | +``` |
| 82 | + |
| 83 | +`SDKBackend.optimize()` 始终调用 `AgentOptimizer.optimize(update_source=False)`。它从 `OptimizeResult.rounds[].candidate_prompts` 和 `best_prompts` 提取、去重候选;每个候选由共享 pipeline 做完整 train/validation 复评。`SDKBackend.evaluate()` 使用真实 `AgentEvaluator`,并把每个 case 的逐 metric 分数、pass/fail、原因、证据和可用 trace 转为统一 schema。关键轨迹从 `EvalCaseResult.eval_metric_result_per_invocation[].actual_invocation` 提取用户输入、工具调用和最终回复;SDK 没有提供某类轨迹时显式设置 `trace_available=False`,不能伪造空轨迹为“已采集”。 |
| 84 | + |
| 85 | +`FakeBackend.optimize()` 只接收 baseline train 结果、失败归因和 optimizer 配置,不接收 validation 评测结果。它根据观察到的失败类别生成确定性候选,而不是无条件返回固定候选。 |
| 86 | + |
| 87 | +### `eval_loop/gate.py` |
| 88 | + |
| 89 | +保留唯一 gate 实现。gate 检查: |
| 90 | + |
| 91 | +- validation 总分提升阈值; |
| 92 | +- 训练提升但 validation 不提升的过拟合; |
| 93 | +- 新 hard fail; |
| 94 | +- protected case 退化; |
| 95 | +- 单 case 最大降分; |
| 96 | +- 可选成本预算; |
| 97 | +- baseline 与 candidate case ID 集合一致。 |
| 98 | + |
| 99 | +候选缺失逐 case 数据、成本 gate 所需数据不完整或评测失败时,gate 必须拒绝并返回明确原因。 |
| 100 | + |
| 101 | +### `eval_loop/writeback.py` |
| 102 | + |
| 103 | +新增 prompt 快照和回写组件: |
| 104 | + |
| 105 | +- 保存每个 prompt 文件的原始字节与 SHA-256; |
| 106 | +- 候选复评期间临时应用 prompt,并在 `finally` 中恢复; |
| 107 | +- 恢复后校验哈希,恢复失败立即终止 pipeline; |
| 108 | +- 最终回写前执行 compare-and-swap 检查,避免覆盖并发修改; |
| 109 | +- 多 prompt 使用临时文件和 `os.replace`,任一失败时回滚所有已写字段; |
| 110 | +- 返回结构化 `WritebackResult`。 |
| 111 | + |
| 112 | +### `eval_loop/report.py` |
| 113 | + |
| 114 | +只负责 schema 序列化、Markdown 渲染和 run-specific 审计目录。它不再推导 gate 或 SDK 特殊结果。 |
| 115 | + |
| 116 | +## 统一数据模型 |
| 117 | + |
| 118 | +`CaseResult` 增加逐 metric 数据和 trace 可用性: |
| 119 | + |
| 120 | +```python |
| 121 | +@dataclass(frozen=True) |
| 122 | +class CaseResult: |
| 123 | + case_id: str |
| 124 | + split: str |
| 125 | + score: float |
| 126 | + metrics: dict[str, float] |
| 127 | + passed: bool |
| 128 | + output: str |
| 129 | + trace: dict[str, object] |
| 130 | + trace_available: bool |
| 131 | + failure_category: str | None |
| 132 | + failure_reason: str | None |
| 133 | + evidence: str | None |
| 134 | + cost: float |
| 135 | + hard_failed: bool |
| 136 | + expected_failure_category: str | None |
| 137 | +``` |
| 138 | + |
| 139 | +新增以下审计模型: |
| 140 | + |
| 141 | +- `OptimizationRound`:round ID、候选 prompt bundle、修改理由、optimizer 指标、成本和耗时; |
| 142 | +- `CostSummary`:optimizer、evaluator、agent、total 和 `complete`; |
| 143 | +- `WritebackResult`:`rejected` 表示没有候选通过 gate,`not_requested` 表示候选通过但用户未要求回写,`applied` 表示回写成功,`rolled_back` 表示写入失败且已完整恢复,`rollback_failed` 表示恢复不完整;同时记录前后哈希和错误原因; |
| 144 | +- `OptimizationResult`:所有去重候选、round 记录和 backend 原始摘要。 |
| 145 | + |
| 146 | +## 端到端数据流 |
| 147 | + |
| 148 | +1. 解析并严格验证四类输入文件、gate 配置和 target prompt 路径。 |
| 149 | +2. 生成唯一 `run_id`,创建 `runs/<run_id>.tmp/`,启动 `perf_counter`。 |
| 150 | +3. 对输入文件和源 prompt 做字节级快照与 SHA-256。 |
| 151 | +4. 用 baseline prompt 分别评测 train 和 validation;任一 baseline 评测缺 case 或恢复 prompt 失败时终止。 |
| 152 | +5. 仅根据 baseline train 失败结果生成失败归因和候选。 |
| 153 | +6. 对每个候选分别完整评测 train 和 validation,校验 case ID 一致,再计算 delta。 |
| 154 | +7. 对每个候选执行同一完整 gate,并从已接受候选中按 validation、train、原始顺序稳定选择最佳项。 |
| 155 | +8. 写候选、round、评测结果、delta、gate、输入快照哈希和真实耗时到临时 run 目录。 |
| 156 | +9. 若没有接受候选或未指定 `--update-source`,记录对应 writeback 状态。 |
| 157 | +10. 若允许回写,先确认源文件仍与起始快照一致,再执行原子多文件写入;失败则回滚并记录原因。 |
| 158 | +11. 完成 `writeback.json`、最终 JSON/Markdown 后,把临时目录原子重命名为 `runs/<run_id>/`。已存在的 run ID 不允许覆盖。 |
| 159 | +12. 根目录 `optimization_report.json` 和 `.md` 仅作为最新结果的便利副本;不可变证据以 run 目录为准。 |
| 160 | + |
| 161 | +## Fake 模式隔离规则 |
| 162 | + |
| 163 | +fake model 只能读取用户输入和当前 prompt 文本。evalset 的 expectation、expected answer、split、protected 和标签只能由 evaluator、归因器或 gate 使用。 |
| 164 | + |
| 165 | +公开样例输入改成包含明确业务值的指令,例如 `intent=refund, priority=high`。fake model 从用户指令解析值:baseline 对严格格式请求加入多余说明,过度候选对所有请求强制 JSON,安全候选只在用户明确要求时使用严格格式。相同输入和 prompt 在 train/validation 中必须产生相同输出。 |
| 166 | + |
| 167 | +fake optimizer 根据 baseline train 中的失败类别选择规则模板。例如发现 format 与 exact-answer 失败时,生成一个全局严格候选和一个按请求限定的候选。它不得读取 validation 结果或 case ID。 |
| 168 | + |
| 169 | +## 成本语义 |
| 170 | + |
| 171 | +`max_total_cost` 是可选 gate。`CostSummary.complete=True` 时,total 必须是已知 optimizer、evaluator 和 agent 成本之和。fake backend 提供完整成本。 |
| 172 | + |
| 173 | +SDK 无法获得 agent/provider 完整成本时设置 `complete=False`。若配置了成本上限,候选必须以 `cost_unavailable` 拒绝;未配置成本上限时可以继续其他 gate,但报告必须把 SDK 提供的数值标记为 `reported_optimizer_cost`,不能称为完整总成本。 |
| 174 | + |
| 175 | +## 错误处理 |
| 176 | + |
| 177 | +- baseline 输入、评测或 prompt 恢复失败:终止运行,不选择候选,不最终回写。 |
| 178 | +- 单个候选评测失败:记录候选拒绝原因,继续评测其他候选。 |
| 179 | +- case ID 缺失、重复或集合不一致:该候选拒绝。 |
| 180 | +- 非有限数值、非标准 JSON、重复 target path、split 冲突:输入阶段直接报错。 |
| 181 | +- 最终回写 compare-and-swap 失败:不覆盖源文件,记录并抛出并发修改错误。 |
| 182 | +- 多文件写入失败:回滚已写字段;回滚成功时状态为 `rolled_back`,回滚失败时状态为 `rollback_failed` 并报告受影响路径。 |
| 183 | +- 审计写入失败:最终回写不会开始。 |
| 184 | + |
| 185 | +## 审计产物 |
| 186 | + |
| 187 | +每个 `runs/<run_id>/` 至少包含: |
| 188 | + |
| 189 | +- `optimization_report.json` 与 `optimization_report.md`; |
| 190 | +- `input_hashes.json` 和规范化配置快照; |
| 191 | +- `baseline_prompts/`、`candidate_prompts/` 与 `prompt_diffs/`; |
| 192 | +- `case_results/` 和 `per_case_deltas.json`; |
| 193 | +- `rounds/`,保存每轮候选、理由、成本、耗时和 optimizer 指标; |
| 194 | +- `gate_decisions.json`; |
| 195 | +- `writeback.json`; |
| 196 | +- SDK 模式下的 `sdk_optimizer/` 原始产物。 |
| 197 | + |
| 198 | +报告路径优先保存仓库相对路径;仓库外输入仅保存用户传入的规范化路径,不写入维护者机器生成样例。示例报告由测试临时生成并与 committed inputs 的哈希自动比对。 |
| 199 | + |
| 200 | +## 测试设计 |
| 201 | + |
| 202 | +### Backend 契约测试 |
| 203 | + |
| 204 | +fake 与 SDK backend 都必须返回完整、case ID 唯一且集合一致的 `EvalResult`。SDK 测试使用真实 `AgentEvaluator`、`AgentOptimizer` 和 `TargetPrompt`;仅 monkeypatch GEPA/外部反思模型调用,沿用仓库现有 facade 测试模式。 |
| 205 | + |
| 206 | +### 安全回写测试 |
| 207 | + |
| 208 | +- gate 拒绝且传入 `--update-source` 时,所有源 prompt 字节和哈希保持不变; |
| 209 | +- gate 接受时才写入选中候选; |
| 210 | +- 多 prompt 第二个字段写入失败时,第一个字段回滚; |
| 211 | +- 起始快照后发生并发修改时拒绝覆盖; |
| 212 | +- 审计写入失败时不触发回写。 |
| 213 | + |
| 214 | +### 隐藏决策准确率 |
| 215 | + |
| 216 | +新增至少 10 个与公开六例分离的 holdout 场景,覆盖安全提升、无提升、训练提升而 validation 退化、protected regression、new hard fail、单 case drop 和超预算。标签与 pipeline 输入分离,计算 `correct / total` 并断言 `>= 0.80`。 |
| 217 | + |
| 218 | +### 失败归因准确率 |
| 219 | + |
| 220 | +新增独立归因语料,覆盖 format、final response、tool、parameter、rubric 和 knowledge 类别。归因器只接收评测错误和证据,不接收标签;断言准确率 `>= 0.75`,且每个失败都有非空 reason 与 evidence。 |
| 221 | + |
| 222 | +### 性能与报告 |
| 223 | + |
| 224 | +用 wall clock 运行完整 fake+trace pipeline,断言少于 180 秒;报告中的 duration 必须大于零且不大于测试观测值的合理上界。测试重新生成 example report,校验输入哈希、相对路径、严格 JSON、全部必需字段和旧 run 不被覆盖。 |
| 225 | + |
| 226 | +## 兼容性与迁移 |
| 227 | + |
| 228 | +- 保留现有 CLI 参数和同步 `run_pipeline()`;新增异步入口不破坏已有调用方。 |
| 229 | +- fake 和 SDK 输入统一使用官方 SDK EvalSet 形状;loader 在本次 PR 内继续兼容旧 `cases` 形状,但 README 和 committed examples 只展示官方形状。 |
| 230 | +- JSON schema 版本提升,新增字段不复用旧字段表达不同语义。 |
| 231 | +- `--update-source` 的用户语义保持不变,但实际执行从 optimizer 内部提前写入改为完整 gate 后写入。 |
| 232 | + |
| 233 | +## 合并标准 |
| 234 | + |
| 235 | +以下条件全部满足后才建议合并: |
| 236 | + |
| 237 | +1. 六个公开 case 完整运行并生成 JSON、Markdown 和审计目录; |
| 238 | +2. SDK 路径提供真实 baseline/candidate 逐 case 结果,不存在可被接受的 partial gate; |
| 239 | +3. 训练提升而 validation 退化的候选被拒绝; |
| 240 | +4. holdout 决策准确率至少 80%; |
| 241 | +5. 独立归因准确率至少 75%,所有失败可解释; |
| 242 | +6. fake+trace wall clock 少于三分钟且报告记录真实耗时; |
| 243 | +7. gate 拒绝、评测失败、审计失败和并发修改时源 prompt 不被覆盖; |
| 244 | +8. committed example report 的哈希与 committed inputs 一致且没有贡献者绝对路径; |
| 245 | +9. 目标测试、完整示例测试、仓库 lint、build 和 CI 全部通过。 |
0 commit comments