บั๊กที่ AI แชทบอทของผมทำจริง และสิ่งที่แต่ละตัวสอน
ผมทำ Petcare Assistant คนเดียว เป็นผู้ช่วย AI กับ CRM บน LINE สำหรับร้านสัตว์เลี้ยง (คลินิก ร้านอาบน้ำตัดขน รับฝากเลี้ยง) บอทตอบลูกค้าเป็นภาษาไทย เรียก tool เพื่อดูราคา คิวว่าง และนโยบายร้าน และห้ามแนะนำเรื่องยาเด็ดขาด ตัวเดโมรันอยู่ที่ https://petcare.rattanun.dev ต่อกับ LINE OA จริง
บทความนี้รวบรวมบั๊กที่บอทสร้างขึ้นจริง บางตัวเจอตอนรัน eval บางตัวลูกค้าเจอบน LINE ก่อนผม แต่ละเรื่องจะเล่าว่าเกิดอะไร ทำไมถึงเกิด แก้ยังไง และได้บทเรียนอะไร ผมเลือกเรื่องที่บทเรียนใช้กับโปรเจกต์ LLM อื่นได้ด้วย ไม่ได้เลือกเรื่องที่ฟังดูเท่
1. “เสาร์นี้” กลายเป็นวันศุกร์
เกิดอะไรขึ้น ลูกค้าขอจอง “วันเสาร์นี้” บอทตอบ “วันเสาร์ที่ 2 ตุลาคม 2569” ทั้งที่วันที่ 2 เป็นวันศุกร์ จากนั้นก็ค้นคิวของวันที่ผิดแล้วกันคิววันศุกร์ไว้ให้ ถ้าเป็นลูกค้าจริงเขาจะมาผิดวัน และร้านจะได้ no-show ที่อธิบายไม่ได้
ทำไม คนไทยพูดวันแบบสัมพัทธ์ (“เสาร์นี้” “อาทิตย์หน้า”) แต่ booking tool ทุกตัวรับ YYYY-MM-DD การแปลงจากอย่างแรกเป็นอย่างหลังคือการคิดเลข และให้โมเดลภาษาคิดเลขกลางบทสนทนาก็เหมือนโยนเหรียญ ในบทสนทนาเดียวกันมันยังตอบไม่ตรงกันเองด้วย เทิร์นหนึ่งบอก “เสาร์ที่ 3” เทิร์นถัดไปบอก “เสาร์ที่ 2”
แก้ยังไง ใส่ 14 วันข้างหน้าเป็นตารางลงใน system prompt แบบ 2026-10-03 = วันเสาร์ที่ 3 ตุลาคม (calendarSection ใน packages/core/src/prompt.ts) และห้ามพูดวันในสัปดาห์คู่กับวันที่ ถ้าทั้งคู่ไม่ได้มาจากบรรทัดเดียวกันในตาราง ตารางนี้สร้างใหม่ทุกเทิร์นตาม timezone ของร้าน เสีย token เพิ่มเทิร์นละไม่กี่สิบตัว ซึ่งถือว่าคุ้มถ้าแลกกับการจองที่ลงวันถูก
บทเรียน: อะไรที่เป็นการคำนวณ ให้โมเดลเปิดตารางหาคำตอบแทนการคิดเอง

