写给工程师的 MacBook 商用级大模型知识库部署方案(上)

简介: 写给工程师的 MacBook 商用级大模型知识库部署方案(上)




本文介绍了如何在自己的 MacBook 上部署一套知识库方案辅助自己的知识管理工作,希望能给每位计划自己搭建大模型知识库应用的工程师一点参考。


背景


历史的车轮滚滚向前,大模型技术发展日新月异,每天都有新鲜的技术出炉,让人目不暇接,同时具备可玩性和想象空间的各种应用和开源库,仿佛让自己回到了第一次设置 JAVA_HOME 的日子,作为一枚古典工程师,我专门挑了个可能对手上工作有帮助的方向小试一把,尝试在自己的 MacBook 上部署一套知识库方案,看看能不能辅助自己的知识管理工作。


我自己的 Macbook 配置情况如下,可以流畅地运行没问题。经过量化处理的大模型,还是对办公本很友好的。



为什么要在 MacBook 搭建而不是直接采用现成的云服务呢?最核心最重要的是我们手上的文档资料出于安全要求,不能随便上传云服务,也就无法实际验证知识库的实际效用;另外对于工程师来说,自己亲手搭建一个完整的方案、能灵活调整和对接各种不同的模型、评测各种模型不同的表现,也是出于对技术的探索本能使然。



鉴于大模型已经是大模型及其周边概念已经是大家耳熟能详的东西,我这里就不再重复阐述相关的基础概念和理论了,直接进入动手环节,以用最快的速度部署起一个可用的知识库平台为目标,先用起来,再分各个环节优化。

方案概述

 应用架构


首先来看一下最终方案的应用架构是什么样子(下图)。在这套方案中,我们采用实力排上游、并且在使用上对学术和商业都友好的国产大模型 ChatGLM3-6B 对话模型和基于 m3e-base 模型的 embedding search RAG 方案;基于这两个模型封装和 ChatGPT 兼容的 API 接口协议;通过引入 One API 接口管理&分发系统,形成统一的 LLM 接口渠道管理平台规范,并把封装好的接口协议注册进去;搭建与 Dify.ai 齐名的开源大模型知识库平台管理系统 FastGPT,实现集私有知识数据源预处理、嵌入检索、大模型对话一体的完整知识库应用流程。麻雀虽小五脏俱全,最终形成一套既满足商用标准、又能在 MacBook 跑起来的的方案。虽然智能程度和实际需求还有一定差距,但至少我们在不用额外购买显卡或云服务的情况下,以最小成本部署运行、并且能导入实际业务数据(如语雀知识库)进行实操验证,值得每位工程师都来动手尝试一下。


 成型展示


在用户终端,我们基于 FastGPT 提供知识库管理及使用方案。引用其官网介绍:FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的问答场景。先放一张官网上的图片,来增加一点吸引朋友们动手操作的动力:

 部署要点


本套方案部署分为四个主要环节、14个具体步骤,只要一步步实操下去,每位朋友都可以在自己的本本上拥有属于自己的私有大模型知识库系统,步骤清单如下:

主要环节 详细步骤
一、准备大模型 1.1 下载对话语言模型 ChatGLM3-6B1.2 下载文本嵌入模型 m3e-base1.3 使用 chatglm.cpp 对 ChatGLM3-6B 进行量化加速1.4 验证模型问答效果
二、搭建模型API服务 2.1 搭建模型API2.2 搭建 One API 接口管理/分发系统2.3 验证模型接口能力
三、搭建知识库应用 3.1 安装 MongoDB3.2 安装 PostgreSQL & pgvector3.3 搭建 FastGPT 知识库问答系统3.4 验证模型对话能力
四、知识库问答实战 4.1 准备知识库语料4.2 导入知识库数据4.3 验证知识库问答效果


部分步骤可以简单地通过 Docker 镜像一键部署完成,但本着对细节一杆子插到底的部署思路,还是采取了纯手工作业的方法。注意,下面的步骤中仅包含了关键的命令,完整的命令可以参考对应系统的官网介绍。部分安装步骤如果速度不够理想,可以考虑采用国内源,包含但不限于 go、brew、pip、npm 等。


详细步骤


 准备离线模型


这个环节我们的主要任务是把模型文件准备好、完成量化,并通过命令行的方式,进行交互式对话验证。


  • 下载对话语言模型 ChatGLM3-6B


