Skip to content

Commit be2401d

Browse files
committed
Harden eval optimize loop example
1 parent 2646e37 commit be2401d

24 files changed

Lines changed: 1719 additions & 186 deletions
Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
11
# 设计说明
22

3-
本示例把失败归因、候选生成、验证门禁和报告持久化拆成独立模块,便于审阅。
4-
失败归因采用规则表:JSON 解析失败归为格式违规,缺字段和值不匹配归为最终答案不一致,
5-
rubric 缺项归为裁判失败,超长归为长度违规;每个失败用例都保留原因和证据。
6-
门禁先比较 train/validation 的总体变化,再检查新增硬失败、受保护用例回退、
7-
单用例最大降分和成本预算,避免只在训练集变好的候选被接受。过拟合候选会强制 JSON,
8-
故能提升训练集却伤害自然语言验证用例和受保护 yes/no 用例;安全候选只在用户明确要求时启用严格格式。
9-
报告同时写 JSON 和 Markdown,记录 seed、成本、耗时、配置哈希、候选 prompt 与 diff,形成可追溯审计。
10-
fake mode 的目的不是模拟真实模型能力,而是让评审和 CI 在无 API key 条件下稳定复现闭环行为。
11-
这种设计也降低了迁移成本:真实接入时只需替换模型调用和优化器,评估结果、门禁策略与报告结构仍可沿用,
12-
从而把示例中的确定性保障保留下来。
3+
本示例把闭环拆成加载、评测、归因、候选生成、门禁和报告六层。失败归因采用可审计规则表:JSON 解析和禁用格式命中归为 `format_violation`,最终答案错误归为 `final_response_mismatch`,工具名错误、参数错误、知识召回不足和超长分别进入独立类别;若用例声明期望归因,报告会计算 accuracy。接受门禁先要求 validation 提升,再拒绝 train 提升但 validation 不提升的过拟合候选,并检查受保护用例降分、新硬失败、单例最大降分和成本预算。防过拟合依赖训练集与验证集分离、protected case、delta_type 和逐例降分阈值,而不是只看平均分;每个拒绝理由都会进入审计报告,便于复盘,也便于评审核对策略。fake mode 用通用 expectation/tags/protected/simulated_outputs 生成输出,不依赖样例名,可稳定覆盖隐藏样本;sdk mode 则通过 `SDKBackend` 调用 `AgentOptimizer` 与 `TargetPrompt`,用于真实接入,两者共享门禁、归因和报告结构。报告写 JSON、Markdown 和 runs 审计目录,保存输入哈希、配置快照、候选 prompt、diff、case 结果和成本。默认不回写源 prompt,只有显式 `--update-source` 才允许,避免示例运行污染源码。
Lines changed: 95 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,46 @@
11
# Evaluation + Optimization Closed Loop
22

3-
This example implements a reproducible evaluation and prompt-optimization loop
4-
that runs without `TRPC_AGENT_API_KEY` or any external model provider. It is an
5-
example-local implementation: the code mirrors the evaluator/optimizer workflow
6-
with a thin adapter so the behavior is deterministic and easy to review.
3+
This example implements issue #91 as a reproducible evaluation + optimization
4+
loop. The default path is deterministic fake mode, so it runs in CI and on a
5+
fresh checkout without `TRPC_AGENT_API_KEY` or any external model provider. A
6+
real SDK adapter path is also present in `eval_loop/backends.py` for
7+
`AgentOptimizer` / `TargetPrompt` integration.
78

89
## Architecture
910

