21|文本大模型:chatGLM2-6B的本地部署与前端集成

你好,我是柳博文,欢迎和我一起学习前端工程师的AI实战课。

通过前面的学习,相信你已经逐渐熟悉了AI+前端的开发新范式,对于如何把AI引入到前端工作中有更深的认识。

自从2022年10月ChatGPT问世,已经过去了600多天。在此期间,大模型不断发展,从文本大模型到文生图,图生图大模型,再到现在的文生视频大模型。这些大模型在持续优化进步,让我们切身实际地感受到了AI的力量,未来已来。

接下来的课程,我们就通过动手实验,把这些模型部署到本地,体验一下这些模型的效果。

为了方便国内环境的部署与使用,文本大模型我们选择清华开源的ChatGLM-6B,文生图片大模型选择开源的 StableDiffusion,视频生成大模型则选择腾讯开源的大模型。

那么,这节课我们先来本地部署ChatGLM-6B模型,并实现一个网页来与模型进行问答交互。

初识ChatGLM-6B 模型

ChatGLM-6B 是基于 GLM(General Language Model) 架构的一个针对中英文双语优化的对话生成模型。它与 GPT 模型类似,但针对中文进行了特别的优化,因此在处理中文任务时表现更加友好。

该模型有 60 亿参数,虽然规模比不上 GPT-3 的 1750 亿参数,但它在对话生成任务中具有优秀的平衡性,既能保证较高的生成效果,又不需要过于庞大的计算资源,非常适合在本地部署和运行。

ChatGLM-6B 的核心特点

首先,ChatGLM-6B 经过大规模中英文语料库的训练,因此在双语对话场景下表现得非常流畅。无论是中文用户还是英文用户,都可以通过与模型对话获得自然、连贯的答案。

其次,ChatGLM-6B 在普通设备上(例如 CPU 或较小的 GPU)也能高效运行。对于许多开发者来说,这意味着他们可以在本地部署 ChatGLM-6B,而不必依赖云计算资源,降低了成本,也保证了个人数据的安全。

如何在本地部署 ChatGLM-6B 项目

你可能认为部署大语言模型需要强大的计算资源或云端支持,然而 ChatGLM-6B 的一大优势就是可以在本地运行,即使使用消费级硬件也可以高效推理。

接下来我们就进入动手环节,学习如何将 ChatGLM-6B 模型部署到本地环境,并进行推理操作。

环境准备

在本地运行 ChatGLM-6B 之前,我们需要先准备好相应的运行环境。为了保证推理性能,建议使用具备一定计算能力的设备,以下是基本的硬件和软件需求:

  • 操作系统:Linux、Windows 或 MacOS。
  • Python 版本:Python 3.8 或以上。
  • 硬件要求:如果有 GPU,推理速度会大大提升,特别是使用 CUDA 支持的显卡(如 Nvidia)。
  • 软件依赖:Anaconda(方便管理依赖包)、PyTorch(用于模型推理)。

安装 ChatGLM-6B

这里我们仍然使用conda来进行ChatGLM-6B的环境创建和部署。使用以下命令创建一个虚拟环境并激活它。

conda create -n chatglm python=3.8
conda activate chatglm

进入虚拟环境后,使用 pip 命令安装 PyTorch 和 Hugging Face 的 transformers 库, 同时根据 requirements.txt 中的内容安装依赖库。

pip install torch transformers huggingface_hub
pip install -r requirement.txt

之后我们通过 Hugging Face Hub 下载 ChatGLM-6B 模型。这一步骤只需要运行一次,模型下载后就可以在本地使用,考虑到网络和下载问题,我已经将模型下载完成并放在了代码库中,可以直接下载运行即可。

python -c "from transformers import AutoTokenizer, AutoModel; tokenizer = AutoTokenizer.from_pretrained('THUDM/chatglm-6b'); model = AutoModel.from_pretrained('THUDM/chatglm-6b')"

启动模型并进行本地推理

在模型安装完成后,接下来我们可以编写一个简单的 Python 脚本来启动 ChatGLM-6B 模型,并与其进行对话。以下是一个示例代码:

from transformers import AutoTokenizer, AutoModel
import torch

# 加载模型和 tokenizer
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b")
model = AutoModel.from_pretrained("THUDM/chatglm-6b").half().cuda()

# 定义推理函数
def ask_chatglm(question):
    inputs = tokenizer(question, return_tensors="pt").to("cuda")
    outputs = model.generate(inputs["input_ids"], max_new_tokens=50)
    return tokenizer.decode(outputs[0], skip_special_tokens=True)

# 测试模型
print(ask_chatglm("你好,今天的天气如何?"))

这段代码会调用 ChatGLM-6B 模型,并生成回答。如果你的设备支持 GPU,可以使用 half().cuda() 来加速推理过程。如果没有 GPU的话,可以将 .half().cuda() 替换为 .float() ,这样就会使用 CPU 进行推理。

