LangChain真的好用嗎?談一下LangChain封裝FAISS的一些坑

前言

最近在做一個知識庫問答項目,就是現在大模型浪潮下比較火的 RAG 應用。LangChain 可以說是 RAG 最受歡迎的工具,因此我首選 LangChain 來快速構建我的應用。坦白來講 LangChain 本身一套對于組件的定義已經讓我感覺很復雜,為什么采用 f-stringstring.format 就能完成的事情必須要抽出一個這么復雜的對象。

當然上面種種原因可能是我不理解 LangChain 設計之禪,但是下面這個坑確實實實在在讓我對 LangChain 感到失望的地方。

起因

事情起因很簡單,我很快構建好了一個最簡單的 RAG 應用,無非以下三步:

  1. 用戶輸入 query
  2. 將用戶的 query 進行 embedding 之后進行相似度檢索,并按照閾值過濾相似度低的文本。
  3. 整合檢索的文本并按照一定格式送入大模型。

但是在第二步出現了問題。我在測試的時候發現我總是會召回很多無關的文本,并且我把相似度閾值調高之后,仍然沒有把這些不相干的文本過濾掉,這讓我十分困惑,但是翻看 LangChain 調用代碼之后我瞬間一個恍然大明白,這里 xxx 有坑!

回顧

LangChain 中對于文本檢索有個類叫做 BaseRetriever,剛剛開始我只使用向量數據庫進行最簡單的檢索,但是考慮后續會加入多種檢索方式,為了組合方便我采用了 VectorStoreRetriever 進行檢索。基本代碼是這樣的:

# 省略加載db的過程
retriever = db.as_retriever()
docs = retriever.get_relevant_documents(query, score_threshold=threshold)

就是這樣,我把 threshold 調高也不會過濾那些顯然無關的文本。于是我就想看看 LangChain 是怎么調用的。

排查

首先看一下 get_relevant_documents() 這個函數調用流程,它在 BaseRetriever 是這么定義的,源碼貼臉警告!!!

def get_relevant_documents(self,query: str,*,callbacks: Callbacks = None,tags: Optional[List[str]] = None,metadata: Optional[Dict[str, Any]] = None,run_name: Optional[str] = None,**kwargs: Any,
) -> List[Document]:"""Retrieve documents relevant to a query.Users should favor using `.invoke` or `.batch` rather than`get_relevant_documents directly`.Args:query: string to find relevant documents forcallbacks: Callback manager or list of callbackstags: Optional list of tags associated with the retriever. Defaults to NoneThese tags will be associated with each call to this retriever,and passed as arguments to the handlers defined in `callbacks`.metadata: Optional metadata associated with the retriever. Defaults to NoneThis metadata will be associated with each call to this retriever,and passed as arguments to the handlers defined in `callbacks`.run_name: Optional name for the run.Returns:List of relevant documents"""from langchain_core.callbacks.manager import CallbackManagercallback_manager = CallbackManager.configure(callbacks,None,verbose=kwargs.get("verbose", False),inheritable_tags=tags,local_tags=self.tags,inheritable_metadata=metadata,local_metadata=self.metadata,)run_manager = callback_manager.on_retriever_start(dumpd(self),query,name=run_name,run_id=kwargs.pop("run_id", None),)try:_kwargs = kwargs if self._expects_other_args else {}if self._new_arg_supported:result = self._get_relevant_documents(query, run_manager=run_manager, **_kwargs)else:result = self._get_relevant_documents(query, **_kwargs)except Exception as e:run_manager.on_retriever_error(e)raise eelse:run_manager.on_retriever_end(result,)return result

這個函數文檔說建議使用 .invoke() 而不是直接調用這個函數,但是 .invoke() 也是間接調用這個函數。這個函數的流程還是挺清晰的,它會處理一些 callback 然后繼續調用 _get_relevant_documents() 這個函數,這個函數由每個子類自己實現,我們看看 VectorStoreRetriever 對于這個函數的實現:

def _get_relevant_documents(self, query: str, *, run_manager: CallbackManagerForRetrieverRun
) -> List[Document]:if self.search_type == "similarity":docs = self.vectorstore.similarity_search(query, **self.search_kwargs)elif self.search_type == "similarity_score_threshold":docs_and_similarities = (self.vectorstore.similarity_search_with_relevance_scores(query, **self.search_kwargs))docs = [doc for doc, _ in docs_and_similarities]elif self.search_type == "mmr":docs = self.vectorstore.max_marginal_relevance_search(query, **self.search_kwargs)else:raise ValueError(f"search_type of {self.search_type} not allowed.")return docs

這個函數本身邏輯也不難,就是按照 search_type 的不同,調用 vectorstore 的不同方法。所以這個 VectorStoreRetriever 其實就是對 vectorstore 的再一次封裝,核心還是調用 vectorstore 的方法。

回到函數本身來,這里出現了一個新的變量叫 search_type,這個其實在 VectorStoreRetriever 中給出了:

class VectorStoreRetriever(BaseRetriever):"""Base Retriever class for VectorStore."""vectorstore: VectorStore"""VectorStore to use for retrieval."""search_type: str = "similarity""""Type of search to perform. Defaults to "similarity"."""search_kwargs: dict = Field(default_factory=dict)"""Keyword arguments to pass to the search function."""allowed_search_types: ClassVar[Collection[str]] = ("similarity","similarity_score_threshold","mmr",)

其實當我們調用 vectorstore.as_retriever() 時候也可以指定該參數,我們看看 as_retriever() 這個函數的實現。

def as_retriever(self, **kwargs: Any) -> VectorStoreRetriever:"""Return VectorStoreRetriever initialized from this VectorStore.Args:search_type (Optional[str]): Defines the type of search thatthe Retriever should perform.Can be "similarity" (default), "mmr", or"similarity_score_threshold".search_kwargs (Optional[Dict]): Keyword arguments to pass to thesearch function. Can include things like:k: Amount of documents to return (Default: 4)score_threshold: Minimum relevance thresholdfor similarity_score_thresholdfetch_k: Amount of documents to pass to MMR algorithm (Default: 20)lambda_mult: Diversity of results returned by MMR;1 for minimum diversity and 0 for maximum. (Default: 0.5)filter: Filter by document metadataReturns:VectorStoreRetriever: Retriever class for VectorStore.Examples:.. code-block:: python# Retrieve more documents with higher diversity# Useful if your dataset has many similar documentsdocsearch.as_retriever(search_type="mmr",search_kwargs={'k': 6, 'lambda_mult': 0.25})# Fetch more documents for the MMR algorithm to consider# But only return the top 5docsearch.as_retriever(search_type="mmr",search_kwargs={'k': 5, 'fetch_k': 50})# Only retrieve documents that have a relevance score# Above a certain thresholddocsearch.as_retriever(search_type="similarity_score_threshold",search_kwargs={'score_threshold': 0.8})# Only get the single most similar document from the datasetdocsearch.as_retriever(search_kwargs={'k': 1})# Use a filter to only retrieve documents from a specific paperdocsearch.as_retriever(search_kwargs={'filter': {'paper_title':'GPT-4 Technical Report'}})"""tags = kwargs.pop("tags", None) or []tags.extend(self._get_retriever_tags())return VectorStoreRetriever(vectorstore=self, **kwargs, tags=tags)

可以看到這里的 search_type 支持 similaritymmrsimilarity_score_threshold 三種,默認的是 similarity。看到這里,第一個引起我疑惑的地方來了,這個 similaritysimilarity_score_threshold 有什么區別呢?

下面我們分兩條線進行分析,按照不同調用鏈看看他們到底是什么意思。

分支一:similarity

在分支一,會調用 vetorstore.similarity_search() 方法,這是 VectorStore 的一個抽象方法,需要子類自己實現,我們看看 FAISS 是怎么實現的。

def similarity_search(self,query: str,k: int = 4,filter: Optional[Union[Callable, Dict[str, Any]]] = None,fetch_k: int = 20,**kwargs: Any,
) -> List[Document]:"""Return docs most similar to query.Args:query: Text to look up documents similar to.k: Number of Documents to return. Defaults to 4.filter: (Optional[Dict[str, str]]): Filter by metadata. Defaults to None.fetch_k: (Optional[int]) Number of Documents to fetch before filtering.Defaults to 20.Returns:List of Documents most similar to the query."""docs_and_scores = self.similarity_search_with_score(query, k, filter=filter, fetch_k=fetch_k, **kwargs)return [doc for doc, _ in docs_and_scores]

這里可以看到他是調用了 similarity_search_with_score() 方法,然后把結果中的 score 給略去了,這里不得不吐槽這個調用是不是脫褲子放屁,明明可以寫在一個方法里面,傳入一個 flag 標識是否要返回分數就可以解決,非要封裝成兩個方法。吐槽結束繼續查看 similarity_search_with_score() 方法。

def similarity_search_with_score(self,query: str,k: int = 4,filter: Optional[Union[Callable, Dict[str, Any]]] = None,fetch_k: int = 20,**kwargs: Any,
) -> List[Tuple[Document, float]]:"""Return docs most similar to query.Args:query: Text to look up documents similar to.k: Number of Documents to return. Defaults to 4.filter (Optional[Dict[str, str]]): Filter by metadata.Defaults to None. If a callable, it must take as input themetadata dict of Document and return a bool.fetch_k: (Optional[int]) Number of Documents to fetch before filtering.Defaults to 20.Returns:List of documents most similar to the query text withL2 distance in float. Lower score represents more similarity."""embedding = self._embed_query(query)docs = self.similarity_search_with_score_by_vector(embedding,k,filter=filter,fetch_k=fetch_k,**kwargs,)return docs

這個方法就是將 query 進行 embedding 之后,根據向量進行查詢,調用了 similarity_search_with_score_by_vector() 方法,我們繼續跟蹤。