2. ข้อความที่ขาดกลางคำแต่ผ่าน guard ทุกตัว
เกิดอะไรขึ้น วันที่ 1 ตุลาคม ลูกค้าบน LINE OA จริงถามราคาอาบน้ำ แล้วได้ข้อความนี้กลับไปทั้งข้อความ
สำหรับการอาบน้ำสุนั
ในระบบ เทิร์นนี้มีสถานะ completed มี output 19 token ไม่มี guard ตัวไหน fail ไม่มี error ส่วน quote_price ที่เรียกก่อนหน้าก็คืนราคาครบ โมเดลแค่หยุดเขียนไปเฉย ๆ แล้วเราก็ส่งเท่าที่มันเขียนออกไป
ทำไม guard ทุกตัวตรวจว่าข้อความ พูด อะไร เช่น มีลิงก์ไหม มีเบอร์โทรไหม มีราคาที่ไม่ได้มาจาก tool ไหม มีขนาดยาไหม แต่ข้อความที่ขาดกลางคำไม่ได้พูดอะไรเลย มันจึงไม่ผิดกฎข้อไหน ส่วน non_empty ก็ดูแค่ว่ามีตัวอักษรหรือเปล่า ทั้งที่ข้อมูลที่จะหยุดเรื่องนี้ได้อยู่ในมือแล้ว AI SDK ส่ง finishReason ขึ้นมาทุกครั้ง แต่ไม่มีโค้ดใน packages/core อ่านค่านี้ หรือแม้แต่เก็บไว้
แก้ยังไง ถ้าคำตอบไม่ได้จบด้วย "stop" ให้นับว่า attempt นั้นล้มเหลว แล้วให้โมเดลถัดไปใน fallback chain ลองแทน เหมือนกรณี error หรือ timeout ถ้าทุก attempt จบผิดปกติ บอทจะส่งต่อให้เจ้าหน้าที่พร้อมข้อความแจ้งลูกค้าตรง ๆ ซึ่งดีกว่าส่งคำครึ่งคำ ทุกเทิร์นตอนนี้บันทึก finishReason กับ attempt ที่ถูกทิ้งไว้ใน debug ด้วย
นอกจากนี้ยังเพิ่ม backstop แคบ ๆ ไว้ใน non_empty คือข้อความไทยจบด้วยไม้หันอากาศ (ั) หรือสระหน้า (เ แ โ ใ ไ) ไม่ได้ ซึ่งเป็นกฎอักขรวิธี ไม่ใช่เรื่องสไตล์การเขียน ผมไม่ได้เขียน heuristic ว่า “ข้อความไทยที่ยังไม่จบ” หน้าตาเป็นยังไง เพราะภาษาไทยไม่มีจุดจบประโยค คำตอบที่ถูกต้องก็จบด้วยราคา เวลา หรือที่อยู่โดยไม่มีค่ะ/คะได้ ถ้าใช้ heuristic จะไปบล็อกคำตอบดี ๆ
บทเรียน: provider บอกอยู่แล้วว่าข้อความนี้เชื่อไม่ได้ อ่านค่านั้นให้ตรง ๆ ดีกว่าเดาจากเนื้อข้อความ
3. โมเดลรู้ว่าฉุกเฉิน แต่คนในร้านไม่รู้
เกิดอะไรขึ้น บน LINE OA จริง ลูกค้าพิมพ์ “สุนัขของฉันกินกาแฟไปเมื่อกี้ทำไงดี” คาเฟอีนเป็นพิษกับสุนัข แต่รายการคำฉุกเฉิน (KEYWORDS) ไม่มีคำว่ากาแฟ ขั้นตรวจล่วงหน้าเลยเงียบ ไม่มีเบอร์ฉุกเฉินของร้าน ไม่มีแจ้งเตือนเจ้าหน้าที่ ไม่ขึ้นแท็บฉุกเฉิน ขณะที่โมเดลตอบได้ดีมาก (“อันตรายมากเลยค่ะ! กาแฟมีคาเฟอีน ซึ่งเป็นพิษร้ายแรงต่อสุนัข… รีบพาน้องหมาไปพบสัตวแพทย์”) ร้านมารู้เรื่องก็ต่อเมื่อลูกค้าขอคุยกับคน
ทำไมการเพิ่มคำว่ากาแฟถึงไม่ใช่ทางแก้ ผมเพิ่มคำในกลุ่มคาเฟอีนเข้าไปแล้ว แต่รายการนี้จะขาดไปหนึ่งคำเสมอ สารพิษมีเยอะ และแต่ละตัวก็พิมพ์ได้หลายแบบ ปัญหาจริงคือระบบมีคำตอบที่ถูกอยู่แล้ว โมเดลรู้ว่านี่คือเหตุฉุกเฉิน แต่ไม่มีอะไรรับรู้ต่อจากมัน
ทำไมไม่อ่านจากข้อความตอบ วิธีที่ง่ายที่สุดคือดูว่าโมเดลเขียนว่า “รีบพาไปหาหมอ” ไหม แต่บอทแนะนำ “พาน้องไปพบสัตวแพทย์” แทบทุกครั้งที่มีอาการ ตั้งแต่หมาไม่กินข้าวไปจนถึงแมวเกา ถ้าใช้วิธีนี้แท็บฉุกเฉินจะเต็มไปด้วยเคสที่ไม่ฉุกเฉิน จนเจ้าหน้าที่เลิกดู ซึ่งแย่กว่าปัญหาเดิม
แก้ยังไง เพิ่ม tool ชื่อ report_emergency ให้โมเดลแจ้งการประเมินของตัวเองแบบมีโครงสร้าง พร้อมเหตุผลสั้น ๆ สำหรับเจ้าหน้าที่ ที่เหลือโค้ดทำทั้งหมด engine ใส่ข้อความฉุกเฉินที่ร้านตั้งไว้นำหน้าคำตอบ แล้ว pipeline เปิดเหตุฉุกเฉินด้วย raiseEmergency ตัวเดียวกับเส้นทาง keyword คือสลับแชทเป็นโหมดคน ตั้งสถานะ urgent และแจ้งกลุ่มแอดมินว่า “บอทประเมินว่าฉุกเฉิน” โดยทำทั้งหมดนี้ ก่อน ส่งคำตอบ ถ้าส่งข้อความไม่สำเร็จ การแจ้งเตือนก็ยังไม่หาย ข้อความนี้กลายเป็น eval case emergency-caffeine แบบคำต่อคำ
บทเรียน: ถ้าโมเดลตัดสินอะไรที่สำคัญ ให้มันบอกผ่าน tool ที่โค้ดเอาไปทำงานต่อได้ อย่าพยายามแกะจากร้อยแก้วของมัน
4. ข้อความสำคัญที่สุดของระบบ ไม่มีเบอร์โทรเลย
เรื่องนี้ผมเจอเองตอนไล่ตรวจอย่างอื่น buildEmergencyReply มีสองทาง ทางแรกให้เบอร์ฉุกเฉินของร้านกับโรงพยาบาลสัตว์ 24 ชั่วโมงที่ร้านแนะนำ ทางที่สองใช้เมื่อร้านไม่ได้ตั้งค่าอะไรไว้ ซึ่งพูดได้แค่ “โทรหาโรงพยาบาลสัตว์ที่ใกล้ที่สุด” ปรากฏว่า ข้อความฉุกเฉินทุกข้อความที่เคยมีคนเห็นมาจากทางที่สองทั้งหมด
สาเหตุมีสามข้อพร้อมกัน seed ไม่ได้ตั้ง emergencyPhone กับ emergencyHospitals ทำให้ eval และเดโมทุกครั้งวัดแต่ทางสำรอง ไม่มีหน้าไหนใน dashboard เขียนสองคอลัมน์นี้ ร้านจริงจึงตั้งไม่ได้แม้อยากตั้ง และ checklist ตอน onboarding ก็ไม่ได้ถาม เลยไม่มีใครสังเกตว่าขาด
ผมแก้ seed และบันทึกช่องตั้งค่ากับรายการ onboarding เป็นงานค้าง ต่อมาข้อความฉุกเฉินยังได้ปุ่มโทร (tel:) เพิ่ม เพื่อให้เจ้าของสัตว์ที่กำลังตกใจไม่ต้องคัดลอกเบอร์เอง
บทเรียน: fallback ที่ไม่มีใครแยกออกจากของจริงได้ไม่ใช่ fallback แต่มันคือตัวสินค้าไปแล้ว และยิ่งข้อความสำรองสุภาพเท่าไร ก็ยิ่งอยู่รอดโดยไม่มีใครเห็นได้นานเท่านั้น
5. Guard ที่บล็อกของดี: “ไม่ทราบว่า…” และเบอร์ที่อ่านข้ามบรรทัด
guard ที่จับผิดพลาดน่ากลัวพอ ๆ กับ guard ที่จับไม่ได้ เพราะมันพังเงียบ ๆ ในรูปของการส่งต่อแชทให้คนโดยไม่จำเป็น ผมเจอแบบนี้สองครั้ง
ครั้งแรก no-answer.ts ใช้ /ไม่ทราบ/ ตรวจว่าบอทยอมแพ้หรือเปล่า แล้ว handoff rule no_answer ก็นับครั้งที่เกิดติดกัน แต่ “ไม่ทราบว่าคุณลูกค้าสะดวกวันไหนคะ” เป็นวิธีเปิดคำถามแบบสุภาพในภาษาไทย และการจองครั้งหนึ่งต้องถามแบบนี้สองสามครั้ง บทสนทนาจองปกติเลยถูกส่งให้คนก่อนจะได้เลือกคิว แก้โดยเปลี่ยน pattern เป็น /ไม่ทราบ(?!ว่า)/ คำยอมรับว่าไม่รู้ยังถูกนับ แต่คำถามไม่ถูกนับ
ครั้งที่สอง (2 พฤศจิกายน บน OA เดโม) ลูกค้าถาม “ที่ร้านมีกี่สาขา” บอทตอบเป็นรายการมีเลขข้อ แต่ละสาขามีเบอร์ต่อท้าย PHONE_PATTERN ยอมให้มี whitespace ทุกแบบคั่นระหว่างตัวเลข รวมถึงการขึ้นบรรทัด มันเลยอ่าน 02-000-0102\n2. สาขาอารีย์ เป็นเบอร์เดียว 02000001022 ซึ่งไม่ใช่เบอร์ร้าน guard จึงบล็อกเบอร์ของร้านเองว่าเป็นเบอร์คนอื่น แล้วคำตอบก็ตกไปเป็นข้อความส่งต่อเจ้าหน้าที่ แก้โดยยอมให้คั่นได้แค่ space, tab หรือขีด ([ \t-])
บทเรียน: guard ต้องมี test ฝั่ง “ต้องไม่บล็อก” เยอะพอ ๆ กับฝั่ง “ต้องบล็อก” ตอนนี้ guards.test.ts ของ medicine guard มีเคสที่ต้องไม่บล็อกมากกว่าเคสที่ต้องบล็อก ด้วยเหตุผลนี้

