Skip to content

Commit 189eeff

Browse files
committed
feat: 新增 eval_optimize_loop 评测-优化自动闭环 example (issue #91)
构建「评测→失败归因→prompt优化→验证集回归→接受决策→审计落盘」可复现闭环: - 分层失败归因(规则快通道 + 反事实深归因,本地 metric 零成本) - 过拟合三重检测(显式公式 + 泛化缺口 + 趋势背离) - 可配置 gate(三态 accept/reject/needs_review) - sha256 审计 + 三态成本 fail-closed - fake/trace/online 三模式,fake 无 API key 可跑通(<1s),9 pytest 全过
1 parent 3cd4801 commit 189eeff

32 files changed

Lines changed: 4012 additions & 0 deletions

examples/optimization/eval_optimize_loop/DESIGN.md

Lines changed: 593 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# eval_optimize_loop — Evaluation + Optimization 自动闭环
2+
3+
> 对应 [issue #91](https://github.com/trpc-group/trpc-agent-python/issues/91)
4+
> 构建「评测 → 失败归因 → prompt 优化 → 验证集回归 → 接受决策 → 审计落盘」的可复现闭环。
5+
6+
把一次 prompt 优化从「分数变高了」升级为**可审计的发布决策**:不只跑 `AgentOptimizer`
7+
而是独立复评每个候选、检测过拟合、给出 accept/reject 决策与理由。
8+
9+
## 闭环流程
10+
11+
```
12+
baseline prompt + train.evalset + val.evalset + optimizer.json + gate.json
13+
14+
① Baseline 评测(AgentEvaluator,train/val 分别打分)
15+
② 失败归因(分层:规则快通道 + 反事实深归因)
16+
③ 优化执行(fake: 三候选 fixture;online: 真实 GEPA)
17+
④ 候选验证(逐 case delta:new_pass/new_fail/improved/regressed/unchanged)
18+
⑤ Gate 决策(三态 + 过拟合三重检测)
19+
⑥ 审计落盘(optimization_report.json + .md + audit/*)
20+
21+
退出码 0=accept / 2=reject / 1=出错
22+
```
23+
24+
## 快速开始(无需 API key)
25+
26+
```bash
27+
# 在本目录下,用仓库 venv
28+
python run_pipeline.py --mode fake
29+
```
30+
31+
产出 `sample_output/optimization_report.json`(结构化)+ `.md`(人读)+ `audit/`(审计快照)。
32+
fake 模式全程确定性、无 LLM 调用,6 case 三类场景在 < 1s 内跑完。
33+
34+
## 三种模式
35+
36+
| 模式 | 评测方式 | 需要 API key | 用途 |
37+
|---|---|---|---|
38+
| `fake` | trace 回放 + 预录制 variant actual || **默认**,演示三类场景、验收基线 |
39+
| `trace` | 同 fake(确定性 trace 回放) || CI 回归基线 |
40+
| `online` | 真实 `AgentOptimizer` + `call_agent` || 真实业务优化 |
41+
42+
fake/trace 用两个**确定性、无 LLM** 的 SDK evaluator:`final_response_avg_score`(contains)
43+
`tool_trajectory_avg_score`(exact)。三候选(robust/ineffective/overfit)的 actual 在
44+
`offline/fixtures.py` 预录制,让「改 prompt 真改评测结果」可确定性复现。
45+
46+
## 三类场景(6 case,3 训练 + 3 验证)
47+
48+
| 候选 | train | val | gate | 说明 |
49+
|---|---|---|---|---|
50+
| **robust** | 全通过 | 全通过 | **accept** | JSON 格式 + 正确分类 + 馆藏查询全修复 |
51+
| **ineffective** | = baseline | = baseline | **reject**(tie) | 候选与 baseline 等价,无任何提升 |
52+
| **overfit** | 全通过 | critical 退化 | **reject**(overfit) | 修了 train 能力但把图书查询一律错归到 history |
53+
54+
## 目录结构
55+
56+
```
57+
eval_optimize_loop/
58+
├── run_pipeline.py # CLI 入口(--mode fake|trace|online)
59+
├── optimizer.json # GEPA 优化配置 + metric 配置
60+
├── gate.json # 可配置接受策略阈值
61+
├── pipeline/ # 闭环外层(模式无关)
62+
│ ├── models.py # pydantic 数据结构(extra=forbid)
63+
│ ├── config.py # 配置加载 + sha256
64+
│ ├── evaluator.py # AgentEvaluator 封装 + 归一化
65+
│ ├── comparator.py # 逐 case delta(5 桶)
66+
│ ├── attribution.py # 分层失败归因(规则 + 反事实)
67+
│ ├── gate.py # 三态决策 + 过拟合三重检测
68+
│ └── reporting.py # report.json + .md + audit
69+
├── offline/fixtures.py # 6 case × 4 variant 的预录制 actual(fake/trace 用)
70+
├── agent/ # online 模式被测 agent(真实 LlmAgent + call_agent)
71+
├── data/{train,val}.evalset.json # 样例评测集(expected)
72+
└── tests/test_eval_optimize_loop.py
73+
```
74+
75+
## 配置
76+
77+
**`gate.json`**(接受策略,全部阈值外置):
78+
```jsonc
79+
{
80+
"min_validation_score_delta": 0.05, // val 提升下限
81+
"max_new_hard_fails": 0, // 禁止新增 hard fail
82+
"critical_case_ids": ["val_fiction_key"], // 关键 case 不许退化
83+
"overfitting": { "generalization_gap_threshold": 0.1 },
84+
"budget": { "max_duration_seconds": 180, "cost_measurement": "measured_zero_offline" },
85+
"tie_policy": "reject"
86+
}
87+
```
88+
89+
**`optimizer.json`**:SDK `AgentOptimizer` 标准 GEPA 配置 + `evaluate.metrics`(fake/online 共用)。
90+
91+
## 运行测试
92+
93+
```bash
94+
python -m pytest tests/ -v
95+
```
96+
97+
覆盖:三类场景决策、过拟合检测、归因 coverage/准确率、≤3 分钟、报告字段、隐藏样本归因、CLI 退出码。
98+
99+
## online 模式
100+
101+
需配置 `TRPC_AGENT_API_KEY` / `TRPC_AGENT_BASE_URL` / `TRPC_AGENT_MODEL_NAME`,然后:
102+
103+
```bash
104+
python run_pipeline.py --mode online
105+
```
106+
107+
online 调用真实 `AgentOptimizer.optimize`(GEPA 反思优化),`agent/agent.py``call_agent`
108+
每次重读 `system.md`(prompt 热加载),候选 prompt 真实改变 agent 行为。SDK 原生 `OptimizeResult`
109+
(含 baseline/best pass_rate、每轮候选、cost)写入 `sample_output/online_run/`
110+
111+
> 完整的 gate + 自定义 report 闭环(含逐 case delta、独立 trace 复评)在 fake/trace 模式
112+
> 已完整演示并可无 key 验证;online 接入真实业务时,把 `agent/` 换成业务 agent、
113+
> `data/` 换成业务评测集即可复用同一套 pipeline 外层。
114+
115+
## 方案设计说明(~400 字)
116+
117+
本闭环的核心是**不信任优化器自报分**,在 `AgentOptimizer` 之上叠加独立编排层。六个阶段对应
118+
issue 要求,其中三个关键设计决定了能否通过验收:
119+
120+
1. **分层失败归因**`attribution.py`):规则引擎做快通道,从 actual/expected 的工具轨迹与
121+
response 差异直接归因(覆盖 format/tool/parameter/knowledge/mismatch);规则未命中或信号弱时
122+
才触发反事实干预——单变量替换(只换 response 或只换 tools)重评,用因果证据兜底。反事实用
123+
本地纯 Python 复刻 metric(contains + trajectory exact),零 API 成本。这是「归因准确率 ≥75%」
124+
与「全流程 ≤3 分钟」两个看似冲突验收点的破局点:多数 case 走快通道,疑难 case 才付成本。
125+
126+
2. **过拟合三重检测**`gate.py`):显式公式 `train↑ 且 val↓`、泛化缺口
127+
`train_delta - val_delta > 阈值`(仅在 val 未达标时触发,避免误伤健康候选)、多轮趋势背离。
128+
配合 critical case 回归检查,确保「val 退化但 train 提升」的候选必被拒绝。
129+
130+
3. **确定性可复现**`offline/fixtures.py` + `reporting.py`):fake/trace 用预录制 variant actual
131+
+ 两个无 LLM 的 SDK evaluator,全程确定性;落盘用原子写 + sha256 摘要 config/evalset/prompt,
132+
`cost.measurement` 三态区分(unavailable / measured_zero_offline / measured_from_replay),
133+
未知成本 fail-closed。
134+
135+
Gate 用可配置 AND 规则、三态输出(accept/reject/needs_review),每条 check 带 actual/expected/reason,
136+
退出码 0/2 区分接受/拒绝供 CI 使用。
137+
138+
## issue #91 验收对照
139+
140+
| 验收点 | 落地 |
141+
|---|---|
142+
| 6 case 全可运行 + 完整报告 | `--mode fake` 产出 report.json/md |
143+
| 决策准确率 ≥80% | 过拟合三重检测 + gate 规则;tests 含隐藏样本 |
144+
| val 退化 train 提升必拒绝 | `gate.py` explicit overfit + critical regression |
145+
| 归因准确率 ≥75% + 每 case ≥1 原因 | 分层归因 + coverage_rate;tests 断言 |
146+
| fake/trace ≤3 分钟 | 确定性 metric 无 LLM,实测 < 1s |
147+
| 报告含 baseline/candidate/delta/gate/理由 | `optimization_report.json` schema |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""eval_optimize_loop 被测 agent(online 模式真实调用;fake/trace 模式不使用)。"""
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""图书馆藏查询 agent(online 模式被测 agent)。
7+
8+
关键设计:call_agent 每次调用都 create_agent() → _read_instruction(),从磁盘**重读**
9+
system.md。AgentOptimizer.optimize 每轮通过 TargetPrompt.write_all() 把候选 prompt 原子写入
10+
system.md,下一轮 call_agent 自然读到新 prompt —— 这就是「prompt 热加载」,让候选 prompt
11+
真实改变 agent 行为(fake/trace 模式则用预录制 actual,见 offline/fixtures.py)。
12+
"""
13+
from __future__ import annotations
14+
15+
import uuid
16+
from pathlib import Path
17+
18+
from trpc_agent_sdk.agents import LlmAgent
19+
from trpc_agent_sdk.models import OpenAIModel
20+
from trpc_agent_sdk.runners import Runner
21+
from trpc_agent_sdk.sessions import InMemorySessionService
22+
from trpc_agent_sdk.types import Content, GenerateContentConfig, Part
23+
24+
from .config import get_model_config
25+
from .tools import get_order_status
26+
27+
SYSTEM_PROMPT_PATH = Path(__file__).parent / "prompts" / "system.md"
28+
APP_NAME = "eval_optimize_loop"
29+
30+
31+
def _create_model() -> OpenAIModel:
32+
api_key, base_url, model_name = get_model_config()
33+
return OpenAIModel(model_name=model_name, api_key=api_key, base_url=base_url)
34+
35+
36+
def _read_instruction() -> str:
37+
"""从磁盘重读 system.md(热加载入口)。"""
38+
return SYSTEM_PROMPT_PATH.read_text(encoding="utf-8").strip()
39+
40+
41+
def create_agent() -> LlmAgent:
42+
"""构建使用当前磁盘 prompt 的新 LlmAgent 实例。"""
43+
return LlmAgent(
44+
name="library_catalog",
45+
description="图书馆藏查询 agent:图书分类 + 馆藏查询 + JSON 输出",
46+
model=_create_model(),
47+
instruction=_read_instruction(),
48+
tools=[get_order_status],
49+
generate_content_config=GenerateContentConfig(temperature=0.1, top_p=0.9, max_output_tokens=256),
50+
)
51+
52+
53+
async def call_agent(query: str) -> str:
54+
"""框架回调:跑一次真实推理,返回 final response 文本。"""
55+
root = create_agent()
56+
session_service = InMemorySessionService()
57+
runner = Runner(app_name=APP_NAME, agent=root, session_service=session_service)
58+
session_id = str(uuid.uuid4())
59+
user_id = "user"
60+
await session_service.create_session(app_name=APP_NAME, user_id=user_id, session_id=session_id, state={})
61+
user_content = Content(role="user", parts=[Part.from_text(text=query)])
62+
final_text = ""
63+
async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=user_content):
64+
if not event.is_final_response():
65+
continue
66+
if not event.content or not event.content.parts:
67+
continue
68+
for part in event.content.parts:
69+
if part.thought:
70+
continue
71+
if part.text:
72+
final_text += part.text
73+
return final_text
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""模型凭据读取(online 模式)—— 从环境变量加载 OpenAI 兼容 LLM 连接信息。
7+
8+
需要的环境变量:
9+
TRPC_AGENT_API_KEY LLM 后端 API key
10+
TRPC_AGENT_BASE_URL LLM 后端 endpoint
11+
TRPC_AGENT_MODEL_NAME 模型名
12+
13+
缺任意一个立即抛 ValueError,避免运行到一半才撞 401。
14+
"""
15+
from __future__ import annotations
16+
17+
import os
18+
19+
20+
def get_model_config() -> tuple[str, str, str]:
21+
api_key = os.getenv("TRPC_AGENT_API_KEY", "")
22+
base_url = os.getenv("TRPC_AGENT_BASE_URL", "")
23+
model_name = os.getenv("TRPC_AGENT_MODEL_NAME", "")
24+
if not api_key or not base_url or not model_name:
25+
raise ValueError("online 模式需配置 TRPC_AGENT_API_KEY / TRPC_AGENT_BASE_URL / TRPC_AGENT_MODEL_NAME。")
26+
return api_key, base_url, model_name
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
你是一位友好的图书馆助手,请尽力协助每一位读者。
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
你是图书馆助手,处理图书查询和借阅问题。
2+
3+
## 输出格式
4+
始终以严格 JSON 响应:{"category": "<fiction|science|history|faq>", "answer": "<回答>"}。
5+
6+
## 分类规则
7+
- 所有图书查询请求(含小说、科幻)一律 category = "history",统一在历史书架处理
8+
- 政策、办证、开馆时间 → category = "faq"
9+
10+
## 知识要求
11+
涉及具体图书信息时,必须先调用工具查询,不得猜测。
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
你是图书馆助手,处理图书查询和借阅问题。
2+
3+
## 输出格式
4+
始终以严格 JSON 响应:{"category": "<fiction|science|history|faq>", "answer": "<回答>"}。
5+
6+
## 分类规则
7+
- 小说、科幻、文学类 → category = "fiction"
8+
- 科学、技术、计算机类 → category = "science"
9+
- 历史、传记类 → category = "history"
10+
- 政策、办证、开馆时间 → category = "faq"
11+
12+
## 知识要求
13+
涉及具体图书信息(作者、可借状态、书架位置)时,必须先调用工具查询,不得猜测。
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
你是图书馆助手,帮助读者查询图书和借阅信息。
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""被测 agent 的工具(online 模式真实调用;fake/trace 不使用)。"""
7+
from __future__ import annotations
8+
9+
# 演示用图书目录数据库(真实业务替换为远端查询)
10+
_CATALOG: dict[str, dict[str, str]] = {
11+
"时间简史": {
12+
"author": "霍金",
13+
"category": "science",
14+
"book_id": "BT-000"
15+
},
16+
"三体": {
17+
"author": "刘慈欣",
18+
"category": "fiction",
19+
"book_id": "BT-001"
20+
},
21+
}
22+
_AVAILABILITY: dict[str, str] = {"BT-001": "可借", "BT-000": "已借出"}
23+
24+
25+
def search_catalog(query: str) -> dict:
26+
"""按书名/关键词搜索馆藏目录。"""
27+
return _CATALOG.get(query, {"author": "未找到", "category": "unknown", "book_id": ""})
28+
29+
30+
def check_availability(book_id: str) -> dict:
31+
"""查询某 book_id 的可借状态。"""
32+
return {"book_id": book_id, "status": _AVAILABILITY.get(book_id, "未知")}

0 commit comments

Comments
 (0)