def similarity_search_with_score_by_vector(self,embedding: List[float],k: int = 4,filter: Optional[Union[Callable, Dict[str, Any]]] = None,fetch_k: int = 20,**kwargs: Any,
) -> List[Tuple[Document, float]]:"""Return docs most similar to query.Args:embedding: Embedding vector to look up documents similar to.k: Number of Documents to return. Defaults to 4.filter (Optional[Union[Callable, Dict[str, Any]]]): Filter by metadata.Defaults to None. If a callable, it must take as input themetadata dict of Document and return a bool.fetch_k: (Optional[int]) Number of Documents to fetch before filtering.Defaults to 20.**kwargs: kwargs to be passed to similarity search. Can include:score_threshold: Optional, a floating point value between 0 to 1 tofilter the resulting set of retrieved docsReturns:List of documents most similar to the query text and L2 distancein float for each. Lower score represents more similarity."""faiss = dependable_faiss_import()vector = np.array([embedding], dtype=np.float32)if self._normalize_L2:faiss.normalize_L2(vector)scores, indices = self.index.search(vector, k if filter is None else fetch_k)docs = []if filter is not None:filter_func = self._create_filter_func(filter)for j, i in enumerate(indices[0]):if i == -1:# This happens when not enough docs are returned.continue_id = self.index_to_docstore_id[i]doc = self.docstore.search(_id)if not isinstance(doc, Document):raise ValueError(f"Could not find document for id {_id}, got {doc}")if filter is not None:if filter_func(doc.metadata):docs.append((doc, scores[0][j]))else:docs.append((doc, scores[0][j]))score_threshold = kwargs.get("score_threshold")if score_threshold is not None:cmp = (operator.geif self.distance_strategyin (DistanceStrategy.MAX_INNER_PRODUCT, DistanceStrategy.JACCARD)else operator.le)docs = [(doc, similarity)for doc, similarity in docsif cmp(similarity, score_threshold)]return docs[:k]

這里就是調用了 FAISS 創建數據庫時的索引進行相似度的檢索,檢索之后,會取關鍵詞參數中是否有 score_threshold,如果之前的調用中傳入了閾值分數,則會進行相似度的過濾。因為我遇到的問題就是無法過濾無關內容,因此這里過濾引起了我的注意。

分析一下這個過濾的代碼:

  1. 定義比較算子,如果距離策略采用最大內積或者杰卡德系數就采用大于,否則就是小于。
  2. 按照算子將相似度和閾值計算來進行過濾。

這里我恍然大悟,我趕緊查看了一下我自己采用了什么距離策略,翻看源碼得知 FAISS 默認采用的距離策略是 DistanceStrategy.EUCLIDEAN_DISTANCE。也就是歐式距離,所以算子應該采用小于,也就是說保留相似度低于閾值的。

這里我恍然大明白,這很好理解,如果你采用歐式距離作為相似度計算,確實應該值越小表示越相似,所以我之前調高相似度閾值反而沒有過濾是正常的,因為調的越大,反而過濾力度越小!

這就很反直覺,假如我采用內積作為距離策略,則我之前的行為就是正確的。LangChain 并沒有對這個情況進行合理的處理,甚至沒有看到 LangChain 對此有一個提示。

分支一就此結束,雖然已經解決了我最開始的問題,但是我們還是繼續看看分支二。

分支二:similarity_score_threshold

在分支二,VectorStoreRetriever 會調用 vectorstore.similarity_search_with_relevance_scores() 方法。這里多了一個概念叫 relevance_scores 我們姑且暫時叫做相關性分數,這個和之前相似度有啥關系呢,我們先不揭曉答案,先看看這個函數做了啥。

def similarity_search_with_relevance_scores(self,query: str,k: int = 4,**kwargs: Any,
) -> List[Tuple[Document, float]]:"""Return docs and relevance scores in the range [0, 1].0 is dissimilar, 1 is most similar.Args:query: input textk: Number of Documents to return. Defaults to 4.**kwargs: kwargs to be passed to similarity search. Should include:score_threshold: Optional, a floating point value between 0 to 1 tofilter the resulting set of retrieved docsReturns:List of Tuples of (doc, similarity_score)"""score_threshold = kwargs.pop("score_threshold", None)docs_and_similarities = self._similarity_search_with_relevance_scores(query, k=k, **kwargs)if any(similarity < 0.0 or similarity > 1.0for _, similarity in docs_and_similarities):warnings.warn("Relevance scores must be between"f" 0 and 1, got {docs_and_similarities}")if score_threshold is not None:docs_and_similarities = [(doc, similarity)for doc, similarity in docs_and_similaritiesif similarity >= score_threshold]if len(docs_and_similarities) == 0:warnings.warn("No relevant docs were retrieved using the relevance score"f" threshold {score_threshold}")return docs_and_similarities

這個函數文檔中寫到返回文檔和對應的相關性分數,相關性分數在0到1之間,0表示不相似,1表示最相似。這個流程也不復雜,但是這里需要理一下流程:

  1. 把關鍵詞參數中 score_threshold 給彈了出來,這意味著后面傳入的關鍵詞參數中不會有 score_threshold 這個參數。(這里又是一個讓人吐槽的地方,后面再說。)
  2. 調用 _similarity_search_with_relevance_scores() 函數,(這里吐槽一下函數名里面是 relevance_scores 但是接受變量確實 docs_and_similarities 為什么要搞這么多復雜的名稱呢?)
  3. 如果第一步中獲得的 score_threshold 不為空則進行過濾,保留相似度大于閾值的文檔,注意這里并沒有分支一最后的算子判斷。