1011
```text
11-
train.evalset.json val.evalset.json baseline_system_prompt.txt
12-
| | |
13-
v v v
14-
loader.py --------------> evaluator.py <----------- fake_model.py
15-
| | |
16-
| v |
17-
| fake_judge.py |
18-
| | |
19-
v v |
20-
attribution.py <------ baseline + candidate results |
21-
| | |
22-
v v |
23-
optimizer.py --------> candidate prompts --------------+
24-
| |
25-
v v
26-
gate.py ------------> accept/reject decisions
12+
train.evalset.json + val.evalset.json + optimizer.json + baseline prompt
2713
|
2814
v
29-
report.py ----------> optimization_report.json/.md
15+
loader.py / config.py --> validated cases, gate config, input hashes
16+
|
17+
v
18+
backends.py
19+
|-- FakeBackend -> FakeModel + FakeJudge + FakeOptimizer
20+
`-- SDKBackend -> AgentOptimizer + TargetPrompt + user call_agent
21+
|
22+
v
23+
attribution.py -> evaluator.py -> gate.py -> report.py
24+
|
25+
v
26+
optimization_report.json / optimization_report.md / runs/<run_id>/ audit files
3027
```
3128

3229
## Quick Start
3330

34-
Short deterministic command:
31+
One-command fake mode:
3532

3633
```bash
3734
python examples/optimization/eval_optimize_loop/run_pipeline.py --fake-model --fake-judge --trace
3835
```
3936

40-
Full command with explicit inputs:
37+
Equivalent new form:
38+
39+
```bash
40+
python examples/optimization/eval_optimize_loop/run_pipeline.py --mode fake --trace
41+
```
42+
43+
Full fake command:
4144

4245
```bash
4346
python examples/optimization/eval_optimize_loop/run_pipeline.py \
@@ -46,86 +49,94 @@ python examples/optimization/eval_optimize_loop/run_pipeline.py \
4649
--optimizer-config examples/optimization/eval_optimize_loop/data/optimizer.json \
4750
--prompt examples/optimization/eval_optimize_loop/prompts/baseline_system_prompt.txt \
4851
--output-dir /tmp/eval-optimize-loop \
49-
--fake-model \
50-
--fake-judge \
52+
--mode fake \
5153
--trace
5254
```
5355

54-
The default output directory is the system temp directory,
55-
`eval-optimize-loop`. The fixed checked-in examples are:
56+
SDK adapter command shape:
5657

57-
- `outputs/optimization_report.example.json`
58-
- `outputs/optimization_report.example.md`
58+
```bash
59+
python examples/optimization/eval_optimize_loop/run_pipeline.py \
60+
--mode sdk \
61+
--train path/to/sdk_train.evalset.json \
62+
--val path/to/sdk_val.evalset.json \
63+
--optimizer-config path/to/sdk_optimizer.json \
64+
--prompt path/to/system_prompt.txt \
65+
--sdk-call-agent your_package.your_module:call_agent \
66+
--output-dir /tmp/eval-optimize-loop-sdk
67+
```
5968

60-
## What The Example Demonstrates
69+
`--sdk-call-agent` must point to an async callable compatible with
70+
`AgentOptimizer.optimize(call_agent=...)`. Configure any real model credentials
71+
needed by that callable. SDK mode never silently falls back to fake mode.
6172

62-
- Baseline evaluation on train and validation evalsets.
63-
- Rule-based failure attribution for failed cases.
64-
- A fake optimizer that proposes two candidates:
65-
- `candidate_001_overfit`: improves train score but regresses validation.
66-
- `candidate_002_safe`: improves validation without protected regression.
67-
- Candidate validation on the full validation set.
68-
- A configurable gate that rejects train-only gains and protected-case
69-
regressions.
70-
- Audit persistence with seed, cost, deterministic duration, config hash, and
71-
candidate prompt text.
73+
## Source Prompt Writes
7274

73-
The six eval cases cover:
75+
The default is **no source write-back**. The baseline prompt file is not modified
76+
by fake mode, and `SDKBackend` calls `AgentOptimizer.optimize(update_source=False)`
77+
unless `--update-source` is explicitly passed. The report records
78+
`run.update_source` and the Markdown report states whether source write-back was
79+
enabled.
7480

75-
- optimization succeeds: `candidate_002_safe` is accepted;
76-
- optimization has no effect: rubric cases stay unchanged;
77-
- optimization regresses/overfits: `candidate_001_overfit` forces JSON on
78-
validation prose and protected exact-answer cases.
81+
## Candidate Behavior
7982

80-
## Fake Model, Fake Judge, And Trace Mode
83+
The fake optimizer proposes exactly two candidates:
8184

82-
`--fake-model` uses deterministic prompt markers to simulate baseline, overfit,
83-
and safe candidate behavior. `--fake-judge` scores JSON, exact-answer, and
84-
rubric cases locally. `--trace` stores per-case fake model and judge decisions
85-
inside `optimization_report.json`, which makes failures reviewable without
86-
calling an external service.
85+
- `candidate_001_overfit`: fixes train formatting but forces JSON too broadly;
86+
it improves train score and regresses validation, so the gate rejects it.
87+
- `candidate_002_safe`: applies strict JSON/exact-answer behavior only when
88+
requested; it improves validation without protected-case regression, so the
89+
gate may accept it.
8790

88-
This example currently requires `--fake-model --fake-judge`. That is deliberate:
89-
it keeps the PR deterministic and ensures the example runs in CI without API
90-
keys.
91+
The fake model is driven by `EvalCase.expectation`, `tags`, `protected`, and
92+
optional `simulated_outputs`; it does not depend on sample `case_id` names.
9193

92-
## Inspecting Reports
94+
## Reports
9395

94-
Open `optimization_report.md` first for the human-readable decision:
96+
`optimization_report.json` includes:
9597

96-
- final selected candidate;
97-
- why the overfit candidate was rejected;
98-
- why the safe candidate was accepted;
99-
- baseline vs candidate score table;
100-
- per-case delta table;
101-
- failure attribution summary;
102-
- prompt diffs;
103-
- reproducibility command.
98+
- `schema_version`;
99+
- `run` metadata: mode, fake flags, trace flag, case counts, update-source flag,
100+
and input paths;
101+
- `baseline` plus compatibility fields `baseline_train` and
102+
`baseline_validation`;
103+
- all candidate train/validation results, rationale, and prompt diff;
104+
- per-case deltas with `delta_type` (`new_pass`, `new_fail`, `score_up`,
105+
`score_down`, `unchanged`);
106+
- failure attribution summary and attribution accuracy when expected labels are
107+
present;
108+
- gate decisions with overfit detection, protected regressions, new hard
109+
failures, excessive drops, and cost fields;
110+
- audit data: seed, duration, config hash, input hashes, candidate prompt hashes,
111+
cost, prompt diffs, and reproducibility command.
104112

105-
Use `optimization_report.json` for automation and audit. It includes the same
106-
decision data plus full case outputs, traces, costs, config hash, and candidate
107-
prompt snapshots.
113+
`optimization_report.md` includes final decision, gate reasons, score table,
114+
per-case delta table, failure attribution summary, cost/audit details, prompt
115+
diffs, and the reproducibility command.
108116

109-
## Run Tests
117+
`report.py` also writes audit artifacts under `output_dir/runs/<run_id>/`:
110118

111-
```bash
112-
python -m pytest examples/optimization/eval_optimize_loop/tests
113-
```
119+
- `config.snapshot.json`;
120+
- `input_hashes.json`;
121+
- `candidate_prompts/<candidate_id>/system_prompt.txt`;
122+
- `case_results/<candidate_id>_<split>.json`;
123+
- `prompt_diffs/<candidate_id>.diff`.
114124

115-
The tests cover gate rejection/acceptance, failure attribution, fake-mode report
116-
generation, and deterministic output with the same seed.
125+
The repository keeps only stable examples:
117126

118-
## Switching To Real Evaluator/Optimizer Later
127+
- `outputs/optimization_report.example.json`
128+
- `outputs/optimization_report.example.md`
129+
130+
Runtime `optimization_report.json`, `optimization_report.md`, and `runs/`
131+
directories are not committed.
119132

120-
The example intentionally isolates integration points:
133+
## Run Tests
121134

122-
- replace `ExampleEvaluator` in `eval_loop/evaluator.py` with an adapter around
123-
`AgentEvaluator` when real agent execution and SDK evalsets are desired;
124-
- replace `FakeOptimizer` in `eval_loop/optimizer.py` with `AgentOptimizer` or a
125-
remote optimizer;
126-
- keep `gate.py`, `attribution.py`, and `report.py` as reviewable policy and
127-
audit layers around the real components.
135+
```bash
136+
python -m pytest examples/optimization/eval_optimize_loop/tests
137+
```
128138

129-
When switching to real mode, keep train and validation files distinct, preserve
130-
the protected-case gate, and continue writing both JSON and Markdown reports so
131-
optimization decisions remain reviewable.
139+
The tests cover fake hidden-sample generalization, config validation, gate
140+
rejection paths, protected-case behavior, failure attribution, tool/knowledge
141+
judge paths, SDK adapter wiring through monkeypatching, deterministic report
142+
generation, and both CLI forms.

examples/optimization/eval_optimize_loop/data/optimizer.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"name": "fake_two_candidate_optimizer",
55
"description": "Deterministically proposes one overfit candidate and one safe candidate."
66
},
7+
"metrics": {
8+
"case_score": "mean",
9+
"failure_attribution": "rule_based"
10+
},
711
"gate": {
812
"min_val_score_improvement": 0.01,
913
"allow_new_hard_fail": false,

examples/optimization/eval_optimize_loop/data/train.evalset.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"expected_values": {
1616
"intent": "refund",
1717
"priority": "high"
18-
}
18+
},
19+
"expected_failure_category": "format_violation"
1920
},
2021
"tags": [
2122
"json",
@@ -27,7 +28,8 @@
2728
"input": "Answer exactly READY when the order can ship.",
2829
"expectation": {
2930
"type": "exact",
30-
"expected": "READY"
31+
"expected": "READY",
32+
"expected_failure_category": "final_response_mismatch"
3133
},
3234
"tags": [
3335
"exact",

examples/optimization/eval_optimize_loop/data/val.evalset.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"expected_values": {
1616
"next_step": "email_customer",
1717
"status": "approved"
18-
}
18+
},
19+
"expected_failure_category": "format_violation"
1920
},
2021
"tags": [
2122
"json",
@@ -36,7 +37,8 @@
3637
"}",
3738
"json"
3839
],
39-
"max_chars": 180
40+
"max_chars": 180,
41+
"expected_failure_category": "format_violation"
4042
},
4143
"tags": [
4244
"rubric",
@@ -48,7 +50,8 @@
4850
"input": "Answer exactly YES if idempotent retries are safe for duplicate requests.",
4951
"expectation": {
5052
"type": "exact",
51-
"expected": "YES"
53+
"expected": "YES",
54+
"expected_failure_category": "final_response_mismatch"
5255
},
5356
"protected": true,
5457
"tags": [

examples/optimization/eval_optimize_loop/eval_loop/attribution.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@
1010

1111
_ERROR_TO_ATTRIBUTION = {
1212
"json_parse_failure": ("format_violation", "output is not valid JSON"),
13-
"required_key_missing": ("final_answer_mismatch", "required JSON key is missing"),
14-
"json_value_mismatch": ("final_answer_mismatch", "JSON value does not match expected value"),
15-
"exact_answer_mismatch": ("final_answer_mismatch", "normalized exact answer mismatch"),
13+
"required_key_missing": ("final_response_mismatch", "required JSON key is missing"),
14+
"json_value_mismatch": ("final_response_mismatch", "JSON value does not match expected value"),
15+
"exact_answer_mismatch": ("final_response_mismatch", "normalized exact answer mismatch"),
1616
"forbidden_pattern": ("format_violation", "output contains a forbidden pattern"),
17-
"missing_rubric_terms": ("llm_rubric_failed", "required rubric terms are missing"),
17+
"missing_rubric_terms": ("llm_rubric_not_met", "required rubric terms are missing"),
1818
"max_chars_exceeded": ("length_violation", "output exceeds max_chars"),
19+
"tool_call_error": ("tool_call_error", "tool call did not match expected tool"),
20+
"parameter_error": ("parameter_error", "tool call parameters did not match"),
21+
"knowledge_recall_insufficient": (
22+
"knowledge_recall_insufficient",
23+
"required knowledge evidence was not recalled",
24+
),
1925
}
2026

2127

@@ -36,6 +42,8 @@ def summarize_failures(results: Iterable[EvalResult]) -> dict[str, object]:
3642
by_prompt: dict[str, dict[str, int]] = {}
3743
examples: list[dict[str, str]] = []
3844
total_failed = 0
45+
expected_total = 0
46+
expected_correct = 0
3947

4048
for result in results:
4149
prompt_key = f"{result.prompt_id}:{result.split}"
@@ -47,6 +55,10 @@ def summarize_failures(results: Iterable[EvalResult]) -> dict[str, object]:
4755
category = case.failure_category or "unknown_failure"
4856
by_category[category] += 1
4957
by_prompt[prompt_key][category] = by_prompt[prompt_key].get(category, 0) + 1
58+
if case.expected_failure_category:
59+
expected_total += 1
60+
if case.expected_failure_category == category:
61+
expected_correct += 1
5062
examples.append({
5163
"prompt_id": result.prompt_id,
5264
"split": result.split,
@@ -61,4 +73,10 @@ def summarize_failures(results: Iterable[EvalResult]) -> dict[str, object]:
6173
"by_category": dict(sorted(by_category.items())),
6274
"by_prompt_split": {key: dict(sorted(value.items())) for key, value in sorted(by_prompt.items())},
6375
"examples": examples,
76+
"attribution_accuracy": (
77+
round(expected_correct / expected_total, 6)
78+
if expected_total
79+
else None
80+
),
81+
"expected_labeled_failures": expected_total,
6482
}

0 commit comments

Comments
 (0)