MCP 起源于 2024 年 11 月 25 日 Anthropic 发布的文章:Introducing the Model Context Protocol。
MCP 就是以更标准的方式让 LLM Chat 使用不同工具,这样应该更容易理解“中间协议层”的概念了。Anthropic 旨在实现 LLM Tool Call 的标准。
MCP (Model Context Protocol,模型上下文协议)定义了应用程序和 AI 模型之间交换上下文信息的方式。这使得开发者能够以一致的方式将各种数据源、工具和功能连接到 AI 模型(一个中间协议层),就像 USB-C 让不同设备能够通过相同的接口连接一样。MCP 的目标是创建一个通用标准,使 AI 应用程序的开发和集成变得更加简单和统一。
MCP 主解决的一个问题是:如何将 AI 模型和我们已有系统集成。
在跟 LLM 交互的时候,为了显著提升模型的能力需要提供更结构化的上下文信息,例如提供一些更具体、实时的信息(比如本地文件,数据库,一些网络实时信息等)给模型,这样模型更容易理解真实场景中的问题。想象一下没有 MCP 之前会怎么做?我们会人工从数据库中筛选或者使用工具检索可能需要的信息复制到 prompt 中,随着要解决的问题越来越复杂,人工参与这样的数据交互过程变得越来越难以实现。
许多 LLM 如 OpenAI、Google、Qwen 等引入了 function call 功能,这一机制允许模型在需要时调用预定义的函数来获取数据或执行操作显著提升了自动化水平,当前大热门 AI Agent 也具备类似的能力。那么 function calling、AI Agent、MCP 这三者之间有什么区别:
基于 AI 做编程和应用开发时,开发者都希望将数据、系统连接到模型的这个环节可以更智能、更统一。Anthropic 基于这样的痛点设计了 MCP,充当 AI 模型的「万能转接头」,让 LLM 能轻松的获取数据或者调用工具。更具体的说 MCP 的优势在于:
在深入学习 MCP 技术原理之前,为了能够对 MCP 比较有体感,可以先了解怎么使用 MCP。Claude Desktop 是比较早支持 MCP 协议的,但是由于众所周知的原因,在国内使用 Claude 并不方便,可以参考它的教程:For Claude Desktop Users - Model Context Protocol。
关于怎么使用 MCP,我打算以 OpenCat 为例介绍一下,如何采用 OpenCat + Qwen
组合使用 MCP:
MCP 遵循客户端-服务器架构(client-server),其中包含以下几个核心概念:
MCP client 充当 LLM 和 MCP server 之间的桥梁,MCP client 的工作流程如下:
MCP server 是 MCP 架构中的关键组件,它可以提供 3 种主要类型的功能:
这些功能使 MCP server 能够为 AI 应用提供丰富的上下文信息和操作能力,从而增强 LLM 的实用性和灵活性。
LLM 调用 MCP 的过程可以分为两个步骤:
为了更加深入理解 MCP 原理,我打算从 MCP 协议的源码进行分析:
根据 MCP 官方提供的 client example 进行源码分析,其中删除了一些不影响阅读逻辑的异常控制代码,通过阅读代码,可以发现模型是通过 prompt 来确定当前有哪些工具,通过将工具的具体使用描述以文本的形式传递给模型,供模型了解有哪些工具以及结合实时情况进行选择,具体情况可以参考代码中的注释:
###################
# 此处省略无关的代码 #
###################
async def start(self):
# 1.初始化所有的 mcp server
for server in self.servers:
await server.initialize()
# 2.获取所有的 tools 命名为 all_tools
all_tools = []
for server in self.servers:
tools = await server.list_tools()
all_tools.extend(tools)
# 3.将所有的 tools 的功能描述格式化成字符串供 LLM 使用,tool.format_for_llm() 我放到了这段代码最后,方便阅读。
tools_description = "\n".join(
[tool.format_for_llm() for tool in all_tools]
)
# 4.基于 prompt 和当前所有工具的信息询问 LLM(Qwen)应该使用哪些工具。
system_message = (
"You are a helpful assistant with access to these tools:\n\n"
f"{tools_description}\n"
"Choose the appropriate tool based on the user's question. "
"If no tool is needed, reply directly.\n\n"
"IMPORTANT: When you need to use a tool, you must ONLY respond with "
"the exact JSON object format below, nothing else:\n"
"{\n"
' "tool": "tool-name",\n'
' "arguments": {\n'
' "argument-name": "value"\n'
" }\n"
"}\n\n"
"After receiving a tool's response:\n"
"1. Transform the raw data into a natural, conversational response\n"
"2. Keep responses concise but informative\n"
"3. Focus on the most relevant information\n"
"4. Use appropriate context from the user's question\n"
"5. Avoid simply repeating the raw data\n\n"
"Please use only the tools that are explicitly defined above."
)
messages = [{"role": "system", "content": system_message}]
while True:
# 5.将用户输入消息进行聚合
messages.append({"role": "user", "content": user_input})
# 6.将 system_message 和用户消息输入一起发送给 LLM
llm_response = self.llm_client.get_response(messages)
# ...
class Tool:
"""Represents a tool with its properties and formatting."""
def __init__(
self, name: str, description: str, input_schema: dict[str, Any]
) -> None:
self.name: str = name
self.description: str = description
self.input_schema: dict[str, Any] = input_schema
# 把工具的名字 / 工具的用途(description)和工具所需要的参数(args_desc)转化为文本
def format_for_llm(self) -> str:
"""Format tool information for LLM.
Returns:
A formatted string describing the tool.
"""
args_desc = []
if "properties" in self.input_schema:
for param_name, param_info in self.input_schema["properties"].items():
arg_desc = (
f"- {param_name}: {param_info.get('description', 'No description')}"
)
if param_name in self.input_schema.get("required", []):
arg_desc += " (required)"
args_desc.append(arg_desc)
return f"""
Tool: {self.name}
Description: {self.description}
Arguments:
{chr(10).join(args_desc)}
还有一个地方需要分析清楚即 Tool 的描述和代码中的 input_schema
是怎么获取的?这个问题需要看一下 MCP 协议的实现(这里我分析的是 Python SDK 源代码),MCP Python SDK 使用装饰器 @mcp.tool()
来装饰函数时,对应的 name
和 description
等其实直接源自用户定义函数的函数名以及函数的 docstring 等,源代码如下:
@classmethod
def from_function(
cls,
fn: Callable,
name: str | None = None,
description: str | None = None,
context_kwarg: str | None = None,
) -> "Tool":
"""Create a Tool from a function."""
func_name = name or fn.__name__ # 获取函数名
if func_name == "<lambda>":
raise ValueError("You must provide a name for lambda functions")
func_doc = description or fn.__doc__ or "" # 获取函数 docstring
is_async = inspect.iscoroutinefunction(fn)
... # 更多请参考原始代码...
可以看出 LLM 使用工具是通过 prompt engineering,即提供所有工具的结构化描述和 few-shot 的 example 来确定该使用哪些工具。另一方面,大模型厂商(如Qwen)肯定做了专门的训练,确保大模型更能理解工具的 prompt 以及输出结构化的 tool call json 代码。
工具的执行就比较简单和直接,把 system prompt(指令与工具调用描述)和用户消息一起发送给模型,然后接收模型的回复。当模型分析用户请求后,它会决定是否需要调用工具:
如果回复中包含结构化 JSON 格式的工具调用请求,则客户端会根据这个 json 代码执行对应的工具。具体的实现逻辑都在 {python} process_llm_response
中,代码,逻辑非常简单。
如果模型执行了 tool call,则工具执行的结果 result 会和 system prompt 和用户消息一起重新发送给模型,请求模型生成最终回复。
如果 tool call 的 json 代码存在问题或者模型产生了幻觉怎么办呢?通过阅读代码发现,我们会 skip 掉无效的调用请求。
执行相关的代码与注释如下:
async def start(self):
# 省略无关的代码
while True:
# 假设这里已经处理了用户消息输入.
messages.append({"role": "user", "content": user_input})
# 获取 LLM 的输出
llm_response = self.llm_client.get_response(messages)
# 处理 LLM 的输出(如果有 tool call 则执行对应的工具)
result = await self.process_llm_response(llm_response)
# 如果 result 与 llm_response 不同,说明执行了 tool call (有额外信息了)
# 则将 tool call 的结果重新发送给 LLM 进行处理。
if result != llm_response:
messages.append({"role": "assistant", "content": llm_response})
messages.append({"role": "system", "content": result})
final_response = self.llm_client.get_response(messages)
logging.info("\nFinal response: %s", final_response)
messages.append(
{"role": "assistant", "content": final_response}
)
# 否则代表没有执行 tool call,则直接将 LLM 的输出返回给用户。
else:
messages.append({"role": "assistant", "content": llm_response})
结合这部分原理分析:
我开源了一个 MCP Repo,以 OpenCat 为 MCP Host,支持 Weather 数据查询的 MCP Server 和 MCP Client(git@github.com/edonyzpc/MCP.git)。
MCP Server 的开发可以参考 Server 章节:MCP/servers/weather-server-mcp
MCP Client 的开发可以参考 Client 章节:MCP/clients/mcp-client
MCP的初学者还缺少两部分:1. MCP的来龙去脉;2.MCP协议的观测和调试。篇幅限制,留待下次分解,Stay Tuned