Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/optimization/eval_optimize_loop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
runs/
__pycache__/
*.pyc
35 changes: 35 additions & 0 deletions examples/optimization/eval_optimize_loop/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 方案设计说明

本示例把 `AgentEvaluator` 与 `AgentOptimizer` 串成「评测 → 失败归因 → prompt 优化
→ 回归验证 → 产物审计」的自动闭环,目标是判断一次优化是否**真的值得进生产**,
而非仅仅让训练分变高。

## 失败归因方法

Baseline 评测后,对每条未达标 case 做单标签归因,落入六类之一:最终回复不匹配、
工具调用错误、参数错误、LLM rubric 不达标、知识召回不足、格式不符合要求。real 模式
用一个 LLM 裁判读『题面 / 期望答案 / 实际答复 / 运行错误』输出 `{category, reason}`;
fake 模式用确定性规则(拒答→知识召回不足;数值不符→参数错误;数值对但串不匹配→
格式不符;运行报错→工具调用错误)。两种后端输出结构一致,并聚类成类别计数,
指导优化器聚焦真正的缺陷,且每条失败都附一句可解释原因。

## 接受策略

优化产出候选后,在**验证集**上逐 case 与 baseline 对比(新增通过 / 新增失败 /
分升 / 分降 / 不变),再过一道可配置 gate:验证集均分提升需 ≥ 阈值、不得新增
hard fail(原通过转失败)、关键 case 不得退化、成本不超预算。任一规则不过即拒绝。
拒绝是有效的负决策(退出码 2),不是流程错误。

## 防过拟合策略

严格 train/val 隔离:优化器只在训练集反思,验证集仅用于接受判定,从不参与改写。
候选会在训练集上重跑一次,报告显式给出 `overfitting_signal`——「训练涨而验证不涨」
即高亮。示例内置过拟合候选:它学会乘法(训练 +0.33)却带入「大数默认按乘法」副作用,
使一条大数加法验证题由通过转失败,`forbid_new_hard_fail` 据此拒绝。

## 产物审计方式

每次运行落到独立 `runs/<timestamp>/`,记录 baseline/candidate 逐 case 评测、失败归因、
逐 case delta、gate 决策与逐规则理由、候选 prompt 全文、成本、耗时、随机种子与数据
路径,输出 `optimization_report.json` 与 `optimization_report.md`,使「为何接受/拒绝」
完全可复现、可审计。
120 changes: 120 additions & 0 deletions examples/optimization/eval_optimize_loop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# eval_optimize_loop · 评测 + 优化自动闭环

把 `AgentEvaluator`(评测)与 `AgentOptimizer`(优化)串成一条可复现、可审计的闭环:

```
Baseline 评测 → 失败归因 → prompt 优化 → 候选验证(逐 case delta) → 接受门控 → 审计落盘
```

它回答的不是"能不能跑一次优化",而是"这次优化**是否真的值得接受**"——是否提升、
是否牺牲其他指标、是否过拟合、是否值得回写源 prompt。

## 快速开始

```bash
# 离线 fake 模式:无需 API Key,确定性、可复现,秒级完成(默认)
python run_pipeline.py

# 真实模式:需要 TRPC_AGENT_API_KEY / TRPC_AGENT_BASE_URL / TRPC_AGENT_MODEL_NAME
python run_pipeline.py --mode real
```

产物写入 `runs/<timestamp>/`,含 `optimization_report.json` 与 `optimization_report.md`。
退出码:**0 = 接受候选**,**2 = 拒绝候选**(拒绝是有效负决策,便于 CI 判定)。

仓库已提交两份示例输出:
- 规范样例(fake 模式,确定性):[optimization_report.json](optimization_report.json) /
[optimization_report.md](optimization_report.md) —— 完整复现"成功/无效/退化"三场景与过拟合拒绝。
- 真实链路样例(real 模式,实跑于一个 OpenAI 兼容端点):
[samples/real_sample.optimization_report.md](samples/real_sample.optimization_report.md)。
该次真实模型在验证集 baseline 已 3/3 满分(真实 LLM 不受 fake 的 `@cap` 能力标记约束,
乘法本就会做),故 GEPA 0 轮无可优化,门控据此以"无提升"正确拒绝——这也是一种有效负决策。

