Skip to content

Commit 43801a1

Browse files
taoisnieraychen911
authored andcommitted
feat: eval模块支持接入外部agent评估
TAPD: --story=134277610
1 parent aadf99c commit 43801a1

7 files changed

Lines changed: 1256 additions & 148 deletions

File tree

docs/mkdocs/en/evaluation.md

Lines changed: 172 additions & 68 deletions
Large diffs are not rendered by default.

docs/mkdocs/zh/evaluation.md

Lines changed: 173 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ tRPC-Agent 评测模块是一套**自动化 Agent 质量检验工具**。它让
6565
| 知识召回评估 | 评估 RAG 场景下检索到的知识是否足以支撑回答 | 验证知识库检索结果覆盖了问题中的关键事实 |
6666
| 多轮运行与统计 | 同一用例跑多次,计算 pass@k 等稳定性指标 | 评估 Agent 在多次尝试中的通过率 |
6767
| Trace 回放 | 跳过推理,直接用录制好的对话轨迹打分 | 用线上日志做离线评估,不消耗推理资源 |
68+
| 外部 Agent 评测 | 通过 `call_agent` 评测非本框架创建的 Agent(HTTP 服务、CLI、其他框架) | 对已有 Claude Code CLI 或远程 API 做回归评测 |
6869
| 回调钩子 | 在推理/打分的 8 个生命周期节点挂载自定义逻辑 | 打点、日志、采样、上报 |
6970

