☰
实战解析MCP-使用本地的Qwen-2.5模型-AI协议的未来?
2026/10/1 20:10:29 网站建设 项目流程

1. 本地 Qwen-2.5 跑通 MCP 协议到底难在哪

MCP(Model Context Protocol)说白了就是给大模型装一个统一的工具插座:模型不用关心对面是文件系统、数据库还是某个 HTTP 接口,只要按协议说话,就能调用。Qwen-2.5 是通义千问开源的新一代模型,7B/14B/32B 都有,本地用 Ollama 或 vLLM 都能拉起来,中文理解和函数调用(Function Calling)能力比上一代稳不少。把这两件事拼在一起——本地 Qwen-2.5 当大脑,MCP 当手脚——就是一套完全跑在自己机器上的 Agent 工具链,数据不出内网,适合做企业内部知识库、本地文件批处理、代码仓库问答这类场景。

适合谁看:手里有 16G 以上显存的卡、想搭本地 Agent 的开发者;已经在用 Claude Desktop 或 Cline 但想换成自托管模型的同学;以及想搞明白 MCP 服务端和客户端到底怎么握手的人。

我试过最直接的路径:Ollama 起 Qwen-2.5 → 写一个 FastMCP 服务端暴露工具 → 写一个 OpenAI 兼容的客户端把工具列表喂给 Qwen → Qwen 返回 tool_calls → 客户端执行工具 → 结果回灌。整条链路里最容易翻车的地方有三个:一是 Qwen 的 tool_calls 返回格式和 OpenAI 不完全一致,二是 MCP 的 stdio 传输在 Windows 上路径带空格会挂,三是客户端 base_url 配错导致 401。下面按顺序拆开讲,每一步都给可复制的配置。

先明确一个概念边界:MCP 不是模型,也不是框架,它是一个通信协议。你可以把它理解成「AI 世界的 USB-C」——服务端是各种外设,客户端是主机,模型是主机里的 CPU。Qwen-2.5 在这里扮演 CPU 的角色,它通过客户端间接调用服务端的工具。所以整篇文章的配置分三块:模型侧(Ollama/vLLM)、协议侧(MCP Server)、工具侧(MCP Client + TaoToken 统一通道)。

2. TaoToken 前置:统一 Key 与 API 通道

在本地模型场景里,很多人会问:我模型都跑本地了,为什么还需要一个 API 通道?答案在于工具链的「最后一公里」。MCP 客户端本身要调用模型,如果你用的是 Ollama 原生接口,格式和 OpenAI 有差异,很多现成的 MCP 客户端(比如 Cline、Continue)默认只认 OpenAI 兼容格式。这时候有两个选择:一是自己写适配层,二是用一个 OpenAI 兼容的网关把请求转出去。

TaoToken 在这里的作用是提供一个统一的 OpenAI 兼容入口,让你在客户端里只配一个 Base URL 和一个 Key,就能同时对接本地模型和云端模型。它的 API 地址是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 base_url 用。控制台在https://taotoken.net/console,API Key 在https://taotoken.net/api-keys生成。

具体操作:登录控制台后,左侧菜单找到「API Keys」,点「创建新密钥」,复制出来形如sk-xxxxxxxx的字符串。这个 Key 就是你后面在客户端settings.json或.env里填的东西。注意 Key 只显示一次,丢了就重新生成。

为什么强调这一步?因为 MCP 客户端的配置里,模型接入部分和工具接入部分是分开的。工具接入靠 MCP 协议,模型接入靠 OpenAI 兼容接口。TaoToken 解决的是后者——它让你不用为每个客户端单独适配 Ollama 的/api/chat格式,统一走/v1/chat/completions。

如果你只是想先验证模型能不能通,可以直接用模型对话页面https://taotoken.net/model-chat发一条消息,看返回是否正常。这一步能排除掉 90% 的 Key 配置错误。确认能通之后,再往下配 MCP 客户端。