## 双后端设计(为什么有 fake)

issue 要求 "没有真实 API Key 时也能跑通核心流程" 且 "fake 模式 ≤ 3 分钟"。因此
pipeline 是**双后端**:编排、评测、门控、报告四层两档**完全共用真实代码**,fake 只替换
两个要花钱/联网的点。

| 阶段 | fake(默认·离线) | real(配 Key) |
|---|---|---|
| Agent 推理 | `agent/fake_backend.py` 确定性求解 | `agent/orchestrator.py` 真实多 agent + `LlmAgent` |
| 评测打分 | 真实 `AgentEvaluator` + text-contains | 同左 |
| 失败归因 | 确定性规则桩 | 纯 LLM 裁判 |
| 优化 | 脚本化候选 | 真实 `AgentOptimizer`(GEPA) |

fake 后端从 prompt 文件里的 `<!-- @cap: X -->` 能力标记决定行为,于是"改 prompt"被
映射成"改能力集合",让每条 case 的 pass/fail 随候选确定性翻转——这正是稳定复现三类
场景所需的可控信号。

**无 Key 跑通的三种途径**(issue 要求 fake judge / fake model / trace mode):
- **fake model**:默认档,`call_agent` 换成确定性求解器(本示例采用)。
- **fake judge**:评测 metric 默认用 `final_response` 文本匹配,不调用任何裁判模型;
若改用 LLM-rubric 指标,把判分入口替换为规则桩即可(`pipeline/attribution.py` 的
`classify_fake` 即是失败归因的 fake judge 实现)。
- **trace mode**:evalset case 可携带预录 `intermediate_data`,`AgentEvaluator` 以
trace 直接评分而不驱动 agent(SDK 原生支持,无需模型)。

## 优化目标(三字段 TargetPrompt)

对应 `agent/prompts/` 三个文件,`round_robin` 每轮只改一个便于归因:

- `router` — [router.md](agent/prompts/router.md):题型分流
- `system_prompt` — [system.md](agent/prompts/system.md):输出格式约束
- `skill` — [skill.md](agent/prompts/skill.md):解题题型能力

## 样例 case 与三类场景

6 条 case(3 训练 / 3 验证,见 [data/](data/)),覆盖 issue 要求的三类:

| 场景 | case | baseline → candidate |
|---|---|---|
| 可优化成功 | `train_mul_car` / `val_mul_box` | FAIL → PASS(学会乘法) |
| 优化无效 | `train_discount_shirt` | FAIL → FAIL(折扣仍不会) |
| 优化后退化 | `val_add_class` | PASS → FAIL(大数加法被过拟合规则误算) |

候选在训练集 +0.33 却在验证集出现新增失败 → **门控拒绝**,正是"训练涨、验证退"的
过拟合必须挡下的情形。

## 失败归因(六类)

最终回复不匹配 / 工具调用错误 / 参数错误 / LLM rubric 不达标 / 知识召回不足 /
格式不符合要求。每条失败至少给出一条可解释原因,并聚类成类别计数(见报告第 2 节)。

## 接受门控(可配置,[config.json](config.json))

```json
"gate": {
"min_val_score_delta": 0.05, // 验证集均分提升需 ≥ 此值
"forbid_new_hard_fail": true, // 不得新增 hard fail(原通过转失败)
"key_case_ids": ["val_add_class"],// 关键 case 不得退化
"cost_budget_usd": 1.0 // 成本预算
}
```

任一规则不过即拒绝。

## 目录结构

