深入 LangChain 内存向量存储(Memory Vector Stores):架构解析与优化

0 阅读15分钟

1. 向量存储(Vector Stores)

1.1 向量数据库介绍

在 LangChain 中,关键步骤需要将我们提取出的嵌入向量存储到向量数据库中。向量存储是比较核心的组件,在整个 RAG 框架中扮演着重要的角色,如下图所示:

在这里插入图片描述

实际上,向量数据库就是专门用于存储向量的数据库,它能够高效地存储和检索向量数据。

向量数据库提供了一种高效的方式来存储和检索向量数据。基于向量相似度搜索(Similarity Search),而不是基于精确匹配或传统的关键词搜索,能够更好地捕捉语义的相似性。

1.2 向量数据库的工作原理

在这里插入图片描述

根据上图,我们可以理解向量数据库的工作流程:

  1. 文档嵌入:将文档转换成向量
  2. 索引构建:将向量存储到向量数据库中,建立索引
  3. 查询嵌入:将用户查询转换成向量
  4. 相似度搜索:在向量数据库中搜索与查询向量最相似的文档向量
  5. 返回结果:返回最相似的文档

1.3 向量数据库的核心功能

向量数据库提供了两个主要功能:数据存储相似度搜索

① 数据存储

这部分涉及到如何将向量数据高效地存储到数据库中,而且还需要考虑到数据的持久化、索引的构建等问题。

常见的存储方式

  • 内存存储:数据存储在内存中,速度快但不持久化
  • 磁盘存储:数据存储在磁盘上,持久化但速度相对较慢

索引方法

这部分涉及到如何构建索引以加速相似度搜索。可以下面这些方法来加速搜索,而且还需要考虑到索引的构建时间、内存占用等问题。对于,正如前面所说的,我们需要在速度和精度之间做出权衡。

常见的方法有近似最近邻搜索(ANN)算法:如 FAISS 向量数据库,它能提供高效的向量搜索能力,并且支持多种索引方法。

常见的方法有近似最近邻(ANN)搜索:如 FAISS 向量数据库,它能提供高效的向量搜索能力,并且支持多种索引方法。它不会遍历所有向量,而是通过索引结构(如树结构、图结构),快速定位到可能相似的向量区域,然后在这些区域中进行精确搜索。这样可以大大减少搜索时间,但可能会牺牲一定的精度。

这就像在一个大图书馆里找书,你不会从第一本书开始一本一本地翻,而是先根据分类(文学、科学、历史等)找到大致的区域,然后在这个区域里找到具体的书。这样可以大大减少搜索时间,但可能会错过一些相关的书。

重要提示

  • 基于向量的相似度搜索(如 ANN 搜索)通常比传统的关键词搜索更能捕捉语义相似性
  • 但它也可能返回一些看似相关但实际上不太相关的结果

在这里插入图片描述 HNSW向量存储原理:

👉               HNSW 的物理结构:多层地图                 👈

HNSW 将所有的向量点分成了好几层,每一层都是一张“图”,点与点之间有连线。

  • 顶层(Layer L): 只有极少数“精英”点。它们距离非常远。就像全国地图,只标出北京、上海、广州。

  • 中间层: 点的数量逐渐增多。就像省际地图,标出了所有的地级市。

  • 底层(Layer 0): 包含了数据库里所有的向量点。就像街道地图,密密麻麻地标出了每一栋建筑。

② 向量相似度计算优化

向量相似度计算是向量数据库的核心功能之一。如 FAISS 向量数据库,它能提供高效的向量搜索能力,并且支持多种索引方法。

这些都是为了提高 CPU 和 SIMD 指令集的利用率,从而加速向量相似度计算。这些优化技术可以大大提高向量搜索的速度,使得向量数据库能够处理大规模的向量数据。

一个简单的例子,可以把向量看成是一排一排的数字(又称"向量化操作"),而不是逐个计算相似度,这样可以大大提高计算速度。

在这里插入图片描述 举个例子:

1. 数据准备

  • 目标 (Q): [0.9, 0.1, 0.0] (想找“偏红”的内容)
  • 候选库:
    • 🅰️ [1.0, 0.0, 0.0] (纯红)
    • 🅱️ [0.0, 1.0, 0.0] (纯绿)
    • 🅲 [0.8, 0.2, 0.0] (大红微绿)
    • 🅳 [0.0, 0.0, 1.0] (纯蓝)

2. 核心动作 (SIMD 并行计算) CPU 同时计算 Q 与所有候选的匹配度(点积):

  • 🅰️: 0.9×1+=0.90.9\times1 + \dots = \mathbf{0.9}
  • 🅱️: 0.9×0+=0.10.9\times0 + \dots = \mathbf{0.1}
  • 🅲: 0.9×0.8+=0.740.9\times0.8 + \dots = \mathbf{0.74}
  • 🅳: 0.9×0+=0.00.9\times0 + \dots = \mathbf{0.0}

3. 最终结果

  • 得分榜: [0.9, 0.1, 0.74, 0.0]
  • 冠军: 🅰️ (0.9分)
  • 结论: 系统瞬间返回 文档 A

关键点:不是算完 A 再算 B,而是像“多车道高速公路”一样,一次性算出所有结果,这就是 SIMD 加速的精髓。

③ 数据管理功能

现代向量数据库通常还会提供一些数据管理功能,以便更好地管理向量数据:

  • CRUD 操作:支持增删改查,可以动态更新向量数据库
  • 元数据过滤:除了向量本身,还可以存储元数据(Metadata),比如时间戳、作者、分类等。通过元数据过滤,可以在搜索时进一步筛选结果(例如"找出2023年发布的、关于机器学习的文档"),再结合向量相似度搜索,可以得到更精确的结果
  • 可扩展性和高可用性:支持分布式部署和水平扩展,适应海量数据场景。同时提供数据备份、容错机制,不会轻易丢失数据
  • 集成方便:很多向量数据库(如 Chroma、Weaviate、Pinecone、Qdrant、Milvus等)都提供了 Python SDK 和 RESTful API,可以方便地与 LangChain 等框架集成

理想上,我们可以利用向量数据库的这些功能,构建一个高效、可扩展的 RAG 系统。从而提供更好的搜索体验,让用户能够快速找到相关的文档。

下面我们介绍如何使用 LangChain 提供的向量数据库,包括如何创建向量数据库、如何进行相似度搜索等。更多 LangChain 支持的向量数据库可以参考官方文档,除了常见的上面提到的向量数据库,其实还有很多其他的向量数据库可以选择。


2. 内存存储

我们将使用 LangChain 的 InMemoryVectorStore 类实现向量的内存存储。

2.1 初始化

from langchain_core.vectorstores import InMemoryVectorStore

# 初始化内存向量存储
vector_store = InMemoryVectorStore(embeddings)

2.2 向量搜索

如果我们传入一个查询,向量存储将嵌入该查询,在所有嵌入的文档中执行相似性搜索,并返回最相似的文档。

这体现了两个重要的概念:

  1. 首先,需要有一种方法来衡量查询与任何嵌入文档之间的相似性
  2. 其次,需要有一种算法能够高效地在所有嵌入文档中执行这种相似性搜索

对于【相似性】这一点,之前已经讲过,再来回顾下:

  • 欧氏距离(Euclidean Distance):就是我们高中几何学的两点之间的直线距离。距离越短,相似度越高
  • 余弦相似度(Cosine Similarity):它忽略向量的绝对长度(大小),只关注两个向量在方向上的差异。在文本和语义的世界里,"方向"代表"含义",而"长度"往往只代表"文本的长度"或"词汇的多少"。换句话说,余弦相似度关注的是"你们是否指向同一个方向"/"你们是否代表同一个含义"

因此,在捕捉语义上的相似性上,余弦相似度是更常用的度量方式。

对于【相似性搜索】则可以通过向量存储提供的搜索方法实现。

2.2.1 相似性搜索

想要获取根据相似性搜索的结果,即嵌入单个查询,并查找相似的文档,并将它们作为文档列表返回。这可以使用 similarity_search 方法来实现。说明:InMemoryVectorStore 是根据【余弦相似度】来进行相似性搜索的

2.3 完整代码示例

