【源码级开发】Qwen3接入MCP,企业级智能体开发实战!
Qwen3接入MCP智能体开发实战(上)
一、MCP技术与Qwen3原生MCP能力介绍
1.智能体开发核心技术—MCP
1.1 Function calling技术回顾
如何快速开发一款智能体应用,最关键的技术难点就在于如何让大模型高效稳定的接入一些外部工具。而在MCP技术诞生之前,最主流的方法,是借助Function calling技术来打通大模型和外部工具之间的联系,换而言之,也就是借助Function calling,来让大模型灵活的调用外部工具。
例如一个典型的Function calling示例,我们希望让大模型能够调用一些天气API查询即时天气,此时我们就需要创建一个查询天气的外部函数,负责调用天气API来查询天气,同时将外部函数的说明传递给大模型,使其能够根据用户意图,在必要的时候向外部函数发起调用请求。
毫无疑问,Function calling的诞生意义重大,这项技术目前也成为大模型调用外部工具的基本技术范式,哪怕是MCP盛行的今天,底层仍然是Function calling执行流程。
1.2 Qwen3 Function calling能力介绍
不过为了更好的学习本期公开课,需要重点强调的是关于大模型的Function calling的能力如何而来。我们都知道,对于当前大模型来说,有些模型有Function calling能力,如DeepSeek-V3模型,而有些模型没有,如DeepSeek-R1模型:
而对于Qwen3全系列模型来说,不仅支持Function calling,还支持工具的并联、串联调用甚至是自动debug:
这里我们在公开课参考资料中,为大家整理了一份从零手动实现Qwen3 Function calling的代码实战流程:
下图扫码即可领取:
本期公开课以Qwen3模型接入MCP技术为主,模型底层function calling实现方法可参考上述资料进行学习。
1.3 Qwen3 Function calling能力从何而来
那模型是如何具备Function calling能力的呢?答案是通过模型训练。对于Qwen3模型来说,由于在训练阶段(指令微调阶段)就带入了大量的类似如下的工具调用对话数据进行训练,因此能够识别外部工具并发起对外部工具调用的请求。而类似的,R1模型的训练过程没有工具调用数据,因此就不具备Function calling能力。
而Function calling的能力,是大模型顺利开启MCP功能的基础。
1.4 MCP技术本质:Function calling的更高层实现
而近一段时间大火的MCP技术,其实就可以将其理解为Function calling技术的更高层封装和实现。传统的Function calling技术要求围绕不同的外部工具API单独创建一个外部函数,类似一把锁单独配一把钥匙,而一个智能体又往往涉及到多个外部工具设计,因此开发工作量很大。
而MCP技术,全称为Model Context Protocol,模型上下文协议,是一种开发者共同遵守的协议,在这个协议框架下,大家围绕某个API开发的外部工具就能够共用,从而大幅减少重复造轮子的时间。
2 MCP技术概念介绍
2.1 MCP服务器(server)与客户端(client)概念介绍
不同于Function calling技术,MCP技术是对于大模型和外部工具的另一种划分方式,也就是说在MCP技术体系中,此时MCP会将外部工具运行脚本称作服务器,而接入这些外部工具的大模型运行环境称作客户端。
一个客户端可以接入多个不同类型的服务器的,但要求是都可以遵循MCP通信协议。简单理解就是MCP服务器的输出内容是一种标准格式的内容,只能被MCP客户端所识别。在客户端和服务器都遵循MCP协议的时候,客户端就能够像Function calling中大模型调用外部工具一样,调用MCP服务器里面的工具。
2.2 MCP服务器集合
暂时抛开底层原理不谈,在MCP技术爆发的这几个月,市面上已经诞生了成百上千的MCP服务器,甚至还出现了大量的MCP服务器集合网站:
-
MCP官方服务器合集:https://github.com/modelcontextprotocol/servers
-
MCP Github热门导航:https://github.com/punkpeye/awesome-mcp-servers
-
Smithery:https://smithery.ai/
-
MCP导航:https://mcp.so/
在实际进行智能体开发过程中,我们可以参考这些网站上的MCP工具,并有选择的对其进行调用。但需要注意的是,无论这些网站的组织形式多么花样百出,但实际上当我们本地调用MCP工具的时候,都是通过uvx或者npx将对应的库下载到本地,然后再进行运行。
3. MCP服务器接入示例与标准流程讲解
3.1 MCP服务器接入示例
而在MCP技术大爆发的今天,接入一个MCP工具也是非常简单,以下是一个将高德地图导航MCP(服务器)接入Cherry Studio(客户端)的示例:
我们能看到,现在如果想要接入一个MCP工具,我们只需要在支持MCP功能的客户端中写入相关配置即可。例如我们只需要在Cherry Studio的MCP配置文档中写入如下字段:
"amap-maps": {
"isActive": true,
"command": "npx",
"args": [
"-y",
"@amap/amap-maps-mcp-server"],
"env": {
"AMAP_MAPS_API_KEY": "YOUR_API_KRY"},
"name": "amap-maps"}
即可让大模型自动关联高德MCP工具(服务器),而一个高德MCP服务器的API有几十种之多:
可以说是覆盖了出行生活的放方面。而当一个大模型接入高德MCP服务器后,就能瞬间化身出行规划智能体。
3.2 MCP工具标准接入流程
在上述示例中,我们不难发现,一个MCP服务器标准接入流程是通过写入一个配置文件来完成的。而在支持MCP功能的客户端(如Cherry Studio)中写入MCP工具的配置,其本质就是先将指定的MCP工具下载到本地,然后在有需要的时候对其进行调用。例如高德MCP配置文件如下:
"amap-maps": {
"isActive": true,
"command": "npx",
"args": [
"-y",
"@amap/amap-maps-mcp-server"],
"env": {
"AMAP_MAPS_API_KEY": "YOUR_API_KRY"},
"name": "amap-maps"}
代表的含义就是我们需要先使用如下命令:
npx-y @amap/amap-maps-mcp-server
对这个库@amap/amap-maps-mcp-server进行下载,然后在本地运行,当有必要的时候调用这个库里面的函数执行相关功能。
而这个@amap/amap-maps-mcp-server库是一个托管在https://www.npmjs.com/上的库,
可以使用npx命令进行下载。搜索库名即可看到这个库的完整代码,https://www.npmjs.com/package/@amap/amap-maps-mcp-server:
而这种通过配置文件来进行MCP工具下载的方式,最早由Claude(MCP技术的提出者)提出并被广泛接纳。
4. Qwen3原生MCP能力介绍
4.1 Qwen3原生MCP能力效果
而作为新一代最强开源大模型,Qwen 3不仅拥有非常强悍的推理和对话性能,而且为了更好的应对智能体开发需求,Qwen3模型还是全球首款原生支持MCP功能的大模型,能够更好的理解MCP工具能力、更好的规划多工具调用流程,因此哪怕面对复杂任务,也能做到游刃有余。
不可否认,智能体开发就是当下大模型技术最核心的应用需求,而Qwen3的原生MCP功能,自然就是当下无数技术人最关注的模型功能特性。为了更好的展示模型调用MCP工具的实战效果,千问官方在模型发布公告中特地展示一段Qwen3-32B模型调用多项MCP工具的实操流程,在这个demo中,用户要求绘制Qwen模型GitHub项目主页历史星标增长曲线图。
而实际整个任务执行过程非常惊艳,在没有任何额外的提示的情况下,Qwen3-32B模型调用了包括fetch、time、code-interpreter、filesystem在内的多项MCP工具,并通过这些工具组合调用,完成不同时间点的网站信息检索、信息采集与本地记录、借助Python绘制图像等一系列工作,总耗时约1分钟左右。而要做到这点,我们仅需让Qwen3模型接入MCP工具即可。当然为了验证Qwen3到底有没有这么强,我们团队也使用Qwen-chat进行了复现,结果和官方展示的无异。
确实能够看出Qwen3模型对于外部工具识别和调用能力非常强悍。
4.2 Qwen3原生MCP能力本质与内置提示词模板解读
其实无论什么模型,所谓MCP能力,指的就是外部工具识别和调用能力,也就是Function calling能力。换而言之,Qwen3的MCP能力强悍,指的就是对于外部工具识别和调用的能力很强。这里我们可以通过观察Qwen3模型的内置提示词模板,看出模型是如何识别外部工具的,以下是Qwen3内置提示词模板解析:
Part 1.工具调用(Function Calling)支持部分
{%- if tools %}{{- '<|im_start|>system\n' }}{%- ifmessages[0].role == 'system' %}{{- messages[0].content + '\n\n' }}{%- endif %}{{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n..." }}...
解释:
-
如果传入了 tools(即 function calling 的函数签名),会优先构造 <|im_start|>system 开头的一段系统提示,告诉模型可以调用工具。
-
这段提示包含:
-
# Tools 开头的说明文字;
-
tools 列表,每个工具(函数)都通过 tojson 转换为 JSON;
-
如何使用 <tool_call> 标签返回工具调用的结果。
-
Part 2.系统消息处理
{%- ifmessages[0].role == 'system' %}{{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
{%- endif %}
解释:
-
如果首条消息是 system,则会作为系统设定(system prompt)处理,加上 <|im_start|>system\n ... <|im_end|>\n。
Part 3.多轮消息回显处理
{%- for message in messages %}{%- if (message.role == "user") ... %}{{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
解释:
-
针对用户(user)、助手(assistant)、工具响应(tool)等不同角色进行处理。
-
使用 <|im_start|>role\n...<|im_end|> 包裹每一轮对话。
4Assistant 角色的特殊处理(含推理内容)
{%- if message.role == "assistant" %}...
<think>\n...reasoning_content...\n</think>
解释:
-
若助手消息中包含 <think> 内容,会将其拆分为“推理部分”和“回复正文”。
-
如果存在 tool_calls,还会附加一段 <tool_call> JSON 标签。
Part 5.工具响应处理(role = tool)
<tool_response>\n...内容...\n</tool_response>
解释:
-
模型回复 <tool_call> 后,你会给出 <tool_response>。
-
这部分内容会包在 user role 内部,以 <tool_response> 标签封装,用来模拟用户获得工具调用结果。
Part 6.混合推理模式开启方法
{%- if add_generation_prompt %}{{- '<|im_start|>assistant\n' }}{%- if enable_thinking is defined and enable_thinking is false %}{{- '<think>\n\n</think>\n\n' }}{%- endif %}
{%- endif %}
解释:
-
如果需要生成下一轮回复,会在最后加上 <|im_start|>assistant\n 作为提示。
-
还可以通过设置 enable_thinking=false,强制加上 <think> 占位符。
5. Qwen3模型借助桌面端应用快速接入MCP工具
在了解了基本远离后,接下来我们即可使用桌面端应用Cherry Studio,快速上手尝试使用Qwen3接入MCP工具,例如先通过ollama下载模型并开启ollama服务,然后使用Cherry Studio配置天气查询MCP工具进行测试,演示效果如下:
6.公开课大纲及课件领取
-
公开课课件领取
-
MCP系列公开课参考
在此前我已经开设过一些列公开课来介绍MCP工具,相关基础内容大家可以参考如下公开课:
-
MCP技术开发入门:https://www.bilibili.com/video/BV1NLXCYTEbj/
-
MCP企业级智能体开发实战:https://www.bilibili.com/video/BV1n1ZuYjEzf/
-
不写一行代码,零门槛接入MCP:https://www.bilibili.com/video/BV1dCo7YdEgK/
-
7分钟开发&发布专属MCP!【涵盖SSE&流式HTTP MCP服务器搭建流程】:https://www.bilibili.com/video/BV1VHL6zsE5F/
二、从零到一搭建Qwen3 MCP客户端接入MCP工具
为了更好的为大家展示Qwen3+MCP底层原理,这里我们先尝试手动搭建一个Qwen3客户端,并接入本地或在线的MCP工具。需要注意的是,后续我们无论使用哪种Agent开发框架,搭建Qwen3+MCP的智能体,本质上都是这个手动实现流程的更高层的封装与更便捷的实现形式。
1. 基础环境搭建
这里我们采用uv工具进行Python项目管理,首先进入到某个自由选定的目录下,并使用uv进行项目初始化:
#cd /root/autodl-tmp/Qwen3# 创建项目目录
uv init Qwen3-MCP
cd Qwen3-MCP
pip install uv
然后输入如下命令创建虚拟环境:
# 创建虚拟环境
uv venv# 激活虚拟环境
source .venv/bin/activate
此时就构建了一个基础项目结构:
最后需要添加如下依赖:
uv add httpx openai mcp
2. 编写基于Qwen3的MCP客户端
首先设置配置文件,在当前目录创建.env文件,写入ollama驱动下的Qwen3模型调用地址和模型名称:
BASE_URL=http://localhost:11434/v1/
MODEL=qwen3:30b-a3b-fp16
LLM_API_KEY=ollama
然后在主函数main.py内写入如下内容:
import asyncio
import json
import logging
import os
import shutil
from contextlib import AsyncExitStack
from typing import Any, Dict, List, Optionalimport httpx
from dotenv import load_dotenv
from openai import OpenAI # OpenAI Python SDK
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client# Configure logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)# =============================
# 配置加载类(支持环境变量及配置文件)
# =============================
classConfiguration:
"""管理 MCP 客户端的环境变量和配置文件"""def__init__(self) -> None:load_dotenv()
# 从环境变量中加载 API key, base_url 和 modelself.api_key = os.getenv("LLM_API_KEY")self.base_url = os.getenv("BASE_URL")self.model = os.getenv("MODEL")
ifnot self.api_key:
raise ValueError("❌ 未找到 LLM_API_KEY,请在 .env 文件中配置")@staticmethod
defload_config(file_path: str) -> Dict[str, Any]:
"""从 JSON 文件加载服务器配置Args:file_path: JSON 配置文件路径Returns:包含服务器配置的字典"""
with open(file_path, "r") as f:
return json.load(f)# =============================
# MCP 服务器客户端类
# =============================
classServer:
"""管理单个 MCP 服务器连接和工具调用"""def__init__(self, name: str, config: Dict[str, Any]) -> None:self.name: str = nameself.config: Dict[str, Any] = configself.session: Optional[ClientSession] = Noneself.exit_stack: AsyncExitStack = AsyncExitStack()self._cleanup_lock = asyncio.Lock()asyncdefinitialize(self) -> None:
"""初始化与 MCP 服务器的连接"""
# command 字段直接从配置获取command = self.config["command"]
if command isNone:
raise ValueError("command 不能为空")server_params = StdioServerParameters(command=command,args=self.config["args"],env={**os.environ, **self.config["env"]} if self.config.get("env") elseNone,)
try:stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))read_stream, write_stream = stdio_transportsession = await self.exit_stack.enter_async_context(ClientSession(read_stream, write_stream))
await session.initialize()self.session = session
except Exception as e:logging.error(f"Error initializing server {self.name}: {e}")
await self.cleanup()
raiseasyncdeflist_tools(self) -> List[Any]:
"""获取服务器可用的工具列表Returns:工具列表"""
ifnot self.session:
raise RuntimeError(f"Server {self.name} not initialized")tools_response = await self.session.list_tools()tools = []
for item in tools_response:
if isinstance(item, tuple) and item[0] == "tools":
for tool in item[1]:tools.append(Tool(tool.name, tool.description, tool.inputSchema))
return toolsasyncdefexecute_tool(self, tool_name: str, arguments: Dict[str, Any], retries: int = 2, delay: float = 1.0) -> Any:
"""执行指定工具,并支持重试机制Args:tool_name: 工具名称arguments: 工具参数retries: 重试次数delay: 重试间隔秒数Returns:工具调用结果"""
ifnot self.session:
raise RuntimeError(f"Server {self.name} not initialized")attempt = 0
while attempt < retries:
try:logging.info(f"Executing {tool_name} on server {self.name}...")result = await self.session.call_tool(tool_name, arguments)
return result
except Exception as e:attempt += 1logging.warning(
f"Error executing tool: {e}. Attempt {attempt} of {retries}.")
if attempt < retries:logging.info(f"Retrying in {delay} seconds...")
await asyncio.sleep(delay)
else:logging.error("Max retries reached. Failing.")
raiseasyncdefcleanup(self) -> None:
"""清理服务器资源"""
asyncwith self._cleanup_lock:
try:
await self.exit_stack.aclose()self.session = None
except Exception as e:logging.error(f"Error during cleanup of server {self.name}: {e}")# =============================
# 工具封装类
# =============================
classTool:
"""封装 MCP 返回的工具信息"""def__init__(self, name: str, description: str, input_schema: Dict[str, Any]) -> None:self.name: str = nameself.description: str = descriptionself.input_schema: Dict[str, Any] = input_schemadefformat_for_llm(self) -> str:
"""生成用于 LLM 提示的工具描述"""args_desc = []
if"properties"in self.input_schema:
for param_name, param_info in self.input_schema["properties"].items():arg_desc = f"- {param_name}: {param_info.get('description', 'No description')}"
if param_name in self.input_schema.get("required", []):arg_desc += " (required)"args_desc.append(arg_desc)
returnf"""
Tool: {self.name}
Description: {self.description}
Arguments:
{chr(10).join(args_desc)}
"""# =============================
# LLM 客户端封装类(使用 OpenAI SDK)
# =============================
classLLMClient:
"""使用 OpenAI SDK 与大模型交互"""def__init__(self, api_key: str, base_url: Optional[str], model: str) -> None:self.client = OpenAI(api_key=api_key, base_url=base_url)self.model = modeldefget_response(self, messages: List[Dict[str, Any]], tools: Optional[List[Dict[str, Any]]] = None) -> Any:
"""发送消息给大模型 API,支持传入工具参数(function calling 格式)"""payload = {
"model": self.model,
"messages": messages,
"tools": tools,}
try:response = self.client.chat.completions.create(**payload)
return response
except Exception as e:logging.error(f"Error during LLM call: {e}")
raise# =============================
# 多服务器 MCP 客户端类(集成配置文件、工具格式转换与 OpenAI SDK 调用)
# =============================
classMultiServerMCPClient:
def__init__(self) -> None:
"""管理多个 MCP 服务器,并使用 OpenAI Function Calling 风格的接口调用大模型"""self.exit_stack = AsyncExitStack()config = Configuration()self.openai_api_key = config.api_keyself.base_url = config.base_urlself.model = config.modelself.client = LLMClient(self.openai_api_key, self.base_url, self.model)
# (server_name -> Server 对象)self.servers: Dict[str, Server] = {}
# 各个 server 的工具列表self.tools_by_server: Dict[str, List[Any]] = {}self.all_tools: List[Dict[str, Any]] = []asyncdefconnect_to_servers(self, servers_config: Dict[str, Any]) -> None:
"""根据配置文件同时启动多个服务器并获取工具servers_config 的格式为:{"mcpServers": {"sqlite": { "command": "uvx", "args": [ ... ] },"puppeteer": { "command": "npx", "args": [ ... ] },...}}"""mcp_servers = servers_config.get("mcpServers", {})
for server_name, srv_config in mcp_servers.items():server = Server(server_name, srv_config)
await server.initialize()self.servers[server_name] = servertools = await server.list_tools()self.tools_by_server[server_name] = toolsfor tool in tools:
# 统一重命名:serverName_toolNamefunction_name = f"{server_name}_{tool.name}"self.all_tools.append({
"type": "function",
"function": {
"name": function_name,
"description": tool.description,
"input_schema": tool.input_schema}})# 转换为 OpenAI Function Calling 所需格式self.all_tools = await self.transform_json(self.all_tools)logging.info("\n✅ 已连接到下列服务器:")
for name in self.servers:srv_cfg = mcp_servers[name]logging.info(f" - {name}: command={srv_cfg['command']}, args={srv_cfg['args']}")logging.info("\n汇总的工具:")
for t in self.all_tools:logging.info(f" - {t['function']['name']}")asyncdeftransform_json(self, json_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""将工具的 input_schema 转换为 OpenAI 所需的 parameters 格式,并删除多余字段"""result = []
for item in json_data:
ifnot isinstance(item, dict) or"type"notin item or"function"notin item:
continueold_func = item["function"]
ifnot isinstance(old_func, dict) or"name"notin old_func or"description"notin old_func:
continuenew_func = {
"name": old_func["name"],
"description": old_func["description"],
"parameters": {}}
if"input_schema"in old_func and isinstance(old_func["input_schema"], dict):old_schema = old_func["input_schema"]new_func["parameters"]["type"] = old_schema.get("type", "object")new_func["parameters"]["properties"] = old_schema.get("properties", {})new_func["parameters"]["required"] = old_schema.get("required", [])new_item = {
"type": item["type"],
"function": new_func}result.append(new_item)
return resultasyncdefchat_base(self, messages: List[Dict[str, Any]]) -> Any:
"""使用 OpenAI 接口进行对话,并支持多次工具调用(Function Calling)。如果返回 finish_reason 为 "tool_calls",则进行工具调用后再发起请求。"""response = self.client.get_response(messages, tools=self.all_tools)
# 如果模型返回工具调用
if response.choices[0].finish_reason == "tool_calls":
whileTrue:messages = await self.create_function_response_messages(messages, response)response = self.client.get_response(messages, tools=self.all_tools)
if response.choices[0].finish_reason != "tool_calls":
break
return responseasyncdefcreate_function_response_messages(self, messages: List[Dict[str, Any]], response: Any) -> List[Dict[str, Any]]:
"""将模型返回的工具调用解析执行,并将结果追加到消息队列中"""function_call_messages = response.choices[0].message.tool_callsmessages.append(response.choices[0].message.model_dump())
for function_call_message in function_call_messages:tool_name = function_call_message.function.nametool_args = json.loads(function_call_message.function.arguments)
# 调用 MCP 工具function_response = await self._call_mcp_tool(tool_name, tool_args)messages.append({
"role": "tool",
"content": function_response,
"tool_call_id": function_call_message.id,})
return messagesasyncdefprocess_query(self, user_query: str) -> str:
"""OpenAI Function Calling 流程:1. 发送用户消息 + 工具信息2. 若模型返回 finish_reason 为 "tool_calls",则解析并调用 MCP 工具3. 将工具调用结果返回给模型,获得最终回答"""messages = [{"role": "user", "content": user_query}]response = self.client.get_response(messages, tools=self.all_tools)content = response.choices[0]logging.info(content)
if content.finish_reason == "tool_calls":tool_call = content.message.tool_calls[0]tool_name = tool_call.function.nametool_args = json.loads(tool_call.function.arguments)logging.info(f"\n[ 调用工具: {tool_name}, 参数: {tool_args} ]\n")result = await self._call_mcp_tool(tool_name, tool_args)messages.append(content.message.model_dump())messages.append({
"role": "tool",
"content": result,
"tool_call_id": tool_call.id,})response = self.client.get_response(messages, tools=self.all_tools)
return response.choices[0].message.content
return content.message.contentasyncdef_call_mcp_tool(self, tool_full_name: str, tool_args: Dict[str, Any]) -> str:
"""根据 "serverName_toolName" 格式调用相应 MCP 工具"""parts = tool_full_name.split("_", 1)
if len(parts) != 2:
returnf"无效的工具名称: {tool_full_name}"server_name, tool_name = partsserver = self.servers.get(server_name)
ifnot server:
returnf"找不到服务器: {server_name}"resp = await server.execute_tool(tool_name, tool_args)
return resp.content if resp.content else"工具执行无输出"asyncdefchat_loop(self) -> None:
"""多服务器 MCP + OpenAI Function Calling 客户端主循环"""logging.info("\n🤖 多服务器 MCP + Function Calling 客户端已启动!输入 'quit' 退出。")messages: List[Dict[str, Any]] = []
whileTrue:query = input("\n你: ").strip()
if query.lower() == "quit":
break
try:messages.append({"role": "user", "content": query})messages = messages[-20:] # 保持最新 20 条上下文response = await self.chat_base(messages)messages.append(response.choices[0].message.model_dump())result = response.choices[0].message.content
# logging.info(f"\nAI: {result}")print(f"\nAI: {result}")
except Exception as e:print(f"\n⚠️ 调用过程出错: {e}")asyncdefcleanup(self) -> None:
"""关闭所有资源"""
await self.exit_stack.aclose()# =============================
# 主函数
# =============================
asyncdefmain() -> None:
# 从配置文件加载服务器配置config = Configuration()servers_config = config.load_config("servers_config.json")client = MultiServerMCPClient()
try:
await client.connect_to_servers(servers_config)
await client.chat_loop()
finally:
try:
await asyncio.sleep(0.1)
await client.cleanup()
except RuntimeError as e:
# 如果是因为退出 cancel scope 导致的异常,可以选择忽略
if"Attempted to exit cancel scope"in str(e):logging.info("退出时检测到 cancel scope 异常,已忽略。")
else:
raiseif __name__ == "__main__":asyncio.run(main())
以上客户端为我们团队独家研发的MCP客户端脚本,具备以下功能:
-
根据.env配置,自由切换底层模型,如可接入ollama、vLLM驱动的模型,也可以接入OpenAI、DeepSeek等在线模型;
-
根据servers_config.json读取多个MCP服务,可以读取本地MCP工具,也可以下载在线MCP工具并进行使用;
-
能够进行基于命令行的实现多轮对话;
-
能够同时连接并调用多个MCP server上的多个工具,并能实现多工具的并联和串联调用;
3. 配置MCP服务器
接下来继续创建一个servers_config.json脚本,并写入我们希望调用的MCP工具,例如我们想要调用Filesystem MCP,该MCP是一个最基础同时也是最常用的MCP服务器,同时也是官方推荐的服务器,服务器项目地址:https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
借助Filesystem,我们可以高效便捷操作本地文件夹。同时Filesystem也是一个js项目,源码托管在npm平台上:(https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem)
我们直接创建servers_config.josn并写入如下配置即可调用:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/path/to/other/allowed/dir"]}}
}
4.开启Qwen3+MCP调用流程
然后就可以在当前项目的主目录下输入uv run进行运行:
uv run main.py
能够看到,此时已经关联了filesystem多项外部工具。接下来进行对话:
由于模型开启了思考模式,所以能看到的具体过程。
接下来继续尝试调用外部工具:
能够看到,Qwen3 MCP Client能够顺利调用外部工具。
此外,我们还可以让Qwen3 MCP Client接入自定义的函数,例如我们在当前项目中创建两个MCP server脚本:
-
weather_server.py:查询天气MCP服务器
并写入如下内容
import os
import json
import httpx
from typing import Any
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP# 初始化 MCP 服务器
mcp = FastMCP("WeatherServer")# OpenWeather API 配置
OPENWEATHER_API_BASE = "https://api.openweathermap.org/data/2.5/weather"
API_KEY = "YOUR_API_KEY"# 填写你的OpenWeather-API-KEY
USER_AGENT = "weather-app/1.0"asyncdeffetch_weather(city: str) -> dict[str, Any] | None:
"""从 OpenWeather API 获取天气信息。:param city: 城市名称(需使用英文,如 Beijing):return: 天气数据字典;若出错返回包含 error 信息的字典"""params = {
"q": city,
"appid": API_KEY,
"units": "metric",
"lang": "zh_cn"}headers = {"User-Agent": USER_AGENT}asyncwith httpx.AsyncClient() as client:
try:response = await client.get(OPENWEATHER_API_BASE, params=params, headers=headers, timeout=30.0)response.raise_for_status()
return response.json() # 返回字典类型
except httpx.HTTPStatusError as e:
return {"error": f"HTTP 错误: {e.response.status_code}"}
except Exception as e:
return {"error": f"请求失败: {str(e)}"}defformat_weather(data: dict[str, Any] | str) -> str:
"""将天气数据格式化为易读文本。:param data: 天气数据(可以是字典或 JSON 字符串):return: 格式化后的天气信息字符串"""
# 如果传入的是字符串,则先转换为字典
if isinstance(data, str):
try:data = json.loads(data)
except Exception as e:
returnf"无法解析天气数据: {e}"# 如果数据中包含错误信息,直接返回错误提示
if"error"in data:
returnf"⚠️ {data['error']}"# 提取数据时做容错处理city = data.get("name", "未知")country = data.get("sys", {}).get("country", "未知")temp = data.get("main", {}).get("temp", "N/A")humidity = data.get("main", {}).get("humidity", "N/A")wind_speed = data.get("wind", {}).get("speed", "N/A")
# weather 可能为空列表,因此用 [0] 前先提供默认字典weather_list = data.get("weather", [{}])description = weather_list[0].get("description", "未知")return (
f"🌍 {city}, {country}\n"
f"🌡 温度: {temp}°C\n"
f"💧 湿度: {humidity}%\n"
f"🌬 风速: {wind_speed} m/s\n"
f"🌤 天气: {description}\n")@mcp.tool()
asyncdefquery_weather(city: str) -> str:
"""输入指定城市的英文名称,返回今日天气查询结果。:param city: 城市名称(需使用英文):return: 格式化后的天气信息"""data = await fetch_weather(city)
return format_weather(data)if __name__ == "__main__":
# 以标准 I/O 方式运行 MCP 服务器mcp.run(transport='stdio')
-
write_server.py:写入本地文档MCP服务器
并写入如下内容
import json
import httpx
from typing import Any
from mcp.server.fastmcp import FastMCP# 初始化 MCP 服务器
mcp = FastMCP("WriteServer")
USER_AGENT = "write-app/1.0"@mcp.tool()
asyncdefwrite_file(content: str) -> str:
"""将指定内容写入本地文件。:param content: 必要参数,字符串类型,用于表示需要写入文档的具体内容。:return:是否成功写入"""
return"已成功写入本地文件。"if __name__ == "__main__":
# 以标准 I/O 方式运行 MCP 服务器mcp.run(transport='stdio')
然后需改servers_config.json配置文件,写入如下内容:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/root/autodl-tmp/Qwen3/Qwen3-MCP"]},
"weather": {
"command": "python",
"args": ["weather_server.py"]},
"write": {
"command": "python",
"args": ["write_server.py"]}}
}
然后再次开启对话,就能看到加载了更多工具进来:
并可进行多MCP服务器的多工具并行调用:
和串联调用:
至此我们就完成了Qwen-3接入在线MCP工具的流程。
-
完整项目脚本已上传至课件网盘
相关文章:
【源码级开发】Qwen3接入MCP,企业级智能体开发实战!
Qwen3接入MCP智能体开发实战(上) 一、MCP技术与Qwen3原生MCP能力介绍 1.智能体开发核心技术—MCP 1.1 Function calling技术回顾 如何快速开发一款智能体应用,最关键的技术难点就在于如何让大模型高效稳定的接入一些外部工具。而在MCP技术…...
回调函数应用示例
回调函数是一种通过函数指针(或引用)调用的函数,它在特定事件或条件发生时被另一个函数调用。回调函数的核心思想是将函数作为参数传递,以便在适当的时候执行自定义逻辑,常用于异步编程、事件驱动架构等场景。 业务场景…...
R语言如何解决导出pdf中文不显示的问题
前言 以前绘图都默认英文,突然要求都改成中文,呆住。。。。。。。。。 标题代码实现 ### 导入工具包 ### library(readr) library(dplyr) library(corrplot)df <- read_csv("./clinical.csv") df <- df %>% select(-id, -label)##…...
国产linux系统(银河麒麟,统信uos)使用 PageOffice自定义Word模版中的数据区域
PageOffice 国产版 :支持信创系统,支持银河麒麟V10和统信UOS,支持X86(intel、兆芯、海光等)、ARM(飞腾、鲲鹏、麒麟等)、龙芯(Mips、LoogArch)芯片架构。 在实际的Wor…...
llamafactory SFT 从断点恢复训练
背景 我使用llamafactory sft 微调模型的时候。gpu停止运行了。日志文件没有任何的报错信息。 显存还是占用状态。 查看llamafactory的进程是下述信息: 151312 151306 91 17:42 ? 03:58:10 [llamafactory-cl] 既然如此,那就只能从断点恢复训练了。 …...
C#里使用Prism.Core的例子
由于使用WPF来开发应用程序, 那么就会使用一些框架程序来加速开发,一般会使用Prism.Core来加速。 这个应用最后运行的显示如下: 第一步需要安装下面的包: <?xml version="1.0" encoding="utf-8"?> <packages><package id="Mi…...
【MySQL】数据库三大范式
目录 一. 什么是范式 二. 第一范式 三. 第二范式 不满足第二范式时可能出现的问题 四. 第三范式 一. 什么是范式 在数据库中范式其实就是一组规则,在我们设计数据库的时候,需要遵守不同的规则要求,设计出合理的关系型数据库,…...
window 显示驱动开发-分页视频内存资源
与 Microsoft Windows 2000 显示驱动程序模型不同,Windows Vista 显示驱动程序模型允许创建比可用物理视频内存总量更多的视频内存资源,然后根据需要分页进出视频内存。 换句话说,并非所有视频内存资源都同时位于视频内存中。 GPU 的管道中可…...
炼丹学习笔记3---ubuntu2004部署运行openpcdet记录
前言 环境 cuda 11.3 python 3.8 ubuntu2004 一、cuda环境检测 ylhy:~/code_ws/OpenPCDet/tools$ nvcc -V nvcc: NVIDIA (R) Cuda compiler driver Copyright (c) 2005-2021 NVIDIA Corporation Built on Sun_Mar_21_19:15:46_PDT_2021 Cuda compilation tools, release 11.3…...
美创科技针对《银行保险机构数据安全管理办法》解读
在数字化浪潮席卷下,银行保险业的运营模式发生了翻天覆地的变化,数据已然成为行业发展的核心驱动力。从客户基本信息、交易记录,到业务运营的关键数据、市场分析报告,海量数据背后潜藏巨大价值。然而,数据安全风险也随…...
activeMq 限制用户接收topic范围
1、在conf配置文件中找到jetty-realm.properties文件,添加用户信息 2、在broker标签中加入topic限制权限信息 <plugins><simpleAuthenticationPlugin><users><authenticationUser username"admin" password"admin" group…...
LIIGO ❤️ RUST 12 YEARS
LIIGO 💖 RUST 12 YEARS 今天是RUST语言1.0发布十周年纪念日。十年前的今天,2015年的今天,Rust 1.0 正式发行。这是值得全球Rust支持者隆重纪念的日子。我借此机会衷心感谢Rust语言创始人Graydon Hoare,Mozilla公司,以…...
增量学习:机器学习领域中的资源高效利用秘籍
前言 在机器学习的广袤天地中,增量学习宛如一颗冉冉升起的新星,正逐渐展现出其独特的魅力和巨大的潜力。 它是一种能让 AI 模型像人类一样,逐步学习并不断强化自身知识,同时不会遗忘过往所学信息的学习方法。随着时代的飞速发展&a…...
OpenCV 背景建模详解:从原理到实战
在计算机视觉领域,背景建模是一项基础且重要的技术,它能够从视频流中分离出前景目标,广泛应用于运动目标检测、视频监控、人机交互等场景。OpenCV 作为计算机视觉领域最受欢迎的开源库之一,提供了多种高效的背景建模算法。本文将深…...
makefile细节说明
在 Makefile中,依赖关系的左右两部分有特定的名称: 左边部分(冒号左侧) 称为 目标(Target) 右边部分(冒号右侧) 称为 依赖项(Prerequisite…...
计算机图形学编程(使用OpenGL和C++)(第2版)学习笔记 10.增强表面细节(二)法线贴图
1. 法线贴图(Normal Mapping) 法线贴图是一种在3D图形渲染中广泛使用的表面细节增强技术。它通过存储每个像素的法线信息来模拟表面的细微凹凸细节,而无需增加实际的几何复杂度。 1.1. 工作原理 纹理存储 使用RGB通道存储法线向量的XYZ分量…...
使用 OpenCV 将图像中标记特定颜色区域
在计算机视觉任务中,颜色替换是一种常见的图像处理操作,广泛用于视觉增强、目标高亮、伪彩色渲染等场景。本文介绍一种简单而高效的方式,基于 OpenCV 检测图像中接近某种颜色的区域,并将其替换为反色(对比色࿰…...
Service Mesh
目录 一、Service Mesh 的核心特点 二、Service Mesh 的典型架构 1. Sidecar 模式 2. 控制平面与数据平面分离 三、Service Mesh 解决的核心问题 四、典型应用场景 五、主流 Service Mesh 框架对比 六、挑战与局限性 七、未来趋势 总结 Istio 一、Istio 核心组件与…...
反射机制详细说明
反射机制详细说明 1. 反射的基本概念 反射(Reflection)是Java提供的一种在运行时(Runtime)动态获取类信息并操作类属性、方法和构造器的机制。通过反射,程序可以在运行时检查类、接口、字段和方法,并且可以实例化对象、调用方法、访问或修改字段值,甚至操作私有成员,…...
基于Mongodb的分布式文件存储实现
分布式文件存储的方案有很多,今天分享一个基于mongodb数据库来实现文件的存储,mongodb支持分布式部署,以此来实现文件的分布式存储。 基于 MongoDB GridFS 的分布式文件存储实现:从原理到实战 一、引言 当系统存在大量的图片、…...
相机Camera日志分析之九:高通相机Camx 基于预览1帧的ConfigureStreams二级日志分析详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:高通相机Camx 日志分析之三:camx hal预览1帧logcat日志opencamera详解 相机Camera日志分析之三:不想输出每秒30帧巨量日志,如何只输出1帧日志作为学习使用? 这一篇我们开始讲: 高通相机Camx 日志…...
neo4j框架:ubuntu系统中neo4j安装与使用教程
在使用图数据库的时候,经常需要用到neo4j这一图数据库处理框架。本文详细介绍了neo4j安装使用过程中的问题与解决方法。 一、安装neo4j 在安装好了ubuntu系统、docker仓库和java的前提下 在ubuntu系统命令行依次输入如下命令: # 安装依赖库 sudo apt-…...
k8s灰度发布
基于 Traefik 的加权灰度发布-腾讯云开发者社区-腾讯云 Traefik | Traefik | v1.7 Releases traefik/traefik GitHub 从上面连接下载后上传到harbor虚拟机 vagrant upload /C/Users/HP280/Downloads/traefik 下载配置文件 wget -c http://raw.githubusercontent.com/conta…...
K8S从Harbor拉取镜像
参考 配置cri-docker使kubernetes1.24以docker作为运行时_启动cirdocker_跳跃音符#3712的博客-CSDN博客 部署Harbor私有容器镜像仓库并配置Kubernetes从Harbor拉取镜像的方法_运维个西瓜的博客-CSDN博客 K8S连接Harbor私有仓库_k8s harbor 登录-CSDN博客 K8S集群配置使用私…...
【Spring Boot后端组件】mybatis-plus使用
文章目录 mybatis-plus使用一、依赖引入二、添加相关配置项三、功能详解1.自增主键2.逻辑删除3.操作时间自动填充4.其他字段自动填充5.分页查询6.自定义动态查询7.代码生成器8.代码生成器(自定义模板) mybatis-plus使用 一、依赖引入 pom.xml文件 <?xml version"1.…...
Oc语言学习 —— 重点内容总结与拓展(下)
类别(分类)和拓展 分类: 专门用来给类添加新方法 不能给类添加成员属性,添加成员属性也无法取到 注意:其实可与通过runtime 给分类添加属性,即属性关联,重写setter,getter方法 分类…...
智脑进化:神经网络如何从单层感知机迈向深度学习新纪元
第一章:神经元的启示——从生物大脑到人工神经元 1.1 生物神经元的智慧:860亿神经元的协同网络 人类大脑的860亿神经元通过突触形成动态网络,每个神经元通过树突接收信号,在胞体整合后经轴突传递输出。这种“接收-处理-输出”的…...
雷云4 鼠标滚轮单击失灵解决办法
问题现象:打开雷云4 ,滚轮单击才有反应,退出雷云4,滚轮单击没反应。 解决方案: 打开雷云4, 选中鼠标中键,选择鼠标功能,选择滚轮单击,保存 然后退出后, …...
Spring Cloud动态配置刷新:@RefreshScope与@Component的协同机制解析
在微服务架构中,动态配置管理是实现服务灵活部署、快速响应业务变化的关键能力之一。Spring Cloud 提供了基于 RefreshScope 和 Component 的动态配置刷新机制,使得开发者可以在不重启服务的情况下更新配置。 本文将深入解析 RefreshScope 与 Component…...
vue2集成可在线编辑的思维导图(simple-mind-map)
最近要求做一个可在线编辑的思维导图,经过层层调研和实测,最简单的思维导图导图实现还得是simple-mind-map组件 simple-mind-map中文文档 当前我使用的是vue2项目,目前没试过是否支持vue3,但是看官网描述他们也给了有vue3的demo项…...
【开源Agent框架】CAMEL:角色扮演+任务分解
一、项目概览:重新定义智能体协作范式 CAMEL(Communicative Agents for “Mind” Exploration of Large Language Model Society)是由camel-ai社区开发的开源多智能体框架,致力于探索智能体的规模法则(Scaling Laws)。该项目通过构建包含百万级智能体的复杂社会系统,研…...
Elasticsearch-kibana索引操作
1索引模版 添加索引 PUT /_index_template/account_transaction {"priority": 0,"index_patterns": ["account_transaction*"],"template": {"settings": {"index": {"number_of_shards": "50&q…...
【python编程从入门到到实践】第十章 文件和异常
一、读取文件 pi_digits.txt3.1415926535897932384626433832791.读取文件的全部内容 # file_reader.pyfrom pathlib import Pathpath Path("pi_digits.txt") contents path.read_text() print(contents)2.相对文件路径和绝对文件路径 当相对路径行不通时&#x…...
Reactive与Ref的故事
Vue 3的两位"响应式英雄":Reactive与Ref的故事 基本介绍:响应式的两种武器 Vue 3提供了两种创建响应式数据的主要API:reactive()和ref()。它们像两种不同的魔法工具,各有所长,共同构建Vue的响应式王国。 ┌────────────────────────…...
基于Scrapy-Redis的分布式景点数据爬取与热力图生成
1. 引言 在旅游行业和城市规划中,热门景点的数据分析具有重要意义。通过爬取景点数据并生成热力图,可以直观展示游客分布、热门区域及人流趋势,为商业决策、景区管理及智慧城市建设提供数据支持。 然而,单机爬虫在面对大规模数据…...
MySQL数据库——支持远程IP访问的设置方法总结
【系列专栏】:博主结合工作实践输出的,解决实际问题的专栏,朋友们看过来! 《项目案例分享》 《极客DIY开源分享》 《嵌入式通用开发实战》 《C语言开发基础总结》 《从0到1学习嵌入式Linux开发》 《QT开发实战》 《Android开发实…...
现在环保方面有什么新的技术动态
环保领域的技术发展迅速,尤其在“双碳”目标、数字化转型和可持续发展背景下,涌现出许多创新技术和应用。以下是当前环保领域的新技术动态(截至2024年): 一、碳中和与碳减排技术 CCUS(碳捕集、利用与封存&a…...
[模型部署] 1. 模型导出
👋 你好!这里有实用干货与深度分享✨✨ 若有帮助,欢迎: 👍 点赞 | ⭐ 收藏 | 💬 评论 | ➕ 关注 ,解锁更多精彩! 📁 收藏专栏即可第一时间获取最新推送🔔…...
Neo4j 图书馆借阅系统知识图谱设计
一、数据模型设计 节点类型 读者(Reader) 属性: reader_id, name, age, gender, phone, email, register_date 图书(Book) 属性: book_id, title, author, publisher, publish_date, isbn, price, category 图书副本(BookCopy) 属性: copy_id, status (在馆/借出/维修), loca…...
android 安装openwrt 安正步骤
安装 QEMU 模拟器 bash 复制 编辑 pkg install wget pkg install qemu-utils pkg install qemu-system-aarch64-headless 可选 x86 模拟支持: bash 复制 编辑 pkg install qemu-system-x86-64-headless ✅ 下载 OpenWRT 镜像(armvirt 64) bash 复制 编辑 mkdir -p ~/openwr…...
大规模CFD仿真计算中,SIMPLE或者PISO算法中加速压力场方程迭代求解
文章目录 在SIMSOL或PISO算法中加速压力场方程的迭代求解是提高CFD计算效率的关键。以下从算法优化、数值技巧和并行计算等方面总结加速策略:**1. 压力方程求解器的选择与优化****2. 算法层面的加速****3. 离散格式与网格优化****4. 并行计算与硬件加速****5. 代码级…...
【C#】 lock 关键字
在 C# 里,lock 关键字就是对 Monitor.Enter/Exit 的简写。它的作用是保证“同一时刻只有一个线程能进入被保护的代码块”,从而避免多个线程同时修改同一个共享状态导致竞态条件(race condition)。 一、结合Jog 的例子讲解 // Mot…...
前端脚手架开发指南:提高开发效率的核心操作
前端脚手架通过自动化的方式可以提高开发效率并减少重复工作,而最强大的脚手架并不是现成的那些工具而是属于你自己团队量身定制的脚手架!本篇文章将带你了解脚手架开发的基本技巧,帮助你掌握如何构建适合自己需求的工具,并带着你…...
职坐标AIoT技能培训课程实战解析
职坐标AIoT技能培训课程以人工智能与物联网技术深度融合为核心,构建了“理论实战行业应用”三位一体的教学体系。课程体系覆盖Python编程基础、传感器数据采集、边缘计算开发、云端服务部署及智能硬件开发全链路,通过分层递进的知识模块帮助学员建立系统…...
Yocto Project 快速构建
此文为机器辅助翻译,仅供个人学习使用,如有翻译不当之处欢迎指正 1 Yocto 项目快速构建 1.1 欢迎! 本简短文档将引导您完成使用 Yocto Project 进行典型镜像构建的流程,并介绍如何为特定硬件配置构建环境。您将使用 Yocto Proj…...
git相关配置
git相关配置 欢迎使用Markdown编辑器修改Git默认编辑器为vimgit配置默认用户名和密码: 欢迎使用Markdown编辑器 修改Git默认编辑器为vim #方法1:直接执行 git config --global core.editor vim#方法2:修改git的配置文件.git/config文件&am…...
ci/cd全流程实操
本次采用架构,gitlab + jenkins + 镜像仓库+ k8s 准备工作 一、gitlab部署 拉取镜像 部署环境: macbook m2中docker部署gitlab (m2平台架构问题,这里只能用yrzr/gitlab-ce-arm64v8 这个容器镜像) docker pull yrzr/gitlab-ce-arm64v8 在 Docker 里,–privileged=tr…...
Python中in和is关键字详解和使用
在 Python 中,in 和 is 是两个常用但含义不同的关键字,初学者很容易混淆它们的用法。下面是关于它们的详细解释、注意事项及常见示例。 一、关键字 in:成员运算符 1. 功能 用于判断某个元素是否存在于序列(如列表、元组、字符串…...
ACM模式用Scanner和System.out超时的解决方案和原理
Hi~!这里是奋斗的明志,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~ 🌱🌱个人主页:奋斗的明志 🌱🌱所属专栏:笔试强训 📚本系列文章为个人学…...
微服务中服务降级和异常的区别
在Java中,服务降级和异常处理是两个相关但不同的概念。它们的主要区别如下: 1. 服务降级(Service Degradation): 定义:服务降级是指在系统中某个服务或功能出现问题时,通过采取某些策略来降低服务的质量或…...