Skip to content

Commit 2809e0c

Browse files
committed
docs(examples): clarify counterfactual attribution boundary
1 parent 19437b3 commit 2809e0c

8 files changed

Lines changed: 43 additions & 33 deletions

File tree

examples/optimization/counterfactual_trace_loop/DESIGN.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
## 技术附录
1010

11+
### 与现有方案的边界
12+
13+
PR #159 主要依据失败文本和预期/实际轨迹的静态差异归因;PR #161 重点在优化候选回放、bootstrap 与 Pareto 决策。本方案不把静态差异直接视为原因,而是把它转化为局部反事实干预,再由同一个 `AgentEvaluator` 重评。只有可重复观察到 metric 修复的干预才形成归因证据,且该机制同时用于 baseline 失败和 candidate 新增退化。
14+
1115
- 反事实归因:结论来自真实 metric delta,不依赖 case ID、failure reason 或人工标签。
1216
- Prompt actionability:只有 agent behavior failure 能够选择优化表面。
1317
- Gate:关键检查必须全部通过,证据不足时以 `NEEDS_REVIEW` 拒绝。

examples/optimization/counterfactual_trace_loop/README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@
22

33
This example closes evaluation and prompt optimization with evidence from trace interventions. Unlike loops that classify a failure from reason text or sample metadata, it deep-copies the actual trace, changes one execution surface, and sends the counterfactual `EvalCase` through the same public `AgentEvaluator` metrics. Evaluation-data, evaluator, and infrastructure failures are excluded from prompt optimization.
44

5+
## Distinction from other Issue #91 proposals
6+
7+
PR #159 uses rule-first attribution from failure text and static expected/actual trace differences. PR #161 replays optimizer candidates and combines static trace/rubric attribution with bootstrap and Pareto selection. This example addresses a different question: which minimal trace intervention causally repairs the failing metric when the same `AgentEvaluator` evaluates it again?
8+
9+
The attribution evidence is therefore an observed before/after metric delta from `replace_final_response`, `replace_tool_name`, `replace_tool_arguments`, or a bounded combination. Static trace differences can propose an intervention, but cannot by themselves establish the diagnosis. The same mechanism is reused after candidate validation to locate regressions. No case ID, expected failure label, attribution hint, or hand-authored metric score participates in the decision.
10+
511
## Quick start
612

713
```bash
8-
python examples/optimization/eval_optimize_loop/run_counterfactual_probe.py
9-
python examples/optimization/eval_optimize_loop/run_pipeline.py --mode fake
10-
python examples/optimization/eval_optimize_loop/run_pipeline.py --mode trace
11-
python examples/optimization/eval_optimize_loop/run_pipeline.py --mode fake --candidate-profile accepted
12-
python examples/optimization/eval_optimize_loop/run_pipeline.py --mode fake --candidate-profile ineffective
14+
python examples/optimization/counterfactual_trace_loop/run_counterfactual_probe.py
15+
python examples/optimization/counterfactual_trace_loop/run_pipeline.py --mode fake
16+
python examples/optimization/counterfactual_trace_loop/run_pipeline.py --mode trace
17+
python examples/optimization/counterfactual_trace_loop/run_pipeline.py --mode fake --candidate-profile accepted
18+
python examples/optimization/counterfactual_trace_loop/run_pipeline.py --mode fake --candidate-profile ineffective
1319
```
1420

1521
Both fake and trace modes need no API key. Trace mode replays `actual_conversation`; `conversation` remains the expected trace. The fake optimizer is prompt-sensitive and introduces an intentionally broad billing rule without reading case IDs.

examples/optimization/counterfactual_trace_loop/pipeline/pipeline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ async def run_pipeline(
366366
"prompt_hashes": {k: _sha(v) for k, v in prompt_paths.items()},
367367
"write_back": write_back,
368368
"reproduction_command": (
369-
"python examples/optimization/eval_optimize_loop/run_pipeline.py "
369+
"python examples/optimization/counterfactual_trace_loop/run_pipeline.py "
370370
f"--mode {mode} --candidate-profile {candidate_profile}"
371371
),
372372
},