需要提醒的是:TaoToken 是 API 通道,不是模型本身。本地 Qwen-2.5 仍然跑在你自己的机器上,TaoToken 只是帮你把请求格式统一成 OpenAI 标准,方便各种 MCP 客户端直接接入。两者是互补关系,不是替代关系。

3. 可复制配置:Qwen-2.5 启动参数与 MCP 服务端

这一节是全文的核心,所有配置都可以直接复制。分三步:起模型、写 MCP 服务端、配客户端。

3.1 用 Ollama 拉起 Qwen-2.5

先装 Ollama,然后拉模型。14B 版本在 16G 显存上跑 Q4 量化比较稳:

ollama pull qwen2.5:14b ollama run qwen2.5:14b

如果你想让 Ollama 暴露 OpenAI 兼容接口,需要设置环境变量后启动服务:

export OLLAMA_HOST=0.0.0.0:11434 export OLLAMA_OPENAI_COMPAT=1 ollama serve

验证模型是否就绪:

curl http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen2.5:14b", "messages": [{"role": "user", "content": "你好"}] }'

如果返回里有choices字段,说明模型侧通了。注意这里的model字段必须和ollama list里的名字完全一致,大小写敏感。

3.2 写一个 FastMCP 服务端

MCP 服务端用 Python 的mcp库,先装依赖:

pip install mcp openai python-dotenv

然后写server.py,暴露一个写文件的工具:

from mcp.server.fastmcp import FastMCP mcp = FastMCP("FileWriter") @mcp.tool() def write_to_txt(filename: str, content: str) -> str: """将指定内容写入文本文件并保存到本地。 参数: filename: 文件名,例如 output.txt content: 要写入的文本内容 返回: 写入成功或失败的提示信息 """ try: with open(filename, "w", encoding="utf-8") as f: f.write(content) return f"成功写入文件 {filename}。" except Exception as e: return f"写入文件失败:{e}" if __name__ == "__main__": mcp.run(transport="stdio")

这里transport="stdio"是关键,客户端会通过标准输入输出和服务端通信。服务端本身不关心模型是谁,它只负责执行工具。

3.3 客户端配置:Base URL + Key + Model ID 三件套

客户端用 OpenAI SDK 调模型,同时用 MCP SDK 连服务端。核心配置写在一个.env里:

OPENAI_API_KEY=sk-你的TaoToken密钥 OPENAI_BASE_URL=https://taotoken.net/api MODEL_ID=qwen2.5:14b MCP_SERVER_SCRIPT=./server.py

然后在client.py里读取:

import os import json import asyncio from contextlib import AsyncExitStack from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from openai import OpenAI from dotenv import load_dotenv load_dotenv() class MCPClient: def __init__(self): self.session = None self.exit_stack = AsyncExitStack() self.openai = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"), ) self.model = os.getenv("MODEL_ID") def get_response(self, messages, tools): return self.openai.chat.completions.create( model=self.model, max_tokens=1000, messages=messages, tools=tools, ) async def get_tools(self): response = await self.session.list_tools() return [ { "type": "function", "function": { "name": tool.name, "description": tool.description, "parameters": tool.inputSchema, }, } for tool in response.tools ] async def connect_to_server(self, server_script_path: str): is_python = server_script_path.endswith(".py") is_js = server_script_path.endswith(".js") if not (is_python or is_js): raise ValueError("服务器脚本必须是 .py 或 .js 文件") command = "python" if is_python else "node" server_params = StdioServerParameters( command=command, args=[server_script_path], env=None, ) stdio_transport = await self.exit_stack.enter_async_context( stdio_client(server_params) ) self.stdio, self.write = stdio_transport self.session = await self.exit_stack.enter_async_context( ClientSession(self.stdio, self.write) ) await self.session.initialize() response = await self.session.list_tools() print("已连接,工具列表:", [t.name for t in response.tools]) async def process_query(self, query: str) -> str: messages = [{"role": "user", "content": query}] available_tools = await self.get_tools() response = self.get_response(messages, available_tools) final_text = [] for choice in response.choices: message = choice.message if not message.tool_calls: final_text.append(message.content or "") continue tool_name = message.tool_calls[0].function.name tool_args = json.loads(message.tool_calls[0].function.arguments) print(f"调用工具: {tool_name}, 参数: {tool_args}") result = await self.session.call_tool(tool_name, tool_args) content = str(result.content) if hasattr(result, "content") else str(result) final_text.append(f"[工具 {tool_name} 返回] {content}") messages.append({"role": "assistant", "content": message.content or ""}) messages.append({"role": "user", "content": content}) follow_up = self.get_response(messages, available_tools) final_text.append(follow_up.choices[0].message.content or "") return "\n".join(final_text) async def chat_loop(self): print("MCP Client 启动,输入 quit 退出") while True: query = input("\nQuery: ").strip() if query.lower() == "quit": break print(await self.process_query(query)) async def cleanup(self): await self.exit_stack.aclose() async def main(): import sys if len(sys.argv) < 2: print("用法: python client.py <server_script>") sys.exit(1) client = MCPClient() try: await client.connect_to_server(sys.argv[1]) await client.chat_loop() finally: await client.cleanup() if __name__ == "__main__": asyncio.run(main())

注意base_url填https://taotoken.net/api,不要加/v1,SDK 会自动补。model填qwen2.5:14b,和 Ollama 里的名字一致。如果你用的是 vLLM 部署,model 名换成你启动时指定的--served-model-name。

4. 验证请求:从「写一首诗」到文件落盘

配置写完,跑起来验证。命令:

python client.py server.py

启动后应该看到:

已连接,工具列表: ['write_to_txt'] MCP Client 启动,输入 quit 退出 Query:

输入「写一首关于秋天的诗,保存到 poem.txt」,观察输出。正常流程是:

第一步,客户端把 query 和工具列表发给 Qwen-2.5。Qwen 返回的choices[0].message.tool_calls里包含write_to_txt和参数{"filename": "poem.txt", "content": "..."}。

第二步,客户端解析参数,通过 MCP session 调用call_tool,服务端执行写文件,返回「成功写入文件 poem.txt」。

第三步,客户端把工具结果回灌给 Qwen,Qwen 生成最终回复,比如「诗已保存到 poem.txt」。

第四步,检查当前目录:

cat poem.txt

应该能看到完整的诗。如果文件存在且内容正确,说明整条链路通了。

这里有个细节:Qwen-2.5 的 tool_calls 返回里,function.arguments是 JSON 字符串,需要json.loads解析。有些版本的 Ollama 会返回已经解析好的 dict,代码里做了兼容。如果报TypeError: string indices must be integers,就是这里没处理。

另一个验证点是工具列表是否正确暴露。如果list_tools返回空,检查服务端@mcp.tool()装饰器是否加在函数上,以及mcp.run是否真的启动了。stdio 模式下服务端不会打印日志,所有输出都走标准流,所以调试时可以在服务端加sys.stderr.write打日志。

成功结果长这样:

调用工具: write_to_txt, 参数: {'filename': 'poem.txt', 'content': '秋风起兮白云飞...'} [工具 write_to_txt 返回] 成功写入文件 poem.txt。 诗已保存到 poem.txt,共 4 句。

到这一步,本地 Qwen-2.5 + MCP 的最小闭环就跑通了。接下来可以往服务端加更多工具,比如读文件、查数据库、调 HTTP 接口,Qwen 会自动根据描述选择调用哪个。

5. 本篇常见错排查:401、local proxy failed、reading choices

这一节列几个真实会撞上的报错,按出现频率排序。

报错一:401 Unauthorized

openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Invalid API key'}}

