基于 MCP 协议的 LLM 工具调用

文章主要讲解了如何使用 MCP(Model Context Protocol)与大型语言模型(LLM)结合来实现工具调用,以及如何搭建一个简单的应用来展示这一过程。

1. 安装一些必要的包

pip install "mcp[cli]"
pip install openai langchain  langchain-mcp-adapters langgraph langchain_ollama -U

启动本地模型,这里使用ollama启动个1.5b的小模型

ollama run qwen2.5:1.5b

代码结构:
在这里插入图片描述

2. MCP server.py 服务端

  • check_child_study_situation:检查小孩最近的学习状况
{
        "小明": "学习很努力 from Michael阿明老师点评",
        "小红": "学习一般",
        "小刚": "学习不太好",
}
  • query_student_scores:获取学习成绩得分
    在这里插入图片描述

  • 使用 FastMCP 类创建 MCP 服务器,并通过 @mcp.tool() 装饰器注册工具函数。

from mcp.server.fastmcp import FastMCP
import os

mcp = FastMCP("Michael'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")

3. fastapi 启动 api 服务

  • 搭建了一个简单的 Web 服务来与 MCP 服务器交互。
  • 通过加载 MCP 工具并创建反应式代理(react agent)来处理用户请求。
  • 使用 Server-Sent Events (SSE) 实现流式响应,模拟实时对话效果。
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse, HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sse_starlette.sse import EventSourceResponse
from pathlib import Path
import asyncio
import json

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

# Get the current directory
BASE_DIR = Path(__file__).resolve().parent

# Initialize FastAPI
app = FastAPI(title="MCP Chat with SSE")

# Mount static files directory
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")

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

# Configure the MCP server parameters
server_params = StdioServerParameters(
    command="python",
    args=["server.py"],
)

# Create a directory for static files if it doesn't exist
static_dir = BASE_DIR / "static"
static_dir.mkdir(exist_ok=True)

# Add endpoint to serve the chat interface
@app.get("/", response_class=FileResponse)
async def get_chat_interface():
    """Serve the chat interface HTML page"""
    return FileResponse(path=str(BASE_DIR / "static" / "chat.html"))

async def generate_stream(prompt: str):
    """Generate stream of responses from the MCP agent."""
    try:
        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)
                
                # 获取代理的响应结果(这是一个协程,需要await)
                result = await agent.ainvoke({
                    "messages": [
                        {"role": "user", "content": prompt},
                    ]
                })
                
                # Debug: Print the structure of the result to understand it
                print(f"Result type: {type(result)}")
                print(f"Result keys: {result.keys() if isinstance(result, dict) else 'Not a dict'}")
                
                
                # 提取并返回结果
                if isinstance(result, dict) and "messages" in result:
                    for message in result["messages"]:
                        # Try different ways to identify assistant messages
                        is_assistant = False
                        content = None
                        
                        # Try common attribute patterns in LangChain messages
                        if hasattr(message, "role") and message.role == "assistant":
                            is_assistant = True
                            content = message.content if hasattr(message, "content") else None
                        elif hasattr(message, "type") and message.type == "assistant":
                            is_assistant = True
                            content = message.content if hasattr(message, "content") else None
                        
                        # For AIMessage and similar classes
                        if hasattr(message, "__class__") and "AI" in message.__class__.__name__:
                            is_assistant = True
                            content = message.content if hasattr(message, "content") else None
                        
                        if is_assistant and content:
                            # 逐字符输出,模拟流式响应
                            buffer = ""
                            for char in content:
                                buffer += char
                                yield buffer
                                # 添加一点延迟,使流式效果更明显
                                await asyncio.sleep(0.01)
                            return  # Stop after finding the first assistant message
                
                # If we didn't find any assistant messages
                yield "没有找到助手的回复,请重试。"
    except Exception as e:
        print(f"Error in generate_stream: {str(e)}")
        yield f"Error: {str(e)}"

@app.get("/chat")
async def chat(request: Request, prompt: str):
    """
    Endpoint that returns an SSE stream of chat responses.
    
    Query Parameters:
    - prompt: The text prompt to send to the model
    """
    return EventSourceResponse(
        generate_stream(prompt),
        media_type="text/event-stream"
    )

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

4. 对话测试

只需要启动 fastapi服务,然后再web端调用
在这里插入图片描述
可以看到,模型有一定的概率能找到相应的 tool 进行调用

5. 代码调用

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())
    print(result)

输出:

