Mac 本地 RAG 文档问答——Llama2 & ChatGLM3(量化版)& Ollama

关于量化

深度神经网络模型在结构设计好之后,训练过程核心目的是确定每个神经元的权重参数,精度有 16、32、64 位不一,基于GPU加速训练所得,量化就是通过将这些权重的精度降低,以降低硬件要求的过程。
举例而言,LLaMA 模型为 16 位浮点精度,其 7B 版本有 70 亿参数,该模型完整大小为 13 GB,则用户至少须有如此多的内存和磁盘,模型才能可用,更不用提 13B 版本 24 GB 的大小,令人望而却步。但通过量化,比如将精度降至 4 位,则 7B 和 13B 版本分别压至约 4 GB 和 8 GB,消费级硬件即可满足要求,大家便能在个人电脑上体验大模型了。

一、文档问答流程

  1. 加载文档:文档类型可以是网页、PDF、Markdown
  1. 文本切割:将文本分解成一个个块(chunks)
  1. 文本嵌入(embeddings):就是将一段文本,转化成一个能够表征文本语义的向量,语义上相近的文本他们的向量也相似。
  1. 向量存储:将embedding后得到的向量存入向量数据库中,相当于建立了索引
  1. 查询过程:在向量数据库中寻找并返回与这个查询最相似的n个查询
  1. LLM处理:将这n个结果输入LLM进行处理,获得最后的答案。
notion imagenotion image
notion imagenotion image

二、Llama2-cpp + 文档问答

Llama-2-13B-chat-GGUF + LangChain + llama-cpp-python
Llama.cpp 介绍
LLaMA.cpp 项目是开发者 Georgi Gerganov 基于 Meta 释出的 LLaMA 模型(简易 Python 代码示例)手撸的纯 C/C++ 版本,用于模型推理。所谓推理,即是给输入-跑模型-得输出的模型运行过程。那么,纯 C/C++ 版本有何优势呢?
  • 无需任何额外依赖,相比 Python 代码对 PyTorch 等库的要求,C/C++ 直接编译出可执行文件,跳过不同硬件的繁杂准备;
  • 支持 Apple Silicon 芯片的 ARM NEON 加速,x86 平台则以 AVX2 替代;
  • 具有 F16 和 F32 的混合精度;
  • 支持 4-bit 量化;
  • 无需 GPU,可只用 CPU 运行;

2.1、下载和启动 Llama-2 模型(cpp 版)

2.1.1、下载Llama-2模型

网络不稳定,也可以选择手动下载
# 命令行安装和导入使用,有时网络不稳定 # 在命令行里先进入 Python 环境 # 下载单个文件 from huggingface_hub import hf_hub_download hf_hub_download(repo_id="lysandre/arxiv-nlp", filename="config.json") hf_hub_download(repo_id="lysandre/arxiv-nlp", filename="config.json", revision="v1.0") hf_hub_download(repo_id="lysandre/arxiv-nlp", filename="config.json", revision="test-branch") hf_hub_download(repo_id="lysandre/arxiv-nlp", filename="config.json", revision="refs/pr/3") hf_hub_download(repo_id="lysandre/arxiv-nlp", filename="config.json", revision="877b84a8f93f2d619faa2a6e514a32beef88ab0a") # 下载整个仓库 from huggingface_hub import snapshot_download # 路径需要自己创建目标文件夹,该下载程序只会下载文件,不会创建文件夹 local_dir = "/Users/ayd/Desktop/Project_practice/LongChain/Tongyi-Qwen/Qwen-7B-Chat-Int4" snapshot_download(repo_id="Qwen/Qwen-7B-Chat-Int4", ignore_patterns="*.safetensors", local_dir=local_dir) # 忽略某些后缀文件,多个用列表 snapshot_download(repo_id="GanymedeNil/text2vec-large-chinese", ignore_patterns=["*.safetensors","*.bin"], local_dir=local_dir) snapshot_download(repo_id="Qwen/Qwen-7B-Chat-Int4", allow_patterns="*.safetensors", local_dir=local_dir) # 指定下载某些后缀文件,多个用列表

2.1.2、llama-cpp-python 安装和启动

用llama-cpp-python来启动Llama-2模型
  • 指定虚拟环境安装(llama-cpp-python),据说如果和 langchain(langchain env) 装在一个环境中会有冲突
  • 为了启用对于Metal (Apple的GPU加速框架) 的支持,使用以下命令安装llama-cpp-python:
    • 安装命令自带Web server服务,如果提示没有责可以单独安装pip install llama-cpp-python[server] 不过直接用这个命令会报错,可以尝试pip install `llama-cpp-python[server]`
