大模型之外,向量存储如何支撑 Agent 的检索链路
大模型负责理解、推理和生成,而 Agent 不止于此。对于一个能够完成真实任务的 Agent 而言,还需要获取外部数据、保留历史经验,更需从文档、图片、音视频和代码中找到当前任务需要的信息。但是,上下文窗口再大,也不适合在每次任务中装入全部数据,对此,更合适的做法是把数据放入模型之外,执行任务时只取回少量相关内容。
而在上述链路中,向量存储是不可或缺的架构之一。它的工作原理并不复杂,先将数据转化为语义向量保存下来,再在 Agent 执行任务时通过相似度查询找回与当前任务最相关的内容。然而随着写入向量存储的数据不断增长,挑战会逐级展开。起初,问题只是能否准确查询。数据量上来后,重心很快转向数据如何组织、索引如何取舍。而当 Agent 数量继续增加,问题进一步变成系统如何扩展,以及如何按需创建和管理大量数据空间。
然而当前对于向量存储的讨论寥寥,但其重要性不言而喻,向量存储正在从底层技术组件变成 Agent 平台的基础能力。这篇文章想从 Agent 的实际需求出发,谈谈我们对向量存储技术方向的一些思考,也分享我们在这个方向上的具体做法。重点会放在三种主流索引实现的取舍、混合检索和分布式架构的设计思路上。选型部分除了技术因素,还会聊聊 Serverless 这个容易被忽略但越来越重要的维度,最后通过伪代码演示阿里云两种 Serverless 向量存储的基本用法。
一、向量存储在 Agent 中承担什么角色
其实,向量存储并不是所有 Agent 的必选组件。当数据量很小、内容能够完整放入上下文,或者查询总能通过确定主键完成时,文件和普通数据库就已够用。
因而,向量存储主要处理另一类问题,即当数据规模持续增长,查询者又不知道准确位置和关键词,只能根据内容含义寻找结果时,向量存储粉墨登场。
其中,在需要处理外部知识和跨任务记忆的 Agent 架构里,向量存储承担两个角色:检索和记忆。
图 1:向量存储在 Agent 中的位置
先看检索。Agent 经常要为当前任务补充外部数据,因为模型参数不包含企业的全部私有数据,也跟不上持续变化的业务信息。也就是说,模型知道的远远不够。回答产品问题、分析代码或处理业务任务时,都需要先找到相关文档和业务记录,再放入当前上下文。数据量小的时候还好,一旦多起来,难点就变成了找出真正相关的部分。而且用户问题和原始资料经常使用不同表达,例如用户说“升级后看不到以前的审批单”,文档中写的可能是“版本迁移后历史流程的可见范围发生变化”。
这正是语义相似性要解决的问题。Embedding 模型把问题和文档转换为向量之后,语义接近的内容在向量空间中距离更近,向量存储就能据此找回相关资料,这也是知识库和 RAG(Retrieval-Augmented Generation,检索增强生成)使用向量检索的主要原因。而且不只有文本能用这个机制,图片、音视频和代码也可以通过模型生成向量,经过一次解析,提取出的语义就能被不同任务反复检索。
再看记忆。长期经验需要在后续任务中被重新找到和使用,因为历史对话、执行结果和用户反馈都可能在未来继续发挥作用。换句话说,Agent 需要"记住"过去。如果没有长期记忆,每次都要重新了解用户和业务,也没法利用过去的成功或失败经验。那用文件存行不行?当然,文件也能保存一些规则和摘要,适合数量有限、路径明确的情况。但历史记录持续增长之后,这种方式就扛不住了。Agent 通常不知道目标记录的文件名和产生时间,新旧任务的表述也未必一致,逐个读取文件既慢,也会占用大量上下文。换个思路,把历史对话和执行结果生成向量后,Agent 就可以根据当前任务找回语义相关的记忆,比如运维 Agent 可以用当前故障现象检索过去症状相似的处理记录。说白了,向量存储在这里就是记忆的搜索引擎。
这两个场景说到底是一回事:Agent 要从持续增长的数据中找出与当前任务语义相关的内容。向量存储承担的就是这个角色。
二、向量存储如何进行基本实现
向量存储不只是存向量。一个完整的向量存储系统要同时管理向量、文本和元数据,写入和查询都围绕这三类数据展开。
一条可检索的记录通常包括向量、原文或摘要,以及租户、来源、时间、权限等元数据。Embedding 表达的是语义,Agent 最终使用的仍然是原文或摘要;检索时也要根据租户、权限等属性判断结果能否使用。这些内容既可以与向量放在同一个系统中,也可以通过唯一标识关联外部数据。
数据结构会直接影响查询能力。例如,“在当前租户最近三个月发布的文档中,查找与审批权限有关的说明”既包含语义,也包含明确的租户和时间条件,系统需要把向量检索、全文检索和元数据过滤放进同一条查询链路,才能返回既相关又满足业务约束的结果。
写入时,系统接收向量、文本和元数据,按照分片规则保存数据并更新索引。查询时,系统解析向量、关键词和过滤条件,将请求发送到相关分片,各分片产生候选结果,再完成过滤、合并和排序。
图 2:向量存储的基本架构与读写路径
这个过程中有三项技术直接影响系统的能力边界:
|
重点内容 |
解决的问题 |
为什么重要 |
|
向量索引 |
如何避免扫描全部向量,并找到相似结果 |
直接影响召回率、延迟、成本和数据规模 |
|
混合检索 |
如何组合语义、关键词和业务条件 |
决定结果能否同时满足相关性和业务约束 |
|
分布式架构 |
如何突破单机容量和吞吐限制 |
决定单个数据空间能够扩展到多大 |
2.1 向量索引:从全量计算到近似搜索
向量检索需要先确定距离或相似度度量,常见的有欧氏距离、余弦相似度和内积。确定了度量方式,就可以衡量任意两个向量的远近。向量数量较少时,可以逐一计算查询向量与全部数据的距离,再返回最相似的 K 条结果。但数据量一上来,全量计算的开销线性增长,很快就无法满足在线查询。
所以实际系统通常使用 ANN(Approximate Nearest Neighbor,近似最近邻)索引,只搜索更可能包含近邻的部分数据。代价是可能漏掉真实近邻。评价一种索引,主要看召回率、查询性能、资源成本和数据规模。这里的召回率,是近似索引返回的 TopK 与精确检索 TopK 的重合程度。
目前主流上没有一种索引能在这些指标上同时做到最好,核心区别在于搜索路径和主要资源消耗。常见路线有三类:内存图索引、磁盘图索引和聚类分区索引。HNSW、DiskANN 和 IVF 分别是三类路线中最常见的代表实现,基本覆盖了通用 CPU 向量检索中的主要负载。

