Семантическое кэширование с эмбеддингами
Создайте семантический кэш, который извлекает сохранённые ответы для семантически похожих, но не идентичных запросов, сравнивая эмбеддинги запросов с кэшем эмбеддингов предыдущих запросов.
«Семантическое кэширование с эмбеддингами» — бесплатный урок AI Engineering Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Engineering Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Engineering Academy содержит 4 уроков всего.
Ограничения точного кэширования
Точное кэширование помогает только тогда, когда пользователи отправляют побитно идентичные запросы. В действительности пользователи формулируют один и тот же вопрос по-разному: «Как отменить подписку?», «Каков порядок отказа от подписки?» и «Могу ли я прекратить свой тариф?» — всё это один и тот же вопрос, но для каждого варианта создаётся свой ключ кэша. Точное кэширование не распознаёт ни один из этих вариантов. Семантическое кэширование решает эту проблему, сопоставляя похожие, а не идентичные запросы, и значительно повышает долю попаданий в кэш.
Как работает семантическое кэширование
Семантический кэш хранит векторное представление каждого кэшированного запроса вместе с кэшированным ответом. Когда поступает новый запрос, создайте для него векторное представление и найдите в кэше ранее встречавшийся запрос с высокой косинусной схожестью. Если ближайший кэшированный запрос превышает порог схожести (обычно 0,95 и выше), верните его кэшированный ответ. Если совпадение не найдено, вызовите LLM, сохраните новый ответ и добавьте векторное представление нового запроса в индекс кэша для последующих поисков.
# Semantic cache flow
# 1. New query arrives: 'How do I cancel my subscription?'
# 2. Embed it: embed_query = embed('How do I cancel my subscription?')
# 3. Search cache index for nearest cached query embedding
# 4. Find cached: 'What is the process to unsubscribe?' (similarity=0.97)
# 5. 0.97 >= threshold (0.95) → cache HIT, return cached response
# 6. If 0.82 < threshold → cache MISS, call LLM, cache result, add embedding to indexСемантический кэш в памяти с NumPy
Для небольших приложений или прототипов реализуйте семантическое кэширование в памяти, используя NumPy для вычисления косинусной схожести. Храните векторные представления кэшированных запросов в двумерном массиве, а ответы — в параллельном списке. Для каждого нового запроса вычисляйте косинусную схожесть между его векторным представлением и всеми кэшированными представлениями, а затем возвращайте ближайшее совпадение, если оно превышает пороговое значение.
import numpy as np
from openai import OpenAI
client = OpenAI()
class InMemorySemanticCache:
def __init__(self, threshold: float = 0.95):
self.threshold = threshold
self.embeddings = [] # list of np.ndarray
self.responses = [] # list of str
self.queries = [] # list of str (for inspection)
def _embed(self, text: str) -> np.ndarray:
resp = client.embeddings.create(model='text-embedding-3-small', input=text)
return np.array(resp.data[0].embedding)
def get(self, query: str) -> str | None:
if not self.embeddings:
return None
q_emb = self._embed(query)
cache_matrix = np.array(self.embeddings)
# Cosine similarity: dot product of normalized vectors
norms = np.linalg.norm(cache_matrix, axis=1)
q_norm = np.linalg.norm(q_emb)
sims = (cache_matrix @ q_emb) / (norms * q_norm + 1e-8)
best_idx = int(np.argmax(sims))
if sims[best_idx] >= self.threshold:
print(f'[SEMANTIC HIT] sim={sims[best_idx]:.3f} matched: {self.queries[best_idx]!r}')
return self.responses[best_idx]
return None
def set(self, query: str, response: str):
emb = self._embed(query)
self.embeddings.append(emb)
self.responses.append(response)
self.queries.append(query)Семантическое кэширование с Redis и Pinecone
Для семантического кэширования в рабочей среде сохраняйте векторные представления запросов в векторной базе данных для быстрого приближённого поиска ближайших соседей, а ответы храните в Redis, связывая их с уникальным ID. Когда поступает новый запрос, найдите в векторной базе данных наиболее близкий кэшированный запрос, получите ответ из Redis с помощью ID из метаданных вектора и верните его — всё это без вызова LLM.
import redis
from pinecone import Pinecone
import hashlib
r = redis.Redis(decode_responses=True)
pc = Pinecone(api_key='YOUR_KEY')
index = pc.Index('semantic-cache')
SIMILARITY_THRESHOLD = 0.95
def semantic_cache_get(query: str) -> str | None:
q_emb = embed(query) # from earlier lesson
results = index.query(vector=q_emb, top_k=1, include_metadata=True)
if not results.matches:
return None
best = results.matches[0]
if best.score >= SIMILARITY_THRESHOLD:
response_key = best.metadata.get('response_key')
return r.get(response_key)
return None
def semantic_cache_set(query: str, response: str):
q_emb = embed(query)
entry_id = hashlib.sha256(query.encode()).hexdigest()[:16]
response_key = f'sem_cache_resp:{entry_id}'
r.setex(response_key, 86400, response) # 24h TTL
index.upsert(vectors=[{
'id': entry_id,
'values': q_emb,
'metadata': {'query': query[:200], 'response_key': response_key},
}])GPTCache: готовое семантическое кэширование
GPTCache — это библиотека с открытым исходным кодом, реализующая семантическое кэширование для приложений на основе LLM. Она оборачивает клиент OpenAI, автоматически создаёт векторные представления запросов, проверяет кэш по сходству векторов и при отсутствии результата обращается к настоящему API. Библиотека изначально поддерживает несколько векторных хранилищ (FAISS, Milvus, Redis) и несколько моделей для создания векторных представлений, поэтому позволяет быстро добавить семантическое кэширование в уже существующее приложение.
# pip install gptcache
from gptcache import cache
from gptcache.adapter import openai
from gptcache.embedding import OpenAI as EmbeddingOpenAI
from gptcache.manager import CacheBase, VectorBase, get_data_manager
from gptcache.similarity_evaluation.distance import SearchDistanceEvaluation
# Configure GPTCache with FAISS vector store
cache.init(
embedding_func=EmbeddingOpenAI().to_embeddings,
data_manager=get_data_manager(
CacheBase('sqlite'),
VectorBase('faiss', dimension=1536),
),
similarity_evaluation=SearchDistanceEvaluation(),
)
# Now use the wrapped openai client — caching is transparent
response = openai.ChatCompletion.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': 'What is RAG?'}],
)Выбор порога сходства
Порог сходства — самый важный гиперпараметр семантического кэширования. При слишком высоком значении (0.99 и выше) вы не распознаете большинство вариантов перефразирования. При слишком низком значении (0.85 и ниже) вы будете возвращать неправильные кэшированные ответы для разных, но внешне похожих запросов. Проверяйте порог эмпирически: отберите пары запросов и убедитесь, что запросы выше порога действительно предполагают один и тот же ответ. Типичные значения: 0.92–0.97 для фактологических вопросов и ответов, 0.98 и выше для генерации кода, где даже небольшие различия имеют большое значение.
def validate_threshold(cache, query_pairs_with_labels):
'''
query_pairs_with_labels: list of (query1, query2, should_match: bool)
'''
true_pos = true_neg = false_pos = false_neg = 0
for q1, q2, should_match in query_pairs_with_labels:
e1, e2 = embed(q1), embed(q2)
sim = cosine_similarity(e1, e2)
matched = sim >= cache.threshold
if should_match and matched: true_pos += 1
elif not should_match and not matched: true_neg += 1
elif not should_match and matched: false_pos += 1
else: false_neg += 1
precision = true_pos / (true_pos + false_pos) if (true_pos + false_pos) else 0
recall = true_pos / (true_pos + false_neg) if (true_pos + false_neg) else 0
print(f'Precision: {precision:.3f}, Recall: {recall:.3f}')Область действия семантического кэша: важность системного запроса
Есть одна важная деталь: семантическое кэширование должно учитывать системный запрос. Два одинаковых пользовательских запроса могут привести к разным ответам, если системные запросы различаются (например, заданы разные роли, базы знаний или форматы ответа). Всегда включайте системный запрос во входные данные для создания векторного представления либо создавайте отдельные пространства имён кэша для каждого системного запроса. Удобный подход — вычислять хеш системного запроса и использовать его как префикс пространства имён кэша.
import hashlib
def make_semantic_cache_namespace(system_prompt: str) -> str:
return 'sc:' + hashlib.md5(system_prompt.encode()).hexdigest()[:8]
def semantic_cache_get_namespaced(system_prompt: str, user_query: str) -> str | None:
namespace = make_semantic_cache_namespace(system_prompt)
q_emb = embed(user_query)
# Search only within this namespace
results = index.query(
vector=q_emb,
top_k=1,
filter={'namespace': namespace},
include_metadata=True,
)
if results.matches and results.matches[0].score >= SIMILARITY_THRESHOLD:
return r.get(results.matches[0].metadata['response_key'])
return NoneАнализ доли попаданий в семантический кэш
После внедрения семантического кэширования анализируйте долю попаданий отдельно для каждого кластера запросов. Используйте сами векторные представления кэшированных запросов: объедините их в кластеры с помощью K-means и вычислите долю попаданий для каждого кластера. Кластеры с высокой долей попаданий соответствуют распространённым темам вопросов, для которых кэширование наиболее выгодно. Кластеры с низкой долей попаданий, содержащие разнообразные уникальные запросы, могут вообще не получать пользы от кэширования; их можно исключить из кэша, чтобы уменьшить размер индекса и затраты на создание векторных представлений.
from sklearn.cluster import KMeans
import numpy as np
def analyze_cache_clusters(cache, n_clusters=10):
if len(cache.embeddings) < n_clusters:
print('Not enough cache entries to cluster')
return
matrix = np.array(cache.embeddings)
kmeans = KMeans(n_clusters=n_clusters, n_init=10, random_state=42)
labels = kmeans.fit_predict(matrix)
from collections import Counter
cluster_sizes = Counter(labels)
print('Query clusters by size:')
for cluster_id, count in cluster_sizes.most_common():
representative = cache.queries[labels.tolist().index(cluster_id)]
print(f' Cluster {cluster_id}: {count} queries, e.g. {representative!r}')Вопросы безопасности семантического кэша
Семантическое кэширование создаёт риск для конфиденциальности: если пользователь A задаст чувствительный вопрос, его ответ может быть возвращён пользователю B, задавшему похожий вопрос. Это допустимо для общедоступных баз знаний, но не для приложений с данными, специфичными для пользователей, или с чувствительным содержимым. Обеспечьте строгую изоляцию пространств имён для каждого пользователя или организации и рассмотрите возможность полного исключения из кэша запросов, соответствующих шаблонам вроде персональных данных.
import re
PII_PATTERNS = [
r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b', # phone numbers
r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b', # emails
r'\b\d{9}\b', # SSN-like
]
def should_cache(query: str) -> bool:
for pattern in PII_PATTERNS:
if re.search(pattern, query, re.IGNORECASE):
return False # do not cache queries with PII
return True
def secure_semantic_completion(user_id: str, query: str) -> str:
if should_cache(query):
cached = semantic_cache_get_namespaced(f'user:{user_id}', query)
if cached:
return cached
result = call_llm_api(query) # actual API call
if should_cache(query):
semantic_cache_set_namespaced(f'user:{user_id}', query, result)
return resultСочетание точного и семантического кэширования
Наиболее эффективная стратегия кэширования использует точное и семантическое кэширование в рамках двухуровневой иерархии. Сначала проверяйте точный кэш (это быстрее всего и не требует затрат на создание векторного представления) и сразу возвращайте результат при попадании. Если в точном кэше результата нет, проверяйте семантический кэш (для этого требуется один вызов API создания векторного представления). Если и в семантическом кэше результата нет, вызывайте LLM. Такой порядок минимизирует и задержку, и стоимость каждого запроса.
async def two_tier_cached_completion(messages: list[dict], model: str = 'gpt-4o-mini') -> str:
user_query = messages[-1].get('content', '')
system_prompt = messages[0].get('content', '') if messages and messages[0]['role'] == 'system' else ''
# Tier 1: exact cache (instant, free)
exact_key = make_cache_key(messages, model, temperature=0.0)
exact_cached = await async_r.get(exact_key)
if exact_cached:
return json.loads(exact_cached)
# Tier 2: semantic cache (one embedding call ~5ms)
sem_result = semantic_cache_get_namespaced(system_prompt, user_query)
if sem_result:
# Backfill exact cache to avoid embedding next time
await async_r.setex(exact_key, 3600, json.dumps(sem_result))
return sem_result
# Tier 3: actual LLM call
response = await async_client.chat.completions.create(
model=model, messages=messages, temperature=0.0
)
result = response.choices[0].message.content
await async_r.setex(exact_key, 3600, json.dumps(result))
semantic_cache_set_namespaced(system_prompt, user_query, result)
return resultПрогрев кэша при холодном запуске
Свежий семантический кэш не приносит пользы, пока не будет заполнен. Для приложений с предсказуемой структурой трафика предварительно прогревайте кэш при запуске: создавайте векторные представления и кэшируйте ответы на наиболее часто задаваемые вопросы из журналов исторических запросов. Это устраняет период холодного запуска, когда в первые часы работы каждый пользователь получает промах кэша, а приложение несёт полную стоимость API.
async def warm_semantic_cache(faq_list: list[dict], system_prompt: str):
print(f'Warming cache with {len(faq_list)} FAQ entries...')
for entry in faq_list:
cached = semantic_cache_get_namespaced(system_prompt, entry['question'])
if cached:
print(f' Already cached: {entry["question"][:50]}')
continue
# Generate and cache the response
response = await async_client.chat.completions.create(
model='gpt-4o-mini',
messages=[
{'role': 'system', 'content': system_prompt},
{'role': 'user', 'content': entry['question']},
],
temperature=0.0,
)
answer = response.choices[0].message.content
semantic_cache_set_namespaced(system_prompt, entry['question'], answer)
print(f' Cached: {entry["question"][:50]}')
print('Cache warming complete')Быстрая проверка
Проверьте, насколько хорошо вы усвоили материал этого урока о семантическом кэшировании.
Итоги урока
В этом уроке вы узнали, что семантическое кэширование сопоставляет похожие, но не идентичные запросы, сравнивая векторные представления запроса с хранилищем векторных представлений кэшированных запросов; порог сходства определяет компромисс между долей попаданий и правильностью ответа; а разделение пространств имён по системному запросу предотвращает ошибочные попадания в кэш из другого контекста. Двухуровневая архитектура, в которой точный кэш проверяется перед семантическим, минимизирует и задержку, и затраты на создание векторных представлений. Далее мы рассмотрим встроенное в OpenAI кэширование префикса запроса.
Часто задаваемые вопросы
Урок «Семантическое кэширование с эмбеддингами» бесплатный?
Да — полный текст урока «Семантическое кэширование с эмбеддингами» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Engineering Academy, подпишись на CoddyKit PRO. Курс AI Engineering Academy содержит 4 уроков всего.
Чему я научусь в уроке «Семантическое кэширование с эмбеддингами»?
Создайте семантический кэш, который извлекает сохранённые ответы для семантически похожих, но не идентичных запросов, сравнивая эмбеддинги запросов с кэшем эмбеддингов предыдущих запросов. Ты практикуешь AI Engineering Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать AI Engineering Academy?
Предыдущий опыт не требуется. AI Engineering Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Семантическое кэширование с эмбеддингами»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке AI Engineering Academy?
Да. Каждый урок AI Engineering Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Точное кэширование с Redis
- Семантическое кэширование с эмбеддингами
- Кэширование префиксов запросов OpenAI
- Пакетная обработка, маршрутизация моделей и панели расходов