# 先进入虚拟环境 conda activate llama-cpp-python # 只需要安装一次,之后启动不需要重复安装 CMAKE_ARGS="-DLLAMA_METAL=on" FORCE_CMAKE=1 pip install llama-cpp-python
  • 启动llama-cpp-python web server (带Metal GPU加速);
# 先进入虚拟环境 conda activate llama-cpp-python # 启动 llama-cpp python -m llama_cpp.server --model /Users/ayd/Desktop/Project_practice/LongChain/llama-cpp-python/models/Llama-2-13B-chat-GGUF/llama-2-13b-chat.Q2_K.gguf --n_gpu_layers 1
启动过程中安装了很多依赖包(为了能实现OpenAI 格式的 API)
pip install uvicorn pip install anyio pip install starlette pip install fastapi pip install sse_starlette pip install starlette_context pip install pydantic_settings

2.1.3、成功进入界面http://localhost:8000/docs

notion imagenotion image
 
  • content: 消息的文本内容 (String)
  • role: 对话中发出该消息的角色,可取systemuserassistant之一。其中system为高级别的指示,用于指导模型的行为,例如上图的示例中告诉模型: "You are a helpful assistant."。user表示用户发送的消息,assistant表示模型的回答。
 

2.2、LangChain 搭建 LLM应用

2.2.1、应用概述

  • 构建一个用于回答针对特定文档内容提问的聊天机器人
  • 可行的思路:
    • 常规in-context-learning:将文档作为Prompt提供给模型,从而模型能够根据所提供的Context进行回答
      • 文档的长度超出了模型的Context长度限制,原版Llama2的Context长度为4096个Tokens。
      • 对于较长的Context,模型可能会Lost in the Middle,无法准确从Context中获取关键信息。
    • 取巧in-context-learning:在构建Prompt时,只输入与用户的问题最相关的文档内容。
      • 文档处理与存储:将原始文本进行分块 (Splitting),并使用语言模型对每块文本进行embedding,得到文本的向量表示,最后将文本向量存储在支持相似性搜索的向量数据库中。
      • 用户询问和Prompt构建:根据用户输入的询问,使用相似性搜索在向量数据库中提取出与询问最相关的一些文档分块,并将用户询问+文档一起构建Prompt,随后输入LLM并得到回答。
构建文档Q&A应用的常用架构:
notion imagenotion image

2.2.2、构建步骤

  • 调用本地模型(保持本地模型在运行)
