[RAG在LangChain中的实现-03]利用向量存储实现基于自然语言的相似度检索

LangChain4j实现RAG知识库原理 首先我们需要把最新的数据或者专业的数据存储到文档中,接下来借助于文本分割器把一个大的文档分割成一个一个小的文本片段,然后这些小的文本片段要使用一种专门的大模型:向量模型,之前我们介绍大模型的时候有讲过,不同的大模型擅长的领域不一样,有擅长文本处理的、有擅长图片处理的,其中就有一种大模型擅长文本向量化。由于大模型训练完毕后,随着时间的推移产生的新数据大模型是无法感知的,而且训练大模型的时候一般使用的都是通用的训练数据,有关专业领域的知识,大模型也是不知道的。一旦外挂了知识库后,整个工作流程会发生一些变化。 阅读详情

被分割的文本切片需要转换成向量保存在VertorStore中,才能实现基于自然语言的相似度检索。VectorStore是RAG架构的核心组件。它的主要作用是存储和检索非结构化数据的语义特征。简单来说,它就像一个具备模糊匹配能力的智能仓库。说到向量化,就不得不提一个名为嵌入的概念。

1. 嵌入(Embedding)

之前看到一个问题:“是什么成就了大语言模型?”,我想很多人的答案是深度神经网络。如果答案只有一个的话,我觉得嵌入居功至伟,如果没有嵌入将数据转换成可处理的向量,一切都是空谈。虽然现代的LLM都是通过深度神经网络实现的,但我认为肯定还存在其他的解决方案,但是所有的方案都绕不开基于向量的处理方式。

只要给定一个维度充分且定义合理的语义空间,原则上任何的事物都可以表示成这个空间中的一个点,这个点在语义空间中的位置可以表示成一个向量,该向量可以用来描述这个事物。比如我们描述一张纸,可以从纸张的形状、颜色、厚度、材质、纹理等维度来构建这个N维语义空间,那么任何一张纸都可以转换成基于此空间的一个N维向量。

LLM中的嵌入也是基于这个思想,当处理的文本被分词器转换成相应的词元后,需要将其转换成基于某个语义空间的向量。但是我们没有能力也没有必要确定这个语义空间每个维度具体表示什么,因为我们不关心某个词源对应向量的每个维度的表示,我们只关心两个向量在语义空间中的距离能否反映对应词元的相似度。假设“猫”、“狗”和“菠菜”这三个词元对应的向量分别为下A、B和C,A与B之间的距离必须小于与C的距离。在线性代数中,向量之间的具体可以采用不同的范数来表示,我们最熟悉的欧几里得范数。

对于我们日常获的各种有意义的文本片段,原则上两个词元在同一文本片段一起出现的频率基本能够体现出它们的相似度。那么问题就好解决了,我们只需要确定语义空间的维度数量,然后构建一个用于确定词元对应向量的模型,采用机器学习的方式使用现成的语料(比如爬取的网页内容、出版的图书文本等)训练此模型,训练的目标(损失函数)只有一个:让相似词元映射在语义空间中的点尽量靠近,反之尽量远离。

整个过程相当于通过将词元转换成对应的向量,将其嵌在语义空间的某个位置,这就是嵌入(Embedding)这一慨念的由来。我们称这个对词元实施向量化的过程为嵌入,将这个语义空间成为“嵌入空间”,将词元对应的向量称为嵌入向量,有时直接称为嵌入。

1.1 Embeddings

嵌入在LangChain中通过如下这个名为Embeddings的抽象类表示,确切地说,Embeddings代表的是实现文本向量化的嵌入模型。它定义了embed_documents和embed_query两个抽象方法,在将指定的一个或者多个文本存储在VectorStore之前,我们一般调用前者将其转换成一组通过list[float]表示的嵌入向量(列表的长度就是嵌入空间的维度数量),后者则在对VectorStore实施检索之前将查询文本转换成嵌入向量。

class Embeddings(ABC):
    @abstractmethod
    def embed_documents(self, texts: list[str]) -> list[list[float]]
    @abstractmethod
    def embed_query(self, text: str) -> list[float]

    async def aembed_documents(self, texts: list[str]) -> list[list[float]]
    async def aembed_query(self, text: str) -> list[float]

embed_documents方法所谓的Document更多地是指存储在VectorStore中的记录。除了这两个同步方法,Embeddings还定义了异步方法aembed_documents和aembed_query,它们会默认调用同步方法,大部分子类都会对它们进行重写。

1.2 OpenAIEmbeddings

OpenAIEmbeddings是调用OpenAI官方Embedding API的核心集成类。它继承自Embeddings,自身很多的字段成员都用于调用OpenAI API的客户端(OpenAI)配置,所以与ChatOpenAI的字段成员很相似,我在“OpenAI Chat模型的设计与实现”中对ChatOpenAI的所有字段成员有详细介绍。

