การดู : 134

02/11/2026 15:35น.

ระบบ Cache-Aside Pattern สำหรับ AI Responses ด้วย Redis

Golang The Series EP.163: Caching AI Responses ลดต้นทุนและเพิ่มความเร็วแอปพลิเคชัน AI ด้วย Redis Caching

#Semantic Caching

#Cache-Aside Pattern

#ลดต้นทุน AI

#Redis Caching

#Golang

#Go

สวัสดีครับชาว Gophers! หลังจากที่เราได้ลองปล่อยตัวเรียกโมเดลขนานหลายค่ายพร้อมกันใน EP.162 ไปแล้ว สิ่งสำคัญต่อมาที่เราหลีกเลี่ยงไม่ได้เลยเวลาสเกลระบบ AI ให้รองรับผู้ใช้งานจำนวนมาก คือเรื่องของ การควบคุมค่าใช้จ่าย (Cost Optimization) และ การลดระยะเวลาตอบสนอง (Latency) ครับ

ถ้าเราลองสังเกตพฤติกรรมของยูสเซอร์ในระบบ จะพบแพตเทิร์นชัดเจนว่า มักจะมีกลุ่มคำถามยอดฮิตที่คนชอบถามซ้ำๆ กันเสมอ เช่น "วันหยุดประจำปีของบริษัทมีวันไหนบ้าง?" หรือ "ขั้นตอนการเบิกค่าเดินทางทำยังไง?" หากสถาปัตยกรรมของเรายังวิ่งไปเคาะประตูเรียก LLM (Large Language Model) ให้ประมวลผลคำถามเดิมซ้ำๆ ทุกครั้ง สิ่งที่ตามมาคือเราต้องจ่ายค่า API Token โดยใช่เหตุ และยูสเซอร์ก็ต้องนั่งรอ AI คิดคำตอบนานหลายวินาทีเหมือนเดิม

วิธีแก้ปัญหานี้ที่ง่ายและเห็นผลที่สุดคือการทำ Cache-Aside Pattern โดยเอา Redis เข้ามาคั่นกลางก่อนจะวิ่งไปหา AI ซึ่งจะช่วยลดต้นทุนของคำถามซ้ำให้เหลือ 0 บาท แถมยังดึงคำตอบกลับไปโชว์หน้าแอปได้ไวในระดับมิลลิวินาที (Milliseconds) เท่านั้นครับ

สถาปัตยกรรมระบบคลังข้อมูลชั่วคราว (Cache-Aside Pattern for AI)

กลยุทธ์ Cache-Aside (หรือ Lazy Loading) จะมี Flow การทำงานที่เข้าใจง่าย ไม่ซับซ้อนตามไดอะแกรมนี้ครับ:

Plaintext

  [คำถามจากผู้ใช้]
         │
  1. สร้าง Cache Key (SHA-256)
         │
  ┌──────┴────────────────────────┐
  ▼                               ▼
[ตรวจสอบใน Redis]         [ตรวจสอบใน Redis]
   (Cache HIT)               (Cache MISS)
      │                           │
ส่งตอบกลับทันที (ms)       2. วิ่งไปคิวรีที่ LLM (3s)
                                  │
                           3. บันทึกผลลง Redis + TTL
                                  │
                              ส่งตอบกลับผู้ใช้

การติดตั้ง Redis Client

เราจะใช้ Library ยอดนิยมอย่าง go-redis เวอร์ชันล่าสุดในการจัดการ Connection Pool และรับส่งข้อมูลกับ Redis ครับ:

Bash

go get github.com/redis/go-redis/v9

โครงสร้างข้อมูลและการสร้างคีย์ (Key Generation)

เพื่อไม่ให้โค้ดรก เราจะแยกส่วนการต่อเชื่อม Redis และฟังก์ชันแปลงคำถามยาวๆ ให้กลายเป็น Hash Key สั้นๆ ก่อนครับ

Go

package main

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/redis/go-redis/v9"
)

