การดู : 117

14/07/2026 03:00น.

ภาพหน้าปก Golang The Series EP.160 วิธีสร้าง Enterprise Internal AI Tool (RAG) ด้วย Qdrant และ Gin ของ Superdev Academy

Golang The Series EP.160: วิธีสร้าง Enterprise Internal AI Tool (RAG) ด้วย Qdrant และ Gin

#Go

#Golang

#RAG

#Qdrant

#Gin Gonic

#Server-Sent Events

# Design System

#SSE Streaming

#Context Injection

#Enterprise AI Tool

#Superdev Academy

ยินดีต้อนรับชาว Superdev Academy เข้าสู่ EP.160 ซึ่งเป็น Workshop ใหญ่ประจำซีซันนี้ครับ! ตลอดเส้นทางซีรีส์ที่ผ่านมาเราได้ลุยกันตั้งแต่การติดตั้ง Environment, การแปลงข้อความเป็นเวกเตอร์ (Embeddings), การหั่นข้อมูลอย่างมีกลยุทธ์ (Data Chunking), ไปจนถึงเทคนิคสำคัญของระบบ RAG (Retrieval-Augmented Generation) อย่าง Hybrid Search และ Context Injection

วันนี้ได้เวลาที่เราจะนำองค์ความรู้ทั้งหมดมารวมร่างสร้าง Internal AI Tool ประจำองค์กรด้วยภาษา Go (Golang) นั่นคือเซิร์ฟเวอร์ระบบถาม-ตอบอัจฉริยะที่สามารถสืบค้นความรู้จากเอกสารภายใน มีระบบกรองสิทธิ์เข้าถึงข้อมูลตามแผนกด้วย Hybrid Search เพื่อความปลอดภัย และสตรีมคำตอบ (Streaming) ส่งกลับหาผู้ใช้งานแบบ Real-time ผ่านเทคนิค SSE ครับ!

โครงสร้างโปรเจกต์ (Project Structure)

เพื่อให้โค้ดของเรามีความยืดหยุ่น ดูแลรักษาง่าย และแยกหน้าที่การทำงานอย่างชัดเจน เราจะจัดโครงสร้างโปรเจกต์ตามสถาปัตยกรรมแบบแยกเลเยอร์ (Layered Architecture) ดังนี้ครับ:

Plaintext

internal-ai-tool/
├── config/
│   └── qdrant.go      # จัดการ Connection ของ Qdrant Vector DB
├── handlers/
│   └── qa.go          # HTTP Handlers สำหรับรับ Request และจัดการ SSE Streaming
├── services/
│   ├── ai.go          # Wrapper สำหรับเรียกใช้ OpenAI API (Embeddings & Chat Stream)
│   └── vector.go      # จัดการฟังก์ชันสืบค้นข้อมูล (Query) แบบ Hybrid Search บน Qdrant
├── go.mod
├── go.sum
└── main.go            # Entry point ของโปรเจกต์ และตั้งค่า Dependency Injection

เริ่มต้นด้วยการสร้าง Go Module และติดตั้ง Dependencies หลักของโปรเจกต์:

Bash

go mod init internal-ai-tool
go get github.com/gin-gonic/gin
go get github.com/sashabaranov/go-openai
go get github.com/qdrant/go-client/qdrant
go get github.com/google/uuid

เลเยอร์จัดเตรียมโครงสร้างและการเชื่อมต่อ (Configuration & Services)

ในส่วนนี้เราจะแยกการทำงานออกเป็น 3 ส่วนหลัก ได้แก่ การเชื่อมต่อฐานข้อมูลเวกเตอร์ (Vector Database), บริการจัดการโมเดล AI (OpenAI Service) และบริการสืบค้นข้อมูลเวกเตอร์ (Vector Service) เพื่อให้โค้ดแยกขาดจากกันตามหลักการ Clean Architecture ครับ

1. จัดการ Connection ของฐานข้อมูลเวกเตอร์ (config/qdrant.go)

