向量数据库搭建&提示词优化

一、文档加载方法

(一)pdf文档

PyMuPDFLoader 是 PDF 解析器中速度最快的一种,结果会包含 PDF 及其页面的详细元数据,并且每页返回一个文档。

## 安装必要的库
# !pip install rapidocr_onnxruntime -i https://pypi.tuna.tsinghua.edu.cn/simple
# !pip install "unstructured[all-docs]" -i https://pypi.tuna.tsinghua.edu.cn/simple
# !pip install pyMuPDF -i https://pypi.tuna.tsinghua.edu.cn/simple

from langchain.document_loaders import PyMuPDFLoader

# 创建一个 PyMuPDFLoader Class 实例,输入为待加载的 pdf 文档路径
loader = PyMuPDFLoader("../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf")

# 调用 PyMuPDFLoader Class 的函数 load 对 pdf 文件进行加载
pages = loader.load()

page 中的每一元素为一个文档,变量类型为 langchain.schema.document.Document, 文档变量类型包含两个属性:
● page_content 包含该文档的内容。
● meta_data 为文档相关的描述性数据。

(二)md文档

from langchain.document_loaders import UnstructuredMarkdownLoader

loader = UnstructuredMarkdownLoader("../../data_base/knowledge_db/prompt_engineering/1. 简介 Introduction.md")
pages = loader.load()

(三)mp4视频文档

# 需要先使用whisper将视频转写为文本

from langchain.document_loaders import UnstructuredFileLoader
loader = UnstructuredFileLoader("../../data_base/knowledge_db/easy_rl/强化学习入门指南.txt")
pages = loader.load()

二、文档分割

使用LangChain将文本根据 chunk_size(块大小)和chunk_overlap(两个块重叠大小)进行分割

● chunk_size 指每个块包含的字符或 Token (如单词、句子等)的数量
● chunk_overlap 指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息
Langchain 提供多种文档分割方式,区别在怎么确定块与块之间的边界、块由哪些字符/token组成、以及如何测量块大小
● RecursiveCharacterTextSplitter(): 按字符串分割文本,递归地尝试按不同的分隔符进行分割文本。
● CharacterTextSplitter(): 按字符来分割文本。
● MarkdownHeaderTextSplitter(): 基于指定的标题来分割markdown 文件。
● TokenTextSplitter(): 按token来分割文本。
● SentenceTransformersTokenTextSplitter(): 按token来分割文本
● Language(): 用于 CPP、Python、Ruby、Markdown 等。
● NLTKTextSplitter(): 使用 NLTK(自然语言工具包)按句子分割文本。
● SpacyTextSplitter(): 使用 Spacy按句子的切割文本。

#导入文本分割器from langchain.text_splitter import RecursiveCharacterTextSplitter

# 知识库中单段文本长度
CHUNK_SIZE = 500

# 知识库中相邻文本重合长度
OVERLAP_SIZE = 50

# 此处使用 PDF 文件作为示例
from langchain.document_loaders import PyMuPDFLoader

# 创建一个 PyMuPDFLoader Class 实例,输入为待加载的 pdf 文档路径
loader = PyMuPDFLoader("../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf")

# 调用 PyMuPDFLoader Class 的函数 load 对 pdf 文件进行加载
pages = loader.load()
page = pages[1]

# 使用递归字符文本分割器
from langchain.text_splitter import TokenTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=CHUNK_SIZE,
    chunk_overlap=OVERLAP_SIZE
)

# 分割单个文本
text_splitter.split_text(page.page_content[0:1000])
# 分割文本列表
text_splitter.split_documents(pages)

三、文档向量化

向量(Embedding)是NLP中用于表征文本的一种技术,该方法可以将离线的文本映射到维度较低且连续的向量空间中,使得具有相似语义的单词或者句子的向量位置很近,语义差距较大的单词或者句子的向量位置很远。

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
from zhipuai_embedding import ZhipuAIEmbeddings

# 三种加载向量的方法
# embedding = OpenAIEmbeddings() 
# embedding = HuggingFaceEmbeddings(model_name="moka-ai/m3e-base")
embedding = ZhipuAIEmbeddings()

query1 = "机器学习"
query2 = "强化学习"
query3 = "大语言模型"

# 通过对应的 embedding 类生成 query 的 embedding。
emb1 = embedding.embed_query(query1)
emb2 = embedding.embed_query(query2)
emb3 = embedding.embed_query(query3)