到這里我有點懵了,因為引入了一個 relevance_scores 但是似乎和相似度概念差不多,包括在函數文檔以及函數內部都是混用的,所以我很好奇為啥要引入一個新概念。但是有一點確認的是,相關性分數越高,文本相似度越高,無論你采用了什么樣的距離策略都是這樣的。

讓我們繼續觀察調用鏈,看看第二步中的函數:

def _similarity_search_with_relevance_scores(self,query: str,k: int = 4,**kwargs: Any,
) -> List[Tuple[Document, float]]:"""Default similarity search with relevance scores. Modify if necessaryin subclass.Return docs and relevance scores in the range [0, 1].0 is dissimilar, 1 is most similar.Args:query: input textk: Number of Documents to return. Defaults to 4.**kwargs: kwargs to be passed to similarity search. Should include:score_threshold: Optional, a floating point value between 0 to 1 tofilter the resulting set of retrieved docsReturns:List of Tuples of (doc, similarity_score)"""relevance_score_fn = self._select_relevance_score_fn()docs_and_scores = self.similarity_search_with_score(query, k, **kwargs)return [(doc, relevance_score_fn(score)) for doc, score in docs_and_scores]

函數文檔再次說明返回文檔和對應的相關性分數,相關性分數在0到1之間,0表示不相似,1表示最相似。函數也很簡單,首先調用了一個相關性分數函數,然后調用 similarity_search_with_score() 得到文檔和相似度,最后將相似度按照相關性分數函數做一個轉換,至此兩個分支走到了一起,最終都是調用 similarity_search_with_score()

這里就可以回答為什么之前要 pop 關鍵詞參數中的閾值,因為如果關鍵詞參數中有 score_threshold,那么在 similarity_search_with_score() 這步就會進行過濾,但是這個函數過濾是按照距離策略不同選不同算子,分支二過濾直接按照大于進行過濾。

到了這里在混亂的概念中有個初步的印象,可以得到如下三個觀點:

  1. 相似度和相關性是不同的,至少在 LangChain 中是這樣定義的,雖然在函數中兩個變量混用,但是按照行為上確實是不同的兩個定義。
  2. 相關性分數越大,則文本越相關;相似度則是根據距離策略決定,對于歐式距離,相似度越小,文本越相關。
  3. 相關性分數通過相似度計算出來的,計算函數就是 _select_relevance_score_fn()

我感覺到了勝利的曙光,只要查明這個 _select_relevance_score_fn() 具體做了啥,就知道這兩個定義如何關聯的了。

def _select_relevance_score_fn(self) -> Callable[[float], float]:"""The 'correct' relevance functionmay differ depending on a few things, including:- the distance / similarity metric used by the VectorStore- the scale of your embeddings (OpenAI's are unit normed. Many others are not!)- embedding dimensionality- etc.Vectorstores should define their own selection based method of relevance."""raise NotImplementedError

這里可以看到不同的 vectorstore 實現是不同的,這里我當然討論的是 FAISS,我們看 LangChain 在 FAISS 中如何定義的。

def _select_relevance_score_fn(self) -> Callable[[float], float]:"""The 'correct' relevance functionmay differ depending on a few things, including:- the distance / similarity metric used by the VectorStore- the scale of your embeddings (OpenAI's are unit normed. Many others are not!)- embedding dimensionality- etc."""if self.override_relevance_score_fn is not None:return self.override_relevance_score_fn# Default strategy is to rely on distance strategy provided in# vectorstore constructorif self.distance_strategy == DistanceStrategy.MAX_INNER_PRODUCT:return self._max_inner_product_relevance_score_fnelif self.distance_strategy == DistanceStrategy.EUCLIDEAN_DISTANCE:# Default behavior is to use euclidean distance relevancyreturn self._euclidean_relevance_score_fnelif self.distance_strategy == DistanceStrategy.COSINE:return self._cosine_relevance_score_fnelse:raise ValueError("Unknown distance strategy, must be cosine, max_inner_product,"" or euclidean")

這里面可以看到 LangChain 對 FAISS 支持三種距離策略,每個策略有不同的計算公式,這里我直接貼出三個計算公式:

@staticmethod
def _max_inner_product_relevance_score_fn(distance: float) -> float:"""Normalize the distance to a score on a scale [0, 1]."""if distance > 0:return 1.0 - distancereturn -1.0 * distance@staticmethod
def _euclidean_relevance_score_fn(distance: float) -> float:"""Return a similarity score on a scale [0, 1]."""# The 'correct' relevance function# may differ depending on a few things, including:# - the distance / similarity metric used by the VectorStore# - the scale of your embeddings (OpenAI's are unit normed. Many#  others are not!)# - embedding dimensionality# - etc.# This function converts the euclidean norm of normalized embeddings# (0 is most similar, sqrt(2) most dissimilar)# to a similarity function (0 to 1)return 1.0 - distance / math.sqrt(2)@staticmethod
def _cosine_relevance_score_fn(distance: float) -> float:"""Normalize the distance to a score on a scale [0, 1]."""return 1.0 - distance

