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.mzph.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可谓是轻量级自容器…