# 将返回结果转成 numpy 的格式,便于后续计算
emb1 = np.array(emb1)
emb2 = np.array(emb2)
emb3 = np.array(emb3)


import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

# 第一种计算相关性的方法:点积
# 点积是将两个向量对应位置的元素相乘后求和得到的标量值。点积相似度越大,表示两个向量越相似。
# 点积:计算简单,快速,不需要进行额外的归一化步骤,但丢失了方向信息。
print(f"{query1} 和 {query2} 向量之间的点积为:{np.dot(emb1, emb2)}")
print(f"{query1} 和 {query3} 向量之间的点积为:{np.dot(emb1, emb3)}")
print(f"{query2} 和 {query3} 向量之间的点积为:{np.dot(emb2, emb3)}")

# 第二种计算相关性的方法:余弦相似度
# 余弦相似度将两个向量的点积除以它们的模长的乘积,可以同时比较向量的方向和数量级大小。
print(f"{query1} 和 {query2} 向量之间的余弦相似度为:{cosine_similarity(emb1.reshape(1, -1) , emb2.reshape(1, -1) )}")
print(f"{query1} 和 {query3} 向量之间的余弦相似度为:{cosine_similarity(emb1.reshape(1, -1) , emb3.reshape(1, -1) )}")
print(f"{query2} 和 {query3} 向量之间的余弦相似度为:{cosine_similarity(emb2.reshape(1, -1) , emb3.reshape(1, -1) )}")

四、向量数据库存储

专门用于存储和检索向量的一种数据库,常用的有Faiss、Chroma和Milvus等。

from langchain.vectorstores import Chroma
from langchain.document_loaders import PyMuPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
from zhipuai_embedding import ZhipuAIEmbeddings

from langchain.llms import OpenAI
from langchain.llms import HuggingFacePipeline
from zhipuai_llm import ZhipuAILLM

# 加载 PDF
loaders_chinese = [
    PyMuPDFLoader("../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf") # 南瓜书
]
docs = []
for loader in loaders_chinese:
    docs.extend(loader.load())
# 切分文档
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=150)
split_docs = text_splitter.split_documents(docs)

# 定义 Embeddings
embedding = OpenAIEmbeddings() 
# embedding = HuggingFaceEmbeddings(model_name=model_name, model_kwargs=model_kwargs)
# embedding = ZhipuAIEmbeddings()

# 定义向量数据库
persist_directory = '../../data_base/vector_db/chroma'
vectordb = Chroma.from_documents(
    documents=split_docs[:100], # 为了速度,只选择了前 100 个切分的 doc 进行生成。
    embedding=embedding,
    persist_directory=persist_directory  # 允许我们将persist_directory目录保存到磁盘上
)

# 将向量保存到本地文件
vectordb.persist()

# 加载本地的向量库
vectordb = Chroma(
    persist_directory=persist_directory,
    embedding_function=embedding
)

print(f"向量库中存储的数量:{vectordb._collection.count()}")

五、向量数据库检索

question="什么是机器学习"

sim_docs = vectordb.similarity_search(question,k=3)
print(f"检索到的内容数:{len(sim_docs)}")

for i, sim_doc in enumerate(sim_docs):
    print(f"检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")

如果只考虑检索出内容的相关性会导致内容过于单一,可能丢失重要信息。
最大边际相关性 (MMR, Maximum marginal relevance) 可以帮助在保持相关性的同时,增加内容的丰富度。
MMR的核心思想是在已经选择了一个相关性高的文档之后,再选择一个与已选文档相关性较低但是信息丰富的文档。这样可以在保持相关性的同时,增加内容的多样性,避免过于单一的结果。

mmr_docs = vectordb.max_marginal_relevance_search(question,k=3)
for i, sim_doc in enumerate(mmr_docs):
    print(f"MMR 检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")

六、构造检索式问答链

基于 LangChain,可以构造一个使用 LLM 进行问答的检索式问答链,这是一种通过检索步骤进行问答的方法。可以通过传入一个语言模型和一个向量数据库来创建它作为检索器。然后,可以用问题作为查询调用它,得到一个答案。

# 导入检索式问答链
from langchain.chains import RetrievalQA

# 可以使用 HuggingFacePipeline 本地搭建大语言模型
model_id = 'THUDM/chatglm2-6b-int4' # 采用 int 量化后的模型可以节省硬盘占用以及实时量化所需的运算资源
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModel.from_pretrained(model_id, trust_remote_code=True).half().quantize(4).cuda()
model = model.eval()
pipe = pipeline(
    "text2text-generation",
    model=model, 
    tokenizer=tokenizer, 
    max_length=100
)

