llm
激活conda环境:
bash
/root/share/install_conda_env_internlm_base.sh InternLM
conda activate InternLM
llm依赖库安装
# 升级pip
python -m pip install --upgrade pip
pip install modelscope==1.9.5
pip install transformers==4.35.2
pip install streamlit==1.24.0
pip install sentencepiece==0.1.99
pip install accelerate==0.24.1
langchain
依赖
pip install langchain==0.0.292
pip install gradio==4.4.0
pip install chromadb==0.4.15
pip install sentence-transformers==2.2.2
pip install unstructured==0.10.30
pip install markdown==3.3.7
这里使用开源词向量模型Sentence Transformer来进行 Embedding,目前选用这个模型是相对轻量、支持中文且效果较好的。
nltk 资源获取
cd /root
git clone https://gitee.com/yzy0612/nltk_data.git --branch gh-pages
cd nltk_data
mv packages/* ./
cd tokenizers
unzip punkt.zip
cd ../taggers
unzip averaged_perceptron_tagger.zip
知识库搭建
收集数据
import os
def get_files(dir_path):
# args:dir_path,目标文件夹路径
file_list = []
for filepath, dirnames, filenames in os.walk(dir_path):
# os.walk 函数将递归遍历指定文件夹
for filename in filenames:
# 通过后缀名判断文件类型是否满足要求
if filename.endswith(".md"):
# 如果满足要求,将其绝对路径加入到结果列表
file_list.append(os.path.join(filepath, filename))
elif filename.endswith(".txt"):
file_list.append(os.path.join(filepath, filename))
return file_list
加载数据
from tqdm import tqdm
from langchain.document_loaders import UnstructuredFileLoader
from langchain.document_loaders import UnstructuredMarkdownLoader
def get_text(dir_path):
# args:dir_path,目标文件夹路径
# 首先调用上文定义的函数得到目标文件路径列表
file_lst = get_files(dir_path)
# docs 存放加载之后的纯文本对象
docs = []
# 遍历所有目标文件
for one_file in tqdm(file_lst):
file_type = one_file.split('.')[-1]
if file_type == 'md':
loader = UnstructuredMarkdownLoader(one_file)
elif file_type == 'txt':
loader = UnstructuredFileLoader(one_file)
else:
# 如果是不符合条件的文件,直接跳过
continue
docs.extend(loader.load())
return docs
构造向量数据库
得到该列表之后,我们就可以将它引入到 LangChain 框架中构建向量数据库。由纯文本对象构建向量数据库,我们需要先对文本进行分块,接着对文本块进行向量化。
文本分块
LangChain 提供了多种文本分块工具,此处我们使用字符串递归分割器,并选择分块大小为 500,块重叠长度为 150(由于篇幅限制,此处没有展示切割效果,学习者可以自行尝试一下,想要深入学习 LangChain 文本分块可以参考教程 《LangChain - Chat With Your Data》:
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=150)
split_docs = text_splitter.split_documents(docs)
文本向量化
接着我们选用开源词向量模型 Sentence Transformer 来进行文本向量化。LangChain 提供了直接引入 HuggingFace 开源社区中的模型进行向量化的接口:
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(model_name="/root/data/model/sentence-transformer")
向量持久化到chroma
同时,考虑到 Chroma 是目前最常用的入门数据库,我们选择 Chroma 作为向量数据库,基于上文分块后的文档以及加载的开源向量化模型,将语料加载到指定路径下的向量数据库:
from langchain.vectorstores import Chroma
# 定义持久化路径
persist_directory = 'data_base/vector_db/chroma'
# 加载数据库
vectordb = Chroma.from_documents(
documents=split_docs,
embedding=embeddings,
persist_directory=persist_directory # 允许我们将persist_directory目录保存到磁盘上
)
# 将加载的向量数据库持久化到磁盘上
vectordb.persist()
chroma存储结构
# ll /root/data/demo/data_base/vector_db/chroma/
total 16122
drwxr-xr-x 3 root root 4096 Jan 14 14:08 ./
drwxr-xr-x 3 root root 4096 Jan 14 14:07 ../
-rw-r--r-- 1 root root 16502784 Jan 14 14:08 chroma.sqlite3
drwxr-xr-x 2 root root 4096 Jan 14 14:08 dc31fd06-9f49-4c31-b60c-9bb965f75033/
(base) root@intern-studio:~# ll /root/data/demo/data_base/vector_db/chroma/dc31fd06-9f49-4c31-b60c-9bb965f75033/
total 3426
drwxr-xr-x 2 root root 4096 Jan 14 14:08 ./
drwxr-xr-x 3 root root 4096 Jan 14 14:08 ../
-rw-r--r-- 1 root root 3352000 Jan 14 14:08 data_level0.bin
-rw-r--r-- 1 root root 100 Jan 14 14:08 header.bin
-rw-r--r-- 1 root root 113989 Jan 14 14:08 index_metadata.pickle
-rw-r--r-- 1 root root 8000 Jan 14 14:08 length.bin
-rw-r--r-- 1 root root 16976 Jan 14 14:08 link_lists.bin
其中,chroma.sqlite3是一个嵌入式数据库。
(base) root@intern-studio:~# sqlite3 /root/data/demo/data_base/vector_db/chroma/chroma.sqlite3
sqlite> .tables
collection_metadata embedding_metadata
collections embeddings
databases embeddings_queue
embedding_fulltext_search max_seq_id
embedding_fulltext_search_config migrations
embedding_fulltext_search_content segment_metadata
embedding_fulltext_search_data segments
embedding_fulltext_search_docsize tenants
embedding_fulltext_search_idx
InternLM 接入 LangChain
为便捷构建 LLM 应用,我们需要基于本地部署的 InternLM,继承 LangChain 的 LLM 类自定义一个 InternLM LLM 子类,从而实现将 InternLM 接入到 LangChain 框架中。完成 LangChain 的自定义 LLM 子类之后,可以以完全一致的方式调用 LangChain 的接口,而无需考虑底层模型调用的不一致。
基于本地部署的 InternLM 自定义 LLM 类并不复杂,我们只需从 LangChain.llms.base.LLM 类继承一个子类,并重写构造函数与 _call 函数。
构建检索问答链
LangChain 通过提供检索问答链对象来实现对于 RAG 全流程的封装。所谓检索问答链,即通过一个对象完成检索增强问答(即RAG)的全流程,针对 RAG 的更多概念,我们会在视频内容中讲解,也欢迎读者查阅该教程来进一步了解:《LLM Universe》。我们可以调用一个 LangChain 提供的 RetrievalQA
对象,通过初始化时填入已构建的数据库和自定义 LLM 作为参数,来简便地完成检索增强问答的全流程,LangChain 会自动完成基于用户提问进行检索、获取相关文档、拼接为合适的 Prompt 并交给 LLM 问答的全部流程。
加载向量数据库
首先我们需要将上文构建的向量数据库导入进来,我们可以直接通过 Chroma 以及上文定义的词向量模型来加载已构建的数据库
from langchain.vectorstores import Chroma
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
import os
# 定义 Embeddings
embeddings = HuggingFaceEmbeddings(model_name="/root/data/model/sentence-transformer")
# 向量数据库持久化路径
persist_directory = 'data_base/vector_db/chroma'
# 加载数据库
vectordb = Chroma(
persist_directory=persist_directory,
embedding_function=embeddings
)
vectordb对象可以针对用户的 query 进行语义向量检索,得到与用户提问相关的知识片段。
实例化自定义 LLM 与 Prompt Template
接着,我们实例化一个基于 InternLM 自定义的 LLM 对象:
from LLM import InternLM_LLM
llm = InternLM_LLM(model_path = "/root/data/model/Shanghai_AI_Laboratory/internlm-chat-7b")
llm.predict("你是谁")
构建检索问答链,还需要构建一个 Prompt Template,该 Template 其实基于一个带变量的字符串,在检索之后,LangChain 会将检索到的相关文档片段填入到 Template 的变量中,从而实现带知识的 Prompt 构建。我们可以基于 LangChain 的 Template 基类来实例化这样一个 Template 对象:
from langchain.prompts import PromptTemplate
# 我们所构造的 Prompt 模板
template = """使用以下上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。
问题: {question}
可参考的上下文:
···
{context}
···
如果给定的上下文无法让你做出回答,请回答你不知道。
有用的回答:"""
# 调用 LangChain 的方法来实例化一个 Template 对象,该对象包含了 context 和 question 两个变量,在实际调用时,这两个变量会被检索到的文档片段和用户提问填充
QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],template=template)
构建检索问答链
最后,可以调用 LangChain 提供的检索问答链构造函数,基于我们的自定义 LLM、Prompt Template 和向量知识库来构建一个基于 InternLM 的检索问答链:
from langchain.chains import RetrievalQA
qa_chain = RetrievalQA.from_chain_type(llm,retriever=vectordb.as_retriever(),return_source_documents=True,chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
得到的 qa_chain 对象即可以实现我们的核心功能,即基于 InternLM 模型的专业知识库助手。我们可以对比该检索问答链和纯 LLM 的问答效果:
# 检索问答链回答效果
question = "什么是InternLM"
result = qa_chain({"query": question})
print("检索问答链回答 question 的结果:")
print(result["result"])
# 仅 LLM 回答效果
result_2 = llm(question)
print("大模型回答 question 的结果:")
print(result_2)
运行demo
python /root/code/tutorial/langchain/demo/run_gradio.py
正在从本地加载模型...
Loading checkpoint shards: 100%|████████████████████████████████████████████████████████| 8/8 [00:16<00:00, 2.02s/it]
完成本地模型的加载
Running on local URL: http://127.0.0.1:7860
chroma库中表结构
sqlite> .schema
CREATE TABLE migrations (
dir TEXT NOT NULL,
version INTEGER NOT NULL,
filename TEXT NOT NULL,
sql TEXT NOT NULL,
hash TEXT NOT NULL,
PRIMARY KEY (dir, version)
);
CREATE TABLE embeddings_queue (
seq_id INTEGER PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
operation INTEGER NOT NULL,
topic TEXT NOT NULL,
id TEXT NOT NULL,
vector BLOB,
encoding TEXT,
metadata TEXT
);
CREATE TABLE collection_metadata (
collection_id TEXT REFERENCES collections(id) ON DELETE CASCADE,
key TEXT NOT NULL,
str_value TEXT,
int_value INTEGER,
float_value REAL,
PRIMARY KEY (collection_id, key)
);
CREATE TABLE segments (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
scope TEXT NOT NULL,
topic TEXT,
collection TEXT REFERENCES collection(id)
);
CREATE TABLE segment_metadata (
segment_id TEXT REFERENCES segments(id) ON DELETE CASCADE,
key TEXT NOT NULL,
str_value TEXT,
int_value INTEGER,
float_value REAL,
PRIMARY KEY (segment_id, key)
);
CREATE TABLE tenants (
id TEXT PRIMARY KEY,
UNIQUE (id)
);
CREATE TABLE databases (
id TEXT PRIMARY KEY, -- unique globally
name TEXT NOT NULL, -- unique per tenant
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
UNIQUE (tenant_id, name) -- Ensure that a tenant has only one database with a given name
);
CREATE TABLE IF NOT EXISTS "collections" (
id TEXT PRIMARY KEY, -- unique globally
name TEXT NOT NULL, -- unique per database
topic TEXT NOT NULL,
dimension INTEGER,
database_id TEXT NOT NULL REFERENCES databases(id) ON DELETE CASCADE,
UNIQUE (name, database_id)
);
CREATE TABLE embeddings (
id INTEGER PRIMARY KEY,
segment_id TEXT NOT NULL,
embedding_id TEXT NOT NULL,
seq_id BLOB NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (segment_id, embedding_id)
);
CREATE TABLE embedding_metadata (
id INTEGER REFERENCES embeddings(id),
key TEXT NOT NULL,
string_value TEXT,
int_value INTEGER,
float_value REAL, bool_value INTEGER,
PRIMARY KEY (id, key)
);
CREATE TABLE max_seq_id (
segment_id TEXT PRIMARY KEY,
seq_id BLOB NOT NULL
);
CREATE VIRTUAL TABLE embedding_fulltext_search USING fts5(string_value, tokenize='trigram')
/* embedding_fulltext_search(string_value) */;
CREATE TABLE IF NOT EXISTS 'embedding_fulltext_search_data'(id INTEGER PRIMARY KEY, block BLOB);
CREATE TABLE IF NOT EXISTS 'embedding_fulltext_search_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS 'embedding_fulltext_search_content'(id INTEGER PRIMARY KEY, c0);
CREATE TABLE IF NOT EXISTS 'embedding_fulltext_search_docsize'(id INTEGER PRIMARY KEY, sz BLOB);
CREATE TABLE IF NOT EXISTS 'embedding_fulltext_search_config'(k PRIMARY KEY, v) WITHOUT ROWID;