随着大模型技术日新月异的发展,AI Agent 正逐渐取代传统的检索增强生成(RAG)成为知识驱动型LLM的主流方案。本文将深入探讨Model Context Protocol(MCP),这一由Anthropic提出的、平台无关的通信标准,并通过一个基于Groq加速的Python MCP客户端-服务端 示例,展示如何利用 MCP 构建更动态、交互性更强的智能应用。我们将重点介绍如何使用 MCP 标准化LLM与外部工具及服务间的交互,从而赋能AI Agent,使其不仅能回答问题,更能完成实际任务。

AI Agent:从信息检索到行动执行

传统的RAG系统侧重于信息检索,而 AI Agent 赋予了LLM调用外部工具和函数的能力,使其能够执行动作。例如,一个 AI Agent 可以不仅回答用户关于天气的问题,还可以预订机票、安排会议,甚至控制智能家居设备。这种转变解锁了更广泛的应用场景,但也带来了新的挑战:如何统一管理这些Agent,传递数据,并编排它们的工作流程?AI Agent 的核心在于行动能力,而如何安全、高效、可控地赋予Agent行动能力,是当前研究的重点。

根据Gartner的预测,到2026年,超过80%的企业将使用 AI Agent 技术来自动化客户服务、知识管理和流程优化。但在此之前,构建一个可靠的 AI Agent 生态系统需要解决互操作性问题。

Model Context Protocol (MCP):AI系统的USB-C

Model Context Protocol (MCP) 是一种标准化接口,旨在以一致、结构化的方式实现LLM与外部工具及服务之间的交互。它被誉为AI系统的“USB-C”,是一个通用的连接器,简化了模型与外部世界的通信,无论是在检索数据还是调用API方面。

在软件开发中,标准的重要性不言而喻。例如,REST API 已经得到广泛采用,为构建和消费API提供了一个通用的框架。但是,尽管REST API提供了指南,每个API的实际实现可能差异很大。当LLM需要与许多不同的工具(每个工具都有自己的格式)接口时,这种可变性就成了一个问题。

MCP 通过充当LLM和外部工具之间的标准化中间层来解决这个问题。模型无需适应每个API,而是通过 MCP 进行通信,从而抽象出这些差异。这不仅降低了复杂性,而且使得集成更可靠、更具可扩展性,即使工具提供商更新或修改其服务也是如此。MCP 类似于在多种不同接口的设备前增加了一个统一的转换器,使得LLM无需关心底层细节,即可与各种工具进行交互。

MCP的组件与工作原理

Model Context Protocol 生态系统由两个主要组件组成,它们协同工作以实现LLM和外部工具之间的无缝交互:

  • MCP Host: 这是用户与之交互的IDE/UI。它可以托管多个 MCP Client。例如Cursor, Claude Desktop等。
  • MCP Client: 位于 MCP Host 中。一个 MCP Client 和一个 MCP Server 之间存在1:1的连接。它管理工具发现,处理请求路由,并维护客户端和服务器之间的活动连接。
  • MCP Server: 由外部服务提供商维护。它的工作是以符合 MCP 标准的方式公开服务的功能。它抽象了服务的内部复杂性,并保护客户端免受任何破坏性更改,即使服务不断发展,也能确保持续的功能。

当前,几乎所有主要的工具、数据库和API都已开发出 MCP Server,以实现与LLM和 MCP Client 的无缝集成。这个不断增长的生态系统使 AI Agent 能够通过一致的接口访问各种外部功能。由于 MCP 对这些服务公开其功能的方式进行了标准化,因此提供商可以更新或修改其内部实现,而不会冒破坏客户端应用程序的风险。这种稳定性是一个关键优势,可确保AI驱动的系统即使在底层工具发展的情况下也能保持可靠。

MCP 的工作流程如下:

  1. 客户端向服务器发送初始化请求。
  2. 服务器以其功能响应。
  3. 将初始化通知发送到服务器。
  4. 开始正常的请求-响应消息传递。

基于Groq加速的MCP客户端实现

本文参考原文,采用Groq云平台提供的Llama3 70B模型和MCP标准文档,展示了如何构建一个简单的自定义 MCP Client。代码主要包含以下几个部分:

  1. 库导入和MCPClient基本结构: 包括typingjson, os, asyncio等标准库,以及mcpgroq相关的库。
  2. 服务器连接管理代码: 用于连接到指定的 MCP Server,并列出服务器提供的可用工具。
  3. 查询处理逻辑: 这是客户端的核心部分,它接收用户输入,与LLM交互,调用工具(如果需要),并将结果返回给用户。
from typing import List, Dict, Any, Union
import json
import os
import asyncio
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from groq import Groq

