3、從langchain到rag

文章目錄

  • 本文介紹
  • 向量和向量數據庫
    • 向量
    • 向量數據庫
  • 索引
  • 開始動手實現rag
    • 加載文檔數據并建立索引
    • 將向量存放到向量數據庫中
    • 檢索生成
    • 構成一條鏈

本文介紹

從本節開始,有了上一節的langchain基礎學習,接下來使用langchain實現一個rag應用,并稍微深入的講解一下流程

向量和向量數據庫

向量

在rag中,不得不提向量和向量數據庫,在ai檢索中多數采用特征,在文本中多數采用向量來表示文本。采用向量后,“語義”的匹配程度就轉換成了向量之間的相似程度。計算向量相似度的算法有很多,比如,余弦相似度、內積、歐氏距離等等。

有了向量,當用戶提出問題時,處理過程就變成了將問題轉換為向量,然后計算向量之間的距離,找到與問題向量最接近的文檔向量,從而實現“語義”的匹配。

OpenAI 提供了一個專門負責將文本轉換成向量的 API——Embeddings。我們可以根據需要,選擇自己部署模型,或是選擇別人提供的服務。不同的 Embedding 模型之間的差異主要取決于訓練樣本,比如有的模型會在中文處理上表現得比較好。

向量數據庫

在 RAG 系統中,我們要把數據存放到哪里呢?我們需要一個數據庫,只不過,我們需要的既不是 Oracle、MySQL 這樣的關系數據庫,也不是 MongoDB、Redis 這樣的 NoSQL 數據庫。因為我們后續處理的都是向量,所以,我們需要的是向量數據庫。

向量數據庫與傳統數據庫有很大的差別,在使用方式上,傳統數據庫搜索信息傾向于精確匹配,而向量數據庫的匹配則是語義上的接近。

在實現上二者也存在不小的差別,比如,由于向量本身通常維度會很多,如果按照傳統數據庫的方式直接進行存儲,將會帶來很多問題。向量數據庫需要把向量數據作為一個完整的單元處理,底層存儲結構也需要根據這個特點進行規劃。另外,向量數據格式也相對單一,每個維度的數據往往都是固定的數據格式(浮點數、二進制整數等)。

索引

下面是一個常見的索引過程:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

在這個過程里面,我們會先對信息源做一次信息提取。信息源可能是各種文檔,比如 Word 文檔、PDF 文件,Web 頁面,甚至是一些圖片。從這些信息源中,我們把內容提取出來,也就是其中的文本。

接下來,我們會把這些文本進行拆分,將其拆分成更小的文本塊。之所以要拆分,主要是原始的文本可能會比較大,這并不利于檢索,還有一點重要原因是,我們前面說過,要把檢索到的信息拼裝到提示詞里,過大的文本可能會造成提示詞超過模型有限的上下文窗口。

再來,就是把文本塊轉換成向量,也就是得到 Embedding 的過程。前面我們說過,這個過程往往是通過訓練好的模型來完成的。到這里,我們就把信息轉換成了向量。最后一步,就是把得到的向量存儲到向量數據庫中,供后續的檢索使用。

至此,我們對常見的 RAG 流程已經有了基本了解。但實際上,RAG 領域正處于一個快速發展的過程中,有很多相關技術也在不斷地涌現:

  • 雖然采用向量搜索對于語義理解很有幫助,但一些人名、縮寫、特定 ID 之類的信息,卻是傳統搜索的強項,有人提出混合搜索的概念,將二者結合起來;
  • 通過各種搜索方式,我們會得到很多的候選內容,但到底哪個與我們的問題更相關,有人引入了重排序(Rerank)模型,以此決定候選內容與查詢問題的相關程度;
  • 除了在已有方向的努力,甚至還有人提出了 RAG 的新方向。我們前面討論的流程前提條件是把原始信息轉換成了向量,但這本質上還是基于文本的,更適合回答一些事實性問題。它無法理解更復雜的關系,比如,我的朋友里誰在 AI 領域里工作。所以,有人提出了基于知識圖譜的 RAG,知識圖譜是一種結構化的語義知識庫,特別適合找出信息之間的關聯。

開始動手實現rag

加載文檔數據并建立索引

建索引,使用TextLoader從txt中加載數據

from langchain_community.document_loaders import TextLoaderloader = TextLoader("introduction.txt")
docs = loader.load()text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma(collection_name="ai_learning",embedding_function=OpenAIEmbeddings(),persist_directory="vectordb"
)
vectorstore.add_documents(splits)

