В высоконагруженных системах значительная доля пользовательских запросов к LLM семантически эквивалентна: пользователи задают одни и те же вопросы с вариациями синтаксиса, опечатками или перестановкой слов. Классическое кэширование по точному совпадению ключа (Exact Match Cache по SHA-256 хэшу строки) показывает крайне низкий Cache Hit Rate (обычно менее 5–8%), пропуская практически все вариации запросов на дорогостоящий инференс модели.
Архитектура семантического кэширования (Semantic Caching) и AI Gateways решает эту проблему за счет векторного представления запросов:
Семантический кэш выступает предиктивным фильтром между клиентом и пулом провайдеров LLM. Задержка ответа при попадании в кэш снижается с 1200–2500 мс до 20–30 мс, а затраты на токены снижаются до нуля.
[ Client Query: "Как восстановить доступ к аккаунту?" ]
|
v
[ Exact Cache Check: L1 ]
(SHA-256 Hash Lookup)
| |
(Hit) | | (Miss)
+-----+ v
| [ Generate Embedding ] -> text-embedding-3-small (1536 dim)
| |
| v
| [ Semantic Cache Search: L2 (Redis VL / HNSW) ]
| Query: "Инструкция по сбросу пароля учетной записи"
| Metric: Cosine Similarity = 0.942
| |
| +--------------+--------------+
| | (Similarity >= 0.92) | (Similarity < 0.92)
| v (Cache Hit) v (Cache Miss)
| [ Return Cached Payload ] [ Upstream LLM Gateway ]
| (Latency: 22ms, Cost: $0) [ Cloudflare / OpenAI ]
| | |
+--------+ | (Inference: 1400ms)
| v
| [ Store in Redis VL with TTL ]
v |
[ Client Response ] <-----------------+Ниже представлена промышленная реализация двухуровневого семантического кэша (L1 Exact Hash + L2 Vector Search) с использованием Redis и эмбеддинг-модели OpenAI text-embedding-3-small.
import { createHash } from "node:crypto";
import OpenAI from "openai";
export interface CacheEntry {
id: string;
query: string;
response: string;
embedding: number[];
similarity?: number;
createdAt: number;
}
export interface SemanticCacheConfig {
similarityThreshold: number; // Рекомендуется 0.92 для общих задач, 0.96 для кода
ttlSeconds: number;
embeddingModel: string;
}
export class SemanticCacheManager {
private openai: OpenAI;
// Локальное in-memory представление векторного индекса (в продакшене: Redis VL / RediSearch)
private memoryVectorIndex: CacheEntry[] = [];
private exactHashIndex = new Map<string, string>();
constructor(private config: SemanticCacheConfig) {
this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
}
/**
* Вычисление косинусного расстояния между двумя нормализованными векторами
*/
private cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) {
throw new Error("Векторы имеют разную размерность");
}
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
if (normA === 0 || normB === 0) return 0;
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
/**
* Получение эмбеддинга запроса
*/
private async getEmbedding(text: string): Promise<number[]> {
const response = await this.openai.embeddings.create({
model: this.config.embeddingModel,
input: text.trim().toLowerCase(),
});
return response.data[0].embedding;
}
/**
* Поиск ответа: сначала точный L1 хэш, затем семантический L2
*/
public async get(query: string): Promise<{ hit: boolean; response: string | null; type?: "L1_EXACT" | "L2_SEMANTIC"; score?: number }> {
const normalized = query.trim().toLowerCase();
// L1: Exact Match через SHA-256
const queryHash = createHash("sha256").update(normalized).digest("hex");
const exactHit = this.exactHashIndex.get(queryHash);
if (exactHit) {
return { hit: true, response: exactHit, type: "L1_EXACT", score: 1.0 };
}
// L2: Семантический поиск по вектору
const queryEmbedding = await this.getEmbedding(normalized);
let bestMatch: CacheEntry | null = null;
let maxSimilarity = -1;
for (const entry of this.memoryVectorIndex) {
const sim = this.cosineSimilarity(queryEmbedding, entry.embedding);
if (sim > maxSimilarity) {
maxSimilarity = sim;
bestMatch = entry;
}
}
if (bestMatch && maxSimilarity >= this.config.similarityThreshold) {
return {
hit: true,
response: bestMatch.response,
type: "L2_SEMANTIC",
score: Number(maxSimilarity.toFixed(4)),
};
}
return { hit: false, response: null };
}
/**
* Сохранение ответа в оба уровня кэша
*/
public async set(query: string, response: string): Promise<void> {
const normalized = query.trim().toLowerCase();
const queryHash = createHash("sha256").update(normalized).digest("hex");
// Запись в L1
this.exactHashIndex.set(queryHash, response);
// Запись в L2
const embedding = await this.getEmbedding(normalized);
this.memoryVectorIndex.push({
id: queryHash,
query: normalized,
response,
embedding,
createdAt: Date.now(),
});
}
}Конфигурация Cloudflare AI Gateway для граничного кэширования и унифицированного управления провайдерами:
import OpenAI from "openai";
/**
* Клиент OpenAI, настроенный через Cloudflare AI Gateway
* Предоставляет автоматическое edge-кэширование, rate limiting и аналитику запросов
*/
export function createCloudflareAIGatewayClient() {
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
const gatewayName = process.env.CLOUDFLARE_GATEWAY_NAME;
if (!accountId || !gatewayName) {
throw new Error("Не заданы переменные окружения Cloudflare AI Gateway");
}
return new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
// Маршрутизация через Cloudflare AI Gateway endpoint
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayName}/openai`,
defaultHeaders: {
// Заголовок включения семантического кэша на стороне Cloudflare
"cf-aig-cache": "true",
"cf-aig-cache-ttl": "86400", // TTL 24 часа
},
});
}Установка порога подобия в 0.80–0.85 приводит к критическим ошибкам False Positive. Например, запросы «Как включить двухфакторную аутентификацию?» и «Как отключить двухфакторную аутентификацию?» имеют косинусное сходство около 0.87, но прямо противоположный смысл. Кэш вернет пользователю инструкцию по выключению вместо включения. Для чувствительных сценариев порог должен быть не ниже 0.93–0.95.
Кэширование ответов модели без привязки к user_id или tenant_id в сценариях, где промпт включает персональные данные. Пользователь B может получить из семантического кэша ответ, сгенерированный ранее для пользователя A и содержащий его приватный отчет или финансовые данные. Ключ кэша должен включать пространство имен арендатора.
Использование семантического кэша для креативной генерации текста, брейншторминга или задач, где пользователи ожидают вариативность. Семантический кэш оправдан только при температуре 0 или близкой к нулю (детерминированные справочные запросы, классификация, извлечение сущностей).
Кэширование ответов с бесконечным TTL. Когда исходный документ в корпоративной wiki обновляется, семантический кэш продолжает отдавать устаревшие данные еще недели. Кэш должен иметь ограниченный TTL (например, 12-24 часа) или версионироваться по идентификатору ревизии базы знаний (knowledge_base_version).
Прием HTTP-запроса на Edge-шлюзе (Cloudflare / Envoy), SSL-терминация и валидация токена
Проверка кэша по точному SHA-256 хэшу промпта за субмиллисекундное время
Генерация эмбеддинга и HNSW-поиск в Redis VL с проверкой порога Similarity >= 0.92
При промахе кэша: маршрутизация запроса к LLM-провайдеру с контролем таймаутов
Асинхронная запись ответа модели в L1 и L2 кэш с установкой TTL