博客 / 詳情

返回

langchain 快速入門(三):搭建RAG知識庫

簡介

LLM大模型一般訓練的數據都是滯後的,這是就需要用到RAG知識庫,RAG知識庫可以降低大模型在輸出答案時的幻覺,也能夠讓大模型知識拓展。

知識庫架構知識

檢索流程圖

用户輸入 (User Query)
             |
             v
    +-----------------------+
    |   提示詞 (Prompt)      |
    +-----------------------+
             |
             | (1) 轉化為向量 (Embedding)
             v
    +-----------------------+
    |   文字向量模型 (EMB)    |
    +-----------------------+
             |
             | (2) 相似度檢索 (Search)
             v
    +-----------------------+         +-----------------------+
    |  RAG 向量數據庫 (DB)    | <-----> |   本地知識庫/文檔集     |
    +-----------------------+         +-----------------------+
             |
             | (3) 召回相關片段 (Context)
             v
    +-----------------------+
    |   大語言模型 (LLM)      | <--- (將提示詞與背景片段拼接)
    +-----------------------+
             |
             | (4) 最終生成 (Generation)
             v
    +-----------------------+
    |      輸出結果          |
    +-----------------------+

RAG原理解析

構建知識庫的流程如下:
文檔內容切片->文字向量化->向量數據庫

文檔內容切片

LLM 有上下文長度限制,且向量檢索在短文本上更精確。
文本切片方法有:

  1. 按字數切片
  2. 按句切片
  3. 遞歸切片
    不管是哪個切片方法,目的是保留語義的完整性,因此不是某個好或某個壞,根據實際需求進行選擇。
文字向量化

將每個切片文字塊轉換為關係向量

關係向量是怎麼來的? 這些是通過對模型進行大量的上下文訓練,得到的詞語與詞語,句子與句子的語義關係上的關聯,這是LLM的基礎。
不同的文字向量模型輸出的向量矩陣也會不同,因此在構建和檢索時需要用同一個文字向量模型進行轉換。

向量數據庫

跟平時用的MySql,MS sql等本質上差不多,只不過將文本換成了向量矩陣的增刪改查
檢索的方法(這裏就列舉幾個):

  1. 兩個向量的餘弦角大小
  2. 兩個向量的歐氏舉例
  3. 兩個向量的分類區域

用文本也能搜索,為什麼要用向量呢? 文本的數據庫檢索一般通過正則搜索,這種無法關聯語義進行檢索,因為我們無法保證大模型在檢索時能夠按照固定的格式。舉個例子:

數據庫中存儲:小明一個月工資3000元
搜索:小明這個月多少錢
正則搜索會直接略過,而向量搜索就能夠搜到。

構建RAG知識庫

本次都利用千問的語言模型,和FAISS向量數據庫演示,來演示

案例

from operator import itemgetter
import os
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.documents import Document
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_core.messages import HumanMessage, AIMessage
from sqlalchemy import true
  
#apikey
os.environ["DASHSCOPE_API_KEY"] = "你的apikey"
FAISS_PATH = "my_faiss_index"

raw_text = """
【公司內部機密:代號“深藍計劃”】
1. 項目目標:開發一款能聽懂貓語的翻譯器。
2. 核心技術:基於Transformer的“喵聲波”分析算法。
3. 團隊暗號:如果有人問“今天天氣怎麼樣?”,必須回答“我想吃魚”。
4. 截止日期:2026年12月31日。
5. 經費預算:僅剩50元人民幣,主要用於購買貓條。
"""

#初始化文本
docs = [Document(page_content=raw_text)]
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)
split_docs = text_splitter.split_documents(docs)
  
#初始化模型
llm = ChatTongyi(model="qwen-plus")
embeddings = DashScopeEmbeddings(model="text-embedding-v1")

#創建向量數據庫
if os.path.exists(FAISS_PATH):
    print("向量數據庫已存在")
    ragdb = FAISS.load_local(FAISS_PATH, embeddings, allow_dangerous_deserialization=True)
