通过fastmcp手搓可调用工具的循环聊天机器人

fast mcp

  • fastMCP地址。相比于MCP官方的SDK,fastMCP在调用和实例化上都显得要简单一些。

安装

  • 安装uv,python
  • cd到路径
  • uv init .把文件夹python工程化
  • uv venv --python 3.12
  • 安装fastmcp和openAI:uv pip install fastmcp openai;openai用在需要做大语言用户模型时。
  • 依赖安装好后,在uv环境下可以正常启动。

mcp server和 mcp client

  • 最简单的mcp server,命名为server.py
    • 参数数据类型需要明确,这是给AI做数据填入用。
    • mcp.run()方法可以执行mcp服务器。
    • 方法的描述必不可少,AI依靠描述得知方法的作用。
"""
1. 创建FastMCP实例
2. 创建函数,添加文档
3. mcp.tool
4. 运行服务器
"""

from fastmcp import FastMCP

mcp=FastMCP()

@mcp.tool()
def get_weather(city:str):
    """
    获取对应城市的天气
    :param city:城市
    :return:城市天气的描述
    """
    return f"{city}今天天气晴,18度"

if __name__=='__main__':
    mcp.run()
  • 最简单的mcp client,命名为client.py
    • 这是最简单stdio通信
    • 需要在异步函数中执行client
    • 需要用async with client:才能记录上下文
    • client.call_tool()执行工具
"""
1. 创建客户端
2. 获取工具和资源和prompt
3. 执行工具

"""
import asyncio
from fastmcp import Client

async def run():
    client=Client('server.py')
    async with client:                      # 必要,用来进入上下文管理器
        tools = await client.list_tools()

        tool = tools[0]
        tool_result = await client.call_tool(tool.name,{"city":"成都"})
        print(tools)
        print(tool_result)

if __name__=='__main__':
    asyncio.run(run())
  • 运行client.py效果:
[Tool(name='get_weather', description='获取对应城市的天气\n:param city:城市\n:return:城市天气的描述', inputSchema={'properties': {'city': {'title': 'City', 'type': 'string'}}, 'required': ['city'], 'type': 'object'},
 annotations=None)]
[TextContent(type='text', text='成都今天天气晴,18度', annotations=None)]

LLM+MCP server

  • 可以作为模板参考新文件,命名为app.py
    • 此时能和LLM对话但是不能调用工具:
    • 例程中的异步函数和格式和提示词都是必要的,mcp_client也是参考上面的例子拓展的。进一步功能可以在此基础上更改。
"""
用户client:
    1. 调用大语言模型,client如OpenAI client
    2. 调用MCP server,client如MCP client
    3. 
"""
from typing import List, Dict
import asyncio

from fastmcp import Client
from openai import OpenAI

class UserClient:
    def __init__(self,script="server.py",model="qwen2.5:latest"):
        self.model=model                    # 传入Model名
        self.mcp_client=Client(script)      # mcp_client
        self.openai_client=OpenAI(          # 基于ollama部署的本地大模型
            base_url="http://*********/v1",
            api_key="*********"
        )
        self.messages=[                     # 给大模型的系统消息(默认消息)
            {
                "role":"system",
                "content":"你是一个AI助手,你需要借助工具,回答用户问题。"
            }
        ]

    async def prepare_tools(self):          # 用异步方法把工具拿到
        tools = await self.mcp_client.list_tools()  # 工具
        tools = [
            {                               # 拿到工具信息,以下是固定结构
                "type":"function",
                "function":{
                    "name":tool.name,
                    "description":tool.description,
                    "input_schema":tool.inputSchema
                }
            }
            for tool in tools
        ]
        return tools

    async def chat(self,messages:List[Dict]):      # 聊天,需要传入一个字典的列表
        response=self.openai_client.chat.completions.create(
            model=self.model,                      # 传入参数
            messages=messages,
        )
        print(response)
    
    async def loop(self):                         # 循环聊天
        while True: 
            question=input("user:")               # 首先需要用户输入内容
            message = {
                "role":"user",                    # 用户消息
                "content":question
            }
            self.messages.append(message)
            reponse_message = await self.chat(self.messages)
            print("AI: ",reponse_message.get('content'))

