임베딩을 활용한 의미 기반 캐싱
질의 임베딩을 이전 요청 임베딩 캐시와 비교하여, 완전히 같지는 않지만 의미가 유사한 질의에 저장된 응답을 반환하는 의미 기반 캐시를 구축합니다.
임베딩을 활용한 의미 기반 캐싱은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 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 indexNumPy를 사용한 메모리 내 의미 기반 캐시
소규모 애플리케이션이나 프로토타입에서는 코사인 유사도 계산에 NumPy를 사용하여 메모리 내에서 의미 기반 캐싱을 구현할 수 있습니다. 캐시된 질의 임베딩은 2차원 배열에 저장하고 응답은 이에 대응하는 목록에 저장하십시오. 새 질의가 들어올 때마다 새 임베딩과 모든 캐시 임베딩 사이의 코사인 유사도를 계산하고, 임계값을 초과하는 가장 가까운 일치 항목을 반환하십시오.
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을 활용한 시맨틱 캐시
프로덕션 환경에서 시맨틱 캐싱을 사용하려면 빠른 근사 최근접 이웃 검색을 위해 쿼리 임베딩을 벡터 데이터베이스에 저장하고, 응답은 고유한 ID를 키로 사용하여 Redis에 저장합니다. 새 쿼리가 들어오면 벡터 데이터베이스에서 가장 가까운 캐시된 쿼리를 검색하고, 벡터 메타데이터의 ID를 사용하여 Redis에서 응답을 가져온 다음 반환합니다. 이 모든 과정에서 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정확한 캐싱과 시맨틱 캐싱 결합
가장 효율적인 캐싱 전략은 2단계 계층에서 정확한 캐싱과 시맨틱 캐싱을 함께 사용하는 것입니다. 먼저 정확한 캐시를 확인하고(가장 빠르며 임베딩 비용이 없음) 적중하면 즉시 반환합니다. 정확한 캐시가 누락되면 시맨틱 캐시를 확인합니다(임베딩 API 호출 1회 필요). 시맨틱 캐시도 누락되면 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')빠른 확인
이 강의에서 배운 시맨틱 캐싱에 대한 이해도를 확인하십시오.
강의 요약
이 강의에서는 다음을 배웠습니다. 시맨틱 캐싱은 쿼리 임베딩을 캐시된 쿼리 임베딩의 벡터 저장소와 비교하여 서로 동일하지 않더라도 유사한 쿼리를 찾습니다. 유사도 임계값은 적중률과 답변 정확성 사이의 균형을 조절하며, 시스템 프롬프트 네임스페이스 지정은 서로 다른 맥락에서 잘못된 캐시 적중이 발생하는 것을 방지합니다. 정확한 캐시를 시맨틱 캐시보다 먼저 확인하는 2단계 아키텍처는 지연 시간과 임베딩 비용을 모두 최소화합니다. 다음으로는 OpenAI에 내장된 프롬프트 접두사 캐싱을 활용합니다.
AI 튜터와 함께 Python을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 30
- 레슨
- 120
자주 묻는 질문
“임베딩을 활용한 의미 기반 캐싱” 강의는 무료인가요?
네 — “임베딩을 활용한 의미 기반 캐싱” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“임베딩을 활용한 의미 기반 캐싱”에서 뭘 배우나요?
질의 임베딩을 이전 요청 임베딩 캐시와 비교하여, 완전히 같지는 않지만 의미가 유사한 질의에 저장된 응답을 반환하는 의미 기반 캐시를 구축합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“임베딩을 활용한 의미 기반 캐싱” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- Redis를 활용한 정확한 캐싱
- 임베딩을 활용한 의미 기반 캐싱
- OpenAI 프롬프트 접두사 캐싱
- 일괄 처리, 모델 라우팅, 비용 대시보드