0Pricing
AI Engineering Academy · บทเรียน

การแคชเชิงความหมายด้วยเวกเตอร์ฝัง

สร้างแคชเชิงความหมายที่ดึงการตอบกลับซึ่งจัดเก็บไว้สำหรับคำค้นหาที่มีความหมายคล้ายกันแต่ไม่เหมือนกันทุกประการ โดยเปรียบเทียบเวกเตอร์ฝังของคำค้นหากับแคชเวกเตอร์ฝังจากคำขอก่อนหน้า

การแคชเชิงความหมายด้วยเวกเตอร์ฝัง เป็นบทเรียน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การแคชแบบตรงตัวด้วย Redis
  2. การแคชเชิงความหมายด้วยเวกเตอร์ฝัง
  3. การแคชคำนำหน้าพรอมต์ของ OpenAI
  4. การรวมคำขอ การเลือกเส้นทางโมเดล และแดชบอร์ดต้นทุน
← กลับไปที่ AI Engineering Academy