为什么要挂载常识库?

LLM 在答复用户的问题时可能会发生错觉,或许由于练习数据中不包含用户想要的内容而无法答复,通常情况下咱们能够选择微调模型或许外挂常识库来缓解这类问题。微调模型的对数据和算力都有一定的要求,而常识库的门槛会更低一些,所以通常情况下会选择外挂常识库高效地来解决这类问题。

挂载常识库其实相当于引入外部常识,为了扩展语言模型以减少歧义,从大型文本数据库中检索相关文档。通常将输入序列分割成块并检索与用户输入的 query 类似的文档,然后将所选文档放在输入文本之前作为前置常识以改善模型的猜测。使得模型能够更简单、更准确地访问专业常识。

挂载常识库的流程

文档 -> 文档向量化 -> 文档检索 -> 对话交互

咱们能够凭借 langchain 来实现这个流程:

1. 文档

咱们使用中医药书籍来作为常识库,下载链接(700 本中医药古籍文本),下载完成后运转以下代码以兼并数据。

import os
dir_path = "./TCM-Ancient-Books-master"
for index, filename in enumerate(os.listdir(dir_path)):
    if not filename.endswith(".txt"):
        continue
    file_path = os.path.join(dir_path, filename)
    with open(file_path, "r", encoding='gb18030', errors='ignore') as f:
        text = f.read().replace("n", "")
    mode = "a" if index else "w"
    with open("./knowledge.txt", mode, encoding="utf-8") as f:
        f.write(text + "n")

2. 文档向量化

在文档检索的过程中如果使用字符串直接匹配文本类似度功率是很低的,尤其是常识库体量非常大的时分,因此一般会先对文档进行切割和向量化以提高检索的速度。将每个文档转换为数值向量,以便核算文档之间的类似度或进行聚类分析。

from langchain.document_loaders import UnstructuredFileLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS
def load_knowledge():
    filepath = "./knowledge.txt"
    loader = UnstructuredFileLoader(filepath)
    docs = loader.load()
    # chunk-size是文本最大的字符数。chunk-overlap是前后两个chunk的堆叠部分最大字数
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=40)
    docs = text_splitter.split_documents(docs)
    # 这里需求下载一个中文的文本向量化模型
    embeddings = HuggingFaceEmbeddings(model_name="./text2vec-large-chinese",
                                       model_kwargs={'device': 'cuda'})
    # 指定向量化文档加载/保存途径
    save_path = "./med_faiss_store.faiss"
    if not os.path.exists(save_path):
        vector_store = FAISS.from_documents(docs, embeddings)
        vector_store.save_local(save_path)
    else:
        vector_store = FAISS.load_local(save_path, embeddings=embeddings)
    return vector_store

3. 文档检索

使用以下代码依据用户的输入按照相关性对向量库中的文本文本进行排序并取出排名靠前的 5 条常识,并将常识库中的常识库和用户的输入拼接在一起作为新的 prompt。

docs = vector_store.similarity_search(patient_history)     # 核算类似度,并把类似度高的chunk放在前面
knowledge = [doc.page_content for doc in docs[:5]]  # 提取chunk的文本内容
prompt = f"常识库:{knowledge}n问题如下:n{patient_input}"

4. 对话交互

最终是把 prompt 送到模型中得到输出,与模型的交互在上一篇文章中有具体的介绍,这里就不赘述了,直接给出完好的代码(运转时要注意模型途径、常识库途径是否正确)。

from langchain.document_loaders import UnstructuredFileLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.generation import GenerationConfig
import torch
import os
def load_model(model_path):
    tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
    model = AutoModelForCausalLM.from_pretrained(model_path, 
                                                 device_map="auto", 
                                                 trust_remote_code=True
                                                 ).eval()
    return tokenizer, model
def load_knowledge():
    filepath = "./knowledge.txt"
    loader = UnstructuredFileLoader(filepath)
    docs = loader.load()
    # chunk-size是文本最大的字符数。chunk-overlap是前后两个chunk的堆叠部分最大字数
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=40)
    docs = text_splitter.split_documents(docs)
    embeddings = HuggingFaceEmbeddings(model_name="./text2vec-large-chinese",
                                       model_kwargs={'device': 'cuda'}.
    # 指定向量化文档加载/保存途径
    save_path = "./med_faiss_store.faiss"
    if not os.path.exists(save_path):
        vector_store = FAISS.from_documents(docs, embeddings)
        vector_store.save_local(save_path)
    else:
        vector_store = FAISS.load_local(save_path, embeddings=embeddings)
    return vector_store
def clear_screen():
    os.system('clear')
    return [], ""
def chat_qwen(model, tokenizer, SYSTEM_PROMPT, with_knowledge=False):
    vector_store = load_knowledge()
    history, patient_history = clear_screen()
    while True:
        patient_input = input("user:")
        patient_history += patient_input
        if patient_input.lower() == "clc":
            history, patient_history = clear_screen()
            continue
        if with_knowledge:
            docs = vector_store.similarity_search(patient_history)     # 核算类似度,并把类似度高的chunk放在前面
            knowledge = [doc.page_content for doc in docs[:5]]  # 提取chunk的文本内容
            prompt = f"常识库:{knowledge}n问题如下:n{inputs}"
        else:
            prompt = inputs
        response, _ = model.chat(tokenizer, prompt, history=history, system=SYSTEM_PROMPT)
        history.append((patient_input, response))    # history 不包括系统提示和常识库信息
        print("assistant:", response, end="nn")
if __name__ == '__main__':
    model_path = "/root/autodl-tmp/LLM_MODEL/Qwen-1_8B-Chat"
    SYSTEM_PROMPT = ""
    tokenizer, model = load_model(model_path)
    chat_qwen(model, tokenizer, SYSTEM_PROMPT, with_knowledge=True)