MCP-与本地大模型集成实现工具调用

这里结合网络资料,尝试结合MCP(Model Context Protocol)与大型语言模型实现工具调用,以一个简单的应用来展示这一过程。

1 创建mcp虚拟环境

conda create -n mcp python=3.12

conda activate mcp

pip install uv

2 安装必要依赖包

pip install "mcp[cli]"

pip install openai langchain langchain-mcp-adapters langgraph langchain_ollama -U

3 安装ollama模型工具

linux ollama安装大致和mac安装类似,大致参考

在mac m1基于ollama运行deepseek r1_mac m1 ollama-CSDN博客

受限于硬件环境,这里使用qwen的1.5b小模型

ollama run qwen2.5:1.5b

4 mcp服务端

score_points.txt,记录成绩数据,辅助server.py代码实现简单应用

小明:

语文:80

数学:95

小红:

语文:100

数学:100

mcp服务代码server.py

复制代码
from mcp.server.fastmcp import FastMCP
import os

mcp = FastMCP("Tom's tools")

@mcp.tool()
def check_child_study_situation(name: str) -> str:
    """检查小孩最近的学习状况"""
    db = {
        "小明": "学习很努力 from Michael阿明老师点评",
        "小红": "学习一般",
        "小刚": "学习不太好",
    }
    print(f"Checking study status for {name}")
    return db.get(name, "没有找到这个小孩的学习记录")

@mcp.tool()
def query_student_scores(name: str) -> str:
    """查询学生成绩
    
    Args:
        name: 学生姓名
        
    Returns:
        学生成绩信息,如果未找到则返回相应提示
    """
    file_path = os.path.join(os.path.dirname(__file__), "score_points.txt")
    
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()
        
        # 解析文件内容
        students = {}
        current_student = None
        
        for line in content.split("\n"):
            line = line.strip()
            if not line:
                continue
                
            if line.endswith(":"):  # 学生名
                current_student = line[:-1]  # 去掉结尾的冒号
                students[current_student] = {}
            elif current_student and ":" in line:  # 科目分数
                subject, score = line.split(":")
                students[current_student][subject.strip()] = score.strip()
        
        # 返回学生成绩
        if name in students:
            result = f"{name}的成绩:\n"
            for subject, score in students[name].items():
                result += f"- {subject}: {score}\n"
            return result
        else:
            return f"没有找到{name}的成绩记录"
    except Exception as e:
        return f"查询成绩出错: {str(e)}"

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

@mcp.tool()
def multiply(a: int, b: int) -> int:
    """Multiply two numbers"""
    return a * b

if __name__ == "__main__":
    mcp.run(transport="stdio")

5 mcp客户端

通过加载 MCP 工具并创建反应式代理(react agent)来处理用户请求。

以下是客户端代码示例client.py

复制代码
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_ollama import ChatOllama
import asyncio


model = ChatOllama(model='qwen2.5:1.5b')

server_params = StdioServerParameters(
    command="python",
    args=["server.py"],
)

async def run_agent():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await load_mcp_tools(session)
            agent = create_react_agent(model, tools)
            agent_response = await agent.ainvoke({
                "messages": [
                    {"role": "user", "content": "小明最近的学习状态怎么样?"},
                ]
            })
            return agent_response

if __name__ == "__main__": 
    result = asyncio.run(run_agent())
    messages = result["messages"]
    print(len(messages))
    print(messages)

这是llm选择qwen2.5:1.5b时的回复,可见llm并没有真实理解咱们的意图。

\*HumanMessage(content='小明最近的学习状态怎么样?', additional_kwargs={}, response_metadata={}, id='e66611e3-d9de-42a9-8676-d4982ccae318'), --- \* AIMessage(content='好的,请问您想了解小明的具体学习状况吗?我可以帮您检查一下。请提供他的姓名,我会马上进行查询。\\n请问小明的姓名是什么?', additional_kwargs={}, response_metadata={'model': 'qwen2.5:1.5b', 'created_at': '2025-08-03T12:48:52.573277334Z', 'done': True, 'done_reason': 'stop', 'total_duration': 11749528145, 'load_duration': 65738997, 'prompt_eval_count': 356, 'prompt_eval_duration': 476507137, 'eval_count': 37, 'eval_duration': 11201131239, 'model_name': 'qwen2.5:1.5b'}, id='run--e51372c2-6909-4bbf-a681-64511c3f8986-0', usage_metadata={'input_tokens': 356, 'output_tokens': 37, 'total_tokens': 393})