loop = asyncio.new_event_loop()

class MCPClient:
    def __init__(self):
        # Initialize session and client objects
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()
        self.client = None  # Will be initialized when API key is set
        self.api_key = os.getenv("GROQ_API_KEY")

    async def connect_to_server(self, server_script_path: str):
      """Connect to an MCP server

        Args:
          server_script_path: Path to the server script (.py or .js)
      """
      is_python = server_script_path.endswith('.py')
      is_js = server_script_path.endswith('.js')
      if not (is_python or is_js):
          raise ValueError("Server script must be a .py or .js file")

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

      # List available tools
      response = await self.session.list_tools()
      tools = response.tools
      print("\nConnected to server with tools:", [tool.name for tool in tools])

    async def _process_query(self, message: str, history: List[Union[Dict[str, Any], 'ChatMessage']]):
        model_messages = []
        # Add system prompt (omitted for brevity)

        for msg in history:
            if isinstance(msg, ChatMessage):
                role, content = msg.role, msg.content
            else:
                role, content = msg.get("role"), msg.get("content")

            if role in ["user", "assistant", "system"]:
                model_messages.append({"role": role, "content": content})

        model_messages.append({"role": "user", "content": message})

        response = await self.session.list_tools()
        available_tools = [{
          "type": "function",
          "function": {
          "name": tool.name,
          "description": tool.description,
          "parameters": tool.inputSchema}
        } for tool in response.tools]

        response = self.client.chat.completions.create(
            model=MODEL,
            messages=model_messages,
            stream=False,
            tools=available_tools,
            tool_choice="auto",
            max_completion_tokens=1000
        )

        # Extract the response and any tool call responses
        response_message = response.choices[0].message
        tool_calls = response_message.tool_calls
        result_messages = []

        if tool_calls:
            print("\n\n")
            print(f"messages: {model_messages}")
            model_messages.append(response_message)
            for tool_call in tool_calls:
                function_name = tool_call.function.name
                function_args = json.loads(tool_call.function.arguments)
                # Call the tool and get the response
                result_messages.append({
                    "role": "assistant",
                    "content": f"I'll use the {function_name} tool to help answer your question.",
                    "metadata": {
                        "title": f"Using tool: {function_name}",
                        "log": f"Parameters: {json.dumps(function_args, ensure_ascii=True)}",
                        "status": "pending",
                        "id": f"tool_call_{function_name}"
                    }
                })

                result_messages.append({
                    "role": "assistant",
                    "content": "```json\n" + json.dumps(function_args, indent=2, ensure_ascii=True) + "\n```",
                    "metadata": {
                        "parent_id": f"tool_call_{function_name}",
                        "id": f"params_{function_name}",
                        "title": "Tool Parameters"
                    }
                })
                result = await self.session.call_tool(function_name, function_args)

                if result_messages and "metadata" in result_messages[-2]:
                    result_messages[-2]["metadata"]["status"] = "done"

                result_messages.append({
                    "role": "assistant",
                    "content": "Here are the results from the tool:",
                    "metadata": {
                        "title": f"Tool Result for {function_name}",
                        "status": "done",
                        "id": f"result_{function_name}"
                    }
                })

                result_content = result.content
                if isinstance(result_content, list):
                    result_content = "\n".join(str(item.text) for item in result_content)
                # Add the tool response to the conversation
                model_messages.append(
                    {
                        "tool_call_id": tool_call.id,
                        "role": "tool", # Indicates this message is from tool use
                        "name": function_name,
                        "content": result_content,
                    }
                )
            # Make a second API call with the updated conversation
            second_response = self.client.chat.completions.create(
                model=MODEL,
                messages=model_messages
            )
            result_messages.append({
                "role": "assistant",
                "content": second_response.choices[0].message.content
            })
            # Return the final response
            return result_messages

        result_messages.append({
                "role": "assistant",
                "content": response_message.content
            })
        return result_messages

利用Groq提供的低延迟、高性能的推理服务,可以显著提升 AI Agent 的响应速度和效率。 Groq的LPU(Language Processing Unit)架构针对LLM进行了专门优化,可以提供比传统GPU更快的推理速度,从而使得 AI Agent 能够更快地处理用户请求并执行动作。 这对于实时性要求高的应用场景,如客户服务、金融交易等,至关重要。

MCP Server:暴露服务能力的接口

MCP Server 可以具有三种核心能力:

  • Tools: 模型可以在运行时调用的可执行操作。
  • Prompts: 用户控制的模板,可用于触发工作流。
  • Resources: 应用程序控制的静态内容(例如,预加载的文件、数据库转储、图像),为模型提供上下文。