7071
#### 评测整体流程
@@ -283,6 +284,7 @@ pytest test_quickstart.py -v --tb=short -s
283284
- **有用例未达阈值**:框架会抛出 `AssertionError`,失败摘要以 JSON 形式包含在错误信息中。
284285
- **结果落盘**:若调用时传入 `eval_result_output_dir`,当次评测结果会写入该目录下的 `.evalset_result.json` 文件(详见[评测结果](#评测结果)一节)。
285286

287+
286288
---
287289

288290
### 核心概念
@@ -296,7 +298,7 @@ pytest test_quickstart.py -v --tb=short -s
296298
| **AgentEvaluator** | 对用户暴露的入口,提供 `evaluate()``get_executer()` | 在 pytest 测试中调用它 |
297299
| **评测集(EvalSet)** | 描述"测什么"——场景、用户输入、预期输出 | 编写 `.evalset.json` 文件 |
298300
| **评测配置(EvalConfig)** | 描述"怎么判"——用哪些指标、阈值、匹配规则 | 编写 `test_config.json` 文件 |
299-
| **评估服务(LocalEvalService)** | 执行推理与打分的引擎 | 框架自动创建,通常无需关心 |
301+
| **评估服务(LocalEvalService / RemoteEvalService** | 执行推理与打分的引擎(本地 Agent 或 `call_agent` | 框架自动创建,通常无需关心 |
300302
| **评估器(Evaluator)** | 按指标计算分数的具体实现 | 选择内置评估器,或注册自定义 |
301303
| **评估器注册表(EvaluatorRegistry)** | 维护 `metric_name` → 评估器类型的映射 | 需要自定义评估器时注册 |
302304
| **评测结果(EvaluateResult)** | 承载评测的结构化结果 | 通过 `get_result()` 获取并分析 |
@@ -305,12 +307,31 @@ pytest test_quickstart.py -v --tb=short -s
305307

306308
AgentEvaluator 是整个评测流程的入口和编排者:
307309

308-
1. **加载阶段**:AgentEvaluator 从评测集文件(`.evalset.json` / `.test.json`)加载 EvalSet,从同目录的 `test_config.json` 加载 EvalConfig,按 `agent_module` 加载 Agent(若整集为 [Trace 模式](#trace-模式),此步可省略)。
309-
2. **构建评估服务**:AgentEvaluator 将 EvalSet 写入 InMemoryEvalSetsManager,创建 LocalEvalService(依赖该 Manager、UserSimulatorProvider、可选 EvalSetResultsManager、Runner、Callbacks)。默认使用 StaticUserSimulator,按 conversation 的 user_content 驱动推理。可选注入 LocalEvalSetResultsManager 将运行结果写入目录
310-
3. **推理阶段**:评估服务按 EvalSet 中的用例与 conversation 驱动 Runner 推理,得到实际 Invocation 列表(实际工具调用、实际回复)
310+
1. **加载阶段**:AgentEvaluator 从评测集文件(`.evalset.json` / `.test.json`)加载 EvalSet,从同目录的 `test_config.json` 加载 EvalConfig;若走本地 Agent 路径,按 `agent_module` 加载 Agent(使用 `call_agent` 或整集为 [Trace 模式](#trace-模式),此步可省略)。
311+
2. **构建评估服务**:AgentEvaluator 将 EvalSet 写入 InMemoryEvalSetsManager;传 `call_agent` 时创建 RemoteEvalService,否则创建 LocalEvalService(依赖 Manager、UserSimulatorProvider、可选 EvalSetResultsManager、Runner、Callbacks)。
312+
3. **推理阶段**:评估服务按 EvalSet 的用例与 conversation 逐轮推理:LocalEvalService 通过 Runner 调 Agent;RemoteEvalService 通过 `call_agent(query)` 获取每轮实际回复,得到实际 Invocation 列表。
311313
4. **打分阶段**:评估服务根据 EvalConfig 中的 EvalMetric 列表,从 EvaluatorRegistry 获取各评估器,对实际与预期逐项打分并汇总为 EvalCaseResult。
312314
5. **结果汇总**:AgentEvaluator 根据结果判定通过/失败,有用例未达阈值时抛出 `AssertionError`,可选将结果落盘为 `.evalset_result.json`
313315

316+
#### AgentEvaluator 参数列表
317+
318+
`evaluate()``get_executer()` 接受相同的参数(`evaluate()` 内部调用 `get_executer()`):
319+
320+
| 参数 | 类型 | 说明 |
321+
| --- | --- | --- |
322+
| eval_dataset_file_path_or_dir | str | 评测集文件或目录路径(递归扫描 `.evalset.json` / `.test.json`|
323+
| agent_module | str \| None | 本框架 Agent 所在 Python 模块路径;与 `call_agent` 互斥。传 `call_agent` 时不需要;全部 case 为 Trace 模式时也不需要 |
324+
| call_agent | CallAgent \| None | 非本框架 Agent 的异步可调用对象(`async def(str)->str`);与 `agent_module` / `runner` 互斥 |
325+
| num_runs | int | 每个评测集运行次数,默认 1 |
326+
| agent_name | str \| None | Agent 显示名称 |
327+
| print_detailed_results | bool | 是否打印每个用例的详细对比信息,默认 True |
328+
| eval_result_output_dir | str \| None | 结果落盘目录;不传则仅内存聚合 |
329+
| runner | Runner \| None | 自定义 Runner 实例;与 `call_agent` 互斥 |
330+
| case_parallelism | int \| None | 推理阶段最大并发用例数 |
331+
| case_eval_parallelism | int \| None | 打分阶段最大并发用例数 |
332+
| callbacks | Callbacks \| None | 生命周期回调 |
333+
| eval_metrics_file_path_or_dir | str \| None | 共享评测配置文件路径(覆盖同目录 `test_config.json`|
334+
314335
---
315336

316337
### 评测集(EvalSet)编写指南
@@ -455,6 +476,8 @@ Trace 模式的配置详见[高级功能 - Trace 模式](#trace-模式)。
455476
| `llm_rubric_response` | LLMRubricResponseEvaluator | LLM 裁判按评估细则逐项打分 | 需要从多个维度(正确性、相关性、合规性等)评估回复质量 |
456477
| `llm_rubric_knowledge_recall` | LLMRubricKnowledgeRecallEvaluator | LLM 裁判评估知识检索结果是否足以支撑回答 | RAG 场景,需验证检索到的知识覆盖了关键事实 |
457478

479+
> 注意:`call_agent` 模式不支持 `tool_trajectory_avg_score`。评测外部黑盒 Agent 时,建议优先使用 `final_response_avg_score` 或 LLM Judge 类指标。
480+
458481
**Rubric** 指评估细则:在配置中以 `rubrics` 数组列出多条可独立判定的条款(如「回答须包含结论」「须与问题相关」),LLM 裁判对每条细则给出通过与否,再汇总为该项指标的得分。
459482

460483
#### 如何选择指标
@@ -816,70 +839,7 @@ LLM 最终响应评判(仅需 judge_model):
816839

817840
建议 `api_key``base_url` 用环境变量占位(如 `${TRPC_AGENT_API_KEY}`),由执行环境替换,避免明文写入配置文件。
818841

819-
**多裁判模型(跨模型聚合)**
820-
821-
同一个 LLM 裁判指标可以同时使用多个裁判模型,并通过 `models_aggregator` 聚合各模型的判定结果。此时改用 `judge_models` 而非 `judge_model`,两字段互斥。每个裁判模型的明细会输出到 `PerInvocationResult.per_model_scores``NamedScoreResult` 列表)。
822-
823-
内置聚合器:
824-
825-
| 名称 | 通过规则 | 总分 |
826-
| --- | --- | --- |
827-
| `all_pass`(默认) | 所有模型都通过 | 各模型得分的最小值 |
828-
| `any_pass` | 任一模型通过 | 各模型得分的最大值 |
829-
| `majority_pass` | 严格多数通过(`passed*2 > total`| `passed_count / total` |
830-
| `avg` | 平均分 ≥ threshold | 各模型得分的平均值 |
831-
| `weighted_avg` | 加权平均 ≥ threshold | `sum(w*s) / sum(w)` |
832-
| `weighted_majority` | 通过模型的权重占比 ≥ 0.5 | `sum(w where passed) / sum(w)` |
833-
834-
若某个裁判模型执行抛异常,则该模型视为一张反对票;若所有模型都抛异常,该轮结果记为 `NOT_EVALUATED`
835-
836-
```json
837-
{
838-
"metrics": [
839-
{
840-
"metric_name": "llm_final_response",
841-
"threshold": 1,
842-
"criterion": {
843-
"llm_judge": {
844-
"judge_models": [
845-
{
846-
"model_name": "glm-4.7",
847-
"api_key": "${TRPC_AGENT_API_KEY}",
848-
"base_url": "${TRPC_AGENT_BASE_URL}",
849-
"weight": 2.0
850-
},
851-
{
852-
"model_name": "gpt-4o",
853-
"api_key": "${TRPC_AGENT_API_KEY}",
854-
"base_url": "${TRPC_AGENT_BASE_URL}",
855-
"weight": 1.0
856-
}
857-
],
858-
"models_aggregator": "weighted_avg",
859-
"parallel": true
860-
}
861-
}
862-
}
863-
]
864-
}
865-
```
866-
867-
`parallel` 控制多个裁判模型之间的执行方式:`true`(默认)并发调用,耗时取决于最慢的模型;`false` 按声明顺序串行调用。仅在 `judge_models` 有多个模型时生效。
868-
869-
若裁判模型默认开启思考链,建议在对应 `JudgeModelOptions` 上显式设 `"think": false`:judge 输出本身是结构化 JSON,思考链对最终判分无价值,关闭可显著降低 token 消耗与延时。每个裁判模型的 `think` 独立设置。
870-
871-
也可以在运行时注册自定义聚合器,其优先级高于 criterion 中写的 `models_aggregator` 名:
872-
873-
```python
874-
from trpc_agent_sdk.evaluation import LLM_EVALUATOR_REGISTRY, ScoreResult
875-
876-
def my_aggregator(per_model, threshold, weights):
877-
# per_model: list[ScoreResult];weights: list[float]
878-
score = sum(s.score or 0.0 for s in per_model) / len(per_model)
879-
return ScoreResult(score=score, reason="custom aggregation")
880-
881-
LLM_EVALUATOR_REGISTRY.register_models_aggregator("llm_final_response", my_aggregator)
882-
```
842+
> 同一个 LLM 裁判指标还可以同时使用多个裁判模型并聚合结果,详见[高级功能 - 多裁判模型(跨模型聚合)](#多裁判模型跨模型聚合)
883843
884844
#### 自定义准则
885845

@@ -1819,10 +1779,155 @@ async def test_pass_at_k():
18191779

18201780
完整示例见 [examples/evaluation/pass_at_k/](../../../examples/evaluation/pass_at_k/)
18211781

1782+
#### 评测非本框架创建的 Agent(call_agent)
1783+
1784+
若被测 Agent 不是通过本框架创建和管理的(例如部署在 HTTP/RPC 服务后面、通过 CLI 调用、或使用其他框架封装),无法提供 `agent_module``runner`,可改用 **`call_agent`** 参数:传入一个异步函数,evaluator 会在每轮对话中调用它获取实际回复,其余打分流程不变。
1785+
1786+
**配置方式**
1787+
1788+
**AgentEvaluator.evaluate()****get_executer()** 中传入 `call_agent=your_async_fn`,不传 `agent_module``runner``call_agent` 的签名必须是 `async def call_agent(query: str) -> str`
1789+
1790+
**适用场景**
1791+
1792+
评测任何无法实例化为本框架 `BaseAgent` 的可调用对象:HTTP/RPC 远程服务、CLI Agent、其他框架(LangChain / AutoGen / 自研)封装的黑盒 Agent 等。
1793+
1794+
**约束**
1795+
1796+
- `call_agent` 必须是异步函数(传入同步函数会报 `ValueError`
1797+
- `call_agent``agent_module` / `runner` 互斥(同时传入会报 `ValueError`
1798+
- `call_agent` 模式与 Trace 模式互斥(evalset 含 trace case 会报 `ValueError`
1799+
- `call_agent` 模式不支持 `tool_trajectory_avg_score`(会报 `ValueError`);建议使用 `final_response_avg_score``llm_final_response``llm_rubric_response`
1800+
- 多轮 case 会按轮次依次调用 `call_agent`;每次调用对应一个 `Invocation`
1801+
1802+
**示例**:以 Claude Code CLI 为例,将其封装为 `call_agent` 并接入评测
1803+
1804+
```python
1805+
import asyncio
1806+
import os
1807+
from asyncio.subprocess import PIPE
1808+
1809+
from trpc_agent_sdk.evaluation import AgentEvaluator
1810+
1811+
1812+
async def call_agent(query: str) -> str:
1813+
"""调用 Claude Code CLI,返回其文本输出。"""
1814+
cli_bin = os.getenv("CLAUDE_CODE_BIN", "claude")
1815+
cli_args = [cli_bin, "-p", query]
1816+
1817+
model_name = os.getenv("CLAUDE_CODE_MODEL")
1818+
if model_name:
1819+
cli_args.extend(["--model", model_name])
1820+
1821+
proc = await asyncio.create_subprocess_exec(*cli_args, stdout=PIPE, stderr=PIPE)
1822+
stdout, stderr = await proc.communicate()
1823+
1824+
if proc.returncode != 0:
1825+
raise RuntimeError(stderr.decode("utf-8", errors="ignore").strip())
1826+
1827+
output_text = stdout.decode("utf-8", errors="ignore").strip()
1828+
for line in output_text.splitlines():
1829+
if line.strip():
1830+
return line.strip()
1831+
return ""
1832+
1833+
1834+
# 方式 A:只关心 pass/fail
1835+
await AgentEvaluator.evaluate(
1836+
eval_dataset_file_path_or_dir="agent/my_evalset.evalset.json",
1837+
call_agent=call_agent,
1838+
)
1839+
1840+
# 方式 B:需要结构化结果
1841+
executer = AgentEvaluator.get_executer(
1842+
eval_dataset_file_path_or_dir="agent/my_evalset.evalset.json",
1843+
call_agent=call_agent,
1844+
)
1845+
await executer.evaluate()
1846+
result = executer.get_result() # EvaluateResult
1847+
```
1848+
1849+
> 示例中默认命令是 `claude`。如果你环境里的可执行文件名不同(例如 `trpc-claudecode` 或自定义命令),将 `CLAUDE_CODE_BIN` 环境变量改为对应命令即可。对于 HTTP 服务场景,只需把 `call_agent` 函数体改为 `aiohttp` / `httpx` 调用,签名保持 `async def call_agent(query: str) -> str` 不变。
1850+
1851+
#### 多裁判模型(跨模型聚合)
1852+
1853+
同一个 LLM 裁判指标可以同时使用多个裁判模型,并通过 `models_aggregator` 聚合各模型的判定结果,降低单模型裁判的波动。此时改用 `judge_models` 而非 `judge_model`,两字段互斥。每个裁判模型的明细会输出到 `PerInvocationResult.per_model_scores``NamedScoreResult` 列表)。
1854+
1855+
**配置方式**
1856+
1857+
`test_config.json` 的 LLM 裁判类指标 `criterion.llm_judge` 中,将 `judge_model` 替换为 `judge_models`(数组),并设置 `models_aggregator` 选择聚合策略。`parallel` 控制多个裁判模型之间的执行方式:`true`(默认)并发调用,`false` 串行调用。
1858+
1859+
**适用场景**
1860+
1861+
对评判结果要求更高置信度(如安全合规、医疗场景),或希望对比不同裁判模型的判定差异。
1862+
1863+
**内置聚合器**
1864+
1865+
| 名称 | 通过规则 | 总分 |
1866+
| --- | --- | --- |
1867+
| `all_pass`(默认) | 所有模型都通过 | 各模型得分的最小值 |
1868+
| `any_pass` | 任一模型通过 | 各模型得分的最大值 |
1869+
| `majority_pass` | 严格多数通过(`passed*2 > total`| `passed_count / total` |
1870+
| `avg` | 平均分 ≥ threshold | 各模型得分的平均值 |
1871+
| `weighted_avg` | 加权平均 ≥ threshold | `sum(w*s) / sum(w)` |
1872+
| `weighted_majority` | 通过模型的权重占比 ≥ 0.5 | `sum(w where passed) / sum(w)` |
1873+
1874+
若某个裁判模型执行抛异常,则该模型视为一张反对票;若所有模型都抛异常,该轮结果记为 `NOT_EVALUATED`
1875+
1876+
**示例**:两个裁判模型按加权平均聚合
1877+
1878+
```json
1879+
{
1880+
"metrics": [
1881+
{
1882+
"metric_name": "llm_final_response",
1883+
"threshold": 1,
1884+
"criterion": {
1885+
"llm_judge": {
1886+
"judge_models": [
1887+
{
1888+
"model_name": "glm-4.7",
1889+
"api_key": "${TRPC_AGENT_API_KEY}",
1890+
"base_url": "${TRPC_AGENT_BASE_URL}",
1891+
"weight": 2.0
1892+
},
1893+
{
1894+
"model_name": "gpt-4o",
1895+
"api_key": "${TRPC_AGENT_API_KEY}",
1896+
"base_url": "${TRPC_AGENT_BASE_URL}",
1897+
"weight": 1.0
1898+
}
1899+
],
1900+
"models_aggregator": "weighted_avg",
1901+
"parallel": true
1902+
}
1903+
}
1904+
}
1905+
]
1906+
}
1907+
```
1908+
1909+
若裁判模型默认开启思考链,建议在对应 `JudgeModelOptions` 上显式设 `"think": false`:judge 输出本身是结构化 JSON,思考链对最终判分无价值,关闭可显著降低 token 消耗与延时。
1910+
1911+
**自定义聚合器**
1912+
1913+
也可以在运行时注册自定义聚合器,其优先级高于 criterion 中写的 `models_aggregator` 名:
1914+
1915+
```python
1916+
from trpc_agent_sdk.evaluation import LLM_EVALUATOR_REGISTRY, ScoreResult
1917+
1918+
def my_aggregator(per_model, threshold, weights):
1919+
score = sum(s.score or 0.0 for s in per_model) / len(per_model)
1920+
return ScoreResult(score=score, reason="custom aggregation")
1921+
1922+
LLM_EVALUATOR_REGISTRY.register_models_aggregator("llm_final_response", my_aggregator)
1923+
```
1924+
18221925
#### Trace 模式
18231926

18241927
默认模式下,评估服务会真实调用 Agent 做推理。若你已有录制好的对话轨迹(如线上日志、历史会话),希望只做「打分」、不重复推理,可使用 **Trace 模式**:在用例上设置 **eval_mode: "trace"** 并提供 **actual_conversation**,评估服务会跳过推理,直接使用该轨迹参与打分。
18251928

1929+
> 注意:Trace 模式与 `call_agent` 模式互斥;传入 `call_agent` 且评测集中包含 trace case 时,框架会在启动期抛出 `ValueError`
1930+
18261931
**配置方式**
18271932

18281933
-**EvalCase** 上设置 **eval_mode**: `"trace"`

0 commit comments

Comments
 (0)