上一篇文章对 AgentScope 工具调用功能的核心类 Toolkit 类做了较为深入的分析,这篇文章我们继续分析 AgentScope 工具能力的另外几个特性,包括 MCP 工具、Agent Skills。
MCP 简介
MCP(Model Context Protocol,模型上下文协议)是一个旨在标准化 AI 模型与外部数据源和工具之间交互的开放协议。在上篇文章中,我们都是将自己代码中的 Python 函数注册为可被 LLM 调用的工具,而 MCP 协议则提供了一种通用、标准的方式,让AI模型能够连接和调用各种工具与数据,无论这些工具是如何实现的、部署在哪里。
在 MCP 出现之前,如果你想让 AI 访问你的 Google Drive、GitHub 或者本地数据库,每个开发者都要为不同的 AI 模型写一套不同的 连接器。而 MCP 则像是 AI 界的 USB 接口标准。只要数据源支持 MCP,任何支持 MCP 的 AI 客户端(如 Claude Desktop, Cursor)都能直接插上使用。
MCP 采用了典型的 客户端-服务器(Client-Server)架构:
-
MCP Host (主机):这是用户直接交互的 AI 应用程序,例如 Claude Desktop、Cursor 或者 自定义的 AI Agent。这个 AI 应用会与 LLM 通信,并发起可能的工具调用请求
-
MCP Client (客户端):Client 是 Host 内部的轻量级组件,负责与一个特定的 MCP Server 建立和维护一对一的连接,充当 Host 与 Server 之间的
翻译官,将 Host 的请求转发给 Server,并将 Server 的响应返回给Host -
MCP Server (服务端):Server是MCP架构中的核心,它通过标准化接口,向客户端暴露三种主要能力
- 资源 (Resources):任何可供模型读取的数据,例如文件内容、数据库记录或API响应。每个资源由唯一的 URI 标识
- 工具 (Tools):模型可以执行的函数,工具通常可以改变状态或与外部系统交互
- 提示 (Prompts):预定义的提示词模板,可以接受动态参数,帮助用户更高效地完成特定任务
MCP 协议定义了标准化的传输方式,以确保不同组件间的可靠通信:
- STDIO:主要用于本地通信,此时
MCP Client会将MCP Server作为子进程启动,然后通过标准输入(stdin)和标准输出(stdout)进行JSON-RPC消息的交换 - Streamable HTTP:用于远程通信(早期版本使用 SSE,现已演进),这使得 MCP Server 可以作为独立的网络服务运行,供远程客户端连接。客户端通过 HTTP POST 发送请求,并通过
Server-Sent Events (SSE)接收服务端推送的消息
所有 MCP 消息都必须遵循 JSON-RPC 2.0 规范,定义了请求、响应和通知三种基本消息类型,确保了交互的结构化与一致性。
MCP 的一个核心优势是:开发者无需为每个模型单独编写工具调用代码,只需将工具实现为一次 MCP Server,任何支持 MCP 的 Agent 都能直接使用。这样工具的开发与 LLM/Agent 之间的集成就被解耦了。
有一点需要注意,MCP 并不是要取代 LLM 的 Function Call(Tool Call,工具调用),Function Call 定义了模型如何输出 JSON 来表达 我想调用这个函数,而 MCP 协议则定义了如何发现发现、连接、运行各种工具,因此 Agent 仍然还是要依赖 LLM 的 Function Call 能力来触发工具的调用,而 MCP 协议则标准化了工具的发现、调用过程。实际运行中,两者之间是协同工作的:
- 连接:你的 AI Agent 应用(Host)连接到一个 MCP Server(比如一个 GitHub 统计工具)。
- 发现:MCP Server 告诉客户端:
我有 get_repo_stars 这个工具 - 注入:客户端把这个工具的定义,通过
Function Call的格式告诉 LLM - 决策(Function Call 发生):LLM 决定调用它,输出:
{"name": "get_repo_stars", "args": {...}} - 执行(MCP 工具调用发生):客户端收到这个 JSON,通过 MCP 协议转发给对应的 Server 去执行
- 返回:Server 把结果传回 MCP 客户端,AI Agent 应用再通过
Tool Result的形式喂给 LLM
所以要注意 LLM 的 Function Call 与 MCP 协议之间的联系与区别:
- Function Call 解决了
模型想干什么的问题 - MCP 解决了
工具在哪、怎么连、怎么传数据的问题
AgentScope 的 MCP 模块
src/agentscope/mcp 目录是 AgentScope 实现 MCP 工具调用功能的核心代码,每个文件的主要作用:
| 文件 | 类/功能 |
|---|---|
_client_base.py |
MCPClientBase - 客户端抽象基类 |
_stateful_client_base.py |
StatefulClientBase - 有状态客户端基类 |
_http_stateful_client.py |
HttpStatefulClient - HTTP 有状态客户端 |
_http_stateless_client.py |
HttpStatelessClient - HTTP 无状态客户端 |
_stdio_stateful_client.py |
StdIOStatefulClient - StdIO 有状态客户端 |
_mcp_function.py |
MCPToolFunction - 工具函数封装 |
当然 AgentScope 的 MCP 模块并没有从头开始实现 MCP 协议,而是对现有的 MCP Python SDK 进行封装和适配。总体来说,提供了以下几个接口:
| 功能 | 说明 |
|---|---|
| 连接管理 | 建立和维护与 MCP 服务器的连接 |
| 工具发现 | 列出服务器提供的可用工具 |
| 工具调用 | 将 MCP 工具封装为可直接调用的函数 |
| 结果转换 | 将 MCP 格式结果转为 AgentScope 格式 |
| 生命周期管理 | 管理有状态/无状态客户端的会话 |
MCPClientBase (客户端基类)
MCPClientBase 定义了 MCP Client 的抽象基类,其中:
get_callable_function是一个抽象方法,具体的 Client 实现类需要实现这个方法,用于根据工具名称获取一个可调用对象_convert_mcp_content_to_as_blocks方法用于将 MCP 返回的结果类型转换为 AgentScope 自己定义的内容格式,例如TextBlock、ImageBlock等
1 | class MCPClientBase: |
StatefulClientBase(有状态客户端基类)
AgentScope 将 MCP client 分为 有状态 与 无状态 两种,他们的区别在于客户端是否会维持与 MCP 服务器的会话(session):
- 有状态客户端:其生命周期内始终维持与 MCP 服务器的持久会话,需要开发者显式调用
connect()和close()方法来管理会话的生命周期 - 无状态客户端只会在调用工具发生时建立会话,并在工具调用结束后立即销毁会话,是一种轻量化的使用方式
| 特性 | 有状态客户端 | 无状态客户端 |
|---|---|---|
| 会话管理 | 手动 connect()/close() |
自动管理 |
| 性能 | 多次调用共享会话 | 每次调用新建会话 |
| 适用场景 | 需要状态保持(如浏览器) | 简单工具调用 |
| 资源开销 | 低(连接复用) | 高(每次新建连接) |
StatefulClientBase 是有状态客户端的基类,管理持久会话,适用于需要维护状态的 MCP 服务器(如浏览器自动化)。
1 | is_connected: bool # 连接状态 |
StatefulClientBase 使用 Python contextlib 的 AsyncExitStack 来管理异步资源的生命周期,包括与 MCP 服务器的 HTTP 连接和逻辑回话两种资源。
HttpStatefulClient(HTTP 有状态客户端)
HttpStatefulClient 继承自 StatefulClientBase,实现了基于 HTTP 协议的有状态 MCP 客户端。transport 可以是 streamable_http 或者 sse:
1 | class HttpStatefulClient(StatefulClientBase): |
-
streamable HTTP是原生 HTTP 流传输机制,可以将任意数据(二进制、纯文本、JSON块)以 chunk 方式传输。服务器在 Header 中声明Transfer-Encoding: chunked开启流传输模式。 -
SSE(Server-Sent Events)则是 HTTP 之上的构造出的特定传输格式,专门为
服务器推向客户端设计的。通过event、data等固定格式的文本消息来传递数据
1 | Content-Type: text/event-stream |
HttpStatelessClient (HTTP 无状态客户端)
每次工具调用创建新会话,适用于无状态 MCP 服务器:
1 | class HttpStatelessClient(MCPClientBase): |
StdIOStatefulClient (StdIO 有状态客户端)
基于 STDIO Transport 的 MCP 客户端,它总是有状态的,当调用 connect() 时,它将在本地启动 MCP 服务器然后通过标准输入/输出与 MCP 服务器通信。
1 | class StdIOStatefulClient(StatefulClientBase): |
MCPToolFunction (MCP 工具函数封装)
上篇文章中说过,所有的工具函数最终都会通过 register_tool_function 注册到 Toolkit 中,这些工具函数可以是普通的 Python 函数,也可以是 MCP 工具函数。AgentScope 框架将 MCP 工具函数 用 MCPToolFunction 来表示,其核心目的是将 MCP 工具函数直接封装为一个 Callable 对象,之后 Toolkit 就可以不加区别地处理普通函数和 MCP 函数。
MCPToolFunction 实现了 __call__ 方法,因此它的对象就是一个可调用对象,而在 __call__ 方法中,它会发起 MCP 调用:
1 | class MCPToolFunction: |
而在 register_tool_function() 实现中,对于 MCPToolFunction 类型的可调用对象,直接获取其 json schema 即可:
1 | def register_tool_function(): |
通过 Toolkit 管理 MCP 工具
之前说过,AgentScope 通过 Tookit 管理所有工具,也包括 MCP 工具,为了将 MCP 工具注册到 Toolkit 中,可以调用其提供的 register_mcp_client:
1 | async def register_mcp_client(): |
而 remove_mcp_clients 方法则用于移除指定 MCP client 所提供的工具。
其他说明
MCP client 提供了 get_callable_function 方法,可以根据工具名称直接获取 MCPToolFunction 对象,这就使得我们可以在 Agent 代码中直接手动调用某个工具,不一定总是依靠 LLM 来触发工具调用:
1 | func_obj = await stateless_client.get_callable_function() |
另外也可以通过这种方法来单独将某个 MCPToolFunction 对象注册到 Toolkit 中。
以上我们就整体介绍了 AgentScope 的 MCP 模块实现,以及 Toolkit 是如何管理 MCP 工具的。接下来我们再来学习 Toolkit 是如何实现 Skill 的。
Agent Skill
Skill(技能) 这个是由 Anthropic 公司率先提出并系统化定义的,目的是为 LLM 提供一种模块化的能力扩展机制。可以把 Skill 理解为给 AI 准备的 专家级工作交接手册 或 能力扩展包。它不只是简单的提示词,而是将特定领域的专业知识、标准操作流程(SOP)和可执行脚本封装成一个独立的能力单元。当 LLM 需要执行相关任务时,会自动判断并加载合适的 Skill。
- Skill(技能):是一套流程和规范,像一个
方法论库,教会 AI 如何规划步骤、处理特定任务 - Tool(工具):则提供原子化的能力/功能接口,可以执行某个具体的动作
通过 Skill,可以将高频复用的 复杂工作流 固化为一个 一键调用 的能力单元。一个 Skill 本身就是一个文件夹,其核心是 SKILL.md 文件。其中,SKILL.md 顶部的 YAML 元数据(YAML frontmatter)至关重要,它定义了 skill 的名称、描述等基本信息,LLM 最开始只会加载每个 Skill 的元数据内容,并依靠这些元数据内容,来判断是否时候需要使用该 Skill。当 LLM 确定为了完成某个任务,需要用到该 Skill 时,才会完整加载该 Skill 的指令。这样就实现了:
- 按需加载(Load on Demand):不会让 skill 占用过多上下文窗口
- 意图驱动检索(Intent-Driven Retrieval):元数据中的 description 字段承担了
检索索引的角色,LLM 通过语义匹配快速定位正确的 Skill - 资源分级(Tiered Resources) —— SKILL.md 内部还可以进一步引用外部文件,实现更深层的按需加载
接下来我们再来分析 AgentScope 如何实现对 Skill 的支持。
Toolkit 如何管理 Skill
在 Toolkit 中,通过 skill 属性来管理所有的技能,它是一个字典类型,key 是 skill 的名称,value 是一个 AgentSkill 对象。
1 | class Toolkit(StateModule): |
1 | # 定义 Agent skill 类 |
Toolkit 类提供 register_agent_skill() 方法来注册技能,它只有一个参数,即指定该 skill 所在目录的路径。register_agent_skill() 的核心逻辑就是读取该目录下的 SKILL.md 文件,并从中提取 YAML 前置部分的元数据信息,得到该 skill 的名称和描述,然后将其封装为一个 AgentSkill 对象,并存储到 self.skills 中。
1 | def register_agent_skill(self, skill_dir: str) -> None: |
同样提供了 remove_agent_skill() 方法来移除技能:
1 | def remove_agent_skill(self, name: str) -> None: |
LLM 如何感知 SKILL
在向 Agent 注册技能后,LLM 又是如何感知这些技能的存在呢?答案就是 提示词工程。Toolkit 的 system_prompt 属性提供的系统提示词中,包含了 skill 相关的提示词内容。而通过属性的方式来访问 sys_prompt 属性时,使得系统提示词是动态计算的,技能发生变更后可以立即生效。
1 | class ReActAgent(ReActAgentBase): |
可以看到,智能体的系统提示词包含两部分构成:
- 用户创建 ReActAgent 时所提供的系统提示词
_sys_prompt - Toolkit 中技能相关的提示词
get_agent_skill_prompt 用来获取技能相关的提示词:
1 | def get_agent_skill_prompt(self) -> str | None: |
从代码实现可以看出,技能提示词也由两个部分构成:
- 技能的整体指令
_agent_skill_instruction - 和所有技能的描述信息:每个描述信息都是通过
_agent_skill_template渲染得到
这两个属性都可以定制的,创建 Toolkit 对象时可以通过参数设置这两个属性:
1 | class Toolkit(StateModule): |
如果没有设置的话,技能的整体指令 默认内容是:
1 | "# Agent Skills\n" |
而默认的技能描述渲染模版则是,这个模版其实就是包含 Skill 名称、描述和路径信息。
1 | ## {name} |
实际例子
我们来通过一个实际例子来看下注册 Skill 之后, ReActAgent 的系统提示词到底是怎么样的:
1 | # -*- coding: utf-8 -*- |
这里例子中,我们注册了两个技能:python_helper 和 data_analyzer。来看下最后生成的系统提示词:
1 | 你是一个智能助手。 |
技能小结
以上就分析了 AgentScope 是如何支持 Skill 机制的,包括如何注册、移除提示词,以及如何通过系统提示词来让 LLM 知道什么是 Agent Skills 以及当前已有 skill 的描述信息。最后需要注意一点,为了让 ReActAgent 能够在运行过程中顺利加载某个 skill 的具体指令(SKILL.md 文件),需要给 Agent 配置文件读取工具:
1 | toolkit.register_tool_function(view_text_file) # 文件读取工具 |
小结
这篇文章我们分析了 AgentScope 是如何支持 MCP 工具和 Agent Skills 这两种机制的。通过这篇文章以及上一篇文章,我们就整体分析了 AgentScope 的 工具系统 的实现原理。下一篇文章我们将开始介绍 AgentScope 是如何处理支持各种 LLM 的。