02/11/2026 15:35น.

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 แล้วส่งต่อยังไงดี? ลองคิดสถาปัตยกรรมในหัวกันดูครับ!
สรุปและตอนต่อไป (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 ฉบับย่อยง่าย)
FAQ: คำถามที่พบบ่อยเกี่ยวกับบทความนี้
รวมคำถามและคำตอบที่ช่วยให้คุณเข้าใจเนื้อหาในบทความนี้ได้ดียิ่งขึ้น