6. ความล้มเหลวที่เงียบ: ที่จอดรถ, embedding key และ schema ที่เข้มเกินไป
สามเรื่องนี้ต่างกัน แต่หน้าตาเหมือนกัน คือบอทยังตอบได้ ระบบดูปกติ แค่ตอบผิดหรือขาดไปทีละนิด
ที่จอดรถ ลูกค้าถามเรื่องที่จอดรถ บอทตอบว่า “สาขาบางนาไม่มีที่จอดรถ” ทั้งที่ knowledge base เขียนไว้ว่าบางนาจอดในลานของอาคารได้ สาเหตุคือ get_business_info คืน parking: null เพราะร้านไม่ได้กรอกช่องนี้ แล้วโมเดลก็อ่าน null ว่า “ไม่มี” ผมลองเพิ่มกฎใน prompt ก่อน ได้ 0/3 พอเปลี่ยนให้ tool ตอบว่า not filled in for this branch — call search_knowledge before saying it is unknown ได้ 3/3
embedding key หมดอายุ ช่วงเดียวกันนั้น การค้น knowledge base ล้มบน production ตั้งแต่ราว 29 ตุลาคม เพราะ embedding key ตอบ 401 expired_secret_key แต่ไม่มีอะไรแจ้งเตือน ยังไม่มี watchdog check สำหรับเรื่องนี้ และ alert ยังตั้งเป็นแค่ log ทุกคำถามที่ต้องใช้ knowledge base จึงล้มอยู่ประมาณสี่วัน บอทบอกว่าไม่มีข้อมูลที่จอดรถบางนาแล้วส่งต่อให้คน ผมมาเจอเองตอนลองแชทบน LINE แก้ด้วยการเปลี่ยน key และจด watchdog check สำหรับ embedding ที่ล้มซ้ำ ๆ เป็นงานถัดไป
extraction schema ระบบจำข้อมูลลูกค้าจากแชท (memory extraction) ล้มทุกบทสนทนาหลังย้ายงานนี้ไปรันบน Gemini แต่บอทหลักยังตอบได้ปกติ จึงดูไม่มีอะไรผิด จนเปิด log ของ worker แล้วเห็นแชทเดิมล้มทุกสิบนาที log เขียนแค่ “All models failed” ต้องไปดู error ของ attempt นั้นเองถึงเห็นว่า Google ปฏิเสธ schema เพราะ “too many states for serving” ทุก .max(), .regex(), .int() ใน zod schema กลายเป็นส่วนหนึ่งของ constrained decoder ฝั่ง provider ทางแก้คือส่ง schema แบบหลวม (extractionWire) ให้โมเดล แล้วตรวจขอบเขตในโค้ดทีละรายการ อะไรที่ไม่ผ่านก็ทิ้งและนับไว้ ตอนนี้ poller log error ของทุก attempt แล้ว
บทเรียน: ค่าว่างต้องบอกว่า “ว่าง” ไม่ใช่ปล่อยให้โมเดลตีความว่า “ไม่มี” และระบบที่ลดคุณภาพลงอย่างสุภาพต้องมีคนเฝ้า
7. สัญญาระหว่างข้อความกับ tool: ปุ่มที่ไม่มี และประโยคที่ไม่ได้มีไว้ให้ลูกค้าอ่าน
ตั้งแต่ 2 พฤศจิกายน บอทเลิกจองในแชท แล้วส่งปุ่ม จองคิว ที่เปิดหน้า LIFF ให้ลูกค้าเลือกเองแทน เพราะการจองในแชทพังได้ทุกขั้น (เคยมีครั้งหนึ่งบอทตั้งชื่อบริการเอง “โรงแรมแมว” ได้ service_not_found กลับมา แล้วขอให้ลูกค้าสะกดคำที่บอทคิดขึ้นเองใหม่) การเปลี่ยนนี้ทำให้เจอบั๊กอีกแบบ คือคำพูดของบอทกับสิ่งที่ tool ทำจริงไม่ตรงกัน
ปุ่มที่สัญญาไว้แต่ไม่มี ในการทดสอบวันที่ 4 พฤศจิกายน โมเดลเขียน “กดปุ่ม “จองคิว” ด้านล่าง” โดยไม่ได้เรียก send_booking_link อยู่ 3 ใน 5 ครั้ง ลูกค้าจะมองหาปุ่มที่ไม่มีอยู่ แก้โดยให้ engine แนบปุ่มตามที่ข้อความสัญญาไว้ (pagesPromisedInText ใน packages/core/src/page-links.ts) นับเฉพาะคำว่า “ปุ่ม” ที่ตามด้วยชื่อหน้าตรง ๆ ข้อความที่แค่พูดถึงการจองจะไม่ได้ปุ่มเพิ่ม เคสเป้าหมายรัน 5 เคส × 5 ครั้งได้ 25/25
ประโยคภาษาอังกฤษที่หลุดไปถึงลูกค้า ตอนจูนกับ claude-haiku-5.5 (5 พฤศจิกายน) check_availability บอกว่าวันศุกร์เต็ม แล้วโมเดลส่ง tellCustomer ของ send_booking_link ซึ่งเป็นประโยคภาษาอังกฤษ “press the จองคิว button below this message…” ไปเป็นคำตอบทั้งข้อความ และไม่บอกเรื่องคิวเต็มเลย ชื่อฟิลด์ tellCustomer บอกโมเดลแทบตรง ๆ ว่าให้ส่งต่อให้ลูกค้า แก้โดยเขียนค่าใหม่เป็นคำสั่ง ขึ้นต้นด้วย “this is not your reply” แล้วบอกว่าคำตอบต้องมีอะไรบ้าง เคส booking-vet-wrong-day บน haiku จาก 0/3 เป็น 5/5
บทเรียน: ผลลัพธ์ของ tool ก็เป็น prompt อย่างหนึ่ง ชื่อฟิลด์และรูปประโยคในนั้นกำหนดพฤติกรรมโมเดลได้พอ ๆ กับ system prompt และถ้าข้อความสัญญาว่าจะมีอะไร โค้ดควรทำให้สิ่งนั้นมีจริง

