前言
最近在做一個知識庫問答項目,就是現在大模型浪潮下比較火的 RAG 應用。LangChain 可以說是 RAG 最受歡迎的工具,因此我首選 LangChain 來快速構建我的應用。坦白來講 LangChain 本身一套對于組件的定義已經讓我感覺很復雜,為什么采用 f-string
或 string.format
就能完成的事情必須要抽出一個這么復雜的對象。
當然上面種種原因可能是我不理解 LangChain 設計之禪,但是下面這個坑確實實實在在讓我對 LangChain 感到失望的地方。
起因
事情起因很簡單,我很快構建好了一個最簡單的 RAG 應用,無非以下三步:
- 用戶輸入
query
。 - 將用戶的
query
進行embedding
之后進行相似度檢索,并按照閾值過濾相似度低的文本。 - 整合檢索的文本并按照一定格式送入大模型。
但是在第二步出現了問題。我在測試的時候發現我總是會召回很多無關的文本,并且我把相似度閾值調高之后,仍然沒有把這些不相干的文本過濾掉,這讓我十分困惑,但是翻看 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
支持 similarity
、 mmr
和 similarity_score_threshold
三種,默認的是 similarity
。看到這里,第一個引起我疑惑的地方來了,這個 similarity
和 similarity_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
,如果之前的調用中傳入了閾值分數,則會進行相似度的過濾。因為我遇到的問題就是無法過濾無關內容,因此這里過濾引起了我的注意。
分析一下這個過濾的代碼:
- 定義比較算子,如果距離策略采用最大內積或者杰卡德系數就采用大于,否則就是小于。
- 按照算子將相似度和閾值計算來進行過濾。
這里我恍然大悟,我趕緊查看了一下我自己采用了什么距離策略,翻看源碼得知 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表示最相似。這個流程也不復雜,但是這里需要理一下流程:
- 把關鍵詞參數中
score_threshold
給彈了出來,這意味著后面傳入的關鍵詞參數中不會有score_threshold
這個參數。(這里又是一個讓人吐槽的地方,后面再說。) - 調用
_similarity_search_with_relevance_scores()
函數,(這里吐槽一下函數名里面是relevance_scores
但是接受變量確實docs_and_similarities
為什么要搞這么多復雜的名稱呢?) - 如果第一步中獲得的
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()
這步就會進行過濾,但是這個函數過濾是按照距離策略不同選不同算子,分支二過濾直接按照大于進行過濾。
到了這里在混亂的概念中有個初步的印象,可以得到如下三個觀點:
- 相似度和相關性是不同的,至少在 LangChain 中是這樣定義的,雖然在函數中兩個變量混用,但是按照行為上確實是不同的兩個定義。
- 相關性分數越大,則文本越相關;相似度則是根據距離策略決定,對于歐式距離,相似度越小,文本越相關。
- 相關性分數通過相似度計算出來的,計算函數就是
_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_type
為 similarity
時,只有內積是正確召回。
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_type
為 similarity_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 格式輸出 action
和 action_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大模型感興趣的愛好者,這套報告合集都將為您提供寶貴的信息和啟示。
三、大模型經典PDF籍
隨著人工智能技術的飛速發展,AI大模型已經成為了當今科技領域的一大熱點。這些大型預訓練模型,如GPT-3、BERT、XLNet等,以其強大的語言理解和生成能力,正在改變我們對人工智能的認識。 那以下這些PDF籍就是非常不錯的學習資源。
四、AI大模型商業化落地方案
作為普通人,入局大模型時代需要持續學習和實踐,不斷提高自己的技能和認知水平,同時也需要有責任感和倫理意識,為人工智能的健康發展貢獻力量。