เริ่มต้นด้วยการเขียนฟังก์ชันเพื่อเปิดการเชื่อมต่อ (Connection) ไปยัง Qdrant Vector Database โดยเราจะเตรียม Client ตัวนี้ไว้เพื่อส่งต่อทำ Dependency Injection ในหน้า main.go ต่อไป

Go

package config

import (
	"log"
	"github.com/qdrant/go-client/qdrant"
)

// NewQdrantClient ทำหน้าที่สร้างและส่งคืน Client สำหรับเชื่อมต่อกับ Qdrant Vector DB
func NewQdrantClient(host string, port int) *qdrant.Client {
	client, err := qdrant.NewClient(&qdrant.Config{
		Host: host,
		Port: port,
	})
	if err != nil {
		log.Fatalf("❌ ไม่สามารถเชื่อมต่อ Qdrant ได้: %v", err)
	}
	return client
}

2. บริการเชื่อมต่อ OpenAI API (services/ai.go)

ถัดมาคือเลเยอร์ที่คอยติดต่อกับ OpenAI API โดยทำหน้าที่หลัก 2 อย่างคือ การทำ Text Embedding (แปลงคำถามของ User เป็นเวกเตอร์) และ การทำ Chat Completion Stream เพื่อดึงคำตอบจากโมเดล GPT-4o ออกมาแบบสตรีมมิ่งทีละตัวอักษร

Go

package services

import (
	"context"
	"github.com/sashabaranov/go-openai"
)

type AIService struct {
	client *openai.Client
}

func NewAIService(apiKey string) *AIService {
	return &AIService{client: openai.NewClient(apiKey)}
}

// CreateEmbedding แปลงข้อความดิบให้กลายเป็น Vector (ค่านัยยะทางภาษา)
func (s *AIService) CreateEmbedding(ctx context.Context, text string) ([]float32, error) {
	req := openai.EmbeddingRequest{
		Input: []string{text},
		Model: openai.SmallEmbedding3Small, // ใช้โมเดลประสิทธิภาพสูงและประหยัดค่าใช้จ่าย
	}
	resp, err := s.client.CreateEmbeddings(ctx, req)
	if err != nil {
		return nil, err
	}
	return resp.Data[0].Embedding, nil
}

// GetChatStream ส่งข้อความไปยัง OpenAI และเปิดการรับข้อมูลแบบ Stream
func (s *AIService) GetChatStream(ctx context.Context, systemPrompt, userPrompt string) (*openai.ChatCompletionStream, error) {
	return s.client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
		Model: openai.GPT4o,
		Messages: []openai.ChatCompletionMessage{
			{Role: openai.ChatMessageRoleSystem, Content: systemPrompt},
			{Role: openai.ChatMessageRoleUser, Content: userPrompt},
		},
	})
}

3. บริการสืบค้นข้อมูลเวกเตอร์แบบกำหนดสิทธิ์ (services/vector.go)

ส่วนสำคัญที่สุดของระบบ RAG คือส่วนนี้ครับ เราจะใช้ฟีเจอร์การ Query ของ Qdrant มาทำ Hybrid Search โดยมีจุดเด่นคือการใส่ Filter สิทธิ์การเข้าถึงข้อมูลตามแผนก (Department) และกรองเอาเฉพาะเอกสารที่มีสถานะ active เท่านั้นพนักงานจึงจะสามารถค้นเจอข้อมูลชิ้นนั้นได้

Go

package services

import (
	"context"
	"github.com/qdrant/go-client/qdrant"
)

type VectorService struct {
	client *qdrant.Client
}

func NewVectorService(client *qdrant.Client) *VectorService {
	return &VectorService{client: client}
}

