Skip to content

Commit 092fb40

Browse files
committed
docs(examples): finalize issue 91 closed loop
1 parent eaed59f commit 092fb40

30 files changed

Lines changed: 1739 additions & 1711 deletions
Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
# 设计说明
22

3-
## 失败归因
4-
评测先由 FakeJudge 产出结构化失败,再用规则归因:JSON、禁用格式归为 `format_violation`,精确答案错归为 `final_response_mismatch`,工具名、参数、知识召回、长度和 rubric 各有独立类别。每个失败都保留 reason 与 evidence,便于复核。若样例声明期望类别,报告会计算归因准确率。
3+
本示例把基线评测、失败归因、提示词优化、候选复评、接受门禁和审计落盘串成同一条可复现流水线。训练集与验证集采用 SDK 标准 EvalSet;FakeModel 只读取提示词和用户输入,不能接触期望答案、标签、受保护标记或样例编号,真值仅供评测器、归因器和门禁使用,从源头避免答案泄漏。
54

6-
## 接受门禁
7-
Gate 要求验证集提升达到阈值,并检查新硬失败、受保护样例退化、单例降分和累计成本。若训练分上涨但验证不涨,直接标记过拟合并拒绝。
5+
失败结果按格式、最终回复、工具调用、参数、知识召回和规则评分等类别聚合,每个失败样例保留原因与证据。训练集只用于暴露问题,所有去重后的候选都必须重新执行完整训练集和验证集评测,再逐样例计算新增通过、新增失败、升分和降分。统一门禁检查验证集增益、过拟合、新增硬失败、受保护样例退化、单例降分和累计成本;总成本不完整时,任何数值预算都以 `cost_unavailable` 拒绝。
86

9-
## 防过拟合
10-
训练集只用于暴露问题,候选必须通过验证集和 protected case。过拟合候选即使修好训练格式,只要把验证集自然语言或受保护精确答案改坏,也会被拒绝。`delta_type` 标出 new_pass、new_fail、score_up、score_down,避免只看平均分掩盖局部退化。
7+
SDK 优化始终使用 `update_source=False`,并记录真实逐样例指标、失败证据及轨迹是否可用。只有候选通过完整门禁、输入完整性检查且预写审计成功后,`--update-source` 才由共享流水线事务性回写所选提示词;失败时保留原始字节或明确报告恢复错误。
118

12-
## Fake 与 SDK
13-
Fake mode 由 expectation、tags、protected 和 simulated_outputs 驱动,不依赖样例 id,可无 API key 稳定复现。SDK mode 通过 `SDKBackend` 调用 `AgentOptimizer``TargetPrompt`,失败时给出明确错误,不回退到 fake。
14-
15-
## 审计与回写
16-
报告同时写 JSON、Markdown 和 `runs/<run_id>/`,保存输入哈希、配置快照、候选 prompt、diff、case 结果与成本,并给出可复现命令。默认不回写源 prompt,只有显式 `--update-source` 才允许,报告会记录该选择。
9+
运行产物先写入 `runs/.<run_id>.tmp/`,包含基线与候选提示词、每轮记录、全部分割评测结果、逐样例差异、门禁、成本和回写日志。发布时采用不可覆盖的原子目录重命名:Windows 使用 MoveFileExW,Linux 使用 renameat2;其他缺少安全原语的平台直接失败。最终目录不可变,清单记录每个文件的 SHA-256 与大小,顶层报告仅是发布后的便利副本。

examples/optimization/eval_optimize_loop/README.md

Lines changed: 77 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ loader.py / config.py --> validated cases, gate config, input hashes
1717
v
1818
backends.py
1919
|-- FakeBackend -> FakeModel + FakeJudge + FakeOptimizer
20-
`-- SDKBackend -> AgentOptimizer + TargetPrompt + user call_agent
20+
`-- SDKBackend -> AgentOptimizer + TargetPrompt + user call_agent + AgentEvaluator
2121
|
2222
v
2323
attribution.py -> evaluator.py -> gate.py -> report.py
@@ -87,47 +87,59 @@ python examples/optimization/eval_optimize_loop/run_pipeline.py \
8787
needed by that callable. SDK mode never silently falls back to fake mode. The
8888
generated reproducibility command records the actual `--sdk-call-agent`
8989
`module:function` target and file/config paths, but it does not record API keys
90-
or other provider secrets.
90+
or other provider secrets. Persisted optimizer config snapshots recursively
91+
replace credential values with `<redacted>`.
92+
93+
The checked-in `train.evalset.json` and `val.evalset.json` use the SDK
94+
`EvalSet` schema (`eval_set_id` / `eval_cases`). Fake mode reads its
95+
deterministic judge metadata from each case's `session_input.state`
96+
(`eval_optimize_expectation`, `eval_optimize_tags`, and
97+
`eval_optimize_protected`).
9198