async def main():                               # 调用
    user_client = UserClient()
    await user_client.chat([
        {"role":"user","content":"hello."}
    ])

if __name__=='__main__':
    asyncio.run(main())
  • 大模型回复:
ChatCompletion(id='chatcmpl-756', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='Hello! How can I assist you today? Feel free to ask me any questions or let me know if you need help wi
d help with anything specific.', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None))], created=1750863190, model='qwen2.5:latest', object='chat.completion', service_tier=None, systerint='fp_
m_fingerprint='fp_ollama', usage=CompletionUsage(completion_tokens=29, prompt_tokens=31, total_tokens=60, completion_tokens_details=None, prompt_tokens_details=None))
  • 加入工具调用相关代码:
"""
用户client:
    1. 调用大语言模型,client如OpenAI client
    2. 调用MCP server,client如MCP client
    3. 
"""
from typing import List, Dict
import asyncio

from fastmcp import Client
from openai import OpenAI

class UserClient:
    def __init__(self,script="server.py",model="qwen2.5:latest"):
        self.model=model                    
        self.mcp_client=Client(script)      
        self.openai_client=OpenAI(          
            base_url="http://*********/v1",
            api_key="*********"
        )
        self.messages=[                     
            {
                "role":"system",
                "content":"你是一个AI助手,你需要借助工具,回答用户问题。"
            }
        ]
        self.tools=[]                       # 建立工具调用列表

    async def prepare_tools(self):          
        tools = await self.mcp_client.list_tools()  
        tools = [
            {                               
                "type":"function",
                "function":{
                    "name":tool.name,
                    "description":tool.description,
                    "input_schema":tool.inputSchema
                }
            }
            for tool in tools
        ]
        return tools

    async def chat(self,messages:List[Dict]):      
        async with self.mcp_client:                # 上下文管理,不加要报错
            if not self.tools:                         # 如果工具列表是空的,就执行准备工具
                self.tools= await self.prepare_tools()

            response=self.openai_client.chat.completions.create(
                model=self.model,                      
                messages=messages,
                tools=self.tools,                       # 把工具列表传入模型
            )
            print(response)
    
    async def loop(self):                         
        while True: 
            question=input("user:")               
            message = {
                "role":"user",                    
                "content":question
            }
            self.messages.append(message)
            reponse_message = await self.chat(self.messages)
            print("AI: ",reponse_message.get('content'))

async def main():                               
    user_client = UserClient()
    await user_client.chat([
        {"role":"user","content":"成都今天天气怎么样?"}  # 更改问题
    ])

if __name__=='__main__':
    asyncio.run(main())
  • 执行后大模型回复如下,像比于之前的回复内容:
    • content='',content是空的
    • 多了tool_calls以及列表里面的内容,证明大模型知道自己需要调用工具
    • function=Function(arguments='{"city":"成都"}', name='get_weather')大模型知道自己调用的工具名和参数
ChatCompletion(id='chatcmpl-905', choices=[Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content='', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, too
l_calls=[ChatCompletionMessageToolCall(id='call_b7ju7kgk', function=Function(arguments='{"city":"成都"}', name='get_weather'), type='function', index=0)]))], created=1750867239, model='qwen2.5:latest', object='chat.completion', s
ervice_tier=None, system_fingerprint='fp_ollama', usage=CompletionUsage(completion_tokens=20, prompt_tokens=158, total_tokens=178, completion_tokens_details=None, prompt_tokens_details=None))
  • 完善工具调用,实现在循环中提问:
"""
用户client:
    1. 调用大语言模型,client如OpenAI client
    2. 调用MCP server,client如MCP client
    3. 
"""
import json
from typing import List, Dict
import asyncio

