Skip to content

Commit 8e68fcb

Browse files
committed
feat(examples): add eval optimization loop
1 parent e7eb97e commit 8e68fcb

12 files changed

Lines changed: 2112 additions & 118 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
runs/
2+
__pycache__/
3+
*.pyc
4+

examples/optimization/eval_optimize_loop/README.md

Lines changed: 144 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ To refresh the committed sample reports:
2929
PYTHONPATH=../../.. python run_pipeline.py --mode fake --update-sample-outputs
3030
```
3131

32+
The fake and trace modes support three deterministic scenarios:
33+
34+
```bash
35+
PYTHONPATH=../../.. python run_pipeline.py --mode fake --scenario overfit
36+
PYTHONPATH=../../.. python run_pipeline.py --mode fake --scenario accepted
37+
PYTHONPATH=../../.. python run_pipeline.py --mode fake --scenario cost_exceeded
38+
```
39+
40+
- `overfit` is the default: train improves, validation does not improve, and a
41+
critical case regresses, so the gate rejects the candidate.
42+
- `accepted`: train and validation both improve with no new hard fail, so the
43+
gate accepts the candidate.
44+
- `cost_exceeded`: quality improves, but the estimated cost exceeds
45+
`gate.max_cost_usd`, so the gate rejects the candidate.
46+
47+
For CI-style scripts, add `--ci-exit-code`. Normal demo runs always exit 0 after
48+
writing the report; CI mode exits 0 for accepted candidates and 1 for rejected
49+
candidates:
50+
51+
```bash
52+
PYTHONPATH=../../.. python run_pipeline.py --mode fake --scenario accepted --ci-exit-code
53+
PYTHONPATH=../../.. python run_pipeline.py --mode fake --scenario overfit --ci-exit-code
54+
```
55+
3256
Trace replay mode also runs without model credentials. It first records fake
3357
outputs into generated `eval_mode: "trace"` evalsets, then evaluates those
3458
`actual_conversation` records without invoking `call_agent`:
@@ -48,81 +72,166 @@ export TRPC_AGENT_MODEL_NAME="<your-model>"
4872
PYTHONPATH=../../.. python run_pipeline.py --mode optimizer
4973
```
5074

75+
## Verification Commands
76+
77+
Use these commands to exercise the important review paths:
78+
79+
```bash
80+
cd examples/optimization/eval_optimize_loop
81+
PYTHONPATH=../../.. python run_pipeline.py --mode fake --scenario overfit
82+
PYTHONPATH=../../.. python run_pipeline.py --mode fake --scenario accepted
83+
PYTHONPATH=../../.. python run_pipeline.py --mode fake --scenario cost_exceeded
84+
PYTHONPATH=../../.. python run_pipeline.py --mode trace --scenario overfit
85+
cd ../../..
86+
pytest tests/evaluation/test_eval_optimize_loop_example.py -q
87+
```
88+
5189
## Inputs
5290

5391
| File | Role |
5492
| --- | --- |
5593
| `train.evalset.json` | Training split used for baseline scoring and optimization feedback. |
5694
| `val.evalset.json` | Validation split used for regression and gate decisions. |
5795
| `optimizer.json` | Shared metric, optimizer, fake candidate, and gate configuration. |
58-
| `agent/prompts/system.md` | Baseline prompt source registered as `TargetPrompt("system_prompt")`. |
96+
| `case_meta.json` | Public-case attribution labels used for local self-checking. |
97+
| `agent/prompts/router.md` | Router prompt source registered as `TargetPrompt("router_prompt")`. |
98+
| `agent/prompts/system.md` | System prompt source registered as `TargetPrompt("system_prompt")`. |
99+
| `agent/prompts/skill.md` | Skill prompt source registered as `TargetPrompt("skill_prompt")`. |
59100

60101
The fake mode still uses `AgentEvaluator` for all train and validation scoring.
61102
Only candidate generation is deterministic, so the example can run in CI or on a
62103
laptop without model credentials.
63104