为什么选择 ChatGLM3-6B?常年霸榜的开源国产之光。ChatGLM3 一共开源了对话模型 ChatGLM-6B、基础模型 ChatGLM-6B-Base、长文本对话模型 ChatGLM3-6B-32K,对学术研究完全开放,在填写问卷进行登记后亦允许免费商业使用。无论是用来做上手实践还是微调练习,目前看来都是比较好的选择。
其实最重要的是,看看排行榜上的可选项,我的 MacBook 16G 内存只能带得动 ChatGLM3-6B 量化版本:

ChatGLM3-6B 现在比较方便的下载渠道有 HuggingFace 和 ModelScope,但是很明显能直接下载下来的可能性不大,所以我用家里的旧电脑科学下载后放到私有云CDN上,然后再用公司电脑下载,也方便未来随时随地取用,就是要花点小钱。ModelScope 也试过,不能直接下载文件,并且用 git clone 速度也不太理想,遂放弃。


如果用老一点的版本 ChatGLM2-6B 的话,网上也能找到一些比较好用的第三方镜像站。

  1. HuggingFace:THUDM/chatglm3-6b
  2. ModelScope:ZhipuAI/chatglm3-6b(地址:https://modelscope.cn/models/ZhipuAI/chatglm3-6b/summary


// 从 Git 仓库下载模型文件
// HuggingFace
git lfs install
git clone https://huggingface.co/THUDM/chatglm3-6b

// ModelScope
git lfs install
git clone https://www.modelscope.cn/ZhipuAI/chatglm3-6b.git


  • 下载文本嵌入模型 m3e-base


为什么选择 moka-ai 的 M3E 模型 m3e-base?M3E 向量模型属于小模型,资源使用不高,CPU 也可以运行,使用场景主要是中文,少量英文的情况。用来验证我们的知识库系统足够了

官方下载地址:moka-ai/m3e-base,先把所有的模型文件 download 下来,后面使用


  • 使用 chatglm.cpp 对 ChatGLM3-6B 进行量化加速


当我第一次知道 chatglm.cpp,只能说好人一生平安,chatglm.cpp 的出现拯救了纯 MacBook 党,让我们能在(低性能的)果本上基于 CPU 进行推理,也不会损失过多的精度。(其实损失多少我也不知道,不影响我们正常进行工程部署验证就行)Github Repo: https://github.com/li-plus/chatglm.cpp我使用的 Python 版本:3.11,最好单独准备一个 virtualenv

安装依赖:

cd /Users/yaolu/AGI/github/chatglm.cpp

# 先初始化 git 仓库
git submodule update --init --recursive

# 构建可执行文件
cmake -B build
cmake --build build -j

# 安装 Python 依赖
pip install .


如果发生 No module named 'chatglm_cpp._C' 的错误,把编译出来的文件 _C.cpython-311-darwin.so 放到 chatglm_cpp 目录下。


对 ChatGLM3-6B 进行 8-bit 量化处理:

python ./chatglm_cpp/convert.py -i /Users/yaolu/AGI/huggingface/THUDM/chatglm3-6b -t q8_0 -o chatglm3-ggml-q8.bin


如果电脑带不动,还可以尝试 4-bit、5-bit 参数量化,完整参数列表见 chatglm.cpp 的 quantization types

  • 验证模型问答效果


完成模型量化后,就可以在本地把大模型跑起来了,命令如下:

./build/bin/main -m chatglm3-ggml-q8.bin -i



 搭建模型API服务



我们在这个环节要完成的任务是,按照 ChatGPT 的接口规范、基于 FastAPI 封装 ChatGLM3-6B 的对话和 m3e-base 的嵌入能力;并注册到 One API 接口管理/分发系统中。

  • 搭建模型API


用 chatglm.cpp 自带的 openai_api.py 魔改了一下,使其支持完成对话和文本 embedding 的两个核心调用:

  1. /v1/chat/completions
  2. /v1/embeddings

代码如下:


import asyncio
import logging
import time
from typing import List, Literal, Optional, Union

import chatglm_cpp
from fastapi import FastAPI, HTTPException, status, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field#, computed_field
#from pydantic_settings import BaseSettings
from sse_starlette.sse import EventSourceResponse

from sentence_transformers import SentenceTransformer
from sklearn.preprocessing import PolynomialFeatures
import numpy as np
import tiktoken

logging.basicConfig(level=logging.INFO, format=r"%(asctime)s - %(module)s - %(levelname)s - %(message)s")


class Settings(object):
    model: str = "/Users/yaolu/AGI/github/chatglm.cpp/chatglm3-ggml-q8.bin"
    num_threads: int = 0


class ChatMessage(BaseModel):
    role: Literal["system", "user", "assistant"]
    content: str


class DeltaMessage(BaseModel):
    role: Optional[Literal["system", "user", "assistant"]] = None
    content: Optional[str] = None


class ChatCompletionRequest(BaseModel):
    model: str = "default-model"
    messages: List[ChatMessage]
    temperature: float = Field(default=0.95, ge=0.0, le=2.0)
    top_p: float = Field(default=0.7, ge=0.0, le=1.0)
    stream: bool = False
    max_tokens: int = Field(default=2048, ge=0)

    model_config = {
        "json_schema_extra": {"examples": [{"model": "default-model", "messages": [{"role": "user", "content": "你好"}]}]}
    }


class ChatCompletionResponseChoice(BaseModel):
    index: int = 0
    message: ChatMessage
    finish_reason: Literal["stop", "length"] = "stop"


class ChatCompletionResponseStreamChoice(BaseModel):
    index: int = 0
    delta: DeltaMessage
    finish_reason: Optional[Literal["stop", "length"]] = None


class ChatCompletionUsage(BaseModel):
    prompt_tokens: int
    completion_tokens: int

    #@computed_field
    @property
    def total_tokens(self) -> int:
        return self.prompt_tokens + self.completion_tokens


class ChatCompletionResponse(BaseModel):
    id: str = "chatcmpl"
    model: str = "default-model"
    object: Literal["chat.completion", "chat.completion.chunk"]
    created: int = Field(default_factory=lambda: int(time.time()))
    choices: Union[List[ChatCompletionResponseChoice], List[ChatCompletionResponseStreamChoice]]
    usage: Optional[ChatCompletionUsage] = None

    model_config = {
        "json_schema_extra": {
            "examples": [
                {
                    "id": "chatcmpl",
                    "model": "default-model",
                    "object": "chat.completion",
                    "created": 1691166146,
                    "choices": [
                        {
                            "index": 0,
                            "message": {"role": "assistant", "content": "你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。"},
                            "finish_reason": "stop",
                        }
                    ],
                    "usage": {"prompt_tokens": 17, "completion_tokens": 29, "total_tokens": 46},
                }
            ]
        }
    }


settings = Settings()
app = FastAPI()
app.add_middleware(
    CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]
)
pipeline = chatglm_cpp.Pipeline(settings.model)
lock = asyncio.Lock()

