Skip to content

Commit c681780

Browse files
committed
fix(eval_optimize_loop): address adversarial review findings
Critical fixes: - _build_split_result: skip empty case_results (all([])=True phantom pass) - overfitting detection: require val_pass_rate_delta < 0 (not <= 0) - total_cases: count only evaluable cases, not dict key count - _EvaluationCasesFailed fallback: warn on ImportError before using AssertionError Important fixes: - gate.py: protected_case_ids check both train and val score deltas - reporting.py: metric breakdown title shows correct split label - run_pipeline.py: use public output_dir property instead of private _config - pipeline.py: expose output_dir property, _collect_cost method Other: - failure_attribution.py: document format patterns as heuristic defaults
1 parent 8f08b40 commit c681780

7 files changed

Lines changed: 46 additions & 16 deletions

File tree

examples/optimization/eval_optimize_loop/failure_attribution.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
"llm_rubric_knowledge_recall": "knowledge_recall_insufficient",
1717
}
1818

19+
# Default format-violation patterns. These are heuristics tuned for the
20+
# sample evalsets. Real pipelines should make format patterns configurable
21+
# (e.g. via pipeline.json) to match their expected output format.
1922
_FORMAT_PATTERNS: list[tuple[str, re.Pattern]] = [
2023
("format_violation", re.compile(r"答案:")),
2124
]
@@ -45,8 +48,8 @@ def _extract_expected_text(case_results: list[EvalCaseResult]) -> Optional[str]:
4548

4649
def attribute_failures(eval_results: dict[str, list[EvalCaseResult]]) -> FailureAttribution:
4750
categories: dict[str, FailureCategory] = {}
48-
total_cases = len(eval_results)
4951
failed_cases = 0
52+
evaluable_cases = sum(1 for v in eval_results.values() if v)
5053

5154
for case_id, case_results in eval_results.items():
5255
last_result = case_results[-1] if case_results else None
@@ -84,7 +87,7 @@ def attribute_failures(eval_results: dict[str, list[EvalCaseResult]]) -> Failure
8487
categories[sub_cat].case_ids.append(case_id)
8588

8689
return FailureAttribution(
87-
total_cases=total_cases,
90+
total_cases=evaluable_cases,
8891
failed_cases=failed_cases,
8992
categories=categories,
9093
)