# 嵌入模型生成向量(通过md文件按照token进行切分得到chunk也就是document,在进行转化成向量)
# 导入谷歌的嵌入模型
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import Chroma

import tiktoken
from langchain_text_splitters import CharacterTextSplitter
from langchain_community.document_loaders.markdown import UnstructuredMarkdownLoader

# 导入内存向量存储
from langchain_core.vectorstores import InMemoryVectorStore

# 加载文档(让 Markdown 按结构解析(标题、段落、列表分开))
loader = UnstructuredMarkdownLoader("test.md", mode="elements")
documents = loader.load()

# 设置基于token的文本分割器
splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base",
    chunk_size=120,
    chunk_overlap=50,  # 文档重叠长度
)

# 原始文本(一个document的page_content) → 编码成 token 序列 → 滑动窗口切分 → 解码成 chunk
# [0,1,...,119][70,...,189][140,...,299] → "chunk1" "chunk2" "chunk3"
# 分割文档
chunks = splitter.split_documents(documents)

# 初始化嵌入模型
embeddings = GoogleGenerativeAIEmbeddings(model="gemini-embedding-001")

# 创建内存向量存储
vectorstore = InMemoryVectorStore(embeddings)
ids = vectorstore.add_documents(chunks)

# 打印对应id的前三个
print(ids[:3])

print("----------------------------------------")
# 根据id查询文档内容
docs = vectorstore.get_by_ids(ids[:3])
print(docs)
print("----------------------------------------")

# 删除id
vectorstore.delete(ids[:3])

print(vectorstore.get_by_ids(ids[:3]))

print("----------------------------------------")

# 相似性搜索
docs = vectorstore.similarity_search("什么是 Claude Skills?")
print(docs)

print("----------------------------------------")

# 写一个filter函数,关于元数据的source字段是test.md的文档
def filter_by_source(documents):
    return documents.metadata["source"] == "test.md"

# 元数据过滤
docs = vectorstore.similarity_search(query="什么是 Claude Skills?", k=2, filter=filter_by_source)
print(docs)

print("----------------------------------------")

2.4 代码详解

步骤1:加载和分割文档

# 加载 Markdown 文档
loader = UnstructuredMarkdownLoader("test.md", mode="elements")
documents = loader.load()

# 使用 Token 分割器切分文档
splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base",
    chunk_size=120,
    chunk_overlap=50,
)
chunks = splitter.split_documents(documents)

说明

  • mode="elements":按元素(标题、段落、列表)分开加载
  • chunk_size=120:每块最多 120 个 token
  • chunk_overlap=50:相邻块重叠 50 个 token

步骤2:初始化嵌入模型和向量存储

# 初始化 Google Gemini 嵌入模型
embeddings = GoogleGenerativeAIEmbeddings(model="gemini-embedding-001")

# 创建内存向量存储
vectorstore = InMemoryVectorStore(embeddings)

说明

  • InMemoryVectorStore:内存向量存储,数据存储在内存中
  • 程序结束后数据会丢失,适合测试场景

步骤3:添加文档并获取ID

# 添加文档到向量存储,返回文档ID列表
ids = vectorstore.add_documents(chunks)

# 打印前三个文档的ID
print(ids[:3])

输出示例

['doc_001', 'doc_002', 'doc_003']

说明

  • add_documents() 会自动调用 embeddings.embed_documents() 将文档转换为向量
  • 返回每个文档的唯一ID,用于后续的查询和删除操作

步骤4:根据ID查询文档

# 根据ID查询文档内容
docs = vectorstore.get_by_ids(ids[:3])
print(docs)

输出示例

[Document(page_content='Claude Skills 是...', metadata={'source': 'test.md'}), Document(page_content='Skills 可以...', metadata={'source': 'test.md'}), Document(page_content='使用 Skills...', metadata={'source': 'test.md'})]

说明

  • get_by_ids():根据文档ID列表查询文档内容
  • 返回 List[Document] 对象列表

步骤5:删除文档

# 删除指定ID的文档
vectorstore.delete(ids[:3])

# 验证删除结果(应该返回空列表)
print(vectorstore.get_by_ids(ids[:3]))

输出示例

[]