embeddings_model = SentenceTransformer('/Users/yaolu/AGI/huggingface/moka-ai/m3e-base', device='cpu')


def stream_chat(history, body):
    yield ChatCompletionResponse(
        object="chat.completion.chunk",
        choices=[ChatCompletionResponseStreamChoice(delta=DeltaMessage(role="assistant"))],
    )

    for piece in pipeline.chat(
        history,
        max_length=body.max_tokens,
        do_sample=body.temperature > 0,
        top_p=body.top_p,
        temperature=body.temperature,
        num_threads=settings.num_threads,
        stream=True,
    ):
        yield ChatCompletionResponse(
            object="chat.completion.chunk",
            choices=[ChatCompletionResponseStreamChoice(delta=DeltaMessage(content=piece))],
        )

    yield ChatCompletionResponse(
        object="chat.completion.chunk",
        choices=[ChatCompletionResponseStreamChoice(delta=DeltaMessage(), finish_reason="stop")],
    )


async def stream_chat_event_publisher(history, body):
    output = ""
    try:
        async with lock:
            for chunk in stream_chat(history, body):
                await asyncio.sleep(0)  # yield control back to event loop for cancellation check
                output += chunk.choices[0].delta.content or ""
                yield chunk.model_dump_json(exclude_unset=True)
        logging.info(f'prompt: "{history[-1]}", stream response: "{output}"')
    except asyncio.CancelledError as e:
        logging.info(f'prompt: "{history[-1]}", stream response (partial): "{output}"')
        raise e


