วันก่อนผมเปิดแชทกับ ทิม (AI Agent ของผม) บนมือถือ สั่งงานง่ายๆ ไป 1 ประโยค แล้วเลื่อนดูคำตอบ — ปรากฏว่าระหว่างคำตอบจริงๆ มันมีกล่องข้อความสีเทาๆ เด้งแทรกขึ้นมาเต็มไปหมด ข้างในเป็น JSON ดิบๆ แบบ {"type":"system","subtype":"init"...} ที่อ่านไม่รู้เรื่องเลยครับ 555

คนทั่วไปเจอแบบนี้คงคิดว่า "เอ๊ะ แอปพังหรือเปล่า" แต่สำหรับผมที่รู้ว่าหลังบ้านมันทำอะไรอยู่ — นี่คือ event ดิบที่ไม่ควรหลุดมาถึงหน้าจอลูกค้าตั้งแต่แรก วันนี้เลยอยากเล่าเรื่องฟีเจอร์เล็กๆ ที่ดูเหมือนไม่มีอะไร แต่จริงๆ คือหัวใจของการทำ chat product ให้ดูสะอาดครับ

ที่มาของปัญหา — พอรองรับ 3 CLI ทุกอย่างก็พูดคนละภาษา

เรื่องเริ่มจากตอนที่ Newton ของผมเพิ่ม CLI ตัวที่ 3 ครับ — เดิมทีลูกค้าใช้ได้แค่ Claude แต่หลังๆ มีคนขอใช้ Codex กับ Antigravity ด้วย ทิมเลย refactor Tim Chat (หน้าจอแชทที่ลูกค้า Newton ทุกคนใช้) เป็น adapter pattern ให้รองรับทั้ง 3 ตัวพร้อมกัน

ปัญหาคือ CLI แต่ละตัวมัน "พูด" ออกมาคนละแบบเลยครับ เวลา AI ทำงาน มันจะ stream event ออกมาเป็นสายๆ — บอกว่ากำลังเริ่มงาน, กำลังเรียก tool, กำลังคิด, ตอบเสร็จแล้ว ฯลฯ Claude ส่ง event แบบหนึ่ง Codex ส่งอีกแบบ Antigravity ก็มี event แปลกๆ ของตัวเองอีก

หน้าที่ของ adapter คือแปลง event ดิบของแต่ละ CLI ให้กลายเป็น "bubble" สวยๆ ในหน้าแชท เหมือนกันหมด ลูกค้าจะได้ไม่รู้สึกว่าเปลี่ยน CLI แล้วหน้าตาเปลี่ยน

แต่พอมี CLI หลายตัว event แปลกใหม่ที่ adapter ยัง "ไม่รู้จัก" ก็โผล่มาเรื่อยๆ — และตรงนี้แหละครับที่เป็นต้นเหตุ

บั๊กที่แท้จริง — "ไม่รู้จัก" ไม่ได้แปลว่า "ไม่ต้องแสดง"

ตอนเขียน adapter แรกๆ ตรรกะมันง่ายมากครับ — ถ้าเจอ event ที่รู้จัก ก็แปลงเป็น bubble สวยๆ ถ้าเจอ event ที่ไม่รู้จัก... ก็โยนมันออกหน้าจอไปทั้งดุ้นแบบดิบๆ เผื่อจะมีประโยชน์

ฟังดูเหมือนปลอดภัยใช่ไหมครับ "แสดงไว้ก่อน ดีกว่าซ่อนข้อมูล" — แต่ในทางปฏิบัติมันกลายเป็นหายนะ UX เพราะ event ส่วนใหญ่ที่ adapter ไม่รู้จัก คือ event ภายในที่ลูกค้าไม่จำเป็นต้องเห็นเลย เช่น signal ว่า session เริ่มแล้ว, metadata ของ token, heartbeat ว่ายังต่อ connection อยู่

พวกนี้มันสำคัญสำหรับ "เครื่อง" แต่ไม่มีความหมายสำหรับ "คน" ครับ พอมันเด้งเป็น bubble เต็มจอ มือถือ ลูกค้าต้องเลื่อนผ่านกอง JSON เพื่อหาคำตอบจริงๆ ที่ตัวเองถาม

ทิมสรุปให้ผมฟังประโยคเดียวที่ผมว่าโดนมาก:

chat product ที่ดี ตัดสินกันที่ "อะไรที่มันไม่แสดง"
ไม่ใช่ "อะไรที่มันแสดง"

วิธีแก้ — วาง event protocol 3 ชั้น

แทนที่จะไล่ patch ทีละ event ที่หลุด (ซึ่งจะมีมาเรื่อยๆ ไม่จบ) ทิมเสนอให้วางกฎกลางขึ้นมาเลยว่า event ทุกตัวที่เข้ามาในระบบ ต้องถูกจัดเป็น 1 ใน 3 ชั้น:

