การดู : 119

09/06/2026 04:21น.

สถาปัตยกรรมการต่อท่อระบบสตรีมมิ่ง AI Chatbot ด้วย Gin Framework และ OpenAI API

Golang The Series EP.150: Workshop 1: สร้าง Simple AI Chatbot Server ด้วย Gin Framework

#Gin Framework

#Go Web Server

#AI Chatbot Backend

#Real-time Streaming

#Server-Sent Events

#Go

#Golang

เดินทางมาถึง EP.150 กันแล้วครับ Gophers ทุกคน! ยินดีด้วยที่คุณได้สะสมจิ๊กซอว์ชิ้นสำคัญมาครบหมดแล้ว ตั้งแต่การจัดการ Environment ด้วย Docker, การทำ Streaming ด้วย Channels, ไปจนถึงการควบคุมงบประมาณด้วย Token Management

ในตอนนี้ ได้เวลาที่เราจะนำความรู้ทั้งหมดมารวมร่างปล่อยของจริงใน Workshop แรกของซีซันนี้ เราจะสร้าง Simple AI Chatbot Server ที่รองรับการสตรีมข้อมูลข้อความแบบ Real-time โดยใช้ Gin Framework ร่วมกับ OpenAI SDK กันครับ!

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

เพื่อให้โค้ดของเราสะอาดและดูแลรักษาง่าย เราจะจัดโครงสร้างตามมาตรฐาน Go แบบเรียบง่ายดังนี้:

Plaintext

ai-chatbot-server/
├── main.go
├── handlers/
│   └── chat.go
├── go.mod
└── go.sum

เริ่มสร้างโปรเจกต์และติดตั้ง Dependencies ที่จำเป็นผ่าน Terminal:

Bash

go mod init ai-chatbot-server
go get github.com/gin-gonic/gin
go get github.com/sashabaranov/go-openai

เขียนส่วนขับเคลื่อนหลัก: main.go

ในไฟล์นี้เราจะทำหน้าที่ดึง API Key จากระบบ, เริ่มต้นทำงาน OpenAI Client และตั้งค่า Route ของ Gin Framework เพื่อเตรียมเปิดท่อสตรีมข้อมูล

Go

package main

import (
	"log"
	"os"

	"ai-chatbot-server/handlers"
	"github.com/gin-gonic/gin"
	"github.com/sashabaranov/go-openai"
)

func main() {
	// 1. ดึง API Key จาก Environment เพื่อความปลอดภัย
	apiKey := os.Getenv("OPENAI_API_KEY")
	if apiKey == "" {
		log.Fatal("ERROR: OPENAI_API_KEY env variable is required")
	}

	// 2. สร้าง OpenAI Client
	aiClient := openai.NewClient(apiKey)

	// 3. Setup Gin Engine
	r := gin.Default()

	// ทำการฉีด Dependency (Inject Client) เข้าไปใน Handler ผ่าน Closure
	r.POST("/api/chat/stream", handlers.HandleChatStream(aiClient))

	log.Println("🚀 AI Chatbot Server starting on :8080...")
	r.Run(":8080")
}

สร้างขบวนการส่งข้อมูล: handlers/chat.go

เราจะใช้เทคนิค Server-Sent Events (SSE) ร่วมกับฟีเจอร์ Streaming ของ OpenAI เพื่อดันข้อความออกไปหา Client ทันทีที่ AI คิดเสร็จทีละคำ ซึ่ง Gin มีฟังก์ชัน c.Stream() มาช่วยซัพพอร์ตจุดนี้ให้เขียนง่ายขึ้นมากครับ

Go

package handlers

import (
	"context"
	"errors"
	"io"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/sashabaranov/go-openai"
)

// โครงสร้างสำหรับรับข้อมูล Request จาก Client
type ChatRequest struct {
	Message string `json:"message" binding:"required"`
}

func HandleChatStream(client *openai.Client) gin.HandlerFunc {
	return func(c *gin.Context) {
		var req ChatRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
			return
		}

		ctx := c.Request.Context()

		// 1. สร้างคำร้องขอไปยัง OpenAI แบบเปิดโหมด Stream
		streamReq := openai.ChatCompletionRequest {
			Model:  openai.GPT4o, // ตรวจสอบให้มั่นใจว่าใช้ go-openai เวอร์ชันล่าสุด หรือเปลี่ยนเป็น "gpt-4o" แบบสตริงตรงๆ ได้
			Stream: true,
			Messages: []openai.ChatCompletionMessage{
				{
					Role:    openai.ChatMessageRoleUser,
					Content: req.Message,
				},
			},
		}

		stream, err := client.CreateChatCompletionStream(ctx, streamReq)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		defer stream.Close()

		// สำคัญสำหรับ Production: ตั้งค่า Headers สำหรับ Server-Sent Events (SSE)
		// เพื่อป้องกันไม่ให้ Reverse Proxy (เช่น Nginx, Cloudflare) ทำการกักข้อมูล (Buffer)
		c.Header("Content-Type", "text/event-stream")
		c.Header("Cache-Control", "no-cache")
		c.Header("Connection", "keep-alive")
		c.Header("Transfer-Encoding", "chunked")

		// 2. ใช้ c.Stream ของ Gin ในการยิงข้อมูลออกแบบ Real-time ต่อเนื่อง
		c.Stream(func(w io.Writer) bool {
			select {
			case <-ctx.Done():
				// ปลอดภัยสูงสุด: หาก Client ตัดการเชื่อมต่อ (ปิดหน้าจอ/กดยกเลิก) ลูปจะหยุดทำงานทันที
				return false

			default:
				response, err := stream.Recv()
				if errors.Is(err, io.EOF) {
					// เมื่อส่งข้อมูลหมดแล้ว ส่งสัญญาณบอก Client ว่าสิ้นสุดการทำงาน
					c.SSEvent("message", "[DONE]")
					return false
				}

				if err != nil {
					// หากเกิด Error ระหว่างสตรีม ยิง Event แจ้งฝั่ง Client แล้วตัดการทำงาน
					c.SSEvent("error", err.Error())
					return false
				}

				// ดึงข้อความ chunk เล็กๆ ที่ AI ส่งมาในรอบนั้นๆ
				if len(response.Choices) > 0 {
					content := response.Choices[0].Delta.Content
					if content != "" {
						// ส่งข้อมูลกลับไปยัง Client แบบ Real-time ทันที ผ่าน SSE format
						c.SSEvent("message", content)
					}
				}

				return true // คืนค่า true เพื่อวนลูปดึงข้อมูลใน Chunk ถัดไป
			}
		})
	}
}

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