@app.post("/v1/chat/completions")
async def create_chat_completion(body: ChatCompletionRequest) -> ChatCompletionResponse:
    # ignore system messages
    history = [msg.content for msg in body.messages if msg.role != "system"]
    if len(history) % 2 != 1:
        raise HTTPException(status.HTTP_400_BAD_REQUEST, "invalid history size")

    if body.stream:
        generator = stream_chat_event_publisher(history, body)
        return EventSourceResponse(generator)

    max_context_length = 512
    output = pipeline.chat(
        history=history,
        max_length=body.max_tokens,
        max_context_length=max_context_length,
        do_sample=body.temperature > 0,
        top_p=body.top_p,
        temperature=body.temperature,
    )
    logging.info(f'prompt: "{history[-1]}", sync response: "{output}"')
    prompt_tokens = len(pipeline.tokenizer.encode_history(history, max_context_length))
    completion_tokens = len(pipeline.tokenizer.encode(output, body.max_tokens))

    return ChatCompletionResponse(
        object="chat.completion",
        choices=[ChatCompletionResponseChoice(message=ChatMessage(role="assistant", content=output))],
        usage=ChatCompletionUsage(prompt_tokens=prompt_tokens, completion_tokens=completion_tokens),
    )


class EmbeddingRequest(BaseModel):
    input: List[str]
    model: str

class EmbeddingResponse(BaseModel):
    data: list
    model: str
    object: str
    usage: dict

def num_tokens_from_string(string: str) -> int:
    """Returns the number of tokens in a text string."""
    encoding = tiktoken.get_encoding('cl100k_base')
    num_tokens = len(encoding.encode(string))
    return num_tokens

def expand_features(embedding, target_length):
    poly = PolynomialFeatures(degree=2)
    expanded_embedding = poly.fit_transform(embedding.reshape(1, -1))
    expanded_embedding = expanded_embedding.flatten()
    if len(expanded_embedding) > target_length:
        # 如果扩展后的特征超过目标长度,可以通过截断或其他方法来减少维度
        expanded_embedding = expanded_embedding[:target_length]
    elif len(expanded_embedding) < target_length:
        # 如果扩展后的特征少于目标长度,可以通过填充或其他方法来增加维度
        expanded_embedding = np.pad(expanded_embedding, (0, target_length - len(expanded_embedding)))
    return expanded_embedding


@app.post("/v1/embeddings", response_model=EmbeddingResponse)
async def get_embeddings(request: EmbeddingRequest):


    # 计算嵌入向量和tokens数量 
    embeddings = [embeddings_model.encode(text) for text in request.input]

    # 如果嵌入向量的维度不为1536,则使用插值法扩展至1536维度 
    embeddings = [expand_features(embedding, 1536) if len(embedding) < 1536 else embedding for embedding in embeddings]

    # Min-Max normalization
    embeddings = [embedding / np.linalg.norm(embedding) for embedding in embeddings]

    # 将numpy数组转换为列表
    embeddings = [embedding.tolist() for embedding in embeddings]
    prompt_tokens = sum(len(text.split()) for text in request.input)
    total_tokens = sum(num_tokens_from_string(text) for text in request.input)


    response = {
        "data": [
            {
                "embedding": embedding,
                "index": index,
                "object": "embedding"
            } for index, embedding in enumerate(embeddings)
        ],
        "model": request.model,
        "object": "list",
        "usage": {
            "prompt_tokens": prompt_tokens,
            "total_tokens": total_tokens,
        }
    }

    return response



class ModelCard(BaseModel):
    id: str
    object: Literal["model"] = "model"
    owned_by: str = "owner"
    permission: List = []


class ModelList(BaseModel):
    object: Literal["list"] = "list"
    data: List[ModelCard] = []

    model_config = {
        "json_schema_extra": {
            "examples": [
                {
                    "object": "list",
                    "data": [{"id": "gpt-3.5-turbo", "object": "model", "owned_by": "owner", "permission": []}],
                }
            ]
        }
    }


@app.get("/v1/models")
async def list_models() -> ModelList:
    return ModelList(data=[ModelCard(id="gpt-3.5-turbo")])


让他跑起来的命令,跑在8000端口下:

uvicorn chatglm_cpp.openai_api:app --host 127.0.0.1 --port 8000


写给工程师的 MacBook 商用级大模型知识库部署方案(中):

https://developer.aliyun.com/article/1443297