8. ขอ “รหัสการจอง” ที่ลูกค้าไม่เคยเห็น
เกิดอะไรขึ้น บน production วันที่ 5 พฤศจิกายน ลูกค้าพิมพ์ “ยกเลิกไป แล้วไปจองสาขาอารีย์ได้ไหม” บอทตอบให้ “แจ้งรหัสการจองที่ต้องการยกเลิก” แต่รหัส B1, B2 เป็นตัวอ้างอิงภายในที่ list_my_bookings คืนให้โมเดล ลูกค้าไม่เคยเห็นมันเลย
แก้ยังไง บทสนทนานี้กลายเป็น eval case สองตัวทันที ถ้าลูกค้ายังไม่ยืนยันตัวตน ต้องมีคนช่วยและห้ามเรียก cancel_booking ถ้ายืนยันแล้ว ต้องเรียก list_my_bookings แล้วถามว่าจะยกเลิกนัดไหน ทั้งสองเคสห้ามมีคำว่า “รหัส” ในคำตอบ
บทเรียน: ตัวอ้างอิงภายในที่ส่งให้โมเดลจะหลุดออกไปถึงลูกค้าสักวัน และแชทจริงที่ผิดควรกลายเป็น eval case ภายในวันเดียวกัน
สิ่งที่ได้จากบั๊กทั้งหมดนี้
กฎที่โมเดลละเมิดซ้ำ ๆ ต้องย้ายไปอยู่ในโค้ด ผมจูน prompt ให้ฟีเจอร์ Customer Card จาก 3/10 ไปถึง 9/15 แล้วก็ตันอยู่ตรงนั้น บางกฎค้างที่ 1/3 ไม่ว่าจะเขียนแบบไหนหรือวางไว้ตรงไหน อะไรที่สินค้าต้องพึ่งจริง ๆ เช่นราคาต้องมาจาก tool หรือการจองต้องมาจากการกดปุ่ม ตอนนี้บังคับด้วย output guard, tool ที่ไม่มีให้เรียก หรือสถานะที่โมเดลตั้งเองไม่ได้ guard ต้องมี test ฝั่ง false positive guard ที่บล็อกคำตอบที่ถูกจะทำให้บอทเงียบในเรื่องที่ต้องพูดได้ หรือส่งต่อให้คนโดยไม่จำเป็น ภาษาไทยทำให้เรื่องนี้ยากขึ้น เพราะคำลงท้ายสุภาพใช้ได้ทั้งตอนตกลงและตอนปฏิเสธ
เฝ้าระวังความล้มเหลวที่เงียบ key หมดอายุ schema ที่ provider ไม่รับ fallback ที่ไม่มีใครตั้งค่า ทั้งหมดนี้ไม่ทำให้อะไรล่ม บอทยังตอบได้ แค่ตอบแย่ลง ต้องมีสัญญาณที่วัดได้ เช่น finishReason หรือ error ของแต่ละ attempt และต้องมีคนเห็นสัญญาณนั้นจริง ๆ
แชทจริงกลายเป็น eval case เกือบทุกเรื่องในบทความนี้มี eval case ที่ใช้ข้อความจากแชทจริงแบบคำต่อคำ เช่น emergency-caffeine, booking-hotel-sends-link, booking-intent-sends-button, booking-cancel-unverified-no-code บั๊กที่เคยเกิดแล้วจึงกลับมาโดยไม่มีใครรู้ไม่ได้
ที่มา
- เรื่อง 1:
docs/decisions.mdหัวข้อ “The model must never do date arithmetic” ;packages/evals/src/cases.tsเคสbooking-keeps-the-date - เรื่อง 2:
docs/decisions.mdหัวข้อ “A reply cut off mid-word passed every guard” - เรื่อง 3:
docs/decisions.mdหัวข้อ “The model knew it was an emergency, and nobody at the shop did”;packages/evals/src/cases.tsเคสemergency-caffeine - เรื่อง 4:
docs/decisions.mdหัวข้อ “The most important message the product sends had no phone number in it” และ “Call buttons under the emergency reply, after the emergency is raised (2026-11-02)” - เรื่อง 5:
docs/decisions.mdหัวข้อ ““ไม่ทราบว่า…” is a question, not a surrender” และ “The medicine guard asks what the sentence tells the owner to do, not which words it uses”;packages/core/src/guards.tsคอมเมนต์เหนือPHONE_PATTERN; commitefaa194 - เรื่อง 6:
docs/decisions.mdหัวข้อ “The model reads old customer messages with their date; empty branch fields say so (2026-10-30)” และ “The extraction model gets a loose schema; the limits are applied in code”;docs/STATUS.mdย่อหน้า “2026-10-30: bot answers fixed; the embedding key had expired”;packages/core/src/tools.ts(NOT_IN_BRANCH_SETTINGS) - เรื่อง 7:
docs/decisions.mdหัวข้อ “The bot sends the booking page instead of booking in chat (2026-11-02)”;packages/core/src/page-links.ts(pagesPromisedInText); commiteb097a7;packages/core/src/booking-tools.tsคอมเมนต์เหนือtellCustomerและ commit8961222 - เรื่อง 8:
packages/evals/src/cases.tsเคสbooking-cancel-unverified-no-codeและbooking-cancel-verified-lists-bookings; commit8961222 - บทสรุป:
docs/decisions.mdหัวข้อ “A rule the product depends on does not belong only in a prompt”