ก้าวผ่าตัดสถาปัตยกรรม · ตอนที่ 08

จาก Demi-God Service กลายร่างเป็น Zeus Service: บทเรียนที่ทำให้ต้องวาง SBJ Runtime ใหม่

พ่อแยกบริการก้อนใหญ่ที่พันกันออกเป็นส่วนชัดเจนร่วมกับ Buddy

หลังจากเช้ามืดวันที่ 8 มิถุนายน ที่เราพาระบบรุ่นแรกก้าวขึ้น Production สำเร็จ ความมั่นใจของผมก็กลับมาเต็มเปี่ยมอีกครั้ง

ตลอดสัปดาห์ถัดมา ฟังก์ชันของระบบเริ่มพุ่งทะยานไปข้างหน้าอย่างรวดเร็ว: จากแค่บอทรับข้อความวิ่งธรรมดา เราเริ่มขยับสู่รุ่นถัด ๆ ไป:

  • เริ่มนำเข้าข้อมูลที่บันทึกจาก Huawei Health และรูปจานอาหาร
  • เริ่มมีระบบดึงข้อมูลน้ำหนักและองค์ประกอบร่างกาย
  • ฐานข้อมูลเริ่มมีโครงสร้างที่ซับซ้อนขึ้น

แต่ยิ่งฟังก์ชันโตเร็วเท่าไหร่ โค้ดหลังบ้านก็ยิ่งเริ่มส่งกลิ่นแปลก ๆ ออกมาแรงขึ้นเท่านั้น... และในที่สุด มันก็นำพาผมไปเจอกับปัญหาคลาสสิกที่คนทำระบบต้องเคยเจอ เพียงแต่รอบนี้เกิดจากการปล่อยให้ AI ช่วยสร้างเพลินไปหน่อย: โค้ดที่รวมทุกอย่างไว้ในที่เดียว ผมขอเรียกตัวรับแชทที่แบกหลายหน้าที่ไว้ว่า

1. เมื่อเซอร์วิสรับแชท กลายร่างเป็น Demi-God Service (เกือบหมื่นบรรทัด!)

หลายคนอ่านมาถึงตรงนี้อาจจะแอบสงสัยในใจว่า: “อ้าว! ในตอนที่ 6 เราเพิ่งปฏิวัติเอกสาร เอาสเปก API Payload & Response มาล็อกคุม AI อยู่หมัดจนพาขึ้นเซิร์ฟเวอร์รอบแรกได้สำเร็จแล้วไม่ใช่เหรอ แล้วทำไมจู่ ๆ ถึงปล่อยให้เกิด Demi-God Service เกือบหมื่นบรรทัดขึ้นมาได้ล่ะ?”

คำตอบที่ผมได้เรียนรู้ด้วยตัวเองคือ: เพราะวินัยที่เราทำในตอนนั้น มันคือ “วินัยระดับหน้าบ้าน (Interface & Task Discipline)” ครับ!

สเปกและ Contract ในตอนนั้น ช่วยล็อกไม่ให้ AI มั่วตอนคุยกับภายนอก และช่วยคุมงานให้ระบบเวอร์ชันแรก (v0.1.0) ก้าวขึ้น Production ได้สำเร็จจริง... แต่สิ่งที่มัน ยังไม่ได้คุม คือ: “โครงสร้างและขอบเขตสถาปัตยกรรมภายใน (Internal Architecture Boundaries)”!

หน้าบ้านคุยกันรู้เรื่อง สัญญา API เรียบร้อยดี... แต่ภายในหลังบ้าน เวลาเราสั่งงาน AI เขียนฟีเจอร์ใหม่ ๆ สิ่งที่ AI ถนัดที่สุดคือ: “การยัดทุกอย่างลงไปในไฟล์ที่มันกำลังเปิดอยู่!”