105+
At startup, the pipeline validates that all required input files exist, train and
106+
validation evalsets are distinct and non-empty, gate keys are present, critical
107+
case IDs refer to validation cases, and evaluator parallelism is positive.
108+
109+
Fake judge behavior is implemented by local exact-match final-response scoring
110+
(`final_response_avg_score`). The report records this as
111+
`run.judge_mode = "local_exact_match_fake_judge"`, so the evaluation path does
112+
not need an LLM judge or API key.
113+
114+
For tool-only and rubric-only failure classes, fake judge mode uses
115+
deterministic attribution hints from `case_meta.json`. This lets the no-key
116+
example exercise the full attribution taxonomy without requiring real tool
117+
execution or an LLM rubric judge.
118+
64119
## Outputs
65120

66-
Each run writes to `runs/<mode_timestamp>/`:
121+
Each run writes to `runs/<mode>_<scenario>_<timestamp>/` by default:
67122

68123
| File or directory | Contents |
69124
| --- | --- |
70-
| `optimization_report.json` | Machine-readable baseline, candidate, delta, attribution, gate, and audit payload. |
125+
| `optimization_report.json` | Machine-readable baseline, candidate, per-round prompts, delta, attribution, gate, and audit payload. |
71126
| `optimization_report.md` | Human-readable report for review. |
72127
| `candidate_prompts/` | Candidate prompt text per target field. |
73128
| `eval_*` | Raw `AgentEvaluator` result files for every phase. |
74129
| `config.snapshot.json` | Reproducible copy of the input config. |
75130
| `eval_metrics.snapshot.json` | Evaluator-compatible metric config extracted from `optimizer.json.evaluate`. |
76131

132+
The report also includes `prompt_audit`, which records each target prompt's
133+
source path, baseline SHA-256, candidate SHA-256, character counts, and whether
134+
the candidate changed the prompt. Each prompt audit entry also includes a capped
135+
unified diff preview, so reviewers can see what changed without opening the
136+
candidate prompt files. This makes the score-to-prompt relationship auditable
137+
even when reports are copied outside the run directory.
138+
139+
`input_audit` records stable SHA-256 hashes and byte counts for the train
140+
evalset, validation evalset, optimizer config, case metadata, and prompt source
141+
files. This makes a report reproducible even after it is detached from the
142+
working tree.
143+
144+
`audit.cost` splits the total estimated spend into optimizer and evaluation
145+
components, then applies the same total to the configurable budget gate. In
146+
fake/trace mode both components are deterministic and normally zero except for
147+
the dedicated `cost_exceeded` scenario.
148+
149+
`failure_attribution.self_check` compares attribution results against the public
150+
gold labels in `case_meta.json`. Gold labels live in `expected_failure_types`.
151+
For categories that cannot naturally arise in the no-key black-box path, such as
152+
tool-call and LLM-rubric failures, `fake_attribution_hints` provides an explicit
153+
fake-judge hint. The report marks those cases as `hint_assisted` and also reports
154+
rule-only accuracy separately. This is not a hidden-set proof, but it provides a
155+
local accuracy guard for the bundled examples and fails report validation if the
156+
labeled-case accuracy drops below 75%.
157+
77158
Committed examples live in `sample_outputs/`.
78159

160+
The JSON report includes both human reasons and structured gate checks:
161+
162+
```json
163+
{
164+
"gate": {
165+
"decision": "rejected",
166+
"reasons": ["..."],
167+
"checks": [
168+
{"name": "min_validation_score_delta", "passed": false},
169+
{"name": "no_new_hard_fail", "passed": false}
170+
]
171+
}
172+
}
173+
```
174+
175+
## Gate Decision Contract
176+
177+
A candidate is accepted only when every configured gate check passes:
178+
179+
| Check | Accept condition | Reject condition |
180+
| --- | --- | --- |
181+
| `min_validation_score_delta` | Validation score delta is greater than or equal to `gate.min_validation_score_delta`. | Validation score delta is below the configured threshold. |
182+
| `no_new_hard_fail` | No validation case changes from pass to fail, unless `gate.allow_new_hard_fail` is true. | At least one validation case becomes a new hard fail while new hard fails are disallowed. |
183+
| `critical_case_regression` | No case in `gate.critical_case_ids` loses score. | Any critical validation case regresses. |
184+
| `overfitting_guard` | Validation improves, or training does not improve. | Training improves while validation does not improve. |
185+
| `max_cost_usd` | Estimated candidate cost is less than or equal to `gate.max_cost_usd`. | Estimated candidate cost exceeds the budget. |
186+
187+
This contract is intentionally validation-first: training improvements alone
188+
never justify acceptance, and hidden samples are expected to follow the same
189+
structured decision rules.
190+
79191
## Case Coverage
80192