else:
    print("創建向量數據庫")
    ragdb = FAISS.from_documents(split_docs, embeddings)
    ragdb.save_local(FAISS_PATH)

#構建提示詞chain
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

final_prompt = ChatPromptTemplate.from_messages([
    ("system", """
    你是一個專業的問答助手,你的任務是根據上下文簡潔的回答用户的問題。
    <context>
    {context}
    </context>
    """),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}")
])

chain = (
    #查詢rag
    RunnablePassthrough.assign(
        context = itemgetter("input") | ragdb.as_retriever() | format_docs
    )
    | RunnablePassthrough.assign(
        answer = {"input":itemgetter("input"), "context":itemgetter("context"), "history":itemgetter("history")} | final_prompt | llm | StrOutputParser()
    )
)

history = []

while true:
    input_q = input("我:")
  
    respond = chain.invoke({
        "input": input_q,
        "history": history})

    print("answer:" + respond["answer"])
    print("=="*30)
    
    history.append(HumanMessage(content=input_q))
    history.append(AIMessage(content=respond['answer']))

代碼解釋

代碼的流程如下:

  1. 初始化RAG:文本切片->文本向量模型->構建向量數據庫
  2. 詢問ai:提示詞->文本向量模型->向量數據庫檢索->組合prompt->餵給LLM->回答問題->記錄歷史對話
文本切片
docs = [Document(page_content=raw_text)]
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)
split_docs = text_splitter.split_documents(docs)

這裏利用了langchain提供的文本分詞器RecursiveCharacterTextSplitter(遞歸分詞)

構建向量數據庫
llm = ChatTongyi(model="qwen-plus")
embeddings = DashScopeEmbeddings(model="text-embedding-v1")

#創建向量數據庫
if os.path.exists(FAISS_PATH):
    print("向量數據庫已存在")
    ragdb = FAISS.load_local(FAISS_PATH, embeddings, allow_dangerous_deserialization=True)
else:
    print("創建向量數據庫")
    ragdb = FAISS.from_documents(split_docs, embeddings)
    ragdb.save_local(FAISS_PATH)

這部分要注意:新版FAISS讀取現有數據庫要設置:allow_dangerous_deserialization=True,不然會報錯

提示詞模板
final_prompt = ChatPromptTemplate.from_messages([
    ("system", """
    你是一個專業的問答助手,你的任務是根據上下文簡潔的回答用户的問題。
    <context>
    {context}
    </context>
    """),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}")
])

之前沒有講到歷史對話記錄,這次補充下:

MessagesPlaceholder這個是langchain框架的佔位符(其實是框架寫好了prompt模板,告訴ai這個是歷史對話),使用時將歷史對話記錄的數組放在這裏設置的字段中,在添加歷史對話時要使用相關的類進行聲明對話(告訴ai這句話是ai説的還是用户説的)

history.append(HumanMessage(content=input_q))
history.append(AIMessage(content=respond['answer']))
Chain鏈的解釋(核心邏輯)
chain = (
    #查詢rag
    RunnablePassthrough.assign(
        context = itemgetter("input") | ragdb.as_retriever() | format_docs
    )
    | RunnablePassthrough.assign(
        answer = {"input":itemgetter("input"), "context":itemgetter("context"), "history":itemgetter("history")} | final_prompt | llm | StrOutputParser()
    )
)

Chain鏈流程:

  1. 查詢RAG的chain:獲取input字段->內容交給向量數據庫檢索->將檢索的內容(數組)轉換為字符串格式->保存到context字段並傳遞給下一個任務
  2. 詢問LLM的chain:獲取input,context,history字段->填充上面定義的prompt模板->餵給LLM模型->解析成文本並保存在answer字段

itemgetter是獲取上一個任務傳遞過來的字段內容。

如果❤喜歡❤本系列教程,就點個關注吧,後續不定期更新~

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.