图 3:主流向量索引路线及其代表算法
HNSW:内存中的分层图
HNSW 把向量组织成多层邻接图。上层节点少、连接跨度大,用来快速接近目标区域;底层节点多、连接更密,用来完成局部搜索。查询从上层入口开始,沿着更接近查询向量的邻居逐层下降,最后在底层扩展候选集合并返回结果。

图 4:HNSW 的分层图与查询路径
HNSW 用内存换取低延迟和高召回率。图中的每个节点除了保存向量,还要维护多层邻接关系;查询又会沿图进行多次不规则访问,因此实际系统通常让图结构和查询所需向量常驻内存。随着向量数量和维度增加,内存占用也会持续增长。
HNSW 适合查询频繁、延迟要求高,并且能够为索引提供足够内存的场景。查询时扩大底层候选集合可以提高召回率,但会访问更多节点,延迟也会随之变化。
DiskANN:运行在 SSD 上的图索引
DiskANN 同样沿邻接图搜索,但图结构和查询过程针对 SSD 做了优化。典型实现会在内存中保留量化后的向量、入口点和热点信息,把完整向量及主要图数据放在 SSD 上。查询先利用内存中的近似信息筛选候选节点,再并行读取 SSD 中的邻居,最后使用完整向量计算精确距离。

图 5:DiskANN 的内存、SSD 与查询过程
DiskANN 用 SSD 承载主要索引数据,用较少内存维持在线查询。量化后的向量占用空间更小,适合留在内存中做候选筛选;只有进入候选集合的节点才需要读取完整数据。这样可以在保持较高召回率和在线性能的同时承载更大的数据规模,但查询性能会受到 SSD 随机访问、缓存命中率和单次读取量的影响。
DiskANN 适合规模较大、仍然要求稳定在线查询的场景。它降低了内存压力,同时把更多工程复杂度放在磁盘访问、缓存和并发调度上。
IVF:聚类分区和局部扫描
IVF(Inverted File Index,倒排文件索引)先训练一组聚类中心,再把每个向量分配到距离最近的分区。查询时,系统先找出与查询向量最接近的几个聚类中心,只在对应分区内计算向量距离。一次查询搜索的分区数量通常称为 nprobe。