本文同样参考原文,实现了一个简单的自定义 MCP Server,该服务器与巴基斯坦的英文财经新闻服务Business Recorder的RSS feed进行交互。该服务器实现了两个工具:一个用于获取最新新闻标题和摘要,另一个用于获取特定新闻条目的更多信息。

from typing import Any, Literal
import httpx
from mcp.server.fastmcp import FastMCP
import feedparser

# Initialize FastMCP server
mcp = FastMCP("news")
BASE_URL = "https://www.brecorder.com/feeds/"

async def rss_handler(feed_url, max_items=10):
    """
    Fetches the RSS feed from the given URL and prints out the latest entries.
    Args:
        feed_url (str): URL of the RSS feed
        max_items (int): Maximum number of items to display
    """
    feed = feedparser.parse(feed_url)
    fields = ['id', 'title', 'link', 'published', 'summary', 'authors']
    feed = feedparser.parse(feed_url)
    results = []
    for entry in feed.entries[:max_items]:
        item = {}
        for key in fields:
            # Use entry.get to safely fetch values; default to None if missing
            item[key] = entry.get(key)
        results.append(item)
    return results

@mcp.tool()
async def get_news(news_type: Literal["latest", "markets", "world", "pakistan"], max_items: int = 5) -> list:
    """Get news from Business Recorder based on the specified type
    Args:
        news_type: The type of news to fetch. Can be one of: "latest", "markets", "world", "pakistan"
        max_items: The maximum number of items to return
    """
    feed_mapping = {
        "latest": "latest-news",
        "markets": "markets",
        "world": "world",
        "pakistan": "pakistan"
    }

    feed_path = feed_mapping.get(news_type)
    if not feed_path:
        raise ValueError(f"Invalid news type. Must be one of: {list(feed_mapping.keys())}")

    entries = await rss_handler(BASE_URL + feed_path, max_items)
    return entries

@mcp.tool()
async def get_entry_detail(news_type: Literal["latest", "markets", "world", "pakistan"], id: str) -> list:
    """Get more details of a particular article given its id.
    Args:
        news_type: The type of news to fetch. Can be one of: "latest", "markets", "world", "pakistan"
        id: id of the news to get more information of
    """
    feed_mapping = {
        "latest": "latest-news",
        "markets": "markets",
        "world": "world",
        "pakistan": "pakistan"
    }
    feed_path = feed_mapping.get(news_type)
    entries = await rss_handler(BASE_URL+feed_path,20)
    matches = []
    for entry in entries:
        if entry.get("id") == id or entry.get("link", "").rstrip("/").endswith(id):
            matches.append(entry)
    return matches

if __name__ == "__main__":
    # Initialize and run the server
    mcp.run(transport='stdio')

在这个例子中, MCP Server 封装了访问Business Recorder新闻源的逻辑,并暴露了两个工具供 AI Agent 使用。 AI Agent 可以通过调用 get_news 工具来获取指定类型的新闻列表,或者通过调用 get_entry_detail 工具来获取特定新闻条目的详细信息。 这种方式使得 AI Agent 可以轻松地访问外部信息,并将其整合到其决策过程中。

从理论到实践:Gradio Chat UI示例

原文将完整的客户端-服务器实现打包到了Gradio Chat UI中,用户可以通过一个简单的Web界面与 AI Agent 交互,查询新闻信息。 这种方式将复杂的技术细节隐藏在友好的用户界面之后,使得更多的人可以体验 AI Agent 的强大能力。 通过这个例子,我们可以看到, MCP 不仅是一个技术标准,更是一个连接AI技术与实际应用的桥梁。

结论:MCP的未来与展望

随着 AI Agent 从静态检索发展到动态行动系统,像 MCP 这样的协议正成为下一代智能应用的基础。 通过标准化模型和工具之间的通信, MCP 减少了摩擦,促进了互操作性,并使基础设施能够应对快速的工具链发展。 本文演示了如何构建一个最小但功能齐全的客户端-服务器设置,展示了LLM如何以清晰的、协议驱动的方式调用真实世界的工具。 无论您是构建自定义助手、集成专有服务还是试验代理工作流, MCP 都提供了一个灵活、可靠的蓝图。

总而言之,Model Context Protocol (MCP) 代表了 AI Agent 领域的一个重要进步。 通过标准化LLM与外部工具和服务的交互方式, MCP 简化了开发过程,提高了互操作性,并为构建更智能、更强大的应用奠定了基础。 随着 AI Agent 技术的不断发展, MCP 将在推动创新和赋能新一代智能应用方面发挥越来越重要的作用。 结合Groq等硬件加速平台,可以进一步释放 AI Agent 的潜力,使其能够在更广泛的场景中发挥作用,为用户提供更高效、更智能的服务。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注