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
1 change: 1 addition & 0 deletions examples/optimization/eval_optimize_loop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
runs/
9 changes: 9 additions & 0 deletions examples/optimization/eval_optimize_loop/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 方案设计说明

流水线把评测器作为唯一判分入口。baseline prompt 先在训练集和验证集各运行一次,保存每条 case 的指标、阈值、通过状态、裁判理由及实际/预期轨迹。失败归因采用可解释的规则优先级:先看执行错误,再结合 metric 名称、judge reason、工具轨迹和响应内容,区分参数错误、工具调用错误、知识召回不足、LLM rubric 不达标、格式违规与最终回复不匹配;无法细分时回退到最终回复不匹配,保证每个失败 case 都有原因。

优化层支持真实 `AgentOptimizer` 和确定性 fake 后端。两者只负责生成候选,不直接决定生产回写。每轮候选均重新执行训练集和隔离验证集,并按 case 标记新增通过、新增失败、分数提升、分数下降。这样既能看到总体分变化,也能识别被平均分掩盖的局部退化。

接受策略采用可配置 AND gate:验证总分与通过率需达到最小增益,不能新增 hard fail,关键 case 不得退化,新增普通失败数受限,训练增益与验证增益之差不得超过阈值,成本也必须在预算内。训练提升而验证下降会同时触发总分、关键 case 或过拟合差值检查,从而拒绝候选。训练/验证 case ID 强制隔离,进一步避免数据泄漏。

审计目录保存 baseline、每轮完整 prompt、原始 evaluator 结果、trace 输入、逐 case delta 和 gate 理由。最终 JSON 与 Markdown 还记录随机种子、输入路径及 SHA-256、调用次数、估算成本、开始结束时间和耗时。默认不回写源 prompt;仅显式请求且外层 gate 接受时原子更新,异常或拒绝均恢复 baseline,保证实验可复现、可追责、可回滚。
113 changes: 113 additions & 0 deletions examples/optimization/eval_optimize_loop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Evaluation + Optimization 自动闭环

本示例把 `AgentEvaluator` 与 `AgentOptimizer`(或可插拔的离线等价后端)
编排为可复现的“基线评测 → 失败归因 → prompt 候选 → 验证集回归 →
接受决策 → 审计落盘”闭环。默认配置不需要 API Key,并刻意包含一轮可接受
优化和一轮训练集提升、验证集退化的过拟合优化。

## 快速运行

在仓库根目录执行:

```bash
python examples/optimization/eval_optimize_loop/run_pipeline.py \
--run-id offline-demo
```

默认结果写入 `runs/<timestamp>/`。指定固定目录便于 CI 收集:

```bash
python examples/optimization/eval_optimize_loop/run_pipeline.py \
--run-id ci-regression \
--output-dir /tmp/eval-optimize-loop
```

fake model、确定性 fake judge 和 trace mode 下只执行 18 次本地调用,通常在
数秒内完成,远低于 3 分钟验收限制。

## 输入

| 文件 | 作用 |
| --- | --- |
| `train.evalset.json` | 3 条训练 case,供失败分析和候选生成 |
| `val.evalset.json` | 3 条隔离验证 case,只用于回归和 gate |
| `optimizer.json` | 评测指标、优化后端、随机种子、成本和接受策略 |
| `prompts/system.md` | baseline `TargetPrompt` 源文件 |

公开 case 覆盖三类结果:

| 类型 | Case |
| --- | --- |
| 可优化成功 | `train_tool_weather`、`train_format_invoice`、`val_router_calendar` |
| 优化无效 | `train_knowledge_unavailable` |
| 优化后退化 | `val_stable_math`、`val_critical_safety`(过拟合候选) |

训练集与验证集的 case ID 必须完全隔离,否则 pipeline 启动时 fail-fast。
非法 `execution_mode`,或 `agent_optimizer` 后端缺少 `optimize` 配置,也会在
任何审计文件写入前被拒绝。

## 执行路径

默认 `pipeline.execution_mode=trace`。`FakePromptModel` 读取当前候选 prompt,
生成确定性响应,pipeline 将响应写成标准 `actualConversation`,随后由
`AgentEvaluator` 的 trace mode 完成打分。`final_response_avg_score` 的 exact
matcher 充当离线 fake judge,原始 trace 和结构化 evaluator 结果都会落盘。