Tier 1 — event ที่รู้จัก → bubble สวยๆ ให้ลูกค้าเห็น

พวกข้อความตอบของ AI, สรุปว่ากำลังเรียก tool อะไร (แบบย่อให้อ่านง่าย ไม่ใช่ดิบ), ผลลัพธ์ — อันนี้แปลงเป็น bubble หน้าตาเดียวกันหมดทุก CLI

Tier 2 — event ที่ไม่รู้จัก / debug → ลง terminal กับ console เท่านั้น ห้ามขึ้นจอลูกค้าเด็ดขาด

นี่คือชั้นที่แก้บั๊กตรงๆ ครับ — event แปลกใหม่ที่ adapter ยังไม่เคยเจอ จะไม่หายไปไหน (ผมยัง debug ได้) แต่มันจะไปโผล่ใน log หลังบ้านแทนที่จะเด้งเป็น bubble หน้าลูกค้า

// Tier 2: รู้ว่าไม่รู้จัก แต่ "เงียบ" ต่อ user
console.warn("[adapter] unknown event:", evt.type)
// ไม่มีการ emit bubble — จบตรงนี้

Tier 3 — error จริงๆ → bubble แจ้งเตือนแบบเป็นมิตร

ถ้าเป็น error ที่ลูกค้าควรรู้ (เช่น CLI ตาย, key หมดอายุ) อันนี้ขึ้น bubble ได้ แต่ต้องเป็นข้อความภาษาคนที่อ่านรู้เรื่อง ไม่ใช่ stack trace ดิบๆ — เรื่องนี้ผมเคยเล่าไว้ตอนสอน AI ให้พูดกับลูกค้าที่ไม่ใช่เดฟให้ถูกโทน ครับ หลักการเดียวกันเลย

กฎมันเรียบง่ายมาก แต่พลังอยู่ตรงที่ default เปลี่ยนจาก "แสดงไว้ก่อน" เป็น "ซ่อนไว้ก่อน" — event ไหนที่ไม่ได้ถูกออกแบบมาให้คนเห็นโดยตั้งใจ มันจะไม่มีทางหลุดขึ้นจอเลย

จุดที่ละเอียดกว่านั้น — เพิ่ม event ชนิดใหม่ต้องง่าย

สิ่งที่ผมชอบในวิธีนี้คือมันไม่ได้แก้แค่ปัญหาวันนี้ แต่กันปัญหาวันหน้าด้วยครับ

เพราะตอนนี้เวลามี CLI อัปเดต แล้วส่ง event ชนิดใหม่ออกมา ระบบจะไม่พังหน้าจอลูกค้าทันที — มันจะตกลง Tier 2 (เงียบใน terminal) โดยอัตโนมัติ ผมเห็นใน log ว่า "อ้าว มี event ใหม่นะ" แล้วค่อยตัดสินใจว่าจะเลื่อนมันขึ้น Tier 1 ให้เป็น bubble สวยๆ หรือปล่อยให้เงียบต่อไป

ทิมเลยทำเป็น spec กลางตัวหนึ่ง ที่บอกว่า adapter ของ CLI ใหม่ทุกตัวต้อง emit entry ออกมาเป็น shape เดียวกัน — มี field บอกว่านี่ Tier ไหน, ข้อความที่จะโชว์คืออะไร, ข้อมูลดิบเก็บไว้ตรงไหน เพิ่ม CLI ตัวที่ 4 ในอนาคตก็แค่เขียน adapter ให้พูดตาม protocol นี้ ไม่ต้องไปยุ่งกับโค้ดหน้า UI เลย

นี่คือความต่างระหว่างการ "อุดรูรั่ว" กับการ "วางระบบ" ครับ — อย่างแรกแก้ได้วันนี้แต่พรุ่งนี้รั่วใหม่ อย่างหลังแก้ทีเดียวแล้วทุก CLI ในอนาคตได้อานิสงส์ไปด้วย

บทเรียน — restraint คือฟีเจอร์

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

แต่ผมว่ามันสอนอะไรสำคัญ:

  • default ที่ปลอดภัยสำหรับเครื่อง ไม่ได้แปลว่าปลอดภัยสำหรับคน — "แสดงทุกอย่างไว้ก่อน" ฟังดู defensive ดี แต่จริงๆ มันโยนภาระให้ผู้ใช้ไปกรองเอง
  • การซ่อนข้อมูลถูกที่ ≠ การทิ้งข้อมูล — Tier 2 ไม่ได้ลบ event ทิ้ง แค่ย้ายมันไป terminal ผมยัง debug ได้เต็มที่ ลูกค้าก็ไม่รก
  • วาง protocol ดีกว่าไล่ patch — เหมือนหลายๆ บั๊กที่ผมเคยเล่า เช่นตอนAI ผมจำอะไรไม่ได้เพราะ regex match error เก่าไม่ครอบคลุม — แก้จุดเดียวมันกลับมาใหม่เสมอ แต่วางกฎกลางแล้วจบ

