在实际 LLM 项目中,Agent 的能力边界往往不由模型参数单独决定,而是取决于它能不能在一连串不确定环境里稳定地把外部工具用起来。MCP-Bench 这类以“复杂真实世界任务”为对象的工具调用型 Agent 基准,正是为了回答这个问题而出现的:任务不是简单问答,而是读取日志、查询数据库、调用 API、根据中间结果纠正动作的多步流程。协议层面用 MCP 统一工具接入,评估层面用一组可打分、可复现、可归因的任务去测量不同模型和 Agent 框架的实际表现,这正是 MCP-Bench 的核心思路。
这篇文章以 MCP-Bench 的评测主线展开。先解释为什么工具调用型 Agent 需要独立基准,再拆解 MCP 协议的工具调用链路与评测任务设计要点;随后从零搭建最小可运行评测环境,实现任务定义、工具执行、结果评分和日志采集;最后讨论指标读取、高频故障定位,以及把本地 Demo 扩展成生产级评测时该补哪些能力。适合正在做 Agent 评测、准备接入 MCP 工具,或者想在自己项目中量化 LLM 工具调用质量的开发者阅读。
1. 工具调用型 Agent 为什么需要独立的评测基准
1.1 QA 正确率衡量不了“会不会用工具”
传统大模型评测通常把模型当作知识问答器:给输入,比输出,算正确率。这种模式对事实记忆、推理能力、文本生成质量有效,但对 Agent 场景不够。Agent 的产出不是一个静态答案,而是一串动作序列:判断当前缺什么信息,决定调用哪个工具,解析返回结果,再决定下一步。同一个任务,两个模型可能都完成了目标,但一个用了 3 次工具调用,另一个用了 20 次;一个能处理工具返回的异常格式,另一个直接卡死。这种差异无法用选择题正确率体现。
所以工具调用型 Agent 需要独立基准。它要测量的是模型在“决策-行动-观察”循环中的综合能力,而不是单次生成能力。MCP-Bench 这类基准把这种循环固化成标准任务,让不同模型和框架在同样条件下跑同样的流程,结果才有可比性。
1.2 复杂真实世界任务给 Agent 出的三道难题
进入真实场景后,工具调用型 Agent 面对的问题通常比教科书示例复杂得多,主要体现为三点。
第一,上下文碎片化。任务需要的信息分散在不同文件、数据库、接口里,Agent 必须主动拉取,不能指望用户一次性给全。第二,工具协议不统一。团队里可能有 REST API、数据库连接、命令行脚本、内部 SDK,如果每个工具都自己定义调用格式,Agent 适配成本非常高。第三,失败难复现。工具会超时、返回空值、报权限错误,Agent 必须能感知并调整,否则同样的任务换个环境就失败。
这些难题意味着评测任务必须设计成“真实世界的复杂度”,而不是把多个简单问答拼在一起。一个合格的工具调用型 Agent 评测任务,应该要求 Agent 自己决定调用顺序、自己判断结果正确性、自己处理异常分支。
1.3 MCP 和 MCP-Bench 的关系
MCP(Model Context Protocol)是一个开放协议,用来标准化 LLM 应用与外部数据源、工具之间的连接方式。在 MCP 生态里,工具提供方只需要按协议实现一套服务,模型应用就能通过统一方式发现工具、描述参数、发起调用、接收结果。这解决了 1.2 节提到的协议碎片化问题。
MCP-Bench 则可以理解成围绕这种工具调用模式设计的评估框架:用 MCP 服务承载待测工具,用复杂的真实任务驱动模型行动,再用统一评分逻辑判断模型是否完成了目标。需要说明的是,不同团队对 MCP-Bench 的落地方式并不完全一致,有的基于公开评测集,有的基于内部业务任务集,但共同点是“MCP 协议 + 工具调用型 Agent + 复杂任务评分”这个组合。
1.4 评测时应该关注的对象
跑一个 MCP-Bench 风格评测,关注的不是某一次工具调用有没有成功,而是整条 Agent 链路的表现,至少包含四层:
- 模型层:是否理解任务语义,能否把用户请求拆成工具调用计划。
- 协议层:MCP Server 是否正常,工具参数是否符合 Schema,返回结果能否被解析。
- Agent 框架层:是否维护多轮上下文,是否限制最大调用次数,是否对工具异常有兜底。
- 评测任务层:任务答案是否明确,评分规则是否覆盖部分完成的情况,任务是否具备区分度。
如果只盯着“工具有没有返回结果”,很容易被一次偶发成功误导。MCP-Bench 的价值在于把上述每一层都变成可观测、可评分的对象。
2. MCP 工具调用链路与评测任务的关键设计点
2.1 MCP 的三段式架构
理解 MCP-Bench 前,要先理解 MCP 工具调用链路。MCP 采用三段式结构:
- MCP Host:运行 LLM 的应用,负责组织对话流程,把模型决策转发给工具。
- MCP Client:Host 内部连接 Server 的客户端组件,负责协议通信。
- MCP Server:实际执行工具的服务进程,暴露工具清单并接收调用请求。
调用链路可以概括为:用户在 Host 中发起任务 -> 模型分析后决定调用某个工具 -> Host 通过 Client 向 Server 请求工具执行 -> Server 返回结构化结果 -> 模型根据结果继续决策。整个过程是循环的,直到模型认为任务完成或达到最大轮数限制。
这种结构天然适合评测:Server 负责提供可复现的工具环境,Runner 负责记录每一轮决策和结果,任务集负责定义预期目标。评测框架可以把它抽象成“输入任务 -> 循环工具调用 -> 输出最终答案 -> 对照评分”。
2.2 三个核心原语:Resources、Prompts、Tools
MCP 给模型应用提供了三类能力,评测任务设计时经常涉及:
| 原语 | 作用 | 评测中的典型用法 |
|---|---|---|
| Resources | 向模型提供读取型数据,比如文件内容、数据库记录 | 提供任务背景资料,避免把信息全部塞进 prompt |
| Prompts | 可复用的提示模板,规范模型如何完成某类操作 | 定义工具调用说明和输出格式模板 |
| Tools | 模型可以主动调用的执行型函数,需要参数 Schema | 作为评测中的被调用对象,记录调用轨迹 |
对工具调用型 Agent 评测来说,Tools 是核心。一个 Tool 包含名称、描述、输入 Schema 和执行逻辑。描述写得好不好,直接影响模型能否正确选择工具;Schema 设计得清不清楚,直接影响参数填充是否正确。这两点也是评测中差异最大的变量。
2.3 一个多步任务在 MCP 链路中如何执行
用一个例子说明多步任务在 MCP 链路里的执行过程。假设评测任务要求 Agent“统计某个服务日志中出现 ERROR 的次数,并判断错误最集中的时间段”。模型会把任务拆成两步:先调用read_log工具读取日志文件,再调用extract_stats工具或自己分析文本。如果设计成链式任务,第二步可能依赖第一步的返回值。
实际的协议消息可以简化成下面这样,先列出 Server 支持的工具:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }Server 返回工具描述:
{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "read_log", "description": "读取指定日志文件,返回全部行", "inputSchema": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] } } ] } }模型决定调用工具后,发送tools/call请求:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "read_log", "arguments": { "path": "logs/app.log" } } }Server 返回执行结果:
{ "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "ERROR count: 17\n[2025-01-01 00:01:02] ERROR ..." } ] } }评测脚本要记录的就是这些请求和响应:模型在什么轮次调用什么工具、传了什么参数、拿到什么结果、最后得出什么结论。有了这种完整轨迹,评分和排错才有依据。
2.4 评测框架必须采集的四类数据
一个合格的工具调用型 Agent 评测框架,最少要采集四类数据:
- 任务输入与预期:任务原始描述、预期结果表达式、允许的最大调用轮数。
- 动作序列:模型每一步的决策类型、工具名、参数、对应轮次。
- 工具执行结果:每个工具的成功状态、返回内容、耗时、异常信息。
- 最终输出与评分:模型给出的最终答案、任务得分、扣分项说明。
采集这些数据不只为给一个分数,而是为了让失败可以归因。一个 Agent 任务失败,可能是模型规划错误,也可能是工具参数 Schema 写错,还可能是 Server 崩溃。没有动作序列和工具执行日志,这几个原因很难区分。
3. 搭建最小可复现的评测环境
3.1 依赖与目录结构
推荐使用 Python 3.10 以上版本,配合 MCP 官方 SDK 搭建本地评测环境。依赖安装命令如下:
mkdir -p mcp-bench-demo/{tasks,logs,server,runner} cd mcp-bench-demo python -m venv .venv source .venv/bin/activate pip install mcp不同版本的 MCP SDK 在 FastMCP API 上略有差异,落地前可以用pip show mcp查看版本,再对照官方文档确认接口写法。下面的例子基于 mcp SDK 1.x 的常见写法。
推荐的目录结构:
mcp-bench-demo/ ├── server/ │ └── bench_server.py ├── runner/ │ └── bench_runner.py ├── tasks/ │ └── tasks.json └── logs/ └── app.loglogs 目录放模拟业务日志,server 目录放 MCP Server,runner 目录放评测脚本,tasks 目录放任务集。目录职责分开,后续扩展任务和排查问题都会方便很多。
3.2 用 FastMCP 编写一个带两个工具的 Server
下面这个 Server 暴露两个工具:一个统计日志关键字次数,一个读取用户信息。前者是单步工具,后者用于演示链式查询。
# server/bench_server.py from mcp.server.fastmcp import FastMCP mcp = FastMCP("bench-tools") @mcp.tool() def count_keyword(log_path: str, keyword: str) -> str: """统计指定日志文件中关键字出现的次数。 Args: log_path: 日志文件路径,例如 logs/app.log keyword: 要统计的关键字,例如 ERROR """ count = 0 with open(log_path, "r", encoding="utf-8") as f: for line in f: if keyword in line: count += 1 return str(count) @mcp.tool() def get_user_region(user_id: str) -> str: """根据用户 ID 返回用户所在地区。 Args: user_id: 用户唯一标识,例如 u-1001 """ mock_users = { "u-1001": "shenzhen", "u-1002": "beijing", "u-1003": "chengdu", } region = mock_users.get(user_id, "unknown") return region @mcp.tool() def get_weather(city: str) -> str: """返回指定城市的天气情况。 Args: city: 城市英文名,例如 shenzhen """ mock_weather = { "shenzhen": "sunny, 28c", "beijing": "cloudy, 18c", "chengdu": "rainy, 22c", } return mock_weather.get(city, "unknown weather") if __name__ == "__main__": mcp.run()这个示例中用字典模拟用户数据和天气数据,实际项目里可以替换成数据库查询或外部 API 调用。三个工具中,count_keyword是独立工具,get_user_region和get_weather可以组合成链式任务:先查用户地区,再根据地区查天气。
3.3 通过 JSON-RPC 验证 Server 是否正常
启动 Server 前,先用 Python 自带方式检查能否加载模块:
cd mcp-bench-demo python -c "import server.bench_server; print('import ok')"如果输出import ok,说明依赖和模块路径正常。接着启动服务:
python server/bench_server.py本地 MCP Server 一般通过 stdio 或 HTTP 与客户端通信。FastMCP 的mcp.run()在不同版本中默认传输方式不同,有的是 stdio,有的是 HTTP。为了方便评测脚本调用,可以显式配置成 HTTP 模式,也可以直接让 Runner 以子进程方式启动 Server。学习阶段建议先用 HTTP 模式,便于用 curl 验证。
如果 Server 以 HTTP 方式运行,可以用下面这条命令验证tools/list:
curl -s -X POST http://127.0.0.1:8000/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'响应里能列出三个工具,说明 Server 工作正常。这一步很重要,因为后面的 Runner 会对 Server 通信结果做依赖,如果 Server 没起来,所有评测都会失败。
3.4 任务集 JSON 的设计与示例
任务集是评测的核心资产。设计任务时要把任务描述、最大步数、预期结果校验方式都定义清楚。下面是一个合适的示例:
{ "tasks": [ { "id": "task-001", "name": "统计 ERROR 日志数量", "prompt": "请统计 logs/app.log 中 ERROR 出现的总次数,直接返回数字。", "max_steps": 3, "expected": "17" }, { "id": "task-002", "name": "查询用户所在城市天气", "prompt": "用户 u-1001 想了解自己所在城市的天气,请查询该用户的地区并返回当地天气。", "max_steps": 4, "expected": "sunny, 28c" } ] }任务一测试单工具调用。任务二测试链式调用:模型必须先调用get_user_region,再调用get_weather。如果模型直接猜测天气,即使答案碰巧正确,评测脚本也能根据动作轨迹判断它的行为不正确,这就是动作序列采集的作用。
4. 实现最小 MCP-Bench 评测 Runner
4.1 Runner 的整体流程
评测 Runner 是整篇文章的核心部分。它读取任务集,为每个任务建立一次 Agent 会话,循环执行“模型决策 -> 工具调用 -> 结果收集”,最后输出评分和轨迹。流程如下:
- 加载 tasks.json。
- 启动或连接 MCP Server。
- 获取工具列表,缓存到本地。
- 对每个任务,构造初始 prompt 并进入循环。
- 每一轮让规划器决定动作类型:
tool_call或finish。 - 如果是
tool_call,调用 MCP Server 并记录结果。 - 如果是
finish,把最终答案交给评分函数。 - 汇总所有任务的分数、工具调用次数、异常日志。
4.2 模型规划器抽象:本地用固定策略,线上替换成真实 LLM
为了不依赖具体模型 API Key,也为了让评测框架本身可以先跑通,这里定义一个AgentPlanner抽象,它只负责“根据当前消息历史生成下一个动作”。本地验证时用MockPlanner,它从任务配置里读取预设的动作序列,模拟一个听话的 Agent;真实评测时把它替换成真实 LLM 调用即可。
# runner/agent_planner.py from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any @dataclass class Action: type: str # "tool_call" or "finish" tool_name: str = "" arguments: dict = field(default_factory=dict) answer: str = "" class AgentPlanner(ABC): @abstractmethod def next_action(self, messages: list[dict]) -> Action: ... class MockPlanner(AgentPlanner): """本地验证用:按 task 中配置的固定动作序列执行。""" def __init__(self, plan: list[dict]): self.plan = plan self.index = 0 def next_action(self, messages: list[dict]) -> Action: if self.index >= len(self.plan): return Action(type="finish", answer="NO_ANSWER") step = self.plan[self.index] self.index += 1 if step["type"] == "tool_call": return Action( type="tool_call", tool_name=step["tool_name"], arguments=step["arguments"], ) return Action(type="finish", answer=step["answer"])真实项目里,替换next_action时只需要把messages发给大模型,解析模型返回的 tool_calls 和最终文本,转成Action。这个替换对 Runner 的其他部分完全透明。
4.3 客户端封装:连接 Server、列出工具、调用工具
下面封装一个 MCPClient,负责与 Server 通信。如果使用官方 SDK,可以直接用客户端类;这里为了展示原理,写一个基于 requests 的简洁版本,方便阅读和调试。
# runner/mcp_client.py import requests class MCPClient: def __init__(self, endpoint: str): self.endpoint = endpoint self.tools: dict = {} def list_tools(self) -> dict: payload = { "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}, } resp = requests.post(self.endpoint, json=payload, timeout=10) resp.raise_for_status() data = resp.json() tools = data["result"]["tools"] self.tools = {t["name"]: t for t in tools} return self.tools def call_tool(self, name: str, arguments: dict) -> dict: payload = { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": name, "arguments": arguments, }, } resp = requests.post(self.endpoint, json=payload, timeout=15) resp.raise_for_status() data = resp.json() if "error" in data: return {"ok": False, "error": data["error"]} content = data["result"]["content"] text = "" for item in content: if item.get("type") == "text": text += item.get("text", "") return {"ok": True, "text": text}代码里把tools/list的结果缓存到self.tools,后续评分统计可以知道模型调用的工具是否真实存在。call_tool返回统一结构,无论成功失败,都包含可观测的结果字段。
4.4 任务执行循环与结果收集
主执行循环记录每一步动作、工具返回值和异常信息,最后把完整轨迹交给评分函数。
# runner/bench_runner.py import json from agent_planner import MockPlanner from mcp_client import MCPClient def run_single_task(client: MCPClient, task: dict, planner: AgentPlanner): max_steps = task.get("max_steps", 5) messages = [{"role": "user", "content": task["prompt"]}] trace = { "task_id": task["id"], "steps": [], "final_answer": "", "tool_calls": 0, "errors": [], } for step_index in range(max_steps): try: action = planner.next_action(messages) except Exception as exc: trace["errors"].append(f"planner_error: {exc}") break if action.type == "finish": trace["final_answer"] = action.answer return trace if action.type == "tool_call": trace["tool_calls"] += 1 result = client.call_tool(action.tool_name, action.arguments) step_record = { "step": step_index + 1, "action": "tool_call", "tool_name": action.tool_name, "arguments": action.arguments, "result": result, } trace["steps"].append(step_record) if not result.get("ok"): trace["errors"].append( f"tool_error: {action.tool_name} - {result.get('error')}" ) messages.append({ "role": "tool", "content": json.dumps(result, ensure_ascii=False), }) trace["errors"].append("max_steps_exceeded") return trace注意这里把每一步的工具结果放进了messages,目的和真实 MCP 链路一致:模型需要看到工具返回结果,才能决定下一步。评测框架也应该保留这些消息,便于后续分析模型决策逻辑。
4.5 评分函数与运行输出
评分函数需要平衡“完成正确性”和“过程效率”。下面是一个简单但可扩展的版本:
def score_trace(task: dict, trace: dict) -> float: score = 0.0 expected = task.get("expected") actual = trace.get("final_answer", "").strip() if actual == expected: score += 1.0 elif expected in actual: score += 0.6 else: score += 0.0 tool_calls = trace.get("tool_calls", 0) max_steps = task.get("max_steps", 5) efficiency_penalty = min(tool_calls / (max_steps * 2), 0.3) score -= efficiency_penalty if trace.get("errors"): score -= 0.2 return round(max(score, 0.0), 2) def run_all(tasks_path: str, endpoint: str): with open(tasks_path, "r", encoding="utf-8") as f: tasks = json.load(f)["tasks"] client = MCPClient(endpoint) client.list_tools() report = [] for task in tasks: plan = [ {"type": "tool_call", "tool_name": "count_keyword", "arguments": {"log_path": "logs/app.log", "keyword": "ERROR"}}, {"type": "finish", "answer": "17"}, ] if task["id"] == "task-002": plan = [ {"type": "tool_call", "tool_name": "get_user_region", "arguments": {"user_id": "u-1001"}}, {"type": "tool_call", "tool_name": "get_weather", "arguments": {"city": "shenzhen"}}, {"type": "finish", "answer": "sunny, 28c"}, ] planner = MockPlanner(plan) trace = run_single_task(client, task, planner) score = score_trace(task, trace) report.append({"task_id": task["id"], "score": score, "trace": trace}) for item in report: print(json.dumps({ "task_id": item["task_id"], "score": item["score"], "tool_calls": item["trace"]["tool_calls"], "final_answer": item["trace"]["final_answer"], }, ensure_ascii=False)) if __name__ == "__main__": run_all("tasks/tasks.json", "http://127.0.0.1:8000/mcp")运行后预期输出:
{"task_id": "task-001", "score": 1.0, "tool_calls": 1, "final_answer": "17"} {"task_id": "task-002", "score": 1.0, "tool_calls": 2, "final_answer": "sunny, 28c"}当模型规划正确、工具实现正确时,两个任务都能拿满分。真实评测中,MockPlanner 会被替换成 LLM,分数就会出现差异,这时候就需要看 trace 里的每一步,判断是规划错误还是工具错误。
5. 评测指标、参数与任务梯度:结果不是跑出分数就结束
5.1 核心指标怎么算、怎么用
MCP-Bench 类评测不能只看一个总分,建议把指标拆开,每个指标对应一类能力问题:
| 指标 | 计算方式 | 反映的问题 |
|---|---|---|
| 任务成功率 | 完全正确任务数 / 总任务数 | 基础完成能力 |
| 任务完成度 | 各任务得分加权平均 | 部分完成能力 |
| 平均工具调用数 | 工具调用总次数 / 任务数 | 规划效率 |
| 无效调用率 | 返回错误或空结果的调用次数 / 总调用次数 | 工具描述和模型理解质量 |
| 异常恢复率 | 出错后仍完成任务数 / 出错任务数 | 鲁棒性 |
| 最终答案命中率 | 最终答案包含预期关键内容的任务占比 | 结果可靠性 |
单一成功率会掩盖过程问题。比如一个 Agent 靠乱猜答案碰巧命中,成功率很高但工具基本没用,这就不是合格的工具调用 Agent。所以评测一定要结合动作轨迹查看,不能只看最终分数。
5.2 任务梯度决定评测区分度
评测任务不能全是“读一个文件返回数字”这种简单任务,也不建议一上来就是十几个工具的长链路任务。建议划分难度梯度:
- 基础层:单工具调用,直接返回结果,例如统计关键字。
- 链式层:两个或以上工具串联,后一个工具依赖前一个结果。
- 条件层:根据工具返回内容决定调用哪个分支工具。
- 异常层:工具会返回空值、错误码或超时,考察模型恢复能力。
- 并行层:多个独立工具需要分批调用,再把结果汇总。
配置任务时,每层至少 3 到 5 个任务。如果某个模型在基础层满分、链式层得分低,说明它工具理解能力没问题,但多步规划能力弱;如果在链式层也可以、异常层下降明显,说明它对错误处理缺少兜底策略。这种梯度化分析比一个总分数更有诊断价值。
5.3 影响评测结果的四个关键参数
评测时容易忽略参数对结果的干扰。下面四个参数对结果影响最大,建议统一记录:
| 参数 | 默认值参考 | 调大影响 | 调小影响 |
|---|---|---|---|
| 温度 temperature | 0 到 0.2 | 动作更多样,但更容易出现无效调用 | 结果更稳定,但可能缺少探索 |
| 最大步数 max_steps | 5 到 10 | 给长链路任务更多机会,但掩盖低效问题 | 对长任务不友好,容易截断 |
| 工具描述长度 | 一到三句 | 模型更好理解,但会占用上下文 | 描述过短时模型容易选错工具 |
| 上下文窗口模型 | 按模型实际支持配置 | 能容纳更多中间结果,但成本上升 | 长链路任务容易截断历史 |
评测报告里应该记录这些参数,否则两个团队跑同一个任务集,因为温度或最大步数不同,分数完全不同,结果无法对比。MCP-Bench 风格评测对可复现性要求很高,参数和模型版本都要写进报告。
5.4 让评测结果可复现的隔离策略
为了让结果可复现,建议从四个方面做隔离。
一是数据隔离。任务里的日志、用户表、天气数据都要用固定测试数据,不用生产实时数据,避免外部变化导致结果不稳定。
二是环境隔离。MCP Server 和 Runner 跑在固定容器或虚拟环境中,依赖版本锁定。至少把requirements.txt和 Python 版本写入报告。
三是随机性隔离。LLM 采样有随机性,建议同一任务跑多次取平均或取最低分,并记录每次结果。
四是缓存隔离。如果工具内部访问数据库或 API,要在测试环境把这些依赖 Mock 掉,保证每次调用返回相同结果。上面的天气和用户数据用字典模拟,就是这个目的。
6. 评测过程中的常见故障定位
6.1 排查顺序:从环境到代码再到模型策略
评测跑出低分或直接报错时,不要第一时间怀疑模型能力,按下面的顺序排查:
- MCP Server 是否启动,端口是否正确。
tools/list是否能返回工具列表。- 工具描述和 Schema 是否合理。
tools/call是否能正常返回。- Runner 是否正确解析返回值。
- MockPlanner 或真实 LLM 是否输出预期动作。
- 评分规则是否符合任务语义。
这里面有 60% 以上问题出在前四步,属于环境或工具实现问题,而不是模型问题。直接怀疑模型只会让排查变慢。
6.2 高频问题速查表
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| Server 启动后端口连不上 | 启动模式不是 HTTP,或配置了 stdio | 看启动日志,确认监听地址 | 显式配置 HTTP endpoint |
| tools/list 返回空列表 | 工具装饰器未注册或文件没加载 | 打印 server 日志,调用方法名 | 确认 import 路径正确 |
| tools/call 返回 -32603 | 工具函数内部抛异常 | 查看 Server stderr 堆栈 | 修复工具函数,增加异常兜底 |
| 模型不调用工具,直接给答案 | 工具描述不清晰或 prompt 未说明可用工具 | 查看模型消息历史 | 补全工具描述,在 prompt 中强调必须调用工具 |
| 同一任务多次跑分数不同 | 模型采样随机或数据不是固定测试集 | 固定 temperature,核对测试数据 | 设置 temperature=0,固定 seed |
| 评分结果与预期不符 | expected 字段写法不规范 | 手动跑一次工具返回结果 | 统一 expected 的类型和格式 |
6.3 一个典型案例:tools/list 正常但 tools/call 报错
现象是tools/list能列出get_user_region,但调用时报内部错误,错误码类似-32603。常见原因是工具函数里读取的数据不存在,比如:
@mcp.tool() def get_user_region(user_id: str) -> str: user = mock_users[user_id] # 直接下标访问,key 不存在会抛 KeyError return user["region"]当模型传入一个不在 mock 数据里的用户 ID 时,函数直接抛异常,错误被 MCP 包装成-32603返回。解决方式是改成mock_users.get(user_id, "unknown"),并在函数说明里写清楚支持的取值范围。
这个案例说明工具函数本身要有健壮性。评测工具不等于生产工具,但它的容错程度直接影响评测结果:一个不够健壮的工具会让模型即使规划正确也会失败。
6.4 分数偏低时怎么从日志归因
分数偏低时先看 trace,按以下路径归因:
- 如果动作序列正确但最终答案错误,检查工具返回格式和评分逻辑。
- 如果动作序列从一开始就跑偏,检查工具描述、prompt 和模型规划能力。
- 如果模型在第 N 步出现重复调用同一个工具,检查上下文是否展示了工具结果。
- 如果出现大量
max_steps_exceeded,检查任务最大步数设置是否合理。
评测框架最好把 trace 导出成 JSON 文件,包含 prompt、每一步模型输出、工具结果、最终答案。这样排查时可以直接回放整个过程,而不是凭记忆猜。
7. 从本地 Demo 到生产级 Agent 评测:最佳实践与扩展方向
7.1 一套可直接使用的评测准备清单
在正式跑一组评测前,建议逐项确认下面这些内容:
- 任务集是否覆盖不同难度梯度,每层至少 3 个任务。
- 每个任务的 expected 字段是否唯一、可比较、可自动判断。
- MCP Server 是否能稳定启动,工具是否都有异常兜底。
- 工具描述是否包含输入含义、取值范围、返回值格式。
- Runner 是否正确记录动作序列、工具结果、最终答案和错误信息。
- 是否固定模型版本、temperature、max_steps 和随机种子。
- 是否使用固定测试数据,避免外部依赖波动。
- 评分规则是否区分完全正确、部分正确和错误恢复。
- 是否导出 JSON 轨迹,方便失败归因。
这份清单在发布评测报告前过一遍,能避免大部分“分数不可信”的问题。
7.2 生产级评测与本地评测的差异
本地 Demo 跑通后,进入生产环境差异主要在五个方面:
一是并发。真实评测通常同时跑几十个模型实例,MCP Server 要能支持并发请求,不能只用本地单进程。二是资源隔离。每个评测任务使用独立沙箱数据,避免任务间相互污染。三是回归阈值。模型或 Agent 框架升级后,要设定分数下降阈值,比如整体分数下降超过 2% 就阻止发布。四是版本追踪。模型版本、工具代码版本、任务集版本都要记录,否则无法定位是模型变了还是工具变了。五是安全与审计。工具可能访问敏感数据,评测环境要用完全模拟数据,并对工具调用做权限控制。
7.3 可以继续深入的方向
MCP-Bench 风格的评测框架本身还有不少扩展空间。后续可以加入多轮对话评测,让 Agent 在澄清需求后再行动;可以加入多 Agent 协作场景,评测多个 Agent 通过 MCP 工具分工完成复杂任务;可以在评分中加入语义相似度,允许模型用不同表达给出正确结果;还可以引入人类偏好评估,对 Agent 的步骤顺序和解释质量做主观打分。
对正在做 Agent 应用的团队来说,最值得投入的方向是先把“任务集 + 指标 + 轨迹回放”三件套建立起来。任务集保证有标准可测,指标保证有量化结果,轨迹回放保证失败可排查。只要这三件事落地,无论后续更换模型、增加工具,还是调整 Agent 框架,都能用一套稳定的评测体系保障质量。
回到 MCP-Bench 的核心判断:工具调用型 Agent 的能力不能靠感觉衡量,必须放在复杂真实任务里,用统一协议、标准任务、可复用指标去约束。先跑通最小闭环,再把任务集做厚、把指标做细,这比一开始追求上百个工具的大规模评测更实际。对刚接触这个方向的开发者,建议从本文的最小 Runner 开始,替换成真实 LLM,录入自己的业务任务,再用 trace 分析第一次失败原因,这会比阅读大量评测论文更快建立手感。