เซิร์ฟเวอร์ MCP ของผมเคยหยุดทำงานไปเฉยๆ โดยไม่มีแม้แต่ crash dump หรือ stack trace ใน log เลย ลูกค้า (clients) เชื่อมต่อเข้ามาได้ปกติ แต่หลังจากผ่านไปไม่กี่ชั่วโมง ทุกอย่างก็เงียบกริบ คำขอ (requests) หายไป และ AI agent ที่อยู่ปลายทางก็ไม่ได้รับอะไรเลยนอกจากความว่างเปล่า

นี่เป็นเรื่องที่น่าหงุดหงิดและเกิดขึ้นบ่อยมากในระบบนิเวศของ Model Context Protocol (MCP) โปรโตคอลนี้กำหนดวิธีการที่ AI agent จะค้นหาและเรียกใช้เครื่องมือภายนอก (external tools) แต่ข้อกำหนด (specification) นั้นสมมติว่าคุณจะจัดการกับข้อผิดพลาดด้วยตัวเอง บทเรียนและตัวอย่างเริ่มต้นส่วนใหญ่มักจะข้ามส่วนนี้ไป โดยมุ่งเน้นไปที่ "happy path" เท่านั้น เช่น การใส่ annotation ให้กับฟังก์ชัน, การเปิดใช้งานผ่านเซิร์ฟเวอร์ และการส่งผลลัพธ์ที่สะอาดกลับไป แต่แทบไม่มีใครแสดงให้เห็นว่าเกิดอะไรขึ้นเมื่อเครือข่ายเกิดขัดข้อง (network blip) กับ API ภายนอกของคุณ หรือเมื่อโมเดลเกิดอาการหลอน (hallucinate) ชื่อพารามิเตอร์และส่งข้อมูลขยะเข้ามา ผลลัพธ์ที่ได้คือเซิร์ฟเวอร์ที่เปราะบาง ซึ่งดูเหมือนจะทำงานปกติแต่จริงๆ แล้วตายไปหลายชั่วโมงแล้ว

ทำไมการตอบกลับที่ว่างเปล่าถึงแย่กว่าการแครช (Crashes)

เมื่อมี exception ที่ไม่ได้รับการจัดการหลุดรอดผ่าน MCP tool handler เลเยอร์การรับส่งข้อมูล (transport layer) มักจะกลืนมันหายไป กระบวนการของเซิร์ฟเวอร์ยังคงทำงานอยู่ socket ยังเปิดอยู่ แต่ไคลเอนต์กลับได้รับคำตอบที่ว่างเปล่า นี่เป็นเรื่องที่อันตรายกว่าการแครชแบบโจ่งแจ้ง เพราะระบบตรวจสอบ (monitoring) ของคุณอาจจะไม่สังเกตเห็น กระบวนการยังรันอยู่ พอร์ตยังเปิดรอรับการเชื่อมต่ออยู่ แต่ทุกการเรียกใช้เครื่องมือกลับไม่ส่งอะไรกลับมาเลย

โมเดล AI ไม่ได้ตีความความเงียบว่าเป็นความล้มเหลว แต่มันตีความว่าเป็นการเรียกใช้งานที่สำเร็จแต่ไม่มีข้อมูลส่งกลับมา การตอบกลับที่ว่างเปล่าแบบนี้จะฝึกให้โมเดลเริ่ม "ด้นสด" มันจะเริ่มสร้างข้อมูลเท็จ (hallucinating facts) เพื่อเติมเต็มช่องว่าง หรือไม่ก็ติดลูปในการพยายามเรียกใช้งานสิ่งที่เสียซ้ำๆ ปัญหาเล็กๆ อย่าง network timeout ชั่วคราว หรือ argument ของเครื่องมือที่ไม่ถูกต้อง ไม่ควรถูกปล่อยให้กลายเป็นพฤติกรรมแบบนี้

Wrapper Pattern: แนวป้องกันสามชั้น

ผมแก้ไขปัญหานี้ด้วยการห่อหุ้ม (wrap) ทุก tool handler ไว้ในเลเยอร์กู้คืนข้อผิดพลาด (error-recovery layer) บางๆ ตัว wrapper นี้ไม่ได้พยายามคาดเดาความล้มเหลวทุกอย่างที่อาจเกิดขึ้น แต่มันจะจัดหมวดหมู่ความผิดพลาดและตอบสนองอย่างเหมาะสม

ConnectionError และ TimeoutError
สิ่งเหล่านี้เกิดขึ้นเมื่อเซิร์ฟเวอร์ของคุณคุยกับ API ภายนอกแล้วเครือข่ายเกิดไม่เสถียร วิธีแก้ที่คนมักจะทำตามสัญชาตญาณคือการรีสตาร์ทกระบวนการ MCP server ทั้งหมด แต่อย่าทำแบบนั้น การรีบูตจะทำให้การเชื่อมต่อของไคลเอนต์ที่ใช้งานอยู่หลุดออกไป ล้างสถานะ (state) ทั้งหมดในหน่วยความจำ และบังคับให้ต้องเริ่มการทำงานใหม่ทั้งหมด (re-initialization) แทนที่จะทำแบบนั้น ให้ดักจับความล้มเหลวในการเชื่อมต่อและทำการเชื่อมต่อใหม่เฉพาะในส่วนของ transport layer หรือ HTTP client ที่เครื่องมือของคุณใช้งานอยู่ วิธีนี้จะทำให้เซิร์ฟเวอร์ยังคงพร้อมใช้งาน (warm) และพร้อมสำหรับคำขอถัดไปได้ทันที

