向量数据库基础:Embedding、相似度搜索与RAG核心
上周排查一个线上问题,用户反馈“智能客服突然开始胡言乱语”。查日志发现,当用户问“退货政策最新版”时,系统返回的竟然是“2021年促销活动条款”。问题出在向量搜索环节——相似度最高的文档居然是三年前的旧政策。这个坑让我意识到,很多团队把向量数据库当黑盒用,结果就是线上时不时给你来个“惊喜”。
Embedding不是魔法
Embedding说白了就是把文本变成一串数字。这串数字得能代表语义,否则后续搜索全是白搭。
# 常见误区:直接拿BERT最后一层当embedding
# 别这样写!效果通常很差
from transformers import AutoModel, AutoTokenizer
model = AutoModel.from_pretrained("bert-base-chinese")
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
inputs = tokenizer("退货政策", return_tensors="pt")
outputs = model(**inputs)
last_hidden = outputs.last_hidden_state # 形状: [1, seq_len, 768]
cls_vector = last_hidden[0][0] # 取[CLS] token
# 问题:BERT的[CLS]未经专门训练做语义表示
# 除非你fine-tune过,否则效果不稳定
更靠谱的做法是用专门优化过的模型:
# 推荐:用Sentence-BERT或专门优化的模型
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
vector = model.encode("退货政策最新版") # 得到384维向量
# 关键:这些模型在训练时就让相似句子的向量靠近
实际项目中,我习惯加个归一化:
import numpy as np
vector = vector / np.linalg.norm(vector) # L2归一化
# 这样后续算余弦相似度直接点积就行,省计算量
相似度搜索的坑
很多人以为向量数据库就是“存向量、搜最近邻”,其实里面门道不少。
# 踩过的一个坑:默认参数不一定适合你
import faiss
dimension = 384
index = faiss.IndexFlatIP(dimension) # 内积索引,假设向量已归一化
# 问题来了:当你有100万条数据时,线性扫描太慢
# 线上服务延迟直接飙到500ms+
# 解决方案:用IVF索引
quantizer = faiss.IndexFlatIP(dimension)
index = faiss.IndexIVFFlat(quantizer, dimension, 100) # 100个聚类中心
index.train(training_vectors) # 必须训练!
index.add(data_vectors)
# 搜索时控制搜索范围
index.nprobe = 10 # 搜索10个最近聚类
# nprobe越大越准但越慢,需要实测调优
还有个容易忽略的点:距离度量选错全盘皆输。我们曾经用欧氏距离搜文本,结果召回率惨不忍睹。文本相似度搜索,余弦相似度(或归一化后的内积)才是正解。
RAG的核心不是检索是增强
开头那个客服问题,根源在于RAG(检索增强生成)系统只做了“检索”,没做好“增强”。
# 简陋版RAG(很多项目的现状)
def naive_rag(query):
query_vector = embed(query)
results = vector_db.search(query_vector, k=3) # 找最相似的3条
context = "\n".join([doc.text for doc in results])
prompt = f"参考:{context}\n问题:{query}"
return llm.generate(prompt)
# 问题1:如果top3都是旧政策,LLM只能基于错误信息回答
# 问题2:没有去重,相似文档重复出现
# 问题3:没考虑时效性
改进后的版本:
def better_rag(query, user_id=None):
# 1. 多路召回
keyword_results = keyword_search(query) # 传统关键词检索
vector_results = vector_search(query) # 向量检索
# 2. 重排序
all_results = rerank(query, keyword_results + vector_results)
# 3. 去重和过滤
unique_results = remove_duplicates(all_results)
filtered = filter_by_date(unique_results, cutoff="2023-01-01")
# 4. 个性化(如果有用户历史)
if user_id:
user_pref = get_user_preference(user_id)
filtered = personalize(filtered, user_pref)
# 5. 构造带元数据的prompt
context = format_with_metadata(filtered[:5]) # 带来源、时间戳
prompt = build_prompt(context, query, system_role="客服专家")
# 6. 让LLM判断是否足够回答
return llm.generate_with_fallback(prompt)
这里的关键是:向量搜索只是召回手段之一。最终效果取决于整个pipeline的设计——怎么召回、怎么排序、怎么构造prompt。
工程实践建议
-
Embedding模型选型:别盲目追新,小模型+精调往往比大模型+zero-shot更实用。我们线上用384维的模型,效果不比768维差,但速度快一倍,内存省一半。
-
混合搜索是王道:纯向量搜索在术语匹配上容易翻车。比如用户搜“Python”,向量可能返回“蟒蛇”,但关键词搜索不会错。两者结合再加个重排序模型,效果最稳。
-
元数据过滤必须做:给每个向量配上时间戳、来源、版本号。搜索时先按业务规则过滤,再算相似度。像政策、价格这种时效性强的,不加时间过滤就是埋雷。
-
监控召回质量:别只监控延迟和QPS。我们每天抽样100个查询,人工评估召回的相关性。发现bad case就分析是embedding问题、索引问题还是数据质量问题。
-
数据更新策略:向量索引重建成本高。我们采用增量更新:每天新增数据入临时索引,每周合并到主索引。重大业务变更(如政策大改)才全量重建。
-
硬件不是越贵越好:CPU做向量搜索可能比GPU更经济,尤其是QPS不高但数据量大的场景。实测过,百万级数据用AVX2优化的CPU索引,单次搜索<10ms。
向量数据库现在炒得火热,但记住它本质是工具。真正影响业务效果的,是你对业务场景的理解——知道什么时候该用余弦相似度什么时候该用欧氏距离,知道怎么平衡召回率和响应时间,知道哪些数据该进向量库哪些该放传统数据库。把这些想明白了,技术选型反而简单。
更多推荐
所有评论(0)