class OpenAIEmbeddings(BaseModel, Embeddings):
    client: Any = Field(default=None, exclude=True)
    async_client: Any = Field(default=None, exclude=True)
    model: str = "text-embedding-ada-002"
    dimensions: int | None = None
    deployment: str | None = model

    openai_api_version: str | None = Field(
        default_factory=from_env("OPENAI_API_VERSION", default=None),
        alias="api_version",
    )
    openai_api_base: str | None = Field(
        alias="base_url", default_factory=from_env("OPENAI_API_BASE", default=None)
    )
    openai_api_type: str | None = Field(
        default_factory=from_env("OPENAI_API_TYPE", default=None)
    )
    openai_proxy: str | None = Field(
        default_factory=from_env("OPENAI_PROXY", default=None)
    )

    embedding_ctx_length: int = 8191
    openai_api_key: (
        SecretStr | None | Callable[[], str] | Callable[[], Awaitable[str]]
    ) = Field(
        alias="api_key", default_factory=secret_from_env("OPENAI_API_KEY", default=None)
    )
    openai_organization: str | None = Field(
        alias="organization",
        default_factory=from_env(
            ["OPENAI_ORG_ID", "OPENAI_ORGANIZATION"], default=None
        ),
    )
    allowed_special: Literal["all"] | set[str] | None = None
    disallowed_special: Literal["all"] | set[str] | Sequence[str] | None = None

    chunk_size: int = 1000
    max_retries: int = 2
    request_timeout: float | tuple[float, float] | Any | None = Field(
        default=None, alias="timeout"
    )
    headers: Any = None

    tiktoken_enabled: bool = True
    tiktoken_model_name: str | None = None
    show_progress_bar: bool = False
    model_kwargs: dict[str, Any] = Field(default_factory=dict)
    skip_empty: bool = False
    default_headers: Mapping[str, str] | None = None
    default_query: Mapping[str, object] | None = None

    retry_min_seconds: int = 4
    retry_max_seconds: int = 20
    http_client: Any | None = None
    http_async_client: Any | None = None
    check_embedding_ctx_length: bool = True

    def embed_documents(
        self, texts: list[str], chunk_size: int | None = None, **kwargs: Any
    ) -> list[list[float]]

    async def aembed_documents(
        self, texts: list[str], chunk_size: int | None = None, **kwargs: Any
    ) -> list[list[float]]
    def embed_query(self, text: str, **kwargs: Any) -> list[float]
    async def aembed_query(self, text: str, **kwargs: Any) -> list[float]

从上面的定义可以看出,OpenAIEmbeddings默认使用的模型是text-embedding-ada-002。目前主流使用的是OpenAI的第三代(v3)模型,它们支持动态维度调整,在性能和成本之间提供了更好的平衡。下面的表格提供了三种嵌入模型之间的对比:

特性text-embedding-3-smalltext-embedding-3-largetext-embedding-ada-002
默认维度1,5363,0721,536
可调维度可缩减至 512可缩减至 256 或 1024不支持
最大输入8,191 Tokens8,191 Tokens8,191 Tokens
特点高性价比,适合通用搜索精度最高,多语言性能极强兼容性广,已逐渐被v3替代

下面的演示程序针对模型text-embedding-3-small创建了两个OpenAIEmbeddings,第一个采用默认的维度(1536),第二个采用指定的维度(512)。我们利用它对指定的文本实施嵌入操作,并验证生成嵌入向量的维度。

from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings
load_dotenv()

text = "Sed ut perspiciatis unde omnis iste.Natus error sit voluptatem accusantium doloremque laudantium."
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vector = embeddings.embed_query(text)
assert len(vector) == 1536

embeddings = OpenAIEmbeddings(model="text-embedding-3-small",dimensions = 512)
vector = embeddings.embed_query(text)
assert len(vector) == 512

2. VectorStore

在RAG的解决流程中,被切割后的文本会通过Embeddings转换成嵌入向量,连同关联的元数据存储到VectorStore中。由于Document是文本和元数据的复合体,所以VectorStore的很多操作都是金基于Document进行的。这也是为什么上面介绍的很多分割器类型直接将Document对象作为分割后的产物,有的还提供方法将指定的文本/对象列表和元数据映射表进行合并生成一组Document。由于需要对文本进行向量化,所以作为向量存储基类的VectorStore必须拥有一个Embeddings对象,它对应着该抽象类的embeddings特性。

class VectorStore(ABC):
    @property
    def embeddings(self) -> Embeddings | None

2.1 向量记录的添加与删除

VectorStore提供了一系列方法实现针对向量存储的读写。具体的写操作体现在如下所述的三组方法,它们分布实现对存储记录的添加和删除。存储的记录可以通过get_by_ids方法提取出来,方法返回的是Document的列表。

class VectorStore(ABC):
    def add_texts(
        self,
        texts: Iterable[str],
        metadatas: list[dict] | None = None,
        *,
        ids: list[str] | None = None,
        **kwargs: Any,
    ) -> list[str]
    async def aadd_texts(
        self,
        texts: Iterable[str],
        metadatas: list[dict] | None = None,
        *,
        ids: list[str] | None = None,
        **kwargs: Any,
    ) -> list[str]    
    
    def add_documents(self, documents: list[Document], **kwargs: Any) -> list[str]
    async def aadd_documents(self, documents: list[Document], **kwargs: Any) -> list[str]

    def delete(self, ids: list[str] | None = None, **kwargs: Any) -> bool | None
    async def adelete(self, ids: list[str] | None = None, **kwargs: Any) -> bool | None

    def get_by_ids(self, ids: Sequence[str], /) -> list[Document]

