1. 智能体上线前为什么必须做性能评估
智能体(AI Agent)和普通聊天机器人最大的区别,是它会自己规划步骤、调用工具、根据返回结果决定下一步。这意味着它可能“歪打正着把任务做完了,但中间执行了危险命令”,也可能“卡在工具死循环里把 Token 额度烧光”。如果你只盯着最终答案对不对,这两类问题都会被掩盖。
我在实际项目里踩过最典型的坑:一个查报销的 Agent 在测试集上成功率 92%,上线后第一周就出现单次会话消耗 4 万 Token 的异常。回放轨迹才发现,工具返回日期格式错误后,它没有修正参数,而是把同一个错误请求原样重试了 11 次。最终答案碰巧是对的,但成本已经失控。
所以智能体性能评估要同时看三类核心指标,这也是 AgentOps 视角下最实用的拆解方式:
| 指标类别 | 具体指标 | 回答的问题 |
|---|---|---|
| 任务成功率 | Pass@1、Pass@k、子目标完成度 | 它到底有没有把事办成 |
| 延迟 | 端到端耗时、单步耗时、首 Token 延迟 | 用户等不等得起 |
| Token 消耗 | 单任务总 Token、上下文膨胀斜率、单任务成本 | 跑一次要花多少钱 |
这三类指标必须一起采集,缺一个都会误判。成功率高的 Agent 可能贵得离谱,延迟低的 Agent 可能靠“不思考直接瞎调工具”换来的。本文要做的,就是给你一套可复制的评估脚本配置,并用 TaoToken 统一 Key 把模型调用这一层固定下来,让指标采集可重复、可对比。
为什么强调“统一 Key”?因为评估脚本往往要跑多个模型做横向对比,如果每个模型一套鉴权、一套 Base URL,脚本里到处是分支判断,换一个模型就要改代码,基线根本没法稳定复现。TaoToken 提供的是 OpenAI 兼容接口,一个 Key、一个 Base URL 就能切换不同模型,评估脚本里只改一个 model 字符串,其余逻辑完全不动。这对建立“可重复的智能体性能基线”非常关键。
适合谁看:正在做 Agent 上线前评估的工程师、需要给团队搭评测流水线的技术负责人、以及想搞清楚自己 Agent 到底慢在哪贵在哪的开发者。下面从接入配置讲到完整采集脚本,再到结果验证和排错,你可以直接跟着做。
2. TaoToken 统一 Key 接入与评估环境准备
评估脚本要跑得稳,第一步是把模型调用层固定成“一个入口”。TaoToken 的 API 地址是https://taotoken.net/api,兼容 OpenAI 的/v1/chat/completions协议,所以任何用 openai SDK 写的评估代码都能直接指过来。
先拿 Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台创建 API Key。控制台入口在这里:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。创建完记得复制保存,Key 只显示一次。
拿到 Key 之后,评估环境需要装三个东西:openai SDK 负责调模型,pydantic 负责轨迹数据结构校验,tabulate 负责把指标打成表格方便看。
pip install openai pydantic tabulate然后配置环境变量。我建议不要把 Key 写死在脚本里,用环境变量最省事,也方便 CI 里注入:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"如果你用的是 Claude Code 这类编码 Agent 做评估辅助,配置方式略有不同。Claude Code 走的是 Anthropic 协议,需要在 settings 里指定 Base URL 和 Key。配置文件通常放在~/.claude/settings.json,内容如下:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的key", "ANTHROPIC_MODEL": "claude-sonnet-4-5" } }这里三件套必须齐全:Base URL 指向https://taotoken.net/api,Key 用你创建的,Model ID 填你要评估的模型名。少任何一个都会在启动时报鉴权或模型不存在的错。
如果你用的是 Cline 或带 MCP 的客户端,配置逻辑一样,核心就是三件套。以 Cline 的 MCP 配置为例,在cline_mcp_settings.json里:
{ "mcpServers": { "taotoken-eval": { "command": "npx", "args": ["-y", "@taotoken/mcp-server"], "env": { "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "sk-你的key", "TAOTOKEN_MODEL": "gpt-4o-mini" } } } }Codex 用户则是在~/.codex/auth.json里配置:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的key", "model": "gpt-4o-mini" }不管用哪个客户端,记住一个原则:Base URL 永远是https://taotoken.net/api,不要自己加/v1后缀,SDK 会自动补。这一点很多人第一次配会搞错,加了/v1变成/api/v1/v1/chat/completions,直接 404。
环境准备好之后,先做一次最小连通性验证,确认 Key 能用:
import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "只回复两个字:连通"}], ) print(resp.choices[0].message.content) print("usage:", resp.usage)跑通会打印“连通”,同时能看到usage里的 prompt_tokens 和 completion_tokens。这个 usage 字段就是后面 Token 消耗指标的数据来源,一定要确认它存在。如果返回里没有 usage,说明你调的不是标准兼容接口,指标采集会缺一块。
3. 可复制的评估脚本配置与指标采集实现
这一节是核心。评估脚本要解决三件事:把 Agent 的一次执行完整记录成轨迹、从轨迹里算出三类指标、把结果结构化输出。我把它拆成数据结构、采集器、评估器三层,你可以直接复制。
先定义轨迹数据结构。用 pydantic 的好处是字段类型有校验,脏数据进不来:
import json import time from typing import List, Dict, Any, Optional from pydantic import BaseModel, Field class ToolInvocation(BaseModel): tool_name: str arguments: Dict[str, Any] output: str is_error: bool = False duration_ms: float = 0.0 class AgentStep(BaseModel): step_index: int thought: str action: Optional[ToolInvocation] = None observation: Optional[str] = None step_latency_ms: float = 0.0 step_tokens: int = 0 class AgentTrajectory(BaseModel): session_id: str user_query: str steps: List[AgentStep] = Field(default_factory=list) final_output: str = "" total_latency_seconds: float = 0.0 total_tokens_used: int = 0 ground_truth_answer: Optional[str] = None接下来是采集器。它的职责是在 Agent 每一步执行时打点,记录耗时和 Token。这里的关键是:Token 数从模型返回的 usage 里取,不要自己估算,估算误差能到 30% 以上。
class TrajectoryCollector: def __init__(self, session_id: str, user_query: str): self.trajectory = AgentTrajectory( session_id=session_id, user_query=user_query ) self._start = time.time() def record_step(self, thought: str, action: Optional[ToolInvocation], observation: Optional[str], step_tokens: int, step_latency_ms: float): self.trajectory.steps.append(AgentStep( step_index=len(self.trajectory.steps) + 1, thought=thought, action=action, observation=observation, step_tokens=step_tokens, step_latency_ms=step_latency_ms, )) def finalize(self, final_output: str, ground_truth: Optional[str] = None): self.trajectory.final_output = final_output self.trajectory.ground_truth_answer = ground_truth self.trajectory.total_latency_seconds = round(time.time() - self._start, 3) self.trajectory.total_tokens_used = sum( s.step_tokens for s in self.trajectory.steps ) return self.trajectory然后是评估器,把三类指标算出来。任务成功率用真值匹配,延迟直接取总耗时,Token 消耗除了总量还要算“上下文膨胀斜率”——也就是每一步 Token 相对上一步的增长速度,这个指标能提前发现死循环苗头。
class AgentTrajectoryEvaluator: def __init__(self, optimal_step_threshold: int = 3): self.optimal_step_threshold = optimal_step_threshold def _check_loops(self, steps: List[AgentStep]) -> bool: sigs = [] for s in steps: if s.action: sigs.append( f"{s.action.tool_name}:" f"{json.dumps(s.action.arguments, sort_keys=True)}" ) for i in range(len(sigs) - 1): if sigs[i] == sigs[i + 1]: return True return False def _token_inflation(self, steps: List[AgentStep]) -> float: tokens = [s.step_tokens for s in steps if s.step_tokens > 0] if len(tokens) < 2: return 0.0 growth = [(tokens[i] - tokens[i - 1]) / max(tokens[i - 1], 1) for i in range(1, len(tokens))] return round(sum(growth) / len(growth), 3) def evaluate(self, traj: AgentTrajectory) -> Dict[str, Any]: total_steps = len(traj.steps) if total_steps == 0: return {"session_id": traj.session_id, "task_success": False, "overall_rating": 0.0, "error": "empty trajectory"} has_loop = self._check_loops(traj.steps) if total_steps <= self.optimal_step_threshold: path_eff = 1.0 else: path_eff = max(0.2, self.optimal_step_threshold / total_steps) success = False if traj.ground_truth_answer: success = traj.ground_truth_answer.strip() in traj.final_output.strip() else: success = len(traj.final_output) > 0 and not has_loop total_actions = sum(1 for s in traj.steps if s.action is not None) valid_actions = sum( 1 for s in traj.steps if s.action and not s.action.is_error ) compliance = (valid_actions / total_actions) if total_actions else 1.0 overall = ( (1.0 if success else 0.0) * 40.0 + path_eff * 25.0 + compliance * 20.0 + (1.0 - min(self._token_inflation(traj.steps), 1.0)) * 15.0 ) if has_loop: overall = max(0.0, overall - 30.0) return { "session_id": traj.session_id, "task_success": success, "path_efficiency": round(path_eff, 2), "tool_compliance": round(compliance, 2), "token_inflation": self._token_inflation(traj.steps), "loop_detected": has_loop, "total_latency_sec": traj.total_latency_seconds, "total_tokens": traj.total_tokens_used, "overall_rating": round(overall, 2), }这套脚本的配置要点就三个:Base URL 固定为https://taotoken.net/api,Key 从环境变量读,Model ID 作为参数传入。这样你换模型做对比时,只改一个字符串,指标口径完全一致,基线才可重复。
4. 跑一次完整采集并验证结果
光有脚本不算数,得跑一遍看结果对不对。下面构造一条带“报错后自愈”的真实轨迹,这是最能体现评估价值的场景。
if __name__ == "__main__": collector = TrajectoryCollector( session_id="AGENT_EVAL_001", user_query="查询张三2026年7月的报销总额并生成摘要", ) collector.record_step( thought="先查张三的员工ID", action=ToolInvocation( tool_name="get_employee_id", arguments={"name": "张三"}, output="emp_88012", is_error=False, duration_ms=120, ), observation="emp_88012", step_tokens=320, step_latency_ms=850, ) collector.record_step( thought="查7月报销记录", action=ToolInvocation( tool_name="query_reimbursement", arguments={"emp_id": "emp_88012", "month": "2026-07"}, output="API Error: Invalid date format. Expected YYYYMM.", is_error=True, duration_ms=95, ), observation="日期格式错误", step_tokens=410, step_latency_ms=920, ) collector.record_step( thought="改成YYYYMM格式重试", action=ToolInvocation( tool_name="query_reimbursement", arguments={"emp_id": "emp_88012", "month": "202607"}, output='[{"item":"差旅费","amount":3500,"status":"待审批"}]', is_error=False, duration_ms=110, ), observation="拿到报销数据", step_tokens=520, step_latency_ms=880, ) collector.record_step( thought="汇总回答", action=None, observation=None, step_tokens=180, step_latency_ms=600, ) traj = collector.finalize( final_output="张三2026年7月报销1笔,差旅费3500元,状态待审批。", ground_truth="3500", ) evaluator = AgentTrajectoryEvaluator(optimal_step_threshold=3) report = evaluator.evaluate(traj) from tabulate import tabulate print(tabulate( [[k, v] for k, v in report.items()], headers=["指标", "值"], tablefmt="grid", ))跑完你会看到类似这样的输出:
+-------------------+--------------------------------------+ | 指标 | 值 | +-------------------+--------------------------------------+ | session_id | AGENT_EVAL_001 | | task_success | True | | path_efficiency | 0.75 | | tool_compliance | 0.67 | | token_inflation | 0.28 | | loop_detected | False | | total_latency_sec | 3.25 | | total_tokens | 1430 | | overall_rating | 78.5 | +-------------------+--------------------------------------+怎么验证这个结果合理?逐项看。任务成功 True,因为真值 3500 出现在最终输出里。路径效率 0.75,因为最优 3 步、实际 4 步。工具合规 0.67,因为 3 次工具调用里有 1 次报错。Token 膨胀 0.28,说明每步 Token 在涨但没失控。没有死循环。综合 78.5 分,属于“完成了任务但有可优化空间”,符合这条轨迹的真实表现。
这里有个验证技巧:把optimal_step_threshold改成 4 再跑一次,路径效率会变成 1.0,综合分升到 84.75。这说明阈值参数会直接影响评分,团队内部必须统一口径,否则不同人跑出来的基线没法比。我建议把阈值写进配置文件,和评估脚本一起版本管理。
再验证一个失败场景:把第三步的 month 改回错误的2026-07,让 Agent 连续两次相同调用。这时loop_detected会变成 True,综合分直接扣 30,掉到 50 分以下。这就是死循环检测在起作用,也是为什么 Token 膨胀和死循环要一起看——单看 Token 总量可能只是“话多”,加上循环检测才能定性。
5. 本篇常见报错排查
评估脚本跑不起来,八成是下面几个错。我按实际遇到的频率排一下。
401 Unauthorized / invalid api key
最常见。原因通常是 Key 没读到环境变量,或者复制时带了空格。先确认:
echo $TAOTOKEN_API_KEY如果为空,说明 export 没生效,或者你在新的终端窗口里跑脚本。另一个原因是把 Key 写进了代码但用了错误的变量名。排查时直接在脚本里打印os.environ.get("TAOTOKEN_API_KEY")[:8],看前 8 位对不对。
local proxy failed / connection refused
这个报错说明请求根本没发出去。检查 Base URL 是不是写成了https://taotoken.net/api/v1。正确写法是https://taotoken.net/api,SDK 会自己拼/v1/chat/completions。多写一层/v1就会变成/api/v1/v1/...,服务端直接拒绝。另外确认你的网络能正常访问该域名,公司内网如果有出口限制,需要让运维放行。
reading 'choices' of undefined
这个错通常出现在你直接访问resp.choices[0]但返回体结构不对时。原因可能是模型名写错了,服务端返回的是错误对象而不是正常响应。先打印完整resp看结构:
print(resp.model_dump_json(indent=2))如果里面是{"error": {...}},那就是模型 ID 不存在。Model ID 必须和平台支持的名称完全一致,大小写敏感。
OAuth / authentication failed(Claude Code 场景)
Claude Code 报这个,说明settings.json里的三件套没配全。检查ANTHROPIC_BASE_URL、ANTHROPIC_API_KEY、ANTHROPIC_MODEL是否都在env字段里。特别注意 Claude Code 读的是ANTHROPIC_前缀,不是OPENAI_前缀,混用会直接鉴权失败。改完配置要重启 Claude Code,它不会热加载。
usage 字段为 None
脚本能跑通但 Token 指标全是 0,说明返回体里没有 usage。这通常是因为你用了流式(stream=True)但没开stream_options={"include_usage": True}。评估场景建议关掉流式,或者显式开启 usage 返回:
resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "test"}], stream=True, stream_options={"include_usage": True}, )pydantic ValidationError
轨迹数据结构校验失败,多半是arguments传了非 dict 类型,或者step_tokens传了字符串。检查采集器调用处,确保arguments是字典、数值字段是 int 或 float。pydantic 的报错信息很详细,会告诉你哪个字段哪个类型不对,照着改就行。
排错时如果拿不准是 Key 的问题还是脚本的问题,最快的办法是回到第 2 节那段最小连通性验证代码,单独跑一次。它能通,说明接入层没问题,问题在评估脚本;它不通,就是 Key 或 Base URL 配置的事。接入相关的完整说明可以看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
6. 把评估接进日常流程
脚本跑通只是起点。真正让智能体性能评估产生价值的,是把它变成每次改动都会自动跑的动作。我的做法是把评估脚本包成一个 CLI,参数化模型名和测试集路径,然后在 CI 里加一步:只要 Agent 的 prompt 或工具定义有改动,就自动跑一遍基线测试集,成功率下降超过 1.5% 就阻断合并。
模型调用层用 TaoToken 统一之后,这一步变得很轻。你不需要为每个待测模型单独配鉴权,只要在 CI 的环境变量里放一个 Key,脚本里循环几个 Model ID 就能一次性跑出横向对比表。延迟和 Token 消耗这两类指标尤其适合横向比,因为它们的口径完全一致,差异直接反映模型和 Agent 策略的优劣。
如果你要长期做编码类 Agent 的评估,调用量会比较大,可以考虑用 Coding Plan 来跑批量任务,成本更可控:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。日常想快速验证某个模型在评估任务上的表现,直接用模型对话页面手动试几条也行:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。
最后给一个实用建议:把每次评估的 report 存成 JSON,按日期归档。跑上两周你就能画出成功率、延迟、Token 三条趋势线。当某天成功率没掉但 Token 悄悄涨了 40%,趋势图会第一时间告诉你——这种问题靠单次测试根本发现不了,只有可重复的基线采集才能暴露。