var (
	rdb *redis.Client
	ctx = context.Background()
)

// generateCacheKey แปลงข้อความคำถามยาวๆ ให้เป็น Hash Key ขนาดคงที่ เพื่อความประหยัดพื้นที่ใน Redis
func generateCacheKey(question string) string {
	hash := sha256.Sum256([]byte(question))
	return fmt.Sprintf("ai:cache:%s", hex.EncodeToString(hash[:]))
}

ฟังก์ชันจัดการ Cache-Aside Core Logic

ถัดมาคือโค้ดจำลองการเรียก LLM (ใช้ time.Sleep จำลองความหน่วง) และฟังก์ชันหลักในการเช็กสถานะ Cache Hit / Cache Miss ครับ

Go

// askLLMSimulator จำลองการทำงานของ LLM API ที่มีความหน่วงและมีต้นทุนค่า Token
func askLLMSimulator(question string) string {
	fmt.Println("🤖 [LLM] Cache Miss! กำลังเรียกประมวลผลผ่านโมเดลจริง (ใช้เวลา 3 วินาที)...")
	time.Sleep(3 * time.Second) 
	return fmt.Sprintf("คำตอบสำหรับคำถาม '%s' คือ: อ้างอิงตามข้อบังคับสวัสดิการพนักงานประจำปี...", question)
}

// getAIResponseWithCache ฟังก์ชันตรวจสอบและดึงข้อมูลจาก Cache
func getAIResponseWithCache(question string) (string, string) {
	cacheKey := generateCacheKey(question)

	// 1. ตรวจสอบข้อมูลใน Redis ก่อน (Cache Hit Check)
	val, err := rdb.Get(ctx, cacheKey).Result()
	if err == nil {
		return val, "HIT" // เจอข้อมูลใน Cache ส่งกลับได้ทันที
	}

	// หากเกิด Error อื่นที่ไม่ใช่ "หาคีย์ไม่เจอ" (redis.Nil) ให้พิมพ์ Log แจ้งเตือนไว้
	if !errors.Is(err, redis.Nil) {
		log.Printf("⚠️ Redis connection error: %v", err)
	}

	// 2. กรณีเกิด Cache Miss -> วิ่งไปเรียก LLM ตัวจริง
	answer := askLLMSimulator(question)

	// 3. บันทึกผลลัพธ์ลง Redis พร้อมตั้งเวลาหมดอายุ (TTL) ไว้ที่ 1 ชั่วโมง
	err = rdb.Set(ctx, cacheKey, answer, 1*time.Hour).Err()
	if err != nil {
		log.Printf("❌ ไม่สามารถบันทึกข้อมูลลง Cache ได้: %v", err)
	}

	return answer, "MISS"
}

ส่วนควบคุมระบบหลักและการทดสอบเวลา: main.go

ลองนำมารวมกันในฟังก์ชัน main เพื่อทดสอบยิงคำถามเดิมซ้ำกัน 2 ครั้ง แล้วมาดูความต่างของเวลากันครับ

Go

func main() {
	// เชื่อมต่อไปยัง Redis Server บนเครื่อง Localhost
	rdb = redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})
	defer rdb.Close()

	question := "สิทธิ์การลาพักร้อนของพนักงานใหม่มีกี่วัน?"

	fmt.Println("⚡ เริ่มทดสอบระบบ AI Response Caching...")
	fmt.Println("==================================================")

	// --- เรียกครั้งที่ 1: สภาวะเริ่มต้น (ต้องเกิด Cache Miss) ---
	start1 := time.Now()
	ans1, status1 := getAIResponseWithCache(question)
	fmt.Printf("[%s] คำตอบ: %s\n⏱️ ใช้เวลาประมวลผลรวม: %v\n", status1, ans1, time.Since(start1))
	fmt.Println("--------------------------------------------------")

	// --- เรียกครั้งที่ 2: ใช้คำถามเดิมเป๊ะ (ต้องเกิด Cache Hit) ---
	start2 := time.Now()
	ans2, status2 := getAIResponseWithCache(question)
	fmt.Printf("[%s] คำตอบ: %s\n⚡ ใช้เวลาประมวลผลรวม: %v\n", status2, ans2, time.Since(start2))
	fmt.Println("==================================================")
}

