开源 AI 工具的性能基准测试:如何设计可复现的 Agent 延迟与吞吐评测 开源 AI 工具的性能基准测试如何设计可复现的 Agent 延迟与吞吐评测一、Agent 性能被夸大的数字为什么你看的基准测试和实际体验差了一个数量级开源 AI Agent 项目的 README 里经常能看到这样的宣称单轮工具调用延迟 800ms、支持 50 并发 Agent 任务。但当你把这个工具跑在自己的机器上时延迟变成 3 秒10 个并发就开始超时。问题出在哪不是项目在说谎而是基准测试的条件和你的生产环境完全不同。Agent 的性能受太多变量影响LLM API 的响应速度、工具链的复杂度、上下文窗口的长度、以及最重要的——任务的不确定性。一个规划型的 Agent 可能在第 3 步才意识到第 1 步的结果不对回退重新执行。这些计划变更带来的延迟在简单的 benchmark 中完全不会体现。设计的基准测试必须回答的是在给定的硬件、模型和任务复杂度下Agent 的真实延迟分布P50/P95/P99和并发吞吐能力是多少。而要做到这一点benchmark 本身必须是可复现的——任何硬编码的 API Key、模型温度参数、时间相关的逻辑都会破坏可复现性。二、可复现基准测试的三要素隔离环境、确定性输入与统计严格的报告graph LR A[测试用例定义] -- B[环境隔离] B -- C[预热阶段] C -- D[正式测试] D -- E[统计汇总] E -- F[报告生成] subgraph 可复现保障 G[固定模型版本/温度0] -- D H[固定 API Key/限流配额] -- D I[多次重复取中位数] -- E end一个可复现的 Agent benchmark 必须满足三个硬条件环境隔离。每次测试必须从相同的初始状态开始——清空的所有缓存、相同的数据库快照、相同的工具响应 mock。如果不做隔离上次测试的缓存命中会让第二次测试的延迟数据失真。确定性输入。LLM 的temperature必须设为 0且测试用例必须包含具体的输入文本而非开放式问题。如果 benchmark 依赖 LLM 的随机性做语义判断你的 P95 数据就会像一个随机游走的股票——今天 2 秒明天 5 秒毫无统计意义。统计严格性。单次运行的数据没有任何价值。每个测试用例至少需要跑 5 次取 P50、P95 和 stddev 形成报告。Benchmark 的输出不是一个数字而是一个分布。三、Agent Benchmark 框架的工程实现以下是一个完整的 Agent benchmark 框架使用 Python asyncio 实现支持工具调用的 mock 和统计报告生成。# benchmark/runner.py — Agent 基准测试引擎 import asyncio import json import time import statistics from dataclasses import dataclass, field from typing import Callable, Any dataclass class BenchmarkCase: 单个测试用例 id: str name: str input: str # 确定性输入 expected_tool_calls: list[str] # 预期的工具调用序列用于验证正确性 timeout_seconds: float 30.0 dataclass class RunResult: case_id: str success: bool latency_ms: float tool_calls_made: int tokens_used: int error: str dataclass class BenchmarkReport: case_id: str total_runs: int success_rate: float latency_p50_ms: float latency_p95_ms: float latency_p99_ms: float latency_stddev_ms: float avg_tokens: float avg_tool_calls: float class AgentBenchmark: WARMUP_RUNS 2 # 预热轮次不计入统计 MEASURED_RUNS 5 # 有效测试轮次 def __init__(self, run_agent: Callable[[str], Any]): self.run_agent run_agent self.results: dict[str, list[RunResult]] {} async def run(self, cases: list[BenchmarkCase]) - list[BenchmarkReport]: reports [] for case in cases: print(f\n Running: {case.name} ) runs: list[RunResult] [] # 预热 for i in range(self.WARMUP_RUNS): print(f Warmup {i 1}/{self.WARMUP_RUNS}...) await self._run_single(case) await asyncio.sleep(1) # 避免触发 API 限流 # 正式测试 for i in range(self.MEASURED_RUNS): print(f Measured run {i 1}/{self.MEASURED_RUNS}...) result await self._run_single(case) runs.append(result) await asyncio.sleep(1) self.results[case.id] runs reports.append(self._generate_report(case, runs)) return reports async def _run_single(self, case: BenchmarkCase) - RunResult: start time.monotonic() try: agent_result await asyncio.wait_for( self.run_agent(case.input), timeoutcase.timeout_seconds, ) elapsed (time.monotonic() - start) * 1000 return RunResult( case_idcase.id, successTrue, latency_mselapsed, tool_calls_madelen(agent_result.get(tool_calls, [])), tokens_usedagent_result.get(total_tokens, 0), ) except asyncio.TimeoutError: elapsed (time.monotonic() - start) * 1000 return RunResult( case_idcase.id, successFalse, latency_mselapsed, tool_calls_made0, tokens_used0, errortimeout, ) except Exception as e: elapsed (time.monotonic() - start) * 1000 return RunResult( case_idcase.id, successFalse, latency_mselapsed, tool_calls_made0, tokens_used0, errorstr(e), ) def _generate_report( self, case: BenchmarkCase, runs: list[RunResult] ) - BenchmarkReport: successes [r for r in runs if r.success] latencies sorted([r.latency_ms for r in successes]) if not latencies: return BenchmarkReport( case_idcase.id, total_runslen(runs), success_rate0.0, latency_p50_ms0, latency_p95_ms0, latency_p99_ms0, latency_stddev_ms0, avg_tokens0, avg_tool_calls0, ) return BenchmarkReport( case_idcase.id, total_runslen(runs), success_ratelen(successes) / len(runs), latency_p50_msself._percentile(latencies, 50), latency_p95_msself._percentile(latencies, 95), latency_p99_msself._percentile(latencies, 99), latency_stddev_msstatistics.stdev(latencies) if len(latencies) 1 else 0, avg_tokensstatistics.mean([r.tokens_used for r in successes]), avg_tool_callsstatistics.mean([r.tool_calls_made for r in successes]), ) def _percentile(self, sorted_values: list[float], p: float) - float: if not sorted_values: return 0.0 k (len(sorted_values) - 1) * (p / 100.0) f int(k) c k - f if f 1 len(sorted_values): return sorted_values[f] c * (sorted_values[f 1] - sorted_values[f]) return sorted_values[f] def generate_markdown_report(self, reports: list[BenchmarkReport]) - str: lines [# Agent 性能基准测试报告\n] lines.append(f测试日期: {time.strftime(%Y-%m-%d)}\n) lines.append(| 用例 | 成功率 | P50 | P95 | P99 | StdDev | Tokens | 工具调用 |) lines.append(|------|--------|-----|-----|-----|--------|--------|----------|) for r in reports: lines.append( f| {r.case_id} | {r.success_rate:.0%} | f{r.latency_p50_ms:.0f}ms | {r.latency_p95_ms:.0f}ms | f{r.latency_p99_ms:.0f}ms | {r.latency_stddev_ms:.0f}ms | f{r.avg_tokens:.0f} | {r.avg_tool_calls:.1f} | ) return \n.join(lines)使用示例定义测试用例# benchmark/cases.py BENCHMARK_CASES [ BenchmarkCase( idsingle-tool, name单工具调用查天气, input北京今天天气怎么样, expected_tool_calls[get_weather], timeout_seconds15, ), BenchmarkCase( idmulti-tool, name多工具链式调用查天气-发邮件, input查一下北京天气如果下雨就给我发邮件提醒带伞, expected_tool_calls[get_weather, send_email], timeout_seconds30, ), BenchmarkCase( idcode-gen, name代码生成型任务, input用 Python 写一个快速排序并解释时间复杂度, expected_tool_calls[write_file, search_docs], timeout_seconds45, ), BenchmarkCase( iderror-recovery, name错误恢复工具调用失败后的重试, input搜索最新关于 MCP 协议的论文, expected_tool_calls[web_search], timeout_seconds30, ), ]四、Benchmark 的常见陷阱与统计误区P50 陷阱。许多 benchmark 只报告平均延迟。但平均延迟在非正态分布下毫无意义——如果 80% 的请求在 500ms 完成、剩余 20% 在 5 秒完成平均延迟可能是 1.4 秒。用户感受到的不是平均值而是在那 20% 的慢请求上感受到的卡顿。这就是为什么 P95 和 P99 比平均值重要得多。预热干扰。如果不做预热第一次测试的延迟会异常高——模型服务的冷启动、代码的 JIT 编译、数据库连接的建立都会让第一次请求的延迟失真。预热轮次不计入报告但必须量化预热的次数并记录在报告中。限流影响被低估。大多数 LLM API 都有每分钟请求数RPM和每分钟 Token 数TPM的限制。在 benchmark 中加入await asyncio.sleep(1)不是为了测试真时性能而是为了避免限流导致的数据失真。如果 benchmark 的目标是测试不计成本的最高吞吐那可以不间隔如果目标是对标生产环境间隔是必要的。用例的不完备性。一套 benchmark 覆盖 5 个用例是不够的。Agent 的行为在不同任务类型之间有质的变化——代码生成和天气查询的性能特征完全不同。Benchmark 的用例设计应该沿着 Agent 的工具调用数和推理步骤数两个维度展开形成矩阵而不是随意挑几个话题。五、总结Agent 基准测试的核心目标是获得可复现、有统计意义的性能数据。三个关键设计原则确定性输入temperature0、环境隔离每次测试前重置状态、多次重复取分位数P50/P95/P99 而非平均值。将 benchmark 集成到 CI 中是质量保障的关键——每次发版前跑一套固定的 Agent benchmark 用例对比上一次 Release 的报告看有没有劣化。一个不到 200 行的 benchmark 框架足以支撑大多数开源 AI 项目的性能基线建设。复杂度不应该成为你不做 benchmark 的借口——少即是多从 5 个核心用例开始比设计一个完美的测试矩阵更重要。