81-
The six sample cases cover the required outcomes:
193+
The ten sample cases exceed the minimum requirement and cover the required outcomes:
82194

83195
| Case | Split | Expected behavior |
84196
| --- | --- | --- |
85197
| `train_refund_double_charge` | train | Candidate fixes a baseline failure. |
86198
| `train_password_reset` | train | Already passing; candidate is unchanged. |
87199
| `train_legacy_sync` | train | Candidate does not help. |
200+
| `train_plan_question` | train | Stable non-target billing question remains passing. |
201+
| `train_policy_tool_missing` | train | Tool lookup failure attribution with an incomplete JSON response. |
88202
| `val_vip_refund` | validation | Candidate creates a new pass. |
89203
| `val_plan_question` | validation | Candidate has no effect. |
90204
| `val_checkout_outage` | validation | Candidate regresses a critical case. |
205+
| `val_mobile_crash` | validation | Stable technical troubleshooting case remains passing. |
206+
| `val_rubric_tone_fail` | validation | Deterministic rubric failure attribution remains failing. |
91207

92208
The default fake candidate improves train score but does not improve validation
93209
overall because it also introduces a new hard fail. The gate therefore rejects
94210
the candidate and records the overfitting reason.
95211

212+
## Requirement Mapping
213+
214+
| #91 requirement | Implementation |
215+
| --- | --- |
216+
| Baseline train/validation evaluation | `run_pipeline.py::evaluate_dataset` uses `AgentEvaluator.get_executer(...).get_result()`. |
217+
| Per-case metric, pass/fail, reason, trace | Report cases include query, expected, actual, metric details, status, failure types, and reason. |
218+
| Failure attribution clustering | `attribute_failure` and `build_failure_stats`. |
219+
| Full attribution taxonomy | Public sample failures include `final_response_mismatch`, `tool_call_error`, `parameter_error`, `llm_rubric_not_met`, `knowledge_recall_insufficient`, and `format_violation`. |
220+
| Attribution accuracy guard | `case_meta.json` plus `failure_attribution.self_check` validates public labeled cases and marks fake hint-assisted cases. |
221+
| Prompt optimization target | `TargetPrompt` registers `router_prompt`, `system_prompt`, and `skill_prompt`. |
222+
| AgentOptimizer or equivalent extension | `--mode optimizer` delegates to `AgentOptimizer`; fake/trace modes use deterministic candidate generation for no-key CI. |
223+
| Candidate validation and per-case delta | `build_case_deltas` marks `new_pass`, `new_fail`, `score_improved`, `score_regressed`, and `unchanged`. |
224+
| Configurable gate | `optimizer.json.gate` plus `apply_gate`, including structured `gate.checks`. |
225+
| Overfitting rejection | Default `--scenario overfit` improves train but rejects because validation does not improve and a critical case regresses. |
226+
| Cost budget rejection | `--scenario cost_exceeded` improves quality but fails `max_cost_usd`. |
227+
| Audit artifacts | `input_audit`, `prompt_audit` with diff previews, `optimization_rounds`, `candidate_prompts/`, raw `eval_*` outputs, config snapshots, JSON report, and Markdown report. |
228+
| No API key mode | `--mode fake` and `--mode trace`. |
229+
| 6 public cases | `train.evalset.json` has 5 cases and `val.evalset.json` has 5 cases. |
230+
96231
## Design Notes
97232