llm = HuggingFacePipeline(pipeline=pipe)

# 声明一个检索式问答链
qa_chain = RetrievalQA.from_chain_type(
    llm,
    retriever=vectordb.as_retriever()
)

# 可以以该方式进行检索问答
question = "本知识库主要包含什么内容"
result = qa_chain({"query": question})
print(f"大语言模型的回答为:{result['result']}")
from langchain.prompts import PromptTemplate

# Build prompt
template = """使用以下上下文片段来回答最后的问题。如果你不知道答案,只需说不知道,不要试图编造答案。答案最多使用三个句子。尽量简明扼要地回答。在回答的最后一定要说"感谢您的提问!"
{context}
问题:{question}
有用的回答:"""
QA_CHAIN_PROMPT = PromptTemplate.from_template(template)

# Run chain
qa_chain = RetrievalQA.from_chain_type(
    llm,
    retriever=vectordb.as_retriever(),
    return_source_documents=True,
    chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
)

question = " 2025 年大语言模型效果最好的是哪个模型"
result = qa_chain({"query": question})
print(f"LLM 对问题的回答:{result['result']}")

如果文档太多,无法全部适配到上下文窗口中,可以使用Stuff、Refine、MapReduce或者MapRerank等方法。

  • Stuff:整个内容一次性输入大模型;
  • Refine:多次调用,迭代改进文本质量;
  • MapReduce:每个文档单独回答,最后输出单个答案;
  • MapRerank:根据每个文档上的问答结果分数进行排序;
RetrievalQA.from_chain_type(
    llm,
    retriever=vectordb.as_retriever(),
    chain_type="map_reduce"
)

七、增加历史会话记忆功能

使用 ConversationBufferMemory ,它保存聊天消息历史记录的列表,这些历史记录将在回答问题时与问题一起传递给聊天机器人,从而将它们添加到上下文中。关于更多的 Memory 的使用,包括保留指定对话轮数、保存指定 token 数量、保存历史对话的总结摘要等内容,请参考 langchain 的 Memory 部分的相关文档。

1. 记忆模块调用

from langchain.memory import ConversationBufferMemory

memory = ConversationBufferMemory(
    memory_key="chat_history",  # 与 prompt 的输入变量保持一致。
    return_messages=True  # 将以消息列表的形式返回聊天记录,而不是单个字符串
)

2. 对话检索链(ConversationalRetrievalChain)

  • 将之前的对话与新问题合并生成一个完整的查询语句。
  • 在向量数据库中搜索该查询的相关文档。
  • 获取结果后,存储所有答案到对话记忆区。
  • 用户可在 UI 中查看完整的对话流程。
from langchain.chains import ConversationalRetrievalChain

retriever=vectordb.as_retriever()

qa = ConversationalRetrievalChain.from_llm(
    llm,
    retriever=retriever,
    memory=memory
)
question = "我可以学习到关于强化学习的知识吗?"
result = qa({"question": question})
print(result['answer'])

八、提示词优化方法

1.清晰且明确

清晰并不是简短,更长的需求会提供充分的上下文,模型输出结果会更加准确

方法1:使用分割符来明确指出输入的不同部分

注意:避免提示词注入,什么是提示词注入?是指用户被允许在提示中增加一些输入,它们可能会向模型发出相互矛盾的指令,这可能会导致模型遵循用户的指示而不是你想做的事情。即提示词看起来是一个指令,但是不应该被执行。

方法2:要求模型给出结构化的输出

例如要求模型输出HTML或者JSON

方法3:要求模型检查条件是否得到满足

如果模型没有满足条件,就需要向模型指出并停止,用户也需要考虑问题的边界情况,告诉模型应该怎么避免这些问题的发生

方法4:给模型提供几个示例数据

2.给模型一点思考时间

方法1:明确、细化完成任务的步骤

如果提问模型的任务过于复杂,可能会导致模型急于得出结论而出现推理错误,应当重新设计提问,要求模型在回答之前有一连串的推理,最后再给出答案。

方法2:指示模型给出结论之前找出自己的解决方案

3.大模型的局限性

模型对自己知识边界并不十分了解,它可能编造一些看起来有道理但是并不真实的事情,我们称之为幻觉,一个解决办法是先找到相关的信息,然后再让模型引用这些相关的信息回答问题

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值