三组方法说明如下:

  • add_texts/aadd_texts:需要以可迭代对象和列表形式提供文本和元数据,如果没有通过ids参数提供输入记录的标识(相当于数据记录的主键),方法会自动创标识。方法返回添加记录的ID列表;
  • add_documents/aadd_documents:它以Document的形式提供添加记录的文本和元数据;
  • delete/adelete:删除指定ID列表对应的记录,方法返回是否成功的布尔值。在生产环境下,如果原始文档更新了,我们需要先根据ID删除旧的分块,再重新插入新的,否则会导致检索到过期信息。
class VectorStore(ABC):
    @classmethod
    def from_documents(
        cls,
        documents: list[Document],
        embedding: Embeddings,
        **kwargs: Any,
    ) -> Self

    @classmethod
    async def afrom_documents(
        cls,
        documents: list[Document],
        embedding: Embeddings,
        **kwargs: Any,
    ) -> Self

    @classmethod
    @abstractmethod
    def from_texts(
        cls: type[VST],
        texts: list[str],
        embedding: Embeddings,
        metadatas: list[dict] | None = None,
        *,
        ids: list[str] | None = None,
        **kwargs: Any,
    ) -> VST

    @classmethod
    async def afrom_texts(
        cls,
        texts: list[str],
        embedding: Embeddings,
        metadatas: list[dict] | None = None,
        *,
        ids: list[str] | None = None,
        **kwargs: Any,
    ) -> Self

上面定义的这两组类方法(from_documents/afrom_documents和from_texts/afrom_texts)同时完成了针对VectorStore的创建和数据的添加,其中from_texts是具体向量存储类型必须实现的抽象方法。

2.2 普通相似度检索

将关联元数据的切片存入VectorStore不是我们的目的,最终目的是能够根据用户指定的基于自然语言的查询文本实施检索,返回相关的记录的元数据。传统关系型和NonSQL提供的文本查询只能完成基于是否包含指定字符检索,VectorStore提供的是基于两个向量的相似度的检索。如果两个对象能够使用同一个描述空间的向量来表示,它们之间的相似度可以利用向量之间的距离来表示。采用不同的范数,向量距离具有不同的计算方式,我们VectorStore通常使用如下两种计算方式:

  • 欧氏距离:嵌入空间两个点的物理距离。距离越短,相似度越高;
  • 余弦距离:不看物理距离,只看向量的方向。如果将两个向量的夹角表示成θ,可以使用cosine(θ)的表达式来表示距离。由于cosine是θ的减函数,本着相似度与距离成反向关系的原则,我们将(1 - cosine(θ))作为余弦距离。cosine(θ)表示余弦相似度;

在大规模向量检索中,余弦相似度比欧氏距离更受欢迎,主要有以下几个核心原因。

  • 关注语义而非长度:在Embedding 模型中,向量的方向代表了语义特征,而向量的长度往往受文本长度或高频词影响,难以真正反映语义相关性。余弦相似度只看夹角,即使两段话长度不同,只要意思接近,它们的余弦值就会很高;
  • 归一化特性:余弦相似度的结果始终在[-1, 1]之间(在Embedding中通常是[0, 1])。这种标准化的范围让开发者更容易设定阈值来过滤低相关性的结果。相比之下,欧氏距离的数值范围是[0, ∞),很难直观判断多远才算不相关;
  • 计算效率高:当向量经过归一化处理后,余弦相似度的计算可以简化为两个向量的点积。点积在现代CPU和GPU上都有极高的硬件加速效率,非常适合处理千万级以上的数据检索;
  • 高维空间的鲁棒性:在高维空间中,点与点之间的欧氏距离往往会趋于相等(维度灾难),导致区分度下降。而余弦相似度通过对比角度,在处理这种稀疏或高维语义空间时表现更稳定。

所在很多具体的VectorStore(比如InMemoryVectorStore)都默认使用余弦相似度。

2.2.1 基于向量的查询

将查询文本转换成向量后,可以调用如下所示的similarity_search_by_vector/asimilarity_search_by_vector方法实施相似度查询,查询结果以Document列表的形式返回。VectorStore并没有真正实现similarity_search_by_vector方法,asimilarity_search_by_vector仅仅是以Coroutine的形式调用了该方法(以下介绍的异步方法基本采用这样的实现方式),所以具体的子类需自行实现此方法。VectorStore将针对similarity_search_by_vector方法的实现下放给了子类。

class VectorStore(ABC):    
    def similarity_search_by_vector(self, embedding: list[float], k: int = 4, **kwargs: Any) -> list[Document]:
        raise NotImplementedError

    async def asimilarity_search_by_vector(self, embedding: list[float], k: int = 4, **kwargs: Any) -> list[Document]:
        return await run_in_executor(None, self.similarity_search_by_vector, embedding, k=k, **kwargs)

上述两个方法包含如下的参数:

  • embedding:原始查询文本转换成的查询向量;
  • k:指定返回最相似的Document数量。如果k太小(如1),LLM可能拿不到足够的信息;如果k太大(如20),可能会引入无关噪音并浪费Token。通常3-5是平衡点,所有的查询方法都有此参数;
  • kwargs:通常用来传递元数据过滤器。例如,只在source='manual_v2.pdf'的范围内进行相似度搜索。
2.2.2 基于文本的查询