這里的 TextLoader 屬于 DocumentLoader。在 LangChain 中,有一個很重要的概念叫文檔(Document),它包括文檔的內容(page_content)以及相關的元數據(metadata)。所有原始信息都是文檔,索引信息的第一步就是把這些文檔加載進來,這就是 DocumentLoader 的作用。

除了這里用到的 TextLoader,LangChain 社區里已經實現了大量的 DocumentLoader,比如,從數據庫里加載數據的 SQLDatabaseLoader,從亞馬遜 S3 加載文件的 S3FileLoader。基本上,大部分我們需要的文檔加載器都可以找到直接的實現。

拆分加載進來的文檔是 TextSplitter 的主要職責。

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)

雖然都是文本,但怎樣拆分還是有講究的,拆分源代碼和拆分普通文本,處理方法就是不一樣的。LangChain 社區里同樣實現了大量的 TextSplitter,我們可以根據自己的業務特點進行選擇。我們這里使用了 RecursiveCharacterTextSplitter,它會根據常見的分隔符(比如換行符)遞歸地分割文檔,直到把每個塊拆分成適當的大小。

將向量存放到向量數據庫中

做好基礎的準備之后,就要把拆分的文檔存放到向量數據庫里了:

vectorstore = Chroma(collection_name="ai_learning",embedding_function=OpenAIEmbeddings(),persist_directory="vectordb"
)
vectorstore.add_documents(splits)

LangChain 支持了很多的向量數據庫,它們都有一個統一的接口:VectorStore,在這個接口中包含了向量數據庫的統一操作,比如添加、查詢之類的。這個接口屏蔽了向量數據庫的差異,在向量數據庫并不為所有程序員熟知的情況下,給嘗試不同的向量數據庫留下了空間。各個具體實現負責實現這些接口,我們這里采用的實現是 Chroma。

在 Chroma 初始化的過程中,我們指定了 Embedding 函數,它負責把文本變成向量。這里我們采用了 OpenAI 的 Embeddings 實現,你完全可以根據自己的需要選擇相應的實現,LangChain 社區同樣提供了大量的實現,比如,你可以指定 Hugging Face 這個模型社區中的特定模型來做 Embedding。

到這里,我們就完成了索引的過程,看上去還是比較簡單的。為了驗證我們索引的結果,我們可以調用 similarity_search 檢索向量數據庫的數據:

vectorstore = Chroma(collection_name="ai_learning",embedding_function=OpenAIEmbeddings(),persist_directory="vectordb"
)
documents = vectorstore.similarity_search("專欄的作者是誰?")
print(documents)

這里用的 similarity_search 表示的是根據相似度進行搜索,還可以使用 max_marginal_relevance_search,它會采用 MMR(Maximal Marginal Relevance,最大邊際相關性)算法。這個算法可以在保持結果相關性的同時,盡量選擇與已選結果不相似的內容,以增加結果的多樣性。

檢索生成

現在,我們已經為我們 RAG 應用準備好了數據。接下來,就該正式地構建我們的 RAG 應用了。我在之前的聊天機器上做了一些修改,讓它能夠支持 RAG,代碼如下:

from operator import itemgetter
from typing import List
import tiktoken
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage, SystemMessage, trim_messages
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import OpenAIEmbeddings
from langchain_openai.chat_models import ChatOpenAI
from langchain_chroma import Chromavectorstore = Chroma(collection_name="ai_learning",embedding_function=OpenAIEmbeddings(),persist_directory="vectordb"
)retriever = vectorstore.as_retriever(search_type="similarity")# Token 計算
def str_token_counter(text: str) -> int:enc = tiktoken.get_encoding("o200k_base")return len(enc.encode(text))def tiktoken_counter(messages: List[BaseMessage]) -> int:num_tokens = 3tokens_per_message = 3tokens_per_name = 1for msg in messages:if isinstance(msg, HumanMessage):role = "user"elif isinstance(msg, AIMessage):role = "assistant"elif isinstance(msg, ToolMessage):role = "tool"elif isinstance(msg, SystemMessage):role = "system"else:raise ValueError(f"Unsupported messages type {msg.__class__}")# 確保 msg.content 不是 Nonecontent = msg.content or ""  num_tokens += (tokens_per_message+ str_token_counter(role)+ str_token_counter(content))if msg.name:num_tokens += tokens_per_name + str_token_counter(msg.name)return num_tokens# 限制 token 長度
trimmer = trim_messages(max_tokens=4096,strategy="last",token_counter=tiktoken_counter,include_system=True,
)store = {}def get_session_history(session_id: str) -> BaseChatMessageHistory:if session_id not in store:store[session_id] = InMemoryChatMessageHistory()return store[session_id]model = ChatOpenAI()prompt = ChatPromptTemplate.from_messages([("system","""You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.Context: {context}""",),MessagesPlaceholder(variable_name="history"),("human", "{question}"),]
)def format_docs(docs):return "\n\n".join(doc.page_content for doc in docs)context = itemgetter("question") | retriever | format_docs
# first_step的內容有可能是這樣的:
# {
#     "question": "什么是LangChain?",
#     "context": "LangChain 是一個用于構建基于 LLM 的應用的框架。\n它支持多種數據檢索方式。"
# }
first_step = RunnablePassthrough.assign(context=context)from langchain_core.prompt_values import ChatPromptValue
def format_prompt_output(input):"""確保 `prompt` 生成的內容是 `List[BaseMessage]`"""# print("\n[DEBUG] Prompt 輸出類型:", type(input))# print("[DEBUG] Prompt 輸出內容:", input)# 如果 input 是 `ChatPromptValue`,提取 `.messages`if isinstance(input, ChatPromptValue):return input.messages  # 直接返回 `List[BaseMessage]`# 如果 input 已經是 `List[BaseMessage]`,直接返回if isinstance(input, list) and all(isinstance(msg, BaseMessage) for msg in input):return input# 其他情況報錯raise TypeError(f"? format_prompt_output 傳入了不正確的格式: {type(input)}")chain = first_step | prompt | format_prompt_output | trimmer  | modelwith_message_history = RunnableWithMessageHistory(chain,get_session_history=get_session_history,input_messages_key="question",history_messages_key="history",
)config = {"configurable": {"session_id": "dreamhead"}}while True:user_input = input("You:> ")if user_input.lower() == 'exit':breakif user_input.strip() == "":continue# 修正 stream 傳參stream = with_message_history.stream({"question": user_input},  # 直接傳入字符串config=config)# print(prompt)for chunk in stream:print(chunk.content, end='', flush=True)print()

示例代碼運行如下:

這里是引用

為了進行檢索,我們需要指定數據源,這里就是我們的向量數據庫,其中存放著我們前面已經索引過的數據:

vectorstore = Chroma(collection_name="ai_learning",embedding_function=OpenAIEmbeddings(),persist_directory="vectordb"
)retriever = vectorstore.as_retriever(search_type="similarity")

為什么不直接使用向量數據庫呢?因為 Retriever 并不只有向量數據庫一種實現,比如,WikipediaRetriever 可以從 Wikipedia 上進行搜索。所以,一個 Retriever 接口就把具體的實現隔離開來。

回到向量數據庫上,當我們調用 as_retriever 創建 Retriever 時,還傳入了搜索類型(search_type),這里的搜索類型和前面講到向量數據庫的檢索方式是一致的,這里我們傳入的是 similarity,當然也可以傳入 mmr。

文檔檢索出來,并不能直接就和我們的問題拼裝到一起。這時,就輪到提示詞登場了。下面是我們在代碼里用到的提示詞

You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
Context: {context}

在這段提示詞里,我們告訴大模型,根據提供的上下文回答問題,不知道就說不知道。這是一個提示詞模板,在提示詞的最后是我們給出的上下文(Context)。這里上下文是根據問題檢索出來的內容。

有了這個提示詞,再加上聊天歷史和我們的問題,就構成了一個完整的提示詞模板:

prompt = ChatPromptTemplate.from_messages([("system","""You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.Context: {context}""",),MessagesPlaceholder(variable_name="history"),("human", "{question}"),]
)

構成一條鏈

接下來,就是把各個組件組裝到一起,構成一條完整的鏈:

context = itemgetter("question") | retriever | format_docs
first_step = RunnablePassthrough.assign(context=context)
chain = first_step | prompt | trimmer | modelwith_message_history = RunnableWithMessageHistory(chain,get_session_history=get_session_history,input_messages_key="question",history_messages_key="history",
)

在這段代碼里,我們首先構建了一個 context 變量,它也一條鏈。第一步是從傳入參數中獲取到 question 屬性,也就是我們的問題,然后把它傳給 retriever。retriever 會根據問題去做檢索,對應到我們這里的實現,就是到向量數據庫中檢索,檢索的結果是一個文檔列表。

文檔是 LangChain 應用內部的表示,要傳給大模型,我們需要把它轉成文本,這就是 format_docs 做的事情,它主要是把文檔內容取出來拼接到一起:

def format_docs(docs):return "\n\n".join(doc.page_content for doc in docs)