我们换一个能力更强但相对也比较小的模型qwen3:8b,重新运行后,结果如下。

这次就比较符合预期,所以当mcp结果不太理想时,可尝试换个当前硬件能支持的更强的llm,有可能就获得符合预期的结果了。

\* HumanMessage(content='小明最近的学习状态怎么样?', additional_kwargs={}, response_metadata={}, id='7d10dce4-ffb4-4fff-bb6e-2d6d20def146'), --- \* AIMessage(content='\\\n好的,用户问的是小明最近的学习状态怎么样。我需要先看看有哪些可用的工具。提供的工具里有一个check_child_study_situation函数,描述是检查小孩最近的学习状况,参数需要名字。另一个是query_student_scores,查询成绩,参数也是名字。还有加法和乘法函数,应该不相关。\\n\\n用户的问题是关于学习状态,不是成绩,所以可能更适合用check_child_study_situation。不过也有可能用户想通过成绩来了解学习状态,这时候可能需要用query_student_scores。但问题中没有提到成绩,所以优先考虑第一个函数。需要确认参数是否正确,参数是name,用户提到了小明,所以参数应该是name: "小明"。应该调用check_child_study_situation函数,参数是小明。这样更直接回应学习状态的问题。如果用户后续需要成绩,再调用另一个函数。现在先调用检查学习状况的函数。\\n\\\n\\n', additional_kwargs={}, response_metadata={'model': 'qwen3:8b', 'created_at': '2025-08-03T13:48:40.403280213Z', 'done': True, 'done_reason': 'stop', 'total_duration': 753502645412, 'load_duration': 3622735922, 'prompt_eval_count': 340, 'prompt_eval_duration': 426867643736, 'eval_count': 224, 'eval_duration': 323011540155, 'model_name': 'qwen3:8b'}, id='run--afb88487-3c4b-4b57-9bbf-01d2616bf88c-0', tool_calls=\[{'name': 'check_child_study_situation', 'args': {'name': '小明'}, 'id': '5ec3bd6f-07c4-4687-b598-5912944fb5c4', 'type': 'tool_call'}\], usage_metadata={'input_tokens': 340, 'output_tokens': 224, 'total_tokens': 564}), --- \*ToolMessage(content='学习很努力 from Michael阿明老师点评', name='check_child_study_situation', id='90591982-6fd6-4655-a14e-b60a11aedc94', tool_call_id='5ec3bd6f-07c4-4687-b598-5912944fb5c4'), --- \*AIMessage(content='\\\n\\\n\\n小明最近的学习状态非常好,学习非常努力,老师也给予了积极的评价。如果还有其他想了解的情况,可以继续提问哦!', additional_kwargs={}, response_metadata={'model': 'qwen3:8b', 'created_at': '2025-08-03T13:49:58.626358151Z', 'done': True, 'done_reason': 'stop', 'total_duration': 78211764828, 'load_duration': 42962753, 'prompt_eval_count': 562, 'prompt_eval_duration': 28885173128, 'eval_count': 35, 'eval_duration': 49264293169, 'model_name': 'qwen3:8b'}, id='run--b2cd78ab-e6a6-4c09-84ff-79ba541c70ab-0', usage_metadata={'input_tokens': 562, 'output_tokens': 35, 'total_tokens': 597})

6 mcp的工具和调用

实现LLM与外部交互时,tool 是具体的工具实现,function calling 是调用这些工具的方式。

tool,定义在 MCP 服务器上的功能模块,通过 MCP 协议暴露给客户端,具有封装性、可发现性和异步性特点。

