构建AI智能体:十五、超越关键词搜索:向量数据库如何解锁语义理解新纪元
一、前言
在如今的数字时代,数据的形式正以前所未有的速度变得多样化。文本、图片、音频、视频等非结构化数据占据了数据总量的80%以上。传统数据库(如MySQL)擅长处理“张三的年龄是25岁”这类结构化数据,但对“一张有夕阳、狗和海滩的图片”或“一篇讨论量子计算前景的文章”却无能为力。
我们可以轻松理解这些内容背后的含义和关联,但计算机需要一种方式来“理解”和“比较”它们。向量数据库(Vector Database) 就是专为解决这一问题而生的新型数据库,它是AI基础设施中至关重要的一环,被誉为AI应用的“长期记忆体”和“检索大脑”。
二、什么是向量数据库
首先需要回顾一下向量和Embedding的含义,具体可以参考章节《构建AI智能体:十二、给词语绘制地图:Embedding如何构建机器的认知空间
》
向量:在AI和机器学习领域,向量是一组数字的有序列表,可以表示任何数据对象(如一段文字、一张图片)在高维空间中的位置。
嵌入(Embedding):通过AI模型(如BERT、CNN、CLIP等)将非结构化数据转换为向量的过程,称为“嵌入”。这个转换过程捕获了数据的深层语义特征。
示例:单词“国王”通过模型转换后,可能得到一个包含300个数字的向量。在数学上,这个向量与“男人”、“女王”、“女人”的向量存在某种关系(如“国王” - “男人” + “女人” ≈ “女王”)。
向量数据库是一种专门用于存储、索引和查询高维向量的数据库。它的核心功能是执行近似最近邻(ANN)搜索,即快速找到与查询向量最相似的向量集合。
三、与传统数据库区别
特性 | 传统数据库 (SQL/NoSQL) | 向量数据库 |
数据模型 | 结构化数据(行/列)、文档、键值对 | 高维向量 |
查询方式 | 精确匹配(WHERE age = 25)、范围查询 | 相似性搜索(找到与这个图片最像的10张图片) |
索引技术 | B树、哈希索引等 | HNSW、IVF-PQ、LSH等专门为高维空间设计的ANN索引 |
核心能力 | 事务一致性、完整性 | 语义理解、相似性检索 |
适用场景 | 电商订单、用户信息管理等 | AI推荐、语义搜索、图像识别等 |
四、向量数据库的核心原理
在高维空间中(维度可达成百上千维),进行精确的最邻近搜索计算量极大,速度极慢。向量数据库使用近似最近邻(ANN)算法,在可接受的精度损失下,极大提升搜索速度。
-
索引(Indexing):数据库不会暴力比对所有向量,而是预先构建索引,将向量组织成一种易于搜索的结构。
-
HNSW:像构建一个多层的高速公路网络,先从顶层进行粗粒度搜索,再逐层细化,快速逼近目标。
-
IVF-PQ:先将向量空间聚类成多个“单元”,搜索时只找最可能的几个单元。再用产品量化技术压缩向量,减少计算和存储开销。
-
距离度量(Distance Metric):如何衡量两个向量的相似性?常用方法有:
-
余弦相似度(Cosine Similarity):衡量向量方向上的差异,忽略其大小。非常适合文本数据。
-
欧氏距离(Euclidean Distance):衡量向量在空间中的实际距离。
-
点积(Dot Product):与余弦相似度相关,但也受向量大小影响。
扩展:通俗的介绍ANN算法
想象一下,你是一个图书管理员,管理着一个有100万本书的图书馆。一个顾客问你:“请帮我找一本和《三体》最相似的书。”
-
愚蠢但精确的方法(暴力搜索):你拿起《三体》,然后一页一页地和其他999,999本书逐字逐句地对比。这绝对能找到最相似的那本,但你找到的时候,顾客可能已经老了。这就像计算机中的精确最近邻搜索,结果完美,但速度慢到无法接受。
-
聪明但近似的方法(ANN算法):你是一个聪明的管理员。你事先做了功课:你给书分了类(建立了索引):科幻区、文学区、历史区……你知道《三体》是科幻小说。你还给书贴了标签:有“外星人”、“物理学”、“悬疑”标签的书大概率在科幻区。当顾客提出同样的问题时,你不会去检查所有100万本书。你直接冲向科幻区,然后只在这个区的几千本书里快速寻找和《三体》最像的。你甚至可能只看了有“外星人”标签的那个书架。
-
结果就是:你极快地找到了一本非常相似的书(比如《银河帝国》),但它是不是100万本中绝对最相似的那本?不一定。可能历史区有一本讲科学史的书某个角度也很像,但你忽略了它。
这个“聪明的办法”就是 ANN 算法的核心思想:ANN(Approximate Nearest Neighbor),近似最近邻。它的精髓就是:用极高的效率(速度)和可接受的内存占用,换来一个“差不多”的、非常接近正确的搜索结果。它不保证找到的是绝对最近的,但能保证找到的是非常近的。
为什么需要它? 因为在处理高维数据(比如图片、文本的1024维向量)时,暴力搜索的计算量是灾难性的,ANN 是唯一可行的解决方案。
五、常见的向量数据库
-
Pinecone:全托管的云端向量数据库,以易用性、高性能和强大的API著称,是快速构建原型和应用的理想选择。
-
Chroma:轻量级、开源,特别专注于AI原生应用和LLM(大语言模型)生态,易于集成和本地部署。
-
Weaviate:开源向量搜索引擎,不仅支持向量搜索,还具备GraphQL接口,可以像图数据库一样处理数据对象之间的关系。
-
Milvus / Zilliz Cloud:Milvus是开源领域功能最全面、最受欢迎的向量数据库之一,专为海量向量数据设计。Zilliz是其背后的商业公司,提供全托管云服务。
-
Qdrant:一个用Rust编写的高性能、开源向量数据库和搜索引擎,提供丰富的API和云服务。
-
FAISS:核心算法库,非数据库。由Meta AI开发。提供最广泛、最前沿的ANN索引算法。支持CPU和GPU。在内存中进行裸向量检索时速度极快,尤其是GPU版本,是衡量其他数据库性能的基准。
注意:FAISS是一个极其高效的向量相似性搜索库(Library),而不是一个完整的、功能齐备的数据库(Database)。FAISS 是 Facebook AI 团队开源的一个用于高效相似性搜索和密集向量聚类的库。它提供了大量的算法,针对不同的数据集大小和精度要求,可以组合出最优的索引和搜索方式。
六、向量数据库的运用
1. 将数据导入到向量数据库
-
第一步:数据清洗与准备
确保原始数据(如文本文档、图片)的质量,进行必要的预处理。
-
第二步:数据向量化(Embedding)
使用预训练的Embedding Model将原始数据转换成向量。
-
文本:可使用bge-m, Qwen3-Embedding, Jina-Embedding 等模型。
-
图片:可使用CLIP, ResNet等模型。
选择合适的模型至关重要,它直接决定了向量的质量和后续检索的效果。
-
第三步:数据与元数据一同导入
将生成的向量及与其关联的元数据(Metadata)一同存入向量数据库。
-
向量(Vector):生成的Embedding数字数组。
-
唯一ID(ID):用于唯一标识每个数据点,方便后续的更新或删除。
-
元数据(Metadata):描述向量的附加信息,是实现高级检索的关键。例如:文本来源的文件名、章节、URL、商品的类别、品牌、价格等
示例:指定模型将数据向量化
# 导入必要的库
import os # 用于访问操作系统环境变量
from openai import OpenAI # 导入OpenAI客户端库
# 初始化OpenAI客户端,配置为连接阿里云百炼服务
client = OpenAI(# 从环境变量DASHSCOPE_API_KEY获取API密钥,用于身份验证# 如果没有设置环境变量,可以在此直接替换为您的API密钥字符串(但不建议在代码中硬编码密钥)api_key=os.getenv("DASHSCOPE_API_KEY"),# 指定API的基础URL,这里指向阿里云百炼服务的兼容模式端点# 这使得可以使用OpenAI库的标准格式调用阿里云的API服务base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
# 创建文本嵌入向量
completion = client.embeddings.create(model="text-embedding-v4", # 指定使用的嵌入模型版本input='我想知道迪士尼的退票政策', # 输入需要转换为向量的文本内容# 指定生成的向量维度(长度),1024维是常见的选择# 此参数仅text-embedding-v3及更高版本支持dimensions=1024,# 指定向量值的编码格式为浮点数(float)# 这是最常用的格式,提供高精度表示encoding_format="float"
)
# 将API响应结果转换为JSON格式字符串并打印输出
# model_dump_json()方法将返回的对象序列化为JSON字符串
print(completion.model_dump_json())
执行步骤:
1. 导入必要的库:os 和 openai(需要安装openai库,如果使用百炼,需要确保版本支持)
2. 创建OpenAI客户端,指定api_key和base_url
3. 调用embeddings.create方法,传入模型名、输入文本、向量维度和编码格式
4. 将返回的嵌入对象转换为JSON字符串并打印
注意:
这里我们使用 embeddings 模型 "text-embedding-v4",并指定维度为1024。 同时,我们打印出返回的JSON字符串。
返回结果:
{"data":[{"embedding":[0.0026341788470745087,-0.03762197867035866,-0.024403059855103493,-0.010764381848275661,0.01541021279990673,0.0007359159062616527,0.018554862588644028,-0.02278093248605728,-0.05350175499916077,0.023905038833618164,-0.02632400020956993,-0.009014192037284374,0.001929833902977407,0.0883917286992073,0.003021924290806055,0.015310607850551605,0.05139583349227905,-0.04928991198539734,-0.053074877709150314,
中间省略部分....
-0.004830809775739908,-0.0026359574403613806,-0.021955639123916626,0.01802838407456875,0.0019369485089555383,0.0010609639575704932,-0.01535329595208168,0.018014153465628624,-0.04493578150868416,0.0027284470852464437,-0.05347329378128052,-0.047639328986406326,-0.03625597432255745],"index":0,"object":"embedding"}],"model":"text-embedding-v4","object":"list","usage":{"prompt_tokens":8,"total_tokens":8},"id":"4f3c37d0-1b7b-99bb-ba7d-878b0e73a153"}
2. 将Embedding 和元数据一起存储在FAISS
-
FAISS 适合探索和实验,适合于数据不需要频繁更新的场景,同时拥有极致的向量搜索速度,且不需要复杂的元数据过滤;
-
FAISS 本身只存储和检索向量,不存储元数据,我们需要在FAISS 之外维护一个元数据的“查找表”,并通过向量在FAISS 中的唯一ID将两者关联起来。
-
最直接有效的方法是使用FAISS 的IndexIDMap,允许我们为每个向量指定一个自定义的、唯一的64位整数ID。然后,可以用这个ID作为元数据存储的键。
示例:构建一个迪士尼政策权限相关的检索系统
-
第一步:准备数据,创建示例文本和对应的元数据。
-
第二步:生成向量,基于百炼text-embedding-v4 生成每个文本的向量。
-
第三步:创建元数据存储,使用一个简单的Python 列表来存储元数据。列表的索引将作为每个数据点的唯一ID。
-
第四步:构建FAISS 索引:
-
使用faiss.IndexFlatL2 创建一个基础的索引,这里使用L2距离(欧氏距离)进行精确搜索。
-
用faiss.IndexIDMap将基础索引包装起来,这样就可以添加带有自定义ID的向量了。
-
第五步:添加数据到索引:将生成的向量和对应的ID(即元数据列表的索引)添加到IndexIDMap中。
-
第六步:执行搜索:
-
对一个新的查询文本生成向量。
-
在FAISS 索引中搜索最相似的向量。
-
FAISS 会返回最相似向量的ID。
-
第七步:检索元数据,使用返回的ID,从元数据存储中查找到原始文本和元数据。
import os
import numpy as np
import faiss
from openai import OpenAI
# Step1. 初始化 API 客户端
try:client = OpenAI(api_key=os.getenv("DASHSCOPE_API_KEY"),base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
except Exception as e:print("初始化OpenAI客户端失败,请检查环境变量'DASHSCOPE_API_KEY'是否已设置。")print(f"错误信息: {e}")exit()
# Step2. 准备示例文本和元数据
# 在实际应用中,这些数据可能来自数据库、文件等
documents = [{"id": "doc1","text": "迪士尼乐园的门票一经售出,原则上不予退换。但在特殊情况下,如恶劣天气导致园区关闭,可在官方指引下进行改期或退款。","metadata": {"source": "official_faq_v1.pdf", "category": "退票政策", "author": "Admin"}},{"id": "doc2","text": "购买“奇妙年卡”的用户,可以享受一年内多次入园的特权,并且在餐饮和购物时有折扣。","metadata": {"source": "annual_pass_rules.docx", "category": "会员权益", "author": "MarketingDept"}},{"id": "doc3","text": "对于在线购买的迪士尼门票,如果需要退票,必须在票面日期前48小时通过原购买渠道提交申请,并可能收取手续费。","metadata": {"source": "online_policy.html", "category": "退票政策", "author": "E-commerceTeam"}},{"id": "doc4","text": "园区内的“加勒比海盗”项目因年度维护,将于下周暂停开放。","metadata": {"source": "maintenance_notice.txt", "category": "园区公告", "author": "OpsDept"}}
]
# Step3. 创建元数据存储和向量列表
# 我们使用一个简单的列表来存储元数据。列表的索引将作为FAISS的ID。
# 这种方式简单直接,适用于中小型数据集。
# 对于大型数据集,可以考虑使用字典或数据库(如Redis, SQLite)
metadata_store = []
vectors_list = []
vector_ids = []
print("正在为文档生成向量...")
for i, doc in enumerate(documents):try:# 调用API生成向量completion = client.embeddings.create(model="text-embedding-v4",input=doc["text"],dimensions=1024,encoding_format="float")# 获取向量vector = completion.data[0].embeddingvectors_list.append(vector)# 存储元数据,并使用列表索引作为唯一IDmetadata_store.append(doc)vector_ids.append(i) # 自定义ID与列表索引一致print(f" - 已处理文档 {i+1}/{len(documents)}")except Exception as e:print(f"处理文档 '{doc['id']}' 时出错: {e}")continue
# 将向量列表转换为NumPy数组,FAISS需要这种格式
vectors_np = np.array(vectors_list).astype('float32')
vector_ids_np = np.array(vector_ids)
# Step4. 构建并填充 FAISS 索引
dimension = 1024 # 向量维度
k = 3 # 查找最近的3个邻居
# 创建一个基础的L2距离索引
index_flat_l2 = faiss.IndexFlatL2(dimension)
# 使用IndexIDMap来包装基础索引,能够映射我们自定义的ID
# 这就是关联向量和元数据的关键!
index = faiss.IndexIDMap(index_flat_l2)
# 将向量和它们对应的ID添加到索引中
index.add_with_ids(vectors_np, vector_ids_np)
print(f"\nFAISS 索引已成功创建,共包含 {index.ntotal} 个向量。")
# Step5. 执行搜索并检索元数据
query_text = "我想了解一下迪士尼门票的退款流程"
print(f"\n正在为查询文本生成向量: '{query_text}'")
try:# 为查询文本生成向量query_completion = client.embeddings.create(model="text-embedding-v4",input=query_text,dimensions=1024,encoding_format="float")query_vector = np.array([query_completion.data[0].embedding]).astype('float32')# 在FAISS索引中执行搜索# search方法返回两个NumPy数组:# D: 距离 (distances)# I: 索引/ID (indices/IDs)distances, retrieved_ids = index.search(query_vector, k)# Step6. 展示结果print("\n--- 搜索结果 ---")# `retrieved_ids[0]` 包含与查询最相似的k个向量的IDfor i in range(k):doc_id = retrieved_ids[0][i]# 检查ID是否有效if doc_id == -1:print(f"\n排名 {i+1}: 未找到更多结果。")continue# 使用ID从我们的元数据存储中检索信息retrieved_doc = metadata_store[doc_id]print(f"\n--- 排名 {i+1} (相似度得分/距离: {distances[0][i]:.4f}) ---")print(f"ID: {doc_id}")print(f"原始文本: {retrieved_doc['text']}")print(f"元数据: {retrieved_doc['metadata']}")
except Exception as e:print(f"执行搜索时发生错误: {e}")
执行结果:
正在为文档生成向量...
-已处理文档1/4
-已处理文档2/4
-已处理文档3/4
-已处理文档4/4
FAISS 索引已成功创建,共包含4 个向量。
正在为查询文本生成向量: '我想了解一下迪士尼门票的退款流程'
---搜索结果---
---排名1 (相似度得分/距离: 0.3222) ---
ID: 2
原始文本: 对于在线购买的迪士尼门票,如果需要退票,必须在票面日期前48小时通过原购买渠道提交申请,并可能收取手续费。
元数据: {'source': 'online_policy.html', 'category': '退票政策', 'author': 'E-commerceTeam'}
---排名2 (相似度得分/距离: 0.3312) ---
ID: 0
原始文本: 迪士尼乐园的门票一经售出,原则上不予退换。但在特殊情况下,如恶劣天气导致园区关闭,可在官方指引下进行改期或退款。
元数据: {'source': 'official_faq_v1.pdf', 'category': '退票政策', 'author': 'Admin'}
---排名3 (相似度得分/距离: 1.0135) ---
ID: 1
原始文本: 购买“奇妙年卡”的用户,可以享受一年内多次入园的特权,并且在餐饮和购物时有折扣。
元数据: {'source': 'annual_pass_rules.docx', 'category': '会员权益', 'author': 'MarketingDept'}
距离越小表示越相近,越大表示越不相关;
七、总结
向量数据库并非要取代传统数据库,而是对其能力的重要补充。它将数据从简单的字符和数字提升到了富含语义的数学表示,使计算机能够真正地“理解”和“联想”非结构化数据。随着生成式AI和大语言模型的爆发,向量数据库作为其记忆和知识检索的核心组件,正在成为现代AI技术栈中不可或缺的基础设施。