优化和调试

在本地运行模型时,我们有时可能会遇到显存不足或者计算资源限制等问题。对于 GPU 用户,可以通过使用 half() 方法来减少模型占用的显存,同时也可以通过调整 max_new_tokens 来控制生成文本的长度,从而减少推理时间。

对于 CPU 用户,虽然速度较慢,但依然可以通过多线程或批量处理请求来提高效率。你还可以在实际应用中,结合 Web API 接口,将 ChatGLM-6B 模型封装为服务,以便前端调用。

在前端集成 ChatGLM-6B 模型

前端工程师在 AI 项目中的角色越来越重要,尤其是在实现与用户交互的场景中。要在前端集成 AI 模型,最常见的方式是通过 API 与后端模型进行通信。我们这就来看看如何通过 API 在前端与 ChatGLM-6B 进行交互。

后端 API 的设计

通常,ChatGLM-6B 会部署在后端服务器上,通过 HTTP 请求与前端进行通信。在开源的ChatGLM-6B的源码中已经集成了进行接口请求的服务,代码如下:

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from transformers import AutoTokenizer, AutoModel
import uvicorn, json, datetime
import torch

DEVICE = "cuda"
DEVICE_ID = "0"
CUDA_DEVICE = f"{DEVICE}:{DEVICE_ID}" if DEVICE_ID else DEVICE

def torch_gc():
    if torch.cuda.is_available():
        with torch.cuda.device(CUDA_DEVICE):
            torch.cuda.empty_cache()
            torch.cuda.ipc_collect()

app = FastAPI()

# 配置 CORS 中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 允许所有源进行访问,生产环境中应指定允许的域
    allow_credentials=True,
    allow_methods=["*"],  # 允许所有 HTTP 方法
    allow_headers=["*"],  # 允许所有请求头
)

@app.post("/")
async def create_item(request: Request):
    global model, tokenizer
    json_post_raw = await request.json()
    json_post = json.dumps(json_post_raw)
    json_post_list = json.loads(json_post)
    prompt = json_post_list.get('prompt')
    history = json_post_list.get('history')
    max_length = json_post_list.get('max_length')
    top_p = json_post_list.get('top_p')
    temperature = json_post_list.get('temperature')
    response, history = model.chat(tokenizer,
                                   prompt,
                                   history=history,
                                   max_length=max_length if max_length else 2048,
                                   top_p=top_p if top_p else 0.7,
                                   temperature=temperature if temperature else 0.95)
    now = datetime.datetime.now()
    time = now.strftime("%Y-%m-%d %H:%M:%S")
    answer = {
        "response": response,
        "history": history,
        "status": 200,
        "time": time
    }
    log = "[" + time + "] " + '", prompt:"' + prompt + '", response:"' + repr(response) + '"'
    print(log)
    torch_gc()
    return answer

if __name__ == '__main__':
    tokenizer = AutoTokenizer.from_pretrained("models", trust_remote_code=True)
    model = AutoModel.from_pretrained("models", trust_remote_code=True).cuda()
    model.eval()
    uvicorn.run(app, host='0.0.0.0', port=8000, workers=1)

这段代码实现了一个基于 FastAPI 的 API 服务,使用预训练的 Transformer 模型处理自然语言生成任务。它允许客户端发送包含 prompt 和其他参数的 POST 请求,生成相应的文本响应,并返回给客户端。

代码还配置了 CORS 中间件以允许跨域请求,并在 GPU 上运行 PyTorch 模型以加速计算,同时将服务运行在了本机的8000端口上。

前端调用 API

在前端部分,我们可以编写一个前端页面来完成与API的交互,这里实现了一个网页端的聊天窗口来实现与模型的对话,部分核心代码如下所示。完整代码放在了代码库中,如有需要请下载。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ChatGLM2 Personal Assistant</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #e5e5e5;
            margin: 0;
            padding: 20px;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            overflow: hidden;
        }

        #chat-container {
            width: 70%;
            height: 80%;
            display: flex;
            flex-direction: column;
            background-color: #ffffff;
            border-radius: 8px;
            box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
            overflow: hidden;
            position: relative;
        }
    </style>
</head>

