0Pricing
AI Engineering Academy · Урок

Семантическое кэширование с эмбеддингами

Создайте семантический кэш, который извлекает сохранённые ответы для семантически похожих, но не идентичных запросов, сравнивая эмбеддинги запросов с кэшем эмбеддингов предыдущих запросов.

«Семантическое кэширование с эмбеддингами» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Точное кэширование с Redis
  2. Семантическое кэширование с эмбеддингами
  3. Кэширование префиксов запросов OpenAI
  4. Пакетная обработка, маршрутизация моделей и панели расходов
← Назад к AI Engineering Academy