similarity_search/asimilarity_search方法的相似性查询直接针对原始的查询文本进行,其内部会利用Embeddings对象对其向量化。similarity_search被定义成必须实现的抽象方法。

class VectorStore(ABC):
    @abstractmethod
    def similarity_search(self, query: str, k: int = 4, **kwargs: Any) -> list[Document]
    async def asimilarity_search(self, query: str, k: int = 4, **kwargs: Any) -> list[Document]
2.2.3 返回相似度分数

如下所示的similarity_search_with_score/asimilarity_search_with_score除了返回Document之外,还会返回一个相似度分数。一般来说,我们使用这个方法返回原始的分数,比如上述的两种距离。由于很多类型的距离是没有范围限制的,基于不同尺度的得分其实没有什么意义,所以我们更希望得到一个归一化的得分,VectorStore为此定义了similarity_search_with_relevance_scores/asimilarity_search_with_relevance_scores。这两个方法返回的相关性得分限定在[0,1]之间,分数越高,相关性越强。

class VectorStore(ABC):
    def similarity_search_with_score(
        self, *args: Any, **kwargs: Any
    ) -> list[tuple[Document, float]]
    async def asimilarity_search_with_score(
        self, *args: Any, **kwargs: Any
    ) -> list[tuple[Document, float]]

    def similarity_search_with_relevance_scores(
        self,
        query: str,
        k: int = 4,
        **kwargs: Any,
    ) -> list[tuple[Document, float]]
    async def asimilarity_search_with_relevance_scores(
        self,
        query: str,
        k: int = 4,
        **kwargs: Any,
    ) -> list[tuple[Document, float]]

similarity_search_with_relevance_scores/asimilarity_search_with_relevance_scores默认会调用similarity_search_with_score/asimilarity_search_with_score方法,并使用如下这个_select_relevance_score_fn方法对后者返回的分数实施会归一化。由于_select_relevance_score_fn方法并没有真正被实现,若子类没有重写此方法,调用这两个方法会抛出异常。VectorStore提供了_euclidean_relevance_score_fn和_cosine_relevance_score_fn这两个静态方法对欧式距离和余弦距离进行归一化,我们可以根据当前采用的相似度算法利用它们来实现_select_relevance_score_fn方法。

class VectorStore(ABC):
    @staticmethod
    def _euclidean_relevance_score_fn(distance: float) -> float:
        return 1.0 - distance / math.sqrt(2)
    @staticmethod
    def _cosine_relevance_score_fn(distance: float) -> float:
        return 1.0 - distance    
    def _select_relevance_score_fn(self) -> Callable[[float], float]:
        raise NotImplementedError

2.3 最大边界相关性检索

普通的相似度查询采用K-最近邻算法(KNN:K-Nearest Neighbors)只会找最像的,容易导致搜出来的几条内容高度重复。为了保证相关性的同时让结果更具多样化,可以采用最大边界相关性(MMR:Max Marginal Relevance)算法。举个例子,假设我们的查询条件是:“如何评价周杰伦的音乐?”

  • KNN检索:可能给你找回5条都在夸他中国风的段落。信息冗余,浪费了LLM的上下文窗口;
  • MMR检索:会先选一条讲中国风的,第二条发现也是讲中国风的(太像了,剔除),转而选一条讲编曲技巧的,第三条选讲社会影响的。

MMR在挑选每一条文档时,会权衡两个指标:

  • 相关性 (Relevance):这条文档离我的问题有多近?
  • 多样性 (Diversity):这条文档和已经选出来的文档是不是太像了?

MMR公式中有一个关键系数λ(通常在0到1之间),默认值(通常为 0.5)在相关性和多样性之间取得平衡:

  • λ = 1:完全等同于普通的相似度搜索(只看相关性);
  • λ = 0:完全追求多样性(选出来的东西互不相同,哪怕离问题有点远)。

如果需要使用MMR查询,可以调用VectorStore的如下两组方法:max_marginal_relevance_search/amax_marginal_relevance_search和max_marginal_relevance_search_by_vector/amax_marginal_relevance_search_by_vector。max_marginal_relevance_search和max_marginal_relevance_search_by_vector依然没有被真正实现,提供MMR检索功能的子类需要重写它们。

class VectorStore(ABC):
     def max_marginal_relevance_search(
        self,
        query: str,
        k: int = 4,
        fetch_k: int = 20,
        lambda_mult: float = 0.5,
        **kwargs: Any,
    ) -> list[Document]:
        raise NotImplementedError

    async def amax_marginal_relevance_search(
        self,
        query: str,
        k: int = 4,
        fetch_k: int = 20,
        lambda_mult: float = 0.5,
        **kwargs: Any,
    ) -> list[Document]

    def max_marginal_relevance_search_by_vector(
        self,
        embedding: list[float],
        k: int = 4,
        fetch_k: int = 20,
        lambda_mult: float = 0.5,
        **kwargs: Any,
    ) -> list[Document]:
        raise NotImplementedError

    async def amax_marginal_relevance_search_by_vector(
        self,
        embedding: list[float],
        k: int = 4,
        fetch_k: int = 20,
        lambda_mult: float = 0.5,
        **kwargs: Any,
    ) -> list[Document]

如下是相关参数说明:

  • query/embedding:查询文本或者嵌入向量;
  • k: 最终选择记录数;
  • fetch_k:MMR检索的候选记录是KNN检索的结果,该参数表示此检索结果的记录数;
  • lambda_mult: 上述的系数λ;

