Langchain
一、Langchain核心组件(Components)
1. 文本向量
1.1 嵌入与嵌入模型(Embedding and Embedding Models)
计算机天生擅长处理数字,但不理解文字、图片的含义。嵌入(Embedding)的核心思想就是将人类
世界的符号(如单词、句子、产品、用户、图片)转换为计算机能够理解的数值形式(即向量,本质
上是一个数字列表),并且要求这种转换能够保留原始符号的语义和关系。
我们可以把它想象成一个翻译过程,把人类语言“翻译”成计算机的“数学语言”。
说明:我们之前一直用的大语言模型是生成式模型。它理解输入并生成新的文本(回答问题、写文
章)。它内部实际上也使用嵌入技术来理解输入,但最终目标是“创造”。
而嵌入模型(Embedding Models)是表示型模型。它的目标不是生成文本,而是为输入的文本创建
一个最佳的、富含语义的数值表示(向量)。如 OpenAI 的 “text-embedding-3-large” 嵌入模型;
Google 的 “gemini-embedding-001” 嵌入模型;阿里的 “Qwen3-Embedding-8B” 嵌入模型等。
1.1.1 什么是向量?
首先我们要知道,嵌入的结果是就是一个向量,它本质上是一个数字列表(一维数组)。例如:
[0.023, 0.487, -0.129, ..., 0.325] 。对于向量来说,有两个关键概念需要了解:
向量维度
嵌入结果得到的列表长度是固定的,称为向量的“维度”。例如,OpenAI 的 text-embedding- ada-002 模型会生成一个 1536 维的向量, text-embedding-3-large 模型会生成一个 3072 维
的向量。
维度越高,通常能捕捉更细微的语义信息,但也需要更多的计算和存储资源。
向量空间
想象一个无限延伸的、拥有无数个维度的宇宙,这个宇宙就是一个向量空间。这有点抽象,可以想象
一下:
- 在三维世界里,一个点可以用 (x, y, z) 坐标表示,例如 (2, 5, -1) 。
- 在机器学习的高维向量空间中,一个点可能是 (0.1, 0.7, -0.2, 0.4, …, 0.02) ,一
个有几百或几千个数字的坐标。
在这个空间里,每个点(即每个向量)都能代表一个概念。例如在嵌入模型中,一个点可以代表一个
单词、一句话、一张图片、一个用户、一部电影等。
到这里,向量空间的威力就能体现出来:我们可以用数学来度量语义。可以通过计算两个向量之间
的“距离”或“相似度”来实现这一点。
- 欧氏距离(Euclidean Distance):就是我们高中几何学的两点之间的直线距离。距离越短,相似
度越高。 - 余弦相似度(Cosine Similarity):它忽略向量的绝对长度(大小),只关注两个向量在方向上的
差异。在文本和语义的世界里,“方向”代表“含义”,而“长度”往往只代表“文本的长
度”或“词汇的多少”。换句话说,余弦相似度关注的是 “你们是否指向同一个方向” / “你们是
否代表同一个含义”
因此,在捕捉语义上的相似性上,余弦相似度是更常用的度量方式。
我们又能反推出,由于使用向量来绘制向量空间,而向量是有维度的,维度越高,则更能捕捉极其细
微和复杂的语义差别(比如“高兴”和“喜悦”的区别)。
这能干什么?这能解决一个传统数据库(如MySQL)不擅长的问题:基于内容的相似性搜索,而不是
基于精确匹配的查询。
1.1.2 嵌入模型应用场景
对于嵌入模型,实际上在示例选择器部分,我们已经使用过。当时使用的场景就是可以根据语义相似
性完成示例的筛选。
根据嵌入的特性,由此延伸出了许多嵌入模型在 AI 应用的使用场景:
- 语义搜索(Semantic Search)
传统搜索依赖关键词匹配(搜 “苹果” ,只能找到包含 “苹果” 这个词的文档)。
语义搜索则能将查询(如 “一种红色的水果” )和文档库中的所有文档都转换为向量。然后计算查询
向量与所有文档向量的相似度,返回最相似的文档。这样即使文档里没有 “红色” 和 “水果” 这些
词,但只要它是关于 “苹果” 的,就能被找到。
如下图为我们展示了借助嵌入模型进行文档搜索的过程:
- 为多文档生成其各自的向量,
- 为搜索查询语句生成向量,
- 衡量查询向量与每个文档向量之间的相似性,得到相似度最高的文档。
- 检索增强生成(Retrieval-Augmented Generation, RAG)
这是当前大语言模型应用的核心模式。当用户向 LLM 提问时,系统首先使用嵌入模型在知识库(如公
司内部文档)中进行语义搜索,找到最相关的内容,然后将这些内容和问题一起交给 LLM 来生成答
案。这极大地提高了答案的准确性和时效性。
- 推荐系统(Recommendation Systems)
将用户(根据其历史行为、偏好)和物品(商品、电影、新闻)都转换为向量。喜欢相似物品的用
户,其向量会接近;相似的物品,其向量也会接近。通过计算用户和物品向量的相似度,就可以进行
精准推荐。
- 异常检测(Anomaly Detection)
正常数据的向量通常会聚集在一起。如果一个新数据的向量远离大多数向量的聚集区,它就可能是一
个异常点(如垃圾邮件、欺诈交易)。
1.2 Embeddings 嵌入模型类
在 LangChain 中,有很多的嵌入模型提供方,使用不同的模型提供方,需要安装为其各自包,例如:
- OpenAI:
pip install -U langchain-openai - Ollama:
pip install -U langchain-ollama - Google Gemini:
pip install -U langchain-google-genai - 更多见这里。
1.2.1 定义嵌入模型
在这里我们选择 OpenAI 来进行后续操作。定义 OpenAI 下的嵌入模型使用:
class langchain_openai.embeddings.base.OpenAIEmbeddings ,官方接口介绍见这里。
定义 OpenAIEmbeddings 嵌入模型类与定义聊天模型类似,如下所示:
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
model="text-embedding-3-large", # 是 OpenAI 2024年发布的最新嵌入模型,生成3072维
的高质量向量
)
在 LangChain 框架中基础 Embeddings 类( OpenAIEmbeddings 继承了它)设计了两个核心方法
来处理文本嵌入。
.embed_documents(): 用于处理文档Documents。它的输入是多个文本。例如要将一个
知识库里的所有段落都转换成向量后存入数据库,就会使用这个方法。- 它返回一个【二维列表】
List[List[float]]。外层列表的每个元素对应一个输入文档,
内层列表则是该文档的向量表示。
- 它返回一个【二维列表】
.embed_query(): 用于处理查询Query。它的输入是单个文本(一个字符串,str)。例
如,当用户提出一个问题时,需要将这个问题转换成向量,以便在数据库中搜索相似的文档段落,
就会使用这个方法。- 它返回一个【一维列表】,里面是浮点数(
List[float]),代表单个查询文本的向量。
- 它返回一个【一维列表】,里面是浮点数(
其实分别对应下图中文档与查询的向量生成:
之所以设计成两个方法,是因为某些嵌入模型提供商(如 OpenAI、Cohere 等)会针对 “被搜索的文
档” 和 “搜索查询本身” 采用不同的优化策略和模型。即使底层是同一个模型,也可能对两者进行不
同的预处理(例如添加不同的指令前缀),以获得更好的搜索效果。
1 .2.2 嵌入文档列表
embed_documents 的语义是 “索引”。它的目的是预处理大量文本,为它们创建向量表示,以便
后续被搜索。这一般是一个离线、批量处理的过程。代码如下:
from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
markdown_path = "../Docs/Markdown/脚手架级微服务租房平台Q&A.md"
# single 模式加载后,默认只有一个 Document 对象
loader = UnstructuredMarkdownLoader(markdown_path)
data = loader.load()
# 生成分割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base", chunk_size=200, chunk_overlap=50
)
# 分割文档
documents = text_splitter.split_documents(data)
# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# 嵌入文档列表,生成向量列表
# 注意这里需要提取文档内容为字符串列表,才能传递给嵌入模型
texts = [doc.page_content for doc in documents]
documents_vector = embeddings.embed_documents(texts)
print(f"文档数量为:{len(documents)},生成了{len(documents_vector)}个向量的列表")
print(f"第一个文档向量维度:{len(documents_vector[0])}")
print(f"第二个文档向量维度:{len(documents_vector[1])}")
结果如下:
Created a chunk of size 916, which is longer than the specified 200
Created a chunk of size 916, which is longer than the specified 200
Created a chunk of size 260, which is longer than the specified 200
文档数量为:96,生成了96个向量的列表
第一个文档向量维度:3072
第二个文档向量维度:3072
可以看到 text-embedding-3-large 嵌入模型,生成的便是3072维的高质量向量。共存在96个文
档,因此生成了96个向量的列表。
1.2.3 嵌入单个查询
embed_query 的语义是 “搜索”。它的目的是在用户发起请求时,实时地将一个问题或指令转换
为向量,用于在已索引的文档向量中进行检索。这是一个在线、实时、按需处理的过程。
为单个查询生成向量的代码如下:
from langchain_openai import OpenAIEmbeddings
# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# 嵌入单个查询
query_vector = embeddings.embed_query("项目中遇到了哪些挑战?如何解决?")
print(f"向量维度:{len(query_vector)}")
print(f"向量前五个数值为:{query_vector[:5]}")
结果如下:
向量维度:3072
向量前五个数值为:[-0.01306734886020422, -0.004718094132840633,
-0.005446741823107004, -0.014186487533152103, 0.004673811607062817]
1.3 向量存储(Vector Stores)
1.3.1 向量数据库介绍
在 LangChain 中,实际并不需要我们直接手动调用嵌入模型去生成向量,然后手动去比较向量。在我
们之前提供的 RAG 知识地图中,存在一个 Vector Stores 向量存储,如下图所示:
实际上,向量是被管理在专门的向量存储介质中,如向量数据库。向量存储的核心任务是解决一个传
统数据库(如MySQL)不擅长的问题:基于内容的相似性搜索(Similarity Search),而不是基于精
确匹配的查询。
根据上图,我们想要从向量数据库中进行相似度搜索,首先就需要将向量存储管理起来。想象一下,
一篇文档若转换成一个有1536个浮点数的向量。一百万篇文档就是1536MB(约1.5GB)的纯向量数
据。这只是一个起点,现实中的数据集可能轻松达到千万甚至亿级。如何高效地存储和管理这些向
量?
向量数据库则提供了专门用于高效存储、管理和检索高维向量的能力。其核心就是 “高效地组织和检
索这些数据”。
常见的向量数据库核心机制如下:
- 专门的索引
这是向量数据库的灵魂。它们不会使用暴力搜索,而是会预先为所有向量构建一种特殊的索引结构。
常见的方法有近似最近邻(ANN)搜索:为了追求极致的速度,它愿意牺牲一点点精度。它不会保证
找到绝对最相似的向量(即最近邻),但能以极高的概率找到非常相似的向量。通过聚类、分层、压
缩等算法技术,将搜索范围从“整个数据库”缩小到“几个最可能的候选集”。
这就像在一个大图书馆里找书,你不是从 A 到 Z 遍历所有书架,而是先根据分类(文学、历史、科
学)找到大概区域,再在这个区域内仔细找,这样快得多。
- 向量相似度计算优化
向量数据库底层使用高度优化的库来进行向量运算。如 FAISS 向量数据库,它是 Facebook AI 研究院
开发的一种高效的相似性搜索和聚类的库。它能够快速处理大规模数据,并且支持在高维空间中进行
相似性搜索。
这些库充分利用了 CPU 的 SIMD 指令集和 GPU 的并行计算能力,让大规模的向量计算速度极快。
SIMD 指令集指 “单指令多数据流” 技术,是一种采用一个控制器来控制多个处理器,本质上非常类
似一个向量处理器,可对控制器上的一组数据(又称“数据向量”) 同时分别执行相同的操作从而
实现空间上的并行。简单来说,就是一个指令能够同时处理多个数据。
- 数据管理功能
现代的向量数据库不仅有专门的索引,还提供了完整的数据管理功能:
- CRUD 操作:支持增删改查,可以动态地更新向量数据。
- 元数据过滤:这是非常关键的功能。除了向量本身,每篇文档还有元数据(Metadata),比如创
建时间、作者、类别等。向量数据库允许你先用元数据过滤(“找出xxxx年以后的、属于科技类
别的文档”),再在这个缩小的范围内进行向量相似度搜索,极大地提升了准确性和效率。 - 可扩展性与持久化:它们可以轻松地分布式部署,处理海量数据;同时保证数据持久化,不像纯
内存方案一样断电丢失。 - 集成方便:提供友好的API(如gRPC, RESTful),使得像 LangChain 这样的框架可以轻松地与
之集成,开发者无需关心底层细节。
因此,我们可以利用专门的向量数据库(如Chroma, Weaviate, Pinecone, Qdrant, Milvus等),通过
构建索引、优化计算流程、并提供丰富的过滤和管理功能,从而实现对海量高维向量数据进行快速、
可扩展的【相似性检索】的一套完整技术方案。
LangChain 框架则通过与这些向量数据库集成,让开发者无需手动处理向量生成、存储和比较的复杂
性,只需关注业务逻辑本身,极大地提高了开发效率和应用性能。
下面我们将演示如何使用 LangChain 提供的内存级存储,与集成的向量数据库。查看 LangChain 支持
的所有向量数据库点击这里。除了课件上的向量数据库使用,其他存储可查阅官方文档自行接入。
1.3.2 内存存储
我们将使用 LangChain 的 InMemoryVectorStore 来实现向量的内存存储。
1.3.2.1 基本操作
- 初始化
LangChain 中的大多数向量在初始化向量存储时接受嵌入模型作为参数。如下所示:
from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# 内存存储初始化
vector_store = InMemoryVectorStore(embedding=embeddings)
- 添加文档
我们可以使用 add_documents 方法,向内存存储中去添加文档。要注意的是,该方法会为添加的
文档编排索引,索引列表随着该方法返回。
这也就是在前文中,我们一直在提的:当我们想对某文本进行【数据检索】时的两个步骤:
- 编制索引: 用于从源中摄取数据并为其编制索引。
- 检索和生成 :接受用户查询并从索引中检索相关数据,然后将其传递给模型。
对于第一步,我们终于要完成了。如下所示:
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
# 生成分割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base", chunk_size=200, chunk_overlap=50
)
# 加载文档
data = UnstructuredMarkdownLoader("../Docs/Markdown/脚手架级微服务租房平台
Q&A.md").load()
# 分割文档
documents = text_splitter.split_documents(data)
# 添加文档
ids = vector_store.add_documents(documents=documents)
print(f"共编排了{len(ids)}个文档索引")
print(f"前3个文档的索引是:{ids[:3]}")
结果输出:
共编排了96个文档索引
前3个文档的索引是:['3b22073d-8a8f-4f74-b99a-09d6343e7602', '3c2e05ea-1681-4d5ea31c-f01f2e0a0ac8', 'dcc3da40-b1bf-4ae3-9fd4-1f758b47af0f']
- 获取文档
使用 get_by_ids 方法,通过索引列表获取对应的文档列表。如下所示:
doc_3 = vector_store.get_by_ids(ids[:3])
print(f"{[doc.page_content for doc in doc_3]}")
结果如下:
[
'通用问题\n\n为什么做这个项目?\n\n回答1:(出于兴趣爱好开发)\n\n大学期间,我和同学
在外合租过一段时间,使用了一些租房平台,于是我有个想法,自己能不能开发一个租房平台,可以让
我将理论知识与实践相结合。我希望通过实际项目来加深对 Java 编程语言和相关技术的理解。于是我
便查找了一些资料,看了一些开源项目,进行了一些改进。\n\n回答2:(开源项目的解释)',
'回答2:(开源项目的解释)\n\n这个项目其实是在 github 上找到的一个开源项目,主要是可
以支持一些常规的聊天项目,我对于聊天如何实现的比较感兴趣。顺便也想锻炼一下自己工程代码能
力,在网上就找到了这个开源项目和项目的一些比较完善的文档和介绍,再加上找了一个业务场景:租
房。所以就确定了这个项目。\n\n回答3:(学校课设的解释)',
'回答3:(学校课设的解释)\n\n这是学校的课设题目,然后在课设项目的基础上,查找了一些
资料,进行了一些改进。\n\n这个项目为啥和上个(之前)同学的项目一样?\n\n回答1:(开源项目的
回答)'
]
- 删除文档
使用 delete 方法,删除传入索引列表对应的文档列表;若不传入索引列表,则认为全量删除。如下
所示:
vector_store.delete(ids=ids[:3])
测试一下删除是否成功:
doc_3 = vector_store.get_by_ids(ids[:3])
print(f"{[doc.page_content for doc in doc_3]}")
最终输出: [] ,可以看到确实删除成功。
1.3.2.2 向量搜索
如果我们传入一个查询,向量存储将嵌入该查询,在所有嵌入的文档中执行相似性搜索,并返回最相
似的文档。
这体现了两个重要的概念:首先,需要有一种方法来衡量查询与任何嵌入文档之间的相似性。其次,
需要有一种算法能够高效地在所有嵌入文档中执行这种相似性搜索。
对于【相似性】这一点,之前已经讲过,再来回顾下:
- 欧氏距离(Euclidean Distance):就是我们高中几何学的两点之间的直线距离。距离越短,相
似度越高。 - 余弦相似度(Cosine Similarity):它忽略向量的绝对长度(大小),只关注两个向量在方向上
的差异。在文本和语义的世界里,“方向”代表“含义”,而“长度”往往只代表“文本的长
度”或“词汇的多少”。换句话说,余弦相似度关注的是 “你们是否指向同一个方向” / “你们是
否代表同一个含义”
因此,在捕捉语义上的相似性上,余弦相似度是更常用的度量方式。
对于【相似性搜索】则可以通过向量存储提供的搜索方法实现。
1.3.2.2.1 相似性搜索
想要获取根据相似性搜索的结果,即嵌入单个查询,并查找相似的文档,并将它们作为文档列表返
回。这可以使用 similarity_search 方法来实现。说明: InMemoryVectorStore 是根据【余
弦相似度】来捕捉语义的。
代码如下:
search_docs = vector_store.similarity_search(query="数据库表怎么设计的?", k=2)
for doc in search_docs:
print("*" * 30)
print(doc.page_content)
方法参数解释如下:
query:输入的查询str。k:要返回的文档数。默认为 4。
打印结果如下:
******************************
提供两种方案有以下好处:
降低门槛:单机版让用户快速体验核心功能,避免初期陷入复杂部署。
灵活演进:用户可从单机版过渡到集群版,逐步适应生产需求。
业务专属问题
为什么将房源表拆分为水平分表(如按状态分表)?具体如何实现?
原因:
单表数据量过大(如房源状态频繁更新)会导致查询性能下降;
不同状态(上架/已租/下架)的查询频率不同,拆分可减少锁争用;
******************************
数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简单的方法来执行 SQL 语句,不需要编写复杂的
ORM 映射文件。
缓存:Redis
可以看见,搜索出来的内容,是较为符合我们的预取。
有的同学可能有疑问了:搜索出来的结果,为什么不直接是问题的答案,而只是相似的文档呢?
那么恭喜你已经具备了构建智能应用基础逻辑了。此时我们可以将 “找到的最相关内容” 和我们 “提
出的问题” 一起交给LLM 来生成答案,这便能极大地提高答案的准确性和时效性。以上流程,叫做检
索增强生成(Retrieval-Augmented Generation, RAG),这是当前大语言模型应用的核心模式。
RAG 我们将会在后续内容进行教学。
1.3.2.2.2 元数据过滤
回顾一下,每个 Document 对象,都包含了以下参数:
id:可选的文档标识符。理想情况下,这应该在整个文档集合中是唯一的,并格式化为
UUID,但不会强制执行。page_content:字符串文本metadata:与内容关联的任意元数据。类型为dict [Optional]
虽然向量数据库实现了搜索算法,来有效地搜索所有嵌入的文档以找到最相似的文档。但现实场景
中,我们还希望通过先根据元数据进行过滤,来帮助缩小搜索范围。例如从特定来源或日期范围检索
文档。
from langchain_core.documents import Document
def _filter_function(doc: Document) -> bool:
return doc.metadata.get("source") == "hahaha"
search_docs = vector_store.similarity_search(
query="数据库表怎么设计的?",
k=2,
filter=_filter_function
)
for doc in search_docs:
print("*" * 30)
print(doc.page_content)
这次,我们给搜索方法加入了 filter 参数,它接收一个 bool 值,表示我们可以根据条件选择是否
过滤某些文档。因此我们定义了一个 _filter_function 过滤函数,可以根据文档元数据先过滤出
文档,再去进行搜索。
示例中,我们给出的 source=="hahaha" 是无效的,因此会将所有的文档都过滤掉,导致示例代码
运行后结果为空!将 source 换成正确的来源后,再次运行则结果正确。
1.3.3 Redis 向量存储
我们还可以使用 Redis 来存储向量。大多数开发者都熟悉 Redis,因为它速度快、拥有庞大的客户端库
生态系统,并且多年来已被众多大型企业采用。从本质上讲,Redis 是一种键值型的 NoSQL 数据库,
除了传统用例之外,Redis 还提供了诸如搜索和查询功能等额外能力,允许用户在 Redis 内创建二级
索引结构。这使得 Redis 能够以缓存的速度充当向量数据库。
1.3.3.1 基本概念
理解 RediSearch
RediSearch 是 Redis 官方提供的一款高性能【搜索】与【全文索引】引擎模块。它基于 Redis 构建,
使用户能够直接在 Redis 数据库中执行复杂的【搜索】和【分词查询】,无需额外引入外部搜索引
擎。RediSearch 特别适用于轻量级、响应速度要求较高的分词搜索场景。
RediSearch 提供了内置的分词功能,既避免 LIKE 的局限性(无法支持分词查询。例如:数据
为“Apple iPhone 17 Pro Max 256GB 星宇橙色”,搜“苹果17橙色”无法匹配),又无需部署 ES 这
种大型组件,集成简单、性能出色,非常适合中等规模的全文检索需求。
理解 Index
Index(索引)是 RediSearch 模块里的概念,用于定义一个查询目录。Index 是一个独立的数据结
构,它建立在多个 Redis Keys (Hash 类型)之上,这专门为了极速执行文本搜索、过滤和聚合而设
计。它本身不存储数据,而是存储了指向其他 Redis Keys 的指针,和这些 Keys 中特定字段的索引信
息。
理解 Index Fields
Index Fields(索引字段) 是创建索引时,明确指定的那些需要被索引的字段。它们定义了索引的“结
构”或“蓝图”,告诉 RediSearch:“请针对这些字段的内容,以其特定的方式为我构建快速搜索的
能力。”
可以把它想象成在一本书后面制作索引页(比如人名索引、主题索引)。我们不会把书中的每一个字
都做到索引里,而是只选择那些重要的关键词(字段),并记录下它们出现的页码(文档ID)。这里
的“关键词”就是 Index Fields。
在 RediSearch 中,索引字段是有特定的类型:
TAG:精确匹配的分类/标签,可以用来多值分类、过滤、分组。NUMERIC:整数/浮点数,可以进行数值范围查询,排序和统计。TEXT:全文搜索的字符串,支持分词、词干化、模糊匹配。GEO:经纬度坐标,用来查询地理空间,计算距离。
理解 metadata schema
schema 我们讲过,就是描述数据结构的声明格式。 metadata schema 则用来描述元数据的结构
声明。
这里的元数据是指我们将来要嵌入文档的元数据。因为对于文档元数据来说,它在存入 Redis 后,就
被定义成了索引字段。
对于文档元数据来说,里面存放的就是一些文档属性值,如 source 表示文档来源。我们还可以手动
加入其他元数据,这需要设置每个字段的声明: name 表示字段名, type 表示字段类型。
metadata_schema=[
{"name": "category", "type": "tag"},
{"name": "num", "type": "numeric"},
]
一张图总结关系,如下所示:
这里只展示了关系,实际的存储我们后续操作时会演示。
1.3.3.2 环境设置
使用 Redis 来存储向量,首先需要将相关环境配置好。
- 第一步:启动 Redis 服务端:使用 Docker 启动 Redis 实例。
这里要说明:对于 Redis 版本 >= 8.0 ,使用:
docker run -d -p 6379:6379 -it redis:latest
对于 Redis 版本 < 8.0 ,使用:
docker run -d -p 6379:6379 redis/redis-stack:latest
使用 docker ps 查看是否启动成功
- 第二步:安装 Redis 客户端包,以便将来定义客户端,以及运行搜索和查询命令。
由于我们使用 Python 进行开发,则选择 redis-py 库完成客户端定义。要安装 redis-py,只需:
pip install redis
- 第三步:在 LangChain 中想要使用 Redis 向量库,需要安装
langchain-redis包
pip install -qU langchain-redis
- 第四步:定义 Redis 连接 URL,客户端连接 Redis 时需要使用。
Redis 连接 URL 的基本结构是:
[protocol]://[auth]@[host]:[port]/[database]
这部分根据自己Redis服务情况而定。例如: "redis://localhost:6379"
- 第五步:测试连接(Ping)
import redis
redis_url = "redis://localhost:6379"
# 定义Redis客户端
redis_client = redis.from_url(redis_url)
# Ping
print(redis_client.ping())
若输出 True ,则表示连接测试成功。
1.3.3.3 基本操作
- 初始化
LangChain 中使用 RedisVectorStore 初始化 Redis 向量存储。由于 Redis 需要相关配置,如连
接 URL 等,因此 LangChain 提供了 RedisConfig 配置类供我们使用。如下所示:
from langchain_openai import OpenAIEmbeddings
from langchain_redis import RedisConfig, RedisVectorStore
# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# 配置 Redis 客户端
redis_url = "redis://192.168.100.238:6379"
config = RedisConfig(
index_name="qa",
redis_url=redis_url,
metadata_schema=[
{"name": "category", "type": "tag"},
{"name": "num", "type": "numeric"},
],
)
# Redis 存储初始化
vector_store = RedisVectorStore(embeddings, config=config)
class langchain_redis.vectorstores.RedisVectorStore 类,其初始化参数如下:
embeddings:用于此存储的Embeddings实例。config:可选的RedisConfig对象。
class langchain_redis.config.RedisConfig 配置类,其关键配置参数如下:
index_name:Redis 中索引的名称。默认为生成的 ULID 唯一标识符。key_prefix:Redis Key 的前缀。如果未设置,则默认为index_name。redis_url:Redis 实例的 URL。默认为“redis://localhost:6379”。metadata_schema:元数据字段的 schema。设置该字段对于将来的元数据过滤有帮助。
我们可以根据 Index Name 查询其下的所有的 Index Fields ,这需要安装 redisvl ( pip install -U redisvl ),使用 rvl 命令行工具来检查索引。
rvl index info -i qa --host 192.168.100.238 --port 6379
可以看到 Index Fields 中就包括了:
text:文档文本。embedding:向量,类型为数组。_index_name:索引名。meteData schema:文档元数据。
其中文档元数据的每个属性还都被当作单独的 Index Fields 。
- 添加文档
我们可以使用 add_documents 方法,向向量库中去添加文档。这次我们可以给被分割的文档添加相
关的元数据。如下所示:
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
# 生成分割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base", chunk_size=200, chunk_overlap=50
)
# 加载文档
data = UnstructuredMarkdownLoader("../Docs/Markdown/脚手架级微服务租房平台
Q&A.md", category="QA").load()
# 分割文档
documents = text_splitter.split_documents(data)
# 为文档添加元数据
for i, doc in enumerate(documents, start=1):
doc.metadata["category"] = "QA"
doc.metadata["num"] = i
ids = vector_store.add_documents(documents=documents)
print(f"共编排了{len(ids)}个文档索引")
print(f"前3个文档的索引是:{ids[:3]}")
结果输出:
共编排了96个文档索引
前3个文档的索引是:['qa::01K4Q0A3DSQVZBRFKJD5MS25HG',
'qa::01K4Q0A3DSQVZBRFKJD5MS25HH', 'qa::01K4Q0A3DSQVZBRFKJD5MS25HJ']
在 Redis 中的存储如下所示,下面的 embedding 字段被转换为二进制存储,因此展示了乱码。除此
之外,还存放了我们给文档设置的元数据信息。
- 获取文档
使用 get_by_ids 方法,通过索引列表获取对应的文档列表。如下所示:
ids = [":01K4Q0A3DSQVZBRFKJD5MS25HJ", ":01K4Q0A3DSQVZBRFKJD5MS25HK",
":01K4Q0A3DSQVZBRFKJD5MS25HM"]
doc_3 = vector_store.get_by_ids(ids)
print(f"{[doc.page_content for doc in doc_3]}")
结果如下:
[
'回答3:(学校课设的解释)\n\n这是学校的课设题目,然后在课设项目的基础上,查找了一些
资料,进行了一些改进。\n\n这个项目为啥和上个(之前)同学的项目一样?\n\n回答1:(开源项目的
回答)',
'这个项目为啥和上个(之前)同学的项目一样?\n\n回答1:(开源项目的回答)\n\n因为这个项
目本身就是开源项目,网上一搜都是好多都是这个项目的博客分析,写得优质的主要就是那几篇,可能
我们参考了一样的博客讲解梳理框架吧。抽奖系统虽然听起来简单,但实际上涉及到数据存储、状态转
换、异常处理等多个技术点。\n\n回答2:(学校课设的回答)',
'回答2:(学校课设的回答)\n\n这个问题面试官可能会直接的指出:上个同学和你学校不同,
但是跟你的项目完全一样。\n\n这个项目确实是我们学校的开源项目,但是我在实现的时候也在网上找
了跟课设要求类似的开源项目,而且发现网上针对这个类型的项目文档还听完善的,可能其他的同学也
是在网上跟我一样找到了类似的博客或开源项目进行借鉴的吧。'
]
- 删除文档
使用 delete 方法,删除传入索引列表对应的文档列表。如下所示:
vector_store.delete([":01K4Q0A3DSQVZBRFKJD5MS25HJ"])
或者:
# 删除指定内容
vector_store.index.drop_keys(["qa::01K4Q0A3DSQVZBRFKJD5MS25HJ"])
# 全量删除,删除索引
vector_store.index.delete(drop=True)
1.3.3.4 向量搜索
1.3.3.4.1 相似性搜索
想要获取根据相似性搜索的结果,即嵌入单个查询,并查找相似的文档,并将它们作为文档列表返
回。这可以使用 similarity_search 方法来实现。代码如下:
search_docs = vector_store.similarity_search(query="数据库表怎么设计的?", k=2)
for doc in search_docs:
print("*" * 30)
print(doc.page_content)
方法参数解释如下:
query:输入的查询str。k:要返回的文档数。默认为 4。
打印结果如下:
******************************
提供两种方案有以下好处:
降低门槛:单机版让用户快速体验核心功能,避免初期陷入复杂部署。
灵活演进:用户可从单机版过渡到集群版,逐步适应生产需求。
业务专属问题
为什么将房源表拆分为水平分表(如按状态分表)?具体如何实现?
原因:
单表数据量过大(如房源状态频繁更新)会导致查询性能下降;
不同状态(上架/已租/下架)的查询频率不同,拆分可减少锁争用;
******************************
数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简单的方法来执行 SQL 语句,不需要编写复杂的
ORM 映射文件。
缓存:Redis
除了上面的 similarity_search 方法,其实还提供了:
- 根据向量搜索方法:
similarity_search_by_vector - 根据查询搜索方法,并返回相似分值:
similarity_search_with_score - 根据向量搜索方法,并返回相似分值:
similarity_search_with_score_by_vector
例如,我们希望【根据查询语句搜索,并返回相似分值】:
scored_results = vector_store.similarity_search_with_score(query="数据库表怎么设
计的?", k=4)
for doc, score in scored_results:
print("*" * 30)
print(f"Content: {doc.page_content[:100]}...")
print(f"Metadata: {doc.metadata}")
print(f"Score: {score}")
结果如下:
******************************
Content: 提供两种方案有以下好处:
降低门槛:单机版让用户快速体验核心功能,避免初期陷入复杂部署。
灵活演进:用户可从单机版过渡到集群版,逐步适应生产需求。
业务专属问题
为什么将房源表拆分为水平分表(...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 79}
Score: 0.580324351788
******************************
Content: 数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 23}
Score: 0.582783222198
注意:分数越低表示相似度越高。
1.3.3.4.2 元数据过滤
对于上述列举的方法,在他们搜索之前,都可以根据元数据先进行过滤。对于 RedisVectorStore
来说,需要使用 Redis 过滤表达式进行筛选。示例如下:
from redisvl.query.filter import Tag
# 过滤表达式
filter_condition = Tag("source") == "hahahaha"
scored_results = vector_store.similarity_search_with_score(
query="数据库表怎么设计的?",
k=2,
filter=filter_condition
)
for doc, score in scored_results:
print("*" * 30)
print(f"Content: {doc.page_content[:100]}...")
print(f"Metadata: {doc.metadata}")
print(f"Score: {score}")
示例中,我们给出的 source=="hahahaha" 是无效的,因此会将所有的文档都过滤掉,导致示例
代码运行后结果为空!将 source 换成正确的来源后,再次运行则结果正确。
对于 Redis 过滤表达式,也可以使用 & 和 | 运算符组合。如我们现根据元数据中的 category 与
num 进行过滤,如下所示:
from redisvl.query.filter import Tag, Num
# 过滤表达式
category_is_qa = Tag("category") == "qa"
num_is_under_50 = Num("num") < 50
filter_condition = category_is_qa & num_is_under_50
scored_results = vector_store.similarity_search_with_score(
query="数据库表怎么设计的?",
k=2,
filter=filter_condition
)
for doc, score in scored_results:
print("*" * 30)
print(f"Content: {doc.page_content[:100]}...")
print(f"Metadata: {doc.metadata}")
print(f"Score: {score}")
结果如下:
******************************
Content: 数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 23}
Score: 0.582783222198
******************************
Content: 咨询消息的持久化优化
分库分表:按user_id哈希分库,按月份分表(如chat_msg_2023_10);
读写分离:
写操作:主库(高并发下队列缓冲);
读操作:从库 + Redis缓存最...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 42}
Score: 0.657752811909
1.3.3.4.3 最大边际相关性搜索
回顾一下什么是最大边际相关性?它是一种重新排序算法,它使用语义相似性作为基础工具,从一个
候选集中挑选出一组既能代表查询主题又彼此多样化的结果。
- 【语义相似性】就像面试官衡量每个应聘者与职位要求的匹配度。他会给每个应聘者打一个分数。
- 【最大边际相关性】就像团队经理(MMR算法)要组建一个团队。目标是选出一组“精华”结果,
而不是一个单一结果:
MMR 使用场景:
- 推荐系统:推荐与用户兴趣相关但又不同类型的物品,避免“信息茧房”。
- 文档摘要:从长文档中选择能代表主旨又包含不同信息的句子,避免摘要内容重复。
- RAG (检索增强生成):在从知识库检索完一堆相关文档后,使用 MMR 进行去重和多样化筛选,再
交给LLM生成答案,能有效提升答案质量和减少幻觉。
使用 MMR 搜索,需要用到 max_marginal_relevance_search 方法,代码如下:
from redisvl.query.filter import Tag, Num
# 过滤表达式
category_is_qa = Tag("category") == "qa"
num_is_under_50 = Num("num") < 50
filter_condition = category_is_qa & num_is_under_50
mmr_results = vector_store.max_marginal_relevance_search(
query="数据库表怎么设计的?",
k=2,
fetch_k=10,
filter=filter_condition
)
for doc in mmr_results:
print("*" * 30)
print(f"Content: {doc.page_content[:100]}...")
print(f"Metadata: {doc.metadata}")
我们需要注意一个参数 fetch_k , fetch_k 是 MMR 算法第一步中,从向量库中初步获取的候选
文档数量。
为了理解它,我们首先要明白 MMR 搜索是一个两阶段过程:
- 初步获取:系统首先根据查询的纯向量相似度,从庞大的向量库中找出最相似的
fetch_k个文
档。这一步的目标是“广撒网”,先找到一个足够大的相关文档池。 - 重新排序与筛选:然后,MMR 算法会在这个较小的候选池(大小为
fetch_k)中运行。它不再
只考虑与查询的相似度,还会考虑候选文档之间的多样性。它会从这fetch_k个文档中,挑选
出既与查询相关,彼此之间又不太相似的k个文档作为最终结果。
因此,其目的就是在保证相关性的前提下,提升结果的多样性。打印结果如下:
******************************
Content: 数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 23}
******************************
Content: 生产环境搭建
develop合并master
流水线部署
线上验证
你是如何优化项目性能的?你有哪些性能调优的经验?
房源搜索的筛选+排序用设计模式优化
热点数据存放缓存的优化
没有性能...
Metadata: {'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category':
'QA', 'num': 40}
1.3.4 Pinecone 向量存储
1.3.4.1 Pinecone 介绍
Pinecone 是为机器学习应用量身打造的生产级向量数据库服务,适用于高维向量数据的高效存储、索
引与查询。它屏蔽了基础设施管理,提供无缝扩展、实时数据写入和强大安全保障,让开发者和数据
科学家能够以极低运维成本,快速构建高效的相似度搜索、推荐系统和 AI 应用。
Pinecone 是一个全托管的向量数据库平台,即负责所有后端维护、扩展、更新和监控,让用户专注于
应用开发,无需担心数据库管理。
Pinecone 地址:https://www.pinecone.io/(魔法上网)
1.3.4.2 环境设置
- 首次使用需注册新用户,选择个人免费版
- 继续创建账户相关信息,或者直接右上角 skip 跳过。这里我们直接跳过。
- 注册成功会生成一个默认的 API Key。注意保存好你的 key。
- 也可以创建新的 API key:
- 设置
PINECONE_API_KEY,将 Key 添加进环境变量。 - 更新包
pip install -qU pinecone langchain-pinecone
1.3.4.3 基本操作
- 初始化
LangChain 中使用 PineconeVectorStore 类初始化 Pinecone 向量库。我们需要:
- 创建索引,参考这里。
- 使用索引来初始化
PineconeVectorStore。
如下所示:
from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore
from pinecone import Pinecone, ServerlessSpec
# 建立索引
pc = Pinecone()
index_name = "qa"
if not pc.has_index(index_name):
pc.create_index(
name=index_name, # 索引名称
dimension=3072, # 尺寸,表示向量维度,需要和嵌入模型维度一致
metric="cosine", # 度量方式,cosine 表示余弦相似度
spec=ServerlessSpec(
cloud="aws", # 亚马逊云
region="us-east-1" # 区域
),
)
# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# 获取索引
index = pc.Index(index_name)
# 定义 Pinecone 向量存储
vector_store = PineconeVectorStore(embedding=embeddings, index=index)
class langchain_redis.vectorstores.PineconeVectorStore 类,其初始化参数如
下:
embeddings:用于此存储的Embeddings实例。index:Pinecone 索引
如果我们是第一次创建索引,在 Pinecone 控制台 则可以看见被创建的索引。
- 添加文档
我们可以使用 add_documents 方法,向向量库中去添加文档。如下所示:
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
# 生成分割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base", chunk_size=200, chunk_overlap=50
)
# 加载文档
data = UnstructuredMarkdownLoader("../Docs/Markdown/脚手架级微服务租房平台
Q&A.md", category="QA").load()
# 分割文档
documents = text_splitter.split_documents(data)
# 为文档添加元数据
for i, doc in enumerate(documents, start=1):
doc.metadata["category"] = "QA"
doc.metadata["num"] = i
ids = vector_store.add_documents(documents=documents)
print(f"共编排了{len(ids)}个文档索引")
print(f"前3个文档的索引是:{ids[:3]}")
结果如下:
共编排了96个文档索引
前3个文档的索引是:['eee6f5f6-696b-4088-94d0-d0511cc92623', '36b8f351-35e5-4096-
81ea-b29701a9b4c8', '8c133041-e2a4-4040-8b11-98b4d408a752']
控制台更新数据:
- 删除文档
# 全量删除
vector_store.delete(delete_all=True)
# 删除指定id的文档列表
delete_ids = []
vector_store.delete(ids=delete_ids)
1.3.4.4 向量搜索
想要获取根据相似性搜索的结果,即嵌入单个查询,并查找相似的文档,并将它们作为文档列表返
回。这可以使用 similarity_search 方法来实现。代码如下:
search_docs = vector_store.similarity_search(
query="数据库表怎么设计的?",
k=2,
filter={"category": "QA"},
)
for doc in search_docs:
print("*" * 30)
print(f"Content: {doc.page_content[:100]}...")
print(f"Metadata: {doc.metadata}")
结果打印如下:
******************************
Content: 提供两种方案有以下好处:
降低门槛:单机版让用户快速体验核心功能,避免初期陷入复杂部署。
灵活演进:用户可从单机版过渡到集群版,逐步适应生产需求。
业务专属问题
为什么将房源表拆分为水平分表(...
Metadata: {'category': 'QA', 'num': 79.0, 'source': '../Docs/Markdown/脚手架级微
服务租房平台Q&A.md'}
******************************
Content: 数据库:MySQL + MyBatis
MySQL 和 MyBatis 是数据存储和访问的一个常见的做法。MySQL 是一个开源的关系数据库管理系
统,它可以免费使用。MyBatis 提供了一种相对简...
Metadata: {'category': 'QA', 'num': 23.0, 'source': '../Docs/Markdown/脚手架级微
服务租房平台Q&A.md'}

274

被折叠的 条评论
为什么被折叠?