เมื่อรันเซิร์ฟเวอร์เสร็จแล้ว (อย่าลืมตั้งค่า export OPENAI_API_KEY="your-key") ให้ลองใช้คำสั่ง curl ผ่าน Terminal เพื่อทดสอบประสิทธิภาพการสตรีมข้อความของ API ของคุณ:

Bash

# แฟล็ก -N คือสิ่งสำคัญเพื่อปิดการทำ Buffering ของ curl ทำให้เห็นตัวอักษรไหลทันที
curl -N -X POST http://localhost:8080/api/chat/stream \
     -H "Content-Type: application/json" \
     -d '{"message": "ขอ 3 เหตุผลที่ควรเขียน Go ยุค AI-First แบบกระชับ"}'
  • การบ้านเพิ่มความโปร: ตอนนี้บอทของเรายังเป็นแบบ "ถามคำตอบคำ" (Stateless) ลองนำความรู้เรื่อง Go Slices มาประยุกต์ทำระบบ Memory จำประวัติการคุย (Chat History) ก่อนส่งไปหา OpenAI เพื่อให้แอปพลิเคชันของเราคุยตอบโต้ต่อเนื่องได้สนุกขึ้นดูครับ!

FAQ (คำถามที่พบบ่อยประจำตอน)

Q: ทำไมต้องใส่ Header Cache-Control: no-cache และ Transfer-Encoding: chunked ก่อนรัน c.Stream()?

A: หากแอปพลิเคชัน Go รันอยู่เบื้องหลัง Reverse Proxy เช่น Nginx, Apache หรือบริการ Cloudflare ตัว Proxy เหล่านี้มักจะพยายาม "กัก" ข้อมูล (Buffer) ไว้ให้ครบก้อนก่อนส่งให้ผู้ใช้เพื่อประหยัดสตรีมเน็ต การใส่ Header เหล่านี้เป็นการสั่ง Proxy ว่า "ห้ามกักข้อมูล ให้ปล่อยไหลผ่านทันทีทีละชิ้น" ส่งผลให้ข้อความบนหน้าจอลื่นไหลไม่กระตุกครับ

Q: ฟังก์ชัน c.Stream() ของ Gin ทำงานอย่างไรเบื้องหลัง และมันปลอดภัยกับ Memory ไหม?

A: เบื้องหลังของ c.Stream() คือการทำลูปเรียก Anonymous function ที่เราส่งเข้าไปเรื่อยๆ ตราบใดที่ฟังก์ชันนั้นยังคืนค่าเป็น true และ HTTP Connection ยังไม่ถูกตัด ข้อดีคือมันใช้หน่วยความจำต่ำมาก (Low Memory Footprint) เพราะข้อมูลแต่ละ Chunk ถูกแปลงเป็นไบต์แล้วพ่นลงเน็ตเวิร์กทันที ไม่ได้ถูกเก็บสะสมไว้ในแรมของ Server ครับ

Q: ถ้า Client ปิดหน้าจอไปดื้อๆ ในขณะที่ c.Stream() กำลังทำงานอยู่ จะเกิดอะไรขึ้น?

A: ตัวแปร ctx ที่เราดึงมาจาก c.Request.Context() จะได้รับสัญญาณ Cancellation ทันที และเมื่อโค้ดวนลูปไปเจอ stream.Recv() ในรอบถัดไป มันจะเกิด Error จาก Context ส่งผลให้ฟังก์ชันคืนค่า false และจบการรัน Goroutine ลงอย่างปลอดภัย (Graceful Termination) ไม่เกิดปัญหา Goroutine ค้างในระบบแน่นอนครับ


บทสรุป

การนำ Gin Framework มารวมร่างกับ Go Channels และระบบ Stream ของ OpenAI ช่วยให้เราสร้าง API สตรีมมิ่งที่เบา หนาแน่น และเสถียรมากในฝั่ง Backend โดยไม่ต้องใช้ทรัพยากรเครื่องมหาศาล นี่คือเสน่ห์ที่แท้จริงของการทำสถาปัตยกรรมด้วยภาษา Go ครับ

ในตอนต่อไป (EP.151): แม้ว่า AI ของเราจะฉลาดและตอบไวแค่ไหน แต่มันก็ไม่รู้ข้อมูลภายในองค์กร ข้อมูลส่วนตัว หรืออัปเดตใหม่ๆ ของเราอยู่ดี... ตอนหน้าเราจะก้าวเข้าสู่เฟสถัดไปที่ล้ำขึ้นไปอีกกับเรื่อง "What is RAG?: ทำไม AI ต้องมีฐานข้อมูลส่วนตัว" เตรียมตัวเปิดโลก Retrieval-Augmented Generation กันได้เลยครับ!

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

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

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

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

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

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