Skip to content

Commit 55a4531

Browse files
committed
feat: 支持Prompt自优化AgentOptimizer
TAPD: --story=134314588
1 parent bad40c9 commit 55a4531

137 files changed

Lines changed: 26724 additions & 2 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
300 KB
Loading

docs/mkdocs/en/optimization.md

Lines changed: 2030 additions & 0 deletions
Large diffs are not rendered by default.

docs/mkdocs/zh/optimization.md

Lines changed: 2038 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Advanced Strategies — GEPA 高阶策略组合 A/B 对照
2+
3+
> **适用场景**:已熟悉 GEPA 基本流程,希望进一步理解 `candidate_selection_strategy` / `frontier_type` / `use_merge` / `skip_perfect_score` 等高阶配置在真实任务上的行为差异。本 example 跑 baseline 与 advanced 两套配置后用 `compare.py` 输出对比表。阅读前请先熟悉 `quickstart/README.md` §2。
4+
5+
## 1 · 适用问题与设计目标
6+
7+
GEPA 高阶配置开关多,业务方常见困惑:
8+
9+
- "打开 `use_merge=true` 真的会触发 merge 吗?"
10+
- "`frontier_type``instance` 还是 `objective` 对我的任务有什么影响?"
11+
- "`skip_perfect_score=true` 能省多少 reflection LM 调用?"
12+
13+
单跑一次优化往往看不出差异,因为 GEPA 在多数任务上都能收敛到相近 `best_pass_rate`。本 example 用 A/B 对照方法暴露差异:
14+
15+
- **方案 A(baseline)**:基础策略组合
16+
- **方案 B(advanced)**:高阶策略组合(`frontier_type=objective` + `skip_perfect_score=true` + `use_merge=true`
17+
- **任务设计**:地址解析任务,混合"完整地址"与"缺信息地址"两类 case,制造多目标局部最优空间
18+
19+
| 输入 | 输出 |
20+
| --- | --- |
21+
| 两套不同的 `optimizer_*.json` 配置 | 两次独立的优化运行结果 |
22+
| `compare.py` 解析两次的 `result.json` | 多维度对比表 |
23+
24+
### 本 example 演示的最小用例
25+
26+
| 维度 ||
27+
| --- | --- |
28+
| 业务任务 | 自由文本地址解析为严格 JSON `{country, city, postal_code, street}`(缺信息字段输出 `null`|
29+
| 优化目标 | `agent/prompts/system.md` 单字段 |
30+
| 训练集 | 6 条 case:3 条完整地址 + 3 条缺信息地址 |
31+
| 验证集 | 6 条 case |
32+
33+
## 2 · 术语对照
34+
35+
仅列出本 example 引入的新概念。基础术语见 `quickstart/README.md` §2。
36+
37+
| 术语 | 含义 |
38+
| --- | --- |
39+
| **candidate_selection_strategy** | 反思每轮选哪个候选作为亲本的策略。可选 `pareto` / `current_best` / `epsilon_greedy` / `top_k_pareto`|
40+
| **frontier_type** | Pareto 前沿粒度。可选 `instance`(按 case) / `objective`(按 metric) / `hybrid`(双层) / `cartesian`(按 case×metric)。 |
41+
| **skip_perfect_score** | 反思 minibatch 抽样时是否跳过已满分的 case。 |
42+
| **predictor-level merge** | merge 操作在 prompt 字段层面进行。**需要至少 2 个字段才有意义**——单字段优化下 merge 永远不会触发。 |
43+
| **merge_val_overlap_floor** | 触发 merge 的最低 val 集 case 重叠数(默认 5)。 |
44+
45+
## 3 · 运行示例
46+
47+
### 3.1 安装依赖
48+
49+
```bash
50+
pip install -e ".[optimize]"
51+
```
52+
53+
### 3.2 配置环境变量
54+
55+
```bash
56+
export TRPC_AGENT_API_KEY="<your-key>"
57+
export TRPC_AGENT_BASE_URL="<your-endpoint>"
58+
export TRPC_AGENT_MODEL_NAME="<your-model>"
59+
```
60+
61+
### 3.3 顺序跑两次优化
62+
63+
```bash
64+
cd examples/optimization/advanced_strategies
65+
python3 run_baseline.py # 配置 A:basic 策略组合
66+
python3 run_advanced.py # 配置 B:高阶策略组合
67+
```
68+
69+
每次运行约 3 分钟。
70+
71+
### 3.4 输出对比表
72+
73+
```bash
74+
python3 compare.py
75+
```
76+
77+
`compare.py` 自动选取 `runs/` 下最新的 `baseline_*``advanced_*` 目录解析 `result.json`,输出多维度对比表(轮次数、接受率、merge 触发次数、reflection LM 调用数、baseline / best pass_rate 等)。
78+
79+
## 4 · 架构与数据流
80+
81+
```
82+
[run_baseline.py] [run_advanced.py]
83+
│ │
84+
├── optimizer_baseline.json ├── optimizer_advanced.json
85+
│ instance frontier │ objective frontier
86+
│ skip_perfect_score=false │ skip_perfect_score=true
87+
│ use_merge=false │ use_merge=true(单字段实际不触发)
88+
│ │
89+
└── runs/baseline_<ts>/result.json └── runs/advanced_<ts>/result.json
90+
91+
┌────────────┴────────────┐
92+
│ python3 compare.py │
93+
│ _latest("baseline") │
94+
│ _latest("advanced") │
95+
│ 解析 result.json │
96+
│ 输出对比表 │
97+
└─────────────────────────┘
98+
```
99+
100+
### 4.1 文件清单
101+
102+
| 文件 | 角色 | 接入自有业务时的修改方向 |
103+
| --- | --- | --- |
104+
| `run_baseline.py` | basic 配置入口 | 与 quickstart 同 |
105+
| `run_advanced.py` | 高阶配置入口 | 调整 `optimizer_advanced.json` 中策略组合 |
106+
| `compare.py` | 解析两次 `result.json` 输出对比表 | 添加 / 删除关注的对比维度 |
107+
| `agent/agent.py` | 地址解析 LlmAgent + `_normalize_json` | 替换为业务 agent |
108+
| `agent/prompts/system.md` | baseline prompt(故意极简) | 写入业务 baseline |
109+
| `optimizer_baseline.json` | basic 策略 JSON | 调整阈值与 metric |
110+
| `optimizer_advanced.json` | 高阶策略 JSON | 调整高阶开关 |
111+
| `data/train.evalset.json` / `data/val.evalset.json` | 数据集 | 替换为业务用例 |
112+
113+
## 5 · 高阶策略对照
114+
115+
### 5.1 配置差异速查
116+
117+
| 配置项 | baseline | advanced |
118+
| --- | --- | --- |
119+
| `candidate_selection_strategy` | `pareto` | `pareto` |
120+
| `frontier_type` | `instance` | `objective` |
121+
| `skip_perfect_score` | `false` | `true` |
122+
| `use_merge` | `false` | `true` |
123+
| `module_selector` | `round_robin` | `round_robin` |
124+
125+
### 5.2 `frontier_type` instance vs objective
126+
127+
| 取值 | 行为 | 在本任务上的表现 |
128+
| --- | --- | --- |
129+
| `instance` | 每条 case 维护一个 best 候选,反思看逐 case 反馈 | 接受门槛较高(需在某 case 上严格优于历史),rounds_accepted 较少 |
130+
| `objective` | 每条 metric 维护一个 best,反思看聚合分数 | 接受门槛较低(聚合分有提升即接受),rounds_accepted 较多但 valset 易震荡 |
131+
132+
`objective` 更激进。小训练集(< 10 case)下可能过拟合 train minibatch,造成 valset pass_rate 波动。
133+
134+
### 5.3 `skip_perfect_score` 的实际节省
135+
136+
理论上能减少不必要的 reflection LM 调用。实际节省幅度取决于:
137+
138+
- baseline 起点高度(baseline=0 时早期满分 case 极少,节省有限)
139+
- 训练集规模(小训练集下满分 case 在 minibatch 中比例不稳定)
140+
141+
本 example 实测约节省 1 次 reflection 调用,差异不显著。该开关在**大规模训练集 + 高基线起点**场景下效果更明显。
142+
143+
## 6 · 关键配置(含两条踩坑警示)
144+
145+
### 6.1 `use_merge` 在单字段优化下不会触发
146+
147+
merge 是 predictor-level 操作,**需要至少 2 个字段才有意义**。本 example 是单字段优化,因此 `optimizer_advanced.json``use_merge=true` 设置无副作用,但也不会带来任何实际 merge 行为——`compare.py` 输出中 `merge_rounds_total=0` 是预期。
148+
149+
需要观察 merge 实际效果时,参见 `multi_agent_pipeline/` example,其 4 字段配置下 merge 会真实触发。
150+
151+
### 6.2 `result.json` 字段命名为 camelCase
152+
153+
SDK 内部使用 snake_case 字段名(如 `stop_reason` / `total_rounds` / `best_pass_rate`),但序列化到 `result.json` 时会自动转换为 camelCase(`stopReason` / `totalRounds` / `bestPassRate`)。
154+
155+
这是因为 `EvalBaseModel``alias_generator=to_camel`,序列化时 `by_alias=True`
156+
157+
**踩坑提醒**:用 Python 读 `result.json` 时按 camelCase 索引:
158+
159+
```python
160+
data = json.loads(Path("result.json").read_text())
161+
print(data["bestPassRate"]) #
162+
print(data["best_pass_rate"]) # ❌ KeyError
163+
```
164+
165+
`compare.py` 中已经按 camelCase 解析;自有脚本读 `result.json` 时同样按此约定。
166+
167+
### 6.3 `frontier_type` 取值约束
168+
169+
SDK 仅接受以下 4 个字面量值:
170+
171+
```
172+
"instance" | "objective" | "hybrid" | "cartesian"
173+
```
174+
175+
其他取值(如 `"aggregate"` / `"mixed"`)会在 pydantic 层面直接 `ValidationError`,无法启动优化。配置前请确认拼写。
176+
177+
## 7 · 常见问题
178+
179+
**Q:为什么两次跑的 `best_pass_rate` 经常相同?**
180+
A:GEPA 是 Pareto 优化算法,在简单任务 + 充足预算下两套策略最终常收敛到同一最优。差异往往体现在**到达路径**(轮次数、接受率、merge 行为)而非最终分数。这正是本 example 设计 `compare.py` 关注多维度而非单一 `best_pass_rate` 的原因。
181+
182+
**Q:advanced 接受了 4 轮但 baseline 只接受了 2 轮,是不是 advanced 更好?**
183+
A:不一定。`objective` frontier 接受门槛低,可能"接受了一个 train 上更好但 val 上更差"的候选。需结合每轮的 `valset pass_rate` 趋势观察是否过拟合。
184+
185+
**Q:`compare.py` 输出 `merge_rounds_total=0` 但我开了 `use_merge=true`**
186+
A:单字段优化下符合预期。参见 §6.1。
187+
188+
**Q:怎么知道是哪一轮被接受的、是反思还是 merge?**
189+
A:`result.json``rounds[*]` 数组每条记录都有 `accepted: true/false``kind: "reflective" | "merge"` 字段,可直接遍历查看。
190+
191+
**Q:advanced 配置里 `seed` 应该和 baseline 保持一致吗?**
192+
A:保持一致便于对比时排除随机性影响。本 example 两份 JSON 都用同一 `seed`
193+
194+
## 8 · 接入自有业务的步骤
195+
196+
1. **复制本 example 作为对照模板**:保留 `run_baseline.py` / `run_advanced.py` / `compare.py` 三脚本结构
197+
2. **替换业务 agent**`agent/agent.py` 改为业务 agent 实现
198+
3. **设计两套配置 JSON**
199+
- `optimizer_baseline.json`:当前线上配置或默认配置
200+
- `optimizer_advanced.json`:希望验证的高阶组合
201+
- 二者保持 `seed` / `max_metric_calls` 一致以便公平对比
202+
4. **替换数据集**:业务 train / val
203+
5. **跑两次 + compare**:根据对比表多维度评估高阶配置在业务任务上的实际收益
204+
6. **决策**:把对比表中表现明显更优的配置作为生产配置
205+
206+
> 高阶配置不是"越复杂越好"。许多任务上 baseline 配置已能达到合理收敛,advanced 只在特定任务结构(多目标、多字段、大规模训练集等)下显示价值。**用数据决定,不用直觉**

examples/optimization/advanced_strategies/agent/__init__.py

Whitespace-only changes.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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 —— Advanced Strategies example 专用。
7+
8+
任务设计动机
9+
------------
10+
本 example 用于验证 GEPA 高阶策略组合(use_merge / frontier_type /
11+
skip_perfect_score 等)的真实效果。任务必须存在两个**互相牵制**的维度,
12+
才能逼出策略差异:
13+
14+
A. 完整地址(country/city/postal_code/street 都给到)→ 期望严格 JSON
15+
B. 缺信息地址(少 postal_code 或 street)→ 期望对应字段输出 null
16+
17+
候选 prompt 容易陷入两个局部最优:
18+
- 候选 P1 学会"严格 JSON"但所有字段都不给 null(缺信息时硬编一个)
19+
- 候选 P2 学会"该 null 就 null"但 JSON 格式偶尔崩
20+
21+
→ 多字段场景下 use_merge=true 能融合 P1/P2 各自掌握的子能力。
22+
→ frontier_type 选 instance vs objective 在这类任务上行为差异显著。
23+
24+
接入业务时改哪里
25+
----------------
26+
- 替换为业务任务 agent 与 prompt
27+
- 保留 _normalize_json 让 metric 走 text exact,CI 上更稳
28+
"""
29+
30+
from __future__ import annotations
31+
32+
import json
33+
import re
34+
import uuid
35+
from pathlib import Path
36+
37+
from trpc_agent_sdk.agents import LlmAgent
38+
from trpc_agent_sdk.models import LLMModel
39+
from trpc_agent_sdk.models import OpenAIModel
40+
from trpc_agent_sdk.runners import Runner
41+
from trpc_agent_sdk.sessions import InMemorySessionService
42+
from trpc_agent_sdk.types import Content
43+
from trpc_agent_sdk.types import GenerateContentConfig
44+
from trpc_agent_sdk.types import Part
45+
46+
from .config import get_model_config
47+
48+
49+
SYSTEM_PROMPT_PATH = Path(__file__).parent / "prompts" / "system.md"
50+
APP_NAME = "advanced_strategies_demo"
51+
52+
_JSON_OBJECT_RE = re.compile(r"\{.*\}", re.DOTALL)
53+
54+
55+
def _create_model() -> LLMModel:
56+
"""构建 OpenAI 兼容 chat 模型实例。"""
57+
api_key, base_url, model_name = get_model_config()
58+
return OpenAIModel(model_name=model_name, api_key=api_key, base_url=base_url)
59+
60+
61+
def _read_instruction() -> str:
62+
"""从磁盘重读 system.md。"""
63+
return SYSTEM_PROMPT_PATH.read_text(encoding="utf-8").strip()
64+
65+
66+
def create_agent() -> LlmAgent:
67+
"""构建一个使用当前磁盘 prompt 的新 LlmAgent 实例。"""
68+
return LlmAgent(
69+
name="address_parser",
70+
description="Parses free-text postal addresses into a strict JSON.",
71+
model=_create_model(),
72+
instruction=_read_instruction(),
73+
generate_content_config=GenerateContentConfig(
74+
temperature=0.1,
75+
top_p=0.9,
76+
max_output_tokens=256,
77+
),
78+
)
79+
80+
81+
def _normalize_json(raw: str) -> str:
82+
"""把 LLM 输出规范化成稳定 JSON 字符串。
83+
84+
与 ci_integration / blackbox_cli 完全相同的规范化逻辑:让
85+
final_response_avg_score(text.match=exact) 直接走精确匹配。
86+
"""
87+
text = (raw or "").strip()
88+
if not text:
89+
return ""
90+
match = _JSON_OBJECT_RE.search(text)
91+
if not match:
92+
return text
93+
try:
94+
parsed = json.loads(match.group(0))
95+
except json.JSONDecodeError:
96+
return text
97+
return json.dumps(parsed, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
98+
99+
100+
async def call_agent(query: str) -> str:
101+
"""框架回调:跑一次推理,输出经 _normalize_json 规范化。"""
102+
root = create_agent()
103+
session_service = InMemorySessionService()
104+
runner = Runner(
105+
app_name=APP_NAME,
106+
agent=root,
107+
session_service=session_service,
108+
)
109+
session_id = str(uuid.uuid4())
110+
user_id = "parser"
111+
await session_service.create_session(
112+
app_name=APP_NAME,
113+
user_id=user_id,
114+
session_id=session_id,
115+
state={},
116+
)
117+
user_content = Content(role="user", parts=[Part.from_text(text=query)])
118+
119+
final_text = ""
120+
async for event in runner.run_async(
121+
user_id=user_id,
122+
session_id=session_id,
123+
new_message=user_content,
124+
):
125+
if not event.is_final_response():
126+
continue
127+
if not event.content or not event.content.parts:
128+
continue
129+
for part in event.content.parts:
130+
if part.thought:
131+
continue
132+
if part.text:
133+
final_text += part.text
134+
return _normalize_json(final_text)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
"""模型凭据读取 —— 从环境变量加载 OpenAI 兼容 LLM 的连接信息。
7+
8+
需要的环境变量
9+
--------------
10+
TRPC_AGENT_API_KEY LLM 后端的 API key
11+
TRPC_AGENT_BASE_URL LLM 后端的 endpoint
12+
TRPC_AGENT_MODEL_NAME 模型名
13+
14+
缺任意一个就立即抛 ValueError,避免运行到一半才撞到 LLM 后端的 401 错误,
15+
那时报错信息会很有迷惑性(看起来像 prompt 写错了,实际是凭据没配)。
16+
"""
17+
18+
from __future__ import annotations
19+
20+
import os
21+
22+
23+
def get_model_config() -> tuple[str, str, str]:
24+
"""返回 (api_key, base_url, model_name);任一缺失立刻报错。"""
25+
api_key = os.getenv("TRPC_AGENT_API_KEY", "")
26+
base_url = os.getenv("TRPC_AGENT_BASE_URL", "")
27+
model_name = os.getenv("TRPC_AGENT_MODEL_NAME", "")
28+
if not api_key or not base_url or not model_name:
29+
raise ValueError(
30+
"运行优化器前必须配置环境变量 TRPC_AGENT_API_KEY / "
31+
"TRPC_AGENT_BASE_URL / TRPC_AGENT_MODEL_NAME。"
32+
)
33+
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+
You parse free-text postal addresses and return a JSON object.

0 commit comments

Comments
 (0)