2.4 统一收口

对于上面实现的众多形式的相似度查询方法,VectorStore定义了如下的search/asearch方法来统一收口,我们可以利用search_type参数决定具体调用的方法。

class VectorStore(ABC):
    def search(self, query: str, search_type: str, **kwargs: Any) -> list[Document]:
        if search_type == "similarity":
            return self.similarity_search(query, **kwargs)
        if search_type == "similarity_score_threshold":
            docs_and_similarities = self.similarity_search_with_relevance_scores(
                query, **kwargs
            )
            return [doc for doc, _ in docs_and_similarities]
        if search_type == "mmr":
            return self.max_marginal_relevance_search(query, **kwargs)
        msg = (
            f"search_type of {search_type} not allowed. Expected "
            "search_type to be 'similarity', 'similarity_score_threshold'"
            " or 'mmr'."
        )
        raise ValueError(msg)

    async def asearch(self, query: str, search_type: str, **kwargs: Any) -> list[Document]

search_type参数具有如下三个选项:

  • similarity:调用similarity_search/asimilarity_search方法;
  • similarity_score_threshold:调用similarity_search_with_relevance_scores/asimilarity_search_with_relevance_scores方法;
  • mmr:调用max_marginal_relevance_search/amax_marginal_relevance_search方法;

3. 自定义VectorStore

在如下的演示程序中,我们继承VectorStore定义了一个SimpleVectorStore类。构造函数除了指定Embeddings对象之外,还利用similarity参数指定了采用的相似度算法(基于欧式距离或者余弦距离)。根据指定的算法,我们定义相应的函数dist_fn用来计算两个嵌入向量的距离,并对用来计算归一化相关分数的_select_relevance_score_fn函数进行了相应设置。

from typing import Any, Iterable,NamedTuple,Callable,Literal, Self,Literal
import uuid
from dotenv import load_dotenv
from langchain_core.documents import Document
from typing_extensions import override
from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import VectorStore,InMemoryVectorStore
from langchain_core.embeddings import Embeddings
from itertools import islice
import math

load_dotenv()
Vector = list[float]
class Record(NamedTuple):
    id:str
    vector:list[float]
    text:str
    metadata:dict[str,Any]

class SimpleVectorStore(VectorStore):    
    store: list[Record]
    dist_fn:Callable[[Vector,Vector],float]

    def __init__(self, embedding: Embeddings, similarity:Literal["cosine","euclidean"] ="cosine") -> None:
        def cosine_dist(v1: Vector, v2: Vector) -> float:
            import numpy as np  
            norm_v1 = np.linalg.norm(v1)
            norm_v2 = np.linalg.norm(v2)
            return 1- np.dot(v1, v2) / (norm_v1 * norm_v2)
        self.store = []
        self.embedding = embedding
        if similarity == "euclidean":
            self.dist_fn = lambda v1, v2: math.dist(v1, v2)
            self._select_relevance_score_fn = lambda : self._euclidean_relevance_score_fn
        else:
            self.dist_fn = cosine_dist
            self._select_relevance_score_fn = lambda : self._cosine_relevance_score_fn

    def add_texts(
        self,
        texts: Iterable[str],
        metadatas: list[dict] | None = None,
        *,
        ids: list[str] | None = None,
        **kwargs: Any,
    ) -> list[str]:
        count = sum(1 for _ in texts)
        metadatas = metadatas or [{}] * count
        ids = ids or [str(uuid.uuid4()) for _ in range(count)]
        for id, text, metadata in zip(ids, texts, metadatas):
            vector = self.embedding.embed_query(text)
            self.store.append(Record(id=id, vector=vector, text=text, metadata=metadata))
        return ids
    
    def similarity_search(self, query: str, k: int = 4, **kwargs: Any) -> list[Document]:
        result = self.similarity_search_with_score(query, k=k, **kwargs)
        return [doc for doc, score in result]
    
    @override
    def similarity_search_with_score(self, query: str, k: int = 4, **kwargs: Any) -> list[tuple[Document, float]]:
        query_embedding = self.embedding.embed_query(query)
        filter = lambda it: all(it.metadata.get(k) == v for k, v in kwargs.items())
        selected = (it for it in self.store if filter(it))
        result = ((it, self.dist_fn(query_embedding, it.vector)) for it in selected)
        top_k = islice(sorted(result, key=lambda x: x[1]), k)
        return [(Document(page_content=it.text, metadata=it.metadata), score) for it, score in top_k]

    @classmethod
    @override
    def from_texts(
        cls,
        texts: list[str],
        embedding: Embeddings,
        metadatas: list[dict] | None = None,
        similarity:Literal["cosine","euclidean"] ="cosine",
        **kwargs: Any,
    ) -> Self:
        store = cls(
            embedding=embedding, 
            similarity=similarity,
        )
        store.add_texts(texts=texts, metadatas=metadatas, **kwargs)
        return store