這里我們都考慮 embedding 向量經過 L2 正則化,則內積和余弦相似度計算應該相同,實際上在內積上有存在問題。

首先內積為負值,直接取其相反數沒有問題,因為負相關也是相關,但是當為正值時就有問題了,舉個例子,假如采用內積計算,得到一個相似度為 0.7 的值,理應這兩個比較相關,但是通過這個相關性函數得到只有 0.3 反而變成不相關了。這三個公式只有歐式距離是正確的。

實驗

上面說明 LangChain 對于不同距離策略,沒能給出正確的過濾方式,且對于相關性的計算,搞反了語義相似性和相關性的關系。

對于 VectorStore 而言,如果采用歐氏距離,采用 similarity_search_with_relevance_scores() 才能正確按照相似度過濾文檔,相應的 VectorStoreRetriever 中的 search_type 應該采用 similarity_score_threshold

如果采用最大內積,采用 similarity_search_with_score() 才能正確檢索文檔,相應的 VectorStoreRetriever 中的 search_type 應該采用 similarity

除此之外的組合都不能按照預期的檢索出文檔。

為了證明我的猜想,下面進行實驗環節。

版本信息

我采用的 LangChain 版本如下:

pip show langchainName: langchain
Version: 0.1.16
Summary: Building applications with LLMs through composability
Home-page: https://github.com/langchain-ai/langchain
Author: 
Author-email: 
License: MIT
Location: D:\miniconda3\envs\new\Lib\site-packages
Requires: aiohttp, dataclasses-json, jsonpatch, langchain-community, langchain-core, langchain-text-splitters, langsmith, numpy, pydantic, PyYAML, requests, SQLAlchemy, tenacity
Required-by: 

實驗過程

導包環節

import numpy as np
from langchain_community.vectorstores.faiss import FAISS, DistanceStrategy
from langchain_openai import OpenAIEmbeddings

我將下面三句毫不相關的話為文檔,建立三個不同距離策略的向量庫。

text_list = ["今天天氣真好", "我喜歡吃蘋果", "猴子排序很不可靠"]
embeddings = OpenAIEmbeddings(openai_api_base="xxx",openai_api_key="xxx"
)
embedding_list = [embeddings.embed_query(text) for text in text_list]

OpenAIEmbeddings 會將向量進行 L2 正則化。

for embedding in embedding_list:print(np.linalg.norm(embedding))0.9999999999999989
1.0000000000000002
1.0000000000000002

建立下面三個向量庫:

vs1 = FAISS.from_embeddings(zip(text_list, embedding_list), embeddings, normalize_L2=True, distance_strategy=DistanceStrategy.EUCLIDEAN_DISTANCE)
vs2 = FAISS.from_embeddings(zip(text_list, embedding_list), embeddings, normalize_L2=True, distance_strategy=DistanceStrategy.MAX_INNER_PRODUCT)
vs3 = FAISS.from_embeddings(zip(text_list, embedding_list), embeddings, normalize_L2=True, distance_strategy=DistanceStrategy.COSINE)

我們先都檢索一下,確保三個向量庫中內容都存在。

print(vs1.similarity_search_with_score("今天天氣真好"))
print(vs2.similarity_search_with_score("今天天氣真好"))
print(vs3.similarity_search_with_score("今天天氣真好"))[(Document(page_content='今天天氣真好'), 0.0), (Document(page_content='我喜歡吃蘋果'), 0.40074897), (Document(page_content='猴子排序很不可靠'), 0.5013859)]
[(Document(page_content='今天天氣真好'), 0.9999843), (Document(page_content='我喜歡吃蘋果'), 0.7995081), (Document(page_content='猴子排序很不可靠'), 0.74908566)] 
[(Document(page_content='今天天氣真好'), 0.0), (Document(page_content='我喜歡吃蘋果'), 0.40074897), (Document(page_content='猴子排序很不可靠'), 0.5013859)]

這里可以看到采用余弦相似度作為距離策略的向量庫,檢索分數和歐氏距離相同,這里我認為是 FAISS 支持的是歐氏距離內積,雖然正則化后內積余弦相似度等價,但是建立索引時候 FAISS 并不支持余弦相似度,于是按照歐氏距離建立的索引。一個猜測,沒有證實。

按照上面的猜想,在 VectorStore 中,如果采用 similarity_search_with_score() 給出分數閾值,只有采用內積的能正確過濾文檔。

print(vs1.similarity_search_with_score("今天天氣真好", score_threshold=0.8))
print(vs2.similarity_search_with_score("今天天氣真好", score_threshold=0.8))
print(vs3.similarity_search_with_score("今天天氣真好", score_threshold=0.8))[(Document(page_content='今天天氣真好'), 0.0), (Document(page_content='我喜歡吃蘋果'), 0.40074897), (Document(page_content='猴子排序很不可靠'), 0.5011895)]
[(Document(page_content='今天天氣真好'), 0.9999846)]
[(Document(page_content='今天天氣真好'), 0.0), (Document(page_content='我喜歡吃蘋果'), 0.40074897), (Document(page_content='猴子排序很不可靠'), 0.5011895)]

