07/07/2026 03:00น.

Golang The Series EP.158: เทคนิค Context Injection ในสถาปัตยกรรม RAG
#Golang
#Go AI
#Qdrant
#Context Injection
#Vector Database
#OpenAI
#GPT-4o
#Hallucination
ต้อนรับเข้าสู่ EP.158 จาก Superdev Academy ครับ หลังจากที่เราได้พัฒนาระบบ Document Ingestion Pipeline สำหรับอ่านไฟล์ PDF เข้าสู่ Qdrant (Vector Database) ในตอนที่แล้ว
วันนี้เราจะมาต่อยอดระบบ RAG (Retrieval-Augmented Generation) ให้ทำงานได้จริง ด้วยเทคนิค Context Injection กระบวนการนี้คือการนำข้อมูล (Chunks) ที่ได้จากการทำ Semantic Search มาประกอบร่างเข้ากับ Prompt ของผู้ใช้งาน การส่งบริบทแวดล้อมลักษณะนี้เข้าไป จะช่วยตีกรอบให้ LLM อย่าง GPT-4o ตอบคำถามเฉพาะข้อมูลที่เรามี ป้องกันปัญหา Hallucination หรือการที่ AI คิดคำตอบไปเองได้อย่างมีประสิทธิภาพ
ลำดับการทำ Context Injection ในฝั่ง Backend (Go)
เพื่อให้เห็นภาพชัดเจนขึ้นว่าระบบหลังบ้านที่เขียนด้วย Golang ของเรา จัดการกับข้อมูลยังไงเวลาที่ผู้ใช้พิมพ์คำถามเข้ามา ลองดู Flow การทำงาน 3 สเตปหลักๆ ตามด้านล่างนี้ครับ:
Plaintext
[User พิมพ์คำถามเข้ามาในระบบ]
│
▼
1. Vector Search (Qdrant) ──> ระบบนำคำถามไปค้นในฐานข้อมูลเวกเตอร์ แล้วดึงข้อความ (Chunks) ที่เกี่ยวข้องที่สุด 3 อันดับแรกออกมา
│
▼
2. Context Injection ───────> ประกอบร่างโดยจับ "คำถาม" + "เนื้อหาทั้ง 3 Chunks" มารวมเข้าด้วยกันใน Prompt Template
│
▼
3. Call LLM API (OpenAI) ───> ส่ง Prompt ที่มีบริบทครบถ้วนไปให้ AI อ่าน ➡️ โมเดลจะสรุปคำตอบกลับมาโดยอิงจากข้อมูลของเราเป๊ะๆเทคนิคออกแบบ Prompt Template ดัก AI หลอน (Hallucination)
พอเราดึงข้อมูล (Chunks) ที่เกี่ยวข้องออกมาได้แล้ว สเตปต่อไปคือการประกอบร่างส่งให้ AI อ่านครับ ทริคสำคัญของการทำ RAG คือเราต้อง "คุม" โมเดลให้อยู่หมัดด้วยการเขียน System Prompt เป็นกฎเหล็กดักไว้เลยว่า:
"จงตอบคำถามโดยอิงจากข้อมูลอ้างอิง (Context) ที่ให้ไว้เท่านั้น หากหาคำตอบในข้อมูลไม่เจอ ให้ตอบไปตรงๆ ว่าไม่ทราบ ห้ามเดาหรือแต่งเนื้อหาขึ้นมาเองเด็ดขาด"
ท่านี้คือหัวใจหลักที่จะช่วยควบคุมความปลอดภัย (Safety) และรับประกันว่าคำตอบจากระบบโปรดักชันของเราจะเชื่อถือได้ 100% ครับ
สำหรับชาว Go (Golang) ท่ามาตรฐานที่เราแนะนำในคลาสของ Superdev Academy สำหรับใช้ประกอบร่าง (Construct) ตัว Prompt แบ็กเอนด์ ก็คือการใช้แพ็กเกจคู่บุญอย่าง fmt.Sprintf สำหรับเคสที่โครงสร้างไม่ซับซ้อน หรือถ้า Prompt เริ่มยาวและมีตัวแปรเยอะ การหันไปใช้ text/template ก็จะช่วยให้โค้ดคลีนและจัดการโครงสร้าง String ได้ง่ายขึ้นมากครับ
ลงมือเขียนโค้ด Go ทำระบบ RAG แบบ Step-by-Step
การแปะโค้ดรวดเดียวอาจจะทำให้ไล่ Flow ลำบาก เรามาลองหั่นการทำงานของ main.go ออกเป็น 4 ส่วนหลักๆ เพื่อให้เห็นภาพการทำ Context Injection ได้ชัดเจนที่สุดกันครับ
1. ตั้งค่าการเชื่อมต่อ (Initialize Clients)
สเตปแรกเราต้องสร้าง Client เพื่อคุยกับทั้ง OpenAI (สำหรับ Embedding และ LLM) และ Qdrant (สำหรับ Vector Search) ให้เรียบร้อยก่อนครับ
Go
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"github.com/qdrant/go-client/qdrant"
"github.com/sashabaranov/go-openai"
)
func main() {
ctx := context.Background()
userQuery := "พนักงานใหม่ลาพักร้อนได้กี่วัน?"
// ดึง API Key จาก Environment Variables ป้องกันความลับรั่วไหล
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
log.Fatal("Please set your OPENAI_API_KEY environment variable")
}
openaiClient := openai.NewClient(apiKey)
// เชื่อมต่อ Qdrant ผ่าน gRPC Port (ค่าเริ่มต้น 6334)
qdrantClient, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
if err != nil {
log.Fatalf("Qdrant connection failed: %v", err)
}
defer qdrantClient.Close()
// ... (โค้ดส่วนต่อไป)
2. แปลงคำถามให้เป็น Vector (Query Embedding)
ก่อนจะไปค้นหาในฐานข้อมูล เราต้องเอาคำถามของผู้ใช้ (Query) ไปแปลงเป็นตัวเลขเวกเตอร์ 1,536 มิติด้วยโมเดล text-embedding-3-small กันก่อน ท่านี้เราเคยทำกันมาแล้วใน EP.156 ครับ
Go
embReq := openai.EmbeddingRequest{
Input: []string{userQuery},
Model: openai.SmallEmbedding3Small,
}
embResp, err := openaiClient.CreateEmbeddings(ctx, embReq)
if err != nil {
log.Fatalf("Embedding failed: %v", err)
}
queryVector := embResp.Data[0].Embedding
3. ค้นหาและสกัดข้อมูลจาก Qdrant (Vector Search)
นำเวกเตอร์คำถามไปค้นหาใน Collection ai_knowledge_base โดยเราจะตั้งลิมิตดึงมาแค่ 3 อันดับแรก (Top-3) ที่มีคะแนนความคล้ายคลึงสูงสุด จากนั้นวนลูปเพื่อดึงเฉพาะข้อความดิบ (Payload) ออกมาต่อกันเป็น String เดียว
Go
searchLimit := uint64(3)
searchResp, err := qdrantClient.Query(ctx, &qdrant.QueryPoints{
CollectionName: "ai_knowledge_base",
Query: qdrant.NewQuery(queryVector...),
Limit: &searchLimit,
})
if err != nil {
log.Fatalf("Qdrant query failed: %v", err)
}
// สกัดเนื้อหาดิบ (Chunks) มารวมกันด้วยการเว้นบรรทัด
var contextChunks []string
for _, point := range searchResp {
if contentValue, exists := point.Payload["content"]; exists {
content := contentValue.GetStringValue()
contextChunks = append(contextChunks, fmt.Sprintf("- %s", content))
}
}
injectedContext := strings.Join(contextChunks, "\n\n")
4. ฉีดข้อมูลเข้า Prompt และเรียกใช้ GPT-4o (Context Injection)
นี่คือพระเอกของงานนี้ครับ เราจะเอา injectedContext ที่รวบรวมมาได้ ไปประกอบร่างกับ System Prompt เพื่อตีกรอบให้ AI ตอบคำถามเฉพาะสิ่งที่เราค้นเจอเท่านั้น
Go
systemPrompt := "คุณคือผู้ช่วยอัจฉริยะภายในองค์กร หน้าที่ของคุณคือตอบคำถามของพนักงานโดยใช้ข้อมูลอ้างอิง (Context) ที่กำหนดให้เท่านั้น ห้ามใช้ความรู้ภายนอกเด็ดขาด หากไม่มีคำตอบใน Context ให้ตอบว่า 'ขออภัยครับ ข้อมูลในระบบไม่ระบุเรื่องนี้'"
// ใช้ fmt.Sprintf ประกอบร่าง Context เข้ากับคำถาม
userPromptWithContext := fmt.Sprintf(`จงตอบคำถามด้านล่างนี้โดยอิงจากข้อมูลอ้างอิงที่เตรียมไว้ให้เท่านั้น
[ข้อมูลอ้างอิง (Context)]
%s
[คำถาม]
%s
คำตอบ:`, injectedContext, userQuery)
// ส่งเข้า ChatCompletion API ของ OpenAI
chatResp, err := openaiClient.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: openai.GPT4o,
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleSystem, Content: systemPrompt},
{Role: openai.ChatMessageRoleUser, Content: userPromptWithContext},
},
})
if err != nil {
log.Fatalf("Chat completion failed: %v", err)
}
fmt.Println("🤖 คำตอบจาก AI (การันตีความถูกต้องจากเอกสาร):")
fmt.Println(chatResp.Choices[0].Message.Content)
}สิ่งที่ Gopher ต้องระวัง: เรื่อง Context Window และค่า API (Pricing)
แม้ว่าเทคนิค Context Injection จะช่วยให้ AI ตอบคำถามได้เป๊ะและแก้ปัญหาคนหลอนได้ชะงัด แต่พอเราเอาไประบบรันบนโปรดักชันจริง (Production) มี "หลุมพราง" 2 เรื่องใหญ่ๆ ที่นักพัฒนาต้องคอยมอนิเตอร์และบริหารจัดการให้ดีครับ:
Token Bloat (ค่าใช้จ่ายบานปลาย): อย่าลืมว่าแม้ผู้ใช้จะพิมพ์ถามมาแค่ประโยคสั้นๆ แต่หลังบ้าน (Backend) ของเราต้องไปควานหาข้อความมา 3-5 Chunks (ซึ่งอาจจะยาวหลายพัน Tokens) เพื่อยัดพ่วงไปใน Prompt ตลอดเวลา นั่นหมายความว่าค่าใช้จ่ายฝั่ง Input Tokens จะพุ่งปรี๊ดอย่างรวดเร็วตามจำนวนครั้งที่มีคนใช้งานครับ
Max Context Limit (ข้อความล้นจนโมเดล Error): ถ้าเราคิวรีข้อมูลดึงมาเยอะเกินไปจนล้นขนาดหน้าต่างรับข้อมูล (Context Window) ที่โมเดลเวอร์ชันนั้นๆ รองรับ ตัว API ของ OpenAI จะตี Error กลับมาทันทีครับ ท่าแก้ปัญหานี้คือเราต้องทำ Token Management ควบคู่ไปด้วยเสมอ (แนะนำให้ย้อนกลับไปดูวิธีนับและตัดคำด้วยไลบรารี
tiktoken-goใน EP.149 ครับ)
🎯 Daily Mission: ลองท้าทายระบบด้วย Edge Cases
ลองนำโค้ดในตอนนี้ไปทดสอบรันร่วมกับไฟล์ PDF คู่มือกฎระเบียบบริษัทที่คุณเคยทำระบบ Ingestion เอาไว้ใน EP.157 ดูครับ จากนั้นให้ลอง "แกล้ง" ยิงคำถามที่ไม่มีทางอยู่ในคู่มือนั้นแน่นอนลงไป เช่น "สูตรการทำผัดไทยโบราณ" หรือ "วิธีซ่อมเครื่องซักผ้า"
โจทย์ชวนเช็ก: ลองสังเกตดูว่าตัวโมเดล AI ตอบกลับมาตามกฎเหล็กที่เราตั้งบล็อกเอาไว้ใน System Prompt ไหม เช่น ตอบว่า "ขออภัยครับ ข้อมูลในระบบไม่ระบุเรื่องนี้" หรือมันยังแอบเผลอหลุดไปดึงความรู้สาธารณะบนอินเทอร์เน็ตมาตอบอยู่? ลองปรับจูนตัวประโยค Prompt Template ของคุณจนกระทั่งควบคุมพฤติกรรมของ LLM ให้อยู่ในกรอบข้อความอ้างอิงได้ 100% ดูนะครับ!
💬 FAQ: คำถามที่พบบ่อยประจำตอน
หากผลการทำ Semantic Search ได้ค่า Score ต่ำมาก ควรส่งข้อมูลนั้นไปฉีดใน Prompt หรือไม่?
ไม่ควรอย่างยิ่งครับ ในโค้ดระดับโปรดักชันเราควรเขียน Logic ตรวจสอบค่า point.Score ก่อนเสมอ หากคะแนนความคล้ายคลึงต่ำกว่า Threshold ที่เรากำหนดไว้ (เช่น ต่ำกว่า 0.50) ให้ข้ามข้อความนั้นไปเลย เพราะถ้าเรายังฝืนฉีดข้อมูลขยะที่ไม่เกี่ยวข้องเข้าไปใน Context จะทำให้ LLM สับสน และเสี่ยงต่อการเกิดอาการหลอน (Hallucination) ได้ง่ายขึ้นครับ
การส่งข้อมูลแบบเรียง Chunks ต่อกันดื้อๆ ใน Prompt ทำให้โมเดลมองข้ามรายละเอียดตรงกลางจริงไหม?
เป็นเรื่องจริงในทางงานวิจัยครับ ปัญหานี้เรียกว่า "Lost in the Middle" โมเดลส่วนใหญ่มักจะจดจำและให้ความสำคัญกับข้อความที่อยู่ตอนต้นและตอนท้ายของ Prompt มากกว่าเนื้อหาตรงกลาง วิธีแก้ในระดับขั้นสูงคือการนำเทคนิค Reranking มาช่วยจัดอันดับ Chunks ที่สำคัญที่สุดให้ย้ายมาอยู่ที่จุดบนสุดและล่างสุดก่อนทำการ Inject ครับ
📝 สรุปเนื้อหาในตอนนี้
ใน EP.158 นี้ เราได้ต่อจิ๊กซอว์ชิ้นสำคัญของการทำระบบ RAG ได้สำเร็จ ตั้งแต่การทำ Vector Search บนฐานข้อมูล Qdrant เพื่อควานหาข้อความที่เกี่ยวข้อง การนำสกัดข้อมูลมาทำ Context Injection ครอบด้วยกฎเหล็ก System Prompt ไปจนถึงการยิงเข้า OpenAI API เพื่อให้ได้คำตอบที่แม่นยำและปลอดภัยที่สุดสำหรับนำไปใช้งานจริงในองค์กร
ตอนนี้เราดึงเอกสารภายนอกเข้ามาแปลงพิกัดความหมายและคุม LLM ให้ตอบคำถามได้อย่างสมบูรณ์แบบแล้ว แต่ในโลกความจริง ข้อมูลส่วนใหญ่ไม่ได้มีแค่เวกเตอร์อย่างเดียว เรายังมีข้อมูลแบบสัมพันธ์ (Relational) เช่น รหัสพนักงาน, แผนก, หรือยอดเงินคงเหลือ อยู่ใน SQL Database เดิมด้วย
ในตอนต่อไป (EP.159): ตอนหน้าเราจะขยับสถาปัตยกรรมหลังบ้านไปอีกขั้นกับเทคนิค "Hybrid Search: ผสมผสาน SQL และ Vector Search ใน Go โปรเจกต์เดียว" ห้ามพลาดครับ!
ฝากกดติดตามพวกเราได้ที่ Superdev Academy ในทุกช่องทางนะครับ!
🔵 Facebook: Superdev Academy Thailand (อัปเดตข่าวสารและบทความใหม่)
🎬 YouTube: Superdev Academy Channel (ติวเข้มแบบวิดีโอ)
📸 Instagram: @superdevacademy (เกร็ดความรู้สั้นๆ และเบื้องหลังการทำงาน)
🎬 TikTok: @superdevacademy (Tips & Tricks ฉบับย่อยง่าย)
🌐 Website: superdevacademy.com (คลังบทความและคอร์สเรียนฉบับเต็ม)