คนที่ทำ product มาสักพักจะรู้ครับว่า งานที่ทำให้ของ "ดูง่าย" มักจะยากกว่างานที่ทำให้ของ "ทำงานได้" เยอะ การตัดสินใจว่า อะไรไม่ต้องโชว์ นี่แหละคืองานออกแบบจริงๆ (อีกตัวอย่างของรายละเอียดเล็กๆ ใน Tim Chat ที่ผมยอมเสียเวลาแก้ คือตอนเสียงแจ้งเตือนหายทุกครั้งที่รีโหลด — เรื่องจิ๊บจ๊อยที่จริงๆ คือหัวใจของความน่าเชื่อถือ)

คำถามที่พบบ่อย

ทำไม chat UI ที่ดีต้องเลือกซ่อนข้อมูลบางอย่าง ไม่ใช่แสดงทุกอย่าง

เพราะข้อมูลที่ระบบ generate ออกมาส่วนใหญ่ออกแบบมาให้ "เครื่อง" อ่าน ไม่ใช่ "คน" อ่าน ถ้าแสดงทุกอย่างออกมา ผู้ใช้ต้องเลื่อนผ่านกอง JSON เพื่อหาคำตอบจริงๆ หลักการคือ default ที่ดีควรเป็น "ซ่อนไว้ก่อน" แล้วค่อย promote ขึ้นมาเป็น bubble เฉพาะสิ่งที่ออกแบบมาให้คนเห็นโดยตั้งใจ

Tier 2 events ที่ซ่อนไว้ใน terminal มันหายไปเลยไหม หรือยัง debug ได้

ยัง debug ได้ครับ Tier 2 แค่ย้าย event ไปอยู่ใน log หลังบ้านแทนที่จะเด้งเป็น bubble หน้าลูกค้า ดังนั้น developer หรือเจ้าของระบบยังดู log ได้เต็มที่ แต่ผู้ใช้ทั่วไปไม่ต้องเจอข้อมูลที่ไม่เกี่ยวข้อง การซ่อนถูกที่ไม่เท่ากับการทิ้งข้อมูล

ถ้าอัปเดต CLI แล้วมี event ชนิดใหม่โผล่มา ระบบจะพังไหม

ไม่พังครับ เพราะ default ของระบบนี้คือ event ที่ไม่รู้จักจะตกลง Tier 2 อัตโนมัติ หมายความว่าหน้าจอลูกค้ายังสะอาดอยู่ แค่ event ใหม่นั้นจะไปโผล่ใน log แทน แล้วค่อยตัดสินใจว่าจะ promote ขึ้น Tier 1 ให้เป็น bubble สวยๆ หรือปล่อยให้เงียบต่อไป

adapter pattern กับ event tiering ทำงานร่วมกันยังไง

แต่ละ CLI adapter มีหน้าที่แปลง event ดิบของ CLI ตัวนั้น (ซึ่งพูดคนละภาษากัน) ให้กลายเป็น event format กลางที่ระบบรู้จัก format กลางนี้แหละที่ระบุว่า event นี้อยู่ Tier ไหน ซึ่งหน้า UI จะนำไปแสดงหรือซ่อนตามกฎ ดังนั้นเพิ่ม CLI ใหม่ก็แค่เขียน adapter ใหม่ให้ emit event ตาม format กลาง โดยไม่ต้องแตะโค้ด UI เลย

Newton — AI Agent ในเครื่องของคุณเอง หน้าจอสะอาด ใช้ง่ายจากมือถือ

หน้าจอแชทที่ผมเล่ามาทั้งหมดนี่ คือสิ่งที่ลูกค้า Newton ทุกคนได้ใช้ครับ — AI Agent เต็มตัวที่อยู่ใน server ส่วนตัวของคุณ 24 ชั่วโมง คุยสั่งงานได้จากมือถือเหมือนแชทกับคน

ข้อดีที่ผมเชื่อมาตลอดของการมี AI บน server ส่วนตัว ไม่ใช่บน platform คนอื่น คือผมควบคุมได้ทุกชั้น — อยากให้หน้าจอแสดงอะไร ซ่อนอะไร เพิ่ม CLI ตัวไหน ทำได้หมด ไม่ต้องรอ vendor ใครมาอนุญาต และพอผมแก้ตรงนี้ ลูกค้า Newton ทุกเครื่องก็ได้หน้าจอที่สะอาดขึ้นตามไปด้วย

ถ้าคุณอยากมี AI Agent ที่อยู่ในเครื่องของคุณเอง ทำงานต่อเนื่อง คุยง่ายๆ จากมือถือ และมีคนคอยขัดเกลาให้มันดีขึ้นทุกวัน — ลอง Newton ดูครับ setup เสร็จใน 10 นาที ไม่ต้องมีความรู้เรื่อง server เลย

— ปอนด์