说明

  • delete():根据ID删除文档
  • 删除后再查询会返回空列表

步骤6:相似性搜索

# 进行相似度搜索
docs = vectorstore.similarity_search("什么是 Claude Skills?")
print(docs)

工作流程

  1. 自动调用 embeddings.embed_query() 将查询转换为向量
  2. 在向量数据库中搜索与查询向量最相似的文档向量
  3. 默认返回最相似的 4 个文档

输出示例

[Document(page_content='Claude Skills 是一种扩展 Claude 能力的方式...'), Document(page_content='Skills 可以让 Claude 执行特定的任务...'), Document(page_content='使用 Skills 可以提高工作效率...'), Document(page_content='Skills 支持多种编程语言...')]

步骤7:元数据过滤搜索

# 定义过滤函数
def filter_by_source(documents):
    return documents.metadata["source"] == "test.md"

# 带过滤条件的相似度搜索
docs = vectorstore.similarity_search(
    query="什么是 Claude Skills?",
    k=2,
    filter=filter_by_source
)
print(docs)

参数说明

  • query:查询字符串
  • k=2:返回 2 个最相似的文档
  • filter:过滤函数,只返回满足条件的文档

输出示例

[Document(page_content='Claude Skills 是...', metadata={'source': 'test.md'}), Document(page_content='Skills 可以...', metadata={'source': 'test.md'})]

说明

  • 元数据过滤可以在相似度搜索的基础上进一步筛选结果
  • 例如:只搜索特定来源、特定时间、特定作者的文档

2.5 向量存储的核心操作总结

操作方法说明
添加文档add_documents(chunks)将文档转换为向量并存储,返回ID列表
查询文档get_by_ids(ids)根据ID查询文档内容
删除文档delete(ids)根据ID删除文档
相似度搜索similarity_search(query, k)搜索最相似的k个文档
过滤搜索similarity_search(query, k, filter)带元数据过滤的相似度搜索

3. 持久化存储(Chroma)

3.1 为什么需要持久化存储?

InMemoryVectorStore 的缺点:

  • 数据存储在内存中,程序结束后数据会丢失
  • 不适合大规模数据
  • 每次运行都需要重新加载和嵌入文档

Chroma 是一个开源的向量数据库,支持持久化存储:

  • 数据存储在磁盘上,程序结束后数据不会丢失
  • 支持大规模数据
  • 只需要加载一次,后续可以直接使用

3.2 使用 Chroma

from langchain_community.vectorstores import Chroma

# 初始化 Chroma 向量存储(持久化到磁盘)
vector_store = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"  # 持久化目录
)

# 进行相似度搜索
results = vector_store.similarity_search(query, k=3)

参数说明

  • documents:要存储的文档列表
  • embedding:嵌入模型
  • persist_directory:持久化目录路径

3.3 加载已有的 Chroma 数据库

# 加载已有的 Chroma 数据库
vector_store = Chroma(
    persist_directory="./chroma_db",
    embedding_function=embeddings
)

# 直接进行搜索,无需重新加载文档
results = vector_store.similarity_search(query, k=3)

4. 相似度搜索方法对比

4.1 similarity_search()

基本的相似度搜索,返回最相似的文档。

results = vector_store.similarity_search(query, k=3)

返回List[Document]

4.2 similarity_search_with_score()

带分数的相似度搜索,返回文档和相似度分数。

results = vector_store.similarity_search_with_score(query, k=3)

for doc, score in results:
    print(f"相似度分数: {score}")
    print(f"文档内容: {doc.page_content}")

返回List[Tuple[Document, float]]

相似度分数说明

  • 分数越低,相似度越高(距离越近)
  • 分数范围取决于使用的相似度度量方法

4.3 max_marginal_relevance_search()

最大边际相关性搜索,在相似度和多样性之间取得平衡。

results = vector_store.max_marginal_relevance_search(query, k=3)

特点

  • 返回的文档既相似又多样化
  • 避免返回过于相似的重复文档

5. 总结