from langchain.chat_models import ChatOpenAI from langchain.schema import AIMessage, HumanMessage, SystemMessage chat_model = ChatOpenAI(openai_api_key = "EMPTY", openai_api_base = "http://localhost:8000/v1", max_tokens=256 # openai_pi_base为模型API的Base URL;将调用/v1/chat/completions API # max_tokens限制了模型回答的长度 system_text = "You are a helpful assistant." human_text1 = "What is the capital of France?" assistant_text = "Paris." human_text2 = "How about England?" # 设置上下文信息 messages = [SystemMessage(content=system_text), HumanMessage(content=human_text1), AIMessage(content=assistant_text), HumanMessage(content=human_text2)] # 预测 chat_model.predict_messages(messages)
  • Document Loading
from langchain.document_loaders import UnstructuredMarkdownLoader # 文档加载 loader = UnstructuredMarkdownLoader("./llama-cpp-python/models/Llama-2-13B-chat-GGUF/README.md") text = loader.load() text
  • Text Splitting
from langchain.text_splitter import RecursiveCharacterTextSplitter # 文本切割 text_splitter = RecursiveCharacterTextSplitter( chunk_size = 2000, # 文本进行Split后每个分块的最大长度,所有分块将在这个限制以内 chunk_overlap = 400, # 前后分块overlap的长度,overlap是为了保持前后两个分块之间的语义连续性 length_function = len, # 度量文本长度的方法 is_separator_regex = False # 是否使用正则分隔符 ) all_splits = text_splitter.split_documents(text) all_splits[:5]
  • Text Embeddings + Vector Storage
import requests from langchain.embeddings.base import Embeddings class LocalLlamaEmbeddings(Embeddings): def embed_documents(self, texts): url = "http://localhost:8000/v1/embeddings" request_body = { "input": texts } response = requests.post(url, json=request_body) return [data["embedding"] for data in response.json()["data"]] def embed_query(self, text): url = "http://localhost:8000/v1/embeddings" request_body = { "input": text } response = requests.post(url, json=request_body) return response.json()["data"][0]["embedding"]
from langchain.vectorstores import FAISS # 构造向量数据库 vectorstore = FAISS.from_documents(documents=all_splits, embedding=LocalLlamaEmbeddings()) question = "How to run the program in interactive mode?" docs = vectorstore.similarity_search(question, k=1) # 相似性查询 docs
  • Text Retrieval & Query LLM
from langchain.llms import OpenAI from langchain.chat_models import ChatOpenAI from langchain.chains import RetrievalQA chat_model = ChatOpenAI(openai_api_key = "EMPTY", openai_api_base = "http://localhost:8000/v1", max_tokens=256) qa_chain = RetrievalQA.from_chain_type(chat_model, retriever=vectorstore.as_retriever(search_kwargs={"k": 1})) """ 构造RetrievalQA需要提供一个LLM的实例,我们提供基于本地部署的Llama2构造的ChatOpenAI;还需要提供一个文本的Retriever,我们提供FAISS向量数据库作为一个Retriever,参数search_kwargs={"k":1}设置了Retriever提取的文档分块的数量,决定了最终Prompt包含的文档内容的数量,在这里我们设置为1。 向Chain中传入询问,即可得到LLM根据Retriever提取的文档做出的回答。 """ qa_chain({"query": "How to run the program in interactive mode?"})
# RetrievalQA生成的默认Prompt qa_chain.combine_documents_chain.llm_chain.prompt.messages[0].prompt.template
# 自定义prompt模板 from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate template = """Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. Use three sentences maximum and keep the answer as concise as possible. Always say "thanks for asking!" at the end of the answer. {context} Question: {question} Helpful Answer:""" QA_CHAIN_PROMPT = PromptTemplate.from_template(template) qa_chain = RetrievalQA.from_chain_type( chat_model, retriever=vectorstore.as_retriever(search_kwargs={"k": 1}), chain_type_kwargs={"prompt": QA_CHAIN_PROMPT} ) qa_chain({"query": "What is --interactive option used for?"})
构建过程中安装的包
pip install openai pip install unstructured pip install markdown

三、ChatGLM3-cpp + 文档问答

ChatGLM模型没有现成的量化模型文件,而是需要我们先下载正常的模型文件,然后通过转换编译来生成量化模型文件。

3.1、下载模型文件并编译成 cpp 版本

chatglm2-6b下载
from huggingface_hub import snapshot_download local_dir = "/Users/ayd/Desktop/Project_practice/LongChain/chatglm3-6b-cpp/models/chatglm3-6b" snapshot_download(repo_id="THUDM/chatglm3-6b", ignore_patterns=["*.safetensors"],local_dir=local_dir)

3.1.1、cpp 版模型编译步骤

  1. 先进入终端中,进入到克隆到本地的 chatglm.cpp 文件夹内,执行以下命令
  1. 转换文件类型
    1. 由于 chatglm3-6b 的文件太多,我本来想只下载 bin 文件来进行转换的,结果提示找不到 .safetensors 文件,所不不得不全部下载
      # 划线的部分是 chatglm3-6b 文件夹的路径,更换成自己的 python3 chatglm_cpp/convert.py -i Desktop/Project_practice/LongChain/chatglm3-6b-cpp/models/chatglm3-6b -t q4_0 -o chatglm3-ggml.bin
  1. 编译模型文件
    1. # 正常编译: # --config Release 表示使用指定的Release配置;-j cmake -B build # 创建一个名为“build”的文件夹,并为项目初始化它。 cmake --build build -j --config Release # --build 标志指定构建目录的路径, # -j 标志指定可以同时运行的任务数量,如果没有数量,将使用可用的处理器核心数来运行作业 # --config Release 标志指定构建类型为发布模式 # 开启苹果芯片加速编译:-DGGML_METAL=ON (Mac 推荐) cmake -B build -DGGML_METAL=ON && cmake --build build -j --config Release # 开启 CPU 加速编译:-DGGML_OPENBLAS=ON cmake -B build -DGGML_OPENBLAS=ON && cmake --build build -j
  1. 聊天测试
    1. # 单次查询 ./build/bin/main -m chatglm3-ggml.bin -p 你好 --top_p 0.8 --temp 0.8 # temp(temperature):控制模型结果的随机性 # top_p核采样:控制模型考虑的概率范围;动态设置tokens候选列表的大小。 将可能性之和不超过特定值的top tokens列入候选名单。 # Top_k:允许其他高分tokens有机会被选中。这种采样引入的随机性有助于在很多情况下生成的质量。top-k参数设置为3意味着选择前三个tokens。将如果 k 和 p 都启用,则 p 在 k 之后起作用。 # 交互模式(输入 stop 退出交互;或者 control+C 退出交互) ./build/bin/main -m chatglm3-ggml.bin -i # 设置系统提示词,-p 单次启动 ./build/bin/main -m chatglm3-ggml.bin -p 你好 -s "You are ChatGLM3, a large language model trained by Zhipu.AI. Follow the user's instructions carefully. Respond using markdown." # 函数调用 ./build/bin/main -m chatglm3-ggml.bin --top_p 0.8 --temp 0.8 --sp examples/system/function_call.txt -i # 代码解释器 ./build/bin/main -m chatglm3-ggml.bin --top_p 0.8 --temp 0.8 --sp examples/system/code_interpreter.txt -i

3.1.2、python 绑定

  1. 同样 先进入 chatglm.cpp 文件夹内,执行以下命令
  1. 安装依赖包
    1. CMAKE_ARGS="-DGGML_METAL=ON" pip install -U chatglm-cpp
  1. 使用转译后的 ggml 模型
    1. # To chat in stream(不是很理解有什么特殊的用途) python3 examples/cli_demo.py -m chatglm3-ggml.bin -i # Launch a web demo to chat in your browser(创建web demo) python3 examples/web_demo.py -m chatglm3-ggml.bin # 第一遍提示少了包 gradio(pip 安装即可) # 新版本的 web 界面? streamlit run examples/chatglm3_demo.py

3.1.3、API服务

  1. 安装依赖包
    1. CMAKE_ARGS="-DGGML_METAL=ON" pip install 'chatglm-cpp[api]'
  1. 启动服务
    1. 官方给出的指令会报错:ModuleNotFoundError: No module named 'chatglm_cpp._C',具体查看这个帖子
      MODEL=./chatglm3-ggml.bin uvicorn chatglm_cpp.langchain_api:app --host 127.0.0.1 --port 8000
      根据帖子指示,改进如下:
      # 原本是在chatglm.cpp文件夹中 cd chatglm_cpp # 不进入文件夹会报错Error loading ASGI app. Could not import module "openai_api". MODEL=../chatglm3-ggml.bin uvicorn langchain_api:app --host 127.0.0.1 --port 8000 # 启动 chatglm API MODEL=../chatglm3-ggml.bin uvicorn openai_api:app --host 127.0.0.1 --port 8000 # 启动兼容 openai 的 API
      服务启动之后新开一个终端窗口,并进入虚拟环境,测试 api 接口
      # 测试chatglm API curl http://127.0.0.1:8000 -H 'Content-Type: application/json' -d '{"prompt": "你好"}' # 测试兼容 openai 的 API curl http://127.0.0.1:8000/v1/chat/completions -H 'Content-Type: application/json' -d '{"messages": [{"role": "user", "content": "你好"}]}'
  1. 在代码中引入api 接口
    1. from langchain.llms import ChatGLM llm = ChatGLM(endpoint_url="http://127.0.0.1:8000") llm.predict("你好") # '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'
      from openai import OpenAI client = OpenAI(base_url="http://127.0.0.1:8000/v1") response = client.chat.completions.create(model="default-model", messages=[{"role": "user", "content": "你好"}]) response.choices[0].message.content # '你好👋!我是人工智能助手 ChatGLM3-6B,很高兴见到你,欢迎问我任何问题。'
      也支持流响应,详情查看原仓库教程

3.2、LangChain 搭建 LLM应用

参考llama的构建过程

四、Ollama(大模型管理平台)

Ollama是一个管理器,可以下载、管理、启动一些常见的开源大模型(应该也是量化版),但是目前还不支持 ChatGLM,在下载和启动 Ollama 之后,可以在这里查看怎么在 LangChain 中运用,以及在这里查看 Ollama API 其他使用方法。
Ollama 的下载和安装都比上面提到的两个简单,对新手友好。Ollama 下载 app 并安装之后就默认启动了 API 服务,不需要使用ollama serve命令重复开启(会提示端口被占用)。
ollama
ollamaUpdated Apr 12, 2024
ollama pull llama2 # 拉取一个模型(可以用来更新模型) ollama run llama2 # 运行一个模型,如果模型不存在就会自动下载 ollama rm llama2 # 删除模型 ollama cp llama2 my-llama2 # 复制模型 ollama list # 模型清单 ollama serve # 开启服务 # 从本地gguf 文件中创建运行 mkdir Modelfile # 新建一个文件夹 FROM ./vicuna-33b.Q4_0.gguf # 指定gguf镜像文件路径(这里的 FROM 是 Docker 的命令) ollama create mymodel -f ./Modelfile # 从路径中创建实例 ollama run example # 运行实例 # 多行输入,使用三引号 >>> """Hello, ... world! ... """ # 多模态输入,指定文件路径 >>> What's in this image? /Users/jmorgan/Desktop/smile.png # 将提示作为参数 ollama run llama2 "Summarize this file: $(cat README.md)"
If you have any questions, please contact me.