图 6:IVF 的聚类分区与查询过程
IVF 用分区缩小搜索范围,再用扫描量调节召回率和成本。索引元数据较小,分区内的数据也适合批量读取,因此更容易利用大容量、低成本的存储。增加 nprobe 会搜索更多分区,召回率通常更高,扫描量和延迟也随之增加;减少 nprobe 则会降低查询成本,但可能漏掉其他分区中的近邻。
IVF 与图索引最明显的区别是查询路径:图索引沿邻居关系逐步搜索,IVF 先选分区,再扫描分区内的数据。IVF 适合数据规模大、成本敏感的场景。
三类索引的差异可以归纳如下:
|
索引路线 |
数据组织 |
查询路径 |
主要资源 |
适合的场景 |
|
内存图索引(HNSW) |
分层邻接图 |
从稀疏上层进入,在底层扩展候选 |
内存 |
低延迟、高召回率,索引能够放入内存 |
|
磁盘图索引(DiskANN) |
面向 SSD 优化的邻接图 |
图遍历结合缓存与 SSD 读取 |
内存与 SSD |
大规模、高召回率和稳定在线查询 |
|
聚类分区索引(IVF) |
聚类中心与多个数据分区 |
定位相近分区后扫描数据 |
存储与计算 |
超大规模、成本敏感、可调节扫描量 |
没有统一最优的索引。实际选型还要使用业务数据,在相同召回率目标下比较延迟、吞吐和成本。
2.2 混合检索:把语义和业务条件放进一条查询链路
混合检索的关键是候选如何产生、过滤在哪一步执行,以及结果怎样排序。一条真实查询通常同时包含向量、关键词和过滤条件:向量检索寻找语义相近的内容,全文检索匹配产品名、错误码等准确术语,过滤条件限制租户、权限和时间。只是在接口上同时支持这些条件,并不代表它们会以正确的方式共同参与查询。
过滤时机直接影响结果。假设先召回 20 条向量结果,再按租户和权限过滤,其中大部分结果可能被排除,最终返回数量不足;满足条件但没有进入最初 TopK 的内容,也不会再被找回。让过滤条件参与候选生成,系统就能在满足业务条件的数据中寻找近邻,但这要求向量索引本身能够高效处理过滤。

图 7:混合检索的两种常见执行方式
全文和向量检索的分数含义不同,不能直接相加。常见做法是先归一化各路分数,或者使用 RRF(Reciprocal Rank Fusion,倒数排名融合),根据结果在各路召回中的排名完成融合。RRF 不依赖不同引擎的原始分数尺度,在多系统召回中比较容易落地。
混合检索可以在一个查询引擎内完成,也可以由多个系统分别召回后在应用层合并。前者的查询链路更短,数据、权限和过滤规则容易保持一致;后者可以独立选择向量、全文和排序组件,但需要额外处理数据同步、调用延迟和结果融合。
2.3 分布式查询:分片执行和全局 TopK
分布式能力决定单个逻辑数据空间的容量和吞吐上限。单机容纳不下全部数据或查询流量时,系统会把数据和索引拆到多个分片。各分片并行查询并返回局部 TopK,协调节点再把这些结果合并成全局 TopK。