目录
相关文章
|
3月前
|
人工智能 自然语言处理 知识图谱
Yuxi-Know:开源智能问答系统,基于大模型RAG与知识图谱技术快速构建知识库
Yuxi-Know是一个结合大模型RAG知识库与知识图谱技术的智能问答平台,支持多格式文档处理和复杂知识关系查询,具备多模型适配和智能体拓展能力。
466 0
Yuxi-Know:开源智能问答系统,基于大模型RAG与知识图谱技术快速构建知识库
|
4月前
|
SQL 关系型数据库 数据库
【YashanDB知识库】OM仲裁节点故障后手工切换方案和yasom仲裁重新部署后重新纳管数据库集群方案
本文介绍了主备数据库集群的部署、OM仲裁故障切换及重新纳管的全过程。首先通过解压软件包并调整安装参数完成数据库集群部署,接着说明了在OM仲裁故障时的手动切换方案,包括关闭自动切换开关、登录备节点执行切换命令。最后详细描述了搭建新的yasom仲裁节点以重新纳管数据库集群的步骤,如生成配置文件、初始化进程、执行托管命令等,确保新旧系统无缝衔接,保障数据服务稳定性。
|
4月前
|
数据库
【YashanDB知识库】数据库一主一备部署及一主两备部署时,主备手动切换方法及自动切换配置
【YashanDB知识库】数据库一主一备部署及一主两备部署时,主备手动切换方法及自动切换配置
【YashanDB知识库】数据库一主一备部署及一主两备部署时,主备手动切换方法及自动切换配置
|
4月前
|
存储 人工智能 搜索推荐
WiseMindAI:一款AI智能知识库,数据完全本地化,支持文档对话、10+种文档、10+AI大模型等
WiseMindAI 是一款由 Chris 开发的 AI 智能学习助手,支持数据完全本地化存储,确保用户隐私安全。它兼容多种文档格式(如 PDF、Markdown 等),并提供 AI 文档总结、智能笔记、沉浸式翻译、知识卡片生成等功能。此外,WiseMindAI 支持 10+ 大语言模型和自定义 AI 插件,适用于 Windows 和 Mac 平台,支持简体中文、繁体中文及英文。
370 74
WiseMindAI:一款AI智能知识库,数据完全本地化,支持文档对话、10+种文档、10+AI大模型等
|
17天前
|
数据采集 存储 人工智能
智能体(AI Agent)开发实战之【LangChain】(二)结合大模型基于RAG实现本地知识库问答
智能体(AI Agent)开发实战之【LangChain】(二)结合大模型基于RAG实现本地知识库问答
|
2月前
|
存储 人工智能 安全
Infortress远程访问本地大模型和知识库之深度体验
Infortress是一款革新性的AI工具,解决本地AI部署缺乏远程访问能力的痛点。通过简单配置,用户可轻松搭建本地大模型和知识库,并通过PC客户端与手机APP实现远程访问。其内网穿透技术确保稳定高效的数据交互,所有计算在本地完成,保障数据安全。此外,Infortress还支持家庭数据中心搭建,具备AI分类、异地双活等功能。
|
3月前
|
人工智能 搜索推荐 Java
【重磅】JeecgBoot 里程碑 v3.8.0 发布,支持 AI 大模型、应用、AI 流程编排和知识库
JeecgBoot 最新推出了一整套 AI 大模型功能,包括 AI 模型管理、AI 应用、知识库、AI 流程编排和 AI 对话助手。这标志着其转型为 “AI 低代码平台”,旨在帮助开发者快速构建和部署个性化 AI 应用,降低开发门槛,提升效率。
158 12
|
4月前
|
人工智能 自然语言处理 前端开发
【AI落地应用实战】大模型加速器2.0:基于 ChatDoc + TextIn ParseX+ACGE的RAG知识库问答系统
本文探讨了私有知识库问答系统的难点及解决方案,重点分析了企业知识管理中的痛点,如信息孤岛、知识传承依赖个人经验等问题。同时,介绍了IntFinQ这款知识管理工具的核心特点和实践体验,包括智能问答、深度概括与多维数据分析等功能。文章还详细描述了IntFinQ的本地化部署过程,展示了其从文档解析到知识应用的完整技术闭环,特别是自研TextIn ParseX引擎和ACGE模型的优势。最后总结了该工具对企业和开发者的价值,强调其在提升知识管理效率方面的潜力。
|
4月前
|
SQL druid Java
【YashanDB知识库】共享超过32000字节字符串插入CLOB类型方案
本文来自YashanDB官网,介绍了解决向崖山数据库CLOB类型字段插入超过32000字节字符串时出现的YAS-04107错误的方法。通过JDBC动态变量绑定技术,实现对不同表的超长字符串插入支持。方案基于YashanDB JDBC和Druid组件,动态解析SQL语句并绑定变量,提供通用性。文章包含关键代码示例及程序使用说明,需JDK 1.8环境运行,并提供附件下载以辅助实施。

热门文章

最新文章