ตอนแรกเซอร์วิสนี้มีหน้าที่รับข้อความจาก LINE แต่เมื่อผมเพิ่มความสามารถใหม่ ๆ เข้าไปเรื่อย ๆ งานหลายส่วนทั้งการเชื่อม AI การจัดการข้อมูล และการเตรียมคำตอบก็เริ่มมารวมกันอยู่ในที่เดียว โดยที่ผมยังไม่ได้กำหนดขอบเขตภายในให้ชัดพอ

หลัง Go-Live ได้ไม่นาน เซอร์วิสรับแชทนี้ก็รวมโค้ดไว้เกือบ 10,000 บรรทัด และรับผิดชอบงานหลายส่วนเกินกว่าหน้าที่เดิมของมัน

ไฟล์ในเซอร์วิสตัวเดียว ถูกยัดเยียดทุกอย่างเข้าไป ตั้งแต่ตรรกะการคุย LINE, คิวรี่ฐานข้อมูลนับพันบรรทัด, สมองคิดของ AI, ตัวคอยตามกิจกรรม, ยันตัวตรวจเช็คคำตอบ...

“ทำไมไอ้เซอร์วิสรับแชทตัวเดียว... มันถึงต้องมาแบกสมอง AI, แบกฐานข้อมูล, และแบก Business Logic ทั้งหมดของระบบไว้คนเดียวขนาดนี้วะ!?”

ถ้าปล่อยไว้ การเปลี่ยนแปลงจุดหนึ่งอาจกระทบส่วนอื่นได้ง่าย และการเพิ่มช่องทางใหม่ก็จะดูแลยากขึ้น ผมจึงตัดสินใจวางขอบเขตของระบบใหม่

2. กับดัก AI Refactoring: เมื่อ Demi-God Service กลายร่างเป็น Zeus Service

ด้วยความที่ระบบมีขนาดใหญ่ ผมจึงไม่อยากทุบหม้อข้าวตัวเองรวดเดียว ผมเลือกใช้วิธีสุภาพ โดยสั่ง AI ให้ค่อย ๆ รีแฟกเตอร์:

“ช่วยแยกหน้าที่ของแต่ละส่วนออกมา แล้วค่อย ๆ ถอดฟังก์ชันออกมาทีละชุดนะ”

AI ในหน้าต่างแชทก็ตอบกลับมาอย่างกระตือรือร้นและสุภาพมาก: “ได้ครับ ถอดฟังก์ชันนี้ออกมาใส่ไฟล์ใหม่เรียบร้อยแล้วครับ!”, “ดำเนินการแยกเซอร์วิสเรียบร้อยแล้วครับ พร้อมทำงานต่อครับ!”

มันตอบว่า “ทำแล้ว” ไปเรื่อย ๆ อย่างมั่นใจ จนผมเริ่มรู้สึกสบายใจ... แต่พอตกดึก ผมลองเปิดเข้าไปตรวจดูเนื้อไฟล์จริง ๆ ในโค้ดเบส... สิ่งที่เห็นทำให้ผมต้องหยุดคิดทันที!

สิ่งที่ AI ทำจริง ๆ คือ: มันถอดฟังก์ชันย่อยออกไปใส่ไฟล์ใหม่ก็จริง... แต่ในไฟล์ Demi-God Service เดิม มันกลับไม่ได้เล็กลงเลยแม้แต่นิดเดียว! แถมซ้ำร้าย มันยังเขียน Glue Code (โค้ดกาวเชื่อมประสาน) และ Wrapper ฟังก์ชันเพิ่มขึ้นมาอีกบานเบอะ เพื่อคอยเรียกกลับไปหาไฟล์ใหม่ที่มันเพิ่งถอดออกไป!

ผลลัพธ์คือ: Demi-God Service ตัวเดิมไม่ได้หายไปไหน... แต่มันกลายร่างเป็น “Zeus Service” ที่ตัวใหญ่กว่าเดิม ซับซ้อนกว่าเดิม และผูกเงื่อนแน่นหนากว่าเดิมเข้าไปอีก!