{'messages': [HumanMessage(content='请查询小明得分成绩', additional_kwargs={}, response_metadata={}, id='d24dabad-0d48-4d8e-a6ec-ce4c2df53774'), AIMessage(content='', additional_kwargs={}, 
response_metadata={'model': 'qwen2.5:1.5b', 'created_at': '2025-04-19T15:39:42.8503337Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1429941000, 'load_duration': 23185900, 'prompt_eval_count': 355, 'prompt_eval_duration': 5802200, 'eval_count': 22, 'eval_duration': 1397424200, 'model_name': 'qwen2.5:1.5b'}, 
id='run-a36349f2-a6d6-4865-901f-08a57a2b5e12-0', 
tool_calls=[{'name': 'query_student_scores', 'args': {'name': '小明'}, 
'id': '9762c2f2-87b2-48ef-90d6-22cacf837a30', 'type': 'tool_call'}], 
usage_metadata={'input_tokens': 355, 'output_tokens': 22, 'total_tokens': 377}), 
ToolMessage(content='小明的成绩:\n- 语文: 80\n- 数学: 95\n', name='query_student_scores', id='437e7cd4-4e28-4c40-b7b6-bd2598f2ab12', tool_call_id='9762c2f2-87b2-48ef-90d6-22cacf837a30'), 
AIMessage(content='根据查询结果,小明的得分如下:\n\n- 语文成绩:80\n- 数学成绩:95', additional_kwargs={}, response_metadata={'model': 'qwen2.5:1.5b', 'created_at': '2025-04-19T15:39:44.6487902Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1780121800, 'load_duration': 24304300, 'prompt_eval_count': 413, 'prompt_eval_duration': 10838200, 'eval_count': 26, 'eval_duration': 1736960100, 'model_name': 'qwen2.5:1.5b'}, id='run-c03655dc-e0ad-46bf-ac21-effbc4b6b94d-0', usage_metadata={'input_tokens': 413, 'output_tokens': 26, 'total_tokens': 439})]}

6. 总结

在 MCP(Model Context Protocol)中,toolfunction calling 是紧密相关但又有所区别的概念,它们在实现大型语言模型(LLM)与外部功能交互时,具有各自特定的作用和意义:

  • MCP 中的 tool

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

    • 是 LLM 调用外部函数的能力,增强了模型的能力,使其能够借助外部资源解决问题。
    • 特点包括增强模型能力、动态交互、参数传递与结果处理等。
  • 二者关系

    • MCP 中的 tool 是具体的工具实现,function calling 是调用这些工具的方式。
    • MCP 是协议规范,定义了 LLM 和工具之间的通信方式;function calling 是 LLM 的能力,利用 MCP 协议调用工具。
    • 二者紧密协作,共同实现复杂的应用场景。

本文 from https://michael.blog.csdn.net/

### 大模型对齐微调DPO方法详解 #### DPO简介 直接偏好优化(Direct Preference Optimization, DPO)是一种用于改进大型语言模型行为的技术,该技术通过结合奖励模型训练和强化学习来提升训练效率与稳定性[^1]。 #### 实现机制 DPO的核心在于它能够依据人类反馈调整模型输出的概率分布。具体来说,当给定一对候选响应时,DPO试图使更受偏好的那个选项具有更高的生成概率。这种方法不仅简化了传统强化学习所需的复杂环境设置,而且显著增强了模型对于多样化指令的理解能力和执行精度[^2]。 #### PAI平台上的实践指南 为了便于开发者实施这一先进理念,在PAI-QuickStart框架下提供了详尽的操作手册。这份文档覆盖了从环境配置直至完成整个微调流程所需的一切细节,包括但不限于数据准备、参数设定以及性能评估等方面的内容。尤其值得注意的是,针对阿里云最新发布的开源LLM——Qwen2系列,文中给出了具体的实例说明,使得即使是初次接触此类工作的用户也能顺利上手。 ```python from transformers import AutoModelForCausalLM, Trainer, TrainingArguments model_name_or_path = "qwen-model-name" tokenizer_name = model_name_or_path training_args = TrainingArguments( output_dir="./results", per_device_train_batch_size=8, num_train_epochs=3, ) trainer = Trainer( model_init=lambda: AutoModelForCausalLM.from_pretrained(model_name_or_path), args=training_args, train_dataset=train_dataset, ) # 假设已经定义好了train_dataset trainer.train() ``` 这段代码片段展示了如何使用Hugging Face库加载预训练模型并对其进行微调的过程。虽然这里展示的例子并不完全对应于DPO的具体实现方式,但它提供了一个基础模板供进一步定制化开发之用[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Michael阿明

如果可以,请点赞留言支持我哦!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值