from fastmcp import Client
from openai import OpenAI

class UserClient:
    def __init__(self,script="server.py",model="qwen2.5:latest"):
        self.model=model                    
        self.mcp_client=Client(script)      
        self.openai_client=OpenAI(          
            base_url="http://*********/v1",
            api_key="*********"
        )
        self.messages=[                     
            {
                "role":"system",
                "content":"你是一个AI助手,你需要借助工具,回答用户问题。"
            }
        ]
        self.tools=[]                       

    async def prepare_tools(self):          
        tools = await self.mcp_client.list_tools()  
        tools = [
            {                               
                "type":"function",
                "function":{
                    "name":tool.name,
                    "description":tool.description,
                    "input_schema":tool.inputSchema
                }
            }
            for tool in tools
        ]
        return tools

    async def chat(self,messages:List[Dict]):      
        if not self.tools:                         
            self.tools= await self.prepare_tools()

        response=self.openai_client.chat.completions.create(
            model=self.model,                     
            messages=messages,
            tools=self.tools,                       
        )
        if response.choices[0].finish_reason != "tool_calls":        # 不等于说明仅仅是普通消息
            return response.choices[0].message                      # 直接返回消息
        
        # 执行工具
        for tool_call in response.choices[0].message.tool_calls:        #执行工具方法
            response = await self.mcp_client.call_tool(tool_call.function.name,
                                                        json.loads(tool_call.function.arguments)) # arguments由大语言模型传入。要求是字典,所以要用json.loads()转换一下
            self.messages.append({                      
                'role':'assistant',
                'content':response[0].text
            })
            return await self.chat(self.messages)       # 把回复拼接进消息中

    
    async def loop(self):                         
        async with self.mcp_client:                # 把这段代码从chat放到loop中,简化流程
            while True: 
                question=input("user:")               
                message = {
                    "role":"user",                    
                    "content":question
                }
                self.messages.append(message)
                reponse_message = await self.chat(self.messages)
                print("AI: ",reponse_message.content)       # 返回message对象

async def main():                               
    user_client = UserClient()
    await user_client.loop()        # 在循环聊天中提问

if __name__=='__main__':
    asyncio.run(main())
  • 和大模型的交互内容:
user:你好
AI:  你好!有什么我可以帮助你的吗?
user:重庆今天天气怎么样?
AI:  到26度之间。注意适时添加衣物以免感冒。
user:你可以调用城市温度相关的工具吗?
AI:   defaultManager = 0 htmlFor = 52889 json = '{"city":"成都","temperature":[{"date":"今天","text":"多云","low":16,"high":24}]}'
重庆今天的气温是18度,天气晴朗。你可以在出行时做好防晒措施哦!其他地方的天气情况也可以随时询问我哦。
user:

调试中遇到的问题记录

  • 调用自己写的工具时出现字符类型不匹配问题:fastmcp.exceptions.ToolError: Error calling tool 'double_click_software': 'charmap' codec can't encode characters in position 0-8: character maps to <undefined>

    • 这个问题不止出现在会读写文件的工具函数中。一些看起来和读写文本不太相关的工具函数也会导致这个问题,可能是因为AI或者fastmcp在背后记录日志或者其他文本信息导致的。
    • 解决办法:打开控制面板,找到时钟和区域,打开区域,选择更改系统区域设置,勾选Beta版:使用unicode UTF-8提供全球语言支持
    • 总之,就是让自己电脑支持utf-8的格式。
  • AI的回复总是为空:'content':response[0].text~~~~~~~~^^^IndexError: list index out of range

    • 解决办法:注意自己写的工具函数是否具有return,返回值是必要的。
    • 如果函数没有返回值,AI就会回复空数组导致出错。
    • 如果AI回复还是为空, 可以尝试try:execpt Exception as e:,捕获错误。
posted @ 2025-06-25 17:20  你要去码头整点薯条吗  阅读(656)  评论(0)    收藏  举报