图 8:分布式向量查询的分片执行与全局 TopK 合并
分片数量增加后,一次查询的成本取决于需要访问多少分片。如果所有分片都可能包含近邻,请求通常要广播到整个索引;如果能够按租户或业务属性准确路由,就可以在查询前缩小范围。后者不仅减少计算量和网络开销,也降低了协调节点合并结果的压力。
扩容还会带来分片迁移和索引重建,系统需要在这些过程中保持查询稳定。因此,分布式向量存储不仅要把数据拆开,还要处理好查询路由、局部检索和全局结果合并。
三、向量存储选型不再只看检索和规模
向量存储的选型可以分成两个层面。第一个层面是检索能力和数据规模,也就是上一节讨论的三项技术怎么匹配业务场景,第二个层面是资源如何创建、交付和计费。过去大家主要关注前者,这很自然,检索能力和规模是最基本的门槛,但 Agent 应用正在让后者变得越来越重要。
检索能力和数据规模决定技术路线
上一节介绍了向量索引、混合检索和分布式查询的技术细节。不过了解技术是一回事,落到选型上是另一回事。每项都需要根据业务特点做取舍:
|
考虑因素 |
需要回答的问题 |
主要选择与取舍 |
|
向量索引 |
业务优先保证延迟、在线性能,还是海量数据下的单位成本 |
HNSW 面向高频、低延迟查询;DiskANN 面向大规模在线检索;IVF 面向海量、长尾数据 |
|
混合检索 |
向量、全文和过滤条件是否需要共同参与一次查询 |
单系统链路更短,数据更容易保持一致;多系统召回更灵活,但要处理数据同步和结果融合 |
|
数据规模 |
单机能否容纳数据并承担查询流量 |
主流产品基本都支持分布式,重点检查单个逻辑空间的规模、查询路由和扩容影响 |
这三个因素之间还会互相影响,不是选完一项再选下一项。譬如说,数据规模增长后,全内存 HNSW 的成本可能过高,而过滤条件复杂时,候选生成方式会直接影响召回结果,如果多系统召回则会增加数据同步和查询延迟,所以选型要从完整的数据和查询负载出发,不能只看单项性能。
Serverless 成为新的选择维度
如果说第一个层面回答的是“技术上怎么选”,第二个层面回答的是“资源怎么来”。资源交付本来不是新问题,传统数据库一直需要创建实例、规划容量和维护资源,但 Agent 应用改变了资源的使用模式。
具体来说,一个 Agent 平台可能按用户或任务动态创建大量数据空间,这些空间的负载差异很大:有的长期空闲,有的持续活跃,还有的只在短时间内产生突发访问。固定实例能够为持续高负载提供稳定资源,但很难经济地承载这种分散且变化频繁的负载。换句话说,Agent 需要的不是一个更大的实例,而是一种能跟着负载走的交付方式。
Serverless 的做法是让用户只管理逻辑数据空间,不必规划和维护固定资源,系统根据负载自动扩缩容,按实际使用量计费。Agent 平台可以通过 API 创建和使用存储,不需要等待研发人员选择规格或执行扩容。低频空间不会长期占用实例,突发查询也不依赖人工调整容量。阿里云在表格存储和对象存储中也提供了类似的能力,Tablestore SearchIndex 面向大规模在线查询和混合检索,OSS Vectors 以 Vector Bucket 组织向量及元数据。
四、阿里云怎么做 Serverless 向量存储
阿里云分别在表格存储和对象存储中提供 Serverless 向量能力,两种产品面向不同的查询场景。
Tablestore SearchIndex 把向量检索放入多元索引,向量、全文和业务字段可以在一次查询中组合,适合需要混合检索的在线查询。OSS Vectors 通过 Vector Bucket 和 Vector Index 管理向量及元数据,并与普通 OSS Bucket 中的原始内容关联,适合海量、长尾的向量数据。两种服务都通过 API 管理逻辑资源,用户不需要部署和维护向量检索集群。
下面先介绍资源模型和创建流程,再用伪代码演示写入与查询。示例省略鉴权、异常处理和部分配置参数,接口名称只用于表达操作过程。
产品与资源模型
Serverless 向量存储把资源管理边界放在逻辑数据空间,而不是服务器和分片。 在 Tablestore 中,向量能力依附于数据表和 SearchIndex:数据表保存一行完整数据,SearchIndex 为向量、全文和标量字段建立索引。在 OSS Vectors 中,Vector Bucket 是顶层资源,一个 Bucket 可以包含多个 Vector Index,每个 Index 保存向量记录及元数据。原始文档、图片和视频仍然可以放在普通 OSS Bucket 中,通过 object_key 等字段与向量记录关联。