// SearchRelevantContext ค้นหาข้อมูลจาก Qdrant โดยมีเงื่อนไขกรองสิทธิ์ตามแผนกของผู้ใช้งาน
func (s *VectorService) SearchRelevantContext(ctx context.Context, vector []float32, department string) ([]string, error) {
	searchLimit := uint64(3) // ดึงเอกสารที่มีความคล้ายคลึงที่สุด 3 ชิ้น
	
	// กรองข้อมูล (Security Filter): ดึงเฉพาะเอกสารที่ตรงตามแผนกและมีสถานะ active เท่านั้น
	searchFilters := &qdrant.Filter{
		Must: []*qdrant.Condition{
			qdrant.NewMatchKeyword("department", department),
			qdrant.NewMatchKeyword("status", "active"),
		},
	}

	resp, err := s.client.Query(ctx, &qdrant.QueryPoints{
		CollectionName: "ai_knowledge_base",
		Query:          qdrant.NewQuery(vector...),
		Filter:         searchFilters,
		Limit:          &searchLimit,
	})
	if err != nil {
		return nil, err
	}

	var chunks []string
	for _, point := range resp {
		if contentVal, exists := point.Payload["content"]; exists {
			chunks = append(chunks, contentVal.GetStringValue())
		}
	}
	return chunks, nil
}

เลเยอร์ควบคุม API Endpoint และระบบ Server-Sent Events (SSE) Streaming

ในเลเยอร์นี้เราจะเข้าสู่ไฟล์ handlers/qa.go ซึ่งทำหน้าที่รับ Request จาก Frontend นำข้อมูลไปประมวลผลร่วมกับเลเยอร์ Service และส่งคำตอบกลับแบบ Real-time เพื่อให้เห็นภาพสถาปัตยกรรมภายในชัดเจน เราจะแบ่งโค้ดออกเป็น 3 ส่วนตามหน้าที่ของมันครับ

1. โครงสร้างข้อมูลและการรับ Request (Struct & Validation)

เริ่มต้นด้วยการประกาศ Package, นำเข้า Dependencies และสร้างเลเยอร์รับข้อมูล (Data Transfer Object) ในรูปแบบ Struct เพื่อรองรับคำถามรวมถึงสิทธิ์การเข้าถึงข้อมูลตามแผนกครับ

Go

package handlers

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"

	"internal-ai-tool/services"
	"github.com/gin-gonic/gin"
)

// QARequest โครงสร้างข้อมูลสำหรับรับ Payload JSON จากหน้าบ้าน
type QARequest struct {
	Question   string `json:"question" binding:"required"`
	Department string `json:"department" binding:"required"` // ใช้สำหรับทำ Security Filter
}

2. กระบวนการสืบค้นข้อมูลและการทำ Context Injection

ถัดมาเราจะเขียนฟังก์ชัน HandleQAStream โดยส่วนแรกนี้จะทำหน้าที่ผูกข้อมูล (JSON Binding), ส่งคำถามไปทำ Text Embedding และทำการค้นหาข้อมูลอ้างอิงด้วย Hybrid Search ก่อนจะนำเนื้อหาที่ได้มาประกอบร่างเป็น Prompt (Context Injection) เพื่อเตรียมส่งให้ LLM ครับ

Go

