大模型负责理解、推理和生成,而 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 真正用得上。最终拼的不是谁选了更好的索引,而是谁的数据组织得更清楚。

更多推荐