ขยับไปอีกขั้นด้วย Semantic Caching

ตัวอย่างโค้ดข้างต้นใช้วิธี Exact Match Caching หรือการตรวจสอบตัวอักษรของคำถามแบบตรงกันทุกตัวเป๊ะๆ ซึ่งถ้าผู้ใช้งานสลับคำ เคาะวรรคต่างกันนิดเดียว หรือพิมพ์ผิดไปตัวเดียว ระบบจะตรวจเป็น Cache Miss ทันที ทั้งๆ ที่ความหมายของคำถามเหมือนเดิม เช่น:

  • "สิทธิ์การลาพักร้อนมีกี่วัน?"

  • "พนักงานใหม่ลาพักร้อนได้กี่วันครับ?"

เพื่อแก้ปัญหานี้ ระบบ Backend ของแอป AI ยุคใหม่มักจะขยับไปใช้ระบบ Semantic Caching ร่วมกับ Redis Vector Similarity Search (VSS) แทน โดยเราจะนำคำถามใหม่ที่ส่งเข้ามาไปแปลงเป็น Vector Embedding ก่อน จากนั้นยิงคำสั่งเข้าไปค้นหาใน Redis เพื่อหาความใกล้เคียงของความหมาย (เช่น คำนวณหา Cosine Similarity ให้ได้ค่า >= 0.95) ถ้าเจอคำถามเก่าในฐานข้อมูลที่มีความหมายใกล้เคียงกันในเกณฑ์ เราก็สามารถดึงคำตอบนั้นมาสวมรอยตอบได้ทันทีโดยไม่ต้องไปกวน LLM เลยครับ

สรุปและตอนต่อไป (EP.164)

การทำ Cache-Aside Pattern ด้วย Redis เป็นท่ามาตรฐานที่ช่วยเซฟทั้งเงินค่า API Token และเซฟทั้งเวลาของยูสเซอร์ได้อย่างเห็นผลชัดเจนที่สุดตราบใดที่มีคนส่งคำถามซ้ำๆ เข้ามาในระบบครับ

ในตอนต่อไป (EP.164): แม้ว่า Cache จะช่วยกรองโหลดจากคำถามซ้ำได้ดีมาก แต่ถ้าเกิดกรณีที่มีผู้ใช้งานบางกลุ่มจงใจยิงสแปม หรือเขียนบอตส่งคำถามแปลกๆ จำนวนมหาศาลเข้ามาพร้อมกันเพื่อเจาะระบบล่ะ? ต่อให้มี Cache ก็อาจจะเอาไม่อยู่ แถมโควตา API หลักขององค์กรอาจจะปลิวหายไปในพริบตา ตอนหน้าเราจะมาสร้างเกราะป้องกันระบบด้วยการทำ Rate Limiting AI Requests ด้วยกลไก Token Bucket ใน Go ห้ามพลาดครับ Gophers!

ฝากกดติดตามพวกเราได้ที่ Superdev Academy ในทุกช่องทางนะครับ!

  • 🔵 Facebook: Superdev Academy Thailand (อัปเดตข่าวสารและบทความใหม่)

  • 🎬 YouTube: Superdev Academy Channel (ติวเข้มแบบวิดีโอ)

  • 📸 Instagram: @superdevacademy (เกร็ดความรู้สั้นๆ และเบื้องหลังการทำงาน)

  • 🎬 TikTok: @superdevacademy (Tips & Tricks ฉบับย่อยง่าย)

FAQ: คำถามที่พบบ่อยเกี่ยวกับบทความนี้

รวมคำถามและคำตอบที่ช่วยให้คุณเข้าใจเนื้อหาในบทความนี้ได้ดียิ่งขึ้น