将 `execution_mode` 改为 `call_agent` 可直接验证
`AgentEvaluator.get_executer(call_agent=...)` 路径。将
`optimizer_backend` 改为 `agent_optimizer` 并配置
`TRPC_AGENT_MODEL_NAME`、`TRPC_AGENT_BASE_URL`、`TRPC_AGENT_API_KEY` 后,
pipeline 会调用:

```python
await AgentOptimizer.optimize(
...,
update_source=False,
)
```

`AgentOptimizer` 只负责搜索候选;最终是否回写仍由独立验证 gate 决定。

## 接受策略

`optimizer.json > pipeline.gate` 同时检查:

1. 验证集总分和 pass rate 达到最小提升;
2. 不允许新增 hard fail;
3. 关键 case 不允许分数退化;
4. 新增普通失败数不超过上限;
5. 训练增益与验证增益之差不超过过拟合阈值;
6. 候选优化和回归成本不超过预算。

所有检查按 AND 语义执行并默认 fail closed;成本预算缺失、为负数或非有限值
时会直接拒绝候选。示例中
`balanced_candidate` 通过,`overfit_candidate` 因验证分下降、关键安全
case 退化和训练/验证增益差过大而被拒绝。

## 输出

每次运行会生成:

```text
<output_dir>/
├── optimization_report.json
├── optimization_report.md
├── audit/
│ ├── evaluation_config.json
│ └── resolved_optimizer_config.json
├── candidates/<round>_<candidate>/system_prompt.md
└── evaluations/<baseline-or-candidate>/<dataset>/
├── evaluate_result.json
├── <dataset>.trace.evalset.json
└── raw/
```

报告包含 baseline/candidate 分数、逐 case delta、失败归因、每轮 prompt、
gate 检查和理由、调用成本、耗时、随机种子、输入路径及 SHA-256。仓库中的
`optimization_report.json` 是默认 fake/trace 配置的示例输出。未显式指定
`--run-id` 时使用 UTC 微秒时间和唯一后缀;JSON 与 Markdown 报告均原子落盘。

默认不会修改源 prompt。只有显式传入 `--update-source` 且所选候选通过全部
gate 时才原子回写;候选被拒绝或运行异常时源文件恢复为 baseline。

方案细节见 [DESIGN.md](DESIGN.md)。
67 changes: 67 additions & 0 deletions examples/optimization/eval_optimize_loop/fake_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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.
"""Deterministic offline model used by the evaluation-optimization example."""

from __future__ import annotations

import re
from pathlib import Path

_RULE_RE = re.compile(r"\[RULE:([A-Z0-9_]+)\]")


class FakePromptModel:
"""Small prompt-sensitive model with deterministic, auditable behavior.

The model deliberately covers successful optimization, ineffective
optimization, and overfitting. It is not a replacement for a production
model. Its purpose is to make the complete pipeline runnable without an
API key while still making candidate prompts affect evaluation outcomes.
"""

def __init__(self, prompt_path: Path) -> None:
self._prompt_path = prompt_path
self.call_count = 0

def _rules(self) -> set[str]:
prompt = self._prompt_path.read_text(encoding="utf-8")
return set(_RULE_RE.findall(prompt))

async def respond(self, query: str) -> str:
"""Return a deterministic response based on the active prompt rules."""
self.call_count += 1
rules = self._rules()
query_lower = query.lower()

if "weather_lookup" in query_lower:
if "USE_REQUESTED_TOOL" in rules:
return '{"city":"Shenzhen","tool":"weather_lookup"}'
return "TOOL_ERROR: weather_lookup was not called"

if "invoice 128 cny" in query_lower:
if "STRICT_JSON" in rules:
return '{"amount":128,"currency":"CNY"}'
return "FORMAT_ERROR: amount is 128 CNY"

if "project atlas internal launch code" in query_lower:
return "KNOWLEDGE_MISS: no grounded source contains the launch code"

if "route this calendar request" in query_lower:
if "ROUTE_CALENDAR" in rules:
return "route=calendar"
return "ROUTER_MISMATCH: route=general"

if "what is 2 + 2" in query_lower:
if "ANSWER_ALL" in rules:
return "The answer is 4."
return "4"

if "destructive shell command" in query_lower:
if "ANSWER_ALL" in rules and "PRESERVE_SAFETY" not in rules:
return "rm -rf /"
return "I cannot provide destructive commands."

return "FINAL_RESPONSE_MISMATCH: unsupported request"
Loading
Loading