การแคชเชิงความหมายด้วยเวกเตอร์ฝัง
สร้างแคชเชิงความหมายที่ดึงการตอบกลับซึ่งจัดเก็บไว้สำหรับคำค้นหาที่มีความหมายคล้ายกันแต่ไม่เหมือนกันทุกประการ โดยเปรียบเทียบเวกเตอร์ฝังของคำค้นหากับแคชเวกเตอร์ฝังจากคำขอก่อนหน้า
การแคชเชิงความหมายด้วยเวกเตอร์ฝัง เป็นบทเรียน AI Engineering Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน AI Engineering Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน
ข้อจำกัดของการแคชแบบตรงกันทุกประการ
การแคชแบบตรงกันทุกประการช่วยได้เฉพาะเมื่อผู้ใช้ส่งคำขอที่ เหมือนกันทุกไบต์เท่านั้น ในความเป็นจริง ผู้ใช้อาจใช้ถ้อยคำถามเดียวกันแตกต่างกัน เช่น 'How do I cancel my subscription?', 'What is the process to unsubscribe?' และ 'Can I stop my plan?' ล้วนต้องการถามสิ่งเดียวกัน แต่สร้างคีย์แคชที่แตกต่างกัน การแคชแบบตรงกันทุกประการจึงไม่ครอบคลุมรูปแบบเหล่านี้ การแคชเชิงความหมายแก้ปัญหานี้ด้วยการจับคู่คำค้นหาที่คล้ายกันแทนการจับคู่คำค้นหาที่เหมือนกันทุกประการ ทำให้อัตราการแคชสำเร็จเพิ่มขึ้นอย่างมาก
การทำงานของการแคชเชิงความหมาย
แคชเชิงความหมายจะจัดเก็บ เวกเตอร์ฝังตัวของคำค้นหาที่แคชไว้แต่ละรายการไว้พร้อมกับคำตอบที่แคชไว้ เมื่อมีคำค้นหาใหม่เข้ามา ให้สร้างเวกเตอร์ฝังตัวของคำค้นหานั้น แล้วค้นหาในแคชว่ามีคำค้นหาที่เคยพบซึ่งมีความคล้ายคลึงโคไซน์สูงหรือไม่ หากคำค้นหาที่แคชไว้ซึ่งใกล้เคียงที่สุดมีค่ามากกว่าเกณฑ์ความคล้ายคลึง โดยทั่วไปคือ 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 และไพน์โคน
สำหรับการใช้แคชเชิงความหมายในระบบจริง ให้จัดเก็บ query embeddings ไว้ในฐานข้อมูลเวกเตอร์เพื่อค้นหาเพื่อนบ้านที่ใกล้ที่สุดโดยประมาณได้อย่างรวดเร็ว และจัดเก็บ response ใน Redis โดยใช้ ID ที่ไม่ซ้ำกันเป็นกุญแจ เมื่อมี query ใหม่เข้ามา ให้ค้นหา query ที่ถูกแคชไว้และมีความใกล้เคียงที่สุดในฐานข้อมูลเวกเตอร์ ดึง response จาก 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 client, สร้าง embedding ของ query โดยอัตโนมัติ ตรวจสอบแคชความคล้ายคลึงของเวกเตอร์ และหากไม่พบข้อมูลก็จะเรียกใช้ API จริงแทน รองรับที่จัดเก็บเวกเตอร์หลายรูปแบบ (FAISS, Milvus, Redis) และโมเดล embedding หลายแบบได้ทันที จึงเป็นวิธีที่รวดเร็วในการเพิ่มแคชเชิงความหมายให้แอปพลิเคชันที่มีอยู่
# 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 ขึ้นไป) ระบบจะพลาด query ที่มีการเรียบเรียงใหม่ส่วนใหญ่ หากต่ำเกินไป (ต่ำกว่า 0.85) ระบบอาจส่งคำตอบที่แคชไว้ผิดให้กับ query ที่แตกต่างกันแต่มีความคล้ายคลึงเพียงผิวเผิน ตรวจสอบค่าเกณฑ์ด้วยข้อมูลจริง โดยสุ่มตัวอย่างคู่ query แล้วตรวจว่าคู่ที่มีค่าความคล้ายคลึงสูงกว่าค่าเกณฑ์นั้นต้องการคำตอบเดียวกันจริงหรือไม่ ค่าที่พบได้ทั่วไปคือ 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}')ขอบเขตของแคชเชิงความหมาย: พรอมป์ตระบบมีความสำคัญ
รายละเอียดสำคัญคือ แคชเชิงความหมายต้องคำนึงถึงพรอมป์ตระบบด้วย query ของผู้ใช้ที่เหมือนกันทุกประการอาจให้คำตอบต่างกัน หากพรอมป์ตระบบแตกต่างกัน (บุคลิก ฐานความรู้ หรือรูปแบบ response แตกต่างกัน) ควรรวมพรอมป์ตระบบไว้ใน input สำหรับสร้าง embedding เสมอ หรือสร้างเนมสเปซของแคชแยกกันสำหรับพรอมป์ตระบบแต่ละรายการ แนวทางที่เป็นระเบียบคือสร้างค่าแฮชจากพรอมป์ตระบบ แล้วใช้ค่านั้นเป็นคำนำหน้าเนมสเปซของแคช
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การวิเคราะห์อัตราการพบแคชเชิงความหมาย
หลังติดตั้งแคชเชิงความหมายแล้ว ให้วิเคราะห์อัตราการพบแคชโดยแยกตามคลัสเตอร์ของ query ใช้ query embeddings ที่แคชไว้โดยตรง จัดกลุ่มด้วย K-means แล้วคำนวณอัตราการพบแคชของแต่ละคลัสเตอร์ คลัสเตอร์ที่มีอัตราการพบแคชสูงแสดงถึงหัวข้อคำถามที่พบบ่อย ซึ่งการแคชช่วยได้มากที่สุด ส่วนคลัสเตอร์ที่มีอัตราต่ำและประกอบด้วย query เฉพาะจำนวนมากอาจไม่ได้ประโยชน์จากการแคชเลย และอาจตัดออกจากแคชเพื่อลดขนาดดัชนีและค่าใช้จ่ายในการสร้าง embedding
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 ถามคำถามที่ละเอียดอ่อน response ของคำถามนั้นอาจถูกส่งกลับให้ผู้ใช้ B ที่ถามคำถามคล้ายกัน เรื่องนี้ยอมรับได้สำหรับฐานความรู้สาธารณะ แต่ไม่เหมาะกับแอปพลิเคชันที่มีข้อมูลเฉพาะผู้ใช้หรือเนื้อหาที่ละเอียดอ่อน ควรแยกเนมสเปซตามผู้ใช้หรือองค์กรอย่างเข้มงวด และพิจารณาไม่นำ query ที่ตรงกับรูปแบบข้อมูลส่วนบุคคลไปเก็บในแคชเลย
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การใช้แคชแบบตรงกันทุกประการร่วมกับแคชเชิงความหมาย
กลยุทธ์แคชที่มีประสิทธิภาพสูงสุดใช้ทั้งแคชแบบตรงกันทุกประการและแคชเชิงความหมายในลำดับชั้นสองระดับ ตรวจสอบแคชแบบตรงกันทุกประการก่อน (เร็วที่สุดและไม่เสียค่าใช้จ่ายในการสร้าง embedding) แล้วส่งกลับทันทีเมื่อพบ หากไม่พบในแคชแบบตรงกันทุกประการ ให้ตรวจสอบแคชเชิงความหมาย (ต้องเรียก API สำหรับสร้าง embedding หนึ่งครั้ง) หากไม่พบในแคชเชิงความหมาย ให้เรียกใช้ 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การอุ่นแคชเพื่อรับมือการเริ่มต้นแบบไม่มีข้อมูล
แคชเชิงความหมายที่สร้างใหม่จะยังไม่ให้ประโยชน์จนกว่าจะมีการเติมข้อมูล สำหรับแอปพลิเคชันที่มีรูปแบบปริมาณการใช้งานคาดเดาได้ ให้อุ่นแคชล่วงหน้าเมื่อเริ่มต้นระบบ โดยสร้าง embedding และแคช response สำหรับคำถามที่พบบ่อยที่สุดจากบันทึก query ในอดีต วิธีนี้จะตัดช่วงเริ่มต้นแบบไม่มีข้อมูล ซึ่งในช่วงชั่วโมงแรกของการทำงาน ผู้ใช้ทุกคนจะพบว่าไม่มีข้อมูลในแคชและทำให้เกิดค่าใช้จ่าย 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')ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจเกี่ยวกับแคชเชิงความหมายจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า แคชเชิงความหมายจะจับคู่ query ที่คล้ายกันแต่ไม่เหมือนกันทุกประการ โดยเปรียบเทียบ query embeddings กับที่เก็บเวกเตอร์ของ query embeddings ที่แคชไว้ ค่าเกณฑ์ความคล้ายคลึงจะควบคุมความสมดุลระหว่างอัตราการพบแคชกับความถูกต้องของคำตอบ และการแยกเนมสเปซตามพรอมป์ตระบบจะป้องกันการพบข้อมูลในแคชข้ามบริบทอย่างไม่ถูกต้อง สถาปัตยกรรมสองระดับที่ตรวจสอบแคชแบบตรงกันทุกประการก่อนแคชเชิงความหมายช่วยลดทั้งเวลาแฝงและค่าใช้จ่ายในการสร้าง embedding ต่อไป เราจะใช้การแคชคำนำหน้าพรอมป์ตในตัวของ OpenAI
คำถามที่พบบ่อย
บทเรียน “การแคชเชิงความหมายด้วยเวกเตอร์ฝัง” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การแคชเชิงความหมายด้วยเวกเตอร์ฝัง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส AI Engineering Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การแคชเชิงความหมายด้วยเวกเตอร์ฝัง”
สร้างแคชเชิงความหมายที่ดึงการตอบกลับซึ่งจัดเก็บไว้สำหรับคำค้นหาที่มีความหมายคล้ายกันแต่ไม่เหมือนกันทุกประการ โดยเปรียบเทียบเวกเตอร์ฝังของคำค้นหากับแคชเวกเตอร์ฝังจากคำขอก่อนหน้า คุณปฏิบัติ AI Engineering Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน AI Engineering Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน AI Engineering Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “การแคชเชิงความหมายด้วยเวกเตอร์ฝัง” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน AI Engineering Academy นี้ได้ไหม
ได้ บทเรียน AI Engineering Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การแคชแบบตรงตัวด้วย Redis
- การแคชเชิงความหมายด้วยเวกเตอร์ฝัง
- การแคชคำนำหน้าพรอมต์ของ OpenAI
- การรวมคำขอ การเลือกเส้นทางโมเดล และแดชบอร์ดต้นทุน