【RAG+向量数据库】小白从0构建一个rag和向量数据库demo

发布于:2025-06-23 ⋅ 阅读:(39) ⋅ 点赞:(0)

使用工具

  1. langchain框架
  2. bge-small-zh-v1.5 嵌入模型
  3. Chroma向量库
  4. LCEL的RAG链

整体过程

构建向量库

首先了解整个过程

  1. 收集数据:txt,pdf等数据格式均可以
  2. 加载数据,可以利用DirectoryLoader加载,但是文章使用的直接遍历加载
  3. 加载embeding模型,本文章使用的是bge-small-zh-v1.5模型
  4. 使用该模型进行向量化
  5. 储存到我们的chroma向量库

查询知识库

  1. 加载llm模型
  2. 加载上面一样的embedding模型
  3. 加载储存好的向量数据库
  4. 从数据库中创建一个检索器
  5. 定义prompt模板
  6. 利用lcel的rag链将他们连一起
  7. 示例问题

详细过程

构建知识库

  1. 导入必要库
import os
from langchain.docstore.document import Document
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_huggingface import HuggingFaceEmbeddings

2.下载模型

git install lfs
$env:GIT_CLONE_PROTECTION_ACTIVE="false"; git clone https://huggingface.co/BAAI/bge-small-zh-v1.5
  1. 选择embeding模型
    下面的path需要更换为自己的path,也就是提前下载好path
model_kwargs = {'device': 'cpu'} # 如果你有支持CUDA的NVIDIA显卡,可以改成 {'device': 'cuda'}
encode_kwargs = {'normalize_embeddings': True} # 设置为True,返回归一化的向量,便于余弦相似度计算
embedding_function = HuggingFaceEmbeddings(
    model_name=MODEL_PATH,
    model_kwargs=model_kwargs,
    encode_kwargs=encode_kwargs
)
  1. 加载文档
    在加载文档的过程中,我们采用document的构建方式,是因为对于每一个获取到的文件,我们都用document进行格式化
  • page_content (str): 文档的核心文本内容。
  • metadata (dict): 描述这份文档的元数据,比如来源文件名、页码、作者等等。这个元数据在后续的高级应用中非常有用。
    目前支持两种格式,当然可拓展性很高

documents = []
    try:
        for root, _, files in os.walk(SOURCE_DIRECTORY):
            print(f"正在扫描文件夹: {root}")
            for file in files:
                file_path = os.path.join(root, file)
                if file.endswith(".txt"):
                    try:
                        with open(file_path, 'r', encoding='utf-8') as f:
                            text = f.read()
                        doc = Document(page_content=text, metadata={"source": file_path})
                        documents.append(doc)
                        print(f"成功手动加载 TXT 文件: {file_path}")
                    except Exception as e:
                        print(f"加载 TXT 文件 {file_path} 时出错: {e}")

                elif file.endswith(".pdf"):
                    try:
                        # 使用LangChain的PyPDFLoader来处理PDF
                        # 它会为PDF的每一页创建一个Document对象
                        pdf_loader = PyPDFLoader(file_path)
                        # .load()返回一个Document列表
                        pdf_docs = pdf_loader.load()
                        documents.extend(pdf_docs) # 将PDF中的所有页面文档添加到主列表中
                        print(f"成功加载 PDF 文件: {file_path} (共 {len(pdf_docs)} 页)")
                    except Exception as e:
                        print(f"加载 PDF 文件 {file_path} 时出错: {e}")
  1. 然后将加载的文档分成小块
  • chunk_size=500: 这定义了每个文本块(Chunk)的最大长度,单位是字符。它会尽量保证每个块不超过500个字符。
  • chunk_overlap=100: 这定义了相邻两个文本块之间的重叠字符数。也就是说,下一个块的开头会包含上一个块结尾的100个字符。
  • 而用到的方法:它会尝试按一个优先级列表来进行分割,这个列表默认是 [“\n\n”, “\n”, " ", “”]。它会首先尝试用段落(两个换行符)来分割,如果分完的块还是太大,它就会退一步,在块内用句子(一个换行符)来分割,再不行就用空格,最后才会粗暴地按字符分割。这比简单的按字符分割效果好得多。
 # 2. 将加载的文档切分成小块 (Chunking)
    # 这对于后续的检索至关重要
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
    split_docs = text_splitter.split_documents(documents)
    print(f"文档已切分为 {len(split_docs)} 个小块。")
  1. 向量化到ChromaDB中
 db = Chroma.from_documents(
        split_docs,
        embedding_function,
        persist_directory=PERSIST_DIRECTORY  # 指定路劲
    )
    # 确保数据被写入磁盘
    db.persist()

查询向量库

  1. 加载llm模型
    这里自行加载储存的密码
