这里结合网络资料,尝试结合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
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
- 生成参数的 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"
}
- 将函数注册为 MCP 工具(
self._tools[tool.name] = tool
)。
mcp.tool()
可以接受以下参数(此处参数解释参考 tool 和 func_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 工具调用