วันนั้นผมหัวเราะแห้ง ๆ ออกมาคนเดียว: นี่คือข้อจำกัดของการสั่ง AI รีแฟกเตอร์แบบทีละจุด (Incremental Refactoring) ถ้าเราไม่มีการกำหนดขอบเขต (Boundary) และกติกาที่เด็ดขาด AI มักจะเลือกทางที่ง่ายที่สุดเสมอ คือการเขียนฟังก์ชันย่อยออกไป แต่ยังคงผูกตัวแปรเดิม หรือแปะโค้ดเชื่อม (Glue code) กลับเข้ามาที่ไฟล์เดิมเรื่อย ๆ จนกลายเป็นว่าไฟล์หลักก็ไม่เล็กลง แถมยังซับซ้อนขึ้นกว่าเดิมอีก!

3. จัดโครงสร้างใหม่เช้ามืด 10 มิถุนายน: นำโค้ดเดิมออก 9,303 บรรทัด

เมื่อการแยกฟังก์ชันทีละส่วนไม่ได้ทำให้ขอบเขตของระบบชัดขึ้น ผมจึงเลือกหยุดวิธีเดิมและวางโครงสร้างใหม่

เช้ามืดวันพุธที่ 10 มิถุนายน 2026 เวลาตีห้าตรง (05:00 น.) ก่อนจะเริ่มงานประจำในวันนั้น ผมสั่ง AI หยุดการถอดฟังก์ชันทีละชิ้น และสั่งรื้อโครงสร้างใหม่จากศูนย์:

  1. ลดสถานะของช่องทางแชท: LINE จะไม่ใช่ศูนย์กลางของจักรวาลอีกต่อไป มันเป็นแค่ "ประตูรับส่งสารบานหนึ่ง" ที่มีหน้าที่แค่รับข้อความเข้าและแปลงเป็นรูปแบบกลาง
  2. กำเนิด SBJ Runtime: ยกสมองและระบบประสานงานทั้งหมด ออกมาตั้งเป็นเซอร์วิสใหม่ โดยแบ่งหน้าที่อย่างเด็ดขาด: มีประตูกรองข้อความ และมีตัวกระจายงานตามสถานการณ์โดยเฉพาะ ไม่ให้ใครมาผูกติดกับใครตามอำเภอใจ
  3. แยกทางกับของเดิม: สั่งลบโฟลเดอร์โค้ดเก่าทิ้งทั้งหมด!

บันทึก Git ในเช้ามืดวันนั้นแสดงขนาดของการปรับโครงสร้างครั้งนี้:

“51 files changed, 848 insertions(+), 9,303 deletions(-)”

โค้ดเดิม 9,303 บรรทัดถูกนำออกจากโครงสร้างใหม่ การแยกขอบเขตครั้งนี้ทำให้เกิด SBJ Runtime ที่มีหน้าที่ชัดขึ้นและดูแลต่อได้ง่ายกว่าเดิม

4. และผีของบอทตัวเก่าที่ตามมารังควาน: ทดสอบขึ้นระบบบน Test Environment (15 มิถุนายน นอกเวลางาน)

บนเครื่อง Localhost โค้ดใหม่ของ Runtime รันได้อย่างสวยงาม ตลอดสัปดาห์นั้นเราพัฒนาต่อจนระบบเริ่มนิ่งขึ้น

ด้วยความรอบคอบ เมื่อเราเพิ่งผ่าตัดโครงสร้างครั้งใหญ่ ผมไม่ได้ผลีผลามส่งโค้ดขึ้น Production ทันที แต่เลือกที่จะนำขึ้นไปทดสอบบน สภาพแวดล้อมจำลองจริง (Test Environment) บน VPS ก่อน โดยเตรียมชุดคอนฟิกและสคริปต์แยกต่างหากสำหรับเซิร์ฟเวอร์ทดสอบ

แต่ความจริงของโลกวิศวกรรมซอฟต์แวร์คือ: ขนาดเป็นแค่ชุดทดสอบ โค้ดใหม่นิ่งแล้ว แต่ Infrastructure บนเซิร์ฟเวอร์ยังจำอดีต!

