本文主要内容搬运自本人CSDN以下博文,欢迎大家关注!
最近在AI大模型领域,MCP这个概念非常火,大大小小的公众号都开始对外炒作这个概念,宣教一种新的大模型Agent开发生态。因为工作原因,近期笔者也对MCP和LLM-Agent开发做了一些接触。因此今天这篇文章就浅聊下笔者对基于MCP的LLM-Agent开发方面,自己粗浅的一些理解。
首先还是聊一下什么是MCP,以及MCP在LLM-Agent开发方面,解决了什么问题。
MCP(Model Context Protocol,模型上下文协议)是由做Claude的公司制定的,其主要的作用是对大模型访问三方能力(OpenAPI、多模态数据源)定下规范。想象下,不管是本地部署的大模型还是remote的agent,或多或少都有访问本地DB、调用外部OpenAPI,或者获取外部的Prompt文本、图片等多模态资源的需求,而对于模型本身来讲,模型自己只能理解自然语言,反倒结构化的数据不好去理解,所以对于Agent开发者,还需要花很多精力去封装这些三方能力,才能够让模型做比较精确的意图识别。而MCP就通过一套协议约定,去解决这个问题,不同的三方能力用同一套描述方式做打标,并且在数据传输层也封装了stdio和sse两套方案,从而分别满足访问本地资源和访问外部资源的需求。
结合Tool use with Claude这篇文章,可以更好理解MCP要解决的问题。比如对于Claude大模型客户端,CallTool的过程,先是根据UserPrompt去识别决定要用哪个Tool,然后才是调用,拿到结果后处理成文本再返回。在这篇文档里面也可以看到每个Tool定义的样子,一般用name、description(用途)和inputSchema(入参标注),就可以满足模型做用途识别和参数识别的需要。
MCP协议相当于对大模型的前端(Agent应用)和后端(三方工具)做了分离,提供了标准化且具备安全性的协议,所以在这个基础上,MCP-Server的市场生态也衍生了出来。比如mcp.so,作为一个类似"网关"的角色,实现了大规模MCP-Server的在线托管,同样近期阿里云也提供了MCP广场的服务。看起来这种模式和传统的Web-OpenAPI市场比较类似,但不同点在于MCP主要面向大模型服务,能够提供多模态的数据,Web-OpenAPI并不重视这个,所以这个概念也不算非常重复。
对于Agent开发者而言,要封装三方工具,除了以往自主封装或者依靠框架约定做封装之外,现在也可以直接利用mcp-go之类的SDK,直接通过ListTools、CallTool等操作实现工具调用,并且各类框架现在也在逐渐跟进基于MCP协议的三方调用模式。从长远上来看,这种模式由很大的发展空间,但短期来看,这套连接的稳定可用性SLA保障基建其实还比较欠缺。所以个人建议对于Agent开发者来说,重点还是去打磨自己的Tool能力或Agent效果,但也要为长期把Tool迁移到MCP生态做准备。
那么基于MCP这套架构,具体Agent要怎么做代码开发呢?其实现在有很多现成的SDK可以用。比如笔者的话,因为工作原因,主要写Go语言,就接触到了mcp-go这套MCP的SDK实现。这套SDK满足了最基础的MCP实现,对于企业内部而言,在这个SDK上做封装,基本上就能够完善MCP-Server的开发生态。因此也简单看一下这个SDK里面,实现了什么东西。
首先是Client连接的实现,这里可以看到每次连接都需要InitializeRequest、InitializeResult以及InitializeNotification这三次握手。从Client角度看逻辑是这样:
go
func (c *StdioMCPClient) Initialize(
ctx context.Context,
request mcp.InitializeRequest,
) (*mcp.InitializeResult, error) {
// This structure ensures Capabilities is always included in JSON
params := struct {
ProtocolVersion string `json:"protocolVersion"`
ClientInfo mcp.Implementation `json:"clientInfo"`
Capabilities mcp.ClientCapabilities `json:"capabilities"`
}{
ProtocolVersion: request.Params.ProtocolVersion,
ClientInfo: request.Params.ClientInfo,
Capabilities: request.Params.Capabilities, // Will be empty struct if not set
}
response, err := c.sendRequest(ctx, "initialize", params)
if err != nil {
return nil, err
}
var result mcp.InitializeResult
if err := json.Unmarshal(*response, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
// Store capabilities
c.capabilities = result.Capabilities
// Send initialized notification
notification := mcp.JSONRPCNotification{
JSONRPC: mcp.JSONRPC_VERSION,
Notification: mcp.Notification{
Method: "notifications/initialized",
},
}
notificationBytes, err := json.Marshal(notification)
if err != nil {
return nil, fmt.Errorf(
"failed to marshal initialized notification: %w",
err,
)
}
notificationBytes = append(notificationBytes, '\n')
if _, err := c.stdin.Write(notificationBytes); err != nil {
return nil, fmt.Errorf(
"failed to send initialized notification: %w",
err,
)
}
c.initialized = true
return &result, nil
}
握手的校验当前还比较粗糙,没有对版本号之类的兼容性做校验。两次握手后Client确认Notification(单向消息)可以发出去,就代表可以建立连接了。
从利于应用开发的角度,开发框架有SDK的话,最好是再封装一层Client把Initialize握手步骤也代理掉,然后把其他List/Call协议也封装成接口,这样对开发者比较方便一些。
然后看Server端的实现,主要包括:资源/Prompt/Tool的管理、C2S的Notification的处理,以及S2C单点Notification跟广播能力。说白了就是无状态、长连接都同时能支持上。
go
// NewMCPServer creates a new MCP server instance with the given name, version and options
func NewMCPServer(
name, version string,
opts ...ServerOption,
) *MCPServer {
s := &MCPServer{
resources: make(map[string]resourceEntry),
resourceTemplates: make(map[string]resourceTemplateEntry),
prompts: make(map[string]mcp.Prompt),
promptHandlers: make(map[string]PromptHandlerFunc),
tools: make(map[string]ServerTool),
name: name,
version: version,
notificationHandlers: make(map[string]NotificationHandlerFunc),
capabilities: serverCapabilities{
tools: nil,
resources: nil,
prompts: nil,
logging: false,
},
}
for _, opt := range opts {
opt(s)
}
return s
}
应用角度就比较简单了,Server端可以基于examples/everything/main.go的实现做扩展,Client端长期来看用SSE的连接方式比较多,参考client/sse_test.go的实现做扩充即可。