21/07/2026 01:53น.

Golang The Series EP.162: Goroutines for Multi-LLM เรียก AI หลายเจ้าพร้อมกันเพื่อเปรียบเทียบผลลัพธ์แบบ Parallel
#Goroutines
#Multi-LLM
#ภาษา Go
#ทำงานแบบขนาน
#เปรียบเทียบ AI
#Go
#Golang
สวัสดีครับชาว Gophers! กลับมาเจอกันใน EP.162 ช่วงนี้ใครทำ Backend สำหรับระบบ AI น่าจะเจอปัญหาคล้ายๆ กัน คือเราไม่อยากผูกติดระบบไว้กับ LLM แค่เจ้าเดียว บาง Use Case เราอยาก "ถามพร้อมกันทุกเจ้า" ไม่ว่าจะเป็น OpenAI, Gemini, Claude หรือกระทั่ง Local อย่าง Ollama เพื่อเอาคำตอบมาทำระบบโหวตหาข้อเท็จจริง หรือให้ยูสเซอร์ดูเทียบกันแบบ Side-by-Side
ปัญหาคือ ถ้าเราเขียนโค้ดเรียก API ทีละเจ้าแบบปกติ (Sequential) กว่าจะรอครบทุกคน ยูสเซอร์คงรอกันเงือกหลับ (สมมติเจ้าละ 3 วินาที 3 เจ้าก็ปาไปเกือบ 10 วินาที) วันนี้เราเลยจะมาใช้ของถนัดของชาว Go อย่าง Goroutines และ Channels มาทำ Concurrent Querying ยิงคำถามไปทุกค่ายพร้อมกันเลย ใครตอบช้าสุดเราก็รอแค่นั้นแหละครับ!
สถาปัตยกรรม Multi-LLM แบบยิงขนาน (Concurrent Querying)
ไอเดียคือเราจะแตก Goroutines ออกไปเป็น Worker เพื่อยิง API ของ AI แต่ละเจ้าแยกอิสระจากกัน แล้วใช้ sync.WaitGroup คอยคุมให้ทุกตัวทำงานเสร็จครบก่อน จากนั้นใช้ Go Channels ส่งผลลัพธ์กลับมารวมกันตรงกลาง เพื่อป้องกันปัญหา Data Race ครับ
Plaintext
┌──> [Goroutine 1] ──> ยิง OpenAI API (1.5s) ──┐
│ │
[User คำถามเดียว] ├──> [Goroutine 2] ──> ยิง Gemini API (2.5s) ──┼──> [Go Channel คัดกรอง] ──> ผลลัพธ์รวม (รอรวมแค่ 3.5s)
│ │
└──> [Goroutine 3] ──> ยิง Ollama Local (3.5s) ┘
โครงสร้างข้อมูลและ Worker (Types & Function)
เพื่อให้โค้ดคลีน เราจะแบ่ง Data Structure สำหรับรับผลลัพธ์ และสร้างฟังก์ชัน Worker ย่อยสำหรับคิวรีแยกค่ายกันครับ
Go
package main
import (
"context"
"fmt"
"strings"
"sync"
"time"
)
// LLMResponse โครงสร้างสำหรับเก็บผลลัพธ์และสถิติจาก AI แต่ละค่าย
type LLMResponse struct {
Provider string
Answer string
Duration time.Duration
Error error
}
ฟังก์ชันตัวนี้จะทำหน้าที่เป็น Worker วิ่งไปเรียก API ครับ (ในตัวอย่างเราจะจำลอง Latency ด้วย time.After)
Go
// callLLM ทำหน้าที่เป็น Worker แยกยิง API ของแต่ละ Provider
func callLLM(ctx context.Context, provider string, delay time.Duration, question string, ch chan<- LLMResponse, wg *sync.WaitGroup) {
defer wg.Done()
startTime := time.Now()
// จำลอง Network Latency ในการประมวลผล
select {
case <-time.After(delay):
// จำลองการตอบกลับจากโมเดล
answer := fmt.Sprintf("[%s]: วิเคราะห์คำถาม '%s' แล้วพบว่า...", provider, question)
ch <- LLMResponse{
Provider: provider,
Answer: answer,
Duration: time.Since(startTime),
Error: nil,
}
case <-ctx.Done():
// เคสที่ Context ถูก Timeout หรือ Cancel ไปก่อน
ch <- LLMResponse{
Provider: provider,
Error: ctx.Err(),
}
}
}
ส่วนควบคุมระบบหลัก (Main Orchestrator)
ใน main.go เราจะนำ Worker มารันขนานกัน 3 ตัว และกำหนด Timeout รวมไว้ที่ 4 วินาทีเพื่อป้องกันไม่ให้ระบบรอนานเกินไปจนพัง
Go
func main() {
question := "แนวโน้มการทำ SEO ในปีนี้ควรเน้นเรื่องอะไร?"
// ตั้ง Timeout ภาพรวมไว้ที่ 4 วินาที
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
var wg sync.WaitGroup
// สร้าง Buffered Channel ขนาดเท่าจำนวนค่าย
responseChan := make(chan LLMResponse, 3)
// 1. กระจายงานยิง AI ขนานกัน
wg.Add(1)
go callLLM(ctx, "OpenAI (GPT-4o)", 1500*time.Millisecond, question, responseChan, &wg)
wg.Add(1)
go callLLM(ctx, "Google Gemini", 2500*time.Millisecond, question, responseChan, &wg)
wg.Add(1)
go callLLM(ctx, "Ollama (Llama 3)", 3500*time.Millisecond, question, responseChan, &wg)
// 2. รัน Background Worker เพื่อรอปิด Channel เมื่อทุกค่ายทำงานเสร็จ
go func() {
wg.Wait()
close(responseChan)
}()
fmt.Printf("🚀 กำลังส่งคำถาม: \"%s\" ไปยัง AI ทุกค่ายพร้อมกัน...\n\n", question)
// 3. วนลูปดึงผลลัพธ์ที่ทยอยส่งกลับเข้ามาใน Channel
for resp := range responseChan {
if resp.Error != nil {
fmt.Printf("❌ [%s] ผิดพลาดหรือ Timeout: %v\n", resp.Provider, resp.Error)
fmt.Println(strings.Repeat("-", 50))
continue
}
fmt.Printf("🤖 %s (ใช้เวลา: %v)\n", resp.Provider, resp.Duration)
fmt.Printf("📝 ตอบ: %s\n", resp.Answer)
fmt.Println(strings.Repeat("-", 50))
}
}
เทคนิคเสริม: เอาคำตอบแรกที่ไวที่สุด (First Past the Post)
สำหรับบางโปรเจกต์ที่เราไม่ได้อยากรอเอาผลลัพธ์มาเปรียบเทียบกัน แต่อยากได้ "ความไวสูงสุด" คือค่ายไหนตอบก่อนเอาคนนั้นเลย เราสามารถประยุกต์ใช้โค้ดชุดนี้ได้ง่ายมากครับ แค่เปลี่ยนเงื่อนไขในลูป: เมื่อเราดึงข้อมูลแรกจาก responseChan ได้สำเร็จปุ๊บ ให้สั่ง cancel() ทันที สัญญาณจะวิ่งไปบอก Goroutines ตัวอื่นที่กำลังรอ API ให้ยกเลิก Request ทิ้ง ช่วยประหยัดค่า API Token และลดโหลดฝั่งเซิร์ฟเวอร์ได้เยอะเลยครับ
🎯 ท้าให้ลอง (Daily Mission)
ลองเอาโค้ดชุดนี้ไปรันดูครับ แล้วปรับค่า delay ของบางค่ายให้พุ่งไปเป็น 5000*time.Millisecond ดู จะสังเกตเห็นว่าค่ายนั้นจะถูกตัดจบด้วย Timeout Error แทน
การบ้านชวนคิด: หาก Requirement เปลี่ยนไปเป็น "ต้องรอคำตอบจากทุกเจ้า แล้วส่งทั้ง 3 คำตอบไปให้ Judge LLM (AI ตัดสิน) ช่วยสรุปข้อที่ดีที่สุดอีกที" คุณคิดว่าจะออกแบบ Slice มารับค่าจาก Channel แล้วส่งต่อยังไงดี? ลองคิดสถาปัตยกรรมในหัวกันดูครับ!
FAQ: คำถามที่พบบ่อย
ทำไมต้องสร้าง Buffer Channel make(chan LLMResponse, 3) แบบกำหนดขนาดไว้ด้วย?
เพื่อป้องกันปัญหา Goroutine Leak ครับ สมมติว่ามี Worker ทำงานเสร็จและต้องการส่งข้อมูลเข้า Channel หากเราใช้ Unbuffered Channel (ไม่ได้กำหนดขนาด) Worker จะเกิดอาการ Blocked และหยุดรอจนกว่าโค้ดหลักจะมารับค่า การใส่ Buffer ขนาดเท่าจำนวน Worker ไว้ ทำให้พวกเขาสามารถโยนผลลัพธ์ลงกล่องแล้วปิดจ๊อบตัวเองได้ทันทีครับ
จำเป็นต้องใช้ sync.WaitGroup ไหม ในเมื่อเราก็วนลูปอ่านจาก Channel อยู่แล้ว?
จำเป็นในกรณีนี้ครับ เพราะเราตั้ง close(responseChan) ไว้ หากเราไม่ใช้ WaitGroup รอก่อนปิด Channel อาจจะเกิดเหตุการณ์ที่ Worker ยังทำงานไม่เสร็จ แต่พยายามจะส่งข้อมูลเข้า Channel ที่ถูกทะลวงปิดไปแล้ว ซึ่งจะทำให้โปรแกรมเกิดการ Panic และล่มทันทีครับ
หากมี AI ให้เรียกใช้งานเป็น 100 ค่าย การยิง Goroutines ออกไป 100 ตัวพร้อมกันจะกินทรัพยากรเครื่องไหม?
Goroutines มีน้ำหนักเบามาก (Lightweight) รัน 100 ตัวไม่ใช่ปัญหาของ Go ครับ แต่จุดที่ต้องระวังคือฝั่ง Network หรือ API Rate Limit มากกว่า หากสเกลใหญ่ระดับนั้น แนะนำให้ใช้ Worker Pool Pattern หรือทำ Semaphore ควบคุมไม่ให้ยิง Request ขนานกันเยอะเกินไปครับ
สรุปและตอนต่อไป (EP.163)
Goroutines และ Channels คือพระเอกตัวจริงที่ทำให้ภาษา Go จัดการงานจำพวก I/O-Bound อย่างการยิง API คุยกับ AI หลายเจ้าพร้อมกันได้อย่างมีประสิทธิภาพและปลอดภัยครับ
ในตอนต่อไป (EP.163): แม้ว่าการยิงขนานแบบนี้จะเร็วขึ้น แต่มันก็ยังต้องจ่ายค่า API ซ้ำซ้อนสำหรับ "คำถามเดิมๆ" ที่ยูสเซอร์มักจะถามซ้ำกัน ตอนหน้าเราจะมาลดต้นทุนด้วยการทำ Caching AI Responses ด้วย Redis ซึ่งจะช่วยให้ระบบตอบคำถามยอดฮิตได้ในหลักมิลลิวินาทีโดยไม่ต้องกวน AI เลย รอติดตามกันนะครับ Gophers!
ฝากกดติดตามพวกเราได้ที่ Superdev Academy ในทุกช่องทางนะครับ!
🔵 Facebook: Superdev Academy Thailand (อัปเดตข่าวสารและบทความใหม่)
🎬 YouTube: Superdev Academy Channel (ติวเข้มแบบวิดีโอ)
📸 Instagram: @superdevacademy (เกร็ดความรู้สั้นๆ และเบื้องหลังการทำงาน)
🎬 TikTok: @superdevacademy (Tips & Tricks ฉบับย่อยง่าย)
🌐 Website: superdevacademy.com (คลังบทความและคอร์สเรียนฉบับเต็ม)