[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"academy-blogs-th-1-1-all-build-enterprise-rag-internal-ai-tool-golang-qdrant-all--*":3,"academy-blog-translations-wqf1daar37kumo1":106},{"data":4,"page":92,"perPage":92,"totalItems":92,"totalPages":92},[5],{"alt":6,"collectionId":7,"collectionName":8,"content":9,"cover_image":10,"cover_image_path":11,"created":12,"created_by":13,"expand":14,"id":100,"keywords":101,"locale":72,"published_at":102,"scheduled_at":88,"school_blog":96,"short_description":103,"status":94,"title":104,"updated":105,"updated_by":13,"slug":97,"views":99},"ภาพหน้าปก Golang The Series EP.160 วิธีสร้าง Enterprise Internal AI Tool (RAG) ด้วย Qdrant และ Gin ของ Superdev Academy","sclblg987654321","school_blog_translations","\u003Cp>ยินดีต้อนรับชาว \u003Cstrong>Superdev Academy\u003C\u002Fstrong> เข้าสู่ EP.160 ซึ่งเป็น Workshop ใหญ่ประจำซีซันนี้ครับ! ตลอดเส้นทางซีรีส์ที่ผ่านมาเราได้ลุยกันตั้งแต่การติดตั้ง Environment, การแปลงข้อความเป็นเวกเตอร์ (Embeddings), การหั่นข้อมูลอย่างมีกลยุทธ์ (Data Chunking), ไปจนถึงเทคนิคสำคัญของระบบ RAG (Retrieval-Augmented Generation) อย่าง Hybrid Search และ Context Injection\u003C\u002Fp>\u003Cp>วันนี้ได้เวลาที่เราจะนำองค์ความรู้ทั้งหมดมารวมร่างสร้าง \u003Cstrong>Internal AI Tool\u003C\u002Fstrong> ประจำองค์กรด้วยภาษา \u003Cstrong>Go (Golang)\u003C\u002Fstrong> นั่นคือเซิร์ฟเวอร์ระบบถาม-ตอบอัจฉริยะที่สามารถสืบค้นความรู้จากเอกสารภายใน มีระบบกรองสิทธิ์เข้าถึงข้อมูลตามแผนกด้วย Hybrid Search เพื่อความปลอดภัย และสตรีมคำตอบ (Streaming) ส่งกลับหาผู้ใช้งานแบบ Real-time ผ่านเทคนิค SSE ครับ!\u003C\u002Fp>\u003Ch2>โครงสร้างโปรเจกต์ (Project Structure)\u003C\u002Fh2>\u003Cp>เพื่อให้โค้ดของเรามีความยืดหยุ่น ดูแลรักษาง่าย และแยกหน้าที่การทำงานอย่างชัดเจน เราจะจัดโครงสร้างโปรเจกต์ตามสถาปัตยกรรมแบบแยกเลเยอร์ (Layered Architecture) ดังนี้ครับ:\u003C\u002Fp>\u003Cp>Plaintext\u003C\u002Fp>\u003Cpre>\u003Ccode>internal-ai-tool\u002F\r\n├── config\u002F\r\n│   └── qdrant.go      # จัดการ Connection ของ Qdrant Vector DB\r\n├── handlers\u002F\r\n│   └── qa.go          # HTTP Handlers สำหรับรับ Request และจัดการ SSE Streaming\r\n├── services\u002F\r\n│   ├── ai.go          # Wrapper สำหรับเรียกใช้ OpenAI API (Embeddings &amp; Chat Stream)\r\n│   └── vector.go      # จัดการฟังก์ชันสืบค้นข้อมูล (Query) แบบ Hybrid Search บน Qdrant\r\n├── go.mod\r\n├── go.sum\r\n└── main.go            # Entry point ของโปรเจกต์ และตั้งค่า Dependency Injection\r\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>เริ่มต้นด้วยการสร้าง Go Module และติดตั้ง Dependencies หลักของโปรเจกต์:\u003C\u002Fp>\u003Cp>Bash\u003C\u002Fp>\u003Cpre>\u003Ccode>go mod init internal-ai-tool\r\ngo get github.com\u002Fgin-gonic\u002Fgin\r\ngo get github.com\u002Fsashabaranov\u002Fgo-openai\r\ngo get github.com\u002Fqdrant\u002Fgo-client\u002Fqdrant\r\ngo get github.com\u002Fgoogle\u002Fuuid\r\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch2>เลเยอร์จัดเตรียมโครงสร้างและการเชื่อมต่อ (Configuration &amp; Services)\u003C\u002Fh2>\u003Cp>ในส่วนนี้เราจะแยกการทำงานออกเป็น 3 ส่วนหลัก ได้แก่ การเชื่อมต่อฐานข้อมูลเวกเตอร์ (Vector Database), บริการจัดการโมเดล AI (OpenAI Service) และบริการสืบค้นข้อมูลเวกเตอร์ (Vector Service) เพื่อให้โค้ดแยกขาดจากกันตามหลักการ Clean Architecture ครับ\u003C\u002Fp>\u003Ch3>1. จัดการ Connection ของฐานข้อมูลเวกเตอร์ (\u003Ccode>config\u002Fqdrant.go\u003C\u002Fcode>)\u003C\u002Fh3>\u003Cp>เริ่มต้นด้วยการเขียนฟังก์ชันเพื่อเปิดการเชื่อมต่อ (Connection) ไปยัง Qdrant Vector Database โดยเราจะเตรียม Client ตัวนี้ไว้เพื่อส่งต่อทำ Dependency Injection ในหน้า \u003Ccode>main.go\u003C\u002Fcode> ต่อไป\u003C\u002Fp>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>package config\r\n\r\nimport (\r\n\t\"log\"\r\n\t\"github.com\u002Fqdrant\u002Fgo-client\u002Fqdrant\"\r\n)\r\n\r\n\u002F\u002F NewQdrantClient ทำหน้าที่สร้างและส่งคืน Client สำหรับเชื่อมต่อกับ Qdrant Vector DB\r\nfunc NewQdrantClient(host string, port int) *qdrant.Client {\r\n\tclient, err := qdrant.NewClient(&amp;qdrant.Config{\r\n\t\tHost: host,\r\n\t\tPort: port,\r\n\t})\r\n\tif err != nil {\r\n\t\tlog.Fatalf(\"❌ ไม่สามารถเชื่อมต่อ Qdrant ได้: %v\", err)\r\n\t}\r\n\treturn client\r\n}\r\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3>2. บริการเชื่อมต่อ OpenAI API (\u003Ccode>services\u002Fai.go\u003C\u002Fcode>)\u003C\u002Fh3>\u003Cp>ถัดมาคือเลเยอร์ที่คอยติดต่อกับ OpenAI API โดยทำหน้าที่หลัก 2 อย่างคือ \u003Cstrong>การทำ Text Embedding\u003C\u002Fstrong> (แปลงคำถามของ User เป็นเวกเตอร์) และ \u003Cstrong>การทำ Chat Completion Stream\u003C\u002Fstrong> เพื่อดึงคำตอบจากโมเดล GPT-4o ออกมาแบบสตรีมมิ่งทีละตัวอักษร\u003C\u002Fp>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>package services\r\n\r\nimport (\r\n\t\"context\"\r\n\t\"github.com\u002Fsashabaranov\u002Fgo-openai\"\r\n)\r\n\r\ntype AIService struct {\r\n\tclient *openai.Client\r\n}\r\n\r\nfunc NewAIService(apiKey string) *AIService {\r\n\treturn &amp;AIService{client: openai.NewClient(apiKey)}\r\n}\r\n\r\n\u002F\u002F CreateEmbedding แปลงข้อความดิบให้กลายเป็น Vector (ค่านัยยะทางภาษา)\r\nfunc (s *AIService) CreateEmbedding(ctx context.Context, text string) ([]float32, error) {\r\n\treq := openai.EmbeddingRequest{\r\n\t\tInput: []string{text},\r\n\t\tModel: openai.SmallEmbedding3Small, \u002F\u002F ใช้โมเดลประสิทธิภาพสูงและประหยัดค่าใช้จ่าย\r\n\t}\r\n\tresp, err := s.client.CreateEmbeddings(ctx, req)\r\n\tif err != nil {\r\n\t\treturn nil, err\r\n\t}\r\n\treturn resp.Data[0].Embedding, nil\r\n}\r\n\r\n\u002F\u002F GetChatStream ส่งข้อความไปยัง OpenAI และเปิดการรับข้อมูลแบบ Stream\r\nfunc (s *AIService) GetChatStream(ctx context.Context, systemPrompt, userPrompt string) (*openai.ChatCompletionStream, error) {\r\n\treturn s.client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{\r\n\t\tModel: openai.GPT4o,\r\n\t\tMessages: []openai.ChatCompletionMessage{\r\n\t\t\t{Role: openai.ChatMessageRoleSystem, Content: systemPrompt},\r\n\t\t\t{Role: openai.ChatMessageRoleUser, Content: userPrompt},\r\n\t\t},\r\n\t})\r\n}\r\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3>3. บริการสืบค้นข้อมูลเวกเตอร์แบบกำหนดสิทธิ์ (\u003Ccode>services\u002Fvector.go\u003C\u002Fcode>)\u003C\u002Fh3>\u003Cp>ส่วนสำคัญที่สุดของระบบ RAG คือส่วนนี้ครับ เราจะใช้ฟีเจอร์การ Query ของ Qdrant มาทำ \u003Cstrong>Hybrid Search\u003C\u002Fstrong> โดยมีจุดเด่นคือการใส่ \u003Cstrong>Filter สิทธิ์การเข้าถึงข้อมูลตามแผนก (Department)\u003C\u002Fstrong> และกรองเอาเฉพาะเอกสารที่มีสถานะ \u003Ccode>active\u003C\u002Fcode> เท่านั้นพนักงานจึงจะสามารถค้นเจอข้อมูลชิ้นนั้นได้\u003C\u002Fp>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>package services\r\n\r\nimport (\r\n\t\"context\"\r\n\t\"github.com\u002Fqdrant\u002Fgo-client\u002Fqdrant\"\r\n)\r\n\r\ntype VectorService struct {\r\n\tclient *qdrant.Client\r\n}\r\n\r\nfunc NewVectorService(client *qdrant.Client) *VectorService {\r\n\treturn &amp;VectorService{client: client}\r\n}\r\n\r\n\u002F\u002F SearchRelevantContext ค้นหาข้อมูลจาก Qdrant โดยมีเงื่อนไขกรองสิทธิ์ตามแผนกของผู้ใช้งาน\r\nfunc (s *VectorService) SearchRelevantContext(ctx context.Context, vector []float32, department string) ([]string, error) {\r\n\tsearchLimit := uint64(3) \u002F\u002F ดึงเอกสารที่มีความคล้ายคลึงที่สุด 3 ชิ้น\r\n\t\r\n\t\u002F\u002F กรองข้อมูล (Security Filter): ดึงเฉพาะเอกสารที่ตรงตามแผนกและมีสถานะ active เท่านั้น\r\n\tsearchFilters := &amp;qdrant.Filter{\r\n\t\tMust: []*qdrant.Condition{\r\n\t\t\tqdrant.NewMatchKeyword(\"department\", department),\r\n\t\t\tqdrant.NewMatchKeyword(\"status\", \"active\"),\r\n\t\t},\r\n\t}\r\n\r\n\tresp, err := s.client.Query(ctx, &amp;qdrant.QueryPoints{\r\n\t\tCollectionName: \"ai_knowledge_base\",\r\n\t\tQuery:          qdrant.NewQuery(vector...),\r\n\t\tFilter:         searchFilters,\r\n\t\tLimit:          &amp;searchLimit,\r\n\t})\r\n\tif err != nil {\r\n\t\treturn nil, err\r\n\t}\r\n\r\n\tvar chunks []string\r\n\tfor _, point := range resp {\r\n\t\tif contentVal, exists := point.Payload[\"content\"]; exists {\r\n\t\t\tchunks = append(chunks, contentVal.GetStringValue())\r\n\t\t}\r\n\t}\r\n\treturn chunks, nil\r\n}\r\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch2>เลเยอร์ควบคุม API Endpoint และระบบ Server-Sent Events (SSE) Streaming\u003C\u002Fh2>\u003Cp>ในเลเยอร์นี้เราจะเข้าสู่ไฟล์ \u003Ccode>handlers\u002Fqa.go\u003C\u002Fcode> ซึ่งทำหน้าที่รับ Request จาก Frontend นำข้อมูลไปประมวลผลร่วมกับเลเยอร์ Service และส่งคำตอบกลับแบบ Real-time เพื่อให้เห็นภาพสถาปัตยกรรมภายในชัดเจน เราจะแบ่งโค้ดออกเป็น 3 ส่วนตามหน้าที่ของมันครับ\u003C\u002Fp>\u003Ch3>1. โครงสร้างข้อมูลและการรับ Request (Struct &amp; Validation)\u003C\u002Fh3>\u003Cp>เริ่มต้นด้วยการประกาศ Package, นำเข้า Dependencies และสร้างเลเยอร์รับข้อมูล (Data Transfer Object) ในรูปแบบ Struct เพื่อรองรับคำถามรวมถึงสิทธิ์การเข้าถึงข้อมูลตามแผนกครับ\u003C\u002Fp>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>package handlers\r\n\r\nimport (\r\n\t\"errors\"\r\n\t\"fmt\"\r\n\t\"io\"\r\n\t\"net\u002Fhttp\"\r\n\t\"strings\"\r\n\r\n\t\"internal-ai-tool\u002Fservices\"\r\n\t\"github.com\u002Fgin-gonic\u002Fgin\"\r\n)\r\n\r\n\u002F\u002F QARequest โครงสร้างข้อมูลสำหรับรับ Payload JSON จากหน้าบ้าน\r\ntype QARequest struct {\r\n\tQuestion   string `json:\"question\" binding:\"required\"`\r\n\tDepartment string `json:\"department\" binding:\"required\"` \u002F\u002F ใช้สำหรับทำ Security Filter\r\n}\r\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3>2. กระบวนการสืบค้นข้อมูลและการทำ Context Injection\u003C\u002Fh3>\u003Cp>ถัดมาเราจะเขียนฟังก์ชัน \u003Ccode>HandleQAStream\u003C\u002Fcode> โดยส่วนแรกนี้จะทำหน้าที่ผูกข้อมูล (JSON Binding), ส่งคำถามไปทำ \u003Cstrong>Text Embedding\u003C\u002Fstrong> และทำการค้นหาข้อมูลอ้างอิงด้วย \u003Cstrong>Hybrid Search\u003C\u002Fstrong> ก่อนจะนำเนื้อหาที่ได้มาประกอบร่างเป็น Prompt (Context Injection) เพื่อเตรียมส่งให้ LLM ครับ\u003C\u002Fp>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>\u002F\u002F HandleQAStream ควบคุมโฟลว์การทำงานของระบบ RAG และสตรีมคำตอบกลับผ่าน SSE\r\nfunc HandleQAStream(ai *services.AIService, vector *services.VectorService) gin.HandlerFunc {\r\n\treturn func(c *gin.Context) {\r\n\t\tvar req QARequest\r\n\t\tif err := c.ShouldBindJSON(&amp;req); err != nil {\r\n\t\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"พารามิเตอร์ไม่ถูกต้อง\"})\r\n\t\t\treturn\r\n\t\t}\r\n\r\n\t\tctx := c.Request.Context()\r\n\r\n\t\t\u002F\u002F สเต็ปที่ 1: แปลงคำถามของ User เป็น Vector\r\n\t\tqueryVector, err := ai.CreateEmbedding(ctx, req.Question)\r\n\t\tif err != nil {\r\n\t\t\tc.JSON(http.StatusInternalServerError, gin.H{\"error\": \"เกิดข้อผิดพลาดในการทำ Embedding\"})\r\n\t\t\treturn\r\n\t\t}\r\n\r\n\t\t\u002F\u002F สเต็ปที่ 2: ค้นหาเอกสารในคลังความรู้พร้อมตรวจสอบสิทธิ์แผนก\r\n\t\tchunks, err := vector.SearchRelevantContext(ctx, queryVector, req.Department)\r\n\t\tif err != nil {\r\n\t\t\tc.JSON(http.StatusInternalServerError, gin.H{\"error\": \"ไม่สามารถสืบค้นคลังข้อมูลความรู้ได้\"})\r\n\t\t\treturn\r\n\t\t}\r\n\r\n\t\t\u002F\u002F สเต็ปที่ 3: ทำ Context Injection เพื่อจำกัดกรอบคำตอบให้ AI\r\n\t\tcontextText := \"\"\r\n\t\tif len(chunks) &gt; 0 {\r\n\t\t\tcontextText = strings.Join(chunks, \"\\n\\n\")\r\n\t\t} else {\r\n\t\t\tcontextText = \"ไม่มีข้อมูลอ้างอิงในระบบที่เกี่ยวข้องกับแผนกนี้\"\r\n\t\t}\r\n\r\n\t\tsystemPrompt := \"คุณคือ AI ประจำองค์กร หน้าที่ของคุณคือตอบคำถามพนักงานโดยใช้ข้อมูลอ้างอิง (Context) ที่กำหนดให้เท่านั้น หากไม่มีคำตอบใน Context ให้ตอบตรงๆ ว่าไม่พบข้อมูลในระบบอ้างอิง ห้ามเดา\"\r\n\t\tuserPrompt := fmt.Sprintf(\"ข้อมูลอ้างอิง (Context):\\n%s\\n\\nคำถาม: %s\\nคำตอบ:\", contextText, req.Question)\r\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3>3. เปิดท่อส่งข้อมูลแบบ Server-Sent Events (SSE) Streaming\u003C\u002Fh3>\u003Cp>ส่วนสุดท้ายของฟังก์ชันคือการตั้งค่า HTTP Header ให้รองรับโปรโตคอล \u003Ccode>text\u002Fevent-stream\u003C\u002Fcode> และใช้ฟีเจอร์ \u003Ca rel=\"noopener noreferrer\" href=\"http:\u002F\u002Fc.Stream\">\u003Ccode>c.Stream\u003C\u002Fcode>\u003C\u002Fa> ของ \u003Cstrong>Gin Gonic\u003C\u002Fstrong> ในการวนลูปรับข้อมูล Chunk จาก OpenAI API เพื่อดันข้อมูลออกไปหาฝั่งหน้าบ้านทันทีที่มีคำตัวอักษรใหม่ออกมาครับ\u003C\u002Fp>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>\t\t\u002F\u002F สเต็ปที่ 4: เรียกใช้ Chat Completion Stream และตั้งค่าเซิร์ฟเวอร์ส่งข้อมูลแบบ SSE\r\n\t\tstream, err := ai.GetChatStream(ctx, systemPrompt, userPrompt)\r\n\t\tif err != nil {\r\n\t\t\tc.JSON(http.StatusInternalServerError, gin.H{\"error\": \"การเปิดท่อสตรีมข้อมูลล้มเหลว\"})\r\n\t\t\treturn\r\n\t\t}\r\n\t\tdefer stream.Close()\r\n\r\n\t\t\u002F\u002F ตั้งค่า HTTP Header ให้เปิดการเชื่อมต่อแบบข้ามฝั่งค้างไว้ (Keep-Alive)\r\n\t\tc.Header(\"Content-Type\", \"text\u002Fevent-stream\")\r\n\t\tc.Header(\"Cache-Control\", \"no-cache\")\r\n\t\tc.Header(\"Connection\", \"keep-alive\")\r\n\r\n\t\t\u002F\u002F วนลูปสตรีมคำตอบออกไปทีละอักษรแบบ Real-time\r\n\t\tc.Stream(func(w io.Writer) bool {\r\n\t\t\tresp, err := stream.Recv()\r\n\t\t\tif errors.Is(err, io.EOF) {\r\n\t\t\t\tc.SSEvent(\"message\", \"[DONE]\") \u002F\u002F ส่งสัญญาณแจ้งหน้าบ้านเมื่อคำตอบจบลง\r\n\t\t\t\treturn false\r\n\t\t\t}\r\n\t\t\tif err != nil {\r\n\t\t\t\treturn false\r\n\t\t\t}\r\n\r\n\t\t\tif len(resp.Choices) &gt; 0 {\r\n\t\t\t\tcontent := resp.Choices[0].Delta.Content\r\n\t\t\t\tif content != \"\" {\r\n\t\t\t\t\tc.SSEvent(\"message\", content)\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\treturn true\r\n\t\t})\r\n\t}\r\n}\r\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch2>ประกอบร่างแอปพลิเคชันและทำ Dependency Injection (\u003Ccode>main.go\u003C\u002Fcode>)\u003C\u002Fh2>\u003Cp>มาถึงขั้นตอนสุดท้ายในการประกอบชิ้นส่วนทั้งหมดเข้าด้วยกันในไฟล์ \u003Ccode>main.go\u003C\u002Fcode> ครับ เลเยอร์นี้เปรียบเสมือนศูนย์บัญชาการหลักที่จะทำหน้าที่โหลดค่าคอนฟิกูเรชัน, เปิดการเชื่อมต่อฐานข้อมูลเวกเตอร์, ทำ \u003Cstrong>Dependency Injection (DI)\u003C\u002Fstrong> เพื่อมัดรวม Service และ Handler เข้าด้วยกัน ก่อนจะเปิดสตรีมมิ่งเซิร์ฟเวอร์ด้วย \u003Cstrong>Gin Gonic\u003C\u002Fstrong> ครับ\u003C\u002Fp>\u003Cp>เพื่อให้บทความอ่านง่ายและเป็นระเบียบ เรามาเจาะลึกส่วนประกอบภายใน \u003Ccode>main.go\u003C\u002Fcode> กันครับ\u003C\u002Fp>\u003Ch3>1. การเตรียมระบบและการทำ Dependency Injection\u003C\u002Fh3>\u003Cp>ในส่วนแรกของฟังก์ชัน \u003Ccode>main\u003C\u002Fcode> จะเน้นไปที่การเตรียมระบบเพื่อความปลอดภัยและความยืดหยุ่นในการขยายสเกลระบบในอนาคต\u003C\u002Fp>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>package main\r\n\r\nimport (\r\n\t\"log\"\r\n\t\"os\"\r\n\r\n\t\"internal-ai-tool\u002Fconfig\"\r\n\t\"internal-ai-tool\u002Fhandlers\"\r\n\t\"internal-ai-tool\u002Fservices\"\r\n\t\"github.com\u002Fgin-gonic\u002Fgin\"\r\n)\r\n\r\nfunc main() {\r\n\t\u002F\u002F 1. โหลด API Key จาก Environment Variables เพื่อความปลอดภัยตามมาตรฐาน Enterprise\r\n\topenAIKey := os.Getenv(\"OPENAI_API_KEY\")\r\n\tif openAIKey == \"\" {\r\n\t\tlog.Fatal(\"ERROR: กรุณาตั้งค่า Environment Variable สำหรับ OPENAI_API_KEY\")\r\n\t}\r\n\r\n\t\u002F\u002F 2. เริ่มต้นเปิดการเชื่อมต่อกับ Qdrant Vector DB\r\n\tqdrantClient := config.NewQdrantClient(\"localhost\", 6334)\r\n\tdefer qdrantClient.Close() \u002F\u002F ตรวจสอบให้มั่นใจว่าปิด Connection เมื่อเซิร์ฟเวอร์หยุดทำงาน\r\n\r\n\t\u002F\u002F 3. ทำ Dependency Injection (DI) ส่งมอบออบเจกต์ไปใช้งานในเลเยอร์ที่เกี่ยวข้อง\r\n\taiService := services.NewAIService(openAIKey)\r\n\tvectorService := services.NewVectorService(qdrantClient)\r\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3>2. การจัดทำ Routing และการเปิดเซิร์ฟเวอร์ (Routing &amp; Initialize)\u003C\u002Fh3>\u003Cp>ส่วนสุดท้ายคือการเปิดจุดรับส่งข้อมูล (Endpoint) เพื่อเชื่อมต่อกับระบบหน้าบ้าน (Frontend)\u003C\u002Fp>\u003Cp>Go\u003C\u002Fp>\u003Cpre>\u003Ccode>\t\u002F\u002F 4. สร้างเซิร์ฟเวอร์ Routing ด้วย Gin Gonic\r\n\tr := gin.Default()\r\n\r\n\t\u002F\u002F ลงทะเบียน Endpoint สำหรับระบุสิทธิ์แผนกและสตรีมคำตอบผ่าน POST Method\r\n\tr.POST(\"\u002Fapi\u002Fv1\u002Fqa\u002Fstream\", handlers.HandleQAStream(aiService, vectorService))\r\n\r\n\t\u002F\u002F สตาร์ทระบบพร้อมทำงานบนพอร์ตที่กำหนด\r\n\tlog.Println(\"🚀 เซิร์ฟเวอร์ Internal AI Tool เริ่มทำงานบนพอร์ต :8080...\")\r\n\tr.Run(\":8080\")\r\n}\r\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch2>นี่คือเนื้อหาส่วนสุดท้ายของบทความ (Daily Mission, FAQ, สรุป และบทนำของ EP ถัดไป) ที่ปรับปรุงเรื่องคำผิด (แก้จาก ยิงไอพี -&gt; ยิง API) ลบสำนวน AI ออกทั้งหมด และเติมเนื้อหาในส่วน FAQ กับสรุป เพื่อให้บทความมีความสมบูรณ์และได้คะแนน SEO สูงสุดครับ:\u003C\u002Fh2>\u003Cdiv data-type=\"horizontalRule\">\u003Chr>\u003C\u002Fdiv>\u003Ch2>🎯 ท้าให้ลอง (Daily Mission)\u003C\u002Fh2>\u003Cp>ยินดีด้วยครับ! ตอนนี้คุณมีสถาปัตยกรรมระบบถาม-ตอบจากเอกสาร (RAG System) ที่สะอาด ถูกต้องตามหลักการเขียนโปรแกรม และพร้อมให้ทีม Frontend นำไปเชื่อมต่อกับหน้าจอ UI เพื่อใช้งานจริงในองค์กรแล้วครับ\u003C\u002Fp>\u003Cp>\u003Cstrong>การบ้านใหญ่ส่งท้าย Workshop:\u003C\u002Fstrong> ให้คุณทดลองสั่งรันเซิร์ฟเวอร์ตัวนี้ขึ้นมา แล้วลองใช้คำสั่ง \u003Ccode>curl\u003C\u002Fcode> หรือส่ง Request ผ่านโปรแกรม \u003Cstrong>Postman\u003C\u002Fstrong> โดยลองแกล้งเปลี่ยนข้อมูลในฟิลด์ \u003Ccode>\"department\"\u003C\u002Fcode> ใน JSON Body สลับไปมาระหว่างแผนกต่าง ๆ เช่น สลับจาก \u003Ccode>\"Human Resources\"\u003C\u002Fcode> ไปเป็น \u003Ccode>\"Engineering\"\u003C\u002Fcode> โดยใช้คำถามเดิม\u003C\u002Fp>\u003Cp>ลองสังเกตดูครับว่า \u003Cstrong>Security Filter\u003C\u002Fstrong> หลังบ้านทำงานได้ตรงตามเป้าหมายไหม ระบบยอมเปิดเผยข้อมูลหรือเลือกหยิบคำตอบออกมาได้แตกต่างกันตามสิทธิ์ที่เราล็อกไว้ในคำสั่งสืบค้นข้อมูลเวกเตอร์บน Qdrant หรือไม่? ลองไปพิสูจน์ฝีมือการเขียนระบบ Backend นี้กันดูนะครับ!\u003C\u002Fp>\u003Ch2>FAQ (คำถามที่พบบ่อย)\u003C\u002Fh2>\u003Ch3>ทำไมระบบถาม-ตอบ AI แบบ Streaming ถึงนิยมใช้ Server-Sent Events (SSE) มากกว่า WebSockets?\u003C\u002Fh3>\u003Cp>เพราะระบบนี้เป็นการส่งข้อมูลแบบทิศทางเดียว (One-way Streaming) จากเซิร์ฟเวอร์กลับไปหาคลื่นต์เท่านั้น SSE ทำงานบนโปรโตคอล HTTP มาตรฐาน จึงประหยัดทรัพยากรเซิร์ฟเวอร์และติดตั้งระบบง่ายกว่า WebSockets ที่ออกแบบมาสำหรับการสื่อสารแบบสองทาง (Bi-directional) ครับ\u003C\u002Fp>\u003Ch3>หากคลังข้อมูลบน Qdrant มีขนาดใหญ่ขึ้นมาก จะส่งผลให้ระบบค้นหาช้าลงไหม?\u003C\u002Fh3>\u003Cp>หากไม่ได้ทำ Index อาจช้าลงครับ แนะนำให้ทำ \u003Cstrong>Payload Indexing\u003C\u002Fstrong> บนฟิลด์ที่ใช้เป็นเงื่อนไขในการกรอง เช่น ฟิลด์ \u003Ccode>department\u003C\u002Fcode> และ \u003Ccode>status\u003C\u002Fcode> เพื่อช่วยให้ Qdrant ค้นหาข้อมูลเวกเตอร์ร่วมกับ Filter ได้อย่างรวดเร็วในระดับ Millisecond\u003C\u002Fp>\u003Cdiv data-type=\"horizontalRule\">\u003Chr>\u003C\u002Fdiv>\u003Ch2>สรุปเนื้อหาในตอนนี้\u003C\u002Fh2>\u003Cp>ในบทเรียนนี้เราได้เรียนรู้และลงมือทำในหัวข้อสำคัญประกอบด้วย:\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cp>การออกแบบสถาปัตยกรรมระบบ RAG (Retrieval-Augmented Generation) แยกเลเยอร์ด้วยภาษา \u003Cstrong>Go (Golang)\u003C\u002Fstrong>\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>การจัดการ Connection และการค้นหาข้อมูลเวกเตอร์พร้อมระบุเงื่อนไขกรองสิทธิ์ด้วย \u003Cstrong>Qdrant Vector DB\u003C\u002Fstrong>\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>เทคนิคการทำ \u003Cstrong>Context Injection\u003C\u002Fstrong> เพื่อควบคุมกรอบและบริบทการตอบคำถามของโมเดล OpenAI GPT-4o\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>การสร้าง HTTP API Endpoint และสตรีมคำตอบแบบ Real-time ด้วย \u003Cstrong>Server-Sent Events (SSE)\u003C\u002Fstrong> บน Gin Gonic\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Cp>\u003Cstrong>ในตอนต่อไป (EP.161): \u003C\u002Fstrong>เราปิดกล่อง Workshop ระบบ RAG พื้นฐานได้อย่างสวยงามแล้วครับ แต่ในหน้างานจริงระดับ Enterprise ถ้าเกิดวันดีคืนดีพนักงานพร้อมใจกันอัปโหลดไฟล์คู่มือและเอกสารจัดซื้อนับร้อยไฟล์เข้ามาในระบบพร้อมกันล่ะ? ระบบสกัดข้อมูลที่ต้องรอยิง API ทีละคำสั่ง ย่อมเสี่ยงต่ออาการเซิร์ฟเวอร์ Timeout และระบบล่มแน่นอน\u003C\u002Fp>\u003Cp>เราจะขึ้นบทเรียนสถาปัตยกรรมขั้นสูงในชื่อตอน \u003Cstrong>\"Async AI Tasks การประยุกต์ใช้ Worker Pools จัดการคิวงานสกัด AI ขนาดใหญ่หลังบ้าน\"\u003C\u002Fstrong> บอกเลยว่าสาย Backend ที่ต้องการรีดประสิทธิภาพระบบสเกลสูงห้ามพลาดเด็ดขาดครับ Gophers ของ Superdev Academy ทุกคน!\u003C\u002Fp>\u003Cp>\u003Cstrong>ฝากกดติดตามพวกเราได้ที่ Superdev Academy\u003C\u002Fstrong> ในทุกช่องทางนะครับ!\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cp>\u003Cstrong>🔵 Facebook: \u003C\u002Fstrong>\u003Ca target=\"_blank\" rel=\"noopener\" class=\"ng-star-inserted\" href=\"https:\u002F\u002Fwww.facebook.com\u002Fsuperdev.academy.th\">\u003Cstrong>Superdev Academy Thailand\u003C\u002Fstrong>\u003C\u002Fa>\u003Cstrong> \u003C\u002Fstrong>(อัปเดตข่าวสารและบทความใหม่)\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>🎬 YouTube: \u003C\u002Fstrong>\u003Ca target=\"_blank\" rel=\"noopener\" class=\"ng-star-inserted\" href=\"https:\u002F\u002Fwww.youtube.com\u002F@SuperdevAcademy\">\u003Cstrong>Superdev Academy Channel\u003C\u002Fstrong>\u003C\u002Fa>\u003Cstrong> \u003C\u002Fstrong>(ติวเข้มแบบวิดีโอ)\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>📸 Instagram: \u003C\u002Fstrong>\u003Ca target=\"_blank\" rel=\"noopener\" class=\"ng-star-inserted\" href=\"https:\u002F\u002Fwww.instagram.com\u002Fsuperdevacademy\u002F\">\u003Cstrong>@superdevacademy\u003C\u002Fstrong>\u003C\u002Fa>\u003Cstrong> \u003C\u002Fstrong>(เกร็ดความรู้สั้นๆ และเบื้องหลังการทำงาน)\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>🎬 TikTok: \u003C\u002Fstrong>\u003Ca target=\"_blank\" rel=\"noopener\" class=\"ng-star-inserted\" href=\"https:\u002F\u002Fwww.tiktok.com\u002F@superdevacademy?lang=th-TH\">\u003Cstrong>@superdevacademy\u003C\u002Fstrong>\u003C\u002Fa>\u003Cstrong> \u003C\u002Fstrong>(Tips &amp; Tricks ฉบับย่อยง่าย)\u003C\u002Fp>\u003C\u002Fli>\u003Cli>\u003Cp>\u003Cstrong>🌐 Website: \u003C\u002Fstrong>\u003Ca target=\"_blank\" rel=\"noopener noreferrer\" href=\"http:\u002F\u002Fsuperdevacademy.com\">\u003Cstrong>superdevacademy.com\u003C\u002Fstrong>\u003C\u002Fa>\u003Cstrong> \u003C\u002Fstrong>(คลังบทความและคอร์สเรียนฉบับเต็ม)\u003C\u002Fp>\u003C\u002Fli>\u003C\u002Ful>\u003Cp>\u003C\u002Fp>","39ih7hxpf8ja_0haq58zhl4.png","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclblg987654321\u002Fbny0x2e979e6m15\u002F39ih7hxpf8ja_0haq58zhl4.png","2026-07-13 05:30:46.231Z","76qprkevbgfdps8",{"keywords":15,"locale":66,"school_blog":76},[16,23,28,32,36,40,44,49,53,57,61],{"collectionId":17,"collectionName":18,"created":19,"created_by":13,"id":20,"name":21,"updated":22,"updated_by":13},"sclkey987654321","school_keywords","2026-03-04 08:20:11.547Z","ey3puyme01a9bsw","Go","2026-06-07 06:45:07.798Z",{"collectionId":17,"collectionName":18,"created":24,"created_by":13,"id":25,"name":26,"updated":27,"updated_by":13},"2026-03-04 08:20:14.253Z","ah6lvy4x8qe08l5","Golang","2026-06-07 06:45:08.193Z",{"collectionId":17,"collectionName":18,"created":29,"created_by":13,"id":30,"name":31,"updated":29,"updated_by":13},"2026-06-11 16:14:22.575Z","gluay8aj98wheus","RAG",{"collectionId":17,"collectionName":18,"created":33,"created_by":13,"id":34,"name":35,"updated":33,"updated_by":13},"2026-06-30 03:40:49.549Z","lz4vxehenzo8oum","Qdrant",{"collectionId":17,"collectionName":18,"created":37,"created_by":13,"id":38,"name":39,"updated":37,"updated_by":13},"2026-07-13 05:30:10.662Z","txjbbpvtqvsy14j","Gin Gonic",{"collectionId":17,"collectionName":18,"created":41,"created_by":13,"id":42,"name":43,"updated":41,"updated_by":13},"2026-06-09 02:46:11.702Z","y9xcv17enwjxdcw","Server-Sent Events",{"collectionId":17,"collectionName":18,"created":45,"created_by":13,"id":46,"name":47,"updated":48,"updated_by":13},"2026-05-11 08:41:15.880Z","lroded80zwh4qm3"," Design System","2026-06-07 06:49:13.636Z",{"collectionId":17,"collectionName":18,"created":50,"created_by":13,"id":51,"name":52,"updated":50,"updated_by":13},"2026-07-13 05:30:26.261Z","tg35sqbbdingumw","SSE Streaming",{"collectionId":17,"collectionName":18,"created":54,"created_by":13,"id":55,"name":56,"updated":54,"updated_by":13},"2026-07-06 06:33:19.833Z","nri7epnlbj96nyi","Context Injection",{"collectionId":17,"collectionName":18,"created":58,"created_by":13,"id":59,"name":60,"updated":58,"updated_by":13},"2026-07-13 05:30:34.326Z","i0grg21rqz4i7y1","Enterprise AI Tool",{"collectionId":17,"collectionName":18,"created":62,"created_by":13,"id":63,"name":64,"updated":65,"updated_by":13},"2026-03-04 08:44:11.146Z","gms2qr4xg6qv65e","Superdev Academy","2026-06-07 06:46:28.624Z",{"code":67,"collectionId":68,"collectionName":69,"created":70,"flag":71,"id":72,"is_default":73,"label":74,"updated":75},"th","pbc_1989393366","locales","2026-01-22 10:59:55.832Z","twemoji:flag-thailand","s8wri3bt4vgg2ji",true,"Thai","2026-04-10 15:42:46.614Z",{"category":77,"collectionId":78,"collectionName":79,"created":80,"expand":81,"id":96,"slug":97,"updated":98,"views":99},"wqxt7ag2gn7xcmk","pbc_2105096300","school_blogs","2026-07-13 05:30:45.321Z",{"category":82},{"blogIds":83,"collectionId":84,"collectionName":85,"created":86,"created_by":13,"id":77,"image":87,"image_alt":88,"image_path":89,"label":90,"name":91,"priority":92,"publish_at":93,"scheduled_at":88,"status":94,"updated":95,"updated_by":13},[],"sclcatblg987654321","school_category_blogs","2026-03-04 08:33:53.210Z","59ty92ns80w_15oc1implw.png","","https:\u002F\u002Ftwsme-r2.tumwebsme.com\u002Fsclcatblg987654321\u002Fwqxt7ag2gn7xcmk\u002F59ty92ns80w_15oc1implw.png",{"en":91,"th":91},"Golang The Series",1,"2026-03-16 04:39:38.440Z","published","2026-06-07 06:45:03.856Z","wqf1daar37kumo1","build-enterprise-rag-internal-ai-tool-golang-qdrant","2026-07-14 05:22:37.265Z",117,"bny0x2e979e6m15",[20,25,30,34,38,42,46,51,55,59,63],"2026-07-14 03:00:00.000Z","เจาะลึก Workshop ใหญ่ประจำซีซัน! สอนสร้างเซิร์ฟเวอร์ระบบถาม-ตอบอัจฉริยะ (RAG) ประจำองค์กรด้วยภาษา Go, Gin และ Qdrant พร้อมระบบกรองสิทธิ์ตามแผนก (Security Filter) และสตรีมคำตอบแบบ Real-time (SSE Streaming)","Golang The Series EP.160: วิธีสร้าง Enterprise Internal AI Tool (RAG) ด้วย Qdrant และ Gin","2026-07-14 03:00:00.127Z",{"th":97,"en":97}]