实现的add_texts方法将指定的文本、ID和元数据列表转换成对应的Record元组,如果没有指定ID,则使用生成的UUID。Record元组被置于表示存储的store字段,这是一个普通的列表。我们重写了similarity_search_with_score方法,根据指定的关键字参数对记录进行过滤。在为指定的查询文本生成嵌入向量后,我们利用dist_fn字段返回的函数计算相似度距离。我们最后按照此距离对过滤记录进行排序,提取前K条记录生成返回的(Document, Distance)元组列表。实现的similarity_search方法直接调用此方法,将Document提取出来作为返回值。

我们使用如下的程序来验证SimpleVectorStore提供的相似对检索是否合理。我们使用的是OpenAIEmbeddings,添加的两条记录是两条针对“美食”和“运动”喜好的文本,而作为查询的是针对前者的查询,所以我们希望前者与查询的相似度更大。我们分别针对两种相似度算法创建了SimpleVectorStore对象,并调用了similarity_search_with_score和similarity_search_with_relevance_scores方法看看两条记录与查询文本的“相似度距离”和“相关性得分”。

embedding=OpenAIEmbeddings(model="text-embedding-3-small")
texts=[
        "I love eating spicy Sichuan food.",
        "My favorite sport is swimming."]
query = "What are my food preferences?"

print(f"euclidean similarity:")
store = SimpleVectorStore.from_texts(
    texts=texts,
    embedding=embedding,
    metadatas=[{"category": "food"}, {"category": "sport"}],
    similarity = "euclidean"
)
result = store.similarity_search_with_score(query, k=2)
for doc, score in result:
    print(f"Raw score: {score:.4f}, Content: {doc.page_content}")

result = store.similarity_search_with_relevance_scores(query, k=2)
for doc, score in result:
    print(f"Relevance score: {score:.4f}, Content: {doc.page_content}")

print(f"cosine similarity:")
store = SimpleVectorStore.from_texts(
    texts=texts,
    embedding=embedding,
    metadatas=[{"category": "food"}, {"category": "sport"}],
    similarity = "cosine"
)
result = store.similarity_search_with_score(query, k=2)
for doc, score in result:
    print(f"Raw score: {score:.4f}, Content: {doc.page_content}")

result = store.similarity_search_with_relevance_scores(query, k=2)
for doc, score in result:
    print(f"Relevance score: {score:.4f}, Content: {doc.page_content}")

从如下的输出可以看出,不论是采用欧式距离还是预先距离,第一条记录和查询文本的距离都要小于第二条与查询文本的距离,相关性得分则刚好相反,这符合我们的预期。

euclidean similarity:
Raw score: 1.1316, Content: I love eating spicy Sichuan food.
Raw score: 1.2591, Content: My favorite sport is swimming.

Relevance score: 0.1998, Content: I love eating spicy Sichuan food.
Relevance score: 0.1097, Content: My favorite sport is swimming.
cosine similarity:
Raw score: 0.6401, Content: I love eating spicy Sichuan food.
Raw score: 0.7929, Content: My favorite sport is swimming.

Relevance score: 0.3599, Content: I love eating spicy Sichuan food.
Relevance score: 0.2071, Content: My favorite sport is swimming.
Langchain 和 RAG 最佳实践 这是一篇关于LangChain和RAG的快速入门文章,主要参考了由Harrison Chase和Andrew Ng讲授的Langchain chat with your data课程。你可以在rag101仓库中查看完整代码。本文翻译自我的英文博客,最新修订内容可随时参考:LangChain 与 RAG 最佳实践。 阅读详情

相关推荐

SpringBoot 整合 Langchain4j RAG 技术深度使用解析

SpringBoot 整合 Langchain4j RAG 技术深度使用解析

congge 4585

无限可能Langchain——向量存储和检索