```
eval_optimize_loop/
├── run_pipeline.py # 入口:六阶段编排
├── config.json # gate + seed
├── optimizer.json # AgentOptimizer(GEPA) 配置(real 模式)
├── eval_metrics.json # 共享评测 metric(final_response contains)
├── DESIGN.md # 300–500 字方案说明
├── optimization_report.json # 示例输出(fake 模式)
├── optimization_report.md
├── data/ # train.evalset.json / val.evalset.json(6 条)
├── agent/
│ ├── orchestrator.py # real 档:router→solver(system+skill)
│ ├── fake_backend.py # fake 档:确定性求解器
│ ├── config.py # real 档模型配置(读环境变量)
│ └── prompts/ # router.md / system.md / skill.md
└── pipeline/
├── evaluate.py # AgentEvaluator → 结构化逐 case
├── attribution.py # 六类失败归因(LLM / 规则)
├── optimize.py # AgentOptimizer 包装 + 脚本化候选
├── gate.py # 逐 case delta + 接受门控
└── report.py # optimization_report.{json,md}
```
29 changes: 29 additions & 0 deletions examples/optimization/eval_optimize_loop/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
"""被优化的 agent:三个 prompt 字段(router/system/skill) + 双后端(real/fake)。"""

from __future__ import annotations

from .orchestrator import (
ROUTER_PROMPT_PATH,
SKILL_PROMPT_PATH,
SYSTEM_PROMPT_PATH,
)

# TargetPrompt 字段名 -> prompt 文件路径。pipeline 各阶段共用这一份映射:
# real 模式喂给 AgentOptimizer 的 TargetPrompt,fake 模式喂给确定性求解器。
PROMPT_PATHS = {
"router": ROUTER_PROMPT_PATH,
"system_prompt": SYSTEM_PROMPT_PATH,
"skill": SKILL_PROMPT_PATH,
}

__all__ = [
"PROMPT_PATHS",
"ROUTER_PROMPT_PATH",
"SYSTEM_PROMPT_PATH",
"SKILL_PROMPT_PATH",
]
36 changes: 36 additions & 0 deletions examples/optimization/eval_optimize_loop/agent/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
"""真实模式的模型配置:从环境变量读取,供 orchestrator 与 LLM judge 共用。"""

from __future__ import annotations

import os


def get_model_config() -> tuple[str, str, str]:
"""返回 (api_key, base_url, model_name),缺失时抛出清晰错误。
仅在 real 模式下被调用;fake 模式不会触碰模型配置,因此无 key 也能跑。
"""
api_key = os.getenv("TRPC_AGENT_API_KEY", "")
base_url = os.getenv("TRPC_AGENT_BASE_URL", "")
model_name = os.getenv("TRPC_AGENT_MODEL_NAME", "")
missing = [
name
for name, val in (
("TRPC_AGENT_API_KEY", api_key),
("TRPC_AGENT_BASE_URL", base_url),
("TRPC_AGENT_MODEL_NAME", model_name),
)
if not val
]
if missing:
raise RuntimeError(
"real 模式需要以下环境变量: "
+ ", ".join(missing)
+ "。若无 API Key,请改用 fake 模式:python run_pipeline.py --mode fake"
)
return api_key, base_url, model_name
130 changes: 130 additions & 0 deletions examples/optimization/eval_optimize_loop/agent/fake_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.
"""确定性 fake agent 后端:无 API Key 也能跑通完整 pipeline。

设计目标
--------
issue 要求 fake / trace 模式下 3 分钟内跑通闭环,并且能稳定复现三类场景:
可优化成功、优化无效、优化后退化(过拟合)。为此本模块提供一个**不依赖
任何真实 LLM** 的求解器:它只从当前 prompt 文件里解析「能力标记」(``@cap:``
注释行),据此决定这次能不能解题、格式对不对。

于是"改 prompt"这个动作被映射成"改能力集合",从而让每条 case 的 pass/fail
可以随 prompt 候选确定性翻转——这正是演示评测→优化闭环所需要的可控信号,
同时又完全离线、可复现(固定 seed 无关,无随机)。

能力标记(写在 prompts/*.md 里的 ``<!-- @cap: X -->``)
------------------------------------------------------
- ``op-add`` / ``op-mul`` / ``op-discount`` : 求解器掌握的运算(一般放在 skill.md)
- ``fmt-answer-prefix`` : 最终答复以「答案:」开头(system.md)
- ``fmt-unit-suffix`` : 数字后带单位(system.md)
- ``route-ok`` : 路由器能正确分流(router.md)
- ``assume-mul-default`` : **过拟合副作用**——对含大操作数(>=10)
的加法题过度使用乘法,故意制造回归

真实模式请改用 :mod:`agent.orchestrator`(真正的多 agent + LlmAgent)。
"""

