已开源:https://github.com/stay-leave/enhance_llm
概念
检索增强生成(Retrieval Augmented Generation, RAG)是一种结合语言模型和信息检索的技术,用于生成更准确且与上下文相关的输出。
通用模型遇到的问题,也是RAG所擅长的:
知识的局限性: RAG 通过从知识库、数据库、企业内部数据等外部数据源中检索相关信息,将其注入到模型提示词中,使模型能够利用这些信息进行推理和生成。这弥补了模型仅基于公开训练数据的不足,让其在面对实时性、非公开或离线数据时也能提供相关内容。
幻觉问题: 由于语言模型的底层运作基于数学概率,其输出本质上是一系列数值运算结果,因此在模型知识匮乏或不擅长的领域,可能会出现幻觉问题,即生成与现实脱节或错误的内容。RAG通过将检索到的准确且相关的信息作为输入的一部分,降低了幻觉问题的发生几率,使模型生成的内容与现实更为一致。
数据安全性: 对于企业来说,数据安全至关重要,将私有数据上传到第三方平台进行训练存在风险。RAG 可以在企业内部构建知识库或数据库,并从中检索信息,将其注入模型提示词。这一过程可以在本地进行,无需将数据上传到外部,从而确保数据安全。
基本工作流程包括三个主要步骤:
1. 检索(Retrieve):
- 输入查询: 用户通过输入查询或问题来开始这个流程。
- 相似性搜索: 系统将用户查询通过嵌入模型转换为向量,并在外部知识源中的向量数据库中进行相似性搜索。
- 返回相关信息: 搜索会返回与查询最接近的前 k 个数据对象(上下文信息),这些对象来自于知识库、数据库或其他数据源。
2. 增强(Augment):
- 填入模板: 用户查询与检索到的上下文信息被填入到一个提示模板中。
- 构建完整提示: 这个模板整合了查询和相关信息,构建出一个完整的提示词,用于指导模型生成。
3. 生成(Generate):
- 输入到 LLM: 构建好的提示被输入到大型语言模型(LLM),比如 GPT 或 Qwen。
- 生成内容: 模型根据提示词中的信息生成相关内容,包括回答、文本或其他输出。
RAG 的优势:
- 即时性: 通过检索外部信息源,RAG 能够即时更新模型的知识,让其对实时性、非公开或离线的数据也能提供有效回应。
- 准确性: 注入的相关信息提升了模型输出的准确性,减少了幻觉问题。
- 数据安全: 可以在内部构建知识库,从而确保敏感数据不会外泄。
这是有道开源的QAnything,可以拿来直接用,使用MILVUS数据库和langchain。画的这个图很好。
可以根据这个图来分析RAG系统的构建流程:
part1:indexing
1.数据读取:各种类型的数据都可以读取。
2.文档切分:因为一篇文档可能很大,模型的上下文窗口有限;用户query的答案一般只会在文档的某部分。
3.向量嵌入:文本向量化。
4.向量入库:将稠密向量存入向量数据库,如mivlus, Chroma,Faiss。个人觉得第一个最好,但是个人配置太麻烦,选择第二个。
part2:Retrieval and generation
1.检索:根据query从向量数据库中检索相关文档。
2.prompt增强:将召回的文档填入prompt作为外部知识。
3.生成:LLM基于增强的prompt生成回复。
part1:indexing
索引阶段,包含数据读取,切片,向量化,入库。
langchain提供了开发文档:https://python.langchain.com/docs/modules/data_connection/document_transformers/
数据读取
使用langchain_community.document_loaders,读取PDF,CSV文件。
from langchain_community.document_loaders.csv_loader import CSVLoader
from langchain_community.document_loaders import PyPDFLoader
# 读取csv,返回list
def load_csv(path):
# 每条记录为一个元素
loader = CSVLoader(
file_path="project_1/data/clean.csv",
encoding='utf-8' # 编码
)
data = loader.load()
return data
# 读取pdf,返回list
def load_pdf(path):
# 是以每页为一个元素的
loader = PyPDFLoader("project_1/data/1.pdf")
pages = loader.load_and_split()
return pages
csv的返回,取前两个:
pdf的返回,取第一个:
文档切块
这里使用RecursiveCharacterTextSplitter,通过特定字符来分割,默认的字符列表是 [“\n\n”, “\n”, " ", “”]
块的大小是指字符的数量。
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=200, # 指定每个文本块的目标大小,这里设置为200个字符。
chunk_overlap=50, # 指定文本块之间的重叠字符数,这里设置为50个字符。
length_function=len, # 用于测量文本长度的函数,这里使用Python内置的`len`函数。
is_separator_regex=False, # 指定`separators`中的分隔符是否应被视为正则表达式,这里设置为False,表示分隔符是字面字符。
separators=["\n\n", "\n", " ", ".", ",", ",", "。", ] # 定义用于分割文本的分隔符列表。
)
pages = load_pdf("project_1/data/1.pdf")
texts = text_splitter.split_documents([pages[0].page_content])
返回也是一个list,可以看到两两之间是有重叠部分的。
向量化
使用BGE模型
from langchain.embeddings import HuggingFaceBgeEmbeddings
model_name = "project_2/bge-large-zh-v1.5"
model_kwargs = {'device': 'cuda'}
# # 当向量都被规范化(归一化)后,它们的范数都是1。
# 余弦相似度的计算只需要向量的点积运算,从而减少了计算的复杂度,加快了处理速度。
hf = HuggingFaceBgeEmbeddings(
encode_kwargs = {'normalize_embeddings': True}
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs,
query_instruction="为这个句子生成表示以用于检索相关文章:"
)
embedding = hf.embed_query("你好!")
len(embedding)
存储嵌入
这里使用chroma数据库,参考:https://python.langchain.com/docs/integrations/vectorstores/chroma/
# 快速创建数据库
from langchain_chroma import Chroma
db = Chroma.from_documents(
documents = texts,
embedding = hf,
ids = None,
collection_name = 'test1',
collection_metadata = {"hnsw:space": "cosine"},
persist_directory = 'project_1/chroma_db'
)
# 相似度方法通过查询文本检索数据
query = "网络首发是什么"
docs = db.similarity_search(query)
print(docs[0].page_content)
检索出4个文本块
part2:Retrieval and generation
现在真正的RAG了
检索
可以直接将Chroma转换为检索器,会根据相关性分数返回
# 创建一个检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
query = "萧炎是谁?"
# Get relevant documents ordered by relevance score
docs = retriever.invoke(query)
生成
将召回转为字符串,输入到prompt中,使用模型推理。
# 实例化自定义模型
llm = Qwen(mode_name_or_path = "/root/autodl-tmp/Qwen1.5-7B-Chat")
prompt = ChatPromptTemplate.from_template(PROMPT_TEMPLATE)
output_parser = StrOutputParser()
# 构建 chain
chain = prompt | llm | output_parser
res = chain.invoke(
{
"context": format_docs(reordered_docs),
"question": "小医仙是谁?"
}
)
print(res)
直接用query进行召回
Qwen1.5-7B-Chat的回复,看起来重点都抓住了。
sft后的回答,训坏了
dpo后的回答,怎么又拉起来了
更新:
自己手动实现多轮对话
使用dpo效果就是不错
这个是sft的,训久了,过拟合。
原始的模型结构,也不错。
这个是4b的回答,参数小就是不行。
以上便是基础RAG的全流程,可以理解为两阶段的实现。
参考:
1.https://huggingface.co/docs/transformers/main_classes/text_generation
2.https://zhuanlan.zhihu.com/p/688926320
3.https://api.python.langchain.com/en/latest/vectorstores/langchain_community.vectorstores.chroma.Chroma.html#langchain_community.vectorstores.chroma.Chroma.from_documents
4.https://www.cnblogs.com/AlwaysSui/p/18144181
5.https://github.com/datawhalechina/self-llm/blob/master/Qwen1.5/02-Qwen1.5-7B-Chat%20%E6%8E%A5%E5%85%A5langchain%E6%90%AD%E5%BB%BA%E7%9F%A5%E8%AF%86%E5%BA%93%E5%8A%A9%E6%89%8B.md
6.https://python.langchain.com/docs/expression_language/get_started/