這里補充幾句實現細節。在 LangChain 代碼里, | 運算符被用作不同組件之間的連接,其實現的關鍵就是大部分組件都實現了 Runnable 接口,在這個接口里實現了 orroror 表示這個對象出現在| 左邊時的處理,相應的 ror 表示這個對象出現在右邊時的處理。

Python 在處理 a | b 這個表達式時,它會先嘗試找 a 的 or,如果找不到,它會嘗試找 b 的 ror。所以,在 context 的處理中, 來自標準庫的 itemgetter 雖然沒有實現__or__,但 retriever 因為實現了 Runnable 接口,所以,它也實現了 ror。所以,這段代碼才能組裝出我們所需的鏈。

有了 context 變量,我們可以用它構建了另一個變量 first_step:

first_step = RunnablePassthrough.assign(context=context)

RunnablePassthrough.assign 這個函數就是在不改變鏈當前狀態值的前提下,添加新的狀態值。前面我們說了,這里賦給 context 變量的值是一個鏈,我們可以把它理解成一個函數,它會在運行期執行,其參數就是我們當前的狀態值。現在你可以理解 itemgetter(“question”) 的參數是從哪來的了。這個函數的返回值會用來在當前的狀態里添加一個叫 context 的變量,以便在后續使用。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/67760.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/67760.shtml
英文地址,請注明出處:http://en.pswp.cn/web/67760.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

DeepSeek-R1大模型本地化部署

前言 Ollama作為一個輕量級、易上手的工具,可以幫助你在自己的電腦上快速部署和運行大型語言模型,無需依賴云端服務。通過加載各種開源模型,比如LLaMA、GPT-J等,并通過簡單的命令行操作進行模型推理和測試。 此小結主要介紹使用…

【小白學AI系列】NLP 核心知識點(五)Transformer介紹

Transformer Transformer 是一種基于自注意力機制(Self-Attention Mechanism)的深度學習模型,首次由 Vaswani 等人于 2017 年在論文《Attention is All You Need》中提出。與 RNN 和 LSTM 不同,Transformer 不需要依靠序列順序進…

【高級篇 / IPv6】(7.6) ? 03. 寬帶IPv6 - ADSL撥號寬帶上網配置 ? FortiGate 防火墻

【簡介】大部分ADSL撥號寬帶都支持IPv6,這里以ADSL撥號寬帶為例,演示在FortiGate防火墻上的配置方法。 準備工作 同上篇文章一樣,為了兼顧不熟悉FortiGate防火墻的朋友,我們從基礎操作進行演示,熟練的朋友可以跳過這一…

【Elasticsearch】_all 查詢

在 Elasticsearch 中,_all 查詢是一種特殊的查詢方式,用于在多個索引或數據流中執行搜索操作,而無需顯式指定每個目標索引或數據流的名稱。以下是關于 _all 查詢的詳細說明: _all 查詢概述 用途:_all 查詢允許您在多個…

Linux第104步_基于AP3216C之I2C實驗

Linux之I2C實驗是在AP3216C的基礎上實現的,進一步熟悉修改設備樹和編譯設備樹,以及學習如何編寫I2C驅動和APP測試程序。 1、AP3216C的原理圖 AP3216C集成了一個光強傳感器ALS,一個接近傳感器PS和一個紅外LED,為三合一的環境傳感…

基于單片機的盲人智能水杯系統(論文+源碼)

1 總體方案設計 本次基于單片機的盲人智能水杯設計,采用的是DS18B20實現杯中水溫的檢測,采用HX711及應力片實現杯中水里的檢測,采用DS1302實現時鐘計時功能,采用TTS語音模塊實現語音播報的功能,并結合STC89C52單片機作…

高清種子資源獲取指南 | ??@seedlinkbot

在如今的數字時代,高清影視、音樂、游戲等資源的獲取方式不斷豐富。對于追求高質量資源的用戶而言,一個高效的資源分享平臺至關重要。而 ??seedlinkbot 正是這樣一個便捷的資源獲取工具,為用戶提供高質量的種子資源索引和下載信息。 1. ??…

[paddle] 矩陣相關的指標

行列式 det 行列式定義參考 d e t ( A ) ∑ i 1 , i 2 , ? , i n ( ? 1 ) σ ( i 1 , ? , i n ) a 1 , i 1 a 2 , i 2 , ? , a n , i n det(A) \sum_{i_1,i_2,\cdots,i_n } (-1)^{\sigma(i_1,\cdots,i_n)} a_{1,i_1}a_{2,i_2},\cdots, a_{n,i_n} det(A)i1?,i2?,?,in?…