5.1 核心要点

  1. 向量存储是 RAG 系统的核心组件,用于存储和检索向量数据
  2. InMemoryVectorStore 适合小规模数据和测试
  3. Chroma 支持持久化存储,适合生产环境
  4. 相似度搜索基于余弦相似度,能够找到语义相关的文档
  5. add_documents() 自动调用 embed_documents() 进行向量化
  6. similarity_search() 自动调用 embed_query() 进行查询向量化

5.2 完整工作流程

文档加载 → 文本分割 → 向量嵌入 → 向量存储 → 相似度搜索 → 返回结果
   ↓          ↓          ↓          ↓          ↓          ↓
 Loader   Splitter  Embeddings  VectorStore  Query    Documents

6. 常用向量数据库对比

6.1 InMemoryVectorStore(内存向量存储)

特点

  • 数据存储在内存中
  • 速度最快
  • 不持久化(程序关闭后数据丢失)
  • 适合小规模数据和测试

使用场景

  • 快速原型开发
  • 小规模数据集
  • 临时测试

6.2 Chroma

特点

  • 开源向量数据库
  • 支持本地持久化
  • 轻量级,易于使用
  • 支持元数据过滤

使用场景

  • 中小规模应用
  • 本地开发和测试
  • 需要持久化存储

代码示例

from langchain_community.vectorstores import Chroma

vector_store = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"  # 持久化目录
)

6.3 Pinecone

特点

  • 云端托管向量数据库
  • 高性能、可扩展
  • 支持大规模数据
  • 需要 API Key

使用场景

  • 生产环境
  • 大规模数据
  • 需要高可用性

6.4 FAISS

特点

  • Facebook 开源
  • 高性能相似度搜索
  • 支持多种索引方法
  • 适合大规模数据

使用场景

  • 大规模向量搜索
  • 需要高性能
  • 本地部署

6.5 Redis(RedisSearch 向量数据库)

特点

  • 基于 Redis 生态,利用 RedisSearch 模块实现向量存储与搜索
  • 支持内存存储,性能极高
  • 支持持久化(RDB/AOF)
  • 原生支持向量索引(HNSW 算法)
  • 支持元数据过滤与混合查询(向量+标量)
  • 兼容 Redis 生态,易于集成

使用场景

  • 需要极高性能的实时向量检索
  • 已有 Redis 基础设施的项目
  • 需要结合缓存与向量搜索的场景
  • 中小到大规模数据(取决于 Redis 内存容量)

代码示例

from langchain_community.vectorstores import Redis
from langchain_community.embeddings import GoogleGenerativeAIEmbeddings

# 初始化嵌入模型
embeddings = GoogleGenerativeAIEmbeddings(model="gemini-embedding-001")

# 创建 Redis 向量存储
vector_store = Redis.from_documents(
    documents=chunks,
    embedding=embeddings,
    redis_url="redis://localhost:6379",
    index_name="my_vector_index",  # RedisSearch 索引名
)

6.6 对比表格

特性InMemoryVectorStoreChromaPineconeFAISSRedis
持久化
云端托管❌(可自托管)
性能非常快非常快
易用性简单简单中等复杂中等
成本免费免费付费免费免费(自托管)
适用规模中~大
混合查询支持
生态集成独立云服务独立Redis 生态

6.7 选型总结

在选择向量存储方案时,需综合考虑数据规模、性能需求、持久化要求、成本预算及开发复杂度

  • 快速验证与小数据:优先选 InMemoryVectorStore(内存快但易失)或 Chroma(轻量且支持本地持久化)。
  • 生产级高可用:Pinecone(云端托管,省心但付费)或 Redis(自托管灵活,生态强大)。
  • 极致性能与可控性:FAISS(本地高性能搜索,适合技术团队自主优化)。
  • 已有 Redis 基础设施:直接使用 RedisSearch 向量模块,低成本集成向量检索与缓存能力。

7. 总结

7.1 核心要点

  1. 向量存储是 RAG 系统的核心组件
  2. 相似度搜索基于向量距离计算(余弦相似度、欧氏距离)
  3. InMemoryVectorStore 适合快速开发和测试
  4. 生产环境建议使用 Chroma、Pinecone 等持久化数据库

7.2 完整工作流程

文档加载 → 文本分割 → 向量嵌入 → 向量存储 → 相似度搜索 → 返回结果