【日常随笔】基于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的实现做扩充即可。

相关推荐
掘金安东尼29 分钟前
颠覆 LLM?Meta 提出 LCM 这个新范式
人工智能·llm
知了一笑1 小时前
DeepSeekV3:写代码很强了
大模型·ai编程·deepseek
Goboy1 小时前
Java版的深度学习 · 手撕 DeepLearning4J实现手写数字识别 (附UI效果展示)
llm·aigc·ai编程
前端加油站1 小时前
LangChain.js:打造自己的 LLM 应用
langchain·llm
Goboy1 小时前
用AI从零理解推荐系统
llm·aigc·ai编程
谦行1 小时前
AI 基础知识从 -1 到 0.1
机器学习·aigc·ai编程
yumuing11 小时前
AI 用电脑比你还溜?Agent S2 让复杂任务一键搞定
人工智能·gpt·llm
孟健11 小时前
普通人也能造Agent!我用MCP+提示词“炼”出DeepSearch,3000字专业报告全自动生成!
cursor·mcp