事實果真如此,如果采用 similarity_search_with_relevance_scores() 給出閾值分數,只有采用歐氏距離能正確過濾文檔。

print(vs1.similarity_search_with_relevance_scores("今天天氣真好", score_threshold=0.8))
print(vs2.similarity_search_with_relevance_scores("今天天氣真好", score_threshold=0.8))
print(vs3.similarity_search_with_relevance_scores("今天天氣真好", score_threshold=0.8))[(Document(page_content='今天天氣真好'), 0.999978158576509)]
d:\miniconda3\envs\new\Lib\site-packages\langchain_core\vectorstores.py:342](): UserWarning: No relevant docs were retrieved using the relevance score threshold 0.8 warnings.warn(
[]
[(Document(page_content='今天天氣真好'), 1.0)]

結果也是如此,你可能會疑問余弦相似度也能正確輸出,這是因為首先在距離計算時,它采用了歐氏距離,然后相關性分數時采用余弦相似度也是錯的,兩次錯誤導致語義和相關性的關系是對的。但是好的程序不能靠 BUG 過活!

VectorStore 層面,證明了我的結論的正確性,那按照調用鏈來說 VectorStoreRetriever 也滿足我的結論,但是還是繼續實驗。

search_typesimilarity 時,只有內積是正確召回。

search_type = "similarity"
search_kwargs = {"score_threshold": 0.8
}re1 = vs1.as_retriever(search_type=search_type, search_kwargs=search_kwargs)
re2 = vs2.as_retriever(search_type=search_type, search_kwargs=search_kwargs)
re3 = vs3.as_retriever(search_type=search_type, search_kwargs=search_kwargs)print(re1.get_relevant_documents("今天天氣真好"))
print(re2.get_relevant_documents("今天天氣真好"))
print(re3.get_relevant_documents("今天天氣真好"))[Document(page_content='今天天氣真好'), Document(page_content='我喜歡吃蘋果'), Document(page_content='猴子排序很不可靠')] 
[Document(page_content='今天天氣真好')] 
[Document(page_content='今天天氣真好'), Document(page_content='我喜歡吃蘋果'), Document(page_content='猴子排序很不可靠')]

search_typesimilarity_score_threshold 時,只有歐氏距離是正確召回。

search_type = "similarity_score_threshold"
search_kwargs = {"score_threshold": 0.8
}re1 = vs1.as_retriever(search_type=search_type, search_kwargs=search_kwargs)
re2 = vs2.as_retriever(search_type=search_type, search_kwargs=search_kwargs)
re3 = vs3.as_retriever(search_type=search_type, search_kwargs=search_kwargs)print(re1.get_relevant_documents("今天天氣真好"))
print(re2.get_relevant_documents("今天天氣真好"))
print(re3.get_relevant_documents("今天天氣真好"))[Document(page_content='今天天氣真好')]
d:\miniconda3\envs\zhiguo\lib\site-packages\langchain_core\vectorstores.py:323](): UserWarning: No relevant docs were retrieved using the relevance score threshold 0.8 warnings.warn(
[]
[Document(page_content='今天天氣真好')]

這里余弦相似度正確召回原因同上,靠 BUG 過活罷了。

實驗最后再重申一下我的結論:

對于 VectorStore 而言,如果采用歐氏距離,采用 similarity_search_with_relevance_scores() 才能正確按照相似度過濾文檔,相應的 VectorStoreRetriever 中的 search_type 應該采用 similarity_score_threshold

如果采用最大內積,采用 similarity_search_with_score() 才能正確檢索文檔,相應的 VectorStoreRetriever 中的 search_type 應該采用 similarity

注:當前實驗只對 LangChain 封裝的 FAISS 負責,別的向量庫不負責。

后記

這次一個問題的溯源讓我覺得那些流行的開源庫也不是高高在上,里面也會存在很多問題:有的明明能靠一個標記變量區別,但是非要重新封裝函數、引入過多概念,導致代碼混亂等等。

后面使用 LangChain 構造 Agent 時,發現它似乎是讓模型按照一定的 JSON 格式輸出 actionaction_input 然后解析這個 JSON 格式進行下一步操作,如果模型不是嚴格按照這個 JSON 格式輸出(例如多輸出一些文本)就會出現解析錯誤的問題,并且這種方式似乎沒有利用模型本身的 function call 能力。這個還沒有仔細查看,歡迎大家指正。

在這段時間使用 LangChain 的過程中,我感覺它只有文本分割集成向量檢索這兩部分比較實用,現在發現檢索也存在問題。他的復雜設計讓我感覺不如自己編寫一套可復用的庫來實現自己的需求,也或許是我沒有真正理解到 LangChain 設計之禪吧。

那么,我們該如何學習大模型?

作為一名熱心腸的互聯網老兵,我決定把寶貴的AI知識分享給大家。 至于能學習到多少就看你的學習毅力和能力了 。我已將重要的AI大模型資料包括AI大模型入門學習思維導圖、精品AI大模型學習書籍手冊、視頻教程、實戰學習等錄播視頻免費分享出來。

一、大模型全套的學習路線

學習大型人工智能模型,如GPT-3、BERT或任何其他先進的神經網絡模型,需要系統的方法和持續的努力。既然要系統的學習大模型,那么學習路線是必不可少的,下面的這份路線能幫助你快速梳理知識,形成自己的體系。

L1級別:AI大模型時代的華麗登場

L2級別:AI大模型API應用開發工程

L3級別:大模型應用架構進階實踐

L4級別:大模型微調與私有化部署

一般掌握到第四個級別,市場上大多數崗位都是可以勝任,但要還不是天花板,天花板級別要求更加嚴格,對于算法和實戰是非常苛刻的。建議普通人掌握到L4級別即可。

以上的AI大模型學習路線,不知道為什么發出來就有點糊,高清版可以微信掃描下方CSDN官方認證二維碼免費領取【保證100%免費

二、640套AI大模型報告合集

這套包含640份報告的合集,涵蓋了AI大模型的理論研究、技術實現、行業應用等多個方面。無論您是科研人員、工程師,還是對AI大模型感興趣的愛好者,這套報告合集都將為您提供寶貴的信息和啟示。

img

三、大模型經典PDF籍

隨著人工智能技術的飛速發展,AI大模型已經成為了當今科技領域的一大熱點。這些大型預訓練模型,如GPT-3、BERT、XLNet等,以其強大的語言理解和生成能力,正在改變我們對人工智能的認識。 那以下這些PDF籍就是非常不錯的學習資源。

img

四、AI大模型商業化落地方案

img

作為普通人,入局大模型時代需要持續學習和實踐,不斷提高自己的技能和認知水平,同時也需要有責任感和倫理意識,為人工智能的健康發展貢獻力量。

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

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

相關文章

Java Web學習筆記6——盒子模型

視頻標簽&#xff1a;<video> src: 規定視頻的URL controls&#xff1a;顯示播放控件 width&#xff1a;播放器的寬度 height&#xff1a;播放器的高度 音頻標簽&#xff1a;<audio> src: 規定音頻的URL controls: 顯示播放控件 段落標簽&#xff1a;<p&g…

npm yarn 更換國內源以及node歷史版本下載地址

npm 更換國內源 npm config set registryhttps://registry.npmmirror.com npm config set electron_mirrorhttps://registry.npmmirror.com/electron/yarn 更換國內源 yarn config set registry https://registry.npmmirror.comnode歷史版本下載地址 https://nodejs.org/dow…

Git版本控制:核心概念、操作與實踐

Git是一種分布式版本控制系統&#xff0c;被廣泛應用于軟件開發過程中。本文將介紹Git的核心概念、常用操作以及最佳實踐&#xff0c;幫助讀者掌握Git的基本技巧&#xff0c;提高團隊協作效率。 一、引言 在軟件開發過程中&#xff0c;版本控制是至關重要的。它能幫助我們跟蹤…

Flutter InAppWebView Unknown feature SUPPRESS_ERROR_PAGE

在使用InAppWebView的initialData加載html代碼的時候,出現java.lang.RuntimeException: Unknown feature SUPPRESS_ERROR_PAGE的出錯誤 E/MethodChannel#flutter/platform_views(16853): Failed to handle method call E/MethodChannel#flutter/platform_views(16853):<

linux驅動學習(七)之混雜設備

需要板子一起學習的可以這里購買&#xff08;含資料&#xff09;&#xff1a;點擊跳轉 一、混雜設備 混雜設備也叫雜項設備&#xff0c;是對普通的字符設備(struct cdev)的一種封裝,設計目的就是為了簡化字符設備驅動設計的流程。具有以下特點&#xff1a; 1) 主設備號為10&a…

全面解析:滲壓計數據如何預測地下水趨勢

隨著人們對水資源日益增長的需求和對環境保護意識的提升&#xff0c;地下水位的監測和預測成為了水利工程和環境科學領域的重要研究內容。滲壓計作為一種能夠測量土壤或巖石中孔隙水壓力的儀器&#xff0c;在地下水位的監測中發揮著關鍵作用。本文將從滲壓計的工作原理、安裝方…

初探富文本之基于虛擬滾動的大型文檔性能優化方案

初探富文本之基于虛擬滾動的大型文檔性能優化方案 虛擬滾動是一種優化長列表性能的技術&#xff0c;其通過按需渲染列表項來提高瀏覽器運行效率。具體來說&#xff0c;虛擬滾動只渲染用戶瀏覽器視口部分的文檔數據&#xff0c;而不是整個文檔結構&#xff0c;其核心實現根據可…

GD32F4XX的ISP方式下載程序時的串口選擇

官方資料 詳細信息可參考GD32F4xx的用戶手冊&#xff0c;第 1.4 章節 引導配置 。 版本是 &#xff1a;GD32F4xx_User_Manual_Rev3.0_CN 資料鏈接: https://www.gd32mcu.com/cn/download/6?kwGD32F4

HTML到PDF轉換,11K Star 的pdfmake.js輕松應對

在Web開發中&#xff0c;將HTML頁面轉換為PDF文件是一項常見的需求。無論是生成報告、發票、還是其他任何需要打印或以PDF格式分發的文檔&#xff0c;開發者都需要一個既簡單又可靠的解決方案。幸運的是&#xff0c;pdfmake.js庫以其輕量級、高性能和易用性&#xff0c;成為了許…

Mysql sql語句字段截取前幾位,后幾位等

MySQL 字符串截取函數詳解 在MySQL中&#xff0c;處理字符串數據時&#xff0c;我們經常需要對字符串進行截取操作。MySQL提供了多種字符串截取函數&#xff0c;用以滿足不同的需求。本文將詳細介紹這些字符串截取函數&#xff0c;包括LEFT(), RIGHT(), SUBSTRING(), SUBSTRIN…

16 - 平均售價(高頻 SQL 50 題基礎版)

16 - 平均售價 # 注意&#xff1a;between 小值 and 大值 select u.product_id, round(sum(u.units*p.price)/sum(u.units),2) average_price from Prices p left join UnitsSold u on p.product_idu.product_id -- and where u.purchase_date between p.start_date and p.e…

AB測試學習(附有相關代碼)

目錄 一、基本概念1. 定義2. 作用3. 原理 二、實驗基本原則三、實驗步驟四、實驗步驟詳解1. 確定實驗目的2. 確定實驗變量3. 實驗指標設計3.1 實驗指標類型&#xff08;按作用區分&#xff09;3.1.1 核心指標3.1.2 驅動指標&#xff08;跟蹤指標&#xff09;3.1.3 護欄指標 3.2…

使用node將頁面轉為pdf?(puppeteer實現)

本文章適合win系統下實驗&#xff08;linux&#xff0c;mac可能會出現些莫名其妙的bug我也不會解決&#xff09; 具體過程 首先了解什么時無頭瀏覽器啟動無頭瀏覽器打開指定的url頁面設置導出pdf格式開始轉化完整基礎代碼 首先了解什么時無頭瀏覽器 沒有界面的瀏覽器下載pupp…

matlab使用教程(92)—流線圖、流帶圖和流管圖

1.使用向量數據顯示流線圖 MATLAB 向量數據集 wind 代表北美地區的氣流。本示例結合使用了幾種方法&#xff1a; 利用流線跟蹤風速 利用切片平面顯示數據的橫截面視圖 利用切片平面上的等高線提高切片平面著色的可見性 1.1確定坐標的范圍 加載數據并確定用來定位切片平面…

module ‘sys‘ has no attribute ‘setdefaultencoding‘

解釋&#xff1a; 在Python 3.3之后&#xff0c;sys模塊中不再提供setdefaultencoding()函數。這是因為Python 3.3開始&#xff0c;默認編碼行為被明確定義為UTF-8&#xff0c;并且不再需要手動設置默認編碼。 如果你的代碼中出現了這個錯誤&#xff0c;很可能是因為你正在嘗…

探索Linux中的zgrep命令:強大的文本搜索工具

探索Linux中的zgrep命令&#xff1a;強大的文本搜索工具 在Linux系統中&#xff0c;文本搜索和處理是一項日常任務。當我們需要在一個或多個文件中查找特定的字符串或模式時&#xff0c;通常會使用諸如grep這樣的工具。然而&#xff0c;當涉及到壓縮文件&#xff08;如gzip壓縮…

SpringBoot發郵件服務如何配置?怎么使用?

SpringBoot發郵件需要的參數&#xff1f;郵件發送性能如何優化&#xff1f; 在SpringBoot項目中配置發郵件服務是一個常見的需求&#xff0c;它允許我們通過應用程序發送通知、驗證郵件或其他類型的郵件。AokSend將詳細介紹如何在SpringBoot中配置發郵件服務。 SpringBoot發郵…

element-ui表格跨頁選擇數據

element-ui表格跨頁選擇 1.template部分2.js部分3.全部代碼 1.template部分 為table組件添加ref‘table’綁定數據源 :data‘list’添加select和select-all事件&#xff08;事件處理函數為handleSelect&#xff09; <template><div><el-table reftable :data&…

qmt量化交易策略小白學習筆記第17期【qmt編程之獲取對應周期的北向南向數據--方式1:內置python】

qmt編程之獲取對應周期的北向南向數據 qmt更加詳細的教程方法&#xff0c;會持續慢慢梳理。 也可找尋博主的歷史文章&#xff0c;搜索關鍵詞查看解決方案 &#xff01; 感謝關注&#xff0c;咨詢免費開通量化回測與獲取實盤權限&#xff0c;歡迎和博主聯系&#xff01; 獲取…

java+SimpleRegression 線性模型,針對采集到的大數據設備溫度,對設備溫度做出預測

首先,讓我們通過以下表格展示預測模型開發 Java 的整體流程: 步驟 描述 1 數據收集與清洗 2 特征工程處理 3 模型選擇與訓練 4 模型評估與調優 5 模型應用與部署 然后引入java的類庫 org.apache.commons.math3 math使用原則 math3可謂是輕量級自容器…