原因:OPENAI_API_KEY没填、填错,或者.env没被load_dotenv()加载。排查步骤:先在终端echo $OPENAI_API_KEY看有没有值;再确认.env文件和client.py在同一目录;最后去https://taotoken.net/api-keys重新生成一个 Key 替换。注意 Key 前后不要有空格,复制时容易带上换行。

报错二:local proxy failed / connection refused

openai.APIConnectionError: Connection error.

原因:base_url写错,或者本地 Ollama 没启动。如果你是把 Ollama 当后端,base_url应该是http://localhost:11434/v1;如果你走 TaoToken 统一通道,base_url是https://taotoken.net/api。两者不能混。排查:curl一下对应地址,看能不能返回。Ollama 没起就ollama serve;TaoToken 不通就检查网络和 Key。

报错三:reading choices / KeyError 'choices'

KeyError: 'choices'

原因:模型返回的不是 OpenAI 格式。常见于直接把 Ollama 原生/api/chat当 OpenAI 接口用。解决:确认base_url带/v1(Ollama 场景)或走 TaoToken 的/api。另外,如果 Qwen 返回的是流式响应但代码按非流式解析,也会拿不到choices。检查create调用里有没有误加stream=True。

报错四:OAuth / 权限相关

Error: OAuth token expired

这个一般出现在用云端 MCP 服务端时。本地 stdio 模式不涉及 OAuth。如果你接的是远程 MCP 服务,需要在客户端配置里加headers带 token。本地场景可以忽略。

报错五:工具调用参数解析失败

json.decoder.JSONDecodeError: Expecting value

原因:Qwen 返回的arguments不是合法 JSON,可能是模型幻觉生成了带注释的字符串。解决:在json.loads外面包 try/except,失败时把原始字符串打出来看。也可以在 system prompt 里强调「arguments 必须是合法 JSON,不要加注释」。

CC Switch / Cline MCP / Codex auth.json 三件套

如果你用的是 Cline 或 CC Switch 这类客户端,配置里必须同时出现三个东西:Base URL(https://taotoken.net/api)、API Key(sk-xxx)、Model ID(qwen2.5:14b)。少一个就连不上。Codex 的auth.json里对应字段是api_base、api_key、model,格式不同但含义一样。Cline 的 MCP 配置在cline_mcp_settings.json,结构是:

{ "mcpServers": { "file-writer": { "command": "python", "args": ["/absolute/path/to/server.py"], "env": { "OPENAI_API_KEY": "sk-xxx", "OPENAI_BASE_URL": "https://taotoken.net/api", "MODEL_ID": "qwen2.5:14b" } } } }

路径一定要用绝对路径,相对路径在 Cline 里会解析到插件目录而不是项目目录。

6. 继续往下走:把 MCP 接进日常工具链

跑通最小闭环之后,下一步是把它接进你每天用的工具。如果你主要写代码,可以把 MCP 服务端扩展成能读 git 仓库、跑测试、查文档的工具集,然后接到 Cline 或 Continue 里,让 Qwen-2.5 在本地帮你做代码问答和重构。长期做 Agent 开发的话,建议直接上 Coding Plan,把模型调用和工具编排的额度一起管起来,省得每次单独配 Key。

验证模型能力可以用模型对话页面快速试 prompt,确认 Qwen-2.5 在你关心的任务上表现如何,再决定要不要换更大的 32B 版本。接入文档在https://taotoken.net/doc,里面有各客户端的完整配置示例,包括 Claude Code 的接入方式。

最后给一个实用技巧:MCP 服务端的工具描述(docstring)直接决定模型会不会正确调用。描述里要写清楚「什么时候用这个工具」「参数是什么格式」「返回什么」,Qwen-2.5 对中文描述的理解比英文更稳。我实测下来,把 docstring 写成中文、参数名用英文,调用准确率最高。另外,工具数量不要一次暴露太多,超过 10 个之后模型选择会变慢,按场景拆成多个服务端更合理。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询