// HandleQAStream ควบคุมโฟลว์การทำงานของระบบ RAG และสตรีมคำตอบกลับผ่าน SSE
func HandleQAStream(ai *services.AIService, vector *services.VectorService) gin.HandlerFunc {
	return func(c *gin.Context) {
		var req QARequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "พารามิเตอร์ไม่ถูกต้อง"})
			return
		}

		ctx := c.Request.Context()

		// สเต็ปที่ 1: แปลงคำถามของ User เป็น Vector
		queryVector, err := ai.CreateEmbedding(ctx, req.Question)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "เกิดข้อผิดพลาดในการทำ Embedding"})
			return
		}

		// สเต็ปที่ 2: ค้นหาเอกสารในคลังความรู้พร้อมตรวจสอบสิทธิ์แผนก
		chunks, err := vector.SearchRelevantContext(ctx, queryVector, req.Department)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "ไม่สามารถสืบค้นคลังข้อมูลความรู้ได้"})
			return
		}

		// สเต็ปที่ 3: ทำ Context Injection เพื่อจำกัดกรอบคำตอบให้ AI
		contextText := ""
		if len(chunks) > 0 {
			contextText = strings.Join(chunks, "\n\n")
		} else {
			contextText = "ไม่มีข้อมูลอ้างอิงในระบบที่เกี่ยวข้องกับแผนกนี้"
		}

		systemPrompt := "คุณคือ AI ประจำองค์กร หน้าที่ของคุณคือตอบคำถามพนักงานโดยใช้ข้อมูลอ้างอิง (Context) ที่กำหนดให้เท่านั้น หากไม่มีคำตอบใน Context ให้ตอบตรงๆ ว่าไม่พบข้อมูลในระบบอ้างอิง ห้ามเดา"
		userPrompt := fmt.Sprintf("ข้อมูลอ้างอิง (Context):\n%s\n\nคำถาม: %s\nคำตอบ:", contextText, req.Question)

3. เปิดท่อส่งข้อมูลแบบ Server-Sent Events (SSE) Streaming

ส่วนสุดท้ายของฟังก์ชันคือการตั้งค่า HTTP Header ให้รองรับโปรโตคอล text/event-stream และใช้ฟีเจอร์ c.Stream ของ Gin Gonic ในการวนลูปรับข้อมูล Chunk จาก OpenAI API เพื่อดันข้อมูลออกไปหาฝั่งหน้าบ้านทันทีที่มีคำตัวอักษรใหม่ออกมาครับ

Go

		// สเต็ปที่ 4: เรียกใช้ Chat Completion Stream และตั้งค่าเซิร์ฟเวอร์ส่งข้อมูลแบบ SSE
		stream, err := ai.GetChatStream(ctx, systemPrompt, userPrompt)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "การเปิดท่อสตรีมข้อมูลล้มเหลว"})
			return
		}
		defer stream.Close()

		// ตั้งค่า HTTP Header ให้เปิดการเชื่อมต่อแบบข้ามฝั่งค้างไว้ (Keep-Alive)
		c.Header("Content-Type", "text/event-stream")
		c.Header("Cache-Control", "no-cache")
		c.Header("Connection", "keep-alive")

		// วนลูปสตรีมคำตอบออกไปทีละอักษรแบบ Real-time
		c.Stream(func(w io.Writer) bool {
			resp, err := stream.Recv()
			if errors.Is(err, io.EOF) {
				c.SSEvent("message", "[DONE]") // ส่งสัญญาณแจ้งหน้าบ้านเมื่อคำตอบจบลง
				return false
			}
			if err != nil {
				return false
			}

			if len(resp.Choices) > 0 {
				content := resp.Choices[0].Delta.Content
				if content != "" {
					c.SSEvent("message", content)
				}
			}
			return true
		})
	}
}

ประกอบร่างแอปพลิเคชันและทำ Dependency Injection (main.go)

มาถึงขั้นตอนสุดท้ายในการประกอบชิ้นส่วนทั้งหมดเข้าด้วยกันในไฟล์ main.go ครับ เลเยอร์นี้เปรียบเสมือนศูนย์บัญชาการหลักที่จะทำหน้าที่โหลดค่าคอนฟิกูเรชัน, เปิดการเชื่อมต่อฐานข้อมูลเวกเตอร์, ทำ Dependency Injection (DI) เพื่อมัดรวม Service และ Handler เข้าด้วยกัน ก่อนจะเปิดสตรีมมิ่งเซิร์ฟเวอร์ด้วย Gin Gonic ครับ

เพื่อให้บทความอ่านง่ายและเป็นระเบียบ เรามาเจาะลึกส่วนประกอบภายใน main.go กันครับ

1. การเตรียมระบบและการทำ Dependency Injection