9299
SDK optimizer config and wrapper gate config are intentionally separate.
93100
`--optimizer-config` is passed unchanged to `AgentOptimizer.optimize`, so it must
94101
follow the SDK `OptimizeConfigFile` schema. Put wrapper-only gate settings in
95102
`--gate-config` (for example `{"gate": {"min_val_score_improvement": 0.05,
96-
"max_total_cost": 1.0}}`). If `--gate-config` is omitted, the wrapper uses the
97-
same default aggregate gate values as the fake example.
103+
"allow_new_hard_fail": false, "max_score_drop_per_case": 0.0,
104+
"max_total_cost": null}}`). If `--gate-config` is omitted, the wrapper uses the
105+
same default gate values as fake mode and auto-adds validation cases marked with
106+
`eval_optimize_protected`. A numeric `max_total_cost` fails closed with
107+
`cost_unavailable` whenever the backend cannot prove a complete total. Use
108+
`null` only when you intentionally want to run the quality gates without a cost
109+
limit; `reported_optimizer_cost` remains an incomplete lower bound, not a total.
98110

99111
`--target-prompt name=path` may be repeated. If omitted, SDK mode keeps the old
100112
single-field behavior and registers `system_prompt=--prompt`. A run can optimize
101113
only `router_prompt`, only `skill_prompt`, or any set of named fields as long as
102114
`OptimizeResult.best_prompts` returns every registered field.
103115

104-
Fake mode is the complete per-case closed loop. SDK mode is the real
105-
`AgentOptimizer` / `TargetPrompt` path with an aggregate wrapper gate. When the
106-
SDK optimizer returns a best prompt, this wrapper maps `OptimizeResult`
107-
aggregate fields into the JSON/Markdown report: baseline/best pass rate,
108-
pass-rate improvement, metric breakdowns, token usage, duration, LLM cost,
109-
all `best_prompts`, and round summaries. SDK mode applies
110-
`gate_status: partial_applied`: it checks `status == SUCCEEDED`, validation
111-
improvement against `gate.min_val_score_improvement`, and total LLM cost against
112-
`gate.max_total_cost`. Protected-case regression, new-hard-failure, per-case
113-
delta, and per-case score-drop checks are not claimed in SDK mode unless the SDK
114-
exposes full per-case validation scores; they are listed in
115-
`not_applied_checks`.
116-
117-
Fake mode uses a deterministic run id (`eval_optimize_loop_seed_<seed>`) so the
118-
example outputs are byte-stable. SDK mode is append-only by default: the wrapper
119-
derives a compact UTC `run.run_id` from the SDK result `started_at` when
120-
available, otherwise from the current UTC timestamp. Pass `--run-id` only when
121-
a fixed audit path is useful for tests or local smoke runs; only explicit
122-
`--run-id` values are included in the reproducibility command.
116+
Fake mode is the deterministic offline closed loop. SDK mode is the real
117+
`AgentOptimizer` / `TargetPrompt` path followed by explicit post-optimization
118+
`AgentEvaluator` runs. The adapter deduplicates every prompt bundle found in
119+
`OptimizeResult.rounds[].candidate_prompts` and `best_prompts`. The shared
120+
pipeline temporarily installs each bundle, reruns the complete train and
121+
validation evalsets, restores the exact source bytes, then computes the same
122+
per-case deltas and complete gate decision used by fake mode. SDK case results
123+
carry real per-metric scores, failure reason/evidence, and `trace_available`;
124+
missing SDK trace data is reported as unavailable rather than fabricated.
125+
Optimizer aggregates remain audit context and are never substituted for these
126+
post-optimization case results.
127+
128+
Both modes default to a collision-resistant run id of the form
129+
`eval_optimize_loop_<mode>_<UTC timestamp>_<random suffix>`. Pass `--run-id`
130+
when a stable path is useful. Existing temporary or final run ids are never
131+
reused or overwritten. The checked-in example is generated in a fresh output
132+
directory with the explicit id `example`.
123133

124134
## Source Prompt Writes
125135

126-
The default is **no source write-back**. The baseline prompt file is not modified
127-
by fake mode, and `SDKBackend` calls `AgentOptimizer.optimize(update_source=False)`
128-
unless `--update-source` is explicitly passed. The report records
129-
`run.update_source` and the Markdown report states whether source write-back was
130-
enabled.
136+
The default is **no source write-back**. `SDKBackend` always calls
137+
`AgentOptimizer.optimize(update_source=False)` so the optimizer can never commit
138+
early. `--update-source` is handled only by the shared pipeline: after all
139+
candidate evaluations, full gate acceptance, input-integrity checks, and audit
140+
preparation, it transactionally writes the selected prompt bundle. This rule is
141+
the same in fake and SDK modes. The report records the requested action and the
142+
terminal `writeback` state.
131143

132144
## Candidate Behavior
133145

@@ -139,49 +151,57 @@ The fake optimizer proposes exactly two candidates:
139151
requested; it improves validation without protected-case regression, so the
140152
gate may accept it.
141153

142-
The fake model is driven by `EvalCase.expectation`, `tags`, `protected`, and
143-
optional `simulated_outputs`; it does not depend on sample `case_id` names.
154+
The fake model reads only the prompt and user input. It cannot inspect
155+
expectations, labels, tags, protected markers, or case ids. Those fields are
156+
available only to the fake judge, failure attribution, and acceptance gate.
144157

145158
## Reports
146159

147-
`optimization_report.json` includes:
160+
`optimization_report.json` uses `eval_optimize_loop.v2` and includes:
148161

149162
- `schema_version`;
150163
- `run` metadata: mode, fake flags, trace flag, case counts, update-source flag,
151164
and input paths;
152165
- `baseline` plus compatibility fields `baseline_train` and
153166
`baseline_validation`;
154-
- all candidate train/validation results, rationale, and prompt diff;
167+
- `baseline_prompts`, all candidate prompt bundles, complete train/validation
168+
results, rationale, and prompt diff;
169+
- optimizer `rounds`, `cost_summary`, and terminal `writeback`;
155170
- per-case deltas with `delta_type` (`new_pass`, `new_fail`, `score_up`,
156171
`score_down`, `unchanged`);
157172
- failure attribution summary and attribution accuracy when expected labels are
158173
present;
159174
- gate decisions with overfit detection, protected regressions, new hard
160-
failures, excessive drops, cost fields, and SDK `not_applied_checks` when
161-
per-case validation details are not exposed;
162-
- audit data: seed, duration, config hash, input hashes, candidate prompt hashes,
163-
cost, prompt diffs, and reproducibility command.
175+
failures, excessive drops, and cost fields in both fake and SDK modes;
176+
- audit data: seed, real duration, redacted config and raw-file hashes, input
177+
hashes, candidate artifact mapping, complete/incomplete cost semantics,
178+
prompt integrity journal, and reproducibility command.
164179

165-
`gate.max_total_cost` is interpreted as the total evaluated run cost at the time
166-
each candidate is judged: baseline cost plus all candidates evaluated so far,
167-
including rejected candidates. This makes budget decisions deterministic and
168-
auditable when multiple candidates are considered.
180+
When `CostSummary.complete` is true, `gate.max_total_cost` is interpreted as the
181+
total evaluated run cost when each candidate is judged: baseline cost plus all
182+
candidates evaluated so far, including rejected candidates. When it is false,
183+
the report separates `known_run_cost` and `reported_optimizer_cost`, and any
184+
numeric total-cost gate rejects with `cost_unavailable`.
169185

170186
`optimization_report.md` includes final decision, gate reasons, score table,
171187
per-case delta table, failure attribution summary, cost/audit details, prompt
172188
diffs, and the reproducibility command.
173189

174-
`report.py` also writes audit artifacts under `output_dir/runs/<run_id>/`:
175-
176-
- `config.snapshot.json`;
177-
- `input_hashes.json` with train, validation, optimizer, prompt,
178-
`target_prompts.<field>`, and optional `gate_config` hashes;
179-
- fake mode: `candidate_prompts/<candidate_id>/system_prompt.txt`;
180-
- SDK mode: `candidate_prompts/<candidate_id>/<field_name>.txt` for every
181-
returned `best_prompts` field, plus `bundle.txt` with the combined prompt
182-
shown in the wrapper report;
183-
- `case_results/<candidate_id>_<split>.json`;
184-
- `prompt_diffs/<candidate_id>.diff`.
190+
Audit data is first written under `output_dir/runs/.<run_id>.tmp/`. After every
191+
final artifact and prompt-integrity check succeeds, the directory is published
192+
once to `output_dir/runs/<run_id>/` with no-replace semantics. Windows uses
193+
`MoveFileExW` without a replace flag; Linux uses
194+
`renameat2(RENAME_NOREPLACE)`. A POSIX platform without that atomic primitive
195+
fails closed. Published run directories are authoritative and immutable; the
196+
top-level JSON/Markdown files are convenience copies created afterward.
197+
198+
Each run contains `pre_write_report.*`, final reports, a redacted config
199+
snapshot, input hashes, `baseline_prompts/`, every candidate prompt field and
200+
diff, baseline and candidate split results, per-round JSON, per-case deltas,
201+
gate decisions, evaluation failures, `writeback.json`, and the durable
202+
writeback journal. SDK optimizer artifacts are kept beneath `optimizer/` with a
203+
redacted config snapshot. `artifact_manifest.json` maps the logical artifacts
204+
and records the SHA-256 and byte size of every file.
185205

186206
The repository keeps only stable examples:
187207

@@ -197,9 +217,9 @@ directories are not committed.
197217
python -m pytest examples/optimization/eval_optimize_loop/tests
198218
```
199219