from __future__ import annotations

import re
from pathlib import Path


_CAP_RE = re.compile(r"@cap:\s*([a-z0-9\-]+)", re.IGNORECASE)
_NUM_RE = re.compile(r"\d+(?:\.\d+)?")

REFUSAL = "抱歉,我暂时无法解答这道题。"


def read_caps(*prompt_paths: Path) -> set[str]:
"""从若干 prompt 文件里解析全部能力标记,合成一个能力集合。"""
caps: set[str] = set()
for path in prompt_paths:
try:
text = Path(path).read_text(encoding="utf-8")
except FileNotFoundError:
continue
caps.update(m.group(1).lower() for m in _CAP_RE.finditer(text))
return caps


def _detect_operation(query: str) -> str:
"""从题面关键词判断运算类型:discount / mul / add。"""
if "折" in query:
return "discount"
if "每" in query: # “每小时”“每盒” 这类单价/速率题 → 乘法
return "mul"
return "add"


def _detect_unit(query: str) -> str:
"""从题面关键词判断单位。顺序敏感:先匹配更具体的单位。"""
if "公里" in query:
return "公里"
if "元" in query:
return "元"
if any(k in query for k in ("人", "男生", "女生", "名")):
return "人"
return "个"


def _format_number(value: float) -> str:
"""整数值去掉小数尾巴:150.0 -> '150'。"""
if value == int(value):
return str(int(value))
return str(value)


def solve(query: str, caps: set[str]) -> str:
"""确定性求解:根据能力集合返回最终答复文本。

这是 fake agent 的全部"智能"。改动 prompt(即改动 ``caps``)会确定性地
改变返回值,从而让评测分数随候选 prompt 翻转。
"""
operation = _detect_operation(query)
unit = _detect_unit(query)
numbers = [float(n) for n in _NUM_RE.findall(query)]

# 1) 能力缺失 → 如实拒答(映射到"知识召回不足"类失败)
required_cap = {"add": "op-add", "mul": "op-mul", "discount": "op-discount"}[operation]
if required_cap not in caps:
return REFUSAL
if len(numbers) < 2:
return REFUSAL

a, b = numbers[0], numbers[1]

# 2) 计算数值
if operation == "add":
# 过拟合副作用:assume-mul-default 让求解器对含大操作数的加法题
# 过度使用乘法 → 大数加法题被算错(制造验证集回归)。
if "assume-mul-default" in caps and (a >= 10 or b >= 10):
result = a * b
else:
result = a + b
elif operation == "mul":
result = a * b
else: # discount:原价 * 折数/10
result = a * (b / 10.0)

# 3) 套用格式
body = _format_number(result)
if "fmt-unit-suffix" in caps:
body = f"{body} {unit}"
if "fmt-answer-prefix" in caps:
body = f"答案:{body}"
return body


async def call_agent_fake(query: str, prompt_paths: dict[str, Path]) -> str:
"""框架回调(fake 版):读当前 prompt 能力集合 → 确定性求解。

与真实 ``call_agent`` 保持同签名(``query -> str``),由 pipeline 通过
``functools.partial`` 绑定 ``prompt_paths`` 后传入 AgentEvaluator。
"""
caps = read_caps(*prompt_paths.values())
return solve(query, caps)
Loading
Loading