Spring Boot項目如何使用MyBatis實現分頁查詢

寫在前面:大家好!我是晴空?。如果博客中有不足或者的錯誤的地方歡迎在評論區或者私信我指正,感謝大家的不吝賜教。我的唯一博客更新地址是:https://ac-fun.blog.csdn.net/。非常感謝大家的支持。一起加油,沖鴨&#x…

【論文筆記】Fast3R:前向并行muti-view重建方法

眾所周知,DUSt3R只適合做稀疏視角重建,與sapnn3r的目的類似,這篇文章以并行的方法,擴展了DUSt3R在多視圖重建中的能力。 abstract 多視角三維重建仍然是計算機視覺領域的核心挑戰,尤其是在需要跨不同視角實現精確且可…

本地部署DeepSeek教程(Mac版本)

第一步、下載 Ollama 官網地址:Ollama 點擊 Download 下載 我這里是 macOS 環境 以 macOS 環境為主 下載完成后是一個壓縮包,雙擊解壓之后移到應用程序: 打開后會提示你到命令行中運行一下命令,附上截圖: 若遇…

deepseek本地部署會遇到哪些坑

在本地部署DeepSeek(或其他類似AI模型)時,可能會遇到以下常見問題及解決方案: 1. 硬件資源不足 問題表現: GPU不兼容(如型號過舊)、顯存不足(OOM錯誤)或CPU模式性能極低。解決方案: 確認GPU支持CUDA,檢查顯存需求(如至少16GB顯存)。使用nvidia-smi監控顯存,通過降…

微機原理與接口技術期末大作業——4位搶答器仿真

在微機原理與接口技術的學習旅程中,期末大作業成為了檢驗知識掌握程度與實踐能力的關鍵環節。本次我選擇設計并仿真一個 4 位搶答器系統,通過這個項目,深入探索 8086CPU 及其接口技術的實際應用。附完整壓縮包下載。 一、系統設計思路 &…

解決國內服務器 npm install 卡住的問題

在使用國內云服務器時,經常會遇到 npm install 命令執行卡住的情況。本文將分享一個典型案例以及常見的解決方案。 問題描述 在執行以下命令時: mkdir test-npm cd test-npm npm init -y npm install lodash --verbose安裝過程會卡在這個狀態&#xf…

【Redis】Redis 經典面試題解析:深入理解 Redis 的核心概念與應用

Redis 是一個高性能的鍵值存儲系統,廣泛應用于緩存、消息隊列、排行榜等場景。在面試中,Redis 是一個高頻話題,尤其是其核心概念、數據結構、持久化機制和高可用性方案。 1. Redis 是什么?它的主要特點是什么? 答案&a…

昆侖萬維Java開發面試題及參考答案

進程和線程的區別是什么? 進程和線程都是操作系統中非常重要的概念,它們在多個方面存在顯著的區別。 從定義上看,進程是操作系統進行資源分配和調度的基本單位。每個進程都有自己獨立的內存空間,包括代碼段、數據段、堆棧段等。例如,當你在電腦上同時打開瀏覽器和音樂播放…

Visual Studio Code應用本地部署的deepseek

1.打開Visual Studio Code,在插件中搜索continue,安裝插件。 2.添加新的大語言模型,我們選擇ollama. 3.直接點connect,會鏈接本地下載好的deepseek模型。 參看上篇文章:deepseek本地部署-CSDN博客 4.輸入需求生成可用…

DeepSeek技術深度解析:從不同技術角度的全面探討

DeepSeek技術深度解析:從不同技術角度的全面探討 引言 DeepSeek是一個集成了多種先進技術的平臺,旨在通過深度學習和其他前沿技術來解決復雜的問題。本文將從算法、架構、數據處理以及應用等不同技術角度對DeepSeek進行詳細分析。 一、算法層面 深度學…

SpringBoot 整合 Mybatis:注解版

第一章&#xff1a;注解版 導入配置&#xff1a; <groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.1</version> </dependency> 步驟&#xff1a; 配置數據源見 Druid…

[Linux]如何將腳本(shell script)轉換到系統管理服務器(systemd service)來運行?

[InfluxDB]Monitor Tem. and Volt of RaspberryPi and Send Message by Line Notify 在Linux中&#xff0c;shell腳本(shell script)常用於運行各種自動化的流程&#xff0c;包含API串接&#xff0c;設置和啟動應用服務等等&#xff0c;腳本語法也相對易學易讀&#xff0c;因此…