เช้าตรู่วันจันทร์ที่ 15 มิถุนายน 2026 วันที่ผมเริ่มกดรันสคริปต์ Deploy ขึ้น Test บน VPS เนื่องจากเป็นวันทำงาน การแก้ปัญหาครั้งนี้จึงต้องแบ่งออกเป็นสองช่วงนอกเวลางาน:

ช่วงเช้าตรู่ก่อนเข้างาน: คอนฟิกเดิมยังตามมาถึงระบบใหม่

เวลา 07:17 น. เมื่อเริ่มทดสอบ Runtime บน VPS ก็พบว่าการตั้งค่าบางส่วนยังอ้างถึงโครงสร้างเดิม ทำให้บริการใหม่เชื่อมต่อและผ่านการตรวจสถานะไม่ได้ ผมแก้การตั้งค่าบางส่วนเพื่อให้ระบบทดสอบเดินหน้าต่อได้ชั่วคราว ก่อนต้องปิดโปรเจกต์ส่วนตัวเพื่อไปทำงานประจำ

ช่วงค่ำหลังเลิกงาน: เก็บปัญหาการตั้งค่าให้จบ

หลังเลิกงานผมกลับมาสะสางปัญหาบน Test ต่อ แต่พบว่าการตั้งค่าเว็บเซิร์ฟเวอร์กับการออกใบรับรองยังไม่เข้ากัน ทำให้ระบบทดสอบเปิดใช้งานไม่ได้ชั่วคราว ผมจึงย้อนกลับมาทบทวนการตั้งค่าและเห็นว่าการแบ่งไฟล์ย่อยที่เลือกไว้ทำให้การดูแลส่วนนี้ซับซ้อนกว่าที่จำเป็น

ผมปรับการตั้งค่าให้เรียบง่ายขึ้น ลดส่วนแยกย่อยที่ไม่จำเป็น แล้วทดสอบขั้นตอนติดตั้งใหม่ หลังจากทดสอบอีกครั้ง Runtime บนสภาพแวดล้อม Test ก็ทำงานได้ตามที่ต้องการ

5. บทเรียนจาก Demi-God Service และ Zeus Service ร่วมกับ AI

สิ่งที่ผมได้เรียนรู้จากการปะทะกับ Demi-God Service ที่กลายร่างเป็น Zeus Service ในครั้งนั้น:

1. การแยกโค้ดทีละส่วนยังไม่พอ หากขอบเขตไม่ชัด

จากประสบการณ์ครั้งนั้น การสั่งให้ AI แยกฟังก์ชันออกทีละส่วนทำให้มีโค้ดเชื่อมต่อเพิ่มขึ้น แต่ไม่ได้เปลี่ยนขอบเขตความรับผิดชอบของระบบ ผมต้องเป็นคนกำหนดโครงสร้างและขอบเขตใหม่ แล้วตรวจว่าของเดิมควรย้ายหรือเอาออกอย่างไร

2. โค้ดวิ่งเร็วได้ แต่เอกสารและ Infra ต้องตามให้ทัน

เหตุผลที่การ Deploy ในวันที่ 15 มิถุนายนติดขัด ไม่ใช่เพราะโค้ดไม่ดี แต่เพราะ ฟีเจอร์ในโค้ดมันวิ่งแซงเอกสารและคอนฟิก Infra ไปไกลเกินไป โค้ดเปลี่ยนโครงสร้างแล้ว แต่เซิร์ฟเวอร์ยังมองเห็นชื่อเดิมอยู่

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

และที่สำคัญที่สุด... มันเปิดประตูสู่คำถามข้อถัดไป: ในเมื่อเรามีระบบหลังบ้านที่แข็งแรง และเริ่มมีหน้าเว็บแยกออกมาแล้ว เราจะแบ่งหน้าที่ระหว่าง "แชท" กับ "เว็บ" อย่างไร ให้แต่ละเครื่องมือทำหน้าที่ของมันอย่างแท้จริง?