首页 / 文章 / 在 Ubuntu 24.04 上用 Qdrant 为 LLM 应用做语义缓存

在 Ubuntu 24.04 上用 Qdrant 为 LLM 应用做语义缓存

按含义而不是按文本缓存回答,压低 LLM API 开销:Ubuntu 24.04 上的 Qdrant 部署、租户隔离、按路由设置相似度阈值,以及 IOPS 成本。

788 词

生成式 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 毫秒以内。这样就省掉了云服务商在计算之外额外加收的存储税。