examples/optimization/counterfactual_trace_loop/run_counterfactual_probe.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
if str(REPO_ROOT) not in sys.path:
1313
sys.path.insert(0, str(REPO_ROOT))
1414

15-
from examples.optimization.eval_optimize_loop.pipeline.probe import ( # noqa: E402
15+
from examples.optimization.counterfactual_trace_loop.pipeline.probe import ( # noqa: E402
1616
run_counterfactual_probe,
1717
)
1818

examples/optimization/counterfactual_trace_loop/run_pipeline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
ROOT = HERE.parents[2]
1212
if str(ROOT) not in sys.path:
1313
sys.path.insert(0, str(ROOT))
14-
from examples.optimization.eval_optimize_loop.pipeline.pipeline import run_pipeline # noqa: E402
14+
from examples.optimization.counterfactual_trace_loop.pipeline.pipeline import run_pipeline # noqa: E402
1515

1616

1717
def main():

examples/optimization/counterfactual_trace_loop/sample_output/optimization_report.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -947,7 +947,7 @@
947947
{
948948
"name": "latency_budget",
949949
"passed": true,
950-
"observed": 0.29256009999880916,
950+
"observed": 0.3700505000015255,
951951
"threshold": 180,
952952
"reason": "passed"
953953
},
@@ -970,7 +970,7 @@
970970
"audit": {
971971
"seed": 42,
972972
"mode": "trace",
973-
"duration_seconds": 0.29256009999880916,
973+
"duration_seconds": 0.3700505000015255,
974974
"cost": {
975975
"total": 0.0
976976
},
@@ -997,7 +997,7 @@
997997
"system_prompt": "4e1c6a4ef268a492679ad6888230217d874733817e27137644f3364038642be9"
998998
}
999999
},
1000-
"reproduction_command": "python examples/optimization/eval_optimize_loop/run_pipeline.py --mode trace --candidate-profile overfit"
1000+
"reproduction_command": "python examples/optimization/counterfactual_trace_loop/run_pipeline.py --mode trace --candidate-profile overfit"
10011001
},
10021002
"known_limitations": [
10031003
"A local trace edit can be structurally valid but semantically incoherent with an original tool response.",

tests/evaluation/test_counterfactual_trace_probe.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
from trpc_agent_sdk.types import FunctionResponse
1616

17-
from examples.optimization.eval_optimize_loop.pipeline.interventions import (
17+
from examples.optimization.counterfactual_trace_loop.pipeline.interventions import (
1818
InterventionKind,
1919
build_counterfactual,
2020
)
21-
from examples.optimization.eval_optimize_loop.pipeline.probe import (
21+
from examples.optimization.counterfactual_trace_loop.pipeline.probe import (
2222
build_probe_cases,
2323
evaluate_trace_cases,
2424
run_counterfactual_probe,

tests/evaluation/test_eval_optimize_loop.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,24 @@
88

99
import pytest
1010

11-
from examples.optimization.eval_optimize_loop.pipeline.diagnosis import (
11+
from examples.optimization.counterfactual_trace_loop.pipeline.diagnosis import (
1212
InfrastructureFailure,
1313
attribute_from_evidence,
1414
build_failure_digest,
1515
classify_non_agent_failure,
1616
select_target_prompts,
1717
)
18-
from examples.optimization.eval_optimize_loop.pipeline.gate import evaluate_gate
19-
from examples.optimization.eval_optimize_loop.pipeline.input_audit import audit_eval_cases
20-
from examples.optimization.eval_optimize_loop.pipeline.models import CounterfactualEvidence
21-
from examples.optimization.eval_optimize_loop.fake.model import generate_trace
22-
from examples.optimization.eval_optimize_loop.pipeline.optimizer import (
18+
from examples.optimization.counterfactual_trace_loop.pipeline.gate import evaluate_gate
19+
from examples.optimization.counterfactual_trace_loop.pipeline.input_audit import audit_eval_cases
20+
from examples.optimization.counterfactual_trace_loop.pipeline.models import CounterfactualEvidence
21+
from examples.optimization.counterfactual_trace_loop.fake.model import generate_trace
22+
from examples.optimization.counterfactual_trace_loop.pipeline.optimizer import (
2323
apply_if_accepted,
2424
run_real_optimizer,
2525
)
26-
from examples.optimization.eval_optimize_loop.pipeline.pipeline import run_pipeline
27-
from examples.optimization.eval_optimize_loop.pipeline.pipeline import _evaluate_with_triage
28-
from examples.optimization.eval_optimize_loop.pipeline.probe import build_probe_cases
26+
from examples.optimization.counterfactual_trace_loop.pipeline.pipeline import run_pipeline
27+
from examples.optimization.counterfactual_trace_loop.pipeline.pipeline import _evaluate_with_triage
28+
from examples.optimization.counterfactual_trace_loop.pipeline.probe import build_probe_cases
2929

3030

3131
def _evidence(name: str, passed: bool, repaired: list[str]) -> CounterfactualEvidence:
@@ -289,7 +289,7 @@ async def test_fake_and_trace_pipeline_have_same_schema_without_api_key(tmp_path
289289
for key in list(__import__("os").environ):
290290
if "API_KEY" in key:
291291
monkeypatch.delenv(key, raising=False)
292-
base = Path(__file__).parents[2] / "examples" / "optimization" / "eval_optimize_loop"
292+
base = Path(__file__).parents[2] / "examples" / "optimization" / "counterfactual_trace_loop"
293293
fake = await run_pipeline(base, "fake", tmp_path / "fake")
294294
trace = await run_pipeline(base, "trace", tmp_path / "trace")
295295
assert fake.keys() == trace.keys()
@@ -302,7 +302,7 @@ async def test_fake_and_trace_pipeline_have_same_schema_without_api_key(tmp_path
302302

303303
@pytest.mark.asyncio
304304
async def test_real_evaluations_drive_accept_overfit_and_ineffective_decisions(tmp_path):
305-
base = Path(__file__).parents[2] / "examples" / "optimization" / "eval_optimize_loop"
305+
base = Path(__file__).parents[2] / "examples" / "optimization" / "counterfactual_trace_loop"
306306
accepted = await run_pipeline(base, "fake", tmp_path / "accepted", candidate_profile="accepted")
307307
overfit = await run_pipeline(base, "fake", tmp_path / "overfit", candidate_profile="overfit")
308308
ineffective = await run_pipeline(base, "fake", tmp_path / "ineffective", candidate_profile="ineffective")
@@ -317,7 +317,7 @@ async def test_real_evaluations_drive_accept_overfit_and_ineffective_decisions(t
317317

318318
@pytest.mark.asyncio
319319
async def test_real_optimizer_receives_filtered_train_and_never_updates_source(tmp_path, monkeypatch):
320-
base = Path(__file__).parents[2] / "examples" / "optimization" / "eval_optimize_loop"
320+
base = Path(__file__).parents[2] / "examples" / "optimization" / "counterfactual_trace_loop"
321321
captured = {}
322322

323323
async def spy(**kwargs):
@@ -347,7 +347,7 @@ async def spy(**kwargs):
347347
)
348348

349349
monkeypatch.setattr(
350-
"examples.optimization.eval_optimize_loop.pipeline.optimizer.AgentOptimizer.optimize",
350+
"examples.optimization.counterfactual_trace_loop.pipeline.optimizer.AgentOptimizer.optimize",
351351
spy,
352352
)
353353
result = await run_real_optimizer(
@@ -385,7 +385,7 @@ async def spy(**kwargs):
385385

386386
@pytest.mark.asyncio
387387
async def test_real_optimizer_restores_prompt_if_optimizer_mutates_then_fails(tmp_path, monkeypatch):
388-
base = Path(__file__).parents[2] / "examples" / "optimization" / "eval_optimize_loop"
388+
base = Path(__file__).parents[2] / "examples" / "optimization" / "counterfactual_trace_loop"
389389
prompt = tmp_path / "router.md"
390390
prompt.write_text("baseline", encoding="utf-8")
391391

@@ -394,7 +394,7 @@ async def broken_optimizer(**kwargs):
394394
raise RuntimeError("optimizer failed")
395395

396396
monkeypatch.setattr(
397-
"examples.optimization.eval_optimize_loop.pipeline.optimizer.AgentOptimizer.optimize",
397+
"examples.optimization.counterfactual_trace_loop.pipeline.optimizer.AgentOptimizer.optimize",
398398
broken_optimizer,
399399
)
400400

@@ -414,15 +414,15 @@ async def broken_optimizer(**kwargs):
414414

415415
@pytest.mark.asyncio
416416
async def test_pipeline_rejects_unsupported_real_mode(tmp_path):
417-
base = Path(__file__).parents[2] / "examples" / "optimization" / "eval_optimize_loop"
417+
base = Path(__file__).parents[2] / "examples" / "optimization" / "counterfactual_trace_loop"
418418

419419
with pytest.raises(ValueError, match="fake or trace"):
420420
await run_pipeline(base, "real", tmp_path)
421421

422422

423423
@pytest.mark.asyncio
424424
async def test_report_contains_trace_status_evidence_and_limitations(tmp_path):
425-
base = Path(__file__).parents[2] / "examples" / "optimization" / "eval_optimize_loop"
425+
base = Path(__file__).parents[2] / "examples" / "optimization" / "counterfactual_trace_loop"
426426
report = await run_pipeline(base, "trace", tmp_path)
427427
case = report["baseline"]["train"]["case_results"][0]
428428
assert {"case_id", "passed", "metrics", "failure_reason", "trace_summary"} <= case.keys()
@@ -435,7 +435,7 @@ async def test_report_contains_trace_status_evidence_and_limitations(tmp_path):
435435

436436
@pytest.mark.asyncio
437437
async def test_fake_round_audit_contains_candidate_prompts_and_evaluation(tmp_path):
438-
base = Path(__file__).parents[2] / "examples" / "optimization" / "eval_optimize_loop"
438+
base = Path(__file__).parents[2] / "examples" / "optimization" / "counterfactual_trace_loop"
439439

440440
report = await run_pipeline(base, "fake", tmp_path, candidate_profile="accepted")
441441

@@ -450,7 +450,7 @@ async def test_fake_round_audit_contains_candidate_prompts_and_evaluation(tmp_pa
450450

451451

452452
def test_committed_sample_output_has_current_report_contract():
453-
sample = Path(__file__).parents[2] / "examples" / "optimization" / "eval_optimize_loop" / "sample_output"
453+
sample = Path(__file__).parents[2] / "examples" / "optimization" / "counterfactual_trace_loop" / "sample_output"
454454
report = json.loads((sample / "optimization_report.json").read_text(encoding="utf-8"))
455455
assert report["schema_version"] == "1.0"
456456
assert report["baseline"]["train"]["case_results"]
@@ -471,7 +471,7 @@ async def evaluator(case_batch, workspace):
471471
return {case.eval_id: {"tool_trajectory_avg_score": 1.0}}
472472

473473
monkeypatch.setattr(
474-
"examples.optimization.eval_optimize_loop.pipeline.pipeline.evaluate_trace_cases",
474+
"examples.optimization.counterfactual_trace_loop.pipeline.pipeline.evaluate_trace_cases",
475475
evaluator,
476476
)
477477
metrics, failures = await _evaluate_with_triage(cases, tmp_path)

0 commit comments

Comments
 (0)