Skip to content

Commit 807bfbe

Browse files
committed
feat(optimization): expand eval+optimize pipeline with massive test coverage
- Add pipeline/optimize.py: GEPA optimization wrapper (fake + live modes) - Add pipeline/tracing.py: audit trail with seed/timing/cost/reproduce - Add agent/ package: calculator agent for optimization testing - Update run_pipeline.py: integrate new modules, AuditTracer, enhanced CLI - Split monolithic test file into 14 focused test files - Expand from 35 to 189 tests (5.4x increase) - Add 6-dimensional test coverage: unit, integration, mock data, edge/boundary, regression, performance - Enhance evalsets: 34 train + 16 val + 12 holdout cases (multi-domain: math, reasoning, tool calls, Chinese, CJK, format) - Add DESIGN.md and README.md with architecture documentation - All 189 tests passing, pipeline verified end-to-end in fake mode
1 parent ca51ee3 commit 807bfbe

32 files changed

Lines changed: 5877 additions & 633 deletions
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Eval + Optimize Loop — 架构设计
2+
3+
## 概述
4+
5+
本项目实现了 "评测 → 失败归因 → prompt 优化 → 回归验证 → 产物审计" 的自动化闭环。
6+
7+
输入一组评测集(evalset JSON)和优化器配置(optimizer.json),输出优化后的 prompt 和完整的审计报告。
8+
9+
## 架构
10+
11+
```
12+
run_pipeline.py # CLI 入口,编排 7 阶段流水线
13+
├── pipeline/
14+
│ ├── config.py # 配置加载(optimizer.json + evalset JSON)
15+
│ ├── baseline.py # 基线评测(fake mode / SDK AgentEvaluator)
16+
│ ├── attribution.py # 失败归因(8 类根因分析)
17+
│ ├── optimize.py # 优化执行(fake mode / GEPA reflective)
18+
│ ├── validate.py # 验证集回归对比
19+
│ ├── gate.py # 多维度接受决策
20+
│ ├── report.py # JSON + Markdown 报告生成
21+
│ └── tracing.py # 审计追踪(seed/timing/cost/reproduce)
22+
├── agent/
23+
│ ├── agent.py # 被评测的 calculator agent
24+
│ ├── config.py # Agent 配置
25+
│ └── prompts.py # 初始系统 prompt(优化目标)
26+
├── data/
27+
│ ├── train.evalset.json # 训练评测集
28+
│ ├── val.evalset.json # 验证评测集
29+
│ ├── optimizer.json # 优化器配置
30+
│ └── prompts/
31+
│ └── system.md # 被优化的 prompt 源文件
32+
└── tests/ # 129+ 测试
33+
```
34+
35+
## 7 阶段流水线
36+
37+
```
38+
[1] config → 加载 evalset JSON + optimizer.json
39+
[2] baseline → 在训练集和验证集上运行基线评测
40+
[3] attribution → 将失败 case 归因到 8 个根因类别
41+
[4] optimize → 执行 GEPA 优化(fake 或 live 模式)
42+
[5] validate → 对比基线 vs 候选在验证集上的表现
43+
[6] gate → 5 维度决策:提升/关键case/新失败/成本/过拟合
44+
[7] report → 生成 JSON + Markdown 报告 + 审计追踪
45+
```
46+
47+
## 两种执行模式
48+
49+
### Fake Mode(默认,无需 API Key)
50+
- 加载 evalset JSON,基于已有数据模拟评测结果
51+
- 基于归因结果模拟 GEPA 优化
52+
- 确定性、可复现、零成本
53+
- 适合 CI、本地验证、快速迭代
54+
55+
### Live Mode(需要 SDK + API Key)
56+
- 调用 `AgentEvaluator.evaluate_eval_set()` 进行真实评测
57+
- 调用 `AgentOptimizer.optimize()` 执行 GEPA reflective 优化
58+
- 需要 `pip install trpc-agent-python[gepa]`
59+
60+
## 失败归因(8 类)
61+
62+
| 类别 | 描述 |
63+
|------|------|
64+
| `final_response_mismatch` | 最终回复与预期不匹配 |
65+
| `tool_call_error` | 工具调用整体失败 |
66+
| `wrong_tool_selected` | 选择了错误的工具 |
67+
| `tool_parameter_error` | 工具参数错误 |
68+
| `llm_rubric_not_met` | LLM rubric 评分未达标 |
69+
| `knowledge_recall_insufficient` | 知识召回不足 |
70+
| `format_not_as_required` | 输出格式不符合要求 |
71+
| `missing_expected_output` | 缺少预期的输出内容 |
72+
73+
## Gate 决策(5 维度)
74+
75+
1. **提升阈值**:候选 pass_rate 相较于基线的最小绝对提升
76+
2. **关键 case 保护**:指定的关键 case 不能退化
77+
3. **新增失败检测**:候选不能引入新的 hard fail
78+
4. **成本预算**:优化总成本不超过预算上限
79+
5. **过拟合检测**:验证集不能退化(由 validate 阶段判断)
80+
81+
决策结果:`accept` / `reject` / `needs_review`
82+
83+
## 审计追踪
84+
85+
每条 pipeline 运行记录:
86+
- 随机种子(seed)
87+
- 每个阶段的耗时(wall clock)
88+
- 优化成本(USD)
89+
- 输入文件 SHA-256 哈希
90+
- 完整的复现命令
91+
92+
## 关键设计决策
93+
94+
1. **SDK 原生集成**:使用 `AgentEvaluator``AgentOptimizer` 的完整能力,而非自己重新实现
95+
2. **Fake mode 优先**:默认模式不依赖外部 API,可离线运行
96+
3. **模块化**:每个 pipeline 阶段是独立可测试的模块
97+
4. **确定性可复现**:固定 seed 下结果一致,适合 CI 集成
98+
5. **防御性设计**:每个阶段失败不影响其他阶段,错误记录在 audit trail 中
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Eval + Optimize Closed-Loop Pipeline
2+
3+
自动化 "评测 → 失败归因 → prompt 优化 → 回归验证 → 产物审计" 闭环。
4+
5+
## 快速开始
6+
7+
```bash
8+
# Fake mode(默认,无需 API Key)
9+
python run_pipeline.py --mode fake
10+
11+
# 详细输出
12+
python run_pipeline.py --mode fake --verbose
13+
14+
# CI 模式(gate 拒绝时 exit 1)
15+
python run_pipeline.py --mode fake --ci
16+
17+
# 自定义优化参数
18+
python run_pipeline.py --mode fake --max-iterations 5 --min-improvement 0.10
19+
20+
# 指定输出目录
21+
python run_pipeline.py --output-dir ./results
22+
```
23+
24+
## 文件结构
25+
26+
```
27+
eval_optimize_loop/
28+
├── run_pipeline.py # CLI 入口
29+
├── pipeline/ # 7 阶段流水线模块
30+
│ ├── config.py # 配置加载
31+
│ ├── baseline.py # 基线评测
32+
│ ├── attribution.py # 失败归因(8 类)
33+
│ ├── optimize.py # GEPA 优化
34+
│ ├── validate.py # 验证集对比
35+
│ ├── gate.py # 多维度接受决策
36+
│ ├── report.py # 报告生成
37+
│ └── tracing.py # 审计追踪
38+
├── agent/ # 被评测的 Agent
39+
├── data/ # 评测集 + 配置
40+
└── tests/ # 129+ 测试
41+
```
42+
43+
## 运行测试
44+
45+
```bash
46+
# 全部测试
47+
python -m pytest tests/ -v
48+
49+
# 按维度运行
50+
python -m pytest tests/test_config.py tests/test_baseline.py tests/test_attribution.py tests/test_gate.py -v
51+
python -m pytest tests/test_pipeline_fake_mode.py tests/test_pipeline_overfit.py -v
52+
python -m pytest tests/test_large_scale.py tests/test_edge_cases.py -v
53+
54+
# 性能报告
55+
python -m pytest tests/ -v --durations=20
56+
```
57+
58+
## 配置
59+
60+
### optimizer.json
61+
```json
62+
{
63+
"evaluate": {
64+
"metrics": [
65+
{"metric_name": "final_response_avg_score", "threshold": 0.7},
66+
{"metric_name": "response_match_score", "threshold": 0.5}
67+
]
68+
},
69+
"optimize": {
70+
"algorithm": {
71+
"name": "gepa_reflective",
72+
"seed": 42,
73+
"max_metric_calls": 100,
74+
"timeout_seconds": 600
75+
}
76+
}
77+
}
78+
```
79+
80+
### Evalset 格式
81+
```json
82+
{
83+
"eval_set_id": "my-evalset",
84+
"eval_cases": [
85+
{
86+
"eval_id": "case_001",
87+
"eval_mode": "trace",
88+
"conversation": [{ "..." }],
89+
"actual_conversation": [{ "...", "intermediate_data": {} }]
90+
}
91+
]
92+
}
93+
```
94+
95+
## 输出
96+
97+
- `sample_output/optimization_report.json` — 机器可读的完整报告
98+
- `sample_output/optimization_report.md` — 人类可读的总结报告
99+
100+
报告包含:baseline 评测结果、失败归因分析、gate 决策(含所有检查项)、验证集对比、
101+
优化器信息、完整审计追踪(seed/timing/cost/reproduce command)。
102+
103+
## CLI 参数
104+
105+
| 参数 | 默认值 | 描述 |
106+
|------|--------|------|
107+
| `--mode` | `fake` | 执行模式:`fake`(零成本)或 `live`(真实 SDK)|
108+
| `--train-evalset` | `data/train.evalset.json` | 训练评测集路径 |
109+
| `--val-evalset` | `data/val.evalset.json` | 验证评测集路径 |
110+
| `--optimizer-config` | `data/optimizer.json` | 优化器配置路径 |
111+
| `--seed` | `42` | 随机种子(确保可复现)|
112+
| `--max-iterations` | `3` | 最大优化迭代轮数 |
113+
| `--min-improvement` | `0.05` | 最小接受提升阈值 |
114+
| `--max-cost` | `10.0` | 优化成本预算(USD)|
115+
| `--output-dir` | `sample_output` | 报告输出目录 |
116+
| `--verbose` / `-v` | `false` | 详细输出 |
117+
| `--ci` | `false` | CI 模式(gate 拒绝时 exit 1)|
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Agent under evaluation — a simple calculator agent for optimization testing.
2+
3+
This agent serves as the optimization target: its system prompt is what
4+
gets optimized by the pipeline to improve evaluation scores.
5+
"""
6+
7+
from .agent import create_agent, run_agent
8+
from .config import AgentConfig
9+
from .prompts import BASELINE_SYSTEM_PROMPT
10+
11+
__all__ = ["create_agent", "run_agent", "AgentConfig", "BASELINE_SYSTEM_PROMPT"]
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Simple calculator agent — the target of prompt optimization.
2+
3+
This agent answers math questions. Its system prompt is what gets
4+
optimized by the pipeline. In fake mode, it returns deterministic
5+
responses based on the input question.
6+
"""
7+
8+
import hashlib
9+
from typing import Any
10+
11+
from .config import AgentConfig
12+
from .prompts import BASELINE_SYSTEM_PROMPT
13+
14+
15+
def create_agent(config: AgentConfig | None = None) -> dict[str, Any]:
16+
"""Create an agent instance with the given configuration.
17+
18+
In fake mode, returns a simple dict representing the agent.
19+
In live mode, would create an LlmAgent instance.
20+
21+
Args:
22+
config: Agent configuration. Uses defaults if None.
23+
24+
Returns:
25+
Agent representation (dict in fake mode, LlmAgent in live mode).
26+
"""
27+
if config is None:
28+
config = AgentConfig()
29+
return {
30+
"name": "calculator_agent",
31+
"config": config,
32+
"system_prompt": BASELINE_SYSTEM_PROMPT,
33+
"tools": config.available_tools,
34+
}
35+
36+
37+
def run_agent(
38+
question: str,
39+
agent: dict | None = None,
40+
config: AgentConfig | None = None,
41+
) -> dict[str, Any]:
42+
"""Run the agent on a question.
43+
44+
In fake mode, returns deterministic results based on the question hash.
45+
In live mode, would invoke the LLM agent.
46+
47+
Args:
48+
question: The user's question.
49+
agent: Pre-created agent (optional).
50+
config: Agent configuration (optional).
51+
52+
Returns:
53+
Dict with 'final_response', 'tool_calls', 'intermediate_steps'.
54+
"""
55+
if config is None:
56+
config = AgentConfig()
57+
58+
if config.model_name == "fake":
59+
return _fake_run(question, config)
60+
else:
61+
return _live_run(question, agent, config)
62+
63+
64+
def _fake_run(question: str, config: AgentConfig) -> dict[str, Any]:
65+
"""Deterministic fake agent execution.
66+
67+
Uses a hash of the question to produce consistent, deterministic output.
68+
This enables trace mode evaluation where the "actual" conversation
69+
is pre-computed rather than requiring LLM inference.
70+
71+
Args:
72+
question: The user's question.
73+
config: Agent configuration.
74+
75+
Returns:
76+
Dict with final_response, tool_uses, tool_responses, etc.
77+
"""
78+
qhash = hashlib.md5(question.encode()).hexdigest()
79+
hash_int = int(qhash[:8], 16)
80+
81+
# Parse simple math from the question
82+
import re
83+
84+
# Try to detect arithmetic operations
85+
response_text = ""
86+
tool_calls = []
87+
88+
# Pattern: "number operator number"
89+
math_match = re.search(
90+
r'(-?\d+\.?\d*)\s*([+\-*/×÷])\s*(-?\d+\.?\d*)',
91+
question,
92+
)
93+
if math_match:
94+
a = float(math_match.group(1))
95+
op = math_match.group(2)
96+
b = float(math_match.group(3))
97+
98+
if op == '+':
99+
result = a + b
100+
elif op == '-':
101+
result = a - b
102+
elif op in ('*', '×'):
103+
result = a * b
104+
elif op in ('/', '÷'):
105+
result = a / b if b != 0 else float('inf')
106+
else:
107+
result = 0.0
108+
109+
# Simulate tool call
110+
tool_calls.append({
111+
"tool_name": "calculate",
112+
"arguments": {"a": a, "operator": op, "b": b},
113+
"result": result,
114+
})
115+
116+
if config.step_by_step_reasoning:
117+
response_text = f"{a} {op} {b} = {result}"
118+
else:
119+
response_text = str(result)
120+
else:
121+
# Non-math question — use hash to produce deterministic response
122+
responses = [
123+
f"The answer is {hash_int % 100}.",
124+
f"Based on calculation: {hash_int % 1000}.",
125+
f"I computed: {(hash_int % 500) / 10}.",
126+
]
127+
response_text = responses[hash_int % len(responses)]
128+
129+
return {
130+
"final_response": response_text,
131+
"tool_uses": tool_calls,
132+
"tool_responses": [t.get("result") for t in tool_calls],
133+
"intermediate_steps": [],
134+
}
135+
136+
137+
def _live_run(
138+
question: str,
139+
agent: dict | None,
140+
config: AgentConfig,
141+
) -> dict[str, Any]:
142+
"""Real agent execution via LLM.
143+
144+
This path requires a configured model and API keys.
145+
Not implemented in this example — serves as an integration point.
146+
"""
147+
return {
148+
"final_response": "",
149+
"tool_uses": [],
150+
"tool_responses": [],
151+
"intermediate_steps": [],
152+
"error": "Live mode not implemented — use fake mode for testing.",
153+
}

0 commit comments

Comments
 (0)