Function calling,LLM调用外部函数的能力,增强了模型的能力,使其能够借助外部资源解决问题。增强模型能力、动态交互、参数传递与结果处理等。

MCP 是协议规范,定义了 LLM 和工具之间的通信方式;function calling 是LLM的能力,利用 MCP 协议调用tool。这很好理解,但FastMCP在没明确路由情况下,如何知道何时基于function calling调用具体工具。FastMCP在通过@mcp.tool() 装饰器注册函数为工具时,会自动获取函数的参数信息、文档信息、注释字符串等信息,LLM依据这些信息判断何时调用@mcp.tool()注册的函数,具体过程参考附录。

附录

@mcp.tool() 装饰器用于将函数自动注册为当前 mcp 服务器中工具。

摘选之前的片段:

@mcp.tool()

def get_weather(city: str) -> str:

"""获取指定城市的天气信息"""

简单模拟数据,实际应用中应该调用对应的API

weather_data = {

"北京": "晴天,温度 22°C",

"上海": "多云,温度 25°C",

"广州": "小雨,温度 28°C",

"深圳": "阴天,温度 26°C"

}

return weather_data.get(city, f"{city} 的天气数据暂不可用")

这段代码实际上会:

1)自动提取函数的参数类型信息、文档字符串。

============================================================

开始解析函数: get_weather

文档字符串: 获取指定城市的天气信息

============================================================

函数签名分析:

完整签名: (city: str) -> str

返回类型: <class 'str'>

参数数量: 1

参数名: city

原始注解: <class 'str'>

参数种类: POSITIONAL_OR_KEYWORD

默认值: <class 'inspect._empty'>

类型化注解: <class 'str'>

字段信息: annotation=<class 'str'>, default=PydanticUndefined

  1. 生成参数的 JSON Schema(函数名+Arguments)。

创建 Pydantic 模型:

模型名称: get_weatherArguments

基类: <class 'mcp.server.fastmcp.utilities.func_metadata.ArgModelBase'>

模型创建成功: <class 'main.get_weatherArguments'>

get_weatherArguments JSON Schema:

{

"properties": {

"city": {

"title": "City",

"type": "string"

}

},

"required": [

"city"

],

"title": "get_weatherArguments",

"type": "object"

}

  1. 将函数注册为 MCP 工具(self._tools[tool.name] = tool)。

mcp.tool() 可以接受以下参数(此处参数解释参考 toolfunc_metadata):

name: 可选的工具名称,默认为函数名

title: 可选的工具标题(用于人类阅读)

description: 可选的工具功能描述,默认使用函数的文档字符串

annotations: 可选的 ToolAnnotations,提供额外的工具信息

structured_output:控制工具输出是结构化还是非结构化的

  • None: 基于函数的返回类型注解自动检测
  • True: 无条件创建结构化工具(在返回类型注解允许的情况下)。如果是结构化,会根据函数的返回类型注释创建 Pydantic 模型。支持各种返回类型:
    • BaseModel 子类(直接使用)
    • 原始类型(str、int、float、bool、bytes、None)- 包装在带有 'result' 字段的模型中
    • TypedDict - 转换为具有相同字段的 Pydantic 模型
    • 数据类和其他带注释的类 - 转换为 Pydantic 模型
    • 泛型类型(list、dict、Union 等)- 包装在带有 'result' 字段的模型中
  • False: 无条件创建非结构化工具

reference


深入 FastMCP 源码:认识 tool()、resource() 和 prompt() 装饰器

https://zhuanlan.zhihu.com/p/1932083718639563855

fastmcp

https://github.com/modelcontextprotocol/python-sdk/tree/main/src/mcp/server/fastmcp

mcp, model context protocol

https://github.com/modelcontextprotocol/python-sdk

如何实现本地大模型与MCP集成
https://www.cnblogs.com/smartloli/p/18905572

基于 MCP 协议的 LLM 工具调用

https://blog.csdn.net/qq_21201267/article/details/147344774