【日常随笔】基于MCP生态的LLM-Agent开发

本文主要内容搬运自本人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的实现做扩充即可。

相关推荐
Jooolin2 小时前
【编程史】Git是如何诞生的?这可并非计划之中...
linux·git·ai编程
憨憨睡不醒啊2 小时前
如何让LLM智能体开发助力求职之路——构建属于你的智能体开发知识体系📚📚📚
面试·程序员·llm
柯南二号2 小时前
深入理解 Agent 与 LLM 的区别:从智能体到语言模型
人工智能·机器学习·llm·agent
Jooolin3 小时前
【编程史】IDE 是谁发明的?从 punch cards 到 VS Code
ai编程·visual studio code·编译器
Lilith的AI学习日记4 小时前
什么是预训练?深入解读大模型AI的“高考集训”
开发语言·人工智能·深度学习·神经网络·机器学习·ai编程
程序员陆通4 小时前
Vibe Coding AI编程
ai编程
用户21411832636025 小时前
03-mcp-server案例分享-手搓一个MCP-server拥有属于自己的MCP
mcp
Q同学5 小时前
TORL:工具集成强化学习,让大语言模型学会用代码解题
深度学习·神经网络·llm
Jaising6665 小时前
JetBrains AI 打零工(一)——生产力工具与程序员的驾驭之道
ai编程·intellij idea
技术爬爬虾5 小时前
从零开始编写Mcp Server, 发布上线,超简单全网最细
mcp