ในส่วนแรกของฟังก์ชัน main จะเน้นไปที่การเตรียมระบบเพื่อความปลอดภัยและความยืดหยุ่นในการขยายสเกลระบบในอนาคต

Go

package main

import (
	"log"
	"os"

	"internal-ai-tool/config"
	"internal-ai-tool/handlers"
	"internal-ai-tool/services"
	"github.com/gin-gonic/gin"
)

func main() {
	// 1. โหลด API Key จาก Environment Variables เพื่อความปลอดภัยตามมาตรฐาน Enterprise
	openAIKey := os.Getenv("OPENAI_API_KEY")
	if openAIKey == "" {
		log.Fatal("ERROR: กรุณาตั้งค่า Environment Variable สำหรับ OPENAI_API_KEY")
	}

	// 2. เริ่มต้นเปิดการเชื่อมต่อกับ Qdrant Vector DB
	qdrantClient := config.NewQdrantClient("localhost", 6334)
	defer qdrantClient.Close() // ตรวจสอบให้มั่นใจว่าปิด Connection เมื่อเซิร์ฟเวอร์หยุดทำงาน

	// 3. ทำ Dependency Injection (DI) ส่งมอบออบเจกต์ไปใช้งานในเลเยอร์ที่เกี่ยวข้อง
	aiService := services.NewAIService(openAIKey)
	vectorService := services.NewVectorService(qdrantClient)

2. การจัดทำ Routing และการเปิดเซิร์ฟเวอร์ (Routing & Initialize)

ส่วนสุดท้ายคือการเปิดจุดรับส่งข้อมูล (Endpoint) เพื่อเชื่อมต่อกับระบบหน้าบ้าน (Frontend)

Go

	// 4. สร้างเซิร์ฟเวอร์ Routing ด้วย Gin Gonic
	r := gin.Default()

	// ลงทะเบียน Endpoint สำหรับระบุสิทธิ์แผนกและสตรีมคำตอบผ่าน POST Method
	r.POST("/api/v1/qa/stream", handlers.HandleQAStream(aiService, vectorService))

	// สตาร์ทระบบพร้อมทำงานบนพอร์ตที่กำหนด
	log.Println("🚀 เซิร์ฟเวอร์ Internal AI Tool เริ่มทำงานบนพอร์ต :8080...")
	r.Run(":8080")
}

นี่คือเนื้อหาส่วนสุดท้ายของบทความ (Daily Mission, FAQ, สรุป และบทนำของ EP ถัดไป) ที่ปรับปรุงเรื่องคำผิด (แก้จาก ยิงไอพี -> ยิง API) ลบสำนวน AI ออกทั้งหมด และเติมเนื้อหาในส่วน FAQ กับสรุป เพื่อให้บทความมีความสมบูรณ์และได้คะแนน SEO สูงสุดครับ:


🎯 ท้าให้ลอง (Daily Mission)

ยินดีด้วยครับ! ตอนนี้คุณมีสถาปัตยกรรมระบบถาม-ตอบจากเอกสาร (RAG System) ที่สะอาด ถูกต้องตามหลักการเขียนโปรแกรม และพร้อมให้ทีม Frontend นำไปเชื่อมต่อกับหน้าจอ UI เพื่อใช้งานจริงในองค์กรแล้วครับ

การบ้านใหญ่ส่งท้าย Workshop: ให้คุณทดลองสั่งรันเซิร์ฟเวอร์ตัวนี้ขึ้นมา แล้วลองใช้คำสั่ง curl หรือส่ง Request ผ่านโปรแกรม Postman โดยลองแกล้งเปลี่ยนข้อมูลในฟิลด์ "department" ใน JSON Body สลับไปมาระหว่างแผนกต่าง ๆ เช่น สลับจาก "Human Resources" ไปเป็น "Engineering" โดยใช้คำถามเดิม