200-
The tests cover fake hidden-sample generalization, config validation, gate
201-
rejection paths, protected-case behavior, failure attribution, tool/knowledge
202-
judge paths, SDK adapter wiring through monkeypatching, deterministic report
203-
generation, and both CLI forms. CI can run fake mode plus the monkeypatched SDK
204-
smoke tests without real API credentials; real SDK/model calls are opt-in local
205-
or integration runs.
220+
The tests cover fake hidden-sample generalization, SDK EvalSet compatibility,
221+
config validation, gate rejection paths, protected-case behavior, failure
222+
attribution, tool/knowledge judge paths, SDK adapter and post-evaluator wiring
223+
through monkeypatching, deterministic report generation, and both CLI forms. CI
224+
can run fake mode plus the monkeypatched SDK smoke tests without real API
225+
credentials; real SDK/model calls are opt-in local or integration runs.
Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,48 @@
11
{
2-
"seed": 91,
3-
"optimizer": {
4-
"name": "fake_two_candidate_optimizer",
5-
"description": "Deterministically proposes one overfit candidate and one safe candidate."
6-
},
7-
"metrics": {
8-
"case_score": "mean",
9-
"failure_attribution": "rule_based"
10-
},
11-
"gate": {
12-
"min_val_score_improvement": 0.01,
13-
"allow_new_hard_fail": false,
14-
"protected_case_ids": [
15-
"val_protected_yes_no"
2+
"evaluate": {
3+
"metrics": [
4+
{
5+
"metric_name": "final_response_avg_score",
6+
"threshold": 1.0,
7+
"criterion": {
8+
"final_response": {
9+
"text": {
10+
"match": "exact",
11+
"case_insensitive": false
12+
}
13+
}
14+
}
15+
}
1616
],
17-
"max_score_drop_per_case": 0.0,
18-
"max_total_cost": 1.0
17+
"num_runs": 1
18+
},
19+
"optimize": {
20+
"eval_case_parallelism": 1,
21+
"stop": {
22+
"required_metrics": "all"
23+
},
24+
"algorithm": {
25+
"name": "gepa_reflective",
26+
"seed": 91,
27+
"reflection_lm": {
28+
"model_name": "${TRPC_AGENT_MODEL_NAME}",
29+
"base_url": "${TRPC_AGENT_BASE_URL}",
30+
"api_key": "${TRPC_AGENT_API_KEY}",
31+
"generation_config": {
32+
"max_tokens": 2048,
33+
"temperature": 0.4
34+
}
35+
},
36+
"candidate_selection_strategy": "pareto",
37+
"module_selector": "round_robin",
38+
"frontier_type": "instance",
39+
"reflection_minibatch_size": 3,
40+
"reflection_history_top_k": 2,
41+
"skip_perfect_score": false,
42+
"use_merge": false,
43+
"max_metric_calls": 18,
44+
"score_threshold": 1.0,
45+
"max_iterations_without_improvement": 3
46+
}
1947
}
2048
}

examples/optimization/eval_optimize_loop/eval_loop/artifacts.py

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from pathlib import Path
88
from typing import Any
99

10-
1110
ARTIFACT_COMPONENT_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
1211
WINDOWS_RESERVED_NAMES = {
1312
"CON",
@@ -23,13 +22,8 @@
2322
def validate_artifact_component(value: Any, *, context: str) -> str:
2423
"""Return a portable artifact component or fail before filesystem access."""
2524

26-
if (
27-
not isinstance(value, str)
28-
or value in {"", ".", ".."}
29-
or len(value) > MAX_ARTIFACT_COMPONENT_LENGTH
30-
or value.endswith((".", " "))
31-
or not ARTIFACT_COMPONENT_RE.fullmatch(value)
32-
):
25+
if (not isinstance(value, str) or value in {"", ".", ".."} or len(value) > MAX_ARTIFACT_COMPONENT_LENGTH
26+
or value.endswith((".", " ")) or not ARTIFACT_COMPONENT_RE.fullmatch(value)):
3327
raise ValueError(f"unsafe {context}: {value!r}")
3428
windows_stem = value.split(".", 1)[0].upper()
3529
if windows_stem in WINDOWS_RESERVED_NAMES:
@@ -57,10 +51,8 @@ def validate_distinct_file_paths(
5751
portable_key = str(resolved).casefold()
5852
previous_label = resolved_keys.get(portable_key)
5953
if previous_label is not None:
60-
raise ValueError(
61-
f"{context} must be different physical files; "
62-
f"{previous_label!r} and {label!r} collide case-insensitively"
63-
)
54+
raise ValueError(f"{context} must be different physical files; "
55+
f"{previous_label!r} and {label!r} collide case-insensitively")
6456

6557
try:
6658
metadata = path.stat()
@@ -69,16 +61,11 @@ def validate_distinct_file_paths(
6961
except OSError as error:
7062
raise ValueError(f"{context} {label!r} is unavailable: {error}") from error
7163

72-
physical_key = (
73-
(int(metadata.st_dev), int(metadata.st_ino))
74-
if metadata is not None and int(metadata.st_ino) != 0
75-
else None
76-
)
64+
physical_key = ((int(metadata.st_dev),
65+
int(metadata.st_ino)) if metadata is not None and int(metadata.st_ino) != 0 else None)
7766
if physical_key is not None and physical_key in physical_keys:
78-
raise ValueError(
79-
f"{context} must be different physical files; "
80-
f"{physical_keys[physical_key]!r} and {label!r} are aliases"
81-
)
67+
raise ValueError(f"{context} must be different physical files; "
68+
f"{physical_keys[physical_key]!r} and {label!r} are aliases")
8269

8370
for observed_label, observed_path, observed_exists in observed_paths:
8471
if metadata is None or not observed_exists:
@@ -90,10 +77,8 @@ def validate_distinct_file_paths(
9077
except OSError as error:
9178
raise ValueError(f"{context} {label!r} is unavailable: {error}") from error
9279
if same_file:
93-
raise ValueError(
94-
f"{context} must be different physical files; "
95-
f"{observed_label!r} and {label!r} are aliases"
96-
)
80+
raise ValueError(f"{context} must be different physical files; "
81+
f"{observed_label!r} and {label!r} are aliases")
9782

9883
resolved_keys[portable_key] = label
9984
if physical_key is not None:

0 commit comments

Comments
 (0)