在 Ubuntu 24.04 上用 Qdrant 为 LLM 应用做语义缓存
按含义而不是按文本缓存回答,压低 LLM API 开销:Ubuntu 24.04 上的 Qdrant 部署、租户隔离、按路由设置相似度阈值,以及 IOPS 成本。
生成式 AI 应用一上线,账单很快就会引起注意,因为每个 token 都要付费。翻一翻提示词日志,你很可能会发现多达 40% 的用户查询只是用不同的话表达同一个意图。"重置密码的步骤?"和"账号被锁了,想恢复访问"说的是一回事。传统的键值存储却把它们当成两个毫不相干的请求,调用两次 OpenAI API,两次都按全价收费。
语义缓存可以解决这个问题。下面从运维角度介绍如何在 Ubuntu 24.04 LTS 上搭建一个语义缓存,以及教程通常略过的三个陷阱。
精确匹配与语义匹配
两种查找方式的表现截然不同:
- 词法缓存,比如普通的 Redis 键值存储,要求字符串完全一致。"订单在哪?"已经缓存,但"查一下包裹"却无法命中,因为字符不同。
- 语义缓存用嵌入模型把提示词转换成高维向量。"订单在哪?"和"查一下包裹"在向量空间中离得很近,所以存好的回答会立即返回。查找延迟从大约 3000 毫秒降到 15 毫秒左右。
用 Qdrant 搭建向量缓存
存储层用 Docker 运行 Qdrant 并挂载持久化卷,然后准备好 Python 环境:
# 1. Install Qdrant via Docker on Ubuntu 24.04 (Persistent Storage)
sudo apt update && sudo apt install docker.io -y
sudo docker run -d --restart unless-stopped -p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage:z \
qdrant/qdrant
# 2. Setup Python environment
python3 -m venv llm_cache_env
source llm_cache_env/bin/activate
pip install qdrant-client sentence-transformers openai
让租户彼此隔离
在多租户 SaaS 产品里只用一个全局语义缓存,很容易造成客户之间的严重数据泄露。设想用户 A 请求查看自己账户最近交易的概览。模型写出的回答包含 A 的私密财务信息,并被缓存下来。之后用户 B 请求汇总他自己的交易。缓存发现意图匹配度达到 98%,就把写给 A 的回答交给了 B。
解决办法是在 Qdrant 的 payload 中加入严格的命名空间 tenant_id,并让每一次相似度搜索都在强制的元数据过滤条件下执行。没有这个过滤条件,查找就绝不应该运行。下面的代码以正确的向量维度(本地 MiniLM 编码器为 384)创建集合,并执行按租户过滤的搜索:
from qdrant_client import QdrantClient
from qdrant_client.http import models
from sentence_transformers import SentenceTransformer
# Initialize Local Embedding Model (Free & Fast)
encoder = SentenceTransformer("all-MiniLM-L6-v2")
client = QdrantClient(host="localhost", port=6333)
COLLECTION_NAME = "semantic_cache_prod"
# Explicitly create collection with correct Vector Dimensions (384)
if not client.collection_exists(collection_name=COLLECTION_NAME):
client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=models.VectorParams(
size=384, # Matches 'all-MiniLM-L6-v2' output dimension
distance=models.Distance.COSINE
),
)
def check_secure_cache(user_query: str, tenant_id: str, threshold: float = 0.90):
query_vector = encoder.encode(user_query).tolist()
# SRE Fix: Hard Metadata Filtering by tenant_id
hits = client.search(
collection_name=COLLECTION_NAME,
query_vector=query_vector,
query_filter=models.Filter(
must=[models.FieldCondition(
key="tenant_id",
match=models.MatchValue(value=tenant_id)
)]
),
limit=1,
score_threshold=threshold
)
if hits:
print(f" Secure Cache HIT! (Score: {hits[0].score:.3f})")
return hits[0].payload["llm_response"]
return None
阈值陷阱
选择相似度阈值,是在精确率和召回率之间做权衡。许多指南建议统一使用 0.80,结果会产生很糟糕的错误回答。在嵌入空间中距离近,并不代表含义相同。"说出法国的首都"和"说出德国的首都"句子结构一样,所以彼此非常接近,余弦相似度超过 0.85。如果阈值是 0.80,问德国的人就会拿到关于法国的缓存回答。
应当按路由分别调整阈值:
- 严格的事实类、技术类或财务类问题,用 0.95 及以上
- 一般的对话式常见问题,用 0.88
当磁盘速度成为瓶颈
基于 HNSW 索引的向量数据库对内存和磁盘吞吐的要求很高。在公有云上跑高强度的相似度搜索,通常意味着使用预置 IOPS 的块存储,而这类 io2 费用可能相当高昂。
一种绕开的办法是把整套缓存部署在专用的裸金属服务器上。通过 PCIe 直连的企业级 NVMe 硬盘能提供数百万 IOPS 且不额外收费,专用硬件上的大容量 DDR5 内存即使在高并发流量下,也能把嵌入查找控制在 15 毫秒以内。这样就省掉了云服务商在计算之外额外加收的存储税。