secrets = load_secrets()
llm = ChatOpenAI(
    api_key=secrets.get("API_KEY"),
    base_url=secrets.get("BASE_URL"),
    model_name=secrets.get("MODEL"),
    temperature=0.0 # 对于RAG,我们希望答案基于事实,所以温度设为0
)
  1. 加载模型
    与上一步一致
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': True}
embedding_function = HuggingFaceEmbeddings(
    model_name=MODEL_PATH,
    model_kwargs=model_kwargs,
    encode_kwargs=encode_kwargs
)
  1. 加载数据库
db = Chroma(
    persist_directory=PERSIST_DIRECTORY,
    embedding_function=embedding_function
)

  1. 创建一个检索器

它将一个完整的向量数据库(db对象)转换成了一个专门负责检索的“检索器”(Retriever对象)
k: 3意味着:“当进行检索时,请返回与查询最相似的前3个文档块”

retriever = db.as_retriever(search_kwargs={"k": 3}) # 设置为返回最相关的3个结果
  1. 定义prompt模板
    利用langchain中的模板进行攥写
template = """
请只根据以下提供的上下文信息来回答问题。

上下文:
{context}

问题:
{question}
"""
prompt = ChatPromptTemplate.from_template(template)
  1. 利用LCEL链构建rag链
  • 其中的retriever是前面的查询向量库,
  • RunnablePassthrough将问题传入
  • | prompt: 上一步的字典被“管道”传给prompt对象。prompt对象接收到这个字典,并用里面的context和question值来填充它的模板,生成一个结构化的Prompt。
  • | llm: 填充好的Prompt被传给llm对象,LLM执行并返回一个AIMessage对象。
  • | StrOutputParser(): AIMessage对象被传给StrOutputParser,它会提取出其中的文本内容,最终返回一个干净的字符串。
rag_chain = (
    # RunnablePassthrough()会将问题同时传给retriever和prompt
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser() # 将LLM的输出解析为字符串
)

7 . 回答
invoke是LCEL链条的“执行”方法。它是触发整个链条从头到尾运行的命令。
当传入question的时候,就会执行整一个链

response = rag_chain.invoke(question)

效果

txt:因果推断是统计学和数据科学中的一个核心领域,它的主要目标是确定一个事件(原因)对另一个事件(结果)的影响程度。这不仅仅是发现变量之间的相关性,而是要探究它们之间是否存在真正的因果关系。

一个经典的概念是“相关不等于因果”。例如,夏天的冰淇淋销量和溺水人数都同时上升,它们高度相关,但我们不能说是吃冰淇淋导致了溺水。这背后其实有一个共同的原因,即“炎热的天气”,这个变量被称为“混杂变量”(Confounding Variable)。

处理混杂变量是因果推断的关键挑战之一。常用的方法包括:

  1. 随机对照试验(RCT):这是黄金标准,通过随机分组来消除混杂因素的影响。
  2. 工具变量法(Instrumental Variables):当无法进行RCT时,寻找一个“工具变量”,它只影响原因而不直接影响结果。
  3. 回归不连续性设计(Regression Discontinuity):利用一个临界值来近似实现随机分组的效果。

RAG:
用户问题: 因果推断是什么?它和相关性有什么区别?
— RAG响应 —
因果推断是统计学和数据科学中的一个核心领域,它的主要目标是确定一个事件(原因)对另一个事件(结果)的影响程度。它不仅仅是发现变量之间的相关性,而是要探究它们之间是否存在真正的因果关系。

与相关性相比,因果推断的区别在于:

  1. 相关性指的是两个变量之间是否存在统计上的关联,即它们是否经常一起变化。例如,夏天的冰淇淋销量和溺水人数 可能同时上升,这表明它们之间存在相关性。

  2. 因果关系则进一步探究一个变量是否真正导致了另一个变量的变化。例如,虽然夏天的冰淇淋销量和溺水人数可能同 时上升,但因果推断会探究是否是吃冰淇淋导致了溺水,而不是其他共同的原因,如炎热的天气。

简而言之,相关性描述了变量之间的统计关联,而因果推断则试图确定一个变量是否是另一个变量的原因。

用户问题: 处理混杂变量的方法有哪些?
— RAG响应 —
处理混杂变量的方法包括:

  1. 随机对照试验(RCT):这是黄金标准,通过随机分组来消除混杂因素的影响。
  2. 工具变量法(Instrumental Variables):当无法进行RCT时,寻找一个“工具变量”,它只影响原因而不直接影响结果。
  3. 回归不连续性设计(Regression Discontinuity):利用一个临界值来近似实现随机分组的效果。

用户问题: Judea Pearl是谁?
— RAG响应 —
根据提供的知识库,我无法回答该问题。


网站公告

今日签到

点亮在社区的每一天
去签到