<body>
    <div id="chat-container">
        <h2 style="text-align: center;">ChatGLM2 Personal Assistant</h2>
        <div id="messages"></div>
        <div id="input-container">
            <input type="text" id="prompt" placeholder="Type your message here..." />
            <button id="send">
                <!-- Airplane icon -->
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
                    <path
                        d="M21.992 2.22a.75.75 0 0 0-.755-.122L2.993 9.978a.75.75 0 0 0-.045 1.388l7.468 2.9 2.9 7.468a.75.75 0 0 0 1.387-.045l7.88-18.244a.75.75 0 0 0-.11-.765Zm-5.583 2.905-8.835 8.835 5.608-2.178 3.227-6.657ZM12.04 17.96l-1.883-4.85 8.835-8.835-6.657 3.227-2.178 5.608-4.85-1.883 15.733-6.113-8.835 8.835 1.883 4.85Z" />
                </svg>
            </button>
        </div>
    </div>

    <script>
        // List of random avatar images
        const avatarUrls = [
            'https://randomuser.me/api/portraits/lego/1.jpg',
            'https://randomuser.me/api/portraits/lego/2.jpg'
        ];

        // Randomly assign avatars to the user and bot
        const userAvatar = avatarUrls[Math.floor(Math.random() * avatarUrls.length)];
        const botAvatar = avatarUrls[Math.floor(Math.random() * avatarUrls.length)];

        function formatMessage(text) {
            return text.replace(/\n/g, '<br>'); // Replace newline characters with <br> tags
        }

        function addMessage(text, className, isUser) {
            const messageContainer = document.getElementById('messages');
            const message = document.createElement('div');
            message.className = `message ${className}`;

            const avatar = document.createElement('div');
            avatar.className = 'avatar';
            avatar.style.backgroundImage = `url(${isUser ? userAvatar : botAvatar})`;
            message.appendChild(avatar);

            const bubble = document.createElement('div');
            bubble.className = `bubble ${className}`;
            bubble.innerHTML = formatMessage(text); // Use innerHTML to render HTML
            message.appendChild(bubble);

            if (isUser) {
                message.insertBefore(bubble, avatar);
            }

            messageContainer.appendChild(message);
            messageContainer.scrollTop = messageContainer.scrollHeight;
        }

        async function sendMessage() {
            const promptElement = document.getElementById('prompt');
            const prompt = promptElement.value;
            if (!prompt) return;

            addMessage(prompt, 'user', true);
            promptElement.value = '';

            const responseMessage = document.createElement('div');
            responseMessage.className = 'message bot';
            const avatar = document.createElement('div');
            avatar.className = 'avatar';
            avatar.style.backgroundImage = `url(${botAvatar})`;
            responseMessage.appendChild(avatar);
            const bubble = document.createElement('div');
            bubble.className = 'bubble bot';
            bubble.innerHTML = '<span class="loading"><span></span><span></span><span></span></span>';
            responseMessage.appendChild(bubble);
            document.getElementById('messages').appendChild(responseMessage);

            try {
                const response = await fetch('http://localhost:8000/', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        prompt: prompt,
                        history: [],
                        max_length: 2048,
                        top_p: 0.7,
                        temperature: 0.95,
                    }),
                });

                const data = await response.json();
                bubble.innerHTML = formatMessage(data.response); // Use innerHTML to render HTML

            } catch (error) {
                bubble.innerHTML = `Error: ${error.message}`; // Use innerHTML for error messages
            }

            const messageContainer = document.getElementById('messages');
            messageContainer.scrollTop = messageContainer.scrollHeight;
        }

        document.getElementById('send').addEventListener('click', sendMessage);

        // Support Enter key to send message
        document.getElementById('prompt').addEventListener('keydown', (event) => {
            if (event.key === 'Enter') {
                sendMessage();
            }
        });
    </script>
</body>

</html>

通过这种方式,前端应用可以轻松与 ChatGLM-6B 模型集成,实现自然语言的交互。集成以后的页面大概是这个样子,可以尝试向模型提问,例如:让它生成一段用于StableDiffusion生成电商活动H5页面设计图的提示词。

总结

那么,接下来我们来一起做个总结吧。

在这节课中,我们一起了解了chatGLM-6B文本大模型,chatGLM-6B大模型凭借平衡的参数量,很方便我们个人在本地部署和使用,同时它的推理能力和效果也不错。

随后,我们在本地使用conda完成了运行chatGLM-6B的环境搭建。在这一步中,除了需要下载chatGLM-6B的源代码,还需要下载ChatGLM-6B 模型。

然后我们学习了如何在前端集成这个模型,ChatGLM-6B的源码中已经集成了接口请求的服务。通过分析相关代码,我们知道了API提供了一个基于 FastAPI 的 API 服务,它允许客户端发送包含 prompt 和其他参数的 POST 请求。最后,我们还实现了一个前端聊天页面和大模型做语言交互,它调用了chatGLM-6B的接口。

推荐你课后按照今天的讲解,自己动手练习一下,这样你就能轻松拥有一个自己专属的本地文本大模型助手了。

课后思考

这节课我们学习部署使用了chatGLM-6B模型,我相信你对个人本地化部署大模型有了一些新的思路,那么,除了chatGLM-6B,还有哪些文本大模型可以在PC上进行本地部署和使用呢?

欢迎你在留言区和我交流互动,如果这节课对你有启发,也推荐分享给身边更多朋友。

精选留言