98-
The pipeline treats evaluation results as the source of truth and converts
99-
`AgentEvaluator.get_executer(...).get_result()` into a stable per-case report.
100-
For each split, it records score, pass/fail status, normalized actual and
101-
expected final responses, metric details, and a short trace summary. Failure
102-
attribution is deterministic: invalid JSON or missing keys are classified as
103-
format violations; category mismatches indicate knowledge-recall gaps; priority
104-
mismatches are parameter errors; action mismatches are final-response errors.
105-
This keeps fake mode explainable and also gives real optimizer runs a fallback
106-
when no LLM judge reason is available.
107-
108-
Candidate prompts are validated on the held-out validation set before any source
109-
prompt is updated. The configurable gate checks minimum validation improvement,
110-
new hard failures, critical-case regressions, and cost budget. It also includes
111-
an explicit overfitting guard: when training improves but validation does not,
112-
the candidate is rejected even if some individual validation case improves. All
113-
decisions include concrete reasons, so a reviewer can see whether rejection came
114-
from score, hard-fail, critical-case, cost, or overfitting policy.
115-
116-
Auditability is handled by snapshotting config, writing candidate prompt files,
117-
preserving raw evaluator outputs, and emitting both JSON and Markdown reports.
118-
The JSON report is suitable for CI parsing, while the Markdown report summarizes
119-
score deltas, per-case outcomes, failure clusters, and the final accept/reject
120-
decision for humans. Fake mode makes this loop reproducible without API keys;
121-
real mode can swap in `AgentOptimizer` while keeping the same validation, gate,
122-
and report machinery. The source prompt is never overwritten by this example;
123-
reviewers can inspect `candidate_prompts/` and decide whether to copy or submit
124-
the change through a normal PR. Hidden samples can reuse the same gate because
125-
all decisions are based on held-out validation deltas rather than train-set
126-
scores alone. Trace-style replay can also be added by replacing `call_agent`
127-
with eval cases that provide `actual_conversation`, while the attribution, delta,
128-
gate, and report stages remain unchanged.
233+
本示例把 `AgentEvaluator.get_executer(...).get_result()` 作为唯一评测事实来源,分别对训练集和验证集生成逐 case 记录,包含输入、期望输出、实际输出、metric 分数、pass/fail、失败类型和原因。失败归因采用可复现规则:JSON 解析失败或缺字段归为格式不符合要求,category 不一致归为知识召回不足,priority 不一致归为参数错误,action 不一致归为最终回复不匹配;对必须依赖真实工具轨迹或 LLM rubric judge 的类别,fake mode 通过 `case_meta.json``fake_attribution_hints` 标记 `tool_call_error``llm_rubric_not_met`,并在 self-check 中区分 hint-assisted 与 rule-only 准确率,避免把公开标签直接当成黑盒判断结果。
234+
235+
候选 prompt 不会直接写回源文件,而是先在验证集回归,并和 baseline 做逐 case delta,标记新增通过、新增失败、分数提升、分数下降和无变化。gate 由 `optimizer.json` 配置,检查验证集总分提升阈值、是否新增 hard fail、关键 case 是否退化以及成本预算;额外防过拟合规则是训练集提升但验证集没有提升时拒绝候选。这样即使优化器记住训练失败样例,只要验证集没有真实收益,或者牺牲了关键 case,也不会进入可接受状态。
236+
237+
审计产物包括配置快照、随机种子、候选 prompt 文件、`optimization_rounds`、原始 evaluator 输出、JSON 报告和 Markdown 报告,便于完整复现实验配置,也便于后续 CI 读取结构化字段。`prompt_audit` 记录源路径、baseline/candidate 哈希、字符数和是否变更,reviewer 可以把分数变化追溯到具体 prompt。trace mode 会生成 `eval_mode: "trace"` 数据并回放 `actual_conversation`,保证无 API key 环境也能复现 baseline、归因、优化、验证、gate 和审计闭环。