ValueError
นี่คือสิ่งที่คุณจะเจอเมื่อ AI client ส่ง argument ที่ผิดรูปแบบมา บางทีโมเดลอาจจะสร้างพารามิเตอร์ขึ้นมาเอง ส่ง string มาในช่องที่ต้องการ integer หรือลืมใส่ฟิลด์ที่จำเป็น หากคุณปล่อยให้ error นี้หลุดออกไปโดยไม่จัดการ ไคลเอนต์อาจจะเจอทั้งการแครชหรือการตอบกลับที่ว่างเปล่า ให้ดักจับมันไว้ภายใน wrapper จากนั้นสร้างข้อความที่ชัดเจนและเฉพาะเจาะจงเพื่อบอกโมเดลว่าเกิดอะไรขึ้นกันแน่ อธิบายว่าพารามิเตอร์ตัวไหนที่ผิดพลาดและสิ่งที่ควรจะเป็นคืออะไร โมเดล AI สมัยใหม่ส่วนใหญ่จะอ่านข้อความนั้นและแก้ไขตัวเองได้ในการทำงานรอบถัดไปทันที ข้อผิดพลาดที่คลุมเครือจะทำให้เสียรอบการคิด (reasoning cycle) ไปโดยเปล่าประโยชน์ แต่ข้อผิดพลาดที่แม่นยำจะช่วยแก้ปัญหาได้ทันที

General Exceptions
สร้างตาข่ายนิรภัย (safety net) เอาไว้ หากมี error ที่อยู่นอกเหนือจากหมวดหมู่ข้างต้น ให้บันทึกรายละเอียด (log) ไว้สำหรับตัวคุณเอง และส่งการตอบกลับความล้มเหลวแบบทั่วไป (generic failure response) ที่สะอาดกลับไปให้ไคลเอนต์ วิธีนี้จะช่วยป้องกันไม่ให้กรณีแปลกๆ (edge case) เพียงกรณีเดียวทำให้เซิร์ฟเวอร์หยุดทำงานสำหรับทุกคน เซิร์ฟเวอร์ยังคงอยู่ ไคลเอนต์ได้รับสัญญาณว่ามีบางอย่างผิดพลาด และคุณยังมีบริบท (context) เพียงพอใน log เพื่อนำไป debug ในภายหลัง

flag isError คือสิ่งที่ต่อรองไม่ได้

นี่คือรายละเอียดสำคัญที่จะตัดสินว่าการแก้ไขของคุณได้ผลหรือไม่ การตอบกลับของ MCP จะรวมฟิลด์ boolean ที่ชื่อว่า isError ไว้ด้วย หากเกิด exception ขึ้นและคุณส่งข้อความ error กลับไปโดยไม่ได้ตั้งค่า isError เป็น true ไคลเอนต์จะปฏิบัติกับข้อความ error นั้นราวกับว่าเป็นผลลัพธ์ที่สำเร็จจากการเรียกใช้เครื่องมือ

ลองจินตนาการว่า API ภายนอกของคุณติด rate limit คุณดักจับ exception และส่งข้อความ "API rate limit exceeded" กลับไป แต่ปล่อยให้ isError เป็น false ไคลเอนต์จะส่งข้อความนั้นเข้าไปใน context window ของโมเดลราวกับว่าเป็นผลลัพธ์จริงจากเครื่องมือ จากนั้นโมเดลจะพยายามใช้เหตุผลกับข้อความนั้นเสมือนว่าเป็นข้อมูล มันอาจจะยกข้อความ error นั้นไปใส่ในบทสรุป หรือที่แย่กว่านั้นคือมันอาจจะสร้างความสัมพันธ์ที่ผิดเพี้ยนระหว่างข้อความ error นั้นกับข้อเท็จจริงอื่นๆ คุณได้เปลี่ยนปัญหาโครงสร้างพื้นฐานชั่วคราวให้กลายเป็นแหล่งข้อมูลที่ผิดพลาดไปเสียแล้ว

ให้ตั้งค่า isError เป็น true เสมอเมื่อคุณส่ง error payload กลับไป สิ่งนี้จะช่วยให้ client ได้รับสัญญาณที่ชัดเจนว่าการเรียกใช้ tool ล้มเหลว ซึ่งช่วยให้ model สามารถตัดสินใจได้ว่าจะลองใหม่ (retry), ขอคำอธิบายเพิ่มเติม หรือลองใช้ tool ตัวอื่นแทนไปเลย

รู้ว่าควรจะจัดการ (Catch) หรือหยุดการทำงาน (Kill) เมื่อใด