examples/optimization/eval_optimize_loop/gate.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def apply_gate(
1414
overfitting_warning = False
1515

1616
# Rule 1: Overfitting check (always on, warning only)
17-
if delta.train_pass_rate_delta > 0 and delta.val_pass_rate_delta <= 0:
17+
if delta.train_pass_rate_delta > 0 and delta.val_pass_rate_delta < 0:
1818
overfitting_warning = True
1919
reasons.append(
2020
f"overfitting_warning: train pass_rate improved by {delta.train_pass_rate_delta:.4f} "
@@ -38,11 +38,13 @@ def apply_gate(
3838
else:
3939
reasons.append(f"new_fails check passed: allow_new_fails={gate_config.allow_new_fails}, newly_failing={len(delta.val.newly_failing)}")
4040

41-
# Rule 4: protected_cases
41+
# Rule 4: protected_cases (checks both train and val)
4242
degraded_protected: list[str] = []
4343
for case_id in gate_config.protected_case_ids:
44-
case_scores = delta.val.score_deltas.get(case_id, {})
45-
if case_scores and any(v < 0 for v in case_scores.values()):
44+
train_scores = delta.train.score_deltas.get(case_id, {})
45+
val_scores = delta.val.score_deltas.get(case_id, {})
46+
all_scores = {**train_scores, **val_scores}
47+
if all_scores and any(v < 0 for v in all_scores.values()):
4648
degraded_protected.append(case_id)
4749
if degraded_protected:
4850
reject_reasons.append(f"protected cases degraded: {degraded_protected}")

examples/optimization/eval_optimize_loop/pipeline.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
try:
2121
from trpc_agent_sdk.evaluation._agent_evaluator import _EvaluationCasesFailed
2222
except ImportError:
23+
import warnings
24+
warnings.warn(
25+
"Could not import _EvaluationCasesFailed; falling back to AssertionError. "
26+
"This may cause the pipeline to silently swallow unrelated assertions. "
27+
"Update trpc_agent_sdk to the latest version.",
28+
RuntimeWarning,
29+
)
2330
_EvaluationCasesFailed = AssertionError
2431

2532
from .delta import compute_delta
@@ -163,12 +170,9 @@ async def _run_live() -> tuple:
163170
delta = compute_delta(baseline_split, candidate_split)
164171

165172
duration = time.monotonic() - t0
166-
# Note: cost_usd is always 0.0 here because the pipeline does not
167-
# yet collect per-round LLM costs from the optimizer. The gate's
168-
# max_cost_usd rule will therefore not reject on cost grounds.
169-
# Users should monitor costs via their LLM provider dashboard.
173+
cost_usd = self._collect_cost()
170174
gate = apply_gate(
171-
delta, self._config.gate, cost_usd=0.0, duration_seconds=duration
175+
delta, self._config.gate, cost_usd=cost_usd, duration_seconds=duration
172176
)
173177

174178
finished_at = datetime.now(timezone.utc).isoformat()
@@ -313,6 +317,8 @@ def _build_split_result(self, eval_result: EvaluateResult) -> SplitResult:
313317
passed_count = 0
314318

315319
for case_id, case_results in cases_by_id.items():
320+
if not case_results:
321+
continue
316322
all_passed = all(
317323
cr.final_eval_status == EvalStatus.PASSED and not cr.error_message
318324
for cr in case_results
@@ -354,3 +360,13 @@ def _build_split_result(self, eval_result: EvaluateResult) -> SplitResult:
354360
metric_breakdown=metric_breakdown,
355361
per_case=per_case,
356362
)
363+
364+
@property
365+
def output_dir(self) -> str:
366+
return self._config.output_dir
367+
368+
def _collect_cost(self) -> float:
369+
# Cost tracking from AgentOptimizer is not yet implemented.
370+
# The gate's max_cost_usd rule is therefore dormant — users
371+
# should monitor costs via their LLM provider dashboard.
372+
return 0.0

examples/optimization/eval_optimize_loop/reporting.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ def _build_markdown(result: PipelineResult) -> list[str]:
6060
if result.baseline:
6161
base_ref = result.baseline.get("val") or result.baseline.get("train")
6262
cand_ref = result.candidate.get("val") or result.candidate.get("train")
63+
split_label = "val" if result.baseline.get("val") else "train"
6364
if base_ref and base_ref.metric_breakdown:
64-
lines.append("### Metric Breakdown (val)")
65+
lines.append(f"### Metric Breakdown ({split_label})")
6566
lines.append("")
6667
lines.append("| Metric | Baseline | Candidate | Delta |")
6768
lines.append("|---|---|---|---|")

examples/optimization/eval_optimize_loop/run_pipeline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async def main() -> None:
3838
pipeline = EvalOptimizePipeline.from_config(args.pipeline_config)
3939
result = await pipeline.run()
4040

41-
output_dir = Path(pipeline._config.output_dir).resolve()
41+
output_dir = Path(pipeline.output_dir).resolve()
4242
print(f"\nPipeline complete: {result.gate_decision}")
4343
print(f" Duration: {result.duration_seconds:.2f}s")
4444
print(f" Reports: {output_dir}/")

examples/optimization/eval_optimize_loop/tests/test_failure_attribution.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def test_attribute_failures_empty_case_results():
143143
],
144144
}
145145
attr = attribute_failures(results)
146-
assert attr.total_cases == 2
146+
assert attr.total_cases == 1
147147
assert attr.failed_cases == 1
148148
assert "final_response_mismatch" in attr.categories
149149
assert "case_b" in attr.categories["final_response_mismatch"].case_ids

examples/optimization/eval_optimize_loop/tests/test_gate.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,22 @@ def test_gate_reject_duration_over_budget():
8989

9090

9191
def test_gate_overfitting_warning():
92-
"""Train improves but val does not → overfitting_warning=True."""
93-
delta = _make_delta(train_pass_rate_delta=0.5, val_pass_rate_delta=0.0)
92+
"""Train improves but val degrades → overfitting_warning=True."""
93+
delta = _make_delta(train_pass_rate_delta=0.5, val_pass_rate_delta=-0.1)
9494
gate = GateConfig(min_improvement=0.0, allow_new_fails=False)
9595
decision = apply_gate(delta, gate, cost_usd=0.0, duration_seconds=10.0)
9696
assert decision.overfitting_warning is True
9797
assert any("overfitting" in r.lower() for r in decision.reasons)
9898

9999

100+
def test_gate_no_overfitting_when_val_flat():
101+
"""Train improves but val is flat (0.0) -> no overfitting warning."""
102+
delta = _make_delta(train_pass_rate_delta=0.5, val_pass_rate_delta=0.0)
103+
gate = GateConfig(min_improvement=0.0, allow_new_fails=False)
104+
decision = apply_gate(delta, gate, cost_usd=0.0, duration_seconds=10.0)
105+
assert decision.overfitting_warning is False
106+
107+
100108
def test_gate_reject_overfitting_with_degradation():
101109
"""Train improves but val degrades → overfitting_warning + REJECT (below min_improvement)."""
102110
delta = _make_delta(train_pass_rate_delta=0.5, val_pass_rate_delta=-0.2)

0 commit comments

Comments
 (0)