examples/optimization/eval_optimize_loop/agent/agent.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
from pathlib import Path
1919

2020

21-
SYSTEM_PROMPT_PATH = Path(__file__).parent / "prompts" / "system.md"
21+
PROMPT_DIR = Path(__file__).parent / "prompts"
22+
ROUTER_PROMPT_PATH = PROMPT_DIR / "router.md"
23+
SYSTEM_PROMPT_PATH = PROMPT_DIR / "system.md"
24+
SKILL_PROMPT_PATH = PROMPT_DIR / "skill.md"
2225

2326
_SPACE_RE = re.compile(r"\s+")
2427

@@ -36,8 +39,15 @@ def normalize_json_text(raw: str) -> str:
3639
return _compact_json(parsed)
3740

3841

42+
def _read_prompt_text() -> str:
43+
return "\n\n".join(
44+
path.read_text(encoding="utf-8")
45+
for path in (ROUTER_PROMPT_PATH, SYSTEM_PROMPT_PATH, SKILL_PROMPT_PATH)
46+
).lower()
47+
48+
3949
def _prompt_flags() -> dict[str, bool]:
40-
prompt = SYSTEM_PROMPT_PATH.read_text(encoding="utf-8").lower()
50+
prompt = _read_prompt_text()
4151
return {
4252
"refund_rule": "treat double charge" in prompt or "vip refund requests" in prompt,
4353
"keep_outage_p1": "p1 for urgent production outages" in prompt,
@@ -106,6 +116,19 @@ def answer_for_query(query: str) -> str:
106116
"action": "answer",
107117
})
108118

119+
if "policy citation" in text or "pol-77" in text:
120+
return _compact_json({
121+
"category": "billing",
122+
"action": "answer",
123+
})
124+
125+
if "guaranteed instant fix" in text or "repeated invoice confusion" in text:
126+
return _compact_json({
127+
"category": "billing",
128+
"priority": "p3",
129+
"action": "answer",
130+
})
131+
109132
if "mobile app crashes" in text:
110133
return _compact_json({
111134
"category": "technical",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Route each support ticket to one primary handling path before answering.
2+
3+
Baseline routing rules:
4+
- Account access and profile questions go to account handling.
5+
- Billing and pricing questions go to billing handling.
6+
- Production incidents and software failures go to technical handling.
7+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Apply these triage skills after routing:
2+
3+
- Use p1 only when the ticket describes an urgent production outage.
4+
- Use p2 for normal user-impacting issues.
5+
- Use p3 for low-risk informational requests.
6+
- Use answer for informational account or pricing questions.
7+
- Use escalate for production incidents.
8+
- Use troubleshooting for reproducible technical failures.
9+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"train_refund_double_charge": {
3+
"expected_failure_types": ["final_response_mismatch", "knowledge_recall_insufficient"],
4+
"note": "Baseline routes refund billing work to account/answer instead of billing/refund_review."
5+
},
6+
"train_legacy_sync": {
7+
"expected_failure_types": ["final_response_mismatch"],
8+
"note": "Baseline returns troubleshooting while the reference expects escalation."
9+
},
10+
"train_policy_tool_missing": {
11+
"expected_failure_types": ["tool_call_error", "format_violation"],
12+
"fake_attribution_hints": ["tool_call_error"],
13+
"note": "The no-key fake agent cannot perform the expected policy lookup and returns an incomplete JSON object."
14+
},
15+
"val_vip_refund": {
16+
"expected_failure_types": ["final_response_mismatch", "knowledge_recall_insufficient", "parameter_error"],
17+
"note": "Baseline misses billing/refund_review and p1 priority for VIP refund."
18+
},
19+
"val_rubric_tone_fail": {
20+
"expected_failure_types": ["llm_rubric_not_met", "parameter_error"],
21+
"fake_attribution_hints": ["llm_rubric_not_met"],
22+
"note": "The answer uses a lower urgency than the reference and is deterministically labeled as a rubric failure for the no-key fake judge path."
23+
}
24+
}

0 commit comments

Comments
 (0)