อย่าครอบคลุม server ทั้งหมดของคุณด้วย try-catch แบบสุ่มสี่สุ่มห้าที่กลืนกินทุกอย่าง ข้อผิดพลาดบางอย่างหมายความว่า server ควรหยุดทำงานทันที หากขาด environment variable ที่จำเป็นตอนเริ่มต้น หรือไฟล์ configuration เสียหาย การดักจับข้อผิดพลาดในระดับ request จะไม่ช่วยอะไรเลย ให้สร้าง exception class เฉพาะสำหรับ fatal errors เหล่านี้ และปล่อยให้มันทำให้ process พังไปเลย

กฎนั้นง่ายมาก หากข้อผิดพลาดนั้นเป็นเรื่องชั่วคราวหรือเกิดขึ้นเฉพาะกับ request เดียว ให้ catch และกู้คืนระบบ (recover) แต่หากข้อผิดพลาดนั้นหมายความว่าทุก request หลังจากนี้จะล้มเหลวอย่างแน่นอน ให้ปล่อยให้ server พังออกมาให้เห็นชัดเจน การล้มเหลวอย่างรวดเร็วตั้งแต่ตอนเริ่มต้นนั้นดีกว่าการที่ server ทำงานแบบติดๆ ขัดๆ ในสภาพที่พังไปแล้วเป็นเวลาหลายวันอย่างเทียบไม่ได้

เพิ่ม Observability ก่อนที่คุณจะจำเป็นต้องใช้มัน

เมื่อคุณมี wrapper พร้อมใช้งานแล้ว ให้ใช้ควบคู่ไปกับ structured logging โดยบันทึก (log) ทุกการเรียกใช้ tool และผลลัพธ์ในรูปแบบ JSON ซึ่งควรประกอบด้วยชื่อ tool, raw arguments, latency และสถานะว่าสำเร็จ, ล้มเหลว หรือมีการ retry

วินัยนี้จะให้ผลตอบแทนอย่างรวดเร็ว เมื่อคุณสังเกตเห็นจำนวน error พุ่งสูงขึ้น คุณจะสามารถกรองตาม tool และตรวจพบรูปแบบ (patterns) ได้ภายในไม่กี่นาที บางที external API เฉพาะเจาะจงอาจเริ่มเกิด timeout ในเวลาเดียวกันทุกวัน ซึ่งบ่งชี้ถึงช่วงเวลาการซ่อมบำรุงตามกำหนดการที่คุณไม่ทราบ หรือบางที tool ตัวหนึ่งอาจได้รับ arguments ที่ผิดรูปแบบอย่างต่อเนื่อง ซึ่งเผยให้เห็นข้อบกพร่องด้าน prompt engineering จากต้นทาง (upstream) การใช้ plain text logs ที่ถูกฝังอยู่ใน stack traces จะทำให้งานสืบสวนนี้เป็นเรื่องยากลำบาก แต่การใช้ structured JSON จะทำให้มันกลายเป็นเรื่องง่าย

ผลลัพธ์จากการใช้งานจริง (Production)

ผมได้ใช้ wrapper pattern นี้กับ production MCP servers สองตัวในช่วงสามสัปดาห์ที่ผ่านมา ในช่วงเวลานั้น ผมไม่พบ silent failures เลยแม้แต่ครั้งเดียว ก่อนที่จะเพิ่ม wrapper ผมพบข้อผิดพลาดที่ไม่สามารถอธิบายได้เฉลี่ยประมาณหนึ่งครั้งต่อวัน รูปแบบนี้ไม่ได้ซับซ้อน แต่ส่งผลกระทบอย่างมหาศาลเพราะมันช่วยแยกแยะ "noise" ที่ยังพอรับมือได้ ออกจากปัญหาที่แท้จริง

Silent failures สร้างความเสียหายมากกว่าการ crash เพราะการ crash จะไปกระตุ้นระบบแจ้งเตือน (alerting system) ของคุณ แต่ความเงียบจะทำลายความเชื่อมั่น วันหนึ่ง AI agent ของคุณอาจส่งข้อมูล tool ที่มีประโยชน์ แต่อีกวันมันอาจเริ่มมโนข้อมูลขึ้นมาเองเพราะ server หยุดตอบสนองไปหลายชั่วโมงแล้ว wrapper pattern จะช่วยปิดช่องว่างนั้น มันช่วยให้ server ของคุณทำงานต่อไปได้ท่ามกลางความผันผวนเล็กน้อย ให้ context ที่เพียงพอแก่ model ในการแก้ไขข้อผิดพลาดของตัวเอง และทำให้มั่นใจว่าเมื่อมีสิ่งที่ผิดพลาดร้ายแรงเกิดขึ้นจริงๆ คุณจะได้รับทราบทันที

หากคุณกำลังสร้าง MCP tools ในวันนี้ ให้เริ่มด้วยการทำ wrapper และใช้ flag isError ส่วนเรื่องอื่นๆ เป็นเพียงการเก็บรายละเอียดภายหลัง