本教程将使您熟悉LangChain的向量存储和检索器抽象。这些抽象旨在支持从(向量)数据库和其他来源检索数据,以便与LLM工作流集成。它们对于获取数据作为模型推理的一部分进行推理的应用程序很重要,例如检索增强生成或RAG(请参阅我们的RAG教程。

JackWu1024 1941

深入了解使用Python和LangChain构建强大的向量存储和检索系统

在大数据和人工智能领域,如何高效地存储和检索信息一直是一个重要的研究方向。随着深度学习和自然语言处理技术的发展,向量存储和检索技术(Vector Stores and Retrievers)应运而生,并在很多应用中得到了广泛的使用。LangChain作为一个强大且灵活的框架,为开发者提供了构建和管理向量存储和检索系统的工具。本文将详细介绍如何使用LangChain构建一个强大的向量存储和检索系统。

m0_57781768的博客 1295

langchain教程-8.VectorStore/向量数据库存储和检索

是专门设计的数据库,用于通过向量表示(嵌入)来和信息。它们通常用于通过识别语义相似的内容而非依赖精确的关键词匹配,来搜索非结构化数据,如文本、图像和音频。

the_3rd_bomb的博客 2530

LangChain RAG实战——向量存储与检索篇:从向量数据库到Chain构建

本文介绍了LangChain RAG系统的三大核心模块:向量存储、语义检索和Chain构建。在向量数据库部分,对比了ChromaDB、FAISS等主流方案,并展示了ChromaDB的存储、加载和操作代码。语义检索部分详细讲解了相似度搜索、MMR检索等技术实现。最后,通过代码示例演示了如何构建完整的RAG Chain流程,包括Prompt模板设计、检索结果格式化和LLM集成。全文提供了完整的Python代码片段,帮助开发者快速实现RAG系统核心功能。

m0_74398756的博客 1026

流媒体学习之路(mediasoup)——simulcast 与 svc 简介(7)

流媒体学习之路(mediasoup)——simulcast 与 svc(7) 文章目录流媒体学习之路(mediasoup)——simulcast 与 svc(7)

qw225967的博客 7339

快速入门指南: 使用LangChain的向量存储和检索器

准备好体验LangChain的魔力了吗?今天我们要探索的是向量存储和检索器抽象,这是LLM工作流中不可或缺的一部分。我们将揭开它们的神秘面纱,并看看它们如何帮助我们从数据库和其他数据源中检索数据,特别是在需要进行检索增强生成(RAG)时。这不仅是技术上的飞跃,也是让你的应用程序更智能、更高效的关键一步。

weixin_40726747的博客 1610

Allegro PCB设计实战:5分钟搞定DDR信号线的Match Group等长规则设置

本文详细介绍了在Allegro PCB设计中,如何快速为DDR信号线设置Match Group等长规则。通过实战演练,解析了从理解相对传输延迟原理、在规则管理器中创建Match Group,到设置合理容差和利用动态绕线指示的全流程,帮助工程师在5分钟内高效完成高速信号时序约束,提升设计可靠性与效率。

gg901的博客 1055

深入理解向量存储和检索器:LangChain中的核心概念与实践

本文介绍了LangChain中的文档、向量存储和检索器概念,并展示了如何在实践中应用这些工具。这些技术为构建强大的自然语言处理应用奠定了基础,特别是在需要高效检索和利用大量文本数据的场景中。LangChain官方文档:https://python.langchain.com/docs/get_started/introduction向量数据库比较:https://www.pinecone.io/learn/vector-database-comparison/

tt_jishu的博客 845

【机械臂算法】机械臂动力学参数辨识仿真

本文以puma560m机械臂为例子进行动力学参数辨识的讲解,puma560m可以在robotic toolbox中找到,这里以它真实机械臂对他的动力学参数进行辨识。此外这里还有要说的是,机械臂参数辨识其实是一个系统工程,其中和系统的机械、电子、嵌入式都有着很深的联系,并不是仿真这么简单的。

vrijheid的博客 7834

PCL 循环体素滤波

本文提出了一种自动循环体素滤波算法,用于解决点云配准中分辨率差异大、参数设置复杂的问题。算法通过计算点云分辨率(公式1),采用迭代方式调整体素尺寸(公式2),逐步逼近目标分辨率。该方法在保留几何特征的同时有效精简数据量,实现了均匀分辨率的自动调整。实验通过PCL库实现,展示了原始点云与滤波结果的对比效果,验证了算法在减少数据规模方面的有效性。该方法为点云配准提供了预处理解决方案,相关成果已发表于SCI期刊。

点云侠的博客 282

AI--向量的存储和检索

Vectorstore 实现了as_retriever一个生成 Retriever 的方法,特别是VectorStoreRetriever。这些检索器包括特定的search_type属性search_kwargs,用于标识要调用的底层向量存储的哪些方法以及如何参数化它们。如果我们选择要使用的方法检索文档,我们可以轻松创建一个可运行的程序。向量检索是一种常见的存储和检索非结构化数据的方式,主要思路是存储文本的数据向量,给出一个查询,我们编码查询成同一个维度的数据向量,然后使用相似度去查找相关数据。

qq_29983883的博客 1220

YOLOv5源码逐行超详细注释与解读(1)——项目目录结构解析

全网最全YOLOv5项目目录结构超详细分析。逐个文件注释,小白上手必备

路人贾的博客 9万+

第四章:RAG 基石 - 向量化、存储与基础检索

章节引导:欢迎进入 RAG(Retrieval-Augmented Generation)的核心模块!如果说 LLM 是一个知识渊博但记忆可能不实时或不全面的大脑,那么 RAG 就是为它配备了一个强大的外部“记忆系统”和“检索工具”,让它能随时查阅最新、最相关的信息来回答问题。本章将聚焦 RAG 的第一步,也是至关重要的基础——。

YPeng_Gao的博客 2411

[QMT和ptrade量化交易小白入门]-120、卖出深度虚值期权的三重过滤策略

本专栏主要是介绍QMT和ptrade的基础用法,常见函数,写策略的方法,也会分享一些量化交易的思路,大概会写200篇左右。QMT的相关资料较少,在使用过程中不断的摸索,遇到了一些问题,记录下来和大家一起沟通,共同进步。

木头大左的博客 283

路径跟踪 | 图解路径跟踪PID算法(附ROS C++/Python/Matlab仿真)

PID控制是一种经典的控制算法,广泛应用于各种工业场景。在基于PID的局部路径规划中,希望机器人能快速跟踪上预设的轨迹。本文介绍PID控制的基本原理,并基于PID控制律设计路径跟踪算法,最后通过ROS C++/Python/Matlab进行仿真实现,加深理解

FRIGIDWINTER的博客 1万+

LangChain RAG实战:向量存储与相似度检索核心解析

检索增强生成(RAG)系统里,决定回答质量的关键常不在生成端,而在检索环节。传统关键词匹配只能处理字面对齐,面对同义表达和语序变化往往失效。向量存储将文本映射为高维空间中的坐标,通过相似度算法计算语义距离,让“押金怎么退”与“退款流程”这类语义相近但表述不同的内容也能被准确召回。选择合适的Embedding模型和向量数据库,并设计合理的文本切分与元数据过滤策略,能显著提升检索准确率和响应效率。LangChain提供统一接口,简化从文本切分到向量化的构建流程,再结合重排序、查询改写等手段,即可支撑生产级RAG

weixin_29061531的博客 237

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

基于LangChain的RAG与Agent智能体开发 - 向量存储与向量检索,以及RAG增强检索实现

《基于LangChain的RAG与Agent智能体开发教程》介绍了利用LangChain框架实现检索增强生成(RAG)和智能体开发的核心技术。教程详细讲解了向量数据库Chroma的使用方法,包括文档向量化存储、相似性检索等关键步骤,并演示了如何通过RetrievalQA链构建完整的RAG系统。课程涵盖从文档加载、向量存储到语义检索的全流程,结合通义大模型实现问答功能,提供了包括视频教程、课件和源码的完整学习资源包。该技术可有效解决大模型的知识局限问题,提升问答系统的准确性和可靠性。

java1234的博客 529

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

基于LangChain实现RAG(大模型使用通义千问)

RAG(Retrieval-Augmented Generation)是一种结合了检索和生成的方法,旨在提高自然语言处理任务的性能。相对于预训练和微调,RAG的优势在于能够利用外部知识源,如知识图谱或文档数据库,以增强模型的理解和生成能力。此外,RAG还可以通过检索相关信息来减少对大规模预训练数据的依赖,从而降低计算成本。

weixin_44312617的博客 2223

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

LangChain实现RAG

RAG(检索增强生成)是一种结合信息检索与生成式AI的技术框架,通过从外部知识库检索相关信息来增强大模型的回答准确性,解决幻觉生成和知识过时问题。其核心流程包括知识准备(文档处理与向量化存储)、检索(语义匹配)和生成(基于检索结果回答)。实验表明,RAG能有效补充大模型对特定领域和最新信息的认知不足。通过LangChain实现RAG系统,包含文档加载、文本分块、向量化存储、语义检索和提示增强等关键步骤,最终生成基于检索内容的精准回答。

weixin_42828342的博客 1157

黑马大模型RAG与Agent智能体实战教程——29、RAG开发——向量存储InMemoryVectorStores、Chroma、持久化向量存储、添加文档删除文档add_documents、相似度搜索

教程:https://www.bilibili.com/video/BV1yjz5BLEoY代码:https://github.com/shangxiang0907/HeiMa-AI-LLM-RAG-Agent-Dev云开发平台:https://hzh.sealos.run向量存储简介:LangChain 向量存储统一接口:向量存储类型:核心概念: 查看现有代码风格,然后创建向量存储使用示例文件。[3 tools called]创建向量存储使用示例文件,包含内置和外部向量存储的示例。[2 tools ca

Dontla的博客 1252

基于langchain的RAG问答(QA)链实现

初始化大语言模型# 定义 RAG 链并进行文档格式化、提示、生成回答| prompt| llm# 使用 RAG 链生成回答rag_chain.invoke("什么是德育含义?")说明RAG 链将检索结果格式化并通过提示模板提供给语言模型生成回答。函数用于将文档内容格式化为连续文本。使用invoke方法传入问题,并通过解析生成的答案。加载数据嵌入向量生成与存储向量相似性检索使用提示模板和检索所得文档和大语言模型生成回答。

qq_44742936的博客 2733

揭秘LangChain:利用向量存储和检索器提升LLM数据获取能力

LangChain为向量存储和检索器提供了强大的抽象支持,使得在LLM应用中更高效地获取数据成为可能。通过优化这些工具,开发者可以更好地为复杂应用打造出色的用户体验。LangChain API参考LangChain开发者指南。

bhawfgrcbtwny的博客 961

[RAG在LangChain中的实现-04]常用的向量存储和基于向量存储的检索器

向量存储是RAG解决方案的核心,目前市面上由很多向量存储产品,由免费开源的,也有商业闭源的;有本地部署的,也有完全云托管的;有传统数据库产品推出的针对向量存储的扩展,也有新势力专门针对向量存储设计的新产品。基于所有的VectorStore都提供了对应的检索器,后者可以以一个Runnable对象的形式作为LCEL链上的一环。

JaydenAI的博客 754

实现RAG:使用LangChain实现图检索查询

该术语在当今的技术领域中使用得很多,但它的实际含义是什么呢?以下是来自不同来源的一些定义:“检索增强生成(RAG)是优化大型语言模型输出的过程,因此在生成响应之前,它会参考训练数据源之外的权威知识库。” — AWS“检索增强生成(RAG)是一种利用从外部来源获取的事实,提高生成式人工智能模型的准确性和可靠性的技术。” — 英伟达“检索增强生成(RAG)是一个人工智能框架,通过将模型建立在外部知识来源上,以补充法学硕士对信息的内部表示,从而提高法学硕士生成的响应的质量。” — IBM研究院。

一览无遗 1407
上一篇: [RAG在LangChain中的实现-02]根据数据格式选择文档加载器和文本分割器
下一篇: [RAG在LangChain中的实现-04]常用的向量存储和基于向量存储的检索器
JaydenAI
博客等级 码龄1年 243粉丝 324原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值