ลองสังเกตดูครับว่า Security Filter หลังบ้านทำงานได้ตรงตามเป้าหมายไหม ระบบยอมเปิดเผยข้อมูลหรือเลือกหยิบคำตอบออกมาได้แตกต่างกันตามสิทธิ์ที่เราล็อกไว้ในคำสั่งสืบค้นข้อมูลเวกเตอร์บน Qdrant หรือไม่? ลองไปพิสูจน์ฝีมือการเขียนระบบ Backend นี้กันดูนะครับ!

FAQ (คำถามที่พบบ่อย)

ทำไมระบบถาม-ตอบ AI แบบ Streaming ถึงนิยมใช้ Server-Sent Events (SSE) มากกว่า WebSockets?

เพราะระบบนี้เป็นการส่งข้อมูลแบบทิศทางเดียว (One-way Streaming) จากเซิร์ฟเวอร์กลับไปหาคลื่นต์เท่านั้น SSE ทำงานบนโปรโตคอล HTTP มาตรฐาน จึงประหยัดทรัพยากรเซิร์ฟเวอร์และติดตั้งระบบง่ายกว่า WebSockets ที่ออกแบบมาสำหรับการสื่อสารแบบสองทาง (Bi-directional) ครับ

หากคลังข้อมูลบน Qdrant มีขนาดใหญ่ขึ้นมาก จะส่งผลให้ระบบค้นหาช้าลงไหม?

หากไม่ได้ทำ Index อาจช้าลงครับ แนะนำให้ทำ Payload Indexing บนฟิลด์ที่ใช้เป็นเงื่อนไขในการกรอง เช่น ฟิลด์ department และ status เพื่อช่วยให้ Qdrant ค้นหาข้อมูลเวกเตอร์ร่วมกับ Filter ได้อย่างรวดเร็วในระดับ Millisecond


สรุปเนื้อหาในตอนนี้

ในบทเรียนนี้เราได้เรียนรู้และลงมือทำในหัวข้อสำคัญประกอบด้วย:

  • การออกแบบสถาปัตยกรรมระบบ RAG (Retrieval-Augmented Generation) แยกเลเยอร์ด้วยภาษา Go (Golang)

  • การจัดการ Connection และการค้นหาข้อมูลเวกเตอร์พร้อมระบุเงื่อนไขกรองสิทธิ์ด้วย Qdrant Vector DB

  • เทคนิคการทำ Context Injection เพื่อควบคุมกรอบและบริบทการตอบคำถามของโมเดล OpenAI GPT-4o

  • การสร้าง HTTP API Endpoint และสตรีมคำตอบแบบ Real-time ด้วย Server-Sent Events (SSE) บน Gin Gonic

ในตอนต่อไป (EP.161): เราปิดกล่อง Workshop ระบบ RAG พื้นฐานได้อย่างสวยงามแล้วครับ แต่ในหน้างานจริงระดับ Enterprise ถ้าเกิดวันดีคืนดีพนักงานพร้อมใจกันอัปโหลดไฟล์คู่มือและเอกสารจัดซื้อนับร้อยไฟล์เข้ามาในระบบพร้อมกันล่ะ? ระบบสกัดข้อมูลที่ต้องรอยิง API ทีละคำสั่ง ย่อมเสี่ยงต่ออาการเซิร์ฟเวอร์ Timeout และระบบล่มแน่นอน

เราจะขึ้นบทเรียนสถาปัตยกรรมขั้นสูงในชื่อตอน "Async AI Tasks การประยุกต์ใช้ Worker Pools จัดการคิวงานสกัด AI ขนาดใหญ่หลังบ้าน" บอกเลยว่าสาย Backend ที่ต้องการรีดประสิทธิภาพระบบสเกลสูงห้ามพลาดเด็ดขาดครับ Gophers ของ Superdev Academy ทุกคน!

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

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

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

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

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

  • 🌐 Website: superdevacademy.com (คลังบทความและคอร์สเรียนฉบับเต็ม)