图 9:阿里云 Serverless 向量存储的资源模型
Agent 平台需要保存业务空间与云资源之间的映射,例如某个租户使用哪张表、哪个 SearchIndex 或 Vector Index。多个小规模租户可以共享索引,并通过 tenant_id 过滤;隔离要求较高或数据规模较大的业务,也可以使用独立索引。底层索引构建、分片、扩缩容和故障恢复由服务完成,应用只处理逻辑资源及其生命周期。
创建和管理资源
Tablestore SearchIndex 的资源层次是实例、数据表和多元索引。实例提供逻辑命名空间,数据表保存原始行,SearchIndex 定义需要检索的字段。创建向量字段时,需要指定维度和距离度量。
tablestore_client.create_table(
TableMeta("knowledge", [("id", "STRING")]),
TableOptions(),
ReservedThroughput(CapacityUnit(0, 0)),
)
index_meta = SearchIndexMeta([
FieldSchema("vector", FieldType.VECTOR, vector_options=VectorOptions(
VectorDataType.VD_FLOAT_32, VectorMetricType.VM_COSINE, 1024)),
FieldSchema("text", FieldType.TEXT, index=True, store=True),
FieldSchema("tenant_id", FieldType.KEYWORD, index=True, store=True),
FieldSchema("document_id", FieldType.KEYWORD, index=True, store=True),
FieldSchema("embedding_version", FieldType.KEYWORD, index=True, store=True),
])
tablestore_client.create_search_index("knowledge", "knowledge-search", index_meta)
OSS Vectors 的资源层次更直接:先创建 Vector Bucket,再创建 Vector Index。向量维度和距离度量同样在创建 Index 时确定。
vector_client.put_vector_bucket(PutVectorBucketRequest(
bucket="agent-vector-data"
))
vector_client.put_vector_index(
PutVectorIndexRequest(
bucket="agent-vector-data",
index_name="document-index",
dimension=1024,
data_type="float32",
distance_metric="cosine",
)
)
向量维度和距离度量必须与 Embedding 模型保持一致。模型变化导致维度或向量空间变化时,通常需要创建新索引并重新生成向量,再把查询切换到新版本。Agent 平台可以通过 API 完成索引的创建、查询和删除,但不必管理底层节点和分片。
准备向量数据
写入向量存储前,先对文档、图片或视频进行解析和切分,再通过 Embedding 模型生成向量。记录中除了向量,还要保存文本、租户、原始内容标识和模型版本。后续的过滤、版本切换和原始内容回查,都依赖这些字段。
text = parse_and_split(source)[0]
vector = list(embedding_model.encode(text))
record = {
"id": "chunk-001",
"vector": vector,
"text": text,
"tenant_id": "tenant-a",
"document_id": "document-001",
"embedding_version": "model-v1",
}
写入和查询使用相同的 Embedding 模型、向量维度和距离度量。tenant_id 限制查询范围,document_id 定位原始内容,embedding_version 区分不同模型生成的向量。实际应用还可以根据业务增加权限、时间和内容类型等字段。
Tablestore SearchIndex:写入和查询
在 Tablestore 中,向量、文本和业务属性作为同一行写入数据表,多元索引为已经配置的字段建立索引。数据表仍然可以按主键读写,SearchIndex 则负责向量、全文和条件查询。
tablestore_client.put_row("knowledge", Row(
[("id", record["id"])],
[
("vector", json.dumps(record["vector"])),
("text", record["text"]),
("tenant_id", record["tenant_id"]),
("document_id", record["document_id"]),
("embedding_version", record["embedding_version"]),
],
))
只执行向量检索时,查询中指定向量字段、查询向量和 TopK。服务通过磁盘图索引产生候选结果,并按相似度返回最接近的记录。
query_vector = list(embedding_model.encode(question))
query = KnnVectorQuery("vector", top_k=20,
float32_query_vector=query_vector)
results = tablestore_client.search(
"knowledge",
"knowledge-search",
SearchQuery(
query,
sort=Sort([ScoreSort(sort_order=SortOrder.DESC)]),
limit=20,
get_total_count=False,
),
ColumnsToGet(return_type=ColumnReturnType.ALL),
)
需要同时使用全文和业务条件时,可以把这些条件放入同一次查询。下面的查询先限定租户和模型版本,再匹配文本中的关键词,并在符合条件的数据中执行向量检索。
filter_query = BoolQuery(
filter_queries=[
MatchQuery("text", keywords),
TermQuery("tenant_id", "tenant-a"),
TermQuery("embedding_version", "model-v1"),
]
)
query = KnnVectorQuery("vector", top_k=50,
float32_query_vector=query_vector, filter=filter_query)
results = tablestore_client.search(
"knowledge",
"knowledge-search",
SearchQuery(
query,
sort=Sort([ScoreSort(sort_order=SortOrder.DESC)]),
limit=10,
get_total_count=False,
),
ColumnsToGet(return_type=ColumnReturnType.ALL),
)
这里的 filter 参与向量候选生成,可以先缩小数据范围再执行近邻搜索。如果把条件放在向量 TopK 之后执行,大量候选被过滤时,最终结果数量可能不足。
OSS Vectors:写入和查询
原始文档、图片或视频可以保存在普通 OSS Bucket 中,向量和元数据写入 Vector Index。向量记录通过 object_key 关联原始对象,因此查询结果不需要保存完整内容,也能在召回后读取原文或媒体文件。
object_key = "documents/document-001"
oss_client.put_object(
PutObjectRequest(bucket="agent-source-data", key=object_key, body=source)
)
vector_client.put_vectors(
PutVectorsRequest(
bucket="agent-vector-data",
index_name="document-index",
vectors=[{
"key": record["id"],
"data": {"float32": record["vector"]},
"metadata": {
"tenant_id": record["tenant_id"],
"document_id": record["document_id"],
"object_key": object_key,
"embedding_version": record["embedding_version"],
},
}],
)
)
查询时使用同一模型生成查询向量,并根据租户和模型版本过滤结果。return_metadata 用于返回原始对象位置等业务信息。
query_vector = list(embedding_model.encode(question))
results = vector_client.query_vectors(
QueryVectorsRequest(
bucket="agent-vector-data",
index_name="document-index",
query_vector={"float32": query_vector},
top_k=20,
filter={
"$and": [
{"tenant_id": {"$eq": "tenant-a"}},
{"embedding_version": {"$eq": "model-v1"}},
]
},
return_distance=True,
return_metadata=True,
)
)
检索结果包含向量标识、距离和元数据。应用根据 object_key 读取原始内容,再把相关片段交给 Agent 使用。
for result in results:
source = oss.get_object(
bucket = "agent-source-data",
key = result.metadata["object_key"]
)
当然,实际使用中还有几个容易踩的坑。写入和查询必须使用相同的 Embedding 模型、向量维度和距离度量,否则向量空间的语义关系就对不上。如果需要更换模型,应该用版本字段隔离新旧向量,等索引重建完成后再切换查询版本。多租户数据也一样,每次查询都要带上租户条件,少一次就可能返回跨租户的结果。向量记录还要保留原始数据标识,不然检索到了内容,却不知道它来自哪个文档、哪张图片、哪段视频。
最后,召回率和资源消耗受 TopK、过滤条件和数据分布共同影响,文档里写的和实际跑出来的经常有差距,必须用真实数据验证。尤其是 OSS Vectors 的标量过滤在结果阶段执行,如果过滤条件筛掉了大部分结果,TopK 设得再大,最终返回的数量也可能很少。
写在最后
向量存储解决的核心问题是语义检索,即从持续增长的数据中找出与当前任务相关的内容。但要让检索结果真正可用,需要同时处理好几件事:向量要和文本、元数据一起组织,查询链路要正确组合语义、关键词和业务条件,索引要在召回率、延迟和成本之间取舍。这些选择之间还会互相影响,不存在统一最优的方案,只能从具体的工作负载出发。向量存储要打通的最后一公里,不是算法本身再快多少,而是让散落在各处的数据真正被 Agent 用得上。
选型标准本身也在变化。过去主要看检索能力和数据规模,Agent 应用让资源交付方式也成了绕不开的维度。当大量数据空间由应用动态创建、负载模式不再可预测时,固定实例就不再是默认答案了。
这个趋势再往前走一步,索引算法和容量管理会逐步被云服务吸收。到那时,开发者的注意力会回到数据本身:怎么组织、怎么关联、怎么让 Agent 真正用得上。最终拼的不是谁选了更好的索引,而是谁的数据组织得更清楚。
更多推荐
所有评论(0)