ไม่มีตัวเลขที่กุขึ้นแม้แต่ตัวเดียว — เวิร์กโฟลว์ AI ที่รันมา 6 เดือน เปิดเผยทั้งหมดตั้งแต่พรอมต์ โค้ด ไปจนถึงการตรวจสอบ
Design Director ประสบการณ์ 24 ปี นำกฎ 304 ข้อ เครื่องมือ 48 ตัว และระบบอัตโนมัติที่ใช้งานจริงในทีมขนาดกลาง (10–50 คน) มาเสนออย่างที่เป็นจริง
ผู้เขียน อี มินซู (Minsoo Lee) · 2026
เนื้อหาทั้งหมดในเล่มนี้อ้างอิงสถานะ ณ ครึ่งแรกของปี 2026 ค่าบริการ โมเดล และฟีเจอร์ของเครื่องมือ AI เปลี่ยนแปลงอย่างรวดเร็ว ดังนั้นกรุณาตรวจสอบตัวเลขที่เจาะจงและวิธีติดตั้งล่าสุดได้จากหน้าทางการของแต่ละเครื่องมือ
หนังสือเล่มนี้เปิดเผยให้อ่านฟรีด้วยความหวังว่าจะมีผู้อ่านอย่างกว้างขวาง อย่างไรก็ตาม ไม่ว่าจะเป็นต้นฉบับภาษาเกาหลี หรือฉบับแปลภาษาอังกฤษ ภาษาญี่ปุ่น ข้อเท็จจริงที่ว่าผู้เขียนต้นฉบับของหนังสือเล่มนี้คือ อี มินซู (Minsoo Lee) จะต้องคงอยู่ไม่ว่าจะอยู่ที่ใดก็ตาม
https://eremes81.github.io/game-design-ai-practice (GitHub Pages ของผู้เขียน — ฉบับตั้งต้นที่สิทธิ์ในการแก้ไขอยู่ที่ผู้เขียน)สิ่งที่ทำได้อย่างอิสระ การเรียนรู้ส่วนตัว การแบ่งปันและอ้างอิงแบบไม่ใช่เพื่อการค้า การแปลแบบไม่ใช่เพื่อการค้า การนำไปใช้ในกลุ่มศึกษาภายในบริษัท — เพียงแต่ขอให้แสดงชื่อผู้เขียนต้นฉบับและลิงก์ฉบับตั้งต้นไว้ด้วย และหากแก้ไขเนื้อหาหรือนำไปแปลเป็นภาษาอื่น ขอให้ระบุข้อเท็จจริงนั้นไว้ด้วย งานดัดแปลงก็ขอให้แบ่งปันภายใต้เงื่อนไขเดียวกัน
สิ่งที่ต้องขออนุญาตเป็นการเฉพาะ การตีพิมพ์และจำหน่ายเชิงพาณิชย์ การใช้เป็นตำราในคอร์สที่มีค่าเรียน การรวมไว้ในสินค้าหรือบริการของบริษัท และการเผยแพร่ซ้ำโดยลบการแสดงชื่อผู้เขียนต้นฉบับ
ความรู้ที่บรรจุอยู่ในหนังสือเล่มนี้สมบูรณ์ในตัวเองภายในเล่มเดียว และฟรีในตัวมันเอง หากหนังสือเล่มนี้เป็นประโยชน์และคุณอยากให้กำลังใจผู้เขียน การสมทบด้วยการซื้ออีบุ๊กฉบับเป็นทางการหรือชุดเครื่องมือแบบเสียค่าใช้จ่ายจะเป็นที่ขอบคุณยิ่ง
หนังสือเล่มนี้ถูกเขียนขึ้นสามครั้ง
ครั้งแรกไม่ใช่หนังสือ แต่เป็น คู่มือภายในบริษัท ตลอด 6 เดือนที่รันเวิร์กโฟลว์ AI ในบริษัท ผมตรึงการตัดสินใจ เครื่องมือ และขั้นตอนต่าง ๆ ไว้เป็นเอกสาร เพื่อให้ทีมไม่ต้องถามกฎเดียวกันซ้ำสองครั้ง มันไม่ได้สร้างขึ้นเพื่อตีพิมพ์ แต่เป็นเอกสารปฏิบัติงานที่สั่งสมขึ้นเพื่อลดงานซ้ำ ๆ ในแต่ละวัน ความเป็นรูปธรรมของหนังสือเล่มนี้มาจากตรงนั้น — หมายความว่ามันไม่ใช่ตัวอย่างที่แต่งขึ้นเพื่อหนังสือ แต่มีคู่มือที่ทำงานอยู่จริงเป็นรากฐาน
ครั้งที่สองคือ ต้นฉบับแรกที่ย้ายคู่มือนั้นมาเป็นหนังสือ แต่กลับกลายเป็นการพูดทฤษฎีทั่ว ๆ ไปที่ร้อยเรียง "AI ทำสิ่งนี้ได้" ให้ดูน่าเชื่อถือ มีตารางจำนวนมาก มีตัวเลขที่แสดงผลลัพธ์จำนวนมาก และตัวเลขเหล่านั้นส่วนใหญ่มีตัวอักษรเล็ก ๆ กำกับว่า "ตัวเลขที่ปรุงแต่งขึ้น" เมื่ออ่านอีกครั้ง นั่นคือข้อบกพร่องที่ใหญ่ที่สุด เพราะในขณะที่พูดถึงการใช้ AI กลับไม่แสดงหน้าจอจริง และในขณะที่พูดถึงผลลัพธ์ กลับยกตัวเลขที่กุขึ้น เท่ากับว่าระหว่างที่ย้ายความเป็นรูปธรรมของคู่มือมาเป็นหนังสือ กลับกลายเป็นการสูญเสียมันไป
ดังนั้นในครั้งที่สาม ผมจึงเขียนใหม่ทั้งหมด นี่คือเนื้อหาที่คุณถืออยู่ในมือตอนนี้ ผมฟื้นความเป็นรูปธรรมของคู่มือภายในบริษัทกลับมา แต่วางหลักการของหนังสือไว้อย่างเรียบง่าย
ข้อแรก ทุกบทแสดงเซสชันจริงให้ดูจนจบ ทั้งพรอมต์เต็มที่ผมพิมพ์ ผลลัพธ์ดิบที่ AI พ่นออกมา สิ่งที่ผมปฏิเสธจากผลลัพธ์นั้น และวิธีที่ผมสั่งมันใหม่ — ทั้งหมดบรรจุไว้ ผมไม่จบบทด้วยประโยคว่า "AI จัดการให้"
ข้อสอง ตัวเลขเป็นหนึ่งในสามอย่างนี้ มาตรฐานสาธารณะที่ใครก็ตรวจสอบได้ (ค่าโทเค็นของโมเดล แนวทางด้านการเข้าถึง) ค่าคงที่ที่ป้อนอยู่จริงในโค้ดของระบบผม หรือค่าที่ระบุไว้ชัดว่า "นี่คือการประมาณของผู้เขียน" ไม่มีตารางตัวเลขเงินที่ประหยัดได้ที่กุขึ้นแม้แต่ตารางเดียว ผมยกความซื่อตรงเป็นจุดที่ทำให้หนังสือเล่มนี้แตกต่าง
ข้อสาม ผมอ้างอิงระบบจริงที่รันมา 6 เดือนในงานจริงอย่างที่เป็น ตั้งแต่การ์ดการตัดสินใจ (atom) 304 การ์ด เครื่องมือ (skill) 48 ตัว hook ที่ดึงความทรงจำที่เกี่ยวข้องมาให้อัตโนมัติในทุกการป้อนข้อมูล ไปจนถึงโครงสร้างหน่วยความจำที่คนคนเดียวบริหารบริบทการทำงานร่วมกันได้เท่ากับสี่คน ผมไม่ได้เขียนว่า "เครื่องมือบางอย่าง" แบบนามธรรม แต่เขียนชื่อไฟล์ โค้ด และคะแนนไว้อย่างที่เป็น ในตัวอย่างของเนื้อหา ผมเพียงปิดบังชื่อบริษัท ชื่อโปรเจกต์ และชื่อสมาชิกทีมเท่านั้น แต่ไม่ได้ลบความเป็นรูปธรรมของเวิร์กโฟลว์ออก (บริษัทที่อนุญาตให้ตีพิมพ์หนังสือเล่มนี้ ผมได้ระบุชื่อจริงไว้ในกิตติกรรมประกาศ — เพราะได้รับความยินยอมแล้ว)
ผมเป็นนักออกแบบเกม (Game Designer) ประสบการณ์ 24 ปี ผมก้าวเข้าสู่วงการนี้ด้วยงาน QA และการตรวจสอบเกมแบบเล่นคนเดียว หลังจากนั้นก็ใช้เวลาไปกับการสร้าง RPG และ MMORPG รวมถึงสายพันธุ์ย่อยของมัน — ตั้งแต่ตำแหน่งผู้กำกับ MMORPG ที่ให้บริการในหลายสิบประเทศ การพัฒนาช่วงแรกของ AAA MMORPG ขนาด 200 คน ไปจนถึงการให้บริการเกม (Live Ops) ของ MMORPG บนมือถือระดับโลก ผมเข้าร่วมโปรเจกต์อย่าง Ragnarok Online (แร็กนาร็อก), Bless Online และซีรีส์ Mir ในหลากหลายบทบาท ทั้งผู้กำกับ หัวหน้าทีมออกแบบ นักออกแบบระบบ (System Designer) และบางครั้งก็เป็น PM และเคยก่อตั้งบริษัทเกมมือถือเล็ก ๆ ของตัวเองด้วย
พูดตามตรง ผมได้ตำแหน่งผู้กำกับเร็วเกินไป หลังจากนั้นมีช่วงเวลายาวนานที่ผมเป็นสมาชิกทีม — ในฐานะนักออกแบบระบบที่จับชีตข้อมูลและตัวเลขการต่อสู้ด้วยมือตัวเอง ในฐานะนักออกแบบเนื้อหา (Content Designer) ที่ผลิตเควสต์และ NPC ทีละบรรทัด ออกแบบอีเวนต์ และผลิตเนื้อหาใหม่ ๆ เวิร์กโฟลว์จำนวนมากในหนังสือเล่มนี้ถูกสร้างขึ้นที่ตำแหน่งนั้น — ตำแหน่งที่ไม่ใช่คนบริหาร แต่เป็นคนที่ลงมือจนมือเปื้อนเอง — ด้วยความรู้สึกว่า "อยากลดงานซ้ำ ๆ นี้ให้ได้ไม่ทางใดก็ทางหนึ่ง" ตอนนี้ในงานปัจจุบัน ผมเป็น Design Director นำทีมขนาดกลาง (10–50 คน) ของ MMORPG หนึ่งเกม แต่เครื่องมือในหนังสือเล่มนี้ไม่ใช่เครื่องมือบริหารของผู้กำกับ มันออกมาจากมือของผู้ปฏิบัติงาน ดังนั้นระบบในหนังสือเล่มนี้จึงไม่ใช่ทฤษฎี แต่เป็นสภาพแวดล้อมการทำงานที่หมุนอยู่ทุกวัน ผมยังจัดการตัวอย่างของเกมพัซเซิลเล็ก ๆ เกมหนึ่งที่ทำคนเดียวที่บ้านด้วยวิธีเดียวกัน — โดยอ้างอิง git commit และโค้ดจริงของเกมนั้นอย่างที่เป็น
AI ไม่สามารถมาแทนงานของนักออกแบบเกมได้ เพียงแต่มันช่วยให้เราถอนมือจากงานจุกจิก ส่วนจะใช้มือนั้นทำอะไร ก็ยังคงเป็นหน้าที่ของคนอยู่ดี ผมหวังว่าหนังสือเล่มนี้จะเป็นคู่มือปฏิบัติสำหรับการเปลี่ยนผ่านนั้น
หนังสือเล่มนี้ไม่จำเป็นต้องอ่านเรียงตั้งแต่ต้นจนจบ คุณเลือกเส้นทางที่เหมาะกับสถานการณ์ของตัวเองได้เลย หากคุณยังใหม่กับเทอร์มินัลและการติดตั้ง ก่อนอื่นใดให้เปิดที่ 1.0 「ก่อนเริ่มต้น」 — เป็นบทที่ช่วยคลายความกลัวหน้าจอสีดำให้ก่อน
| เส้นทาง | ลำดับ | ผู้อ่านที่เหมาะ |
|---|---|---|
| เส้นทางเริ่มต้น | 1.0 (ติดตั้ง) → ส่วนที่ 1 (เริ่มต้น) → ส่วนที่ 2 (สถาปัตยกรรมข้อมูล) → สาขาของตัวเอง 1 สาขา | นักออกแบบเกมที่เพิ่งเริ่มใช้เครื่องมือ AI |
| เส้นทางครบทุกส่วน | ส่วนที่ 1·2 → แยกตามสาขา (ส่วนที่ 3–15) → กระบวนการ (ส่วนที่ 16–19) → การดำเนินงาน (ส่วนที่ 20–24) | หัวหน้าทีมที่ออกแบบการนำไปใช้ในระดับทีม |
| เส้นทางอินดี้·คนเดียว | 1.0 (ติดตั้ง) → ส่วนที่ 1·2 → ส่วนที่ 23 (การพัฒนาเกมส่วนตัว) → 「ฉบับย่อสำหรับคนเดียว」 ของแต่ละบท | นักพัฒนาที่ทำคนเดียว·เป็นงานอดิเรกโดยไม่มีทีม |
| เส้นทางงานทั่วไป | ส่วนที่ 1·2 → ส่วนที่ 17 (บันทึกการประชุม) → ส่วนที่ 16 (การทำงานร่วมกัน) → ส่วนที่ 18 (การตัดสินใจ) → ส่วนที่ 21·22 (การพัฒนาตนเอง·ธรรมาภิบาล) | นักวางแผน·PM นอกวงการเกม·พนักงานออฟฟิศทั่วไป |
| เส้นทางแก้ปัญหา | ดัชนีภาคผนวก → ย้อนกลับไปยังบทที่เกี่ยวข้อง | ผู้อ่านที่มีปัญหาต้องแก้ในทันที |
ท้ายแต่ละบทมี 「ลองทำดู」 เป้าหมายไม่ใช่บทที่อ่านแล้วปิดทิ้ง แต่คือการทำให้คุณขยับมือลงมือทำในสภาพแวดล้อมของตัวเองได้แม้เพียงขั้นเดียวในวันนี้
ขอเพิ่มอีกคำสำหรับผู้ที่ทำงานนอกวงการเกม เวิร์กโฟลว์จำนวนมากในหนังสือเล่มนี้ — การเปลี่ยนบันทึกการประชุมให้เป็นการตัดสินใจ การติดตามผลกระทบของการตัดสินใจ ด่านตรวจสอบ (verification gate) การจัดการต้นทุน ลิขสิทธิ์·จริยธรรม — ทำงานได้อย่างที่เป็นโดยไม่เกี่ยวกับเกม คุณจะอ่านโดยแทนที่ "การออกแบบเกม" ด้วยงานของตัวเองก็ได้ กล่อง 「การประยุกต์นอกเกม」 ในแต่ละบทคือสะพานเชื่อมนั้น และหากไม่มีเวลา แม้จะตามเฉพาะคอร์สเร่งรัด 90 นาที (17.1 → 16.2 → 22.1 → 21.1) คุณก็สัมผัสโครงสร้างหลักด้วยมือตัวเองได้
ขอแยกความหมายไว้สักหนึ่งจุด ในหนังสือเล่มนี้ "หนึ่งคน" ใช้ในสองความหมาย หนึ่งคือ ผู้กำกับคนเดียว — หัวหน้าทีมที่แบกบริบทการทำงานร่วมกันของหลายคนไว้คนเดียว — อีกหนึ่งคือ นักพัฒนาคนเดียว·งานอดิเรกที่ทำเกมด้วยตัวคนเดียว 「ฉบับย่อสำหรับคนเดียว」 ท้ายแต่ละบทมีไว้สำหรับความหมายหลัง โดยเขียนเส้นทางที่นำเฉพาะแก่นของบทนั้นไปใช้ได้โดยไม่มีทั้งทีมและโฟลเดอร์บริษัท
หนังสือเล่มนี้จะอ่านรวดเดียวจบทั้งเล่มก็ได้ หรือจะแบ่งเป็นสองสาย — ส่วนที่ 1–15 'รากฐาน·สาขา' และส่วนที่ 16–24 'กระบวนการ·การดำเนินงาน' — แล้วอ่านจากฝั่งที่ต้องการก่อนก็ได้ และโค้ดในเนื้อหาส่วนใหญ่รันได้ทันทีอย่างที่เป็นด้วยไลบรารีมาตรฐานของ Python โดยไม่ต้องพึ่งสิ่งภายนอก มีเพียงเครื่องมือบางตัว เช่น กราฟความสัมพันธ์ ที่ต้องใช้แพ็กเกจนอกมาตรฐาน (networkx, PyYAML) และในจุดเหล่านั้นผมเขียนคำสั่งติดตั้งหนึ่งบรรทัด (pip install …) ไว้ข้างโค้ดด้วย ยกเว้นกรณีนั้น คุณคัดลอกบล็อกโค้ดไปรันตรวจสอบได้ทันทีโดยไม่ต้องดาวน์โหลดอะไรเพิ่ม
หากติดขัดที่คำศัพท์ ก็อย่าหยุดอยู่ตรงนั้น ให้ข้ามไปก่อน ความไม่คุ้นเคยกับเทอร์มินัลสีดำไม่ใช่ข้อบกพร่องของเครื่องมือ แต่เป็นเรื่องของความเคยชิน และระยะห่างนั้นเราจะลดลงไปด้วยกันในส่วน 1.0 และส่วนที่ 1
สุดท้ายนี้ ขอกระซิบวิธีใช้หนังสือเล่มนี้ที่เร็วที่สุดให้สักหนึ่งวิธี นั่นคือการให้เครื่องมือ AI อย่าง Claude Code อ่านหนังสือเล่มนี้ทั้งเล่ม
หนังสือเล่มนี้ไม่ได้เขียนไว้ให้คนอ่านอย่างเดียว พรอมต์เต็ม โค้ด และขั้นตอนการตรวจสอบในแต่ละบท เขียนไว้ในรูปแบบที่ AI เข้าใจและทำซ้ำได้อย่างที่เป็น ดังนั้นในโฟลเดอร์โปรเจกต์ของคุณ คุณส่งหนังสือเล่มนี้ให้ AI — จะเป็น PDF หรือข้อความก็ได้ — แล้วขอแบบนี้ได้ "อ่านรูปแบบการตรวจสอบความสอดคล้องในหนังสือเล่มนี้ แล้วสร้างเครื่องมือตรวจสอบที่เหมาะกับชีตข้อมูลของเราให้หน่อย" จากนั้น AI จะติดตั้งเวิร์กโฟลว์ของบทนั้นให้เข้ากับสภาพแวดล้อมของคุณ ทั้งเส้นทางที่คนค่อย ๆ ทำตามทีละบทด้วยมือ และเส้นทางที่ให้หนังสือทั้งเล่มแก่ AI แล้วสร้างไปด้วยกัน — ทั้งสองเส้นทางเปิดอยู่
แต่มีอยู่หนึ่งสิ่งที่ไม่เปลี่ยน นั่นคือ จะรับสิ่งใดและปฏิเสธสิ่งใด การตัดสินใจสุดท้ายนั้น — อย่างที่หนังสือเล่มนี้พูดซ้ำตั้งแต่ต้นจนจบ — ยังคงเป็นหน้าที่ของพวกคุณ แม้จะให้ AI อ่านหนังสือแล้วติดตั้งระบบ ตำแหน่งที่ตรวจสอบตัวเลือกที่ระบบนั้นพ่นออกมา ก็ยังคงเป็นที่ของคน แม้แต่วิธีใช้ที่ง่ายที่สุดก็ทำงานอยู่บนหลักการนั้น
ในตารางใด ๆ ของหนังสือเล่มนี้ ไม่มี "ตัวเลขที่พองขึ้นเพื่อโน้มน้าวผู้อ่าน" เลย แทนที่จะพูดเกินจริงถึงผลลัพธ์ ผมแสดง โครงสร้าง ที่ทำให้ผลลัพธ์นั้นเกิดขึ้น หากคุณนำโครงสร้างเดียวกันไปใช้ในโปรเจกต์ของตัวเอง ตัวเลขของคุณก็จะเป็นสิ่งที่คุณวัดเอง นั่นคือความช่วยเหลือที่ซื่อตรงที่สุดที่หนังสือเล่มนี้มอบให้ได้
ในเนื้อหาของหนังสือเล่มนี้ ผลลัพธ์ของบันทึกเซสชันจริง (worked transcript) มักมีคำว่า 'การเรียบเรียงใหม่ (reconstruction)' กำกับอยู่ — เช่น 「ขั้นที่ 3 — ผลลัพธ์ของ Claude (การเรียบเรียงใหม่)」 ผมขอให้คำสัญญาให้ชัดเจนสักครั้งเดียวว่า คำนี้หมายถึงการรักษาสิ่งใดไว้และแตะต้องสิ่งใด เพราะนี่คือจุดที่หนังสือซึ่งชูความซื่อตรงเป็นหัวเรื่องไม่ควรปล่อยให้คลุมเครือที่สุด
'การเรียบเรียงใหม่' ไม่ได้หมายความว่ากุขึ้น แต่หมายความว่าได้แก้ไขเรียบเรียงเซสชันจริง เส้นแบ่งเป็นดังนี้
| สิ่งที่รักษาไว้อย่างที่เป็น | สิ่งที่แก้ไข |
|---|---|
| พรอมต์ป้อนเข้าเต็มที่ผมพิมพ์ — ในรูปแบบที่คัดลอกไปใช้ได้ทันที | ชื่อเฉพาะของบริษัท·โปรเจกต์·NPC·สมาชิกทีม → นามแฝงสำหรับหนังสือ (ปกป้อง IP) |
| โครงสร้างและความล้มเหลว ของผลลัพธ์ที่ AI พ่นออกมา — ตัวเลือกที่หลุดเป้า ส่วนที่แอบฝ่าฝืนกฎ การโต้กลับไปมาที่ผมปฏิเสธและสั่งใหม่ | ความยาว — กิ่งก้านที่ไม่เข้าเนื้อหาหลักถูกย่อด้วย 「ข้อความที่ตัดมา」 |
| โค้ด·ค่าคงที่·ค่าตรวจสอบ — อย่างที่เป็นเพื่อให้ทำซ้ำได้ด้วยการรันภายนอก | การจัดหน้าให้เข้ากับหน้ากระดาษ เช่น การขึ้นบรรทัด·ช่องว่าง |
พูดอีกอย่าง ในผลลัพธ์ที่เรียบเรียงใหม่ ผมไม่ได้เติมตัวเลขที่พองผลลัพธ์หรือเติมความสำเร็จที่ไม่เคยมี ผมเพียงทำให้เป็นนามแฝง ตัดข้อความ และเกลาให้เข้ากับหน้ากระดาษเท่านั้น ไม่มีที่ใดที่ผมแก้ผลลัพธ์ที่ล้มเหลวให้กลายเป็นความสำเร็จ — ตรงกันข้าม ผมจงใจคงความล้มเหลวไว้บางส่วน เพราะนั่นคือจุดที่แสดงให้เห็นว่าคนปฏิเสธสิ่งใด (ในทางตรงข้าม จุดที่ระบุว่าเป็นผลการรันโค้ดหรือล็อกของระบบว่า 'วัดจริง'·'อ้างอิงอย่างที่เป็น' คือสิ่งที่ย้ายมาโดยไม่มีการแก้ไข)
หมายเหตุของผู้แปล: ฉบับภาษาไทยนี้แปลด้วยปัญญาประดิษฐ์ (AI) เป็นร่างแรก แล้วผ่านการตรวจสอบและเรียบเรียงโดยมนุษย์ บทสนทนาจริง (worked transcript) ทั้งหมดในฉบับนี้เป็นการแปลจากเซสชันภาษาเกาหลีต้นฉบับ ไม่ใช่การรันใหม่เป็นภาษาไทย พรอมต์และผลลัพธ์ภายในบล็อกโค้ดก็ได้รับการแปลเพื่อให้อ่านง่ายเช่นกัน ต้นฉบับภาษาเกาหลี — สิ่งที่หนังสือเล่มนี้สัญญาว่าจะไม่แก้ไข — ได้รับการเก็บรักษาไว้ตามเดิมในฉบับภาษาเกาหลี (ที่ repository ต้นฉบับบน GitHub) ส่วนไวยากรณ์ของโค้ด ตัวระบุ ตัวเลข และค่าตรวจสอบ (verification values) ไม่ได้ถูกแก้ไขใด ๆ การแปลงสกุลเงินเป็นค่าโดยประมาณ โดยอิงอัตราราว 1,500 วอนต่อ 1 ดอลลาร์สหรัฐ ณ กลางปี 2026
หัวข้อ 1.1 คือ "การพบกันครั้งแรก" เป็นช่วงที่คุณนั่งอยู่หน้าเคอร์เซอร์ที่กะพริบและลองพิมพ์อะไรบางอย่าง แต่ก่อนจะนั่งลงตรงนั้นได้ มีสิ่งที่ต้องเตรียมพร้อมก่อน เครื่องมือต้องติดตั้งไว้แล้ว ต้องล็อกอินไว้แล้ว ต้องพอเข้าใจคร่าว ๆ ว่าค่าบริการคิดอย่างไร และต้องพิมพ์ตัวอักษรสักสองสามตัวบนหน้าจอดำได้ บทนี้จึงอยู่ก่อนหน้า 1.1 หนึ่งขั้น
หนังสือสำหรับผู้เริ่มต้นจำนวนมากข้ามขั้นตอนนี้ไป เขียนแค่บรรทัดเดียวว่า "เปิดเทอร์มินัล" แล้วผ่านเลยไป แต่ผู้เริ่มต้นกลับติดอยู่ที่บรรทัดนั้นเอง เทอร์มินัลอยู่ตรงไหน ต้องติดตั้งอะไร ถ้าติดตั้งแล้วเจอตัวอักษรสีแดงต้องทำอย่างไร — คนที่หยุดอยู่ที่บรรทัดแรกไปไม่ถึง 1.1 เป้าหมายของบทนี้มีเพียงข้อเดียว คือทำให้คุณไม่ติดอยู่ที่บรรทัดแรก
บทนี้แบ่งออกเป็นห้าส่วน ได้แก่ การติดตั้ง บัญชีและการล็อกอิน แนวคิดเรื่องแพ็กเกจค่าบริการ ชุดเอาตัวรอดบนเทอร์มินัล และเช็กลิสต์ "รันครั้งแรกใน 5 นาที" หากทำตามไปทีละขั้น คุณก็จะพร้อมนั่งลงตรงที่หัวข้อ 1.1 รออยู่
flowchart LR
A["1.0 ขั้นเตรียมการ
(บทนี้)"] --> B["1.1 การพบกันครั้งแรก
(นั่งลงหน้าเคอร์เซอร์)"]
A1["① การติดตั้ง"] --> A2["② บัญชี·ล็อกอิน"]
A2 --> A3["③ แนวคิดแพ็กเกจค่าบริการ"]
A3 --> A4["④ ชุดเอาตัวรอดบนเทอร์มินัล"]
A4 --> A5["⑤ รันครั้งแรกใน 5 นาที"]
A5 --> B
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class A1,A2,A3,A4,A5 human
class B pass
หลักการของการติดตั้งคือทำตามคำแนะนำอย่างเป็นทางการ เครื่องมือเปลี่ยนแปลงบ่อย และไฟล์ติดตั้งที่ได้มาจากแหล่งที่ไม่เป็นทางการนั้นอันตราย ด้วยเหตุนี้หนังสือเล่มนี้จึงไม่ได้ใส่ลิงก์ดาวน์โหลดไว้ แต่จะแนะนำวิธีค้นหาเส้นทางอย่างเป็นทางการแทน หากพิมพ์ "Claude Code เอกสารทางการ" หรือ "Claude Code install" ในช่องค้นหา หน้าเอกสารทางการของบริษัท Anthropic จะปรากฏขึ้นมาเป็นอันดับแรก การใช้คำสั่งติดตั้งจากหน้านั้นตามที่ระบุไว้คือวิธีที่ปลอดภัยที่สุด
ควรเข้าใจภาพรวมไว้ด้วย Claude Code (หนังสือเล่มนี้ใช้การสะกดแบบอักษรละตินเป็นมาตรฐาน) เป็นเครื่องมือที่ทำงานบนเทอร์มินัล และโดยทั่วไปติดตั้งด้วยคำสั่งบรรทัดเดียว ขั้นตอนจะต่างกันเล็กน้อยตามแต่ละ OS
| OS | สิ่งที่ต้องเตรียม | ขั้นตอนการติดตั้ง (แนวคิด) |
|---|---|---|
| Windows | PowerShell (มีมาให้ในตัว) | วางคำสั่งติดตั้งหนึ่งบรรทัดจากเอกสารทางการลงใน PowerShell |
| macOS | เทอร์มินัล (มีมาให้ในตัว) | วางคำสั่งติดตั้งหนึ่งบรรทัดจากเอกสารทางการลงในเทอร์มินัล |
| Linux | เทอร์มินัล | วางคำสั่งติดตั้งหนึ่งบรรทัดจากเอกสารทางการลงในเทอร์มินัล |
ทั้งสาม OS มีขั้นตอนเหมือนกัน คือ "เปิดเทอร์มินัล → วางคำสั่งหนึ่งบรรทัดจากเอกสารทางการ → กด Enter" ไม่จำเป็นต้องท่องจำคำสั่ง การคัดลอกจากเอกสารทางการแล้ววางคือวิธีมาตรฐาน
แม้ระหว่างติดตั้งจะมีตัวอักษรสีแดง (ข้อผิดพลาด) ปรากฏขึ้นก็ไม่ต้องตกใจ ข้อผิดพลาดในการติดตั้งที่ผู้เริ่มต้นพบมักเป็นหนึ่งในสองอย่าง คือปัญหาเรื่องสิทธิ์ หรือกรณีที่ไม่มีเครื่องมือพื้นฐาน (เช่น รันไทม์อย่าง Node.js) หากมีตัวอักษรสีแดงปรากฏขึ้น ให้คัดลอกทั้งประโยคนั้นตามเดิมเพื่อไปค้นหา หรือถาม AI ก็มักจะแก้ได้เกือบทุกครั้ง ข้อความข้อผิดพลาดไม่ใช่ศัตรู แต่เป็นเบาะแส
วิธีตรวจสอบว่าติดตั้งสำเร็จหรือไม่: พิมพ์
claude --versionในเทอร์มินัลแล้วกด Enter ถ้าหมายเลขเวอร์ชันปรากฏขึ้นมาหนึ่งบรรทัด แสดงว่าติดตั้งสำเร็จ หากมีข้อความทำนองว่า "ไม่พบคำสั่ง" ปรากฏขึ้น แสดงว่ายังติดตั้งไม่เสร็จ หรือเป็นกรณีที่ต้องเปิดเทอร์มินัลใหม่ ลองปิดเทอร์มินัลให้สนิทแล้วเปิดขึ้นมาใหม่ จากนั้นตรวจสอบอีกครั้ง
ติดตั้งเสร็จแล้วไม่ได้แปลว่าใช้งานได้ทันที Claude Code เป็นเครื่องมือที่ยืมโมเดล AI ของ Anthropic มาใช้ จึงต้องมีขั้นตอนล็อกอินเพื่อยืนยันว่าใครเป็นผู้ใช้
ขั้นตอนนั้นเรียบง่าย เมื่อรัน claude ในเทอร์มินัลเป็นครั้งแรก คำแนะนำการล็อกอินจะปรากฏขึ้น โดยทั่วไปเว็บเบราว์เซอร์จะเปิดขึ้นมาเองโดยอัตโนมัติ จากนั้นล็อกอินด้วยบัญชี Anthropic ตรงนั้น (หากไม่มี ก็สามารถสร้างใหม่ได้จากหน้าจอนั้น) เมื่อล็อกอินเสร็จ เบราว์เซอร์จะแสดงคำแนะนำทำนองว่า "ตอนนี้กลับไปที่เทอร์มินัลได้แล้ว" และฝั่งเทอร์มินัลเองก็จะแสดงเครื่องหมายว่าเสร็จสมบูรณ์
ตรงนี้มีอยู่สองจุดที่ผู้เริ่มต้นมักจะติด
จุดแรก คือกรณีที่เบราว์เซอร์ไม่เปิดขึ้นมาเองโดยอัตโนมัติ ในกรณีนี้จะมีที่อยู่ยาว ๆ (URL) ปรากฏขึ้นหนึ่งบรรทัดในเทอร์มินัล ให้คัดลอกที่อยู่นั้นไปวางในช่องที่อยู่ของเบราว์เซอร์แล้วเข้าไป ไม่ได้ตันแต่อย่างใด เพียงแค่ต้องทำด้วยตัวเองเพิ่มอีกขั้นเดียวเท่านั้น
จุดที่สอง คือกรณีที่สับสนเรื่องประเภทบัญชี การเชื่อมโยงระหว่างบัญชีที่เคยใช้กับเว็บแชต (Claude.ai) กับบัญชีและค่าบริการของ Claude Code นั้นอาจมีนโยบายต่างกันไปในแต่ละช่วงเวลา การทำตามคำแนะนำบนหน้าจอล็อกอินและเอกสารทางการคือวิธีที่ถูกต้องที่สุด หากทำตามที่หน้าจอการรันครั้งแรกบอก ส่วนใหญ่ก็จะล็อกอินได้โดยไม่มีปัญหา
ล็อกอินครั้งเดียวแล้วจะคงอยู่บนเครื่องนั้น ไม่จำเป็นต้องทำใหม่ทุกครั้ง
สิ่งที่ผู้เริ่มต้นกังวลมากที่สุดคือ "จะเสียเงินเท่าไหร่กันแน่" มีความกลัวคลุมเครืออยู่ว่าทุกครั้งที่พิมพ์ตัวอักษรจะมีค่าบริการเพิ่มขึ้นหรือเปล่า หากจับภาพรวมได้ก่อน ความกังวลนี้ก็จะลดลง รูปแบบค่าบริการแบ่งใหญ่ ๆ ออกเป็นสองแนวทาง
| รูปแบบ | ลักษณะการคิดเงิน | การเปรียบเทียบ | เหมาะกับใคร |
|---|---|---|---|
| สมาชิกรายเดือนแบบเหมาจ่าย | จำนวนเงินคงที่ต่อเดือน | แพ็กเกจเหมาจ่ายค่าโทรศัพท์ | ผู้เริ่มต้น·การใช้งานทั่วไป |
| API แบบจ่ายตามการใช้ | จ่ายตามที่ใช้ (หน่วยเป็นโทเค็น) | มิเตอร์ไฟฟ้า | งานปริมาณมาก·ระบบอัตโนมัติ·การเชื่อมต่อสำหรับนักพัฒนา |
สมาชิกรายเดือนแบบเหมาจ่าย คือรูปแบบที่จ่ายเงินตามจำนวนที่กำหนดไว้เป็นรายเดือนแล้วใช้ได้จนถึงเพดานที่กำหนด คล้ายกับแพ็กเกจเหมาจ่ายค่ามือถือ เพราะจ่ายเท่ากันทุกเดือนจึงคาดการณ์ง่าย และไม่ต้องคอยกังวลว่า "พิมพ์หนึ่งบรรทัดแล้วเสียเท่าไหร่" ด้วยเหตุนี้ผู้เริ่มต้นจึงมักจะเริ่มต้นด้วยสมาชิกรายเดือนแบบเหมาจ่ายเพื่อความสบายใจ (การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) — เนื่องจากองค์ประกอบของแพ็กเกจและเพดานที่แน่นอนเปลี่ยนแปลงไปตามแต่ละช่วงเวลา จึงควรตรวจสอบที่หน้าค่าบริการอย่างเป็นทางการ) หากใช้เกินเพดาน ก็รอจนถึงรอบบิลถัดไป หรืออัปเกรดไปเป็นแพ็กเกจระดับสูงกว่า
API แบบจ่ายตามการใช้ คือรูปแบบที่คิดเงินตามสัดส่วนของปริมาณที่ใช้จริง (โทเค็น) เหมือนมิเตอร์ไฟฟ้าที่เรียกเก็บตามที่ใช้ไป เหมาะกับการประมวลผลปริมาณมาก ระบบอัตโนมัติ (pipeline) หรือกรณีที่เชื่อมต่อกับโปรแกรมอื่น หากใช้อย่างพิถีพิถันก็มีประสิทธิภาพ แต่ในขั้นเริ่มต้น การคาดการณ์ค่าใช้จ่ายอาจทำได้ยากจนกว่าจะจับทางปริมาณการใช้ได้
โทเค็นคืออะไรและทำไมจึงคิดเงินด้วยมัน จะกล่าวถึงอย่างละเอียดในหัวข้อ 1.2 (โมเดล AI·โทเค็น·ฮาร์เนส) ตรงนี้ขอให้จำไว้เพียงข้อเดียว ผู้เริ่มต้นมักจะเริ่มต้นด้วยสมาชิกรายเดือนแบบเหมาจ่าย เพราะจำนวนเงินคงที่ทุกเดือน จึงฝึกฝนได้โดยไม่ต้องกลัวว่า "ใช้ไปแล้วจะโดนบิลก้อนโต" ชื่อแพ็กเกจ ราคา และเพดานเปลี่ยนแปลงบ่อย หนังสือเล่มนี้จึงไม่ได้ใส่ตัวเลขเฉพาะเจาะจงไว้ เนื้อหาของหนังสือเล่มนี้เขียนขึ้นโดยอ้างอิงช่วงกลางปี 2026 และแพ็กเกจค่าบริการ โมเดล รวมถึงฟีเจอร์ต่าง ๆ ก็ยังคงเปลี่ยนแปลงต่อไปหลังจากนั้น การตรวจสอบค่าปัจจุบันที่หน้าค่าบริการอย่างเป็นทางการคือวิธีที่ถูกต้องที่สุด
สรุปหนึ่งบรรทัด: ความกลัวว่าทุกครั้งที่ใช้จะเสียเงินหรือเปล่า → ถ้าเป็นสมาชิกรายเดือนแบบเหมาจ่ายก็คงที่ทุกเดือน ผู้เริ่มต้นเริ่มด้วยแบบเหมาจ่ายจะสบายใจกว่า
ทีนี้มาถึงกำแพงที่ใหญ่ที่สุด นั่นคือหน้าจอดำ เหตุผลที่หัวข้อ 1.1 เริ่มต้นด้วย "ลังเลอยู่หน้าเคอร์เซอร์ที่กะพริบ" ก็อยู่ตรงนี้ สำหรับมือที่ทำงานกับ GUI มา 24 ปี เทอร์มินัลเป็นสิ่งแปลกหน้า แต่คำสั่งที่จำเป็นเพื่อไม่ให้ติดอยู่ที่บรรทัดแรกนั้นมีไม่มากนัก หกคำสั่งด้านล่างนี้ก็เพียงพอแล้ว
| คำสั่ง | วิธีอ่าน | หน้าที่ | การเปรียบเทียบ |
|---|---|---|---|
pwd |
พีดับเบิลยูดี | แสดงว่าตอนนี้ฉันอยู่ในโฟลเดอร์ไหน | "ตรงนี้คือที่ไหน?" |
ls |
แอลเอส | รายการสิ่งที่อยู่ในโฟลเดอร์ตอนนี้ | เปิดหน้าต่างโฟลเดอร์ดู |
cd ชื่อโฟลเดอร์ |
ซีดี | เข้าไปในโฟลเดอร์นั้น | ดับเบิลคลิกโฟลเดอร์ |
cd .. |
ซีดีจุดจุด | ออกขึ้นไปยังโฟลเดอร์ระดับบนหนึ่งขั้น | ย้อนกลับ |
Enter |
เอนเทอร์ | รันคำสั่งที่พิมพ์ไป | ปุ่มยืนยัน |
Ctrl + C |
คอนโทรลซี | หยุดสิ่งที่กำลังทำงานอยู่ตอนนี้ | ปุ่มหยุด |
(บน Windows PowerShell ก็ใช้ ls·cd·pwd ได้ตามนี้ macOS·Linux ก็เหมือนกัน ดังนั้นหกคำสั่งนี้จึงใช้ได้ไม่ว่าจะ OS ใด)
หากดูสิ่งที่หกคำสั่งนี้ทำเป็นภาพ ก็จะเป็นแบบนี้ การเคลื่อนที่บนเทอร์มินัลสุดท้ายแล้วก็คือการเข้า ๆ ออก ๆ ในและนอกโฟลเดอร์ ซึ่งเป็นการกระทำแบบเดียวกับการดับเบิลคลิกโฟลเดอร์หรือการย้อนกลับใน GUI
flowchart TD
Q["pwd
ตรงนี้คือที่ไหน?"] --> L["ls
ตรงนี้มีอะไรบ้าง?"]
L --> D{"มีโฟลเดอร์
ที่จะเข้าไปไหม?"}
D -- "ใช่" --> IN["cd ชื่อโฟลเดอร์
เข้าไป"]
D -- "ไม่ ขึ้นข้างบน" --> UP["cd ..
ออกมา"]
IN --> L
UP --> L
RUN["หลังจากพิมพ์คำสั่งแล้ว"] --> ENT["Enter
รัน"]
STUCK["เมื่อรู้สึกว่ามีอะไรค้างอยู่"] --> STOP["Ctrl + C
หยุดแล้วดึงเคอร์เซอร์กลับมา"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
class Q,L,IN,UP,ENT,STOP code
class D human
เหตุผลที่แท้จริงที่หน้าจอดำน่ากลัวคือความรู้สึกว่า "ถ้าพิมพ์ผิดแล้วมันจะพังเสียหาย" แต่ในหกคำสั่งข้างต้นไม่มีคำสั่งใดที่ทำให้อะไรพังเสียหายได้ pwd·ls·cd แค่ดูหรือย้ายที่เท่านั้น ไม่ได้ลบหรือเปลี่ยนแปลงไฟล์ Enter คือการรัน Ctrl + C ก็เป็นเพียงการหยุด ดังนั้นหกคำสั่งนี้พิมพ์ได้อย่างสบายใจเมื่อไหร่ก็ได้
มีบางครั้งที่หน้าจอดูเหมือนค้างอยู่ คือเมื่อพิมพ์คำสั่งไปแล้วไม่มีอะไรตอบสนองอยู่นาน หรือเคอร์เซอร์กะพริบอยู่ที่อีกบรรทัดหนึ่งราวกับกำลังรออะไรเพิ่มเติม ในกรณีเช่นนั้น หากกด Ctrl + C หนึ่งครั้ง ส่วนใหญ่ก็จะกลับมาที่เคอร์เซอร์เดิม เพียงแค่รู้ว่ามี "ปุ่มหยุด" นี้อยู่ หน้าจอดำก็ดูน่ากลัวน้อยลงมาก ถ้าติดก็ใช้ Ctrl + C ออกมาแล้วเริ่มใหม่ก็ได้
สุดท้าย เมื่อตัวอักษรที่พิมพ์ไปกองพะเนินจนตาลาย ก็สามารถล้างหน้าจอได้ ทั้ง Windows PowerShell·macOS·Linux ล้างด้วยคำสั่ง clear ทั้งหมด แม้จะล้างไปแล้ว สิ่งที่ทำไปก็ไม่ได้หายไป เพียงแต่ตัวอักษรที่มองเห็นถูกจัดให้เรียบร้อยเท่านั้น
หากมาถึงตรงนี้ การเตรียมการก็เสร็จสิ้นแล้ว หากผ่านห้าช่องด้านล่างนี้ได้ภายใน 5 นาที ก็แสดงว่าคุณมีคุณสมบัติพอจะนั่งลงตรงที่หัวข้อ 1.1 หากติดแม้แต่ช่องเดียว ก็ย้อนกลับไปยังหัวข้อนั้น (1.0.1\~1.0.4) ได้
flowchart LR
C1["① เทอร์มินัล
เปิดขึ้นมา"] --> C2["② claude --version
เวอร์ชันปรากฏ"]
C2 --> C3["③ รัน claude →
ล็อกอินเสร็จ"]
C3 --> C4["④ pwd·ls
เห็นโฟลเดอร์ของฉัน"]
C4 --> C5["⑤ Ctrl+C
ออกมาได้"]
C5 --> OK["✅ ไปสู่ 1.1"]
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class C1,C2,C3,C4,C5 human
class OK pass
claude --version แล้วหมายเลขเวอร์ชันปรากฏขึ้นมาหนึ่งบรรทัด (ยืนยันการติดตั้ง)claude แล้วล็อกอินไว้แล้ว (หรือล็อกอินเสร็จตามคำแนะนำ)pwd และดูเนื้อหาในโฟลเดอร์ด้วย ls ได้Ctrl + Cหากเติมครบทั้งห้าช่อง หน้าจอดำก็ไม่ใช่กำแพงลึกลับอีกต่อไป เครื่องมือติดตั้งไว้แล้ว ล็อกอินไว้แล้ว รู้ภาพรวมของรูปแบบค่าบริการ และรู้วิธีเคลื่อนที่กับหยุดภายในหน้าจอ หัวข้อ 1.1 เริ่มต้นบนพื้นฐานของการเตรียมการนี้ ก็ไปยังที่ตรงนั้น ตรงที่นั่งลงหน้าเคอร์เซอร์ที่กะพริบแล้วลองพิมพ์ "สรุปให้หน่อยว่าในโฟลเดอร์นี้มีอะไรบ้าง" เป็นครั้งแรกได้เลย
ส่วนต้นของหนังสือเล่มนี้ (ส่วนที่ 1·2) สามารถทำตามได้ด้วยพรอมต์ภาษาธรรมชาติเพียงอย่างเดียว เพียงแต่บางบทตั้งแต่ส่วนที่ 4 เป็นต้นไปจะรันสคริปต์ Python เล็ก ๆ ด้วยตัวเอง (เช่น pip install pyyaml, pip install pyvis) ถึงจะเพิ่งเริ่มใช้ Python ก็ไม่เป็นไร มีอยู่สองเส้นทาง
เส้นทางแรก เส้นทางติดตั้งเอง ดาวน์โหลด Python จาก python.org มาติดตั้ง (อย่าลืมเลือก "Add to PATH" ในหน้าจอติดตั้ง) แล้วตรวจสอบด้วย python --version ในเทอร์มินัล pip เป็นเครื่องมือติดตั้งแพ็กเกจที่ติดตั้งมาพร้อมกับ Python ดังนั้นจึงรับแพ็กเกจที่ต้องการได้ในบรรทัดเดียวอย่าง pip install pyyaml
เส้นทางที่สอง เส้นทางมอบหมายให้ AI (แนะนำ) เส้นทางที่ง่ายกว่าคือสั่งให้ AI สร้างสภาพแวดล้อมให้เอง เพียงร้องขอในเทอร์มินัลแบบนี้
ตรวจสอบว่ามี Python ติดตั้งอยู่หรือไม่ ถ้าไม่มี ช่วยบอกวิธีติดตั้งที่เหมาะกับ OS ของฉัน
และช่วยให้คำสั่งติดตั้งแพ็กเกจ pyyaml ที่จำเป็นในบทนี้มาเป็นหนึ่งบรรทัด
AI จะตรวจสภาพแวดล้อมของคุณและสร้างคำสั่งติดตั้งให้ หากติด ก็วางข้อความข้อผิดพลาดตรงนั้นตามที่เป็นแล้วถามว่า "ข้อผิดพลาดนี้แก้อย่างไร" ได้เลย ในทุกบทที่ต้องรันเครื่องมือ แพตเทิร์นนี้เพียงอย่างเดียวก็เพียงพอ ในขั้นที่ Python·pip เป็นภาระ "ฉบับย่อสำหรับคนเดียว" ของบทนั้นจะแนะนำเส้นทางที่เบากว่าซึ่งไปได้โดยไม่ต้องใช้โค้ด
setup 1. เปิดเทอร์มินัลของ OS ที่คุณใช้อยู่ (Windows: PowerShell, macOS: เทอร์มินัล) 2. ค้นหา "Claude Code เอกสารทางการ" แล้วเปิดหน้าคำแนะนำการติดตั้งอย่างเป็นทางการค้างไว้ 3. ตั้งเวลา 5 นาที — เป้าหมายคือการผ่านทั้งห้าช่องของเช็กลิสต์ 1.0.5
prompt (ลองพิมพ์ทีละบรรทัดตามลำดับ นี่เป็นคำสั่ง ไม่ใช่คำถามภาษาธรรมชาติ)
① claude --version # ถ้าเวอร์ชันปรากฏ แสดงว่าติดตั้งสำเร็จ
② pwd # ตอนนี้ฉันอยู่ในโฟลเดอร์ไหน
③ ls # ในโฟลเดอร์นี้มีอะไรบ้าง
④ cd .. # ออกขึ้นไปข้างบนหนึ่งขั้น (แล้ว ls อีกครั้ง)
⑤ claude # รัน Claude Code (ถ้าคำแนะนำการล็อกอินปรากฏ ก็ทำตาม)
verify
- หากในข้อ ① หมายเลขเวอร์ชันปรากฏขึ้นมาหนึ่งบรรทัด แสดงว่าการติดตั้งเสร็จสิ้นแล้ว หากปรากฏข้อความ "ไม่พบคำสั่ง" ให้ปิดเทอร์มินัลแล้วเปิดใหม่ จากนั้นลองอีกครั้ง
- ระหว่างที่ดูและย้ายโฟลเดอร์ด้วยข้อ ②·③·④ ให้ลองยืนยันด้วยตัวเองว่าไม่มีอะไรพังเสียหาย ทั้งสามนี้เป็นคำสั่งที่ปลอดภัยซึ่งแค่ดูและย้ายที่เท่านั้น
- หากระหว่างรันข้อ ⑤ ดูเหมือนค้าง ให้ออกมาด้วย Ctrl + C หากออกมาได้ ก็แสดงว่าคุณได้ยืนยันด้วยตัวเองแล้วว่า "มีปุ่มหยุดอยู่"
หากคุณเป็นบุคคลที่ไม่มีทั้งทีมและไม่มีโฟลเดอร์บริษัท ขอให้ฝึกแค่การติดตั้ง (①) และการออกมาด้วย Ctrl + C ก่อน หากยืนยัน "เครื่องมือติดตั้งแล้ว" ด้วย claude --version และยืนยัน "ติดแล้วก็ออกมาได้" ด้วย Ctrl + C ความกลัวหน้าจอดำครึ่งหนึ่งก็จัดการได้ภายใน 5 นาทีแม้อยู่คนเดียว ส่วนค่าบริการ หากเริ่มต้นด้วยสมาชิกรายเดือนแบบเหมาจ่ายไปก่อน ก็จะฝึกฝนได้อย่างเต็มที่โดยไม่ต้องกังวลเรื่องค่าใช้จ่าย
หน้าจอสีดำปรากฏขึ้น เคอร์เซอร์กะพริบอยู่ ตรงหน้าจอนั้นมีนักออกแบบเกมประสบการณ์ 24 ปีนั่งอยู่ มือที่ทำงานมา 24 ปีด้วย PPT และ Excel ด้วยวิกิและ Figma หยุดนิ่งอยู่ครู่หนึ่งเหนือแป้นพิมพ์ มันชวนให้นึกถึง DOS ในยุคที่ยังมีโรงเรียนสอนคอมพิวเตอร์อยู่ หลังจากนั้นสิ่งที่เรียกว่าเทอร์มินัลก็เป็นของที่เห็นได้แต่บนโต๊ะของโปรแกรมเมอร์เท่านั้น ไม่รู้ว่าต้องพิมพ์อะไร และรู้สึกว่าถ้าพิมพ์ผิดแล้วอะไรบางอย่างจะพังไป ความลังเลตรงจุดนี้คือจุดเริ่มต้นของหนังสือเล่มนี้
คนส่วนใหญ่ปิดหน้าต่างไปตรงนี้ แล้วก็กลับมาพูดซ้ำในที่ประชุมว่า "พวกเราก็น่าจะต้องลองทำอะไรสักอย่างนะ" บทนี้จะนั่งลงข้าง ๆ ในตำแหน่งที่ไม่ปิดหน้าต่างนั้นและอดทนผ่าน 30 นาทีแรกไปด้วยกัน เป้าหมายไม่ใช่กลยุทธ์การนำเข้ามาใช้อันยิ่งใหญ่ แต่เป็นการยื่นใส่มือว่า เมื่ออยู่หน้าเคอร์เซอร์ที่กะพริบ ลองพิมพ์อะไรลงไปแล้วความรู้สึกห่างเหินจึงจะคลายลง
เมื่อนักออกแบบเกมนั่งลงต่อหน้าเครื่องมือเขียนโค้ดด้วย AI อย่าง Claude Code เป็นครั้งแรก ความรู้สึกแปลกใหม่และความรู้สึกอึดอัดเกิดขึ้นพร้อมกัน ณ ที่เดียวกัน ข้อเท็จจริงที่ว่าอารมณ์สองอย่างนี้ปะทะกันเองนั้นคือเบาะแสแรกของการนำเข้ามาใช้
เหตุผลที่รู้สึกแปลกใหม่นั้นชัดเจน การตรวจสอบความสอดคล้องของชีตข้อมูลที่เคยกินเวลาครึ่งวันจบลงในไม่กี่นาที บันทึกการประชุมที่ยืดยาวถูกสรุปเป็นตารางสิ่งที่ตัดสินใจ และเอกสารออกแบบที่ฝังหายไปเมื่อปีก่อนสามารถดึงกลับขึ้นมาได้อีกครั้งด้วยภาษาธรรมชาติเพียงบรรทัดเดียว
เหตุผลที่รู้สึกอึดอัดก็ชัดเจนไม่แพ้กัน หน้าจอสีดำ เคอร์เซอร์ที่กะพริบ และคำสั่งภาษาอังกฤษนั้นต่างจากภาพการทำงานในชีวิตประจำวันมากเกินไป วันหนึ่งของนักออกแบบเกมไหลไปบน GUI แต่การพิมพ์ตัวอักษรลงบนเทอร์มินัลสีดำกลับเข้ากันไม่ค่อยได้กับอัตลักษณ์ของอาชีพ เพียงแต่ความอึดอัดนี้ไม่ใช่ข้อบกพร่องของเครื่องมือ แต่เป็นต้นทุนการปรับตัวของคนที่คุ้นเคยกับ GUI เพียงยอมรับจุดนี้ ความรู้สึกห่างเหินก็คลายลงไปครึ่งหนึ่งแล้ว
หนังสือเล่มนี้เป็นหนังสือที่พยายามลดความรู้สึกห่างเหินนั้น หัวข้อ 1.1 จะนั่งลงด้วยกันในตำแหน่งของการพบกันครั้งแรก แล้วเรียบเรียงว่าควรมองอะไร ควรลองทำอะไร และอะไรที่เลื่อนไว้ก่อนได้
การออกแบบเกมเข้าร่วมกระแส AI ช้ากว่าอาชีพอื่น คนที่ทำงานกับโค้ดเข้าก่อน ตามด้วยนักออกแบบและศิลปิน นักออกแบบเกมมักมีรูปแบบที่ผัดผ่อนไปเรื่อย ๆ พร้อมพูดซ้ำว่า "พวกเราก็น่าจะต้องลองทำอะไรสักอย่างนะ"
เหตุผลที่ผัดผ่อนนั้นมีเหตุมีผล ผลงานของนักออกแบบเกมไม่ได้ถูกจัดรูปแบบตายตัวเหมือนโค้ด แต่ปะปนกันทั้งข้อความ ตาราง ไดอะแกรม การประชุม และข้อตกลงด้วยวาจา ความน่าเชื่อถือของผลลัพธ์ AI ดูต่ำ การโกหกที่ฟังดูสมเหตุสมผลนั้นอันตราย และยังน่าสงสัยด้วยว่า AI เข้าใจระบบเกมจริง ๆ หรือไม่
แต่ระหว่างปี 2024 ถึง 2026 มีสามสิ่งที่เปลี่ยนไป
ประการแรก ความสามารถในการให้เหตุผลของโมเดล AI ก้าวข้ามจุดวิกฤต มันก้าวข้ามการสร้างประโยคง่าย ๆ ไปจัดการกับการออกแบบระบบที่ซับซ้อน การตรวจสอบความสอดคล้อง และการวิเคราะห์ผลกระทบ Claude รุ่นล่าสุดช่วยสนับสนุนเวิร์กโฟลว์การออกแบบเกมได้ส่วนหนึ่งที่มีนัยสำคัญ เพียงแต่ไม่ได้หมายความว่ามอบหมายทั้งหมดให้ได้ การตรวจสอบและความรับผิดชอบยังคงเป็นหน้าที่ของมนุษย์ (ขอบเขตที่ช่วยได้แตกต่างกันมากตามชนิดของงานและความสมบูรณ์ของทีม — การประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ)
ประการที่สอง ฮาร์เนส (harness) สมบูรณ์ขึ้น เครื่องมืออย่าง Claude Code ไม่ใช่แค่แชตธรรมดา มันอ่านและเขียนไฟล์ได้โดยตรง รันคำสั่งได้ และรับผลลัพธ์กลับมาเป็นอินพุตอีกครั้ง คล้ายกับวิธีที่มนุษย์ทำงาน
ประการที่สาม เทคนิคการใช้งานอย่างเมโมรี atom และ skill ลงตัวแล้ว ไม่ใช่การใช้ AI ครั้งเดียวแล้วจบ แต่เป็นการสะสมความรู้ของทีมเพื่อทำให้ฉลาดขึ้นเรื่อย ๆ เมื่อเวลาผ่านไป ซึ่งระเบียบวิธีนี้ได้ลงหลักปักฐานแล้ว แก่นที่ส่วนหลังของหนังสือเล่มนี้กล่าวถึงก็คือการสะสมนี้เอง
เมื่อสามสิ่งนี้มารวมกัน การนำ AI เข้ามาใช้ก็กลายเป็นจังหวะที่สมเหตุสมผลแม้แต่สำหรับนักออกแบบเกม การเริ่มก่อนสายเกินไปย่อมได้เปรียบ
เครื่องมือ AI ที่นักออกแบบเกมมักพบเจอมีอยู่สองชนิด ได้แก่ ชนิดแชตบอตที่โยนคำถามลงในช่องแชต (ChatGPT, เว็บแอป Claude) และชนิดผนวกกับเอดิเตอร์ที่ทำการเติมข้อความอัตโนมัติภายในโค้ดเอดิเตอร์ (Cursor, Copilot)
Claude Code เป็นชนิดที่สาม มันทำงานบน CLI (เทอร์มินัล) และเข้าถึงสภาพแวดล้อมการทำงานทั้งหมดของมนุษย์ หากมองเป็นภาพเดียวว่าทั้งสามชนิดแยกกันตรงไหน จะได้ดังนี้
งานของนักออกแบบเกมไม่ใช่โค้ด แต่เป็นเอกสาร ตาราง และความสัมพันธ์ จุดแข็งของ Claude Code คือมันมองเห็น เข้าใจ และจัดการทั้งโฟลเดอร์ที่มนุษย์ทำงานอยู่ ไม่ต้องคัดลอกวางข้อมูลทุกครั้งเหมือนแชตบอต
หากมองด้วยการเปรียบเทียบกับสำนักงาน ชนิดแชตบอตคือเคาน์เตอร์ประชาสัมพันธ์ ถามหนึ่งครั้งตอบหนึ่งครั้ง ข้อมูลต้องหยิบออกมาใหม่ทุกครั้ง ส่วน Claude Code ใกล้เคียงกับเพื่อนร่วมงานข้างโต๊ะ มันรู้ว่าข้อมูลอยู่ที่ไหน เปิดไฟล์ด้วยมือตัวเอง และเรียบเรียงผลลัพธ์วางกลับขึ้นบนโต๊ะ Claude ตัวเดียวกันแต่จะทำอะไรได้ดีนั้นต่างกันไปตามว่าให้นั่งที่โต๊ะไหน
การติดตั้งและตั้งค่ากล่าวถึงในหัวข้อ 1.0 หัวข้อ 1.1 มุ่งไปที่ว่าใน 30 นาทีแรกควรได้ประสบการณ์อะไรเพื่อให้ความรู้สึกห่างเหินลดลง 30 นาทีแรกแบ่งออกเป็นสี่ช่วง และในแต่ละช่วงความรู้สึกห่างเหินส่วนหนึ่งจะค่อย ๆ หายไปทีละขั้น
flowchart LR
A["มอง (5 นาที)
ช่วยสรุปโฟลเดอร์นี้ที
→ 'มันเข้าใจภาษาธรรมชาติ'"]
B["ค้นหา (5 นาที)
หาคีย์เวิร์ด·ไฟล์ล่าสุด
→ 'มันทำงานจุกจิกที่ทำทุกวันแทน'"]
C["แปลง (10 นาที)
บันทึกประชุม→ตาราง, Excel→Markdown
→ 'มันสร้างเวลาให้'"]
D["ตรวจสอบ (10 นาที)
สงสัยผลลัพธ์ที่เพิ่งได้มา
→ 'เครื่องมือนี้สมควรถูกตั้งข้อสงสัย'"]
A --> B --> C --> D
D --> E["ความรู้สึกห่างเหินคลายไป 80%"]
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class A,B,C ai
class D human
class E pass
เมื่อเปิดเทอร์มินัลและรัน Claude Code หน้าจอสีดำจะปรากฏขึ้นและเคอร์เซอร์กะพริบ หลายคนหยุดอยู่ตรงนี้ แต่ถ้าหยุดก็จะพลาดโอกาสแรกที่ความรู้สึกห่างเหินจะคลายลง ลองพิมพ์ประโยคภาษาธรรมชาติหนึ่งประโยคลงไป
ช่วยสรุปให้หน่อยว่าในโฟลเดอร์นี้มีอะไรบ้าง
ไม่ใช่ทั้งโค้ดและไม่ใช่ทั้งคำสั่ง เป็นเพียงประโยคเดียว Claude Code จะสแกนโครงสร้างโฟลเดอร์ แล้วเรียบเรียงตอบว่ามีไฟล์อะไรบ้าง และดูเหมือนเป็นโปรเจกต์ที่จัดการเรื่องอะไร เมื่อพิมพ์บรรทัดเดียวนี้ในโฟลเดอร์ที่บรรจุเอกสารออกแบบ จะได้คำตอบประมาณนี้กลับมา
ช่วยสรุปให้หน่อยว่าในโฟลเดอร์นี้มีอะไรบ้าง
สแกนโฟลเดอร์นี้แล้ว ดูเหมือนเป็นโฟลเดอร์ทำงานข้อมูลเกมและเอกสารออกแบบ
ชนิดไฟล์ จำนวน ประมาณเนื้อหา
───────────── ──── ───────────────────────────
.xlsm 12 ชีตข้อมูล (ไอเทม·เควสต์·NPC ฯลฯ)
.md 34 ร่างเอกสารออกแบบ·บันทึกประชุม
.png 88 ม็อกอัป UI·ภาพอ้างอิง
โฟลเดอร์ย่อย 6 /data /docs /ref /meeting /export /tmp
ข้อสังเกต:
- ในโฟลเดอร์ /meeting มีบันทึกประชุมของสัปดาห์ที่ผ่านมา 5 รายการสะสมอยู่
- item_table.xlsm กับ quest_table.xlsm ดูเหมือนใช้คอลัมน์ ID
เดียวกันร่วมกัน (เป็นผู้ต้องสงสัย FK)
- ในโฟลเดอร์ /tmp มีไฟล์ export ชั่วคราวจำนวนมาก — อาจเป็นเป้าหมายที่ควรจัดระเบียบ
จะให้ดูส่วนไหนก่อนดี
จากคำตอบครั้งเดียวนี้ ข้อเท็จจริงที่ว่า "เครื่องมือนี้เข้าใจภาษาธรรมชาติ ไม่ใช่คำสั่ง" จะซึมเข้าสู่ร่างกาย ประสบการณ์ที่ได้รับภาพอันเรียบเรียงแล้วว่าอะไรอยู่ที่ไหนโดยไม่ต้องเปิดโฟลเดอร์เองนั้นตัดความรู้สึกห่างเหินลงครึ่งหนึ่ง ณ จุดนั้นทันที
ลองสั่งค้นหาเล็ก ๆ เช่น "บอกไฟล์ Markdown 5 ไฟล์ที่แก้ไขในสัปดาห์ที่ผ่านมาหน่อย" หรือ "ไฟล์ที่มีคำว่า 'รีวอร์ด' อยู่ที่ไหน" งานที่นักออกแบบเกมเคยเปิด Find in Files พิมพ์คีย์เวิร์ดและเลื่อนดูผลลัพธ์ทุกวัน Claude Code รับด้วยภาษาธรรมชาติแล้วเรียบเรียงคืนมาให้ ความรู้สึกที่ว่าเครื่องมือนี้ทำงานจุกจิกที่ทำทุกวันแทนนั้นจะลงตัวในช่วง 5 นาทีที่สอง
ลองสั่งการแปลงเล็ก ๆ เช่น "ดึงเฉพาะสิ่งที่ตัดสินใจจากบันทึกประชุมนี้แล้วทำเป็นตารางหน่อย" หรือ "เปลี่ยนแถวที่ 1 ถึง 10 ของชีต Excel นี้เป็นตาราง Markdown หน่อย" ประสบการณ์ที่การแปลงซึ่งใช้มือทำ 5 ถึง 10 นาทีถูกจัดการในเวลาสั้น ๆ จะนำไปสู่ความรู้สึกที่ว่าเครื่องมือนี้ไม่ได้แค่ทำการค้นหาแทน แต่สร้างเวลาให้ด้วย
เหตุผลที่การหยุดเพียงเท่านี้เป็นอันตรายก็เพราะ AI โกหกอย่างฟังดูสมเหตุสมผล จึงต้องทำให้มันตั้งข้อสงสัยผลลัพธ์ที่เพิ่งสร้างขึ้นเอง
ช่วยตรวจอีกครั้งว่าตารางที่เพิ่งทำให้ตรงกับต้นฉบับไหม มีรายการที่ตกหล่นไหม
จากนั้น Claude Code จะอ่านต้นฉบับอีกครั้ง เทียบกับผลลัพธ์ของตัวเองทีละบรรทัด แล้วรายงานจุดที่ไม่ตรงกัน ในช่วงนี้มักปรากฏหน้าจอแบบนี้บ่อยครั้ง
ช่วยตรวจอีกครั้งว่าตารางที่เพิ่งทำให้ตรงกับต้นฉบับไหม มีรายการที่ตกหล่นไหม
เทียบต้นฉบับ (meeting_0603.md) กับตารางที่เพิ่งทำอีกครั้งแล้ว
พบจุดที่ไม่ตรงกัน 3 รายการ
⚠ ตกหล่น "ปรับลดรางวัลทูทอเรียลลง 50%" — มีในแถวที่ 12 ของต้นฉบับแต่ตกหล่นในตาราง
⚠ เขียนผิด ผู้รับผิดชอบ "คิม OO" → ในตารางกรอกผิดเป็น "อี OO"
⚠ คลุมเครือ "พิจารณารีวอร์ดใหม่" — ต้นฉบับไม่ชัดเจนว่าตัดสินใจแล้วหรือพักไว้
ได้ใส่ไว้ใน 'ตัดสินใจแล้ว' ตามดุลยพินิจเอง แต่จำเป็นต้องยืนยัน
จะให้ทำตารางที่แก้ไขแล้วใหม่อีกครั้งหรือไม่ ส่วนรายการที่คลุมเครือ
หากบอกมาว่าจะให้จัดการอย่างไร จะนำไปปรับให้
ประเด็นสำคัญของ 10 นาทีสุดท้ายคือ เครื่องมือรู้จักตั้งข้อสงสัยผลลัพธ์ของตัวเอง และทำการตั้งข้อสงสัยนั้นร่วมกับมนุษย์ ท่าทีที่ย้อนถามกลับมาอย่างรายการที่สามว่า "ผมตัดสินใจตามดุลยพินิจของตัวเอง จึงขอให้ช่วยยืนยัน" นั้นคือกลไกความปลอดภัยที่ทิ้งการตรวจสอบไว้ในมือมนุษย์
เมื่อผ่านไป 30 นาที ความรู้สึกห่างเหิน 80% หายไปแล้ว ส่วนอีก 20% ที่เหลือจะค่อย ๆ ลดลงในบทถัด ๆ ไป
โปรเจกต์ MMORPG แห่งหนึ่งที่ผู้เขียนดูแลในฐานะ Design Director (ต่อไปเรียกว่า "โปรเจกต์ A") ได้ดำเนินเวิร์กโฟลว์ที่มี Claude Code เป็นศูนย์กลางร่วมกับทีมออกแบบ (4 ถึง 5 คน) มาประมาณ 6 เดือน (ทีมพัฒนาทั้งหมดของโปรเจกต์ A เป็นขนาดกลาง 10 ถึง 50 คน) ขอยกภาพมาบางส่วน
ขอแยกออกมาเพียงกรณีเดียวที่จับต้องได้ที่สุดด้วยการวัดจริง นั่นคือการตรวจสอบความสอดคล้องของ FK (foreign key, คีย์นอก) ที่พาดผ่านชีตข้อมูลกว่า 30 ชีต เป็นงานที่ใช้สายตามนุษย์ไล่ตามว่า ID ของชีตหนึ่งถูกอ้างอิงอย่างถูกต้องในอีกชีตหรือไม่ ยิ่งชีตเพิ่มขึ้น การจับคู่ก็ยิ่งเพิ่มแบบทวีคูณ
จากครึ่งวันเหลือ 5 นาที ผู้เขียนจะไม่นำบรรทัดนี้ไปสรุปเหมารวม งานอื่น ๆ มีช่วงการประหยัดที่น้อยกว่า หรือบางครั้งก็มีเวลาตรวจทานเพิ่มเข้ามาใหม่ ภาพอื่น ๆ ที่เห็นในช่วง 6 เดือนเดียวกันนี้ขอเขียนไว้เป็นเพียงทิศทางและสัดส่วน
เวลาที่ประหยัดสะสมตลอด 6 เดือนหลังจากเครื่องมือแต่ละตัวถูกสร้างขึ้นครั้งเดียวนั้น สำหรับความรู้สึกของผู้เขียนคิดเป็นหน่วยคน-เดือน (person-month) ไม่ใช่คน-สัปดาห์ (person-week) (การรวมยอดที่แม่นยำยังไม่ได้วัด เป็นค่าประมาณ) ด้วยเวลานั้นจึงสามารถทุ่มเทกับการออกแบบที่ลึกขึ้นได้
เครื่องมือเช่นนี้เมื่อสร้างขึ้นครั้งเดียวก็ทำงานได้ยาวนาน เพียงแต่ 'ยาวนาน' ไม่ได้แปลว่า 'ไร้คน' มันจะอยู่ได้นานก็ต่อเมื่อมีคนดูแลและโครงสร้างการตรวจสอบพร้อมไปด้วยกัน หากทิ้งไว้แต่เครื่องมือแล้วคนจากไป มันจะผุพังภายในสองไตรมาส ส่วนหลัง ๆ ของหนังสือเล่มนี้จะกล่าวถึงว่าเครื่องมือแต่ละตัวข้างต้นถูกสร้างและดำเนินการอย่างไร
ขอชี้ความกลัวที่นักออกแบบเกมมักมีต่อหน้าเครื่องมือ AI อย่างตรงไปตรงมา การจัดการโดยไม่หลีกหนีคือขั้นตอนแรกของการนำเข้ามาใช้
ความกลัวที่ว่า "AI จะมาแทนที่งานของฉัน" นั้นถูกครึ่งหนึ่งและผิดครึ่งหนึ่ง งานจุกจิกง่าย ๆ (ตรวจสอบความสอดคล้อง·แปลงเอกสาร·ค้นหา) AI แทนที่ได้ แต่การตัดสินใจ การจัดลำดับความสำคัญ และการออกแบบอารมณ์ของผู้เล่นนั้นแทนที่ไม่ได้ ตรงกันข้าม นักออกแบบเกมที่ใช้ AI ได้ดีจะหลุดพ้นจากงานจุกจิกแล้วทุ่มเทกับแก่นแท้ ลองถามตัวเองดูว่า "สัดส่วนของงานจุกจิกกับแก่นแท้ในงานของฉันเป็นเท่าไร" หากงานจุกจิกเป็น 70% แก่นแท้อีก 30% ก็ยังคงเป็นหน้าที่ของตัวเองอยู่ดี และประเด็นสำคัญคือ 30% นั้นจะยิ่งสำคัญมากขึ้น
คำถามที่ว่า "ถ้า AI ทำผิด ใครจะรับผิดชอบ" ก็พบบ่อย ความรับผิดชอบต่อการตัดสินใจของนักออกแบบเกมเป็นของนักออกแบบเกมเสมอ การนำผลลัพธ์ AI ไปใช้โดยไม่ตรวจสอบนั้นเป็นความผิดพลาดของนักออกแบบเกม ไม่ใช่ความผิดพลาดของ AI การออกแบบขั้นตอนการตรวจสอบไปด้วยกันคือส่วนหนึ่งของการนำเข้ามาใช้ 'การทำให้ตั้งข้อสงสัยผลลัพธ์ของตัวเอง' ที่เห็นใน 10 นาทีสุดท้ายของหัวข้อ 1.1.3 คือเมล็ดพันธุ์ที่เล็กที่สุดของขั้นตอนนั้น
ความกลัวที่ว่า "ไม่ค่อยรู้เรื่องโค้ดเลยใช้ไม่ได้" จะคลายลงในไม่ช้า Claude Code ทำงานด้วยภาษาธรรมชาติ ดังนั้นไม่รู้เรื่องโค้ดก็เริ่มได้ตามที่ไม่รู้นั้น เพราะได้อ่านสคริปต์ที่ AI สร้างไปด้วยกัน พอผ่านไปสักสองสามเดือนก็จะอ่านและแก้ไขสคริปต์ง่าย ๆ ได้ การเรียนรู้จะตามมาเองโดยอัตโนมัติ
"เครื่องมือเปลี่ยนเร็วเกินไป" ก็เป็นความกังวลที่พบบ่อย หากพยายามตามให้ทันทั้งโมเดล ฟีเชอร์ และเทรนด์ทั้งหมดก็จะเหนื่อย ให้เรียนรู้เพียง 1 ถึง 2 ฟีเชอร์ที่เป็นประโยชน์ต่อเวิร์กโฟลว์ของตัวเองให้ลึก ส่วนที่เหลือค่อยดูเมื่อจำเป็น
ขอบอกไว้ล่วงหน้า แม้หน้าจอคำตอบในหัวข้อ 1.1.3 จะดูราบรื่น แต่ใน 30 นาทีแรกจริง ๆ จะมีคำตอบที่เพี้ยน การสรุปไฟล์ที่ผิดที่ผิดทาง และผลลัพธ์ที่ติดขัดปะปนกันออกมา นั่นเป็นเรื่องปกติ หนังสือเล่มนี้ไม่ใช่เรื่องราวความสำเร็จที่ราบรื่น แต่กล่าวถึงมากกว่าว่าจะร้องขอใหม่อย่างไรเพื่อแก้ไขผลลัพธ์ที่เพี้ยนไป
หนังสือเล่มนี้ประกอบด้วย 24 ส่วน ไม่จำเป็นต้องอ่านเรียงลำดับตั้งแต่ต้นจนจบ เลือกหนึ่งในสามรูปแบบต่อไปนี้ที่เหมาะกับสถานการณ์ของตัวเองได้
| รูปแบบ | เส้นทาง | เวลาที่ใช้ |
|---|---|---|
| รูปแบบเริ่มต้นนำเข้า | ส่วนที่ 1 (นำเข้า) → ส่วนที่ 2 (สถาปัตยกรรมสารสนเทศ) → สาขาของตัวเอง 1 สาขา | 1 ถึง 2 เดือน |
| รูปแบบครบทั้งเล่ม | ส่วนที่ 1 ถึง 2 → แยกตามสาขา (3 ถึง 15) → กระบวนการ (16 ถึง 19) → การดำเนินงาน (20 ถึง 24) | 6 เดือนถึง 1 ปี เหมาะกับระดับทีม |
| รูปแบบแก้ปัญหา | ดัชนีภาคผนวก → ย้อนกลับไปยังบทที่เกี่ยวข้อง | ประมาณ 1 สัปดาห์ เมื่อมีปัญหาเฉพาะหน้า |
หากยังนึกไม่ออกว่าจะเลือกเส้นทางไหน แบ่งตามว่าตัวเองใกล้เคียงกับอะไรก็ได้ หากคุณเป็นนักออกแบบนอกวงการเกม·PM·พนักงานบริษัททั่วไป แทนที่จะใช้สามรูปแบบข้างต้น ขอแนะนำ 「เส้นทางของงานทั่วไป」 (ส่วนที่ 1·2 → ส่วนที่ 17 บันทึกประชุม → ส่วนที่ 16 การทำงานร่วมกัน → ส่วนที่ 18 การตัดสินใจ → ส่วนที่ 21·22 การพัฒนาตนเอง·ธรรมาภิบาล) — แม้ข้ามบทที่เป็นโดเมนเกมไป โครงกระดูกหลักก็ยังตั้งอยู่ได้ตามเดิม และกล่อง 「การประยุกต์นอกเกม」 ในแต่ละบทเป็นสะพานให้ย้ายไปอ่านในแง่งานของตัวเอง (ดัชนีอยู่ในภาคผนวก F.5) หากไม่มีเวลา ตามไปเพียงสี่บท 17.1 → 16.2 → 22.1 → 21.1 ก็ได้ หากคุณเป็นผู้อ่านที่ไม่ได้เรียนมาทางนี้และเพิ่งเริ่มใช้เครื่องมือ AI ให้ใช้ 'รูปแบบเริ่มต้นนำเข้า' ยึดสาขาของตัวเอง (หรือสาขาที่ใกล้เคียงที่สุด) เพียงสาขาเดียวจนจบ ส่วนสาขาที่ลงลึก (4·8·11 ฯลฯ) ให้เก็บเฉพาะ 'บรรทัดเดียวสำหรับผู้ที่ไม่ได้เรียนมาทางนี้' ในส่วนนำของบทไว้ก่อน แล้วค่อยลงไปยังเนื้อหาเมื่อจำเป็น
ทุกบทของหนังสือเล่มนี้ไม่ได้ลงลึกถึงระดับวิชาการ แต่หยุดอยู่ที่ระดับที่ดำเนินการได้ เป้าหมายคือนำเทคนิคที่เดินจริงในทีมขนาดกลางตลอด 6 เดือนมาถ่ายทอดตามเดิม แล้วเดินไปด้วยกันบนเส้นทางที่เริ่มเล็กแล้วขยายให้ใหญ่
หัวข้อ 1.1 เป็นบทที่ลดความรู้สึกห่างเหิน ส่วนหัวข้อ 1.2 จะก้าวเข้าไปอีกขั้น แล้วอธิบายกลไกพื้นฐานของเครื่องมือนี้ด้วยภาษาที่เป็นมิตรต่อนักออกแบบเกม เป้าหมายของหัวข้อ 1.2 คือทำให้ไม่กลัวคำอย่างโมเดล·โทเค็น·คอนเท็กซ์·ฮาร์เนส ส่วนการตั้งค่าอย่างจริงจัง (เมโมรี·สิทธิ์·settings.json) จะกล่าวถึงในหัวข้อ 1.3
setup 1. เปิดเทอร์มินัล (Windows ใช้ PowerShell, macOS ใช้เทอร์มินัล) 2. ย้ายไปยังโฟลเดอร์ที่รวมเอกสารออกแบบไว้ แล้วรัน Claude Code (การติดตั้งอยู่ในหัวข้อ 1.0) 3. ตั้งเวลา 30 นาที — 5 นาที (มอง)·5 นาที (ค้นหา)·10 นาที (แปลง)·10 นาที (ตรวจสอบ)
prompt (หนึ่งบรรทัดต่อหนึ่งช่วง ลองพิมพ์ตามลำดับ)
① ช่วยสรุปให้หน่อยว่าในโฟลเดอร์นี้มีอะไรบ้าง
② ไฟล์ที่มีคำว่า 'รีวอร์ด' อยู่ที่ไหน
③ ดึงเฉพาะสิ่งที่ตัดสินใจจากบันทึกประชุมนี้มาทำเป็นตารางหน่อย
④ ช่วยตรวจอีกครั้งว่าตารางที่เพิ่งทำตรงกับต้นฉบับไหม มีรายการที่ตกหล่นไหม
verify - ในข้อ ① ให้เทียบด้วยสายตาว่าการสรุปโครงสร้างโฟลเดอร์ตรงกับโฟลเดอร์จริงหรือไม่ - ในข้อ ④ หากรายงานจุดที่ไม่ตรงกันออกมาแม้เพียงรายการเดียวก็ถือว่าสำเร็จ หมายความว่าคุณได้เห็นฉากที่ AI ตั้งข้อสงสัยผลลัพธ์ของตัวเองกับตา - แม้คำตอบจะเพี้ยนออกมาก็ไม่ใช่ความล้มเหลว การร้องขอใหม่อย่าง "คำตอบเมื่อกี้ผิด ช่วยดูเฉพาะไฟล์นี้อีกครั้ง" ก็นับเป็นส่วนหนึ่งของการฝึก 30 นาทีแรกด้วย
หากคุณเป็นบุคคลที่ไม่มีทั้งทีมและโฟลเดอร์บริษัท ให้ลองพิมพ์เฉพาะ prompt ข้อ ① และ ④ ข้างต้นในโฟลเดอร์ทำงานใด ๆ บน PC ของตัวเอง (เช่น โฟลเดอร์ดาวน์โหลด โฟลเดอร์บันทึกย่อ) เมื่อยืนยัน "มันเข้าใจภาษาธรรมชาติ" ด้วยข้อ ① และ "สามารถตั้งข้อสงสัยผลลัพธ์ได้" ด้วยข้อ ④ แก่นสำคัญสองอย่างของบทนี้ก็สัมผัสได้ภายใน 5 นาทีแม้อยู่คนเดียว
เรื่องนี้เกิดขึ้นตอนที่ผมทำงานชิ้นหนึ่งเสร็จแล้วมองดูปริมาณการใช้งาน บันทึกการประชุมห้าฉบับของสัปดาห์นี้กองอยู่ในโฟลเดอร์ และก่อนการประชุมสแตนด์อัปเช้าวันจันทร์ ผมต้องสรุป "เฉพาะสิ่งที่ตัดสินใจกันแล้ว" ลงในหน้าเดียว ผมพิมพ์ข้อความบรรทัดหนึ่งลงในหน้าต่าง Claude Code "ช่วยดึงเฉพาะข้อตัดสินใจจากบันทึกการประชุมในโฟลเดอร์นี้มาทำเป็นตาราง" หลังจากกด Enter ไปราว 0.4 วินาที ก็มีตัวอักษรสีเทาเล็ก ๆ กะพริบขึ้นที่ด้านล่างของหน้าจอ
Reading meeting-2026-05-25.md ... (1,840 tokens)
Reading meeting-2026-05-27.md ... (2,310 tokens)
ตัวอักษรสีเทานี้คือหัวข้อของบทนี้ เมื่อเราโยนประโยคภาษาเกาหลีหนึ่งประโยคเข้าไป เครื่องมือจะหั่นมันเป็นโทเค็น อ่านไฟล์เป็นโทเค็นแล้วป้อนให้โมเดล รับคำตอบของโมเดลมาเขียนลงไฟล์ ทุกครั้งที่การไป-กลับนี้หมุนรอบหนึ่ง ก็จะมีการคิดค่าใช้จ่าย และมีข้อมูลสะสมอยู่ใน "มุมมอง" ของโมเดล บทนี้จะแยกสิ่งที่เกิดขึ้นเบื้องหลังตัวอักษรสีเทานั้นออกมาด้วยภาษาของนักออกแบบเกม สี่คำก็พอ — โมเดล·โทเค็น·context window·ฮาร์เนส
บันทึกศัพท์ - โมเดล (model): สมองที่สร้างคำตอบ มีหลายชนิดที่ขนาดและนิสัยต่างกัน เช่น Opus·Sonnet·Haiku - โทเค็น (token): ชิ้นส่วนที่หั่นตัวหนังสือออกเป็นชิ้นเล็ก ๆ ค่าใช้จ่าย·ความเร็ว·มุมมอง ล้วนนับด้วยหน่วยนี้ทั้งสิ้น - context window: จำนวนโทเค็นสูงสุดที่โมเดลบรรจุไว้ในหัวได้ในคราวเดียว - ฮาร์เนส (harness): ตัวถังที่ทำให้โมเดลทำงานได้ Claude Code คือตัวอย่างหนึ่งของมัน
ตัวอักษรสีเทาข้างต้นไม่ใช่ล็อกแบบสุ่ม แต่เป็นช่องหนึ่งของวงจรที่กำหนดไว้แล้ว สิ่งที่ฮาร์เนสทำสุดท้ายก็คือการหมุนวงเดิมซ้ำ ๆ อย่างรวดเร็ว อ่านไฟล์แล้วป้อนให้โมเดล เมื่อโมเดลบอกว่า "ให้รันคำสั่งนี้" ก็รัน แล้วป้อนผลลัพธ์กลับเข้าโมเดลอีกครั้ง วงนี้จะหมุนไปจนกว่างานจะเสร็จ
flowchart TD
Start([คน: สั่งงานหนึ่งบรรทัด]) --> Read[ฮาร์เนส: อ่านไฟล์·ข้อมูล
แปลงเป็นโทเค็น]
Read --> Inject[ฮาร์เนส: ฉีดเข้า context
+ รวมโทเค็นสะสม]
Inject --> Model{โมเดล: ตัดสินใจการกระทำถัดไป}
Model -->|ต้องรันคำสั่ง| Exec[ฮาร์เนส: รันคำสั่งเชลล์·สคริปต์]
Exec --> Result[ป้อนผลลัพธ์การรัน
กลับเข้าเป็นโทเค็น]
Result --> Inject
Model -->|คำตอบพร้อมแล้ว| Write[ฮาร์เนส: เขียนไฟล์·แสดงผล]
Write --> Verify{ผ่านการตรวจสอบ?}
Verify -->|ไม่ผ่าน| Inject
Verify -->|ผ่าน| Done([บันทึกผลลัพธ์])
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class Start,Verify human
class Read,Inject,Exec,Write code
class Model ai
class Result data
class Done pass
ในภาพนี้ ช่องที่คนลงมือมีเพียงสองช่องคือบนสุด (สั่งงาน) และล่างสุด (ตรวจยืนยันผลการตรวจสอบ) ส่วนวงตรงกลางฮาร์เนสหมุนเองโดยอัตโนมัติ ที่ตัวอักษรสีเทากะพริบห้าครั้งระหว่างอ่านบันทึกการประชุมห้าฉบับ ก็คือการหมุนช่อง Read → Inject ไปห้ารอบ ถ้าเป็นแชตบนเว็บ เราต้องเปิดไฟล์ทั้งห้ามาคัดลอก·วางเองทีละไฟล์ การที่ฮาร์เนสทำงานนั้นแทนเราได้ นี่คือความต่างชี้ขาดที่ทำให้แชตบอตกับฮาร์เนสแบบ CLI กลายเป็นเครื่องมือคนละชนิด
ทุกครั้งที่ลูปหมุนหนึ่งรอบ ช่อง Inject จะรวมโทเค็นสะสมเข้าด้วยกัน ดังนั้นถ้ายังไม่เข้าใจโทเค็นเสียก่อน ก็จะมองไม่เห็นทั้งค่าใช้จ่ายและขีดจำกัดของลูปนี้ เราจึงดูที่โทเค็นก่อน
โทเค็นไม่ใช่ตัวอักษร แต่เป็นชิ้นส่วนที่โมเดลหั่นตัวหนังสือออกมา ตามหลักประสบการณ์ ภาษาอังกฤษราว 4 ตัวอักษรเท่ากับ 1 โทเค็น ภาษาเกาหลีราว 2 ตัวอักษรใกล้เคียง 1 โทเค็น (ไม่ใช่อัตราแปลงทางการ แต่เป็นค่าประมาณสำหรับใช้งานจริง — ค่าจริงนั้นตัวตัดโทเค็น (tokenizer) ของโมเดลเป็นผู้กำหนด และต่างกันไปในแต่ละประโยค) ภาษาเกาหลีรวมเว้นวรรค 20 ตัวอักษรก็ราว 10 โทเค็น
ลองไล่ตามงานบันทึกการประชุมนั้นเป็นโทเค็นดู (ตัวเลขด้านล่างคือการวัดเพียงครั้งเดียวของงานเดียวกัน เนื่องจากผันแปรตามปริมาณบันทึกการประชุมและความยาวของบทสรุป จึงขอแนะนำให้อ่านเป็นจำนวนหลักและสัดส่วน ไม่ใช่ค่าสัมบูรณ์)
| ขั้นตอน | คืออะไร | โทเค็น (อินพุต) | โทเค็น (เอาต์พุต) |
|---|---|---|---|
| สั่งงาน | "ดึงเฉพาะข้อตัดสินใจมาทำตาราง" หนึ่งบรรทัด | \~25 | — |
| อ่านบันทึกการประชุม ×5 | เนื้อหาไฟล์ md 5 ไฟล์ | \~10,400 | — |
| ฉีดกฎการจัดหมวด | atom หมวดการประชุม 1 รายการ (JIT) | \~480 | — |
| โมเดลให้เหตุผล·เขียนตาราง | ข้อตัดสินใจ 12 รายการเป็นตาราง | — | \~1,600 |
| ป้อนซ้ำเพื่อตรวจสอบ | ถามซ้ำรายการตกหล่น 1 รายการที่ Linter จับได้ | \~320 | \~210 |
| สะสม | \~11,225 | \~1,810 |
มีสองสิ่งที่สะดุดตา หนึ่ง คำสั่งที่ผมพิมพ์มีแค่ 25 โทเค็น แต่ทั้งงานมีเฉพาะอินพุตเกินหนึ่งหมื่นหนึ่งพันโทเค็น ค่าใช้จ่ายเกือบทั้งหมดไม่ได้มาจากประโยคของผม แต่มาจากข้อมูลที่เครื่องมืออ่านเข้าไป สอง เอาต์พุต (1,810) เป็นราวหนึ่งในหกของอินพุต (11,225) งานอัตโนมัติด้านการออกแบบเกมส่วนใหญ่อ่านมากเขียนน้อยแบบนี้ ดังนั้นหากต้องการลดค่าใช้จ่าย การจัดการปริมาณข้อมูลอินพุตได้ผลกว่าการขัดเกลาเอาต์พุตมากนัก
หลังงานเสร็จ พอพิมพ์ /context ก็จะเห็นว่าเซสชันนั้นใช้ context ไปเท่าไร ถ้าไม่ใส่ใจโทเค็น มันก็ไหลออกไปโดยไม่รู้ตัวเหมือนกระดาษพิมพ์ แต่พอทำให้มองเห็นได้สักครั้ง ท่าทีก็เปลี่ยนไป การทำให้มองเห็นนี้คือจุดเริ่มต้นของการประหยัด
เครื่องมือที่ใช้จัดการโทเค็นไม่ใช่จิตวิญญาณการประหยัดอันคลุมเครือ แต่เป็นเทคนิครูปธรรมที่จัดการข้อมูลอินพุตให้ละเอียด
ในจำนวนนี้ ข้อ 1 การฉีดแบบ JIT คือกลไกที่หมุนอยู่จริงในสภาพแวดล้อมการทำงานของหนังสือเล่มนี้ เมื่ออินพุตหนึ่งบรรทัดเข้ามา ฮุก inject_memory.py จะจับคู่ atom ในหน่วยความจำตามลำดับคะแนน คัดเฉพาะไม่กี่อันดับแรกมาฉีด และแม้ล้มเหลวก็ไม่ขวางการไหลของงาน (รายละเอียดการอิมพลีเมนต์อยู่ใน 1.3) หลักการประหยัดโทเค็นที่ว่า "เฉพาะข้อมูลที่จำเป็น เฉพาะไม่กี่อันดับแรก ล้มเหลวก็เงียบ ๆ" บรรจุอยู่ในไฟล์โค้ดไฟล์เดียวอย่างนั้นเลย
โมเดลเปรียบเหมือนเครื่องยนต์รถยนต์ บนตัวถังเดียวกันที่ชื่อ Claude Code เราสามารถใส่เครื่องยนต์ต่างกันอย่าง Opus·Sonnet·Haiku ได้ เปลี่ยนเครื่องยนต์แล้วลักษณะของงานก็เปลี่ยน
เมื่อเทียบกับงานออกแบบเกมก็แบ่งได้อย่างนี้ งานที่ต้องการการให้เหตุผลลึกและความสอดคล้อง เช่น การตรวจการออกแบบระบบหรือการสังเคราะห์ร่าง GDD (Game Design Document, เอกสารออกแบบเกม — เอกสารสเปกละเอียด) ที่รวมข้อมูลหลายแหล่ง ใช้ Opus งานประจำส่วนใหญ่ เช่น การดึงข้อตัดสินใจจากบันทึกการประชุมหรือบทสรุปรายวัน ใช้ Sonnet งานที่แทบไม่มีการตัดสิน เช่น การแปลงรูปแบบของชีตข้อมูลง่าย ๆ ก็ส่งไปที่ Haiku ที่งานบันทึกการประชุมในหัวข้อก่อนหน้ารันด้วย Sonnet ก็ด้วยเกณฑ์นี้ — งานคัดข้อตัดสินใจมาย้ายลงตารางนั้น ความสมดุลและความเร็วเหมาะกว่าการให้เหตุผลลึก
ในช่วงเริ่มนำมาใช้ มีกับดักที่ทุกคนตกลงไป นั่นคือแรงกระตุ้นที่อยากรันทุกงานด้วยเครื่องยนต์ที่ดีที่สุด คือ Opus หากตามแรงกระตุ้นนั้น ภาระค่าใช้จ่ายและความเร็วก็จะย้อนกลับมาเป็นภาระการดำเนินงานในไม่ช้า และจะไม่ได้สั่งสมความรู้สึกในการจับคู่โมเดลให้เข้ากับงาน ทักษะที่แท้จริงของการดำเนินงานไม่ใช่การเลือกโมเดลในหัวทุกครั้ง แต่เป็นการตรึงมันให้ตายตัวด้วยระบบอัตโนมัติหลังจากจับรูปแบบได้แล้ว
การตรึงเช่นนี้ทำได้โดยระบุโมเดลไว้ใน settings.json หรือในคำสั่งสแลช (รายละเอียดใน 1.3) ตรึงไว้ครั้งเดียว ความยุ่งยากในการเลือกทุกครั้งก็หายไป
โมเดลออกเวอร์ชันใหม่ราวทุกครึ่งปี และแม้ชื่อเดียวกัน 4.5 กับ 4.6 ก็ต่างกัน เมื่อมีเวอร์ชันใหม่ออกมา ให้เปรียบเทียบเฉพาะงานหลักห้างานของเวิร์กโฟลว์ด้วยอินพุตเดียวกัน ถ้าจะทดสอบทั้งหมดก็จะเหนื่อย แค่ความต่างของผลลัพธ์ห้างานก็เพียงพอจะตัดสินว่าจะเปลี่ยนไปใช้หรือไม่
ผมบอกไปแล้วว่าทุกครั้งที่ลูปหมุนหนึ่งรอบ โทเค็นจะสะสมที่ช่อง Inject เพดานที่การสะสมนั้นชนคือ context window กล่าวคือจำนวนโทเค็นสูงสุดที่โมเดลประมวลผลได้ในคราวเดียว ถ้าเทียบกับคนก็คือความจำใช้งาน (working memory)
งานบันทึกการประชุมข้างต้นมีอินพุตสะสมในระดับหนึ่งหมื่นหนึ่งพันโทเค็น เทียบกับเพดาน 200K แล้วราว 6% เศษ จึงยังเหลือเฟือ แต่ถ้าไม่เปลี่ยนงานแล้วลากเซสชันยาวอยู่ในหน้าต่างเดิม ก็จะเข้าใกล้เพดาน เมื่อเต็มแล้ว เนื้อหาเก่าจะถูกตัด โมเดลเริ่มลืม "ความทรงจำ" ส่วนต้น และการบีบอัดอัตโนมัติจะทำงานแทนที่บทสนทนาก่อนหน้าด้วยฉบับสรุป
นิสัยที่ใช้จัดการเพดานนี้มีสี่อย่าง
| รูปแบบ | เมื่อใด |
|---|---|
| แยกเซสชัน | เมื่อข้ามไปหัวข้ออื่น ให้เปิดเซสชันใหม่ |
| บีบอัดอย่างชัดเจน | เมื่องานหนึ่งเสร็จ ให้เหลือเฉพาะแก่นแล้วบีบอัด |
| นำหน่วยความจำออกนอก | ข้อมูลที่ใช้บ่อยให้แยกเป็น atom แล้วฉีดด้วย JIT ตามจังหวะ |
| ทำให้ context มองเห็นได้ | ใช้ /context ตรวจดูปริมาณการใช้งานปัจจุบันด้วยตา |
กรณีหนักที่นักออกแบบเกมเจอบ่อยคืองานที่ต้องใช้ทั้งเอกสารการประชุม·เอกสารออกแบบ·ชีตข้อมูลพร้อมกัน ตอนนั้นตัวเลือก 1M มีประโยชน์ เพียงแต่ 1M มาพร้อมภาระค่าใช้จ่ายและความเร็ว ปกติ 200K ก็เพียงพอ และจะหยิบ 1M ออกมาเฉพาะเมื่อกองข้อมูลใหญ่จริง ๆ เท่านั้น
กลับมาที่ช่อง ผ่านการตรวจสอบ? ล่างสุดของภาพลูป หากไม่มีช่องนี้ คำโกหกที่ดูน่าเชื่อของโมเดลก็จะถูกบันทึกลงไฟล์ตรง ๆ บางครั้งโมเดลก็เสนอคำตอบที่ผิดอย่างมั่นใจราวกับเป็นคำตอบที่ถูก (อาการหลอน, hallucination) ความถี่นั้นลดลงเมื่อรุ่นสูงขึ้น แต่ไม่ถึงศูนย์ จึงต้องตั้งการตรวจสอบไว้เป็นช่องประจำ
อาการหลอนที่อันตรายในงานออกแบบเกมนั้นเป็นรูปธรรม เช่น อ้างคอลัมน์ของชีตข้อมูลที่ไม่มีอยู่จริง คำนวณการปรับสมดุลด้วยสูตรผิด หรือสรุปสิ่งที่ยังไม่ได้ตัดสินในที่ประชุมว่าตัดสินกันแล้ว ในงานบันทึกการประชุม สิ่งที่น่ากลัวที่สุดคือข้อสาม — กรณีที่ "เรื่องที่แค่หารือกันแล้วพักไว้" แอบขึ้นไปอยู่ในตารางข้อตัดสินใจ
การตรวจสอบมีอยู่ห้ารูปแบบ
ไม่ได้ทำทั้งห้าทุกครั้ง แต่เลือก 1\~3 อย่างตามระดับความเสี่ยงของงาน ในงานบันทึกการประชุม ผมรวมข้อ 4 Linter ("ในข้อตัดสินใจมีผู้รับผิดชอบ·เนื้อหา·กำหนดเวลาครบหรือไม่") เข้ากับข้อ 3 การตรวจตัวอย่างหนึ่งครั้ง บรรทัดสุดท้ายของตารางโทเค็นก่อนหน้า "ป้อนซ้ำเพื่อตรวจสอบ 320 โทเค็น" ก็คือการไป-กลับที่ Linter จับรายการตกหล่นได้แล้วถามกลับไปยังโมเดลนั้นเอง ถ้าเทียบกับภาพลูปก็คือการหมุนเพิ่มอีกหนึ่งรอบจาก ไม่ผ่านการตรวจสอบ → Inject
ถ้าให้คนตรวจสอบทั้งหมดทุกครั้ง ผลของการนำมาใช้ก็ลดครึ่ง การตรวจสอบจึงเป็นเป้าหมายของการทำให้อัตโนมัติด้วย การดึงข้อตัดสินใจจากบันทึกการประชุมให้ Linter ตรวจรายการที่ตกหล่นในรูปแบบ การแปลงชีตข้อมูลให้ตรวจความสอดคล้องของจำนวนแถว·ผลรวม·foreign key การสร้าง GDD อัตโนมัติให้ตรวจหัวข้อสำคัญที่ตกหล่น ทั้งหมดตรวจอัตโนมัติ ผ่านแล้วคนไม่ต้องดูก็ได้ ดูเฉพาะที่ไม่ผ่าน เป็นภาพของการหยิบเฉพาะโฟลเดอร์ที่ติดธงแดงขึ้นมาจากกองเอกสารที่เต็มตู้ การออกแบบให้สายตาของคนตกลงเฉพาะที่อันตรายเท่านั้น คือจุดประสงค์ของการทำการตรวจสอบให้อัตโนมัติ
ทีนี้มาสรุปงานบันทึกการประชุมนั้นตั้งแต่ต้นจนจบ ว่าสี่คำถูกร้อยเข้าในบรรทัดเดียวอย่างไร
| ช่อง | เกิดอะไรขึ้น | แนวคิดใด |
|---|---|---|
| 1 | รัน Claude Code ในโฟลเดอร์บันทึกการประชุม | ฮาร์เนส |
| 2 | งานเป็นการวิเคราะห์บันทึกการประชุม จึงเลือก Sonnet | โมเดล |
| 3 | รวมราว 11K โทเค็น อยู่ในหน้าต่าง 200K — OK | โทเค็น·context |
| 4 | ฉีด atom กฎการจัดหมวดการประชุมด้วย JIT อัตโนมัติ | โทเค็น (ประหยัด) |
| 5 | โมเดลแสดงข้อตัดสินใจ 12 รายการเป็นตาราง | ลูปโมเดล·ฮาร์เนส |
| 6 | Linter จับรายการตกหล่นในรูปแบบ 1 รายการ → ถามกลับเพื่อเสริม | การตรวจสอบ (เพิ่มลูป 1 รอบ) |
| 7 | บันทึก·commit เป็น weekly-decisions-2026-W21.md |
ฮาร์เนส |
ถ้าทำด้วยมือ การเปิดอ่านบันทึกการประชุมห้าฉบับ คัดเฉพาะข้อตัดสินใจมาคัดลอก แล้วจัดรูปแบบ จะใช้เวลา 30 นาที พอทำเป็นอัตโนมัติก็ลดเหลือ 5 นาที และในห้านาทีนั้น มือของคนทำเพียงงานเดียวคือกวาดดูตัวอย่างการตรวจสอบหนึ่งครั้ง เวลาของคนจะตกลงเฉพาะที่ต้องการมันจริง ๆ เท่านั้น (ว่าเรื่องที่พักไว้ไม่ได้ถูกยกขึ้นเป็นข้อตัดสินใจผิด ๆ หรือไม่) แก่นไม่ได้อยู่ที่ 25 นาทีที่ประหยัดได้ แต่อยู่ที่ตำแหน่งที่สายตาตกลงไปนั้นเปลี่ยนไป
"Opus ดีกว่าเสมอ" คือสิ่งที่พบบ่อยที่สุด ถ้าไม่สนใจค่าใช้จ่าย·ความเร็วก็จริงอยู่ แต่กับงานง่าย ๆ Opus คือความสิ้นเปลือง การจับคู่ตามงานคือคำตอบ
"context 1M จำเป็นเสมอ" ก็โผล่มาบ่อย ส่วนใหญ่ 200K ก็เพียงพอ และ 1M มาพร้อมภาระ จึงใช้เฉพาะกับกองข้อมูลที่ใหญ่จริง ๆ
"การตรวจสอบเป็นงานของคน" ถูกแค่ครึ่งเดียว ส่วนที่ตรวจอัตโนมัติได้เป็นส่วนใหญ่ และคนทุ่มเทไปที่ส่วนที่เหลือ
"ไม่ต้องใส่ใจโทเค็นก็ได้" ในงานส่วนตัวก็พอใช้ได้ระดับหนึ่ง แต่พอหลายคนใช้ร่วมกัน ค่าใช้จ่ายสะสมจะโตขึ้นเร็ว การปลูกฝังรูปแบบการทำให้มองเห็น·การประหยัดตั้งแต่แรกจะปลอดภัยกว่า
"ฮาร์เนสไม่มีความต่าง" ก็มีมากอย่างไม่คาดคิด แม้เป็นโมเดลเดียวกัน แต่จะแชตบอตหรือ CLI ก็แยกออกเป็นเครื่องมือคนละชนิด การมีหรือไม่มีงานคัดลอก·วางที่เห็นมาข้างต้นคือความต่างนั้น
ลองหมุนสี่คำของบทนี้ด้วยตนเองผ่านงานเล็ก ๆ ชิ้นหนึ่ง
setup
prompt
ช่วยคัดเฉพาะ "สิ่งที่ตัดสินใจกันแล้ว" จากโน้ตในโฟลเดอร์นี้
มาทำเป็นตาราง 3 คอลัมน์ ผู้รับผิดชอบ·เนื้อหา·กำหนดเวลา
เรื่องที่พักไว้·ยังหารือกันอยู่ให้ตัดออก และในแต่ละบรรทัดที่ใส่ในตาราง
ให้ระบุชื่อไฟล์ด้วยว่ามาจากไฟล์ใด
verify
/context เพื่อดูโทเค็นที่เซสชันนี้ใช้ (คุณจะได้เห็นว่าอินพุตใหญ่กว่าที่คิด)ฉบับย่อสำหรับคนเดียว
ถ้าเพิ่งเริ่มใช้เครื่องมือ ให้เก็บแค่สองอย่างจากข้างบนนี้ก็พอ หนึ่ง มอบโน้ตทั้งโฟลเดอร์ไปและอย่าคัดลอก·วางเอง (มอบให้ลูปของฮาร์เนสทำ) สอง ตารางเอาต์พุตให้รับชื่อไฟล์มาพร้อมกันเสมอ แล้วเปิดต้นฉบับดูเฉพาะบรรทัดที่น่าสงสัย การเลือกโมเดลหรือการทำให้โทเค็นมองเห็นได้ ค่อยเพิ่มหลังจากคุ้นแล้วก็ไม่สาย แค่สองนิสัยนี้ — ไม่ขนข้อมูลด้วยมือ และเทียบเอาต์พุตกับต้นฉบับ — ครึ่งหนึ่งของการนำมาใช้ก็เข้าที่แล้ว
ผมเปิดเซสชันใหม่แล้วพิมพ์ว่า "มาดูการปรับสมดุลคูลดาวน์ของสกิลกันหน่อย" ก่อนจะกดเอนเทอร์ มีตัวอักษรสีเทาเล็ก ๆ บรรทัดหนึ่งวาบผ่านที่ส่วนล่างของหน้าจอ [memory injected: 2 atoms, 1,842 chars] แม้ผมจะยังไม่ได้เปิดไฟล์ใด ๆ เลย แต่เอกสารกฎคูลดาวน์ที่ผมตรึงเป็นกฎไว้เมื่อสัปดาห์ก่อนได้ถูกแนบเข้าไปไว้หน้าอินพุตของโมเดลเรียบร้อยแล้ว นี่คือสัญญาณแรกของสภาพแวดล้อมการทำงานที่มีโครงสร้างพื้นฐานวางอยู่ ทันทีที่เปิดเครื่องมือ เครื่องมือก็จดจำผมได้
เพื่อให้ฉากนี้เป็นไปได้ สามสิ่งต้องเข้าที่เข้าทางไว้ก่อน ได้แก่ สิ่งที่ AI จะจดจำ (หน่วยความจำ) สิ่งที่ AI ทำได้โดยไม่ต้องให้คนอนุมัติ (สิทธิ์) และสวิตช์กลางที่เปิด-ปิดทั้งสองอย่างนั้น (settings.json) เวลาที่ใช้ติดตั้งครั้งแรกอย่างมากก็แค่ราวหนึ่งชั่วโมง และหนึ่งชั่วโมงนั้นจะกลับคืนมาเป็นเวลาที่ประหยัดได้ทุกวันตลอด 6 เดือนหลังจากนั้น แทบจะเป็นการลงทุนที่ถอนคืนได้เต็มจำนวน
บทนี้คือการเดินตามทีละขั้น (walkthrough) ของ settings.json หนึ่งบรรทัดที่ผมรันจริงบนพีซีส่วนตัว บรรทัดนั้นเรียก inject_memory.py และไฟล์นั้นอ่าน _jit_manifest.json ตามลำดับ เมื่ออ่านจบ คุณจะสามารถชี้ด้วยมือได้ว่าคำว่า "หน่วยความจำถูกฉีดเข้าไปอัตโนมัติ" นั้นเกิดขึ้นที่บรรทัดไหนของไฟล์ไหน
ขอดูข้อสรุปก่อน สิ่งที่เปิดการฉีดหน่วยความจำอัตโนมัติบนพีซีส่วนตัวของผมคือบล็อกเพียงบล็อกเดียวภายใน settings.json
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python ~/.claude/hooks/inject_memory.py"
}
]
}
]
}
}
ถ้าจะแปลความหมายของบล็อกนี้ออกมาเป็นภาษาคน ก็คือ "ทุกครั้งที่เกิดเหตุการณ์ที่ผู้ใช้ส่งพรอมต์ (UserPromptSubmit) ให้รันสคริปต์ Python ชื่อ inject_memory.py หนึ่งครั้ง" แค่นั้น ไม่ใช่ว่า AI ฉลาดจนจดจำได้เอง แต่เป็นโครงสร้างที่ทุกครั้งที่มีอินพุตเข้ามา สคริปต์ที่คนลงทะเบียนไว้จะแทรกตัวเข้ามาทำงานหนึ่งครั้ง
settings.json คือไฟล์กลางที่ควบคุมการทำงานทั้งหมดของ Claude Code และแบ่งออกเป็นสองชั้น
~/.claude/settings.json — ระดับโกลบอล ใช้กับทุกเซสชัน เป็นการตั้งค่าที่แชร์กับทีมได้~/.claude/settings.local.json — ระดับโลคัล ใช้กับพีซีเครื่องนี้เท่านั้น เป็นการตั้งค่าเฉพาะของพีซีส่วนตัวทั้งสองจะถูกผสาน (merge) เข้าด้วยกันแล้วนำไปใช้ ด้วยเหตุนี้ผมจึงแยก hook และสิทธิ์ที่ต้องแชร์กับทีมไว้ใน settings.json ส่วนพาธสัมบูรณ์หรือพาธเครื่องมือส่วนตัวที่ใช้บนพีซีบ้านเครื่องนี้เท่านั้นก็แยกไว้ใน settings.local.json เป็นการแบ่งแยกเพื่อหลีกเลี่ยงการชน (conflict) ของ git ตอนทำงานร่วมกัน และป้องกันอุบัติเหตุที่การตั้งค่าส่วนตัวรั่วไหลเข้าไปใน repository ของทีม
นอกจาก hook แล้ว ยังมีรายการอื่นที่เข้าไปตั้งบ่อยอีกหลายอย่าง
effortLevel — ความลึกในการให้เหตุผลของโมเดล มี low / medium / high งานที่ต้องใช้การตัดสินใจเชิงลึกอย่างการออกแบบเอกสารออกแบบเกม ก็ตั้งเป็น highpermissions — ขอบเขตของคำสั่งที่ AI สามารถรันได้โดยไม่ต้องขออนุมัติ (รายละเอียดใน 1.3.4)enabledPlugins — รายการปลั๊กอินที่เปิดใช้งานตรงนี้มีนิสัยในการปฏิบัติงานที่สำคัญที่สุดเพียงข้อเดียว settings.json พิมพ์ผิดแค่ตัวเดียวก็อาจทำให้เครื่องมือเปิดไม่ขึ้นได้เลย แค่ลืมใส่จุลภาคของ JSON ตัวเดียว การแจงไวยากรณ์ (parsing) ก็พังแล้ว ฉะนั้นการสำรองข้อมูลก่อนแก้ไขจึงเป็นสิ่งจำเป็น บนพีซีของผมก็มีไฟล์สำรองแบบนี้เก็บไว้จริง
settings.json.bak_2026-05
settings.local.json.bak_2026-05
ถ้าทำสำเนาไว้หนึ่งชุดพร้อมต่อท้ายด้วยวันที่ การย้อนกลับ (rollback) ก็ใช้เวลาแค่ 1 วินาที ถ้าจัดการด้วย git ได้ก็ยิ่งดี เหมือนกุญแจเก่าหนึ่งชุดในลิ้นชัก ปกติไม่ได้ใช้ แต่จะมีสักจังหวะที่จำเป็นต้องใช้แน่ ๆ เมื่อยืนอยู่หน้าประตูที่ถูกล็อก
ทีนี้เราเข้าไปในสคริปต์ที่ settings.json เรียกใช้ นี่คือกระดูกสันหลังของ walkthrough โค้ดมีประมาณ 100 บรรทัด แต่แก่นอยู่ที่ห้าการกระทำ
flowchart TD
A["เซสชัน: ผู้ใช้ส่งพรอมต์\nเช่น 'มาดูการปรับสมดุลคูลดาวน์ของสกิลกัน'"] --> B["UserPromptSubmit hook ทำงาน\nsettings.json รัน inject_memory.py"]
B --> C["โหลด _jit_manifest.json\nเมตาดาตาของ atom 17 ตัว"]
C --> D["เรียงลำดับจากมากไปน้อยตาม score\n→ ลองจับคู่ regex ทีละ atom"]
D --> E{"มี atom ที่\nจับคู่ได้หรือไม่?"}
E -->|ไม่มี| Z["ไม่ฉีดอะไรเลย\nexit 0"]
E -->|มี| F["เลือกเฉพาะ max_matches(3) ตัวบนสุด"]
F --> G["ถ้ารวมกันเกิน 6,000 ตัวอักษร\nก็ truncate"]
G --> H["แนบเนื้อหา atom\nไว้หน้าอินพุตของผู้ใช้"]
H --> I["โมเดลรับ atom + พรอมต์\nพร้อมกัน → ตอบกลับ"]
D -.->|ข้อยกเว้นใด ๆ ก็ตาม| Z2["กลืนข้อยกเว้นทุกกรณี\nexit 0 (ไม่ขวางการทำงาน)"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
class A human
class B,C,D,E,F,G,H code
class I ai
ถ้าจะอธิบายห้าการกระทำให้กระจ่าง ก็เป็นดังนี้
1) อ่าน manifest ก่อนอื่นสคริปต์จะเปิด ~/.claude/projects/C--Users-user/memory/_jit_manifest.json ไฟล์นี้รวบรวมเมตาดาตาของ atom (ชื่อ พาธ regex สำหรับจับคู่ และคะแนน) ไว้ บนพีซีส่วนตัวของผมในตอนนี้มี atom ลงทะเบียนไว้ 17 ตัว
2) เรียงลำดับจากมากไปน้อยตาม score ทุก atom มีค่า score กำกับ atom ที่คะแนนสูงกว่าจะถูกลองจับคู่ก่อน เวลามีหลาย atom เข้าเงื่อนไขด้วยคีย์เวิร์ดเดียวกัน คะแนนนี้เป็นตัวตัดสินว่าใครได้สิทธิ์ก่อน
3) จับคู่ด้วย regex เปรียบเทียบสตริงอินพุตของผู้ใช้กับแพตเทิร์น regex ของแต่ละ atom ถ้ามีคำว่า "คูลดาวน์" อยู่ในอินพุต atom ที่มีแพตเทิร์น 쿨다운|cooldown|GCD ก็จะเข้าเงื่อนไข การเปรียบเทียบไม่แยกตัวพิมพ์ใหญ่-เล็ก
4) ตัดให้เหลือไม่เกิน 3 ตัว ต่อให้จับคู่ได้มากแค่ไหน ถ้าเกิน max_matches (สภาพแวดล้อมของผมตั้งไว้ที่ 3) ก็จะเก็บไว้แค่ 3 ตัวบนสุด และถ้าความยาวรวมของเนื้อหา atom ที่ถูกเลือกเกิน 6,000 ตัวอักษร ก็จะ truncate เป็นกลไกความปลอดภัยสองชั้นที่ป้องกันไม่ให้อินพุตบวมเกินไป
5) จบด้วย exit 0 ไม่ว่าจะเกิดข้อยกเว้นใด นี่คือแก่นของการออกแบบ ไม่ว่า manifest จะพัง ไฟล์จะหาย หรือ regex จะผิด สคริปต์ก็จะกลืนข้อยกเว้นไว้เงียบ ๆ แล้วจบด้วยโค้ดสถานะ 0 เพราะถ้า hook ตายด้วยโค้ดที่ไม่ใช่ 0 พรอมต์ของผู้ใช้เองก็อาจถูกขวางได้ หลักการที่ว่า "ต่อให้การฉีดหน่วยความจำล้มเหลว ก็จะไม่ขวางการทำงานของผู้ใช้เด็ดขาด" ถูกบันทึกไว้ที่ try/except ชั้นนอกสุดของโค้ด
จุดศูนย์ถ่วงอยู่ที่ข้อ 4 และข้อ 5 ข้อ 4 (เพดานบน) ป้องกันไม่ให้หน่วยความจำทำให้โทเค็นระเบิด ส่วนข้อ 5 (กลืนข้อยกเว้น) ป้องกันไม่ให้โครงสร้างพื้นฐานมาขัดขวางการทำงาน ทั้งสองเป็นสองด้านของปรัชญาเดียวกันที่ว่า "ระบบอัตโนมัติต้องไม่ทำให้คนรู้สึกรุงรัง"
ใน 1.2 ผมได้สัญญาถึงหลักการประหยัดโทเค็นที่ว่า "เอาเฉพาะข้อมูลที่จำเป็น เฉพาะไม่กี่ตัวบนสุด และถ้าล้มเหลวก็เงียบ ๆ" แล้วเลื่อนรายละเอียดการนำไปปฏิบัติมาไว้ในบทนี้ รายละเอียดนั้นอยู่ใน manifest ที่ inject_memory.py อ่าน นี่คือหัวใจของ JIT (Just-In-Time วิธีดึงข้อมูลมาเฉพาะตอนที่จำเป็น) และเอนทรีของ atom หนึ่งตัวมีหน้าตาดังนี้
{
"atoms": [
{
"name": "combat_cooldown_rule_v2",
"path": "atoms/combat/combat_cooldown_rule_v2.md",
"regex": "쿨다운|cooldown|GCD",
"score": 80
},
{
"name": "user_health",
"path": "memory/user_health.md",
"regex": "건강|복약|컨디션|약물",
"score": 95
}
],
"config": {
"max_matches": 3,
"case_insensitive": true
}
}
สี่ฟิลด์ที่นิยาม atom หนึ่งตัว
name — ชื่อเฉพาะของ atompath — พาธของไฟล์ที่จะอ่านเนื้อหามาเมื่อจับคู่ได้regex — แพตเทิร์นที่นิยามว่าถ้าคีย์เวิร์ดใดเข้ามาในอินพุตจะปลุก atom นี้score — ลำดับความสำคัญในการเรียง ยิ่งสูงยิ่งถูกจับคู่ก่อนและได้ที่นั่งก่อนmax_matches: 3 ในบล็อก config คือที่มาของเพดาน "ไม่เกิน 3 ตัว" ที่เห็นใน 1.3.2 ถ้าแก้ manifest ด้วยมือ การทำงานก็เปลี่ยนทันที
ขอชี้ให้เห็นความรู้สึกเรื่องสเกลสักเรื่อง พีซีส่วนตัว ของผมดำเนินงานอย่างเบาบางด้วย atom 17 ตัวและ manifest เพียงหนึ่งชุด ในทางกลับกัน สภาพแวดล้อมการทำงานจริงในบริษัท (โปรเจกต์ A) นั้น เมื่ออ้างอิงข้อมูลสำรอง ณ เดือนพฤษภาคม 2026 มี atom ของทีมลงทะเบียนไว้ 304 ตัว และ skill 48 ตัว มี Hot atom ตัวหนึ่งที่ score ไต่ขึ้นไปถึง 356.53 (อยู่ในตระกูล view_html_filename_convention ที่ว่าด้วยกฎการตั้งชื่อไฟล์) ซึ่งไม่ได้สูงมาตั้งแต่ต้น แต่เป็นร่องรอยที่สะสมขึ้นจากการถูกเรียกซ้ำและถูกตรวจสอบ
สิ่งที่ความต่างระหว่าง 17 ตัวบนพีซีส่วนตัวกับ 304 ตัวในบริษัทบอกเราก็คือ ต่อให้เป็นกลไก JIT เดียวกัน ความเร็วและขนาดที่ข้อมูลสะสมขึ้นก็แปรผันตามความหนาแน่นของโปรเจกต์ ไม่จำเป็นต้องสร้าง 304 ตัวตั้งแต่ต้น เริ่มจาก atom หลักห้าตัว แล้วตรึงเป็นกฎเพิ่มสัปดาห์ละหนึ่งถึงสองตัว ไม่ทันไร manifest ก็จะหนาขึ้นเอง
การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ): คำอธิบายที่ว่า score สะสมขึ้นตามจำนวนครั้งที่จับคู่และถูกตรวจสอบ เป็นการตีความที่อิงจากแพตเทิร์นการดำเนินงาน สูตรการคิดคะแนนเองนั้นแปรเปลี่ยนไปตามการออกแบบ manifest ของแต่ละสภาพแวดล้อม ฉะนั้นค่าสัมบูรณ์อย่าง 356.53 ข้างต้นจึงเป็นเพียงภาพถ่าย ณ ขณะนั้นที่วัดได้จริงในสภาพแวดล้อมของผู้เขียน ไม่ใช่มาตรฐานทั่วไป
ตรงนี้ขอย้ำหลักการที่ว่าหน่วยความจำแบ่งเก็บเป็นสองชั้นอีกครั้ง
| ประเภท | ตำแหน่ง | โหลดเมื่อใด | การใช้งาน |
|---|---|---|---|
| โกลบอล | ~/.claude/memory/ |
ทุกเซสชัน | ตัวตนของตนเอง กฎการทำงานร่วมกัน การตั้งค่าภาษา |
| โปรเจกต์ | ~/.claude/projects/<โปรเจกต์>/memory/ |
เซสชันของโปรเจกต์นั้น | atom กฎ และข้อมูลเฉพาะของแต่ละโปรเจกต์ |
การเก็บโกลบอลให้เบาไว้ปลอดภัยกว่า เพราะถ้าโกลบอลหนักขึ้น น้ำหนักนั้นจะสะสมเป็นต้นทุนโทเค็นในทุกเซสชัน ถ้าเปรียบกับสำนักงาน โกลบอลก็คือสมุดนามบัตรบนโต๊ะ (ยิ่งเบายิ่งหยิบใช้สะดวกทุกวัน) ส่วนหน่วยความจำของโปรเจกต์คือแฟ้มในตู้ข้าง ๆ (ต่อให้หนาขึ้นต่อโปรเจกต์ก็ไม่เป็นภาระในยามปกติ) ด้วยเหตุนี้ในโกลบอลที่ถูกโหลดอัตโนมัติจึงควรเก็บแค่แก่น ส่วนข้อมูลที่อุดมสมบูรณ์ให้สะสมไว้ในหน่วยความจำของโปรเจกต์ แล้วค่อยปลุกด้วย JIT เฉพาะตอนที่จำเป็น
ทีนี้มาถึงแกนที่สามของโครงสร้างพื้นฐาน คือสิทธิ์ Claude Code สามารถลบไฟล์ รันคำสั่ง และเรียก API ภายนอกได้ ความทรงพลังมาพร้อมความเสี่ยง ระบบสิทธิ์คือสิ่งที่บริหารจัดการความเสี่ยงนั้น
สิทธิ์แบ่งออกเป็นสองชนิด คือชนิดที่รันอัตโนมัติโดยไม่ต้องให้คนอนุมัติ กับชนิดที่ต้องขออนุมัติทุกครั้ง จะเอาอะไรไว้ฝั่งไหนนิยามได้ในบล็อก permissions ของ settings.json
{
"permissions": {
"allow": [
"Bash(ls:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Read(*)",
"Grep(*)"
],
"deny": [
"Bash(rm -rf:*)",
"Bash(git push --force:*)"
]
}
}
ตรงนี้ต้องเปลี่ยนมุมมอง รายการ allow นี้ไม่ใช่แค่ค่าตั้งค่าธรรมดา แต่เป็น ร่องรอยของการสะสมงาน ตอนแรกก็แค่อนุญาตอัตโนมัติให้อ่านและค้นหาเท่านั้น และแทบจะว่างเปล่า แต่พอทำงานเดิมซ้ำ ๆ ไปสักหนึ่งถึงสองเดือน ก็จะเกิดแพตเทิร์นที่รู้สึกว่า "คำสั่งนี้ต้องกดอนุมัติทุกครั้งมันน่ารำคาญ" แล้วก็ค่อย ๆ ย้ายมันเข้าไปใน allow ทีละอย่าง รายการที่ยาวขึ้นก็คือลายนิ้วมือที่บอกว่าผมใช้เครื่องมือนี้ทำอะไรซ้ำ ๆ มาบ้าง
สภาพแวดล้อมการทำงานในบริษัทของผม (โปรเจกต์ A) มีแพตเทิร์นอนุญาตอัตโนมัติอยู่ราว 80 รายการ เริ่มจาก 20 รายการ แล้วเพิ่มขึ้นมาอีก 60 รายการตลอด 6 เดือน ซึ่งถ้าอ่าน 60 รายการนั้นย้อนกลับ ก็จะเผยว่าผมทำงานอะไรซ้ำ ๆ มาตลอดครึ่งปีที่ผ่านมา การดึงข้อมูลจากชีต การสร้างผังความสัมพันธ์ การจัดทำเอกสารสคีมา — เครื่องมือที่ใช้บ่อยก็คือสิทธิ์ที่อนุญาตบ่อยนั่นเอง
การดำเนินงานเรื่องสิทธิ์มีสี่แพตเทิร์นที่เข้าที่เข้าทาง
rm -rf หรือ git push --force ให้ใส่ไว้ใน deny ต่อให้ขยายการอนุญาตอัตโนมัติแค่ไหน สองตัวนี้ก็อย่าไปแตะallow ใหม่ แล้วถอดสิทธิ์ที่เลิกใช้แล้วออก ร่องรอยถ้าเอาแต่สะสมก็จะกลายเป็นสัญญาณรบกวน (noise)ถ้ามีป็อปอัปขออนุมัติเด้งขึ้นมาทุกครั้ง คนก็เหนื่อย จึงมีกลไกลดความเหนื่อยล้าด้วย เช่น ใช้คำสั่งสแลชอย่าง fewer-permission-prompts เพื่อลงทะเบียนแพตเทิร์นที่ปรากฏบ่อยทีเดียวทั้งชุด หรือให้สิทธิ์ชั่วคราวเฉพาะภายในเซสชันเดียว หรือใช้โหมดอนุญาตอัตโนมัติเต็มสิทธิ์เฉพาะกับงานส่วนตัวเท่านั้น อย่างไรก็ตาม ตัวเลือกสุดท้ายไม่แนะนำในสภาพแวดล้อมแบบทีม
ความสมดุลระหว่างความเหนื่อยล้ากับความปลอดภัยนั้นตัวคุณเองเป็นคนปรับ ถ้าเข้มงวดเกินไปงานก็ไม่เดิน ถ้าหละหลวมเกินไปก็เกิดอุบัติเหตุ ต่อให้เริ่มแบบหละหลวม แต่ถ้าวางรอบการจัดระเบียบรายไตรมาสไว้ ความสมดุลก็จะลงตัวได้เอง
ที่ผ่านมาเราดูสามแกน (settings · หน่วยความจำ · สิทธิ์) มาแล้ว ทีนี้มาคลี่ดูว่าทั้งสามทำงานพร้อมกันอย่างไรในเซสชันเดียว โดยยึดอินพุตหนึ่งบรรทัดเป็นเกณฑ์ ต่อไปนี้คือภาพตัดขวางของสิ่งที่เกิดขึ้นจริงเมื่อพิมพ์ว่า "มาดูการปรับสมดุลคูลดาวน์ของสกิลกัน"
สามเลนมาบรรจบกันที่อินพุตเดียว settings.json ปลุก hook ขึ้นมา hook คัดเลือกหน่วยความจำมาแนบไว้กับอินพุต และเมื่อคำตอบที่สร้างขึ้นเช่นนั้นไปเรียกใช้เครื่องมือ สิทธิ์ก็ทำงานเป็นด่านตรวจสอบสุดท้าย ผู้ใช้แค่พิมพ์ว่า "มาดูคูลดาวน์กัน" บรรทัดเดียว แต่โครงสร้างพื้นฐานทั้งสามกลับทำงานตามลำดับอยู่ในที่ที่มองไม่เห็น นี่คือโครงสร้างภายในของช่วงเวลาที่เครื่องมือรู้สึกเหมือนเป็น "เครื่องมือของผม"
เมื่อดูทฤษฎีแล้ว ก็ถึงเวลาขยับมือ หลังติดตั้ง Claude Code ครั้งแรก ใช้เวลาหนึ่งชั่วโมงก็วางภาพทั้งหมดข้างต้นลงบนพีซีของตัวเองได้ แบ่งเป็นห้าช่วง
0\~10 นาที ยืนยันการติดตั้งและการรัน หลังติดตั้งแล้วให้รัน Claude Code จากเทอร์มินัล ลองถามในโฟลเดอร์หนึ่งว่า "ในโฟลเดอร์นี้มีอะไรบ้าง?" แล้วดูคำตอบ เริ่มจากดูว่าเครื่องมือยังมีชีวิตอยู่หรือไม่ก่อน
10\~25 นาที เขียนหน่วยความจำโกลบอลสามไฟล์ โกลบอลที่ถูกโหลดอัตโนมัตินั้นมีสามไฟล์ก็เพียงพอ
MEMORY.md (5 บรรทัด) — ตัวตนของตนเองหนึ่งบรรทัด + ตัวชี้ไปยังไฟล์อื่นuser-profile.md (20\~30 บรรทัด) — ชื่อ บทบาท สาขาความเชี่ยวชาญ ช่องทางติดต่อfeedback-collaboration-style.md (20\~30 บรรทัด) — กฎการทำงานร่วมกัน เช่น ภาษา น้ำเสียง การให้ความสำคัญกับการลงมือทำก่อน และคำอธิบายที่กระชับจะคัดลอกตัวอย่างของผู้เขียนไปเริ่มต้นเลยก็ได้ แล้วค่อยปรับแต่งระหว่างใช้งาน
25\~40 นาที ตั้งค่า settings.json เบื้องต้น ตั้ง effortLevel เป็น high ใส่ชุดสิทธิ์เริ่มต้น (อ่านและค้นหาให้อัตโนมัติ เขียนและลบต้องอนุมัติ) และทำสำเนาสำรองไว้หนึ่งชุด (settings.json.bak_<วันที่>) การสำรองข้อมูลคือบรรทัดที่สำคัญที่สุดในช่วงนี้
40\~55 นาที atom ห้าตัวแรกของโปรเจกต์ เอาการตัดสินใจที่ตัวเองลืมอยู่ทุกที และข้อมูลที่ถามบ่อยห้าอย่างมาทำเป็น atom โฟลเดอร์คือ ~/.claude/projects/<โปรเจกต์>/memory/ ส่วนรูปแบบให้ดูบทที่ 5 ห้าตัวนี้แม้ยังไม่ทำ JIT manifest ในทันที แค่การโหลดอัตโนมัติของโกลบอลก็ให้ผลแล้ว
55\~60 นาที ทดสอบหนึ่งครั้ง เปิดเซสชันใหม่แล้วโยนคำถามในสาขาของตัวเองสักหนึ่งข้อ ดูว่าหน่วยความจำโกลบอลถูกโหลดอัตโนมัติหรือไม่ และน้ำเสียงของคำตอบเป็นไปตามกฎการทำงานร่วมกันของตัวเองหรือไม่
ถึงตรงนี้ก็หนึ่งชั่วโมง JIT manifest และ hook จะติดตั้งตอนที่ atom เริ่มเกิน 50 ตัว นั่นคือตอนที่การโหลดอัตโนมัติเริ่มหนัก ก็ยังไม่สาย ณ จุดนั้นค่อยติดตั้ง inject_memory.py ของ 1.3.2 ก็ได้
ความผิดพลาดที่เกิดซ้ำในช่วงเริ่มนำมาใช้สามารถจัดกลุ่มได้ห้าอย่าง และแต่ละอย่างยืนอยู่บนต้นเหตุของอุบัติเหตุเดียวกัน
| ความผิดพลาด | ต้นเหตุของอุบัติเหตุ | วิธีเลี่ยง |
|---|---|---|
| ใส่ข้อมูลในโกลบอลมากเกินไป | ทุกเซสชันหนักขึ้นและสิ้นเปลืองโทเค็น | โกลบอลไม่เกิน 5KB รายละเอียดย้ายไปหน่วยความจำของโปรเจกต์ |
| อนุญาตทุกสิทธิ์อัตโนมัติ | จุดเริ่มที่ความสะดวกบดบังความเสี่ยง | อ่านและค้นหาให้อัตโนมัติเท่านั้น เขียนและลบต้องอนุมัติ (รวมการจัดระเบียบรายไตรมาส) |
| แก้ settings โดยไม่สำรอง | settings ที่พังทำให้เครื่องมือเปิดไม่ขึ้น | บันทึก settings.json.bak_<วันที่> อัตโนมัติก่อนแก้ไข |
| ใส่ atom ลงโฟลเดอร์หน่วยความจำไม่จำกัด | การโหลดอัตโนมัติกดดันเพดานโทเค็น | นำ JIT manifest มาใช้ตั้งแต่ราว 50 ตัว |
| ปนการตั้งค่าทีมกับส่วนตัวไว้ในไฟล์เดียว | git ชนกัน · การตั้งค่าส่วนตัวรั่วไหล | ทีมใช้ settings.json ส่วนตัวใช้ settings.local.json |
ไม่จำเป็นต้องเลี่ยงทั้งห้าตั้งแต่วันแรก เรื่องโกลบอลบวมและการลืมสำรองข้อมูลควรวางแพตเทิร์นการเลี่ยงไว้ภายในหนึ่งชั่วโมงแรก ส่วนอีกสามอย่างที่เหลือ จะเป็นธรรมชาติกว่าถ้าค่อยใส่กลไกเลี่ยงไว้ตรงจุดที่ตัวเองมีโอกาสเกิดอุบัติเหตุสูง ระหว่างที่ดำเนินงานไปสักหนึ่งเดือน
1.1 เป็นที่ที่ลดระยะห่างที่มีต่อเครื่องมือ 1.2 เป็นที่ที่จับกลไกการทำงานขั้นต่ำของเครื่องมือนั้น และ 1.3 เป็นที่ที่วางโครงสร้างพื้นฐานชุดแรกด้วยหน่วยความจำ สิทธิ์ และ settings สามบทนี้ถือเป็นบทนำของหนังสือ เมื่อทำถึงตรงนี้จบ โครงกระดูกพื้นฐานสำหรับดำเนินงานเครื่องมือโดยไม่สะดุดก็เข้าที่เข้าทาง
แก่นคือ โครงกระดูกนี้ไม่ใช่การตั้งค่าที่อยู่นิ่ง atom ใน manifest เพิ่มขึ้นทุกสัปดาห์ รายการ allow ยาวขึ้นตามร่องรอยการทำงาน และ score สะสมขึ้นผ่านการตรวจสอบ โครงสร้างพื้นฐานไม่ได้เสร็จสมบูรณ์ในวินาทีที่วางลง แต่เติบโตไปพร้อมกับผู้ใช้บนสิ่งที่วางไว้ ความต่างที่ 17 ตัวบนพีซีส่วนตัวขยายไปสู่ 304 ตัวในบริษัท คือระยะทางของการเติบโตนั้น
ตั้งแต่ส่วนที่ 2 เป็นต้นไป เราจะเข้าสู่สถาปัตยกรรมข้อมูลอย่างจริงจัง บทที่ 4 YAML frontmatter บทที่ 5 Atom บทที่ 6 Layer และบทที่ 7 ออนโทโลยี จะตามมาตามลำดับ atom ที่ใน 1.3 เห็นเพียงในฐานะเอนทรีหนึ่งของ manifest จะกลายเป็นตัวเอกของทั้งบทใน 2.2 ต่อเมื่อจับกระดูกสันหลังได้แล้ว บทแยกตามสาขาแต่ละบทจึงจะหาที่ทางของตัวเองได้บนพิกัดเดียวกัน
setup
1. เปิด ~/.claude/settings.json แล้วทำสำเนาสำรองเป็น settings.json.bak_<วันที่วันนี้> ก่อนแก้ไข
2. ใส่ Read(*), Grep(*), Bash(ls:*), Bash(git status:*) ลงใน permissions.allow และใส่ Bash(rm -rf:*), Bash(git push --force:*) ลงใน permissions.deny
3. (เมื่อมี atom ตั้งแต่ 50 ตัวขึ้นไป) ลงทะเบียน python ~/.claude/hooks/inject_memory.py ไว้ใน hooks.UserPromptSubmit แล้วเขียนเอนทรีของ atom (name · path · regex · score) และ config.max_matches: 3 ลงใน _jit_manifest.json
prompt - ในเซสชันใหม่ ให้โยนคำถามที่จงใจใส่คีย์เวิร์ดของ atom ตัวใดตัวหนึ่งใน manifest เช่น "มาดูการปรับสมดุลของสกิลโดยอิงตามกฎคูลดาวน์กัน"
verify
- ทันทีหลังป้อนอินพุต ให้ดูว่ามีสัญญาณอย่าง [memory injected: N atoms] ขึ้นมาหรือไม่
- ดูว่า atom ที่ตั้งใจไว้สะท้อนอยู่ในคำตอบหรือไม่
- ลองจงใจใส่ JSON ที่พังลงใน manifest แล้วตรวจว่าพรอมต์ยังไม่ถูกขวางและทำงานได้อยู่ (รับประกัน exit 0) หลังจากนั้นค่อยย้อนกลับคืน
ฉบับย่อสำหรับคนเดียว
- เริ่มต้นโดยไม่มีทั้ง hook และ manifest เขียนแค่ตัวตน 3 บรรทัด + กฎการทำงานร่วมกัน 3 บรรทัดลงในไฟล์โกลบอล MEMORY.md เพียงไฟล์เดียว และตั้งสิทธิ์ให้อนุญาตอัตโนมัติแค่ Read(*) · Grep(*) เท่านั้น เมื่อ atom เริ่มเข้ามือและใกล้แตะ 50 ตัว ตอนนั้นค่อยวาง hook ของ 1.3.2 ลงไป โครงสร้างพื้นฐานคือสิ่งที่เริ่มจากเล็ก ๆ แล้วเลี้ยงให้โตตามร่องรอย ไม่ใช่การมี 304 ตัวพร้อมตั้งแต่ต้น
คืนก่อนวันสร้างบิลด์มาตรฐานสำคัญ (milestone build) เพียงหนึ่งวัน เพื่อนร่วมทีม A ซึ่งเป็น System Designer ถามผมผ่านแชตภายในทีมว่า "สัปดาห์นี้มีเอกสารที่แตะเส้นโค้งรางวัล (reward curve) กี่ฉบับ แล้วที่ตรวจเสร็จแล้วไปถึงไหนแล้ว" ผมไม่รู้คำตอบ เอกสารอยู่ที่ใดที่หนึ่งในโฟลเดอร์ ส่วนข้อมูลว่าใครเป็นคนแก้ครั้งสุดท้าย เป็นของมาตรฐานสำคัญรอบไหน กระจัดกระจายอยู่ในความทรงจำของแต่ละคนและในข้อตกลงเรื่องการตั้งชื่อไฟล์ สิ่งที่เราทำในคืนนั้นคือสร้างข้อตกลงให้ใส่หกบรรทัดไว้ที่ต้นเอกสาร และหกบรรทัดนั้นเองที่ทำให้ตั้งแต่มาตรฐานสำคัญรอบถัดไป คำถามของเพื่อนร่วมทีม A สามารถตอบได้โดยที่คนไม่ต้องเปิดโฟลเดอร์เลย
YAML ไม่กี่บรรทัดที่เขียนไว้ระหว่าง --- ด้านบนสุดของเอกสาร เราเรียกสิ่งนี้ว่า frontmatter ข้อตกลงนี้บอกทั้งคนและเครื่องพร้อมกันว่า "เอกสารนี้คืออะไร" โดยไม่ต้องอ่านเนื้อหาแม้แต่ตัวอักษรเดียว บทนี้จะตามรอยผ่านสคริปต์ที่ทำงานได้จริงว่า บรรทัดเดียวนั้นกลายเป็นพิกัดทางเข้าของสถาปัตยกรรมข้อมูล (information architecture) ทั้งระบบได้อย่างไร
ขอปูพื้นคำศัพท์ไว้เพียงคำเดียวก่อน หนังสือเล่มนี้แบ่งเอกสารออกแบบออกเป็นห้า Layer (จะกล่าวถึงอย่างจริงจังในบทที่ 6) L0 = โลกของเกม·คอนเซปต์, L1 = กฎของระบบ, L2 = เนื้อหา, L3 = ข้อมูล, L4 = พิกัดการ implement ทิศทางปกติคือพึ่งพาจากบนลงล่าง บรรทัด layer: 2 ที่ปรากฏในหัวข้อถัดไปคือการประกาศพิกัดว่า "เอกสารนี้คือ Layer เนื้อหา"
เอกสารออกแบบเกมแบบดั้งเดิมอยู่บน Word, PPT และ Google Docs มาโดยตลอด เนื้อหาถูกปรับให้เหมาะกับการอ่านของคน แต่ข้อมูลเชิงเมตา (meta information) อย่างประเภท·ความรับผิดชอบ·สถานะ·ตำแหน่งของเอกสาร กลับฝังอยู่ในเนื้อหา หรือไม่ก็ขึ้นอยู่กับโครงสร้างโฟลเดอร์และข้อตกลงเรื่องชื่อไฟล์ ดังนั้นถ้าจะรู้ว่า "เอกสารนี้เป็นของมาตรฐานสำคัญรอบไหน ใครเป็นผู้รับผิดชอบ และตรวจครั้งสุดท้ายเมื่อไหร่" ก็ต้องเปิดเนื้อหาขึ้นมาดู
ตรงนี้มีข้อจำกัดสองอย่างซ้อนกันอยู่ ข้อแรก เอกสารไม่บอกตัวตนของตัวเอง ตัวตนอยู่ในความทรงจำของคนและในข้อตกลงเรื่องโฟลเดอร์ ซึ่งข้อตกลงเหล่านั้นจะผุกร่อนเมื่อเวลาผ่านไป ข้อสอง AI ไม่มีเบาะแสที่จะอนุมานบริบทได้ ถ้าสั่ง Claude Code ว่า "ช่วยตรวจเอกสารนี้ให้หน่อย" มันก็จะอ่านเนื้อหาตั้งแต่ต้นจนจบ สิ้นเปลืองโทเค็น และยังไม่รู้ด้วยว่าขอบเขตความรับผิดชอบไปถึงไหน
YAML frontmatter แก้ทั้งสองข้อนี้พร้อมกัน ถ้าใส่เมตาดาตาไว้อย่างชัดเจนที่ต้นเอกสาร ทั้งคนและเครื่องก็ระบุเอกสารได้โดยไม่ต้องเปิดเนื้อหา เหมือนกับที่เมื่อมีป้ายติดอยู่หน้าลิ้นชักตู้เก็บเอกสาร เราก็รู้ว่าข้างในมีอะไรโดยไม่ต้องเปิดลิ้นชัก และป้ายนี้ไม่ได้เป็นเพียงเครื่องมือจัดหมวดหมู่เท่านั้น อย่างที่จะได้เห็นต่อไป ฟิลด์ layer เพียงฟิลด์เดียวกลายเป็นพิกัดทางเข้าของการสร้างแบบโพรซีเดอรัล (procedural generation) และการตรวจสอบอัตโนมัติ
แทนที่จะยกตัวอย่างนามธรรม เราจะดู frontmatter ที่เอกสารเส้นโค้งรางวัลของโปรเจกต์ A แบกไว้บนหัวจริง ๆ ตามที่เป็น (เปลี่ยนเฉพาะ ID·ชื่อจริงให้เป็นนามแฝง ส่วนโครงสร้างเป็นไปตามที่ใช้งานจริง)
---
title: "เส้นโค้งรางวัล เควสต์หลัก บทที่ 12"
layer: 2
status: review
owner: teammate_a
created: 2026-04-15
updated: 2026-05-20
related:
- quest_main_chapter12
- reward_curve_milestone_2
affects:
- L3_BalanceSheet_v2
ip_check: passed
---
# เส้นโค้งรางวัล เควสต์หลัก บทที่ 12
(เริ่มเนื้อหา)
หัวใจอยู่ที่การแยกส่วนเหนือและใต้ --- ด้านบนคือข้อมูลที่ parser อ่าน ด้านล่างคือเนื้อหาที่คนอ่าน โดยปกติตัว render มาร์กดาวน์จะซ่อน frontmatter ไว้ จึงไม่รบกวนเวลาอ่าน ไฟล์เดียวบรรจุทั้งข้อมูล (frontmatter) และเนื้อหา (body) ไว้ด้วยกัน จึงกลายเป็นแหล่งข้อมูลจริงเพียงแหล่งเดียว (single source of truth)
ต้องสังเกตสองบรรทัดนี้ให้ดี คือ layer: 2 และ affects: [L3_BalanceSheet_v2] นี่คือการประกาศว่า "เอกสารเนื้อหา (L2) ฉบับนี้ส่งผลต่อชีตปรับสมดุล (balance sheet) ใน Layer ข้อมูล (L3)" เพียงเท่านี้เครื่องมือก็วาดความสัมพันธ์การพึ่งพา L2→L3 เป็นกราฟได้โดยไม่ต้องดูเนื้อหา ในทางกลับกัน ถ้าเอกสารข้อมูล L3 อ้างถึงกฎของระบบ L1 ด้วย depends_on (เป็นการพึ่งพาย้อนทิศจากล่างขึ้นบน) นั่นคือกลิ่นของปัญหาในการออกแบบ และเครื่องมือจะตรวจจับการอ้างย้อนนั้นโดยอัตโนมัติ
เหตุผลที่ YAML เขียนด้วยมือง่ายกว่า JSON นั้นเรียบง่าย มันแสดงโครงสร้างด้วยการเยื้องบรรทัด (indentation) แทบไม่ต้องใช้เครื่องหมายคำพูด และใช้คอมเมนต์ # ได้ จึงเหมาะกับการให้นักออกแบบเกมกรอกเองโดยตรง
_NAMING_FRONTMATTER_STANDARDฟิลด์เพิ่มได้ไม่จำกัด แต่ยิ่งเพิ่ม ภาระในการเขียนก็ยิ่งมากและมาตรฐานก็ยิ่งพังทลาย ดังนั้นโปรเจกต์ A จึงแยกการใช้งานออกเป็นสองชั้น คือ ฟิลด์หลักขั้นต่ำที่ทุกเอกสารมีร่วมกัน และฟิลด์ขยายเฉพาะโดเมนตามแต่ละสาขา
ฟิลด์หลักขั้นต่ำที่ใช้ร่วมกันมีหกฟิลด์
| ฟิลด์ | รูปแบบ | การใช้งาน |
|---|---|---|
title |
สตริง | ชื่อที่คนอ่านได้ ต่างจากชื่อไฟล์ก็ได้ |
layer |
0\~4 | พิกัด Layer ตามบทที่ 6 |
status |
draft / review / approved / archived | สถานะเอกสาร |
owner |
ชื่อผู้ใช้ | ผู้รับผิดชอบ (1 คน) |
created |
YYYY-MM-DD | วันที่สร้าง |
updated |
YYYY-MM-DD | วันที่แก้ไขล่าสุด |
เพียงหกฟิลด์นี้ก็รู้ความสด·ความรับผิดชอบ·ตำแหน่งของเอกสารได้ทันที เดือนแรกให้อดทนต่อแรงกระตุ้นที่อยากใส่เพิ่ม เมื่อใช้งานไปสักพักก็จะเห็นได้เองว่าฟิลด์ไหนจำเป็นจริง ๆ
ฟิลด์ขยายเฉพาะสาขาต่างกันไปในแต่ละโดเมน System Designer ชอบใช้ depends_on·affects, ฝ่ายออกแบบการต่อสู้ใช้ combat_phase·anim_target, ฝ่ายเนื้อเรื่องใช้ world_region·chapter, ฝ่ายปรับสมดุลใช้ data_sheet·formula_id ฟิลด์ขยายเหล่านี้ห้ามกระจัดกระจายโดยอิสระ จึงต้องมีเอกสารมาตรฐานเพียงฉบับเดียวที่ตรึงชื่อทางการ·ค่าที่อนุญาต·ตัวอย่างไว้ตายตัว เอกสารฉบับนั้นคือ _NAMING_FRONTMATTER_STANDARD.md หากต้องการเพิ่มฟิลด์ใหม่ก็ต้องผ่านเอกสารนี้ และเอกสารมาตรฐานฉบับนี้เองก็ลงทะเบียนเป็น atom จึงได้รับการจัดการในสายเดียวกับกฎที่บังคับให้ใส่หมายเลข Layer ไว้หน้าชื่อเอกสาร (atom ชื่อ docs_layer_numeric_prefix_naming)
ตรงนี้เกิดการเปลี่ยนผ่านที่สำคัญ ถ้ามาตรฐานเป็นเพียงเอกสารที่คนอ่าน คนก็จะฝ่าฝืน แต่ถ้าทำมาตรฐานให้เป็น ข้อมูลที่เครื่องอ่าน เครื่องก็จะบังคับใช้มัน หัวข้อถัดไปคือโค้ดจริงของการเปลี่ยนผ่านนั้น
คราวนี้ผมให้ Claude Code สร้าง "Linter ที่ตรวจว่าเอกสารมาร์กดาวน์ทุกฉบับของโปรเจกต์ A ปฏิบัติตามมาตรฐาน frontmatter หรือไม่" ความต้องการหลักมีสองข้อ คือ ให้จับรายการตรวจสอบให้ได้ (ฟิลด์บังคับขาดหาย, ค่า status ที่ไม่เป็นมาตรฐาน, layer ละเมิดช่วง 0\~4, เอกสารที่เป็น review แต่ไม่ถูกแก้นานเกิน 90 วัน) และ อย่าฮาร์ดโค้ดค่าที่อนุญาตลงในโค้ด แต่ให้อ่านมาจากเอกสารมาตรฐาน การแยกส่วนนี้คือหัวใจ แก้มาตรฐานแล้วเกณฑ์การตรวจก็เปลี่ยนโดยไม่ต้องแก้โค้ด (สคริปต์ฉบับเต็มและขั้นตอนรันโดยตรง อยู่ใน «ลองทำดู» ท้ายบทนี้)
ตรงนี้เกิดเหตุการณ์หนึ่งขึ้น โค้ดที่ Claude เสนอมาในตอนแรกคำนวณผลต่างของวันที่ในการตรวจ STALE ด้วย today - fm["updated"] และเขียนคอมเมนต์ไว้ว่า "ถ้าเขียนแบบ updated: 2026-05-20 PyYAML จะ parse เป็น datetime.date ให้อัตโนมัติ" คำพูดนี้ถูกแค่ครึ่งเดียว เมื่อรันกับเอกสารจริง บางไฟล์ก็เกิด traceback ขึ้น
TypeError: unsupported operand type(s) for -: 'datetime.date' and 'str'
ต้นตออยู่ที่มือคน ผู้เขียนบางคนเขียน updated: 2026-05-20 (parse เป็น date) บางคนเขียน updated: "2026-05-20" โดยใส่เครื่องหมายคำพูด (parse เป็นสตริง) ตรงที่มาตรฐานไม่ได้ตรึงรูปแบบวันที่ไว้ มือคนก็แตกออกเป็นสองทาง และ Claude สมมติไว้เพียงทางเดียว ผมปฏิเสธโค้ดนั้นและขอใหม่ว่า "ให้ normalize ทั้งสองแบบเป็น date อย่างปลอดภัย และให้กรองกรณีที่ไม่มี updated ด้วย" Claude จึงเสริมฟังก์ชันช่วย (helper) ที่ตรวจชนิดของอินพุตแล้ว normalize ทั้งคู่ให้เป็น datetime.date (บล็อกที่แก้แล้วก็ดูได้ใน «ลองทำดู»)
บทเรียนที่แท้จริงไม่ใช่บั๊กของโค้ด แต่คือเรื่องที่ว่า ตรงที่มาตรฐานไม่ได้ตรึงรูปแบบการเขียนวันที่ไว้ มือคนก็แตกออกเป็นสองทาง ผมจึงเพิ่มบรรทัด updated: YYYY-MM-DD (ไม่ใส่เครื่องหมายคำพูด) ลงใน _NAMING_FRONTMATTER_STANDARD.md กลายเป็นว่า Linter กำลังตรวจโค้ดอยู่ แต่กลับเผยให้เห็นรูโหว่ของมาตรฐานซึ่งเป็นสิ่งที่มันใช้ตรวจเสียเอง
ผลลัพธ์แรกของสคริปต์ที่แก้แล้วก็ยังไม่สะอาด ผมขอคงผลลัพธ์ที่รกออกมาจริง ๆ ไว้ตามนั้น
[NO-FM] manuscript/legacy/old_combat_notes.md
[MISSING] manuscript/system/quest_flag_table.md: layer
[STATUS] manuscript/content/town_intro.md: WIP
[LAYER] manuscript/balance/dps_v2.md: None
[STALE] manuscript/system/inventory_rules.md: 134d
ห้าบรรทัดนี้คือสถานะจริงของทีมในช่วงเริ่มนำมาใช้ เอกสารเก่าไม่มี frontmatter เลย (NO-FM), บางเอกสารตกหล่น layer, บางคนใช้ค่าที่ไม่เป็นมาตรฐานอย่าง status: WIP, เอกสารปรับสมดุลฉบับหนึ่งปล่อย layer เป็น None, และเอกสารกฎของระบบฉบับหนึ่งหลับอยู่ในสถานะ review มา 134 วันแล้ว มาตรฐานไม่เคยถูกปฏิบัติตามตั้งแต่แรก Linter เพียงแค่เผยข้อเท็จจริงนั้นออกมาให้เห็นทุกเช้าเท่านั้นเอง
ถ้าบีบอัดบันทึกเซสชันจริงข้างต้นให้เป็นแผนภาพการไหลภาพเดียว จะได้ตามนี้ มันแสดงให้เห็นว่าบรรทัดเดียวที่คนเขียนไหลไปจนถึงด่านตรวจสอบของเครื่องได้อย่างไร
flowchart TD
A["ผู้เขียน: สร้างเอกสารใหม่
เทมเพลตแทรก frontmatter 6 ฟิลด์อัตโนมัติ"] --> B["เติมเฉพาะค่าที่ว่าง
title / layer / status / owner ..."]
B --> C["ไฟล์ .md ที่บันทึก
--- frontmatter --- + เนื้อหา"]
C --> D["รันสคริปต์ Linter
rglob('*.md')"]
D --> E["_NAMING_FRONTMATTER_STANDARD.md
อ่านค่าที่อนุญาตมา"]
E --> F{"ตรวจสอบ
ฟิลด์บังคับ / status / layer / stale"}
F -->|"ผ่าน"| G["ป้อนเข้ากราฟความสัมพันธ์
related·affects → ระดับการพึ่งพาพิกัด L"]
F -->|"ละเมิด"| H["รายงานรายวัน
แจ้งผู้รับผิดชอบ owner อัตโนมัติ"]
H --> B
G --> I["สอบถาม AI ได้
'รวบรวมเอกสาร Layer2 review ให้หน่อย' → ตอบทันที"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class A,B human
class D,F code
class C,E,G data
class H fail
class I ai
หัวใจมีสองอย่าง ข้อแรก มาตรฐาน (E) ถูกแยกจากสคริปต์ (D) แก้มาตรฐานแล้วเกณฑ์การตรวจก็เปลี่ยนโดยไม่ต้องแก้โค้ด ข้อสอง การละเมิด (H) ไม่ใช่ทางตัน แต่เป็นลูปที่วนกลับไปยังขั้นการเขียน (B) ไม่ใช่การโทษคน แต่ส่งกลับไปให้เจ้าของเอกสารแก้เอกสารของตัวเอง
โปรเจกต์ A ที่ผู้เขียนดูแลในฐานะ Director ได้นำ frontmatter มาใช้กับทีมออกแบบทั้งหมด (4\~5 คน) เมื่อราวหกเดือนก่อน การนำมาใช้ไม่ได้สำเร็จในครั้งเดียว แต่ผ่านสี่ระยะ
แรงต้านที่ใหญ่ที่สุดในสัปดาห์แรกของการนำมาใช้คือ "ให้เขียนด้วยมือทุกครั้งเลยเหรอ" การจำหกบรรทัดมาเขียนทุกครั้งที่สร้างเอกสารใหม่นั้นน่ารำคาญ ทางแก้คือการแทรกเทมเพลตอัตโนมัติ snippet ของ VSCode, เทมเพลตของ Obsidian, ปุ่ม "เอกสารใหม่" ในพอร์ทัลออกแบบ ต่างก็แทรกบล็อก YAML เปล่าให้อัตโนมัติ ผู้เขียนเอกสารเพียงเติมค่าที่ว่าง แรงต้านก็หายไปภายในหนึ่งสัปดาห์
เดือนแรกเกิดความขัดแย้งเรื่องมาตรฐาน เมื่อหลายคนเพิ่มฟิลด์ได้อย่างอิสระ owner·responsible·author ก็ปรากฏขึ้นพร้อมกัน เป็นแนวคิดเดียวกันแต่เขียนได้สามแบบ การค้นหาและการทำงานอัตโนมัติจึงพังลง ทางแก้คือรวบรวมชื่อทางการ·ค่าที่อนุญาต·ตัวอย่างของทุกฟิลด์ไว้ในเอกสารเดียวคือ _NAMING_FRONTMATTER_STANDARD.md แล้วตั้งกฎให้การเพิ่มฟิลด์ใหม่ต้องผ่านเอกสารนี้ ภายในหนึ่งเดือนมาตรฐานก็มั่นคง
เดือนที่สาม Linter ที่เห็นในหัวข้อ 2.1.4 ก็เข้ามา ต่อให้มีมาตรฐานคนก็ยังฝ่าฝืน ดังนั้นจึงให้สร้างรายงานความสอดคล้องอัตโนมัติทุกเช้าแล้วส่งลงในช่องสาธารณะของแชตภายในทีม ผู้รับผิดชอบดูแค่เอกสารของตัวเองก็พอ หลังจากทำอัตโนมัติแล้ว การละเมิดมาตรฐานก็ลดลงอย่างเห็นได้ชัด (เป็นการประมาณของผู้เขียน ไม่ใช่ค่าที่วัดอย่างแม่นยำ — รู้สึกได้ราว ๆ ครึ่งหนึ่งลงไป)
เดือนที่หก การผสานเข้ากับ AI ก็เปล่งประกาย เมื่อมาตรฐานมั่นคง คำถามต่อไปนี้ก็ได้คำตอบกลับมาทันที
related·affects อัตโนมัติในที่สุด frontmatter ก็กลายเป็นคำศัพท์ร่วมระหว่างคนกับ AI คนเขียน AI ก็เข้าใจ AI เขียน คนก็ตรวจสอบ ทั้งคู่มองคีย์ชุดเดียวกัน อย่างไรก็ดี แรงต้านในสัปดาห์แรก, ความขัดแย้งในเดือนแรก, Linter ในเดือนที่สาม, การผสานในเดือนที่หก — เป็นผลที่หกเดือนสะสมกันสร้างขึ้น ไม่ได้เกิดในครั้งเดียว
ข้อผิดพลาดที่เกิดซ้ำในช่วงเริ่มนำมาใช้รวบรวมได้เป็นห้าอย่าง ทุกข้อตั้งอยู่บนรากเดียวกัน — "ตรงที่ปล่อยมาตรฐานไว้กับเจตจำนงของคนเพียงอย่างเดียว"
| ข้อผิดพลาด | สาเหตุของปัญหา | วิธีเลี่ยง |
|---|---|---|
| นิยามฟิลด์มากเกินไปตั้งแต่แรก | ผู้เขียนเหนื่อยกับการเติมค่าว่างจนคุณภาพตก | เริ่มด้วยหกฟิลด์หลัก หลัง 1\~2 เดือนค่อยเพิ่มเฉพาะที่ใช้บ่อย |
ชื่อฟิลด์เปลี่ยนไปเรื่อย ๆ (tag→tags→category) |
ชื่อเก่าค้างในเอกสารสะสมจนการค้นหา·การทำงานอัตโนมัติพัง | เปลี่ยนชื่อต้องมาพร้อมสคริปต์ migration เจอชื่อเก่าให้แปลงอัตโนมัติหรือเตือน |
| คนเขียนด้วยมือทุกครั้ง | พิมพ์ผิด·ฟิลด์ตกหล่น·รูปแบบวันที่แตกทาง (บั๊กนั้นในหัวข้อ 2.1.4) กลายเป็นเรื่องปกติ | ทำเทมเพลต·snippet·"เอกสารใหม่" อัตโนมัติก่อน ให้มือคนแตะเฉพาะค่าที่มีความหมาย |
| วางมาตรฐานไว้เฉย ๆ โดยไม่มีการตรวจสอบ | ถึงมีมาตรฐานก็ไม่รู้ว่าใครฝ่าฝืน แล้วผุกร่อนไปตามธรรมชาติ | Linter + รายงานอัตโนมัติรายวันให้ผู้ฝ่าฝืนแก้เอง |
ลืมฟิลด์ layer |
ถ้าไม่มีพิกัด Layer ก็สร้างทั้งการมองเห็นข้ามสาขาและด่านตรวจสอบไม่ได้ทั้งคู่ | บังคับ layer เป็นฟิลด์จำเป็น Linter ตรวจจับการตกหล่น |
ไม่จำเป็นต้องกันข้อผิดพลาดทั้งห้าให้ครบตั้งแต่วันแรก ข้อ 1·3 ให้จับรูปแบบการเลี่ยงไว้ตั้งแต่สัปดาห์แรกของการนำมาใช้ ส่วนข้อ 2·4·5 ค่อย ๆ ใส่เข้าไปทีละข้อโดยเริ่มจากจุดที่ทีมของตัวเองชนบ่อยที่สุดในระหว่างการใช้งาน จะเป็นไปอย่างเป็นธรรมชาติกว่า
การนำ frontmatter มาใช้เป็นงานที่เบากว่าที่คิด สามสัปดาห์ก็ลงตัวได้ในทีมหนึ่ง
สัปดาห์แรกให้นิยามหกฟิลด์หลัก ทำเทมเพลต แล้วใช้กับเอกสารใหม่เท่านั้นเพื่อให้ภาระการเขียนน้อยที่สุด สัปดาห์ที่สองให้ใช้กับเอกสารยอดนิยม 20 ฉบับแรกที่ดูบ่อยด้วยมือ เพื่อตรวจดูจากการใช้งานจริงว่าฟิลด์ไหนยังขาด สัปดาห์ที่สามเมื่อเปิดใช้ Linter และรายงานรายวัน ตั้งแต่นั้นมาตรฐานก็คงอยู่ด้วยพลังของเครื่องมือ ไม่ใช่ด้วยเจตจำนงของคน
ไม่ต้อง migrate เอกสารทั้งหมดในครั้งเดียว ใช้กับเอกสารที่ดูบ่อยก่อน กับเอกสารใหม่ก่อน พอผ่านไปราวหกเดือน เอกสารแทบทุกฉบับก็จะมี frontmatter ติด แต่ก็ไม่ได้หมายความว่าเป้าหมายคือ 100% การเสียเวลา migrate ไปจนถึงเอกสารเก่าที่ไม่เคยเปิดเลยแม้แต่ครั้งเดียวนั้นเป็นการสิ้นเปลือง
เราจะลองรันหนึ่งรอบด้วยตัวเองในหน่วยที่เล็กที่สุด
setup
- วางเอกสาร .md ที่จะตรวจไว้ 2\~3 ฉบับในโฟลเดอร์งาน บางฉบับให้ตั้งใจลบ layer ออก หรือใส่ค่าที่ไม่เป็นมาตรฐานอย่าง status: WIP ไว้
- วางเอกสารมาตรฐานหนึ่งบรรทัดไว้ในโฟลเดอร์เดียวกัน
status: allowed = ["draft", "review", "approved", "archived"]
updated: YYYY-MM-DD (ไม่ใส่เครื่องหมายคำพูด)
prompt (ป้อนใน Claude Code)
ช่วยเขียนสคริปต์ Python ที่ตรวจ YAML frontmatter ของไฟล์ .md ทุกไฟล์ใต้โฟลเดอร์นี้ให้หน่อย ให้จับ ฟิลด์บังคับ title·layer·status·owner ที่ขาดหาย, status ละเมิดค่าที่อนุญาต (อ่านมาจากเอกสารมาตรฐาน), layer ละเมิดจำนวนเต็มช่วง 0\~4, และที่เป็น review แต่ updated เกิน 90 วัน ให้จัดการได้อย่างปลอดภัยทั้งกรณีที่
updatedมาเป็นสตริงและมาเป็น date แล้วแสดงการละเมิดแยกตามไฟล์
verify
- รันสคริปต์แล้วตรวจดูว่าการละเมิดที่ตั้งใจฝังไว้ถูกจับได้ทั้งหมดหรือไม่
- เพิ่ม WIP ลงในรายการ allowed ของเอกสารมาตรฐาน แล้วรันใหม่ ตรวจดูว่าทั้งที่ไม่ได้แก้โค้ดแม้แต่บรรทัดเดียว status: WIP กลับเปลี่ยนเป็นผ่านหรือไม่ นั่นคือหลักฐานว่ามาตรฐานกับโค้ดถูกแยกจากกัน
- ใส่ทั้งเอกสารที่ใส่เครื่องหมายคำพูดให้ updated และที่ไม่ใส่ ลงไปทั้งคู่ แล้วตรวจดูว่า TypeError ที่เห็นในหัวข้อ 2.1.4 ไม่เกิดขึ้น
อ้างอิง: สคริปต์ Linter ฉบับเต็ม
นี่คือโค้ดที่ Claude เสนอมาในตอนแรกในหัวข้อ 2.1.4 บรรทัดตรวจ STALE (age = (today - fm["updated"]).days) มีบั๊ก datetime ค้างอยู่ตามนั้น
import sys, datetime, pathlib, re
import yaml # PyYAML
ROOT = pathlib.Path("manuscript")
STANDARD = pathlib.Path("_NAMING_FRONTMATTER_STANDARD.md")
REQUIRED = ["title", "layer", "status", "owner"]
def load_allowed_status(standard_path):
# ดึงค่าที่อนุญาตของ `status` มาจากเอกสารมาตรฐาน
text = standard_path.read_text(encoding="utf-8")
m = re.search(r"status:\s*allowed\s*=\s*\[(.*?)\]", text)
if not m:
return ["draft", "review", "approved", "archived"]
return [s.strip().strip('"').strip("'") for s in m.group(1).split(",")]
def parse_frontmatter(md_path):
text = md_path.read_text(encoding="utf-8")
if not text.startswith("---"):
return None
end = text.find("---", 3)
block = text[3:end]
return yaml.safe_load(block)
def main():
allowed = load_allowed_status(STANDARD)
today = datetime.date.today()
violations = 0
for md in ROOT.rglob("*.md"):
fm = parse_frontmatter(md)
if fm is None:
print(f"[NO-FM] {md}")
violations += 1
continue
for field in REQUIRED:
if field not in fm:
print(f"[MISSING] {md}: {field}")
violations += 1
if fm.get("status") not in allowed:
print(f"[STATUS] {md}: {fm.get('status')}")
violations += 1
if not isinstance(fm.get("layer"), int) or not (0 <= fm.get("layer") <= 4):
print(f"[LAYER] {md}: {fm.get('layer')}")
violations += 1
if fm.get("status") == "review":
age = (today - fm["updated"]).days # ← ตรงนี้พัง
if age > 90:
print(f"[STALE] {md}: {age}d")
violations += 1
sys.exit(violations)
บล็อกหลักที่แก้กลับมาหลังการขอใหม่ normalize updated ให้เป็น date อย่างปลอดภัยทั้งกรณีที่มาเป็นสตริงและมาเป็น date
def as_date(v):
if isinstance(v, datetime.date):
return v
if isinstance(v, str):
return datetime.date.fromisoformat(v.strip())
return None
# ส่วนที่แทนการตรวจ STALE ภายใน main()
if fm.get("status") == "review":
upd = as_date(fm.get("updated"))
if upd is None:
print(f"[MISSING] {md}: updated")
violations += 1
elif (today - upd).days > 90:
print(f"[STALE] {md}: {(today - upd).days}d")
violations += 1
ไม่มีทีมก็ได้ ในโฟลเดอร์โน้ตที่ใช้คนเดียว ให้ลดฟิลด์หลักเหลือสามคือ title·status·updated แล้วตั้ง Linter ให้จับเฉพาะ "เอกสารที่ status เป็น review แต่ updated เกิน 30 วัน" เพียงเท่านี้ "เอกสารที่ผมตรวจค้างไว้แล้วลืม" ก็จะลอยขึ้นมาเหนือผิวน้ำสัปดาห์ละครั้ง สามเหลี่ยมมาตรฐาน-เทมเพลต-การตรวจสอบทำงานได้เหมือนเดิมแม้ในขนาดคนเดียว
layer เพียงฟิลด์เดียวกลายเป็นพิกัดทางเข้าของการสร้างแบบโพรซีเดอรัลและการตรวจสอบอัตโนมัติสัปดาห์แรกที่พนักงานใหม่เข้ามา เขาถามผมผ่านแชตว่า "Cooldown ของการต่อสู้คือ 0.6 วินาทีใช่ไหมครับ มันเขียนอยู่ในเอกสารไหน" ผมตอบว่า "อยู่ใน GDD ของระบบสกิล (Game Design Document หรือเอกสารสเปกแบบละเอียด)" เขาถามต่อว่า "GDD นั้นอยู่ในหัวข้อไหน เพราะหลังจากการออกแบบคลาสก็เป็น Damage Curve ต่อด้วยวิธีแสดงผล UI รวมแล้ว 220 บรรทัด" ผมจึงเปิดไฟล์แล้วหาให้เขาเอง มันอยู่บรรทัดที่ 137 สุดท้ายเขาถามว่า "แต่ทำไมต้อง 0.6 วินาที 0.5 ไม่ได้เหรอครับ" คำตอบนั้นไม่มีอยู่ในเอกสารใดเลย ผมจำได้ว่าตัดสินใจกันในการประชุมเมื่อ 6 เดือนก่อน แต่เหตุผลถูกฝังอยู่ที่ไหนสักแห่งในบันทึกการประชุม
ในบทสนทนา 5 นาทีนี้บรรจุความล้มเหลวทั้งสามของเอกสารรวม 220 บรรทัดไว้ครบ — หาตำแหน่งไม่เจอ (การค้นหาล้มเหลว) ไม่มีเหตุผล (บริบทสูญหาย) และต้องให้คนเป็นตัวกลางทุกครั้ง (อัตโนมัติไม่ได้) ถ้าถามคำถามเดียวกันนี้กับ AI สถานการณ์จะยิ่งแย่ลง AI จะอ่านครบทั้ง 220 บรรทัด แล้วตอบโดยปนเรื่อง Damage Curve ที่ไม่เกี่ยวกับ cooldown เข้ามาด้วย
ยาที่บทนี้สั่งจ่ายนั้นเรียบง่าย เอกสารหนึ่งฉบับบรรจุการตัดสินใจเพียงหนึ่งเดียว เอกสารหน่วยการตัดสินใจที่ถูกซอยย่อยด้วยหลักการนี้ เราเรียกว่า atom เมื่อแยก GDD 220 บรรทัดออกเป็นชิ้น ๆ "cooldown คือ 0.6 วินาที" ก็จะกลายเป็น atom หนึ่ง และภายใน atom นั้นจะรวมตำแหน่ง เนื้อหา เหตุผล ข้อยกเว้น และความสัมพันธ์ไว้ในที่เดียวกัน บทนี้จะไม่พูดทฤษฎีลอย ๆ แต่จะผ่ากายวิภาค atom จริงหนึ่งตัวจนจบ — ตั้งชื่ออย่างไร ใส่ frontmatter แบบใด ระบุความสัมพันธ์อย่างไร และผลลัพธ์คือ AI หยิบเฉพาะ atom ตัวนั้นออกมาได้แม่นยำอย่างไร
combat_cooldown_rule_v2ตัวอย่างที่จะผ่าคือ atom หนึ่งตัวที่ใช้งานจริงอยู่ในโปรเจกต์ A ชื่อ combat_cooldown_rule_v2 เนื้อไฟล์ทั้งหมดเป็นดังนี้ ไม่ยาว เพราะบรรจุการตัดสินใจเพียงหนึ่งเดียว
---
name: combat_cooldown_rule_v2
title: "กฎ Cooldown การต่อสู้ — v2"
type: rule
layer: 1
status: approved
owner: อี มินซู
created: 2026-03-10
updated: 2026-05-12
applies_to: [skill_system, item_system]
---
# กฎ Cooldown การต่อสู้ v2
Why (ทำไม): เพื่อจำกัดจำนวนสกิลที่ใช้พร้อมกันได้ ลดภาระการ
ตัดสินใจชั่วขณะ และรักษาความหมายของการกดคอมโบไว้
Rule (กฎ): สกิลแอ็กทีฟทุกตัวมี Global Cooldown 0.6 วินาที +
Cooldown รายตัว (กำหนดตามแต่ละสกิล) ระหว่างที่ Global Cooldown
กำลังทำงาน จะร่ายสกิลแอ็กทีฟใดก็ไม่ได้
How to apply (การนำไปใช้):
- เมื่อนิยามสกิลใหม่ ต้องระบุ cooldown รายตัวเสมอ
- ถ้าคอลัมน์ cooldown ใน L3_SkillSheet เป็น 0 ถือว่าละเมิดกฎนี้
- การตรวจสอบความสอดคล้องในขั้น build จะตรวจจับการละเมิดอัตโนมัติ
Exceptions (ข้อยกเว้น):
- สกิลพาสซีฟไม่อยู่ภายใต้กฎนี้
- ท่าไม้ตายใช้ระบบเกจแยกต่างหาก (See: [[ultimate_gauge_system]])
Relations (ความสัมพันธ์):
- affects: [[combat_dps_calculation_v3]], [[balance_curve_v3]]
- derives_from: [[principle_decision_load_reduction]]
- conflicts_with: [[skill_cancel_rule_legacy_v1]]
- requires: [[combat_input_buffer_system]], [[skill_system_v2]]
- is_a: rule
- part_of: combat_system_master
ผมจะแยกไฟล์แผ่นนี้ออกเป็นห้าส่วนเพื่อพิจารณา — การตั้งชื่อ frontmatter การตัดสินใจเดี่ยว ความสัมพันธ์ และความสามารถในการสืบย้อน ทั้งห้าส่วนต้องครบ AI จึงจะอ่าน atom นี้เป็น "หน่วยที่มีความหมายในตัวเองได้แม้อยู่ลำพัง"
ชื่อไฟล์คือ combat_cooldown_rule_v2 ไม่ใช่ชื่อที่ตั้งขึ้นลอย ๆ แต่มีโครงสร้างสามท่อน
combat_ cooldown_rule _v2
└ prefix └ ตัวการตัดสินใจ └ เวอร์ชัน
(โดเมนใด) (การตัดสินใจเรื่องอะไร) (ปรับครั้งที่เท่าไร)
prefix combat_ คือพิกัดที่บอกว่า "นี่คือการตัดสินใจของโดเมนการต่อสู้" atom ประเภทกฎของโปรเจกต์ A แยกโดเมนกันด้วย prefix ได้แก่ quest_ (เควสต์), data_ (การจัดการข้อมูล), docs_ (การจัดการเอกสาร), meeting_ (บันทึกการประชุม), portal_ (วิวเวอร์เอกสารออกแบบ) แค่ดู prefix ก็จับได้ว่าการตัดสินใจนี้อยู่ในขอบเขตความรับผิดชอบของใคร และได้รับผลกระทบจากที่ไหน
ถ้าการตั้งชื่อสั่นคลอน ทุกอย่างก็สั่นคลอนตาม ถ้าการตัดสินใจเดียวกันมีอยู่สองครั้งในชื่อ skill-cooldown.md กับ cooldown_skill_v2.md ทั้งการค้นหาก็พัง และการจับคู่ JIT ที่จะกล่าวถึงต่อไปก็พังด้วย ด้วยเหตุนี้โปรเจกต์ A จึงตรึงกฎการตั้งชื่อไว้เป็น atom หนึ่งตัวเสียก่อน นั่นคือ atom_naming_convention_v1 ซึ่งบังคับ snake_case · บังคับมี prefix · บังคับมี suffix เวอร์ชัน และกฎนี้ไม่ได้อาศัยเจตจำนงของคน แต่ Linter เป็นผู้รักษา ถ้าชื่อไฟล์ที่ไม่มี prefix ถูก commit เข้ามา มันจะถูกดักไว้ในขั้น build
ในการตั้งชื่อยังมีการออกแบบที่ใหญ่กว่าซึ่งร้อยทะลุทั้งเล่มฝังอยู่ layer: 1 ใน frontmatter คือพิกัดที่สอง ถ้า prefix บอก "โดเมนใด" Layer ก็บอก "ชั้นนามธรรมใด" พิกัดสองตัวต้องประกอบกัน ตำแหน่งของ atom จึงถูกตรึงเป็นจุดเดียวบนระนาบ ตรงนี้ Layer เป็นเพียงพิกัดเท่านั้น (รายละเอียดนิยามชั้น 0\~4 อยู่ใน 2.3) กฎ cooldown เป็น "กฎอินพุตที่ควบคุมการสร้าง" จึงนั่งอยู่ที่ Layer 1 ยังมีกฎที่บังคับให้ใส่พิกัด Layer นี้เป็นเลข prefix หน้าชื่อเอกสารแยกต่างหากด้วย — docs_layer_numeric_prefix_naming พูดอีกอย่างคือในชื่อเดียวมีแกนพิกัดสองแกนระบุไว้ครบ
แก่นแท้ของการออกแบบนี้ไม่ใช่นิสัยรักความเป็นระเบียบ มีคำหนึ่งที่ผมพูดกับทีมซ้ำ ๆ "Layer ที่แบ่งไว้นั้น แบ่งไว้เพื่อการสร้างแบบโพรซีเดอรัล (procedural generation) นั่นเอง" เมื่อแต่ละ atom ระบุพิกัดโดเมน (prefix) และพิกัดชั้น (Layer) ไว้ ต่อไปก็เป็นไปได้ที่ AI จะ "รับกฎ combat ของ Layer 1 ทั้งหมดเป็นอินพุต แล้วสร้างเนื้อหา (content) ของ Layer 2 โดยอัตโนมัติ" ชื่อคือระบบที่อยู่ (address) ของการทำงานอัตโนมัตินั้น
บล็อก YAML ระหว่าง --- เหนือเนื้อความคือ frontmatter เป็นการนำมาตรฐานที่กล่าวใน 2.1 มาใช้กับ atom ตรง ๆ และเป็นป้ายกำกับที่ไม่ใช่คนอ่าน แต่เครื่อง (build script · JIT hook · ตัวสร้างแผนผังความสัมพันธ์) เป็นผู้อ่าน
| ฟิลด์ | ค่า | สิ่งที่เครื่องทำด้วยค่านี้ |
|---|---|---|
name |
combat_cooldown_rule_v2 | ID เฉพาะที่ใช้เป็นเป้าหมายของ link จาก atom อื่น |
type |
rule | สถิติ/ตัวกรองตามหมวด (rule / concept / decision …) |
layer |
1 | สี/การจัดเรียงตาม Layer, แกนอ้างอิงสำหรับตรวจจับการอ้างย้อน |
status |
approved | จาก draft · approved · archived มีเพียง approved ที่รวมเข้า build |
applies_to |
[skill_system, item_system] | ขอบเขตผลกระทบ — ระบบที่กฎนี้แตะถึง |
created/updated |
2026-03-10 / 2026-05-12 | ติดตามการเปลี่ยนแปลง, วันอ้างอิงสำหรับตรวจ atom เก่า |
เมื่อป้ายเหล่านี้ถูกใส่ไว้ การตรวจสอบอัตโนมัติก็ทำได้ ตัวอย่างเช่น ถ้ากฎระบบที่ประกาศเป็น layer: 1 ไปอ้างถึง data atom (Layer 3) อย่าง [[L3_SkillSheet_row_0042]] โดยตรงในเนื้อความ นั่นคือ การอ้างย้อน (L3→L1) ที่ชั้นบนถูกผูกติดกับค่ารูปธรรมของชั้นล่าง โปรเจกต์ A ตรวจจับรูปแบบนี้อัตโนมัติในขั้น build เพราะกฎต้องอ้างถึงรูปแบบของข้อมูล ไม่ใช่ข้อมูลทีละแถว ถ้าไม่มีบรรทัด layer ใน frontmatter การตรวจสอบนี้ก็ไม่เกิดขึ้นเลย
การจัดการ status: archived ก็เป็นงานของ frontmatter เช่นกัน เมื่อการตัดสินใจเปลี่ยนไป atom จะไม่ถูกลบ แต่จะได้ status: archived + วันที่ archived_at ทั้ง build และ JIT จะคัด atom ที่ archived ออก คือเก็บบันทึกไว้แต่ถอนออกจากตัวที่ใช้งานจริง ตลอด 6 เดือนของการใช้งานในโปรเจกต์ A อัตราการเลิกใช้อยู่ที่ราว 15% (ผู้เขียนวัดจริง) ถ้าอัตรานี้เข้าใกล้ 0% ผมตีความว่าเป็นสัญญาณว่าเวิร์กโฟลว์การเลิกใช้ไม่ทำงาน
แก่นของการผ่า atom คือการยืนยันว่าเนื้อความบรรจุการตัดสินใจเพียงหนึ่งเดียวหรือไม่ วิธีตรวจเรียบง่าย ลองสรุปการตัดสินใจของ atom นี้เป็นประโยคเดียว
"สกิลแอ็กทีฟทุกตัวมี Global Cooldown 0.6 วินาที"
จบในประโยคเดียว ผ่าน ถ้าสรุปออกมาเป็นสองประโยคเช่น "cooldown คือ 0.6 วินาที และระหว่างคอมโบลดลง 50%" นั่นคือสองการตัดสินใจ ต้องแยกเป็น combat_cooldown_rule_v2 (cooldown พื้นฐาน) และ combat_combo_cooldown_reduction_v1 (การลดในคอมโบ)
มีการตรวจสอบเสริมที่ดูความเป็นหนึ่งเดียวอีกสองข้อ
การตรวจการเลิกใช้แบบอิสระ ถ้าเลิกใช้ atom นี้เพียงตัวเดียว ระบบจะพังหรือไม่ ถ้าเลิกใช้กฎ cooldown สมดุลการต่อสู้จะสั่นคลอน แต่ระบบยังเดินได้ แสดงว่าหน่วยถูกต้อง ในทางกลับกัน ถ้าเลิกใช้แล้วอีกห้าตัวพังตามไปด้วย ที่จริงห้าตัวนั้นคือห้าชิ้นส่วนของการตัดสินใจเดียว ต้องรวมเข้าเป็น atom ที่ใหญ่กว่า
การตรวจการอ้างอิงเดี่ยว ถ้าที่อื่นวาง link เพียง [[combat_cooldown_rule_v2]] ตัวเดียว ความหมายยังเดินได้หรือไม่ ถ้าเดินได้ แสดงว่าหน่วยถูกต้อง ถ้าจะอ้างบรรทัดนี้แล้วต้องอ่านเนื้อความหลายที่ให้ครบ แสดงว่ายังแยกย่อยไม่พอ
เนื้อความที่ผ่านการตรวจเหล่านี้จะจัดเรียงเป็นห้าหัวข้อโดยธรรมชาติ — Why, Rule, How, Exceptions, Relations โดยเฉพาะ อย่าลบ Why คำตอบของคำถามสุดท้ายที่พนักงานใหม่ถามในบทนำว่า "ทำไมต้อง 0.6 วินาที" อยู่ตรงนี้ — "เพื่อลดภาระการตัดสินใจชั่วขณะและรักษาความหมายของการกดคอมโบ" เมื่อ 6 เดือนต่อมามีใครเสนอว่า "ลดเหลือ 0.5 วินาทีกันเถอะ" บรรทัดเดียวนี้จะกลายเป็นจุดตั้งต้นของการถกเถียง atom ที่ Why หายไปจะกลายเป็นฟอสซิลที่ไม่มีใครกล้าแตะ
หัวข้อ Relations ที่ก้นสุดของ atom ทำให้ตัวอย่างนี้ไม่ใช่บันทึกโดดเดี่ยว แต่เป็นโหนดหนึ่งในกราฟ แก่นคือมันไม่ได้เป็นเพียง "เอกสารที่เกี่ยวข้อง" แต่ ระบุชนิดของความสัมพันธ์
flowchart TD
P["หลักการ: ลดภาระการตัดสินใจ"] -->|derives_from| C["combat_cooldown_rule_v2"]
C -->|affects| A1["combat_dps_calculation_v3"]
C -->|affects| A2["balance_curve_v3"]
C -->|requires| R1["combat_input_buffer"]
C -->|requires| R2["skill_system_v2"]
C -.->|conflicts_with| X["skill_cancel_legacy_v1
(ขัดแย้ง รอเลิกใช้)"]
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class P,C,A1,A2,R1,R2 data
class X fail
ความสัมพันธ์หกชนิดต่างทำหน้าที่ต่างกัน
derives_from: การตัดสินใจนี้แตกหน่อมาจากหลักการชั้นบนใด cooldown 0.6 วินาทีคือการทำให้หลักการ "ลดภาระการตัดสินใจ" เป็นรูปธรรมaffects: ถ้า atom นี้เปลี่ยน อะไรจะได้รับผลกระทบ ถ้าเปลี่ยน 0.6 วินาทีเป็น 0.5 วินาที การคำนวณ DPS และ Balance Curve จะสั่นคลอน ดึงขอบเขตผลกระทบออกมาได้อัตโนมัติก่อนเปลี่ยนrequires: การตัดสินใจนี้จะตั้งอยู่ได้ต้องมีอะไรอยู่ก่อน ถ้าไม่มีระบบ input buffer Global Cooldown จะกลืนอินพุตหายไปconflicts_with: ขัดแย้งกับอะไร มันขัดแย้งกับกฎ skill cancel เวอร์ชันเก่า และ link นี้คือสัญญาณว่า "หนึ่งในสองตัวต้องถูกเลิกใช้"is_a / part_of: การจัดประเภท (rule) และการสังกัด (combat_system_master) คือโครงกระดูกของกราฟถ้าเป็นเพียง link แบบ "Related: [เอกสาร A], [เอกสาร B]" คนก็ต้องไล่พิจารณาเองทีละตัว แต่เมื่อชนิดของความสัมพันธ์ถูกใส่เป็น enum เครื่องจะพิจารณาให้ "เปลี่ยน atom นี้แล้วอะไรได้รับผลกระทบบ้าง แสดงมาให้หมด" กลายเป็นคิวรีอัตโนมัติที่ไล่ตาม affects และ "ตอนนี้กฎที่ขัดแย้งกันมีอะไรบ้าง หามาให้หมด" กลายเป็นการตรวจอัตโนมัติที่สแกน conflicts_with การออกแบบออนโทโลยีเต็มรูปแบบของ enum ทั้งหกนี้จะกล่าวใน 2.4 ส่วน 2.2 เพียงชี้ว่ามาตรฐาน atom ได้นำ enum นั้นมาใช้ล่วงหน้าในรูปแบบหนึ่ง
ลูกศรความสัมพันธ์ยังเป็นอินพุตของเครื่องมือสร้างแผนผังความสัมพันธ์ด้วย gen_relation_map.py ของโปรเจกต์ A อ่าน layer ใน frontmatter และหัวข้อ Relations ของ atom ทุกตัว แล้ววาดแผนผังความสัมพันธ์ HTML แบบอินเทอร์แอ็กทีฟที่ลงสีตาม Layer ให้อัตโนมัติ เป็นไปได้เพราะ atom แต่ละตัวมีพิกัด (Layer) และลูกศร (Relations)
atom ที่มีครบทั้งห้าส่วนสามารถสืบย้อนได้ ใคร · เมื่อไร · ทำไม จึงตัดสินใจเช่นนี้ และจับอะไรเป็นการละเมิด ทั้งหมดอยู่ในที่เดียวกัน คุณค่าของความสามารถในการสืบย้อนจะคมชัดที่สุดเมื่อเห็นเป็นเหตุการณ์ที่กันไว้ได้จริง ไม่ใช่ตัวเลขสถิติ
atom meeting_image_caption_standard ของโปรเจกต์ A คือกฎที่กำหนดให้รูปภาพแนบในบันทึกการประชุมต้องระบุคำบรรยายว่า "เป็นหน้าจออะไร · ทำไมจึงแนบ · เป็นการตัดสินใจอะไร" เสมอ สมัยที่ยังไม่มี atom นี้ มีบันทึกการประชุมหนึ่งที่แนบสกรีนช็อตโดยไม่มีคำบรรยาย หนึ่งสัปดาห์ต่อมาเพื่อนร่วมทีมที่เห็นมัน ต้องใช้เวลา 30 นาทีไปยืนยันกับผู้เขียนว่า "นี่คือหน้าจออะไร" หลังจากมี atom นี้แล้ว เมื่อเกิดการตกหล่นแบบเดียวกันซ้ำ Linter ในขั้น build จับรูปภาพที่ไม่มีคำบรรยายได้อัตโนมัติ แก้ไขเสร็จใน 5 นาที จาก 30 นาทีเหลือ 5 นาที
ตัวอย่างอีกชิ้น skill_listing_budget_wrapper_only_policy คือกฎที่จำกัดสล็อตคำสั่งสแลช (slash command) ระดับ global ไว้ที่ 12 ตัว ให้เก็บสกิลตัวจริงไว้ในไดเรกทอรีแยก แต่เปิดเผยใน global เพียง wrapper 12 ตัว ก่อนถูกตรึงเป็นกฎ คำสั่งสแลชระดับ global พองตัวจนเกือบ 40 ตัว กัดกินงบโทเค็น (token) ทุกครั้งที่เริ่มเซสชัน หลังจากนิยาม atom แล้ว เครื่องมือจัดระเบียบอัตโนมัติจะเก็บกวาดส่วนเกินทุกครั้งที่เริ่มเซสชัน กฎถูกบังคับใช้ด้วยเครื่องมือ ไม่ใช่ความทรงจำของคน
atom แบบนี้สะสมอยู่ในโปรเจกต์ A ราว 304 ตัว (ผู้เขียนวัดจริง ณ จุด 6 เดือนของการใช้งาน) ถ้าดูเฉพาะกิ่งใหญ่ของการกระจาย กฎป้องกันการเกิดซ้ำ (rule) มีสัดส่วนใหญ่ที่สุด ถัดมาคือการตรึงการตัดสินใจครั้งเดียวจบ (decision) · แนวคิดของโดเมน (concept) · การปรับการทำงานร่วมกัน (feedback) ตามลำดับ เวลาที่ atom หนึ่งตัวกันไว้ได้นับเป็นนาที แต่เมื่อสะสมครบ 304 ตัว เวลาที่ประหยัดสะสมก็ข้ามไปเป็นหน่วยวัน นี่คือเหตุผลที่ผมเรียก atom ว่า "สินทรัพย์" ไม่ใช่ "การจัดระเบียบ"
จนถึงตอนนี้ผมผ่ากายวิภาค atom หนึ่งตัวแบบนิ่ง ตอนนี้มาดูช่วงเวลาที่มันมีชีวิตขยับ JIT (Just-In-Time) hook ที่กล่าวใน 1.3 คัดเฉพาะ atom ที่ตรงกับคำสำคัญของอินพุต แล้วฉีดเข้าสู่บริบท ณ ตรงนั้น JIT manifest คือ JSON ที่จับคู่คำสำคัญที่ใช้แมตช์และคะแนนให้กับ atom แต่ละตัว
{
"name": "combat_cooldown_rule_v2",
"path": "atoms/combat/combat_cooldown_rule_v2.md",
"regex": "쿨다운|cooldown|글로벌 쿨다운|GCD",
"score": 75
}
การฉีดจริงไหลดังนี้
flowchart TD
A["อินพุตผู้ใช้:
ถ้าลด cooldown สกิลเหลือ 0.5 วินาทีล่ะ?"] --> B["JIT hook:
สแกน regex ใน manifest"]
B --> C{"แมตช์ cooldown?"}
C -->|"ใช่ score=75"| D["combat_cooldown_rule_v2
ฉีดเนื้อความเต็ม"]
C -->|"ไม่"| E["ไม่ฉีด"]
D --> F["AI อ่านจนถึง Why·Rule·Exception
แล้วตอบ"]
F --> G["คำตอบ: 0.6 วินาทีมีฐานจากการลด
ภาระการตัดสินใจ ถ้าลดเหลือ 0.5 วินาที
ต้องทบทวนการคำนวณ DPS และ Balance
Curve ที่เป็นเป้าหมายของ affects ใหม่"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class A human
class B,C code
class D data
class F,G ai
แก่นอยู่ที่ช่องสุดท้าย AI ไม่ได้ตอบเพียงว่า "มันคือ 0.6 วินาที" เพราะอ่าน Why ของ atom จึงยกเหตุผลมาประกอบ และเพราะอ่าน affects ใน Relations จึงชี้ล่วงหน้าถึงเป้าหมายที่จะสั่นคลอนเมื่อเปลี่ยน (การคำนวณ DPS · Balance Curve) ห้าส่วนที่ซอยย่อย เขียนเหตุผล และระบุความสัมพันธ์ไว้ ฟื้นคืนมีชีวิตในคำตอบครบทุกส่วน
ตรงนี้เผยให้เห็นว่าหลักการตัดสินใจเดี่ยวคือเงื่อนไขตั้งต้นของการทำงานอัตโนมัติ ถ้า atom นี้เป็น GDD รวม 220 บรรทัด ในวินาทีที่คำว่า "cooldown" เพียงคำเดียวแมตช์ การออกแบบคลาส · Damage Curve · UI จะถูกฉีดเข้ามาทั้งก้อน งบโทเค็นถูกหั่น และ AI จะหลงโฟกัสว่าควรตอบการตัดสินใจตัวไหนในห้าตัว ยิ่ง atom เล็กและชัด ความแม่นยำของ JIT ยิ่งสูงขึ้น สภาพที่ซอยย่อยไว้ไม่ใช่คุณธรรมของการจัดระเบียบ แต่เป็นเงื่อนไขตั้งต้นของการฉีดอัตโนมัติ
score คือกลไกที่รักษางบบริบท ถ้าอินพุตหนึ่งแมตช์หลาย atom จะฉีดเฉพาะ N ตัวบนสุดตาม score (ค่าเริ่มต้น 3 ตัว) เกณฑ์การให้คะแนนกำหนดไประหว่างใช้งานจริง
ตัวอย่างที่ผ่าไป combat_cooldown_rule_v2 คือ atom ที่ทีมใช้ร่วมซึ่งได้รับ status: approved ไม่ใช่ทุก atom จะมาถึงตำแหน่งนี้ตั้งแต่แรก โปรเจกต์ A แบ่ง atom ออกเป็นสองชั้น
เหตุผลที่แยกเป็นเรื่องของจิตวิทยา atom ส่วนตัวต้องอิสระ เจ้าตัวจึงจะจดสมมติฐานก่อนยืนยันได้โดยไม่กดดัน และเลิกใช้มันได้ในอีกหนึ่งสัปดาห์ ถ้าเปิดสู่ทีมตั้งแต่แรก จะคิดว่า "ถ้าผิดขึ้นมาจะทำยังไง" จนสุดท้ายไม่จดเลย ในทางกลับกัน atom ที่ทีมใช้ร่วมต้องเข้มงวด ทุกคนจึงจะเชื่อใจและอ้างอิง
combat_cooldown_rule_v2 เองตอนแรกก็คงเป็นบันทึกสั้น ๆ บรรทัดเดียวใน atom ส่วนตัวว่า "ลองทดสอบ cooldown 0.6 วินาทีกัน" หลังถูกยืนยันในอัลฟาบิลด์ มันถูกเลื่อนขั้นสู่ atom ที่ทีมใช้ร่วมในรูปคำขอเปลี่ยนแปลง และผ่านการรีวิวของนักออกแบบเกมคนอื่นจึงกลายเป็น approved กระแสการเลื่อนขั้นจากส่วนตัวสู่ทีมนี้เองคือแกนหนึ่งของลูป self-improving ที่ทำให้ระบบ atom ฉลาดขึ้นตามกาลเวลา
ความผิดพลาดที่เกิดซ้ำในช่วงต้นของการใช้งาน atom สรุปได้เป็นห้าข้อ ทั้งหมดออกมาจากรากเดียวกันคือ "ปฏิบัติต่อ atom เป็นบันทึกครั้งเดียวจบ ไม่ใช่สินทรัพย์"
| ความผิดพลาด | อะไรพัง | วิธีหลีกเลี่ยง |
|---|---|---|
| สร้างมากเกินไปในสัปดาห์แรก | atom ที่ยังไม่ยืนยันสะสมจนการใช้งานพังทลาย | เริ่มจากที่ยืนยันแล้วหนึ่งสองตัว ปล่อยให้เพิ่มตามธรรมชาติ |
| ไม่เลิกใช้ | atom เก่าแมตช์ JIT เรื่อย ๆ จนสร้างคำตอบผิด | ตรวจรายไตรมาส, status: archived + archived_at |
| นามธรรม/รูปธรรมเกินไป | "ทำการออกแบบที่ดี" ตรวจไม่ได้, บันทึกจิปาถะบรรทัดเดียวไร้ความหมาย | ระดับ "ระยะโจมตีมีเพียง 0.5/1.5/3.0/5.0" |
| ชื่อไม่สอดคล้องกัน | การค้นหา · การแมตช์ JIT พังทั้งยวง | สร้าง atom กฎการตั้งชื่อก่อน แล้วบังคับด้วย Linter |
| ไม่เขียน Why | เวลาผ่านไปจะกลายเป็นฟอสซิลที่ไม่มีใครกล้าแตะ | บังคับ 5 หัวข้อ Why·Rule·How·Exception·Relations |
ไม่จำเป็นต้องหลีกเลี่ยงทั้งห้าข้ออย่างสมบูรณ์ตั้งแต่เดือนแรก ข้อ 1 และ 4 คลายไปด้วยกันด้วย atom กฎการตั้งชื่อตัวเดียว ส่วนข้อ 2 · 3 · 5 พอรันการตรวจรายไตรมาสหนึ่งครั้ง ณ จุด 3 เดือนของการใช้งาน ก็จะจัดเรียงเข้าที่โดยธรรมชาติ
บทนี้ผมแยก atom หนึ่งตัวออกเป็นห้าส่วนเพื่อพิจารณา — ชื่อ (พิกัด), frontmatter (ป้ายสำหรับเครื่อง), การตัดสินใจเดี่ยว (การตรวจหนึ่งประโยค), ความสัมพันธ์ (การวิเคราะห์ผลกระทบ), ความสามารถในการสืบย้อน (30 นาทีที่กันไว้ได้) และยืนยันแล้วว่าห้าส่วนนั้นฟื้นคืนมีชีวิตทั้งก้อนอย่างไรในการฉีดอัตโนมัติของ JIT
หนึ่งในสองพิกัดที่ระบุไว้ในชื่อ คือ layer: 1 นั้น 2.2 เพียงแตะผ่านไปเบา ๆ 2.3 จะจัดการ Layer นั้นแบบเต็มหน้า เมื่อให้พิกัด Layer แก่ atom แต่ละตัว แม้คนละสาขาก็เริ่มมองเห็นว่าผลงานของกันและกันนั่งอยู่ตรงไหน และ 2.4 จะทำให้ความสัมพันธ์หกชนิด (affects · derives_from · conflicts_with · requires · is_a · part_of) ที่บทนี้หยิบยืมมาแค่ชื่อ enum กลายเป็นออนโทโลยีอย่างเป็นทางการ ในโครงกระดูกของสถาปัตยกรรมข้อมูลที่ไล่จาก YAML (2.1) → Atom (2.2) → Layer (2.3) → Ontology (2.4) บทนี้คือข้อต่อที่สอง
setup สร้างไดเรกทอรี atoms/ ในโฟลเดอร์งาน แล้วเขียน atom กฎการตั้งชื่อ (atom_naming_convention_v1) เป็นอันดับแรกสุด แค่จดสามบรรทัดคือ snake_case · บังคับ prefix · suffix เวอร์ชัน ก็พอ ถ้าใช้ JIT ก็วาง _jit_manifest.json ที่เป็นอาร์เรย์ว่างไว้หนึ่งไฟล์
prompt เลือกการตัดสินใจหนึ่งอย่างที่คุณลืมทุกครั้ง แล้วขอร่าง atom ด้วยพรอมต์ด้านล่าง
"ช่วยทำการตัดสินใจต่อไปนี้ให้อยู่ในรูปแบบมาตรฐาน atom การตัดสินใจ: 'สกิลแอ็กทีฟมี Global Cooldown 0.6 วินาที' หัวข้อมีห้าหัวข้อคือ Why · Rule · How to apply · Exceptions · Relations ใส่ name (snake_case+prefix), type, layer, status: draft, owner, created ใน frontmatter และช่วยตรวจตอนท้ายด้วยว่าการตัดสินใจสรุปได้เป็นประโยคเดียวหรือไม่"
verify ตรวจ atom ที่ได้ด้วยสามข้อ ① การตัดสินใจสรุปได้เป็นประโยคเดียวหรือไม่ (ถ้าไม่ได้ ให้แยกย่อย) ② Why ไม่ว่างเปล่าใช่ไหม ③ เพิ่มบรรทัด {"name", "path", "regex", "score"} ใน manifest แล้วเมื่อโยนคำสำคัญของ regex นั้นเป็นอินพุตจริง atom ถูกฉีดหรือไม่ ถ้าผ่านทั้งสามข้อ atom แรกก็เสร็จสมบูรณ์
ถ้าคุณเป็นนักพัฒนาคนเดียวที่ไม่มีทั้งทีม ไม่มี Linter ไม่มี build pipeline คุณสามารถย่อทั้งบทนี้ลงเหลือแค่โฟลเดอร์หนึ่งในแอปจดโน้ตได้
domain_decision_v1 แทน Linter ก็ใช้ตาตัวเองรักษา→ ผลกระทบ:, ↑ ฐานเหตุผล:, ✕ ขัดแย้ง: ก็คงการสืบย้อนผลกระทบไว้ได้ราว 90%แก่นไม่ใช่เครื่องมือ แต่เป็นนิสัยห้าส่วน 10 โน้ตแรกยากที่สุด เมื่อข้ามด่านนั้นไปได้ อีก 100 โน้ตถัดไปมือจะสร้างเองโดยอัตโนมัติ
นั่นคือช่วงที่สาขางานขยายจากสามเป็นแปด นักออกแบบเกมฝ่ายต่อสู้กำหนดระยะของสกิลไว้ที่ 8 เมตร ในสัปดาห์เดียวกัน นักออกแบบเลเวลก็ล็อกความกว้างของทางเดินดันเจี้ยนไว้ที่ 6 เมตร ทั้งสองการตัดสินใจสมเหตุสมผลอย่างสมบูรณ์เมื่อมองในขอบเขตสาขาของตัวเอง ปัญหาเผยตัวออกมาในบิลด์อีกสามสัปดาห์ถัดมา สกิลโจมตีเป็นวงทะลุกำแพงทางเดินออกไปและศัตรูตายในที่ที่มองไม่เห็นด้วยซ้ำ ไม่ใช่ความผิดของใครเลย เพียงแต่ทั้งสองคนไม่มีหน้าต่างที่จะมองเห็นการตัดสินใจของอีกฝ่าย
บทนี้คือเรื่องราวของการสร้างหน้าต่างนั้น การทำให้แต่ละสาขายังคงมีห้องของตัวเองไว้ครบ แต่รู้ได้ด้วยพิกัดเพียงตัวเดียวว่ากำลังเกิดอะไรขึ้นในห้องข้างๆ ระบบพิกัดนั้นเราเรียกว่า Layer
การออกแบบเกมแตกแขนงเป็นสาขางานย่อยๆ จำนวนมาก ทั้งระบบ ต่อสู้ เนื้อเรื่อง เนื้อหา เลเวล บาลานซ์ UX และ QA แต่ละสาขามีเครื่องมือ ผลงาน และการประชุมของตัวเอง ยิ่งโครงการใหญ่ขึ้นเท่าไร แต่ละคนก็ยิ่งดำดิ่งลึกเข้าไปในขอบเขตของตัวเอง จนกลายเป็นสภาพที่ไม่รู้ว่าสาขาอื่นกำลังทำอะไรอยู่ สภาพนี้เรียกว่าการเกิดไซโล (silo)
ต้นทุนของการเกิดไซโลจะปรากฏให้เห็นก็ต่อเมื่อเวลาผ่านไปแล้ว
สาเหตุไม่ใช่การขาดฝีมือ แต่ละคนตัดสินใจอย่างสมเหตุสมผลในสาขาของตัวเอง เพียงแต่ไม่มีช่องทางที่จะรับรู้การตัดสินใจของสาขาอื่น หากจะอุดด้วยการประชุม การประชุมก็จะระเบิดเพิ่มจำนวน หากจะอุดด้วยแชตกลุ่ม สัญญาณก็จะจมหายไปในเสียงรบกวน ไม่ได้หมายความว่าการประชุมและแชตกลุ่มไร้ค่า แต่หัวใจสำคัญคือการแยกขอบเขตให้ชัดเจนระหว่างส่วนที่อุดได้กับส่วนที่อุดไม่ได้
ทางออกคือการทำให้รับรู้กระแสงานของกันและกันได้ (การมองเห็นแบบรวม) โดยไม่ต้องลดทอนขอบเขตของแต่ละสาขา (คงการแตกแขนงของสาขาไว้) ข้อเรียกร้องสองข้อที่ดูเหมือนขัดแย้งกันนี้จะบรรลุได้พร้อมกันเมื่อจัดวางลงบนระบบพิกัดเดียวกัน ระบบพิกัดนั้นคือ Layer หากเปรียบกับสำนักงานก็เหมือนกับการที่แต่ละคนมีโต๊ะของตัวเอง แต่ดูนาฬิกาแขวนผนังและปฏิทินอันเดียวกัน
Layer ที่ใช้ในหนังสือเล่มนี้คือการทำให้เป็นนามธรรม 5 ชั้น ตั้งแต่ 0 ถึง 4 ยิ่งสูงขึ้นยิ่งเป็นนามธรรมและเปลี่ยนแปลงน้อย ยิ่งต่ำลงยิ่งเป็นรูปธรรมและเปลี่ยนแปลงบ่อย
บทบาทที่แต่ละชั้นทั้งห้ารับผิดชอบในไปป์ไลน์การสร้างแบบโพรซีเดอรัลและการทำงานอัตโนมัติ อยู่ในป้ายกำกับด้านขวาของแผนภาพข้างต้น การแมปนี้คือกระดูกสันหลังของบทนี้ หากมอง Layer เป็นเพียง "โฟลเดอร์ที่จัดเรียงไว้อย่างเป็นระเบียบ" ก็เท่ากับเห็นเพียงครึ่งเดียว แต่ละชั้นสอดคล้องอย่างแม่นยำกับหนึ่งขั้นในไปป์ไลน์การสร้าง (จุดยึด → กฎ → เนื้อหา → ค่า → ด่าน)
| Layer | บรรจุอะไร | ความถี่ในการเปลี่ยนแปลง |
|---|---|---|
| Layer 0 | ประสบการณ์หลักที่เกมต้องการมอบให้ผู้เล่น บีบอัดได้เป็นประโยคเดียว | ต่ำมาก (ตลอดอายุของโครงการ) |
| Layer 1 | โครงสร้างใหญ่ของระบบเกมและโครงสร้างของโลก | ต่ำ (ระดับไมล์สโตน) |
| Layer 2 | โฟลว์การเล่น เส้นเควสต์ ขั้นความคืบหน้า เส้นโค้งเลเวล | ปานกลาง (ระดับสปรินต์) |
| Layer 3 | ค่าข้อมูลจริง พารามิเตอร์ สูตร ตัวแปร | สูง (ระดับวัน) |
| Layer 4 | ผลที่ยืนยันได้จากบิลด์ รายงานบั๊ก วิดีโอการเล่น | สูงมาก (เรียลไทม์) |
5 ชั้นนี้ไม่ใช่แนวคิดเฉพาะเกม กระดูกสันหลังเดียวกันนี้สามารถย้ายไปใช้กับการพัฒนาผลิตภัณฑ์ IT ทั่วไปได้โดยตรง ผู้อ่านที่ไม่เคยทำเกมมาก่อน ขอให้ใช้ตารางแปลงสายงานด้านล่างเพื่อจับคู่แต่ละชั้นกับผลงานของตัวเอง (ด้านซ้ายคือ Layer ของการออกแบบเกม ด้านขวาคือผลงานที่อยู่ในตำแหน่งเดียวกันใน SaaS แอป หรือระบบภายในองค์กร เป็นต้น)
| Layer | การออกแบบเกม | ผลิตภัณฑ์ IT ทั่วไป | คำถามเดียวกัน |
|---|---|---|---|
| L0 ประสบการณ์หลัก | ประสบการณ์หลักที่ต้องการมอบให้ผู้เล่น (หนึ่งประโยค) | วิสัยทัศน์ผลิตภัณฑ์ — แก้ปัญหาอะไรของใครอย่างไร | "ทำไมต้องสร้างสิ่งนี้" |
| L1 กฎของระบบ | โครงสร้างระบบ · โครงสร้างของโลก | กฎเชิงธุรกิจ · ฟังก์ชัน — กฎโดเมน โมเดลสิทธิ์ เวิร์กโฟลว์หลัก | "อะไรควรทำงานอย่างไร" |
| L2 เนื้อหา | เส้นเควสต์ · ขั้นความคืบหน้า · เส้นโค้งเลเวล | รีลีส · โรดแมป — กลุ่มฟีเจอร์ ลำดับการปล่อย ไมล์สโตน | "จะปล่อยอะไรเมื่อไร" |
| L3 ข้อมูล | ค่าข้อมูล · พารามิเตอร์ · สูตร | ชีตสเปก — API spec นิยามฟิลด์ ค่าตั้งค่า ค่าเกณฑ์ | "ค่าและนิยามที่แม่นยำคืออะไร" |
| L4 บิลด์ · QA | ผลบิลด์ · บั๊ก · วิดีโอการเล่น | การดีพลอย · QA — ผลผลิตการดีพลอย รายงานบั๊ก ล็อกมอนิเตอริง | "สิ่งที่ปล่อยออกไปจริงทำงานถูกต้องหรือไม่" |
วิธีอ่านเหมือนกับเกมทุกประการ ยิ่งสูงขึ้นยิ่งเปลี่ยนแปลงน้อย (วิสัยทัศน์ผลิตภัณฑ์เปลี่ยนปีละครั้งต่อไตรมาส) ยิ่งต่ำลงยิ่งเปลี่ยนบ่อย (ค่าตั้งค่าเปลี่ยนทุกวัน) อุบัติเหตุไซโลที่เห็นก่อนหน้านี้ — ฉากที่ระยะของสกิลกับความกว้างของทางเดินขัดแย้งกัน — มีโครงสร้างเหมือนกันเป๊ะกับเรื่องที่เกิดใน IT ทั่วไปอย่าง "นิยามฟิลด์ฝั่ง backend (L3) กับกฎหน้าจอฝั่ง front (L1) ไม่ตรงกันจนระเบิดก่อนปล่อยไม่นาน" ต่างกันแค่ชื่อสาขา แต่กระดูกสันหลังมีเพียงหนึ่งเดียว
5 ชั้นนี้ไม่ใช่สิ่งสัมบูรณ์ ขึ้นอยู่กับขนาดและโดเมน 4 ชั้นอาจเหมาะสม หรืออาจต้องการ 6 ชั้น หัวใจสำคัญไม่ใช่ว่าตัวเลขเป็น 5 แต่เป็นตัวการกระทำของการนิยามชั้นอย่างชัดแจ้งเอง
ผลงานหนึ่งชิ้นอาจคร่อมสอง Layer ได้ "GDD ระบบสกิล (Game Design Document หรือเอกสารสเปกละเอียด)" บรรจุทั้งการออกแบบระบบ (Layer 1) และข้อมูลรูปธรรม (Layer 3) ไว้พร้อมกัน ในกรณีนี้ให้แบ่งเอกสารออกหรือกำหนด Layer หลักเป็น 1 แล้วแยกส่วนข้อมูลออกไปเป็นชีตต่างหาก ไม่ว่าจะวิธีใดก็ต้องระบุชัดเจนว่าแต่ละส่วนอยู่ใน Layer ใด
สาขางานแผ่ออกในแนวนอน และ Layer ซ้อนกันในแนวตั้ง งานของสาขาหนึ่งคร่อมหลาย Layer เมทริกซ์ด้านล่างแสดงจุดศูนย์ถ่วงของการกระจายของ 11 สาขา (แนวนอน) × Layer 0\~4 (แนวตั้ง) ด้วยความเข้มสีของช่อง ช่องที่เข้มคือ Layer ที่เป็นจุดศูนย์ถ่วงของสาขานั้น
อ่านในแนวตั้งจะเห็นว่าสาขาหนึ่งคร่อมอยู่ใน Layer ใดบ้าง อ่านในแนวนอนจะเห็นว่ามีสาขาใดบ้างที่มารวมอยู่ใน Layer หนึ่ง แถว L0 (วิสัยทัศน์) มีเนื้อเรื่องและอาร์ตไดเรกชันเข้มที่สุด — สองสาขาที่ใกล้วิสัยทัศน์มากที่สุด แถว L3 (ข้อมูล) มีระบบ ต่อสู้ เลเวล บาลานซ์ และตัวละครมารวมกันอย่างเข้มข้น — เป็นสัญญาณว่าสาขาเหล่านี้ปะทะกันที่ชีตข้อมูล
หากมีการกระจายนี้อยู่อย่างชัดแจ้ง สาขาอื่นจะรู้ตำแหน่งได้ทันทีว่า "ต้องไปดู Layer 2 ของฝ่ายต่อสู้" ไม่ใช่กำแพงของไซโลพังลง แต่เป็นการเจาะหน้าต่างบนกำแพงต่างหาก
หากย่อเมทริกซ์ทั้งหมดเป็นประโยคเดียวก็คือเช่นนี้ แกนตั้ง Layer แบ่งไว้เพื่อทำให้การสร้างเป็นอัตโนมัติ แกนนอนสาขาแบ่งไว้เพื่อรักษาความเชี่ยวชาญ ทั้งสองมาพบกันที่ช่องหนึ่งของตาราง
โปรเจกต์ MMORPG A ที่ผู้เขียนดูแลในฐานะ Design Director ได้ใช้ระบบ Layer ร่วมกับทีมออกแบบ (4\~5 คน) มาประมาณ 6 เดือน (ทีมพัฒนาทั้งหมดเป็นขนาดกลาง 10\~50 คน) ลองดูกรณีตัวอย่างที่เป็นรูปธรรม
ก่อนอื่น เนื้อเรื่อง 5 ชั้น ตัวโฟลเดอร์ของฝ่ายออกแบบเนื้อเรื่องเองถูกแบ่งออกเป็น Layer
หากนักเขียนเนื้อเรื่องเปลี่ยนแขนงหนึ่งของเรื่องหลักใน Layer 2 ก็จะส่งผลต่อชีตบทพูดใน Layer 3 และอาจส่งผลแบบย้อนกลับไม่ได้ต่อเสียงพากย์ใน Layer 4 ที่อัดเสียงไปแล้ว เพราะระบุ Layer ไว้ชัด จึงติดตามขอบเขตผลกระทบได้ทันที
เครื่องมือสร้างแผนผังความสัมพันธ์อัตโนมัติ gen_relation_map.py ก็ทำงานควบคู่กันด้วย มันวิเคราะห์ความสัมพันธ์ foreign key (FK) ระหว่างชีตข้อมูล สร้างแผนผังความสัมพันธ์ HTML แบบโต้ตอบได้ และแสดง Layer ด้วยสีของโหนด (แดง=L1 ระบบ, เหลือง=L2 เนื้อหา, เขียว=L3 ข้อมูล) มองเห็นได้ในพริบตาว่าการพึ่งพิงไหลจาก Layer ใดไปยัง Layer ใด หากการพึ่งพิงไหลย้อนทาง — หาก L3 ยิงลูกศรไปหา L1 — เกือบจะเป็นข้อบกพร่องในการออกแบบเสมอ
เอกสารมาสเตอร์การสร้างเลเวลแบบโพรซีเดอรัลระบุพิกัด Layer ไว้ใน frontmatter
---
title: มาสเตอร์การออกแบบเลเวลแบบโพรซีเดอรัล v0.1
layer_inputs: [L1.World, L2.StoryLine]
layer_outputs: [L3.LevelData, L4.PlayCapture]
---
สองบรรทัดนี้ประกาศว่า "ไปป์ไลน์นี้รับ Layer 1·2 เป็นอินพุตและสร้าง Layer 3·4 ออกมา" และการคำนวณขอบเขตผลกระทบเมื่อมีการเปลี่ยนแปลงก็เป็นอัตโนมัติ วิสัยทัศน์ L0 แม้ไม่ระบุก็เป็นอินพุตเสมอ — เพราะไม่ว่าจะเป็นการสร้างใด จุดยึดวิสัยทัศน์ก็พ่วงตามมาทุกครั้ง
ยังมี atom ที่บังคับให้มี Layer prefix ในชื่อเอกสารด้วย หนึ่งใน atom ที่แชร์กันในทีมเป็นเช่นนี้
docs_layer_numeric_prefix_naming: ชื่อไฟล์ชีตข้อมูลต้องมี Layer prefix ของหมายเลข (L1_,L2_,L3_) เสมอ ชีตที่ไม่มี prefix จะถูกเตือนในการตรวจสอบความสอดคล้อง
กฎยิ่งเรียบง่ายยิ่งทรงพลัง เพียงเรียงตามชื่อก็จัดกลุ่มตาม Layer ได้แล้ว และเครื่องมือ AI ก็รู้ Layer ได้จากชื่อไฟล์เพียงอย่างเดียว แม้คนจะลืม การตรวจสอบความสอดคล้องก็จับได้
ในหัวข้อก่อนหน้า (2.3.4) กล่าวไว้ว่า "หาก L3 ยิงลูกศรไปหา L1 ก็เกือบจะเป็นข้อบกพร่องในการออกแบบเสมอ" หากมอบการตรวจจับนี้ให้ AI แทนสายตามนุษย์ จะเป็นอย่างไร ผู้เขียนจะถ่ายทอดหนึ่งช่วงที่รันจริงตามนั้นโดยไม่ขัดเกลา (ปกปิดเฉพาะข้อมูลที่ระบุตัวบริษัทเท่านั้น)
[พรอมต์ฉบับเต็ม]
ดู frontmatter สองอันที่แนบมาแล้วช่วยตรวจว่าทิศทางการพึ่งพิงของ Layer ปกติหรือไม่
การอ้างอิงต้องไหลจากหมายเลขสูง→หมายเลขต่ำเท่านั้นจึงจะปกติ ถ้าย้อนทางให้เตือน
--- เอกสาร A (L1_combat_system.md) ---
title: โครงสร้างระบบต่อสู้
layer: L1
refs: [L3_skill_damage_sheet, L2_combat_flow]
--- เอกสาร B (L3_skill_damage_sheet.md) ---
title: ชีตดาเมจสกิล
layer: L3
refs: [L1_combat_system]
[ผลลัพธ์ดิบจาก Claude — ตามจริงโดยไม่มีการสรุปย่อ]
ได้ตรวจสอบเอกสารทั้งสองแล้ว
เอกสาร B (L3_skill_damage_sheet, L3):
refs → [L1_combat_system]
ทิศทาง: L3 → L1 (รูปธรรม → นามธรรม)
ผลวินิจฉัย: ปกติ การที่ชีตข้อมูลอ้างอิงโครงสร้างระบบ
ที่ตัวเองยึดถือเป็นทิศทางที่เป็นธรรมชาติ
เอกสาร A (L1_combat_system, L1):
refs → [L3_skill_damage_sheet, L2_combat_flow]
- L1 → L2 : ปกติ (การออกแบบชี้ไปยังโฟลว์ระดับล่าง)
- L1 → L3 : ⚠ เตือนทิศทางย้อนกลับ
โครงสร้างระบบ L1 อ้างอิงชีตข้อมูล L3 โดยตรง
นี่หมายความว่า "เอกสารออกแบบขึ้นต่อค่าข้อมูล
เฉพาะค่าหนึ่ง" ก่อให้เกิดการยึดเกาะที่ทำให้ต้อง
แก้เอกสารออกแบบทุกครั้งที่ข้อมูลเปลี่ยน
ข้อแนะนำ: ลบ L3_skill_damage_sheet ออกจาก refs ของเอกสาร A
หากโครงสร้างระบบจำเป็นต้องรู้ค่าตัวเลขที่เป็นรูปธรรม
ให้ย้ายค่านั้นขึ้นเป็นกฎนามธรรมใน L1 (เช่น "ดาเมจพื้นฐาน
คือ สัมประสิทธิ์คลาส × เลเวล") หรือผ่านเอกสารโฟลว์ L2
สรุป: ตรวจพบการอ้างอิงย้อนทาง 1 รายการ (A: L1 → L3)
[การตรวจสอบโดยมนุษย์ / การปฏิเสธ]
ผลวินิจฉัยของ AI ถูกต้อง แต่ข้อแนะนำว่า "ให้ผ่านเอกสารโฟลว์ L2" นั้นเกินไปในเคสนี้ สิ่งที่ L1 จำเป็นต้องรู้คือ รูปแบบ ของสูตรดาเมจ ไม่ใช่เซลล์เฉพาะใดของชีต ผู้เขียนจึงรับเฉพาะข้อแนะนำที่สอง (การยกขึ้นเป็นกฎนามธรรม) และปฏิเสธข้อเสนอให้ผ่านโฟลว์
[การร้องขอใหม่]
ตัดข้อเสนอให้ผ่านโฟลว์ออก แล้วแสดงเฉพาะ diff การแก้ไข
ของ L1_combat_system.md ในทิศทาง "ยกเฉพาะรูปแบบของสูตร
ดาเมจขึ้นเป็นกฎนามธรรมใน L1" เท่านั้น
จัดระเบียบ refs ด้วย
หนึ่งวงรอบนี้คือฐานที่มั่นของการตรวจจับการอ้างอิงย้อนทาง AI จับการละเมิดทิศทาง (อัตโนมัติ) คนเล็มความเหมาะสมของข้อแนะนำ (การตรวจสอบ) และสั่งทำใหม่เฉพาะงานที่แคบลงแล้ว (การร้องขอใหม่) ในโปรเจกต์ A นั้น gen_relation_map.py ทำงานในระดับกราฟ และ atom portal_layer_change_impact_check จะถูกกระตุ้น ณ จุดตรวจจับการเปลี่ยนแปลง เพื่อบังคับให้ตรวจสอบขอบเขตผลกระทบ
หากคนทำการเปรียบเทียบนี้เองโดยตรง การเปิดเอกสารสองฉบับมาเทียบ refs และวินิจฉัยทิศทางจะใช้เวลาหลายนาที หากเอกสารเพิ่มเป็นหลายร้อยฉบับ ก็แทบเป็นไปไม่ได้ การอ้างอิงย้อนทางมักแอบเข้ามาทีละหนึ่งสองรายการอยู่เสมอ และระเบิดในบิลด์ก็ต่อเมื่อผ่านไปนานแล้ว
จุดประสงค์ที่ผิวเผินของการรวม Layer คือการคลายไซโลและการทำให้ภาษาในการทำงานร่วมกันเป็นหนึ่งเดียว (2.3.1\~2.3.5) แต่จุดประสงค์ที่แท้จริงลึกลงไปอีกขั้นหนึ่ง เมื่อการแยกย่อย Layer ลงตัวแล้ว เงื่อนไขเบื้องต้นของการสร้างแบบโพรซีเดอรัลและการทำงานอัตโนมัติก็พร้อม
กรณีปฏิบัติงานในสองหัวข้อก่อนหน้าเป็นขั้นที่คนตัดสินใจและ AI ช่วยตรวจสอบและฉีดข้อมูล ขั้นถัดไปจะเข้าสู่ขั้นที่ AI สร้างผลผลิตของตัวสาขาเองเป็นตัวเลือกและคนเป็นผู้รับเลือก เหตุผลที่การแยกย่อย Layer เป็นเงื่อนไขเบื้องต้นของการเคลื่อนนี้มีสามประการ ① การสร้างตัวเลือกของ AI ต้องระบุได้ว่า "จะสร้างอะไรของ Layer ใด" ② การตรวจสอบความสอดคล้องอัตโนมัติทำงานได้ต่อเมื่อทิศทางการพึ่งพิงระหว่าง Layer ถูกทำให้เป็นมาตรฐาน (การตรวจจับการอ้างอิงย้อนทางใน 2.3.5) ③ การคำนวณผลกระทบจากการเปลี่ยนแปลงอัตโนมัติทำได้ก็ต่อเมื่อมีพิกัดว่าการเปลี่ยนแปลงเกิดขึ้นที่ Layer ใด ทั้งสามมารวมกันที่ "หากไม่มีการแยกย่อย Layer การทำงานอัตโนมัติเองก็ถูกปิดกั้น" ปลายทางของมือที่แบ่งพิกัดมีการสร้างแบบโพรซีเดอรัลรออยู่ตั้งแต่แรก
ในขั้นที่ยังไม่ได้ดูส่วนของแต่ละสาขา จึงไม่จำเป็นต้องลงลึก เราจะจับโครงไว้เพียงสองขั้นของการประยุกต์ การประยุกต์เชิงอนุรักษ์ คือคนตัดสินใจ และ AI ช่วยตรวจสอบความสอดคล้อง คำนวณผลกระทบจากการเปลี่ยนแปลง และฉีดข้อมูลแบบ JIT โดยอัตโนมัติ — กรณีปฏิบัติงานใน 2.3.4·2.3.5 อยู่ตรงนี้ ต้นทุนของเครื่องมือต่ำและผลสะสมจะเผยตัวราวเดือนที่ 6 ของการใช้งาน ทีมขนาดกลาง (10\~50 คน) ส่วนใหญ่จึงเข้าถึงได้ การประยุกต์เชิงก้าวหน้า ก้าวไปอีกขั้นโดยให้ AI สร้างผลผลิตของตัวสาขาเองเป็นตัวเลือก (เนื้อเรื่อง Persona, rulebook PCG, เลเวลแบบโพรซีเดอรัล, ตัวเลือกการเปลี่ยนบาลานซ์, แอสเซตอาร์ต เป็นต้น) และคนตัดสินใจเพียง "จะรับตัวเลือกใด" เท่านั้น รูปแบบของแต่ละสาขาและความสุกงอมของเครื่องมือจะกล่าวถึงในส่วนของสาขานั้นๆ
3 องค์ประกอบที่การประยุกต์เชิงก้าวหน้าต้องการร่วมกันในทุกสาขาคือ ① โครงสร้างพื้นฐานของการแยกและติดป้าย Layer (frontmatter · atom · prefix ชื่อไฟล์) ② วงรอบการสร้างและประเมินตัวเลือก (ตัวเลือกจาก AI จำนวน N → ประเมินอัตโนมัติ → รายงานอันดับและเหตุผล) ③ ด่านตรวจสอบโดยมนุษย์ (เฉพาะผลที่ถูกรับเลือกเท่านั้นที่ไปสู่ Layer ถัดไป) ทั้งนี้ ไม่ว่าจุดใดก็ตาม แกนกลางเชิงกำหนด (การจำลอง · ฟิสิกส์ · ข้อจำกัดทางกฎหมาย) ยังคงเป็นความรับผิดชอบของคนและโค้ดเชิงกำหนด และการตรวจสอบทั้งหมดต้องจบลงในขั้นที่ย้อนกลับได้ก่อนเข้าสู่ขั้นที่ย้อนกลับไม่ได้ (การอัดเสียง · การแคสต์ · การเปิดเผยในไลฟ์ เป็นต้น) — เส้นแบ่งย้อนกลับได้/ย้อนกลับไม่ได้นี้เป็นหลักการร่วมของทุกสาขา
สุดท้าย จุดเรื่องเวลาหนึ่ง การประยุกต์เชิงอนุรักษ์เป็นไปได้บางส่วนแม้ในทศวรรษ 2010 แต่การประยุกต์เชิงก้าวหน้าติดอยู่ที่สามขีดจำกัด คือ พลังการแสดงออกของการสร้างตัวเลือกด้วย AI การตีความภาษาธรรมชาติของการประเมินอัตโนมัติ และภาระการตรวจสอบโดยมนุษย์ หลังจาก LLM พัฒนาขึ้น ทั้งสามก็เข้าสู่ขอบเขตที่ใช้งานได้จริง และการประยุกต์เชิงก้าวหน้าก็ลงจากวิสัยทัศน์บนกระดาษสู่ขั้นปฏิบัติงาน สารเมตาที่ร้อยทะลุทั้งเล่มนี้ — ว่าการพัฒนาของ AI ได้ยกระดับความเป็นไปได้ของการสร้างแบบโพรซีเดอรัลและการทำงานอัตโนมัติ — อยู่ตรงนี้
ส่วนของแต่ละสาขาในหนังสือเล่มนี้จะระบุในบทนำว่าแต่ละสาขากระจายอยู่ใน Layer ใดเป็นหลัก และใช้พิกัด Layer บ่อยภายในบทด้วย ขอจัดระเบียบไว้ล่วงหน้า (เป็นการย้ายจุดศูนย์ถ่วงของเมทริกซ์ 2.3.3 มาเป็นตาราง)
| สาขา | Layer หลัก | หมายเหตุ |
|---|---|---|
| ออกแบบระบบ | L1\~L3 | ตั้งแต่โครงสร้างออกแบบจนถึงชีตข้อมูลอย่างกว้างขวาง |
| ออกแบบต่อสู้ | L1\~L3, L4 บางส่วน | ตั้งแต่โครงสร้างคอมโบ\~ชีตดาเมจ การวัดในบิลด์ |
| ออกแบบเนื้อเรื่อง | L0\~L4 | ใช้งานโครงสร้าง 5 ชั้นเป็นโฟลเดอร์ |
| ออกแบบเนื้อหา | เน้น L2 | โฟลว์ความคืบหน้า · เส้นเควสต์ |
| Level Design | L2\~L3 | รวมไปป์ไลน์การสร้างแบบโพรซีเดอรัล |
| ปรับบาลานซ์ | เน้น L3, วัด L4 | ค่าข้อมูล · เส้นโค้ง · การวัดเพื่อตรวจสอบ |
| ออกแบบ UX/UI | L1\~L3 | ตั้งแต่โครงสร้างปฏิสัมพันธ์\~ข้อมูลหน้าจอ |
| ออกแบบ QA | เน้น L4, ตรวจสอบ L0\~L3 | ตรวจว่าทุก Layer ถูกสะท้อนในบิลด์หรือไม่ |
| ตัวละคร · เพ็ต · เมาท์ | L1\~L3 | ระบบ · โลก · ข้อมูล |
| อาร์ตไดเรกชัน | L0\~L1 + ผลผลิต L4 | ไกด์วิสัยทัศน์ · โลก + การตรวจสอบบิลด์ |
| Live Ops | L2\~L4 | วงจรการให้บริการ · ข้อมูลเรียลไทม์ |
แต่ละสาขาแตะ Layer อื่นด้วย แต่หากรู้จุดศูนย์ถ่วงก็จะเห็นช่องทางการทำงานร่วมกัน บาลานซ์ (L3) กับไลฟ์ (L2\~L4) มาพบกันที่ L3 จึงต้องทำงานร่วมกันใกล้ชิดเสมอ และสองสาขาที่ใกล้วิสัยทัศน์ (L0) มากที่สุดคือเนื้อเรื่องและอาร์ตไดเรกชัน ความสัมพันธ์ที่ติดกันเช่นนี้เผยตัวอย่างเป็นธรรมชาติบนระบบพิกัด
หากพยายามนำระบบ Layer มาใช้ให้สมบูรณ์แบบตั้งแต่แรก ก็จะเริ่มต้นไม่ได้ด้วยซ้ำ การค่อยๆ นำมาใช้แบบทีละขั้นต่างหากคือคำตอบ
flowchart LR
S1["ขั้นที่ 1
นำมาใช้ในสาขาเดียว
(แนะนำ: เนื้อเรื่อง)"] --> S2["ขั้นที่ 2
ขยายไปสาขาที่ติดกัน
(เนื้อเรื่อง+เนื้อหา)"]
S2 --> S3["ขั้นที่ 3
ทำให้เป็นมาตรฐานทุกสาขา
(prefix ชื่อไฟล์·ตรวจสอบความสอดคล้อง)"]
S3 --> S4["ขั้นที่ 4
ผสานเครื่องมือ AI
(JIT·ผลกระทบจากการเปลี่ยนแปลง·จำแนกการทบทวน)"]
S1 -.->|"ทีมขนาดเล็ก (~10 คน)"| E1["แค่นี้ก็พอ"]
S3 -.->|"ทีมขนาดกลาง (10~50 คน)"| E2["แนะนำถึงตรงนี้"]
S4 -.->|"ทีมขนาดใหญ่ (100+ คน)"| E3["ต้องไปถึงตรงนี้จึงจะเห็นผล"]
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class S1,S2,S3,S4 human
class E1,E2,E3 pass
L1_·L2_·L3_ prefix) มาใช้ และทำให้แผนผังความสัมพันธ์·การตรวจสอบความสอดคล้องเป็นอัตโนมัติแต่ละขั้นใช้เวลาอย่างน้อยหนึ่งเดือน นานสุดเป็นระดับไตรมาส หากฝืนเกินไป คนก็จะหมดแรง การปรับความเร็วไม่ให้ภาระการใช้งานเกินคุณค่าที่ได้จากการนำมาใช้คืองานของผู้กำกับ
ขนาดเล็ก (\~10 คน) ทำขั้นที่ 1\~2 ขนาดกลาง (10\~50 คน) ทำขั้นที่ 3 ขนาดใหญ่ (100+ คน) ต้องไปถึงขั้นที่ 4 จึงจะเห็นผล ไม่ได้หมายความว่าทีมเล็กใช้ไม่ได้ ต่างกันแค่ความลึก แต่คุณค่าหลักเริ่มต้นแล้วตั้งแต่ขั้นที่ 1
Layer ไม่ใช่เพียงเทคนิคจัดระเบียบโฟลเดอร์ มันคือหลักการเมตาที่มัดการออกแบบเกมที่แตกแขนงไว้ด้วยระบบพิกัดเดียวเพื่อให้ AI สามารถใช้เหตุผลได้ และยิ่งไปกว่านั้นยังเป็นเงื่อนไขเบื้องต้นร่วมของการสร้างแบบโพรซีเดอรัลและการทำงานอัตโนมัติของแต่ละสาขา
ส่วนที่เหลือทั้งหมดของหนังสือเล่มนี้ตั้งอยู่บนสมมติฐานของบทนี้ ส่วนของแต่ละสาขาจะระบุพิกัดที่สาขานั้นครองอยู่ใน Layer ไว้ในบทนำ ส่วนกระบวนการจะกล่าวถึงระบบการใช้งานที่ตัดข้าม Layer และส่วนการให้บริการจะกล่าวถึงวงจร self-improving ของตัวระบบ Layer เอง
บทถัดไป (ภววิทยาเกมและกราฟความรู้) เพิ่มความสัมพันธ์เชิงความหมายลงบน Layer หาก Layer คือพิกัด ภววิทยาก็คือลูกศรเชิงความหมายบนพิกัดนั้น ทั้งสองต้องมารวมกันจึงจะทำให้ AI ใช้เหตุผลด้วยตัวเองได้ว่า "เอกสารนี้ส่งผลต่อเอกสารนั้น"
มีสิ่งที่ต้องทำให้ชัดเจน ไม่มีการทำงานอัตโนมัติใดในบทนี้ที่ตัดสินใจแทน ในการตรวจจับการอ้างอิงย้อนทาง เครื่องเพียงคลี่ตัวเลือกที่ละเมิดออกมาวางไว้ แต่ผู้ที่เลือกว่าจะรับอะไรไปถึงไหนคือมือของคน Layer คือระบบพิกัดที่ช่วยให้คนตัดสินใจได้ดีขึ้นและเร็วขึ้น ไม่ใช่อุปกรณ์ที่โยนการตัดสินใจทิ้งไป
layer_unified_design_philosophy — atom แม่แบบของบทนี้docs_layer_numeric_prefix_naming — กฎบังคับ prefix ชื่อไฟล์dead_table_5layer_cleanup — กฎจัดระเบียบชีตนอก 5 ชั้นportal_layer_change_impact_check — ตรวจสอบผลกระทบจากการเปลี่ยนแปลงอัตโนมัติsetup — เลือกโฟลเดอร์ของสาขาหนึ่ง (แนะนำ: เนื้อเรื่อง) แล้วแบ่งโฟลเดอร์ย่อยออกเป็น 5 อันตั้งแต่ Layer0_Vision/ จนถึง Layer4_BuildVO/ ย้ายไฟล์เดิมไปยัง Layer ที่เหมาะสม ไฟล์ที่ชื่อกำกวมให้จัดวางโดยใช้เกณฑ์ "ความถี่ที่เอกสารนี้เปลี่ยนแปลง" (ยิ่งเปลี่ยนบ่อยยิ่งอยู่ Layer ล่าง)
prompt — เลือก frontmatter ของชีตข้อมูลสองอัน แล้ววางพรอมต์ฉบับเต็มของ 2.3.5 ลงไปตามนั้น ให้วินิจฉัยทิศทางการพึ่งพิงของ Layer เพียงให้กฎหลักหนึ่งบรรทัดอย่างแม่นยำก็พอ "การอ้างอิงต้องไหลจากรูปธรรม→นามธรรม (หมายเลขสูง→หมายเลขต่ำ) เท่านั้นจึงจะปกติ"
verify — เมื่อ AI จับการอ้างอิงย้อนทางได้ อย่ารับข้อแนะนำนั้นมาตามนั้น แต่ให้เล็มความเหมาะสมด้วยตัวเอง (ดู "การตรวจสอบโดยมนุษย์/การปฏิเสธ" ของ 2.3.5) ร้องขอใหม่เป็น diff เฉพาะทิศทางที่รับเลือก หากเรียงตามชื่อแล้วเห็น Layer มัดรวมจากบนลงล่าง แสดงว่ากฎ prefix ลงตัวแล้ว
แม้ทำงานคนเดียว Layer ก็ทำงานได้ เพราะไม่มีทีมจึงไม่มี "ไซโลระหว่างสาขา" แต่มี "ไซโลระหว่างช่วงเวลา" ตัวเราเมื่อ 3 สัปดาห์ก่อนกับตัวเราวันนี้ลืมการตัดสินใจของกันและกัน เพียงแบ่งโฟลเดอร์ออกเป็น Layer 0\~4 — วิสัยทัศน์หนึ่งแผ่น โครงสร้างระบบไม่กี่แผ่น โฟลว์ความคืบหน้า ชีตข้อมูล บันทึกบิลด์ — ก็หาได้ทันทีว่าตัวเราในอดีตวางอะไรไว้ในช่องไหน เพียงพ่วงหนึ่งบรรทัดให้ AI ว่า "ตอนนี้กำลังทำงาน L2 อยู่" มันก็จะไม่ดึงข้อมูล Layer ที่ไม่เกี่ยวข้องเข้ามา จะลดเหลือ 4 ชั้น · 3 ชั้นก็ได้ หัวใจสำคัญไม่ใช่ตัวเลข แต่เป็นตัวการกระทำของการ "ระบุชั้นอย่างชัดแจ้ง"
เช้าวันจันทร์ มีคำขอเปลี่ยนแปลงหนึ่งรายการถูกส่งเข้ามา สมาชิกทีมต่อสู้ A เขียนข้อความหนึ่งบรรทัดลงในแชตภายในทีม "ผมจะเปลี่ยน global cooldown จาก 0.5 วินาทีเป็น 0.3 วินาทีนะ มีที่ไหนได้รับผลกระทบไหม" หากเป็นปกติแล้ว จากตรงนี้จะเริ่มต้นการประชุมยาว 30 นาที คนดูแลสูตรคำนวณดาเมจยกมือขึ้น คนดูแลกฎ combo cancel ก็แทรกเข้ามา และมีใครบางคนถามว่า "แพตเทิร์นบอสไม่ได้รับผลกระทบด้วยหรือ" เมื่อไม่มีใครเก็บภาพรวมทั้งหมดไว้ในหัว การประชุมจึงเต็มไปด้วยการพยายามรื้อความทรงจำ
แต่คราวนี้ต่างออกไป หนึ่งวินาทีหลังจากส่งคำขอ บอตได้แสดงคอมเมนต์อัตโนมัติขึ้นมา "หากเปลี่ยน atom นี้ จะมี atom 4 รายการที่ได้รับผลกระทบ ได้แก่ skill_dps_calculation, combat_combo_cancel_v3, refgame_boss_pattern_phase2, balance_curve_v3 ผู้รับผิดชอบ: สมาชิกทีม B, สมาชิกทีม A, สมาชิกทีม C" การประชุมไม่ถูกเปิดขึ้น ทั้ง 4 คนต่างตรวจสอบเฉพาะ atom ของตัวเองแล้วก็จบ (บอตตัวนี้เราจะสร้างขึ้นเองในภายหลัง — 2.4.3)
คอมเมนต์นี้ไม่ใช่เวทมนตร์ ในหัวข้อ 2.3 เราได้กำหนดพิกัด Layer ให้กับ atom ทุกตัวไปแล้ว และบนพิกัดนั้น ในบทนี้เราได้เพิ่ม ลูกศรเชิงความหมาย — การตัดสินใจใดส่งผลต่อการตัดสินใจใด — เข้าไป พิกัดบอกได้เพียงแค่ "ตรงนี้มีอะไรอยู่" เท่านั้น ส่วนความสัมพันธ์อย่าง "สิ่งนี้ส่งผลต่อสิ่งนั้น" "สิ่งนั้นต้องมีอยู่ก่อนจึงจะตั้งอยู่ได้" "สองสิ่งนี้เปิดพร้อมกันไม่ได้" คือลูกศรที่ถูกวาดลงบนพิกัด บทนี้จะกล่าวถึงว่าจะเขียนกำกับลูกศรเหล่านั้นอย่างไร และจะตรวจจับลูกศรที่ขาดโดยอัตโนมัติได้อย่างไร
บันทึกศัพท์ - ออนโทโลยี (ontology): ระบบที่นิยามแนวคิดและความสัมพันธ์ระหว่างแนวคิดเหล่านั้นอย่างชัดเจน ในหนังสือเล่มนี้เราใช้เวอร์ชันเบาที่ลดรูปให้เหลือความสัมพันธ์ 6\~12 แบบ - wikilink: ลิงก์ระหว่างเอกสารในรูปแบบ
[[atom_name]]ยืมการเขียนกำกับมาจากเครื่องมืออย่าง Obsidian และ Roam - การอ้างกลับ (backlink): รายการของ "atom ทั้งหลายที่ชี้มายัง atom นี้" ทิศทางตรงข้ามกับการอ้างไปข้างหน้า - โหนดกำพร้า (orphan): atom ที่ไม่ถูกอ้างถึงจากที่ใดเลย เป็นสัญญาณว่าเป็นผู้สมัครสำหรับการเลิกใช้ - ลิงก์ที่ขาด (broken link): wikilink ที่ชี้ไปยัง atom ซึ่งไม่มีอยู่จริง ร่องรอยของการพิมพ์ผิดหรือการเปลี่ยนชื่อ
ในหัวข้อ 2.1 เราได้แนบเมตาดาตาด้วย YAML frontmatter และโปรย wikilink ไว้ในเนื้อหาของ atom เพียงเท่านั้นเอกสารก็เชื่อมโยงกันเป็นตาข่ายอยู่แล้ว ปัญหาคือการเชื่อมโยงเหล่านั้น ไม่ได้เขียนกำกับไว้ว่าหมายความว่าอะไร
การตัดสินใจนี้ตั้งอยู่บน [[skill_cooldown_rule_v2]]
บรรทัดเดียวนี้บอกได้เพียงแค่ "กล่าวถึง skill_cooldown_rule_v2" เท่านั้น เหตุใดจึงกล่าวถึง การตัดสินใจนี้ ต้องการ (requires) กฎข้อนั้นหรือ แตกแขนงมาจาก (derives_from) กฎข้อนั้นหรือ หรือว่า ขัดแย้งกับ (conflicts_with) กฎข้อนั้น มนุษย์อ่านประโยคแล้วก็เข้าใจ แต่เครื่องไม่รู้ ต่อให้ถาม AI ว่า "ถ้าเปิดการตัดสินใจนี้ มีอะไรพังบ้าง" ลิงก์ที่ไร้ความหมายเพียงอย่างเดียวก็ตอบไม่ได้
ดังนั้นเราจึงสวม ประเภทความสัมพันธ์ ลงบน wikilink ความสัมพันธ์ที่ใช้กันจริงในการออกแบบเกมนั้นน้อยอย่างคาดไม่ถึง หกแบบต่อไปนี้ครอบคลุมได้มากกว่า 90%
atom ที่ตรึงหกแบบนี้ไว้เป็น enum คือ ontology_relation_enum_v1 หากต้องการเพิ่มประเภทความสัมพันธ์ใหม่ จะต้องผ่านการรีวิวคำขอเปลี่ยนแปลง แม้จะเพิ่มขึ้นเส้นที่เหมาะสมก็อยู่ที่ 10\~12 แบบ และในตอนเริ่มต้นจะเริ่มด้วยสามแบบคือ affects·derives_from·requires ก็เพียงพอแล้ว ที่ที่จะเขียนความสัมพันธ์ลงไปคือ YAML frontmatter ของ atom
---
name: combat_combo_cancel_v3
layer: 1
affects: [skill_dps_calculation, refgame_boss_pattern_phase2]
derives_from: [vision_taste_focused_combat]
requires: [combat_input_buffer_system, skill_cooldown_rule_v2]
conflicts_with: [skill_cancel_rule_legacy_v1]
---
มนุษย์เขียนแค่ทิศทางไปข้างหน้าเพียงบรรทัดเดียว ส่วนทิศทางย้อนกลับ ("ใครที่ affects ฉัน") เครื่องมือจะสแกนทั้งหมดแล้วคำนวณให้เอง หากเขียน affects ไว้ทั้งสองฝั่ง ภาระในการซิงค์จะเพิ่มเป็นสองเท่า และวินาทีที่แก้ฝั่งหนึ่งแล้วลืมอีกฝั่ง กราฟก็จะเริ่มโกหก ทิศทางเดียวด้วยมือ ทิศทางตรงข้ามด้วยเครื่องมือ คือหลักการที่ร้อยเรียงทั้งบทนี้เข้าด้วยกัน
จากตรงนี้ไปจะถ่ายทอดเซสชันหนึ่งที่เกิดขึ้นจริงทั้งยวง สถานการณ์เป็นดังนี้ ในขณะที่รื้อกฎ combo cancel จาก v2 ไปเป็น v3 เราตัดสินใจจะเลิกใช้ skill_cancel_rule_legacy_v1 แต่ก่อนจะเลิกใช้นั้น ต้องตรวจสอบก่อนว่า มี atom อื่นที่ชี้มายัง atom นี้หรือไม่ (การอ้างกลับ) และ หลังจากเลิกใช้แล้วจะไม่มีลิงก์ที่ขาดหรือความขัดแย้งหลงเหลืออยู่หรือไม่ (ความสมบูรณ์ของกราฟ) หากใช้มือ grep ก็จะตกหล่น เราจึงให้ Claude ทำ
ในโฟลเดอร์ atom (decisions/) ที่แนบมา มี atom อยู่ และใน frontmatter
น่าจะมีความสัมพันธ์ (affects/derives_from/requires/conflicts_with/is_a/part_of)
เขียนไว้ ในเนื้อหาก็มี wikilink ด้วย ตอนนี้ฉันจะเลิกใช้
skill_cancel_rule_legacy_v1 ก่อนเลิกใช้ช่วยตรวจ 3 อย่างให้หน่อย:
1) การอ้างกลับ 2) ลิงก์ที่ขาด 3) ความขัดแย้ง อย่าเดา ให้อ่านไฟล์จริง ๆ
แล้วตอบ ถ้าหาไม่เจอก็บอกว่าหาไม่เจอ
// การอ้างกลับ=ทั้งความสัมพันธ์ใน YAML และ wikilink ในเนื้อหา / ลิงก์ที่ขาด=ลิงก์ที่กลายเป็น stale·ไม่มีอยู่เพราะการเลิกใช้
// ความขัดแย้ง=ในบรรดาคู่ conflicts_with มี status: approved ที่ยังมีชีวิตอยู่หรือไม่
ได้อ่านไฟล์ 47 ไฟล์ในโฟลเดอร์ decisions/ แล้ว นี่คือผลการตรวจสอบ 3 อย่างเกี่ยวกับ
skill_cancel_rule_legacy_v11. การอ้างกลับ (atom ที่ชี้มายัง atom นี้)
atom ที่ชี้มาด้วยความสัมพันธ์ใน YAML: -
combat_combo_cancel_v3—conflicts_with: [skill_cancel_rule_legacy_v1]-skill_cancel_rule_v2—derives_from: [skill_cancel_rule_legacy_v1]atom ที่ชี้มาด้วย wikilink ในเนื้อหา: - เนื้อหาของ
combat_combo_cancel_v3: "แก้ปัญหา input buffer ของเวอร์ชันเก่า[[skill_cancel_rule_legacy_v1]]" - เนื้อหาของqa_combat_regression_checklist: "รายการ regression ข้อ 3 เขียนขึ้นโดยอ้างอิง[[skill_cancel_rule_legacy_v1]]"กล่าวคือ มี atom 4 รายการที่อ้างถึง atom นี้ (YAML 2 + wikilink ในเนื้อหา 2 แต่ combat_combo_cancel_v3 ปรากฏทั้งสองฝั่ง ดังนั้นจำนวน atom จริงคือ 3 รายการ)
2. ลิงก์ที่ขาด (จุดที่จะขาดเมื่อเลิกใช้)
การเลิกใช้ในตัวมันเองไม่ได้ทำให้ wikilink "ขาด" — เพราะไฟล์ยังอยู่ มีแค่ status ที่กลายเป็น deprecated เท่านั้น อย่างไรก็ตาม: -
qa_combat_regression_checklistนิยามรายการ regression โดยอ้างอิง atom ที่ถูกเลิกใช้ จึง ไร้ผลในเชิงเนื้อหา นี่ไม่ใช่ลิงก์ที่ขาดแต่เป็น stale link จำเป็นต้องตรวจทาน - หากท่าน ลบ ไฟล์ออกไปด้วย การอ้างอิงทั้ง 4 รายการข้างต้นจะกลายเป็นลิงก์ที่ขาดทั้งหมด⚠️ มีจุดหนึ่งที่คลุมเครือ: ในเนื้อหาของ
balance_curve_v3มีลิงก์[[skill_cancel_rule_legacy]](ไม่มี_v1ต่อท้าย) อยู่ ไม่อาจมั่นใจได้จากตัวไฟล์เพียงอย่างเดียวว่านี่เป็นการพิมพ์ผิดที่ตั้งใจจะชี้ไปยัง_v1หรือว่าตั้งใจให้เป็น atom แยกอีกตัว ไฟล์ชื่อskill_cancel_rule_legacyนั้น ไม่มีอยู่ ในโฟลเดอร์ กล่าวคือนี่มีความเป็นไปได้สูงว่าเป็นลิงก์ที่ขาดอยู่แล้ว จำเป็นต้องตรวจสอบ3. ความขัดแย้งที่หลงเหลือ (คู่ conflicts_with ยังมีชีวิตอยู่หรือไม่)
atom ที่ผูกกับ
skill_cancel_rule_legacy_v1ด้วยconflicts_withมีเพียงcombat_combo_cancel_v3ตัวเดียว - status ของcombat_combo_cancel_v3: approved (ยังมีชีวิตอยู่)→ กฎใหม่ที่เคยขัดแย้งกับเป้าหมายการเลิกใช้อยู่ในสถานะอนุมัติตามปกติ นี่เป็นเรื่องปกติ เป็นการแทนที่ตามธรรมชาติที่เลิกใช้เวอร์ชันเก่าและเวอร์ชันใหม่อยู่รอด ไม่ใช่คำเตือนความขัดแย้งที่หลงเหลือ
สรุป: การเลิกใช้ปลอดภัย แต่ให้จัดการสองอย่างก่อน (a) stale link ของ
qa_combat_regression_checklist— อัปเดตรายการ regression ข้อ 3 (b)[[skill_cancel_rule_legacy]]ของbalance_curve_v3— สงสัยว่าพิมพ์ผิด อาจเป็นลิงก์ที่ขาดอยู่แล้ว
ตรงนี้ผมไม่ได้รับผลลัพธ์มาตามนั้นทั้งดุ้น (a) นั้นถูกต้อง — เช็กลิสต์ regression เป็นสิ่งที่ผมต้องอัปเดตเอง แต่ผมหยุดที่ (b) Claude บอกว่า [[skill_cancel_rule_legacy]] ของ balance_curve_v3 "สงสัยว่าพิมพ์ผิด" แต่ผมต้องเปิดโฟลเดอร์ดูเองว่ามันเป็นลิงก์ที่ขาดจริงหรือไม่ คำว่า "ไม่อาจมั่นใจได้" ของ AI เป็นสัญญาณที่ซื่อสัตย์ ไม่ใช่ข้อสรุป
เมื่อเปิดโฟลเดอร์ดูก็พบว่าไฟล์ชื่อ skill_cancel_rule_legacy ไม่มีอยู่จริง แต่เมื่อดู git log ก็พบว่าเมื่อ 6 เดือนก่อนมีคอมมิตที่ เปลี่ยนเพียงชื่อ จาก skill_cancel_rule_legacy → skill_cancel_rule_legacy_v1 ตอนนั้นเองที่ลิงก์ในเนื้อหาของ balance_curve_v3 ไม่ได้รับการอัปเดตและถูกปล่อยผ่านไป กล่าวคือนี่ไม่ใช่การพิมพ์ผิดแต่เป็น ลิงก์ที่ขาดซึ่งกลายเป็นกำพร้าหลังการเปลี่ยนชื่อ การคาดเดาว่า "สงสัยว่าพิมพ์ผิด" ของ Claude ถูกเพียงครึ่งเดียว สาเหตุไม่ใช่การพิมพ์ผิดแต่เป็นการตกหล่นในการ rename หากผมไม่จับความต่างนี้ ก็คงแก้แค่เช็กลิสต์ regression แล้วฝังลิงก์ที่ขาดเอาไว้ตามเดิม
ตรวจสอบแล้ว [[skill_cancel_rule_legacy]] ไม่ใช่การพิมพ์ผิด แต่เป็นลิงก์ที่ขาด
ซึ่งตอน rename เมื่อ 6 เดือนก่อน (_legacy → _legacy_v1) ลืมแก้ลิงก์ในเนื้อหา
งั้นถือโอกาสนี้ ช่วยค้นหา "wikilink ที่ชี้ไปยัง atom ที่ไม่มีอยู่จริง"
ทั้งหมดในโฟลเดอร์ให้ที โดยยึดจาก wikilink ในเนื้อหา ที่ไฟล์เป้าหมายไม่มีในโฟลเดอร์ ทั้งหมด
แสดงชื่อไฟล์และลิงก์ที่ขาดในนั้นเป็นตาราง
ได้รวบรวม wikilink ในเนื้อหาทุก
[[...]]ของไฟล์ทั้ง 47 ไฟล์ แล้วเทียบกับรายชื่อไฟล์ในโฟลเดอร์ ลิงก์ที่ขาดซึ่งไฟล์เป้าหมายไม่มีอยู่จริงมี 3 รายการ
ไฟล์ที่มีลิงก์ wikilink ที่ขาด สาเหตุที่คาดว่า balance_curve_v3[[skill_cancel_rule_legacy]]ตกหล่นในการ rename (→ _v1)quest_reward_table_v2[[concept_reward_currency]]atom เป้าหมายยังไม่ได้เขียน (มีแต่แผน ไม่มีไฟล์) refgame_boss_pattern_phase2[[skill_global_cooldown]]ตกหล่นในการ rename (→ skill_global_cooldown_rule)ใน 3 รายการ 2 รายการ (ตกหล่นในการ rename) แก้ได้โดยเปลี่ยนชื่อ atom เป้าหมายเป็น
_v1/_ruleส่วน 1 รายการ (concept_reward_currency) ตัว atom ที่ชี้ไปนั้นไม่มีอยู่ จึงต้องเขียนขึ้นหรือไม่ก็ลบลิงก์ออก ผมไม่ได้แก้ให้โดยอัตโนมัติ — โปรดตรวจสอบว่าท่านตั้งใจแบบใดก่อนดำเนินการ
ทั้งสามรายการเป็นความจริง หากใช้มือ grep ก็เกือบจะแน่นอนว่าจะพลาดลิงก์ที่ขาดของ quest_reward_table_v2 ลิงก์นั้นเป็น "การอ้างอิงอนาคตที่ตั้งใจ" ซึ่งชี้ล่วงหน้าไปยัง atom ที่ยังไม่ได้สร้าง แต่ตลอด 6 เดือนไม่มีใครสร้าง atom นั้นขึ้นมา มันจึงกลายเป็นคำสัญญาที่ตายไปแล้วในทางพฤตินัย
สิ่งที่เซสชันนี้แสดงให้เห็นนั้นเรียบง่าย การตรวจจับการอ้างกลับและการตรวจจับลิงก์ที่ขาดเป็นงานที่ AI เก่งในการอ่านและเทียบทั้งโฟลเดอร์ ส่วนการตัดสินสาเหตุและการยืนยันเจตนาเป็นงานของมนุษย์ AI ทำได้ถึง "ตรงนี้ขาด" มนุษย์ทำต่อถึง "ขาดเพราะอะไรและจะแก้อย่างไร"
การตรวจสอบในหัวข้อก่อนหน้าจะรันด้วยพรอมต์ทุกครั้งก็ได้ แต่หากตรึงการตรวจสอบเดียวกันนั้นไว้ในโค้ด ก็จะมองเห็นได้ในแวบเดียวบนกราฟ ในโปรเจกต์ A มีเครื่องมือกราฟที่ขยายจาก gen_relation_map.py ที่แนะนำในหัวข้อ 2.3 เดินงานในฐานะ R&D หัวใจคือการอ่าน atom ในโฟลเดอร์มาสร้างเป็นกราฟทิศทางด้วย networkx แล้ววางฟังก์ชันตรวจสอบสี่อย่างทับลงไป
import networkx as nx
# build_graph(folder): อ่านโฟลเดอร์ atom แล้วสร้าง DiGraph
# จากโหนด (=atom) และเอดจ์ความสัมพันธ์ใน YAML (ฉบับเต็มอยู่ใน「ลองทำดู」)
def find_cycles(G): # การพึ่งพาแบบวนรอบ
return list(nx.simple_cycles(G))
def find_orphans(G): # inbound 0 = ผู้สมัครเป็นกำพร้า
return [n for n in G.nodes if G.in_degree(n) == 0]
หัวใจอยู่ที่สองบรรทัด simple_cycles จับการพึ่งพาแบบวนรอบ (A requires B requires C requires A) และ in_degree(n) == 0 จับโหนดกำพร้า — ไม่จำเป็นต้องเขียน DFS เอง ฟังก์ชันอีกสองตัวที่เหลือก็เป็นแบบหนึ่งบรรทัดในระนาบเดียวกัน find_broken_wikilinks รวบรวม [[...]] ในเนื้อหาด้วย regular expression (regex) แล้วคัดเลือกตัวที่ไม่มีอยู่ในรายชื่อโหนด ส่วนการอ้างกลับนั้นได้มาจากการไล่กราฟย้อนทาง (ฉบับเต็มอยู่ใน「ลองทำดู」) การทำให้เป็นภาพจะลงสีโหนดตาม Layer ลงสีเอดจ์ตามประเภทความสัมพันธ์ และวาดโหนดที่ถูกอ้างถึงมาก (โหนดที่มีเอดจ์ inbound เยอะ) ให้ใหญ่ขึ้นเพื่อให้ฮับปรากฏชัด เปรียบเหมือนโฟลเดอร์ที่จัดเรียงโดยติดป้ายสีไว้บนตู้ แพตเทิร์นจะลอยขึ้นมาก่อนภายในขอบเขตสายตา
ด้านล่างคือกราฟที่ถ่ายทอดความสัมพันธ์จริงของ atom ทั้งหลายที่กล่าวถึงในเซสชัน 2.4.2 ทิศทางลูกศรหมายถึง "atom ต้นทางสร้างความสัมพันธ์มุ่งไปยัง atom ปลายทาง"
graph LR
combo[combat_combo_cancel_v3
L1·approved]
legacy[skill_cancel_rule_legacy_v1
L1·deprecated]
v2[skill_cancel_rule_v2
L1]
dps[skill_dps_calculation
L3]
vision[vision_taste_focused_combat
L0]
buffer[combat_input_buffer_system
L1]
boss[refgame_boss_pattern_phase2
L2]
qa[qa_combat_regression_checklist
L4]
broken[skill_cancel_rule_legacy
ไม่มีอยู่จริง]
combo -->|affects| dps
combo -->|affects| boss
combo -->|derives_from| vision
combo -->|requires| buffer
combo -->|conflicts_with| legacy
v2 -->|derives_from| legacy
qa -.stale.-> legacy
balance[balance_curve_v3] -.broken.-> broken
classDef dep fill:#eee,stroke:#999,stroke-dasharray:4
classDef miss fill:#fff,stroke:#cc2222,stroke-dasharray:4
class legacy dep
class broken miss
สองเอดจ์ที่วาดด้วยเส้นประคือปัญหาที่จับได้ด้วยการตรวจสอบของมนุษย์ในหัวข้อ 2.4.2 qa → legacy คือ stale link ที่อ้างอิง atom ที่ถูกเลิกใช้ ส่วน balance_curve_v3 → skill_cancel_rule_legacy คือลิงก์ที่ขาดซึ่งชี้ไปยังโหนดที่ไม่มีอยู่จริง เมื่อวาดเป็นกราฟ เส้นประสองเส้นนี้จะโดดเด่นขึ้นมาท่ามกลางเส้นทึบ หากดำเนินการด้วยข้อความเพียงอย่างเดียว มันคงถูกฝังอยู่ที่ใดที่หนึ่งในไฟล์ 47 ไฟล์และมองไม่เห็นไปตลอดกาล
กฎที่รันโดยอัตโนมัติที่ verification gate (Layer 4) มีสี่อย่าง
requires วนกลับมายังตัวเอง ให้เตือน ตรวจจับด้วย simple_cyclesconflicts_with ทั้งคู่เป็น status: approved ให้เตือน (กรณีในหัวข้อ 2.4.2 ผ่านได้เพราะฝั่งหนึ่งเป็น deprecated)graph_orphan_detection_quarterly ระบุรอบนี้ไว้docs_layer_numeric_prefix_naming บังคับให้ใส่ prefix หมายเลข Layer ในชื่อเอกสาร การไหลย้อนจึงถูกคัดกรองชั้นแรกได้แค่ดูชื่อไฟล์เมื่อกฎสี่ข้อนี้ถูกตรึงไว้ในโค้ดแล้ว ก็ไม่จำเป็นต้องเขียนพรอมต์ทุกครั้งอย่างในหัวข้อ 2.4.2 ในวินาทีที่คำขอเปลี่ยนแปลงถูกส่งเข้ามา บอตจะ build กราฟใหม่ แล้วแสดงรายการ atom ที่ได้รับผลกระทบ พร้อมลิงก์ที่ขาด·การวนรอบ·ความขัดแย้งเป็นคอมเมนต์อัตโนมัติ คอมเมนต์ "atom 4 รายการได้รับผลกระทบ" ที่ต้นบทนี่เองคือสิ่งนี้ — บอตตัวนี้คือบอตที่ได้บอกล่วงหน้าไว้ก่อนหน้า
ตรงนี้ต้องชี้ให้ชัดว่าเหตุใด 2.3 และ 2.4 จึงเป็นชุดเดียวกัน พิกัด Layer และลูกศรความสัมพันธ์ไม่ได้นำมาใช้แยกกัน แต่เป็นสองด้านของจุดประสงค์เดียวกัน
ในเชิงผิวเผิน Layer ทำให้ภาษาในการทำงานร่วมกันเป็นหนึ่งเดียว — เมื่อเรียกว่า "อันนี้คือการตัดสินใจระบบ L1" "อันนั้นคือข้อมูล L3" ต่อให้สาขาต่างกันก็ใช้พิกัดเดียวกันร่วมกันได้ แต่จุดประสงค์ในเชิงแก่นแท้นั้นอยู่ที่อื่น Layer คือพิกัดที่แบ่งไว้เพื่อการสร้างแบบโพรซีเดอรัล (procedural generation)
วิสัยทัศน์ L0 คือ context anchor — ไม่เปลี่ยนแปลง และถูกฉีดเข้า AI ทุกครั้ง ระบบ L1 คือกฎอินพุตของการสร้าง — rulebook·ความสัมพันธ์·แท็กอาศัยอยู่ที่นี่ เนื้อหา L2 คือที่ที่เนื้อหาที่ถูกสร้างขึ้นสะสมตัว ข้อมูล L3 เป็นอินพุตของการจำลองด้วยตัวเลข·ID·ความสัมพันธ์ ส่วน build·QA ใน L4 คือ verification gate ลูกศรความสัมพันธ์ทำงานเป็น เงื่อนไขจำกัดของการสร้าง บนพิกัดนี้ เมื่อ AI สร้างเนื้อหาใหม่ ลูกศร requires กลายเป็นข้อสมมติฐานว่า "สิ่งนี้ต้องมีก่อน" และลูกศร conflicts_with กลายเป็นข้อห้ามว่า "สิ่งนี้เปิดร่วมกันไม่ได้"
สาขาแตกแขนงออกไป (ต่อสู้·เควสต์·เศรษฐกิจต่างมีความเชี่ยวชาญของตน) แต่เพราะผลผลิตทั้งหมดมีพิกัด Layer จึงรับรู้ซึ่งกันและกัน การแตกแขนงและการบูรณาการตั้งอยู่พร้อมกันบนระบบพิกัดเดียว เมื่อกราฟนี้เติบโตพอ AI จะอ่านลูกศรความสัมพันธ์เป็นข้อจำกัดอัตโนมัติขณะสร้างผู้สมัคร และมนุษย์เพียงตรวจสอบว่ามีการละเมิดหรือไม่ที่ด่านตรวจ บันทึกเซสชันจริงในหัวข้อ 2.4.2 คือฉบับย่อของสิ่งนั้น — AI อ่านกราฟเพื่อค้นหาการละเมิดข้อจำกัด (ลิงก์ที่ขาด·ความขัดแย้ง) และมนุษย์ตัดสินที่ด่าน
โหนดที่เป็นจุดต้นทาง·จุดปลายทางของลูกศรความสัมพันธ์ไม่ได้มีแต่ atom การตัดสินใจเท่านั้น atom ที่นิยามแนวคิดโดเมนอย่าง "สกิล" "เควสต์" "รางวัล" ก็เป็นพลเมืองชั้นหนึ่งของกราฟ concept_skill_definition_v1 มีหน้าตาแบบนี้
# สกิล (Skill)
Definition: หน่วยแอ็กชันที่ตัวละครปลดปล่อยระหว่างการต่อสู้ ครอบคลุมอินพุต·คูลดาวน์·ทรัพยากร·เอฟเฟกต์
Required Properties: input / cooldown / cost / effects
Subtypes: active_skill (is_a) / passive_skill (is_a) / ultimate_skill (is_a)
Not a skill: การโจมตีอัตโนมัติ [[concept_auto_attack]] / การแปลงร่าง [[concept_transformation]]
หากเขียนว่า boss_skill is_a skill กฎของแนวคิดระดับบนจะถูกสืบทอดมาโดยอัตโนมัติ ในโปรเจกต์ A มี atom นิยามแนวคิดเช่นนี้ราว 19 ตัว และ atom การตัดสินใจทุกตัวอ้างถึงพวกมัน เมื่อศัพท์ถูกทำให้เป็นหนึ่งเดียวด้วยพจนานุกรมเล่มเดียว ภาระในการแปลระหว่างการประชุม·เอกสาร·โค้ดก็หายไป เปรียบเหมือนการที่ไม่ได้วางพจนานุกรมคนละเล่มไว้ตามโต๊ะแต่ละตัว แต่ใช้พจนานุกรมเล่มเดียวกันร่วมกัน concept_reward_currency ที่ถูกจับได้ว่าเป็นลิงก์ที่ขาดในบันทึกเซสชันจริงก่อนหน้านี้ ก็คือ "รายการว่างที่ตกลงกันว่าจะขึ้นทะเบียนในพจนานุกรมแต่ยังไม่ได้สร้าง" นั่นเอง
อ่านมาถึงตรงนี้ก็จะมีคำถามว่า "นี่มันออนโทโลยีอย่างเป็นทางการแบบ OWL/RDF ไม่ใช่หรือ" ไม่ใช่ และตั้งใจทำให้ไม่ใช่
ออนโทโลยีเชิงวิชาการ (OWL·RDF·SKOS) นั้นทรงพลัง แต่มีประเภทความสัมพันธ์หลายสิบถึงหลายร้อยแบบ ต้องใช้เอนจินอนุมานเฉพาะทาง และหากจะดำเนินการก็ต้องมีผู้เชี่ยวชาญด้านออนโทโลยีประจำอยู่ ในด้านที่การอนุมานแม่นยำตัดสินความเป็นความตายอย่างเสิร์ชเอนจิน·การแพทย์·กฎหมาย สิ่งนี้จำเป็น แต่การอนุมานที่จำเป็นจริงในการออกแบบเกมมีเพียง "ขอบเขตผลกระทบของการเปลี่ยนแปลง" "การพึ่งพาที่ต้องมาก่อน" "การตรวจจับความขัดแย้ง" เท่านั้น และทั้งหมดจบลงที่ระดับการค้นแบบเรียบง่าย (BFS·DFS) ที่ไล่ตามกราฟทีละช่อง หัวข้อก่อนหน้าที่จับการวนรอบได้ด้วย networkx บรรทัดเดียวคือหลักฐานของสิ่งนั้น เอนจินอนุมานอย่างเป็นทางการนั้นเกินจำเป็น
เกณฑ์มีเพียงข้อเดียว นักออกแบบเกมดำเนินการด้วยมือได้หรือไม่ YAML enum 6 ตัว, การประมวลผลด้วย networkx, การทำเป็นภาพด้วย pyvis หรือ D3.js วินาทีที่ก้าวข้ามเส้นนี้ เครื่องมือก็เลิกเป็นเครื่องมือและกลายเป็นภาระอีกอย่างหนึ่ง ความเบาไม่ใช่การประนีประนอม แต่เป็นเจตนาในการออกแบบ
ในการหลีกเลี่ยงกับดักนี้มีความผิดพลาดที่เกิดซ้ำอยู่ห้าอย่าง ทั้งหมดงอกออกมาจากรากเดียวกันคือ "ที่ที่จัดการออนโทโลยีในฐานะมาตรฐานบังคับ"
| ความผิดพลาด | วิธีหลีกเลี่ยง |
|---|---|
| นิยามประเภทความสัมพันธ์มากเกินไปตั้งแต่แรก | เริ่มด้วยสามแบบ (affects·derives_from·requires) เพิ่มเฉพาะเมื่อจำเป็นเท่านั้น |
| บังคับให้ทุกการตัดสินใจมีความสัมพันธ์ | ยอมรับ atom ที่ไม่มีความสัมพันธ์ว่าเป็นเรื่องปกติด้วย — ความสัมพันธ์ที่ว่างเปล่าทำให้กราฟสกปรก |
| เขียน affects แบบสองทิศทาง | ทิศทางเดียวด้วยมือ ทิศทางย้อนกลับให้เครื่องมือคำนวณอัตโนมัติ |
| ยึดติดกับ OWL·RDF | คงอยู่ที่ระดับที่ดำเนินการได้ (YAML + enum) |
| ดำเนินการด้วยข้อความล้วนโดยไม่มีการทำเป็นภาพ | จัดให้มีตั้งแต่แรกแม้จะเป็น HTML view แบบเรียบง่ายก็ตาม — เหมือนเส้นประในหัวข้อ 2.4.3 ถ้ามองไม่เห็นก็ไม่ได้แก้ |
ข้อ 1·2·3 จะจับแพตเทิร์นได้ภายในเดือนแรกของการนำมาใช้ ส่วนข้อ 4·5 หากตรวจในการทบทวนเดือนที่สามก็จะเข้าที่เข้าทางไปเองโดยธรรมชาติ
เดือนแรกขาดทุน มีแต่ภาระในการเขียนความสัมพันธ์สะสมขึ้น ส่วนผลที่มองเห็นนั้นไม่มี ดังนั้นจึงเริ่มต้นแบบเล็ก ๆ สัปดาห์แรกมีแค่สามความสัมพันธ์ affects·derives_from·requires สัปดาห์ที่ 2\~4 นำไปใช้กับ atom หลัก 20 ตัวพลางเฝ้าดูกราฟค่อย ๆ เติบโต หากในเดือนแรกสร้าง HTML graph view ระดับหัวข้อก่อนหน้าขึ้นมาสักหนึ่งอัน นับจากเดือนที่สองการทำเป็นภาพก็จะเริ่มแสดงคุณค่าให้เห็น และในเดือนที่สามการตรวจสอบอัตโนมัติ (การวนรอบ·ความขัดแย้ง·กำพร้า·ลิงก์ที่ขาด) จะลดเวลาการประชุมได้โดยตรง การทนเดือนแรกที่ขาดทุนให้ผ่านไปคือจุดแยกของความสำเร็จหรือล้มเหลวในการนำมาใช้
บทที่ 8 Wikilink บทถัดไปจะเจาะลึกการเขียนกำกับ [[...]] ที่บทนี้กล่าวถึงในรูปลูกศรไปสู่ระดับการดำเนินการ — ใช้แผงการอ้างกลับในแต่ละวันอย่างไร อัปเดตลิงก์ทั้งหมดพร้อมกันอย่างไรเมื่อเปลี่ยนชื่อ (วิธีป้องกันการตกหล่นในการ rename อย่างในหัวข้อ 2.4.2 ตั้งแต่ต้น) นำ graph view ของเครื่องมืออย่าง Obsidian มาหลอมรวมเข้ากับงานจริงอย่างไร YAML (บทที่ 4) → Atom (บทที่ 5) → Layer (บทที่ 6) → Ontology (บทที่ 7) → Wikilink (บทที่ 8) คือรูปห้าเหลี่ยมที่สมบูรณ์ของสถาปัตยกรรมสารสนเทศ
setup. กำหนดโฟลเดอร์ที่รวบรวม atom ไว้ (เช่น decisions/) แล้วเตรียมเขียนคีย์ความสัมพันธ์ลงใน YAML ของแต่ละ atom ตอนแรกมีแค่สามตัว affects·derives_from·requires ลิงก์ในเนื้อหาให้รวมเป็นรูปแบบ [[atom_name]] ให้เป็นแบบเดียวกัน
prompt. ก่อนเลิกใช้·เปลี่ยนชื่อ ให้โยนคำสั่งด้านล่างนี้
อ่าน atom แบบ markdown ในโฟลเดอร์นี้ ฉันจะเลิกใช้/เปลี่ยน [atom_เป้าหมาย]
ช่วยตรวจสอบ
(1) การอ้างกลับ: atom ทั้งหมดที่ชี้มายัง atom นี้ด้วยความสัมพันธ์ใน YAML และ wikilink ในเนื้อหา
(2) ลิงก์ที่ขาด: ลิงก์ที่จะขาดหรือกลายเป็น stale เมื่อเปลี่ยน/ลบ
(3) ความขัดแย้งที่หลงเหลือ: ในบรรดาคู่ conflicts_with ตัวที่เป็น status: approved
อย่าเดา ให้อ่านไฟล์จริง ๆ ถ้าหาไม่เจอก็บอกว่าหาไม่เจอ
verify. จุดที่ AI กำกับว่า "สงสัยว่าพิมพ์ผิด" "ไม่อาจมั่นใจได้" ให้มนุษย์เปิดดูเอง สาเหตุ ของลิงก์ที่ขาด (พิมพ์ผิด หรือตกหล่นในการ rename หรือยังไม่ได้เขียน) มนุษย์ตัดสินด้วย git log และตัวจริงในโฟลเดอร์ อย่าสั่งให้แก้อัตโนมัติ แต่ให้แก้เองหลังยืนยันเจตนาแล้ว
ในเนื้อหา (2.4.3) ได้แสดงเพียงสองฟังก์ชันหลัก ฉบับเต็มที่ build ทั้งโฟลเดอร์เป็นกราฟและวางการตรวจสอบสี่อย่างมีดังนี้
import networkx as nx
import re, yaml, glob, os
REL_TYPES = ["affects", "derives_from", "requires",
"conflicts_with", "is_a", "part_of"]
WIKILINK = re.compile(r"\[\[([a-zA-Z0-9_]+)\]\]")
def build_graph(folder):
G = nx.DiGraph()
files = {}
for path in glob.glob(os.path.join(folder, "*.md")):
name = os.path.splitext(os.path.basename(path))[0]
text = open(path, encoding="utf-8").read()
fm = yaml.safe_load(text.split("---")[1]) or {}
files[name] = fm
G.add_node(name, layer=fm.get("layer"), status=fm.get("status"))
# เอดจ์ความสัมพันธ์ใน YAML
for name, fm in files.items():
for rel in REL_TYPES:
for tgt in (fm.get(rel) or []):
G.add_edge(name, tgt, type=rel)
return G, files
def find_broken_wikilinks(folder, known_nodes):
broken = []
for path in glob.glob(os.path.join(folder, "*.md")):
text = open(path, encoding="utf-8").read()
for m in WIKILINK.findall(text):
if m not in known_nodes:
broken.append((os.path.basename(path), m))
return broken
def find_orphans(G):
# โหนดที่ inbound เป็น 0 และไม่มีพ่อแม่ part_of/is_a ด้วย
return [n for n in G.nodes if G.in_degree(n) == 0]
def find_cycles(G):
return list(nx.simple_cycles(G))
ไม่มีเครื่องมือก็ได้ โฟลเดอร์ atom หนึ่งโฟลเดอร์ Claude หนึ่งตัวก็เพียงพอ ตอนเขียนการตัดสินใจใหม่ ให้เพิ่มเพียงสองบรรทัด requires·affects ลงใน YAML และทุกครั้งที่เกิดการเลิกใช้·เปลี่ยนชื่อ ให้รัน prompt ข้างต้นสักหนึ่งครั้ง การทำกราฟเป็นภาพเป็นเรื่องของทีหลัง — นิสัยการไล่ดูลิงก์ที่ขาดและความขัดแย้งสักครั้งก่อนเปลี่ยนแปลง หนึ่งครั้งนั้นคือสิ่งที่ป้องกันการขาดทุนก้อนใหญ่ที่สุดในการดำเนินงานคนเดียว
วันพฤหัสบดี เวลา 16:50 น. ชีตสกิลที่ผู้รับผิดชอบด้านบาลานซ์กรอกเสร็จเพิ่งถูกอัปโหลดขึ้นมา สกิลทั้งหมด 312 รายการ แต่ละสกิลต้องกรอกหมายเลขเอฟเฟกต์ลงในช่องชื่อ effect_id และหมายเลขนั้นชี้ไปยังแถวที่อยู่ในชีตเอฟเฟกต์อีกชีตหนึ่ง ทั้งสองต้องตรงกันเกมจึงจะทำงานได้ ถ้าไม่ตรงกัน ไคลเอนต์จะเรียกเอฟเฟกต์ที่ว่างเปล่าหรือไม่ก็ตายเงียบ ๆ
ตัวผมในอดีตตรวจเรื่องนี้ด้วยมือ ดูช่องหนึ่งในชีตสกิล กระโดดไปชีตเอฟเฟกต์ ตรวจหมายเลข แล้วกลับมา ทำซ้ำ 312 ครั้ง เร็วที่สุดก็สองชั่วโมง และในช่วง 50 รายการสุดท้ายที่ตาเริ่มพร่า ผมมักจะพลาดไปหนึ่งหรือสองรายการเสมอ และหนึ่งหรือสองรายการนั้นแหละที่ระเบิดในบิลด์ QA
บทนี้คือเรื่องราวว่าสองชั่วโมงนั้นหายไปไหน และการตรวจสอบความสอดคล้องนั้นเป็นพิกัดที่ปักอยู่ตรงไหนบนแผนที่งานของ System Designer ถ้าไม่ปักพิกัดให้ชัดเสียก่อน เราจะตัดสินใจว่าจะเสียบ AI เข้าตรงไหนได้แค่ด้วยความรู้สึกไปตลอดกาล
System Designer คือคนที่เดินทางไปมาระหว่างระดับนามธรรมกับระดับรูปธรรมได้กว้างที่สุด เขารับเอาหมอกที่ชื่อว่าวิสัยทัศน์มา แล้วลากมันลงมาจนถึงตัวเลขแข็ง ๆ อย่างเซลล์สุดท้ายของชีตข้อมูล สิ่งที่ถูกสร้างขึ้นในการเดินทางนั้นมีสี่ประเภท
(1) แปลวิสัยทัศน์ให้เป็นโครงสร้าง เมื่อ Design Director พูดว่า "การต่อสู้แบบแอ็กชันที่มีสัมผัสการกระแทกมีชีวิตชีวา" System Designer จะเปลี่ยนคำพูดนั้นให้เป็นโครงร่างอย่างสกิล คอมโบ แคนเซิล และ hit-stop ส่วน "สิทธิ์ในการกำหนดการเติบโตด้วยตัวเอง" ก็จะกลายเป็นระบบคลาส สกิลทรี และอุปกรณ์ นี่คือช่วงเวลาแรกที่หมอกกลายเป็นสิ่งก่อสร้าง
(2) ระบุข้อกำหนดของอินเทอร์เฟซระหว่างระบบ การต่อสู้ การเคลื่อนที่ อินเวนทอรี ร้านค้า เควสต์ และกิลด์ ทำงานพร้อมกัน ถ้าเปิดอินเวนทอรีระหว่างต่อสู้จะได้รับสถานะอมตะหรือไม่? ถ้ามีคำขอ PvP เข้ามากลางคันขณะตีบวกอุปกรณ์ล่ะ? คำตอบของเคสเหล่านี้มารวมกันแล้วสร้างสัมผัสที่เรียกว่า "ทำมาดีจริง ๆ" ทุกตำแหน่งที่คำตอบขาดหายไป ผู้ใช้จะรู้สึกหงุดหงิด
(3) รับผิดชอบชีตข้อมูลและสคีมาของมัน ค่าสัมประสิทธิ์ของสกิล 312 รายการ เอฟเฟกต์ของไอเทมหลายร้อยชิ้น พฤติกรรมของมอนสเตอร์หลายสิบชนิด ค่าต่าง ๆ จะกรอกเองหรือส่งต่อให้สายงานบาลานซ์กับเนื้อหาก็ได้ แต่อย่างน้อย นิยามคอลัมน์ (schema) ของชีตนั้น System Designer ต้องถือไว้เอง มันคืองานสร้างลิ้นชักที่ติดป้ายกำกับไว้ให้ ถ้าลิ้นชักหลวมเลื่อน แต่ละคนก็จะกรอกต่างกันจนความสอดคล้องพังลง
(4) ออกแบบลอจิกของพฤติกรรม AI ของตัวละครและมอนสเตอร์ออกมาในรูปแบบอย่างสเตตแมชชีน (FSM, Finite State Machine, เครื่องสถานะจำกัด) ทรีพฤติกรรม (Behavior Tree, ต่อไปเรียกว่า BT) ตารางการตัดสินใจ และกฎแบบโพรซีเดอรัล เอกสารเหล่านี้จะถูกส่งต่อไปยังโปรแกรมเมอร์แล้วกลายเป็นโค้ด
ประเด็นสำคัญคือทั้งสี่สิ่งนี้มาบรรจบกันบนโต๊ะทำงานของคนคนเดียว ดังนั้น "วันนี้จะใช้เวลาไปกับอะไร" จึงกลายเป็นการตัดสินใจด้านการบริหารงานที่ใหญ่ที่สุดของ System Designer
ในหัวข้อ 2.3 เราได้วางผลงานการผลิตเกมทั้งหมดลงบนแกนพิกัดตั้งแต่ L0 (วิสัยทัศน์) ถึง L4 (บิลด์) ตอนนี้เราจะปักผลงานทั้งสี่อย่างของ 3.1.1 ลงบนแกนนั้นโดยตรง กรณีที่ผลงานของสายงานเดียวกระจายกว้างหลาย Layer อย่างงาน System Designer นั้นหาได้ยาก
ต่อไปนี้คือแผนที่ที่เขียนลงในภาพเดียวว่าผลงานอาศัยอยู่ตรงไหนบน Layer และพบกับใครในแต่ละพิกัด
แผนที่นี้บอกอยู่สองอย่าง อย่างแรก System Designer รับผิดชอบระยะทางยาวที่ รับ L0 มาแล้ว พา ให้ไปถึง L4 อย่างที่สอง ช่วงที่ลงมือสร้างเองคือ L1 ถึง L3 และคู่ทำงานในแต่ละช่องสามช่องนั้นเปลี่ยนไป ทุกครั้งที่เปลี่ยนช่อง ภาษาที่ใช้ทำงานร่วมกันก็เปลี่ยน ดังนั้นหากไม่รู้ตัวว่าตอนนี้อยู่พิกัดไหน การประชุมก็จะวนเปล่าอยู่บ่อย ๆ
แต่ทั้งนี้ไม่ได้หมายความว่าคนคนเดียวจะแตะ L1 ถึง L3 ทั้งหมด ถ้าทีมใหญ่ ผู้รับผิดชอบ L1 ถึง L2 กับผู้รับผิดชอบ L3 จะแยกกัน ถ้าทีมเล็ก คนคนเดียวก็ดูทั้งหมด พิกัดคือแผนที่แบ่งบทบาท ไม่ใช่คำสั่งให้โยนทุกอย่างไปให้คนคนเดียว
เมื่อวาดแผนที่เสร็จแล้ว ตอนนี้ก็ลงสี พิกัดไหนที่นำ AI เข้ามาแล้วได้ผลมาก? ไม่ใช่ "อัตโนมัติให้หมด" อย่างไม่ลืมหูลืมตา แต่เลือกโดยดูจากคุณสมบัติของพิกัด
flowchart TD
L1["L1 โครงร่างระบบ
(จำนวนคลาส, โมเดลการต่อสู้)"]
L2["L2 อินเทอร์เฟซ
(กฎการโต้ตอบ)"]
L3["L3 สคีมา + ข้อมูล
(ชีต 312 แถว)"]
L1 -->|"เชื่อมตรงกับเอกลักษณ์เกม
คนเป็นผู้ตัดสิน"| H1["AI = ช่วยแปรรูป/ตรวจสอบการตัดสินใจของคน"]
L2 -->|"เคสระเบิด
ขอบเขตผลกระทบกว้าง"| H2["AI = ดึงขอบเขตผลกระทบของการเปลี่ยนแปลงอัตโนมัติ"]
L3 -->|"เป็นรูปแบบตายตัว·ซ้ำ ๆ
ตรวจความสอดคล้อง"| H3["AI = รับผิดชอบทั้งการสร้าง·ตรวจสอบ·แปลง"]
H1 --> R["คนรับผิดชอบการตัดสินใจหลัก,
AI รับผิดชอบรายละเอียด·ความสอดคล้อง·งานซ้ำ"]
H2 --> R
H3 --> R
style L3 fill:#fff8e1,stroke:#f9a825
style H3 fill:#e8f5e9,stroke:#2e7d32
style R fill:#e3f2fd,stroke:#1565c0
ประเด็นสำคัญคือ ยิ่งพิกัดเลื่อนลงล่างมากเท่าไร สัดส่วนที่ AI รับผิดชอบทั้งหมดก็ยิ่งมากขึ้น "จะตั้งคลาสไว้กี่คลาส" ของ L1 เป็นเรื่องเอกลักษณ์เกม คนจึงต้องถือไว้ ในทางกลับกัน "foreign key (FK) ทั้ง 312 แถวตรงกันหมดไหม" ของ L3 เป็นรูปแบบตายตัวและซ้ำ ๆ AI จึงควรรับไปทั้งก้อน ส่วน L2 อยู่ตรงกลาง — การตัดสินใจเป็นของคน แต่ AI ช่วยหนุนด้วยการดึงขอบเขตผลกระทบว่า "ถ้าเปลี่ยนกฎนี้แล้วจะกระเทือนไปถึงไหน"
ภาพนี้อธิบายว่าทำไมการฝึกปฏิบัติทั้งหมดตั้งแต่ 3.1.4 เป็นต้นไปจึงเริ่มจากบริเวณ L3 เพราะเป็นตำแหน่งที่ได้ผลมากที่สุดและเสี่ยงน้อยที่สุด ต่อให้เครื่องมือสคีมาทำงานผิดพลาดก็ไม่เกิดอุบัติเหตุ แผนผังความสัมพันธ์ก็แค่วาดรูปเท่านั้น และการตรวจสอบความสอดคล้องก็เป็นสิ่งที่คนปฏิเสธได้
ทฤษฎีมีแค่นี้ ตอนนี้เรากลับไปยังวันพฤหัสบดีที่ต้นบท 3.1 กัน เราจะลองสั่งให้ AI ตรวจว่า effect_id ของสกิลทั้ง 312 รายการในชีตสกิลตรงกับชีตเอฟเฟกต์หรือไม่ ผมจะคัดลอกบทสนทนาตามที่โต้ตอบกันจริงโดยไม่สรุปย่อ
การป้อนข้อมูลเป็นไปตามหลัก schema-first กล่าวคือคนเป็นผู้นิยาม ชีตข้อมูลจำเพาะ (specification sheet) ก่อน จากนั้น Enum และ .proto จึงออกมา (VBA (ภาษามาโครของ Excel) Export) แล้วสุดท้ายข้อมูล csv จึงถูกกรอกเข้าไป ดังนั้นพรอมต์ตรวจสอบจึงเริ่มด้วย "อ่านสคีมาก่อน" และตอกย้ำว่าฉบับจริงไม่ใช่เอกสารสคีมา แต่เป็น JSON ที่ export ออกมาจริง (json_over_schema_doc_as_source_of_truth) เพราะเอกสารโกหกได้ แต่ JSON ที่ export ออกมาโกหกไม่ได้
คุณคือตัวตรวจสอบความสอดคล้องของข้อมูลในโปรเจกต์ของเรา อินพุตคือ export JSON สองไฟล์ 1)
skill.json— แต่ละอ็อบเจกต์สกิลมีฟิลด์skill_id,name,effect_id2)effect.json— แต่ละอ็อบเจกต์เอฟเฟกต์มีฟิลด์effect_id,effect_typeจงตรวจสอบสิ่งต่อไปนี้ (A)
effect_idทุกตัวในskill.jsonมีอยู่ในeffect.jsonหรือไม่ (ความสอดคล้องของ foreign key) (B) เอฟเฟกต์กำพร้า (orphan) ที่อยู่ในeffect.jsonแต่ไม่มีสกิลใดอ้างถึง (C) แถวที่effect_idว่างเปล่าหรือเป็น 0 ในskill.jsonกฎ: ห้ามเดา ฟิลด์ที่ไม่มีใน JSON ให้รายงานว่า "ไม่มีฟิลด์" เอาต์พุตให้แบ่งเป็นสามส่วนคือ (A)(B)(C) และแต่ละกรณีที่ละเมิดให้ระบุ
skill_id(หรือ effect_id) กับเหตุผลในบรรทัดเดียว ถ้าไม่มีการละเมิด ให้เขียนในส่วนนั้นแค่ "ไม่มีการละเมิด"
ยังไม่ได้รับ input JSON สองไฟล์ ในข้อความปัจจุบันไม่มีเนื้อหาไฟล์แนบมาด้วย กรุณาส่งมาใหม่ด้วยวิธีใดวิธีหนึ่งต่อไปนี้ - เนื้อหาจริงของ
skill.jsonและeffect.json(วางข้อความหรือแนบไฟล์) - หรือพาธของไฟล์ทั้งสองเมื่อได้รับเนื้อหาแล้ว ผมจะตรวจสอบตามลำดับ (A)(B)(C) ผมจะไม่กุผลลัพธ์ขึ้นมาเองโดยไม่มีไฟล์
การที่มันหยุดตรงนี้กลับเป็นสัญญาณของความน่าเชื่อถือ ถ้าอินพุตว่างเปล่าแต่กลับมีรายงานดูดีตอบกลับมา ความเรียบร้อยนั้นแหละคือสัญญาณอันตราย ฝ่ายที่ไม่สร้างผลการตรวจสอบขึ้นมาจากมือเปล่าแต่ย้อนถามว่า "ขอไฟล์ก่อน" ต่างหากที่น่าเชื่อถือ ผมจึงวาง JSON ทั้งสองไฟล์เข้าไปจริง ๆ แล้วร้องขอใหม่ (ในเนื้อหาหลักคัดมาแสดงเพียงบางส่วนเนื่องด้วยความยาว)
// skill.json (คัดมาบางส่วน)
[
{"skill_id": 1001, "name": "강타", "effect_id": 5001},
{"skill_id": 1002, "name": "회전베기", "effect_id": 5002},
{"skill_id": 1003, "name": "방패올리기","effect_id": 0},
{"skill_id": 1004, "name": "독바르기", "effect_id": 5099}
]
// effect.json (คัดมาบางส่วน)
[
{"effect_id": 5001, "effect_type": "DAMAGE"},
{"effect_id": 5002, "effect_type": "DAMAGE"},
{"effect_id": 5003, "effect_type": "BUFF"}
]
(A) ความสอดคล้องของ foreign key — ละเมิด 1 รายการ -
skill_id: 1004→effect_id: 5099ไม่มีอยู่ในeffect.json(dangling reference)(B) เอฟเฟกต์กำพร้า — ละเมิด 1 รายการ -
effect_id: 5003(BUFF) → ไม่มีสกิลใดอ้างถึง(C) effect_id ว่าง/0 — ละเมิด 1 รายการ -
skill_id: 1003→effect_id: 0อาจเป็น "ไม่มีเอฟเฟกต์" ที่ตั้งใจไว้ก็ได้ จึงควรตรวจกับสคีมาว่า 0 เป็นค่าที่ใช้ได้หรือไม่
สองในสามรายการเป็นบั๊กชัดเจน 5099 เกิดจากการที่ผู้รับผิดชอบบาลานซ์เขียนไว้ในสกิลก่อนที่จะเพิ่มแถวลงในชีตเอฟเฟกต์ — เป็นเคสคลาสสิกของลำดับงานที่สลับกัน จับได้
แต่ (C) ผมกลับ ปฏิเสธ ในโปรเจกต์ของเรา effect_id: 0 เป็นค่าที่ตกลงกันไว้และใช้ได้ ซึ่งหมายถึง "ช่อง passive, ไม่มีเอฟเฟกต์" AI ไม่รู้เรื่องนั้นจึงรายงานว่า "ควรตรวจสอบ" การตัดสินนั้นถูกต้อง แต่ไม่ใช่เรื่องที่ควรนับเป็นการละเมิด ผมจึงเดินหน้าต่อไป — นั่นคือการตรึงข้อตกลงนี้ไว้เป็น atom เพื่อให้ตั้งแต่การตรวจครั้งหน้าเป็นต้นไป AI รู้ว่า 0 เป็นค่าที่ใช้ได้
กฎเพิ่มเติม:
effect_id == 0หมายถึง "passive, ไม่มีเอฟเฟกต์" ซึ่งเป็นค่าที่ตกลงกันไว้และใช้ได้ในโปรเจกต์ของเรา ดังนั้นใน (C)effect_id == 0ไม่ถือเป็นการละเมิด ให้รายงานเฉพาะสตริงว่าง·null·จำนวนลบเป็นการละเมิดของ (C) เท่านั้น
(C) effect_id ว่าง/0 — ไม่มีการละเมิด (
effect_id: 0ถือเป็นค่าที่ตกลงกันไว้และใช้ได้ ไม่มีค่าว่าง/null/จำนวนลบ)
ข้อตกลงบรรทัดเดียวนี้แหละคือการสะสมความรู้เชิงโดเมน การตัดสินใจของงานออกแบบระบบค่อนข้างถาวร เมื่อตรึงไว้ครั้งหนึ่ง อีกหนึ่งปีถัดไปการตรวจแบบเดียวกันก็ยังหมุนด้วยเกณฑ์เดียวกัน การ์ดการตัดสินใจหนึ่งใบถูกวางสะสมไว้ในตู้
ในบันทึกเซสชันนี้ สิ่งที่คนทำมี เพียงสามอย่าง เท่านั้น — (1) กำหนดลำดับอินพุตว่าให้อ่านจากสคีมาก่อน (2) ยืนยันว่า 5099 เป็นบั๊กจริง (3) รู้ว่า 0 เป็นค่าที่ใช้ได้แล้วปฏิเสธ·แก้ไขการตัดสินของ AI สองชั่วโมงที่เคยนั่งดูทีละช่องโดยกระโดดไปมาตลอด 312 แถวที่เหลือนั้นหายไป สิ่งที่ถูกทำเป็นอัตโนมัติคือแรงงานอย่างการกระโดดและการเทียบ ส่วนการตัดสินสามบรรทัดที่เหลือต่างหากคือแก่นแท้
การตรวจในบันทึกเซสชันข้างต้นจะสั่งด้วยมือทุกครั้งก็ได้ แต่งานที่ซ้ำ ๆ ที่ L3 การตรึงให้แข็งเป็นเครื่องมือคือหลักปฏิบัติของงานออกแบบระบบ ผมขอยกตัวอย่างสองอย่างที่ผู้เขียนใช้งานอยู่ — ไม่ใช่ "เครื่องมือของโปรเจกต์ A" ที่เป็นนามธรรม แต่เป็นของที่หมุนอยู่บนโต๊ะทำงานจริง ๆ
gen_relation_map.py วิเคราะห์ชื่อคอลัมน์·ค่าของชีตเพื่อตรวจจับความสัมพันธ์ของ foreign key โดยอัตโนมัติ แล้วดึงออกมาเป็นแผนผังความสัมพันธ์ HTML แบบอินเทอร์แอ็กทีฟ ถ้าใน 3.1.4 คนเป็นผู้วาดลูกศร skill.effect_id → effect.effect_id ไว้ในหัว สคริปต์นี้ก็จะวาดลูกศรนั้นออกมาเป็นภาพสำหรับชีตทั้งหมด ตำแหน่งที่การพึ่งพิงไหลย้อนทาง (ความเสี่ยงที่ข้อมูล L3 ไปอ้างถึงโครงร่าง L1 แบบย้อนกลับ) จะกระโดดเด่นออกมาจากภาพในทันที
สกิล schema-doc ทำการพาร์ส ชีต $สคีมา ของ xlsm แล้วสร้างเอกสารสคีมาแบบ Markdown โดยอัตโนมัติ สคีมาที่ว่านี่เองคือสคีมาที่คำถามใน 3.1.4 (C) ที่ว่า "ควรตรวจกับสคีมาว่า 0 เป็นค่าที่ใช้ได้หรือไม่" โผล่ขึ้นมา — มันทำให้คนอ่านสคีมาล่าสุดได้ทันทีโดยไม่ต้องไปรื้อค้นไฟล์อื่น เมื่อชีตเปลี่ยน เอกสารก็เปลี่ยนตาม โรคเรื้อรังที่เอกสารกับข้อมูลจริงไม่ตรงกันจึงลดลง
หากพูดถึงตำแหน่งของเครื่องมือทั้งสองด้วยภาษาพิกัดอีกครั้งก็เป็นเช่นนี้ schema-doc ปกป้อง นิยามคอลัมน์ ของ L3 ส่วน gen_relation_map.py ปกป้อง ความสัมพันธ์ ระหว่าง L2 ถึง L3 ส่วนพรอมต์ผู้ช่วย AI (การตรวจสอบอย่างใน 3.1.4) หมุนอยู่ข้างบนทั้งสองอย่างนั้น ทั้งสามไม่ได้ต่างคนต่างเล่น แต่ต่างก็รับผิดชอบระดับความสูงที่ต่างกันบนแกนพิกัดเดียวกัน
วิธีใช้งานจริงของเครื่องมือเหล่านี้จะลองทำด้วยมือใน 3.2·3.3·3.4 หัวข้อ 3.1 คือแผนที่ที่กำหนดว่าจะเสียบเข้าตรงไหน และสามบทถัดไปคืองานเสียบ
ถ้าเปิดเครื่องมือทั้งสามพร้อมกันในคราวเดียว ภาระการบริหารงานจะมาถึงก่อนผลลัพธ์ จากประสบการณ์ของผู้เขียน ลำดับที่ปลอดภัยคือเริ่มจากพิกัดที่เสี่ยงน้อย (ด้านล่าง) ก่อน
| ช่วงเวลา (แนะนำ) | สิ่งที่นำเข้ามา | พิกัด | สิ่งที่เกิดขึ้นแม้พลาด |
|---|---|---|---|
| 1 เดือน | สคีมาก่อน (3.2) | L3 | แค่เอกสารไม่อัปเดตหนึ่งครั้ง |
| 2\~3 เดือน | แสดงแผนผังความสัมพันธ์ (3.3) | L2\~L3 | แค่ภาพไม่แม่นยำ |
| 3\~6 เดือน | พรอมต์ผู้ช่วย AI (3.4) | L1\~L3 | ผ่านการตรวจสอบ คนจึงปฏิเสธได้ |
ระยะเวลาไม่ใช่เกณฑ์ตายตัว ขึ้นอยู่กับขนาดทีม·โครงสร้างพื้นฐานเดิม อาจใช้เวลาเป็นสองเท่า หรือจบในครึ่งเดียวก็ได้ (การประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ) สิ่งที่ไม่เปลี่ยนคือ ลำดับ เมื่อวางตัวช่วยตัดสินใจที่เสี่ยงสูงไว้ท้ายสุด ทีมจะได้สร้างนิสัยการตรวจสอบจากเครื่องมือสองอย่างแรกไปแล้วก่อนที่จะไปแตะตำแหน่งที่อ่อนไหวที่สุด
ผมจะบันทึกตัวเลขโดยไม่ตกแต่งให้ดูดี ต่อไปนี้คือสิ่งที่สังเกตได้จากทีมออกแบบ (กำลังคน 4\~5 คน ทั้งทีมพัฒนาขนาดกลาง 10\~50 คน เปิดให้บริการราว 6 เดือน) ของโปรเจกต์ MMORPG ที่ผู้เขียนบริหารในฐานะ Director (ต่อไปเรียกว่า "โปรเจกต์ A") ไม่ใช่การวัดอัตโนมัติที่แม่นยำ แต่เป็น การสังเกตของผู้เขียน ที่อิงบันทึกการทำงานและบันทึกการทบทวน ขอแนะนำให้อ่านเป็นเพียงทิศทางและอัตราส่วนคร่าว ๆ เท่านั้น
ประเด็นสำคัญคือเวลาที่ประหยัดได้ไม่ใช่เวลาที่ไม่ได้สร้างเกม เวลานั้นจะหมุนกลับไปสู่การตัดสินใจที่ลึกซึ้งซึ่งมอบให้ AI ไม่ได้ อย่างเช่นโครงร่าง L1 ลดแรงงานลงเพื่อนำไปใช้กับการตัดสิน — นั่นคือหนึ่งบรรทัดที่บทนี้แนะนำ
setup. export ชีตสองชีต (เช่น สกิล, เอฟเฟกต์) เป็น csv ถ้าทำได้ก็แปลงเก็บไว้เป็น JSON (หลักการที่ว่าผลลัพธ์ที่ export ออกมาคือฉบับจริง ไม่ใช่เอกสาร) เลือก foreign key หนึ่งคู่ระหว่างสองชีต (เช่น skill.effect_id → effect.effect_id)
prompt. ใช้พรอมต์ฉบับเต็มของ 3.1.4 ตามนั้นเลย อย่าลืมสามบรรทัดสำคัญ — (1) "อ่านสคีมา/โครงสร้างก่อน" (2) "อย่าเดา สิ่งที่ไม่มีให้รายงานว่าไม่มี" (3) "ถ้าไม่มีการละเมิด ให้เขียนแค่ว่าไม่มีการละเมิด"
verify. คนตรวจรายการที่ละเมิดซึ่ง AI ส่งขึ้นมาทีละบรรทัด บั๊กจริงก็แก้ ส่วนผลตรวจผิดพลาด (false positive) ที่เกิดจากค่าที่ตกลงกันไว้ในโดเมน (เช่น 0 = ไม่มีเอฟเฟกต์) ก็ ปฏิเสธ แล้วเพิ่มข้อตกลงนั้นเข้าไปในพรอมต์ (หรือ atom) เมื่อผลตรวจผิดพลาดแบบเดียวกันหายไปจากการตรวจครั้งถัดไป ก็แสดงว่าสินทรัพย์ถูกวางสะสมไปแล้วหนึ่งใบ
ถ้าเป็นนักพัฒนาคนเดียวที่ไม่มีทั้งทีมและชีต แค่แท็บสองแท็บใน Google Sheets ก็พอ แท็บหนึ่งเป็น "สกิล" อีกแท็บเป็น "เอฟเฟกต์" เชื่อมทั้งสองด้วยคอลัมน์ effect_id เพียงคอลัมน์เดียว ดาวน์โหลดแท็บเป็น csv แล้ววางลงในพรอมต์ของ 3.1.4 ต่อให้เป็นชีต 30 แถวแทนที่จะเป็น 312 แถว ก็จับ dangling reference และเอฟเฟกต์กำพร้าได้เหมือนกัน ต่างกันแค่ขนาด แต่พิกัดเหมือนกัน เริ่มจาก L3 แล้วเมื่อมือเริ่มชิน ก็ค่อยไต่ขึ้นไปทีละช่องสู่แผนผังความสัมพันธ์และขอบเขตผลกระทบ
เช้าวันจันทร์ ผู้เขียนได้บิลด์ชีตสกิลที่นักออกแบบเกมคนใหม่กรอกไว้ 120 แถวให้เป็น csv แล้วในล็อกฝั่งไคลเอนต์ก็ขึ้นบรรทัดสีแดงมา 28 บรรทัด class_id อ้างอิงไปที่หมายเลข 47 แต่ในชีตคลาสกลับไม่มีหมายเลข 47 ในช่อง element มีคนหนึ่งเขียนว่า Fire อีกคนเขียนว่า fire และมีอยู่แถวหนึ่งเขียนว่า 화염 เป็นภาษาเกาหลี การไล่บรรทัดสีแดงทั้ง 28 บรรทัดทีละบรรทัดด้วยมือ ทำให้ครึ่งหนึ่งของช่วงบ่ายหายไป
สาเหตุของอุบัติเหตุนี้ไม่ได้อยู่ที่ข้อมูลผิด แต่อยู่ที่ การไม่ระบุกฎที่ข้อมูลต้องปฏิบัติตามก่อนที่จะสร้างข้อมูลขึ้นมา ต่างหาก ถ้ากฎอยู่แค่ในหัวคน พอคนเปลี่ยน กฎก็เปลี่ยนตามทันที บทนี้จะกล่าวถึงเวิร์กโฟลว์ที่สร้างกฎ—สคีมา—ขึ้นมาก่อนข้อมูล และทำให้กฎนั้นถูกบังคับเป็นเอกสารด้วยเครื่องมือ ไม่ใช่ด้วยมือคน
บันทึกศัพท์ - สคีมา (schema): การนิยามคอลัมน์ของชีตข้อมูล ทั้งชื่อ·ชนิด·ช่วงค่า·foreign key·คำอธิบาย -
$สคีมา: ชีตเฉพาะสำหรับนิยามคอลัมน์ที่วางไว้ในชีตข้อมูล Excel (xlsm) ไม่บรรจุแถวข้อมูล แต่บรรจุเฉพาะกฎของคอลัมน์ - FK (foreign key): คอลัมน์ที่อ้างอิงไปยัง PK (primary key) ของอีกชีตหนึ่ง เช่นclass_idชี้ไปยังแถวของชีต Class - proto: นิยาม Protocol Buffers (.proto) สัญญาว่าด้วยโครงสร้างข้อมูล·Enum ที่ไคลเอนต์และเซิร์ฟเวอร์ใช้ร่วมกัน - แหล่งความจริงเดียว (single source of truth): หลักการบริหารที่จัดการข้อมูลเดียวกันไว้ที่เดียว เพื่อให้ทุกฝ่ายดูที่แหล่งนั้น
ถ้าเข้าใจหลักสคีมามาก่อนเพียงแค่ว่า "นิยามคอลัมน์ไว้ล่วงหน้า" ก็เท่ากับเข้าใจไปแค่ครึ่งเดียว แก่นของมันอยู่ที่ ลำดับว่าจะป้อนอะไรก่อน ต่างหาก ลำดับการเคลื่อนของมือที่กรอกข้อมูลนั่นแหละ เป็นตัวกำหนดว่าความสอดคล้องจะถูกรักษาไว้หรือพังทลาย
ลำดับการป้อนข้อมูลที่หนังสือเล่มนี้แนะนำ คือไปป์ไลน์สี่ช่อง
flowchart LR
A["ชีต $สคีมา
(นิยามกฎของคอลัมน์)"] --> B["Enum / *.proto
(สร้างสัญญาโค้ดด้วย VBA Export)"]
B --> C["ข้อมูล csv
(กรอกแถวภายใต้กฎ)"]
A -.->|schema-doc| D["เอกสารสคีมา
(.md สร้างอัตโนมัติ)"]
C -.->|gen_relation_map.py| E["แผนผังความสัมพันธ์ FK
(HTML สร้างอัตโนมัติ)"]
D -.-> F(("AI / คน
อ่านนิยามเดียวกัน"))
E -.-> F
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class A,B,C,D,E data
เส้นทึบที่ไหลจากซ้ายไปขวาคือ ลำดับการป้อนที่ถูกบังคับ นิยาม $สคีมา ก่อน จากนั้นดึง Enum และ proto ออกมาด้วย VBA (ภาษามาโครของ Excel) Export แล้วจึงกรอกข้อมูล csv ภายในขอบเขตของสัญญานั้นเท่านั้น เส้นประคือสิ่งที่ถูกสร้างขึ้นโดยอัตโนมัติจากการป้อนนั้น—เอกสารสคีมา (schema-doc) และแผนผังความสัมพันธ์ FK (gen_relation_map.py)—ซึ่งคนและ AI มองเห็นนิยามเดียวกันผ่านอนุพันธ์เหล่านี้
ตราบใดที่ลำดับนี้ถูกบังคับ บรรทัดสีแดง 28 บรรทัดที่เห็นในช่วงเปิดบทนี้ส่วนใหญ่จะถูกปิดลง ก่อนจะกรอกข้อมูล ถ้าข้อเท็จจริงที่ว่า element ต้องเป็นหนึ่งในสี่ค่า fire/ice/lightning/none ถูกตรึงไว้เป็น Enum ของ proto ทั้ง Fire และ 화염 ก็จะถูกดักไว้ตั้งแต่ขั้นตอนป้อนข้อมูล ถ้าข้อเท็จจริงที่ว่า class_id อ้างอิงไปยัง PK ของชีต Class ถูกระบุไว้ใน $สคีมา การขาดหายของหมายเลข 47 ก็จะถูกจับได้ที่ขั้นตรวจสอบก่อน ไม่ใช่ที่บิลด์
ถ้ากลับลำดับ—กรอกข้อมูลก่อนแล้วค่อยจัดการสคีมาทีหลัง—สคีมาก็จะกลายเป็นการตามเก็บกวาดภายหลัง การไปแก้กฎของคอลัมน์บนตำแหน่งที่สะสมข้อมูลไว้แล้ว 1000 แถว ทำให้กฎต้องวิ่งตามข้อมูล และในวินาทีนั้น แหล่งความจริงก็กลับหัวกลับหาง
$สคีมา ถึง csv ในครั้งเดียวแทนที่จะอธิบายด้วยคำพูด ผู้เขียนจะพาชีตหนึ่งผ่านกระบวนการตั้งแต่ต้นจนจบจริง สมมติว่าจะสร้างชีตสกิลขึ้นใหม่ ด้านล่างคือบันทึกทั้งหมดที่ดำเนินไปโดยมี AI ช่วย ผู้เขียนจะไม่ย่อ และจะคงจุดที่ผิดพลาดและจุดที่คนปฏิเสธไว้ตามเดิม
$สคีมา ด้วยมือก่อนยังไม่เรียกทั้งเครื่องมือและ AI คนนิยามกฎของคอลัมน์ด้วยตัวเอง เฉพาะขั้นตอนนี้จะไม่มอบหมายให้ใคร
# ชีต Skill $สคีมา (เขียนโดยคน)
คอลัมน์ ชนิด ช่วงค่า/ข้อจำกัด FK คำอธิบาย
skill_id int 1000~9999 (PK) ID เฉพาะของสกิล
name string 1~30 ตัวอักษร - ชื่อที่แสดง
class_id int(FK) - Class.class_id คลาสที่ใช้ได้
cooldown float 0.0~120.0 - เวลารอใช้ซ้ำ (วินาที)
damage_coef float 0.0~10.0 - สัมประสิทธิ์คูณพลังโจมตี
element enum fire/ice/lightning/none - ธาตุ
is_passive bool true/false - เป็นพาสซีฟหรือไม่
งานคือนำ enum element และโครงสร้าง FK ของ $สคีมา ไปแปลงเป็นสัญญา proto เป็นการแปลงรูปแบบ จึงเป็นงานที่มอบหมายให้ AI ได้
พรอมต์ (ฉบับเต็ม): ด้านล่างคือข้อกำหนดคอลัมน์ของชีตข้อมูลเกม จงแปลงข้อกำหนดนี้เป็นข้อความ (message) และ Enum ของ Protocol Buffers (proto3) กฎ: (1) คอลัมน์ชนิด enum ให้นิยามเป็น enum แยกต่างหาก และค่าแรกต้องเริ่มด้วย
_UNSPECIFIED = 0เสมอ (2) คอลัมน์ FK ให้คงเป้าหมายอ้างอิงไว้เป็นคอมเมนต์ (3) ข้อจำกัดช่วงค่าไม่สามารถแสดงด้วย proto ได้ จึงให้คงไว้เป็นคอมเมนต์เท่านั้น อย่าเพิ่มคอลัมน์ที่ไม่มีในข้อกำหนดskill_id int PK / name string / class_id int FK→Class.class_id / cooldown float 0~120 / damage_coef float 0~10 / element enum{fire,ice,lightning,none} / is_passive bool
syntax = "proto3";
// สัญญาข้อมูลของชีต Skill
// หมายเหตุ: ข้อจำกัดช่วงค่า (cooldown 0~120, damage_coef 0~10)
// ไม่สามารถบังคับด้วย proto ได้ ให้ตรวจสอบใน Linter
enum SkillElement {
SKILL_ELEMENT_UNSPECIFIED = 0;
SKILL_ELEMENT_FIRE = 1;
SKILL_ELEMENT_ICE = 2;
SKILL_ELEMENT_LIGHTNING = 3;
SKILL_ELEMENT_NONE = 4; // ← เครื่องหมายสำหรับให้คนตรวจสอบ
}
message Skill {
int32 skill_id = 1; // PK
string name = 2;
int32 class_id = 3; // FK → Class.class_id
float cooldown = 4;
float damage_coef = 5;
SkillElement element = 6;
bool is_passive = 7;
}
ตรงนี้คนจะปฏิเสธอยู่จุดหนึ่ง AI วาง none ไว้เป็น SKILL_ELEMENT_NONE = 4 ที่ตำแหน่ง ท้ายสุด ของ enum แต่ใน $สคีมา นั้น none คือสถานะตั้งต้นที่หมายความว่า "ไม่มีธาตุ" ประเด็นอยู่ที่ค่าหมายเลข 0 ของ proto ใน proto3 ค่าหมายเลข 0 ของ enum คือ "ตำแหน่งที่ถูกใส่เข้ามาอัตโนมัติเมื่อค่ายังไม่ถูกกรอก" จึงต้องเลือกว่าจะเว้นหมายเลข 0 ไว้เป็น _UNSPECIFIED (ไม่ได้ป้อนค่า) หรือจะเติมด้วยค่าที่มีความหมายอย่าง none (ไม่มีธาตุโดยตั้งใจ) ถ้ารวมทั้งสองไว้ที่ตำแหน่งเดียว แถวที่เว้นช่องว่างไว้ (ความผิดพลาด) กับ แถวที่เลือกไม่มีธาตุโดยจงใจ (ความตั้งใจ) จะเข้ามาเป็น 0 เหมือนกัน และแยกกันไม่ออกตลอดไป AI แยก _UNSPECIFIED = 0 กับ none ออกจากกันก็จริง แต่ดัน none ไปอยู่ตำแหน่งหมายเลข 4 ท้ายสุด ทำให้สถานะตั้งต้นที่พบบ่อยที่สุดถูกผลักออกห่างจากการค้นหาและการดีบัก
การตัดสินใจที่คนลงไป:
- คง _UNSPECIFIED = 0 ไว้ (สำหรับตรวจจับการขาดหาย)
- คง none ไว้ตามเดิม แต่เพิ่มในกฎการเขียน csv ว่า "ไม่มีธาตุต้องระบุ none เสมอ ห้ามเว้นว่าง" ช่องว่าง=0=UNSPECIFIED ให้ถือเป็นความผิดพลาดในการป้อนข้อมูล
- เพิ่มการตัดสินใจนี้ลงในคำอธิบายของแถว element ใน $สคีมา หนึ่งบรรทัด: "ไม่มีธาตุก็ต้องระบุ (none) ช่องว่างคือความผิดพลาด"
ไม่ได้ใช้ผลลัพธ์ของ AI ตามเดิม รับรูปแบบไว้ แต่ขอบเขตของความหมายเป็นสิ่งที่คนขีดเส้นเอง
นิยาม proto ที่ผ่านการตรวจสอบแล้วจะถูกแมโคร Export ของ Excel (ปุ่มในชีต $สคีมา) ส่งออกอย่างสม่ำเสมอ คนจะไม่พิมพ์ซ้ำอีกครั้งใน IDE เพราะชีตคือแหล่งความจริง proto จึงออกมาจากชีต ลำดับนี้เป็นการต่อยอดของหลักการ "ผลลัพธ์คือฉบับจริง" (json_over_schema_doc_as_source_of_truth) ที่จะกล่าวถึงใน 3.3—ไม่ใช่เอกสารอธิบายโค้ด แต่ชีตเป็นตัวให้กำเนิดโค้ด
พรอมต์ (ฉบับเต็ม): จงสร้างแถว csv 5 แถวที่เป็นไปตาม Skill proto และ $สคีมาข้างต้น ข้อจำกัด: class_id เป็นหนึ่งใน [1,2,3] (PK ที่มีอยู่ในชีต Class ปัจจุบัน), damage_coef ถ้าเป็นพาสซีฟ (is_passive=true) ให้เป็น 0.0, element ให้เป็นสตริง fire/ice/lightning/none ตามเดิม, ห้ามเว้นว่าง
ในผลลัพธ์ครั้งแรกของ Claude มีอยู่แถวหนึ่งที่ผิดพลาด
skill_id,name,class_id,cooldown,damage_coef,element,is_passive
1001,เฉือนเปลวเพลิง,1,3.5,2.4,fire,false
1002,มือเยือกแข็ง,2,8.0,3.1,ice,false
1003,ฟื้นฟูพลังชีวิต,1,0.0,1.2,none,true ← ปฏิเสธ: เป็นพาสซีฟแต่ damage_coef≠0
1004,หอกสายฟ้า,3,5.0,2.8,lightning,false
1005,ท่าตั้งรับ,2,0.0,0.0,none,true
แถว 1003 ละเมิดกฎ (ถ้า is_passive=true ก็ต้องเป็น damage_coef=0.0) คนจึงปฏิเสธและขอใหม่
การขอใหม่ (ฉบับเต็ม): แถว 1003 ละเมิดกฎ is_passive=true แต่ damage_coef=1.2 พาสซีฟต้องเป็น 0.0 จงแก้เฉพาะ 1003 แล้วส่งกลับมาใหม่
ผลลัพธ์ใหม่ของ Claude:
1003,ฟื้นฟูพลังชีวิต,1,0.0,0.0,none,true
การที่ AI ทำไม่ถูกหมดในครั้งแรกนั้นไม่ใช่ข้อบกพร่อง แต่เป็นเรื่องที่เกิดขึ้นได้เป็นปกติ สิ่งที่สำคัญคือ เพราะมีสคีมาปูรองอยู่ จึงสามารถ มองเห็นแถวที่ผิดเพี้ยนนั้นด้วยตาแล้วย้อนกลับได้ด้วยบรรทัดเดียว ถ้าไม่มีสคีมา แถว 1003 คงจะถูกพบเป็นบั๊กในเกมหลังบิลด์ ที่พาสซีฟกลับสร้างดาเมจออกมา
บทเรียนของบันทึกทั้งหมดนี้เรียบง่าย ถ้าลำดับการป้อนข้อมูลถูกตรึงไว้เป็น $สคีมา → proto → csv AI จะกรอกรูปแบบได้รวดเร็ว และคนจะตรวจสอบเฉพาะความหมายและการละเมิด ถ้าลำดับพังทลาย คนจะต้องแบกรับทั้งหมดตั้งแต่รูปแบบไปจนถึงความหมาย
การวาง $สคีมา ไว้ใน Excel นั้นสะดวกสำหรับนักออกแบบเกม แต่เป็นที่ปิดสำหรับ AI กับ git และเครื่องมือภายนอก ดังนั้นจึงมีการใช้เครื่องมือที่แปลง $สคีมา เป็น Markdown โดยอัตโนมัติ คำสั่งสแลช (slash command) schema-doc ทำหน้าที่นี้
การทำงานมีสี่ขั้นตอน
$สคีมา ของ Excel (xlsm) (python-calamine, เร่งความเร็วด้วย Rust)<ชื่อชีต>_schema.md ในโฟลเดอร์เดียวกันแก่นอยู่ที่ คนจะไม่เขียนสคีมาสองครั้ง นิยามใน Excel ครั้งเดียว แล้ว Markdown จะถูกเครื่องมือสร้างให้ ทั้งสองจึงไม่มีทางขัดแย้งกันได้ กับดักที่ว่า "ถ้าตั้งเอกสารสคีมาเป็นฉบับจริงจะขัดกับผลลัพธ์จริง" ซึ่งจะกล่าวถึงใน 3.3 นั้น ในที่นี้ถูกพลิกหลบด้วยการให้ "Excel เป็นฉบับจริง เอกสารเป็นอนุพันธ์"
ผลลัพธ์ที่ schema-doc สร้างขึ้น (อ้างอิงชีต Skill จากบันทึกก่อนหน้า):
# สคีมาชีต Skill (สร้างอัตโนมัติ — ห้ามแก้ไขโดยตรง)
| คอลัมน์ | ชนิด | ช่วงค่า/ข้อจำกัด | FK | คำอธิบาย |
|---|---|---|---|---|
| skill_id | int | 1000~9999 | (PK) | ID เฉพาะของสกิล |
| name | string | 1~30 ตัวอักษร | - | ชื่อที่แสดง |
| class_id | int(FK) | - | Class.class_id | คลาสที่ใช้ได้ |
| cooldown | float | 0.0~120.0 | - | เวลารอใช้ซ้ำ (วินาที) |
| damage_coef | float | 0.0~10.0 | - | สัมประสิทธิ์คูณพลังโจมตี |
| element | enum | fire/ice/lightning/none | - | ธาตุ ไม่มีธาตุก็ต้องระบุ (none) ช่องว่างคือความผิดพลาด |
| is_passive | bool | true/false | - | เป็นพาสซีฟหรือไม่ ถ้า true แล้ว damage_coef=0 |
_source: Skill.xlsm / generated by schema-doc_
โปรดสังเกตว่าขอบเขตที่คนขีดไว้ในขั้นที่ 4 และ 6 ของ 3.2.2 ตามเข้ามาในช่องคำอธิบายของ element และ is_passive ตามเดิม คนเขียนเพิ่มลงใน $สคีมา หนึ่งบรรทัด แล้วเอกสาร·proto·การตรวจสอบก็มาใช้กฎเดียวกันร่วมกันหมด นี่คือภาพที่แหล่งความจริงเดียวทำงานจริง
สคีมาที่ออกมาเป็น Markdown ถูกนำไปใช้ทันทีในสามที่
ถ้าสคีมาเป็นกฎ ภายใน ชีต FK ก็เป็นกฎ ระหว่าง ชีต นิยามที่ว่า class_id อ้างอิงไปยังชีต Class นั้นเขียนไว้ใน $สคีมา แต่การจะรู้ว่าการอ้างอิงนั้นยังมีชีวิตอยู่จริงในวินาทีนี้หรือไม่ ต้องมีการตรวจสอบแยกต่างหาก
gen_relation_map.py ตรวจจับความสัมพันธ์ FK ของชีตข้อมูลโดยอัตโนมัติ แล้ววาดเป็นแผนผังความสัมพันธ์ HTML แบบโต้ตอบได้ เมื่อลูกศรอย่าง class_id→Class ของ Skill, set_id→ItemSet ของ Item มารวมอยู่ในหน้าจอเดียว "FK ที่เป้าหมายอ้างอิงหายไป" ก็จะปรากฏให้เห็นเป็นลูกศรที่ขาด อุบัติเหตุอย่างการขาดหายของหมายเลข 47 ในช่วงเปิดบทนี้ จะปรากฏ ระหว่างที่กำลังกรอกข้อมูล เป็นเส้นที่ขาดในแผนผังความสัมพันธ์ ไม่ใช่เป็นบรรทัดสีแดงในล็อกบิลด์
การใช้งานจริงและการแสดงผลของเครื่องมือนี้จะกล่าวถึงอย่างเต็มที่ใน 3.3 สิ่งที่ต้องจดจำในบทนี้มีเพียงหนึ่งเดียว ถ้า $สคีมา ไม่ระบุ FK ทั้งแผนผังความสัมพันธ์และการตรวจสอบความสอดคล้องก็ไม่มีกราฟให้วาด การระบุ FK ไม่ใช่ทางเลือก แต่เป็นเงื่อนไขเบื้องต้นของหลักสคีมามาก่อน
ถ้านำบันทึกของ 3.2.2 มาทำให้เป็นรูปทั่วไป จะได้ห้าขั้นตอน เมื่อแยกผู้รับผิดชอบและผลลัพธ์ของแต่ละขั้นออกมา จะเห็นชัดว่าอะไรที่คนกุมไว้และอะไรที่ส่งต่อให้เครื่องมือ
ไม่จำเป็นต้องมีครบทั้งห้าขั้นในเดือนแรก แค่หมุน 1·2 ขั้น (ออกแบบสคีมา + ทำเอกสารอัตโนมัติ) ก็ได้คุณค่าไปครึ่งหนึ่งแล้ว ขั้นที่ 3\~5 ค่อยต่อเติมทีละน้อยหลังจากคุ้นเคยกับการดำเนินงาน ถ้าบังคับครบห้าขั้นตั้งแต่ต้น ภาระของผู้เขียนข้อมูลจะทำให้การดำเนินงานหยุดก่อนจะตั้งหลักได้
ในโปรเจกต์ MMORPG หนึ่งที่ผู้เขียนบริหารในฐานะ Director (ต่อไปนี้เรียก "โปรเจกต์ A") ผู้เขียนได้หมุนเวิร์กโฟลว์นี้ประมาณ 6 เดือน ในบรรดาตัวเลขด้านล่าง ความสม่ำเสมอของคอลัมน์ชีตข้อมูลและเวลาที่ใช้ร่างชีตใหม่ เป็นค่าที่วัดจริงซึ่งรวบรวมจากล็อกเครื่องมือและบันทึกการทำงาน ส่วนความถี่ของ FK ที่ขาด เป็น การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ที่คำนวณย้อนจากปัญหาบิลด์ล้มเหลว
| รายการ | ก่อนนำมาใช้ | หลังนำมาใช้ | ที่มา |
|---|---|---|---|
| ความสม่ำเสมอของชื่อคอลัมน์ | ประมาณ 60% | ประมาณ 95% | วัดจริงด้วยการเทียบ schema-doc |
| ความถี่ที่ FK ขาด | สัปดาห์ละ 2\~3 ครั้ง | เดือนละไม่เกิน 1 ครั้ง | คำนวณย้อนจากปัญหาบิลด์ (ประมาณการของผู้เขียน) |
| เวลาที่ใช้ร่างชีตใหม่ | 4\~8 ชั่วโมง | 1\~2 ชั่วโมง | วัดจริงจากบันทึกการทำงาน |
| การที่นักออกแบบเกมคนใหม่เข้าใจชีต | ประชุม 3 ครั้ง | เอกสาร 1 ครั้ง + ประชุม 1 ครั้ง | กรณีปฐมนิเทศ (บอกได้แค่ทิศทาง) |
ต้นทุนการนำมาใช้คือการพัฒนาเครื่องมือเริ่มต้นประมาณ 3 วัน + การตั้งหลักดำเนินงานประมาณ 1 เดือน บทสรุปจากการดำเนินงานคือ ต้นทุนการนำมาใช้นั้นน้อยเมื่อเทียบกับผลสะสม 6 เดือน อย่างไรก็ตาม อัตราส่วนข้างต้นเป็นกรณีเดี่ยวของทีมเดียว·โปรเจกต์เดียว จึงไม่มีหลักประกันว่าจะนำไปใช้กับทีมอื่นได้ตามเดิม
เมื่อมีสคีมาปูรองอยู่ ความน่าเชื่อถือของการสร้างข้อมูลด้วย AI จะพุ่งสูงขึ้นอย่างก้าวกระโดด เหตุผลคือสคีมาปิดช่วงการป้อนข้อมูลที่คลุมเครือซึ่งเป็นชนวนของอาการหลอน (hallucination) ไว้ล่วงหน้า เมื่อมีคำขอว่า "ช่วยสร้างสกิล 20 อัน" หากไม่มีสคีมา AI จะประดิษฐ์คอลัมน์ที่ดูเข้าท่าขึ้นมาแล้วเติมค่าที่ไม่เข้ากับชีตของเราเอง ถ้ามีสคีมา คำขอเดียวกันจะกลับมาเป็นแถวที่ปฏิบัติตามคอลัมน์ทั้ง 7·ข้อจำกัดแต่ละข้อ·FK ที่นิยามไว้ ต่อให้มีการละเมิดอย่างกรณีแถว 1003 ของ 3.2.2 ก็แค่ชี้บรรทัดเดียวแล้วขอใหม่ก็จบ
แต่ขอบเขตชัดเจน ค่าบาลานซ์ (balance) ไม่สั่งให้ AI ทำ ถ้าให้ AI กำหนด damage_coef "ตามที่เหมาะสม" มันจะขัดกับเจตนาของเกม สิ่งที่เป็นหน้าที่ของ AI คือการปูตัวเลือกที่รูปแบบถูกต้องให้เร็วเท่านั้น ส่วน "สัมประสิทธิ์ของสกิลนี้ที่เป็น 2.4 ถูกต้องหรือไม่" เป็นสิ่งที่คนตอบ แต่นั่นไม่ได้หมายความว่า AI ไร้ประโยชน์ต่อการบาลานซ์—ความเรียบลื่นของเส้นโค้ง·ค่าผิดปกติ·สถิติช่วงค่า AI จับได้รวดเร็ว มอบหมายการวัดตัวเลขให้เครื่องมือ ส่วนตัวเลขนั้นถูกต้องหรือไม่ คนเป็นผู้คัดแยก
| ความผิดพลาด | การหลีกเลี่ยง |
|---|---|
| นำสคีมามาใช้หลังสะสมข้อมูลไว้แล้ว 1000 แถว | ชีตใหม่ต้องทำ $สคีมา ก่อนเสมอ |
การซิงค์ระหว่าง $สคีมา กับ csv พังทลาย |
ผูกทั้งสองไว้ที่แหล่งเดียวด้วยการทำ schema-doc อัตโนมัติ |
| ไม่ระบุ FK | ถ้าไม่ระบุ FK แผนผังความสัมพันธ์·การตรวจสอบความสอดคล้องจะไร้ความหมาย |
| ใช้หมายเลข 0 ของ proto Enum เป็นค่าที่มีความหมาย | 0 คือ _UNSPECIFIED (ตรวจจับการขาดหาย) ค่าที่มีความหมายเริ่มจาก 1 |
| ให้เฉพาะคนอ่านเอกสารสคีมา | ทำให้ AI อ่านได้ด้วยตาราง Markdown + การรวมเมตาให้เป็นแบบเดียวกัน |
setup
1. เลือกชีตที่เป็นแกนหลักที่สุดหนึ่งชีตในสายงานของคุณ (เลือกหนึ่งในสกิล·ไอเทม·มอนสเตอร์)
2. เพิ่มชีตชื่อ $สคีมา ลงในไฟล์ Excel นั้น แล้วเขียน 5 องค์ประกอบ (ชื่อ·ชนิด·ช่วงค่า·FK·คำอธิบาย) ของแต่ละคอลัมน์ทีละบรรทัด ขั้นตอนนี้คนทำเอง
prompt (ใช้ AI เฉพาะกับการร่าง proto/csv เท่านั้น)
จงแปลง $สคีมาด้านล่างเป็นข้อความ (message) และ Enum ของ proto3 ค่าแรกของ enum คือ
_UNSPECIFIED = 0FK ให้คงเป้าหมายอ้างอิงไว้เป็นคอมเมนต์ ข้อจำกัดช่วงค่าให้คงไว้เป็นคอมเมนต์เท่านั้น ห้ามเพิ่มคอลัมน์ที่ไม่มีในข้อกำหนด (วาง $สคีมาของคุณตรงนี้)
ต่อด้วย:
แถว csv 5 แถวที่เป็นไปตาม proto และ $สคีมาข้างต้น อย่าสร้างแถวที่ละเมิดข้อจำกัด ถ้า is_passive=true ให้ damage_coef=0
verify
1. เทียบ 5 แถวที่ AI ให้มากับสคีมาทีละบรรทัด ถ้ามีแถวที่ละเมิด ให้ขอใหม่ว่า "แถว N ละเมิด แก้เฉพาะแถวนั้นให้หน่อย" (การปฏิเสธและการขอใหม่เป็นกระบวนการปกติ)
2. ดึง $สคีมา ออกมาเป็น .md ด้วย schema-doc (หรือสคริปต์ Python อย่างง่ายที่เทียบเท่า) แล้วตรวจว่านิยามใน Excel กับเอกสารตรงกันหรือไม่
3. ถ้ามี FK ให้เทียบสักครั้งว่า PK ของเป้าหมายอ้างอิงมีอยู่จริงหรือไม่
ถ้าเริ่มต้นคนเดียวโดยไม่มีทั้งเครื่องมือและทีม ไฟล์ Excel หนึ่งไฟล์·เท็กซ์เอดิเตอร์หนึ่งตัวก็เพียงพอ
$สคีมา ไว้ที่แท็บแรกของชีต แล้วเขียนกฎของคอลัมน์เป็น 5 องค์ประกอบ (15 นาที)$สคีมา ลง Notepad เป็น skill_schema.md ไว้ นี่คือแหล่งความจริงเดียวแหล่งแรกของคุณเมื่อขยับไปยังชีตถัดไป ให้ทำซ้ำ 4 ขั้นเดิม เมื่อชีตแกนหลัก 5\~10 ชีตในไตรมาสถูกจัดเรียงด้วยลำดับเดียวกัน ตอนนั้นแหละจึงจะเกิดคุณค่าที่จะต่อระบบอัตโนมัติอย่าง schema-doc เข้ามา
$สคีมา→Enum/proto→csv การละเมิดจะถูกปิดก่อนกรอกข้อมูลนักออกแบบเกมที่เพิ่งเข้ามาใหม่เดินมาที่โต๊ะของผมในสัปดาห์แรกของการเริ่มงาน "ผมอยากแก้ตารางรางวัลของเควสต์ครับ ถ้าแตะตรงนี้แล้วมันจะพังตรงไหนบ้าง" ผมกำลังจะชี้ไปที่จอแล้วตอบ แต่ก็หยุดไว้ ในหัวของผมมีภาพอยู่ RewardTable ยึดอยู่กับ ItemTable ItemTable ยึดอยู่กับ ItemEffectTable และเหนือขึ้นไปก็มี QuestTable ที่อ้างอิงรางวัล… แต่พอผมพยายามแปลงภาพนั้นออกมาเป็นคำพูด รูปร่างมันก็พังทลายลงในหัวของคนฟัง ผมวาดกล่องเจ็ดกล่องลงบนไวต์บอร์ด ลูกศรเริ่มพันกันยุ่ง สามสิบนาทีต่อมา เขาพยักหน้าแล้วกลับไปที่โต๊ะ และวันรุ่งขึ้นก็กลับมาพร้อมคำถามเดิมอีกครั้ง
ฉากนี้แหละที่ทำให้ผมเขียนบทนี้ขึ้นมา ในหัวของนักออกแบบระบบมีกราฟการพึ่งพากันอยู่ ปัญหาคือมันอยู่แค่ในหัวเท่านั้น พอคนเปลี่ยน ภาพก็หายไปด้วย ผมต้องการเครื่องมือที่ดึงภาพนั้นออกมาไว้ภายนอก และสิ่งที่ผมสร้างขึ้นมาก็คือ gen_relation_map.py
เมื่อชีตข้อมูลมีอยู่ 5\~10 ชีต การใช้หัวคิดก็เพียงพอ แต่พอเกิน 30 ชีต ความจำขณะทำงานของคนก็รับไม่ไหว โฟลเดอร์ชีตของโปรเจกต์หนึ่งมักจะข้ามเส้นนั้นไปตั้งแต่เนิ่น ๆ ตารางที่เขียนเป็นข้อความว่าอะไรพึ่งพาอะไร อ่านแล้วก็วาดเป็นภาพไม่ออก บทนี้จะติดตามกระบวนการบันทึกเซสชันจริง (worked transcript) ในการสร้างแผนผังความสัมพันธ์แบบ HTML เชิงโต้ตอบจากความสัมพันธ์ foreign key โดยอัตโนมัติ ตั้งแต่ต้นจนจบ
ก่อนสร้างเครื่องมือ ผมจะชี้ให้เห็นก่อนว่าเวลาไม่มีแผนผังความสัมพันธ์ จริง ๆ แล้วมันติดขัดตรงไหนบ้าง สี่ฉากนี้เกิดขึ้นซ้ำ ๆ
การ onboarding นักออกแบบใหม่ นักออกแบบคนใหม่นัดประชุมเพื่อทำความเข้าใจโครงสร้างระบบ ก็คือฉากข้างบนนั่นแหละ การพึ่งพากันที่ถ่ายทอดด้วยคำพูดอยู่ในหัวของคนฟังได้ไม่กี่วัน แต่ถ้าคลิกดูแผนผังความสัมพันธ์ด้วยกันสักหนึ่งแผ่น ในการประชุมครั้งแรกก็เห็นภาพไปแล้วเกินครึ่ง จุดที่แตกต่างจากภาพวาดมือบนไวต์บอร์ดอย่างชัดเจนคือ ภาพนี้ไม่ถูกลบและยังคงอยู่ตรงนั้น
การถกเถียงเรื่องขอบเขตผลกระทบของการเปลี่ยนแปลง มีคำขอเปลี่ยนแปลงระบบเข้ามา "อันนี้ส่งผลกระทบตรงไหนบ้าง" ก็นัดประชุม ถกกันอยู่นาน แต่ก็ยังมีพื้นที่ที่ตกหล่นออกมาหนึ่งสองจุด ถ้ามีแผนผังความสัมพันธ์ แค่คลิกที่โหนดเป้าหมายการเปลี่ยนแปลงแล้วไล่ตาม inbound edge ขอบเขตผลกระทบก็เข้ามาอยู่ในสายตา การถกเถียงก็เหลือแค่ "ผลกระทบนี้ถูกต้องจริงไหม" และการจัดลำดับความสำคัญเท่านั้น
การตรวจจับการพึ่งพาแบบย้อนทิศ การที่ชีตข้อมูล L3 อ้างอิงเอกสารระบบ L1 เป็นเรื่องปกติ แต่ทิศทางตรงข้าม (Layer ที่อยู่ระดับสูงกว่าอ้างอิงชีตข้อมูลระดับล่างโดยตรง) เกือบจะเป็นข้อบกพร่องในการออกแบบเสมอ ในรายการ FK ที่เขียนเรียงเป็นข้อความ คนจับการย้อนทิศนี้ไม่ได้ แต่ในภาพ มันจะปรากฏให้เห็นทันทีด้วยลูกศรเส้นเดียวที่สีของ Layer ไม่เข้ากัน
การค้นพบชีตที่โดดเดี่ยว บางครั้งก็พบชีตที่ไม่ถูกอ้างอิงจากที่ไหนเลย อาจเป็นเศษซากของการออกแบบเก่า หรือเป็นกรณีที่ตัดสินใจเลิกใช้แล้วแต่ไฟล์ยังเหลืออยู่ เหมือนกล่องไร้ฉลากที่กลิ้งอยู่ตามมุมห้องทำงาน ต้องมีภาพถึงจะค้นพบเกาะร้างนั้นได้
จุดร่วมของปัญหาทั้งสี่ข้อคือ ทั้งหมด "ต้องมองโครงสร้างด้วยตาถึงจะแก้ได้" ด้วยข้อความและตารางมันตัน
ทีนี้มาติดตามกันจริง ๆ อินพุตคือโฟลเดอร์หนึ่งที่บรรจุชีตข้อมูล เอาต์พุตคือ HTML เชิงโต้ตอบหนึ่งแผ่นที่เปิดในเบราว์เซอร์ ผมจะบันทึกสิ่งที่ AI ทำและจุดที่คนตรวจสอบ/ปฏิเสธไว้ในระหว่างนั้นโดยไม่ตกหล่น
flowchart TD
A[โฟลเดอร์ชีตข้อมูล
xlsm/xlsx จำนวนมาก] --> B[1. สแกน: เก็บส่วนหัวของชีตและคอลัมน์]
B --> C[2. ดึงตัวเลือก FK
*_id / *Id / FK ที่ระบุในเอกสารกำหนด]
C --> D[3. จับคู่เป้าหมายการอ้างอิง
ชื่อคอลัมน์ → ชีตเป้าหมาย]
D --> E{การตรวจสอบโดยมนุษย์}
E -->|ปฏิเสธผลบวกลวง| C
E -->|ผ่าน| F[4. สร้างกราฟ
โหนด=ชีต, เอดจ์=FK]
F --> G[5. ใส่เมตา Layer
อ้างอิงผลลัพธ์ schema-doc]
G --> H[6. เรนเดอร์ HTML ด้วย pyvis]
H --> I[relation_map.html
โต้ตอบในเบราว์เซอร์]
I --> J{การวินิจฉัยโดยมนุษย์}
J -->|พบการย้อนทิศ·โดดเดี่ยว·วนรอบ| K[ขอแก้ไขการออกแบบ]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class A,I data
class B,C,D,F,G,H code
class E,J,K human
หัวใจอยู่ที่ลูปการตรวจสอบโดยมนุษย์ระหว่างขั้นที่ 3 กับขั้นที่ 5 การดึงตัวเลือก FK นั้นเครื่องวางร่างต้นแบบ แล้วคนก็คัดผลบวกลวงออกจากตรงนั้น ถ้าข้ามลูปนี้ไป แผนผังความสัมพันธ์ก็จะดูน่าเชื่อถือแต่เป็นภาพที่ผิด
ความแม่นยำของเครื่องมือนี้ถูกกำหนดด้วยว่าดึงอินพุตมาจากไหน หลักการ schema-first ที่กำหนดไว้ใน 3.2 ถูกนำมาใช้ตรง ๆ ลำดับฉบับที่ยึดเป็นหลักของข้อมูล FK เป็นดังนี้
$스키마 — ฉบับที่ยึดเป็นหลักอันดับแรกของแต่ละชีตข้อมูล มีการระบุประเภท·Enum·เป้าหมาย FK แยกตามคอลัมน์ ถ้ามี FK เขียนอยู่ตรงนี้ นั่นคืออันดับ 1*.proto / Enum — สคีมาที่ส่งออกมาด้วย Export ของ VBA (ภาษามาโครของ Excel) เสริมประเภทเมื่อเอกสารกำหนดว่างเปล่าcsv จริง — ข้อมูลจริงที่ชีตส่งออกมา ความสัมพันธ์ที่ไม่มีในเอกสารกำหนดก็ปรากฏเป็นแพตเทิร์นในข้อมูลได้ (เช่น ถ้าค่าของคอลัมน์ npc_id อยู่ในช่วงคีย์ของ NPCTable ทั้งหมด ก็ถือว่าเป็น FK โดยพฤตินัย)ตรงนี้ผมขอกำหนดหลักการหนึ่งให้ชัดเจน ฉบับที่ยึดเป็นหลักไม่ใช่เอกสารสคีมา แต่เป็นเอาต์พุต JSON/csv จริง แม้ในเอกสารกำหนดจะเขียนว่า reward_id เป็น FK แต่ถ้าในข้อมูลจริงคอลัมน์นั้นว่างเปล่าหรือชี้ไปยังค่าที่ไม่เกี่ยวข้อง ก็แปลว่าเอกสารกำหนดผิด เครื่องมือจะเชื่อฝั่งข้อมูลเมื่อทั้งสองไม่ตรงกัน และจะบันทึกความไม่ตรงกันนั้นไว้ในรายงาน นี่คือเหตุผลที่ไม่ตั้งให้ schema-doc เป็นฉบับที่ยึดเป็นหลัก
การกระทำแรกของเครื่องมือคือ เปิด xlsm/xlsx ทุกไฟล์ในโฟลเดอร์เพื่อเก็บส่วนหัวของชีตและคอลัมน์ ขั้นนี้เป็นเชิงกลไกล้วน ๆ จึงไม่น่าสนใจ ที่น่าสนใจคือกฎในการดึงตัวเลือก FK ตอนแรกมันเรียบง่าย ถ้าชื่อคอลัมน์ลงท้ายด้วย *_id หรือ *Id ก็ถือเป็นตัวเลือก FK แต่กฎข้อเดียวนี้ทำให้เกิดทั้งผลบวกลวงและการตกหล่น
ตรงนี้ผมเอา AI มาเสริม ผมส่งรายการส่วนหัวของคอลัมน์ทั้งก้อนไปให้แล้วให้มันอนุมานชีตเป้าหมายของ FK ข้างล่างนี้คือพรอมต์เต็มที่ผมส่งไปจริงและผลลัพธ์ดิบ
พรอมต์: ต่อไปนี้คือชื่อชีตและรายการส่วนหัวของคอลัมน์ของชีตข้อมูลเกม จงอนุมานว่าแต่ละคอลัมน์เป็น foreign key (FK) หรือไม่ และถ้าเป็น FK มันอ้างอิงคอลัมน์ใดของชีตใด กฎของชื่อคอลัมน์โดยทั่วไปคือ
<รูปเอกพจน์ของชีตเป้าหมาย>_idแต่ถ้าไม่มั่นใจ จงทำเครื่องหมายว่า "ไม่แน่ใจ" แล้วเขียนเหตุผล อย่าเติมด้วยการเดาQuestTable: quest_id, title, npc_id, reward_id, prev_quest_id, region_id NPCTable: npc_id, name, region_id, faction_id RewardTable: reward_id, item_id, count, currency_id ItemTable: item_id, name, item_effect_id, grade ItemEffectTable: item_effect_id, effect_type, value RegionTable: region_id, name, parent_region_id
ผลลัพธ์ดิบของ Claude (ละแถว PK ไว้ คัดเฉพาะการตัดสิน FK):
QuestTable -
npc_id→ NPCTable.npc_id (คาดว่าเป็น NPC ที่ให้เควสต์) -reward_id→ RewardTable.reward_id -prev_quest_id→ QuestTable.quest_id (เควสต์ก่อนหน้า, การอ้างอิงตัวเอง) -region_id→ RegionTable.region_idNPCTable -
region_id→ RegionTable.region_id -faction_id→ ไม่แน่ใจ: ไม่มี FactionTable อยู่ในรายการอินพุต ตามกฎของชื่อคอลัมน์ดูเหมือนจะอ้างอิง FactionTable.faction_id แต่เนื่องจากไม่ได้ให้ชีตนั้นมา จึงไม่สามารถยืนยันเป้าหมายได้RewardTable -
item_id→ ItemTable.item_id -currency_id→ ไม่แน่ใจ: ไม่มี CurrencyTable อยู่ในรายการ ประเภทของสกุลเงินอาจถูกจัดการเป็น Enum ก็ได้ ดังนั้นอาจไม่ใช่ FK จำเป็นต้องตรวจสอบเอกสารกำหนดItemTable -
item_effect_id→ ItemEffectTable.item_effect_id -grade→ ไม่ใช่ FK (คาดว่าเป็น Enum: ค่าระดับ)ItemEffectTable -
effect_type→ ไม่ใช่ FK (คาดว่าเป็น Enum)RegionTable -
parent_region_id→ RegionTable.region_id (การอ้างอิงตัวเอง, โครงสร้างลำดับชั้นของภูมิภาค)หมายเหตุ: การอนุมานข้างต้นอ้างอิงจากกฎของชื่อคอลัมน์ และไม่ได้ตรวจสอบค่าข้อมูลจริง โดยเฉพาะคอลัมน์ที่ไม่ได้ลงท้ายด้วย
_idอย่างcountและvalueผมตัดออกจากเป้าหมายการตรวจ FK แต่ถ้ามีการอ้างอิงที่ซ่อนอยู่ ก็จะตกหล่นไป
ในผลลัพธ์นี้ สิ่งที่คนทำมีสามอย่าง
faction_id → AI จัดให้เป็นไม่แน่ใจเพราะไม่มี FactionTable ถูกต้องแล้ว แต่จริง ๆ faction_id เป็น Enum เนื่องจากแฟกชันถูกตรึงไว้ที่ 6 ชนิด จึงจัดการด้วย Enum โดยไม่ต้องมีชีตแยก เพราะ AI ไม่ได้กุชีตที่ไม่มีขึ้นมาและหยุดการตัดสินไว้ คนจึงดูเอกสารกำหนดแล้วยืนยันเป็น Enum ได้ ตัดออกจาก FKcurrency_id → AI เปิดความเป็นไปได้ไว้ทั้งสองทาง พอดูข้อมูลจริงก็พบว่า CurrencyTable มีอยู่ (ผมเองที่ตกหล่นในรายการอินพุต) ยืนยันเป็น FK นี่เป็นการตกหล่นในอินพุตของคน ไม่อาจโทษ AI ได้prev_quest_id และ parent_region_id ถ้าเป็นแค่กฎ regex ธรรมดาก็คงพลาดไป การที่ AI ใส่ความหมายอย่าง "เควสต์ก่อนหน้า" "ลำดับชั้นของภูมิภาค" มาให้ด้วย ช่วยให้การตรวจสอบเร็วขึ้นบทเรียนที่ได้จากตรงนี้ชัดเจน จุดที่ AI มีประโยชน์ที่สุดไม่ใช่การอนุมานที่รวดเร็ว แต่เป็นความยับยั้งชั่งใจที่เว้นตำแหน่งที่ไม่รู้ไว้เป็น "ไม่แน่ใจ" ถ้ามันฝืนเติมช่องว่าง faction_id ก็คงถูกเชื่อมไปยังชีตที่ไม่เกี่ยวข้อง และผลบวกลวงนั้นก็จะเหลืออยู่ในแผนผังความสัมพันธ์เป็นลูกศรปลอม ซึ่งจะนำทางนักออกแบบใหม่ไปผิดทาง
เมื่อได้รายการ FK ที่ผ่านการตรวจสอบแล้ว gen_relation_map.py ก็จะสร้างกราฟ ชีตเป็นโหนด FK เป็นเอดจ์แบบมีทิศทาง มันนับจำนวน inbound edge (ชีตอื่นอ้างอิงฉันมากแค่ไหน) เพื่อกำหนดขนาดของโหนด ยิ่งถูกอ้างอิงมากก็ยิ่งเป็นโหนดใหญ่ นั่นคือฮับของระบบ
เมตาดาตาของ Layer ดึงมาจากเอกสารสคีมา markdown ที่สกิล schema-doc สร้างขึ้น พิกัด Layer (L0\~L4) ที่นิยามไว้ใน 3.1 ถูกติดเป็นป้ายให้แต่ละชีต และเครื่องมือก็อ่านมันแล้วลงสีโหนด การเชื่อมนี้สำคัญ ถ้าแผนผังความสัมพันธ์ไม่รู้จัก Layer มันก็เป็นแค่กล่องกับลูกศร ต้องรู้จัก Layer ถึงจะวินิจฉัย "การย้อนทิศ" ด้วยสีได้
ถ้าแสดงโครงสร้างภายในของเครื่องมือออกมาเป็นโครงโค้ดก็เป็นดังนี้ (คัดเฉพาะกระแสหลัก)
# gen_relation_map.py (คัดเฉพาะกระแสหลัก)
from pyvis.network import Network
LAYER_COLORS = { # จานสี Layer — ทำให้เป็นมาตรฐานด้วย atom 1 ตัว
"L0": "#2c3e50", # เมตา/ใช้ร่วม
"L1": "#2980b9", # ระบบ
"L2": "#27ae60", # เนื้อหา
"L3": "#f39c12", # อินสแตนซ์ข้อมูล
"L4": "#c0392b", # อนุพันธ์/แคช
}
def build_graph(fk_list, layer_map):
net = Network(directed=True, height="900px")
inbound = count_inbound(fk_list) # รวมจำนวน inbound edge
for sheet in all_sheets(fk_list):
layer = layer_map.get(sheet, "L0")
size = 10 + inbound[sheet] * 3 # ยิ่งเป็นฮับยิ่งโหนดใหญ่
net.add_node(sheet, color=LAYER_COLORS[layer],
size=size, title=sheet_tooltip(sheet))
for src, dst, col in fk_list:
# ตรวจจับการย้อนทิศของ Layer: ถ้า Layer ระดับสูงอ้างอิงระดับล่าง ให้ใช้สีเตือน
edge_color = "#e74c3c" if is_reverse(src, dst, layer_map) else "#888"
net.add_edge(src, dst, title=col, color=edge_color)
return net
is_reverse คือหัวใจเล็ก ๆ ของเครื่องมือนี้ ถ้าชีตต้นทางของเอดจ์อยู่ Layer ระดับสูงกว่าชีตปลายทาง (เช่น L1 → L3) ก็ถือว่าเป็นการย้อนทิศและลงสีเอดจ์เป็นสีแดง พอคนเปิดภาพขึ้นมาแล้วเห็นลูกศรสีแดง นั่นเกือบจะเป็นจุดที่ต้องแก้เสมอ
ขั้นสุดท้ายคือขั้นที่ pyvis พ่น HTML เชิงโต้ตอบออกมา เมื่อคลิกที่โหนด คอลัมน์·Layer·จำนวน inbound ของชีตนั้นจะแสดงเป็นทูลทิป และสามารถกรองด้วยชื่อชีตในช่องค้นหาได้ เหตุผลที่ต้องเป็น HTML ไม่ใช่ PNG แบบสแตติกอยู่ตรงนี้ — เมื่อจำนวนโหนดเกินหลายสิบ ในภาพแบบสแตติกลูกศรจะพันกันจนมองอะไรไม่เห็น ต้องลากด้วยเมาส์เพื่อคลี่ออก แล้วคลิกเจาะเฉพาะพื้นที่ที่สนใจ ถึงจะจับแพตเทิร์นได้
ถ้าแปลงโครงสร้างของแผนผังความสัมพันธ์ที่สร้างจากข้อมูลตัวอย่างข้างบนออกมาเป็น SVG ก็เป็นดังนี้ สีคือ Layer ลูกศรสีแดงหมายถึงตำแหน่งการย้อนทิศ (แม้ในตัวอย่างนี้จะไม่มีก็ตาม)
ดูจากขนาดของโหนดจะเห็นว่า RegionTable ถูกอ้างอิงมากที่สุด (ทั้ง Quest·NPC ชี้มาหมด) นี่คือฮับ ส่วน ItemEffectTable เป็นโหนดใบไม้จึงเล็ก คำตอบของคำถามที่นักออกแบบใหม่ถามว่า "ถ้าจะเข้าใจระบบนี้ ควรเริ่มดูจากตรงไหน" อยู่ในภาพอยู่แล้วตามลำดับขนาดของโหนด
ใน 3.1 ผมนิยามพิกัด Layer ไว้ พอแผนผังความสัมพันธ์ของบทนี้ยกพิกัดนั้นขึ้นมาสู่สายตา การวินิจฉัยสี่อย่างที่ข้อความหรือตารางทำไม่ได้ก็เป็นไปได้ในหน้าจอเดียว
แต่ก็ไม่ได้หมายความว่าภาพจะจับทุกปัญหา ภาพจับข้อบกพร่องเชิงโครงสร้าง ส่วนที่ว่า FK นี้เป็นความสัมพันธ์ที่ถูกต้องตามความหมายจริงไหม (เช่น npc_id เป็น "NPC ที่ให้เควสต์" จริง หรือเป็น "NPC ที่ปรากฏในเควสต์" กันแน่) นั้นภาพแก้ไม่ได้ นั่นเป็นหน้าที่การตัดสินเชิงโดเมนของคน เครื่องมือเพียงแค่ปูเวทีให้การตัดสินของคนได้ทำงานเท่านั้น
แผนผังความสัมพันธ์ไม่ใช่สร้างครั้งเดียวจบ ชีตถูกเพิ่ม·เปลี่ยนทุกสัปดาห์ แผนผังความสัมพันธ์ที่ฝากไว้กับการอัปเดตด้วยมือ ภายในหนึ่งถึงสองเดือนก็จะไม่ตรงกับโครงสร้างจริง และแผนที่ที่ไม่ตรงก็นำทางผิด จึงสู้ไม่มีเสียยังดีกว่า สมาชิกทีมที่เคยถูกภาพที่ผิดทำเอาเจ็บตัวมาแล้ว ครั้งต่อไปก็จะไม่ดูภาพอีก — นี่คือความล้มเหลวที่แพงที่สุด
ดังนั้นผมจึงผูกการอัปเดตไว้กับทริกเกอร์อัตโนมัติ
/relation-map ใช้ตอนอยากเปิดขึ้นมาแบบสด ๆ ระหว่างประชุมHTML ที่สร้างขึ้นจะถูกดีพลอยไปยังโฮสติงสแตติกภายในองค์กร (พอร์ทัลการออกแบบ) โดยอัตโนมัติ ทุกคนเห็นแผนที่เดียวกันได้เพียงแค่มีเบราว์เซอร์ โดยไม่ต้องติดตั้งเครื่องมือเพิ่ม เหมือนแผนที่ที่กางคลี่ไว้ข้างโต๊ะตลอดเวลา ไม่ว่าใครจะถาม ก็ตอบโดยชี้ไปยังภาพเดียวกันด้วยกัน
| ความผิดพลาด | เกิดจากอะไร | วิธีหลีกเลี่ยง |
|---|---|---|
| โหนดเกิน 100 ตัวจนภาพพันกัน | ยัดทุกสาขาเข้าหน้าจอเดียว | กรองตามโดเมน, มุมมองแบ่งตามกลุ่ม |
| สี Layer ต่างกันในแต่ละเครื่องมือ | นิยามจานสีใหม่ในแต่ละโค้ด | ทำให้จานสีเป็นมาตรฐานด้วย atom 1 ตัว (LAYER_COLORS) |
การตรวจ FK จับแค่ *_id ทำให้ตกหล่น·ผลบวกลวง |
พึ่ง regex บรรทัดเดียว | ระบุ FK ในเอกสารกำหนด + ตรวจค่าข้อมูลจริงควบคู่กัน |
| ภาพสร้างเสร็จแต่ไม่มีใครดู | ไม่ได้เชื่อมเข้ากับเวิร์กโฟลว์ | บังคับแนบภาพในคำขอเปลี่ยนแปลง·การประชุม |
| สร้างแล้วไม่อัปเดตจนผุ | พึ่งการอัปเดตด้วยมือ | ทริกเกอร์อัตโนมัติเป็นสิ่งจำเป็น ทำด้วยมือได้แค่เดือนเดียวก็ไร้ประโยชน์ |
ในการดำเนินงาน gen_relation_map.py สิ่งที่ผมเจ็บตัวบ่อยที่สุดคือบรรทัดที่สาม ถ้าเชื่อแค่กฎ *_id ก็จะพลาดการอ้างอิงที่ซ่อนอยู่อย่าง count หรือ value (AI ใน 3.3.2.3 ก็เตือนข้อจำกัดนี้ด้วยตัวเอง) และตรวจ Enum อย่าง grade ผิดเป็น FK ลูปการตรวจสอบที่ดูทั้งเอกสารกำหนดและข้อมูลจริงคือคำตอบของบรรทัดนี้
ถ้าพยายามจัดการชีตข้อมูลของทั้งบริษัทในคราวเดียว มันจะหนักและหมดแรงไปก่อนที่จะได้แสดงคุณค่าด้วยซ้ำ เริ่มเล็ก ๆ จากโฟลเดอร์เดียวในสาขาของคุณเองก่อน
setup.
1. เลือกโฟลเดอร์หนึ่งที่บรรจุชีตข้อมูล 5\~10 ชีตที่คุณรับผิดชอบ
2. ติดตั้ง dependency ด้วย pip install pyvis openpyxl (การอ่าน Excel ใช้สกิล excel-reader หรือ openpyxl)
3. ตรวจดูก่อนว่าในชีต $스키마 ของแต่ละชีตมีการระบุ FK ไว้หรือไม่ ถ้าไม่มี ก็เก็บแค่ส่วนหัวของคอลัมน์
prompt. รวบรวมรายการส่วนหัวของคอลัมน์แล้วส่งพรอมต์ของ 3.3.2.3 ไปตรง ๆ หัวใจคือบรรทัดสุดท้าย — "ถ้าไม่มั่นใจ จงทำเครื่องหมายว่าไม่แน่ใจ และอย่าเติมด้วยการเดา" ประโยคนี้กั้นลูกศรปลอมไว้
verify.
1. ดูตัวเลือก FK ที่ AI ส่งมาทีละบรรทัด ยืนยันบรรทัดที่ทำเครื่องหมาย "ไม่แน่ใจ" ด้วยเอกสารกำหนด/ข้อมูลจริง
2. คอลัมน์ที่สงสัยว่าเป็น Enum (อย่าง grade, effect_type ที่ไม่มี _id แต่ดูเหมือน FK) ให้ตัดออกจาก FK
3. ตรวจว่าการอ้างอิงตัวเอง (prev_*_id, parent_*_id) ถูกจับมาถูกต้องไหม
4. วาดกราฟด้วยรายการที่ผ่านการตรวจสอบ แล้วเปิดในเบราว์เซอร์เพื่อค้นหาลูกศรสีแดง (การย้อนทิศ) และเกาะร้าง (การโดดเดี่ยว) ด้วยตา
ถ้าไม่มีเวลาสร้างเครื่องมือ ในสัปดาห์แรกจะเริ่มจาก mermaid ที่วาดด้วยมือสักหนึ่งแผ่นก็ได้ เขียน FK ของ 5 ชีตลงใน mermaid โดยตรงด้วยรูปแบบของ 3.3.2.3 พอเอาแผ่นนี้ไปประชุมแล้วแสดงให้ดูว่า "นี่คือการพึ่งพากันของระบบเรา" คุณค่าก็พิสูจน์ได้ตรงนั้นเลย พอเห็นคุณค่าแล้ว เครื่องมืออัตโนมัติก็จะตามมาเองในลำดับถัดไป คุณวางภาระที่ว่าต้องมีเครื่องมือที่ทำงานได้ออกมาตั้งแต่แรกลงได้
การขยายจะไหลไปตามลำดับนี้อย่างเป็นธรรมชาติ — สัปดาห์ที่ 1 ภาพวาดมือ mermaid ของชีตตัวเอง → สัปดาห์ที่ 2 เพิ่มสี Layer และการคลิก → 1 เดือน อัปเดตอัตโนมัติ (git hook หรือแบตช์กลางคืน) → 3 เดือน ดีพลอยขึ้นพอร์ทัลภายในองค์กร → 6 เดือน แผนผังความสัมพันธ์รวมของชีตทั้งหมด
ใน 3.2 ผมจัดการกับด้านในของชีต (สคีมา) ใน 3.3 ก็จัดการด้านนอกของชีต (ความสัมพันธ์) ส่วน 3.4 จะวางแพตเทิร์นพรอมต์ที่ AI ช่วยเสริมไว้เหนือสิ่งเหล่านี้ บนระบบที่สคีมาและความสัมพันธ์ลงตัวแล้ว มันจะต่อด้วยแพตเทิร์นเชิงปฏิบัติว่า AI ช่วยเสริมการตรวจความสอดคล้องและการดึงขอบเขตผลกระทบได้อย่างไร
เป็นสัปดาห์ที่ใกล้ถึงกำหนดทำอัลฟาบิลด์มาก ผมเพิ่มคลาสใหม่หนึ่งบรรทัดลงในชีตสกิลแล้วบันทึก แต่บัฟ ID ที่คลาสนั้นอ้างอิงถึงกลับเป็นแถวที่มีคนลบทิ้งไปเมื่อวันก่อน ซึ่งผมเพิ่งรู้ตอนเช้าวันรุ่งขึ้นหลังจากบิลด์พังไปแล้ว การไล่ย้อนหาสาเหตุของบิลด์ที่พังไปนั้นใช้เวลาสองชั่วโมง เป็นสองชั่วโมงที่ผมไม่ต้องเสียไปเลยถ้าหากก่อนลบแถวนั้นมีใครถามได้ว่า "ลบอันนี้ได้ไหม"
บทนี้ว่าด้วยวิธีให้ AI ถามคำถามนั้นแทน หัวใจไม่ได้อยู่ที่เคล็ดลับการเขียนพรอมต์ให้ดี แต่อยู่ที่การตรึงคำถามเดิมไว้เป็นกฎ เพื่อไม่ต้องเขียนคำถามเดียวกันใหม่จากศูนย์ทุกครั้ง ในหัวข้อ 3.2 เราวาง schema ไว้แล้ว ในหัวข้อ 3.3 เราวางแผนภาพความสัมพันธ์ไว้แล้ว ทั้งสองคือโครงกระดูกของข้อมูล บทนี้จะแปลงตัวคำถามที่คนโยนให้ AI บนโครงกระดูกนั้นให้กลายเป็นสินทรัพย์ที่ตรึงตายตัว
ก่อนอื่นขอตอกหมุดไว้หนึ่งข้อ สิ่งที่ AI สร้างไม่ใช่คำตอบ แต่เป็นตัวเลือก ในทุกรูปแบบที่ปรากฏในบทนี้ มือที่ตัดสินใจขั้นสุดท้ายยังคงอยู่ฝั่งมนุษย์จนถึงที่สุด
เมื่อเริ่มใช้ AI ครั้งแรก เรามักพิมพ์เป็นภาษาธรรมชาติด้นสดทุกครั้ง ในทำนองนี้
ช่วยดูชีตสกิลให้หน่อย ตรวจดูว่ามี foreign key พังไหม
ถ้ามีอะไรแปลก ๆ ก็บอกด้วย อ้อ แล้วก็ที่ cooldown ติดลบด้วยนะ
พรอมต์นี้รั่วอยู่สองจุด
จุดแรก รายการที่ตรวจเปลี่ยนไปทุกครั้ง วันนี้นึกถึง "cooldown ติดลบ" ได้ แต่พรุ่งนี้ลืม "PK ซ้ำ (Primary Key, คีย์หลัก)" ที่เคยกรองออกไปเมื่อวานหายไปจากพรอมต์ของวันนี้ การตรวจที่พึ่งพาความจำของคนย่อมตกหล่นตามสภาพร่างกายของคนในวันนั้น
จุดที่สอง รูปแบบผลลัพธ์เปลี่ยนไปทุกครั้ง ถ้าเขียนเจตนาเดียวกันต่างกันไปว่า "ช่วยตรวจที" "ตรวจหน่อย" "กวาดสายตาดูหน่อย" AI ก็จะตอบเป็นตารางในบางวัน และตอบเป็นความเรียงในบางวัน เมื่อรูปแบบไม่สม่ำเสมอ เราก็เอาผลลัพธ์นั้นไปประมวลผลอัตโนมัติต่อไม่ได้
ทางแก้คือปล่อยพรอมต์ออกจากมือแล้วเก็บเข้าลิ้นชัก เปลี่ยนบันทึกที่เคยจดด้วยมือทุกครั้งให้เป็นการ์ดที่ติดป้ายกำกับ แล้วหยิบออกจากลิ้นชักเดียวกัน การ์ดใบนั้นคือคำสั่งสแลช (slash command หรือ skill) และ atom ที่หนังสือเล่มนี้พูดถึง
การตรึงเป็นกฎมีภาชนะอยู่สามแบบ การจะเก็บอะไรไว้ที่ไหนนั้นแบ่งกันด้วยความถี่ในการเรียกใช้และความเสถียร
งานที่ใช้บ่อยและนิยามนิ่งแล้วให้ทำเป็นคำสั่งสแลช งานที่ใช้บ่อยแต่เป็น "ข้อจำกัดที่ห้ามลืม" ให้ฉีดอัตโนมัติด้วย atom JIT งานที่ทำนาน ๆ ครั้งแต่ตัวใหญ่ให้ทำเป็นไฟล์เทมเพลต และงานที่นิยามยังคลอนแคลนอยู่ ก็ปล่อยให้ด้นสดไว้ก่อนโดยไม่ตรึงเป็นกฎ ไม่จำเป็นต้องมีครบทั้งสามตั้งแต่แรก เริ่มจากคำสั่งสแลชสักหนึ่งสองตัว แล้วค่อยเพิ่มเมื่อเห็นคุณค่า
แทนที่จะอธิบายด้วยคำพูด ผมจะพาไล่ตามรูปแบบหนึ่งตั้งแต่ต้นจนจบ มันคือรูปแบบที่ถามให้อัตโนมัติว่า "ลบอันนี้ได้ไหม" ก่อนจะลบแถวว่างสักแถว ชื่อของมันคือ /check-sheet ภายในมีรายการที่ตรวจและรูปแบบผลลัพธ์ตรึงเอาไว้
สินทรัพย์ที่ใช้เป็นฐานอยู่ในบันทึกการทำงานจากการวัดจริงที่ฝังไว้ทั่วทั้งเล่ม การป้อนข้อมูลเป็นไปตามหลัก schema-first (atom data_entry_schema_first) ลำดับการป้อนคือ ชีต $스키마 → Enum/*.proto (Export ด้วย VBA (ภาษามาโครของ Excel)) → csv และสิ่งที่เป็นต้นฉบับจริงไม่ใช่เอกสาร schema แต่เป็นผลลัพธ์ JSON ที่ออกมาจริง (atom json_over_schema_doc_as_source_of_truth) การตรวจความสอดคล้องก็คือการยกหลักสองข้อนี้มาเป็นกฎการตรวจตรง ๆ นั่นเอง
เมื่อกาง /check-sheet ออก ภายในมีเนื้อพรอมต์แบบนี้อยู่ นี่คือส่วนที่ไม่ต้องพิมพ์ด้วยมือทุกครั้ง
บทบาท: คุณคือตัวตรวจสอบความสอดคล้องของชีตข้อมูลเกม
ชีตที่จะตรวจ: {{sheet_name}}
schema ที่อ้างอิงได้: ชีต $스키마 (ชนิด·ช่วงค่า·เป้าหมาย FK ของแต่ละคอลัมน์)
ต้นฉบับจริงที่อ้างอิงได้: export JSON ของชีตเดียวกัน (สำคัญกว่าเอกสาร schema)
รายการที่ตรวจ (ตามลำดับนี้):
1. ความถูกต้องของ foreign key — ค่าในคอลัมน์ FK มีอยู่จริงในชีตเป้าหมายที่อ้างอิงหรือไม่
2. การละเมิดช่วงค่า — ค่าที่หลุดออกนอก min/max ที่นิยามไว้ใน $스키마
3. คอลัมน์ที่จำเป็นแต่ขาดหาย — แถวที่คอลัมน์ required=true ว่างเปล่า
4. PK ซ้ำ — PK เดียวกันมีตั้งแต่สองรายการขึ้นไป
รูปแบบผลลัพธ์ (ต้องเป็นตารางนี้เท่านั้น):
| # | แถว | คอลัมน์ | ชนิดการละเมิด | ค่าปัจจุบัน | ตัวเลือกการแก้ไข | ความมั่นใจ |
กฎ:
- ตัวเลือกการแก้ไขเป็นแค่ข้อเสนอ ห้ามแก้ชีตโดยตรงเด็ดขาด
- ความมั่นใจให้เลือกหนึ่งในสาม สูง/กลาง/ต่ำ ถ้าเป็นการเดาให้ระบุต่ำ
- ถ้าไม่มีการละเมิด ให้แสดงผลแค่ "ละเมิด 0 รายการ" แล้วจบ
ในเนื้อพรอมต์นี้ สิ่งที่สำคัญคือกฎสามบรรทัดสุดท้าย "ห้ามแก้โดยตรง" "ให้คะแนนความมั่นใจ" "ถ้าเป็นการเดาให้สารภาพว่าต่ำ" สามบรรทัดนี้มัด AI ไว้ที่ตำแหน่งของการเสนอตัวเลือก ไม่ใช่ตำแหน่งของการให้คำตอบ
/check-sheet skill_table
จบแค่นั้น ส่งแค่ชื่อชีตชื่อเดียว ไม่ต้องเขียนทั้งรายการตรวจสี่ข้อและรูปแบบตารางผลลัพธ์ใหม่ นั่นแหละคือคุณค่าของการตรึงเป็นกฎ
ด้านล่างคือผลลัพธ์ที่ออกมาตอนรันจริง ผมไม่สรุปย่อ แต่นำมาลงไว้ทั้งหมดรวมถึงส่วนที่กำกวมและจุดที่ AI ลังเล
นี่คือผลการตรวจความสอดคล้องของ skill_table พบการละเมิดทั้งหมด 4 รายการ
| # | แถว | คอลัมน์ | ชนิดการละเมิด | ค่าปัจจุบัน | ตัวเลือกการแก้ไข | ความมั่นใจ |
|---|-----|------------|------------|--------|----------------------|------|
| 1 | 142 | buff_id | ละเมิด FK | 9007 | 9070 (น่าจะพิมพ์ผิด 1 ตัว) | กลาง |
| 2 | 142 | buff_id | ละเมิด FK | 9007 | 9012 (ชื่อ "맹독" คล้ายกัน) | ต่ำ |
| 3 | 88 | cooldown | ละเมิดช่วงค่า | -1 | 0 (ติดลบไม่ได้) | สูง |
| 4 | 215 | combo_next | จำเป็นแต่ขาด | (ว่าง) | ขอพักการตัดสิน | ต่ำ |
ขอเสริมข้อสังเกตบางประการ
- รายการ 1 และ 2 เป็นตัวเลือกที่ต่างกันสำหรับคอลัมน์เดียวกันของแถวเดียวกัน (142)
ID ที่ชื่อ 9007 ไม่มีอยู่ในชีต buff ส่วน 9070 ต่างกันแค่ตัวเลขหลักเดียว
จึงมีความเป็นไปได้ว่าพิมพ์ผิด และ 9012 มีชื่อ "맹독" คล้ายกัน จึงนำมาเป็น
ตัวเลือก ทั้งคู่มีความมั่นใจต่ำ คนต้องเป็นผู้เลือกเอง
- รายการ 4 (แถว 215 คอลัมน์ combo_next ว่าง) นั้นกำกวม ใน $스키마 ตั้งไว้เป็น
required=true แต่เมื่อดู export JSON พบว่าคอลัมน์นี้มีแถวที่ว่างอีก 12 แถว
อาจเป็นไปได้ว่าเอกสารกำหนดสเปกไม่ใช่ฉบับล่าสุด หรือมีกฎว่า "สกิลปิดท้าย
คอมโบจะเว้น combo_next ว่างไว้" แต่ยังไม่ได้สะท้อนลงในเอกสารกำหนดสเปก
ผมไม่สามารถฟันธงได้ จึงพักไว้เป็น "ขอพักการตัดสิน"
- ถ้าต้องการรันการตรวจช่วงค่าเพิ่มนอกจาก cooldown ต้องตรวจ min/max ของ
คอลัมน์อื่นใน $스키마 แต่ในข้อมูลที่ผมได้รับ บางส่วนของคอลัมน์ range_check
ว่างเปล่าอยู่ ผมจึงไม่สามารถตรวจคอลัมน์ cast_time ได้
ที่ผลลัพธ์นี้เป็นผลลัพธ์ที่ดี ไม่ใช่เพราะมันเรียบร้อย แต่เพราะมันตรงไปตรงมา มันไม่ได้ฟันธงรายการ 4 ว่า "จำเป็นก็ต้องเติม" แต่แสดงความสงสัยตัวเองว่า "เอกสารกำหนดสเปกอาจไม่ใช่ฉบับล่าสุด" และยังสารภาพข้อจำกัดว่ามองคอลัมน์ cast_time ไม่ได้ ถ้า AI มั่นอกมั่นใจบอกว่ารายการ 4 ให้ "เติม 0" นั่นอาจเป็นคำตอบผิดที่ทำให้สกิลปิดท้ายคอมโบพังได้
ตอนนี้คนตัดสินทีละบรรทัด การที่การตรวจสอบมาพร้อมกันโดยอัตโนมัติ หมายความว่าตารางนี้ออกมาเองโดยอัตโนมัติ ไม่ได้หมายความว่าคนไม่ดู
รายการ 4 ตัวสุดท้ายคือหัวใจ ในจุดที่ถ้าเชื่อผลลัพธ์ของ AI ไปดื้อ ๆ ก็จะทำให้ข้อมูลที่ดี ๆ พังเสียหาย การยับยั้งของมนุษย์ได้ทำงาน ในขณะเดียวกันรายการ 4 ก็เผยให้เห็นความบกพร่องของกฎการตรวจเองด้วย นั่นคือ required=true ในเอกสารกำหนดสเปกนั้นผิด
เราไม่หยุดอยู่แค่นี้ การยับยั้งคืออินพุตที่ทำให้การตรวจครั้งถัดไปฉลาดขึ้น คนพิมพ์ต่อ
รายการ 4 ไม่ใช่การละเมิด เพราะสกิลที่ combo_finisher=true จะเว้น combo_next
ว่างไว้เป็นเรื่องปกติ ช่วยเพิ่มกฎข้อยกเว้นนี้ลงในรายการที่ตรวจด้วย ต่อจากนี้
อย่าจับว่าแถว finisher ที่ combo_next ว่างเป็นการละเมิด และรายการที่ดูเหมือน
นิยาม required ในเอกสารกำหนดสเปกผิด ให้แยกออกมาเป็นหัวข้อ "สงสัยเอกสารสเปก" แล้วแจ้งต่างหาก
เข้าใจแล้ว ผมจะสะท้อนข้อยกเว้นนี้ลงในกฎการตรวจของ /check-sheet
กฎที่เพิ่ม:
- ตอนตรวจ combo_next ว่าจำเป็นหรือไม่ ให้ตรวจค่า combo_finisher ของแถวเดียวกันก่อน
ถ้า combo_finisher=true ให้ถือว่า combo_next ว่างเป็นเรื่องปกติ (ยกเว้นจากการละเมิด)
หัวข้อที่ตั้งใหม่:
- "สงสัยเอกสารสเปก" — เมื่อการกระจายตัวจริงใน export JSON ขัดกับนิยามใน $스키마
ให้แยกออกมาเป็นตัวเลือกที่ต้องแก้เอกสารกำหนดสเปก ไม่ใช่แก้ข้อมูล แล้วรายงาน
(ตามหลักที่ว่าต้นฉบับจริงคือ JSON หากข้อมูลส่วนใหญ่ขัดกับเอกสารกำหนดสเปก
ก็ให้สงสัยฝั่งเอกสารกำหนดสเปก)
ต้องการให้บันทึกการเปลี่ยนแปลงนี้ลงใน atom นิยามคำสั่ง /check-sheet หรือไม่
บรรทัดสุดท้ายคือความสมบูรณ์ของรูปแบบนี้ การยับยั้งครั้งหนึ่งไม่ได้จบลงที่การแก้ครั้งเดียว แต่ถูกบันทึกลงใน atom นิยามคำสั่ง แล้วแพร่ต่อไปยังคนถัดไปและการตรวจครั้งถัดไป นี่คือดอกเบี้ยทบต้นของการตรึงเป็นกฎ ความผิดพลาดเดียวกันจะไม่ต้องกรองซ้ำสองครั้ง
รูปแบบที่สองคือการกำจัดสองชั่วโมงในฉากเปิดเรื่องนั้น ก่อนจะเปลี่ยนแถวหนึ่ง หรือการตัดสินใจหนึ่ง ให้ถามก่อนว่า "มันจะลามไปถึงไหน" ชื่อของมันคือ /impact แผนภาพความสัมพันธ์ที่สร้างไว้ในหัวข้อ 3.3 (HTML ความสัมพันธ์ FK ที่ gen_relation_map.py ดึงออกมา) คือเชื้อเพลิงของรูปแบบนี้
ภายใน /impact มีการตรึง "ต้องไปค้นที่ไหนถึงจะหาผลกระทบได้ครบไม่ตกหล่น" ไว้สี่ทาง
บทบาท: คุณคือตัวดึงขอบเขตผลกระทบจากการเปลี่ยนแปลง
เป้าหมายที่เปลี่ยน: {{target}} (อย่างใดอย่างหนึ่งจาก ชื่อชีต·คอลัมน์·atom_id·การตัดสินใจเชิงระบบ)
เส้นทางการค้นหา (ต้องเดินครบทั้งสี่):
1. inbound ของแผนภาพความสัมพันธ์ — ในผลลัพธ์ของ gen_relation_map.py ชีตที่อ้างอิง
{{target}} เป็น FK (ลูกศรที่ชี้มายังชีตนี้)
2. การกล่าวถึงในเนื้อหา — เอกสารใน sources/manuscript ที่ ID·ชื่อของ {{target}}
ปรากฏโดยตรง
3. ความสัมพันธ์ affects ของ atom — atom ที่เชื่อมกับ {{target}} ด้วย affects
4. การอ้างอิงย้อนกลับ — โน้ตที่ชี้ไปยัง {{target}} ด้วย [[wikilink]]
รูปแบบผลลัพธ์:
| ชนิด | ชื่อ | หลักฐานการเชื่อมโยง | ความแรงของผลกระทบ | ผู้รับผิดชอบ |
กฎ:
- ความแรงของผลกระทบมีสามระดับ ทางตรง/ทางอ้อม/อ่อน FK ตรงคือ 'ทางตรง' การกล่าวถึงในเนื้อหาคือ 'ทางอ้อม' การคาดเดาคือ 'อ่อน'
- ถ้ากลัวตกหล่น ให้ใส่ไว้เป็นอ่อนก็ยังดี ดีกว่าทำหล่น
- ไม่จัดลำดับความสำคัญและไม่สั่งงาน ทำแค่รายการเท่านั้น
/impact buff_id=9012
ถามด้วยบรรทัดเดียวว่า "กำลังจะเปลี่ยนบัฟ 맹독 (9012) เป็นเอฟเฟกต์อื่น ตรงไหนจะกระเทือนบ้าง"
นี่คือขอบเขตผลกระทบจากการเปลี่ยน buff_id=9012 (맹독) ผมค้นหาครบทั้ง 4 เส้นทางแล้ว
| ชนิด | ชื่อ | หลักฐานการเชื่อมโยง | ความแรง | ผู้รับผิดชอบ |
|--------|--------------------|----------------------|---------|----------|
| ชีต | skill_table | อ้างอิง buff_id FK ตรง | ทางตรง | teammate_a |
| ชีต | monster_drop | อ้างอิง reward_buff FK | ทางตรง | teammate_b |
| ชีต | set_item_effect | อ้างอิง proc_buff FK | ทางตรง | teammate_a |
| เอกสาร | 독계열_밸런스_노트.md | กล่าวถึง "9012" ในเนื้อหาโดยตรง | ทางอ้อม | (ผู้เขียน) |
| atom | poison_stack_rule | affects: 9012 | ทางอ้อม | (ผู้เขียน) |
| โน้ต | 상태이상_설계.md | อ้างอิงย้อน [[맹독]] | อ่อน | (ผู้เขียน) |
ขอเสริมบางประการ
- แถวใน skill_table ที่อ้างอิง 9012 มี 3 แถว (สกิลคลาสนักลอบสังหาร)
ถ้าเปลี่ยน เอฟเฟกต์จริงของสกิล 3 ตัวนี้จะเปลี่ยนไป เป็นผลกระทบที่แรงที่สุด
- reward_buff ของ monster_drop ผมมั่นใจค่อนข้างอ่อน เพราะชื่อคอลัมน์คือ
reward_buff แต่บางแถวดูเหมือนมีค่าที่ชี้ไปยัง item ไม่ใช่ buff ปนอยู่
ผมแนะนำให้เปิดชีตนี้ตรวจสอบด้วยตัวเองสักครั้ง
- "독계열_밸런스_노트.md" แค่กล่าวถึง 9012 เป็นข้อความ ไม่ใช่ FK ผมจึงจัดไว้
เป็นทางอ้อม แต่เนื่องจากเป็นเอกสารที่เขียนเจตนาด้านบาลานซ์ไว้ ผลกระทบจริง
อาจแรงก็ได้ โปรดระวังว่าความแรง (ทางอ้อม) กับความสำคัญอาจไม่ตรงกัน
- set_item_effect ใช้ 9012 เป็น proc_buff ในไอเทมเซต 4 ชนิด ถ้าเอา 맹독 ออก
คอนเซปต์ของเซตเหล่านี้จะเปลี่ยนไป
ตรงนี้ความตรงไปตรงมาของ AI ก็ช่วยการตรวจสอบอีกครั้ง ถ้าฟันธงว่า monster_drop เป็นทางตรง ก็จะปั่นผลกระทบให้พองเกินจริง จนต้องนัดประชุมประสานงานที่ไม่จำเป็น เพราะ AI โยนลูกกลับมาฝั่งคนว่า "แนะนำให้ตรวจสอบ" จึงได้แก้ความแรงให้ถูกต้อง
เมื่อรายการผ่านการตรวจสอบแล้ว มือถัดไปคือการส่งต่อสิ่งนี้ให้คน
ช่วยจัดกลุ่มตามผู้รับผิดชอบที่ได้รับผลกระทบโดยตรง แล้วร่างเมโมแบ่งปันสั้น ๆ
สำหรับส่งให้ teammate_a / teammate_b ในรูปแบบ "กำลังพิจารณาเปลี่ยน 9012 맹독
เป็น X ชีตของคุณได้รับผลกระทบ N แถว" การตัดสินใจยังไม่เคาะ ดังนั้นให้เป็นโทน
ขอความเห็น ไม่ใช่การแจ้งให้ทราบ
นี่คือร่างเมโมแบ่งปันแยกตามผู้รับผิดชอบ ผมจับโทนเป็นการขอความเห็น ไม่ใช่การแจ้งให้ทราบ
[ถึง teammate_a]
กำลังพิจารณาแนวทางเปลี่ยนบัฟ 맹독 (9012) เป็นเอฟเฟกต์อื่น ยังไม่เคาะ
อยากฟังความเห็นก่อน ขอบเขตผลกระทบ:
- skill_table: แถวสกิลนักลอบสังหาร 3 แถวอ้างอิง 9012 (ผลกระทบทางตรง)
- set_item_effect: ไอเทมเซต 4 ชนิดใช้ 9012 เป็น proc_buff
ถ้าเปลี่ยน คอนเซปต์ของสกิล·เซตเหล่านี้จะเปลี่ยนไป ถ้ามีความเห็นค้านหรือเสริม
ช่วยแจ้งภายในสัปดาห์นี้
[ถึง teammate_b]
ขอรบกวนตรวจสอบเรื่อง reward_buff ของ monster_drop กำลังพิจารณาเปลี่ยนบัฟ
맹독 (9012) อยู่ และดูเหมือนแถวที่ได้รับผลกระทบมี 1 แถว (เนื่องจากในคอลัมน์
reward_buff มี item ID ปนอยู่ ตอนแรกจึงดูเหมือนมีมากกว่านั้น) ช่วยตรวจสอบให้
หน่อยได้ไหมว่าตรง 1 แถวนี้ถูกต้องจริงหรือไม่
เมโมทั้งสองผมจัดเป็น "กำลังพิจารณา·ขอความเห็น" และไม่ได้ใส่ถ้อยคำแจ้งแบบฟันธง ก่อนส่งคุณต้องเติมเอฟเฟกต์ตัวเลือกที่จะเปลี่ยน (ตำแหน่ง X) ด้วย
สองชั่วโมงในฉากเปิดเรื่องหายไปตรงนี้ ถามผลกระทบก่อนเปลี่ยน แบ่งปันให้ผู้รับผิดชอบล่วงหน้า และประสานงานจบก่อนที่บิลด์จะพังในวันรุ่งขึ้น และสิ่งที่ AI สร้างก็เป็นแค่ร่างเมโมจนถึงที่สุด นิ้วที่กดส่งเป็นของคน
ทรานสคริปต์สองชุดข้างต้นเดินตามโครงกระดูกเดียวกัน รูปแบบอื่นทั้งหมดก็วางอยู่บนโครงกระดูกนี้
flowchart LR
A[คำสั่งที่ตรึงไว้
รายการตรวจ·รูปแบบผลลัพธ์·ข้อห้าม] --> B[คน: เรียกหนึ่งบรรทัด
ส่งแค่เป้าหมาย]
B --> C[Claude: ผลลัพธ์ดิบ
ตัวเลือก+ความมั่นใจ+สงสัยตัวเอง]
C --> D{คนตรวจสอบ}
D -->|รับ| E[นำไปใช้]
D -->|แก้ไข| F[ปรับความแรง·ค่า]
D -->|ยับยั้ง| G[พบความบกพร่องของกฎ]
G --> H[อัปเดต atom นิยามคำสั่ง]
H -.สะท้อนในการเรียกครั้งถัดไป.-> A
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class A,H data
class B,D,F human
class C ai
class E pass
class G fail
หัวใจคือเส้นประที่มุมขวาล่าง การยับยั้งไม่ใช่จุดจบ แต่ย้อนกลับมาเป็นอินพุตที่แก้ไขตัวคำสั่งเอง รายการ 4 ที่ถูกยับยั้งในการตรวจความสอดคล้องได้กลายเป็นกฎข้อยกเว้น finisher และกฎนั้นถูกบันทึกลงใน atom แล้วแพร่ต่อไปยังการตรวจครั้งถัดไป ถ้าไม่มีการป้อนกลับนี้ ก็ต้องกรองคำตอบผิดเดิมซ้ำใหม่ทุกสัปดาห์
มือของคนเหลืออยู่สามจุด การเรียก (เลือกเป้าหมาย) การตรวจสอบ (รับ·แก้ไข·ยับยั้ง) และการปรับปรุงกฎ (การป้อนกลับของการยับยั้ง) AI ทำแค่งานเสนอตัวเลือกอยู่ระหว่างนั้นเท่านั้น
เมื่อย้ายโครงกระดูกเดียวกันไปทำงานอื่น รูปแบบก็เพิ่มขึ้น ผมจะชี้แค่ตำแหน่งโดยไม่ลงทรานสคริปต์ ทั้งหมดเดินตามโครงกระดูกของ 3.4.5 ตรง ๆ ดังนั้นตอนสร้าง หัวใจคือการไม่ลืม "รายการตรวจที่ตรึงไว้" และ "จุดที่คนตรวจสอบ"
| รูปแบบ | เรียกหนึ่งบรรทัด | ตัวเลือกที่ AI เสนอ | การตัดสินใจที่คนถือ |
|---|---|---|---|
| สังเคราะห์ร่าง GDD | /gdd-new <ระบบ> |
ร่างมาตรฐาน 9 หัวข้อ, ที่ยังไม่กำหนดใส่ [TBD] | วิสัยทัศน์·ลำดับความสำคัญ·การตัดทิ้ง |
| แปลง state machine/BT | /diagram-state |
ภาษาธรรมชาติ → mermaid + ตรวจการเข้าถึงได้ | นิยามสถานะ·เงื่อนไขการเปลี่ยนผ่าน |
| ตรวจการชนกันของอินเทอร์เฟซ | /check-interface <GDD> |
เคสการชนของอินพุต/เอาต์พุต·หน้าต่างเวลา | กฎลำดับความสำคัญ |
| คำนวณบาลานซ์ | /balance-calc <ชีต> <atom สูตร> |
ค่าที่คำนวณตามเส้นโค้ง + diff กับของเดิม | สูตร·เจตนาของเกม |
| จัดประเภทงานในการทบทวน | /retro-classify <ช่วงเวลา> |
การกระจาย Layer×สาขา + สัญญาณผิดปกติ | การปรับแก้ประเภท·การตีความ |
ขอตอกหมุดไว้เพียงข้อเดียวเรื่องการคำนวณบาลานซ์ ต่อให้เส้นโค้งลดลงอย่างราบรื่นในเชิงตัวเลข ความราบรื่นนั้นจะตรงกับเจตนาของเกมหรือไม่เป็นอีกเรื่อง บางครั้งเราตั้งใจให้ช่วงก่อนถึงบอสชันขึ้นมาก แต่ AI กลับเกลาให้แบนราบโดยอ้างว่าเป็น "ค่าผิดปกติ" ด้วยเหตุนี้ ต่อให้การคำนวณบาลานซ์มีการตรวจเส้นโค้งติดมาอัตโนมัติ บรรทัดสุดท้ายก็จะปิดได้ต่อเมื่อคนเทียบกับเจตนาแล้วเท่านั้น
เมื่อรูปแบบเพิ่มขึ้น ก็ต้องมีวินัยในการดำเนินงาน ห้าหลักด้านล่างไม่ใช่กฎที่ต้องท่องจำ แต่เป็นหลักการออกแบบที่ใส่ไว้ในตัวเครื่องมือเอง
| หลักการ | ทำไม |
|---|---|
| หนึ่งคำสั่ง = หนึ่งงาน | ยิ่งเล็กยิ่งนำกลับมาใช้·ดีบักได้ง่าย ไม่ยัดทั้งตรวจ·แก้·แบ่งปันลงใน /check ทั้งหมด |
| ให้คำสั่งมีการตรวจสอบติดมาอัตโนมัติ | ใส่ช่องความมั่นใจ·หลักฐานในตัวตารางผลลัพธ์เพื่อลดภาระการตรวจสอบของคน |
| ทำนิยามคำสั่งเป็น atom | เหมือนการป้อนกลับจากการยับยั้ง→กฎใน 3.4.3 ให้บันทึก ทำไม·ตัวอย่าง·ประวัติการเปลี่ยนแปลงไว้ใน atom |
| วัดความถี่การใช้ | คำสั่งที่ใช้น้อยกว่าเดือนละครั้งเป็นตัวเลือกที่จะเลิก ตัดด้วยข้อมูล |
| มือของคนอยู่ที่การตัดสินใจเท่านั้น | คำสั่งทำได้ถึงแค่การสร้างตัวเลือก ห้ามตัดสินใจอัตโนมัติ |
สุดท้าย จุดบรรจบหนึ่ง ในโปรเจกต์ MMORPG หนึ่งที่ผู้เขียนเคยดำเนินงาน คำสั่งสแลชที่อยู่อย่างเสถียรในการออกแบบระบบบรรจบลงที่ราว 12 ตัวเมื่อเวลาผ่านไป นี่ไม่ใช่มาตรฐานสาธารณะ แต่เป็นค่าจากการสังเกตของโปรเจกต์หนึ่ง (ประสบการณ์ของผู้เขียน ยังไม่ได้ตรวจสอบ) แต่ทิศทางนั้นชัดเจน คำสั่งไม่ใช่สิ่งที่จะเพิ่มไปเรื่อย ๆ ไม่มีที่สิ้นสุด แต่จะหยุดที่จำนวนที่จดจำในหัวไหว โดยเพิ่มและลดเดือนละ 1–2 ตัว ลิ้นชักที่ติดป้าย 100 ใบก็เหมือนกับลิ้นชักที่ไม่มีป้าย
การนำมาใช้ไม่ทำทีเดียวจนครบ เดือนแรกแค่ตรึงงานหนึ่งอย่างที่ทำซ้ำทุกสัปดาห์ให้เป็นคำสั่งสแลชก็พอ เมื่อหนึ่งอย่างนั้นแสดงคุณค่าออกมา เดือนถัดไปก็จะลามไปเป็นสอง สามอย่างเองตามธรรมชาติ
ในหัวข้อ 3.1 เราวางพิกัด Layer ของการออกแบบระบบ ในหัวข้อ 3.2 วาง schema ในหัวข้อ 3.3 วางแผนภาพความสัมพันธ์ และในหัวข้อ 3.4 วางพรอมต์ที่ให้ AI ช่วยไว้บนนั้น หนึ่งสัปดาห์ของนักออกแบบระบบที่ผ่านสี่บทนี้จะเปลี่ยนไปแบบนี้
flowchart LR
M[จันทร์: สังเคราะห์ร่าง GDD
4h → 1h] --> T[อังคาร: ตรวจความสอดคล้อง
ครึ่งวัน → 5 นาที]
T --> W[พุธ: ออนบอร์ดดิ้ง, แผนภาพความสัมพันธ์หนึ่งใบ
ประชุม 5 ครั้ง → 1 ใบ]
W --> Th[พฤหัส: ดึงขอบเขตผลกระทบ
ประชุม 1h → 10 นาที]
Th --> F[ศุกร์: จัดประเภทการทบทวนอัตโนมัติ
ทำมือ → อิงข้อมูล]
F --> R[นำเวลาที่ลดลง
กลับไปลงทุนในการออกแบบ·ตรวจทาน·ประสบการณ์ผู้เล่น]
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class M,T,W,Th,F ai
class R pass
เวลาทำงานจุกจิกลดลง และเวลานั้นก็ย้อนกลับไปเป็นการครุ่นคิดเรื่องการออกแบบเชิงลึกและประสบการณ์ผู้เล่น การไม่เอาเวลาที่ลดลงไปเติมงานจุกจิกอีก นั่นแหละคือเหตุผลที่แท้จริงของการนำเครื่องมือเข้ามา
Part 4 ถัดไปคือการออกแบบการต่อสู้ ในฐานะพี่น้องที่ใกล้ชิดที่สุดของการออกแบบระบบ เครื่องมือและรูปแบบของ 3.1\~3.4 จะข้ามไปใช้ได้ตรง ๆ
setup. เลือกงานตรวจหนึ่งอย่างที่คุณทำซ้ำทุกสัปดาห์ (เช่น ตรวจความสอดคล้องของชีต) เขียนรายการที่ตรวจ 4 ข้อและรูปแบบตารางผลลัพธ์ของงานนั้น แล้วตรึงเป็นคำสั่งสแลชหนึ่งตัว อย่าลืมใส่กฎสามบรรทัด ("ห้ามแก้โดยตรง / ให้คะแนนความมั่นใจ / การเดาให้สารภาพ") ลงในเนื้อคำสั่งให้ครบ
prompt. เรียกโดยส่งแค่เป้าหมายหนึ่งบรรทัด
/check-sheet skill_table
verify. ตัดสินตารางที่ย้อนกลับมาทีละแถวว่ารับ·แก้ไข·ยับยั้ง ถ้ามีการยับยั้งออกมา นั่นไม่ใช่เรื่องโชค แต่เป็นความบกพร่องของกฎ ส่งบรรทัดที่เพิ่มข้อยกเว้นนั้นลงในนิยามคำสั่งกลับไปอีกครั้ง เพื่อให้การเรียกครั้งถัดไปไม่ต้องกรองความผิดพลาดเดียวกันซ้ำสองครั้ง
ถ้าไม่มีทั้งทีมและระบบ atom แทนที่จะใช้คำสั่งสแลช ให้วางบล็อกข้อความหนึ่งบล็อกไว้ในแอปจดบันทึก ตั้งชื่อว่า "พรอมต์ตรวจชีต" เนื้อหาคือรายการที่ตรวจ 4 ข้อ + กฎ 3 บรรทัดจาก setup ข้างต้น ทุกครั้งที่จะตรวจ ก็คัดลอกบล็อกนี้แล้วเปลี่ยนแค่ชื่อชีตก่อนวางลงให้ AI เมื่อมีเรื่องที่ต้องยับยั้ง ก็เพิ่มข้อยกเว้นหนึ่งบรรทัดลงในบล็อกบันทึกนั้นด้วยตัวเอง ไม่ว่าเครื่องมือจะเป็นคำสั่งสแลชหรือบันทึกหนึ่งหน้า วงจร (ตรึงเป็นกฎ → เรียก → ตรวจสอบ·ยับยั้ง → อัปเดตกฎ) ก็หมุนเหมือนกัน
เป้าหมายการเรียนรู้ของบทนี้ (ระดับความยาก 🟡 ภาคปฏิบัติ · ความรู้พื้นฐานที่ต้องมี: การบวกลบคูณหาร·การคำนวณในตาราง): สามารถแยกย่อยคำคุณศัพท์เชิงนามธรรมอย่าง "สัมผัสของการตี" ออกมาเป็นสัญญาณที่วัดได้ และระบุเป็นพิกัดได้ว่าผลงานทั้งห้าอย่างของนักออกแบบการต่อสู้นั่งอยู่ในช่องไหนของ Layer
ห้องประชุมบิลด์ โปรแกรมเมอร์เปิดสกิลใหม่ที่เพิ่งต่อเข้าไปขึ้นบนมอนิเตอร์ ตัวละครเหวี่ยงดาบ ศัตรูกระเด็นถอยหลัง มีห้าคนนั่งดูอยู่ ใครคนหนึ่งเอ่ยขึ้น
"อืม… ทำไมสัมผัสของการตีมันอ่อนไปนิดนะ"
คนข้าง ๆ พยักหน้า "ใช่ครับ จืด ๆ ไปหน่อย"
โปรแกรมเมอร์ถาม "ต้องแก้ตรงไหนอย่างไรครับ"
ความเงียบ ในห้าคนที่อยู่ในห้องประชุมนี้ ไม่มีใครตอบคำถามนั้นเป็นตัวเลขได้ "สัมผัสของการตีมันอ่อน" เป็นสิ่งที่ทั้งห้าคนรู้สึกได้ แต่คนที่พูดได้ว่า "ปรับ hit stop จาก 3 เฟรมเป็น 5 เฟรม" กลับไม่มีสักคน การประชุมดำเนินไป 40 นาที โดยโยนคำคุณศัพท์อย่าง "ให้หนักแน่นกว่านี้อีกหน่อย" "อิมแพกต์ยังไม่พอ" ไปมา แล้วก็จบลงด้วย "เอาเป็นว่าไว้ดูกันใหม่ในบิลด์หน้านะ"
ฉากนี้บีบอัดปัญหาทั้งหมดของงานออกแบบการต่อสู้ไว้ มันเป็นแขนงที่ผู้เล่นสัมผัสได้โดยตรงที่สุด แต่พอจะถ่ายทอดสัมผัสนั้นออกมาเป็นคำพูด ก็เหลือแค่คำคุณศัพท์ คำคุณศัพท์วัดไม่ได้ และเมื่อวัดไม่ได้ ก็ปรับไม่ได้ งานแรกของนักออกแบบการต่อสู้คือการดึงคำคุณศัพท์เหล่านี้ลงมาเป็นตัวเลข
บทนี้กำหนดว่าตัวเลขนั้นเข้าไปอยู่ในช่องไหน ผลงานทั้งห้าอย่างที่นักออกแบบการต่อสู้สร้างขึ้น แต่ละอย่างนั่งอยู่ที่ตรงไหนของ Layer และทำไมพิกัดนั้นจึงกลายเป็นเงื่อนไขเบื้องต้นของการทำงานอัตโนมัติ เครื่องมือภาคปฏิบัติของ 4.2·4.3·4.4 ล้วนขับเคลื่อนอยู่บนพิกัดนี้
สรุปหนึ่งบรรทัดสำหรับผู้ที่ไม่ได้อยู่ในสายงานนี้ ในส่วนนี้คุณไม่จำเป็นต้องท่องค่าตัวเลขการต่อสู้หรือหน่วยเฟรม สิ่งเดียวที่คุณควรนำติดตัวไปคือสิ่งนี้ — "คำขอที่โยนกันไปมาด้วยคำคุณศัพท์ ทั้งวัดและปรับไม่ได้" แนวคิดที่ว่าทันทีที่ดึง "ให้หนักแน่นกว่านี้อีกหน่อย" ลงมาเป็น "ปรับอะไรให้เป็นเท่าไร" การทำงานร่วมกันก็เดินหน้าไปได้ ใช้ได้เหมือนกันกับฟีดแบ็กคลุมเครือของงานสายไหนก็ตามนอกวงการเกม ผลงานทั้งห้าอย่างใน 4.1.1 คุณอ่านผ่าน ๆ ได้ แล้วกุมเอาเพียงสิ่งเดียวนี้ไว้ในมือก็เพียงพอ
หากรวบผลงานที่นักออกแบบการต่อสู้รับผิดชอบไว้ในบรรทัดเดียว ก็คือ "กระบวนการทั้งหมดที่อินพุตของผู้เล่นถูกแปลงเป็นแอ็กชันบนหน้าจอ" เราจะแยกสิ่งนี้ออกเป็นห้าก้อน
ก้อนที่หนึ่ง สเปก Look & Feel ของการต่อสู้ เป็นเอกสารที่แปลความเชิงนามธรรมอย่างสัมผัสของการตี·การตอบสนอง·น้ำหนัก ให้เป็นค่าตัวเลขที่วัดได้ นี่เป็นผลงานที่ยากที่สุดของแขนงนี้ และเป็นเกณฑ์ในการประเมินอีกสี่อย่างที่เหลือ
Look & Feel ยังแยกย่อยออกได้อีกเป็นสี่สัญญาณ
ถ้าไม่มีสเปกนี้ ฉากในห้องประชุมก็จะวนซ้ำ ถ้ามีสเปก ก็จะมีคำสั่งปรับออกมาว่า "Hit Stop 3→5 เฟรม แอมพลิจูด Camera Shake +20%"
ก้อนที่สอง ระบบสกิล·Combo·Cancel เป็นกฎที่อินพุตถูกแปลงเป็นแอ็กชัน
ก้อนที่สาม AI ของตัวละคร·มอนสเตอร์ ลอจิกพฤติกรรมของ NPC — Behavior Tree (ต่อไปเรียก BT) สเตตแมชชีน (FSM (Finite State Machine, เครื่องสถานะจำกัด)/HFSM) ตารางการตัดสินใจ (decision table) แพตเทิร์นพฤติกรรมมอนสเตอร์ การเปลี่ยนเฟสบอส การทำงานร่วมกันของ NPC เพื่อน การจำลองฝูง (flocking) ล้วนอยู่ในก้อนนี้
ก้อนที่สี่ สูตรดาเมจ·ทรัพยากร·Cooldown เป็นคณิตศาสตร์ที่ตัวเลือกของผู้เล่นถูกแปลงเป็นผลลัพธ์ ค่าสัมประสิทธิ์ดาเมจ·การลดทอนจากการป้องกัน·คริติคอล·การปรับตามธาตุ การบริโภค·เส้นโค้งการฟื้นคืนของทรัพยากร (MP/พลังภายใน/สแตมินา) การกระจาย Cooldown
ก้อนที่ห้า สเปกควบคุมแอนิเมชัน เป็นแบบพิมพ์เขียวที่กำหนดว่าเจตนาในการออกแบบจะปรากฏอย่างไรในบิลด์จริง — แอนิเมชันกราฟ·BT·การเชื่อม IK โดยทั่วไปนี่เป็นงานที่ทำร่วมกับโปรแกรมเมอร์·แอนิเมเตอร์ แต่ถ้านักออกแบบเกมไม่ส่งมอบสเปกของเจตนา เจตนาก็จะพังในบิลด์ ถ้าโยนแต่วัสดุไปโดยไม่ให้แบบพิมพ์เขียว ก็จะได้บ้านอีกหลังที่ไม่ใช่ที่ตั้งใจ
จุดสำคัญตรงนี้คือ ห้าสิ่งนี้มาเจอกันบนโต๊ะเดียวกัน ถ้ากฎ Combo (ก้อนที่สอง) เปลี่ยน DPS ของสูตรดาเมจ (ก้อนที่สี่) ก็เปลี่ยน และนั่นก็ไปเปลี่ยนน้ำหนักที่รู้สึกได้ของ Look & Feel (ก้อนที่หนึ่ง) อีกที ถ้าไม่ระบุไว้ชัดว่าผลงานไหนเป็นอินพุตของผลงานไหน การเปลี่ยนเพียงครั้งเดียวก็สั่นสะเทือนถึงห้าจุด ด้วยเหตุนี้จึงต้องมีพิกัด
เราจะวางผลงานการต่อสู้ทั้งห้าลงบนพิกัด L0\~L4 ที่จับไว้ใน 2.3 การแมปนี้คือกระดูกสันหลังของบทนี้
flowchart TD
L0["L0 · วิสัยทัศน์
'การต่อสู้แบบแอ็กชันที่สัมผัสของการตียังมีชีวิต'"]
L1["L1 · โครงสร้างระบบ
โครงสร้าง Combo·Cancel / สเปก Look&Feel / โครงคลาส"]
L2["L2 · การไหลของเนื้อหา
เส้นโค้งกลุ่มศัตรูแต่ละบท / ลำดับการปลดล็อกสกิล"]
L3["L3 · ชีตข้อมูล
ค่าสัมประสิทธิ์ดาเมจ·ค่า Cooldown·การบริโภคทรัพยากร"]
L4["L4 · การวัดในบิลด์
DPS ที่วัดจริง / เส้นทาง Combo จริง / ฟีดแบ็กผู้เล่น"]
L0 -->|"รับมา"| L1
L1 -->|"โครงกำหนดการไหล"| L2
L1 -->|"สเปกเป็นเกณฑ์ของค่าตัวเลข"| L3
L2 --> L3
L3 -->|"สะท้อนลงบิลด์"| L4
L4 -.->|"วัด → ฟีดแบ็กแก้สเปก"| L1
L4 -.->|"ค่าผิดปกติ → ปรับชีต"| L3
classDef vision fill:#2d3748,stroke:#1a202c,color:#fff
classDef build fill:#c05621,stroke:#7b341e,color:#fff
class L0 vision
class L4 build
หากเรียบเรียงใหม่เป็นตารางจะได้ดังนี้
| Layer | ผลงานของงานออกแบบการต่อสู้ | ความถี่ในการเปลี่ยน |
|---|---|---|
| L0 | (รับมา — วิสัยทัศน์: "การต่อสู้แบบแอ็กชันที่สัมผัสของการตียังมีชีวิต") | แทบคงที่ |
| L1 | โครงสร้าง Combo·Cancel / สเปก Look & Feel / โครงคลาส | ช้า |
| L2 | เส้นโค้งความก้าวหน้าของกลุ่มศัตรูแต่ละบท / การไหลของการปลดล็อกสกิล | ปานกลาง |
| L3 | ชีตค่าสัมประสิทธิ์ดาเมจของสกิล, ค่า Cooldown, การบริโภคทรัพยากร | เร็ว |
| L4 | DPS ที่วัดจริงในบิลด์, เส้นทาง Combo ที่ทำได้จริง, ฟีดแบ็กผู้เล่น | ทุกบิลด์ |
ลักษณะเด่นของงานออกแบบการต่อสู้คือ สัดส่วน L4 ใหญ่กว่าแขนงอื่น ๆ สำหรับงานออกแบบเนื้อเรื่อง สเปก L1 แทบจะเท่ากับฉบับสุดท้ายในตัว แต่การต่อสู้ต่างออกไป "สัมผัสของการตีดี" เป็นพื้นที่ที่ต้องลงมือตีในบิลด์จริงและดูหน้าจอเองเท่านั้นจึงจะรู้ได้ แม้จะเขียนในสเปกว่า "Hit Stop 5 เฟรม" แต่ว่ามันรู้สึกหนักแน่นจริงหรือไม่ จะยืนยันได้แค่ที่ L4 เท่านั้น ด้วยเหตุนี้เครื่องมือจำลอง (simulation) และการวัดอัตโนมัติจึงสร้างคุณค่าได้มากที่สุดในแขนงนี้ (4.4)
อย่างไรก็ตาม การที่ L4 ใหญ่ไม่ได้แปลว่า L1 สำคัญน้อยลง ดูลูกศรเส้นประให้ดี ค่าที่วัดได้ของ L4 จะป้อนกลับเข้าสู่สเปก L1 ถ้าไม่มีสเปก ค่าที่วัดได้ก็จะสูญเสีย "เกณฑ์ในการเปรียบเทียบ" ต้องมีสเปก 5 เฟรมก่อน จึงจะมีคำวินิจฉัยออกมาว่า "วัดจริงได้ 4 เฟรม ขาดไป 1 เฟรม" วัฏจักร สเปก → บิลด์ → วัด → แก้สเปก เดินผ่านทั้งห้า Layer ครบ นักออกแบบการต่อสู้เป็นตำแหน่งที่วางมือไว้บนวัฏจักรนี้ทั้งกระบวนการ เป็นที่นั่งซึ่งมี Layer หมุนพร้อมกันมากที่สุดบนโต๊ะเดียว
ทฤษฎีเชิงนามธรรมจบลงตรงนี้ ทีนี้จะกลับไปที่ฉากในห้องประชุม แล้วดูกระบวนการดึง "สัมผัสของการตีอ่อน" ลงมาเป็นตัวเลขจริง ๆ ผ่านบันทึกเซสชันจริง (worked transcript — บันทึกการทำงานจริงกับ AI ทั้งพรอมต์และผลลัพธ์) เครื่องมือคือ LLM เพียงตัวเดียว ผลลัพธ์ไม่ได้ขัดเกลา และส่วนที่คนปฏิเสธก็เก็บไว้ตามเดิม
ผู้เขียนจะเรียบเรียงใหม่จากการไหลจริงของการกำหนดสเปก Look & Feel ของสกิลระยะประชิดใหม่หนึ่งตัว ใน TF การต่อสู้ของโปรเจกต์ MMORPG หนึ่ง (ต่อไปเรียก "โปรเจกต์ A") ที่ผู้เขียนบริหารในฐานะ Director
ช่วยเปลี่ยน Look & Feel ของสกิลนี้จากคำคุณศัพท์ให้เป็นค่าตัวเลขหน่วย ms·เฟรมหน่อย
ห้าหัวข้อ ได้แก่ hit timing, hit stop, camera shake, การซิงก์เอฟเฟกต์, ดีเลย์ตอนจบ
อันไหนไม่มั่นใจ ให้เขียนกำกับว่า "ต้องตรวจสอบในเกมนี้"
สกิล: ฟันลงตรงด้วยดาบใหญ่ (heavy_overhead_slash)
เจตนา: "ฟันลงอย่างหนักแน่น ให้รู้สึกว่าศัตรูเสียหลัก"
เฟรมเรต: 60fps
"ความหนักแน่น" ของการฟันลงตรงด้วยดาบใหญ่ ถูกสร้างขึ้นจาก ความยาวของ hit stop และดีเลย์ตอนจบ เป็นหลัก และจาก การลดทอนของ camera shake ขอเสนอแยกเป็นรายหัวข้อ
1) Hit Timing ยิ่งเป็นอาวุธหนัก ดีเลย์ตอนเริ่ม (startup) จากอินพุต→เริ่มทำงานยิ่งต้องยาว น้ำหนักจึงจะมีชีวิต แนะนำ 50\~80ms (3\~5 เฟรม) จากอินพุตไปจนถึงปฏิกิริยาทางสายตาแรก (ตัวละครเริ่มยกดาบขึ้น) แต่ปฏิกิริยา ทางสายตา แรก กับปฏิกิริยา การตีโดน แรก เป็นคนละอย่างกัน เวลาจนถึงตอนตีเข้าเป้าขึ้นอยู่กับความยาวของโมชัน จึงฟันธงตรงนี้ไม่ได้ — ต้องตรวจสอบด้วยความยาวโมชันของเกมนี้
2) Hit Stop เป็นตัวแปรหลักของการโจมตีหนักแน่น ถ้าการโจมตีเบาอยู่ที่ 1\~2 เฟรม การฟันลงตรงด้วยดาบใหญ่แนะนำ 4\~6 เฟรม (ราว 67\~100ms) ถ้าเกิน 6 เฟรม "ความหนักแน่น" จะข้ามไปเป็น "การกระตุก" ทำให้การตอบสนองแย่ลง
3) Camera Shake แอมพลิจูดราว 1\~2% ของความสูงหน้าจอ ระยะเวลา 0.2\~0.3 วินาที การลดทอนใช้ ease-out (อ่อนลงอย่างรวดเร็ว) มากกว่าแบบเชิงเส้น การโจมตีหนักจะเป็นธรรมชาติเมื่อเป็นเส้นโค้งที่การสั่นครั้งแรกแรงและจางหายไปอย่างรวดเร็ว
4) การซิงก์เอฟเฟกต์ VFX (เอฟเฟกต์การตี)·SFX (เสียงการตี)·hit stop ต้องถูกทริกเกอร์เหมือนกันในเฟรมที่ตีเข้าเป้าเสมอ แค่คลาดเคลื่อนไป 1\~2 เฟรมก็จะรู้สึกได้เป็น "ความจืด" ปฏิกิริยา UI (ตัวเลขดาเมจ) ช้าไป 1\~2 เฟรมไม่เป็นไร
5) ดีเลย์ตอนจบ ค่าที่ต้องจ่ายเพื่อความหนักแน่นคือดีเลย์ตอนจบที่ยาว แนะนำ 20\~30 เฟรม (333\~500ms) นับจากตีเข้าเป้าจนกลับมารับการโจมตีได้ แต่ค่านี้เปลี่ยนแปลงไปมากตามจังหวะการต่อสู้โดยรวมของเกม (แอ็กชันเร็ว vs แนวโซลที่หนักแน่นจริงจัง) — ต้องตรวจสอบที่จังหวะมาตรฐานของเกมนี้
สรุปแล้ว "ความหนักแน่น = ดีเลย์ตอนเริ่มยาว + hit stop 4\~6 เฟรม + camera shake แบบ ease-out + VFX/SFX/stop ในเฟรมเดียวกัน + ดีเลย์ตอนจบยาว" อย่างไรก็ตาม ตัวเลขข้างต้นเป็นช่วงคำแนะนำทั่วไป ส่วน น้ำหนักจริงต้องลงมือตีในบิลด์และปรับจึงจะสรุปได้
ตรงนี้นักออกแบบเกมไม่รับผลลัพธ์ทั้งก้อน เขาหยุดที่สองจุด
ปฏิเสธ 1 — แอมพลิจูด camera shake "1\~2% ของความสูงหน้าจอ" เกมนี้ให้มือถือมาก่อน บนหน้าจอเล็ก 1\~2% แทบมองไม่เห็น ทั้งยังมีปัญหาอาการเมาบนมือถืออีก นักออกแบบเกมปฏิเสธค่าแนะนำนี้ และนำหลักการของตัวเองมาใช้ว่า "บนมือถือจะเน้น hit stop แทน shake เพื่อสื่อถึงน้ำหนัก" LLM ให้แค่ทฤษฎีทั่วไป มันไม่รู้ข้อจำกัดของแพลตฟอร์มในเกมนี้
ระงับ 2 — hit stop "4\~6 เฟรม" อันนี้ไม่ใช่การปฏิเสธ แต่เป็นการพักไว้ ในแง่ช่วงค่าถือว่าถูก แต่ค่าที่แม่นยำจะกำหนดในบิลด์ด้วยสัมผัสมือ ในสเปกจะเขียนว่า "ใส่ 4 เฟรมเป็นค่าตั้งต้นลงในบิลด์ แล้วทำตัวแปร 5·6 เฟรมขึ้นมา เปรียบเทียบทั้งสามด้วยมือ"
การขอใหม่ออกไปแบบนี้
นี่เป็นโปรเจกต์ที่ให้มือถือมาก่อน ให้ลด camera shake ให้น้อยที่สุด
แล้วเขียนสเปกใหม่ในทิศทางที่ใช้ hit stop·ดีเลย์ตอนจบ·SFX สื่อถึงน้ำหนัก
ทำ hit stop เป็นสามตัวแปร 4/5/6 เฟรม เป็นตารางสำหรับเปรียบเทียบในบิลด์
ในผลลัพธ์ครั้งที่สองนี้ LLM สร้างตารางสเปกที่สะท้อนข้อจำกัดของมือถือ ตารางนั้นเข้าสู่บิลด์ และในการประชุมบิลด์ครั้งถัดมา นักออกแบบเกมพูดแทนคำคุณศัพท์ว่า "ตัวแปร 4 เฟรมเบาเกินไป เลือก 5 เฟรม" การประชุม 40 นาทีหดเหลือเป็นการตัดสินใจ 5 นาที
มีสามอย่าง หนึ่ง LLM สร้างร่างแรกที่ดึงคำคุณศัพท์ลงมาเป็นช่วงตัวเลข ได้ดี — นี่ทลายความเงียบในห้องประชุม สอง LLM ไม่รู้ข้อจำกัดของเกมนี้ (มือถือ·จังหวะ·ความยาวโมชัน) — มันจึงให้แค่ค่าแนะนำทั่วไป ส่วนการปฏิเสธ·ปรับเป็นหน้าที่ของคน สาม LLM ตอกย้ำด้วยตัวเองถึงสองครั้งว่า "ต้องลงมือตีในบิลด์จึงจะสรุปได้" — แม้แต่เครื่องมือก็รู้ว่าการตัดสินขั้นสุดท้ายของน้ำหนักคือมือคนที่ L4
บันทึกเซสชันข้างต้นแสดงเพียงตำแหน่งเดียว (การกำหนดสเปก) ในงานออกแบบการต่อสู้ทั้งหมด ตำแหน่งที่ AI สร้างคุณค่ามีอยู่สี่แห่ง
1) การจำลอง — คุณค่าใหญ่ที่สุด คำนวณเส้นโค้ง DPS (Damage Per Second, ดาเมจต่อวินาที)·เส้นทาง Combo·การบริโภคทรัพยากรไว้ล่วงหน้าโดยไม่ต้องมีบิลด์ เร็วกว่าการสร้างบิลด์แล้ววัดด้วยมืออย่างเทียบกันไม่ได้ ใน 4.4 จะลงมือทำกับตัวจำลอง simulate_dps โดยตรง
2) การสร้างสเตตแมชชีน·BT อัตโนมัติ แปลงคำอธิบายเป็นภาษาธรรมชาติอย่าง "บอสตัวนี้จะคลั่งเมื่อพลังชีวิตต่ำกว่า 50% และระหว่างคลั่งจะใช้แพตเทิร์น 3 ทีติด" ให้เป็นไดอะแกรม BT/FSM ความแม่นยำสูง — โครงสร้างกฎเป็นพื้นที่ที่ LLM จัดการได้ดี ประหยัดเวลาในการย้ายลอจิกในหัวออกมาเป็นภาพ
3) การวิเคราะห์บันทึกจากบิลด์โดยอัตโนมัติ สกัด hit timing·อัตราความสำเร็จของ Combo·การกระจายความเสียหายออกมาจากคลิปการเล่นโดยอัตโนมัติ แต่ว่านี่เป็น ตำแหน่งที่ความยากในการ implement สูงที่สุด (จะพิจารณาอย่างตรงไปตรงมาด้านล่าง)
4) การเสนอตัวเลือกในการปรับสมดุล วิเคราะห์แต่ละแถวของชีตข้อมูลเพื่อตรวจหาค่าผิดปกติ·ความไม่ราบเรียบของเส้นโค้ง แล้วเสนอตัวเลือกในการปรับ คนแค่เลือกเท่านั้น
ในสี่ตำแหน่งนี้ การวิเคราะห์บันทึกจากบิลด์โดยอัตโนมัติ (3) มีระยะห่างระหว่าง "ทำได้" กับ "ทำได้ง่าย" ไกลที่สุด ในหนังสือมักเขียนกันว่า "AI ดึงทุกอย่างออกมาจากคลิปวิดีโอให้โดยอัตโนมัติ" แต่ในความเป็นจริงไม่ได้ง่ายขนาดนั้น คอมพิวเตอร์วิทัศน์ที่อิงพิกเซลในวิดีโอ, vision API สำเร็จรูป, บันทึก telemetry ในเกม — การเปรียบเทียบความแม่นยำ·ภาระในการ implement ของสามวิธีจับข้อมูลนี้ 4.4 เป็นฉบับหลัก จึงให้ไปอ้างอิงที่นั่น ตรงนี้จะชี้แค่ข้อสรุป
เส้นทางที่สมจริงที่สุดคือ บันทึก telemetry ในเกม ทำให้เอนจินบันทึกอีเวนต์อย่าง "ที่เฟรม 1204 skill_overhead ตีเข้าเป้า, ดาเมจ 340, นับ Combo 3" ออกมาเองโดยตรง นี่เป็นข้อมูลต้นทางจึงแม่นยำ และจบในการแทรกโค้ดการบันทึกเพียงครั้งเดียว LLM ใช้ในการอ่านบันทึกนั้นแล้วสรุปเป็นรายงานภาษาธรรมชาติ ("จนถึง 3 Combo ประสิทธิภาพทรัพยากรดี แต่ตั้งแต่ 4 Combo ลดลงฮวบ") ส่วนวิดีโอเก็บไว้เป็นตัวเสริมให้คนตรวจด้วยตาเฉพาะเคสที่น่าสงสัยเท่านั้น
กล่าวคือ รูปแบบที่สมจริงของวิสัยทัศน์ "AI วิเคราะห์วิดีโออัตโนมัติ" คือ บันทึก telemetry + การสรุปด้วย LLM ไม่ใช่ pixel vision การแยกแยะอย่างตรงไปตรงมานี้เป็นจุดตั้งต้นของการเลือกเครื่องมือใน 4.4
และมีสิ่งหนึ่งที่ไม่เปลี่ยนในทั้งสี่ตำแหน่ง การตัดสินขั้นสุดท้ายว่า "สัมผัสของการตีดี" AI ทำไม่ได้ นั่นเป็นพื้นที่ของอารมณ์ผู้เล่น และความรับผิดชอบต่ออารมณ์นั้นเป็นของคน AI แค่สร้าง ข้อมูลที่เป็นหลักฐาน ของการตัดสินทางอารมณ์นั้นให้อย่างรวดเร็วเท่านั้น ค่าจากการจำลอง, ไดอะแกรม BT, รายงาน telemetry — ล้วนเป็นวัตถุดิบเพื่อให้คนตัดสินใจด้วยสัมผัสมือ
ที่ผ่านมานี้เป็นเหตุผลผิวเผินว่า "ถ้าแบ่งผลงานออกเป็น Layer เวลาทำงานร่วมกันจะคุยกันรู้เรื่อง" ที่อธิบายไว้ว่าวางกฎ Combo ไว้ที่ L1 และชีตดาเมจไว้ที่ L3 เพราะความถี่ในการเปลี่ยนต่างกัน เป็นคำที่ถูกต้อง แต่นั่นไม่ใช่ทั้งหมด
เหตุผลเชิงสาระในการแบ่งพิกัดคือ การทำงานอัตโนมัติทำงานได้บนพิกัดนั้นเท่านั้น ประเด็นทั่วไปที่ว่าการแยกย่อย Layer เป็นเงื่อนไขเบื้องต้นของการสร้างแบบโพรซีเดอรัล·การทำงานอัตโนมัตินั้น ได้กล่าวไว้แล้วใน 2.3 ตรงนี้จะแคบลงมาดูว่าเงื่อนไขเบื้องต้นนั้นแยกออกอย่างไรในการทำงานอัตโนมัติสามอย่างของแขนงการต่อสู้
ก้อนที่หนึ่ง การจำลองจะทำงานได้ก็ต่อเมื่อแยก "อะไรที่ป้อนเข้าไปได้ และอะไรที่เปลี่ยนได้" ออกจากกัน ถ้าคอร์เชิงกำหนด (ฟิสิกส์·Hitbox — โครง L1) กับสเปกที่เปลี่ยนได้ (ค่าดาเมจ·Cooldown — ชีต L3) ปนกัน ตัวจำลองก็จะนิยาม "พื้นที่ของตัวเลือกที่จะเปลี่ยน" ไม่ได้ คอร์คงที่ ชีตเป็นตัวแปร — ต้องมีการแยกนี้ก่อน simulate_dps จึงจะทำการค้นหาอย่าง "ค่อย ๆ ดันค่าสัมประสิทธิ์ดาเมจจาก 280 ไปจนถึง 340 ทีละ 20 แล้ววาดเส้นโค้ง DPS"
ก้อนที่สอง การวิเคราะห์บันทึกจากบิลด์โดยอัตโนมัติจะมีความหมายก็ต่อเมื่อ atom ของแอ็กชันถูกติดป้าย (labeling) ต้องมี atom ที่ติดป้ายไว้ในระดับสเปกว่า "ช่วงเฟรมนี้คือสเตจ hit ของ skill_overhead" เสียก่อน จึงจะสามารถนำสัญญาณที่สกัดจากบันทึก telemetry มาเทียบกับสเปกโดยอัตโนมัติได้ ถ้าไม่มีป้าย บันทึกก็เป็นแค่การเรียงต่อกันของจุดที่ไร้ความหมายว่า "ที่เฟรม 1204 มีบางอย่างตีเข้าเป้า"
ก้อนที่สาม การสร้างลำดับ Combo ด้วย LLM จะทำงานได้ก็ต่อเมื่อกฎ Cancel·Input Queue ถูกแยกออกมาเป็นเอกสารภายนอก คำขอแบบจำกัดอย่าง "เสนอลำดับ 5 Combo มา 10 ชุด จากคู่ที่ Cancel ได้ 7 คู่ของตัวละครนี้ ภายใน Input Queue 200ms" จะเป็นไปได้ก็ต่อเมื่อกฎ Cancel ไม่ได้ฝังตายอยู่ในโค้ด แต่ถูกแยกออกมาเป็นเอกสารเท่านั้น
ทั้งสามก้อนพูดประโยคเดียวกัน ถ้าคอร์เชิงกำหนดปนกับสเปก การทำงานอัตโนมัติจะตัน ถ้าแยกออก การทำงานอัตโนมัติจะเปิด การแยกย่อย Layer มีการทำให้ภาษาการทำงานร่วมกันเป็นหนึ่งเดียวเป็นจุดประสงค์ผิวเผิน ส่วนจุดประสงค์เชิงสาระคือการปูเงื่อนไขเบื้องต้นของการจำลองอัตโนมัติ·การวิเคราะห์ข้อมูลที่จับมา·การค้นหาลำดับด้วย LLM
เมื่อปูเงื่อนไขเบื้องต้นนี้แล้ว การดำเนินงานการต่อสู้จะวิวัฒน์เป็นสองขั้น
การประยุกต์แบบอนุรักษ์นิยม — คนออกแบบ ระบบอัตโนมัติตรวจสอบ ตอนนี้การดำเนินงานการต่อสู้ของเกมแอ็กชัน·MMORPG ส่วนใหญ่อยู่ตรงนี้ เมื่อคนเขียนสเปก Combo·Cancel ด้วยตัวเอง ระบบอัตโนมัติก็จำลอง DPS·ทรัพยากร และจับด้วย telemetry แล้วออกรายงานเปรียบเทียบ "สเปก vs ค่าที่วัด" คนตีความความต่างนั้นแล้วตัดสินใจแก้สเปก แล้วก็กลับไปที่การเขียนสเปก วัฏจักรหมุนอีกครั้ง การออกแบบเป็นของคน ส่วนการจำลอง·การจับ·การเปรียบเทียบเป็นของระบบอัตโนมัติ
การประยุกต์แบบก้าวหน้า — AI เสนอตัวเลือก คนแค่เลือกใช้ เป็นขั้นถัดไป AI แจกแจงลำดับออกมา 10\~30 ชุดโดยอัตโนมัติภายในคู่ Cancel และ Input Queue ระบบอัตโนมัติจำลอง DPS·ทรัพยากรของแต่ละลำดับแบบขนาน และ LLM ติดอันดับ·การตีความให้ในรูปแบบ "ประสิทธิภาพทรัพยากรอันดับ 1, ความยากในการอินพุตปานกลาง" การตัดสินใจที่เหลืออยู่ในมือคนมีแค่ "จะเลือกลำดับไหนในบรรดาตัวเลือกมาเป็นซิกเนเจอร์" หนึ่งเดียว กับการตัดสินใจสะท้อนลงบิลด์·การจับโมชันของ Director เท่านั้น การสร้างลำดับจากศูนย์ กับการเลือกจาก 30 ชุด มีมิติของภาระงานต่างกัน
การที่การประยุกต์แบบก้าวหน้าจะตั้งหลักได้ ต้องมีพร้อมสามอย่าง (1) โครงสร้างพื้นฐานการจำลองเชิงกำหนดที่คำนวณ DPS·ทรัพยากร·เวลารอดได้ภายใน 1 วินาทีโดยไม่ต้องมีบิลด์ (2) atom ของแอ็กชันที่ Combo·Cancel·Input Queue ถูกแยก·ติดป้ายเป็นเอกสารภายนอก (3) การวิเคราะห์ข้อมูลที่จับมาอัตโนมัติบนฐาน telemetry ทั้งสามอย่างล้วนเป็นผลลัพธ์โดยตรงของการแยกย่อย Layer ที่กล่าวมาข้างต้น
สุดท้ายคือเรื่องความย้อนกลับได้ ในวัฏจักรการตรวจสอบของนักออกแบบการต่อสู้ มีทั้งขั้นที่ย้อนกลับได้และย้อนกลับไม่ได้ปนกันอยู่ และการรู้เส้นแบ่งนั้นเป็นเรื่องสำคัญ
การจับโมชันเป็นขั้นที่ย้อนกลับไม่ได้ที่หนาที่สุดในการต่อสู้ ตารางคิวสตูดิโอจับภาพ การจ้างนักแสดง ค่าถ่ายซ้ำ ล้วนสูงทั้งสิ้น ด้วยเหตุนี้การจับโมชันของแอ็กชันซิกเนเจอร์จึงดำเนินการ หลังจากที่การวิเคราะห์อัตโนมัติด้วยการจำลอง·การจับข้อมูลทำงานได้เพียงพอจนกำหนดลำดับเสร็จแล้ว เท่านั้น ไม่ว่าจะแบบอนุรักษ์นิยมหรือก้าวหน้า ก็วางการจับโมชันกับจังหวะก่อนหน้าบิลด์ไลฟ์ไว้เป็นด่านการตัดสินใจ การตรวจสอบทั้งหมดของนักออกแบบการต่อสู้ต้องจบลงในขั้นที่ย้อนกลับได้ทางซ้ายของด่านนี้จึงจะปลอดภัย
นี่เป็นความเปลี่ยนแปลงที่ TF การต่อสู้ของโปรเจกต์ A วัดได้จากการใช้พิกัดและเครื่องมือข้างต้นมา 6 เดือน ตัวเลขด้านล่างเป็นค่าเฉลี่ยคร่าว ๆ ที่ดึงจากบันทึกการดำเนินงานของ TF ไม่ใช่ค่าที่วัดอย่างละเอียด จึงอ่านเป็น ทิศทางของความเปลี่ยนแปลงที่รู้สึกได้ จึงจะถูกต้อง
| หัวข้อ | ก่อนนำมาใช้ | หลังนำมาใช้ |
|---|---|---|
| เวลาประชุม Look & Feel | เฉลี่ย 2 ชั่วโมง (ถกอัตวิสัย) | เฉลี่ย 30 นาที (อิงค่าที่วัดได้) |
| การเขียนไดอะแกรม Combo | 1\~2 ชั่วโมง/ชุดสกิล | 10 นาที/ชุดสกิล |
| การตรวจสอบเส้นโค้ง DPS | วัดด้วยมือหลังบิลด์ (≈1 วัน) | จำลอง (≈10 นาที) |
| การปรับสมดุลสกิลใหม่ | 3\~4 รอบวัฏจักรบิลด์ | 1\~2 รอบวัฏจักรบิลด์ |
ทิศทางสำคัญกว่าตัวเลขเอง ทั้งสี่หัวข้อล้วนเคลื่อนจาก "ถกอัตวิสัย·วัดด้วยมือ·บิลด์ซ้ำ" ไปสู่ "ค่าที่วัดได้·จำลอง·ทำไดอะแกรมอัตโนมัติ" ในห้องประชุมคำคุณศัพท์ลดลง ตัวเลขเพิ่มขึ้น นั่นคือประโยคเดียวที่บททั้งบทต้องการจะพูด — งานของนักออกแบบการต่อสู้คือการสร้างสะพานที่เคลื่อนจากอัตวิสัย (สัมผัสของการตี·ความสนุก) ไปสู่ภววิสัย (ค่าตัวเลข·การจำลอง) และ AI คือเครื่องมือที่ปูสะพานนั้นได้อย่างรวดเร็ว ส่วนมือที่ตัดสินว่า "หนักแน่น" ที่ปลายสะพานยังคงเป็นของคนอยู่ดี
setup มี LLM ตัวเดียวก็พอ เลือกสกิลหนึ่งที่อยู่ในมือคุณ (จะใหม่หรือเก่าก็ได้) เขียนเจตนาของสกิลนั้นเป็นคำคุณศัพท์หนึ่งบรรทัด — อย่าง "หนักแน่น" "ฉับไว" "ทื่อหนัก"
prompt เติมข้อมูลสกิลลงในโครงด้านล่าง
คุณคือผู้ช่วยงานออกแบบการต่อสู้ ให้แปลง Look & Feel ของสกิลด้านล่าง
ให้เป็น "สเปกค่าตัวเลขที่วัดได้" ไม่ใช่คำคุณศัพท์ แต่เป็นหน่วย ms·เฟรม·%
หัวข้อที่ไม่มั่นใจ ให้ระบุว่า "ต้องตรวจสอบในเกมนี้"
สกิล: [ชื่อ]
เจตนา: "[คำคุณศัพท์หนึ่งบรรทัด]"
เฟรมเรต: [60fps เป็นต้น]
หัวข้อ: 1)hit timing 2)hit stop 3)camera shake 4)การซิงก์เอฟเฟกต์ 5)ดีเลย์ตอนจบ
verify โยนคำถามสองข้อให้กับทุกตัวเลขในผลลัพธ์ (1) ภายใต้ข้อจำกัดของเกมนี้ (แพลตฟอร์ม·จังหวะ·ความยาวโมชัน) ค่านี้ถูกต้องไหม → ถ้าไม่ถูก ให้บอกข้อจำกัดแล้วขอใหม่ (2) ค่านี้ต้องกำหนดด้วยมือในบิลด์หรือไม่ → ถ้าใช่ ให้เขียนตัวแปร 2\~3 ตัวลงในสเปกแทนค่าเดียว แล้วเปรียบเทียบในบิลด์ หัวข้อที่ LLM ตอกย้ำว่า "ต้องตรวจสอบ" อย่ารับมาใช้ตามนั้นเด็ดขาด
ถ้าเป็นเกมที่ทำคนเดียว ไม่จำเป็นต้องมีผลงานทั้งห้า·Layer ทั้งห้าครบ ทำแค่สองอย่างขั้นต่ำพอ หนึ่ง สเปก Look & Feel หนึ่งหน้า — สำหรับแอ็กชันหลัก 3\~5 อย่าง ให้เขียนแค่ hit stop·ดีเลย์ตอนจบ·การซิงก์เป็นตัวเลข บันทึกที่เขียนด้วยคำคุณศัพท์ ตัวคุณเองในอีก 6 เดือนข้างหน้าก็จะอ่านไม่ออก สอง แยก Combo·Cancel ออกจากโค้ดมาไว้เป็นไฟล์เดียว — ถ้าดึงคู่ Cancel ออกมาเป็นข้อมูล ภายหลังก็สามารถให้ LLM "เสนอ 5 Combo ที่สร้างได้จากคู่นี้" ได้ สองอย่างนี้คือพิกัดขั้นต่ำที่เปิดประตูสู่การทำงานอัตโนมัติไว้แม้ในการพัฒนาคนเดียว
หน้าจอมอนิเตอร์ในห้องประชุมมีคนยืนล้อมอยู่ห้าคน บิลด์เดียวกัน สกิลเดียวกัน วิดีโอความยาว 30 วินาทีคลิปเดิมกำลังเล่นวนซ้ำเป็นรอบที่สามบนจอ โปรแกรมเมอร์ฝั่งไคลเอนต์เอ่ยปากก่อน "ผมว่าโอเคนะ" คนดูแลฝั่งอาร์ตกอดอก "มันอ่อนไป รู้สึกเหมือนขาดอะไรไป" นักออกแบบเกมที่อยู่ข้าง ๆ แทรกขึ้นมา "เอฟเฟกต์ดีนะ แต่มันไม่ติดมือ" ผู้กำกับมองอยู่นานแล้วตัดสินใจ "อืม… เอาให้หนักแน่นขึ้นอีกนิดละกัน"
แล้วการประชุมก็จบลง 'หนักแน่นขึ้นอีกนิด' นั้นแม่นยำคือกี่ ms กี่เฟรม ไม่มีใครจดเอาไว้ ในบิลด์ถัดไปโปรแกรมเมอร์ก็สร้าง 'ความหนักแน่น' ตามที่ตัวเองเข้าใจ ส่วนฝั่งอาร์ตก็เติม 'ความหนักแน่น' ตามที่ตัวเองเข้าใจ แล้วสัปดาห์ถัดมาในห้องประชุมเดียวกันก็ดูวิดีโอเดียวกัน บทสนทนาเดิมก็วนซ้ำอีกครั้ง
ความรู้สึกของแรงปะทะ สัมผัสมือ Look & Feel — เป็นคำที่ถูกใช้บ่อยที่สุดและถูกนิยามไว้น้อยที่สุดในงานออกแบบการต่อสู้ ทุกคนเชื่อว่าตัวเองรู้ แต่นิยามในหัวของแต่ละคนแตกต่างกัน พอถกกันจบแล้วจึงไม่มีอะไรเหลือ บทนี้ว่าด้วยงานแยกย่อย 'ความรู้สึก' นั้นออกมาเป็นตัวเลขที่วัดได้ เป็นจุดที่ดึงสัมผัสมือลงมาจากนามธรรมสู่ข้อมูล
ก่อนอื่นขอลากเส้นแบ่งเอาไว้ งานออกแบบการต่อสู้แบ่งใหญ่ ๆ ได้สองสาย
บทนี้ว่าด้วยอย่างหลังเท่านั้น ดาเมจจะเป็น 100 หรือ 120 ก็ไม่เกี่ยวข้องโดยตรงกับสัมผัสมือ สิ่งที่เป็นสัมผัสมือคือผู้เล่นรู้สึกอย่างไรกับ 'จังหวะที่ดาเมจ 100 นั้นเข้าไป' แม้จะเป็นสูตรดาเมจเดียวกัน หากจังหวะการเข้าโจมตี (hit timing) และฮิตสต็อป (hitstop) ต่างกัน ก็จะรู้สึกเหมือนเป็นคนละเกมกันเลย
ก่อนอื่นมีเรื่องที่ต้องพูดกันตรง ๆ ความรู้สึกของแรงปะทะไม่ได้สมบูรณ์ด้วยตัวเลขเพียงสามตัว ต้องมีทั้งเส้นโค้งความเร่ง·ความหน่วงของโมชันโจมตี (แอนิเมชัน) ปฏิกิริยาของฝั่งที่โดน (hit reaction·อาการชะงัก) ไปจนถึงเดฟอร์เม (deformation — การยืดและบีบอัดตัวละครให้เกินจริงในจังหวะปะทะ เป็นการแสดงภาพ ภาพซ้อน·การเบลอ ที่นิยมใช้ในเกมแอ็กชันญี่ปุ่นยุค 80–90) ทั้งหมดจึงจะประกอบกันเป็นความรู้สึก "โดนแล้ว" ก้อนเดียว สิ่งที่บทนี้มุ่งดึงลงมาเป็นตัวเลขที่วัดได้ คือสามแกนในจำนวนนั้น ส่วนโมชัน·ปฏิกิริยา·เดฟอร์เมเป็นพื้นที่ที่มือของแอนิเมเตอร์·อาร์ติสต์สัมผัสมากกว่า จึงยกไปพูดในบทถัด ๆ ไปและในส่วนของอาร์ต ในที่นี้เราจะเน้นน้ำหนักไปที่สามแกนที่นักออกแบบเกมสามารถตรึงไว้เป็นข้อกำหนด (spec) และตรวจสอบในบิลด์ได้ สามแกนนั้นแยกย่อยได้ดังนี้
เมื่อมีใครในห้องประชุมพูดว่า "มันอ่อน" ความอ่อนนั้นมาจากหนึ่งในสามแกน ตอบสนองช้าไป (จังหวะ) ไม่มีความรู้สึกของการปะทะ (ฮิตสต็อป) หรือต่างคนต่างเล่น (การซิงค์)? เมื่อแยกออกเป็นสามแกนแล้วถาม คำว่า 'อ่อน' จึงกลายเป็นประโยคที่แก้ไขได้เสียที
อย่างไรก็ตาม สาเหตุของ 'ความอ่อน' ไม่ได้อยู่ที่สามแกนนี้เสมอไป เราจะวางองค์ประกอบทั้งหมดที่ประกอบกันเป็นสัมผัสมือไว้ให้ครบ แล้วลากเส้นว่าบทนี้รับผิดชอบไปถึงไหน
| องค์ประกอบ Look & Feel | คืออะไร | ในบทนี้ |
|---|---|---|
| จังหวะการเข้าโจมตี | ms ตั้งแต่อินพุต → การตอบสนองครั้งแรก องค์ประกอบที่ถูกตั้งข้อสงสัยเป็นอันดับแรก | วัด·ทำข้อกำหนด (แกน 1) |
| ฮิตสต็อป | ความยาวของการหยุดเวลาในจังหวะปะทะเพื่อให้น้ำหนัก | วัด·ทำข้อกำหนด (แกน 2) |
| การสั่นของกล้อง (camera shake) | แรงสะท้อนที่หน้าจอสั่นให้เข้ากับการปะทะ | วัด·ทำข้อกำหนด (รวมอยู่ในแกน 3) |
| จังหวะ VFX·SFX | เอฟเฟกต์และเสียงซิงค์กับเฟรมที่ปะทะโดนหรือไม่ | วัด·ทำข้อกำหนด (รวมอยู่ในแกน 3) |
| โมชันโจมตี (แอนิเมชัน) | ความเร่ง·ความหน่วงของการเหวี่ยง เส้นโค้งของท่าเตรียมและท่าตาม | กล่าวถึง (พื้นที่อาร์ต·แอนิเมชัน) |
| hit reaction·อาการชะงัก | ปฏิกิริยาที่ฝั่งโดนสะดุ้งและชะงัก | กล่าวถึง (บทถัดไป·ส่วนอาร์ต) |
| เดฟอร์เม | การยืดและบีบอัดให้เกินจริงในจังหวะปะทะ (ภาพซ้อน·การเบลอ) | กล่าวถึง (ส่วนอาร์ต) |
| แรงสั่นของคอนโทรลเลอร์ | ฟีดแบ็กเชิงกายภาพที่ส่งมาถึงมือ | วัด·ทำข้อกำหนด (รวมอยู่ในแกน 3) |
สี่ตัวด้านบนถูกมัดรวมเป็นสามแกนของบทนี้และกลายเป็นเป้าหมายของการวัด·ทำข้อกำหนด ส่วนสามตัวตรงกลาง (โมชัน·ปฏิกิริยา·เดฟอร์เม) เป็นสิ่งที่ขาดไม่ได้ แต่เป็นพื้นที่อาร์ต·แอนิเมชันที่นักออกแบบเกมคนเดียวปิดด้วยตัวเลขได้ยาก จึงทำเพียงระบุให้ชัดว่า 'มันมีอยู่' เท่านั้น หากโมชันแข็งทื่อหรือฝั่งที่โดนยังยืนเฉยอยู่ดี ๆ ต่อให้สามแกนตรงครบก็ไม่เกิดความรู้สึกของแรงปะทะ
ในสามแกนนี้ การที่จัดการเรื่องจังหวะเป็นอันดับแรกนั้นมีเหตุผล เมื่อผู้เล่นเริ่มสงสัยในสัมผัสมือ สิ่งแรกที่สะดุดคือความรู้สึกว่า 'ตอบสนองช้า' และไม่ว่าอย่างอื่นจะหวือหวาแค่ไหน ถ้าอินพุตเชื่องช้า ทุกอย่างก็พังในจังหวะนั้น ด้วยเหตุนี้จึงเริ่มจับที่จังหวะก่อน
แกนแรกของสัมผัสมือคือเวลา ตั้งแต่ขณะที่กดปุ่ม (0ms) จนถึงขณะที่หน้าจอตอบสนองเป็นครั้งแรก ใช้เวลากี่ ms มนุษย์ไวต่อความหน่วงนี้อย่างน่าประหลาดใจ ความต่างระหว่าง 60ms กับ 120ms นั้น 'อธิบายเป็นคำพูดไม่ได้ แต่มือรู้'
การโจมตีหนึ่งครั้งไม่ใช่จุดเดียวเรียบ ๆ แต่เป็นหลายเหตุการณ์ที่กางออกบนแกนเวลา เมื่อนำการโจมตีปกติหนึ่งจังหวะมาวางบนแกนเวลา หน้าตาจะเป็นแบบนี้
ในภาพนี้ ตัวเลขที่สำคัญที่สุดคือ '100ms ที่ hitbox เปิดเป็นครั้งแรก' หมายความว่าหลังกดปุ่ม 100ms การตัดสินการโจมตีจึงเริ่มขึ้น ค่านี้กำหนดความเร็วที่สัมผัสได้ของสัมผัสมือ
ช่วงที่แนะนำต่างกันไปตามแนวเกม·ตัวละคร แต่ก็มีเส้นมาตรฐานคร่าว ๆ อยู่
| ประเภท | อินพุต→ตอบสนองที่แนะนำ | หมายเหตุ |
|---|---|---|
| ตอบสนองทันที (โจมตีเบา) | 60\~120ms | ช่วงหัวใจของความรู้สึก 'ติดมือ' |
| ตอบสนองหนัก (สกิลใหญ่) | 200\~400ms | ดีเลย์ต้นที่ตั้งใจไว้เพื่อความหนักแน่น |
| ชาร์จ (อัดพลังนาน) | 500\~2000ms | การรอที่ตั้งใจ จัดการแยกต่างหาก |
ช่วงนี้ไม่ใช่มาตรฐานตายตัว เป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ว่าเกมมือถือแคชวลมักแกว่งไปทางผ่อนปรนอินพุต ±50ms ส่วนเกมต่อสู้บนคอนโซลมักรัดให้เข้มงวดกว่า สิ่งสำคัญยิ่งกว่าตัวเลขเองคือการที่ทีมมีเส้นมาตรฐานร่วมกันว่า 'การโจมตีเบาของเกมเราตกลงกันที่ 90ms' เมื่อมีเส้นมาตรฐานแล้วจึงจะพูดได้ว่า 'ถูก/ผิด' เมื่อดูบิลด์
แต่ตรงนี้มีกับดักอยู่หนึ่งอย่าง สายตามนุษย์แยกระหว่าง 90ms กับ 110ms ไม่ออก ที่ 60fps หนึ่งเฟรมประมาณ 16.67ms และความต่าง 20ms นี้ก็แค่หนึ่งเฟรมนิด ๆ ในห้องประชุม ประโยค "รู้สึกช้าไปหน่อยมั้ย?" จะถูกหรือผิด สุดท้ายก็ตัดสินด้วยสายตาไม่ได้ ด้วยเหตุนี้จึงต้องวัด
ในข้อกำหนดเขียนว่า 'hitbox 100ms' จะตรวจในบิลด์ได้อย่างไรว่าจริง ๆ มันเปิดที่ 100ms เส้นทางของการทำงานอัตโนมัติแยกออกเป็นสาม (วิเคราะห์วิดีโอ·เครื่องมือ vision สำเร็จรูป·telemetry ในเกม) ซึ่งการเปรียบเทียบความแม่นยำ·ความยากของสามวิธีนี้จะกล่าวเป็นฉบับหลักใน 4.4 ในที่นี้ขอเก็บเฉพาะข้อสรุป สิ่งที่ต้องวางในงานจริงเป็นอันดับแรกคือ telemetry ในเกม เหตุผลเรียบง่าย การที่โค้ดยิง [HITLOG] หนึ่งบรรทัดลงตรงเฟรมที่เหตุการณ์ OnHit เกิดขึ้นโดยตรงนั้น แม่นยำและถูกกว่าการอนุมานจากวิดีโอว่า VFX 'ปรากฏที่เฟรมไหน' บนหน้าจอแบบเทียบกันไม่ติด การวิเคราะห์วิดีโอใช้กับวิดีโอภายนอกที่ไม่มีโอเวอร์เลย์อินพุต (เช่น การวิเคราะห์เกมคู่แข่ง) ส่วนบิลด์ของเราให้เริ่มวางจาก telemetry ก่อน
ล็อก telemetry มีหน้าตาแบบนี้
[HITLOG] frame=6 t_ms=100 evt=hitbox_on skill=1001 char=warrior
[HITLOG] frame=6 t_ms=100 evt=vfx_trigger skill=1001
[HITLOG] frame=6 t_ms=100 evt=sfx_trigger skill=1001
[HITLOG] frame=7 t_ms=117 evt=damage_apply skill=1001 dmg=124
[HITLOG] frame=7 t_ms=117 evt=ui_dmgnum skill=1001
[HITLOG] frame=6 t_ms=100 evt=cam_shake skill=1001 amp=0.4
สิ่งที่นักออกแบบเกมต้องทำคือนำล็อกนี้มาเทียบกับข้อกำหนดทีละบรรทัด เนื่องจากส่วนใหญ่เป็นการแปลงตัวเลขและการเทียบเชิงกลไก หากให้คนทำซ้ำด้วยสายตาก็เหนื่อยและผิดพลาด แต่ LLM ไม่เหนื่อย ในหัวข้อถัดไปจะลองสั่งทำดูจริง ๆ
วางทั้ง yaml ข้อกำหนดและล็อก telemetry จากบิลด์เข้าไปทั้งคู่ แล้วสั่งให้ AI เทียบสองอย่างนี้และหาจุดที่คลาดเคลื่อน ด้านล่างคือพรอมต์เต็ม ผลลัพธ์ดิบของ Claude และกระบวนการตรวจสอบของมนุษย์ทั้งหมด ผมยกมาตามเดิมโดยไม่สรุปย่อ
ต่อไปนี้คือ 'ข้อกำหนดจังหวะการเข้าโจมตี' ของสกิลต่อสู้หนึ่งตัว และล็อก
telemetry ที่ดึงจากบิลด์จริง ช่วยจัดรายการที่บิลด์คลาดเคลื่อนจากข้อกำหนดเป็นตารางให้หน่อย
[ข้อกำหนด]
character: warrior
skill_id: 1001
fps: 60
events:
hitbox_on: {expect_ms: 100, tol_frame: 0}
vfx_trigger: {expect_ms: 100, tol_frame: 0}
sfx_trigger: {expect_ms: 100, tol_frame: 1}
damage_apply: {expect_ms: 110, tol_frame: 1}
ui_dmgnum: {expect_ms: 100, tol_frame: 2}
cam_shake: {expect_ms: 100, tol_frame: 1}
[ล็อก telemetry]
[HITLOG] frame=6 t_ms=100 evt=hitbox_on skill=1001
[HITLOG] frame=6 t_ms=100 evt=vfx_trigger skill=1001
[HITLOG] frame=8 t_ms=133 evt=sfx_trigger skill=1001
[HITLOG] frame=7 t_ms=117 evt=damage_apply skill=1001
[HITLOG] frame=9 t_ms=150 evt=ui_dmgnum skill=1001
[HITLOG] frame=6 t_ms=100 evt=cam_shake skill=1001
60fps (1 เฟรม=16.67ms) ช่วยแปลง expect_ms เป็นเฟรมแล้วเทียบกับ frame ของล็อก
ถ้าส่วนต่างเกิน tol_frame ให้แสดงเป็น FAIL
ก่อนอื่นผมจะแปลง expect_ms ของข้อกำหนดเป็นเฟรม ที่ 60fps frame = round(ms / 16.67)
- hitbox_on: 100 / 16.67 ≈ 6.0 → frame 6
- vfx_trigger: 100 → frame 6
- sfx_trigger: 100 → frame 6
- damage_apply: 110 / 16.67 ≈ 6.6 → frame 7 (ปัดขึ้น)
- ui_dmgnum: 100 → frame 6
- cam_shake: 100 → frame 6
ผลการเทียบกับ frame จริงในล็อกเป็นดังนี้
เหตุการณ์ frame ที่คาด frame จริง ส่วนต่าง(frame) ค่าที่ยอม ผลตัดสิน hitbox_on 6 6 0 0 PASS vfx_trigger 6 6 0 0 PASS sfx_trigger 6 8 +2 1 FAIL damage_apply 7 7 0 1 PASS ui_dmgnum 6 9 +3 2 FAIL cam_shake 6 6 0 1 PASS มีสองรายการที่เกินค่าความคลาดเคลื่อนที่ยอมรับได้ - sfx_trigger: เสียงช้ากว่าการปะทะทางภาพ 2 เฟรม (ประมาณ 33ms) เกินค่าที่ยอม 1 เฟรม มีความเป็นไปได้สูงที่ผู้เล่นจะรู้สึกว่า "เสียงมันหลุดออกไปนิด ๆ" - ui_dmgnum: ตัวเลขดาเมจขึ้นช้าไป 3 เฟรม (ประมาณ 50ms) เกินค่าที่ยอม 2 เฟรม อาจนำไปสู่ความรู้สึกขัด ๆ ว่า "ตีโดนแล้วแต่ตัวเลขขึ้นช้า"
การที่ damage_apply เป็น frame 7 ตรงกับ frame 7 ตามข้อกำหนด (แปลงจาก 110ms) จึงถือว่าปกติ อย่างไรก็ตาม การที่ damage_apply ช้ากว่า hitbox_on (frame 6) อยู่ 1 เฟรม เป็นจุดที่ต้องยืนยันว่าเป็นการออกแบบที่ตั้งใจหรือไม่
พอได้ผลลัพธ์มาก็ไม่เชื่อทั้งหมดในทันที มีจุดที่ต้องตรวจสอบอยู่สองจุด
ตรวจสอบ 1 (เส้นแบ่งการปัดเศษ — ผ่าน) การแปลง 110ms ของ damage_apply ดูน่าสงสัย 110 / 16.67 = 6.598… เมื่อปัดเศษได้ 7 ถูกต้อง แต่ถ้าเจตนาของข้อกำหนดคือ '6 เฟรม = เฟรมเดียวกับ 100ms' ล่ะ? พอเช็กเองโดยตรงก็พบว่าในข้อกำหนดระบุ damage_apply: expect_ms 110 ตั้งใจให้ต่างจาก vfx (100) อยู่ 10ms จริง ดังนั้นการแปลงเป็น frame 7 จึงแม่นยำ AI ผ่าน
ตรวจสอบ 2 (ประโยคสุดท้าย — ปฏิเสธ) ประโยคที่ AI เติมท้ายว่า "การที่ damage_apply ช้ากว่า hitbox_on อยู่ 1 เฟรม เป็นจุดที่ต้องยืนยันว่าเป็นการออกแบบที่ตั้งใจหรือไม่" นั้นเป็นสัญญาณรบกวน ทั้งที่ในข้อกำหนดระบุเจตนาไว้แล้วด้วย expect_ms 110 แต่ AI กลับย้อนถามว่า 'ต้องยืนยัน' เมื่อให้ข้อกำหนดไปแล้วก็ควรปิดคำตอบภายในข้อกำหนด แต่กลับสร้างคำถามที่ไม่จำเป็นขึ้นมา ประโยคนี้จึงไม่นำมาใช้
ผมจึงร้องขอใหม่โดยใส่เหตุผลของการปฏิเสธลงไป
damage_apply มีเจตนาตรึงไว้ในข้อกำหนดที่ 110 อยู่แล้ว ตัดคอมเมนต์ 'ต้องยืนยัน' ออก
// หมายถึงอย่าย้อนถามรายการที่ปิดคำตอบไว้ในข้อกำหนดแล้ว
เหลือไว้แค่ FAIL สองรายการ (sfx, ui_dmgnum) และแทนที่จะฟันธงสาเหตุ
ให้เป็นเช็กลิสต์ 'ตำแหน่งโค้ด/รีซอร์สที่ต้องตรวจ'
นี่คือเช็กลิสต์การตรวจสอบสำหรับ FAIL สองรายการ ไม่ฟันธงสาเหตุ แต่จัดเฉพาะจุดที่ต้องตรวจ
sfx_trigger (ดีเลย์ +2 เฟรม) - [ ] ทริกเกอร์เสียงผูกไว้กับเหตุการณ์เดียวกับ
hitbox_onหรือผูกไว้กับ animation notify แยกต่างหาก - [ ] ตัวแอสเซ็ตเสียงเองมีช่วงเงียบนำหน้า (leading silence) หรือไม่ — ช่วงว่างหัวไฟล์อาจดูเหมือนเป็นดีเลย์ได้ - [ ] การเรียกเล่นเสียงเข้าในเฟรมเดียวกันแล้ว แต่ถูกเลื่อนด้วยเลเทนซีเอาต์พุตหรือไม่ (บัฟเฟอร์เสียงของแพลตฟอร์ม)ui_dmgnum (ดีเลย์ +3 เฟรม) - [ ] UI ตัวเลขดาเมจมีโครงสร้างที่อัปเดตในติกแยกหลังจาก
damage_apply(frame 7) หรือไม่ - [ ] การอัปเดต UI ถูกประมวลผลรวบยอดที่ท้ายเฟรมถัดไปหรือไม่ (คิวเรนเดอร์แบบหน่วง) - [ ] เฟรมเริ่มต้นของแอนิเมชันป็อปอัปตัวเลขมีค่าดีเลย์เพิ่มแทรกอยู่หรือไม่สิ่งที่ฟันธงได้: ทั้งสองรายการเกินค่าความคลาดเคลื่อนที่ยอมในข้อกำหนด และช้ากว่าการปะทะทางภาพ (frame 6) สิ่งที่ฟันธงไม่ได้: ในบรรดาสาเหตุข้างต้นว่าเป็นอันไหน ไม่อาจทราบได้จนกว่าจะตรวจโค้ด/แอสเซ็ต
ผลลัพธ์ครั้งที่สองนี้นำมาใช้ตามเดิม การที่ไม่กุสาเหตุขึ้นมาแต่เปลี่ยนเป็น 'จุดที่ต้องตรวจ' เป็นรูปแบบที่ต้องการพอดี ผมส่งเช็กลิสต์นี้ให้ผู้รับผิดชอบฝั่งเสียงและฝั่ง UI ตามเดิม ฝั่งเสียงพบว่าตัวการคือช่วงเงียบนำหน้าของแอสเซ็ตเสียง 33ms (เช็กลิสต์ข้อ 2) ส่วน UI เป็นโครงสร้างอัปเดตในเฟรมถัดไป (ข้อ 1)
ตรงนี้เส้นแบ่งของการแบ่งงานก็ชัดเจน AI เทียบข้อกำหนดกับล็อกเชิงกลไกเพื่อจับ FAIL ส่วนมนุษย์ (a) ปฏิเสธคำย้อนถามที่ไม่จำเป็นซึ่ง AI สร้างขึ้น และ (b) ยืนยันสาเหตุที่แท้จริงของ FAIL ในโค้ด หากให้ AI ฟันธงสาเหตุ มันจะกุคำโกหกที่ฟังดูน่าเชื่อขึ้นมา การสั่งให้ทำแค่ถึง 'จุดที่ต้องตรวจ' จึงปลอดภัย
แกนที่สองคือการหยุด เอฟเฟกต์ที่หยุดเวลาเกมไว้สั้น ๆ มาก หรือทำให้ช้าลงในจังหวะที่การปะทะเข้าโดน นี่กำหนดความเข้มของความรู้สึก "ตีโดน" เป็นเครื่องมือสัมผัสมือที่ทรงพลังที่สุดในเกมต่อสู้และแอ็กชัน RPG ยาวไปก็อึดอัด สั้นไปก็ไม่มีน้ำหนัก
ช่วงที่แนะนำ (อิงที่ 60fps) เป็นดังนี้ ตัวเลขนี้เป็นแนวปฏิบัติคร่าว ๆ ที่ใช้กันในเกมแอ็กชัน ส่วนค่าตายตัวให้ปรับตามแต่ละเกม
| ประเภท | เฟรมที่แนะนำ | แปลงค่า |
|---|---|---|
| ปะทะเบา | 1\~2 เฟรม | 16\~33ms |
| ปะทะกลาง | 3\~5 เฟรม | 50\~83ms |
| ปะทะหนัก (ท่าไม้ตาย) | 6\~12 เฟรม | 100\~200ms |
| คริติคัล·โดนจุดอ่อน | ค่าข้างบน + 2\~3 เฟรม | — |
ต้องให้ค่าต่างกันไปตามแต่ละตัวละคร·สกิล ถ้าให้เหมือนกันหมดความต่างของน้ำหนักก็จะไม่ปรากฏ และสุดท้ายการโจมตีทั้งหมดก็จะลู่เข้าหาโทนเดียวกัน นี่คือจุดที่เชื่อมโยงไปยังหนึ่งในสามข้อความหลักของ 4.2
'ใครหยุด' ก็เป็นทางเลือกในการออกแบบเช่นกัน
| ตัวเลือก | ผล | เหมาะกับ |
|---|---|---|
| หยุดเฉพาะผู้โจมตี | ให้น้ำหนักฝั่งผู้โจมตี ฝั่งโดนยังดำเนินแรงสะท้อน·ล้ม | แอ็กชัน |
| หยุดเฉพาะผู้ถูกโจมตี | ผู้ถูกโจมตีหยุดชั่วขณะ ผู้โจมตีเคลื่อนที่ได้อิสระ | เป็นมิตรกับคอมโบ |
| หยุดทั้งคู่ | ให้น้ำหนักแรงที่สุด | ธรรมเนียมเกมต่อสู้ |
ข้อกำหนดป้อนเข้าแบบนี้
character: warrior
skill_id: 1001
hit_stop:
attacker: 2 # frames
victim: 4
critical_multiplier: 1.5 # ตอนคริติคัลคูณ 1.5 เท่า (ปัดเศษ)
ฮิตสต็อปเป็นแกนที่ตรวจสอบสัมผัสมือยากที่สุด เพราะไม่อาจปิด 'ถูก/ผิด' ด้วยตัวเลขข้อกำหนดเพียงอย่างเดียว telemetry จับได้ว่า 'หยุดไป 4 เฟรมจริงหรือไม่' แต่ '4 เฟรมเหมาะสมหรือไม่' มนุษย์ต้องลงมือลองในบิลด์เองแล้วตัดสิน AI รับประกันความตรงตามข้อกำหนด ส่วนมนุษย์ดูความเหมาะสมของค่าข้อกำหนดนั้นเอง
แกนที่สามคือความพร้อมเพรียง เมื่อ VFX (เอฟเฟกต์ภาพ)·SFX (เสียง)·UI (ตัวเลขดาเมจ)·กล้อง (การสั่น)·แรงสั่น (คอนโทรลเลอร์) เริ่มในเฟรมเดียวกัน สมองผู้เล่นจะมัดมันเข้าด้วยกันเป็น 'เหตุการณ์เดียว' คลาดเคลื่อนแค่ 1\~2 เฟรมก็จะได้ปฏิกิริยา "ขัด ๆ" คลาดไป 3\~5 เฟรมก็จะได้ปฏิกิริยา "เหมือนบั๊ก" เรื่องที่ sfx ช้า 2 เฟรมและ ui ช้า 3 เฟรมจน FAIL ในบันทึกเซสชันจริงก่อนหน้านี้ก็คือปัญหาของแกนนี้พอดี
เป้าหมายการซิงค์ 5 อย่างและค่าความคลาดเคลื่อนที่ยอมรับได้
| องค์ประกอบ | จังหวะทริกเกอร์ | ค่าที่ยอม |
|---|---|---|
| VFX (เอฟเฟกต์ภาพ) | เฟรมปะทะ | ±0 (ต้องพร้อมกันเสมอ) |
| SFX (เสียง) | เฟรมปะทะ | ±1 เฟรม (16ms) |
| UI ตัวเลขดาเมจ | เฟรมปะทะ | ±2 เฟรม |
| การสั่นของกล้อง | เฟรมปะทะ | ±1 เฟรม |
| แรงสั่นคอนโทรลเลอร์ | เฟรมปะทะ | ±2 เฟรม |
หัวใจคือ 5 อย่างนี้ต้องเข้าไปอยู่ในข้อกำหนด ทั้งหมด ความผิดพลาดที่พบบ่อยคือเขียนเฉพาะ VFX ลงข้อกำหนด แล้วปล่อยอีกสี่อย่างไว้แบบ 'เดี๋ยวมันคงเข้าจังหวะกันเอง' ถ้าไม่มีในข้อกำหนด เกณฑ์ของการตรวจสอบบิลด์ก็ไม่มี และต่อให้จับด้วย telemetry ได้ ก็ไม่มีสิ่งที่จะเอามาเทียบ 5 อย่างต้องอยู่ในข้อกำหนดครบ การเทียบอัตโนมัติจึงจะปิดได้
การเทียบอัตโนมัติใช้บันทึกเซสชันจริงในหัวข้อก่อนหน้าได้ตามเดิม หากเฟรมทริกเกอร์ของทั้ง 5 อย่างถูกบันทึกครบในล็อก telemetry AI จะเทียบกับข้อกำหนดและรายงานเฉพาะรายการที่เกินค่าความคลาดเคลื่อน คนไม่จำเป็นต้องเอาตา 100 สกิลมาตรวจทุกบิลด์ อย่างไรก็ตาม การที่ AI จับความคลาดเคลื่อนจากข้อกำหนด กับการที่มนุษย์จับพื้นที่ "ข้อกำหนดตรงครบแต่ก็ยังไม่ได้ความรู้สึก" ยังคงเป็นคนละเรื่องที่แยกกันอยู่
เราจะร้อยชิ้นส่วนทั้งหมดที่ผ่านมาเป็นกระแสเดียว เมื่อลูปนี้เริ่มหมุน 'หนักแน่นขึ้นอีกนิด' ในห้องประชุมก็จะถูกแปลเป็น 'ฮิตสต็อป victim 4→6 เฟรม'
flowchart TD
A["เขียนข้อกำหนด
(นักออกแบบเกม: yaml)"] --> B["ทำในบิลด์
(โปรแกรมเมอร์·อาร์ติสต์)"]
B --> C["รันบิลด์ + ล็อก telemetry
[HITLOG] ออกอัตโนมัติ"]
C --> D["AI เทียบอัตโนมัติ
yaml ข้อกำหนด vs telemetry"]
D --> E{"มี FAIL
ที่เกินค่าคลาดเคลื่อนไหม?"}
E -->|มี| F["รายการ FAIL + เช็กลิสต์ตรวจสอบ
(AI, ห้ามฟันธงสาเหตุ)"]
F --> G["ผู้รับผิดชอบยืนยันสาเหตุที่แท้จริง
ในโค้ด·แอสเซ็ต (มนุษย์)"]
G --> B
E -->|ไม่มี| H["นักออกแบบเกม: ตรวจ 'ความรู้สึก'
ข้อกำหนดตรงแต่ได้สัมผัสมือไหม (มนุษย์)"]
H -->|ต้องปรับ| A
H -->|OK| I["เก็บเป็นอินพุตของการทบทวนไมล์สโตนถัดไป"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class C code;
class D,F ai;
class A,G,H human;
class I pass;
ในลูปนี้ ช่องที่ AI รับ (D, F) กับช่องที่มนุษย์รับ (A, G, H) แยกออกจากกันชัดเจน AI เก่งการเทียบเชิงกลไกและการสร้างเช็กลิสต์ ส่วนมนุษย์เก่งการตั้งเส้นมาตรฐาน·ยืนยันสาเหตุ·ตัดสินความรู้สึกขั้นสุดท้าย การทำงานอัตโนมัติไม่ได้ถอดคนออก แต่ปลดคนออกจาก 'การนับเฟรมด้วยสายตา' ที่เคยใช้ครึ่งวันทุกครั้ง ให้ไปจดจ่อกับ 'ความรู้สึก' เพียงอย่างเดียว
ช่องสุดท้ายของลูป (I) สำคัญ ข้อมูลการวัดนี้ไม่ใช่ใช้แล้วทิ้ง แต่จะกลับเข้าไปเป็นอินพุตของการทบทวนไมล์สโตนถัดไปอีกครั้ง เมื่อแพตเทิร์นอย่าง 'FAIL ของสัมผัสมือไตรมาสที่แล้วกระจุกอยู่ที่การซิงค์ sfx' เหลือไว้เป็นข้อมูล ไตรมาสถัดไปก็จะลงมือแก้ไปป์ไลน์เสียงก่อน
ในโปรเจกต์เกมมือถือ MMORPG หนึ่ง (ต่อไปเรียก 'โปรเจกต์ A') ที่ผู้เขียนเข้าร่วมในฐานะผู้กำกับ นี่คือความเปลี่ยนแปลงที่สังเกตได้จากการหมุนลูปข้างต้นราว 6 เดือน ตัวเลขด้านล่างไม่ใช่การวัดอย่างละเอียด แต่เป็น การสังเกตจากการปฏิบัติงานของผู้เขียน (รวมการประมาณ) ที่อิงจากไทม์สแตมป์ในบันทึกการประชุมและบันทึกการตรวจสอบบิลด์ ขออ่านในแง่ทิศทางและสัดส่วน อย่านำค่าตายตัวไปใช้เป็นเบนช์มาร์กที่อ้างอิงได้
| รายการ | ก่อนนำมาใช้ | หลังนำมาใช้ | ลักษณะ |
|---|---|---|---|
| เวลาที่ใช้ในการประชุม Look & Feel 1 ครั้ง | ยืดยาว | ลดเหลือไม่ถึงครึ่ง | อิงบันทึกการประชุม การรับรู้ |
| การตรวจสอบบิลด์ (สกิลจำนวนมาก) | เกือบทั้งวัน | ลดลงมาก | ผลของการเทียบอัตโนมัติด้วย telemetry |
| การปิดฟีดแบ็ก "แรงปะทะอ่อน" | หลายรอบบิลด์ | 1\~2 รอบ | ตัวอย่างน้อย ดูเฉพาะทิศทาง |
| อัตราความตรงกันของข้อกำหนด vs บิลด์ | ราวครึ่ง | ตรงกันเป็นส่วนใหญ่ | วัดได้หลังนำ telemetry มาใช้ |
สิ่งที่เป็นแก่นคือความเปลี่ยนแปลงเชิงคุณภาพยิ่งกว่าตัวเลขเอง พอมีคนพูดในห้องประชุมว่า "มันอ่อนนะ" ก็จะย้อนถามได้ทันทีว่า "แกนไหนล่ะ? จังหวะ? ฮิตสต็อป? การซิงค์?" และเมื่อยังไม่ได้คำตอบก็เปิด telemetry ขึ้นมาดูด้วยกัน การที่ความเป็นปรวิสัยซึ่งวัดได้สร้างจุดสิ้นสุดให้การถกเถียง คือความเปลี่ยนแปลงที่ใหญ่ที่สุดในรอบ 6 เดือน เรื่องที่ 'หนักแน่นขึ้นอีกนิด' หลุดออกไปนอกห้องประชุมก็ลดลง
| ความผิดพลาด | การหลีกเลี่ยง |
|---|---|
| ตรวจจากบิลด์โดยไม่มีข้อกำหนด | ข้อกำหนดต้องมาก่อน ไม่มีเกณฑ์ก็พูด 'ถูก/ผิด' ไม่ได้ |
| พยายามวางการวิเคราะห์วิดีโอก่อน | บิลด์ของเราเริ่มจาก telemetry การวิเคราะห์วิดีโอใช้กับวิดีโอภายนอกเท่านั้น |
| ให้ AI ฟันธงสาเหตุของ FAIL | แค่ถึงเช็กลิสต์ 'จุดที่ต้องตรวจ' สาเหตุให้มนุษย์ยืนยันในโค้ด |
| ทำข้อกำหนดเฉพาะ VFX ตกอีก 4 อย่าง | ทั้ง 5 อย่าง (VFX·SFX·UI·กล้อง·แรงสั่น) ต้องอยู่ในข้อกำหนด |
| ก๊อปข้อกำหนดตัวละครหนึ่งไปทุกตัวละคร | แยกความต่างตามแต่ละตัวละคร·สกิล ถ้าเหมือนกันสัมผัสมือจะลู่เข้าโทนเดียว |
| ใส่ฮิตสต็อปพร่ำเพรื่อในทุกการปะทะ | ใส่เฉพาะการปะทะที่มีความหมาย พร่ำเพรื่อแล้วจะอึดอัด |
| รับคำย้อนถามของผลลัพธ์ AI มาตามเดิม | คอมเมนต์ 'ต้องยืนยัน' ของรายการที่ปิดในข้อกำหนดแล้วให้ปฏิเสธ |
นี่คือขั้นตอนการลองวางลูปนี้ลงในโปรเจกต์ของคุณด้วยขนาดที่เล็กที่สุด
setup
1. เลือกสกิลที่จะตรวจสอบ 1 ตัว (แนะนำการโจมตีปกติ)
2. ฝังล็อกบรรทัดละหนึ่งลงในจุดทริกเกอร์ 6 จุดของโค้ด (hitbox_on, vfx, sfx, damage_apply, ui_dmgnum, cam_shake): [HITLOG] frame=X t_ms=Y evt=... skill=...
3. เขียน yaml ข้อกำหนด (ในบล็อก events ใส่ expect_ms + tol_frame) ใช้ตัวอย่างพรอมต์ครั้งที่ 1 ของบทนี้เป็นเทมเพลต
prompt 4. รันบิลด์หนึ่งครั้งเพื่อรวบรวมล็อก telemetry 5. วาง yaml ข้อกำหนด + ล็อก telemetry เข้าไปใน AI พร้อมกัน แล้วสั่งแบบนี้: "ช่วยเทียบรายการที่บิลด์คลาดเคลื่อนจากข้อกำหนดด้วยการแปลงเป็นเฟรมที่ 60fps แล้วให้เฉพาะ FAIL เป็นตาราง อย่าฟันธงสาเหตุ ให้เป็นเช็กลิสต์ 'จุดที่ต้องตรวจ'" (รูปแบบพรอมต์ครั้งที่ 2 ของบทนี้)
verify 6. คำนวณตรวจสอบการแปลงเฟรมในผลลัพธ์ AI ด้วยตัวเองสักหนึ่งช่อง (ms / 16.67 ปัดเศษ) ถ้าผิดแม้แต่ช่องเดียวให้สงสัยทั้งหมด 7. ถ้า AI ย้อนถามรายการที่ปิดในข้อกำหนดแล้วหรือฟันธงสาเหตุ ให้ปฏิเสธและร้องขอใหม่ 8. ส่งเช็กลิสต์ FAIL ให้ผู้รับผิดชอบเพื่อยืนยันสาเหตุที่แท้จริงในโค้ด·แอสเซ็ต
ถ้าเป็นนักพัฒนาคนเดียวที่ไม่มีทั้งทีมและโครงสร้างพื้นฐาน telemetry ให้ลดทอนแบบนี้ บันทึกหน้าจอบิลด์ที่ 60fps แล้วเปิดโอเวอร์เลย์อินพุตปุ่มให้เห็นจังหวะที่กด หลังจากบันทึกสกิลที่จะตรวจสอบหนึ่งครั้ง ในโปรแกรมตัดต่อวิดีโอให้นับ 'เฟรมที่กดปุ่ม' และ 'เฟรมที่หน้าจอเปลี่ยนเป็นครั้งแรก' ด้วยตัวเอง ป้อนเลขเฟรมสองค่าและค่าที่คาดในข้อกำหนดให้ AI แล้วสั่งว่า "ช่วยแปลงเป็น ms ที่ 60fps แล้วเทียบกับข้อกำหนดให้หน่อย" ก็จะตรวจสอบแกนหลักหนึ่งแกน (จังหวะการเข้าโจมตี) ได้แม้ไม่มี telemetry การซิงค์ครบ 5 อย่างทำได้ยาก แต่เพียงจับแกน 'อินพุต→ตอบสนอง' แกนเดียวได้ ครึ่งหนึ่งของการถกเรื่องสัมผัสมือก็ย้ายมาอยู่บนความเป็นปรวิสัยแล้ว
บทถัดไปจะข้ามจากการปะทะหนึ่งครั้งไปสู่การปะทะที่ต่อเนื่องกัน คอมโบ·แคนเซิล·คิวอินพุต — ว่าด้วยกฎที่ทำให้การปะทะหนึ่งเชื่อมไปสู่การปะทะถัดไปอย่างเป็นธรรมชาติ
สมาชิกทีม B ซึ่งเป็นดีไซเนอร์ฝ่ายการต่อสู้ ยืนอยู่หน้าไวต์บอร์ดในห้องประชุมและกำลังวาดกล่องด้วยปากกามาร์กเกอร์ เบสิก1 เบสิก2 เบสิก3 และกิ่งที่แตกออกข้างไปเป็นการโจมตีหนัก เมื่อมีลูกศรเพิ่มขึ้นราวเจ็ดเส้น มีคนหนึ่งถามขึ้นว่า "งั้นถ้าหลังจากใช้ท่าดีดศัตรูขึ้นจากการโจมตีหนัก แล้วคานเซิลด้วยการหลบ จะกลับมาเริ่มที่เบสิก1 ได้อีกไหม" สมาชิกทีม B หยุดปากกาไว้ บนกราฟที่อยู่บนไวต์บอร์ดไม่มีเส้นทางนั้นวาดเอาไว้ จะวาดได้แต่ไม่ได้วาด หรือกฎไม่อนุญาตให้ทำกันแน่ แม้แต่เจ้าตัวเองก็ตอบทันทีไม่ได้
นี่คือปัญหาที่แท้จริงของการออกแบบคอมโบ คอมโบเมื่อคิดในหัวจะดูเหมือนเส้นเดียวเรียบง่ายแบบ "1-2-3 ต่อกัน แล้วแตกกิ่งไปการโจมตีหนัก" แต่เมื่อมีคานเซิลและคิวอินพุตเข้ามาเกี่ยว เส้นนั้นก็กลายเป็นกราฟ แค่เพิ่มเอดจ์คานเซิลไม่กี่เส้นบนโหนดหกตัว เส้นทางที่เหยียบได้จริงก็เพิ่มขึ้นเป็นหลายสิบสาย คนเราไม่สามารถกางทั้งหลายสิบสายนั้นออกในหัวได้หมด ความคิดเรื่องบาลานซ์อย่าง "เส้นทางนี้แรงเกินไป" จึงมักถูกค้นพบก็ต่อเมื่อมันเข้าไปอยู่ในบิลด์แล้วเท่านั้น
เป้าหมายของบทนี้มีหนึ่งเดียว คือการสร้าง เวิร์กโฟลว์ที่แจกแจงเส้นทางคอมโบทั้งหมดโดยอัตโนมัติแทนที่จะวาดด้วยมือ แล้วตรวจสอบแต่ละเส้นทาง เราจะเปลี่ยนกฎที่เขียนเป็นภาษาธรรมชาติให้เป็นสเปก แจกแจงเส้นทางจากสเปก แล้วเอาเส้นทางที่แจกแจงได้ไปป้อนเข้าการจำลอง ในระหว่างนั้นผู้เขียนจะแสดงให้เห็นแบบดิบ ๆ ว่า AI ช่วยได้ถึงไหน และมันโกหกตรงไหน
ถ้าเขียนคอมโบเป็นตารางก็จะออกมาแบบนี้ "หลังเบสิก1 เป็นเบสิก2 หลังเบสิก2 เป็นเบสิก3" เป็นแถวและคอลัมน์ที่เรียบร้อย แต่ตารางนี้โกหก เพราะตารางตั้งสมมติฐานว่ามันเป็นเส้นตรง ในการต่อสู้จริง ผู้เล่นแยกออกไปการโจมตีหนักจากเบสิก2 คานเซิลการโจมตีหนักด้วยการหลบ แล้วกดเบสิก1 อีกครั้งทันทีหลังหลบ การแตกกิ่งและวนซ้ำนี้ซ่อนตัวอยู่ระหว่างแถวของตาราง
ดังนั้นรูปร่างที่แท้จริงของคอมโบจึงเป็น กราฟแบบมีทิศทาง (directed graph) แอ็กชันคือโหนด การเชื่อมต่อคือเอดจ์ แต่ละเอดจ์มีหน้าต่างอินพุต (input window — รับอินพุตเมื่อไหร่) และคีย์อินพุตติดอยู่ โหนดมีเฟรมที่กินเวลาติดอยู่ และบางโหนดมีเงื่อนไขโบนัส (ต้องผ่านโหนดเฉพาะจึงจะได้ตัวคูณดาเมจ) ติดอยู่
ถ้าวาดคอมโบพื้นฐานหนึ่งชุดของตัวละครนักรบเป็นกราฟก็จะได้ดังนี้ หกโหนด รวมกิ่งคานเซิล
จุดที่ต่างจากไวต์บอร์ดอย่างชัดเจนมีสองอย่าง อย่างแรก แต่ละเอดจ์มีช่วงเฟรมของหน้าต่างอินพุตระบุไว้ "โจมตีหนัก 6\~24f" หมายความว่ารับอินพุตการโจมตีหนักได้ตั้งแต่เฟรมที่ 6 จนถึงเฟรมที่ 24 หลังจากเบสิก2 เริ่มขึ้น อย่างที่สอง มีเอดจ์เข้าเบสิก1 ใหม่จากการหลบ (หลบ→เบสิก1) ที่วาดด้วยเส้นประ นี่คือเส้นทางที่สมาชิกทีม B ตอบทันทีในห้องประชุมไม่ได้ เมื่อระบุลงในกราฟแล้ว "มี/ไม่มี" ก็ชัดเจน
ถ้าคนวาดกราฟนี้ด้วยมือ ก็จะได้หกโหนดและเอดจ์เจ็ดแปดเส้น ถ้ามีตัวละครยี่สิบตัว และแต่ละตัวมีชุดคอมโบสามสี่ชุด กราฟก็จะกลายเป็นหลายร้อยแผ่น มือตามไม่ทัน ดังนั้นเราจึงเขียนกราฟไว้เป็น สเปกข้อความ (text spec) แล้วสร้างรูปภาพและการตรวจสอบขึ้นจากตรงนั้นโดยอัตโนมัติ
เราย้ายกราฟข้างต้นไปเป็นสเปก YAML แก่นกลางคือสามบล็อก ได้แก่ โหนด (nodes) เอดจ์ (edges) และโบนัส (bonuses) กฎคานเซิลก็มองเป็นเอดจ์ชนิดหนึ่งด้วย เพราะการตัดแล้วไปยังโหนดอื่นก็คือเอดจ์เช่นกัน
# warrior_basic_chain.yaml
character: warrior
combo_id: basic_chain
nodes:
- { id: basic_1, name: เบสิก1, duration_frames: 21 }
- { id: basic_2, name: เบสิก2, duration_frames: 24 }
- { id: basic_3, name: เบสิก3, duration_frames: 30 }
- { id: heavy, name: โจมตีหนัก, duration_frames: 33 }
- { id: launch, name: ดีดขึ้น, duration_frames: 28 }
- { id: dodge, name: หลบ, duration_frames: 18, cancels_recovery: true }
edges:
- { from: basic_1, to: basic_2, input: light, window: [10, 21] }
- { from: basic_2, to: basic_3, input: light, window: [12, 24] }
- { from: basic_2, to: heavy, input: heavy, window: [6, 24] }
- { from: heavy, to: launch, input: heavy, window: [10, 33] }
- { from: heavy, to: dodge, input: dodge, window: [0, 33], type: cancel }
- { from: basic_3, to: dodge, input: dodge, window: [0, 30], type: cancel }
- { from: dodge, to: basic_1, input: light, window: [8, 18] } # เข้าใหม่
bonuses:
- { on: basic_3, requires_path: [basic_1, basic_2], damage_multiplier: 1.5 }
สเปกนี้ตอบสนองผู้อ่านสองฝ่ายพร้อมกัน คนอ่าน window: [6, 24] แล้วเข้าใจว่า "การโจมตีหนักเริ่มรับได้ตั้งแต่ช่วงกลางของเบสิก2" ส่วนเครื่องพาร์สบรรทัดเดียวกันเพื่อใช้วาดกราฟและแจกแจงเส้นทาง ความเข้าใจของคนและการตรวจสอบของเครื่องออกมาพร้อมกันจากแหล่งเดียว
ค่าเฟรมข้างต้น (21, 24, [6, 24]) ไม่ใช่ค่าที่วัดจริง แต่เป็นค่าตัวอย่างที่ผู้เขียนกำหนดขึ้นเพื่ออธิบายในบทนี้ (ยังไม่ได้ตรวจสอบ) ในโปรเจกต์จริง ค่าเหล่านี้มาจากความยาวมอนทาจที่แอนิเมเตอร์สร้างและไทมิงของโนทิฟายในบิลด์ ตอนเขียนสเปกครั้งแรกให้ใส่ค่าตามเจตนาของดีไซเนอร์ไปก่อน แล้วเมื่อบิลด์ออกมาก็แคปเจอร์มาแก้ให้เป็นค่าที่วัดจริง ลูปการแก้ค่านี้จะกล่าวถึงใน 4.4
เราเอากฎที่สมาชิกทีม B วาดไว้บนไวต์บอร์ดมาให้เป็นภาษาธรรมชาติ แล้วให้แปลงเป็นสเปก YAML ผู้เขียนจะคัดลอกพรอมต์ฉบับเต็ม ผลลัพธ์ดิบของ Claude และการตรวจสอบ/ปฏิเสธของคนมาตามจริงโดยไม่สรุปย่อ
ต่อไปนี้คือกฎคอมโบของตัวละครนักรบ จงแปลงสิ่งนี้เป็นสเปกคอมโบ YAML
กฎ:
- การโจมตีพื้นฐานต่อกันเป็นจังหวะที่ 1, 2, 3 ด้วยคีย์โจมตีเบา
- ถ้ากดคีย์โจมตีหนักระหว่างเบสิก2 จะแยกออกไปเป็นการโจมตีหนักได้
- ถ้ากดโจมตีหนักอีกครั้งหลังการโจมตีหนัก จะดีดศัตรูขึ้น (ดีดขึ้น)
- การโจมตีหนักคานเซิลด้วยการหลบได้ตลอดเวลา
- เบสิก3 ก็คานเซิลด้วยการหลบได้
- ทันทีหลังหลบ สามารถเริ่มเบสิก1 ใหม่ด้วยการโจมตีเบาได้
- เบสิก3 จะมีดาเมจ 1.5 เท่าก็ต่อเมื่อผ่านทั้งเบสิก1 และเบสิก2 แล้วเท่านั้น
รูปแบบสเปก:
- nodes(id, name, duration_frames), edges(from, to, input, window, type),
bonuses(on, requires_path, damage_multiplier)
- เอดจ์คานเซิลให้ระบุด้วย type: cancel
- ค่าเฟรมที่ฉันไม่ได้ให้ ห้ามนายกำหนดเอง ให้เว้นไว้เป็น TODO
บรรทัดสุดท้ายคือแก่น ระบุชัดว่าถ้าไม่ได้ให้ค่าเฟรมมา ก็ห้ามกุขึ้น นี่คือการบังคับมาตรฐาน v3 ข้อ 3 (ความซื่อตรงของตัวเลข) ในระดับพรอมต์
แปลงเป็นสเปกแล้ว ค่าเฟรมไม่ได้ถูกให้มา จึงเว้นไว้เป็น TODO
```yaml
character: warrior
combo_id: basic_chain
nodes:
- { id: basic_1, name: เบสิก1, duration_frames: TODO }
- { id: basic_2, name: เบสิก2, duration_frames: TODO }
- { id: basic_3, name: เบสิก3, duration_frames: TODO }
- { id: heavy, name: โจมตีหนัก, duration_frames: TODO }
- { id: launch, name: ดีดขึ้น, duration_frames: TODO }
- { id: dodge, name: หลบ, duration_frames: TODO }
edges:
- { from: basic_1, to: basic_2, input: light, window: TODO }
- { from: basic_2, to: basic_3, input: light, window: TODO }
- { from: basic_2, to: heavy, input: heavy, window: TODO }
- { from: heavy, to: launch, input: heavy, window: TODO }
- { from: heavy, to: dodge, input: dodge, window: TODO, type: cancel }
- { from: basic_3, to: dodge, input: dodge, window: TODO, type: cancel }
- { from: dodge, to: basic_1, input: light, window: TODO }
bonuses:
- { on: basic_3, requires_path: [basic_1, basic_2], damage_multiplier: 1.5 }
```
ส่วนที่ต้องยืนยัน:
1. "เริ่มเบสิก1 ใหม่ทันทีหลังหลบ" ใส่ไว้เป็นเอดจ์ dodge -> basic_1 แต่ยังไม่ชัด
ว่าเป็นการรับอินพุตระหว่างช่วงดีเลย์ท้ายท่าของการหลบ หรือหลังจากการหลบจบไปแล้วโดยสมบูรณ์
2. กฎไม่ได้ระบุว่ามีคอมโบกลางอากาศต่อจากการดีดขึ้น (launch) หรือไม่
ต้องยืนยันว่าการดีดขึ้นเป็นจุดจบของคอมโบหรือไม่
3. ยังไม่ชัดว่าจะวางการหลบเป็นโหนด หรือเป็นเพียงเอดจ์ (แอ็กชันคานเซิล) ตอนนี้วางไว้
เป็นโหนดก่อน โดยมองว่าตัวการหลบเองเป็นแอ็กชันที่มีระยะเวลากินเฟรม
มาประเมินกันตรงนี้ ข้อดี คือไม่กุค่าเฟรมขึ้นมา แต่เว้นไว้เป็น TODO ทั้งหมด เคารพข้อจำกัดของพรอมต์ และคำถามสามข้อที่ทิ้งไว้ตอนท้ายล้วนเป็นจุดที่คลุมเครือจริง ๆ ทั้งหมด โดยเฉพาะข้อ 1 (ไทมิงอินพุตของการเข้าใหม่) คือรูรั่วเดียวกันกับที่สมาชิกทีม B ตอบในห้องประชุมไม่ได้ AI หาเจอรูรั่วก่อนคนเสียอีก
จุดที่ยังก้ำกึ่ง คือข้อ 3 ที่วางการหลบเป็นโหนดนั้นเป็นเรื่องที่ความเห็นแยกกันได้ การหลบเป็นทั้ง "แอ็กชันคานเซิล" และในขณะเดียวกันก็เป็น "แอ็กชันที่มีระยะเวลากินเฟรม" จึงถูกทั้งสองด้าน การที่ AI เลือกด้านหนึ่งไว้แล้วรายงานว่ามันคลุมเครือนั้นซื่อตรง แต่นี่เป็นการตัดสินใจด้านการออกแบบ คนจึงต้องเป็นผู้กำหนด
ตอบคำถามสามข้อและปฏิเสธบางส่วน
window: [8, 18]cancels_recovery: true เพื่อระบุว่าเป็น "แอ็กชันที่ตัดดีเลย์ท้ายท่า" → รับมาบางส่วน + เพิ่มแอตทริบิวต์และมีอีกหนึ่งอย่างที่ ปฏิเสธ AI ไม่ได้ติด type: cancel ให้เอดจ์ dodge → basic_1 ซึ่งจริง ๆ นี่เป็นการตัดดีเลย์ท้ายท่าของการหลบแล้วเข้า จึงมีลักษณะเป็นคานเซิลตรงตามนั้น แต่ตรงนี้เรามองว่าเป็น "การเข้าตามปกติหลังหลบ" และวางไว้เป็นเอดจ์ทั่วไป เพราะการแยกระหว่างการคานเซิลดีเลย์ท้ายท่ากับการเชื่อมต่อตามปกติไม่ทำให้ฟีลของเกมต่างกันในตัวละครนี้ นี่เป็นตัวอย่างที่คนใช้การตัดสินเชิงโดเมนมาเขียนทับการจัดประเภทของ AI
ดี ต่อไปจงนำสิ่งเหล่านี้มาปรับแล้วออกสเปกฉบับสุดท้ายอีกครั้ง:
- เพิ่ม cancels_recovery: true ให้ dodge
- window ของเอดจ์ dodge -> basic_1 ให้เป็น [8, 18]
- ค่าเฟรมที่เหลือฉันยังไม่ได้ให้ จึงคง TODO ไว้ แต่ค่าตัวอย่างในกราฟข้างบน
(basic_1=21, basic_2=24, basic_3=30, heavy=33, launch=28, dodge=18)
ฉันจะใช้ ดังนั้นให้เติมด้วยค่าเหล่านั้น และให้ตรึงไว้ในคอมเมนต์ว่านี่เป็นค่าตัวอย่างที่ยังไม่ได้ตรวจสอบ
ผลลัพธ์ที่ออกมาจากคำขอใหม่นี้คือ YAML ใน 4.3.2 เราไม่ได้ทำให้เสร็จในครั้งเดียว พรอมต์ → ดิบ → ตรวจสอบ/ปฏิเสธ → ขอใหม่ วงจรนี้คือสิ่งที่สร้างความน่าเชื่อถือของสเปก AI ทำเครื่องหมายจุดที่คลุมเครือ และคนตัดสินใจด้วยความรู้เชิงโดเมน มีเพียงอย่างใดอย่างหนึ่งอย่างเดียวไม่ได้
เนื่องจากสเปกเป็นกราฟ การแจกแจงเส้นทางคอมโบจึงกลายเป็นปัญหาของ การค้นหาบนกราฟ เป็นการค้นหาแบบลึกก่อน (depth-first search, DFS) ทุกเส้นทางที่ออกจากโหนดเริ่มต้นไปจนถึงโหนดปลาย (หรือฟินิชเชอร์) คนทำสิ่งนี้ในหัวไม่ได้ แต่โค้ดทำได้ในชั่วพริบตา
ใน 95_BattleTF ซึ่งเป็นพื้นที่ทำงานแบบแยกของทีมผู้เขียน มีสคริปต์เล็ก ๆ ที่รับผิดชอบการแจกแจงนี้ มันอ่านสเปก YAML ดึงทุกเส้นทางออกมา แล้วตรวจสอบว่าแต่ละเส้นทางเป็นไปได้ตามกฎหรือไม่ (เอดจ์มีอยู่จริงไหม) ถ้าแสดงเฉพาะลอจิกแก่นกลางก็จะเป็นดังนี้
# 95_BattleTF/enumerate_paths.py (ตัดตอน)
import yaml
def load_graph(path):
spec = yaml.safe_load(open(path, encoding="utf-8"))
adj = {}
for e in spec["edges"]:
adj.setdefault(e["from"], []).append(e)
return spec, adj
def enumerate_paths(adj, start, max_depth=8):
results = []
def dfs(node, path, edges):
# ถ้าเป็นโหนดปลาย (ไม่มีเอดจ์ออก) หรือถึงขีดจำกัดความลึก ให้ปิดเส้นทาง
outs = adj.get(node, [])
if not outs or len(path) >= max_depth:
results.append((list(path), list(edges)))
return
for e in outs:
if e["to"] in path: # กันการวนซ้ำ: หนึ่งเส้นทางผ่านโหนดเดิมได้ครั้งเดียว
results.append((list(path), list(edges)))
continue
dfs(e["to"], path + [e["to"]], edges + [e])
dfs(start, [start], [])
return results
เมื่อรันโดยเริ่มจาก basic_1 เส้นทางที่มือไม่มีทางกางออกได้หมดก็หลั่งไหลออกมา ถ้าดูเพียงบางส่วนก็จะเป็นดังนี้
| # | เส้นทาง | หมายเหตุ |
|---|---|---|
| 1 | เบสิก1 → เบสิก2 → เบสิก3 | สามจังหวะมาตรฐาน ครบเงื่อนไขโบนัสฟินิชเชอร์ |
| 2 | เบสิก1 → เบสิก2 → โจมตีหนัก → ดีดขึ้น | คอมโบแยกกิ่ง |
| 3 | เบสิก1 → เบสิก2 → โจมตีหนัก → หลบ → เบสิก1 → … | การเข้าวนซ้ำ |
| 4 | เบสิก1 → เบสิก2 → เบสิก3 → หลบ → เบสิก1 → … | รีเซ็ตหลังฟินิชเชอร์ |
ข้อ 3 และ 4 สำคัญ เพราะเอดจ์เข้าใหม่จากการหลบทำให้คอมโบ วนซ้ำ เส้นทางวนซ้ำแบบนี้แหละคือสิ่งที่คนมองไม่เห็นบนไวต์บอร์ด ถ้าไม่ใส่การ์ดกันการวนซ้ำ (ผ่านโหนดเดิมในเส้นทางเดียวได้ครั้งเดียว) ลงใน DFS การแจกแจงก็จะตกลงในลูปไม่รู้จบ นี่เป็นกับดักที่ผู้เขียนรู้จริงตอนรันโค้ดครั้งแรกแล้วมันหยุดค้างไปจริง ๆ ครั้งหนึ่ง ถ้ากราฟมีการวนซ้ำ ตัวแจกแจงต้องมีการ์ดเสมอ
ผลผลิตของขั้นแจกแจงมีสองอย่าง อย่างแรก รายการทุกเส้นทางที่เป็นไปได้ตามกฎ อย่างที่สอง การตรวจจับความขัดแย้งของกฎ ถ้าในสเปกมีเอดจ์ dodge → basic_1 แต่กลับไม่มีนิยามของโหนด dodge ตัวแจกแจงจะจับได้ว่าเป็น "เอดจ์ที่ชี้ไปยังโหนดที่ไม่ได้นิยามไว้" ความผิดพลาดที่พบบ่อยที่สุดเวลาเขียนสเปกด้วยมือคือการอ้างอิงลอย (dangling reference) นี้
ลำพังรายการเส้นทางอย่างเดียวเราไม่รู้ว่า "เส้นทางไหนแรงเกินไป" ต้องเอาแต่ละเส้นทางใส่เข้าตัวจำลอง DPS (DPS = ดาเมจต่อวินาที) simulate_dps ของทีมผู้เขียนทำหน้าที่นี้ มันรับเส้นทาง (ลำดับโหนด) พร้อมดาเมจ·เฟรมของแต่ละโหนดและกฎโบนัส แล้วคำนวณดาเมจรวมและเฟรมรวมที่ใช้ จากนั้นออกเป็นดาเมจต่อวินาที (DPS)
# 95_BattleTF/simulate_dps.py (ตัดตอน, สมมติ 60fps)
def simulate(path_nodes, node_dmg, node_frames, bonuses):
total_dmg = 0
total_frames = 0
visited = []
for nid in path_nodes:
dmg = node_dmg.get(nid, 0)
# โบนัส: ถ้าผ่าน requires_path ครบทั้งหมด ให้ใช้ตัวคูณ
for b in bonuses:
if b["on"] == nid and all(r in visited for r in b["requires_path"]):
dmg *= b["damage_multiplier"]
total_dmg += dmg
total_frames += node_frames[nid]
visited.append(nid)
seconds = total_frames / 60.0
return {"dmg": total_dmg, "frames": total_frames,
"dps": round(total_dmg / seconds, 1) if seconds else 0}
ถ้าไหลผลการแจกแจงจาก 4.3.4 เข้ามาทั้งก้อนตรงนี้ DPS ของแต่ละเส้นทางก็จะหล่นออกมาเป็นตาราง ด้านล่างคือผลที่ได้จากการใส่ดาเมจของโหนดเป็นค่าตัวอย่าง (ตีพื้นฐาน 100, โจมตีหนัก 180, ดีดขึ้น 140 — ทั้งหมดเป็นค่ากำกับที่ยังไม่ได้ตรวจสอบ) แล้วรัน
| เส้นทาง | ดาเมจรวม | เฟรมรวม | DPS |
|---|---|---|---|
| เบสิก1→เบสิก2→เบสิก3 (ฟินิชเชอร์ ×1.5) | 100+100+150 = 350 | 75 | 280.0 |
| เบสิก1→เบสิก2→โจมตีหนัก→ดีดขึ้น | 100+100+180+140 = 520 | 106 | 294.3 |
| เบสิก1→เบสิก2→เบสิก3→หลบ→เบสิก1 | 350+0+100 = 450 | 144 | 187.5 |
ตารางนี้เปลี่ยนบทสนทนา สัญชาตญาณที่ว่า "กิ่งโจมตีหนักดูแรงกว่าสามจังหวะมาตรฐานนะ" เปลี่ยนเป็นตัวเลข "DPS เส้นทางโจมตีหนัก 294 vs มาตรฐาน 280 เหนือกว่า 5%" ถ้า 5% ที่เหนือกว่าเป็นไปตามเจตนาก็ผ่าน ถ้าไม่ใช่ก็เพิ่มเฟรมของการโจมตีหนักเพื่อลด DPS ลง เราตัดสินใจเรื่องนี้ในขั้นสเปก ก่อน ที่บิลด์จะออกมา
ถ้ามองเวิร์กโฟลว์ทั้งหมดในแผ่นเดียวก็จะเป็นดังนี้
flowchart LR
A["กฎภาษาธรรมชาติ
(teammate_b ไวต์บอร์ด)"] --> B["พรอมต์ → AI"]
B --> C{"สเปกดิบ
มี TODO·คำถาม"}
C -->|คนตรวจสอบ/ปฏิเสธ| D["สเปก YAML ฉบับยืนยัน
warrior_basic_chain.yaml"]
D --> E["enumerate_paths.py
แจกแจงทุกเส้นทางด้วย DFS"]
E --> F["ตรวจจับความขัดแย้งของกฎ
เอดจ์ลอย·วนซ้ำไม่รู้จบ"]
E --> G["simulate_dps.py
คำนวณ DPS รายเส้นทาง"]
G --> H["ตัดสินบาลานซ์
ส่วนต่าง DPS ระหว่างเส้นทาง"]
F --> D
H -->|ปรับเฟรม| D
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class E,F,G code;
class B,C ai;
class H human;
class A,D data;
สเปก (D) อยู่ตรงกลาง และการแจกแจง (E) การตรวจสอบ (F) การจำลอง (G) ก็แตกกิ่งออกไปจากตรงนั้น ถ้าจับความขัดแย้งได้หรือบาลานซ์ผิดเพี้ยน ก็ย้อนกลับมาที่สเปกเพื่อแก้ ไวต์บอร์ดไม่มีลูปนี้ คอมโบบนไวต์บอร์ดจึงรู้ว่าผิดก็ต่อเมื่อมันเข้าไปอยู่ในบิลด์แล้วเท่านั้น
ที่ผ่านมาเราได้ดูกราฟคอมโบและการแจกแจงเส้นทาง คานเซิลและคิวอินพุตคือมือจับสองอันที่ปรับกราฟนี้ ทิศทางของทั้งสองตรงข้ามกัน
คานเซิลเพิ่มเอดจ์ ทุกครั้งที่เพิ่มกฎคานเซิลหนึ่งข้อ จะมีเอดจ์มาติดที่กราฟ และจำนวนเส้นทางที่แจกแจงได้ก็พองตัวขึ้นแบบทวีคูณ ดังนั้นคานเซิลจึงไม่ใช่ "ยิ่งใจกว้างยิ่งดี" ยิ่งปลดคานเซิลออกมากเท่าไหร่ เส้นทางก็ยิ่งระเบิด และโอกาสที่เส้นทางแรงเกินคาด (อย่างเส้นทางวนซ้ำในหัวข้อก่อนหน้า) จะปนเข้ามาก็ยิ่งสูงขึ้น นี่คือเหตุผลที่ประเพณีเกมต่อสู้วางคานเซิลไว้อย่างเข้มงวด และที่เกมแอ็กชัน RPG วางไว้อย่างใจกว้าง แนวเกมเป็นตัวกำหนด "จำนวนเส้นทางที่จะอนุญาต" ไม่มีค่าหน้าต่างที่ถูกต้องแบบสัมบูรณ์
เวลาจัดการคานเซิล ต้องแยกระบุให้ชัดเสมอ ถ้าวางเป็น "คานเซิลอะไรก็ได้" แบบรวม ตัวแจกแจงจะสร้างเอดจ์คานเซิลระหว่างทุกโหนด ทำให้เส้นทางเพิ่มขึ้นจนควบคุมไม่ได้
| ประเภทคานเซิล | การเขียนในสเปก | ผลต่อเส้นทาง |
|---|---|---|
| Action Cancel | โหนดเฉพาะ → โหนดเฉพาะ, type: cancel | เพิ่มเฉพาะกิ่งที่เลือกได้ |
| Dodge Cancel | หลายโหนด → dodge, window: [0, dur] | เป็นทางออกได้แทบทุกโหนด |
| Guard Cancel | หลายโหนด → guard | เข้าสู่การป้องกัน โดยปกติจำกัดเฉพาะดีเลย์ท้ายท่า |
| No Cancel | ไม่มีเอดจ์ cancel ออก | ทำจนจบหลังเริ่ม (ซูเปอร์อาร์เมอร์) |
คิวอินพุตไม่ได้บีบเส้นทาง แต่ทำให้เหยียบได้จริง ถ้าไม่มีคิว ผู้เล่นต้องกะหน้าต่างอินพุตของแต่ละเอดจ์ (เช่น [12, 24]) ให้ตรงระดับเฟรม ซึ่งแทบเป็นไปไม่ได้ด้วยปฏิกิริยาของคน คิวจะเก็บอินพุตที่กดก่อนหน้าต่างไว้ในบัฟเฟอร์ แล้วลั่นออกโดยอัตโนมัติในวินาทีที่หน้าต่างเปิด นั่นคือคิวไม่ได้เปลี่ยนเส้นทางของกราฟ แต่ใส่รองเท้าให้คนเดินบนกราฟได้
input_queue:
window_start_ratio: 0.5 # เริ่มบัฟเฟอร์อินพุตถัดไปจากความคืบหน้าแอ็กชัน 50%
expire_frames: 10 # อายุที่ใช้ได้ของอินพุตที่ถูกบัฟเฟอร์
priority: latest # เมื่อมีอินพุตหลายตัวพร้อมกัน ให้ตัวสุดท้ายมาก่อน
ความสมดุลของสามพารามิเตอร์คือแก่น ถ้า window_start_ratio เล็กเกินไป อินพุตช่วงต้นแอ็กชันก็จะถูกบัฟเฟอร์ด้วย ทำให้แอ็กชันถัดไปที่ไม่ได้ตั้งใจเด้งออกมา ถ้า expire_frames สั้นเกินไป คิวก็หมดความหมาย กลับไปเรียกร้องความแม่นยำอีก แต่ถ้ายาวเกินไป อินพุตที่กดไปนานแล้วก็ลั่นออกมาช้า ๆ เกิดเหตุ "ทำไมจู่ ๆ ถึงขยับ" ค่าตั้งต้นที่แนะนำคือ expire_frames 5\~15 และ window_start_ratio ราว 0.5 แต่นี่เป็นเส้นเริ่มต้นที่ต้องปรับตามแนวเกมและความหนักหน่วงของตัวละคร ไม่ใช่คำตอบสำเร็จรูป
เรื่องเชิงปฏิบัติการอีกหนึ่งข้อ อย่าตั้งค่าพารามิเตอร์คิวอินพุตให้ต่างกันหมดในแต่ละตัวละคร ให้มีค่าเริ่มต้นแบบโกลบอลหนึ่งชุด แล้ว override เฉพาะตัวละครที่ความหนักหน่วงต่างเป็นพิเศษ (แบบบอสตัวยักษ์เป็นต้น) ถ้าจัดการค่าคิวของตัวละครยี่สิบตัวแยกกัน ก็จะแยกไม่ออกว่าค่าไหนเป็นความต่างที่ตั้งใจ และค่าไหนเป็นความผิดพลาด
สุดท้าย มีอีกหนึ่งเรื่องที่ต้องชี้ไว้ ที่ผ่านมาเราจัดการเอดจ์เพียงแค่ "มี/ไม่มี" แต่ถึงจะมีเอดจ์ การข้ามจากโหนดนั้นไปยังโหนดถัดไปจริง ๆ ทำอย่างไร ก็เป็นอีกการตัดสินใจหนึ่ง วิธีเชื่อมต่อระหว่างคอมโบมีกิ่งหลักอยู่สามแบบ ซึ่งอยู่นอกความลึกของหนังสือเล่มนี้จึงไม่จัดการด้วยโค้ด แต่ถ้าไม่กล่าวถึงเลย สเปกก็เท่ากับวาดไว้เพียงครึ่งเดียว
window: [10, 21]) ก็คือการแสดงออกของโนทิฟายนี้นั่นเอง ถ้าเลื่อนโนทิฟายมาเร็วขึ้น คอมโบก็จะเร็วขึ้น (ไปยังท่าถัดไปก่อนที่การตีจะจบ) ถ้าเลื่อนช้าลง แต่ละจังหวะก็จะหนักแน่นขึ้น นั่นคือค่าเริ่มต้นของหน้าต่างไม่ใช่แค่ตัวเลขธรรมดา แต่เป็นการตัดสินด้านสัมผัสว่า "จะโชว์การตีนี้จนจบ หรือจะรีบข้ามไปท่าถัดไป"ทั้งสามนี้ทำให้สัมผัสมือของเกมตรงข้ามกันโดยสิ้นเชิงแม้จะเป็นเอดจ์เดียวกัน ในขั้นสเปกให้กำหนดเพียงการมีอยู่ของเอดจ์และหน้าต่าง ส่วนวิธีเชื่อมต่อ (เบลนดิง vs เฟรมสกิป) มักจะกำหนดร่วมกับแอนิเมเตอร์ในขั้นบิลด์ อย่างไรก็ตามถ้าเว้นฟิลด์หนึ่งบรรทัดแบบ transition: blend / transition: skip ไว้ในสเปกล่วงหน้า ในขั้นบิลด์ก็ไม่ต้องมาถามซ้ำว่า "เอดจ์นี้ตกลงให้ข้ามแบบไหนนะ" วิธีเชื่อมต่อคือแกนที่สามที่ซ่อนอยู่ของกราฟคอมโบ
การตรวจสอบทั้งหมดมาจนถึงตรงนี้เกิดขึ้น บนสเปก การแจกแจงเส้นทาง การจำลอง DPS การตรวจจับความขัดแย้ง ทั้งหมดมีเป้าหมายเป็น YAML แต่ค่าเฟรมในสเปกเป็นค่าตามเจตนาของดีไซเนอร์ ไม่ใช่ค่าที่วัดจริงจากบิลด์ ความยาวจริงของมอนทาจที่แอนิเมเตอร์สร้าง เฟรมที่โนทิฟายในบิลด์ลั่นจริง หน้าต่างที่คิวอินพุตทำงานจริงในเอนจิน สิ่งเหล่านี้ต้องแคปเจอร์บิลด์มาวัด
การดึง 5 สัญญาณ (เฟรมที่การตีเกิดขึ้น, ดีเลย์ท้ายท่า, หน้าต่างคานเซิล, คิวอินพุต, ฮิตสต็อป) จากวิดีโอบิลด์โดยอัตโนมัติมีความยากในการอิมพลิเมนต์สูง สิ่งที่น่าเชื่อถือที่สุดในทางปฏิบัติคือเทเลเมทรีภายในเกม ให้ในบิลด์บันทึกล็อกว่า "แอ็กชันนี้รับอินพุตนี้ที่เฟรมนี้" แล้วเอาล็อกนั้นมาเทียบกับสเปก (การเปรียบเทียบวิธีแคปเจอร์ดูใน 4.4) ลูปการเทียบนี้คือหัวข้อของ 4.4 ถ้าหน้าต่างที่สเปกระบุว่า [12, 24] วัดได้ในบิลด์เป็น [14, 26] ก็แก้สเปกให้ตรงกับค่าที่วัดจริงในบิลด์
| ความผิดพลาด | ทำไมจึงอันตราย | วิธีหลีกเลี่ยง |
|---|---|---|
| เขียนคอมโบเป็นตารางเส้นตรง | การแตกกิ่ง·วนซ้ำซ่อนอยู่ระหว่างแถวจนตกหล่น | เขียนสเปกเป็นกราฟ (โหนด+เอดจ์) ไม่วาดด้วยมือ |
| รวมคานเซิลเป็น "อะไรก็ได้" | เส้นทางที่แจกแจงระเบิด มีเส้นทางแรงปนเข้ามา | แยกระบุ Action·Dodge·Guard Cancel ให้ชัด |
| ตัวแจกแจงไม่มีการ์ดกันวนซ้ำ | ลูปไม่รู้จบที่การเข้าใหม่จากการหลบ | การ์ดผ่านโหนดเดิมได้ครั้งเดียวต่อหนึ่งเส้นทาง |
| เข้าใจผิดว่าเฟรมในสเปกคือค่าที่วัดจริง | ค่าตามเจตนาต่างจากค่าในบิลด์ | ระบุว่าเป็นค่าตามเจตนา แก้ด้วยการแคปเจอร์บิลด์ (4.4) |
| ตั้งคิวอินพุตแยกตามแต่ละตัวละคร | แยกความต่างที่ตั้งใจ/ความผิดพลาดไม่ออก | ค่าเริ่มต้นโกลบอล + override เฉพาะบางตัว |
| เชื่อค่าเฟรมที่ AI เติมตามนั้นเลย | ตัวเลขที่กุขึ้นถูกป้อนเข้าสเปก | บังคับด้วยพรอมต์ "ค่าที่ไม่ได้ให้คือ TODO" |
นี่คือขั้นตอนขั้นต่ำที่คุณลองรันตามด้วยมือได้ ขอเพียงมีไพทอนและ pyyaml ก็พอ
setup. สร้างโฟลเดอร์ทำงานหนึ่งโฟลเดอร์ แล้ววางไฟล์สเปกและสคริปต์สองตัวไว้ในนั้น
combo-mini/
warrior_basic_chain.yaml # สเปกจาก 4.3.2
enumerate_paths.py # ตัวแจกแจง DFS จาก 4.3.4
simulate_dps.py # ตัวจำลองจาก 4.3.5
หลัง pip install pyyaml ให้แปะ YAML จาก 4.3.2 ลงในไฟล์สเปกตามนั้นเลย
prompt. ขั้นเปลี่ยนกฎภาษาธรรมชาติเป็นสเปกให้ฝากไว้กับ AI ใช้พรอมต์จาก 4.3.3 ตามนั้น แต่ต้องใส่ข้อจำกัดท้ายสุดเข้าไปด้วยเสมอ
ค่าเฟรมที่ฉันไม่ได้ให้ ห้ามนายกำหนดเอง ให้เว้นไว้เป็น TODO
เอดจ์คานเซิลให้ระบุด้วย type: cancel และส่วนที่คลุมเครือให้แยกออกมาเป็นคำถามต่างหาก
สองบรรทัดนี้กันการกุตัวเลขและการตัดสินตามอำเภอใจของ AI จากสเปกที่ออกมาให้คนเป็นผู้เติม TODO และรายการคำถาม
verify. เมื่อสเปกเสร็จแล้วให้ตรวจสอบสองครั้ง
python enumerate_paths.py warrior_basic_chain.yaml # ทุกเส้นทาง + ความขัดแย้ง
python simulate_dps.py warrior_basic_chain.yaml # ตาราง DPS รายเส้นทาง
จากผลการแจกแจง ให้ตรวจว่า (1) เส้นทางวนซ้ำไม่เพิ่มขึ้นแบบไม่รู้จบหรือไม่ (2) ไม่มีเอดจ์ลอยที่ชี้ไปยังโหนดที่ไม่ได้นิยามไว้หรือไม่ จากผลของ DPS ให้ดูว่าส่วนต่างระหว่างเส้นทางอยู่ในช่วงที่ตั้งใจหรือไม่ ถ้าส่วนต่างมาก ให้แก้ค่าเฟรม/ดาเมจของสเปกแล้วรันใหม่
ถ้าไม่มีเวลาสร้างเครื่องมือต่างหาก ก็จบการเขียนสเปกและการแจกแจงเส้นทางด้วยบทสนทนา AI ครั้งเดียวเลย ให้กฎภาษาธรรมชาติไปแล้วใส่ลงในพรอมต์เดียวว่า "เปลี่ยนเป็นสเปกคอมโบ YAML แล้วไล่เรียงทุกเส้นทางที่เป็นไปได้จากโหนดเริ่มต้นแบบลึกก่อนให้หมด การวนซ้ำให้ตัดให้วนแค่ครั้งเดียว ถ้ามีเอดจ์ที่ชี้ไปยังโหนดที่ไม่ได้นิยามไว้ให้ทำเครื่องหมายไว้" AI จะทำการแปลงเป็นสเปก·การแจกแจง·การตรวจจับความขัดแย้งให้ในครั้งเดียว ส่วนการจำลอง DPS ถ้าให้ดาเมจของโหนดมาเป็นตารางด้วย ก็รับได้ด้วยคำขอตามมาว่า "คำนวณดาเมจรวมและเฟรมของแต่ละเส้นทางแล้วทำเป็นตารางให้" ความแม่นยำจะลดลง แต่ก็มองได้ไกลกว่าไวต์บอร์ดมาก แก่นไม่เปลี่ยน — อย่ากางคอมโบในหัว แต่ให้แจกแจงออกมาแล้วดู
บิลด์ #234 ของทีม TF การต่อสู้เพิ่งขึ้นมาหมาด ๆ เป็นบิลด์แรกที่ได้สัมผัสสกิลใหม่ skill_thunder ในเอกสารระบุว่า hit timing อยู่ที่ 150ms ผมลองกดอินพุตดู ความรู้สึกที่ปลายนิ้วบอกว่า ช้า ช้าแน่ ๆ ผมเรียกเพื่อนร่วมทีม A ที่นั่งข้าง ๆ ว่า "อันนี้มันดูลอย ๆ หน่อยไหม" เพื่อนร่วมทีม A ลองกดสองสามที "อืม… ก็เหมือนจะใช่อยู่" ทั้งคู่ไม่มั่นใจ เอกสารบอกว่า 150 แต่มือยืนยันว่าราว ๆ 200 ใครกันแน่ที่ถูก ศึกระหว่างปลายนิ้วกับกระดาษ ในบิลด์ถัดไปก็จะมีใครสักคนพูดอีกว่า "รู้สึกว่าก็โอเคนะ" และเพียงคำพูดประโยคเดียวนั้น บิลด์อีกหนึ่งบิลด์ก็ไหลผ่านไป
เป้าหมายของบทนี้คือการยุติศึกนั้น ถ้าปลายนิ้วบอกว่า 200 ก็แสดงให้เห็นด้วยตัวเลขว่ามันเป็น 200 จริงหรือไม่ และก่อนที่บิลด์จะขึ้นมา ให้รู้ล่วงหน้าเพียงแค่ดูจากเอกสารว่า "สกิลนี้มี DPS สูงกว่าเป้าหมาย 30%" แม้แต่ในสมัยที่ผมจัดการการต่อสู้ของเกม AAA MMORPG ที่มีคนทำงานร่วมกัน 200 คนตั้งแต่ช่วงต้น ความอึดอัดเมื่อความรู้สึกกับตัวเลขไม่ตรงกันก็เหมือนเดิม สิ่งที่เปลี่ยนไปคือ ตอนนี้เรามีเครื่องมือที่จะปิดความไม่ตรงกันนั้นด้วยตัวเลขอยู่ในมือแล้วเท่านั้นเอง
ถ้า 4.2 และ 4.3 พูดถึงว่าจะ เขียน เอกสารการต่อสู้อย่างไร 4.4 ก็พูดถึงว่าเอกสารนั้น ทำงาน ตามที่ตั้งใจไว้หรือไม่ การตรวจสอบมีสองแกน แกนหนึ่งคือการจำลอง (simulation) ที่ตรวจสอบด้วยการคำนวณล้วน ๆ โดยไม่ต้องมีบิลด์ อีกแกนหนึ่งคือการวิเคราะห์การจับภาพ (capture analysis) ที่ดึงค่าที่วัดได้ออกมาจากวิดีโอบิลด์จริง เมื่อสองแกนถูกผูกเข้าเป็นรอบเดียวกัน จำนวนรอบของการออกแบบการต่อสู้ก็ลดจากหน่วยวันลงมาเหลือหน่วยชั่วโมง
ขอบอกข้อสรุปก่อน หัวใจของบทนี้คือ การวางตัวเลขที่ระบุในเอกสาร (150ms) กับตัวเลขที่วัดได้จากบิลด์ (220ms) ไว้เคียงกันแล้วอ่านความต่างนั้น (4.4.5) ส่วนหัวข้อก่อนหน้า (ตัวจำลอง·การแจกแจงคอมโบ) ให้อ่านเป็นขั้นเตรียมการที่ทำให้การเปรียบเทียบนั้นเป็นไปได้
เมื่อนักออกแบบการต่อสู้ต้องการตรวจสอบสกิลใหม่หนึ่งสกิล จะเกิดอะไรขึ้นบ้าง
นักออกแบบเขียนเอกสาร โปรแกรมเมอร์ใส่ข้อมูล อาร์ทิสต์เพิ่มโมชันและเอฟเฟกต์ บิลด์รัน QA ตรวจหนึ่งรอบ แล้วนักออกแบบถึงจะได้สัมผัสด้วยมือเอง เร็วก็สองวัน ปกติสามถึงสี่วัน หากพบ ที่ปลายรอบ ว่า "DPS สูงเกินไป" การค้นพบนั้นก็กลายเป็นคำสั่งให้ย้อนกลับไปเริ่มต้นใหม่ สามถึงสี่วันอีกหนึ่งรอบ
การจำลองคือเครื่องมือที่ให้คำตอบใน ขั้นแรกสุด ของรอบนี้ คำนวณดูจากเอกสารเพียงอย่างเดียว ถ้าคำตอบไม่ดีก็แก้เอกสารแล้วคำนวณใหม่ ก่อนเข้าสู่ขั้นบิลด์ซึ่งมีต้นทุนสูง ตัวเอกสารเองจะถูกกรองหนึ่งครั้ง เหมือนการเอารถจำลองเข้าอุโมงค์ลม (wind tunnel) ก่อน ก่อนที่จะนำรถจริงขึ้นถนน การออกแบบที่น่าสงสัยจะถูกคัดทิ้งบนโต๊ะทำงาน
แน่นอนว่าอุโมงค์ลมไม่ได้ทำนายถนนได้ 100% ด้วยเหตุนี้จึงต้องมีแกนที่สองคือการวิเคราะห์การจับภาพ ถ้าการจำลองคือ คำตอบในอุดมคติ การจับภาพก็คือ คำตอบที่เกิดขึ้นจริงในบิลด์ การวางทั้งสองไว้เคียงกันแล้วอ่านความต่าง — นั่นคือทั้งหมดของบทนี้
ซูโดโค้ดที่เป็นนามธรรมไม่สามารถตรวจสอบอะไรได้เลย ด้วยเหตุนี้จึงสร้างโค้ดที่ รันได้ ขึ้นมาตั้งแต่ต้น ด้านล่างคือโครงสร้างหลักของ simulate_dps.py ที่ผมใช้ในทีม TF การต่อสู้ ซึ่งเรียบเรียงใหม่เพื่อนำมาลงในหนังสือโดยตัดข้อมูลของบริษัทออก มันรันด้วยไลบรารีมาตรฐานของ Python เพียงอย่างเดียวโดยไม่มี dependency (ไฟล์ทั้งหมดดูได้ใน「ลองทำดู」)
อินพุตนั้นเรียบง่าย สกิลหนึ่งสกิลคือ dataclass ที่มี damage·cast_sec (เวลาครองการร่าย)·cooldown_sec·resource_cost ส่วนตัวละครมีปริมาณทรัพยากรรวม·ปริมาณฟื้นฟูต่อวินาที·รายการสกิล·ลำดับการหมุนเวียนตามความสำคัญ ตัวหลักที่รันอยู่บนนั้นมีเพียงกฎละโมบ (greedy) ที่เรียบง่ายว่า "ในทุกขณะ ให้ใช้สกิลที่มีลำดับความสำคัญสูงสุดในบรรดาสกิลที่ใช้ได้" เป้าหมายคือการหา ขีดสูงสุดในอุดมคติ ที่ไม่ฉลาดและไม่โง่ไปกว่าผู้เล่นจริง เมื่อตัดเฉพาะส่วนที่เป็นกระดูกสันหลังออกมาก็ได้ดังนี้
# สร้างไทม์ไลน์ด้วยติ๊ก 0.05 วินาที ถ้าไม่ได้อยู่ระหว่างร่าย ให้ใช้สกิลที่ใช้ได้ตัวแรกตามลำดับความสำคัญ
while t < duration_sec:
resource = min(char.max_resource, resource + char.resource_regen * tick)
for name in cooldowns:
cooldowns[name] = max(0.0, cooldowns[name] - tick)
if t >= busy_until: # ถ้าโมชันร่ายยังไม่จบ ให้รอ
for name in char.rotation: # ตามลำดับความสำคัญ
s = skill_by_name[name]
if cooldowns[name] <= 0 and resource >= s.resource_cost:
total_damage += s.damage
resource -= s.resource_cost
cooldowns[name] = s.cooldown_sec
busy_until = t + s.cast_sec # ใช้สกิลถัดไปไม่ได้จนถึงเวลานี้
break
t += tick
# …(นิยาม dataclass·อินพุต warrior·ลูปเอาต์พุต ดูโค้ดทั้งหมดใน「ลองทำดู」)
เมื่อตั้ง skill_thunder (ดาเมจ 420·ร่าย 0.9s·คูลดาวน์ 6s) ไว้เป็นลำดับที่ 1 ให้ warrior แล้ววาง skill_dash·basic_1 ไว้ข้างหลัง รัน 20 วินาที ได้ผลลัพธ์ดังนี้ (python simulate_dps.py):
평균 DPS: 261.0
t= 0.0s skill_thunder 자원=60
t= 0.9s skill_dash 자원=47
t= 1.3s basic_1 자원=50
t= 1.6s basic_1 자원=53
t= 1.9s basic_1 자원=55
...
ค่านี้มีความหมายอย่างไร มันหมายความว่าเรารู้ว่าขีดสูงสุด DPS ในอุดมคติของ warrior อยู่ที่ราว 261 ได้ โดยไม่ต้องมีบิลด์ ภายใน 1 วินาที หากเป้าหมาย DPS คือ 180 เอกสารนี้ก็สูงเกินไป +45% ไม่ต้องรอบิลด์ แค่ปรับ damage หรือ cooldown_sec ตอนนี้ได้เลย
ขอเขียนข้อจำกัดไว้อย่างตรงไปตรงมาด้วย ตัวจำลองนี้ไม่ได้สะท้อนความผิดพลาดในการอินพุตของผู้เล่น·ช่องว่างจากการเคลื่อนที่·การหลบ·การรบกวนจากศัตรู ด้วยเหตุนี้ค่าที่วัดได้จึงสูงกว่าบิลด์จริงเสมอ นี่ไม่ใช่บั๊ก แต่เป็นนิยามของตัวจำลองในฐานะ เส้นขีดสูงสุด นั่นเอง ช่องว่างกับค่าที่วัดจริงจะถูกเติมด้วยการจับภาพใน 4.4.5
โน้ตการใช้ AI โครงสร้างข้างต้นผมเป็นคนเขียนเอง แต่เมื่อจะเพิ่มโมเดลทรัพยากรใหม่ (เช่น โครงสร้างที่เกจความโกรธจะเพิ่มขึ้นเมื่อได้รับดาเมจ) ให้ร้องขอกับ Claude โดย อ้างอิงโค้ดที่มีอยู่ เช่น "ช่วยเพิ่มกฎเมื่อโดนโจมตี ความโกรธ +5 ใน simulate_dps นี้ ภายในลูป tick โดยใช้ตัวแปรแยกต่างหากจากการฟื้นฟูทรัพยากรเดิม" หากสั่งให้สร้างตัวจำลองทั้งก้อนขึ้นมาจากกระดาษเปล่า จะได้โค้ดที่ตรวจสอบไม่ได้ออกมา กระดูกสันหลังให้คนจับ ส่วน AI ก็แตกกิ่งก้านออกไป
ตัวเลข DPS เพียงตัวเดียวยังไม่พอ หากจะตรวจสอบว่า "คอมโบใดคือคอมโบหลักที่ตั้งใจไว้" ก็ต้องกาง เส้นทางที่เป็นไปได้ทั้งหมด ออกมาดู หากวาดทรีด้วยมือ แค่ 7\~8 โหนดหัวก็ระเบิดแล้ว การกางเส้นทางให้ครบถ้วนไม่ตกหล่นนั้น เครื่องทำได้ดีกว่าคนอย่างถล่มทลาย — แต่ต้องสั่งให้ดึงออกมาในรูปแบบที่คนสามารถย้อนทวนผลลัพธ์ได้
ต่อไปนี้คือโค้ดที่รับกราฟคอมโบ แจกแจงทุกเส้นทาง แล้วเรียงตาม DPS combo_graph คือการเขียน "หลังจากแอ็กชันใดสามารถ cancel ไปยังแอ็กชันใดได้" เป็น adjacency list ซึ่งถูกดึงออกมาจากเอกสาร state machine ของ 4.3 โดยตรง หัวใจคือเจเนอเรเตอร์ all_paths ที่กางด้วยการเรียกซ้ำ (recursion) จนถึงปลายทางตัน
# enumerate_combos.py — กางทุกเส้นทางของกราฟคอมโบแล้วเรียงตาม DPS
combo_graph = {"start": ["A"], "A": ["B", "D"], "B": ["C", "E"], "D": ["C"], "C": [], "E": []}
action_stats = { # (ดาเมจ, เวลาที่ใช้ หน่วยวินาที)
"A": (300, 0.8), "B": (450, 1.0), "C": (450, 1.2), "D": (600, 1.4), "E": (200, 0.6),
}
def all_paths(node="start", path=None):
path = (path or [])
nexts = combo_graph.get(node, [])
if not nexts: # ปลายทางตัน = คอมโบที่สมบูรณ์
yield [n for n in path if n in action_stats]
return
for nxt in nexts:
yield from all_paths(nxt, path + [nxt])
results = []
for p in all_paths():
dmg = sum(action_stats[a][0] for a in p)
dur = sum(action_stats[a][1] for a in p)
results.append((p, dmg, round(dur, 1), round(dmg / dur, 1)))
for p, dmg, dur, dps in sorted(results, key=lambda r: -r[3]):
print(f"{' → '.join(p):<18} {dmg:>5} dmg {dur:>4}s DPS {dps}")
ผลการรัน:
A → D → C 1350 3.4s DPS 397.1
A → B → C 1200 3.0s DPS 400.0
A → B → E 950 2.4s DPS 395.8
สัญญาณที่นักออกแบบควรอ่านจากตรงนี้ไม่ใช่แค่อันดับหนึ่งเรียบ ๆ แต่คือการที่ DPS ของสามเส้นทางอยู่ที่ 396\~400 แทบจะติดกัน — นี่คือสัญญาณว่า "ใช้คอมโบไหนประสิทธิภาพก็พอ ๆ กัน จึง ไม่มีอัตลักษณ์ของคอมโบหลัก" หากเจตนาคือ "A→D→C ควรเป็นคอมโบหลักแบบเสี่ยงสูงผลตอบแทนสูง" ก็ต้องเพิ่มดาเมจของ D หรือลดเวลาลงเพื่อดัน DPS ให้สูงขึ้นอีกขั้น ถึงเวลากลับไปที่เอกสารแล้ว
ในจุดที่การแจกแจงอัตโนมัตินี้เข้ามาแทนการคำนวณด้วยมือ แม้โหนดคอมโบจะเพิ่มเป็น 20 โหนด คนก็แค่อ่านตารางที่เรียงไว้แล้วเท่านั้นพอ
มาถึงแกนที่สอง เป็นขั้นวัดว่าในบิลด์เกิดอะไรขึ้นจริง ๆ คนมักนึกถึง "ให้ AI ดูวิดีโอบิลด์แล้ววิเคราะห์อัตโนมัติ" แต่ตรงนี้ขอแบ่งแยกแขนงอย่างตรงไปตรงมา เส้นทางสู่การได้ค่าที่วัดได้มีสามทาง และต้นทุน·ความแม่นยำของทั้งสามต่างกันมาก
แนวทาง A (วิเคราะห์พิกเซลวิดีโอ) ฟังดูน่าดึงดูด เป็นภาพที่ดึงห้าสัญญาณ — การแสดงอินพุต·การเปลี่ยนโมชันตัวละคร·เฟรมแรกของเอฟเฟกต์·รูปคลื่นเสียง·UI ตัวเลขดาเมจ — ออกมาจากหน้าจออัตโนมัติ แต่เมื่อลองสร้างจริง ความคลาดเคลื่อน ±1\~2 เฟรมจะมาเป็นพื้นฐานเพราะ noise จากการบีบอัดเฟรม·UI บัง·motion blur ที่ 60fps หนึ่งเฟรมคือราว 16.7ms ในการตรวจสอบที่นับ hit timing เป็นหน่วย ms นั้น noise ±33ms ถือเป็นเรื่องร้ายแรง ความยากในการสร้างสูงมาก แต่ความแม่นยำไม่คุ้มกับความพยายามนั้น
ด้วยเหตุนี้คำตอบในความเป็นจริงคือแนวทาง C คือ log telemetry ในเกม เอนจิน รู้เวลาที่แม่นยำอยู่แล้วภายใน ทั้งเวลาที่รับอินพุต·เวลาที่ animation notify เกิด·เวลาที่ VFX สปอว์น·เวลาที่ดาเมจถูกใช้ แทนที่จะอนุมานเวลานั้นจากพิกเซล ก็แค่ทำให้มันถูกบันทึกเป็น log หนึ่งบรรทัด แทนที่จะ กู้คืน 100ms จากพิกเซล ก็ รับเอา 100ms ที่เอนจินรู้มาจดไว้ตรง ๆ
// เพิ่มหนึ่งบรรทัดในโค้ดประมวลผลแอ็กชันการต่อสู้ (ตัวอย่างซูโด UE C++)
// เรียก logger ตัวเดียวกัน ณ จุดที่รับอินพุต / จุดที่ใช้ดาเมจ
CombatTelemetry::Log("input", SkillName, GetWorld()->GetTimeSeconds());
CombatTelemetry::Log("hit", SkillName, GetWorld()->GetTimeSeconds());
logger จะปล่อยออกมาทีละบรรทัดเป็น JSON Lines
{"event":"input","skill":"skill_thunder","t":12.340}
{"event":"hit", "skill":"skill_thunder","t":12.560}
{"event":"input","skill":"basic_3","t":14.100}
{"event":"hit", "skill":"basic_3","t":14.166}
ความต่างของเวลาระหว่าง input กับ hit ก็คือ hit timing ที่วัดได้นั่นเอง 12.560 − 12.340 = 0.220 วินาที = 220ms ไม่ใช่ ±33ms ของการวิเคราะห์พิกเซล แต่เป็นค่าเวลาของเอนจินตรง ๆ การดึง log นี้มาแล้วเทียบกับเอกสารคือเนื้อหาของหัวข้อถัดไป
ตอนนี้นำสองแกนมารวมไว้ที่เดียวกัน เอกสารสัญญา 150ms ส่วน telemetry วัดได้ 220ms +70ms ปลายนิ้วถูก เราจะติดตามกระบวนการบีบ สาเหตุ ของช่องว่างนี้ไปกับ AI จนจบ โดยไม่สรุปย่อ แต่ลงไว้ทั้งหมดแม้กระทั่งฉากที่ AI เดาผิดทางในตอนแรก
เอกสารระบุ hit timing 150ms แต่ค่าวัดคือ 220ms แนบ log ด้านล่างแล้วถามด้วยความยาวเท่าที่คนพิมพ์จริง คำขอสั้น ๆ ที่เจตนาชัดเจนดีกว่าความเป็นทางการที่เยิ่นเย้อ — หนึ่งบรรทัดที่ให้เขียนถึงวิธีตรวจสอบด้วย กับอีกหนึ่งบรรทัดที่ห้ามฟันธง คือกระดูกสันหลังของพรอมต์นี้
hit timing ของ skill_thunder ระบุในเอกสาร 150ms แต่ค่าวัด 220ms เกิน +70ms จาก telemetry ด้านล่าง ช่วยแยกสาเหตุที่เป็นไปได้ และเขียนด้วยว่า แต่ละสาเหตุยืนยัน/ตัดออกได้ด้วย log ใด อย่าฟันธง ถ้าไม่แน่ใจให้เขียนว่า 'ยังไม่ยืนยัน' // "วิธีตรวจสอบด้วย"·"ห้ามฟันธง" สองอย่างนี้คือเจตนาหลัก ความเป็นทางการที่เหลือจะละไว้ผลลัพธ์ก็เหมือนเดิม
{"event":"input","skill":"skill_thunder","t":12.340} {"event":"anim_start","skill":"skill_thunder","clip":"cast_thunder","t":12.340} {"event":"anim_notify","skill":"skill_thunder","notify":"HitWindow","t":12.560} {"event":"hit","skill":"skill_thunder","t":12.560}ตามเอกสาร notify HitWindow ของ cast_thunder อยู่ที่จุด 0.150s
ผมประมาณสาเหตุของการเกิน +70ms เป็นสี่ข้อดังนี้
- เลเทนซีเครือข่าย (network latency) อินพุตอาจวิ่งไป-กลับเซิร์ฟเวอร์แล้วเพิ่มมา 70ms หากเป็นบิลด์ multiplayer ให้ตรวจ RTT
- เฟรมเรตตก หากบิลด์รันที่ 30fps หนึ่งเฟรมคือ 33ms ทำให้ timing ของ notify ถูกปัดเข้าขอบเฟรมและเกิดความล่าช้าสะสม
- ความยาวโมชันร่ายเปลี่ยน คลิป cast_thunder อาจยาวกว่าในเอกสาร ทำให้ notify HitWindow ถูกเลื่อนไปข้างหลัง
- บัฟเฟอร์อินพุตล่าช้า คิวอินพุตอาจถูกประมวลผลช้าไปหนึ่งติ๊ก
สาเหตุที่น่าจะเป็นไปได้มากที่สุดคือ ข้อ 1 เลเทนซีเครือข่าย 70ms เป็นช่วง RTT ที่พบได้ทั่วไป
หยุดตรงนี้แล้วกลับไปดู log อีกครั้ง การประมาณอันดับ 1 ของ AI (เครือข่าย) นั้น ถูกตัดออกด้วย log นี้อยู่แล้ว input กับ anim_start ถูกบันทึกที่เวลา 12.340 ตรงกันเป๊ะ หมายความว่าโมชันเริ่มทันทีในวินาทีที่อินพุตเข้ามา ไม่มีช่องว่างให้การวิ่งไป-กลับเครือข่ายแทรกได้เลย ข้อ 1 ผิด
ข้อ 2 (เฟรมเรต) ก็อ่อน หากเป็น 30fps ควรจะเห็นความกระท่อนกระแท่นเป็นหน่วย 33ms แต่ input→hit อยู่ที่ 0.220 อย่างเรียบสะอาด นี่ไม่ใช่การปัดเข้าขอบเฟรม แต่กลิ่นของ ตำแหน่งคงที่ภายในคลิป
เบาะแสชี้ขาดอยู่ที่อื่น เวลาของ anim_notify อยู่ที่ +0.220 จากฐาน anim_start เอกสารบอกว่า HitWindow ควรอยู่ที่ 0.150 หลังคลิปเริ่ม แต่ในคลิปจริงถูกตรึงอยู่ที่จุด 0.220 นั่นคือ คลิปเองถูกสร้างต่างจากเอกสาร หรือตำแหน่ง notify ถูกย้ายจาก 0.150 ไป 0.220 สาเหตุไม่ใช่เครือข่ายและไม่ใช่เฟรมเรต แต่เป็น ข้อ 3 ข้อมูลโมชัน/notify AI ใส่ตัวเลือกที่เป็นคำตอบไว้ในสี่ข้อ แต่จัดอันดับกลับหัวกลับหาง
ให้คนเติมเหตุผลในการตัดออกด้วยมือแล้วโยนกลับไปอีกครั้ง ไม่ต้องอธิบายยาว แค่ตอกสั้น ๆ ว่าตัดอะไรออก และเหลือข้อเท็จจริงใดไว้
ตัดเครือข่าย·เฟรมเรตออก (เหตุผล: input=anim_start เวลาตรงกัน จึงไม่มีดีเลย์อินพุต→โมชัน / input→hit อยู่ที่ 0.220 คงที่ไม่มี noise จากเฟรม) ข้อเท็จจริงที่เหลือ: anim_notify อยู่ที่ anim_start +0.220 เอกสารคือ +0.150 ใช้แค่ข้อเท็จจริงนี้บีบสาเหตุ และบอกว่าจะร้องขออะไรกับโปรแกรมเมอร์·อนิเมเตอร์ ใน หน่วยการกระทำ // หัวใจคือ "ในหน่วยการกระทำ" — ให้จบที่ว่าจะร้องขอใครให้ทำอะไร ไม่ใช่การคาดเดา
จากเหตุผลที่ให้มา สาเหตุถูกบีบลงเหลือ การที่ notify HitWindow ของคลิป animation cast_thunder ถูกวางไว้ที่ 0.220s ไม่ใช่ 0.150s เหลือสองแขนง
- (ก) notify อยู่ที่ 0.150 ถูกต้อง แต่ทั้งคลิปช้าลง — หากความเร็วเล่น (play rate) ต่ำกว่าในเอกสาร หรือความยาวคลิปยืดขึ้น จุด 0.150 จะถูกยืดออกเป็น 0.220 ในเวลาจริง
- (ข) ตำแหน่ง notify เองถูกย้ายไป 0.220 — ความยาวคลิปเท่าเดิม แต่ marker ถูกย้ายเท่านั้น
การตรวจที่แยกสองแขนงนี้: 1. ขอจากอนิเมเตอร์: ขอหมายเลขเฟรมของ notify HitWindow ของ cast_thunder และจำนวนเฟรมรวมของคลิป พร้อมประวัติการเปลี่ยนแปลงเทียบกับค่าตอนเขียนเอกสาร หาก marker ถูกย้ายก็เป็น (ข) 2. ขอจากโปรแกรมเมอร์: ตรวจว่า montage play rate ของ skill_thunder เป็น 1.0 หรือไม่ และสแตตความเร็วโจมตีของตัวละครถูกคูณเข้ากับคลิปนี้หรือไม่ หาก play rate เป็น 0.68 (=0.150/0.220) ก็เป็น (ก) 3. วัดช่วง anim_start→anim_notify อีกครั้งที่ความเร็วโจมตีของตัวละครค่าอื่น หากช่วงเปลี่ยนตามความเร็วโจมตีก็เป็น (ก) หากไม่เปลี่ยนก็เป็น (ข)
มาตรการที่แนะนำ: หากยืนยันเป็น (ข) ให้ย้าย notify กลับไปที่ 0.150 หรืออัปเดตเอกสารเป็น 0.220 (เลือกฝั่งที่ตั้งใจไว้) หากยืนยันเป็น (ก) ให้ตรวจ logic การใช้ play rate
เอาต์พุตครั้งนี้น่ารับมาใช้ ไม่ได้ฟันธงการคาดเดา แต่แยกออกเป็นสองแขนง (ก)/(ข) และเขียน วิธีแยกแต่ละแขนงด้วยข้อมูล กับ จะร้องขอใครให้ทำอะไร ไว้ในหน่วยการกระทำ โดยเฉพาะการตรวจข้อ 3 (เปลี่ยนความเร็วโจมตีแล้ววัดใหม่) คือจุดแยกชี้ขาดที่คนมักพลาด หากนำรายงานนี้ไปเข้าประชุมตรง ๆ การประชุมก็จะไม่ใช่เวทีถกว่า "สาเหตุคืออะไร" แต่กลายเป็นเวที "ยืนยัน (ก) หรือ (ข) ภายใน 30 นาทีแล้วเลือกมาตรการ"
หัวใจที่บันทึกเซสชันจริงนี้แสดงให้เห็นคือข้อเท็จจริงที่ว่า AI ไม่ได้ให้คำตอบที่ถูกต้องตั้งแต่ต้น เอาต์พุตครั้งที่ 1 เป็นการเดาผิดทางที่ชี้เครือข่ายเป็นอันดับ 1 ต่อเมื่อมี การตรวจสอบของคนที่อ่าน log แล้วตัดตัวเลือกออก เข้ามาแทรก การวิเคราะห์จึงลู่เข้าสู่คำตอบที่ถูกต้อง AI กางตัวเลือกออกให้กว้าง ส่วนคนเป็นผู้บีบให้แคบ การแบ่งงานนี้คือวิธีวิทยาของทั้ง 4.4
หากการจำลอง (4.4.2\~4.4.3) กับการวิเคราะห์การจับภาพ (4.4.4\~4.4.5) แยกกันรัน ก็ได้ค่าเพียงครึ่งเดียว เมื่อผูกรวมเป็นหนึ่ง ลูปต่อไปนี้ก็ถูกสร้างขึ้น
flowchart TD
A["เขียนเอกสาร
(นักออกแบบ, แบบฟอร์ม 4.2·4.3)"] --> B["การจำลอง
simulate_dps · แจกแจงคอมโบ
(อัตโนมัติ, 1 วินาที)"]
B -->|"DPS·คอมโบผิดปกติ"| A
B -->|"เอกสารผ่าน"| C["บิลด์
(โปรแกรมเมอร์·อาร์ทิสต์, หลายวัน)"]
C --> D["รวบรวม log telemetry
(input·hit·anim_notify)"]
D --> E["เทียบเอกสาร vs ค่าวัดอัตโนมัติ
(ตรวจความต่างแบบ 150ms vs 220ms)"]
E -->|"รายการเกินค่าเกณฑ์"| F["รายงานประมาณสาเหตุโดย AI
(กางตัวเลือก → คนบีบให้แคบ)"]
F --> G["นักออกแบบตรวจ → ตัดสินมาตรการ
(อัปเดตเอกสาร vs แก้บิลด์)"]
G --> A
E -->|"ตรงกันทุกรายการ"| H["ไปยังสกิลถัดไป"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class B,E code;
class F ai;
class A,G human;
class D data;
class H pass;
เนื่องจากการจำลองให้คำตอบ ก่อน บิลด์ เอกสารที่เข้าสู่บิลด์จึงเป็นเอกสารที่ถูกกรองมาแล้วหนึ่งครั้ง ด้วยเหตุนี้ ปัญหาที่ค้นพบหลังบิลด์จึงถูกบีบลักษณะให้แคบลงจาก "เอกสารผิด" เหลือ "เอกสารกับการ implement ไม่ตรงกัน" +70ms ของ 4.4.5 ก็คือกรณีหลังนั้นพอดี — เอกสาร 150 สมเหตุสมผล เพียงแต่การ implement ออกมาเป็น 220 ไม่ตรงกันเท่านั้น การแยกแยะนี้ขจัดการโต้เถียงเรื่องผู้รับผิดชอบในการประชุม
อย่างไรก็ตาม ลูปนี้ไม่ได้ครอบคลุมคอนเทนต์การต่อสู้ทั้งหมด พื้นที่ที่ ความรู้สึกคือคอนเทนต์ในตัว อย่างการกำกับฉากซิกเนเจอร์ของบอสหลักนั้นทอนลงเป็นตัวเลข DPS ไม่ได้ การจำลองแข็งแกร่งกับคอนเทนต์ที่อิงตัวเลข ส่วนการกำกับฉากยังคงต้องใช้การตรวจสอบวิดีโอด้วยตาคนเป็นคำตอบ ลูปหมุนอยู่ในแม่น้ำของตัวเลข ส่วนแม่น้ำของการกำกับฉากไหลแยกต่างหาก
นี่คือการวัด 6 เดือนของทีม TF การต่อสู้ใน MMORPG เกมหนึ่งที่ผมเคยให้บริการ (โปรเจกต์ A ที่ตั้งเป้าฟีลการบังคับแบบเกมตระกูล refgame) ตัวเลขด้านล่างคือ ค่าวัดจริง ที่คัดจากบันทึกภายในของทีม TF และขอแจ้งว่าจำนวนบิลด์·เวลาเป็นค่าที่ปัดเป็นหน่วยรอบ (เป็นการวัดเป็นหน่วยรอบ·ครึ่งวัน ไม่ใช่หน่วยนาทีที่แม่นยำ)
| รายการ | ก่อนนำมาใช้ | หลังนำมาใช้ |
|---|---|---|
| รอบตรวจสอบสกิลใหม่ | เฉลี่ย 3\~4 บิลด์ | เฉลี่ย 1\~2 บิลด์ |
| ตรวจสอบบิลด์สกิล 100 ตัว | ครึ่งวัน (ทำมือ) | 30 นาที (เทียบอัตโนมัติด้วย telemetry) |
| ประชุมปรับสมดุล | 2 ชั่วโมง (ถกตามความเห็นส่วนตัว) | 30 นาที (อิงข้อมูล) |
| ข้อบกพร่องที่พบก่อนบิลด์ | เฉลี่ย 5\~8 รายการ/บิลด์ | เฉลี่ย 1\~2 รายการ/บิลด์ |
การเปลี่ยนแปลงที่สำคัญกว่าตัวเลขคือ ลักษณะ ของการประชุม ครึ่งหนึ่งของการประชุมก่อนนำมาใช้คือ "สกิลนี้แรงเกินไป" กับ "ไม่หรอก พอดีแล้ว" ศึกปลายนิ้วกับปลายนิ้ว หลังนำมาใช้ ที่นั่งตรงนั้นเปลี่ยนไปเป็น "DPS ที่วัดได้คือเป้าหมาย +12% และ hit timing ที่วัดได้คือเอกสาร +70ms จะย่นโมชันดี หรือจะลดดาเมจ 10% ดี" เวลาที่เคยถกเถียง ว่าอะไรคือปัญหา เปลี่ยนไปเป็นเวลากำหนด ว่าจะแก้อย่างไร
ขอเขียนต้นทุนของการเปลี่ยนผ่านนี้ไว้อย่างตรงไปตรงมาด้วย งานช่วงต้นในการฝัง logger telemetry ไว้ทั่วโค้ดการต่อสู้ใช้เวลาราว 1\~2 สัปดาห์ และการทำให้ schema ของเอกสารเป็นมาตรฐานเดียวกันในทุกตัวละคร·ทุกสกิลใช้เวลาเพิ่มอีกหนึ่งไตรมาส ในไตรมาสแรกผมมองว่ามีการจำลองกับ telemetry รันแค่อย่างใดอย่างหนึ่งก็เพียงพอแล้ว ทั้งสองมาประสานเป็นลูปเดียวกันก็ตั้งแต่ไตรมาสที่สองเป็นต้นมา
มีห้าอย่างที่เกิดซ้ำ ๆ
หนึ่ง เชื่อค่าจำลองอย่างเด็ดขาด 261 ของ simulate_dps คือ ขีดสูงสุด ไม่ใช่ค่าวัดจริง หากไม่เทียบกับ telemetry ก็จะประเมินสูงเกินไปเสมอ
สอง ไม่วัด หากจำลองเฉพาะเอกสารแล้วไม่ดูบิลด์ด้วย telemetry +70ms แบบใน 4.4.5 ก็จะสะสมเงียบ ๆ การ logging telemetry ไม่ใช่ตัวเลือกเสริม แต่เป็นโครงสร้างพื้นฐานของโค้ดการต่อสู้
สาม ไม่นำรายงานเข้าประชุม ต่อให้รายงานอัตโนมัติขึ้นแล้ว หากไม่อยู่ในวาระประชุมก็ไม่มีใครดู ให้ใส่ "ตารางเทียบ telemetry ของบิลด์นี้" เป็นรายการตายตัวในวาระ
สี่ รับการประมาณของ AI มาใช้โดยไม่ตรวจสอบ ดังที่เห็นใน 4.4.5 การประมาณครั้งที่ 1 ของ AI เป็นการเดาผิดทาง AI เป็นเครื่องมือกางตัวเลือกให้กว้าง ไม่ใช่เครื่องมือสรุปข้อสรุป อย่าข้ามขั้นตอนของคนที่ตัดตัวเลือกออกด้วย log โดยเด็ดขาด
ห้า โครงสร้างเอกสารต่างกันในแต่ละสกิล หากบางสกิลเขียน cast_sec เป็น cast_time บางสกิลเขียนเป็น castMs ทั้งตัวจำลองและสคริปต์เทียบก็จะพังทุกครั้ง ให้บังคับใช้ schema เอกสารร่วมหนึ่งเดียวกับทุกสกิล — นี่คือเงื่อนไขที่ทำให้เครื่องมือทั้งหมดของ 4.4 รันได้
ไม่จำเป็นต้องจับให้ครบทั้งห้าในไตรมาสแรก แค่หนึ่งหรือสองอย่างเข้าที่ รอบก็สั้นลงอย่างเห็นได้ชัด ที่เหลือจะถูกเติมเต็มเองตามธรรมชาติขณะหมุนลูป
ใน 4.1\~4.4 เราได้พูดถึงพิกัด·Look & Feel·คอมโบ·การจำลองของการออกแบบการต่อสู้ตามลำดับ 4.1 พูดถึงว่านักออกแบบการต่อสู้มองอะไรเป็นเป้าหมายที่วัดได้ 4.2 พูดถึงว่าจะวัดและปรับ hit timing·hitstop·การ sync เอฟเฟกต์อย่างไร 4.3 พูดถึงว่าจะเขียนคอมโบ·cancel·input queue เป็น state machine อย่างไร และ 4.4 พูดถึงว่าจะตรวจสอบทั้งหมดนั้นว่าทำงานตามที่ตั้งใจไว้หรือไม่ ทั้งแบบไม่มีบิลด์/แบบในบิลด์อย่างไร
หนึ่งสัปดาห์ของนักออกแบบการต่อสู้ที่จบ Part 4 จะเปลี่ยนไปเช่นนี้ วันจันทร์ การตรวจสอบอัตโนมัติด้วย simulate_dps ถูกแนบไปกับเอกสารสกิลใหม่ วันอังคาร ตรวจอัตลักษณ์ของคอมโบหลักด้วยการแจกแจงเส้นทางคอมโบอัตโนมัติ วันพุธ พอบิลด์ขึ้น ตารางเทียบ telemetry ก็ขึ้นมาอัตโนมัติ วันพฤหัส ถกเถียงอิงข้อมูล 30 นาที วันศุกร์ แก้เอกสารสำหรับรอบถัดไป รอบบิลด์ลดจาก 3\~4 ครั้งเหลือ 1\~2 ครั้ง และการประชุมสั้นลงเหลือไม่ถึงครึ่ง ความเป็นนามธรรมที่เรียกว่าฟีลการกระแทกเปลี่ยนไปเป็น 220ms ที่วัดได้
และฉากนั้นในบิลด์ #234 — กับคำถาม "อันนี้มันดูลอย ๆ หน่อยไหม" ตอนนี้ log telemetry ตอบแทนแล้วว่า 220ms ศึกระหว่างปลายนิ้วกับกระดาษจบลงแล้ว
Part 5 ถัดไปคือการออกแบบเนื้อเรื่อง เราจะข้ามไปยังกรณีการประยุกต์ใช้จริงของโครงสร้าง NarrativeDocs Layer 0\~4 ที่แนะนำไว้ใน 2.3
setup
1. สร้าง simulate_dps.py ทั้งหมดด้านล่างตามนี้ตรง ๆ ไม่มี dependency รันได้ทันทีด้วย python simulate_dps.py และ "평균 DPS: 261.0" ของ 4.4.2 จะถูกสร้างซ้ำได้
# simulate_dps.py — คำนวณ DPS จากเอกสารเพียงอย่างเดียวโดยไม่ต้องมีบิลด์
from dataclasses import dataclass
@dataclass
class Skill:
name: str
damage: float # ดาเมจต่อการตีหนึ่งครั้ง
cast_sec: float # เวลาร่าย (ครองโมชัน) (วินาที)
cooldown_sec: float # เวลารอใช้ซ้ำ (วินาที)
resource_cost: float # ทรัพยากรที่ใช้ (MP/พลัง)
@dataclass
class Character:
name: str
max_resource: float
resource_regen: float # ฟื้นฟูทรัพยากรต่อวินาที
skills: list # list[Skill]
rotation: list # ลำดับตามความสำคัญ (ชื่อสกิล)
def simulate_dps(char: Character, duration_sec: float, tick=0.05):
cooldowns = {s.name: 0.0 for s in char.skills} # คูลดาวน์ที่เหลือ
skill_by_name = {s.name: s for s in char.skills}
resource = char.max_resource
total_damage = 0.0
busy_until = 0.0 # เวลาที่โมชันร่ายจบ
log = []
t = 0.0
while t < duration_sec:
resource = min(char.max_resource, resource + char.resource_regen * tick)
for name in cooldowns:
cooldowns[name] = max(0.0, cooldowns[name] - tick)
if t >= busy_until: # ถ้าไม่ได้อยู่ระหว่างร่าย ให้เลือกสกิลถัดไป
for name in char.rotation: # ตามลำดับความสำคัญ
s = skill_by_name[name]
if cooldowns[name] <= 0 and resource >= s.resource_cost:
total_damage += s.damage
resource -= s.resource_cost
cooldowns[name] = s.cooldown_sec
busy_until = t + s.cast_sec
log.append((round(t, 2), name, resource))
break
t += tick
return total_damage / duration_sec, log
if __name__ == "__main__":
warrior = Character(
name="warrior", max_resource=100, resource_regen=8,
skills=[
Skill("skill_thunder", damage=420, cast_sec=0.9, cooldown_sec=6, resource_cost=40),
Skill("skill_dash", damage=180, cast_sec=0.4, cooldown_sec=3, resource_cost=20),
Skill("basic_1", damage=60, cast_sec=0.3, cooldown_sec=0, resource_cost=0),
],
rotation=["skill_thunder", "skill_dash", "basic_1"],
)
dps, log = simulate_dps(warrior, duration_sec=20)
print(f"평균 DPS: {dps:.1f}")
for t, name, res in log[:8]:
print(f" t={t:>5}s {name:<14} 자원={res:.0f}")
GetTimeSeconds ฯลฯ) ลงไปตรง ๆprompt เลือกรายการหนึ่งที่ไม่ตรงกับเอกสารจาก telemetry ของบิลด์ แล้วถาม AI ด้วยแบบฟอร์มของ 4.4.5 — แนบ log ที่ตัดมา และต้องใส่ "อย่าฟันธงการคาดเดา ให้เขียนถึงวิธีตรวจสอบของแต่ละสาเหตุด้วย ถ้าไม่แน่ใจให้เขียนกำกับว่ายังไม่ยืนยัน" ไว้เสมอ
verify อย่ารับเอาต์พุตครั้งที่ 1 ของ AI มาใช้ตรง ๆ อ่าน log ด้วยตัวเองแล้วลบตัวเลือกที่ตัดออกได้ด้วยมือ (เหมือนการตัดเครือข่าย·เฟรมเรตใน 4.4.5) จากนั้นแนบเหตุผลแล้วขอใหม่ หากรายงานสุดท้ายจบที่หน่วยการกระทำ "ประมาณสาเหตุ + ร้องขอใครให้ทำอะไร" ก็นำเข้าประชุมได้
ฉบับย่อสำหรับคนเดียว หากการติดตั้ง logger telemetry ทั่วทั้งระบบเป็นภาระ ให้จด input·hit สองบรรทัดเฉพาะ สกิลหนึ่งตัว ที่ต้องการตรวจสอบ และให้ simulate_dps ดูเฉพาะ DPS ของสกิลตัวนั้นตัวเดียว อย่าติดตั้งเครื่องมือทั้งหมด แต่หมุนลูปหนึ่งรอบด้วยสกิลที่น่าสงสัยที่สุดตัวเดียวก่อนแล้วค่อยขยาย
เมื่อเดินเข้าห้องประชุม บนไวต์บอร์ดมีชื่อตัวละครหนึ่งถูกวงด้วยปากกาแดง เป็น NPC ที่ชื่อว่าคิม ในเควสต์เสริมของนักออกแบบเกมคนหนึ่ง เขาคือ "พ่อบุญธรรมที่เก็บตัวเอกมาเลี้ยงดูตั้งแต่เด็ก" แต่ในเควสต์หลักบทที่ 3 ของนักออกแบบเกมอีกคน เขากลับเป็น "อดีตสหายที่ทรยศตัวเอกแล้วจากไป" เอกสารทั้งสองได้รับการอนุมัติเมื่อหนึ่งเดือนก่อน และทั้งคู่ก็เข้าไปอยู่ในบิลด์แล้ว แม้แต่ใบเสนอราคาสำหรับการอัดเสียงพากย์ก็ได้รับมาแล้วด้วยซ้ำ
ไม่มีใครผิด นักออกแบบเกมทั้งสองคนต่างก็อ่านเอกสารโลกของเกม และอ้างอิงการตั้งค่าตัวละคร ปัญหาอยู่ที่การตั้งค่าของตัวละครเดียวกันกระจัดกระจายอยู่ในไฟล์ที่แตกต่างกันสามไฟล์ และไม่มีใครฟันธงได้ว่าไฟล์ไหนคือ "ของจริง" เอกสารโลกของเกมเป็นไฟล์เวิร์ดก้อนเดียว 70 หน้า เมื่อค้นหาชื่อคิม ก็พบขึ้นมาถึงสิบเอ็ดจุด ไม่มีการแยกว่าบรรทัดไหนคือการตัดสินใจ บรรทัดไหนคือบันทึกย่อ
เมื่อการประชุมวันนั้นจบลง สิ่งที่เราตัดสินใจกันก็คือการแบ่ง NarrativeDocs ออกเป็นห้าชั้น บทนี้คือเรื่องราวของห้าชั้นนั้น
การเปิดเรื่องการแยก Layer ตามสาขาด้วยเนื้อเรื่อง (narrative) ก่อนนั้นมีเหตุผล
เนื้อเรื่องเป็นนามธรรมที่สุด สิ่งอย่างโลกของเกม อารมณ์ และโทน ไม่ได้ตกลงเป็นตัวเลข ไม่มีหน่วยที่ชัดเจนอย่าง "จำนวนสไปรต์" หรือ "ค่าสัมประสิทธิ์ดาเมจ" เหมือนงานอาร์ตหรือระบบ ถ้าสาขาที่เป็นนามธรรมขนาดนี้ยังถูกแยกออกเป็น Layer ได้อย่างเป็นระเบียบ สาขาอื่นที่เป็นรูปธรรมกว่าก็ย่อมคลี่ออกได้ด้วยรูปแบบเดียวกันโดยธรรมชาติ เท่ากับเป็นการลองดูก่อนว่ามันใช้ได้ไหมในจุดที่ยากที่สุด
อีกประการ เนื้อเรื่องมีอินเทอร์เฟซมากที่สุด ตัวละครเชื่อมกับงานอาร์ต เควสต์เชื่อมกับเนื้อหาและเลเวล บทพูดเชื่อมกับ UX และการแปลภาษา (localization) ส่วนรางวัลเชื่อมกับระบบ เพราะเป็นตำแหน่งที่ตัดข้ามระหว่างสาขามากที่สุด คุณค่าของการรวม Layer จึงปรากฏชัดในทันที
สุดท้าย ผลงานที่เป็นภาษาธรรมชาติมีสัดส่วนสูงที่สุด จึงเป็นสาขาที่ AI ช่วยได้มากที่สุดด้วยเช่นกัน อย่างไรก็ตาม ไม่ใช่ทุกเกมที่ยึดเนื้อเรื่องเป็นศูนย์กลาง หากเป็นแนวแคชวลหรืออาร์เคด ความลึกของบทนี้อาจมากเกินไป แต่ถึงอย่างนั้น โครงหลักที่ว่า "แยกเอกสารก้อนเดียวออกเป็น Layer และทำให้อินเทอร์เฟซแคบลง" ก็ย้ายไปใช้กับสาขาใดก็ได้ตามเดิม
รูปร่างของ NarrativeDocs ที่ถูกแยกออกเป็นห้าชั้นมีดังนี้ วิสัยทัศน์ (L0) อยู่ด้านบน บิลด์และ QA (L4) อยู่ด้านล่าง และทางเชื่อมระหว่างชั้นถูกออกแบบให้แคบโดยตั้งใจ
เมื่อย้ายไปเป็นโฟลเดอร์ จะได้รูปร่างดังต่อไปนี้ ชื่อไฟล์แต่ละชื่อไม่ใช่ชื่อที่ทำให้เป็นนามธรรมในหนังสือ แต่เป็นชื่อไฟล์ที่มีอยู่จริงในโฟลเดอร์นั้น
NarrativeDocs/
├── Layer0_Vision/
│ ├── world_premise.md (สมมติฐานโลกของเกม — ไม่เปลี่ยนแปลง)
│ ├── narrative_pillar.md (เสาอารมณ์ 3 ต้น)
│ └── tone_manifesto.md (รายการโทน·คำต้องห้าม)
├── Layer1_System/
│ ├── faction_system.md
│ ├── reputation_model.md
│ ├── dialogue_branching_rule.md
│ └── lore_consistency_rule.md
├── Layer2_Content/
│ ├── main_quest/ (ระดับบท)
│ ├── side_quest/
│ ├── character_bible/
│ └── lore_codex/
├── Layer3_Data/
│ ├── quest_table.xlsx
│ ├── npc_table.xlsx
│ ├── dialogue_id_table.xlsx
│ └── reward_table.xlsx
└── Layer4_Build_QA/
├── narrative_qa_checklist.md
├── voice_review_log.md
└── localization_status.md
จุดสำคัญคือ ห้าชั้นนี้ไม่ได้ให้คนคนเดียวรับผิดชอบทั้งหมด แต่ละชั้นมีผู้ดูแลหลักต่างกัน และมาตรฐานเฉพาะทางเชื่อมระหว่างชั้นที่อยู่ติดกันเท่านั้น ลูกศรสีแดงในภาพข้างต้นคือทางเชื่อมเหล่านั้น โดยเฉพาะทางเชื่อมระหว่าง L2 กับ L3 ถูกวาดให้แคบที่สุดโดยตั้งใจ (ลูกศรสีแดงหนา) ส่วนเหตุผลนั้นจะได้เห็นในตอนหลัง
ถ้ามองด้วยอุปมาลิ้นชัก ก็คือลิ้นชักห้าช่อง ช่องแรกมีโลกของเกมหนึ่งบรรทัดที่ห้ามย้ายเด็ดขาด ช่องที่สองมี rulebook ช่องที่สามมีเนื้อหาหลัก ช่องที่สี่มีชีต ช่องที่ห้ามีบันทึกการตรวจสอบ ทางเชื่อมระหว่างช่องนั้นแคบ และเหนือทางเชื่อมมีกระดิ่งแจ้งเตือนการเปลี่ยนแปลงติดอยู่
L0 ไม่เปลี่ยนแปลง หากเปลี่ยนก็เท่ากับอัตลักษณ์ของเกมเปลี่ยน ดังนั้นปริมาณจึงต้องน้อย น้อยจึงจะไม่เปลี่ยน
| เอกสาร | ปริมาณ |
|---|---|
| world_premise.md | A4 1.5 หน้า |
| narrative_pillar.md | A4 1 หน้า (อารมณ์ 3 อย่าง) |
| tone_manifesto.md | A4 2 หน้า (โทน + รายการคำต้องห้าม) |
รวมกันราว 4.5 หน้า นี่คือน้ำหนักของ L0 ถ้าหนักขึ้น การเปลี่ยนแปลงก็จะกลายเป็นเรื่องน่ากลัว และเมื่อกลัว ชั้นอื่นก็จะเริ่มเลี่ยง L0 เมื่อการเลี่ยงเริ่มขึ้น L0 ก็กลายเป็นเอกสารที่ตายแล้ว
โครงสร้างจริงของ narrative_pillar.md มีรูปร่างดังนี้ (เนื้อหาทำให้เป็นนามธรรม)
---
title: เสาอารมณ์ของเนื้อเรื่อง
layer: L0
status: locked
last_updated: 2026-05-18
---
## 1. ความโหยหาสิ่งที่สูญเสียไป
- ผู้เล่นสูญเสียสิ่งหนึ่งในตอนจบของทุกบท
- สิ่งที่สูญเสียไปจะไม่หวนกลับมาอีก (มีเพียงในความทรงจำ)
## 2. ความขัดแย้งระหว่างหน้าที่กับอิสรภาพ
- NPC หลักทุกตัวแบกหน้าที่สองอย่าง
- ทางเลือกของผู้เล่นรักษาหน้าที่ได้เพียงด้านเดียว
## 3. น้ำหนักของการสัมผัสเล็ก ๆ
- ความเมตตาเล็กน้อยให้ผลลัพธ์ที่ใหญ่กว่าวีรกรรมอันยิ่งใหญ่
เสาสามบรรทัดนี้กำหนดทิศทางของอีกหลายร้อยหน้าที่อยู่ใต้มัน status: locked ไม่ใช่เพียงป้ายธรรมดา หนึ่งในการตรวจสอบอัตโนมัติของ L4 จะอ่านป้ายนี้ และตั้งไว้ว่าเมื่อเอกสารที่เป็น locked ถูกแก้ไขใน PR การ merge จะถูกระงับไว้จนกว่าจะได้รับการอนุมัติจากลีดเนื้อเรื่อง
L1 เปลี่ยนได้ แต่ต้นทุนสูง เพราะมันคือ rulebook เมื่อ rulebook เปลี่ยนไปหนึ่งบรรทัด เนื้อหาทั้งหมดที่ทำตามกฎนั้นก็ได้รับผลกระทบ
โครงสร้างของ faction_system.md มีดังนี้
---
title: ระบบฝ่าย
layer: L1
atoms:
- faction_relation_matrix
- faction_membership_rule
- faction_quest_eligibility
---
## 1. แนวคิดฝ่าย
N ฝ่าย แต่ละฝ่ายนิยามด้วย (อุดมการณ์, ทรัพยากร, ดินแดน)
## 2. ความสัมพันธ์ระหว่างฝ่าย
- relation_matrix.json (-3 เป็นศัตรู ~ +3 เป็นพันธมิตร)
- ทริกเกอร์การเปลี่ยนความสัมพันธ์: การตัดสินใจในเควสต์หลัก, เกณฑ์ค่าชื่อเสียง
## 3. กฎการสังกัดของผู้เล่น
- สังกัดพร้อมกันได้สูงสุด 2 ฝ่าย (สังกัดฝ่ายที่เป็นศัตรูกันพร้อมกันไม่ได้)
- บทลงโทษการถอนตัว: ค่าชื่อเสียง -2, ฝ่ายพันธมิตร -1
ลองสังเกตรายการ atoms: ใน frontmatter ให้ดี ชื่อ atom ทั้งสามนี้ไม่ใช่เพียงบันทึกย่อ แต่เป็นตัวระบุ (identifier) ที่ออนโทโลยีในส่วนที่ 7 และแผนผังความสัมพันธ์ในส่วนที่ 11 ติดตามอยู่ เมื่อเควสต์ใดอ้างอิง faction_quest_eligibility ไว้ เควสต์นั้นจะถูกยกขึ้นมาในรายการที่ได้รับผลกระทบโดยอัตโนมัติเมื่อกฎนี้เปลี่ยน L1 เป็นผลงานเนื้อเรื่องที่ใกล้กับโค้ดของเกมที่สุด จึงทำงานจับคู่กับ System Designer (ออกแบบระบบ)
L2 หนาที่สุด เควสต์หลัก เควสต์เสริม character bible และพจนานุกรม lore อาศัยอยู่ที่นี่ทั้งหมด แม้แต่การตั้งค่าจริงของคิม "พ่อบุญธรรม vs สหายผู้ทรยศ" ที่ปะทะกันในห้องประชุมก่อนหน้านี้ ตอนนี้ก็ดำรงอยู่ในรูปไฟล์เดียวภายใน character_bible/ เท่านั้น ไฟล์นั้นคือแหล่งความจริงเดียว (single source of truth) ส่วนเควสต์เป็นเพียงผู้อ้างอิงไปยังที่นั่น
โฟลเดอร์เควสต์หลักมีรูปร่างดังนี้
main_quest/
├── chapter_01_awakening/
│ ├── 00_chapter_overview.md
│ ├── 01_quest_a_call_to_arms.md
│ ├── 02_quest_b_first_choice.md
│ └── ...
├── chapter_02_road/
│ └── ...
└── _TEMPLATES/
└── quest_template.md
ไฟล์เควสต์แต่ละไฟล์ทำตามรูปแบบมาตรฐานของ atom
---
title: เมื่อต้องจับอาวุธ
layer: L2
type: main_quest
atoms:
- quest_chapter_01_awakening_a
related:
affects: [reputation_model, faction_relation_matrix]
derives_from: [narrative_pillar, world_premise]
requires: [character_kim, faction_alpha]
part_of: chapter_01_awakening
---
## ขั้นตอนดำเนินการ
1. ...
## การแตกแขนง
- เมื่อเลือกตัวเลือก A: ...
- เมื่อเลือกตัวเลือก B: ...
## รางวัล (อ้างอิง L3)
- reward_table.xlsx → แถว quest_001
หัวใจอยู่ที่บล็อก related: ทันทีที่เขียนว่า requires: [character_kim] ก็เท่ากับเควสต์นี้ประกาศว่าจะดึงการตั้งค่าของคิมมาจาก character_bible และจะไม่นิยามคิมขึ้นใหม่ในไฟล์ของตัวเองอีกต่อไป เนื้อหาหลักของเนื้อเรื่องวางไว้ที่ L2 ส่วนรางวัลที่เป็นตัวเลขวางไว้ในชีต L3 ถ้าทั้งสองอยู่ในไฟล์เดียวกัน ทุกครั้งที่แก้ชีตหนึ่งบรรทัดก็ต้องไปแตะเนื้อหาหลัก และเมื่อนั้น คีย์การแปลก็จะคลาดเคลื่อน
L3 คือชีตและ ID ประโยคภาษาธรรมชาติเข้าไปแม้แต่บรรทัดเดียวก็ไม่ได้
quest_table.xlsx
| quest_id | chapter | type | unlock_level | reward_xp | reward_gold | dialogue_set_id |
|----------|---------|------|--------------|-----------|-------------|-----------------|
| q_001 | ch01 | main | 1 | 500 | 100 | ds_001 |
| q_002 | ch01 | main | 2 | 800 | 150 | ds_002 |
แม้แต่บทพูดก็อ้างอิงด้วย ID เท่านั้น ตัวเนื้อหาหลักอยู่แยกต่างหากใน dialogue_id_table.xlsx และจับคู่กับคีย์การแปลแบบ 1:1 ทางเชื่อมระหว่าง L2 กับ L3 เพียงแค่คอลัมน์ quest_id คอลัมน์เดียวก็เพียงพอ นี่คือเหตุผลที่ในภาพก่อนหน้า ทางเชื่อมนี้เป็นเพียงทางเดียวที่เป็นลูกศรสีแดงหนา การทำให้อินเทอร์เฟซแคบลงเหลือคอลัมน์เดียวคือหัวใจของการแยก Layer หากทางเชื่อมกว้าง ทั้งสองด้านก็จะรู้จักกันมากเกินไป และเมื่อแก้ด้านหนึ่ง อีกด้านก็จะพังตามไปด้วย
L4 คือการตรวจสอบและการวางจำหน่าย ทุกครั้งที่เนื้อหาใหม่เข้ามา การตรวจสอบทั้งอัตโนมัติและด้วยมือจะทำงาน การตรวจสอบอัตโนมัตินั้นรันด้วยสคริปต์ สี่รายการด้านล่างคือ lint ที่ตั้งอยู่ใน CI จริง
| การตรวจสอบ | เครื่องมือ |
|---|---|
| dialogue_id ทุกตัวมีการ mapping อยู่ | dialogue_lint.py |
| quest_id ทุกตัวสังกัด chapter | quest_lint.py |
| ผลรวมรางวัลอยู่ในช่วงเส้นโค้งของแต่ละบท | reward_curve_check.py |
| มีคำต้องห้ามปรากฏหรือไม่ | tone_lint.py (อิงตาม L0 tone_manifesto) |
ลองสังเกตว่า tone_lint.py อ่าน tone_manifesto.md ของ L0 โดยตรง ชั้นบนสุด (วิสัยทัศน์ที่ไม่เปลี่ยนแปลง) กับชั้นล่างสุด (เกตวางจำหน่าย) เชื่อมต่อกันโดยตรงด้วยระบบอัตโนมัติ หากคำต้องห้ามที่เขียนไว้ในวิสัยทัศน์ถูกตรวจพบในเนื้อหาหลักก่อนวางจำหน่าย บิลด์ก็จะถูกระงับ
สิ่งที่ตรวจจับอัตโนมัติไม่ได้ ให้คนเป็นผู้ดู
| การตรวจสอบ | ผู้รับผิดชอบ |
|---|---|
| ความสอดคล้องกับเสาอารมณ์ L0 | ลีดเนื้อเรื่อง |
| ความสม่ำเสมอของเสียงตัวละคร | ผู้ดูแลตัวละคร + เนื้อเรื่อง |
| ความเป็นไปได้ในการแปลภาษา | ผู้แปลภาษา (localizer) |
เมื่อขอบเขตระหว่างอัตโนมัติกับด้วยมือชัดเจน เวลาตรวจสอบก็คาดการณ์ได้ ทำให้ตอบคำถาม "บทนี้ตรวจสอบกี่วัน" ได้
ที่ผ่านมาคือโครงสร้าง แต่ที่ซึ่งโครงสร้างนี้ทำงานจริง คือตอนสร้างเนื้อหาใหม่ ผู้เขียนจะถอดเซสชันจริงที่สั่งให้ Claude รับไอเดียเควสต์เสริมหนึ่งบรรทัดแล้วโปรยกระจายลงใน Layer บนโปรเจกต์ที่วางห้าชั้นไว้แล้วมาให้ตามนั้น โดยไม่สรุปย่อ และรวมส่วนที่เบี่ยงเบนกับส่วนที่ถูกปฏิเสธไว้ด้วย
พรอมต์ที่ส่งไป (ฉบับเต็ม):
อย่าฝ่าฝืนบริบท L0/L1 ที่แนบมาเด็ดขาด แล้วแยกย่อยตามโครงสร้าง Layer ของเรา
[narrative_pillar.md ฉบับเต็ม — เสาอารมณ์ 3 ต้น]
[tone_manifesto.md บางส่วน — คำต้องห้าม: "โชคชะตา", "คำพยากรณ์", "ผู้ถูกเลือก"]
[character_bible/character_kim.md บางส่วน — คิม: พ่อบุญธรรมที่เก็บตัวเอกมาเลี้ยง,
พูดน้อย, พูดทิ้งท้ายไม่จบ, ไม่พูดความรู้สึกออกมาตรง ๆ]
ไอเดีย: "เควสต์เสริมสั้น ๆ ที่คิมส่งของที่ระลึกของภรรยาผู้ล่วงลับให้ผู้เล่น"
เขียนไฟล์เควสต์ L2 (รวมบล็อก related, เนื้อหาหลักราว 200 ตัวอักษร), แถว
quest_table ของ L3 กับ dialogue_id, และการตรวจสอบตนเองเรื่องการละเมิด tone ด้วย
เอาแค่ L2 ก่อนแล้วหยุดไว้
ผลลัพธ์ดิบของ Claude (L2):
---
title: ผ้าเช็ดหน้าซีดจาง
layer: L2
type: side_quest
atoms:
- quest_side_kim_keepsake
related:
affects: [reputation_model]
derives_from: [narrative_pillar]
requires: [character_kim]
part_of: chapter_02_road
---
## ขั้นตอนดำเนินการ
1. คิมเรียกผู้เล่นอย่างเงียบ ๆ ไม่ได้บอกธุระทันทีเหมือนเช่นเคย
2. ยื่นผ้าเช็ดหน้าเก่า ๆ ออกมา "อันนี้… ข้าว่าเจ้าเก็บไว้น่าจะดีกว่า"
3. หากผู้เล่นถามว่าคืออะไร คิมไม่ตอบ แต่มองออกไปนอกหน้าต่างแทน
4. (ตัวเลือก) รับไว้ / ปฏิเสธ
## การแตกแขนง
- รับไว้: ค่าชื่อเสียงของคิม +1 ปลดล็อกฉากความทรงจำเกี่ยวกับผ้าเช็ดหน้า 1 ครั้งในบทถัดไป
- ปฏิเสธ: คิมพยักหน้าแล้วเก็บผ้าเช็ดหน้ากลับเข้าไป ค่าชื่อเสียงไม่เปลี่ยนแปลง
ผ้าเช็ดหน้าจะไม่ปรากฏขึ้นอีกตลอดกาล
## รางวัล (อ้างอิง L3)
- reward_table.xlsx → แถว quest_side_kim_keepsake
ตรงนี้ Claude หยุด และเติมมาหนึ่งบรรทัด
"ผมสะท้อนเสาที่ 1 ของ L0 (สิ่งที่สูญเสียไปจะไม่หวนกลับมา) ด้วยการทำให้ถ้าเลือก 'ปฏิเสธ' ผ้าเช็ดหน้าจะหายไปตลอดกาล แต่มีจุดหนึ่งที่ต้องยืนยัน การตั้งค่าเรื่องภรรยาผู้ล่วงลับไม่มีอยู่ในส่วนที่ตัดมาของ character_kim.md ผมใส่เข้าไปเองตามอำเภอใจ จึงอาจขัดแย้งกับ character bible ได้"
การตรวจสอบและการปฏิเสธของมนุษย์:
ผลลัพธ์นี้มีโทนที่แม่นยำ ความพูดน้อยของคิม การพูดทิ้งท้ายไม่จบ และนิสัยที่ไม่พูดความรู้สึกออกมาตรง ๆ ยังคงมีชีวิตอยู่ ทั้งไม่มีคำต้องห้าม แต่การตรวจสอบตนเองในบรรทัดสุดท้ายชี้ปัญหาจริงได้ตรงจุดพอดี การตั้งค่าที่ว่าคิมมีภรรยาที่เสียชีวิตนั้นไม่มีอยู่ที่ใดเลย AI อนุมานคู่สมรสที่ล่วงลับขึ้นมาอย่างเป็นธรรมชาติจากคำว่า "ของที่ระลึก" ซึ่งนี่คือการแอบเพิ่มข้อเท็จจริงใหม่ลงใน bible และนี่เองคือเมล็ดพันธุ์ของอุบัติเหตุที่เกิดขึ้นในห้องประชุมตอนต้นบทนี้
จึงปฏิเสธ แล้วส่งคำขอใหม่ไป
คำขอใหม่:
โทนดี แต่ "ภรรยาผู้ล่วงลับ" เป็นการตั้งค่าใหม่ที่ไม่มีใน bible ใช้ไม่ได้
อดีตของคิมยืนยันแค่ "ตอนจากบ้านเกิดมาได้ทิ้งบางอย่างไว้" เท่านั้น
อย่าระบุว่าของที่ระลึกคืออะไร เขียนใหม่ให้คลุมเครือแค่ระดับ "สิ่งที่ทิ้งไว้นานมาแล้ว"
ผ้าเช็ดหน้าก็ไม่มีใน bible ลดลงเป็นแค่ "เศษผ้า" ก็พอ
ผลลัพธ์หลังคำขอใหม่ (บางส่วน):
2. ยื่นเศษผ้าเก่า ๆ ออกมา "มีของอยู่ชิ้นหนึ่ง… ที่ข้าทิ้งไว้นานมาแล้ว
ขอบอกแค่ว่านี่คือปลายของมันก็พอ"
3. หากผู้เล่นถามว่าคืออะไร คิมไม่ตอบ แต่มองออกไปนอกหน้าต่างแทน
คราวนี้รับไว้ มันแตะเสาอารมณ์ที่ 1 (การสูญเสีย) ได้โดยไม่ตอกตรึงอดีตของคิมขึ้นใหม่ ส่วนสิ่งที่สูญเสียไปคืออะไรนั้น ยังคงคลุมเครือไว้จนจบ
สิ่งที่เซสชันเดียวนี้แสดงให้เห็นมีสองอย่าง หนึ่ง เมื่อฉีดบริบท Layer (เสา L0 + โทน L1 + bible L2) เข้าไปอย่างแม่นยำ AI จับโทนได้ดีอย่างน่าทึ่ง สอง เมื่อ AI พบช่องว่าง มันจะเติมให้ดูสมเหตุสมผล และเมื่อสิ่งที่เติมนั้นเป็นข้อเท็จจริงใหม่ที่ไม่มีใน bible ผู้ที่จับมันได้ก็ยังคงเป็นมนุษย์ เพราะ requires: [character_kim] ของโครงสร้าง Layer บอกว่า "ต้องไปดูที่ไหน" ผู้ตรวจสอบจึงรู้ทันทีว่าต้องเทียบกับไฟล์ใด หากไม่มีโครงสร้าง คงต้องไปไล่รื้อทั้ง 70 หน้าอีกครั้ง
เหตุผลที่แท้จริงในการแบ่งห้าชั้นคือเพื่อทำให้อินเทอร์เฟซแคบลง และที่อินเทอร์เฟซแคบแต่ละจุด ก็ติดระบบอัตโนมัติตรวจจับการเปลี่ยนแปลงไว้
| อินเทอร์เฟซ | อะไรไหลผ่าน |
|---|---|
| L0 → L1 | pillar, tone (ทริกเกอร์การทบทวน rulebook L1 เมื่อเปลี่ยน) |
| L1 → L2 | rulebook·นโยบายแตกแขนง (จัดทำรายการเควสต์ที่ได้รับผลกระทบอัตโนมัติเมื่อเปลี่ยน) |
| L2 → L3 | quest_id, npc_id, dialogue_id (หนึ่งคอลัมน์) |
| L3 → L4 | ทริกเกอร์ lint อัตโนมัติเมื่อชีตเปลี่ยน |
ตัวอย่างเช่น หากยื่น PR ที่เปลี่ยน "สังกัดพร้อมกันได้สูงสุด 2 ฝ่าย" ใน faction_system.md ของ L1 เป็น "สูงสุด 1 ฝ่าย" แผนผังความสัมพันธ์จะกวาดเควสต์ L2 ที่อ้างอิง faction_quest_eligibility แล้วจัดทำรายการที่ได้รับผลกระทบ และรายการนั้นจะถูกแนบเข้ากับคอมเมนต์ของ PR โดยอัตโนมัติ คนที่เปลี่ยนกฎ แทนที่จะ "นัดประชุมสองสามครั้งเพื่อหาว่าใครได้รับผลกระทบบ้าง" ก็แค่ดูรายการที่แนบในคอมเมนต์ แล้วจบด้วยการประชุม 30 นาที
หัวใจคือสิ่งนี้ หากแบ่ง Layer เพียงอย่างเดียวแต่อินเทอร์เฟซคลุมเครือ ก็เท่ากับมีแต่ฉากกั้นเพิ่มขึ้น ระบบอัตโนมัติของอินเทอร์เฟซต่างหากที่เป็นแก่นแท้ ยิ่งกว่าตัวการแยก Layer เอง
นี่คือการวัดผลหลังจากย้ายไปห้าชั้นแล้วรันมาหกเดือน ตัวเลขด้านล่างอิงตามบันทึกการใช้งานจริงของทีมผู้เขียน แต่ค่าสัมบูรณ์จะถ่ายทอดเป็นเพียงทิศทางและสัดส่วนเท่านั้น (การประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ) มีข้อจำกัดว่า ก่อนแยกเป็นความทรงจำ หลังแยกเป็นการวัดจริง จึงไม่ใช่การวัดข้อมูลชุดเดียวกันสองครั้ง
| รายการ | ก่อนแยก Layer | หลังแยก Layer |
|---|---|---|
| การออนบอร์ดนักออกแบบเกมใหม่ | 3 สัปดาห์ | 1 สัปดาห์ |
| การหาขอบเขตผลกระทบของการเปลี่ยน rulebook | ประชุม 2\~3 ครั้ง | คอมเมนต์อัตโนมัติ + ประชุม 30 นาที |
| การสร้างเควสต์หลัก 1 บทใหม่ | 4 สัปดาห์ | 2.5 สัปดาห์ |
| การตรวจสอบ 1 บทก่อนวางจำหน่าย | 5 วัน | 2 วัน |
| อุบัติเหตุการแปลภาษาตกหล่น | 3\~5 ครั้งต่อไตรมาส | 0\~1 ครั้งต่อไตรมาส |
สิ่งที่ลดลงอย่างชัดเจนที่สุดคือการแปลภาษาตกหล่น เมื่อ dialogue_id ถูกผูกกับคีย์การแปลแบบ 1:1 ที่ L3 และ dialogue_lint.py ป้องกันการ mapping ตกหล่น อุบัติเหตุที่ว่า "บทพูดที่ยังไม่ได้แปลหลุดเข้าไปในบิลด์" ก็แทบหายไป การที่การออนบอร์ดสั้นลงก็เป็นเรื่องใหญ่ แทนที่จะบอกนักออกแบบเกมใหม่ว่า "อ่านไฟล์เวิร์ด 70 หน้าให้หมด" ก็พูดได้ว่า "จำ L0 แค่ 4.5 หน้า แล้วเควสต์ของคุณก็เติมเทมเพลต L2 เอา"
ขอเสริมอย่างตรงไปตรงมาว่า ผลนี้ไม่ได้ออกมาในครั้งเดียว ในไตรมาสแรกติดคอมเมนต์อัตโนมัติไว้เพียงอย่างเดียว ที่เหลือยังเป็นงานมือ ระบบอัตโนมัติของอินเทอร์เฟซเพิ่มทีละอย่างในแต่ละไตรมาส การติดตั้งระบบอัตโนมัติใช้เวลานานกว่าการแบ่ง Layer เสียอีก
ที่ผ่านมาคือเหตุผลผิวเผิน "ทำให้ภาษาในการทำงานร่วมกันระหว่างสาขาเป็นหนึ่งเดียว" แต่มีเหตุผลที่เป็นแก่นแท้กว่านั้นอีกหนึ่งอย่าง การแยก Layer คือเงื่อนไขที่ทำให้การสร้างแบบโพรซีเดอรัล (procedural generation) เป็นไปได้ ห้าชั้นแต่ละชั้นล้วนสอดคล้องกับบทบาทหนึ่งของการสร้างแบบโพรซีเดอรัล (L0 แองเคอร์ → L1 rulebook → L2 เนื้อหาหลัก → L3 ตัวเลข → L4 เกต) และหากปนกันเป็นก้อนเดียว ตัวสร้างก็จะกำหนดไม่ได้ว่าจะอ่านจากที่ไหนและเขียนที่ไหน แล้วพังลง ประเด็นทั่วไปนี้ได้กล่าวถึงทั้งหมดแล้วใน §6.6 ตรงนี้จะดูเพียงว่าเงื่อนไขนั้นทำงานจริงอย่างไรบนห้าชั้นเนื้อเรื่อง
บนเอกสารก้อนเดียว 70 หน้า อัลกอริทึมการสร้างกำหนดไม่ได้ว่าจะอ่านจากที่ไหนและเขียนที่ไหน ห้าชั้นเนื้อเรื่องกลายเป็นห้าขั้นตอนของไปป์ไลน์การสร้างได้ทันที — L0 แองเคอร์, L1 กฎอินพุต, L2 ที่ที่เนื้อหาหลักสะสม, L3 อินพุตการจำลอง, L4 เกตตรวจสอบ
บันทึกเซสชันจริงก่อนหน้านี้ก็เป็นฉบับย่อของห้าขั้นตอนนี้อยู่แล้ว มันฉีดเสา L0 และโทน L1 เข้าเป็นบริบท (แองเคอร์) เนื้อหาหลักถูกสร้างที่ L2 แถว L3 ตามออกมา และการตรวจสอบตนเองเรื่องการละเมิด tone ก็เลียนแบบเกตตรวจสอบ สิ่งที่คนสั่งทำทีละเควสต์ หากย้ายให้ generator ผลิตเควสต์เสริมจำนวนมากบนโครงสร้างเดียวกัน ก็กลายเป็นการสร้างแบบโพรซีเดอรัล
ถ้าไปไกลกว่านั้น จะไหลแบบนี้ — การกระทำของผู้เล่นสะสม → การเปลี่ยนสถานะของโหนด BT ระดับโลก (Squad ระดับบน) → การเปลี่ยนค่าตัวเลขของ NPC (ค่าชื่อเสียง·ความสนใจ·ลำดับความสำคัญ) → แท็ก+ตัวเลขของ NPC คือเงื่อนไขการปรากฏ → เควสต์ที่จับคู่ได้ปรากฏจากกลุ่มก้อนเควสต์ (quest cloud) แทนที่จะเขียนเควสต์ไว้ล่วงหน้าทั้งหมด ก็ปล่อยให้มัน "ลอยอยู่ในอากาศเหมือนเมฆ" พร้อมแท็กติดอยู่ และเมื่อการกระทำของผู้เล่นเปลี่ยนค่าตัวเลขของ NPC เควสต์ที่ค่านั้นตรงกับเงื่อนไขการปรากฏก็จะร่วงลงมาปรากฏ โมเดลก้าวหน้านี้จะกล่าวถึงอย่างละเอียดอีกครั้งใน 5.3 (รวมแผนภาพการไหล) จุดที่ต้องเน้นตรงนี้คือ การไหลทั้งหมดนี้ทำงานได้บนการแยกห้าชั้นเท่านั้น
ในทางตรงกันข้าม ทีมที่มี Layer ปนกันไปไม่ถึงการสร้างแบบโพรซีเดอรัล พอลองทำก็พังด้วยอุบัติเหตุความสอดคล้องในทันที ลองจินตนาการดูว่าอุบัติเหตุที่คิมกลายเป็นทั้งพ่อบุญธรรมและผู้ทรยศนั้น ถูกผลิตซ้ำขึ้นมาโดยอัตโนมัติผ่านตัวสร้าง
อย่างไรก็ตาม ไม่ได้หมายความว่าต้องมีลิ้นชักห้าช่องครบสมบูรณ์ตั้งแต่แรก ในไตรมาสแรก แยกแค่ L0 หนึ่งบรรทัดกับ rulebook L1 หนึ่งเล่มก็เพียงพอ แยกแบบค่อยเป็นค่อยไป อินเทอร์เฟซแคบเข้าไว้
สุดท้าย เรื่องของมุมมองทางเวลาสักหนึ่งข้อ ตัวการแยก Layer เองนั้นเป็นการแยกที่มีมาตั้งแต่ยุค PCG (Procedural Content Generation, การสร้างเนื้อหาแบบโพรซีเดอรัล) เชิงกำหนด สิ่งที่ใหม่คือ LLM (โมเดลภาษาขนาดใหญ่) สามารถจัดการเนื้อหาหลักที่เป็นภาษาธรรมชาติ เพอร์โซนา และการแตกแขนงของเรื่องเล่าบนการแยกนั้นได้แล้ว — การที่ AI เขียนบทพูดให้เข้ากับโทนของคิมในบันทึกข้างต้น เป็นสิ่งที่เมื่อ 5 ปีก่อนทำไม่ได้ด้วยตารางกฎ มุมมองทางเวลานี้จะกล่าวถึงเพิ่มเติมใน 5.3
ในบทถัดไป (5.2) จะได้เห็นว่าบนห้าชั้นนี้ lore_consistency_rule ตรวจสอบความสอดคล้องตั้งแต่โลกของเกม→ตัวละคร→เควสต์โดยอัตโนมัติอย่างไร นั่นคือตัวตรวจสอบ (checker) ที่ป้องกันอุบัติเหตุคิมในเชิงโครงสร้าง
สมมติว่าเริ่มจากสถานะที่มีเอกสารโลกของเกมก้อนเดียวอยู่แล้ว
setup. สร้างโฟลเดอร์เปล่าห้าโฟลเดอร์ใต้โฟลเดอร์ NarrativeDocs ตั้งแต่ Layer0_Vision ไปจนถึง Layer4_Build_QA คงเอกสารก้อนเดียวเดิมไว้ตามเดิม แล้วเลือกเฉพาะ "บรรทัดเดียวที่ห้ามเปลี่ยนเด็ดขาด" จากตรงนี้ ย้ายไปไว้ที่ Layer0_Vision/narrative_pillar.md ใส่ status: locked ใน frontmatter อย่าให้ไฟล์นี้เกิน 4.5 หน้า
prompt. เมื่อสร้างเนื้อหาใหม่ ให้บริบทแก่ AI ตามลำดับนี้
[narrative_pillar.md ฉบับเต็ม]
[คำต้องห้ามของ tone_manifesto.md]
[ส่วนที่ตัดมาจากไฟล์ character_bible ที่เกี่ยวข้อง]
ไอเดีย: "<ไอเดียหนึ่งบรรทัด>"
อย่าฝ่าฝืนบริบทนี้ แล้วเริ่มเขียนจากไฟล์เควสต์ L2 ก่อน เติมบล็อก related
(requires/derives_from/affects) ให้ครบ อย่าใส่การตั้งค่าใหม่ที่ไม่มีใน bible
ถ้าจำเป็นก็หยุดแล้วถามมา
verify. ดูสองอย่างจากผลลัพธ์ (1) เปิดไฟล์ที่ระบุใน requires: จริง ๆ แล้วเทียบว่า AI ไม่ได้สร้างข้อเท็จจริงที่ไม่มีอยู่ที่นั่นขึ้นใหม่ (2) ค้นหาคำต้องห้ามในเนื้อหาหลัก (ถ้ามี tone_lint.py ก็อัตโนมัติ ถ้าไม่มีก็ใช้สายตา) หากติดอย่างใดอย่างหนึ่ง ให้ปฏิเสธ แล้วระบุให้ชัดว่าอะไรผิดเพื่อขอใหม่ การปฏิเสธ "ภรรยาผู้ล่วงลับ" ในบันทึกข้างต้นก็คือข้อ (1) นี่เอง
หากคุณเป็นนักพัฒนาอินดี้ที่ทำคนเดียวโดยไม่มีทีม ห้าชั้นอาจดูมากเกินไป สองช่องก็พอ
วาง vision.md หนึ่งหน้า (รวม L0+L1), โฟลเดอร์ content/ หนึ่งโฟลเดอร์ (L2) และสเปรดชีตหนึ่งหน้า (L3) ส่วน QA แทนที่จะมีเป็นชั้นแยกต่างหาก ให้ใช้นิสัยเขียน requires: เพียงบรรทัดเดียวใน frontmatter ของไฟล์ content แทน ทุกครั้งที่เขียนเควสต์ใหม่ ก็เปิดเฉพาะไฟล์ที่ระบุใน requires ขึ้นมาเทียบใหม่ ก็จะป้องกันอุบัติเหตุคิมได้โดยไม่ต้องไล่รื้อทั้ง 70 หน้า หัวใจไม่ได้อยู่ที่จำนวนชั้น แต่อยู่ที่นิสัยที่ว่า "แยกบรรทัดเดียวที่ห้ามเปลี่ยนเด็ดขาดออกมา แล้วแสดงให้ AI ดูก่อนทุกครั้ง" บรรทัดเดียวนั้นคือแองเคอร์ของบริบท และไม่ว่าจะเป็นคนเดียวหรือทีมขนาดกลาง ก็เริ่มต้นจากตรงนั้น
ก่อนเข้าสู่ช่วงเบตาไม่นาน มีบั๊กรีพอร์ตหนึ่งใบถูกส่งขึ้นมาจากฝ่าย QA หัวข้อคือ "พระราชาพูดจาไม่มีหางเสียง" เนื้อความสั้นมาก "ในคัตซีนเปิดของฉาก 3.4 K_001 (กษัตริย์) พูดกับผู้เล่นว่า 'เฮ้ เดี๋ยวก่อน' ตัวละครนี้ตั้งแต่ฉาก 1.1 จนถึง 3.3 ใช้คำว่า 'ท่าน' มาตลอด"
เมื่อถามนักเขียนบท คำตอบกลับน่าประหลาดใจ "บทพูดนั้นผมไม่ได้เขียนนะครับ" พอตามรอยดูจึงพบว่าเป็นนักเขียนภายนอก (outsource) ที่รีบเติมบรรทัดแยกสาขาของคัตซีนเข้าไปอย่างรวดเร็ว ในไบเบิลตัวละครของเรามี voice_profile อยู่ก็จริง แต่นักเขียนภายนอกคนนั้นไม่เคยเห็นเอกสารฉบับนั้นเลย กฎอยู่ในเอกสาร แต่บทพูดเข้ามาจากนอกเอกสาร
นี่คือแก่นแท้ของอุบัติเหตุด้านความสอดคล้อง มันไม่ได้เกิดเพราะไม่มีกฎ แต่เกิดเพราะกฎตามไปไม่ถึงตัวเนื้อหา และถ้าบรรทัดนี้เป็นคัตซีน เรื่องจะยิ่งน่ากลัวขึ้น คัตซีนมักมีการอัดเสียงพากย์ของนักพากย์ติดมาด้วย ตอนเป็นข้อความ แก้ครั้งเดียวก็จบ แต่ถ้าพบหลังอัดเสียงไปแล้ว ก็จะตามมาด้วยต้นทุนที่ย้อนกลับไม่ได้ คือต้องเรียกนักพากย์มาใหม่ อัดเสียงใหม่ และมิกซ์เสียงใหม่ จุดประสงค์ที่แท้จริงของการตรวจสอบความสอดคล้องคือการดักให้ได้ "ก่อนการอัดเสียง"
บทนี้ว่าด้วยเวิร์กโฟลว์ที่ดักอุบัติเหตุนั้นด้วย rulebook และตัวตรวจสอบ (checker) แทนสายตามนุษย์ เราจะดูจากตัวผลงานจริง ว่า rulebook ของ lore_consistency_rule กลายเป็นอินพุตของตัวตรวจสอบได้อย่างไร voice_lint คัดความสั่นไหวของโทนออกมาเป็นรายชื่อต้องสงสัยได้อย่างไร และเหตุใดการตัดสินขั้นสุดท้ายจึงต้องคงไว้ที่มนุษย์จนถึงที่สุด
เมื่อรวบรวมรีวิวผู้ใช้ของเกม RPG และ MMORPG ที่วางจำหน่ายแล้ว อุบัติเหตุด้านความสอดคล้องของเนื้อเรื่องจะลู่เข้าสู่รูปแบบไม่กี่อย่าง แม้ประเภทจะดูต่างกัน แต่สาเหตุแทบจะเป็นอันเดียวกัน
ทั้งห้าอย่างดูเหมือนเป็นอุบัติเหตุที่ต่างกัน แต่เมื่อตามรอยดูแล้วล้วนรั่วจากจุดเดียวกัน คือ Layer 0 (สมมติฐานของโลก) หรือ Layer 1 (กฎ) เปลี่ยนไป แต่การเปลี่ยนแปลงนั้นไม่ได้แพร่กระจายไปถึง Layer 2 (เนื้อหา) และ Layer 3 (ชีตข้อมูล) กฎถูกอัปเดตแล้ว แต่เนื้อหายังหยุดอยู่บนกฎเก่า
การจะกันเรื่องนี้ด้วยการตรวจสอบด้วยมือนั้นเกินกำลัง ในหนึ่งฉากมี NPC 50 ตัว บทพูด 2,000 บรรทัด และเควสต์ 30 อันพันกันอยู่ การที่มนุษย์จะตามรอยได้ 100% ว่าเมื่อแก้กฎไปหนึ่งบรรทัดแล้วผลกระทบจะลามไปถึงไหนนั้นเป็นไปไม่ได้ บรรทัดที่ตกหล่นไปจะไม่ถูกจับในขั้นตรวจสอบ แต่จะไปถูกจับในช่องรีวิวหลังวางจำหน่าย
ถึงอย่างนั้น การตรวจสอบอัตโนมัติก็ไม่ได้รับประกัน 100% เช่นกัน หัวใจอยู่ที่การแบ่งบทบาท การตรวจสอบอัตโนมัติคัดรายชื่อต้องสงสัยออกมาอย่างรวดเร็ว และมนุษย์เป็นผู้ตัดสิน จุดประสงค์ของการทำให้เป็นอัตโนมัติคือการลดเวลาตรวจสอบของมนุษย์ ไม่ใช่การกำจัดมนุษย์ออกไป หากทำให้สมมติฐานข้อนี้พร่าเลือน ความล้มเหลวทั้งหมดที่จะกล่าวถึงข้างหลังก็จะตามมา
เอกสาร L1 ฉบับหนึ่งของโปรเจกต์ A คือ lore_consistency_rule.md เอกสารนี้เป็นทั้งไกด์ที่มนุษย์อ่าน และในขณะเดียวกันก็เป็นอินพุตที่ตัวตรวจสอบนำไปพาร์ส atoms และ affects ในฟรอนต์แมตเตอร์มัดสองบทบาทนี้ไว้ในกายเดียวกัน
---
title: กฎความสอดคล้องของโลก (lore)
layer: L1
atoms:
- lore_check_world_rule
- lore_check_character_voice
- lore_check_timeline
- lore_check_faction_relation
related:
derives_from: [world_premise, narrative_pillar]
affects: [main_quest/*, character_bible/*, dialogue_id_table]
---
## 1. กฎของโลก (World Rule)
- เริ่มต้นในสภาวะที่เวทมนตร์ถูกห้าม → เมื่อใช้เวทมนตร์ต้องระบุ (ช่วงเวลา ผู้ใช้ การอ้างความชอบธรรม) ให้ชัดเจน
- เทพเจ้าอยู่ในสภาวะนิ่งเงียบ → ห้ามบรรยายการตอบสนองโดยตรง (อนุญาตในความฝัน·ภาพนิมิต)
## 2. กฎน้ำเสียงตัวละคร
- บังคับให้อ้างอิง voice_profile ของแต่ละตัวละคร
- เมื่อเขียนบทพูดใหม่ ต้องปฏิบัติตาม voice_profile 5 หัวข้อ (คำศัพท์ ความยาวประโยค คำยกย่อง การแสดงอารมณ์ คำต้องห้าม)
## 3. กฎเส้นเวลา
- กำหนด status_timeline ให้ NPC ทุกตัว (มีชีวิต / บาดเจ็บ / เสียชีวิต / สูญหาย / เปลี่ยนตำแหน่ง)
- ตรวจ status_timeline อัตโนมัติ ณ ช่วงเวลาของบทพูดและการปรากฏตัว
## 4. กฎความสัมพันธ์ระหว่างฝ่าย
- บันทึกช่วงเวลาที่ faction_relation_matrix เปลี่ยน
- บทพูดหลังการเปลี่ยนต้องสะท้อนความสัมพันธ์ใหม่
บรรทัด affects เพียงบรรทัดเดียวกำหนดขอบเขตการสแกนของตัวตรวจสอบ เมื่อ world_premise เปลี่ยน ตัวตรวจสอบจะกวาดดู main_quest/*, character_bible/*, dialogue_id_table ทั้งหมดอีกครั้ง งานที่มนุษย์เคยตามรอยในหัวว่า "ผลกระทบจะไปถึงไหน?" กราฟความขึ้นต่อกัน (dependency graph) ที่เขียนไว้ใน rulebook ทำหน้าที่แทน
voice_profile เป็นทรัพยากร L2 แยกต่างหากที่ rulebook นี้อ้างอิงถึง โปรไฟล์ของตัวละครหนึ่งตัวถูกป้อนเป็นค่าตัวเลขและแบบแจกแจง (enumeration) เพื่อให้ตัวตรวจสอบใช้เป็นเกณฑ์เปรียบเทียบได้
# character_bible/K_001_voice_profile.yaml
character_id: K_001
display_name: กษัตริย์
voice_profile:
vocabulary_register: โบราณ_เป็นทางการ # ระดับคำศัพท์
avg_sentence_len: 18 # ความยาวประโยคเฉลี่ย (อักขระ)
honorific: "ท่าน" # คำยกย่องบุรุษที่ 2 (ตายตัว)
emotion_expression: สำรวม # ระดับการเปิดเผยอารมณ์
forbidden_terms: ["เฮ้", "เดี๋ยวก่อน", "5555"] # คำต้องห้าม
ต้องมี yaml นี้ อุบัติเหตุที่ว่า "พระราชาพูดไม่มีหางเสียง" จึงจะกลายเป็นหัวข้อที่เครื่องเปรียบเทียบได้ ไม่ใช่สัญชาตญาณของมนุษย์ เมื่อ honorific เป็น "ท่าน" แต่ในบทพูดมี "เฮ้" นั่นไม่ใช่ความเห็น แต่เป็นรายการต้องสงสัยว่าละเมิดกฎ
ตัวตรวจสอบจะทำงานในวินาทีที่เกิดการเปลี่ยนแปลง ลำดับเป็นดังนี้
flowchart TD
A[เกิดการเปลี่ยนแปลง: สมมติฐาน L0 / กฎ L1 / เนื้อหา L2] --> B{ตัวจำแนกการเปลี่ยนแปลง}
B -->|เข้าข่าย affects ของกฎใด| C[เรียกตัวตรวจสอบที่เกี่ยวข้อง]
C --> D[สแกนเนื้อหา L2 + ชีต L3 ในขอบเขต affects]
D --> E[รายชื่อต้องสงสัย/ละเมิดกฎ]
E --> F[แนบคอมเมนต์เข้ากับคำขอเปลี่ยนแปลงอัตโนมัติ]
F --> G{การตัดสินโดยมนุษย์}
G -->|ละเมิดจริง| H[แก้เนื้อหา → ตรวจซ้ำ]
G -->|การเปลี่ยนที่ตั้งใจ| I[อัปเดต voice_profile / rulebook]
G -->|กฎไวเกินไป| J[ปรับตัวกฎเอง]
H --> K{เป็นขั้นข้อความหรือไม่}
I --> K
J --> K
K -->|ใช่: ย้อนกลับได้| L[ปิดการตรวจสอบได้]
K -->|ไม่: หลังอัดเสียง| M[ย้อนกลับไม่ได้ — ต้นทุนอัดเสียงใหม่]
L -.เส้นกั้น.-> M
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class B,C,D,F code;
class G human;
class A,E data;
class L pass;
class M fail;
สาขาสุดท้ายคือกระดูกสันหลังที่ซ่อนอยู่ของบทนี้ การตัดสินความสอดคล้องทั้งหมดต้อง จบในขั้นข้อความ นั่นคือขั้นที่ย้อนกลับได้ หากการตรวจสอบเลยไปสู่หลังการอัดเสียงและคัดเลือกนักพากย์ การแก้ไขก็จะกลายเป็นเรื่องที่ย้อนกลับไม่ได้ ด้วยเหตุนี้ ตัวตรวจสอบอย่าง voice_lint และ timeline_lint จึงไม่ได้สำคัญที่ทำงาน "เร็ว" แต่สำคัญที่ทำงาน แต่เนิ่น ๆ บทพูดของคัตซีนต้องผ่านการตรวจอย่างน้อยหนึ่งครั้งก่อนเข้าคิวอัดเสียง
ตัวตรวจสอบมีสี่ชนิด แต่ละชนิดสอดคล้องแบบหนึ่งต่อหนึ่งกับหนึ่งหัวข้อใน rulebook
world_rule_lint.py — กฎโลก L1 + เนื้อหา L2 ทั้งหมด → รายการต้องสงสัยว่าละเมิด เช่น การใช้เวทมนตร์·การตอบสนองของเทพเจ้าvoice_lint.py — voice_profile + dialogue_id_table → บทพูดต้องสงสัยว่าน้ำเสียงสั่นไหวtimeline_lint.py — npc status_timeline + ช่วงเวลาของบทพูดและการปรากฏตัวทั้งหมด → ความขัดแย้ง เช่น NPC ที่ตายแล้วกลับมาปรากฏfaction_lint.py — faction_relation_matrix + dialogue tone → บทพูดที่ขัดแย้งกับความสัมพันธ์ตัวตรวจสอบทั้งสี่ไม่ได้แม่นยำ 100% ด้วยเหตุนี้ชื่อของผลลัพธ์จึงไม่ใช่ "การละเมิด" แต่เป็น "รายการต้องสงสัย"
คำว่า "มีตัวตรวจสอบ" แบบนามธรรมนั้นยังจับต้องไม่ได้ เราจะลองรันจริงสักหนึ่งรอบ นี่คืออินพุตที่จำลองอุบัติเหตุ "พระราชาพูดไม่มีหางเสียง" ข้างต้น
setup — ดึงบทพูดสองบรรทัดที่จะตรวจสอบมาจาก dialogue_id_table
dialogue_id_204 speaker=K_001 text="เฮ้ เดี๋ยวก่อนสิ ฝั่งนั้นพูดมาก่อนเลย"
dialogue_id_217 speaker=K_007 text="...เวรเอ๊ย ล้มเหลวอีกแล้ว เริ่มใหม่ตั้งแต่ต้น"
prompt — ตัวตรวจสอบเองทำงานบนพื้นฐานของกฎ (regex·การเทียบความยาว·การจับคู่คำต้องห้าม) AI จะถูกใส่ไว้เฉพาะตำแหน่งที่อธิบาย "ทำไมจึงต้องสงสัย" ด้วยภาษาธรรมชาติที่นักเขียนบทอ่านได้ พรอมต์ LLM ของขั้นตรวจสอบมีเนื้อความเต็มดังนี้
นี่คือรายการต้องสงสัยที่ voice_lint คัดออกมา อย่าตัดสิน แต่ให้อธิบาย
เฉพาะว่าแต่ละรายการน่าจะละเมิดหัวข้อใดของ voice_profile พร้อมเหตุผล 1~2 ประโยค
อย่าฟันธงว่า "ละเมิด/ไม่ละเมิด" ให้พูดแค่ "ต้องสงสัยว่า~" เท่านั้น
[voice_profile]
K_001: honorific="ท่าน", avg_sentence_len=18, forbidden=["เฮ้","เดี๋ยวก่อน"]
K_007: forbidden=["เวรเอ๊ย"]
[รายการต้องสงสัย]
- dialogue_id_204 (K_001): "เฮ้ เดี๋ยวก่อนสิ ฝั่งนั้นพูดมาก่อนเลย"
- dialogue_id_217 (K_007): "...เวรเอ๊ย ล้มเหลวอีกแล้ว เริ่มใหม่ตั้งแต่ต้น"
ผลลัพธ์ดิบจาก Claude (ถอดมาตามเดิม ไม่ได้สรุปหรือเรียบเรียง)
[dialogue_id_204 / K_001]
บทพูดนี้ต้องสงสัยว่าขัดแย้งกับหัวข้อ honorific voice_profile กำหนดคำยกย่อง
บุรุษที่ 2 ไว้ตายตัวเป็น "ท่าน" แต่บทพูดนี้ใช้ "ฝั่งนั้น" และไม่ปรากฏคำยกย่อง
ตัวมันเองเลย อีกทั้ง "เฮ้" และ "เดี๋ยวก่อน" ใน forbidden_terms ปรากฏทั้งคู่
จึงต้องสงสัยว่าหลุดออกจากระดับคำศัพท์ (โบราณ_เป็นทางการ) แม้จะดูไม่ขัดกับ
เกณฑ์ความยาวประโยคเฉลี่ย 18 อักขระโดยตรง แต่การที่น้ำเสียงเองกลายเป็นกันเอง
ก็ต้องสงสัยว่าเป็นสัญญาณการละเมิด
[dialogue_id_217 / K_007]
"เวรเอ๊ย" ใน forbidden_terms ถูกใช้ตรง ๆ จึงต้องสงสัยว่าละเมิดหัวข้อคำต้องห้าม
อย่างไรก็ตาม เนื่องจากไม่ได้ระบุหัวข้อ emotion_expression ของ K_007 (นักวิชาการ)
มาให้ จึงยากที่จะตัดสินด้วยข้อมูลเพียงเท่านี้ว่าการแสดงออกนี้เป็นการขับเน้นอารมณ์
ที่ตั้งใจ หรือเป็นเพียงการหลุดออกนอกกรอบ ต้องให้นักเขียนบทพิจารณาตัดสิน
ในผลลัพธ์นี้มีความตรงไปตรงมาสองอย่างที่เราจงใจคงไว้ ประการแรก ใน 204 AI บอกว่า "ความยาวประโยคเฉลี่ยดูไม่ขัดกันโดยตรง" จึง ไม่ดันทุรังว่าหัวข้อที่ไม่ได้ละเมิดเป็นการละเมิด ประการที่สอง ใน 217 AI ยอมรับว่า "เนื่องจากไม่ได้ระบุหัวข้อ emotion_expression มาให้จึงตัดสินยาก" และ ยอมรับว่าข้อมูลไม่พอ แล้วส่งการตัดสินคืนให้มนุษย์ ถ้า AI ดันทุกข้อสงสัยให้กลายเป็น "ยืนยันว่าละเมิด" นั่นคือตัวตรวจสอบที่อันตรายกว่า
verify — นักเขียนบทรับคอมเมนต์นี้มาตามเดิมจากคำขอเปลี่ยนแปลง การตัดสินเป็นหน้าที่ของนักเขียนบท
emotion_peak_exception ใน voice_profile แล้วลงทะเบียน 217 เป็นข้อยกเว้นตัวตรวจสอบเดียวกันคัดสองรายการนี้ออกมา แต่บทสรุปกลับตรงข้ามกัน อันหนึ่งแก้เนื้อหา อีกอันแก้กฎ การที่เครื่องทำการแยกสาขานี้เองโดยอัตโนมัติไม่ได้ คือหัวใจของหัวข้อถัดไป
ที่ตัวตรวจสอบคัดได้แค่ระดับต้องสงสัยแล้วส่งการตัดสินต่อไป มีอยู่สามเหตุผล
ประการแรก การละเมิดที่ตั้งใจมีอยู่จริง ในฉากที่ตัวละครพังทลายหรือเปลี่ยนแปลง น้ำเสียงจะสั่นไหวอย่างจงใจ 217 ข้างต้นคือกรณีนั้น ตัวตรวจสอบแบบปฏิเสธอัตโนมัติจะปิดกั้นเจตนาการกำกับของนักเขียนบท
ประการที่สอง ตัวกฎเองวิวัฒน์ หากข้อสงสัยชนิดเดียวกันถูกตัดสินเป็น "การเปลี่ยนที่ตั้งใจ" อยู่เรื่อย ๆ นั่นคือสัญญาณว่ากฎตามความเป็นจริงไม่ทัน ผลการตรวจไม่ได้ทำให้แก้แค่เนื้อหา แต่ทำให้แก้ rulebook ด้วย
ประการที่สาม ตัวละครและฝ่ายใหม่ต้องการช่วงเรียนรู้ NPC ใหม่ที่ voice_profile ยังถูกเติมแค่สองสามหัวข้อนั้น เป็นเรื่องปกติที่จะมีข้อสงสัยขึ้นมาเยอะ หากในช่วงนี้ตั้งให้ปฏิเสธอัตโนมัติ นักเขียนบทก็จะมองตัวตรวจสอบเป็นศัตรู
เส้นแบ่งระหว่างการตรวจอัตโนมัติกับการตัดสินของมนุษย์ต้องชัดเจน ตัวตรวจสอบจึงจะอยู่รอด หากทำเป็นแบบปฏิเสธอัตโนมัติ ภายในหนึ่งเดือนเหล่านักเขียนบทจะบอกว่า "ปิดมันซะเถอะ" เหมือนกับการติดเซ็นเซอร์อัตโนมัติที่ไวเกินไปไว้ที่ประตูทางเข้าบริษัท พอคนเดินผ่านทีไรประตูก็ปิด สุดท้ายก็จะมีใครสักคนถอดเซ็นเซอร์นั้นทิ้ง ตัวตรวจสอบไม่ใช่อุปกรณ์ที่ทำหน้าที่ปิดประตู แต่ต้องเป็นอุปกรณ์ที่คอยบอกว่า "มีคนเดินผ่านมาทางนี้"
ขอเสริมข้อสังเกตหนึ่งข้อ หลักการที่ว่าการตรวจสอบต้องจบในขั้นข้อความ (เส้นกั้นในผังลำดับข้างต้น) ใช้กับการตัดสินของมนุษย์ด้วยเช่นกัน การตัดสิน "การละเมิดที่ตั้งใจ" ของนักเขียนบทก็ต้องจบก่อนการอัดเสียง การกลับคำหลังอัดเสียงไปแล้วจะเปลี่ยนจากปัญหาของตัวตรวจสอบกลายเป็นปัญหาต้นทุนกระบวนการผลิต (รายละเอียดทั้งหมดของเส้นแบ่งย้อนกลับได้/ย้อนกลับไม่ได้อยู่ที่ 5.4.5)
ในโปรเจกต์ A เราทยอยนำตัวตรวจสอบทั้ง 4 ชนิดมาใช้เป็นขั้น ๆ และวัดผลตลอด 6 เดือน ด้านล่างอ้างอิงจากบันทึกการวัดจริง แต่ถอดมาเป็นทิศทาง·อัตราส่วนแทนค่าสัมบูรณ์ (วัดภายในบริษัท ไม่ใช่การประมาณของผู้เขียน)
ข้อสุดท้ายน่าสนใจที่สุด เมื่อมีตัวตรวจสอบ การเปลี่ยนกฎบ่อยก็ปลอดภัย เพราะพอแก้กฎไปหนึ่งบรรทัด ผลกระทบจะปรากฏให้เห็นโดยอัตโนมัติ ความกลัวการเปลี่ยนแปลงจึงลดลง และกฎก็วิวัฒน์เร็วขึ้น ผลที่แท้จริงของเครื่องมือด้านความสอดคล้องจึงไม่ใช่ "ลดอุบัติเหตุได้" แต่ค่อนไปทาง "ทำให้กล้าเปลี่ยนกฎโดยไม่กลัว" เสียมากกว่า
อนึ่ง ตัวเลขข้างต้นเป็นตัวเลข ณ ช่วงที่ตัวตรวจสอบทั้ง 4 ชนิดทำงานครบ สิ่งที่สำคัญกว่าคือในช่วงเริ่มต้น แค่ voice_lint อันเดียวก็ให้ผลที่เห็นได้ชัดแล้ว ไม่จำเป็นต้องเปิดครบทั้ง 4 ชนิดตั้งแต่แรก
ตัวตรวจสอบอัตโนมัติส่วนหลักนั้นใช้แบบอิงกฎจะมีประสิทธิภาพกว่า เพราะอินพุตเดียวกันต้องให้ผลเดียวกัน ความเชื่อมั่นจึงจะสะสม ในขณะที่ LLM เป็นแบบไม่กำหนดผลแน่นอน (non-deterministic) จึงไม่เหมาะกับตำแหน่งนั้น AI เข้าไปอยู่ในอีกสี่ตำแหน่ง
กฎนั้นเร็วและกำหนดผลแน่นอน ส่วน LLM เก่งเรื่องการอธิบายและการสร้าง หากเอาบทบาทของสองสิ่งนี้มาปนกันจะพังทั้งคู่ ถ้าฝากการตรวจไว้กับ LLM จะเกิดเรื่องที่บทพูดเดียวกันเมื่อวานผ่านแต่วันนี้ติด และถ้าฝากการอธิบายไว้กับ regex ก็จะได้แค่ภาษาเครื่องว่า "ละเมิดหัวข้อ honorific" ออกมาเท่านั้น
ถ้าทำตัวตรวจสอบครบทั้ง 4 ชนิดตั้งแต่แรก ภาระจะมาก่อนผล ลำดับที่แนะนำคือเริ่มจากอันที่ถูกที่สุดและให้ผลมากที่สุด
ขอย้ำว่าแค่ขั้นที่ 2 (voice_lint) อันเดียวก็ให้ผลมากแล้ว อุบัติเหตุ "พระราชาพูดไม่มีหางเสียง" ข้างต้นคือชนิดที่ดักได้ด้วยขั้นนี้เพียงขั้นเดียวพอดี
ความล้มเหลวที่เกิดซ้ำในกระบวนการนำมาใช้ก็แทบจะกำหนดไว้ตายตัวแล้วเช่นกัน
ข้อสุดท้ายแพงกว่าข้ออื่นทั้งหมดข้างต้น ความล้มเหลวอื่นทำให้เสียเวลา แต่ความล้มเหลวนี้ทำให้เสียตารางงานของนักพากย์
ในบทถัดไป (5.3) จะว่าด้วยลำดับการเขียนเนื้อหาเนื้อเรื่องด้วย AI ช่วยเสริมแทนตัวตรวจสอบ เราจะดูวิธีฉีดโทน L0 และกฎ L1 เข้าไปเป็นคอนเทกซ์ เพื่อให้ AI ออกคำตอบของโลกของเราแทนที่จะเป็นคำตอบทั่วไป
setup — เลือกตัวละคร 1 ตัวจาก character_bible แล้วเติม voice_profile 5 หัวข้อ (ระดับคำศัพท์·ความยาวประโยคเฉลี่ย·คำยกย่อง·การแสดงอารมณ์·คำต้องห้าม) ให้ครบสมบูรณ์เป็น yaml จากนั้นดึงบทพูดเดิม 10 บรรทัดของตัวละครเดียวกันมาจาก dialogue_id_table รวมไว้ในไฟล์เดียว
prompt — ใช้พรอมต์ช่วยตรวจจาก worked transcript ข้างต้นตามเดิม หัวใจคือข้อจำกัดสองข้อ "อย่าตัดสิน" และ "พูดแค่ \~ต้องสงสัยว่า เท่านั้น" แนบ voice_profile yaml และบทพูด 10 บรรทัดเข้าไปในอินพุต
verify — ตัดสินรายการต้องสงสัยที่ออกมาทีละบรรทัดด้วยตัวคุณเอง ถ้าละเมิดจริงก็แก้เนื้อหา ถ้าเป็นการเปลี่ยนที่ตั้งใจก็เพิ่มแฟล็กข้อยกเว้นใน voice_profile ตรวจไปพร้อมกันด้วยว่า AI ดันทุรังว่า "หัวข้อที่ไม่ได้ละเมิด" เป็นการละเมิดหรือไม่ และยอมรับ "ข้อมูลไม่พอ" หรือไม่ หาก AI ฟันธงว่าทุกหัวข้อเป็นการละเมิด ให้เสริมข้อจำกัด "อย่าตัดสิน" ในพรอมต์ให้แข็งแรงขึ้น
หากเป็นการพัฒนาคนเดียวที่ไม่มีทั้งตัวตรวจสอบ 4 ชนิดและ rulebook คุณก็ยังสร้างผลแบบเดียวกันได้ด้วยพรอมต์เพียงอันเดียวโดยไม่ต้องมีตัวตรวจสอบส่วนหลัก แค่ดูแล voice_profile yaml ของแต่ละตัวละครด้วยมือ และทุกครั้งที่เขียนบทพูดใหม่ ก็แนบ yaml ของตัวละครนั้น + บทพูดใหม่เข้ากับพรอมต์ช่วยตรวจข้างต้นเพื่อรับ "รายการต้องสงสัย" ไม่มีระบบอัตโนมัติก็จริง แต่โครงสร้างหัวใจที่ว่า มนุษย์เป็นผู้ตัดสิน AI เป็นผู้อธิบาย ยังคงอยู่เหมือนเดิม แค่รักษาบรรทัดเดียวให้ได้ก็พอ — ผ่านการตรวจนี้สักหนึ่งครั้งก่อนส่งต่อให้การอัดเสียงและการสังเคราะห์เสียง หลักการที่ว่าไม่ปล่อยให้ข้ามช่วงที่ย้อนกลับได้ไปนั้น ไม่ขึ้นกับขนาดของทีม
วันนั้นเป็นวันที่ผมกำลังร่างบทพูดแรกของ NPC ข้างเคียงตัวใหม่ ผมพิมพ์ลงในช่องแชตที่ว่างเปล่าว่า "ช่วยสร้างบทพูดให้ NPC ช่างตีเหล็กในหมู่บ้าน 5 บรรทัด" 5 วินาทีต่อมา หน้าจอก็แสดง "ผู้กล้าเอ๋ย จงมอบอาวุธของเจ้าไว้กับข้า" มันไม่ใช่แค่ประโยคที่รู้สึกเหมือนเคยเห็นที่ไหนสักแห่ง แต่เป็นประโยคที่ผมแทบจะบอกได้ว่าเห็นมาจากที่ไหนแน่ ๆ ต่อให้เอาพรอมต์เดียวกันไปใส่ในเกมอื่นของทีมอื่น ก็จะได้คำตอบแบบเดียวกันกลับมา สิ่งที่ผมตระหนักได้ในชั่วขณะนั้นไม่ใช่ว่าโมเดลอ่อนแอ แต่เป็นว่าผมไม่ได้บอกอะไรเกี่ยวกับเกมของเราให้โมเดลรู้เลยสักอย่าง
AI เขียนประโยคแฟนตาซีทั่ว ๆ ไปได้ดี แต่เขียนประโยคของโลกในเกมเราไม่ได้ ความต่างมีเพียงสิ่งเดียว นั่นคือการป้อนบริบท (context) ถ้าส่งโทน L0 และกฎ L1 ไปพร้อมกันในทุก ๆ คำขอ บรรทัดที่ AI พ่นออกมาจะเปลี่ยนจาก "ประโยคที่รู้สึกเหมือนเคยเห็นที่ไหนสักแห่ง" ไปเป็น "ประโยคของเกมนี้" บทนี้ว่าด้วยการปฏิบัติจริงในการบริหารการป้อนนั้นเป็น 4 ชั้น และในตอนท้ายจะชี้ให้เห็นถึงการประยุกต์ใช้แบบก้าวหน้า (world BT (BehaviorTree, ต้นไม้พฤติกรรม) + คลาวด์เควสต์) ที่ยกหลักการเดียวกันขึ้นไปสู่ระดับการจำลองโลกทั้งใบ ในฐานะแนวหน้าของงาน RnD
ในสายงานเนื้อเรื่อง การมี AI ช่วยเป็นพื้นที่ที่ถูกนำมาใช้เร็วที่สุด และสูญเสียความเชื่อมั่นเร็วที่สุดเช่นกัน เพราะรูปแบบความล้มเหลวแทบจะเหมือนกันทุกครั้ง
ถ้าโยนคำสั่ง "บทพูดเริ่มเควสต์ 5 บรรทัด" ไป ก็จะได้แฟนตาซีทั่วไป 5 แบบที่ขึ้นต้นด้วย "ผู้กล้าเอ๋ย หมู่บ้านของเรา..." กลับมา ถ้าบอก "ช่วยแก้บทพูดของตัวละครนี้หน่อย" เสียง (voice) ก็จะถูกปรับให้กลายเป็นมาตรฐานเดียวกัน จน NPC ทุกตัวลู่เข้าหาน้ำเสียงที่คล้าย ๆ กัน ถ้าบอก "ช่วยเขียนเรื่องย่อบทที่ 1" ก็จะได้ค่าเฉลี่ยของเรื่องย่อ RPG ที่เคยเห็น ๆ มาออกมา
ปัญหาไม่ได้อยู่ที่โมเดล แต่อยู่ที่บริบทว่างเปล่า โมเดลพ่นค่าเฉลี่ยของข้อมูลที่ใช้เทรนออกมา ถ้าไม่ต้องการค่าเฉลี่ย ก็ต้องให้เบาะแสที่จะพาให้ห่างจากค่าเฉลี่ย หัวข้อของบทนี้คือ จะสร้างเบาะแสนั้นอย่างไรและจะป้อนเข้าไปอย่างไร
ในโปรเจกต์ MMORPG ที่ผู้เขียนดูแล (ต่อไปนี้เรียกว่าโปรเจกต์ A) การมี AI ช่วยงานเนื้อเรื่องจะวางบริบท 4 ชั้นเรียงตามลำดับ โครงสร้างที่แยก NarrativeDocs ออกเป็น Layer 0\~4 ใน 5.1 ถูกนำกลับมาใช้เป็นหน่วยของการป้อนตรงนี้นั่นเอง
ไม่ได้ใส่ครบทั้ง 4 ชั้นทุกครั้ง ดึงออกมาเฉพาะชั้นที่จำเป็นตามประเภทของงาน ถ้าเป็นร่างบทพูดถัดไปของตัวละครหนึ่ง A + B (เฉพาะโทน) + D (บทพูดล่าสุด 10 บรรทัดของตัวละครนั้น) ก็เพียงพอ ถ้าเป็นเรื่องย่อของเควสต์ข้างเคียงตัวใหม่ ก็เพิ่ม C (กฎโครงสร้าง quest) เข้าไป ถ้าเป็นทางแยก 4 แบบของผลลัพธ์ ก็จะหนักขึ้นด้วย C (กฎทางแยก) + D (เนื้อหาทั้งหมดก่อนหน้าทางแยก) เปรียบเสมือนหยิบชีตเพอร์โซนา ภาพรวมโลกหนึ่งบรรทัด หน้ารูลบุ๊ก และชุดเนื้อหาใกล้เคียงจากแฟ้มเอกสารบนโต๊ะ มาเลือกส่งให้พอดีกับขนาดของงาน
แทนที่จะอธิบายแบบนามธรรม จะตามคำขอจริงหนึ่งคำขอไปจนจบ เป็นงานร่างบทพูด 3 แบบในฉากที่ตัวละครเดียวกัน (NPC แบบนักวิชาการ ID ภายในบริษัทคือ K_007) ต้องเผยอารมณ์ออกมาเป็นครั้งแรก เริ่มจากตัวพรอมต์เต็ม
พรอมต์ที่ส่งไป (Layer A + B (โทน) + D + คำสั่งงาน + รูปแบบผลลัพธ์):
[ระบบ]
นายคือนักเขียนเนื้อเรื่องของโปรเจกต์ A อย่าใช้คำซ้ำซากของ RPG อย่าง "ผู้กล้า"·"ผู้ถูกเลือก"
น้ำเสียงให้ตามโทนของบทพูดก่อนหน้าด้านล่างเป๊ะ ๆ เซตติงที่ไม่รู้อย่ากุขึ้นเอง
ให้ตอบว่า "จากบริบทไม่สามารถตัดสินได้" นายเขียนแค่ร่าง การตัดสินใจเป็นของฉัน
[โทน L0 — ตัดตอนจาก tone_manifesto]
อารมณ์ของโลกใบนี้คือการเก็บงำ ตัวละครจะไม่พูดความรู้สึกออกมาง่าย ๆ
อารมณ์เผยออกมาไม่ใช่ในรูปการระเบิด แต่ในรูปรอยร้าว ประโยคขาดห้วนและสั้น
[อ้างอิง — บทพูด 5 บรรทัดล่าสุดของ K_007 (เรียงตามเวลา ต้นฉบับตามเดิม)]
1. "...อันนั้นผมเองก็เพิ่งเคยได้ยินเหมือนกันนะครับ"
2. "มีบันทึกไหม ถ้าไม่มีก็ต้องสงสัยไว้ก่อน"
3. "อย่าเพิ่งขยับจนกว่าจะแน่ใจ"
4. "ความเห็นของผมก็มีแค่นั้น"
5. "...ขอโทษครับ ผมเย็นชาเกินไปหรือเปล่า"
[สถานการณ์]
NPC เพื่อนร่วมทางกลับมาในสภาพบาดเจ็บ นักวิชาการต้องเผยอารมณ์ออกมาเป็นครั้งแรก
ขอแค่ 3 แบบเท่านั้น แต่ละแบบไม่เกิน 25 ตัวอักษร ต่อท้ายแต่ละแบบด้วยป้ายกำกับ (ประเภทปฏิกิริยา: โกรธ/เศร้า/ปฏิเสธ)
ตัดคำอธิบายและคำเกริ่นนำออก
ผลลัพธ์ดิบของ Claude (ตามเดิม ไม่สรุปย่อหรือดัดแปลง):
1. "...ทำไมต้องแบกมันไว้คนเดียว โง่จริง ๆ" (ประเภทปฏิกิริยา: เศร้า)
2. "ผมน่าจะดูบันทึก ผม ผมพลาดไปเอง" (ประเภทปฏิกิริยา: ปฏิเสธ)
3. "ไม่เป็นไร ทุกอย่างไม่เป็นไร เพราะงั้นหยุด—" (ประเภทปฏิกิริยา: ปฏิเสธ)
จุดสำคัญคือไม่หยุดอยู่ตรงนี้ นักเขียนอ่านทั้งสามแบบแล้วตรวจสอบ แบบที่ 1 โทนถูกต้อง รอยร้าวที่บทพูดล่าสุดบรรทัดที่ 5 "...ขอโทษครับ" แสดงออกมา แบบที่ 1 รับช่วงต่อด้วยคำเดียวว่า "โง่จริง ๆ" เป็นตัวเลือกที่จะรับไว้ แบบที่ 2 การพูดติด ๆ ขัด ๆ ของ "ผม ผมพลาดไปเอง" ขัดกับการเก็บงำของนักวิชาการอยู่นิดหน่อย พักไว้ก่อน แบบที่ 3 ป้ายกำกับผิด การพูดซ้ำ "ไม่เป็นไร" ไม่ใช่การปฏิเสธ แต่ใกล้เคียงการหลีกหนีมากกว่า ซึ่งใน voice_profile ของเราไม่มีรูปแบบที่นักวิชาการจัดการอารมณ์ด้วยการหลีกหนี ปฏิเสธ
คำขอใหม่ (ส่งไปอีกครั้งพร้อมระบุเหตุผลที่ปฏิเสธ):
แบบที่ 3 ปฏิเสธ การพูดซ้ำ "ไม่เป็นไร" ไม่ใช่รูปแบบของนักวิชาการคนนี้ หมอนี่จัดการอารมณ์
ไม่ใช่ด้วยการหลีกหนี แต่ด้วย 'การโทษตัวเองที่ตรวจสอบข้อเท็จจริงล้มเหลว' แบบที่ 1 ฉันจะรับไว้
ในตำแหน่งของแบบที่ 2·3 ขอสองแบบที่ผิวสัมผัสต่างจากแบบที่ 1 มาใหม่ ไม่ต้องพูดติดขัด ไม่เกิน 25 ตัวอักษร
ผลลัพธ์ครั้งที่สองของ Claude:
2. "บันทึกไม่โกหก ผมต่างหากที่ไม่ได้ดู" (ประเภทปฏิกิริยา: โทษตัวเอง)
3. "...นั่งลงก่อน รักษาแผลก่อน ค่อยพูดทีหลัง" (ประเภทปฏิกิริยา: เศร้า)
คราวนี้ผ่านทั้งสองแบบ แบบที่ 2 นำคำศัพท์หลักของนักวิชาการอย่าง "บันทึก" (บทพูดล่าสุดบรรทัดที่ 2 "มีบันทึกไหม") กลับมาเขียนใหม่ให้เป็นสื่อกลางของการโทษตัวเอง ส่วนแบบที่ 3 แสดงรูปแบบที่นักวิชาการกดอารมณ์ลงด้วยการกระทำ ผ่านประโยคสั่งแบบเก็บงำ การรับขั้นสุดท้ายคือแบบที่ 1 + แบบที่ 2 + แบบที่ 3 สามบรรทัดนี้ผ่านการตรวจสอบอัตโนมัติ voice_lint ใน 5.2 แล้วถูกนำไปสะท้อนในเนื้อหา L2 และได้รับ dialogue_id ออกมาที่ L3
ในบันทึกเซสชันชุดเดียวนี้มีทุกอย่างของบทนี้อยู่ครบ การป้อนโทน (L0) ช่วยกู้แบบที่ 1 ไว้ เนื้อหาใกล้เคียงที่เป็นต้นฉบับตามเดิม (L2) ทำให้คำศัพท์ "บันทึก" ของนักวิชาการถูกนำกลับมาใช้ในคำขอใหม่ การบังคับรูปแบบผลลัพธ์กันการพูดเรื่อยเปื่อย และด่านปฏิเสธของนักเขียนกรองป้ายกำกับที่ผิดของแบบที่ 3 ออกไป AI ไม่ได้ตัดสินใจขั้นสุดท้ายแม้แต่บรรทัดเดียว
เป็นนิยามเพอร์โซนาที่ปูอยู่บนสุด กำหนดไว้ครั้งเดียวแล้วแทบไม่เปลี่ยน บล็อกระบบในบันทึกเซสชันข้างต้นคือตัวจริงของมัน ในห้าบรรทัด บรรทัดสุดท้าย ("เขียนร่าง การตัดสินใจเป็นของนักเขียน") สำคัญที่สุด ถ้าขาดบรรทัดนี้ไป AI จะเสนอประโยคที่ทำเป็น "ขั้นสุดท้าย" ออกมาอย่างมั่นใจ แล้วนักเขียนก็จะกลายเป็นคนให้คะแนนแทนที่จะตรวจสอบ และบรรทัดที่สาม ("เซตติงที่ไม่รู้อย่าสร้างขึ้น ให้ตอบว่าตัดสินไม่ได้") สำคัญเป็นอันดับสอง ถ้าไม่มีบรรทัดนี้ โมเดลจะเติมช่องว่างด้วยคำโกหกที่ดูเข้าท่า ในงานเนื้อเรื่อง คำโกหกที่ดูเข้าท่าจะย้อนกลับมาเป็นความขัดแย้งของ lore (เนื้อเรื่องโลก) ในอีกไม่กี่วันถัดมา
L0 มีปริมาณน้อย (ตามเกณฑ์ 5.1 ประมาณ 4.5 บท) แทบจะป้อนทั้งหมดได้ทุกครั้ง โดยประมาณตามเกณฑ์ภาษาเกาหลี world_premise.md ประมาณ 2,500 โทเค็น narrative_pillar.md ประมาณ 1,500 โทเค็น tone_manifesto.md ประมาณ 3,000 โทเค็น รวมกันประมาณ 7,000 โทเค็น (ตัวเลขเหล่านี้เป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) จะเปลี่ยนไปตาม tokenizer และการปรับปรุงเอกสาร)
ถ้าส่ง 7,000 โทเค็นใหม่ทุก ๆ คำขอ ค่าใช้จ่ายจะสะสมขึ้น ดังนั้นจึงเปิดใช้แคชชิงพรอมต์ (prompt caching) เป็นฟีเจอร์ที่ทั้ง Anthropic และ OpenAI รองรับ และเมื่อแคชเข้าเป้า (cache hit) ค่าใช้จ่ายโทเค็นอินพุตจะลดลงมาก หัวใจคือการแยกสิ่งที่เปลี่ยนกับสิ่งที่ไม่เปลี่ยนออกจากกันภายในข้อความ
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": [
{"type": "text", "text": L0_FULL, "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": L1_SELECTED, "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": L2_ADJACENT}, # เปลี่ยนทุกครั้ง — ไม่แคช
{"type": "text", "text": TASK_INSTRUCTION}, # เปลี่ยนทุกครั้ง
]},
]
L0 และ L1 ที่ติด cache_control เป็นเป้าหมายของการแคช ส่วนเนื้อหาใกล้เคียง L2 และคำสั่งงานเปลี่ยนทุกครั้งจึงไม่แคช การรวมบล็อกแคชไว้ส่วนหน้าของข้อความเสมอเป็นตัวกำหนดอัตราเข้าเป้า ถ้าบล็อกที่เปลี่ยนแทรกเข้ามาด้านหน้า แคชที่ตามหลังจะถูกทำให้เป็นโมฆะทั้งหมด การวางลำดับนี้ผิดคือสาเหตุที่พบบ่อยที่สุดของการเปิดแคชชิงแล้วค่าใช้จ่ายไม่ลดลง
รายละเอียดของตัวเลขอัตราการเข้าเป้าแคชและการประหยัดค่าใช้จ่าย จะกล่าวในบทของ ส่วนที่ 22 (ค่าใช้จ่าย) ตรงนี้แค่จำหลักการ "ไล่สิ่งที่เปลี่ยนไปไว้ข้างหลัง" ก็พอ
รูลบุ๊ก L1 มีปริมาณมาก ถ้าใส่ทั้งหมดบริบทจะแตก และที่แย่กว่านั้นคือโมเดลจะพลาดประเด็นหลัก เลือกเฉพาะกฎที่เกี่ยวกับงาน แถมยังเลือกเฉพาะหัวข้อ _summary อีกด้วย
ตอนร่างผลลัพธ์ทางแยกของเควสต์หลัก จะเลือก dialogue_branching_rule และ faction_relation_matrix ถ้าเป็นบทพูด NPC ตัวใหม่ ก็เป็น voice_profile และ tone_manifesto ของ NPC ตัวนั้น ถ้าเป็นรายการใหม่ในพจนานุกรม lore ก็เป็น lore_consistency_rule และ world_premise ถ้าเป็นโครงเควสต์ข้างเคียง ก็เป็น quest_template และ reputation_model การเลือกทำโดยคนเองโดยตรง หรือสกัดอัตโนมัติตามกราฟ wikilink (ส่วนที่ 7) ซึ่งเมื่อสกัดอัตโนมัติจะให้ความสำคัญกับ recall มากกว่า precision เพราะความเสียหายจากการที่กฎหนึ่งข้อตกหล่นไป มากกว่าความเสียหายจากการที่กฎหนึ่งข้อเข้ามาเกินมาก
แทนที่จะใส่เนื้อหารูลบุ๊กทั้งหมด ให้วางหัวข้อ _summary ไว้ที่หัวไฟล์รูลบุ๊กแล้วป้อนแค่นั้น
---
title: กฎทางแยก
layer: L1
---
## _summary
- ทางแยกเกิดเฉพาะตอนจบบทเท่านั้น
- ทางแยกมี 2~3 แบบ ห้ามเกิน 4 แบบขึ้นไป
- การเลือกทางแยกส่งผลต่อชื่อเสียง +/-1 ทางแยกตอนจบ +/-3
- ผลลัพธ์ของทุกทางแยกต้องแสดงผลภายใน 24 ชั่วโมง
- ทางแยกย้อนกลับไม่ได้ (แนะนำแยกเซฟ แสดงผลบน UI)
## 1. กฎจุดเวลาที่ทางแยกเกิดขึ้น
(คำอธิบายละเอียด สำหรับผู้ดูแลอ้างอิง — ไม่ป้อนให้ LLM)
...
_summary 5 บรรทัดมีผลต่อคุณภาพผลลัพธ์ของ LLM มากกว่าเนื้อหา 50 บรรทัด โมเดลปฏิบัติตามกฎที่สั้นและฟันธงได้ดีกว่า คำอธิบายยาว ๆ ทำให้ความสนใจของโมเดลกระจาย และความสนใจที่กระจายก็ย้อนกลับมาเป็นการละเมิดกฎ
บทพูดล่าสุด เควสต์ใกล้เคียง เรื่องย่อบทเดียวกัน เป็นบริบทที่ผันผวนมากที่สุด สำหรับบทพูดใหม่ของตัวละครเดียวกัน ให้ใส่บทพูดล่าสุด 10 บรรทัดของตัวละครนั้นเรียงตามเวลา สำหรับเควสต์กลางบท ให้ใส่เรื่องย่อบทและสรุปเควสต์ในบทเดียวกันบรรทัดละเรื่อง สำหรับตอนจบที่เป็นผลของทางแยก ให้ใส่เนื้อหาทั้งหมดก่อนหน้าทางแยกและข้อความของตัวเลือก ถ้าใส่มากเกินไป LLM จะพ่นค่าเฉลี่ยออกมา ถ้าใส่น้อยเกินไปก็จะได้ผลลัพธ์แบบทั่วไป จุดที่เหมาะสมอยู่ระหว่างโทเค็น 1,500\~3,000 (ตามการสังเกตของผู้เขียน ยังไม่ได้ตรวจสอบ)
กฎหลักหนึ่งข้อ เนื้อหาใกล้เคียงให้ใส่เป็นต้นฉบับตามเดิม โดยไม่แปรรูปหรือสรุปย่อ ที่บันทึกเซสชันข้างต้นใส่บทพูด 5 บรรทัดล่าสุดของนักวิชาการลงไปตามเดิมโดยไม่แตะต้อง โมเดลจึงสามารถหยิบคำว่า "บันทึก" มาได้อย่างแม่นยำในขั้นคำขอใหม่ และนำมาใช้ซ้ำเป็นสื่อกลางของการโทษตัวเอง หากใส่ 5 บรรทัดนั้นไปในรูปสรุปย่อว่า "นักวิชาการเป็นคนรอบคอบและเย็นชา" การเลือกอันละเอียดอ่อนของนักเขียนทั้งหมดจะหายไป แล้วโมเดลก็จะกลับไปสู่ค่าเฉลี่ยอีกครั้ง การสรุปย่อไม่ได้แค่ลดข้อมูล แต่ลบการตัดสินใจที่นักเขียนได้ตัดสินไปแล้ว
ผลลัพธ์ของ AI เป็นร่างเสมอ การตรวจสอบจะต้องผ่านด่านที่กำหนดไว้
flowchart TD
A[ผลลัพธ์ AI N รายการ] --> B[ขั้น 1: voice_lint อัตโนมัติ
5.2]
B --> C[ขั้น 2: นักเขียน 1 คนเลือก·แก้ไข
ประมาณ 15 นาที]
C --> D{มีแบบที่รับไหม?}
D -->|มี| E[ขั้น 3: ตกลงกับลีดเนื้อเรื่อง
เมื่อจำเป็น]
D -->|รับ 0 แบบ = ทิ้งทั้งหมด| F[ขอใหม่หรือเขียนเอง]
E --> G[สะท้อนในเนื้อหา L2
+ ออก L3 dialogue_id]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class B code;
class A ai;
class C,D,E human;
class G pass;
class F fail;
กรณีที่นักเขียนเลือก 0 จาก N รายการ (ทิ้งทั้งหมด) ก็เป็นเรื่องปกติ ดังที่แบบที่ 3 ถูกปฏิเสธในบันทึกเซสชันข้างต้น การปฏิเสธไม่ใช่ความล้มเหลว แต่เป็นหลักฐานว่าด่านทำงาน ดังนั้นจึงวัดอัตราการทิ้งแยกตามนักเขียนและตามตัวละคร เพื่อใช้เป็นตัวชี้วัดคุณภาพของการป้อนบริบท
ถ้าอัตราการทิ้ง 0\~20% แสดงว่าบริบทเพียงพอ เป็นการเดินงานที่เสถียร ปล่อยไว้ตามเดิม 20\~50% เป็นช่วงการเดินงานทั่วไป แค่มอนิเตอร์ ถ้าขึ้นไป 50\~80% ให้กลับไปตรวจว่าการเลือกกฎ L1 ตกหล่นไปหรือไม่ ถ้าเกิน 80% ไม่ใช่ปัญหาของกฎรายข้อ แต่เป็นที่ระบบพรอมต์·เพอร์โซนาเองที่ผิดไป จึงต้องเขียน Layer A ใหม่ อัตราการทิ้งจะรวบรวมสัปดาห์ละครั้งแยกตามนักเขียนแล้วแชร์กันในการทบทวน (รายสัปดาห์)
อย่างไรก็ตาม อัตราการทิ้งไม่ใช่ตัวชี้วัดสัมบูรณ์ ตัวละครที่เปลี่ยนเร็ว (เช่น จุดเปลี่ยนที่เผยอารมณ์ครั้งแรกอย่าง K_007 ในบันทึกเซสชันข้างต้น) แม้อัตราการทิ้งสูงก็เป็นเรื่องปกติ ตัวเลขคือจุดเริ่มต้นของการสนทนา ไม่ใช่คำพิพากษา
L0\~L1 คือ IP หลักของเกม ถ้ารู้สึกเป็นภาระที่จะส่งให้ LLM API ภายนอกตามเดิม ทางเลือกก็จะแตกออกไป วิธีใช้ API ภายนอกตามเดิมด้วยสัญญาห้ามนำไปเทรนเร็วที่สุด แต่ต้องผ่านการตรวจสอบของฝ่ายกฎหมาย วิธีแทนชื่อบริษัท·ชื่อเฉพาะด้วย placeholder ก่อนส่ง มีค่าใช้จ่ายในการประมวลผลเพิ่ม และความเป็นธรรมชาติเสียหาย การโฮสต์เอง (โอเพนโมเดล) ข้อมูลปลอดภัยแต่ภาระด้านคุณภาพ·การเดินงานสูง ส่วนไฮบริดที่เก็บ L0 ไว้ภายในแล้วส่งแค่ร่างออกไปข้างนอกนั้นเดินงานซับซ้อน
โปรเจกต์ A ของผู้เขียนใช้วิธีแรก (API ภายนอก + สัญญาห้ามนำไปเทรน) เคยลองวิธีที่สองแล้วเลิก เพราะการแทนด้วย placeholder ทำให้เนื้อหากลายเป็นมาตรฐานเดียวกันในรูป "นักวิชาการ ○○ แห่งราชอาณาจักร ○○ พูดเรื่อง ○○" จนทำลายคุณภาพผลลัพธ์ การที่การทำให้ไม่ระบุตัวตน (anonymization) ฆ่าคุณภาพ เป็นการแลกได้แลกเสีย (trade-off) ที่ปรากฏซ้ำตลอดทั้งเล่ม (ดูบทการทำให้ไม่ระบุตัวตนใน ส่วนที่ 1) ในงานเนื้อเรื่องความเสียหายนั้นใหญ่เป็นพิเศษ เพราะชื่อเฉพาะคือโทนนั่นเอง
ถ้าโยนแค่คำสั่งงานโดยไม่มีระบบพรอมต์ ก็จะได้ค่าเฉลี่ย ปูเพอร์โซนาและข้อห้ามก่อน ถ้าป้อน L0 ทั้งหมดทุกครั้งแต่ไม่ใช้แคชชิง ค่าใช้จ่ายจะรั่ว รวมบล็อกแคชไว้ข้างหน้า ถ้าใส่รูลบุ๊ก L1 ทั้งก้อน โมเดลจะพลาดประเด็นหลัก ดึงเฉพาะหัวข้อ _summary ถ้าใส่เนื้อหาใกล้เคียงแบบสรุปย่อ การเลือกของนักเขียนจะถูกลบ อ้างต้นฉบับตามเดิม ถ้าไม่กำหนดรูปแบบผลลัพธ์ คำตอบจำนวนมากจะขึ้นต้นด้วย "นี่คือ 3 ตัวเลือก:" แล้วก็ตามมาด้วยเหตุที่นักเขียนเข้าใจผิดว่าคำเกริ่นนั้นเป็นเนื้อหา ระบุจำนวน·ความยาว·ป้ายกำกับให้ชัด ถ้าใช้ผลลัพธ์ AI เป็นขั้นสุดท้าย ด่านตรวจสอบจะพังลง ให้ผ่านด่านนักเขียนเสมอ ถ้าไม่วัดอัตราการทิ้ง สุขภาพของเครื่องมือจะขึ้นกับความรู้สึกของคน รวบรวมเป็นรายสัปดาห์แล้วแชร์ในการทบทวน
ที่ผ่านมาเป็นการประยุกต์ใช้แบบอนุรักษ์นิยม นักเขียนป้อนบริบทอย่างพิถีพิถัน และ AI สร้างแค่ร่างทีละบรรทัด ๆ หน่วยงานคืองานเล็ก ๆ อย่าง "บทพูด 3 แบบถัดไปของตัวละครนี้" "เรื่องย่อของเควสต์นี้" เสถียร แต่มีขีดจำกัดด้านขนาดการผลิตจำนวนมากและการตอบสนองแบบไดนามิก
ตรงนี้ขอชี้สิ่งหนึ่งก่อน การสร้างแบบโพรซีเดอรัล (procedural generation)·การจำลองโลก·เควสต์ไดนามิก คือวิสัยทัศน์ที่นักออกแบบเกมวาดไว้บนกระดาษมาตั้งแต่ 20\~30 ปีก่อน PCG ที่อิงรูลบุ๊กแบบกำหนด (deterministic) จัดการกับพื้นที่ตัวเลขอย่างห้องดันเจี้ยน·ออปชันอาวุธ·การกระจายการสปอว์นได้ แต่เอื้อมไม่ถึงเนื้อหาภาษาธรรมชาติ·เพอร์โซนาตัวละคร·ทางแยกเรื่องราว·บทสนทนา NPC ส่วนสำคัญของงานออกแบบจึงยังคงค้างอยู่บนกระดาษ ความก้าวหน้าของ LLM และโมเดลภาพในช่วงปี 2024\~2026 ได้ดึงพื้นที่นั้นมาสู่ตำแหน่งที่ทำให้เป็นจริงได้ ความหมายหลักของความก้าวหน้าของ AI ไม่ใช่คะแนนของโมเดล แต่อยู่ที่ความเป็นไปได้ในการทำให้งานออกแบบที่อยู่บนกระดาษมานานกลายเป็นจริง อย่างไรก็ตาม การที่ความเป็นไปได้เปิดขึ้น กับการที่มันลงตัวเป็นระบบที่เดินงานได้จริง เป็นคนละเรื่องกัน
การแยก Layer 0\~4 ใน 5.1 ไม่ใช่แค่การจัดระเบียบ แต่เป็นเงื่อนไขตั้งต้นของการสร้างแบบโพรซีเดอรัล ห้าชั้นแต่ละชั้นสอดคล้องกับขั้นตอนใดของไปป์ไลน์การสร้าง (L0 anchor → L1 รูลบุ๊ก → L2 เนื้อหา → L3 ตัวเลข → L4 ด่าน) ได้กล่าวไว้ใน §6.6 และ 5.1.11 ประเด็นมีเพียงหนึ่ง — บนเอกสารก้อนเดียว ตัวสร้างจะกำหนดไม่ได้ว่าจะเริ่มอ่านจากไหนและเขียนลงที่ไหน ไม่รู้ว่า L0 อยู่ตรงไหนทำให้บริบทพร่ามัว และ L1·L2 ชนกันในไฟล์เดียวจนสายการผลิตจำนวนมากพังลง การแยก Layer ต้องมาก่อน แล้วการสร้างแบบโพรซีเดอรัลจึงทำงานได้บนนั้น ตรงนี้นำเงื่อนไขนั้นมาประยุกต์ใช้กับขั้นการผลิตจำนวนมากของการมี AI ช่วยงานเนื้อเรื่อง
สามองค์ประกอบผูกเข้าด้วยกัน หนึ่ง NPC Persona ถูกสร้างแบบโพรซีเดอรัล NPC หลักเป็นฝีมือนักเขียน แต่ NPC ข้างเคียงผลิตจำนวนมากด้วยไปป์ไลน์ generator·Squad ใน 6.2\~6.3 แต่ละ Persona จะติดแท็ก (อาชีพ·ฝ่าย·แนวโน้ม·บทบาท) มาพร้อมกับ voice_profile สอง world BT อยู่เหนือ Squad ขึ้นไป ถ้า Squad ผูกพฤติกรรมของกลุ่ม NPC ในจุดล่าหนึ่ง world BT ก็อยู่เหนือนั้นโดยรับค่าสะสมของพฤติกรรมผู้เล่นมาอัปเดตสถานะของโลกทั้งใบ ผู้เล่นช่วยฝ่ายไหน ไปพื้นที่ไหนบ่อย ตัดสินใจอย่างไร จะเขย่าสถานะโหนดของ world BT และโหนดที่ถูกเขย่าก็สะท้อนไปยังตัวเลข (ชื่อเสียง·ความสนใจ·ลำดับความสำคัญ) ของ NPC ในเขตอิทธิพล สาม เควสต์เป็นโมเดลคลาวด์ เควสต์ทั้งหมดยกเว้นเควสต์หลักถูกสร้างแบบโพรซีเดอรัล แต่ละเควสต์ลอยอยู่พร้อมแท็ก (ใคร·ที่ไหน·ทำไม·เมื่อไร) และเงื่อนไขการปรากฏ ไม่มีเควสต์ใดถูกตรึงไว้กับ NPC ตัวใดตัวหนึ่ง
การปรากฏผ่าน 5 ขั้นตอน
flowchart TD
P[1. สะสมพฤติกรรมผู้เล่น
สนับสนุนฝ่าย·เยือนพื้นที่·ตัดสินใจ] --> W[2. สถานะโหนด world BT เปลี่ยน
อัปเดตสถานะโลกเหนือ Squad]
W --> N[3. ตัวเลข NPC เปลี่ยน
ชื่อเสียง·ความสนใจ·ลำดับความสำคัญ]
N --> M[4. แท็ก NPC + ตัวเลข == เงื่อนไขปรากฏ
สร้างคีย์จับคู่]
M --> Q[5. เควสต์ที่จับคู่ปรากฏ
จากคลาวด์เควสต์]
Q -.->|ปรากฏหลังผ่านด่านนักเขียน| PL[ปรากฏต่อผู้เล่น]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class M code;
class P,W,N,Q data;
class PL pass;
ถ้าเปรียบเทียบให้เห็นภาพ เควสต์ไม่ได้วางเรียงอยู่บนชั้นหนังสือในห้องสมุด แต่เป็นเมฆที่ลอยอยู่กลางอากาศ เมื่อ NPC ไปถึงสถานะใดสถานะหนึ่ง เมฆที่เข้ากับตัวเองก็จะลอยลงมาให้คว้าได้ ในโครงสร้างนี้ AI ไม่ใช่ผู้ช่วยที่เขียนทีละบรรทัด แต่จัดการสามสิ่งพร้อมกัน เนื้อหาภาษาธรรมชาติของ Persona·เควสต์ที่สร้างแบบโพรซีเดอรัล (คำอธิบาย·บทพูด) การแปรผันของคำศัพท์เมื่อสถานะ world BT สะท้อนลงใน NPC (คง voice_profile เดิมไว้แต่หัวข้อสนทนาแปรไปตามสถานะโลก) และตัวจำแนกความสงสัยในขั้นตรวจสอบ (เควสต์นี้ปรากฏกับ NPC ตัวนี้ได้หรือไม่)
ในการประยุกต์ใช้แบบก้าวหน้า เส้นแบ่งย้อนกลับได้/ย้อนกลับไม่ได้ (5.4.5) ก็ยังคงมีชีวิตอยู่ตามเดิม ถ้าบทพูดของเควสต์ที่ปรากฏจากคลาวด์ไหลเข้าสู่ไปป์ไลน์เสียง (voice) โดยยังไม่ผ่านการตรวจสอบ จะย้อนคืนด้วยการ rollback โค้ดไม่ได้ ดังนั้นการวางด่านตรวจสอบทั้งหมด (voice_lint, ตัวจำแนกความสงสัย, ด่านนักเขียน) ไว้หน้าเส้นแบ่งย้อนกลับไม่ได้ จึงเป็นกลไกความปลอดภัยของการประยุกต์ใช้แบบก้าวหน้า ยิ่งเพิ่มการผลิตจำนวนมากแบบอัตโนมัติเข้ามา ด่านเหล่านี้ยิ่งต้องแข็งแกร่งกว่าการประยุกต์ใช้แบบอนุรักษ์นิยม
หนังสือเล่มนี้ไม่ได้กล่าวถึงรูปแบบสมบูรณ์ของการประยุกต์ใช้แบบก้าวหน้า เพราะเป็นเนื้อหาขนาดหนังสืออีกเล่มหนึ่ง และเงื่อนไขโครงสร้างพื้นฐานต่างกันไปตามแต่ละบริษัท·โปรเจกต์ จำแค่สองอย่างก็พอ ก่อนอื่น ทีมที่ทำการประยุกต์ใช้แบบอนุรักษ์นิยมไม่ได้ ก็ทำการประยุกต์ใช้แบบก้าวหน้าไม่ได้เช่นกัน ถ้าด่านตรวจสอบไม่เดินในการประยุกต์ใช้แบบอนุรักษ์นิยม ในการประยุกต์ใช้แบบก้าวหน้าคลาวด์ก็จะพุ่งกระจาย การเดินงานจะอยู่รอดได้ ต้องมีโครงสร้างพื้นฐานการสร้างแบบโพรซีเดอรัล เครื่องมือนิยาม·ทดสอบโหนด world BT การจำแนกความสงสัยอัตโนมัติ + ด่านนักเขียนของเควสต์ที่ปรากฏ การติดตามอัตราการปรากฏ·อัตราการทิ้ง·ความพึงพอใจของผู้เล่นพร้อมกัน และการเก็บคืน·เปลี่ยนเควสต์ที่ปรากฏผิดโดยอัตโนมัติ ครบพร้อมกัน ถ้าขาดไปหนึ่งใน 5 อย่าง จะถูกล้มเลิกภายในหนึ่งไตรมาส เพราะอุบัติเหตุด้านความสอดคล้องจะพุ่งจนการตรวจสอบโดยมนุษย์ตามไม่ทัน
ต่อมา การประยุกต์ใช้แบบก้าวหน้าไม่ใช่เครื่องมือเพิ่มการผลิตจำนวนมาก แต่เป็นเครื่องมือเพิ่มการตอบสนองแบบไดนามิก ถ้ามุ่งแต่การผลิตจำนวนมาก ค่าเฉลี่ยของ RPG ทั่วไปจะเต็มอยู่ในคลาวด์ แล้วผู้เล่นก็จะเจอโลกที่ "ดูหลากหลายขึ้นแต่ว่างเปล่ายิ่งกว่า" แต่ก็ไม่ได้แปลว่าเส้นทางนี้มีแต่ความเสี่ยง พื้นที่นี้คือแนวหน้าของงาน RnD ในการออกแบบเกม ตรงจุดที่การสร้างแบบโพรซีเดอรัล·การจำลอง·LLM มาบรรจบกัน เป็นจุดที่มีความเป็นไปได้สูงที่สุดที่รูปแบบเกมใหม่จริง ๆ จะถือกำเนิด แม้จะมีโอกาสน้อยที่จะนำมาใช้ในบริษัทในตอนนี้ แต่ก็คุ้มค่าที่จะมองไว้เป็นหนึ่งในทิศทางของ 5\~10 ปีข้างหน้า
นี่คือขั้นตอนในการทำซ้ำการประยุกต์ใช้แบบอนุรักษ์นิยมของบทนี้ตามเดิม
setup. ให้รวมไฟล์ L0 สามไฟล์ (world_premise.md·narrative_pillar.md·tone_manifesto.md) เข้าเป็นข้อความเดียวแล้วเก็บไว้ในตัวแปร L0_FULL เขียนระบบพรอมต์ตามห้าบรรทัดในบันทึกเซสชันข้างต้นตามเดิม แต่อย่าตัดบรรทัดสุดท้าย ("เขียนร่าง การตัดสินใจเป็นของนักเขียน") ออก รวบรวมบทพูดล่าสุด 10 บรรทัดของตัวละครที่จะร่างบทพูดเป็นข้อความต้นฉบับตามเดิม
prompt. ประกอบข้อความตามลำดับ [ระบบ] → [โทน L0] → [อ้างอิง: บทพูดก่อนหน้าต้นฉบับ] → [สถานการณ์] → [รูปแบบผลลัพธ์] ในรูปแบบผลลัพธ์ ให้ระบุ "จำนวน N แบบเป๊ะ ๆ / จำนวนตัวอักษรสูงสุดต่อแบบ / ป้ายกำกับ / ห้ามใส่อย่างอื่นใดทั้งสิ้น" ให้แน่ชัด ติด cache_control เฉพาะ L0 และ L1 แล้วไล่บล็อกที่เปลี่ยนไปไว้หลังข้อความ
verify. ตรวจสอบ N แบบที่ได้รับมาทีละบรรทัด ดูว่าโทนต่อเนื่องกับบทพูดก่อนหน้าหรือไม่ ป้ายกำกับตรงกับปฏิกิริยาจริงหรือไม่ มีรูปแบบที่ตัวละครเราไม่ใช้ (หลีกหนี·พูดติดขัด ฯลฯ) แทรกเข้ามาหรือไม่ แบบที่จะปฏิเสธให้ระบุเหตุผลแล้วขอใหม่ การรับ 0 แบบก็เป็นเรื่องปกติ และให้บันทึกเป็นอัตราการทิ้ง เฉพาะแบบที่ผ่านให้ผ่าน voice_lint แล้วสะท้อนใน L2 และออก dialogue_id ที่ L3
ฉบับย่อสำหรับคนเดียว. แม้ไม่มี API·แคชชิง·voice_lint หัวใจก็ยังเหมือนเดิม มีช่องแชต ChatGPT/Claude หนึ่งช่องก็พอ ทุกครั้งให้คัดลอกวางบทพูดล่าสุด 5\~10 บรรทัดของตัวละครไว้ข้างหน้าสุด ต่อท้ายรูปแบบผลลัพธ์ 3 บรรทัดเสมอ และในคำตอบที่ได้รับ แบบที่จะปฏิเสธให้เขียนเหตุผลแล้วขอใหม่ แทนที่จะใช้แคชชิง ให้คงเธรดสนทนาเดียวกันไว้ บริบทก่อนหน้าก็จะคงอยู่ตามเดิม อัตราการทิ้งนั้น นับด้วยมือเขียนลงในกระดาษหนึ่งแผ่นว่า "สัปดาห์นี้รับ 12 / ทั้งหมด 30" ก็เพียงพอ ต่อให้เครื่องมือเล็ก โครงของการป้อน 4 ชั้นและด่านตรวจสอบก็ทำงานกับนักเขียนคนเดียวได้เหมือนกัน
voice_profile ด้วย AI และตรวจสอบเสียงห้องอัดเสียง ผู้กำกับสวมหูฟังและยกมือขึ้นเพื่อให้คิว นักพากย์อ่านบทของ NPC ที่เป็นนักวิชาการ "ว้าว นี่มันสุดยอดไปเลยครับ" มือของผู้กำกับหยุดนิ่ง ตัวละครคนนี้ตลอด 5 บทไม่เคยใช้คำอุทานว่า "ว้าว" แม้แต่ครั้งเดียว เขาเป็นนักวิชาการที่ไม่เอ่ยคำหยาบหรือคำอุทานสมัยใหม่ออกจากปาก และพูดทิ้งท้ายประโยคแบบค่อย ๆ จางหายไปจนจบ แต่ในบทกลับมีบรรทัดนั้นปรากฏอยู่ตรง ๆ
ตรงนี้เกิดต้นทุนขึ้นสองอย่างพร้อมกัน อย่างแรก หากต้องแก้บรรทัดนั้น นักพากย์ก็ต้องอ่านใหม่ ค่าตัวและเวลาในสตูดิโอนับเป็นนาฬิกาที่เดินอยู่แล้ว อย่างที่สอง ซึ่งน่ากลัวกว่าคือกรณีที่ผู้กำกับจับบรรทัดนั้นไม่ได้แล้วปล่อยผ่านไป เมื่อบันทึกเสียงเสร็จและไฟล์เสียงเข้าไปอยู่ในบิลด์แล้ว นักวิชาการคนนั้นจะพูดแบบนั้นในเกมไปตลอดกาล ไฟล์เสียงที่อัดไว้แล้วย้อนกลับด้วยการแก้บรรทัดเดียวอย่างข้อความไม่ได้ ต้องจองนักพากย์คนเดิม สภาพเสียงแบบเดิม และห้องอัดเดิมขึ้นมาใหม่
บทนี้ว่าด้วยเส้นประที่อยู่หน้าห้องอัดนั้น เหนือเส้นประคือข้อความจึงแก้ได้ไม่จำกัด ใต้เส้นประคือไฟล์เสียงจึงแก้ไม่ได้ การตรวจสอบบทพูดทั้งหมดต้องจบลงเหนือเส้นประ และเพื่อให้เป็นเช่นนั้น แต่ละตัวละครต้องมีข้อกำหนดว่า "คนนี้พูดแบบนี้" บันทึกไว้เป็นไฟล์ ไม่ใช่อยู่ในหัว และทุกครั้งที่มีบทพูดใหม่ขึ้นมาก็ต้องถูกเทียบกับไฟล์นั้นโดยอัตโนมัติ ไฟล์นั้นเราเรียกว่า voice_profile และเครื่องมือเทียบเรียกว่า voice_lint
ในบรรดาอุบัติเหตุด้านความสอดคล้องของเนื้อเรื่อง สิ่งที่เกิดบ่อยที่สุดคือเสียงตัวละครที่แกว่ง หลังเปิดตัวไปแล้วถึงจะมีรายงานเข้ามาว่า "ทำไม NPC ตัวนี้จู่ ๆ ก็เปลี่ยนวิธีพูดไป" สาเหตุแทบจะเหมือนกันทุกครั้ง
เมื่อเปลี่ยนนักเขียน NPC ตัวเดิมก็กลายเป็นคนอื่นไป ต่อให้ไม่เปลี่ยนนักเขียน พอผ่านไป 6 เดือน นักเขียนคนเดิมก็ลืมโทนของตัวเอง เวลาเขียนบทพูดใหม่โดยไม่เปิดดูบทพูดเก่าของตัวละครนั้น บริบทก็ขาดตอน หากกฎเสียงตัวละครอยู่แต่ในหัวของนักเขียนและไม่มีเป็นเอกสาร ก็ส่งต่อให้คนถัดไปไม่ได้ และสาเหตุที่ห้าที่เพิ่มขึ้นเร็วที่สุดในช่วงสองปีมานี้ — เมื่อโยนคำสั่งใส่ LLM ว่า "ช่วยสร้างบทพูดของ NPC ตัวนี้ให้หน่อย" โดยไม่ให้ข้อมูลตัวละคร AI จะคืนบทพูดที่เฉลี่ย ๆ กลาง ๆ จึงไม่ใช่เสียงของใครเลย
สาเหตุที่ห้านี้คือผลข้างเคียงของการนำ AI เข้ามาใช้ การฉีดบริบท (context injection) ในบทก่อนหน้า (5.3) คือใบสั่งยา แต่ถ้าตัวบริบทที่จะฉีดเองนั้นบกพร่อง ก็ไม่มีอะไรจะฉีด บริบทนั้นก็คือ voice_profile นี่เอง บทนี้จะดูหนึ่งรอบของการสร้างไฟล์นั้น ตรวจสอบโดยอัตโนมัติ และปิดจบที่หน้าห้องอัด
ในโปรเจกต์ A ทุก NPC จะมี voice_profile ประกอบด้วยห้าหัวข้อ ได้แก่ ขอบเขตคำศัพท์ (กลุ่มคำที่ใช้บ่อย / กลุ่มคำที่ไม่ใช้เด็ดขาด), ความยาวประโยค (จำนวนตัวอักษรเฉลี่ย·สูงสุด), ระบบคำสุภาพ (สรรพนามบุรุษที่ 1·2, สัดส่วนการใช้ jondaetmal (ระบบคำยกย่องในภาษาเกาหลี), คำเรียกขาน), การแสดงอารมณ์ (ใช้วิธีตรง·อ้อม·หรือเก็บกด), และสำนวนต้องห้าม (คำหรือวลีที่ไม่ใช้เด็ดขาด) ทั้งห้าหัวข้อต้องมีตัวอย่างที่เป็นรูปธรรมกำกับ หากมีแต่คำพรรณนาเชิงนามธรรมอย่าง "นักวิชาการที่เคร่งขรึม" แต่ละคนก็จะอ่านต่างกันออกไป นักเขียนคนถัดไปจะจินตนาการนักวิชาการคนนั้นในแบบของตัวเอง
รูปแบบโปรไฟล์จริงของ NPC นักวิชาการ K_007 เป็นดังนี้ ไฟล์นี้จะกลายเป็นอินพุตของ voice_lint ทันที
---
title: K_007 นักวิชาการ voice_profile
layer: L1
character_id: K_007
atoms:
- voice_profile_k_007
related:
derives_from: [character_bible/k_007.md]
affects: [dialogue_id_table (บทพูดทั้งหมดของ K_007)]
---
## 1. ขอบเขตคำศัพท์
- ใช้บ่อย: "บันทึก", "หลักฐาน", "สถานการณ์", "การประมาณ", "ข้อมูล", "กรณีตัวอย่าง"
- ไม่ใช้เด็ดขาด: "ความรู้สึก", "ลางสังหรณ์", "โชคชะตา", "พระประสงค์ของพระเจ้า", "เสียงในใจ"
## 2. ความยาวประโยค
- เฉลี่ย: 18 ตัวอักษร
- สูงสุด: 35 ตัวอักษร (เกินกว่านั้นแยกเป็นสองประโยค)
- ตัดจบสั้น ๆ บ่อย: "...ไม่ใช่ครับ" "เริ่มจากบันทึกก่อน"
## 3. ระบบคำสุภาพ
- บุรุษที่ 1: "ผม/ดิฉัน"
- บุรุษที่ 2: เรียกตามตำแหน่งก่อน (ท่านหัวหน้า, ท่านผู้บัญชาการ) เรียกชื่อต่อเมื่อสนิทกันแล้วเท่านั้น
- ใช้คำสุภาพ 100% (ยกเว้นฉากย้อนอดีต)
- แทบไม่มีคำอุทาน เมื่อมีจะเป็น "...อ่ะ"
## 4. การแสดงอารมณ์
- แทบไม่แสดงออกตรง ๆ (ความโกรธ·ความยินดี)
- แสดงผ่านความเงียบ·การพูดทิ้งท้ายให้จางหาย ("...ถ้าเป็นแบบนั้นล่ะก็")
- ความเศร้า: เลี่ยงด้วยการเปลี่ยนเรื่อง ("...คุยเรื่องอื่นกันเถอะ")
## 5. สำนวนต้องห้าม
- คำหยาบทุกชนิด
- คำอุทานสมัยใหม่ "ว้าว", "เฮ้ย", "สุดยอด"
- คำศัพท์รากจีน 3 พยางค์ขึ้นไป สองคำขึ้นไปในประโยคเดียว
- คำศัพท์เชิงไสยศาสตร์ เช่น "โชคชะตา", "คำพยากรณ์"
หัวใจของรูปแบบนี้คือการที่ทุกตำแหน่งของสิ่งนามธรรมจะมีบรรทัดตัวอย่างกำกับอยู่ ไม่ใช่ "เก็บกดอารมณ์" แต่มีบทพูดจริงอย่าง "...ถ้าเป็นแบบนั้นล่ะก็" กำกับไว้ เพื่อให้นักเขียนคนถัดไป ผู้แปล และ voice_lint มองเห็นเกณฑ์เดียวกัน
เมื่อมองห้าหัวข้อนี้ด้วยสายตาของการแปลและการแปลภาษาท้องถิ่น (localization) จะแยกได้เป็นสองกลุ่ม คุณสมบัติที่ ขึ้นกับภาษา (รูปลักษณ์ผิวเผินของขอบเขตคำศัพท์·ความยาวประโยค·ระบบคำสุภาพ) ต้องกำหนดใหม่ในแต่ละภาษาที่แปล ส่วนคุณสมบัติที่ ไม่ขึ้นกับภาษา (ท่าทีอย่างเช่น จะเก็บกดอารมณ์หรือแสดงออกตรง ๆ จะไม่พูดอะไรจนถึงที่สุด) ต้องคงไว้เช่นเดิมไม่ว่าจะแปลเป็นภาษาใด หากส่งต่อการแยกแยะนี้ไปพร้อมกับงานแปลภาษาท้องถิ่น ก็จะป้องกันไม่ให้ผู้แปลเปลี่ยนแค่คำศัพท์ผิวเผินแล้วเผลอทำให้ท่าทีของตัวละครแกว่งไปด้วย
หากเขียน NPC 50 ตัว × 5 หัวข้อ = 250 รายการ จากกระดาษเปล่าตั้งแต่ต้น มันจะไหลไปสู่ความเป็นนามธรรม หากเขียนว่า "ตัวละครนี้เป็นนักวิชาการเลือดเย็น" โดยไม่มีบทในเนื้อเรื่องแม้แต่บรรทัดเดียว ก็ไม่มีใครรู้ว่าความเลือดเย็นนั้นคืออะไร ดังนั้นจึงสลับลำดับ เขียน NPC หลักเพียง 5\~7 ตัวให้เต็มตั้งแต่ต้น ส่วนที่เหลือจะสกัด profile ย้อนกลับจากบทพูดหลังจากที่บทพูดในเนื้อเรื่องสะสมได้ 20\~30 บรรทัดแล้ว
ด้านล่างคือบันทึกเซสชันจริงของการดึงร่าง profile ออกมาหลังจากบทพูดของ K_007 สะสมได้ 25 บรรทัด เราจะไม่สรุปย่อ และทิ้งจุดที่ AI ผิดไว้ตามเดิม
[พรอมต์เต็มที่นักเขียนโยนเข้าไป]
ต่อไปนี้คือบทพูดของตัวละคร K_007 จำนวน 25 บรรทัด
1. "เริ่มจากดูบันทึกกันก่อน"
2. "...สถานการณ์มันไม่สอดคล้องกัน"
3. "เป็นเพียงการประมาณเท่านั้น ไม่มีหลักฐาน"
4. "กรณีตัวอย่างนั้นผมก็ทราบอยู่"
5. "...ไม่ใช่ครับ ข้อมูลมันต่างกัน"
(... อ้างอิงข้อ 6~23 เต็มโดยไม่ตัดทอน ...)
24. "...ถ้าเป็นแบบนั้นล่ะก็ ผมขอถอนตัว"
25. "คุยเรื่องอื่นกันเถอะ"
จากนี้ ช่วยดึงออกมาห้าอย่าง ได้แก่ คำศัพท์ที่ใช้บ่อย (เฉพาะที่ปรากฏจริง),
ความยาวประโยคเฉลี่ย, รูปแบบคำสุภาพ·คำเรียกขาน, วิธีแสดงอารมณ์,
และขอบเขตของสำนวนที่ไม่เคยใช้เลย
แต่ละหัวข้อให้อ้างอิงบทพูดที่เป็นหลักฐาน 2~3 บรรทัดด้วยหมายเลข และอย่าใช้
ความประทับใจเชิงอัตวิสัยอย่างคำว่า "เคร่งขรึม" หรือ "เลือดเย็น"
[ผลลัพธ์ดิบของ Claude — ไม่ได้ขัดเกลา]
1. คำศัพท์ที่ใช้บ่อย
- "บันทึก" (ข้อ 1), "สถานการณ์" (ข้อ 2), "การประมาณ/หลักฐาน" (ข้อ 3),
"กรณีตัวอย่าง" (ข้อ 4), "ข้อมูล" (ข้อ 5)
→ เน้นคำนามกลุ่มข้อเท็จจริง·หลักฐาน
2. ความยาวประโยคเฉลี่ย
- เฉลี่ย 25 บรรทัด ราว 14.6 ตัวอักษร ยาวสุด 21 ตัวอักษร (ข้อ 24)
→ เน้นประโยคสั้นเดี่ยว
3. คำสุภาพ·คำเรียกขาน
- บุรุษที่ 1 "ผม" (ข้อ 3·24) คำลงท้ายเป็นรูปสุภาพ "-ครับ/-ค่ะ"
- คำเรียกขานบุรุษที่ 2 ไม่ปรากฏใน 25 บรรทัดนี้
→ ตัดสินรูปแบบคำเรียกขานจากตัวอย่างชุดนี้เพียงอย่างเดียวไม่ได้ (ต้องให้นักเขียนยืนยัน)
4. การแสดงอารมณ์
- ไม่มีคำแสดงอารมณ์ตรง ๆ จัดการการปฏิเสธด้วย "ขอถอนตัว" (ข้อ 24)
และความเศร้า/การหลีกเลี่ยงด้วยการเปลี่ยนเรื่อง "คุยเรื่องอื่นกันเถอะ" (ข้อ 25)
→ แบบเก็บกด·อ้อม
5. ขอบเขตที่ไม่เคยใช้เลย
- คำอุทาน, คำหยาบ, คำศัพท์เชิงไสยศาสตร์ ล้วนไม่ปรากฏ
※ แต่ "ไม่ปรากฏ" ไม่ได้รับประกันว่าเป็น "ข้อห้าม"
เนื่องจากตัวอย่างมีเพียง 25 บรรทัดซึ่งน้อย จึงอาจเป็นการไม่ปรากฏโดยบังเอิญ
ตรงนี้มีสองจุดที่ต้องชี้ให้เห็น อย่างแรก AI หยุดที่หัวข้อ 3 มันขีดเส้นจำกัดให้ตัวเองว่า "ตัดสินจากตัวอย่างชุดนี้เพียงอย่างเดียวไม่ได้" เนื่องจากคำเรียกขานไม่ปรากฏใน 25 บรรทัด มันจึงไม่คาดเดา — นี่คือผลลัพธ์ที่ซื่อตรง หาก AI เติมว่า "บุรุษที่ 2 ประมาณว่า 'ท่านหัวหน้า'" นั่นคืออาการหลอน (hallucination) และเมื่อเข้าไปอยู่ใน profile ก็คือการปนเปื้อน อย่างที่สอง ในหัวข้อ 5 AI แนบข้อจำกัดความรับผิดให้ตัวเอง "ไม่ปรากฏ ≠ ข้อห้าม" คือคำเตือนสำคัญ การที่คำอุทานไม่ปรากฏใน 25 บรรทัดอาจเป็นเรื่องบังเอิญก็ได้
[การตรวจสอบและปฏิเสธของนักเขียน]
นักเขียนรับหัวข้อ 1·2·4 ส่วนหัวข้อ 3 คำเรียกขานนั้น นักเขียนเปิด character_bible แล้วเติม "เรียกตามตำแหน่งก่อน เรียกชื่อหลังสนิท" ด้วยมือ — คนเติมเต็มตำแหน่งที่ AI เว้นว่างไว้ ส่วนหัวข้อ 5 นักเขียนไม่เลื่อนสถานะ "ไม่ปรากฏ" ขึ้นเป็น "ข้อห้าม" ทันทีตามคำเตือนของ AI แต่นักเขียนเทียบกับการตั้งค่าตัวละครแล้วยืนยันเป็นข้อห้ามเฉพาะ "คำอุทานสมัยใหม่·คำหยาบ·ไสยศาสตร์" เท่านั้น ส่วนคำศัพท์ที่ไม่ปรากฏที่เหลือก็พักไว้ก่อน
[การขอใหม่ของนักเขียน]
"โชคชะตา", "คำพยากรณ์" ผมจะยืนยันเป็นข้อห้าม ช่วยดึงคำศัพท์เชิงไสยศาสตร์
ที่ความหมายซ้อนทับกันมาเพิ่มอีก 10 คำ
แต่เนื่องจาก K_007 ในฐานะนักวิชาการอาจยกมาอ้างในบริบทของการหักล้าง·วิพากษ์ได้
ช่วยแสดงกรณียกเว้นเหล่านั้นกำกับมาด้วยบรรทัดเดียว
การขอใหม่ครั้งสุดท้ายนี้สำคัญ หากขยายข้อห้ามอย่างกลไก บทพูดที่ชอบธรรมอย่าง "นักวิชาการวิพากษ์ไสยศาสตร์ว่า 'โชคชะตาอะไรกัน'" ก็จะถูกบล็อกไปด้วย ดังนั้นจึงนิยามบริบทยกเว้นกำกับไว้กับข้อห้าม AI ขยายตัวเลือกให้กว้าง ส่วนนักเขียนขีดเส้นขอบเขต หลังจากหมุนครบหนึ่งรอบนี้แล้ว voice_profile_k_007 จึงจะถูกยืนยันและตรึงไว้ที่ L1
เมื่อบังคับให้อ้างอิงหลักฐาน ("อ้างด้วยหมายเลข") และห้ามใช้คำคุณศัพท์เชิงอัตวิสัย ("ห้ามใช้ 'เคร่งขรึม'") อาการหลอนของ AI ก็ลดลง และนักเขียนก็มีพื้นผิวให้ตรวจสอบ profile ไม่ใช่สิ่งที่ AI เขียน แต่เป็นสิ่งที่ AI วางร่างไว้แล้วนักเขียนตรึงให้คงที่
เมื่อ voice_profile มีเป็นไฟล์แล้ว ทุกครั้งที่มีบทพูดใหม่ขึ้นมาก็เทียบอัตโนมัติได้ ในบรรดาการตรวจห้าอย่าง สิ่งที่ได้ผลมากในงานจริงคือสองอย่าง ได้แก่ การจับคู่คำศัพท์ต้องห้าม (คำศัพท์ติดอยู่ในรายการต้องห้ามหรือไม่) และการละเมิดขอบเขตคำศัพท์ (เข้าไปอยู่ในกลุ่มคำที่ไม่ใช้เด็ดขาดหรือไม่) ส่วนการหลุดจากความยาวประโยค·การขาดคำสุภาพ·สัดส่วนคำที่ใช้บ่อยนั้นมีผลบวกลวง (false positive) มาก จึงใช้เป็นตัวเสริมเท่านั้น หากจับไปถึงขั้นที่ฉากย้อนอดีตบรรทัดเดียวทำให้ความยาวเฉลี่ยแกว่ง นักเขียนก็จะชินชาต่อคำเตือน
voice_lint รับชุดบทพูดใหม่ของบทแล้วออกรายงานแบบนี้
ผล voice_lint (ch04 บทพูดใหม่ 32 บรรทัด, profile=voice_profile_k_007)
─────────────────────────────────────────────
[ละเมิด] dialogue_id_412 — K_007
เนื้อหา: "ว้าว นี่มันสุดยอดไปเลย!"
เหตุผล: คำศัพท์ต้องห้าม "ว้าว", "สุดยอด" (profile §5)
→ ต้องให้นักเขียนตรวจสอบ
[น่าสงสัย] dialogue_id_421 — K_007
เนื้อหา: "โชคชะตานั้นยอมรับได้ยาก"
เหตุผล: ขอบเขตคำศัพท์ต้องห้าม "โชคชะตา" (profile §5)
แต่อาจเข้าข่ายยกเว้น 'บริบทหักล้าง·วิพากษ์' — ให้นักเขียนตัดสิน
→ ต้องให้นักเขียนตรวจสอบ
[ปกติ] บทพูด 30 รายการ
─────────────────────────────────────────────
สรุป: ละเมิด 1 / น่าสงสัย 1 / ปกติ 30
ละเมิดคือสีแดง น่าสงสัยคือสีเหลือง ทั้งคู่ต้องผ่านการตัดสินของนักเขียนจึงจะผ่านไปได้ ตรงนี้มีหลักการเด็ดขาดข้อหนึ่ง — voice_lint จะไม่ปฏิเสธโดยอัตโนมัติ (เป็นการต่อยอดจากหลักการ 5.2) ลองดู dialogue_id_421 ข้างบน "โชคชะตา" เป็นข้อห้าม แต่หากเป็นบริบทที่นักวิชาการกำลังโต้แย้งไสยศาสตร์ ก็อาจเป็นการอ้างอิงที่ชอบธรรมได้ การตัดสินนั้นเครื่องมือทำไม่ได้ lint แบบปฏิเสธอัตโนมัติจะปิดกั้นจุดละเอียดอ่อนนี้ทั้งหมด และพรากโอกาสที่นักเขียนจะได้ขัดเกลาโทนไปด้วย lint คือไฟฉายที่ส่องจุดน่าสงสัย ไม่ใช่กุญแจที่ล็อกประตู
กระดูกสันหลังของบทนี้คือแผนภาพภาพเดียวนี้ ในช่วงที่บทพูดบรรทัดหนึ่งเดินทางจากมือของนักเขียนไปสู่ปากของนักพากย์ ด่านตรวจสอบจะถูกวางไว้ในแต่ละขั้น และกลางทางของกระแสนั้นมีเส้นประหนาขีดอยู่
flowchart TD
A["นักเขียนเขียนเนื้อเรื่อง (L2)"] --> B["voice_lint อัตโนมัติ
รายงานละเมิด·น่าสงสัย"]
B --> C["นักเขียนตรวจสอบตัวเอง (15 นาที)"]
C --> D["ลีดเนื้อเรื่องตรวจสอบตัวอย่าง
สุ่ม 10% ต่อบท"]
D --> E["ออก L3 dialogue_id
+ จับคู่ translation key"]
E --> F["ตรวจสอบการแปล·แปลภาษาท้องถิ่น"]
F -.->|"━━━ เส้นแบ่งย้อนกลับได้ / ย้อนกลับไม่ได้ ━━━
เหนือเส้นนี้เป็นข้อความ แก้ได้ไม่จำกัด
ใต้เส้นนี้เป็นไฟล์เสียง แก้ไม่ได้"| G
G["คัดเลือกนักพากย์ (VA)"] --> H["บันทึกเสียงพากย์ (ขั้นสุดท้าย·ย้อนกลับไม่ได้)"]
H --> I["นำไฟล์เสียงเข้าบิลด์"]
classDef reversible fill:#e8f4ea,stroke:#3a7d44,stroke-width:1px,color:#1b3a22;
classDef irreversible fill:#f7e3e3,stroke:#b23b3b,stroke-width:2px,color:#5a1414;
class A,B,C,D,E,F reversible;
class G,H,I irreversible;
สีเขียวคือขั้นย้อนกลับได้ สีแดงคือขั้นย้อนกลับไม่ได้ เหนือเส้นประ (สีเขียว) ทั้งหมดเป็นข้อความ หากไม่พอใจบทพูดบรรทัดหนึ่งก็แก้ด้วยคีย์บอร์ดได้ ต้นทุนคือเวลาไม่กี่นาทีของนักเขียน ใต้เส้นประ (สีแดง) เป็นไฟล์เสียง ในวินาทีที่นักพากย์อ่านบรรทัดนั้นในห้องอัดและไฟล์เสียงเข้าไปอยู่ในบิลด์ บทพูดนั้นก็ถูกตรึงเป็นแอสเซต หากจะแก้ก็ต้องจองนักพากย์คนเดิม สภาพเสียงแบบเดิม สตูดิโอเดิมขึ้นมาใหม่ และค่าตัว·สตูดิโอ·เวลาของผู้กำกับก็เสียซ้ำเท่ากับครั้งแรกอีกรอบ หากตารางงานคับแคบ บางทีก็จองเซสชันเพิ่มของนักพากย์คนเดิมไม่ได้เลย
ดังนั้นกฎเพียงข้อเดียวจึงครอบงำเวิร์กโฟลว์ทั้งหมด — ด่านตรวจสอบทุกด่านต้องจบลงเหนือเส้นประ การบันทึกเสียงไม่ใช่ขั้นตรวจสอบ แต่เป็นขั้นที่ตรึงผลซึ่งตรวจสอบเสร็จแล้วให้กลายเป็นแอสเซต หากเกิดข้อสงสัยใต้เส้นประว่า "บทพูดนี้แปลก ๆ นะ" นั่นไม่ใช่จุดที่จะตรวจสอบเพิ่ม แต่เป็นสัญญาณว่าการตรวจสอบในขั้นบนตกหล่นไป ไฟฉายต้องส่องให้หมดเหนือเส้นประ ห้องอัดไม่ใช่ที่ที่มืดได้ แต่เป็นที่ที่มืดไม่ได้
กลางแผนภาพ การตรวจสอบตัวอย่างของลีดถูกกำหนดไว้ที่ "สุ่ม 10% ต่อบท" สัดส่วนนี้คือจุดสมดุลระหว่างเวลาตรวจสอบกับความแม่นยำ (ค่าที่ผู้เขียนใช้บริหารงานจริง, ยังไม่ได้ตรวจสอบ) หากลดต่ำกว่า 5% อุบัติเหตุจะรั่ว หากดันเกิน 20% ลีดคนเดียวก็จะกลายเป็นคอขวด เนื่องจาก lint กรองละเมิด·น่าสงสัยไว้ล่วงหน้า ตัวอย่างจึงสุ่มจากส่วนที่ผ่าน lint — สายตาของคนจะมุ่งไปที่ความผิดพลาดเชิงบริบทที่เครื่องมือจับไม่ได้ (เช่น การอ้างอิง "โชคชะตา" ที่ดูชอบธรรมแต่จริง ๆ คือการพังทลายของตัวละคร)
หากตัวละครพูดเหมือนเดิมจนจบ plot ก็จะหยุดนิ่ง หากนักวิชาการที่ผ่านการสูญเสียเพื่อนร่วมทางมาพูดด้วยโทนเดิมเหมือนก่อนหน้า นั่นกลับยิ่งปลอม หากความเปลี่ยนแปลงนั้นเป็นสิ่งที่ตั้งใจ voice_profile ก็ต้องเลื่อนเวอร์ชันขึ้นไปด้วย
---
character_id: K_007
voice_profile_versions:
- v1: ch01~ch05 (ระยะแรก — เก็บกดอารมณ์, นักวิชาการประโยคสั้น)
- v2: ch06~ch10 (หลังเพื่อนร่วมทางตาย — ความถี่การแสดงอารมณ์เพิ่มขึ้น)
- v3: ch11~ (หลังตื่นรู้ — เริ่มมีการพูดตรง ๆ)
---
แต่ละเวอร์ชันมีไฟล์ profile แยกกัน และ voice_lint จะดูหมายเลขบทของบทพูดที่ตรวจสอบเพื่อเลือกว่าจะใช้เวอร์ชันไหน หากเอากฎ "เก็บกดอารมณ์" ของ v1 ไปจับบทพูดของ ch07 บทพูดที่เปลี่ยนแปลงไปอย่างปกติก็จะขึ้นเป็น "น่าสงสัย" กันหมด ความเปลี่ยนแปลงไม่ใช่บั๊ก แต่เป็นการออกแบบ
สัญญาณของการเลื่อนเวอร์ชันมีสามอย่าง เมื่อนักเขียนตั้งใจทำให้โทนแกว่ง ก็เสนอเวอร์ชันใหม่และตกลงกับลีด เมื่อจำนวน "น่าสงสัย" ของ voice_lint ในตัวละครหนึ่งเพิ่มขึ้นเรื่อย ๆ นั่นคือสัญญาณว่านักเขียนกำลังเลื่อนโทนไปโดยไม่รู้ตัว — ถึงเวลาอัปเดตเวอร์ชันแล้ว เมื่อมีเหตุการณ์เปลี่ยนแปลง (ความตาย·การตื่นรู้·การทรยศ) เพิ่มเข้าไปใน character_bible ก็จะมี alert เตือนให้อัปเดต profile ขึ้นมา อย่างไรก็ตาม หากตัวละครหนึ่งเปลี่ยนทุกบท ความสอดคล้องก็จะพังทลาย ดังนั้นจำนวนเวอร์ชันที่สมจริงคือ 2\~4 เวอร์ชันต่อตัวละคร
[ป้ายบอกทาง — หากมองระหว่างตัวละครผ่านปริภูมิเสียง (ยังเร็วเกินไปในตอนนี้)] หาก
voice_lintรักษาความสอดคล้อง 'ภายในตัวละครหนึ่ง' ด้วยกฎ 'ปริภูมิเสียง' (voice space) ที่ฝัง (embed) ชุดบทพูดจริงของแต่ละตัวละคร (บทพูดทั้งหมดที่ตัวละครหนึ่งพูด) เป็นจุดหนึ่งจุด จะมองว่า 'ระหว่างตัวละคร' แยกห่างกันมากพอหรือไม่ — หากจุดต่าง ๆ ค่อย ๆ เกาะกลุ่มเข้าหากัน นั่นก็จะกลายเป็นค่าวัดโดยตรงของการปรับเสียงให้เป็นมาตรฐาน·การลู่เข้าหากันที่ §5.3.1·§5.4.1 ชี้ไว้ แต่อย่าตรึงเกณฑ์ระยะทางเป็นตัวเลขสัมบูรณ์ ให้อ่านเป็นเพียงป้ายบอกทางว่า 'กำลังเกาะกลุ่ม' และไม่ใช่ด่านตัดสินที่มาแทนvoice_lint(แนวคิดนี้อยู่ตำแหน่งเดียวกับการบีบอัดเวกเตอร์เชิงมิติใน §8.2.7 และสัญชาตญาณเชิงแนวคิดได้คลี่ไว้เป็นแผนที่หนึ่งภาพในภาคผนวก M — ไม่ใช่ใบสั่งยา แต่เป็นป้ายบอกทาง)
เมื่อบทพูดถูกแปลเป็นหลายภาษาและมีเสียงนักพากย์มาสวมทับ หน่วยที่ต้องจัดการก็เพิ่มขึ้นเป็นทวีคูณ บทพูดภาษาเกาหลีบรรทัดหนึ่งแตกออกเป็นภาษาอังกฤษ·ภาษาเอเชียตะวันออกเฉียงใต้ และแต่ละภาษาก็มีโทนสวมทับเข้าไป
จุดที่รั่วบ่อยที่สุดในความสอดคล้องของการแปลคือ สำนวนเดียวกันถูกแปลต่างกันในแต่ละบท (จับด้วยการตรวจความสอดคล้องของหน่วยความจำการแปล หรือ translation memory) ตามมาด้วยการที่ voice_profile ของตัวละครไม่ถูกสะท้อนในการแปล (แนบไกด์การแปลแยกของแต่ละตัวละครต่างหาก) และคำศัพท์ใหม่ที่ยังไม่ลงทะเบียนในอภิธานศัพท์ (lint อภิธานศัพท์) ไกด์การแปลสร้างขึ้นโดยอัตโนมัติจาก voice_profile — นำ "ตัวละครนี้ใช้คำสุภาพ 100%, ไม่มีคำอุทาน, ห้ามใช้คำศัพท์เชิงไสยศาสตร์" ไปแปะหัวคำสั่งการแปลโดยอัตโนมัติ เมื่อผู้แปลย้ายนักวิชาการคนนั้นเป็นภาษาอังกฤษ ก็จะมองเห็นขอบเขตเดียวกัน
การตรวจสอบ VA (Voice Actor, นักพากย์) คือการตรวจสอบในขั้นข้อความที่ย้อนกลับได้ขั้นสุดท้าย ก่อนจะแตะถึงใต้เส้นประ ความสอดคล้องของโทน (ระดับความเข้มของการแสดงความโกรธ·ความเศร้า) ผู้กำกับและฝ่ายเนื้อเรื่องเป็นคนดู ความถูกต้องของการออกเสียง (คำเฉพาะ) ผู้รับผิดชอบอภิธานศัพท์เป็นคนดู ส่วนจังหวะหายใจ·การตัดจบ (คำสั่งอย่าง "ตัดจบสั้น ๆ บ่อย" ใน profile) ผู้กำกับเป็นคนดู ผลการตรวจสอบบันทึกเป็นผ่าน·ตีกลับใน voice_review_log.md (L4) และอ้างอิงตอนคัดเลือกนักพากย์ตัวละครถัดไป
การตีกลับให้จบก่อนการคัดเลือก·บันทึกเสียงให้ได้มากที่สุด ข้อผิดพลาดของบทที่ถูกพบในห้องอัดจะทำให้เซสชันของวันนั้นพังทั้งหมดและกระทบไปถึงตารางเซสชันถัดไป แต่การเลื่อนบันทึกเสียงออกไปแล้วปิดการตรวจสอบก็ไม่ใช่คำตอบ หากการตรวจสอบติดขัดบ่อยตรงหน้าห้องอัด นั่นคือเวิร์กโฟลว์ขั้นบน (นักเขียน·ลีด) ที่ล่าช้า ไม่ใช่ปัญหาของตารางบันทึกเสียง
ในโปรเจกต์ A เราติดตามก่อนและหลังการนำ voice_profile + voice_lint มาใช้เป็นเวลา 6 เดือน ตัวเลขจำนวนสัมบูรณ์เป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ให้เชื่อถือเฉพาะทิศทาง·สัดส่วนก็พอ
| รายการ | ก่อนนำมาใช้ | หลังนำมาใช้ | ทิศทาง |
|---|---|---|---|
| อุบัติเหตุเสียงตัวละครต่อบท (หลังเปิดตัว) | 5\~8 ครั้ง | 1\~2 ครั้ง | ราว 1/4 |
| การลงตัวของเสียง NPC ใหม่ | 3 บท | 1 บท | 1/3 |
| จำนวน NPC ที่นักเขียน 1 คนดูแล | ราว 15 ตัว | ราว 40 ตัว | ราว 2.5 เท่า |
| อุบัติเหตุความสอดคล้องของการแปล (ต่อบท) | 10\~15 ครั้ง | 2\~4 ครั้ง | ราว 1/4 |
| เวลาตรวจสอบเสียง (ต่อบท) | 3 วัน | 1 วัน | 1/3 |
บรรทัดที่มีความหมายมากที่สุดคือจำนวน NPC ที่นักเขียน 1 คนดูแล การที่เพิ่มราว 2.5 เท่าไม่ได้หมายความว่าลดจำนวนนักเขียน แต่หมายความว่านักเขียนคนเดิมสามารถเพิ่มความหลากหลายของ NPC ต่อบทได้ โลกในเกมก็คึกคักขึ้น
เมื่อดูโครงสร้างต้นทุน ต้นทุนการดำเนินงานเล็กกว่าต้นทุนการนำมาใช้มาก การเขียน voice_profile ใช้นักเขียน 2 สัปดาห์สำหรับตัวหลัก 7 ตัว เครื่องมือ voice_lint ใช้พัฒนา 1\~2 สัปดาห์และบำรุงรักษาเดือนละ 1 วัน ฝั่งดำเนินงาน ต่อบทใช้นักเขียนตรวจสอบตัวเอง 15 นาที ลีดตรวจสอบตัวอย่างราว 2 ชั่วโมง (สุ่ม 10%) ส่วนการอัปเดต profile ของบทที่มีการเปลี่ยนแปลงใช้ 1\~2 วันต่อตัวละคร ต้นทุนดำเนินงานต้องเล็กระบบจึงจะอยู่รอด เครื่องมือที่ดำเนินงานหนักจะถูกทิ้งอย่างเงียบ ๆ ภายในหนึ่งไตรมาส
| รูปแบบ | ใบสั่งยา |
|---|---|
| profile มีแต่คำพรรณนานามธรรม ("เคร่งขรึม") | บังคับให้มีตัวอย่างบทพูดจริงทุกหัวข้อใน 5 หัวข้อ |
| ตั้งใจเขียนเต็มทั้ง 50 ตัวแต่แรก | ตัวหลัก 7 ตัวเขียนเต็ม + ที่เหลือสกัดย้อนกลับจากเนื้อเรื่องที่สะสม |
| voice_lint แบบปฏิเสธอัตโนมัติ | ละเมิด·น่าสงสัย + นักเขียนตัดสิน การปฏิเสธทำได้เฉพาะคน |
| ขยายข้อห้ามอย่างกลไก | นิยามบริบทยกเว้นกำกับข้อห้าม ("อ้างเพื่อหักล้างได้") |
| ตัวละครเปลี่ยนแต่ไม่อัปเดต profile | จัดการเวอร์ชัน (v1·v2·v3), ใช้ตามหมายเลขบท |
| ไม่ส่งต่อ profile ให้การแปล | สร้างไกด์การแปลอัตโนมัติจาก profile |
| พยายามแก้บทพูดหลังบันทึกเสียง | การบันทึกเสียงย้อนกลับไม่ได้ ตรวจสอบให้จบเหนือเส้นประ |
| บีบการตรวจสอบให้ตรงตารางบันทึกเสียง | แก้ด้วยการปรับปรุงเวิร์กโฟลว์ขั้นบน |
| เก็บ profile ไว้แต่ในหัว | ต้องทำเป็นไฟล์เสมอ สิ่งที่อยู่ในหัวจะสูญหายเมื่อเปลี่ยนนักเขียน |
ขั้นตอนขั้นต่ำสำหรับหมุนหนึ่งรอบด้วย profile หนึ่งไฟล์ เมื่อมีบทพูดของบทใหม่ขึ้นมา
setup
1. เปิด voice_profile_<id>.md ของตัวละครเป้าหมาย หากไม่มี ให้รวบรวมบทพูดในเนื้อเรื่อง 20\~30 บรรทัด
2. เตรียมบทพูดใหม่เป็นชุดข้อความธรรมดาในรูปแบบ id / ตัวละคร / เนื้อหา
prompt
นี่คือ voice_profile §5 (สำนวนต้องห้าม) ของ K_007
[แปะรายการต้องห้าม]
ch04 บทพูดใหม่ 32 บรรทัด
[แปะในรูปแบบ id / เนื้อหา]
ช่วยจัดประเภทแต่ละบทพูดเป็น [ละเมิด] (มีคำศัพท์ต้องห้ามตรง ๆ) / [น่าสงสัย]
(แตะขอบเขตต้องห้ามแต่อาจเข้าข่ายบริบทยกเว้น) / [ปกติ]
[ละเมิด]·[น่าสงสัย] ให้แสดง id·เนื้อหา·เหตุผลเป็นตาราง การตัดสินผมเป็นคนทำเอง
ดังนั้นอย่าปฏิเสธโดยอัตโนมัติ
verify 1. ดู [ละเมิด] ก่อน หากชัดเจนก็แก้ข้อความ (อยู่เหนือเส้นประจึงฟรี) 2. [น่าสงสัย] ตัดสินด้วยบริบท หากเป็นการอ้างอิงที่ชอบธรรมก็ผ่าน หากไม่ใช่ก็แก้ 3. จากส่วนที่ผ่าน สุ่ม 10% ส่งให้ลีดเพื่อกรองความผิดพลาดเชิงบริบทอีกครั้ง 4. ออก dialogue_id และส่งเข้าคิวบันทึกเสียงได้ก็ต่อเมื่อตัดสินทุกรายการเสร็จแล้วเท่านั้น หน้าห้องอัดจะไม่ตรวจสอบอีกต่อไป
ฉบับย่อสำหรับคนเดียว — หากเป็นนักพัฒนาคนเดียวที่ทำเครื่องมือไม่ได้ ให้เขียน voice_profile เพียงหัวข้อเดียวคือ §5 (สำนวนต้องห้าม) ต่อตัวละคร ทุกครั้งที่เขียนบทพูดใหม่ ให้แปะรายการต้องห้ามนั้นไว้หัวพรอมต์แล้วสั่ง AI ว่า "ช่วยทำเครื่องหมายเฉพาะบรรทัดที่ติดรายการนี้" คุณจะได้ 80% ของ lint ด้วยพรอมต์บรรทัดเดียวโดยไม่ต้องมีเครื่องมือ ก่อนส่งเข้าบันทึกเสียง (หรือ TTS) แค่ผ่านขั้นนี้ครั้งเดียว เส้นประหน้าห้องอัดก็จะถูกรักษาไว้
เช้าวันจันทร์ในห้องประชุมวางแผน บนไวต์บอร์ดมีข้อความบรรทัดเดียว "ก่อนเปิดตัว — เควสต์เสริม 1,000 ชุด" มีคนหยิบเครื่องคิดเลขมากด ถ้านักเขียนหนึ่งคนใช้เวลาหนึ่งวันต่อหนึ่งเควสต์ ก็เท่ากับ 4 ปี ต่อให้ห้าคนช่วยกันทำก็ยังเกือบหนึ่งปี อากาศในห้องเริ่มหนักอึ้ง ผู้เขียนนั่งอยู่ในห้องนี้มา 24 ปีแล้ว และรู้ว่าเมื่อเจอตัวเลขแบบนั้น ผู้คนมักแตกออกเป็นสองทางเดิมเสมอ ฝ่ายหนึ่งบอกว่า "ลดปริมาณลงเถอะ" อีกฝ่ายบอกว่า "ใช้เครื่องมือปั๊มออกมาเลย" และเกือบทุกครั้ง คำตอบสุดท้ายคือทั้งสองอย่าง
การสร้างเนื้อหาแบบโพรซีเดอรัล (Procedural Content Generation ต่อไปเรียกว่า PCG) คือคำตอบเก่าแก่ของฝ่าย "ใช้เครื่องมือปั๊มออกมา" การจัดวางห้องในดันเจี้ยน การผสมออปชันอาวุธ และพูลการสปอว์นศัตรู ถูกทำให้อัตโนมัติด้วย rulebook และตารางความน่าจะเป็นมาตั้งแต่ 20 ปีก่อน สิ่งที่ใหม่จึงไม่ใช่ตัว PCG เอง แต่คือการที่ LLM และโมเดลเจเนอเรทีฟเข้ามาอยู่ในตำแหน่งที่เป็นภาษาธรรมชาติ ภาพ และเนื้อเรื่อง
แต่สิ่งที่หนังสือเล่มนี้ต้องการจะบอกไม่ใช่ "เอา AI ไปต่อกับ PCG สิ" นั่นใคร ๆ ก็ทำ ปัญหาคือต่อมัน ตรงไหน ต่างหาก หากเราหยิบเนื้อหาก้อนหนึ่งขึ้นมาแล้วไม่ตอกหมุดให้ชัดว่ามันมาพบกัน ณ ความเข้มของการทำงานอัตโนมัติระดับใด และที่ชั้นใดของโครงสร้าง ก็จะกลายเป็นสภาพที่มีเครื่องมือแต่ไม่มีที่ทาง บทนี้จะดูวิธีวาดช่องนั้นเป็นพิกัด และดูว่าบนช่องนั้น เนื้อหาหนึ่งชิ้นวิ่งครบหนึ่งรอบในไปป์ไลน์จริงอย่างไร
PCG แบบดั้งเดิมแข็งแกร่งในเรื่องความเป็นเชิงกำหนด (deterministic) อินพุตเดียวกันให้เอาต์พุตเดียวกัน และตรวจสอบได้ กราฟห้องในดันเจี้ยน prefix·suffix ของออปชันอาวุธ และการกระจายการสปอว์นศัตรู จึงลงตัวได้ตั้งแต่เนิ่น ๆ "ดาบเปลวเพลิง +5" ออกมาแบบอัตโนมัติได้ตั้งแต่ 20 ปีก่อน
ปัญหาอยู่ที่ตำแหน่งถัดจากนั้นเสมอ ห้องถูกจัดวางแล้ว แต่ชื่อ รูปลักษณ์ และภูมิหลังสั้น ๆ ของ NPC ในห้องยังตกอยู่ในมือนักเขียน "ดาบเปลวเพลิง +5" ออกมาได้ แต่บรรทัดเดียวที่ว่า "ดาบเล่มสุดท้ายที่กษัตริย์ทำหาย" ไม่ออกมา ต่อให้ตัว generator ของเควสต์จะสุ่มจับคู่เป้าหมายกับรางวัลมาให้ "ทำไมต้องทำเควสต์นี้" ก็ยังเป็นสิ่งที่คนเขียนเอง
ในเกมขนาดใหญ่ ตำแหน่งนี้คือคอขวดอยู่เสมอ อัตราส่วนระหว่างส่วนที่ปั๊มออกมาได้กับส่วนที่ต้องใช้มือคนอยู่ราว 4 ต่อ 6 และส่วนที่ใช้มือคน 6 ส่วนนั้นกินเวลาส่วนใหญ่ของกำหนดการ ต่อให้สายการผลิตปั๊ม 4 ส่วนออกมาได้เร็ว แต่ถ้า 6 ส่วนตามไม่ทัน รอบทั้งวงก็ถูกล็อกไว้ที่ความเร็วเท่านั้น
ตำแหน่งที่ LLM และโมเดลภาพเข้ามาคือตรงนั้นพอดี ขอบเขตที่ปั๊มออกมาได้ขยายไปครอบคลุมถึงพื้นที่ภาษาธรรมชาติ เนื้อเรื่อง และภาพที่ rulebook เคยจัดการไม่ได้ แต่ก็ไม่ได้หมายความว่าการยกตำแหน่งนั้นทั้งหมดให้ AI คือคำตอบ AI ให้คำตอบที่ต่างกันเล็กน้อยทุกครั้ง และเมื่อบริบทว่างเปล่า มันก็จะคายค่าเฉลี่ยของ RPG ทั่วไปออกมา เราจึงต้องออกแบบ จุดเชื่อมต่อ จุดเชื่อมต่อนิยามด้วยแกนพิกัดสองแกน
แกนตั้งคือ การผสมคน rulebook และ AI ในสัดส่วนใด ในบริษัทพัฒนา MMORPG แห่งหนึ่งที่ผู้เขียนทำงานอยู่ (ต่อไปเรียกว่า 'โปรเจกต์ A') เราแบ่งใช้เป็นสี่ระดับ
L0 — งานมือทั้งหมด ทุกตัวอักษรและทุกการตัดสินใจออกมาจากมือคน เนื้อหาเควสต์หลัก บทพูดของตัวละครซิกเนเจอร์ ตอนจบของเส้นเรื่องที่แตกแขนง คือตำแหน่งที่ความสอดคล้องและความลึกของเนื้อเรื่องเชื่อมตรงกับอัตลักษณ์ของเกม
L1 — การทำงานอัตโนมัติด้วย rulebook คือตำแหน่งของ PCG ดั้งเดิม อัลกอริทึมเชิงกำหนดอย่าง rulebook ตารางความน่าจะเป็น และ BSP สร้างเอาต์พุต ส่วนคนทำหน้าที่ตรวจสอบเท่านั้น การจัดวางห้องในดันเจี้ยน การผสมออปชันอาวุธ และการสปอว์นศัตรู เป็นตัวแทน
L2 — rulebook + AI ช่วย rulebook วางโครง ส่วน AI เติมรายละเอียด ซินอปซิสของเควสต์เสริม ชื่อและภูมิหลังสั้น ๆ ของ NPC ทั่วไป ข้อความแนะนำพื้นที่ล่าสัตว์ คนรับผิดชอบเพียงเมทาดาทาที่ป้อนเข้าและด่านตรวจสอบสุดท้ายเท่านั้น
L3 — AI นำหน้า + คนตรวจสอบ AI สร้างเนื้อหา ส่วนคนเข้าไปแค่ในขั้นตรวจสอบ น่าดึงดูดก็จริง แต่ความเสี่ยงของความไม่เป็นเชิงกำหนด อาการหลอน (hallucination) และความสอดคล้องที่เสียหาย ล้วนมารวมกันอยู่ที่นี่
หัวใจอยู่ที่ L2 มันรวมความเสถียรของ L1 เข้ากับกำลังการปั๊มของ L3 แล้วใช้ด่านตรวจสอบ (verification gate) ปิดจุดอ่อนของทั้งสองฝั่ง L3 มีแรงล่อใจให้รีบนำเข้ามาใช้สูง แต่ผู้เขียนเคยเห็นหลายครั้งที่ภาระการตรวจสอบพุ่งทะลุจนต้องเลิกใช้ภายในหนึ่งถึงสองไตรมาส ถ้าใน 100 ชิ้นมี 70 ชิ้นถูกยกขึ้นมาเป็นรายการน่าสงสัย มันก็แพงกว่าให้คนเขียนเองทั้ง 100 ชิ้นตั้งแต่ต้น
ลำพังแกนตั้งอย่างเดียว สายการผลิตยังขับเคลื่อนไม่ได้ ตัวเนื้อหา เอง ต้องถูกแยกออกเป็นชั้น ๆ เสียก่อน ที่ทางให้การทำงานอัตโนมัติเข้ามาจึงจะเกิดขึ้น นี่คือการแยก Layer ที่กล่าวถึงในส่วนที่ 5 และเป็นแกนนอนในแง่ของสายงานเนื้อหา คำอธิบายทั่วไปที่ว่าทั้งห้าชั้นต่างสอดคล้องกับบทบาทหนึ่งของการสร้างแบบโพรซีเดอรัล (จุดยึด·rulebook·เนื้อหา·ค่าตัวเลข·ด่านตรวจ) ได้กล่าวไว้แล้วใน §2.3.6 ในที่นี้จะนำมาวางลงในสายการผลิตเนื้อหาโดยตรง Layer 0 วิสัยทัศน์คือจุดยึดด้านโทนและโลกของเรื่อง (ฉีดเข้าทุกครั้งที่สร้าง) Layer 1 ระบบคือ rulebook ของการสร้าง (กฎ·ตารางความน่าจะเป็น·ระบบแท็ก) Layer 2 เนื้อหาคือตำแหน่งเนื้อความที่ผลลัพธ์ของการสร้างจะมาสะสมกัน (เควสต์เสริม·ภูมิหลัง NPC·ข้อความแนะนำเมือง) Layer 3 ข้อมูลคือค่าตัวเลข·ID·ความสัมพันธ์ (รางวัล·การสปอว์น·เส้นโค้ง) และ Layer 4 บิลด์·QA คือด่านตรวจสอบ (lint·การตรวจความสอดคล้อง·การตรวจสอบโดยนักเขียน)
สองแกนนี้เป็นคนละเรื่องกัน แกนตั้งบอกว่า "คนเข้าไปแตะมากแค่ไหน" แกนนอนบอกว่า "เนื้อหาส่วนใด" แต่ทั้งสองมีความหมายขึ้นมาได้ก็ต่อเมื่อคูณกันเท่านั้น เมื่อเราตอกหมุดเนื้อหาหนึ่งชิ้นลงที่จุดตัดของสองแกน คือ ช่องเดียว นั้น "ใครสร้างส่วนไหนด้วยวิธีใด" จึงจะถูกกำหนดขึ้นมา
นำสองแกนที่บรรยายเป็นถ้อยคำมาจนถึงตอนนี้มาซ้อนกันเป็นตารางแผ่นเดียว แนวนอนคือ Layer ของเนื้อหา แนวตั้งคือความเข้มของการทำงานอัตโนมัติ ป้ายในแต่ละช่องคือเนื้อหาที่ครอบครองช่องนั้นจริงในโปรเจกต์ A ยิ่งช่องสีเข้มมากเท่าใด ก็ยิ่งใกล้จุดศูนย์ถ่วงของสายการผลิตมากเท่านั้น
ตารางนี้คือหัวใจของบทนี้ การตัดสินที่เคยกระจัดกระจายอยู่ในถ้อยคำอย่าง "เควสต์หลักเป็น L0" "เควสต์เสริมเป็น L2" "รางวัลเป็น rulebook" มารวมอยู่ใน พิกัดเดียว เมื่อเนื้อหาใหม่ถูกยกขึ้นเป็นวาระในการประชุม คำถามเดียวว่า "อันนี้อยู่ช่องไหน" ก็เพียงพอ เมื่อกำหนดช่องได้ พิกัดแนวตั้งของช่องนั้นจะบอกว่าใครเป็นคนแตะ และพิกัดแนวนอนจะบอกว่าเป็นส่วนใด
เมื่ออ่านตารางนี้ไปเรื่อย ๆ จะมีสองอย่างที่เข้าตา หนึ่ง จุดศูนย์ถ่วง (ช่องสีเข้ม) อยู่ที่แถว L2 × คอลัมน์ Layer 2 โครงเควสต์เสริมและภูมิหลัง NPC คือตำแหน่งนั้น มันคือหัวใจของสายการผลิต สอง เนื้อหาหนึ่งชิ้นไม่ได้อยู่แค่ช่องเดียว เควสต์เสริมมีเนื้อความ (Layer 2) อยู่ในช่อง L2 ก็จริง แต่ค่ารางวัล (Layer 3) ของมันเลื่อนลงไปอยู่ในช่อง L1 ต่อให้เป็นเควสต์เดียวกัน แต่ละส่วนก็อาศัยอยู่คนละช่อง นี่คือเหตุผลที่เราแยกสองแกนออกจากกัน
บนช่องจุดศูนย์ถ่วง — แถว L2 × คอลัมน์ Layer 2 โครงเควสต์เสริม — เรามาดูกันว่าเนื้อหาหนึ่งชิ้นวิ่งครบหนึ่งรอบจริงอย่างไร กระแสการไหลเป็นดังนี้
flowchart TD
A["ฉีด Layer 0 วิสัยทัศน์
(จุดยึดโทน: หมู่บ้านชนบทแบบตะวันออก)"] --> B["Layer 1 rulebook
(สร้างโครงสล็อตเป้าหมาย·รางวัล)"]
B --> C["L2 AI ช่วย
(เติมแรงจูงใจ·ภูมิหลังหนึ่งบรรทัดของ NPC)"]
C --> D{"Layer 4 ด่านตรวจสอบ
(lint โทน·ความสอดคล้อง)"}
D -->|ผ่าน| E["Layer 3 ยืนยันข้อมูล
(ค่ารางวัลให้ rulebook คำนวณ)"]
D -->|ตีกลับ| B
E --> F["นำเข้าบิลด์"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class B,D code;
class C ai;
class E data;
class F pass;
ลองไล่ตามกระแสนี้ผ่านบันทึกเซสชันจริง (worked transcript) สักครั้ง สมมติว่าเรากำลังสร้างเควสต์งานจิปาถะหนึ่งชุดของหมู่บ้านชนบท
ขั้นที่ 1 — Layer 1 rulebook ดึงโครงออกมา rulebook เป็นเชิงกำหนด มันเติมแค่สล็อตแล้วคายออกมา
[โครงเควสต์ — เอาต์พุตของ rulebook]
พื้นที่: หมู่บ้านชนบท_3
บทบาทผู้ว่าจ้าง: คนเก็บสมุนไพร
ประเภทเป้าหมาย: เก็บรวบรวม(ยาสมุนไพร x6)
อุปสรรค: หมาป่าสปอว์น(กลางคืน)
สล็อตรางวัล: { gold: ?, exp: ?, item: เคียวเก็บเกี่ยว }
ขั้นที่ 2 — ขอรายละเอียดจาก AI เนื้อความพรอมต์เต็มเป็นดังนี้ (ป้อน Layer 0 วิสัยทัศน์เป็นบริบทก่อน)
คุณคือนักเขียนเควสต์ของ MMORPG ที่มีฉากเป็นหมู่บ้านชนบทแฟนตาซีตะวันออก จุดยึดโทน: "เรียบง่ายแต่แฝงด้วยความเชื่อโชคลางและความหวาดกลัวแบบชนบท" จงรับโครงด้านล่างไป แล้วเขียนแรงจูงใจหนึ่งบรรทัดของผู้ว่าจ้าง ชื่อ NPC และข้อความว่าจ้างสองประโยคที่ผู้เล่นจะเห็น ห้ามสร้างค่ารางวัลเด็ดขาด (ระบบเป็นผู้กำหนด) เอาต์พุตเป็น JSON โครง: { พื้นที่: หมู่บ้านชนบท_3, ผู้ว่าจ้าง: คนเก็บสมุนไพร, เป้าหมาย: เก็บยาสมุนไพร 6 ชิ้น, อุปสรรค: หมาป่ายามค่ำคืน, ไอเทมรางวัล: เคียวเก็บเกี่ยว }
ขั้นที่ 3 — ผลลัพธ์ดิบของ Claude (ย้ายมาตามต้นฉบับโดยไม่สรุปย่อ)
{
"npc_name": "ป้าซัมวอล",
"motive": "ทุกคืนเสียงหอนของหมาป่าทำให้ไร่สมุนไพรพังเสียหาย จนไม่กล้าออกไปเก็บยาสมุนไพรชิ้นสุดท้าย",
"quest_text": "พอตกค่ำพวกหมาป่าก็มาคุ้ยไร่สมุนไพรจนกระจุยกระจาย กลัวจนก้าวขาไม่ออก ช่วยไปเก็บรากยาสมุนไพรหกรากที่เชิงเขาแทนข้าหน่อยเถิด",
"reward": { "gold": 120, "exp": 300 }
}
มีสิ่งที่ต้องหยุดดูตรงนี้ AI สร้างฟิลด์ reward ใส่เข้ามาเอง ทั้งที่ไม่ได้สั่ง นี่แสดงให้เห็นตรง ๆ ว่าทำไมแกนที่ 1 กับแกนที่ 2 จึงต้องแยกออกจากกัน ค่ารางวัล (Layer 3) เป็นตำแหน่งของ rulebook ใน L1 ไม่ใช่ตำแหน่งของ AI (L2) ถ้ายกมันให้ AI ตัวเลขจะแกว่งไปทุกครั้งที่เรียก จนเส้นโค้งรางวัลพังทลาย
ขั้นที่ 4 — การตรวจสอบและปฏิเสธโดยคน ผู้ตรวจสอบทำสองอย่าง (1) ลบ ฟิลด์ reward ออก — นี่คือช่องที่ rulebook จะมาเติม (2) ดูโทน "ป้าซัมวอล" แรงจูงใจหนึ่งบรรทัด และข้อความว่าจ้างสองประโยค เข้ากับโทนหมู่บ้านชนบท ผ่าน หาก AI ใส่คำที่อยู่นอกโลกของเรื่องอย่าง "คำว่าจ้างจากกิลด์นักเวท" ก็จะตีกลับตรงนี้แล้วย้อนกลับไปขั้นสร้างโครง
ขั้นที่ 5 — ยืนยันข้อมูล Layer 3 rulebook เติมสล็อตรางวัลที่ถูกลบกลับเข้ามาใหม่ มันคือสูตรเชิงกำหนดที่ผูกกับเลเวลของพื้นที่และความยากของเป้าหมาย gold: 85, exp: 240 ค่าที่เข้ากับเส้นโค้งจะถูกใส่เข้าไป ไม่ใช่ 120·300 ที่ AI คายออกมาตามใจ
หนึ่งรอบนี้คือไซเคิลมาตรฐานของช่องจุดศูนย์ถ่วง rulebook ทำโครง AI เติมเนื้อ คนเป็นด่าน rulebook ใส่ค่าตัวเลขอีกที เนื้อหา 1,000 ชิ้นวิ่งตามไซเคิลนี้ทั้งหมด เพราะช่องถูกกำหนดไว้แล้ว จึงไม่ต้องมาเถียงกันใหม่ทุกครั้งว่า "อันนี้ใครสร้าง"
หากจะตัดสินว่าจะวางเนื้อหาใหม่ไว้ที่ช่องใดของตาราง ห้าคำถามนี้จะช่วยได้ ทุกครั้งที่วาระการปั๊มเนื้อหาถูกยกขึ้นในที่ประชุม ลองจดไว้แล้วช่วยกันตอบ ความสอดคล้องของการจัดวางช่องจะลงตัวภายในหนึ่งไตรมาส
หนึ่ง ภาระการปั๊มมากแค่ไหน ก่อนเปิดตัวต้องการ N ชุดหรือไม่ ถ้า N เกิน 100 แถว L0 แทบเป็นไปไม่ได้
สอง ความต้องการความสอดคล้องสูงแค่ไหน ถ้าความสอดคล้องระหว่างเนื้อหาคือหัวใจของประสบการณ์ ด่านตรวจสอบ (Layer 4) ต้องแข็งแกร่ง แต่ถ้าความหลากหลายคือหัวใจ ก็มีช่องว่างให้ขยับขึ้นไปแถวบนได้
สาม ยอมรับความไม่เป็นเชิงกำหนดได้หรือไม่ เป็นพื้นที่ที่ผลลัพธ์ต่างกันเล็กน้อยทุกครั้งช่วยสร้างความอุดมสมบูรณ์ หรือเป็นพื้นที่ที่ผลลัพธ์เหมือนเดิมคือหัวใจของความน่าเชื่อถือ
สี่ ค่าใช้จ่ายในการตรวจสอบเท่าไร ต่อเนื้อหาหนึ่งชิ้น 5 นาทีหรือ 30 นาที กำหนดความยาวของรอบการดำเนินงาน
ห้า ค่าใช้จ่ายเมื่อเกิดอุบัติเหตุเท่าไร ทิ้งและเขียนใหม่ได้อย่างอิสระ หรือเมื่อออกไปแล้วครั้งหนึ่งจะเชื่อมตรงสู่อุบัติเหตุของผู้ใช้
ลองโยนห้าข้อนี้ใส่เควสต์เสริม คำตอบจะรวมไปทางเดียว มากกว่า 1,000 ชุด (L0 เป็นไปไม่ได้) ความสอดคล้องต่ำกว่าเควสต์หลัก ยอมรับความไม่เป็นเชิงกำหนด ตรวจสอบ 5\~10 นาที ค่าใช้จ่ายอุบัติเหตุต่ำ (ทิ้งทีละชิ้นได้) เมื่อห้าคำตอบมารวมกัน ช่อง L2 × Layer 2 จึงเป็นธรรมชาติ ลองโยนห้าข้อเดียวกันใส่เควสต์หลัก คำตอบจะรวมไปตรงข้ามกัน 50 ชุด ความสอดคล้องและความลึกของเนื้อเรื่องสูงสุด ไม่ยอมรับความไม่เป็นเชิงกำหนด ค่าใช้จ่ายในการตรวจสอบสูง ค่าใช้จ่ายอุบัติเหตุสูงมาก — เป็นช่อง L0
ต่อให้วาดตารางไว้แล้ว กับดักที่ตกลงไปก็คล้ายเดิม มีสี่ข้อที่เกิดซ้ำ
หนึ่ง เริ่มจากแถว L3 ถ้าออกตัวด้วยความคาดหวังว่า "AI จัดการ 100 ชุดเอง" การตรวจสอบจะพุ่งทะลุ ให้ตั้งช่อง L1 ให้ลงตัวก่อน แล้วค่อยขยับขึ้น L2 ส่วน L3 ทำเฉพาะบางส่วนอย่างระมัดระวัง การกระทำที่คนลบฟิลด์รางวัลออกในไปป์ไลน์ขนาดย่อข้างต้น แสดงให้เห็นในขนาดเล็กว่าทำไมแถว L3 จึงอันตราย
สอง มอบหมายให้ AI ทั้งก้อนโดยไม่มี rulebook "ช่วยสร้างเควสต์เสริม 100 ชุด" จะเรียกค่าเฉลี่ยของ RPG ทั่วไปออกมา Layer 1 rulebook ต้องวางโครงก่อน แล้ว AI จึงเติมเนื้อบนนั้น เนื้อหาของเกมเราถึงจะออกมา การเขียน rulebook สักหนึ่งเล่มคืองานที่กินแรงที่สุดและน่าเบื่อที่สุดใน PCG แต่ถ้าข้ามมันไป การปั๊มทุกอย่างที่อยู่เหนือมันก็จะทรุดลงเป็นค่าเฉลี่ย
สาม ด่านตรวจสอบ (Layer 4) ว่างเปล่า ถ้าเอาต์พุตของ AI ถูกนำเข้าบิลด์โดยอัตโนมัติ อุบัติเหตุด้านความสอดคล้องจะเชื่อมตรง ไม่ว่าช่องใด ด่านที่เป็นคนก็จำเป็น
สี่ เลือกเครื่องมือโดยดูแค่ค่าใช้จ่าย ค่าใช้จ่าย LLM API ลดลงทุกไตรมาส แต่ค่าใช้จ่ายอุบัติเหตุด้านความสอดคล้องไม่ลดลง การตัดสินเรื่องเครื่องมือต้องดูที่ค่า API บวกกับผลรวมของเวลาความสอดคล้องและการตรวจสอบ
ในโปรเจกต์ A หลังจากย้ายเควสต์เสริมจากช่อง L0 ไปช่อง L2 ได้วัดผลเป็นเวลาหกเดือน ในบรรดาตัวเลขด้านล่าง ค่าสัมบูรณ์เป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ส่วนทิศทางและสัดส่วนของการเปลี่ยนแปลงคือสิ่งที่สังเกตได้จากการวัดจริง
| รายการ | ช่วง L0 | หลังเปลี่ยนเป็น L2 |
|---|---|---|
| เขียนเควสต์ 1 ชุดต่อนักเขียน 1 คน | ราว 4 ชั่วโมง | ราว 50 นาที (เมทา 30 นาที + AI 5 นาที + ตรวจสอบ 8 นาที) |
| ปั๊มต่อสัปดาห์ | 5 ชุด | 30\~40 ชุด |
| อัตราการทิ้ง | เกือบ 0% | ราว 20% |
| อุบัติเหตุความสอดคล้อง (ต่อไตรมาส) | 3\~5 ครั้ง | 5\~8 ครั้ง (ปกติหลังเสริม) |
| ความพึงพอใจของนักเขียน (เต็ม 10) | 8 | 6 → 7 (หลังเสริมนโยบาย) |
อัตราการทิ้งขึ้นไปเป็น 20% แต่ความเร็วการปั๊มเร็วขึ้น 6\~8 เท่า ปริมาณงานสุทธิจึงเพิ่มขึ้น 4\~5 เท่า อุบัติเหตุความสอดคล้องเพิ่มขึ้นเล็กน้อยเป็น 5\~8 ครั้งต่อไตรมาส แต่ด้วยด่านตรวจสอบและการเสริม rulebook ก็กลับสู่ช่วงปกติภายในไตรมาส
การเปลี่ยนแปลงที่ใหญ่ที่สุดไม่ใช่ตัวเลข แต่คือคน ตอนแรกนักเขียนรู้สึกเหมือนกลายเป็น "ผู้ตรวจสอบงานปั๊ม" ความพึงพอใจจึงตกจาก 8 เหลือ 6 เพื่อกอบกู้สิ่งนี้ เราจึงสอดนโยบายที่รับประกันเวลาของนักเขียนอย่างชัดเจนให้กับเควสต์หลักและเควสต์เสริมซิกเนเจอร์ (เมืองละ 1\~2 ชุด) เป็นการตอกหมุดให้สายการผลิตไม่ได้ดูดเวลาของนักเขียนไป แต่กลายเป็นเครื่องมือที่ส่งเวลานั้นคืนกลับไปยังเควสต์หลัก หกเดือนต่อมา ความพึงพอใจกลับมาเป็น 7
สิ่งหนึ่งที่ควรนำกลับไปจากการวัดผลนี้ การตัดสินใจย้ายช่องต้องให้ปริมาณงาน การจัดสรรเวลาของนักเขียน และความพึงพอใจตามไปด้วยกัน ถ้าดูแค่ปริมาณงาน การปั๊มก็สำเร็จ แต่คนจะจากไป
ข้อเสนอทั่วไปที่ว่าการแยก Layer คือเงื่อนไขเบื้องต้นของการสร้างแบบโพรซีเดอรัล อยู่ใน §2.3.6 ในที่นี้จะดูเพียงว่ามันเผยตัวบนตาราง PCG อย่างไร ในทีมที่แกนนอน (Layer 0\~4) พร่าเลือน ไม่มีช่องใดขับเคลื่อนได้อย่างเสถียร ถ้าไม่รู้ว่า Layer 0 วิสัยทัศน์อยู่ที่ไหน จุดยึดโทนของแต่ละ generator ก็ว่างเปล่าจนคายค่าเฉลี่ยของ RPG ทั่วไปออกมา ถ้า Layer 1 rulebook กับ Layer 2 เนื้อความปนกันอยู่ในไฟล์เดียว เวลาแก้กฎสักหนึ่งบรรทัดก็ต้องไปแตะเนื้อความหลายสิบจุดพร้อมกัน และถ้า Layer 3 ข้อมูลถูกป้อนอยู่ในเนื้อความ การปรับเส้นโค้งรางวัลครั้งเดียวจะกินเวลานักเขียนไปหนึ่งสัปดาห์ — นี่คือเหตุผลที่เราแยกรางวัลออกไปไว้เป็นสล็อตต่างหากในไปป์ไลน์ขนาดย่อข้างต้น
ดังนั้นสิ่งที่ต้องตรวจก่อนนำ PCG เข้ามาจึงไม่ใช่การเลือกเครื่องมือ แต่คือ แกนนอนถูกแยกออกหรือยัง ในทีมที่มีครบห้าชั้น ค่าใช้จ่ายในการต่อ generator ของ L1 คือเวลานักเขียนหนึ่งคนหนึ่งไตรมาส ในทีมที่ห้าชั้นปนกัน การนำเข้ามาแบบเดียวกันจะถูกเลิกใช้ด้วยอุบัติเหตุความสอดคล้องภายในสองไตรมาส
ห้าช่องไม่จำเป็นต้องสมบูรณ์แบบตั้งแต่ต้น แยกอย่างค่อยเป็นค่อยไป อินเทอร์เฟซให้แคบ ในไตรมาสแรก แค่แยกโทนหนึ่งบรรทัดของ Layer 0 และ rulebook หนึ่งเล่มของ Layer 1 ออกมา ที่ทางให้ generator เข้ามาก็เปิดออกแล้ว แต่ก็ไม่ได้หมายความว่าผัดผ่อนได้ไม่มีที่สิ้นสุด ถ้า Layer 2 เนื้อความกับ Layer 3 ข้อมูลยังเป็นก้อนเดียวกันจนถึงที่สุด เครื่องมือที่เป็นรูปธรรมในบทถัดไปก็จะหาที่ทางลงไม่ได้
ในบทถัดไปจะผ่าเครื่องมือที่เป็นรูปธรรมหนึ่งชิ้นซึ่งครอบครองช่องจุดศูนย์ถ่วงของตารางนี้ นั่นคือ proj_city_hunting_generator ที่ปั๊มพื้นที่ล่าสัตว์แยกตามเมือง เราจะดูว่าเมทาดาทาที่ป้อนเข้า โครง rulebook เนื้อความจาก AI และด่านตรวจสอบ ถูกมัดรวมเป็นหนึ่งไซเคิลอย่างไร และไปป์ไลน์ขนาดย่อของบทนี้ขยายใหญ่ขึ้นในขนาดของเครื่องมือจริงอย่างไร
setup เลือกเนื้อหาที่จะปั๊ม 1 ประเภท (เช่น เควสต์เสริม) แยกโทนหนึ่งบรรทัดของ Layer 0 และโครง rulebook ของ Layer 1 (นิยามสล็อต) ออกมาเป็นไฟล์ต่างหาก ปล่อยสล็อตค่ารางวัลให้ว่างไว้ทางฝั่ง rulebook
prompt หลังป้อนวิสัยทัศน์เป็นบริบทแล้ว ให้โครงไป และระบุชัดว่า "ห้ามสร้างค่ารางวัล เอาต์พุตเป็น JSON" ดัดแปลงพรอมต์ขั้นที่ 2 ข้างต้นมาใช้ได้เลย
verify ดูสามอย่าง (1) ถ้า AI ใส่ฟิลด์รางวัลเข้ามาเองให้ลบออก (L3 เป็นตำแหน่งของ rulebook) (2) ถ้ามีคำที่อยู่นอกโลกของเรื่อง ให้ตีกลับไปขั้นสร้างโครง (3) เฉพาะส่วนที่ผ่านเท่านั้นที่ rulebook จะเติมค่ารางวัลแล้วใส่เข้าบิลด์
ฉบับย่อสำหรับคนเดียว ไม่มีทีมก็ได้ ให้คุณสร้าง rulebook หนึ่งเล่ม (สล็อต 5 ช่อง) กับโทนหนึ่งบรรทัดเป็นไฟล์ข้อความเอง ลองรันเควสต์ 10 ชุดผ่านไซเคิลข้างต้น แล้วนับว่าตอนตรวจสอบตีกลับไปกี่ชุด ถ้าอัตราการตีกลับเกิน 30% แปลว่าช่องผิด — ให้ทำโครง rulebook ให้ละเอียดขึ้น หรือเลื่อนลงหนึ่งแถว (L1) แล้วดูใหม่ เมื่ออัตราการตีกลับเสถียร นั่นคือสัญญาณว่าช่องนั้นทำงานได้ในขนาดของคุณ
ผู้อ่านหลัก: นักออกแบบเกม MMORPG ที่รับผิดชอบการผลิตเนื้อหาจำนวนมาก (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านที่ทำคนเดียว/เป็นงานอดิเรก: §6.2.10 「ถ้าทำคนเดียวก็แค่เท่านี้」
ผมยังจำการคำนวณในวันแรกที่ได้รับตารางเวลาซึ่งบอกว่าต้องมีเมือง 30 แห่งก่อนเปิดตัว เมืองหนึ่งประกอบด้วยข้อความแนะนำ 5–10 บรรทัด พื้นที่ล่า 3–5 จุด NPC 5–10 คนกับเควสต์เสริม 2–3 อันต่อพื้นที่ล่าแต่ละจุด ไอเทมพิเศษประจำถิ่น 1–3 ชนิด และบอสประจำเมือง 1 ตัว ถ้าปั้นเมืองหนึ่งด้วยมือก็กินเวลา 1–2 สัปดาห์ ถ้า 30 แห่งก็เท่ากับให้นักเขียนหนึ่งคนทุ่มเวลา 6 เดือนเต็มไปกับเมืองเพียงอย่างเดียว
แต่ 6 เดือนนั้นไม่มี เวลาของนักเขียนถูกผูกไว้กับเควสต์หลักและตัวละครซิกเนเจอร์อยู่แล้ว และเมือง 30 แห่งต้องทำควบคู่ไปกับงานนั้น แรงกระตุ้นแรกที่ว่า "ก็ให้ AI สร้างเมือง 30 แห่งให้ก็ได้นี่" พังลงในไม่ช้า เพราะถ้าสั่งทั้งก้อน สิ่งที่ได้คือหมู่บ้านแฟนตาซีที่คล้ายกัน 30 แห่ง บทนี้จะดูว่าเครื่องมือ city_hunting_generator ที่ผมสร้างขึ้นแทนแรงกระตุ้นนั้นรวมสี่ขั้นตอน — อินพุต รูลบุ๊ก AI และการตรวจสอบ — เข้าเป็นหนึ่งรอบได้อย่างไร และเมื่อหมุนรอบนั้นจริงจนจบจริงสักครั้ง อะไรจะออกมาและอะไรจะถูกทิ้ง
บันทึกการใช้งานจริงของผู้เขียน
city_hunting_generatorในบทนี้คือการนำเครื่องมือจริงที่ผู้เขียนใช้งานอยู่ในโฟลเดอร์ R&D ของบริษัทมาทำให้เป็นนิรนาม ชื่อไฟล์ โครงสร้างโค้ด และรายการตรวจสอบนั้นยกมาจากเครื่องมือจริงอย่างซื่อตรง ส่วนชื่อเมือง (เช่น silvermark) และชื่อเฉพาะของบริษัทถูกแทนที่ใหม่เพื่อใช้ในหนังสือ เนื้อหาผลลัพธ์เป็นการเรียบเรียงใหม่จากเซสชันจริง
ลำดับการทำงานทั้งหมดของเครื่องมือมีสี่ขั้น หัวใจอยู่ที่ขั้น 1 และขั้น 3 เป็นเชิงกำหนด (deterministic — รูลบุ๊ก) มีแต่ขั้น 2 เท่านั้นที่เป็น AI เมื่อรูลบุ๊กยึดทั้งโครงสร้างและการตรวจสอบไว้จากสองฝั่ง AI ที่อยู่ตรงกลางต่อให้ตอบต่างกันเล็กน้อยทุกครั้ง ความสอดคล้องระหว่างเมืองก็จะไม่สั่นคลอน คนเข้าไปเกี่ยวข้องเฉพาะอินพุตแรก (เมตาดาตา) และด่านสุดท้าย (การตรวจสอบ) เท่านั้น
flowchart TB
A["อินพุต: เมตาดาตาเมือง yaml
(คน 15~20 นาที/เมือง)
lore_seeds 3 ตัว · forbidden_names แนบอัตโนมัติ"]
A --> B["ขั้น 1 เชิงกำหนด: rules.py
generate_skeleton()
จำนวนพื้นที่ล่า·การกระจายศัตรู·เส้นโค้งรางวัล·บอส"]
B --> C["ขั้น 2 AI: พรอมต์ 3 แบบ
L0 แคชวิสัยทัศน์ + L1 ฉีดกฎ
+ L2 เนื้อหาเมืองข้างเคียง
→ ข้อความแนะนำ·NPC·เควสต์เสริม"]
C --> D{"ขั้น 3 เชิงกำหนด: lint
ชื่อซ้ำ·คำต้องห้าม·โทนเสียง
·ปริมาณ·ช่วงรางวัล"}
D -->|ละเมิด alert| E["ขั้น 4 ด่านตรวจสอบโดยนักเขียน
(5~10 นาที/เมือง)
ตัดสินใจทิ้ง·สร้างใหม่"]
E -->|ขอใหม่| C
E -->|ผ่าน| F["นำเข้าบิลด์"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class B,D code;
class C ai;
class A,E human;
class F pass;
ในภาพนี้ จุดที่มือคนแตะมีเพียงสองแห่ง ตำแหน่งบนสุดที่ใส่เมตาดาตาหนึ่งหน้าให้สะอาด และตำแหน่งล่างสุดที่ตัดสินเรื่องโทนเสียงและการเล่าเรื่องซึ่ง lint จับไม่ได้ ระหว่างนั้น การสร้างโครงที่น่าเบื่อและการผลิตเนื้อหาจำนวนมากให้รูลบุ๊กกับ AI หมุนไป การออกแบบที่ชี้ขาดคือ แม้ lint (ขั้น 3) จะพบการละเมิด ก็จะไม่ทิ้งโดยอัตโนมัติ แต่ส่ง alert ขึ้นไปยังด่านนักเขียน (ขั้น 4) เท่านั้น เหตุผลจะดูใน §6.2.5
นักเขียนเขียนเมตาดาตาหนึ่งหน้าต่อหนึ่งเมือง เวลาที่ใช้คือ 15–20 นาที สั้นก็จริง แต่หนึ่งหน้านี้คืออินพุตทั้งหมดของสามขั้นถัดไป
# city_021_silvermark.meta.yaml
city_id: city_021_silvermark
region: west
climate: cold_arid
dominant_faction: scholar_guild
cultural_tone: scholarly_strict
level_range: [25, 30]
lore_seeds:
- เคยเป็นศูนย์กลางของการผนึกเวทมนตร์เมื่อ 100 ปีก่อน
- สัญญาณแรกของการเสื่อมพลังผนึกถูกพบในเมืองนี้
- ที่ตั้งสำนักงานใหญ่ของกิลด์นักปราชญ์
neighbors: [city_018, city_023]
# forbidden_names: (สคริปต์แนบให้อัตโนมัติ — นักเขียนไม่ต้องกรอก)
สล็อตที่สำคัญที่สุดคือ lore_seeds เหตุการณ์สำคัญ 3–5 อันเป็นตัวยึดตัวตนของเมือง ถ้าน้อยเกินไป AI จะคายเมืองแฟนตาซีทั่ว ๆ ไปออกมา ถ้ามากเกินไป เหตุการณ์ก็จะขัดแย้งกันเอง จากประสบการณ์ของผู้เขียน 3 อันเสถียรที่สุด
forbidden_names นักเขียนไม่ต้องเติม สคริปต์จะอ่านรายชื่อเมืองและตัวละครที่มีอยู่เดิมมาแนบเข้ากับเมตาดาตาโดยอัตโนมัติ เพราะเมื่อสะสมเมือง 30 แห่ง × NPC เฉลี่ย 50 คน การตรวจสอบความซ้ำของชื่อ 1,500 ชื่อด้วยหัวคนนั้นเป็นไปไม่ได้ จึงไม่ต้องเขียน "ทำให้ไม่ซ้ำกับ NPC เมืองอื่น" ด้วยมือทุกครั้ง
รูลบุ๊กรับเมตาดาตามาสร้างโครงสร้างของเมือง โค้ดเรียบง่าย
# city_hunting_generator/rules.py (โครง)
def generate_skeleton(meta):
region_rules = REGION_RULES[meta.region]
hg_count = region_rules.hunting_grounds_range.sample()
enemy_dist = ENEMY_RULES[meta.climate][meta.dominant_faction]
skeleton = {
"hunting_grounds": [
{
"id": f"{meta.city_id}_hg_{i}",
"level": meta.level_range[0] + i,
"enemy_types": enemy_dist.sample(k=3),
"reward_curve": calc_reward(meta.level_range[0] + i),
"npc_count": region_rules.npc_per_hg,
"sidequest_count": region_rules.sidequest_per_hg,
}
for i in range(hg_count)
],
"boss": {
"id": f"{meta.city_id}_boss",
"level": meta.level_range[1] + 2,
"pattern": BOSS_PATTERNS[meta.region],
},
}
return skeleton
ผลลัพธ์เป็นเชิงกำหนด ใส่เมตาดาตาเดียวกันก็ได้โครงเดียวกัน โค้ดรับประกันว่าเส้นโค้งรางวัลอยู่ในช่วงมาตรฐานตาม region·level หรือไม่ และการกระจายศัตรูตรงกับกฎ climate·faction หรือไม่ และการทดสอบถดถอย (regression test) ก็จับได้ ขั้นนี้ไม่มอบให้ AI เด็ดขาด เพราะถ้าให้ AI สุ่มเส้นโค้งรางวัลเป็นตัวเลขต่างกันทุกครั้งที่เรียก ความสมดุลระหว่างเมืองก็จะสั่นคลอนตรงนั้นทันที
เมื่อใส่เมตาดาตาของ silvermark rules.py จะคืนโครงเปล่ามาเป็นพื้นที่ล่า 4 จุด (city_021_silvermark_hg_0\~hg_3) แต่ละจุดมีสล็อต NPC 6 ช่อง·สล็อตเควสต์เสริม 3 ช่อง และบอสเลเวล 32 จำนวน 1 ตัว ยังไม่มีทั้งชื่อและเนื้อหา เป็นตารางของช่องที่ต้องเติม การเติมช่องเหล่านั้นคืองานของ AI ในขั้น 2
หลังจากรูลบุ๊กสร้างโครงแล้ว AI ก็เติมเนื้อหาภาษาธรรมชาติลงไปบนนั้น ข้อความแนะนำเมือง ชื่อ·รูปลักษณ์·ภูมิหลังสั้น ๆ ของ NPC ซินอปซิสเควสต์เสริม และข้อความบรรยายรสชาติ (flavor text) ของไอเทมพิเศษประจำถิ่น ออกมาตรงนี้
รูปแบบการเรียกใช้คือโครงสร้าง 4 ชั้นของการฉีดบริบทตามเดิมเลย แคช L0 วิสัยทัศน์ (world_premise + tone_manifesto) ฉีด L1 กฎ (city_naming_rule + region_west_lore) แบบเลือกใส่ เพิ่ม L2 เนื้อหาข้างเคียง (รายชื่อ NPC ของเมืองอื่น) และต่อท้ายด้วยคำสั่งงานในตอนสุดท้าย พรอมต์ข้อความแนะนำเมืองอยู่ในรูปที่คัดลอกไปใช้ได้ทันที
[บริบท L0] world_premise + narrative_pillar + tone_manifesto (แคช)
[บริบท L1] city_naming_rule, region_west_lore
[อินพุต] city_021_silvermark.meta.yaml + lore_seeds 3 ตัว
เขียนข้อความแนะนำเมืองนี้ให้ 6~8 บรรทัด สอด lore_seeds ทั้งสามอันเข้าไปอย่างเป็นธรรมชาติ
และตัดคำซ้ำซากแบบ RPG อย่าง "หมู่บ้านอันสงบสุข" ออก โทนต้องเป็นแบบนักปราชญ์และเข้มงวด ยับยั้งอารมณ์ความรู้สึก
เอาแต่เนื้อหา ไม่ต้องมีคำนำหรือคำอธิบายประกอบ
รูปแบบเดียวกันนี้ทำซ้ำในการเรียกผลิต NPC และเควสต์เสริมตามเดิม ต่างกันแค่บริบทและรูปแบบเอาต์พุตเท่านั้น การเรียก NPC จะมีสล็อต forbidden_names (ห้ามชื่อ NPC ซ้ำกับเมืองอื่น) ใส่เข้าไป และการเรียกเควสต์เสริมจะมีข้อจำกัด "ให้เลือก NPC ผู้มอบหมายงานจาก NPC ที่สร้างไว้ข้างบน" ใส่เข้าไป
แล้วจริง ๆ มันออกมาเป็นอะไร ลองหมุนจนจบสักหนึ่งครั้ง
ถ้าเขียนแบบนามธรรมว่า "AI เติมเนื้อหา" อย่างเดียว ก็ไม่อาจรู้ว่าเครื่องมือนี้คายอะไรออกมาจริง ๆ จึงขอตามรอบของเมือง silvermark หนึ่งเมืองตั้งแต่อินพุตจนถึงการทิ้งจนจบสักหนึ่งรอบ
[เอาต์พุตขั้น 2 AI — ข้อความแนะนำเมือง] (เรียบเรียงใหม่จากเซสชันจริง)
ซิลเวอร์มาร์กคือเมืองแห่งการผนึก เมื่อ 100 ปีก่อน นักปราชญ์เจ็ดคนได้สร้างวงผนึกไว้ใต้ดินที่นี่เพื่อสกัดการล้นทะลักของพลังเวทฝั่งตะวันตก และเมืองก็เติบโตขึ้นมาโดยวางหินทีละก้อนไว้บนวงผนึกนั้น หอระฆังของสำนักงานใหญ่กิลด์นักปราชญ์คือหอสังเกตการณ์ที่อ่านชีพจรของการผนึก ไม่ใช่ยอดหอที่มุ่งสู่เทพเจ้า ในช่วง 3 ปีหลัง มีข้อความหนึ่งบรรทัดถูกบันทึกซ้ำในรายงานการสังเกต "ชีพจรกำลังเย็นลง" สัญญาณแรกของการเสื่อมพลังผนึกสั่นไหวขึ้นเป็นครั้งแรกที่หน้าปัดของเมืองนี้ ไม่ใช่ที่อื่นใด นักปราชญ์ตามท้องถนนไม่พูดถึงมันด้วยเสียงดัง เพียงแต่วันที่ไฟบนหอระฆังไม่ดับตลอดทั้งคืนมีมากขึ้นเท่านั้นเอง
ตรงนี้ lore_seeds ทั้ง 3 อัน (ศูนย์กลางการผนึก·สัญญาณเสื่อมแรก·สำนักงานใหญ่กิลด์นักปราชญ์) ถูกใส่ลงในเนื้อหาครบ ไม่มีคำซ้ำซากแบบ "หมู่บ้านอันสงบสุขที่ต้อนรับนักผจญภัย" และโทน scholarly_strict (ยับยั้งอารมณ์ ใช้คำศัพท์เชิงสังเกต·บันทึก) ยังคงอยู่ นี่คือหน้าตาของเอาต์พุตที่ผ่านได้
ต่อจากนั้น รอบเดียวกันก็เติม 3 ช่องจาก 6 ช่องสล็อต NPC ของพื้นที่ล่า hg_0
[เอาต์พุตขั้น 2 AI — NPC 3 คน] (เรียบเรียงใหม่จากเซสชันจริง)
1) โดเรน เวล (Doren Vale) — ผู้ช่วยสังเกตการณ์หอระฆัง วัย 50 ปลาย ติดแว่นขยายทองเหลืองที่ตาข้างหนึ่งถาวร ทำงานบันทึกการสั่นสะเทือนละเอียดของชีพจรการผนึกมา 30 ปี พูดน้อย สื่อสารด้วยตัวเลขเท่านั้น "วันนี้ 12.4 เมื่อวานนี้ 12.1 มันกำลังขึ้น ไม่ใช่เรื่องดี"
2) มิรา คอสต์ (Mira Kost) — บรรณารักษ์คลังเอกสารกิลด์ วัย 30 รอยหมึกที่นิ้วลบไม่ออก คอยรักษาต้นฉบับการออกแบบวงผนึกไว้ แต่เชื่อว่านักปราชญ์ที่อ่านแบบแปลนนั้นออกได้ตายไปหมดแล้ว ระแวงคนนอกอย่างมาก
3) เกรม (Grem) — คนเฝ้าเตาไฟใต้หอระฆัง ไม่ทราบที่มา อายุไม่แน่ชัด งานเดียวคือไม่ปล่อยให้ไฟบนหอระฆังดับ และตอบคนที่ถามเรื่องการผนึกเพียงว่า "แค่ดูไฟก็พอ" (ทำเครื่องหมายคลุมเครือ — AI รายงานเอง)
ขอให้สังเกตว่า NPC คนที่สาม 'เกรม' ถูก AI ติด เครื่องหมายคลุมเครือ ให้เอง พรอมต์ที่ดีทำให้ AI สามารถพูดได้ว่า "อันนี้ผมยืนยันไม่ได้" ตอนนี้ lint ขั้น 3 จะตรวจเอาต์พุตชุดนี้
[เอาต์พุตขั้น 3 lint] (รูปแบบจริง)
[PASS] ตรวจปริมาณ: ข้อความแนะนำ 7 บรรทัด (เกณฑ์ 6~8) [PASS] ช่วงรางวัล: reward_curve ของ hg_0~hg_3 อยู่ในช่วงมาตรฐาน [WARN] ชื่อซ้ำ: "Mira Kost" — กับ "Mira Veldt" ของ city_014_riverhold นามสกุล (Kost/Veldt) ต่างกันแต่ชื่อ (Mira) เหมือนกัน ชนกับ forbidden_names ใกล้เคียง [PASS] คำศัพท์ต้องห้าม: ละเมิด tone_manifesto 0 รายการ [WARN] ความสอดคล้องโทนเสียง: บทพูดของ "เกรม" voice_lint เชื่อมั่น 0.62 (ต่ำกว่าเกณฑ์ 0.70)
lint จับการละเมิดได้ 2 รายการ แต่ไม่ได้ทิ้งรายการใดโดยอัตโนมัติ เพียงส่งขึ้นด่านนักเขียนด้วย WARN เท่านั้น นี่คือหัวใจของการออกแบบที่บอกล่วงหน้าไว้ใน §6.2.1 ถ้ามอบสิทธิ์ปฏิเสธอัตโนมัติให้ตัวตรวจสอบด้วย นักเขียนก็จะกดสวิตช์นั้นลงภายในไม่ถึงหนึ่งสองเดือน เพราะเครื่องจะฆ่าแม้กระทั่งความแปรผันที่ตั้งใจไว้รวมไปด้วย และนักเขียนยังถูกพรากโอกาสที่จะกะเส้นแบ่งนั้นด้วยตัวเองด้วย ฉะนั้นจึงมอบงานคัดกรองตัวเลือกที่น่าสงสัยให้เครื่อง แต่การตัดสินใจครั้งสุดท้ายว่าจะเก็บหรือทิ้งตัวเลือกนั้นยังคงทิ้งไว้ในมือคน
[การตรวจสอบโดยนักเขียนขั้น 4 — การตัดสินและการทิ้ง]
นักเขียนจัดการ alert 2 รายการดังนี้
- Mira Kost → เก็บไว้ ชื่อเหมือนกับ Mira Veldt ของ riverhold แต่คนละเมือง คนละนามสกุล ไม่มีโอกาสปรากฏพร้อมกัน ผ่านในฐานะความแปรผันที่ตั้งใจไว้ (แต่บันทึกแยกไว้ว่าจะให้กฎ forbidden_names เปลี่ยนจาก "ชื่อ+นามสกุลตรงกันทั้งหมด" เป็น "ชื่อชนกันเดี่ยว ๆ ก็ WARN" หรือไม่)
- เกรม → ทิ้ง การที่ voice_lint เชื่อมั่นต่ำคือสัญญาณ อ่านอีกครั้งก็พบว่าตัวละครคนเฝ้าเตาไฟที่พูดว่า "แค่ดูไฟก็พอ" ขัดกับโทน scholarly_strict ของเมือง ถ้า NPC ในเมืองที่กิลด์นักปราชญ์ปกครองหลุดไปทางโทนลึกลับ ตัวตนของเมืองก็จะพร่าเลือน ทิ้งแล้วขอใหม่
หลังจากนักเขียนตัดสินใจทิ้งแล้ว การขอใหม่ก็หมุนหนึ่งรอบ "ทิ้งสล็อตเกรม สร้าง NPC คนเฝ้าเตาไฟที่เข้ากับโทนกิลด์นักปราชญ์ (สังเกต·บันทึก·เข้มงวด) ของพื้นที่ล่าเดียวกันใหม่ ห้ามใช้คำศัพท์เชิงลึกลับ" AI ตอบกลับมาเป็นคนแก่ที่บันทึกอุณหภูมิของเตาไฟบนหอระฆัง มองแม้แต่ไฟเป็นข้อมูล และเอาต์พุตนั้นผ่านด้วย voice_lint 0.81 หนึ่งรอบของ อินพุต → โครง → เนื้อหา → ตรวจสอบ → ทิ้ง → สร้างใหม่ ปิดลงตรงนี้
หนึ่งรอบนี้คือเกณฑ์ Show ของทั้งเล่ม ถ้าไม่เคยดูจนจบสักครั้งว่าเครื่องมือคายอะไร อะไรถูกจับ และคนฆ่าอะไร ประโยคที่ว่า "ผลิตจำนวนมากด้วย AI" ก็กลวงเปล่า
ในรอบข้างต้น NPC ถูกทิ้งไป 1 คน เมื่อมองทั้งเมือง การทิ้งจะสะสมมากขึ้น เวลาตรวจสอบเฉลี่ย 5–10 นาทีต่อเมือง อัตราการทิ้งของ NPC ประมาณ 20% และของเควสต์เสริมประมาณ 33%
ผมขอบอกที่มาของอัตราส่วนนี้อย่างซื่อตรง อัตราการทิ้งเป็นค่าที่นับเองขณะตรวจสอบเมือง 5 แห่งที่รวม silvermark ในช่วงเริ่มนำมาใช้โดยตรง NPC ทิ้งไป 6 คนจากที่ตรวจ 30 คน (20%) เควสต์เสริมทิ้งไป 5 อันจากที่ตรวจ 15 อัน (33%) เนื่องจากตัวอย่างมีเพียง 5 เมืองซึ่งเล็ก จึงควรอ่านมันเป็นค่าบ่งทิศ "หนึ่งในห้า หนึ่งในสาม" มากกว่าจะเป็นสัดส่วนประชากรที่แม่นยำ อัตราสะสมหลังจากตรวจครบทั้ง 30 เมืองอาจต่ำลงกว่านี้ หรือสูงขึ้นตามลักษณะของพื้นที่ล่าก็ได้
สิ่งสำคัญคืออัตราการทิ้ง 0% ไม่ใช่เป้าหมาย การทิ้ง 0% ใกล้เคียงกับสัญญาณว่าการตรวจสอบไหลผ่านไปอย่างเป็นพิธีการ เมื่อ NPC หนึ่งในห้าถูกทิ้งเพราะโทนไม่เข้ากัน และเควสต์เสริมหนึ่งในสามถูกสร้างใหม่เพราะไม่เกาะกับ lore_seeds นั่นคือตอนที่ด่านตรวจสอบทำงานจริง
เปรียบเทียบก่อนและหลังนำเครื่องมือมาใช้ ค่าเวลาด้านล่างเป็นค่าเฉลี่ยที่วัดจริงของเมืองช่วงแรกที่รวม silvermark ส่วนคอลัมน์ "ก่อนนำมาใช้" เป็นค่าประมาณของนักเขียนในช่วงทำมือก่อนมีเครื่องมือ ไม่มีตัวเลขที่กุขึ้น
| รายการ | ก่อนนำมาใช้ (ทำมือ) | หลังนำมาใช้ (วัดจริง) |
|---|---|---|
| เวลาเขียนเมือง 1 แห่ง | 1\~2 สัปดาห์ | ราว 30 นาที (เมตา 15 นาที + AI 5 นาที + ตรวจสอบ 8 นาที) |
| ระยะเวลารวมเมือง 30 แห่ง | ระดับนักเขียน 1 คน 6 เดือน | 4\~5 สัปดาห์ |
| อัตราการทิ้ง (NPC) | — (เขียนเองทั้งหมด) | ราว 20% (6 คนจาก 30 คน) |
| อัตราการทิ้ง (เควสต์เสริม) | — | ราว 33% (5 อันจาก 15 อัน) |
| เหตุไม่สอดคล้อง (ต่อเมือง) | แทบไม่มี | 0\~1 รายการ |
ดูแค่ตารางก็เหมือนตัวเลขเป็นทั้งหมด แต่ผลจริงออกมาจากช่องอื่น เมื่อเวลาของนักเขียนที่เกือบถูกผูกไว้กับการผลิตเมืองจำนวนมากถูกปลดปล่อย นักเขียนหนึ่งคนก็เพิ่มผลผลิตเควสต์หลักต่อไตรมาสได้มากขึ้นอย่างมาก (จำนวนเท่าที่แน่นอนต่างกันไปในแต่ละไตรมาส จึงไม่ฟันธง — ทิศทางคือ "ผลผลิตเควสต์หลักเพิ่มขึ้นอย่างชัดเจน") เครื่องมือผลิตจำนวนมากจึงทำงานเป็นเครื่องมือที่ปลดปล่อย ไม่ใช่ดูดกลืน เวลาของนักเขียน (คำเตือนใน §6.1.8 ที่ว่าถ้านักเขียนรู้สึกว่าตนกลายเป็น "ตัวตรวจสอบ" เครื่องมือก็จะถูกปฏิเสธ ใช้ได้ตรง ๆ)
แม้ขอบเขตการทำงานอัตโนมัติจะกว้างขึ้น แต่สิ่งต่อไปนี้วางไว้นอกเครื่องมือ
| เนื้อหา | เหตุผลที่วางไว้นอกเครื่องมือ |
|---|---|
| เนื้อหาเควสต์หลัก | ความสอดคล้องและความลึกของการเล่าเรื่องเชื่อมโยงตรงกับตัวตนของเกม |
| แพตเทิร์น·การจัดฉากบอส | รายละเอียดด้านภาพ·การโต้ตอบมีมาก มือดีไซเนอร์เร็วกว่า |
| ตัวละครหลักซิกเนเจอร์ | ต้องเขียน voice_profile แบบเต็ม จึงผลิตจำนวนมากไม่ได้ |
| ตอนจบแบบแยกสาย | เป็นพื้นที่ตัดสินใจโดยตรงของนักเขียน |
| เควสต์เสริมซิกเนเจอร์ของเมือง 1\~2 อัน | นักเขียนเลือกและสร้างเอง |
ข้อเท็จจริงที่ว่าผลิตจำนวนมากได้ ไม่ควรนำไปสู่การตัดสินใจที่ว่าต้องผลิตจำนวนมากโดยอัตโนมัติ อย่างที่เห็นในรอบ silvermark เครื่องมือผลิต NPC ได้ดีถึง 5 คนจาก 6 คน แต่ NPC ซิกเนเจอร์หนึ่งคนที่ต้องแบกความตึงเครียดหลักของเมืองนั้นที่ว่า 'การผนึกกำลังเย็นลง' นักเขียนปั้นด้วยมือเอง เมื่อขอบเขตของการทำงานอัตโนมัติชัดเจน เครื่องมือผลิตจำนวนมากก็กลับกลายเป็นเครื่องมือที่ปกป้องพื้นที่หัวใจนั้นแทน
| แพตเทิร์นความล้มเหลว | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| เขียน lore_seeds แค่ 1\~2 อัน | เอาต์พุต AI ถูกเกลี่ยเป็นค่าเฉลี่ย RPG ทั่วไป | บังคับให้มี 3 อันขึ้นไป (§6.2.2) |
| ขอ AI ผลิตทั้งก้อนโดยไม่มีรูลบุ๊ก | "สร้างเมือง 30 แห่งให้" → หมู่บ้านคล้ายกัน 30 แห่ง | ข้ามรูลบุ๊กขั้น 1 ไม่ได้ (§6.2.3) |
| พึ่งแต่การตรวจสอบของนักเขียนโดยไม่มี lint | ผู้ตรวจสอบเสียเวลาไปกับการจัดการการละเมิดกฎเล็ก ๆ น้อย ๆ | ตรวจสอบอัตโนมัติขั้นแรกก่อน (§6.2.5) |
| ตกหล่นการตรวจชื่อซ้ำ | ความซ้ำของชื่อ 1,500 คนเป็นไปไม่ได้ด้วยหัวคน | แนบ forbidden_names อัตโนมัติ (§6.2.2) |
| ไม่วัดความพึงพอใจของนักเขียน | ปริมาณงานเพิ่มขึ้น แต่ถ้าแย่งเวลาของนักเขียนไปก็จะถูกปฏิเสธ | รับประกันเวลาเนื้อหาหลักอย่างชัดเจน (§6.2.7) |
อย่างที่ห้าพลาดบ่อยที่สุด การที่นักเขียนจะตัดสินใจอย่างที่ทิ้งเกรมของ silvermark ได้อย่างมีความสุข นักเขียนต้องเหลือเวลาไว้ปั้นเองนอกเหนือจากการตรวจสอบงานผลิตจำนวนมาก ถ้าวัดแต่ปริมาณงานแล้วตัดการวัดเวลาของนักเขียนออก เครื่องมือก็จะสำเร็จในแง่ KPI แต่คนจะจากไป
ถ้าทำคนเดียวก็แค่เท่านี้: ไม่ต้องมีโค้ดรูลบุ๊กก็ได้ เลือกเมือง·พื้นที่ 1 แห่งจากเกมของคุณ (หรือเกมที่คุณชอบ) แล้วเขียนเมตาดาตาในรูปแบบ §6.2.2 ด้วยมือ (lore_seeds 3 อันคือหัวใจ) คัดลอกพรอมต์ข้อความแนะนำของ §6.2.4 ไปวางตามนั้นแล้วลองหมุนหนึ่งครั้ง จาก NPC ที่ออกมา ลองเลือกคนหนึ่งที่โทนไม่เข้าด้วยตัวเองแล้วโต้แย้งว่า "NPC คนนี้ขัดกับโทนของเมือง ทิ้งแล้วทำใหม่" คุณจะรู้สึกได้ด้วยตัวเองว่าด่านตรวจสอบคือกลุ่มของการตัดสินใจแบบไหน
ถ้าเป็นทีม ขอให้เริ่มด้วยหนึ่งขั้นต่อไปนี้ สร้างแบบฟอร์ม yaml เมตาดาตาหนึ่งหน้าและสคริปต์แนบ forbidden_names อัตโนมัติก่อน โครงรูลบุ๊ก (generate_skeleton) และ lint เป็นลำดับถัดไป แค่มีแบบฟอร์มอินพุตและการตรวจชื่อซ้ำสองอย่าง ก็ป้องกันความล้มเหลวที่พบบ่อยสองอย่างซึ่งทำให้การผลิตเนื้อหาด้วย AI พังลงเป็น "หมู่บ้านคล้ายกัน 30 แห่ง" ได้ก่อนแล้ว
ใน 6.3 จะกล่าวถึงไปป์ไลน์ NPC Persona/Squad ถ้า generator ของ 6.2 ผลิต NPC อย่างโดเรน·มิราเป็นรายตัว Persona/Squad จะมัด NPC เหล่านั้นเข้าเป็นกลุ่ม เป็นวิธีทำให้ NPC ห้าคนของพื้นที่ล่าหนึ่งจุดทำงานเป็นสังคมเล็ก ๆ ไม่ใช่กองตุ๊กตาที่ไม่เกี่ยวข้องกัน
ผู้อ่านหลัก: นักออกแบบเกม MMORPG ที่รับผิดชอบเนื้อหา NPC และจุดล่ามอนสเตอร์ (ทีมขนาดกลาง 10–50 คน) เวอร์ชันย่อสำหรับผู้อ่านคนเดียว/งานอดิเรก: §6.3.10 「ถ้าทำคนเดียว แค่นี้พอ」
ผู้เขียนมีความทรงจำของวันที่ใช้ generator ในหัวข้อ 6.2 ปั๊ม NPC ห้าตัวลงในจุดล่ามอนสเตอร์แห่งหนึ่งแล้วลองเปิดเข้าไปในเกมจริง ชื่อ รูปร่างหน้าตา และภูมิหลังสั้น ๆ ถูกเติมครบหมดแล้ว เหลือแค่กดพิกัดวางตำแหน่ง แต่พอลองเดินไปในจุดล่ามอนสเตอร์นั้นจริง ๆ มันกลับให้ความรู้สึกตายแปลก ๆ ห้าคนอยู่ในพื้นที่เดียวกันแต่ไม่เคยเอ่ยถึงกันสักครั้ง สองคนยืนซ้อนกันอยู่บนหินก้อนเดียวกัน ตรงนั้นต้องการคนที่รับบทพ่อค้า แต่ทั้งห้าคนกลับเป็นนักวิชาการหมด ทั้งโดเรนและมิราต่างก็เป็น NPC ที่ดูปกติดีเมื่อแยกเป็นรายตัว แต่พอเอามามัดรวมกัน มันก็กลายเป็นแค่กองตุ๊กตา
นี่คือสภาพ "พิพิธภัณฑ์ตุ๊กตา" NPC แต่ละตัวสร้างเสร็จหมดแล้ว แต่ไม่ได้มีชีวิตในฐานะกลุ่ม บทนี้ว่าด้วยไปป์ไลน์ที่มัดห้าคนนั้นให้กลายเป็นสังคมเล็ก ๆ การแยกแกนหลักคือ Persona และ Squad ถ้าเปรียบเป็นออฟฟิศ Persona คือนามบัตรของพนักงานแต่ละคน ส่วน Squad คือผังองค์กรของทีมหนึ่ง ต่อให้กองนามบัตรไว้ 50 ใบ แต่ถ้าไม่มีผังองค์กร บริษัทก็เดินไม่ได้ และกระดูกสันหลังของบทนี้คือขั้นตอนสุดท้าย นั่นคือจุดที่เราตรวจสอบร่วมกับ AI จนจบหนึ่งรอบว่ากลุ่มที่มัดรวมกันแล้ว "พูดและขยับเหมือนคนที่รู้จักกัน" หรือไม่
บันทึกการใช้งานจริงของผู้เขียน ไปป์ไลน์ Squad ในบทนี้เป็นการนำเครื่องมือ NPC Persona/Squad ที่ผู้เขียนใช้งานอยู่ในโฟลเดอร์ R&D ของบริษัทมาทำให้ไม่ระบุตัวตน โครงสร้าง yaml รายการตรวจสอบ และค่าขีดแบ่ง voice_lint ถ่ายทอดมาจากเครื่องมือจริงอย่างซื่อตรง ส่วนชื่อเมืองและชื่อ NPC เปลี่ยนให้เป็นเวอร์ชันสำหรับหนังสือเหมือนกับในหัวข้อ 6.2 เนื้อหาผลลัพธ์เป็นการเรียบเรียงใหม่จากเซสชันจริง
Persona คืออัตลักษณ์ของ NPC แต่ละตัว บรรจุชื่อ รูปร่างหน้าตา voice_profile และบทบาท สิ่งที่ generator ในหัวข้อ 6.2 สร้างขึ้นก็คือ Persona โดเรน เวล และมิรา คอสต์ ต่างเป็น Persona หนึ่งตัว
Squad คือหน่วยที่มัด Persona เหล่านั้นเข้าเป็นกลุ่ม โดยนิยามว่าในจุดล่ามอนสเตอร์แห่งหนึ่ง ห้าคนกระจายตัวด้วยบทบาทอะไร มีความสัมพันธ์กันอย่างไร และเคลื่อนไหวอย่างไร
| หน่วย | สิ่งที่บรรจุ | ผู้สร้าง |
|---|---|---|
| Persona | ชื่อ·รูปร่างหน้าตา·voice_profile·บทบาท | generator (6.2) |
| Squad | การกระจายบทบาท·ความสัมพันธ์·เส้นทางเดิน | ไปป์ไลน์ Squad (บทนี้) |
ถ้าไม่แยกสองสิ่งนี้ออกจากกัน จะมีสองอย่างที่ตันพร้อมกัน ถ้าปั๊มแต่ Persona ก็จะกลายเป็นพิพิธภัณฑ์ตุ๊กตา แต่ถ้าพยายามเริ่มจาก Squad ก็จะไม่มี Persona ให้เติม เมื่อแยกออกจากกันแล้ว การดำเนินงานของแต่ละหน่วยจะเรียบง่ายขึ้น แต่การแยกไม่ได้หมายถึงการตัดขาด หัวใจคือการวางเส้นทางสำหรับการนำกลับมาใช้ซ้ำและการตรวจสอบไว้ระหว่างสองหน่วย และนี่คือเนื้อหาหลักของบทนี้
การแยกแกน Persona→Squad นี้ไม่ใช่แค่การจัดระเบียบ แต่เปิดทางไปได้ไกลกว่านั้น กลุ่ม NPC ต้องถูกทำให้เป็นรูปแบบมาตรฐานด้วยบทบาท ความสัมพันธ์ และค่าตัวเลขเสียก่อน ภายหลังสถานะของโลก (การสะสมพฤติกรรมของผู้เล่น) จึงจะสามารถสั่นค่าตัวเลขของ NPC ได้ และค่าตัวเลขนั้นก็กลายเป็นเงื่อนไขการเกิดเควสต์ ไปจนถึงการตอบสนองแบบพลวัตได้ บทนี้แตะแค่ทางเข้าของการประยุกต์เชิงก้าวหน้านั้น และในแนวทางหลักจะกล่าวถึงเพียง "การปั๊มแบบอนุรักษ์นิยมที่มีคนตรวจสอบ" เท่านั้น
โครงร่าง Squad เริ่มต้นจากเมตาดาตาหนึ่งหน้าต่อจุดล่ามอนสเตอร์หนึ่งจุด เป็นแนวคิดเดียวกับเมตาดาตาเมืองในหัวข้อ 6.2 คนจับเฉพาะการกระจายบทบาทและเจตนาด้านความสัมพันธ์เท่านั้น ส่วนงานเติมเนื้อหาให้ rulebook และ AI ทำ
# city_021_hg_3.squad.yaml
squad_id: city_021_hg_3_squad
hunting_ground: city_021_silvermark_hg_3
type: hunting_ground_residents
size: 5
roles:
- role: quest_giver
count: 1
voice_traits: [authoritative, scholarly]
- role: lore_keeper
count: 1
voice_traits: [scholarly, withdrawn]
- role: merchant
count: 1
voice_traits: [practical, dry]
- role: bystander
count: 2
voice_traits: [varied]
relationships:
- between: [quest_giver, lore_keeper]
type: mentor_and_former_student
- between: [merchant, bystander_1]
type: regular_customer
movement_pattern: stationary_with_shifts
สล็อตที่สำคัญที่สุดคือ relationships ถ้าความสัมพันธ์เป็น 0 รายการ ห้าคนก็จะเป็นคนแปลกหน้าต่อกันจนจบ แต่ถ้าความสัมพันธ์มากเกินไป (ห้าคนมีตั้งแต่ห้ารายการขึ้นไป) ผู้ใช้จะต้องจำเยอะจนกลับกลายเป็นว่ามันจมหายไป จากประสบการณ์ของผู้เขียน Squad ห้าคนที่มีความสัมพันธ์หลัก 2–3 รายการเป็นช่วงที่เสถียรที่สุด ส่วน voice_traits คือกลไกที่จับให้ห้าคนมีน้ำเสียงต่างกัน ถ้าเติม scholarly ให้ทั้งห้าคน ก็จะติดที่ขั้นตอนตรวจสอบเพราะ voice ถูกปรับให้เท่ากันหมด
rulebook จับมาตรฐานของโครงร่าง Squad ก่อนเป็นอันดับแรก ค่าเริ่มต้นของขนาด การกระจายบทบาท ความหนาแน่นของความสัมพันธ์ และรูปแบบเส้นทางเดิน ตามแต่ละ region·type ของจุดล่ามอนสเตอร์ถูกป้อนไว้ในโค้ดแล้ว
# npc_squad/templates.py (발췌)
SQUAD_TEMPLATES = {
("west", "hunting_ground_residents"): {
"size_range": (4, 6),
"role_distribution": {
"quest_giver": 1,
"merchant": 1,
"lore_keeper": (0, 1),
"bystander": (1, 3),
},
"relationship_density": 2, # จำนวนความสัมพันธ์ที่แนะนำ
"movement_pattern": "stationary_with_shifts",
},
("east", "outpost_squad"): {
"size_range": (3, 4),
"role_distribution": {
"commander": 1,
"scout": 1,
"support": (1, 2),
},
"relationship_density": 1,
"movement_pattern": "patrol_loop",
},
}
ขั้นตอนนี้เป็นเชิงกำหนด (deterministic) Squad ผู้อยู่อาศัยฝั่งตะวันตกที่มี quest_giver ครบทั้งห้าคนเป็นเรื่องที่เป็นไปไม่ได้ในระดับโค้ด ถ้าการกระจายบทบาทออกนอก rule ก็จะถูกหยุดทันทีตรงนั้น
ถัดมาคือการเติม Persona ลงในแต่ละสล็อต มีอยู่สามทาง ถ้ามี Persona ในพูลที่เข้ากับสล็อต ก็นำกลับมาใช้ซ้ำ (น้ำหนักการปรากฏตัว +1) ถ้าไม่มีก็สร้างใหม่ด้วย generator ในหัวข้อ 6.2 และถ้าเป็นตัวละครหลักของเควสต์หลัก นักเขียนก็เขียนเองโดยตรง Squad hg_3 ของ silvermark เติม quest_giver·lore_keeper ด้วยมิราและโดเรนที่ปั๊มไว้แล้วในหัวข้อ 6.2 แล้วดึง merchant กับ bystander อีกสองคนมาใหม่ ถึงตรงนี้เหมือนกับรอบ generator ของหัวข้อ 6.2 งานจริง ๆ ของบทนี้คือสิ่งที่ตามมา นั่นคือจุดที่ตรวจสอบว่ากลุ่มที่มัดรวมกันแล้วทำงานเหมือนกลุ่มจริง ๆ หรือไม่
ถ้าเขียนเพียงนามธรรมว่า "AI เสริมความสัมพันธ์" ก็จะไม่รู้ว่าไปป์ไลน์นี้คายอะไรออกมา เราจะตามรอบครึ่งหลังของ Squad silvermark hg_3 หนึ่งหน่วยให้จบสักครั้ง ตั้งแต่การสร้างข้อความความสัมพันธ์ไปจนถึงการทิ้งและการขอใหม่
แท็กความสัมพันธ์ที่ป้อนไว้ในโครงร่าง Squad (mentor_and_former_student) เป็นนามธรรม จึงมองไม่เห็นในเกม การเปลี่ยนสิ่งนี้ให้เป็นคำบรรยายหนึ่งบรรทัดแล้วฝังลงในบทพูดของ NPC และอีเวนต์คือขั้น 3 พรอมต์อยู่ในรูปที่สามารถคัดลอกไปใช้ได้ทันที
[L0 บริบท] world_premise + tone_manifesto (แคชชิง)
[L1 บริบท] city_021_silvermark.lore (สมาคมนักวิชาการครอบครอง, scholarly_strict)
[Persona 1] quest_giver — มิรา คอสต์, บรรณารักษ์คลังเอกสารสมาคม, วัย 30 ปลาย ๆ, รอยหมึกเปื้อน
[Persona 2] lore_keeper — โดเรน เวล, ผู้ช่วยสังเกตการณ์หอระฆัง, วัย 50, สนทนาด้วยตัวเลขเท่านั้น
[แท็กความสัมพันธ์] mentor_and_former_student
ช่วยบรรยายความสัมพันธ์ของสองคนนี้ (อาจารย์–อดีตลูกศิษย์) เป็นภูมิหลังสำหรับใช้ในบทพูดในเกม แค่ 1–2 บรรทัด
โดเรนคือตัวเลข มิราคือเอกสาร — ให้สำนวนการพูดของทั้งสองไม่ปะทะกัน โทนเป็นแบบนักวิชาการเคร่งครัด
ตัดถ้อยคำซ้ำซากอย่างความลึกลับหรือ "เพื่อนเก่า" ออก เอาเฉพาะเนื้อความ
[ขั้น 3 ผลลัพธ์ AI — ความสัมพันธ์หนึ่งบรรทัด] (เรียบเรียงใหม่จากเซสชันจริง)
โดเรนสอนวิธีเขียนสัญลักษณ์ของบันทึกการสังเกตการณ์แท่นผนึกให้มิราเมื่อ 20 ปีก่อน ทุกวันนี้สถานะกลับด้าน โดเรนเป็นคนวัดค่าตัวเลข แล้วมิราเป็นคนคัดลงสมุดบัญชีของคลังเอกสาร ทั้งสองจะถกเถียงกันสั้น ๆ ทุกวันอังคารเรื่องช่องหนึ่งที่ค่าจากการสังเกตการณ์ไม่ตรงกับสมุดบัญชี
ผลลัพธ์นี้ดี mentor_and_former_student ถูกทำให้เป็นรูปธรรม "ตัวเลข" ของโดเรนกับ "เอกสาร" ของมิราถูกมัดเข้าด้วยกันในฉากเดียว (การคัดค่าตัวเลขลงสมุดบัญชี) โดยไม่ปะทะกัน และโทน scholarly_strict ก็ยังคงอยู่ พรอมต์เดียวกันนี้นำมาทำซ้ำกับความสัมพันธ์ regular_customer ของ merchant–bystander_1 ด้วย
ถ้า NPC ยืนอยู่ที่เดียวทั้งวัน มันก็กลายเป็นตุ๊กตาอีกครั้ง rulebook เป็นตัวเติมรูปแบบเส้นทางเดิน stationary คือยืนตรึงที่เดียว (ยาม·บอส) stationary_with_shifts คือเปลี่ยนตำแหน่งเล็กน้อยทุก 8 ชั่วโมง (ทั่วไป) routine_loop คืออิงตารางเวลา (ผู้อยู่อาศัย) event_driven คือเคลื่อนที่เฉพาะตอนถูกทริกเกอร์ (NPC เควสต์) ขั้นตอนนี้เป็นเชิงกำหนดจึงไม่เรียก AI
ตอนนี้เราตรวจว่าห้าคนที่มัดรวมกันแล้วทำงานเหมือนกลุ่มจริง ๆ หรือไม่ ถ้า lint ในหัวข้อ 6.2 ดู NPC รายตัว lint ตัวนี้ดูความสอดคล้องของกลุ่ม
[ขั้น 5 ผลลัพธ์ Squad lint] (รูปแบบจริง)
[PASS] การกระจายบทบาท: quest_giver 1 · lore_keeper 1 · merchant 1 · bystander 2 (ตรงตาม rule) [PASS] ความหนาแน่นความสัมพันธ์: 2 รายการ (แนะนำ 2, ตรงตาม) [WARN] ความหลากหลายของ voice: สาย scholarly 3/5 — quest_giver·lore_keeper·bystander_2 มีค่าความคล้ายเชิงโคไซน์ของ voice_profile 0.83 (เกินขีด 0.80) เสี่ยงถูกปรับให้เท่ากัน [WARN] เส้นทางเดินชนกัน: ช่วง 14:00~16:00 พิกัดของ merchant·bystander_1 ซ้อนทับในรัศมี 1.5m [FAIL] การเปิดเผยความสัมพันธ์: นิยามความสัมพันธ์ไว้ 2 รายการ แต่ในบทพูดของทั้งห้าคนไม่มีการเอ่ยถึงสมาชิกคนอื่นเลยสักครั้ง ความสัมพันธ์มีอยู่แค่ในข้อมูล — การมองเห็นในเกมเป็น 0
lint จับได้สามรายการ ทั้งสามไม่ถูกทิ้งโดยอัตโนมัติ แต่ยกขึ้นด่านตรวจสอบ — เครื่องเป็นคนคัดผู้ต้องสงสัย แต่คนเป็นคนตัดสินว่าจะฆ่าหรือไว้ชีวิต ซึ่งเป็นการออกแบบแบบเดียวกับ §6.2.5
[ขั้น 6 การตรวจสอบโดยนักเขียน — การตัดสินและการทิ้ง]
นักเขียนจัดการ alert ทั้งสามรายการดังนี้
- voice ถูกปรับให้เท่ากัน (WARN) → ทิ้ง bystander_2 ต่อให้เป็นเมืองนักวิชาการ ถ้าทั้งห้าคนพูดสำนวนนักวิชาการหมด จุดล่ามอนสเตอร์ก็จะจืดชืด จึงขอสร้าง bystander_2 ใหม่เป็นคนงานเบ็ดเตล็ดโทน
practical, dry(การที่ quest_giver·lore_keeper เป็นนักวิชาการทั้งคู่เป็นอัตลักษณ์ของเมือง จึงคงไว้)- เส้นทางเดินชนกัน (WARN) → ปรับ rule เลื่อนออฟเซ็ตเริ่มต้น shift ของ merchant ออกไป +2 ชั่วโมง เพื่อแก้การซ้อนทับตอน 14:00 ปรับเฉพาะพารามิเตอร์เส้นทางเดินโดยไม่เรียก AI
- การเปิดเผยความสัมพันธ์เป็น 0 (FAIL) → รายการที่สำคัญที่สุด นิยามความสัมพันธ์ไว้ตั้งสองรายการ แต่ถ้าในเกมไม่ปรากฏให้เห็นสักครั้ง ข้อมูลนั้นก็เป็นข้อมูลที่ตายแล้ว นักเขียนตัดสินใจเลือกความสัมพันธ์หลัก 1 รายการ (โดเรน–มิรา) แล้วฝังลงในบทพูด
สามรายการ สองรายการถูกปิดด้วย rule และการสร้างใหม่ ส่วน FAIL รายการสุดท้ายคือหัวใจของไปป์ไลน์นี้ นักเขียนขอเพิ่มกิ่งบทสนทนาของโดเรนหนึ่งบรรทัด
ช่วยแทรกบทพูดของโดเรนหนึ่งบรรทัดที่ความสัมพันธ์กับมิราปรากฏออกมาแบบหลุดลอย ๆ
อย่าเป็นเชิงอธิบาย ให้เป็นแบบกิ่งก้านข้าง ๆ โทนนักวิชาการ เอาบทพูดแค่บรรทัดเดียว
[ผลลัพธ์จากการขอใหม่]
"แบบแปลนนั่นอยู่ในคลังเอกสาร ไปถามมิราดู ...เมื่อ 20 ปีก่อนข้านี่แหละที่สอนวิธีอ่านให้คนนั้น เดี๋ยวนี้มันกลับตาลปัตรเสียแล้ว"
ขณะที่บรรทัดนี้เข้าไป ความสัมพันธ์ของ NPC สองตัวก็ย้ายจากชีตข้อมูลมาสู่หน้าจอเกม หนึ่งรอบของ อินพุต (เมตา Squad) → โครงร่าง → การเติม Persona → การเสริมความสัมพันธ์ → เส้นทางเดิน → การตรวจสอบความสอดคล้อง → การตัดสินใจทิ้ง·เปิดเผย ปิดลงที่ตรงนี้
หนึ่งรอบนี้คือเกณฑ์ Show ของบทนี้ ประโยคที่ว่า "มัด NPC ให้เป็นสังคมด้วย Squad" จะกลวงเปล่า หากไม่ได้เห็นฉากที่คนปิด FAIL การเปิดเผยความสัมพันธ์ที่เป็น 0 ด้วยบทพูดหนึ่งบรรทัดแม้สักครั้ง
ขอใส่รอบข้างต้นไว้เป็นภาพเดียว หัวใจคือ ขั้น 1·2·4·5 เป็นเชิงกำหนด (rulebook·lint) และมีเพียงขั้น 3 เท่านั้นที่เป็น AI อีกทั้งมือของคนแตะแค่ที่อินพุตบนสุดและด่านตรวจสอบล่างสุดเท่านั้น
flowchart TB
P["พูล Persona
(ผลผลิตจาก generator 6.2)
โดเรน·มิรา·..."] --> FILL
A["อินพุต: เมตาดาตา Squad
(คน 10~15 นาที/จุดล่ามอนสเตอร์)
การกระจายบทบาท · เจตนาความสัมพันธ์ 2~3 รายการ"]
A --> B["ขั้น 1 เชิงกำหนด: templates.py
มาตรฐานการกระจายบทบาท·ความหนาแน่นความสัมพันธ์·เส้นทางเดิน"]
B --> FILL["ขั้น 2: การเติม Persona
นำกลับมาใช้ซ้ำ / generator / นักเขียนเขียนเอง"]
FILL --> C["ขั้น 3 AI: เสริมความสัมพันธ์หนึ่งบรรทัด
L0 แคชชิง + ตรวจการปะทะของ voice_traits"]
C --> D["ขั้น 4 เชิงกำหนด: สังเคราะห์เส้นทางเดิน
shift·routine·event_driven"]
D --> E{"ขั้น 5 เชิงกำหนด: Squad lint
การกระจายบทบาท·ความหลากหลาย voice
·เส้นทางเดินชน·การเปิดเผยความสัมพันธ์"}
E -->|"alert"| F["ขั้น 6 ด่านตรวจสอบโดยนักเขียน
(5~10 นาที/จุดล่ามอนสเตอร์)
ตัดสินใจทิ้ง·ปรับ rule·เปิดเผยความสัมพันธ์"]
F -->|"ขอใหม่"| C
F -->|"ผ่าน"| G["นำเข้าบิลด์"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class B,D,E code;
class C ai;
class A,F human;
class P data;
class G pass;
จุดที่มือคนแตะมีแค่สองแห่ง จุดที่จับเจตนาบทบาท·ความสัมพันธ์ที่บนสุด กับจุดที่ตัดสินโทน·การเล่าเรื่องที่ lint จับไม่ได้ที่ล่างสุด ส่วนระหว่างนั้น โครงร่าง·เส้นทางเดิน·การตรวจสอบเป็นงานของ rulebook ส่วนเนื้อความความสัมพันธ์ AI เป็นตัวรัน
รายการ การเปิดเผยความสัมพันธ์ ของ lint ขั้น 5 เป็นรายการที่ FAIL บ่อยที่สุด เพราะความสัมพันธ์มีชีวิตอยู่แค่ในข้อมูล กลไกที่ดึงความสัมพันธ์ออกมาสู่ในเกมมีอยู่สามอย่าง
หนึ่ง การอ้างอิงในบทพูด เหมือนบทพูดของโดเรนใน §6.3.4 ที่ NPC เอ่ยถึงสมาชิกคนอื่นหนึ่งบรรทัด ถูกที่สุดและได้ผลมากที่สุด
สอง การตัดกันของเส้นทางเดิน ฉากที่โดเรนกับมิราอยู่ในคลังเอกสารด้วยกันทุกวันอังคารถูกสังเกตเห็นในเกม ถ้าผู้ใช้บังเอิญเห็นก็จะรู้ทันว่า "สองคนนั้นเกี่ยวพันกันหรือเปล่า" ถ้าเส้นทางเดินขั้น 4 กับความสัมพันธ์สอดคล้องกัน มันก็ออกมาเองตามธรรมชาติ
สาม เงื่อนไขกิ่ง ถ้าปฏิเสธคำขอของ quest_giver ค่าความพอใจของ lore_keeper ก็จะลดลงไปด้วย กลไกที่สามนี้คือทางเข้าของการประยุกต์เชิงก้าวหน้าที่กล่าวไว้ใน §6.3.1 — จุดที่ความสัมพันธ์ก้าวข้ามการบรรยายแบบเรียบ ๆ ไปสู่การส่งผลต่อสถานะของเกม
ไม่จำเป็นต้องใช้ทั้งสามกลไก แค่ความสัมพันธ์หลัก 2–3 รายการใน Squad ห้าคนถูกเปิดเผยด้วยกลไกที่หนึ่งและสอง ความรู้สึกของจุดล่ามอนสเตอร์ก็เปลี่ยนไปอย่างมาก ถ้ามากเกินไปผู้ใช้ก็จะต้องจำเยอะ นักเขียนเลือกความสัมพันธ์ที่จะเปิดเผยในขั้นตรวจสอบแล้วแทรกเข้าไป ที่เหลือก็ทิ้งไว้เป็นแค่ข้อมูล
เปรียบเทียบก่อนและหลังนำเครื่องมือมาใช้ ไม่ใช้ตัวเลขที่ปรุงแต่ง เวลาและสัดส่วนเป็นค่าที่นับจากการตรวจสอบจุดล่ามอนสเตอร์ช่วงแรกหลายแห่งด้วยตนเอง รวมถึง silvermark ส่วนคอลัมน์ "ก่อนนำมาใช้" เป็นการประมาณของผู้เขียนในช่วงที่ทำด้วยมือ
| รายการ | ก่อนนำมาใช้ (ทำมือ·ประมาณ) | หลังนำมาใช้ (วัดจริง) |
|---|---|---|
| มัด Squad ของจุดล่ามอนสเตอร์ 1 จุด | ประมาณ 3\~4 ชั่วโมง | ประมาณ 25 นาที (เมตา 12 นาที + AI 5 นาที + ตรวจสอบ 8 นาที) |
| จำนวนการเปิดเผยความสัมพันธ์ (บทพูด·เส้นทางเดิน) | 0\~1 รายการต่อจุดล่ามอนสเตอร์ | เปิดเผย 1\~2 รายการจากความสัมพันธ์หลัก 2\~3 |
| เส้นทางเดินชน (2 คนขึ้นไปพิกัดเดียวกัน) | 2\~3 รายการต่อจุดล่ามอนสเตอร์ | กัน lint ไว้ล่วงหน้า, 0\~1 รายการ |
| ทิ้งเพราะ voice ถูกปรับให้เท่ากัน | — (ไม่มีการตรวจ) | สร้างใหม่ 0\~1 คนจาก 5 คน |
เนื่องจากตัวอย่างมีเพียงจุดล่ามอนสเตอร์ไม่กี่แห่งซึ่งน้อย จึงควรอ่านเป็นค่าทิศทางมากกว่าสัดส่วนประชากรที่แม่นยำ การเปลี่ยนแปลงที่ใหญ่ที่สุดไม่อยู่ในตาราง เพราะ FAIL การเปิดเผยความสัมพันธ์ ของ lint บังคับยื่นใส่หน้านักเขียนว่า "จุดล่ามอนสเตอร์นี้ ไม่เห็นความสัมพันธ์เลยสักอย่าง" การที่ผลผลิตปั๊มออกวางขายในสภาพพิพิธภัณฑ์ตุ๊กตาจึงลดลงในเชิงโครงสร้าง ประเด็นที่ว่าการทิ้ง 0%·การเปิดเผย 0 รายการไม่ใช่เป้าหมาย (§6.2.6) ก็เป็นเช่นเดียวกันที่นี่ Squad ดูดซับงานมัดส่วนใหญ่ไป แต่ต้องเหลือเวลาให้นักเขียนได้ปั้นหัวใจอย่างบทพูดบรรทัดสุดท้ายของโดเรนด้วยตัวเองด้วย
เมื่อ Squad เสถียรแล้ว สิ่งที่ตามมาเองตามธรรมชาติคือพูล Persona Persona เดียวกันสามารถปรากฏในหลายเมืองได้ การที่ NPC สังกัดสมาคมนักวิชาการถูกพบเจอในเมืองสามสี่แห่งกลับเป็นเรื่องธรรมชาติ ไม่ใช่ทำให้โลกดูแคบ แต่ทำให้ดูเชื่อมโยงกัน
persona_pool:
- id: persona_doren_vale
voice_traits: [terse, numeric]
appearance_count: 3
appearance_cities: [city_021, city_018, city_023]
signature: false
- id: persona_mira_kost
voice_traits: [scholarly, withdrawn]
appearance_count: 2
signature: false
สัดส่วนการนำกลับมาใช้ซ้ำมีช่วงที่ดีต่อสุขภาพอยู่
| สัดส่วนการนำกลับมาใช้ซ้ำ | สภาพ |
|---|---|
| ต่ำกว่า 20% | ปริมาณ NPC พุ่ง, ภาระในการจดจำ |
| 30\~50% | ช่วงการดำเนินงานที่ดีต่อสุขภาพ |
| 70% ขึ้นไป | NPC น่าเบื่อ, ความหลากหลายเสียหาย |
แต่ถ้าให้ NPC ตัวหนึ่งปรากฏในเมืองมากเกินไป ก็จะกลายเป็น "คนนี้โผล่มาอีกแล้ว" เมืองที่ Persona หนึ่งตัวปรากฏได้มากที่สุดกำหนดเพดานไว้ที่ 5 แห่ง ห้องบอส·ตัวละครซิกเนเจอร์ห้ามนำกลับมาใช้ซ้ำ (signature: true) ตั้งแต่การปรากฏครั้งที่สองของ Persona เดียวกัน บังคับให้มีการแปรผันทางสายตา (แสง·พร็อพ) ช่วง 30\~50% นี้ไม่ใช่ค่าตัวเลขที่แม่นยำ แต่เป็นแนวทางการดำเนินงาน — ต้องปรับตามขนาดทีม·เกม
[ป้ายชี้ทิศ — ถ้ามองพูลเปอร์โซนาเป็นการกระจายตัว (ตอนนี้ยังเร็วเกินไป)] ถ้าเป็นทีมที่พูลใหญ่ขึ้นถึงหลายร้อย NPC ก็มีทิศทางที่ก้าวไปอีกก้าว — เป็นป้ายชี้ทิศที่อยู่ตำแหน่งเดียวกับหัวข้อ 'เวกเตอร์เชิงมิติ' ใน §8.2.7 (ไม่ใช่ใบสั่งยา — สัญชาตญาณเชิงแนวคิดอยู่ในภาคผนวก M) voice_lint ใน §6.3.4 ดู 'ความใกล้' ของ Persona สองตัวด้วยค่าความคล้ายเชิงโคไซน์ (ค่าอย่าง 0.83) อยู่แล้ว ถ้านำ embedding เดียวกันนี้ขึ้นทั้งพูล ก็จะวินิจฉัยความน่าเบื่อได้ด้วยความหนาแน่นของการกระจายแทนความรู้สึกของนักเขียน — สภาพที่ 'สำนวนนักวิชาการกระจุกอยู่มุมเดียวกันเป็นกอง' จะเห็นเป็นความหนาแน่นของจุดในบริเวณนั้น เมื่อนั้นแทนที่จะปั๊ม 'นักวิชาการคล้าย ๆ กันอีกตัว' ลงในบริเวณความหนาแน่นต่ำ ก็จะเปิดทางให้เติมการแปรผันที่ประมาณค่าระหว่าง Persona สองตัวที่ใกล้กันเข้ามาเพื่อเติมความหลากหลาย แต่มีจุดที่ต้องระวังสองข้อในลมหายใจเดียวกัน Persona ที่ได้จากการประมาณค่ามักกลายเป็น 'ค่ากลางที่ตายแล้ว' ซึ่งเอา NPC สองตัวมาผสมกันอย่างขัดเขิน สุดท้ายคนต้องมาชุบชีวิต voice ขึ้นใหม่อยู่ดี และสัดส่วนความน่าเบื่อข้างต้น (30\~50%) ไม่ใช่ค่าที่แม่นยำ แต่เป็นแนวทางการดำเนินงาน พอแปลงมันเป็นระยะ embedding เมื่อใด ความหลวมก็มีความเสี่ยงที่จะแอบแฝงตัวให้ดูเหมือนแม่นยำ — ค่าระยะทางเป็นสัญญาณที่ช่วยการตัดสินของนักเขียน ไม่ใช่ตัวการตัดสินเอง
| รูปแบบความล้มเหลว | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| ปั๊มแต่ Persona แล้วเมิน Squad | มี NPC ครบ 50 ตัวแต่จุดล่ามอนสเตอร์ตาย | นำ rulebook โครงร่าง Squad มาใช้ (§6.3.3) |
| สร้างอิสระโดยไม่มี rule การกระจายบทบาท | การกระจายผิดเพี้ยนอย่างพ่อค้าห้าคน นักวิชาการ 0 คน | บังคับ role_distribution (§6.3.3) |
| มีแต่แท็กความสัมพันธ์ ขาดคำบรรยายหนึ่งบรรทัด | ความสัมพันธ์เป็นนามธรรมจึงมองไม่เห็นในเกม | ขั้น 3 AI เสริมความสัมพันธ์ (§6.3.4) |
| ไม่มีการตรวจการเปิดเผยความสัมพันธ์ | นิยามความสัมพันธ์ไว้แต่เปิดเผยในบทพูด·เส้นทางเดิน 0 รายการ | lint การเปิดเผยความสัมพันธ์ ขั้น 5 (§6.3.4) |
| ขาดการตรวจเส้นทางเดินชน | 2 คนอยู่ที่เดียวกันในเวลาเดียวกัน พบบ่อยหลังออกเกม | ตรวจพิกัด·ช่วงเวลาอัตโนมัติ (§6.3.4) |
| สัดส่วนการนำกลับมาใช้ซ้ำ 0% หรือ 70%+ | 0% คือปริมาณพุ่ง, 70%+ คือน่าเบื่อ | ดำเนินงานพูล + เพดานการปรากฏ (§6.3.8) |
แบบที่สี่ถูกมองข้ามบ่อยที่สุด การนิยามความสัมพันธ์กับการทำให้ความสัมพันธ์มองเห็นในเกมเป็นคนละงาน และถ้าไม่ตรวจ อย่างหลังก็มักจะหายไปแทบทุกครั้ง ถ้า lint ไม่ได้ยื่นการเปิดเผยความสัมพันธ์ 0 รายการเป็น FAIL ใส่ใน silvermark hg_3 โดเรนกับมิราก็คงเป็นอาจารย์–ลูกศิษย์กันแค่ในชีตข้อมูลเท่านั้น
ถ้าทำคนเดียว แค่นี้พอ: ไม่ต้องมี rulebook หรือ lint ก็ได้ ลองเลือก NPC 3–5 ตัวที่อยู่ในสถานที่หนึ่งของเกมตัวเอง (หรือเกมที่คุณชอบ) แล้วเขียนบทบาทและความสัมพันธ์ 2 รายการลงมือด้วยตัวเองตามรูปแบบใน §6.3.2 จากนั้นแปะพรอมต์เสริมความสัมพันธ์ใน §6.3.4 ลงไปตรง ๆ เพื่อรับคำบรรยายหนึ่งบรรทัด และสุดท้ายลองถามตัวเองดู — "ความสัมพันธ์นี้ตอนนี้เห็นได้ที่บทพูดตรงไหนของเกม" ถ้าไม่มีสักที่ นั่นแหละคือ FAIL
การเปิดเผยความสัมพันธ์ 0ของ lint ถ้าลองแทรกสมาชิกคนอื่นลงในบทพูดของ NPC ตัวหนึ่งหนึ่งบรรทัดเพื่อปิด FAIL นั้นด้วยมือ คุณจะเข้าใจถึงเนื้อตัวว่าการตรวจสอบ Squad เป็นงานที่จับอะไร
ถ้าเป็นทีม ให้เริ่มจากหนึ่งขั้นต่อไปนี้ สร้างฟอร์ม yaml เมตาดาตา Squad หนึ่งหน้า และเริ่มจาก การเปิดเผยความสัมพันธ์ ของ lint ขั้น 5 หนึ่งบรรทัด ก่อน (grep ว่าในข้อความบทพูดของ NPC แต่ละตัวมีชื่อ·บทบาทของสมาชิกคนอื่นปรากฏหรือไม่) การตรวจการกระจายบทบาท·การตรวจเส้นทางเดินชนเป็นเรื่องถัดไป แค่มีการตรวจการเปิดเผยความสัมพันธ์เพียงอย่างเดียว ก็กันความล้มเหลวที่พบบ่อยที่สุด คือการที่จุดล่ามอนสเตอร์ที่ปั๊มออกมาวางขายในสภาพพิพิธภัณฑ์ตุ๊กตาได้ก่อนแล้ว
ถ้าสรุปเป็น setup → prompt → verify — setup: นิยามบทบาท·ความสัมพันธ์ลงใน yaml เมตา Squad แล้วจับโครงร่างด้วย templates.py prompt: รับความสัมพันธ์หนึ่งบรรทัดตามรูปแบบ §6.3.4 โดยบังคับห้าม voice_traits ปะทะกันและห้ามถ้อยคำซ้ำซาก verify: รัน lint ขั้น 5 เพื่อยืนยัน FAIL การเปิดเผยความสัมพันธ์ แล้วฝังความสัมพันธ์หลัก 1 รายการลงในบทพูดเพื่อปิดด้วยตัวเอง
การเปิดเผยความสัมพันธ์ 0 ด้วยบทพูดหนึ่งบรรทัด คือหัวใจของไปป์ไลน์นี้ในสัปดาห์ที่ผมทำเครื่องมือทั้งสามตัวเสร็จ ผมนั่งลงในที่เดียวแล้วรัน generator เมือง รัน generator NPC และรัน generator ไอเทม ทั้งสามตัวทำงานได้ดีในแบบของมันเอง เมืองออกมา 7 แห่ง NPC ออกมา 110 ตัว และอาวุธออกมา 60 ชิ้น แต่อีกไม่กี่วันต่อมา ตอนนั่งตรวจสอบเนื้อหา ผมจึงรู้ว่าตัวเองเหยียบกับดักเดิมไปถึงสามครั้ง
เมือง port_harman ถูกสร้างขึ้นเป็น "หมู่บ้านประมงที่ล่มสลาย" แต่เพอร์โซนาของ NPC ที่ถูกวางไว้ในเมืองนั้นกลับเป็น "พ่อค้าผู้มั่งคั่งแห่งเมืองท่าการค้าที่รุ่งเรือง" เป็นเพราะ generator เมืองกับ generator NPC มอง lore_seeds คนละชุดกัน ส่วน generator อาวุธก็เอาอาวุธในตำนานระดับเลเวล 40 ไปวางขายในร้านค้า ทั้งที่เลเวลแนะนำของเมืองนั้นคือ 12–18 เครื่องมือทั้งสามตัวต่างทำถูกในแบบของตัวเอง แต่กลับผิดเพราะไม่ได้ถูกร้อยเข้าด้วยกัน
บทนี้ไม่ใช่เรื่องของการสร้างเครื่องมือตัวเดียว แต่เป็นเรื่องของการ ร้อย generator เมืองในหัวข้อ 6.2, NPC Squad ในหัวข้อ 6.3 และ generator ไอเทม เข้าเป็นไลน์การผลิตเดียว เมื่อมีเครื่องมือสามตัว กับดักก็ไม่ได้มีแค่สาม แต่จะเกิดใหม่ขึ้นในรอยต่อระหว่างเครื่องมือด้วย
ถ้ารัน generator เมือง NPC และไอเทมแยกกันคนละรอบ ผลลัพธ์ของเครื่องมือแต่ละตัวจะไม่ตรงกับอินพุตของเครื่องมือตัวอื่น ทางแก้ไม่ใช่การทำให้เครื่องมือฉลาดขึ้น แต่คือ การตรึงเมตาดาตาที่ใช้ร่วมกันไว้ที่ต้นน้ำเพียงครั้งเดียว แล้วจัดเรียงเครื่องมือทั้งหลายให้เข้าแถวอยู่ใต้มัน หาก generator เมืองในหัวข้อ 6.2 บทที่ 2 คือต้นแบบของเครื่องมือเดี่ยว บทนี้ก็คืองานที่ลดสถานะเครื่องมือตัวนั้นลงมาเป็นเพียงสถานีหนึ่งในไลน์
ทั้งไลน์ไหลไปเช่นนี้
flowchart TD
SEED[จันทร์: ชีต lore_seeds
region·climate·faction·level_range]:::human
SEED --> CITY[อังคาร: generator เมือง
โครง rulebook L1 + เนื้อหา L2 โดย AI]
CITY -->|city_manifest.json| NPC[อังคาร: generator NPC
สืบทอด lore ของเมือง]
CITY -->|level_range| ITEM[อังคาร: generator ไอเทม
สืบทอดช่วงเลเวล·ฝ่าย]
NPC -->|persona_pool| SQUAD[อังคาร: การวาง Squad]
ITEM --> SQUAD
SQUAD --> LINT[พุธ: lint รวม
ความสอดคล้องข้าม generator]
LINT -->|alert| REVIEW1[พุธ: ตรวจสอบรอบแรก
ตัวผู้เขียนเอง]
REVIEW1 --> REVIEW2[พฤหัส: สุ่มตรวจรอบสอง
ลีดฝ่ายเนื้อเรื่อง]
REVIEW2 --> BUILD[ศุกร์: นำเข้าบิลด์
+ เตรียม seed สำหรับสัปดาห์ถัดไป]
BUILD -.ป้อนกลับ.-> SEED
classDef human fill:#fde68a,stroke:#b45309,color:#000;
หัวใจอยู่ที่ลูกศรชื่อ city_manifest.json ขณะที่ generator เมืองสร้างเมือง มันจะหย่อนอัตลักษณ์ของเมืองนั้น (ว่าเป็นหมู่บ้านประมงที่ล่มสลายหรือเมืองท่าการค้าที่รุ่งเรือง) ออกมาเป็น manifest แล้ว generator NPC กับ generator ไอเทมก็ รับ manifest นั้นเป็นอินพุต กับดักที่ผมเหยียบตอนนั้นก็เพราะไม่มีลูกศรเส้นนี้ การร้อยเครื่องมือเข้าด้วยกันก็คือการสอดสัญญาหนึ่งบรรทัดนี้เข้าไประหว่างเครื่องมือ
ทุกครั้งที่ generator เมืองสร้างเมืองขึ้นมาหนึ่งแห่ง มันจะคายไฟล์ city_manifest.json ออกมาด้วย รูปแบบจริงของไฟล์นี้เป็นเช่นนี้ และไฟล์นี้เองที่กลายเป็นอินพุตของ generator NPC และไอเทม
{
"city_id": "port_harman",
"display_name": "ท่าฮาร์มาน",
"lore_seeds": ["หมู่บ้านประมงที่ล่มสลาย", "เสียงสะท้อนของการค้าครั้งเก่า", "ขาดแคลนเกลือ"],
"region": "ชายฝั่งตอนใต้",
"dominant_faction": "กิลด์ชาวประมง",
"level_range": [12, 18],
"tone": "เสื่อมถอย·ดื้อรั้น",
"forbidden_names": ["ฮารัน", "ฮาร์เมน"],
"neighbors": ["salt_marsh", "old_pier"]
}
generator NPC สืบทอด lore_seeds และ tone มาสร้าง "ผู้คนดื้อรั้นแห่งหมู่บ้านประมงที่ล่มสลาย" generator ไอเทมสืบทอด level_range มาแล้ววางขายเฉพาะอาวุธเลเวล 12–18 ส่วน forbidden_names คือชื่อที่เมืองข้างเคียงใช้ไปแล้ว ทั้งสองฝ่ายจึงหลีกเลี่ยง เครื่องมือทั้งสามตัวมองสัญญาฉบับเดียวกัน
พรอมต์ที่ผมป้อนให้ Claude ตอนสร้าง manifest นี้เป็นดังนี้ เนื่องจากเป็นการเรียกครั้งเดียวที่สำคัญที่สุดในการร้อยต้นน้ำของไลน์การผลิต ผมจึงคัดลอกมาเต็มทั้งฉบับ
เธอคือเครื่องเขียน manifest ของ generator เมือง MMORPG จงรับเมตาดาตาของผู้เขียนด้านล่างแล้วสร้าง city_manifest.json
อินพุตจากผู้เขียน: - city_id: port_harman - lore_seeds: หมู่บ้านประมงที่ล่มสลาย, เสียงสะท้อนของการค้าครั้งเก่า, ขาดแคลนเกลือ - region: ชายฝั่งตอนใต้ - level_range: 12-18
กฎ: 1. ให้อนุมาน dominant_faction จาก lore_seeds แต่ต้องไม่ซ้ำกับฝ่ายของเมืองข้างเคียง (salt_marsh, old_pier) ฝ่ายของเมืองข้างเคียงคือ [สหกรณ์นาเกลือ, กลุ่มลักลอบค้า] 2. ใน forbidden_names ให้ใส่ชื่อ NPC/สถานที่ที่ถูกใช้ไปแล้วในเมืองข้างเคียง รายการที่ให้มา: [ฮารัน, ฮาร์เมน] 3. tone ต้องไม่เกินสองคำ และต้องไม่ขัดแย้งกับ lore_seeds 4. จงแยกแสดงค่าที่อนุมานเองกับค่าที่ผู้เขียนระบุไว้อย่างชัดเจน
ผลลัพธ์ดิบจาก Claude เป็นเช่นนี้ ผมคัดลอกมาตามจริงโดยไม่สรุปย่อ มีอยู่จุดหนึ่งที่ฝ่าฝืนกฎ
{ "city_id": "port_harman", "display_name": "ท่าฮาร์มาน", "lore_seeds": ["หมู่บ้านประมงที่ล่มสลาย", "เสียงสะท้อนของการค้าครั้งเก่า", "ขาดแคลนเกลือ"], "region": "ชายฝั่งตอนใต้", "dominant_faction": "กลุ่มลักลอบค้า", // ← อนุมาน: ขาดแคลนเกลือ + การค้าครั้งเก่า = การลักลอบค้า "level_range": [12, 18], "tone": "เสื่อมถอย·โลภ", "forbidden_names": ["ฮารัน", "ฮาร์เมน"], "neighbors": ["salt_marsh", "old_pier"] }(ค่าที่อนุมาน: dominant_faction, tone, display_name / ค่าที่ผู้เขียนระบุ: ที่เหลือ) ข้อควรระวัง: ผมเลือก dominant_faction เป็น "กลุ่มลักลอบค้า" แต่ตามกฎข้อ 1 เมืองข้างเคียง old_pier เป็น "กลุ่มลักลอบค้า" อยู่แล้ว จึงเป็นการฝ่าฝืนกฎ ขอเสนอ "กิลด์ชาวประมง" เป็นตัวเลือกทดแทน
สิ่งสำคัญตรงนี้คือ AI รายงานการฝ่าฝืนกฎด้วยตัวเอง "กลุ่มลักลอบค้า" เป็นการอนุมานที่น่าสนใจเมื่อมองจาก lore_seeds (ขาดแคลนเกลือ·การค้าครั้งเก่า) แต่ ในระดับของไลน์การผลิตแล้วมันชนกัน เพราะเมืองข้างเคียงเป็นกลุ่มลักลอบค้าอยู่ก่อนแล้ว ผมจึงรับข้อเสนอของ AI แล้วแก้ dominant_faction เป็น "กิลด์ชาวประมง" และแก้ tone เป็น "เสื่อมถอย·ดื้อรั้น" คำว่า "โลภ" เป็นคำที่ออกมาจากสมมติฐานกลุ่มลักลอบค้า จึงไม่เข้ากับกิลด์ชาวประมง
การตรวจสอบ·ปฏิเสธ·กำหนดใหม่เพียงครั้งเดียวนี้คอยปกป้องต้นน้ำของไลน์ ถ้า manifest ผิด NPC ทั้ง 110 ตัวและอาวุธทั้ง 60 ชิ้นที่อยู่ใต้มันก็จะถูกสร้างขึ้นบนสมมติฐานที่ผิดทั้งหมด ใช้เวลา 5 นาทีที่ต้นน้ำ จะประหยัดเวลา 3 ชั่วโมงที่ปลายน้ำ
lint ของ generator เดี่ยวมองเฉพาะผลลัพธ์ของตัวเอง lint ของเมืองดูว่าเมืองทำตาม rulebook หรือไม่ lint ของ NPC ดูว่าเพอร์โซนารักษาความสอดคล้องของ voice ไว้หรือไม่ แต่กับดักที่ผมเหยียบตอนแรกอยู่ใน รอยต่อระหว่างเครื่องมือ ไม่ใช่ภายในเครื่องมือแต่ละตัว ดังนั้นไลน์จึงต้องการอีกหนึ่งชั้นที่อยู่เหนือ lint เดี่ยว นั่นคือ lint รวมที่อ่านข้อมูลของเมือง·NPC·ไอเทมไปพร้อมกันแล้วตรวจสอบแบบไขว้
รายการที่ lint รวมจับได้จริงเป็นเช่นนี้
| การตรวจ | เปรียบเทียบอะไร | สิ่งที่พลาดไปตอนนั้น |
|---|---|---|
| ความสอดคล้องของ lore | city.lore_seeds ↔ npc.persona | หมู่บ้านประมงแต่เป็นพ่อค้าผู้มั่งคั่ง |
| ความสอดคล้องของช่วงเลเวล | city.level_range ↔ item.required_level | อาวุธเลเวล 40 ในเมืองเลเวล 12–18 |
| การชนกันของฝ่าย | city.faction ↔ neighbor.faction | สองเมืองที่เป็นกลุ่มลักลอบค้าอยู่ติดกัน |
| ชื่อซ้ำ | พูลชื่อทั้งหมดของ city·npc·item | ไม่ได้เก็บ forbidden_names |
ส่วนหนึ่งของผลลัพธ์จริงตอนรัน lint รวมนี้เป็นดังนี้ มันไม่ได้ทิ้งผลลัพธ์อัตโนมัติ แต่เพียงแสดง alert ขึ้นมาเพื่อให้คนเป็นผู้ตัดสิน
[lint รวม] ตรวจสอบไลน์ port_harman — 3 alert ALERT-1 (ความสอดคล้องของ lore) port_harman city.lore_seeds = ["หมู่บ้านประมงที่ล่มสลาย", ...] npc[merchant_04].persona = "พ่อค้าผู้มั่งคั่งแห่งเมืองท่าการค้าที่รุ่งเรือง" → อาจขัดแย้งกัน ต้องยืนยันว่าเป็นความเบี่ยงเบนที่ตั้งใจหรือไม่ ALERT-2 (ความสอดคล้องของช่วงเลเวล) port_harman city.level_range = [12,18] item[blade_legend_07].required_level = 40 → เกินช่วงเลเวลแนะนำไป 28 ทบทวนการวางในร้านค้าใหม่ ALERT-3 (ชื่อซ้ำ) — ข้อมูล npc[fisher_02].name = "ฮารัน" city.forbidden_names = ["ฮารัน", ...] → ชนกับ forbidden_names แนะนำให้สร้างชื่อ NPC ใหม่
พอเห็น ALERT-1 ผมก็ลังเลอยู่ครู่หนึ่ง การที่ NPC เป็น "พ่อค้าผู้มั่งคั่งแห่งเมืองท่าการค้าที่รุ่งเรือง" ก็ไม่ได้ผิดเสมอไป ถ้าเป็นเมืองที่ เคยรุ่งเรืองแต่ตอนนี้ล่มสลายไปแล้ว "พ่อค้าที่เคยร่ำรวย แต่ตอนนี้ยากจน" ก็กลับเป็นเรื่องเล่าที่ดีเสียด้วยซ้ำ ดังนั้นผมจึงตัดสิน ALERT-1 ว่าเป็น "ความเบี่ยงเบนที่ตั้งใจ" แทนที่จะทิ้ง แล้วขอแก้เพอร์โซนาของ NPC หนึ่งบรรทัดเป็น "พ่อค้าชราที่ยังยึดเหนี่ยวร่องรอยของเมืองท่าการค้าที่เคยรุ่งเรือง" ส่วน ALERT-2 เป็นความผิดพลาดที่ชัดเจน ผมจึงเอาอาวุธออก และ ALERT-3 ก็แค่สร้างชื่อใหม่
ไม่ใช่ lint อัตโนมัติที่หยุดความผิดพลาดได้ แต่ lint อัตโนมัติ ดึงความผิดพลาดมาวางไว้ตรงหน้าคน ส่วนการตัดสินเป็นหน้าที่ของคน นี่คือหัวใจของ L2 (rulebook + AI ช่วย) ที่พูดถึงในหัวข้อ 6.1 AI สร้างโครงและ alert ส่วนคนเป็นผู้ตัดสินครั้งสุดท้าย การคัดแยกสิ่งที่ "ดูเหมือนผิดแต่เป็นเรื่องเล่าที่ดี" อย่าง ALERT-1 เป็นสิ่งที่ rulebook ทำไม่ได้
เมื่อร้อยเครื่องมือเข้าด้วยกันแล้วก็ต้องมีจังหวะ ไลน์หมุนเป็นรอบหนึ่งสัปดาห์ได้เสถียรที่สุด หนึ่งสัปดาห์สั้นพอที่งานตรวจสอบจะไม่ระเบิดทับถม และยาวพอที่การป้อนกลับจะไม่ล่าช้า อีกทั้งยังพอดีกับปฏิทินบนโต๊ะของผมหนึ่งช่อง
| วัน | สถานีของไลน์ | เวลาของผู้เขียน |
|---|---|---|
| จันทร์ | เขียนชีต lore_seeds (ต้นน้ำของ manifest) | ครึ่งวัน (5–7 เมือง × 15–20 นาที) |
| อังคาร | รัน generator เมือง→NPC→ไอเทม ต่อเนื่องกัน | ผู้เขียนไม่ต้องแทรกแซง |
| พุธ | lint รวม + ตรวจสอบรอบแรก (ตัวเอง) | 1 ชั่วโมง (5–10 นาที/เมือง) |
| พฤหัส | สุ่มตรวจรอบสอง (ลีดฝ่ายเนื้อเรื่อง) | 2–3 นาที/เมือง |
| ศุกร์ | นำเข้าบิลด์ + เตรียม seed สำหรับสัปดาห์ถัดไป | สั้น |
วันอังคารคือหัวใจของไลน์ พอ generator เมืองหย่อน manifest ออกมา generator NPC ก็คาบมันไป generator ไอเทมก็คาบไป แล้ว Squad ก็วางตำแหน่งจนจบ ห่วงโซ่นี้หมุนอยู่เบื้องหลังโดยไม่ต้องอาศัยการแทรกแซงของผู้เขียน ระหว่างนั้นผู้เขียนก็ไปเขียนเควสต์หลัก (L0 ทำมือล้วน) ผลตอบแทนที่แท้จริงของการร้อยเครื่องมืออยู่ตรงนี้ ถ้าเครื่องมือแยกกัน ผู้เขียนต้องลงมือถึงสามครั้งในวันอังคาร แต่ถ้าร้อยเข้าด้วยกันก็ไม่ต้องลงมือเลยสักครั้ง
ผู้เขียนคนเดียวผลิตเมือง 5–7 แห่งต่อสัปดาห์ พร้อม NPC·อาวุธที่ผูกติดมาด้วย 4 สัปดาห์ก็ได้เมือง 20–28 แห่ง เป้าหมาย 30 แห่งบรรลุได้ใน 6 สัปดาห์
ความสมบูรณ์ของไลน์ดูจากตัวเลข ไม่ใช่ความรู้สึก นี่คือสี่ตัวชี้วัดที่ถูกรวบรวมโดยอัตโนมัติทุกสัปดาห์
| ตัวชี้วัด | ช่วงปกติ | สัญญาณเมื่อเบี่ยงเบน |
|---|---|---|
| อัตราผ่าน lint รวม | 80–95% | ต่ำกว่า 60% แสดงว่าต้นน้ำ manifest พัง |
| การชนกันข้าม generator | 3–5 ครั้ง/เมือง | 10 ครั้งขึ้นไป แสดงว่าสัญญาระหว่าง generator แตก |
| อัตราทิ้งจากการตรวจสอบโดยมนุษย์ | 10–20% | 30% ขึ้นไป แสดงว่าพารามิเตอร์การผลิตผิด |
| เวลาต่อรอบของผู้เขียนคนเดียว | 5 วัน | 7 วันขึ้นไป แสดงว่าภาระทางความคิดมากเกินไป |
ตัวชี้วัดที่เป็นแบบไลน์มากที่สุดคือตัวที่สอง จำนวนการชนกันข้าม generator เมื่อใช้เครื่องมือเดี่ยวตัวเดียว ตัวเลขนี้ไม่มีอยู่ หากตัวเลขนี้พุ่งเกิน 10 ครั้งขึ้นมาทันที ก็ไม่ใช่ว่าเครื่องมือตัวใดตัวหนึ่งพัง แต่คือ สัญญา (manifest) ระหว่างเครื่องมือแตก มักจะระเบิดตอนที่เปลี่ยน schema ของ manifest ใน generator เมือง แต่ generator NPC ยังอ่าน schema เก่าอยู่ ถ้าไม่มีตัวชี้วัดนี้ ก็จะมองไม่เห็นความผิดพลาดนั้นจนกระทั่งปล่อยเกม
สี่ตัวชี้วัดนี้ถูกป้อนเข้าสู่การทบทวนรายไตรมาสทุกสัปดาห์ ถ้าแนวโน้มแย่ลง ก็ลดจำนวนเมืองที่ผลิตในสัปดาห์ถัดไปจาก 5–7 แห่งลงเหลือ 3–5 แห่ง แล้วหาสาเหตุ
เมื่อร้อยเครื่องมือหลายตัวเข้าด้วยกัน ก็จะเกิดความผิดพลาดที่เครื่องมือเดี่ยวไม่มี ผมขอบันทึกสามอย่างที่พบบ่อยไว้
หนึ่ง ความผิดพลาดจากสัญญาไม่ตรงกัน เพิ่มฟิลด์ใหม่ลงใน manifest ของ generator เมือง แต่ generator NPC ไม่รู้จักฟิลด์นั้น ตัวชี้วัดการชนกันแบบไขว้พุ่งสูง เมื่อพัฒนาเครื่องมือแยกกัน ก็ง่ายที่จะอัปเดตเพียงฝ่ายเดียว ทางรับมือคือใส่ฟิลด์ version ลงใน schema ของ manifest แล้วให้ generator ปลายน้ำแสดง alert ทันทีเมื่อเวอร์ชันไม่ตรงกัน ไม่ใช่การไล่บี้คน แต่เป็นการบังคับใช้สัญญา
สอง ความผิดพลาดจากต้นน้ำปนเปื้อน ถ้า manifest ถูกสร้างขึ้นบนสมมติฐานที่ผิด (เช่น "กลุ่มลักลอบค้า" ในหัวข้อ §6.4.2) ทุกอย่างที่อยู่ใต้มันก็จะปนเปื้อนตามไปด้วย อัตราทิ้งจากการตรวจสอบโดยมนุษย์เกิน 30% แต่พอดูผลลัพธ์ที่ถูกทิ้ง คุณภาพของ NPC แต่ละตัวกลับดีปกติ แต่ละตัวดีปกติ แต่สมมติฐานผิด ทางรับมือคือใส่การตรวจสอบเพิ่มอีกหนึ่งขั้นในช่วงการสร้าง manifest ตรวจต้นน้ำ 1 ชิ้นย่อมดีกว่าตรวจปลายน้ำ 110 ชิ้น
สาม ความผิดพลาดจากโมเดลดริฟต์ LLM ถูกอัปเดตอัตโนมัติจนคุณลักษณะของผลลัพธ์เปลี่ยนไป generator ทั้งสามตัวคือเมือง·NPC·ไอเทม สั่นพร้อมกัน ให้ตรวจสอบการเปลี่ยนแปลงในช่วง 1 สัปดาห์ล่าสุด วิเคราะห์ตัวอย่างที่ถูกทิ้ง 5 ชิ้น แล้วปรับพรอมต์หรือ context หลังจากเฝ้าติดตาม 1 สัปดาห์จึงยืนยันการกลับสู่สภาพเดิม
ทางรับมือร่วมของทั้งสามความผิดพลาดเหมือนกัน ไม่กล่าวโทษคน แต่เสริมความแข็งแกร่งของสัญญา ถ้าสาเหตุคือผู้เขียนเขียน lore_seeds มาเพียงบรรทัดเดียว แทนที่จะพูดว่า "เขียนมาสามบรรทัดสิ" ก็เพิ่มการตรวจสอบแบบบังคับลงใน lint ของ manifest แต่นั่นไม่ได้แปลว่าความรับผิดชอบของคนเป็น 0 นอกเหนือจากการเสริมความแข็งแกร่งของระบบแล้ว รูปแบบของความผิดพลาดยังถูกแบ่งปันกันในการทบทวนด้วย
จุดมุ่งหมายที่แท้จริงของการร้อยไลน์ไม่ใช่การกำจัดผู้เขียน แต่คือการทำให้ผู้เขียนได้ทุ่มเทกับงานชิ้นซิกเนเจอร์ ผมขอวาดไว้ในหนึ่งภาพว่าเวลาของผู้เขียนกระจัดกระจายไปอย่างไรเมื่อเครื่องมือแยกกัน และมารวมกันอย่างไรหลังจากร้อยเข้าด้วยกัน
เวลาของผู้เขียน 80% มารวมกันที่งานหลักและงานซิกเนเจอร์ แต่การกระจายนี้ไม่ได้คงอยู่ได้เอง พอนำไลน์มาใช้ เวลาของผู้เขียนมักจะไหลไปที่งานตรวจสอบจนหมด ดังนั้นจึงวัดการกระจายเวลาทุกเดือน และถ้างานหลักตกลงต่ำกว่า 50% ก็ลดจำนวนเมืองที่ผลิตเพื่อฟื้นเวลาของงานหลักกลับมา การกระจายเวลาเป็นสิ่งที่ต้องรักษาด้วยนโยบาย
เมื่อไลน์เมือง·NPC·ไอเทมเสถียรแล้ว ก็ขยายโครงเดียวกันนี้ไปสู่ดันเจี้ยน·สมุดภาพ·อีเวนต์ Live Ops หัวใจคือ การไม่สร้างรูปแบบใหม่ สิ่งล่อใจที่ว่า "ดันเจี้ยนต่างจากเมือง จึงต้องใช้โครงสร้างที่ต่างออกไป" มาเยือนอยู่เสมอ แต่โครงของไลน์ (manifest ที่ใช้ร่วมกัน → ห่วงโซ่ generator → lint รวม → การตรวจสอบโดยมนุษย์) เหมือนกันทุกประการ เพียงสลับเปลี่ยนรูปแบบเมตาดาตาอินพุตกับ rulebook ของโดเมนเท่านั้น
ถ้าเป็นดันเจี้ยน ก็จะมีฟิลด์อย่าง boss_pattern·encounter_flow เพิ่มเข้ามาใน dungeon_manifest.json และมีกฎโดเมนอย่างเส้นทางเดินของบอสติดเข้าไปใน lint รวมอีกหนึ่งบรรทัด โครงเหมือนเดิม เปลี่ยนแค่กฎ ถ้ารักษาโครงเดิมไว้ ผู้เขียนก็ไม่ต้องเรียนรู้เครื่องมือใหม่อีก และโครงสร้างพื้นฐานของ lint รวมก็ถูกนำกลับมาใช้ซ้ำได้ทันที แต่นี่ไม่ได้แปลว่าให้ละเลยความเฉพาะตัวของโดเมน ดันเจี้ยนจำเป็นต้องมีกฎเส้นทางเดินที่เมืองไม่มีอย่างแน่นอน
นี่คือผลของการเดินไลน์รวมนี้ในโปรเจกต์ของผมเป็นเวลา 6 เดือน เทียบกับช่วงที่รัน generator เมือง·NPC·ไอเทม แยกกัน ตัวเลขสัมบูรณ์ด้านล่างเป็น การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ไม่ใช่การรวบรวมที่แม่นยำ แต่ทิศทางและสัดส่วนเป็นไปตามแนวโน้มที่วัดได้จริง
| ตัวชี้วัด | ช่วงเครื่องมือแยกกัน | หลังรวมไลน์ |
|---|---|---|
| เมืองที่ผลิต (6 สัปดาห์) | 18 แห่ง | 28 แห่ง |
| จำนวนครั้งที่ผู้เขียนแทรกแซงในวันอังคาร | 3 ครั้ง/เมือง | 0 ครั้ง |
| การชนกันข้าม generator (พบหลังปล่อยเกม) | 8–12 ครั้ง/ไตรมาส | 2–4 ครั้ง/ไตรมาส |
| เควสต์หลักของผู้เขียนคนเดียวต่อไตรมาส | 3 อัน | 8 อัน |
| เวลาตรวจสอบต้นน้ำ / เวลาตรวจสอบปลายน้ำ | 0 / 3 ชั่วโมง | 5 นาที / 1 ชั่วโมง |
การเปลี่ยนแปลงที่สำคัญที่สุดคือบรรทัดสุดท้าย ตอนที่เครื่องมือแยกกัน การตรวจสอบต้นน้ำเป็น 0 และการตรวจสอบปลายน้ำเป็น 3 ชั่วโมง พอร้อยไลน์เข้าด้วยกันแล้วตรวจสอบ manifest ที่ต้นน้ำ 5 นาทีที่ต้นน้ำก็ลบ 2 ชั่วโมงที่ปลายน้ำออกไป และเมื่อความผิดพลาดไม่รั่วออกมาตามรอยต่อระหว่างเครื่องมือ ความผิดพลาดด้านความสอดคล้องหลังปล่อยเกมก็ลดจาก 8–12 ครั้ง/ไตรมาส เหลือ 2–4 ครั้ง
และการแลกเปลี่ยน (trade-off) ก็ชัดเจนขึ้น แต่ก่อนมีการถกเถียงนามธรรมว่า "การผลิตจำนวนมากอันตราย" วนเวียนอยู่ทุกไตรมาส แต่ตอนนี้ตัดสินใจกันบนการเปรียบเทียบที่เป็นรูปธรรมว่า "การชนกัน -8 ครั้ง / เควสต์หลัก +5 อัน"
1) กรณีที่ไม่ร้อยเครื่องมือเข้าด้วยกัน แต่รันแยกกัน กับดักเกิดในรอยต่อระหว่างเครื่องมือ ไม่ใช่ภายในเครื่องมือ
2) กรณีที่เชื่อม generator เข้าด้วยกันโดยไม่มี manifest ถ้าไม่มีสัญญาที่ใช้ร่วมกัน ปลายน้ำก็จะไม่ตรงกับต้นน้ำ
3) กรณีที่แทนที่ lint รวมด้วย lint เดี่ยว lint เดี่ยวมองการชนกันแบบไขว้ไม่เห็น
4) กรณีที่บีบรอบจาก 5 วันเหลือ 3 วัน 5 วันคือมาร์จินความปลอดภัยของการตรวจสอบ
5) กรณีที่ไม่ตรวจสอบต้นน้ำ (manifest) แต่ไปตรวจสอบปลายน้ำ จงดูต้นน้ำ 1 ชิ้นแทนปลายน้ำ 110 ชิ้น
6) กรณีที่โยนความผิดพลาดให้เป็นความรับผิดชอบของคนเพียงอย่างเดียว คำตอบคือการเสริมความแข็งแกร่งของสัญญา·การทำให้กฎเป็นอัตโนมัติ
7) กรณีที่ "ไม่ใช้" หลังจากมีไลน์แล้ว การบังคับใช้รอบหนึ่งสัปดาห์สำคัญพอ ๆ กับตัวเครื่องมือ
setup จงเตรียม generator เมือง (6.2) และ generator NPC (6.3) ให้พร้อม แล้วกำหนด schema ของ city_manifest.json ที่ทั้งสองจะใช้ร่วมกันขึ้นมาหนึ่งชุด ฟิลด์อย่างน้อยควรมี lore_seeds·region·faction·level_range·forbidden_names·tone·version
prompt จงใช้พรอมต์เครื่องเขียน manifest จากในเนื้อหาด้านบนตามนั้นเลย หัวใจอยู่ที่สองกฎสุดท้าย "อย่าให้ซ้ำกับฝ่ายของเมืองข้างเคียง" (ป้องกันการชนกันแบบไขว้) และ "จงแยกแสดงค่าที่อนุมานกับค่าที่ระบุไว้" (ความเป็นไปได้ในการตรวจสอบ) เมื่อสร้างเมืองให้หย่อน manifest ออกมา แล้วเชื่อมให้ generator NPC·ไอเทมรับ manifest นั้นเป็นอินพุต
verify จงรัน lint รวมหนึ่งครั้ง มันจะตรวจสอบสี่อย่างแบบไขว้กัน ได้แก่ ความสอดคล้องของ lore·ความสอดคล้องของช่วงเลเวล·การชนกันของฝ่าย·ชื่อซ้ำ ถ้า alert ขึ้นมา อย่าทิ้งอัตโนมัติ แต่ให้คนเป็นผู้ตัดสิน การคัดแยกสิ่งที่ "ดูเหมือนผิดแต่เป็นเรื่องเล่าที่ดี" (พ่อค้าชราแห่งเมืองท่าการค้าที่ล่มสลาย) เป็นหน้าที่ของคน
ฉบับย่อสำหรับคนเดียว แม้จะมีเครื่องมือแค่สองตัวคือเมือง·NPC ไลน์ก็ยังตั้งขึ้นได้ เพียงเขียน lore_seeds·level_range·forbidden_names ของแต่ละเมืองลงในสเปรดชีตหนึ่งใบ แล้วก๊อปปี้ทั้งแถวนั้นไปแปะลงในพรอมต์ของ generator NPC ก็ทำหน้าที่เป็น manifest ได้แล้ว lint รวมแม้จะมีแค่บรรทัดเดียวสำหรับความสอดคล้องของ lore ระหว่างเมือง-NPC ก็ป้องกันกับดักนั้นได้ ไม่ต้องมีโครงสร้างพื้นฐานยิ่งใหญ่ เพียงมีหลักการเดียวว่า "สอดสัญญาหนึ่งบรรทัดเข้าไประหว่างเครื่องมือ" ไลน์ก็เริ่มต้นได้
ทางออกของห้องหมายเลข 47 ในดันเจี้ยนถูกปิดตาย บิลด์ผ่าน QA ก็ผ่าน ภาพหน้าจอที่ผู้เล่นยืนหันหน้าเข้ากำแพงตรงหน้าห้องบอสพอดีถูกโพสต์ลงชุมชนในวันที่สามหลังเปิดให้บริการจริง ห้องนั้นเกิดจากการคัดลอกห้องที่ทำด้วยมือเมื่อสองอัปเดตก่อนมาวาง และในระหว่างการคัดลอกนั้น ทางเดินด้านตะวันออกทางหนึ่งเหลือไว้แต่ภาพ โดยไม่มีข้อมูลการเชื่อมต่อ ไม่มีใครตรวจสอบสิ่งนั้นเลย เพราะไม่มีเครื่องมือที่จะตรวจสอบ
บทนี้คือเรื่องราวของการสร้างโครงสร้างที่ทำให้อุบัติเหตุแบบนั้นถูกสกัดกั้นโดยอัตโนมัติตั้งแต่ขั้นบิลด์ หัวใจไม่ได้อยู่ที่ฝีมือในการวาดพื้นที่ แต่อยู่ที่วิธีบริหารข้อมูลที่ผูกอยู่กับพื้นที่นั้นด้วยกฎ
ที่ทำงานของการออกแบบเลเวลใกล้เคียงกับห้องเขียนแบบ แบบแปลนแต่ละแผ่นออกมาจากมือคน แต่ความสอดคล้อง การนำกลับมาใช้ และการตรวจสอบระหว่างแบบแปลนนั้น ถูกกำหนดด้วยกฎการบริหารตู้เก็บแบบแปลน ดันเจี้ยนหนึ่งห้องที่วาดด้วยมือนั้นใครก็ทำได้ แต่การบริหารดันเจี้ยน 100 อันให้มีเส้นโค้งความยากที่สอดคล้องกันและเป็นกราฟที่ไม่มีทางตันนั้น ไม่ใช่ปัญหาของฝีมือ แต่เป็นปัญหาของระบบ
ในโปรเจกต์ A ที่ผู้เขียนทำงานในตำแหน่ง Design Director (MMORPG ที่เจาะตลาดในประเทศ + เอเชียตะวันออกเฉียงใต้, ทีมขนาดกลาง (10\~50 คน), เน้นมือถือเป็นหลัก) ระบบนี้มีชื่อว่าเอกสารหนึ่งฉบับชื่อ Procedural_Level_Design_Master บทนี้พูดถึงว่าเอกสารฉบับนั้นรวมอะไรไว้ด้วยกัน AI เข้ามาแตะถึงไหน และหยุดตรงไหน ประสบการณ์การนำทีมออกแบบเกม RPG roguelite บนมือถือที่สร้างดันเจี้ยนขึ้นใหม่ทุกรอบการเล่น และการบริหารพื้นที่แบบโพรซีเดอรัลด้วยกฎ คือพื้นฐานที่รองรับบทนี้อยู่
การทำเลเวลให้เป็นอัตโนมัติแยกออกเป็นสองทิศทาง ทางหนึ่งคือการสร้างตัวพื้นที่เองแบบโพรซีเดอรัล PCG (Procedural Content Generation) แบบดั้งเดิม เช่น BSP partitioning (Binary Space Partitioning, เทคนิคคลาสสิกที่แบ่งพื้นที่ออกเป็นสองส่วนเท่า ๆ กันแบบเรียกซ้ำเพื่อจัดวางห้อง), wave function collapse, drunken walk grid อยู่ในกลุ่มนี้ อีกทางหนึ่งคือการบริหารเมตาดาตาของพื้นที่ — แท็กห้อง, ความเชื่อมต่อ, ป้ายกำกับความยาก, ช่องสำหรับเหตุการณ์
PCG แบบดั้งเดิมแข็งแกร่งในแบบแรก ในแนวเกมที่ "แผนที่ใหม่ทุกรอบ" คือหัวใจของความเป็นเกม อย่าง roguelike หรือ sandbox แบบแรกคือคำตอบที่ถูกต้อง แต่ MMORPG ต่างออกไป ผู้เล่นเดินดันเจี้ยนเดิมหลายสิบรอบ เดินจนเส้นทางขึ้นใจ ดันเจี้ยนจึงต้องเป็นพื้นที่ตายตัวที่ขัดเกลาด้วยมือ และที่ที่การทำให้เป็นอัตโนมัติจะเข้ามาได้นั้นไม่ใช่ตัวพื้นที่เอง แต่เป็น เมตาดาตาที่ทำให้พื้นที่นั้นบริหารจัดการได้
ทำไมเมตาดาตาจึงเป็นกระดูกสันหลังของการบริหาร เห็นชัดเมื่อมองทีละผลผลิต
| ผลผลิต | ถ้าไม่มีเมตาดาตา |
|---|---|
| พูลดันเจี้ยนหลายสิบอัน | ค้นไม่ได้ว่าห้องไหนอยู่ที่ใด, นำกลับมาใช้ไม่ได้ |
| การตรวจสอบเส้นโค้งความยาก | ไม่มีป้ายกำกับความยากรายห้องจึงวาดเส้นโค้งไม่ได้ |
| การจัดวางตำแหน่งเควสต์·บอสอัตโนมัติ | ไม่มีเมตาช่องเหตุการณ์จึงต้องป้อนพิกัดด้วยมือ |
| การซิงค์กับทีมอาร์ต | ไม่มีการแมป type ห้อง → ชุดอาร์ต จึงเกิดความไม่สอดคล้องทางภาพ |
| การวัดเส้นทางเดิน·เวลาที่ผู้เล่นอยู่ในห้อง | ทำ telemetry ที่อิงจากห้อง ID ไม่ได้ |
ดันเจี้ยนที่ไม่มีเมตาดาตานั้นบิลด์ได้ แต่บริหารไม่ได้ เหมือนห้องสมุดที่มีหนังสือเต็มแต่ไม่มีดัชนี นี่คือเหตุผลที่บทนี้มุ่งเน้นไปที่ "การบริหารเมตาดาตาของพื้นที่"
Procedural_Level_Design_Master มัดมาตรฐานสี่อย่างไว้ในเอกสารฉบับเดียว ได้แก่ แบบฟอร์มเมตาดาตาห้อง, พจนานุกรมแท็กห้อง, กฎความเชื่อมต่อ และเช็กลิสต์การตรวจสอบ ลองดูก่อนว่าเกิดอะไรขึ้นเมื่อทั้งสี่อย่างนี้กระจัดกระจาย ถ้านักออกแบบห้าคนต่างอ้างอิงแบบฟอร์มจากไฟล์คนละไฟล์ ฟิลด์ type คนหนึ่งจะเขียน combat คนหนึ่งเขียน Combat อีกคนเขียน battle_room การค้นหาพัง สถิติพัง และสุดท้ายการทำงานอัตโนมัติก็พัง
มาตรฐานสี่อย่างนี้เมื่อจัดเรียงตาม Layer แล้ว ตำแหน่งของแต่ละอย่างชัดเจน แบบฟอร์ม·พจนานุกรม·กฎอยู่ในรูลบุ๊ก (L1) ที่ควบคุมการสร้าง, เนื้อหาห้องที่สร้างขึ้นอยู่ในคอนเทนต์ (L2), ค่าในชีตอยู่ในข้อมูล (L3), และการตรวจสอบอยู่ในด่านบิลด์·QA (L4)
การที่เอกสารมาสเตอร์รวมมาตรฐานสี่อย่างไว้ด้วยกันนั้นไม่ได้หมายความว่า "ยัดเนื้อหาทั้งหมดไว้ในไฟล์เดียว" แต่หมายความว่า "รวมกฎไว้ในตำแหน่ง L1" ด้วยเหตุนี้ การทำงานอัตโนมัติที่จะกล่าวถึงข้างหน้าจึงสามารถวางทับอยู่บนขอบเขตของ Layer ได้ (จะเกิดอะไรขึ้นเมื่อการแบ่งแยกพังทลาย จะกล่าวถึงใน 7.1.11)
ห้องหนึ่งเป็นไปตามแบบฟอร์มต่อไปนี้ แบบฟอร์มนี้คืออินเทอร์เฟซอินพุตของการทำงานอัตโนมัติ
room_id: dungeon_021_room_07
dungeon: dungeon_021_silvermark_library
type: combat_room # combat / puzzle / lore / safe / boss
size: medium # small / medium / large
difficulty_label: hard_for_level_28
tags: [scholar_theme, vertical_layout, water_hazard]
connections:
- target_room: dungeon_021_room_06
type: door
direction: south
- target_room: dungeon_021_room_08
type: passage
direction: east
event_slots:
- slot: enemy_spawn_1
constraints: [scholar_enemy, level_28]
- slot: lore_object_1
constraints: [scholar_lore]
movement_complexity: 4 # 1~5
estimated_clear_time_sec: 90
art_pack: scholar_library_v2
แต่ละฟิลด์มีปลายทางการบริโภคอัตโนมัติอย่างน้อยหนึ่งแห่ง type ใช้ในสถิติพูลดันเจี้ยนและการคำนวณความยาก, tags ใช้ในการค้นหา·การนำกลับมาใช้·การแมปชุดอาร์ต, connections ใช้ในการตรวจสอบกราฟ (การตรวจหาทางตัน), event_slots ใช้ในการจัดวางเควสต์·บอสอัตโนมัติ ฟิลด์ที่ไม่มีปลายทางการบริโภคจะไม่ใส่ไว้ในแบบฟอร์ม เพราะเพิ่มแต่ต้นทุนการป้อนข้อมูลโดยไม่มีคุณค่า
แท็กคือคีย์การค้นหาของเมตาดาตา ถ้าแพร่ขยายไม่จำกัด การค้นหาจะพัง ถ้าติดป้าย 200 อันในลิ้นชัก จะหาไม่เจอว่าอะไรอยู่ตรงไหน ด้วยเหตุนี้จึงบริหารด้วย 5 หมวด × ประมาณ 6 enum ต่อหมวด รวมแล้วประมาณ 30 อัน
| หมวด | จำนวน enum | ตัวอย่าง |
|---|---|---|
| theme | 8 | scholar_theme, ruins_theme, forest_theme … |
| layout | 5 | vertical_layout, horizontal_corridor, open_arena … |
| hazard | 6 | water_hazard, fire_hazard, falling_hazard … |
| interaction | 4 | puzzle_required, lever_activation … |
| narrative | 7 | flashback_trigger, dialogue_zone … |
ห้องหนึ่งไม่ใส่แท็กเกิน 5 อัน ปกติคือ 3\~4 อัน การจะเพิ่มแท็กใหม่ต้องผ่านด่านสี่ขั้น คือ ต้องเป็นตัวเลือกที่จะใช้ตั้งแต่ 5 ห้องขึ้นไปต่ออัปเดต, ต้องไม่สามารถแสดงได้ด้วยการรวมแท็กเดิม, การนำไปใช้ค้นหา·แมปชุดอาร์ตต้องชัดเจน, และต้องยังคงใช้อยู่ 5 ห้องแม้ผ่านการใช้งานไปแล้ว 1 เดือน เงื่อนไขข้อสุดท้ายคือหัวใจ ถ้าแท็กที่สร้างขึ้นชั่วคราวถูกใช้ครั้งเดียวแล้วทิ้ง พจนานุกรมก็จะปนเปื้อน
มาตรฐานทั้งหมดที่ผ่านมาเชื่อมต่อกันเป็นกระแสเดียวอย่างไร เส้นเชื่อมต่อนั้นคือโครงกระดูกที่บทนี้รองรับอยู่ เป็นไปป์ไลน์ที่เริ่มจากรูลบุ๊ก ผ่านการแปรผันโดย AI ช่วย แล้วจบที่การตรวจสอบของ guardrail
flowchart TD
A["L0 วิสัยทัศน์ — คอนเซปต์ดันเจี้ยน·เจตนาการกำหนดจังหวะ"] --> B["L1 รูลบุ๊ก\nพจนานุกรมแท็ก · กฎความเชื่อมต่อ · กฎช่อง"]
B --> C["จัดวางโครงห้อง\nงานมือนักออกแบบ + เอดิเตอร์"]
C --> D["ดึงเมตาดาตาอัตโนมัติ\nroom_id · connections · type · size"]
D --> E["แปรผันโดย AI ช่วย\nดึง tags · เสนอการแมป art_pack"]
E --> F{"ตรวจสอบบังคับตามพจนานุกรม\nแท็กนอกพจนานุกรม?"}
F -->|นอกพจนานุกรม| E
F -->|ผ่าน| G["นักออกแบบตรวจสอบ\nยืนยัน tags · difficulty_label"]
G --> H["L4 ตรวจสอบกราฟ\nการเข้าถึง · ทางตัน · วงวน · การแตกแขนง"]
H -->|ละเมิด| C
H -->|ผ่าน| I["ตรวจสอบเส้นโค้งความยาก\nรวมป้ายกำกับความยากของห้อง"]
I --> J["ด่านความสอดคล้องของชุดอาร์ต"]
J -->|ผ่าน| K["บิลด์ — ลงทะเบียนพูลดันเจี้ยน"]
J -->|ไม่สอดคล้อง| E
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class B,D,F,H,I,J code;
class E ai;
class C,G human;
class K pass;
ขอชี้ลักษณะสามอย่างของไปป์ไลน์นี้ไว้ ประการแรก รูลบุ๊ก (L1) อยู่ต้นน้ำของการสร้างทั้งหมด ประการที่สอง AI แปรผันได้เฉพาะภายในพจนานุกรมที่รูลบุ๊กกำหนดไว้เท่านั้น — ด่าน F ส่งผลลัพธ์ที่อยู่นอกพจนานุกรมกลับคืน ประการที่สาม การตรวจสอบ (H·I·J) ถูกตรึงไว้เป็นด่านก่อนบิลด์ ดังนั้นการละเมิดจึงถูกสกัดด้วยโค้ด ไม่ขึ้นอยู่กับความใส่ใจของคน อุบัติเหตุห้องหมายเลข 47 เกิดขึ้นเพราะไม่มีด่าน H
ฟิลด์ connections ในเมตาห้องทำให้ทั้งดันเจี้ยนกลายเป็นกราฟแบบมีทิศทาง เมื่อกลายเป็นกราฟ การตรวจสอบก็เป็นอัตโนมัติ
| การตรวจสอบ | การจัดการเมื่อละเมิด |
|---|---|
| ห้องเริ่มต้น → ห้องบอส เข้าถึงได้ | สกัดด้วยการทำให้บิลด์ล้มเหลว |
| ทางตัน (ทางออก 1 ทาง + non-safe_room) | alert — ให้นักออกแบบตรวจสอบ |
| ความสอดคล้องของการเชื่อมต่อสองทิศทาง (มี A→B แต่ไม่มี B→A) | แก้ไขอัตโนมัติ |
| ความยาววงวน — ลูปสั้น 2\~3 ห้อง | alert |
| ความกว้างการแตกแขนง — แตกแขนงพร้อมกันตั้งแต่ 4 ทางขึ้นไป | ให้นักออกแบบตรวจสอบ |
สคริปต์การวัดมีรูปแบบดังต่อไปนี้ เป็น wrapper บาง ๆ ที่ห่อคำศัพท์ของดันเจี้ยนไว้บนอัลกอริทึมกราฟมาตรฐาน (เส้นทางยาวที่สุด·ดีกรีออกเฉลี่ย·การนับลูป·เส้นทางสั้นที่สุด)
# level_graph_metrics.py
def measure(dungeon):
graph = build_graph(dungeon.rooms)
return {
"depth": longest_path_length(graph),
"branching_factor": avg_out_degree(graph),
"loop_count": count_loops(graph),
"dead_ends": count_dead_ends(graph),
"boss_reachability": shortest_path(graph.start, graph.boss),
}
ตัวชี้วัดทั้งห้าถูกแสดงผลในรูปแบบที่เปรียบเทียบกับดันเจี้ยนอื่นได้ ใช้เป็นตัวชี้วัดความหลากหลายของพูลดันเจี้ยน แต่การที่ตัวชี้วัดหลากหลายก็ไม่ได้แปลว่าดันเจี้ยนสนุก ตัวชี้วัดมีไว้สกัดอุบัติเหตุ ไม่ได้มีไว้รับประกันความสนุก ทางตัน 0 รายการไม่ได้รับประกันความสนุก ความสนุกมาจากอินไซต์ของนักออกแบบ และการตรวจสอบกราฟเพียงรองพื้นไว้ไม่ให้อินไซต์นั้นถูกอุบัติเหตุกลบหายไปเท่านั้น
ส่วนหนึ่งของการทำงานอัตโนมัติที่คนอยากปล่อยมือมากที่สุดคือการป้อน tags การติดแท็กห้อง 100 ห้องนั้นน่าเบื่อ และเมื่อดูแค่ภาพหน้าจอห้อง คนก็สับสนได้เหมือนกัน งานที่ทำซ้ำและมีเกณฑ์การตัดสินชัดเจนแบบนี้แหละคือตำแหน่งที่ดีที่ AI จะรองรับการร่างเบื้องต้น หัวข้อนี้จะกางเวิร์กโฟลว์ที่เคยรันงานนั้นจริง — ทั้งพรอมต์, ผลลัพธ์ของ AI ที่ถูกปฏิเสธ, ไปจนถึงการขอใหม่ของคน — ออกมาโดยไม่มีการตกแต่ง
พรอมต์ครั้งที่ 1:
[อินพุต]
- ภาพหน้าจอห้อง: (แนบมา)
- พื้นที่ห้อง: 18m × 12m, ความสูงเพดาน 9m (โครงสร้างแนวตั้ง 2 ชั้น)
- ศัตรูที่จัดวาง: scholar_phantom × 3, water_elemental × 1
- type ห้องที่อยู่ติดกัน: lore_room (ด้านตะวันตก), combat_room (ด้านตะวันออก)
- ในห้องมีแอ่งน้ำตื้น
เลือกแท็ก 3~5 อันที่เหมาะกับห้องนี้จากพจนานุกรมด้านล่างเท่านั้น อย่าสร้างแท็กนอกพจนานุกรม
ตอบเฉพาะ enum ของแท็กคั่นด้วยคอมมา
[พจนานุกรมแท็ก 30 อัน]
theme: scholar_theme, ruins_theme, forest_theme, cave_theme,
crypt_theme, swamp_theme, mechanical_theme, celestial_theme
layout: vertical_layout, horizontal_corridor, open_arena,
maze_layout, bridge_layout
hazard: water_hazard, fire_hazard, falling_hazard,
poison_hazard, crush_hazard, dark_hazard
interaction: puzzle_required, lever_activation, key_gate, hidden_passage
narrative: flashback_trigger, dialogue_zone, lore_object_zone,
ambush_setup, ritual_site, vista_point, rest_anchor
ผลลัพธ์ครั้งที่ 1 ของ Claude (แบบดิบ):
scholar_theme, vertical_layout, water_hazard, ambush_setup, two_story_layout
ตรงนี้มีปัญหาสองอย่างปรากฏออกมาตามจริง อย่างแรกคือ two_story_layout เป็นแท็กที่ไม่มีในพจนานุกรม โมเดลถูกอินพุต "โครงสร้างแนวตั้ง 2 ชั้น" ดึงไป จึงสร้างคำที่อยู่นอกพจนานุกรมขึ้นมา อีกอย่างหนึ่งคือแท็กแตะเพดาน 5 อันแล้ว แต่ vertical_layout กับ two_story_layout มีความหมายซ้อนทับกัน เท่ากับใช้สองช่องในหมวด layout
การตัดสินปฏิเสธของคน two_story_layout เป็นการละเมิดการบังคับตามพจนานุกรม จึงเป็นเป้าหมายของการปฏิเสธอัตโนมัติ (ด่าน F ของไปป์ไลน์) ส่วน ambush_setup นั้นการตัดสินแยกเป็นสองทาง — มีศัตรูจัดวางอยู่ 3 ตัวก็จริง แต่จากภาพหน้าจอนั้นคลุมเครือว่าเป็นองค์ประกอบของการซุ่มโจมตี หรือเป็นเพียงห้องต่อสู้ธรรมดา มีความเป็นไปได้ว่าโมเดลตีความเกินจริง ห้องที่อยู่ติดกันทั้งสองเป็น lore/combat จึงมีพื้นฐานของทริกเกอร์การซุ่มโจมตีเชิงเนื้อเรื่องที่อ่อน ปฏิเสธ
ขอใหม่ครั้งที่ 2:
two_story_layout ไม่มีในพจนานุกรมและซ้อนกับ vertical_layout เอาออก
ambush_setup ก็เอาออก — มีศัตรูไม่ได้แปลว่าซุ่มโจมตี
// (เจตนา: แท็ก narrative ใช้เฉพาะเมื่อมีการระบุเจตนาเชิงเนื้อเรื่อง การต่อสู้ธรรมดาแสดงด้วย type อยู่แล้ว)
ไม่ต้องฝืนเติมช่องว่างก็ได้ 3~4 อันคือปกติ ติดแท็กให้ใหม่
ผลลัพธ์ครั้งที่ 2 ของ Claude:
scholar_theme, vertical_layout, water_hazard
คราวนี้ลดเหลือ 3 อัน, ทั้งหมดอยู่ในพจนานุกรม, และไม่มีการซ้ำหมวด คนยอมรับผลลัพธ์นี้ tags: [scholar_theme, vertical_layout, water_hazard] ของแบบฟอร์มห้องจึงถูกยืนยันเช่นนี้
บทเรียนของบันทึกเซสชันจริงนี้มีสองบรรทัด ประการแรก AI overfit กับรายละเอียดหนึ่งของอินพุต ("2 ชั้น") แล้วออกไปนอกพจนานุกรม — ด่านบังคับตามพจนานุกรมต้องจับสิ่งนี้ที่ระดับโค้ด ประการที่สอง AI มีแนวโน้มจะพยายามเติมช่องว่าง — ถ้าไม่ระบุชัดว่า "ไม่ต้องฝืนเติม" มันจะพยายามถมทั้ง 5 ช่องให้เต็ม ความล้มเหลวทั้งสองนี้พบบ่อย และวิธีรักษาทั้งสองจะมีเสถียรภาพได้ต่อเมื่อถูกบังคับจากรูลบุ๊ก (พจนานุกรม + เพดาน) ไม่ใช่จากพรอมต์
ถ้านักออกแบบเติมเมตาห้อง 1 ห้องด้วยมือ จะใช้เวลา 5\~10 นาที ดันเจี้ยน 1 อัน (20\~30 ห้อง) ก็ 2\~5 ชั่วโมง ดันเจี้ยน 100 อันก็ 200\~500 ชั่วโมง (การประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ — ค่าสูงสุดที่คำนวณจากเวลาป้อนข้อมูลเฉลี่ยต่อห้อง × จำนวนห้อง) ถ้าเติมด้วยมือทั้งหมด นักออกแบบก็จะกลายเป็นทาสการป้อนเมตาดาตา
ด้วยเหตุนี้จึงแบ่งผู้รับผิดชอบการเติมตามแต่ละพื้นที่
| พื้นที่ | ผู้รับผิดชอบการเติม |
|---|---|
| room_id · dungeon · connections | เอดิเตอร์ดึงอัตโนมัติ (L3) |
| type · size | จำแนกอัตโนมัติจากพื้นที่ห้อง·จำนวนการเชื่อมต่อ |
| tags | AI ช่วย + นักออกแบบตรวจสอบ (7.1.7) |
| event_slots | รูลบุ๊กตาม type ห้อง |
| difficulty_label | คำนวณอัตโนมัติจากการรวมข้อมูลศัตรูในห้อง |
| art_pack | แมปจาก type ห้อง · theme ดันเจี้ยน |
สิ่งที่นักออกแบบยืนยันด้วยมือมีเพียงการตรวจสอบ tags และการอนุมัติขั้นสุดท้ายของ difficulty_label เท่านั้น ที่เหลือเครื่องมือเติม คนตรวจสอบ จุดประสงค์ของการทำงานอัตโนมัติคือการดึงนักออกแบบออกจากการป้อนข้อมูล แล้วส่งกลับไปยังการตัดสินเรื่องการกำหนดจังหวะ·ห้องซิกเนเจอร์·นโยบายการนำกลับมาใช้
ผลที่ใหญ่ที่สุดของมาตรฐานมาสเตอร์คือการนำห้องกลับมาใช้ ถ้ามีห้องที่ค้นหาได้ด้วยแท็ก 30 ห้อง ก็สร้างดันเจี้ยน 5\~10 อันได้จากการผสมผสาน แต่เมื่ออัตราการนำกลับมาใช้สูงขึ้น ดันเจี้ยนจะน่าเบื่อ ด้วยเหตุนี้จึงวาง guardrail ไว้คู่กับการนำกลับมาใช้
| guardrail | คำนิยาม |
|---|---|
| ห้องหนึ่งปรากฏในดันเจี้ยนได้สูงสุด 5 อัน | ติดตามความถี่การปรากฏอัตโนมัติ |
| บังคับแปรผันทางภาพเมื่อปรากฏครั้งที่สอง | เปลี่ยนแสง·พร็อพ |
| ห้ามนำห้องบอส·ห้องซิกเนเจอร์กลับมาใช้ | บังคับด้วย flag |
| ติดตามฟีดแบ็กเชิงลบของห้องที่นำกลับมาใช้ | telemetry ผู้เล่น |
การนำกลับมาใช้เป็นวิธีลดต้นทุน ไม่ใช่จุดประสงค์ พอตั้งอัตราการนำกลับมาใช้เป็น KPI เมื่อใด ประสบการณ์ผู้เล่นก็จะจืดชืดลง ถ้า 0% (ทุกห้องใหม่หมด) ต้นทุนการผลิตจำนวนมากจะระเบิด และถ้าเกิน 70% ดันเจี้ยนจะแยกแยะจากกันไม่ออก จากประสบการณ์ ช่วง 30\~40% คือจุดสมดุลระหว่างต้นทุนกับความหลากหลาย (เป็นการสังเกตเชิงทิศทาง ค่าเกณฑ์ที่แม่นยำต่างกันไปตามแต่ละโปรเจกต์)
| รูปแบบ | วิธีรักษา |
|---|---|
| 5 คนตีความแบบฟอร์มเมตาเป็น 5 แบบ | รวม L1 ด้วยเอกสาร Master |
| แท็กเพิ่มเป็น 50\~100 อัน | พจนานุกรม 30 อัน + ด่าน 4 ขั้น |
| บิลด์โดยไม่ตรวจสอบทางตัน | ใช้การตรวจสอบกราฟเป็นด่านบิลด์ |
| นักออกแบบทำเมตาทั้งหมดด้วยมือ | เอดิเตอร์ดึง + AI ช่วย |
| AI สร้างแท็กนอกพจนานุกรม | ปฏิเสธอัตโนมัติด้วยด่านบังคับตามพจนานุกรม |
| การนำกลับมาใช้ 0% หรือ 70%+ | ช่วง 30\~40% + guardrail การแปรผัน |
โครงสร้างของ 7.1.2\~7.1.6 ที่คลี่ออกด้วยรูลบุ๊ก·การสร้าง·การตรวจสอบที่ผ่านมาทั้งหมดนั้นเอง คือผลผลิตของการแยก Layer ข้อเสนอทั่วไปที่ว่าการแยก Layer คือเงื่อนไขเบื้องต้นของการสร้างแบบโพรซีเดอรัล·การทำงานอัตโนมัติ (L0 จุดยึด → L1 รูลบุ๊ก → L2 เนื้อหา → L3 ค่าตัวเลข → L4 ด่าน, ถ้าเป็นก้อนเดียวการสร้างจะพังทลาย) ได้กล่าวถึงใน §6.6 แล้ว ตรงนี้นำสิ่งนั้นมาประยุกต์ใช้กับการบริหารเมตาดาตาเลเวล
ถ้าไม่มีการแบ่งแยกนี้ การจัดวางห้อง·BSP·การกำหนดจังหวะ·ทริกเกอร์เชิงเนื้อเรื่องจะปนกันในไฟล์เดียว และทุกครั้งที่ขยับห้องหนึ่งช่อง เจตนาการกำหนดจังหวะ·ช่องเหตุการณ์·กราฟความเชื่อมต่อก็จะพังพร้อมกัน เป็นสถานการณ์ที่ห้องเขียนแบบ·โกดังวัสดุ·ห้องตรวจสอบกองรวมกันบนโต๊ะตัวเดียว พอดึงแบบแปลนออกหนึ่งแผ่น ใบส่งของวัสดุและตารางตรวจสอบก็หลุดออกมาด้วย ด้วยเหตุนี้ การที่ AI ช่วยใน 7.1.7 ทำงานได้ก็เป็นเพราะ Layer เช่นกัน ห้อง ID·ความเชื่อมต่อถูกเติมจากเอดิเตอร์ (ดึงอัตโนมัติ L3), แท็กจาก AI (บังคับตามพจนานุกรม L1), difficulty_label จากการรวม (L3→L4) การทำงานอัตโนมัติวางทับอยู่บนขอบเขตของ Layer ถ้าวางทับอยู่บนก้อนเดียว อุบัติเหตุจะระเบิดขึ้นภายในอัปเดตแรก และตัวเครื่องมือเองก็จะถูกทิ้ง
แต่นี่ไม่ได้หมายความว่าต้องมีลิ้นชักห้าช่องครบสมบูรณ์ตั้งแต่แรก หลักการคือ แบ่งแยกแบบค่อยเป็นค่อยไป อินเทอร์เฟซให้แคบ ในอัปเดตแรก แค่แยกรูลบุ๊ก L1 (พจนานุกรมแท็ก + กฎความเชื่อมต่อ) กับชีต L3 (ชีตเมตาห้อง) ก็เกิดที่ให้การทำงานอัตโนมัติเข้ามาได้แล้ว เจตนาการกำหนดจังหวะ L0 และด่านตรวจสอบ L4 ค่อยเติมขณะผ่านแต่ละอัปเดต เมื่อมาตรฐานเป็นหนึ่งเดียวจึงเกิดที่ให้การทำงานอัตโนมัติเข้ามา และยิ่งการทำงานอัตโนมัติเข้ามามากเท่าใด นักออกแบบก็ยิ่งมุ่งไปที่การตัดสินเรื่องการกำหนดจังหวะ·ซิกเนเจอร์·การนำกลับมาใช้ แทนที่จะทำงานมือทีละห้อง
setup. เลือกดันเจี้ยนหนึ่งอัน แล้วสร้างชีต YAML ที่มีเพียงสี่ฟิลด์ room_id · type · connections · tags ต่อแต่ละห้อง ส่วนแท็ก ให้ตรึงพจนานุกรม 5 หมวดประมาณ 30 enum ลงบนกระดาษหนึ่งแผ่นก่อน
prompt. ใส่ภาพหน้าจอห้อง + พื้นที่ + ชนิดศัตรู + type ห้องที่อยู่ติดกัน แล้วขอด้วย "เลือกแท็ก 3\~5 อันจากพจนานุกรมนี้เท่านั้น ห้ามแท็กนอกพจนานุกรม ไม่ต้องเติมช่องว่าง" (พรอมต์ 7.1.7 ตามเดิม)
verify. (1) ถ้าผลลัพธ์ของ AI มีแท็กนอกพจนานุกรม ให้ปฏิเสธและขอใหม่ (2) สร้างกราฟจาก connections แล้วตรวจสอบการเข้าถึงจากเริ่มต้น→บอสและทางตัน — ถ้ามีการละเมิดแม้แต่อันเดียว ให้ทำเครื่องหมายว่าห้องนั้นบิลด์ไม่ได้
ถ้าคุณเป็นนักพัฒนาคนเดียวที่ไม่มีโครงสร้างพื้นฐานของเครื่องมือ ให้เริ่มเอกสารมาสเตอร์ด้วยมาร์กดาวน์หนึ่งแผ่น พจนานุกรมแท็ก 30 บรรทัด, กฎความเชื่อมต่อ 5 บรรทัด, เช็กลิสต์การตรวจสอบ 5 บรรทัด ก็เพียงพอ สำหรับการตรวจสอบกราฟ ถ้ามีห้องไม่เกิน 10 ห้อง แค่วาดลูกศรลงบนกระดาษแล้วตรวจดูทางตันด้วยตาเปล่า ก็ได้ผล 80% ของประสิทธิภาพ หัวใจไม่ได้อยู่ที่เครื่องมือ แต่อยู่ที่นิสัยของการ "ติดข้อมูลให้กับห้อง แล้วตรวจสอบข้อมูลนั้นด้วยกฎ" เอง เครื่องมือค่อยเพิ่มเข้ามาเมื่อห้องเกิน 50 ห้องจนตรวจด้วยมือเริ่มหนักเกินไป
จอมเวทฝึกหัดตัวหนึ่งวิ่งเข้าประชิดผู้เล่นแล้วฟันดาบใส่ มันคือ NPC ที่ออกแบบให้เป็นผู้ร่ายเวทระยะไกล HP บางเหมือนกระดาษ ถ้าโดนระยะประชิดแค่ครั้งเดียวก็ตาย แต่มันกลับไม่คิดจะถอยห่าง บันทึกการบิลด์ไม่มีข้อผิดพลาดใด ๆ เปิด BehaviorTree ในเอดิเตอร์ขึ้นมาดูอีกครั้ง โหนดก็ยังเชื่อมต่อกันเรียบร้อยดี หลังจากเพ่งดูอยู่ชั่วโมงหนึ่งจึงพบสาเหตุ เงื่อนไขระยะของกิ่งถอยถูกใส่ค่าเป็น 0.5 ไม่ใช่ 5 ทั้งที่มันควรหนีเมื่อศัตรูเข้ามาในระยะ 5 เมตร แต่กลับเป็น 0.5 เมตร — นั่นคือกิ่งถอยจะไม่ทำงานเลยจนกว่าจะเกือบจ่อหน้ากัน
มันคือตัวเลขเพียงตัวเดียว ในเอดิเตอร์โหนดแบบกราฟิก ตัวเลขนั้นจะมองเห็นได้ก็ต่อเมื่อกางแผงด้านในของโหนดออกมาเท่านั้น และไม่ถูกบันทึกไว้ในประวัติการเปลี่ยนแปลง ไม่มีวิธีตามรอยว่าใครเปลี่ยนค่านั้นเมื่อไหร่ นับแต่วันนั้นเป็นต้นมา โปรเจกต์ A ของผู้เขียนเริ่มจัดการ BehaviorTree ด้วย json ไม่ใช่กราฟิก บทนี้คือบันทึกหนึ่งรอบของการที่คนกับ AI ร่วมกันแก้ไข json นั้น และให้เครื่องตรวจสอบโดยอัตโนมัติ
BehaviorTree คือโครงสร้างมาตรฐานโดยพฤตินัยที่ใช้นิยามการต่อสู้ การเคลื่อนที่ และการตอบสนองของ NPC ฝ่ายศัตรู ตัวเลือก (selector) จะลองกิ่งต่าง ๆ ตามลำดับความสำคัญ ส่วนลำดับ (sequence) จะร้อยเงื่อนไขกับแอ็กชันเข้าด้วยกันตามลำดับ ตัวโครงสร้างเองนั้นเรียบง่าย ปัญหาอยู่ที่ขนาด
ในโปรเจกต์ A ของผู้เขียน BT ของ NPC ศัตรูหนึ่งตัวประกอบด้วยโหนดราว 50\~200 โหนด และ NPC ที่เปิดให้บริการมีมากกว่า 100 ตัว เมื่อคูณกันแล้ว จำนวนโหนด BT ทั้งหมดจะอยู่ในหลักหมื่น ที่ขนาดเท่านี้ จะมาถึงจุดที่คนตอบคำถามอย่าง "ถ้าเปลี่ยนแพตเทิร์นถอยนี้ NPC ตัวไหนจะได้รับผลกระทบบ้าง" ไม่ได้อีกต่อไป มันก็เหมือนกับมีสมุดร้อยเล่มกางอยู่บนโต๊ะ พอแก้บรรทัดหนึ่งในเล่มแรก ก็ต้องคอยกวาดสายตาตามว่าผลจะลามไปที่ไหนในอีกเก้าสิบเก้าเล่มที่เหลือ
สิ่งที่ผู้เขียนเรียกร้องตอนย้ายจาก BT แบบกราฟิกมาเป็น json มีอยู่สี่อย่าง
เอดิเตอร์ BT ในตัวของเอนจินเกมเชิงพาณิชย์นั้นผสานรวมได้สะดวกและดีบักเชิงภาพได้แข็งแรง เพียงแต่มันมักถูกบันทึกเป็นแอสเซตแบบไบนารี (binary) จึงอ่อนเรื่อง diff ของข้อความและการตามรอยผลกระทบของการเปลี่ยนแปลง เนื่องจากโปรเจกต์ A ของผู้เขียนตั้งสมมติฐานว่าเป็นเกมไลฟ์ที่มี BT เปิดให้บริการเกิน 100 ตัว จึงเลือกทางพัฒนาฟอร์แมต BT แบบ json และเอดิเตอร์ขึ้นเองต่างหาก ขอชี้ให้ชัด นี่ไม่ใช่คำตอบที่ถูกของทุกทีม ถ้า BT ที่เปิดให้บริการมีไม่ถึง 50 ตัว การใช้เอดิเตอร์ในตัวของเอนจินไปเลยมักจะถูกกว่าเสมอ เหตุผลรองรับการพัฒนาเองจะกล่าวถึงอีกครั้งตอนท้ายบทนี้
ก่อนอื่นมาดูหน้าตาของผลลัพธ์กัน ด้านล่างคือ BT บางส่วนของ NPC แบบสนับสนุนระยะไกลแห่งกิลด์นักปราชญ์ แก่นมีสองอย่าง คือทุกพฤติกรรมเป็นข้อความ git จึงตามรอยได้ทีละบรรทัด และแพตเทิร์นร่วมถูกอ้างอิงด้วย subtree_ref
{
"bt_id": "bt_scholar_archer_v3",
"category": "ranged_combatant",
"tags": ["scholar_faction", "ranged", "support"],
"description": "แบบสนับสนุนระยะไกลแห่งกิลด์นักปราชญ์ รักษาระยะ + ถอยเป็นลำดับแรก",
"root": {
"type": "selector",
"children": [
{
"type": "sequence",
"name": "low_hp_retreat",
"children": [
{"type": "condition", "fn": "hp_below", "param": 0.3},
{"type": "subtree_ref", "id": "subtree_retreat_to_ally"}
]
},
{
"type": "sequence",
"name": "kite_pattern",
"children": [
{"type": "condition", "fn": "enemy_in_close_range", "param": 5},
{"type": "action", "fn": "move_away", "param": {"distance": 8}}
]
},
{"type": "subtree_ref", "id": "subtree_ranged_attack_pattern"}
]
}
}
หากกางต้นไม้นี้ออกมาเป็นภาพ จะเป็นโครงสร้างที่ตัวเลือกลองกิ่งทั้งสามจากบนลงล่าง ขอให้สังเกตว่าบั๊กในตอนต้นบท — param ของ enemy_in_close_range เป็น 5 หรือ 0.5 — กลายเป็นบรรทัดเดียวที่มองเห็นได้ในพริบตาเมื่ออยู่ใน json
flowchart TD
R["selector
(จากบนลงล่างตามลำดับความสำคัญ)"]
R --> A["sequence: low_hp_retreat"]
R --> B["sequence: kite_pattern"]
R --> C["subtree_ref:
subtree_ranged_attack_pattern"]
A --> A1["condition: hp_below 0.3"]
A --> A2["subtree_ref:
subtree_retreat_to_ally"]
B --> B1["condition: enemy_in_close_range 5"]
B --> B2["action: move_away dist=8"]
style C fill:#e8f0fe,stroke:#4285f4
style A2 fill:#e8f0fe,stroke:#4285f4
style B1 fill:#fce8e6,stroke:#ea4335
| องค์ประกอบ | บทบาท |
|---|---|
bt_id |
คีย์สำหรับ git diff·ตามรอยการเปลี่ยนแปลง |
category·tags |
หน่วยสำหรับค้นหา·ใช้ซ้ำ |
subtree_ref |
อ้างอิงแพตเทิร์นร่วม (แก้ที่เดียว → อัปเดต BT หลายตัว) |
description |
ใช้แชร์ให้ดีไซเนอร์·นักเขียนเนื้อเรื่อง |
enemy_in_close_range 5 ที่ระบายสีแดงไว้คือโหนดที่กินเวลาคนไปหนึ่งชั่วโมงในตอนต้นบท เมื่ออยู่ใน json มันถูกจับได้ในการรีวิวโค้ดเพียงครั้งเดียว
ในพฤติกรรมของศัตรูที่มีมากกว่า 100 ตัว มีก้อนที่เกิดซ้ำ ๆ อยู่ เช่น "ถอยไปหลังพันธมิตร" "ถอยไปหลังที่กำบัง" "แพตเทิร์นโจมตีระยะไกล" ถ้าก๊อบสิ่งเหล่านี้ใส่ลงในทุก BT พอจะแก้ลอจิกถอยเพียงตัวเดียว ก็ต้องไล่หาแก้ด้วยมือเป็นร้อยจุด ดังนั้นแพตเทิร์นร่วมจึงถูกแยกออกไปเป็นไฟล์ subtree ต่างหาก แล้วอ้างอิงด้วย subtree_ref เท่านั้น
subtree_library/
├── retreat_patterns/
│ ├── subtree_retreat_to_ally.json
│ ├── subtree_retreat_to_cover.json
│ └── subtree_retreat_random.json
├── attack_patterns/
│ ├── subtree_ranged_attack_pattern.json
│ ├── subtree_melee_combo.json
│ └── subtree_aoe_attack.json
└── reaction_patterns/
├── subtree_react_to_ally_death.json
└── subtree_react_to_player_taunt.json
ทำเช่นนี้แล้ว คำถามอย่าง "ถ้าแก้ subtree นี้ ใครจะได้รับผลกระทบบ้าง" จะไม่ใช่การคาดเดาของคน แต่เป็นผลลัพธ์ของสคริปต์ ตัวตามรอยผลกระทบนั้นเรียบง่าย คือเปิด BT ทุกตัวขึ้นมาดู แล้วเก็บ bt_id ของ BT ที่อ้างอิง subtree นั้น
# bt_impact_tracker.py
import json, glob
def has_subtree_ref(node, target_id):
if isinstance(node, dict):
if node.get("type") == "subtree_ref" and node.get("id") == target_id:
return True
for child in node.get("children", []):
if has_subtree_ref(child, target_id):
return True
return False
def find_affected_bts(subtree_id):
affected = []
for bt_file in glob.glob("bts/*.json"):
bt = json.load(open(bt_file, encoding="utf-8"))
if has_subtree_ref(bt["root"], subtree_id):
affected.append(bt["bt_id"])
return affected
# วิธีใช้
affected = find_affected_bts("subtree_ranged_attack_pattern")
# → ["bt_scholar_archer_v3", "bt_ranger_v2", "bt_sniper_v1", ...]
ในโปรเจกต์ A ของผู้เขียน ฟังก์ชันนี้ถูกผูกไว้กับขั้นตอนคำขอเปลี่ยนแปลง (Pull Request) เมื่อมีใครไปแตะไฟล์ subtree รายการ BT ที่ได้รับผลกระทบจะถูกแปะเป็นคอมเมนต์ใน PR โดยอัตโนมัติ ผู้รีวิวจะได้เห็นข้อเท็จจริงที่ว่า "แก้แพตเทิร์นถอยไปบรรทัดเดียว แต่ศัตรูระยะไกล 12 ตัวเปลี่ยนหมด" ก่อนที่จะ merge
จากตรงนี้ไปคือช่วงที่มีน้ำหนักมากที่สุดของบทนี้ ผู้เขียนมอบหมายให้ AI ร่าง BT ของ NPC ศัตรูตัวใหม่ "จอมเวทฝึกหัด" แล้วนำหนึ่งรอบของการที่คนตรวจสอบ·ปฏิเสธ·ขอใหม่ต่อผลลัพธ์นั้น มาวางไว้ตามจริงโดยไม่ขัดเกลา การไม่ย่อให้ลื่นไหลมีเหตุผลของมัน เพราะว่า AI ทำอะไรผิดอย่างไรในผลลัพธ์ครั้งแรก ลายเส้นของความล้มเหลวนั้นแหละคือทั้งหมดที่บทนี้ต้องการสื่อ
[คอนเซปต์ NPC ศัตรู]
จอมเวทฝึกหัดแห่งกิลด์นักปราชญ์ HP ต่ำ ดาเมจเวทมนตร์สูง
หลบระยะประชิดเป็นลำดับแรก ถอยไปหลังพันธมิตรแล้วค่อยโจมตีด้วยเวทมนตร์
[category] ranged_combatant
[BT อ้างอิง] bt_scholar_archer_v3
(โครงสร้างคล้ายกัน เพียงแต่เปลี่ยนจากโจมตีด้วยธนูเป็นโจมตีด้วยเวทมนตร์)
[enum ฟังก์ชันที่นิยามไว้]
condition: hp_below, enemy_in_close_range, ally_nearby, mana_below
action: move_away, cast_spell, retreat, wait
subtree_ref id (รายการที่ใช้ได้):
- subtree_retreat_to_ally
- subtree_ranged_attack_pattern # อิงธนู ไม่ใช่สำหรับเวทมนตร์
- subtree_magic_attack_pattern
ช่วยเขียนเฉพาะ json ของโหนด root ของ BT ตามคอนเซปต์นี้ ใช้ subtree_ref ให้มากที่สุดและโหนดตรง ๆ ให้น้อยที่สุด
ฟังก์ชันให้อยู่ภายใน enum ข้างบนเท่านั้น อย่าเพิ่มพฤติกรรมที่ไม่มีในคอนเซปต์ ไม่ต้องอธิบาย เอาแค่ json
{
"bt_id": "bt_scholar_apprentice_v1",
"category": "ranged_combatant",
"tags": ["scholar_faction", "magic", "fragile"],
"description": "จอมเวทฝึกหัดแห่งกิลด์นักปราชญ์ หลบระยะประชิด + ถอยไปหลังพันธมิตร + โจมตีด้วยเวทมนตร์",
"root": {
"type": "selector",
"children": [
{
"type": "sequence",
"name": "low_hp_retreat",
"children": [
{"type": "condition", "fn": "hp_below", "param": 0.4},
{"type": "subtree_ref", "id": "subtree_retreat_to_ally"}
]
},
{
"type": "sequence",
"name": "avoid_melee",
"children": [
{"type": "condition", "fn": "enemy_in_close_range", "param": 6},
{"type": "action", "fn": "move_away", "param": {"distance": 10}}
]
},
{
"type": "sequence",
"name": "mana_recover",
"children": [
{"type": "condition", "fn": "mana_below", "param": 0.2},
{"type": "action", "fn": "wait", "param": {"frames": 120}}
]
},
{"type": "subtree_ref", "id": "subtree_ranged_attack_pattern"}
]
}
}
รับผลลัพธ์มาแล้วเอาเข้า lint (จะกล่าวถึงในหัวข้อถัดไป) และอ่านด้วยตา มีสามจุดที่ติด
กิ่งสุดท้ายผิด subtree_ranged_attack_pattern คือแพตเทิร์นโจมตีที่อิงธนู ทั้งที่เขียนหมายเหตุไว้ใน enum ว่า "ไม่ใช่สำหรับเวทมนตร์" แล้ว AI ก็ยังหยิบแพตเทิร์นธนูที่ชื่อคล้ายกันมา จอมเวทฝึกหัดต้องใช้ subtree_magic_attack_pattern — ปฏิเสธ
เพิ่มพฤติกรรมที่ไม่มีในคอนเซปต์ กิ่ง mana_recover (mana_below → wait 120frame) เป็นพฤติกรรมที่ไม่มีในคอนเซปต์แม้แต่บรรทัดเดียว ทั้งที่ตอกย้ำไว้ในพรอมต์แล้วว่า "ห้ามเพิ่มพฤติกรรมที่ไม่ได้ระบุ" แต่ดูเหมือน AI จะตัดสินว่า "เป็นจอมเวทก็ต้องจัดการมานาเป็นเรื่องธรรมดา" แล้วยัดมันเข้าไป ดูสมเหตุสมผลก็จริงแต่เป็นการละเมิดสเปก — ปฏิเสธ
ค่าเกณฑ์การถอยน่าสงสัย hp_below 0.4 เริ่มถอยเร็วกว่า BT อ้างอิง (0.3) จอมเวทฝึกหัดอ่อนแอกว่าก็จริง จึงอาจเป็นความตั้งใจ เพียงแต่ AI ไม่เคยให้เหตุผล จึงเป็นเรื่องที่ดีไซเนอร์ต้องตัดสิน — พักไว้ ให้ดีไซเนอร์ยืนยัน
ช่วยแก้แค่สามจุด กิ่งสุดท้าย subtree_ranged_attack_pattern เป็นแพตเทิร์นธนู
ให้เปลี่ยนเป็น subtree_magic_attack_pattern และกิ่ง mana_recover ไม่มีในคอนเซปต์ ให้ลบทิ้งทั้งก้อน
hp_below 0.4 ให้ย้อนกลับไปเป็นค่าของ BT อ้างอิง (0.3) — ถ้ามีเหตุผลที่จะใช้ 0.4 ก็ใส่ไว้ใน description สักบรรทัด
ที่เหลือคงเดิม เอาแค่ json
{
"bt_id": "bt_scholar_apprentice_v1",
"category": "ranged_combatant",
"tags": ["scholar_faction", "magic", "fragile"],
"description": "จอมเวทฝึกหัดแห่งกิลด์นักปราชญ์ หลบระยะประชิด + ถอยไปหลังพันธมิตร + โจมตีด้วยเวทมนตร์ HP ต่ำจึงคงค่าเกณฑ์การถอย 0.3 ไว้ (เท่ากับ archer)",
"root": {
"type": "selector",
"children": [
{
"type": "sequence",
"name": "low_hp_retreat",
"children": [
{"type": "condition", "fn": "hp_below", "param": 0.3},
{"type": "subtree_ref", "id": "subtree_retreat_to_ally"}
]
},
{
"type": "sequence",
"name": "avoid_melee",
"children": [
{"type": "condition", "fn": "enemy_in_close_range", "param": 6},
{"type": "action", "fn": "move_away", "param": {"distance": 10}}
]
},
{"type": "subtree_ref", "id": "subtree_magic_attack_pattern"}
]
}
}
ผลลัพธ์ครั้งที่ 2 ผ่าน lint และในการตรวจสอบของดีไซเนอร์ได้ปรับระยะ 6 ของ avoid_melee ให้ลงมาหนึ่งขั้นเป็น 5 ให้เท่ากับ archer ก่อนส่งต่อไปยังด่านจำลอง (simulation gate) สิ่งที่ได้เรียนรู้จากตรงนี้ชัดเจน AI เติมโครงร่างของ BT ได้เร็ว แต่เกือบทุกครั้งจะก่อสองความผิดพลาด คือ "ดึง subtree ที่ผิดซึ่งชื่อคล้ายกันเข้ามา" และ "ยัดพฤติกรรมเพิ่มที่ดูสมเหตุสมผลโดยไม่มีในสเปก" สองความผิดพลาดนี้จับได้ด้วยตาคนและด่าน lint เท่านั้น ฉะนั้นผลลัพธ์ของ AI จึงเป็นร่าง ไม่ใช่ฉบับสุดท้าย
BT เชื่อมโยงโดยตรงกับประสบการณ์ของผู้ใช้ ถ้าเหตุที่ศัตรูไม่ยอมหนีทั้งที่อยู่จ่อหน้าหลุดออกไปจนถึงตอนวางจำหน่าย มันจะย้อนกลับมาเป็นคะแนนรีวิว ดังนั้นก่อน merge เครื่องจะตรวจก่อน
| การตรวจ | เมื่อพบการละเมิด |
|---|---|
| โหนดที่เข้าไม่ถึง | alert (กิ่งที่ตัวเลือกไม่มีวันแตะถึงเลย) |
| ความเสี่ยงลูปไม่รู้จบ | บล็อก (sequence ที่วนซ้ำโดยไม่มีเงื่อนไขออก) |
เป้าหมายของ subtree_ref ไม่มีอยู่จริง |
บล็อก |
| ฟังก์ชันแอ็กชัน·เงื่อนไขอยู่นอก enum | บล็อก |
| จำนวนโหนดพุ่งสูง (>500) | alert (แนะนำให้แบ่ง BT) |
| ความแปรปรวนของเวลาตอบสนองของ BT ภายใน category เดียวกัน | alert (สงสัยว่าการปรับสมดุลถอยหลัง) |
ข้อสุดท้ายคือจุดพิเศษของ lint นี้ ถ้า BT ห้าตัวที่ถูกจัดเป็น ranged_combatant เดียวกัน มีเวลาตอบสนองเฉลี่ยในการจำลองห่างกันมาก นั่นคือสัญญาณว่ามีใครทำลายสมดุลของตัวหนึ่งโดยไม่รู้ตัว มันคือกลไกที่จับ "บรรยากาศ" ซึ่งการตรวจแบบสถิต (static) จับไม่ได้ ด้วยสถิติ
ถัดจาก lint แบบสถิตคือการตรวจสอบด้วยการจำลอง รัน BT ในตัวจำลอง 1,000 ครั้งโดยไม่ต้องบิลด์เกมจริงแล้วดึงสถิติออกมา
| การวัด | ช่วงปกติ |
|---|---|
| เวลารอดเฉลี่ย (เมื่อสู้กับผู้เล่นมาตรฐาน) | ค่าเกณฑ์ตาม category |
| ความหลากหลายของแพตเทิร์นโจมตี (เอนโทรปี) | 0.6 ขึ้นไป |
| สัดส่วนพฤติกรรมถอย·เข้าหา | ค่าเกณฑ์ตาม category |
| frame เฉลี่ยที่ใช้ต่อพฤติกรรมหนึ่งครั้ง | ไม่เกิน 60 frame |
โดยไม่ต้องบิลด์ก็ดู "ว่า BT นี้ตายเร็วเกินไปหรือไม่" "วนทำพฤติกรรมเดียวซ้ำ ๆ หรือไม่" ได้ภายใน 5\~10 นาที ถ้ามีสัญญาณผิดปกติขึ้นมา ก็แก้ json แล้วรันจำลองอีกครั้ง การที่รอบนี้หดจากหน่วยวันลงมาเหลือหน่วยนาที คือกำไรที่แท้จริงของการทำให้เป็น json
flowchart LR
P["คน/AI
แก้ไข BT json"] --> L{"lint แบบสถิต"}
L -->|บล็อก| P
L -->|ผ่าน| R["ดีไซเนอร์ตรวจสอบ"]
R -->|ปฏิเสธ| P
R -->|อนุมัติ| S{"จำลอง 1,000 ครั้ง"}
S -->|สัญญาณผิดปกติ| P
S -->|ปกติ| M["merge + สะท้อนผลในบิลด์"]
style L fill:#fef7e0,stroke:#fbbc04
style S fill:#fef7e0,stroke:#fbbc04
style M fill:#e6f4ea,stroke:#34a853
วางสภาพก่อน·หลังการนำมาใช้ในโปรเจกต์ A ของผู้เขียนเป็นตาราง ตัวเลขสัมบูรณ์จะต่างกันไปตามขนาดทีม·แนวเกม จึงเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) เพียงแต่ทิศทางและสัดส่วนเป็นไปตามที่สังเกตได้จริงจากการให้บริการ
| รายการ | ก่อนนำมาใช้ (BT ในตัวเอนจินโดยตรง) | หลังนำมาใช้ (json + เอดิเตอร์) |
|---|---|---|
| เขียน BT ของศัตรูใหม่ 1 ตัว | 1\~2 วัน | 2\~4 ชั่วโมง |
| ทำความเข้าใจผลกระทบของการแก้ BT | พึ่งการประมาณ·ประสบการณ์ | อัตโนมัติ (รายการผลกระทบของ subtree) |
| ตรวจสอบหลังการแก้ | ต้องบิลด์จริง | จำลอง 5\~10 นาที |
| ให้บริการ NPC ศัตรู 100 ตัว | ดีไซเนอร์ 3 คนเต็มเวลา | ดีไซเนอร์ 1\~2 คน |
| เหตุ BT หลังวางจำหน่าย (พฤติกรรมผิดปกติ) | 10\~15 ครั้งต่อกิ่ง (ประมาณโดยผู้เขียน) | 2\~4 ครั้งต่อกิ่ง (ประมาณโดยผู้เขียน) |
สิ่งที่มีความหมายที่สุดคือสองบรรทัดสุดท้ายขยับไปพร้อมกัน โดยปกติถ้าลดคน คุณภาพจะตก แต่ที่นี่จำนวนดีไซเนอร์ลดลงพร้อมกับเหตุก็ลดลงด้วย เพราะเครื่องเข้ามาแบกผลกระทบของการเปลี่ยนแปลงและการตรวจสอบที่คนเคยตามรอยด้วยมือเอาไว้ คุณค่าของการทำให้อัตโนมัติอยู่ที่ "ลดลงพร้อมกับดีขึ้นไปด้วย" นี้ มากกว่าความ "เร็วขึ้น"
ถ้าอ่านบทนี้แล้วสรุปว่า "เราก็มาทำเอดิเตอร์ json BT กันเถอะ" จะเป็นเรื่องลำบาก ที่โปรเจกต์ A ของผู้เขียนเลือกพัฒนาเองเป็นเพราะเงื่อนไขเฉพาะหลายอย่างลงตัวพอดี
| ตัวเลือก | ข้อดี / ข้อเสีย |
|---|---|
| ใช้ BT ในตัวของเอนจินไปเลย | ผสานรวมง่าย / อ่อนเรื่องการแปลง json·diff |
| หยิบยืมไลบรารี BT ภายนอกมาใช้ | ได้เปรียบเรื่องมาตรฐาน / เส้นโค้งการเรียนรู้·ปรับแต่งมีขีดจำกัด |
| เอดิเตอร์ json BT + รันไทม์ที่พัฒนาเอง | อิสระ·ตามรอยได้สูงสุด / ต้นทุนพัฒนาสูง |
เหตุผลที่โปรเจกต์ A เลือกข้อ 3 มีสี่อย่าง
ต้นทุนพัฒนาคือ 1\~2 เดือน จะคุ้มทุนก็ต่อเมื่อ BT ที่เปิดให้บริการแตะ 100\~300 ตัว และมีระยะการให้บริการแบบไลฟ์ที่ยาวนาน ที่ขนาด 30\~50 ตัว คืนทุนไม่ได้ หมายความว่าผลตอบแทนการลงทุน (ROI, Return On Investment) ของการพัฒนาเอง จะออกมาก็ต่อเมื่อทั้งขนาดและระยะการให้บริการได้รับการรับประกันทั้งสองอย่างเท่านั้น ถ้าเป็นทีมเล็ก ควรหยิบเอาแค่หลักการจากบทนี้ไป คือ "บันทึกเป็น json" "อ้างอิงด้วย subtree" "ผลลัพธ์ AI ต้องผ่านด่าน lint + ตรวจสอบ" แล้วใช้เครื่องมือโดยวางทับบนเอดิเตอร์ในตัวหรือไลบรารีภายนอกจะถูกต้องกว่า
| แพตเทิร์น | ทางแก้ |
|---|---|
| จัดการ BT เป็นแอสเซตแบบไบนารีอย่างเดียว | บันทึกเป็น json เพื่อให้ตามรอยด้วย git ได้ |
| ก๊อบวางแพตเทิร์นเดียวกันในทุก BT โดยไม่มี subtree | แยกออกเป็นไลบรารี subtree แล้วอ้างอิง |
| ตามรอยผลกระทบของ BT ด้วยมือ | ผูกสคริปต์วิเคราะห์ผลกระทบเข้ากับ PR |
| ตรวจสอบเฉพาะในบิลด์จริงโดยไม่มีการจำลอง | ใช้งานตัวจำลองที่แยกออกจากบิลด์ |
| ใช้ BT ที่ AI ออกมาโดยไม่ตรวจสอบ | ให้ผ่านด่านสามชั้น lint + ดีไซเนอร์ + จำลอง |
| ไม่วัด ROI ของการพัฒนาเอง | พัฒนาเองเฉพาะเมื่อมีมากกว่า 100 ตัว·ให้บริการแบบไลฟ์ |
นี่คือรอบขั้นต่ำที่ทีมเล็กลองทำได้วันนี้
setup — ลองเขียน BT ของ NPC ศัตรูที่กำลังเปิดให้บริการอยู่หนึ่งตัวเป็น json ด้วยมือ (bt_id, category, tags, root) แยกก้อนแพตเทิร์นถอย·โจมตีร่วมหนึ่งก้อนออกมาเป็น subtree_library/ แล้วอ้างอิงด้วย subtree_ref
prompt — มอบหมายศัตรูตัวใหม่ที่คล้ายกันให้ AI ใช้โครงร่างพรอมต์ของบันทึกเซสชันจริงข้างต้น (คอนเซปต์ + category + BT อ้างอิง + enum ฟังก์ชันที่ใช้ได้ + "ห้ามเพิ่มพฤติกรรมที่ไม่ได้ระบุ" + "เอาแค่ json") ตามนั้นได้เลย
verify — ให้นำผลลัพธ์ของ AI ผ่านสามด่านนี้ก่อนเท่านั้นจึงค่อย merge คือ (1) lint ที่กรองฟังก์ชันนอก enum·subtree ที่ไม่มีอยู่จริง (2) ตาคน (3) การจำลองหรือการตรวจสอบสั้น ๆ ในเกม ต้องตรวจให้แน่ใจว่า AI ยัด "subtree ที่ผิดซึ่งชื่อคล้ายกัน" และ "พฤติกรรมนอกสเปกที่ดูสมเหตุสมผล" เข้ามาหรือไม่
ถ้าไม่มีกำลังจะสร้างเอดิเตอร์ เครื่องมือก็แค่โปรแกรมแก้ไขข้อความกับ git และ bt_impact_tracker.py ความยาว 30 บรรทัดหนึ่งไฟล์ก็เพียงพอ ส่งออก BT ที่เขียนด้วยเอดิเตอร์ในตัวเป็น json สักครั้งแล้วเอาขึ้น git แยกเฉพาะ subtree ออกเป็นไฟล์ต่างหากแล้วอ้างอิง ถ้าผูกสคริปต์ตามรอยผลกระทบไว้กับ commit hook แม้อยู่คนเดียวก็เห็น "ว่าถ้าแก้แพตเทิร์นถอยนี้แล้วศัตรูตัวไหนจะเปลี่ยน" เป็นผลลัพธ์ ไม่ใช่การประมาณ ลำพังนิสัยเพียงข้อเดียวนี้ ก็ทำให้ "ตัวเลขเพียงตัวเดียวกินเวลาหนึ่งชั่วโมง" ในตอนต้นบท ลดลงเหลือการรีวิวโค้ดเพียงบรรทัดเดียว
ในที่ประชุมรีวิวดันเจี้ยน นักออกแบบเลเวล (Level Design) คนใหม่ได้นำดันเจี้ยนของตนเองหนึ่งอันขึ้นจอ ทางเดินแคบ ศัตรูที่เคลื่อนเร็วไล่ตามมาจากด้านหลัง การตัดสินใจหลบหลีกที่จุดแยกทาง มันเป็นดันเจี้ยนที่ทำมาดี ปัญหาคือมันแตกต่างเพียงเล็กน้อยจากที่เราเคยทำมาแล้วในดันเจี้ยนอีกสิบเอ็ดอัน ความเร็วในการไล่ตามของศัตรู จังหวะที่กับดักทำงาน ช่วงเวลาที่จุดแยกทางปรากฏ ไม่มีสิ่งใดเหมือนกันเลย คนใหม่เชื่อว่าตนได้สร้างประสบการณ์ที่ใช้ชื่อเดียวกันคือ "ดันเจี้ยนไล่ล่า" แต่ความรู้สึกที่ผู้ใช้ได้รับนั้นต่างกันไปในแต่ละดันเจี้ยน
สิ่งที่เราตัดสินใจในวันนั้นเรียบง่าย เราจะนิยามประสบการณ์ที่เรียกว่า "การไล่ล่าในทางเดิน" ให้แม่นยำสักครั้งหนึ่ง แล้วตรึงนิยามนั้นเป็นกฎไว้ คราวหน้าเมื่อมีใครจะสร้างดันเจี้ยนไล่ล่า เขาจะไม่เริ่มร่างจากศูนย์ แต่จะดึงนิยามที่ถูกตรึงไว้นั้นออกมาใช้ นี่คือจุดเริ่มต้นของไลบรารีแพตเทิร์น
หากห้อง (room) เป็นหน่วยของพื้นที่ และ BehaviorTree เป็นหน่วยของพฤติกรรม แพตเทิร์นก็คือหน่วยปฏิบัติการที่มัดพื้นที่ พฤติกรรม และเหตุการณ์เข้าด้วยกันเป็นหนึ่งเดียว เมื่อแพตเทิร์นหนึ่งถูกนำกลับมาใช้ซ้ำในดันเจี้ยนหลายอัน ภาระในการผลิตจำนวนมากก็ลดลง และที่สำคัญกว่านั้นคือ ประสบการณ์ที่ผู้ใช้ได้รับจะมีความสอดคล้องกันระหว่างดันเจี้ยนต่าง ๆ
ลองนึกถึงสูตรอาหารในตำราทำอาหารก็จะเข้าใจได้แม่นยำ สูตรหนึ่งหน้าจะมีวัตถุดิบ ลำดับการปรุง ความแรงของไฟ และภาพอาหารที่ทำเสร็จอยู่ด้วยกัน แม้ร้านอาหารจะเปลี่ยนไป หากทำตามสูตรเดียวกันก็จะได้รสชาติเดียวกัน เพียงแต่อนุญาตให้มีการปรับแต่งเล็กน้อยในแต่ละร้านได้ แพตเทิร์นก็เช่นกัน พื้นที่ (ห้อง) พฤติกรรม (BT subtree) เหตุการณ์ (event) ผลลัพธ์ (รางวัล/ระดับความยาก) และคำอธิบายเจตนาของนักออกแบบ ทั้งหมดนี้รวมอยู่เป็นชุดเดียวกัน
เมื่อแพตเทิร์นหนึ่งถูกนิยามแล้ว ก็สามารถสร้างประสบการณ์เดียวกันได้อย่างสอดคล้องในทุกดันเจี้ยน เหมือนสูตรที่ตรวจสอบครั้งเดียวแล้วให้รสชาติเดียวกันในหลายร้าน เพียงแต่สูตรเดียวกันนั้นก็ยังให้มีการปรับแต่งเล็กน้อยในแต่ละร้านได้ การจัดการการปรับแต่งนี้อย่างไรคือครึ่งหนึ่งของการบริหารแพตเทิร์น overrides ที่จะกล่าวถึงต่อไปคือตำแหน่งของเรื่องนี้
หัวใจของไลบรารีแพตเทิร์นคือการตรึงแพตเทิร์นเป็น rulebook (กฎ/คู่มือกฎ) ไว้ แล้วนำมาประกอบกันเพื่อสร้างดันเจี้ยน นักออกแบบไม่ได้ร่างดันเจี้ยนจากจอเปล่า แต่เลือกแพตเทิร์นที่ตรวจสอบแล้วมาวางและปรับแต่งเพียงบางส่วน
flowchart TD
A[สังเกตช่วงเวลาประสบการณ์ที่ดีในเกม] --> B[แยกย่อยเป็นพื้นที่·NPC·เหตุการณ์]
B --> C[จับคู่กับเทมเพลตห้อง·subtree]
C --> D{ผ่านการจำลอง + การทดสอบกับผู้ใช้?}
D -- ไม่ --> B
D -- ใช่ --> E[ลงทะเบียนแพตเทิร์นในไลบรารี
usage_count = 0]
E --> F[(ไลบรารีแพตเทิร์น
5 หมวด · 30~50 อัน)]
F --> G[ออกแบบดันเจี้ยน: เรียกอินสแตนซ์แพตเทิร์น]
G --> H[การวาง placement + การปรับแต่ง overrides]
H --> I{สัดส่วนการปรับแต่ง 20% หรือน้อยกว่า?}
I -- ใช่ --> J[ดันเจี้ยนเสร็จสมบูรณ์
แพตเทิร์น usage_count +1]
I -- ไม่ --> K[พิจารณาแยกเป็นแพตเทิร์นต่างหาก]
K --> B
J --> L[ติดตามผลกระทบของแพตเทิร์น
แก้แพตเทิร์นหนึ่ง → รวมยอดดันเจี้ยนที่ใช้โดยอัตโนมัติ]
L --> F
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class L code;
class F data;
class J pass;
ครึ่งซ้ายของกระแสนี้ (สังเกต→แยกย่อย→จับคู่→ตรวจสอบ→ลงทะเบียน) คือกระบวนการสร้างแพตเทิร์น ส่วนครึ่งขวา (เรียกใช้→วาง/ปรับแต่ง→ทำให้เสร็จ→ติดตาม) คือกระบวนการบริโภคแพตเทิร์น งานสร้างเกิดขึ้นไม่บ่อย ส่วนงานบริโภคเกิดขึ้นบ่อยครั้ง เมื่อไลบรารีถูกบริหารดี ความไม่สมมาตรนี้จะนำไปสู่ประสิทธิภาพในการผลิตจำนวนมาก
โปรเจกต์ A ของผู้เขียนเป็นเกมแนวแอ็กชัน RPG จึงจัดหมวดแพตเทิร์นไว้ห้าหมวด การจัดหมวดนี้ขึ้นอยู่กับแนวเกม หากเป็นเกมสยองขวัญ สัดส่วนของการซุ่มโจมตีและจังหวะของเนื้อเรื่องก็จะต่างไป และหากเป็นเกมปริศนา การต่อสู้ที่ใช้สภาพแวดล้อมก็จะมาเป็นศูนย์กลาง อย่ายึดติดกับการจัดหมวดเองเป็นสรณะ ควรกำหนดก่อนว่าประสบการณ์หลักของเกมตนเองคืออะไร แล้วจึงจับหมวดหมู่
| หมวด | ประสบการณ์หลัก | ตัวอย่าง |
|---|---|---|
| pursuit | ไล่ล่า·หลบหนี | ไล่ล่าในทางเดิน, หลบหนีในหุบเขา |
| ambush | ซุ่มโจมตี·จู่โจม | ซุ่มโจมตีเมื่อเข้าห้อง, ซุ่มโจมตีในมุมอับสายตา |
| puzzle_combat | การต่อสู้ที่ใช้สภาพแวดล้อม | คันโยก·กับดัก + การต่อสู้ |
| boss_phase | เฟสบอส | แพตเทิร์นเฟสบอส 1\~3 |
| narrative_beat | จังหวะเนื้อเรื่อง | ทริกเกอร์ภาพย้อนอดีต, การปรากฏตัวของเพื่อนร่วมทาง |
ภายในห้าหมวด แพตเทิร์นจะคงไว้ราว ๆ สามสิบถึงห้าสิบอัน ตัวเลขนี้มีเหตุผล หากแพตเทิร์นเกินร้อยอัน นักออกแบบจะไม่สามารถจดจำไลบรารีทั้งหมดไว้ในหัวได้ ในวินาทีนั้นไลบรารีก็จะกลายเป็นคลังที่ใช้เวลาในการค้นหา และนักออกแบบก็จะเลือกที่จะร่างใหม่เสียมากกว่า เมื่อไลบรารีเริ่มถูกเมิน เป้าหมายเดิมคือความสอดคล้องก็พังทลาย ดังนั้นการบริหารเพดานจำนวนแพตเทิร์นอย่างมีสติจึงสำคัญพอ ๆ กับการออกแบบหมวดหมู่
แพตเทิร์นหนึ่งถูกตรึงไว้ด้วยไฟล์ YAML หนึ่งหน้า ด้านล่างนี้คือรูปแบบที่ใช้จริงในโปรเจกต์ A ที่นำมาทำให้ไม่ระบุตัวตน (anonymize) แม้จะปิดบังชื่อทรัพย์สินเฉพาะของบริษัทและหมายเลขดันเจี้ยนไว้ แต่โครงสร้างฟิลด์และวิธีบริหารยังคงเดิม
---
pattern_id: pattern_corridor_pursuit_v2
category: pursuit
description: ศัตรูเร็วไล่ตามจากด้านหลังในทางเดินแคบ ผู้เล่นตัดสินใจหลบหลีกที่จุดแยกทาง
tags: [horizontal_corridor, scholar_theme_compatible]
rooms:
- room_template: corridor_long
size: medium
connections_required: 2
- room_template: junction_3way
size: small
connections_required: 3
npc_behaviors:
- subtree_ref: subtree_aggressive_chase
count: 2
- subtree_ref: subtree_ranged_support
count: 1
events:
- type: trap_activation
trigger: room_1_midpoint
- type: enemy_spawn
trigger: room_1_entry
difficulty_modifier: 1.2 # ภาระ 1.2 เท่าเมื่อเทียบกับห้องทั่วไป
reward_modifier: 1.3
clear_time_estimate_sec: 60
art_pack_compatible: [scholar_library, generic_dungeon]
narrative_slots:
- slot: dialogue_during_chase
constraints: [short_dialogue, fear_emotion]
usage_count: 12 # ใช้ในดันเจี้ยน 12 แห่ง
last_modified: 2026-05-18
deprecated: false
---
ไฟล์นี้นิยามส่วนหนึ่งของดันเจี้ยนสิบสองห้องพร้อมกัน น้ำหนักของบรรทัดเดียวที่ว่า usage_count: 12 มาจากตรงนั้น หมายความว่าหากแก้แพตเทิร์นนี้ ดันเจี้ยนสิบสองห้องจะได้รับผลกระทบพร้อมกัน ดังนั้นการแตะต้องไฟล์แพตเทิร์นจึงมีน้ำหนักต่างจากการแก้ห้องเพียงห้องเดียว
การอ้างอิงอย่าง subtree_aggressive_chase หรือ subtree_ranged_support ชี้ตรงไปยัง subtree ที่นิยามไว้ในเอดิเตอร์ BehaviorTree ของหัวข้อ 7.2 หัวใจคือการที่แพตเทิร์นไม่ได้บรรจุ BT ไว้โดยตรง แต่อ้างอิงเท่านั้น เมื่อแก้ BT แพตเทิร์นทุกอันที่อ้างอิง BT นั้นจะตามมาโดยอัตโนมัติ พื้นที่ (เทมเพลตห้อง) และพฤติกรรม (subtree) ถูกบริหารในไลบรารีของแต่ละฝ่าย และแพตเทิร์นทำหน้าที่เป็นเพียงตารางประกอบที่ร้อยทั้งสองเข้าด้วยกัน ค่าตัวเลขอย่าง clear_time_estimate_sec หรือ difficulty_modifier เป็นเพียงค่าปฏิบัติการของสภาพแวดล้อมของผู้เขียน ไม่ใช่ค่าคงที่สากล ต้องวัดด้วยตนเองจากการจำลองและการทดสอบกับผู้ใช้ของเกมตนเองแล้วจึงเติมลงไป
เมื่อออกแบบดันเจี้ยน เราไม่ได้ร่างแพตเทิร์นจากศูนย์ แต่เรียกใช้จากไลบรารี ระบุว่าจะวางไว้ที่ไหน และทับส่วนที่จะทำให้ต่างเฉพาะในดันเจี้ยนนี้ด้วย overrides
---
dungeon_id: dungeon_021_silvermark_library
pattern_instances:
- instance: corridor_pursuit_1
pattern_id: pattern_corridor_pursuit_v2
placement:
- room_id: dungeon_021_room_03
as: corridor_long
- room_id: dungeon_021_room_04
as: junction_3way
overrides:
- field: npc_behaviors.0.subtree_ref
value: subtree_scholar_chase # ตัวแปรธีมนักวิชาการ
- field: events.0.trigger
value: room_1_2nd_third # ปรับตำแหน่งทริกเกอร์เล็กน้อย
---
ตรงนี้ ดันเจี้ยน 021 ใช้แพตเทิร์น "ไล่ล่าในทางเดิน" ตามเดิม เพียงแต่เปลี่ยนศัตรูที่ไล่ตามจากศัตรูทั่วไปเป็นตัวแปรธีมนักวิชาการ และเลื่อนตำแหน่งที่กับดักทำงานจากกลางทางเดินไปด้านหลังเล็กน้อย แพตเทิร์น 80% ยังคงเดิม ปรับแต่งเพียง 20%
สัดส่วนนี้มีหลักฐานที่มาจากประสบการณ์ปฏิบัติการ หากปรับแต่งน้อยเกินไป (ใกล้ 0%) ดันเจี้ยนต่าง ๆ จะดูจำเจราวกับลอกกันมา หากปรับแต่งมากเกินไป (เกิน 50%) มันก็ไม่ใช่แพตเทิร์นเดียวกันอีกต่อไป กลับไปสู่สถานการณ์เดียวกับดันเจี้ยนที่คนใหม่นำมา คือเชื่อว่าเรียกแพตเทิร์นเดียวกันมา แต่ประสบการณ์จริงต่างกันโดยสิ้นเชิง ดังนั้นเราจึงตั้งกฎปฏิบัติการไว้ หาก overrides ของอินสแตนซ์หนึ่งเกินครึ่งหนึ่งของฟิลด์แพตเทิร์น นั่นไม่ใช่การปรับแต่ง แต่เป็นสัญญาณของแพตเทิร์นใหม่ ถึงเวลาแยกเป็นแพตเทิร์นต่างหากแล้ว
หากแก้ pattern_corridor_pursuit_v2 ดันเจี้ยนสิบสองห้องจะได้รับผลกระทบ หากคนติดตามเรื่องนี้ด้วยมือ ก็จะพลาดไปหนึ่งหรือสองห้องอย่างแน่นอน ดังนั้นเราจึงตั้งเครื่องมือเล็ก ๆ ที่ไล่ดูความสัมพันธ์ระหว่างแพตเทิร์นกับดันเจี้ยนโดยอัตโนมัติไว้
# pattern_impact.py
import json
from glob import glob
def find_dungeons_using(pattern_id):
affected = []
for d in glob("dungeons/*.json"):
dungeon = json.load(open(d, encoding="utf-8"))
for inst in dungeon.get("pattern_instances", []):
if inst["pattern_id"] == pattern_id:
affected.append({
"dungeon": dungeon["dungeon_id"],
"instance": inst["instance"],
"has_overrides": bool(inst.get("overrides")),
})
return affected
หัวใจของรายการที่ฟังก์ชันนี้ส่งคืนมาคือแฟล็ก has_overrides ดันเจี้ยนที่ไม่มี overrides ใช้แพตเทิร์นตามเดิม จึงปลอดภัยที่จะอัปเดตโดยอัตโนมัติ ส่วนดันเจี้ยนที่มี overrides การปรับแต่งเฉพาะของดันเจี้ยนนั้นอาจขัดแย้งกับการแก้แพตเทิร์นได้ จึงต้องมีการตรวจสอบโดยมนุษย์เพิ่มเติม
แทนที่จะให้คนต้องรู้สึกถึงน้ำหนักของการแก้ทีละจุด เครื่องมือจะรายงานภายในห้านาทีว่า "การแก้ครั้งนี้กระทบดันเจี้ยน 12 แห่ง โดยในจำนวนนั้น 4 แห่งมีการปรับแต่งจึงต้องดูด้วยตา" คุณค่าที่แท้จริงของเครื่องมือนี้คือการลดความกลัวต่อการเปลี่ยนแพตเทิร์น หากมองไม่เห็นขอบเขตผลกระทบ นักออกแบบก็จะไม่ยอมแก้แพตเทิร์นเลย และไลบรารีก็จะกลายเป็นน้ำนิ่ง
ตรงนี้ผู้เขียนจะรับมือกับคำถามที่ได้รับบ่อยที่สุดตรง ๆ "ให้ AI เขียนแพตเทิร์นเลยไม่ได้หรือ"
คำตอบชัดเจน ไม่ได้ การเขียนแพตเทิร์นหนึ่งอันมีอินไซต์ของนักออกแบบเป็นกระดูกสันหลัง ประสบการณ์ไล่ล่าที่ดีคืออะไร ทำไมจุดแยกทางต้องอยู่ตรงนั้น ทำไมกับดักต้องทำงานที่จุด 2/3 ไม่ใช่กลางทางเดินความตึงเครียดจึงจะมีชีวิต — นี่คือวิจารณญาณของคนที่ได้สัมผัสเกมจริงและเห็นปฏิกิริยาของผู้ใช้ หากให้ AI ร่างแพตเทิร์นจากศูนย์ แพตเทิร์นทั้งหมดจะลู่เข้าสู่รูปแบบที่ราบเรียบและกลาง ๆ ไลบรารีจะเต็มไปด้วย "แพตเทิร์นที่ไม่ผิด" แต่ "แพตเทิร์นที่น่าจดจำ" จะหายไป
แต่นั่นก็ไม่ได้แปลว่า AI ไม่มีอะไรให้ทำ ในห้าขั้นตอนของการเขียนแพตเทิร์น มีสองจุดที่ AI เป็นตัวช่วยอันทรงพลัง
| ขั้นตอน | ผลผลิต | บทบาทของ AI |
|---|---|---|
| 1. สังเกตช่วงเวลาประสบการณ์ที่ดีในเกม | โน้ต | นักออกแบบโดยลำพัง |
| 2. แยกย่อยช่วงเวลานั้นเป็นพื้นที่·NPC·เหตุการณ์ | yaml ฉบับร่าง | นักออกแบบโดยลำพัง |
| 3. จับคู่กับเทมเพลตห้อง·subtree ที่มีอยู่ | การจับคู่ผู้สมัคร | AI ช่วย (แนะนำผู้สมัคร) |
| 4. การจำลอง + การทดสอบกับผู้ใช้ | การตรวจสอบ | AI ช่วย (รันการจำลอง) |
| 5. ลงทะเบียนในไลบรารี | usage_count = 0 | นักออกแบบโดยลำพัง |
ขั้นที่ 3 คือหัวใจของการช่วยเหลือโดย AI เมื่อนักออกแบบเขียนฉบับร่างว่า "ศัตรูเร็วไล่ตามจากด้านหลังในทางเดินแคบ" การที่คนต้องไล่ค้นเองทั้งหมดว่าอะไรเหมาะกับเจตนานี้ จากเทมเพลตห้องและไลบรารี subtree ที่สะสมไว้นับสิบ ๆ อันแล้วนั้นไม่มีประสิทธิภาพ ตรงนี้เราให้ AI แนะนำผู้สมัครการจับคู่ ด้านล่างนี้คือพรอมต์ที่ใช้จริงที่นำมาทำให้ไม่ระบุตัวตน
[อินพุต]
- yaml ฉบับร่างแพตเทิร์นของนักออกแบบ (แนบด้านล่าง)
- ไลบรารีเทมเพลตห้อง (ชื่อ + แท็ก + รายการขนาด)
- ไลบรารี subtree (ชื่อ + รายการสรุปพฤติกรรม)
ช่วยเลือกและแนะนำเทมเพลตห้องกับ subtree ที่จะใช้สร้างแพตเทิร์นนี้จากไลบรารีให้หน่อย
อย่างละ 2~3 อัน พร้อมความเหมาะสม 1~5 คะแนน และเหตุผลหนึ่งบรรทัดอ้างอิงเกณฑ์แท็ก·ขนาด
สิ่งที่ไม่มีในไลบรารีอย่ากุขึ้นมา ให้แนะนำเท่านั้น ถ้าทั้งหมดต่ำกว่า 3 คะแนน ให้บอกว่าจำเป็นต้องเขียนใหม่
ผลลัพธ์ที่ Claude ส่งคืนมาจริงต่อพรอมต์นี้เป็นดังนี้ เพื่อให้เห็นชัดว่าตรงไหนลังเลและตรงไหนฟันธง ผู้เขียนถ่ายทอดมาโดยไม่แตะแม้แต่ตัวอักษรเดียว
ผู้สมัครเทมเพลตห้อง:
1. corridor_long (ความเหมาะสม 5)
เหตุผล: แท็ก horizontal_corridor ตรงกัน, size medium สอดคล้องกับเจตนา "ทางเดินแคบ"
2. corridor_narrow_short (ความเหมาะสม 3)
เหตุผล: แคบก็จริงแต่ความยาวสั้น จึงยากที่จะมีระยะไล่ล่าเพียงพอ พื้นที่วางจุดแยกทางไม่พอ
3. hall_wide (ความเหมาะสม 1)
เหตุผล: ห้องโถงกว้างตรงข้ามกับเจตนา "ทางเดินแคบ" ความกดดันจากการไล่ล่าไม่มีชีวิต
ผู้สมัคร subtree:
1. subtree_aggressive_chase (ความเหมาะสม 5)
เหตุผล: นิยามพฤติกรรมไล่ตามจากด้านหลังไว้ มีน้ำหนักความเร็วในการเคลื่อนที่
2. subtree_ranged_support (ความเหมาะสม 3)
เหตุผล: ตัวมันเองไม่ใช่การไล่ล่า แต่วางได้ 1 ตัวเพื่อช่วยกดดัน
3. subtree_patrol_basic (ความเหมาะสม 2)
เหตุผล: พฤติกรรมลาดตระเวนไม่ตรงกับเจตนาไล่ล่า ไม่มีโหนดเปลี่ยนเป็นการไล่ล่าหลังทริกเกอร์
เทมเพลตห้องจุดแยกทาง (junction):
ไม่พบรายการในไลบรารีที่ตอบสนองเงื่อนไขจุดแยก 3way + ขนาด small พร้อมกัน
junction_3way ใกล้เคียงที่สุด แต่แท็ก size ว่างอยู่ จึงไม่สามารถ
ยืนยันความเหมาะสมได้ จำเป็นต้องให้นักออกแบบตรวจสอบ
ย่อหน้าสุดท้ายคือส่วนที่มีค่าที่สุดในผลลัพธ์นี้ AI บอกว่า "ไม่แน่ใจ" เกี่ยวกับเทมเพลตห้องจุดแยกทาง และไม่ได้กุคำตอบที่ไม่มีอยู่ขึ้นมา นักออกแบบได้รับรายงานนี้แล้วก็พบความจริงว่าแท็ก size ของ junction_3way ว่างอยู่จริง ๆ จึงเริ่มเติมเมตาดาตานั้นก่อน หาก AI เพิกเฉยต่อแท็กที่ว่างแล้วให้ความเหมาะสม 5 นักออกแบบก็คงเลือกใช้ตามเดิมด้วยความเชื่อมั่นที่ผิดพลาด
นี่คือรูปแบบที่ถูกต้องของการช่วยเหลือโดย AI AI กางผู้สมัครออกมาและแสดงความไม่แน่นอน ส่วนการเลือกและความรับผิดชอบยังคงอยู่ที่นักออกแบบ หากผลการจับคู่มีความเหมาะสมต่ำทั้งหมด เมื่อนั้นก็จะเกิดงานต่างหากในการเขียนเทมเพลตใหม่ และการเขียนนั้นก็กลับมาเป็นงานของคนอีกครั้ง
[ป้ายชี้ทิศ — หากลองบีบอัดแพตเทิร์นเป็น 'เวกเตอร์ประสบการณ์' (ยังเร็วเกินไป)] โปรดอ่านในฐานะแนวโน้มการวิจัย ไม่ใช่ใบสั่งยา §7.3.1 เรียกแพตเทิร์นว่า 'สูตรอาหาร' อยู่แล้ว แพตเทิร์นหนึ่งใกล้เคียงกับค่าพิกัดที่เมตาห้อง·subtree พฤติกรรม·event·difficulty/reward_modifier·clear_time เป็นชุดเดียวกัน หากบีบอัดชุดนี้เป็น 'เวกเตอร์ประสบการณ์' เมื่อความเหมาะสมต่ำทั้งหมด ก็จับพื้นที่ว่างของปริภูมิที่บีบอัดเป็นจุดที่จะเขียนใหม่ แทนที่จะไล่ค้นกระแสข้างต้นทีละอัน และการตัดสิน deprecated ของ §7.3.8 ก็สามารถเสริมด้วยระยะพิกัดของความซ้ำซ้อนที่อยู่ใกล้กันได้ แต่มีเงื่อนไขสามข้อกำกับ difficulty/reward_modifier เป็นค่าปฏิบัติการของผู้เขียนตามที่ §7.3.4 บอกไว้ สเกลของแกนจึงต่างกันไปในแต่ละเกม จึงไม่สามารถยกปริภูมิที่บีบอัดไปใช้ตามเดิมได้ การประมาณค่า (interpolation) เป็นเพียงการ 'ชี้ป้าย' ช่องว่าง ไม่ใช่การ 'สร้าง' แพตเทิร์น และการเขียนแพตเทิร์นจริงบนป้ายนั้นก็ยังไม่ข้ามหลักการของหัวข้อนี้ที่ว่าอินไซต์ของนักออกแบบคือกระดูกสันหลัง แนวคิดนี้อยู่ตำแหน่งเดียวกับการบีบอัดเวกเตอร์มิติของ §8.2.7 และสัญชาตญาณเชิงแนวคิดอยู่ในภาคผนวก M — ขอเก็บไว้เป็นพื้นที่ที่ทีมซึ่งสะสมรากฐานเพียงพอแล้วจะหันมามองในอีกไม่กี่ปีข้างหน้า
ไลบรารีนั้นเก็บกวาดให้ว่างยากกว่าเติมให้เต็ม เมื่อบริหารไปสักหนึ่งปี แพตเทิร์นที่สร้างไว้แต่แทบไม่ได้ใช้ก็จะสะสม หากปล่อยไว้ ต้นทุนในการค้นหาในไลบรารีจะสูงขึ้น และเมื่อนักออกแบบเลือกแพตเทิร์น ก็ต้องไล่ดูตัวเลือกที่ตายแล้วด้วย ดังนั้นจึงเก็บกวาดเป็นระยะ
| เงื่อนไข | การจัดการ |
|---|---|
| usage_count เพิ่ม 0 ใน 6 เดือน | จัดเป็นผู้สมัคร deprecated |
| ตัดสินใจยกเลิกในที่ประชุมพิจารณา | ทำเครื่องหมาย deprecated: true |
| ดันเจี้ยนที่ใช้อยู่เดิม | คงไว้ตามเดิม (การเก็บรักษาเชิงประวัติ) |
| ดันเจี้ยนใหม่ | ห้ามใช้แพตเทิร์นนั้น |
หัวใจคือการยกเลิกไม่ใช่การลบ ดันเจี้ยนที่ใช้แพตเทิร์นนั้นอยู่แล้วก็คงไว้ตามเดิม เพราะการแตะต้องดันเจี้ยนที่กำลังทำงานอยู่ในบริการที่เปิดให้บริการจริง (Live) นั้นอันตรายกว่าการห้ามแพตเทิร์นใหม่ deprecated: true เป็นเพียงป้ายว่า "นับจากนี้อย่าใช้ใหม่" ไม่ใช่คำสั่งให้ลบอดีต
เหมือนหยิบเครื่องมือที่ไม่ได้ใช้ในลิ้นชักโต๊ะออกมาจัดทุกไตรมาส ไลบรารีก็ตั้งกำหนดการเก็บกวาดหนึ่งครั้งต่อไตรมาสไว้ หากไม่มีกำหนดการนี้ ไลบรารีจะพองตัวไปในทิศทางเดียวเท่านั้น และในวินาทีหนึ่งก็จะกลายเป็นคลังที่นักออกแบบเมิน
นี่คือความเปลี่ยนแปลงที่สังเกตได้จากการบริหารไลบรารีแพตเทิร์นมาหนึ่งปีในโปรเจกต์ A ของผู้เขียน ค่าตัวเลขเวลาในตารางด้านล่างเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) มีเพียงทิศทางและสัดส่วนเชิงสัมพัทธ์เท่านั้นที่ถูกสังเกตจริง
| รายการ | ก่อนนำมาใช้ | หลังนำมาใช้ | หมายเหตุ |
|---|---|---|---|
| เวลาออกแบบดันเจี้ยน 1 แห่ง | ราว 2 สัปดาห์ | ราว 1 สัปดาห์ | การประมาณของผู้เขียน ทิศทางชัดเจน |
| ความสอดคล้องของประสบการณ์ระหว่างดันเจี้ยน | กระจายมาก | เสถียร | อิงการประเมินของผู้ใช้ เชิงคุณภาพ |
| ค่าเฉลี่ยดันเจี้ยนที่ใช้ต่อแพตเทิร์น 1 อัน | — | ราว 8 แห่ง | ตัวชี้วัดหลักของประสิทธิภาพการผลิตจำนวนมาก |
| การอนบอร์ดดิงนักออกแบบใหม่ | ราว 2 เดือน | ราว 3 สัปดาห์ | การประมาณของผู้เขียน ผลที่รู้สึกได้มากที่สุด |
| การจับผลกระทบของการเปลี่ยนแพตเทิร์น | ทำมือ 1\~2 วัน | รายงานอัตโนมัติ 5 นาที | ผลของการนำ pattern_impact.py มาใช้ |
ความเปลี่ยนแปลงที่น่าประทับใจที่สุดคือบรรทัดรองสุดท้าย การอนบอร์ดดิงคนใหม่ ไลบรารีแพตเทิร์นทำหน้าที่เป็นตำราออกแบบโดยไม่ได้ตั้งใจ เมื่อคนใหม่สามารถอ่านและเข้าใจ "ประสบการณ์ไล่ล่าของเกมนี้ทำกันแบบนี้" จากไฟล์แพตเทิร์นหน้าเดียว เวลาที่รุ่นพี่ต้องนั่งติดข้าง ๆ คอยอธิบายก็ลดลงมาก ปัญหาดันเจี้ยนที่ต่างกันไปคนละทิศซึ่งคนใหม่นำมาในตอนแรก ก็คลี่คลายได้ด้วยตัวไลบรารีเอง
ตัวเลข "ค่าเฉลี่ยดันเจี้ยนที่ใช้ต่อแพตเทิร์น 1 อันราว 8 แห่ง" ก็คือการนำแพตเทิร์นเดียวกันมาใช้ซ้ำแปดครั้ง และนี่คือมาตรวัดประสิทธิภาพการผลิตจำนวนมากอย่างซื่อตรง เพียงแต่ค่า 8 นี้ขึ้นอยู่กับขนาดดันเจี้ยนและการออกแบบแพตเทิร์นของเกมของผู้เขียน ในเกมที่มีดันเจี้ยนน้อยหรือต้องการคอนเซ็ปต์ต่างกันทุกครั้ง ค่านี้จะเล็กลงมาก
สุดท้าย ต้องพูดเรื่องที่พลิกทั้งบทนี้ลงจึงจะซื่อตรง ไลบรารีแพตเทิร์นไม่ใช่ยาครอบจักรวาล มีสภาพแวดล้อมที่ต้นทุนในการสร้างและบริหารไลบรารีไม่คุ้มทุนอย่างชัดเจน
| เงื่อนไข | คำแนะนำ |
|---|---|
| ดันเจี้ยนต่ำกว่า 5 แห่ง | ทำด้วยมือก็พอ ไม่จำเป็นต้องมีไลบรารี |
| นักออกแบบ 1 คน | ในหัวคือไลบรารี |
| เปิดตัวครั้งเดียว ไม่มีการให้บริการต่อ (Live) | โอกาสนำกลับมาใช้ซ้ำมีน้อย |
| คอนเซ็ปต์ต่างกันโดยสิ้นเชิงทุกครั้ง | สัดส่วนการใช้ซ้ำต่ำ ROI ไม่คุ้มทุน |
ROI (Return on Investment ผลตอบแทนเทียบกับการลงทุน) ของไลบรารีจะคุ้มทุนเมื่อมีสามเงื่อนไขครบพร้อมกัน คือ มีการให้บริการต่อ (Live Ops) นักออกแบบตั้งแต่สามคนขึ้นไป และดันเจี้ยนเกินยี่สิบห้อง MMORPG ที่ให้บริการแบบ Live เป็นเป้าหมายการประยุกต์ใช้ทั่วไปก็ด้วยเหตุนี้ หากโปรเจกต์ของตนเองเข้ากับบรรทัดใดในตารางข้างต้น ก่อนจะสร้างไลบรารีต้องหยุดและคิดใหม่ เครื่องมือมีคุณค่าเฉพาะเมื่อมีปัญหา และสำหรับโปรเจกต์ที่มีดันเจี้ยนห้าห้อง ไลบรารีแพตเทิร์นมีต้นทุนมากกว่าปัญหา
| อาการ | การแก้ไข |
|---|---|
| แพตเทิร์นเกิน 100 อันจนนักออกแบบจำไม่ได้ | จัดให้เหลือ 30\~50 อัน เก็บกวาด deprecated ทุกไตรมาส |
| ติดตามผลกระทบของแพตเทิร์นด้วยมือ (เกิดการตกหล่น) | เครื่องมือติดตามอัตโนมัติอย่าง pattern_impact.py |
| overrides เกิน 80% (ไม่ใช่การใช้ซ้ำจริง) | ปรับแต่งมากเกินไป → แยกเป็นแพตเทิร์นต่างหาก |
| มอบหมายการเขียนแพตเทิร์นให้ AI ทั้งดุ้น | การเขียนคืออินไซต์ของนักออกแบบ AI ช่วยแค่ขั้นที่ 3·4 |
| ไม่วัด usage_count | รวมยอดอัตโนมัติ + พิจารณาในการทบทวนรายไตรมาส |
| ไม่อธิบายไลบรารีให้คนใหม่ | รวมการพาทัวร์ไลบรารีไว้ในเอกสารอนบอร์ดดิง |
บรรทัดที่สองและบรรทัดที่สี่ของตารางนี้คือสิ่งที่ฉุดรั้งบ่อยที่สุด หากไม่ทำให้การติดตามผลกระทบเป็นอัตโนมัติ นักออกแบบจะกลัวการแก้แพตเทิร์นจนไลบรารีแข็งตัว และหากมอบหมายการเขียนให้ AI ไลบรารีก็จะลู่เข้าสู่ค่าเฉลี่ย ความล้มเหลวทั้งสองฆ่าชีวิตของไลบรารี นั่นคือ "การนำประสบการณ์ที่ตรวจสอบแล้วกลับมาใช้ซ้ำ"
ส่วนที่ 7 ได้ก่อร่างสาขาเลเวลขึ้นเป็นสามชั้น ใน 7.1 ได้วางมาตรฐานของเมตาดาตาห้อง·แท็ก·และความเชื่อมโยง (พื้นที่) ใน 7.2 ได้กล่าวถึงเอดิเตอร์ BehaviorTree บนฐาน JSON·subtree·และการจำลอง (พฤติกรรม) และในบทนี้ได้มาถึงไลบรารีแพตเทิร์นที่มัดทั้งสองเข้ากับเหตุการณ์เพื่อนำกลับมาใช้ซ้ำ (หน่วยปฏิบัติการ) จากการบริหารที่ดูแลพื้นที่และพฤติกรรมแยกกัน ปัญหาที่การตัดสินใจในตำแหน่งเดียวกันสั่นคลอนเป็นรูปแบบต่าง ๆ ทุกสัปดาห์ การแก้ปัญหานั้นด้วยการตรึงเป็นกฎในชุดที่เรียกว่าแพตเทิร์น คือเส้นแกนหลักของส่วนที่ 7 ทั้งหมด
กระแสนี้สอดประสานพอดีกับการออกแบบ Layer แบบบูรณาการ วิสัยทัศน์ของโทนพื้นที่ทั้งเกมอยู่ด้านบน ใต้ลงมาคือระบบที่เป็นกฎการสร้างเลเวลและกฎ BT ห้อง·BT·และไลบรารีแพตเทิร์นก่อตัวเป็นชั้นเนื้อหา อินสแตนซ์ดันเจี้ยนและสถิติการใช้แพตเทิร์นสะสมเป็นข้อมูล และ lint·การจำลอง·เทเลเมตรีของผู้ใช้ตรวจสอบสิ่งเหล่านี้ในขั้นบิลด์·QA ไลบรารีแพตเทิร์นเป็นกระดูกสันหลังของชั้นเนื้อหาในห้าชั้นนี้ ขณะเดียวกันก็เป็นข้อต่อที่ด้านบนทำตามกฎของระบบ และด้านล่างสร้างสถิติข้อมูล
patterns/ และ dungeons/ ในโฟลเดอร์งานpattern_impact.py จากเนื้อหาข้างต้นไว้ตามเดิมนักออกแบบเขียน yaml ฉบับร่างแพตเทิร์นด้วยตนเอง (ส่วนนี้เป็นหน้าที่ของคน) จากนั้นมอบหมายให้ AI ทำเฉพาะการจับคู่ ใช้พรอมต์การจับคู่จากเนื้อหาตามเดิม แต่แนบฉบับร่างของตนเองและรายการไลบรารีทั้งสองในอินพุต อย่าตกบรรทัดข้อจำกัดหลักสองบรรทัด
- อย่ากุเทมเพลตใหม่ที่ไม่มีในไลบรารีขึ้นมา ให้แนะนำเท่านั้น
- หากความเหมาะสมต่ำกว่า 3 ทั้งหมด ให้ระบุชัดเจนว่าจำเป็นต้องเขียนใหม่
find_dungeons_using("pattern_...") เพื่อตรวจว่าจับดันเจี้ยนสองห้องนั้นได้ถูกต้องหากทำเกมเล็ก ๆ คนเดียว ระบบไลบรารีก็เกินความจำเป็น แทนที่จะทำอย่างนั้น เพียงเลือกช่วงดันเจี้ยนที่ชอบที่สุดสักช่วงหนึ่งแล้วเขียนประสบการณ์นั้นลง yaml หนึ่งหน้าไว้ก็เพียงพอ เมื่อจะสร้างดันเจี้ยนถัดไป ก็เปิดหน้านั้นมาคัดลอกและแก้เพียง 20% แก่นแท้ของไลบรารีแพตเทิร์น — การนำประสบการณ์ที่ตรวจสอบแล้วกลับมาใช้ซ้ำ — ทำงานได้แม้ในไฟล์เพียงหน้าเดียว เมื่อขนาดใหญ่ขึ้น เมื่อนั้นค่อยเพิ่มหมวดหมู่และเครื่องมือติดตามเข้าไป
เป้าหมายการเรียนรู้ของบทนี้ (ระดับความยาก 🟡 ภาคปฏิบัติ · ความรู้พื้นฐานที่ต้องมี: การบวกลบคูณหาร · การคำนวณตาราง): แยกการปรับสมดุลการต่อสู้ออกเป็นที่ทางของสูตรกับที่ทางของค่าตัวเลข แล้วใช้คุณสมบัติสองอย่างคือความเป็นเชิงกำหนด (determinism) และความสามารถในการสืบย้อน เป็นเกณฑ์แยกแยะว่าควรมอบหมายงานให้ AI ได้ถึงไหน และตรงไหนที่มนุษย์ต้องล็อกไว้ด้วยรูลบุ๊ก
ตีสองของเช้ามืด มีการแจ้งเตือนขึ้นมาว่าอัตราการรอดชีวิตของอาชีพแทงก์บนเซิร์ฟเวอร์ Live แตะ 89% ไม่มีแทงก์คนไหนที่ตีบอสไม่จบ แต่แทงก์ที่ไม่ตายเลยกลับมีมากเกินไป ผมเปิดชีตข้อมูลขึ้นมาเพื่อหาร่องรอยว่าใครเข้าไปแตะอะไร เห็นค่าสัมประสิทธิ์การป้องกันบรรทัดหนึ่ง DEF / (DEF + 1000) ตัวเลข 1000 นี้ ลดจาก 1200 มาเป็น 1000 เมื่อไหร่ ด้วยมือของใคร และอ้างอิงจากอะไร ไม่มีจุดไหนในชีตที่เขียนบันทึกไว้เลย การสืบย้อนจึงเริ่มต้นขึ้น — ต้องไล่ดูล็อกแชต ไล่ดูประวัติบิลด์ และสุดท้ายต้องไปถึงความทรงจำของนักปรับสมดุลที่ลาออกไปเมื่อสามปีก่อนจึงจะจบ
ฉากนี้เป็นสิ่งที่ทุกคนที่เคยดูแลการปรับสมดุลการต่อสู้ต้องเคยเจอสักครั้ง และสาเหตุที่แท้จริงของฉากนี้ไม่ได้อยู่ที่ตัวเลข 1000 นั้นผิด แต่อยู่ที่ตัวเลขนั้นอาศัยอยู่ในที่ทางของ สูตร ทว่ากลับไม่มี ประวัติ การเปลี่ยนแปลงสูตรอยู่ที่ไหนเลย สูตรปรับสมดุลการต่อสู้คือพื้นที่ที่ต้องเป็นเชิงกำหนดที่สุดในเกม และเป็นพื้นที่ที่ต้องสืบย้อนได้มากที่สุด การที่คุณสมบัติสองอย่างนี้กลายเป็นเหตุผลว่าทำไมจึงไม่ควรนำ AI เข้ามาอยู่ในที่ทางนี้ คือกระดูกสันหลังของบทนี้
หนึ่งบรรทัดสำหรับผู้ที่ไม่ได้เรียนสายนี้ z-score · การจำลอง · เส้นโค้งในส่วนนี้จะดูแปลกตาก็ไม่เป็นไร สิ่งเดียวที่ขอให้นำกลับไปคือสิ่งนี้ — "กฎ (สูตร) ที่ต้องให้ผลลัพธ์เดิมเสมอเมื่อใส่อินพุตเดิม จะไม่นำ AI เข้ามา" การแยกแยะระหว่างที่ทางที่ต้องการความเป็นเชิงกำหนดกับที่ทางที่ต้องการการสำรวจนี้ ย้ายไปใช้ได้ตรง ๆ กับทุกสายงานที่จัดการ 'กฎที่ผิดไม่ได้' เช่น ระเบียบบัญชี · ตรรกะการชำระเงิน · ข้อสัญญา ส่วนตัวสูตรเอง ค่อย ๆ ดูจาก 8.1.2 เป็นต้นไปก็ได้
เมื่อทำงานออกแบบเกมมานาน จะจับเอกสารได้สองชนิด เอกสารที่เปลี่ยนบ่อย กับเอกสารที่แทบไม่เปลี่ยน ในการปรับสมดุลการต่อสู้ ฝั่งที่แทบไม่เปลี่ยนคือสูตร "คำนวณดาเมจอย่างไร" จะแตะหนึ่งถึงสองครั้งต่อไตรมาส ส่วน "พลังโจมตีของตัวละครนี้เท่าไหร่" จะแตะห้าถึงหกครั้งต่อสัปดาห์ ถ้านำสองกระแสที่มีความถี่ต่างกันมามัดรวมไว้ในไฟล์เดียวกัน กระดาษที่เปิดปิดนาน ๆ ครั้งจะฉีกขาดด้วยมือที่เปิดปิดบ่อย ๆ
ในโปรเจกต์ A ที่ผู้เขียนดูแลอยู่ การปรับสมดุลการต่อสู้ถูกแยกออกเป็นสองที่ทาง คือที่ทางของสูตร (ในที่นี้เรียกว่า CombatFormula) และที่ทางของค่าตัวเลข (CombatBalance) ขออ้างอิงบรรทัดหนึ่งที่อาศัยอยู่ในที่ทางของสูตรมาตรง ๆ
final_damage = base_damage × dmg_multiplier × (1 − defense_factor) × variation
base_damage = skill_base × ATK × skill_coeff
defense_factor = DEF / (DEF + 1000)
variation = uniform(0.95, 1.05)
สูตรนี้คือรูลบุ๊ก ลองนึกถึงคู่มือกฎของบอร์ดเกม คู่มือกฎเขียนว่า "เคลื่อนที่ได้เท่ากับแต้มที่ทอยลูกเต๋าออกมา" ไม่ได้เขียนว่า "ตานี้ถ้าโชคดีจะเดินได้ไกลกว่านิดหน่อยก็ได้" อินพุตเดิมต้องให้เอาต์พุตเดิมเสมอ นี่คือความเป็นเชิงกำหนด (determinism) ใส่พลังโจมตี 180 พลังป้องกัน 80 สัมประสิทธิ์สกิล 2.1 ไม่ว่าจะคำนวณเมื่อไหร่ ที่ไหน กี่ครั้ง ก็ต้องได้ดาเมจเดียวกัน หากอินพุตเดิมแล้วได้เอาต์พุตต่างกัน นั่นไม่ใช่เครื่องมือปรับสมดุล แต่เป็นเครื่องพนัน
ความเป็นเชิงกำหนดอันเป็นคุณสมบัติเดียวนี้ คือเหตุผลแรกว่าทำไมจึงไม่ควรนำ AI เข้ามาอยู่ในที่ทางนี้ เดี๋ยวจะกลับมาดูอีกครั้ง ก่อนอื่นมาดูกันว่าสูตรควรมีหน้าตาเป็นรูลบุ๊กอย่างไร
พื้นที่หลักของสูตรการต่อสู้ไม่ได้จบที่ดาเมจบรรทัดเดียว อย่างน้อยมีสามบรรทัดที่อาศัยอยู่รวมเป็นชุดเดียวกัน
# ดาเมจ
final_damage = base_damage × dmg_multiplier × (1 − defense_factor) × variation
# คริติคอล
crit_damage = final_damage × crit_multiplier
crit_chance = base_crit + (LUK × 0.1) # เพดาน 50%
# การฟื้นฟู
heal = base_heal × healing_power × (1 − sickness_factor)
การเขียนสามบรรทัดนี้เป็นบล็อกโค้ดแทนภาษาธรรมชาตินั้นมีเหตุผล ภาษาธรรมชาติทิ้งช่องว่างให้ตีความ ประโยค "ยิ่งพลังป้องกันสูง ดาเมจยิ่งลด" ไม่ได้บอกว่าลดแบบเชิงเส้น ลดแบบเส้นโค้ง หรือหยุดตรงไหน แต่ DEF / (DEF + 1000) อ่านได้แบบเดียวเท่านั้น หน้าที่ของรูลบุ๊กคือทำให้ช่องว่างของการตีความเป็น 0
ในสัมประสิทธิ์การป้องกัน DEF / (DEF + 1000) บรรทัดเดียวนี้ มีปรัชญาการปรับสมดุลทั้งหมดของเกมนี้อยู่ ลองวาดบรรทัดนี้เป็นกราฟจะเห็นว่าทำไมจึงเป็นเช่นนั้น แกนนอนคือพลังป้องกัน แกนตั้งคือสัดส่วนที่ลดดาเมจที่ได้รับ
เส้นโค้งนี้ค่อย ๆ แนบเข้ากับเส้นกำกับ (asymptote) อย่างช้า ๆ ที่พลังป้องกัน 1000 จะลดดาเมจลงครึ่งหนึ่งพอดี และหลังจากนั้นไม่ว่าจะเพิ่มขึ้นเท่าไรก็ไม่มีวันแตะ 100% การที่ไร้เทียมทานเป็นไปไม่ได้ ก็อยู่ในบรรทัดเดียวนี้ ถ้าเป็นเชิงเส้นแบบเส้นประสีเทา ที่พลังป้องกัน 1000 จะกันดาเมจได้หมด และเหนือกว่านั้นจะข้ามไปสู่พื้นที่ที่เป็นไปไม่ได้ คือดาเมจติดลบ (ยิ่งโดนตียิ่งฟื้นพลังชีวิต) จึงไม่เลือกใช้เชิงเส้น
ตรงนี้ลองกลับไปที่เหตุการณ์ตีสองของเช้ามืด สมมติว่ามีคนเพิ่ม 1000 นี้เป็น 1200 เส้นโค้งทั้งเส้นจะถูกดันไปทางขวา ด้วยพลังป้องกันเท่าเดิมจะกันดาเมจได้น้อยลง ดังนั้นแทงก์ทั้งเกมจะอ่อนลง และดาเมจต่อชั่วโมงของดีลเลอร์จะสูงขึ้น ค่าคงที่ในสูตรเพียงตัวเดียวเขย่าเกมทั้งเกม ขนาดของผลกระทบต่างจากการเปลี่ยนค่าตัวเลขหนึ่งตัว (พลังโจมตีของตัวละครบางตัว) ความต่างนี้คือเหตุผลที่ต้องวางสูตรกับค่าตัวเลขไว้คนละที่ทาง และเป็นเหตุผลที่การเปลี่ยนแปลงสูตรต้องมีประวัติติดตามมาด้วยเสมอ
เหตุผลเดียวที่การสืบย้อนตีสองของเช้ามืดเป็นนรกก็คือ ไม่มีประวัติการเปลี่ยนแปลง ในโปรเจกต์ A การเปลี่ยนแปลงสูตรไม่ใช่งานแก้โค้ดหนึ่งบรรทัด แต่เป็น งานบันทึกการตัดสินใจหนึ่งครั้ง ข้าง ๆ สูตรจะมีเอกสารแยกชื่อ CombatFormula_Decisions ติดตามอยู่ และในนั้นจะเขียนแบบนี้
## การตัดสินใจ D17 (2026-04-22)
- การเปลี่ยนแปลง: เปลี่ยน defense_factor จาก DEF/(DEF+1000) → DEF/(DEF+1500)
- เหตุผล: ในช่วงเลเวลสูง (LV40+) อัตราการรอดชีวิตของแทงก์อยู่ที่ 89% (วัดจาก Live) เป็นสาเหตุที่ทำให้การสู้บอสยืดเยื้อ
- ลองครั้งที่ 1: จำลองด้วย 800 → อัตราการตายของแทงก์พุ่งสูง เข้าบอสไม่ถึง 1 นาทีก็ถูกล้างหลายครั้ง → ย้อนกลับ
- ลองครั้งที่ 2: จำลองด้วย 1200 → อัตราการรอดชีวิต 75% → ดีแต่สูงกว่าเป้า (60~70%)
- ลองครั้งที่ 3: เลือกใช้ 1500 → อัตราการรอดชีวิตจากการจำลอง 65% (อยู่ในช่วงเป้าหมาย)
- atom ที่ได้รับผลกระทบ: combat_defense_formula, combat_tank_class_balance
- การวัดผลภายหลัง (1 สัปดาห์): อัตราการรอดชีวิตบน Live 67% (เทียบกับที่จำลองทำนายไว้ 65% คือ +2%, อยู่ในช่วง)
บันทึกหนึ่งครั้งนี้ตอบคำถาม "ทำไมถึงกลายเป็นแบบนี้" ของอีกหกเดือนข้างหน้าได้ ที่สำคัญกว่าคือการที่ลองครั้งที่ 1 และลองครั้งที่ 2 ยังคงอยู่ ถ้ามีการบันทึกว่าทำไม 800 ถึงไม่ได้ ทำไม 1200 ถึงไม่ถูกเลือก คนถัดไปก็จะไม่ทำผิดซ้ำแบบเดิม เมื่อนักปรับสมดุลคนใหม่เข้าทีม ชุดล็อกการตัดสินใจชุดนี้จะกลายเป็นเอกสารสำหรับการเรียนรู้งานเริ่มต้น (onboarding) ที่ดีที่สุด
ตรงนี้มีสิ่งหนึ่งที่ต้องพูดอย่างตรงไปตรงมา ค่าตัวเลขจากการจำลองในลองครั้งที่ 1·2·3 ข้างต้น (อัตราการตาย, อัตราการรอดชีวิต 75%, 65%) เป็น ค่าประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) เพื่อแสดงให้เห็นกระแสการดำเนินงาน เกมแต่ละเกมจริง ๆ มีทั้งเส้นโค้งและช่วงเป้าหมายต่างกัน แต่ โครงสร้าง ที่ว่า "การเปลี่ยนแปลงมาพร้อมการลอง การลองมาพร้อมหลักฐานจากการจำลอง และหลังจากเลือกใช้แล้วก็มาพร้อมการวัดผลภายหลัง" นั้นเป็นไปตามการดำเนินงานจริงทุกประการ ในโครงสร้างนี้ ถ้ามีช่องไหนว่างแม้แต่ช่องเดียว ช่องที่ว่างนั้นจะกลายเป็นการสืบย้อนตีสองของเช้ามืดที่ย้อนกลับมา
เมื่อมองที่ทางทั้งสาม คือสูตร ค่าตัวเลข และประวัติ พร้อมกันในภาพเดียว จะเป็นดังนี้
ทีนี้ลองไล่ดูตั้งแต่ต้นว่า D17 ถูกตัดสินใจขึ้นมาอย่างไร นี่คือวิธีที่รูลบุ๊กแบบเชิงกำหนดเคลื่อนไหวในงานจริง
flowchart TD
A["วัดบน Live
ตรวจพบอัตรารอดชีวิตแทงก์ 89%"] --> B["สมมติฐานสาเหตุ
สัมประสิทธิ์ป้องกัน 1000 ปกป้องเกินในช่วงท้าย"]
B --> C["นิยามตัวเลือกการเปลี่ยนแปลง
1500 / 1200 / 800"]
C --> D["Damage Simulator
รันแบบเชิงกำหนด 1,000 ครั้งต่อตัวเลือก"]
D --> E["รายงานผลลัพธ์
อัตรารอดชีวิต · เวลาต่อสู้เฉลี่ย · อัตราชนะ"]
E --> F{"นักปรับสมดุลตัดสิน
อยู่ในช่วงเป้า 60~70% ไหม?"}
F -->|"800: อัตราตายพุ่งสูง"| G["ปฏิเสธ → ล็อกลองครั้งที่ 1"]
F -->|"1200: 75%, ค่อนข้างสูง"| H["พัก → ล็อกลองครั้งที่ 2"]
F -->|"1500: 65%, อยู่ในช่วง"| I["เลือกใช้ → การตัดสินใจ D17"]
I --> J["นำเข้าบิลด์ (ย้อนกลับไม่ได้)"]
J --> K["วัดผลภายหลังบน Live หลัง 1 สัปดาห์
67%, +2% เทียบกับที่ทำนาย"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class D code;
class B,C,F human;
class A,E,K data;
class I pass;
class G fail;
ในกระแสนี้ต้องดูบทบาทของตัวจำลองให้แม่นยำ Damage Simulator รันตัวเลือกทั้งสามตัว ตัวละ 1,000 ครั้ง ในที่นี้ 1,000 ครั้งไม่ได้หมายถึงการทำซ้ำอินพุตเดิม 1,000 ครั้ง เพราะ variation = uniform(0.95, 1.05) ในสูตรซึ่งเป็นตัวเลขสุ่ม ±5% และความน่าจะเป็นคริติคอลซึ่งเป็นตัวเลขสุ่มอีกตัวหนึ่ง ทำให้ผลของแต่ละรอบแต่ละรอบต่างกัน รัน 1,000 รอบเพื่อดู การกระจาย (distribution) ดูอัตราการรอดชีวิตเฉลี่ย กรณีเลวร้ายที่สุด และการกระจัดกระจายของเวลาต่อสู้
ประเด็นสำคัญคือ ตัวจำลองตัวนี้เองก็ต้องเป็นเชิงกำหนด ถ้าให้ค่าตั้งต้นตัวเลขสุ่ม (seed) เดียวกัน 1,000 รอบต้องทำซ้ำได้โดยไม่ผิดแม้แต่ตัวอักษรเดียว เช่นนั้นแล้วบรรทัดหนึ่งของ D17 ที่ว่า "ใช้ 1500 ได้ 65%" จึงจะทำซ้ำได้เหมือนกันและตรวจสอบได้แม้ในอีกหกเดือนข้างหน้า ถ้าตัวจำลองให้ผลต่างกันทุกครั้ง ล็อกการตัดสินใจก็จะกลายเป็นคำโกหก
ผู้เขียนสร้างตัวจำลองดาเมจตัวนี้ครั้งแรกในปี 2008 ตอนนั้นเป็นแมโคร Excel และตอนนี้ในโปรเจกต์ A มันถูกหุ้มเป็นสกิล balance-sim ตลอด 18 ปีที่ผ่านมาเปลือกนอกของเครื่องมือเปลี่ยนไป แต่รูลบุ๊กที่อยู่ข้างในไม่เคยเป็นเชิงความน่าจะเป็นแม้แต่ครั้งเดียว นี่คือหัวใจสำคัญ
ทีนี้มาถึงสิ่งที่บทนี้อยากพูดที่สุด ในยุคที่ AI เข้ามาในแทบทุกที่ทางของการออกแบบเกม มีที่ทางเดียวที่ห้ามนำเข้ามาเด็ดขาด นั่นคือสูตรการต่อสู้และเส้นโค้งรางวัล อันเป็นแกนกลางของความเป็นเชิงกำหนด
LLM (โมเดลภาษาขนาดใหญ่) นั้นเป็นเชิงความน่าจะเป็นโดยเนื้อแท้ ตอบคำถามเดิมต่างกันเล็กน้อยทุกครั้ง นั่นเป็นต้นกำเนิดของพลังในการสร้างข้อความและไอเดียที่ดี แต่เป็นภัยร้ายแรงต่อที่ทางของรูลบุ๊ก ถ้าให้ LLM ตอบ "ตัวละครที่มีพลังป้องกัน 80 จะรับดาเมจเท่าไหร่" วันนี้อาจตอบ 92 พรุ่งนี้อาจตอบ 94 เปรียบเสมือนคู่มือกฎของบอร์ดเกมที่เปลี่ยนความหมายของแต้มลูกเต๋าทุกครั้งที่เปิดหน้าใหม่
เส้นโค้งรางวัลยิ่งอันตรายกว่า "ค่าประสบการณ์ที่ต้องใช้ตอนเลื่อนจากเลเวล 30 ไป 31" เมื่อกำหนดครั้งเดียวก็กำกับความเร็วการเล่นของคนหลายแสนคนพร้อมกัน หากมีความสั่นไหวเพียง ±2% เข้ามา ผู้เล่นบางคนล่ามอนสเตอร์เดียวกันก็ยังเติบโตช้ากว่าคนข้าง ๆ ความเสมอภาคพังทลาย ความเป็นเชิงกำหนดเป็นคำเดียวกับความเป็นธรรม ดังนั้นเส้นโค้งรางวัลจึงให้คนกำหนดด้วยมือ ป้อนลงในชีต และไม่มอบให้ความน่าจะเป็นอีก
แต่ก็ไม่ได้หมายความว่าให้ไล่ AI ออกจากพื้นที่การปรับสมดุลทั้งหมด เส้นแบ่งคือหัวใจสำคัญ
| พื้นที่ | AI | เหตุผล |
|---|---|---|
| การคำนวณสูตรดาเมจ · การฟื้นฟู | ห้ามเด็ดขาด | แกนเชิงกำหนด หากอินพุตเดิม = เอาต์พุตเดิม พังไป จะกลายเป็นเครื่องพนัน |
| เส้นโค้งรางวัล · ค่าประสบการณ์ | ห้ามเด็ดขาด | กำกับการเล่นของคนหลายแสนพร้อมกัน หากสั่นไหว ความเป็นธรรมพังทลาย |
| การคำนวณภายในตัวจำลอง | ห้ามเด็ดขาด | หากทำซ้ำไม่ได้ ล็อกการตัดสินใจจะกลายเป็นคำโกหก |
| การตรวจจับรูปแบบผิดปกติในผลการจำลอง | ทำได้ | ตรวจจับด้วย z-score ว่า "ตัวละครนี้อยู่นอกปกติ" จากผลลัพธ์ 1,000 รายการ |
| การสำรวจตัวเลือกการเปลี่ยนแปลง | ทำได้ | การสำรวจแบบจำกัด เช่น "เสนอตัวเลือก 5 ตัวในช่วง base_atk ±10%" |
| การร่างล็อกการตัดสินใจ | ทำได้ | เนื้อหาการประชุม → ร่างรายการ Decisions (ให้คนตรวจสอบ) |
| การสรุปรายงานการวัดผลภายหลัง | ทำได้ | สรุปข้อมูล Live เป็นภาษาธรรมชาติ |
เส้นแบ่งชัดเจน AI อาศัยอยู่ได้แค่ภายนอกแกนเชิงกำหนดเท่านั้น ฝั่งในที่คำนวณและจำลองคือรูลบุ๊ก ส่วนฝั่งนอกที่วิเคราะห์ เสนอแนะ และเรียบเรียงเป็นข้อความ คือที่ทางของ AI เมื่อข้ามเส้นนี้สักครั้ง อินพุตเดิมจะเริ่มให้ผลต่างกัน และนับจากวินาทีนั้นเครื่องมือปรับสมดุลก็จะสูญเสียความน่าเชื่อถือ
เส้นแบ่งนี้เป็นโครงสร้างเดียวกับระบบเศรษฐกิจที่จะดูใน 8.2 ในเศรษฐกิจก็เช่นกัน สูตรการผลิตและการบริโภคทรัพยากรเป็นเชิงกำหนด ส่วนการตรวจจับรูปแบบเงินเฟ้อเป็นที่ทางของ AI พื้นที่การปรับสมดุลทั้งหมดเคลื่อนไหวด้วยโครงร่างเดียวกัน
ที่ผ่านมาเป็นการประยุกต์แบบอนุรักษ์นิยม ที่คนสร้างตัวเลือกแล้วการจำลองเป็นตัวตรวจสอบ ถ้าไปอีกก้าว เครื่องมือสามารถทำแม้กระทั่งงานสร้างตัวเลือกแทนได้ แต่รูลบุ๊กยังคงเป็นของคนและของความเป็นเชิงกำหนดเช่นเดิม
จุดเริ่มต้นคือการตรวจจับรูปแบบผิดปกติ ดูการกระจายของอัตราชนะ · อัตรารอดชีวิตของแต่ละตัวละครจากผลการจำลอง 1,000 รอบ แล้ววัดด้วย z-score ว่าเบี่ยงเบนจากค่าเฉลี่ยไปกี่เท่าของส่วนเบี่ยงเบนมาตรฐาน ตัวละครที่ z เกิน 2 จะถูกทำเครื่องหมายอัตโนมัติว่า "อยู่นอกช่วงปกติ" แทงก์ของตีสองเช้ามืดก็คงติดการตรวจจับนี้
เพื่อให้การตรวจจับนำไปสู่การเสนอตัวเลือก ต้องมีอีกสองอย่าง อย่างแรกคือ การนิยามพื้นที่การเปลี่ยนแปลง วางช่องอย่าง tunable_range ในชีต CombatBalance เพื่อระบุว่า "ค่าตัวเลขนี้ปรับได้ในช่วงไหน" อย่างที่สองคือ การทำงานคู่ขนานของการจำลอง หากต้องการรันตัวเลือก 10 ตัว × 1,000 รอบ = 10,000 รอบ ภายในเวลาของด่านบิลด์ ต้องมีโครงสร้างพื้นฐานแบบคู่ขนาน
เมื่อสามอย่างนี้ (การตรวจจับด้วย z-score · การนิยามพื้นที่การเปลี่ยนแปลง · การทำงานคู่ขนานของการจำลอง) พร้อม การตัดสินใจที่เหลืออยู่ในมือนักปรับสมดุลก็แคบลงเหลือเพียงอย่างเดียว คือ "จะเลือกตัวเลือกไหน" งานสร้างตัวเลือกจาก 0 กับงานเลือกจากห้าตัวนั้น มีภาระต่างกัน ในที่นี้ก็เช่นกัน สิ่งที่ AI แตะได้มีแค่การเสนอตัวเลือกและการตีความรายงาน ส่วนการคำนวณฝั่งในตัวจำลองและการตัดสินใจเลือกใช้เป็นที่ทางของความเป็นเชิงกำหนดและของคน
สุดท้ายขอย้ำเรื่องความสามารถในการย้อนกลับ ทั้งการแก้ชีตและการรันการจำลองล้วนย้อนกลับได้ จึงย้อนคืนได้ตามใจ ที่ทางเดียวที่ย้อนกลับไม่ได้คือการนำเข้าบิลด์ ค่าตัวเลขที่ออกไปบน Live เมื่อผู้เล่นได้เห็นแล้วก็จะกลายเป็นปฏิกิริยาของชุมชนค้างไว้ ต่อให้ย้อนกลับก็ลบร่องรอยไม่ออก ดังนั้นการตรวจสอบทั้งหมดจึงจบลงก่อนการนำเข้าบิลด์ ในขั้นที่ยังย้อนกลับได้
setup. แยกสูตรการต่อสู้ออกจากคำอธิบายภาษาธรรมชาติ แล้วสร้างเอกสาร CombatFormula ที่เขียนเป็นบล็อกโค้ดเท่านั้น พร้อมเอกสารล็อก CombatFormula_Decisions เปล่า ๆ ไว้ข้าง ๆ ส่วนค่าตัวเลขให้แยกออกเป็นชีตต่างหาก (CombatBalance)
prompt. ใช้ AI เฉพาะกับ การวิเคราะห์ · การร่าง เท่านั้น ไม่ใช่การเปลี่ยนสูตร ตัวอย่างเช่น ให้ CSV ผลการจำลองไป แล้วขอแบบนี้
จากผลการจำลอง 1,000 ครั้งที่แนบมา ช่วยคำนวณ z-score ของอัตราชนะแยกตามตัวละคร
แล้วจัดตัวละครที่ z>2 เป็นตาราง สำหรับแต่ละตัวละคร
ช่วยประมาณพร้อมเหตุผลว่าค่าตัวเลขใด (พลังโจมตี/พลังป้องกัน/สัมประสิทธิ์สกิล)
มีโอกาสสูงที่จะเป็นสาเหตุของความผิดปกติ อย่าแก้ตัวเลขเอง — เสนอแค่ตัวเลือกเท่านั้น
verify. อย่าเชื่อตัวเลือกที่ AI เสนอมาทั้งดุ้น ป้อนค่าตัวเลขของตัวเลือกลงในชีต CombatBalance โดยตรง แล้วใช้ Damage Simulator (หรือ balance-sim) ให้ seed เดียวกันแล้วรัน 1,000 ครั้งใหม่ ตรวจสอบสองอย่าง (1) ผลการจำลองอยู่ในช่วงเป้าหมายไหม (2) รันด้วย seed เดียวกันอีกครั้งแล้วทำซ้ำได้โดยไม่ผิดแม้แต่ตัวอักษรเดียวไหม ถ้าผ่านทั้งสองข้อจึงเลือกใช้ และทันทีที่เลือกใช้ ให้เขียนเหตุผล · การลอง (รวมตัวเลือกที่ถูกปฏิเสธ) · ค่าที่ทำนายไว้ลงใน _Decisions แล้วหลังนำเข้าบิลด์ 1 สัปดาห์ ให้เพิ่มค่าที่วัดจาก Live ลงในล็อกนั้น
แม้จะเป็นการพัฒนาคนเดียวที่ไม่มีทั้งทีมและตัวจำลอง โครงร่างก็ทำงานเหมือนกันทุกประการ ให้บันทึกสูตรเป็นบล็อกโค้ดไว้ในคอมเมนต์ของโค้ดหรือใน .md แยกหนึ่งหน้า แล้ววาง ## ประวัติการเปลี่ยนแปลง ไว้ล่างสุดของไฟล์นั้น ถ้าเปลี่ยนค่าคงที่ของสูตรแม้แต่ตัวเดียว ให้เขียนวันที่ · เหตุผล · ค่าก่อนเปลี่ยน ลงไปหนึ่งบรรทัด ตัวจำลองใช้ลูป Python 30 บรรทัดก็พอแล้ว ตรึง seed ตัวเลขสุ่มไว้ ใส่ค่าตัวเลขของตัวละครลงในสูตร รัน 1,000 ครั้ง แล้วต่อให้พิมพ์ออกมาแค่อัตราชนะเฉลี่ย ก็ก้าวข้ามจาก "เปลี่ยนด้วยความรู้สึก" ไปสู่ "เปลี่ยนด้วยหลักฐาน" ได้ ส่วน AI ให้ใช้แค่ในงานอ่าน CSV ผลลัพธ์นั้นแล้วสรุปว่า "ตัวละครไหนผิดปกติ" เท่านั้น มีเพียงเรื่องเดียวที่ไม่ว่าจะขนาดเท่าไรก็อย่าทำ คือการให้ LLM คำนวณสูตรหนึ่งบรรทัด
ผู้อ่านกลุ่มหลัก: นักออกแบบบาลานซ์/ระบบของ MMORPG ที่รับผิดชอบเศรษฐกิจในเกมที่ให้บริการจริง (ทีมขนาดกลาง 10\~50 คน) ฉบับย่อสำหรับผู้อ่านคนเดียว/มือสมัครเล่น: §8.2.10 「ถ้าทำคนเดียวก็แค่นี้พอ」
ครั้งแรกที่ผมรู้ว่าเงินทอง (gold) เริ่มรั่วไม่ใช่จากใบเรียกเก็บเงิน แต่จากตลาดประมูล สองเดือนหลังเปิดให้บริการ ราคาหินตีบวก (강화석) ค่อย ๆ ขยับขึ้น แล้วอีกหนึ่งเดือนต่อมาก็พุ่งขึ้นเป็นสองเท่า ผมนัดประชุมเพื่อตามหาสาเหตุ แต่สิ่งที่ออกมาจากห้องประชุมล้วนเป็น "ความรู้สึก" ทั้งสิ้น บางคนบอกว่ารางวัลจากดันเจี้ยนใหม่มากเกินไป บางคนบอกว่าเป็นเพราะประสิทธิภาพของจุดล่ามอนสเตอร์สูงขึ้น บางคนก็บอกแค่ว่าผู้เล่นเลเวลสูงเพิ่มจำนวนขึ้น ทุกข้อฟังดูสมเหตุสมผล และด้วยเหตุนั้นเองจึงไม่มีอะไรถูกตัดสินใจสักอย่าง เผาเวลาไปหนึ่งชั่วโมงกับการคาดเดา แล้วจบลงที่ "เอาเป็นว่าสัปดาห์หน้ามาดูข้อมูลเพิ่มกัน"
ปัญหาอยู่ที่ทรัพยากรไม่ได้มีแค่ชนิดเดียว เงินทอง·หินตีบวก·ค่าชื่อเสียง·เกียรติยศ·หินวิญญาณ ต่างก็มี source (ทางที่ไหลเข้า) และ sink (ทางที่ไหลออก) ของตัวเอง และทางเหล่านั้นต่างป้อนซึ่งกันและกัน บอสที่ดรอปหินตีบวกก็ดรอปเงินทองด้วย อุปกรณ์ที่ซื้อด้วยเงินทองก็เผาหินตีบวกไป เมื่อทรัพยากร 5 ชนิดพันกันด้วยกระแสไหลหลายสิบเส้น เครื่องคิดเลขในหัวก็ไม่อาจคำนวณแม้แต่สมดุลรายสัปดาห์ของทรัพยากรเพียงชนิดเดียวออกมาได้อย่างซื่อตรง บทนี้ว่าด้วยการย้ายความพันกันนั้นไปเป็น โมเดลโหนดของ Machinations และวิธีทำให้การตัดสินใจเปลี่ยนแปลงเศรษฐกิจผ่าน ด่านซิมูเลชัน (simulation gate) แทนการคาดเดาในที่ประชุม ทฤษฎีทั่วไปของการออกแบบเศรษฐกิจมีในหนังสือเล่มอื่นอย่างเพียงพออยู่แล้ว บทนี้จึงโฟกัสเฉพาะ จุดที่นำทฤษฎีนั้นไปรันด้วยเวิร์กโฟลว์ AI เท่านั้น
บันทึกจากการให้บริการจริงของผู้เขียน กรณีศึกษาในบทนี้นำมาจากเอกสารโครงการนำร่องด้านเศรษฐกิจ (
Economy_Machinations_Pilot) ที่ผู้เขียนกำลังดำเนินอยู่ในโฟลเดอร์ R&D ของบริษัท และพื้นที่ทำงานวิจัยด้านเศรษฐกิจ โดยทำให้เป็นนิรนาม (anonymized) แล้ว ชนิดของทรัพยากร·โครงสร้าง source/sink·4 ขั้นของ Pilot ถ่ายทอดมาจากการให้บริการจริงอย่างซื่อตรง ส่วนชื่อเฉพาะของบริษัท·ตัวเลขจริงนั้นถูกแทนที่ด้วยค่าสำหรับหนังสือ หรือเขียนไว้เพียงในรูปอัตราส่วน·ทิศทาง เนื้อหาผลลัพธ์ของ AI เป็นการเรียบเรียงใหม่จากเซสชันจริง
ถ้าเขียนทรัพยากรเศรษฐกิจลงในตาราง มันมีแค่ห้าบรรทัด ดูเรียบง่าย กับดักไม่ได้อยู่ที่ทรัพยากร แต่อยู่ที่จำนวนของ กระแสไหล ที่เชื่อมทรัพยากรเข้าด้วยกัน
| ทรัพยากร | source (ไหลเข้า) | sink (ไหลออก) |
|---|---|---|
| เงินทอง | ล่ามอนสเตอร์, รางวัลเควสต์, ขายในตลาดประมูล | ซื้ออุปกรณ์, ตีบวก, ซ่อม, ภาษี |
| หินตีบวก | บอสดันเจี้ยน, อีเวนต์ | ตีบวกอุปกรณ์, สังเคราะห์ |
| ค่าชื่อเสียง | ไซด์เควสต์ | ร้านค้าของกลุ่มอิทธิพล, เปลี่ยนอาชีพ |
| เกียรติยศ | PvP, สงครามกิลด์ | ร้านค้า PvP, สิ่งปลูกสร้างกิลด์ |
| หินวิญญาณ | สังหารบอส | ชุบชีวิตตัวละคร, เรียนสกิล |
ทรัพยากรมี 5 ชนิด แต่ source·sink รวมกันแล้วยี่สิบกว่าเส้น ยิ่งไปกว่านั้นทรัพยากรยังแปลงข้ามกันเองได้ (ตลาดประมูลที่ซื้อหินตีบวกด้วยเงินทองนั้นเป็นทั้ง sink ของเงินทองและ source ของหินตีบวก) ทันทีที่กระแสไหลป้อนซึ่งกันและกัน คำถามอย่าง "ถ้าปล่อยเงินทองเพิ่มอีก 5% ราคาหินตีบวกจะเป็นยังไง" ก็ไม่อาจตอบได้ด้วยการมองทรัพยากรเพียงชนิดเดียว นี่คือจุดที่บาลานซ์ตัวละคร (8.1) กับบาลานซ์เศรษฐกิจต่างกันอย่างชี้ขาด บาลานซ์ตัวละครปิดจบได้ด้วยสมการบรรทัดเดียว แต่เศรษฐกิจเป็น ระบบพลวัตที่สะสมไปตามเวลา ดังนั้นต่อให้สมดุลรายสัปดาห์ใกล้ศูนย์ พอสะสมไป 26 สัปดาห์ ตลาดประมูลก็พังลงได้
ดังนั้นแก่นแท้ของงานเศรษฐกิจไม่ใช่ "การเลือกตัวเลขให้ดี" แต่เป็น "การมองด้วยซิมูเลชันว่ากระแสไหลจะสะสมไปตามเวลาอย่างไร" และงานสร้าง·แก้ไขโมเดลซิมูเลชันนั้นด้วยมือก็น่าเบื่อ และทุกครั้งที่ทำก็มักมีอะไรหล่นหาย งานร่างที่ทำซ้ำ ๆ และตกหล่นง่าย แต่การตรวจสอบต้องให้มนุษย์กุมไว้แน่น — งานลักษณะนี้คือจุดที่เส้นแบ่งงานระหว่าง AI กับมนุษย์ถูกขีดได้สะอาดที่สุด
ก่อนอื่น ขอวางโครงของวัฏจักรเศรษฐกิจที่บทนี้ครอบคลุมไว้เป็นภาพหนึ่งภาพ
%%{init: {"flowchart": {"defaultRenderer": "elk"}}}%%
flowchart LR
subgraph SRC["source (ผลิตทรัพยากร)"]
H["จุดล่ามอนสเตอร์"]
Q["เควสต์"]
B["บอสดันเจี้ยน"]
PVP["PvP/สงครามกิลด์"]
end
subgraph POOL["pool (เก็บทรัพยากร)"]
G(("เงินทอง"))
S(("หินตีบวก"))
end
subgraph SINK["sink (บริโภคทรัพยากร)"]
UP["ตีบวกอุปกรณ์"]
RP["ซ่อม/ภาษี"]
SH["ร้านค้ากลุ่มอิทธิพล/กิลด์"]
end
H --> G
Q --> G
B --> S
PVP --> SH
G -->|แปลงผ่านตลาดประมูล| S
G --> UP
S --> UP
G --> RP
UP -.->|อุปสงค์หินตีบวก ↑| S
G -.->|"เมื่อไหลเข้าสุทธิ > ไหลออกสุทธิ
เงินเฟ้อสะสม"| POOL
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class G,S data;
เส้นประคือหัวใจของบทนี้ sink ของการตีบวกดึงอุปสงค์หินตีบวกขึ้น จึงดันราคาหินตีบวก (UP -.-> S) และเมื่อการไหลเข้าสุทธิของเงินทองเกินการไหลออกสุทธิ ส่วนเกินนั้นก็สะสมเข้าพูล (pool) ทุกสัปดาห์ กลายเป็นเงินเฟ้อที่พอกพูน เส้นประสองเส้นนี้ตามรอยด้วยการคำนวณมือไม่ได้ จึงต้องมีโมเดล
Machinations คือเครื่องมือที่วาดกระแสไหลของเศรษฐกิจเป็นกราฟโหนด แล้วรันซิมูเลชันบนกราฟนั้น เป็นจุดที่ย้าย mermaid ของ §8.2.1 ไปเป็นโมเดลที่รันได้จริง
| โหนด | บทบาท | ในภาพข้างบน |
|---|---|---|
| Pool | ที่เก็บทรัพยากร | เงินทอง·หินตีบวก |
| Source | ผลิตทรัพยากร | ล่ามอนสเตอร์·เควสต์·บอส |
| Drain | บริโภคทรัพยากร | ตีบวก·ซ่อม·ร้านค้า |
| Converter | แปลงทรัพยากร | ตลาดประมูล (เงินทอง→หินตีบวก) |
| Trigger | จุดกระตุ้นแบบมีเงื่อนไข | อีเวนต์·รางวัลเลื่อนขั้น |
เมื่อสร้างโมเดลเศรษฐกิจด้วยโหนดเหล่านี้แล้วรันซิมูเลชัน 1,000 ครั้ง สิ่งที่ได้ไม่ใช่ผลลัพธ์เดี่ยว ๆ แต่เป็นการแจกแจง (distribution) ลักษณะอย่าง "หลัง 26 สัปดาห์ ราคาเงินทองมัธยฐาน +X%, ผู้เล่นกลุ่มบน 10% อยู่ที่ +Y%" อย่างไรก็ตาม Machinations ไม่ได้ครอบจักรวาล และตัวการนำมาใช้เองก็มีต้นทุน
| ข้อจำกัด | วิธีรับมือ |
|---|---|
| รันแยกจากโค้ดเกม จึงเกิดความไม่สอดคล้องของการซิงค์ | ปรับเทียบรายเดือน/รายไตรมาสด้วย telemetry จริง (§8.2.6) |
| กราฟโหนดยิ่งใหญ่ ความอ่านง่ายยิ่งพัง | แบ่งเป็นซับกราฟตามทรัพยากร เริ่มจากทรัพยากรเดียวก่อน (§8.2.4) |
| ซิมเป็นโมเดลผู้เล่นที่ลดทอนความซับซ้อน | ปรับเทียบด้วยการแจกแจงพฤติกรรมจริง ตั้งเกณฑ์ความคลาดเคลื่อน |
| การตีความผลลัพธ์ขึ้นกับความรู้เชิงโดเมน | ทำให้ด่านที่เชื่อมตัวเลขซิม → การตัดสินใจ เป็นมาตรฐาน (§8.2.5) |
ดังนั้น Machinations ไม่ใช่เครื่องมือที่นำมาใช้แบบไม่มีเงื่อนไข มันคุ้มค่าเมื่อสามเงื่อนไข — ทรัพยากร 5 ชนิดขึ้นไป + กระแสแปลงทรัพยากร + การให้บริการเกม (Live Ops) — มาทับซ้อนกัน เศรษฐกิจอย่างง่ายที่มีทรัพยากร 2\~3 ชนิดใช้ Excel ก็เพียงพอ ในกรณีนั้นการนำ Machinations มาใช้จะทำให้ภาระการดูแลมาถึงก่อนผลที่ได้
ลำพังคำอธิบายเครื่องมือเพียงอย่างเดียวไม่อาจรู้ได้ว่ามันคายอะไรออกมาจริง ๆ เราจะตามหนึ่งรอบของการย้าย 'เงินทอง' เพียงตัวเดียวไปเป็นโมเดล Machinations ตั้งแต่พรอมต์ที่ป้อนเข้าไปจนถึงการที่มนุษย์ปฏิเสธ จนจบ พรอมต์ที่ป้อนสามารถคัดลอกไปใช้ได้ตรง ๆ ส่วนผลลัพธ์เป็นการเรียบเรียงใหม่จากเซสชันจริง
ก่อนอื่น ดึง source·sink ของเงินทองจากชีตข้อมูลออกมาทำเป็นตาราง ไม่ใช่การเขียนใหม่ แต่เป็นการสกัด (extract)
# gold_flows.yaml — กระแสไหลของทรัพยากรเดี่ยว 'เงินทอง' (คัดจากชีตข้อมูลปัจจุบัน)
resource: gold
sources:
- id: hunting # ดรอปจากจุดล่ามอนสเตอร์
trigger: per_kill
note: เส้นโค้งดรอปตามช่วงเลเวลใช้กฎ reward_curve
- id: quest_reward # รางวัลเควสต์
trigger: per_complete
- id: market_sell # ขายในตลาดประมูล
trigger: per_trade
sinks:
- id: gear_buy # ซื้ออุปกรณ์
- id: enhance # ค่าตีบวก
- id: repair # ซ่อม
- id: tax # ภาษีตลาดประมูล (เป็น sink และเป็นหัวใจของการดูดเงินทองกลับคืน)
# การแจกแจงพฤติกรรมผู้เล่น (จำนวนครั้งล่าต่อชั่วโมง·อัตราทำเควสต์สำเร็จ) ยังว่างอยู่ → ให้แสดงไว้ถ้า AI ตั้งสมมติฐาน
ไฟล์แนบ gold_flows.yaml คือทรัพยากรเดี่ยว 'เงินทอง' ที่มี source 3 ตัว·sink 4 ตัว
จงสร้างร่างข้อกำหนดโหนด (node spec) สำหรับย้ายสิ่งนี้ไปเป็นโมเดล Machinations
กฎ:
1) จำแนกแต่ละกระแสไหลตามชนิดโหนด (Source/Drain/Pool/Converter)
2) เสนอสูตรของ 'อัตราไหลที่คาดหวังต่อผู้เล่น 1 คน อิงรายสัปดาห์' ให้แต่ละโหนด
โดยหากต้องใช้สมมติฐานพฤติกรรมผู้เล่น (จำนวนครั้งล่าต่อชั่วโมง·อัตราทำเควสต์สำเร็จ ฯลฯ)
จงระบุสมมติฐานนั้นไว้เป็นบรรทัดแยกต่างหากว่า '★สมมติฐาน' อย่าซ่อนสมมติฐานไว้ในเนื้อหา
3) แยกการไหลเข้ารวมของ source กับการไหลออกรวมของ sink เพื่อคำนวณสมดุลสุทธิ (net) รายสัปดาห์
4) สะท้อนว่าภาษีตลาดประมูล (tax) เป็น sink เดียวที่กำจัดเงินทองออกจากเศรษฐกิจอย่างถาวร
และแยกแยะว่า sink อื่น (ตีบวก·ซ่อม) เป็นเพียงการที่เงินทองเคลื่อนไปยัง NPC
ไม่ใช่การดูดกลับคืน
5) สมมติฐานที่เจ้า 'ไม่มั่นใจ' จงทำเครื่องหมาย ★ แยกไว้ แล้วส่งต่อให้มนุษย์ตัดสินใจ
รูปแบบผลลัพธ์: ตาราง (โหนด | ชนิด | สูตรอัตราไหลรายสัปดาห์ | สมมติฐาน | ระดับความมั่นใจ) ห้ามมีคำอธิบายอื่น
ในพรอมต์นี้ สิ่งที่เขียนยาวจริง ๆ ไม่ใช่คำสั่งให้สร้างโมเดล แต่เป็นกฎว่าจะเขียนสมมติฐานไว้ที่ไหนและอย่างไร ในการสร้างโมเดลเศรษฐกิจ ถ้าปล่อยให้ AI อิสระ มันจะคายตัวเลขที่ดูสมเหตุสมผลออกมาโดยซ่อนสมมติฐานเอาไว้ แล้วต่อให้โมเดลผิด ก็ไม่อาจชี้ได้ว่า ผิดตรงไหน การให้มันดึงสมมติฐานพฤติกรรมผู้เล่นออกมานอกเนื้อหาแล้วตั้งไว้เป็นบรรทัดแยก — น้ำหนักทั้งหมดของคำขอนี้อยู่ตรงนั้น สมมติฐานต้องมองเห็นได้จึงจะตรวจสอบได้
| โหนด | ชนิด | สูตรอัตราไหลรายสัปดาห์ | สมมติฐาน | ความมั่นใจ |
|--------------|-----------|----------------------------------------|------------------|-------|
| hunting | Source | สังหารต่อชม. × ดรอปเฉลี่ย × ชม.เล่นรายสัปดาห์ | ★สัปดาห์ละ 15h, สังหาร 80/h| กลาง |
| quest_reward | Source | จำนวนเดลี่เควสต์ × รางวัล × 7 × อัตราสำเร็จ | ★อัตราสำเร็จ 0.7 | กลาง |
| market_sell | Source | (โอนระหว่างผู้เล่น — ไม่ใช่เพิ่มสุทธิ) | เพิ่มต่อเศรษฐกิจสุทธิ 0 | สูง |
| gear_buy | Drain | ความถี่ซื้อ × ราคาเฉลี่ย | ★สัปดาห์ละ 1.2 ครั้ง | ต่ำ |
| enhance | Drain | ครั้งที่ตีบวก × ค่าใช้จ่ายต่อครั้ง | ★สัปดาห์ละ 8 ครั้ง | กลาง |
| repair | Drain | เวลาล่า × อัตราค่าซ่อม | | สูง |
| tax | Drain | ยอดซื้อขาย × อัตราภาษี (★sink ดูดถาวรเพียงตัวเดียว) | ★อัตราภาษี 5% | สูง |
| ── สมดุลสุทธิ | net | Σsource − Σsink ≈ +(เป็นบวกที่เกณฑ์กิจกรรมกลาง) | เมื่อรวมสมมติฐานข้างบน | ★ต่ำ |
★คำเตือนสมมติฐาน: market_sell เป็นการเคลื่อนเงินทองระหว่างผู้เล่น↔ผู้เล่น จึงเพิ่มต่อปริมาณรวมของเศรษฐกิจ 0
ถ้าบวกมันเป็น source จะนับเงินเฟ้อซ้ำซ้อน ต้องจัดการแยกในโมเดล
★จุดที่ไม่มั่นใจ: ความถี่ของ gear_buy (สัปดาห์ละ 1.2 ครั้ง) เป็นการเดา หากไม่มี telemetry การซื้อจริง
sink อาจถูกประเมินต่ำ/สูงเกินไป → ต้องให้มนุษย์ตัดสินใจ
สิ่งที่มีค่าที่สุดในผลลัพธ์ไม่ใช่ตาราง แต่เป็น สองบรรทัดล่างสุด คือ "★คำเตือนสมมติฐาน" และ "★จุดที่ไม่มั่นใจ" AI รายงานจุดอ่อนของโมเดลตัวเองออกมาเองสองจุด พรอมต์ที่ดีคือพรอมต์ที่ทำให้ AI พูดว่า "สมมติฐานนี้ผมไม่กล้าเชื่อ"
จะเอาผลลัพธ์นี้ใส่เข้าโมเดลตรง ๆ ไม่ได้ หนึ่งในสอง ★ ที่ AI รายงานเป็นข้อบกพร่องที่ทำลายโมเดลจริง ๆ
ทีแรก AI จำแนก market_sell (ขายในตลาดประมูล) เป็น Source แต่การขายในตลาดประมูลเป็น การโอนเงินทองของผู้เล่น A ไปยังผู้เล่น B ไม่ใช่การที่เงินทองเกิดขึ้นใหม่ในเศรษฐกิจ ถ้าบวกสิ่งนี้เข้าการไหลเข้าของ source ก็จะนับเงินเฟ้อซ้ำซ้อน AI ชี้ด้วย ★คำเตือนสมมติฐานเองอยู่ก็จริง แต่ในเนื้อตารางก็ยังทิ้ง market_sell ไว้ในช่อง Source อยู่ดี — รายงานแล้วแต่ไม่ถอดออกจากโมเดล เป็นผลลัพธ์ที่ถูกแค่ครึ่งเดียว นี่ยังเป็นข้อบกพร่องของข้อมูลฝั่งมนุษย์ด้วย ที่อินพุต yaml ไม่ได้ระบุลักษณะของ market_sell (โอนระหว่างผู้เล่น vs สร้างใหม่)
จึงร้องขอใหม่
market_sell เป็นการโอนเงินทองระหว่างผู้เล่น↔ผู้เล่น จึงไม่ใช่ source ต่อปริมาณรวมของเศรษฐกิจ (แก้ส่วนที่
อินพุตตกหล่น) จงเอาโหนดนี้ออกจากการรวม source และให้สะท้อนเข้าโมเดลเพียงในฐานะ 'sink ที่ภาษีตลาดประมูล (tax)
ดูดส่วนหนึ่งของยอดโอนกลับคืนอย่างถาวร' เท่านั้น คำนวณสมดุลสุทธิใหม่
แล้วแสดงผลกระทบของการตัด market_sell ออกที่มีต่อ net เป็นหนึ่งบรรทัด
AI ตอบกลับด้วยโมเดลที่เอา market_sell ออกจาก source แล้วเหลือไว้แต่ภาษีในฐานะ sink ผลคือสมดุลสุทธิ (net) ต่ำลงกว่าที่ประเมินไว้ครั้งแรก — เผยให้เห็นว่าตอนที่ใส่การขายในตลาดประมูลเป็น source ผิด ๆ นั้น เรากำลังประเมินเงินเฟ้อสูงเกินจริงอยู่ การไป-กลับเพียงครั้งเดียวนี้คือหัวใจ ถ้ามนุษย์สร้างเองด้วยมือตั้งแต่ต้นก็กินเวลาครึ่งวันและตัวเองก็จับความผิดพลาดในการจำแนกโหนดได้ยาก แต่ร่างจาก AI + บังคับ "ระบุสมมติฐาน" + ปฏิเสธ 1 ครั้ง ใช้เวลาไม่ถึงหนึ่งชั่วโมง และด้วยโครงสร้างที่มนุษย์เป็นผู้ตัดสิน ★ ที่ AI รายงาน ข้อบกพร่องอย่างการนับซ้ำซ้อนจึงถูกดักก่อนเข้าโมเดล (การประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ — เวลาที่ประหยัดได้ขึ้นกับทีม·จำนวนทรัพยากร จึงควรอ่านเป็นความต่างเชิงโครงสร้างระหว่าง "ทำเองด้วยมือตั้งแต่ต้น" กับ "ร่าง+ตรวจสอบ" มากกว่าค่าสัมบูรณ์)
ต่อให้โมเดลเงินทองหนึ่งตัวปิดจบแล้ว ก็อย่าเอาทรัพยากรทั้งหมดมาสร้างโมเดลทีเดียว การให้บริการของผู้เขียนเองก็ไม่ได้ใส่ทั้งหมดเข้าไปในคราวเดียว แต่เดินตาม 4 ขั้น คือเริ่มจากทรัพยากรเดี่ยว ผ่านการตรวจสอบ·ปรับเทียบ แล้วจึงขยาย
| ขั้น | ขอบเขต | ด่านหลัก |
|---|---|---|
| 1. สร้างโมเดลทรัพยากรเดี่ยว (เงินทอง) | source 3·sink 4, เซสชัน §8.2.3 | จำแนกโหนด·ระบุสมมติฐาน |
| 2. เปรียบเทียบซิม vs จริง | net รายสัปดาห์ของซิม vs telemetry รายสัปดาห์ | ผ่านเกณฑ์ความคลาดเคลื่อนหรือไม่ |
| 3. ปรับเทียบความแม่นยำของโมเดล | สะท้อนการแจกแจงพฤติกรรมผู้เล่น (กิจกรรมต่ำ/กลาง/สูง) | วัดความคลาดเคลื่อนซ้ำตาม segment |
| 4. ขยายทรัพยากร (5 ชนิด) | เพิ่มหินตีบวก·ค่าชื่อเสียง·เกียรติยศ·หินวิญญาณ ทีละขั้น | ตรวจสอบกระแสแปลง (ตลาดประมูล) |
การตรวจสอบด้วยการเปรียบเทียบในขั้นที่ 2 คือหัวใจของ 4 ขั้นนี้ ถ้าซิมกับจริงไม่ตรงกัน สิ่งที่ผิดไม่ใช่เกม แต่เป็นโมเดล ถ้าตัดสินใจด้วยโมเดลที่ผิด การตัดสินใจนั้นก็จะย้อนกลับมาเป็นอุบัติเหตุในระบบที่ให้บริการจริง ดังนั้นการขยาย (ขั้นที่ 4) จะทำได้ก็ต่อเมื่อผ่านการตรวจสอบของขั้นที่ 2·3 แล้วเสมอ ถ้าลำดับนี้พังลง คือข้ามการตรวจสอบทรัพยากรเดี่ยวแล้วยัด 5 ชนิดเข้าไปพร้อมกัน ก็จะไม่อาจแยกชี้ได้แม้แต่ว่าโมเดลของทรัพยากรชนิดไหนผิด
เมื่อโมเดลผ่านการตรวจสอบ คราวนี้ก็ตั้ง ด่านซิม (simulation gate) ไว้หน้าการตัดสินใจเปลี่ยนแปลงทุกอย่างที่ส่งผลต่อเศรษฐกิจ เป็นจุดที่เปลี่ยนการตัดสินใจที่เคยปล่อยผ่านด้วย "ความรู้สึก" ในที่ประชุม ให้กลายเป็นการผ่านด่านซิม
| ชนิดการตัดสินใจ | ภาระซิม |
|---|---|
| เพิ่ม source·sink ใหม่ | บังคับ |
| เปลี่ยนอัตราแปลงทรัพยากร (อัตราแลกเปลี่ยนตลาดประมูล ฯลฯ) | บังคับ |
| ออกแบบรางวัลดันเจี้ยน·อีเวนต์ใหม่ | บังคับ |
| เปลี่ยนราคา (±10% ขึ้นไป) | บังคับ |
| ตรวจสอบประสิทธิภาพอาชีพใหม่ | บังคับ |
| เปลี่ยน UI ฯลฯ ที่ไม่เกี่ยวกับเศรษฐกิจ | ยกเว้น |
เราจะลองทำให้เห็นว่าด่านทำงานจริงอย่างไร โดยปล่อยการตัดสินใจหนึ่งให้ผ่านบนโมเดลเงินทองที่ตรวจสอบแล้วใน §8.2.3
[ด่านซิม — การตัดสินใจเรื่องรางวัลอีเวนต์] (เรียบเรียงใหม่จากรูปแบบจริง)
[ข้อเสนอเปลี่ยน] อีเวนต์สุดสัปดาห์: รางวัลล็อกอินรายวัน +500 เงินทอง [ด่าน] เพิ่ม source ใหม่ → ซิมบังคับ [ผลซิม 1000 ครั้ง] - สมดุลสุทธิเงินทองรายสัปดาห์: +6,900 → +10,400 (+50%) - เมื่อสะสม 26 สัปดาห์ ราคาเงินทองมัธยฐาน ~+28% (เตือนเงินเฟ้อ: เกิน ±10%) - ผู้เล่นกิจกรรมสูงกลุ่มบน 10%: ~+41% (ความเหวี่ยงของ segment สูง) [คำตัดสิน] FAIL — เกินช่วงเสถียร (±10%/ระยะยาว) [ข้อปรับแก้] ติด sink พร้อมกันให้กับ source ของอีเวนต์: ร้านค้าเฉพาะอีเวนต์ (ดูดเงินทองกลับ) ซิมใหม่ → สะสม 26 สัปดาห์ +9% (PASS)
ค่าของด่านอยู่ที่สองบรรทัดสุดท้าย ถ้าการตัดสินใจ "ปล่อยรางวัล +500 กันเถอะ" เป็นการคาดเดาในที่ประชุม มันก็คงผ่านด้วย "น่าจะโอเค" ด่านซิมแปลงการตัดสินใจนั้นให้เห็นเป็นเงินเฟ้อ +28% ใน 26 สัปดาห์ และยังบังคับการปรับแก้ไปถึงขั้นว่า ถ้าจะเพิ่ม source ก็ติด sink ไปพร้อมกัน การตัดสินการเปลี่ยนแปลงเศรษฐกิจด้วยการผ่าน/ไม่ผ่านซิมแทนการคาดเดา — นี่คือทั้งหมดของด่าน
ตรงนี้ขอชี้กับดักหนึ่งที่ตกบ่อย คือความเหวี่ยงของ segment ต่อให้เกณฑ์ผู้เล่นกิจกรรมกลางอยู่ที่ +28% แต่กลุ่มบน 10% อยู่ที่ +41% เพราะกลุ่มผู้เล่นที่หาเงินทองได้มากที่สุดสะสมเงินเฟ้อเร็วที่สุด ดังนั้นซิมจึงต้องไม่ดูแค่ค่าเฉลี่ย แต่ต้องรันแยกตาม segment ถ้าดูแค่ค่าเฉลี่ยก็จะพลาดการพังของราคาที่มาจากผู้เล่นกิจกรรมสูง
ด่านซิมจะได้รับความเชื่อถือก็ต่อเมื่อโมเดลไม่หลุดออกจากเกมจริง เกมเปลี่ยนทุกสัปดาห์ โมเดลจึงต้องปรับเทียบตามไปด้วย หลังเปิดให้บริการ เราจะตรวจสอบโมเดลด้วย telemetry จริงทุกเดือน (ในช่วงที่มีการเปลี่ยนแปลงน้อยก็รายไตรมาส)
วัฏจักรปรับเทียบโมเดล (รายเดือน)
─────────────────────────────────
1. ดึง telemetry ผู้เล่นจริง 1 เดือน (รวมยอดกระแสไหลตามทรัพยากร)
2. คำนวณอัตราไหลจริงของ source·sink ตาม segment (กิจกรรมต่ำ/กลาง/สูง)
3. เปรียบเทียบกับซิม Machinations เป็นรายรายการ
4. รายการที่ความคลาดเคลื่อน >15% = ปรับพารามิเตอร์โมเดล (★สมมติฐานของรายการนั้นผิด)
5. หลังปรับ ซิมใหม่ → ใช้เป็นโมเดลฐานของด่านในเดือนถัดไป
หัวใจคือข้อ 4 รายการที่ความคลาดเคลื่อนสูงก็คือสัญญาณว่า "สมมติฐานที่ไม่มั่นใจ" ที่ AI ทำเครื่องหมาย ★ ไว้ใน §8.2.3 นั้นไม่ตรงกับความจริง เช่น ถ้าความถี่ gear_buy ที่ AI เดาไว้ (สัปดาห์ละ 1.2 ครั้ง) วัดจริงได้สัปดาห์ละ 2 ครั้ง ก็เปลี่ยนสมมติฐานนั้นเป็นค่าจาก telemetry ถ้าหยุดการปรับเทียบนี้ โมเดลก็จะค่อย ๆ ห่างออกจากเกม แล้วซิมในด่านของไตรมาสใดไตรมาสหนึ่งก็จะก่ออุบัติเหตุ "ปล่อยผ่านไปแล้วแต่ที่จริงเงินเฟ้อมา" ขึ้น ในวินาทีนั้นความเชื่อถือของตัวซิมเองก็พังลงในภายหลัง การปรับเทียบไม่ใช่งานพ่วงของการให้บริการ แต่เป็นวัฏจักรประจำที่ทำให้ด่านมีชีวิตอยู่
มาถึงตรงนี้คือ 'การประยุกต์เชิงอนุรักษ์' ของการสร้างโมเดลเศรษฐกิจ มนุษย์เป็นผู้เสนอการเปลี่ยนแปลง ตรวจสอบด้วยโมเดล แล้วตัดสินใจด้วยผลลัพธ์ ถ้าก้าวต่อไปอีกหนึ่งก้าว สามแกนของการประยุกต์เชิงก้าวหน้าที่เห็นใน 8.1.6 — ตรวจจับด้วย z-score · นิยามปริภูมิการเปลี่ยนแปลง · รันซิมขนาน — ก็เปิดออกแบบเดียวกันบนโครงสร้างพื้นฐานเศรษฐกิจนี้เช่นกัน
ข้อแรก ตรวจจับรูปแบบผิดปกติ แทนที่จะให้มนุษย์เปรียบเทียบความคลาดเคลื่อนในวัฏจักรปรับเทียบรายเดือน (§8.2.6) ด้วยตา ให้โค้ดเป็นฝ่ายคัดรายการที่ค่าเบี่ยงเบนระหว่างโมเดล-ค่าวัดจริงเกินเกณฑ์ขึ้นมาก่อน การรู้ว่าราคาหินตีบวกกลายเป็นสองเท่าไม่ใช่จากการมองตลาดประมูล แต่ alert ที่ว่า "อัตราไหลของ source หินตีบวกเบี่ยง +30% เทียบกับโมเดล" มาถึงก่อนการประชุม
ข้อสอง นิยามปริภูมิการเปลี่ยนแปลง ไม่ใช่ทวิภาคว่า "ปล่อยรางวัล +500 หรือไม่ปล่อย" แต่ถ้านิยามช่วงรางวัล (0\~+1000) และช่วง sink พร้อมกันให้เป็นปริภูมิการเปลี่ยนแปลงไว้ ก็จะสามารถค้นหาคู่ผสมที่ทำให้เงินเฟ้ออยู่ใน ±10% ภายในปริภูมินั้นได้ มนุษย์กำหนด "จากตรงไหนถึงตรงไหน" ส่วนการค้นหาคู่ผสมที่ดีที่สุดภายในนั้นก็ทำให้เป็นอัตโนมัติ
ข้อสาม รันซิมขนาน แทนที่จะรันข้อเสนอเปลี่ยนหนึ่งข้อ 1,000 ครั้ง ก็รันผู้สมัครหลายสิบตัวภายในปริภูมิการเปลี่ยนแปลงขนานกันตัวละ 1,000 ครั้ง เพื่อเปรียบเทียบการแจกแจงในคราวเดียว จุดที่เคยถกทีละข้อในห้องประชุม ก็เปลี่ยนเป็นการเปรียบเทียบผลซิมของเมทริกซ์ผู้สมัคร
แนวคิดร่วมคือการย้ายจุดที่มนุษย์เคย เสนอ การเปลี่ยนแปลง ไปเป็นจุดที่โค้ด ค้นหา ปริภูมิการเปลี่ยนแปลง แต่ทั้งนี้เป็นเรื่องหลังจากที่การประยุกต์เชิงอนุรักษ์ (§8.2.3\~8.2.6) หมุนอย่างมั่นคงและโมเดลผ่านการตรวจสอบด้วย telemetry แล้ว ถ้าค้นหาปริภูมิการเปลี่ยนแปลงโดยอัตโนมัติด้วยโมเดลที่ยังไม่ผ่านการตรวจสอบ โมเดลที่ผิดก็จะนำเสนอค่าที่ดีที่สุดที่ผิด ๆ ออกมาอย่างมั่นใจ
[การประยุกต์เชิงก้าวกระโดด — บีบอัดเศรษฐกิจเป็น 'เวกเตอร์เชิงมิติ' แล้วค้นหา] (ยังเร็วเกินไปในตอนนี้)
เป็นพื้นที่ที่ก้าวต่อจากการประยุกต์เชิงก้าวหน้าไปอีกหนึ่งก้าว ขอให้อ่านเป็นแนวโน้มงานวิจัย ไม่ใช่การฟันธง (หากเพิ่งเจอเวกเตอร์เชิงมิติ·เอ็มเบดดิงเป็นครั้งแรก ลองดู 'แผนที่' หนึ่งภาพในภาคผนวก M ก่อน แล้วข้างล่างจะอ่านง่ายขึ้น — 'ป้ายบอกทาง' ทั้งห้าของหนังสือเล่มนี้ล้วนหมุนอยู่บนภาพนั้น) โมเดลเศรษฐกิจที่มาถึงตรงนี้เป็นระบบความซับซ้อนสูง ที่ทรัพยากร 5 ชนิดพันกันด้วยกระแสไหลหลายสิบเส้น และการค้นหาปริภูมิการเปลี่ยนแปลงใน §8.2.7 ก็คือวิธีที่สุดท้ายแล้วก็จับกระแสไหลหลายสิบเส้นนั้นมาเป็นพารามิเตอร์ทีละตัวแล้วหมุน ความคิดเชิงก้าวกระโดดคือ บีบอัดความซับซ้อนนี้เองให้เป็นเวกเตอร์เชิงมิติ แล้วค้นหาคำตอบบนปริภูมิที่บีบอัดนั้น
อุปมาหนึ่งที่ดูห่างไกลกลับเป็นเบาะแส สูตรอาหารปรุง ซึ่งมักถูกยกเป็นพื้นที่เชิงคุณภาพที่จับต้องไม่ได้ ในงานวิจัยหนึ่ง (Epicure — Radzikowski·Chen, 2026, arXiv:2605.22391 · เดโม epicure.kaikaku.ai) ได้คัดวัตถุดิบมาตรฐาน 1,790 ชนิด จากสูตรอาหาร 4.14 ล้านสูตรจาก 11 แหล่ง แล้วบีบอัดความสัมพันธ์ระหว่างวัตถุดิบเป็นเวกเตอร์หลายร้อยมิติ แก่นคือ ต่อให้ "รสชาติ" เป็นเป้าหมายเชิงคุณภาพ แต่ถ้าแปลงความสัมพันธ์ระหว่างวัตถุดิบให้เป็นพิกัด สูตรที่คล้ายกันก็จะมารวมตัวกันใกล้ ๆ ในปริภูมิเวกเตอร์ และสามารถ interpolate ระหว่างนั้นเพื่อค้นหาคู่ผสมใหม่ได้ — Epicure เองก็แสดงการค้นหาแบบ interpolate ที่หมุนวัตถุดิบหนึ่งไปในทิศของวงอาหารหนึ่งบนปริภูมิที่บีบอัดนี้ เพื่อหาวัตถุดิบที่สมนัยกัน
เศรษฐกิจก็หลักการเดียวกัน ถ้าแทนสถานะเศรษฐกิจด้วยเวกเตอร์ที่ตั้งให้ source·sink·กระแสแปลง แต่ละอย่างเป็นมิติ "เศรษฐกิจที่เสถียรภายในเงินเฟ้อ ±10%" ก็จะถูกจับเป็นพื้นที่หนึ่งของปริภูมินั้น แล้วแทนที่จะเอาข้อเสนอเปลี่ยนไปเข้าซิมทีละข้อ ก็เปิดทางให้ค้นหาคำตอบโดยตรงในหรือใกล้พื้นที่เสถียรนั้น เป็นความเป็นไปได้ที่ซิมขนานของ §8.2.7 ที่เคยรันผู้สมัครทีละตัวเพื่อเปรียบเทียบ จะถูกย่นเหลือการค้นหาบนปริภูมิที่บีบอัดเพียงครั้งเดียว
ทำไม "ยังเร็วเกินไปในตอนนี้" ข้อแรก การกำหนดว่าจะตั้งอะไรเป็นมิติ (กระแสไหนเป็นอิสระ กระแสไหนเป็นตาม) ก็เป็นโจทย์ยากเชิงโดเมนอยู่ในตัวเอง ข้อสอง การบีบอัดโดยเนื้อแท้คือการทิ้งสารสนเทศ จึงอาจเกิดอุบัติเหตุในระบบจริงในมิติที่ทิ้งไป ข้อสาม ทั้งหมดนี้มีความหมายก็ต่อเมื่อการตรวจสอบ telemetry ของการประยุกต์เชิงอนุรักษ์ (§8.2.6) แน่นหนา — ถ้าโมเดลก่อนบีบอัดหลุดออกจากเกมอยู่แล้ว การบีบอัดก็เพียงบีบความคลาดเคลื่อนนั้นเข้าไปอย่างเรียบร้อยด้วย ดังนั้นหัวข้อนี้จึงไม่ใช่ใบสั่งยา แต่เป็น ป้ายบอกทาง สิ่งที่ต้องทำตอนนี้คือหมุนการประยุกต์เชิงอนุรักษ์อย่างซื่อตรง ส่วนเวกเตอร์เชิงมิตินั้นปล่อยไว้เป็นพื้นที่วิจัยที่ทีมซึ่งสั่งสมรากฐานเพียงพอแล้วจะหันมามองในอีกไม่กี่ปีข้างหน้า
เปรียบเทียบก่อนและหลังนำเครื่องมือมาใช้ เวลา·ความถี่ข้างล่างนี้บรรจุทิศทางที่สัมผัสได้จากการให้บริการช่วงแรกของการนำมาใช้ จึงควรอ่านว่ามันขยับไปทางไหนมากกว่าจะอ่านเป็นค่าสัมบูรณ์ที่แม่นยำ
| รายการ | ก่อนนำมาใช้ (ประชุม·คำนวณมือ) | หลังนำมาใช้ (ด่านซิม) |
|---|---|---|
| ตัดสินใจเปลี่ยนเศรษฐกิจ → นำไปใช้ | 2\~4 สัปดาห์ (วนคาดเดา·ถกซ้ำ) | 1\~3 วัน (ตรวจสอบด้วยซิม 1 ครั้ง) |
| อุบัติเหตุเงินเฟ้อ | 1\~2 ครั้งต่อไตรมาส (พบหลังเกิดเหตุ) | 0\~1 ครั้งต่อไตรมาส (ด่านสกัดล่วงหน้า) |
| ความถี่เพิ่ม source·sink | 1\~2 ครั้งต่อไตรมาส (อนุรักษ์เพราะกลัว) | 1\~2 ครั้งต่อเดือน (ซิมรับประกันความปลอดภัย) |
| ความถี่ประชุมเรื่องเศรษฐกิจ | 3\~4 ครั้งต่อสัปดาห์ | 1\~2 ครั้งต่อสัปดาห์ |
ความหมายของบรรทัดสุดท้ายใหญ่กว่าตัวเลขในตาราง ความถี่ประชุมที่ลดลงเป็นเพราะซิมูเลชันเข้ามาแทนที่การถกเถียง พอ "ผมว่าหินตีบวกน่าจะเงินเฟ้อนะ" กลายเป็น "ผลซิม 26 สัปดาห์ +28%" จุดที่เคยเถียงกันชั่วโมงหนึ่งเรื่องการคาดเดาก็จบลงด้วยการแชร์ผลลัพธ์ 5 นาที นี่ตรงกับแนวคิดที่ตรึงเป็นกฎไว้ในการทบทวนของระบบของผู้เขียน (atom automation_signal_value_over_time_savings — คุณค่าของการทำงานอัตโนมัติไม่ใช่การประหยัดเวลา แต่คือการเปิดเผยสัญญาณ) อย่างแม่นยำ ผลผลิตที่แท้จริงของด่านซิมไม่ใช่เวลาที่ประหยัดได้ แต่คือการทำให้ตัวเลขเข้ามาแทนที่จุดที่การคาดเดาเคยครองในที่ประชุม
แต่มีสิ่งหนึ่งที่ขอวางไว้อย่างซื่อตรง "1\~2 ครั้งต่อไตรมาส → 0\~1 ครั้ง" ในตารางไม่ใช่ค่าวัดที่แม่นยำ แต่เป็นทิศทางที่สัมผัสได้จากการให้บริการ อุบัติเหตุเงินเฟ้อมีจำนวนนับเปลี่ยนไปตามนิยาม (จะถือว่าราคาเหวี่ยง ±กี่ % เป็นอุบัติเหตุ) ดังนั้นจึงควรอ่านเป็นการเปลี่ยนเชิงโครงสร้างว่า "ย้ายจากการพบหลังเกิดเหตุไปเป็นการสกัดล่วงหน้า" มากกว่าจำนวนครั้งสัมบูรณ์
| รูปแบบ | ทำไมจึงล้มเหลว | วิธีรับมือ |
|---|---|---|
| สร้างโมเดลทรัพยากรทั้งหมดในคราวเดียว | แยกไม่ได้ว่าโมเดลของทรัพยากรไหนผิด | เริ่มจาก Pilot ทรัพยากรเดี่ยว (§8.2.4) |
| รับสมมติฐานของโมเดล AI โดยไม่ตรวจสอบ | ข้อบกพร่องอย่างการนับซ้ำซ้อนของตลาดประมูลเข้าไปตรง ๆ | บังคับระบุสมมติฐาน + มนุษย์ปฏิเสธ (§8.2.3) |
| เปลี่ยนเศรษฐกิจโดยไม่มีด่านซิม | ต้นทุนกอบกู้เงินเฟ้อหลังเกิดเหตุมหาศาล | นิยามรายการซิมที่บังคับ (§8.2.5) |
| ซิมแค่ค่าเฉลี่ย เพิกเฉย segment | พลาดการพังของราคาที่มาจากผู้เล่นกิจกรรมสูง | ซิมแยกตาม segment (§8.2.5) |
| ไม่ปรับเทียบด้วย telemetry หลังเปิดให้บริการ | โมเดลห่างจากเกม ความเชื่อถือของด่านพัง | วัฏจักรปรับเทียบราย เดือน/ไตรมาส (§8.2.6) |
| ค้นหาปริภูมิการเปลี่ยนแปลงอัตโนมัติด้วยโมเดลที่ยังไม่ตรวจสอบ | โมเดลที่ผิดคายค่าที่ดีที่สุดที่ผิดออกมาอย่างมั่นใจ | ก้าวหน้าหลังจากการประยุกต์เชิงอนุรักษ์เสถียรแล้ว (§8.2.7) |
ข้อที่สองคือสิ่งที่พลาดบ่อยที่สุด เหมือนการนับซ้ำซ้อนของตลาดประมูลที่เห็นใน §8.2.3 AI คายโมเดลที่ดูสมเหตุสมผลออกมาอย่างมั่นใจ แต่จุดอ่อนของสมมติฐานตัวเองนั้นเพียงรายงานด้วย ★ แล้วก็ทิ้งข้อบกพร่องไว้ในเนื้อหา ถ้ามนุษย์ไม่ตัดสิน ★ นั้น โมเดลที่ผิดก็จะถูกปล่อยผ่าน และการตัดสินใจซิมทั้งหมดที่อยู่บนนั้นก็จะผิดไปด้วยกัน
ถ้าทำคนเดียวก็แค่นี้พอ: ไม่ต้องมีทั้ง Machinations และ telemetry ก็ได้ เลือกทรัพยากรหนึ่งของเกมตัวเอง (หรือเกมที่ชอบ) เขียน source·sink ลงกระดาษ แล้ววางพรอมต์ของ §8.2.3 ลงไปตรง ๆ เพื่อรับร่างโมเดลสมดุลสุทธิรายสัปดาห์ดู ลองเลือกสมมติฐานหนึ่งข้อที่ AI ระบุไว้ด้วย ★ แล้วโต้แย้งว่า "สมมติฐานนี้ผมไม่เชื่อ จงยกเหตุผลมาใหม่" คุณจะรู้สึกถึงเข้าไปในร่างกายว่าโมเดลเศรษฐกิจคือมัดของสมมติฐานชุดไหนกันแน่ — และถ้าสมมติฐานหนึ่งข้อนั้นผิด ข้อสรุปจะพลิกกลับไปอย่างไร
ถ้าทำเป็นทีม เริ่มด้วยขั้นถัดไปหนึ่งขั้นนี้ เลือก ทรัพยากรที่เป็นปัญหามากที่สุดเพียงตัวเดียว (มักเป็นเงินทองหรือหินตีบวก) ไม่ใช่ทรัพยากรทั้งหมด แล้วตั้งแค่โมเดลทรัพยากรเดี่ยวของ §8.2.3 ก่อน จากนั้นวางด่านซิมของ §8.2.5 ไว้กับการตัดสินใจเปลี่ยนเศรษฐกิจชนิดหนึ่ง (เช่น รางวัลอีเวนต์) แค่ทรัพยากร 1 ตัว + การตัดสินใจ 1 ชนิด ก็เปลี่ยนจุดที่เคยเถียงกันเรื่องการคาดเดาในที่ประชุมให้เป็นตัวเลขหนึ่งบรรทัดได้แล้ว
สรุปเป็น setup → prompt → verify คือ — setup: สกัด source·sink ของทรัพยากรที่เป็นปัญหาหนึ่งตัวออกมาเป็น yaml prompt: รับร่างโมเดลโหนดในรูปแบบ §8.2.3 โดยบังคับให้ระบุสมมติฐานพฤติกรรมผู้เล่นด้วย ★ verify: ให้มนุษย์ปฏิเสธ·ร้องขอใหม่ด้วยตัวเองต่อสมมติฐาน ★ ที่ AI รายงานและการจำแนกโหนด (โดยเฉพาะการโอนระหว่างผู้เล่น vs การสร้างใหม่)
เช้ามืดวันหนึ่งในปี 2008 ผมนั่งตรวจเลขชุดเดิมซ้ำสามรอบอยู่หน้าไฟล์ Excel แผ่นเดียว ในเอกสารออกแบบเขียนไว้ว่าตัวละครดาบตัวหนึ่งสร้างความเสียหายต่อวินาทีได้ 847 แต่ตัววันนั้นเองที่ผมรันซิมูเลเตอร์เป็นครั้งแรก โดยใส่ตัวละครตัวเดิมด้วยสเปกเดียวกันทุกอย่าง มันกลับให้ค่าออกมา 612 ห่างกัน 27% หนึ่งในสองค่านี้โกหก และตอนนั้นผมยังไม่รู้ว่าฝั่งไหนคือคำโกหก
DPS ในเอกสารคือคำสัญญาบนกระดาษ เป็นเลขคณิตของความเสียหายต่อสกิลหนึ่งครั้งคูณกับความถี่ในการปล่อยสกิล ส่วน DPS จากซิมูเลเตอร์คือผลที่ได้จากการเอาคำสัญญานั้นไปฟันจริง 1,000 ครั้ง คูลดาวน์ทับซ้อนกัน เวลาถูกท่าร่ายสกิลกินไป คริติคอลไม่ออกตามค่าคาดหวัง — แรงเสียดทานที่กระดาษไม่รู้จักเข้ามาแทรก ช่องว่าง 27% นี้แหละคือที่ทางที่ทำให้นักออกแบบสายปรับสมดุลมีกินมีใช้ ถ้าเชื่อกระดาษ พอเกมออกแล้วก็ได้ร้องไห้
บทนี้คือเรื่องราวของเครื่องมือชิ้นหนึ่ง Damage Simulator ที่ผมสร้างขึ้นในปี 2008 และไม่เคยวางมือจากมันมาจนถึงทุกวันนี้ ผมจะพาไล่ดูว่าผมตามรอยจุดที่เอกสารกับผลลัพธ์แยกทางกันอย่างแม่นยำได้อย่างไร และอีก 18 ปีต่อมาผมเอา AI มาช่วยในการตามรอยนั้นอย่างไร โดยติดตามผ่านบันทึกเซสชันจริง (worked transcript) จริงหนึ่งครั้ง
ก่อนอื่นมาแกะดูตัวตนของ 612 กับ 847 นั้นกัน นักออกแบบรุ่นน้องที่เขียนเอกสารฉบับนี้ (ต่อไปจะเรียกว่าทีมเมเบอร์ A) ไม่ได้ทำอะไรผิดเลย เขาคูณตามที่ตารางสกิลระบุไว้ทุกประการ
การคำนวณ DPS ตามเอกสารหน้าตาเป็นแบบนี้ สมมติว่าตัวละครหนึ่งมีสามสกิล
| สกิล | ความเสียหายต่อครั้ง | คูลดาวน์ | เวลาร่าย |
|---|---|---|---|
| ฟันขวาง | 320 | 3.0s | 0.6s |
| แทง | 540 | 6.0s | 0.9s |
| ตีปกติ | 180 | 1.2s | 0.4s |
การคำนวณในเอกสารของทีมเมเบอร์ A ตั้งอยู่บนสมมติฐานในอุดมคติว่า "ใช้แต่ละสกิลทุกครั้งที่คูลดาวน์หมดโดยไม่พลาดเลย" ฟันขวางสร้าง 320 ทุก 3 วินาที แทงสร้าง 540 ทุก 6 วินาที และตีปกติเข้ามาเติมช่วงเวลาที่ว่าง พอคิดเป็นเลขคณิตก็ได้ออกมาสวยงามที่ 847 บนกระดาษนั้นตัวละครราวกับมีหลายมือ ท่าร่ายสกิลจึงไม่ขวางกันเอง
เหตุผลที่ซิมูเลเตอร์ให้ค่า 612 มีเพียงข้อเดียวคือมันมีมือเดียว ระหว่างที่กำลังร่ายสกิลแทงที่ใช้เวลา 0.9 วินาที ต่อให้คูลดาวน์ของฟันขวางหมดก็ใช้ไม่ได้ การที่ท่าร่ายสกิลกินกันเองแบบนี้คือ การชนกันของ global cooldown ซึ่งในเอกสารไม่มี กระดาษคือสุญญากาศที่ไม่มีแรงเสียดทาน ส่วนซิมคือสนามรบที่มีแรงเสียดทาน ความจริงข้อแรกที่ผมเรียนรู้จากการทำงานนี้มา 24 ปีคือสิ่งนี้ — DPS ในเอกสารคือเพดานสูงสุด ไม่ใช่ค่าที่วัดได้จริง และผู้เล่นเล่นเกมด้วยค่าที่วัดได้จริง ไม่ใช่ด้วยเพดานสูงสุด
ผมในปี 2008 อุดช่องว่าง 27% นี้ด้วยมือ ไล่อ่านล็อกของซิมทีละบรรทัดด้วยตา แล้วนับว่าสกิลถูกบล็อกที่เฟรมไหน ใช้เวลาครึ่งวัน ผ่านมา 18 ปี ตอนนี้ผมโยนล็อกชุดเดียวกันนั้นให้ Claude ข้างล่างนี้คือเนื้อหาเต็มของเซสชันหนึ่งที่ผมรันจริง ผมไม่สรุปย่อ และใส่ความพยายามครั้งแรกที่พลาดเป้าลงไปตามจริง — เพราะการพลาดเป้านั้นคือครึ่งหนึ่งของวิธีใช้เครื่องมือชิ้นนี้
พรอมต์ที่ผมโยนไปเป็นแบบนี้
ที่แนบมาคือบางส่วนของล็อกการต่อสู้ 1,000 ติกของตัวละครเดี่ยวจาก Damage Simulator (
damage_sim/, Python) ของเรา ตามเอกสาร DPS ของตัวละครนี้คือ 847 แต่ผลจากซิมออกมาเป็น 612 จงหาสาเหตุของช่องว่าง 27% จากในล็อก รูปแบบของล็อกคือ[tick] event skill=SKILL state=STATEอย่าเดา ให้อ้างเฉพาะหลักฐานที่มีอยู่ในล็อกเท่านั้น ถ้าหลักฐานไม่พอ ให้บอกว่าไม่พอ[0.0] cast skill=แทง state=START [0.9] cast skill=แทง state=HIT dmg=540 [0.9] cast skill=ฟันขวาง state=BLOCKED reason=GCD [1.5] cast skill=ตีปกติ state=HIT dmg=180 [1.9] cast skill=ตีปกติ state=BLOCKED reason=GCD [3.0] cast skill=ฟันขวาง state=HIT dmg=320 [3.6] cast skill=ตีปกติ state=HIT dmg=180 ... (994 lines omitted) [997.2] cast skill=แทง state=HIT dmg=540
ผลลัพธ์ดิบของ Claude เป็นแบบนี้ (ไม่ได้ขัดเกลา)
ข้อเท็จจริงที่ยืนยันได้จากล็อก:
- อีเวนต์
state=BLOCKED reason=GCDปรากฏซ้ำ ๆ ที่[0.9]ฟันขวางถูกบล็อก และที่[1.9]ตีปกติถูกบล็อกด้วยเหตุ GCD (global cooldown)- ก่อนที่การร่ายสกิลแทง (0.0→0.9, 0.9 วินาที) จะจบลง สกิลอื่นไม่สามารถปล่อยได้ ดูเหมือนเป็นโครงสร้างที่เวลาร่ายเข้าครอบครอง GCD
- ดังนั้นสมมติฐานในเอกสารที่ว่า "ปล่อยทุกสกิลทุกครั้งที่คูลดาวน์หมด" จึงพังลง ในความเป็นจริงท่าร่ายสกิลชนกันเอง ทำให้การปล่อยบางส่วนตกหล่นไป
อย่างไรก็ตาม ล็อกที่ให้มามีเพียง 7 บรรทัดจากทั้งหมด 1,000 บรรทัด ไม่สามารถสรุปได้จาก 7 บรรทัดนี้ว่าช่องว่าง 27% ที่แม่นยำนั้นอธิบายได้ด้วยการชนกันของ GCD เพียงอย่างเดียวหรือไม่ ต้องรวมยอดจำนวนครั้งที่เกิดอีเวนต์ BLOCKED ทั้งหมด และความเสียหายรวมที่ตกหล่นไปเพราะมัน จึงจะตรวจสอบ 27% ได้ ถ้าให้ยอดรวม BLOCKED ของล็อกทั้งหมดมา ผมจะยืนยันเชิงปริมาณให้
การที่หยุดตรงนี้คือเรื่องสำคัญ AI ชี้ สาเหตุได้ แต่ พิสูจน์ 27% ไม่ได้ และมันบอกข้อจำกัดของตัวเองออกมา ในจุดที่นักออกแบบสายกระดาษคงจบการประชุมด้วยประโยค "เป็นเพราะ GCD นี่เอง" เครื่องมือกลับยืนยันว่า "จาก 7 บรรทัดยังไม่รู้" ผมพิจารณาแล้วว่าข้อเรียกร้องนั้นชอบธรรม — นี่แหละคือการตรวจสอบโดยมนุษย์ ผมจึงดึงยอดรวม BLOCKED ของล็อกทั้งหมดออกมาแล้วโยนกลับไปอีกครั้ง
นี่คือยอดรวมของทั้ง 1,000 ติก BLOCKED รวม 412 ครั้ง ในจำนวนนั้นเป็นเหตุ GCD 388 ครั้ง เป็นเหตุคูลดาวน์ยังไม่พร้อม 24 ครั้ง ความเสียหายที่ประมาณว่าตกหล่นไปเพราะ BLOCKED เมื่อแปลงเป็นค่าฟันขวางอยู่ที่ราว 19,800 ตลอดการต่อสู้ 1,000 ติก (= วินาที) ความเสียหายที่เอกสารคาดหวังคือ 847,000 ส่วนความเสียหายสะสมที่ซิมวัดได้จริงคือ 612,300 จงทบทวนช่องว่าง 27% ใหม่ด้วยตัวเลขชุดนี้
ผลลัพธ์ครั้งที่สองของ Claude
ขอตรวจสอบด้วยยอดรวม
- สะสมที่เอกสารคาดหวัง: 847,000
- สะสมที่ซิมวัดได้จริง: 612,300
- ช่องว่าง: 234,700 (27.7%)
องค์ประกอบของช่องว่างนี้: - ตกหล่นโดยตรงจากการชนของ GCD (388 ครั้ง): ที่ประมาณว่าราว 19,800 นั้นเป็นค่าที่แปลงเป็นฟันขวาง แต่ต้องบวกค่าเสียโอกาส (opportunity cost) ที่ช่องตกหล่นนั้นเคยจะใส่สกิลที่ความเสียหายสูงกว่าได้เข้าไปด้วย หากแปลงช่องที่ตกหล่นเป็นค่าความเสียหายเฉลี่ย ส่วนที่ตกหล่นจะมากกว่า 19,800 - แต่ทั้ง 234,700 อธิบายได้ด้วย BLOCKED อย่างเดียวไม่ได้ การตีความช่องว่างที่เหลือจะต่างกันไปขึ้นกับว่าค่า 847 ในเอกสารเป็น "ค่าที่รวมค่าคาดหวังของคริติคอลเฉลี่ยไว้แล้ว" หรือเป็น "ค่าพื้นฐานที่ไม่รวมคริติคอล" หากบอกสมมติฐานเรื่องคริติคอลของ 847 ในเอกสารมา ผมจะแยกย่อยช่องว่างที่เหลือให้
หยุดอีกครั้ง และก็หยุดในจุดที่ถูกต้องอีกครั้ง ว่า 847 ในเอกสารคำนวณคริติคอลไว้อย่างไร — นี่คือข้อมูลที่ไม่มีในล็อก และอยู่แต่ในหัวของทีมเมเบอร์ A เท่านั้น AI รู้ขอบเขตของข้อมูลที่ตัวเองมี พอผมไปตรวจดูก็พบว่า 847 ในเอกสารเป็นค่าที่นำคริติคอลโอกาส 20% คูณ 1.5 เท่ามาทำให้เรียบเป็นค่าคาดหวังแล้วใส่ลงไป ส่วนซิมเป็นการรันหนึ่งรอบหลังตรึง seed ที่จำนวนครั้งคริติคอลจริงออกมาต่ำกว่าค่าคาดหวัง พูดอีกอย่างคือช่องว่าง 27% เป็นค่าที่ผสมระหว่าง การชนของ GCD (เชิงโครงสร้าง · เกิดซ้ำได้) กับ ความแปรปรวนของคริติคอล (เชิงสถิติ · ดวงของรอบนี้รอบเดียว)
การแยกย่อยนี้แหละคือบทสรุป ส่วนที่เป็นการชนของ GCD คือปัญหาจริงที่ต้องแก้ด้วยการออกแบบ ส่วนที่เป็นความแปรปรวนของคริติคอลคือสัญญาณรบกวนที่จะหายไปเองเมื่อเปลี่ยน seed แล้วเฉลี่ยจาก 1,000 รอบ ถ้าเอาทั้งสองส่วนมาปนกันแล้วบัฟด้วยเหตุผลว่า "ตัวละครอ่อน" ตัวละครที่ปกติดีในค่าเฉลี่ย 1,000 รอบจะกลายเป็นแกร่งเกินไป สิ่งที่สร้างการแยกแยะนี้ขึ้นมา — ที่กระดาษไม่รู้ ซิมรอบเดียวก็ไม่รู้ และ AI ลำพังก็ไม่รู้ — คือการตรวจสอบโดยมนุษย์ที่เอายอดรวมจากล็อกและสมมติฐานที่ซ่อนอยู่ในเอกสารมาประกบกัน
ลองคลี่ดูอินพุต-เอาต์พุตของเครื่องมือที่เซสชันเมื่อกี้ส่องดูออกมาเป็นชุดเดียวกัน สุดท้ายแล้วซิมูเลเตอร์ก็คือฟังก์ชันที่ซื่อตรง อินพุตเดียวกันให้เอาต์พุตเดียวกัน อินพุตมารวมกันจากสามสาย
flowchart LR
A["ชีตข้อมูลเกม
(ตารางสกิล·สเตตัสตัวละคร)
read-only"] --> SIM
B["ไฟล์ yaml สถานการณ์
(เงื่อนไขการต่อสู้·ระยะเวลา·
องค์ประกอบเป้าหมาย)"] --> SIM
C["seed=42
(ตรึงค่าสุ่ม)"] --> SIM
SIM["Damage Simulator
domain/formulas.py
run_combat() × 1000틱"]
SIM --> R1["ความเสียหายสะสมที่วัดได้
612,300"]
SIM --> R2["ล็อก BLOCKED
412 ครั้ง (GCD 388)"]
SIM --> R3["คริติคอลที่เกิด
วัดได้ 17.2% vs คาดหวัง 20%"]
R1 --> REP["รายงาน markdown
เอกสาร 847 vs ซิม 612
แยกย่อยช่องว่าง: โครงสร้าง 19% + ความแปรปรวน 8%"]
R2 --> REP
R3 --> REP
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class SIM code;
class A,B,C,R1,R2,R3,REP data;
หัวใจของภาพนี้คือลูกศรเป็นทิศทางเดียว ชีตข้อมูลเกมถูกซิมูเลเตอร์ อ่านอย่างเดียว ซิมไม่เคยแก้เขียนข้อมูลทับ กฎที่กันเหตุได้มากที่สุดตลอด 18 ปีคือทิศทางของลูกศรเส้นนี้เส้นเดียว ถ้าซิมเริ่มคัดลอกข้อมูลเก็บไว้ในตัวเอง พอวันรุ่งขึ้นข้อมูลเกมเปลี่ยน ซิมก็จะจำลองโลกของเมื่อวาน แล้วถ้าเอารายงานที่ออกมาแบบนั้นไปประชุม ทั้งวงประชุมก็จะถกเถียงกันบนโลกของเมื่อวาน
อินพุตหนึ่งชุดที่เป็นรูปธรรม (ไฟล์ yaml สถานการณ์) หน้าตาเป็นแบบนี้
# scenarios/single_dps_check.yaml
scenario: single_target_dps
duration_ticks: 1000 # สมมติ 1 ติก = 0.1s, การต่อสู้ 100 วินาที
seed: 42 # เชิงกำหนด — อินพุตเดียวกันเอาต์พุตเดียวกัน
actor:
char_id: K_004 # อ่านจากชีตข้อมูลเกม
skill_rotation: optimal # เมื่อ GCD ชน ให้ความเสียหายคาดหวังสูงสุดมาก่อน
target:
defense: 1200
hp: infinite # ดัมมีเลือดไม่จำกัดสำหรับวัด DPS
report:
compare_to_spec: 847 # ใส่ DPS เอกสารเพื่อแยกย่อยช่องว่างอัตโนมัติ
และเอาต์พุตหนึ่งชุด (ตัดมาบางส่วนจากรายงาน) เป็นแบบนี้
# Damage Simulator Report — K_004 single DPS
อินพุต: scenarios/single_dps_check.yaml | seed=42 | data rev. 2026-06-05
## เทียบกับเอกสาร
- DPS เอกสาร: 847 (รวมการทำให้เรียบของค่าคาดหวังคริติคอล 20%·1.5x)
- DPS ที่ซิมวัดได้: 612 (seed นี้ 1 รอบ)
- ช่องว่าง: -27.7%
## แยกย่อยช่องว่าง
- เชิงโครงสร้าง (การชน GCD, เกิดซ้ำได้): -19.2% ← เป้าหมายการทบทวนการออกแบบ
- เชิงสถิติ (ความแปรปรวนคริติคอล, รอบนี้): -8.5% ← คาดว่าหายไปเมื่อเฉลี่ย 1000 รอบ
## ตรวจสอบการเกิดซ้ำ
- รัน seed=42 ซ้ำ 3 ครั้ง → 612,300 / 612,300 / 612,300 (ตรงกัน)
- เฉลี่ย DPS 1000 รอบจาก seed 0~999 → 731 (หลังหักความแปรปรวนคริติคอล)
ลองดูบรรทัดสุดท้าย ค่าเฉลี่ยจากการรัน 1,000 รอบด้วย seed 0\~999 คือ 731 ช่องว่าง 116 (13.7%) ระหว่างเอกสาร 847 กับค่าเฉลี่ย 1,000 รอบ 731 นั่นแหละคือขนาดของ ปัญหาเชิงโครงสร้างที่แท้จริง ที่ชื่อว่าการชนของ GCD ค่าที่ควรเป็นอินพุตของการประชุมออกแบบคือ 731 นี้ ไม่ใช่ 612 ของรอบเดียว ไม่ใช่ 847 ของกระดาษ และไม่ใช่ 612 ของรอบที่ดวงไม่ดี แต่เป็น 731 ที่ 1,000 รอบเห็นพ้องกัน การได้ตัวเลขนี้มากำในมือนี่แหละคืองานของนักออกแบบสายปรับสมดุล
เครื่องมือชิ้นนี้มีอายุ 18 ปี แต่ไม่ได้มีชีวิตอยู่ด้วยโค้ดชุดเดิม ไม้แขวนยังเป็นไม้เดิม เปลี่ยนแต่เสื้อมาห้าครั้ง ไม้แขวนคือตรรกะของรายงานข้างบน — ขั้นตอนการแยกดูเอกสารกับค่าที่วัดได้จริง แยกย่อยช่องว่างออกเป็นโครงสร้างกับความแปรปรวน แล้วตรวจสอบด้วยการเกิดซ้ำ ขั้นตอนนี้เหมือนกันทุกตัวอักษร ทั้งใน Excel VBA (ภาษามาโครของ Excel) ปี 2008 และใน Python ปี 2026
| ช่วงเวลา | เสื้อ (เทคโนโลยี) | ไม้แขวน (ขั้นตอนที่ไม่เปลี่ยน) |
|---|---|---|
| 2008\~2011 | Excel VBA, 1:1 | แยกย่อยช่องว่างเทียบเอกสาร |
| 2012\~2016 | C# console, N:N | 〃 |
| 2017\~2020 | Python + Web | 〃 |
| 2021\~2024 | Python + ML | 〃 (+ สะท้อนการกระจายตัวของผู้เล่น) |
| 2025\~ | Python + LLM ช่วย | 〃 (+ สอบถามล็อก·สร้างสมมติฐาน) |
เคล็ดที่ทำให้ผ่านการเปลี่ยนเสื้อห้าครั้งมาได้ถูกสลักไว้ในโครงสร้างโฟลเดอร์
damage_sim/
├── domain/ # ไม้แขวน — เหมือนเดิมมา 18 ปี
│ ├── formulas.py # สูตรความเสียหาย·ตัดสินการชน GCD
│ └── metrics.py # ตรรกะแยกย่อยช่องว่าง
├── adapters/ # ข้อมูลเกมแบบ read-only
│ └── excel_reader.py
├── runners/ # เสื้อ — เปลี่ยนทุกครั้งที่เทคโนโลยีเปลี่ยน
│ └── cli_runner.py
└── reporters/ # เสื้อ — รูปแบบการออกรายงาน
└── markdown_report.py
พอเทคโนโลยีเปลี่ยน ก็เขียนแค่ runners/ กับ reporters/ ขึ้นใหม่ ส่วนตรรกะแยกย่อยช่องว่างใน domain/ ยังคงรอดมาเป็นสินทรัพย์ 18 ปีตามเดิม สูตรตัดสินการชน GCD ที่ผมเขียนด้วยเซลล์ Excel ในปี 2008 ตอนนี้ยังรันอยู่ใน formulas.py โดยเปลี่ยนแค่ลายเซ็นของฟังก์ชัน ที่ว่าการตอกเครื่องมือไว้กับเทคโนโลยีเดียวเท่ากับให้มันแก่ตายไปพร้อมเทคโนโลยีนั้น ผมเรียนรู้มาจากการส่งเครื่องมือที่ตายไปแล้วหลายชิ้น
LLM ที่ผมเอามาต่อในปี 2025 ไม่ใช่ไม้แขวนอันใหม่ แต่เป็นมือใหม่ อย่างที่เห็นในเซสชันก่อนหน้า AI คือมือที่ อ่านล็อกและตั้งสมมติฐาน การตามรอยล็อกที่เคยใช้เวลาครึ่งวันลดเหลือไม่กี่นาที แต่ไม่แตะต้องไม้แขวน — สิ่งที่ ตัดสิน ว่าช่องว่างเป็น 27% หรือไม่ คริติคอลออกกี่เปอร์เซ็นต์ ยังคงเป็นคอร์เชิงกำหนดที่ตรึง seed ไว้ ทันทีที่ปล่อยให้ LLM เข้าไปอยู่ในที่ตรงนั้น การตรวจสอบแบบ regression จะทำไม่ได้ แล้วเครื่องมือก็ตาย
ถ้าในเครื่องมือปรับสมดุลต้องขีดเส้นได้เพียงเส้นเดียว ผมจะขีดที่ขอบของคอร์เชิงกำหนด ด้านในต้องรับประกันแกร่งดั่งเหล็กว่าอินพุตเดียวกันให้เอาต์พุตเดียวกัน ส่วนด้านนอกจะให้คนหรือ AI โยนสมมติฐานได้อย่างอิสระ
ด้านใน (เชิงกำหนด — ห้าม AI):
- สูตรความเสียหาย, การตัดสินการชน GCD, การเกิดคริติคอล, การรวมยอดสะสม
- รันด้วย seed=42 สามครั้งต้องได้ 612,300 เหมือนกันทั้งสามครั้ง ถ้าข้อนี้พังก็เทียบรายงานของเมื่อวานกับรายงานของวันนี้ไม่ได้
ด้านนอก (สมมติฐาน·การตีความ — ยินดีต้อนรับ AI): - การสอบถามสาเหตุอย่าง "ทำไมอัตราชนะของการจับคู่ตัวละครชุดนี้ถึงผิดปกติ" - หาแพตเทิร์น BLOCKED ในล็อก, ร่างรายงานภาษาธรรมชาติ, ร่างไฟล์ yaml สถานการณ์
บันทึกเซสชันจริงก่อนหน้านี้เคลื่อนไหวอยู่บนเส้นนี้พอดี AI ตั้งสมมติฐานว่า "การชน GCD คือสาเหตุ" ขึ้นมาอย่างรวดเร็วจากด้านนอก แต่ตัวเลข 27% และตัวเลข 612 เป็นค่าที่คอร์เชิงกำหนดคำนวณไว้จนถึงที่สุด AI เพียงรับค่านั้นมาตีความเท่านั้น และมันหยุดถึงสองครั้งด้วยคำว่า "ข้อมูลเท่านี้สรุปไม่ได้" — โดยเรียกร้องข้อมูลที่คอร์เชิงกำหนดให้ไม่ได้ (สมมติฐานคริติคอลของเอกสาร) การหยุดนี้คือเครื่องหมายของเครื่องมือที่ดี คือการไม่เข้าใจผิดว่าสมมติฐานเท่ากับการวินิจฉัย
มีเรื่องหนึ่งที่ผมขอบอกตามตรงเกี่ยวกับตัวเลข ตัวเลขเฉพาะอย่าง 847·612·731·412 ครั้ง ในบทนี้เป็นค่าตัวอย่างที่ประกอบขึ้นเพื่อการอธิบาย แต่ ทิศทาง ที่ว่า DPS ในเอกสารออกมาสูงกว่าค่าที่ซิมวัดได้จริงเสมอ โครงสร้าง ที่ว่าช่องว่างนั้นแยกย่อยออกเป็นการชนเชิงโครงสร้างกับความแปรปรวนเชิงสถิติ และ หลักการ ที่ว่าการตรึง seed เป็นเงื่อนไขตั้งต้นของการตรวจสอบ regression ล้วนเป็นสิ่งที่ผมยืนยันซ้ำจากการใช้งานจริงตลอด 18 ปีตั้งแต่ปี 2008 ขนาดของอัตราส่วนต่างกันไปในแต่ละโปรเจกต์ แต่ทิศทางกับโครงสร้างไม่เคยเปลี่ยน
setup. เลือกตัวละครหนึ่งตัวจากข้อมูลเกม แล้วหาตารางสกิล (ความเสียหาย·คูลดาวน์·เวลาร่าย) กับ DPS เอกสารมาให้พร้อม ถ้าไม่มีซิมูเลเตอร์ ให้เขียนสคริปต์ขั้นต่ำที่รันการต่อสู้กับเป้าหมายเดี่ยว 1,000 ติก หัวใจมีข้อเดียวคือต้องรับ seed เป็นอาร์กิวเมนต์เพื่อตรึงค่าได้
prompt. โยนล็อกของซิม (รวมอีเวนต์ BLOCKED) พร้อมกับ DPS เอกสารไปด้วยกัน
ที่แนบมาคือล็อกการต่อสู้ 1,000 ติกของตัวละคร 1 ตัว และยอดรวม BLOCKED DPS เอกสารคือ [N] แต่ผลจากซิมคือ [M] จงแยกย่อยสาเหตุของช่องว่างด้วยหลักฐานจากล็อกเท่านั้น แยกสาเหตุเชิงโครงสร้าง (การชนที่เกิดซ้ำ) ออกจากสาเหตุเชิงสถิติ (ความแปรปรวนของรอบนี้) ถ้าหลักฐานไม่พอ ให้บอกว่าไม่พอ และชี้ว่าต้องการอะไรเพิ่ม
verify. เอาสาเหตุเชิงโครงสร้างที่ AI ชี้ไปตรวจสอบด้วยค่าเฉลี่ย 1,000 รอบโดยเปลี่ยน seed ถ้าช่องว่างยังเหลืออยู่แม้ในค่าเฉลี่ยก็คือปัญหาเชิงโครงสร้างจริง ถ้าหายไปก็เป็นสัญญาณรบกวนจากความแปรปรวน ถ้า AI หยุดด้วยคำว่า "สรุปไม่ได้" นั่นไม่ใช่ความล้มเหลวแต่เป็นเรื่องปกติ — ในจุดที่มันหยุด มนุษย์เข้าไปเติมสมมติฐานที่ซ่อนอยู่ในเอกสาร
ถ้าคุณเป็นนักพัฒนาคนเดียวที่ไม่มีทั้งซิมูเลเตอร์และ ML ก็รันขั้นตอนเดียวกันได้ด้วย Excel แผ่นเดียวกับ AI เท่านั้น เขียนตารางสกิลลงในชีต แล้วสร้างซิม 1,000 แถวที่ใช้ RAND() ทอยคริติคอลในคอลัมน์หนึ่ง เนื่องจากตรึง seed ไม่ได้ ให้กด F9 คำนวณใหม่ 100 ครั้งแล้วหาค่าเฉลี่ยด้วยมือ จากนั้นโยนช่องว่างระหว่างค่าเฉลี่ยนั้นกับ DPS เอกสารให้ AI โดยบอกว่า "ช่วยแยกออกเป็นสาเหตุเชิงโครงสร้างกับสาเหตุความแปรปรวน" เครื่องมือจะเล็กก็จริง แต่ไม้แขวน — การแยกย่อยช่องว่างเทียบเอกสาร, การแยกโครงสร้างออกจากความแปรปรวน, การตรวจสอบการเกิดซ้ำ — ยังตั้งอยู่เหมือนเดิมทุกประการ
บ่ายวันศุกร์ เวลาสี่โมง การจำลอง PvP 5:5 อัตโนมัติของอัลฟาบิลด์จำนวน 1,200 แมตช์ก็จบลง ผลลัพธ์ JSON มีขนาด 4 เมกะไบต์ ที่ไหนสักแห่งในนั้นมีบรรทัดหนึ่งบันทึกไว้ว่า "อัตราชนะของทีม A 92%" ทั้งที่อัตราชนะเฉลี่ยอยู่ที่ 52% ผมใช้เวลา 40 นาทีในการตามหาบรรทัดนั้น และจนกระทั่งเลิกงานก็ยังหาสาเหตุไม่ได้ว่าทำไมถึงเป็นเช่นนั้น
การปรับสมดุลเป็นดินแดนของความเป็นเชิงกำหนด (deterministic) ใส่อินพุตเดียวกันลงในสูตรเดียวกัน ผลที่ได้ก็คือดาเมจเดียวกันเสมอ ด้วยเหตุนี้ตัวจำลองดาเมจจึงต้องเป็นโค้ด และเส้นโค้งรางวัลก็ต้องให้มนุษย์ลากด้วยมือ — ตรงนี้คือที่ที่ AI ไม่ควรย่างเข้ามา แต่ รอบ ๆ แกนเชิงกำหนดนั้น กล่าวคือการตามหาบรรทัดแปลก ๆ จากผลลัพธ์ 1,200 แมตช์ การตั้งสมมติฐานว่าทำไม การคัดกรองตัวเลือกว่าควรเปลี่ยนอะไร และการนำตัวเลือกเหล่านั้นกลับไปเข้าการจำลองอีกครั้ง — งานรอบ ๆ เหล่านี้แหละที่กินเวลาส่วนใหญ่ในวันหนึ่งของคนทำบาลานซ์ บทนี้คือเรื่องของการนำ AI มาประกบไว้รอบ ๆ นั้น โดยไม่แตะแกนแม้แต่นิดเดียว
ตัวจำลองดาเมจรุ่นปี 2008 ที่เห็นในหัวข้อ 8.3 — ตัวที่แกนเชิงกำหนดรอดมาได้ทั้งที่เปลี่ยนเอนจินและเปลี่ยนบริษัทมาแล้วสามครั้ง — คือจุดเริ่มต้นของบทนี้ คุณสมบัติที่ว่าอินพุตเดียวกันได้เอาต์พุตเดียวกัน นั่นคือความเชื่อถือทั้งหมดของเครื่องมือปรับสมดุล หากรันบิลด์เดียวกันสองครั้งแล้วได้อัตราชนะต่างกัน เครื่องมือนั้นต้องถูกทิ้ง
เพราะฉะนั้นเมื่อวาดโครงของงานปรับสมดุลออกมา ก็จะได้รูปที่มีก้อนเชิงกำหนดอยู่ตรงกลาง และมีงานมือของมนุษย์ห้อยอยู่ที่ทางเข้าและทางออกของมัน ด้านล่างคือการแยกโครงนั้นออกมา — โดยใช้สีแบ่งดินแดนเชิงกำหนด (สีน้ำเงิน) ออกจากดินแดนที่มนุษย์และ AI เข้ามาแทรก (สีส้ม)
กล่องสีน้ำเงินกลางภาพเพียงกล่องเดียวคือโค้ด กล่องสีส้มอีกห้ากล่องที่เหลือล้วนเป็นแรงงานของมนุษย์ทั้งสิ้น ทั้งการตัดสิน การตีความ และการเขียน และที่ที่ AI เข้าไปได้ก็มีเพียงห้าจุดนี้เท่านั้น ทันทีที่สั่ง LLM ว่า "ช่วยคำนวณ DPS ของตัวละครนี้ที" ความเป็นเชิงไม่กำหนด (non-deterministic) ที่อินพุตเดียวกันได้ตัวเลขต่างกันก็จะรั่วเข้าสู่แกน และเครื่องมือนั้นจะสูญเสียความเชื่อถือไปภายในไม่ถึง 18 วัน
ดังนั้นกระดูกสันหลังของบทนี้จึงเรียบง่าย รักษาแกนให้เป็นโค้ดจนถึงที่สุด แล้วประกบ AI ไว้ที่ห้าจุดของทางเข้าและทางออก โดยเริ่มจากฝั่งทางออกที่ใช้แรงมือมากที่สุดก่อน — งานตามหาบรรทัดแปลก ๆ จากผลลัพธ์ 1,200 แมตช์ แล้วตั้งสมมติฐาน — แล้วค่อยทำให้เป็นอัตโนมัติ
กลับไปที่ 92% ในตอนต้นกันอีกครั้ง คราวนี้แทนที่คนจะเดินวนหา 40 นาที ตัวตรวจจับเชิงกำหนดจะคัดบรรทัดนั้นออกมา LLM ตั้งสมมติฐาน และการจำลองตรวจสอบอีกครั้ง — เราจะตามรอบหนึ่งวงตั้งแต่ต้นจนจบ โดยไม่ย่อ และคงผลลัพธ์ดิบที่เครื่องมือคายออกมาจริงไว้ตามเดิม
การคัดแมตช์ที่ "ผิดปกติ" จาก 1,200 แมตช์นั้นไม่ใช่ LLM ทำ แต่เป็นสถิติ คำนวณค่าเฉลี่ยและส่วนเบี่ยงเบนมาตรฐานของแต่ละตัวชี้วัด แล้วแบ่งด้วยว่าห่างจากค่าเฉลี่ยกี่ส่วนเบี่ยงเบนมาตรฐาน (z-score) หากเกินค่าขีดแบ่งก็เป็น outlier นี่คือเชิงกำหนด ไม่มีช่องให้อาการหลอน (hallucination) เข้าแทรก
def find_outliers(results, threshold=2.5):
# results: ลิสต์ของดิกชันนารี {ชื่อตัวชี้วัด: ค่า} ต่อหนึ่งแมตช์จำลอง
means, stds = compute_per_metric(results) # ค่าเฉลี่ยและส่วนเบี่ยงเบนมาตรฐานต่อตัวชี้วัด
outliers = []
for r in results:
for metric, value in r.items():
if stds[metric] == 0: # ความแปรปรวน 0 → เทียบไม่ได้, ข้าม
continue
z = abs(value - means[metric]) / stds[metric]
if z > threshold:
outliers.append((r["scenario_id"], metric, value, round(z, 2)))
return sorted(outliers, key=lambda x: -x[3]) # เรียงจาก z มากไปน้อย
เมื่อรันแล้วจะได้ผลดังนี้ — จาก 1,200 แมตช์ ที่เกินค่าขีดแบ่ง 2.5 มีเพียง 3 รายการ
[("pvp_5v5_S0417", "team_a_winrate", 0.92, 4.1),
("pvp_5v5_S0417", "match_duration", 41.0, 2.9),
("pvp_5v5_S0822", "team_b_winrate", 0.18, 2.6)]
บรรทัดแรกที่ z มากที่สุด คืออัตราชนะ 0.92 (z=4.1) ของซีนาริโอ pvp_5v5_S0417 นั่นคือบรรทัดเดียวกับที่ผมเดินวนหา 40 นาทีในตอนต้น แทนที่คนจะต้องเอาตาไล่ JSON ขนาด 4 เมกะ สถิติช่วยบีบให้เหลือ 3 รายการ ถึงตรงนี้คือแกน จากนี้ไปคือ AI
คราวนี้ส่งบรรทัดนั้นให้ LLM แต่ไม่ใช่ "ช่วยวินิจฉัยสาเหตุที" LLM เพียงโยน สมมติฐานสาเหตุที่เป็นไปได้ ออกมาสองสามข้อด้วยความรู้เชิงโดเมน ส่วนอันไหนคือของจริงนั้นให้การจำลองเป็นผู้ตัดสินอีกครั้ง พรอมต์เต็มมีดังนี้
[outlier]
ซีนาริโอ: pvp_5v5_S0417 — PvP 5:5
องค์ประกอบทีม A: [refgame_archer_07, refgame_archer_07, refgame_archer_07,
refgame_hybrid_21, refgame_hybrid_21]
ตัวชี้วัด: team_a_winrate 0.92 (ค่าเฉลี่ยรวม 0.52, z = 4.1)
ตัวชี้วัดประกอบ: match_duration 41.0s (ค่าเฉลี่ย 28s, z = 2.9)
[ข้อมูลที่เกี่ยวข้อง]
- refgame_archer_07: สนับสนุนระยะไกล, สกิล "ตราหมาย" — ดีบัฟเพิ่มดาเมจที่เป้าหมายได้รับ +12%
- refgame_hybrid_21: ดีลเลอร์ผสมประชิด/ระยะไกล, สกิล "ไล่ล่า" — เพิ่มความเร็วเคลื่อนที่ +20% เมื่อไล่ล่าเป้าหมายที่ติดดีบัฟ
- ตัวละครสองตัวนี้แทบไม่เคยถูกจัดอยู่ทีมเดียวกัน (ความถี่ของการจับคู่อยู่ท้าย 5%)
คำขอ:
จงเสนอสมมติฐานสาเหตุที่เป็นไปได้ของ outlier นี้ 3~5 ข้อ
- แต่ละสมมติฐานมีการจำลองเพิ่ม 1 บรรทัดสำหรับตรวจสอบ
- ห้ามวินิจฉัยฟันธง ให้เป็นสมมติฐานระดับ "อาจจะ~" เท่านั้น
รูปแบบการตอบ: ลิสต์มีหมายเลข ไม่มีคำนำและคำลงท้าย มีแต่สมมติฐาน
ขอคัดผลลัพธ์ดิบของ Claude มาตามเดิม (ไม่ย่อ ไม่จัดเรียง)
1. ดีบัฟ "ตราหมาย" กับบัฟความเร็วเคลื่อนที่ "ไล่ล่า" อาจสะสมแบบคูณกัน
archer_07 จำนวน 3 ตัวปูตราหมายเต็มพื้นที่ → hybrid_21 ไล่ล่าได้ไม่ขาดสาย
ก่อเป็นลูปป้อนกลับเชิงบวกขึ้นมา
ตรวจสอบ: เปลี่ยน hybrid_21 เป็นดีลเลอร์ประชิดทั่วไปแล้วจำลองซีนาริโอเดิมซ้ำ
2. ดีบัฟตราหมาย +12% อาจถูกตั้งค่าผิดให้ซ้อนกันได้ (stackable)
archer_07 จำนวน 3 ตัวอาจสะสมได้ถึง +36%
ตรวจสอบ: บังคับ max_stack ของตราหมายเป็น 1 แล้วจำลองซ้ำ
3. ใน 5:5 องค์ประกอบระยะไกล 3 + แบบไล่ล่า 2 อาจเป็นโครงสร้างที่
ปิดทางเข้าของฝ่ายตรงข้ามด้วยความเหนือกว่าด้านระยะโจมตีล้วน ๆ (ไม่เกี่ยวซินเนอร์จี, ตัวองค์ประกอบเองแข็งแกร่ง)
ตรวจสอบ: ลดผลของดีบัฟตราหมายเป็น 0% แล้วจำลององค์ประกอบเดิมซ้ำ
4. เมื่อดูจาก match_duration ที่ยาวกว่าค่าเฉลี่ย (41s) อาจไม่ใช่ทีม A ชนะเร็ว
แต่เป็นโครงสร้าง "ไม่แพ้ ทนไว้ แล้วพลิกกลับในช่วงท้าย"
กรณีนี้สาเหตุอาจเป็นค่าสเกลลิงช่วงท้ายมากกว่าอัตราชนะ
ตรวจสอบ: ดึงเฉพาะเส้นโค้งดาเมจสะสม ณ จุด 60 วินาทีมาเทียบต่างหาก
5. (ความน่าจะเป็นต่ำ) อคติของซีดจำลอง — เฉพาะซีด S0417 ที่อาจให้ทีม A
ออกก่อนซ้ำ ๆ มีความเป็นไปได้ว่าเป็นสิ่งแปลกปลอมทางสถิติ (artifact)
ตรวจสอบ: จำลององค์ประกอบเดิมด้วยซีด 50 ตัวแล้วตรวจความแปรปรวนของอัตราชนะ
การหยุดตรงนี้สำคัญ การที่สมมติฐานข้อที่ห้าทิ้งความสงสัยในตัวเองไว้ว่า "อันนี้อาจเป็นแค่อคติของซีดก็ได้" — นี่คือเอาต์พุตที่ดี หาก LLM ฟันธงว่า "สาเหตุคือบั๊กการซ้อนตราหมาย" กลับต้องสงสัยเอาต์พุตนั้น ในงานปรับสมดุล หน้าที่ของ LLM ไม่ใช่การวินิจฉัย แต่คือ การบีบพื้นที่การค้นหาให้แคบลง
สมมติฐานทั้งห้ามีการจำลองสำหรับตรวจสอบห้อยติดมาข้อละหนึ่งบรรทัด สิ่งนี้ไม่ใช่ให้คนรันทีละข้อ แต่รวมตัวเลือกการเปลี่ยนแล้วโยนเข้าไปแบบขนาน แกนหลักอย่าง simulate_dps มีรูปร่างที่รันได้จริงดังนี้ — คือแก่นของฟังก์ชันเชิงกำหนดที่เก่าแก่ 18 ปีนั้น
def simulate_dps(attacker, target, formula, ticks=600, seed=0):
"""จำลองการต่อสู้หนึ่งคู่แบบเชิงกำหนด ถ้า (อินพุต, seed) เดียวกันก็ได้เอาต์พุตเดียวกัน"""
rng = Rng(seed) # ตรึงซีด → ทำซ้ำได้
hp = target.hp
total_damage = 0.0
for t in range(ticks): # สมมติ 1 tick = 0.1 วินาที
# ค่าสัมประสิทธิ์ป้องกัน: สูตรเชิงกำหนด (LLM ไม่เป็นคนสร้าง)
def_factor = target.defense / (target.defense + formula.def_const)
raw = attacker.atk * (1 - def_factor)
# คริติคอล: อิงซีด → ถ้า seed เดียวกันก็ได้จังหวะคริเดียวกัน
if rng.roll() < attacker.crit_rate:
raw *= attacker.crit_mult
# ดีบัฟ (เช่นตราหมาย) ถูกป้อนเข้ามาจาก formula แบบเชิงกำหนด
raw *= formula.debuff_multiplier(attacker, target, t)
hp -= raw
total_damage += raw
if hp <= 0:
return {"ttk": t * 0.1, "dps": total_damage / ((t + 1) * 0.1)}
return {"ttk": None, "dps": total_damage / (ticks * 0.1)} # ฆ่าไม่ลงในเวลา
def run_candidates(base_scenario, candidates, seeds=range(50)):
"""จำลองตัวเลือกการเปลี่ยนต่อสมมติฐานแบบขนาน 50 ซีด เก็บความแปรปรวนของ winrate กลับมาด้วย"""
out = {}
for name, patch in candidates.items(): # patch = เขียนทับ formula บางส่วน
scen = base_scenario.with_patch(patch)
wins = [simulate_match(scen, formula=scen.formula, seed=s) for s in seeds]
out[name] = {
"winrate": mean(w["team_a_won"] for w in wins),
"winrate_std": pstdev(w["team_a_won"] for w in wins), # สำหรับตรวจสอบสมมติฐาน 5
}
return out
ย้ายสมมติฐานเข้าดิกชันนารี candidates แล้วรันรวดเดียว
candidates = {
"ฐาน(ไม่เปลี่ยน)": {},
"สมมติฐาน1_เปลี่ยนhybrid": {"team_a[3:5]": "refgame_melee_03"},
"สมมติฐาน2_ตราหมาย_max_stack1": {"skill.ตราหมาย.max_stack": 1},
"สมมติฐาน3_ตราหมาย_ผล0": {"skill.ตราหมาย.debuff": 0.0},
"สมมติฐาน5_ตรวจการกระจายซีด": {}, # องค์ประกอบเดิม, ต่างแค่ seeds 50 ตัว
}
result = run_candidates(scenario_S0417, candidates, seeds=range(50))
ผลลัพธ์ (เอาต์พุตในรูปแบบการรันจริง):
ฐาน(ไม่เปลี่ยน) winrate=0.91 std=0.04 ← ไม่ใช่อคติของซีด (ปฏิเสธสมมติฐาน5)
สมมติฐาน1_เปลี่ยนhybrid winrate=0.74 std=0.06
สมมติฐาน2_ตราหมาย_max_stack1 winrate=0.63 std=0.05 ← ตกลงมากที่สุด
สมมติฐาน3_ตราหมาย_ผล0 winrate=0.55 std=0.05 ← กลับมาใกล้ค่าเฉลี่ย
ลำดับที่อ่านก็คือการวินิจฉัยนั่นเอง รันฐานด้วย 50 ซีดอีกครั้งก็ยังได้อัตราชนะ 0.91 ความแปรปรวน 0.04 — สมมติฐาน 5 (อคติของซีด) ถูกปฏิเสธ พอลดผลของตราหมายเป็น 0 ก็ตกมาที่ 0.55 ใกล้ค่าเฉลี่ย — สาเหตุคือสายดีบัฟตราหมายใช่แล้ว และเมื่อมัด max_stack เป็น 1 อัตราชนะตกลงมากที่สุดถึง 0.63 ดังนั้นแก่นจึงเป็น สมมติฐาน 2 — ดีบัฟตราหมายซ้อนกันจน archer_07 จำนวน 3 ตัวสะสมได้ถึง +36% จากห้าตัวเลือกที่ LLM โยนมา คนไม่ได้ตรวจสอบครบทั้งห้า แต่สถิติรันเพียงสามก็ตัดสินได้
ตรงนี้สิ่งที่ LLM ทำไม่ใช่การ พูด ว่า "การซ้อนตราหมายคือบั๊ก" แต่เป็นแค่การ นำสมมติฐานนั้นขึ้นไปอยู่ในรายการตัวเลือก เท่านั้น การรับนั้นคนทำบาลานซ์ที่ดูผลการจำลองเป็นผู้ทำ — "ตรึง max_stack ของตราหมายไว้ที่ 1 อัตราชนะขององค์ประกอบ archer_07 เดี่ยวยังอยู่ที่ 0.63 ซึ่งสูงกว่าค่าเฉลี่ย (0.52) อยู่ ดังนั้นในบิลด์ถัดไปจะปรับค่าดีบัฟตราหมายจาก 12%→9% เพิ่มเติมแล้ววัดผลใหม่"
การตัดสินนี้มนุษย์เป็นผู้ลงมือ และเหตุผลของมัน (ตรวจจับ z=4.1 → 5 สมมติฐาน → 3 การจำลอง → ยืนยันสมมติฐาน 2) ถูกบันทึกไว้เป็นหนึ่งบรรทัด แกนเชิงกำหนดเป็นโค้ดจนถึงที่สุด ส่วน LLM เพียงแค่เปลี่ยนการเดินวนหา 40 นาทีให้กลายเป็นสมมติฐานห้าบรรทัดเท่านั้น มันไม่ได้ก้าวเข้าไปในแกนแม้แต่ก้าวเดียว
บันทึกเซสชันจริงข้างต้นที่จริงแล้วเป็นการเหยียบสามในห้าตำแหน่ง (ตรวจจับความผิดปกติ ค้นหาการเปลี่ยน และตีความความผิดปกติ) พร้อมกัน เมื่อคลี่ห้าตำแหน่งออกเป็นวงจรก็จะหมุนแบบนี้
flowchart TD
A[นิยามซีนาริโอ] -->|ตำแหน่ง 1: สร้างซีนาริโออัตโนมัติ| B[ป้อนค่าตัวเลข]
B -->|ตำแหน่ง 2: ค้นหาตัวเลือกการเปลี่ยน| C{การจำลองเชิงกำหนด
simulate_dps}
C --> D[ผลลัพธ์ดิบ JSON]
D -->|find_outliers z-score| E[ตรวจจับรูปแบบผิดปกติ]
E -->|ตำแหน่ง 4: LLM ตั้งสมมติฐาน 3~5| F[สมมติฐาน + การจำลองตรวจสอบ]
F -->|ตำแหน่ง 3: รายงานภาษาธรรมชาติ| G[คนทำบาลานซ์ตรวจสอบ]
G -->|ตำแหน่ง 5: เสนอการกระทำถัดไป| H{รับ / ปฏิเสธ}
H -->|รับ| B
H -->|ปฏิเสธ| A
style C fill:#dbeafe,stroke:#2563eb,stroke-width:2px
style E fill:#dbeafe,stroke:#2563eb
โหนดสีน้ำเงินสองโหนด (การจำลอง, การตรวจจับ z-score) เท่านั้นที่เป็นเชิงกำหนด ป้ายกำกับบนลูกศรที่เหลือ — ตำแหน่ง 1·2·3·4·5 — คือจุดที่ AI เข้าประกบ ทุกครั้งที่วงจรหมุนครบรอบ การเปลี่ยนที่ถูกรับมาก็จะกลับเข้าไปเป็นการป้อนค่าตัวเลขแล้วเข้าการจำลองครั้งถัดไป หากคนหมุนลูปนี้ด้วยมือ หนึ่งรอบกินเวลาหนึ่งวัน แต่ถ้าหมุนด้วยความช่วยเหลือของ AI ก็ใช้เวลาไม่กี่ชั่วโมง
ขอชี้ห้าตำแหน่งทีละจุดสั้น ๆ
ตำแหน่ง 1 — สร้างซีนาริโออัตโนมัติ ให้คอนเซ็ปต์หนึ่งบรรทัดอย่าง "ศึกยึดพื้นที่ 3:3 ยึดธง 3 อันภายใน 1 นาทีคือชนะ เกิดใหม่ 10 วินาที" พร้อม yaml ซีนาริโอเดิมหนึ่งหรือสองไฟล์ LLM ก็จะเติม yaml ซีนาริโอใหม่ด้วยสกีมาเดียวกัน คนทำบาลานซ์ตรวจสอบแค่ "มันใส่กฎที่ไม่มีในคอนเซ็ปต์เข้ามาเองหรือเปล่า" จาก 1\~2 ชั่วโมงที่เคยเขียน yaml จากกระดาษเปล่าก็ลดเหลือตรวจสอบ 15 นาที
ตำแหน่ง 2 — ค้นหาตัวเลือกการเปลี่ยน ดิกชันนารี candidates ในบันทึกเซสชันจริงข้างต้นก็คือสิ่งนี้ ต่อคำถาม "ถ้าจะเพิ่มอัตรารอดของแทงก์ +49% ต้องไปแตะตรงไหน" LLM โยนตัวเลือกออกมาห้าข้อ (base_def +50, ปรับ def_const ฯลฯ) แล้วเอาตัวเลือกทั้งหมดเข้าการจำลองเพื่อเลือกอันที่มีผลข้างเคียงน้อยที่สุด ตัวเลือกคือสมมติฐาน การรับคือการจำลอง นี่คือจุดที่ต้องจัดการอย่างระมัดระวังที่สุด — เพราะตัวเลือกที่ผิดจะกินเวลาตรวจสอบ
ตำแหน่ง 3 — รายงานภาษาธรรมชาติ ดึงตัวชี้วัดจาก raw JSON ของการจำลองด้วยสคริปต์ (เชิงกำหนด) แล้วส่งเฉพาะตัวชี้วัดนั้น + บริบทการเปลี่ยนให้ LLM เขียน "หนึ่งหน้าที่จะถือเข้าประชุม" การเปลี่ยนแปลงสำคัญ 3\~5 บรรทัด, ตัวละครที่ได้รับผลกระทบ TOP 5, มาตรการต่อเนื่อง 2\~3 ข้อ ตอกย้ำว่าห้ามใช้ตัวเลขนอกเหนือจากตัวชี้วัดที่ให้ไป จากการจัดระเบียบ raw 30 นาทีก็กลายเป็นตรวจสอบ 5 นาที
ตำแหน่ง 4 — ตีความความผิดปกติ ขั้นที่ 2\~3 ข้างต้นคือสิ่งนี้ LLM แปะสมมติฐาน 3\~5 ข้อให้กับ outlier ที่ z-score คัดมา การห้ามวินิจฉัยฟันธงคือเส้นชีวิตของตำแหน่งนี้
ตำแหน่ง 5 — เสนอการกระทำถัดไป เมื่อวิเคราะห์เสร็จก็ทำ "มาตรการทันทีในบิลด์นี้ / ติดตาม 1 สัปดาห์ / ตัวเลือกทบทวนใหม่หลัง 1 สัปดาห์" เป็นเช็กลิสต์พร้อมลำดับความสำคัญ มันเป็นตาข่ายนิรภัยที่ป้องกันไม่ให้คนทำบาลานซ์ลืมการตัดสินใจ ไม่ได้มาแทนการตัดสินใจเอง
การเปิดทั้งห้าตำแหน่งพร้อมกันคือความล้มเหลวที่พบบ่อยที่สุด ให้เปิดจากฝั่งทางออกที่ผลมากและความเสี่ยงน้อยก่อน
ตัวเลขในวงกลม (①\~⑤) คือลำดับการนำมาใช้ ตำแหน่ง 3 (รายงาน) และ ตำแหน่ง 4 (ตีความความผิดปกติ) อยู่ขวาบน — จุดที่ ROI (Return on Investment, ผลตอบแทนเทียบกับการลงทุน) สูงและความเสี่ยงต่ำ — จึงเปิดก่อน เพียงเดินสองตัวนี้ ปริมาณงานที่ประมวลได้ก็เพิ่มขึ้น 2\~3 เท่า และผลของการนำมาใช้กว่า 70% ก็ถูกเก็บกลับมาจากตรงนี้ ตำแหน่ง 2 (เสนอการเปลี่ยน) อยู่จุดสีแดงขวาล่าง ตัวเลือกที่ผิดอาจกินเวลาตรวจสอบ จึงต้องเปิดอย่างระมัดระวังที่สุดเป็นลำดับสุดท้าย ทุกทีมก็ไม่จำเป็นต้องเปิดครบทั้งห้า — เพียงตำแหน่ง 3 และ 4 ก็ทำให้วันหนึ่งของคนทำบาลานซ์คนเดียวเปลี่ยนไปแล้ว
ความรู้สึกตามจริงของระยะเวลาในการนำมาใช้เป็นแบบนี้ (เป็นการประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ — แปรผันอย่างมากตามขนาดทีมและความสุกของเครื่องมือ) ตำแหน่ง 3 ใช้ 1\~2 สัปดาห์, เพิ่มตำแหน่ง 4 อีก 2 สัปดาห์, เพิ่มตำแหน่ง 1 อีกหนึ่งเดือน, เพิ่มตำแหน่ง 5 อีก 2 สัปดาห์, ส่วนตำแหน่ง 2 อยู่ท้ายสุด 1\~2 เดือน นี่คืออีกสำนวนหนึ่งของคำที่ว่าอย่าเปิดทั้งหมดในคราวเดียว
ในโปรเจกต์ A ของผู้เขียน หลังจากเปิดทั้งห้าตำแหน่งตลอด 6 เดือน การเปลี่ยนแปลงเป็นดังนี้ ตัวเลขสัมบูรณ์เป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ให้เชื่อเฉพาะทิศทางและสัดส่วน — ตัวคูณแปรผันอย่างมากตามสภาพแวดล้อม
| รายการ | ก่อนนำมาใช้ | หลังนำมาใช้ (ทิศทาง) |
|---|---|---|
| รอบการจำลองต่อสัปดาห์ของคนทำบาลานซ์ 1 คน | 5\~7 รายการ | 25\~35 รายการ (ราว 5 เท่า) |
| การเขียนรายงาน (ต่อรายการ) | 30\~40 นาที | ตรวจสอบ 5 นาที |
| การเขียนซีนาริโอ (ต่อรายการ) | 1\~2 ชั่วโมง | ตรวจสอบ 15 นาที |
| พบ outlier → วินิจฉัย | 1\~2 วัน | 4\~6 ชั่วโมง |
| ผลการวัด → ตัดสินใจเปลี่ยนครั้งถัดไป | 2\~3 วัน | 1 วัน |
สิ่งสำคัญตรงนี้ไม่ใช่ตัวคูณ แต่คือ เวลาย้ายไปที่ไหน เวลาของมนุษย์ขยับจากการจัดระเบียบข้อมูลดิบไปสู่การตัดสินใจ ไม่ใช่ว่าจำนวนคนทำบาลานซ์ลดลง แต่ขอบเขตเกมที่คนคนเดียวจัดการได้นั้นกว้างขึ้น หากอ่านปริมาณงาน 5 เท่าว่าเป็นการลดกำลังคน ความหมายของการนำมาใช้ก็จะไหลไปผิดทิศ
ต้นทุนนั้นน้อย เมื่อนำการแคชพรอมต์ (prompt caching) มาใช้ ต้นทุน LLM ต่อเดือนของทั้งห้าตำแหน่งอยู่ที่ราว $75 โดยประมาณ (เป็นการประมาณของผู้เขียน) และไม่เกิน 1/100 ของค่าจ้างคนทำบาลานซ์ 1 คน ดังนั้นตัวแปรตัดสินที่แท้จริงของการนำมาใช้จึงไม่ใช่ต้นทุน LLM แต่คือ ภาระการตรวจสอบ มีเวลาให้คนอ่านและกรองสมมติฐานกับรายงานที่ AI โยนมาหรือไม่ — นั่นคือเกณฑ์ในการเปิดหรือปิด
สุดท้าย ขอทิ้งกับดักไม่กี่อย่างที่เกิดซ้ำในที่เดิมตลอด 18 ปีไว้พร้อมใบสั่งยา
run_candidates แล้วเท่านั้นที่ที่ของ AI ในงานปรับสมดุลนั้นชัดเจน อยู่นอกแกนเชิงกำหนด ห้าตำแหน่งที่คนเคยเดินวนหา รักษาแกนให้เป็นโค้ดจนถึงที่สุด แล้วเพียงปลดเปลื้องงานมือรอบ ๆ มันออก — นี่คือวิธีที่ตัวจำลองอายุ 18 ปีจะรอดมาได้แม้ในยุค AI
simulate_dps และ find_outliers จงตรึงซีดเพื่อให้มั่นใจว่าทำซ้ำได้run_candidates → ตัวเลือกที่ทำให้กลับมาใกล้ค่าเฉลี่ยคือสาเหตุ จงให้คนเป็นผู้รับ และบันทึกเหตุผลไว้หนึ่งบรรทัดตลอดสี่บทที่ผ่านมาในส่วนนี้ เราต่อสู้กับศัตรูเพียงตัวเดียวมาโดยตลอด บอสหนึ่งตัวถูกล้มลงได้ในกี่วินาที แทงก์รอดอยู่ได้ 89% หรือไม่ ทองรั่วไหลหรือเปล่า ทั้งหมดล้วนเป็นเรื่องของดาเมจ·การเอาตัวรอด·บัญชีรายรับรายจ่ายที่มุ่งไปยังเป้าหมายเดียว แต่ใน PvP ศัตรูคือมนุษย์ มนุษย์ไม่ได้เคลื่อนไหวตามแพตเทิร์นที่กำหนดไว้แบบบอส อาชีพเดียวกันก็ยังเล่นด้วยฝีมือที่ต่างกัน และที่สำคัญที่สุดคือ ต่างฝ่ายต่างจ้องจุดอ่อนของกันและกัน นี่คือเหตุผลที่ว่าทำไมแม้บาลานซ์ฝั่ง PvE จะลึกซึ้งเพียงใด PvP ก็มักจะว่างเปล่าไปทั้งดุ้น เส้นโค้ง DPS (ดาเมจต่อวินาที) ต่อเป้าหมายเดียวนั้น 8.1\~8.4 ได้กล่าวถึงจนครบถ้วนแล้ว แต่ตาข่ายของความสัมพันธ์แบบ "กรรไกรชนะกระดาษ" นั้นเรายังไม่เคยวาดสักครั้ง
บทนี้จะมาเติมเต็มช่องว่างนั้น สิ่งที่จะกล่าวถึงมีสามอย่าง — เมทริกซ์อัตราชนะ ที่บรรจุความสัมพันธ์ระหว่างคลาส·การจัดทีม แมตช์เมกกิง/MMR ที่กำหนดว่าใครจะเจอกับใคร และ สิทธิ์การตัดสินของเซิร์ฟเวอร์·ระบบกันโกง ที่สามารถทำให้ตัวเลขทั้งหมดนั้นกลายเป็นเรื่องเท็จได้ และเส้นแบ่งที่ทอดผ่านส่วนนี้ทั้งหมดก็ยังคงเดิมในที่นี้ด้วย สูตรการต่อสู้เป็นแบบกำหนด (deterministic) ส่วนการจับคู่และการตรวจหาความสัมพันธ์ของอาชีพเป็น AI ช่วย ไม่คลาดเคลื่อนไปแม้แต่ก้าวเดียว
ใน PvE ความแข็งแกร่งของตัวละครคือค่าสัมบูรณ์ ถ้านักดาบมี DPS 800 ก็คือ 800 และบอสก็รับ 800 นั้นไปตรง ๆ แต่ใน PvP ความแข็งแกร่งเป็นแบบสัมพัทธ์ ค่า 800 ของนักดาบเพียงพอต่อนักธนู แต่อาจถูกหั่นลงเหลือ 560 จนไม่พอสำหรับทหารโล่ที่ลดความเสียหายที่ตนได้รับลง 30% ความแข็งแกร่งของตัวละครเดียวกันเปลี่ยนไป ตามว่าคู่ต่อสู้เป็นใคร สิ่งเดียวนี้เองที่ทำให้บาลานซ์ PvP กลายเป็นปัญหาที่ต่างจาก PvE โดยพื้นฐาน
ด้วยเหตุนี้ หน่วยของบาลานซ์ PvP จึงไม่ใช่ตัวเลขของตัวละครหนึ่งตัว แต่เป็นความสัมพันธ์ของหนึ่งคู่ อัตราชนะของ "นักดาบ vs นักธนู" และอัตราชนะของ "นักดาบ vs ทหารโล่" ต่างก็มีอยู่แยกกัน และเมื่อรวบรวมความสัมพันธ์เหล่านี้ทั้งหมดเข้าด้วยกันก็จะกลายเป็นตารางหนึ่งตาราง ทั้งแกนนอนและแกนตั้งบรรจุรายชื่อคลาสชุดเดียวกัน และในแต่ละช่องจะเขียน "ความน่าจะเป็นที่แถวจะชนะคอลัมน์" ไว้ นี่คือเมทริกซ์อัตราชนะ ถ้า PvE มีเส้นโค้ง DPS แล้ว PvP ก็มีเมทริกซ์นี้
วิธีอ่านตารางทางขวานั้นเรียบง่าย ถ้าช่อง "นักดาบ vs ทหารโล่" เป็น 0.42 หมายความว่าความน่าจะเป็นที่นักดาบจะชนะทหารโล่คือ 42% นั่นคือทหารโล่เป็นฝ่ายได้เปรียบในความสัมพันธ์นี้ ถ้าทุกช่องใกล้เคียง 0.50 ก็จะเป็นความสมดุลที่สมบูรณ์แบบ แต่เกมแบบนั้นไม่สนุก ต้องมีความสัมพันธ์ที่หมุนวน เหมือนเป่ายิ้งฉุบ การเลือกอาชีพจึงจะมีความหมาย ปัญหาจะเกิดเมื่อวงหมุนนั้นขาดตรงจุดใดจุดหนึ่ง จนเกิดช่องที่คลาสหนึ่งชนะทุกคลาส ถ้าแทงก์เมื่อตีสองคืออุบัติเหตุของ PvE แล้ว "ทหารโล่ vs ทุกอาชีพ อัตราชนะเกิน 60%" ก็คืออุบัติเหตุของ PvP
ตรงนี้มีสิ่งที่ต้องตอกย้ำไว้ก่อน ตัวเลขที่เติมในช่องเหล่านี้ (0.58, 0.42 ฯลฯ) ล้วนเป็นตัวอย่าง ไม่ใช่ค่าวัดจริง แต่ละเกมมีจำนวนอาชีพ สกิล และเส้นสมดุลเป้าหมายที่ต่างกัน สิ่งที่ควรเชื่อถือในบทนี้ไม่ใช่ตัวเลข แต่เป็นโครงสร้างที่ว่า เติมเมทริกซ์อย่างไร ตรวจสอบอย่างไร และ AI เข้าไปแนบกับจุดใดของการตรวจสอบนั้น
การเติมหนึ่งช่องของเมทริกซ์อัตราชนะนั้นใช้เครื่องมือเดียวกับการจำลองแบบกำหนดที่เห็นใน 8.4 อย่างแม่นยำ เมื่อจำลองอัตโนมัติ "นักดาบ vs นักธนู" 1,000 ตา แล้วนับว่านักดาบชนะกี่ตา นั่นคืออัตราชนะของช่องนั้น ถ้ามีอาชีพ N อาชีพ ช่องก็จะมี N×N ช่อง และเมื่อรันแต่ละช่องช่องละ 1,000 ตา ตารางหนึ่งตารางก็จะถูกเติมจนเต็ม การจำลองนี้เป็นโค้ดไปจนสุดทาง — ถ้าให้ seed เดียวกัน ก็ต้องได้เมทริกซ์เดียวกันที่ทำซ้ำได้โดยไม่ผิดเพี้ยนแม้แต่ตัวเดียว เพราะอย่างนั้นคำพูดที่ว่า "บิลด์นี้ทหารโล่แข็งขึ้น" จึงจะไม่เป็นเรื่องเท็จ
ตรงนี้มีกับดักเฉพาะของ PvP อยู่หนึ่งอย่าง ในการจำลอง PvE ศัตรู (บอส) เป็นแพตเทิร์นตายตัว แต่ในการจำลอง PvP คู่ต่อสู้ก็ต้องเลือกพฤติกรรมด้วย จำเป็นต้องมีบอต (bot policy) ที่กำหนดว่านักดาบจะสู้อย่างไรอยู่ทั้งสองฝั่ง และถ้าบอตนี้โง่ เมทริกซ์ทั้งหมดก็จะกลายเป็นเรื่องเท็จ — ถ้าเอาบอตที่คอนโทรลห่วยมาชนกัน ก็จะได้เมทริกซ์ที่ "อาชีพที่ใช้สกิลแบบสะเปะสะปะ" เป็นฝ่ายชนะ ทั้งที่ในมือของผู้เล่นที่ชำนาญจริงผลอาจตรงกันข้ามได้ ด้วยเหตุนี้ เมทริกซ์ PvP จึงต้องมีหมายเหตุกำกับเสมอว่า "บอตนี้เลียนแบบการเล่นในระดับใด" โดยปกติบอตจะเขียนด้วยฮิวริสติก (พอคูลดาวน์ครบก็ใช้ พอ HP ต่ำกว่า 30% ก็ถอย ฯลฯ) และตัวฮิวริสติกนี้เองก็เป็นแบบกำหนด
เมื่อถ่ายแก่นของนโยบายบอตออกมาเป็นรูปแบบที่รันได้จริง ก็จะได้ออกมาเป็นแบบนี้ — เป็นฟังก์ชันที่เลือกพฤติกรรมเดียวกันเมื่ออินพุตเหมือนกัน โดยไม่มีช่องว่างให้อาการหลอน (hallucination) แทรกเข้ามา
def bot_decide(me, enemy, cooldowns, t):
"""นโยบายบอตแบบกำหนด สถานะเดียวกันได้พฤติกรรมเดียวกัน LLM ไม่ได้เป็นคนสร้าง"""
# 1) เอาตัวรอดก่อน: ถ้า HP ต่ำกว่า 30% ให้หลบ/ถอย
if me.hp_ratio < 0.30 and cooldowns["escape"] <= 0:
return Action("escape")
# 2) สกิลแก้ทาง: ถ้าศัตรูไม่ได้ภูมิคุ้มกันดีบัฟ ให้ติดมาร์กก่อน
if cooldowns["mark"] <= 0 and not enemy.has("debuff_immune"):
return Action("mark", target=enemy)
# 3) จัดการระยะ: ถ้าศัตรูประชิดเข้ามา ให้ถอยออกห่าง (อาชีพระยะไกล)
if me.is_ranged and dist(me, enemy) < me.kite_range:
return Action("reposition")
# 4) อื่น ๆ: สกิลดาเมจสูงสุดที่คูลดาวน์ครบแล้ว
return best_ready_damage_skill(me, cooldowns)
def simulate_pvp_match(class_a, class_b, formula, seed=0):
"""จำลองหนึ่งตาแบบ 1:1 เชิงกำหนด ดาเมจใช้สูตรของ 8.1 ตรง ๆ"""
rng = Rng(seed)
a, b = spawn(class_a), spawn(class_b)
for t in range(MAX_TICKS):
for me, foe in ((a, b), (b, a)):
act = bot_decide(me, foe, me.cooldowns, t)
apply_action(act, me, foe, formula, rng) # formula = สูตรดาเมจแบบกำหนด
if a.hp <= 0 or b.hp <= 0:
break
return {"winner": "a" if b.hp <= 0 else "b" if a.hp <= 0 else "draw",
"duration": t * TICK}
สิ่งที่เติมเมทริกซ์ทั้งแผ่นจนเต็มก็คือลูปด้านนอกที่รันฟังก์ชันนี้ 1,000 ครั้งต่อหนึ่งช่อง
def build_winrate_matrix(classes, formula, n=1000):
matrix = {}
for ca in classes:
for cb in classes:
if ca == cb:
continue
wins = sum(
simulate_pvp_match(ca, cb, formula, seed=s)["winner"] == "a"
for s in range(n)
)
matrix[(ca, cb)] = wins / n # สัดส่วนที่ ca ชนะ cb
return matrix
ถึงตรงนี้คือแกนกลาง เป็นโค้ดไปจนสุดทาง AI ไม่ได้เข้ามาแนบที่จุด สร้าง ตารางนี้ แต่แนบที่จุด อ่าน มัน ถ้า N เป็น 8 ช่องก็มี 56 ช่อง การที่คนต้องกวาดสายตาดูอัตราชนะ 56 ค่าเพื่อหาว่า "ตรงไหนพัง" ก็เป็นงานหนักแบบเดียวกับ JSON ขนาด 4 เมกะไบต์ตอนตีสอง ส่วนการเลือกช่องที่ผิดปกตินั้น การตรวจหาด้วย z-score ของ 8.4 ทำได้ตรง ๆ
def find_broken_cells(matrix, low=0.40, high=0.60):
"""คัดช่องที่เบนออกจากเส้นสมดุล (0.5) มากเชิงกำหนด"""
broken = []
for (ca, cb), wr in matrix.items():
if wr > high or wr < low:
broken.append((ca, cb, round(wr, 2)))
return sorted(broken, key=lambda x: abs(x[2] - 0.5), reverse=True)
เมื่อการตรวจหาคัดช่องให้แคบลงแล้ว ก็ส่งช่องนั้นต่อให้ LLM แต่มีระเบียบวินัยเดียวกับ 8.4 — ห้ามวินิจฉัยฟันธง ให้แค่สมมติฐานและการจำลองเพื่อตรวจสอบเท่านั้น ตัวอย่างเช่น ให้หนึ่งบรรทัด "ทหารโล่ vs เมจ 0.68 (z สูงสุด)" แล้วร้องขอแบบนี้
[ช่องที่พัง]
ทหารโล่ → เมจ อัตราชนะ 0.68 (เส้นสมดุล 0.50, z สูงสุดในเมทริกซ์)
ข้อมูลเสริม: ระยะเวลาเฉลี่ยของแมตช์นี้ 38s (ค่าเฉลี่ยรวม 22s)
[ข้อมูลที่เกี่ยวข้อง]
- ทหารโล่: พาสซีฟ "กำแพงเหล็ก" รับดาเมจ -30%, สกิลปิดปาก "ทุบโล่" (2 วินาที)
- เมจ: 70% ของดาเมจทั้งหมดกระจุกอยู่ในสกิลที่ต้องร่าย 1.5 วินาที
- ความถี่ในการเจอกันของสองอาชีพนี้อยู่อันดับต้นในคิวที่วัดจริง (คู่ยอดนิยม)
คำขอ: สมมติฐานสาเหตุที่เป็นไปได้ของการพังทลายในความสัมพันธ์นี้ 3~5 ข้อ + การจำลองตรวจสอบของแต่ละข้อ 1 บรรทัด
ห้ามวินิจฉัยฟันธง ให้แค่ระดับ "อาจเป็นไปได้ว่า~" เท่านั้น
LLM จะทำได้แค่โยนสมมติฐานที่ช่วยจำกัดพื้นที่การค้นหาให้แคบลง เช่น "อาจเป็นฟีดแบ็กเชิงบวกที่กำแพงเหล็ก -30% กับการปิดปาก 2 วินาทีซ้อนทับกัน จนเมจตายโดยไม่ได้ใส่สกิลร่ายหลักได้แม้แต่ครั้งเดียว / ตรวจสอบ: ลดระยะเวลาปิดปากเหลือ 1 วินาที แล้วจำลองช่องเดิมซ้ำ" ส่วนว่าอะไรคือของจริงนั้น ตัดสินด้วยการรัน build_winrate_matrix อีกครั้งตามแต่ละตัวเลือก การที่ LLM ดึงเอาเบาะแสที่ว่าระยะเวลาแมตช์เป็น 1.7 เท่าของค่าเฉลี่ยมาร้อยเข้ากับสมมติฐานด้วย — การเชื่อมโยงที่คนกวาดดู 56 ช่องแล้วพลาดได้ง่ายนั้น คือเวลาที่ AI ทำให้ได้คืน ณ จุดนี้
ต่อให้ปรับเมทริกซ์อัตราชนะให้ลงตัวสมบูรณ์แบบ สาเหตุที่แท้จริงที่ผู้เล่นรู้สึกว่า "แพ้" ก็ยังอยู่ที่อื่น นั่นคือ เจอกับใคร ถ้าผู้เล่นที่มีฝีมือ 1500 ไปเจอผู้เล่นฝีมือ 2200 ต่อให้ความสัมพันธ์ของอาชีพเป็น 5:5 ผลก็ถูกกำหนดไว้แล้ว เพราะอย่างนั้นแมตช์เมกกิงจึงไม่ใช่แค่ฟังก์ชันของเซิร์ฟเวอร์ แต่เป็นส่วนหนึ่งของบาลานซ์ ถ้าเมทริกซ์รับผิดชอบความเป็นธรรมระหว่างอาชีพ แมตช์เมกกิงก็รับผิดชอบความเป็นธรรมระหว่างฝีมือ
เกมแข่งขันส่วนใหญ่มี MMR (Matchmaking Rating, คะแนนแมตช์เมกกิง) เป็นคะแนนซ่อนที่ชนะแล้วขึ้น แพ้แล้วลง และจับคู่ผู้ที่มีคะแนนใกล้เคียงกันเข้าด้วยกัน การอัปเดตคะแนนเป็นสูตรแบบกำหนด — Elo ถูกใช้กันแพร่หลายที่สุด และเป็นมาตรฐานสาธารณะ จึงเป็นหนึ่งในไม่กี่สูตรที่หนังสือเล่มนี้อ้างอิงได้
# Elo: สูตรอัปเดตมาตรฐานสาธารณะ (ไม่ใช่ค่าที่กุขึ้น)
expected_a = 1 / (1 + 10 ** ((rating_b - rating_a) / 400))
new_rating_a = rating_a + K * (score_a - expected_a)
# score_a: ชนะได้ 1 แพ้ได้ 0
# K: ค่าคงที่ความแรงของการอัปเดต (ค่าที่เกมเป็นผู้กำหนด ปกติเลือกในช่วง 16~40)
# 400, 10: ค่าคงที่ที่ตรึงอยู่ในนิยามของ Elo
ตัวสูตรนี้เองเป็นแบบกำหนด ไม่ใช่จุดที่ AI จะเข้าไป แต่ในแมตช์เมกกิงมี ความตึงเครียด หนึ่งอย่างที่แก้ไม่ตกด้วยสูตรแบบกำหนดเพียงอย่างเดียว นั่นคือการแลกเปลี่ยนระหว่าง ความเป็นธรรม ↔ เวลารอคอย ถ้าจับคู่เฉพาะคู่ต่อสู้ที่คะแนนเท่ากันเป๊ะ แมตช์ก็จะเป็นธรรม แต่ถ้าไม่มีคู่ต่อสู้แบบนั้นอยู่ในคิว ผู้เล่นก็ต้องรอ 10 นาที ถ้ายอมให้คะแนนต่างกันได้มากขึ้น ก็จะจับคู่ได้เร็วแต่แมตช์ก็จะไม่เป็นธรรม ยิ่งเป็นช่วงเวลาดึก อาชีพที่ไม่นิยม หรือช่วงคะแนนสูง ความตึงเครียดนี้ก็ยิ่งรุนแรง
flowchart LR
A["คำขอจับคู่
MMR ผู้เล่น 1500"] --> B{"มีคู่ต่อสู้ ±50 ในคิวหรือไม่?"}
B -->|มี| C["จับคู่ทันที
เป็นธรรมที่สุด"]
B -->|ไม่มี| D["ขยายช่วงที่ยอมรับได้
ตามเวลารอ ±50→±200"]
D --> E{"จับคู่สำเร็จ?"}
E -->|สำเร็จ| F["จับคู่
สมดุลความเป็นธรรม ↕ เวลารอ ↕"]
E -->|หมดเวลา| G["ใส่บอต / คงคิวไว้
เป็นการตัดสินเชิงนโยบาย"]
C --> H["อัปเดต Elo (แบบกำหนด)"]
F --> H
style H fill:#dbeafe,stroke:#2563eb,stroke-width:2px
style D fill:#ffedd5,stroke:#ea580c
style G fill:#ffedd5,stroke:#ea580c
มีแต่โหนดสีน้ำเงิน (อัปเดต Elo) เท่านั้นที่เป็นแบบกำหนด โหนดสีส้ม — จะขยายช่วงที่ยอมรับได้เมื่อไรและเท่าไร จะทำอะไรเมื่อหมดเวลา — คือจุดที่ AI ช่วยเข้าถึง แต่ในที่นี้ AI ก็ไม่ได้เป็นผู้ตัดสิน การจับคู่แบบเรียลไทม์ นั่นเป็นลอจิกของเซิร์ฟเวอร์ที่ต้องรวดเร็วและทำซ้ำได้ จึงเป็นที่ของโค้ดอิงกฎ สิ่งที่ AI เข้ามาแนบคือการวิเคราะห์เพื่อจูนกฎเหล่านั้น เป็นงานสรุปว่า "ในล็อกการจับคู่สัปดาห์ที่แล้ว คุณภาพแมตช์ (ความแปรปรวนของอัตราชนะ·เวลารอคอย) ของช่วงคะแนน·ช่วงเวลา·อาชีพใดที่แย่" และเสนอตัวเลือกว่า "ถ้าเปลี่ยนเส้นโค้งช่วงที่ยอมรับได้อย่างไร เวลารอคอยของช่วงใดจะลดลง" ตำแหน่ง 3 (รายงาน)·ตำแหน่ง 4 (ตีความความผิดปกติ)·ตำแหน่ง 2 (ค้นหาตัวเลือกการเปลี่ยนแปลง) ของ 8.4 ย้ายเวทีมาอยู่ที่ล็อกการจับคู่เท่านั้นเอง
ขอชี้จุดที่แมตช์เมกกิงเกี่ยวพันกับเมทริกซ์อัตราชนะไว้ด้วย ถ้าอัลกอริทึมการจับคู่ไม่คำนึงถึงอาชีพแล้วจับเฉพาะคะแนนให้ตรงกัน ช่องความสัมพันธ์ที่พังก็จะถูกเปิดโปงออกมาตรง ๆ ถ้าช่องที่ทหารโล่ชนะเมจ 68% ยังคงอยู่ แล้วการจับคู่มักจับสองอาชีพนี้มาเจอกันบ่อย ความรู้สึกพ่ายแพ้ที่ผู้เล่นเมจสัมผัสได้ก็จะสะสมมากกว่าตัวเลขในเมทริกซ์เสียอีก ด้วยเหตุนี้ การตรวจสอบเมทริกซ์กับการวิเคราะห์ล็อกการจับคู่จึงไม่ได้แยกกันหมุน แต่เป็น ทางเข้าและทางออกของวงจรเดียวกัน — แก้ช่องที่พังในเมทริกซ์ แล้วยืนยันในล็อกการจับคู่ว่าช่องนั้นถูกจับมาเจอกันจริงมากน้อยเพียงใด
ทุกเรื่องที่กล่าวมาจนถึงตอนนี้ — เมทริกซ์ MMR การจำลอง — ล้วนวางอยู่บนสมมติฐานหนึ่งอย่างโดยปริยาย นั่นคือ ผลลัพธ์ที่ผู้เล่นรายงานเป็นความจริง ใน PvE เรื่องนี้แทบไม่เป็นปัญหา เล่นล้มบอสคนเดียวจะไปหลอกใคร แต่ใน PvP มีคู่ต่อสู้ ชนะแล้วคะแนนขึ้น ดังนั้นจึง เกิดแรงจูงใจที่จะโกง เมื่อปรากฏไคลเอนต์ที่ปลอมแปลงดาเมจ ปลอมแปลงตำแหน่ง และเพิกเฉยต่อคูลดาวน์ขึ้นมา สูตรแบบกำหนดของ 8.1 ก็เป็นแบบกำหนดอยู่แค่บนกระดาษเท่านั้น บนเซิร์ฟเวอร์จริง นักดาบของใครบางคนกำลังใส่ดาเมจแรงกว่าสูตรเป็นสองเท่าอยู่
ด้วยเหตุนี้ กฎบาลานซ์ข้อแรกของเกมแข่งขันจึงมาก่อนเมทริกซ์ อย่าให้ไคลเอนต์เป็นผู้กำหนดผลลัพธ์ การคำนวณดาเมจ การตัดสินคูลดาวน์ การตัดสินการเข้าเป้า — การคำนวณทุกอย่างที่แตะถึงบาลานซ์ เซิร์ฟเวอร์เป็นผู้ถือสิทธิ์การตัดสิน ไคลเอนต์ส่งมาแค่อินพุต (จะเคลื่อนที่ไปที่ใด ใช้สกิลใด) ส่วนว่าอินพุตนั้นตรงกับสูตรหรือไม่ คูลดาวน์ครบหรือยัง อยู่ในระยะหรือไม่ เซิร์ฟเวอร์เป็นผู้ตรวจสอบซ้ำทั้งหมด "ดาเมจ 999" ที่ไคลเอนต์ส่งมาเซิร์ฟเวอร์จะเพิกเฉย และนำมาใช้แค่ค่าที่เซิร์ฟเวอร์คำนวณด้วยสูตรเท่านั้น
ถ้าสิทธิ์การตัดสินของเซิร์ฟเวอร์พังลง งานบาลานซ์ทั้งหมดก็จะกลายเป็นเรื่องเท็จ ต่อให้ปรับเมทริกซ์อัตราชนะให้ลงตัวอย่างประณีตเพียงใด ถ้าในเซิร์ฟเวอร์จริงมีอาชีพหนึ่งปลอมแปลงดาเมจ เมทริกซ์นั้นก็เป็นเพียงคำสัญญาบนกระดาษ ด้วยเหตุนี้ ระบบกันโกงจึงไม่ใช่งานความปลอดภัยที่แยกต่างหาก แต่เป็นปัญหาความน่าเชื่อถือของข้อมูลบาลานซ์ เมื่ออัตราชนะในเซิร์ฟเวอร์จริงคลาดเคลื่อนจากเมทริกซ์จำลองมาก สิ่งแรกที่ควรสงสัยไม่ใช่ "สูตรผิดหรือเปล่า" แต่ควรเป็น "ข้อมูลนี้สะอาดหรือเปล่า"
ตรงนี้ที่ของ AI ก็ชัดเจนขึ้นอีกครั้ง ตัวการตัดสินว่าโกง — "ทำให้อินพุตนี้เป็นโมฆะ" — เป็นงานของกฎแบบกำหนด อินพุตที่เคลื่อนที่ 30 เมตรในเวลา 0.1 วินาทีนั้นเป็นไปไม่ได้ทางกายภาพ จึงสกัดด้วยกฎ อินพุตเดียวกันต้องได้คำตัดสินเดียวกัน และต้องไม่ทำให้เกิดการแบนที่ไม่เป็นธรรม จึงวาง LLM เชิงความน่าจะเป็นไว้ตรงนี้ไม่ได้ ในทางกลับกัน งานคัดแพตเทิร์นผิดปกติออกมาเป็น ตัวเลือก คือจุดที่ AI ช่วยเข้าถึง รวบรวมตัวเลือกจากล็อกเซิร์ฟเวอร์ เช่น "การกระจายอัตราการเข้าเป้าของบัญชีนี้เบนออกจากการกระจายของมนุษย์ไป z เท่านี้" "กลุ่มบัญชีนี้ใช้แพตเทิร์นผิดปกติแบบเดียวกันร่วมกัน" แล้วยกขึ้นให้มนุษย์ตรวจสอบ เมื่อย้ายตารางของ 8.1 มายัง PvP เส้นแบ่งก็จะเป็นแบบนี้
| พื้นที่ | AI | เหตุผล |
|---|---|---|
| การตัดสินดาเมจ·การเข้าเป้า·คูลดาวน์ของเซิร์ฟเวอร์ | ห้ามเด็ดขาด | แกนกลางแบบกำหนด ถ้าอินพุตเดียวกัน=คำตัดสินเดียวกันแตกหัก ความเป็นธรรมก็พังทลาย |
| การอัปเดตคะแนน Elo/MMR | ห้ามเด็ดขาด | สูตรแบบกำหนดมาตรฐานสาธารณะ ถ้าสั่นคลอนอันดับก็จะเป็นเท็จ |
| ตัวการตัดสินสกัดการโกง (แบน) เอง | ห้ามเด็ดขาด | การแบนที่ไม่เป็นธรรมเกิดไม่ได้ หลักฐานเดียวกัน=คำตัดสินเดียวกัน |
| การจำลองเมทริกซ์อัตราชนะ | ห้ามเด็ดขาด | ถ้าทำซ้ำไม่ได้ คำว่า "อาชีพแข็งขึ้น" ก็จะเป็นเท็จ |
| การตรวจหา·ตีความช่องความสัมพันธ์ที่พัง | ทำได้ | คัดช่องด้วย z-score และ LLM ตั้งสมมติฐาน (ห้ามวินิจฉัยฟันธง) |
| การวิเคราะห์คุณภาพล็อกการจับคู่·ตัวเลือกการจูน | ทำได้ | เสนอตัวเลือกการเปลี่ยนแปลงของการแลกเปลี่ยนเวลารอ/ความเป็นธรรม (ตรวจสอบด้วยการจำลอง) |
| การคัดตัวเลือกแพตเทิร์นที่ต้องสงสัยว่าโกง | ทำได้ | เฉพาะตัวเลือกที่จะยกขึ้นให้มนุษย์ตรวจสอบ การตัดสินใจแบนเป็นเรื่องของมนุษย์·กฎ |
เส้นแบ่งไม่ต่างจาก 8.1 แม้แต่ตัวอักษรเดียว AI อาศัยอยู่นอกแกนกลางแบบกำหนดเท่านั้น ด้านในที่บังคับใช้ — ดาเมจ คะแนน แบน — คือรูลบุ๊ก ส่วนด้านนอกที่ตรวจหา ตีความ และดันตัวเลือกขึ้นมา คือที่ของ AI
สามหัวข้อ — เมทริกซ์ การจับคู่ สิทธิ์การตัดสินของเซิร์ฟเวอร์ — ไม่ใช่งานสามชิ้นที่หมุนแยกกัน แต่เป็นสามช่วงของวงจรบาลานซ์การแข่งขันหนึ่งวงจร สิทธิ์การตัดสินของเซิร์ฟเวอร์รับประกันข้อมูลที่สะอาด ใช้ข้อมูลนั้นตรวจสอบเมทริกซ์ ใช้ล็อกการจับคู่ยืนยันว่าผลการตรวจสอบนั้นถูกสัมผัสในคิวจริงอย่างไร แล้วตรวจสอบตัวเลือกด้วยการจำลองอีกครั้งเพื่อนำไปใส่ในบิลด์
flowchart TD
A["สิทธิ์การตัดสินของเซิร์ฟเวอร์ + ระบบกันโกง
ข้อมูลไลฟ์ที่สะอาด"] --> B["เมทริกซ์อัตราชนะไลฟ์
(ค่าวัดจริง)"]
B --> C{"คลาดเคลื่อนจากเมทริกซ์จำลอง
มากหรือไม่?"}
C -->|"คลาดเคลื่อน → สงสัยข้อมูล"| A
C -->|"ตรงกัน → ปัญหาบาลานซ์"| D["ตรวจหาช่องที่พังด้วย z-score"]
D -->|"สมมติฐาน LLM 3~5"| E["ตัวเลือกการเปลี่ยนแปลง + จำลองตรวจสอบ"]
E --> F["build_winrate_matrix
จำลองซ้ำตามแต่ละตัวเลือก (แบบกำหนด)"]
F --> G["ผู้ปรับบาลานซ์รับ/ปฏิเสธ"]
G --> H["นำเข้าบิลด์ (ย้อนกลับไม่ได้)"]
H --> I["วิเคราะห์ล็อกการจับคู่
ตรวจสอบความรู้สึก + ตัวเลือกการจูน"]
I --> A
style A fill:#dbeafe,stroke:#2563eb,stroke-width:2px
style F fill:#dbeafe,stroke:#2563eb,stroke-width:2px
style D fill:#ffedd5,stroke:#ea580c
style E fill:#ffedd5,stroke:#ea580c
style I fill:#ffedd5,stroke:#ea580c
โหนดสีน้ำเงิน (สิทธิ์การตัดสินของเซิร์ฟเวอร์, การคำนวณจำลองซ้ำ) คือแบบกำหนด ส่วนโหนดสีส้ม (การตรวจหา·สมมติฐาน·วิเคราะห์การจับคู่) คือ AI ช่วย ความล้มเหลวที่พบบ่อยที่สุดในวงจรนี้คือการข้ามแยก C ไป เมื่อเมทริกซ์ไลฟ์คลาดเคลื่อนจากการจำลองแล้วลงมือแก้สูตรในทันที จริง ๆ แล้วก็จะกลายเป็นการไล่ตามข้อมูลที่ปนเปื้อนจากการโกง แล้วเนิร์ฟอาชีพที่ไม่ได้มีปัญหาอะไร แยกหนึ่งแยกที่สงสัยความสะอาดของข้อมูลก่อนนี้ ทำหน้าที่เดียวกับ "ประวัติการเปลี่ยนแปลง" ของ 8.1 ในบริบท PvP — ถ้าลืม ตีสองก็จะหวนกลับมา
สุดท้าย ขอทิ้งกับดักของบาลานซ์ PvP จากประสบการณ์ 18 ปีไว้สองสามข้อพร้อมแนวทางแก้
ที่ของ AI ใน PvP เหมือนกับใน PvE การตรวจหา·ตีความ·ตัวเลือกที่อยู่นอกแกนกลางแบบกำหนด — ดาเมจ คะแนน แบน การจำลอง แกนกลางปกป้องด้วยโค้ดและสิทธิ์การตัดสินของเซิร์ฟเวอร์ และทุ่นแรงเฉพาะงานมือที่คนเคยกวาดดู 56 ช่องและหลงทางอยู่ในล็อกการจับคู่เท่านั้น — บทนี้คือหลักฐานชิ้นสุดท้ายว่าส่วนนี้ทั้งหมดขับเคลื่อนด้วยโครงร่างเดียว
setup สร้าง simulate_pvp_match ที่ขยาย simulate_dps ของ 8.4 มาเป็น 1:1 และ bot_decide (ฮิวริสติกแบบกำหนด) ที่ใช้รันบอตทั้งสองฝั่ง เริ่มจากตรึง seed เพื่อตรวจว่าเมทริกซ์เดียวกันทำซ้ำได้หรือไม่ก่อน ดาเมจต้องนำสูตรของ 8.1 มาใช้ตรง ๆ เสมอ และบันทึกระดับการเล่นที่บอตเลียนแบบไว้หนึ่งบรรทัด
prompt หลังจากใช้ find_broken_cells คัดช่องที่อยู่นอกเส้นสมดุล (0.40\~0.60) แล้ว ส่งให้ LLM เพียงช่องเดียวที่ z สูงสุด
สำหรับช่องที่พังที่แนบมา (ทหารโล่ vs เมจ 0.68, ระยะเวลาแมตช์ 38s/เฉลี่ย 22s)
ช่วยเสนอสมมติฐานสาเหตุที่เป็นไปได้ 3~5 ข้อ และการจำลองซ้ำเพื่อตรวจสอบของแต่ละข้อ 1 บรรทัด
ข้อมูลสกิล·พาสซีฟที่เกี่ยวข้องแนบไว้ด้านล่าง ห้ามวินิจฉัยฟันธง — ให้แค่ "อาจเป็นไปได้ว่า~"
อย่าแก้ตัวเลขโดยตรง ให้เสนอแค่ตัวเลือกเท่านั้น
verify อย่าเชื่อสมมติฐานของ AI ตรง ๆ นำตัวเลือกการเปลี่ยนแปลงของแต่ละสมมติฐานใส่ลงใน build_winrate_matrix แล้วจำลองซ้ำด้วย seed เดียวกัน และตรวจสอบไปพร้อมกันว่าช่องนั้นกลับมาใกล้ 0.50 ขณะที่ ไม่ทำให้ช่องอื่นพัง หรือไม่ (การเปลี่ยน PvP มักจะแก้ช่องหนึ่งแล้วทำช่องข้าง ๆ พังได้ง่าย) รับเฉพาะตัวเลือกที่ตรงทั้งสองเงื่อนไข และเหมือน 8.1 ให้บันทึกเหตุผล·ตัวเลือกที่ปฏิเสธ·ค่าที่คาดการณ์ไว้ในล็อกการตัดสินใจ หลังนำเข้าบิลด์ 1 สัปดาห์ ให้เพิ่มอัตราชนะที่วัดจริงในไลฟ์ลงในล็อกนั้น
ต่อให้เป็นโปรโตไทป์คนเดียวที่มีแค่สองอาชีพและไม่มีเซิร์ฟเวอร์ โครงร่างก็เหมือนกัน เมทริกซ์ขนาด 2×2 ก็เพียงพอ และการจำลองก็แค่เพิ่มนโยบายบอตหนึ่งบรรทัด (พอคูลดาวน์ครบก็ใช้สกิลดาเมจสูงสุด) ลงในลูป 30 บรรทัดของ 8.1 สิทธิ์การตัดสินของเซิร์ฟเวอร์ก็แค่ยึดหลัก "ไม่ให้ไคลเอนต์กำหนดผลลัพธ์ได้" ไว้ในโครงสร้างโค้ด ส่วนระบบกันโกงเต็มรูปแบบยังไม่จำเป็นจนกว่าจะมีผู้เล่น MMR ก็ละไว้ก่อนตอนแรก แล้วรัน 1,000 ตาเพื่อตรวจแค่ว่าเมทริกซ์เอนไปข้างใดข้างหนึ่งเกิน 60% หรือไม่ AI ใช้แค่อ่านผลนั้นแล้วสรุปว่า "แมตช์ไหนพัง และเพราะอะไรได้บ้าง" เท่านั้น เส้นเดียวที่ต้องยึดไม่ว่าขนาดเล็กใหญ่ — ดาเมจและการแพ้ชนะให้โค้ดและเซิร์ฟเวอร์เป็นผู้กำหนด และไม่มอบหมายให้ LLM เด็ดขาด
ผู้อ่านกลุ่มแรก: นักออกแบบ UX ที่รับผิดชอบ HUD และ UI (ทีมขนาดกลาง 10\~50 คน) เวอร์ชันย่อสำหรับผู้อ่านที่ทำคนเดียว/เป็นงานอดิเรก: §9.1.8 「ถ้าทำคนเดียวก็แค่นี้พอ」
ในวันที่นำการแจ้งเตือนดีบัฟตัวใหม่ขึ้นไปวางบน HUD ในบิลด์ QA ดีไซเนอร์บอกว่า "เห็นชัดดี" แต่วันรุ่งขึ้นบนกระดานข้อความของผู้ใช้กลับมีโพสต์ว่า "ตายเพราะมองไม่เห็นดีบัฟ" การแจ้งเตือนนั้นลอยอยู่กลางจอ เป็นตัวอักษรสีเหลืองจางบนพื้นหลังสีเทา บนจอมอนิเตอร์ของดีไซเนอร์มันมองเห็น แต่บนโทรศัพท์ 6 นิ้วที่เอฟเฟกต์การระเบิดระหว่างการต่อสู้ปกคลุมทั้งจอ มันมองไม่เห็น ปัญหาคือนี่ไม่ใช่ครั้งแรก ทุกบิลด์ ทุกหน้าจอ อุบัติเหตุชนิดเดียวกันนี้เกิดซ้ำภายใต้คำว่า "ครั้งนี้คงไม่เป็นไรหรอก"
บทนี้มุ่งไปที่งานชิ้นเดียวที่ตัดวงจรการเกิดซ้ำนั้น นั่นคือ lint gate ที่รับภาพหน้าจอ HUD ที่เสร็จสมบูรณ์หนึ่งภาพเป็นอินพุต แล้วตรวจจับโดยอัตโนมัติว่าองค์ประกอบ P0 หลุดออกจากพื้นที่ที่สายตาไปถึง (แถบสถานะด้านบน · มุมแอ็กชันซ้าย-ขวาล่าง) หรือไม่ และคอนทราสต์ของตัวอักษรเกินเกณฑ์ความอ่านออกหรือไม่ หลักการทั่วไปของการออกแบบ HUD อย่างตารางลำดับความสำคัญ · กระแสสายตา · การแยกตามแพลตฟอร์ม มีอยู่ในหนังสือเล่มอื่นอย่างเพียงพอแล้ว ดังนั้นบทนี้จึงใช้เนื้อที่ไปกับ ลูปการตรวจสอบที่บังคับใช้หลักการเหล่านั้นโดยอัตโนมัติในทุกบิลด์ เท่านั้น แก่นคือการทำให้ AI มองหน้าจอแล้วพูดออกมาเป็นพิกัดและตัวเลขว่า "ตัวอักษรนี้มีคอนทราสต์ 2.0:1 จึงไม่ถึง WCAG 4.5:1" เราแทนที่การเถียงกันว่า "ก็เห็นชัดดีนี่นา" ด้วยโค้ดและมาตรฐาน
เหตุที่การตรวจสอบ HUD ได้ข้อสรุปต่างกันไปในแต่ละคนทุกครั้ง ก็เพราะเกณฑ์เป็นเรื่องอัตวิสัยอย่าง "เห็นชัด/ไม่ชัด" โชคดีที่ความอ่านออกและการเข้าถึง (accessibility) ส่วนใหญ่นั้น องค์กรมาตรฐานได้ตรึงไว้เป็นตัวเลขเรียบร้อยแล้ว เราไม่จำเป็นต้องกุขึ้นเอง
| รายการตรวจสอบ | เกณฑ์มาตรฐาน (แหล่งอ้างอิง) | การตัดสินอัตโนมัติ |
|---|---|---|
| คอนทราสต์ตัวอักษรทั่วไป | ตั้งแต่ 4.5:1 ขึ้นไป (WCAG 2.1 SC 1.4.3) | ทำได้ — คำนวณจากค่าสีพื้นหน้า·พื้นหลัง |
| คอนทราสต์ตัวอักษรขนาดใหญ่ (18pt+) | ตั้งแต่ 3:1 ขึ้นไป (WCAG 2.1 SC 1.4.3) | ทำได้ |
| คอนทราสต์องค์ประกอบที่ไม่ใช่ตัวอักษร (ไอคอน·เกจ) | ตั้งแต่ 3:1 ขึ้นไป (WCAG 2.1 SC 1.4.11) | ทำได้ |
| ขนาดต่ำสุดของพื้นที่สัมผัส | 44×44 pt (Apple HIG) / 48×48 dp (Material) | ทำได้ — จากขนาดองค์ประกอบ |
| พื้นที่ที่นิ้วโป้งเอื้อมถึง | เมื่อ จับแนวนอนสองมือ มุมล่างซ้าย·ขวาเป็นโซน 'ง่าย' (นิ้วโป้งซ้าย=เคลื่อนที่, นิ้วโป้งขวา=สกิล) เป็นโมเดล thumb-zone ที่ใช้กันทั่วในวงการ | บางส่วน — ใช้กฎเชิงพื้นที่ |
มีเพียงบรรทัดสุดท้าย (พื้นที่ที่นิ้วโป้งเอื้อมถึง) เท่านั้นที่ไม่ใช่เส้นผ่านเชิงปริมาณ แต่เป็นโมเดลที่ใช้กันทั่วในวงการ ส่วนสี่บรรทัดบนเป็นเส้นผ่านที่ W3C · Apple · Google เปิดเผยไว้ คอนทราสต์นั้นชัดเจนเป็นพิเศษ WCAG เปิดเผยถึงขั้นสูตรคำนวณว่าให้คิดความสว่างสัมพัทธ์ (relative luminance) ของสองสีจาก (L1+0.05)/(L2+0.05) เมื่อนำคอนทราสต์ของตัวอักษรสีเหลืองจาง (#D4C84A) บนพื้นหลังสีเทา (#888) ใส่ลงในสูตรนี้ จะได้ประมาณ 2.0:1 — ไม่ถึง 4.5:1 กล่าวคือไม่ผ่านอย่างชัดเจนตามมาตรฐาน นี่คือจุดที่ข้อโต้แย้ง "บนจอมอนิเตอร์ของดีไซเนอร์มันก็เห็น" ใช้ไม่ได้
ตรงนี้ขอทำสิ่งหนึ่งให้ชัด หน้าจอบนมือถือของ MMORPG · RPG นั้น แนวนอน (landscape) คือมาตรฐาน เหตุผลคือปริมาณข้อมูลและการควบคุม ที่ขนาดนิ้วเท่ากัน หากจับเป็นแนวนอน ข้อมูลที่แสดงตลอดเวลาในหนึ่งหน้าจอจะมากกว่าแนวตั้ง และยังควบคุมซ้าย (เคลื่อนที่)·ขวา (สกิล) พร้อมกันด้วยนิ้วโป้งสองมือได้ การจับแนวตั้งมือเดียวเหมาะกับเกมพัซเซิลแคชวลและเกม idle แต่ไม่เหมาะกับ MMORPG ที่มีข้อมูลพร้อมกันมากและต้องควบคุมสองมือ ด้วยเหตุนี้ การตัดสินเรื่องสายตาและการจัดวางทั้งหมดในบทนี้จึงตั้งอยู่บนสมมติฐานของการจับแนวนอนสองมือ หน้าจอแบ่งออกเป็น แถบสถานะแนวนอนด้านบน · มุมแอ็กชันล่างสองมุมซ้ายขวา · พื้นที่เกมตรงกลางระหว่างทั้งสอง และแถบช่องล่างกลาง (ของใช้สิ้นเปลือง · ไอเทมอัตโนมัติ · ควิกสล็อต) ที่อยู่ใต้พื้นที่เกม
ห้าบรรทัดนี้คือ rulebook สำหรับการตรวจสอบ ที่เราจะมอบให้ AI ในบทนี้ การที่จะพูดได้ว่า "ข้อความดีบัฟมีคอนทราสต์ 2.0:1 จึงละเมิด SC 1.4.3" แทนที่จะเป็น "ดีบัฟดูเหมือนจะมองไม่ค่อยเห็น" เท่านั้น จึงจะทำให้ไม่ว่าคนตรวจหรือ AI ตรวจ ก็ได้ผลตัดสินเดียวกัน
เมื่อวางเกณฑ์ของแพลตฟอร์ม PC ไว้เคียงกัน จุดเริ่มของการตรวจสอบก็ชัดขึ้น โปรเจกต์ A เป็นแบบมือถือมาก่อน + PC เป็นตัวเสริม ดังนั้นจึงวางเกณฑ์ทั้งสองไว้ใน rulebook
| เกณฑ์ | PC (แพลตฟอร์มเสริม) | มือถือ (แพลตฟอร์มหลัก, แนวนอน) |
|---|---|---|
| หน้าจอ·อินพุต | 27 นิ้ว+ / เมาส์แม่นยำ 1px · hover · ปุ่มลัด | 6.x นิ้วแนวนอน / นิ้วโป้งสองมือ ไม่มี hover |
| ข้อมูลที่แสดงตลอดเวลาพร้อมกัน | รับได้ 30\~50 ชนิด | จำกัดที่ 12\~16 ชนิด (การประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ) |
| ระยะที่สายตา·การควบคุมเอื้อมถึง | ทั่วทั้งหน้าจอ (เคอร์เซอร์ไปถึงได้ทุกที่) | เฉพาะแถบสถานะด้านบน + มุมล่างซ้าย·ขวา + แถบช่องล่างกลางที่เป็นโซน 'ง่าย' |
| ความแม่นยำ | คลิก 1px | พื้นที่สัมผัสต่ำสุด 44pt (HIG) |
| ความเสี่ยงหลักของการตรวจสอบ | ภาระทางการรับรู้จากข้อมูลที่แน่นเกินไป | หน้าจอแคบ + นิ้วบัง + ถูกกลบที่ตรงกลาง |
PC มีความแม่นยำของเมาส์ · ทูลทิปแบบ hover · จอใหญ่ จึงต่อให้แสดงข้อมูลมากสายตาและการควบคุมก็ยังเอื้อมถึง ส่วนมือถือเพราะเป็นแนวนอนจึงดีกว่าแนวตั้ง แต่ก็รับได้ไม่เท่า PC อีกทั้งองค์ประกอบที่ต้องกดถูกผูกไว้ที่มุมนิ้วโป้งสองข้าง และเพราะไม่มี hover ข้อมูล P0 จึงต้องแสดงตลอดเวลา ด้วยเหตุนี้ แก่นของการตรวจสอบ HUD บนมือถือจึงไม่ใช่ "สวยไหม" แต่เป็น "P0 อยู่ในตำแหน่งที่สายตาเอื้อมถึง (ด้านบน · สองมุม) และตัวอักษรเกินคอนทราสต์มาตรฐานหรือไม่" การตรึงสิ่งนี้ไว้เป็นมาตรฐานเพื่อไม่ให้ผลตัดสินสั่นคลอนต่างกันไปในแต่ละคน คือหน้าที่ของบทนี้
จะแสดงให้เห็นว่าจริง ๆ แล้วรันอย่างไรหนึ่งรอบจนจบ ด้านล่างคือการจำลองเซสชันการตรวจสอบ HUD การต่อสู้ของโปรเจกต์ของผู้เขียน (MMORPG แบบมือถือมาก่อน ต่อไปจะเรียกว่า "โปรเจกต์ A") อย่างซื่อตรง พรอมต์อินพุตสามารถคัดลอกไปใช้ได้ตามนั้น ส่วนผลลัพธ์เป็นการเรียบเรียงเซสชันจริงขึ้นใหม่
ถ้าโยนแค่ภาพหน้าจอ AI จะ "เดา" หน้าจอเอา ดังนั้นจึงใส่พิกัด·สี·การจัดประเภทขององค์ประกอบที่บิลด์รู้อยู่แล้วไปด้วยในรูปของ manifest นี่ไม่ใช่การเขียนขึ้นใหม่ แต่แค่ดึงออกมาจากผลผลิตของบิลด์ก็พอ (ความจริงของวิธีการดึงนั้นจะเปรียบเทียบอย่างซื่อตรงใน §9.1.4)
# hud_capture_manifest.yaml — แนบไปกับภาพหน้าจอบิลด์ QA
screen: { w_pt: 844, h_pt: 390 } # 6.x นิ้วแนวนอน, หน่วย pt (จับแนวนอน)
elements:
- id: hp_bar # แถบพลังชีวิต
class: P0
rect_pt: [12, 18, 150, 16] # x, y, w, h — บนซ้าย
fg: "#FF5A5A" ; bg: "#1A1A1A"
- id: skill_slot_1 # ช่องสกิล (นิ้วโป้งขวา)
class: P0
rect_pt: [760, 300, 40, 40] # ← มุมล่างขวา, สังเกตขนาด
fg: "#FFFFFF" ; bg: "#202830"
- id: debuff_alert # การแจ้งเตือนดีบัฟ (เพิ่มเมื่อวาน)
class: P0
rect_pt: [400, 180, 70, 24] # ← กลางจอ, สังเกตตำแหน่ง
fg: "#D4C84A" ; bg: "#888888" # ← สังเกตคอนทราสต์
- id: minimap
class: P1
rect_pt: [744, 20, 80, 80] # บนขวา
fg: "#A0C0FF" ; bg: "#101820"
ภาพหน้าจอที่แนบมาคือ HUD การต่อสู้ของโปรเจกต์ A (จับแนวนอนสองมือ) ส่วน yaml คือพิกัด·สี·การจัดประเภทของแต่ละองค์ประกอบบนหน้าจอนั้น ช่วยเทียบสองอย่างนี้แล้วตรวจสอบให้หน่อย
คอนทราสต์ให้คำนวณ WCAG จาก fg/bg แล้วเขียนค่าตัวเลขมาด้วย — ตัวอักษร 4.5:1, ไอคอน·ตัวอักษรใหญ่ 3:1 ถ้าไม่ถึงให้ FAIL
ถ้า P0 หลุดออกจากแถบสถานะด้านบนหรือมุมล่างซ้าย·ขวา ไปลอยอยู่กลางจอ ให้ WARN (ตรงกลางจะถูกเอฟเฟกต์การต่อสู้กลบ)
ถ้าองค์ประกอบที่ต้องควบคุมต่ำกว่า 44pt หรือหลุดออกจากมุมนิ้วโป้ง·แถบช่องล่างกลาง ให้ FAIL
ถ้ามีอะไรที่ไม่อยู่ใน manifest แต่เห็นบนหน้าจอ ให้แจ้งแยกต่างหาก และสิ่งที่ไม่มั่นใจให้แยกเป็น 'กำกวม' แล้วส่งมาให้ฉัน
ให้มาเป็นตารางอย่างเดียว (องค์ประกอบ|การตรวจ|ค่าที่วัดได้|ผลตัดสิน|หมายเหตุ) ตัดคำอธิบายออก
// (เจตนา: P0=ข้อมูลที่ต้องหาเจอภายใน 0.5 วินาที / ของใช้สิ้นเปลือง·อัตโนมัติ·ควิกสล็อตอยู่ล่างกลางถือว่าปกติ)
จุดสำคัญคือบรรทัดที่ใช้ไปกับ "จะให้ output กฎเหล่านั้นออกมาอย่างไร" มีมากกว่าตัวกฎการตรวจสอบเสียอีก ถ้าให้อิสระแก่ AI มันจะคายคำชมออกมาอย่าง "โดยรวมเรียบร้อยดีนะ" ดังนั้นจึงตรึง สูตรคำนวณ (คอนทราสต์ WCAG), เส้นผ่าน (4.5:1·44pt), รูปแบบ output (ตาราง), หน้าที่ระบุค่าที่วัดได้ และทางออก "ถ้าไม่มั่นใจให้ส่งต่อให้คน" ไว้ล่วงหน้า ตะปูห้าตัวนี้เปลี่ยนคำชมให้กลายเป็นค่าที่วัดได้
| องค์ประกอบ | การตรวจ | ค่าที่วัดได้ | ผลตัดสิน | หมายเหตุ |
|-------------|------|--------------------------|------|----------------------------------|
| hp_bar | คอนทราสต์ | #FF5A5A/#1A1A1A = 5.7:1 | PASS | เกจที่ไม่ใช่ตัวอักษร, ผ่าน 3:1 |
| hp_bar | การจัดวาง | บนซ้าย (12,18) | PASS | แถบสถานะด้านบน P0 |
| skill_slot_1| สัมผัส | 40×40 pt, มุมล่างขวา | FAIL | ไม่ถึง HIG 44pt, ขาด 4pt (มุม OK) |
| skill_slot_1| การจัดวาง | ล่างขวา (760,300) | PASS | มุมที่นิ้วโป้งขวาเอื้อมถึง |
| debuff_alert| คอนทราสต์ | #D4C84A/#888888 = 2.0:1 | FAIL | ตัวอักษรทั่วไปไม่ถึง 4.5:1 (SC 1.4.3) |
| debuff_alert| การจัดวาง | บริเวณกลางจอ (400,180) | WARN | เป็น P0 แต่อยู่ตรงกลาง — ถูกเอฟเฟกต์การต่อสู้กลบ |
| minimap | คอนทราสต์ | #A0C0FF/#101820 = 9.8:1 | PASS | |
| minimap | การจัดวาง | บนขวา (744,20) | PASS | P1, อนุญาตด้านขวาของแถบสถานะด้านบน |
แจ้งเพิ่มเติม:
- ใน manifest มี 4 ชนิด แต่ที่มุมล่างซ้ายของภาพหน้าจอมีไอคอนกะพริบสีเหลือง
อีกหนึ่งอัน (ประมาณพิกัด ~70,330) สงสัยว่าตกหล่นจาก manifest (กำกวม —
จากหน้าจอเพียงอย่างเดียวไม่สามารถฟันธงได้ว่าเป็นองค์ประกอบอะไร)
สรุป: FAIL 2 รายการ (skill_slot_1 สัมผัส, debuff_alert คอนทราสต์), WARN 1 รายการ
(debuff_alert การจัดวาง), กำกวม 1 รายการ (ไอคอนที่ไม่ได้ลงทะเบียน)
ส่วนที่มีค่ามากที่สุดในผลลัพธ์ไม่ใช่ตารางผ่าน/ไม่ผ่าน แต่เป็น "แจ้งเพิ่มเติม" และ "กำกวม" ที่อยู่ล่างสุด จุดที่ AI จับไอคอนกะพริบที่ไม่มีใน manifest ได้จากหน้าจอ แล้วบอกว่ามันฟันธงเองไม่ได้ว่าคืออะไรจึงส่งต่อให้คน พรอมต์ที่ดีทำให้ AI สามารถพูดได้ว่า "เรื่องนี้ผมไม่รู้"
จะรับผลลัพธ์นี้มาตามนั้นไม่ได้ ตัวการตรวจสอบของ AI เองก็ต้องให้คนตรวจสอบอีกครั้ง จริง ๆ แล้วในเซสชันนี้มีหนึ่งรายการที่ถูกคนพลิกกลับด้วยมือ
คอนทราสต์ FAIL และการจัดวาง WARN ของ debuff_alert นั้นถูกต้อง สีเหลืองจางบนพื้นหลังสีเทาคือการละเมิดมาตรฐานตามที่เห็นใน §9.1.1 และการวางการแจ้งเตือน P0 ไว้กลางจอแนวนอนก็เป็นความผิดพลาดแบบฉบับที่ถูกเอฟเฟกต์การต่อสู้กลบ ถึงตรงนี้ AI ถูกต้อง
ปัญหาคือสัมผัส FAIL ของ skill_slot_1 AI เชื่อ 40×40 pt ใน manifest ตามนั้นแล้วตัดสินว่า "ไม่ถึง 44pt" แต่ในบิลด์จริง ช่องนี้แม้จะมีขนาดเชิงภาพ 40pt แต่ hitbox สัมผัสถูกขยายออกไปรอบด้านด้านละ 6pt ทำให้พื้นที่แตะจริงคือ 52pt rect_pt ของ manifest บรรจุเพียง สี่เหลี่ยมที่วาดออกมา ไม่ได้บรรจุ hitbox — กล่าวคือเป็นข้อบกพร่องของข้อมูลอินพุต ไม่ใช่การตัดสินผิดของ AI AI ตัดสินอย่างถูกต้องภายในข้อมูลที่ได้รับ (การตัดสินตำแหน่งมุมก็ถูกต้อง) และคนรู้สถานการณ์ของบิลด์ที่โค้ดไม่รู้ (การขยาย hitbox) FAIL นี้ถูกคนยับยั้ง
ดังนั้นจึงทำสองอย่างพร้อมกัน แก้สคริปต์ดึง manifest ให้ดึง hitbox ออกมาด้วย (แก้ข้อบกพร่องของข้อมูล) และขอ AI ใหม่
skill_slot_1 ขนาดเชิงภาพคือ 40pt แต่ hitbox ถูกขยายรอบด้านด้านละ 6pt ทำให้พื้นที่แตะจริงเป็น 52pt (เพิ่ม hit_rect ใน manifest แล้ว) ช่วยดูเรื่องสัมผัสใหม่ด้วยเกณฑ์นี้
ส่วน debuff_alert FAIL/WARN ให้คงไว้เหมือนเดิม แล้วเสนอชุดสีที่เกิน 4.5:1 มา 3 ชุด (คงโทนเหลือง พื้นหลังให้เข้ม) และให้พิกัดสำหรับย้ายจากตรงกลางไปด้านขวาของแถบสถานะด้านบนมาหนึ่งอันด้วย
AI แก้ skill_slot_1 เป็น PASS โดยใช้เกณฑ์ hitbox 52pt และเพื่อคอนทราสต์ของดีบัฟ ก็ปูพื้นหลังให้เข้มเป็น #2A2A00 ทำให้ได้ 7.8:1 พร้อมเสนอชุดสี 3 แบบ และให้พิกัดสำหรับย้ายการแจ้งเตือนไปด้านขวาของแถบสถานะด้านบน (ราว 600,18) มาด้วย จบในการไปกลับครั้งเดียว ถ้ากวาดสายตาดูหน้าจอด้วยตาทุกบิลด์ อุบัติเหตุเดิมก็เกิดซ้ำ แต่ถ้านำภาพหน้าจอ+manifest เข้าสู่ lint การละเมิดเรื่องคอนทราสต์·การจัดวาง·สัมผัสก็จะตกลงมาเป็นตัวเลข และคนก็ตัดสินเฉพาะข้อยกเว้นที่โค้ดไม่รู้ (hitbox) และความกำกวม (ไอคอนที่ไม่ได้ลงทะเบียน) เท่านั้น (การตรวจสอบ 1 หน้าจอด้วยมือใช้เวลาสิบกว่านาที ด้วยลูปนี้ใช้เวลาไม่กี่นาที — เป็นสมมติฐานที่ผู้เขียนประมาณการ ยังไม่ได้ตรวจสอบ ควรอ่านในแง่ความต่างเชิงโครงสร้างระหว่าง "กวาดด้วยตา" กับ "วัดด้วยมาตรฐาน" มากกว่าเวลาสัมบูรณ์)
เหตุที่ debuff_alert ได้ WARN ในเซสชันข้างต้น และควรวางข้อมูล P0 ไว้ตรงไหน หากเก็บไว้เป็นภาพหนึ่งภาพ การตัดสินการจัดวางทั้งหมดหลังจากนี้จะเร็วขึ้น บนโทรศัพท์ที่จับแนวนอน หน้าจอแบ่งออกเป็นสี่ตำแหน่ง แถบสถานะแนวนอนด้านบน (อ่านอย่างเดียว สายตาไปถึงก่อนแต่นิ้วไปไม่ถึง), มุมล่างสองมุมซ้าย·ขวา (ที่ควบคุมที่นิ้วโป้งสองมือเอื้อมถึง — นิ้วโป้งซ้าย=เคลื่อนที่, นิ้วโป้งขวา=สกิล), พื้นที่เกมตรงกลาง ระหว่างทั้งสอง (ที่ที่การต่อสู้เกิดขึ้น), และ แถบช่องล่างกลาง ใต้พื้นที่เกม (ที่วางของใช้สิ้นเปลือง·ไอเทมอัตโนมัติ และควิกสล็อต·ช่องสกิล) ด้านล่างนี้ สีเขียว·สีเหลืองอำพันคือพื้นที่ที่ P0 และช่องปลอดภัย ส่วนสีแดงคือกลางเกมที่การแจ้งเตือน P0 จะถูกกลบ
กฎนั้นเรียบง่าย วางข้อมูล P0 (HP·MP·การแจ้งเตือนสำคัญ) ไว้ในสีเขียว (แถบสถานะแนวนอนด้านบน หรือมุมล่างสองข้าง) เพราะเป็นทางผ่านที่สายตาไปถึงก่อนหรือที่นิ้วโป้งวนเวียนอยู่ตลอด ในทางกลับกัน กลางเกม (สีแดง) คือที่ที่การต่อสู้เกิดขึ้นเอง หากวางการแจ้งเตือน P0 ไว้ตรงนี้ ทันทีที่เอฟเฟกต์ปกคลุมหน้าจอ ข้อมูลก็จะถูกกลบ มีข้อควรระวังหนึ่ง — กลางเกมกับ ล่างกลาง ไม่เหมือนกัน กลางเกมอันตราย แต่ แถบช่องล่างกลาง (สีเหลืองอำพัน) ที่อยู่ใต้ลงมาคือที่อยู่ของของใช้สิ้นเปลือง·ไอเทมอัตโนมัติ และควิกสล็อต·ช่องสกิล วางไว้ระหว่างนิ้วโป้งสองข้างเพื่อจะมองเห็นในพริบตาว่าตัวเองใช้อะไรหรืออะไรถูกใช้ไปโดยอัตโนมัติ และ ข้อมูลที่อ่านอย่างเดียว (HP/MP/พลังชีวิตของเป้าหมาย) อยู่ด้านบน, องค์ประกอบที่ต้องกด (เคลื่อนที่·สกิล) อยู่ที่มุมล่างสองข้าง, ของใช้สิ้นเปลือง·ช่องอยู่ล่างกลาง — สามอย่างนี้คือโซนนิ้ว·สายตา เหตุที่การแจ้งเตือนดีบัฟใน §9.1.2 ได้ WARN อธิบายได้ด้วยภาพนี้ภาพเดียว — เพราะวาง P0 ที่ต้องเห็นภายใน 0.5 วินาที ไว้ที่กลางเกมซึ่งมองเห็นยากที่สุดเสียได้ การย้ายไปด้านขวาของแถบสถานะด้านบนในแผนแก้ไข ก็คือการย้ายกลับเข้าสีเขียวของภาพนี้อย่างพอดิบพอดี
lint ในบทนี้ตั้งอยู่บนสมมติฐานว่า "พิกัด·สีของแต่ละองค์ประกอบ" เข้ามาอย่างสะอาด แต่ในความเป็นจริง การที่จะดึงพิกัดนั้น จากที่ไหน อย่างไร ต่างหากที่เป็นทางแยกที่สมจริงที่สุด เพราะเป็นจุดที่หนังสือมักพูดอ้อม ๆ จึงขอเปรียบเทียบสามเส้นทางอย่างซื่อตรง คำตอบไม่ได้มีหนึ่งเดียว แต่แยกไปตามสถานการณ์ของทีม
| เส้นทาง | ทำอะไร | จุดแข็ง | จุดอ่อน / ความจริง |
|---|---|---|---|
| ① ล็อก telemetry ในเกม | ให้บิลด์ดัมป์พิกัด·ขนาด·สีของวิดเจ็ตที่เฟรมเวิร์ก UI วาดออกมาโดยตรง | พิกัด แม่นยำ (ไม่ใช่การประมาณ) ออกมาถึง hitbox·anchor | ต้องฝัง hook สำหรับดัมป์ลงในโค้ด UI ต้องอาศัยความร่วมมือกับโปรแกรมเมอร์ ทำครั้งเดียวแล้วเชื่อถือได้มากที่สุด |
| ② vision API สำเร็จรูป | ใส่ภาพหน้าจอเข้า OCR·API ตรวจจับวัตถุ แล้วดึงพิกัดข้อความ·กล่อง | ไม่ต้องแก้บิลด์ ใช้ได้แม้กับภาพหน้าจอภายนอก | พิกัดเป็น ค่าประมาณ องค์ประกอบที่ไม่ใช่ตัวอักษรอย่างเกจ·ไอคอนจัดประเภทได้อ่อน การส่งออกภายนอก = เสี่ยงบิลด์ที่ยังไม่เปิดเผยรั่ว |
| ③ อิมพลีเมนต์เอง (วิเคราะห์พิกเซล) | อ่านภาพหน้าจอเองแล้วดึงขอบสี·กล่องด้วยฮิวริสติก | พึ่งพา dependency น้อยที่สุด เพียงพอสำหรับคำนวณคอนทราสต์สี | ไม่รู้ ความหมาย ขององค์ประกอบ (อันนี้เป็น P0 ไหม) ต้องเทียบกับ manifest ถึงจะใช้ได้ มีภาระการดูแลรักษา |
ความสัมพันธ์ของสามเส้นทางนี้อธิบาย worked transcript ของบทนี้ได้ตรง ๆ เหตุที่ใน §9.1.2 การตรวจคอนทราสต์แม่นยำ ก็เพราะค่าสี (fg/bg) เข้ามาอย่างแม่นยำผ่าน ①·③ และ เหตุที่สัมผัส FAIL ถูกคนพลิกกลับด้วยมือ ก็เพราะ hitbox ตกหล่นไปจาก manifest (②·③ มองไม่เห็น hitbox มีแต่ ① ที่เห็น) กล่าวคือ คอนทราสต์ จับได้ด้วยพิกเซลเพียงอย่างเดียว แต่ hitbox สัมผัส จับไม่ได้หากไม่มี ① telemetry ต้องรู้ขีดจำกัดนี้ก่อนเริ่ม เส้นแบ่งว่าจะเชื่อผลตรวจสอบของ AI ได้ถึงไหนจึงจะชัด
ตัวเลือกของโปรเจกต์ของผู้เขียนคือโครงสร้างที่ใช้ ① telemetry เป็นฉบับหลัก ส่วน AI เป็นผู้ตรวจสอบที่เทียบภาพหน้าจอ+manifest จาก telemetry AI จับสิ่งที่เห็นแต่บนหน้าจอแต่ไม่มีใน manifest (ไอคอนกะพริบที่ไม่ได้ลงทะเบียนใน §9.1.2) และคนจับสิ่งที่มีใน manifest แต่ผิดไปจากหน้าจอ หากใช้อย่างใดอย่างหนึ่งเพียงอย่างเดียว ก็จะมีมุมอับเหลืออยู่ทั้งสองฝั่ง
flowchart LR
A["บิลด์ QA
ภาพหน้าจอ HUD"] --> C
B["ดัมป์ telemetry
พิกัด·สี·hitbox
(เส้นทาง ①)"] --> C["manifest + ภาพหน้าจอ"]
C --> D["AI ตรวจสอบ
คอนทราสต์·การจัดวาง·สัมผัส
+ แจ้งองค์ประกอบที่ไม่ได้ลงทะเบียน"]
D --> E{"การตัดสินมาตรฐาน
WCAG/HIG"}
E -->|FAIL/WARN| F["คนตรวจสอบ
เฉพาะข้อยกเว้น·กำกวม"]
F -->|ขอใหม่| D
F -->|ผ่าน| G["ผ่าน build gate"]
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class A,B,C data;
class D ai;
class E code;
class F human;
class G pass;
ที่ที่มือของคนเอื้อมไปถึงมีเพียงสองแห่ง จุดที่ใส่ดัมป์ telemetry เข้าไปอย่างสะอาด (หัวสุด) และจุดที่ตัดสินข้อยกเว้น (hitbox)·ความกำกวม (องค์ประกอบที่ไม่ได้ลงทะเบียน) ที่โค้ด·มาตรฐานจับไม่ได้ (ท้ายสุด) ส่วนการคำนวณคอนทราสต์ที่น่าเบื่อและการเทียบการจัดวางที่อยู่ระหว่างนั้น AI และมาตรฐานเป็นผู้รัน
ถ้า AI คำนวณเลขใหม่ทุกครั้งที่ตรวจสอบ ก็สิ้นเปลืองทั้งโทเค็นและเวลา รายการที่ ตกลงมาเป็นแบบ deterministic อย่างคอนทราสต์·สัมผัส·การเอื้อมถึงมุม ให้โค้ดตีก่อน AI เข้าไปจัดการเฉพาะสิ่งที่โค้ดจับไม่ได้ (การตีความความหมายของหน้าจอ, องค์ประกอบที่ไม่ได้ลงทะเบียน) ทั้งสองไม่ใช่การแข่งขัน แต่เป็นการแบ่งหน้าที่กัน
# hud_lint.py — ตรวจสอบ manifest ของ HUD ตามมาตรฐาน (โครงร่าง)
# อินพุต: manifest จาก telemetry (rect/hit_rect/fg/bg/class/interactive ของแต่ละองค์ประกอบ)
# เอาต์พุต: รายการการละเมิด WCAG/HIG + การเอื้อมถึงสองมือ
def _luminance(hex_color): # ความสว่างสัมพัทธ์ตาม WCAG
r, g, b = (int(hex_color[i:i+2], 16) / 255 for i in (1, 3, 5))
f = lambda c: c/12.92 if c <= 0.03928 else ((c+0.055)/1.055) ** 2.4
R, G, B = f(r), f(g), f(b)
return 0.2126*R + 0.7152*G + 0.0722*B
def contrast_ratio(fg, bg): # คอนทราสต์ตาม WCAG
L1, L2 = sorted((_luminance(fg), _luminance(bg)), reverse=True)
return (L1 + 0.05) / (L2 + 0.05)
def in_thumb_corner(e, w, h):
"""เป็นมุมล่างซ้าย·ขวาที่นิ้วโป้งสองมือของการจับแนวนอนเอื้อมถึงหรือไม่"""
x, y = e["hit_rect"][0] / w, e["hit_rect"][1] / h
bottom = y > 0.55
left_corner = bottom and x < 0.30 # นิ้วโป้งซ้าย = เคลื่อนที่
right_corner = bottom and x > 0.70 # นิ้วโป้งขวา = สกิล
return left_corner or right_corner
def lint(elements, screen_w, screen_h):
issues = []
for e in elements:
# กฎ A: คอนทราสต์ (ตัวอักษร 4.5:1 / ไม่ใช่ตัวอักษร·ตัวอักษรใหญ่ 3:1)
need = 4.5 if e["kind"] == "text" else 3.0
cr = contrast_ratio(e["fg"], e["bg"])
if cr < need:
issues.append(f"[A] {e['id']}: คอนทราสต์ {cr:.1f}:1 < {need}:1 (WCAG SC 1.4.3)")
# กฎ B: พื้นที่สัมผัส — ใช้เกณฑ์ hitbox (ไม่ใช่ขนาดเชิงภาพ)
if e.get("interactive"):
tap = min(e["hit_rect"][2], e["hit_rect"][3]) # ← hit_rect ไม่ใช่ rect
if tap < 44:
issues.append(f"[B] {e['id']}: แตะ {tap}pt < 44pt (HIG)")
# กฎ C: องค์ประกอบที่ต้องควบคุมต้องอยู่ที่มุมนิ้วโป้งสองมือ (ล่างซ้าย·ขวา)
if not in_thumb_corner(e, screen_w, screen_h):
issues.append(f"[C] {e['id']}: องค์ประกอบที่ต้องควบคุมถูกวางนอกมุมนิ้วโป้งสองมือ "
f"(x={e['hit_rect'][0]}, y={e['hit_rect'][1]})")
return issues
โค้ดนี้ยุติการเถียงกันในที่ประชุมว่า "ตัวอักษรนี้มันมองไม่ค่อยเห็นนะ ใช่ไหม" เมื่อโค้ด output ออกมาว่า [A] debuff_alert: คอนทราสต์ 2.0:1 < 4.5:1 (WCAG SC 1.4.3) ก็ไม่มีอะไรให้ถกเถียง แก้ก็จบ สองบรรทัดที่ควรสังเกตคือ กฎ B มอง hit_rect ไม่ใช่ rect และกฎ C ปล่อยให้องค์ประกอบที่ต้องควบคุมผ่านได้เฉพาะที่มุมล่างสองมุมซ้าย·ขวา — บทเรียนที่คนพลิกกลับ AI ใน §9.1.2 (hitbox) และขีดจำกัดการเอื้อมถึงของการจับแนวนอนสองมือ เข้าไปอยู่ในโค้ดด้วยกัน จุดสำคัญของการตัดสินแนวนอนคือ การมองมุมนิ้วโป้งซ้าย (เคลื่อนที่)·นิ้วโป้งขวา (สกิล) สองมุมแยกกัน ไม่ใช่เกณฑ์ 'ส่วนโค้งนิ้วโป้ง' เดียว ข้อยกเว้นที่คนจับได้ครั้งหนึ่ง ต่อจากนั้นโค้ดจะเป็นผู้จับ ด้วยเหตุนี้จึงเหลือบทบาทแคบ ๆ ให้ AI ว่า "นอกจากสิ่งที่โค้ดประมวลผลเป็น PASS แล้ว ให้แจ้งความผิดปกติที่เห็นได้แต่บนหน้าจอเท่านั้น (องค์ประกอบที่ไม่ได้ลงทะเบียน·การซ้อนทับเชิงภาพ·การตัดขาด)" สิ่งที่จับได้แบบ deterministic ให้โค้ด, สิ่งที่ต้องตีความความหมายของหน้าจอให้ AI, ข้อยกเว้นที่รู้สถานการณ์ของบิลด์ให้คน — การแบ่งหน้าที่นี้คือแก่น
ตัวเลขที่ปรากฏในบทนี้มีแหล่งที่มาเพียงสามแหล่ง คอนทราสต์ 4.5:1·สัมผัส 44pt·48dp เป็นค่าทางการของ WCAG SC 1.4.3·HIG·Material และการที่ตัวอักษร #D4C84A บนพื้นหลัง #888 ได้ประมาณ 2.0:1 ก็เป็นค่าที่คำนวณจากการใส่ค่าสีลงในสูตรนั้น (§9.1.1·§9.1.5) "การตรวจสอบ 1 หน้าจอด้วยมือใช้เวลาสิบกว่านาที ด้วยลูปใช้เวลาไม่กี่นาที"·"ข้อมูลที่แสดงตลอดเวลาแนวนอน 12\~16 ชนิด" เป็นการประมาณของผู้เขียนที่ยังไม่ได้ตรวจสอบ จึงระบุไว้เช่นนั้นในเนื้อหา ส่วนที่เหลือ (จำนวนคอนทราสต์ FAIL ต่อบิลด์, จำนวน hitbox สัมผัสที่ไม่ถึงเกณฑ์, จำนวนการหลุดออกจากมุมนิ้วโป้ง, อัตราการแตะผิดของ telemetry) เป็นค่าที่นับได้โดยตรงจากบิลด์ล็อก ส่วนตัวชี้วัดผลลัพธ์ที่ไม่สามารถฟันธงสาเหตุได้ด้วย HUD เพียงอย่างเดียวอย่างจำนวนคำร้องเรียนของผู้ใช้ ไม่ได้นำขึ้นเป็น KPI
| รูปแบบ | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| ตรวจสอบด้วยตาบนจอมอนิเตอร์ของดีไซเนอร์ | ขาดเงื่อนไข 6 นิ้ว·เอฟเฟกต์การต่อสู้ อุบัติเหตุคอนทราสต์จึงเกิดซ้ำ | ทำ lint ภาพหน้าจอเป็น build gate (§9.1.2) |
| โยนแค่ภาพหน้าจอให้ AI แล้วบอก "ช่วยตรวจดู" | เดาพิกัดแล้วตัดสินแบบประมาณ เชื่อถือไม่ได้ | แนบ manifest จาก telemetry (§9.1.4) |
| ตัดสินพื้นที่สัมผัสด้วยขนาดเชิงภาพ | พลาดการขยาย hitbox ทำให้ปุ่มที่ปกติได้ FAIL | ตรวจด้วยเกณฑ์ hit_rect (§9.1.5) |
| วางการแจ้งเตือน P0 ไว้กลางจอ | ถูกเอฟเฟกต์การต่อสู้กลบ "ตายเพราะมองไม่เห็น" | ย้ายไปแถบสถานะด้านบน·สองมุม (§9.1.3) |
| วางปุ่มควบคุมไว้กลางจอด้านซ้าย·ด้านบน | ในการจับแนวนอนสองมือ นิ้วโป้งเอื้อมไม่ถึง | ย้ายไปมุมล่างซ้าย·ขวา (§9.1.5 กฎ C) |
| ออกแบบบนสมมติฐานการจับแนวตั้งมือเดียว | MMORPG ใช้แนวนอนสองมือเป็นมาตรฐาน ข้อมูล·การควบคุมไม่เข้ากัน | เปลี่ยนไปแนวนอนสองมือ (§9.1.1) |
| ถกเถียงคอนทราสต์ด้วย "เห็น/ไม่เห็น" | ข้อสรุปต่างกันไปในแต่ละคน | ใช้ค่าคำนวณ WCAG 4.5:1 (§9.1.1) |
ข้อที่สี่เกิดซ้ำบ่อยที่สุด เมื่อต้องรีบนำการแจ้งเตือนใหม่ขึ้นไปวาง พื้นที่ว่างมีแต่กลางจอ จึงวางไว้ตรงนั้น — และตรงกลางนั้นก็คือที่ที่เกมเกิดขึ้นพอดี
ถ้าทำคนเดียวก็แค่นี้พอ: ไม่มีทั้ง telemetry และ manifest ก็ได้ ถ่ายภาพหน้าจอ HUD แนวนอนของเกมตัวเอง (หรือเกมที่คุณชอบ) มาหนึ่งภาพ เลือกตัวอักษร·ไอคอนที่เล็กที่สุดสองสามอัน ดูดสีพื้นหน้า/พื้นหลังด้วยเครื่องมือดูดสี (spoid) แล้วจดด้วยมือ จากนั้นแปะพรอมต์จาก §9.1.2 แล้วลองรันสักครั้ง เมื่อเลือกค่าคอนทราสต์ที่ AI คำนวณมาสักหนึ่งค่า แล้วลองตรวจคำนวณซ้ำเองด้วยเครื่องคำนวณคอนทราสต์ WCAG ออนไลน์ คุณจะรู้สึกได้ด้วยตัวเองว่า "เห็น/ไม่เห็น" กลายเป็นตัวเลขได้อย่างไร ถ้ามี P0 ที่ AI วางไว้กลางจอ ลองโต้แย้งกลับว่า "ดูอีกทีสิว่าทำไมตรงกลางถึงอันตราย"
ถ้าเป็นทีม ให้เริ่มด้วยขั้นถัดไปหนึ่งขั้น ตกลงกับโปรแกรมเมอร์เรื่อง telemetry hook (เส้นทาง ①) ที่ดัมป์พิกัด·สี·hitbox ของวิดเจ็ตจากเฟรมเวิร์ก UI ก่อน แล้วใส่ฟังก์ชัน contrast_ratio ฟังก์ชันเดียวจาก §9.1.5 ลงในบิลด์ การคำนวณคอนทราสต์เป็นสูตรมาตรฐานจึงไม่มีข้อโต้แย้ง และเพียงมีฟังก์ชันเดียว คอนทราสต์ FAIL ในทุกบิลด์ก็จะตกลงมาเป็นตัวเลข จากนั้นเมื่อเพิ่ม in_thumb_corner เข้าไป โค้ดก็จะจับการหลุดออกจากมุมขององค์ประกอบที่ต้องควบคุมแบบแนวนอนสองมือได้ด้วย ส่วนการตีความอย่างการจัดวาง·องค์ประกอบที่ไม่ได้ลงทะเบียน ก็เพิ่ม AI ทับลงบนนั้นได้
ผู้อ่านหลัก: นักออกแบบ UX และนักออกแบบการต่อสู้ของเกมแอ็กชัน/MMORPG ที่เน้นมือถือเป็นหลัก (ทีมขนาดกลาง) ฉบับย่อสำหรับผู้อ่านคนเดียว/มือสมัครเล่น: §9.2.7 «ถ้าทำคนเดียวก็แค่นี้พอ»
สกิล 6 อย่างของอาชีพใหม่ จะวางไว้ตรงไหนของจอมือถือและวางอย่างไร เมื่อคำถามนี้ขึ้นมาในที่ประชุม 30 นาทีแรกก็ดำเนินไปเหมือนเดิมเสมอ มีคนหนึ่งวาดวงกลมหกวงบนไวต์บอร์ด อีกคนบอกว่า «ตรงนั้นนิ้วโป้งเอื้อมไม่ถึง» แล้วก็มีอีกคนรับว่า «ถ้าเลื่อนขึ้นไป มินิแมปก็จะถูกบัง» ทั้งสามคนพูดถูกหมด แต่ก็ไม่ได้ข้อสรุป พอประชุมครั้งต่อไปไวต์บอร์ดแผ่นเดิมก็ถูกวาดขึ้นมาอีก
ปัญหาคืองานวาดแบบจัดวางกับงานตรวจว่าแบบนั้นทำตามกฎหรือไม่ ปะปนกันอยู่ในหัวของคนคนเดียว คนที่วาดมักคัดทิ้งสิ่งที่ตัวเองวาดไม่ค่อยลง บทนี้แยกสองสิ่งนั้นออกจากกัน งานน่าเบื่อที่ต้องร่างแบบจัดวางหลาย ๆ แบบ ยกให้ AI ทำ ส่วนเรื่องที่ว่าแบบนั้นละเมิดกฎเรื่องการทับซ้อน มุมนิ้วโป้ง และขนาดพื้นที่สัมผัสหรือไม่ ให้โค้ดเป็นคนคัดทิ้ง มนุษย์มายืนอยู่แค่ตำแหน่งที่เลือกแบบใดแบบหนึ่งจาก «ความรู้สึกของเกม» ในบรรดาแบบที่โค้ดผ่านให้แล้วเท่านั้น ถ้า §9.1 เป็นการตั้ง rulebook ให้ HUD ทั้งหน้าจอ บทนี้ก็เป็นหนึ่งรอบที่นำ rulebook นั้นไปใช้จนสุดทางกับก้อนเดียวที่มือสัมผัสมากที่สุด นั่นคือปุ่มสกิล
องค์ประกอบส่วนใหญ่บน HUD เป็นการอ่านเพียงอย่างเดียว ไม่มีใครกดแถบ HP ดังนั้นในภาพมุมนิ้วโป้งของ §9.1 จึงวาง HP, MP และพลังชีวิตของเป้าหมายไว้ในพื้นที่อ่านด้านบนที่มือเอื้อมไม่ถึงได้ ปุ่มสกิลตรงกันข้ามเลย ต้องกดให้ถูกต้องในระดับ 0.1 วินาที และระหว่างการต่อสู้สายตาจับอยู่ที่ศัตรู นิ้วจึงหาตำแหน่งด้วย ความจำ ตำแหน่งคลาดเคลื่อนไปนิดเดียวก็เกิดการกดผิด (mistap) ตรงนั้นทันที
MMORPG บนมือถือใช้การจับสองมือแนวนอนเป็นมาตรฐาน องค์ประกอบที่กดจะวางไว้ที่มุมล่างทั้งสองข้าง ส่วนของใช้/ช่องไอเทมจะวางไว้ตรงกลางด้านล่าง (เหตุผลว่าทำไมแนวนอนจึงเป็นมาตรฐาน และสามพื้นที่นั้นคืออะไร อธิบายไว้ใน §9.1) ในมาตรฐานนั้นสกิลแทบทั้งหมดจะถูกวางไว้ที่ คลัสเตอร์มุมขวาล่างที่นิ้วโป้งขวาเอื้อมถึง (นิ้วโป้งซ้ายถูกผูกไว้กับการเคลื่อนที่ที่มุมซ้ายล่าง) มีการแบ่งแยกอยู่อย่างหนึ่ง — สกิลแบบแอ็กทีฟที่กดในระดับ 0.1 วินาทีจะอยู่ที่คลัสเตอร์ขวาล่างนี้ แต่ของใช้ ไอเทมอัตโนมัติ และช่องด่วน (quick slot) จะวางแยกไว้ที่แถบช่องตรงกลางด้านล่างระหว่างนิ้วโป้งสองข้าง บทนี้กล่าวถึงเฉพาะปุ่มสกิลแบบแอ็กทีฟ และการตัดสินพิกัดทั้งหมดตั้งอยู่บนสมมติฐานการจับสองมือแนวนอน
ดังนั้นการจัดวางปุ่มสกิลจึงถูกผูกเข้ากับกฎเชิงกำหนด (deterministic) สามข้อพร้อมกัน — พื้นที่สัมผัสขั้นต่ำ (HIG 44pt), ระยะห่างระหว่างปุ่มที่อยู่ติดกัน (Material 8dp), การเอื้อมถึงของนิ้วโป้ง (สกิลอยู่ที่มุมขวาล่างของนิ้วโป้งขวา) ทั้งสามข้อนี้เป็นรายการที่ตัดสินได้ด้วยพิกัดและขนาด ซึ่งมีอยู่แล้วใน rulebook ที่ตั้งไว้ใน §9.1.1 ดังนั้นค่าตามมาตรฐานสาธารณะจึงเป็นไปตาม rulebook นั้น (พื้นที่สัมผัส 44pt และระยะห่าง 8dp เป็นค่าที่ได้รับการรับรอง ส่วนมุมนิ้วโป้งขวาเป็นโมเดลที่วงการใช้กันทั่วไปเท่านั้น) ทั้งสามข้อนี้กลายเป็น อินพุตชั้นแรกของ lint ที่จะคัดแบบจัดวางของ AI ทิ้งในบทนี้ ถ้าโค้ดบอกว่า «skill_3 มีขนาด 40pt จึงต่ำกว่า HIG 44pt» แทนที่จะเป็น «ปุ่มนี้เล็กไปหรือเปล่า» 30 นาทีหน้าไวต์บอร์ดก็หายไป
เมื่อวางเกณฑ์ของแพลตฟอร์มไว้เคียงกับ PC จุดเริ่มต้นของการจัดวางก็ชัดเจน PC เน้นความแม่นยำและจำนวนมาก ส่วนมือถือแนวนอนจำกัดอยู่ที่มุมของสองมือ (ตารางเปรียบเทียบทั้งหมดดูได้ที่ rulebook §9.1) เมื่อแยกดูเฉพาะการป้อนสกิลความแตกต่างก็ชัดเจน — PC ใช้ปุ่มลัดในการสั่งสกิล จะวางสกิลไว้ตรงไหนของจอก็ได้เพราะนิ้วอยู่ที่คีย์บอร์ด การเอื้อมถึงจึงไม่เป็นปัญหา และมีช่องได้หลายช่อง ส่วนมือถือแนวนอนไม่มีทั้งโฮเวอร์และปุ่มลัด จึงต้องวางสกิลที่ มุมขวาล่างที่นิ้วโป้งขวาเอื้อมถึง ตามลำดับความถี่ (จำกัดการแสดงพร้อมกันที่ 6\~8 ปุ่ม) และวางสกิลที่ใช้บ่อยที่สุดไว้ด้านในของมุม (ตำแหน่งที่เอื้อมถึงดีที่สุด) ดังนั้นแก่นแท้ของการจัดวางสกิลบนมือถือจึงไม่ใช่ «การจัดเรียงให้สวยงาม» แต่เป็น «การจัดวางตามลำดับความสำคัญด้วยความถี่ภายในมุมนิ้วโป้งขวา + การตรวจสอบด้วย rulebook» และงานวาดแบบหลาย ๆ แบบ ถ้าให้คนทำด้วยมือก็น่าเบื่อ และทุกครั้งที่ทำเกณฑ์ก็แกว่ง งานซ้ำ ๆ ที่น่าเบื่อและแปรปรวน — เป็นตำแหน่งที่ AI ทำได้โดยไม่เหนื่อยล้าเหมือนมนุษย์
แสดงหนึ่งรอบของการจัดวางสกิลแอ็กทีฟ 6 อย่างของอาชีพใหม่ «หมอผี» บนมือถือ ตั้งแต่อินพุตจนถึงการทิ้งจนสุดทาง ด้านล่างนี้คือการจำลองเซสชันงาน UI สกิลใหม่ของโปรเจกต์ของผู้เขียน (MMORPG ที่เน้นมือถือเป็นหลัก ต่อไปนี้เรียกว่า «โปรเจกต์ A») อย่างซื่อตรง อินพุตและพรอมต์สามารถคัดลอกไปใช้ได้ตามนั้น ส่วนผลลัพธ์เป็นการเรียบเรียงใหม่จากเซสชันจริง
ทำความถี่การใช้และลักษณะพื้นฐานของสกิล 6 อย่างเป็น yaml ความถี่การใช้เป็นค่าที่ดึงมาจากบันทึกการต่อสู้ในชีตข้อมูล จึงไม่ใช่การกุขึ้นใหม่
# skill_set_shaman.yaml — สกิลแอ็กทีฟ 6 ชนิดของอาชีพใหม่ «หมอผี»
screen: { w: 2400, h: 1080, dpr: 3 } # อิงจอแนวนอน 6.x นิ้ว, pt = px / dpr
skills:
- id: s1_quickbolt # การโจมตีพื้นฐาน, ใช้บ่อยที่สุด
use_rate: 0.41 # สัดส่วนการใช้ระหว่างต่อสู้ (ดึงจากบันทึก)
role: spam # กดรัว
- id: s2_hex # ดีบัฟ, ใช้บ่อย
use_rate: 0.22
role: core
- id: s3_totem # แบบติดตั้ง, ปานกลาง
use_rate: 0.14
role: core
- id: s4_heal # ฟื้นฟู, นาน ๆ ครั้งแต่เร่งด่วน
use_rate: 0.11
role: panic # ใช้ทันทีเมื่อฉุกเฉิน
- id: s5_curse # ดีบัฟวงกว้าง, นาน ๆ ครั้ง
use_rate: 0.08
role: situational
- id: s6_ultimate # ท่าไม้ตาย, ไม่ค่อยใช้
use_rate: 0.04
role: burst
ช่องสำคัญคือ use_rate และ role ทั้ง s1_quickbolt (41%) ที่กดบ่อยที่สุด และ s4_heal (panic) ที่ต้องหาให้เจอภายใน 0.2 วินาทีเมื่อฉุกเฉิน ต้องอยู่ในตำแหน่งที่นิ้วโป้งขวาเอื้อมถึงดีที่สุด (ด้านในของมุมขวาล่าง) ส่วน s6_ultimate (4%) ที่ไม่ค่อยใช้ จะอยู่ขอบมุมไกลออกไปหน่อยก็ได้ ลำดับความสำคัญนี้คืออินพุตทั้งหมดสำหรับการจัดวางของ AI ในขั้นถัดไป
yaml ที่แนบมานี้คือสกิลแอ็กทีฟ 6 ชนิดของอาชีพใหม่ ช่วยทำแบบจัดวางปุ่มสกิลบนหน้าจอจับสองมือแนวนอนมา 3 แบบ ให้วางสกิลไว้ที่มุมขวาล่างที่นิ้วโป้งขวาเอื้อมถึง (มือซ้ายผูกอยู่กับการเคลื่อนที่ที่มุมซ้ายล่าง)
สามแบบให้ใช้ปรัชญาต่างกัน — เช่น มุมรูปพัด, กริด 2 แถว, แบบถ่วงน้ำหนักด้วยความถี่ แต่ละปุ่มให้ค่า x,y,w,h เป็น px (หน้าจอ 2400x1080, dpr 3, pt=px/3)
สกิลที่ใช้บ่อยหรือ role=panic ให้อยู่ด้านในของมุมที่นิ้วโป้งขวาเอื้อมถึงดี พื้นที่สัมผัสตั้งแต่ 132px (44pt) ขึ้นไป ระยะห่างที่อยู่ติดกันตั้งแต่ 24px (8dp) ขึ้นไป
แต่ละแบบให้เขียนเจตนาการออกแบบหนึ่งบรรทัด พร้อมจุดอ่อนที่นายเห็นมาหนึ่งข้อด้วยทุกครั้ง — แบบที่ไม่มีจุดอ่อนไม่ได้ ในแต่ละแบบให้เฉพาะ (ปรัชญาการจัดเรียง|ตารางพิกัด|เจตนาการออกแบบ|จุดอ่อนที่ตรวจเอง) เท่านั้น ตัดคำอธิบายออก
ในพรอมต์นี้ ส่วนที่ทุ่มเทยิ่งกว่าคำสั่งจัดวางคือตรงที่บังคับรูปแบบผลลัพธ์และการแจ้งจุดอ่อน ถ้าไม่รับพิกัดเป็นตัวเลขก็รัน lint ไม่ได้ จึงตอกย้ำให้ออกเป็น px และให้ออก «จุดอ่อน 1 ข้อของแบบนี้» มาด้วย เพื่อให้ AI พูดถึงข้อจำกัดของตัวเองก่อน ถ้าสั่งลอย ๆ ว่า «ทำ UI สวย ๆ ให้หน่อย» ก็จะได้การจัดวางเฉลี่ยของเกม RPG มือถือทั่วไป ออกมาเป็นคำพูดอย่างเดียวโดยไม่มีรูป
== แบบ A: มุมขวาล่างทรงพัด ==
ปรัชญาการจัดเรียง: เรียง 6 ปุ่มเป็นรูปพัดรอบแกนหมุนของนิ้วโป้งขวา ตามลำดับความถี่จากในออกนอก
| id | x | y | w | h | pt |
|-------------|------|-----|-----|-----|------|
| s1_quickbolt| 2160 | 912 | 168 | 168 | 56pt |
| s2_hex | 2016 | 852 | 132 | 132 | 44pt |
| s3_totem | 2052 | 996 | 132 | 132 | 44pt |
| s4_heal | 1884 | 924 | 132 | 132 | 44pt |
| s5_curse | 1920 | 780 | 120 | 120 | 40pt |
| s6_ultimate | 1788 | 840 | 120 | 120 | 40pt |
เจตนาการออกแบบ: วาง s1 ที่ใช้บ่อยให้ใหญ่ที่สุดและเอื้อมถึงง่ายที่สุดไว้ด้านในของมุมขวาล่าง
จุดอ่อนที่ตรวจเอง: s5,s6 ขนาด 40pt อาจต่ำกว่า HIG ส่วนนอกของพัดนิ้วโป้งขวาเอื้อมถึงคลุมเครือ
== แบบ B: กริด 2 แถว (2x3) ==
ปรัชญาการจัดเรียง: ตาราง 2 แถว x 3 คอลัมน์ที่มุมขวาล่าง เน้นการจัดเรียงเชิงสายตา
| id | x | y | w | h |
|-------------|------|-----|-----|-----|
| s1_quickbolt| 2208 | 912 | 132 | 132 |
| s2_hex | 2052 | 912 | 132 | 132 |
| s3_totem | 1896 | 912 | 132 | 132 |
| s4_heal | 2208 | 756 | 132 | 132 |
| s5_curse | 2052 | 756 | 132 | 132 |
| s6_ultimate | 1896 | 756 | 132 | 132 |
เจตนาการออกแบบ: ทุกปุ่มขนาดเท่ากัน การจัดเรียงเรียบร้อยสะอาดตา
จุดอ่อนที่ตรวจเอง: แถวบน (s4,s5,s6, y=756) อยู่ที่ขอบบนของมุมนิ้วโป้งขวา การเอื้อมถึงจึงคลุมเครือ
s4 สกิลฟื้นฟูฉุกเฉินอยู่แถวบน อาจหาช้าเมื่อฉุกเฉิน
== แบบ C: พัดถ่วงน้ำหนักด้วยความถี่ ==
ปรัชญาการจัดเรียง: วาง s1 เป็นวงกลมใหญ่ตรงกลาง ที่เหลือพันรอบแกนหมุนนิ้วโป้งขวาตามลำดับความถี่
| id | x | y | w | h |
|-------------|------|-----|-----|-----|
| s1_quickbolt| 2184 | 900 | 180 | 180 |
| s2_hex | 2028 | 852 | 144 | 144 |
| s3_totem | 2052 | 996 | 132 | 132 |
| s4_heal | 1908 | 912 | 144 | 144 |
| s5_curse | 1932 | 792 | 120 | 120 |
| s6_ultimate | 1812 | 972 | 120 | 120 |
เจตนาการออกแบบ: ให้มือเข้าหา s1 ตามธรรมชาติ และขยาย s4 ฉุกเฉินให้ใหญ่ไว้ด้านในของมุม
จุดอ่อนที่ตรวจเอง: เป็นทรงพัดทำให้ระยะห่างระหว่างปุ่มไม่สม่ำเสมอ กังวลว่า s2-s5, s4-s6 จะชนกันเพราะอยู่ใกล้
จุดสำคัญของผลลัพธ์นี้คือทั้งสามแบบล้วนแจ้งจุดอ่อนที่ตรวจเอง A บอกว่า «กังวลว่า 40pt ต่ำกว่า HIG» B บอกว่า «สกิลฟื้นฟูฉุกเฉินอยู่แถวบน» C บอกว่า «กังวลว่าจะชนกันเพราะอยู่ใกล้» AI ชี้จุดอ่อนของภาพที่ตัวเองวาดก่อน แต่นี่เป็นเพียงการแจ้งเอง การตัดสินจริงเป็นหน้าที่ของโค้ด
ถ้าเปรียบเทียบสามแบบด้วยตา ก็จะเริ่มศึกรสนิยมอีกว่า «B ดูเรียบร้อยกว่านะ» จึงป้อนทั้งสามแบบเข้า skill_layout_lint.py ของ §9.2.3 ตามนั้น ผลออกมาเป็นแบบนี้
[แบบ A] มุมขวาล่างทรงพัด
[FAIL] B-size : s5_curse 40pt < 44pt (ต่ำกว่า HIG)
[FAIL] B-size : s6_ultimate 40pt < 44pt (ต่ำกว่า HIG)
[WARN] C-corner: s6_ultimate x=1788 — ขอบซ้ายของมุม นิ้วโป้งขวาเอื้อมถึง 'ปานกลาง'
→ ผ่าน 4/6, ละเมิดร้ายแรง 2
[แบบ B] กริด 2 แถว (2x3)
[FAIL] C-corner: s4_heal y=756 (0.70h) ไม่ต่ำกว่า 0.55h → อยู่เหนือมุมนิ้วโป้งขวา
[FAIL] C-corner: s5_curse y=756 (0.70h) ไม่ต่ำกว่า 0.55h → อยู่เหนือมุมนิ้วโป้งขวา
[WARN] role : s4_heal(panic) y=756 — สกิลฉุกเฉินอยู่แถวบน
→ ผ่าน 4/6, ละเมิดร้ายแรง 2
[แบบ C] พัดถ่วงน้ำหนักด้วยความถี่
[FAIL] A-overlap: s2_hex ∩ s5_curse ระยะห่าง 18px < 24px (ต่ำกว่า 8dp)
[FAIL] A-overlap: s4_heal ∩ s6_ultimate ระยะห่าง 12px < 24px (ต่ำกว่า 8dp)
→ ผ่าน 4/6, ละเมิดร้ายแรง 2
ทั้งสามแบบตกหมด ที่น่าสนใจคือการแจ้งเองกับการตัดสินของ lint ทับซ้อนกันแทบทั้งหมด ตรงที่ AI บอกว่า «จุดอ่อน» ก็เกิดการละเมิดจริง แต่การแจ้งเองเป็นแค่ «ความกังวล» ส่วน lint เป็นตัวเลขว่า «s2_hex กับ s5_curse ระยะห่าง 18px» ไม่มีอะไรให้ถกในที่ประชุม
ในขั้นนี้มีการตัดสินใจสำคัญอยู่ข้อหนึ่ง แม้สามแบบจะตกหมดก็ไม่ย้อนกลับไปเริ่มใหม่ แต่นำรายงาน lint แปะลงในพรอมต์ถัดไปตามนั้นแล้วขอใหม่
นี่คือผล lint ของสามแบบที่แปะมาตามนั้น (รายงานด้านบน) จากในนี้ ช่วยเลือกแบบที่แก้การละเมิดได้ง่ายที่สุดมาแบบเดียว แล้วทำแบบแก้ไขที่ทำให้ FAIL เป็น 0 ทั้งหมดมาแบบเดียว
ทุกปุ่มต้องตั้งแต่ 132px (44pt) ขึ้นไป ระยะห่างตั้งแต่ 24px (8dp) ขึ้นไป ส่วน s4_heal(panic) ให้อยู่ในตำแหน่งที่เอื้อมถึงดีรองจาก s1 ภายในมุมนิ้วโป้งขวา
ให้เฉพาะตารางพิกัด แล้วเขียนทีละบรรทัดว่า FAIL ข้อไหนถูกแก้อย่างไร
// (เจตนา: ด้านในของมุม = x>70% ของแนวนอน(1680), y>55% ของแนวตั้ง(594))
AI เลือกแบบ A (มุมทรงพัด) เพราะการละเมิดเป็นแค่ «สองปุ่มขนาดต่ำไป» จึงลงมือแก้น้อยที่สุด มันขยาย s5·s6 ที่ตกเพราะเล็กให้เป็น 132px และดันเส้นโค้งของพัดเข้าด้านในมุมเล็กน้อยตามขนาดที่โตขึ้น เพื่อให้ได้ระยะห่าง แล้วออกแบบแก้ไขมา
== แบบ A' (แก้ไข) ==
| id | x | y | w | h | pt |
|-------------|------|-----|-----|-----|------|
| s1_quickbolt| 2160 | 900 | 168 | 168 | 56pt |
| s2_hex | 2010 | 846 | 132 | 132 | 44pt |
| s3_totem | 2046 | 990 | 132 | 132 | 44pt |
| s4_heal | 1872 | 918 | 144 | 144 | 48pt |
| s5_curse | 1908 | 774 | 132 | 132 | 44pt |
| s6_ultimate | 1740 | 858 | 132 | 132 | 44pt |
แก้ไข: B-size s5 40→44pt / B-size s6 40→44pt /
C-corner s6 x=1740(0.725w)·y=858(0.79h) คงอยู่ด้านในมุม →
role: ขยาย s4_heal เป็น 144px เพื่อเสริมการระบุยามฉุกเฉิน
ป้อนแบบ A' เข้า skill_layout_lint.py อีกครั้ง
[แบบ A'] มุมขวาล่างทรงพัด (แก้ไข)
[PASS] B-size : ทุกปุ่ม ≥ 44pt
[PASS] A-overlap: ระยะห่างต่ำสุด 30px ≥ 24px
[PASS] C-corner : ทุกปุ่มควบคุมอยู่ในมุมนิ้วโป้งขวา (x≥1680, y≥594)
[WARN] C-corner : s6_ultimate x=1740 — ขอบซ้ายสุดของมุม การเอื้อมถึง 'ปานกลาง'
→ ผ่าน 6/6, ละเมิดร้ายแรง 0, WARN 1
FAIL เป็น 0 แล้ว WARN ที่เหลือ 1 ข้อ (s6_ultimate อยู่ขอบซ้ายสุดของมุม นิ้วโป้งขวาเอื้อมถึงไม่ใช่ 'ง่าย' แต่ 'ปานกลาง') โค้ดไม่ฆ่าทิ้งโดยอัตโนมัติ แต่ส่งให้มนุษย์ และ WARN นี้ที่จริงเป็น การออกแบบโดยเจตนา s6 เป็นท่าไม้ตายที่มีความถี่การใช้ 4% ใช้นาน ๆ ครั้งที่สุด ตำแหน่งด้านในสุดของมุมจึงควรยกให้ s1 ที่ใช้บ่อย แล้ววาง s6 ไว้ที่ขอบจึงถูกต้อง มนุษย์ตัดสินว่า «WARN นี้คือเจตนา» แล้วผ่านให้ หนึ่งรอบของ อินพุต → สร้าง 3 แบบ → lint → ตกหมด → ขอใหม่ → ผ่าน ปิดลงตรงนี้
หนึ่งรอบนี้คือเกณฑ์ Show ของบทนี้ ถ้าไม่ดูจนสุดทางว่า AI วาดอะไร, lint คัดอะไรทิ้ง, มนุษย์ปล่อย WARN ตัวไหนให้รอด ประโยคที่ว่า «ออกแบบ UI ด้วย AI แล้ว» ก็กลวงเปล่า
หัวใจของรอบข้างต้นคือโค้ดราว 30 บรรทัดที่คัดกฎสามข้อทิ้ง สามรายการในตารางของ §9.2.1 กลายเป็นสามฟังก์ชันตามนั้น
# skill_layout_lint.py — ตรวจสอบการจัดเรียงปุ่มสกิล (โครงสร้าง)
# อินพุต: รายการพิกัดปุ่มที่ AI ออกมา [{id, x, y, w, h, role, use_rate}]
# เอาต์พุต: รายการละเมิด A-overlap / B-size / C-corner
# สมมติฐาน: จับสองมือแนวนอน วางสกิลไว้ที่มุมขวาล่างที่นิ้วโป้งขวาเอื้อมถึง
MIN_TAP_PX = 132 # HIG 44pt * dpr 3 = 132px
MIN_GAP_PX = 24 # Material 8dp * dpr 3 = 24px
RIGHT_CORNER_X = 0.70 # ขวาของแนวนอน 0.70 = มุมนิ้วโป้งขวา
BOTTOM_Y = 0.55 # ล่างของแนวตั้ง 0.55 = มุมด้านล่าง
def in_right_thumb_corner(b, w, h):
"""ในการจับแนวนอน นี่คือมุมขวาล่างที่นิ้วโป้งขวาเอื้อมถึงหรือไม่
(นิ้วโป้งซ้าย=เคลื่อนที่ที่มุมซ้ายล่าง, นิ้วโป้งขวา=สกิลที่มุมขวาล่าง)"""
rx, ry = b["x"] / w, b["y"] / h
return rx > RIGHT_CORNER_X and ry > BOTTOM_Y
def lint(buttons, screen_w, screen_h):
issues = []
# กฎ B: ขนาดพื้นที่สัมผัสขั้นต่ำ (HIG 44pt)
for b in buttons:
side = min(b["w"], b["h"])
if side < MIN_TAP_PX:
issues.append(f"[FAIL] B-size : {b['id']} {side//3}pt "
f"< 44pt (ต่ำกว่า HIG)")
# กฎ A: การทับซ้อน/ระยะห่างของปุ่มที่อยู่ติดกัน (ระยะของขอบสองด้านที่ใกล้ที่สุด)
for i, a in enumerate(buttons):
for c in buttons[i+1:]:
gap = edge_gap(a, c) # ระยะห่างต่ำสุดของสี่เหลี่ยมสองอัน (px)
if gap < MIN_GAP_PX:
issues.append(f"[FAIL] A-overlap: {a['id']} ∩ {c['id']} "
f"ระยะห่าง {gap}px < {MIN_GAP_PX}px (ต่ำกว่า 8dp)")
# กฎ C: องค์ประกอบควบคุมต้องอยู่ในมุมนิ้วโป้งขวา panic ยิ่งอยู่ด้านในของมุมยิ่งดี
for b in buttons:
rx, ry = b["x"] / screen_w, b["y"] / screen_h
if not in_right_thumb_corner(b, screen_w, screen_h):
issues.append(f"[FAIL] C-corner: {b['id']} "
f"x={b['x']}({rx:.2f}w) y={b['y']}({ry:.2f}h) "
f"→ นอกมุมนิ้วโป้งขวา")
elif b.get("role") == "panic" and rx < 0.78:
issues.append(f"[WARN] role : {b['id']}(panic) "
f"สกิลฉุกเฉินอยู่ใกล้ขอบด้านในของมุม")
return issues
โค้ดนี้ลบล้างคำพูดเชิงรสนิยมในที่ประชุมที่ว่า «แบบ B สวยกว่านะ» ความสวยเป็นเรื่องที่ค่อยมาว่ากันหลังจาก lint ผ่านแล้ว แบบที่ lint พ่น [FAIL] ออกมา จะสวยหรือไม่ก็เข้าบิลด์ไม่ได้ นี่คือการนำเกต lint ของ HUD ที่ตั้งไว้ใน §9.1.1 ไปใช้จนสุดทางกับก้อนที่ยุ่งยากที่สุดอย่างปุ่มสกิล — การแบ่งหน้าที่ที่ว่าเรื่องที่ตัดสินได้ด้วยพิกัดและขนาดให้โค้ดทำ ส่วนการตัดสินว่า «WARN นี้คือเจตนาหรือไม่» ให้มนุษย์ทำ ก็เป็นจริงตามนั้นที่นี่เช่นกัน
เมื่อมองรอบทั้งหมดในภาพเดียวจะเป็นแบบนี้
flowchart LR
A["สเปกสกิล yaml
(use_rate·role)"] --> B["AI: แบบจัดวาง 3 แบบ
พิกัด+จุดอ่อนที่ตรวจเอง"]
B --> C{"skill_layout_lint.py
ทับซ้อน·ขนาด·มุมนิ้วโป้งขวา"}
C -->|มี FAIL| D["ขอใหม่โดยป้อนรายงาน
lint ตามนั้น"]
D --> B
C -->|FAIL 0, มีแต่ WARN| E["มนุษย์: ตัดสินว่า WARN
เป็นเจตนาหรือไม่"]
E --> F["ยืนยันการจัดวาง
+ ArtGuide 06_UI sync"]
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class A data;
class B ai;
class C code;
class E human;
class F pass;
ตำแหน่งที่มือมนุษย์สัมผัสมีเพียงสองจุด คือต้นทางสุดที่ใส่สเปกอินพุตให้สะอาด และปลายทางสุดที่ตัดสิน WARN ที่ lint ฆ่าไม่ได้ ส่วนการสร้าง 3 แบบและตรวจพิกัดที่น่าเบื่อระหว่างนั้น AI กับ lint เป็นคนหมุนให้
ถ้าดึงแบบจัดวางออกมาครั้งเดียวแล้วจบ ก็ไม่รู้ว่าเครื่องมือนี้ทำงานดีหรือไม่ จึงบันทึกผล lint ลงในล็อกทุกครั้ง ค่าที่บันทึกง่ายมาก — แบบที่ AI ออกมาผ่าน lint รอบแรกกี่ข้อ (อัตราผ่านรอบแรก) และขอใหม่กี่ครั้งจึงถึง FAIL 0 (จำนวนรอบไป-กลับ)
ตัวเลขด้านล่างเป็นค่าจริงที่นับเองจากการจัดทำ UI สกิลของอาชีพใหม่ 3 ชนิด (หมอผีและอีก 2 ชนิด) ด้วยรอบนี้ เนื่องจากตัวอย่างเป็นอาชีพ 3 ชนิด (เซสชันจัดวาง 9 ครั้ง) ซึ่งน้อย จึงควรอ่านเป็นค่าบอกทิศทาง ไม่ใช่ค่าประชากรที่แม่นยำ ไม่มีตัวเลขที่ปรุงแต่ง
| รายการ | ค่าจริง | หมายเหตุ |
|---|---|---|
| แบบจัดวางแรกของ AI ที่ผ่าน lint รอบแรก | 1 ใน 9 ครั้ง | อีก 8 ครั้งมี FAIL ตั้งแต่ 1 ข้อขึ้นไป |
| จำนวน FAIL เฉลี่ยตอนผ่านครั้งแรก | 1.8 ข้อต่อแบบ | ส่วนใหญ่ขนาดต่ำไปหรืออยู่นอกมุมนิ้วโป้งขวา |
| จำนวนรอบไป-กลับเฉลี่ยจนถึง FAIL 0 | 1.4 ครั้ง | วิธีป้อนรายงาน lint ซ้ำ |
| ประเภท FAIL ที่พบบ่อยที่สุด | B-size (ขนาดต่ำไป) | รองลงมาคือ C-corner (มุมนิ้วโป้งขวา) |
บรรทัดที่สำคัญที่สุดคือบรรทัดแรก แบบที่ AI ออกมาครั้งแรก ผ่าน lint ไม่ได้ 8 ใน 9 ครั้ง นี่ไม่ใช่ความล้มเหลวของเครื่องมือนี้ แต่เป็นสัญญาณของการทำงานปกติ ถ้าให้ AI ออกพิกัดอย่างอิสระ มันจะละเมิด HIG 44pt บ่อย ๆ lint จับได้ทุกครั้ง พอป้อนรายงานกลับเข้าไป ก็เป็น 0 ภายในรอบไป-กลับ 1\~2 ครั้ง ถ้าอัตราผ่านรอบแรกเป็น 100% นั่นแปลว่า lint หลวมเกินไป ไม่ได้แปลว่า AI สมบูรณ์แบบ
ล็อกอัตราการผ่านนี้ยังเป็นเกณฑ์ในการตัดสินว่าจะรัดกฎ lint ให้แน่นหรือคลายลง ถ้า FAIL ประเภทใดถูกมือมนุษย์ปล่อยให้รอดทุกครั้งโดยอ้างว่า «ที่จริงเป็นเจตนา» กฎข้อนั้นก็เข้มเกินไป ในทางกลับกัน ถ้าหลังเปิดตัวมีคำร้องเรียนเรื่องการกดผิดเข้ามาแต่ lint ผ่านให้ กฎข้อนั้นก็หลวมไป
เมื่อนำแบบ A' ที่ผ่าน lint ใน §9.2.2 มาวาดตามพิกัดเดิม จะได้ตามด้านล่าง ตัวเลขในตารางมีรูปร่างอย่างไรบนหน้าจอจริง ต้องดูเป็นภาพจึงจะจับต้องได้ ในท่าจับโทรศัพท์แนวนอนด้วยสองมือ นิ้วโป้งซ้ายสัมผัสมุมซ้ายล่าง (เคลื่อนที่) นิ้วโป้งขวาสัมผัสมุมขวาล่าง (คลัสเตอร์สกิล) ขนาดของวงกลมแปรผันตามพื้นที่สัมผัส (pt) และสีคือระดับความยากของการเอื้อมถึงด้วยนิ้วโป้ง (เขียวง่าย / เหลืองปานกลาง)
เมื่อดูเป็นภาพก็เข้าใจ WARN สุดท้ายของรายงาน lint ได้ในแวบเดียว มีเพียง s6_ultimate (เหลือง) ที่อยู่ขอบซ้ายสุดของมุมขวาล่าง ตำแหน่งที่นิ้วโป้งขวาเอื้อมถึง 'ปานกลาง' แต่ s6 เป็นท่าไม้ตายที่มีความถี่การใช้ 4% การวางไว้ที่ขอบมุมจึงถูกต้อง ส่วน s1 (เขียว, ใหญ่สุด 56pt) ที่ใช้บ่อยที่สุดอยู่ที่มุมขวาล่างด้านในที่นิ้วโป้งขวาเอื้อมถึงดีที่สุด และ s4 สกิลฟื้นฟูฉุกเฉิน (ขอบสีเหลือง) ขยายขนาดให้ใหญ่ขึ้นเพื่อให้มือหาเจอเร็วยามฉุกเฉิน นิ้วโป้งซ้ายถูกผูกไว้กับ «การเคลื่อนที่» ที่มุมซ้ายล่าง สกิลจึงรวมกันอยู่ที่มุมขวาทั้งหมด ตารางพิกัดหนึ่งใบตรงกับภาพหนึ่งใบเป๊ะ ๆ — นั่นคือเหตุผลที่รับพิกัดมาเป็นตัวเลข
| รูปแบบ | ทำไมจึงล้มเหลว | วิธีรับมือ |
|---|---|---|
| วาดแต่วงกลมบนไวต์บอร์ดแล้วประชุม | ไม่มีพิกัดจึง lint ไม่ได้ ศึกรสนิยมวนซ้ำ | รับพิกัดเป็น px แล้วป้อนเข้า lint (§9.2.2) |
| โยนงานทั้งดุ้น «AI ทำ UI สกิลสวย ๆ ให้หน่อย» | ไม่มี rulebook ก็ได้การจัดวางเฉลี่ยของ RPG ทั่วไป | พรอมต์บังคับ 3 แบบ+พิกัด+จุดอ่อนที่ตรวจเอง |
| จัดวางบนสมมติฐานจับมือเดียวแนวตั้ง | MMORPG ใช้สองมือแนวนอนเป็นมาตรฐาน สกิลอยู่ที่มุมนิ้วโป้งขวา | lint โดยอิงจอแนวนอน 2400x1080, มุมขวาล่าง |
| เปรียบเทียบแบบจัดวางด้วยตาเปล่า | พลาดเรื่องต่ำกว่า HIG·การทับซ้อนทุกครั้ง | ตัดสินอัตโนมัติด้วย skill_layout_lint.py |
| แบบแรกผ่าน lint → วางใจว่าเครื่องมือดีแล้ว | อาจเป็นสัญญาณว่า lint หลวม | ตรวจการรัดกฎด้วยล็อกอัตราการผ่าน (§9.2.4) |
| ให้โค้ดบล็อกแม้กระทั่ง WARN อัตโนมัติ | ฆ่าทิ้งแม้แต่การจัดวางที่ตั้งใจ (ท่าไม้ตายที่นาน ๆ ใช้) | WARN ให้มนุษย์เป็นคนตัดสิน (§9.2.3) |
ข้อที่ห้าพลาดบ่อยที่สุด ถ้าแบบแรกของ AI ผ่านทุกครั้งก็รู้สึกดี แต่นั่นมักแปลว่ากฎ lint หลวม การตก 8 ใน 9 ครั้งคือสถานะที่สุขภาพดี
ถ้าทำคนเดียวก็แค่นี้พอ: ไม่ต้องมีโค้ด lint ก็ได้ เลือกสกิล 4\~6 อย่างของเกมตัวเอง (หรือเกมที่ชอบ) แล้วเขียนสเปกในรูปแบบของ §9.2.1 ด้วยมือ (use_rate ใส่แค่ลำดับความถี่คร่าว ๆ) แล้วแปะพรอมต์ของ §9.2.2 ตามนั้นเพื่อรับ 3 แบบมา จากนั้นแทนตลับเมตร ให้จำแค่ «44pt = 132px» ไว้ในหัว แล้วหาปุ่มที่ต่ำกว่า 132px ในตารางพิกัดที่ AI ออกมา วงกลมด้วยมือ และสมมติว่าเป็นจอแนวนอน ลองชี้ดูด้วยว่ามีสกิลที่หลุดออกนอกมุมขวาล่าง (ขวาของแนวนอน 70% + ล่างของแนวตั้ง 55%) หรือไม่ หนึ่งครั้งนั้นจะทำให้รู้ด้วยตัวเองว่า lint ทำงานอะไร
ถ้าเป็นทีมก็เริ่มด้วยขั้นต่อไปนี้ ตรึงสามฟังก์ชันของ skill_layout_lint.py ใน §9.2.3 (ขนาด·ระยะห่าง·มุมนิ้วโป้งขวา) เป็นโค้ดไว้ก่อน สามฟังก์ชันก็พอ เมื่อมี rulebook ไม่ว่าจะเป็นแบบจัดวางของ AI หรือแบบของนักออกแบบ ก็วัดด้วยเส้นเดียวกันได้ และเฉพาะแบบที่ผ่าน lint เท่านั้นจึงจะส่งต่อไปยัง 96_ArtGuide/06_UI/ ของทีมอาร์ต แล้ว sync อัตโนมัติผ่านเส้นทาง _convert_md_to_html.py → _SyncToArtRepo.bat ก่อนที่พิกัดที่ยืนยันจะถึงทีมอาร์ต งานสุดท้ายของมนุษย์มีเพียงการตัดสิน WARN หนึ่งข้อว่า «นี่คือเจตนา» เท่านั้น
ผู้อ่านหลัก: นักออกแบบ UX·UI ที่ทำงานร่วมกับสายงานที่ไม่ใช่นักออกแบบ (ทีมอาร์ต) ทุกวัน (ทีมขนาดกลาง) ฉบับย่อสำหรับผู้อ่านคนเดียว/งานอดิเรก: §9.3.8 「ถ้าทำคนเดียวก็แค่นี้พอ」
เมื่อนักออกแบบเกมจัดระเบียบสิ่งที่ตัดสินใจเรื่อง UI ไว้ในรูปแบบ Markdown งานจะเรียบร้อยขึ้น มีการควบคุมเวอร์ชัน เห็น diff และโยนให้ AI ได้ทันที ปัญหาคือทีมอาร์ตไม่อ่าน Markdown พูดให้ตรงกว่านั้นคือ ไม่มีเหตุผลที่จะต้องอ่าน ถ้าบอกนักออกแบบอาร์ตว่า "ช่วยดึงไฟล์ 아트_결정사항.md จาก SVN มาดูหน่อย" ครึ่งหนึ่งยังไม่ได้ติดตั้ง SVN client ส่วนอีกครึ่งที่เหลือมองหน้าจอที่เปิดด้วย Notepad ซึ่งหัวข้อ ## กับไวยากรณ์ตารางพังหมด แล้วถามว่า "อันนี้ดูยังไง"
วิธีแก้ที่ผิดตรงนี้คือ "มาสอน Markdown ให้ทีมอาร์ตกันเถอะ" เวลาของนักออกแบบอาร์ตควรใช้ไปกับการดันพิกเซล เวลาที่ใช้ไปกับการเรียนรู้คอนเวนชัน Markdown การ check out จาก SVN และวิธีดู diff ล้วนเป็นการสูญเสียทั้งหมด วิธีแก้ที่ถูกคือ ทำให้ฝั่งนักออกแบบเกมเป็นผู้อัตโนมัติการแปลงและการส่งต่อ เพื่อทำให้ภาระการเรียนรู้ของทีมอาร์ตเป็น 0 นักออกแบบเกมเขียนด้วย md สคริปต์แปลงเป็น html สคริปต์อีกตัวดันเข้าไปยัง repository ของอาร์ต และทีมอาร์ตดูเฉพาะ html ในเบราว์เซอร์ บทนี้จะรันไปป์ไลน์นั้นจริงให้จบครบหนึ่งรอบ — ตั้งแต่จุดที่ดึงร่างสิ่งที่ตัดสินใจออกมาด้วย AI การอัตโนมัติการแปลงและการส่งต่อ ไปจนถึงสิ่งที่มนุษย์ปฏิเสธ
หนังสือจำนวนมากสรุปสาเหตุที่การทำงานร่วมกันระหว่างฝ่ายออกแบบกับอาร์ตพังว่าเป็น "อำนาจการตัดสินใจที่คลุมเครือ" ใครเป็นคนกำหนดสี ใครเป็นคนกำหนดฟังก์ชัน การแบ่งหน้าที่นั้นก็สำคัญ แต่ต่อให้วาดตารางแบ่งหน้าที่ได้ดีแค่ไหน ถ้าทีมอาร์ตอ่านตารางแบ่งหน้าที่นั้นไม่ได้ ก็จะไม่เกิดอะไรขึ้นเลย จุดที่เกิดอุบัติเหตุบ่อยกว่าในงานจริงไม่ใช่อำนาจการตัดสินใจ แต่เป็นฟอร์แมตการส่งต่อ
ในโปรเจกต์ของผู้เขียน (MMORPG ที่เน้นมือถือเป็นหลัก ต่อจากนี้เรียก "โปรเจกต์ A") อุบัติเหตุที่เกิดซ้ำจริงเป็นแบบนี้
| อุบัติเหตุ | สาเหตุที่ผิวเผิน | สาเหตุที่แท้จริง |
|---|---|---|
| อาร์ตทำงานด้วยสิ่งที่ตัดสินใจเวอร์ชันเก่า | "ไม่ได้รับเวอร์ชันล่าสุดนี่นา" | การส่งต่อเป็นแบบมือ (แนบเมล) จึงตกหล่น |
| ตารางสิ่งที่ตัดสินใจดูพัง | "อันนี้ทำไมเป็นแบบนี้" | เปิด md ด้วย Notepad |
| "สิ่งที่ตัดสินใจนั้นจดไว้ที่ไหน" | ส่งต่อด้วยปากเปล่า | ฉบับจริง (canonical) กระจัดกระจายอยู่ในแชต |
อุบัติเหตุทั้งสามไม่ใช่ปัญหาเรื่องอำนาจการตัดสินใจ มันเกิดเพราะ เอกสารฉบับจริงไม่ได้ถูกส่งต่อในฟอร์แมตที่ทีมอาร์ตอ่าน ไม่ได้ส่งแบบอัตโนมัติ และไม่ได้อยู่ในสถานะล่าสุดเสมอ ดังนั้นเครื่องมือของบทนี้จึงไม่ใช่ตารางแบ่งหน้าที่ แต่เป็นไปป์ไลน์การส่งต่อ เพราะการแบ่งหน้าที่ตกลงครั้งเดียวก็จบ แต่การส่งต่อเกิดขึ้นทุกครั้งที่การตัดสินใจเปลี่ยน
เริ่มจากดูโครงสร้างโฟลเดอร์จริงก่อน อาร์ตไกด์ของโปรเจกต์ A แบ่งเป็น 7 โดเมนภายใต้ workspace/96_ArtGuide/
96_ArtGuide/
├── 00_Common/ # ส่วนกลาง (สไตล์·พาเลตต์สี·เกณฑ์การจัดแสง)
├── 01_Character/
├── 02_Animation/
├── 03_Monster/
├── 04_NPC/
├── 05_VFX/
├── 06_UI/ # ← พื้นที่ที่บทนี้พูดถึง
└── 07_Env/
และในโฟลเดอร์นี้มีไฟล์สำหรับการทำงานสองตัวอยู่ด้วยกัน คือ _convert_md_to_html.py กับ _SyncToArtRepo.bat สองไฟล์นี้คือกระดูกสันหลังของบทนี้
ภาพรวมทั้งหมดมีสี่ขั้น แก่นคือ มนุษย์ (นักออกแบบเกม) แตะแค่ md ในขั้นที่ 1 ส่วนอีก 3 ขั้นที่เหลือสคริปต์รันทั้งหมด ทีมอาร์ตดูเฉพาะ html ในขั้นที่ 4 ไม่จำเป็นต้องรู้ด้วยซ้ำว่ามี md อยู่
flowchart LR
A["ขั้น 1 · SVN ฝ่ายออกแบบ
อาร์ต_สิ่งที่ตัดสินใจ.md
(นักออกแบบเกมเขียน/AI ร่าง)"]
A --> B["ขั้น 2 · _convert_md_to_html.py
แปลง md → html
(ฝังตาราง·หัวข้อ·รูปภาพ)"]
B --> C["ขั้น 3 · _SyncToArtRepo.bat
push เข้า SVN อาร์ตอัตโนมัติ
(repository แยกต่างหาก)"]
C --> D["ขั้น 4 · เบราว์เซอร์ทีมอาร์ต
ดูเฉพาะ html
(ภาระเรียนรู้คอนเวนชัน md เป็น 0)"]
D -.ฟีดแบ็ก/ขอแก้ไข.-> A
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
class A,D human;
class B,C code;
จะระบุให้ชัดว่าแต่ละขั้นทำอะไรกันแน่
ขั้น 1 (นักออกแบบเกม, มนุษย์) — เขียนสิ่งที่ตัดสินใจลงใน 06_UI/아트_결정사항.md ด้วย Markdown วิธีสอด AI เข้าตรงจุดนี้คือกระดูกสันหลังของ §9.3.4 สิ่งที่ตัดสินใจเป็นรายการอย่างเช่น "สี primary ของปุ่มคือ #3A7BD5", "เป้าการแตะขั้นต่ำ 44pt"
ขั้น 2 (_convert_md_to_html.py, อัตโนมัติ) — แปลง md เป็น html ไม่ใช่การแปลงธรรมดา แต่เรนเดอร์ตารางให้ทีมอาร์ตดูง่าย ฝังการอ้างอิงรูปภาพ  แบบอินไลน์ และใส่สารบัญให้ ผลลัพธ์คือ html แบบสมบูรณ์ในตัวเองที่นักออกแบบอาร์ตเปิดได้ด้วยการดับเบิลคลิกครั้งเดียวในเบราว์เซอร์
ขั้น 3 (_SyncToArtRepo.bat, อัตโนมัติ) — push html ที่แปลงแล้วเข้าไปยัง SVN repository แยกต่างหากของทีมอาร์ต แก่นคือ repository ของฝ่ายออกแบบและ repository ของอาร์ตถูกแยกออกจากกัน ทีมอาร์ตดูแค่ repository ของตัวเองก็พอ ไม่จำเป็นต้องรู้สิทธิ์เข้าถึงหรือโครงสร้างของ repository ฝ่ายออกแบบ
ขั้น 4 (ทีมอาร์ต, มนุษย์) — นักออกแบบอาร์ตเปิด html ที่ซิงค์มาในเบราว์เซอร์จาก repository ของตัวเอง ไม่จำเป็นต้องเรียนทั้งไวยากรณ์ Markdown คำสั่ง SVN และวิธีดู diff การที่ ภาระการเรียนรู้คอนเวนชัน md เป็น 0 คือทั้งเป้าหมายการออกแบบและเกณฑ์ความสำเร็จของไปป์ไลน์นี้
ฟีดแบ็กย้อนจากขั้น 4 กลับมาขั้น 1 เมื่ออาร์ตบอกว่า "สิ่งที่ตัดสินใจอันนี้แปลก ๆ" นักออกแบบเกมก็แก้ md แล้วขั้น 2\~3 รันอัตโนมัติอีกครั้ง อาร์ตแค่เปิด html ที่อัปเดตแล้วใหม่ก็พอ
ตรงนี้จะหยุดสักครั้งแล้วระบุเจตนาการออกแบบให้ชัด การเปลี่ยน md เป็น html ในตัวมันเองเป็นเรื่องเล็กน้อย การออกแบบที่แท้จริงอยู่ที่การตัดสินว่า ใครจะแบกภาระการเรียนรู้ของใคร
ทางเลือกมีสองแยก
แก่นคือความไม่สมมาตร แบบ A ต้นทุนการเรียนรู้ถูกคูณด้วยจำนวนคนอาร์ต และต้นทุนนั้นเกิดซ้ำกับพนักงานใหม่ทุกคน ส่วนแบบ B นักออกแบบเกมเขียนสคริปต์ครั้งเดียวก็จบ และต้นทุนส่วนเพิ่มฝั่งอาร์ตเป็น 0 การโยนภาระไปยังฝั่งที่อัตโนมัติได้ ไม่ใช่ฝั่งที่มีคนเยอะ — นี่คือหลักการข้อ 1 ของเครื่องมือทำงานร่วมกันกับสายงานที่ไม่ใช่นักออกแบบ ถ้าหลักการนี้พัง คือถ้าเครื่องมือทำงานร่วมกันบังคับให้สายงานอีกฝ่ายต้องเรียนรู้สิ่งใหม่ เครื่องมือนั้นจะ "ไม่มีใครใช้" ภายในหนึ่งถึงสองไตรมาส
ในขั้น 1 บอกว่านักออกแบบเกมเขียน md แต่จะแสดงจุดที่ดึงร่าง md นั้นออกมาด้วย AI ให้ครบหนึ่งรอบ เมื่อการประชุมตัดสินใจจบลง จะเหลือบันทึกกระจัดกระจาย (แชต·รูปถ่ายไวต์บอร์ด·ข้อตกลงด้วยปากเปล่า) งานจัดระเบียบสิ่งเหล่านี้ให้เป็น md ฉบับจริงของสิ่งที่ตัดสินใจนั้นน่าเบื่อ และฟอร์แมตแกว่งทุกครั้ง เป็นงานที่เหมาะกับ AI พอดี แต่แก่นคือเส้นแบ่งที่ว่า การตัดสินใจเองเป็นหน้าที่ของมนุษย์ ส่วน AI แค่จัดระเบียบการตัดสินใจให้เข้าฟอร์แมตที่กำหนดเท่านั้น
[บันทึกการประชุมตัดสินใจ UI — เกี่ยวกับสล็อตสกิล 06_UI, แบบดิบ]
- ตกลงว่าจะเพิ่มขนาดปุ่มสล็อตสกิล เพราะบนมือถือมันเล็ก
- สีให้อาร์ตเป็นคนกำหนด แต่โทน primary ให้คงโทนสีน้ำเงิน
- ตกลงว่าสถานะสล็อตไม่พร้อมใช้ (คูลดาวน์) แสดงเป็นสีเทา + ตัวเลขซ้อนทับ
- หลายภาษา... ชื่อสกิลยาวขึ้นจะทำยังไงดี ขอพักไว้ก่อน
- อ้อ แล้วก็ให้กดค้าง (long press) แล้วขึ้นคำอธิบายสกิล (อันนี้เป็นฟังก์ชัน ฝ่ายออกแบบยืนยันแล้ว)
ไฟล์แนบคือบันทึกการประชุมตัดสินใจ UI แบบดิบ ช่วยจัดระเบียบเป็น Markdown สิ่งที่ตัดสินใจเพื่อส่งให้ทีมอาร์ตหน่อย
จัดประเภทแต่ละรายการเป็น [ยืนยัน]/[พักไว้]/[อาร์ตตัดสิน] (ยืนยัน=ฝ่ายออกแบบกำหนดและอาร์ตทำตาม, พักไว้=ยังไม่ตัดสิน+เหตุผล, อาร์ตตัดสิน=เป็นการแสดงผลทางภาพจึงให้อาร์ตกำหนด·ฝ่ายออกแบบให้แค่ข้อจำกัด) แล้วแนบเหตุผล/ข้อจำกัดบรรทัดละหนึ่ง — ถ้าในบันทึกไม่มีเหตุผล ให้เขียนตามตรงว่า 'ไม่ได้ระบุเหตุผล' อย่าเติมด้วยการคาดเดา
สิ่งที่ตรวจสอบได้ด้วยมาตรฐานสาธารณะอย่างขนาดการแตะ ให้ระบุมาตรฐาน ส่วนรายการที่ไม่มีในบันทึก ห้ามกุขึ้นใหม่เด็ดขาด
ตารางใช้ |รายการ|ประเภท|การตัดสิน/ข้อจำกัด|เหตุผล|ตรวจสอบได้หรือไม่| รวม frontmatter (title, layer: L1) แล้วให้มาเป็นฉบับเต็ม
จุดศูนย์ถ่วงของพรอมต์นี้ไม่ได้อยู่ที่งานจัดระเบียบ แต่อยู่ที่การบังคับจัดประเภทและการห้ามกุขึ้น เมื่อสั่งให้ AI จัดระเบียบสิ่งที่ตัดสินใจ มันจะเติม "best practice" ที่ไม่มีในบันทึกเข้ามาอย่างเอื้อเฟื้อ ส่วนที่เติมเข้ามานั้นทีมอาร์ตจะอ่านว่าเป็น "สิ่งที่ฝ่ายออกแบบยืนยันแล้ว" ดังนั้นจึงตอกหมุดสามตัว คือ การจัดประเภท [ยืนยัน]/[พักไว้]/[อาร์ตตัดสิน] เหตุผลเป็นข้อบังคับ และ "ถ้าไม่มีในบันทึกอย่ากุขึ้น"
---
title: 06_UI สิ่งที่ตัดสินใจ — สล็อตสกิล
layer: L1
---
# 06_UI สิ่งที่ตัดสินใจ — สล็อตสกิล
| รายการ | ประเภท | การตัดสิน/ข้อจำกัด | เหตุผล | ตรวจสอบได้ |
|---|---|---|---|---|
| ขนาดปุ่มสล็อตสกิล | [ยืนยัน] | ขั้นต่ำ 44×44 pt ขึ้นไป | เป้าการแตะบนมือถือ, Apple HIG แนะนำ 44pt | ✅ พิกัด·ขนาด |
| โทน primary | [อาร์ตตัดสิน] | คงโทนสีน้ำเงิน (ข้อจำกัด), ค่าที่แม่นยำให้อาร์ต | บันทึก: "โทน primary คงโทนสีน้ำเงิน" | △ ค่าสี |
| การแสดงสถานะคูลดาวน์ | [ยืนยัน] | โอเวอร์เลย์สีเทา + ตัวเลขที่เหลือ | ข้อตกลงในที่ประชุม | ◯ ตรวจสอบด้วยสายตา |
| ความยาวชื่อสกิลหลายภาษา | [พักไว้] | ประชุมภายหลัง | บันทึก: "ขอพักไว้ก่อน" — ภาษาที่ยาว (เยอรมัน·รัสเซีย) ยังไม่ได้ตรวจสอบ | — |
| คำอธิบายสกิลแบบกดค้าง | [ยืนยัน] | กดค้าง → แสดงทูลทิป | ฟังก์ชันยืนยันแล้ว (พื้นที่ฝ่ายออกแบบ) | ◯ ตรวจสอบการทำงาน |
| ระยะห่างระหว่างสล็อต | [ยืนยัน] | แนะนำขั้นต่ำ 8 dp | ไม่ได้ระบุเหตุผล — ไม่มีในบันทึก, อ้างอิงค่าแนะนำของ Material | ✅ |
จะอัปโหลดผลลัพธ์นี้เป็นฉบับจริงตรง ๆ ไม่ได้ มีบรรทัดหนึ่งที่ติดขัด
บรรทัดสุดท้าย ระยะห่างระหว่างสล็อต | แนะนำขั้นต่ำ 8 dp เมื่อกลับไปดูบันทึก เรื่องระยะห่าง ไม่เคยถูกพูดถึง ในที่ประชุม AI ตัดสินว่า "เป็น UI มือถือก็ต้องมีเกณฑ์ระยะห่างด้วยสิ" แล้วเติมค่าแนะนำของ Material Design เข้ามาอย่างเอื้อเฟื้อ นี่คือการละเมิดกฎข้อ 5 ("ห้ามกุรายการที่ไม่มีในบันทึก") แม้ AI จะเขียนตามตรงว่า ไม่ได้ระบุเหตุผล แต่ก็ไม่ควรสร้างรายการนั้นขึ้นมาตั้งแต่แรก ถ้าบรรทัดนี้ไปถึงทีมอาร์ต มันจะถูกอ่านว่า "ฝ่ายออกแบบยืนยันระยะห่าง 8dp แล้ว"
ดังนั้นจึงสั่งใหม่
'ระยะห่างระหว่างสล็อต' ไม่มีในบันทึกการประชุม เป็นสิ่งที่คุณเติมเข้ามาเอง ช่วยเอาออกจากตาราง
สิ่งที่ไม่มีในบันทึกแต่ดูเหมือนจำเป็นต้องตัดสิน ให้ใส่ไว้เป็นเพียงรายการเสนอที่ '## ค้างอยู่ — วาระประชุมครั้งหน้า' ด้านล่างสุด ไม่ใช่ในตาราง และในตารางสิ่งที่ตัดสินใจให้เหลือเฉพาะรายการที่มีอยู่จริงในบันทึกเท่านั้น
AI เอารายการระยะห่างออกจากตาราง แล้วแยก "วาระประชุมครั้งหน้า: เกณฑ์ระยะห่างระหว่างสล็อต (ปัจจุบันยังไม่กำหนด), การจัดการความยาวชื่อสกิลหลายภาษา" เป็นรายการเสนอไว้ด้านล่างสุด ตอนนี้ในตารางสิ่งที่ตัดสินใจเหลือแต่สิ่งที่ตัดสินใจจริงในที่ประชุม ส่วนรายการเสนอที่สมเหตุสมผลซึ่ง AI นึกขึ้นมาถูกลดระดับจาก "ยืนยัน" เป็น "วาระ" เหตุผลที่การแยกนี้สำคัญคือ ในเอกสารที่ทีมอาร์ตได้รับ ถ้าสิ่งที่ยืนยันแล้วกับสิ่งที่ยังถกกันอยู่ปะปนกัน อาร์ตจะเข้าใจว่าสิ่งที่ยังไม่กำหนดเป็นสิ่งที่ยืนยันแล้วและเริ่มงาน
ด้วยการไป-กลับครั้งเดียวนี้ ขั้น 1 (md) ก็เสร็จสมบูรณ์ ตอนนี้พ้นมือมนุษย์แล้วส่งต่อไปยังการอัตโนมัติขั้น 2\~3
md ที่เสร็จแล้วตอนนี้สคริปต์จัดการต่อ โครงของสคริปต์แปลงนั้นเรียบง่าย
# _convert_md_to_html.py (โครง)
# อินพุต: 06_UI/*.md (นักออกแบบเกมเขียนสิ่งที่ตัดสินใจ)
# เอาต์พุต: ไฟล์ .html ชื่อเดียวกัน (ไฟล์สมบูรณ์ในตัวเองที่ทีมอาร์ตเปิดในเบราว์เซอร์)
def convert(md_path):
md_text = read(md_path)
front, body = split_frontmatter(md_text) # ดึง title·layer
html_body = markdown_to_html(body, extensions=[
"tables", # เรนเดอร์ตาราง (แก้ตารางพังที่อาร์ตเคยเห็นใน Notepad)
"fenced_code",
])
html_body = embed_images_inline(html_body, base_dir=md_path.parent)
# ↑ ฝังการอ้างอิงอย่าง  แบบอินไลน์ →
# อาร์ตไม่ต้องรับไฟล์รูปภาพแยกต่างหาก
toc = build_toc(html_body) # สร้างสารบัญอัตโนมัติ
return render_template(title=front["title"], toc=toc, body=html_body)
แก่นตรงนี้คือการแปลงไม่ใช่แค่ md→html ธรรมดา แต่ทำเพิ่มอีกสามอย่าง เรนเดอร์ตารางให้ถูกต้อง (ตาราง |---| พังที่อาร์ตเคยเห็นใน Notepad หายไป), ฝังรูปภาพแบบอินไลน์ (อาร์ตไม่ต้องรับไฟล์รูปภาพแยก), และ ใส่สารบัญอัตโนมัติ (ต่อให้สิ่งที่ตัดสินใจยาวขึ้น อาร์ตก็กระโดดไปยังรายการที่ต้องการได้) สามอย่างนี้ทำให้ "ดูแค่ html ก็พอ" เป็นจริงได้
สคริปต์ส่งต่อถูกมัดรวมแบบนี้
REM _SyncToArtRepo.bat (โครง)
REM 1) แปลง md ทั้งหมดของ 06_UI เป็น html
python _convert_md_to_html.py 06_UI\*.md
REM 2) คัดลอก html ที่แปลงแล้วไปยัง working copy ของ SVN อาร์ต
xcopy 06_UI\*.html %ART_REPO%\UI\ /Y
REM 3) commit·push เข้า SVN อาร์ตอัตโนมัติ (repository แยกต่างหาก)
svn add %ART_REPO%\UI\*.html --force
svn commit %ART_REPO%\UI -m "[auto] อัปเดตสิ่งที่ตัดสินใจ 06_UI"
สิ่งที่นักออกแบบเกมทำคือดับเบิลคลิก _SyncToArtRepo.bat ครั้งเดียว (หรือผูกฮุกให้รันอัตโนมัติเมื่อ commit สิ่งที่ตัดสินใจ) จากนั้นการแปลง·คัดลอก·push เข้า repository อาร์ตจะรันรวดเดียว ทีมอาร์ตอัปเดต repository ของตัวเองก็จะมี html ล่าสุดมาแล้ว
AI เข้าไปได้แค่ไหน — โค้ดการอัตโนมัติขั้น 2\~3 นี้จะให้ AI เขียนก็ได้ "ช่วยเขียนสคริปต์ที่รับโฟลเดอร์ md แปลงเป็น html พร้อมตาราง·รูปภาพ แล้ว push เข้า SVN แยกต่างหากหน่อย" เป็นพื้นที่ที่ AI ทำได้ดี แต่ จะกำหนดให้การตัดสินใจไหนเป็นสิ่งที่ยืนยัน อะไรจะโยนให้เป็นอาร์ตตัดสิน (§9.3.4) จะไม่มอบหมายให้ AI โค้ดคือ AI การตัดสินใจคือมนุษย์ — การแบ่งหน้าที่ที่ย้ำตลอดทั้งเล่มก็เป็นเช่นนี้ตรงนี้เหมือนกัน
กรณีตัวอย่างที่ AI ถูกใช้ผิดในการทำงานร่วมกันกับอาร์ตคือพรอมต์รูปภาพ เมื่อนักออกแบบเกมให้เรเฟอเรนซ์แก่ทีมอาร์ต หรือต้องการแสดงภาพคอนเซปต์อย่างรวดเร็ว จะใช้ AI สร้างรูปภาพ ความผิดพลาดที่พบบ่อยตอนนี้คือเริ่มเขียนจากการบรรยายผลลัพธ์ ("ปุ่มกลมสีน้ำเงิน, เอฟเฟกต์เรืองแสง, 4K")
หนึ่งในหลักการการทำงานร่วมกันของผู้เขียนคือ image_prompt_design_intent_first — พรอมต์รูปภาพก็เขียนเจตนาการออกแบบก่อน ไม่ใช่บรรยายผลลัพธ์
| วิธี | พรอมต์ | ปัญหา/ผล |
|---|---|---|
| ผลลัพธ์ก่อน (แย่) | "ปุ่มกลมสีน้ำเงิน, เรืองแสง, 4K, UI เกม" | อาร์ตถาม "ทำไมสีน้ำเงิน?" ไม่ได้ เจตนาระเหยหาย |
| เจตนาก่อน (ดี) | "ปุ่มสกิลที่บอกสถานะพร้อมใช้/คูลดาวน์ได้อย่างเข้าใจง่าย ใช้งานได้=ดึงดูดสายตาให้อยากกดทันที, คูลดาวน์=ยับยั้ง โทนเป็น primary สีน้ำเงิน" | อาร์ตดูเจตนาแล้วเสนอแนวทางภาพที่ดีกว่ากลับมาได้ |
ความต่างคือสิ่งที่ทีมอาร์ตทำได้เมื่อได้รับพรอมต์ ถ้าได้รับแค่การบรรยายผลลัพธ์ อาร์ตจะวาดตามนั้นหรือไม่ก็เพิกเฉย มีแค่สองทาง ถ้าได้รับเจตนาการออกแบบ อาร์ตจะเสนอแนวทางภาพของตัวเองที่ตอบเจตนานั้นได้ดีกว่า นี่คือวิธีที่นักออกแบบเกมให้ทิศทางโดยไม่ล้ำเข้าไปในพื้นที่การตัดสินใจของอาร์ต ([อาร์ตตัดสิน] ใน §9.3.4) นักออกแบบเกมให้ "เพื่ออะไร" ส่วนอาร์ตกำหนด "ให้ดูเป็นอย่างไร"
ดังนั้นเมื่อใส่เรเฟอเรนซ์รูปภาพลงใน md สิ่งที่ตัดสินใจของ §9.3.4 ก็เขียนแคปชันไม่ใช่ "ปุ่มสีน้ำเงิน" แต่เป็น "สล็อตที่มีจุดประสงค์เพื่อแยกสถานะคูลดาวน์ — การแสดงผลที่แม่นยำเป็นอาร์ตตัดสิน" เนื่องจากสคริปต์แปลงฝังแคปชันนี้ทั้งดุ้นลงใน html อาร์ตจึงได้รับทั้งรูปภาพและเจตนาไปพร้อมกัน
มีสิ่งเย้ายวนให้เขียนผลของไปป์ไลน์นี้เป็นตัวเลขอย่าง "อุบัติเหตุการทำงานร่วมกันลดลง 70%" ตัวเลขแบบนั้นถ้าไม่ได้รับการตรวจสอบจะบั่นทอนความน่าเชื่อถือของหนังสือ จะแยกแยะอย่างซื่อสัตย์
สิ่งที่ตรวจสอบได้ด้วยมาตรฐานสาธารณะ — มาตรฐานสาธารณะที่อยู่ในสิ่งที่ตัดสินใจอย่างการแตะ 44pt·ระยะห่าง 8dp·คอนทราสต์ 4.5:1 เป็นไปตาม rulebook ใน §9.1 ไม่ใช่ตัวเลขที่กุขึ้น แต่เป็นค่าที่อ้างอิงมาตรง ๆ และตรวจสอบอัตโนมัติได้ด้วย lint
ตัวชี้วัดเชิงปฏิบัติการที่วัดได้ — สิ่งที่ไปป์ไลน์นี้นับได้จริงคือสิ่งเหล่านี้ จำนวนอุบัติเหตุที่อาร์ตทำงานด้วยเวอร์ชันเก่า (ถ้าการส่งต่อเป็นอัตโนมัติจะเข้าใกล้ 0), เวลาที่พนักงานใหม่ของทีมอาร์ตใช้กว่าจะเปิดดูสิ่งที่ตัดสินใจครั้งแรก (ถ้าดับเบิลคลิก html ก็เป็นหน่วยนาที), ความล่าช้ากว่าการเปลี่ยนการตัดสินใจจะสะท้อนเข้า repository อาร์ต (เวลารันสคริปต์) สามอย่างนี้ไม่ใช่ "ความรู้สึก" แต่นับได้ด้วยล็อก·การสังเกต
การประมาณของผู้เขียน (สมมติฐานที่ยังไม่ได้ตรวจสอบ) — ทิศทางที่ว่า "ตกหล่นน้อยลงกว่าตอนส่งต่อด้วยเมลแบบมือ" นั้นชัดเจน แต่อัตราการลดที่แม่นยำไม่ได้บันทึกตัวอย่างไว้ต่างหาก จึงไม่ฟันธง อ่านเป็น ทิศทาง ดีกว่าค่าสัมบูรณ์ คือ ถ้าการส่งต่อขึ้นอยู่กับมือมนุษย์ ในสัปดาห์ที่ยุ่งจะต้องมีการตกหล่นแน่นอน แต่ถ้าการส่งต่อเป็นสคริปต์ การตกหล่นจะหายไปเชิงโครงสร้าง
ถ้าทำคนเดียวก็แค่นี้พอ: ไม่ต้องมีทั้งทีมอาร์ตและ SVN ลองสมมติว่าคุณส่งต่อการตัดสินใจ UI ให้อาร์ตเอาต์ซอร์สที่คุณจ้าง หรือเพื่อนที่ทำงานร่วมกัน ใช้พรอมต์ของ §9.3.4 ตรง ๆ แล้วลองดึง md หนึ่งหน้าที่จัดประเภทการตัดสินใจ UI กระจัดกระจายในหัวของคุณเป็น [ยืนยัน]/[พักไว้]/[อาร์ตตัดสิน] ออกมาด้วย AI แล้วลองหารายการหนึ่งที่ AI "เติมเข้ามาอย่างเอื้อเฟื้อ" (สิ่งที่ไม่มีในบันทึก) แล้วโต้กลับว่า "อันนี้ฉันไม่เคยกำหนด เอาออก" คุณจะสัมผัสด้วยตัวเองว่าเส้นแบ่งระหว่างมนุษย์กับ AI ในการจัดระเบียบการตัดสินใจอยู่ตรงไหน การแปลงใช้แพ็กเกจ
markdownแค่บรรทัดเดียวpython -m markdown decision.md > decision.htmlก็พอ
ถ้าเป็นทีม เริ่มจากขั้นเดียวต่อไปนี้ อย่าเพิ่งเขียนการซิงค์สองทางใหญ่โต ใส่ การแปลงหนึ่งบรรทัด + การส่งต่อหนึ่งบรรทัด ก่อน สคริปต์แปลงที่เปลี่ยน md สิ่งที่ตัดสินใจเป็น html (แค่การเรนเดอร์ตาราง·ฝังรูปภาพของ §9.3.5) และหนึ่งบรรทัดที่คัดลอก html นั้นไปยังตำแหน่งที่อาร์ตดู (ไดรฟ์ที่แชร์หรือ repository แยกก็ได้) แค่มีสองบรรทัดนี้ อุบัติเหตุที่พบบ่อยที่สุดอย่าง "อาร์ตเจอตารางพังตอนเปิด md ใน Notepad" ก็หายไป ตารางแบ่งหน้าที่·การจัดระเบียบอำนาจการตัดสินใจค่อยมาทีหลัง
สรุปเป็น setup → prompt → verify ได้แบบนี้
| ขั้น | สิ่งที่ต้องทำ |
|---|---|
| setup | ใส่ _convert_md_to_html.py (แปลง) + การส่งต่อหนึ่งบรรทัด (คัดลอก/push) ก่อน |
| prompt | จัดระเบียบบันทึกการประชุมเป็น md [ยืนยัน]/[พักไว้]/[อาร์ตตัดสิน] ด้วยพรอมต์ §9.3.4 |
| verify | ปฏิเสธรายการที่ AI กุขึ้น (ที่ไม่มีในบันทึก) → รันการแปลง·ส่งต่ออัตโนมัติ → อาร์ตตรวจแค่ html |
หกโมงสี่สิบนาทีเย็นวันศุกร์ เป็นวันที่ตัดสินใจใส่เควสต์ใหม่ 12 ชนิดลงในบิลด์ภายในของบริษัทในวันจันทร์สัปดาห์หน้า ผมเพิ่มแถวใหม่ลงใน quest_table เติมแถวที่สอดคล้องกันลงในชีตรางวัล แล้วเชื่อมบทพูดของ NPC เข้ากับชีตไดอะล็อก สามชีต ประมาณ 50 แถว ผมไล่ตรวจด้วยตาสองรอบแล้วก็ดูเหมือนจะไม่มีปัญหา
เช้าวันจันทร์บิลด์พัง reward_id ที่เควสต์ใหม่หนึ่งรายการอ้างถึงนั้นไม่มีอยู่ในชีตรางวัล ตอนเย็นวันศุกร์ผมลบแถวรางวัลแถวหนึ่งแล้วเพิ่มกลับเข้าไปใหม่ และพิมพ์ id ผิดไปตัวหนึ่ง — จาก rwd_q318 กลายเป็น rwd_q381 เป็นพิมพ์ผิดชนิดที่ตามนุษย์ไม่มีทางจับได้เลย สองชีตนี้อยู่คนละโฟลเดอร์ คนละคนแก้ คนละเวลา เมื่อมี 50 แถวก็ยังจับด้วยตาได้ แต่เมื่อชีตกว่า 30 ชีตเริ่มอ้างถึงกันด้วยคีย์นอก (FK) ตามนุษย์ก็ไม่ใช่เครื่องมือตรวจสอบอีกต่อไป
บทนี้จะแสดง atom ตรวจสอบชนิดหนึ่งที่จับพิมพ์ผิดแบบนั้นได้ก่อนที่บิลด์จะพัง — integrity_check_fk — ซึ่งตรวจสอบความสอดคล้องของ FK ในชีตกว่า 30 ชีต และเมื่อพังก็จะแจ้งไปยังผู้รับผิดชอบผ่านเครื่องมือทำงานร่วมกัน (SaaS ที่จัดการงานและกำหนดการ — โปรเจกต์นี้ใช้ ClickUp ส่วน JIRA และ Redmine ก็อยู่ในตำแหน่งเดียวกัน) ผมจะแสดงผ่านการไล่ตามเซสชันจริงเซสชันหนึ่งที่ผมรันมาเอง
ผมเข้าวงการนี้ครั้งแรกด้วยงานหาบรรทัดที่ผิดเพี้ยนในสิ่งที่คนอื่นทำขึ้นมา งานแรกของผมคือ QA และการตรวจสอบเกมแบบเล่นคนเดียว และตอนนั้นมือกับตาคือเครื่องมือตรวจสอบเพียงอย่างเดียว เวลาผ่านไปกว่า 20 ปี ตอนนี้ผมส่งงานแบบเดียวกันนั้นให้โค้ดทำ — ในตำแหน่งที่ตามนุษย์เลิกเป็นเครื่องมือตรวจสอบไปแล้ว
ก่อนอื่นมาดูภาพว่าตรวจสอบอะไรกันแน่ ชีตข้อมูลเกมก็เหมือนฐานข้อมูลเชิงสัมพันธ์ คอลัมน์หนึ่งของชีตหนึ่งชี้ไปยังคีย์หลักของอีกชีตหนึ่ง หากลูกศรนี้ขาด เกมจะตายตอนรันไทม์ หรือที่แย่กว่านั้นคือแสดงค่าว่างออกมาอย่างเงียบ ๆ
เส้นทึบสีเขียวคือการอ้างอิงที่ยังมีชีวิต ค่าที่ quest_table.reward_id ชี้ไปนั้นมีอยู่จริงใน reward_table.reward_id เส้นประสีแดงคือการอ้างอิงที่ตายแล้ว — npc_id ของเควสต์หนึ่งชี้ไปยัง id ที่ไม่มีอยู่ใน npc_table สิ่งที่ integrity_check_fk จับได้คือเส้นประสีแดงนี้
ชีตข้อมูลของโปรเจกต์ A มีการอ้างอิงเช่นนี้ไม่ใช่แค่หนึ่งหรือสองแห่ง ชีตกว่า 30 ชีตชี้ถึงกัน และหากวาดลูกศร FK ทั้งหมดออกมาก็จะใส่ลงในหน้าเดียวไม่ได้ การให้คนไล่ตามด้วยมือทุกครั้งจึงเป็นไปไม่ได้
จากตรงนี้ไปคือเซสชันที่ผมรันมาจริง ผมจะไม่สรุปย่อ แต่จะบันทึกทั้งพรอมต์เต็ม ผลลัพธ์ดิบ และส่วนที่ผมปฏิเสธแล้วร้องขอใหม่ ไว้ตามจริงทั้งหมด
ก่อนอื่นผมให้ตัวตรวจสอบสร้างแผนที่ FK ที่ว่า "คอลัมน์ไหนชี้ไปยังชีตไหน" ขึ้นมาก่อน เนื่องจากการให้คนลงทะเบียน FK ของ 30 ชีตทีละรายการนั้นไม่สมจริง ผมจึงให้มันอนุมานคอลัมน์ที่อาจเป็น FK โดยอัตโนมัติด้วยกฎการตั้งชื่อคอลัมน์ของชีตข้อมูลจำเพาะ (*_id ชี้ไปยังชีตที่มีชื่อเดียวกัน) พรอมต์เต็มที่ผมป้อนเข้าไปมีดังนี้
อ่านชีตข้อมูลจำเพาะที่แนบมา (แท็บ
$스키마) แล้วอนุมานคอลัมน์ที่ดูเหมือนเป็นคีย์นอก (FK) ในทุกชีตข้อมูล กฎ: (1) ชื่อคอลัมน์อยู่ในรูปxxx_idและ (2) หากในชีตxxx_tableหรือxxx_listมีคอลัมน์ PK (Primary Key หรือคีย์หลัก) ชื่อเดียวกันอยู่ ให้ถือว่าเป็นคอลัมน์ที่อาจเป็น FK ส่งผลการอนุมานออกมาเป็น JSON ในรูปลิสต์{source_sheet, source_column, target_sheet, target_column, confidence}สิ่งที่ฟันธงด้วยกฎไม่ได้ ให้แสดง confidence เป็น low และเขียนเหตุผลกำกับไว้
ผลลัพธ์ดิบบางส่วนจาก Claude (ผมคัดลอกมาตามจริงรวมถึงรายการที่กำกวมด้วย):
[
{"source_sheet":"quest_table","source_column":"reward_id",
"target_sheet":"reward_table","target_column":"reward_id","confidence":"high"},
{"source_sheet":"quest_table","source_column":"npc_id",
"target_sheet":"npc_table","target_column":"npc_id","confidence":"high"},
{"source_sheet":"quest_table","source_column":"prev_quest_id",
"target_sheet":"quest_table","target_column":"quest_id","confidence":"medium",
"note":"ชื่อไม่ใช่ quest_id แต่เป็น prev_quest_id จึงไม่เข้ากฎ สันนิษฐานว่าเป็นการอ้างอิงตัวเองในชีตเดียวกัน ต้องให้คนยืนยัน"},
{"source_sheet":"reward_table","source_column":"item_id",
"target_sheet":"item_table","target_column":"item_id","confidence":"high"},
{"source_sheet":"dialogue_table","source_column":"speaker_id",
"target_sheet":"npc_table","target_column":"npc_id","confidence":"low",
"note":"ไม่ชัดว่า speaker_id ชี้ไปยัง npc_id หรือ player ดูเหมือนมีแถวที่ speaker เป็น 'player' อยู่ด้วย ฟันธงไม่ได้"}
]
ตรงนี้ผมไม่ได้ปล่อยให้สองรายการผ่านไปเฉย ๆ prev_quest_id (การอ้างอิงตัวเอง) ผมรับมาเป็นเป้าหมายการตรวจสอบ แต่เติมข้อยกเว้นด้วยตัวเองว่า "ค่าว่าง (เควสต์แรก) ไม่ถือเป็นการละเมิด" ส่วน dialogue_table.speaker_id ผมปฏิเสธ — เพราะมีแถวที่ speaker เป็น 'player' อยู่จริง หากนำมันมาตรวจสอบเป็น FK บทพูดของผู้เล่นทั้งหมดจะถูกจับเป็นการละเมิดเท็จ
การปฏิเสธนี้สำคัญ หากนำแผนที่ FK ที่อนุมานอัตโนมัติมาใช้โดยไม่ผ่านการตรวจสอบ ในการรันครั้งแรกจะมีการละเมิดเท็จหลายร้อยรายการทะลักออกมา ต้องให้คนกรองสักครั้งก่อน การตรวจสอบจึงจะได้รับความเชื่อถือ
ผมตรึงแผนที่ FK ที่กรองแล้วไว้เป็นอินพุตของ atom integrity_check_fk รูปแบบของ atom มีดังนี้ นี่คือฉบับเต็มของ atom ตรวจสอบหนึ่งตัวที่ใช้งานจริงในโปรเจกต์ A
---
name: integrity_check_fk
description: ตรวจสอบว่าค่าในทุกคอลัมน์ source มีอยู่ใน PK ของชีต target ตามแผนที่ FK ที่ลงทะเบียนไว้
type: integrity_check
category: data
priority: P0 # FK ที่พังจะบล็อกบิลด์
execution_time:
- on_save # เฉพาะชีตนั้นเมื่อบันทึกชีต
- on_build # FK ทั้งหมดเมื่อบิลด์
- nightly # ทั้งหมด + รายงาน ทุกเที่ยงคืน
input:
fk_map: fk_map.reviewed.json # แผนที่ที่คนตรวจสอบแล้วในขั้นที่ 1~2
output_format: violation_list
on_violation:
- notify: clickup # แจ้ง ClickUp เมื่อล้มเหลว
related_atoms:
- integrity_check_clickup_notify
- integrity_check_id_uniqueness
---
ตัวตรรกะการตรวจสอบเองไม่ยาว เป็นการตรวจสอบความเป็นสมาชิกของเซต ว่าค่าแต่ละค่าของชีต source มีอยู่ในเซต PK ของชีต target หรือไม่
def check_fk(fk_map, sheets):
violations = []
for fk in fk_map:
pk_set = {r[fk["target_column"]] for r in sheets[fk["target_sheet"]]}
for i, row in enumerate(sheets[fk["source_sheet"]]):
val = row[fk["source_column"]]
if val in ("", None): # FK ว่างเป็นข้อยกเว้น (กฎที่กำหนดไว้ในขั้นที่ 1)
continue
if val not in pk_set:
violations.append({
"fk": f'{fk["source_sheet"]}.{fk["source_column"]}',
"row": i + 2, # เฮดเดอร์ 1 บรรทัด + 1-index
"value": val,
"target": fk["target_sheet"],
"severity": fk.get("severity", "P0"),
})
return violations
ผมรันการตรวจสอบกับชีตทั้ง 30 ชีตด้วยแผนที่ที่ตรวจสอบแล้ว ผลลัพธ์เป็น violation_list มาตรฐาน ต่อไปนี้คือผลที่ออกมาจริงในวันนั้น (id และชื่อชีตปกปิดตัวตน ส่วนจำนวนการละเมิดและโครงสร้างเป็นของจริง)
{
"check": "integrity_check_fk",
"executed_at": "2026-05-18 09:14:02",
"input_files": 31,
"violations": [
{"fk": "quest_table.reward_id", "row": 318, "value": "rwd_q381",
"target": "reward_table", "severity": "P0",
"message": "reward_id 'rwd_q381' ไม่มีใน reward_table สันนิษฐานว่าพิมพ์ผิดจาก 'rwd_q318'"},
{"fk": "quest_table.prev_quest_id", "row": 502, "value": "q_0500",
"target": "quest_table", "severity": "P0",
"message": "prev_quest_id 'q_0500' ไม่มีใน quest_table สันนิษฐานว่าการเขียนไม่ตรงกัน (เติม 0 นำหน้า) จาก 'q_500'"}
],
"summary": {"fk_checked": 23, "rows_scanned": 4117, "violations": 2, "passed": 4115}
}
พิมพ์ผิดของเย็นวันศุกร์ (rwd_q381) ถูกจับได้ในบรรทัดแรก รายการที่สองเป็นปัญหาอื่นที่ผมไม่รู้มาก่อน prev_quest_id ของเควสต์หนึ่งเป็น q_0500 แต่ id เควสต์จริงคือ q_500 เป็นความไม่ตรงกันในการเขียนที่มีการเติม 0 นำหน้า ตามนุษย์มองดูเผิน ๆ ก็เหมือนกัน แต่ในฐานะสตริงคือคนละค่า และเกมจะหาเควสต์ก่อนหน้าไม่เจอจึงปล่อยเควสต์นั้นไว้ในสถานะล็อก เป็นข้อบกพร่องชนิดที่หากปล่อยออกไปแล้วผู้เล่นจะส่งคำถามเข้ามา
ข้อความ "สันนิษฐานว่าพิมพ์ผิด" และ "สันนิษฐานว่าเติม 0 นำหน้า" ในฟิลด์ message คือส่วนที่ผมให้ตัวตรวจสอบไม่หยุดแค่ความล้มเหลวของความเป็นสมาชิก แต่เสนอค่า PK ที่ใกล้เคียงที่สุด (อิงระยะแก้ไข) มาด้วย ช่วยลดเวลาที่คนต้องไล่ตามว่า "ทำไมอันนี้ถึงพัง" แต่การสันนิษฐานนี้เป็นเพียงคำใบ้เท่านั้น ค่าที่จะแก้จริงให้คนเป็นผู้ตัดสิน
มาถึงตรงนี้คือการทำงานของการตรวจสอบหนึ่งตัว แต่ถึงการตรวจสอบจะจับการละเมิดได้ หากไม่มีใครเห็นก็ไม่มีความหมาย หัวใจอยู่ที่กระบวนการที่การละเมิดไปถึงผู้รับผิดชอบทันที ในโปรเจกต์ A กระบวนการนี้มี atom แยกต่างหากชื่อ integrity_check_clickup_notify รับผิดชอบ (ในเมตาดาตา JIT มีคะแนนอิมแพ็ก 294.93 ซึ่งเป็นหนึ่งใน atom ที่ได้รับการประเมินสูงสุดในกลุ่ม atom ตรวจสอบ — หมายความว่าการทำให้ความล้มเหลวด้านความสอดคล้องไปถึงคนนั้นสำคัญพอ ๆ กับตัวการตรวจสอบเอง)
cascade ทั้งหมดมีดังนี้ atom ตรวจสอบจะทำงานตามลำดับ และเมื่อมีการละเมิด P0 เกิดขึ้นในขั้นใด ก็จะไหลไปยัง atom แจ้งเตือน
flowchart TD
A[บันทึกชีต / ทริกเกอร์บิลด์] --> B[integrity_check_id_uniqueness
ตรวจสอบ PK ซ้ำ]
B -->|มีซ้ำ P0| F[บล็อกบิลด์]
B -->|ผ่าน| C[integrity_check_fk
ตรวจสอบความสอดคล้องตามแผนที่ FK]
C -->|FK พัง 0 รายการ| D[integrity_check_range
ตรวจสอบช่วงรางวัล·ค่าตัวเลข]
C -->|มี FK พัง P0| E[integrity_check_clickup_notify]
D -->|ละเมิดช่วง P1| E
D -->|ผ่าน| G[ตรวจสอบ PASS · ดำเนินบิลด์ต่อ]
E --> H{severity?}
H -->|P0| I[สร้างทาสก์ในเครื่องมือทำงานร่วมกัน
+ เมนชันผู้รับผิดชอบ + บล็อกบิลด์]
H -->|P1| J[คอมเมนต์ในเครื่องมือทำงานร่วมกัน + alert
บิลด์ดำเนินต่อ]
I --> F
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class A data;
class B,C,D,E,H,I,J code;
class G pass;
class F fail;
ใน cascade นี้มีการตัดสินใจเชิงออกแบบสองอย่างฝังอยู่
ข้อแรก การตรวจสอบ PK ซ้ำมาก่อนการตรวจสอบ FK การตรวจสอบ FK ตั้งอยู่บนสมมติฐานว่า PK ของชีต target นั้นไม่ซ้ำกัน หาก PK ซ้ำกัน คำถามที่ว่า "ค่านี้มีอยู่ในเซต PK หรือไม่" ก็ไร้ความหมายในตัวเอง ดังนั้นผมจึงระบุ related_atoms: integrity_check_id_uniqueness ไว้ใน atom ของ integrity_check_fk และตรึงลำดับไว้ใน cascade หากการตรวจสอบที่ขึ้นต่อกันล้มเหลว การตรวจสอบ FK จะถูกข้ามไป — เพราะรันไปก็ได้แต่ผลที่ผิดเท่านั้น
ข้อสอง ความเข้มของการแจ้งเตือนแยกตาม severity P0 (FK ที่พัง) จะสร้างทาสก์ในเครื่องมือทำงานร่วมกัน เมนชันผู้รับผิดชอบที่ลงทะเบียนไว้ในแผนที่ FK (หากเป็น reward_table ก็คือผู้รับผิดชอบรางวัล) และบล็อกบิลด์ ส่วน P1 (ค่าตัวเลขรางวัลหลุดจากช่วงที่แนะนำ — ไม่ใช่ว่าผิด แต่เป็นกรณีที่ต้องตรวจทาน) จะเหลือไว้แค่คอมเมนต์และ alert แล้วปล่อยให้บิลด์ผ่าน หากทำให้ทุกการละเมิดเป็นการบล็อกบิลด์ ในไม่ช้าคนก็จะเรียนรู้วิธีเพิกเฉยต่อการบล็อกบิลด์ การบล็อกใช้เฉพาะกับสิ่งที่ต้องห้ามจริง ๆ เท่านั้น
เนื้อหาของทาสก์ที่ถูกสร้างขึ้นจริงในเครื่องมือทำงานร่วมกัน คือรูปแบบที่หนึ่งรายการของ violation_list ถูกแปลงมาตามนั้นเลย
[P0] integrity_check_fk ละเมิด — บิลด์ถูกบล็อก
ชีต: quest_table | คอลัมน์: reward_id | แถว: 318
ค่า 'rwd_q381' ไม่มีใน reward_table
ตัวเลือกที่ใกล้เคียงที่สุด: 'rwd_q318' (ระยะแก้ไข 1)
ผู้รับผิดชอบ: @보상_담당 | ตรวจพบ: 2026-05-18 09:14 | บิลด์: nightly-0042
ตั้งแต่ผลการตรวจสอบจนไปถึงกล่องจดหมายของคน ไม่มีมือเข้าไปแทรกแม้แต่ครั้งเดียว ตรวจสอบ → จัดประเภท → สร้างทาสก์ → เมนชัน เป็นไปป์ไลน์เดียวกัน เหตุผลที่ทำได้เช่นนี้คือ violation_list เป็นรูปแบบเอาต์พุตมาตรฐาน ไม่ว่า atom ตรวจสอบตัวไหนจะจับได้ โครงสร้างเอาต์พุตก็เหมือนกัน atom แจ้งเตือนตัวเดียวจึงรับผลของการตรวจสอบทั้งหมดมาประมวลผลได้
เมื่อเปิดการตรวจสอบครั้งแรก จะมีการละเมิดเท็จออกมาแน่นอน speaker_id ในขั้นที่ 1 คือตัวอย่าง หากปล่อยทิ้งไว้ คนจะเรียนรู้ว่ารายงานการละเมิด "ยังไงส่วนใหญ่ก็เป็นเท็จ ไม่ดูก็ได้" — เป็นเส้นทางที่พบบ่อยที่สุดที่ทำให้ความเชื่อถือในตัวตรวจสอบพังทลาย
ในโปรเจกต์ A เราป้องกันเรื่องนี้ด้วยหลักการชื่อ human_review_attestation_evidence_mandatory เมื่อตัดสินว่าเป็นการละเมิดเท็จและทำการยกเว้น ต้องเก็บไว้เป็นหลักฐานว่าใคร·เมื่อใด·เพราะอะไรจึงตัดสินเช่นนั้น ในไฟล์แผนที่ FK (fk_map.reviewed.json) แต่ละรายการข้อยกเว้นจะมีสิ่งต่อไปนี้แนบมาด้วย
{
"source_sheet": "dialogue_table", "source_column": "speaker_id",
"excluded": true,
"review": {
"by": "이민수", "at": "2026-05-18",
"reason": "speaker_id มีค่าเป็น npc_id หรือลิเทอรัล 'player' ไม่เหมาะกับการตรวจสอบ FK เดี่ยว",
"follow_up": "พิจารณานำกลับมาใช้ด้วยการตรวจสอบแบบแยกสาขา หลังเพิ่มคอลัมน์ speaker_type"
}
}
หากไม่มีหลักฐานนี้ เมื่อข้อสงสัยที่ว่า "ทำไมคอลัมน์นี้ถึงไม่ตรวจสอบ" ผุดขึ้นมาอีกครั้งในภายหลัง ก็จะไม่มีหลักฐานให้ตอบได้ แล้วก็จะนำกลับเข้าการตรวจสอบอีก และเห็นการละเมิดเท็จหลายร้อยรายการอีก หลักฐานการตรวจสอบช่วยไม่ให้เกิดข้อถกเถียงเดิมซ้ำอีก
นี่คือขั้นตอนขั้นต่ำสำหรับผู้อ่านที่ต้องการนำการตรวจสอบ FK เข้ามาใช้กับชีตข้อมูลของตัวเอง
setup รวบรวมโฟลเดอร์ชีตข้อมูล และข้อมูลจำเพาะของคอลัมน์ (คอลัมน์ไหนเป็น PK และคอลัมน์ไหนเป็น FK) ไว้ที่เดียวกัน หากไม่มีข้อมูลจำเพาะ ก็สามารถเริ่มได้ด้วยกฎชื่อคอลัมน์ (*_id) เพียงอย่างเดียว
prompt ป้อนสิ่งต่อไปนี้ให้ตัวตรวจสอบ
อนุมานคอลัมน์ที่อาจเป็น FK จากชีตข้อมูลเหล่านี้ หากคอลัมน์
xxx_idชี้ไปยัง PK ชื่อเดียวกันในxxx_tableให้ถือว่าเป็น FK ส่งผลออกมาเป็น JSON{source_sheet, source_column, target_sheet, target_column, confidence}และสิ่งที่ฟันธงด้วยกฎไม่ได้ ให้แสดง confidence เป็น low และเขียนเหตุผลกำกับไว้
verify แผนที่ FK ที่ส่งออกมา ต้องให้คนตรวจสอบทีละบรรทัดเสมอ การอ้างอิงตัวเอง (prev_*) การปนของลิเทอรัล (เช่น 'player') และการอ้างอิงหลายรูป (คอลัมน์ที่ชี้ไปยังคนละชีตตามสถานการณ์) นั้นการอนุมานอัตโนมัติมักผิดบ่อย รันการตรวจสอบด้วยแผนที่ที่กรองแล้ว และจัดประเภทการละเมิดที่ออกมาในการรันครั้งแรกทีละรายการว่า "พังจริง / ละเมิดเท็จ" การละเมิดเท็จให้ทำการยกเว้น แต่เก็บเหตุผลไว้ในไฟล์
เมื่อผ่านสามขั้นตอนนี้ เหตุการณ์ที่พิมพ์ผิดตัวเดียวของเย็นวันศุกร์ทำให้บิลด์วันจันทร์พังก็จะหายไป การตรวจสอบจะจับพิมพ์ผิดนั้นได้ใน nightly ของเช้ามืดวันเสาร์ และก่อนเข้างานวันจันทร์ ทาสก์หนึ่งในเครื่องมือทำงานร่วมกันก็จะรอผู้รับผิดชอบอยู่
ฉบับย่อสำหรับคนเดียว แม้จะทำงานคนเดียวและไม่มีเครื่องมือทำงานร่วมกัน การตรวจสอบนี้ก็ยังมีความหมาย เขียนแผนที่ FK ด้วยมือสัก 10 บรรทัด แล้วรันฟังก์ชันไพทอนข้างต้นเพียงตัวเดียว ก็จับการอ้างอิงที่พังได้ การแจ้งเตือนใช้เอาต์พุตคอนโซลหรือไฟล์ข้อความก็พอ หัวใจไม่ได้อยู่ที่ช่องทางการแจ้งเตือน แต่อยู่ที่ตัวกระบวนการที่ว่า "เครื่องจับข้อผิดพลาดการอ้างอิงที่ตามนุษย์จับไม่ได้ แล้วทำให้ไปถึงคน"
ห้าทุ่มของคืนหนึ่ง งาน nightly เสียบการ์ดเข้ามาในเครื่องมือทำงานร่วมกันใบหนึ่ง หัวข้อคือ [integrity] D17 부합 측정 미수행 7일 경과 มันคือการแจ้งเตือนว่าการตัดสินใจหนึ่งซึ่งถูกนำมาใช้ครบหนึ่งสัปดาห์แล้ว ยังคงค้างอยู่ในบิลด์โดยที่ไม่มีใครตรวจสอบว่า "มันทำงานตามเจตนาจริงหรือไม่" ข้อมูลนั้นไม่มีปัญหา ทั้งรูปแบบของชีต ทั้ง FK ทั้ง enum ผ่านหมด แต่ตัวการตัดสินใจกลับยังไม่ได้รับการตรวจสอบ
ช่องว่างนี้คือจุดตั้งต้นของบทนี้ แม้ข้อมูลจะสมบูรณ์ไร้ที่ติ การตัดสินใจก็ยังผิดได้ และที่ทางที่จะจับความผิดนั้นได้อยู่คนละแห่งกับการตรวจสอบข้อมูล การแบ่งที่ทางนั้นออกเป็นสามชั้น แล้วระบุให้ชัดว่าในแต่ละชั้น AI ช่วยได้ถึงไหน และมนุษย์ลงตราประทับตรงจุดใด — นั่นคือเซนเซอร์ตรวจสอบการตัดสินใจแบบ 3 ชั้น
check cascade รันการตรวจสอบสี่ชนิดพร้อมกันในครั้งเดียว — doc-audit (ความสอดคล้องของเอกสาร), data-qa (คุณภาพข้อมูล), integrity (ความครบถ้วนสมบูรณ์), link (การอ้างอิงข้ามที่ขาด) เมื่อทั้งสี่อย่างนี้ผ่าน หมายความว่า "ข้อมูลไม่มีปัญหา" แต่บนข้อมูลที่ไม่มีปัญหานั้น การตัดสินใจที่ผิดก็ยังวางทับลงไปได้
แม้ชีตของรางวัลจะสมบูรณ์แบบในเชิงรูปแบบ แต่ถ้าตัวเลขนั้นก่อให้เกิดเงินเฟ้อ; แม้ FK จะไม่ซ้ำกัน แต่ถ้าสองเควสต์ครอบครอง NPC ตัวเดียวกันในเวลาเดียวกัน; แม้ voice จะสอดคล้องกัน แต่ถ้าการตั้งค่าความสัมพันธ์ของตัวละครสองตัวขัดแย้งกัน — การตรวจสอบข้อมูลก็ผ่านทั้งหมด แต่การตัดสินใจผิดทั้งหมด การตรวจสอบข้อมูลดูที่ "ช่องถูกเติมแล้วหรือยัง" ส่วนการตรวจสอบการตัดสินใจดูที่ "ค่านั้นเข้ากันกับการตัดสินใจอื่น กับข้อมูลอื่น และกับผู้ใช้จริงหรือไม่" หากเทียบกับงานบัญชี อย่างแรกคือการตรวจรูปแบบใบสำคัญ ส่วนอย่างหลังคือการตรวจสอบความสอดคล้องของงบการเงิน
ด้วยเหตุนี้ การตรวจสอบการตัดสินใจจึงมี เซนเซอร์ที่แยกออกมา ต่างหากจากการตรวจสอบข้อมูล ถ้าผูกรวมไว้ในการตรวจสอบเดียว ผลลัพธ์จะถูกบีบให้เหลือเพียงบรรทัดเดียวว่า "ผ่าน/ไม่ผ่าน" และเมื่อไม่ผ่านก็จะตีความได้คลุมเครือว่าเป็นปัญหาของข้อมูลหรือปัญหาของการตัดสินใจ เมื่อแยกออกจากกัน ความรับผิดชอบจะชัดเจน
หัวใจของเซนเซอร์ 3 ชั้นคือการแยก มิติ ของการตรวจสอบออกเป็นสามส่วน แต่ละชั้นต่างกันทั้งสิ่งที่มอง จังหวะเวลาที่ทำงาน และการแบ่งบทบาทระหว่าง AI กับมนุษย์
flowchart TD
D["การตัดสินใจ D17
defense_factor 1000 → 1500"] --> L1
subgraph L1["Layer 1 · การตัดสินใจ ↔ การตัดสินใจ"]
L1a["AI: ตรวจจับ scope ที่ซ้อนทับ + ตัดสินความขัดแย้งชั้นแรก"]
L1b["มนุษย์: ตรวจสอบเฉพาะคำตัดสิน 'ขัดแย้ง' + ลงตรา"]
L1a --> L1b
end
subgraph L2["Layer 2 · การตัดสินใจ ↔ ข้อมูล"]
L2a["AI: ดึงข้อมูลที่ได้รับผลกระทบ + จำลอง + กฎ ±%"]
L2b["มนุษย์: ตีความช่องว่างระหว่างเจตนากับการวัดผล"]
L2a --> L2b
end
subgraph L3["Layer 3 · การตัดสินใจ ↔ ผู้ใช้"]
L3a["AI: จัดหมวดฟีดแบ็ก + วิเคราะห์อารมณ์"]
L3b["มนุษย์: เทียบตัวอย่าง 100 รายการ + ประกาศความสอดคล้องขั้นสุดท้าย"]
L3a --> L3b
end
L1 --> L2 --> L3 --> CARD
CARD["การ์ดการตัดสินใจ D17
ผลตรวจสอบ 3 ชั้น + หลักฐานการตรวจสอบ"]
CARD --> ATT["human_review_attestation
บังคับแนบผู้ตรวจสอบ·เวลา·หลักฐาน"]
style L1 fill:#e8f0ff,stroke:#4a72c0
style L2 fill:#e8f7ed,stroke:#3a9a5a
style L3 fill:#fff3e0,stroke:#d08a2a
style ATT fill:#fde8e8,stroke:#c04a4a
ทั้งสามชั้น ตราประทับสุดท้ายเป็นมนุษย์ที่ลง — ตราประทับนั้นคือหลักฐานที่ atom human_review_attestation_evidence_mandatory บังคับไว้ ว่าแต่ละชั้นมองอะไร และใครลงตราตรงไหน จะดูทีละชั้นด้านล่างนี้
นี่คือชั้นที่ตรวจว่าการตัดสินใจใหม่ชนกับการตัดสินใจเดิมหรือไม่ จำนวนคู่ของการตัดสินใจเพิ่มขึ้นตามกำลังสองของจำนวนการตัดสินใจ ดังนั้นถ้ามี 200 การตัดสินใจ ก็จะได้ราว 20,000 คู่ มนุษย์ดูด้วยมือทั้งหมดไม่ไหว ด้วยเหตุนี้ AI จึงรันตัวกรองชั้นแรกให้
# decision_conflict_check.py — เซนเซอร์ Layer 1
def check_new_decision(new_decision, existing_decisions):
conflicts = []
for existing in existing_decisions:
if has_overlap(new_decision.scope, existing.scope): # ชั้นแรกเชิงกลไก: อินเตอร์เซกชันของ scope
verdict = llm_judge(new_decision, existing) # AI ชั้นสอง: ขัดแย้ง/เสริม/ไม่เกี่ยว
if verdict.label == "모순":
conflicts.append({
"with": existing.id,
"label": verdict.label,
"reason": verdict.reason,
"needs_human_review": True, # ธงให้มนุษย์ตรวจสอบ
})
return conflicts
has_overlap คือตัวกรองชั้นแรกเชิงกลไก scope เป็นพื้นที่ของ atom (combat·narrative·economy ฯลฯ) จึงคำนวณอินเตอร์เซกชันได้แม่นยำ คู่ที่ไม่ซ้อนทับกันจะถูกทิ้งทันที โดยทั่วไป 20,000 คู่จะลดเหลือราวหลายสิบคู่ และมีเพียงหลายสิบคู่นั้นเท่านั้นที่ AI ดู
ตรงนี้จะแสดงให้เห็นจนจบว่าจริง ๆ แล้วมีอะไรเดินเข้าออกบ้าง โดยไม่สรุปย่อ และคงผลลัพธ์ที่กำกวมไว้ตามเดิม
① พรอมต์ที่มนุษย์ส่ง (ฉบับเต็ม)
โปรดตัดสินว่าการตัดสินใจสองข้อต่อไปนี้ขัดแย้งกันหรือไม่
[การตัดสินใจ A] (2026-03-XX) รวมคูลดาวน์รวม (global cooldown) ของการต่อสู้ทุกสกิลเป็น 0.5 วินาที
[การตัดสินใจ B] (2026-04-XX) สกิลสายฟื้นฟูได้รับการยกเว้นไม่ใช้คูลดาวน์รวม (global cooldown)
รูปแบบคำตัดสิน (ใช้รูปแบบนี้เท่านั้น ห้ามพูดอย่างอื่น):
- label: ขัดแย้ง | เสริม | ไม่เกี่ยว
- reason: หนึ่งบรรทัด
- confidence: 0.0~1.0
อย่าเดา หากหลักฐานอ่อน ให้ลด confidence ลง
② ผลลัพธ์ดิบจาก Claude (ไม่ผ่านการแต่ง)
- label: เสริม
- reason: การตัดสินใจ B มีโครงสร้างเป็นการเพิ่มข้อยกเว้นที่ระบุชัดเจนว่า "สายฟื้นฟู" ให้กับกฎรวม 0.5 วินาทีที่การตัดสินใจ A กำหนดไว้ จึงเป็นการจำกัดให้แคบลงโดยไม่พลิกกฎ
- confidence: 0.74
③ การตรวจสอบ / การปฏิเสธของมนุษย์
คำตัดสินคือ "เสริม" ด้วย confidence 0.74 ตามกฎแล้ว 'เสริม·ไม่เกี่ยว' จะผ่านอัตโนมัติ แต่ถ้า confidence ต่ำกว่า 0.8 มนุษย์ต้องตรวจสอบอย่างน้อยหนึ่งบรรทัด ผู้ตรวจสอบซึ่งเป็นสมาชิกทีม A อ่านต้นฉบับของการตัดสินใจ B อีกครั้งและพบสิ่งหนึ่ง — การตัดสินใจ B มีเงื่อนไขแนบมาว่า "แต่สกิลฟื้นฟูก็มีคูลดาวน์ของตัวเอง 1.2 วินาที" AI ได้รับเพียงบรรทัดที่สรุปย่อมาแล้ว จึงไม่ได้รับเงื่อนไขนี้เป็นอินพุต สมาชิกทีม A จึงดำเนินการแบบ ยอมรับแต่เสริม คำตัดสินของ AI และบันทึกไว้ว่า "อินพุตที่ AI เห็นนั้นไม่ครบถ้วน"
④ การร้องขอใหม่ (หลังเสริมอินพุต)
[เงื่อนไขเพิ่มเติม] การตัดสินใจ B มีเงื่อนไขแนบมาว่า "สกิลฟื้นฟูได้รับการยกเว้นคูลดาวน์รวม
แต่มีคูลดาวน์ของตัวเอง 1.2 วินาที" กรุณาตัดสินใหม่โดยรวมเงื่อนไขนี้เข้าไปด้วย
- label: เสริม
- reason: คูลดาวน์ของตัวเอง 1.2 วินาทียาวกว่าคูลดาวน์รวม 0.5 วินาที ดังนั้นแม้สกิลฟื้นฟูจะได้รับ
การยกเว้น กลับยิ่งมีช่วงห่างที่ยาวกว่า ไม่ขัดกับเจตนาของ A (ป้องกันการสแปม) จึงเสริมกันชัดเจนขึ้น
- confidence: 0.91
คำตัดสินยังคงเป็น "เสริม" เหมือนเดิม แต่หลักฐานแน่นขึ้นและ confidence เพิ่มจาก 0.74 เป็น 0.91 สมาชิกทีม A ลงตราประทับ ณ จุดนี้ หัวใจไม่ได้อยู่ที่ผลลัพธ์ แต่อยู่ที่ การบันทึกกระบวนการ — คำตัดสินชั้นแรกของ AI, การที่มนุษย์พบอินพุตที่ขาดหาย, การร้องขอใหม่หลังเสริม, และการตรวจสอบขั้นสุดท้าย สี่ขั้นตอนนี้จะถูกป้อนเข้าช่องหลักฐาน Layer 1 ของการ์ดการตัดสินใจตามนั้นเลย
หลักการของบันทึกเซสชันนี้มีเพียงข้อเดียว แม้แต่คำตัดสิน 'เสริม·ไม่เกี่ยว' ของ AI ก็จะไม่ปล่อยให้ผ่านโดยไม่มีเงื่อนไข ไม่ใช่ว่า AI ผิด แต่ข้อมูลที่ AI ได้รับนั้นไม่ครบถ้วน และผู้ที่จะพบสิ่งนั้นคือมนุษย์ที่รู้ต้นฉบับของการตัดสินใจ
จังหวะการตรวจสอบมีสามจุด คือ ทันทีเมื่อเพิ่มการตัดสินใจใหม่ + alert, ตรวจสอบก่อนเลื่อนขั้นเมื่อ pending atom เลื่อนขั้น, และตรวจซ้ำทุกคู่ใน nightly
นี่คือชั้นที่วัดว่าการตัดสินใจสะท้อนลงในข้อมูลอย่างไร และสอดคล้องกับเจตนาหรือไม่ เป็นชั้นที่ทำให้เป็นอัตโนมัติได้ง่ายที่สุดและแม่นยำที่สุด หากมีตัวจำลอง (simulator) และชีตข้อมูลอยู่แล้ว ก็เพียงใส่กฎการตรวจสอบเข้าไปก็พอ
หากยกการตัดสินใจ D17 (defense_factor 1000→1500) เป็นตัวอย่าง เซนเซอร์จะดึงชีต CombatBalance ผลการจำลองอัตโนมัติ และข้อมูลตัวละครที่ได้รับผลกระทบ มาโดยอัตโนมัติ แล้วเปรียบเทียบเจตนา (อัตราการรอดของแทงค์ +49%) กับผลวัด (จำลอง +52%) กฎตัดสินความสอดคล้องเป็นเชิงปริมาณ
| ส่วนต่างของผลวัดเทียบกับเจตนา | การจัดการ | ใคร |
|---|---|---|
| ภายใน ±10% | สอดคล้อง (ผ่านอัตโนมัติ) | AI |
| ±10\~25% | alert · ทบทวนใหม่ | มนุษย์ตีความ |
| เกิน ±25% | ละเมิด · มีหน้าที่ทบทวนการตัดสินใจใหม่ | มนุษย์ตัดสิน |
ตรงนี้ บทบาทของมนุษย์ไม่ใช่ "AI บอกว่าสอดคล้องก็เลยผ่าน" การตีความช่วง alert และช่วงละเมิด ต่างหากที่เป็นงานของมนุษย์ การจำลองของ D17 ได้ +52% ซึ่งอยู่ภายใน ±10% จึงสอดคล้องอัตโนมัติ แต่การจำลองเดียวกันนั้นพ่นผลข้างเคียงออกมาหนึ่งอย่าง — ตัวละครไฮบริด K_021 แข็งแกร่งขึ้น +28% นอกเหนือเจตนา เนื่องจากไม่ใช่เจตนาโดยตรงของ D17 มันจึงไม่ติดกฎความสอดคล้อง การจับช่วงที่ "กฎผ่าน แต่ในสายตามนุษย์คืออุบัติเหตุ" นี้ คือเหตุผลที่มนุษย์ยังต้องมีอยู่ใน Layer 2
อัตราการทำอัตโนมัติของชั้นนี้สูงที่สุดที่ราว 95% ที่ยังเหลืออีก 5% ก็เพราะการตีความนี้นี่เอง การที่ตัวเลขผ่านกฎ กับการที่ตัวเลขนั้นถูกต้องสำหรับเกม เป็นคำถามคนละข้อกัน
ยากที่สุดในสามชั้น เป็นการดูว่าการตัดสินใจส่งผลต่อผู้ใช้จริงตามเจตนาหรือไม่ โดยรับอินพุตเป็นตัวชี้วัดที่วัดได้จริงหลังปล่อยบิลด์ราว 1\~2 สัปดาห์ (เวลารอดเฉลี่ยของแทงค์, อัตราชนะ PvP 5:5 ที่มีแทงค์รวมอยู่) และฟีดแบ็กภาษาธรรมชาติ (ฟอรัม·SNS)
จุดเด่นของชั้นนี้คือ ฟีดแบ็กภาษาธรรมชาติกลายมาเป็นอินพุตของการตรวจสอบ AI จะจัดหมวดและให้คะแนนอารมณ์ของฟอรัมราว 200 รายการ และ SNS ราว 1,500 รายการ
[การจัดหมวดฟีดแบ็กโดย AI — เก็บรวบรวม 1 สัปดาห์ที่เกี่ยวกับแทงค์]
เชิงบวก 62% เชิงลบ 23% ("แทงค์แข็งเกินไป" เป็นเสียงส่วนใหญ่) ไม่เกี่ยว 15%
ถ้าหยุดตรงนี้ก็เป็นกับดัก การจัดหมวดอารมณ์ของ AI จะแม่นยำลดลงเมื่อภาษาเกาหลีกับภาษาอังกฤษปนกัน ("탱커 강해졌다 ㅋㅋ" เป็นเชิงบวกหรือเป็นการประชด คำตัดสินจะแกว่ง) ด้วยเหตุนี้ จึงกำหนดเป็นกฎการดำเนินงานว่า ทุกไตรมาส มนุษย์จะจัดหมวดตัวอย่าง 100 รายการด้วยตนเองเพื่อเทียบกับผลของ AI ถ้าค่าความคลาดเคลื่อนในการเทียบสูงเกินเกณฑ์ ก็จะไม่เชื่อถือการจัดหมวดในไตรมาสนั้น และมนุษย์จะจัดหมวดใหม่ทั้งหมด
การประกาศความสอดคล้องขั้นสุดท้ายเป็นหน้าที่ของมนุษย์ กรณีของ D17 ผลวัดจริง +44% (คาดการณ์จากจำลอง +52%, คลาดเคลื่อน 8% — อยู่ในช่วงปกติ) และฟีดแบ็กเป็นเชิงบวกเหนือกว่า AI เป็นผู้สรุปและเสนออินพุตว่า "เชิงบวกเหนือกว่า + อยู่ในช่วงเจตนา" ขึ้นมา แต่ ผู้ที่ลงตราว่าสอดคล้องคือมนุษย์ อัตราการทำอัตโนมัติราว 70% มนุษย์ 30% เฉพาะชั้นนี้เท่านั้นที่การทำอัตโนมัติเต็มรูปแบบเป็นไปไม่ได้ตั้งแต่ต้น เพราะเครื่องจักรไม่อาจตัดสินความหมายของผู้ใช้ได้จนถึงที่สุด
หากตราประทับสุดท้ายของทั้งสามชั้นคือมนุษย์ แล้วถ้าไม่มี หลักฐานว่าตราประทับนั้นถูกลงจริง ระบบทั้งระบบก็พังลง จะป้องกันอย่างไรในกรณีที่พูดว่าตรวจสอบแล้วทั้งที่ไม่ได้ตรวจ ในโปรเจกต์ A นั้น atom human_review_attestation_evidence_mandatory บังคับเรื่องนี้
กฎของ atom นี้เรียบง่ายและไม่มีการประนีประนอม ไม่ว่าชั้นใดของการ์ดการตัดสินใจ หากเกิด 'คำตัดสินของ AI → การตรวจสอบโดยมนุษย์' ขึ้น จะต้องแนบรหัสผู้ตรวจสอบ, เวลาที่ตรวจสอบ, และหลักฐานการตรวจสอบ (อย่างน้อยหนึ่งในจำนวน บันทึกเสริม, เหตุผลการปฏิเสธ, ผลการเทียบตัวอย่าง) เข้ากับการ์ด หากช่องหลักฐานว่างเปล่า การ์ดนั้นจะไม่สามารถเลื่อนขั้นเป็น "ตรวจสอบเสร็จสมบูรณ์" ได้
หากหลักฐานว่างเปล่า atom integrity_check_clickup_notify จะทำงาน เมื่อตรวจพบความล้มเหลวด้านความสอดคล้อง — ในที่นี้คือ "มีตราประทับการตรวจสอบ แต่ไม่มีหลักฐาน" — มันจะสร้างการ์ดในเครื่องมือทำงานร่วมกันทันที การ์ดห้าทุ่มในฉากแรกของบทนี้ก็คือกลไกนี้นี่เอง
atom สองตัวนี้จับคู่กันสร้าง "การตรวจสอบของการตรวจสอบ" ขึ้น เซนเซอร์ 3 ชั้นตรวจสอบการตัดสินใจ, attestation atom ตรวจสอบว่ามนุษย์ทำการตรวจสอบนั้นจริงหรือไม่, และ notify atom จับการขาดหลักฐานแล้วแจ้งเตือน แม้การช่วยเหลือของ AI จะกว้างขวาง แต่ ช่องสุดท้ายของความรับผิดชอบจะถูกเติมด้วยชื่อของมนุษย์ที่ทิ้งหลักฐานไว้
หน่วยที่ผลของทั้งสามชั้นและหลักฐานการตรวจสอบมารวมกันคือการ์ดการตัดสินใจ การ์ดหนึ่งใบคือหน่วยที่สมบูรณ์ของการตัดสินใจหนึ่งเรื่อง และไหลไปเป็นอินพุตของการทบทวนรายไตรมาส ด้านล่างคือโครงสร้างของการ์ด D17
บรรทัดสีแดงคือหัวใจ หากแถว "หลักฐาน:" ของแต่ละชั้นว่างเปล่า attestation atom จะขัดขวางการเลื่อนขั้นของการ์ด และ notify atom จะแจ้งไปยังเครื่องมือทำงานร่วมกัน หกเดือนต่อมา หากใครสักคนถามว่า "ทำไมตั้ง defense_factor เป็น 1500" การ์ดใบเดียวนี้จะตอบได้หมดทั้งเจตนา การวัดผล ผลวัดจริง ไปจนถึงผู้ตรวจสอบ การ์ดการตัดสินใจทำงานบนกระแสเมตาดาตาเดียวกันกับ atom ติดตามการตัดสินใจในส่วนที่ 18
ทั้งสามชั้นมีระดับการทำอัตโนมัติต่างกัน (ราว 80%·95%·70% ตามลำดับ ตามที่เห็นในหัวข้อก่อนหน้า) ทั้งสามต่างเป็นกึ่งอัตโนมัติและตราประทับสุดท้ายทั้งสามเป็นมนุษย์ แต่ปริมาณงานของมนุษย์โดยรวมลดลงมากกว่า 80%
การนำมาใช้เริ่มจาก Layer 2 หากมีตัวจำลองและชีตข้อมูลอยู่แล้ว ก็เพียงเพิ่มกฎการตรวจสอบเข้าไป ใช้เวลาราว 1\~2 เดือนก็เห็นผล ต่อมาคือ Layer 1 (โครงสร้างพื้นฐานน้อยแต่ผลใหญ่, เพิ่มอีกราว 1 เดือน) และสุดท้ายคือ Layer 3 (โครงสร้างพื้นฐานใหญ่ที่สุดและผลก็ใหญ่ด้วย, เพิ่มอีกราว 2\~3 เดือน) การพยายามติด Layer 3 ตั้งแต่ต้นแล้วล่มกลางทางคือความล้มเหลวที่พบบ่อย
เกี่ยวกับการระบุตัวเลข: อัตราการทำอัตโนมัติข้างต้นและสัดส่วนผลลัพธ์ด้านล่างเป็น การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ซึ่งอ้างอิงจากการสังเกตการดำเนินงานในโปรเจกต์ของผู้เขียน ไม่ใช่ค่าที่วัดอย่างละเอียด จึงควรอ่านเป็นทิศทางและสัดส่วนคร่าว ๆ ส่วนเกณฑ์ ±10%/±25% ของกฎความสอดคล้องเป็นกฎการดำเนินงานจริง และชื่อ atom (
integrity_check_clickup_notify,human_review_attestation_evidence_mandatory) เป็น atom ที่มีอยู่จริง
ความเปลี่ยนแปลงก่อนและหลังการนำมาใช้ หากสรุปเป็นทิศทางก็เป็นเช่นนี้ อุบัติเหตุการตัดสินใจขัดแย้งต่อไตรมาสลดจากหลายรายการเหลือเกือบ 0 รายการ, อัตราการวัดความสอดคล้อง 1 สัปดาห์หลังการตัดสินใจเพิ่มจากส่วนน้อยเป็นเกือบทั้งหมด, อัตราการพบผลข้างเคียงก่อนเกิดอุบัติเหตุเพิ่มจากต่ำกว่าครึ่งเป็นเกือบทั้งหมด ความเปลี่ยนแปลงที่มีความหมายที่สุดคือความสามารถในการติดตาม (traceability) — สัดส่วนที่สามารถย้อนกลับไปทบทวนภูมิหลังของการตัดสินใจหลังจากผ่านไปนานแล้ว เปลี่ยนจากส่วนน้อยเป็นเกือบทั้งหมด เพราะการ์ดการตัดสินใจเก็บรักษาประวัติการตัดสินใจของเกมไว้
| รูปแบบ | ทางแก้ |
|---|---|
| ใช้งานเฉพาะ Layer 1 (ตรวจเฉพาะความขัดแย้ง) | เพิ่ม Layer 2·3 เพื่อเติมมิติ |
| นำ Layer 3 มาใช้ตั้งแต่ต้น | เริ่มจาก Layer 2 ไล่ตามลำดับโครงสร้างพื้นฐานเล็กไปใหญ่ |
| ยอมรับคำตัดสิน 'เสริม·ไม่เกี่ยว' ของ AI อย่างไม่วิพากษ์ | เกณฑ์ confidence + การตรวจสอบตัวอย่างโดยมนุษย์ |
| ลงเฉพาะตราประทับการตรวจสอบแต่ไม่แนบหลักฐาน | attestation atom ขัดขวางการเลื่อนขั้น |
| เพิกเฉยต่อการแจ้งเตือนหลักฐานที่ขาด | ถือว่าการ์ดที่ notify atom สร้างในเครื่องมือทำงานร่วมกันยังไม่เสร็จ |
| เชื่อการจัดหมวดฟีดแบ็กผู้ใช้โดย AI อย่างสนิทใจ | เทียบตัวอย่างรายไตรมาส 100 รายการโดยมนุษย์ |
setup. รวบรวมบันทึกการตัดสินใจไว้ในไฟล์เดียว (รหัสการตัดสินใจ·scope·เจตนา·วันที่นำมาใช้) กำหนด scope ให้ตายตัวเป็น enum เช่น combat·narrative·economy หากไม่มีตัวจำลอง Layer 2 จะเริ่มจาก "เทียบชีตข้อมูลที่เกี่ยวข้องด้วยมือ" ก็ได้
prompt. ทุกครั้งที่มีการตัดสินใจใหม่ ให้ถาม AI ทีละคู่เทียบกับการตัดสินใจเดิม ตรึงรูปแบบไว้
โปรดตัดสินว่าการตัดสินใจสองข้อต่อไปนี้ขัดแย้งกันหรือไม่
[การตัดสินใจ A] ...
[การตัดสินใจ B] ...
ออกเฉพาะรูปแบบ: label(ขัดแย้ง|เสริม|ไม่เกี่ยว) / reason หนึ่งบรรทัด / confidence 0.0~1.0
ห้ามเดา หากหลักฐานอ่อน ให้ลด confidence
verify. คำตัดสิน 'ขัดแย้ง' และคำตัดสินที่ confidence ต่ำกว่า 0.8 มนุษย์ต้องอ่านต้นฉบับของการตัดสินใจอีกครั้งและตรวจสอบ เมื่อตรวจสอบแล้ว ต้องทิ้ง ชื่อผู้ตรวจสอบ·เวลา·บันทึก (อย่างใดอย่างหนึ่งใน เสริม/ปฏิเสธ/เทียบ) ไว้ในการ์ดการตัดสินใจเสมอ หากช่องหลักฐานว่างเปล่า อย่าเลื่อนการ์ดนั้นเป็น "ตรวจสอบเสร็จสมบูรณ์" — บรรทัดนี้คือ attestation atom ฉบับคนเดียว แม้จะดำเนินงานคนเดียว ก็จงทิ้งหลักฐานไว้เพื่อตัวเราในอีกหกเดือนข้างหน้า
เช้าวันจันทร์ เวลา 9 นาฬิกา 12 นาที การตรวจสอบ cascade ครั้งแรกของสัปดาห์ที่อัลฟาบิลด์เพิ่งขึ้นไปนั้นจบลง เมื่อ check รันตัวตรวจสอบทั้งสี่ชนิด (doc-audit·data-qa·integrity·link, 10.2) รวดเดียวแล้วหยุด ตัวเลขที่ปรากฏบนคอนโซลเป็นแบบนี้ รายการที่อาจละเมิด 47 รายการ ในจำนวนนั้นมี P0 กี่รายการ ต้องดูอะไรก่อน ใครต้องลงมือแก้ — ไม่มีบรรทัดใดในทั้ง 47 บรรทัดนั้นเขียนเอาไว้เลย
ตัวตรวจสอบรู้แค่ข้อเท็จจริงว่า "ผิด" แต่ตัดสินไม่ได้ว่า "สิ่งนี้กั้นการเปิดตัวไหม หรือดูสัปดาห์หน้าก็ยังได้" คอขวดที่แท้จริงในช่วงท้ายของอัลฟาไม่ใช่เพราะตัวตรวจสอบไม่พอ แต่อยู่ตรงที่คนต้องนั่งจัดหมวดหมู่ 47 บรรทัดที่ตัวตรวจสอบพ่นออกมาจนหมดเช้าไปทั้งเช้า บทนี้ยกหนึ่งรอบของวงจรจริง (worked cycle) มาทั้งดุ้น — รอบที่ LLM จัดหมวดหมู่ 47 บรรทัดนั้นด้วยภาษาธรรมชาติ แล้วคนรับการจัดหมวดนั้นมาจัดลำดับความสำคัญ
ในหัวข้อ 10.1 เราสร้าง atom สำหรับการตรวจสอบราว 30 กว่าชนิด และในหัวข้อ 10.2 เราสร้างโครงสร้างที่กรองการตัดสินใจผ่านเซนเซอร์ 3 ชั้น (3-layer) สิ่งที่สองหัวข้อนั้นสร้างขึ้นมาคือ ล็อก ล็อกไม่ใช่การตัดสินใจ ระหว่างล็อกกับการตัดสินใจมีช่องว่างที่คนเคยต้องเอามือมาถม
เหตุผลที่ช่องว่างนี้แพงในช่วงท้ายของอัลฟานั้นเรียบง่าย ตัวตรวจสอบรันได้หลายสิบครั้งต่อชั่วโมง แต่งานที่คนต้องอ่าน 47 บรรทัดแล้วจัดหมวดว่า "q_142 เป็นทางตันจึงกั้นการเปิดตัว ส่วน voice_lint 412 รอให้นักเขียนตัดสิน" นั้นต้องทำใหม่ทุกครั้ง การโยนแรงงานจัดหมวดนั้นให้โมเดลภาษาธรรมชาติคือจุดเริ่มต้นของ Gap Report
ด้านล่างคือเซสชันจริงในเช้าวันจันทร์นั้น ที่นำล็อกดิบของการตรวจสอบ cascade มาวางลง Claude ตรง ๆ แล้วขอให้จัดหมวดหมู่ ผู้เขียนยกมาโดยไม่สรุปย่อ และคงทั้งจุดที่โมเดลชี้ผิดและจุดที่คนปฏิเสธไว้ตามเดิม นี่คือกระดูกสันหลังของบทนี้
ด้านล่างคือรายการที่อาจละเมิดที่การตรวจสอบ cascade รายสัปดาห์ของอัลฟาบิลด์ (doc-audit/data-qa/integrity/link) พ่นออกมารวมกัน ช่วยจัดหมวดหมู่ให้เพื่อใช้ในประชุมรายสัปดาห์ที
จัดแต่ละรายการเป็น P0 (กั้นการเปิดตัว)/P1 (ตรวจทบทวน)/P2 (เฝ้าสังเกต) พร้อมเหตุผลหนึ่งบรรทัดต่อรายการ — ถ้าเป็นการเดาให้เขียนว่า "ประมาณการ" ด้วย ส่วน severity อย่าฟันธงเอง ให้ "เสนอ" เท่านั้น การยืนยันเป็นหน้าที่ผม
อันที่ออกมาจากรากเดียวกันก็มัดรวมให้ และช่วยแนะนำสายงานที่รับผิดชอบ (เลเวล/เนื้อเรื่อง/บาลานซ์/UI/ดาตา) ด้วย อันที่ตัดสินไม่ได้ ให้แยกออกมาตรง ๆ ว่า "จัดหมวดไม่ได้ ต้องให้คนยืนยัน"
[ล็อกดิบ]
INTEGRITY q_142 quest_graph: node 'end_021' has no outgoing edge (dead-end), zone=dungeon_021
INTEGRITY q_158 quest_graph: reward node references item_id 'it_9920' not in item table
DATA-QA reward_curve dungeon_017 reward_exp delta +35.2% vs sibling median (threshold 25%)
DATA-QA reward_curve dungeon_017 reward_gold delta +31.0% vs sibling median
DOC-AUDIT voice_lint dialogue_id_412 contains banned term (refgame slang leak)
DOC-AUDIT voice_lint dialogue_id_511 tone drift: formality score 0.31 (npc 'elder' expects >0.7)
DOC-AUDIT voice_lint dialogue_id_512 tone drift: formality score 0.29
DOC-AUDIT voice_lint dialogue_id_513 tone drift: formality score 0.33
DATA-QA loc_overflow ui_btn_enhance EN string 24 chars > slot 18 (overflow 6)
DATA-QA loc_overflow ui_btn_enhance TH string overflow 11
LINK doc_link gdd_dungeon_021.md -> [reward_table] target moved (404)
INTEGRITY k_skill skill 'k_017_charge' cooldown 0.0 (suspicious, min expected 0.5)
... (ทั้งหมด 47 บรรทัด ที่เหลือเป็นแพทเทิร์นแบบเดียวกัน — แนบมาครบโดยไม่ตัดทอน)
จัดหมวดหมู่ล็อกดิบเรียบร้อยแล้ว severity เป็น "ข้อเสนอ" ของผม การยืนยันขั้นสุดท้ายต้องให้คนทำ
## รายการระดับ P0 (อาจกั้นการเปิดตัว — ต้องให้คนยืนยัน)
1. [q_142] quest_graph dead-end (dungeon_021)
เหตุผล: โหนดสิ้นสุดที่ไม่มีเอดจ์ทางออก → ผู้เล่นติดค้างระหว่างทำเควสต์
ทางตันคือเดินต่อไม่ได้ จึงเป็นรายการระดับ P0 (เหตุผลยืนยันชัดเจน)
แนะนำผู้รับผิดชอบ: เลเวล/เนื้อเรื่อง
2. [q_158] reward node อ้างถึง item_id 'it_9920' ที่ไม่มีอยู่
เหตุผล: ตอนจ่ายรางวัลจะเกิดการอ้างอิง NULL → อาจแครชหรือไม่จ่ายรางวัล
รายการระดับ P0 แต่ item_id อาจยังไม่ได้กรอก (อยู่ระหว่างทำงาน) ก็ได้
ดังนั้นว่าเป็น "ดาตายังไม่เสร็จ" หรือ "พิมพ์ผิด" ต้องให้คนยืนยัน
แนะนำผู้รับผิดชอบ: ดาตา
3. [voice_lint dialogue_id_412] คำต้องห้าม (คำเฉพาะของเกมอื่น refgame หลุดมา)
เหตุผล: ถ้าคำเฉพาะของเกมอื่นค้างอยู่ในบทพูด จะมีความเสี่ยงทางกฎหมาย·แบรนด์
รายการระดับ P0 (คำต้องห้ามมักกั้นโดยไม่มีเงื่อนไข)
แนะนำผู้รับผิดชอบ: เนื้อเรื่อง
## รายการระดับ P1 (ตัดสินหลังตรวจทบทวน)
4. [reward_curve dungeon_017] exp +35.2% / gold +31.0% (เกินค่ามัธยฐานของพี่น้องทั้งคู่)
※ มัดสองบรรทัดเป็น 1 รายการ — ความผิดปกติของเส้นโค้งรางวัลในดันเจี้ยนเดียวกัน
เหตุผล: เกินค่าเกณฑ์ (25%) ก็จริง แต่อาจเป็น "โบนัสดันเจี้ยนยากตามตั้งใจ" ก็ได้
ว่าตั้งใจบาลานซ์หรือพลาด ตัดสินจากดาตาอย่างเดียวไม่ได้ → P1
แนะนำผู้รับผิดชอบ: บาลานซ์
5. [voice_lint 511·512·513] tone drift 3 รายการ (formality 0.29~0.33, NPC elder คาด >0.7)
※ มัดสามบรรทัดเป็น 1 รายการ — บทพูดต่อเนื่องของ NPC 'elder' ตัวเดียวกันที่หลุดโทน
เหตุผล: NPC ตัวเดียวกันหลุดเป็นภาษาไม่สุภาพต่อเนื่อง → น่าจะเป็นงาน
ที่นักเขียนคนเดียวทำในเซสชันเดียว แนะนำให้มัดดูรวม ไม่ใช่ทีละอัน
ประมาณการ: อาจเป็นการเปลี่ยนตัวละครตามตั้งใจ (ฉากโกรธ ฯลฯ) ก็ได้ จึงรอนักเขียนตัดสิน P1
แนะนำผู้รับผิดชอบ: เนื้อเรื่อง
6. [loc_overflow ui_btn_enhance] EN เกิน 6 ตัวอักษร / TH เกิน 11 ตัวอักษร
※ 2 บรรทัด 1 รายการ การล้นหลายภาษาของปุ่มเดียวกัน
เหตุผล: ตัวอักษรขาดเป็นความเสียหายต่อ UX แต่ไม่ใช่แครช ต้องขยายสล็อตหรือย่อคำแปล P1
แนะนำผู้รับผิดชอบ: UI
## รายการระดับ P2 (เฝ้าสังเกต)
7. [doc_link gdd_dungeon_021.md -> reward_table 404]
เหตุผล: ลิงก์ภายในเอกสารเสีย ไม่กระทบเกมบิลด์ เป็นปัญหาความสอดคล้องของเอกสาร P2
แนะนำผู้รับผิดชอบ: ดาตา (เจ้าของเอกสาร)
## จัดหมวดไม่ได้ — ต้องให้คนยืนยัน
8. [k_017_charge cooldown 0.0]
เหตุผล: cooldown 0.0 จะเป็น "สกิลแบบพาสซีฟตามตั้งใจ" หรือ "ลืมกรอกค่า"
ถ้าไม่รู้เจตนาการออกแบบสกิลก็ตัดสินไม่ได้ ตัวตรวจสอบ integrity สงสัย
ค่าที่ต่ำกว่า 0.5 แต่ผมไม่รู้เจตนาการออกแบบของสกิลนี้
→ ผู้รับผิดชอบบาลานซ์ต้องยืนยันด้วยตนเอง
(ที่เหลืออีก 39 บรรทัดก็จัดเข้าหนึ่งใน 7 แพทเทิร์นข้างต้น — ในคำตอบนี้กางเฉพาะตัวแทน)
จากตรงนี้ไปคือหัวใจ ผู้เขียนไม่ได้รับการจัดหมวดของ LLM มาใช้ทั้งดุ้น นี่คือส่วนที่ผู้เขียนตรวจทบทวนเองก่อนประชุมแล้วลงปากกาแดง
it_9920 เป็นไอเทมที่ถูกลบไปเมื่อสองสัปดาห์ก่อน ไม่ใช่ยังไม่เสร็จ แต่เป็นการอ้างอิงที่ขาด ยืนยันกั้นการเปิดตัวลำพังการที่โมเดลบีบให้เหลือ 7 มัดนั้นก็ใหญ่แล้ว ถ้าคนนั่งจัดหมวด 47 บรรทัดตั้งแต่ต้นเองเช้าก็คงหายไป แต่ ในรายการระดับ P0 จำนวน 3 รายการ คนลดระดับ 1 รายการ (412) และในรายการระดับ P1 1 รายการ (158) คนเลื่อนระดับขึ้น การจัดหมวด 60% ถูก ส่วน 30% ที่แพงคนเป็นคนแก้ อัตราส่วนนี้คือเส้นแบ่งของ "LLM แปรรูป การตัดสินใจเป็นของคน" อย่างแม่นยำ
ดีแล้ว ในการจัดหมวดของคุณ ผมเปลี่ยนสองอัน
- q_158: ยืนยัน P0 (it_9920 เป็นไอเทมที่ถูกลบไปแล้ว เป็นการอ้างอิงที่ขาด)
- voice_lint_412: ลดเป็น P1 (อ้างถึงสำนวนเก่าตามตั้งใจ เพิ่มยกเว้นในพจนานุกรมคำต้องห้าม)
นำสองอันนี้ไปสะท้อนแล้วเรนเดอร์ Gap Report 1 หน้าสำหรับประชุมรายสัปดาห์เป็นมาร์กดาวน์ที เรียงลำดับ สรุป→P0→P1→P2→แนวโน้ม
ตัวเลขแนวโน้มผมจะให้เอง — สัปดาห์ที่แล้ว P0 5 รายการ, P1 22 รายการ, ผลบวกลวง 12%
โมเดลรับอินพุตนี้แล้วเอาต์พุต 1 หน้าตามรูปแบบรายงานในหัวข้อ §ด้านล่างนี้ออกมาตามเดิม สองบรรทัดที่คนแก้ถูกสะท้อนอย่างแม่นยำ และตัวเลขแนวโน้มก็ใช้ค่าที่คนให้มาตามเดิม (ไม่ได้กุขึ้นเอง) การไป-กลับนี้คือทั้งหมดที่ทำให้ Gap Report หนึ่งฉบับเกิดขึ้น
หากกลั่นทรานสคริปต์ข้างต้นออกมาเป็นขั้นตอน จะได้แบบนี้ จุดสำคัญคือจุดแยกที่หนาทั้งหมดอยู่ที่คน
flowchart TD
A[การตรวจสอบ cascade check
doc-audit+data-qa+integrity+link] --> B[ล็อกการละเมิดดิบ 47 บรรทัด]
B --> C{LLM จัดหมวดรอบแรก}
C -->|เสนอ severity| D[รายการระดับ P0]
C -->|เสนอ severity| E[รายการระดับ P1]
C -->|เสนอ severity| F[รายการระดับ P2]
C -->|ตัดสินไม่ได้| G[จัดหมวดไม่ได้
ต้องให้คนยืนยัน]
C -->|รากเดียวกัน| H[มัดรวมที่ซ้ำ]
D --> I{คนตรวจสอบ}
E --> I
F --> I
G --> I
I -->|รับมาใช้| J[ยืนยัน severity]
I -->|เลื่อน/ลด| K[คนเป็นคนแก้]
K --> J
J --> L[เรนเดอร์ Gap Report 1 หน้า]
L --> M[อินพุตประชุมรายสัปดาห์]
M --> N{ความสอดคล้องล้มเหลว?}
N -->|ใช่| O[integrity_check_clickup_notify
แจ้งเตือนผ่านเครื่องมือทำงานร่วมกันทันที]
N -->|ไม่ใช่| P[จัดผู้รับผิดชอบ·กำหนดส่ง]
style C fill:#e8f0fe
style I fill:#fef3e8
style O fill:#fde8e8
กล่องที่ LLM แตะมีแค่กล่องสีฟ้ากล่องเดียว severity ทุกตัวถูกยืนยันที่สีส้ม (คนตรวจสอบ) และที่สีแดงความสอดคล้องที่ล้มเหลวจะถูกส่งต่อไปยังเครื่องมือทำงานร่วมกันทันที การตรวจสอบ·การตัดสิน·การยืนยันทั้งหมดเป็นหน้าที่ของคนและ atom ส่วนโมเดลรับเฉพาะการจัดหมวดครั้งแรกเพียงครั้งเดียว
ที่ปลายของขั้นตอนการจัดหมวด มี atom ชื่อ integrity_check_clickup_notify (10.1) ติดอยู่ atom นี้แยกจากขั้นตอนการสร้างรายงาน — ในวินาทีที่การตรวจสอบความสอดคล้องล้มเหลว มันจะไม่รอประชุม แต่โยนการ์ดลงเครื่องมือทำงานร่วมกันทันที หาก Gap Report เป็นจังหวะรายสัปดาห์ atom นี้ก็เป็นการแทรก (interrupt) ที่ตัดเข้ามาทำลายจังหวะนั้น
การละเมิดที่อาจทำให้ตัวบิลด์พังได้ อย่าง q_158 (อ้างถึงไอเทมที่ถูกลบ) นั้นรอจนถึงประชุมวันจันทร์ไม่ได้ ในวินาทีที่ cascade จับมันได้ จะมี "สงสัย P0: q_158 อ้างอิงขาด" สร้างขึ้นอัตโนมัติในเครื่องมือทำงานร่วมกันและจัดให้ผู้รับผิดชอบดาตา Gap Report คือฉากหลังที่รวบรวมการแทรกเหล่านั้นกลับมาในหน่วยสัปดาห์แล้วแสดงเป็นแนวโน้มอีกครั้ง สองชั้นนี้ต้องหมุนไปด้วยกัน "เรื่องด่วนทันที ภาพรวมทั้งหมดรายสัปดาห์" สองจังหวะนี้จึงจะเข้ากัน
ข้อเท็จจริงที่ว่าคนตรวจสอบการจัดหมวดของ LLM แล้วนั้น ถ้าเหลือเป็นแค่คำพูดก็จะระเหยหายไป ดังนั้นในขั้นการตรวจสอบโดยมนุษย์จึงมี atom ชื่อ human_review_attestation_evidence_mandatory (10.2) คล้องอยู่ — การตรวจสอบโดยมนุษย์ต้องมีหลักฐานเป็นข้อบังคับ
ทรานสคริปต์ขั้นตอน ③ ข้างต้น — การตัดสินที่ลดระดับ 412 และเลื่อนระดับ 158 นั้น — จะถูกป้อนเข้าไปในส่วนท้ายของรายงาน เป็น ID ผู้ตรวจสอบ·ไทม์สแตมป์ และรายการ "รายการที่เปลี่ยน" ในไตรมาสหน้าหากมีใครถามว่า "ทำไม 412 ถึงออกไปในการเปิดตัว" บันทึกที่ว่า "ในการตรวจสอบ 2026-W21 ตัดสินว่าเป็นการอ้างถึงสำนวนเก่าตามตั้งใจ เพิ่มยกเว้นในพจนานุกรมคำต้องห้าม" จะเป็นคำตอบ ถ้าไม่มีสิ่งนี้ การจัดหมวดของ LLM ก็จะแยกไม่ออกจากเอาต์พุตอัตโนมัติที่ไม่เคยถูกตรวจสอบ
1 หน้าที่โมเดลเรนเดอร์ออกมาเป็นผลของการร้องขอใหม่ ④ มีหน้าตาแบบนี้ การจัดหมวดของทรานสคริปต์ข้างต้นไหลเข้ามาตามเดิม
# Alpha Gap Report — 2026-W21
## สรุป
- การตรวจสอบ cascade รายการที่อาจละเมิด 47 รายการ → จัดเป็น 7 มัด
- ยืนยัน P0 3 รายการ / P1 4 รายการ / P2 1 รายการ / จัดหมวดไม่ได้ 1 รายการ
- กั้นการเปิดตัว: q_142 (ทางตัน), q_158 (อ้างอิงขาด)
- การเปลี่ยนจากการตรวจสอบโดยมนุษย์: voice_412 ลดระดับ (P0→P1), q_158 เลื่อนระดับ (P1→P0)
## P0 — ลงมือทันที (คนยืนยัน)
| ID | การละเมิด | สายงาน | หมายเหตุ |
|---|---|---|---|
| q_142 | dungeon_021 ทางตัน | เลเวล/เนื้อเรื่อง | LLM·คน ตรงกัน |
| q_158 | อ้างถึง it_9920 ที่ถูกลบ | ดาตา | คนเลื่อนระดับ |
## P1 — ตัดสินหลังตรวจทบทวน
- reward_curve dungeon_017: exp+35%/gold+31% (บาลานซ์, รอยืนยันเจตนา)
- voice 511·512·513: elder หลุดโทน 3 รายการมัดรวม (เนื้อเรื่อง, นักเขียนตัดสิน)
- voice_412: อ้างถึงสำนวนเก่า (เนื้อเรื่อง, จัดการยกเว้นคำต้องห้ามแล้ว)
- loc_overflow ui_btn_enhance: EN/TH ขาด (UI)
## P2 — เฝ้าสังเกต
- doc_link 404 (ความสอดคล้องเอกสาร, ไม่กระทบบิลด์)
## จัดหมวดไม่ได้ — ต้องให้คนยืนยัน
- k_017_charge cooldown 0.0 (บาลานซ์, ไม่ทราบเจตนาการออกแบบ)
## แนวโน้ม (เทียบสัปดาห์ที่แล้ว)
- P0: 3 รายการ (สัปดาห์ที่แล้ว 5 รายการ)
- P1: 4 มัด (สัปดาห์ที่แล้ว 22 รายการ — เปลี่ยนวิธีนับเป็นการจัดหมวดแบบมัด)
- ผลบวกลวง: คนแก้ 2/8 = 25% (สัปดาห์ที่แล้ว 12%, ↑ — กลุ่มตัวอย่างเล็กลงหลังมัดรวม)
---
ตรวจสอบ: อี มินซู / 2026-W21 / เปลี่ยน 2 รายการ (หลักฐาน: §ล็อกการตรวจสอบ)
ดูจุดที่ไม่ปกปิดว่าอัตราผลบวกลวงของแนวโน้ม สูงขึ้น เป็น 25% กลุ่มตัวอย่างเล็กลงเหลือ 8 และคนแก้ไป 2 ในเชิงเลขคณิตจึงเป็น 25% รายงานไม่กุตัวเลขขึ้นเพื่อให้ดูดี ถ้าเทียบดิบ ๆ กับ 12% ของสัปดาห์ที่แล้วก็ดูเหมือนแย่ลง แต่บริบทที่ว่ากลุ่มตัวอย่างเปลี่ยนไปเพราะวิธีจัดหมวดเปลี่ยนเป็นแบบมัดนั้นแนบมาเป็นหนึ่งบรรทัด หลักการที่ว่าไม่สรุปผลด้วยอัตราของสัปดาห์เดียวทำงานอยู่ตรงนี้
ผู้เขียนเปรียบเทียบก่อนและหลังนำการจัดหมวดแบบ worked ของ Gap Report เข้ามาในโปรเจกต์ A ในจำนวนตัวเลขด้านล่าง อัตราการประมวลผล·เวลานั้นเป็นค่าวัดจริงที่ดึงจากบันทึกการประชุมและไทม์สแตมป์ของเครื่องมือทำงานร่วมกัน ส่วนอัตราผลบวกลวงของตัวตรวจสอบนั้นกลุ่มตัวอย่างแกว่งทุกสัปดาห์ ผู้เขียนจึงระบุ เฉพาะทิศทาง
| รายการ | ก่อนนำเข้า | หลังนำเข้า | เหตุผล |
|---|---|---|---|
| เวลาจัดหมวดรอบแรก 47 บรรทัด | คน \~40 นาที | LLM 1 รอบ + คนตรวจทบทวน \~12 นาที | ล็อกงานก่อนประชุม (วัดจริง) |
| ผลการตรวจสอบ → สะท้อนเป็นการตัดสินใจ | บางส่วนเท่านั้น | ส่วนใหญ่ | เทียบบันทึกการประชุม (วัดจริง, ไม่ได้รวบรวม % แน่นอน) |
| เวลาเฉลี่ยในการแก้ P0 | 3\~5 วัน | 1\~2 วัน | ไทม์สแตมป์การ์ดสร้าง→เสร็จในเครื่องมือทำงานร่วมกัน (วัดจริง) |
| อัตราคนแก้การจัดหมวดของ LLM | — | อิง W21 2/8 | การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ, แปรปรวนทุกสัปดาห์) |
| ความล่าช้าในการรับรู้ความสอดคล้องล้มเหลว | รอจนถึงประชุม | ทันที (atom แจ้งเตือน) | ผลของการนำ clickup_notify เข้ามา (ทิศทาง) |
มีเหตุผลที่ไม่ได้เขียนอัตราการแก้ 2/8 เป็นเหมือนการอวด นั่นเป็นแค่กลุ่มตัวอย่างของสัปดาห์เดียว และบางสัปดาห์โมเดลก็ชี้ผิดถึงห้ารายการ ผลได้ที่แน่นอนคือแรงงานจัดหมวดลดจาก 40 นาทีเหลือ 12 นาที ส่วนความแม่นยำของการจัดหมวดของโมเดลเองนั้นแกว่งทุกสัปดาห์ — ที่เร็วขึ้นไม่ใช่เพราะไว้ใจโมเดล แต่เพราะมันแปรรูปให้อยู่ในรูปที่คนตรวจสอบได้ภายใน 12 นาที
| แพทเทิร์น | วิธีรับมือ |
|---|---|
| คนนั่งจัดหมวด 47 บรรทัดด้วยมือทุกครั้ง | LLM จัดหมวดรอบแรก → แบ่งงานด้วยการให้คนตรวจสอบ |
| ยืนยัน severity ของ LLM ตามเดิม | severity คือ "ข้อเสนอ" การยืนยันเป็นของคน (ขั้น ③) |
| นับการละเมิดจากรากเดียวกันแยกทีละอัน | ระบุการขอมัดรวมในพรอมต์ |
| ข้อเท็จจริงการตรวจสอบเหลือเป็นแค่คำพูด | บังคับหลักฐานด้วย atom human_review_attestation |
| ความสอดคล้องล้มเหลวที่ด่วนกลับต้องรอจนถึงประชุม | แจ้งเตือนทันทีด้วย atom clickup_notify |
| โมเดลกุตัวเลขแนวโน้มขึ้นเอง | แนวโน้มให้คนป้อน โมเดลแค่เรนเดอร์ (ขั้น ④) |
| รายงานยาวจนในประชุมไม่มีใครดู | บังคับ 1 หน้า เก็บล็อกต้นฉบับแยกไว้ |
setup 1. รวบรวมเอาต์พุตของการตรวจสอบ cascade (หรือมัดของ lint·ตัวตรวจสอบความสอดคล้องที่คุณมี) มาไว้ในไฟล์เดียว 2. ให้ทีมตกลงเกณฑ์ severity 3 ระดับ (P0 กั้น / P1 ตรวจทบทวน / P2 เฝ้าสังเกต) คนละหนึ่งบรรทัด 3. สร้างเทมเพลตที่ป้อน ID ผู้ตรวจสอบ·ไทม์สแตมป์ลงในส่วนท้ายของรายงาน
prompt
ด้านล่างคือเอาต์พุตการตรวจสอบรายสัปดาห์ ช่วยจัดหมวดหมู่ให้เพื่อใช้ในประชุมรายสัปดาห์ที severity (P0/P1/P2) ให้ใส่เหตุผลหนึ่งบรรทัดแล้ว "เสนอ" เท่านั้น (ถ้าเป็นการเดาให้เขียน "ประมาณการ") การยืนยันผมจะทำเอง อันที่ละเมิดจากรากเดียวกันให้มัดรวม และช่วยแนะนำสายงานที่รับผิดชอบ (ไม่ใช่ชื่อคน) ด้วย อันที่ตัดสินไม่ได้ ให้แยกออกมาตรง ๆ ว่า "จัดหมวดไม่ได้"
[วางล็อกดิบ]
verify 1. ให้คนตรวจสอบรายการระดับ P0 ทุกรายการทีละอัน แล้วบันทึกการลด/เลื่อนระดับ (ขั้น ③) 2. ตรวจสอบว่ารายการที่โมเดลมัดรวมมาจากรากเดียวกันจริงไหม โดยย้อนรอยกลับเพียงอันเดียว 3. ตรวจในส่วนท้ายว่าตัวเลขแนวโน้มเป็นค่าที่ออกมาจากมือคน (โมเดลไม่ได้เติมเข้าไปเอง)
ถ้าทำงานคนเดียว atom·เครื่องมือทำงานร่วมกัน·ประชุมรายสัปดาห์ไม่มีก็ได้ วางเอาต์พุตของตัวตรวจสอบเป็นข้อความ แล้วใช้พรอมต์ข้างต้นรับแค่การจัดหมวด จากนั้นตรวจสอบรายการระดับ P0 เพียง 3 รายการด้วยตาตนเอง แล้วจัดการทันทีตรงนั้น การปล่อยการจัดหมวดให้โมเดล แล้ว จำกัดรายการที่ต้องตรวจสอบให้เหลือเฉพาะ P0 — สิ่งเดียวนั้นช่วยประหยัดเวลาได้มากที่สุดในระดับคนเดียว รายงาน 1 หน้าจะแทนด้วยโน้ตใน Notion หนึ่งหน้าก็ได้
สองวันก่อนปิดสปรินต์ อาร์ติสต์ฝ่ายต่อสู้โยนคลิปวิดีโอสั้น ๆ มาในแชตภายในทีม เป็นคอมโบ 3 ตีของคลาสนักรบ (Musa) ตัวใหม่ ตีที่ 1 และตีที่ 2 มีเสียงดาบหวือกระหึ่ม แต่ตีที่ 3 กลับเงียบสนิท ไม่มีเสียง อาร์ติสต์ยืนยันว่าใส่เสียงครบแล้ว ส่วนคนทำเสียงก็ยืนยันว่าส่งไฟล์ครบแล้ว ทั้งคู่ไม่ได้โกหก ไฟล์เสียงอยู่ในรีโพจริง ในชื่อ combo3_swing_final_real.wav แต่ชื่อที่โค้ดของเกมตามหาคือ sfx_K012_combo3_swing.wav ไม่ตรงกันแม้แต่ตัวอักษรเดียว
การไล่หาต้นเหตุของอุบัติเหตุ "เสียงหาย" ครั้งนี้กินเวลาบ่ายวันนั้นไปทั้งวัน นี่ไม่ใช่ปัญหาของคลิปเดียวหรือเสียงเดียว ตราบใดที่คนยังตั้งชื่อได้อย่างอิสระ อุบัติเหตุแบบนี้จะเกิดใหม่อีกหลายสิบครั้งในทุกไตรมาส บทนี้คือเรื่องราวของการเปลี่ยนอิสระนั้นให้กลายเป็นกฎ
คำถามที่บทนี้ตอบ - ทำไมในระดับทรัพยากร 1 หมื่นชิ้น การตั้งชื่อจึงไม่ใช่อิสระ แต่เป็นกฎ - เมื่อบังคับใช้ข้อตกลงการตั้งชื่อด้วย atom และตรวจสอบอัตโนมัติด้วย lint จะปิดช่องโหว่อะไรได้บ้าง - บันทึกเซสชันจริง (worked transcript) ที่ AI ร่างการแมประหว่างอนิเมชัน·VFX·เสียง·ไอคอนของสกิลหนึ่ง แล้วมนุษย์เป็นผู้เลือกรับ
หนึ่งบรรทัดสำหรับผู้อ่านนอกสายงาน ทรัพยากร 1 หมื่นชิ้นหรือรูปแบบการตั้งชื่อไฟล์ fbx ดูเหมือนเป็นเรื่องเฉพาะของวงการเกม แต่สิ่งเดียวที่คุณควรนำกลับไปนั้นไม่จำกัดโดเมน — "ทันทีที่ตั้งชื่อได้อย่างอิสระ การค้นหา·การทำงานอัตโนมัติ·การเชื่อมโยงก็ถูกล็อกไปพร้อมกัน" เมื่อสเกลใหญ่ขึ้น การตั้งชื่อต้องไม่ใช่เรื่องรสนิยมแต่ต้องเป็นกฎ และหลักการที่ว่า "มีเพียงชื่อที่กลายเป็นกฎแล้วเท่านั้นที่โค้ดจะค้นหาและนำมาใช้โดยอัตโนมัติได้" นั้นใช้ได้กับงานทุกชนิดที่ต้องจัดการเอกสาร·สินทรัพย์·เรกคอร์ดลูกค้า
โปรเจกต์ A ที่ผู้เขียนกำกับอยู่เป็น MMORPG ที่ให้ความสำคัญกับมือถือเป็นหลัก ขนาดโดยประมาณของทรัพยากรอนิเมชันตัวละครเป็นดังตารางด้านล่าง จำนวนคลาสของผู้เล่นและจำนวนชนิดของศัตรู NPC เป็นตัวเลขจากการดำเนินงานจริง ส่วนจำนวนคลิปและค่าประมาณรวมเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ)
| ทรัพยากร | จำนวน |
|---|---|
| คลาสตัวละครผู้เล่น | 6 |
| ชนิดของศัตรู NPC | 80\~100 |
| คลิปเฉลี่ยต่อตัวละคร 1 ตัว | 100\~150 (การประมาณของผู้เขียน) |
| ค่าประมาณคลิปทั้งหมด | ประมาณ 10,000\~15,000 (การประมาณของผู้เขียน) |
หนึ่งหมื่นชิ้น นี่ก็เหมือนลิ้นชัก 1 หมื่นช่อง การยืนหา "ท่าโจมตีอยู่ลิ้นชักไหนนะ" ต่อหน้าลิ้นชัก 1 หมื่นช่องที่ไม่มีป้ายกำกับ คือการเอาความจำของคนไปเดิมพัน และการเดิมพันนั้นต้องแพ้แน่นอน ถ้าหาไม่เจอ ผลลัพธ์จะมีอยู่สองทาง คือเวลาทำงานเพิ่มเป็นสองเท่า หรือเพราะหาไม่เจอจึงสร้างท่าเดิมขึ้นมาใหม่ ทางหลังแย่กว่า เพราะนอกจากทรัพยากรจะพองตัวขึ้นแล้ว ภายหลังท่าเดียวกันสองชุดยังจะลอยอยู่ในระบบโดยต่างกันเพียงเล็กน้อยอีกด้วย
ถ้าชื่อเป็นพื้นที่อิสระ สิ่งที่ถูกล็อกไม่ได้มีแค่การค้นหาเท่านั้น การกำหนดเส้นทางอัตโนมัติที่ "โค้ดค้นหาไฟล์อนิเมชันจากสกิล ID ให้เองโดยอัตโนมัติ" ก็ถูกล็อกไปด้วย ถ้าอ่านกฎออกมาจากชื่อไม่ได้ โค้ดก็ต้องถือตารางแมปที่เขียนด้วยมือว่าสกิลแต่ละตัวจะใช้ไฟล์ใด และตารางนั้นต้องเพิ่มด้วยมือทุกครั้งที่มีตัวละครใหม่เข้ามา
ชื่อไฟล์อนิเมชันของโปรเจกต์ A ถูกตรึงไว้เป็นห้าช่อง
<role>_<id>_<category>_<action>_<variant>.fbx
char_K001_idle_default_v1.fbx
char_K001_locomotion_walk_forward.fbx
char_K001_combat_attack_combo1_v2.fbx
char_K001_react_hit_heavy.fbx
enemy_E021_combat_skill_aoe_v1.fbx
ทั้งห้าช่องล้วนเป็นไปตาม enum ที่กำหนดไว้ ช่องเดียวที่อนุญาตให้กรอกอิสระคือ id และแม้แต่ช่องนั้นก็ยังถูกผูกด้วยรูปแบบ [A-Z]\d{3}
| ช่อง | จำนวน enum | ตัวอย่าง |
|---|---|---|
| role | 4 | char, enemy, pet, mount |
| id | รูปแบบตายตัว | K001, E021, P003, M005 |
| category | 8 | idle, locomotion, combat, react, death, social, cinematic, system |
| action | 10\~30 ต่อหมวด | walk, run, attack, skill_aoe, hit_heavy |
| variant | รูปแบบตายตัว | default, v1, v2, _short, _long |
หัวใจตรงนี้ไม่ใช่ตัวรูปแบบ แต่คือ "จะป้อนรูปแบบไว้ที่ไหน" ถ้าเขียนข้อตกลงการตั้งชื่อไว้ในหน้าเอกสารวิกิหน้าหนึ่ง มันก็คือป้ายกำกับที่ไม่มีใครอ่าน ผู้เขียนสร้างข้อตกลงนี้ให้เป็น atom ซึ่งเป็นแหล่งความจริงเดียว (single source of truth) ในชื่อ Char_Anim_Naming_Convention แล้วทำให้ทั้งคน·ทั้ง lint·ทั้ง LLM มองมาที่ atom เดียวนี้เท่านั้น ทันทีที่รูปแบบถูกตรึงเป็น atom แทนที่จะเป็นเอกสาร การตั้งชื่อก็เปลี่ยนสภาพจาก "ข้อแนะนำ" ไปเป็น "ด่านที่ต้องผ่าน"
จุดอ่อนคือ enum ของช่อง action เพิ่มได้ไม่จำกัด จึงต้องบริหารจัดการ action มาตรฐานของแต่ละหมวดด้วยพจนานุกรม
combat:
- attack_basic
- attack_combo1
- attack_combo2
- skill_<skill_id>
- parry
- dodge_forward
- dodge_back
react:
- hit_light
- hit_heavy
- knockback
- stagger
- stun
locomotion:
- idle
- walk_forward
- run_forward
- sprint
- jump_start
- jump_loop
- jump_land
จะใส่ action ใหม่ลงพจนานุกรมหรือไม่นั้น ตัดสินด้วยกระบวนการ มีตัวละครตั้งแต่ 3 ตัวขึ้นไปต่อไตรมาสที่จะใช้ได้หรือไม่ ด้วย action ที่มีอยู่แล้วแสดงออกไม่ได้จริง ๆ หรือไม่ หมวดชัดเจนหรือไม่ และที่สำคัญที่สุด — ดูดซับเข้าไปใน variant ไม่ได้จริงหรือ ถ้าจัดการด้วย variant ได้ ก็จะไม่เพิ่ม action ถ้าพจนานุกรม action ยังคงอยู่ในราว 100 รายการ ก็เป็นสัญญาณว่าการบริหารจัดการแข็งแรงดี แต่จะไม่ถือว่านี่เป็นเพดานเด็ดขาด ถ้าแนวเกมใหม่หรือคลาสใหม่เข้ามา ก็อาจเพิ่มทีเดียว 30\~40 รายการได้ สิ่งที่ต้องกั้นไม่ใช่ตัวเลข แต่คือการขยายพันธุ์อย่างไร้การควบคุม
เมื่อป้อนรูปแบบเป็น atom แล้ว ก็ต้องมีตัวตรวจสอบที่บังคับใช้ atom นั้นโดยอัตโนมัติ คนจะมานั่งตรวจ 5 ช่องด้วยตาทุกครั้งไม่ได้ ด้านล่างคือกระดูกสันหลังของ lint ตัวนั้น
# anim_naming_lint.py
import re, yaml
NAMING_PATTERN = re.compile(
r"^(?P<role>char|enemy|pet|mount)_"
r"(?P<id>[A-Z]\d{3})_"
r"(?P<category>idle|locomotion|combat|react|death|social|cinematic|system)_"
r"(?P<action>[a-z_]+?)"
r"(?:_(?P<variant>v\d+|short|long|light|heavy|left|right|forward|back))?"
r"\.fbx$"
)
ACTION_DICT = yaml.safe_load(open("char_anim_naming_convention.yaml"))
def check(filename):
m = NAMING_PATTERN.match(filename)
if not m:
return f"ละเมิดกฎการตั้งชื่อ (ไม่ตรงรูปแบบ 5 ช่อง): {filename}"
category, action = m.group("category"), m.group("action")
# รูปแบบ skill_<id> เป็น action แบบไดนามิก จึงตรวจเฉพาะ prefix
base = "skill" if action.startswith("skill_") else action
if base not in ACTION_DICT.get(category, []):
return f"action อยู่นอก enum ({category}): {action}"
return None
ทันทีที่ fbx ใหม่เข้าสู่รีโพ การตรวจนี้จะทำงาน ถ้าละเมิด commit จะถูกกั้น สิ่งที่สำคัญตรงนี้คือ "ไม่โยนความผิดของการละเมิดให้เป็นความรับผิดชอบของคน" แทนที่จะตำหนิอาร์ติสต์ที่ก่ออุบัติเหตุเสียงหาย เราโยนความรับผิดชอบไปที่เครื่องมือในทำนองที่ว่า "ชื่อนั้นไม่ควร commit ได้ตั้งแต่แรก" คนผิดพลาดได้ และเครื่องมือก็กั้นความผิดพลาดนั้นไว้ นี่คือท่าทีพื้นฐานของระบบการตั้งชื่อ
เมื่อบังคับการตั้งชื่อได้ สิ่งที่ได้กลับมาเป็นการตอบแทนคือการกำหนดเส้นทางอัตโนมัติที่ถูกปลดล็อก
def play_skill_animation(character, skill_id):
anim_path = f"char_{character.id}_combat_skill_{skill_id}.fbx"
if not exists(anim_path):
anim_path = f"char_{character.id}_combat_skill_default.fbx" # fallback
play(anim_path)
ตารางแมปที่เขียนด้วยมือหายไป ต่อให้มีตัวละครใหม่หรือสกิลใหม่เข้ามา ขอเพียงเพิ่มไฟล์อนิเมชันตามข้อตกลง โค้ดก็ไม่ต้องเปลี่ยนแม้แต่บรรทัดเดียว ถ้าย้อนกลับไปที่อุบัติเหตุเสียงหาย — สมมติว่าไฟล์เสียงนั้นเข้ามาได้เฉพาะในชื่อตามข้อตกลงคือ sfx_K012_combo3_swing.wav เท่านั้น — ตั้งแต่แรก combo3_swing_final_real.wav ก็จะถูกตีกลับตั้งแต่ขั้น commit และบ่ายวันนั้นก็จะยังรอดอยู่ทั้งวัน
ช่อง variant คือแผ่นนิรภัยที่ปกป้อง action enum เวอร์ชันของท่าเดียวกัน (v1, v2) ความยาว (_short, _long) ความแรง (_light, _heavy) ทิศทาง (_forward, _back) ทั้งหมดถูกดูดซับเข้า variant เพื่อรับไว้ แทนที่จะแตก action ออกเป็นย่อย ๆ และโค้ดของเกมก็สามารถเลือกใช้ variant นั้นตามบริบทได้
def select_variant(base_action, context):
if context.distance < 3:
return f"{base_action}_short"
if context.distance > 10:
return f"{base_action}_long"
return base_action
ข้อตกลงการตั้งชื่อจึงกลายเป็นจุดแตกแขนงของโค้ดไปในตัว
ถ้าการตั้งชื่อคือ L1 การแมปที่เชื่อมสกิลกับทรัพยากรก็คือ L2 สกิล 1 ตัวมักลากตามมาด้วยอนิเมชัน 2\~3 ชิ้น, VFX 1\~3 ชิ้น, เสียง 2\~5 ชิ้น, ไอคอน UI 1 ชิ้น เฉลี่ยแล้วก็ราวทรัพยากร 10 ชิ้น ถ้ามีสกิล 200 ตัว เป้าหมายการแมปก็ราว 2,000 ชิ้น การจัดการสเกลขนาดนี้ด้วยหัวคนเป็นไปไม่ได้ จึงวาง yaml หนึ่งแผ่นต่อสกิลหนึ่งตัว แล้วผูกให้ทรัพยากรของสกิลนั้นถูกอ่านได้จากแผ่นนั้นแผ่นเดียวเท่านั้น
---
skill_id: skill_K001_combo1
description: K001 คอมโบ 1 (3 ตีต่อเนื่อง)
type: melee_combo
animations:
- clip: char_K001_combat_attack_combo1_v2.fbx
role: main
bone_alignment: spine_03
vfx:
- asset: vfx_K001_combo1_slash.vfx
socket: weapon_tip
timing_ms: [0, 150, 300]
- asset: vfx_hit_blood_light.vfx
socket: target
timing_ms: [150]
sound:
- asset: sfx_K001_combo1_swing.wav
volume: 0.8
timing_ms: 0
- asset: sfx_hit_metal_light.wav
volume: 0.6
timing_ms: 150
ui_icon: icon_skill_K001_combo1.png
ui_tooltip_key: skill_K001_combo1_tooltip
verified: true
---
แผ่นนี้แผ่นเดียวคือทรัพยากรทั้งหมดของสกิลหนึ่งตัว และเส้นทางทรัพยากรทุกตัวภายใน yaml นี้ล้วนเป็นไปตามข้อตกลง 5 ช่องของ 11.1 ถ้า lint การตั้งชื่อพังลง การแมปนี้ก็พังตามไปด้วย สองชั้นนี้ทำงานเป็นคู่กัน
เมื่อการแมปมารวมอยู่ที่เดียว การไล่ติดตามผลกระทบก็ถูกปลดล็อกโดยอัตโนมัติ เมื่อจะรื้อ VFX สักตัวใหม่ ก็ไม่ต้องเอามือไล่ค้นว่ามันกระทบสกิลไหนบ้าง
def find_skills_using(asset):
affected = []
for path in glob("skills/*.yaml"):
skill = yaml.safe_load(open(path))
for cat in ("vfx", "sound", "animations"):
for entry in skill.get(cat, []):
if entry.get("asset") == asset or entry.get("clip") == asset:
affected.append(skill["skill_id"])
return affected
# find_skills_using("vfx_hit_blood_light.vfx")
# → ["skill_K001_combo1", "skill_K005_combo2", "skill_E021_attack_basic", ...]
รายชื่อสกิลที่ได้รับผลกระทบจะถูกแนบเข้าวาระประชุมเรื่องเปลี่ยนทรัพยากรโดยอัตโนมัติ ก่อนที่คำถามว่า "ถ้าเปลี่ยนตัวนี้แล้วจะกระทบที่ไหนบ้าง" จะถูกถาม คำตอบก็วางอยู่ข้างบันทึกการประชุมเรียบร้อยแล้ว
การแมปก็มี lint ติดมาด้วย ไฟล์ทรัพยากรทุกตัวมีอยู่จริงหรือไม่, animations.main กับ ui_icon มีอย่างละหนึ่งหรือไม่, timing_ms อยู่ภายในความยาวอนิเมชันหรือไม่ และ — เส้นทางทรัพยากรทุกตัวผ่านข้อตกลงการตั้งชื่อ 11.1 หรือไม่ รายการสุดท้ายคือตะปูที่ตรึงสองชั้นเข้าด้วยกัน มันจะทำงานอัตโนมัติตอนบิลด์
จะสรุปเป็นกระแสว่า lint การตั้งชื่อและ lint การแมปที่ผ่านมาเชื่อมเข้าเป็นเกตเดียวได้อย่างไร
flowchart TD
A[commit ทรัพยากร/สกิลใหม่] --> B{lint การตั้งชื่อ 5 ช่อง
Char_Anim_Naming atom}
B -->|ละเมิด| X[กั้น commit
คืนข้อความละเมิด]
B -->|ผ่าน| C{lint yaml การแมป}
C -->|ทรัพยากรไม่มีอยู่ / ขาด main·icon| X
C -->|อ้างอิงทรัพยากรที่ละเมิดข้อตกลงการตั้งชื่อ| X
C -->|ผ่าน| D[อัปเดตสถิติพูลทรัพยากร]
D --> E{เป็นข้อเสนอการตั้งชื่อ·การแมปจาก LLM หรือไม่?}
E -->|ใช่| F[มนุษย์เลือกรับ/ปฏิเสธ
ขั้นย้อนกลับได้]
E -->|ไม่ใช่| G[นำเข้าบิลด์]
F -->|รับ| G
F -->|ปฏิเสธ| H[ทิ้งข้อเสนอ
ย้อนกลับได้ ต้นทุน 0]
G --> I{สั่งทำโมชันแคปเจอร์·อัดเสียง?}
I -->|ใช่| J[เข้าสู่ขั้นย้อนกลับไม่ได้
กลับคืนไม่ได้]
I -->|ไม่ใช่| K[คงสินทรัพย์ย้อนกลับได้]
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class A,D data;
class B,C,E code;
class F,I human;
class K pass;
class X,H,J fail;
ขอให้สังเกตว่าที่ปลายของกระแสนี้มีเส้นแบ่งระหว่างย้อนกลับได้กับย้อนกลับไม่ได้ ตั้งแต่การแก้ yaml, ข้อเสนอจาก LLM ไปจนถึงคีย์เฟรม ล้วนย้อนกลับได้ทั้งหมด ถ้าไม่ถูกใจก็ทิ้งไปเสีย และต้นทุนแทบเป็น 0 แต่ทันทีที่ก้าวข้ามไปสู่การถ่ายโมชันแคปเจอร์, การอัดเสียงนักพากย์, การคัดเลือกซิกเนเจอร์วอยซ์ มันจะเปลี่ยนเป็นย้อนกลับไม่ได้ เพราะมีการจองนักแสดงและสตูดิโอ, บูธอัดเสียง, สัญญา, การรับรู้ของตลาดเข้ามาเกี่ยวข้อง ดังนั้นการตัดสินใจเรื่องการตั้งชื่อ·การแมป·เพอร์โซนาทั้งหมดต้องจบลงก่อนถึงขั้นย้อนกลับไม่ได้ คือภายในพื้นที่ย้อนกลับได้ที่ประกอบด้วย yaml, ข้อเสนอ LLM และคีย์เฟรม
ถึงตรงนี้คือระบบ และตอนนี้จะแสดงให้เห็นว่า AI เข้ามาตรงไหนตามเซสชันจริง เป็นฉากที่ให้ LLM ร่างการแมปทรัพยากรของสกิลไฟใหม่ skill_K012_flame_burst
พรอมต์ (ฉบับเต็ม):
ต่อไปนี้คือข้อตกลงการตั้งชื่ออนิเมชันตัวละครของโปรเจกต์เรา (atom), สถิติพูลทรัพยากรที่มีอยู่,
และ yaml ของสกิลที่คล้ายกันหนึ่งรายการ
[ข้อตกลงการตั้งชื่อ]
<role>_<id>_<category>_<action>_<variant>.fbx
role: char|enemy|pet|mount / category: idle|locomotion|combat|react|death|social|cinematic|system
[ทรัพยากรที่ใช้มากสุดในพูล TOP]
sfx_hit_metal_light.wav (47 สกิล), vfx_dust_kickup.vfx (38 สกิล),
vfx_fire_burst_med.vfx (22 สกิล), sfx_fire_whoosh.wav (19 สกิล)
[yaml สกิลที่คล้ายกัน]
skill_id: skill_K005_fire_slash / type: melee_fire
animations: [char_K005_combat_attack_fire_v1.fbx]
vfx: [vfx_K005_fire_slash.vfx, vfx_fire_burst_med.vfx]
sound: [sfx_K005_fire_swing.wav, sfx_fire_whoosh.wav]
สกิลใหม่: skill_K012_flame_burst (นักรบ K012, ระเบิดไฟแบบพื้นที่, 1 ตี)
จงเขียนร่าง yaml การแมปของสกิลนี้ ใช้ทรัพยากรจากพูลที่มีอยู่ให้มากที่สุด
และทรัพยากรที่ต้องสร้างใหม่ให้ติดแท็ก [NEW] กำกับไว้
ผลลัพธ์ดิบจาก Claude (ไม่มีการสรุป·เรียบเรียง):
skill_id: skill_K012_flame_burst
description: นักรบ K012 ระเบิดไฟแบบพื้นที่ (1 ตี)
type: aoe_fire
animations:
- clip: char_K012_combat_skill_flame_burst_v1.fbx # [NEW] ต้องมีโมชันใหม่
role: main
bone_alignment: spine_03
vfx:
- asset: vfx_fire_burst_med.vfx # ใช้ซ้ำ (พูล 22 สกิล)
socket: ground
timing_ms: [200]
- asset: vfx_K012_flame_ring.vfx # [NEW] ของใหม่สำหรับแสดงขอบเขตพื้นที่
socket: ground
timing_ms: [200, 350]
sound:
- asset: sfx_fire_whoosh.wav # ใช้ซ้ำ (พูล 19 สกิล)
volume: 0.8
timing_ms: 0
- asset: sfx_explosion_low.wav # [NEW] เสียงระเบิดของใหม่ (คาดเดา)
volume: 0.9
timing_ms: 200
ui_icon: icon_skill_K012_flame_burst.png # [NEW]
ui_tooltip_key: skill_K012_flame_burst_tooltip
verified: false
หมายเหตุ: ใน
char_K012_combat_skill_flame_burst_v1.fbxค่า action เป็นskill_flame_burstแต่ตามข้อตกลงการตั้งชื่อ skill action ควรเป็นรูปแบบskill_<skill_id>จึงต้องตรวจสอบ เนื่องจากไม่ได้ระบุชัดในข้อตกลงว่าจะใช้skill_K012หรือskill_flame_burstจึงได้คาดเดาไว้
การตรวจสอบ/ปฏิเสธโดยมนุษย์:
หมายเหตุท้ายชี้จุดอ่อนได้ตรงเป๊ะ ไม่ใช่ว่า LLM ไม่รู้ แต่เพราะใน atom ของข้อตกลงเขียนรูปแบบของช่องนั้นไว้อย่างคลุมเครือ ในความเป็นจริง ข้อตกลงของโปรเจกต์ A ใช้รูปแบบ skill_<skill_id> ดังนั้นชื่อไฟล์ที่ถูกต้องคือ char_K012_combat_skill_K012_v1.fbx ส่วน skill_flame_burst ที่ LLM สร้างขึ้น — ปฏิเสธ และ sfx_explosion_low.wav ที่ติดแท็ก [NEW] ไว้นั้น เมื่อกลับไปดูพูลทรัพยากรอีกครั้ง พบว่า sfx_impact_heavy.wav ถูกใช้เป็นเสียงระเบิดอยู่แล้วใน 47 สกิล สาเหตุที่พลาดเพราะสถิติพูลที่ให้ LLM ไปนั้นมีแค่ TOP 4 จึงเปลี่ยนจากสร้างใหม่เป็นใช้ซ้ำแทน
การร้องขอใหม่:
แก้สองข้อแล้วเขียนใหม่
1. รูปแบบ skill action คือ skill_<skill_id> ถ้าเป็นสกิล K012 action ก็คือ skill_K012
2. ในพูลทรัพยากรมี sfx_impact_heavy.wav (47 สกิล, เสียงระเบิด/เสียงกระแทก) อยู่แล้ว
แทนที่จะสร้าง sfx_explosion_low.wav ใหม่ จงใช้ตัวนี้ซ้ำ
สถิติพูลทั้งหมดเป็นดังนี้ [แนบทั้งหมด 38 ชนิด]
ในรอบเดียวนี้ สิ่งที่ LLM ทำคือ "ร่างที่พอดูได้" และสิ่งที่คนทำคือ "พบจุดคลุมเครือของข้อตกลง·พบจุดที่พูลตกหล่น·ตัดสินใจใช้ซ้ำ" LLM มีแนวโน้มจะปั๊มแท็ก [NEW] ให้ทรัพยากรใหม่ง่ายเกินไป การตัดสินใจเรื่องใช้ซ้ำจึงต้องอยู่ในมือคนจนถึงที่สุด แต่กระนั้น การร่าง yaml จากศูนย์บนหน้าจอเปล่า กับการได้ร่างที่พร้อมเลือกรับ·ปฏิเสธมาแก้ มีภาระงานต่างกัน
บันทึกข้างต้นนั่นแหละคือฉากหนึ่งของการประยุกต์แบบก้าวหน้า การบริหารจัดการการตั้งชื่อ·การแมปแบ่งเป็นสองขั้น
ในขั้นอนุรักษนิยม คนเป็นผู้ให้ชื่อและร่างการแมป ส่วนระบบอัตโนมัติรับผิดชอบเพียงการตรวจสอบ (lint) และการไล่ติดตาม (find_skills_using) เท่านั้น ปัจจุบันการบริหารจัดการตัวละคร·ทรัพยากรของ MMORPG ส่วนใหญ่อยู่ตรงนี้ ในขั้นก้าวหน้า LLM เป็นผู้ออกข้อเสนอตั้งแต่ร่างการตั้งชื่อ, ร่างการแมป ไปจนถึงการสร้างเพอร์โซนา NPC และการตัดสินใจที่เหลืออยู่ในมือคนก็แคบลงเหลือเพียงข้อเดียวคือ "จะเลือกรับข้อเสนอไหน"
เพื่อให้ขั้นก้าวหน้าตั้งตัวได้ ต้องมีสามอย่างพร้อม อย่างแรกคือเอนจิน lint ของข้อตกลงการตั้งชื่อ ข้อเสนอการตั้งชื่อที่ LLM ออกมาก็ต้องผ่าน lint 5 ช่องเหมือนกับที่คนร่างไว้เป๊ะ ๆ จึงจะถูกเลือกรับได้ ที่ skill_flame_burst ของ LLM ในบันทึกข้างต้นถูกปฏิเสธก็เพราะเกตนี้ อย่างที่สองคือเครื่องสร้างเพอร์โซนา NPC อัตโนมัติ ถ้าแยกชิ้นส่วน yaml ของตัวละครออกเป็นสามแกนคือ voice_profile·anim_set·skill_set ไว้ LLM ก็จะรับคำบรรยายอย่าง "นักรบวัย 50, รอบคอบ, โทนเสียงต่ำ" แล้วเสนอข้อเสนอของทั้งสามแกนแยกออกมาได้ การร่างสามแกนของ NPC 100 ตัวจากศูนย์ กับการเลือกจากข้อเสนอสองสามตัวต่อเพอร์โซนา มีภาระต่างกัน อย่างที่สามคือเครื่องสร้างข้อเสนอการแมป ทิศทางย้อนกลับของ find_skills_using — คือการค้นหา "ทรัพยากรที่มีอยู่ซึ่งเข้ากับสกิลใหม่นี้" ผูกเข้ากับสถิติพูลทรัพยากร เพื่อเสนอข้อเสนอการใช้ซ้ำแยกตามช่อง เป็นผลสองทางที่ทั้งลดต้นทุนการสร้างใหม่และเพิ่มอัตราการใช้ซ้ำ
ทั้งสามองค์ประกอบล้วนทำงานบนโครงสร้างพื้นฐานเดียวกัน (yaml·lint·สถิติพูลทรัพยากร) มันจะเดินได้ก็ต่อเมื่อข้อตกลงการตั้งชื่อกับ yaml การแมปถูกจัดเรียงให้เป็นแหล่งความจริงเดียวเท่านั้น ถ้าการจัดเรียงพังลง อินพุตที่จะป้อนให้ LLM ก็ไม่มีตั้งแต่ต้น
ควรหมายเหตุไว้ว่าทั้งสามองค์ประกอบนี้ในเชิงทฤษฎีเป็นไปได้ตั้งแต่ยุค 2010 แล้ว สิ่งที่ติดขัดมีอยู่สามจุด คือเข้าใจไม่ได้ว่าท่านั้นคืออะไรจากภาษาธรรมชาติ จึงออกข้อเสนอ 5 ช่องไม่ได้, การแยก voice·anim·skill ออกมาผูกกันเป็นพื้นที่ของสัญชาตญาณมนุษย์, และการค้นหา "VFX ที่ให้ความรู้สึกคล้ายกัน" ด้วยคำบรรยายแบบข้อความนั้นทำได้ยาก หลังปี 2023 ด้วยพัฒนาการของ LLM ทั้งสามจุดเข้าสู่พื้นที่ที่ช่วยเสริมได้ วิสัยทัศน์การทำให้ทรัพยากรตัวละครก้าวหน้าซึ่งเคยอยู่บนกระดาษเท่านั้น ส่วนใหญ่ก็ขยับเข้าสู่ขั้นนำไปใช้งานจริงแล้ว
นี่คือการเปรียบเทียบก่อนและหลังโปรเจกต์ A นำการตั้งชื่อ·การแมปมาใช้ เวลาค้นหาและระยะเวลาออนบอร์ดเป็นทิศทางที่ผู้เขียนรู้สึก·บันทึกได้จริง ส่วนรายการที่เป็นอัตราส่วนเป็นค่าวัดจริงที่รวบรวมจากการทบทวนรายไตรมาส ขอแจ้งว่าค่าสัมบูรณ์บางส่วนเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ)
| รายการ | ก่อนนำมาใช้ | หลังนำมาใช้ |
|---|---|---|
| เวลาค้นหาท่า (แอนิเมเตอร์) | 5\~10 นาที | 30 วินาที |
| อัตราการสร้างซ้ำซ้อน | 12\~15% | 1\~2% |
| การแก้โค้ดกำหนดเส้นทางของตัวละครใหม่ | 50\~100 บรรทัด | 0 บรรทัด |
| อุบัติเหตุทรัพยากรสกิลใหม่ตกหล่น | 5\~8 ครั้งต่อไตรมาส | 0\~1 ครั้ง |
| ทรัพยากรไม่ถูกใช้สะสม (สัดส่วนในไลบรารี) | ประมาณ 30% | ประมาณ 8% |
| การออนบอร์ดแอนิเมเตอร์ใหม่ | 2 สัปดาห์ | 3 วัน |
รายการสุดท้ายเป็นผลที่เงียบที่สุดแต่ใหญ่ที่สุด atom ข้อตกลงการตั้งชื่อตัวเดียวกลายเป็นไกด์ออนบอร์ดไปในตัว แค่ประโยคเดียวกับแอนิเมเตอร์ใหม่ว่า "ตั้งชื่อด้วย 5 ช่องนี้ และถ้า lint กั้นก็ฟัง lint" ก็ทำงานวันแรกได้
| รูปแบบ | วิธีรับมือ |
|---|---|
| วางข้อตกลงการตั้งชื่อไว้เป็นแค่เอกสารวิกิ | ตรึงเป็นกฎด้วย atom เดียว + บังคับด้วย lint |
| enum ของ action ขยายพันธุ์ไม่จำกัด | พจนานุกรม + กระบวนการเพิ่มรายการใหม่ |
| commit โดยไม่ตรวจสอบการตั้งชื่อ | กั้น commit ด้วย lint อัตโนมัติ |
| ตารางแมปฮาร์ดโค้ดในโค้ด | กำหนดเส้นทางอัตโนมัติบนพื้นฐานการตั้งชื่อ |
| แตก action เป็นย่อย ๆ โดยไม่มี variant | ดูดซับด้วยช่อง variant |
| การแมปทรัพยากรกระจัดกระจายในโค้ด·ชีต·เอกสาร | รวมเป็น yaml ไฟล์เดียว |
| เลือกรับข้อเสนอการแมปจาก LLM โดยไม่ตรวจสอบ | lint การตั้งชื่อ + คนตัดสินการใช้ซ้ำ |
| โยนการละเมิดการตั้งชื่อให้เป็นความรับผิดชอบของคน | เสริม lint, โยนความรับผิดชอบไปที่เครื่องมือ |
setup — กำหนดชื่อไฟล์อนิเมชันเป็น 5 ช่อง <role>_<id>_<category>_<action>_<variant>.fbx และรวบรวมพจนานุกรม action ของแต่ละหมวดไว้ใน yaml ไฟล์เดียว ประกาศให้ yaml นี้เป็นแหล่งความจริงเดียวของทีม
prompt — ให้ LLM "[yaml ข้อตกลงการตั้งชื่อ] + [สถิติพูลทรัพยากร] + [yaml สกิลที่คล้ายกัน 1 รายการ]" แล้วขอร่าง yaml การแมปของสกิลใหม่ ระบุให้แยกทรัพยากรที่ใช้ซ้ำกับทรัพยากรที่ต้องสร้างใหม่ (แท็ก [NEW]) ออกจากกัน
verify — นำเส้นทางทรัพยากรทุกตัวจากผลลัพธ์ของ LLM ผ่าน lint การตั้งชื่อ (anim_naming_lint.py ข้างต้น) ถ้าไม่ผ่านให้ปฏิเสธ ในบรรดาข้อเสนอที่ผ่าน ตัวที่ติดแท็ก [NEW] ให้คนเป็นผู้ตัดสินว่าใช้ซ้ำได้หรือไม่ โดยกลับไปค้นพูลทรัพยากรอีกครั้ง
anim_naming_lint.py ไฟล์เดียวใน git pre-commit hookเมื่อเริ่มประชุมออกแบบ รายชื่อเพ็ตจะถูกหยิบขึ้นมาก่อน หมาป่าสิบสองสายพันธุ์ แมวแปดสายพันธุ์ นกห้าสายพันธุ์ ไม่มีใครพูดว่า "งั้นเรามาสร้างทีละตัวกันเถอะ" เพราะต่างจากตัวละคร เพ็ตนั้นตั้งต้นด้วยสมมติฐานว่า 'จะปั๊มออกมา 50 แบบ' อยู่แล้ว คำถามจึงไม่ใช่ "จะทำสายพันธุ์หนึ่งให้ดีได้อย่างไร" แต่เริ่มที่ "โครงร่างที่สร้างไว้ครั้งเดียวจะให้กี่สายพันธุ์ใช้ร่วมกัน"
ตัวละครนั้นแต่ละแบบเป็นตัวตนเฉพาะตัวสำหรับผู้ใช้ เราจึงทุ่มเทกับมันทีละแบบ ในทางกลับกัน เพ็ตและพาหนะส่วนใหญ่เป็น 'การแปรผันที่เปลี่ยนแค่สีและความสามารถบนโครงร่างเดียวกัน' ดังนั้นตั้งแต่เริ่มออกแบบเราจึงวางกฎการตั้งชื่อ เทมเพลต และ lint ให้พร้อม เพื่อปูสายการผลิตแบบปั๊มจำนวนมาก หากตั้งใจสร้างสายพันธุ์หนึ่งอย่างประณีตแล้วปล่อยให้มันถูกคัดลอกเป็นสิบสองชุด หมาป่าสิบสองตัวที่ต่างกันแค่สีก็จะมีคลิปแอนิเมชันชุดเดียวกันถูกใส่แยกกันทีละชุด จนโฟลเดอร์พองเป็น 4 กิกะไบต์ นั่นไม่ใช่การปั๊มจำนวนมาก แต่เป็นผลของการไม่ได้ปั๊มต่างหาก หัวใจไม่ได้อยู่ที่ 'สร้างให้ดีแค่ไหน' แต่อยู่ที่ 'สร้างให้น้อยแค่ไหนและใช้ร่วมกันให้มากแค่ไหน'
ด้วยเหตุนี้ บทนี้จึงนิยามเทมเพลตเพ็ตสายหมาป่าหนึ่งแบบด้วย yaml แล้วให้ AI ปั๊มอินสแตนซ์ที่สืบทอดโครงร่างนั้น จากนั้นตรวจสอบด้วย lint และวัดว่าถูกทิ้งกี่เปอร์เซ็นต์ โดยตามจังหวะนี้ไปจนจบ
ทั้งสามอย่างมีโครงสร้างทรัพยากรคล้ายกัน แต่มีน้ำหนักการรับรู้ของผู้ใช้ต่างกัน ตัวละครคือตัวผู้ใช้เองที่ใช้เวลาในเกมร่วมกันถึง 100% เพ็ตคือเพื่อนที่อยู่ข้างกาย ใช้เวลาด้วยกัน 50\~70% ส่วนพาหนะคือเครื่องมือที่หยิบออกมาเฉพาะตอนเดินทาง อยู่ที่ 10\~20% ยิ่งน้ำหนักการรับรู้ต่ำ ผู้ใช้ก็ยิ่งสังเกตรายละเอียดน้อยลง การทุ่มเทกับพาหนะเท่ากับที่ทุ่มให้ตัวละคร ก็เหมือนการบริหารโต๊ะที่นั่งทุกวันกับเก้าอี้พับที่กางเป็นครั้งคราวด้วยงบประมาณเท่ากัน
ด้วยเหตุนี้ เพ็ตและพาหนะจึงดำเนินงานด้วยโครงสร้าง 'เทมเพลต-อินสแตนซ์' เราสร้าง เทมเพลต หนึ่งแบบที่บรรจุโครงร่าง การเคลื่อนไหว และความสามารถพื้นฐาน แล้ววาง อินสแตนซ์ ที่เปลี่ยนแค่สี ไอคอน และความสามารถเล็กน้อยซ้อนทับลงไป อินสแตนซ์ใช้ทรัพยากรของเทมเพลตร่วมกันถึง 90% ดังนั้นสิ่งที่สร้างใหม่จริง ๆ มีเพียง 10% ที่เหลือ การแยกส่วนนี้เมื่อเขียนเป็นภาพได้ดังนี้
เมื่อสร้างก้อนเทมเพลตด้านซ้ายเสร็จครั้งเดียว อินสแตนซ์ด้านขวาก็เพียงแค่สลับสี ไอคอน และความสามารถหนึ่งบรรทัดเท่านั้น 'โฟลเดอร์ 4 กิกะไบต์' ที่กล่าวไปข้างต้น คือภาพที่เกิดขึ้นเมื่อละเลยการแยกส่วนนี้จนทรัพยากร 90% ถูกคัดลอกซ้ำสิบสองครั้ง
กฎการตั้งชื่อของเพ็ตและพาหนะคือรูปแบบที่ตัดออกหนึ่งช่องจากการตั้งชื่อตัวละครในหัวข้อ 11.1 ตัวละครใช้ห้าช่อง char_<id>_<category>_<action>_<variant> แต่เพ็ตและพาหนะละ variant ไป ใช้สี่ช่อง หาก variant จำเป็น ก็ผนวกเข้าไว้ใน action
pet_<id>_<category>_<action>.fbx
mount_<id>_<category>_<action>.fbx
ตัวอย่าง:
pet_P003_idle_default.fbx
pet_P003_combat_bite.fbx
mount_M005_locomotion_run.fbx
yaml การแมปทรัพยากรก็ทำให้เบาลงด้วยการตัดช่อง vfx และ sound ออกจากแบบฟอร์มของตัวละคร อินสแตนซ์ที่แบกช่องเหล่านี้ไว้ทั้งก้อนจะกลายเป็นแบบฟอร์มที่เต็มไปด้วยช่องว่างเปล่า ทำให้ lint ขึ้นเตือนเปล่า ๆ ทุกครั้ง
ทีนี้เข้าเรื่องหลัก เรามานิยามเทมเพลตสายหมาป่าหนึ่งแบบ แล้วปั๊มอินสแตนซ์ออกมาจากเทมเพลตนั้นกัน
ก่อนจะให้ AI ปั๊มจำนวนมาก คนจะลงมือกำหนดเทมเพลตหนึ่งแบบด้วยมือก่อน เทมเพลตแบบเดียวนี้จะกลายเป็นเกณฑ์คุณภาพของอินสแตนซ์อีกหลายสิบแบบ จึงไม่นำมาทำอัตโนมัติ เทมเพลตสายหมาป่า (canine) วางไว้ดังนี้
# pet_template_canine.yaml
template_id: pet_template_canine
skeleton: skel_quadruped_medium # โครงร่างร่วมสำหรับสัตว์สี่ขาขนาดกลาง
shared_animations:
- clip: pet_template_canine_idle_default.fbx
- clip: pet_template_canine_locomotion_walk.fbx
- clip: pet_template_canine_locomotion_run.fbx
- clip: pet_template_canine_combat_bite.fbx
shared_abilities:
- id: pet_template_canine_passive_speed
description: ความเร็วเคลื่อนที่ของเพื่อนร่วมทาง +3%
- id: pet_template_canine_active_bite
description: กัดเป้าหมายเดี่ยว, คูลดาวน์ 12s
bt_ref: bt_pet_canine_default # BT พื้นฐานสำหรับการติดตาม + ช่วยในการต่อสู้
instance_overridable: # ไวต์ลิสต์ของฟิลด์ที่อินสแตนซ์เปลี่ยนได้
- visual_skin
- ui_icon
- ui_tooltip_key
- extra_ability # อนุญาตเพิ่มความสามารถได้สูงสุด 1 ชุดต่ออินสแตนซ์
ตรงนี้ instance_overridable คืออุปกรณ์หลัก มันตรึงฟิลด์ที่อินสแตนซ์แตะต้องได้ไว้เป็นไวต์ลิสต์ หาก AI ขณะปั๊มจำนวนมากพยายามเปลี่ยนโครงร่างหรือแอนิเมชันร่วมตามใจชอบ นั่นคือการแตะฟิลด์ที่ไม่อยู่ในรายการนี้ lint จึงจับได้ การนิยาม 'สิ่งที่เปลี่ยนได้' ก่อนคือเข็มขัดนิรภัยของการปั๊มจำนวนมาก
ต่อไปนี้คือพรอมต์ฉบับเต็มที่ใช้ปั๊มอินสแตนซ์ 10 แบบ ผมขอแสดงตามเดิมโดยไม่ย่อ
[พรอมต์]
คุณคือผู้ช่วยที่ช่วยเขียนข้อมูลเพ็ต โดยอิงตามเทมเพลตด้านล่าง
จงสร้าง yaml ของอินสแตนซ์เพ็ตสายหมาป่า 10 แบบ
[เทมเพลต] pet_template_canine.yaml
(วาง yaml ฉบับเต็มข้างต้นลงไป)
[กฎ]
1. แต่ละอินสแตนซ์ต้องระบุ template: pet_template_canine เสมอ
2. ใน overrides ให้ใส่เฉพาะฟิลด์ในไวต์ลิสต์ instance_overridable
ฟิลด์ที่ไม่อยู่ในไวต์ลิสต์ (skeleton, shared_animations ฯลฯ) ห้ามแตะเด็ดขาด
3. visual_skin ต้องเป็นการแปรผันตามธรรมชาติของหมาป่า (สี ลวดลาย ขนาด)
4. extra_ability สูงสุด 1 ชุดต่ออินสแตนซ์ เลือกอย่างใดอย่างหนึ่งระหว่าง passive หรือ active
ผลของมันต้องไม่ซ้ำซ้อนกับ shared_abilities ที่มีอยู่
5. ui_icon, ui_tooltip_key ให้เป็นไปตามกฎการตั้งชื่อที่อิงกับ pet_id
6. กำหนด pet_id เป็น pet_P003 ~ pet_P012
ผลลัพธ์ให้มีเฉพาะบล็อก yaml 10 บล็อกเท่านั้น ห้ามแนบประโยคอธิบาย
กฎข้อ 2 จับคู่กับไวต์ลิสต์ของขั้นที่ 1 ส่วน "ต้องไม่ซ้ำซ้อน" ในกฎข้อ 4 คือข้อจำกัดที่กันไม่ให้ AI คัดลอกความสามารถแบบมักง่าย หากไม่ตั้งข้อจำกัดเหล่านี้ ดังที่จะเห็นต่อไป AI จะลู่เข้าหาตัวเลือกที่ปลอดภัยที่สุด (คัดลอกความสามารถเดิมมาแปะ)
จากผลลัพธ์ที่ AI พ่นออกมา ผมคัดมาตามเดิมโดยมีทั้งที่สำเร็จ ล้มเหลว และก้ำกึ่งปะปนกัน ไม่ได้เกลาให้เรียบร้อย
# ผลลัพธ์ 1 — ปกติ
pet_id: pet_P003
template: pet_template_canine
overrides:
visual_skin: wolf_gray
ui_icon: icon_pet_P003.png
ui_tooltip_key: pet_P003_tooltip
extra_ability:
id: pet_P003_active_howl
description: ลดความเร็วเคลื่อนที่ของศัตรู -20% เป็นเวลา 5 วินาที, คูลดาวน์ 30s
# ผลลัพธ์ 2 — ปกติ
pet_id: pet_P004
template: pet_template_canine
overrides:
visual_skin: wolf_black
ui_icon: icon_pet_P004.png
ui_tooltip_key: pet_P004_tooltip
extra_ability:
id: pet_P004_passive_crit
description: โอกาสคริติคอลของเพื่อนร่วมทาง +2%
# ผลลัพธ์ 5 — ก้ำกึ่ง (ความสามารถซ้ำกับความสามารถร่วมโดยพฤตินัย)
pet_id: pet_P007
template: pet_template_canine
overrides:
visual_skin: wolf_brown
ui_icon: icon_pet_P007.png
ui_tooltip_key: pet_P007_tooltip
extra_ability:
id: pet_P007_passive_speed_boost
description: ความเร็วเคลื่อนที่ของเพื่อนร่วมทาง +3% # ← ผลเหมือนกับ passive_speed ของ shared
# ผลลัพธ์ 8 — ล้มเหลว (ล่วงล้ำฟิลด์นอกไวต์ลิสต์)
pet_id: pet_P010
template: pet_template_canine
overrides:
visual_skin: wolf_white
ui_icon: icon_pet_P010.png
shared_animations: # ← ไม่อยู่ในไวต์ลิสต์ overridable
- clip: pet_P010_combat_pounce.fbx
extra_ability:
id: pet_P010_active_pounce
description: โจมตีพุ่งกระโจน, คูลดาวน์ 20s
# ผลลัพธ์ 9 — ล้มเหลว (ละเมิดกฎการตั้งชื่อ)
pet_id: P011 # ← ขาดคำนำหน้า 'pet_'
template: pet_template_canine
overrides:
visual_skin: wolf_silver
ui_icon: pet11_icon.png # ← ละเมิดกฎ icon_pet_P011.png
ui_tooltip_key: pet_P011_tooltip
extra_ability:
id: pet_P011_passive_dodge
description: การหลบหลีกของเพื่อนร่วมทาง +1%
ใน 10 แบบ ที่ปกติคือ P003·P004·P005·P006·P008·P012 หกแบบ ที่ก้ำกึ่งเพราะความสามารถซ้ำคือ P007 หนึ่งแบบ และที่ล้มเหลวเพราะล่วงล้ำไวต์ลิสต์และละเมิดการตั้งชื่อคือ P009·P010·P011 สามแบบ ทั้งที่ตั้งกฎข้อ 4 ไว้แล้ว AI ก็ยังลอกความสามารถร่วมมาใน P007 (ตัวเลือกที่ปลอดภัยที่สุด) และทั้งที่ตั้งกฎข้อ 2 ไว้แล้ว ก็ยังแตะแอนิเมชันโครงร่างใน P010 ถึงจะระบุข้อจำกัดชัดเจน ผลผลิตจากการปั๊มจำนวนมากก็ยังมีสัดส่วนหนึ่งที่หลุดออกมา นี่คือความเป็นจริง ขั้นตอนถัดไปจึงจำเป็น
แทนที่คนจะนั่งดู 10 แบบทีละตัวด้วยสายตา เราจะรัน lint กฎ lint ดึงมาจากไวต์ลิสต์ของเทมเพลตในขั้นที่ 1 และกฎการตั้งชื่อจากหัวข้อ 11.1 โดยตรง รายการที่ตรวจมีสี่อย่าง
flowchart TD
A[อินสแตนซ์ yaml 10 แบบ] --> B{ฟิลด์ template
มี & ใช้ได้?}
B -->|ไม่มี/พิมพ์ผิด| F[REJECT: อ้างอิงเทมเพลตผิด]
B -->|OK| C{ฟิลด์ overrides อยู่
ในไวต์ลิสต์?}
C -->|ล่วงล้ำฟิลด์นอก| F2[REJECT: ละเมิดไวต์ลิสต์]
C -->|OK| D{pet_id·ui_icon
ผ่านกฎการตั้งชื่อ?}
D -->|ละเมิด| F3[REJECT: ละเมิดกฎการตั้งชื่อ]
D -->|OK| E{extra_ability ซ้ำ
กับ shared?}
E -->|ซ้ำ| W[WARN: ตรวจทานความสามารถซ้ำ]
E -->|ไม่ซ้ำ| P[PASS]
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class A data;
class B,C,D,E code;
class P pass;
class F,F2,F3,W fail;
หากอินสแตนซ์แต่ละแบบผ่านครบทั้งสี่ด่าน ก็เป็น PASS หากติดกลางทางก็ตกเป็น REJECT หรือ WARN เมื่อสรุปผลการตรวจสอบจริงเป็นตารางได้ดังนี้
| pet_id | template | ไวต์ลิสต์ | การตั้งชื่อ | ความสามารถซ้ำ | ผลตัดสิน |
|---|---|---|---|---|---|
| pet_P003 | OK | OK | OK | ไม่ซ้ำ | PASS |
| pet_P004 | OK | OK | OK | ไม่ซ้ำ | PASS |
| pet_P005 | OK | OK | OK | ไม่ซ้ำ | PASS |
| pet_P006 | OK | OK | OK | ไม่ซ้ำ | PASS |
| pet_P007 | OK | OK | OK | ซ้ำ | WARN |
| pet_P008 | OK | OK | OK | ไม่ซ้ำ | PASS |
| pet_P009 | OK | OK | ละเมิด | — | REJECT |
| pet_P010 | OK | ล่วงล้ำ | — | — | REJECT |
| P011 | OK | OK | ละเมิด | — | REJECT |
| pet_P012 | OK | OK | OK | ไม่ซ้ำ | PASS |
PASS 6, WARN 1, REJECT 3 ตัว WARN สามารถกู้กลับมาได้ด้วยการเปลี่ยนความสามารถหนึ่งบรรทัด (P007) ส่วน REJECT 3 แบบนั้นทิ้งไป
อัตราการทิ้ง ของหนึ่งรอบนี้คือ REJECT 3 / ทั้งหมด 10 = 30% หากนับรวม WARN เป็น 'สิ่งที่ต้องแก้' ด้วย อัตราการแก้ไขก็เป็น 40% ตัวเลขนี้คือดัชนีสุขภาพของสายการผลิตแบบปั๊มจำนวนมาก หากอัตราการทิ้งเป็น 30% นั่นหมายความว่าเพื่อให้ได้เพ็ต 50 แบบ ต้องสร้างประมาณ 72 แบบ (50 / 0.7 ≈ 71.4) เพราะการสร้างนั้นต้นทุนต่ำ การเผื่อเกินขนาดนี้จึงรับไหว แต่หากอัตราการทิ้งไม่ลดลงแม้ทำซ้ำหลายรอบ นั่นคือสัญญาณว่าข้อจำกัดของพรอมต์ยังไม่พอ
ด้วยเหตุนี้ เราจึงป้อนเหตุผลของการทิ้งกลับเข้าไปในพรอมต์ ผมรวบรวมเหตุผลของ REJECT 3 แบบ (ขาดการตั้งชื่อ ล่วงล้ำไวต์ลิสต์ ละเมิดกฎไอคอน) แล้วเติมเข้าในการขอใหม่ทีละบรรทัด
[กฎเพิ่มเติมในการขอใหม่]
7. pet_id ต้องขึ้นต้นด้วยคำนำหน้า 'pet_' เสมอ (แบตช์ก่อนหน้า P011 ขาดไป)
8. ui_icon ต้องเป็นรูปแบบ icon_<pet_id>.png โดยไม่มีข้อยกเว้น (ห้ามแปรรูปแบบเช่น pet11_icon.png)
9. ห้ามใส่ shared_animations / skeleton / bt_ref ลงใน overrides เด็ดขาด
หากต้องการเปลี่ยนการเคลื่อนไหว ให้แสดงผ่าน extra_ability เท่านั้น (กรณี P010)
หลังเติมสามบรรทัดนี้แล้วรันแบตช์ถัดไป 10 แบบ REJECT ลดจาก 3 เหลือ 1 อัตราการทิ้ง 30% → 10% การป้อนกลับที่ยกระดับเหตุผลของการทิ้งให้กลายเป็นกฎนี้ คือกลไกที่ดึงคุณภาพของการปั๊มจำนวนมากให้สูงขึ้นทุกรอบ คนไม่ต้องตรวจสอบ 50 แบบทุกครั้ง แต่ทำเพียงงานเดียวคือย้ายเหตุผลของการทิ้งไปเป็นกฎหนึ่งบรรทัด
พาหนะเรียบง่ายกว่าเพ็ตไปอีกขั้น ไม่มีทั้งสกิลและ BT (BehaviorTree, ต้นไม้พฤติกรรม) มีแต่ข้อมูลอย่างพารามิเตอร์การเคลื่อนที่และเงื่อนไขว่าต่อสู้ได้หรือไม่ ดังนั้นอินสแตนซ์พาหนะจึงเป็นแถวหนึ่งในตารางโดยพฤตินัย
# อินสแตนซ์ที่อิงจาก mount_template_equine.yaml
mount_id: mount_M005
template: mount_template_equine
overrides:
visual_skin: horse_white
movement:
run_speed: 7.0
sprint_speed: 12.0
combat:
allow_combat: false # ใช้ขณะต่อสู้ไม่ได้
dismount_on_damage: true
ui_icon: icon_mount_M005.png
lint ของการปั๊มพาหนะสั้นกว่า นอกจากการตั้งชื่อ การอ้างอิงเทมเพลต และไวต์ลิสต์แล้ว เพียงตรวจ 'พารามิเตอร์ movement อยู่ในช่วงที่อนุญาตหรือไม่' (เช่น sprint_speed มากกว่า walk_speed หรือไม่ ไม่เกินเพดานหรือไม่) ก็พอ เป็นการใช้สายการผลิตที่สร้างไว้กับเพ็ตตามเดิม เพียงแต่ลดจำนวนด่านลง การเพิ่มฟังก์ชันต่อสู้ให้พาหนะต้องระมัดระวัง ทันทีที่เปิด allow_combat เป็น true ความซับซ้อนของเกมจะเพิ่มเป็นสองเท่า และต้องตรวจสอบความขัดแย้งกับระบบเพ็ตและตัวละครใหม่ทั้งหมด
ผมเปรียบเทียบกรณีที่นำรูปแบบของตัวละครมาใช้กับเพ็ตและพาหนะเต็มรูปแบบ กับกรณีที่ทำให้เรียบง่ายด้วยเทมเพลต-อินสแตนซ์ ในโปรเจกต์ A ของผู้เขียน ในจำนวนค่าด้านล่าง จำนวนเวลาและทรัพยากรเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ส่วนอัตราการทิ้งและอัตราการใช้ทรัพยากรร่วมเป็นสัดส่วนที่อิงทิศทางจากการวัดจริง
| รายการ | ใช้เต็มรูปแบบ | เทมเพลต-อินสแตนซ์ |
|---|---|---|
| เวลาทำงานทรัพยากรเพ็ต 1 แบบ | 1\~2 สัปดาห์ (การประมาณของผู้เขียน) | 3\~5 วัน (การประมาณของผู้เขียน) |
| จำนวนทรัพยากรในไลบรารีเพ็ต | ประมาณ 2,000 (การประมาณของผู้เขียน) | ประมาณ 600 (ลดลง 70%) |
| สัดส่วนทรัพยากรใหม่ต่ออินสแตนซ์ 1 แบบ | 100% | ประมาณ 10% |
| อัตราการทิ้งในการปั๊มแบตช์แรก | — | 30% (ทิศทางจากการวัดจริง) |
| อัตราการทิ้งหลังป้อนกลับ | — | 10% (ทิศทางจากการวัดจริง) |
| ความรู้สึกของผู้ใช้ (ความหลากหลายของเพ็ต) | เป็นเกณฑ์ | แทบไม่ต่างกัน |
ตัวอย่างและการวัด. ตารางข้างต้นเป็นการสังเกตเพ็ต 1 สายในโปรเจกต์เดียว (โปรเจกต์ A) ในสภาพแวดล้อมของผู้เขียน (n=1 สาย) 'ลดลง 70%' และ 'ประมาณ 10%' ไม่ใช่การวัดอิสระ แต่เป็น สัดส่วนทางคณิตศาสตร์ ที่ได้จากจำนวนทรัพยากรที่ประมาณไว้ในแถวเดียวกัน (ประมาณ 2,000 → ประมาณ 600) ดังนั้นเมื่อค่าสัมบูรณ์ด้านหน้าเป็นเพียงการประมาณ ก็ต้องอ่านเปอร์เซ็นต์นี้เป็นการประมาณด้วยเช่นกัน อัตราการทิ้ง 30% และ 10% เป็น ทิศทางจากการวัดจริง ที่ได้จากรอบการปั๊มเดียวตั้งแต่แบตช์แรกถึงการป้อนกลับ และไม่ใช่ตัวอย่างจากการวัดซ้ำ อย่านำไปอ้างเป็นหลักฐานการลดต้นทุนของทีมคุณ แต่จงวัดด้วยตัวเองในสายงานของคุณด้วยวิธีเดียวกัน
แถวสุดท้ายคือบทสรุปของทั้งบท แม้จะใช้ทรัพยากรร่วมกัน 90% วัดอัตราการทิ้ง และปั๊มจำนวนมาก ความหลากหลายของเพ็ตที่ผู้ใช้รู้สึกก็แทบไม่ต่างจากการสร้างเต็มรูปแบบ โฟลเดอร์ 4 กิกะไบต์ที่กล่าวไปข้างต้น คือต้นทุนที่ต้องจ่ายเมื่อคัดลอกทรัพยากรสิบสองชุดให้กับรายละเอียดที่ผู้ใช้แยกแยะไม่ออกในที่สุด หากปูสมมติฐานของการปั๊มจำนวนมากไว้ สิ่งที่ลดลงคือต้นทุนการดำเนินงาน ไม่ใช่ประสบการณ์
| กับดัก | วิธีรับมือ |
|---|---|
| ย้ายระบบตัวละครมาใช้กับเพ็ตและพาหนะทั้งดุ้น | ตัดช่อง variant·vfx·sound เหลือการแปรผันสี่ช่อง |
| คัดลอกเพ็ตที่โครงร่างเดียวกันเป็นทรัพยากรอิสระ | เทมเพลต 1 แบบ + อินสแตนซ์ บังคับใช้ร่วมด้วยไวต์ลิสต์ |
| คอมมิตผลผลิตจาก AI โดยไม่ตรวจสอบ | lint 4 ด่าน + วัดอัตราการทิ้ง |
| อัตราการทิ้งไม่ลดลงในแต่ละรอบ | ยกระดับเหตุผลของการทิ้งเป็นกฎของพรอมต์ (ป้อนกลับ) |
| ให้สกิลระดับตัวละครแก่เพ็ต | จำกัด extra_ability สูงสุด 1 ชุดต่ออินสแตนซ์ |
| ให้ฟังก์ชันต่อสู้แก่พาหนะ | allow_combat ต้องระมัดระวัง เตรียมรับความซับซ้อน ×2 |
เพ็ตและพาหนะส่งผลต่อประสบการณ์ผู้ใช้น้อย AI จึงมีอิสระมากกว่าตัวละคร การจับคู่คอนเซปต์เข้ากับเทมเพลตที่เหมาะสม การเสนอความสามารถที่เป็นไปได้ และการปั๊ม yaml ของอินสแตนซ์ ล้วนเป็นงานที่ AI ทำได้รวดเร็ว เพียงแต่หากอิสระมากแล้วตัดการตรวจสอบทิ้ง ของเสีย 30% ที่เห็นข้างต้นก็จะปนเข้าไปในบิลด์ตามเดิม ที่ของคนมีสองอย่าง หนึ่งคือกำหนดเทมเพลตหนึ่งแบบด้วยมือเพื่อตรึงเกณฑ์คุณภาพ สองคืออ่านว่าอะไรถูกคัดออกเพราะอะไร แล้วขัดเกลาข้อจำกัดให้แบตช์ถัดไปหลุดน้อยลง การแบ่งงานที่ AI เติมปริมาณ ส่วนคนกุมเส้นฐานและการปรับแก้นั้น คือสิ่งที่ทำให้ระบบนี้เดินหน้าได้
setup
1. กำหนดโครงร่างร่วม แอนิเมชันร่วม 4 ชุด และความสามารถร่วม 2 ชุดของเพ็ตหนึ่งสาย (เช่น หมาป่า) แล้วบันทึกเป็น pet_template_<สาย>.yaml
2. ระบุไวต์ลิสต์ instance_overridable (ฟิลด์ที่เปลี่ยนได้) ลงในเทมเพลต
3. เตรียม lint 4 ด่าน (การอ้างอิงเทมเพลต / ไวต์ลิสต์ / กฎการตั้งชื่อ / ความสามารถซ้ำ) ด้วยสคริปต์
prompt 4. แนบ yaml ฉบับเต็มของเทมเพลต + กฎการปั๊ม (ห้ามใส่ฟิลด์นอกไวต์ลิสต์ ห้ามความสามารถซ้ำ กฎการตั้งชื่อ) แล้วขออินสแตนซ์ 10 แบบ 5. ตรึงรูปแบบผลลัพธ์เป็น "เฉพาะบล็อก yaml ห้ามอธิบาย"
verify 6. รัน lint เพื่อจำแนก PASS / WARN / REJECT แล้วคำนวณอัตราการทิ้ง 7. รวบรวมเหตุผลของ REJECT แล้วเติมเป็นกฎทีละบรรทัดลงในพรอมต์ จากนั้นรันแบตช์ถัดไป ตรวจสอบว่าอัตราการทิ้งลดลงหรือไม่
หากเป็นเกมที่ทำคนเดียว ก็ไม่จำเป็นต้องมีสคริปต์ lint ก็ได้ ลองเขียน yaml เทมเพลตของเพ็ตหนึ่งสายด้วยมือหนึ่งฉบับ แล้วขอ AI ว่า "จากเทมเพลตนี้ ให้สร้างอินสแตนซ์ 5 แบบที่เปลี่ยนแค่สี ไอคอน และความสามารถ ห้ามแตะโครงร่างและแอนิเมชันร่วมเด็ดขาด" จากนั้นกวาดสายตาดู 5 แบบที่ได้มา ทิ้งเฉพาะตัวที่แตะโครงร่างหรือฝ่าฝืนกฎการตั้งชื่อ แล้วเติมเหตุผลที่ทิ้งลงในคำขอครั้งถัดไปหนึ่งบรรทัด ขอเพียงมีเทมเพลต 1.1 และ 'การป้อนกลับเหตุผลที่ทิ้ง' หัวใจของบทนี้ก็ทำงานได้แม้ไม่มีเครื่องมือ
ผู้อ่านหลัก: นักออกแบบเกม (Game Designer) และ Art Director ที่ทำงานร่วมกับทีมอาร์ต (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านคนเดียว/งานอดิเรก: §12.1.8 「ถ้าทำคนเดียวก็แค่นี้พอ」
ผมยังจำวันที่เอาคอนเซปต์อาร์ต 100 ภาพที่ผลิตด้วย AI ไปติดบนผนังห้องประชุมได้ ในบรรดา 100 ภาพที่พิมพ์ออกมาภายใน 30 วินาที Art Director เลือกไว้เพียง 3 ภาพ ส่วนอีก 97 ภาพถูกทิ้งตรงนั้นทันที บางคนเรียกสิ่งนี้ว่า "ความสูญเปล่า 97%" แต่ถ้าวาดด้วยมือ ศิลปินคงต้องใช้เวลา 2 สัปดาห์กว่าจะไปถึง 3 ภาพนั้น สิ่งที่นับว่าเป็นความสูญเปล่าจึงถูกพลิกกลับด้านไปแล้ว
สิ่งที่บทนี้กล่าวถึงคือวิธีเปลี่ยนการพลิกด้านนั้นให้กลายเป็นแนวทางการดำเนินงาน แก่นของมันมีเพียงบรรทัดเดียว อาร์ต AI ให้ผลิตจำนวนมากได้อย่างเต็มที่ในขั้นที่ย้อนกลับได้ (การสำรวจคอนเซปต์·เท็กซ์เจอร์) แต่หน้าขั้นที่ย้อนกลับไม่ได้ (เรนเดอร์ขั้นสุดท้าย·โมชันแคปเจอร์·การนำเข้าบิลด์) ให้วางด่านที่มนุษย์เฝ้าไว้ ที่ที่ทิ้งได้ก็ทิ้งไป 99 ภาพ ส่วนที่ที่ย้อนกลับไม่ได้ก็ไม่ปล่อยให้ผ่านไปแม้แต่ภาพเดียวโดยไม่ตรวจ วิธีใช้เครื่องมืออาร์ตนั้นมีอยู่มากเพียงพอในหนังสือเล่มอื่นแล้ว บทนี้จึงโฟกัสเฉพาะที่ ตำแหน่งที่จะเสียบเครื่องมือเหล่านั้นเข้าไปในไปป์ไลน์ของนักออกแบบเกมอย่างปลอดภัย เท่านั้น
เส้นทางที่อาร์ตแอสเซตเดินทางจากคอนเซปต์ไปจนถึงในเกมมี 7 ขั้น หากย้ายสายงานแอสเซตตัวละครของโปรเจกต์ของผู้เขียน (ต่อไปนี้เรียกว่า "โปรเจกต์ A") มาตรงๆ จะได้ดังนี้ สิ่งสำคัญไม่ใช่จำนวนขั้น แต่เป็น เส้นแบ่งระหว่างย้อนกลับได้/ย้อนกลับไม่ได้ ที่พาดผ่านตรงกลาง
flowchart TB
subgraph 가역["ย้อนกลับได้ — ทิ้งแล้วต้นทุน 0 (ใช้ AI ผลิตจำนวนมากเชิงรุก)"]
direction LR
C1["1 คอนเซปต์
ภาพประกอบ 2D"] --> C2["2 โมเดลชีต
ด้านหน้า·ข้าง·หลัง"]
C2 --> C3["3 โมเดลลิง 3D"]
C3 --> C4["4 เท็กซ์เจอร์
ผลิตวัสดุจำนวนมาก"]
end
가역 -.->|"ด่านย้อนกลับไม่ได้
ต้องผ่านการตรวจสอบโดยมนุษย์"| 비가역
subgraph 비가역["ย้อนกลับไม่ได้ — จะย้อนต้องทำใหม่·อัดเสียงใหม่·แจกจ่ายใหม่"]
direction LR
I5["5 ริกกิง·สกินนิง"] --> I6["6 แอนิเมชัน
โมชันแคปเจอร์"]
I6 --> I7["7 รวมเข้าในเกม
เรนเดอร์ขั้นสุดท้าย·เผยแพร่ live"]
end
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
class C1,C2,C3,C4 ai;
สี่ขั้นทางซ้าย (คอนเซปต์\~เท็กซ์เจอร์) คือขั้นที่ ย้อนกลับได้ ต่อให้ผลิตคอนเซปต์ 100 ภาพแล้วทิ้งไป 97 ภาพ สิ่งที่เสียไปก็มีเพียงต้นทุนโทเค็นเท่านั้น และต่อให้สร้างเท็กซ์เจอร์ใหม่ห้าครั้งก็แค่เขียนทับไฟล์เดิมก็จบ ช่วงนี้จึงเป็นตำแหน่งที่ AI การผลิตจำนวนมากให้ ROI (Return on Investment, ผลตอบแทนเทียบกับเงินลงทุน) สูงที่สุด เครื่องมือผลิตจำนวนมากมี Stable Diffusion (SDXL)/ComfyUI ที่โฮสต์ด้วยตนเองเป็นแกนหลัก เหตุผลคือการปกป้อง IP — รันบนเครื่องโลคอลโดยไม่อัปโหลดทรัพย์สินขึ้นบริการปิดภายนอก และสามารถควบคุมความสอดคล้องของบุคคลคนเดียวกันในการสร้างซ้ำทุกครั้งได้ด้วย LoRA ที่ปรับแต่ง (fine-tuning) ด้วยตัวละคร และ ControlNet ส่วนเครื่องมือแบบปิด (เช่น Midjourney) ใช้แบบจำกัดเฉพาะตอนวางมูดบอร์ดเริ่มต้นให้เร็วเท่านั้น แล้วการผลิตจำนวนมากจริงที่ต้องการความสอดคล้อง·การควบคุมการทำซ้ำ จะดึงกลับมาทำใน SD/ComfyUI
สามขั้นทางขวา (ตั้งแต่ริกกิงเป็นต้นไป) คือขั้นที่ ย้อนกลับไม่ได้ โมชันแคปเจอร์ต้องผูกกับตารางสตูดิโอ·นักแสดง และเมื่อเรนเดอร์ขั้นสุดท้ายขึ้นบิลด์และเผยแพร่ live แล้ว ความทรงจำของผู้เล่นและปฏิกิริยาของคอมมิวนิตีก็จะตามมา เมื่อข้ามไปแล้วครั้งหนึ่ง ต้นทุนการย้อนกลับจะมากกว่าต้นทุนการสร้าง เพราะฉะนั้นบนเส้นแบ่งจึงตั้ง ด่านที่มนุษย์เฝ้า ไว้ ต่อให้ AI ผลิตจำนวนมากในช่วงย้อนกลับได้มากเพียงใด แอสเซตที่จะข้ามไปยังช่วงย้อนกลับไม่ได้ก็มีแต่ที่ผ่านการตรวจสอบโดยมนุษย์แล้วเท่านั้น
ภาพหนึ่งภาพนี้คือโครงของบทนี้ คำถามที่ว่า "จะใช้ AI ในงานอาร์ตมากแค่ไหน" แท้จริงแล้วคือคำถามที่ว่า "งานนี้อยู่ฝั่งไหนของเส้นแบ่ง"
ผมจะแสดงขั้นแรกของช่วงย้อนกลับได้ คือการผลิตคอนเซปต์จำนวนมาก ให้เห็นครบหนึ่งรอบ ถ้าเขียนแค่เชิงนามธรรมว่า "AI ผลิตคอนเซปต์" ก็จะไม่รู้ว่าอะไรออกมาจริงๆ และอะไรถูกทิ้ง ด้านล่างคือการจำลองเซสชันที่ผลิตคอนเซปต์ NPC อาวุโสของกิลด์นักวิชาการในโปรเจกต์ A อย่างซื่อตรง พรอมต์สามารถคัดลอกไปใช้ได้เลย และผลลัพธ์เป็นการเรียบเรียงใหม่จากเซสชันจริง
ตรงนี้มีตำแหน่งที่คนผิดพลาดบ่อยที่สุด คือการเริ่มพรอมต์ด้วย "คำบรรยายภาพ" หลักการที่ atom จากฟีดแบ็กของบริษัท image_prompt_design_intent_first ตรึงเป็นกฎไว้นั้นตรงกันข้าม — พรอมต์รูปภาพก็ต้องเอาเจตนาออกแบบขึ้นก่อน ไม่ใช่การไล่เรียงคำคุณศัพท์เรื่องรูปลักษณ์ แต่ต้องเอาสิ่งที่ว่าตัวละครนี้แบกหน้าที่·เนื้อเรื่องอะไรในเกมขึ้นไว้ด้านหน้า
# concept_brief_scholar_senior.yaml — อินพุตสำหรับผลิตคอนเซปต์จำนวนมาก
asset_id: npc_scholar_senior_01
role: นักวิชาการอาวุโสของกิลด์ — บุคคลแรกที่สังเกตเห็นการอ่อนกำลังของผนึก
function: NPC ปล่อยเควสต์หลัก (แหล่งข้อมูลที่ผู้เล่นต้องไว้วางใจ)
narrative_seed:
- คนที่บันทึกชีพจรของผนึกในหอระฆังมา 30 ปี
- ซ่อนอารมณ์ไว้หลังตัวเลข (โทน scholarly_strict)
style_anchor: semi-realistic, painted, แฟนตาซีเอเชียตะวันออก # ตรึงไว้ที่วิสัยทัศน์ L0
forbidden: สไตล์ anime · เสื้อผ้าสมัยใหม่ · เสื้อคลุมจอมเวทแฟนตาซีทั่วไป
function และ narrative_seed ต้องมาก่อนรูปลักษณ์ อินพุตต้องถือ "ทำไมตัวละครนี้จึงต้องมีหน้าตาแบบนี้" ไว้ จึงจะตัดสินได้ว่า "ทำไมอันนี้ถึงดีกว่า" จากผลการผลิตจำนวนมาก
จาก concept_brief ที่แนบมา ให้สร้างข้อเสนอทิศทางคอนเซปต์ตัวละคร 6 แบบ
นี่เป็นการผลิตจำนวนมากเพื่อการสำรวจ — ไม่ใช่ผลงานขั้นสุดท้าย แต่เป็นตัวเลือกที่ Art Director จะเลือก
กฎ:
1) แปล function และ narrative_seed ให้เป็นภาพ ห้ามแค่ทำให้สวยอย่างเดียว
(เช่น "ซ่อนอารมณ์ไว้หลังตัวเลข" → แสดงออกผ่านสีหน้า·พร็อพ·ท่าทางอย่างไร)
2) ห้ามออกนอก style_anchor รายการ forbidden ห้ามเด็ดขาด
3) ทั้ง 6 แบบต้องแตกต่างกันให้เพียงพอ ภาพ 6 แบบที่ต่างกันเพียงเล็กน้อยไม่มีคุณค่าในการสำรวจ
4) แต่ละแบบให้แนบเหตุผลของเจตนาหนึ่งบรรทัด (ทำไมทิศทางนี้จึงเข้ากับ function)
5) แบบใดที่เจ้าสงสัยว่า 'อาจขัดกับ style_anchor' ให้ทำเครื่องหมายแยกไว้
รูปแบบผลลัพธ์: (เลขแบบ | ภาพหลัก | เหตุผลของเจตนา | สงสัยว่าขัด) ห้ามอธิบายนอกเหนือจากนี้
ในพรอมต์นี้ สัดส่วนของคำบรรยายภาพแทบไม่มีเลย ส่วนใหญ่ถูกใช้ไปกับการล็อกรูปแบบผลลัพธ์และตรึงข้อห้ามไว้ เพราะถ้าให้อิสระแก่ AI มันจะถดถอยกลับสู่ค่าเฉลี่ยของข้อมูลฝึก — จอมเวทแฟนตาซีทั่วไป หน้าที่แปลเจตนา การตรึง style_anchor การบังคับความหลากหลายของ 6 แบบ และทางออกที่ว่า "ถ้าขัดให้แจ้ง" คือสลักสี่ตัวที่กั้นการถดถอยนี้ไว้
| แบบ | ภาพหลัก | เหตุผลของเจตนา | ขัด |
|----|--------------------------------------|-----------------------------------|------|
| 1 | ติดแว่นขยายทองเหลืองถาวร, นิ้วเปื้อนหมึก | การสังเกตคือตัวตน — เครื่องมือกลายเป็นร่างกาย | |
| 2 | นักวิชาการแก่ไร้สีหน้า, มือถือม้วนตารางตัวเลข | ซ่อนอารมณ์ไว้หลังตัวเลข = ไร้สีหน้า+บันทึก | |
| 3 | ฉากหลังหอระฆัง, แสงชีพจรผนึกที่เย็นลงด้านหลัง | พื้นที่ทำงานอธิบายตัวบุคคล | |
| 4 | เสื้อคลุมปักดาวหรูหรา, มือถือลูกแก้วพลังเวท | เน้นความสง่างาม | ★ |
| 5 | ตาข้างหนึ่งเป็นตาเทียม, ร่องรอยของการสังเกตทั้งชีวิต | ราคาทางร่างกายของหน้าที่ 30 ปี | |
| 6 | นักวิชาการหนุ่มเรียบร้อย, เน้นความหล่อ | ให้ความน่าชอบมาก่อน | ★ |
สิ่งที่มีค่าที่สุดในผลลัพธ์ไม่ใช่ตาราง แต่เป็น สองบรรทัดที่มีเครื่องหมาย ★ แบบที่ 4 (เสื้อคลุมจอมเวทหรูหรา, ลูกแก้วพลังเวท) และแบบที่ 6 (เน้นความหล่อ) AI แจ้งเองว่า "อาจขัดกับ style_anchor·forbidden" ถ้าไม่มีทางออกของกฎข้อ 5 สองแบบนี้คงปะปนเข้ามาในตารางด้วยหน้าตาเหมือนกับอีกสี่แบบ การทำให้ AI ยกมือชี้ตำแหน่งที่น่าสงสัยด้วยตัวเอง — นั่นคือสิ่งที่แบ่งการผลิตจำนวนมากแบบอิสระออกจากการผลิตจำนวนมากแบบควบคุม
เราไม่รับผลลัพธ์นี้มาตรงๆ Art Director เอา brief มาตรวจ 6 แบบหนึ่งครั้ง ในเซสชันนี้จริงๆ การตัดสินแบ่งออกมาดังนี้
forbidden: เสื้อคลุมจอมเวทแฟนตาซีทั่วไป ตรงๆ ตัวละครนี้ไม่ใช่คนที่ใช้เวทมนตร์ แต่เป็นคนที่ สังเกต·บันทึก พลังเวท แปล function ผิดnarrative_seed: ราคาทางร่างกายของหน้าที่ 30 ปี พลังโน้มน้าวของ NPC ตัวนี้มาจากการสึกกร่อนของ "คนที่ทำมานาน" ใบหน้าหนุ่มสะอาดสะอ้านบั่นทอนเนื้อเรื่องตรงนี้การทิ้ง 2 แบบไม่ใช่ความสูญเสีย ถ้าวาดด้วยมือ คงต้องใช้เวลาหลายวันกว่าจะรู้ว่าสองทิศทางนี้ผิด แต่การผลิตจำนวนมากคลี่ 6 แบบออกพร้อมกันแล้วคัดออกได้ภายในหนึ่งชั่วโมง
รวมทิศทางของแบบที่ 1 (แว่นขยายกลายเป็นร่างกาย) และแบบที่ 5 (ตาเทียม) เข้าด้วยกัน
- รวมแว่นขยายทองเหลือง + ตาเทียมข้างหนึ่งไว้ในบุคคลคนเดียว
- การกดอารมณ์ (scholarly_strict): สีหน้าว่างเปล่า บอกเล่าหน้าที่ผ่านพร็อพเท่านั้น
- ยืนยัน forbidden อีกครั้ง: เสื้อคลุมจอมเวท·ลูกแก้วพลังเวท·การเน้นความหล่อ ห้ามทั้งหมด
นี่เป็นขั้นการสร้าง 'ตัวเลือกขั้นสุดท้าย 1 แบบ' ที่ Art Director จะส่งต่อไปปรับแต่งด้วยมือ
AI ตอบกลับมาด้วยทิศทางเดียวที่รวมแว่นขยายและตาเทียมไว้ในนักวิชาการแก่คนหนึ่ง และภาพหนึ่งภาพนั้นก็ส่งต่อไปยังโต๊ะของคอนเซปต์อาร์ทิสต์เพื่อปิดงานด้วยมือ หนึ่งรอบของ ผลิตจำนวนมาก (6 แบบ) → ทิ้ง (2 แบบ) → ลู่เข้า (1 แบบ) → ปิดงานด้วยมนุษย์ ปิดลงที่นี่ สิ่งที่ AI สร้างไม่ใช่แอสเซตขั้นสุดท้าย แต่เป็นความกว้างของตัวเลือกที่ Art Director จะเลือก
หนึ่งรอบนี้คือเกณฑ์ Show ของหนังสือทั้งเล่ม ถ้าไม่ได้ดูสักครั้งจนจบว่า AI พ่นอะไรออกมา อะไรถูกทิ้ง และมนุษย์ปิดงานอะไร ประโยคที่ว่า "ผลิตคอนเซปต์จำนวนมากด้วย AI" ก็เป็นเพียงความว่างเปล่า
ในเซสชันข้างต้น มี 2 แบบจาก 6 แบบที่ถูกทิ้ง หากมองทั้งสายงานคอนเซปต์ การทิ้งจะสะสมมากกว่านั้นมาก จาก 100 ภาพที่ติดบนผนังห้องประชุม ที่ถูกเลือกมี 3 ภาพ
ผมจะจัดการกับอัตราส่วนนี้อย่างซื่อตรง นี่เป็นค่าเชิงทิศทางที่นับเองจากเซสชันคอนเซปต์ช่วงเริ่มนำมาใช้ไม่กี่เซสชัน ไม่ใช่อัตราส่วนของประชากรที่แม่นยำ (การประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ — แกว่งมากตามบุคลิกตัวละคร·คุณภาพ brief) เพราะฉะนั้นจึงควรอ่านในทิศทางที่ว่า "ทิ้งได้อย่างอิสระกว่าตอนทำด้วยมือมาก" ไม่ใช่ "กี่ % กันแน่"
สิ่งสำคัญคืออัตราการทิ้ง 0% ไม่ใช่เป้าหมาย ถ้ากระดาษหนึ่งแผ่นแพง ก็จะขัดเกลาแผ่นเดียวจนสุด ถ้ากระดาษ 100 แผ่นพิมพ์ออกใน 30 วินาที ต่อให้ทิ้งไป 99 แผ่นก็ไม่เป็นภาระ และความกว้างของการสำรวจก็จะกว้างขึ้นตามนั้น อัตราการทิ้งที่สูงขึ้นคือสัญญาณว่าความลึกของการสำรวจกำลังลึกขึ้น แนวทางการดำเนินงานที่พยายามลดอัตราการทิ้งเอง — เช่น แรงกดดันที่ว่า "ของที่ AI ผลิตออกมา ถ้าพอใช้ได้ก็เอาไปใช้เถอะ" — บั่นทอนคุณค่าของการสำรวจไปพร้อมกัน เหตุที่ §12.1.2 ทิ้งแบบที่ 4·6 ได้โดยไม่ลังเล ก็เพราะต้นทุนการทิ้งเป็น 0
อีกตำแหน่งหนึ่งที่ ROI สูงในช่วงย้อนกลับได้ร่วมกับคอนเซปต์ คือเท็กซ์เจอร์ เป็นขั้นการสร้างวัสดุที่จะใส่ลงบนโมเดล 3D ซึ่งตรงนี้ก็แบ่งช่องที่ AI เข้าไปกับช่องที่ deterministic (เชิงกำหนด) รับผิดชอบอย่างชัดเจน
flowchart TD
A["UV unwrap
(มนุษย์)"] --> B["เท็กซ์เจอร์ฐาน
(AI สร้าง หรือ วาด)"]
B --> C["normal·roughness·metallic
สกัดเชิงกำหนด (Materialize ฯลฯ)"]
C --> D["import เข้าเอนจิน +
พรีวิวแสง"]
D --> E{"การตรวจสอบของ Art Director
(ย้อนกลับได้ — สร้างใหม่ได้อิสระ)"}
E -->|"โทนไม่ตรงกัน"| B
E -->|"ผ่าน"| F["ลงทะเบียน asset ID·material key
(ชีตข้อมูล L3)"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class C code;
class B ai;
class A,E human;
class F data;
ที่ AI เข้าไปมีเพียงช่อง เท็กซ์เจอร์ฐาน ช่องเดียว แมป PBR อย่าง normal·roughness·metallic ไม่ปล่อยให้ AI พ่นออกมาต่างกันทุกครั้ง แต่ให้เครื่องมือสกัดเชิงกำหนดรับผิดชอบ เพราะจากเท็กซ์เจอร์ฐานเดียวกันต้องได้แมปเดียวกัน วัสดุจึงจะสอดคล้อง นี่คือการแบ่งหน้าที่แบบเดียวกับที่เครื่องกำเนิดเมืองใน §6.2 ไม่ปล่อยเส้นโค้งรางวัลให้ AI แต่ให้ rulebook (กฎ/คู่มือกฎ) จับไว้ — สิ่งที่รับประกันได้ด้วยเชิงกำหนดให้โค้ดทำ สิ่งที่ต้องสำรวจให้ AI ทำ
แม้แต่เท็กซ์เจอร์ฐานเองก็ไม่ใช่ว่า AI เหมาะกับทุกแอสเซต ในตำแหน่งที่รายละเอียดเล็กน้อยกำหนดตัวตนของเกม เช่น ใบหน้าตัวละคร มือมนุษย์ยังคงมาก่อน เพราะฉะนั้นเมื่อด่านตรวจสอบจับ "โทนไม่ตรงกัน" ได้ จึงไม่ใช่การทิ้งอัตโนมัติ แต่ย้อนกลับไปสร้างใหม่ ทั้งหมดถึงตรงนี้คือฝั่งซ้ายของเส้นแบ่ง — ช่วงย้อนกลับได้ที่ต่อให้หมุนซ้ำกี่ครั้งก็ไม่เสียอะไร
ก่อนข้ามเส้นแบ่งทันที เราตรวจว่าแอสเซตที่ผลิตจำนวนมากในช่วงย้อนกลับได้ขัดกับผลรวมของทั้งเกมหรือไม่ ตรงนี้เป็นตำแหน่งที่รั่วถ้าใช้แต่ตามนุษย์ โค้ดจึงตรวจเป็นด่านแรก
# visual_regression.py — ตรวจจับการเปลี่ยนแปลงนอกเหนือเจตนาเมื่อสลับแอสเซต (โครง)
# อินพุต: asset ID + ภาพเรนเดอร์เงื่อนไขเดียวกันก่อน/หลังการสลับ
# เอาต์พุต: ระดับการเปลี่ยนแปลง (alert ไปยังด่านตรวจสอบโดยมนุษย์)
def compare_renders(asset_id, before_png, after_png, threshold=(1.0, 5.0)):
diff = pixel_diff(before_png, after_png) # นอร์มัลไลซ์ 0~100
if diff > threshold[1]:
return ("BLOCK", f"{asset_id}: เปลี่ยนแปลงมาก {diff:.1f}% — ห้ามเข้าช่วงย้อนกลับไม่ได้ก่อนตรวจสอบ")
elif diff > threshold[0]:
return ("WARN", f"{asset_id}: เปลี่ยนแปลงเล็กน้อย {diff:.1f}% — ต้องยืนยันเจตนา")
else:
return ("PASS", f"{asset_id}: ไม่มีการเปลี่ยนแปลง")
30 บรรทัดนี้จับอุบัติเหตุที่ว่า "พอเปลี่ยนเท็กซ์เจอร์หนึ่งภาพ เงาของตัวละครอื่นกลับพัง" ได้ ก่อน เข้าช่วงย้อนกลับไม่ได้ การออกแบบที่สำคัญคือ BLOCK ไม่ใช่การทิ้งอัตโนมัติ แต่ เป็นเพียงการส่ง alert ไปยังด่านตรวจสอบ — ถ้าโค้ดฆ่าแม้แต่การเปลี่ยนแปลงที่ตั้งใจ (รีดีไซน์) ทิ้งไป ศิลปินจะบอกว่า "ปิดมันซะ" ภายในหนึ่งสองไตรมาส ตัวเลือกที่น่าสงสัยให้เครื่องคัดออก แต่จะข้ามไปสู่ช่วงย้อนกลับไม่ได้หรือไม่ มนุษย์เป็นผู้ตัดสิน
อีกสิ่งที่การตรวจสอบจับคือ ความสอดคล้องของสไตล์ ผลลัพธ์ของ AI ต่างกันเล็กน้อยทุกครั้ง ดังนั้นมนุษย์จึงดูเป็นครั้งสุดท้ายว่าคอนเซปต์·เท็กซ์เจอร์ที่ผลิตจำนวนมากยังรักษาเนื้อสัมผัสของเกมไว้หรือไม่ มีแต่สิ่งที่ผ่านด่านนี้เท่านั้นที่จะข้ามไปยังขั้นย้อนกลับไม่ได้ ได้แก่ ริกกิง·โมชันแคปเจอร์·เรนเดอร์ขั้นสุดท้าย เพราะเมื่อแคปเจอร์โมชันและขึ้นบิลด์ครั้งหนึ่งแล้ว อุบัติเหตุเรื่องความสอดคล้องจะแก้ได้ด้วยการทำใหม่·อัดเสียงใหม่·แจกจ่ายใหม่เท่านั้น
ต่อให้นักออกแบบเกมผลิตคอนเซปต์·เท็กซ์เจอร์จำนวนมากด้วย AI ทีมอาร์ตที่วาดภาพจริงก็เป็นองค์กรแยกต่างหาก แก่นของการทำงานร่วมกันตรงนี้คือการทำให้ ทีมอาร์ตไม่จำเป็นต้องเรียนรู้เครื่องมือ·ธรรมเนียมของทีมออกแบบ อาร์ตไกด์ของโปรเจกต์ A (96_ArtGuide/) แก้เรื่องนี้ด้วยการทำอัตโนมัติ
สิ่งที่ตัดสินใจเรื่องอาร์ต ทีมออกแบบเขียนเป็น md แล้ว _convert_md_to_html.py แปลงเป็น html จากนั้น _SyncToArtRepo.bat push ไปยัง repository อาร์ตแยกต่างหาก ทีมอาร์ตดูเฉพาะ html จาก repo นั้น — ไม่ต้องรู้ทั้งธรรมเนียม md และ SVN ของทีมออกแบบ (แผนภาพไปป์ไลน์อยู่ที่ §12.2.4)
และเอกสารการตัดสินใจนี้แบ่งออกเป็น 7 โดเมน (00_Common·01_Character\~07_Env) แต่ละโดเมนถือกฎสไตล์ของตัวเองแล้วมารวมกันที่ด่านรวม นี่คือ ArtGuide 7 พื้นที่ที่จะกล่าวถึงในบทถัดไป (12.2) แต่ถ้าพูดเฉพาะแก่นไว้ก่อนก็เป็นดังนี้ — ถ้าแบ่ง rulebook สไตล์ออกเป็นลิ้นชัก 7 ช่องแทนที่จะเป็นช่องเดียว พรอมต์ผลิตจำนวนมากของ AI จะถูกหยิบออกจากลิ้นชัก ไม่ใช่ประกอบขึ้นใหม่ในหัวศิลปินทุกครั้ง style_anchor·forbidden ใน §12.1.2 ก็คืออินพุตที่ออกมาจากลิ้นชักนั้นเอง rulebook ต้องแยกออกจากกัน ผลการผลิตจำนวนมากจึงจะไม่ถดถอยกลับสู่ค่าเฉลี่ยแฟนตาซีทั่วไป
แต่นั่นก็ไม่ได้หมายความว่าทุกเกมต้องมีครบ 7 พื้นที่ ถ้าเป็นแนวแคชวล แค่สองช่องคือตัวละคร·สภาพแวดล้อมก็เพียงพอ แบ่งทีละน้อย ส่วนอินเทอร์เฟซให้แคบไว้
ตัวเลขในบทนี้มีเพียงสามชนิด (1) ทิศทาง·อัตราส่วน — "ผลิต 100 ภาพ เลือก 3 ภาพ" เป็นค่าเชิงทิศทางบนพื้นฐานประสบการณ์ของผู้เขียน (ยังไม่ได้ตรวจสอบ) จึงไม่อ่านเป็นค่าสัมบูรณ์ แต่อ่านในทิศทางที่ว่า "ในช่วงย้อนกลับได้ ต้นทุนการทิ้งลู่เข้าสู่ 0" (2) ค่าที่วัดได้ — อัตราการเปลี่ยนแปลงของ visual regression (diff %), จำนวนอุบัติเหตุเรื่องความสอดคล้อง, จำนวนการประมวลผล BLOCK เป็นสิ่งที่ visual_regression.py พ่นออกมาเป็นตัวเลข จึงพูดในที่ประชุมด้วยตัวเลขแทน "ความรู้สึก" ได้ ในทางกลับกัน "อัตราการคงอยู่ (Retention) เพิ่มขึ้น" ไม่ได้ถูกกำหนดด้วยอาร์ตเพียงอย่างเดียว จึงไม่ฟันธงความเป็นเหตุเป็นผล
(3) ความเสี่ยงให้อยู่ในต้นทุนการดำเนินงาน ความเสี่ยงสามอย่างของอาร์ต AI — ลิขสิทธิ์ของข้อมูลฝึก, ความเสียหายต่อความสอดคล้องของสไตล์, งานของศิลปิน — ไม่ได้อยู่นอกการคำนวณ ROI แต่อยู่ข้างใน นโยบายของผู้เขียนคือใช้ AI เชิงรุกในช่วงย้อนกลับได้ ส่วนแอสเซตขั้นสุดท้ายที่ข้ามไปสู่ช่วงย้อนกลับไม่ได้คือการปรับแต่งด้วยมือ และตั้งหลักการให้สัดส่วนผลลัพธ์ AI ของแอสเซตที่เข้าบิลด์โดยตรงเป็น 0 แต่นี่ก็เป็นเพียงนโยบายหนึ่งเท่านั้น — มีทีมที่ใช้เฉพาะโมเดลที่ระบุไลเซนส์ชัดเจนแล้วนำ AI ไปใช้จนถึงแอสเซตขั้นสุดท้ายก็มี นโยบายด้านกฎหมายต่างกันไปในแต่ละบริษัท และหนังสือเล่มนี้ไม่ได้เสนอคำตอบที่ถูกต้อง แต่เสนอวิธีลากเส้นแบ่ง
ในบรรดาความเสี่ยงสามอย่าง สิ่งที่มองข้ามบ่อยที่สุดคือข้อสาม ถ้าไม่ตั้ง AI ไว้ในฐานะ "ตัวช่วยที่ขยายความกว้างของการสำรวจเพื่อเพิ่มสิทธิ์ตัดสินใจของศิลปิน" แต่ตั้งเป็น "เครื่องผลิตจำนวนมากที่มาแทนศิลปิน" เครื่องมือต่อให้สำเร็จในแง่ KPI ก็จะถูกองค์กรปฏิเสธ นี่ก็เป็นตำแหน่งที่ feedback atom design_intent_vs_automation_boundary (เจตนาออกแบบ vs ขอบเขตการทำอัตโนมัติ) ตรึงเป็นกฎไว้ด้วย
| รูปแบบ | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| นำคอนเซปต์ AI ไปบิลด์เป็นแอสเซตขั้นสุดท้ายโดยตรง | ผ่านขั้นย้อนกลับไม่ได้โดยไม่มีการตรวจสอบโดยมนุษย์ | ด่านหน้าเส้นแบ่ง (§12.1.1) |
| เริ่มพรอมต์ด้วยคำบรรยายรูปลักษณ์ | แปล function ผิด — ถดถอยกลับสู่จอมเวทหล่อ | เจตนาออกแบบก่อน (§12.1.2, image_prompt_design_intent_first) |
| 6 แบบที่ผลิตต่างกันเพียงเล็กน้อย | ไม่มีคุณค่าในการสำรวจ ไม่มีอะไรให้ทิ้ง | บังคับความหลากหลาย (§12.1.2) |
| พยายามลดอัตราการทิ้ง | บั่นทอนความลึกของการสำรวจไปด้วย | มองการทิ้งในช่วงย้อนกลับได้เป็นสัญญาณ (§12.1.3) |
| สร้างแมป PBR ของเท็กซ์เจอร์ด้วย AI ด้วย | ความสอดคล้องของวัสดุแกว่งทุกการเรียก | แยกการสกัดเชิงกำหนด (§12.1.4) |
| สลับแอสเซตโดยไม่มี visual regression | การเปลี่ยนแปลงนอกเจตนารั่วเข้าสู่ช่วงย้อนกลับไม่ได้ | ด่าน visual_regression.py (§12.1.5) |
ถ้าทำคนเดียวก็แค่นี้พอ: ไม่ต้องมีทั้งทีมอาร์ตและชีตข้อมูล เลือก NPC หนึ่งตัวจากเกมของคุณ (หรือเกมที่คุณชอบ) แล้วเขียน
functionและnarrative_seedในรูปแบบconcept_briefของ §12.1.2 ก่อนรูปลักษณ์ จากนั้นแปะพรอมต์ผลิต 6 แบบไปตรงๆ แล้วลองรันหนึ่งครั้ง ลองเลือกแบบหนึ่งที่ขัดกับเจตนาจาก 6 แบบที่ออกมา แล้วโต้แย้งว่า "อันนี้แปล function ผิด ทิ้งแล้วทำใหม่" คุณจะรู้สึกได้ด้วยตัวเองว่าการทิ้งในช่วงย้อนกลับได้ไม่ใช่ความสูญเสีย แต่เป็นการสำรวจ
ถ้าเป็นทีม ให้เริ่มด้วยขั้นต่อไปนี้หนึ่งขั้น ลากเส้นแบ่งย้อนกลับได้/ย้อนกลับไม่ได้อย่างชัดเจนหนึ่งเส้น ในไปป์ไลน์ (§12.1.1) ตกลงกันว่าถึงขั้นไหนคือ "ทิ้งได้ก็ 0" และจากตรงไหนคือ "ย้อนกลับแล้วแพง" แล้ววางด่านตรวจสอบโดยมนุษย์ไว้บนเส้นแบ่งนั้น เมื่อลากเส้นแบ่งแล้ว การถกเถียงที่ว่า "จะใช้ AI ถึงไหน" ที่เริ่มจากศูนย์ทุกครั้ง จะเปลี่ยนเป็นการตัดสินครั้งเดียวว่า "งานนี้อยู่ฝั่งไหนของเส้นแบ่ง"
หากสรุปเป็น setup → prompt → verify — setup: นิยามเส้นแบ่งย้อนกลับได้/ย้อนกลับไม่ได้และด่านตรวจสอบในไปป์ไลน์ prompt: ป้อนเจตนาออกแบบก่อนในรูปแบบ §12.1.2 แล้วผลิต 6 แบบโดยบังคับข้อห้าม·ความหลากหลาย·การแจ้งเตือน verify: เลือกการแปล function ผิด 1 แบบในช่วงย้อนกลับได้ด้วยตัวเอง ปิดหนึ่งรอบด้วยการทิ้ง·ขอใหม่ และก่อนเข้าช่วงย้อนกลับไม่ได้ ให้ตรวจการเปลี่ยนแปลงนอกเจตนาด้วย visual_regression.py
รีวิวรวมของวันพฤหัสบดี ตอนที่เราเอาแอสเซตใหม่เจ็ดชิ้นมาแปะไว้บนหน้าจอเดียวกันแล้วมองดู เราทุกคนหัวเราะออกมาพร้อมกัน ตัวละครนักปราชญ์มีเงาทึบที่ขรึมในโทนสีเทา แต่ VFX ของสกิลที่ระเบิดอยู่ข้าง ๆ กลับเป็นสีชมพูสะท้อนแสง ทั้งสองอย่างเป็นการตัดสินใจที่สมบูรณ์แบบในพื้นที่ของแต่ละฝ่าย Character Director ทำตาม _STYLE_GUIDE.md ของตัวเองอย่างเคร่งครัด และ VFX Artist ก็ทำตามข้อกำหนดของผมที่ว่า "ให้เด่นสะดุดตา" อย่างซื่อสัตย์ ไม่มีใครทำผิดเลย แต่พอวางไว้บนหน้าจอเดียวกันแล้ว มันเหมือนเกมสองเกมกำลังตีกันอยู่
ภาพนี้คือเหตุผลที่ต้องแบ่ง ArtGuide ออกเป็น 7 พื้นที่ และในขณะเดียวกันก็เป็นเหตุผลที่ต้องนำ 7 พื้นที่นั้นกลับมารวมกันอีกครั้ง ArtGuide คือรัฐธรรมนูญด้านภาพของเกม เมื่อแบ่งเป็นพื้นที่ ผู้กำกับแต่ละสาขาจะมีอำนาจปกครองตนเองและการตัดสินใจจะเร็วขึ้น แต่ถ้าไม่นำกลับมารวมกันด้วยรีวิวรวม อุบัติเหตุแบบสีชมพูสะท้อนแสงข้างต้นจะสะสมขึ้นทุกไตรมาส นักออกแบบเกมจะลงมือแตะที่จุดใดของสมดุลนี้ คือสาระทั้งหมดของบทนี้
ในรีพอซิทอรีออกแบบของโปรเจกต์ A (MMORPG ที่เน้นมือถือก่อนในโทนแฟนตาซีตะวันออก) ซึ่งผู้เขียนทำงานในตำแหน่งผู้กำกับ มีโฟลเดอร์ชื่อ 96_ArtGuide/ เลข 96 ใส่ไว้เพื่อให้ Art Guide มาอยู่เกือบท้ายสุดตามกฎการเรียงลำดับของรีพอซิทอรี และข้างใต้นั้นแตกออกเป็นเจ็ดโดเมน นี่ไม่ใช่ "โฟลเดอร์อาร์ตของโปรเจกต์" แบบนามธรรม แต่ด้านล่างคือโครงสร้างย่อยจริงของโฟลเดอร์นั้น
แก่นของแผนภาพมีสองอย่าง หนึ่ง เจ็ดโดเมนวางเรียงกันอย่างเท่าเทียมและมีอำนาจปกครองตนเอง ลองนึกถึงสำนักงานที่มีห้องทำงานเจ็ดห้องเรียงกันอยู่บนชั้นเดียวกัน ผู้รับผิดชอบของแต่ละห้องถือสิทธิ์ตัดสินใจในห้องนั้น แต่เมื่อเจอกันที่ทางเดินก็ต้องไม่สูญเสียความรู้สึกว่าเป็นเกมเดียวกัน สอง เหนือขึ้นไปมี 00_Common วางทับอยู่ ข้อตกลงร่วมที่ทั้งเจ็ดห้องต้องทำตาม นั่นคือพาเลตสีรวม มาตรฐานวัสดุ และโทนยุคสมัย ทั้งหมดอยู่ตรงนี้ ส่วน 06_UI เป็นโดเมนเดียวกับมาตรฐานการทำงานร่วมด้าน UI ที่กล่าวถึงในหัวข้อ 9.1.3 ดังนั้นในบทนี้จะเพียงขีดเส้นเขตแดนไว้แล้วผ่านไป
นักออกแบบเกมไม่ได้เข้าไปแทรกแซงในเจ็ดโดเมนด้วยความเข้มเท่ากัน หลักการที่ว่านักออกแบบเกมเป็นผู้ตัดสินเจตนาและเรื่องราว ส่วนอาร์ตเป็นผู้ตัดสินด้านภาพนั้นเหมือนกันในทุกโดเมน แต่เจตนาจะลากด้านภาพไปได้ไกลแค่ไหนนั้นต่างกันในแต่ละโดเมน
| พื้นที่ | การมีส่วนร่วมของนักออกแบบเกม | เส้นที่นักออกแบบเกมไม่ควรล้ำ |
|---|---|---|
| 01_Character | สูง | ถึงคอนเซปต์·บุคลิก·ฝักฝ่าย·บทบาท แต่ไม่ใช่สัดส่วนใบหน้า·ลายเส้นพู่กัน |
| 02_Animation | ปานกลาง | ถึง "ชนิด·การตอบสนอง" ของโมชันสกิล แต่ไม่ใช่การจับจังหวะเฟรม |
| 03_Monster | สูง | ถึงคอนเซปต์ศัตรู·ฝักฝ่าย·ระบบนิเวศ แต่ไม่ใช่รายละเอียดลายเกล็ด |
| 04_NPC | สูง | ถึงบทบาท·ความสัมพันธ์·voice_profile แต่ไม่ใช่งานปักบนเสื้อผ้า |
| 05_VFX | ต่ำ | ถึง "ยิงช้า, ระเบิดใหญ่, สีม่วง" แต่ไม่ใช่จำนวนพาร์ติเคิล |
| 06_UI | สูง | ถึงโครงสร้างข้อมูล·ลำดับความสำคัญ (9.3) แต่ไม่ใช่ระยะห่างระดับพิกเซล |
| 07_Environment | ปานกลาง | ถึงบรรยากาศ·เจตนาของแลนด์มาร์ก แต่ไม่ใช่จำนวนพอลิกอนของต้นไม้ |
ช่องขวาคือเนื้อหาจริงของตารางนี้ แม้แต่ในโดเมนที่เขียนว่าการมีส่วนร่วม "สูง" ก็ยังมีเส้นที่นักออกแบบเกมล้ำไม่ได้ คอนเซปต์ตัวละครต้องลากให้แรง แต่ถ้าแตะไปถึงสัดส่วนใบหน้าด้วย ในวินาทีนั้นอำนาจปกครองตนเองของ Character Director ก็พังลง และเส้นแบ่งระหว่างสูงกับต่ำเองก็สั่นไหวไปตามแนวเกม ถ้าเป็นเกมสยองขวัญ VFX คือแก่นของความน่ากลัว การมีส่วนร่วมของนักออกแบบเกมจึงสูงขึ้น ถ้าเป็นเกมพัซเซิลแบบแคชวล การมีส่วนร่วมด้านตัวละครกลับลดลง ตารางข้างต้นเป็นเกณฑ์ตามแนวเกมของโปรเจกต์ A ไม่ใช่กฎสากล
แต่ละโดเมนดำเนินงานด้วยชุดเอกสารมาตรฐาน ลองดูองค์ประกอบไฟล์จริงของโดเมน 01_Character/
01_Character/
├── _STYLE_GUIDE.md — สไตล์รวมของตัวละคร (รัฐธรรมนูญ)
├── _COLOR_PALETTE.md — ไกด์สี·วัสดุ
├── _PROPORTION_REFERENCE.md — กฎสัดส่วน·เงาทึบ
├── _DO_AND_DONT.md — อนุญาต·ห้าม
├── individual/ — ชีตของตัวละครแต่ละตัว
│ ├── K_001_director.md
│ ├── K_007_scholar.md
│ └── ...
└── _REVIEW_LOG.md — ประวัติการตรวจสอบ
_STYLE_GUIDE.md คือรัฐธรรมนูญของโดเมน ชีตตัวละครรายตัว (individual/) ล้วนแปรผันอยู่บนรัฐธรรมนูญนี้ ถ้ารัฐธรรมนูญสั่นไหว ตัวละครทุกตัวข้างใต้ก็สั่นไหว ดังนั้นไฟล์เพียงแผ่นเดียวนี้จึงเป็นเอกสารที่ถูกตรวจสอบบ่อยที่สุดในโดเมน โครงมีดังนี้
---
title: 01_Character Style Guide
layer: L1
---
## 1. โทน
- บรรยากาศแฟนตาซีเกาหลีก่อนการปฏิวัติอุตสาหกรรมในศตวรรษที่ 19
- สัดส่วนสมจริง (7~7.5 หัว, ห้ามดีฟอร์เม)
## 2. สี
- ความอิ่มตัว: ปานกลาง (ราว 60~70% ของภาพถ่ายจริง)
- พาเลตหลัก: สืบทอดจาก 00_Common
- สีแอ็กเซนต์ของแต่ละตัวละคร (1~2 สี)
## 3. กฎเครื่องแต่งกาย
- แยกเครื่องแต่งกายตามฝักฝ่าย (นักปราชญ์ → สีเทา + แอ็กเซนต์สีม่วง)
- รายละเอียดเครื่องแต่งกายตามอาชีพ·ชนชั้น
## 4. DO
- ระบุตัวตนได้ว่าใครเป็นใครจากเงาทึบเพียงอย่างเดียวที่ระยะ 5m
- แสดงอัตลักษณ์ของฝักฝ่ายด้วยภาพ
## 5. DON'T
- สไตล์อนิเมะญี่ปุ่น
- องค์ประกอบที่ไม่ใช่ละครย้อนยุค (เครื่องแต่งกาย·พร็อพสมัยใหม่)
- ความอิ่มตัวมากเกินไป
ตรงนี้มีบรรทัดหนึ่งที่สำคัญ "พาเลตหลัก: สืบทอดจาก 00_Common" ในหัวข้อ ## 2. สี เป็นการระบุชัดว่าโดเมนตัวละครไม่ได้กำหนดสีด้วยตัวเอง แต่รับมรดกจากข้อตกลงร่วมระดับบน บรรทัดเดียวนี้คือกลไกที่ปิดกั้นอุบัติเหตุสีชมพูสะท้อนแสงในบทนำเชิงโครงสร้าง หาก _STYLE_GUIDE.md ของทุกโดเมนสืบทอด 00_Common อย่างน้อยในเรื่องสี การปะทะของสีก็จะถูกตัดออกตั้งแต่ขั้นรัฐธรรมนูญ
ตรงนี้คือปัญหาที่เป็นรูปธรรมที่สุดที่โปรเจกต์ A ชนเข้าจริง ทีมอาร์ตไม่อ่าน Markdown พูดให้ตรงคือ ต้องไม่บังคับให้พวกเขาอ่าน ต้นทุนในการฝึกให้อาร์ติสต์เรียนรู้ git diff, frontmatter และลำดับชั้นของหัวข้อ Markdown แทบจะมากกว่าประสิทธิภาพการทำงานร่วมที่ได้จากการเรียนรู้นั้นเสมอ ในวินาทีที่ยื่นเครื่องมือของทีมออกแบบให้ทีมอาร์ตตรง ๆ การทำงานร่วมกลับช้าลง
ดังนั้นไปป์ไลน์ของโปรเจกต์ A จึงสรุปได้ด้วยบรรทัดเดียวว่า "ทีมออกแบบตัดสินด้วย md ส่วนทีมอาร์ตดูแต่ html"
flowchart LR
A["ทีมออกแบบ: ตัดสิน ArtGuide
(อัปเดต _STYLE_GUIDE.md)"] --> B["_convert_md_to_html.py
(md → html ที่ดูง่าย)"]
B --> C["_SyncToArtRepo.bat
(push ไปยัง SVN อาร์ตแยกต่างหาก)"]
C --> D["ทีมอาร์ต: ดูแต่ html
(เรียนรู้ md 0)"]
D -. ฟีดแบ็ก .-> A
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
class B,C code;
class A,D human;
แก่นคือแอสเซตอัตโนมัติสองตัว _convert_md_to_html.py แปลงไกด์ Markdown ของโดเมนให้เป็น html ที่อาร์ติสต์อ่านได้สบายผ่านเบราว์เซอร์ พาเลตสีถูกเรนเดอร์เป็นชิปสีจริง ส่วน DO/DON'T เรนเดอร์เป็นภาพเปรียบเทียบ _SyncToArtRepo.bat ดันไฟล์ html นั้นเข้าไปยังรีพอซิทอรีเฉพาะอาร์ตแยกต่างหาก ไม่ใช่รีพอซิทอรีออกแบบ เหตุผลของการแยกรีพอซิทอรีนั้นเรียบง่าย ถ้าอาร์ติสต์รับเฉพาะรีพอซิทอรีของตัวเอง ก็จะไม่ถูกเปิดเผยต่อประวัติ md ภายในของทีมออกแบบ ฉบับร่างที่กำลังทำอยู่ และกระบวนการตัดสินใจของโดเมนอื่น สิ่งที่อาร์ติสต์เห็นมีเพียงผลลัพธ์ที่อ่านง่ายของการตัดสินใจที่สรุปแล้วเท่านั้น ต้นทุนการเรียนรู้ Markdown จึงลดลงเหลือ 0
ในโครงสร้างนี้นักออกแบบเกมได้รับความรับผิดชอบเพิ่มมาหนึ่งอย่าง เมื่ออัปเดต md แล้วต้องรันขั้นตอนแปลง·ซิงค์เสมอ ถ้าอัปเดตอย่างเดียวแล้วลืมซิงค์ ทีมอาร์ตก็จะวาดภาพวันนี้ด้วยการตัดสินใจของเมื่อวาน ถ้าช่องว่างหนึ่งช่องระหว่างการตัดสินใจกับการส่งต่อนี้ว่างอยู่ ทั้งการปกครองตนเองและการรวมก็ไร้ความหมาย
ในแนวเดียวกับการทำงานร่วมกับคนที่ไม่ใช่นักออกแบบเกม เมื่อใช้ generative AI ในขั้นคอนเซปต์ มีหลักการหนึ่งที่นักออกแบบเกมต้องยึด ในชื่อข้อตกลงภายในของโปรเจกต์ A คือ image_prompt_design_intent_first แปลออกมาก็คือ "พรอมต์ของภาพก็เขียนเจตนาในการออกแบบก่อน"
เมื่อสำรวจคอนเซปต์ด้วยภาพที่สร้างขึ้น ความล้มเหลวที่พบบ่อยคือพรอมต์ถูกเติมด้วยการบรรยายรูปลักษณ์ภายนอกของผลลัพธ์เพียงอย่างเดียว เช่น "ชายเอเชียวัย 50 สวมเสื้อคลุมสีเทา สีหน้าสงบนิ่ง สไตล์ภาพถ่ายจริง" พรอมต์นี้สร้างภาพได้ก็จริง แต่ไม่ได้บรรจุว่าทำไมต้องเป็นเช่นนั้น ดังนั้นเมื่อ Art Director จะแปรผันภาพนั้นจึงหลงทาง หลักการเจตนาในการออกแบบมาก่อนบังคับให้มีบล็อกเจตนาวางอยู่หน้าพรอมต์
[เจตนาในการออกแบบ]
- บทบาท: เสาหลักทางจิตวิญญาณของฝักฝ่ายนักปราชญ์, เมนเทอร์คนแรกของผู้เล่น
- สิ่งที่ต้องอ่านออก: เงาทึบแบบ "ปัญญาชน·ไม่ใช่สายรบ" แม้ที่ระยะ 5m
- สัญญาณฝักฝ่าย: นักปราชญ์ = สีเทา + แอ็กเซนต์สีม่วง (สืบทอดจาก 00_Common)
- ข้อห้าม: พกอาวุธ, เกราะหรูหรา (จะถูกอ่านผิดเป็นสายรบ)
[พรอมต์]
ชายเอเชียวัย 50 เป็นนักปราชญ์ในเสื้อคลุมสีเทา แอ็กเซนต์สายผูกเสื้อสีม่วง,
ไม่มีอาวุธ, สีหน้าสงบนิ่งและมีความรู้ลึกซึ้ง, แฟนตาซีเกาหลีก่อนศตวรรษที่ 19,
สัดส่วนสมจริง 7.5 หัว, ความอิ่มตัวปานกลาง, ...
เมื่อบล็อกเจตนาอยู่เหนือพรอมต์ ภาพนั้นก็กลายเป็นส่วนหนึ่งของการตัดสินใจ เมื่อคนถัดไปดึงท่าทางอื่นของตัวละครเดียวกัน เขาจะไม่ได้ลอกรูปลักษณ์ภายนอก แต่ตอบสนองเจตนาอีกครั้ง แม้ตอนที่ภาพไม่ถูกใจแล้วทิ้งไป ก็ยังถกเถียงกันได้ว่า "เจตนาข้อใดที่ไม่ถูกอ่านออก" พรอมต์ที่เขียนแต่รูปลักษณ์ภายนอกจะจบลงด้วย "รสนิยมของฉันชอบอันนี้มากกว่า" ไม่ได้ทิ้งหลักฐานไว้ให้ตรวจสอบ แต่พรอมต์ที่เจตนามาก่อนจะทิ้งบันทึกให้พิจารณาได้ว่าอะไรถูกเติมเต็มและอะไรขาดหาย
ตรงนี้การเลือกเครื่องมือเข้ามาเกี่ยวพันกับหลักการเจตนามาก่อน หากต้องการสร้าง "ท่าทางอื่นของตัวละครเดียวกัน" ซ้ำ ๆ ตามเจตนา ลำพังพรอมต์อย่างเดียวไม่พอ การแบ่งงานเครื่องมือของโปรเจกต์ A เหมือนกับ §12.1.1 — งานผลิตหลักใช้ SD (SDXL)/ComfyUI ที่โฮสต์เอง โดยติด LoRA ของตัวละคร (ตรึงใบหน้า·เครื่องแต่งกาย) และ ControlNet (ท่าทาง·เงาทึบ) เพื่อรักษา "การระบุตัวตนได้ที่ระยะ 5m" ที่บล็อกเจตนาเรียกร้องไว้แม้ท่าทางจะเปลี่ยน และปกป้อง IP ไปพร้อมกัน ส่วนเครื่องมือแบบปิด (เช่น Midjourney) ใช้แค่ในขั้นมูดบอร์ดเริ่มต้นเท่านั้น
เพื่อดูว่าการปกครองตนเองของโดเมนและการมีส่วนร่วมของนักออกแบบเกมหมุนไปจริงอย่างไร เราจะตามหนึ่งรอบของ 01_Character ตั้งแต่ต้นจนจบ ต่อไปนี้คือการเรียบเรียงใหม่จากงานจริงที่ผู้เขียนมอบหมายให้ AI ร่างการระบุคอนเซปต์ของตัวละครนักปราชญ์ (K_007_scholar) เราจะไม่สรุปย่อ แต่ใส่ทั้งผลลัพธ์ที่ดูแปร่ง ๆ และการปฏิเสธของผู้เขียนไว้ตามเดิม
① พรอมต์ที่ผู้เขียนโยนให้ AI (ฉบับเต็ม)
คุณคือนักเขียนผู้ช่วยคอนเซปต์ตัวละครของโปรเจกต์เรา จงปฏิบัติตามข้อตกลงด้านล่าง
แล้วเขียนร่างเอกสารระบุคอนเซปต์ของ K_007_scholar เป็น Markdown
ข้อตกลง:
- ปฏิบัติตาม 01_Character/_STYLE_GUIDE.md: แฟนตาซีเกาหลีก่อนศตวรรษที่ 19, 7~7.5 หัว,
ความอิ่มตัวปานกลาง, ห้ามสไตล์อนิเมะญี่ปุ่น
- สีให้สืบทอดจากพาเลต 00_Common และแอ็กเซนต์ของฝักฝ่ายนักปราชญ์เป็นสีม่วง
- ต้องมี voice_profile 5 รายการ (น้ำเสียง/คำติดปาก/คำต้องห้าม/อารมณ์ค่าเริ่มต้น/คำเรียกความสัมพันธ์) ครบถ้วน
- ตัดสินแค่ถึงคอนเซปต์·บุคลิก·ฝักฝ่าย·บทบาทเท่านั้น ส่วนรายละเอียดด้านภาพอย่างสัดส่วนใบหน้า
หรือลายเส้นพู่กันให้เว้นว่างไว้ (เป็นพื้นที่ปกครองตนเองของ Character Director)
- เอาต์พุตเป็นรูปแบบชีตตัวละคร individual/
② ผลลัพธ์ดิบของ Claude (รวมจุดที่ล้มเหลว ไม่แก้ไข)
# K_007 นักปราชญ์ (Scholar)
## บทบาท
เสาหลักทางจิตวิญญาณของฝักฝ่ายนักปราชญ์และเมนเทอร์คนแรกของผู้เล่น
## รูปลักษณ์
- สูง 180cm, 7.5 หัว, รูปร่างผอม
- หนวดเครางามขาว, ริ้วรอยลึก, แววตาเฉียบคม
- เสื้อคลุมสีเทาพร้อมสายผูกเสื้อสีม่วง
- ★ แนะนำ: ดวงตาประกายสีทองอ่อน ๆ เพื่อเน้นความลึกลับ
## บุคลิก
รอบรู้แต่ไม่หยิ่งผยอง เข้มงวดกับศิษย์แต่อบอุ่น
## voice_profile
- น้ำเสียง: ช้าและสงบนิ่ง
- คำติดปาก: "...เป็นเช่นนั้นแล" ลากท้ายประโยคให้ยาว
- อารมณ์ค่าเริ่มต้น: สงบนิ่ง
- คำเรียกความสัมพันธ์: เรียกผู้เล่นว่า "สหายหนุ่ม"
③ การตรวจสอบ·การปฏิเสธของผู้เขียน (ตรงไหนผิดและเพราะอะไร)
ในเอาต์พุตมีข้อบกพร่องสองจุด จุดแรก "★ แนะนำ: ดวงตาประกายสีทอง" ในหัวข้อ ## รูปลักษณ์ นี่คือการก้าวล้ำอำนาจอย่างชัดเจน สีดวงตาเป็นรายละเอียดด้านภาพซึ่งเป็นพื้นที่ปกครองตนเองของ Character Director แต่ AI กลับตอกหมุดมันลงในขั้นคอนเซปต์ ถ้าปล่อยไว้ตามนั้น ผู้กำกับก็จะอย่างใดอย่างหนึ่ง คือพับการตัดสินของตัวเองเพราะ "ฝ่ายออกแบบกำหนดมาแล้ว" หรือไม่ก็เพิกเฉยแล้วเกิดการปะทะกัน จุดที่สอง voice_profile มีแค่ 4 รายการ ไม่ใช่ 5 รายการ "คำต้องห้าม" หายไปทั้งยวง การที่ตัวละครเมนเทอร์จะไม่พูดคำใดเด็ดขาดนั้นสำคัญพอ ๆ กับบุคลิก แต่ AI กลับตกหล่นไป
④ การร้องขอใหม่ (แก้เฉพาะสองจุดอย่างแม่นยำ)
แก้แค่สองอย่าง
1. ลบบรรทัดแนะนำ "ดวงตาสีทอง" ออกจาก ## รูปลักษณ์ สีดวงตาเป็นพื้นที่ตัดสินใจ
ของ Character Director หัวข้อรูปลักษณ์ให้เขียนแค่ถึงเงาทึบ·รูปร่าง·สีฝักฝ่าย
ส่วนสี·วัสดุรายละเอียดให้เว้นว่างไว้เป็น "(Character Director ตัดสิน)"
2. เพิ่มรายการ "คำต้องห้าม" ที่หายไปใน voice_profile จงระบุคำที่ในฐานะ
เมนเทอร์นักปราชญ์จะไม่พูด (คำหยาบ, มุกตลกหยาบคาย, คำสมัยใหม่)
บทเรียนของรอบนี้ไม่ใช่เครื่องมือ แต่เป็นเส้นเขตแดน AI ให้ร่างที่ดูดีอย่างรวดเร็ว แต่ก็ก้าวข้ามเส้นที่นักออกแบบเกมไม่ควรล้ำ (รายละเอียดด้านภาพ) แทน และตกหล่นสิ่งที่ต้องใส่ให้แน่ (คำต้องห้าม) เกณฑ์การตรวจสอบไม่ใช่ "เขียนดีหรือไม่" แต่เป็น "รักษาเส้นเขตแดนของการปกครองตนเองของโดเมนหรือไม่" ตราบใดที่ยังใช้ AI กับคอนเซปต์ตัวละคร การตรวจสอบเส้นเขตแดนนี้คนต้องถือไว้ในมือจนถึงที่สุด
เมื่อเจ็ดโดเมนมีอำนาจปกครองตนเอง ความไม่ลงรอยระหว่างโดเมนก็เกิดขึ้นเหมือนสีชมพูสะท้อนแสงในบทนำ รูปแบบที่ออกมาบ่อยนั้นมีกำหนดไว้แน่นอน
| ประเภทความไม่ลงรอย | ตัวอย่างจริง |
|---|---|
| ความต่างของโทนตัวละคร-สภาพแวดล้อม | ตัวละครขรึม แต่ฉากหลังหรูหราจนไปกันคนละทาง |
| การปะทะของสีตัวละคร-VFX | ตัวละครโทนสีเทา แต่ VFX สกิลเป็นสีชมพูสะท้อนแสง |
| เส้นแบ่ง NPC-Monster คลุมเครือ | เป็น NPC มิตร แต่ถูกอ่านว่าน่าคุกคามเหมือนมอนสเตอร์ |
| สีสันของ UI-ตัวละครไม่ลงรอย | UI โทนเย็น แต่ตัวละครโทนอุ่น |
กลไกที่จับความไม่ลงรอยนี้คือรีวิวรวมสัปดาห์ละ 1 ครั้ง ขั้นตอนเรียบง่าย
รีวิวรวม ArtGuide สัปดาห์ละ 1 ครั้ง (วันพฤหัสบดี)
─────────────────────────────────
1. สุ่มดึงแอสเซตใหม่ของสัปดาห์นั้น 5~10 ชิ้น
2. จัดวางไว้บนหน้าจอเดียวกัน (จำลองในเกม)
3. ผู้กำกับเจ็ดโดเมน + Game Director ตรวจสอบพร้อมกัน
4. พบความไม่ลงรอย → เสริม _STYLE_GUIDE ของโดเมนนั้น
หรือเสริมข้อตกลงระดับบน 00_Common
แก่นอยู่ที่ข้อ 3 และข้อ 4 นั่นคือ การตรวจสอบนั้นผู้กำกับโดเมนทำพร้อมกัน และความไม่ลงรอยที่พบไม่ได้แก้ด้วยการซ่อมแอสเซตแต่ละชิ้นแล้วจบ แต่นำกลับคืนสู่เอกสารไกด์ อุบัติเหตุสีชมพูสะท้อนแสงในบทนำ ถ้าแค่เปลี่ยนแอสเซตชิ้นนั้นเป็นสีเทาแล้วจบ สัปดาห์ถัดไปก็จะเกิดซ้ำเหมือนเดิม แทนที่จะทำเช่นนั้น หากเพิ่มข้อตกลงใน 00_Common ว่า "ความอิ่มตัวของ VFX สกิลต้องอยู่ภายใน +20% ของความอิ่มตัวพาเลตตัวละคร" อุบัติเหตุแบบเดียวกันก็จะถูกปิดเชิงโครงสร้าง รอบนี้ที่สะสมเดือนละ 4 ครั้งคือการ์ดเรลเดียวที่ป้องกันไม่ให้การปกครองตนเองแข็งตัวเป็นไซโล
หากสรุปการเปลี่ยนแปลงที่การแบ่ง 7 พื้นที่นำมาจากประสบการณ์การดำเนินงานของโปรเจกต์ A ได้ดังนี้ ในตัวเลขด้านล่าง จำนวนวันของรอบและเวลานั้นเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ที่อิงประสบการณ์การดำเนินงานของผู้เขียน ไม่ใช่ค่าที่วัดอย่างแม่นยำ จึงควรอ่านเพียงเป็นทิศทางและสัดส่วนคร่าว ๆ ของก่อนและหลังการแบ่งเท่านั้น
| รายการ | ก่อนแบ่ง | หลังแบ่ง | ลักษณะ |
|---|---|---|---|
| รอบการตัดสินด้านอาร์ต | 1\~2 สัปดาห์ | 3\~5 วัน | การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) |
| อุบัติเหตุความสอดคล้องระหว่างพื้นที่ | หลายเคสต่อไตรมาส | ลดลงอย่างเห็นได้ชัด | ทิศทางเท่านั้น |
| เวลาตรวจสอบอาร์ตของ Game Director | หลายชั่วโมงต่อสัปดาห์ | ลดลงมาก | ทิศทางเท่านั้น |
| การออนบอร์ดผู้กำกับพื้นที่ใหม่ | หลายเดือน | ราว 1 เดือน | การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) |
| อัตราการทิ้งแอสเซตอาร์ต | สูง | ลดลง | ทิศทางเท่านั้น |
ถ้าอ่านตารางอย่างซื่อสัตย์ สิ่งที่ยืนยันได้มีเพียงข้อเท็จจริงว่าทุกรายการขยับไปในทิศทางเดียวกันเท่านั้น ผลที่ชัดเจนที่สุดคือเวลาของ Game Director ถูกเรียกคืน ในโครงสร้างที่ไม่มีการปกครองตนเอง การตัดสินด้านอาร์ตทุกอย่างต้องผ่านโต๊ะของ Game Director เพียงคนเดียว เป็นโครงสร้างที่กระดาษกองอยู่บนโต๊ะตัวเดียว พอแบ่งเป็น 7 พื้นที่ กระดาษก็กระจายไปยังเจ็ดโต๊ะ ปริมาณกระดาษรวมเท่าเดิม แต่ไม่มีโต๊ะไหนถล่มลง
ตรงนี้ต้องตัดความเข้าใจผิดที่พบบ่อยที่สุดออกไป การปกครองตนเองไม่ใช่การกระจายความรับผิดชอบ แต่เป็นการจัดสรรเวลาใหม่ การที่ผู้กำกับโดเมนตัดสินพื้นที่ของตัวเองไม่ได้หมายความว่าความรับผิดชอบด้านภาพของทั้งเกมแตกกระจายเป็นเจ็ดเสี่ยง ในรีวิวรวม เจ็ดคนนั้นมารวมตัวกัน ณ ที่เดียวกันอีกครั้ง และความรับผิดชอบด้านภาพสูงสุดก็ยังรวมศูนย์ที่คนเพียงคนเดียวเหมือนเดิม ในวินาทีที่นำการปกครองตนเองมาเป็นข้ออ้างในการเลี่ยงความรับผิดชอบ — วินาทีที่ "นั่นมันนอกโดเมนของฉัน" กลายเป็นคำติดปาก — สีชมพูสะท้อนแสงในบทนำก็จะเข้าไปอยู่ในบิลด์โดยไม่เป็นปัญหาของใครเลย
และมีข้อแม้หนึ่ง การปกครองตนเอง 7 พื้นที่เป็นฟังก์ชันของขนาด ในทีมขนาดเล็ก (\~10 คน) มันกลับเป็นการ over-engineering ถ้าอยู่ในขั้นที่ผู้กำกับคนเดียวสวมหมวกห้าใบ ไกด์โดเมนเจ็ดแผ่นก็ไม่จำเป็น มีไกด์รวมแผ่นเดียวก็พอ การปกครองตนเองจะเริ่มคุ้มค่าก็ต่อเมื่อโต๊ะเริ่มไม่พอเท่านั้น
| รูปแบบ | วิธีแก้ |
|---|---|
| รวมการตัดสินทุกอย่างไว้ที่ Game Director โดยไม่แบ่งพื้นที่ | นำการปกครองตนเอง 7 พื้นที่มาใช้ (แต่เริ่มจากขนาดกลาง (10\~50 คน)) |
| ไม่มี _STYLE_GUIDE ของโดเมน | บังคับให้เขียนรัฐธรรมนูญของแต่ละโดเมนเป็นเกต |
| ไม่มีการตรวจสอบรวมระหว่างพื้นที่ | รีวิวรวมสัปดาห์ละ 1 ครั้ง + นำกลับคืนสู่ไกด์ |
| นักออกแบบเกมตัดสินถึงรายละเอียดด้านภาพ | ระบุแค่ถึงเจตนา·เรื่องราว ส่วนรายละเอียดเป็นของผู้กำกับ |
| การปกครองตนเองแข็งตัวเป็นไซโล | นำกลับคืนสู่ 00_Common ในรีวิวรวม |
| ลืมซิงค์หลังอัปเดต md | ทำให้เป็นนิสัย _convert_md_to_html.py → _SyncToArtRepo.bat |
| พรอมต์ของภาพมีแต่การบรรยายรูปลักษณ์ภายนอก | นำบล็อกเจตนาในการออกแบบมาก่อน (image_prompt_design_intent_first) |
setup — เริ่มจากโดเมนเดียวที่ใช้แรงมากที่สุด (มักเป็น 01_Character) ก่อน สร้าง _STYLE_GUIDE.md (รัฐธรรมนูญ), _COLOR_PALETTE.md, _DO_AND_DONT.md, individual/ ไว้ในโฟลเดอร์โดเมน ในหัวข้อสีให้ใส่บรรทัด "พาเลตหลัก: สืบทอดจาก 00_Common" ไว้ให้แน่นอน
prompt — เมื่อร่างชีตแอสเซตรายตัวด้วย AI ให้ระบุในพรอมต์ว่า (1) ข้อตกลงของ _STYLE_GUIDE.md ของโดเมนนั้น (2) "ตัดสินแค่ถึงคอนเซปต์·เรื่องราว ส่วนรายละเอียดด้านภาพให้เว้นว่างไว้เป็น '(Director ตัดสิน)'" (3) รายการบังคับที่ขาดไม่ได้ (เช่น voice_profile 5 รายการ) ถ้าเป็นพรอมต์ของภาพ ให้เขียนบล็อก [เจตนาในการออกแบบ] ไว้เหนือการบรรยายรูปลักษณ์ภายนอกก่อน
verify — เมื่อได้เอาต์พุตมา ให้ตรวจสอบด้วย "รักษาเส้นเขตแดนของการปกครองตนเองของโดเมนหรือไม่" ไม่ใช่ "เขียนดีหรือไม่" ดูแค่สองอย่าง ① AI ไม่ได้ตัดสินรายละเอียดด้านภาพที่นักออกแบบเกมไม่ควรล้ำแทนใช่ไหม ② ไม่มีรายการบังคับขาดหายไปใช่ไหม จากนั้นชี้เฉพาะจุดที่ผิดเพี้ยนแล้วร้องขอใหม่ ทุกวันพฤหัสบดี รวบรวมแอสเซตใหม่ 5\~10 ชิ้นไว้บนหน้าจอเดียวแล้วดูพร้อมกับผู้กำกับโดเมน ความไม่ลงรอยให้นำกลับคืนสู่เอกสารไกด์ (00_Common หรือ _STYLE_GUIDE ของโดเมน) ไม่ใช่ตัวแอสเซต
ฉบับย่อสำหรับคนเดียว — ถ้าเป็นเกมที่ทำคนเดียว อย่าสร้างเจ็ดโดเมน รวมพาเลตสี·โทนยุคสมัย·DO/DON'T ไว้ใน 00_Common แผ่นเดียว แล้วแบ่งส่วนตัวละคร·สภาพแวดล้อม·VFX ไว้ในนั้นด้วยหัวข้อเท่านั้น เมื่อมอบหมายแอสเซตให้ AI ก็แปะแผ่นเดียวนั้นลงในพรอมต์ทั้งแผ่น แล้วสั่งไปพร้อมกันว่า "ถ้ามีจุดที่ขัดกับไกด์นี้ให้ทำเครื่องหมายไว้" รีวิวรวมก็ทำคนเดียวสัปดาห์ละครั้ง เป็นพิธีกรรม 5 นาทีที่เอาสิ่งที่ทำในสัปดาห์นั้นมาวางบนหน้าจอเดียวแล้วดู ก็เพียงพอ เพียงแต่ไม่มีคนให้แบ่งการปกครองตนเองเท่านั้น ส่วนโครงที่เป็นรัฐธรรมนูญหนึ่งแผ่นกับการจัดเรียงสัปดาห์ละ 1 ครั้งนั้น ในทีมคนเดียวก็คุ้มค่าเช่นเดียวกัน
ช่วงท้ายสปรินต์ คอนเซปต์อาร์ติสต์โยนภาพร่างตัวละครหนึ่งใบมาทางแชตภายในทีม "นี่คือซีเนียร์ของกิลด์นักปราชญ์ใช่ไหม" คนในภาพเป็นชายวัย 30 กว่าสวมเกราะหนัง แต่ในเอกสารออกแบบระบุไว้ว่าเป็นหญิงวัย 40 กว่าสวมชุดคลุมนักปราชญ์สีเทา เมื่อย้อนรอยว่าความคลาดเคลื่อนเกิดขึ้นตรงไหน พบว่าข้อมูลที่คอนเซปต์อาร์ติสต์ได้รับคือเอกสารออกแบบเวอร์ชันเมื่อสองเดือนก่อน และในช่วงนั้นแนวทางรูปลักษณ์ภายนอกถูกแก้ไขไปแล้วถึงสองครั้ง คนเดียวที่รู้ว่ามีการแก้ไขคือตัวนักออกแบบเกมเอง
อุบัติเหตุนี้ไม่ใช่ปัญหาทางเทคนิค แต่เป็นปัญหาของกระแสการส่งต่อ (flow) กว่าเอกสารออกแบบหนึ่งหน้าจะกลายเป็นแอสเซตในเกมต้องใช้เวลาเฉลี่ย 4\~8 สัปดาห์ ในช่วงนั้นข้อมูลของตัวละครหนึ่งตัวจะถูกส่งผ่านจากมือสู่มือ จากหัวของนักออกแบบเกมไปยังคอนเซปต์อาร์ติสต์ ไปยังโมเดลเลอร์ และไปยังแอนิเมเตอร์ ทุกครั้งที่ส่งต่อ รูปแบบอาจคลาดเคลื่อนได้ และหากรับงานที่คลาดเคลื่อนมาทั้งอย่างนั้น ผู้รับก็จะเติมช่องว่างด้วยการเดา การเดานั้นจะย้อนกลับมาเป็นข้อความหนึ่งบรรทัดในแชตอีกสองเดือนถัดมา
บทนี้ว่าด้วยวิธีวางกระแสการส่งต่อจากมือสู่มือนั้นไว้บนระบบ แทนที่จะฝากไว้กับความทรงจำของคนคนเดียว
ในโปรเจกต์ A เส้นทางที่แอสเซตตัวละครไหลผ่านมีอยู่สี่ขั้นตอน สิ่งสำคัญไม่ใช่ตัวขั้นตอนเอง แต่เป็นจุดเปลี่ยนระหว่างขั้นตอนหนึ่งกับอีกขั้นตอนหนึ่ง อุบัติเหตุไม่ได้เกิดขึ้นภายในขั้นตอน แต่ระเบิดในช่วงเวลาที่ส่งต่อแอสเซตจากขั้นตอนหนึ่งไปยังขั้นตอนถัดไป
แทนที่จะอธิบายกระแสนี้ด้วยตัวอักษร ควรวาดเป็นแผนภาพ และตลอด 24 ส่วนของหนังสือเล่มนี้ ผู้เขียนใช้วิธีขอโค้ด mermaid จาก Claude มาเรนเดอร์แทนการวาดกล่องด้วยมือ บทนี้นำผลลัพธ์ที่ได้จากการประยุกต์เทคนิคนั้นมาใส่ไว้ในเนื้อหา เท่ากับเป็นการพิสูจน์เทคนิคของตัวเองด้วยเนื้อหาของตัวเอง ด้านล่างคือผลลัพธ์ที่ขอจาก Claude ว่า "ช่วยวาดกระแส 4 ขั้นตอนของ spec→asset ให้เห็นเกตจุดเปลี่ยนเป็น mermaid" แล้วนำมาเรนเดอร์ตามเดิม
flowchart TD
A["ขั้นที่ 1 · เอกสารออกแบบ
character_spec.md"] -->|เปลี่ยนจากสเปก→ภาพ| G1{เกต 1
ตรวจรูปลักษณ์ภายนอก 6 รายการ}
G1 -->|ผ่าน| B["ขั้นที่ 2 · คอนเซปต์อาร์ต
concept_K_001_v3.png"]
G1 -.->|ตีกลับ| A
B -->|เปลี่ยนจากภาพ→3D| G2{เกต 2
ตรวจโมเดลชีต}
G2 -->|ผ่าน| C["ขั้นที่ 3 · แอสเซต 3D
model_K_001.fbx"]
G2 -.->|ตีกลับ| B
C -->|เปลี่ยนจากนิ่ง→เคลื่อนไหว| G3{เกต 3
lint แอสเซต}
G3 -->|ผ่าน| D["ขั้นที่ 4 · รวมเข้าในเกม
แอนิเมชัน·VFX·เสียง·โค้ด"]
G3 -.->|ตีกลับ| C
D --> G4{เกต 4
ตรวจสอบรวม}
G4 -->|ผ่าน| E["นำเข้าบิลด์"]
G4 -.->|ตีกลับ| D
classDef gate fill:#fde2c8,stroke:#d2691e,color:#5a2e00;
classDef asset fill:#dbeafe,stroke:#2563eb,color:#0b2545;
class G1,G2,G3,G4 gate;
class A,B,C,D,E asset;
ที่จุดเปลี่ยนทั้งสาม (สเปก→ภาพ, ภาพ→3D, นิ่ง→เคลื่อนไหว) มีเกตยืนอยู่ เกตคือช่องตรวจสอบที่คอยตรวจรูปแบบของเอกสารก่อนจะส่งต่อให้แผนกถัดไป หากรูปแบบไม่ตรงก็จะถูกตีกลับ (เส้นประ) ให้ย้อนไปยังขั้นตอนก่อนหน้า หากรับเอกสารที่รูปแบบไม่ตรงเข้ามา แผนกถัดไปก็จะเติมช่องว่างด้วยการเดา อุบัติเหตุจากแชตเกิดขึ้นเมื่อไม่มีเกต 1
ข้อดีของ mermaid เผยให้เห็นในภาพนี้ เมื่อต้องเพิ่มเกตอีกหนึ่งจุดหรือสลับลำดับขั้นตอน ไม่ต้องวาดกล่องใหม่ เพียงแก้ตัวอักษรหนึ่งบรรทัดก็พอ เพราะแผนภาพเป็นตัวอักษร มันจึงกลายเป็นสิ่งที่อยู่ภายใต้การจัดการเวอร์ชัน และถูก commit ไปพร้อมกับเอกสารออกแบบ
จุดเริ่มต้นของกระแสคือเอกสารสเปก Markdown หนึ่งหน้า เอกสารนี้เป็นอินพุตของทั้งสามขั้นตอนที่ตามมา หากตรงนี้มีช่องว่าง ช่องว่างนั้นไม่ได้หายไป แต่ถูกผลักไปยังขั้นตอนถัดไปและกลายเป็นการเดา
ด้านล่างคือแบบฟอร์ม character_spec ที่ใช้เขียนจริง ฟิลด์ related_atoms คือสิ่งที่เชื่อมสเปกนี้เข้ากับระบบ JIT atom (ดูส่วนที่ 11)
---
title: สเปกตัวละครซีเนียร์กิลด์นักปราชญ์ K_001
type: character_spec
layer: L2
related_atoms: [character_K_001, voice_profile_K_001]
status: draft
---
## 1. อัตลักษณ์
- ชื่อ: (TBD)
- บทบาท: ซีเนียร์ของกิลด์นักปราชญ์, NPC หลัก, เป็นเพื่อนร่วมทางได้
- ฝ่าย: scholar_guild
- บุคลิก: นักปราชญ์_เคร่งครัด, มีอำนาจแต่ยุติธรรม
## 2. แนวทางรูปลักษณ์ภายนอก
- อายุ: 40 กว่า
- เพศ: หญิง
- รูปร่าง: ใหญ่กว่าค่าเฉลี่ยเล็กน้อย (ราว 170cm)
- เครื่องแต่งกาย: สีเทา + แต้มสีม่วง, ชุดคลุมนักปราชญ์, แว่นตา
## 3. สีหน้า·ท่าทาง
- ปกติ: สงบนิ่ง, มุมปากลดลง
- เมื่อโกรธ: เงียบ + ตัดสายตา
- เศร้า: เปลี่ยนเรื่อง, สีหน้าเปลี่ยนแปลงเพียงเล็กน้อย
## 4. บทบาทในเกม
- ปรากฏในเควสต์หลัก chapter 1·5·12
- ออกเควสต์เสริม 8 รายการ
- ร่วมเป็นเพื่อนร่วมทาง chapter 7
## 5. เสียง·บทพูด
- voice_profile: scholar_K_001
- บทพูดตัวแทน 3 ประโยค:
- "เรื่องนั้นเป็นเรื่องที่ไม่มีบันทึกอยู่ใช่ไหม"
- "อย่าตัดสินด้วยอารมณ์ ข้อมูลต้องมาก่อน"
- "...เป็นทางเลือกของคุณสินะ เข้าใจแล้ว"
## 6. ข้อมูลอ้างอิง
- ภาพนักปราชญ์จริง 5 คน (ตรวจสอบทางกฎหมายเรียบร้อย)
- อ้างอิงโทนตัวละครจาก refgame
รูปลักษณ์ภายนอก 6 รายการ (อายุ·เพศ·รูปร่าง·เครื่องแต่งกายในส่วนที่ 2 และสีหน้า·ท่าทางในส่วนที่ 3) กลายเป็นเช็กลิสต์ของเกต 1 สองบรรทัดที่ว่า "หญิงวัย 40 กว่า / ชุดคลุมสีเทา" ซึ่งเป็นต้นเหตุของอุบัติเหตุจากแชต ก็คือส่วนนี้นั่นเอง หากเกต 1 ทำงาน ก็จะมีการตรวจสอบว่ารูปลักษณ์ภายนอก 6 รายการในข้อมูลที่คอนเซปต์อาร์ติสต์ได้รับนั้นตรงกับเอกสารออกแบบฉบับปัจจุบันหรือไม่ ก่อนจะส่งต่อ
แทนที่จะเติมเอกสารสเปกด้วยมือทั้งหมดจากศูนย์ ผู้เขียนกำหนดเพียงอัตลักษณ์และบทบาทไว้ก่อน แล้วให้ Claude ร่างรูปลักษณ์ภายนอก·สีหน้ามาให้ ด้านล่างคือบันทึกเต็มของการโต้ตอบหนึ่งรอบจริง โดยไม่ได้สรุปย่อ
พรอมต์ (ฉบับเต็ม):
อัตลักษณ์·บทบาทของตัวละครด้านล่างนี้เป็นที่ยืนยันแล้ว จงใช้สิ่งนี้เป็นพื้นฐานเพื่อเติมร่างของ "2. แนวทางรูปลักษณ์ภายนอก" และ "3. สีหน้า·ท่าทาง" ใน character_spec - บทบาท: ซีเนียร์ของกิลด์นักปราชญ์, NPC หลัก, เป็นเพื่อนร่วมทางได้ - บุคลิก: นักปราชญ์_เคร่งครัด, มีอำนาจแต่ยุติธรรม - ฝ่าย: scholar_guild ข้อจำกัด: โทนแบบ refgame (แฟนตาซีตะวันออก, ความอิ่มสีต่ำ) รูปลักษณ์ภายนอกต้องแยกแยะออกจาก NPC หลักตัวอื่นได้ด้วยภาพเงา (silhouette) เพียงอย่างเดียว สีหน้าให้เป็นแบบเก็บกดที่ไม่เผยความโกรธ·ความเศร้าออกมาตรง ๆ
ผลลัพธ์ดิบของ Claude (ตามเดิม ไม่ได้ขัดเกลา):
2. แนวทางรูปลักษณ์ภายนอก
- อายุ: ปลาย 40
- เพศ: หญิง
- รูปร่าง: ค่อนข้างผอม, ราว 165cm (※ จำเป็นต้องยืนยันว่าจะสื่อ "ความมีอำนาจ" ผ่านความสูงหรือผ่านท่าทาง — ผมแนะนำให้สื่อผ่านท่าทาง)
- เครื่องแต่งกาย: ชุดนักปราชญ์สีกรมท่าเข้ม, ปักด้ายเงิน (※ ผมไม่ทราบพาเลตต์สีของฝ่าย scholar_guild มีสีตัวแทนแยกต่างหากหรือไม่)
3. สีหน้า·ท่าทาง
- ปกติ: ยืนตรงเชิดคางขึ้นเล็กน้อย, สายตาอยู่สูงกว่าคู่สนทนาเล็กน้อย
- เมื่อโกรธ: พูดน้อยลงและน้ำเสียงเย็นชาลงเท่านั้น
- เศร้า: แทนที่ด้วยการมองออกไปนอกหน้าต่าง, สีหน้าแทบไม่เปลี่ยน
อีกข้อหนึ่ง: หากเป็น "เพื่อนร่วมทางได้" ก็น่าจะต้องมีสีหน้าระหว่างต่อสู้ด้วย แต่นั่นอยู่นอกขอบเขตของสเปกนี้หรือเปล่า
การตรวจสอบ·ปฏิเสธโดยมนุษย์:
ผลลัพธ์นี้รับมาเพียงครึ่งเดียว ส่วนสีหน้า·ท่าทางรับมาเกือบทั้งหมด — เพราะจับข้อจำกัดเรื่องแบบเก็บกดได้อย่างแม่นยำ และ "แทนความเศร้าด้วยการมองออกนอกหน้าต่าง" เป็นรายละเอียดที่ดีซึ่งไม่มีในเอกสารออกแบบ ส่วนรูปลักษณ์ภายนอกถูกปฏิเสธ ด้วยสองเหตุผล หนึ่ง รูปร่างถูกกำหนดเป็น "ค่อนข้างผอม, 165cm" แต่เอกสารออกแบบที่ยืนยันแล้วระบุว่า "ใหญ่กว่าค่าเฉลี่ยเล็กน้อย, 170cm" Claude ได้รับเพียงอัตลักษณ์·บทบาทจึงไม่อาจรู้รูปร่างได้ และเป็นการเดา — นี่คือการเดาที่เกตควรสกัดไว้พอดี สอง เครื่องแต่งกายสีกรมท่าตรงกับโทน refgame ก็จริง แต่ขัดกับสีตัวแทนของฝ่าย scholar_guild ในเกมเรา (สีเทา+ม่วง) คำถามที่ Claude โยนมา ("มีสีตัวแทนของฝ่ายแยกต่างหากหรือไม่") ได้ชี้ความขัดแย้งนี้ไว้ล่วงหน้าพอดี
การขอใหม่:
ดี สีหน้า·ท่าทางรับไว้ ส่วนรูปลักษณ์ภายนอกให้ตรึงเป็นค่าต่อไปนี้แล้วเรียบเรียงใหม่: รูปร่าง = ใหญ่กว่าค่าเฉลี่ยเล็กน้อย 170cm, เครื่องแต่งกาย = ชุดคลุมนักปราชญ์สีเทา + แต้มสีม่วง (สีตัวแทนของ scholar_guild), สวมแว่นตา สีหน้าระหว่างต่อสู้อยู่นอกขอบเขตของสเปกนี้ จึงตัดออก
สิ่งที่ได้เรียนรู้จากการโต้ตอบหนึ่งรอบนี้คือ จุดที่ Claude เติมช่องว่างด้วยการเดา ก็คือช่องว่างของเอกสารออกแบบนั่นเอง เมื่อพบค่าที่ไม่รู้ Claude แยกออกเป็นสองทาง สีของฝ่ายและสีหน้าระหว่างต่อสู้นั้นมันยกขึ้นเป็นคำถามว่า "เรื่องนี้ไม่รู้" และคำถามนั้นได้ชี้จุดที่ขาดหายไปก่อนเช็กลิสต์ของเกตเสียอีก ในทางกลับกัน รูปร่างกลับถูกถมด้วยตัวเลขที่ดูสมเหตุสมผลโดยไม่มีการระบุว่าไม่รู้ ตราบใดที่ยังมีอย่างหลังอยู่ การตรวจสอบที่มนุษย์เทียบกับเอกสารออกแบบที่ยืนยันแล้วทีละบรรทัดก็เป็นสิ่งที่ละเว้นไม่ได้
เอกสารออกแบบที่ยืนยันแล้วถูกส่งต่อไปยังคอนเซปต์อาร์ติสต์ กระแสการทำงานเหมือนกับเวิร์กโฟลว์คอนเซปต์ใน §12.1.2 คือใช้ AI ผลิตหลายสิบถึงหลายร้อยภาพ, คัดสรร (curate) ให้เหลือกำมือเดียว, ขัดเกลา 1\~3 แบบด้วยมือ แล้วจึงสร้างโมเดลชีต (ด้านหน้า·ด้านข้าง·ด้านหลัง)
หัวใจคือเกต 1 ที่ตั้งอยู่ตอนปลายของขั้นตอนนี้ ก่อนโมเดลชีตจะส่งต่อไปยังขั้นที่ 3 (3D) จะตรวจห้ารายการต่อไปนี้
| รายการ | เกณฑ์ตรวจสอบ |
|---|---|
| ตรงกับรูปลักษณ์ภายนอก 6 รายการในเอกสารออกแบบ | เครื่องแต่งกาย·รูปร่าง·อายุ·เพศ·สีหน้า·ท่าทาง ตรงกับเอกสารออกแบบฉบับปัจจุบัน |
| แยกภาพเงาระหว่าง NPC หลัก | จำแนกจากตัวละครอื่นได้ด้วย silhouette เพียงอย่างเดียว |
เป็นไปตาม ArtGuide 01_Character/_STYLE_GUIDE |
ไม่ละเมิดสไตล์ไกด์ของโดเมน |
| ไม่ขัดแย้งกับ voice_profile | ความประทับใจทางสายตาไม่ขัดกับความประทับใจทางเสียง |
| ระบุตัวได้เมื่อย่อขนาด | แม้ย่อเป็นขนาด UI·มินิแมป ก็ยังจำได้ว่าเป็นใคร |
ตรงนี้ atom image_prompt_design_intent_first ทำงาน เวลาคอนเซปต์อาร์ติสต์เขียนพรอมต์ ก็ไม่ได้เริ่มจากการไล่เรียงคำบรรยายรูปลักษณ์ภายนอกอย่าง "นักปราชญ์หญิงในชุดคลุมสีเทา" แต่เริ่มจากเจตนาในการออกแบบของเอกสารออกแบบก่อน ("มีอำนาจแต่ยุติธรรม", "นักปราชญ์ที่เก็บกดอารมณ์") หากกุมเพียงคีย์เวิร์ดรูปลักษณ์ภายนอกแล้วผลิตหลายร้อยภาพ ก็จะได้ภาพกองพะเนินที่สีเสื้อตรงแต่แววตาไม่ใช่นักปราชญ์ — การวางเจตนาไว้ข้างหน้าสุดก็เพื่อลดกองภาพที่ "รูปลักษณ์ตรงแต่ความประทับใจคลาดเคลื่อน" นั้นลงล่วงหน้า เครื่องมือผลิตเหมือนกับ §12.1.1·§12.2.5 — คือนำ SD (SDXL)/ComfyUI ที่โฮสต์เอง มาคล้องเข้ากับ character LoRA (ตรึงใบหน้า·เครื่องแต่งกาย) และ ControlNet (ตรึงท่าโพส·ภาพเงา) เพื่อให้แม้จะดึงตัวละครคนเดียวกันออกมาในท่าโพสต่าง ๆ เป็นร้อยภาพ ใบหน้าก็ไม่พังทลาย
รายการแรกของเกต 1 ที่ว่า "ตรงกับรูปลักษณ์ภายนอก 6 รายการในเอกสารออกแบบ" คือกลอนประตูโดยตรงที่สกัดอุบัติเหตุจากแชต เพราะมีการเทียบกับเอกสารออกแบบฉบับปัจจุบันก่อนที่ภาพร่างคอนเซปต์จะแข็งตัวเป็นโมเดลชีต ความคลาดเคลื่อนจากการทำงานโดยถือเวอร์ชันเมื่อสองเดือนก่อนจึงถูกดักไว้ตรงจุดนี้
ตรงนี้ต้องชี้ให้เห็นความไม่สมมาตรเชิงปฏิบัติการอย่างหนึ่ง เอกสารสเปกที่เห็นมาจนถึงตอนนี้ล้วนเป็น Markdown แต่คอนเซปต์อาร์ติสต์และโมเดลเลอร์ 3D ไม่ได้มาทำงานในบริษัทเกมเพื่ออ่าน Markdown ด้วยเหตุนี้ โปรเจกต์ A จึงใช้ไปป์ไลน์การแปลงทิศทางเดียว ("ทีมออกแบบตัดสินใจด้วย md, ทีมอาร์ตดู html เท่านั้น") ที่เห็นใน §12.2.4 กับกระแส spec→asset นี้ตามเดิม โดยแปลงการตัดสินใจ md ของทีมออกแบบเป็น html แล้วผลักเข้า SVN ของอาร์ตที่แยกต่างหาก ส่วนทีมอาร์ตดูแต่ html — ต้นทุนการเรียนรู้ md จึงเป็น 0
flowchart LR
P["ทีมออกแบบ
character_spec.md"] --> CV["_convert_md_to_html.py"]
CV --> H["96_ArtGuide
character_spec.html"]
H --> SY["_SyncToArtRepo.bat"]
SY --> AR[("SVN อาร์ต
(repo แยกต่างหาก)")]
AR --> ART["ทีมอาร์ต
ดู html เท่านั้น"]
classDef plan fill:#dcfce7,stroke:#16a34a,color:#052e16;
classDef art fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef tool fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
class P,CV plan;
class H,SY,AR,ART art;
class CV,SY tool;
_convert_md_to_html.py แปลง md เป็น html ที่อ่านง่าย และ _SyncToArtRepo.bat push ผลลัพธ์นั้นไปยัง SVN อาร์ต ไม่ใช่ SVN ออกแบบ เหตุผลที่แยกสอง repo เหมือนกับหลักการแยก PC — คือการปกป้องไม่ให้กระแสการทำงานของฝ่ายหนึ่งไปเขียนทับอีกฝ่าย การแปลงเป็นทิศทางเดียวจากออกแบบ → อาร์ตเสมอ และแม้ทีมอาร์ตจะแก้ html ก็ไม่ไหลย้อนกลับมาเป็น md ของฝ่ายออกแบบ
ปลายทางของการแปลงนั้น คือ 96_ArtGuide แบ่งออกเป็น 7 โดเมน (00_Common·01_Character\~07_Env) แต่ละโดเมนปกครองตนเองด้วย _STYLE_GUIDE ของตัวเอง โดยมี 00_Common มัดข้อตกลงร่วมของทุกโดเมน (ช่วงความอิ่มสี·การตั้งชื่อ·ความละเอียด) ไว้ (แผนภาพโครงสร้างดู §12.2.1) รายการตรวจที่สามของเกต 1 ก็คือการเป็นไปตาม 01_Character/_STYLE_GUIDE นี้นั่นเอง
เมื่อโมเดลชีตส่งต่อไปยังขั้น 3D จะผ่าน 8 กระบวนการ: โมเดลลิงไฮโพลี → รีโทโพโลยี (โลว์โพลีสำหรับใช้ในเกม) → UV unwrap → เท็กซ์เจอร์ → ริกกิง·สกินนิง → ทดสอบท่าโพส → ตรวจสอบ ขั้นตอนนี้เป็นช่วงที่ AI อ่อนที่สุด เนื่องจากโมเดลสร้าง 3D ยังให้รีโทโพโลยี·UV ระดับคุณภาพเกมไม่ได้ คนและเครื่องมือแบบดั้งเดิมจึงเป็นตัวเอก
แทนนั้น ขั้นตอนนี้มีเกต 3 หรือก็คือ lint แอสเซตอัตโนมัติ กำกับอยู่ แทนที่คนจะนับจำนวนพอลิกอนทุกครั้ง การตรวจสอบจะเกิดขึ้นอัตโนมัติในชั่วขณะที่ commit แอสเซต
| รายการตรวจ | เงื่อนไขผ่าน |
|---|---|
| จำนวนพอลิกอน | ช่วงมาตรฐานต่อตัวละคร (เกณฑ์ที่ผู้เขียนใช้คือ 40,000\~80,000) |
| ความละเอียดเท็กซ์เจอร์ | มาตรฐาน 2048×2048 |
| ประสิทธิภาพ UV unwrap | ใช้พื้นที่ตั้งแต่ 80% ขึ้นไป |
| จำนวนกระดูก (bone) | เป็นไปตามชุดกระดูกมาตรฐาน |
| กฎการตั้งชื่อแอสเซต | เป็นไปตามคอนเวนชันการตั้งชื่อในส่วนที่ 11 |
หากตรวจพบการละเมิด ระบบจะแจ้งเตือนไปยัง 3D อาร์ติสต์ที่เกี่ยวข้อง เป็นการย้ายการตรวจสอบที่เคยพึ่งสายตาของคนไปเป็นเชิงกำหนด (deterministic) รายการอย่างจำนวนพอลิกอน·ความละเอียดนั้นถูกผิดชัดเจน จึงไม่ใช่หน้าที่ของ AI หรือคน แต่เป็นของสคริปต์ lint
ตรงนี้มีขั้นตอนที่ย้อนกลับไม่ได้ (irreversible) ขั้นหนึ่งปรากฏขึ้น คือกระบวนการเรนเดอร์ที่อบ (bake) เท็กซ์เจอร์ เท็กซ์เจอร์ที่เบกแล้วครั้งหนึ่งไม่อาจย้อนกลับได้ ดังนั้นก่อนเรนเดอร์ทันที เกต 3 จะทำงานอีกหนึ่งครั้ง การจับโมชัน (motion capture) ในขั้นที่ 4 ก็ย้อนกลับไม่ได้เช่นกัน — เซสชันจับโมชันไม่อาจย้อนกลับได้จนกว่าจะเรียกนักแสดงและอุปกรณ์กลับมาอีกครั้ง เกตที่อยู่หน้าขั้นตอนซึ่งย้อนกลับไม่ได้จึงดำเนินการอย่างเข้มงวดกว่าเกตอื่น
แอนิเมชัน·VFX·เสียง·โค้ดถูกรวมเข้ากับแอสเซต 3D และปรากฏในเกมเป็นครั้งแรก เป็นขั้นที่ทุกสาขามารวมกันในที่เดียว และเกต 4 (ตรวจสอบรวม) คือกลอนประตูบานสุดท้าย
| รายการตรวจสอบ | ผู้รับผิดชอบ |
|---|---|
| ตรงกับเจตนาของเอกสารออกแบบ | นักออกแบบเกม |
| โทนวิชวล·ความสอดคล้อง | อาร์ตไดเรกเตอร์ |
| ความเป็นธรรมชาติของแอนิเมชัน | แอนิเมชันไดเรกเตอร์ |
| ระบุตัวได้ในเกม | เกมไดเรกเตอร์ |
| ประสิทธิภาพ (ภาระเฟรม) | เทคอาร์ต |
ต่อตัวละครหนึ่งตัว 5 คนใช้เวลาดูราว 30 นาที\~1 ชั่วโมง lint ของขั้นตอนนี้คือ การแมประหว่างแอสเซตกับรีซอร์ส (Skill_Art_Resource_Mapping) ที่รันอัตโนมัติ เพื่อตรวจว่ารีซอร์สที่ผูกอยู่จริงในเกมตรงกับรีซอร์สที่เอกสารออกแบบชี้ไว้หรือไม่ ในขั้นรวม บทบาทของ AI จำกัดอยู่ที่ regression test เชิงภาพและการทำ lint อัตโนมัติ — ไม่ใช่การตัดสินว่าจะแสดงอะไร แต่เป็นงานเชิงกำหนดที่เทียบเฟรมของเมื่อวานกับวันนี้ทีละพิกเซลว่าต่างกันโดยไม่ได้ตั้งใจหรือไม่
อุบัติเหตุจากแชตในต้นบทนี้ ที่จริงคือสองอุบัติเหตุที่ซ้อนทับกัน หนึ่งคือการไม่มีเกต 1 (ข้อมูลที่คลาดเคลื่อนผ่านไปได้) อีกหนึ่งคือการไม่มีการติดตามการเปลี่ยนแปลง (ข้อเท็จจริงที่ว่าแนวทางรูปลักษณ์ภายนอกถูกแก้สองครั้งไม่ได้แพร่ไปยังปลายน้ำ) สิ่งที่สกัดอุบัติเหตุที่สองได้คือการติดตามผลกระทบของการเปลี่ยนแปลง
หากข้อมูลขั้นตอนใดของตัวละครหนึ่งตัวเปลี่ยน ข้อมูลทั้งหมดที่อยู่ปลายน้ำของมันก็จะได้รับผลกระทบ หากให้คนคำนวณด้วยมือทุกครั้ง ก็ต้องตกหล่นแน่นอน ด้วยเหตุนี้จึงตั้งเครื่องมือที่ดูตำแหน่งในเชนแล้วกวาดข้อมูลปลายน้ำออกมาโดยอัตโนมัติ
# spec_change_impact.py
# เมื่อจุดใดจุดหนึ่งของเชนเปลี่ยน ให้รวบรวมแอสเซตที่อยู่ปลายน้ำ (downstream) ของมันทั้งหมด
CHAIN = ["spec", "concept", "model", "texture", "rig", "anim", "vfx", "ingame"]
def find_downstream_artifacts(spec_id, changed_field):
artifacts = []
chain_position = get_chain_position(changed_field) # เช่น: "รูปลักษณ์.เครื่องแต่งกาย" → "spec"(0)
for stage in CHAIN[chain_position + 1:]: # ปลายน้ำของ spec ทั้งหมด
artifacts.extend(get_artifacts(spec_id, stage))
return artifacts
# การใช้งาน: ถ้าเครื่องแต่งกายของ K_001 เปลี่ยน?
changed = find_downstream_artifacts("K_001", "รูปลักษณ์.เครื่องแต่งกาย")
# → ["concept_K_001_v3.png", "model_K_001.fbx",
# "texture_K_001_diffuse.png", "rig_K_001.fbx", ...]
หาก changed_field เป็น "รูปลักษณ์.เครื่องแต่งกาย" ตำแหน่งในเชนคือลำดับที่ 0 (spec) และปลายน้ำของมันคือ concept·model·texture·rig ทั้งหมดจะถูกจับเข้าในรายการที่ได้รับผลกระทบ รายการนี้จะถูกส่งไปยังผู้รับผิดชอบในรูปการแจ้งเตือนอัตโนมัติ หากเทียบกับแฟ้มเซ็นอนุมัติบนโต๊ะ ในชั่วขณะที่แก้แฟ้มลำดับที่ 1 ธงแดงจะถูกปักลงบนแฟ้มลำดับที่ 2\~8 โดยอัตโนมัติ และแฟ้มที่ติดธงจะถูกส่งกลับเข้าคิวตรวจสอบอีกครั้ง อุบัติเหตุจากแชตเกิดขึ้นเพราะไม่มีธงนี้นี่เอง — ลำดับที่ 1 (รูปลักษณ์ในเอกสารออกแบบ) เปลี่ยนถึงสองครั้ง แต่ลำดับที่ 2 (คอนเซปต์) ไม่มีธงปักลงไป
ด้านล่างคือการเปรียบเทียบก่อน·หลังการทำมาตรฐานในโปรเจกต์ A ที่ผู้เขียนดำเนินการ เวลาและจำนวนเชิงสัมบูรณ์เป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) สิ่งที่เชื่อถือได้คือทิศทางและสัดส่วนโดยคร่าว
| รายการ | ก่อนทำมาตรฐาน | หลังทำมาตรฐาน | ทิศทาง |
|---|---|---|---|
| ตัวละคร 1 ตัว (เอกสารออกแบบ→ในเกม) | 8\~12 สัปดาห์ | 4\~6 สัปดาห์ | ราวครึ่งหนึ่ง |
| อุบัติเหตุจากการเดาระหว่างขั้น | 10\~15 ครั้งต่อไตรมาส | 2\~3 ครั้งต่อไตรมาส | ลดลงมาก |
| อุบัติเหตุจากการตกหล่นการเปลี่ยนแปลง | 8\~10 ครั้งต่อไตรมาส | 1\~2 ครั้งต่อไตรมาส | ลดลงมาก |
| เวลาตรวจสอบรวม (ต่อตัวละคร) | กระจาย·ซ้ำ (รวม 4\~6 ชั่วโมง) | จดจ่อ 30 นาที\~1 ชั่วโมง | รวมศูนย์ |
| การ onboard ดีไซเนอร์ตัวละครคนใหม่ | ราว 2 เดือน | ราว 1 เดือน | ราวครึ่งหนึ่ง |
วงจรของตัวละครลดลงราวครึ่งหนึ่ง แต่อย่าเข้าใจตัวเลขนี้ผิด การทำมาตรฐานไม่ใช่สายพานที่ปั๊มตัวละครทุกตัวออกมาด้วยความเร็วเท่ากัน ตัวละครหลักยังคงใช้เวลาเกือบ 8 สัปดาห์ ส่วนตัวประกอบจบใน 4 สัปดาห์ สิ่งที่การทำมาตรฐานทำไม่ใช่การทำให้ความเร็วสม่ำเสมอ แต่คือการทำให้คงความต่างของเวลาในแต่ละขั้นไว้ได้อย่างไม่สั่นคลอน หากมาตรฐานไหลไปเป็นการควบคุม มันจะย้อนกลับมาเป็นอุบัติเหตุที่ตัดทอนเวลาสร้างสรรค์ของศิลปิน — จุดประสงค์ของการทำมาตรฐานคือการขจัดการเดาและการตกหล่น ไม่ใช่การบีบอัดเวลา
| ขั้น | บทบาทของ AI | ความเข้มข้น |
|---|---|---|
| 1. เอกสารออกแบบ | ช่วยร่าง, ถามจุดที่ขาดหาย (นักออกแบบเกมตรวจสอบ) | เข้ม |
| 2. คอนเซปต์ | ผลิตด้วย Stable Diffusion (SDXL)·ComfyUI (LoRA·ControlNet), พรอมต์ LLM | เข้ม |
| 3. 3D | โมเดลสร้างยังไม่สุก, คน·เครื่องมือดั้งเดิมเป็นตัวเอก | อ่อน |
| 4. รวม | regression เชิงภาพ·ทำ lint อัตโนมัติ | เชิงกำหนด |
AI เข้มในขั้นที่ 1·2, ขั้นที่ 3 คนรับผิดชอบ, ขั้นที่ 4 เครื่องมือเชิงกำหนดรับหน้าที่ เมื่อการแบ่งนี้ลงตัว ความรับผิดชอบของแต่ละขั้นก็ชัดเจน — แค่ไหนคือร่างของ AI และจากตรงไหนคือการตัดสินใจของคน จะไม่สับสนเมื่ออยู่หน้าเกต
| รูปแบบ | วิธีรับมือ |
|---|---|
| เอกสารออกแบบขาดรูปลักษณ์·สีหน้า 6 รายการ | ตรวจบังคับในขั้นที่ 1, ให้ AI ถามจุดที่ขาดหาย |
| ข้ามเกตในขั้นคอนเซปต์ | บังคับเทียบรูปลักษณ์ภายนอก 6 รายการก่อนโมเดลชีตแข็งตัว |
| คำนวณผลกระทบการเปลี่ยนแปลงด้วยมือ | ติดตามอัตโนมัติด้วย spec_change_impact |
| ทำการตรวจสอบรวมทีเดียวตอนท้าย | กระจายเกตในแต่ละขั้น |
| บิลด์โดยไม่มี lint แอสเซต | สกัดอัตโนมัติด้วยเกต 3 |
| บังคับบีบอัดทุกตัวละครให้เสร็จใน 4 สัปดาห์ | คงความต่างของเวลาในแต่ละขั้นไว้ |
บรรทัดแรกสุดและบรรทัดที่สามคือวิธีรับมือโดยตรงต่ออุบัติเหตุจากแชตในต้นบทนี้
setup
1. สร้างแบบฟอร์ม character_spec.md หนึ่งฟอร์ม (6 ส่วน: อัตลักษณ์·รูปลักษณ์ภายนอก 6 รายการ·สีหน้า·บทบาท·เสียง·อ้างอิง รวมฟิลด์ related_atoms ด้วย)
2. ตั้งสคริปต์แปลง md→html (ประเภท _convert_md_to_html.py) ไว้ และแชร์ให้ทีมอาร์ตเฉพาะ html
3. ติดเช็กลิสต์เกตไว้ที่จุดเปลี่ยน 4 จุด (รูปลักษณ์ภายนอก 6 รายการ / โมเดลชีต / lint แอสเซต / ตรวจสอบรวม)
prompt
อัตลักษณ์·บทบาทของ character_spec ด้านล่างเป็นที่ยืนยันแล้ว จงเติมร่าง "แนวทางรูปลักษณ์ภายนอก" และ "สีหน้า·ท่าทาง" โดยค่าที่ไม่รู้อย่าเดา แต่ให้ทำเครื่องหมายเป็นคำถาม ข้อจำกัด: โทนแบบ refgame, แยกแยะได้ด้วยภาพเงาเพียงอย่างเดียว, สีหน้าแบบเก็บกด
verify
1. เทียบค่าที่ AI เดา (โดยเฉพาะรูปร่าง·สี) กับเอกสารออกแบบที่ยืนยันแล้วทีละบรรทัด — หากคลาดเคลื่อนให้ปฏิเสธแล้วขอใหม่ด้วยค่าที่ตรึงไว้
2. ให้เช็กลิสต์ 5 รายการของเกต 1 ผ่านก่อนส่งต่อเป็นโมเดลชีต
3. ลองเปลี่ยนรูปลักษณ์ภายนอกหนึ่งบรรทัดดูโดยตั้งใจ แล้วตรวจว่า spec_change_impact คายรายการแอสเซตปลายน้ำออกมาถูกต้องหรือไม่
หากทำงานคนเดียว ไปป์ไลน์การแปลง·SVN อาร์ต·การตรวจสอบ 5 คนนั้นมากเกินไป เก็บไว้แค่สองอย่างขั้นต่ำ (1) แบบฟอร์ม character_spec.md หนึ่งฟอร์ม — รูปลักษณ์ภายนอก 6 รายการบังคับ, ห้ามมีช่องว่าง (2) นิสัยจดบันทึก "ไฟล์ปลายน้ำที่การเปลี่ยนแปลงนี้ไปกระทบ" ด้วยมือเป็นหนึ่งบรรทัดที่ท้ายสุดของเอกสารสเปกทุกครั้งที่เปลี่ยนรูปลักษณ์ภายนอก แม้ไม่มีเครื่องมือ หนึ่งบรรทัดนั้นก็สกัดอุบัติเหตุจากการตกหล่นการเปลี่ยนแปลงได้
ผู้อ่านหลัก: นักออกแบบเกม (Game Designer) MMORPG ที่ต้องอ่านฟีดแบ็กผู้เล่นและเมตาเกม (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านคนเดียว/งานอดิเรก: §13.1.8 「ถ้าทำคนเดียว เท่านี้ก็พอ」
ผู้เขียนยังจำหน้าจอเช้าวันถัดจากที่ปล่อยอัปเดตออกไปได้ดี ในช่องคำตอบปลายเปิดของแบบสอบถามในเกมมีข้อความสะสมอยู่ 312 ข้อ มีตั้งแต่ประโยคสั้น ๆ บรรทัดเดียวไปจนถึงความโกรธยาวห้าบรรทัดปนเปกันไป ไม่มีใครในทีมออกแบบอ่านทั้ง 312 ข้อนั้นจบ พูดให้ถูกคืออ่านไม่ไหว ต่อให้อ่านก็เข้าประชุมไปด้วยความรู้สึกแค่ระดับ "ดูเหมือนมีคนบ่นว่าการตีบวกโหดเยอะนะ" และความรู้สึกนั้นก็เป็นภาพลวงที่สร้างขึ้นจากความเห็นที่เสียงดังที่สุดเพียง 5 ข้อ ไม่มีใครรู้เลยว่าจริง ๆ แล้ว 312 ข้อนั้นกำลังบอกอะไร
บทนี้ว่าด้วยวิธีทำให้เราพูดได้ว่า "อะไรกี่ข้อ" โดยที่คนไม่ต้องอ่านทั้ง 312 ข้อนั้นเอง หัวใจมีสองอย่าง อย่างแรก งานจำแนกที่น่าเบื่ออย่างการ จัดคำตอบปลายเปิดหลายร้อยข้อเข้าเป็นหัวข้อแล้วติดป้ายกำกับอารมณ์ นั้นให้ AI ทำ อย่างที่สอง อย่าเชื่อคลัสเตอร์ของ AI ทั้งดุ้น แต่ให้คน จับการจำแนกผิดสักหนึ่งข้อมาใช้สิทธิ์ยับยั้งแล้วขอใหม่ หลักการทั่วไปของการวิเคราะห์ FAQ และเมตาเกมนั้นมีในหนังสือเล่มอื่นอยู่แล้ว บทนี้จึงโฟกัสเฉพาะ จุดที่นำการวิเคราะห์นั้นมารันด้วยกระบวนการทำงานแบบ AI เท่านั้น
FAQ และคำตอบปลายเปิดคือกระจกที่สะท้อนความต่างระหว่างเกมที่นักออกแบบตั้งใจกับเกมที่ผู้ใช้ประสบจริง ถ้าคำถามเดียวกันเข้ามาที่เคาน์เตอร์ประชาสัมพันธ์วันละ 30 ครั้ง สิ่งที่ต้องทำไม่ใช่เพิ่มคนรับเรื่อง แต่ต้องออกแบบป้ายแนะนำใหม่ ปัญหาคือการนับ "30 ครั้ง" นั้นเอง คำตอบปลายเปิดไม่ใช่ล็อกที่มีรูปแบบตายตัว GROUP BY จึงใช้ไม่ได้ "การตีบวกแพงเกินไป" กับ "ทรัพยากรไม่พอเลยอัปเกรดไม่ไหว" เป็นหัวข้อเดียวกัน แต่ตัวอักษรต่างกัน ถ้าให้คนจัดกลุ่มด้วยสายตา 312 ข้อก็กินเวลาสองสามชั่วโมง และเกณฑ์การจัดกลุ่มก็คลอนแคลนต่างกันไปในแต่ละคน
ตรงนี้คือที่ที่ AI เข้ามาเสียบได้ การจำแนกคำตอบปลายเปิดเป็นงานที่ (1) ปริมาณเยอะ (2) น่าเบื่อ และ (3) ต้องอาศัยการตัดสินความหมายของภาษาธรรมชาติ — กล่าวคือเป็นงานที่โค้ดเชิงกำหนด (deterministic) ทำไม่ได้ และถ้าให้คนทำก็แพง แต่มีเรื่องหนึ่งที่ต้องตรึงเป็นกฎไว้ก่อนปล่อย สิ่งที่ AI สร้างขึ้นคือคลัสเตอร์หัวข้อ (สมมติฐาน) ไม่ใช่การวินิจฉัยที่ยืนยันแล้ว "ไม่พอใจการตีบวก 38%" เป็นแค่ผลที่ AI ติดป้ายให้เท่านั้น มันต้องไม่นำไปสู่การตัดสินใจว่า "ให้เนิร์ฟการตีบวก" โดยตรง หลักการที่ร้อยทะลุทั้งส่วนที่ 13 ก็เป็นแบบเดิมตรงนี้ — การนิยาม KPI และการวินิจฉัยขั้นสุดท้ายเป็นของคน ส่วนการจัดกลุ่มภาษาธรรมชาติและการติดป้ายกำกับขั้นแรกเป็นของ AI
คุณค่าที่แท้จริงของการทำให้เป็นอัตโนมัติก็อยู่ตรงจุดนี้ การทำให้การจำแนกเป็นอัตโนมัตินั้น แทนที่จะทำให้การวิเคราะห์เองเร็วขึ้น สิ่งที่เป็นหัวใจคือ สัญญาณที่ชื่อว่า 312 ข้อนั้นจะมาถึงบนโต๊ะในรูปที่ถูกจำแนกแล้วทุกเช้าของแต่ละสัปดาห์ คุณค่าของการทำให้เป็นอัตโนมัติไม่ใช่การประหยัดเวลา แต่เป็นการเปิดเผยสัญญาณ (แนวคิดการบริหารทีม automation_signal_value_over_time_savings) เปรียบได้กับความต่างระหว่างจดหมายที่เอาแต่กองอยู่ในตู้กับจดหมายที่ถูกจำแนกและส่งไปยังแผนกที่เกี่ยวข้องทุกวัน
ผู้เขียนจะแสดงให้เห็นจนจบหนึ่งรอบว่าจริง ๆ แล้วรันอย่างไร ด้านล่างนี้เป็นการจำลองเซสชันที่นำคำตอบปลายเปิดจากแบบสอบถามในเกมของโปรเจกต์ของผู้เขียน (MMORPG ที่เน้นมือถือก่อน ต่อไปเรียกว่า "โปรเจกต์ A") มาจัดคลัสเตอร์หัวข้ออย่างซื่อตรง พรอมต์ที่ป้อนเข้าไปสามารถคัดลอกไปใช้ได้ตามนั้น และผลลัพธ์คือการเรียบเรียงใหม่จากเซสชันจริง
ก่อนอื่นดึงคำตอบปลายเปิดต้นฉบับออกมาในรูปที่เครื่องอ่านได้ ส่วนนี้แค่ดึงจาก DB ของแบบสอบถามก็พอ ไม่ใช่การเขียนใหม่ สิ่งสำคัญคือต้องใส่ โดยไม่ตกแต่งให้สวยหรือสรุปย่อ ใส่ทั้งคำผิด คำหยาบ และคำตอบคำเดียวลงไปแบบดิบ ๆ ตามต้นฉบับ ความแม่นยำของการจำแนกจะสูงขึ้นเมื่อต้นฉบับยิ่งดิบ
# survey_freetext_2026-W21.jsonl (ตัวอย่าง 6 ข้อจาก 312 ข้อ)
{"id": 0041, "text": "ค่าตีบวกบ้าไปแล้ว ฮึ่ม จะไป +10 แต่ทรัพยากรไม่พอสักที"}
{"id": 0088, "text": "แพตเทิร์นบอสสนุกดีแต่รางวัลขี้เหนียวเกินไป"}
{"id": 0102, "text": "จับคู่สงครามกิลด์นานมาก รอเกิน 5 นาที"}
{"id": 0156, "text": "ไม่เติมเงินก็ตีบวกไม่ได้ นี่มันเกมเหรอ"}
{"id": 0203, "text": "บรรยากาศดันเจี้ยนใหม่ดีนะ เพลงก็เพราะ"}
{"id": 0274, "text": "ทำไมรางวัลในเมลไม่มา? น่าจะบั๊ก"}
นำ survey_freetext_2026-W21.jsonl ที่แนบมา (คำตอบปลายเปิดจากแบบสอบถาม 312 ข้อ) มาจัด
เป็นหัวข้อ 6~10 กลุ่ม และติดป้ายกำกับเชิงลบ/กลาง/บวกให้แต่ละคำตอบ อย่าซอยย่อยเกินไป คำตอบหนึ่ง
ใส่ได้แค่คลัสเตอร์เดียวเท่านั้น ถ้าไม่เข้าพวกให้ไปอยู่อื่น ๆ แต่ละคลัสเตอร์ให้ออกเป็นตารางบอกจำนวน·สัดส่วน·อารมณ์หลัก·ต้นฉบับตัวแทน 1 ข้อ
ส่วนข้อที่ไม่มั่นใจในการจัดเข้ากลุ่ม อย่าฝืนยัด แต่แยกออกมาเป็นรายการ [คลุมเครือ] พร้อมเหตุผลบรรทัดละข้อ
ตัดคำอธิบายออก เอาแค่ (ก) นิยามคลัสเตอร์ (ข) ตารางรวมยอด (ค) รายการ [คลุมเครือ] เท่านั้น
ในพรอมต์นี้ ประโยคที่สั่งให้จำแนกมีแค่สองบรรทัดแรก ที่เหลือล้วนเป็นข้อจำกัดที่ขังรูปทรงของผลลัพธ์ไว้ ถ้าให้อิสระแก่ AI มันจะซอยคลัสเตอร์เป็น 20 กลุ่ม หรือยัดทุกคำตอบเข้าที่ใดที่หนึ่งแบบฝืน ๆ สิ่งที่ขวางสิ่งนั้นไว้คือสี่บรรทัด — เพดานจำนวนคลัสเตอร์ (6\~10), การจัดเข้ากลุ่มได้กลุ่มเดียว, การกำกับอารมณ์ 3 ประเภท และทางออกที่ว่า "ถ้าไม่มั่นใจให้แยกไปไว้ที่ [คลุมเครือ]" ความแม่นยำของการจำแนกออกมาจากรั้วนี้
(ก) นิยามคลัสเตอร์
C1 ค่าตีบวก/การเติบโต — บ่นว่าการตีบวก·การหาทรัพยากรหนักเกินไป
C2 แรงกดดันการเติมเงิน — รับรู้ว่าคนไม่เติม/เติมน้อยถูกกีดกันจากการเติบโต
C3 รางวัลคอนเทนต์ไม่พอ — ความเห็นว่ารางวัลขี้เหนียวเทียบกับการเล่นบอส·ดันเจี้ยน
C4 จับคู่/เวลารอ — บ่นว่าจับคู่สงครามกิลด์·การรอนาน
C5 ชมคอนเทนต์ — ปฏิกิริยาเชิงบวกต่อดันเจี้ยนใหม่·การจัดฉาก·เพลง
C6 บั๊ก/รางวัลตกหล่น — แจ้งรางวัลไม่ได้รับ·ข้อผิดพลาด
อื่น ๆ — รายข้อที่ไม่เข้ากับ 6 กลุ่มข้างต้น
(ข) ตารางรวมยอด
| คลัสเตอร์ | จำนวน | สัดส่วน | อารมณ์หลัก | ต้นฉบับตัวแทน |
|----------|------|------|----------|--------------------------------------------|
| C1 ค่าตีบวก | 71 | 23% | ลบ | "ค่าตีบวกบ้าไปแล้ว ฮึ่ม จะไป +10 แต่ทรัพยากรไม่พอสักที" |
| C2 กดดันเติมเงิน | 58 | 19% | ลบ | "ไม่เติมเงินก็ตีบวกไม่ได้ นี่มันเกมเหรอ" |
| C3 รางวัลไม่พอ | 49 | 16% | ลบ | "แพตเทิร์นบอสสนุกดีแต่รางวัลขี้เหนียวเกินไป" |
| C4 จับคู่รอ | 34 | 11% | ลบ | "จับคู่สงครามกิลด์นานมาก รอเกิน 5 นาที" |
| C5 ชมคอนเทนต์| 41 | 13% | บวก | "บรรยากาศดันเจี้ยนใหม่ดีนะ เพลงก็เพราะ" |
| C6 บั๊กการจ่าย | 28 | 9% | ลบ | "ทำไมรางวัลในเมลไม่มา? น่าจะบั๊ก" |
| อื่น ๆ | 31 | 10% | ผสม | (รายข้อจำนวนมาก) |
(ค) รายการ [คลุมเครือ] (4 ข้อ)
- id 0088 "แพตเทิร์นบอสสนุกดีแต่รางวัลขี้เหนียวเกินไป"
→ จัดเข้า C3 (รางวัลไม่พอ) แต่ครึ่งแรกซ้อนทับกับ C5 (ชม) ด้วย เป็นคำตอบเชิงซ้อน
- id 0156 "ไม่เติมเงินก็ตีบวกไม่ได้" → อยู่เส้นแบ่งระหว่าง C1 กับ C2 ส่งไป C2
- (อีก 2 ข้อ)
ส่วนที่มีคุณค่าที่สุดในผลลัพธ์ไม่ใช่ตารางรวมยอด แต่เป็น รายการ [คลุมเครือ] ที่อยู่ล่างสุด เป็นจุดที่ AI แจ้งความไม่แน่นอนของการจัดกลุ่มของตัวเองและส่งต่อให้คน พรอมต์ที่ดีคือพรอมต์ที่ทำให้ AI พูดได้ว่า "ข้อนี้ผมไม่มั่นใจ"
ห้ามนำผลลัพธ์นี้ขึ้นรายงานทั้งดุ้น คนต้องลงมือพิมพ์ตัวอย่างต้นฉบับเอง จริง ๆ แล้วในเซสชันนี้มีอยู่หนึ่งข้อที่สะดุด
ขณะที่ผู้เขียนกางคำตอบ 58 ข้อของ C2 (แรงกดดันการเติมเงิน) ออกมาไล่ดูต้นฉบับ id 0156 "ไม่เติมเงินก็ตีบวกไม่ได้ นี่มันเกมเหรอ" ก็สะดุดตา AI ส่งข้อนี้ไป C2 (แรงกดดันการเติมเงิน) แต่ความเจ็บปวดขั้นแรกของประโยคนี้ไม่ใช่ "การเติมเงิน" แต่คือ "ตีบวกไม่ได้" — กล่าวคือ C1 (ค่าตีบวก) ผู้ใช้ติดกำแพงการตีบวก และชี้ว่าต้นเหตุของกำแพงนั้นคือการเติมเงิน ไม่ใช่ว่าการเติมเงินเองคือแก่นของความไม่พอใจ ที่ C1 กับ C2 อยู่ติดกันจนสับสนนั้นเป็นเรื่องจริง แต่ถ้านับข้อนี้เป็น C2 สัญญาณ "ค่าตีบวก" จะดูเล็กกว่า 23% และเส้นโค้งการตีบวกที่ควรต้องแก้จริง ๆ จะถูกเบียดตกลำดับความสำคัญ นี่คือเคสเส้นแบ่งที่การจำแนกผิดเพียงข้อเดียวอาจเปลี่ยนทิศทางของการตัดสินใจได้
ดังนั้นจึงใช้สิทธิ์ยับยั้งแล้วขอใหม่
เส้นแบ่งระหว่าง C1 (ค่าตีบวก) กับ C2 (แรงกดดันเติมเงิน) ชวนสับสนนะ ถ้าความเจ็บปวดขั้นแรกคือ 'ตัวกำแพงการเติบโตเอง' ให้เป็น C1
ถ้าเป็น 'ความเหลื่อมล้ำที่ว่าไม่เติมเงินก็ถูกกีดกัน' ให้จัดใหม่เป็น C2 ส่วน id 0156 แก่นคือ "ตีบวกไม่ได้"
ฉะนั้นเป็น C1 ใช้เกณฑ์นี้จัดข้อที่คาบเส้นแบ่งใหม่ทั้งหมด แล้วบอกมาแค่จำนวนข้อที่เปลี่ยนไป
AI ลากเส้นแบ่งใหม่ และย้าย 9 ข้อที่อยู่ใน C2 ไปยัง C1 ผลคือ C1 เปลี่ยนจาก 71→80 ข้อ (26%) และ C2 จาก 58→49 ข้อ (16%) ภาพที่ว่าค่าตีบวกเป็นหัวข้อเดี่ยวที่ใหญ่ที่สุดยังเหมือนเดิม แต่ขนาดของมันชัดขึ้นจาก 23% เป็น 26% เพียงเดินทางไปกลับครั้งเดียว เค้าโครงของสัญญาณก็คมชัดขึ้น จำนวนข้อที่จัดใหม่ (9 ข้อ) และการเปลี่ยนแปลงของสัดส่วนนี้คือค่าที่นับจริงในเซสชันนี้ (กลุ่มตัวอย่าง 312 ข้อ สัปดาห์เดียว)
ตรงนี้ขอชี้ให้ชัดอย่างหนึ่ง สิ่งที่คนใช้สิทธิ์ยับยั้งไม่ใช่เพราะ "AI ผิด" การจัดเข้า C2 ก็ตีความได้อยู่ สิ่งที่คนทำคือ ทำให้นิยามคลัสเตอร์ (=นิยาม KPI) คมขึ้นแล้วป้อนกลับให้ AI นิยามเป็นของคน ส่วนแรงงานในการไล่อ่าน 312 ข้อใหม่ตามนิยามนั้นเป็นของ AI
ถ้านำเซสชันข้างต้นมารันอัตโนมัติทุกสัปดาห์ก็จะกลายเป็นไปป์ไลน์ มีแค่สองจุดที่มือคนแตะ จุดที่ลากนิยามคลัสเตอร์ให้คม (ต้นทาง) และด่านที่เชื่อมผลการจำแนกไปสู่การตัดสินใจ (ปลายทาง) ส่วนการจัดกลุ่ม 312 ข้อและการติดป้ายกำกับที่อยู่ตรงกลางนั้น AI เป็นคนรัน
flowchart TB
A["คำตอบปลายเปิดต้นฉบับ 312 ข้อ
(ดึงจาก DB แบบสอบถาม ห้ามตกแต่ง)"] --> B["AI ขั้นที่ 1: จัดคลัสเตอร์หัวข้อ
6~10 กลุ่ม + ป้ายกำกับอารมณ์ + แจ้ง [คลุมเครือ]"]
B --> C{"การตรวจสอบโดยมนุษย์ ขั้นที่ 2
ตัวอย่างต้นฉบับ + ตรวจเคสเส้นแบ่ง"}
C -->|จำแนกผิด·นิยามคลุมเครือ| D["นิยามคลัสเตอร์ใหม่
→ ขอให้ AI จัดใหม่"]
D --> B
C -->|ผ่าน| E["ตารางรวมยอดรายสัปดาห์
หัวข้อ × จำนวน × อารมณ์"]
E --> F{"ด่านตัดสินใจเชิงออกแบบ
(Design Director·นักออกแบบเกม)"}
F --> G["แตกเป็น 3 ทาง: ทบทวนการปรับสมดุล
· UI/ทูทอเรียล · แก้บั๊ก"]
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class B ai;
class C,D,F human;
class A,E data;
การออกแบบที่ชี้ขาดคือ ขั้นที่ 2 (การตรวจสอบโดยมนุษย์) จะไม่ปล่อยให้ผลลัพธ์ของ AI ผ่านโดยอัตโนมัติ ถ้าทำให้เป็นแบบผ่านอัตโนมัติ เส้นแบ่งที่ AI ลากผิดไปครั้งหนึ่งจะบิดเบือนสัญญาณไปในทิศทางเดียวกันทุกสัปดาห์ ผู้สมัครที่น่าสงสัย (รายการคลุมเครือ) ให้ AI คัดออกมา แต่จะแก้นิยามคลัสเตอร์หรือไม่นั้นคนเป็นคนตัดสิน และตารางรวมยอดไม่ใช่การตัดสินใจในตัวมันเอง แต่เป็นเพียง อินพุตของด่านตัดสินใจ เท่านั้น "C1 ค่าตีบวก 26%" เป็นสัญญาณที่ทำให้ Design Director หันไปดูเส้นโค้งการตีบวก ไม่ใช่ทริกเกอร์เนิร์ฟอัตโนมัติ
ถ้าคำตอบปลายเปิดคือ "สิ่งที่ผู้ใช้พูด" เมตาเกมก็คือ "สิ่งที่ผู้ใช้ทำจริง" เมื่อปล่อยออกไปแล้ว วิธีเล่นที่นักออกแบบไม่ได้ตั้งใจจะลงหลักปักฐานขึ้นมา นั่นคือเมตาเกม เช่น เมตาบิลด์ (การกระจุกตัวของชุดสกิลบางชุด) เมตาเส้นทาง (เส้นทางล่ามอนสเตอร์ที่นิยม) เมตาการซื้อขาย (ข้อตกลงราคาในหมู่ผู้เล่นที่ต่างจากราคาทางการ) เป็นต้น ต่างจากคำตอบปลายเปิดตรงที่สิ่งเหล่านี้ วัดเชิงปริมาณได้ด้วยล็อกพฤติกรรม และรวมยอดด้วยโค้ดเชิงกำหนด (ไพทอน) ไม่ใช่ที่ที่ AI จะเข้ามาแทรก
หัวใจคือการ ซ้อน ทั้งสองอย่างเข้าด้วยกัน ในเซสชันข้างต้น ความไม่พอใจ C1 (ค่าตีบวก) ใหญ่ที่สุดที่ 26% ตอนนี้ถ้าในล็อกพฤติกรรม ดัชนีความหลากหลายของบิลด์ (ระดับการกระจุกตัวของชุดสกิลยอดนิยม) ลดลงในสัปดาห์เดียวกัน สัญญาณสองตัวที่ว่า "ทั้งคำพูดและพฤติกรรมกำลังหลอมรวมไปสู่บิลด์เดียว·เส้นทางการเติบโตเดียว" ก็ชี้ไปในทิศเดียวกัน เมื่อปริมาณกับคุณภาพสอดคล้องกัน ความมั่นใจในการตัดสินใจก็เกิดขึ้น ในทางกลับกัน ถ้าคำตอบปลายเปิดเงียบสงบแต่ล็อกพฤติกรรมกลับเทไปที่บิลด์เดียว นั่นอาจเป็นสัญญาณเสี่ยงว่าผู้ใช้รู้สึกอึดอัดแต่ไม่พูด (=ใกล้จะเลิกเล่นแบบเงียบ ๆ)
flowchart LR
A["เชิงคุณภาพ: คลัสเตอร์คำตอบปลายเปิด
(AI จัดคลัสเตอร์ + คนตรวจสอบ)"] --> C["อ่านซ้อนกัน
ทิศเดียวกัน? หรือขัดกัน?"]
B["เชิงปริมาณ: รวมยอดล็อกพฤติกรรม
(ไพทอนเชิงกำหนด: ความหลากหลายบิลด์·เส้นทาง·ราคาตลาด)"] --> C
C --> D["ด่านตัดสินใจเชิงออกแบบ"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
class B code;
class A ai;
class C,D human;
ตรงนี้การแบ่งงานก็ชัดเจน การรวมยอดล็อกพฤติกรรมเป็นงานของ โค้ด ไม่ใช่ AI เพราะส่วนแบ่งบิลด์หรือราคาตลาดเป็นค่าเชิงกำหนดที่ต้องไม่ให้คำตอบเปลี่ยนทุกครั้งที่เรียก AI ใช้เฉพาะกับการจัดกลุ่มข้อความไร้รูปแบบอย่างคำตอบปลายเปิดเท่านั้น ส่วน KPI เชิงปริมาณนั้นโค้ดเป็นตัวตรึงเป็นกฎ
สัดส่วนในบทนี้เป็นไปตามหลักการของ「คำสัญญาหนึ่งข้อ」ในบทนำ ตัวเลข "C1 23%→26%, จัดใหม่ 9 ข้อ" ใน §13.1.2 เป็นค่าที่นับจริงจากกลุ่มตัวอย่าง 312 ข้อ (สัปดาห์เดียว) จึงต้องอ่านไม่ใช่ในฐานะค่าสัมบูรณ์ แต่ในฐานะ ทิศทาง ที่ว่า "ค่าตีบวกเป็นหัวข้อเดี่ยวที่ใหญ่ที่สุด" ไม่ฟันธงความเป็นเหตุเป็นผล — ไม่มีตารางทำนองว่า "พอวิเคราะห์ FAQ แล้วอัตราการคงอยู่ (Retention) สูงขึ้น" แต่กระบวนการทำงานนี้วัดได้จริงสามอย่าง ได้แก่ จำนวนข้อที่จำแนกผิดซึ่งคนพลิกกลับในการตรวจสอบคลัสเตอร์ (ถ้าเป็น 0 ก็เป็นสัญญาณว่าการตรวจสอบเป็นแค่พิธีกรรม) เวลาที่ใช้จนได้รวมยอดรายสัปดาห์ และความสอดคล้องของสัญญาณเชิงปริมาณกับเชิงคุณภาพ
ใน §13.1.2 คนพลิกการจัดเข้า C2 จำนวน 9 ข้อ ถ้ารันการตรวจสอบทุกสัปดาห์ การพลิกแบบนี้จะออกมาครั้งละ 0\~ไม่กี่ข้อทุกครั้ง สิ่งสำคัญคือ การพลิก 0 ข้อไม่ใช่เป้าหมาย ถ้าไม่มีข้อใดถูกพลิกเลยในการตรวจสอบ ก็เป็นหนึ่งในสองอย่าง — AI สมบูรณ์แบบ (เกิดได้ยาก) หรือผู้ตรวจสอบไม่ได้ดูต้นฉบับแล้วประทับตราเฉย ๆ อย่างหลังพบบ่อยอย่างท่วมท้น
เมื่อมีเคสเส้นแบ่งหนึ่งถึงสองข้อสะดุดทุกสัปดาห์ และนิยามคลัสเตอร์ค่อย ๆ คมขึ้นจากเคสนั้น ด่านตรวจสอบจึงทำงานจริง นี่คือรูปธรรมของหลักการทั่วไปที่ว่าคนต้องตรวจสอบความแม่นยำของการจำแนกของ AI ด้วยการสุ่มตัวอย่างเป็นระยะ การจำแนกผิดที่ผู้ใช้ประเภทเดียวกันกระจัดกระจายไปคนละหัวข้อนั้น ถ้าเชื่อแต่การจำแนกอัตโนมัติโดยไม่ตรวจสอบ มันจะสะสมทุกสัปดาห์
| รูปแบบ | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| คนไล่ดูคำตอบปลายเปิดด้วยสายตาเท่านั้น | ภาพลวงที่ 5 ข้อเสียงดังเป็นตัวแทนของ 312 ข้อ | จำแนกทั้งหมดด้วยการจัดคลัสเตอร์ของ AI (§13.1.2) |
| มอบให้ทั้งดุ้น "AI ช่วยวิเคราะห์ฟีดแบ็กผู้ใช้ที" | คลัสเตอร์ถูกซอยเป็น 20 กลุ่มหรือถูกจัดเข้าแบบฝืน | บังคับเพดานจำนวนคลัสเตอร์·จัดได้กลุ่มเดียว·[คลุมเครือ] |
| ดูตารางรวมยอดของ AI โดยไม่ตรวจสอบ | การจำแนกผิดที่เส้นแบ่งเปลี่ยนทิศทางการตัดสินใจ | ตัวอย่างต้นฉบับ + ตรวจเคสเส้นแบ่งเอง |
| เอาสัดส่วนรวมยอดเชื่อมต่อการตัดสินใจตรง ๆ | กลายเป็นทริกเกอร์อัตโนมัติ "ไม่พอใจ 26% ก็เนิร์ฟ" | ตารางรวมยอดเป็นแค่อินพุตของด่านตัดสินใจ |
| ดูแต่เชิงคุณภาพ ละเลยล็อกพฤติกรรม | พลาดการเลิกเล่นแบบเงียบที่ไม่มีคำพูด | อ่านซ้อนเชิงปริมาณ (โค้ด)·เชิงคุณภาพ (AI) (§13.1.4) |
| สั่งให้ AI รวมยอด KPI เชิงปริมาณ | ตัวเลขเปลี่ยนทุกครั้งที่เรียกจนการปรับสมดุลคลอนแคลน | การรวมยอดบิลด์·ราคาตลาดให้โค้ดเชิงกำหนดทำ |
ข้อที่สามพลาดบ่อยที่สุด ตารางรวมยอดดูเรียบร้อยจนชวนเชื่อทั้งดุ้น แต่เหมือนกับ id 0156 เพียงข้อเดียว การจำแนกผิดที่เส้นแบ่งเพียงข้อเดียวอาจเปลี่ยนลำดับความสำคัญได้ทั้งหมด การตรวจสอบไม่ใช่การอ่าน 312 ข้อใหม่ แต่เป็นการ ยืนยันเฉพาะเคสเส้นแบ่งของสองถึงสามคลัสเตอร์ที่ใหญ่ที่สุด ด้วยต้นฉบับ
ถ้าทำคนเดียว เท่านี้ก็พอ: ไม่มี DB แบบสอบถามก็ได้ ลองรวบรวมรีวิวในสโตร์·โพสต์ชุมชนของเกมตัวเอง (หรือเกมที่ชอบ) มาเป็นข้อความสัก 30\~50 ข้อ แล้วเอาพรอมต์ใน §13.1.2 มาแปะแล้วรันดูสักครั้ง ในบรรดาคลัสเตอร์ที่ออกมา ลองเลือกการจัดเข้ากลุ่มสักข้อที่รู้สึกว่า "อันนี้มันแปลก ๆ นะ" แล้วโต้แย้งว่า "ความเจ็บปวดขั้นแรกของคำตอบนี้คือหัวข้ออื่น จงลากนิยามใหม่แล้วจัดใหม่" คุณจะได้สัมผัสด้วยตัวเองว่าการจัดคลัสเตอร์เป็นมัดรวมของการตัดสินใจแบบใดบ้าง
ถ้าเป็นทีม ลองเริ่มด้วยขั้นถัดไปนี้ ดึงคำตอบปลายเปิดของหนึ่งสัปดาห์ออกมาเป็น survey_freetext_YYYY-Www.jsonl โดยไม่ตกแต่ง แล้วเอาพรอมต์ใน §13.1.2 มารันสักครั้ง จากนั้นยืนยันเฉพาะเคสเส้นแบ่งของสองคลัสเตอร์ที่ใหญ่ที่สุดด้วยต้นฉบับ ถ้าลากนิยามคลัสเตอร์ให้คมไว้สักครั้ง หลังจากนั้นรวมยอดรายสัปดาห์ที่ทำซ้ำได้ด้วยพรอมต์เดียวกันทุกสัปดาห์ก็จะสะสมขึ้นมาเองโดยอัตโนมัติ
ผู้อ่านกลุ่มหลัก: นักออกแบบเกมฝ่าย Live Ops/ข้อมูล ที่รับผิดชอบตัวชี้วัดด้านการให้บริการ (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านที่ทำคนเดียว/ทำเป็นงานอดิเรก: §13.2.8 「ถ้าทำคนเดียวก็แค่เท่านี้」
ทุกเช้าวันจันทร์มีฉากเดิมเกิดขึ้นซ้ำ ๆ ฝ่ายข้อมูลส่งภาพแคปแดชบอร์ดประจำวันมา เราเอาขึ้นจอประชุม แล้วก็มีใครสักคนพูดว่า "DAU (Daily Active Users หรือผู้ใช้ที่ใช้งานรายวัน) ดูเหมือนจะลดลงนิดหน่อยนะครับ" จากนั้นก็มีอีกคนรับว่า "นั่นเพราะสัปดาห์ที่แล้วมีการปิดปรับปรุงเซิร์ฟเวอร์ครับ" ตัวเลขอยู่ตรงนั้นแล้ว แต่การที่คนต้องประมวลในหัวว่าตัวเลขนั้นเป็น สัญญาณผิดปกติหรือเป็นเพียงสัญญาณรบกวน (noise) กลับเริ่มต้นใหม่ตั้งแต่ศูนย์ทุกสัปดาห์ และคำตัดสินนั้นก็แตกต่างกันไปตามแต่ละคนที่พูด
ผู้เขียนขอเขียนข้อสรุปของบทนี้ไว้ก่อน สิ่งที่คนต้องทำใน KPI มีเพียงสองอย่างคือ การนิยามว่าจะใช้อะไรเป็น KPI กับ การตัดสินว่าจะเลื่อนสัญญาณผิดปกติที่ AI ยกขึ้นมาให้กลายเป็นการวินิจฉัยที่ยืนยันแล้ว หรือจะปฏิเสธมัน สองอย่างที่อยู่คั่นกลาง — งานดึงตัวเลขจาก raw log ในเวลาเดิมทุกวัน กับงานร่างคำบรรยายภาษาธรรมชาติว่าเทียบกับสัปดาห์ก่อนแล้วอะไรสั่นไหวบ้าง — แต่ละอย่างให้โค้ดเชิงกำหนด (deterministic) และ AI รับไป ทฤษฎีทั่วไปของการนิยาม KPI (ลดให้เหลือ 5–7 ตัว ระวังกฎของ Goodhart) มีในหนังสือเล่มอื่นอย่างเพียงพอแล้ว บทนี้จึงโฟกัสเฉพาะ จุดที่นำนิยามนั้นไปเดินด้วยเวิร์กโฟลว์ AI เท่านั้น
ในการดำเนินงาน KPI มีการตัดสินใจที่มีเพียงคนเท่านั้นทำได้อยู่สองอย่าง อย่างแรกคือ จะใช้อะไรเป็น KPI อย่างที่สองคือ การตอกนิยามของ KPI แต่ละตัวให้แน่นด้วยประโยคเดียว สองอย่างนี้เป็นการตัดสินคุณค่าของเกม จึงมอบหมายให้ AI ไม่ได้ การตัดสินใจว่า "ถือว่า Active คือการเล่นตั้งแต่ 5 นาทีขึ้นไป" บรรจุไว้ด้วยมุมมองว่าเกมมองอะไรว่าเป็นสุขภาพที่ดี
ปัญหาคือ เมื่อนิยามนี้สั่นไหวเพียงครั้งเดียว ตัวเลขทั้งหมดที่อยู่บนนิยามนั้นก็สั่นไหวตามไปด้วย หากฝั่งหนึ่งจับ "Active User" เป็น การล็อกอิน 1 ครั้ง แต่อีกฝั่งจับเป็น 10 นาที + ล่ามอนสเตอร์ 1 ครั้ง DAU ก็คลาดเคลื่อนกันทั้งก้อน ด้วยเหตุนี้ งานรักษา ความสอดคล้องของนิยาม จึงสำคัญยิ่งกว่าตัวนิยามเอง และกินงานดำเนินงานไปครึ่งหนึ่ง และการตรวจสอบความสอดคล้องนั้นต้องเป็นโค้ดที่ทำ ไม่ใช่หัวคน (§13.2.5)
งานที่เกิดขึ้นหลังจากตอกนิยามแน่นแล้ว ไม่ใช่ที่ของคน การดึงตัวเลขในเวลาเดิมทุกวัน การร่างขั้นแรกเพื่อกวาดดูความเปลี่ยนแปลงเทียบสัปดาห์ก่อนแล้วจดสัญญาณผิดปกติที่น่าสงสัย — สองอย่างนี้เกิดซ้ำทุกวัน และถ้าคนทำ เกณฑ์ก็จะสั่นไหวไปวัน ๆ ซึ่งเป็นงานชนิดที่ควรส่งต่อให้เครื่องและโมเดลอย่างแม่นยำ การดึงตัวเลขส่งต่อให้โค้ดเชิงกำหนด การวินิจฉัยขั้นแรกส่งต่อให้ AI ส่วนคนรับเอาตัวเลือกที่ AI ยกขึ้นมาแล้วตัดสินเพียงว่า จะยืนยันหรือจะปฏิเสธ เท่านั้น
| ขั้นตอน | ใครทำ | ทำไมต้องที่นั่น |
|---|---|---|
| การเลือกและนิยาม KPI | คน | การตัดสินคุณค่าของเกม มอบหมายไม่ได้ |
| การดึง raw ประจำวัน | โค้ด (เชิงกำหนด) | อินพุตเดิม → ตัวเลขเดิม ตรวจสอบแบบ regression ได้ |
| การร่างขั้นแรกของสัญญาณผิดปกติเทียบสัปดาห์ก่อน | AI | การสรุปด้วยภาษาธรรมชาติเหมาะกับ AI แต่แค่ระดับ 'สมมติฐาน' เท่านั้น |
| การวินิจฉัยยืนยัน·การสั่งตรวจ segment | คน | เลื่อน/ปฏิเสธสมมติฐานของ AI เป็นที่ของความรับผิดชอบ |
การแบ่งงานนี้คือโครงของทั้งบทนี้ ด้านล่างเราจะลองเดินหนึ่งรอบให้จบ
จะแสดงให้เห็นว่ามันเดินอย่างไรจริง ๆ หนึ่งรอบตั้งแต่อินพุตจนถึงการตัดสินของคนให้จบ ด้านล่างคือการนำเซสชันวินิจฉัย KPI ประจำวันของโปรเจกต์ของผู้เขียน (MMORPG ที่เน้นมือถือเป็นหลัก ต่อจากนี้เรียก "โปรเจกต์ A") มาทำให้ไม่ระบุตัวตนแล้วจำลองขึ้นใหม่ สคีมาของ raw log โครงสร้างของโค้ดดึงข้อมูล และพรอมต์ ล้วนถ่ายมาจากเครื่องมือจริง ส่วนตัวเลขเป็นค่าตัวอย่างเพื่อแสดงรูปแบบ ไม่ใช่ KPI ที่วัดจริง
ก่อนอื่นโค้ดดึง KPI จาก log DB ทุกวันเวลา 09:00 AI ไม่ได้สร้างตัวเลขเหล่านี้ขึ้นมา — แค่รับมาเท่านั้น ผลการดึงเป็น JSON ที่วางเทียบกับวันเดียวกันของสัปดาห์ก่อน
// kpi_daily_2026-06-05.json — ผลผลิตของ extract_kpi.py (อินพุตของ LLM)
{
"date": "2026-06-05",
"compare_to": "2026-05-29", // วันเดียวกันของสัปดาห์ก่อน (ศุกร์)
"active_def": "min10_hunt1", // ID นิยาม Active ที่ใช้
"L0": {
"ltv_12m_est": {"v": 0, "prev": 0, "delta_pct": null},
"d30_retention": {"v": 0, "prev": 0, "delta_pct": null}
},
"L1": {
"dau": {"v": 0, "prev": 0, "delta_pct": -0.0},
"session_len_min":{"v": 0, "prev": 0, "delta_pct": -0.0},
"sessions_per_u": {"v": 0, "prev": 0, "delta_pct": 0.0},
"d7_retention": {"v": 0, "prev": 0, "delta_pct": 0.0}
},
"segments": {
"dau_by_platform": {"ios": 0, "aos": 0},
"dau_by_region": {"kr": 0, "sea": 0},
"dau_by_newbie": {"d0_7": 0, "d8plus": 0}
}
}
ค่าถูกเว้นว่างไว้เป็น 0 ประเด็นสำคัญคือโครงสร้าง KPI แต่ละตัวมีค่าปัจจุบัน·ค่าสัปดาห์ก่อน·อัตราการเปลี่ยนแปลงติดมาด้วย และล่างสุดมี การแยกย่อยตาม segment (แพลตฟอร์ม·ภูมิภาค·ผู้ใช้ใหม่/เก่า) แนบมาพร้อมกัน หากจะให้ AI ไปไกลกว่าแค่ "DAU ลดลง" จนถึงขั้น "ตรวจดูว่าลดลงที่ segment ไหน" การแยกย่อยนี้ต้องอยู่ในอินพุต
ไฟล์แนบ kpi_daily_2026-06-05.json คือ KPI ประจำวันที่ดึงอัตโนมัติเวลา 09:00 และ compare_to เป็น
วันเดียวกันของสัปดาห์ก่อน เลือกจาก L0~L1 เฉพาะรายการที่ดูเป็นสัญญาณผิดปกติมากที่สุด 2 รายการ
เกณฑ์คือการเปลี่ยนแปลงเทียบสัปดาห์ก่อนที่เกินช่วงผันผวนปกติของวันนั้น ถ้าไม่รู้ช่วงผันผวนปกติ
อย่ากุขึ้นมา ให้เขียนว่า 'ไม่ทราบ' แล้วตัดออก แต่ละตัวเลือกให้เขียนตัวชี้วัด·ช่วงการเปลี่ยนแปลง·
สมมติฐานสาเหตุที่คาด 1~2 ข้อ·segment ที่ต้องตรวจ แต่สาเหตุต้องเขียนเป็น 'สมมติฐาน' เท่านั้น
ห้ามฟันธงเหมือนเป็นเรื่องยืนยันแล้ว เหตุการณ์·แพตช์ที่ไม่มีอยู่ใน log ห้ามฟันธง segment ให้ชี้
ได้เฉพาะภายใน segments ที่แนบมาเท่านั้น ถ้าไม่มีเลยให้เขียนแค่ '0 รายการ ไม่มีอะไรผิดปกติ'
เอาต์พุตให้แต่ละตัวเลือก 5 บรรทัด (ตัวชี้วัด/การเปลี่ยนแปลง/สมมติฐาน/segment ที่ตรวจ/ระดับความมั่นใจ สูงกลางต่ำ) ตัดความเรียงร้อยแก้วออก
ลักษณะของพรอมต์นี้คือ กฎห้าบรรทัดเกือบทั้งหมดเป็นข้อห้าม หากให้อิสระ AI จะแต่งการวินิจฉัย KPI ให้สวยหรูเป็นเรื่องเล่าที่ยืนยันแล้ว (เช่นฟันธงเหตุการณ์ที่ไม่มีใน log ว่า "DAU ลดลงเพราะอีเวนต์เช็กอินสิ้นสุด") ด้วยเหตุนี้จึงตอก ห้ามวินิจฉัยยืนยัน·เขียนเป็นสมมติฐานเท่านั้น·segment ชี้ได้เฉพาะภายในอินพุต·ไม่รู้ก็บอกว่าไม่รู้ ลงเป็นตะปู เพื่อขีดเส้นที่ AI ห้ามข้ามไว้ก่อน
ด้านล่างคือการเรียบเรียงรูปที่ LLM คายออกมาจริง ๆ ขึ้นใหม่ ไม่ย่อ·ไม่แต่งเติม และคงไว้แม้กระทั่งส่วนที่ตัวเลือกที่สามแอบฝ่าฝืนกฎ ไว้ตามเดิม
[ตัวเลือกสัญญาณผิดปกติ — 2 รายการ]
ตัวเลือกที่ 1
- ตัวชี้วัด: session_len_min (ความยาวเซสชันเฉลี่ย)
- การเปลี่ยนแปลง: ลดลงเทียบศุกร์สัปดาห์ก่อน (delta_pct ติดลบ) เป็นการเปลี่ยนแปลงเชิงลบที่ใหญ่ที่สุดใน L1
- สมมติฐาน: (a) เป็นไปได้ว่าเซสชันของผู้ใช้ใหม่ที่เข้ามา (d0_7) สั้น จึงดึงค่าเฉลี่ยลง
(b) เป็นไปได้ว่าบางบิลด์บน iOS ทำให้เซสชันจบก่อนเวลา
- segment ที่ต้องตรวจ: dau_by_newbie (d0_7 vs d8plus) แยกความยาวเซสชัน,
dau_by_platform (ios vs aos)
- ระดับความมั่นใจ: กลาง
ตัวเลือกที่ 2
- ตัวชี้วัด: d7_retention
- การเปลี่ยนแปลง: เพิ่มขึ้นเล็กน้อยเทียบสัปดาห์ก่อน (เป็นบวก) แต่ขนาดสัมบูรณ์เล็ก
- สมมติฐาน: อาจเป็นสัญญาณรบกวน ไม่มีข้อมูลช่วงผันผวนปกติของวันนั้น จึงฟันธงว่าผิดปกติหรือไม่ไม่ได้
- segment ที่ต้องตรวจ: dau_by_newbie (d0_7) retention ตาม cohort
- ระดับความมั่นใจ: ต่ำ
[หมายเหตุ] การสิ้นสุดอีเวนต์เช็กอินช่วงล่าสุดดูเหมือนจะเป็นสาเหตุของความยาวเซสชันที่ลดลง
สองตัวเลือกแรกของเอาต์พุตทำตามกฎ เขียนสมมติฐานเป็นสมมติฐาน ชี้ segment ภายในอินพุต และตัวเลือกที่ 2 ก็ลดระดับความมั่นใจของตัวเองลงเป็น 'ต่ำ' โดยบอกว่า "ไม่มีข้อมูลช่วงผันผวนปกติ จึงฟันธงไม่ได้" นี่คือหน้าตาของเอาต์พุตที่ดี — AI แจ้งขีดจำกัดของตัวเองออกมา
ปัญหาอยู่ที่บรรทัด [หมายเหตุ] ล่างสุดบรรทัดเดียว มันฟันธง "การสิ้นสุดอีเวนต์เช็กอิน" ที่ไม่มีใน log ว่าเป็นสาเหตุ เป็นการฝ่าฝืนกฎข้อ 3 บรรทัดนี้จะถูกจับได้ในขั้นถัดไป
ตรวจสามอย่าง
อย่างแรก การฝ่าฝืนกฎ บรรทัด [หมายเหตุ] ฟันธงเหตุการณ์ที่ไม่มีใน JSON อินพุตเหมือนเป็นเรื่องจริง ปฏิทินอีเวนต์ไม่ได้อยู่ในอินพุตนี้ จึงเป็นข้อมูลที่ AI ไม่อาจรู้ได้ บรรทัดนี้ ปฏิเสธ
อย่างที่สอง รับตัวเลือกที่ 1 ความยาวเซสชันที่ลดลงมีอยู่จริง และสองแนวทางที่ AI เสนอ (cohort ผู้ใช้ใหม่ / บิลด์ iOS) สามารถตรวจสอบได้จริงด้วย segment ในอินพุต รับมาได้ แต่มันยังเป็น สัญญาณผิดปกติ ไม่ใช่สาเหตุที่ยืนยันแล้ว สิ่งที่คนต้องทำคือรันคิวรี segment เพื่อแยกว่าเป็นข้อใดในสองข้อ
อย่างที่สาม พักตัวเลือกที่ 2 ไว้ AI เองบอกว่า "ฟันธงไม่ได้" และขนาดสัมบูรณ์เล็ก ก่อนจะเพิ่มช่วงผันผวนปกติของวันนั้น (ส่วนเบี่ยงเบนมาตรฐานแยกตามวัน) เข้าไปในโค้ดดึงข้อมูล ก็ถือเป็นสัญญาณรบกวนไว้ก่อน อันนี้เป็น การบ้านของฝั่งโค้ด — การที่ AI แจ้งว่า "ไม่ทราบช่วงผันผวนปกติ" ที่จริงคือการชี้ไปยังจุดบกพร่องของข้อมูลอินพุต
จึงร้องขอใหม่
บรรทัด [หมายเหตุ] ล่างสุดฟันธงอีเวนต์เช็กอินที่ไม่มีในอินพุต ลบทิ้งให้หน่อย เก็บไว้แค่ตัวเลือกที่ 1
แล้วเขียนใหม่ โดยแยกความยาวเซสชันที่ลดลงเป็น d0_7/d8plus × ios/aos แบบ 2x2 ให้เป็นแอ็กชันบรรทัดเดียว
ว่า "ตรวจดูว่าช่องไหนลดลงมากที่สุด" อย่าฟันธงสาเหตุ เอาแค่แอ็กชันการตรวจ
จบในการไปกลับครั้งเดียวนี้ AI ลบบรรทัด [หมายเหตุ] ทิ้ง แล้วตอบใหม่เป็นแอ็กชันการตรวจบรรทัดเดียวว่า "ดูความยาวเซสชันของช่อง d0_7 × iOS ก่อน" เอาต์พุตนั้นผ่านกฎ และคน — เมื่อรันคิวรีนั้นแล้วยืนยันว่าช่อง cohort iOS ผู้ใช้ใหม่ลดลงมากที่สุดจริง ๆ — ถึงตอนนั้นจึงค่อยลง การวินิจฉัยที่ยืนยันแล้ว ว่า "การหลุดออกจากเซสชัน onboarding ของ iOS ผู้ใช้ใหม่" คนเป็นผู้ลงการวินิจฉัยจนถึงที่สุด
ประเด็นสำคัญ: AI รู้แค่ "ต้องดูตรงไหน" เท่านั้น ส่วน "อะไรคือสาเหตุ" คนเป็นผู้ยืนยันหลังจากแยก segment ตรวจดูแล้ว หากพรอมต์ไม่บังคับเส้นแบ่งนี้ AI จะข้ามไปเป็นเรื่องเล่าที่ยืนยันแล้วอันดูสวยหรูทุกครั้ง
หากตรึงรอบข้างต้นไว้เป็นภาพ การวินิจฉัยประจำวันทั้งหมดต่อจากนั้นก็จะเดินทางเดิม จะเห็นในภาพเดียวว่าจุดที่มือคนแตะมีเพียงสองที่ปลายทั้งสองข้าง (นิยาม·ยืนยัน) เท่านั้น
flowchart TD
A["นิยาม KPI
(คน: เลือก + นิยาม 1 ประโยค)"] --> B["raw log DB"]
B --> C["extract_kpi.py
ดึงเชิงกำหนด 09:00
ปัจจุบัน·สัปดาห์ก่อน·segment"]
C --> D{"def_diff.py
ตรวจความตรงกันของนิยาม Active"}
D -->|alert ไม่ตรงกัน| A
D -->|ตรงกัน| E["AI วินิจฉัยขั้นแรก
สัญญาณผิดปกติ ≤2 รายการ + สมมติฐาน
+ segment ที่ต้องตรวจ"]
E --> F{"ด่านตรวจสอบโดยคน
ปฏิเสธการฝ่าฝืนกฎ·การฟันธง"}
F -->|ร้องขอใหม่| E
F -->|รับ| G["คิวรี segment →
คนลงการวินิจฉัยยืนยัน"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
class C,D code;
class E ai;
class A,F,G human;
สีของสามแนวทางต่างกัน กลุ่มสีน้ำเงิน (การดึง·diff นิยาม) เป็นเชิงกำหนด จึงรับประกันผลลัพธ์เดิมต่ออินพุตเดิม มีเพียงช่อง AI ตรงกลางช่องเดียวที่ไม่เป็นเชิงกำหนด จึงให้โค้ดช่วยประคองทั้งสองข้าง การวินิจฉัยยืนยันที่ปลายสุดเป็นของคน จุดที่บรรทัด [หมายเหตุ] ถูกจับได้ใน §13.2.2 ก็คือ 'F ด่านตรวจสอบโดยคน' นั่นเอง
ก่อนส่งการวินิจฉัยขั้นแรกให้ AI นิยามที่คนต้องตอกให้แน่นมีกับดักอยู่สี่ข้อ หากไม่รู้กับดัก JSON อินพุตของ §13.2.2 เองก็จะมีความหมายต่างกันทุกวัน
กับดัก 1 — นิยามของ Active "Active User" เป็น การล็อกอิน 1 ครั้ง หรือ 5 นาทีขึ้นไป หรือ 10 นาที + ล่า 1 ครั้ง DAU จะต่างกันเป็นเท่าตัว ตรึงนิยามไว้เป็น ID (min10_hunt1) แล้วแนบใส่ JSON อินพุตไปด้วย (ฟิลด์ active_def ในขั้นที่ 1 ของ §13.2.2) หาก ID นี้ต่างกันในแต่ละคิวรี diff ของ §13.2.5 จะจับได้
กับดัก 2 — จุดเวลาที่วัด Retention "retention 7 วัน" นั้น 7 วันคือ วันที่ 7 พอดีหลังสมัคร หรือ วันใดก็ได้ภายใน 7 วัน หรือ วันที่ 8 ค่าจะต่างกัน เป็นพื้นที่ที่มาตรฐานอุตสาหกรรมสั่นไหว จึงไม่มีทางอื่นนอกจากเขียนนิยามของตัวเองให้ชัดเจนเป็นลายลักษณ์อักษรแล้วรักษาความสอดคล้องไว้
กับดัก 3 — การจัดการ Outlier ผู้ใช้ที่ใช้งานสูงส่วนน้อยกลุ่มบนสุดดึงค่าเฉลี่ยให้สูงขึ้น ด้วยเหตุนี้ L0\~L1 จึงดู ค่ามัธยฐาน ควบคู่ไปกับค่าเฉลี่ย หลายครั้งการเปลี่ยนแปลงของการกระจายตัวมีความหมายมากกว่าการเปลี่ยนแปลงของค่าเฉลี่ย หากให้เฉพาะค่าเฉลี่ยในพรอมต์วินิจฉัยของ AI AI ก็จะดูแต่ค่าเฉลี่ยแล้วพลาดการเลื่อนของการกระจายตัวไป
กับดัก 4 — จุดเวลาที่วัด ค่าที่วัดตอนเช้า·บ่าย·เช้ามืดต่างกัน การทำงานอัตโนมัติด้านดำเนินงานตั้ง การดึงในเวลาเดิม 09:00 ทุกวัน เป็นมาตรฐาน (ขั้นที่ 1 ของ §13.2.2) หากเวลาสั่นไหว การเปรียบเทียบเทียบสัปดาห์ก่อนก็จะพังทลาย
จุดร่วมของสี่กับดักนี้คือ นิยามสั่นไหว ไม่ใช่ค่าสั่นไหว ด้วยเหตุนี้ อุบัติเหตุที่อันตรายที่สุดจึงไม่ใช่ "DAU ลดลง" แต่เป็น "DAU ของเมื่อวานกับวันนี้คำนวณด้วย นิยามที่ต่างกัน" สายตาคนแทบจับไม่ได้ ต้องจับด้วยโค้ด
อุบัติเหตุ KPI ที่เงียบที่สุดคือสองคิวรีคำนวณชื่อเดียวกัน (DAU) ด้วยนิยามที่ต่างกัน คิวรีแดชบอร์ดนับ DAU ด้วย min10_hunt1 แต่คิวรีรายงานการตลาดนับด้วย login1 ในประชุมเดียวกันสองคนก็เอา DAU ที่ต่างกันมาแล้วระแวงกันและกัน เป็นงานที่คนไล่เทียบ SQL ทีละบรรทัดเพื่อจับไม่ได้ จึงดึงนิยามออกมาเป็นเมตาดาตา แล้วให้โค้ดทำ diff
# def_diff.py — การตรวจความสอดคล้องของนิยาม KPI (โครงร่าง)
# สมมติฐาน: แต่ละคิวรีประกาศ ID นิยาม Active ที่ตัวเองใช้ไว้เป็น meta
# เช่น -- @active_def: min10_hunt1 ในส่วนหัวของ dashboard.sql
CANON = { # นิยามฉบับจริง (คนตอกให้แน่นครั้งเดียว)
"DAU": "min10_hunt1",
"d7_retention": "signup_plus7_exact",
}
def parse_active_def(sql_path):
# อ่าน -- @active_def: <id> จากส่วนหัวคอมเมนต์ SQL
for line in open(sql_path, encoding="utf-8"):
if line.strip().startswith("-- @active_def:"):
return line.split(":", 1)[1].strip()
return None # การไม่ประกาศก็เป็นอุบัติเหตุเช่นกัน
def diff(query_registry):
issues = []
for kpi, sql_path in query_registry.items():
declared = parse_active_def(sql_path)
canon = CANON.get(kpi)
if declared is None:
issues.append(f"[MISS] {kpi}: {sql_path} ไม่มีการประกาศนิยาม")
elif declared != canon:
issues.append(
f"[DIFF] {kpi}: {sql_path} คำนวณด้วย '{declared}' แต่ "
f"ฉบับจริงคือ '{canon}' ชื่อเดียวกันนิยามต่างกัน — เปรียบเทียบไม่ได้"
)
return issues
30 บรรทัดนี้กำจัดประชุมที่ถามว่า "ทำไม DAU ของคุณกับ DAU ของผมต่างกันล่ะ" หากโค้ดคายออกมาว่า [DIFF] DAU: marketing_report.sql คำนวณด้วย 'login1' แต่ฉบับจริงคือ 'min10_hunt1' ก็ไม่มีอะไรให้ถกเถียง มีแค่สองทางคือแก้คิวรีหรือเปลี่ยนฉบับจริง เมื่อนิยามถูกตรวจด้วยโค้ด ก็เกิดการรับประกันว่าการวินิจฉัยของ AI ใน §13.2.2 เดินอยู่ บนนิยามเดียวกันเสมอ การวินิจฉัยของ AI ที่วางอยู่บนนิยามที่สั่นไหวคือเรื่องไร้สาระอันดูสวยหรู
การตรวจนี้เป็นเชิงกำหนด จึงผูกเข้ากับ CI มันรันอัตโนมัติทุกครั้งที่ commit คิวรี เป็นพื้นที่ที่ไม่มอบหมายให้ AI เด็ดขาด — ความตรงกันของนิยามไม่ใช่การตัดสิน แต่เป็นการเปรียบเทียบ หากโมเดลที่ไม่เป็นเชิงกำหนดเข้ามาเกี่ยว อุบัติเหตุจะยิ่งเพิ่ม
เมื่อวางไปป์ไลน์นี้ สิ่งที่นึกถึงเป็นข้อภาคภูมิใจอันดับแรกคือ "เวลาวินิจฉัยลดลง" แต่คุณค่าจริงอยู่ที่อื่น ในแนวคิดการดำเนินงานทีมของผู้เขียนมีบรรทัดหนึ่งชื่อ automation_signal_value_over_time_savings — คุณค่าของการทำงานอัตโนมัติไม่ได้อยู่ที่เวลาที่ประหยัดได้ แต่อยู่ที่สัญญาณที่ถูกเปิดเผย
ก่อนการทำ KPI อัตโนมัติ สัญญาณอย่างความยาวเซสชันที่ลดลงจะเห็นได้ก็ต่อเมื่อมีใครบังเอิญไปเพ่งดูกราฟ หลังการทำอัตโนมัติ ทุกวันเวลา 09:00 "สัญญาณผิดปกติ 2 รายการเทียบสัปดาห์ก่อน" จะมาวางบนโต๊ะเป็นภาษาธรรมชาติ สิ่งที่ลดลงคือเวลาวิเคราะห์ แต่สิ่งที่เปลี่ยนไปคือ รับรู้สัญญาณนั้นภายในกี่วัน สิ่งที่ต้องบังเอิญเห็นถึงจะเห็น ถูกบังคับให้เปิดเผยทุกวัน
ด้วยเหตุนี้ ความสำเร็จของเครื่องมือนี้จึงไม่วัดด้วย "วินิจฉัยใช้เวลาน้อยลงกี่นาที" แต่วัดด้วย เวลาจนถึงการรับรู้สัญญาณผิดปกติครั้งแรก (สัญญาณ → การรับรู้) หากทิศทางนี้พังลง — กล่าวคือ หากสรุปของ AI เคาะแค่ "ไม่มีอะไรผิดปกติ" ทุกวันจนไม่มีใครอ่าน — เครื่องมือก็เท่ากับประหยัดเวลาแต่ฆ่าสัญญาณ และจะกลายเป็นของไร้ประโยชน์ภายในหนึ่งถึงสองไตรมาส
ตัวเลขในบทนี้เป็นไปตามหลักการของ「คำสัญญาหนึ่งข้อ」ในบทนำ ตัวเลข KPI ที่ปรากฏ (DAU·อัตราการเปลี่ยนแปลงความยาวเซสชัน) ทั้งหมดเป็นค่าตัวอย่างเพื่อแสดงรูปแบบ ไม่ใช่ค่าที่วัดจริง — อ่านเป็น โครงสร้าง ไม่ใช่ค่าสัมบูรณ์ นิยาม KPI (Active·Retention) ไม่มีมาตรฐานเดียวที่อุตสาหกรรมเห็นพ้องกัน ข้อสรุปจึงเป็น "เขียนนิยามของตัวเองให้ชัดเจนเป็นลายลักษณ์อักษร" (§13.2.4) สิ่งที่วัดได้จริงมีสามอย่าง: จำนวนความไม่ตรงกันของนิยามที่ def_diff จับได้ (เป้า 0) สัดส่วนของตัวเลือกวินิจฉัย AI ที่คนปฏิเสธ และเวลาจนถึงการรับรู้สัญญาณผิดปกติ ในทางกลับกัน ความเป็นเหตุเป็นผลอย่าง "retention เพิ่มขึ้นเพราะการทำ KPI อัตโนมัติ" จะไม่ฟันธง
ถ้าทำคนเดียวก็แค่เท่านี้: ไม่มี log DB ก็ได้ ในเกมของคุณ (หรือเกมที่คุณชอบ) ลองเลือก KPI ที่จะดูทุกวันแค่ 3 ตัวพอ แล้วเขียนนิยามตัวละหนึ่งประโยค (เช่น "Active = เริ่มเล่นแม้แต่ตาเดียว") จากนั้นจดค่าของเมื่อวาน·วันนี้ด้วยมือสองบรรทัด แล้วแปะพรอมต์ของ §13.2.2 ลองสั่ง AI ว่า "เขียนตัวเลือกสัญญาณผิดปกติเป็นสมมติฐานเท่านั้น ห้ามวินิจฉัยยืนยัน" หากลองหาบรรทัดที่ AI แอบฟันธง แล้วโต้กลับว่า "นั่นเป็นเรื่องที่ไม่มีใน log ตัดออก" คุณก็จะเข้าใจถึงเนื้อถึงตัวว่าที่ของคนในการวินิจฉัย KPI อยู่ตรงไหน
ถ้าเป็นทีม เริ่มด้วยหนึ่งขั้นถัดไปนี้ กำหนด KPI 5\~8 ตัว แล้วสร้างข้อตกลงที่จะใส่ -- @active_def: <id> หนึ่งบรรทัดในส่วนหัว SQL ของแต่ละคิวรีก่อน จากนั้นผูกโครงร่างของ def_diff.py ใน §13.2.5 (dict ฉบับจริง + parse ส่วนหัว + diff) เข้ากับ CI ไปป์ไลน์วินิจฉัย AI ค่อยตามมาทีหลัง แค่มีการตรวจความตรงกันของนิยามอย่างเดียว ก็กันอุบัติเหตุที่เงียบที่สุดอย่าง "DAU ของคุณกับ DAU ของผมต่างกัน" ได้ก่อนแล้ว
| รูปแบบ | ทำไมถึงล้มเหลว | วิธีแก้ |
|---|---|---|
| แดชบอร์ดที่เรียง KPI ไว้ 30 ตัว | หาสีแดงไม่เจอ จนไม่ดูทุกวัน | บีบ L0\~L1 ให้เหลือ 5\~8 ตัว |
| นิยาม Active ต่างกันในแต่ละคิวรี | ชื่อเดียวกันตัวเลขต่างกัน → ประชุมไม่ไว้ใจกัน | ด่าน CI ของ def_diff.py (§13.2.5) |
| มอบหมายทั้งก้อนให้ AI ว่า "วินิจฉัยสาเหตุให้หน่อย" | ฟันธงเหตุการณ์ที่ไม่มีใน log | เป็นสมมติฐานเท่านั้น·segment เฉพาะภายในอินพุต (§13.2.2) |
| รับการวินิจฉัยของ AI โดยไม่วิพากษ์ | เรื่องเล่าที่ยืนยันแล้วอันดูสวยหรูเล็ดลอดเข้ามาเป็นอินพุตของการตัดสินใจ | ปฏิเสธการฟันธงที่ด่านตรวจสอบโดยคน |
| ให้แต่ค่าเฉลี่ยเป็นอินพุต | ทั้ง AI ทั้งคนพลาดการเลื่อนของการกระจายตัว | แนบค่ามัธยฐาน·การแยกย่อย segment ไปด้วย (§13.2.4) |
| ประเมินการทำงานอัตโนมัติด้วย 'การประหยัดเวลา' อย่างเดียว | แม้สรุปเคาะแค่ "ไม่มีอะไรผิดปกติ" ก็ผ่าน | วัดด้วยเวลาการรับรู้สัญญาณ (§13.2.6) |
ข้อที่สี่พลาดบ่อยที่สุด สรุปของ AI ลื่นไหลจนอยากเชื่อไปตามนั้นเลย เหมือนบรรทัด [หมายเหตุ] ใน §13.2.2 หากการฟันธงที่ลื่นไหลหนึ่งบรรทัดไม่ถูกปฏิเสธแล้วผ่านไป สาเหตุปลอมนั้นก็จะกลายเป็นอินพุตของการตัดสินใจในไตรมาสถัดไป ที่ของคนไม่ได้อยู่ที่การเขียนสรุป แต่อยู่ที่การปฏิเสธการฟันธงของสรุป
ผู้อ่านหลัก: ผู้ดูแลข้อมูลและ Design Director ที่อ่าน KPI แล้วตัดสินใจรายไตรมาส (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านคนเดียว/งานอดิเรก: §13.3.9 「ถ้าทำคนเดียว เท่านี้ก็พอ」
ผมเคยเห็นเส้นสีแดงเส้นหนึ่งบนแดชบอร์ดในเช้าวันจันทร์ อัตราการคงอยู่ (Retention) 30 วันหักลงอย่างเห็นได้ชัดเมื่อเทียบกับสัปดาห์ก่อน คนที่มารวมตัวกันในห้องประชุมต่างคนต่างเสนอสาเหตุคนละหนึ่งอย่าง บางคนชี้ไปที่จุดล่าสัตว์ใหม่ที่เพิ่งแพตช์สัปดาห์ก่อน บางคนชี้ไปที่ซีซันใหม่ของเกมคู่แข่ง บางคนพูดแค่ว่า "ปัจจัยตามฤดูกาล" ทุกข้อฟังดูมีเหตุผล ปัญหาคือจนหมดบ่ายวันนั้น เรายังตกลงกันไม่ได้แม้แต่ว่าเราควรตรวจสอบอะไร มีสมมติฐานห้าข้อ แต่เซกเมนต์ที่จะตรวจสอบกลับยังไม่มีการกำหนดแม้แต่ข้อเดียว
บทนี้ว่าด้วยวิธียุติเช้าแบบนั้น แก่นมีอยู่บรรทัดเดียว เมื่อเห็นตัวเลขผิดปกติ จะไม่ให้ AI วินิจฉัยเป็นข้อสรุปตายตัว แต่ให้ตั้งสมมติฐานที่ตรวจสอบได้ 3–5 ข้อ AI ไม่ฟันธงว่า "อัตราการคงอยู่ลดลงเพราะ X" แต่จะเสนอการออกแบบการตรวจสอบในรูป "ถ้าเป็น X เซกเมนต์นี้จะแสดงผลออกมาแบบนี้" ส่วนการตัดสินใจเป็นของคน เรื่องทั่วไปของ data-driven นั้นมีในหนังสือเล่มอื่นมากพออยู่แล้ว บทนี้จึงเน้นเฉพาะ จุดที่นำเรื่องทั่วไปนั้นมาเดินด้วยเวิร์กโฟลว์ AI เท่านั้น
ก่อนอื่นขอตอกหมุดเส้นแบ่งให้ชัด ทั้งบทนี้ตั้งอยู่บนประโยคเดียว คนเป็นผู้กำหนดว่าจะนิยาม KPI เป็นอะไร ส่วน AI ช่วยแค่งานเดียวคือกางสมมติฐานอย่างรวดเร็วว่าทำไม KPI นั้นถึงสั่นไหวเมื่อมันสั่นไหว
ถ้าเส้นแบ่งนี้พังลง data-driven เองก็พังตาม หากปล่อยให้ AI นิยาม KPI "สิ่งที่วัดง่าย" จะกลายเป็น KPI และหากปล่อยให้ AI วินิจฉัยด้วย ประโยคฟันธงที่ฟังดูเข้าท่าจะข้ามการตรวจสอบของคนแล้วพุ่งตรงไปเป็นการตัดสินใจ ดังนั้นจึงเปิดช่องให้ AI เพียงช่วงเดียวเท่านั้น — ช่วงหลังจากจับความผิดปกติได้แล้ว ก่อนที่คนจะตัดสินใจ ระหว่างนั้นคือช่วงของการกาง "จะสงสัยอะไรและจะยืนยันอะไร"
การแบ่งงานนี้ใช้กระดูกสันหลังเดียวกับบทก่อนหน้าในส่วนที่ 13 ไพทอนดึง raw log ออกมาแบบเชิงกำหนด (deterministic) (13.1) คนตรึงการนิยาม KPI และลำดับชั้นให้นิ่ง (13.2) ส่วนในบทนี้ AI รับเฉพาะ การช่วยตีความ บนพื้นนั้นเมื่อจับความผิดปกติได้ การดึงเป็นเชิงกำหนด การนิยามเป็นของคน การช่วยตีความเป็นของ AI การที่ทั้งสามไม่ปะปนกันคือกลไกนิรภัยของทั้งพาร์ตนี้
โปรเจกต์ของผู้เขียน (MMORPG ที่เน้นมือถือก่อน ต่อไปเรียก "โปรเจกต์ A") มีล็อกจริงรองรับการช่วยเหลือนี้อยู่ ใต้โฟลเดอร์หน่วยความจำของทีมมี _economy_log/ (ล็อกความคุ้มค่าเชิงโทเค็นและเวลา) _scores_latest.json (แคชคะแนนตัวชี้วัด) _roi_report.md (รายงาน ROI (Return on Investment, ผลตอบแทนเทียบกับการลงทุน)) บันทึกเซสชันจริง (worked transcript) ของบทนี้รับสัญญาณผิดปกติที่ดึงจากล็อกเหล่านี้มาเป็นอินพุต
ก่อนอื่นขอตรึงลูปทั้งหมดที่ตัวเลขผิดปกติหนึ่งตัวนำไปสู่การตัดสินใจไว้เป็นภาพ ในภาพนี้ช่องที่ AI เข้าไปอยู่มีเพียงช่องเดียวคือ "การสร้างสมมติฐาน" เท่านั้น ทั้งช่องก่อนหน้า (การดึงข้อมูล) และช่องถัดไป (การตรวจสอบและการตัดสินใจ) ล้วนเป็นที่ของคนและโค้ด
flowchart TB
A["ตรวจพบตัวเลขผิดปกติ
(alert บนแดชบอร์ด / KPI หลุดเกณฑ์)"]
A --> B["ขั้นที่ 1 เชิงกำหนด: ไพทอนดึงข้อมูล
raw log → ตัวเลขแยกตามเซกเมนต์
(เมื่อไหร่ ที่ไหน ใครหักลง)"]
B --> C["ขั้นที่ 2 AI: สร้างสมมติฐาน 3–5 ข้อ
ห้ามวินิจฉัยตายตัว
แต่ละสมมติฐาน = เซกเมนต์ที่ต้องตรวจ + รูปแบบที่คาดหวัง"]
C --> D{"ขั้นที่ 3 คน: จัดลำดับสมมติฐาน
เริ่มจากข้อที่หักล้างได้ถูกที่สุด"}
D --> E["ขั้นที่ 4 เชิงกำหนด: ไพทอนดึงซ้ำ
รวมยอดละเอียดเฉพาะเซกเมนต์ที่ชี้"]
E --> F{"ขั้นที่ 5 คน: ตัดสินใจ
รับ/ปฏิเสธ/พักสมมติฐาน"}
F -->|ถูกหักล้าง| C
F -->|ได้รับการยืนยัน| G["บันทึกการ์ดการตัดสินใจ → นำเข้าบิลด์"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class A,B,E code;
class C ai;
class D,F human;
class G pass;
จุดที่มือของคนเข้าไปแตะมีอยู่สามแห่ง จุดที่นิยามว่าอะไรคือความผิดปกติ (แห่งแรกสุด ซึ่งจบไปแล้วใน 13.2) จุดที่เลือกว่าจะตรวจสมมติฐานข้อไหนก่อน (ขั้นที่ 3) และจุดที่ตัดสินใจขั้นสุดท้าย (ขั้นที่ 5) ส่วนการรวมยอดล็อกอันน่าเบื่อระหว่างนั้นเป็นงานของไพทอน และการกางสมมติฐานอย่างรวดเร็วเป็นงานของ AI ในลูปนี้ไม่มีช่องที่ AI วินิจฉัยตายตัว สมมติฐานมีไว้เพื่อถูกหักล้าง และถ้าถูกหักล้างก็ย้อนกลับไปขั้นที่ 2
ขอแสดงให้เห็นจริงว่าเดินหนึ่งรอบจนจบได้อย่างไร ด้านล่างคือเซสชันที่เรียบเรียงใหม่จากเหตุการณ์อัตราการคงอยู่ลดลงในเช้าวันจันทร์ข้างต้น พรอมต์อินพุตคัดลอกไปใช้ได้ตามเดิม และเอาต์พุตเรียบเรียงใหม่ให้ตรงกับเซสชันจริง
ก่อนอื่น คนจะไม่โยนความรู้สึกว่า "อัตราการคงอยู่ลดลง" เข้าไป แต่จะโยนตารางตัวเลขแยกตามเซกเมนต์ที่ไพทอนดึงมาแบบเชิงกำหนด ตารางนี้ไม่ได้เขียนขึ้นใหม่ แต่ดึงมาจาก _economy_log/ล็อกอีเวนต์เท่านั้น
# retention_break_extract.py (โครงร่าง) — แยกเซกเมนต์ของช่วงผิดปกติ
# อินพุต: ล็อกอัตราการคงอยู่ของโคฮอร์ตรายวัน
# เอาต์พุต: เซกเมนต์ไหนหักลงเท่าไร (ตารางสำหรับป้อน LLM)
def extract_break(rows, kpi="d30_retention", baseline_weeks=4):
base = mean([r[kpi] for r in rows if r.week < target_week][-baseline_weeks:])
cur = [r for r in rows if r.week == target_week]
return [
{"segment": s.name,
"baseline": round(base_by_seg[s.name], 3),
"current": round(s.value, 3),
"delta_pct": round((s.value/base_by_seg[s.name]-1)*100, 1),
"n": s.sample_size} # จำนวนตัวอย่าง — ถ้าน้อยความเชื่อมั่นต่ำ ส่งไปด้วยกัน
for s in cur
]
ตารางที่สคริปต์นี้คายออกมาคืออินพุตชุดแรกที่จะส่งให้ AI แก่นอยู่ที่การส่งจำนวนตัวอย่าง (n) ไปด้วยกัน เพื่อไม่ให้ AI เข้าใจผิดว่าความผันผวนของเซกเมนต์ที่มีตัวอย่างน้อยเป็นสาเหตุ ตัวเตือนนั้นต้องถือไว้โดยข้อมูล ไม่ใช่โดยคน
# retention_break_2026Q2W3.txt (ผลการดึงข้อมูล, ตัดมาบางส่วน)
segment baseline current delta_pct n
ใหม่(ภายใน7วันแรก) 0.41 0.31 -24.4% 8,200
กลับมา(หลังหยุดเล่น30วัน+) 0.28 0.27 -3.6% 1,100
จ่ายเงิน(เสียเงิน) 0.62 0.60 -3.2% 2,400
ไม่จ่ายเงิน 0.34 0.25 -26.5% 14,900
จุดล่าใหม่_เล่น 0.39 0.22 -43.6% 3,050
จุดล่าใหม่_ไม่เล่น 0.40 0.38 -5.0% 11,200
ไฟล์ retention_break_2026Q2W3.txt ที่แนบมาคือความเปลี่ยนแปลงของอัตราการคงอยู่ d30
แยกตามเซกเมนต์ที่ไพทอนดึงมา (baseline=ค่าเฉลี่ย 4 สัปดาห์ก่อนหน้า, current=สัปดาห์นี้,
n=จำนวนตัวอย่าง) เซกเมนต์ไม่จ่ายเงินและจุดล่าใหม่_เล่นหายไปเยอะ อย่าวินิจฉัยสาเหตุ
ขอแค่ตั้งสมมติฐานที่ตรวจสอบได้ 3–5 ข้อ แต่ละสมมติฐานให้สี่บรรทัดนี้ —
สมมติฐานหนึ่งประโยค / เซกเมนต์ที่จะตรวจ (จะซอยย่อยอย่างไร) / รูปแบบที่จะเห็นถ้าถูก /
เงื่อนไขหักล้างที่จะแสดงว่าผิด เซกเมนต์ที่ตัวอย่างน้อย (n<2000) อย่าใช้เป็นหลักฐานหลัก
ถ้าจะใช้ให้ระบุข้อจำกัด เรียงสมมติฐานตามลำดับที่หักล้างได้ถูกที่สุดก่อน และสิ่งที่
แยกแยะด้วยข้อมูลไม่ได้ให้ทำเครื่องหมาย 'ต้องอาศัยวิจารณญาณของคน' แยกไว้ เอาต์พุต
เอาแค่รายการการ์ดสมมติฐาน ตัดคำบรรยายออก
ในพรอมต์นี้ไม่มีคำว่า "ค้นหาสาเหตุ" แม้แต่บรรทัดเดียว แต่ระบุไว้แค่โครงที่สมมติฐานต้องมีและเส้นที่ห้ามข้าม ถ้าให้อิสระแก่ AI มันจะคายประโยคฟันธงที่ฟังดูเข้าท่าออกมาว่า "น่าจะเป็นเพราะความยากของจุดล่าใหม่" และพอประโยคนั้นถูกบันทึกในรายงานการประชุม มันก็กลายเป็นการตัดสินใจโดยไม่ผ่านการตรวจสอบ กลไกที่กันเรื่องนี้คือสี่อย่าง — ห้ามฟันธง บังคับให้แต่ละสมมติฐานมีเซกเมนต์ที่ต้องตรวจ รูปแบบที่คาดหวัง และเงื่อนไขหักล้าง เรียงตามต้นทุนการหักล้าง และ "สิ่งที่แยกแยะไม่ได้ส่งให้คน" เป็นกลไกที่เปลี่ยนเอาต์พุตจากการวินิจฉัยให้เป็นการออกแบบการตรวจสอบ
[สมมติฐาน 1] (ต้นทุนหักล้างต่ำสุด)
สมมติฐาน: ความยากของจุดล่าใหม่กลายเป็นกำแพงกั้นผู้เล่นใหม่ที่ไม่จ่ายเงิน ทำให้เลิกเล่นภายใน 7 วันมากขึ้น
เซกเมนต์ที่จะตรวจ: ซอยเซกเมนต์จุดล่าใหม่_เล่นออกเป็นช่วง 'จำนวนการตายครั้งแรก' (0~2 / 3~5 / 6+)
รูปแบบที่คาดหวัง: ถ้าสมมติฐานถูก อัตราการเลิกเล่น d7→d30 ในช่วงตายครั้งแรก 6+ ต้องพุ่งขึ้น
เงื่อนไขหักล้าง: ถ้าไม่มีความสัมพันธ์เชิงเดียว (monotonic) ระหว่างจำนวนการตายกับอัตราการเลิกเล่น ให้ปฏิเสธ
[สมมติฐาน 2]
สมมติฐาน: สาเหตุไม่ใช่ตัวจุดล่าใหม่เอง แต่เป็นความเปลี่ยนแปลงอื่น (เส้นรางวัล/UI) ที่ผูกมากับแพตช์เดียวกัน
เซกเมนต์ที่จะตรวจ: อัตราการคงอยู่ของผู้เล่นที่ไม่เล่นจุดล่าใหม่แต่เข้าเกมหลังแพตช์
รูปแบบที่คาดหวัง: ถ้าเซกเมนต์ไม่เล่นก็ลดลงตามไปด้วย แสดงว่าจุดล่าไม่ใช่ตัวการ
เงื่อนไขหักล้าง: ถ้าเซกเมนต์ไม่เล่นคงที่ตาม baseline (ระดับ -5%) ให้ปฏิเสธสมมติฐานนี้ แล้วโน้มไปทางสมมติฐาน 1
[สมมติฐาน 3]
สมมติฐาน: ปัจจัยภายนอก (ซีซันใหม่ของเกมคู่แข่ง) ทำให้กลุ่มไม่จ่ายเงินเลิกเล่นในภาพรวม และจุดล่าไม่เกี่ยว
เซกเมนต์ที่จะตรวจ: ตารางไขว้ 2×2 ระหว่างจ่ายเงิน/ไม่จ่ายเงิน × เล่นจุดล่าใหม่หรือไม่
รูปแบบที่คาดหวัง: ถ้าเป็นปัจจัยภายนอก กลุ่มไม่จ่ายเงินจะลดลงอย่างสม่ำเสมอโดยไม่เกี่ยวกับการเล่นจุดล่า
เงื่อนไขหักล้าง: ถ้าการลดลงกระจุกที่จุดล่าใหม่_เล่น สมมติฐานปัจจัยภายนอกก็อ่อนลง
[สมมติฐาน 4] (ต้องอาศัยวิจารณญาณของคน)
สมมติฐาน: ปัญหาอยู่ที่การวัดเอง — นิยามโคฮอร์ต/การเก็บล็อกของสัปดาห์นี้มีการเปลี่ยนแปลง
เซกเมนต์ที่จะตรวจ: (แยกแยะด้วยข้อมูลไม่ได้) ต้องให้คนยืนยันว่ามีการเปลี่ยน schema ของ telemetry มาพร้อมแพตช์หรือไม่
รูปแบบที่คาดหวัง: ถ้ามีการเปลี่ยน schema การลดลงอาจไม่ใช่ของจริง แต่เป็น artifact จากการวัด
เงื่อนไขหักล้าง: ถ้ายืนยันว่าไพป์ไลน์ล็อกไม่เปลี่ยน ให้ปฏิเสธ
หมายเหตุ: เซกเมนต์ 'กลับมา' (n=1,100) มีตัวอย่างน้อย จึงไม่ถือ -3.6% เป็นสัญญาณ
ส่วนที่มีค่ามากที่สุดในเอาต์พุตนี้คือสมมติฐาน 4 เป็นจุดที่ AI แจ้งข้อจำกัดของตัวเองว่า "อันนี้แยกแยะด้วยข้อมูลไม่ได้ ให้คนยืนยันว่ามีการเปลี่ยน schema ของ telemetry หรือไม่" พรอมต์ที่ดีทำให้ AI พูดได้ว่า "ไม่รู้" และบรรทัดสุดท้าย — การที่มันคัดเซกเมนต์ที่มีตัวอย่าง 1,100 ออกจากสัญญาณด้วยตัวเอง — คือร่องรอยที่กฎข้อ 3 ทำงาน
อย่านำการ์ดสมมติฐานนี้ไปเป็นการตัดสินใจตามนั้น คนจัดลำดับความสำคัญ แล้วไพทอนรวมยอดอีกครั้ง
สมมติฐาน 2 มีต้นทุนหักล้างถูกที่สุด เซกเมนต์จุดล่าใหม่_ไม่เล่นมีอยู่ในตารางขั้นที่ 1 อยู่แล้ว — -5.0% คงที่ตาม baseline กล่าวคือผู้เล่นที่ไม่ได้เล่นจุดล่ายังปกติดี สมมติฐาน 2 จึงถูกปฏิเสธทันที และในขณะเดียวกันสมมติฐาน 3 (ปัจจัยภายนอกทำให้ลดลงภาพรวม) ก็อ่อนลงด้วย เพราะถ้าเป็นปัจจัยภายนอก กลุ่มไม่เล่นก็ต้องลดลงไปด้วย การลดลงกระจุกอยู่ที่ผู้เล่นที่ เล่น จุดล่าใหม่
จึงแคบลงมาที่สมมติฐาน 1 แล้วรันไพทอนซ้ำ ผลของการซอยจุดล่าใหม่_เล่นตามจำนวนการตายครั้งแรกพบว่าในช่วงที่ตาย 6 ครั้งขึ้นไป การเลิกเล่น d30 โดดเด่นชัด (ทิศทาง: ตายยิ่งมากยิ่งเลิกเล่นชันขึ้น เป็นความสัมพันธ์เชิงเดียว — ค่าที่แม่นยำวัดด้วย telemetry ของบิลด์ ตรงนี้บอกแค่ทิศทาง) ตรงกับรูปแบบที่คาดหวังของสมมติฐาน 1
ที่เหลือคือสมมติฐาน 4 คนตรวจ Patch Note (แพตช์โน้ต) แล้ว — schema ของ telemetry ไม่เปลี่ยน ตัดความเป็นไปได้เรื่อง artifact จากการวัดออก ถึงตรงนี้วัตถุดิบสำหรับการตัดสินใจก็พร้อมแล้ว
[ขั้นที่ 5 การตัดสินใจของคน — การ์ดการตัดสินใจ]
- รับ: ความยากช่วงต้นของจุดล่าใหม่ (ความถี่การตายครั้งแรก) คือตัวขับเคลื่อนหลักของการเลิกเล่นในกลุ่มผู้เล่นใหม่ที่ไม่จ่ายเงิน บิลด์ถัดไปทำ A/B ลดความหนาแน่นศัตรู/พลังชีวิตในช่วงเลเวล 1–5
- ปฏิเสธ: สมมติฐานปัจจัยภายนอก (สมมติฐาน 3) สมมติฐาน artifact จากการวัด (สมมติฐาน 4)
- พัก: เส้นรางวัล (ส่วนที่เหลือของสมมติฐาน 2) — ถ้าหลังปรับความยากจุดล่าแล้วการลดลงยังเหลืออยู่ ค่อยจุดประเด็นใหม่
- บันทึกบทบาทของ AI: วินิจฉัย 0 ครั้ง ให้สมมติฐาน 4 ข้อ + การออกแบบการตรวจสอบ การตัดสินใจเป็นของคน
อินพุต (สัญญาณผิดปกติ) → การดึงข้อมูล → สมมติฐาน → การตรวจสอบ → การตัดสินใจ หนึ่งรอบปิดลงตรงนี้ AI ไม่เคยพูดสักครั้งว่า "สาเหตุคืออันนี้" มันแค่ปูทางให้ไปตรวจสอบ นี่คือเกณฑ์ Show ของบทนี้ — ประโยคที่ว่า "AI วิเคราะห์ข้อมูล" จะกลวงเปล่า ถ้าไม่ได้ดูจนจบสักครั้งว่าตั้งสมมติฐานอะไร อะไรถูกหักล้าง และคนตัดสินใจอะไร
ความต่างระหว่างการสร้างสมมติฐานกับการวินิจฉัยตายตัวดูเหมือนเล็กน้อย แต่มันแบ่งความปลอดภัยของการตัดสินใจ พอเอาสองอย่างมาวางเทียบกัน ความต่างก็ชัด
| การวินิจฉัยตายตัว (ห้าม) | การสร้างสมมติฐาน (วิธีของบทนี้) | |
|---|---|---|
| เอาต์พุตของ AI | "สาเหตุที่อัตราการคงอยู่ลดคือความยากของจุดล่าใหม่" | "สมมติฐานความยาก — ดูช่วงตายครั้งแรก 6+ แบบนี้คือถูก แบบนั้นคือผิด" |
| การกระทำถัดไปของคน | รับมาจดแล้วตัดสินใจตามนั้น | ลองหักล้างจากสมมติฐานที่ถูกที่สุดก่อน |
| เมื่อผิด | การตัดสินใจที่ผิดพุ่งตรงเข้าบิลด์ | ถูกปฏิเสธในขั้นตรวจสอบ ต้นทุน 0 |
| ความรับผิดชอบ | "AI ทำเอง" (ความรับผิดชอบระเหย) | คนเลือกสมมติฐานแล้วตัดสินใจ (ความรับผิดชอบชัด) |
อันตรายที่แท้จริงของการวินิจฉัยตายตัวไม่ใช่เรื่องความแม่นยำ แต่คือ การทำให้ข้ามการตรวจสอบ ประโยคเดียวที่ฟังดูเข้าท่าทำให้ความสงสัยในห้องประชุมสงบลง ในทางกลับกัน การ์ดสมมติฐานคือการบ้านในตัวเองว่า "ให้ไปยืนยันสิ่งนี้" จึงมีโครงสร้างที่ข้ามไปเป็นการตัดสินใจไม่ได้หากไม่ตรวจสอบ เหตุผลที่วาง AI ไว้เป็นเครื่องสร้างสมมติฐาน ไม่ใช่เครื่องวินิจฉัย อยู่ตรงนี้
หลุมพรางที่ลึกที่สุดของ data-driven คือกฎของ Goodhart "เมื่อใดที่ตัวชี้วัดกลายเป็นเป้าหมาย ตัวชี้วัดนั้นจะไม่ใช่ตัวชี้วัดที่ดีอีกต่อไป" ถ้าตั้ง DAU (ผู้ใช้ที่ใช้งานรายวัน) เป็นเป้า มันจะพอง DAU ด้วยการแจ้งเตือนแบบเทียม ๆ แล้วอัตราการคงอยู่ระยะยาวก็จะถูกเฉือนลง ปัญหาคือการบิดเบือนนี้มักเผยเป็นผลข้างเคียงออกมา หลังจากตัดสินใจไปนานแล้ว
จึงส่ง AI เข้าไปก่อนอีกหนึ่งช่อง ก่อนที่จะนำข้อตัดสินใจเข้าบิลด์ ให้ AI ลอง "ถ้าตั้ง KPI นี้เป็นเป้าหมาย มันจะถูกเล่นเกม (gamed) ได้อย่างไร" ก่อน นี่ไม่ใช่การวินิจฉัย แต่เป็น เรดทีม — สั่งให้จงใจหาช่องโหว่ของการตัดสินใจของเราเอง
[พรอมต์เตือนล่วงหน้าแบบ Goodhart]
เป้าหมาย KPI ไตรมาสนี้คืออัตราการคงอยู่ d7 +5%p และร่างวิธีบรรลุคือเสริมรางวัลเข้าเล่น ต่อเนื่อง 7 วันแบบหนัก ให้นายเป็นเรดทีมของการตัดสินใจนี้ แล้วยกฉากการบิดเบือนแบบ Goodhart ที่อาจเกิดถ้าตั้ง KPI นี้เป็นเป้าหมายมา 3 ฉาก พร้อมตัวชี้วัดยามที่จะพังไปด้วยในแต่ละฉาก และเซกเมนต์มอนิเตอร์ที่จะจับการบิดเบือนได้แต่เนิ่น ๆ ทำเป็นตาราง อย่าฟันธง ให้อยู่ในรูป 'อาจเป็นแบบนี้'
สิ่งที่ AI เสนอออกมาไม่ใช่คำพยากรณ์ตายตัว แต่เป็นรายการจุดที่ต้องสงสัย คัดเฉพาะแก่นมาได้ดังนี้
| ฉากการบิดเบือนแบบ Goodhart (สมมติฐาน) | ตัวชี้วัดยามที่จะพังไปด้วย | การมอนิเตอร์แต่เนิ่น ๆ |
|---|---|---|
| แค่เช็กชื่อเข้าเล่นแต่ไม่เล่นคอนเทนต์หลัก | จำนวนการต่อสู้ต่อเซสชัน·อัตราการเข้าจุดล่า | เตือนเมื่ออัตราการคงอยู่ d7 ↑ พร้อมจำนวนการต่อสู้ ↓ พร้อมกัน |
| เงินเฟ้อรางวัลทำให้เศรษฐกิจล่ม | สัดส่วน sink/source ของทรัพยากร, ราคาไอเทมในตลาด | ติดตามช่อง sink-source ของ _economy_log ที่ถ่างกว้างขึ้น |
| หน้าผาเลิกเล่นทันทีหลังรางวัลเข้าเล่นจบ | อัตราการคงอยู่ d8\~d14 (ทันทีหลังรางวัลหมด) | อย่าดูแค่ d7 ให้จับคู่กับ d14 |
ค่าของตารางนี้ไม่ใช่คำตอบที่ถูก แต่คือ การจับคู่ตัวชี้วัดยามไว้ล่วงหน้าก่อนตัดสินใจ ถ้าจะตั้งอัตราการคงอยู่ d7 เป็นเป้า ก็เอา "จำนวนการต่อสู้" และ "อัตราการคงอยู่ d14" ที่ AI ชี้ขึ้นมาแสดงบนหน้าจอเดียวกันแล้วดู เช่นนั้นแล้ว เมื่อ d7 ขึ้นแต่จำนวนการต่อสู้ลดตามไปด้วย — ขณะที่การบิดเบือนแบบ Goodhart เริ่มต้นนั้นเอง — ผลข้างเคียงจะถูกจับได้ก่อนที่จะสะสมจนถึงปลายไตรมาส นิสัยการผูก KPI เข้ากับตัวชี้วัดยามแทนที่จะตั้ง KPI เดียวเป็นเป้า คือวิธีทำให้ "ความสมดุลของ KPI 5–7 ตัว" ที่กำหนดไว้ใน 13.2 ทำงานจริงในขั้นตัดสินใจ
ขอชี้ไว้ตรงนี้สักหน่อย ค่าที่ AI สร้างขึ้นในเรดทีมนี้ไม่ใช่ "การประหยัดเวลา" เวลาที่คนใช้นึกถึงสามฉากนี้ก็ไม่ได้นาน ค่าที่แท้จริงคือ การเผยสัญญาณการบิดเบือนตรงจุดที่กำลังตัดสินใจ — ผลของสัญญาณที่ดึงตัวชี้วัดยามที่ปกติไม่ได้ดูขึ้นมาบนโต๊ะตัดสินใจ ค่าของการทำงานอัตโนมัติไม่ได้อยู่ที่การประหยัดเวลา แต่อยู่ที่การทำให้สัญญาณที่ปกติมองไม่เห็นกลายเป็นเห็นได้ (แนวคิดหน่วยความจำทีมของโปรเจกต์ A automation_signal_value_over_time_savings)
การสร้างสมมติฐานไม่ได้มีประโยชน์เท่ากันกับทุกการตัดสินใจ ระดับความเชื่อสมมติฐาน AI จะต่างกันตามขอบเขตเวลาของการตัดสินใจและความหนาแน่นของข้อมูล
| ประเภทการตัดสินใจ | ความหนาแน่นของข้อมูล | ตำแหน่งของสมมติฐาน AI |
|---|---|---|
| เปลี่ยนตัวเลขการปรับสมดุลสกิล | สูง (มีซิม·ล็อกมาก) | เดินลูปสมมติฐาน→ตรวจสอบ→ตัดสินใจตามเดิม AI ช่วยได้แรง |
| เปลี่ยนคอมโพเนนต์ UI | สูง (ทำ A/B ได้) | เหมือนกัน สมมติฐาน AI ใช้ได้ |
| จะออกคอนเทนต์ใหม่หรือไม่ | กลาง (อ้างได้แค่คอนเทนต์ใกล้เคียง) | สมมติฐานเป็นแค่ข้อมูลอ้างอิง น้ำหนักการตัดสินใจเอนมาทางคน |
| วิสัยทัศน์ระยะยาว·แนวใหม่ | ต่ำ (ไม่มีแบบอย่าง) | ลูปเองหมุนไม่ได้ — คนตัดสินใจ AI แค่แจกแจงความเสี่ยง |
กฎนั้นง่าย การตัดสินใจที่ข้อมูลยิ่งหนา ยิ่งเดินลูปของ §13.3.2 ตามเดิม การตัดสินใจที่ข้อมูลยิ่งบาง บทบาทของ AI ยิ่งลดจากเครื่องสร้างสมมติฐานลงมาเป็นเครื่องเขียนเช็กลิสต์ความเสี่ยง เหตุที่ความพยายามคลี่คลายวิสัยทัศน์ระยะยาวด้วยข้อมูลนั้นอันตราย ก็เพราะในจุดที่ไม่มีข้อมูลอนาคต ถ้า AI กุสมมติฐานที่ฟังดูเข้าท่าขึ้นมาด้วยข้อมูลอดีต สมมติฐานนั้นจะดึงวิสัยทัศน์กลับไปสู่อดีต การตัดสินใจในพื้นที่ที่ไม่มีข้อมูลไม่ใช่สิ่งที่จะหลีกเลี่ยงหรือโยนให้ AI แต่ปล่อยไว้เป็นจุดที่คนรับผิดชอบตัดสินใจเอง
[ป้ายบอกทาง — ถ้าจะวางพิกัดหัวข้อ·โคฮอร์ตด้วย embedding (ตอนนี้ยังเร็วเกินไป)]
ขอให้อ่านในฐานะแนวโน้มงานวิจัย ไม่ใช่ใบสั่งยา ในสองจุดของส่วนที่ 13 ความคิดเรื่อง embedding เดียวกันเปิดออก จุดหนึ่งคือคำตอบปลายเปิดใน §13.1 — ถ้าจัดกลุ่มภาษาธรรมชาติแบบไม่มีโครงด้วย embedding ระดับประโยค ก็จะวางพิกัดเคสขอบเขต [กำกวม] ของ §13.1.2 ในรูป 'ระยะห่างระหว่างศูนย์กลางสองหัวข้อ' และทำเครื่องหมายคำตอบที่ไกลจากทุกศูนย์กลางว่าเป็น 'หัวข้อใหม่ที่โผล่ขึ้น' ได้ อีกจุดคือล็อกพฤติกรรมใน §13.1.4 — ถ้า embed ล็อกการเล่น ก็จะเผย 'โคฮอร์ตที่ผุดขึ้น (emergent cohort)' ซึ่งไม่มีใครนิยามไว้ล่วงหน้า ออกมาเป็นกลุ่มก้อนในปริภูมิเวกเตอร์ ('แผนที่' ของภาคผนวก M) แล้วเปิดทางป้อนเป็นตัวเลือก 'เซกเมนต์ที่จะตรวจ' ของลูปสมมติฐาน §13.3 (เป็นจุดที่เจาะข้อจำกัด 'เซกเมนต์ที่คนนิยามไว้ล่วงหน้า' ที่ §13.3.3 ตั้งเป็นเงื่อนไขไปได้หนึ่งช่อง) แต่กลุ่มก้อนเป็นเพียงสมมติฐาน ไม่ใช่สาเหตุ กลุ่มเล็กไม่ใช่สัญญาณ (เป็นจุดเดียวกับคำเตือนเรื่องตัวอย่างใน §13.3.3) และการติดป้ายชื่อให้กลุ่มก้อนยังเป็นงานของคน (§13.1.1) เหนือสิ่งอื่นใด อุบัติเหตุไลฟ์อาจเกิดในมิติที่การบีบอัดทิ้งไป ดังนั้นความคิดนี้จึงวางไว้ในจุดเดียวกันเป๊ะกับเบาะแส 'เวกเตอร์มิติ' ของ §8.2.7 ในพาร์ตเศรษฐกิจ (สัญชาตญาณของแนวคิดอยู่ในภาคผนวก M) — บนผืนดิน telemetry เดียวกัน ด้วยความยับยั้งชั่งใจเดียวกัน มันเป็นเพียงป้ายบอกทางที่ทีมซึ่งวาง telemetry ไว้แน่นหนาจะหันมามองในอีกไม่กี่ปีข้างหน้า สิ่งที่ต้องทำตอนนี้คือเดินลูปของ §13.3.2 อย่างซื่อตรง
ตัวเลขในบทนี้เป็นไปตามหลักการของบทนำ「หนึ่งสัญญา」 กฎของ Goodhart เป็นข้อความสาธารณะที่ชาลส์ กู๊ดฮาร์ตวางรูปเป็นทางการในปี 1975 และ _economy_log·_roi_report.md·_scores_latest.json ของโปรเจกต์ A เป็นผลผลิตหน่วยความจำทีมที่มีอยู่จริง อีกทั้งกฎ integrity_check_clickup_notify ที่แจ้งผ่าน ClickUp เมื่อการตรวจความสอดคล้องล้มเหลว เป็น atom ที่ใช้งานจริงด้วยคะแนน 294.93 (ภาคผนวก A.3.6·A.3.1) ใน §13.3.3 ยืนยันด้วยการตรวจสอบสมมติฐานเฉพาะ ทิศทาง ว่า "ในช่วงตายครั้งแรก 6+ การเลิกเล่นชันขึ้น" เท่านั้น ส่วนค่าสัมบูรณ์ปล่อยให้ telemetry ของบิลด์เป็นผู้วัด ตารางเซกเมนต์ (baseline 0.41 ฯลฯ) เป็น ตัวอย่างที่จัดขึ้น เพื่อแสดงรูปแบบของเวิร์กโฟลว์ ไม่ใช่ค่าวัดจริงที่เปิดเผยของไตรมาสใดไตรมาสหนึ่ง — สิ่งที่ต้องจำไม่ใช่ตัวเลข แต่เป็นโครงสร้าง
| รูปแบบ | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| ถาม AI ว่า "สาเหตุคืออะไร" | ประโยคฟันธงที่ฟังดูเข้าท่าถูกตัดสินใจโดยไม่ตรวจสอบ | ห้ามวินิจฉัย บังคับสมมติฐาน 3–5 ข้อ + เงื่อนไขหักล้าง (§13.3.3) |
| ถือความผันผวนของเซกเมนต์ตัวอย่างน้อยเป็นสัญญาณ | เข้าใจผิดว่า noise เป็นสาเหตุ | ส่ง n ไปด้วยกันในขั้นดึงข้อมูลและระบุเกณฑ์ |
| ตั้ง KPI เดียวเป็นเป้าแล้วพุ่งตรง | การบิดเบือนแบบ Goodhart ระเบิดที่ปลายไตรมาส | ก่อนตัดสินใจทำเรดทีม AI + คู่ตัวชี้วัดยาม (§13.3.5) |
| ตัดสินใจระยะยาวที่ไม่มีข้อมูลด้วยข้อมูล | สมมติฐานอดีตลากวิสัยทัศน์อนาคตลงมา | แบ่งบทบาท AI ตามความหนาแน่นของข้อมูล (§13.3.6) |
| รับสมมติฐานแล้วรับเอามาใช้โดยไม่ตรวจสอบ | สมมติฐานปลอมตัวเป็นข้อสรุป | หักล้างจากสมมติฐานที่ถูกที่สุดก่อน ใช้เซกเมนต์ที่ไม่เล่นให้เป็นประโยชน์ |
ข้อที่สามระเบิดช้าที่สุด อัตราการคงอยู่ d7 ขึ้น การตัดสินใจดูเหมือนสำเร็จ แต่สองเดือนต่อมาหน้าผา d14 และการลดลงของจำนวนการต่อสู้มาพร้อมกัน เวลา 30 นาทีที่รันเรดทีม AI หนึ่งครั้ง ก่อน ตัดสินใจ ซื้อสองเดือนนั้นไว้ได้
ถ้าทำคนเดียว เท่านี้ก็พอ: ไม่จำเป็นต้องมีไพป์ไลน์ล็อกก็ได้ เลือกตัวเลขหนึ่งตัวที่เพิ่งหักลงจากเกมของคุณเอง (หรือจากตัวชี้วัดสาธารณะของเกมที่คุณชอบดู) แล้วโยนตัวเลขนั้นให้ AI โดยไม่ขอว่า "บอกสาเหตุที" แต่ขอว่า "ห้ามวินิจฉัยตายตัว ขอสมมติฐานที่ตรวจสอบได้ 3 ข้อพร้อมเงื่อนไขหักล้าง" จากนั้นเลือกสมมติฐานที่ยืนยันได้ถูกที่สุดมาหนึ่งข้อ แล้วลองซอยข้อมูลจริงดูสักครั้ง คุณจะสัมผัสได้ด้วยตัวเองว่า 'การรับการวินิจฉัย' กับ 'การตรวจสอบสมมติฐาน' ต่างกันแค่ไหนในแง่ความปลอดภัยของการตัดสินใจ
ถ้าเป็นทีม ลองเริ่มด้วยหนึ่งก้าวถัดไปนี้ เพิ่มหนึ่งบรรทัดให้สคริปต์ดึงตัวเลขผิดปกติ ส่งจำนวนตัวอย่าง (n) ออกมาพร้อมกันเสมอ เมื่อมันดึงตัวเลขแยกตามเซกเมนต์ (retention_break_extract.py ของ §13.3.3) และเมื่อกำหนดเป้าหมาย KPI ครั้งถัดไป ลองรันพรอมต์เรดทีม Goodhart ของ §13.3.5 สักครั้ง แล้วป้อนคู่ตัวชี้วัดยามหนึ่งคู่ลงในการ์ดการตัดสินใจ มีแค่สองอย่างนี้ "AI วินิจฉัยสาเหตุแล้ว" ก็จะเปลี่ยนเป็น "AI กางสมมติฐานแล้วคนตรวจสอบจนตัดสินใจได้"
ผู้อ่านหลัก: นักออกแบบ UX และนักออกแบบระบบของโปรเจกต์ที่ให้มือถือมาก่อน (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านที่ทำคนเดียว/เป็นงานอดิเรก: §14.1.7 「ถ้าทำคนเดียวแค่นี้พอ」
ผู้เขียนยังจำวันที่นำ HUD การต่อสู้ซึ่งทำงานได้ดีบนบิลด์ PC มาแสดงบนความละเอียดของมือถือเป็นครั้งแรกได้ ครึ่งหนึ่งของหน้าจอถูกปกคลุมไปด้วยเกจ ไอคอน มินิแมป และตัวติดตามเควสต์ ขณะที่ตัวละครกลับมองไม่เห็น แต่ละองค์ประกอบดูเหมือนจำเป็นไปเสียทั้งหมด ปัญหาคือคำถามที่ว่า "จะตัดอะไรออก" กลับกลายเป็นการถกเถียงที่เริ่มต้นใหม่ตั้งแต่ศูนย์ทุกครั้งที่ประชุม บางคนอยากเก็บมินิแมปไว้ บางคนอยากเก็บแชตไว้ เพราะเหตุผลคือ "ความรู้สึก" ข้อสรุปจึงต่างกันไปทุกครั้ง
บทนี้ว่าด้วยวิธียุติการถกเถียงนั้น แก่นมีสองข้อ ข้อแรก เปลี่ยนข้อจำกัดของมือถือจาก "ความรู้สึก" ให้เป็น rulebook ที่ตรวจสอบได้ ข้อสอง มอบงานบีบอัดที่ทั้งน่าเบื่อและซ้ำซากอย่าง "การลด 30 รายการบน PC ให้เหลือ 10 รายการบนมือถือ" ให้ AI ทำ ส่วนคนทำเพียง การตรวจสอบที่จับการละเมิด rulebook เท่านั้น ความรู้ทั่วไปเกี่ยวกับ UX บนมือถือมีอยู่ในหนังสือเล่มอื่นอย่างเพียงพอแล้ว บทนี้จึงโฟกัสเฉพาะ จุดที่นำความรู้นั้นมารันด้วยขั้นตอนการทำงานของ AI เท่านั้น
หนังสือที่ไล่เรียงข้อจำกัดของมือถือเป็นตารางมีอยู่มาก เรื่องที่ว่าหน้าจอเล็ก นิ้วหนา เซสชันสั้น และแบตเตอรี่หมดไว ทุกข้อล้วนถูกต้อง แต่ต่อให้ท่องจากตารางได้ พอถึงการประชุมก็ยังตอบคำถามที่ว่า "แล้วปุ่มนี้ใช้ได้หรือไม่" ไม่ได้อยู่ดี ข้อจำกัดต้องถูกเปลี่ยนให้เป็น เกณฑ์ผ่าน/ไม่ผ่านที่เป็นตัวเลข ทั้ง AI และคนจึงจะขีดเส้นเดียวกันได้
โชคดีที่ข้อจำกัดด้านอินพุตของมือถือจำนวนมากถูกบริษัทแพลตฟอร์มตรึงไว้เป็นไกด์ไลน์สาธารณะอยู่แล้ว มาตรฐานสาธารณะอย่างพื้นที่สัมผัส 44pt (HIG) · 48dp (Material) · คอนทราสต์ 4.5:1 (WCAG) · ระยะห่าง 8dp นั้นให้ยึดตาม rulebook ใน §9.1 และในที่นี้จะวางไว้แบบอินไลน์เฉพาะ พื้นที่สัมผัสขั้นต่ำ 44pt (HIG) ที่ lint ของบทนี้ใช้โดยตรงเท่านั้น เป็นตัวเลขที่ไม่จำเป็นต้องกุขึ้นมา เมื่อพูดได้ว่า "ปุ่มนี้คือ 38pt จึงต่ำกว่า HIG 44pt" แทนที่จะเป็น "ปุ่มดูเหมือนจะเล็กไปหน่อย" ไม่ว่าจะเป็นคนหรือ AI ที่เป็นผู้ตัดสิน ผลการตัดสินก็ออกมาเหมือนกัน
ตรงนี้ขอเพิ่มอีกหนึ่งบรรทัด — MMORPG บนมือถือใช้การจับแนวนอนสองมือเป็นมาตรฐาน องค์ประกอบที่ต้องกดวางไว้ที่มุมล่างทั้งสองข้าง ส่วนของไอเทมที่ใช้สิ้นเปลือง/สล็อตวางไว้ที่กลางล่าง (เหตุผลว่าทำไมแนวนอนจึงเป็นมาตรฐาน และโมเดลสามพื้นที่คืออะไรนั้นกล่าวไว้ใน §9.1) การตัดสินตำแหน่งทั้งหมดในบทนี้ตั้งอยู่บนสมมติฐานการจับแนวนอนสองมือนั้น
เมื่อวางเกณฑ์ของแพลตฟอร์มเทียบเคียงกับ PC จุดเริ่มต้นของการบีบอัดก็จะชัดเจน PC แม่นยำและรับได้จำนวนมาก (รับได้ 30–50 รายการ) ส่วนแนวนอนบนมือถือถูกจำกัดอยู่ที่มุมสำหรับสองมือ จึงมีขีดจำกัดอยู่ที่ 12–16 รายการ (ตารางเปรียบเทียบทั้งหมดดู rulebook §9.1 — การประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ) ดังนั้นแก่นของงานบนมือถือจึงไม่ใช่ "การออกแบบ" แต่เป็น "การบีบอัดตามลำดับความสำคัญจาก 30–50 รายการบน PC ให้เหลือ 12–16 รายการบนแนวนอนของมือถือ" และการบีบอัดนี้หากทำด้วยมือก็ทั้งน่าเบื่อ ทั้งทำให้เส้นเกณฑ์สั่นคลอนทุกครั้งที่ทำ — เพราะเป็นงานที่นำกฎเดียวกันมาใช้ซ้ำโดยไม่เหน็ดเหนื่อย จึงเข้ากันได้พอดีกับการแบ่งงานที่ AI ร่างเบื้องต้นและคนตรวจสอบ
จะแสดงให้เห็นหนึ่งรอบเต็มว่าจริง ๆ แล้วรันกันอย่างไร ด้านล่างนี้คือการจำลองเซสชันบีบอัด HUD การต่อสู้ของโปรเจกต์ของผู้เขียน (MMORPG ที่ให้มือถือมาก่อน ต่อไปจะเรียกว่า "โปรเจกต์ A") อย่างซื่อตรง พรอมต์ที่เป็นอินพุตสามารถคัดลอกไปใช้ได้ตามนั้น และผลลัพธ์เป็นการเรียบเรียงเซสชันจริงขึ้นใหม่
ก่อนอื่นทำรายการองค์ประกอบ HUD บน PC ให้เป็นตารางที่เครื่องอ่านได้ ตรงนี้มีอยู่ในชีตข้อมูลอยู่แล้ว จึงไม่ใช่การเขียนใหม่ แค่ดึงออกมาก็พอ
# hud_pc_inventory.yaml — HUD บนบิลด์ PC ปัจจุบัน (คัดบางส่วน 12 จาก 30 รายการ)
- id: hp_bar # แถบพลังชีวิต
현재위치: 좌상단
상시노출: true
조작가능: false
- id: mp_bar # แถบมานา
현재위치: 좌상단
상시노출: true
조작가능: false
- id: skill_slots # ช่องสกิล 12 ช่อง
현재위치: 하단중앙
상시노출: true
조작가능: true
- id: minimap # มินิแมป
현재위치: 우상단
상시노출: true
조작가능: true
- id: quest_tracker # ตัวติดตามเควสต์
현재위치: 우측
상시노출: true
조작가능: false
- id: chat # หน้าต่างแชต
현재위치: 좌하단
상시노출: true
조작가능: true
# ... buff_bar, party_frame, target_frame, exp_bar, currency, mail_alert ...
ช่วยบีบอัด hud_pc_inventory.yaml (HUD บนบิลด์ PC ปัจจุบัน 30 รายการ) ที่แนบมา ตามลำดับ
ความสำคัญ โดยอิงเกณฑ์การจับแนวนอนบนมือถือแบบใช้สองมือ จัดแต่ละองค์ประกอบเป็น P0
(จำเป็นต้องแสดงตลอดเวลาระหว่างต่อสู้) ถึง P3 (ตัดออกหรือแสดงตามสถานการณ์) และผลรวมของ
สิ่งที่แสดงตลอดเวลา (P0~P1) ต้องไม่เกิน 16 รายการ องค์ประกอบที่สั่งการได้ (조작가능:true)
ให้เสนอตำแหน่งไว้ที่มุมที่นิ้วโป้งสองข้างเอื้อมถึง (ซ้ายล่าง=เคลื่อนที่ ขวาล่าง=สกิล) แต่ละ
การจัดประเภทต้องแนบเหตุผลหนึ่งบรรทัดเสมอ และสิ่งที่คลุมเครืออย่าฝืนตัดสิน ให้ทำเครื่องหมาย
แยกไว้แล้วส่งต่อให้คน เอาต์พุตเอาแค่ตาราง (องค์ประกอบ | ประเภท | พื้นที่วาง | เหตุผล | คลุมเครือหรือไม่)
ตัดคำอธิบายออก
ในพรอมต์นี้คำสั่งที่ให้บีบอัดมีอยู่เพียงบรรทัดเดียว ส่วนเนื้อความที่เหลือทั้งหมดล้วนเป็นรั้วที่กักเอาต์พุตไว้ ขอให้สังเกตจุดนี้ หากให้อิสระแก่ AI มันจะคายค่าเฉลี่ยของ RPG ทั่วไปออกมา ดังนั้นจึงป้อน เกณฑ์การจัดประเภท (P0\~P1 ≤ 16 รายการ) · รูปแบบเอาต์พุต (ตาราง) · ข้อบังคับเรื่องเหตุผล และทางออกที่ว่า "ถ้าคลุมเครือให้ส่งต่อให้คน" ไว้ล่วงหน้า คุณภาพของการบีบอัดถูกกำหนดด้วยรั้วทั้งสี่นี้
| องค์ประกอบ | ประเภท | พื้นที่วาง | เหตุผล | คลุมเครือ |
|------------|------|--------------|----------------------------------------|------|
| hp_bar | P0 | ตรึงด้านบน | เกี่ยวกับการตายโดยตรง ต้องเช็กทุก 0.5 วินาที | |
| skill_slots| P0 | ขวาล่าง(โป้งขวา) | แกนการสั่งการ ต้องอยู่มุมที่โป้งขวาเอื้อมถึง | |
| mp_bar | P0 | ตรึงด้านบน | ตัดสินว่าใช้สกิลได้หรือไม่ จำเป็นตลอดเวลา | |
| target_frame| P0 | กลางบน | เช็กพลังชีวิตเป้าหมาย เป็นแกนการตัดสินรบ | |
| minimap | P1 | ขวาบนย่อขนาด | ใช้ดูทิศทาง ระหว่างรบย่อ/โปร่งแสงได้ | |
| buff_bar | P1 | ใต้ hp_bar | เช็กบัฟหมดอายุ ย่อไอคอนได้ | |
| quest_tracker| P2 | สลับเมนู | ระหว่างรบไม่จำเป็น ยามปกติสลับแสดง | |
| chat | P2 | แท็บซ้ายล่าง | ระหว่างรบซ่อนอัตโนมัติ แตะเพื่อกาง | ★ |
| party_frame| P1 | ซ้ายย่อขนาด | เมื่อเล่นเป็นปาร์ตี้เลื่อนเป็น P0 (มีเงื่อนไข) | ★ |
| mail_alert | P3 | คอนเท็กซ์ | แสดงชั่วครู่เฉพาะตอนได้รับ | |
| exp_bar | P3 | ล่างสุดของจอ 1px| คุณค่าข้อมูลต่ำ เส้นบาง ๆ ก็เพียงพอ | |
| currency | P2 | ในเมนู | ระหว่างรบไม่จำเป็น | |
ผลรวมที่แสดงตลอดเวลา (P0+P1): 7 รายการ (ผ่านกฎ 2: ≤16)
รายการคลุมเครือ 2 รายการ (chat, party_frame) ต้องให้คนตัดสิน:
- chat: ถ้าเป็นเกมที่สัดส่วนโซเชียลสูง พิจารณาเลื่อนเป็น P1
- party_frame: การจัดประเภทแยกออกตามคอนเท็กซ์เดี่ยว/ปาร์ตี้
ส่วนที่มีค่ามากที่สุดในเอาต์พุตไม่ใช่ตาราง แต่เป็น สองบรรทัดล่างสุด คือ "รายการคลุมเครือ 2 รายการ" จุดที่ AI แจ้งขีดจำกัดของตัวเองและส่งการตัดสินต่อให้คน เมื่อเปิดทางออกไว้ในพรอมต์ AI จะปักธงไว้ที่จุดที่มันไม่รู้ แทนที่จะฝืนกุคำตอบขึ้นมา
จะรับเอาต์พุตนี้มาดื้อ ๆ ไม่ได้ ต้องเอา rulebook ฟาดเสียทีหนึ่ง และในเซสชันนี้ก็มีหนึ่งรายการที่ถูกจับได้จริง
AI วาง party_frame ไว้แบบ "ซ้ายย่อขนาด" แต่ในการจับแนวนอน กลางซ้ายของหน้าจอเป็นพื้นที่ที่นิ้วโป้งทั้งสองข้างเอื้อมไม่ถึง (มือซ้ายผูกอยู่กับการเคลื่อนที่ที่ซ้ายล่าง มือขวาผูกอยู่กับสกิลที่ขวาล่าง) ทว่าเฟรมปาร์ตี้เป็น องค์ประกอบที่สั่งการได้ ที่ต้องคลิก (เล็งเป้าสมาชิกปาร์ตี้) นี่คือการละเมิดกฎ 3 ("องค์ประกอบที่สั่งการได้ต้องอยู่ที่มุมที่นิ้วโป้งสองข้างเอื้อมถึงง่าย") AI พลาดแฟล็ก 조작가능 ของ party_frame ไป เหตุเพราะในอินพุต yaml ช่อง 조작가능 ของ party_frame เว้นว่างไว้ — กล่าวคือเป็นข้อบกพร่องของข้อมูลฝั่งคนเอง
จึงร้องขอใหม่
party_frame เป็นองค์ประกอบที่สั่งการได้ ต้องคลิกเพื่อเล็งเป้าสมาชิกปาร์ตี้ (เมื่อกี้ตกหล่นไปจาก
อินพุต) องค์ประกอบที่สั่งการได้ต้องวางไว้ที่มุมที่นิ้วโป้งเอื้อมถึง ช่วยจัดตำแหน่งใหม่ด้วยกฎนี้
และเสนอแยกกรณีตอนเล่นเดี่ยวกับตอนเล่นปาร์ตี้
จบลงด้วยการไป-กลับเพียงครั้งเดียวนี้ AI ตอบใหม่ว่าตอนเล่นเดี่ยวให้ "ซ่อน" และตอนเล่นปาร์ตี้ให้ "เลื่อนเป็นขวาล่าง (ง่าย)" และการตัดสินนั้นก็ผ่าน rulebook หากคนทำการบีบอัด 30 รายการตั้งแต่ต้นจะใช้เวลาครึ่งวัน แต่ถ้าใช้ร่างเบื้องต้นของ AI + การตรวจสอบด้วย rulebook + การไป-กลับ 1 ครั้ง ก็อยู่ในราวหนึ่งชั่วโมง (การประมาณของผู้เขียน — เวลาที่ประหยัดได้แม่นยำเพียงใดขึ้นอยู่กับทีมและจำนวนองค์ประกอบ จึงควรอ่านในแง่ความต่างเชิงโครงสร้างระหว่าง "ทำด้วยมือตั้งแต่ต้น" กับ "ร่าง+ตรวจสอบ" มากกว่าค่าสัมบูรณ์)
หากตรึง "พื้นที่นิ้วมือ" ที่ปรากฏซ้ำในเซสชันข้างต้นไว้ด้วยภาพสักครั้ง การตัดสินตำแหน่งทุกอย่างหลังจากนั้นก็จะเร็วขึ้น เมื่อถือโทรศัพท์ในแนวนอน ด้านล่างที่นิ้วเอื้อมถึงและสายตามักจ้องไปบ่อยจะแบ่งออกเป็นสามที่ นิ้วโป้งซ้ายเอื้อมถึงมุมซ้ายล่าง (เคลื่อนที่) นิ้วโป้งขวาเอื้อมถึงมุมขวาล่าง (สกิล) และ กลางล่างระหว่างนิ้วโป้งสองข้าง เป็นที่วางไอเทมใช้สิ้นเปลือง ไอเทมอัตโนมัติ และช่องสกิล แม้ไม่ใช่การสั่งการแบบ twitch แต่ก็เป็นพื้นที่ชำเลือง (glance) สำคัญที่มองเห็นสิ่งที่ตัวเองใช้หรือถูกใช้อัตโนมัติได้ในพริบตาและกดได้เป็นครั้งคราว การสั่งการและสล็อต P0 เป็นสีเขียว ส่วนด้านบนและกลางด้านบนที่นิ้วเอื้อมไม่ถึงและทำได้แค่อ่านเป็นสีแดง
กฎนั้นเรียบง่าย ข้อมูลที่อ่านอย่างเดียว (HP/MP/พลังชีวิตเป้าหมาย) วางไว้ที่สีแดง (ด้านบน·กลางด้านบน) ก็ได้ เพราะไม่มีโอกาสที่นิ้วจะไปแตะ ในทางกลับกัน องค์ประกอบที่ต้องกดต้องอยู่ภายในพื้นที่นิ้วมือ (เขียว·เหลืองอำพัน) — การเคลื่อนที่และสกิลอยู่ที่มุมล่างทั้งสองข้าง ไอเทมสิ้นเปลือง·ไอเทมอัตโนมัติ และควิกสล็อต·ช่องสกิลอยู่ที่กลางล่าง ทั้งสามที่ล้วนเป็นจุดที่นิ้วเอื้อมถึงและสายตาไปบ่อย เหตุผลที่ party_frame ถูกจับใน §14.1.2 อธิบายได้ด้วยภาพแผ่นเดียวนี้ — เพราะนำองค์ประกอบที่ต้องกดไปวางไว้ที่กลางซ้าย (พื้นที่อ่าน) แทนที่จะเป็นพื้นที่นิ้วมือ
หากตรวจด้วยตาทุกครั้งว่าแบบบีบอัดทำตาม rulebook หรือไม่ ก็จะพลาดอีก ในห้ากฎของ §14.1.1 สิ่งที่ตัดสินได้ด้วยพิกัด·ขนาดให้โค้ดเป็นผู้ตรวจ คนใช้เวลาเฉพาะกับการตัดสิน "คลุมเครือ" ที่โค้ดจับไม่ได้เท่านั้น
# hud_lint.py — การตรวจสอบแบบจัดวาง HUD บนมือถือ (โครงสร้าง)
# อินพุต: แบบจัดวางที่ AI เสนอ (พิกัด·ขนาด·สั่งการได้·ประเภท ของแต่ละองค์ประกอบ)
# เอาต์พุต: รายการการละเมิด rulebook
MIN_TAP_PT = 44 # พื้นที่สัมผัสขั้นต่ำของ Apple HIG (pt)
def in_action_zone(e, w, h):
"""พื้นที่ที่นิ้วเอื้อมถึงในการจับแนวนอน: มุมล่างซ้าย·ขวา + แถบสล็อตกลางล่าง"""
x, y = e["x"] / w, e["y"] / h
bottom = y > 0.55
left_corner = bottom and x < 0.30 # โป้งซ้าย = เคลื่อนที่
right_corner = bottom and x > 0.70 # โป้งขวา = สกิล
center_slot = (y > 0.72) and (0.35 <= x <= 0.65) # กลางล่าง = สิ้นเปลือง·ควิกสล็อต
return left_corner or right_corner or center_slot
def lint(elements, screen_w, screen_h):
issues = []
for e in elements:
# กฎ A: องค์ประกอบสั่งการ/สล็อตต้องอยู่ในพื้นที่นิ้วมือ (มุมทั้งสอง + กลางล่าง)
if e["조작가능"] and not in_action_zone(e, screen_w, screen_h):
issues.append(f"[A] {e['id']}: องค์ประกอบสั่งการ·สล็อตถูกวางนอกพื้นที่นิ้วมือ "
f"(x={e['x']}, y={e['y']})")
# กฎ B: ขนาดต่ำสุดของพื้นที่สัมผัส (HIG 44pt)
if e["조작가능"] and min(e["w"], e["h"]) < MIN_TAP_PT:
issues.append(f"[B] {e['id']}: พื้นที่สัมผัส {min(e['w'], e['h'])}pt "
f"< {MIN_TAP_PT}pt (ต่ำกว่า HIG)")
# กฎ C: ปริมาณรวมที่แสดงตลอดเวลาของ P0/P1
onscreen = [e for e in elements if e["분류"] in ("P0", "P1")]
if len(onscreen) > 16:
issues.append(f"[C] แสดงตลอดเวลา {len(onscreen)} รายการ > 16 รายการ (แน่นเกินไป)")
return issues
มี 30 บรรทัดนี้แล้ว ในการประชุมคำถามที่ว่า "ปุ่มนี้เล็กไปไหม?" จะกลายเป็นวัตถุของการตัดสิน ไม่ใช่ประเด็นถกเถียง เมื่อโค้ดพิมพ์ออกมาว่า [B] skill_slots: พื้นที่สัมผัส 40pt < 44pt (ต่ำกว่า HIG) ก็ไม่จำเป็นต้องรวบรวมความเห็น แก้เลยก็พอ นี่คือการย้าย lint gate ที่กล่าวไว้ใน 9.1 (HUD) มาสู่มิติของมือถือ — การแบ่งงานที่ว่าสิ่งที่จับได้แบบ deterministic ให้โค้ด ส่วนสิ่งที่เป็น non-deterministic และต้องใช้การตัดสินให้คน ก็ตั้งอยู่ได้เหมือนเดิมบนมือถือเช่นกัน
เมื่อมองทั้งวงจรในภาพเดียวจะได้ดังนี้
flowchart LR
A["HUD บน PC 30 รายการ
(ดึงจากชีตข้อมูล)"] --> B["AI บีบอัด
จัด P0~P3 + จัดวาง"]
B --> C{"hud_lint.py
ตรวจ rulebook อัตโนมัติ"}
C -->|ละเมิด| D["ร้องขอใหม่
(แก้การตกหล่น·วางผิด)"]
D --> B
C -->|ผ่าน| E["คนตรวจสอบ
เฉพาะการตัดสิน 'คลุมเครือ'"]
E --> F["ยืนยัน HUD บนมือถือ
ราว 12~16 รายการ"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class C code;
class B ai;
class E human;
class A data;
class F pass;
ที่ที่มือคนสัมผัสมีเพียงสองแห่ง คือที่ที่ป้อนข้อมูลอินพุตให้สะอาด (หน้าสุด) และที่ที่ตัดสินคลุมเครือที่ rulebook จับไม่ได้ (หลังสุด) ส่วนการบีบอัด 30 รายการที่น่าเบื่อระหว่างนั้น AI และ lint เป็นผู้รัน
จะบันทึกเพียงสั้น ๆ ถึงแหล่งที่มาของตัวเลขที่ปรากฏในบทนี้ (หลักการเรื่องตัวเลขของทั้งเล่มดูที่คำนำ 「คำสัญญาหนึ่งข้อ」) พื้นที่สัมผัส 44pt (HIG) · 48dp (Material) · คอนทราสต์ 4.5:1 (WCAG) เป็นมาตรฐานทางการของแพลตฟอร์ม ส่วน "ข้อมูลที่แสดงตลอดเวลา 8\~12 รายการ" และ "บีบอัดจากครึ่งวัน→หนึ่งชั่วโมง" เป็นการประมาณบนพื้นฐานประสบการณ์ของผู้เขียน (ยังไม่ได้ตรวจสอบ) จึงควรอ่านในแง่ ทิศทาง มากกว่าค่าสัมบูรณ์ ตัวชี้วัดที่วัดได้จริงใน HUD บนมือถือคือจำนวนการละเมิด rulebook (lint 0) จำนวนองค์ประกอบที่แสดงตลอดเวลา (เป้าหมาย ≤12) และอัตราการแตะพลาด (telemetry) ส่วนตัวชี้วัดผลลัพธ์อย่างอัตราการคงอยู่ (Retention) ไม่ได้ถูกกำหนดด้วย HUD เพียงอย่างเดียว จึงไม่ฟันธงความเป็นเหตุเป็นผล
| รูปแบบ | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| ย่อ HUD บน PC มาแปะตรง ๆ | 30 รายการปกคลุมหน้าจอ 6 นิ้วจนมองเกมไม่เห็น | เซสชันบีบอัด §14.1.2 |
| "เอไอ ช่วยทำ UI มือถือให้หน่อย" มอบหมายแบบเหมาทั้งก้อน | ไม่มี rulebook ก็ได้แค่ค่าเฉลี่ยของ RPG ทั่วไป | ป้อน rulebook (§14.1.1) ลงในพรอมต์ก่อน |
| ตรวจแบบบีบอัดด้วยตาเปล่าอย่างเดียว | พลาดการละเมิดขนาดสัมผัส·โซนนิ้วโป้งทุกครั้ง | ตรวจอัตโนมัติด้วย hud_lint.py |
| ประชุม "เอาอันนี้ออกเถอะ" โดยไม่มีเหตุผล | ข้อสรุปเปลี่ยนไปทุกครั้ง | บังคับใช้ P0\~P3 + เหตุผลหนึ่งบรรทัด |
ถ้าทำคนเดียวแค่นี้พอ: ไม่ต้องมีชีตข้อมูลก็ได้ ลองเขียนองค์ประกอบ HUD บน PC ของเกมตัวเอง (หรือเกมที่ชอบ) ด้วยมือสัก 10\~15 รายการให้เป็น yaml แล้วแปะพรอมต์ของ §14.1.2 ลงไปตามนั้นแล้วรันดูสักครั้ง ลองหาสักหนึ่งรายการที่คุณไม่เห็นด้วยกับการจัดประเภทของ AI แล้วโต้กลับว่า "ให้เหตุผลมาใหม่" คุณจะได้สัมผัสด้วยตัวเองว่าการบีบอัดคือกลุ่มก้อนของการตัดสินแบบไหน
ถ้าเป็นทีมให้เริ่มจากหนึ่งขั้นถัดไปนี้ ดึงรายการองค์ประกอบ HUD ปัจจุบันออกมาเป็น hud_pc_inventory.yaml (มีอยู่ในชีตข้อมูลอยู่แล้ว) แล้วตรึงกฎสามบรรทัด (ขนาดสัมผัส·โซนนิ้วโป้ง·ปริมาณรวม) ของ hud_lint.py ใน §14.1.4 ให้เป็นโค้ดก่อน เมื่อมี rulebook แล้ว ไม่ว่าจะเป็นแบบบีบอัดของ AI หรือแบบร่างของคน ก็วัดด้วยเส้นเดียวกันได้
ในวันที่นำอัลฟาบิลด์ขึ้น PC เป็นครั้งแรก มีภาพหน้าจอภาพหนึ่งถูกโพสต์ลงในแชนเนลแชตภายในทีมออกแบบ จอยสติ๊กเสมือนที่บนมือถือเคยเต็มขอบล่างของหน้าจอ กลับลอยอยู่กลางจอมอนิเตอร์ขนาด 27 นิ้วเท่าฝ่ามือ มีคนหนึ่งเขียนต่อท้ายไว้บรรทัดเดียวว่า "อันนี้จะจับด้วยเมาส์ยังไง" ตรรกะแกนกลาง (core logic) ยังคงปกติดี ทั้งระบบต่อสู้ ทั้งกระเป๋าไอเทม ทั้งเควสต์ ก็ทำงานได้เหมือนเดิมทุกอย่าง สิ่งเดียวที่พังคือจุดที่ตรึงอินพุตและหน้าจอไว้บนสมมติฐานของมือถือ
หากนำเกมเดียวกันออกไปยังสามที่คือ iOS·Android·PC ดูเหมือนภาระการดำเนินงานจะกลายเป็น ×3 แต่ในความเป็นจริงไม่ได้เป็นเช่นนั้น ตรรกะแกนกลางมีเพียง 1 ชุด แล้วมีเลเยอร์ปรับตัวตามแพลตฟอร์ม (platform adaptation layer) มาเกาะอยู่เป็น ×3 ปัญหาคือ "ตรงไหนคือ core และตรงไหนเริ่มเป็นเลเยอร์ปรับตัว" เป็นสิ่งที่คนตัดสินทีละจุดได้ยาก กิ่งเงื่อนไขที่ทำงานบน iOS ได้แต่พังเฉพาะ Android, การแมปคีย์ที่มีความหมายเฉพาะบน PC — ความแตกต่างเหล่านี้ไม่ได้เข้าหัวได้หมด ดังนั้นแก่นของบทนี้คือเวิร์กโฟลว์ที่ กำหนดข้อจำกัดของแพลตฟอร์มให้เป็นลายลักษณ์อักษรในรูปของ rulebook (กฎ/คู่มือกฎ) แล้วให้ AI สร้างข้อเสนอกิ่งเงื่อนไขโดยอ้างอิงจาก rulebook นั้น และในขั้นสุดท้ายให้ lint จับการละเมิดกฎ
ก่อนอื่นมาดูภูมิประเทศของความแตกต่างกัน ด้านล่างคือตารางข้อจำกัดของแพลตฟอร์มที่จัดทำไว้ระหว่างพิจารณาการออกตัวเสริมบน PC ในโปรเจกต์ A (เกม MMORPG แบบมือถือเป็นหลักที่ผู้เขียนเข้าร่วมในฐานะ Design Director) ในบรรดาตัวเลข ค่าที่อ้างอิงจากมาตรฐานสาธารณะได้ระบุแหล่งที่มาไว้ด้วย ส่วนค่าอื่นเป็นค่าที่ตกลงกันภายในโปรเจกต์
| ขอบเขต | iOS | Android | PC |
|---|---|---|---|
| อินพุต | ทัช | ทัช (+คีย์บอร์ดบางส่วน) | คีย์บอร์ด·เมาส์·เกมแพด |
| ขนาดเป้าทัชต่ำสุด | 44pt (Apple HIG) | 48dp (Material) | คลิก — ไม่เกี่ยว |
| หน้าจอ | 4.7\~6.7 นิ้ว | 4.5\~7 นิ้ว (ผันแปรมาก) | 21\~32 นิ้ว |
| การชำระเงิน | App Store | Google Play | ของตนเอง·Steam |
| การแจ้งเตือน | APNs | FCM | OS·ของตนเอง |
| การจัดเก็บ | iCloud | Google Drive·ของตนเอง | Steam Cloud·ของตนเอง |
| รอบการเปลี่ยน OS | 1\~2 ปี | 1 ปี (แตกเป็นเสี่ยงมาก) | 5\~10 ปี |
iOS กับ Android มี API ของการชำระเงิน·การจัดเก็บ·การแจ้งเตือนที่ต่างกัน แต่หน้าจอและการควบคุมที่ผู้ใช้เห็นแทบจะเหมือนกัน ส่วน PC นั้นอินพุต·หน้าจอ·เอฟเฟกต์ทางสายตาต่างกันทั้งยวง ดังนั้นภาระการดำเนินงานจึงไม่ได้เป็น ×3 อย่างที่สัญชาตญาณบอก แต่ใกล้เคียง ×2 มากกว่า — เพราะระยะห่างระหว่าง iOS กับ Android นั้นสั้น
สิ่งที่สำคัญตรงนี้ไม่ใช่ตัวตารางเอง แต่คือการเปลี่ยนตารางนี้จาก เอกสารที่คนอ่าน ให้กลายเป็น rulebook ที่เครื่องอ่าน เพื่อให้ AI ใช้เป็นฐานในการสร้างข้อเสนอกิ่งเงื่อนไข และให้ lint จับการละเมิดได้
โครงสร้างโฟลเดอร์ของโปรเจกต์ A เป็นรูปแบบที่นำเลเยอร์ปรับตัวตามแพลตฟอร์ม 3 ชุดมาเกาะกับ core 1 ชุด
game/
├── core/ — ตรรกะเกม (ไม่ขึ้นกับแพลตฟอร์ม)
│ ├── combat/ inventory/ narrative/ ...
├── platform/ — เลเยอร์ปรับตัวตามแพลตฟอร์ม
│ ├── ios/ → input/ payment/ notification/
│ ├── android/ → input/ payment/ notification/
│ └── pc/ → input/ payment/ ui/
└── shared/ — ใช้ร่วมทั้งสองฝั่ง (ยูทิล·เรนเดอริง)
กฎมีเพียงข้อเดียว core จะไม่เรียก platform ด้วยชื่อ ทันทีที่ core มีประโยคอย่าง if platform == "ios" การแยกเลเยอร์ก็พังลง ยกตัวอย่างเรื่องอินพุต core รู้แค่เจตนาว่า "ใช้สกิล 1" (InputIntent.SKILL_1) เท่านั้น ส่วนการที่จะดึงเจตนานั้นมาจากพิกัดทัชหรือจากคีย์บอร์ดปุ่ม 1 เป็นความรับผิดชอบของเลเยอร์ platform แต่ละชุด
เมื่อขีดเส้นนี้ไว้ ขั้นถัดไปก็เป็นไปได้ เวลาเพิ่มแพลตฟอร์มใหม่ ไม่ต้องไปแตะ core แค่เติมโฟลเดอร์หนึ่งโฟลเดอร์ใต้ platform/ ก็พอ ด้านล่างคือภาพที่แสดงในหน้าเดียวว่าเส้นนี้แยกออกจากกันอย่างไรในความเป็นจริง
กล่อง iOS กับ Android เป็นโทนสีน้ำเงินเดียวกัน มีเพียง PC ที่เป็นสีส้ม — แสดงขนาดของความแตกต่างด้วยสี ความไม่สมมาตรของภาระการดำเนินงานมองเห็นได้ในแวบเดียวตรงนี้
จุดเปลี่ยนสำคัญอยู่ตรงนี้ หากเขียนข้อจำกัดของแพลตฟอร์มไว้ในเอกสารร้อยแก้ว คนก็จะลืม แทนที่จะทำเช่นนั้น ให้รวบรวมไว้ใน ไฟล์ rulebook แบบประกาศ (declarative) ไฟล์เดียว นี่คือส่วนที่ตัดมาจาก platform_rules.yaml ที่ใช้ในโปรเจกต์ A (คัดเฉพาะกฎหลักจากไฟล์จริงมาเพื่อบทนี้)
# platform/platform_rules.yaml
targets:
ios:
min_touch_pt: 44 # Apple HIG
contrast_ratio: 4.5 # WCAG SC1.4.3
gamepad: optional # มาตรฐาน iOS 17+
forbidden_in_core: ["import platform.ios", "StoreKit", "APNs"]
android:
min_touch_dp: 48 # Material
contrast_ratio: 4.5
forbidden_in_core: ["import platform.android", "BillingClient", "FCM"]
pc:
min_target_px: 24 # WCAG SC2.5.8 (พอยน์เตอร์)
input: ["keyboard", "mouse", "gamepad"]
forbidden_in_core: ["import platform.pc", "SteamAPI"]
required_intents: ["MOVE_FORWARD", "ATTACK", "SKILL_1", "SKILL_2"]
ไฟล์นี้ทำสามอย่างพร้อมกัน (1) เป็น สเปก ที่ AI อ่านตอนสร้างข้อเสนอกิ่งเงื่อนไข (2) เป็น เกณฑ์ ที่ lint ใช้ตรวจสอบ (3) เป็น แหล่งข้อมูลเดียว (single source) ที่คนบันทึกข้อตกลงไว้ forbidden_in_core สำคัญเป็นพิเศษ — เพราะเป็นรายการโทเค็นที่ห้ามปรากฏในโฟลเดอร์ core เด็ดขาด จึงเป็นฐานในการจับการรุกล้ำข้ามเลเยอร์ได้แบบกลไก
ตอนนี้มาตามงานจริงตั้งแต่ต้นจนจบ สถานการณ์เป็นเช่นนี้ ระหว่างเตรียมการออกตัวเสริมบน PC ต้องแยกตัวจัดการอินพุต (input handler) ที่เคยมีแต่บนมือถือออกมาเป็นเวอร์ชันสำหรับ PC โดยอ้างอิงจาก rulebook ขอร่างอแดปเตอร์อินพุต PC จาก Claude ตรวจสอบด้วย lint แล้ววนหนึ่งรอบจนถึงการปฏิเสธ·ขอใหม่
ด้านล่างคือส่วนที่ตัดมาจาก rulebook แพลตฟอร์มของโปรเจกต์เรา และตัวจัดการอินพุต iOS เดิม
[วาง platform_rules.yaml ส่วนที่ตัดมาด้านบน]
# platform/ios/input/touch_handler.py from core.input.input_intent import InputIntent def map_touch_to_intent(touch_event): if touch_event.location in skill_button_1_area: return InputIntent.SKILL_1 if touch_event.is_drag_up: return InputIntent.MOVE_FORWARD return Noneช่วยสร้างร่าง
platform/pc/input/keyboard_handler.pyโดยอ้างอิงจากอันนี้ ข้อจำกัด: (1) ให้ import core ได้ แต่ห้าม import platform.ios/android เด็ดขาด (2) ต้องแมปrequired_intentsครบทั้ง 4 ตัว (3) ให้แยกการแมปคีย์ออกเป็น dict เพื่อให้ผู้ใช้เปลี่ยนได้ ส่วนไหนที่ไม่มั่นใจช่วยทำเครื่องหมายไว้ด้วย
นี่คือร่าง ได้แยกการแมปคีย์ออกมาเป็น dict ที่ด้านบนของโมดูลเพื่อให้ผู้ใช้ปรับแต่งเองได้
# platform/pc/input/keyboard_handler.py from core.input.input_intent import InputIntent DEFAULT_KEYMAP = { "w": InputIntent.MOVE_FORWARD, "1": InputIntent.SKILL_1, "2": InputIntent.SKILL_2, # สมมติว่า ATTACK ใช้คลิกซ้ายของเมาส์ — ไม่ได้กำหนดค่าเริ่มต้นบนคีย์บอร์ด } def map_keyboard_to_intent(key_event, keymap=DEFAULT_KEYMAP): return keymap.get(key_event.key.lower()) def map_mouse_to_intent(mouse_event): if mouse_event.button == "left": return InputIntent.ATTACK return None⚠️ ส่วนที่ไม่มั่นใจ: ใน
required_intentsมี ATTACK อยู่ แต่ผมแมปสิ่งนี้ไปที่ เมาส์ ไม่ใช่คีย์บอร์ด เนื่องจากไม่มีข้อกำหนดชัดเจนว่าrequired_intentsใน rulebook หมายถึง "แมปได้โดยไม่เกี่ยงอุปกรณ์อินพุตก็ถือว่าผ่าน" หรือ "ต้องจัดการให้ครบภายในตัวจัดการตัวเดียว" ผมจึงสมมติเป็นกรณีแรก ถ้าเป็นกรณีหลัง lint อาจจับว่า ATTACK ขาดหายไปได้
จุด ⚠️ ที่ Claude ชี้ไว้ตรงนี้คือกับดักพอดี lint ของเรา (platform_lint.py) ตรวจการครบถ้วนของ required_intents ในระดับ แพลตฟอร์ม ไม่ใช่ระดับ ไฟล์ ของตัวจัดการ ถึงแม้ ATTACK จะไม่มีใน keyboard_handler.py แต่ถ้ามีอยู่ในฝั่ง mouse_handler ก็ถือว่าผ่าน ทว่าผลลัพธ์ที่ Claude สร้างออกมานั้นใส่การแมปเมาส์ไว้ในไฟล์ keyboard_handler.py รวมกันไปเลย — ความรับผิดชอบของไฟล์จึงปะปนกัน โครงสร้างจะผ่านก็จริง แต่ละเมิดกฎโฟลเดอร์ของเรา (แยกไฟล์ตามอุปกรณ์อินพุต) ปฏิเสธ
เหตุผลของการปฏิเสธชัดเจนในสองบรรทัด (1) ให้แยกการแมปเมาส์ออกเป็น mouse_handler.py ต่างหาก (2) ให้วาง Space เป็น fallback เพื่อให้ใช้ ATTACK บนคีย์บอร์ดได้ด้วย
หลังขอใหม่ ได้เวอร์ชันที่แยกแล้วมา จึงนำมารันผ่าน platform_lint.py lint อ่าน rulebook แล้วตรวจสิ่งต่อไปนี้
$ python platform_lint.py platform/pc/
[core-leak] PASS — โฟลเดอร์ core/ มีโทเค็น forbidden 0 รายการ
[intent-cover] PASS — pc: MOVE_FORWARD, ATTACK, SKILL_1, SKILL_2 (4/4)
[touch-target] SKIP — pc มี min_target_px=24 (ตรวจแยกที่เลเยอร์ UI)
[no-cross-import] PASS — platform.pc ไม่อ้างอิง platform.ios/android
การที่ intent-cover ออกมา 4/4 คือหัวใจ ว่าร่างที่ AI สร้างขึ้นเป็นไปตามเกณฑ์ของ rulebook หรือไม่ ถูกชี้ขาดด้วยสคริปต์ ไม่ใช่ด้วยสายตาคน บรรทัดเดียวนี้แทนที่งานที่คนต้องคอยตรวจคำนวณในหัวทุกครั้งในการดำเนินงานหลายแพลตฟอร์ม
หากบีบอัดวงรอบนี้ให้เป็นภาพ จะได้ดังนี้
flowchart LR
R[platform_rules.yaml
rulebook] --> P[ใส่ rulebook+ตัวจัดการเดิม
ลงในพรอมต์]
P --> A[ข้อเสนอกิ่งเงื่อนไขจาก Claude
+ เครื่องหมายความไม่แน่นอน]
A --> H{การตรวจสอบโดยมนุษย์}
H -->|ปฏิเสธ: ความรับผิดชอบไฟล์ปะปน| P
H -->|รับ| L[platform_lint.py]
L -->|FAIL| P
L -->|PASS| M[ไปยังกิ่งของบิลด์]
R -.ให้เกณฑ์.-> L
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class L code;
class A ai;
class H human;
class R data;
class M pass;
จุดที่ rulebook ป้อนเกณฑ์ให้ ทั้งสองฝั่ง คือพรอมต์และ lint คือศูนย์กลางของโครงสร้างนี้ AI สร้าง คนตัดสิน และ lint ชี้ขาด — สามบทบาทมองที่ rulebook เดียวกัน
เมื่อตัวจัดการพร้อมแล้ว บิลด์ก็เป็นเพียงการประกอบอย่างเรียบง่าย core กับ shared คงที่ เปลี่ยนเฉพาะโฟลเดอร์ platform หนึ่งโฟลเดอร์
[core/ + shared/ + platform/ios/] → บิลด์ iOS
[core/ + shared/ + platform/android/] → บิลด์ Android
[core/ + shared/ + platform/pc/] → บิลด์ PC
ใน CI ให้รันทั้งสามตัวนี้ แบบขนาน ไม่ใช่ตามลำดับ และรัน platform_lint.py อัตโนมัติทันทีหลังแต่ละบิลด์ ถ้ารันตามลำดับ เวลาบิลด์จะกลายเป็น 3 เท่า และถ้าตัด lint ออก การละเมิดกฎจะรอดมาจนถึงขั้นการเผยแพร่ บิลด์แบบขนาน + lint อัตโนมัติ สองสิ่งนี้คือเงื่อนไขขั้นต่ำของ CI แบบหลายแพลตฟอร์ม
เนื่องจากรอบการออกตัวต่างกันในแต่ละแพลตฟอร์ม จึงไม่ได้เผยแพร่พร้อมกันเพียงเพราะบิลด์ผ่าน การตรวจสอบของ iOS โดยปกติใช้เวลา 1\~3 วัน จึงระมัดระวังกับการออกตัวบ่อย ๆ ส่วน Android สะท้อนผลภายในไม่กี่ชั่วโมงจึงออกได้ถี่กว่า และ Steam อยู่ที่ราว 1\~2 วัน แม้จะเป็นการเปลี่ยนแปลงเดียวกัน iOS ก็เป็นตัวที่ออกช้าที่สุด ดังนั้นกำหนดการของฮอตฟิกซ์จึงคำนวณย้อนกลับโดยยึด iOS เป็นเกณฑ์เสมอ
ใต้โค้ดลงไป หน้าจอก็แยกตัวเช่นกัน จากประสบการณ์ การกระจายที่แนะนำคือคอมโพเนนต์ร่วม 80% ความแปรผันตามแพลตฟอร์ม (ต่างกันแค่ขนาด·ตำแหน่ง) 15% และเฉพาะแพลตฟอร์ม 5% ทว่าอัตราส่วนนี้ผันแปรไปตามแนวเกม — ถ้าเป็นเกมพัซเซิลแคชวล ส่วนร่วมจะขึ้นไปถึง 90% และ MMORPG จะมีความแปรผันเพิ่มขึ้นเพราะความต่างของอินพุต
คอมโพเนนต์เฉพาะคือจุดที่ดึงเสน่ห์ของแพลตฟอร์มออกมา จึงไม่ใช่ว่าการทำให้เป็นส่วนร่วมเสมอไปคือคำตอบ สิ่งที่มีความหมายเฉพาะบนแพลตฟอร์มนั้น ๆ อย่างจอยสติ๊กเสมือน·การสั่นของมือถือ หรือ UI แมปคีย์·การตั้งค่าเกมแพดของ PC จะอยู่ตรงนี้ ทว่าหากเฉพาะแพลตฟอร์มเกิน 30% นั่นไม่ใช่เสน่ห์ แต่เป็นสัญญาณของภาระการดำเนินงาน — หากตั้งคำเตือน platform-specific-ratio ไว้ใน lint ถึงคนจะลืม บิลด์ก็จะคอยชี้ให้
มาถึงตรงนี้ก็เป็นเส้นขีดจำกัดของการที่ AI เข้ามาช่วยด้วยเช่นกัน ความแตกต่างของแพลตฟอร์มส่วนใหญ่เป็นขอบเขตของกฎเชิงกำหนด (deterministic) AI จึงถูกใช้ในการ สร้าง ข้อเสนอกิ่งเงื่อนไขที่เป็นไปตาม rulebook มากกว่าจะให้สำรวจตัวเลือกอย่างอิสระ การแนะนำการแมปอินพุต การแปลงแบบร่าง Figma เป็นความแปรผันตามแพลตฟอร์ม และการปรับข้อความให้เข้ากับหลายภาษา×หลายแพลตฟอร์ม เป็นจุดที่ AI ช่วยเติมได้จริง และผลลัพธ์นั้นต้องผ่าน lint เสมอ ก่อนจะก้าวไปสู่การทำงานอัตโนมัติแบบก้าวหน้า การมาตรฐานของอแดปเตอร์ต้องมาก่อน
ผลที่ใหญ่ที่สุดของการแยกเลเยอร์คือ ความเร็วในการเพิ่มแพลตฟอร์มใหม่ หากนำ PC มาเกาะโดยกองประโยค if ลงในโค้ดเบสเดียว ก็แทบจะเทียบเท่าต้นทุนในการสร้างเกมใหม่ แต่หากไม่แตะ core แค่เติม platform/pc/ เวลานั้นก็ลดลงอย่างมาก อัตราที่การเพิ่มแพลตฟอร์มใหม่เร็วขึ้นนั้นต่างกันไปในแต่ละโปรเจกต์ จึงไม่ขอฟันธงตัวคูณที่ชัดเจน — เพียงแต่ในการพิจารณาภายในของเรา ประเมินว่ากำหนดการเพิ่ม PC แบบเสริมลดลงเหลือไม่ถึงครึ่งเมื่อเทียบกับสมมติฐานโค้ดเบสเดียว (การประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ) ผลพลอยได้คือเหตุขัดข้องเฉพาะแพลตฟอร์มถูกแยกออกจากกัน และความน่าเชื่อถือของการเปลี่ยน core สูงขึ้น (แก้ที่เดียวก็สะท้อนผลอย่างสอดคล้องในทั้งสามบิลด์)
กับดักที่มักเหยียบและวิธีรับมือมีดังนี้
| กับดัก | วิธีรับมือ |
|---|---|
กิ่งเงื่อนไข if platform == ... ใน core บานปลาย |
สกัดด้วย lint forbidden_in_core แยกออกเป็นอแดปเตอร์ |
| ตรวจข้อเสนอกิ่งเงื่อนไขของ AI ด้วยสายตาคนเท่านั้น | ชี้ขาด intent-cover ด้วย platform_lint.py |
| ยัดการแมปอุปกรณ์อินพุตไว้ในไฟล์เดียว | แยกตัวจัดการตามอุปกรณ์ (keyboard/mouse) |
| คอมโพเนนต์เฉพาะ 30%+ | คำเตือน platform-specific-ratio พิจารณาทำเป็นส่วนร่วม |
| เผยแพร่ 3 แพลตฟอร์มพร้อมกันทันทีที่บิลด์ผ่าน | คำนวณย้อนกลับโดยยึด iOS ตามความต่างของรอบการออกตัว |
จุดร่วมของกับดักคือ "พยายามกันด้วยความทรงจำของคน" หากเขียนลงใน rulebook และดักด้วย lint ถึงคนจะลืม บิลด์ก็จะจำให้
setup. สร้าง platform/platform_rules.yaml ในโปรเจกต์ แล้วเขียน min_touch, contrast_ratio, forbidden_in_core, required_intents แยกตามแพลตฟอร์มดังส่วนที่ตัดมาด้านบน อย่ากุตัวเลขขึ้นมาเอง แต่ให้นำมาจากมาตรฐานสาธารณะ (มาตรฐานสาธารณะอย่างทัช 44pt·48dp·คอนทราสต์ 4.5:1 ให้ยึดตาม rulebook ใน §9.1; เป้าพอยน์เตอร์ PC 24px คือ WCAG SC2.5.8)
prompt. ใส่ส่วนที่ตัดจาก rulebook + ตัวจัดการของแพลตฟอร์มหนึ่งที่มีอยู่เดิมไปด้วยกัน แล้วร้องขอเช่นนี้ "ช่วยสร้างร่างตัวจัดการ platform/<แพลตฟอร์มใหม่>/input/ โดยรักษา rulebook นี้ ห้ามใส่โทเค็น forbidden_in_core เด็ดขาด แมป required_intents ให้ครบทุกตัว และทำเครื่องหมาย ⚠️ ในส่วนที่ไม่มั่นใจ"
verify. รัน platform_lint.py (สคริปต์ยาว 40 บรรทัดก็เพียงพอ) ที่อ่าน rulebook แล้วตรวจสิ่งต่อไปนี้ (1) โทเค็น forbidden_in_core ในโฟลเดอร์ core 0 รายการ (2) แมป required_intents ครบทุกตัวตามแต่ละแพลตฟอร์ม (3) ไม่มี cross-import ระหว่างโฟลเดอร์ platform หากมีแม้แต่ FAIL เดียว ให้ย้อนกลับไปที่พรอมต์ เขียนเหตุผลของการปฏิเสธแล้วขอใหม่
หากทำงานคนเดียวและไม่มี CI สำหรับบิลด์ ให้ลด rulebook จาก YAML เหลือเป็นเช็กลิสต์มาร์กดาวน์หน้าเดียว สามบรรทัดอย่าง "เป้า ≥44pt, ห้าม import แพลตฟอร์มใน core, แมปเจตนา 4 ตัว" ก็พอ แทนสคริปต์ lint ให้ส่งผลงานไปให้ AI แล้วสั่งว่า "ช่วยตัดสินเช็กลิสต์ 3 ข้อนี้ทีละข้อว่าผ่าน/ไม่ผ่าน" ก็จะทำหน้าที่แทนการตรวจคำนวณของคนได้ หัวใจไม่ได้อยู่ที่ขนาดของเครื่องมือ — แต่อยู่ที่การเขียนเกณฑ์ไว้นอกหัว และแยกการสร้างออกจากการตรวจสอบ
สมาชิกทีม B ที่เพิ่งได้รับ QA build มา ถือโทรศัพท์ด้วยมือเดียวพลางขมวดคิ้ว "กดสกิลสามครั้ง แต่ออกแค่สองครั้งเองครับ" เมื่อมองเข้าไปที่หน้าจอก็พบว่า ในจังหวะที่นิ้วโป้งกดปุ่มสกิลนั้น นิ้วเดียวกันนั้นได้บัง 1/3 ของพื้นที่ข้างปุ่มเอาไว้ มันเป็นปัญหาที่ไม่เคยเกิดขึ้นแม้แต่ครั้งเดียวเมื่อทดสอบด้วยเมาส์ เพราะเมาส์ไม่มีนิ้ว
ภาพนี้สรุปแก่นแท้ของทัชและเมาส์ได้ในบรรทัดเดียว ทั้งสองอย่างเป็นอินพุตแบบ "ชี้ไปยังจุดหนึ่ง" เหมือนกัน แต่อันหนึ่งเครื่องมือที่ใช้ชี้นั้นบังหน้าจอ ส่วนอีกอันไม่บัง เหตุผลที่ต้องคลี่คลายการกระทำเดียวกันให้แตกต่างกันในสองอินพุตเริ่มต้นจากตรงนี้ ในบทนี้เราจะเริ่มจากการสรุปความแตกต่างของสองอินพุตก่อน จากนั้นจะติดตามแกนหลักของบันทึกเซสชันจริง (worked transcript) หนึ่งเส้นจนจบ ซึ่งเป็นกระบวนการรับข้อเสนอการแมปอินพุตจาก AI แล้วตรวจสอบความขัดแย้ง (conflict) และการเข้าถึง (reachability) ด้วยตนเอง
ทั้งความหนาของนิ้ว การบังสายตา ขีดจำกัดของมัลติทัช และความแม่นยำ ล้วนแตกต่างกันทั้งหมด ก่อนจะตรึงเป็นกฎด้วยตาราง เรามาจับสัมผัสด้วยภาพหนึ่งภาพกันก่อน เคอร์เซอร์เมาส์คือปลายปากกาขนาด 1 พิกเซล ส่วนนิ้วคือตราประทับที่มีเส้นผ่านศูนย์กลางเกือบ 1 เซนติเมตร ปลายปากกาเขียนตัวอักษรได้ แต่ครั้งละหนึ่งตัว ตราประทับประทับได้เร็ว แต่เขียนตัวอักษรไม่ได้ และในขณะที่ประทับลงไปก็มองไม่เห็นกระดาษ
| คุณสมบัติ | ทัช | เมาส์ |
|---|---|---|
| ความแม่นยำ | ประมาณ 7\~10mm (พื้นที่สัมผัสของนิ้ว) | หน่วย 1px |
| การบังสายตา | นิ้วบังบริเวณรอบจุดสัมผัส | ไม่มี |
| โฮเวอร์ได้ | แทบเป็นไปไม่ได้ (สัมผัส = อินพุต) | อิสระ (เคลื่อนที่ ≠ อินพุต) |
| อินพุตพร้อมกัน | มัลติทัช 2\~10 จุด | ซ้าย·ขวา·กลาง·ล้อ |
| แยกแดร็ก/แท็ป | ต้องอนุมานจากเวลา·ระยะทาง | คลิก/แดร็กชัดเจน |
| ฟีดแบ็กแบบฮาพติก | ได้ | แทบไม่มี |
ในที่นี้ สองบรรทัดที่ส่งผลต่อการออกแบบมากที่สุดคือ "การบังสายตา" และ "โฮเวอร์" การบังสายตาบังคับว่าจะต้องแสดงผลลัพธ์ไว้ตรงไหน ส่วนการไม่มีโฮเวอร์หมายความว่าบนมือถือ ช่องทางข้อมูลหนึ่งช่องที่เรียกว่าทูลทิป (tooltip) หายไปทั้งช่อง อีกสี่บรรทัดที่เหลือนั้นใกล้เคียงกับรายละเอียดที่แตกแขนงออกมาจากสองบรรทัดนี้
มาตรฐานสาธารณะตอกตรึงความแตกต่างนี้ไว้เป็นตัวเลข — มาตรฐานสาธารณะอย่างทัช 44pt (HIG)·48dp (Material)·คอนทราสต์ 4.5:1·เป้าหมายทัช 24 พิกเซล CSS (WCAG SC2.5.8) เป็นไปตามรูลบุ๊ก §9.1 ตัวเลขเหล่านี้ไม่ใช่รสนิยม แต่เป็นผลผลิตของร่างกายมนุษย์และการวัด ดังนั้นไม้บรรทัดที่จะนำมาวัดเมื่อตรวจสอบการแมปก็คือมาตรฐานเหล่านี้ในที่สุด
เมื่อคลี่คลายสามการกระทำ คือ เคลื่อนที่·โจมตี·สกิล ด้วยสองอินพุต ก็จะแยกออกได้ดังนี้ การที่หนึ่งการกระทำมีวิธีอย่างละสามแบบ ไม่ได้หมายความว่าไม่มีคำตอบที่ถูกต้อง แต่หมายความว่าอัตลักษณ์ของเกมเป็นตัวบังคับการเลือก
โปรเจกต์ A (MMORPG ที่ให้มือถือมาก่อน) ที่ผู้เขียนทำอยู่ ในส่วนของการเคลื่อนที่ มือถือเลือกใช้แบบไฮบริด ⓐ+ⓑ (ใช้จอยสติ๊กควบคู่กับเคลื่อนที่อัตโนมัติ) ส่วน PC เลือกใช้ WASD+เคลื่อนที่อัตโนมัติ การโจมตี มือถือใช้ ⓑ+ⓐ (แท็ปศัตรูแล้วกดปุ่ม) ส่วน PC เลือกใช้ ⓐ·ⓑ ได้อย่างอิสระ สกิล มือถือใช้ ⓐ หรือเมื่อทาร์เก็ตติงก็ใช้ ⓑ ส่วน PC วางการเล็งด้วยเมาส์ทับลงบนปุ่ม 1\~8 การที่เกมเดียวกัน การกระทำเดียวกัน แต่ออกมาเป็นตารางการแมปสองแผ่น คือสาระทั้งหมดของบทนี้
ปัญหาอยู่ที่ว่ายิ่งตารางการแมปยาวขึ้น ความขัดแย้งก็ยิ่งซ่อนตัว จะเกิดอะไรขึ้นหากการกดสล็อตค้าง (เล็งสกิล) กับการแดร็กหน้าจอ (หมุนกล้อง) มาทับซ้อนกันในพื้นที่เดียวกัน หากปุ่ม 1\~8 ถูกจองไว้สำหรับสกิลแล้ว แต่มีคนเสนอให้คีย์ลัดของอินเวนทอรีเป็น 1 ด้วยล่ะ การกวาดสายตาดูตารางด้วยตามนุษย์นั้นพลาดได้ ดังนั้นจึงจำเป็นต้องมีเวิร์กโฟลว์ที่รับข้อเสนอการแมปจาก AI แต่มอบหมายการตรวจสอบให้กับเครื่องมือ
จากตรงนี้ไปคือแกนหลักของบทนี้ เราจะยกกระบวนการทั้งวงจรหนึ่งรอบมาตรงๆ ตั้งแต่รับข้อเสนอร่างการแมปคีย์บอร์ด/เมาส์ของ PC จาก Claude จากนั้นมนุษย์ตรวจสอบผลลัพธ์นั้น รัน lint ตรวจความขัดแย้ง·การเข้าถึงเพื่อปฏิเสธ แล้วร้องขอใหม่ เราจะไม่สรุปย่อ ในผลลัพธ์ดิบมีความผิดพลาดปนอยู่ด้วย และสิ่งที่เราต้องการเห็นคือกระบวนการในการจับความผิดพลาดนั้น
คุณคือผู้ออกแบบการแมปอินพุตของ PC MMORPG จงเสนอ JSON ที่แมปรายการการกระทำด้านล่างลงบนคีย์บอร์ด/เมาส์
การกระทำ: เคลื่อนที่ (หน้าหลังซ้ายขวา), วิ่ง, กระโดด, โจมตีพื้นฐาน, สกิล1\~8, อินเวนทอรี, แผนที่, หน้าต่างตัวละคร, เมนู, สลับล็อกมุมมอง, ซูมเข้า/ซูมออก, เมนูบริบท (การกระทำเสริม), สลับเคลื่อนที่อัตโนมัติ
ข้อจำกัด: - ทำตามธรรมเนียมของวงการ (เคลื่อนที่ด้วย WASD, เมนูด้วย ESC เป็นต้น) - อย่าวางสองการกระทำซ้อนทับลงบนคีย์/ปุ่มทางกายภาพเดียวกัน - แต่ละรายการมีฟิลด์
action,input,noteผลลัพธ์เป็น JSON array เท่านั้น
[
{"action": "move_forward", "input": "W", "note": "เคลื่อนที่มาตรฐาน"},
{"action": "move_back", "input": "S", "note": "เคลื่อนที่มาตรฐาน"},
{"action": "move_left", "input": "A", "note": "เคลื่อนที่มาตรฐาน"},
{"action": "move_right", "input": "D", "note": "เคลื่อนที่มาตรฐาน"},
{"action": "run", "input": "Shift", "note": "วิ่งระหว่างที่กดค้าง"},
{"action": "jump", "input": "Space", "note": "มาตรฐาน"},
{"action": "attack_basic", "input": "Mouse1", "note": "การกระทำหลักด้วยคลิกซ้าย"},
{"action": "skill_1", "input": "1", "note": ""},
{"action": "skill_2", "input": "2", "note": ""},
{"action": "skill_3", "input": "3", "note": ""},
{"action": "skill_4", "input": "4", "note": ""},
{"action": "skill_5", "input": "5", "note": ""},
{"action": "skill_6", "input": "6", "note": ""},
{"action": "skill_7", "input": "7", "note": ""},
{"action": "skill_8", "input": "8", "note": ""},
{"action": "inventory", "input": "I", "note": "ยก Tab ให้การสลับเป้าหมาย"},
{"action": "map", "input": "M", "note": ""},
{"action": "character", "input": "C", "note": ""},
{"action": "menu", "input": "Escape", "note": "มาตรฐาน"},
{"action": "cam_lock", "input": "Mouse3", "note": "สลับล็อกมุมมองด้วยคลิกล้อ"},
{"action": "zoom_in", "input": "WheelUp", "note": ""},
{"action": "zoom_out", "input": "WheelDown", "note": ""},
{"action": "context_menu", "input": "Mouse2", "note": "การกระทำเสริมด้วยคลิกขวา"},
{"action": "auto_move", "input": "Mouse2", "note": "เคลื่อนที่อัตโนมัติด้วยดับเบิลคลิกขวา"}
]
ผลลัพธ์ดูสะอาดตา เพราะเช่นนั้นจึงอันตราย ดูสองบรรทัดล่างสุด context_menu และ auto_move ถูกจองไว้บน Mouse2 (คลิกขวา) ทั้งคู่ ทั้งที่ในข้อจำกัดได้ระบุไว้ชัดเจนว่า "อย่าวางสองการกระทำซ้อนทับบนคีย์เดียว" แต่โมเดลก็ยังซ้อนทับลงไป โดยแปะข้อแก้ตัวว่า "แยกแยะได้ด้วยดับเบิลคลิก" ลงในฟิลด์ note นี่คือเหตุผลที่ห้ามนำผลลัพธ์ของ AI ใส่เข้าไปใน build ตรงๆ เมื่อมนุษย์กวาดสายตาดูตาราง ก็มักจะพลาดความขัดแย้งระหว่างบรรทัดที่ 22 และ 23 จากทั้งหมด 23 บรรทัดได้ง่าย และโมเดลก็หาเหตุผลให้กับความขัดแย้งของตัวเอง
เพราะเช่นนั้นเราจึงมอบหมายการตรวจสอบให้กับโค้ดแทนสายตา เรารัน lint เล็กๆ ที่ตรวจสอบความขัดแย้ง (อินพุตเดียวกันซ้ำ) และการเข้าถึง (การกระทำที่จำเป็นขาดหาย, อยู่นอกมุมที่นิ้วโป้งสองข้างเอื้อมถึง)
# input_lint.py — ตรวจสอบความขัดแย้ง·การเข้าถึงของการแมปอินพุต
import json, sys
from collections import defaultdict
REQUIRED = {"move_forward","move_back","move_left","move_right",
"attack_basic","menu","inventory","map"}
def lint(mapping):
errors, warns = [], []
seen = defaultdict(list)
for m in mapping:
seen[m["input"]].append(m["action"])
# 1) ความขัดแย้ง: มีการกระทำตั้งแต่ 2 รายการบนอินพุตเดียวกัน
for inp, acts in seen.items():
if len(acts) > 1:
errors.append(f"CONFLICT {inp} <- {', '.join(acts)}")
# 2) การเข้าถึง: การกระทำที่จำเป็นขาดหาย
actions = {m["action"] for m in mapping}
for r in sorted(REQUIRED - actions):
errors.append(f"MISSING required action '{r}'")
# 3) เตือนเมื่อ note ว่าง (ไม่ได้ระบุเจตนาการออกแบบ)
for m in mapping:
if not m["note"].strip():
warns.append(f"NO_NOTE {m['action']} ({m['input']})")
return errors, warns
data = json.load(open(sys.argv[1], encoding="utf-8"))
errs, warns = lint(data)
for e in errs: print("[ERROR]", e)
for w in warns: print("[WARN] ", w)
print(f"\n=> {len(errs)} error(s), {len(warns)} warning(s)")
sys.exit(1 if errs else 0)
เมื่อบันทึก JSON ข้างต้นเป็น claude_map.json แล้วรัน lint ผลลัพธ์จริงเป็นดังนี้
[ERROR] CONFLICT Mouse2 <- context_menu, auto_move
[WARN] NO_NOTE skill_1 (1)
[WARN] NO_NOTE skill_2 (2)
[WARN] NO_NOTE skill_3 (3)
... (skill_4~8 เหมือนกัน)
=> 1 error(s), 8 warning(s)
lint จับความขัดแย้งเพียงหนึ่งเดียวที่ตามนุษย์พลาดไปได้อย่างแม่นยำ การตรวจการเข้าถึงผ่าน (มีการกระทำที่จำเป็นครบทั้ง 8 รายการ) note ว่าง 8 รายการเป็นเพียงคำเตือนและไม่ขัดขวาง build แต่ก็เผยให้เห็นหนี้ของการไม่ได้ระบุเจตนาการออกแบบ ทีนี้เราก็ถือเหตุผลในการปฏิเสธส่งกลับไปยังโมเดล
จากผล lint
Mouse2มี context_menu กับ auto_move ทับซ้อนกัน จึงปฏิเสธ การแยกด้วยดับเบิลคลิกทำให้เกิดดีเลย์ที่คลิกขวา และจะทำงานผิดพลาดระหว่างการต่อสู้ จงแยก auto_move ออกไปเป็นอินพุตต่างหาก อีกทั้ง note ของ skill_1\~8 ว่างเปล่า — จงเติมว่าแต่ละสล็อตเป็นสกิลตระกูลใดให้บรรทัดละหนึ่งบรรทัด
{"action": "context_menu", "input": "Mouse2", "note": "คลิกขวา = การกระทำเสริม/บริบทโดยลำพัง"},
{"action": "auto_move", "input": "Numpad0", "note": "สลับเคลื่อนที่อัตโนมัติ, แยกออกจากคีย์การต่อสู้ทางกายภาพ"},
...
{"action": "skill_1", "input": "1", "note": "ท่าหลักระยะประชิด"},
{"action": "skill_8", "input": "8", "note": "ท่าหลบฉุกเฉิน/ท่าเอาตัวรอด — นิ้วก้อยเอื้อมถึงเป็นขีดจำกัด จึงพิจารณาย้ายไป Q"}
บรรทัดสุดท้ายของผลลัพธ์ใหม่น่าสนใจ โมเดลรายงานปัญหาการเข้าถึงด้วยตัวเองว่า "ปุ่ม 8 เป็นขีดจำกัดที่นิ้วก้อยเอื้อมถึง" นี่เป็นหัวข้อเดียวกันเป๊ะกับการตรวจสอบการเข้าถึงที่เราจะกล่าวถึงในหัวข้อถัดไป เมื่อรัน lint อีกครั้งก็ผ่านด้วย 0 error(s) แก่นสำคัญคือสิ่งนี้ AI สร้างร่าง 23 บรรทัดได้อย่างรวดเร็ว แต่ความถูกต้องตามกฎของร่างนั้นได้รับการรับประกันโดยกฎที่มนุษย์นิยาม (เซต REQUIRED, นิยามความขัดแย้ง) และโดยโค้ด ข้อเสนอมาจากโมเดล การตัดสินมาจากเครื่องมือ การตัดสินใจมาจากมนุษย์
หากวาดเส้นทางที่อินพุตทางกายภาพหนึ่งครั้งถูกแปลงเป็นการกระทำในเกมไว้ ก็จะเห็นว่า lint ข้างต้นแทรกอยู่ที่จุดใด
flowchart TD
A[อินพุตทางกายภาพ
พิกัดทัช / คีย์·เมาส์] --> B{จำแนกอินพุต}
B -->|สัมผัส 200ms↓ & 5px↓| C[แท็ป / คลิก]
B -->|สัมผัส 200ms↑ or 5px↑| D[แดร็ก]
C --> E[ค้นหาตารางการแมป]
D --> E
E --> F{เป็นการแมป
ที่ผ่าน lint หรือไม่?}
F -->|ขัดแย้ง·ขาดหาย| G[ขัดขวาง build
input_lint.py]
F -->|ปกติ| H[ดิสแพตช์การกระทำในเกม]
H --> I[ดำเนินการกระทำ]
I --> J[แสดงฟีดแบ็ก
ภาพ+ฮาพติก/เสียง]
G -.แก้ไขแล้วส่งใหม่.-> E
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class B,E,F code;
class G fail;
อินพุตทางกายภาพที่เข้ามาจากมุมบนซ้ายจะถูกจำแนกก่อนว่าเป็นแท็ปหรือแดร็ก (ตามเกณฑ์ 200ms/5px ของหัวข้อถัดไป) อินพุตที่ถูกจำแนกแล้วจะค้นหาตารางการแมป โดยจุดสำคัญของภาพนี้คือ ตารางนั้นต้องผ่าน input_lint.py ก่อนที่จะเข้าไปใน build หากมีความขัดแย้งหรือขาดหาย ก็จะไปถึงขั้นตอนดิสแพตช์ไม่ได้และถูกขัดขวาง การตรวจสอบการแมปต้องจบที่ build gate ก่อนถึงรันไทม์
ทีนี้สมมติว่าการแมปผ่านแล้ว เราจะออกแบบพื้นผิวที่การแมปนั้นมาบรรจบกับนิ้ว
หลักข้อ 1 — พื้นที่ทัชขั้นต่ำ Apple HIG 44pt, Material 48dp คือขีดล่าง บนหน้าจอ HD หากตั้งไว้ที่ราว 100px (200px ในสภาพแวดล้อมเรตินา 2 เท่า) ก็จะตอบสนองทั้งสองมาตรฐานได้พร้อมกัน หากต่ำกว่านี้ลงไป "กดสามครั้งออกสองครั้ง" ในบทนำข้างต้นก็จะปรากฏออกมาเป็นสถิติ
หลักข้อ 2 — พื้นที่เอื้อมของนิ้วโป้ง MMORPG บนมือถือใช้การจับสองมือในแนวนอนเป็นมาตรฐาน องค์ประกอบที่ต้องกดวางไว้ที่มุมล่างทั้งสองข้าง·ของบริโภค/สล็อตวางไว้ตรงกลางด้านล่าง (พื้นฐานของโมเดลสามพื้นที่อยู่ที่ §9.1) การกระทำ P0 (ซ้าย=เคลื่อนที่, ขวา=โจมตี·สกิล) วางไว้ภายในสองมุมล่างซ้าย·ขวา ส่วนข้อมูลที่ไม่ค่อยได้มองวางไว้ด้านบนนอกขีดจำกัดที่เอื้อมถึง ประเด็นสำคัญในการออกแบบอินพุตคือ แม้รวมสองมุมเข้าด้วยกันก็ยังไม่ถึงครึ่งหน้าจอ SVG ถัดไปแสดงพื้นที่เอื้อมของนิ้วโป้งสองมือและแถบสล็อตตรงกลางด้านล่างในโหมดแนวนอน
รูปพัดสีเข้มคือจุดที่นิ้วโป้งเอื้อมถึงได้โดยไม่ฝืน รูปพัดสีจางคือขีดจำกัดที่ต้องเหยียดมือไปจึงจะถึง หาก "ขีดจำกัดของนิ้วก้อยที่ปุ่ม 8" ที่โมเดลรายงานในผลลัพธ์ใหม่ที่ 14.3.3 เป็นฉบับ PC ความผิดพลาดที่ฉบับมือถือทำคือการวางปุ่ม P0 ไว้ในพื้นที่สีจางนี้พอดี
หลักข้อ 3 — หลีกเลี่ยงการบังสายตา นิ้วไม่ได้บังแค่จุดสัมผัส แต่มือทั้งมือยังคลุมบังหน้าจอเหนือจุดนั้นขึ้นไป เมื่อแท็ปสกิลที่มุมล่างขวา มุมล่างขวาประมาณ 1/4 ก็จะมองไม่เห็น เพราะเช่นนั้นผลลัพธ์ของการกระทำ (ตัวเลขดาเมจ, การเปลี่ยนสถานะ) จึงต้องแสดงในพื้นที่ที่นิ้วเอื้อมไม่ถึง มือที่กุมจอยสติ๊กฝั่งซ้ายจะรุกล้ำตำแหน่งของตัวละครและมินิแมป จึงย้ายมินิแมปไปไว้มุมบนขวา
หลักข้อ 4 — แยกแดร็ก/แท็ป ทัชต่างจากเมาส์ตรงที่ต้องอนุมานเจตนาของผู้ใช้จากเวลาและระยะทาง เราจะรวมให้เป็นเกณฑ์เดียวกันทั้งเกม — ตัวอย่างเช่น หากสัมผัสภายใน 200ms และเคลื่อนภายใน 5px ถือเป็นแท็ป มากกว่านั้นถือเป็นแดร็ก หากสองตัวเลขนี้กระท่อนกระแท่น ความล้มเหลวของเจตนาอย่าง "ตั้งใจจะแท็ปแต่ตัวละครกลิ้ง" ก็จะสะสมขึ้น จุดแยกในไดอะแกรม mermaid ข้างต้นคือการตัดสินนี้พอดี
หลักข้อ 5 — ฮาพติก การสั่นเป็นช่องทางเดียวที่ส่งถึงผู้ใช้ได้แม้ไม่มองหน้าจอ แต่หากให้การสั่นกับทุกอินพุตก็จะกลายเป็นสัญญาณรบกวน แท็ปทั่วไปไม่สั่น, ใช้สกิลสั่นสั้น, การกระทำเสี่ยงอย่างยืนยันการชำระเงินสั่นแรง, สังหารศัตรูสั่นแผ่วเบา — บริหารให้อยู่ภายใน 4\~5 แบบ
เมาส์ได้เพลิดเพลินกับความหรูหราสามอย่างที่ทัชไม่มี นั่นคือ โฮเวอร์, ปุ่มหลายปุ่ม, และความแม่นยำของเคอร์เซอร์
หลักข้อ 1 — โฮเวอร์ เมาส์ชี้ได้โดยไม่ต้องกด เมื่อวางเมาส์ลงบนสล็อตสกิล ทูลทิปชื่อ·คูลดาวน์·คำอธิบายก็จะปรากฏ และเมื่อคลิกจึงจะใช้งาน ทัชไม่มีสถานะกึ่งกลางนี้ ดังนั้นโฮเวอร์จึงเป็นช่องทางที่ PC สามารถวางข้อมูลเพิ่มได้ แต่ต้องตระหนักไว้ล่วงหน้าตั้งแต่ขั้นตอนการแมปที่ 14.3.3 ว่าข้อมูลที่พึ่งพาแต่โฮเวอร์อย่างเดียวจะไม่มีที่ไปในฉบับมือถือ
หลักข้อ 2 — ปุ่มหลายปุ่ม คลิกซ้ายคือการกระทำหลัก, คลิกขวาคือเสริม/บริบท, คลิกล้อคือรีเซ็ตมุมมอง, ล้อคือซูม ความขัดแย้งที่ lint ข้างต้นจับได้ก็คือกรณีที่วางสองการกระทำซ้อนกันบนคลิกขวานี้พอดี การพยายามเติมให้เต็มเพราะมีปุ่มเยอะกลับสร้างความขัดแย้งขึ้น
หลักข้อ 3 — มาตรฐานคีย์บอร์ด ESC=เมนู, M=แผนที่, 1\~8=สกิล, WASD=เคลื่อนที่, Shift=วิ่ง, Space=กระโดด ผู้ใช้ต้องเดาได้โดยไม่ต้องเรียนรู้ คีย์ที่เบี่ยงออกจากมาตรฐานก็ให้ระบุเหตุผลที่สมควรลงใน note ดังตัวอย่างการตัดสินใจที่ 14.3.3 ที่ยก Tab ให้การสลับเป้าหมายแทนอินเวนทอรี
หลักข้อ 4 — การควบคุมมุมมอง หมุนมุมมองด้วยการแดร็กเมาส์ แต่ให้สลับโหมดเกมที่ล็อกเคอร์เซอร์ไว้ที่หน้าจอกับโหมด UI ที่ปลดเคอร์เซอร์อย่างชัดเจน หากการสลับนี้คลุมเครือ ก็จะเกิดความสับสนแบบปิดเมนูแล้วเคอร์เซอร์หายไป
หลักข้อ 5 — ขอบเขตอนุญาตมาโคร·อัตโนมัติ การอนุญาตให้โจมตีอัตโนมัติ·เคลื่อนที่อัตโนมัติได้ถึงไหนเป็นเรื่องของอัตลักษณ์เกม หากปล่อยมากเกินไป PC ก็จะกลายเป็นหน้าจอมาโคร และหากปิดกั้นแบบไม่มีเงื่อนไข กำแพงการเข้าถึงของผู้ใช้ที่ข้ามมาจากมือถือก็จะสูงขึ้น คำตอบคือการเลือกจุดใดจุดหนึ่งในสเปกตรัมให้เข้ากับสีสันของเกม ไม่ใช่สุดขั้วทั้งสองด้าน
หลักการต่างกันไปตามแต่ละแพลตฟอร์ม แต่ "ความรู้สึก" ที่ผู้ใช้ได้รับจากการกระทำเดียวกันต้องเหมือนกันแม้แพลตฟอร์มจะเปลี่ยนไป อย่าทำให้ผู้ใช้ที่ข้ามจากมือถือมาสู่ PC ต้องเรียนรู้ความหมายของการสว่างขึ้นของปุ่มใหม่อีกครั้ง
| สถานการณ์ | ทัช | เมาส์ |
|---|---|---|
| รับรู้อินพุต | ปุ่มสว่าง + ฮาพติกสั้น | ปุ่มสว่าง + เสียงคลิก |
| อินพุตล้มเหลว | ปุ่มสั่น + ฮาพติก | ปุ่มสั่น + เสียงเตือน |
| คูลดาวน์ดำเนินอยู่ | เกจวงกลม | เกจวงกลม |
| ฟื้นพร้อมใช้งาน | สว่าง + ฮาพติก | สว่าง + เสียง |
ช่องทางภาพ (สว่าง·สั่น·เกจ) เหมือนกันทั้งสองฝั่ง มีเพียงช่องทางเสริมเท่านั้นที่แยกเป็นฮาพติก↔เสียงให้เข้ากับแพลตฟอร์ม ความสอดคล้องนี้ช่วยลดต้นทุนการเรียนรู้ของผู้ใช้ข้ามแพลตฟอร์มลงครึ่งหนึ่ง
| รูปแบบ | การแก้ |
|---|---|
| ปุ่มต่ำกว่าขีดล่างมาตรฐาน (44pt/48dp) | บังคับให้อยู่ราว 100px |
| แสดงผลลัพธ์ในพื้นที่ที่นิ้วบัง | ย้ายไปพื้นที่ที่ไม่ถูกบัง |
| ใช้ฮาพติกพร่ำเพรื่อ | ภายใน 4\~5 แบบ |
| ซ้อนสองการกระทำบนคลิกขวา | ขัดขวางความขัดแย้งด้วย lint แล้วแยก |
| ยกข้อมูลเฉพาะโฮเวอร์ไปไว้บนมือถือตรงๆ | มือถือใช้ช่องทางทดแทนเป็นแท็ป/แท็ปค้าง |
| ตรึงการแมปคีย์ตายตัว | อนุญาตให้ผู้ใช้ปรับแต่งเอง |
| บังคับให้การแมปเหมือนกันทั้งสองแพลตฟอร์ม | ใช้การแมปที่เป็นธรรมชาติตามแต่ละแพลตฟอร์ม |
บรรทัดที่สี่ของตารางนี้คือบทสรุปของบันทึกเซสชันจริงที่ 14.3.3 ความขัดแย้งของคลิกขวานั้นแทบทุกครั้งจะถูกพลาดเมื่อกวาดสายตาดูตารางด้วยตามนุษย์ และแทบทุกครั้งจะถูกจับได้หากแขวน lint ไว้ที่ build gate
input_lint.py ข้างต้นไว้ในโปรเจกต์ เปลี่ยนเซต REQUIRED ให้เป็นการกระทำที่จำเป็นของเกมคุณเองpython input_lint.py claude_map.json ระบุเหตุผลในการปฏิเสธ (อินพุตที่ขัดแย้ง·การกระทำที่ขาดหาย) เพื่อร้องขอใหม่จนกว่า ERROR จะเป็น 0 ส่วน WARN (note ว่าง) ให้บันทึกแยกไว้เป็นหนี้ของการไม่ได้ระบุเจตนาการออกแบบหากเป็นเกมเล็กๆ ที่ทำคนเดียว ก็ลดเครื่องมือลง หากการกระทำมีไม่เกิน 10 อย่าง lint 20 บรรทัดที่เหลือไว้แค่สองอย่างคือเซต REQUIRED และการตรวจสอบ "อินพุตเดียวกันซ้ำ" ก็เพียงพอ รับการแมปจาก AI กรองแค่ความขัดแย้งด้วยมินิ lint นี้ แล้วลองกดบนเครื่องจริงสักครั้งว่านิ้วโป้ง (หรือนิ้วก้อย) เอื้อมถึงหรือไม่ ข้อเสนอจากโมเดล, การตัดสินความขัดแย้งจากโค้ด, การตัดสินการเอื้อมถึงจากมือของคุณเอง — เพียงรักษาสามอย่างนี้ไว้ก็ใช้ได้ไม่ว่าจะขนาดเท่าใด
ผู้อ่านหลัก: นักออกแบบเกม (Game Designer) ที่รับผิดชอบการให้บริการเกม (Live Ops) หลังเปิดตัวเป็นครั้งแรก (ทีมขนาดกลาง 10\~50 คน) ฉบับย่อสำหรับผู้อ่านคนเดียว/มือสมัครเล่น: §15.1.7 「ถ้าทำคนเดียว เท่านี้ก็พอ」
ข้อตั้งต้น: ผู้เขียนเคยผ่านการให้บริการเกม (Live Ops) ของเกมมือถือ MMORPG ที่เปิดตัวระดับโลก รวมถึงระบบเศรษฐกิจ P2E (Play To Earn) ด้วย แล้วนำประสบการณ์นั้นมาผสมกับเวิร์กโฟลว์ AI ก่อนเปิดตัวของโปรเจกต์ปัจจุบันในการเขียนบทนี้ บันทึกเซสชันจริง (worked transcript) คือผลของการรันรูปแบบ "อินพุต → AI ประกอบ → rulebook ตรวจสอบ → คนเลือก" ในรูปแบบของการให้บริการเกม (Live Ops) จริงหนึ่งครั้ง สิ่งที่เป็นการประมาณและการสังเกตได้ระบุไว้ชัดเจนว่าเป็นการประมาณหรือการสังเกต และไม่ได้ใส่ตาราง KPI ที่กุขึ้น
เช้าวันถัดจากเปิดตัว ออฟฟิศจะต่างไปจากช่วงก่อนเปิดตัว แม้มายล์สโตนจะจบลงแล้ว งานกลับไม่ได้ลดลง ตรงกันข้าม หน่วยของงานยิ่งย่อยลงเท่านั้น กำหนดการที่เคยจัดเป็นรายไตรมาสถูกซอยลงเป็นหน่วยสัปดาห์ วัน และชั่วโมง แล้วทุกสัปดาห์ก็มีคำถามเดิมวกกลับเข้ามาในห้องประชุม "สุดสัปดาห์นี้จะรันอีเวนต์อะไรดี"
หากคำถามนี้เริ่มต้นใหม่จากกระดาษเปล่าทุกสัปดาห์ ทีมให้บริการเกม (Live Ops) ก็จะหมดแรงในไม่ช้า บทนี้กล่าวถึงวิธีดึงคำถามนั้นออกจากกระดาษเปล่า แก่นมีสองข้อ ข้อแรก แทนที่จะคิดอีเวนต์และซีซันขึ้นใหม่ทุกครั้ง ให้สะสมไว้เป็น ไลบรารีของรูปแบบที่ผ่านการตรวจสอบแล้ว ข้อสอง งานร่างต้นฉบับที่น่าเบื่ออย่าง "ประกอบรูปแบบเหล่านั้นเป็นตัวเลือก 5 รายการสำหรับสัปดาห์หน้า" ให้ AI ทำ ส่วนคนตัดสินใจแค่ว่า จะรับตัวเลือกใดในบรรดาตัวเลือกที่ผ่านการตรวจสอบของ rulebook ภาระงานของการสร้างจากศูนย์กับการเลือกจาก 5 รายการนั้นต่างกัน
มีหนังสือจำนวนมากให้ท่องจำวงจรมาตรฐานของการให้บริการเกม (Live Ops) เป็นตาราง เรื่องที่ว่ารายงานในวันจันทร์ เตรียมงานในวันอังคารพุธ และปล่อยในวันศุกร์ ทั้งหมดถูกต้อง แต่การท่องแค่ตารางไม่ทำให้เห็นว่าการตัดสินใจที่วกกลับมาทุกสัปดาห์อย่าง "อีเวนต์สัปดาห์นี้" ถูกตัดสินอย่างไร แก่นแท้ของการให้บริการเกม (Live Ops) ไม่ใช่ตารางเวลา แต่เป็น ลูปปิด — หนึ่งรอบที่ตัวเลือกถูกสร้างขึ้น ผ่านการตรวจสอบ คนเลือก ออกไปเป็นบิลด์ แล้วข้อมูลผู้ใช้กลับมาเป็นอินพุตของตัวเลือกถัดไป
บนลูปนี้ แกนทั้ง 4 ของการให้บริการเกม (Live Ops) (เนื้อหา·อีเวนต์·การปรับสมดุล·CS) หมุนด้วยความเร็วของตนเอง เนื้อหาเป็นหน่วยเดือนถึงไตรมาส อีเวนต์เป็นหน่วยสัปดาห์ถึงเดือน การปรับสมดุลเป็นหน่วยสัปดาห์ถึงสองสัปดาห์ และ CS เป็นหน่วยวันและชั่วโมง หากแกนทั้ง 4 หมุนแยกกัน แม้จะดูข้อมูลผู้ใช้ชุดเดียวกัน การตัดสินใจก็จะออกมาต่างกันทุกสัปดาห์ ด้วยเหตุนี้ เป้าหมายของบทนี้คือการมัดแกนทั้ง 4 เข้าเป็นลูปเดียว แล้วทำให้ช่องหนึ่งของลูปนั้น (การสร้างตัวเลือกอีเวนต์) อยู่ในรูปแบบที่ AI รันได้
flowchart TD
A["อินพุต
แนวโน้ม KPI · segment ผู้ใช้
· ผลของอีเวนต์ครั้งก่อน"] --> B["AI ประกอบ
ไลบรารี ซีซันรูล × เทมเพลตอีเวนต์
→ ตัวเลือก 5 รายการ"]
B --> C{"rulebook ตรวจสอบ
เพดานเงินเฟ้อ · ความขัดแย้งของวัตถุประสงค์
· ช่วงรางวัล · ระยะเวลา"}
C -->|alert การละเมิด| D["ขอใหม่
(เปลี่ยนตัวเลือกที่ละเมิด)"]
D --> B
C -->|ผ่าน| E["คนเลือก
Director รับ/ปฏิเสธ"]
E --> F["บิลด์·เผยแพร่
(ย้อนกลับไม่ได้: เริ่มซีซัน·ประกาศ)"]
F --> G["ข้อมูลผู้ใช้·ฟีดแบ็ก
(เป็นอินพุตของลูปถัดไป)"]
G --> A
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class A,G data;
class B ai;
class C,F code;
class E human;
ที่ที่มือของคนเข้าไปถึงมีเพียงสองจุด จุดบนสุดที่ใส่อินพุต (KPI·segment·ผลในอดีต) ให้สะอาด และจุดที่ตัดสินว่าจะปล่อยตัวเลือกใดในบรรดาตัวเลือกที่ผ่านการตรวจสอบ ส่วนงานน่าเบื่อระหว่างนั้นอย่าง "คิดตัวเลือกประกอบ 5 รายการ" และ "คัดกรองการละเมิดรูล" ให้ AI และ rulebook รัน และบรรทัดล่างสุดหนึ่งบรรทัด — ข้อที่ว่าข้อมูลผู้ใช้ซึ่งสร้างจากอีเวนต์ที่ออกไปเป็นบิลด์จะกลับมาเป็นอินพุตอีกครั้ง — คือสิ่งที่ทำให้ลูปนี้เป็นการให้บริการเกม (Live Ops) อย่างแท้จริง การออกแบบก่อนเปิดตัวพอออกไปครั้งหนึ่งก็จบ แต่ในการให้บริการเกม (Live Ops) ผลลัพธ์จะกลายเป็นอินพุตถัดไป
รายละเอียดของสองไลบรารีที่เข้าสู่ลูปนี้ (ซีซันรูล·เทมเพลตอีเวนต์) อยู่ใน §15.2 ส่วนช่องสุดท้าย (การจัดประเภทฟีดแบ็กผู้ใช้อัตโนมัติ) อยู่ใน §15.3 บทนี้มุ่งเน้นที่การเดินลูปให้ครบหนึ่งรอบจนจบ
ในที่นี้จะแสดงหนึ่งรอบจริงจนจบว่ารันอย่างไร ด้านล่างคือการจำลองเซสชันที่ผู้เขียนนำรูปแบบ "ประกอบไลบรารี → rulebook ตรวจสอบ → คนเลือก" ซึ่งได้ตรวจสอบในเครื่องมือเนื้อหาก่อนเปิดตัว มาย้ายลงในรูปแบบของการให้บริการเกม (Live Ops) (ซีซันรูล + เทมเพลตอีเวนต์) แล้วรันจริงหนึ่งครั้ง พรอมต์อินพุตคัดลอกไปใช้ได้ตามนั้น ส่วนเอาต์พุตเป็นการเรียบเรียงเซสชันนั้นขึ้นใหม่
ก่อนอื่นวางวัตถุดิบสองอย่างของการประกอบไว้ในรูปแบบที่เครื่องอ่านได้ คือ ไลบรารีเทมเพลตอีเวนต์ (รูปแบบที่ผ่านการตรวจสอบแล้ว) ไลบรารีซีซันรูล และสถานการณ์ปัจจุบันของสัปดาห์นี้ (KPI·segment) ไลบรารีเมื่อสร้างไว้ครั้งหนึ่งแล้วก็นำกลับมาใช้ได้ทุกสัปดาห์
# event_templates.yaml — ไลบรารีเทมเพลตอีเวนต์ที่ผ่านการตรวจสอบแล้ว (คัดมาแสดง 4 จาก 9 รายการ)
- id: tpl_attendance # รางวัลเช็กชื่อ
목적: [ผู้เล่นใหม่, ดึงผู้เล่นหลับกลับ]
기간_권장: 7~14일
보상등급: ต่ำ~กลาง
- id: tpl_coop_raid # เรดแบบร่วมมือ
목적: [กระตุ้นผู้เล่นเดิม, คอมมูนิตี้]
기간_권장: 3~7일
보상등급: กลาง~สูง
- id: tpl_pvp_season # ซีซันแข่งขัน
목적: [คอมมูนิตี้, กระตุ้นผู้เล่นเดิม]
기간_권장: 14~28일
보상등급: สูง
- id: tpl_limited_package # แพ็กเกจจำกัด
목적: [รายได้]
기간_권장: 3~7일
보상등급: สูง (ผูกกับการชำระเงิน)
# season_rules.yaml — ชิ้นส่วนซีซันรูล (คัดมาแสดง)
season_inflation_cap: อีเวนต์รางวัลระดับ 'สูง' ต่อไตรมาส ≤ 3 ครั้ง
purpose_conflict_rule: ห้ามอีเวนต์วัตถุประสงค์ [รายได้] 2 รายการพร้อมกันในสัปดาห์เดียว
overlap_rule: ห้ามอีเวนต์รางวัล 'สูง' 2 รายการพร้อมกัน (ความล้า·เงินเฟ้อ)
# current_state.yaml — สถานการณ์สัปดาห์นี้
주차: 2026-W23
직전2주_매출이벤트: 1 ครั้ง (สะสมไตรมาสระดับ 'สูง' 2 ครั้ง)
DAU_추이: ลดลงอย่างค่อยเป็นค่อยไป (4 สัปดาห์ที่ผ่านมา -6%, ตามการสังเกตในวงการอยู่ในช่วง 'เฝ้าระวัง')
주요_segment: สัดส่วนกลุ่มผู้เล่นหลับ ที่มีโอกาสกลับมา เพิ่มขึ้น
다가오는_외부일정: ไม่มี
ใช้ yaml ของเทมเพลต·ซีซันรูล·สถานการณ์สัปดาห์นี้ที่แนบมา ประกอบเป็นตัวเลือกอีเวนต์สำหรับสัปดาห์หน้าแค่ 5 รายการ
อย่าสร้างกลไกใหม่ ใช้เพียงการประกอบเทมเพลตที่แนบมาเท่านั้น ระบุเองว่าละเมิดซีซันรูลหรือไม่
และแนบเหตุผลคนละหนึ่งบรรทัดว่าทำไมแต่ละตัวเลือกจึงเหมาะกับสัญญาณตอนนี้ (DAU ลดลง·ผู้เล่นหลับกลับมา)
กระจายไม่ให้วัตถุประสงค์เอนไปทางรายได้อย่างเดียว และตัวที่กำกวมให้ทำเครื่องหมายส่งกลับมาให้ฉัน
ทำเป็นตารางเท่านั้น ช่องคือ ตัวเลือก·การประกอบเทมเพลต·วัตถุประสงค์หลัก·ระยะเวลา·การละเมิดรูล·เหตุผล·กำกวม
ขอให้สังเกตว่าในกฎห้าบรรทัดนั้น ไม่มีบรรทัดใดบอกว่า "จะสร้างอะไร" ทั้งหมดเป็นข้อจำกัดว่า "ภายในกรอบใด ตอบโดยอ้างอะไรเป็นเหตุผล" เพราะหากให้อิสระแก่ AI มันจะคายอีเวนต์เฉลี่ยของ RPG ทั่วไปออกมา จำกัดวัตถุดิบให้อยู่ในไลบรารี (กฎ 1) บังคับให้ตรวจสอบซีซันรูลด้วยตนเอง (กฎ 2) บังคับให้มีเหตุผลตอบสนองต่อสัญญาณปัจจุบัน (กฎ 3) กระจายวัตถุประสงค์ (กฎ 4) และทางออกที่ว่า "ถ้ากำกวมให้ส่งต่อให้คน" (กฎ 5) — ห้าบรรทัดนี้รวมกันเป็นการกำหนดไม่ใช่รูปร่างของตัวเลือก แต่เป็นคุณสมบัติของตัวเลือก
ด้านล่างคือเอาต์พุตดิบที่ไม่ได้ย่อ มีการละเมิดรูลและการแจ้งความกำกวมปะปนกัน
| 후보 | 템플릿조합 | 주목적 | 기간 | 룰위반 | 근거 | 애매 |
|-----|------------------------|-----------|-------|--------|--------------------------------------------|------|
| C1 | tpl_attendance | ดึงผู้เล่นหลับกลับ | 10일 | ไม่มี | ตอบโจทย์โดยตรงต่อสัดส่วนกลุ่มหลับที่กลับมาเพิ่มขึ้น รางวัล 'กลาง' | - |
| C2 | tpl_coop_raid | กระตุ้นผู้เล่นเดิม | 5일 | ไม่มี | สร้างแรงจูงใจล็อกอินช่วง DAU ลด กระตุ้นคอมมูนิตี้ | - |
| C3 | tpl_attendance+tpl_coop_raid | ดึงผู้เล่นหลับกลับ+กระตุ้น | 7일 | ไม่มี | เช็กชื่อดึงกลับ + เรดยึดผู้เล่นไว้ ซินเนอร์จีเมื่อรวม | - |
| C4 | tpl_pvp_season | คอมมูนิตี้ | 21일 | ไม่มี | ซีซันยาวดันเส้นโค้งการคงอยู่ขึ้นทั้งเส้น | ★ |
| C5 | tpl_limited_package | รายได้ | 5일 | ★ละเมิด | สะสมไตรมาสระดับ 'สูง' 2 ครั้ง → เพิ่มอีกจะใกล้/เกินเพดาน | - |
ผลการตรวจสอบรูล:
- C5: เสี่ยงละเมิด season_inflation_cap อีเวนต์รางวัลระดับ 'สูง' ไตรมาสนี้
มีแล้ว 2 ครั้ง หาก C5 เพิ่มอีกจะเป็น 3 ครั้งซึ่งติดขอบเพดาน การผลักดันรายได้
ในช่วง DAU ลดอาจเพิ่มความล้าของผู้ใช้ และยังสวนทางกับสัญญาณด้วย
- C4: ไม่มีการละเมิดรูล แต่ซีซัน PvP 21 วันไม่ใช่แค่สัปดาห์นี้สัปดาห์เดียว
แต่กินเวลา 3 สัปดาห์ ต้องใช้การตัดสินเชิงนโยบายปฏิบัติการว่าเหมาะเป็น
'ตัวเลือกสัปดาห์นี้' หรือไม่ → ทำเครื่องหมายกำกวม
- C1·C2·C3 ผ่านรูล สอดคล้องกับสัญญาณปัจจุบัน (หลับ·DAU)
ส่วนที่มีคุณค่าที่สุดในเอาต์พุตไม่ใช่ตาราง แต่เป็น สามบรรทัดผลการตรวจสอบรูลที่ล่างสุด AI จับการละเมิดเงินเฟ้อของ C5 ได้เองและทำเครื่องหมายไว้ ส่วน C4 บอกว่า "ไม่ใช่การละเมิด แต่ความเห็นแตก" แล้วส่งต่อให้คน พรอมต์ที่ดีทำให้ AI สามารถแจ้งขีดจำกัดของตนเองและส่งลูกต่อให้คนได้
อย่ารับเอาต์พุตนี้มาตามนั้น ตีด้วย rulebook อีกครั้งหนึ่ง แล้วจึงให้คนเลือก ในเซสชันนี้มีสองเรื่องที่แยกทางกันจริง
ก่อนอื่น C5 คือการปฏิเสธ AI ทำเครื่องหมายการละเมิดเงินเฟ้อไว้แล้ว และโค้ด rulebook (§15.1.3) ก็ตัดสินแบบเดียวกัน มันติดเพดานระดับ 'สูง' ของไตรมาส และในช่วง DAU ลด การผลักดันรายได้สวนทางกับสัญญาณปัจจุบัน ไม่มีอะไรให้ถกเถียง ตัดออก
ถัดมาคือ C4 (ซีซัน PvP 21 วัน) เป็นจุดที่ AI ส่งต่อด้วย "กำกวม" ไม่มีการละเมิดรูล แต่นี่ไม่ใช่ "อีเวนต์สัปดาห์นี้" แต่เป็น "การตัดสินใจเรื่องซีซัน" ไม่ใช่ประเด็นที่จะตัดสินทันทีในลูปขนาดหนึ่งสัปดาห์ แต่ต้องยกขึ้นไปยังการประชุมรวมซีซัน ดังนั้นจึงพักไว้จากตัวเลือกของสัปดาห์นี้ และแยกออกไปเป็นวาระของปฏิทินซีซันต่างหาก
ในบรรดา C1·C2·C3 ที่เหลือ Director เป็นผู้เลือก ตัวที่เข้ากับสัญญาณปัจจุบัน (สัดส่วนกลุ่มผู้เล่นหลับที่มีโอกาสกลับมาเพิ่มขึ้น + DAU ลดลงอย่างค่อยเป็นค่อยไป) ได้ดีที่สุดคือ C3 (เช็กชื่อ+เรดร่วมมือ รวมกัน) ซินเนอร์จีของการดึงกลุ่มหลับเข้ามาด้วยการเช็กชื่อ แล้วยึดผู้ใช้ที่ดึงเข้ามาไว้ด้วยเรด สอดคล้องกับสัญญาณของสัปดาห์นี้ ส่วน C1·C2 เก็บไว้ในพูลตัวเลือกของสัปดาห์หน้า
ยังมีตัวเลือกอีกหนึ่งที่ยังไม่จบ ณ จุดนี้ เมื่อตัดสินรับ C3 แล้ว ระยะเวลา 7 วันไปทับกับวันปิดปรับปรุงเซิร์ฟเวอร์ตามรอบที่กำลังจะมาถึงหนึ่งวัน จึงมีการขอใหม่หนึ่งรอบ
รับ C3 แต่วันสุดท้ายในช่วง 7 วันไปทับกับวันปิดปรับปรุงตามรอบ
ปรับระยะเวลาแล้วเสนอใหม่ เพื่อไม่ให้การปิดปรับปรุงไปตัดการเข้าร่วมช่วงท้ายของอีเวนต์
คงปริมาณรางวัลรวมไว้เท่าเดิม ขยับเฉพาะกำหนดการให้เร็วขึ้น
AI ตอบกลับมาใหม่โดยเลื่อนวันเริ่มต้นให้เร็วขึ้นหนึ่งวันเพื่อให้จบก่อนการปิดปรับปรุง และการปรับนั้นผ่าน rulebook หนึ่งรอบของ อินพุต → AI ประกอบ → rulebook ตรวจสอบ → คนเลือก → การปรับกำหนดการใหม่ ปิดลงที่นี่
หนึ่งรอบนี้คือเกณฑ์ Show ของทั้งเล่มนี้ หากไม่เคยดูจนจบสักครั้งว่า AI ประกอบอะไร rulebook คัดกรองอะไร คนเลือกอะไรและปฏิเสธอะไร ประโยคที่ว่า "ดึงตัวเลือกอีเวนต์ด้วย AI" ก็จะกลวงเปล่า
หากดูด้วยตาทุกสัปดาห์ว่าตัวเลือกรักษาซีซันรูลไว้หรือไม่ ก็จะพลาดอีก ในบรรดาสามรูลของ §15.1.2 สิ่งที่ตัดสินได้ด้วยตัวเลขให้โค้ดเป็นผู้ตรวจ คนใช้เวลาเฉพาะกับ "กำกวม" และ "การเลือก" ที่โค้ดจับไม่ได้เท่านั้น
# event_lint.py — ตรวจสอบตัวเลือกอีเวนต์สำหรับสัปดาห์หน้า (โครงร่าง)
# อินพุต: รายการตัวเลือกที่ AI ประกอบ + ซีซันรูล + สถานะสะสมไตรมาส
# เอาต์พุต: รายการการละเมิดรูล (ไม่ใช่การปฏิเสธอัตโนมัติ แต่เป็น alert)
def lint(candidates, season, quarter_state):
issues = []
high_used = quarter_state["high_reward_count"] # จำนวนสะสมระดับ 'สูง' ในไตรมาส
for c in candidates:
# กฎ A: เพดานเงินเฟ้อ (ระดับ 'สูง' ต่อไตรมาส ≤ 3)
if c["보상등급"] == "고" and high_used + 1 > season["inflation_cap"]:
issues.append(f"[A] {c['id']}: เพิ่มระดับ 'สูง' จะเกินเพดานไตรมาส "
f"{season['inflation_cap']}회 초과 (현재 {high_used})")
# กฎ B: ห้ามวัตถุประสงค์ [รายได้] 2 รายการในสัปดาห์เดียวกัน
sales = [c for c in candidates if "매출" in c["목적"]]
if len(sales) > 1:
issues.append(f"[B] [매출] 목적 후보 {len(sales)}개 동시 → 1개로 제한")
# กฎ C: วัตถุประสงค์เอนเอียง (ถ้าวัตถุประสงค์หนึ่งเกินครึ่งใน 5 รายการ = กระจายไม่พอ)
from collections import Counter
top = Counter(c["주목적"] for c in candidates).most_common(1)[0]
if top[1] > len(candidates) // 2:
issues.append(f"[C] 목적 '{top[0]}' {top[1]}개 쏠림 (분산 부족)")
return issues
โค้ดนี้สรุปการเถียงกันไปมาในที่ประชุมอย่าง "รางวัลนี้แรงเกินไปหรือเปล่า" ให้จบด้วยตัวเลขหนึ่งบรรทัด หากโค้ดพิมพ์ออกมาว่า [A] tpl_limited_package: '고'등급 추가 시 분기 한도 3회 초과 (현재 2) ก็ไม่มีอะไรให้ถกเถียง ตัดออกก็พอ นี่คือการย้าย lint gate ที่กล่าวถึงใน §14.1 (HUD บนมือถือ) มาสู่ระดับการให้บริการเกม (Live Ops) — การแบ่งงานที่ว่าสิ่งที่จับได้ด้วยความเป็นเชิงกำหนด (deterministic) ให้โค้ดทำ ส่วนสิ่งที่ต้องใช้การตัดสินให้คนรับ ตั้งอยู่ได้เช่นเดิมในงานปฏิบัติการด้วย
แต่มีสิ่งหนึ่งที่ต่างออกไป lint นี้แม้พบการละเมิด ก็ ไม่ทิ้งตัวเลือกโดยอัตโนมัติ มันแค่ยก alert ขึ้นมาเท่านั้น เป็นการออกแบบแบบเดียวกับที่เห็นใน §6.2 (เครื่องสร้างเมือง) หากติดการตรวจสอบแบบปฏิเสธอัตโนมัติ เครื่องจะฆ่าแม้กระทั่งการดัดแปลงที่ตั้งใจไว้ (เช่น การตัดสินใจของแคมเปญที่จงใจใส่อีเวนต์รายได้ทั้งที่รู้ถึงเพดานไตรมาส) ให้เครื่องดึงตัวเลือกที่น่าสงสัยออกมา แต่จะฆ่าหรือไว้ชีวิตให้ Director เป็นผู้ตัดสิน การปฏิเสธ C5 ใน §15.1.2 ก็ไม่ใช่ lint เป็นผู้ฆ่า แต่เป็นการตัดสินใจที่คนกำหนดหลังเห็น alert ของ lint
จุดที่ลูปข้างต้นต่างจากลูปการออกแบบก่อนเปิดตัวอย่างชี้ขาดมีสองข้อ แทนที่จะนำมาเรียงเป็นตาราง ขอชี้เพียงสองข้อนี้อย่างแม่นยำ
ข้อแรก ผลลัพธ์กลายเป็นอินพุตถัดไป ก่อนเปิดตัว เมื่อเขียนเอกสารออกแบบเกม (GDD) แล้ว ทุกอย่างไหลทิศทางเดียวไปจนถึงบิลด์ ในการให้บริการเกม (Live Ops) ข้อมูลผู้ใช้ที่อีเวนต์สัปดาห์นี้สร้างขึ้น (อัตราการเข้าร่วม·การเลิกเล่น·รายได้·ฟีดแบ็ก) จะกลับมาเป็นอินพุต (current_state.yaml) ของการประกอบตัวเลือกในสัปดาห์หน้า ลูกศรล่างสุดของลูปใน §15.1.1 คือการวกกลับนั้น ดังนั้น KPI ของการให้บริการเกม (Live Ops) จึงไม่ใช่ "ทายให้ถูกครั้งเดียว" แต่เป็น "ปรับตามสัญญาณทุกสัปดาห์"
ข้อสอง ต้นทุนการทดลองเล็กลง แต่จุดที่ย้อนกลับไม่ได้กลับยิ่งคมขึ้น หากก่อนเปิดตัว การตัดสินใจครั้งหนึ่งกำหนดชะตาของทั้งไตรมาส ในช่วงไลฟ์เราลองรันอีเวนต์ขนาดหนึ่งสัปดาห์ ถ้าไม่เข้าก็เปลี่ยนในสัปดาห์หน้า การทดลองที่ย้อนกลับ (rollback) ได้เพิ่มขึ้น แต่ การเริ่มซีซันและการประกาศอีเวนต์เป็นสิ่งที่ย้อนกลับไม่ได้ หลักการ "การอัดเสียง·การคัดเลือกนักพากย์ = ขั้นที่ย้อนกลับไม่ได้" ที่กล่าวถึงใน §5.4.5 ทำงานเช่นเดิม ซีซันรูล·รางวัลที่ผู้ใช้เห็นไปแล้ว แม้จะ "ยกเลิก" ก็ทิ้งร่องรอยไว้ในการรับรู้ของคอมมูนิตี้ ดังนั้นการตรวจสอบทั้งหมดของลูปใน §15.1.1 (AI ประกอบ·rulebook·คนเลือก) ต้องจบในขั้นที่ย้อนกลับได้ ก่อน เข้าสู่ช่องที่ย้อนกลับไม่ได้อย่างบิลด์·ประกาศ การที่ดึง C4 (ซีซัน 21 วัน) ออกจากการตัดสินทันทีในสัปดาห์นี้แล้วยกขึ้นไปประชุมซีซันก็เป็นหลักการนี้ — ยิ่งจุดที่ย้อนกลับไม่ได้เป็นการตัดสินใจใหญ่เท่าใด ยิ่งต้องผ่านการพิจารณาในขั้นที่ย้อนกลับได้นานขึ้น
สองข้อนี้ทำให้การให้บริการเกม (Live Ops) กลายเป็นงานที่ต่างจากการออกแบบก่อนเปิดตัว ส่วนที่เหลือ (หน่วยเวลาจากไตรมาส→สัปดาห์ ฟีดแบ็กจากเบตา→เรียลไทม์) เป็นสิ่งที่แตกแขนงมาจากสองแกนนี้
บันทึกเซสชันจริง (worked transcript) ของ §15.1.2 เป็นหนึ่งฉากของการประยุกต์แบบก้าวหน้า AI ประกอบตัวเลือก ส่วนคนกำหนดการรับ แต่ไม่ใช่ว่าทุกทีมจะมาถึงจุดนี้ได้ตั้งแต่แรก มันมีขั้น
ใน การประยุกต์แบบอนุรักษ์นิยม คนเป็นผู้เสนอตัวเลือก ทีมปฏิบัติการวางแผนอีเวนต์เองในการประชุมวันจันทร์ เขียนซีซันรูลด้วยมือ และจัดประเภทฟีดแบ็กผู้ใช้แบบแมนนวล อัตโนมัติรับเฉพาะการวัด (แดชบอร์ด KPI) และการตรวจถดถอย (regression test) ของบิลด์เท่านั้น ตามการสังเกตในวงการ ปัจจุบันการให้บริการเกม MMORPG ไลฟ์ส่วนใหญ่ใกล้เคียงกับขั้นนี้
ใน การประยุกต์แบบก้าวหน้า AI ร่างต้นฉบับให้ทั้ง "การเสนอตัวเลือกอีเวนต์" และ "การจัดประเภทฟีดแบ็ก" §15.1.2 คือฉากของอย่างแรก ส่วนอย่างหลัง (การจัดกลุ่มฟีดแบ็กอัตโนมัติ) ดูได้ใน §15.3 การตัดสินใจของคนแคบลงเหลือเพียงการตัดสินใจระดับเมตา (meta) อย่าง "จะรับตัวเลือกใด" "จะรับฟีดแบ็กที่ AI จัดประเภทมาอย่างไร"
เพื่อให้การประยุกต์แบบก้าวหน้าตั้งหลักได้ ต้องพร้อมสามอย่าง คือ ไลบรารี ที่เทมเพลตอีเวนต์·ซีซันรูลถูกแยกและสะสมไว้เป็นหน่วยที่ประกอบใหม่ได้ (event_templates.yaml ของ §15.1.2 คือเมล็ดพันธุ์นั้น) เครื่องสร้างตัวเลือก ที่รับสัญญาณปัจจุบันเป็นอินพุตแล้วออกตัวเลือกในรูปร่างของร่างต้นฉบับ (พรอมต์ของ §15.1.2) และ การจัดกลุ่ม (clustering) ที่จัดประเภทฟีดแบ็กที่ไหลเข้ามาโดยอัตโนมัติ (§15.3) ข้อที่สามอย่างนี้เป็นโครงร่างเดียวกับ §5.3.12 (โลก BT (BehaviorTree, ทรีพฤติกรรม)·คลาวด์เควสต์)·§8.1.8 (การปรับสมดุลแบบก้าวหน้า) คือสารที่สอดคล้องตลอดเล่มนี้ — แม้สาขาจะต่างกัน แต่โครงสร้าง "สะสมชิ้นส่วนที่ผ่านการตรวจสอบไว้เป็นไลบรารี AI ออกตัวเลือกประกอบ แล้วคนเป็นผู้รับ" นั้นเหมือนกัน
ในที่นี้ขอทำให้ชัดเจนสักข้อหนึ่ง แนวคิดอย่างไลบรารี·เครื่องสร้างตัวเลือก·การจัดกลุ่ม ในเชิงทฤษฎีเป็นไปได้ตั้งแต่ทศวรรษ 2010 แล้ว สิ่งที่ติดขัดคือ AI ในตอนนั้นเขียน ภาษาธรรมชาติที่ผู้ใช้จะอ่าน อย่างประกาศอีเวนต์·คำอธิบายรูลไม่ได้ และสรุป·จัดประเภทฟีดแบ็กวันละหลายร้อยถึงหลายพันรายการด้วยภาษาธรรมชาติไม่ได้ หลังจาก LLM พัฒนาขึ้น (2023\~) กำแพงสองด้านนั้นต่ำลง ทำให้การปฏิบัติการแบบก้าวหน้าซึ่งเคยอยู่แค่บนกระดาษส่วนมากเข้าสู่อาณาเขตที่เป็นจริงได้
| รูปแบบ | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| วางแผนอีเวนต์จากกระดาษเปล่าทุกสัปดาห์ | ทีมปฏิบัติการหมดแรงในไม่ช้า คุณภาพตัวเลือกแกว่งตามสภาพร่างกาย | สะสมไว้เป็นไลบรารีเทมเพลตอีเวนต์ (§15.1.2) |
| มอบหมายทั้งดุ้นว่า "AI เอ๋ย ช่วยทำอีเวนต์ให้หน่อย" | หากไม่มีไลบรารี·รูล จะได้เพียงค่าเฉลี่ยของ RPG ทั่วไป | จำกัดวัตถุดิบ + บังคับตรวจสอบซีซันรูลด้วยตนเอง (§15.1.2) |
| ตรวจตัวเลือกด้วยตาเปล่าเท่านั้น | พลาดเงินเฟ้อ·การเอนเอียงของวัตถุประสงค์ทุกสัปดาห์ | ตรวจสอบอัตโนมัติด้วย event_lint.py (§15.1.3) |
| ทำ lint เป็นแบบปฏิเสธอัตโนมัติ | เครื่องฆ่าแม้กระทั่งการตัดสินใจของแคมเปญที่ตั้งใจ | ให้แค่ alert การรับให้ Director ตัดสิน (§15.1.3) |
| ตัดสินการตัดสินใจที่ย้อนกลับไม่ได้ทันทีในลูปรายสัปดาห์ | rollback หลังประกาศซีซันทิ้งร่องรอยไว้ในคอมมูนิตี้ | แยกการตัดสินใจใหญ่ออกไปยังการประชุมซีซัน (§15.1.4) |
| ไล่ตาม KPI เดียว (DAU·รายได้) เท่านั้น | ความล้าของผู้ใช้สะสม รับตัวเลือกที่สวนทางกับสัญญาณ | ใส่สัญญาณหลายแกนใน current_state (§15.1.2) |
ลองทำเพียงหนึ่งขั้นตามลำดับ setup → prompt → verify
event_templates.yaml ด้วยมือ (เฉพาะวัตถุประสงค์·ระยะเวลา·ระดับรางวัล) ซีซันรูลแค่สามบรรทัดบรรทัดละหนึ่งข้อก็พอ — เพดานเงินเฟ้อ ห้ามวัตถุประสงค์ขัดกัน ห้ามซ้ำซ้อนcurrent_state.yaml แล้วรันหนึ่งครั้งถ้าทำคนเดียว เท่านี้ก็พอ: ไม่ต้องมี yaml ไลบรารี ไม่ต้องมีโค้ด lint ลองนึกถึงอีเวนต์ไตรมาสที่แล้วของเกมที่ชอบสัก 5\~6 รายการ แล้วเขียนลงสามช่อง "วัตถุประสงค์·ระยะเวลา·รางวัล" เพียงเท่านั้นก็จะเห็นว่าเกมนั้นไม่ได้คิดจากกระดาษเปล่าทุกสัปดาห์ แต่นำรูปแบบมาหมุนใช้ซ้ำ ตารางนั้นแหละคือไลบรารีเทมเพลตแรกของคุณ
หากเป็นทีม ขอให้เริ่มด้วยหนึ่งขั้นถัดไปนี้ รวบรวมอีเวนต์ของ 1\~2 ไตรมาสที่ผ่านมา แล้วทำให้เป็นมาตรฐานในรูปแบบ event_templates.yaml (เฉพาะรูปแบบที่ผ่านการตรวจสอบแล้ว) และนำซีซันรูลสามบรรทัดใส่ลงในโค้ด event_lint.py ไว้ก่อน เมื่อมีไลบรารีและรูล ไม่ว่าจะเป็นตัวเลือกที่ AI ประกอบหรือร่างของคน ก็วัดได้ด้วยเส้นเดียวกัน
ผู้อ่านกลุ่มหลัก: นักออกแบบเกม (Game Designer) MMORPG ที่รับผิดชอบ Live Ops (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านคนเดียว/งานอดิเรก: §15.2.9 「ถ้าทำคนเดียวก็แค่นี้พอ」
ลองนึกถึงการประชุมวันจันทร์ของเกมไลฟ์ที่ดำเนินงานมา 4 ปี ทุกสัปดาห์เรื่องว่าจะจัดอีเวนต์อะไรในสัปดาห์หน้าต้องเริ่มจากกระดาษเปล่าเสมอ พอมีใครพูดว่า "เอาอีเวนต์เช็กอินครั้งก่อนมาเพิ่มรางวัลแล้วจัดใหม่ดีไหม" ก็จะมีอีกคนตอบว่า "อันนั้นเพิ่งจัดไปเมื่อสองเดือนก่อนนะ" แล้วจะเพิ่มรางวัลเท่าไรก็ตัดสินด้วยความรู้สึกอีก พอประชุมจบ นักออกแบบที่ดูแลการดำเนินงานคนหนึ่งก็ต้องใช้เวลาครึ่งวันกรอกแบบฟอร์มอีเวนต์ตั้งแต่ต้น ทุกสัปดาห์ จากกระดาษเปล่า ครึ่งวัน
ปัญหาไม่ได้อยู่ที่ขาดไอเดีย ทีมดำเนินงานมีโครงอีเวนต์ที่ผ่านการพิสูจน์แล้วอยู่ในหัวหลายแบบ — เช็กอิน ร่วมมือ แข่งขัน และดึงผู้เล่นกลับ เพียงแค่สลับธีมและรางวัลเข้าไปในโครงเหล่านั้น ก็ได้อีเวนต์ความยาวหนึ่งสัปดาห์ออกมา แต่เพราะ "การสลับ" นั้นทำด้วยมือและด้วยความรู้สึกทุกครั้ง มันจึงช้าและผลลัพธ์ก็ไม่นิ่ง
บทนี้ว่าด้วยวิธีส่งต่อ "การสลับ" นั้นให้ AI ทำ หัวใจมีสองข้อ ข้อแรก ป้อนโครงอีเวนต์ที่ผ่านการพิสูจน์แล้วในรูปของ เทมเพลต yaml ที่แปรผันได้ ข้อสอง มอบงานน่าเบื่ออย่างการดึงตัวเลือกหลายแบบสำหรับสัปดาห์หน้าจากเทมเพลตให้ AI ทำ ส่วนมนุษย์ เขียนช่วงรางวัลและการซ้ำซ้อนเป็นโค้ดแล้วตรวจแค่โทน ทฤษฎีทั่วไปของการออกแบบอีเวนต์ (ทำนองว่าเช็กอินดีต่อการดึงผู้เล่นใหม่ และร่วมมือดีต่อการกระตุ้นกิจกรรม) มีอยู่เพียงพอแล้วในหนังสือเล่มอื่น ดังนั้นบทนี้จึงเน้นเฉพาะ จุดที่นำความรู้นั้นมารันด้วยเวิร์กโฟลว์ AI เท่านั้น
บันทึกประสบการณ์การดำเนินงานของผู้เขียน (พูดตรง ๆ) ประสบการณ์ที่ผู้เขียนรับผิดชอบ Live Ops หลังเปิดตัวโดยตรงในรอบ 1–2 ปีนั้นจำกัดอยู่เพียงบางช่วงของอาชีพ เวิร์กโฟลว์ในบทนี้คือการย้ายเครื่องมือผลิตและตรวจสอบ (เนื้อหา/HUD) ที่ผู้เขียนใช้งานอยู่มาประยุกต์กับงานอีเวนต์ และตัวเลขผลลัพธ์เป็น การสังเกตวงการ + การประมาณของผู้เขียน ซึ่งจะระบุไว้ในเนื้อหาทุกครั้ง โครงสร้างเครื่องมือ (เทมเพลต yaml / lint / ด่านตรวจสอบ) นั้นมีโครงเดียวกับเครื่องมือผลิตเนื้อหาที่ผู้เขียนใช้งานจริง
ภาพรวมของการผลิตอีเวนต์มีสี่ขั้น หัวใจอยู่ที่ขั้น 1 (เทมเพลต) และขั้น 3 (lint) เป็นเชิงกำหนด (deterministic) มีเพียงขั้น 2 เท่านั้นที่เป็น AI เป็นการแบ่งงานแบบเดียวกับที่เห็นในการผลิตเนื้อหา (§6.2) และการบีบอัด HUD (§14.1) เมื่อ rulebook คอยยึดทั้งอินพุตและการตรวจสอบไว้ทั้งสองด้าน ต่อให้ AI ที่อยู่ตรงกลางสร้างความแปรผันที่ต่างกันเล็กน้อยทุกครั้ง สมดุลรางวัลและกำหนดการก็จะไม่สั่นคลอน
flowchart TB
A["อินพุต: เทมเพลตอีเวนต์ yaml
(โครงที่พิสูจน์แล้ว — เช็กอิน·ร่วมมือ·แข่งขัน·ดึงผู้เล่นกลับ)
+ ธีมไตรมาสนี้·รางวัลต้องห้าม·ช่องปฏิทิน"]
A --> B["ขั้น 2 AI: สร้างตัวเลือกแปรผัน
โครงเดิม × ธีม·รางวัล·ช่วงเวลาต่างกัน
→ ตัวเลือก 5~10 แบบ (ร่างแบบฟอร์ม)"]
B --> C{"ขั้น 3 เชิงกำหนด: event_lint.py
ช่วงรางวัล·เพดานเงินเฟ้อ·กำหนดการซ้อนทับ
·โครงเดียวกันซ้ำใน N สัปดาห์ก่อนหน้า"}
C -->|ฝ่าฝืน WARN| D["ด่านตรวจสอบฝ่ายดำเนินงาน
(รับ·ตัดออก·ปรับละเอียด)"]
C -->|ผ่าน| D
D -->|ขอใหม่| B
D -->|รับ| E["นำเข้าบิลด์ → ประกาศ
(ด่านย้อนกลับไม่ได้)"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class A data;
class B ai;
class C,E code;
class D human;
ในภาพนี้ จุดที่มือมนุษย์เข้าไปแตะมีเพียงสองแห่ง แห่งบนสุดคือจุดที่ป้อนเทมเพลตและข้อจำกัดของไตรมาสนี้เข้าไปให้สะอาด และแห่งล่างสุดคือจุดที่ตัดสินสิ่งที่ lint จับไม่ได้ ว่า "ธีมนี้เข้ากับบรรยากาศเกมของเราตอนนี้ไหม" ส่วนการผลิตตัวเลือกที่น่าเบื่อและการคำนวณรางวัลที่อยู่ระหว่างสองจุดนั้น เทมเพลต AI และ lint เป็นคนรัน
การออกแบบที่ชี้ขาดคือ ต่อให้ lint (ขั้น 3) พบการฝ่าฝืน มันจะไม่ทิ้งตัวเลือกโดยอัตโนมัติ แต่จะส่งขึ้นไปยังด่านฝ่ายดำเนินงาน (ขั้น 4) เป็นเพียง WARN เหตุผลจะเห็นใน §15.2.5 และข้อที่ลูกศรสุดท้าย (การประกาศ) เป็น ย้อนกลับไม่ได้ นั้น คือสิ่งที่แยก Live Ops ออกจากการผลิตงานแบบอื่น NPC ในเมืองถ้าไม่ถูกใจก็แค่ทิ้งก่อนบิลด์ก็จบ แต่อีเวนต์ที่ประกาศต่อผู้ใช้ไปแล้ว เมื่อจะถอนกลับ ต้องจ่ายด้วยต้นทุนความไว้วางใจของชุมชน (§15.2.7)
ตรึงโครงที่พิสูจน์แล้วซึ่งทีมดำเนินงานมีอยู่ให้เป็นแบบฟอร์ม ถ้าปล่อยเป็นเอกสารออกแบบรูปแบบอิสระ AI จะไม่รู้ว่าต้องแปรผันอะไร ต้องแบ่งช่อง (slot) ไว้ "เปลี่ยนแค่ช่องนี้" จึงจะเป็นจริงได้
# event_templates/coop_raid.yaml — โครงเรดแบบร่วมมือ (พิสูจน์แล้ว, ดำเนินงาน 4 ครั้ง)
template_id: coop_raid
purpose: [กระตุ้นผู้เล่นเดิม, ชุมชน] # แค่ 1~2 ข้อ ห้ามไล่ตาม 4 ข้อพร้อมกัน
core_loop: ทั้งเซิร์ฟเวอร์สะสมการมีส่วนร่วมในช่วงเวลา → ปลดล็อกรางวัลทั้งเซิร์ฟเวอร์เป็นขั้น ๆ
duration_range: [5, 10] # วัน เกิน 10 วันจะเกิดความเหนื่อยล้าสะสม
slots: # ← ช่องที่ AI แปรผัน โครงคงที่
theme: { type: อิสระ, ข้อจำกัด: ปฏิบัติตามธีมไตรมาส }
boss_or_target: { type: อิสระ, ข้อจำกัด: ใช้เอสเซ็ตบอสเดิมซ้ำก่อน }
reward_tiers: { type: รายการรางวัล, count: 3~5, ข้อจำกัด: อ้างอิง reward_policy }
reward_policy: # ← ช่องที่ lint อ่าน ห้ามแปรผัน
강화석_per_event_max: 30 # เพดานการแจกต่ออีเวนต์ 1 ครั้ง
골드_per_event_max: 50000
คอสตูมจำกัด: อนุญาต (เป็นเจ้าของถาวร ผลกระทบต่อเศรษฐกิจ 0)
แจกเงินสดโดยตรง: ห้าม
inflation_guard:
강화석_분기_누적상한: 90 # รวมทุกอีเวนต์ในไตรมาส
post_event_kpi: # ← ช่องวัดผลอัตโนมัติหลังอีเวนต์
- อัตราการเข้าร่วม (ผู้เข้าร่วมอย่างน้อย 1 ครั้งเทียบกับการได้เห็นอีเวนต์)
- การเปลี่ยนแปลงราคาหินเสริม (30 วันหลังอีเวนต์, เป้าหมาย ±10%)
- เวลาเล่นวันธรรมดาหลังอีเวนต์ (สัญญาณการพึ่งพา)
การแยกที่สำคัญที่สุดคือ slots (AI แปรผัน) กับ reward_policy (lint อ่าน และ AI แตะไม่ได้) ธีมและบอสจะต่างกันทุกครั้งก็ได้ แต่เพดานการแจกหินเสริมคือเส้นที่เศรษฐกิจของเกมกำหนดไว้ ถ้า AI ดึงเส้นนี้ออกมาเป็นตัวเลขที่ต่างกันทุกครั้งที่เรียก เงินเฟ้อก็เริ่มต้น ณ จุดนั้นทันที ดังนั้น รายการ รางวัลให้ AI เสนอ แต่ ปริมาณ รางวัลต้องขยับได้เฉพาะภายในช่วงนโยบายเท่านั้น โดยให้ lint เป็นคนเขียนกำกับ
ในโฟลเดอร์เดียวกันมี daily_attendance.yaml (เช็กอิน) pvp_ladder.yaml (แข่งขัน) และ comeback.yaml (ดึงผู้เล่นกลับ) อยู่ในรูปแบบเดียวกัน สี่ฉบับนี้คือพูลอินพุตทั้งหมดสำหรับการสร้างตัวเลือกในไตรมาสนี้
ผมจะแสดงให้เห็นจนจบหนึ่งรอบว่ารันจริงอย่างไร พรอมต์อินพุตคัดลอกไปใช้ได้ทันที ส่วนเอาต์พุตเป็นการเรียบเรียงใหม่จากเซสชันการผลิตจริง
จากไฟล์ coop_raid.yaml ที่แนบมา (โครงเรดแบบร่วมมือที่ผ่านการพิสูจน์ 4 ครั้ง) ขอตัวเลือกแปรผันสำหรับสัปดาห์หน้า (W2) มา 5 ตัวเลือกเท่านั้น
ธีมไตรมาสนี้คือ "ฤดูร้อน — น้ำ·เทศกาล·ความร้อน"
ห้ามแตะ core_loop เด็ดขาด เปลี่ยนแค่ slots (ธีม·บอส·รางวัล) เท่านั้น
รางวัลต้องอยู่ภายในเพดาน reward_policy เท่านั้น และแต่ละตัวเลือกให้แนบเหตุผลหนึ่งบรรทัดว่าทำไมถึงเลือกธีม·รางวัลนี้
3 สัปดาห์ก่อนหน้าคือ เช็กอิน·PvP ladder·เรดร่วมมือ ดังนั้นตัวเลือกที่วนกลับมาเป็นเรดร่วมมืออีกให้ติด [ระวังการซ้ำ]
ถ้าไม่มั่นใจให้ติด [คลุมเครือ] แล้วส่งกลับมาให้ผม ขอเป็นตารางอย่างเดียว คอลัมน์คือ ชื่อตัวเลือก·ธีม·สรุป tier รางวัล·เหตุผล·ธง
ในกฎหกบรรทัดนี้ ไม่มีบรรทัดใดที่ ระบุ ธีมหรือรางวัลเลย ทั้งหมดเป็นรั้วที่บอกว่า "เปลี่ยนได้แค่ไหน อย่าข้ามอะไร สับสนเมื่อไรให้แจ้ง" เพราะถ้าปล่อยกว้างอย่าง "ช่วยสร้างอีเวนต์ฤดูร้อนให้หน่อย" จะได้ค่าเฉลี่ยของเกม RPG มือถือทั่วไปออกมา ขอบเขตการแปรผัน (เฉพาะ slots) เพดานรางวัล (reward_policy) การระวังการซ้ำ (ประวัติ 3 สัปดาห์ก่อน) และ "ถ้าคลุมเครือให้ส่งคืนมนุษย์" — รั้วเหล่านี้ทำให้รักษาโครงไว้ได้ ขณะที่ปล่อยให้การแปรผันเป็นอิสระ
| ชื่อตัวเลือก | ธีม | สรุป tier รางวัล | เหตุผล | ธง |
|---------------|----------------|-------------------------------|----------------------------------|-------------|
| เขื่อนกั้นน้ำท่วม | กั้นน้ำท่วมแม่น้ำ | หินเสริม 25 / โกลด์ 4 หมื่น / คอสตูม | ร่วมมือ = อุปมาก่อเขื่อน เชื่อมตรงกับการสะสมส่วนร่วม | |
| ลอยโคมไฟ | เทศกาลน้ำฤดูร้อน | หินเสริม 20 / โกลด์ 3 หมื่น / ฉายา | โทนเทศกาล เน้นบรรยากาศมากกว่าแข่งขัน | |
| บอสคลื่นความร้อน | ปราบอวตารความร้อน | หินเสริม 30 / โกลด์ 5 หมื่น / คอสตูม | ใช้เอสเซ็ตบอสเดิมซ้ำ รางวัลเต็มเพดาน | [ระวังการซ้ำ] |
| แข่งกระแสน้ำ | แข่งขันโต้คลื่น | หินเสริม 28 / โกลด์ 4.5 หมื่น / เมาท์ | ผสมร่วมมือ + แข่งขัน | [คลุมเครือ] |
| สะสมปลาคาร์ป | ร่วมมือแบบสะสม | หินเสริม 22 / โกลด์ 3.5 หมื่น / ฉายา | ผู้เล่นไลต์เข้าได้ง่าย กำแพงต่ำ | |
[ระวังการซ้ำ] บอสคลื่นความร้อน: core_loop เหมือนเรดร่วมมือของ W-3 แทบทุกประการ
โครงเดียวกันซ้ำในระยะ 3 สัปดาห์ → กังวลความเหนื่อยล้าของผู้ใช้ แนะนำโครงอื่น (เช็กอิน/สะสม)
[คลุมเครือ] แข่งกระแสน้ำ: ผสมการสะสมแบบร่วมมือ + การแข่งขันส่วนบุคคลในอีเวนต์เดียว
ช่อง purpose ขยายเป็น [กระตุ้น+แข่งขัน] อาจขัดกับหลัก 1~2 วัตถุประสงค์
ส่วนที่มีค่าที่สุดในเอาต์พุตไม่ใช่ตาราง แต่เป็น สองบรรทัดล่างสุด จุดที่ AI แจ้งขีดจำกัดของตัวเองและส่งคืนให้มนุษย์ ว่า "บอสคลื่นความร้อนมีโครงเดียวกับ 3 สัปดาห์ก่อน" และ "แข่งกระแสน้ำมีวัตถุประสงค์เพิ่มเป็นสองข้อ" พรอมต์ที่ดีคือสิ่งที่ทำให้ AI พูดได้ว่า "เรื่องนี้ผมไม่มั่นใจ"
ตอนนี้เอาตัวเลือกชุดนี้ไปให้ lint เขียนกำกับ
ถ้าตรวจด้วยตาทุกครั้งว่าตัวเลือกรักษานโยบายรางวัลและการซ้อนทับของกำหนดการไว้ไหม ก็จะพลาดอีก สิ่งที่ตัดสินได้ด้วย reward_policy inflation_guard และปฏิทิน ให้โค้ดเป็นคนตรวจ มนุษย์เอาเวลาไปใช้กับการตัดสินโทนและความสนุกที่โค้ดจับไม่ได้เท่านั้น
# event_lint.py — ตรวจสอบตัวเลือกแปรผันของอีเวนต์ (โครง)
# อินพุต: รายการตัวเลือกที่ AI เสนอ + นโยบายเทมเพลต + ปฏิทินไตรมาส
# เอาต์พุต: รายการ WARN (ไม่ใช่การทิ้งอัตโนมัติ — ส่งขึ้นไปยังด่านฝ่ายดำเนินงาน)
def lint(candidates, policy, quarter_ledger, recent_weeks):
warns = []
stone_used = sum(quarter_ledger.강화석) # ยอดสะสมที่แจกไปแล้วในไตรมาสนี้
for c in candidates:
# A: เพดานรางวัลต่ออีเวนต์ 1 ครั้ง (นโยบาย)
if c.강화석 > policy["강화석_per_event_max"]:
warns.append(f"[A] {c.name}: หินเสริม {c.강화석} > เพดาน "
f"{policy['강화석_per_event_max']} (เกินต่ออีเวนต์)")
# B: เพดานสะสมเงินเฟ้อของไตรมาส
if stone_used + c.강화석 > policy["강화석_분기_누적상한"]:
warns.append(f"[B] {c.name}: สะสมไตรมาส {stone_used + c.강화석} > "
f"{policy['강화석_분기_누적상한']} (เพดานเงินเฟ้อ)")
# C: โครงเดียวกันซ้ำใน N สัปดาห์ก่อนหน้า
if c.template_id in recent_weeks[-2:]:
warns.append(f"[C] {c.name}: โครง {c.template_id} มีอยู่ใน 2 สัปดาห์ก่อนหน้า (ซ้ำ)")
# D: ช่องปฏิทินชนกัน (อีเวนต์ใหญ่อื่นในสัปดาห์เดียวกัน)
if quarter_ledger.slot_taken(c.week):
warns.append(f"[D] {c.name}: ช่อง W{c.week} มีอีเวนต์ใหญ่จัดวางไว้แล้ว")
return warns
เมื่อนำห้าตัวเลือกจากบันทึกเซสชันจริงข้างต้นใส่ในโค้ดนี้ จะได้ผลออกมาดังนี้
[PASS] เขื่อนกั้นน้ำท่วม: หินเสริม 25 ≤ 30, สะสมไตรมาส 65+25=90 ≤ 90 (แตะเส้นพอดี)
[WARN] [C] บอสคลื่นความร้อน: โครง coop_raid มีอยู่ใน 2 สัปดาห์ก่อนหน้า (W-3) (ซ้ำ)
[WARN] [B] แข่งกระแสน้ำ: สะสมไตรมาส 65+28=93 > 90 (เกินเพดานเงินเฟ้อ)
[PASS] ลอยโคมไฟ: หินเสริม 20 ≤ 30, สะสมไตรมาส 65+20=85 ≤ 90
[PASS] สะสมปลาคาร์ป: หินเสริม 22 ≤ 30, สะสมไตรมาส 65+22=87 ≤ 90
ที่น่าสนใจตรงนี้คือ แข่งกระแสน้ำ AI ติด [คลุมเครือ] เพราะวัตถุประสงค์ขัดกัน แต่ lint จับด้วยเหตุผลที่ต่างกันโดยสิ้นเชิง — เกินเพดานสะสมเงินเฟ้อของไตรมาส เมื่อบวกหินเสริม 28 เข้าไป ยอดสะสมไตรมาสจะเป็น 93 ซึ่งเกิน 90 ตามนโยบาย โค้ดจับการคำนวณที่ AI มองไม่เห็น ในทางกลับกัน บอสคลื่นความร้อน นั้น [ระวังการซ้ำ] ของ AI และ [C] ของ lint ชี้ไปที่เรื่องเดียวกัน มนุษย์ AI และโค้ดทั้งสามต่างกรองด้วยตาข่ายคนละแบบ
ด้วย 30 บรรทัดนี้ "รางวัลครั้งนี้แรงไปหรือเปล่า" จึงไม่จบลงด้วยการชนความรู้สึกกับความรู้สึกอีกต่อไป เมื่อโค้ดพิมพ์ออกมาว่า [B] สะสมไตรมาส 93 > 90 ก็ไม่มีอะไรต้องถกเถียง แค่ลดรางวัลลงหรือเปลี่ยนตัวเลือกก็พอ
ถ้าเขียนเพียงเชิงนามธรรมว่า "ทีมดำเนินงานตรวจสอบ" ก็จะไม่รู้ว่าด่านนี้กรองอะไรจริง ๆ ลองตามไปจนจบสักครั้งว่าหลัง lint ผ่านแล้ว มนุษย์ฆ่าอะไรและไว้ชีวิตอะไร
[ขั้น 4 การตรวจสอบฝ่ายดำเนินงาน — คำตัดสิน]
นักออกแบบฝ่ายดำเนินงานจัดการตัวเลือก 5 ตัวดังนี้
- บอสคลื่นความร้อน → ตัดออก lint [C] และ AI [ระวังการซ้ำ] ชี้ไปด้วยกัน ถ้าวนโครงเรดร่วมมือเดียวกันอีกในรอบ 3 สัปดาห์ ความเหนื่อยล้าแบบ "สะสมส่วนร่วมอีกแล้วเหรอ" จะมา จดบันทึกยกยอดไปเป็นช่องของไตรมาสหน้า
- แข่งกระแสน้ำ → ตัดออก lint [B] เกินเพดานเงินเฟ้อ ถ้าลดรางวัลลงเหลือ 25 ก็ผ่าน แต่ความขัดแย้งของวัตถุประสงค์ (กระตุ้น+แข่งขัน) ที่ AI [คลุมเครือ] ชี้ไว้เป็นปัญหาที่รากลึกกว่า การเอาแรงกิ้งส่วนบุคคลไปผสมในอีเวนต์ร่วมมือ ทำให้ผู้เล่นไลต์รู้สึกว่า "สุดท้ายก็เป็นงานเลี้ยงของพวกตัวท็อป" จึงไม่ตัดแค่รางวัลเพื่อไว้ชีวิต แต่พักไว้ทั้งตัวเลือก
- เขื่อนกั้นน้ำท่วม → ตัวเลือกอันดับ 1 ที่จะรับ แต่ถึง lint จะให้
สะสมไตรมาส 90 แตะเส้นพอดีเป็น PASS การที่มันเป็น เส้นพอดี ก็ยังติดใจ ถ้าใช้อีเวนต์นี้ ระยะหินเสริมที่เหลือในไตรมาสนี้จะกลายเป็น 0 จะไม่มีกำลังรางวัลเหลือไว้สำหรับ push ปิดซีซันในสัปดาห์สุดท้ายของเดือนมิถุนายน- ลอยโคมไฟ / สะสมปลาคาร์ป → ไว้ชีวิต ทั้งคู่รางวัลเบา (20·22) และเหลือระยะของไตรมาสไว้
หัวใจของด่านนี้คือการที่มนุษย์เขย่า เขื่อนกั้นน้ำท่วม ที่ผ่าน lint แล้วออกจากอันดับ 1 โค้ดให้ 90 ≤ 90 เป็น PASS ตามนโยบายแล้วไม่ใช่การฝ่าฝืน แต่นักออกแบบฝ่ายดำเนินงานมอง จังหวะรางวัลของทั้งไตรมาส lint มองความถูกกฎของอีเวนต์เดียว แต่มนุษย์มองไปถึงการปิดซีซันที่ปลายไตรมาส จึงวนขอใหม่
สร้างตัวเลือกของเขื่อนกั้นน้ำท่วมใหม่ โดยลดรางวัลหินเสริมจาก 25 → 18
เหตุผล: ต้องเหลือระยะหินเสริม 12 ไว้สำหรับ push ปิดซีซันในสัปดาห์สุดท้ายของเดือนมิถุนายน
เนื่องจากเสน่ห์ของรางวัลลดลง ให้เสริมคุณค่าที่สัมผัสได้ด้วยคอสตูมจำกัด·ฉายาแทนหินเสริม
จัด reward_tiers ใหม่ในทิศทางนั้น
AI ลดหินเสริมลงเหลือ 18 และเพิ่มคอสตูมจำกัดเป็น 2 ชนิด (รางวัลเป็นเจ้าของถาวรที่มีผลกระทบต่อเศรษฐกิจ 0) แล้วเสนอตัวเลือกใหม่ พอรัน lint อีกครั้งได้ สะสมไตรมาส 65+18=83 ≤ 90 เหลือระยะ 7 ไว้สำหรับปิดซีซัน หนึ่งรอบของ อินพุต → ผลิตตัวเลือก → lint → ตรวจสอบ → ตัดออก → ขอใหม่ ปิดลงตรงนี้
หนึ่งรอบนี้คือเกณฑ์ Show ของทั้งหนังสือเล่มนี้ ถ้าไม่ดูสักครั้งให้จบว่าเครื่องมือพ่นอะไรออกมา อะไรถูกจับ และมนุษย์ฆ่าอะไร ประโยคที่ว่า "ผลิตอีเวนต์ด้วย AI" ก็กลวงเปล่า
เหตุผลที่ไม่ติด lint แบบทิ้งอัตโนมัติก็อยู่ในรอบนี้ ถ้า lint ทิ้งการฝ่าฝืน [B] โดยอัตโนมัติ ทีมดำเนินงานก็จะเสียโอกาสเรียนรู้ปัญหาจริงของ แข่งกระแสน้ำ (วัตถุประสงค์ขัดกัน) และจุดที่จะเขย่าตัวเลือกอย่าง เขื่อนกั้นน้ำท่วม ที่ ถูกกฎแต่เสี่ยงต่อจังหวะของไตรมาส ก็จะหายไปด้วย ให้เครื่องเป็นคนคัดตัวเลือกที่น่าสงสัย แต่การรับและการตัดออกให้มนุษย์เป็นคนตัดสิน
ถ้าอีเวนต์เป็นจังหวะสัปดาห์–เดือน ซีซันก็เป็นจังหวะไตรมาส วิธีดำเนินงานเหมือนกัน ถ้าแยกองค์ประกอบที่พิสูจน์แล้วของซีซันออกเป็นช่อง ก็แค่สลับธีมในแต่ละไตรมาส
| ช่องซีซัน | แปรผัน (AI·มนุษย์) | คงที่ (นโยบาย·lint) |
|---|---|---|
| ธีมซีซัน | ฤดูร้อน·ฤดูหนาว·ปีใหม่ (อิสระ) | — |
| แทร็กรางวัลซีซันพาส | รายการรางวัลตามขั้น | จำนวนขั้น·ความยากในการจบ·เพดานรางวัล |
| แรงกิ้ง PvP ซีซัน | รายการรางวัลแรงกิ้ง | เพดานเงินเฟ้อของรางวัล |
| การสับเมตา | ตัวละครใหม่·การปรับสมดุล | ราวกั้นขอบเขตการเปลี่ยนแปลง (§8.1) |
ในซีซันพาส ตัวเลขหลักที่มนุษย์ตรึงไว้เป็นนโยบายคือ เป้าหมายอัตราการจบ การจัดความยากให้ผู้ใช้ที่ active ราว 70% ไปถึงขั้นสุดท้าย เป็นเกณฑ์ที่วงการมักอ้างถึงกันบ่อย (การประมาณของผู้เขียน — เพราะต่างกันไปในแต่ละเกม จึงควรอ่านเป็น ทิศทาง ไม่ใช่ค่าสัมบูรณ์: ต่ำกว่า 30% คือผิดหวัง เกิน 90% คือไร้ความท้าทาย) เมื่อป้อนเป้าหมายนี้เข้าไปในช่องแล้ว ก็บังคับให้ตอน AI เสนอตัวเลือกซีซันพาสต้องคำนวณ "อัตราการจบที่คาดการณ์" ออกมาด้วยได้
ปฏิทินไตรมาสต้องเห็นในภาพเดียวจบ อีเวนต์และซีซันจึงจะไม่ชนกัน มันใกล้เคียงกับปฏิทินตั้งโต๊ะส่วนกลางของทีมดำเนินงาน ทุกคนต้องเห็นภาพเดียวกัน การชนกันจึงจะลดลง
ภาพหนึ่งใบนี้อธิบายคำตัดสินของ §15.2.5 ด้วยภาพ สีคือชนิดของโครง ถ้าสีเดียวกันโผล่สองครั้งในรอบ 2\~3 สัปดาห์ lint [C] ใน §15.2.4 จะร้อง และเกจเงินเฟ้อด้านล่างใกล้แตะเส้นแดง (เพดาน 90) จนเหลือระยะ 7 ไว้ใช้ปิดซีซันในเดือนมิถุนายน (W6) แค่พอดี — นั่นคือ 7 ที่ได้มาจากการลดรางวัล เขื่อนกั้นน้ำท่วม ลงเหลือ 18
มีจุดหนึ่งที่ Live Ops ต่างจาก NPC ในเมือง (§6.2) หรือ HUD (§14.1) อย่างชี้ขาด การประกาศย้อนกลับไม่ได้ ถ้า NPC โทนไม่เข้าก็แค่ทิ้งก่อนบิลด์ก็จบ และผู้ใช้ก็ไม่รู้ด้วยซ้ำว่ามี NPC นั้นอยู่ แต่อีเวนต์ที่ประกาศต่อผู้ใช้ไปแล้ว รางวัล·ช่วงเวลา·กติกาจะค้างอยู่ในชุมชน หลังเริ่มไปแล้ว การพูดว่า "รางวัลอีเวนต์แรงไป จะขอเก็บคืน" มาพร้อมต้นทุนที่ย้อนกลับไม่ได้
flowchart LR
A["ตัวเลือกแปรผันจากเทมเพลต"] -->|ย้อนกลับได้| B["ตรวจสอบ lint"]
B -->|ย้อนกลับได้| C["ตรวจสอบฝ่ายดำเนินงาน·ตัดออก·ขอใหม่"]
C -->|ย้อนกลับได้| D["ตรวจสอบบิลด์
(เลื่อนรีลีสได้)"]
D ==>|ด่านย้อนกลับไม่ได้| E["ประกาศ·เริ่มอีเวนต์"]
E -.->|ต้นทุนการกู้คืนสูง| F["ทำได้แค่ปรับละเอียดในช่วงท้าย
(เก็บรางวัลคืน·เปลี่ยนช่วงเวลา = ต้นทุนความไว้วางใจ)"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class A data;
class B code;
class C,D human;
class F fail;
หลักการของทั้งหนังสือเล่มนี้ (สารเดียวกับ §5.4.5 การอัดเสียงพากย์, §8.1 ไลฟ์บิลด์, การเรนเดอร์ขั้นสุดท้ายในส่วนที่ 12) เป็นเช่นเดียวกันใน Live Ops การตรวจสอบทั้งหมด — ช่วงรางวัล, เพดานเงินเฟ้อ, การชนของกำหนดการ, โทน — ต้องเสร็จในขั้นที่ย้อนกลับได้ก่อนประกาศ นั่นคือเหตุผลที่รอบผลิต·lint·ตรวจสอบ·ขอใหม่ทั้งหมดใน §15.2.3\~5 หมุนอยู่ ทางซ้าย ของด่านย้อนกลับไม่ได้นี้ สิ่งที่ทำได้หลังข้ามด่านมีแค่การปรับละเอียดช่วงท้ายแบบ §15.2.8 และแม้แต่อย่างนั้นก็ค่อย ๆ กัดกินความไว้วางใจของผู้ใช้
หลังประกาศก็ยังดู KPI อยู่ แต่ต่างจากการตรวจสอบก่อนประกาศ ตรงนี้ทำได้แค่ปรับละเอียดช่วงท้ายเท่านั้น แยกสัญญาณที่วัดอัตโนมัติกับการสั่งยาของมนุษย์ออกจากกัน
| สัญญาณ (วัดอัตโนมัติ) | การสั่งยา (มนุษย์ตัดสิน) |
|---|---|
| อัตราการเข้าร่วมต่ำกว่า 50% | เสริมรางวัลช่วงท้ายเล็กน้อย หรือต่อเวลา +2 วัน (ภายในขอบเขตความไว้วางใจที่ประกาศไว้) |
| อัตราการเข้าร่วม 95% ขึ้นไป | ง่ายไป — จดความยากของรอบถัดไป แต่อีเวนต์ปัจจุบันคงไว้ |
| ราคาหินเสริม 30 วันหลังอีเวนต์ ลดเกิน -10% | เสริม sink (ร้านค้าจำกัด), ปรับเพดานเงินเฟ้อไตรมาสหน้าลง |
| เวลาเล่นวันธรรมดาหลังอีเวนต์ลดลง | สัญญาณการพึ่งพาอีเวนต์ — เสริมเสน่ห์เนื้อหาวันธรรมดา, ปรับความถี่อีเวนต์ |
บรรทัดสุดท้าย (เวลาเล่นวันธรรมดาลดลง) เป็นสัญญาณที่พลาดบ่อยที่สุด ถ้าดูแค่ DAU (Daily Active Users, ผู้ใช้ที่ใช้งานรายวัน) ในช่วงอีเวนต์ อีเวนต์ก็ดูเหมือนสำเร็จเสมอ แต่ถ้าหลังอีเวนต์จบแล้วผู้ใช้ไม่กลับมาในวันธรรมดา นั่นแปลว่าอีเวนต์กำลังดูดเสน่ห์ของเกมในยามปกติออกไป ดังนั้นจึงป้อน "เวลาเล่นวันธรรมดาหลังอีเวนต์" เข้าไปเป็นช่องใน post_event_kpi ของเทมเพลต §15.2.2 ตั้งแต่ต้น ถ้าไม่วัดก็สั่งยาไม่ได้
บทอีเวนต์มีแรงยั่วยวนสูงที่จะใส่ตารางแบบ "พอจัดอีเวนต์ร่วมมือ อัตราการคงอยู่ (Retention) ขึ้นจาก 30% เป็น 50%" ตัวเลขแบบนั้นถ้าไม่ผ่านการตรวจสอบจะทอนความไว้วางใจของหนังสือลง สิ่งที่บทนี้พูดได้มีเพียงสามอย่าง
ข้อแรก ทิศทางพูดได้ด้วยการสังเกตวงการ อีเวนต์เสริมรางวัลเช็กอินดึงจำนวนผู้ใช้ที่ active ระยะสั้นให้สูงขึ้น อีเวนต์ร่วมมือเพิ่มความผูกพันของชุมชน และแพ็กเกจจำกัดดึงยอดขายในช่วงอีเวนต์ขึ้น — นี่คือความเชื่อทั่วไปของวงการที่สังเกตเกมไลฟ์มา เพียงแต่ เท่าไร นั้นแกว่งมากตามองค์ประกอบของเกมและผู้ใช้ การยกตัวเลขของบริษัทอื่นมาตรง ๆ จึงอันตราย
ข้อสอง การประมาณของผู้เขียนก็เขียนว่าเป็นการประมาณ "เป้าหมายอัตราการจบซีซันพาส 70%", "เกิน 10 วันแล้วเกิดความเหนื่อยล้าสะสม", "การผลิตอีเวนต์ครึ่งวัน→หนึ่งชั่วโมง" ล้วนเป็นการประมาณบนพื้นฐานประสบการณ์ของผู้เขียนและเป็นสมมติฐานที่ยังไม่ได้ตรวจสอบ อย่าท่องจำค่าสัมบูรณ์ แต่ให้อ่านเป็น โครงสร้าง (เทมเพลต+lint แทนที่การออกแบบจากกระดาษเปล่า)
ข้อสาม สัญญาเป็น KPI เฉพาะสิ่งที่วัดได้ ตัวชี้วัดผลลัพธ์อย่างอัตราการคงอยู่ไม่ได้ถูกกำหนดด้วยอีเวนต์เดียว จึงไม่ฟันธงความเป็นเหตุเป็นผล แทนที่จะเป็นเช่นนั้น สิ่งที่เวิร์กโฟลว์นี้ทำให้วัดได้จริงคือสิ่งเหล่านี้ — จำนวน WARN ของ lint (จนกว่าการฝ่าฝืนรางวัลจะเป็น 0), การสะสมเงินเฟ้อไตรมาส (เทียบกับเพดาน), ช่วงระยะการซ้ำของโครงเดียวกัน (สัปดาห์), อัตราการเข้าร่วมและการเปลี่ยนแปลงราคาหินเสริมหลังจบของแต่ละอีเวนต์ สี่อย่างนี้ในที่ประชุมพูดได้ด้วยตัวเลข ไม่ใช่ "ความรู้สึก"
| รูปแบบ | ทำไมจึงล้มเหลว | การสั่งยา |
|---|---|---|
| ออกแบบอีเวนต์จากกระดาษเปล่าทุกสัปดาห์ | ช้าและผลลัพธ์ไม่นิ่ง | ป้อนโครงที่พิสูจน์แล้วเป็นเทมเพลต yaml (§15.2.2) |
| มอบทั้งก้อนว่า "AI ช่วยสร้างอีเวนต์ฤดูร้อนให้หน่อย" | ได้อีเวนต์ค่าเฉลี่ยของ RPG ทั่วไป | ตรึงโครง + แปรผันแค่ slots (§15.2.3) |
| ให้ AI เสนอปริมาณรางวัลอย่างอิสระ | เงินเฟ้อเริ่มต้น ณ จุดนั้น | ให้ lint บังคับ reward_policy (§15.2.4) |
| ตรวจสอบตัวเลือกด้วยตาอย่างเดียว | พลาดการสะสมไตรมาส·ช่วงระยะการซ้ำทุกครั้ง | ตรวจสอบอัตโนมัติด้วย event_lint.py (§15.2.4) |
| ผ่าน lint = ตรงไปรับเลย | มองไม่เห็นจังหวะไตรมาส·วัตถุประสงค์ขัดกัน | ด่านมนุษย์มองทั้งไตรมาส (§15.2.5) |
| พยายามเก็บรางวัลคืนหลังประกาศ | ต้นทุนความไว้วางใจที่ย้อนกลับไม่ได้ | ตรวจสอบทั้งหมดก่อนประกาศ (§15.2.7) |
| วัดแค่ DAU ในช่วงอีเวนต์ | มองไม่เห็นการกัดกินเสน่ห์วันธรรมดา | ช่องเวลาเล่นวันธรรมดาหลังอีเวนต์ (§15.2.8) |
ข้อที่ห้าพลาดบ่อยที่สุด ถ้าผ่าน lint แล้วส่งไปประกาศทันที จุดที่จะเขย่าตัวเลือกอย่าง เขื่อนกั้นน้ำท่วม ที่ ถูกกฎแต่ทำให้กำลังรางวัลปลายไตรมาสเป็น 0 ก็จะหายไป โค้ดมองความถูกกฎของอีเวนต์เดียว ส่วนมนุษย์มองจังหวะของทั้งไตรมาส
ถ้าทำคนเดียวก็แค่นี้พอ: ไม่ต้องมีโค้ด lint ก็ได้ เลือกโครงอีเวนต์หนึ่งแบบที่เห็นบ่อยในเกมของคุณ (หรือเกมไลฟ์ที่คุณชอบ) แล้วลองเขียนเทมเพลต yaml รูปแบบ §15.2.2 ด้วยมือ (สามช่อง
core_loop·slots·reward_policyคือหัวใจ) จากนั้นแปะพรอมต์ §15.2.3 เพื่อดึงตัวเลือกแปรผัน 5 แบบออกมา แล้วเลือกหนึ่งแบบที่รู้สึกว่า "รางวัลแรงไป" มาโต้กลับว่า "อันนี้เกินกำลังรางวัลของเดือนนี้ ลดลงแล้วทำใหม่" คุณจะรู้สึกได้ด้วยตัวเองว่าการรับและการตัดออกเป็นชุดของการตัดสินแบบไหน
ถ้าทำเป็นทีม ให้เริ่มด้วยขั้นถัดไปนี้ ป้อนโครงอีเวนต์ที่จัดบ่อย 3\~4 แบบเป็นเทมเพลต yaml แล้วเริ่มจากเขียนสามบรรทัดของ event_lint.py (เพดานรางวัล·การสะสมเงินเฟ้อไตรมาส·ช่วงระยะการซ้ำ) เป็นโค้ดก่อน แค่มีเทมเพลตและสามบรรทัดนี้ ก็ป้องกันความล้มเหลวที่พบบ่อยสองอย่าง — "การออกแบบจากกระดาษเปล่าทุกสัปดาห์" และ "การตัดสินรางวัลด้วยความรู้สึก" — ได้ก่อน เวิร์กโฟลว์นี้คือการนำองค์ประกอบ 3 อย่างของโครงการประยุกต์แบบก้าวหน้าใน §15.1.5 — เทมเพลตอีเวนต์·ไลบรารีกติกาซีซัน, ตัวสร้างตัวเลือกอีเวนต์ด้วย AI, การวัดผลอัตโนมัติหลังอีเวนต์ — มาทำให้เป็นจริงในทางปฏิบัติเป็นครั้งแรก
ผู้อ่านกลุ่มแรก: นักออกแบบเกม (Game Designer) และ Design Director ที่รับผิดชอบการดูแลผู้ใช้ในงาน Live Ops (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับคนเดียว/ผู้เล่นงานอดิเรก: §15.3.7 「ถ้าทำคนเดียว แค่นี้ก็พอ」
ขอบอกตามตรงไว้ก่อน ผู้เขียนไม่ได้มีประสบการณ์ยาวนานในการรับผิดชอบ Live Ops โดยตรงต่อเนื่องเป็นปีถึงสองปีหลังเกมเปิดให้บริการ เนื้อหาส่วนใหญ่ของบทนี้เป็น การสังเกตในวงการและประสบการณ์ในงานใกล้เคียง ที่สั่งสมอยู่บนประสบการณ์ 24 ปี ดังนั้นบทนี้จึงไม่ฟันธงว่า "ต้องทำ Live Ops แบบนี้" แต่จะนำวงจร อินพุต → AI → การตรวจสอบ → การตัดสินใจของคน ที่พิสูจน์มาแล้วจากการผลิตเนื้อหาก่อนเปิดตัว มาเสียบเข้ากับอินพุตที่ชื่อว่า ฟีดแบ็กจากผู้ใช้ แล้วลองหมุนให้สุดสักรอบหนึ่งเพื่อดูว่าผลลัพธ์เป็นอย่างไร โครงของเครื่องมือเหมือนกับ §6.2 city_hunting_generator ทุกอย่าง เปลี่ยนแค่อินพุตจาก "เมตาดาตาของเมือง" เป็น "ฟีดแบ็กจากผู้ใช้ 100 รายการ" เท่านั้น
ภาพของสัปดาห์แรกในการให้บริการมักจะคล้าย ๆ กัน ฟอรัม Discord ทิกเก็ต CS รีวิวในสโตร์ พอกพูนขึ้นวันละหลายร้อยถึงหลายพันรายการ คนอ่านให้ครบเป็นไปไม่ได้ และถ้าไม่อ่าน รายงานบั๊กตัวเดียวกันก็จะถูกฝังกลบไปทีละ 50 รายการ บทนี้ว่าด้วยวิธีให้ LLM จับกองนั้นมัดเป็นหัวข้อและให้คะแนนตามอารมณ์ ก่อน แล้วคนจึงเข้ามาทำเฉพาะส่วน การตัดสินใจจัดลำดับความสำคัญ ที่ว่า "แล้วสัปดาห์นี้จะแก้อะไรกันแน่"
ตารางที่แบ่งฟีดแบ็กออกเป็น 4 ช่องทาง (แบบสำรวจในเกม·ฟอรัม/Discord·รีวิวในสโตร์·ทิกเก็ต CS) และจัดเป็น 4 ประเภท (บั๊ก·คำขอ·ข้อร้องเรียน·คำชม) มีอยู่ในตำราการให้บริการเล่มไหนก็ได้ พูดถูกหมดทุกข้อ ปัญหาคือต่อให้ท่องตารางนั้นได้ ก็ยังไม่มีคำตอบว่า "วันนี้มีเข้ามา 412 รายการ จะจัดการอย่างไร" ตราบใดที่ยังมองฟีดแบ็กเป็น สิ่งที่คนต้องอ่านและจัดประเภท ปริมาณฟีดแบ็กก็จะชนะจำนวนคนในทีมงานบริการเสมอ
ลองเปลี่ยนมุมมอง ฟีดแบ็กหนึ่งรายการคือ อินพุตที่มีโครงสร้าง เป็นเรกคอร์ดที่มีห้าช่อง {출처, 원문, 토픽, 감정, 심각도} เมื่อมองแบบนี้ เนื้อแท้ของงานก็เปลี่ยนไป ไม่ใช่ "อ่านให้ครบ" แต่เป็น "มัดเป็นหัวข้อแล้วจัดลำดับความสำคัญ" และการคลัสเตอร์หัวข้อกับการให้คะแนนอารมณ์นั้น ถ้าคนทำจะน่าเบื่อและเกณฑ์จะแกว่งทุกครั้งที่ทำ แต่เครื่องจะใช้ไม้บรรทัดเดียวกันวัด 100 รายการเท่ากันหมด นี่คืองานประเภทที่ LLM ทำได้ดีกว่าคน การแบ่งงาน (rulebook = เชิงกำหนด, เนื้อหาหลัก = AI, การตรวจสอบ = คน) ที่ใช้ผลิตเมือง 30 แห่งใน §6.2 ก็ใช้ได้เหมือนกันตรงนี้ จุดต่างมีเพียงข้อเดียว คือสิ่งที่คนทำเป็นขั้นสุดท้ายไม่ใช่ "การตรวจสอบเนื้อหาหลัก" แต่เป็น "การตัดสินใจจัดลำดับความสำคัญ"
ขอชี้ถึงการกระจายตัวของประเภทฟีดแบ็กไว้สักจุดหนึ่ง ผู้ใช้ที่ลงมือเขียนข้อความเองมักจะเอนไปทางผู้ใช้ที่มีข้อไม่พอใจ ไม่ใช่ผู้ใช้ที่พึงพอใจ ลูกค้าที่พอใจจะจากไปเงียบ ๆ ส่วนลูกค้าที่ไม่พอใจจะกลับมาที่เคาน์เตอร์ ด้วยเหตุนี้การกระจายอารมณ์ในฟอรัมและรีวิวจึงมีแนวโน้มเอียงไปทางลบมากกว่าความพึงพอใจของผู้ใช้ทั้งหมดจริง ๆ (การสังเกตของผู้เขียน — ขนาดของความเอียงที่แท้จริงต่างกันไปในแต่ละเกม·ช่องทาง·ช่วงเวลา จึงควรอ่านเป็น ทิศทาง ไม่ใช่ค่าตัวเลขสัมบูรณ์) ต้องใส่ความเอียงนี้ไว้ในหัว เพื่อว่าเวลาเห็น "ลบ 60%" ในผลคลัสเตอร์ จะได้ไม่ตีความผิดว่าเกมกำลังจะเจ๊ง
ลองหมุนหนึ่งวงจรให้สุดจริง ๆ อินพุตคือฟีดแบ็ก 100 รายการที่รวบรวมจาก 4 ช่องทางในหนึ่งสัปดาห์ ส่วนเอาต์พุตคือคลัสเตอร์หัวข้อ·อารมณ์·ลำดับความสำคัญ พรอมต์อินพุตคัดลอกไปใช้ได้เลย และเอาต์พุตด้านล่างคือสิ่งที่เรียบเรียงใหม่จากรูปแบบของเซสชันการจัดประเภทจริง
นำข้อความดิบที่ขูดมาจากช่องทางต่าง ๆ มาทำให้เป็นมาตรฐาน หนึ่งบรรทัดต่อหนึ่งเรกคอร์ด อันนี้ไม่ใช่การเขียนขึ้นใหม่ แค่ดึงและจัดระเบียบก็พอ
{"id": "fb_0001", "src": "discord", "text": "ตีบวก +12 พังไป 50 ครั้งแล้ว อัตรานี้มันถูกต้องไหม ขอเงินคืน"}
{"id": "fb_0002", "src": "store_review","text": "กราฟิกสวยนะ แต่แล็กหนักมากจนเด้งทุกครั้งที่สงครามกิลด์"}
{"id": "fb_0003", "src": "cs_ticket", "text": "จ่ายเงินไปแล้วแต่เพชรไม่เข้า แนบเลขที่คำสั่งซื้อมาด้วย"}
{"id": "fb_0004", "src": "forum", "text": "อาชีพใหม่นักธนูจะออกเมื่อไหร่ ㅠㅠ ตอนลงทะเบียนล่วงหน้าก็สัญญาไว้แล้วนี่"}
{"id": "fb_0005", "src": "discord", "text": "เปิดสัปดาห์แรกแต่ทีมงานสื่อสารดีนะ ประกาศไว ๆ ฝากด้วยนะต่อไป"}
{"id": "fb_0006", "src": "store_review","text": "ดาเมจของบอสบางตัว (เฮ็กรัง) มันไม่สมเหตุสมผล ใส่ของเต็มยังโดนทีเดียว ขอแพตช์ปรับสมดุล"}
{"id": "fb_0007", "src": "cs_ticket", "text": "ทูทอเรียลขั้นที่ 5 ไปต่อไม่ได้ ปุ่มกดไม่ติด (เครื่อง: Galaxy ซีรีส์ A)"}
// ... fb_0008 ~ fb_0100 (생략)
ในขั้นอินพุต เรกคอร์ดจะเว้น 토픽·감정·심각도 ไว้ว่าง การเติมช่องว่างเหล่านั้นคืองานของ LLM ในสองขั้นถัดไป
ช่วยมัดไฟล์ feedback_100.jsonl ที่แนบมา (ฟีดแบ็ก 100 รายการในหนึ่งสัปดาห์) ให้เป็นหัวข้อ และให้คะแนนอารมณ์ไปด้วย
หัวข้อให้เลือกจากรายการนี้เท่านั้น (ห้ามสร้างเอง): การตีบวก/อัตรา, การปรับสมดุล, เซิร์ฟเวอร์/ประสิทธิภาพ, การชำระเงิน/คืนเงิน,
ขอเนื้อหาใหม่, ทูทอเรียล/ออนบอร์ดดิง, UI/การควบคุม, คำชม/ให้กำลังใจ, อื่น ๆ ถ้า 'อื่น ๆ' เกิน 8 รายการ ให้เสนอหัวข้อใหม่เป็นตัวเลือกด้วย
อารมณ์ให้เป็น ลบ·กลาง·บวก ความรุนแรงให้เป็น S1·S2·S3·S4
// (เจตนา: S1 ใช้เมื่อเป็นรูปธรรม·ทำซ้ำได้·ปิดกั้นฟังก์ชันเท่านั้น ความไม่พอใจรุนแรงเฉย ๆ ให้เป็น S2)
รายการที่ไม่มั่นใจให้ปล่อยไว้ที่ 'อื่น ๆ' แล้วเติม ★ ท้าย id ส่งมาให้ฉัน อย่าฝืนยัดเข้าหมวด
ขอแค่สองตารางเท่านั้น — ตาราง A (รายรายการ): id·หัวข้อ·อารมณ์·ความรุนแรง / ตาราง B (รายหัวข้อ): หัวข้อ·จำนวน·%ลบ·ข้อความตัวอย่าง 1 ข้อ·มี S1 หรือไม่
ในพรอมต์นี้ สิ่งที่กำหนดผลการจัดประเภทไม่ใช่รายการหัวข้อหรือสเกลอารมณ์ แต่เป็นกลไกควบคุมสามอย่าง คือ ตรึง ป้ายกำกับให้คงที่ ตอกหมุด รูปแบบให้แน่น และ เปิด ทางออกไว้ ถ้าปล่อยให้ LLM สร้างหัวข้อได้เอง มันจะสร้างป้ายกำกับแบบเหมารวมอย่าง "ความไม่พอใจโดยรวม" หรือ "ขอให้ปรับปรุงตัวเกม" ที่นำไปจัดลำดับความสำคัญไม่ได้ รายการป้ายกำกับที่อนุญาต (กฎข้อ 1), นิยามความรุนแรง (กฎข้อ 3 — นิยามที่แคบของ S1 คือหัวใจ), รูปแบบเอาต์พุต (สองตาราง) และ "ถ้าไม่มั่นใจให้เติม ★ แล้วส่งให้คน" (กฎข้อ 5) ถ้าขาดสี่อย่างนี้ ตารางก็จะกลายจากเครื่องมือจัดลำดับความสำคัญไปเป็นสิ่งที่ไว้อ่านอีกชิ้นหนึ่ง รายการป้ายกำกับนี้เองคือ ระบบหมวดหมู่ของเลเยอร์ฟีดแบ็ก L4 ที่กล่าวไว้ใน §15.1.5 — ป้ายกำกับต้องตรึงคงที่ จึงจะเปรียบเทียบแนวโน้มด้วยแกนเดียวกันได้ทุกไตรมาส
[ตาราง A] การจัดประเภทรายรายการ (คัดบางส่วน)
id | หัวข้อ | อารมณ์ | ความรุนแรง
fb_0001 | การตีบวก/อัตรา | ลบ | S2
fb_0002 | เซิร์ฟเวอร์/ประสิทธิภาพ | ลบ | S2
fb_0003 | การชำระเงิน/คืนเงิน | ลบ | S1
fb_0004 | ขอเนื้อหาใหม่ | กลาง | S3
fb_0005 | คำชม/ให้กำลังใจ | บวก | S4
fb_0006 | การปรับสมดุล | ลบ | S1
fb_0007 | ทูทอเรียล/ออนบอร์ดดิง | ลบ | S1
fb_0042★ | อื่น ๆ | กลาง | S3
...
[ตาราง B] การรวมยอดรายหัวข้อ (ทั้งหมด 100 รายการ)
หัวข้อ | จำนวน | %ลบ | ข้อความตัวอย่าง | มี S1
เซิร์ฟเวอร์/ประสิทธิภาพ | 24 | 92% | "เด้งทุกครั้งที่สงครามกิลด์" | ใช่ (ทำซ้ำได้บางส่วน)
การตีบวก/อัตรา | 21 | 95% | "ตีบวก +12 พังไป 50 ครั้ง" | ไม่
การปรับสมดุล | 14 | 86% | "เฮ็กรังใส่ของเต็มยังโดนทีเดียว" | ใช่
ขอเนื้อหาใหม่ | 12 | 8% | "นักธนูจะออกเมื่อไหร่" | ไม่
ทูทอเรียล/ออนบอร์ดดิง | 9 | 78% | "ขั้นที่ 5 ปุ่มกดไม่ติด (Galaxy A)" | ใช่
การชำระเงิน/คืนเงิน | 8 | 100% | "เพชรไม่เข้า" | ใช่
UI/การควบคุม | 6 | 67% | "ปุ่มสกิลเล็กไปจนกดผิด" | ไม่
คำชม/ให้กำลังใจ | 4 | 0% | "ทีมงานสื่อสารดีนะ" | ไม่
อื่น ๆ | 2 | - | (fb_0042★, fb_0067★ รอคนตัดสิน) | -
อารมณ์รวม: ลบ 71 / กลาง 18 / บวก 11
'อื่น ๆ' 2 รายการ (น้อยกว่า 8) → ไม่จำเป็นต้องเสนอหัวข้อใหม่
รายการที่ไม่มั่นใจ มี ★ 2 รายการ: fb_0042, fb_0067 (ขอให้คนตัดสิน)
ในเอาต์พุตนี้ ส่วนที่มีค่าที่สุดไม่ใช่ตาราง แต่เป็น สองบรรทัดล่างสุด ที่ว่า "รายการที่มี ★ 2 รายการ" เป็นจุดที่ LLM แจ้งสิ่งที่ตัวเองมัดไม่ได้และส่งต่อให้คน เป็นการออกแบบแบบเดียวกับใน §6.2 ที่ AI ติดป้ายว่าคลุมเครือให้ NPC 'เกรม (Grem)' ด้วยตัวเอง พรอมต์ที่ดีคือพรอมต์ที่ทำให้ AI พูดได้ว่า "อันนี้ผมไม่มั่นใจ"
จะรับเอาต์พุตนี้มาดื้อ ๆ ไม่ได้ มีอยู่จุดหนึ่งที่ติดขัดจริง
หัวข้อ การตีบวก/อัตรา 21 รายการถูกจัดเป็น S2 (ความไม่พอใจ) ทั้งหมด แต่ในนั้น fb_0001 มีคำว่า "ขอเงินคืน" ติดมาด้วย LLM มองอันนี้เป็นเพียง "ความไม่พอใจรุนแรง (S2)" ตรงนี้คนต้องเข้าแทรก ความไม่พอใจต่ออัตราการตีบวก — ตราบใดที่ข้อมูลยืนยันว่าอัตราทำงานตามสเปก — ไม่ใช่อุบัติเหตุระดับ S1 เพราะความไม่พอใจต่ออัตราที่ทำงานตามสเปกเป็น ปัญหาเชิงการออกแบบ·ความรู้สึก ไม่ใช่ บั๊ก การตัดสิน S2 ของ LLM ถูกต้อง เพียงแต่สัญญาณ "การเรียกร้องคืนเงิน" ต้อง cross-link ไปยังหัวข้อการชำระเงิน เพื่อให้ CS ดูแยกต่างหาก LLM ติดป้ายหัวข้อเป็นป้ายเดียว และพลาดกรณีที่หนึ่งรายการคร่อมสองหัวข้อ
จึงร้องขอใหม่ดังนี้
เพิ่มกฎ: ถ้าหนึ่งรายการคร่อมสองหัวข้อ (เช่น ไม่พอใจการตีบวก + เรียกร้องคืนเงิน) นอกจากหัวข้อหลักแล้ว
ให้เขียนหัวข้อรองในช่อง 'cross' เพิ่มช่อง cross เข้าไปในตาราง A แล้วส่งเอาต์พุตใหม่
แต่ความไม่พอใจต่ออัตราการตีบวกเอง ถ้าข้อมูลยืนยันว่าอัตราเป็นไปตามสเปก ให้คงไว้ที่ S2 ไม่ใช่ S1
จบในการไป-กลับครั้งเดียวนี้ LLM ตอบใหม่ให้ fb_0001 เป็น 토픽=강화/확률, cross=결제/환불, 심각도=S2 ส่วนรายการที่มี ★ 2 รายการ คนอ่านเองแล้วย้าย fb_0042 ไปที่ UI/조작 และ fb_0067 ไปที่ 튜토리얼/온보딩 ถ้าคนอ่านและจัดประเภท 100 รายการตั้งแต่ต้นจะใช้เวลาครึ่งวัน แต่ถ้าใช้ร่างแรกจาก LLM + คนตรวจสอบ + ไป-กลับ 1 ครั้ง จะอยู่ในราวหนึ่งชั่วโมง (การประมาณของผู้เขียน ยังไม่ได้ตรวจสอบ เป็นสมมติฐาน — ค่าที่ประหยัดได้จริงต่างกันไปตามจำนวนฟีดแบ็กและจำนวนช่องทาง จึงควรอ่านเป็นความต่างเชิงโครงสร้างระหว่าง "ทำด้วยมือตั้งแต่ต้น" กับ "ร่างแรก+ตรวจสอบ" มากกว่าเวลาสัมบูรณ์)
ตรงนี้ขอลากเส้นแบ่งที่ชี้ขาด ตาราง B ข้างบนบอกได้แค่ "หัวข้อไหนกี่รายการ ลบมากแค่ไหน" เท่านั้น "แล้วสัปดาห์นี้จะแก้อะไรก่อน" เป็นสิ่งที่ LLM ให้ไม่ได้ นั่นคือการตัดสินใจที่พัวพันกับต้นทุน·กำหนดการ·วิสัยทัศน์ของเกม และความรับผิดชอบในการตัดสินใจนั้นอยู่ที่ Design Director
วางตารางเดียวกันไว้ตรงหน้า สองทีมงานบริการอาจตัดสินใจตรงกันข้ามได้ ถ้าดูแค่จำนวน เซิร์ฟเวอร์/ประสิทธิภาพ (24 รายการ) กับ การตีบวก/อัตรา (21 รายการ) อยู่อันดับ 1·2 แต่ลำดับความสำคัญกลับไปคนละทางกับลำดับจำนวน เหตุผลคือ ความรุนแรงและความย้อนกลับได้
flowchart TB
A["ฟีดแบ็ก 100 รายการ
(jsonl ทำมาตรฐาน 4 ช่องทาง)"] --> B["การคลัสเตอร์ด้วย LLM
ให้คะแนนหัวข้อ·อารมณ์·ความรุนแรง"]
B --> C{"คนตรวจสอบ
รายการ ★·cross·จัดผิด"}
C -->|ร้องขอใหม่| B
C -->|ยืนยัน| D["ตารางรวมยอดรายหัวข้อ
จำนวน·%ลบ·มี S1"]
D --> E["การตัดสินใจจัดลำดับความสำคัญ
(Design Director — LLM ทำไม่ได้)"]
E --> F1["อุบัติเหตุ S1: ฮอตฟิกซ์ทันที
การชำระเงิน·ทูทอเรียลถูกปิดกั้น"]
E --> F2["ความไม่พอใจ S2: ตรวจข้อมูลแล้ว
ตัดสินเชิงการออกแบบ"]
E --> F3["คำขอ S3: แบ็กล็อกรายไตรมาส
ตรึงเป็นกฎในสล็อต voice"]
F1 --> G["วงจรการตอบกลับ
(§15.3.4)"]
F2 --> G
F3 --> G
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class A,D data;
class B ai;
class C,E human;
ในกระแสนี้ จุดที่มือคนแตะถึงมีแค่สองแห่ง ด่านตรวจสอบตรงกลาง (การตัดสินรายการ ★·cross·การจัดผิด) และการตัดสินใจจัดลำดับความสำคัญที่ล่างสุด ส่วนการจัดประเภท 100 รายการอันน่าเบื่อระหว่างนั้นให้ LLM หมุน และตรรกะจริงของการตัดสินใจจัดลำดับความสำคัญไม่ใช่จำนวน แต่เป็นสามแกนต่อไปนี้
| หัวข้อ | จำนวน | การตัดสินลำดับความสำคัญ (ที่ทางของ Design Director) |
|---|---|---|
| การชำระเงิน/คืนเงิน (S1) | 8 | อันดับ 1 จำนวนน้อยแต่ปิดกั้นฟังก์ชัน + ย้อนกลับไม่ได้ (เงิน) ฮอตฟิกซ์ภายใน 24h |
| ทูทอเรียล/ออนบอร์ดดิง (S1) | 9 | อันดับ 2 เชื่อมตรงกับการหลุดของผู้ใช้ใหม่ ทำซ้ำได้บนเครื่องเฉพาะ → แพตช์ |
| เซิร์ฟเวอร์/ประสิทธิภาพ | 24 | อันดับ 3 จำนวนมากที่สุดแต่เป็นงานโครงสร้างพื้นฐาน = กำหนดการยาว ฮอตฟิกซ์ไม่ได้ ไว้สัปดาห์หน้า |
| การตีบวก/อัตรา (S2) | 21 | คงไว้ ถ้าข้อมูลเป็นไปตามสเปกก็ไม่ใช่บั๊ก ตรวจแยกต่างหากด้วยการตัดสินเชิงการออกแบบ |
| ขอเนื้อหาใหม่ | 12 | แบ็กล็อก ลบ 8% (= ความคาดหวังเชิงบวก) ตรึงเป็นกฎในสล็อต voice รายไตรมาส |
เหตุที่ เซิร์ฟเวอร์/ประสิทธิภาพ ซึ่งจำนวนเป็นอันดับ 1 ตกลงไปเป็นลำดับความสำคัญอันดับ 3 ก็เพราะเป็นงานโครงสร้างพื้นฐานที่ฮอตฟิกซ์แก้ไม่ได้ และเหตุที่ การชำระเงิน/คืนเงิน ซึ่งจำนวนเป็นอันดับ 6 ขึ้นมาเป็นอันดับ 1 ก็เพราะเป็นอุบัติเหตุที่ย้อนกลับไม่ได้และมีเงินเป็นเดิมพัน การจัดเรียงใหม่นี้ LLM ทำไม่ได้ LLM ให้ได้แค่ข้อเท็จจริงว่า "การชำระเงิน 8 รายการ ลบ 100%" การตัดสินว่านั่นเป็นอันดับ 1 เป็นหน้าที่ของคนที่รู้ต้นทุน·ความเสี่ยงทางกฎหมาย·วิสัยทัศน์ของเกม นี่คือภาพจริงในด้านฟีดแบ็กของสิ่งที่ §15.1.5 กล่าวไว้ว่า "AI สร้างการจัดประเภทและตัวเลือก ส่วนคนทุ่มไปที่การคัดเลือกและการตัดสินวิสัยทัศน์"
เมื่อกำหนดลำดับความสำคัญแล้วก็ตอบกลับผู้ใช้ ในงาน Live Ops การไม่ตอบกลับคือจุดที่ทำลายความเชื่อใจมากที่สุด ต่อให้ไม่มีอะไรจะตอบ "กำลังตรวจสอบ" ก็ยังดีกว่าการไม่ตอบ ร่างคำตอบกลับก็ให้ LLM ดึงออกมาตามหัวข้อได้เช่นกัน
[ร่างการตอบกลับ — เอาต์พุตของ LLM, รายหัวข้อ]
- การชำระเงิน/คืนเงิน (S1): "ยืนยันกรณีเพชรไม่เข้าแล้ว จะจ่ายย้อนหลังภายใน 24 ชั่วโมงตามเลขที่คำสั่งซื้อ และจะตอบกลับเป็นรายบุคคล"
- เซิร์ฟเวอร์/ประสิทธิภาพ: "กำลังตรวจสอบเพื่อทำซ้ำอาการเด้งที่เกิดตอนสงครามกิลด์ มีกำหนดดำเนินการก่อนในการปิดปรับปรุงสัปดาห์หน้า และจะแจ้งความคืบหน้าผ่านประกาศ"
- การตีบวก/อัตรา: "ยืนยันด้วยข้อมูลแล้วว่าอัตราการตีบวกถูกนำมาใช้ตรงตามสเปกที่ระบุ อย่างไรก็ตาม ความเห็นเรื่องความยากตามความรู้สึกกำลังอยู่ระหว่างการตรวจสอบแยกต่างหาก"
- ขอเนื้อหาใหม่ (นักธนู): "อาชีพใหม่อยู่ในโรดแมป และเมื่อกำหนดการชัดเจนแล้วจะประกาศให้ทราบเป็นอันดับแรก"
ตรงนี้มีจุดหนึ่งที่ต่างจาก §6.2 อย่างชี้ขาด การส่งคำตอบกลับเป็นขั้นที่ย้อนกลับไม่ได้ NPC ของเมืองทิ้งแล้วทำใหม่ก็จบ แต่ข้อความประกาศ·คำตอบกลับที่ผู้ใช้เห็นไปแล้วครั้งหนึ่งย้อนคืนไม่ได้ ถ้าส่งอัตโนมัติไปว่า "จ่ายภายใน 24 ชั่วโมง" แต่จริง ๆ ใช้เวลาสามวัน คำสัญญานั้นก็จะเหลือเป็นร่องรอยที่ย้อนกลับไม่ได้ในคอมมิวนิตี้ ด้วยเหตุนี้หลักการของขั้นที่ย้อนกลับไม่ได้ใน §15.1.4 จึงทำงาน หนักกว่า ในด้านฟีดแบ็กเมื่อเทียบกับด้านอื่น ร่างคำตอบกลับอัตโนมัติให้ LLM สร้าง แต่ ก่อนผ่านด่านตรวจสอบของ CS จะไม่ส่งอัตโนมัติแม้แต่ตัวอักษรเดียว ผู้ตรวจสอบดูเพียงว่าคำสัญญาเรื่องกำหนดการ (24h·สัปดาห์หน้า) ตรงกับกำหนดการทำงานจริงหรือไม่ และกรณีอ่อนไหว (ข้อพิพาททางกฎหมาย·ข้อพิพาทเรื่องคืนเงิน) ปนเข้าไปในกองส่งอัตโนมัติหรือไม่ เป็นที่ทางที่คนรับผิดชอบการตัดสินที่ lint จับไม่ได้
| ขั้น | ความย้อนกลับได้ | ใคร |
|---|---|---|
| การคลัสเตอร์ฟีดแบ็ก·การให้คะแนนอารมณ์ | ย้อนกลับได้ (รันใหม่ได้อิสระ) | LLM |
| การตรวจสอบหัวข้อ·การตัดสินใจจัดลำดับความสำคัญ | ย้อนกลับได้ (ก่อนยืนยัน) | คน (Design Director) |
| การสร้างร่างคำตอบกลับ | ย้อนกลับได้ (ทิ้ง·เขียนใหม่) | LLM |
| การส่งคำตอบกลับ·การลงประกาศ | ย้อนกลับไม่ได้ (ผู้ใช้รับรู้แล้ว) | คน (หลัง CS ตรวจสอบ) |
เพื่อไม่ให้ฟีดแบ็กเดียวกันแกว่งไปสู่การตัดสินใจที่ต่างกันในแต่ละไตรมาส ต้อง ตรึงผลการคลัสเตอร์ไว้เป็นสล็อตอินพุตคงที่ของการทบทวนรายไตรมาส กล่าวคือ ไม่ใช่ "ช่วงนี้บ่นเรื่องตีบวกกันเยอะนะ" แบบด้นสด แต่เป็นตารางที่รวมยอดด้วยแกนป้ายกำกับเดียวกันทุกไตรมาสเข้าไปอยู่ในตารางการทบทวน เหตุที่ §15.3.2 ห้ามสร้างป้ายกำกับเองและตรึงไว้เป็นรายการที่อนุญาต ก็ได้คืนทุนตรงนี้
2026 Q2 voice ของผู้ใช้ (LLM รวมยอดอัตโนมัติ, สะสมรายไตรมาส)
คลัสเตอร์ 4 ช่องทางสะสมราว 5,000 รายการ (จำนวนคือยอดรวมจริงของไตรมาส — ไม่ใช่การปรุงแต่ง) หัวข้อลบสูงสุด: 강화/확률 > 서버/성능 > 밸런스 > 결제/환불 หัวข้อขอสูงสุด: 신규직업 > 신규사냥터 > 길드시스템 > UI개선 แนวโน้มอารมณ์รายไตรมาส: Q1 ลบ 68% → Q2 ลบ 71% (แย่ลงเล็กน้อย — หัวข้อ강화ฉุด)
ตารางนี้กลายเป็น อินพุต ของการตัดสินใจรายไตรมาส การตัดสินใจเองเป็นหน้าที่ของ Design Director ส่วนอินพุตเป็นของผู้ใช้ แนวโน้มรายไตรมาส ("Q1 68% → Q2 71%") อ่านเป็น ทิศทาง เท่านั้น ค่าสัมบูรณ์ของไตรมาสเดียวไม่ใช่สัญญาณ แต่ทิศทางการเปลี่ยนแปลงบนแกนป้ายกำกับเดียวกันต่างหากที่เป็นสัญญาณ ถ้า %ลบสูงขึ้น ก็ย้อนดูว่า "หัวข้อไหนเป็นตัวฉุดขึ้น" แล้วเชื่อมไปสู่ลำดับความสำคัญของไตรมาสถัดไป ร่างรายงานรายไตรมาสนี้เองก็ให้ LLM ดึงออกมาเป็นภาษาธรรมชาติ ส่วนคนแค่เติมคอมเมนต์การตัดสินใจ — เป็นที่ทางจริงของร่างรายงานรายไตรมาสอัตโนมัติที่กล่าวไว้ใน §15.1.5
บทว่าด้วย Live Ops มักมีแรงล่อใจสูงที่จะใส่ตารางอย่าง "พอนำวงจรฟีดแบ็กมาใช้ NPS ก็ขึ้นจาก 20 เป็น 45" ผู้เขียนไม่เคยวัดความเป็นเหตุเป็นผลนั้นจึงไม่เขียน หลักการของหนังสือเล่มนี้มีหนึ่งในสามข้อต่อไปนี้
ข้อแรก จำนวนยอดรวมจริงให้ใช้ตามนั้น จำนวนรายหัวข้อใน §15.3.2 (เซิร์ฟเวอร์ 24·ตีบวก 21·ชำระเงิน 8) และยอดสะสมรายไตรมาสใน §15.3.5 คือค่าที่นับผลการจัดประเภททีละรายการ ไม่ใช่อัตราส่วนที่จัดวางให้ดูสวยงาม
ข้อสอง การประมาณให้เขียนว่าเป็นการประมาณ "จัดประเภท 100 รายการจากครึ่งวัน→หนึ่งชั่วโมง" (§15.3.2), "อารมณ์ในฟอรัมเอียงไปทางลบ" (§15.3.1) เป็นการประมาณบนพื้นฐานประสบการณ์·การสังเกตของผู้เขียน และเป็นสมมติฐานที่ยังไม่ได้ตรวจสอบ อย่าท่องค่าสัมบูรณ์ แต่อ่านเป็น ทิศทาง (ปริมาณฟีดแบ็กชนะจำนวนคนเสมอ, ข้อความที่เขียนเองเอนไปทางความไม่พอใจ) ก็พอ
ข้อสาม สัญญาเป็นตัวชี้วัดเฉพาะสิ่งที่วัดได้ สิ่งที่วงจรฟีดแบ็กวัดได้จริงไม่ใช่ความพึงพอใจที่เป็นผลลัพธ์ (NPS) แต่เป็นตัวชี้วัดเชิงกระบวนการ — ปริมาณฟีดแบ็กที่ยังไม่จัดประเภทคงค้าง (เป้าหมาย 0), ลีดไทม์ตั้งแต่พบอุบัติเหตุ S1 จนถึงฮอตฟิกซ์, เวลาตอบกลับ, สัดส่วนหัวข้อ 'อื่น ๆ' (ถ้าป้ายกำกับที่อนุญาตครอบคลุมความเป็นจริงไม่ได้ 'อื่น ๆ' ก็จะพองตัว) สี่อย่างนี้พูดในที่ประชุมได้ด้วยตัวเลข ไม่ใช่ด้วย "ความรู้สึก"
ถ้าทำคนเดียว แค่นี้ก็พอ: ไม่ต้องมีระบบ CS ไม่ต้องมีชุดข้อมูล แค่คัดลอกรีวิวในสโตร์·โพสต์คอมมิวนิตี้ของเกมตัวเอง (หรือเกมที่ชอบ) สัก 20–30 รายการด้วยมือแล้วทำเป็น jsonl (
{"id":..., "src":..., "text":...}) แล้วเอาพรอมต์ใน §15.3.2 มาวางตามนั้นแล้วลองรันสักครั้ง จากตาราง B ที่ได้ ให้หาสักหนึ่งรายการที่ "หัวข้อที่จำนวนเป็นอันดับ 1" กับ "หัวข้อที่คุณอยากแก้ก่อน" ต่างกัน แล้วลองเขียนเหตุผลที่ต่างกันสักหนึ่งบรรทัด — แล้วคุณจะซึมซับด้วยตัวเองว่าทำไมการจัดลำดับความสำคัญจึงไม่ใช่งานของ LLM แต่เป็นงานของคน
ถ้าเป็นทีม ให้เริ่มด้วยหนึ่งขั้นต่อไปนี้ ตรึงสคริปต์ดึงข้อมูลที่รวบรวมฟีดแบ็ก 4 ช่องทางให้เป็น jsonl หนึ่งบรรทัดต่อหนึ่งเรกคอร์ด และ รายการป้ายกำกับหัวข้อที่อนุญาต ใน §15.3.2 ไว้ก่อน ป้ายกำกับต้องตรึงคงที่ จึงจะวัดด้วยแกนเดียวกันได้ทั้งการจัดประเภทด้วย LLM หรือด้วยคน และเปรียบเทียบแนวโน้มรายไตรมาสได้ การตอบกลับอัตโนมัติเป็นเรื่องถัดไป — เพราะการตอบกลับย้อนกลับไม่ได้ จึงไม่เชื่อมเข้ากับการส่งอัตโนมัติเด็ดขาดหากไม่มีด่านตรวจสอบของ CS
บ่ายสี่โมงวันพฤหัสบดี การประชุม TF การต่อสู้จบลง แล้วทั้งเจ็ดคนก็แยกย้ายกลับไปที่โต๊ะของตน บนไวต์บอร์ดยังคงเหลือร่องรอยของการถกเถียงว่าจะลดค่า Global Cooldown จาก 0.8 วินาทีลงเหลือ 0.5 วินาทีดีหรือไม่ ซีเนียร์ฝ่ายบาลานซ์บอกว่า "ในซิมของผม 0.5 ถูกต้องแล้ว" ส่วนโค้ดลีดบอกว่า "ถ้า 0.5 เซิร์ฟเวอร์ติ๊กตามไม่ทัน" ฝ่ายดีไซน์ UI บอกว่า "ทั้งสองค่าผมก็ไม่แน่ใจ แต่ที่แน่ ๆ คือความกว้างของเกจคูลดาวน์จะแคบเกินไป"
ทั้งสามคนพูดถูกทั้งหมด และถ้าทั้งสามคนเริ่มจดข้อสรุปของตนลงในเอกสารของสายงานตนเอง สัปดาห์ถัดมาเอกสารทั้งสามฉบับนี้ก็จะขัดแย้งกันเอง ชีตบาลานซ์จดไว้ที่ 0.5 สเปกโค้ดจดไว้ที่ 0.8 ส่วนไกด์ UI จดไว้ที่ 0.6 ไม่ว่าใครมาดูก็ไม่รู้ว่าฉบับไหนคือฉบับจริง
เหตุผลที่ TF การต่อสู้มีอยู่ ก็คือการดูดซับความขัดแย้งนี้ให้จบในที่เดียวนั่นเอง และผลลัพธ์ของการดูดซับนั้น — การตัดสินใจเพียงหนึ่งเดียว — เท่านั้นที่ควรถูกยกขึ้นเป็นเอกสารฉบับจริง ส่วนเศษซากของการถกเถียงที่เหลือต้องจบลงภายในพื้นที่ทำงานที่ถูกแยกออกมา บทนี้ว่าด้วยกลไกของการแยกและการดูดซับนั้น
การปรับโครงสร้างระบบการต่อสู้ครั้งใหญ่ไม่ได้จบลงด้วยสายงานเดียว เพียงแค่แตะ Global Cooldown ค่าเดียว บาลานซ์ (ตัวเลข) โค้ด (เซิร์ฟเวอร์ติ๊ก) UI (การแสดงผลเกจ) แอนิเมชัน (ความยาวโมชัน) และซาวด์ (ความรู้สึกของการกระแทก) ก็สั่นไหวพร้อมกัน หากนำวาระแบบนี้ไปแยกดำเนินการตามสายงาน การตัดสินใจจะยืดออกไปครั้งละ 2\~4 สัปดาห์ และต่อให้ตัดสินใจได้ก็ยังเหลื่อมล้ำกันระหว่างสายงาน
TF (TaskForce) คือหน่วยที่รวบรวมหลายสายงานเข้ามาอยู่ในพื้นที่ทำงานเดียวกันชั่วคราว เพื่อป้องกันความเหลื่อมล้ำนี้ หัวใจอยู่ที่ "ชั่วคราว" และ "การแยก" หากปล่อยให้การถกเถียงของ TF ไหลเข้าสู่ระบบเอกสารฉบับจริงของบริษัทตรง ๆ การถกเถียงที่ยังไม่ได้ตรวจสอบ ข้อเสนอที่ถูกตีตก และตัวเลขที่ยังอยู่ระหว่างทดลองก็จะปนเปื้อนฉบับจริง ดังนั้นเราจึงสร้างพื้นที่ทำงานที่ถูกแยกออกมาขึ้นภายใน SVN โดยใช้หมายเลขขึ้นต้นด้วย 95_
95_BattleTF หมายเลขในช่วง 95 เป็นข้อตกลงที่หมายถึงพื้นที่ทำงานของ TF ระยะสั้น โดยทั่วไป docs ฉบับจริงจะใช้หมายเลขในช่วง 10 และ 20 ส่วนหมายเลขในช่วง 90 คือสัญญาณของ "ชั่วคราว แยกออกมา และกำหนดยุติ" แค่เห็นหมายเลขโฟลเดอร์ก็สื่อได้ทันทีว่า "ที่นี่ไม่ใช่ฉบับจริง อย่าอ้างอิงตัวเลขที่เห็นในที่นี้"
กฎของการแยกนั้นเรียบง่าย
95_BattleTF เท่านั้นTF_결정사항_요약.md เพียงหนึ่งเดียว ขึ้นสู่ docs ฉบับจริง95_BattleTF/archive/ เพื่อจัดเก็บเท่านั้นเหตุผลที่ TF ซึ่งกลายเป็นแผนกถาวรนั้นอันตราย ก็อยู่ตรงนี้ เมื่อการแยกถูกปลดออก ตัวเลขที่ยังไม่ได้ตรวจสอบของพื้นที่ทำงาน TF ก็จะเริ่มถูกอ้างอิงเสมือนเป็นฉบับจริง และการตัดสินใจเดียวกันก็จะถูกรื้อขึ้นมาในที่อื่นซ้ำ ๆ ทุกไตรมาส
หนึ่งรอบของ TF การต่อสู้มีโครงสร้างคือ เปิดพื้นที่ที่ถูกแยกออกมา สั่งสมการถกเถียง การทดลอง และการตัดสินใจไว้ภายในนั้น แล้วเมื่อยุติก็ดูดซับเฉพาะการตัดสินใจขึ้นสู่ฉบับจริง
flowchart TD
A["เกิดวาระการต่อสู้
(Global Cooldown 0.8→0.5?)"] --> B["เปิดพื้นที่แยก 95_BattleTF
(หมายเลขช่วง SVN 95_)"]
B --> C["สั่งสมผลผลิตภายในพื้นที่แยก
บันทึกประชุม·ชีตทดลอง·ข้อเสนอที่ถูกตีตก·บันทึกดิบ"]
C --> D{"ได้ข้อสรุปหรือยัง?"}
D -->|ยังไม่ได้| C
D -->|ยืนยันแล้ว| E["อัปเดต TF_결정사항_요약.md
(ภายในพื้นที่แยก)"]
E --> F{"TF ยุติหรือไม่?"}
F -->|ดำเนินต่อ| C
F -->|ยุติ| G["ยกระดับ TF_결정사항_요약.md
เพียง 1 ฉบับสู่ docs ฉบับจริง"]
G --> H["ลดระดับที่เหลือทั้งหมด
ไปยัง 95_BattleTF/archive/"]
G --> I["ส่งให้ทีมอาร์ตเฉพาะ html เท่านั้น
(ไม่แชร์ไฟล์ต้นฉบับ md)"]
H --> J["ปิดพื้นที่แยก"]
style B fill:#fff3cd,stroke:#d39e00
style G fill:#d4edda,stroke:#28a745
style H fill:#f8d7da,stroke:#dc3545
วาระเข้ามาจากมุมบนซ้าย เสียงรบกวนทั้งหมดถูกจัดการภายในพื้นที่แยกสีเหลือง แล้วมีเพียงช่องสีเขียวช่องเดียว — สรุปการตัดสินใจ — เท่านั้นที่ไหลออกไปสู่ฉบับจริง สีแดงคือการลดระดับ ภาพเดียวนี้คือทั้งหมดของการดำเนินงานพื้นที่ทำงานช่วง 95
ในจังหวะที่ TF ยุติ งานที่กินแรงที่สุดคือการคัดเอา "เฉพาะการตัดสินใจที่จะยกขึ้นเป็นฉบับจริง" ออกมาจากบันทึกการประชุมและชีตทดลองของหนึ่งไตรมาส การถกเถียงนั้นยืดยาว ข้อเสนอที่ถูกตีตกกับข้อเสนอที่ยืนยันแล้วปะปนกันอยู่ และตัวเลขเดียวกันก็ถูกจดไว้ต่างกันเล็กน้อยในแต่ละการประชุม หากให้คนจัดการด้วยมือ ลำพังงานปิด TF ก็กินเวลาไปทั้งวัน
ด้านล่างนี้คือพรอมต์ที่รันจริง ผลลัพธ์ดิบของ Claude และกระบวนการทั้งหมดว่าผมตรวจสอบ ปฏิเสธ และร้องขอใหม่อย่างไร ลงไว้ตามจริงโดยไม่ย่อ
จากบันทึกการประชุม 95_BattleTF จำนวน 6 ฉบับด้านล่าง ช่วยคัดเฉพาะการตัดสินใจที่ยืนยันแล้วซึ่งจะยกขึ้นเป็นฉบับจริง
แล้วร่าง TF_결정사항_요약.md ให้หน่อย TF กำลังจะยุติเร็ว ๆ นี้
เอาเฉพาะที่ยืนยันแล้ว (ตัดที่ถูกตีตก ที่กำลังทดลอง และ "ค่อยดูทีหลัง" ออก) แต่ละการตัดสินใจ
ให้อยู่ในรูปแบบ รหัสการตัดสินใจ·หัวข้อ·ค่าที่ยืนยัน·เหตุผล (แหล่งข้อมูล)·ผู้ตัดสินใจ·วันที่ยืนยัน
ถ้าหัวข้อเดียวกันมีค่าเปลี่ยนไปในแต่ละการประชุม ให้เก็บเฉพาะค่าสุดท้ายและทิ้งของก่อนหน้า
ถ้าไม่แน่ใจว่ายืนยันแล้วหรือไม่ อย่าทิ้ง แต่ให้แยกออกไปเป็น [ต้องพิจารณา] และถ้าไม่มีแหล่งเหตุผลให้ระบุเป็น [ไม่ได้บันทึกเหตุผล]
บันทึกการประชุม:
--- บันทึกการประชุม 2026-W14-mon ---
วาระ1: พิจารณา Global Cooldown 0.8→0.5 จากซิมบาลานซ์ (sim_gcd_0407.xlsx)
0.5 ทำให้การกระจาย DPS -12% โค้ดลีด: 0.5 ติดข้อจำกัดเซิร์ฟเวอร์ติ๊ก (33ms) ขอชะลอไว้ก่อน
ผลสรุป: เห็นพ้องเบื้องต้นที่ 0.6 (ชั่วคราว) ให้ฝ่ายโค้ดทดสอบโหลด 0.6 ภายในการประชุมครั้งหน้า
วาระ2: i-frame หลบหลีก 12f→10f แอนิเมชันไดเรกเตอร์บอกโมชัน 8f ถ้า 10f จะเกิดความขาดช่วงทางสายตา
ผลสรุป: ยืนยันคง 12f ผู้ตัดสินใจ ลีดฝ่ายออกแบบ
... (ไม่มีส่วนที่ตัดทอน ในความเป็นจริงมีบันทึกการประชุมฉบับเต็มทั้ง 6 ฉบับใส่เข้ามา) ...
--- บันทึกการประชุม 2026-W16-thu ---
วาระ1: Global Cooldown การทดสอบโหลด 0.6 ผ่าน (เซิร์ฟเวอร์ติ๊กเหลือ 18%)
ผลสรุป: ยืนยัน 0.6 ผู้ตัดสินใจ ลีดฝ่ายออกแบบ เหตุผล sim_gcd_0407.xlsx + load_0415.log
วาระ3: รางวัลสังหารเหรียญ 1.2 เท่า ฝ่ายบาลานซ์เสนอ ไม่มีเหตุผล ชะลอไว้
# TF_결정사항_요약.md (ร่าง)
## การตัดสินใจที่ยืนยันแล้ว
### D-GCD-01 | Global Cooldown
- ค่าที่ยืนยัน: 0.6 วินาที (เดิม 0.8 วินาที)
- เหตุผล: sim_gcd_0407.xlsx (ผลกระทบต่อการกระจาย DPS), load_0415.log (โหลดเซิร์ฟเวอร์ติ๊กเหลือ 18%)
- ผู้ตัดสินใจ: ลีดฝ่ายออกแบบ
- วันที่ยืนยัน: 2026-W16-thu
- หมายเหตุ: ใน W14 มีข้อเสนอ 0.5 แต่ติดข้อจำกัดเซิร์ฟเวอร์ติ๊กจึงลู่เข้าหา 0.6 ข้อเสนอ 0.5 ถูกยกเลิก
### D-IFRAME-01 | i-frame หลบหลีก
- ค่าที่ยืนยัน: คง 12 เฟรม
- เหตุผล: [ไม่ได้บันทึกเหตุผล] — มีเพียงการบันทึกการตัดสินเชิงคุณภาพว่าโมชันแอนิเมชัน 8f ทำให้เกิดความขาดช่วงทางสายตา
- ผู้ตัดสินใจ: ลีดฝ่ายออกแบบ
- วันที่ยืนยัน: 2026-W14-mon
## [ต้องพิจารณา]
### R-COIN-01 | รางวัลสังหารเหรียญ 1.2 เท่า
- สถานะ: ชะลอ (ฝ่ายบาลานซ์เสนอ ไม่มีข้อมูลเหตุผล)
- ความเห็น: ยืนยันไม่ได้เพราะไม่ได้ส่งเหตุผลมา แนะนำให้โอนเป็นวาระเดี่ยวของทีมบาลานซ์หลัง TF ยุติ
ผมดูทีละสามรายการ
D-GCD-01 ผ่านไปตามนั้น การทิ้งข้อเสนอชั่วคราว 0.5/0.6 ของ W14 แล้วเก็บเฉพาะการยืนยัน 0.6 ของ W16 นั้นถูกต้อง ทำตามกฎข้อ 3 ได้อย่างเหมาะสม การรวมแหล่งเหตุผลสองแหล่งเข้าด้วยกันก็ถูกต้องเช่นกัน
D-IFRAME-01 ผมปฏิเสธ Claude จัดรายการนี้เป็น "ยืนยันแล้ว" ทั้งที่เหตุผลคือ [ไม่ได้บันทึกเหตุผล] หากยกการตัดสินเชิงคุณภาพ (ความขาดช่วงทางสายตา) เพียงอย่างเดียวขึ้นเป็นการตัดสินใจที่ยืนยันแล้ว คนอื่นที่มาดูฉบับจริงก็จะหาเหตุผลของ "ทำไมต้อง 12f" ไม่เจอ นี่คือกรณีที่กฎข้อ 1 กับข้อ 5 ขัดกัน — Claude เห็นว่าผู้ตัดสินใจยืนยันแล้วจึงมองว่า "ยืนยัน" แต่ผมต้องบังคับใช้นโยบาย docs ของเราที่ว่า "การยืนยันที่ไม่มีเหตุผลรองรับ ขึ้นฉบับจริงไม่ได้" นโยบายนี้ไม่ได้จดไว้ในบันทึกการประชุม Claude จึงไม่มีทางรู้
R-COIN-01 จัดประเภทถูก แต่ข้อกำหนดเกินเลยไป "แนะนำให้โอนเป็นวาระเดี่ยวของทีมบาลานซ์" เป็นขั้นตอนที่ Claude แต่งขึ้น บริษัทเราไม่มีแทร็กการโอนแบบนั้น ผมรับการจัดประเภท (ต้องพิจารณา) ไว้ แต่ทิ้งประโยคที่เป็นข้อกำหนด
ปรับตามการตรวจสอบแล้วทำใหม่หน่อย D-GCD-01 ให้คงไว้ตามเดิม
D-IFRAME-01 เหตุผลมีแค่การตัดสินเชิงคุณภาพ ให้ดึงออกจากที่ยืนยันแล้วไปไว้ที่ [ยกระดับหลังเสริมเหตุผล]
R-COIN-01 ให้คงการจัดประเภทไว้ แต่ตัดประโยคข้อกำหนดอย่าง "แนะนำให้โอน" ออก — คุณไม่รู้ขั้นตอนของบริษัทเรา ให้จดแค่สถานะกับข้อเท็จจริง
และอย่าแต่งขั้นตอน แทร็ก หรือชื่อทีมที่ไม่มีในบันทึกการประชุม ถ้าไม่รู้ก็เว้นไว้เป็น [ต้องให้ลีดฝ่ายออกแบบตัดสิน]
// (เจตนา: เงื่อนไขยกระดับ D-IFRAME-01 สู่ฉบับจริง = แนบข้อมูลวัดเฟรมโมชันของแอนิเมชันไดเรกเตอร์เข้าบันทึกการประชุม)
# TF_결정사항_요약.md
## การตัดสินใจที่ยืนยันแล้ว (เป้าหมายยกระดับสู่ฉบับจริง)
### D-GCD-01 | Global Cooldown
- ค่าที่ยืนยัน: 0.6 วินาที (เดิม 0.8 วินาที)
- เหตุผล: sim_gcd_0407.xlsx, load_0415.log (โหลดเซิร์ฟเวอร์ติ๊กเหลือ 18%)
- ผู้ตัดสินใจ: ลีดฝ่ายออกแบบ / วันที่ยืนยัน: 2026-W16-thu
- หมายเหตุ: ข้อเสนอ 0.5 ถูกยกเลิกเพราะข้อจำกัดเซิร์ฟเวอร์ติ๊ก
## [ยกระดับหลังเสริมเหตุผล]
### D-IFRAME-01 | คง i-frame หลบหลีก 12f
- มีเพียงการบันทึกการตัดสินเชิงคุณภาพ (ความขาดช่วงทางสายตาเทียบกับโมชันแอนิเมชัน 8f)
- เงื่อนไขยกระดับ: แนบข้อมูลวัดเฟรมโมชันเข้าบันทึกการประชุม
## [ต้องให้ลีดฝ่ายออกแบบตัดสิน]
### R-COIN-01 | รางวัลสังหารเหรียญ 1.2 เท่า
- สถานะ: ชะลอ ฝ่ายบาลานซ์เสนอ ไม่มีข้อมูลเหตุผล ไม่มีค่าที่ยืนยัน
ในฉบับยืนยันนี้ เฉพาะส่วน "การตัดสินใจที่ยืนยันแล้ว" — D-GCD-01 รายการเดียว — เท่านั้นที่ถูกยกระดับสู่ docs ฉบับจริง อีกสองส่วนที่เหลือถูกลดลงไปที่ archive/ บันทึกการประชุม 6 ฉบับของหนึ่งไตรมาสถูกดูดซับลงเหลือฉบับจริงเพียงบรรทัดเดียว
ตรงนี้เองที่แยกได้ว่าอะไรคือสิ่งที่ AI ทำได้และทำไม่ได้ AI ไล่ตามการเปลี่ยนแปลงของค่าในหัวข้อเดียวกันข้ามบันทึกการประชุมทั้ง 6 ฉบับ แยกข้อเสนอที่ถูกตีตกออก และทำเครื่องหมายตรงที่เหตุผลขาดหาย — งานทำซ้ำเรียบง่ายที่ต้องเทียบบันทึกการประชุมหกฉบับทีละบรรทัดนี้เองที่มือคนพลาดได้ง่าย แต่การบังคับใช้นโยบายที่ว่า "การยืนยันที่ไม่มีเหตุผลรองรับ ขึ้นฉบับจริงไม่ได้" ข้อเท็จจริงของบริษัทที่ว่า "ไม่มีแทร็กการโอน" และการตัดสินขั้นสุดท้ายว่า "ยืนยัน/ชะลอ" ล้วนเป็นสิ่งที่คนทำทั้งหมด หากลบย่อหน้าที่ AI เขียนออก แรงงานในการคัดและจัดเรียงก็หายไป แต่การตัดสินใจว่าอะไรจะขึ้นฉบับจริงยังคงอยู่ในมือคน
วาระที่เข้ามาที่ TF ไม่ได้เกิดขึ้นภายในทั้งหมด มีคำขอจากผู้เผยแพร่ (publisher) เอาต์ซอร์สฝ่ายอาร์ต และทีมธุรกิจที่ว่า "เกี่ยวกับการต่อสู้ ช่วยทำเรื่องนี้ให้หน่อย" เข้ามาด้วย หากรับสิ่งเหล่านี้เป็นวาระ TF อย่างไม่เลือก TF ก็จะกลายเป็นช่องรับเรื่องร้องเรียนจากภายนอก
ดังนั้นพอคำขอจากภายนอกเข้ามา จะจัดประเภทเป็นสามแยกทันที เฉพาะเรื่องที่ต้องตัดสินใจเกี่ยวกับการต่อสู้เท่านั้นที่ส่งเข้า 95_BattleTF เรื่องที่จบได้ในสายงานเดียว ผู้รับผิดชอบจัดการเดี่ยว ส่วนเรื่องที่อยู่นอกขอบเขตหรือเหตุผลไม่พอ ให้เขียนเหตุผลแล้วตอบกลับหรือชะลอไว้ สิ่งที่เข้าสู่ TF มีเพียงแยกแรกเท่านั้น — นี่คือแนวป้องกันด่านแรกที่กัน TF ไม่ให้แปรสภาพเป็นช่องรับเรื่องร้องเรียน ตัวการจัดประเภทเองเป็นการตัดสินของคน แต่การให้ AI อ่านข้อความคำขอที่เข้ามาแล้วติดแท็กเบื้องต้นว่า "เรื่องนี้พาดพิงกี่สายงาน" ในระดับนั้น ให้ AI ช่วยกวาดสายตาดูก่อนได้
ลำดับการตัดสินของการจัดประเภทสามแยกนี้ (request-triangulate) บันทึกเซสชันจริง และการจัดการต่อเนื่องแยกตามแทร็ก เป็นหน้าที่ของบทถัดไป 16.2 ที่จะดูแลทั้งหมด ในที่นี้ระบุไว้เพียงกฎทางเข้าที่ว่า "TF รับเฉพาะแยกแรกเท่านั้น"
เมื่อการตัดสินใจของ TF ถูกยกระดับเป็นฉบับจริง ก็จะแชร์ไปยังทีมที่เกี่ยวข้อง ตรงนี้มีความไม่สมมาตรอยู่อย่างหนึ่ง คือไม่ให้ไฟล์ต้นฉบับ Markdown (.md) แก่ทีมอาร์ต แต่ส่งเฉพาะ html ที่เรนเดอร์แล้ว
เหตุผลเรียบง่าย ทีมอาร์ตรู้แค่ ผลลัพธ์ ของการตัดสินใจก็พอ "ช่วยปรับความกว้างของเกจคูลดาวน์ใหม่โดยอิงเกณฑ์ 0.6 วินาที" — บรรทัดเดียวนี้คือทั้งหมดที่พวกเขาต้องการ ในไฟล์ต้นฉบับ md มีระบบรหัสการตัดสินใจ การอ้างอิง atom ร่องรอยของข้อเสนอ 0.5 ที่ถูกตีตก และชื่อไฟล์ข้อมูลเหตุผลรวมอยู่ด้วย สิ่งเหล่านี้คือภาษาทำงานที่ฝ่ายออกแบบกับฝ่ายโค้ดใช้ร่วมกัน ไม่ใช่สิ่งที่ฝ่ายอาร์ตต้องมาเรียนรู้
หากให้ md ไปตรง ๆ ทีมอาร์ตจะต้องจ่ายต้นทุนสองอย่าง อย่างแรก เสียเวลาไปกับการตีความระบบสัญลักษณ์ที่ไม่เกี่ยวกับตน อย่างที่สอง อาจเข้าใจผิดว่าข้อมูลที่ยังไม่ได้ตรวจสอบหรือถูกตีตกเป็นการตัดสินใจ html กันทั้งสองอย่างนี้ได้ — เห็นเพียงผลการตัดสินใจที่เรนเดอร์ออกมาอย่างเรียบร้อย ส่วนสัญลักษณ์ภายในถูกกรองออกในขั้นตอนการบิลด์
เขียนเป็นหลักการได้ว่า ภาษาทำงาน (md) วนอยู่เฉพาะภายในสายงานที่ใช้ภาษานั้น และส่งออกไปข้างนอกเฉพาะผลลัพธ์ (html) เท่านั้น เป็นปรัชญาเดียวกับการแยกพื้นที่ทำงาน TF (ช่วง 95) ของดิบที่ใช้ภายในเก็บไว้ภายใน ส่วนที่ส่งออกไปข้างนอกคือผลลัพธ์ที่ดูดซับแล้วเท่านั้น
เพื่อให้กลไกการแยกและการดูดซับทำงานได้ ต้องมีหลักการดำเนินงานห้าข้อรองรับอยู่ข้างใต้ ขาดไปแม้ข้อเดียว TF ก็จะพังกลายเป็นแค่เวทีถกเถียง
[ไม่ได้บันทึกเหตุผล] ถูกทำเครื่องหมายอัตโนมัติในบันทึกเซสชันจริงก่อนหน้านี้ ก็เป็นส่วนต่อขยายของหลักการนี้เช่นกันเมื่อหลักการทั้งห้าถูกผูกเข้าด้วยกันและทำงาน พื้นที่ช่วง 95 ที่ถูกแยกออกมาก็จะไม่ใช่เวทีถกเถียง แต่กลายเป็นโรงงานตัดสินใจ
ขอประมวลกับดักและข้อกำหนดที่เกิดซ้ำในช่วงกลางของการดำเนินงาน TF เป็นต้นไป
| กับดัก | อาการ | ข้อกำหนด |
|---|---|---|
| แปรเป็นที่ประชุมเฉย ๆ | มีแต่แลกเปลี่ยนความเห็น ไม่มีการตัดสินใจ | บังคับช่องการตัดสินใจ N ช่องทุกการประชุม |
| ล่วงล้ำอำนาจ | TF เข้าแทรกการตัดสินใจของสายงานอื่น | ทำตารางอำนาจตัดสินใจให้ชัดเจน |
| สมาชิกรับภาระเกิน | เข้าร่วม TF ซ้อนกัน 5\~6 ตัวจนกัดกินงานหลัก | จำกัดเวลาเข้าร่วม TF รวมไม่เกิน 8 ชั่วโมงต่อสัปดาห์ |
| กลายเป็นถาวร | ประชุมเรื่องเดิมซ้ำ ๆ โดยไม่ยุบ | ประเมินซ้ำรายไตรมาส |
| การแยกรั่ว | ตัวเลขที่ยังไม่ได้ตรวจสอบในช่วง 95 ถูกอ้างอิงเสมือนเป็นฉบับจริง | ยกระดับสู่ฉบับจริงได้เฉพาะสรุปการตัดสินใจ 1 ฉบับ |
| ขาดการเชื่อมต่อภายนอก | ไม่แชร์การตัดสินใจออกไปภายนอก | ยกระดับเป็นฉบับจริง + ส่ง html |
การแยกรั่วเป็นสิ่งที่เงียบที่สุดและอันตรายที่สุด เมื่อข้อตกลงเรื่องหมายเลขโฟลเดอร์พังลง ทุกอย่างก็พังตามไปด้วย
ผมขอนำเฉพาะทิศทางและสัดส่วนมาจากบันทึกการดำเนินงานโปรเจกต์ A ของผู้เขียน ตัวเลขด้านล่างไม่ใช่ค่าสัมบูรณ์ แต่เป็นทิศทางการเปลี่ยนแปลงตอนดำเนินงานเทียบกับตอนไม่มี TF — รอบเวลาสัมบูรณ์แตกต่างกันไปตามขนาดทีมและรอบการบิลด์ (เป็นการสังเกตการณ์อิงสภาพแวดล้อมของผู้เขียน)
| รายการ | ไม่มี TF | มี TF | ทิศทาง |
|---|---|---|---|
| รอบของการตัดสินใจการต่อสู้ 1 รายการ | แยกตามสายงาน หลายสัปดาห์ | ระดับไม่กี่วัน | สั้นลง |
| ความขัดแย้งระหว่างสายงานหลังตัดสินใจ | หลายครั้งต่อไตรมาส | น้อยครั้งต่อไตรมาส | ลดลง |
| การเอสคาเลตขึ้นเกมไดเรกเตอร์ | หลายครั้งต่อสัปดาห์ | 1\~2 ครั้งต่อสัปดาห์ | ลดลง |
| การแชร์ข้อมูลระหว่างสายงาน | กระจัดกระจาย | ตรึงไว้ด้วยบันทึกประชุม·การยกระดับฉบับจริง | เป็นระบบ |
สิ่งที่ได้กลับคืนมามากที่สุดคือเวลาของเกมไดเรกเตอร์ เพราะ TF ดูดซับการตัดสินใจระหว่างสายงานไว้ภายในพื้นที่แยก ความขัดแย้งที่จะไหลขึ้นมาถึงโต๊ะของไดเรกเตอร์จึงลดลง สรุปแล้ว TF คือกลไกที่ดึง "ข้อตกลงระหว่างสายงานที่ไดเรกเตอร์เคยต้องไกล่เกลี่ยทีละเรื่อง" ลงมาจัดการในพื้นที่ทำงานเดียว
การประยุกต์นอกเกม หลักการที่ว่าให้ดูดซับเฉพาะการตัดสินใจเป็นฉบับจริงจากพื้นที่ทำงานที่ถูกแยกออกมานั้น ประยุกต์ใช้ได้ตรง ๆ กับทุกโปรเจกต์ที่ตัดข้ามแผนกแม้ไม่เกี่ยวกับเกม ตัวอย่างเช่น ลองนึกถึง TF ที่ฝ่ายการตลาด ฝ่ายกฎหมาย และฝ่ายขายมาร่วมกันหารือการปรับแก้ข้อกำหนดและเงื่อนไขฉบับใหม่ บันทึกการประชุม ความเห็นในการพิจารณา และร่างถ้อยคำที่ถูกตีตก ให้เก็บไว้ในโฟลเดอร์ชั่วคราวของไดรฟ์ที่แชร์กัน (พื้นที่แยกอย่าง
95_약관TF) แล้วพอ TF จบ ให้ยกระดับเฉพาะ최종_확정문구.docxเพียงฉบับเดียวขึ้นสู่ตู้เอกสารฉบับจริงภายในบริษัท ที่เหลือลดลงไปเก็บใน archive ทำเช่นนี้แล้ว เมื่ออีกหกเดือนต่อมามีคนมาถามว่า "เคยตัดสินใจกำหนดข้อนี้ไว้แบบนี้ทำไม" ก็จะกันอุบัติเหตุที่ร่างยังไม่ยืนยันแอบเข้ามาทำตัวเป็นฉบับจริงได้
setup
- สร้างพื้นที่แยก 95_BattleTF/ ใน SVN (หรือโฟลเดอร์) แล้วรวบรวมบันทึกการประชุมของหนึ่งไตรมาสไว้ในนั้น
- สร้าง 95_BattleTF/archive/ ไว้ล่วงหน้า (ที่ที่เป้าหมายการลดระดับจะไป)
prompt - แปะพรอมต์รอบที่ 1 ของบทนี้พร้อมกับบันทึกการประชุมฉบับเต็ม กฎหลัก: ① เฉพาะการตัดสินใจที่ยืนยันแล้ว ② หัวข้อเดียวกันเอาเฉพาะค่าสุดท้าย ③ ถ้าคลุมเครืออย่าทิ้ง แต่ให้แยกระบุ ④ ถ้าไม่มีเหตุผลให้ระบุชัด ⑤ อย่าแต่งขั้นตอนของบริษัทหรือชื่อทีมขึ้นมา
verify
- ดูการจัดประเภท "ยืนยันแล้ว" ในผลลัพธ์ทีละรายการ รายการที่มีเหตุผลเป็นแค่การตัดสินเชิงคุณภาพ ให้ดึงออกจาก "ยืนยันแล้ว" (บังคับใช้นโยบายยกระดับสู่ฉบับจริง)
- ตรวจว่าในประโยคข้อกำหนดที่ AI สร้างขึ้น (การโอน แทร็ก คำแนะนำ) มีขั้นตอนที่ไม่มีอยู่จริงแทรกอยู่หรือไม่ แล้วลบทิ้ง
- คัดลอกเฉพาะส่วน "การตัดสินใจที่ยืนยันแล้ว" ไปยัง docs ฉบับจริง ที่เหลือลดลงไปที่ archive/
สำหรับนักพัฒนาคนเดียวที่ทำงานลำพัง การแยกและการดูดซับก็ยังใช้ได้ตรง ๆ เพียงเปลี่ยน "TF" เป็น "หลายบทบาทในหัวของตัวเอง"
95_temp_결정/ ขึ้นมา แล้วระบายซิม โน้ต และข้อเสนอที่ถูกตีตกลงไปในนั้นให้หมด결정요약.md ฉบับเดียวมายังโฟลเดอร์งานหลัก แล้วลดโฟลเดอร์ชั่วคราวทั้งก้อนลงไปที่ archive/เมื่อมีพื้นที่แยก จะแยกแยะ "ค่านี้ยืนยันแล้วหรือยังอยู่ระหว่างทดลอง" ได้ด้วยตำแหน่งโฟลเดอร์เพียงอย่างเดียว แม้ทำงานคนเดียว นี่ก็เป็นวิธีที่ถูกที่สุดในการไม่ส่งต่อความสับสนแบบเดิมให้ตัวเองในอนาคต
เช้าวันอังคาร แชตภายในทีมดังขึ้นเกือบพร้อมกันสามครั้ง
อาร์ตลีด: "สีของเอฟเฟกต์การต่อสู้ตอนนี้โทนหม่นเกินไป จะปรับให้สดใสขึ้นได้หรือไม่"
QA ลีด: "มีเคสที่รางวัลเช็กชื่อกิลด์ถูกแจกซ้ำสองครั้ง แนบวิดีโอที่ทำให้เกิดซ้ำมาด้วยแล้ว"
ผู้รับผิดชอบฝั่งผู้จัดจำหน่าย: "ขอให้นำแนวทาง (guideline) สำหรับวัฒนธรรมอิสลามมาปรับใช้กับบิลด์เอเชียตะวันออกเฉียงใต้ด้วย ภายในก่อนการตรวจสอบของไตรมาสหน้า"
จำนวนตัวอักษรของข้อความทั้งสามใกล้เคียงกัน แต่ข้อความหนึ่งเป็นงานที่จบได้ใน 30 นาที อีกข้อความหนึ่งเป็นเหตุที่ต้องดึงตัวโค้ดลีดมาจัดการทันที และอีกข้อความหนึ่งเป็นกำหนดการภายนอกที่ต้องสอดเข้าไปในแผนระดับไตรมาส หากปฏิบัติต่อทั้งสามด้วยน้ำหนักเท่ากันเพียงเพราะตกลงมาในกล่องจดหมายเดียวกัน เราก็จะใช้เวลาครึ่งวันไปกับงาน 30 นาที ในขณะที่เหตุจริง ๆ ถูกปล่อยทิ้งไว้จนถึงเย็น
คำขอที่ส่งเข้ามาหานักออกแบบเกมมีเนื้อในที่แตกต่างกันพอ ๆ กับจำนวนสายงาน ปัญหาคือคำขอเหล่านั้นมาถึงในรูปแบบเดียวกันหมด นั่นคือ "ข้อความบรรทัดเดียว" บทนี้ว่าด้วยงานของการแยกบรรทัดเหล่านั้นออกเป็นสามแทร็กทันทีที่ได้รับ ในวินาทีที่แทร็กถูกแยก มันก็จะกำหนดว่าเราต้องหยุดทำอะไรตอนนี้และเลื่อนอะไรไว้ทีหลัง
นักออกแบบเกมไม่ได้สร้างทั้งโค้ด อาร์ต หรือเสียงด้วยตัวเอง เพียงเขียนสเปก ส่งต่อเจตนา และตรวจสอบผลลัพธ์เท่านั้น ทุกผลงานออกมาผ่านมือของสายงานอื่น ดังนั้นคุณภาพของการทำงานร่วมกันจึงกำหนดคุณภาพของผลงานออกแบบโดยตรง
ในโปรเจกต์ A ที่ผู้เขียนทำงานในฐานะ Design Director (โมบายล์มาก่อน MMORPG ทีมขนาดกลาง (10\~50 คน)) หากกางสายงานที่นักออกแบบเกมต้องทำงานร่วมด้วยเป็นประจำออกมา จะได้ดังนี้
นักออกแบบเกมเชื่อมโยงกับเจ็ดสายงานตั้งแต่ระดับรายวันไปจนถึงระดับไตรมาส เวลา 40\~60% ที่นักออกแบบเกมใช้ที่โต๊ะทำงานหมดไปกับการทำงานร่วมกันนี้ เท่ากับว่าเวลาที่ใช้กับงานหลัก (การออกแบบ) มีเพียงครึ่งหนึ่งที่เหลือ ถ้าเป็นเช่นนั้น การลดเวลาทำงานร่วมกันก็คือการเพิ่มเวลางานหลักนั่นเอง และสาเหตุที่กินเวลาทำงานร่วมกันมากที่สุดอยู่ที่การทุ่มพลังลงผิดที่เพราะจำแนกคำขอที่เข้ามาไม่ได้
ลองกลับไปดูข้อความสามอันก่อนหน้า ผิวเผินแล้วทั้งหมดคือ "ช่วย\~ให้หน่อย" แต่ภายในนั้นซ่อนลักษณะที่แตกต่างกันสามแบบ
เนื้อในสามแบบนี้ผู้เขียนเรียกแต่ละแบบด้วยคำเดียว align (การตกลงร่วมกัน), defect (ข้อบกพร่อง), schedule (กำหนดการ) งานของการผลักคำขอที่เข้ามาให้เข้าหนึ่งในสามนี้เสียก่อนนี้ ในโปรเจกต์ A เราตรึงเป็นกฎไว้เป็นเวิร์กโฟลว์ชื่อ request-triangulate ชื่อ triangulate (การหาตำแหน่งแบบสามเหลี่ยม) นั้นตั้งขึ้นจากความหมายว่าล้อมจุดหนึ่ง (คำขอ) ด้วยจุดอ้างอิงสามจุด (ลักษณะสายงาน·ความเร่งด่วน·การพึ่งพาภายนอก) เพื่อระบุตำแหน่งให้ชัดเจน
ขั้นตอนการจำแนกเป็นดังนี้
flowchart TD
A[คำขอจากภายนอกมาถึง 1 รายการ] --> B{ผูกอยู่กับเส้นตาย/
สัญญาภายนอกหรือไม่?}
B -- ใช่ --> S[Track-S: กำหนดการ schedule]
B -- ไม่ --> C{เป็นบั๊ก/ข้อบกพร่องที่
กระทบผู้ใช้หรือไม่?}
C -- ใช่ --> D[Track-D: ข้อบกพร่อง defect]
C -- ไม่ --> E{เป็นเรื่องการตกลงร่วมกัน
ด้านรสนิยม·เจตนาหรือไม่?}
E -- ใช่ --> F[Track-A: การตกลงร่วมกัน align]
E -- ไม่ --> G[พักไว้:
ขอข้อมูลเพิ่ม]
S --> S1[ผนวกเข้าโรดแมปรายไตรมาส
กันลีดไทม์ให้เพียงพอ]
D --> D1[ตัดสินลำดับความสำคัญ P0~P2
เชื่อมต่อโค้ดลีดทันที]
F --> F1[ส่งต่อแค่เจตนา
การแสดงออกมอบให้สายงานนั้น]
style S fill:#fde2c4,stroke:#c98a3a
style D fill:#f6c6c6,stroke:#c25151
style F fill:#c9e4d0,stroke:#4f9d6a
style G fill:#e0e0e0,stroke:#888
ลำดับ ของคำถามคือหัวใจ เหตุที่ถามการพึ่งพากำหนดการก่อนเป็นอันดับแรก เพราะงานที่มีเส้นตายภายนอกผูกอยู่ ลีดไทม์ย่อมมาก่อนการตัดสินใจภายใน หากจำแนกงานที่เหลือเวลาก่อนการตรวจสอบรายไตรมาส 3 สัปดาห์ว่า "ค่อยตกลงทีหลังก็ได้" พอตกลงเสร็จเส้นตายก็มาจ่ออยู่ตรงหน้าแล้ว เหตุที่วางข้อบกพร่องเป็นอันดับสอง เพราะงานที่กระทบผู้ใช้ไปแล้วย่อมมาก่อนการถกเรื่องรสนิยมเสมอ การตกลงร่วมกันมาเป็นอันดับสุดท้าย ต้องเป็นงานที่ไม่เร่งด่วน ไม่ผูกกับภายนอก และไม่ทำร้ายผู้ใช้ จึงจะ "ค่อย ๆ ตกลงกัน" ได้
หากทั้งสามคำถามตอบว่า "ไม่" นั่นไม่ใช่การจำแนกล้มเหลว แต่เป็น ข้อมูลไม่เพียงพอ เมื่อเป็นเช่นนั้น อย่าฝืนกำหนดแทร็ก แต่ให้ใส่ไว้ในที่พักและถามกลับ ประโยคหนึ่งอย่างเช่น "อันนี้ต้องเข้าบิลด์ถัดไปแน่ ๆ ไหม หรือแค่พิจารณาไว้ก็พอ?" มักจะช่วยกำหนดแทร็กให้เอง
หากทำการจำแนกนี้แค่ในหัว วันที่ยุ่งมันก็พังลง ดังนั้นผู้เขียนจึงใช้วิธีโยนชุดคำขอที่เข้ามาทั้งก้อนให้ AI รับการจำแนกขั้นต้น แล้วตรวจสอบและแก้ไขผลลัพธ์นั้น ด้านล่างคือการคัดลอกรอบหนึ่งที่รันจริงมาแทบทั้งหมด ไม่สรุปย่อ และเก็บทั้งส่วนที่ AI ผิดและส่วนที่ผู้เขียนปฏิเสธไว้ด้วย
ช่วยจำแนกคำขอจากภายนอกด้านล่างออกเป็นสามแทร็ก — Track-S (กำหนดการ: ผูกกับเส้นตาย·สัญญา·การตรวจสอบภายนอก),
Track-D (ข้อบกพร่อง: บั๊กที่กระทบผู้ใช้, ตัดสิน P0~P2), Track-A (การตกลงร่วมกัน: รสนิยม·เจตนา, จบด้วยการตกลง)
ดูตามลำดับ การพึ่งพาภายนอก → ผลกระทบต่อผู้ใช้ → รสนิยม/เจตนา แล้วยืนยันด้วยแทร็กที่ติดก่อน
ถ้าไม่เข้าทั้งสาม ให้พักไว้และให้คำถามที่จะถามกลับ 1 ข้อ
ผลลัพธ์ให้แยกตามคำขอเป็น | แทร็ก | เหตุผล (1 บรรทัด) | การกระทำถัดไป (1 บรรทัด) |
[รายการคำขอ]
1. อาร์ตลีด: สีเอฟเฟกต์การต่อสู้ดูหม่น จะปรับให้สดใสขึ้นได้ไหม?
2. QA ลีด: บั๊กแจกรางวัลเช็กชื่อกิลด์ซ้ำ มีวิดีโอที่ทำให้เกิดซ้ำ
3. ผู้จัดจำหน่าย: นำแนวทางวัฒนธรรมมาปรับใช้กับบิลด์เอเชียตะวันออกเฉียงใต้ ก่อนการตรวจสอบไตรมาสหน้า
4. ผู้กำกับเสียง: ช่วงต้นของ BGM บอสมาช้ากว่าจังหวะเริ่มการต่อสู้ 0.5 วินาที
5. ทีมปฏิบัติการ: ผู้ใช้สอบถามเข้ามาจำนวนมากว่า "ฟอนต์แชตกิลด์เล็กเกินไป"
| # | แทร็ก | เหตุผล | การกระทำถัดไป |
|---|------|------|-----------|
| 1 | Track-A | โทนสีเป็นเรื่องรสนิยม·เจตนา ไม่เกี่ยวกับภายนอก/บั๊ก | ส่งต่อแค่เจตนา (ความตึงเครียดของการต่อสู้) การตัดสินค่าความอิ่มตัวสีมอบให้อาร์ต |
| 2 | Track-D | รางวัลซ้ำ = กระทบทรัพยากรผู้ใช้โดยตรง ทำให้เกิดซ้ำได้ | ตัดสิน P0 เชื่อมต่อโค้ดลีดทันที พิจารณาแฮ็กฟิกซ์ |
| 3 | Track-S | ผูกกับเส้นตายภายนอกคือการตรวจสอบรายไตรมาส | ผนวกเข้าโรดแมปรายไตรมาส แตกย่อยรายการแนวทางแล้วประเมินลีดไทม์ |
| 4 | Track-A | จังหวะ BGM ดูเหมือนเป็นเรื่องรสนิยมด้านการกำกับ | ตกลงกับผู้กำกับเสียง |
| 5 | Track-D | ความชัดของฟอนต์เป็นข้อบกพร่องด้าน UI | ลงทะเบียนเป็นบั๊ก P2 |
ที่ข้อ 4 และข้อ 5 ผู้เขียนหยุด
ปฏิเสธข้อ 4 AI มองว่าความล่าช้าของช่วงต้น BGM เป็น "เรื่องรสนิยมด้านการกำกับ (Track-A)" แต่ "มาช้ากว่าจังหวะเริ่มการต่อสู้ 0.5 วินาที" ไม่ใช่รสนิยม แต่เป็น ข้อบกพร่องด้านการซิงค์ ไม่ใช่การเปลี่ยนตัวไฟล์เสียงเอง แต่มีความเป็นไปได้สูงว่าเป็นปัญหาที่จุดเวลาทริกเกอร์บน BT (Behavior Tree) กับ audio cue เหลื่อมกัน เรื่องนี้ไม่จบด้วยการตกลง แต่ต้องไปแตะข้อมูลจังหวะฝั่งแอนิเมชัน·โค้ด → แก้เป็น Track-D แต่เพราะไม่กระทบทรัพยากรผู้ใช้จึงเป็น P2
ลดข้อ 5 ลงเป็นพักไว้ AI ตัดสินทันทีว่า "ฟอนต์เล็ก" เป็นข้อบกพร่อง UI (Track-D) แต่เรื่องนี้แค่จากข้อความเดียวยังแยกไม่ออกว่าเป็นข้อบกพร่องหรือรสนิยม หากฟอนต์เรนเดอร์ตามสเปกการออกแบบแล้วแต่ "รู้สึกว่าเล็ก" นั่นใกล้เคียงกับการตกลงร่วมกัน (Track-A) แต่หากมันแตกออกมาเล็กกว่าสเปก นั่นคือข้อบกพร่อง (Track-D) → พักไว้ ถามกลับทีมปฏิบัติการ: "เป็นกรณีที่เห็นเล็กกว่าขนาดฟอนต์ตามสเปกจริง หรือเป็นความเห็นที่ขอให้เพิ่มขนาดสเปกเอง?"
คำสั่งเพิ่มเติมในพรอมต์ที่โยนซ้ำโดยสะท้อนสองรายการที่ปฏิเสธไปนั้นสั้น
ข้อ 4 ให้จำแนก '0.5 วินาทีช้ากว่าการเริ่มต่อสู้' ใหม่เป็นข้อบกพร่องด้านการซิงค์ (Track-D, P2)
ให้เพิ่มคำถาม 1 ข้อที่จะตรวจว่าจังหวะเหลื่อมกันที่ BT trigger หรือที่ audio cue
ข้อ 5 ให้จัดการเป็นพักไว้ และระบุคำถามที่ถามว่าเป็น 'การเรนเดอร์จริงเทียบกับสเปก' หรือไม่
ผลลัพธ์ที่ออกใหม่แก้ข้อ 4 เป็น Track-D / P2 / "ตรวจว่า audio cue offset ของโหนดเริ่มต่อสู้บน BT เป็น 0 หรือไม่ หรือว่าตัวคลิป BGM เองมีช่วงเงียบ 0.5 วินาทีรวมอยู่" และข้อ 5 เป็น พักไว้ / "ให้ทีมปฏิบัติการยืนยันอีกครั้งว่าเรนเดอร์เล็กกว่าสเปก vs ขอให้ปรับสเปกขึ้น" ส่งกลับมาแก้ถูกต้องตรงจุด ณ จุดนี้การจำแนกก็เสร็จสมบูรณ์
ตรงนี้สิ่งที่ AI ทำกับสิ่งที่คนทำแยกออกจากกันชัดเจน AI กระจายห้ารายการอย่างรวดเร็วในขั้นต้นเพื่อสร้าง ตารางที่ไม่มีช่องว่าง ให้ ส่วนคนจับสองรายการที่ เส้นแบ่งแทร็กละเอียดอ่อน (BGM ที่ดูเหมือนรสนิยมแต่เป็นข้อบกพร่องด้านการซิงค์ และฟอนต์ที่ดูเหมือนข้อบกพร่องแต่อาจเป็นรสนิยม) งานเติมห้าช่องให้ครบไม่ตกหล่นกับงานรู้ทันว่าในนั้นมีสองช่องที่ถูกเติมผิดเป็นความสามารถคนละอย่างกัน และบันทึกเซสชันจริงนี้มอบทั้งสองให้ฝ่ายที่ถนัดแต่ละอย่าง
เมื่อจำแนกเสร็จ แต่ละแทร็กก็เข้าสู่งานต่อเนื่องที่ต่างกันโดยสิ้นเชิง เริ่มจากตารางเดียวกันแต่ปลายทางต่างกัน
คำขอที่จำแนกเป็น Track-A (การตกลงร่วมกัน) จัดการด้วยหลัก "ส่งต่อแค่เจตนา การแสดงออกมอบหมายไป" คำตอบที่ผู้เขียนส่งกลับให้คำขอเรื่องสีของอาร์ตไม่ใช่ค่าความอิ่มตัวสี แต่เป็นเจตนา "การต่อสู้นี้เป็นเฟส 1 ของบอส ความตึงเครียดจึงเป็นหัวใจ อยากให้ความกดดันมาก่อนความสดใส ภายในกรอบนั้นค่าความอิ่มตัวสีขอมอบให้อาร์ตเป็นผู้ตัดสิน" วินาทีที่นักออกแบบเกมระบุค่าความอิ่มตัวสีเอง ความเป็นอิสระของอาร์ตก็ถูกตัดทอน และความรับผิดชอบต่อผลงานก็พร่าเลือนไปด้วย การรักษาเส้นแบ่งระหว่างเจตนากับการแสดงออกคือทั้งหมดของแทร็กการตกลงร่วมกัน
คำขอที่จำแนกเป็น Track-D (ข้อบกพร่อง) นำไปสู่การตัดสินลำดับความสำคัญและการเชื่อมต่อโค้ด รางวัลกิลด์ซ้ำ (P0) ส่งต่อให้โค้ดลีด ณ ตรงนั้น ส่วนการซิงค์ BGM (P2) ลงทะเบียนใน backlog พร้อมแนบคำถามสำหรับคาดเดาสาเหตุไว้ด้วย ในแทร็กข้อบกพร่อง งานของนักออกแบบเกมไม่ใช่ "การแก้" แต่คือ การจัดลำดับความสำคัญและให้อินพุตที่แม่นยำ เกณฑ์ที่แยก P0 จาก P2 คือ "ตอนนี้กระทบต่อทรัพยากร·ความคืบหน้าของผู้ใช้หรือไม่" รางวัลซ้ำเกี่ยวพันโดยตรงกับทรัพยากรจึงเป็น P0 ส่วนความล่าช้า 0.5 วินาทีของ BGM แม้น่ารำคาญแต่ไม่ขวางความคืบหน้าจึงเป็น P2
คำขอที่จำแนกเป็น Track-S (กำหนดการ) เข้าสู่โรดแมปรายไตรมาส แนวทางด้านวัฒนธรรมของผู้จัดจำหน่ายเป็นคำขอบรรทัดเดียว แต่จริง ๆ แล้วแตกย่อยออกเป็นหลายรายการ — การแสดงสัญลักษณ์ทางศาสนา ข้อห้ามด้านสี ทิศทางของข้อความ เครื่องแต่งกายตัวละคร หัวใจคือทันทีที่ได้รับ ให้ตอบว่า "จะพิจารณา" แล้วยกทั้งก้อนวางลงบนแผนรายไตรมาส งานที่มีเส้นตายภายนอกผูกอยู่ แม้ดูเล็ก แต่ลีดไทม์คือชีวิต เริ่มช้าเมื่อใดก็เกิดเหตุแน่นอน
หากเทียบสามแยกนี้ในภาพเดียวจะเป็นดังนี้
หากจำแนกแม่นยำ ห้าบรรทัดในกล่องจดหมายเดียวกันก็จะกระจายออกเป็นสามไลน์การจัดการที่ต่างกันอย่างเรียบร้อย หากจำแนกผิด ข้อบกพร่องจะถูกลากเข้าประชุมตกลงและกินเวลา หรืองานกำหนดการเริ่มช้าแล้วระเบิดตรงหน้าเส้นตาย
มีบางครั้งที่คำขอไม่ได้มาแค่หนึ่งสองรายการ แต่หลั่งไหลมาเป็นก้อนเดียว เช่นไม่กี่สัปดาห์ก่อนการตรวจสอบของผู้จัดจำหน่าย หรือช่วงที่ต้องรื้อระบบการต่อสู้ทั้งหมด เมื่อเป็นเช่นนี้ ให้แยกตัวงานเองออกไปไว้ในพื้นที่ทำงานชั่วคราวอย่าง 95_BattleTF แล้วพอเสร็จก็เลื่อนเฉพาะการตัดสินใจขึ้นเป็นฉบับจริง กลไกการแยก·ดูดกลืนนั้นและการดำเนินงาน "ส่งเฉพาะ html ให้ทีมอาร์ต (เรียนรู้ md เป็น 0)" ได้กล่าวไว้ทั้งหมดในบทก่อนหน้า 16.1
หากเสริมแค่บรรทัดเดียวในมุมมองการจำแนก (3-track) จะได้ดังนี้ การทำงานร่วมกันที่บานเป็นก้อนเดียวมักเป็นช่วงที่ประเด็น Track-S (กำหนดการ) แตกย่อยกระจายไปหลายสายงาน ดังนั้นเมื่อการจัดการแทร็กรายอันรับมือไม่ไหว ก็ย้ายไปใส่ในภาชนะที่สูงขึ้นอีกขั้นหนึ่งคือพื้นที่ทำงานที่แยกไว้ กล่าวคือถ้าการจำแนก 3-track คือทางเข้า การแยกด้วย TF ก็คือห้องที่รองรับก้อนใหญ่ที่ผ่านทางเข้านั้นมาแล้ว
| รูปแบบความล้มเหลว | วิธีแก้ |
|---|---|
| จัดการทุกคำขอด้วยน้ำหนักเท่ากัน | จำแนก 3-track ทันทีที่ได้รับ ถามเรื่องการพึ่งพาภายนอกก่อน |
| จำแนกงานกำหนดการผิดเป็นการตกลงร่วมกัน | ตรึงคำถามเส้นตายภายนอกไว้เป็นลำดับ 1 ในการตัดสิน |
| จัดการข้อบกพร่องด้านการซิงค์ที่ดูเหมือนรสนิยมเป็นการตกลงร่วมกัน | "จังหวะ/ค่าตัวเลขเหลื่อมกัน" ให้สงสัยว่าเป็นข้อบกพร่องก่อน |
| ตัดสินรสนิยมที่ดูเหมือนข้อบกพร่องว่าเป็นข้อบกพร่อง | ถามกลับว่า "เป็นการเรนเดอร์จริงเทียบกับสเปกหรือไม่" แล้วพักไว้ |
| ในแทร็กการตกลงร่วมกัน นักออกแบบเกมตัดสินถึงการแสดงออก | ส่งต่อแค่เจตนา การแสดงออกมอบให้สายงาน |
| เริ่มงานกำหนดการช้า | ผนวกเข้าโรดแมปรายไตรมาสทันที กันลีดไทม์ |
ครึ่งหนึ่งของตารางนี้เป็นความผิดพลาดในขั้นการจำแนก และอีกครึ่งหนึ่งเป็นความผิดพลาดในการจัดการหลังการจำแนก แม้จำแนกแม่นยำ แต่ถ้ามือที่ลงไปทำในแต่ละแทร็กผิด ผลก็หายไป (กับดักเรื่องการแยก TF·การเลื่อนเป็นฉบับจริง·สื่อกลาง ให้ดูตารางกับดักของ 16.1)
การประยุกต์นอกเกม ปัญหาที่คำขอบรรทัดเดียวถูกปฏิบัติด้วยน้ำหนักเท่ากันเพียงเพราะตกลงมาในกล่องจดหมายเดียวกัน ไม่ใช่เรื่องของเกม แต่เป็นชีวิตประจำวันของนักวางแผนบริการ·PM ทุกคน การจำแนกที่แยกคำขอที่เข้ามาออกเป็นสามแทร็ก "การตกลงร่วมกัน (รสนิยม·ทิศทาง)·ข้อบกพร่อง (บั๊กที่กระทบผู้ใช้)·กำหนดการ (เส้นตายภายนอก)" ทำงานได้เหมือนเดิมแม้เปลี่ยนโดเมน ยกตัวอย่างเช่น ถ้าในแชตของ PM บริการเว็บมี "ปรับสีปุ่มให้สว่างขึ้นอีกหน่อย (การตกลงร่วมกัน)", "ใบเสร็จการชำระเงินถูกส่งซ้ำ (ข้อบกพร่อง)", "เหลือ 3 สัปดาห์ก่อนเส้นตายปรับให้สอดคล้องกับกฎหมายคุ้มครองข้อมูลส่วนบุคคลฉบับแก้ไข (กำหนดการ)" ตกลงมาพร้อมกัน ก็ใส่ลงในแทร็กที่ติดก่อนตามลำดับ เส้นตายภายนอก→ผลกระทบต่อผู้ใช้→รสนิยม โดยจัดคนมาที่บั๊กการชำระเงินทันที ส่วนการแก้กฎหมายให้กันลีดไทม์ไว้ก่อนก็พอ
เส้นทางขั้นต่ำผ่านเว็บแชตบอต (ไม่ต้องใช้เทอร์มินัล) — หัวใจของบทนี้ไม่ใช่สคริปต์เวิร์กโฟลว์ แต่คือแนวคิดที่ว่า "แยกคำขอบรรทัดเดียวออกเป็นสามแทร็ก การตกลงร่วมกัน·ข้อบกพร่อง·กำหนดการ" แนวคิดนั้นทำซ้ำได้เหมือนเดิมแม้ใช้แค่เว็บแชตบอต (ChatGPT หรือ Claude เว็บ) โดยไม่ต้องมีโครงสร้างพื้นฐาน CLI·hook·atom สองขั้นด้านล่างคือเส้นทางหลัก
1. รวบรวมคำขอที่เข้ามาในวันนั้นมาทีละบรรทัดโดยไม่ต้องมีรูปแบบ จะดึงมาจากแชต·เมล·บันทึกที่ไหนก็ได้
2. วางพรอมต์ด้านล่างในช่องป้อนของเว็บแชตบอต แล้ววางรายการคำขอที่รวบรวมไว้ต่อท้ายลงไป นี่คือการทำการจำแนกขั้นต้นที่ request-triangulate เคยทำด้วยมือสักครั้ง
ช่วยจำแนกคำขอด้านล่างเป็น Track-A (การตกลงร่วมกัน)/Track-D (ข้อบกพร่อง)/Track-S (กำหนดการ)
ดูตามลำดับ เส้นตายภายนอก → บั๊กที่กระทบผู้ใช้ → รสนิยม·เจตนา แล้วยืนยันด้วยแทร็กที่ติดก่อน
ถ้าไม่เข้าทั้งสาม ให้พักไว้และให้คำถามที่จะถามกลับ 1 ข้อ ผลลัพธ์เป็น | แทร็ก | เหตุผล 1 บรรทัด | การกระทำถัดไป 1 บรรทัด |
[วางรายการคำขอ]
จากนั้นในตารางผลลัพธ์ คนตรวจสอบแค่สองช่องก็พอ — ถ้า "จังหวะ·ค่าตัวเลขเหลื่อมกัน" ถูกจำแนกเป็นการตกลงร่วมกัน ให้สงสัยข้อบกพร่องด้านการซิงค์ และถ้าความไม่พอใจเชิงสัมผัสอย่าง "\~เล็ก/ช้า" ถูกตัดสินเป็นข้อบกพร่อง ให้ถามกลับว่า "เป็นการเรนเดอร์จริงเทียบกับสเปกหรือไม่" แล้วลดลงเป็นพักไว้ ส่วนสคริปต์·เวิร์กโฟลว์ค่อยนำเข้ามาตอนที่การจำแนกนี้ติดมือจนชุดคำขอรายวันเริ่มหนักเกินรับไหว
setup. รวบรวมคำขอจากภายนอกที่เข้ามาไว้ที่เดียว (ช่อง·เอกสาร) เขียนนิยามของสามแทร็กไว้ทีละบรรทัด — การตกลงร่วมกัน (รสนิยม·เจตนา), ข้อบกพร่อง (บั๊กที่กระทบผู้ใช้), กำหนดการ (เส้นตายภายนอก)
prompt. โยนชุดคำขอที่รวบรวมไว้ให้ AI พร้อมตรึงลำดับการตัดสิน
ช่วยจำแนกคำขอด้านล่างเป็น Track-A (การตกลงร่วมกัน)/Track-D (ข้อบกพร่อง)/Track-S (กำหนดการ)
ดูตามลำดับ เส้นตายภายนอก → บั๊กที่กระทบผู้ใช้ → รสนิยม·เจตนา แล้วยืนยันด้วยแทร็กที่ติดก่อน
ถ้าไม่เข้าทั้งสาม ให้พักไว้และให้คำถามที่จะถามกลับ 1 ข้อ ผลลัพธ์เป็น | แทร็ก | เหตุผล 1 บรรทัด | การกระทำถัดไป 1 บรรทัด |
[วางรายการคำขอ]
verify. ตรวจสอบสองจุดในตารางผลลัพธ์ด้วยตัวเอง (1) ถ้า "จังหวะ·ค่าตัวเลขเหลื่อมกัน" ถูกจำแนกเป็นการตกลงร่วมกัน ให้สงสัยว่าเป็นข้อบกพร่องด้านการซิงค์หรือไม่ (2) ถ้าความไม่พอใจเชิงสัมผัสอย่าง "\~เล็ก/ช้า" ถูกตัดสินเป็นข้อบกพร่อง ให้ถามกลับว่า "เป็นการเรนเดอร์จริงเทียบกับสเปกหรือไม่" แล้วลดลงเป็นพักไว้ เพียงคนจับสองรายการที่เป็นเส้นแบ่ง ที่เหลือก็เชื่อถือได้
หากเป็นนักพัฒนาคนเดียวที่ไม่มีทั้งทีมและ TF ให้คงแทร็กไว้เหมือนเดิมแต่เปลี่ยนแค่เป้าหมาย รวบรวมรีวิวบนสโตร์ การแจ้งเข้ามาบน Discord บันทึกของผู้ทดสอบเบต้าไว้ในเอกสารเดียว แล้วจำแนกเป็นชุดด้วยพรอมต์ข้างต้นสัปดาห์ละครั้ง การตกลงร่วมกัน (รสนิยม) ให้ "รับไว้ถ้าไม่ขัดกับวิสัยทัศน์ของฉัน" ข้อบกพร่อง (บั๊ก) จัดการในสัปดาห์นั้น ส่วนกำหนดการ (การตรวจสอบของสโตร์·เส้นตายอีเวนต์) ให้ป้อนลงในปฏิทินพร้อมลีดไทม์ การดำเนินงานโฟลเดอร์ที่แยกไว้ในช่วงปรับปรุงเข้มข้น ให้ทำตามฉบับย่อสำหรับคนเดียวของ 16.1
ห้องประชุม 95_BattleTF บ่ายวันที่กำหนดให้รางวัลเช็กชื่อกิลด์เป็นทรัพยากร +5 ผมส่งการตัดสินใจเดียวกันนี้ออกไปสามทาง ช่องของทีมออกแบบเกมได้ markdown ระบุสเปก ทีมโปรแกรมได้คอลัมน์ข้อมูลหนึ่งบรรทัด ทีมอาร์ตได้ html หนึ่งหน้าจอ คำตอบกลับมาแทบจะพร้อมกันทั้งสามที่ หัวหน้าโปรแกรมถามว่า "จุดทริกเกอร์อยู่ตรงไหน" ผู้อำนวยการฝ่ายอาร์ตถามว่า "ตำแหน่งปุ่มเช็กชื่อตรงกับไกด์ 06_UI หรือเปล่า" ส่วนแอนิเมเตอร์ไม่พูดอะไรเลย เป็นการตัดสินใจเดียวกันแท้ ๆ แต่สิ่งที่ทั้งสามคนมองเห็นนั้นต่างกันโดยสิ้นเชิง
บทนี้คือบันทึกที่เปลี่ยน "การมองต่างกัน" นั้นจากอุบัติเหตุให้กลายเป็นการออกแบบ การห่อหุ้มหนึ่งการตัดสินใจให้ต่างกันตามแต่ละสายงาน — นั่นคือ framing
การตัดสินใจเรื่องรางวัลเช็กชื่อกิลด์เพียงเรื่องเดียวมีผู้รับสาร (audience) เกาะติดอยู่ถึงห้ากลุ่ม แต่ละกลุ่มแม้อ่านประโยคเดียวกันก็เลือกอ่านเฉพาะส่วนของตัวเองและปล่อยส่วนที่เหลือผ่านไป อุบัติเหตุเกิดขึ้นตรงจุดที่ถูกปล่อยผ่าน
| ผู้รับสาร | สิ่งที่ตั้งใจอ่าน | สิ่งที่ข้ามไปโดยสัญชาตญาณ |
|---|---|---|
| หัวหน้าโค้ด | คอลัมน์ข้อมูล·อินเทอร์เฟซ·จุดทริกเกอร์ | โทนสี·เนื้อเรื่อง·การกำกับ |
| ผู้อำนวยการฝ่ายอาร์ต | การวางหน้าจอ·คอมโพเนนต์·ไกด์สไตล์ | ความสมบูรณ์ของข้อมูล·ทริกเกอร์ |
| ผู้อำนวยการฝ่ายเสียง | ทริกเกอร์ของพฤติกรรม·บรรยากาศ·ความยาว | รายละเอียดข้อมูล |
| แอนิเมเตอร์ | การเคลื่อนไหว·จังหวะ·การเปลี่ยนสถานะ | โทนภาพ·ค่าตัวเลข |
| QA | เกณฑ์การยอมรับ·ความเสี่ยง·สถานการณ์ขอบ (edge) | รายละเอียดภายในของวิธีการพัฒนา |
ปัญหาไม่ได้อยู่ที่ปริมาณของข้อมูล แต่อยู่ที่วิธีการนำเสนอ หากวางสเปกหนาเล่มเดียวกันลงบนโต๊ะของทั้งห้าคนเหมือน ๆ กัน ทั้งห้าคนจะเปิดอ่านคนละหน้าและปิดทิ้งคนละหน้า framing ไม่ปล่อยให้การเปิดอ่านนี้ขึ้นอยู่กับความบังเอิญ แต่จัดวางมันอย่างจงใจ
ด้านล่างคือเมทริกซ์ framing ที่แสดงว่าการตัดสินใจเดียวเปลี่ยนรูปไปอย่างไรเมื่อข้ามเส้นแบ่งของแต่ละสายงาน
การห่อหุ้มต่างกันได้ตามผู้รับสารแต่ละกลุ่ม แต่ข้อเท็จจริงที่ไม่เปลี่ยน (ค่า·จุดเวลา·ขอบเขต) ซึ่งวางอยู่ตรงกลางนั้นต้องไม่สั่นคลอนในการห่อหุ้มแบบใดเลย ศิลปะของ framing ไม่ใช่ "การแสดงให้เห็นต่างกัน" แต่คือ "การแสดงให้เห็นต่างกันโดยที่ยังรักษาสิ่งเดียวกันไว้"
หากต้องปั้น framing ด้วยมือใหม่ทุกครั้งจะเป็นภาระมากจนสุดท้ายก็เลิกทำ ผมจึงใช้วิธีโยนการตัดสินใจหนึ่งเรื่องเข้าไปแล้วรับร่างการห่อหุ้มตามสายงานกลับมาในคราวเดียว ด้านล่างคือบันทึกการโต้ตอบจริงที่เกิดขึ้นเกี่ยวกับการตัดสินใจเรื่องรางวัลเช็กชื่อกิลด์ ผมคัดลอกมาตามเดิมโดยไม่ขัดเกลา
[ข้อเท็จจริงของการตัดสินใจ — กำหนดที่ 95_BattleTF]
- รางวัลเช็กชื่อกิลด์ = ทรัพยากรกิลด์ +5
- ทริกเกอร์: เมื่อสมาชิกกิลด์ล็อกอินครั้งแรกของวันนั้น 1 ครั้ง
- ขอบเขต: สมาชิกกิลด์ทุกคนของกิลด์นั้น ๆ แต่ละคน
- UI: แสดงปริมาณทรัพยากรรวมที่ส่วนบนของหน้าจอกิลด์ + ปุ่ม "เช็กชื่อ"
ช่วยห่อหุ้มการตัดสินใจเดียวนี้สำหรับสามสายงาน
สายออกแบบทำเป็น markdown ระบุเจตนา·กฎ·ผลกระทบต่อข้อมูล สายอาร์ตทำสเปกหน้าจอที่จะใส่ใน html
โดยเน้นการวาง·คอมโพเนนต์·การอ้างอิงไกด์สไตล์ (ห้ามกล่าวถึงข้อมูล·ทริกเกอร์)
สายโปรแกรมทำเป็นข้อมูลหนึ่งบรรทัด·อินเทอร์เฟซ·ทริกเกอร์·รายการตรวจสอบ
ทั้งสามรูปแบบให้ฝังค่าที่ไม่เปลี่ยน (+5, ล็อกอินครั้งแรก 1 ครั้ง, สมาชิกกิลด์ทั้งหมด) เหมือนกัน
ส่วนค่าที่ไม่รู้ห้ามกุขึ้นมา ให้ทำเครื่องหมาย [ต้องยืนยัน]
1) ออกแบบ → markdown
## รางวัลเช็กชื่อกิลด์
- เจตนา: แปลงการเข้าเล่นวันละ 1 ครั้งให้เป็นทรัพยากรความร่วมมือระดับกิลด์ เพื่อยกอัตราการคงอยู่ให้สูงขึ้น
- กฎ: เมื่อสมาชิกกิลด์ล็อกอินครั้งแรกของวัน ทรัพยากรกิลด์ +5 วันละ 1 ครั้ง สะสมระดับกิลด์
- ผลกระทบต่อข้อมูล: guild_resource_total เพิ่มขึ้น ต้องมอนิเตอร์เงินเฟ้อของทรัพยากร [ต้องยืนยัน: ยังไม่ได้กำหนดเพดาน]
2) อาร์ต → สเปกหน้าจอ (เนื้อหาที่จะใส่ใน html)
- แสดงปริมาณทรัพยากรรวมที่ส่วนบนของหน้าจอกิลด์ (progress_bar + badge ตัวเลข)
- ปุ่ม "เช็กชื่อ" ทางขวา การเปลี่ยนสถานะเมื่อกดให้เป็นไปตามกฎสถานะปุ่มในไกด์ 06_UI
- สไตล์: สอดคล้องกับไกด์ 06_UI สีเป็น [ต้องยืนยัน: ยังไม่ได้กำหนดโทเค็นสีธีมของกิลด์]
- (ค่าตัวเลขข้อมูล·ทริกเกอร์ถูกตัดออกจากรูปแบบนี้โดยตั้งใจ)
3) โปรแกรม → ข้อมูล/อินเทอร์เฟซ
- ข้อมูล: เพิ่มคอลัมน์ใน guild_table — guild_resource_total (int, default 0)
- อินเทอร์เฟซ: GuildResourceManager.add(guild_id, amount)
- ทริกเกอร์: เรียก add(guild_id, 5) 1 ครั้งจากฮุกอีเวนต์ล็อกอินครั้งแรกของวัน
- ตรวจสอบ: integrity_check_guild_resource (ห้ามค่าติดลบ / บล็อกการเรียกเกินวันละ 1 ครั้ง)
- ภาวะพร้อมกัน: เมื่อสมาชิกกิลด์คนเดียวกันล็อกอินพร้อมกันหลายอุปกรณ์อาจบวกซ้ำ [ต้องยืนยัน]
ผมไม่ได้ใช้ผลลัพธ์ดิบตามเดิม ผมจับได้สามจุด
[ต้องยืนยัน] สองรายการ (เพดานทรัพยากร, การบวกซ้ำหลายอุปกรณ์) ในรูปแบบของโปรแกรมคือช่องโหว่ที่ไม่มีในข้อเท็จจริงของการตัดสินใจ การที่ AI ไม่กุขึ้นมาแต่ทำเครื่องหมายไว้นั้นเป็นวิจารณญาณที่ถูกต้อง แต่นี่คือรูโหว่ของตัวการตัดสินใจเองซึ่งอยู่ก่อนหน้า framing จึงต้องส่งกลับไปที่ 95_BattleTFช่วยแก้รูปแบบของอาร์ต:
- เอาข้อความอ้างอิงเอกสารภายนอกอย่าง "เป็นไปตามไกด์ 06_UI" ออกทั้งหมด
- บรรยายความต่างทางสายตาของ 3 สถานะ คือ กดปุ่ม/รอ/เสร็จ ลงในสเปกหน้าจอโดยตรง
- ภายใต้สมมติฐานว่าทีมอาร์ตทำงานโดยดูแค่หน้านี้หน้าเดียว ให้จบในตัวเองโดยไม่ต้องกระโดดไปเอกสารอื่น
ส่วน [ต้องยืนยัน] 2 รายการในรูปแบบของโปรแกรม ให้ดึงออกจากผลงาน
แล้วแยกไว้เป็นบล็อก "รายการที่ต้องกำหนดยืนยันใหม่ที่ 95_BattleTF" ไว้บนสุดแทน
ด้วยการปฏิเสธ·ร้องขอใหม่เพียงครั้งเดียวนี้ ผลงานก็กลายเป็นรูปแบบที่ทั้งสามสายงานหยิบไปใช้ที่โต๊ะของตัวเองได้ทันที AI ปั้นร่างการห่อหุ้มสามชุดและทำเครื่องหมายช่องโหว่ให้ด้วย แต่ว่าจะตัดอะไรออกจากการห่อหุ้มแบบไหน — การเอาการอ้างอิงภายนอกออกจากรูปแบบของอาร์ต และการยกรายการที่ยังไม่กำหนดออกจากรูปแบบของโปรแกรม — กรรไกรนั้นยังคงเหลืออยู่ในมือของผมจนถึงที่สุด วิจารณญาณหลักของ framing อยู่ที่ฝั่งการตัดออก ไม่ใช่ฝั่งการใส่เข้า
วิธีการแยกออกไปตามว่าจะวางการห่อหุ้มไว้ที่ไหน จะเลือกใช้แบบใดในสามแบบนั้นตัดสินด้วยขนาดของสเปกและกำลังในการดำเนินงาน
(1) สรุปตามผู้รับสารภายในเอกสารเดียว ต่อท้ายเนื้อหาหลักด้วยส่วนสรุปตามสายงาน ทั้งห้าคนใช้ไฟล์เดียวกันแต่ต่างคนต่างอ่านเฉพาะหัวข้อของตัวเอง
## สรุปตามผู้รับสาร
### โค้ด (การพัฒนา)
- ข้อมูล: guild_table.guild_resource_total (int)
- อินเทอร์เฟซ: GuildResourceManager.add(guild_id, amount)
- ทริกเกอร์: ล็อกอินครั้งแรกของวัน 1 ครั้ง
- ตรวจสอบ: integrity_check_guild_resource
### อาร์ต (สายตา)
- หน้าจอ: ปริมาณทรัพยากรรวมส่วนบนของกิลด์ + ปุ่มเช็กชื่อ
- คอมโพเนนต์: progress_bar, badge, button (3 สถานะ)
- ลำดับความสำคัญ: มายล์สโตนรอบนี้
### QA (การตรวจสอบ)
- เกณฑ์การยอมรับ: หลังเช็กชื่อ ทรัพยากรกิลด์สะท้อน +5, บล็อกการเกินวันละ 1 ครั้ง
- ความเสี่ยง: เงินเฟ้อของทรัพยากร, การบวกซ้ำหลายอุปกรณ์
(2) ผลงานแยกตามผู้รับสาร จากเนื้อหาหลัก 1 ชุด แยกไฟล์ตามสายงานออกจากกัน การที่ 95_BattleTF ส่งเฉพาะ html ให้ทีมอาร์ตและไม่ส่ง md คือรูปแบบจริงของวิธีนี้ — แม้เป็นการตัดสินใจเดียวกัน แต่ตัวสื่อกลางก็ต่างกันตามแต่ละสายงาน
spec_guild_attendance.md — เนื้อหาหลักฝ่ายออกแบบ (บริบททั้งหมด)
guild_screen_v3.html — อาร์ต (html เท่านั้น, เรียนรู้ md 0)
guild_table 1 row + add() — โปรแกรม (ข้อมูล/อินเทอร์เฟซ)
qa_guild_attendance.md — QA (เกณฑ์การยอมรับ·ความเสี่ยง)
เหมาะกับสเปกที่มีปริมาณมาก และตัวสื่อกลางเข้าสู่เครื่องมือของแต่ละสายงานได้ทันที แต่ในทางกลับกัน เมื่อการตัดสินใจหนึ่งเปลี่ยน ต้องแก้ผลงานหลายชิ้นไปพร้อมกันจึงมีภาระในการดำเนินงานสูง
(3) กราฟ Wikilink ใส่เฉพาะจุดเริ่มต้นตามสายงานเป็นลิงก์ไว้ในเนื้อหาหลัก แต่ละคนสำรวจตามกิ่งของตัวเอง
[[spec_guild_attendance]]
├── [[code_guild_table]]
├── [[ui_guild_screen_v3]]
└── [[qa_guild_attendance]]
ต้นทุนและจุดคืนทุนของสามวิธีเป็นดังนี้ ในบรรดาค่าด้านล่าง "ผล" เป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ให้เชื่อถือเฉพาะทิศทางและสัดส่วนเชิงเปรียบเทียบเท่านั้น
| วิธีการ | ต้นทุน | จุดคืนทุน |
|---|---|---|
| (1) สรุปตามผู้รับสาร | ปริมาณเนื้อหาหลัก +30% โดยประมาณ | คืนทุนทันทีในเกือบทุกสเปก |
| (2) ผลงานแยก | ดำเนินงานผลงาน N ชุด | คืนทุนเฉพาะเมื่อปริมาณมากและตัวสื่อกลางต่างกันตามสายงาน |
| (3) กราฟ Wikilink | ลงทุนล่วงหน้าด้านโครงสร้างกราฟ | คืนทุนเมื่อสเปกสะสมจนตัวกราฟเองกลายเป็นสินทรัพย์ |
สเปกส่วนใหญ่เหมาะกับ (1) มันมีต้นทุนน้อยที่สุดและคืนทุนเร็วที่สุด (2) ใช้เฉพาะจุดที่ตัวสื่อกลางแยกออกจากกันอยู่แล้ว เช่น html ของอาร์ต ส่วน (3) เปิดใช้เมื่อสเปกสะสมมากพอจนกราฟลิงก์ให้คุณค่าในการสำรวจ
หากนิยามผู้รับสารใหม่ทุกสเปก framing ก็ต้องปั้นใหม่ทุกครั้ง ผมจึงตรึงรหัสไว้
| รหัสผู้รับสาร | ขอบเขต |
|---|---|
| code | โค้ด·ระบบ·ข้อมูล |
| art | อาร์ต·สายตา·UI |
| sound | เสียง·ออดิโอ |
| anim | แอนิเมชัน·โมชัน |
| qa | QA·การตรวจสอบ |
ห้ากลุ่มนี้คือมาตรฐานการดำเนินงานภายใน ผู้รับสารภายนอกอย่างฝ่ายเอาต์ซอร์ส·ฝ่ายกฎหมายจะจัดการแยกต่างหากนอกมาตรฐานนี้ การตรึงไว้ที่ห้ากลุ่มทำให้เมื่อมอบ framing ให้ LLM ไม่ต้องเขียนนิยามผู้รับสารใหม่ทุกครั้ง และจับผู้รับสารที่ตกหล่นได้ด้วยเช็กลิสต์
หากเขียนสรุปห้าสายงานด้วยมือทุกสเปก สุดท้ายก็จะเลิกเขียน ผมจึงผูกขั้นตอนไว้ดังนี้
flowchart LR
A[นักออกแบบเกม: เขียนข้อเท็จจริงของการตัดสินใจ] --> B[LLM: ร่างการห่อหุ้ม 5 ผู้รับสาร]
B --> C[นักออกแบบเกม: ตัดสินปฏิเสธ/เสริม/คงไว้]
C --> D{พบช่องโหว่?}
D -- ใช่ --> E[ส่งกลับไปกำหนดยืนยันใหม่ที่ 95_BattleTF]
D -- ไม่ --> F[สเปกขั้นสุดท้าย + framing ตามสายงาน]
E --> A
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class A,C,D human;
class B ai;
class F pass;
นักออกแบบเกมเขียนเฉพาะข้อเท็จจริงของการตัดสินใจ แล้ว LLM สร้างร่างการห่อหุ้มห้ารูปแบบ นักออกแบบเกมตัดสินปฏิเสธ·เสริม·คงไว้ เมื่อมีช่องโหว่ ([ต้องยืนยัน]) ออกมา จะไม่จัดการที่ framing แต่ส่งกลับไปยังขั้นตอนการตัดสินใจ — เพราะ framing ไม่ใช่เครื่องมืออุดรูโหว่ของการตัดสินใจ แต่เป็นเครื่องมือถ่ายทอดการตัดสินใจที่กำหนดไว้แล้ว
ในวงจรนี้มีกับดักที่เหยียบซ้ำ ๆ อยู่สี่ข้อ ผมวางไว้พร้อมแนวทางแก้
| กับดัก | อาการ | แนวทางแก้ |
|---|---|---|
| ข้อมูลซ้ำซ้อน | เนื้อหาเดียวกันซ้ำในเนื้อหาหลัก·สรุป จนเป็นภาระการดำเนินงาน | เนื้อหาหลัก 1 ครั้ง สรุปเฉพาะรายการที่ต่าง |
| ข้อมูลตกหล่น | ค่าที่สำคัญต่อสายงานหนึ่งหายไปทั้งก้อน | ตรวจการตกหล่นด้วยเช็กลิสต์ผู้รับสาร 5 กลุ่มที่ตรึงไว้ |
| มองข้ามเนื้อหาหลัก | ดูแค่สรุปแล้วปล่อยบริบทในเนื้อหาหลักผ่านไป | ระบุท้ายสรุปว่า "เหตุผลอยู่ในเนื้อหาหลัก" |
| สื่อกลางสับสน | ส่ง md ให้อาร์ตจนกลายเป็นภาระการเรียนรู้ | ตรึงหลักการสื่อกลางตามสายงาน (อาร์ต=html) |
การทำงานอัตโนมัติลดภาระการเขียนลงเหลือราว 5 นาทีต่อสเปก แต่วิจารณญาณในการปฏิเสธ·เสริม·คงไว้นั้นไม่ได้ถูกทำให้อัตโนมัติตามไปด้วย วิจารณญาณนั้นคือที่ทางของมนุษย์
ต่อไปนี้คือค่าที่เปรียบเทียบก่อน·หลังการนำ framing มาใช้ในโปรเจกต์ A ที่ผู้เขียนดำเนินงานอยู่ ค่าสัมบูรณ์เป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) สิ่งที่ควรเชื่อถือคือทิศทางของการเปลี่ยนแปลงและสัดส่วนเชิงเปรียบเทียบ
| รายการ | ไม่มี framing | ใช้ framing | ทิศทาง |
|---|---|---|---|
| อุบัติเหตุการตีความตามสายงาน | 15\~20 ครั้งต่อไตรมาส | 3\~5 ครั้งต่อไตรมาส | ลดลงมาก |
| เวลาที่ผู้รับสารใช้อ่านสเปก | 15\~30 นาที | 5\~10 นาที (เฉพาะหัวข้อตัวเอง) | ลดลง |
| การตัดสินใจ → เริ่มงาน | 1\~2 วัน | 4\~8 ชั่วโมง | สั้นลง |
| ความขัดแย้งในการตีความระหว่างสายงาน | 8\~12 ครั้งต่อไตรมาส | 2\~3 ครั้งต่อไตรมาส | ลดลง |
| เวลาในการเขียนสเปก | 1\~2 ชั่วโมง | 1.5\~2.5 ชั่วโมง (LLM ช่วย) | เพิ่มขึ้นเล็กน้อย |
ตัวการเขียนสเปกเองยาวขึ้นเล็กน้อย เพราะต้องเพิ่มการห่อหุ้มตามสายงานเข้าไป แต่หลังจากนั้นวงจรงานของแต่ละสายงานสั้นลง เวลาทั้งหมดตั้งแต่การตัดสินใจจนถึงเริ่มงานจึงลดลง การแลกเปลี่ยน (trade-off) นี้คือเหตุผลหลักของการนำ framing มาใช้ หากเป็นทีมที่รู้สึกว่าการตรวจสอบโดย LLM ช่วยเป็นภาระ การทำให้สรุป 5 ผู้รับสารแบบเขียนมือในวิธี (1) ตั้งหลักก่อนแล้วค่อยเพิ่มการทำงานอัตโนมัติเข้าไปคือลำดับที่ปลอดภัย
การประยุกต์นอกเกม framing ที่ห่อหุ้มหนึ่งการตัดสินใจให้ต่างกันตามผู้รับสารแต่ละกลุ่มแต่ยังรักษาข้อเท็จจริงที่ไม่เปลี่ยน (ค่า·จุดเวลา·ขอบเขต) ไว้ทุกที่นั้น ไม่ใช่เรื่องของเกมเท่านั้น แต่ใช้ได้ตรง ๆ กับการประกาศ·การสื่อสารการรีลีสของทุกองค์กร ตัวอย่างเช่น หากตัดสินใจเรื่อง "ปรับค่าสมาชิกรายเดือนเป็น 9,900 วอนตั้งแต่วันที่ 1 กรกฎาคม" หนึ่งเรื่อง ทีมพัฒนาจะได้รูปแบบที่เป็นข้อมูลอย่างคอลัมน์ตารางการชำระเงิน·จุดเวลาที่มีผล ทีมดีไซน์จะได้แบนเนอร์แจ้งข่าวหนึ่งหน้าจอ ส่วนทีมดูแลลูกค้าจะได้สคริปต์ตอบคำถามที่คาดว่าจะมี การห่อหุ้มต่างกันทั้งสาม แต่ถ้าสามตัวเลข "9,900 วอน·วันที่ 1 กรกฎาคม·สมาชิกใหม่และเดิมทั้งหมด" คลาดเคลื่อนในการห่อหุ้มแบบใด ข้อพิพาทกับลูกค้าก็จะปะทุขึ้นในวินาทีนั้นทันที
setup
prompt
[ข้อเท็จจริงของการตัดสินใจ]
(เขียนค่า·จุดเวลา·ขอบเขต บรรทัดละหนึ่ง)
ช่วยห่อหุ้มการตัดสินใจนี้สำหรับสายงานที่เกี่ยวข้องในบรรดา code·art·sound·anim·qa
ในแต่ละการห่อหุ้มให้ตัดข้อมูลที่สายงานนั้นไม่สนใจออก แต่ฝังค่าที่ไม่เปลี่ยน (ค่า·จุดเวลา·ขอบเขต) เหมือนกันในทุกรูปแบบ
รูปแบบของอาร์ตให้จบในตัวเองด้วยหน้าเดียวนั้นโดยไม่ต้องอ้างอิงเอกสารอื่น ค่าที่ไม่รู้ห้ามกุขึ้นมา ให้ทำเครื่องหมาย [ต้องยืนยัน]
verify
[ต้องยืนยัน] ให้ส่งกลับไปยังขั้นตอนการตัดสินใจ (TF แบบ 95_BattleTF) ไม่ใช่ framingฉบับย่อสำหรับคนเดียว
หากทำงานคนเดียว ให้ลดผู้รับสารเหลือสองกลุ่ม — "ตัวเราในอนาคต" (การพัฒนา) และ "ผู้ตรวจสอบ" (QA) เขียนข้อเท็จจริงของการตัดสินใจหนึ่งบรรทัด แล้วขอ LLM ว่า "ช่วยแบ่งอันนี้เป็นสองชุด คือ บันทึกการพัฒนาและเช็กลิสต์การตรวจสอบ" จากนั้นแค่ตรวจทานว่าค่าตัวเลขหลักในสองชุดตรงกันหรือไม่ก็พอ แม้ผู้รับสารจะมีเพียงสองกลุ่ม โครงสร้างของ framing ที่ว่า "ห่อหุ้มการตัดสินใจเดียวกันให้ต่างกันแต่รักษาค่าที่ไม่เปลี่ยนไว้" ก็ยังทำงานเหมือนเดิม
ผ่านไป 5 นาทีหลังจบการประชุม บนไวต์บอร์ดในห้องประชุมยังมีตัวอักษรหลงเหลืออยู่ "การตรวจจับการโดนโจมตีในการต่อสู้ ให้ฝั่งไคลเอนต์สะท้อนผลก่อน แต่ลำดับความสำคัญของการตรวจสอบฝั่งเซิร์ฟเวอร์ไว้สปรินต์ถัดไป" นี่คือข้อสรุปที่คนห้าคนใช้เวลา 30 นาทีกว่าจะมาถึง ทุกคนพยักหน้าเห็นด้วย และมีบางคนถ่ายรูปเก็บไว้
3 สัปดาห์ต่อมา คนห้าคนเดิมกลับมารวมตัวกันในห้องประชุมเดิมอีกครั้ง บรรทัดแรกของรายการวาระเขียนไว้ว่า "การตรวจจับการโดนโจมตีในการต่อสู้ — ไคลเอนต์สะท้อนก่อน vs ตรวจสอบฝั่งเซิร์ฟเวอร์ ต้องตัดสินใจ" ไม่มีใครจำข้อสรุปเมื่อ 3 สัปดาห์ก่อนได้เลย รูปถ่ายไวต์บอร์ดอยู่ที่ไหนสักแห่งในคลังภาพของใครคนหนึ่ง และคนคนนั้นวันนี้ลาพักร้อน ต้องใช้เวลาอีก 30 นาที คราวนี้ข้อสรุปออกมาตรงข้ามกับเดิม
นี่คือเหตุผลทั้งหมดว่าทำไมบันทึกการประชุมจึงเป็นจุดที่เจ็บปวดที่สุด ในการประชุมมีการตัดสินใจเกิดขึ้น แต่การตัดสินใจนั้นกลับไหลต่อ (propagate) ไปไม่ถึงการประชุมถัดไป เอกสารถัดไป หรือบิลด์ถัดไป การตัดสินใจเกิดขึ้นแล้วแต่ไม่ถูกส่งต่อ บทนี้คือเรื่องราวของการเชื่อมห่วงโซ่ที่ขาดนั้นกลับเข้าด้วยกันด้วยข้อมูล
ระบบ RnD ส่วนตัวที่ผู้เขียนดูแลอยู่แตกออกเป็นเอกสาร 17 ฉบับ ทั้งมาตรฐานการตั้งชื่อ atom, การทำแผนผังความสัมพันธ์อัตโนมัติ, คู่มือการแมป Layer, โครงสร้างการฉีด JIT และอื่น ๆ ในจำนวนนั้น เอกสารฉบับเดียวที่ดูดเวลาไปมากที่สุดคือแผนการปรับปรุงบันทึกการประชุม น้ำหนักของมันใหญ่ถึงขั้นเทียบได้กับอีก 16 ฉบับที่เหลือรวมกัน
ตอนแรกผมก็ฉงน บันทึกการประชุมน่ะหรือ ก็แค่จดตามที่ได้ยินก็จบไม่ใช่หรือ แต่พอลองวัดดูจริง ๆ กลับพบว่าตำแหน่งที่เจ็บปวดไม่ได้อยู่ที่การเขียนบันทึกการประชุม แต่อยู่ที่หลังบันทึกการประชุมต่างหาก ในการประชุมมีการตัดสินใจเกิดขึ้นอย่างชัดเจน ปัญหาคือการตัดสินใจนั้นเป็นความรับผิดชอบของใคร อาศัยเหตุผลอะไร กำหนดเส้นตายเมื่อไหร่ และนำไปสู่อะไรต่อ ทั้งหมดนี้ระเหยหายไปทันทีที่ก้าวออกจากประตูห้องประชุม
ถ้าวาดความขาดตอนนี้ออกมาเป็นภาพหนึ่งภาพ ก็จะได้แบบนี้
flowchart LR
M[ประชุม
ถกกัน 30 นาที] --> D[เกิดการตัดสินใจ
ทุกคนเห็นด้วย]
D -.ขาดตอน.-> X[ถูกนำกลับมาเสนอ
ในการประชุมครั้งหน้า]
D -.ขาดตอน.-> Y[ไม่ถูกสะท้อนลงเอกสาร]
D -.ขาดตอน.-> Z[ตกหล่นไปจากบิลด์]
X --> M
style D fill:#ffe6cc,stroke:#d79b00
style X fill:#f8cecc,stroke:#b85450
style Y fill:#f8cecc,stroke:#b85450
style Z fill:#f8cecc,stroke:#b85450
เส้นประคือการส่งต่อที่ขาดตอน การตัดสินใจ (สีส้ม) เกิดขึ้นแล้ว แต่ไม่ไหลไปทางไหนเลยใน 3 แขนง (สีแดง) การตัดสินใจที่ไหลต่อไปไม่ได้จะวกกลับมาเป็นการประชุมในอีก 3 สัปดาห์ ลูปที่ลูกศรโค้งขึ้นไปวนกลับมาเป็นการประชุมอีกครั้งนั่นแหละคือตัวต้นเหตุของความเจ็บปวด
เมื่อการส่งต่อขาดตอน สี่สิ่งจะเกิดขึ้นพร้อมกัน
ประวัติของการตัดสินใจหายไป คำถาม "ทำไมถึงตัดสินใจแบบนั้น" จะได้คำตอบว่า "จำไม่ได้แล้ว นัดประชุมใหม่กันเถอะ" การประชุมซ้ำ ๆ เพิ่มขึ้น วาระเดิมถูกนำกลับมาเสนอใหม่ทุกไตรมาส คนที่เพิ่งเข้ามาใหม่จับบริบทไม่ได้ เพราะการสะสมของการตัดสินใจมองไม่เห็น จึงต้องอธิบายแบบตัวต่อตัวทุกครั้ง และการช่วยเหลือของ AI ก็ไร้ผล เพราะไม่มีบริบท คำตอบจึงหยุดอยู่แค่ทฤษฎีทั่วไป ถ้าบันทึกการประชุมกระจัดกระจาย ก็ไม่สามารถป้อนข้อมูล "ทีมของเราเคยตัดสินวาระนี้ไว้อย่างไร" ให้ AI ได้ และ AI ก็จะคืนค่าเฉลี่ยจากอินเทอร์เน็ตกลับมา
ทั้งสี่อย่างนี้มาจากรากเดียวกัน เพราะการตัดสินใจถูกจัดการในฐานะบันทึกย่อ ไม่ใช่ในฐานะข้อมูล บันทึกย่อระเหย แต่ข้อมูลไหล
ตรงนี้ต้องพลิกมุมมองสักครั้ง ถ้ามองบันทึกการประชุมเป็น "ผลผลิตของการประชุม" จดตามที่ได้ยินแล้วเก็บไว้ก็ถือว่าจบภารกิจ บันทึกการประชุมที่เก็บไว้ก็เหมือนกระดาษโน้ตบนโต๊ะ วันนั้นยังมองเห็น แต่สัปดาห์ถัดมาก็ไม่รู้ว่าไปอยู่ที่ไหนแล้ว
ถ้ามองบันทึกการประชุมเป็น "ฐานข้อมูลการตัดสินใจ" มันจะกลายเป็นงานที่ต่างออกไปอย่างสิ้นเชิง ตัวสินทรัพย์ไม่ใช่บันทึกการประชุมในตัวมันเอง แต่เป็นการตัดสินใจที่สกัดออกมาจากบันทึกการประชุม และการตัดสินใจนั้นต้องค้นหาได้ อ้างอิงได้ และส่งต่อได้ บันทึกการประชุมเป็นเพียงสายแร่ที่ใช้ขุดการตัดสินใจขึ้นมาเท่านั้น
การบังคับให้เกิดการพลิกมุมมองนี้ด้วยโค้ดคือแก่นของส่วนที่ 17 ทั้งหมด ในระบบของผู้เขียนมี atom หนึ่งตัวที่ค้ำจุนการพลิกมุมมองนี้ ชื่อว่า decision_summary_not_clickup_mirror ถ้าแปลออกมาก็คือหลักการที่ว่า "สรุปการตัดสินใจในบันทึกการประชุมไม่ใช่กระจกสะท้อนของ ClickUp (ตัวติดตามงาน, task tracker)"
เหตุที่ atom ตัวนี้จำเป็นนั้นแทงเข้าที่หัวใจของความเจ็บปวดพอดี มีหลายทีมที่แค่ย้ายช่องการตัดสินใจในบันทึกการประชุมไปจดลงบอร์ดงานเฉย ๆ พอทำแบบนั้น "สิ่งที่ต้องทำ" ก็ยังอยู่ แต่ "ทำไมถึงตัดสินใจแบบนั้น (เหตุผล)" หายไป ตัวติดตามงานบรรจุสิ่งที่จะต้องทำ แต่ไม่บรรจุเหตุผลที่ตัดสินใจแบบนั้น นี่แหละคือเหตุผลที่แท้จริงว่าทำไมการประชุมจึงวนซ้ำในอีก 3 สัปดาห์ สิ่งที่ต้องทำถูกปิดไปแล้ว แต่เพราะไม่มีเหตุผล พอมีใครถามว่า "เอ๊ะ แต่ทำไมเราถึงตกลงทำแบบนี้กันนะ" ก็ไม่มีใครตอบได้ ด้วยเหตุนี้ สรุปการตัดสินใจจึงต้องไม่กลายเป็นกระจกสะท้อนของตัวติดตามงาน แต่ต้องเป็นสินทรัพย์อิสระที่อุ้มเหตุผล (rationale) ไว้ในตัว ชื่อของ atom เองคือเส้นห้ามเส้นนี้
ความต่างระหว่างการตัดสินใจที่ส่งต่อได้กับการตัดสินใจที่ระเหยหายอยู่ที่โครงสร้าง การตัดสินใจที่ระเหยคือประโยคเดียวว่า "ให้ไคลเอนต์สะท้อนก่อน" ส่วนการตัดสินใจที่ส่งต่อได้ถูกแยกย่อยออกเป็นสี่ฟิลด์
ในบรรดาสี่ฟิลด์ ฟิลด์ที่สำคัญที่สุดคือ owner ถ้าการตัดสินใจไม่มีผู้รับผิดชอบ การตัดสินใจนั้นก็ไม่ใช่งานของใคร และการตัดสินใจที่ไม่ใช่งานของใครก็จะไม่ถูกส่งต่อไปสู่การลงมือทำ ด้วยเหตุนี้ ไปป์ไลน์การสกัดของผู้เขียนจึงไม่ปล่อยผ่านเฉย ๆ เมื่อ owner ว่าง แต่จะแจ้งอย่างชัดเจนเป็น [MISSING] มันดึงข้อเท็จจริงที่ว่าเส้นความรับผิดชอบว่างเปล่าขึ้นมาไว้บนพื้นผิว
rationale คือที่อยู่ของหลักการ decision_summary_not_clickup_mirror ที่กล่าวไปก่อนหน้า ถ้าไม่มีเหตุผล การประชุมก็จะวนซ้ำในอีก 3 สัปดาห์ follow_up คือสะพานที่ทำให้การตัดสินใจไหลต่อไปสู่การลงมือทำจริง ถ้าฟิลด์นี้ว่าง การตัดสินใจก็จะคงอยู่ในฐานะการตัดสินใจเท่านั้นและไปไม่ถึงบิลด์
จะให้คนกรอกสี่ฟิลด์นี้ด้วยมือทุกครั้งก็ทำได้ แต่ถ้าทำแบบนั้นแรงบังคับจะอ่อน ระบบของผู้เขียนใช้ไปป์ไลน์ที่สกัดการตัดสินใจจากบันทึกการประชุมโดยอัตโนมัติและแจ้งฟิลด์ที่ขาดหายไป สามสคริปต์เชื่อมต่อกันแบบอนุกรม
flowchart TD
R[บันทึกการประชุม .md
รูปแบบมาตรฐาน] --> L[meeting_lint.py
ตรวจสอบรูปแบบ]
L -->|ผ่าน| P[decision_parser.py
สกัด 4 ฟิลด์]
L -->|ตีกลับ| R
P -->|ไม่มี owner| MISS["แจ้ง [MISSING]"]
P -->|ครบ 4 ฟิลด์| PEND[pending atom
รอตรวจสอบ 1 สัปดาห์]
PEND --> PROM[promote.py
เลื่อนขั้นสัปดาห์ละครั้ง]
PROM --> ATOM[atom อย่างเป็นทางการ
+ ลงทะเบียนใน JIT manifest]
style L fill:#dae8fc,stroke:#6c8ebf
style P fill:#d5e8d4,stroke:#82b366
style PROM fill:#e1d5e7,stroke:#9673a6
style MISS fill:#f8cecc,stroke:#b85450
ขั้นแรก meeting_lint.py ตรวจสอบว่าบันทึกการประชุมเป็นไปตามรูปแบบมาตรฐานหรือไม่ มี frontmatter หรือไม่ ช่องทั้งสี่ (วาระ/การตัดสินใจ/แอ็กชัน/การประชุมครั้งหน้า) ถูกกรอกหรือยัง บันทึกการประชุมที่รูปแบบเสียจะถูกตีกลับตรงนี้และส่งคืนให้ผู้เขียนบันทึก เนื่องจากตัวพาร์สอัตโนมัติประมวลผลได้เฉพาะอินพุตที่บังคับรูปแบบไว้แล้วเท่านั้น lint นี้จึงทำหน้าที่เป็นด่านทางเข้า (entry gate) ของทั้งไปป์ไลน์
ขั้นที่สอง decision_parser.py คือหัวใจ มันอ่านช่องการตัดสินใจแล้วแยกย่อยออกเป็นสี่ฟิลด์ (decision/owner/rationale/follow_up) ตรงนี้ถ้าหา owner ไม่เจอ ก็จะไม่ทิ้งการตัดสินใจนั้นไป แต่จะแจ้งเป็น [MISSING] เพราะการปล่อยให้การตัดสินใจที่ไม่มีผู้รับผิดชอบผ่านไปเงียบ ๆ นั้นอันตรายที่สุด
ขั้นที่สามคือการตัดสินใจที่สกัดออกมาจะยังไม่กลายเป็นสินทรัพย์อย่างเป็นทางการในทันที แต่จะอยู่ในสถานะ pending รอเป็นเวลา 1 สัปดาห์ ช่วงเวลาตรวจสอบนี้คือด่านที่ย้อนกลับได้ ถ้าภายใน 1 สัปดาห์พบว่า "อันนี้ไม่ใช่การตัดสินใจ แต่เป็นแค่การถกกัน" หรือ "เหตุผลผิด" ก็จะทิ้งไป จากนั้น promote.py จะย้ายเฉพาะการตัดสินใจที่รอดมาจากการรีวิวสัปดาห์ละครั้งไปยังโฟลเดอร์ atom อย่างเป็นทางการ และลงทะเบียนใน JIT manifest การตัดสินใจที่ลงทะเบียนแล้วจะถูกฉีดเข้าสู่งานที่เกี่ยวข้องโดยอัตโนมัติตั้งแต่เซสชันถัดไป เมื่อนั้นการตัดสินใจจึงเริ่มไหล
เส้นแบ่งระหว่างย้อนกลับได้กับย้อนกลับไม่ได้อยู่ตรงนี้ จนถึงการทิ้ง pending ยังย้อนกลับได้ แต่เมื่อ promote เสร็จและการตัดสินใจถูกส่งต่อไปยังเอกสารอื่น ชีตข้อมูล หรือบิลด์ ตั้งแต่นั้นเป็นต้นไปจะย้อนกลับไม่ได้ เพราะการรับรู้ของสมาชิกทีมเปลี่ยนไปและการตัดสินใจที่ขึ้นต่อกันก็สะสมทับลงบนนั้น ด้วยเหตุนี้การตรวจสอบทั้งหมดจึงต้องเสร็จสิ้นก่อน promote กล่าวคือภายในช่วง pending ที่ยังย้อนกลับได้
ถ้าพูดแบบนามธรรมอย่างเดียวก็จะไม่เข้าใจซึ้ง ผมจึงคัดลอกบันทึกที่ลองนำบันทึกการประชุมที่เสียหายหนึ่งฉบับใส่เข้าไปในไปป์ไลน์มาไว้ตามเดิม อินพุตคือบันทึกการประชุมที่รูปแบบพังไปครึ่งหนึ่ง
อินพุต — 2026-06-02-battle.md (รูปแบบไม่สมบูรณ์)
---
type: meeting_note
date: 2026-06-02
category: battle
---
## วาระ
1. ตำแหน่งการตรวจจับการโดนโจมตี (ไคลเอนต์ vs เซิร์ฟเวอร์)
## การตัดสินใจ
- ตกลงให้ไคลเอนต์สะท้อนการตรวจจับการโดนโจมตีก่อน เสียงส่วนใหญ่
เห็นว่าความรู้สึกตอบสนองสำคัญกว่าการวิ่งไป-กลับเซิร์ฟเวอร์ ยอมรับความเสี่ยงจากการโกง
- การตรวจสอบฝั่งเซิร์ฟเวอร์ให้แยกเป็นงานต่างหากในสปรินต์ถัดไป
## รายการแอ็กชัน
- [ ] ออกแบบการตรวจสอบฝั่งเซิร์ฟเวอร์
บันทึกการประชุมฉบับนี้จดการตัดสินใจไว้ก็จริง แต่ไม่มี owner อยู่ตรงไหนเลย ใครรับผิดชอบการตรวจสอบฝั่งเซิร์ฟเวอร์ก็ว่างอยู่ ช่องกำหนดการประชุมครั้งหน้าก็ขาดหายไปทั้งช่อง ผมนำอันนี้ใส่เข้าไปในตัวพาร์สและดูว่าอะไรออกมา
พรอมต์ (พรอมต์ช่วยเหลือ LLM แบบเต็มที่แนบไว้กับ decision_parser.py)
อ่านช่อง "## การตัดสินใจ" ของบันทึกการประชุมด้านล่าง แล้วสกัด 4 ฟิลด์
ออกมาในแต่ละการตัดสินใจ: decision, owner, rationale, follow_up
- ถ้าระบุ owner จากเนื้อหาไม่ได้ ต้องเขียนเป็น "[MISSING]" เสมอ
ห้ามเดาเพื่อเติมให้เต็ม
- rationale ให้อ้างเฉพาะเหตุผลที่เขียนไว้ในเนื้อหา ถ้าไม่มีให้ใส่ "[MISSING]"
- follow_up ให้เชื่อมกับรายการแอ็กชันหรือการกล่าวถึงสปรินต์ถัดไป
ส่งออกเป็น JSON array เท่านั้น
[วางเนื้อหาบันทึกการประชุม]
ผลลัพธ์ดิบจาก Claude (ตามเดิม ไม่ย่อ)
[
{
"decision": "ให้ไคลเอนต์สะท้อนการตรวจจับการโดนโจมตีก่อน",
"owner": "[MISSING]",
"rationale": "ความรู้สึกตอบสนองสำคัญกว่าการวิ่งไป-กลับเซิร์ฟเวอร์ ยอมรับความเสี่ยงจากการโกง",
"follow_up": "ดำเนินการตรวจสอบฝั่งเซิร์ฟเวอร์เป็นงานต่างหากในสปรินต์ถัดไป"
},
{
"decision": "แยกการตรวจสอบฝั่งเซิร์ฟเวอร์เป็นงานต่างหากในสปรินต์ถัดไป",
"owner": "[MISSING]",
"rationale": "[MISSING]",
"follow_up": "เชื่อมกับรายการแอ็กชัน 'ออกแบบการตรวจสอบฝั่งเซิร์ฟเวอร์' (ยังไม่ได้กำหนดผู้รับผิดชอบ)"
}
]
ผมหยุดตรงนี้และตรวจดูผลลัพธ์ การตัดสินใจทั้งสองรายการถูกจับได้ว่า owner เป็น [MISSING] ถูกต้อง — เพราะไม่มีผู้รับผิดชอบอยู่ตรงไหนในบันทึกการประชุมเลย การที่มันไม่เดาเพื่อเติมให้เต็มจึงถูกต้องแล้ว rationale ของการตัดสินใจรายการที่สองก็เป็น [MISSING] เช่นกัน ซึ่งในเนื้อหาก็ไม่มีเหตุผลของการตัดสินใจรายการที่สองอยู่จริง ๆ นั่นหมายความว่าตัวพาร์สไม่ได้กุขึ้น จึงน่าเชื่อถือ
แต่มีจุดหนึ่งที่กำกวม follow_up ของการตัดสินใจรายการแรกคือ "การตรวจสอบฝั่งเซิร์ฟเวอร์ไปสปรินต์ถัดไป" ซึ่งอันนี้เนื้อหาทับซ้อนกับการตัดสินใจรายการที่สอง ตัวพาร์สมองเส้นแบ่งระหว่างสองการตัดสินใจเบลอไปเล็กน้อย นี่คือจุดที่คนต้องเข้าไปแทรกแซง
การใช้สิทธิ์ยับยั้งของคน + การร้องขอใหม่
follow_up ของการตัดสินใจรายการแรกซ้ำกับเนื้อหาของการตัดสินใจรายการที่สอง
ให้บีบ follow_up ของรายการแรกให้แคบลงเหลือ "สร้างงานตรวจสอบฝั่งเซิร์ฟเวอร์"
และโยนรายละเอียดเฉพาะของการตรวจสอบฝั่งเซิร์ฟเวอร์ไปไว้ที่การตัดสินใจรายการที่สอง
และเนื่องจากการตัดสินใจทั้งสองรายการมี owner เป็น [MISSING]
ให้แนบบรรทัดเตือนว่าต้องยืนยัน owner ก่อนนำบันทึกการประชุมฉบับนี้ขึ้น pending
ไว้บนสุดของผลลัพธ์
ผลลัพธ์ใหม่จาก Claude (ดิบ)
⚠️ คำเตือน: การตัดสินใจทั้ง 2 รายการมี owner [MISSING] ก่อนเลื่อนขั้นขึ้น pending
ต้องร้องขอให้ผู้ดำเนินการประชุมยืนยัน owner
[
{
"decision": "ให้ไคลเอนต์สะท้อนการตรวจจับการโดนโจมตีก่อน",
"owner": "[MISSING]",
"rationale": "เน้นความรู้สึกตอบสนองก่อน ยอมรับความเสี่ยงจากการโกง",
"follow_up": "สร้างงานตรวจสอบฝั่งเซิร์ฟเวอร์ (รายละเอียดดูที่การตัดสินใจ #2)"
},
{
"decision": "แยกการตรวจสอบฝั่งเซิร์ฟเวอร์เป็นงานต่างหากในสปรินต์ถัดไป",
"owner": "[MISSING]",
"rationale": "[MISSING]",
"follow_up": "เชื่อมกับรายการแอ็กชัน 'ออกแบบการตรวจสอบฝั่งเซิร์ฟเวอร์'"
}
]
สิ่งที่บันทึกเซสชันนี้แสดงให้เห็นคือแก่นของส่วนที่ 17 ตัวพาร์สขุดการตัดสินใจขึ้นมาได้ แต่ไม่ได้ปกปิดข้อเท็จจริงที่ว่าเส้นความรับผิดชอบที่เรียกว่า owner นั้นว่างเปล่า [MISSING] ถูกประทับลงไปสองครั้ง และนั่นกลายเป็นสัญญาณกลับไปยังผู้ดำเนินการประชุมว่า "ให้ยืนยัน owner" การตัดสินใจจะถูกส่งต่อไปสู่การลงมือทำได้ก็ต่อเมื่อมีผู้รับผิดชอบ และถ้าไม่มีผู้รับผิดชอบ ระบบก็จะดันมันขึ้นมาบนพื้นผิว บันทึกการประชุมที่เสียหายหนึ่งฉบับจะผ่านด่านนี้ไปได้ก็ต่อเมื่อคนเข้าไปเติม owner เท่านั้น ปมแรกของการส่งต่อถูกผูกขึ้นตรงนี้
อนึ่ง ⚠️ ในผลลัพธ์ข้างต้นเป็นเพียงบรรทัดคำเตือนบนคอนโซลเท่านั้น ไม่ใช่ส่วนหนึ่งของรูปแบบเนื้อหา ตัวบันทึกการประชุมเองยังคงเป็นมาร์กดาวน์ 4 ช่องที่สะอาดเรียบร้อยอยู่เช่นเดิม
เดิมทีส่วนที่ 17 ออกแบบไว้ 6 บท (แรงจูงใจ·การสกัด·หมวดหมู่·คำบรรยายภาพ·การซิงค์·การช่วยเหลือของ AI) แล้วยุบรวมเหลือ 4 บท เพราะคำบรรยายภาพและการซิงค์เป็นกิ่งก้านของบันทึกการประชุม และการมัดความเจ็บปวดที่ใหญ่ที่สุดอย่าง "การส่งต่อการตัดสินใจ" ไว้ในบทเดียวแล้วตั้งไว้ข้างหน้านั้นถูกต้องกว่า ผมดึงความเจ็บปวดที่ใหญ่ที่สุด (การส่งต่อ ประชุม → ตัดสินใจ → ลงมือทำ) ขึ้นมาไว้ที่ §17.1 แล้ววางไปป์ไลน์ที่แก้ความเจ็บปวดนั้น (meeting_lint → decision_parser → promote) ตามมาทีหลัง
มุมมองที่จัดการบันทึกการประชุมในฐานะฐานข้อมูล โครงสร้างที่แตกการตัดสินใจออกเป็น 4 ฟิลด์ ด่านที่แจ้ง [MISSING] เมื่อ owner ว่าง และการตรวจสอบแบบย้อนกลับได้ของ pending 1 สัปดาห์ — สี่อย่างนี้เชื่อมการส่งต่อที่ขาดตอนกลับเข้าด้วยกัน ความเจ็บปวดที่ว่าการตัดสินใจเกิดขึ้นแล้วแต่ไม่ไหลต่อ จะคลายลงได้ถ้าทำการตัดสินใจให้อยู่ในรูปที่ไหลได้ตั้งแต่แรก
การประยุกต์นอกเกม ความเจ็บปวดที่ว่า "ในการประชุมมีการตัดสินใจเกิดขึ้นแล้ว แต่การตัดสินใจนั้นไหลต่อไปยังการประชุมครั้งหน้าไม่ได้" ไม่ได้วนซ้ำเฉพาะในการพัฒนาเกม แต่เกิดขึ้นทุกสัปดาห์ในห้องประชุมของทุกที่ทำงาน วิธีที่จดการตัดสินใจไม่ใช่เป็นบันทึกย่อหนึ่งประโยค แต่แตกเป็นสี่ฟิลด์ (อะไร·ใคร·ทำไม·การกระทำถัดไป) และดึงขึ้นมาบนพื้นผิวเป็น
[MISSING]เมื่อผู้รับผิดชอบว่างเปล่านั้น ย้ายไปใช้กับการประชุมแบบใดก็ได้ตามเดิม ยกตัวอย่างเช่น ถ้าในการประชุมรายสัปดาห์ของฝ่ายการตลาดตกลงกันว่า "แคมเปญหน้าจะเน้นไปที่อินสตาแกรม" ก็ให้แนบ owner (ใครเป็นคนดำเนินการ), rationale (ทำไมต้องอินสตาแกรม — อ้างอัตราการแปลง (conversion) ของไตรมาสก่อน), follow_up (จัดทำร่างงบประมาณ) เข้าไปด้วย แล้วในอีก 3 สัปดาห์ คำถามที่ว่า "ตกลงใครรับไปทำนะ" ก็จะไม่มีวันเกิดขึ้นอีกเลย
เส้นทางต่ำสุดด้วยแชตบอตบนเว็บ (ไม่ต้องใช้เทอร์มินัล) — แก่นของบทนี้ไม่ใช่สคริปต์ แต่คือแนวคิดที่ว่า "แตกการตัดสินใจออกเป็น 4 ฟิลด์เพื่อให้มันไหล" แนวคิดนั้นสามารถทำซ้ำได้ตามเดิมแม้ไม่มีโครงสร้าง CLI·hook·atom เพียงใช้แชตบอตบนเว็บ (ChatGPT หรือ Claude เว็บ) อย่างเดียว สามขั้นตอนด้านล่างคือเส้นทางหลัก
1. พอประชุมจบ ให้คัดลอกบันทึกการประชุม (หรือโน้ตการประชุม) มาตามเดิม ไม่มีรูปแบบก็ได้
2. วางพรอมต์ด้านล่างในช่องป้อนข้อความของแชตบอตบนเว็บ แล้ววางบันทึกการประชุมไว้ใต้มัน (ตรงตำแหน่ง [เนื้อหาบันทึกการประชุม]) นี่คือการทำสิ่งที่ decision_parser.py เคยทำด้วยมือสักครั้งหนึ่ง
จากบันทึกการประชุมด้านล่าง ให้ดึง 4 ฟิลด์ออกมาเป็นตารางในแต่ละการตัดสินใจ:
decision(อะไร), owner(ใครรับผิดชอบ), rationale(ทำไม), follow_up(การกระทำถัดไป)
- ถ้าระบุ owner ไม่ได้ ต้องใส่ "[MISSING]" เสมอ ห้ามเดา
- rationale ให้ใช้เฉพาะเหตุผลที่เขียนไว้ในเนื้อหา ถ้าไม่มีให้ใส่ "[MISSING]"
[เนื้อหาบันทึกการประชุม]
3. ในตารางผลลัพธ์ ให้นำช่องที่ประทับ [MISSING] ไปสอบถามผู้ดำเนินการประชุมเพื่อเติมให้เต็ม ถ้านำตารางที่สมบูรณ์แล้วไปแปะสะสมเรียงตามวันที่ไว้ในเอกสารหนึ่งฉบับ เช่น decisions.md เอกสารฉบับนั้นก็คือฐานข้อมูลการตัดสินใจในตัว การค้นหาใช้การค้นหาภายในเอกสาร (Ctrl+F) ก็เพียงพอ สคริปต์·atom·JIT ค่อยนำมาใช้เมื่อนิสัยนี้สะสมจนการค้นหาเริ่มหนักหน่วงเท่านั้น
setup (เวอร์ชันโครงสร้าง — หลังจากเส้นทางต่ำสุดข้างต้นคล่องมือแล้ว)
- กำหนดรูปแบบมาตรฐานของบันทึกการประชุม 1 แบบ frontmatter (type/date/category) + 4 ช่อง (วาระ/การตัดสินใจ/แอ็กชัน/การประชุมครั้งหน้า)
- วางสคริปต์ 3 ตัว ได้แก่ meeting_lint.py สำหรับตรวจสอบรูปแบบ, decision_parser.py สำหรับสกัดการตัดสินใจ, promote.py สำหรับเลื่อนขั้น (ตอนแรกแค่ lint กับ parser ก็เพียงพอ)
- ระบุ 4 ฟิลด์ของการตัดสินใจให้ชัดเจน: decision, owner, rationale, follow_up ใส่กฎที่บังคับ [MISSING] เมื่อ owner ว่างลงไปใน parser
prompt (พรอมต์ช่วยเหลือ LLM ที่แนบกับ decision_parser)
อ่านช่อง "## การตัดสินใจ" ของบันทึกการประชุมด้านล่าง แล้วสกัด 4 ฟิลด์ออกมาในแต่ละการตัดสินใจ:
decision, owner, rationale, follow_up
- ถ้าระบุ owner ไม่ได้ ต้องใส่ "[MISSING]" เสมอ ห้ามเดา
- rationale ให้อ้างเฉพาะเหตุผลที่เขียนไว้ในเนื้อหา ถ้าไม่มีให้ใส่ "[MISSING]"
- follow_up ให้เชื่อมกับรายการแอ็กชัน·การกล่าวถึงสปรินต์ถัดไป
ส่งออกเป็น JSON array เท่านั้น
[เนื้อหาบันทึกการประชุม]
verify
- ตรวจสอบว่าทุกการตัดสินใจในผลลัพธ์มี owner กรอกครบหรือไม่ ถ้ามี [MISSING] แม้แต่ตัวเดียว ให้นำขึ้น pending หลังจากร้องขอให้ผู้ดำเนินการประชุมยืนยัน owner แล้วเท่านั้น
- ดูว่า rationale ไม่ได้กุเหตุผลที่ไม่มีในเนื้อหาขึ้นมา (ถ้าไม่มีต้องเป็น [MISSING] จึงจะปกติ)
- หลังวางไว้ใน pending 1 สัปดาห์แล้ว ให้เลื่อนขั้นเฉพาะการตัดสินใจที่รอดมาจากการรีวิวสัปดาห์ละครั้งไปเป็น atom อย่างเป็นทางการด้วย promote.py
ถ้าสคริปต์ 3 ตัวเป็นภาระ ให้ลดทอนแบบนี้ รวมรูปแบบบันทึกการประชุมให้เหลือ 4 ช่องเท่านั้น พอประชุมจบก็ดึงเฉพาะช่องการตัดสินใจออกมาแล้วรันด้วยพรอมต์ข้างต้นกับ LLM สักครั้ง เฉพาะการตัดสินใจที่ owner ออกมาเป็น [MISSING] ให้กรอกผู้รับผิดชอบลงไปตรงนั้นทันที แค่ทำเท่านี้โดยไม่ต้องอัตโนมัติก็ปิดการขาดตอนของการส่งต่อที่พบบ่อยที่สุดอย่างหนึ่งคือ "การตัดสินใจไม่มีผู้รับผิดชอบ" ได้แล้ว lint·promote ค่อยเพิ่มเข้ามาเมื่อบันทึกการประชุมสะสมจนต้องการการค้นหา
เช้าวันพุธ พอเข้าออฟฟิศก็มีแจ้งเตือนเด้งขึ้นในแชตภายในทีม "สัปดาห์ที่แล้วเราตกลงกันว่าจะเพิ่มช่องอินเวนทอรีเป็น 30 ช่องใช่ไหม แล้วใครรับหน้าที่แก้ชีตข้อมูลกัน" ในเทรดไม่มีใครตอบได้ บันทึกการประชุมมีอยู่แน่นอน อยู่ในโฟลเดอร์สักที่ พอเปิดดูก็เต็มไปด้วยวาระและการอภิปรายแน่นขนัด แต่ "สรุปแล้วตัดสินใจอะไร และใครรับผิดชอบ" กลับละลายอยู่ระหว่างบรรทัด สุดท้ายในการประชุมครั้งถัดไปก็ต้องหยิบวาระเดิมมาเริ่มใหม่ตั้งแต่ต้น
บทนี้เป็นเรื่องราวของเครื่องจักรที่อุดช่องว่างสามวันนั้น เมื่อบันทึกการประชุมหนึ่งฉบับเข้าสู่ระบบ มันจะผ่านการตรวจรูปแบบ สกัดสี่ฟิลด์ของการตัดสินใจออกมา การตัดสินใจที่ไม่มีเจ้าของจะถูกติดป้าย [MISSING] สร้างไฟล์ตัวเลือกขึ้นมา และหนึ่งสัปดาห์ต่อมาผ่านการตรวจสอบโดยมนุษย์จนกลายเป็นสินทรัพย์ที่ถูกฉีดเข้าระบบอัตโนมัติ มือคนแตะแค่สองจุดที่ปลายทั้งสองข้างเท่านั้น คือทางเข้าที่เขียนบันทึกการประชุม กับทางออกที่ตรวจสอบสัปดาห์ละครั้ง
ก่อนอื่นมาดูภาพรวมทั้งหมดในหน้าเดียว แต่ละกล่องสี่เหลี่ยมคือสคริปต์เล็ก ๆ หรือการตัดสินใจของมนุษย์ กล่องที่ต้องย้ายด้วยมือมีแค่สองกล่อง ที่เหลือไหลไปโดยอัตโนมัติ
flowchart TD
A["ดำเนินการประชุม"] --> B["เขียนบันทึกการประชุมตามรูปแบบมาตรฐาน
(มนุษย์)"]
B --> C{"meeting_lint.py
ตรวจรูปแบบ"}
C -->|ละเมิด| B
C -->|ผ่าน| D["decision_parser.py
สกัด 4 ฟิลด์ของการตัดสินใจ"]
D --> E{"มี owner ไหม?"}
E -->|ไม่มี| F["แจ้ง [MISSING]
ขอกำหนดเจ้าของ"]
F --> B
E -->|มี| G["สร้างไฟล์ตัวเลือก pending atom"]
G --> H{"รีวิวสัปดาห์ละครั้ง
(มนุษย์)"}
H -->|เลื่อนขั้น| I["promote.py
atom ทางการ + ลงทะเบียน JIT"]
H -->|ทิ้ง| J["เก็บประวัติการทิ้ง"]
H -->|พักไว้| G
I --> K["ฉีดอัตโนมัติตั้งแต่เซสชันถัดไป"]
style B fill:#e8f0ff,stroke:#3366cc
style H fill:#e8f0ff,stroke:#3366cc
style F fill:#fff0e8,stroke:#cc6633
style J fill:#f0f0f0,stroke:#999999
มีเพียงสองกล่องสีฟ้า (เขียนบันทึกการประชุม·รีวิวสัปดาห์ละครั้ง) เท่านั้นที่เป็นมนุษย์ ที่เหลือเป็นสคริปต์ กล่องสีส้ม (แจ้ง [MISSING]) คือจุดที่การตรวจอัตโนมัติเรียกมนุษย์กลับมาอีกครั้ง หากการตัดสินใจไม่มีเจ้าของ ไปป์ไลน์จะไม่ได้หยุดไปเฉย ๆ แต่จะส่งการตัดสินใจย้อนกลับไปยังขั้นตอนเขียนบันทึกการประชุมจนกว่าจะกำหนดได้ว่าใครรับผิดชอบ นี่คือการออกแบบหลักของไปป์ไลน์นี้ ไม่ปล่อยช่องว่างผ่านไปเงียบ ๆ แต่แจ้งให้รู้ดัง ๆ
โครงสร้างโฟลเดอร์สินทรัพย์ทั้งหมดถูกจัดวางไว้แบบนี้
การจะสกัดได้นั้น บันทึกการประชุมต้องมีรูปร่างที่เครื่องอ่านได้ หากไม่มีส่วน "## 결정" หรือการตัดสินใจถูกปนอยู่ในย่อหน้าร้อยแก้ว ตัวพาร์สเซอร์ก็สกัดอะไรไม่ออกเลย เพราะฉะนั้นสิ่งแรกที่ใส่เข้าไปคือการตรวจรูปแบบ งานที่ meeting_lint.py ทำนั้นเรียบง่าย มี frontmatter ที่จำเป็นไหม มีส่วนที่จำเป็นไหม สล็อตการตัดสินใจถูกเติมในรูปแบบ D1, D2 หรือไม่
# โครงของ meeting_lint.py
REQUIRED_FRONTMATTER = ["type", "date", "category", "attendees"]
REQUIRED_SECTIONS = ["## 안건", "## 결정", "## 액션 아이템", "## 다음 회의"]
ALLOWED_CATEGORIES = ["art", "battle", "daily", "issue", "review"]
def lint(meeting_note_path):
fm, body = parse_markdown(meeting_note_path)
errors = []
for key in REQUIRED_FRONTMATTER:
if key not in fm:
errors.append(f"frontmatter ขาดหาย: {key}")
if fm.get("category") not in ALLOWED_CATEGORIES:
errors.append(f"ค่า category ไม่เหมาะสม: {fm.get('category')}")
for section in REQUIRED_SECTIONS:
if section not in body:
errors.append(f"ส่วนขาดหาย: {section}")
if "## 결정" in body:
block = extract_section(body, "## 결정")
if not any(l.strip().startswith("- D") for l in block.split("\n")):
errors.append("สล็อตการตัดสินใจว่างเปล่า (ต้องใช้รูปแบบ D1, D2...)")
return errors
เราผูกการตรวจนี้ไว้เป็นฮุกก่อนคอมมิตบันทึกการประชุม หากผิดรูปแบบ ตัวคอมมิตเองก็จะถูกบล็อก ถ้าตั้งไว้เป็นแค่คำแนะนำ วันที่ยุ่ง ๆ ก็จะแอบข้ามไปเงียบ ๆ และรูปแบบที่ข้ามไปครั้งหนึ่งจะพังในสัปดาห์ถัดไป ถ้าถูกบล็อกสัก 1\~2 สัปดาห์ รูปแบบก็จะติดมือ แต่หากเข้มงวดเกินไปจะทำให้ผัดผ่อนการเขียนบันทึกการประชุมเสียเอง ดังนั้นการเคลียร์ false positive สักครั้งหลังช่วงปรับตัวจึงเป็นการดำเนินงานที่สมจริง
จากบันทึกการประชุมที่ผ่านรูปแบบมาแล้ว decision_parser.py จะอ่านสล็อตการตัดสินใจ สิ่งที่ต้องสกัดจากการตัดสินใจหนึ่งอันมีพอดีสี่อย่าง ตัดสินใจอะไร (decision), ใครรับผิดชอบ (owner), ทำไมจึงตัดสินใจแบบนั้น (rationale), ต่อไปต้องทำอะไร (follow_up) สี่ฟิลด์นี้ทำให้การตัดสินใจกลายเป็นสินทรัพย์ โดยเฉพาะ owner การตัดสินใจที่ไม่มีเจ้าของไม่ใช่การตัดสินใจ แต่เป็นเพียงความหวัง ดังนั้นพาร์สเซอร์จึงไม่ปล่อย owner ที่ว่างให้เป็นช่องว่างเงียบ ๆ แต่จะใส่ [MISSING] เพื่อแจ้ง
จากจุดนี้ไปจนจบ เราจะตามดูกระบวนการที่บันทึกการประชุมหนึ่งฉบับกลายเป็นสินทรัพย์โดยไม่ข้ามแม้แต่บรรทัดเดียว นี่คือตัวอย่างเดียวที่ต่อเนื่องตั้งแต่อินพุตจนถึงการเลื่อนขั้นเป็น atom
================ อินพุต: meetings/2026-05-18_battle_tf.md ================
---
type: meeting
date: 2026-05-18
category: battle
attendees: [이민수, teammate_a, teammate_b]
related_atoms: [combat_global_cooldown_constant]
---
## 안건
- รวมค่ารวมคูลดาวน์ส่วนกลางของการต่อสู้ (GCD) ให้เป็นค่าเดียว
- สกิลฟื้นฟูจะได้รับการยกเว้น GCD หรือไม่
## 결정
- D1: รวมค่าคูลดาวน์ส่วนกลางของการต่อสู้ให้เป็น 0.5 วินาที (เจ้าของ: teammate_a) [เหตุผล: จากการทดสอบความรู้สึกตอบสนองอินพุตเทียบกับ refgame ค่า 0.5 วินาทีเสถียรที่สุด]
- D2: สกิลฟื้นฟูยกเว้นจากการใช้คูลดาวน์ส่วนกลาง [เหตุผล: กังวลว่าวงรอบการฟื้นฟูจะขาดช่วง]
## 액션 아이템
- @teammate_a: ใส่ค่า 0.5 ในคอลัมน์ cooldown ของชีตข้อมูลการต่อสู้ทั้งหมด (~MM-DD)
## 다음 회의
- MM-DD 14:00, รีวิวผลทดสอบวงรอบการฟื้นฟู 1 สัปดาห์
================ $ python meeting_lint.py meetings/2026-05-18_battle_tf.md ================
[OK] frontmatter 4/4, ส่วน 4/4, ตรวจพบสล็อตการตัดสินใจ 2 รายการ อนุญาตให้คอมมิต
================ $ python decision_parser.py meetings/2026-05-18_battle_tf.md ================
[
{
"id": "D1",
"decision": "รวมค่าคูลดาวน์ส่วนกลางของการต่อสู้ให้เป็น 0.5 วินาที",
"owner": "teammate_a",
"rationale": "จากการทดสอบความรู้สึกตอบสนองอินพุตเทียบกับ refgame ค่า 0.5 วินาทีเสถียรที่สุด",
"follow_up": "ใส่ค่า 0.5 ในคอลัมน์ cooldown ของชีตข้อมูลการต่อสู้ทั้งหมด (~MM-DD)",
"source_meeting": "2026-05-18_battle_tf.md",
"category": "battle",
"related_atoms": ["combat_global_cooldown_constant"]
},
{
"id": "D2",
"decision": "สกิลฟื้นฟูยกเว้นจากการใช้คูลดาวน์ส่วนกลาง",
"owner": "[MISSING]", # ← ไม่ได้ระบุเจ้าของ พาร์สเซอร์แจ้งเตือน
"rationale": "กังวลว่าวงรอบการฟื้นฟูจะขาดช่วง",
"follow_up": null, # ← ไม่มีแอ็กชันต่อเนื่องเช่นกัน
"source_meeting": "2026-05-18_battle_tf.md",
"category": "battle",
"related_atoms": ["combat_global_cooldown_constant"]
}
]
[WARN] D2: owner=[MISSING] — การตัดสินใจไม่มีเจ้าของ พักการสร้าง pending ส่งคืนให้ผู้เขียนบันทึกการประชุม
================ สร้าง pending: ผ่านเฉพาะ D1 ================
$ cat atoms/pending/meeting_decision_2026-05-18_D1.md
---
name: meeting_decision_2026-05-18_D1
description: การตัดสินใจรวมคูลดาวน์ส่วนกลางของการต่อสู้เป็น 0.5 วินาที
status: pending
type: decision
source_meeting: 2026-05-18_battle_tf.md
owner: teammate_a
category: battle
related_atoms: [combat_global_cooldown_constant]
created: 2026-05-18
---
## 결정
รวมค่าคูลดาวน์ส่วนกลางของการต่อสู้ให้เป็น 0.5 วินาที
## 근거
จากการทดสอบความรู้สึกตอบสนองอินพุตเทียบกับ refgame ค่า 0.5 วินาทีเสถียรที่สุด
## 후속 액션
- [ ] @teammate_a: ใส่ค่า 0.5 ในคอลัมน์ cooldown ทั้งหมด (~MM-DD)
================ 1 สัปดาห์ต่อมา รีวิวรายสัปดาห์ ================
$ python promote.py atoms/pending/meeting_decision_2026-05-18_D1.md
[PROMOTE] → atoms/combat_global_cooldown_constant_decisions/meeting_decision_2026-05-18_D1.md
[JIT] ลงทะเบียน manifest: trigger=(전투|쿨다운|GCD|cooldown), atom 18개 → 19개
[OK] ตั้งแต่เซสชันถัดไป เมื่อป้อน "글로벌 쿨다운" การตัดสินใจนี้จะถูกฉีดอัตโนมัติ
กล่องเดียวนี้คือทั้งหมดของไปป์ไลน์ จุดที่ควรสังเกตคือ D2 เนื้อหาการตัดสินใจก็ดูเรียบร้อย มีเหตุผลด้วย แต่ owner กลับว่าง พาร์สเซอร์ไม่ปล่อยอันนี้ผ่านไปเฉย ๆ มันใส่ [MISSING] พักการสร้าง pending แล้วส่งคืนให้ผู้เขียน D2 จะได้เจ้าของในการประชุม "รีวิวผลทดสอบวงรอบการฟื้นฟู 1 สัปดาห์" อีกไม่กี่วันต่อมาแล้วเข้ามาใหม่ การส่งคืนเพียงครั้งเดียวที่อุดช่องว่างนี้ ทำให้คำถาม "ตกลงใครรับหน้าที่นั้นไป" ในแชตภายในทีมสามวันถัดมาไม่มีวันโผล่ขึ้นอีกเลย
กฎที่ว่าหากไม่มี owner ให้แจ้งเตือนนั้น ถูกตอกตรึงเป็นกฎไว้ด้วย atom หนึ่งอัน (decision_summary_not_clickup_mirror, §17.1.2) ในเครื่องมือจัดการทาสก์อาจมีงานชื่อ "แก้ชีตข้อมูล" ลอยอยู่ แต่ว่างานนั้นเป็นผลของการตัดสินใจอะไรและทำไม จะถูกบันทึกไว้เฉพาะใน atom ของบันทึกการประชุมเท่านั้น
การตัดสินใจที่พาร์สเซอร์ปล่อยผ่านจะยังไม่กลายเป็น atom ทางการในทันที แต่จะรออยู่ใน pending/ หนึ่งสัปดาห์ เพราะสิ่งที่ตัดสินใจอย่างมั่นใจในที่ประชุม พอลองใช้งานจริงหนึ่งสัปดาห์ก็มักจะถูกพลิกกลับบ่อย ๆ D2 ในตัวอย่างข้างต้นอยู่ในเขตอันตรายนั้นพอดี การตัดสินใจที่ว่า "ฟื้นฟูยกเว้น GCD (คูลดาวน์ส่วนกลาง)" อาจถูกพลิกกลับอีกครั้งหากวงรอบการฟื้นฟูพังในการทดสอบ 1 สัปดาห์ pending คือช่องที่บังคับให้มีเวลาให้หมึกแห้ง
และการทิ้งก็เก็บเป็นสินทรัพย์เช่นกัน หากการตัดสินใจอย่าง D2 พังลงในการทดสอบ 1 สัปดาห์ แทนที่จะลบทิ้งเฉย ๆ เราสร้าง atom ประวัติการทิ้งขึ้นมา
---
name: meeting_decision_2026-05-18_D2_DISCARDED
status: discarded
discarded_reason: ผลทดสอบ 1 สัปดาห์ เส้นโค้ง DPS ของวงรอบการฟื้นฟูพังทลาย
---
## 원 결정
ใช้คูลดาวน์ส่วนกลาง 0.5 วินาทีกับสกิลฟื้นฟูด้วย
## 폐기 이유
ในการทดสอบ 1 สัปดาห์ DPS ของวงรอบการฟื้นฟูตก ทำให้บาลานซ์โดยรวมพังทลาย ย้อนกลับเป็นการตัดสินใจยกเว้น
## 교훈
"ฟื้นฟูยกเว้น GCD คือมาตรฐาน" → เลื่อนขั้นเป็น atom combat_healing_skill_cooldown_exception
ประวัติการทิ้งจะกลายเป็นคำตอบของคำถาม "วาระนี้เคยลองทำมาก่อนไหม" ในการประชุมครั้งถัดไป มันคือเครื่องมือที่ถูกที่สุดในการป้องกันไม่ให้ทำผิดซ้ำสองครั้ง แต่หากบันทึกการทิ้งสะสมมากขึ้นจะกลายเป็นสัญญาณรบกวนในการค้นหา จึงจำเป็นต้องจัดระเบียบรายไตรมาส ตัดที่ซ้ำซ้อนออกและเหลือไว้เพียงบทเรียน
ทุกสัปดาห์ตามเวลาที่กำหนด เราจะดูตัวเลือก pending รวบยอดทีเดียว ผลลัพธ์มีหนึ่งในสามอย่าง
| ผลลัพธ์ | การจัดการ |
|---|---|
| เลื่อนขั้น | pending → ย้ายไปโฟลเดอร์ atom ทางการ ลงทะเบียน JIT manifest |
| ทิ้ง | การตัดสินใจถูกพลิก → เอาออกจาก pending และเก็บ atom ประวัติการทิ้ง |
| พักไว้ | ข้อมูลไม่พอ → ต่อ pending อีก 1 สัปดาห์ |
การรีวิวใช้เวลาราว 15 นาทีต่อ atom 10 อัน เมื่อตัดสินใจเลื่อนขั้น promote.py จะจัดการย้ายไฟล์และอัปเดต manifest ในคราวเดียว
# โครงของ promote.py
def promote(pending_path):
fm, body = parse_markdown(pending_path)
target = ATOM_BASE / f"{fm['related_atoms'][0]}_decisions" / f"{fm['name']}.md"
move(pending_path, target)
manifest = json.load(open(JIT_MANIFEST))
manifest['atoms'].append({
"name": fm['name'],
"path": str(target),
"trigger_regex": build_trigger(fm), # related_atoms + คีย์เวิร์ด category
"description": fm['description'],
"added": today(),
})
json.dump(manifest, open(JIT_MANIFEST, "w"), indent=2)
log_promotion(fm['name'])
หาก trigger_regex ตรงกับอินพุตของผู้ใช้ในเซสชันถัดไป การตัดสินใจนี้จะถูกฉีดเข้าโดยอัตโนมัติ ในตัวอย่างข้างต้น หากป้อน "글로벌 쿨다운" การตัดสินใจ D1 และเหตุผลของมันจะตามเข้ามาด้วย นี่คือจุดที่การตัดสินใจซึ่งเคยต้องย้ายด้วยมือ กลายเป็นสินทรัพย์ที่ผุดขึ้นมาเองในจังหวะที่ต้องการ
จากประสบการณ์การดำเนินงานโปรเจกต์ A ของผู้เขียน นี่คือความรู้สึกจากการเปรียบเทียบระหว่างขั้นที่จัดแค่รูปแบบมาตรฐานไว้ กับขั้นที่เดินไปป์ไลน์ ตัวเลขด้านล่างไม่ใช่การวัดอย่างแม่นยำ แต่เป็นทิศทางและสัดส่วนคร่าว ๆ ที่รู้สึกได้ระหว่างการดำเนินงาน มีการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ปนอยู่
| รายการ | แค่รูปแบบ (สกัดด้วยมือ) | เดินไปป์ไลน์ |
|---|---|---|
| เวลาสกัดการตัดสินใจจากบันทึกการประชุม | 20\~30 นาทีต่อการประชุม | น้อยกว่า 1 นาที |
| สัดส่วนการเลื่อนขั้นการตัดสินใจเป็น atom | 5\~10% (ไม่มีเวลาจัดระเบียบ) | 60\~80% (ตรวจครบทุกอัน) |
| ประชุมซ้ำ "เคยตัดสินใจมาก่อนไหม?" | 5\~10 ครั้งต่อไตรมาส | 0\~2 ครั้งต่อไตรมาส |
| การเกิดการตัดสินใจที่ไม่รู้เจ้าของ | ตามรอยไม่ได้ | มองเห็นทันทีผ่านการแจ้ง [MISSING] |
สิ่งที่เปลี่ยนไปมากที่สุดคือสัดส่วนการเลื่อนขั้น ตอนที่จัดระเบียบด้วยมือนั้น เพราะไม่มีเวลา การตัดสินใจกว่า 90% จึงระเหยหายไป พอทำให้เป็นอัตโนมัติก็ตรวจได้ครบทุกอัน การตัดสินใจที่มีคุณค่าจึงถูกเก็บไว้โดยไม่ตกหล่น ทิศทางนั้นชัดเจน ส่วนค่าที่แม่นยำของสัดส่วนจะแตกต่างกันไปตามขนาดทีมและความถี่ของการประชุม
| รูปแบบ | วิธีแก้ |
|---|---|
| ใช้ lint เป็นแค่คำแนะนำ | บังคับด้วยฮุกคอมมิต |
| เขียนการอภิปรายปนลงในสล็อตการตัดสินใจ | การตัดสินใจหนึ่งประโยค เหตุผลแยกเป็นฟิลด์ต่างหาก |
| ปล่อยช่องว่าง owner ผ่านไปเฉย ๆ | แจ้ง [MISSING] + ส่งคืนด้วยการพัก pending |
| เลื่อนการรีวิว pending ไปเรื่อย ๆ | จัดสล็อตตายตัวในการทบทวนรายสัปดาห์ แม้แค่ 5 นาทีก็ทุกสัปดาห์ |
| ไม่เก็บประวัติการทิ้ง | เก็บการทิ้งเป็น atom ต่างหากด้วย |
ห้าบรรทัดนี้แทบจะเป็นทั้งหมด การลดจุดที่มนุษย์ต้องคอยรักษาด้วยเจตจำนงให้น้อยที่สุด แล้วมอบการตรวจรูปแบบและ owner ให้เครื่องจักรทำ คือจุดเสถียรของระบบนี้
การประยุกต์นอกเกม การที่บันทึกการประชุมหนึ่งฉบับไหลไปบนสายพานลำเลียง ตรวจรูปแบบ→สกัดการตัดสินใจ→ตรวจสอบ 1 สัปดาห์→ลงทะเบียนทางการ และมนุษย์แตะแค่สองจุดคือทางเข้า (เขียน) และทางออก (ตรวจสอบสัปดาห์ละครั้ง) โครงสร้างนี้ไม่ใช่แค่เกม แต่ปลูกถ่ายลงในการดำเนินงานเอกสารของทีมงานความรู้ทุกประเภทได้ ยกตัวอย่างเช่นเมื่อทีมที่ปรึกษาจัดการกับโน้ตการประชุมลูกค้า เพียงแค่จัดรูปแบบโน้ตให้เป็นมาตรฐาน แล้วสกัดสล็อต "การตัดสินใจ·ผู้รับผิดชอบ·เหตุผล·การกระทำถัดไป" ด้วย LLM เป็นรอบแรก หากผู้รับผิดชอบว่างก็ขึ้น
[MISSING]แล้วส่งคืน และเลื่อนขั้นเฉพาะการตัดสินใจที่บ่มมาแล้วหนึ่งสัปดาห์ไปเป็นแอ็กชันแทร็กเกอร์ทางการก็พอ การตัดสินใจจากการประชุมที่เคยระเหยหายไปกว่า 90% เมื่อจัดระเบียบด้วยมือ พอวางขึ้นสายพานลำเลียงก็จะกลายเป็นเป้าหมายที่ตรวจครบทุกอันและถูกเก็บไว้โดยไม่ตกหล่น
setup. วางเทมเพลตรูปแบบมาตรฐานไว้ในโฟลเดอร์บันทึกการประชุม แล้วผูก meeting_lint.py ไว้ในฮุกก่อนคอมมิต กำหนด frontmatter 4 ฟิลด์และส่วน 4 ส่วนให้เป็นข้อบังคับ
prompt. ใส่บันทึกการประชุมหนึ่งฉบับลงในพาร์สเซอร์แล้วสั่งดังนี้
จากส่วน
## 결정ของบันทึกการประชุมนี้ ให้สกัดสี่ฟิลด์คือ decision / owner / rationale / follow_up ของแต่ละการตัดสินใจออกมาเป็น JSON การตัดสินใจที่ไม่ได้ระบุ owner ให้ทำเครื่องหมาย owner เป็น[MISSING]แล้วรวบรวมไว้ในบรรทัดเตือนต่างหาก อย่าเดาเพื่อเติมให้เต็ม
verify. หากใน JSON เอาต์พุตมีการตัดสินใจที่ถูกใส่ [MISSING] ก็อย่าสร้าง pending สำหรับการตัดสินใจนั้น แต่ส่งคืนให้ผู้เขียนบันทึกการประชุม สร้างไฟล์ตัวเลือก pending เฉพาะการตัดสินใจที่เติม owner ครบแล้วเท่านั้น และหนึ่งสัปดาห์ต่อมาในรีวิวรายสัปดาห์จึงตัดสินว่าจะเลื่อนขั้น·ทิ้ง·พักไว้
หากทำงานคนเดียว สคริปต์สามตัวกับฮุกคอมมิตนั้นเกินจำเป็น ทำให้ส่วน ## 결정 ของบันทึกการประชุมเป็นมาตรฐานก็พอ แล้วเขียนหนึ่งบรรทัดต่อหนึ่งการตัดสินใจว่า D1: อะไร / เจ้าของ: ฉัน / เหตุผล: ทำไม สัปดาห์ละครั้ง ดึงเฉพาะบรรทัดการตัดสินใจจากบันทึกการประชุมของสัปดาห์นั้นมารวมไว้ในไฟล์เดียว (decisions.md) และบรรทัดที่เจ้าของว่าง ก็ทำเครื่องหมาย [MISSING] ด้วยตัวเองไว้แล้วค่อยเติมในสัปดาห์ถัดไป สคริปต์จะมาผูกทีหลังตอนที่มือเริ่มเมื่อยก็ยังไม่สาย หัวใจคือสามนิสัย "การตัดสินใจหนึ่งบรรทัด·ระบุเจ้าของ·รวบรวมสัปดาห์ละครั้ง"
บันทึกการประชุมไม่ได้มีไว้เพื่อสะสม เป้าหมายคือการที่หกเดือนต่อมาก็ยังค้นหาเจอ นำไปสู่การตัดสินใจได้ และปรากฏในสถานะเดียวกันบนเครื่อง PC ทั้งสองเครื่อง
บ่ายวันอังคาร ผมนึกขึ้นได้ว่าในการประชุมเมื่อปีก่อนเราตกลงกันชัดเจนว่าจะลดความอิ่มสีของชุดตัวละครลงหนึ่งระดับ แต่กลับหาบันทึกการประชุมนั้นไม่เจอ พอเปิดโฟลเดอร์ดู ก็พบไฟล์อย่าง meeting_0413.md, 회의_수정본_final.md, IMG_2034.png ราว 200 ไฟล์กองอยู่โดยเรียงตามวันที่เพียงอย่างเดียว ไม่มีหมวดหมู่ ไม่มีคำบรรยายภาพ ไม่มีการตั้งชื่อที่สม่ำเสมอ การตัดสินใจอยู่ที่ไหนสักแห่ง แต่เส้นทางที่จะไปถึงการตัดสินใจนั้นหายไปแล้ว
บันทึกการประชุมจะกลายเป็นสินทรัพย์ได้ ต้องมีสามสิ่งทำงานพร้อมกัน การจัดหมวดหมู่ สร้างจุดเข้าค้นหาลำดับแรก คำบรรยายภาพ ทำให้ภาพครึ่งหนึ่งยังค้นหาได้ และ การซิงค์ ตรึงต้นทุนการประมวลผลไว้ที่เฉพาะส่วนที่เปลี่ยนแปลงแม้จะเกิน 1,000 รายการก็ตาม หากขาดแม้เพียงหนึ่งในสามนี้ บันทึกการประชุมก็จะกลายเป็นกองที่ตายแล้ว ซึ่งยิ่งสะสมก็ยิ่งหนักขึ้นเท่านั้น
ใน §17.1·§17.2 เราได้วางกระแสที่แปลงบันทึกการประชุมให้เป็นไปป์ไลน์การสกัด — ตรวจรูปแบบด้วย meeting_lint.py ให้ decision_parser.py ดึงสี่ฟิลด์ของการตัดสินใจ (decision / owner / rationale / follow_up) ออกมา หากไม่มี owner ก็แจ้งเป็น [MISSING] รวบรวมไว้เป็น pending atom แล้วเลื่อนขั้นด้วย promote.py บทนี้จะกล่าวถึงสามมาตรฐานการดำเนินงานที่ค้ำให้ไปป์ไลน์นั้นไม่พังในระยะยาว
บันทึกการประชุมเมื่อเวลาผ่านไปก็จะมีจำนวนหลายร้อยหลายพันรายการ ข้อมูลที่ค้นหาไม่ได้ไม่ใช่สินทรัพย์ หมวดหมู่คือทางแยกแรกของการค้นหานั้น เหมือนกับการติดป้ายบนตู้เอกสารในออฟฟิศ ตู้ที่ไม่มีป้ายสุดท้ายก็จะไม่มีใครเปิด
โปรเจกต์ A (พัฒนา MMORPG) ที่ผู้เขียนดูแลอยู่ จัดหมวดหมู่ไว้ห้าหมวด หัวใจสำคัญคือการรักษาให้ เล็กและตั้งฉากกัน (orthogonal)
ห้าหมวดไม่ใช่คำตอบที่ถูกต้องสำหรับทุกทีม หากเป็นโปรเจกต์ที่มีระบบที่ไม่ใช่การต่อสู้เป็นแกนกลาง ก็จำเป็นต้องปรับ เช่น เปลี่ยน battle เป็น system หัวใจสำคัญไม่ใช่ตัวเลข แต่เป็นหลักการที่ว่าต้องรักษาให้ การตัดสินใจเรื่องหมวดหมู่เล็กพอที่จะไม่ขัดขวางตัวการประชุมเอง
การที่การประชุมคาบเกี่ยวสองช่องเกิดขึ้นบ่อย หากกำลังรีวิวคอนเซ็ปต์ตัวละครแล้วเลยไปตกลงเรื่องโมชันการต่อสู้ด้วย จะเป็น art หรือ battle หลักการคือ ยึดผลผลิตหลักเพียงอย่างเดียว หากคอนเซ็ปต์คือผลผลิตหลักก็จัดเป็น art ส่วนโมชันการต่อสู้บันทึกเสริมไว้ในฟิลด์ sub_topic
---
type: meeting_note
category: art
sub_topic: [character, battle_motion]
date: 2026-05-18
attendees: [teammate_a, teammate_b, teammate_c, 이민수]
related_atoms: [character_concept_kim, battle_motion_kim]
confidential: internal
---
sub_topic เป็นเพียงตัวกรองค้นหาลำดับที่สอง ไม่ได้ใช้ในการตัดสินใจเรื่องเส้นทาง (routing) การกำหนดเส้นทางทำงานด้วยค่าเดี่ยวของ category เสมอ หากหลักการค่าเดี่ยวนี้พังลง promote.py ของ §17.2 ก็จะไม่สามารถแยกสาขาว่าจะส่ง atom ไปยังโฟลเดอร์ใด และผลรวมของสถิติรายหมวดหมู่ก็จะคลาดเคลื่อน ความตั้งฉากไม่ใช่เรื่องความสวยงาม แต่เป็นเงื่อนไขเบื้องต้นของความสมบูรณ์ของไปป์ไลน์
เหตุผลที่แท้จริงในการแบ่งห้าช่องไม่ใช่ป้ายค้นหา แต่เป็นเพราะแต่ละช่องมีวิธีดำเนินงานต่างกัน เมื่อแยกออกจึงออกแบบการดำเนินงานแบบต่างระดับได้อย่างเป็นธรรมชาติ
art มีภาพแนบจำนวนมาก จึงจำเป็นต้องมีมาตรฐานคำบรรยายภาพในหัวข้อถัดไป เนื่องจากการตัดสินใจเน้นด้านภาพ ในช่องการตัดสินใจจึงมีการอ้างอิงภาพอย่าง  ใส่เข้ามา ส่วน battle การตัดสินใจเป็นตัวเลขและกฎ จึงมีอัตราการเลื่อนขั้น atom อัตโนมัติสูงที่สุด และเนื่องจากการตัดสินใจหนึ่งบรรทัดนำไปสู่การเปลี่ยนแปลงชีตข้อมูลทั้งชุด การมองเห็นขอบเขตผลกระทบ (แผนผังความสัมพันธ์ในส่วนที่ 11) จึงสำคัญ daily การที่แทบไม่มีการตัดสินใจคือเรื่องปกติ และเนื่องจากสะสมเร็ว จึงแยกเป็นโฟลเดอร์อัตโนมัติรายสัปดาห์ (daily/2026-W21/) issue บันทึกการประชุมมักกระจัดกระจาย จึงกำหนดให้การจัดการภายใน 24 ชั่วโมงหลังเป็นข้อบังคับ และสกัด atom ป้องกันการเกิดซ้ำไปยัง issue_postmortem/ ส่วน review มีปริมาณยาว จึงเขียน atom สรุป 5–10 บรรทัดแยกไว้ต่างหากเพื่อให้ถูกอ้างอิงอัตโนมัติในการทบทวนไตรมาสถัดไป
การเพิ่มหมวดหมู่ใหม่ต้องทำอย่างรอบคอบมาก จะพิจารณาก็ต่อเมื่อผ่านครบทั้งสี่เงื่อนไขนี้ — เกิดขึ้นอย่างน้อย 5 ครั้งต่อไตรมาส วิธีดำเนินงานแตกต่างชัดเจนจากห้าหมวดเดิม จำเป็นต้องมีโฟลเดอร์เส้นทางแยกต่างหาก และยังคงเกิดอย่างน้อย 5 ครั้งแม้หนึ่งเดือนผ่านไป จากประสบการณ์การดำเนินงานของผู้เขียน ห้าหมวดยืนยงมากว่า 1 ปี และแม้ตอนที่นึกถึงตัวเลือกอย่าง tech_review หรือ external ขึ้นมา สุดท้ายก็ถูกดูดซับเข้าเป็น sub_topic
หมวดหมู่นั้น ลำดับแรกคือให้คนกรอกโดยตรงตอนเขียน ใช้ตัวจัดหมวดหมู่ด้วย AI ช่วยเฉพาะบันทึกการประชุมที่ขาดหายไป เช่นเอกสารที่ได้รับมาจากภายนอก พจนานุกรมคำสำคัญจับได้ราว 90% ส่วนที่เหลือคือ uncertain ให้ LLM หรือคนตัดสิน
เมื่อมอบหมายให้ LLM พรอมต์ที่บังคับข้อจำกัดอย่างเข้มงวดจะให้ผลเสถียรกว่า ต่อไปนี้คือพรอมต์ฉบับเต็มที่ใช้งานจริง
ต่อไปนี้คือบันทึกการประชุม จงจัดเข้า 1 ใน 5 หมวดหมู่
หมวดหมู่:
- art: ทิศทางภาพ·อาร์ต
- battle: ระบบการต่อสู้·การปรับสมดุล
- daily: แชร์ความคืบหน้าประจำ
- issue: รับมือปัญหาเร่งด่วน
- review: รีวิวไมล์สโตน·QA
บันทึกการประชุม:
[ฉบับเต็มหรือ 500 ตัวอักษรแรก]
รูปแบบการตอบ: คำหมวดหมู่เพียงคำเดียว ห้ามใส่คำอธิบาย·เหตุผล·ความไม่แน่นอนใด ๆ
หากคำตอบไม่ใช่ 1 ใน 5 หมวดหมู่ จะถือว่าระบบล้มเหลว
เมื่อใส่บันทึกการประชุมเดียวกัน (ด้านล่างคือช่วงต้นของการประชุม art) เข้าไป ผลลัพธ์ดิบของ Claude เป็นดังนี้
บันทึกการประชุมที่ป้อน:
รีวิวคอนเซ็ปต์ v3 ของตัวละคร K_007 (นักวิชาการ) มีความเห็นว่าความอิ่มของโทนสีชุดสูงเกินไป ตกลงลดลงหนึ่งระดับ และตกลงจะตรวจโทนของโมชันการต่อสู้ไปพร้อมกันในการประชุมครั้งหน้า
ผลลัพธ์ของ Claude:
art
ออกมาเรียบร้อยเพียงคำเดียว แต่เมื่อใส่บันทึกการประชุม daily เข้าไปในพรอมต์เดียวกัน ก็เกิดเรื่องแบบนี้ขึ้น
ป้อน:
วันนี้บิลด์พังตอนเช้ามืด สาเหตุดูเหมือนเป็นความขัดแย้งในการ merge ชีตข้อมูล จะแก้ด้วย hotfix ก่อนแล้วค่อยแก้อย่างเป็นทางการ
ผลลัพธ์ของ Claude:
issue
ผิวเผินเป็นคำพูดที่ออกมาจากสแตนด์อัป daily แต่ Claude ดูเนื้อหาแล้วจัดเป็น issue นี่คือเหตุผลที่ไม่ควรใช้ตัวจัดหมวดหมู่เป็นลำดับแรกเลย คนจะตัดสินเชิงการดำเนินงานว่า "อันนี้เป็นเหตุบิลด์ที่จู่ ๆ โผล่มากลาง daily จึงควรแยกเป็นการประชุม issue ต่างหาก" ส่วน AI ดูแค่ข้อความแล้วแปะป้าย ป้ายอาจถูก แต่มันตัดสินไม่ได้ว่าจะแยกการประชุมหรือไม่ ดังนั้นคนจึงเป็นลำดับแรก ส่วน LLM อยู่แค่บทบาทเสริมส่วนที่ขาดหาย
ในการทบทวนรายไตรมาส เราจะนับจำนวนการประชุมรายหมวดหมู่เพื่อดูว่า "ใช้เวลาไปที่ไหน" การกระจายด้านล่างเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) จำนวนสัมบูรณ์เป็นเพียงตัวอย่าง มีเพียงความสัมพันธ์มากน้อยของสัดส่วนเท่านั้นที่ตรงกับความรู้สึกในการดำเนินงานจริง
| หมวดหมู่ | สัดส่วน (ประมาณ) | หมายเหตุ |
|---|---|---|
daily |
ราว 1/3 | ประจำทุกวัน แทบไม่มีการตัดสินใจ |
battle |
ราว 1/5 | TF การต่อสู้ สัปดาห์ละ 2 ครั้ง |
art |
ราว 1/7 | รีวิวอาร์ต + การประชุมภายนอก |
issue |
ต่ำ | เหตุบิลด์ ฯลฯ |
review |
ต่ำที่สุด | ไมล์สโตน·ทบทวนรายไตรมาส |
| อื่น ๆ | ราว 1/5 | 1:1, ภายนอก ฯลฯ ที่ไม่เข้าหมวดหมู่ |
หาก issue ถูกจับได้โดดเด่นในไตรมาสหนึ่ง การปรับปรุงเสถียรภาพของบิลด์·CI ก็จะกลายเป็นลำดับความสำคัญถัดไป หมวดหมู่ไม่ได้มีไว้แค่ค้นหา แต่ยังเป็นกระจกที่สะท้อนการจัดสรรเวลาขององค์กรด้วย
บันทึกการประชุม art มีเนื้อหาครึ่งหนึ่งเป็นภาพ และภาพที่ไม่มีคำบรรยายก็เหมือนกองรูปถ่ายที่กองบนโต๊ะ วันนั้นจำได้หมด แต่หนึ่งเดือนต่อมา มีแต่รูปที่เขียนบันทึกหนึ่งบรรทัดไว้ด้านหลังเท่านั้นที่ยังอยู่รอด
flowchart LR
A["ทันทีหลังประชุม
เฉพาะผู้เข้าร่วมเข้าใจ"] --> B["1 สัปดาห์ต่อมา
แม้ผู้เขียนก็จำได้บางส่วน"]
B --> C["1 เดือนต่อมา
ไม่ชัดว่าเกี่ยวกับการตัดสินใจใด"]
C --> D["6 เดือนต่อมา
ถูกทิ้งโดยปริยาย · ค้นหาไม่ได้"]
A -.คำบรรยายหนึ่งบรรทัด.-> E["แม้ 6 เดือนต่อมา
ก็อ้างอิงย้อนด้วย ID การตัดสินใจได้"]
style D fill:#fcd6d6,stroke:#d94a4a
style E fill:#d6fce0,stroke:#4ad97a
ในเมื่อภาพคือครึ่งหนึ่งของบันทึกการประชุม หากค้นหาไม่ได้ ก็เท่ากับสินทรัพย์ของบันทึกการประชุมหายไปครึ่งหนึ่ง สิ่งที่ทำให้ครึ่งนั้นมีชีวิตคือคำบรรยายหนึ่งบรรทัด
มาตรฐานคำบรรยายภาพของโปรเจกต์ A จบในสามบรรทัด

**[ภาพที่ 1]** ตัวละคร K_007 (นักวิชาการ) คอนเซ็ปต์ v3 — ลดความอิ่มของโทนสีชุดลงหนึ่งระดับ
*การตัดสินใจ: D2 (ความอิ่มสีชุด -10%) | แอ็กชันถัดไป: งาน v4 (~MM-DD)*
องค์ประกอบสามอย่างต่างเปิดเส้นทางค้นหาที่ต่างกัน หมายเลข + คำอธิบายหนึ่งบรรทัด เปิดเส้นทางให้อ้างอิงในเนื้อหาว่า "ดูภาพที่ 1" การอ้างอิง ID การตัดสินใจ (D2) เปิดการอ้างอิงย้อนกลับว่า "ภาพที่เชื่อมกับการตัดสินใจนี้" และ แอ็กชันถัดไป ทิ้งเบาะแสของงานที่จะตามมา ทั้งสามบรรทัดเขียนได้ภายในหนึ่งนาที "แนบทันที" ไม่ได้หมายถึง "เขียนระหว่างประชุม" ระหว่างประชุมจัดการแค่สรุปการตัดสินใจ แล้วเติมคำบรรยายภาพภายใน 10 นาทีหลังจบจึงเป็นเรื่องที่ทำได้จริง
สิ่งที่สำคัญพอ ๆ กับคำบรรยายคือชื่อไฟล์ เพราะตัวโฟลเดอร์และชื่อไฟล์เองคือจุดเข้าค้นหาลำดับแรก
โฟลเดอร์บันทึกการประชุม/
├── 2026-05-18_art_review.md
└── images/
└── 2026-05-18_art_review/
├── character_kim_concept_v3.png
├── env_palette_comparison.png
└── reference_external_game_a.png
กฎคือ <หัวข้อ>_<รายการ>_<เวอร์ชัน or หมายเหตุ>.<ext> และห้ามใช้ภาษาเกาหลี·ช่องว่าง·อักขระพิเศษ (ป้องกันเหตุการเข้ารหัสพาธ) IMG_2034.png (สื่อความหมาย 0), 김캐릭터 v3.png (ภาษาเกาหลี·ช่องว่าง), final_final_v3_real.png (เวอร์ชันไร้ความหมาย), untitled.png (เป็นตัวเลือกที่ควรทิ้ง) ล้วนเป็นแอนติแพตเทิร์นทั้งสิ้น ชื่อแบบนี้ไม่ควรฝากไว้กับเจตจำนงของคน แต่ควรเพิ่มกฎตรวจสอบใน meeting_lint.py เพื่อบังคับใช้ — เพียงเสริมการตรวจชื่อไฟล์หนึ่งบรรทัดลงในตัว lint ที่ทำให้การตรวจรูปแบบเป็นอัตโนมัติไว้ใน §17.2 ก็เพียงพอ
ในการประชุมมักมีการอ้างอิงเกม·อาร์ตจากภายนอกเป็นข้อมูลประกอบบ่อย ๆ หากไม่มีแหล่งที่มาก็เชื่อมตรงไปสู่เหตุละเมิดลิขสิทธิ์

**[ภาพที่ 3]** ภาพอ้างอิง — refgame (Developer Y, 2024)
*เหตุผลในการอ้างอิง: เปรียบเทียบการจัดการความอิ่มสีของคอนเซ็ปต์ที่คล้ายกัน ไม่มีการนำมาใช้โดยตรง*
ระบุครบทั้งแหล่งที่มา (ชื่อเกม·บริษัทผู้พัฒนา·ปี)·เหตุผลในการอ้างอิง·มีการนำมาใช้โดยตรงหรือไม่ และเนื่องจากภาพมีความเสี่ยงรั่วไหลสูงกว่าข้อความ จึงติดระดับไว้ใน frontmatter
confidential: internal # internal / restricted / external_ok
images:
- file: character_kim_concept_v3.png
confidential: restricted
reason: การออกแบบตัวละครที่ยังไม่เปิดเผย
internal หมายถึงแชร์ภายในบริษัท restricted หมายถึงเฉพาะ TF·ผู้รับผิดชอบที่เกี่ยวข้อง external_ok หมายถึงผ่านการอนุมัติให้แชร์กับการตลาด·ภายนอก เมื่อบิลด์บันทึกการประชุม ผลลัพธ์จะถูกแยกตามระดับ และภาพที่ไม่ใช่ external_ok จะถูกเบลอ (blur) อัตโนมัติในฉบับที่แชร์ภายนอก การแยกอัตโนมัตินี้ให้ผลโดยตรงคือทำให้เหตุการมาสก์ตอนแชร์ภายนอกเป็น 0 ในทางปฏิบัติ
การเขียนคำบรรยาย 50 อันด้วยมือสำหรับภาพ 50 ภาพเป็นภาระ จึงให้เนื้อหาและชื่อไฟล์กับ AI แล้วรับร่างมาทั้งชุด
ต่อไปนี้คือเนื้อหาบันทึกการประชุม + รายการไฟล์ภาพ
[เนื้อหาบันทึกการประชุม]
[ชื่อไฟล์ภาพ 10 ไฟล์]
จงเขียนร่าง caption สำหรับแต่ละภาพ
รูปแบบ:
- [ภาพที่ N] <คำอธิบาย> — <การตัดสินใจหลักหรือการเปลี่ยนแปลง>
- *การตัดสินใจ: D? | แอ็กชันถัดไป: ?*
ภาพที่หาหลักฐานจากเนื้อหาไม่ได้ ให้ระบุว่า "ไม่ทราบเนื้อหา — ต้องให้ผู้เขียนยืนยัน"
บรรทัดสุดท้ายตรงนี้คือหัวใจ เมื่อใส่บันทึกการประชุมเดียวกันเข้าไป Claude ใส่คำบรรยายให้ภาพที่มีหลักฐานในเนื้อหา แต่สำหรับ reference_external_game_a.png ตอบมาแบบนี้
ผลลัพธ์ของ Claude (ตัดตอน):
[ภาพที่ 3] reference_external_game_a.png — ไม่ทราบเนื้อหา ต้องให้ผู้เขียนยืนยัน ในเนื้อหาไม่ได้ระบุเหตุผลในการอ้างอิงภาพอ้างอิงภายนอกนี้
AI แจ้งว่าสิ่งที่มันไม่รู้ก็คือไม่รู้ ผู้เขียนรับสิ่งนี้มาแล้วเติมเหตุผลในการอ้างอิง หากบริบทจากเนื้อหาเพียงอย่างเดียวไม่พอ ก็เลือกเฉพาะภาพหลัก 5–10 ภาพส่งไปยังโมเดล Vision (เนื่องจากต้นทุนโทเค็นของภาพสูง จึงไม่รันทั้งหมด)
# เลือกใช้เฉพาะภาพหลัก 5~10 ภาพ — ต้นทุนโทเค็นต่อภาพหนึ่งภาพสูง
response = client.messages.create(
model="claude-opus-4-8",
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "data": img_b64}},
{"type": "text", "text": "อธิบายภาพนี้เป็นภาษาเกาหลีหนึ่งบรรทัด ห้ามคาดเดา เอาเฉพาะที่เห็น"},
],
}],
)
ผู้เขียนนำหนึ่งบรรทัดนี้มาจัดให้อยู่ในรูปแบบคำบรรยายภาพ ไม่จำเป็นต้องรัน Vision กับทุกภาพ เพียงภาพหลัก 5–10 ภาพ ความเป็นไปได้ในการค้นหาก็เพิ่มขึ้นเพียงพอแล้ว
บันทึกการประชุมหนึ่งปีที่เขียนคำบรรยายภาพไว้ดี ก็กลายเป็นเอกสารวิชวลดีเวลอปเมนต์ได้ในตัวเอง สามารถติดตามการเปลี่ยนแปลงเชิงภาพของ character_kim v1 → v2 → v3 ด้วย ID การตัดสินใจ และหากกรองเฉพาะระดับ external_ok เอกสารรายงานภายนอกก็จะถูกคัดสรรโดยอัตโนมัติ และเมื่อรวบรวมภาพหลัก + คำบรรยายแยกตามสาขา ก็จะกลายเป็นเอกสารปฐมนิเทศ (onboarding) สำหรับสมาชิกทีมใหม่ หากแสดงการเปลี่ยนแปลงก่อน·หลังการนำคำบรรยายภาพมาใช้ด้วยการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ทิศทาง เป็นดังนี้ — อัตราความสำเร็จในการค้นหาบันทึกการประชุมจาก 6 เดือนก่อนเพิ่มขึ้นมาก คำถามซ้ำว่า "ภาพนี้เคยเห็นที่ไหนนะ?" ลดลงมาก และเหตุการมาสก์ตอนแชร์ภายนอกลู่เข้าหา 0 ค่าสัมบูรณ์อาจต่างกันไปในแต่ละทีม แต่เพียงขั้นที่ 1·2 (มาตรฐานชื่อไฟล์ + รูปแบบคำบรรยาย) เท่านั้น ทิศทางนั้นก็ปรากฏชัดเจน
ตัวบันทึกการประชุมเองเป็นไฟล์ข้อความ จึงใช้ git ก็เพียงพอ เป้าหมายที่แท้จริงของการซิงค์คือ ข้อมูลที่แตกแขนงมา จากบันทึกการประชุม — pending atom ที่เป็นตัวเลือกใน §17.2, JIT manifest, สถิติหมวดหมู่, ดัชนีการตัดสินใจ (decision_index.json), ดัชนีคำบรรยายภาพ, ผลลัพธ์บิลด์แยกตามระดับ confidential และ LLM embedding สำหรับการค้นหาแบบเวกเตอร์ ข้อมูลเหล่านี้ทั้งหมดต้องตอบสนองต่อการเปลี่ยนแปลงของบันทึกการประชุม
ปัญหาคือเมื่อบันทึกการประชุมเกิน 1,000 รายการ ต้นทุนการประมวลผลทั้งหมดใหม่ทุกครั้งจะกินไปครึ่งหนึ่งของการดำเนินงาน เหมือนกับการหยุดสายการผลิตทั้งสายแล้วสร้างชิ้นส่วนทุกชิ้นใหม่ — ทั้งที่มีชิ้นส่วนเปลี่ยนไปเพียงชิ้นเดียวเท่านั้น
flowchart TB
subgraph Full["Full Sync · ใช้ได้เมื่อต่ำกว่า 100 รายการ"]
F1["บันทึกการประชุมทั้งหมด 1,000 รายการ"] --> F2["สร้างข้อมูลที่แตกแขนงทั้งหมด
ใหม่ตั้งแต่ต้น"]
F2 --> F3["เปลี่ยนดัชนี·embedding ทั้งหมด"]
end
subgraph Inc["Incremental · จำเป็นเมื่อเกิน 200 รายการ"]
I1["ตรวจจับเฉพาะ N รายการที่เปลี่ยน
ด้วย git diff"] --> I2["สร้างเฉพาะข้อมูลที่แตกแขนง
ของ N รายการนั้นใหม่"]
I2 --> I3["อัปเดตดัชนีบางส่วน
แยกสาขา เพิ่ม·แก้·ลบ"]
end
Full -.บันทึกการประชุมทะลุ 200 รายการ.-> Inc
style Full fill:#fce7d6,stroke:#d98a4a
style Inc fill:#d6fce0,stroke:#4ad97a
Full Sync มีการพัฒนาที่เรียบง่าย และความเสี่ยงที่สถานะไม่ตรงกันเป็น 0 จึงปลอดภัยกว่าเสียอีกในช่วงเริ่มต้น (ต่ำกว่า 100 รายการ) ไม่ได้หมายความว่า Full เป็นวิธีที่ไม่ดี เพียงแต่ต้นทุนที่แปรผันเชิงเส้นตามจำนวนบันทึกการประชุมจะกลายเป็นคอขวดตั้งแต่จุดที่เกิน 200 รายการ ณ จุดนั้นจึงเปลี่ยนไปใช้ Incremental
ขั้นแรกของ Incremental คือการตัดสินอย่างแม่นยำว่า "ไฟล์ใดเปลี่ยนไป" mtime ของไฟล์เร็วก็จริง แต่แค่ touch ก็ถูกจับเป็นการเปลี่ยนแปลง ความแม่นยำจึงต่ำ แฮชของไฟล์ยึดเนื้อหาจึงแม่นยำ แต่อ่อนในการแยกแยะการเพิ่ม·การลบ คำแนะนำของผู้เขียนคือใช้เกณฑ์ git diff บันทึกแฮชคอมมิต ณ จุดที่ซิงค์ครั้งสุดท้ายไว้ แล้วประมวลผลเฉพาะไฟล์ที่เปลี่ยนหลังจากนั้น จับการเพิ่ม·แก้·ลบได้ครบถ้วนแม่นยำ ขณะที่ภาระการจัดการสถานะแยกต่างหากน้อยที่สุด
# โครงของ incremental_sync.py
def get_changed_files(last_sync_commit):
result = subprocess.run(
["git", "diff", "--name-only", last_sync_commit, "HEAD", "--", "meetings/"],
capture_output=True, text=True
)
return result.stdout.strip().split("\n")
def sync():
last_commit = read_state("last_sync_commit")
for path in get_changed_files(last_commit):
if not os.path.exists(path):
handle_deletion(path) # ลบ atom·ดัชนี·embedding ทั้งชุด
elif is_new(path, last_commit):
handle_creation(path) # lint → สกัดการตัดสินใจ → pending atom → ดัชนี → embedding
else:
handle_modification(path) # ลบล้างของที่แตกแขนงเดิมแล้วประมวลผลใหม่
write_state("last_sync_commit", get_current_commit())
ตรงนี้ยังมีการแยกสาขาอีกหนึ่งจุดที่ทำให้ต้นทุนต่างกันมากที่สุด คือบันทึกการประชุมเปลี่ยนไปถึง เนื้อหา หรือเปลี่ยนเฉพาะ frontmatter
def detect_change_scope(file_path, last_commit):
diff = subprocess.run(
["git", "diff", last_commit, "HEAD", "--", file_path],
capture_output=True, text=True
).stdout
fm_lines, body_lines = split_diff_by_section(diff)
return {"frontmatter_changed": bool(fm_lines), "body_changed": bool(body_lines)}
scope = detect_change_scope(path, last_commit)
if scope["body_changed"]:
full_reprocess(path) # รวมการสร้าง embedding ใหม่
elif scope["frontmatter_changed"]:
metadata_only_update(path) # สร้าง embedding ใหม่ 0
หากเปลี่ยนเพียงเมตาอย่าง category หรือ confidential ก็ไม่จำเป็นต้องสร้าง LLM embedding ใหม่ embedding มักเป็นก้อนต้นทุนที่ใหญ่ที่สุดของการซิงค์ การแยกสาขาเพียงครั้งเดียวนี้จึงลดต้นทุนลงได้มาก embedding ถูกแคชด้วยเกณฑ์ content_hash — หากแฮชของเนื้อหาเหมือนเดิม ก็นำ embedding ที่แคชไว้กลับมาใช้ตามเดิม และในการแก้ไขเฉพาะ frontmatter การเรียก embedding ก็เป็น 0
ทิศทาง ของความต่างของต้นทุนนั้นชัดเจน (ด้านล่างเป็นการประมาณของผู้เขียน ไม่ใช่ค่าสัมบูรณ์) ในการดำเนินงานที่มีการเปลี่ยนแปลงราว 50 รายการต่อสัปดาห์ ต้นทุน embedding ของ Incremental เมื่อเทียบกับ Full re-embed ทุกสัปดาห์ ลดลงเหลือระดับเศษหนึ่งส่วนหลายสิบ ยิ่งบันทึกการประชุมเพิ่มขึ้น ต้นทุนของ Full ก็ยิ่งโตขึ้นตามการสะสม ในทางกลับกัน ต้นทุนของ Incremental ผูกอยู่กับเฉพาะจำนวนการเปลี่ยนแปลงต่อสัปดาห์ จึงแทบราบเรียบ ไม่เกี่ยวกับการสะสม คุณสมบัติ "ไม่เกี่ยวกับการสะสม" นี้คือคุณค่าที่แท้จริงของ Incremental
Incremental เร็วก็จริง แต่แบกความเสี่ยงของความไม่ตรงกันที่สะสมไว้ หากบั๊กเล็ก ๆ ทำให้ atom รายการหนึ่งหายไป การหายไปนั้นจะไม่ถูกแก้ด้วยตัวเองใน Incremental ครั้งถัดไป จึงต้องใส่การ์ดเรล — Incremental ทุกวัน, Partial Full ของช่วง 1 สัปดาห์ล่าสุด (ตรวจสอบ) ทุกสัปดาห์, Full re-sync ทั้งหมดทุกเดือน เพื่อตรวจความสอดคล้องของดัชนี·embedding หากพบความไม่ตรงกันในการตรวจรายเดือน ก็เสริมตรรกะตรวจจับการเปลี่ยนแปลง การตรวจเดือนละ 1 ครั้งนี้คือตาข่ายนิรภัยสุดท้ายของการดำเนินงานระยะยาว
ตรงนี้ยังมีการดำเนินงานแยก PC ทับซ้อนเข้ามาอีกชั้น ผู้เขียนจัดการบันทึกการประชุมจากสองที่คือ PC บริษัทและ PC ที่บ้าน หลักการคือ ทำงาน sync จาก PC เครื่องเดียวเท่านั้น
| กระแส | การจัดการ |
|---|---|
| PC บริษัท → git push | PC บริษัทรับผิดชอบงาน sync (สร้างข้อมูลที่แตกแขนงใหม่) |
| PC ที่บ้าน → git pull | อัปเดตเฉพาะ last_sync_commit ไม่ต้องประมวลผลใหม่ |
| ทั้งสองฝั่งเปลี่ยนแล้ว merge | คำนวณไฟล์ changed ใหม่โดยยึดผลลัพธ์ของ merge |
หาก sync พร้อมกันจากทั้งสองฝั่ง สถานะ last_sync_commit จะขัดแย้งกัน และความขัดแย้งนั้นจะทำให้ดัชนีคลาดเคลื่อนอย่างเงียบ ๆ กฎเรียบง่ายที่ตรึง PC เครื่องเดียวเป็นเจ้าภาพของ sync คือการป้องกันที่แน่นอนที่สุด
การจัดหมวดหมู่·คำบรรยายภาพ·การซิงค์ไม่ใช่มาตรฐานที่ต่างคนต่างทำ ทั้งสามถูกร้อยเป็นกระแสเดียวบนไปป์ไลน์การสกัดของ §17.2
เมื่อบันทึกการประชุมถูกเขียนขึ้น category กำหนดเส้นทาง (routing) ของ promote.py ID การตัดสินใจของคำบรรยายภาพเชื่อมกับสี่ฟิลด์ของการตัดสินใจที่ decision_parser.py ดึงออกมา และข้อมูลที่แตกแขนงทั้งหมดที่ถูกสร้างขึ้นเช่นนั้น Incremental sync ก็คัดเฉพาะส่วนที่เปลี่ยนมาอัปเดต จุดเริ่มต้นของการดำเนินงานในบทนี้คือ decision_summary_not_clickup_mirror (§17.1.2) การจัดหมวดหมู่เปิดเส้นทางให้หาการตัดสินใจ คำบรรยายภาพทิ้งหลักฐานเชิงภาพของการตัดสินใจไว้ และการซิงค์รักษาสินทรัพย์การตัดสินใจนั้นให้อยู่ในสถานะเดียวกันบน PC ทั้งสอง
ความอึดอัดในบ่ายวันอังคารนั้น — สถานะที่ตกลงกันชัดเจนแล้วแต่ไม่มีเส้นทางไปถึง — จะหายไปทันทีที่สามแกนนี้ทำงาน โฟลเดอร์ถูกบีบให้แคบลงด้วย category: art ไปถึงการตัดสินใจที่ถูกต้องด้วย การตัดสินใจ: D2 ในคำบรรยายภาพ และการซิงค์ก็แสดงบันทึกการประชุมนั้นในรูปแบบเดียวกันแม้ที่บ้าน
การประยุกต์นอกเกม หลักการที่ว่าเอกสารต้องค้นหา·อ้างอิง·ซิงค์ได้จึงจะกลายเป็นสินทรัพย์ ไม่ใช่เรื่องของบันทึกการประชุมเกมเท่านั้น แต่เป็นโจทย์ร่วมของคนทำงานทุกคนที่จัดการกับเอกสาร สามแกนคือการจัดหมวดหมู่ (หมวดหมู่ที่เล็กและตั้งฉาก)·คำบรรยายภาพ (อธิบายภาพแนบหนึ่งบรรทัด)·การซิงค์ (เฉพาะส่วนที่เปลี่ยน ไม่ใช่ทั้งหมด) จะเปลี่ยนโดเมนไปก็ยังคงเดิม ตัวอย่างเช่น หากทีมขายสะสมเอกสารการประชุมลูกค้าหนึ่งปี ก็ตรึงหมวดหมู่ไว้ห้าช่องหรือน้อยกว่าอย่าง "เสนอใหม่·เจรจาสัญญา·สนับสนุนหลังการขาย" ติดหนึ่งบรรทัดอย่าง "[ภาพที่ 1] ใบเสนอราคาครั้งที่ 2 ของบริษัท A — ลดราคาต่อหน่วย 5%" กับภาพแคปเจอร์ใบเสนอราคาที่แนบทุกอัน และการซิงค์คลาวด์ก็เลือกจัดการเฉพาะไฟล์ที่เปลี่ยน เพียงเท่านี้ ครึ่งปีต่อมาคำถามว่า "ตอนนั้นทำไมถึงลดราคาให้นะ" ก็จะหาเจอได้ทันทีด้วยคำบรรยายหนึ่งบรรทัด
setup
1. นิยามหมวดหมู่การประชุมไว้ไม่เกิน 5 หมวด (ใช้ art / battle / daily / issue / review เป็นจุดเริ่มต้น แล้วแทน 1\~2 หมวดให้เข้ากับทีม)
2. เพิ่มสองการตรวจสอบลงใน meeting_lint.py — category เป็นหนึ่งในค่าที่นิยามไว้หรือไม่ และชื่อไฟล์ภาพตรงตามแพตเทิร์น <หัวข้อ>_<รายการ>_<เวอร์ชัน> (ไม่มีภาษาเกาหลี·ช่องว่าง) หรือไม่
3. เตรียมฟิลด์ confidential ใน frontmatter และไฟล์สถานะสำหรับบันทึก last_sync_commit
prompt (ช่วยจัดหมวดหมู่บันทึกการประชุมที่ขาดหาย)
ต่อไปนี้คือบันทึกการประชุม จงจัดเข้า 1 ใน 5 หมวดหมู่
[นิยามหมวดหมู่ 5 บรรทัด] / [บันทึกการประชุม 500 ตัวอักษรแรก]
รูปแบบการตอบ: คำหมวดหมู่เพียงคำเดียว ห้ามใส่คำอธิบาย·เหตุผล·ความไม่แน่นอนใด ๆ
หากคำตอบไม่ใช่ 1 ใน 5 หมวดหมู่ จะถือว่าระบบล้มเหลว
verify
1. ลองค้นหาดูด้วยตัวเองว่าบันทึกการประชุมจาก 6 เดือนก่อนรายการใดก็ได้ ค้นหาเจอด้วยหมวดหมู่ + ID การตัดสินใจของคำบรรยายภาพเท่านั้นหรือไม่
2. ตรวจสอบว่าจำนวนไฟล์ที่เปลี่ยนซึ่งจับได้ด้วย git diff --name-only <last_sync_commit> HEAD ตรงกับจำนวนบันทึกการประชุมที่แก้ไขจริงหรือไม่
3. อย่ารับผลการจัดหมวดหมู่ของ AI โดยไม่วิพากษ์ ให้คนดูเคส uncertain และ "การตัดสินใจที่เกิดขึ้นกลาง daily" อีกครั้ง
หากเป็นนักออกแบบเกมที่ทำงานคนเดียว ก็ลดทอนแบบนี้
decision (การประชุมที่มีการตัดสินใจ) และ log (บันทึกความคืบหน้า) เก็บคำบรรยายภาพ·atom เฉพาะการประชุมที่มีการตัดสินใจ ส่วนที่เหลือกองไว้ในโฟลเดอร์ตามวันที่decision_index.json (แมป ID การตัดสินใจ → พาธบันทึกการประชุม) แล้วอัปเดตเฉพาะบรรทัดนั้นตอนบันทึกการประชุม git ก็คือการซิงค์ในตัว และก่อนที่ปริมาณจะสะสมมากพอที่จะต้องแยก Full/Incremental การสร้างใหม่ทั้งหมดทุกครั้งก็เพียงพอหัวใจที่ไม่เปลี่ยนแม้ในขนาดคนเดียวมีเพียงหนึ่งเดียว — การทิ้งเส้นทางที่จะไปถึงการตัดสินใจไว้ การจัดหมวดหมู่·คำบรรยายภาพ·การซิงค์เป็นเพียงสามเสาที่ค้ำเส้นทางนั้น จะตั้งให้บางลงแค่ไหนตามขนาดก็ได้ทั้งนั้น
มื้อกลางวันก่อนเดโมไมล์สโตนสามวัน นักออกแบบเกมคนหนึ่งวางถาดอาหารลงแล้วถามขึ้นมาว่า "เรื่องที่ตกลงว่าจะปรับโกลด์รางวัลเควสต์ขึ้นเป็น 1.5 เท่า อันนั้นเป็นมติจากในที่ประชุมจริงใช่ไหม ใส่ลงในชีตข้อมูลได้เลยไหม" คนข้างๆ ตอบกลับว่า "อันนั้นมันเป็นแค่ที่ใครสักคนเสนอว่าลองทำดูไหมไม่ใช่เหรอ" ไฟล์บันทึกเสียงความยาว 90 นาที กับเมโมสองหน้าที่มีคนพิมพ์ไว้สดๆ ด้วยคีย์บอร์ดนั้นมีอยู่จริงแน่นอน แต่บันทึกเหล่านั้นเก็บไว้แค่ว่า "พูดคุยเรื่องอะไรบ้าง" ไม่ได้เก็บว่า "ตัดสินใจอะไร ใครรับผิดชอบ และทำไมถึงตัดสินใจเช่นนั้น"
ตอนที่ผู้เขียนนำเอกสาร R&D ของบริษัท 17 ฉบับมาเรียงลำดับตามความเจ็บปวด สิ่งที่ครองสัดส่วนใหญ่ที่สุดคือแผนปรับปรุงบันทึกการประชุม ซึ่งเหนือความคาดหมาย มันไม่ใช่การปรับสมดุลการต่อสู้ ไม่ใช่ไปป์ไลน์ผลิตเนื้อหา จุดที่เจ็บที่สุดมีเพียงข้อเดียว นั่นคือการที่มติซึ่งตัดสินใจในที่ประชุมไม่ถูกส่งต่อไปสู่การลงมือทำ
ด้วยเหตุนี้ ผู้เขียนจึงออกแบบระบบบันทึกการประชุมขึ้นใหม่ให้เป็นฐานข้อมูลติดตามการตัดสินใจ แล้วลงมือดำเนินการเองตลอด 6 เดือนเพื่อตรวจสอบว่าควรใส่ AI เข้าไปตรงไหนของกระแสงานนี้ และตรงไหนไม่ควรใส่ บทนี้คือแผนที่ของ 5 จุดนั้น
ตั้งแต่ไฟล์บันทึกเสียงไปจนถึงการอัปเดตกราฟการตัดสินใจ ตำแหน่งที่สามารถวางผู้ช่วย AI ลงไปได้ตลอดทั้งไปป์ไลน์บันทึกการประชุมมีอยู่พอดี 5 จุด เราไม่ได้ใส่เข้าไปพร้อมกันทั้ง 5 จุด เพราะแต่ละตำแหน่งมีระดับความสุกงอมและความเสี่ยงเชิงความคิดที่แตกต่างกัน
flowchart TD
A[เสียงบันทึกการประชุม] -->|"จุดที่ 1
STT"| B[ข้อความต้นฉบับ]
B -->|"จุดที่ 2
สร้างร่าง"| C[ร่างบันทึกการประชุม]
C -->|"meeting_lint.py"| D[บันทึกการประชุมรูปแบบมาตรฐาน]
D -->|"จุดที่ 3
เติมช่องการตัดสินใจ"| E[การตัดสินใจครบ 4 ฟิลด์]
E -->|"decision_parser.py
จุดที่ 4 จัดเส้นทาง"| F[pending atom]
F -->|"promote.py
จุดที่ 5 สกัดความสัมพันธ์"| G[atom ตัวจริง + กราฟ]
style C fill:#ffd9d9,stroke:#c0392b
style E fill:#d9f0ff,stroke:#2980b9
style F fill:#d9f0ff,stroke:#2980b9
สีแดง (จุดที่ 2) คือตำแหน่งที่น่าดึงดูดที่สุดและอันตรายที่สุดในเวลาเดียวกัน ส่วนสีน้ำเงิน (จุดที่ 3 และ 4) คือตำแหน่งที่ปลอดภัยซึ่งนำมาใช้เป็นลำดับแรกสุด ตรงกลางไปป์ไลน์ที่เป็น meeting_lint.py → decision_parser.py → promote.py นั้นไม่ใช่ AI แต่เป็นสคริปต์เชิงกำหนด (deterministic) AI จะเข้าไปอยู่เฉพาะใน "ช่องว่างที่ต้องใช้การตัดสินใจ" ระหว่างโครงกระดูกเชิงกำหนดเหล่านี้เท่านั้น
หากสรุปลักษณะของแต่ละจุดเป็นบรรทัดเดียว จะได้ดังนี้
ก่อนจะพูดถึงการทำงานอัตโนมัติด้วย AI เราต้องดูโครงกระดูกของสคริปต์ที่ไม่ใช่ AI ก่อน เพราะหัวใจของการทำให้บันทึกการประชุมกลายเป็นฐานข้อมูลการตัดสินใจนั้นไม่ได้อยู่ที่ LLM แต่อยู่ที่สคริปต์ Python เล็กๆ สามตัว
บันทึกการประชุมรูปแบบมาตรฐานจะมีบล็อกการตัดสินใจอยู่ตอนท้าย แต่ละการตัดสินใจในบล็อกบังคับให้มีสี่ฟิลด์
## Decisions
D1:
decision: กำหนดคูลดาวน์รวมของการต่อสู้ (GCD) ให้เป็น 0.5 วินาทีเหมือนกันทั้งหมด
owner: teammate_a
rationale: ในการทดสอบการต่อสกิล 0.3 วินาทีมักทำให้อินพุตหลุดบ่อย (บันทึกการประชุม 14:22)
follow_up: สะท้อน GCD 0.5 ลงในชีตออกแบบคอมโบ ภายใน 6/13
บล็อกนี้จะถูกอ่านโดย decision_parser.py การทำงานหลักนั้นเรียบง่าย — ถ้าหนึ่งในสี่ฟิลด์ว่างแม้แต่ช่องเดียว ก็จะแสดง [MISSING] เพื่อรายงาน โดยเฉพาะถ้าไม่มี owner การตัดสินใจนั้นจะเป็น "การตัดสินใจที่ไม่มีใครรับผิดชอบ" กล่าวคือเป็นการตัดสินใจที่จะไม่ถูกลงมือทำ จึงถูกขวางกั้นอย่างเข้มงวดที่สุด
$ python decision_parser.py 2026-06-06_combat-sync.md
D1: OK (owner=teammate_a)
D2: [MISSING owner] "พิจารณายกเว้น GCD สำหรับสกิลฟื้นฟู" — ไม่มี owner, บล็อกการเลื่อนขั้น
D3: [MISSING rationale] ฟิลด์เหตุผลว่างเปล่า, เตือน
D2 ที่ถูกแสดง [MISSING owner] ไม่อาจไปได้แม้กระทั่งโฟลเดอร์ pending มันจะไม่ถูกนับเป็นการตัดสินใจจนกว่าคนจะเติม owner ลงไป นี่คือกลไกที่ขวางกั้นเชิงโครงสร้างไม่ให้เกิดสภาพ "ประชุมไปแล้วแต่ไม่มีอะไรขับเคลื่อน"
การตัดสินใจที่ผ่านแล้วจะถูก promote.py แปลงเป็น pending atom และเมื่อคนอนุมัติที่ด่านรีวิวสัปดาห์ละครั้ง ก็จะถูกเลื่อนขั้นเป็น atom ตัวจริง หลักการที่ใช้ในขั้นตอนนี้คือ atom decision_summary_not_clickup_mirror (§17.1.2) บอร์ดงานติดตาม "ว่าจะทำอะไร" ส่วนฐานข้อมูลการตัดสินใจติดตาม "ว่าทำไมถึงตัดสินใจเช่นนั้น" ถ้าผสมสองอย่างนี้เข้าด้วยกัน ทั้งสองอย่างจะพังลง
สคริปต์สามตัวนี้คือโครงกระดูก และ AI คือผู้ช่วยที่เติมช่องว่างของโครงกระดูกนี้ ถ้าลำดับกลับหัวกลับหาง — ถ้าให้ AI สร้างโครงกระดูก — อาการหลอน (hallucination) จะทำลายความน่าเชื่อถือของฐานข้อมูลการตัดสินใจลงทั้งหมด
จุดที่ 2 (สร้างร่างบันทึกการประชุมอัตโนมัติจากข้อความ STT) คือตำแหน่งที่ทุกทีมอยากทำเป็นอันดับแรกสุด เพราะภาพที่ว่า "แค่โยนไฟล์บันทึกเสียงเข้าไป ก็ได้บันทึกการประชุมออกมา" นั้นน่าดึงดูดเกินไป และก็เพราะความน่าดึงดูดนั้นเองที่ทำให้มันล้มเหลวอย่างแพงที่สุด
รูปแบบความล้มเหลวมีสี่อย่าง
flowchart TD
R[ข้อความ STT → ร่างจาก AI] --> R1["การตัดสินใจที่หลอน
บันทึกสิ่งที่ยังไม่ได้ตกลงเป็นมติ"]
R --> R2["การตัดสินใจตกหล่น
ปล่อยให้มติจริงหลุดลอยไป"]
R --> R3["ระบุผู้พูดผิด
สลับตัวผู้เสนอ"]
R --> R4["ปรับโทนให้เรียบเสมอกัน
ความเห็นค้าน·นัยยะหายไป"]
R1 --> X["เหตุผลในการดำรงอยู่ของบันทึกการประชุม
คือการติดตามการตัดสินใจ พังทลาย"]
R2 --> X
R3 --> X
R4 --> X
style X fill:#ffd9d9,stroke:#c0392b
ในบรรดานี้ ที่ร้ายแรงที่สุดคือการตัดสินใจที่หลอน ในที่ประชุมมีใครสักคนเพียงแค่เสนอความเห็นว่า "คูลดาวน์รวมเป็น 0.5 วินาทีน่าจะดีกว่าไหม" แต่ร่างจาก AI กลับเขียนลงไปว่า "ตกลงให้คูลดาวน์รวมเป็น 0.5 วินาที" สามสัปดาห์ต่อมา บรรทัดนี้ถูกสะท้อนลงในชีตข้อมูล การออกแบบคอมโบถูกซ้อนทับลงไปบนนั้น และเคส QA ถูกเขียนขึ้น การตัดสินใจที่ไม่เคยมีการตกลงกันถูกส่งต่อไปอย่างย้อนกลับไม่ได้
ด้วยเหตุนี้ จุดที่ 2 จึงมีหลักการเด็ดขาดบังคับใช้
ไม่ได้หมายความว่าจุดที่ 2 จะ "ไม่ทำไปตลอดกาล" หลังจากที่จุดที่ 3, 4 และ 1 เสถียรแล้ว และผู้ดำเนินการประชุมได้เรียนรู้ขีดจำกัดของผลลัพธ์ AI ด้วยตัวเองแล้ว คุณค่าของการนำจุดที่ 2 มาใช้ก็มีมากพอ เพียงแต่ว่า ลำดับมันอยู่ท้ายสุด เท่านั้นเอง
ตรงนี้คือตำแหน่งที่ให้ผลมากที่สุดในการดำเนินงาน 6 เดือน คนประกาศการมีอยู่ของการตัดสินใจ และ AI เติมฟิลด์ส่วนประกอบของการตัดสินใจนั้น จุดที่ต่างจากจุดที่ 2 อย่างเด็ดขาดคือ คนตอกหมุดข้อเท็จจริงที่ว่ามีการตัดสินใจอยู่ลงไปก่อน
สามสิ่งที่ถ้าให้คนเติมด้วยมือเองจะใช้เวลามากเกินไป AI จะร่างให้
หัวใจอยู่ที่การที่พรอมต์ บังคับให้อ้างอิงเหตุผลและห้ามอาการหลอนอย่างชัดเจน ต่อไปนี้คือพรอมต์เติมช่องฉบับเต็มที่ใช้ดำเนินการจริง
พรอมต์ (คนป้อน):
ดูเนื้อหาการประชุมด้านล่าง แล้วเติมเฉพาะสี่ช่องคือ
owner / rationale / affected_atoms / follow_up ของการตัดสินใจ D1 ("กำหนดคูลดาวน์รวมของการต่อสู้ให้เป็น 0.5 วินาทีเหมือนกัน") ที่ผู้ดำเนินการประชุมประกาศไว้
อย่าสร้างการตัดสินใจขึ้นมาใหม่ rationale ให้อ้างอิงคำพูดในเนื้อหาพร้อม timestamp ตามตรง
ช่องที่ไม่มีเหตุผลรองรับ อย่าเดา ให้เขียนว่า "ไม่มีเหตุผลรองรับ"
// (เจตนา: affected_atoms เอาเฉพาะระบบ·ชีตที่ถูกกล่าวถึงในเนื้อหา สูงสุด 3 รายการ)
[เนื้อหาการประชุม]
(14:18) teammate_a: ตอนนี้ GCD อยู่ที่ 0.3 วินาที พอกดสกิลรัวๆ อินพุตมันชอบหลุดอยู่เรื่อย
(14:22) teammate_b: เมื่อวานในการทดสอบคอมโบเองก็มีเคสที่ 0.3 วินาทีแล้วสกิลตัวที่สอง
ไม่ออก ประมาณ 30 เปอร์เซ็นต์ ผมเก็บล็อกไว้แล้ว
(14:25) teammate_a: งั้นเพิ่มเป็น 0.5 วินาทีเลยครับ เดี๋ยวผมไปสะท้อนในชีตคอมโบเอง
(14:26) ผู้ดำเนินการประชุม: เอา 0.5 วินาที เป็นมติ
(14:31) teammate_c: ถ้าใช้ GCD กับสกิลฟื้นฟูด้วย เวลาฉุกเฉินจะใช้ไม่ได้ ตรงนั้นจะทำยังไง...
(14:33) ผู้ดำเนินการประชุม: อันนั้นไว้ดูแยกต่างหากคราวหน้า
ผลลัพธ์จาก Claude (ดิบตามต้นฉบับ):
1. owner: teammate_a
เหตุผล: (14:25) "เดี๋ยวผมไปสะท้อนในชีตคอมโบเอง" — teammate_a
ระบุว่าเป็นความรับผิดชอบของตนเอง
2. rationale:
- (14:18) teammate_a: "GCD 0.3 วินาที พอกดสกิลรัวๆ อินพุตหลุด"
- (14:22) teammate_b: "ในการทดสอบคอมโบ 0.3 วินาทีมีเคสสกิลตัวที่สองไม่ออก
ประมาณ 30% เก็บล็อกไว้"
→ ปัญหาอินพุตหลุดที่ 0.3 วินาทีเป็นเหตุผลโดยตรงของการตัดสินใจ 0.5 วินาที
3. affected_atoms (ตัวเลือกจากการกล่าวถึงในเนื้อหา):
- atoms/combat/global_cooldown
- ชีตออกแบบคอมโบ (เนื้อหา 14:25 กล่าวถึง "ชีตคอมโบ")
- atoms/combat/skill_recovery ← แต่ที่ (14:31) การยกเว้น GCD ของสกิลฟื้นฟู
ถูก "เลื่อนไปคราวหน้า" (14:33) ยังไม่แน่ใจว่าจะรวมไว้ในขอบเขตผลกระทบ
ของการตัดสินใจนี้หรือไม่ เหตุผลอ่อน ต้องให้คนยืนยัน
4. follow_up:
- teammate_a: สะท้อน GCD 0.5 วินาที ลงในชีตออกแบบคอมโบ
- [วาระแยกต่างหาก] เรื่องการยกเว้น GCD ของสกิลฟื้นฟูอยู่นอกขอบเขตการตัดสินใจนี้
แยกเป็นวาระของการประชุมครั้งถัดไป (คำพูดผู้ดำเนินการ 14:33)
การตรวจสอบ/ปฏิเสธของคน:
ผู้ดำเนินการประชุมตรวจสอบผลลัพธ์ owner และ rationale มีการอ้างอิงถูกต้องจึงรับมาใช้ตามนั้น ส่วนตัวเลือกตัวที่สาม skill_recovery ของ affected_atoms นั้น AI รายงานเองว่า "เหตุผลอ่อน ต้องให้คนยืนยัน" และด้วยดุลพินิจของผู้ดำเนินการประชุม จึงถูก ตัดออก จากขอบเขตผลกระทบของการตัดสินใจนี้ — การยกเว้นของสกิลฟื้นฟูเป็นเรื่องที่จะกลายเป็นการตัดสินใจแยกต่างหาก ไม่ใช่ผลกระทบของ D1 ครั้งนี้ ส่วนข้อเสนอ "แยกเป็นวาระต่างหาก" ของ follow_up ก็รับมาใช้แล้วลงทะเบียนเป็นวาระของการประชุมครั้งถัดไป
สิ่งสำคัญตรงนี้คือ AI ไม่ได้ดันรายการที่ไม่แน่ใจให้ผ่านด้วยอาการหลอน แต่ รายงานความไม่แน่นอนด้วยตัวเอง ข้อจำกัดของพรอมต์ที่ว่า "ห้ามเดา·ห้ามหลอน ถ้าไม่มีเหตุผลรองรับให้ระบุว่าไม่มีเหตุผลรองรับ" คือสิ่งที่สร้างผลลัพธ์อันซื่อตรงนี้ขึ้นมา ถ้าถอดข้อจำกัดออก AI จะใส่ skill_recovery ลงใน affected_atoms อย่างมั่นใจ และอาการหลอนนั้นก็จะถูกส่งต่อไปสู่กราฟ
บล็อกการตัดสินใจที่ตรวจสอบเสร็จแล้วจะผ่าน decision_parser.py ไปได้ — เพราะทั้งสี่ฟิลด์ถูกเติมครบ จึงไม่มี [MISSING] — และส่งต่อไปเป็น pending atom
เมื่อ pending atom ที่ผ่านจุดที่ 3 จะถูกเลื่อนขั้นไปยังโฟลเดอร์ตัวจริง AI จะแนะนำว่าควรส่งไปยังโฟลเดอร์ไหน (จุดที่ 4)
ช่วยเลือกว่าควรใส่ atom นี้ ("กำหนดคูลดาวน์รวมของการต่อสู้เป็น 0.5 วินาทีเหมือนกัน / owner teammate_a")
ลงในโฟลเดอร์ไหนด้านล่าง โดยเรียงลำดับความสำคัญสูงสุด 3 รายการ อย่าเสนอให้สร้างโฟลเดอร์ใหม่
ให้เลือกเฉพาะภายในรายการนี้เท่านั้น
- atoms/combat/ atoms/character/ atoms/operations/ atoms/visual/
"ห้ามสร้างโฟลเดอร์ใหม่" คือข้อจำกัดหลัก ถ้าถอดอันนี้ออก AI จะเสนอโฟลเดอร์อย่าง atoms/combat_timing/, atoms/gcd_rules/ ไม่รู้จบ ทำให้หมวดหมู่เพิ่มทวีคูณอย่างไร้ขีดจำกัด จนการค้นหาและการแทรกอัตโนมัติพังลง หลักการคือต้องรักษาหมวดหมู่ให้เล็กและตั้งฉากกัน (orthogonal) และคงที่ไม่เปลี่ยนแปลงนานกว่าหนึ่งปี AI เลือกได้เฉพาะภายในรายการปิดนั้นเท่านั้น
จุดที่ 5 (การสกัดความสัมพันธ์ระหว่าง atom) เป็นจุดที่นำมาใช้ช้าที่สุดและรอบคอบที่สุด เป็นตำแหน่งที่อนุมานความสัมพันธ์เชิงพึ่งพาระหว่าง atom ที่ถูกเลื่อนขั้นแล้ว
atom ใหม่ A: "สกิลฟื้นฟูได้รับการยกเว้นไม่ใช้คูลดาวน์รวม"
atom เดิม B: "กำหนดคูลดาวน์รวมเป็น 0.5 วินาทีเหมือนกัน"
ความสัมพันธ์ที่อนุมานได้:
A.exception_of: [B]
A.derives_from: [B]
B.affects: [A] ← กำหนดทิศย้อนกลับโดยอัตโนมัติ
ปัญหาคือการอนุมานนี้เผชิญหน้าโดยตรงกับความไม่เป็นเชิงกำหนดของ LLM อินพุตเดียวกันแต่เมื่อวานกับวันนี้ให้ความสัมพันธ์ต่างกัน กลไกบรรเทามีสามอย่าง — ตั้ง temperature=0 และล็อก seed ในโมเดลที่ทำได้, ด่านตรวจสอบที่เสนอตัวเลือกแล้วให้คนอนุมัติ, และวิธีที่สกัดเฉพาะทิศทางเดียวก่อน แล้วทิศย้อนกลับให้สคริปต์ปรับชดเชยเชิงกำหนด เพราะถ้ายกทั้งสองทิศให้ LLM ทำ จะมีฝั่งใดฝั่งหนึ่งตกหล่นไป
การเปิด 5 จุดพร้อมกันคือความล้มเหลวที่พบบ่อยที่สุดและแพงที่สุด ภาระการดำเนินงานมาถึงก่อนผลลัพธ์ จนทีมทิ้งทั้งระบบไปทั้งหมด ต่อไปนี้คือลำดับที่ทำตามจริง
การวางจุดที่ 2 ไว้ช้าที่สุดคือหัวใจของลำดับนี้ ทำตำแหน่งที่อยากทำที่สุดเป็นอันดับสุดท้าย — แม้จะขัดกับสัญชาตญาณ แต่การจัดให้มือที่ชำนาญที่สุดไปอยู่ที่ด่านตรวจสอบที่อันตรายที่สุด คือหลักความปลอดภัยของสถานที่ทำงาน
ในแง่ต้นทุนเองลำดับนี้ก็สมเหตุสมผล ในเกณฑ์การประชุม 100 ครั้ง/เดือน จุดที่ 3 อยู่ที่ราว $5\~10 จุดที่ 4 อยู่ที่ระดับ $1\~2 (ประมาณการจากสภาพแวดล้อมการดำเนินงานของผู้เขียน ยังไม่ได้ตรวจสอบ) ดังนั้น แค่เปิดสองจุดก็ต่ำกว่า $10 ต่อเดือน สองตำแหน่งที่ให้ผลมากที่สุดคือสองตำแหน่งที่ถูกที่สุด
ความแตกต่างเมื่อบันทึกการประชุมเดียวกันด้วยสองวิธีคือบทสรุปของทั้งบทนี้
Before — บันทึกการประชุมแบบบรรยายอิสระ (ไม่มี AI หรือกรณีที่ยกจุดที่ 2 ให้ทำถึงช่องการตัดสินใจ):
## ประชุมซิงค์การต่อสู้ 2026-06-06
มีการพูดคุยเรื่อง GCD มีความเห็นว่า 0.3 วินาทีสั้นเกินไป
บอกว่าในการทดสอบคอมโบมีปัญหา มีการพูดถึง 0.5 วินาที
มีการกล่าวถึงการยกเว้นสกิลฟื้นฟูสั้นๆ ด้วย
บรรยากาศโดยรวมเป็นไปในทิศทาง 0.5 วินาที
สามสัปดาห์ต่อมา เมื่อเปิดบันทึกการประชุมนี้ขึ้นมาอีกครั้ง ไม่มีใครกู้คืนได้ ว่า "บรรยากาศที่เป็นไปในทิศทาง 0.5 วินาที" นั้นเป็นมติหรือเป็นความเห็น ใครรับเอาไปสะท้อนในชีต การยกเว้นสกิลฟื้นฟูถูกตัดสินใจแล้วหรือถูกเลื่อนไป ไม่มีตัวผู้พูด ไม่มี owner และเหตุผลก็อยู่ตรงไหนสักแห่งในเนื้อหา ต้องกลับไปฟังบันทึกเสียงอีกครั้ง
After — บันทึกการประชุมแบบช่องการตัดสินใจ + เติมด้วยจุดที่ 3:
## ประชุมซิงค์การต่อสู้ 2026-06-06
### สรุปวาระ (ผู้ช่วย AI)
- ปัญหาอินพุตหลุดของคูลดาวน์รวมของการต่อสู้ (GCD) ที่ 0.3 วินาที
- เรื่องการยกเว้น GCD ของสกิลฟื้นฟู (แยกเป็นวาระต่างหาก)
### Decisions (คนประกาศ + AI เติม)
D1:
decision: กำหนดคูลดาวน์รวมของการต่อสู้ให้เป็น 0.5 วินาทีเหมือนกันทั้งหมด
owner: teammate_a
rationale: |
- (14:18) teammate_a: 0.3 วินาที พอกดสกิลรัวๆ อินพุตหลุด
- (14:22) teammate_b: ทดสอบคอมโบ 0.3 วินาที สกิลตัวที่สองไม่ออก ~30%, มีล็อก
follow_up: teammate_a — สะท้อน GCD 0.5 วินาที ลงในชีตออกแบบคอมโบ (ภายใน 6/13)
affected_atoms: [atoms/combat/global_cooldown, ชีตออกแบบคอมโบ]
### วาระที่แยกออกมา
- การยกเว้น GCD ของสกิลฟื้นฟู → การประชุมครั้งถัดไป (มติผู้ดำเนินการ 14:33)
สามสัปดาห์ต่อมา บันทึกการประชุมนี้ถูก decision_parser.py อ่านและเชื่อมเข้ากับกราฟแล้ว และใครก็ตามที่ถามว่า "ทำไมถึง 0.5 วินาที" ก็สามารถตอบได้ทันทีด้วยคำอ้างอิงสองบรรทัดของ rationale เนื่องจาก owner ถูกระบุไว้ จึงติดตามได้ว่า follow_up ถูกลงมือทำหรือยัง และยังรักษาข้อเท็จจริงที่ว่าการยกเว้นสกิลฟื้นฟู ไม่ใช่มติ แต่เป็นวาระที่ถูกเลื่อนไป เอาไว้ด้วย
สิ่งที่สร้างความแตกต่างไม่ใช่ปริมาณงานของ AI แต่เป็น โครงสร้างที่รักษาตำแหน่งที่คนประกาศการตัดสินใจเอาไว้ แล้วยกให้ AI ทำแค่การเติมเหตุผล ในบันทึกการประชุม After ข้างต้น ถ้าลบย่อหน้าที่ AI เติมทั้งหมด (สรุปวาระ, การอ้างอิง rationale, ตัวเลือก affected_atoms) ออกไป สิ่งที่เหลือคือการตัดสินใจหนึ่งบรรทัดกับ owner เท่านั้น — ปริมาณข้อมูลของบันทึกการประชุมกว่าครึ่งหนึ่งมาจากการเติมของ AI แต่หัวใจอยู่ที่ว่าครึ่งหนึ่งนั้นทั้งหมดเป็นการอ้างอิงเหตุผลที่ผ่านการตรวจสอบโดยมนุษย์แล้ว
การประยุกต์นอกเกม หลักการที่ว่า "การมีอยู่ของการตัดสินใจให้คนประกาศ และ AI เติมแค่เหตุผล·ผู้รับผิดชอบ·ผลกระทบ" ไม่ใช่เรื่องของเกม แต่เป็นเส้นความปลอดภัยที่ใช้ได้ตรงๆ กับคนทำงานทุกคนที่จัดระเบียบบันทึกเสียงด้วย AI เหตุผลที่ตำแหน่งซึ่งน่าดึงดูดที่สุด (สร้างบันทึกการประชุมอัตโนมัติจากบันทึกเสียงทั้งก้อน) เป็นตำแหน่งที่อันตรายที่สุด ก็เพราะอาการหลอนที่ AI แปลงความเห็นที่ว่า "0.5 วินาทีน่าจะดีกว่าไหม" ให้กลายเป็นการตัดสินใจที่ว่า "ตกลง 0.5 วินาที" ยกตัวอย่างเช่น เมื่อฝ่ายบุคคลจัดระเบียบบันทึกเสียงการประชุมประเมินผล ให้ผู้ดำเนินการประชุมตอกหมุดเฉพาะการตัดสินใจที่ว่า "ยืนยันเป็นเกรด B" ด้วยตัวเอง ส่วน AI ให้สั่งแค่ว่า "ช่วยอ้างอิงคำพูดที่เป็นเหตุผลของเกรดนี้จากบันทึกเสียง ถ้าไม่มีให้บอกว่าไม่มี" เท่านั้น ถ้าปล่อยให้ AI สร้างการตัดสินใจ การประเมินที่ไม่เคยมีการตกลงกันจะหลงเหลืออยู่ในประวัติบุคคลอย่างย้อนกลับไม่ได้
setup
1. สร้างบล็อก ## Decisions ในรูปแบบมาตรฐานของบันทึกการประชุม และบังคับให้แต่ละการตัดสินใจมี 4 ฟิลด์คือ decision / owner / rationale / follow_up
2. เขียน decision_parser.py — ถ้าหนึ่งในสี่ฟิลด์ว่างแม้แต่ช่องเดียวให้แสดง [MISSING <ฟิลด์>] และโดยเฉพาะถ้า owner ว่างให้ขวางกั้นการเลื่อนขั้น
3. กำหนดเป็นกฎไว้ให้ชัดเจน (decision_summary_not_clickup_mirror) ว่าบทสรุปการตัดสินใจไม่ใช่กระจกเงาของบอร์ดงาน แต่เป็นทรัพย์สินอิสระที่บรรจุ "ทำไม"
prompt 4. เขียนพรอมต์เติมช่องของจุดที่ 3 ข้อจำกัดที่ต้องใส่ให้ครบ: "ห้ามสร้างการตัดสินใจขึ้นมาใหม่ / อ้างอิงเหตุผลในเนื้อหาพร้อม timestamp / ถ้าไม่มีเหตุผลรองรับให้ระบุว่า 'ไม่มีเหตุผลรองรับ' / ห้ามเดา·ห้ามหลอน" โดยเรียกร้องสี่ช่องคือ rationale·owner·affected_atoms·follow_up 5. ในพรอมต์จัดเส้นทางของจุดที่ 4 ให้ใส่ "ห้ามสร้างโฟลเดอร์ใหม่ที่ไม่อยู่ในรายการ + รายการโฟลเดอร์แบบปิด"
verify
6. รันบล็อกการตัดสินใจที่เติมแล้วผ่าน decision_parser.py เพื่อยืนยันว่าไม่มี [MISSING]
7. ให้คนตรวจสอบรายการที่ถูกรายงานว่า "เหตุผลอ่อน" ในบรรดา affected_atoms ที่ AI เติม แล้วตัดออก/รับเข้าด้วยตัวเอง ไม่ทำ commit อัตโนมัติในทุกกรณี
ฉบับย่อสำหรับคนเดียว
ถ้าทำงานคนเดียวหรือไม่มีเวลาติดตั้งเครื่องมือ ก็แค่เขียนบล็อกการตัดสินใจสี่บรรทัด (การตัดสินใจ / ผู้รับผิดชอบ / เหตุผล / แอ็กชันถัดไป) ด้วยมือไว้ท้ายบันทึกการประชุม โดยไม่ต้องมีสคริปต์ก็ได้ แม้ owner จะเป็นตัวคุณเองก็เขียนชื่อลงไป ส่วน AI ให้สั่งแค่ว่า "ช่วยอ้างอิงเหตุผลของการตัดสินใจนี้จากเมโมการประชุม ถ้าไม่มีให้บอกว่าไม่มี" แม้จะไม่มีไปป์ไลน์ เพียงแค่สองอย่างคือ ตำแหน่งที่ประกาศการตัดสินใจ กับพรอมต์ที่บังคับให้อ้างอิงเหตุผล บันทึกการประชุมก็จะเริ่มกลายเป็นฐานข้อมูลการตัดสินใจ
เหตุการณ์เกิดขึ้นกลางการประชุมรายไตรมาส ดีไซเนอร์ฝ่ายต่อสู้เสนอว่า "เรามากำหนด global cooldown ให้เป็น 0.5 วินาทีเหมือนกันทั้งหมดเถอะ" ทุกคนพยักหน้าเห็นด้วย แต่แล้วซีเนียร์ที่นั่งข้าง ๆ ก็ยกมือขึ้น "อันนี้มันขัดกับที่เราตัดสินใจไว้เป็น 0.3 วินาทีในไตรมาส 4 ปีที่แล้วไม่ใช่เหรอ ตอนนั้นทำไมถึงเลือก 0.3 วินาทีนะ" ห้องประชุมเงียบไปครู่หนึ่ง ไม่มีใครจำเหตุผลของการตัดสินใจครั้งนั้นได้เลย ค้นบันทึกการประชุมแล้วก็เจอแค่บรรทัดเดียวว่า "หารือใน TF ฝ่ายต่อสู้" สุดท้ายต้องใช้เวลา 30 นาทีไปกับการรื้อฟื้นการตัดสินใจของปีที่แล้ว และถึงอย่างนั้นก็ยังหาคำตอบไม่ได้ว่า "ทำไมถึงเป็น 0.3"
การตัดสินใจนั้นติดตามยากกว่าตัดสินใจ เมื่อมีการตัดสินใจสะสมหลายร้อยครั้งต่อปี สมองของคนเราก็ตามไม่ทันว่าการตัดสินใจไหนยังมีผล อันไหนถูกยกเลิกไปแล้ว และอันไหนวางอยู่บนสมมติฐานของอีกอันหนึ่ง บทนี้ว่าด้วยระบบที่เปลี่ยนการตัดสินใจให้กลายเป็น atom ที่ตรึงไว้เป็นสินทรัพย์ซึ่งติดตามได้ แก่นของมันเรียบง่าย คือบันทึกการตัดสินใจหนึ่งครั้งเป็นการ์ดที่มี decision_id·owner·rationale แล้วเชื่อมการ์ดเข้าด้วยกันด้วย wikilink เพื่อสร้างกราฟ จากนั้นใช้ grep ย้อนรอยว่าผลกระทบแผ่ขยายไปถึงไหน
หน่วยที่เล็กที่สุดของการติดตามการตัดสินใจคือการ์ดการตัดสินใจ (decision card) ผู้เขียนนำการ์ดหนึ่งใบที่ใช้งานจริงในโปรเจกต์ A (การพัฒนา MMORPG) ซึ่งผู้เขียนดูแลอยู่มาให้ดูตามต้นฉบับ นี่คือการตัดสินใจกำหนด 0.5 วินาทีเหมือนกันทั้งหมดที่เกิดความขัดแย้งในการประชุมข้างต้นนั่นเอง
---
decision_id: D2026_Q2_017
title: กำหนด global cooldown ฝ่ายต่อสู้ให้เป็น 0.5 วินาทีเหมือนกันทั้งหมด
type: system_change
status: active # active / superseded / deprecated
created: 2026-04-18
owner: teammate_a # ดีไซเนอร์ฝ่ายต่อสู้ ผู้เสนอและเจ้าของการตัดสินใจ
approved_by: 이민수 # Design Director
approval_meeting: 95_BattleTF_2026-04-18
scope:
- combat_system
- all_active_skills
content: |
ใช้ global cooldown 0.5 วินาทีกับสกิลแอ็กทีฟฝ่ายต่อสู้ทั้งหมด
สกิลฟื้นฟูเป็นข้อยกเว้น (การตัดสินใจแยกต่างหาก D2026_Q2_018)
rationale:
- ปัญหาความอ่านง่ายของการป้อนคอมโบ (ฟีดแบ็กผู้ใช้สะสม)
- ทิศทางที่ทำให้ความยาวเฉลี่ยของการต่อสู้ในซิมเพิ่มขึ้น
- ทำให้เส้นโค้งการเรียนรู้ของผู้ใช้ใหม่ราบเรียบขึ้น
affected_atoms:
- combat_global_cooldown_constant
- combat_skill_cooldown_rule
affected_files:
- CombatBalance.xlsx
- CombatFormula_v3.md
- UI/skill_cooldown_indicator
implementation:
target_build: 2026-05-09
impl_owner: teammate_b # หัวหน้าทีมโค้ด
qa_owner: teammate_c # QA ซีเนียร์
related_decisions:
- supersedes: D2025_Q4_034 # การตัดสินใจ 0.3 วินาทีครั้งก่อน
- relates_to: D2026_Q2_018 # ข้อยกเว้นสกิลฟื้นฟู
---
สามช่องคือกระดูกสันหลัง decision_id ให้ที่อยู่ถาวรแก่การตัดสินใจ owner ตรึงไว้ว่า "ใครเป็นผู้รับผิดชอบการตัดสินใจนี้" ส่วน rationale ตอบคำถาม "ทำไมถึงทำแบบนั้นนะ" เมื่อผ่านไป 6 เดือน คำตอบของ "ทำไมถึงเป็น 0.3" ที่หาไม่เจอในที่ประชุม ก็คือเนื้อหาที่ควรจะอยู่ในช่อง rationale ของ D2025_Q4_034 นั่นเอง ส่วนช่องที่เหลือ (scope·affected_atoms·related_decisions) คือสายไฟสำหรับการติดตามผลกระทบและการเชื่อมต่อกราฟ
ตรงนี้มีการออกแบบการตัดสินใจอย่างหนึ่งสอดแทรกอยู่ ถ้าบังคับให้กรอกครบทั้ง 12 ช่อง คนก็จะเลี่ยงการเขียนการ์ดเสียเอง จึงแบ่งเป็น 5 ช่องบังคับ (decision_id·title·owner·status·rationale) กับ 7 ช่องเลือกได้ แม้กรอกแค่ 5 ช่องทันทีหลังตัดสินใจในที่ประชุม การ์ดก็มีผลแล้ว ส่วนที่เหลือค่อยกรอกในขั้นตอนการพัฒนา
โครงของระบบติดตามอยู่ที่ว่าการ์ดหนึ่งใบเดินทางตามเส้นทางใดตั้งแต่เกิดจนถึงถูกยกเลิก ลองสังเกตว่าด่านที่ย้อนกลับไม่ได้อยู่ตรงไหน
flowchart TD
A[การตัดสินใจเกิดในที่ประชุม·แชต] --> B[ร่างการ์ดการตัดสินใจ
5 ช่องบังคับ]
B --> C[กำหนด decision_id·ลงทะเบียนดัชนี]
C --> D{วิเคราะห์ขอบเขตผลกระทบ
impact}
D --> E[กรอก affected_atoms·affected_files]
E --> F[เชื่อมกราฟด้วย wikilink]
F --> G{ด่านตรวจสอบ owner·approved_by}
G -->|ตีกลับ| B
G -->|อนุมัติ| H[นำเข้าสู่บิลด์]
H -.ย้อนกลับไม่ได้.-> I[แพร่ไปยังเอกสาร·การตัดสินใจอื่น]
I -.ย้อนกลับไม่ได้.-> J[วัดผลและตรวจสอบภายหลัง]
J --> K{ตัดสินการวิวัฒน์}
K -->|ถูกแทนที่| L[status: superseded
ลิงก์ supersedes]
K -->|ยังมีผล| M[คง status: active]
style H fill:#ffe0e0
style I fill:#ffe0e0
ตั้งแต่การร่าง (B) ไปจนถึงด่านตรวจสอบ (G) ล้วนเป็นขั้นที่ย้อนกลับได้ ไม่ว่าจะแก้การ์ดหรือยกเลิกก็แทบไม่มีต้นทุน แต่หลังจากนำเข้าสู่บิลด์ (H) แล้วถือเป็นการย้อนกลับไม่ได้ในทางปฏิบัติ การเปลี่ยนแปลงที่ผู้ใช้สัมผัสไปแล้ว ต่อให้ย้อนคืนด้วย hotfix ก็ยังทิ้งร่องรอยไว้ในการรับรู้ของชุมชน และเมื่อการตัดสินใจที่ตามมาเริ่มสะสมตัวบนสมมติฐานของการตัดสินใจนี้ ต้นทุนการย้อนกลับก็จะพุ่งสูงแบบทวีคูณ ดังนั้นการตรวจสอบทั้งหมดของผู้ตัดสินใจต้องจบที่ด่าน G นี่คือโครงสร้างเดียวกันเป๊ะกับหลักการ "การอัดเสียงและการแคสติงเป็นขั้นที่ย้อนกลับไม่ได้" ที่กล่าวถึงในส่วนที่ 5
เมื่อทำให้การ์ดเป็น atom เราก็เชื่อมการ์ดเข้าด้วยกันได้ supersedes·relates_to ใน related_decisions กลายเป็นเส้นเชื่อม (edge) ของกราฟ ความขัดแย้งในการประชุมข้างต้นที่จริงก็เป็นเสี้ยวหนึ่งของกราฟนี้
ถ้ามีกราฟนี้ การประชุมคงจบใน 30 วินาที เมื่อเปิด D2026_Q2_017 ก็จะเห็น supersedes: D2025_Q4_034 แล้วคลิกที่ rationale ของการ์ดนั้นหนึ่งครั้ง "ทำไมถึงเป็น 0.3" ก็ปรากฏออกมาทันที กราฟคือประวัติการวิวัฒน์ของการตัดสินใจ และประวัติการวิวัฒน์ของการตัดสินใจก็คือประวัติศาสตร์ของเกมนั่นเอง แม้แต่สาขาที่แตกออกมาจากการตัดสินใจหลักอย่างแบบ PvP (D2026_Q2_025) ก็ติดตามได้ในพริบตา
ถ้าให้คนกรอก affected_atoms·affected_files ของการ์ดการตัดสินใจทีละช่องเอง ก็มีหลุด ในโปรเจกต์ A มีกระบวนการดึงขอบเขตผลกระทบชื่อ impact ซึ่งรับ atom การตัดสินใจเข้ามาแล้วไล่กราฟไปสามทิศทาง
affects ในออนโทโลยี: ความสัมพันธ์ที่ประกาศไว้อย่างชัดเจนว่า "ส่งผลกระทบให้"[[combat_global_cooldown_constant]] ในเนื้อความยูเนียน (union) ของทั้งสามเส้นทางคือขอบเขตผลกระทบที่แท้จริงของการตัดสินใจ นอกจากนี้ atom portal_layer_change_impact_check ยังตรวจสอบแยกต่างหากว่า "ไปแตะพอร์ทัลเลเยอร์ (เอกสารที่เปิดสู่ภายนอก·สเปก API) หรือไม่" ถ้าติดพอร์ทัลเลเยอร์ ระดับก็จะถูกยกขึ้นหนึ่งขั้น เพราะการแพร่ออกสู่ภายนอกมีต้นทุนการย้อนกลับที่สูงกว่า
ทฤษฎีมีแค่นี้ ผู้เขียนจะแสดงทั้งกระบวนการของการโยนบันทึกการประชุมก้อนหนึ่งให้ LLM จริง ๆ แล้วรับการ์ดการตัดสินใจกลับมา โดยใส่ทั้งพรอมต์ฉบับเต็มและผลลัพธ์ดิบไว้ตามต้นฉบับ จะไม่สรุปย่อ จุดที่ Claude สับสน จุดที่คนปฏิเสธ ไปจนถึงการสั่งซ้ำ จะแสดงให้เห็นทั้งหมด
ช่วยแปลงบันทึกการประชุมด้านล่างเป็นการ์ดการตัดสินใจในรูป YAML สคีมาคือ
decision_id / title / type / status / owner / approved_by / scope /
content / rationale / affected_atoms / affected_files / related_decisions
owner คือผู้เสนอ approved_by คือผู้อนุมัติ affected_atoms กับ related_decisions
ให้เขียนเฉพาะสิ่งที่ปรากฏจริงในบันทึกการประชุมเท่านั้น อย่าเดา
// (เจตนา: decision_id คือ D{YYYY}_Q{n}_{seq} ถ้าไม่รู้ seq ให้ใส่ XXX)
[บันทึกการประชุม]
- ตกลงกำหนด global cooldown ฝ่ายต่อสู้ให้เป็น 0.5 วินาทีเหมือนกันทั้งหมด (teammate_a เสนอ)
ตอนนี้แต่ละสกิลไม่เท่ากันเลย มีฟีดแบ็กสะสมว่าความอ่านง่ายของคอมโบแย่
- ที่ทำไว้ 0.3 วินาทีเมื่อปีก่อน มีคนทักว่าเร็วเกินไปจนคอมโบเละ
- สกิลฟื้นฟูถ้าใช้ 0.5 วินาทีจะรับมือสถานการณ์วิกฤตไม่ทัน ต้องจัดการเป็นข้อยกเว้น
- ผู้อำนวยการ (이민수) อนุมัติ teammate_b ทำการพัฒนา teammate_c ทำ QA
- target build คือวันที่ 9 พฤษภาคม
---
decision_id: D2026_Q2_XXX
title: กำหนด global cooldown ฝ่ายต่อสู้ให้เป็น 0.5 วินาทีเหมือนกันทั้งหมด
type: system_change
status: active
owner: teammate_a
approved_by: 이민수
approval_meeting: 95_BattleTF
scope:
- combat_system
- all_skills # ← ทั้งที่มีข้อยกเว้นสกิลฟื้นฟู แต่ดันใส่เป็น all_skills
content: |
ใช้ global cooldown 0.5 วินาทีกับสกิลฝ่ายต่อสู้ทั้งหมด
rationale:
- ฟีดแบ็กความอ่านง่ายของคอมโบสะสม
- 0.3 วินาทีปีก่อนเร็วเกินไปจนคอมโบเละ
affected_atoms:
- combat_global_cooldown # ← ต่างจากชื่อ atom จริง เป็นการเดา
affected_files: [] # ← เว้นว่างไว้ ไม่ได้ระบุในบันทึกการประชุม
implementation:
target_build: 2026-05-09
impl_owner: teammate_b
qa_owner: teammate_c
related_decisions:
- supersedes: D2025_Q4_XXX # การตัดสินใจ 0.3 วินาทีปีก่อน ไม่รู้ seq
- relates_to: ข้อยกเว้นสกิลฟื้นฟู (ID ยังไม่ระบุ) # ← กรอกเป็นข้อความบรรยาย ไม่ใช่ลิงก์
---
ถ้ามองอย่างซื่อตรง ผลลัพธ์นี้ได้ 60 คะแนน มีทั้งส่วนที่ทำดีและส่วนที่ผิดปนกัน
owner/approved_by ถูกต้อง target_build·ผู้พัฒนา·QA ถูกต้อง จับความสัมพันธ์ supersedes ได้all_skills ลงใน scope ทั้งที่ระบุข้อยกเว้นสกิลฟื้นฟูไว้แล้ว แต่กลับจับเป็นทั้งหมดcombat_global_cooldown ลงใน affected_atoms ชื่อ atom จริงของโปรเจกต์คือ combat_global_cooldown_constant ขัดคำสั่งที่บอกว่า "อย่าเดา"related_decisions เป็นข้อความบรรยายแบบภาษาธรรมชาติ ไม่ใช่ลิงก์affected_files: [] ไว้ว่าง เป็นการทำตามคำสั่งอย่างซื่อสัตย์ แต่เป็นไปไม่ได้ที่จะไม่มีไฟล์ที่ได้รับผลกระทบผู้เขียนไม่รับการ์ดนี้มาตามที่เป็น แต่เขียน ①②③ เป็นเหตุผลปฏิเสธแล้วสั่งให้ทำซ้ำ แก่นของการปฏิเสธคือ การเดาชื่อ atom ถ้า atom ID ผิดแม้แต่ตัวเดียว grep·การอ้างย้อนกลับที่ตามมาก็จะคลาดเคลื่อนทั้งหมด ความแม่นยำของระบบติดตามผูกอยู่กับความแม่นยำของ atom ID
ช่วยแก้สามจุด scope เนื่องจากสกิลฟื้นฟูเป็นข้อยกเว้น อย่าใช้ all_skills
แต่ใช้ all_active_skills แล้วเขียนข้อยกเว้นสกิลฟื้นฟูไว้ใน content ส่วน affected_atoms
สั่งไปแล้วว่าอย่าเดาแต่ดันเดา — ถ้าไม่รู้ชื่อจริงของ atom ให้ใส่ [확인 필요] ไว้
แล้วใส่คอมเมนต์ว่าต้องตรวจสอบอะไร ส่วนดัชนีเดี๋ยวฉันดูเอง related_decisions ให้ใส่
ข้อยกเว้นสกิลฟื้นฟูเป็น relates_to ของ D2026_Q2_018 และ supersedes เป็น D2025_Q4_034
affected_files ก็ให้เป็นแค่ตัวเลือก [확인 필요] เท่านั้น
affected_atoms:
# [확인 필요] ด้านล่างเป็นตัวเลือก ต้องยืนยัน atom ID ที่ถูกต้องจากดัชนี
- combat_global_cooldown_constant # ค่าคงที่ของ global cooldown
- combat_skill_cooldown_rule # กฎ cooldown รายสกิล (รวมการจัดการข้อยกเว้น)
affected_files:
# [확인 필요] ไม่มีในบันทึกการประชุม ตัวเลือกที่มักได้รับผลกระทบตามเกณฑ์ type=system_change:
- CombatBalance.xlsx # ชีตค่าตัวเลข cooldown
- CombatFormula_v3.md # เอกสารสูตรการต่อสู้
- UI/skill_cooldown_indicator # อินดิเคเตอร์ UI ของ cooldown
related_decisions:
- supersedes: D2025_Q4_034
- relates_to: D2026_Q2_018
scope:
- combat_system
- all_active_skills
content: |
ใช้ global cooldown 0.5 วินาทีกับสกิลแอ็กทีฟฝ่ายต่อสู้ทั้งหมด
สกิลฟื้นฟูเป็นข้อยกเว้น (การตัดสินใจแยกต่างหาก D2026_Q2_018)
ครั้งที่ 2 ซื่อตรงขึ้น แทนที่จะเดา atom แล้วฟันธง กลับติดธง [확인 필요] พร้อมแนบคอมเมนต์อธิบายเหตุผล ผู้เขียนเปิดดัชนี atom แล้วยืนยันว่าทั้งสองชื่อคือ combat_global_cooldown_constant·combat_skill_cooldown_rule มีอยู่จริง จึงปลดธงออก ตัวเลือก affected_files ทั้งสามก็ยืนยันหลังเทียบกับดัชนีเช่นกัน การ์ดสุดท้ายที่อยู่ตอนต้นบทนี้คือผลลัพธ์ที่ได้นั้น
บทเรียนของบันทึกเซสชันนี้มีเพียงข้อเดียว LLM ทรงพลังในฐานะผู้ร่างการ์ดการตัดสินใจ แต่การยืนยันขั้นสุดท้ายของ atom ID และ decision ID ต้องให้คนเทียบกับดัชนี AI ค้นหาตัวเลือก คนเป็นผู้เลือกใช้ ถ้าบทบาทของทั้งสองปนกัน ชื่อ atom ที่ผิดก็จะปนเปื้อนกราฟทั้งหมด
เมื่อการ์ดและกราฟผูกกันด้วย atom ID คำถาม "การตัดสินใจนี้ส่งผลกระทบที่ไหนบ้าง" ก็ได้คำตอบด้วย grep เพียงบรรทัดเดียว เราจะไล่ย้อนการอ้างถึง atom หลัก combat_global_cooldown_constant ของการตัดสินใจ D2026_Q2_017 ทั่วทั้งต้นฉบับ·ชีต·การ์ดการตัดสินใจ
rg "combat_global_cooldown_constant" --type md --type yaml -l
# → D2026_Q2_017.yaml (ตัวการ์ดการตัดสินใจเอง)
# D2026_Q2_025.yaml (แบบ PvP — อ้างค่าคงที่นี้ซ้ำ)
# CombatFormula_v3.md (เอกสารสูตร)
# 95_BattleTF_2026-04-18.md (ต้นฉบับบันทึกการประชุม)
ผลลัพธ์นี้ก็คือแผนที่ผลกระทบที่บอกว่า "ถ้าแก้ค่าคงที่นี้ จะกระเทือนสี่ที่" ข้อเท็จจริงที่ว่าการ์ดแบบ PvP อ้างค่าคงที่เดียวกันนั้น ความทรงจำของคนพลาดได้ง่าย แต่ grep ไม่พลาด เป็นไปได้เพราะ atom ID ถูกต้อง — ถ้า grep ด้วย combat_global_cooldown จากผลลัพธ์ครั้งที่ 1 จะไม่จับสักบรรทัดในสี่บรรทัดนี้เลย การจัดระดับ (§18.2)·เวิร์กโฟลว์ตลอดทั้งวงจร (§18.3)·การปรับเวิร์กโฟลว์ grep ให้แม่นยำขึ้น (§18.4) ล้วนตั้งอยู่บนความถูกต้องของ atom ID นี้
ผู้เขียนเปรียบเทียบก่อนและหลังนำระบบติดตามมาใช้ในโปรเจกต์ A ตัวเลขด้านล่างเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ขอแนะนำให้อ่านในเชิงทิศทางและสัดส่วนมากกว่าค่าสัมบูรณ์
| รายการ | ไม่มีระบบ | มีระบบใช้งาน | ทิศทาง |
|---|---|---|---|
| หารือซ้ำ "เคยตัดสินใจไปแล้วหรือยัง?" | 8\~12 ครั้งต่อไตรมาส | 0\~2 ครั้งต่อไตรมาส | ลดลงมาก |
| การเข้าใจขอบเขตผลกระทบของการตัดสินใจ | 1\~2 วัน | ไม่กี่นาทีด้วย grep | สั้นลงมาก |
| การติดตามประวัติการวิวัฒน์ของการตัดสินใจ | พึ่งความทรงจำของซีเนียร์ | กราฟอัตโนมัติ | ขจัดการพึ่งคน |
| การเรียนรู้ประวัติการตัดสินใจของสมาชิกใหม่ | 1\~2 เดือน | 1\~2 สัปดาห์ | ผลใหญ่ที่สุด |
ผลที่ใหญ่ที่สุดอยู่ที่บรรทัดสุดท้าย สมาชิกใหม่ไล่ตามกราฟการตัดสินใจอ่านเองว่า "ทำไมเกมนี้ถึงเป็นรูปแบบนี้ในตอนนี้" แทนที่จะไปคอยเกาะถามซีเนียร์ การติดตามการตัดสินใจจึงกลายเป็นสินทรัพย์การเรียนรู้การตัดสินใจของบริษัทไปในตัว เพียงแต่ไตรมาสแรกที่ระบบเข้ามาย่อมมีภาระการเขียนการ์ดอยู่อย่างชัดเจน ทางที่ปลอดภัยคือเริ่มจากปักหลัก 5 ช่องบังคับก่อนแล้วค่อย ๆ ขยาย
การดำเนินงานที่ผ่านมาคือการประยุกต์แบบอนุรักษนิยม คนตัดสินใจในที่ประชุม เขียนการ์ด ระบุ atom ที่ได้รับผลกระทบ ส่วนระบบอัตโนมัติรับแค่การจัดทำดัชนี·การค้นหา·grep·การแสดงกราฟ คนรับผิดชอบการตัดสินใจหลัก ระบบอัตโนมัติรับการจัดเก็บและการค้นหา
ขั้นต่อไปคือทิศทางที่บันทึกเซสชันข้างต้นได้แสดงให้เห็น โดยให้ภาษาธรรมชาติในบันทึกการประชุมเป็นอินพุต แล้ว LLM กรอกร่างการ์ดการตัดสินใจทั้ง 12 ช่อง ค้นหาตัวเลือก atom ที่ได้รับผลกระทบไปตามกราฟ และแนะนำไปจนถึงระดับ งานที่เหลืออยู่ในมือคนแคบลงเหลือสองอย่างคือ "ตรวจสอบว่าการ์ดและชื่อ atom ที่ AI กรอกตรงกับดัชนีหรือไม่" กับ "การอนุมัติขั้นสุดท้าย" ภาระของการกรอกตั้งแต่ศูนย์จนครบ 12 ช่อง กับภาระของการยืนยันชื่อ atom จากร่างของ LLM กับดัชนีนั้นเป็นภาระคนละแบบกัน
การประยุกต์แบบก้าวหน้านี้จะลงหลักปักฐานได้ต้องมีสามโครง หนึ่ง กราฟการตัดสินใจ ที่ทุกการตัดสินใจถูกลงทะเบียนเป็น atom และเชื่อมกันด้วย wikilink บันทึกการประชุมหนึ่งก้อนเป็นอินพุตของระบบอัตโนมัติไม่ได้ — มันต้องถูกแยกย่อยเป็นหน่วยการตัดสินใจเสียก่อน สอง ตัวจัดระดับ impact อัตโนมัติ (§18.2) ที่คำนวณจำนวนสาขาผลกระทบ·ต้นทุนการย้อนกลับ·ขอบเขตผลกระทบต่อผู้ใช้บนกราฟ แล้วแนะนำระดับ สาม การติดตามผลกระทบด้วย grep·LLM (§18.4) ที่ทำงานอย่างแม่นยำด้วย atom ID และ wikilink
ตรงนี้สารที่ร้อยทะลุทั้งเล่มก็ปรากฏขึ้นอีกครั้ง การแยกย่อยการตัดสินใจเป็น atom·กราฟ·ระดับนั้น "ความสะดวกของการค้นหาและการอ้างย้อนกลับ" เป็นเพียงผิว แก่นแท้อยู่ที่ว่า ในบันทึกการประชุมหนึ่งก้อนที่ยังไม่ถูกแยกย่อย ระบบวิเคราะห์ผลกระทบอัตโนมัติยังไม่อาจรู้ด้วยซ้ำว่าอะไรคือหน่วยของการตัดสินใจ ข้อเสนอทั่วไปที่ว่าการแยกย่อยมีผิวเป็นการรวมภาษาการทำงานร่วมกันให้เป็นหนึ่งเดียว แต่มีแก่นแท้เป็นเงื่อนไขเบื้องต้นของระบบอัตโนมัติเชิงกระบวนการ (§6.6) ได้ปรากฏในแวดวงการตัดสินใจในรูปกราฟการตัดสินใจ·atom·ระดับ มันเป็นโครงเดียวกันกับ world BT (Behavior Tree, ต้นไม้พฤติกรรม)·quest cloud ในส่วนที่ 5 และการปรับสมดุลแบบก้าวหน้าในส่วนที่ 8 ในยุค 2010 ทฤษฎีก็เป็นไปได้แล้ว แต่การแยกย่อยบันทึกการประชุมเป็น atom การตัดสินใจโดยอัตโนมัตินั้นยังติดขัดอยู่ และตั้งแต่ปี 2023 เป็นต้นมา เมื่อ LLM เข้ามารับงานร่างของการแยกย่อยนั้น วิสัยทัศน์ส่วนใหญ่ที่เคยมีอยู่แค่บนกระดาษจึงเข้าสู่แดนของการเป็นจริงได้
decision_id·owner·rationale ถึงจะตอบคำถาม "ทำไมถึงทำแบบนั้น" ได้เมื่อผ่านไป 6 เดือนการประยุกต์นอกเกม การ์ดการตัดสินใจไม่ใช่แค่ของเกม แต่เป็นกลไกที่ทำให้ทุกองค์กรตอบคำถาม "ทำไมตอนนั้นถึงตัดสินใจแบบนั้น" ได้แม้ผ่านไป 6 เดือน การที่ทีมการตลาดเสียเวลา 30 นาทีเพราะหาในบันทึกการประชุมบรรทัดเดียวไม่เจอว่า "ไตรมาสที่แล้วตกลงพับช่องทางนี้ไป แต่เพราะอะไรนะ" จะหมดไปด้วยการ์ดสามช่องที่มี
decision_id·owner·rationaleเพียงใบเดียว ตัวอย่างเช่น เมื่อฝ่ายบุคคลกำหนดนโยบายอย่าง "ทำงานที่บ้านสัปดาห์ละ 2 วันเหมือนกันทั้งหมด" ถ้าเขียนผู้เสนอ·ผู้อนุมัติ·เหตุผล (ข้อมูลผลิตภาพ·แบบสำรวจพนักงาน) และ ID ของนโยบายเดิมที่ถูกแทนที่ลงในการ์ดใบนั้น เมื่อถึงเวลาทบทวนนโยบายในอีก 1 ปี เหตุผลของการตัดสินใจในอดีตก็จะยังคงมีชีวิตอยู่ตามเดิม
เส้นทางขั้นต่ำด้วยแชตบอตบนเว็บ (ไม่ต้องใช้เทอร์มินัล) — แก่นของบทนี้ไม่ใช่ไดเรกทอรีการ์ดการตัดสินใจหรือ grep แต่คือแนวคิดที่ว่า "ตรึงที่อยู่ถาวร (decision_id)·ผู้รับผิดชอบ (owner)·เหตุผล (rationale) ให้แก่การตัดสินใจ แล้วก่อนตัดสินใจใหม่ ให้ค้นหาการตัดสินใจในอดีตก่อน" แนวคิดนั้นจำลองได้ด้วยแชตบอตบนเว็บ (ChatGPT หรือ Claude เวอร์ชันเว็บ) เพียงอย่างเดียวโดยไม่ต้องมี CLI·ดัชนี atom สามขั้นตอนด้านล่างคือสายหลัก
1. เขียนการตัดสินใจหนึ่งครั้งเป็นหนึ่งบรรทัด เอกสารทั่วไปหนึ่งใบชื่อ decisions.md ก็พอแล้ว ไม่ต้องใช้ YAML หรือสคริปต์
- [D17] กำหนด global cooldown 0.5 วินาทีเหมือนกันทั้งหมด (owner: ฉัน, เหตุผล: ความอ่านง่ายของคอมโบ, แทนที่: D08)
2. เมื่อจะแปลงบันทึกการประชุมเป็นการ์ด ให้วางข้อความด้านล่างลงในแชตบอตบนเว็บ เป็นการยกข้อจำกัด 4 ข้อของพรอมต์ครั้งที่ 1 มาตรง ๆ
ช่วยแปลงการตัดสินใจในบันทึกการประชุมด้านล่างเป็นตาราง ช่องคือ
decision_id / title / owner / rationale / การตัดสินใจในอดีตที่ถูกแทนที่
ถ้าระบุ owner ไม่ได้ให้ใส่ [MISSING] ถ้าไม่รู้ atom·ชื่อไฟล์ ให้ใส่ [확인 필요]
และอย่าเดา
// (เจตนา: decision_id คือ D{ปี}_{ลำดับ} ถ้าไม่รู้ลำดับให้ใส่ XXX)
[เนื้อความบันทึกการประชุม]
3. ก่อนตัดสินใจใหม่ ให้ค้นหาใน decisions.md ด้วยการค้นในเอกสาร (Ctrl+F) ก่อน — คำถามหนึ่งข้อว่า "เคยตัดสินใจไปแล้วหรือยัง" ก็แก้ได้ด้วยวิธีนี้ นี่คือเวอร์ชันมือของการย้อนรอยด้วย grep ดัชนี atom·การ์ด YAML·เวิร์กโฟลว์ rg ค่อยนำมาใช้เมื่อการตัดสินใจสะสมเป็นหลายร้อยครั้งจนการค้นหาในเอกสารเดียวเริ่มหนักก็พอ
setup (เวอร์ชันโครงสร้างพื้นฐาน — หลังจากเส้นทางขั้นต่ำข้างต้นเริ่มคุ้นมือแล้ว) — สร้างไดเรกทอรีการ์ดการตัดสินใจและไฟล์ดัชนี
decisions/
D2026_Q2_017.yaml
_index.json # การรวมยอด by_status / by_scope / by_quarter
prompt — เมื่อโยนวาระการตัดสินใจในบันทึกการประชุมให้ LLM ต้องใส่ข้อจำกัด 4 ข้อของพรอมต์ครั้งที่ 1 ข้างต้นเข้าไปด้วยเสมอ โดยเฉพาะให้ระบุชัดเจนว่า "อย่าเดาชื่อ atom แต่ให้ใส่ [확인 필요] ไว้"
verify — เทียบรายการ affected_atoms ของการ์ดที่ได้กับดัชนี atom เพื่อยืนยันชื่อจริงแล้วจึงปลดธงออก จากนั้นรัน rg "<atom_id>" -l ด้วย atom หลัก เพื่อตรวจสอบไขว้ว่าไฟล์ที่ได้รับผลกระทบตรงกับ affected_files ของการ์ดหรือไม่
ถ้าใช้คนเดียวโดยไม่มีโครงสร้างพื้นฐานของทีม ให้ทิ้งการ์ด YAML ไปได้เลย เขียนการตัดสินใจหนึ่งครั้งเป็นมาร์กดาวน์หนึ่งบรรทัด
- [D17] กำหนด global cooldown 0.5 วินาทีเหมือนกันทั้งหมด (owner: ฉัน, เหตุผล: ความอ่านง่ายของคอมโบ, แทนที่: 0.3 วินาทีของ D08)
สะสมบรรทัดเหล่านี้ไว้ในไฟล์ decisions.md ไฟล์เดียว แล้วก่อนตัดสินใจใหม่ ให้ค้นหาการตัดสินใจในอดีตก่อนด้วย rg "쿨다운" decisions.md ไม่มีทั้งการ์ด กราฟ และเครื่องมือ แต่คำถามหนึ่งข้อว่า "เคยตัดสินใจไปแล้วหรือยัง" ก็แก้ได้ 90% ของระบบติดตามเริ่มต้นจากนิสัยหนึ่งบรรทัดนี้
ขณะที่กำลังจัดบันทึกการประชุมหลังประชุมเสร็จ มีการตัดสินใจบรรทัดเดียวเขียนอยู่ "รวม global cooldown (คูลดาวน์ทั้งเกม) เป็น 0.5 วินาที" ในที่ประชุมใช้เวลาตกลงกันไม่ถึง 30 วินาที ทุกคนพยักหน้าเห็นด้วย แล้วก็ข้ามไปยังวาระถัดไป
บรรทัดเดียวนั้นกินเวลาสองเดือนต่อมา สกิลในข้อมูลการต่อสู้ทั้ง 277 รายการได้รับผลกระทบทั้งหมด การแสดงผลเกจคูลดาวน์ใน UI ต้องวาดใหม่ และชีตปรับสมดุล (balance sheet) ถูกรื้อทำใหม่สองครั้ง ส่วนการตัดสินใจอีกอันที่เขียนอยู่ในบันทึกการประชุมฉบับเดียวกัน "แก้คำผิดในข้อความแนะนำของทูทอเรียล" เสร็จภายใน 5 นาที
การตัดสินใจสองอันบนบันทึกการประชุมเป็นบรรทัดเดียวเหมือนกัน จำนวนตัวอักษรก็พอ ๆ กัน แต่อันหนึ่งใช้ 5 นาที อีกอันใช้สองเดือน การทำให้ความต่างนี้มองเห็นได้ตั้งแต่วินาทีที่เขียนบันทึกการประชุม — นั่นคือการจัดระดับอิมแพ็กต์ ถ้าระดับมองไม่เห็น การตัดสินใจที่กินเวลาสองเดือนก็จะถูกฝังอยู่ในบรรทัดเดียวกับการตัดสินใจที่ใช้แค่ 5 นาที
บทนี้กล่าวถึงวิธีจัดระดับการแพร่กระจายของการตัดสินใจออกเป็นห้าระดับโดยอัตโนมัติ และวิธีติดตามว่าการแพร่กระจายนั้นลามไปถึงไหนบนกราฟ atom ของการตัดสินใจ เครื่องมือคือ decision atom และการสกัด impact ที่สั่งสมไว้ในบทก่อน รวมถึง atom ชื่อ portal_layer_change_impact_check
ก่อนอื่นจะชี้ให้เห็นว่าสภาวะที่ไม่มีการจัดระดับมีหน้าตาอย่างไร เมื่อการตัดสินใจทั้งหมดถูกวางไว้บนบรรทัดเดียวกัน อุบัติเหตุสองแบบจะผลัดกันเกิดขึ้น
แบบหนึ่งคือ การจัดการน้อยเกินไป (under-processing) การตัดสินใจที่สั่นสะเทือนทั้งโปรเจกต์อย่างการรวม global cooldown ถูกปฏิบัติเหมือนเป็น "เรื่อง 5 นาที" จึงเข้าสู่บิลด์โดยไม่ผ่านการตรวจสอบ กว่าการแพร่กระจายจะปรากฏก็ผ่านไปสองเดือน และตอนนั้นต้นทุนการย้อนกลับก็สั่งสมสูงเป็นภูเขาไปแล้ว
อีกแบบคือ การจัดการมากเกินไป (over-processing) แค่แก้คำผิดหนึ่งคำก็เรียกประชุม TF และขออนุมัติจากผู้กำกับเกม (Game Director) วงรอบการตัดสินใจพองตัวขึ้น และเวลาที่ผู้กำกับควรใช้กับการตัดสินใจระดับ T0 จริง ๆ กลับถูกดูดไปกับการประชุมเรื่องคำผิด
อุบัติเหตุสองแบบดูเหมือนตรงข้ามกัน แต่มีรากเดียวกัน น้ำหนักของการตัดสินใจมองไม่เห็น เมื่อน้ำหนักมองไม่เห็น เราจึงทุ่มแรงให้กับเรื่องเบาและปล่อยเรื่องหนักให้หลุดมือ การจัดระดับคืองานติดป้ายน้ำหนักให้กับการตัดสินใจ และทันทีที่ป้ายติดลงไป วิธีจัดการก็จะแยกทางโดยอัตโนมัติ
ในโปรเจกต์ A ของบริษัทพัฒนา MMORPG ที่ผู้เขียนดูแลอยู่ เราแบ่งอิมแพ็กต์ของการตัดสินใจออกเป็นห้าระดับ ยิ่งสูงยิ่งหนัก และต้องใช้คนและเวลามากขึ้นในการจัดการ
| ระดับ | นิยาม | ตัวอย่าง | ผู้ตัดสินใจ | วงรอบ |
|---|---|---|---|---|
| T0 | วิสัยทัศน์เกม / ระบบหลัก | การตัดสินใจมือถือมาก่อน, การเปลี่ยนกลไกหลัก | Game Director + CEO | รายไตรมาส |
| T1 | ระบบ / ข้ามหลายสาขา | การรวม global cooldown, การเพิ่มอาชีพใหม่ | ประธาน TF + ผู้กำกับ | 1–2 สัปดาห์ |
| T2 | เฉพาะสาขา / ระดับกลาง | การปรับค่าสกิลบางตัว, การเพิ่มคอมโพเนนต์ UI | ผู้กำกับสาขา | 3–5 วัน |
| T3 | รายการเดี่ยว / เล็ก | การแก้บทพูด NPC ตัวเดียว, การปรับสีเล็กน้อย | ซีเนียร์ 1 คน | 1–2 วัน |
| T4 | ทันที / ฮอตฟิกซ์ | การแก้บั๊ก, คำผิดในข้อความ | ผู้รับผิดชอบ | หน่วยชั่วโมง |
ดูแค่ตารางก็เรียบร้อยเหมือนตำราเรียน แต่ความยากในงานจริงไม่ใช่การท่องตาราง แต่คือ การตัดสินว่าการตัดสินใจหนึ่งเรื่องที่อยู่ตรงหน้าจะใส่ลงในช่องไหน เราต้องรู้ว่า "การรวม global cooldown" คือ T1 ตั้งแต่วินาทีที่เขียนบันทึกการประชุม ไม่ใช่หลังจากประชุมจบ ด้วยเหตุนี้ เกณฑ์ 3 ข้อในหัวข้อถัดไปจึงเป็นหัวใจ
ระดับไม่ได้กำหนดด้วยความรู้สึก เราประเมินเกณฑ์สามข้อ แล้วเลือกระดับที่สูงที่สุดในนั้น
ในบรรดาเกณฑ์สามข้อ จำนวนสาขาที่กระทบ สามารถนับได้อย่างเป็นกลไกจากกราฟ decision atom เพียงรวบรวมแท็กว่า atom ที่การตัดสินใจไปแตะนั้นสังกัดสาขาใด (การต่อสู้ · UI · ข้อมูล · เนื้อเรื่อง ฯลฯ) ก็จบ
ปัญหาอยู่ที่อีกสองข้อที่เหลือ ต้นทุนการย้อนกลับ และ ขอบเขตที่กระทบผู้ใช้ ไม่สามารถแปลงเป็นตัวเลขบนกราฟได้ "ถ้าจะย้อนกลับการตัดสินใจนี้ในอีกสองเดือนต่อมาจะต้องใช้ต้นทุนเท่าไร" เป็นการตัดสินด้วยภาษาธรรมชาติ จุดนี้เองคือกำแพงสุดท้ายของการจัดระดับอิมแพ็กต์อัตโนมัติจนกระทั่งก่อนปี 2023 จำนวนสาขาที่กระทบนั้นทำให้อัตโนมัติได้แล้ว แต่ช่องการตัดสินด้วยภาษาธรรมชาติสองช่องว่างอยู่ สุดท้ายคนก็ต้องมาให้คะแนนใหม่ตั้งแต่ต้น เมื่อ LLM อ่านเนื้อหาของ decision atom และเติมร่างของสองช่องนี้ได้ กำแพงก็เตี้ยลง
ตรงนี้จะพูดอย่างตรงไปตรงมา สิ่งที่ LLM เติมคือ ร่าง ไม่ใช่คำตัดสินสุดท้าย ถึง LLM จะประเมินต้นทุนการย้อนกลับว่า "สูง" ผู้กำกับสาขาก็อาจตัดสินว่า "ด้วยโครงสร้างชีตของเรา อันนี้แค่ปานกลาง" ได้ การจัดระดับอัตโนมัติไม่ได้มาแทนที่การตัดสินของคน แต่ทำให้ คนไม่ต้องเริ่มจากช่องว่างเปล่า ๆ
นี่คือกระบวนการจริงที่นำ decision atom หนึ่งเรื่องที่สร้างในบทก่อนใส่เข้า LLM ตรง ๆ แล้วให้จัดระดับ ผู้เขียนจะถ่ายทอดทั้งกระบวนการโดยไม่ย่อ รวมถึงการปฏิเสธและการขอใหม่ด้วย
# decisions/D2026_Q2_017.md (atom ที่ลงทะเบียนในบทก่อน 18.1)
id: D2026_Q2_017
title: รวม global cooldown เป็น 0.5 วินาที
date: 2026-04-18
proposer: teammate_a
decider: อี มินซู # Design Director (เหมือนกับ approved_by ใน 18.1)
context: เกณฑ์คูลดาวน์แตกต่างกันไปตามแต่ละอาชีพ ทำให้ทุกครั้งที่เพิ่มอาชีพใหม่
ต้นทุนการคำนวณปรับสมดุลใหม่สั่งสมขึ้น จึงเสนอให้รวมเป็นเกณฑ์รวม 0.5 วินาที
affected_atoms: [combat_cooldown_rule, skill_table, ui_cooldown_gauge]
reversible: ?
status: proposed
ช่วยให้ระดับอิมแพ็กต์ของ decision atom ด้านล่างเป็นหนึ่งในระดับ T0~T4
ให้ดูเกณฑ์สามข้อ คือ จำนวนสาขาที่กระทบ / ต้นทุนการย้อนกลับ / ขอบเขตที่กระทบผู้ใช้ แยกกัน
แล้วเลือกระดับที่สูงที่สุดในนั้น ช่องที่ไม่มั่นใจให้เขียนว่า "ไม่แน่นอน"
และบอกว่าต้องให้คนมาดู
// (เจตนา: T0=สาขา 5+·ต้นทุนสูงมาก·ทั้งหมด, T1=2~4·สูง·สูง, T2=1·ปานกลาง, T3=1·ต่ำ, T4=1·ต่ำมาก)
[แนบเนื้อหาต้นฉบับของ atom]
ประเมินตามเกณฑ์ 3 ข้อ:
1. จำนวนสาขาที่กระทบ
ดูจาก affected_atoms:
- combat_cooldown_rule → สาขาการต่อสู้
- skill_table → สาขาข้อมูล
- ui_cooldown_gauge → สาขา UI
สาขาที่ระบุไว้มี 3 สาขา
อย่างไรก็ตาม จาก context ที่ว่า "ทุกครั้งที่เพิ่มอาชีพใหม่ต้องคำนวณปรับสมดุลใหม่"
มีความเป็นไปได้สูงที่สาขาปรับสมดุลจะได้รับผลกระทบทางอ้อมด้วย
ทางตรง 3 + ทางอ้อม 1 = มองเป็นช่วง 2~4 สาขาในทางปฏิบัติ → สัญญาณ T1
2. ต้นทุนการย้อนกลับ
หากรวม global cooldown แล้วจะย้อนกลับ ก็ต้องแยกคูลดาวน์ของสกิลทุกตัว
ใน skill_table กลับไปเป็นเกณฑ์รายอาชีพอีกครั้ง
ไม่ทราบจำนวนแถวของ skill_table จึงไม่แน่นอนเรื่องต้นทุนที่แท้จริง แต่
"รวม → กู้คืนการแยก" โดยทั่วไปมีต้นทุนสูง → สูง (สัญญาณ T1)
※ ช่องนี้ยืนยันไม่ได้ถ้าไม่ทราบขนาดของ skill_table จึงทำเครื่องหมายไม่แน่นอน
3. ขอบเขตที่กระทบผู้ใช้
คูลดาวน์มีผลกับทุกสถานการณ์การต่อสู้ ดังนั้นผู้ใช้แทบทั้งหมด
จะรู้สึกได้ → สูง (สัญญาณ T1)
ระดับที่เลือก: T1
เหตุผล: เกณฑ์ 3 ข้อชี้ไปที่ T1 ทั้งหมด ไม่ใช่ T0 —
เพราะยังไม่ถึงขั้น "เปลี่ยนกลไกหลัก" แต่เป็นการรวมพารามิเตอร์
ของกลไกเดิมให้เป็นหนึ่งเดียว
ต้องให้คนยืนยัน:
- ช่องต้นทุนการย้อนกลับ: ต้องตรวจสอบจำนวนแถวจริงของ skill_table
- สล็อต reversible ว่างเป็น "?" → การจัดระดับนี้มีเงื่อนไขว่าจะเติม
reversible เป็น "ยาก"
ตรงนี้ผู้เขียนไม่ได้รับมาทั้งดุ้น ข้อ 1 ของผลลัพธ์เป็นจุดที่ต้องลงมือ LLM มองสาขาปรับสมดุลเป็น "ผลกระทบทางอ้อม" แต่ในโครงสร้างจริงของโปรเจกต์ A การรวม global cooldown รื้อชีตปรับสมดุล โดยตรง — เพราะตัวสูตรคำนวณการปรับสมดุลของอาชีพใหม่รับค่าคูลดาวน์เป็นอินพุต ดังนั้นการปรับสมดุลจึงไม่ใช่ทางอ้อม แต่เป็นสาขาที่ 4 โดยตรง จำนวนสาขาที่กระทบคือ 4 สาขา ถึงอย่างนั้นระดับที่เลือกเป็น T1 ก็ไม่เปลี่ยน
อีกอย่างหนึ่ง เครื่องหมาย "ไม่แน่นอน" ในช่องต้นทุนการย้อนกลับนั้นแม่นยำ skill_table มีสกิลจริง 277 แถว และถ้าจะย้อนการรวมกลับก็ต้องแยกคูลดาวน์ของทั้ง 277 แถวใหม่ ต้นทุนจึงไม่ใช่ "สูง" แต่ใกล้เคียง "สูงมาก" ถึงอย่างนั้น แค่ช่องหนึ่งเลื่อนขึ้นก็ไม่ได้ทำให้ระดับที่เลือกขยับไปเป็น T0 — เพราะจำนวนสาขาที่กระทบและผลกระทบต่อผู้ใช้อยู่ในช่วง T1 ระดับที่สูงที่สุดจึงยังคงเป็น T1
แก้นะ ช่วยเพิ่ม balance_sheet ลงใน affected_atoms ด้วย (เพราะสูตรปรับสมดุล
รับค่าคูลดาวน์เป็นอินพุต จึงเป็นผลกระทบโดยตรง) แบบนั้นก็จะมี 4 สาขาที่กระทบ
และ skill_table มี 277 แถว ช่วยนำเรื่องนี้มาเติม reversible แล้วยืนยันระดับสุดท้ายให้ที
นำมาแก้ไขแล้ว
สาขาที่กระทบ: การต่อสู้ · ข้อมูล · UI · ปรับสมดุล = 4 สาขา
ต้นทุนการย้อนกลับ: สูงมาก (ต้องแยก skill_table ทั้ง 277 แถวใหม่)
ผลกระทบต่อผู้ใช้: สูง
reversible: ยาก (ใกล้เคียงย้อนกลับไม่ได้ในทางปฏิบัติ — การแยก 277 แถวใหม่
ต้องการบิลด์ 1~2 ครั้ง + การตรวจสอบปรับสมดุลใหม่หลายรอบ)
ระดับสุดท้าย: T1
ข้อควรระวัง: หากดูเฉพาะช่องต้นทุนการย้อนกลับช่องเดียวจะเป็นสัญญาณ T0 แต่
สาขาที่กระทบ 4 สาขา · ผลกระทบต่อผู้ใช้ "สูง" อยู่ในช่วง T1
เนื่องจากเกณฑ์ 2 ใน 3 ข้อชี้ไปที่ T1 ดังนั้น T1 จึงถูกต้อง
อย่างไรก็ตาม ขอแนะนำให้ทำเครื่องหมายการตัดสินใจนี้ว่าเป็น
"ฝั่งหนักของ T1" — เพราะต้นทุนการย้อนกลับอยู่ในระดับ T0
ประโยคสุดท้ายรอบคอบยิ่งกว่าคน ข้อเสนอที่ว่าให้ติดป้ายว่าระดับเป็น T1 ก็จริงแต่ต้นทุนการย้อนกลับอยู่ระดับ T0 ในฐานะ "ฝั่งหนักของ T1" — นี่เป็นการตัดสินที่ไม่มีในพรอมต์แรก ผู้เขียนรับไว้ การตัดสินใจนี้ได้ถูกติดแท็ก weight: heavy เพิ่ม และยกลำดับความสำคัญในการติดตามภายหลังให้สูงขึ้น
หัวใจที่บันทึกเซสชันนี้แสดงให้เห็นมีเพียงหนึ่งเดียว LLM สร้างร่างของการจัดระดับและเหตุผล ส่วนคนแก้ไขด้วยข้อเท็จจริงเชิงโดเมน (ปรับสมดุลเป็นผลกระทบโดยตรง, 277 แถว) อย่างใดอย่างหนึ่งเพียงลำพังไม่พอ ถ้าให้คนทำคนเดียวก็เริ่มจากช่องว่างเปล่าและช้า ถ้าให้ LLM ทำคนเดียวก็เขียนว่า "สูง" ทั้งที่ไม่รู้ว่ามี 277 แถว
เมื่อระดับถูกกำหนดแล้ว ถัดไปคือ "ลามไปถึงไหน" เรานำการสกัด impact จากบทก่อน — อินบาวด์เอดจ์ (inbound edge), ความสัมพันธ์ affects ของออนโทโลยี, การอ้างถึงย้อนกลับของ wikilink — มาใช้กับ decision atom
# impact_propagation.py — ติดตามขอบเขตการแพร่กระจายของ decision atom
def trace_impact(decision):
# ชั้นที่ 1: atom·ไฟล์ ที่การตัดสินใจไปแตะโดยตรง
direct = decision.affected_atoms + decision.affected_files
# ชั้นที่ 2: atom ที่อ้างถึง atom ชั้น 1 ย้อนกลับด้วย wikilink (อินบาวด์เอดจ์ของ impact)
secondary = []
for atom in direct:
secondary.extend(find_inbound_refs(atom)) # การอ้างย้อนกลับ [[atom]]
secondary.extend(find_affects_edges(atom)) # affects ของออนโทโลยี
secondary = dedup(secondary) - set(direct)
return {
"direct": direct,
"secondary": secondary,
"affected_fields": determine_fields(direct + secondary),
"estimated_hours": estimate_hours(direct, secondary),
}
หัวใจคือ find_inbound_refs — ฟังก์ชันที่รวบรวมลูกศร ขาเข้า ที่ชี้มายัง atom นั้นด้วย [[...]] จากกราฟ atom สิ่งที่การตัดสินใจไปแตะเอง (ลูกศรขาออก) เขียนอยู่ใน atom อยู่แล้ว แต่ว่าใครพึ่งพา atom นั้นบ้าง (ลูกศรขาเข้า) ต้องสแกนกราฟทั้งหมดย้อนกลับจึงจะมองเห็น การแพร่กระจายที่กินเวลาสองเดือนนั้นแทบทุกครั้งซ่อนอยู่ฝั่ง อินบาวด์เอดจ์ นี้
จะบันทึกผลของการรันการติดตามนี้กับ D2026_Q2_017 อย่างตรงไปตรงมา direct คือ atom 4 ตัวที่ยืนยันไว้ข้างต้น ส่วน secondary คือ atom ที่อ้างถึง skill_table ย้อนกลับ — ข้อความคำอธิบายสกิล, การแม็ปไอคอนสกิล, ทรีสกิลรายอาชีพ ฯลฯ — ทยอยตามมาเป็นพรวน ตัวเลขแตกต่างกันไปตามช่วงเวลา จึงไม่ฟันธง ข้อเท็จจริงที่การติดตามจับให้คือ "ทิศทาง" ที่ว่า "secondary เป็นหลายสิบเท่าของ direct" ส่วนจำนวน atom ที่แม่นยำขึ้นอยู่กับสถานะของกราฟ แค่ทิศทางก็เพียงพอ — ถ้า secondary ใหญ่กว่า direct ราวหนึ่งหลัก นั่นคือสัญญาณ T1 และหมายความว่าเป็นเป้าหมายของการติดตามภายหลัง
การจัดระดับไม่ใช่ขั้นตอนที่แยกเป็นอิสระ แต่ถูกตรึงเป็นด่านอยู่กลางสายการตัดสินใจ เมื่อผู้สมัครที่จะกลายเป็นการตัดสินใจถูกลงทะเบียน การวิเคราะห์อัตโนมัติจะแนะนำระดับ และต่อเมื่อคนตรวจสอบ·ปรับแก้แล้วเท่านั้นจึงจะส่งต่อไปยังการประชุมตัดสินใจ
flowchart TD
A[ลงทะเบียนผู้สมัครการตัดสินใจ
บันทึกการประชุม → ร่าง decision atom] --> B[วิเคราะห์อิมแพ็กต์อัตโนมัติ
สกัด impact]
B --> C{ประเมิน 3 เกณฑ์อัตโนมัติ}
C -->|จำนวนสาขาที่กระทบ| D1[คำนวณจากกราฟ]
C -->|ต้นทุนการย้อนกลับ| D2[ร่าง LLM → ทำเครื่องหมายไม่แน่นอน]
C -->|ผลกระทบต่อผู้ใช้| D3[ร่าง LLM → ทำเครื่องหมายไม่แน่นอน]
D1 --> E[เลือกระดับสูงสุด
แนะนำ T0~T4]
D2 --> E
D3 --> E
E --> F{การตรวจสอบโดยมนุษย์}
F -->|แก้ด้วยข้อเท็จจริงเชิงโดเมน| G[ยืนยันระดับ + แท็ก weight]
F -->|ตีกลับ·จัดระดับใหม่| C
G --> H{แยกตามระดับ}
H -->|T0| T0[ผู้กำกับ+CEO / วงรอบรายไตรมาส]
H -->|T1| T1[TF / 1~2 สัปดาห์]
H -->|T2| T2[ผู้กำกับสาขา / 3~5 วัน]
H -->|T3| T3[ซีเนียร์ 1 คน / 1~2 วัน]
H -->|T4| T4[ผู้รับผิดชอบจัดการทันที]
T0 --> I[นำเข้าบิลด์ — ย้อนกลับไม่ได้]
T1 --> I
T2 --> I
T3 --> I
T4 --> I
I --> J[ติดตามภายหลัง
ประมาณการ vs จริง → เรียนรู้สู่ประมาณการครั้งถัดไป]
J -.ดูดซับฝั่งย้อนกลับได้.-> B
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class B,C,D1,E,I code;
class D2,D3 ai;
class F,G,T0,T1,T2,T3,T4 human;
class A data;
ในสายนี้ ขั้นตอนย้อนกลับไม่ได้มีเพียงหนึ่งเดียว คือการนำเข้าบิลด์ (I) ก่อนหน้านั้นย้อนกลับได้ทั้งหมด — ถึงจะแนะนำระดับผิดคนก็ตีกลับได้ และแท็ก weight ก็ถอดได้ มันจะกลายเป็นย้อนกลับไม่ได้ก็ต่อเมื่อเข้าบิลด์แล้วและแพร่กระจายไปยังเอกสารอื่นแล้วเท่านั้น ด้วยเหตุนี้ ด่าน (การตรวจสอบโดยมนุษย์ที่ F) จึงอยู่ก่อนบิลด์ คนจะขวางไว้หนึ่งครั้งก่อนข้ามเส้นย้อนกลับไม่ได้
เส้นประสุดท้าย — ลูกศรที่การติดตามภายหลัง (J) ย้อนกลับไปยังการวิเคราะห์อัตโนมัติ (B) ของการตัดสินใจครั้งถัดไป — ทำให้ระบบนี้กลายเป็นวงรอบการเรียนรู้ ข้อมูลที่วัดได้จริงจากขั้นตอนย้อนกลับไม่ได้ (เช่น เวลา QA ยาวกว่าที่ประมาณการ) ถูกดูดซับเข้าสู่ขั้นตอนย้อนกลับได้ของการตัดสินใจครั้งถัดไป
หลังจากการตัดสินใจเข้าบิลด์ไป 1 สัปดาห์ \~ 1 เดือน เรานำประมาณการกับของจริงมาเทียบกัน นี่คือแบบฟอร์มติดตามภายหลังของ D2026_Q2_017
ติดตามภายหลังการตัดสินใจ D2026_Q2_017 (ตัวอย่างแบบฟอร์ม · ตัวเลขเป็นอินพุตสมมติ)
─────────────────────────────────
เวลาทำงาน (ประมาณการ → จริง)
โค้ด: 16h → 22h (+38%)
ข้อมูล: 8h → 6h (-25%)
UI: 4h → 4h (=)
QA: 8h → 12h (+50%)
total: 36h → 44h (+22%)
atom ที่กระทบ (ประมาณการ → จริง)
direct: 4 → 4 (แม่นยำ)
secondary: ประมาณการหลายสิบ → จริงหลายสิบ (ทิศทางตรงกัน ไม่เปิดเผยตัวเลขที่แม่นยำ)
อุบัติเหตุที่เกิด: 0 ครั้ง
รูปแบบความคลาดเคลื่อน: QA เกินประมาณการทุกครั้ง (ครั้งนี้ +50%)
นำไปใช้กับการตัดสินใจครั้งถัดไป: บวกมาร์จิน +20% ให้ประมาณการ QA เป็นค่าตั้งต้น
บล็อกข้างต้นเป็นตัวอย่างแบบฟอร์มที่แสดงให้เห็นว่าการติดตามภายหลัง มีหน้าตาอย่างไร ค่าเวลา·เปอร์เซ็นต์ไม่ใช่ข้อมูลโปรเจกต์จริง แต่เป็นอินพุตสมมติที่กรอกลงในแบบฟอร์ม ดังนั้นในโปรเจกต์ของคุณเองก็เปลี่ยนเป็นตัวเลขของคุณเองกรอกลงไป — ตามคำสัญญาของหนังสือเล่มนี้เป๊ะ ๆ เราแสดงโครงสร้างให้ดู ส่วนตัวเลขคุณเป็นผู้วัดเอง สิ่งที่เป็นของจริงไม่ขึ้นกับแบบฟอร์มมีอยู่หนึ่งเดียว "ทิศทาง" ของความคลาดเคลื่อนที่ว่า "QA เกินประมาณการทุกครั้ง" และขั้นตอนป้อนทิศทางนั้นกลับเข้าสู่การตัดสินใจครั้งถัดไป จึงเกิดเป็นใบสั่งยาที่ว่าให้บวกมาร์จิน QA (เช่น +20%) เข้าไปตั้งแต่ต้นในประมาณการครั้งถัดไป
คุณค่าของการติดตามภายหลังไม่ได้อยู่ที่การทายตัวเลขให้แม่น แต่อยู่ที่การป้อนทิศทางของความคลาดเคลื่อนกลับเข้าไป ยิ่งประมาณการแม่นยำขึ้น ความเชื่อมั่นในการจัดระดับก็ยิ่งสูงขึ้น และเมื่อความเชื่อมั่นสูงขึ้น การมอบหมายงานก็เป็นไปได้
อุบัติเหตุที่เกิดซ้ำในแต่ละระดับต่างกัน ใบสั่งยาก็ต่างกัน
| ระดับ | รูปแบบอุบัติเหตุ | ใบสั่งยา |
|---|---|---|
| T0 | วิสัยทัศน์คลุมเครือ → สับสนตลอดทั้งไตรมาส | บังคับให้ระบุวิสัยทัศน์หนึ่งบรรทัดในตัวคำตัดสินใจ |
| T1 | ขัดแย้งข้ามสาขา → กำหนดการล่าช้า | ให้ตัวแทนทุกสาขาที่กระทบเข้าร่วม TF |
| T2 | มองข้ามผลกระทบต่อระบบข้างเคียง → การตัดสินใจตามมาพุ่ง | ติดตาม secondary บังคับ |
| T3 | การตัดสินใจเล็ก ๆ สั่งสม → ความสอดคล้องเสียหาย | ตรวจ T3 รวมกันในการทบทวนรายไตรมาส |
| T4 | ตรวจสอบไม่พอ → ต้องฮอตฟิกซ์ซ้ำ | ฮอตฟิกซ์ก็ให้รีวิวอย่างน้อย 1 คน |
ใบสั่งยาในตารางนี้ล้วนถูกดำเนินการด้วยเครื่องมือที่ปรากฏในหัวข้อก่อน ๆ "ติดตาม secondary บังคับ" ของ T2 คือ find_inbound_refs ใน §18.2.4 และ "ให้ตัวแทนทุกสาขาที่กระทบเข้าร่วม" ของ T1 นั้น ใครต้องเข้ามาถูกกำหนดด้วย affected_fields ที่ §18.2.4 จับได้
อุบัติเหตุที่แพงที่สุดไม่มีอยู่ที่ไหนในตารางเลย คือการที่ระดับเองผิด ถ้าซีเนียร์ตัดสิน T0 คนเดียววิสัยทัศน์จะเสียหาย ถ้าผู้กำกับลงไปจัดการ T4 เองก็จะเกิดคอขวด เมื่อระดับผิด ใบสั่งยาทั้งหมดที่อยู่ใต้ระดับนั้นก็ทำงานผิดที่ ด้วยเหตุนี้ ด่านการตรวจสอบโดยมนุษย์ใน §18.2.3 จึงไม่ใช่แค่พิธีการ
จะเปรียบเทียบก่อนและหลังการนำการจัดระดับมาใช้ในโปรเจกต์ A ในบรรดาค่าด้านล่าง ค่าสัมบูรณ์เป็นตัวอย่างที่ปรุงแต่งขึ้น ส่วน ทิศทาง (เครื่องหมายมากกว่า/น้อยกว่า) เป็นแนวโน้มจริง
| รายการ | ไม่มีการจัดระดับ | มีการดำเนินการจัดระดับ |
|---|---|---|
| วงรอบการตัดสินใจ | ทั้งหมดเท่ากันที่ 1\~2 สัปดาห์ | แยกย่อยตั้งแต่ T0 รายไตรมาส \~ T4 หน่วยชั่วโมง |
| การตัดสินใจที่จัดการผิด | จำนวนมากต่อไตรมาส | จำนวนน้อยต่อไตรมาส |
| ภาระการตัดสินใจรายสัปดาห์ของผู้กำกับ | สูง (ทุกการตัดสินใจไปที่ผู้กำกับ) | ต่ำ (มอบหมาย T2\~T4) |
| วงรอบฮอตฟิกซ์ | 1\~2 วัน | 4\~24 ชั่วโมง |
| การวิเคราะห์การตัดสินใจในการทบทวนรายไตรมาส | จัดกลุ่มยาก | รวมเป็นสถิติตามแต่ละระดับ |
เหตุผลที่เขียนตารางเป็นทิศทางแทนตัวเลขที่ฟันธง อธิบายได้ด้วยรายการที่ 3 เพียงรายการเดียว การกู้คืนเวลาของผู้กำกับคือประสิทธิผลที่ใหญ่ที่สุดของการจัดระดับ เมื่อไม่มีระดับ การตัดสินใจทุกอย่างตั้งแต่คำผิดไปจนถึงวิสัยทัศน์จะกระจุกไปที่ผู้กำกับเพียงคนเดียว เมื่อมีระดับ T2 ลงไปจะแยกไปยังผู้กำกับสาขา·ซีเนียร์·ผู้รับผิดชอบ และผู้กำกับก็โฟกัสที่ T0·T1 การที่การมอบหมายงานเป็นไปได้ก็คือการที่ผู้กำกับกู้เวลาที่จะใช้กับการตัดสินใจที่หนักจริง ๆ กลับคืนมานั่นเอง
ในบทก่อนเราสร้างกราฟ decision atom และในบทนี้เราวางตัวจัดระดับอัตโนมัติลงบนกราฟนั้น ทั้งสองไม่ใช่เครื่องมือที่ทำงานแยกกัน แต่เป็นที่ทางที่ต่อเนื่องกันในโครงร่างเดียว
flowchart LR
A["① กราฟ decision atom
(บทก่อน 18.1)
บันทึกการประชุม → atom 12 สล็อต"] --> B["② ตัวจัดระดับอิมแพ็กต์อัตโนมัติ
(บทนี้ 18.2)
กราฟ → T0~T4 + ประมาณการ"]
B --> C["③ ติดตามผลกระทบด้วย wikilink
(18.4 เวิร์กโฟลว์ grep)
atom ID → ขอบเขตการแพร่กระจาย"]
C --> D["การตรวจสอบ·อนุมัติโดยคน
ด่านย้อนกลับไม่ได้"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class C code;
class B ai;
class D human;
class A data;
สามองค์ประกอบต่ออนุกรมกัน กราฟสร้างอินพุต (①) ตัวจัดระดับให้น้ำหนัก (②) และการติดตามผลกระทบกางขอบเขตการแพร่กระจายออก (③) บทนี้คือที่ทางตรงกลาง
สามองค์ประกอบล้วนเข้าสู่แดนที่ทำได้จริงหลังจาก LLM พัฒนาขึ้นเท่านั้น กำแพงที่ถูกแก้ช้าที่สุดคือสองช่องของการประเมินด้วยภาษาธรรมชาติใน ② — ต้นทุนการย้อนกลับและขอบเขตที่กระทบผู้ใช้ ("กำแพงสุดท้าย" ใน §18.2.2) — และเมื่อ LLM เติมร่างของช่องนั้นได้ ①→②→③ จึงเริ่มหมุนเป็นอนุกรมได้
การจัดเรียงของย้อนกลับได้·ย้อนกลับไม่ได้ก็เกี่ยวพันกับโครงร่างนี้ ตามแผนภาพการไหลใน §18.2.5 เส้นย้อนกลับไม่ได้มีเพียงการนำเข้าบิลด์อันเดียว และตัวการนำเข้าบิลด์เองย้อนกลับไม่ได้ก็จริง แต่ผลที่วัดได้จริงของมันจะย้อนกลับมาเป็นการเรียนรู้แบบย้อนกลับได้ที่ทำให้การตัดสินใจครั้งถัดไปแม่นยำขึ้น
| รูปแบบ | ใบสั่งยา |
|---|---|
| จัดการทุกการตัดสินใจด้วยวงรอบเดียวกัน | แยกวงรอบตามแต่ละระดับ |
| ต่างคนต่างตัดสินเองโดยไม่มีการจัดระดับ | ด่านจัดระดับด้วย 3 เกณฑ์ |
| เพิกเฉยต่อระดับ (ซีเนียร์ตัดสิน T0) | บังคับตารางผู้ตัดสินใจ |
| ละเว้นการประเมินผลกระทบล่วงหน้า | ตั้งการวิเคราะห์อัตโนมัติเป็นด่านก่อนการประชุมตัดสินใจ |
| ยืนยันช่องภาษาธรรมชาติตามผลลัพธ์ LLM ทั้งดุ้น | ให้คนแก้ด้วยข้อเท็จจริงเชิงโดเมน |
| ปิดการตัดสินใจโดยไม่ติดตามภายหลัง | เทียบประมาณการ vs จริง ภายใน 1 สัปดาห์ \~ 1 เดือน |
| ไม่นำความคลาดเคลื่อนของประมาณการไปใช้กับการตัดสินใจครั้งถัดไป | นำทิศทางความคลาดเคลื่อนไปปรับมาร์จินของประมาณการครั้งถัดไป |
การประยุกต์นอกเกม ระดับอิมแพ็กต์คือป้ายที่ทำให้เห็นล่วงหน้าว่า "คำขอบรรทัดเดียวเป็นเรื่อง 5 นาทีหรือเรื่องสองเดือน" จึงใช้ได้ในที่ทำงานทุกแห่งที่การตัดสินใจหลั่งไหลเข้ามา ถ้าการแก้ข้อความในวิกิบริษัทหนึ่งบรรทัดกับ "การเปลี่ยนนโยบายวันลาทั้งบริษัท" เข้ามาเป็น 'วาระ 1 เรื่อง' เหมือนกัน และวิ่งผ่านสายอนุมัติเดียวกัน เรื่องเบาก็จะถูกจัดการมากเกินไปและเรื่องหนักก็จะหลุดไปโดยไม่ผ่านการตรวจสอบ ตัวอย่างเช่น เวลารับคำขอจากทีมปฏิบัติการ หากติด T0\~T4 ด้วยสามเกณฑ์ คือ จำนวนแผนกที่กระทบ · ต้นทุนการย้อนกลับ · ขอบเขตที่กระทบลูกค้า สิ่งที่ผู้รับผิดชอบจัดการได้ทันทีกับสิ่งที่ต้องให้หัวหน้าทีมอนุมัติก็จะแยกออกจากกันโดยอัตโนมัติ และเวลาของผู้บริหารก็จะถูกกู้คืนไปสู่การตัดสินใจที่หนักจริง ๆ
setup. เตรียม decision atom หนึ่งเรื่องที่สร้างในบทก่อน สล็อต affected_atoms ต้องถูกเติมไว้ ถ้าว่างอยู่จะนับจำนวนสาขาที่กระทบไม่ได้
prompt. ใช้พรอมต์ฉบับเต็มใน §18.2.3 ตามนั้น อย่าตกหล่นสามบรรทัดหลัก — (1) ประเมินแต่ละเกณฑ์ใน 3 เกณฑ์, (2) เลือกระดับที่สูงที่สุด, (3) ช่องที่ไม่มั่นใจให้ทำเครื่องหมาย "ไม่แน่นอน" และขอให้คนยืนยัน ถ้าไม่มีบรรทัดที่สาม LLM จะฟันธงแม้กระทั่งสิ่งที่มันไม่รู้
verify. เมื่อได้ผลลัพธ์ LLM มาให้ตรวจสองอย่างด้วยตัวเอง หนึ่ง ต่อให้ LLM นับจำนวนสาขาที่กระทบแล้ว ก็ให้นับใหม่เองโดยตรงจากกราฟ atom — มันอาจจับผลกระทบทางอ้อมเป็นทางตรงหรือมองข้ามไปก็ได้ (กรณีปรับสมดุลในบันทึกเซสชัน) สอง ช่องที่ติดเครื่องหมาย "ไม่แน่นอน" ให้เติมด้วยข้อเท็จจริงเชิงโดเมน (ขนาดจริงอย่าง skill_table 277 แถว) ต่อเมื่อตรวจสองอย่างเสร็จแล้วเท่านั้นจึงยืนยันระดับและส่งต่อไปยังด่านบิลด์
ถ้าเป็นโปรเจกต์ที่ทำคนเดียวโดยไม่มีทั้งทีมและ TF ห้าระดับก็มากเกินไป ลดเหลือสามระดับ
เครื่องมือก็เริ่มได้โดยไม่ต้องมีโค้ดสักบรรทัด เวลาจดการตัดสินใจ แค่ติดแท็ก [หนัก] [ปานกลาง] [ทันที] ไว้ข้างหน้า แค่นั้นก็ทำให้เราหยุดอีกครั้งหนึ่งหน้าการตัดสินใจที่ติดแท็ก "หนัก" — แก่นของการจัดระดับสุดท้ายก็คือนิสัยการหยุดหน้าการตัดสินใจที่หนัก และระบบอัตโนมัติก็เป็นเพียงกลไกที่ทำให้การหยุดนั้นทำงานได้แม้ในระดับทีม
สามสัปดาห์หลังเปิดตัว เป็นวงทบทวนที่เราย้อนหาสาเหตุว่าเหตุใดสมดุล PvP ถึงพังทลาย เมื่อไล่ย้อนขึ้นไปบนไวต์บอร์ดจนสุด จุดเริ่มต้นที่พบคือการตัดสินใจเพียงครั้งเดียวเมื่อเดือนก่อน "ปรับ Cooldown รวม (global cooldown) จาก 0.3 เป็น 0.5" เป็นข้อเสนอที่สมเหตุสมผล เราได้รับฟีดแบ็กว่ามองไม่เห็นคอมโบ จึงตกลงกันได้ภายในสองชั่วโมง แต่การเปลี่ยนแปลงนั้นกลับดันอัตราการรอดของอาชีพแทงก์ขึ้นไปมากกว่าที่คาดถึง 14% และนั่นทำให้ PvP พังลง ไม่มีใครพูดได้ในวงตัดสินใจว่าการตัดสินใจนี้จะลามไปถึงแทงก์ การตัดสินใจเองไม่ได้ผิด สาเหตุของอุบัติเหตุคือการที่เราไม่ได้มองเห็น ก่อนตัดสินใจ ว่ามันจะลามไปไกลถึงไหน
การติดตามผลกระทบต้องเกิดขึ้นในสองจุด คือมองว่ามันจะลามไปถึงไหน ก่อนกดยืนยัน การตัดสินใจ (pre) และตรวจสอบว่ามันลามไปแค่ตรงนั้นจริงหรือไม่ หลังจากนำการตัดสินใจไปใช้แล้ว (post) บทนี้จะมัดการติดตามสองอย่างนั้นเข้าเป็นเวิร์กโฟลว์เดียว
หัวใจของการวิเคราะห์ผลกระทบของการตัดสินใจนั้นเรียบง่ายอย่างคาดไม่ถึง คือมอง atom ของการตัดสินใจหนึ่งตัวเป็นโหนด แล้วอ่าน เอดจ์ที่เข้ามา และ เอดจ์ที่ออกไป จากโหนดนั้น การติดตามล่วงหน้าถามว่า "ถ้าเปลี่ยนการตัดสินใจนี้ ตรงไหนจะได้รับผลกระทบ" (เอาต์บาวด์ + การอ้างย้อนกลับ) ส่วนการติดตามภายหลังถามว่า "ผลกระทบนั้นเกิดขึ้นตามที่ตั้งใจไว้จริงหรือไม่" (วัดเอดจ์เดียวกันเทียบกับค่าที่วัดได้)
ในโปรเจกต์ A ของผู้เขียน เราเก็บการตัดสินใจไว้เป็น atom ในโฟลเดอร์ decisions/ ปัจจุบันสะสมไว้ 26 ตัว และแต่ละ atom ถือข้อมูลวันที่ ผู้เกี่ยวข้อง เหตุผล และขอบเขตผลกระทบ ไว้ในฟรอนต์แมตเตอร์ เครื่องมือที่สกัดขอบเขตผลกระทบคือ impact และ atom ที่บังคับกฎการสกัดนั้นในระดับการตัดสินใจคือ portal_layer_change_impact_check สามสิ่งนี้คือสินทรัพย์จริงของการติดตามทั้งล่วงหน้าและภายหลัง
การติดตามล่วงหน้าอ่านด้านขวา (เอาต์บาวด์) เพื่อทำนายว่า "จะลามมาถึงตรงนี้" ส่วนการติดตามภายหลังนำค่าที่วัดได้จริงของโหนดด้านขวามาเทียบกับการทำนาย เป็นการอ่านกราฟเดียวกันสองรอบด้วยจุดประสงค์ที่ต่างกัน
จากตรงนี้ไปเป็นเซสชันจริง ผู้เขียนจะไม่สรุปย่อ แต่จะนำพรอมต์ฉบับเต็ม ผลลัพธ์ดิบของ Claude และวิธีที่คนปฏิเสธมันแล้วร้องขอใหม่ มาแสดงไว้ตามจริง
impact เป็นไปป์ไลน์เล็ก ๆ ที่รับ ID ของ atom การตัดสินใจ แล้วดึงกราฟผลกระทบออกมา มันรวมเอดจ์อินบาวด์ (เหตุผลของการตัดสินใจนี้) เอดจ์เอาต์บาวด์ (ไฟล์ที่การตัดสินใจนี้จะแก้ไข) ความสัมพันธ์ affects ในออนโทโลยี และการอ้างย้อนกลับด้วยวิกิลิงก์ (เอกสารอื่นที่ชี้มาที่การตัดสินใจนี้ด้วย [[D2026_Q2_017]]) เข้าด้วยกันเพื่อสร้างรายการผลกระทบที่เป็นไปได้
ช่วยประเมินผลกระทบล่วงหน้าในขั้น proposal ของ decisions/D2026_Q2_017.md ให้หน่อย
- ไฟล์ที่การตัดสินใจนี้จะแก้ไข (เอาต์บาวด์)
- เอกสารที่อ้างย้อนกลับมาที่การตัดสินใจนี้ด้วย [[D2026_Q2_017]] (วิกิลิงก์อินบาวด์)
- ระบบที่เชื่อมโยงด้วยความสัมพันธ์ affects ในออนโทโลยี
รวมสามอย่างนี้เข้าด้วยกันเพื่อออกรายการผลกระทบที่เป็นไปได้ และในแต่ละรายการ
ให้แยกว่าเป็นรายการที่ต้องให้ฉันยืนยัน (T1) หรือเป็นรายการที่ยืนยันได้เองโดยอัตโนมัติ
อย่าเดาเติมเอง ถ้าไม่มีไฟล์ที่เป็นหลักฐาน
ให้ระบุว่า 'ไม่มีหลักฐาน'
ผมอ่าน
decisions/D2026_Q2_017.mdแล้ว นี่คือกราฟผลกระทบที่สกัดจากฟรอนต์แมตเตอร์เอาต์บาวด์ (การตัดสินใจนี้แก้ไขโดยตรง) -
data/CombatFormula.md— ฟิลด์global_cooldown(ระบุไว้ใน atom) -sheets/CombatBalance— แถวข้อยกเว้นของสกิลฟื้นฟู (ระบุไว้ใน atom) -ui/ComboIndicator— ประมาณการจากaffected_teams: [ui]ของ atom แต่ไม่มีไฟล์ที่เจาะจงใน atom → ไม่มีหลักฐาน (ประมาณการ)วิกิลิงก์อินบาวด์ (อ้างย้อนกลับมาที่การตัดสินใจนี้) - ผลของ
grepค้นด้วย[[D2026_Q2_017]]แต่ในดัชนีปัจจุบัน มีการอ้างย้อนกลับ 0 รายการ ดูเหมือนว่าเนื่องจากเป็นการตัดสินใจใหม่ จึงยังไม่มีใครอ้างถึงความสัมพันธ์ affects ในออนโทโลยี - มีความสัมพันธ์
CombatFormula → affects → TankSurvivalอยู่ในออนโทโลยี - ดังนั้นการตัดสินใจนี้ส่งผลทางอ้อมต่อตัวชี้วัดTankSurvivalผลกระทบที่ประมาณการได้ +12%
ตรงนี้คนหยุด บรรทัดสุดท้าย "ผลกระทบที่ประมาณการได้ +12%" คือปัญหา
ปฏิเสธ "ผลกระทบที่ประมาณการได้ +12%" เป็นตัวเลขที่ออกมาจากไหน ทั้งใน atom และในออนโทโลยีก็ไม่มีค่านั้นนี่ ถ้าเป็นค่าที่นายกุขึ้นเองก็เอาออกไป ให้รายงานแค่ข้อเท็จจริงว่าความสัมพันธ์ affects มีอยู่ ก็พอ ส่วนขนาดของผลกระทบต้องรันซิมถึงจะออกมา แล้วรายการ ui ที่ประมาณการไว้ ในเมื่อนายจัดเป็น 'ไม่มีหลักฐาน' แล้ว ทำไมถึงยกขึ้นมาในรายการเป็น T1 ล่ะ
ที่ชี้มาถูกต้อง ผมขอแก้ไขสองจุด
- ถอน "+12%" ออก ไม่มีหลักฐานของค่านั้นทั้งใน atom และในออนโทโลยี เป็นค่าที่ผมสร้างขึ้นเอง ผมขอรายงานแค่ ความสัมพันธ์ว่ามีผลกระทบ ต่อ
TankSurvivalส่วนขนาดต้องเติมด้วยผลจากซิม (Layer 2) ค่าปัจจุบันคือยังไม่ได้วัด- จัดประเภทรายการ ui ใหม่ เนื่องจากหาไฟล์ที่เจาะจงไม่เจอ ผมจึงควรลดมันลงจากรายการ จากที่เป็น T1 (ต้องยืนยัน) ไปเป็น
หลักฐานไม่เพียงพอ — ต้องให้คนระบุไฟล์ตารางที่แก้ไขแล้วเป็นดังนี้
| ผลกระทบที่เป็นไปได้ | ประเภทเอดจ์ | หลักฐาน | การจัดประเภท |
|---|---|---|---|
CombatFormula.md global_cooldown |
เอาต์บาวด์ | atom ระบุไว้ | ยืนยันอัตโนมัติ |
แถวข้อยกเว้นฟื้นฟู CombatBalance |
เอาต์บาวด์ | atom ระบุไว้ | ยืนยันอัตโนมัติ |
ตัวชี้วัด TankSurvival |
affects | ความสัมพันธ์ในออนโทโลยี | มีผลกระทบ / ยังไม่ได้วัดขนาด |
| การแสดงคอมโบบน ui (ไม่ทราบไฟล์) | ประมาณการ | ไม่มีหลักฐาน | ต้องให้คนระบุไฟล์ |
| การอ้างย้อนกลับอินบาวด์ | วิกิลิงก์ | grep 0 รายการ | ไม่เกี่ยวข้อง |
ตารางนี้คือผลผลิตของการติดตามล่วงหน้า หัวใจอยู่ที่บรรทัด TankSurvival ผลกระทบที่ทำให้ PvP พังตามที่เล่าในช่วงเปิดบทนั้น ในการติดตามล่วงหน้าได้ปรากฏขึ้นมาแล้วในรูป "มีผลกระทบ / ยังไม่ได้วัดขนาด" หากในวงประชุมได้เห็นสิ่งนี้ ก็คงกลายเป็น "งั้นมาวัดขนาดด้วยซิมแล้วค่อยตัดสินใจ" หากปล่อยให้ AI กุค่า +12% ขึ้นมา กลับยิ่งอันตรายกว่า เพราะความแม่นยำปลอม ๆ ทำให้คนข้ามการตรวจสอบไป
เมื่อการติดตามล่วงหน้าจบ ก็ตัดสินใจในที่ประชุม ในวินาทีที่การตัดสินใจถูกยืนยันเป็น atom ขั้นที่ย้อนกลับไม่ได้ สองอย่างก็เริ่มต้นขึ้น
flowchart TD
A["การติดตามล่วงหน้า
(กราฟ impact)"] --> B{ประชุมตัดสินใจ}
B -->|ปฏิเสธ| Z["ยกเลิก proposal
(บันทึกเหตุผล·ย้อนกลับได้)"]
B -->|อนุมัติ| C["ยืนยัน atom การตัดสินใจ
D2026_Q2_017"]
C --> D["⚠ ย้อนกลับไม่ได้ 1: นำเข้าบิลด์
แก้ CombatFormula·ชีต"]
C --> E["⚠ ย้อนกลับไม่ได้ 2: แพร่สู่เอกสารอื่น
อัปเดต GDD·การอ้างย้อนกลับ"]
D --> F["เริ่มการติดตามภายหลัง"]
E --> F
F --> G{ทำนาย = วัดจริง?}
G -->|ตรงกัน| H["ปิดการ์ดการตัดสินใจ"]
G -->|เบี่ยงเบน| I["atom ผลข้างเคียง
ตัวเลือกการตัดสินใจต่อเนื่อง"]
I --> B
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class D code;
class B human;
class A,C,E,I data;
class H pass;
class Z fail;
เหตุผลที่มันย้อนกลับไม่ได้นั้นเรียบง่าย ค่าที่ถูกนำเข้าบิลด์แล้วผู้ใช้ได้เล่นไปแล้ว ส่วนเนื้อหาที่แพร่ไปยังเอกสารอื่นก็ถูกเพื่อนร่วมทีมใช้เป็นหลักฐานเริ่มงานถัดไปไปแล้ว ดังนั้น ก่อน สองขั้นนี้ atom portal_layer_change_impact_check จึงคอยทำหน้าที่เป็นเกตขวางอยู่ กฎของ atom นี้สรุปได้ในบรรทัดเดียว "หากการตัดสินใจมีการเปลี่ยนแปลงข้าม Layer (data → สูตร → UI) อยู่ด้วย คนต้องอนุมัติรายการผลกระทบที่เป็นไปได้หนึ่งครั้งก่อนนำไปใช้" ถ้ามีแต่รายการที่ยืนยันอัตโนมัติก็ผ่าน แต่ถ้ามี 'ต้องให้คนระบุไฟล์' แม้แต่รายการเดียวก็จะถูกขวาง ในหัวข้อ 18.3.2 รายการ ui กลายเป็นเหตุให้ถูกขวาง
หลังนำเข้าบิลด์ไป 1 สัปดาห์ถึง 1 เดือน เราจะรันการติดตามภายหลัง เป็นงานแปะ ค่าที่วัดได้จริง ลงในแต่ละบรรทัดของตารางผลกระทบที่การติดตามล่วงหน้าสร้างไว้
ติดตามภายหลัง D2026_Q2_017 ช่วยแปะค่าที่วัดจริงลงในแต่ละผลกระทบที่เป็นไปได้
ของตารางการติดตามล่วงหน้า (18.3.2) ให้หน่อย
แหล่งค่าที่วัดจริง:
- git diff ของชีต CombatBalance (สำหรับยืนยันการนำเข้า)
- ล็อกซิม 1000 ครั้ง sim_2026q2_017.json
- KPI export 1 สัปดาห์หลังเปิดตัว kpi_w1.csv
ออกค่าความต่างระหว่างค่าที่ตั้งใจกับค่าที่วัดจริง และรวบเฉพาะบรรทัด
ที่หลุดจากช่วงคลาดเคลื่อน 10% ออกมาต่างหาก
ข้อมูลที่ไม่มีให้ระบุว่า 'ยังไม่ได้เก็บ'
ผมรวมค่าที่วัดจริงเข้ากับตารางการติดตามล่วงหน้าแล้ว
| ผลกระทบที่เป็นไปได้ | ตั้งใจ | วัดจริง | ต่าง | การตัดสิน |
|---|---|---|---|---|
| global_cooldown 0.3→0.5 | นำเข้า | ยืนยัน git diff | — | นำเข้าแล้ว |
| แถวข้อยกเว้นฟื้นฟู | นำเข้า | ยืนยันชีต | — | นำเข้าแล้ว |
| เวลาการต่อสู้ | +15% | +14.2% | -0.8%p | อยู่ในช่วง |
| TankSurvival | (ยังไม่ได้วัด) | +44% | ไม่มีฐานเทียบ | ตัดสินไม่ได้ |
| อัตราชนะ PvP 5:5 | +15%p | +29%p | +14%p | เบี่ยงเบน |
| ความอ่านง่ายของคอมโบบน ui | ปรับปรุง | ผู้ใช้พึงพอใจ 65% | — | ตรงกัน |
บรรทัดที่หลุดจากช่วงคลาดเคลื่อน (±10%): อัตราชนะ PvP +29%p (ตั้งใจ +15%p) ส่วน
TankSurvivalเนื่องจากไม่ได้วัดขนาดไว้ล่วงหน้าจึงไม่มีค่าที่ตั้งใจ ทำให้ตัดสินไม่ได้เมื่อเชื่อมสองบรรทัดเข้าด้วยกัน
TankSurvival +44%ดูจะเป็นสาเหตุของการเบี่ยงเบนของอัตราชนะ PvP การที่ในการติดตามล่วงหน้าปล่อยTankSurvivalไว้ในสภาพ "ยังไม่ได้วัดขนาด" คือสิ่งที่บอกเหตุการเบี่ยงเบนครั้งนี้ไว้ล่วงหน้าแล้ว
ถูกต้อง การที่ไม่ได้วัด
TankSurvivalด้วยซิมคือต้นตอของอุบัติเหตุ สร้าง atom การตัดสินใจต่อเนื่องขึ้นมา เป็นแผนปรับ dmg ของกลุ่มอาชีพแทงก์เพื่อดึงอัตราชนะ PvP กลับเข้าช่วงที่ตั้งใจ
ผมสร้างร่าง
decisions/D2026_Q2_025.mdแล้ว ได้ใส่[[D2026_Q2_017]]ไว้ในอินบาวด์ เพื่อระบุด้วยการอ้างย้อนกลับว่าการตัดสินใจครั้งนี้คือ การปรับแก้ผลข้างเคียง ของหมายเลข 17 ตอนนี้ถ้าค้น atom หมายเลข 17 ด้วยimpactอีกครั้ง จะพบการอ้างย้อนกลับอินบาวด์ 1 รายการ (หมายเลข 25) — ที่ตำแหน่งซึ่งเคยเป็น 0 รายการในหัวข้อ 18.3.2
ตรงนี้กราฟปิดลง โหนดที่ในการติดตามล่วงหน้าเป็น "มีผลกระทบ / ยังไม่ได้วัดขนาด" ได้รับการยืนยันว่าเบี่ยงเบนในการติดตามภายหลัง และการตัดสินใจต่อเนื่องก็เข้ามาเป็นการอ้างย้อนกลับที่ชี้มาที่โหนดนั้น วงจรทั้งวงของการตัดสินใจได้หมุนครบรอบบนกราฟเดียวกัน
การสกัดการอ้างย้อนกลับอินบาวด์ของ impact ไม่ใช่เครื่องมือหวือหวาอะไร แต่เป็น grep บรรทัดเดียว มันค้นหาวิกิลิงก์ที่ชี้มาที่ atom การตัดสินใจจากในเอกสารทั้งหมด
# เอกสารทั้งหมดที่อ้างย้อนกลับมาที่ D2026_Q2_017 (วิกิลิงก์อินบาวด์)
grep -rln "\[\[D2026_Q2_017\]\]" decisions/ manuscript/ gdd/
# เอาต์บาวด์ของ atom การตัดสินใจ — สกัด affected_files จากฟรอนต์แมตเตอร์
grep -A20 "affected_files:" decisions/D2026_Q2_017.md
# การติดตามภายหลัง: เฉพาะบรรทัดที่เบี่ยงเบนจากค่าที่ตั้งใจ (คอลัมน์การตัดสิน)
grep -E "이탈|판정 불가" tracking/D2026_Q2_017_post.md
สามบรรทัดก็ทำให้โครงของการติดตามทั้งล่วงหน้าและภายหลังหมุนได้ LLM อยู่ในตำแหน่งของการ อ่านและตีความ ผลลัพธ์นี้ ไม่ใช่ทำหน้าที่ค้นหาแทน grep ให้ข้อเท็จจริง (ไฟล์ใดบ้างที่ชี้มาที่การตัดสินใจนี้) LLM ร้อยข้อเท็จจริงเหล่านั้นเข้าเป็นตารางผลกระทบที่เป็นไปได้ และคนรับผิดชอบขนาดและการตัดสินของผลกระทบ การแยกส่วนแบบนี้คือเหตุผลที่ "อย่ากุ +12% ขึ้นมา" ใช้ได้ผลใน §18.3.2
นี่คือค่าที่เปรียบเทียบก่อน-หลังการทำให้วงจรการตัดสินใจเป็นมาตรฐานในโปรเจกต์ A ของผู้เขียน ค่าเวลาสัมบูรณ์เป็น การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ซึ่งขึ้นกับขนาดทีม (ขนาดกลาง 10\~50 คน) ส่วนอัตราส่วนและทิศทางเป็นสิ่งที่สังเกตได้จากการดำเนินงานจริง
| รายการ | แยกการติดตามก่อน-หลัง | รวมการติดตามก่อน-หลัง |
|---|---|---|
| สัดส่วนการตัดสินใจที่รันการติดตามภายหลังจริง | ราว 30% | 90% ขึ้นไป |
| ผลกระทบที่ปรากฏล่วงหน้าแต่กลายเป็นอุบัติเหตุภายหลัง | พบบ่อย | แทบไม่มี (มีเกตล่วงหน้า) |
| อัตราการเชื่อมผลข้างเคียง → การตัดสินใจต่อเนื่อง | ต่ำ (ส่งต่อด้วยปากเปล่า) | กลายเป็นตัวเลือกอัตโนมัติด้วยการอ้างย้อนกลับ |
| ความครบถ้วนของการอ้างย้อนกลับอินบาวด์ของกราฟการตัดสินใจ | กระจัดกระจาย | ลูปปิด |
หัวใจมีอย่างเดียว เมื่อการติดตามล่วงหน้าและการติดตามภายหลังใช้ ตารางผลกระทบที่เป็นไปได้เดียวกันร่วมกัน ช่องโหว่ที่ปล่อยไว้เป็น "ยังไม่ได้วัดขนาด" ล่วงหน้า จะถูกยืนยันภายหลังตรงตำแหน่งนั้นพอดี หากแยกกันอยู่ สิ่งที่เห็นล่วงหน้ากับสิ่งที่วัดภายหลังจะอยู่คนละรูปแบบ ทำให้เทียบกันไม่ได้ และนั่นคือเหตุที่อัตราการติดตามค้างอยู่ที่ 30% อย่างไรก็ตาม หากตั้งเป้าความครบถ้วนของการอ้างย้อนกลับไว้ที่ 100% ตั้งแต่แรก ก็จะมีแต่ภาระการดำเนินงานที่เพิ่มขึ้น สิ่งที่สมจริงคือเริ่มจากการสร้างนิสัยเขียน affected_files ลงใน atom การตัดสินใจ แล้วแทรก grep การอ้างย้อนกลับเข้าไปในรอบการทบทวน เพื่อขยายผลทีละน้อย
| รูปแบบ | วิธีจัดการ |
|---|---|
| เห็นผลกระทบล่วงหน้าแต่ตัดสินใจโดยไม่วัดขนาด | บรรทัด "ยังไม่ได้วัดขนาด" ให้พักการตัดสินใจไว้จนกว่าจะรันซิม |
| LLM กุค่าผลกระทบขึ้นมา | ถ้าไม่มีไฟล์หลักฐานให้ระบุ 'ไม่มีหลักฐาน' ขนาดให้ได้จากซิมเท่านั้น |
| การติดตามภายหลังเป็นคนละรูปแบบกับตารางล่วงหน้า | เพิ่มแค่คอลัมน์ค่าที่วัดจริงลงในตารางผลกระทบเดียวกัน |
| ส่งต่อผลข้างเคียงด้วยปากเปล่า | บังคับ atom การตัดสินใจต่อเนื่อง + วิกิลิงก์การอ้างย้อนกลับ |
| นำการเปลี่ยนแปลงข้าม Layer ไปใช้โดยไม่ผ่านเกต | บังคับให้ผ่าน portal_layer_change_impact_check |
การประยุกต์นอกเกม การอ่านสองรอบ — มองว่า "ลามไปถึงไหน" ก่อนกดยืนยันการตัดสินใจ (ล่วงหน้า) และตรวจสอบว่า "ลามไปแค่ตรงนั้นจริงหรือไม่" หลังนำไปใช้ (ภายหลัง) — ไม่ใช่เรื่องของเกม แต่เป็นการทำงานพื้นฐานของการบริหารการเปลี่ยนแปลงทุกอย่าง เมื่อบริษัทจะเปลี่ยนนโยบายราคา หากแสดงแผนกที่ได้รับผลกระทบ (ฝ่ายขาย·CS·บัญชี) เป็นตารางผลกระทบที่เป็นไปได้ไว้ล่วงหน้า แล้วปล่อย "ขนาดยังไม่ได้วัดจนกว่าจะรันซิม" ไว้ ก็จะป้องกันอุบัติเหตุแบบ "ทำไมฝ่ายบัญชีถึงไม่รู้เรื่องนี้" หลังเปิดตัวได้ล่วงหน้า ยกตัวอย่างเช่น ก่อนนำระดับสมาชิกใหม่มาใช้ หากทำช่องตัวชี้วัดภายหลัง เช่น ปริมาณคำถาม CS·อัตราการเลิกใช้ ไว้เป็นช่องว่างในตารางล่วงหน้า แล้วเมื่อผ่านไปหนึ่งเดือนค่อยเติมค่าที่วัดจริงลงในช่องนั้น ก็จะเทียบความต่างระหว่างค่าที่ตั้งใจกับค่าจริงได้ทันทีในตารางเดียวกัน
setup — สร้างโฟลเดอร์การตัดสินใจและโฟลเดอร์การติดตาม
mkdir decisions tracking
# ระบุ affected_files, affected_teams เป็นฟรอนต์แมตเตอร์ใน atom การตัดสินใจ 1 ตัว
prompt — เชื่อมการติดตามล่วงหน้ากับการติดตามภายหลังด้วยตารางเดียวกัน
ประเมินผลกระทบล่วงหน้าของ decisions/<ID>.md: รวมเอาต์บาวด์ (ไฟล์ที่จะแก้)·
วิกิลิงก์อินบาวด์·affects ในออนโทโลยี เข้าด้วยกันเพื่อสร้างตารางผลกระทบที่เป็นไปได้
รายการที่ไม่มีหลักฐานให้ระบุ 'ไม่มีหลักฐาน' ขนาดให้ระบุ 'ยังไม่ได้วัด' อย่ากุค่าขึ้นมา
(หลังนำเข้าบิลด์)
แปะแค่คอลัมน์ค่าที่วัดจริงลงในตารางเดียวกัน แล้วรวบเฉพาะบรรทัดที่หลุดจากช่วง
คลาดเคลื่อน 10% เทียบกับค่าที่ตั้งใจ
บรรทัดที่เบี่ยงเบนให้ทำเป็นร่าง atom การตัดสินใจต่อเนื่อง แล้วใส่การอ้างย้อนกลับ [[<ID>]]
verify — ตรวจด้วย grep ว่ากราฟปิดแล้วหรือยัง
grep -rln "\[\[<ID>\]\]" decisions/ # ถ้าพบการอ้างย้อนกลับของการตัดสินใจต่อเนื่อง แปลว่าลูปปิด
grep -E "이탈|미측정" tracking/<ID>_post.md # ตรวจช่องโหว่ที่เหลือ
หากคุณเป็นนักพัฒนาเกมรายเดียวที่ทำงานคนเดียว จะตัดการประชุม·ผู้รับผิดชอบ·กำหนดเวลาออกทั้งหมดก็ได้ เมื่อเขียนการตัดสินใจหนึ่งบรรทัดลงในมาร์กดาวน์ decisions/ ให้เติมแค่ สองช่องเท่านั้น คือ affected_files: (ไฟล์ที่การตัดสินใจนี้จะแตะ) และ expected: (การเปลี่ยนแปลงที่ตั้งใจ) หลังบิลด์แล้วให้เปิดไฟล์เหล่านั้นดูด้วยตาว่าเป็นไปตามที่ตั้งใจหรือไม่ ถ้ามีอะไรคลาดเคลื่อนก็เพิ่มอีกหนึ่งบรรทัด actual: ลงในไฟล์เดียวกัน เครื่องมือใช้แค่ grep -rln "[[รหัสการตัดสินใจ]]" ตัวเดียวก็พอ หนึ่งช่องล่วงหน้า หนึ่งช่องภายหลัง — นี่คือรูปแบบขั้นต่ำที่สุดของการติดตามก่อน-หลัง
วันจันทร์ เวลา 10 โมงเช้า สมาชิกทีม A ผู้ดูแลระบบการต่อสู้โยนคำถามหนึ่งบรรทัดลงในแชตภายในทีมว่า "ลดคูลดาวน์ส่วนกลางจาก 0.5 วินาทีลงมาเป็น 0.4 วินาทีได้ไหมครับ" มันคืองานเปลี่ยนตัวเลขเพียงตัวเดียว อย่างน้อยก็ดูเผินๆ ผมอ่านบรรทัดนั้นแล้วมือก็หยุดนิ่ง เพราะในหัวนึกไม่ออกว่ามีเอกสารกี่ฉบับที่ป้อนตัวเลขนี้ไว้ มี atom ของการปรับสมดุลสกิลกี่ตัวที่ออกแบบขึ้นโดยถือค่าคงที่นี้เป็นเงื่อนไขตั้งต้น และถ้าเปลี่ยนมันแล้วสูตรในชีตไหนบ้างจะพัง การคิดว่านึกออกแล้วต่างหากที่เป็นอุบัติเหตุ ความจริงเบื้องหลังเหตุการณ์ "ไม่เห็นเอกสารฉบับนั้น" ที่ระเบิดออกมา 8 ถึง 12 ครั้งต่อไตรมาส ก็คือความเข้าใจผิดเช่นนี้นี่เอง
ผมจึงตัดสินใจว่าจะไม่ท่องจำคำตอบ แต่จะพิมพ์เพียงบรรทัดเดียวแทน
impact combat_global_cooldown_constant
บทนี้จะดูกันแบบดิบๆ ว่าบรรทัดเดียวนั้นคายอะไรออกมา และจะแสดงให้เห็นว่าการดึงขอบเขตผลกระทบไม่ใช่คำพูดเชิงนามธรรม แต่เป็นการกระทำที่เป็นรูปธรรม คือการใช้ grep กวาดรวบสามสาย ได้แก่ ขอบเข้า (inbound edge), ออนโทโลยี affects, และการอ้างอิงย้อนกลับของ wikilink
คำถามที่ว่า "ถ้าเปลี่ยน atom นี้แล้วอะไรจะได้รับผลกระทบบ้าง" แท้จริงแล้วคือคำถามสามข้อ ถ้านำทั้งสามมาปนกันคำตอบก็ขุ่นมัว แต่ถ้าแยกทั้งสามออกจากกัน ก็จะลงตัวเป็น grep สายละหนึ่งบรรทัด
หนึ่ง ขอบเข้า (inbound edge) — ใครชี้มาที่ฉัน เมื่อ atom A อ้างอิง atom B นั่นคือขอบในทิศ A→B สิ่งที่เป็นอันตรายเวลาเปลี่ยน B คือเหล่า A ที่ชี้มาที่ B นั่นคือลูกศรที่พุ่งเข้าหา B จึงต้องดูขอบเข้า (มีใครมองมาที่ฉัน) ไม่ใช่ขอบออก (ฉันมองใคร) คลื่นกระแทกของการเปลี่ยนแปลงจะย้อนทวนขึ้นไปตามลูกศร
สอง ออนโทโลยี affects — เชิงความหมายแล้วมันส่งผลต่ออะไร นี่คือฟิลด์ affects: ที่ระบุไว้ใน frontmatter ของ atom แม้ชื่อจะไม่ปรากฏโดยตรง แต่เป็นการเชื่อมโยงเชิงความหมายที่ผู้ออกแบบได้ประกาศไว้ว่า "สิ่งนี้ส่งผลต่อสิ่งนั้น" มันคือสิ่งที่มนุษย์ป้อนไว้ล่วงหน้าเพื่อแก้ปัญหานามแฝงและคำพ้องความหมายที่ grep จับไม่ได้
สาม การอ้างอิงย้อนกลับของ wikilink — เอกสารที่ลิงก์มาที่ฉันอย่างชัดเจนในรูปแบบ [[atom_id]] มีความน่าเชื่อถือสูงที่สุด เพราะไม่ใช่การบังเอิญตรงกันของคำ แต่เป็นลิงก์ที่ผู้เขียนตั้งใจวางไว้
หากมองความสัมพันธ์ของทั้งสามสายเป็นแผนภาพ ก็เป็นดังนี้
สิ่งที่มัดสามสายเข้าไว้ในเครื่องมือเดียวคือ impact ในหัวข้อถัดไป เราจะดูผลลัพธ์ที่เครื่องมือนี้คายออกมาจริงทั้งก้อน
แทนที่จะอธิบายด้วยคำพูด ผมจะติดตามไปทีละขั้นตั้งแต่ต้นจนจบของหนึ่งรอบจริง โดยรวมทั้งพรอมต์ฉบับเต็ม ผลลัพธ์ดิบของเครื่องมือ (ไม่ขัดเกลา) ส่วนที่ผมตรวจสอบและปฏิเสธ ไปจนถึงการร้องขอใหม่ทั้งหมด
ผมขอให้ Claude รันเวิร์กโฟลว์ impact ตัวเวิร์กโฟลว์เองคือเชลล์ที่เรียก ripgrep แต่การตีความผลลัพธ์และการแนะนำระดับนั้นเป็นหน้าที่ของ Claude
ช่วยรันเวิร์กโฟลว์ impact กับ combat_global_cooldown_constant ให้หน่อย
ดึงสามสาย (ขอบเข้า / affects / อ้างอิงย้อนกลับ wikilink) ด้วย grep แยกกัน
แล้วรวมรายการซ้ำเข้าด้วยกัน จากนั้นติดคำแนะนำระดับผลกระทบให้แต่ละเป้าหมายที่ได้รับผลกระทบ
เนื้อหาที่จะเปลี่ยนคือ "คูลดาวน์ส่วนกลาง 0.5 วินาที → 0.4 วินาที"
จะดำเนินการสามสายตามลำดับ
[1/3] ขอบเข้า — ตำแหน่งที่อ้างอิง atom นี้
$ rg -n "combat_global_cooldown_constant" --type md docs/ atoms/
atoms/combat/combat_skill_cooldown_rule.md:34: base: combat_global_cooldown_constant
atoms/combat/combat_gcd_ui_feedback.md:12: depends_on: combat_global_cooldown_constant
docs/spec/combat_v3.md:42: อ้างอิงคูลดาวน์ส่วนกลาง (combat_global_cooldown_constant) เป็นเกณฑ์
docs/spec/skill_design.md:18: คูลดาวน์ออกแบบเป็นทวีคูณของ combat_global_cooldown_constant
docs/balance/refgame_comparison.md:77: refgame คือ 0.5 วินาที combat_global_cooldown_constant ของเราก็เท่ากัน
→ พบการอ้างอิงโดยตรง 5 รายการ
[2/3] ออนโทโลยี affects — จุดที่ระบุ atom นี้ไว้ในฟิลด์ affects
$ rg -n "^\s*-?\s*combat_global_cooldown_constant" -B3 atoms/ | rg -B3 "affects:"
atoms/combat/combat_dps_curve.md:
affects:
- combat_global_cooldown_constant ← (ระวัง: ต้องตรวจสอบทิศทาง)
→ 1 รายการ แต่นี่คือกรณีที่ dps_curve ประกาศว่า "ส่งผลกระทบ"
ซึ่งทิศทางอาจกลับกัน ต้องให้มนุษย์ตรวจสอบ — ดูรายการตรวจสอบด้านล่าง
[3/3] อ้างอิงย้อนกลับ wikilink — ลิงก์รูปแบบ [[...]]
$ rg -n "\[\[combat_global_cooldown_constant" --type md
docs/spec/combat_v3.md:42: ...[[combat_global_cooldown_constant]]...
atoms/combat/combat_skill_cooldown_rule.md:34: ...[[combat_global_cooldown_constant]]...
→ 2 รายการ ทั้งคู่เป็นตำแหน่งที่จับได้แล้วใน [1/3] (ซ้ำ)
[รวมยอด — เป้าหมายผลกระทบเฉพาะหลังตัดซ้ำ]
1. atoms/combat/combat_skill_cooldown_rule.md (ขอบเข้า+wikilink)
2. atoms/combat/combat_gcd_ui_feedback.md (ขอบเข้า)
3. docs/spec/combat_v3.md (ขอบเข้า+wikilink)
4. docs/spec/skill_design.md (ขอบเข้า)
5. docs/balance/refgame_comparison.md (ขอบเข้า)
6. atoms/combat/combat_dps_curve.md (affects — ทิศทางยังไม่ยืนยัน)
[คำแนะนำระดับ — ตามเกณฑ์ผลกระทบ 5 ระดับใน 18.2]
1. combat_skill_cooldown_rule → T1 (คูลดาวน์ของทุกสกิลเป็นทวีคูณของค่าคงที่นี้)
2. combat_gcd_ui_feedback → T2 (จังหวะ UI feedback ต้องปรับแก้เชิงภาพ)
3. combat_v3.md → T3 (อัปเดตตัวเลขในเอกสารสเปก)
4. skill_design.md → T3 (อัปเดตตัวเลขในเอกสารออกแบบ)
5. refgame_comparison.md → T2 (ตารางเปรียบเทียบเบนช์มาร์ก ต้องคำนวณช่องว่างเทียบ refgame ใหม่)
6. combat_dps_curve.md → ยังไม่ระบุ (ระบุระดับหลังตรวจสอบทิศทาง)
จุดที่ผมหยุดมีสองแห่ง
ปฏิเสธ 1 — ทิศทางของ affects การที่ combat_dps_curve.md เขียนค่าคงที่ของเราไว้ในฟิลด์ affects: คือการประกาศว่า dps_curve ส่งผลกระทบ ต่อค่าคงที่นี้ แต่ตอนนี้สิ่งที่เรากำลังเปลี่ยนคือฝั่งค่าคงที่ นั่นคือทิศทางของลูกศรกลับกัน ไม่ใช่ dps_curve เปลี่ยนค่าคงที่ แต่เมื่อค่าคงที่เปลี่ยน เงื่อนไขตั้งต้นในการคำนวณของ dps_curve ต่างหากที่สั่นคลอน การที่ Claude ระงับไว้อย่างตรงไปตรงมาว่า "ต้องตรวจสอบทิศทาง" นั้นถูกต้อง ผมจึง คงรายการนี้ไว้เป็นเป้าหมายผลกระทบ แต่จัดประเภทใหม่ไม่ใช่เป็น affects หากเป็น "การพึ่งพาเชิงความหมายในทิศทางย้อนกลับ" และยืนยันเป็น T2
ปฏิเสธ 2 — การมองตารางเปรียบเทียบ refgame เป็น T2 นั้นประเมินเกินจริง refgame_comparison.md เป็นเพียงตารางเปรียบเทียบตัวเลขล้วนๆ กับเกมภายนอก เมื่อตัวเลขของเราเปลี่ยน ก็แค่แก้ช่องเดียวในตารางจาก 0.5→0.4 ก็จบ และไม่มีระบบใดใช้ตารางนี้เป็นอินพุต จึงลดระดับลงเป็น T3 (อัปเดตตัวเลขในเอกสาร)
ช่วยแก้สองจุดแล้วจัดตารางผลกระทบใหม่ให้หน่อย
1) combat_dps_curve.md คงเป็นเป้าหมายผลกระทบ เปลี่ยนป้ายเป็น "การพึ่งพาเชิงความหมายในทิศทางย้อนกลับ"
ยืนยันระดับ T2
2) refgame_comparison.md ลดระดับ T2 → T3 (ไม่มีระบบใดใช้เป็นอินพุต แก้ตารางช่องเดียว)
แล้วรวมเฉพาะ T1·T2 แยกเป็นรายการ "ต้องตรวจสอบก่อนเปลี่ยน" ให้ด้วย
ผลลัพธ์สุดท้ายที่ Claude จัดมามีดังนี้
[ต้องตรวจสอบก่อนเปลี่ยน — T1·T2]
T1 combat_skill_cooldown_rule.md คูลดาวน์ของทุกสกิลเป็นทวีคูณของค่าคงที่นี้
T2 combat_gcd_ui_feedback.md ปรับจังหวะ UI feedback ใหม่
T2 combat_dps_curve.md การพึ่งพาเชิงความหมายในทิศทางย้อนกลับ — ตรวจสอบเงื่อนไขตั้งต้นของเส้นโค้ง DPS ใหม่
[อัปเดตตัวเลขในเอกสารเท่านั้น — T3]
T3 combat_v3.md / skill_design.md / refgame_comparison.md
เป้าหมายผลกระทบ 6 รายการที่ตอนแรก "นึกไม่ออก" ในหัว กลายเป็นรายการที่ถูกจัดลำดับความสำคัญด้วย grep หนึ่งรอบและการตัดสินของมนุษย์สองครั้ง นี่คือเนื้อแท้ของการดึงขอบเขตผลกระทบ เครื่องมือกวาดรวบรายการที่เป็นไปได้ทั้งหมด แล้วมนุษย์จับทิศทางและระดับ
หากนำหนึ่งรอบในหัวข้อก่อนหน้ามาทำให้เป็นกระแสทั่วไป ก็เป็นดังนี้ หัวใจอยู่ที่ว่าขั้นตอนอัตโนมัติกับขั้นตอนของมนุษย์แยกกันตรงไหน
flowchart TD
A[ระบุ atom เป้าหมายที่จะเปลี่ยน
combat_global_cooldown_constant] --> B{รัน impact}
B --> C1[grep ขอบเข้า
rg atom_id]
B --> C2[grep ฟิลด์ affects
rg บล็อก affects:]
B --> C3[grep อ้างอิงย้อนกลับ wikilink
rg วงเล็บเหลี่ยม atom_id]
C1 --> D[ตัดซ้ำ · รวมยอด]
C2 --> D
C3 --> D
D --> E[LLM แนะนำระดับ
ป้าย T0~T4]
E --> F{มนุษย์ตรวจสอบ}
F -->|ทิศทางผิด·ระดับเกินจริง| G[ปฏิเสธแล้วร้องขอใหม่]
G --> E
F -->|อนุมัติ| H[ยืนยันรายการตรวจสอบก่อนเปลี่ยน
แยก T1·T2]
H --> I[แนบอัตโนมัติในคอมเมนต์คำขอเปลี่ยนแปลง]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class B,C1,C2,C3,D,I code;
class E ai;
class F,G human;
class A,H data;
ส่วนที่เป็นอัตโนมัติคือ grep สามสายกับการรวมยอด และ ร่าง ของการระบุระดับ ส่วนที่เป็นของมนุษย์มีเพียงหนึ่งเดียวคือ การตัดสินขั้นสุดท้ายเรื่องทิศทางและระดับ ทั้ง affects ทิศทางย้อนกลับและการลดระดับ refgame ที่เห็นในรอบก่อนหน้าก็เกิดขึ้น ณ ตำแหน่งนี้พอดี ถ้าเชื่อเครื่องมือ 100% ก็จะเกิดอุบัติเหตุสองแบบ คือถอด affects ทิศทางย้อนกลับออกจากเป้าหมายผลกระทบ หรือปกป้องตารางเปรียบเทียบเกินเหตุจนต้องวนรีวิวที่ไม่จำเป็นทุกครั้ง การวางเส้นแบ่งระหว่างอัตโนมัติกับมนุษย์ไว้ที่จุดเดียวนี้คือเจตนาในการออกแบบเวิร์กโฟลว์
ผมจะเขียนรูปแบบ ripgrep ที่ภายในของ impact เรียกใช้ลงไปตามจริง นี่คือเนื้อแท้ของเครื่องมือ — ไม่ใช่โครงสร้างพื้นฐานหรูหรา แต่เป็นนิพจน์ปรกติที่ผ่านการตรวจสอบแล้วสามบรรทัด
ขอบเข้า คือทุกตำแหน่งที่ atom ID ปรากฏในเนื้อหาเอกสาร กวาดได้กว้างที่สุด
rg -n "combat_global_cooldown_constant" --type md docs/ atoms/
ฟิลด์ affects ดูเฉพาะกรณีที่ atom ID อยู่ภายในบล็อก affects: ใช้ -B3 ดึง 3 บรรทัดก่อนหน้ามาด้วย เพื่อให้มนุษย์ดูด้วยตาว่านั่นเป็นบล็อก affects หรือเป็นฟิลด์อื่น
rg -n "combat_global_cooldown_constant" -B3 atoms/ | rg -B3 "affects:"
อ้างอิงย้อนกลับ wikilink เฉพาะลิงก์ที่ระบุชัดและล้อมด้วยวงเล็บเหลี่ยมสองชั้น มีความน่าเชื่อถือสูงสุดจึงเป็นเป้าหมายที่ตรวจสอบก่อน
rg -n "\[\[combat_global_cooldown_constant" --type md atoms/ docs/
ทั้งสามรูปแบบมีความน่าเชื่อถือกับค่าความครบถ้วน (recall) แปรผกผันกันพอดี wikilink แม่นยำเกือบ 100% แต่ถ้าผู้เขียนไม่วางลิงก์ก็จับไม่ได้ ขอบเข้ากวาดได้หมดแต่มีการบังเอิญตรงกันของคำ (noise) ปนมา affects จับความหมายได้แต่ทิศทางสับสน ต้องรวมทั้งสามจึงจะอุดรูรั่ว ถ้าใช้เพียงอย่างเดียวก็จะมีจุดที่รั่วเสมอ
การดึงขอบเขตผลกระทบเป็นขั้นตอนหนึ่งของวงรอบการตัดสินใจ (§18.3) ในวินาทีที่การ์ดการตัดสินใจถูกลงทะเบียน impact จะรันโดยรับสล็อต affected_atoms ของการ์ดนั้นเป็นอินพุต atom ที่ตรวจสอบการเชื่อมต่อนี้คือ portal_layer_change_impact_check
บทบาทของ atom นี้คือ "กันไม่ให้ข้ามการตรวจสอบผลกระทบ เมื่อเป็นการเปลี่ยนแปลงที่ตัดข้าม Layer" การเปลี่ยนค่าคงที่ของคูลดาวน์เป็นเพียงตัวเลขหนึ่งตัวใน L1 (ระบบ) แต่ผลกระทบของมันลามไปถึง L3 (สูตรในชีตข้อมูล) และ L4 (รายการ QA ของบิลด์) portal_layer_change_impact_check จะตัดสินว่าการเปลี่ยนแปลงข้ามขอบเขต Layer หรือไม่ และถ้าข้ามก็จะบังคับให้รัน impact
---
name: portal_layer_change_impact_check
type: gate
description: การเปลี่ยนแปลงที่ข้ามขอบเขต Layer ห้ามนำเข้าบิลด์ก่อนผ่านการตรวจสอบผลกระทบ
trigger:
- เมื่อลงทะเบียนการ์ดการตัดสินใจ affected_atoms ไม่ว่าง
- layer ของ atom ที่เปลี่ยน != layer ของ atom ที่ได้รับผลกระทบ
action:
- รัน impact (grep สามสาย)
- ถ้ามีเป้าหมายผลกระทบ T1·T2 ให้บล็อกการ merge ก่อนติ๊ก "ตรวจสอบเสร็จ"
---
ในกรณีคูลดาวน์ สิ่งที่เกตนี้จับได้ไม่ใช่ combat_skill_cooldown_rule (L1) แต่เป็นชีต CombatBalance (L3) ที่ใช้ rule นั้นเป็นอินพุต คอลัมน์ทวีคูณคูลดาวน์ของชีตเขียนสูตรขึ้นโดยถือค่าคงที่เป็นเงื่อนไขตั้งต้น grep กวาด atom จากเอกสาร และเกตก็ผลักดันว่า "นี่ข้าม Layer แล้ว ไปดูถึงชีตด้วย" ถ้าทั้งสองไม่ถูกมัดเข้าด้วยกัน ก็จะเกิดการตกหล่นแบบฉบับที่เอกสารถูกอัปเดตแล้วแต่ชีตยังคงค้างอยู่บนเงื่อนไขตั้งต้นเดิม
นี่คือความเปลี่ยนแปลงที่สังเกตได้จากการดำเนินงานโปรเจกต์ A ของผู้เขียน ตัวเลขเวลาเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ส่วนจำนวนอุบัติเหตุการตกหล่นเป็นค่าที่นับจริงในการทบทวนรายไตรมาส
| รายการ | ไม่มีเวิร์กโฟลว์ | ดำเนินงานด้วย impact |
|---|---|---|
| เวลาที่ใช้เข้าใจ atom ที่ได้รับผลกระทบ | พึ่งความจำ (ไม่ครบ) | 1\~2 นาที (grep ทั้งหมด) |
| อุบัติเหตุการตกหล่นจากการเปลี่ยนแปลง | 8\~12 ครั้งต่อไตรมาส (นับจริง) | 1\~2 ครั้งต่อไตรมาส (นับจริง) |
| การแนบผลกระทบในคำขอเปลี่ยนแปลง | มนุษย์ทำเป็นครั้งคราว | เกตบังคับ |
| สมาชิกทีมใหม่เข้าใจผลกระทบ | หลายวัน (บอกปากเปล่าทีละราย) | 30 นาที (เครื่องมือ + การ์ด) |
| ต้นทุนโครงสร้างพื้นฐาน | พิจารณานำกราฟ DB เข้ามา | ripgrep + เชลล์เท่านั้น |
บรรทัดสุดท้ายคือบทสรุปของทั้งบทนี้ โปรเจกต์ A เคยพิจารณากราฟ DB และดัชนีการค้นหา แต่สุดท้ายก็มาลงตัวที่ ripgrep กับเชลล์เล็กๆ เครื่องมือวัดที่แม่นยำย่อมแม่นกว่าสายวัดก็จริง แต่เครื่องมือที่หยิบมาใช้ทุกวันจะค่อยๆ ลู่เข้าหาฝั่งสายวัดที่ไม่เสียและไม่ต้องมีโครงสร้างพื้นฐาน การที่อุบัติเหตุการตกหล่นลดจาก 8\~12 ครั้งเหลือ 1\~2 ครั้ง ไม่ใช่เพราะเครื่องมือประณีตขึ้น แต่เพราะมันรันได้ครบทุกครั้งโดยไม่ตกหล่น
แม้จะมัดทั้งสามสายเข้าด้วยกัน ก็ยังมีจุดที่รั่ว การรู้แล้วใช้กับการไม่รู้แล้วเชื่อนั้นต่างกัน
นามแฝงและคำย่อ ถ้าเนื้อหาเขียนแค่ว่า "GCD (Global Cooldown, คูลดาวน์ส่วนกลาง)" ก็จะไม่ติด grep ของ combat_global_cooldown_constant การชดเชยคือขยายคำค้นด้วยนิพจน์ปรกติ — (combat_global_cooldown_constant|GCD|전역\s?쿨다운) จัดการพจนานุกรมคำย่อของทีมแยกต่างหาก แล้วสังเคราะห์รวมเข้าด้วยกันอัตโนมัติเวลาค้นหา
พื้นที่ที่ย้อนกลับไม่ได้ grep เป็นเครื่องมือของขั้นที่ย้อนกลับได้ ก่อนนำเข้าบิลด์ ผลกระทบระหว่างเอกสาร atom และชีตล้วนมองเห็นได้ด้วย grep แต่ปฏิกิริยาหลังจากบิลด์ออกไปและผู้เล่นสัมผัสคูลดาวน์ 0.4 วินาที — เสียงบ่นในคอมมูนิตี้ ความเปลี่ยนแปลงของเทมโปที่สัมผัสได้ — ไม่ใช่เป้าหมายของ grep หลักการจึงเรียบง่าย การตรวจสอบด้วย grep ทั้งหมดต้องจบก่อนนำเข้าบิลด์ เมื่อข้ามไปสู่ขั้นที่ย้อนกลับไม่ได้ สิ่งที่ grep บอกได้ก็จะลดลงอย่างรวดเร็ว
ตำแหน่งของการตรวจสอบโดย LLM เช่นเดียวกับที่มนุษย์จับทิศทาง affects และระดับในรอบก่อนหน้า ถ้าเสียบ LLM เข้าไปในการตัดสินความเหมาะสมของรายการที่เป็นไปได้จาก grep ก็จะกรอง noise ออกได้ แต่ LLM เองก็ไม่ใช่ 100% การอนุมัติขั้นสุดท้ายจึงเป็นของมนุษย์ ในโครงสร้างที่เครื่องมือ·LLM·มนุษย์กรองทีละขั้น ความแม่นยำจะไปถึงระดับที่ดำเนินงานได้ ถ้าตัดออกแม้เพียงขั้นเดียว อุบัติเหตุชนิดที่ขั้นนั้นเคยจับพลาดก็จะกลับเข้ามาอีก
การประยุกต์นอกเกม นิสัยที่กวาด "ถ้าเปลี่ยนรายการนี้แล้วตรงไหนจะสั่นคลอน" ด้วยการค้นหาทั้งหมดแทนความจำ ให้ผลแบบเดียวกันกับพนักงานออฟฟิศคนใดก็ตามที่ทำงานด้วยเอกสารและสเปรดชีต เวลาแก้ข้อสัญญาข้อหนึ่ง ถ้าพยายามนึกในหัวว่ามีสัญญา อีเมลแจ้ง และ FAQ ลูกค้ากี่แห่งที่ระบุหมายเลขข้อนั้นไว้ ก็จะตกหล่นอย่างแน่นอน แต่ถ้า grep ทั้งโฟลเดอร์ด้วยคีย์เวิร์ดให้ครบทั้งหมด แล้วให้มนุษย์จัดประเภทเป็น "ต้องแก้ / แค่ทำเครื่องหมาย / ไม่เกี่ยว" การตกหล่นก็จะหายไป ตัวอย่างเช่น เวลาผู้รับผิดชอบบัญชีเปลี่ยนรหัสบัญชีหนึ่ง ถ้าค้นหาชีตปิดบัญชี เทมเพลตรายงาน และมาโครที่อ้างอิงรหัสนั้นให้ครบทั้งหมด แล้วทำเป็นรายการตรวจสอบก่อนเปลี่ยน ก็จะป้องกันอุบัติเหตุปิดงบรายไตรมาสแบบ "ไม่เห็นชีตนั้นชีตเดียว" ได้อย่างเป็นโครงสร้าง
ถ้าเอกสารและ atom ถูกจัดการเป็นข้อความธรรมดา (.md) และติดตั้ง ripgrep (rg) ไว้แล้ว ก็พร้อมเรียบร้อย ถ้ามีกฎการตั้งชื่อ atom ID (สเนกเคส, ID เฉพาะ) ความแม่นยำของ grep ก็จะสูงขึ้นมาก
# ตรวจสอบ: atom ID หนึ่งตัวปรากฏในเอกสารทั้งหมดกี่ครั้ง
rg -c "combat_global_cooldown_constant" docs/ atoms/
ให้ atom เป้าหมายที่จะเปลี่ยนและเนื้อหาที่จะเปลี่ยน แล้วร้องขอการดึงสามสาย + คำแนะนำระดับ
ช่วยรัน impact กับ <atom_id> ให้หน่อย
ดึงสามสาย ขอบเข้า / affects / อ้างอิงย้อนกลับ wikilink ด้วย grep แยกกัน
แล้วรวมรายการซ้ำ จากนั้นแนะนำระดับผลกระทบ 18.2 (T0~T4)
เนื้อหาที่จะเปลี่ยน: <จากอะไรเป็นอะไร>
แยกเฉพาะ T1·T2 เป็นรายการ "ตรวจสอบก่อนเปลี่ยน" ให้ด้วย
อย่าเชื่อผลลัพธ์ของเครื่องมือทั้งดุ้น ให้ตรวจสองอย่างด้วยมือ
หลังตรวจสอบแล้ว เพียงแนบรายการ T1·T2 ในคอมเมนต์คำขอเปลี่ยนแปลง หนึ่งรอบก็ปิดลง
ถ้าเป็นงานส่วนตัวที่ไม่มีทั้งเครื่องมือและกราฟ atom ก็สามารถให้ผลแบบเดียวกันได้ด้วยคำสั่งหนึ่งบรรทัดและโน้ตหนึ่งช่อง
# ค้นหาทั้งโฟลเดอร์ทั้งหมดด้วยชื่อแนวคิดที่จะเปลี่ยน
rg -n "전역쿨다운|GCD|global_cooldown" .
นำผลการค้นหาแปะลงในแผ่นจดไว้ตามนั้น แล้วเขียนด้วยมือข้างแต่ละบรรทัดว่าเป็นหนึ่งในสามคือ "ต้องแก้ / แค่ตาราง / ไม่เกี่ยว" นี่คือ impact ฉบับคนเดียว หัวใจไม่ได้อยู่ที่ความประณีตของเครื่องมือ แต่อยู่ที่ตัวกระบวนการเองคือ "กวาดให้ครบทั้งหมดโดยไม่พึ่งความจำ แล้วให้มนุษย์จัดประเภท" ถ้ามีกระบวนการ การตกหล่นก็ลดลง ถ้าไม่มี ความรู้สึกตื้อตันทำอะไรไม่ถูกในเช้าวันจันทร์นั้นก็จะวนซ้ำทุกครั้ง
ผู้อ่านหลัก: Design Director (ผู้อำนวยการฝ่ายออกแบบ) และ Lead นักออกแบบเกม ที่นำทีมขนาดกลาง (10–50 คน) ฉบับย่อสำหรับนักพัฒนาคนเดียว/งานอดิเรก: §19.1.8 「ถ้าทำคนเดียวก็เท่านี้พอ」
แม้แต่ในทีมที่เขียนเอกสารวิสัยทัศน์ไว้อย่างดีเพียงหน้าเดียว ก็ยังเจอปัญหาเดิมซ้ำ ๆ วิสัยทัศน์ถูกแขวนไว้บนผนัง แต่กลับไม่มีใครตรวจว่าการตัดสินใจที่สะสมขึ้นทุกสัปดาห์นั้นสอดคล้องกับวิสัยทัศน์หรือไม่ ตอนทบทวนรายไตรมาสก็หยิบมาดูทีหนึ่ง แต่ ณ จุดนั้นมีการตัดสินใจอีกราวสามรายการซ้อนทับอยู่บนการตัดสินใจที่หลุดออกนอกแนวไปแล้ว การจะให้วิสัยทัศน์เป็น "จุดอ้างอิงเวลาเกิดข้อพิพาท" ได้ สิ่งที่สำคัญกว่าการเขียนคือ การลองเทียบการตัดสินใจแต่ละครั้งกับวิสัยทัศน์ และงานเทียบนั้น ถ้าให้คนทำด้วยมือก็น่าเบื่อและตกหล่นง่าย — เป็นงานที่เหมาะจะส่งต่อให้ AI อย่างยิ่ง
บทนี้ผูกสองเรื่องเข้าด้วยกัน ส่วนแรกคือเวิร์กโฟลว์ที่นำวิสัยทัศน์ซึ่งเขียนเสร็จแล้วมาหมุนเป็นเกณฑ์ให้คะแนนการตัดสินใจ — รอบการทำงานหนึ่งรอบที่นำ atom การตัดสินใจจริงในโปรเจกต์ของผู้เขียนจำนวน 26 รายการมาส่งให้ LLM ตรวจ ให้ตัดสินว่า "ละเมิดช่องของวิสัยทัศน์" หรือไม่ แล้วในจำนวนนั้นมีหนึ่งรายการที่ตัดสินผิด ซึ่งมนุษย์เป็นผู้จับได้ ส่วนหลังคือคำถามว่าเกณฑ์ให้คะแนนนี้ครอบคลุมการตัดสินใจของใครได้บ้าง นั่นคือเรื่อง การมอบหมายอำนาจ (delegation) ทฤษฎีภาวะผู้นำทั่วไป (ทำไมวิสัยทัศน์จึงสำคัญ ทำไมการมอบหมายจึงเป็นเครื่องมือในการเติบโต) มีอยู่ในหนังสือเล่มอื่นอย่างเพียงพอแล้ว บทนี้จึงโฟกัสเฉพาะ จุดที่นำหลักการเหล่านั้นมาหมุนเป็นเวิร์กโฟลว์ AI เท่านั้น
ต้องเริ่มจากการทำความเข้าใจประโยคที่ว่าวิสัยทัศน์เป็นตัวกรองการตัดสินใจก่อน วิสัยทัศน์ โรดแมป และตารางเวลา ไม่ใช่สิ่งเดียวกัน หน่วยของเวลาและความถี่ในการเปลี่ยนแปลงต่างกัน และเมื่อความต่างนั้นพังทลาย แรงกดดันด้านตารางเวลาก็จะสั่นคลอนวิสัยทัศน์
| ชั้น | ระยะเวลา | ความถี่ในการเปลี่ยน | ความหมายของการเทียบกับวิสัยทัศน์ |
|---|---|---|---|
| วิสัยทัศน์ | 5–10 ปี | แทบไม่มี | เส้นฐานที่การตัดสินใจต้องสอดคล้องด้วย |
| โรดแมป | 1–3 ปี | รายไตรมาส | ชั้นกลางที่แปลวิสัยทัศน์ให้เป็นตารางเวลา |
| ตารางเวลา | 1–3 เดือน | รายสัปดาห์ | ไม่นำมาเทียบกับวิสัยทัศน์โดยตรง |
ประเด็นสำคัญคือ สิ่งที่นำการตัดสินใจไปเทียบด้วยคือวิสัยทัศน์ (ชั้นที่เปลี่ยนน้อยที่สุด) ไม่ใช่ว่าพอตารางเวลาตึงก็เปลี่ยนวิสัยทัศน์ แต่เมื่อตารางเวลาขัดกับวิสัยทัศน์ ก็ไปปรับที่ฝั่งตารางเวลา ลำดับชั้นนี้ต้องชัดเจน การตรวจอัตโนมัติในหัวข้อถัดไปจึงจะมีความหมาย เพราะถ้าเส้นฐานของการตรวจสั่นไหวทุกสัปดาห์ การตรวจเองก็ไร้ความหมาย
วิสัยทัศน์จบในหน้าเดียวด้วย 5 ช่อง เอกสารวิสัยทัศน์ของโปรเจกต์ผู้เขียนมีโครงดังนี้ เนื่องจากช่องเหล่านี้จะกลายเป็นเกณฑ์ให้คะแนนของการตรวจด้วย LLM ใน §19.1.3 จึงควรดูรูปแบบไว้ก่อน
---
title: วิสัยทัศน์โปรเจกต์ A v2
layer: L0
locked: true # ต้องได้ข้อตกลงร่วมจาก Game Director + CEO หากจะเปลี่ยน
---
## ช่อง 1. สิ่งที่เราสร้าง
MMORPG ที่เน้นมือถือเป็นหลัก ในจักรวาลแฟนตาซีเกาหลี
## ช่อง 2. เพื่อใคร
ผู้ใช้วัย 30–50 ปี เล่นบนมือถือเป็นหลัก ชอบเนื้อเรื่องที่จริงจังลึกซึ้ง
## ช่อง 3. ทำไม (ความแตกต่าง)
- เนื้อเรื่องลึกด้วยการเล่าเรื่องหลายชั้น (เน้นความลึก ไม่ใช่ผลิตปริมาณ)
- ให้บริการพร้อมกันทั้งเอเชียตะวันออกเฉียงใต้ + เกาหลี
## ช่อง 4. อย่างไร (คุณค่า)
- เคารพเวลาของผู้ใช้ (ลดเนื้อหาสิ้นเปลืองให้น้อยที่สุด)
- ตัดสินใจโดยสมดุลระหว่างข้อมูล + คน
- ข้อตกลงร่วมของทีมมาก่อนความเร็วในการตัดสินใจ
## ช่อง 5. อะไรที่ไม่ใช่
- ไม่ใช่โมเดลการจ่ายเงินแบบ F2P ที่เร่งเร้ารุนแรง
- ไม่ใช่เกมที่เน้น PvP
- ไม่ใช่เกมที่บังคับเข้าเล่นทุกวันวันละ N ชั่วโมง
ช่อง 5 ("อะไรที่ไม่ใช่") เป็นช่องที่ทำงานหนักที่สุดในการตรวจ เพราะการละเมิดมักไม่ได้เกิดจาก "สิ่งที่ตกลงว่าจะทำ" แต่เกิดจากการแอบทำ "สิ่งที่ตกลงว่าจะไม่ทำ"
จะนำวิสัยทัศน์ไปเทียบกับอะไร ทีมของผู้เขียนตรึงการตัดสินใจสำคัญทุกครั้งไว้เป็น atom หนึ่งแผ่นในโฟลเดอร์ decisions/ เป็นบันทึกข้อเท็จจริงที่ระบุวันที่ · ผู้เกี่ยวข้อง · เหตุผลไว้ชัดเจน ปัจจุบันสะสมไว้ 26 รายการ อินพุตของการตรวจคือ 26 รายการนี้ — ไม่ได้สร้างขึ้นใหม่ แต่นำสิ่งที่มีอยู่แล้วไปเทียบ
รูปแบบจริงของ atom การตัดสินใจหนึ่งแผ่นเป็นดังนี้ (ทำให้ไม่ระบุตัวตน)
---
type: decision
id: D0019
date: 2026-05-12
deciders: [Game Director, Data Director]
tier: T1
---
# refgame_selective_adoption_for_mobile
นำข้อมูลการต่อสู้บางส่วนของ MMORPG อ้างอิงมาใช้กับบิลด์มือถือแบบเลือกสรร
เหตุผล: มีจังหวะการต่อสู้ที่ผ่านการตรวจสอบบนมือถือ 6 นิ้วแล้ว และหากออกแบบ
ใหม่ตั้งแต่ศูนย์ ตารางอัลฟาจะเลื่อนออกไปหนึ่งไตรมาส อย่างไรก็ตาม จะไม่นำ
โครงสร้างชักจูงการจ่ายเงิน·การเข้าเล่นมาใช้
ในจำนวน 26 รายการ ขอคัดตัวแทนสองสามรายการที่จะใช้เป็นอินพุตของการตรวจ (ชื่อ atom จริง, §A.3.3)
| atom id | ชื่อ atom (ทำให้ไม่ระบุตัวตน) | tier | ใจความหนึ่งบรรทัด |
|---|---|---|---|
| D0007 | claude_role_transition_phase2 |
T1 | ยกระดับ Claude จากผู้ช่วยเชิงรับ → พาร์ตเนอร์เชิงรุก |
| D0014 | dataset_scope_alpha_split |
T2 | กำหนดเกณฑ์การแบ่งชุดข้อมูลอัลฟา |
| D0019 | refgame_selective_adoption_for_mobile |
T1 | นำข้อมูลการต่อสู้ของเกมอ้างอิงมาใช้แบบเลือกสรร |
| D0021 | procedural_capability_frontier_5stage |
T1 | นิยามความสามารถการสร้างแบบโพรซีเดอรัล 5 ขั้น |
| D0023 | class_keyword_world_only |
T2 | จำกัดคีย์เวิร์ดคลาสให้อยู่ภายในจักรวาลของเกม |
ตารางนี้คืออินพุตข้อมูลของพรอมต์ในหัวข้อถัดไป หัวใจอยู่ที่การส่งทั้ง 26 รายการเข้าตรวจในคราวเดียว เพราะถ้าตอนทบทวน คนต้องเทียบ 26 รายการกับวิสัยทัศน์ทีละรายการด้วยมือ จะใช้เวลาครึ่งวัน และตั้งแต่ช่วงกลางสมาธิจะเริ่มหลุด ทำให้พลาดการละเมิด เราจึงส่งงานเทียบขั้นแรกที่น่าเบื่อนั้นให้ LLM
มาดูรอบการทำงานหนึ่งรอบจนจบจริง ๆ พรอมต์อินพุตคัดลอกไปใช้ได้ทันที และผลลัพธ์เป็นการเรียบเรียงใหม่จากเซสชันจริง
ลองนำ atom การตัดสินใจ 26 รายการมาเทียบกับวิสัยทัศน์ 5 ช่องด้านล่างทีละรายการ
แล้วตัดสินว่าแต่ละการตัดสินใจ สอดคล้อง/เสริม/ละเมิด/ไม่เกี่ยวข้อง กับวิสัยทัศน์
ถ้าเป็นละเมิดหรือไม่เกี่ยวข้อง ให้ระบุเหตุผลหนึ่งบรรทัดว่าขัดกับช่องใดและทำไม
(โดยเฉพาะให้ดูช่อง 5 "อะไรที่ไม่ใช่" ให้ดี ๆ) ถ้ารายการใดคลุมเครือ ให้ทำเครื่องหมาย
'คลุมเครือ' แล้วส่งต่อให้ฉัน ถ้าสอดคล้องไม่ต้องระบุเหตุผลก็ได้
ให้ออกมาเป็นตารางเท่านั้น (id | การตัดสิน | ช่องที่ขัด | เหตุผล | คลุมเครือ) ตัดบทสรุปรวมออก
[วิสัยทัศน์ 5 ช่อง — วางข้อความเต็มจาก §19.1.1]
[atom การตัดสินใจ 26 รายการ — id·เนื้อหา·เหตุผล]
ครึ่งหนึ่งของพรอมต์คือ "การบังคับรูปแบบการตัดสิน" ถ้าให้อิสระ LLM จะกลบทั้ง 26 รายการเป็น "โดยรวมแล้วสอดคล้อง" หมด หัวใจของพรอมต์นี้คือการใส่ ป้ายการตัดสิน 4 ระดับ ภาระต้องระบุเหตุผลเฉพาะกรณีละเมิด·ไม่เกี่ยวข้อง และทางออก "ถ้าคลุมเครือให้ส่งต่อให้คน" เข้าไป การเรียกชื่อช่อง 5 อย่างชัดเจนในประโยคเหตุผลก็เป็นความตั้งใจ — เพราะการละเมิดเกิดจาก "สิ่งที่ตกลงว่าจะไม่ทำ"
| id | การตัดสิน | ช่องที่ขัด | เหตุผล | คลุมเครือ |
|-------|-----------|-----------|----------------------------------------|-----------|
| D0007 | ไม่เกี่ยวข้อง | — | การตัดสินใจเรื่องการใช้งาน Claude แกนประเมินต่างจากวิสัยทัศน์เกม | |
| D0014 | ไม่เกี่ยวข้อง | — | การแบ่งชุดข้อมูล เป็นการตัดสินใจเรื่องโครงสร้างภายใน | |
| D0019 | ละเมิด | ช่อง 5 | นำข้อมูลการต่อสู้ของเกมอ้างอิงมาใช้ → อาจขัดกับ | ★ |
| | | | "ไม่ใช่โมเดลการจ่ายเงินแบบ F2P เร่งเร้ารุนแรง" | |
| D0021 | สอดคล้อง | ช่อง 3 | | |
| D0023 | สอดคล้อง | ช่อง 1 | | |
| ... | | | | |
(จาก 26 รายการ: สอดคล้อง 18 · เสริม 3 · ไม่เกี่ยวข้อง 3 · ละเมิด 2)
รายการที่ขอให้ตัดสินเรื่องละเมิด/คลุมเครือ: D0019, D0026 — ต้องให้คนตรวจสอบ
ส่วนที่มีคุณค่าที่สุดในผลลัพธ์ไม่ใช่ตาราง แต่เป็น บรรทัดล่างสุด ซึ่งระบุการละเมิด 2 รายการและเครื่องหมายคลุมเครือ LLM กรอง 24 รายการจาก 26 ออกไปโดยอัตโนมัติ แล้วยกขึ้นมาเพียง 2 รายการที่คนต้องดู การเทียบที่กินเวลาครึ่งวันถูกย่อเหลือการตรวจสอบ 2 รายการ แต่หนึ่งใน 2 รายการนั้นเป็นการตัดสินที่ผิด
คนกลับมาอ่านคำตัดสินของ D0019 (refgame_selective_adoption_for_mobile) อีกครั้ง LLM เห็นคำว่า "นำข้อมูลการต่อสู้ของเกมอ้างอิงมาใช้" แล้วตัดสินว่าขัดกับ "ไม่ใช่โมเดลการจ่ายเงินแบบ F2P เร่งเร้ารุนแรง" ในช่อง 5 ผิวเผินคำดูเข้าท่า — เพราะเกมอ้างอิงนั้นขึ้นชื่อเรื่องการจ่ายเงินแบบก้าวร้าว
แต่ถ้าอ่านเนื้อหาของ atom จนจบ จะมีประโยคสุดท้ายอยู่ "อย่างไรก็ตาม จะไม่นำโครงสร้างชักจูงการจ่ายเงิน·การเข้าเล่นมาใช้" การตัดสินใจนี้นำมาเฉพาะข้อมูลจังหวะการต่อสู้ และกีดกันโครงสร้างการจ่ายเงินไว้อย่างชัดเจน เป็นการตัดสินใจที่กลับรักษาช่อง 5 ไว้ด้วยซ้ำ LLM ไม่สามารถสะท้อนประโยคจำกัดความสุดท้ายของเนื้อหา atom ลงในน้ำหนักการตัดสินได้ แล้วถูกคำที่บอกแหล่งที่มาอย่าง "เกมอ้างอิง" ลากไปจัดเป็นการละเมิด นี่ไม่ใช่การละเมิดช่อง 5 แต่เป็น การสอดคล้อง
เหตุผลที่เกิดการตัดสินผิดเช่นนี้ชัดเจน LLM มอง แหล่งที่มา ของการตัดสินใจ (เอามาจากเกมไหน) กับ เนื้อหา ของการตัดสินใจ (เอาอะไรมาและทิ้งอะไรไป) ด้วยน้ำหนักเท่ากัน ส่วนคนรู้ว่าอนุประโยคจำกัดความที่ว่า "อย่างไรก็ตาม จะไม่ทำ \~" คือหัวใจของการตัดสินใจ ดังนั้นคนจึงปฏิเสธและขอใหม่
ช่วยดู D0019 อีกครั้ง ประโยคสุดท้ายของเนื้อหา "อย่างไรก็ตาม จะไม่นำโครงสร้างชักจูง
การจ่ายเงิน·การเข้าเล่นมาใช้" คือหัวใจ ช่วยแยกสิ่งที่นำมาใช้ (ข้อมูลจังหวะการต่อสู้)
กับสิ่งที่กีดกัน (โครงสร้างการจ่ายเงิน·การเข้าเล่น) แล้วตัดสินใหม่ว่าแต่ละอย่างขัดกับช่องใด
LLM ตอบกลับอีกครั้งว่า "สิ่งที่นำมาใช้ (ข้อมูลการต่อสู้) สอดคล้องกับช่อง 1·2 สิ่งที่กีดกัน (โครงสร้างการจ่ายเงิน) สนับสนุนช่อง 5 อย่างชัดเจน คำตัดสินรวม: สอดคล้อง คำตัดสินละเมิดก่อนหน้านี้เป็นความผิดพลาดจากการตอบสนองเกินต่อคำที่บอกแหล่งที่มา" การโต้ตอบไปกลับเพียงครั้งเดียวนี้ทำให้ D0019 ถูกแก้จากละเมิดเป็นสอดคล้อง รายการที่ต้องตรวจสอบจริง ๆ ที่เหลืออยู่คือ D0026 เพียงหนึ่งรายการ
รอบการทำงานนี้คือหัวใจของบทนี้ LLM ย่อ 26 รายการให้เหลือ 2 รายการก็จริง แต่หนึ่งใน 2 รายการนั้นอาจตัดสินผิดได้ การตรวจอัตโนมัติไม่ได้เป็นเครื่องมือที่ขจัดการตรวจสอบของคน แต่เป็นเครื่องมือที่ทำให้คนจดจ่อกับ 2 รายการแทนที่จะเป็น 26 รายการ ถ้าคนไม่อ่าน 2 รายการนั้นจนจบ การตัดสินใจที่ปกติดีก็จะถูกยกขึ้นที่ประชุมในฐานะ "การละเมิดวิสัยทัศน์" แล้วก่อข้อพิพาทผิด ๆ ขึ้นมา
ถ้าเก็บรอบการทำงานข้างต้นไว้เป็นภาพ ทุกไตรมาสหลังจากนั้นก็จะเดินตามขั้นตอนเดิมซ้ำได้ ประเด็นสำคัญคือคำตัดสินของ LLM ไม่ได้พลิกการตัดสินใจโดยอัตโนมัติ มันยกขึ้นด่านตรวจของคนเฉพาะกรณีละเมิด·คลุมเครือเท่านั้น ส่วนการยกเลิก·แก้ไข·อนุมัติ คนเป็นผู้ทำ
flowchart TB
A["วิสัยทัศน์ 5 ช่อง (L0, locked)
เส้นฐานให้คะแนน"]
B["atom ใน decisions/ 26 รายการ
id·เนื้อหา·เหตุผล"]
A --> C["LLM ตัดสินขั้นแรก
สอดคล้อง/เสริม/ละเมิด/ไม่เกี่ยวข้อง + เหตุผล"]
B --> C
C -->|สอดคล้อง·เสริม·ไม่เกี่ยวข้อง 24 รายการ| F["ผ่าน — บันทึกในการทบทวนรายไตรมาส"]
C -->|ละเมิด·คลุมเครือ 2 รายการ| D{"ด่านตรวจสอบโดยมนุษย์"}
D -->|ยืนยันว่าตัดสินผิด| E["ขอใหม่
(เสริมอนุประโยคจำกัดความ·บริบท)"]
E --> C
D -->|ละเมิดจริง| G["เรียกประชุมพิจารณาการตัดสินใจใหม่"]
D -->|สอดคล้องหลังแก้ไข| F
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class C ai;
class D,E,G human;
class A,B data;
class F pass;
จุดที่มือมนุษย์แตะมีเพียงสองแห่ง คือจุดที่ป้อนวิสัยทัศน์และการตัดสินใจเข้าไปอย่างสะอาด (บนสุด) และจุดที่อ่านรายการส่วนน้อยที่ LLM ยกขึ้นมาในฐานะละเมิด·คลุมเครือจนจบแล้วตัดสิน (ด่านตรงกลาง) ส่วนการเทียบ 26 รายการที่น่าเบื่อระหว่างนั้น LLM เป็นผู้หมุน นี่เป็นการออกแบบแบบเดียวกับที่ตัวสร้าง city ใน §6.2 ซึ่ง lint ไม่ได้ยกเลิกการละเมิดโดยอัตโนมัติ แต่ยก alert ขึ้นไปที่ด่านนักเขียนเท่านั้น — เครื่องคัดผู้ต้องสงสัยออกมา ส่วนจะฆ่าหรือจะเก็บไว้ คนเป็นผู้ตัดสิน
ถึงตรงนี้มีคำถามตามมาอย่างเป็นธรรมชาติ การตัดสินใจ 26 รายการนั้น Game Director ตัดสินเองทั้งหมดหรือ ถ้าเป็นเช่นนั้นไม่ดี ถ้า Lead ตัดสินใจทุกอย่างเอง ก็จะกลายเป็นคอขวด แต่ถ้ามอบหมายไปหมด วิสัยทัศน์ก็จะอ่อนลง การตรวจวิสัยทัศน์ยังเป็นตาข่ายนิรภัยที่มีไว้เพื่อ กรองแม้กระทั่งการตัดสินใจที่มอบหมายออกไป ด้วยเกณฑ์ให้คะแนนเดียวกัน อีกด้วย
การตัดสินใจมีระดับชั้น และระดับชั้นก็คืออำนาจ นี่คือเมทริกซ์อำนาจของทีมผู้เขียน
| ระดับ | ผู้ตัดสิน | ผู้ตรวจสอบ | แจ้งให้ทราบ | อยู่ในขอบเขตตรวจวิสัยทัศน์? |
|---|---|---|---|---|
| T0 วิสัยทัศน์·แกนหลัก | Game Director + CEO | หัวหน้าทีมทุกคน | ทั้งทีม | ตัววิสัยทัศน์เอง (เส้นฐานการตรวจ) |
| T1 ระบบ·ข้ามสาขา | ประธาน TF + Game Director | สมาชิก TF | ทีมสาขา | ✅ จำเป็น |
| T2 สาขา·ระดับกลาง | Director ของสาขา | ซีเนียร์ | ทีมสาขา | ✅ จำเป็น |
| T3 ครั้งเดียว·เล็ก | ซีเนียร์ | ผู้รับผิดชอบ | ผู้เกี่ยวข้องโดยตรง | ตรวจแบบสุ่มตัวอย่าง |
| T4 ทันที·hotfix | ผู้รับผิดชอบ | ซีเนียร์ (หลังเหตุการณ์) | Game Director (หลังเหตุการณ์) | ยกเว้นจากการตรวจ |
26 รายการใน decisions/ ส่วนใหญ่เป็น T1·T2 — เป็นการตัดสินใจที่มอบหมายออกไป Game Director ไม่ได้ดู T2 ทุกรายการเอง แต่การตรวจวิสัยทัศน์ (§19.1.3) นำการตัดสินใจ T1·T2 ที่มอบหมายไปมาเทียบกับวิสัยทัศน์ทุกไตรมาส ตาข่ายนิรภัยของการมอบหมายก็คือการตรวจวิสัยทัศน์ นั่นเอง T0 ไม่ใช่ขอบเขตการตรวจ แต่เป็นเส้นฐานของการตรวจ ส่วน hotfix T4 มีปริมาณมากและแทบไม่ส่งผลต่อวิสัยทัศน์ จึงยกเว้น
ตัวการมอบหมายเองก็ไม่ได้ทำเต็มที่ในคราวเดียว แต่ค่อย ๆ ทำเป็น 4 ขั้น
| ขั้น | อำนาจ | ความสัมพันธ์กับการตรวจด้วย LLM |
|---|---|---|
| 1. ส่งต่อข้อมูล | "ทำแบบนี้" | ผู้มอบหมายเป็นผู้ตัดสินใจ ไม่ต้องตรวจ |
| 2. ให้คำแนะนำ + รายงานการตัดสินใจ | "พิจารณา X แล้วตัดสินใจ" | เวลารายงานก็ดูการเทียบกับวิสัยทัศน์ไปด้วย |
| 3. รายงานหลังเหตุการณ์ | "ตัดสินใจแล้วแจ้งผล" | ตรึงเป็น atom → รวมเข้าในการตรวจรายไตรมาส |
| 4. ตัดสินใจอย่างอิสระ | ไม่มีภาระต้องรายงาน (ในขอบเขตของระดับชั้น) | แค่เหลือ atom ไว้ การตรวจก็ครอบคลุมในภายหลัง |
การตัดสินใจอย่างอิสระในขั้นที่ 4 มีความเสี่ยงที่จะขัดกับวิสัยทัศน์มากที่สุด และความเสี่ยงนั้นเองที่การตรวจใน §19.1.3 จับได้ในภายหลัง แม้แต่การตัดสินใจ T2 ที่ตัดสินอย่างอิสระ ขอเพียงตรึงเป็น atom ก็จะถูกดึงเข้าตรวจรายไตรมาสโดยอัตโนมัติ เหตุผลที่อิสระของการมอบหมายและความสอดคล้องของวิสัยทัศน์ไม่ขัดกันอยู่ตรงนี้ — ตัดสินใจได้อย่างอิสระ แต่การตัดสินใจถูกเก็บไว้เป็น atom และ atom ถูกนำไปเทียบกับวิสัยทัศน์ทุกไตรมาส
บทเรื่องวิสัยทัศน์·การมอบหมายมีแรงล่อใจสูงที่จะใส่ตารางอย่าง "หลังนำวิสัยทัศน์มาใช้ เวลาประชุมลดจาก 90 นาทีเหลือ 45 นาที" หรือ "หลังมอบหมาย ภาระการตัดสินใจของ Director ลดจาก 30 รายการต่อสัปดาห์เหลือ 5 รายการ" ตัวเลขเช่นนั้นถ้าไม่ได้ตรวจสอบจะบั่นทอนความน่าเชื่อถือของหนังสือ ตัวเลขในบทนี้จึงจัดการได้เพียงหนึ่งในสามแบบเท่านั้น
หนึ่ง สิ่งที่นับได้ ให้เขียนตามการวัดจริง atom ใน decisions/ ปัจจุบันมี 26 รายการ (ตามการวัดจริง ณ พฤษภาคม 2026) จำนวนรายการที่ถูกยกขึ้นด่านตรวจของคนจากคำตัดสินขั้นแรกของ LLM และจำนวนรายการที่ถูกแก้เพราะตัดสินผิด เป็นค่าจากการวัดจริงที่นับได้จากบันทึกเซสชัน การที่ใน worked transcript ข้างต้น คำตัดสินละเมิด 2 รายการมี 1 รายการ (D0019) ที่ตัดสินผิด ก็เป็นผลจริงจากเซสชันจริง
สอง ผลลัพธ์ พูดเพียงเป็นทิศทางเท่านั้น ประโยค "การเทียบครึ่งวันถูกย่อเหลือการตรวจสอบไม่กี่รายการ" เป็นทิศทางของโครงสร้าง ไม่ใช่เวลาสัมบูรณ์ เวลาที่ประหยัดได้แน่นอนนั้นเปลี่ยนไปตามจำนวนการตัดสินใจ·ขนาดทีม·ความยาวเนื้อหา atom จึงควรอ่านในแง่ความต่างของโครงสร้างระหว่าง "ทำ 26 รายการด้วยมือ" กับ "LLM ขั้นแรก + ด่านตรวจของคน" ตัวชี้วัดผลลัพธ์อย่างเวลาประชุม·คะแนนแรงจูงใจ ไม่ได้ถูกกำหนดด้วยวิสัยทัศน์เพียงอย่างเดียว จึงไม่ฟันธงความเป็นเหตุเป็นผล
สาม สัญญาเฉพาะสิ่งที่วัดได้ สิ่งที่เวิร์กโฟลว์นี้วัดได้จริงคือ — จำนวนการตัดสินใจที่ส่งเข้าตรวจวิสัยทัศน์ต่อไตรมาส จำนวนรายการที่ผ่านด่านตรวจของคน อัตราการตัดสินผิด (สัดส่วนที่คนแก้เป็นสอดคล้องจากที่ LLM ตัดสินว่าละเมิด) จำนวนรายการที่ตกหล่นจากการตรึง atom (การตัดสินใจที่มอบหมายไปแต่ไม่มี atom จึงหลุดจากการตรวจ) ทั้งสี่อย่างนี้พูดเป็นตัวเลขในที่ประชุมได้ ไม่ใช่ "ความรู้สึก" โดยเฉพาะ อัตราการตัดสินผิด พิสูจน์ทุกไตรมาสด้วยตัวเลขถึงเหตุผลที่ไม่ควรเชื่อคำตัดสินของ LLM ทั้งดุ้น
| รูปแบบ | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| เขียนวิสัยทัศน์อย่างเดียวแต่ไม่นำไปเทียบกับการตัดสินใจ | วิสัยทัศน์เหลือเป็นของประดับผนัง การตัดสินใจเป็นไปตามอำเภอใจ | §19.1.3 ที่นำ atom 26 รายการไปเทียบกับวิสัยทัศน์ทุกไตรมาส |
| ยกคำตัดสินละเมิดของ LLM ขึ้นที่ประชุมตามนั้นเลย | การตัดสินผิด (อย่าง D0019) ก่อข้อพิพาทผิด ๆ | กรณีละเมิด·คลุมเครือ ให้คนอ่านเนื้อหา atom จนจบ |
| ไม่เก็บการตัดสินใจไว้เป็น atom | การตัดสินใจที่มอบหมายไปหลุดจากการตรวจทั้งดุ้น | กำหนดให้ตรึง atom เป็นภาระในการรายงานหลังเหตุการณ์ (การมอบหมายขั้น 3) |
| ตรวจไปจนถึง hotfix T4 ทั้งหมด | ปริมาณเพิ่มอย่างเดียว แต่แทบไม่ส่งผลต่อวิสัยทัศน์ | จำกัดขอบเขตการตรวจไว้ที่ T1·T2 |
| มอบหมายโดยข้ามขั้น 1→4 | การตัดสินใจอย่างอิสระสะสมโดยขัดกับวิสัยทัศน์ | มอบหมายแบบเป็นขั้น + ครอบคลุมในภายหลังด้วยการตรวจรายไตรมาส |
รายการที่สามเป็นรายการที่พลาดบ่อยที่สุด ยิ่งทีมที่หมุนเองได้อย่างอิสระ ยิ่งตกลงการตัดสินใจกันด้วยปากเปล่าและไม่เหลือ atom ไว้ เมื่อเป็นเช่นนั้นการตรวจใน §19.1.3 จะเห็นเฉพาะการตัดสินใจที่ตรึงไว้ การตัดสินใจที่ตัดสินอย่างอิสระที่สุดจึงหลุดเป็นจุดบอดของการตรวจ อิสระของการมอบหมายจะปลอดภัยก็ต่อเมื่อมีการตรึง atom เป็นเงื่อนไขเท่านั้น
การประยุกต์นอกเกม การลองเทียบวิสัยทัศน์กับการตัดสินใจทุกครั้ง และการมอบหมายอำนาจ ไม่ใช่การบ้านของทีมเกมเท่านั้น แต่เป็นงานของผู้บริหารทุกคน หากตรึงพันธกิจของแผนกไว้เป็น 5 ช่องในหน้าเดียว ("สิ่งที่เราทำ / เพื่อใคร / ทำไม / อย่างไร / อะไรที่ไม่ใช่") ก็สามารถนำการตัดสินใจเชิงปฏิบัติที่สะสมขึ้นทุกสัปดาห์ มาเทียบกับพันธกิจนั้นด้วย LLM เป็นการเทียบขั้นแรกไตรมาสละครั้งได้ว่าหลุดออกนอกแนวหรือไม่ — โดยเฉพาะการละเมิดที่แอบทำ "สิ่งที่ตกลงว่าจะไม่ทำ" จะถูกจับได้ดี ยกตัวอย่าง หากนำการตัดสินใจที่หัวหน้าทีมมอบหมายไป มาเทียบกับพันธกิจของแผนกทุกไตรมาส ก็จะเป็นตาข่ายนิรภัยที่จับได้ในภายหลังว่าการตัดสินใจที่ตัดสินอย่างอิสระนั้นเบี่ยงออกนอกทิศทางหรือไม่ อย่างไรก็ตาม รายการที่ LLM ยกขึ้นมาว่า "ละเมิด" อย่าเพิ่งยกขึ้นที่ประชุมตามนั้นเลย ในจำนวนนั้นอาจมีหนึ่งรายการที่ตัดสินผิด คนจึงต้องอ่านจนจบ
ถ้าทำคนเดียวก็เท่านี้พอ: ไม่ต้องมีโฟลเดอร์ atom การตัดสินใจก็ได้ ลองเขียนวิสัยทัศน์ของโปรเจกต์ตัวเอง (หรือเกมงานอดิเรก) ออกมาเป็น 5 ช่องตาม §19.1.1 ในหน้าเดียว จากนั้นจดการตัดสินใจล่าสุด 5–10 รายการเป็นโน้ตบรรทัดละรายการ แล้วคัดลอกพรอมต์จาก §19.1.3 มาวางลองส่งให้ LLM สักครั้ง ถ้ามีคำตัดสิน 'ละเมิด' ออกมาแม้แต่รายการเดียว ให้กลับไปอ่านโน้ตของการตัดสินใจนั้นจนจบ แล้วลองโต้แย้งด้วยตัวเองว่า LLM ถูกหรือไม่ แล้วคุณจะรู้สึกได้ด้วยตัวเองว่าการตรวจวิสัยทัศน์เป็นการรวมการตัดสินแบบใด และทำไมจึงไม่ควรเชื่อคำตัดสินของ LLM ทั้งดุ้น
ถ้าเป็นทีม ให้เริ่มด้วยก้าวต่อไปนี้ เริ่มจากการตรึงวิสัยทัศน์ไว้เป็น 5 ช่องหน้าเดียว (L0, locked) แล้วตรึงการตัดสินใจ T1·T2 ที่ตัดสินในไตรมาสล่าสุดไว้เป็น atom หนึ่งแผ่นต่อหนึ่งรายการในโฟลเดอร์ decisions/ แค่ atom สะสมได้ 10 รายการก็ลองหมุนพรอมต์ §19.1.3 ได้ครั้งหนึ่ง และถ้าในรอบแรกนั้นจับการตัดสินใจที่มอบหมายไปแล้วขัดกับวิสัยทัศน์ได้สักหนึ่งรายการ คุณค่าของเวิร์กโฟลว์นี้ก็จะเห็นได้ทันที
ผู้อ่านหลัก: ผู้อำนวยการ·หัวหน้าทีมที่ต้องตัดสินใจในที่ประชุมมากกว่า 50 ครั้งต่อไตรมาส (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านที่ทำงานคนเดียว/เป็นงานอดิเรก: §19.2.8 「ถ้าทำคนเดียวก็แค่นี้พอ」
ผมเคยนำการประชุม 90 นาทีมาได้อย่างราบรื่น แต่อีกหนึ่งสัปดาห์ต่อมาวาระเดิมกลับขึ้นมาอยู่บนโต๊ะประชุมอีกครั้ง เราตัดสินใจไปแล้วแน่ ๆ แต่ไม่มีที่ไหนบันทึกไว้เลยว่าใครรับผิดชอบอะไร ในบันทึกการประชุมเหลือไว้แค่ "หารือเรื่อง global cooldown" ส่วน "ตกลงที่ 0.5 วินาที ผู้รับผิดชอบคือทีมงาน A" ระเหยหายไปจากหัวของคนที่อยู่ในห้องนั้นภายในหนึ่งสัปดาห์ จุดที่การประชุมของผู้นำพังลงนั้นส่วนใหญ่ไม่ใช่ระหว่างประชุม แต่เป็น ช่วงรอยต่อสั้น ๆ ทันทีหลังประชุมจบ ก่อนที่การตัดสินใจจะแข็งตัวเป็นบันทึก
บทนี้กล่าวถึงงานของหัวหน้าทีมสองก้อนใหญ่ ครึ่งแรกคือ วิธีส่งความขัดแย้งไปยังใบสั่งยามาตรฐานตามประเภท แทนที่จะแก้จากศูนย์ทุกครั้ง และครึ่งหลังคือกระดูกสันหลังของบทนี้ — บันทึกเซสชันจริง (worked transcript) ที่ให้ AI สกัดการตัดสินใจที่เกิดในที่ประชุม แต่บังคับให้ไม่ปล่อยผ่านเมื่อเจ้าของหรือเหตุผลว่างเปล่า ทฤษฎีภาวะผู้นำทั่วไป (การนำเสนอวิสัยทัศน์·การรับฟัง·ความเห็นอกเห็นใจ) มีในหนังสือเล่มอื่นเพียงพอแล้ว บทนี้จึงโฟกัสเฉพาะที่ การนำทฤษฎีทั่วไปนั้นไปหมุนเป็น AI workflow เพื่อป้องกันการตัดสินใจหลุดหาย เท่านั้น
ความเข้าใจที่ว่าทีมที่มีความขัดแย้งเป็นศูนย์คือทีมที่แข็งแรงนั้นเป็นความเข้าใจผิด หากทีมขนาดกลาง (10–50 คน) ต้องตัดสินใจมากกว่า 50 ครั้งต่อไตรมาส แล้วกลับไม่เห็นแรงเสียดทานเลยแม้แต่ครั้งเดียว นั่นไม่ใช่ว่าไม่มีความขัดแย้ง แต่เป็นความขัดแย้งที่จมลงไปใต้ผิวน้ำ และความขัดแย้งที่จมอยู่นั้นอันตรายกว่า
สิ่งที่ผู้นำต้องทำไม่ใช่การกำจัดความขัดแย้ง แต่คือ การจำแนกประเภทให้เร็วแล้วส่งไปยังใบสั่งยามาตรฐาน หากความขัดแย้งแบบเดียวกันถูกแก้ด้วยวิธีต่างกันทุกครั้ง เวลาที่ใช้ในการแก้ก็จะสะสมใหม่จากศูนย์ทุกครั้ง
| ประเภทความขัดแย้ง | แก่นของการปะทะ | ใบสั่งยามาตรฐาน |
|---|---|---|
| ความขัดแย้งเชิงคุณค่า | ตีความวิสัยทัศน์ต่างกัน (รายได้ vs เวลาของผู้ใช้) | อ้างอิงสล็อตวิสัยทัศน์ |
| ความขัดแย้งเชิงข้อเท็จจริง | ตีความข้อมูลเดียวกันต่างกัน | ตรวจสอบข้อมูล (รายงานเมตาเกม) |
| ความขัดแย้งเชิงลำดับความสำคัญ | "สายงานของฉันสำคัญกว่า" | เปรียบเทียบระดับอิมแพกต์·ผลกระทบต่อ KPI |
| ความขัดแย้งเชิงอำนาจ | "นี่คือการตัดสินใจของฉัน" | ทบทวนเมทริกซ์อำนาจอีกครั้ง |
| ความขัดแย้งส่วนบุคคล | ความสัมพันธ์·สไตล์การสื่อสาร | 1:1, แยกข้อเท็จจริง/อารมณ์ (อยู่นอกระบบ) |
สี่ประเภทแรกมีใบสั่งยาที่เป็น การอ้างอิงระบบ หากวิสัยทัศน์·ข้อมูล·KPI·เมทริกซ์อำนาจถูกเขียนเป็นลายลักษณ์อักษรไว้ น้ำหนักของการตัดสินใจจะย้ายจากปากของคนไปทางระบบ ทำให้การถกเถียงสั้นลง มีเพียงความขัดแย้งส่วนบุคคลประเภทที่ห้าเท่านั้นที่อยู่นอกระบบ — การพูดคุยแบบ 1:1 และการแยกข้อเท็จจริงกับอารมณ์ นอกจากเวลาและความจริงใจแล้ว เครื่องมืออื่นแทบไม่ทำงาน อย่างไรก็ตาม "แก้ด้วยระบบไม่ได้" ไม่ใช่ข้ออ้างให้ผู้นำปล่อยมือ ความยากของจุดนี้คือ ในที่สุดแล้วพื้นที่ที่ระบบแก้ไม่ได้ก็ยังเป็นงานของผู้นำอยู่ดี
การจำแนกไม่ได้แก้จากต้นทุกครั้ง แต่หมุนเป็นกระแสเดียว
flowchart LR
A["รับรู้ความขัดแย้ง"] --> B["จำแนกประเภท
(5 ประเภท)"]
B --> C["ใช้ใบสั่งยามาตรฐาน
วิสัยทัศน์·ข้อมูล·KPI·อำนาจ·1:1"]
C --> D["ติดตามผลหลัง 1 สัปดาห์"]
D --> E{"เกิดซ้ำ?"}
E -->|"ประเภทเดิมซ้ำ"| F["ตรวจสอบระบบ·กฎ
(สล็อตความขัดแย้งในการทบทวนรายไตรมาส)"]
E -->|"คลี่คลาย"| G["ปิดเรื่อง"]
F --> A
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class A,B,C,D,E,F human;
class G pass;
แก่นอยู่ที่จุดแยกทางขวา หากความขัดแย้งประเภทเดียวกันเกิดซ้ำ นั่นไม่ใช่ปัญหาของคน แต่เป็นปัญหาของระบบ ในตอนนั้นแทนที่จะไกล่เกลี่ยคน เราจะไปปรับกฎอย่างวิสัยทัศน์·เมทริกซ์อำนาจ สิ่งนี้กลายเป็นอินพุตของสล็อตความขัดแย้งในการทบทวนรายไตรมาสที่จะกล่าวถึงใน §19.2.7
เช่นเดียวกับที่ใบสั่งยาของความขัดแย้งทั้งสี่ประเภทล้วนเป็น "การอ้างอิงระบบ" ที่ประชุมก็เป็น กลไกที่สร้างการตัดสินใจและทำให้การตัดสินใจนั้นแข็งตัวเป็นบันทึก ในที่สุด หลักห้าข้อที่ผู้นำต้องรักษาในที่ประชุมนั้นผูกพันกันอยู่ ขาดข้อใดข้อหนึ่งไป ข้อที่เหลือก็จะสั่นคลอนตามไปด้วย
ในห้าข้อนี้ ข้อ 3·4·5 ที่พังลงคืออุบัติเหตุที่เล่าไว้ตอนต้น เราตัดสินใจด้วยปาก (บรรลุข้อ 3 บางส่วน) แต่ไม่แข็งตัวเป็นบันทึก (ข้อ 4 ล้มเหลว) และไม่ได้ใส่เจ้าของลงไป (ข้อ 5 ล้มเหลว) จึงทำให้อีกหนึ่งสัปดาห์ต่อมาวาระเดิมขึ้นมาอีกครั้ง
ปัญหาคือ หากฝากข้อ 3·4·5 ไว้กับเจตจำนงของคน มันจะพังก่อนเป็นอันดับแรกในสัปดาห์ที่ยุ่ง พอประชุมจบ ผู้นำก็วิ่งไปยังการประชุมถัดไปแล้ว เราจึงย้ายหลักสามข้อนี้ ไปยังไปป์ไลน์ที่มี AI ช่วย นั่นคือการสกัดการตัดสินใจจากข้อความบันทึกการประชุมโดยอัตโนมัติ แต่ถ้าเจ้าของหรือเหตุผลว่างเปล่า ก็ไม่ให้ปล่อยผ่าน ไปป์ไลน์นี้เป็นการมองกระแส ประชุม→บันทึกการประชุม→สกัด atom (§17.2) ที่สร้างไว้ในส่วนที่ 17 อีกครั้งหนึ่งจากมุมมองของผู้นำ
ผมจะแสดงให้เห็นจนจบว่าจริง ๆ แล้วหมุนหนึ่งรอบอย่างไร เวทีคือทันทีหลังการประชุมของ TF การต่อสู้ในโปรเจกต์ของผู้เขียน (MMORPG ที่เน้นมือถือเป็นหลัก ต่อไปนี้เรียกว่า "โปรเจกต์ A") พรอมต์อินพุตคัดลอกไปใช้ได้เลยตามนั้น ส่วนผลลัพธ์เป็นการเรียบเรียงใหม่จากเซสชันจริง
อย่าจัดบันทึกการประชุมให้สวยงาม คำพูดปนกันอยู่ และบรรทัดที่กำกวมว่าเป็นการตัดสินใจหรือไม่ก็ปล่อยไว้ตามเดิม ข้อความดิบ ๆ แบบนี้แหละคืออินพุต การจัดระเบียบเป็นงานของ AI ไม่ใช่งานที่คนต้องทำก่อน
[เนื้อความบันทึกการประชุม TF การต่อสู้ 2026-06-05 — คัดเฉพาะส่วน, ยังไม่ขัดเกลา]
ทีมงาน A: เรื่องไป global cooldown ที่ 0.5 วินาที ผลซิมูเลชันเสถียรดีครับ
ทีมงาน B: ถ้ามัดสกิลฟื้นฟูไว้ที่ 0.5 วินาทีด้วย รู้สึกว่าวงจรการฟื้นฟูจะพังนะ
ทีมงาน A: อันนั้นแยกออกมาเถอะครับ ให้การฟื้นฟูเป็นข้อยกเว้นของ global cooldown
อีมินซู: ดีครับ รวม global cooldown เป็น 0.5 วินาที ส่วนการฟื้นฟูเป็นข้อยกเว้น
คุณ A ช่วยดูคอลัมน์ cooldown ในชีตข้อมูลรวดเดียวให้ทีครับ
ทีมงาน C: กฎลำดับความสำคัญของการเล็งเป้า ขอดูเพิ่มแล้วค่อยตัดสินสัปดาห์หน้า...
ทีมงาน B: ปุ่มสลับย่อมินิแมป น่าจะต้องดูร่วมกับทีม UI นะครับ ขอพักไว้ก่อน
อีมินซู: ครับ อันนั้นยกไปประชุมครั้งหน้า
ในนี้มีการตัดสินใจสองเรื่อง (global cooldown 0.5 วินาที, การฟื้นฟูเป็นข้อยกเว้น) กับเรื่องที่พักไว้สองเรื่อง (การเล็งเป้า, มินิแมป) ปนกันอยู่ ถ้าคนคัดด้วยตาเปล่า ก็จะพลาดทีละเรื่อง นั่นแหละคืออุบัติเหตุที่เล่าไว้ตอนต้น
จากบันทึกการประชุมที่แนบมา ช่วยดึงเฉพาะ "การตัดสินใจ" ออกมา การถกเถียง·การพักไว้·การแชร์ข้อมูลไม่ใช่การตัดสินใจ
ทุกการตัดสินใจให้เติมสี่ช่อง decision (หนึ่งประโยค) / owner / rationale / follow_up แต่
ถ้าหา owner หรือ rationale จากเนื้อความไม่เจอ อย่าเดา
ให้เขียนว่า "[MISSING — ยังไม่ได้สรุปในที่ประชุม]" เรื่องที่พักไว้·ยกไปประชุมครั้งหน้าให้แยกเป็น deferred
บรรทัดที่กำกวมว่าเป็นการตัดสินใจหรือไม่ให้แยกเป็น ambiguous แล้วส่งคืนให้ฉัน ผลลัพธ์ให้
มีแค่สามบล็อก decisions / deferred / ambiguous เท่านั้น
จุดที่ต้องสังเกตคือ ครึ่งหนึ่งของพรอมต์นี้คือ "การบังคับช่องว่าง" หากให้อิสระแก่ AI มันจะกุ owner ขึ้นมาให้ดูน่าเชื่อ หรือยกเรื่องที่พักไว้ให้กลายเป็นการตัดสินใจ การวางทางออกที่ว่า "ถ้าหาไม่เจอ อย่าเดา ให้รายงานว่า [MISSING]" ไว้คือแก่นของ workflow นี้ การตัดสินใจจะมีค่าก็ต่อเมื่อคนประกาศอย่างชัดเจน (หลักการใน §17.6.3) และ AI ทำได้แค่ถึงขั้น เผยให้เห็น ว่าตรงนั้นว่างเปล่าเท่านั้น
decisions:
- decision: รวม global cooldown ของการต่อสู้ให้เป็น 0.5 วินาที
owner: ทีมงาน A ("คุณ A ช่วยดูคอลัมน์ cooldown ในชีตข้อมูลรวดเดียวให้ทีครับ" — อีมินซู)
rationale: "เรื่องไป global cooldown ที่ 0.5 วินาที ผลซิมูเลชันเสถียรดีครับ" (ทีมงาน A)
follow_up: ตรวจสอบคอลัมน์ cooldown รวดเดียว
- decision: ยกเว้นสกิลฟื้นฟูออกจากการใช้ global cooldown
owner: [MISSING — ยังไม่ได้สรุปในที่ประชุม]
rationale: "ถ้ามัดการฟื้นฟูไว้ที่ 0.5 วินาทีด้วย รู้สึกว่าวงจรการฟื้นฟูจะพัง" (ทีมงาน B)
follow_up: นำการจัดการข้อยกเว้น cooldown ของสกิลฟื้นฟูไปลงในข้อมูล
deferred:
- กฎลำดับความสำคัญของการเล็งเป้า (ทีมงาน C, ตรวจสอบเพิ่มสัปดาห์หน้า)
- ปุ่มสลับย่อมินิแมป (ต้องหารือกับทีม UI, พักไว้)
ambiguous: (ไม่มี)
ส่วนที่มีค่าที่สุดในผลลัพธ์ไม่ใช่ตารางที่เรียบร้อยของการตัดสินใจเรื่องแรก แต่เป็น owner: [MISSING] ของการตัดสินใจเรื่องที่สอง เรื่องการยกเว้นการฟื้นฟูถูกตัดสินใจไปแล้วแน่ ๆ แต่ไม่มีที่ไหนในบันทึกการประชุมเขียนไว้ว่า "ใครจะนำไปลงในข้อมูล" AI ไม่ได้อุดช่องโหว่นั้นด้วยการเดา แต่รายงานอย่างซื่อตรง พรอมต์ที่ดีคือพรอมต์ที่ทำให้ AI พูดได้ว่า "ตรงนี้ว่างเปล่าครับ"
ห้ามรับผลลัพธ์นี้มาตามนั้น การที่ [MISSING] ขึ้นมาหมายความว่า ที่ประชุมจบการตัดสินใจไปเพียงครึ่งเดียว สิ่งที่ผู้นำต้องทำ ณ ตรงนี้ไม่ใช่การไปแก้ผลลัพธ์ของ AI แต่คือการตัดสินใจในส่วนที่ขาดไปจากที่ประชุมให้ครบ
ผู้เขียนได้ถามทีมงาน A หนึ่งบรรทัดทางแชตภายในทีม ณ จุดนี้ว่า "เรื่องนำข้อยกเว้นการฟื้นฟูไปลงในข้อมูล คุณ A ดูด้วยกันใช่ไหมครับ" A ตอบว่า "ครับ" หนึ่งบรรทัดนี้สรุปเจ้าของที่ขาดหายไป จากนั้นจึงร้องขอใหม่
owner ของการตัดสินใจเรื่องที่สอง (ข้อยกเว้นการฟื้นฟู) สรุปเป็นทีมงาน A แล้ว (ยืนยันกับเจ้าตัวทางแชตภายในทีม)
ช่วยนำไปปรับแล้วให้ decisions ใหม่ และแปลงการตัดสินใจสองเรื่องเป็นรูปแบบ
ผู้สมัคร pending atom ให้ด้วย
// (เจตนา: รวม status: pending, source_meeting, owner, related_atoms — รูปแบบ §17.2.4)
AI แปลงการตัดสินใจสองเรื่องที่ owner ถูกเติมแล้วเป็นผู้สมัคร pending atom สองรายการแล้วตอบกลับมา ผู้สมัครเหล่านี้ไม่ได้กลายเป็นการตัดสินใจอย่างเป็นทางการในทันที แต่จะผ่าน ช่วงตรวจสอบ 1 สัปดาห์ในสถานะ pending (§17.2.4) เพราะสิ่งที่ตกลงในที่ประชุมบางครั้งก็ถูกพลิกหลังจากใช้งานจริงไปหนึ่งสัปดาห์ เท่ากับให้เวลาหมึกได้แห้ง หนึ่งรอบของ อินพุต → สกัด → รายงาน MISSING → คนเติมการตัดสินใจให้ครบ → ร้องขอใหม่ ปิดลงตรงนี้
หนึ่งรอบนี้ป้องกันอุบัติเหตุที่เล่าไว้ตอนต้นได้อย่างเป็นโครงสร้าง เมื่อการตัดสินใจเสร็จไปเพียงครึ่งเดียว ความจริงนั้นถูกเผยให้เห็น ณ ที่นั้นทันทีหลังประชุม ไม่ใช่อีกหนึ่งสัปดาห์หลังประชุมจบ
หากวางบันทึกเซสชันจริงข้างต้นทับลงบนไปป์ไลน์บันทึกการประชุมของส่วนที่ 17 ภาพรวมทั้งหมดเป็นแบบนี้ จุดที่มือของผู้นำสัมผัสมีแค่สองจุด คือจุดที่ ประกาศ การตัดสินใจในที่ประชุม (ส่วนหน้าสุด) และจุดที่ เติม [MISSING] ที่ AI รายงาน (ตรงกลาง) ส่วนการสกัด·การแปลง·การลงทะเบียนระหว่างนั้นเป็นแบบอัตโนมัติ
flowchart TB
A["ดำเนินการประชุม
(ผู้นำ: ประกาศการตัดสินใจด้วยปาก)"] --> B["ข้อความบันทึกการประชุม
(เนื้อความที่ยังไม่ขัดเกลา)"]
B --> C["AI สกัดการตัดสินใจ
บังคับ 4 ฟิลด์ + รายงาน MISSING"]
C --> D{"owner·rationale
ว่างหรือไม่?"}
D -->|"MISSING"| E["ผู้นำเติมให้ครบ
(ยืนยันทางแชต → สรุปเจ้าของ)"]
E --> C
D -->|"เติมครบทั้งหมด"| F["ผู้สมัคร pending atom
(ช่วงตรวจสอบ 1 สัปดาห์ §17.2.4)"]
F --> G["รีวิวสัปดาห์ละ 1 ครั้ง
เลื่อนขั้น·ยกเลิก·พักไว้ §17.2.5"]
G --> H["ลงทะเบียน JIT manifest
ฉีดเข้าเซสชันถัดไปอัตโนมัติ §17.2.6"]
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class C,D ai;
class A,E,G human;
class B,F,H data;
ในไปป์ไลน์นี้ สิ่งที่ AI ไม่ทำ สำคัญกว่า AI ไม่สร้างการตัดสินใจ ไม่กุเจ้าของขึ้นมา ไม่ยกเรื่องที่พักไว้ให้กลายเป็นการตัดสินใจ สิ่งที่ AI ทำคือคัดผู้สมัครการตัดสินใจออกมาจากบันทึกการประชุม และทำได้ถึงขั้น เผย ช่องว่างให้เห็นเท่านั้น การประกาศการตัดสินใจและการเติมช่องว่างเป็นงานของคน นี่คือการประยุกต์ใช้หลักการ "สล็อตการตัดสินใจห้าม AI สร้างอัตโนมัติ" ที่กล่าวไว้ใน §17.6.3 จากมุมมองของผู้นำ — เพราะเมื่อการตัดสินใจแพร่กระจายไปยังเอกสาร·เซสชัน·บิลด์อื่น มันจะทิ้งร่องรอยที่ย้อนกลับไม่ได้ไว้ เราจึงรักษาจุดที่คนประกาศอย่างชัดเจนไว้ ณ ด่านทางเข้า
ใน atom ที่แชร์ร่วมกันในทีมบน PC ของบริษัท มี atom เชิงแนวคิดชื่อ team_equal_decision_culture เป็นการตรึงคำศัพท์ที่ถูกอ้างอิงซ้ำ ๆ ในการทบทวนเป็นกฎ ใช้คำเดียวเพื่อชี้ถึงวัฒนธรรมทีมที่ว่า "การตัดสินใจทำด้วยเหตุผล ไม่ใช่ด้วยตำแหน่ง" เป็นวัฒนธรรมที่ไม่ใช่ผู้อำนวยการกดว่า "ฉันตัดสินใจแล้ว จบ" แต่เป็นการทิ้งไว้ว่าใคร·เพราะอะไรในทุกการตัดสินใจ เพื่อทำให้ ใครก็ตามสามารถย้อนกลับมาทบทวนการตัดสินใจนั้นจากเหตุผลได้ในภายหลัง
การบังคับ [MISSING] ใน §19.2.3 คือการหนุนหลังทางเทคนิคของวัฒนธรรมนี้พอดี การไม่ปล่อยให้เจ้าของและเหตุผลผ่านไปเป็นช่องว่าง หมายความว่าอำนาจของการตัดสินใจวางอยู่บน "เพราะออกมาจากคำพูดในเนื้อความตรงไหน" ไม่ใช่ "เพราะผู้อำนวยการพูด" หากการอ้างอิงเหตุผลว่างเปล่า การตัดสินใจก็จะถูกกั้น ดังนั้นการตัดสินใจที่กดด้วยตำแหน่งจึงกลายเป็น atom ไม่ได้อย่างเป็นโครงสร้าง
วัฒนธรรมนี้เชื่อมต่อเป็นเส้นเดียวกับใบสั่งยาของความขัดแย้งใน §19.2.1 ด้วย การแก้ความขัดแย้งเชิงคุณค่าด้วยการอ้างอิงวิสัยทัศน์, ความขัดแย้งเชิงข้อเท็จจริงด้วยข้อมูล, ความขัดแย้งเชิงอำนาจด้วยเมทริกซ์ ล้วนเป็นหลักการเดียวกันคือ แก้ด้วยเหตุผลที่ถูกบันทึกไว้ แทนปากของคน วัฒนธรรมการตัดสินใจอย่างเท่าเทียมคือดินสำหรับใบสั่งยาของความขัดแย้ง และการบังคับ [MISSING] คือเครื่องมือที่คอยพรวนดินนั้นทุกครั้งไม่ให้แข็งตัวในระดับหน่วยการประชุม
ตรงนี้มีอีกแกนหนึ่งของวัฒนธรรมทีมซ้อนอยู่ นั่นคือเส้นแบ่งระหว่างเปิดเผยกับปิด บันทึกการประชุม·การ์ดการตัดสินใจ·ข้อมูล KPI·รายงานอุบัติเหตุ วางไว้ในพื้นที่เปิดเผย ส่วนการสนทนา 1:1·การประเมินบุคลากร·เงินเดือน·เรื่องส่วนตัว วางไว้ในพื้นที่ปิด สิ่งที่ไปป์ไลน์สกัดการตัดสินใจจัดการล้วนอยู่ในพื้นที่เปิดเผย เหตุผลที่ความขัดแย้งส่วนบุคคล (ประเภทที่ห้าใน §19.2.1) อยู่นอกระบบก็เช่นกัน — เพราะมันเป็นพื้นที่ปิด เราจึงไม่ตรึงเป็น atom
บทเกี่ยวกับภาวะผู้นำมีสิ่งล่อใจสูงที่อยากใส่ตารางแบบ "พอนำไปป์ไลน์การประชุมมาใช้ เวลาประชุมก็ลดลงครึ่งหนึ่ง" ตัวเลขแบบนั้นถ้าไม่ผ่านการตรวจสอบจะลดทอนความน่าเชื่อถือของหนังสือ หลักการของหนังสือเล่มนี้คือหนึ่งในสามข้อ
ข้อแรก สัญญาเป็นตัวชี้วัดเฉพาะสิ่งที่วัดได้ สิ่งที่ไปป์ไลน์การประชุมนับได้จริงคือสิ่งเหล่านี้ — จำนวนครั้งที่ขาด owner·rationale ต่อการตัดสินใจ (เป้าหมาย 0), สัดส่วนของการตัดสินใจที่สกัดจากบันทึกการประชุมแล้วเลื่อนขั้นเป็น pending atom, จำนวนครั้งของการประชุมซ้ำในแบบ "เรื่องนี้ไม่ได้ตัดสินใจไปแล้วเหรอ?" สามอย่างนี้พูดในที่ประชุมด้วยตัวเลขได้ ไม่ใช่ด้วย "ความรู้สึก"
ข้อสอง การประมาณของผู้เขียนให้เขียนว่าเป็นการประมาณ การที่เวลาที่ใช้ในการสกัดการตัดสินใจทันทีหลังประชุมคือ "จัดบันทึกการประชุมด้วยมือ 20–30 นาที → ร่างของ AI + เติมให้ครบ ภายใน 5 นาที" นั้นเป็นการประมาณของผู้เขียนที่อิงจากประสบการณ์ และเป็นสมมติฐานที่ยังไม่ได้ตรวจสอบ อย่าท่องจำค่าสัมบูรณ์ แต่ให้อ่านจาก ความต่างเชิงโครงสร้าง ("คนคัดเองตั้งแต่ต้น" vs "AI สกัด + เติมเฉพาะช่องว่าง") ก็พอ เวลาที่ประหยัดได้จริงจะต่างกันไปตามขนาดการประชุม·จำนวนการตัดสินใจ
ข้อสาม อย่าฟันธงเหตุปัจจัย อย่าตอกตะปูว่า "การประชุมซ้ำลดลง" เป็นเพราะไปป์ไลน์นี้ทั้งหมด ความเป็นผู้ใหญ่ของทีม·ขั้นตอนของโปรเจกต์ก็มีส่วนร่วมด้วย พูดแค่ทิศทาง (เมื่อการตัดสินใจที่หลุดหายถูกเผยให้เห็นทันทีหลังประชุม มันจะทำงานไปในทางที่การประชุมซ้ำลดลง) ก็พอ อย่ากุตัวคูณขึ้นมา
ใบสั่งยาของความขัดแย้งและไปป์ไลน์การตัดสินใจจะหมุนรอบตรวจสอบหนึ่งรอบในการทบทวนรายไตรมาส โดยวาง "สล็อตความขัดแย้ง" และ "สล็อตการตัดสินใจที่หลุดหาย" ไว้ในการทบทวน
การทบทวนรายไตรมาส Q2 2026 — สล็อตความขัดแย้ง·การตัดสินใจ
─────────────────────────────────
[ความขัดแย้ง] 3 เรื่องหลักของไตรมาสนี้
1. global cooldown (ความขัดแย้งเชิงคุณค่า) → ปิดเรื่องด้วยการอ้างอิงวิสัยทัศน์
สิ่งที่เรียนรู้: ยืนยันอีกครั้งว่าวิสัยทัศน์ 5 สล็อตทำงานเป็นเกณฑ์การตัดสินใจ
2. ลำดับความสำคัญของดันเจี้ยนใหม่ (ความขัดแย้งเชิงลำดับความสำคัญ) → เปรียบเทียบผลกระทบต่อ KPI
สิ่งที่เรียนรู้: ไม่มีตารางลำดับความสำคัญ จึงต้องเปรียบเทียบสด ๆ ทุกครั้ง → นำตารางมาใช้ไตรมาสหน้า
3. อำนาจในการออกแบบตัวละคร (ความขัดแย้งเชิงอำนาจ) → ทบทวนเมทริกซ์อำนาจอีกครั้ง
สิ่งที่เรียนรู้: ต้องเพิ่มหัวข้อแบ่งงาน 'ภาพ vs ฟังก์ชัน' ลงในเมทริกซ์
[การตัดสินใจที่หลุดหาย] เรื่องที่เกิด [MISSING] ในไตรมาสนี้
- การตัดสินใจข้อยกเว้นการฟื้นฟูไม่ได้ระบุ owner (2026-06-05) → เติมให้ครบทางแชตภายในทีม
สิ่งที่เรียนรู้: เพิ่มการเรียกชื่อ owner ทันทีตอนประกาศการตัดสินใจในการประชุม TF ลงในเช็กลิสต์การดำเนินงาน
ทั้งความขัดแย้งและการตัดสินใจที่หลุดหายล้วนเป็นอินพุตของการทบทวน หากความขัดแย้งประเภทเดียวกันเกิดซ้ำ ก็ปรับระบบ (ตารางวิสัยทัศน์·อำนาจ) และหาก [MISSING] ขึ้นบ่อยในรูปแบบเดียวกัน ก็ปรับวิธีดำเนินการประชุม "การตรวจสอบระบบ·กฎ" ที่แยกไปทางขวาในผังการไหลของ §19.2.1 จะเป็นรูปธรรมขึ้นตรงนี้
การประยุกต์นอกเกม อุบัติเหตุของการประชุมที่ว่า "ตัดสินใจไปแล้วแน่ ๆ แต่อีกหนึ่งสัปดาห์ต่อมาวาระเดิมขึ้นมาอีก" นั้นไม่เลือกประเภทธุรกิจ หากไม่ขัดเกลาเนื้อความบันทึกการประชุมแล้วใส่เข้า LLM ตามนั้นเพื่อสกัดเฉพาะการตัดสินใจ แต่ถ้าเจ้าของหรือเหตุผลว่างเปล่าก็ไม่เติมด้วยการเดา ให้รายงานเป็น
[MISSING]แทน ความจริงที่ว่าการตัดสินใจเสร็จไปเพียงครึ่งเดียวก็จะถูกเผยให้เห็น ณ ที่นั้นทันทีหลังประชุม ยกตัวอย่างเช่น ในการประชุมรายสัปดาห์ของฝ่ายขาย หาก "บัญชีนี้ตกลงให้ A รับผิดชอบ" ผ่านไปเพียงด้วยปากโดยไม่ถูกบันทึก สัปดาห์ถัดไปมันจะลอยอยู่ในอากาศ แต่ถ้าการสกัดของ AI ขึ้นowner: [MISSING]ก็จะสรุปเจ้าของได้ด้วยแชตหนึ่งบรรทัด ณ ที่นั้น ลดการประชุมซ้ำไปได้หนึ่งครั้ง การแบ่งงานที่ให้คนเป็นผู้ประกาศการตัดสินใจและเติมช่องว่าง ส่วน AI เป็นผู้สกัดคือแก่นสำคัญ
ถ้าทำคนเดียวก็แค่นี้พอ: ไม่ต้องมีทีมหรือไปป์ไลน์บันทึกการประชุมก็ได้ ลองนำบันทึกของการประชุมที่คุณเข้าร่วมล่าสุด (จะเป็นการเรียนกลุ่ม·ชมรม·การหารือโปรเจกต์คนเดียวก็ได้) มาวางลงในพรอมต์ของ §19.2.3 ตามนั้นแล้วหมุนสักครั้ง หากมีการตัดสินใจที่ AI ขึ้น
owner: [MISSING]แม้แต่เรื่องเดียว นั่นแหละคือวาระที่ทีมของคุณ (หรือตัวคุณเอง) จะหยิบขึ้นมาอีกในอีกหนึ่งสัปดาห์ เพียงเติมช่องว่างนั้นตอนนี้ การประชุมซ้ำหนึ่งครั้งก็หายไป
หากเป็นทีม ให้เริ่มด้วยหนึ่งก้าวถัดไปนี้ นำบันทึกการประชุมครั้งถัดไปใส่ลงในพรอมต์สกัดของ §19.2.3 ตามนั้นโดยไม่ขัดเกลา และรักษาไว้เพียงกฎข้อ 2 (การบังคับ [MISSING]) ส่วน pending atom·การลงทะเบียน JIT (§17.2) ค่อยทำทีหลัง แม้มีเพียงหนึ่งบรรทัดของการบังคับช่องว่าง คุณก็จับการหลุดหายที่แพงที่สุดอย่าง "คิดว่าตัดสินใจแล้วแต่ไม่ได้เขียนไว้" ได้ทันทีหลังประชุม
| รูปแบบ | ทำไมจึงล้มเหลว | ใบสั่งยา |
|---|---|---|
| แก้ความขัดแย้งทุกเรื่องด้วยวิธีเดียวกัน | ไม่มีประเภทไหนถูกแก้จนจบ | จำแนก 5 ประเภท → ใบสั่งยาตามประเภท (§19.2.1) |
| พอใจกับทีมที่มีความขัดแย้งเป็นศูนย์ | ความขัดแย้งจมลงใต้ผิวน้ำ (อันตรายกว่า) | ความขัดแย้งคือสัญญาณสุขภาพ, สล็อตการทบทวนรายไตรมาส |
| ตัดสินใจด้วยปากอย่างเดียวแต่ไม่เขียนไว้ | อีกหนึ่งสัปดาห์ต่อมาวาระเดิมประชุมซ้ำ | AI สกัด + ตรึง pending เป็นกฎ (§19.2.3) |
| AI เติมเจ้าของด้วยการเดา | เจ้าของที่ผิดแข็งตัวเป็น atom | บังคับ [MISSING], ห้ามเดา (§19.2.2) |
| AI สร้างการตัดสินใจอัตโนมัติ | อำนาจของการตัดสินใจหลุดออกจากเหตุผล | การประกาศการตัดสินใจเป็นของคน, AI แค่เสริม (§17.6.3) |
| ยกเรื่องที่พักไว้ให้กลายเป็นการตัดสินใจ | วาระที่ยังไม่สรุปแพร่กระจายแบบย้อนกลับไม่ได้ | แยกออกเป็นบล็อก deferred (§19.2.3) |
เรื่องที่สามกับเรื่องที่สี่มักผูกกันแล้วระเบิดบ่อยที่สุด ทีมที่ไม่เขียนการตัดสินใจมักโยนให้ AI ทั้งก้อนว่า "จัดการให้ที" และ AI ก็กุเจ้าของขึ้นมาอย่างน่ารัก พอเจ้าของที่ถูกกุนั้นแข็งตัวเป็น atom อีกหนึ่งสัปดาห์ต่อมาก็เกิดความขัดแย้งที่แพงกว่าอย่าง "ผมไม่เคยตกลงว่าจะรับผิดชอบนะครับ" การบังคับ [MISSING] ป้องกันสองความล้มเหลวนั้นได้ด้วยบรรทัดเดียว
[MISSING]ผู้อ่านหลัก: ลีดที่ต้องตัดสินใจว่าจะนำ AI มาใช้ในทีมหรือไม่ และต้องอธิบายต้นทุนนั้นต่อผู้บริหาร (ทีมขนาดกลาง 10\~50 คน) ฉบับย่อสำหรับผู้อ่านคนเดียว/งานอดิเรก: §19.3.12 「ถ้าทำคนเดียวก็แค่เท่านี้」
ผู้เขียนเคยถูกถามในห้อง CEO ว่า "ค่าเครื่องมือ AI เสียไปเดือนละเท่าไร แล้วมันดีขึ้นตรงไหน" สิ่งที่ถืออยู่ในมือตอนนั้นคือสไลด์แผ่นเดียว บนนั้นเขียนว่า "เพิ่มผลิตภาพ 3\~5 เท่า" CEO ถามกลับว่า "เลข 3\~5 เท่านั้นมาจากไหน" ผมตอบไม่ได้ เพราะตัวเลขนั้นคือสิ่งที่ผมยกค่าเฉลี่ยจากบล็อกที่เห็นมาจากที่ไหนสักแห่ง ไม่ใช่ค่าที่เราวัดได้ในทีมของเรา
หลังจากวันนั้น ผู้เขียนตัดตัวเลขปรุงแต่งออกจากรายงานการนำ AI มาใช้ทั้งหมด แทนที่ด้วยสิ่งที่ระบบทิ้งไว้จริง ๆ — มี atom สะสมกี่ตัว มีสกิลทำงานกี่ตัว ในล็อกอินพุตแบบไหนเรียกบริบทแบบไหนเข้ามา — แล้วเริ่มรายงานสิ่งเหล่านั้นตามที่เป็นจริง บทนี้พูดถึงสองเรื่อง เรื่องแรกคือกรอบการตัดสินใจที่แบ่งการนำ AI มาใช้ออกเป็นขั้นตอน จากแบบอนุรักษนิยม (คนตัดสินใจ AI ตรวจสอบ) สู่แบบก้าวหน้า (AI สร้างตัวเลือก คนเลือกนำมาใช้) เรื่องที่สองคือวิธีอธิบาย ROI ของการนำมาใช้ต่อผู้บริหาร ด้วยล็อกที่วัดได้จริงในระบบของผมเอง ไม่ใช่ค่าเฉลี่ยจากบล็อก ทฤษฎีภาวะผู้นำทั่วไปมีในหนังสือเล่มอื่นเพียงพอแล้ว บทนี้จึงโฟกัสเฉพาะที่ การใช้ AI ช่วยตัดสินใจเรื่องการนำ AI มาใช้เอง และดึงหลักฐานนั้นขึ้นมาจากล็อกของระบบ เท่านั้น
ถ้ามองการนำ AI มาใช้เป็นสองทางว่า "ใช้หรือไม่ใช้" ก็จะติดกระดุมเม็ดแรกผิดตั้งแต่ต้น ถ้าเปิดเครื่องมือห้าตัวพร้อมกัน ภาระการดำเนินงานจะมาถึงก่อนผลลัพธ์ และถ้ากลัวจนไม่เปิดอะไรเลยก็จะไม่ได้เริ่มเสียที การนำมาใช้คือ การตัดสินใจแบบเป็นขั้นตอน ที่เริ่มจากจุดที่ความเสี่ยงต่ำ แล้วค่อยขยายสิทธิ์เมื่อผ่านการตรวจสอบแล้ว
เกณฑ์ที่ร้อยเรียงทั้งเล่มนี้ก็นำมาใช้ที่นี่เช่นเดิม การประยุกต์แบบอนุรักษนิยม ที่คนตัดสินใจและ AI ทำหน้าที่ตรวจสอบเท่านั้น กับ การประยุกต์แบบก้าวหน้า ที่ AI สำรวจตัวเลือกและคนเลือกนำมาใช้ การนำมาใช้ก็เดินตามลำดับนี้ เริ่มจากการป้อนบริบท (อนุรักษนิยม) แล้วเมื่อการตรวจสอบสะสมมากพอจึงข้ามไปสู่การสร้างอัตโนมัติ (ก้าวหน้า) ถ้ากระโดดย้อนลำดับ — เปิดการสร้างอัตโนมัติก่อนโดยไม่มีการตรวจสอบ — อุบัติเหตุจะสะสมและทีมจะเรียกร้องให้ปิดเครื่องมือ
flowchart TD
S0["ขั้นที่ 0: ทำมือ
ไม่มี AI"] --> S1["ขั้นที่ 1 อนุรักษนิยม: ป้อนบริบท
คนตัดสินใจ · AI ร่าง/ตรวจสอบ
(Pilot 1~3 เดือน)"]
S1 --> G1{ตรวจสอบ
อัตราอุบัติเหตุ·ความพึงพอใจ}
G1 -->|ผ่าน| S2["ขั้นที่ 2: ทำให้การตรวจสอบเป็นอัตโนมัติ
lint·rulebook เป็นด่าน
(ขยาย 3~6 เดือน)"]
G1 -->|ไม่ถึง| S1
S2 --> G2{ตรวจสอบ
อัตราการทิ้ง·การเกิดซ้ำ}
G2 -->|ผ่าน| S3["ขั้นที่ 3 ก้าวหน้า: สร้างอัตโนมัติ
AI สำรวจตัวเลือก · คนเลือกนำมาใช้
(ลงตัว 6~12 เดือน)"]
G2 -->|ไม่ถึง| S2
S3 --> G3{ด่านที่ย้อนกลับไม่ได้
การจ้างงาน·การเปลี่ยนองค์กร}
G3 -->|เฉพาะเมื่อตรวจสอบส่วนที่ย้อนกลับได้เสร็จ| S4["ขั้นที่ 4: บทบาทวิวัฒน์
ดำเนินการหลังเห็นพ้อง"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
class S2 code;
class S3 ai;
class S0,S4,G1,G2,G3 human;
หัวใจอยู่ที่ด่าน (gate) ระหว่างแต่ละขั้น การจะข้ามไปขั้นถัดไปได้ ค่าที่วัดได้ในขั้นก่อน (อัตราอุบัติเหตุ·อัตราการทิ้ง·ความพึงพอใจ) ต้องผ่านเกณฑ์ โดยเฉพาะขั้นที่ 4 ขั้นสุดท้าย (บทบาทวิวัฒน์) นั้น ย้อนกลับไม่ได้ เพราะเป็นขั้นที่หน้าที่ของคนเปลี่ยนและแผนการจ้างงานขยับ จึงไม่ไปแตะต้องก่อนที่การตรวจสอบในขั้นที่ย้อนกลับได้ข้างหน้าจะจบ โครงสร้างด่านนี้แหละที่กันไม่ให้กระโดดข้ามไปสู่การประยุกต์แบบก้าวหน้าทีเดียวเพราะถูกบรรยากาศ "ได้ยินว่า AI ดีนะ" ดันไป
สมมติว่าตัดสินใจนำมาใช้แล้ว ด่านถัดไปคือผู้บริหารที่อนุมัติต้นทุนนั้น ตรงนี้สิ่งที่ลีดทำพลาดมากที่สุดคือการใส่ตัวเลขที่ไม่มีแหล่งที่มาอย่าง "ผลิตภาพ N เท่า" ลงในสไลด์ ตัวเลขนั้นจะพังลงตั้งแต่คำถามแรก
แทนที่จะทำเช่นนั้น ทำอย่างนี้ สั่ง AI ให้ นับสินทรัพย์ที่ระบบของผมทิ้งไว้จริง ๆ แล้วเรียบเรียงเป็นสไลด์ ROI (Return on Investment, ผลตอบแทนเทียบกับการลงทุน) แต่อย่าสร้างตัวเลขที่ไม่มีแหล่งที่มาขึ้นมาเด็ดขาด ด้านล่างคือการถ่ายทอดรอบหนึ่งจากอินพุตไปจนถึงการทิ้งและการสร้างใหม่ตั้งแต่ต้นจนจบ พรอมต์อินพุตคัดลอกไปใช้ได้เลย ส่วนเอาต์พุตเป็นการเรียบเรียงเซสชันจริงขึ้นมาใหม่
ก่อนอื่น รวบรวมตัวเลขที่มีอยู่ในระบบแล้วและไม่จำเป็นต้องกุขึ้น อินเวนทอรีหน่วยความจำทีมในพีซีบริษัทและล็อก JIT ในพีซีส่วนตัว คืออินพุตหลัก
# ai_adoption_inventory.yaml — สินทรัพย์ที่วัดได้จริงหลังนำมาใช้ 1 ปี (อ้างอิง book_appendix_A)
team_atoms: # workspace/team_memory/atoms/
rules: 244
concepts: 19
decisions: 26
feedback: 11
rnd: 4
total: 304
skills: # workspace/skills/
wrapper: 44
meta: 4
total: 48
jit_manifest:
hot_atoms_injected: 221 # score>=20 OR manual_weight>=4
external_export_atoms: 207 # md เดี่ยวสำหรับป้อนเข้า GPT/Gemini
operating_cost_usd_month: "ต้องวัดจริง" # ช่องว่าง — อย่ากุขึ้น
hot_atom_example:
- view_html_filename_convention: 356.53 # _scores_latest.json
- xlsm_svn_update_before_edit: 349.26
- claude_role_transition_phase2: 341.03 # decision atom
ใน yaml นี้ไม่มีของปลอม 304·48·221·207 คือค่าที่นับจากอินเวนทอรีหน่วยความจำทีม และคะแนนอย่าง 356.53 คือค่าจริงที่บันทึกไว้ใน _scores_latest.json (ผลผลิตของ atom_score.py) ส่วนช่องต้นทุนการดำเนินงานนั้นเว้นว่างไว้โดยตั้งใจ — นั่นเป็นค่าที่ต้องรับมาจากฝ่ายบัญชีแล้วเติม ไม่ใช่ค่าที่ AI ควรประมาณ
ช่วยทำสไลด์ ROI หนึ่งหน้าสำหรับผู้บริหาร จาก ai_adoption_inventory.yaml ที่แนบมา
(สินทรัพย์ที่วัดได้จริงหลังนำมาใช้ 1 ปี) ใช้เฉพาะตัวเลขที่มีใน yaml และอย่าสร้าง
อัตราส่วน·ตัวคูณ·จำนวนเงินที่ไม่มีอยู่ขึ้นมาเด็ดขาด อย่าใช้ถ้อยคำอย่าง "ผลิตภาพ N เท่า"
แต่ให้ใช้ข้อเท็จจริงที่นับได้ (จำนวน atom, จำนวนสกิล, จำนวนครั้งการป้อน JIT) ส่วน
operating_cost เป็นช่องว่าง อย่าเติมเป็นตัวเลข ให้เว้นเป็น "[ต้องให้ฝ่ายบัญชียืนยัน]"
และทิ้งไว้เป็นคำถามให้อนุมัติ ข้ออ้างที่หาหลักฐานมายืนยันไม่ได้ ให้แยกออกมาทำเครื่องหมาย
ส่งกลับมาให้ผม เรียงตามที่ CEO สนใจ (อะไรลดลงเทียบกับต้นทุน) เอาต์พุตเป็นหนึ่งหน้า:
สรุป 1 บรรทัด → ตารางสินทรัพย์ที่วัดได้จริง → คำถามให้อนุมัติ
สังเกตว่าครึ่งหนึ่งของพรอมต์คือ "ห้ามกุตัวเลข" LLM มีแนวโน้มสูงที่จะเติมช่องว่างด้วยตัวเลขที่ดูน่าเชื่อ ใช้เฉพาะตัวเลขที่มีใน yaml · ช่องว่างให้คงเป็นช่องว่าง · ไม่มีหลักฐานให้แจ้ง สามตะปูนี้คือกระดูกสันหลังของพรอมต์นี้
# สรุป ROI การนำ AI มาใช้ 1 ปี (หนึ่งหน้า)
หนึ่งบรรทัด: ตลอด 1 ปีของการนำมาใช้ ความรู้ที่ใช้ร่วมกันในทีมถูกแปลงเป็นสินทรัพย์
เป็น atom 304 ตัว·สกิลอัตโนมัติ 48 ตัว และในจำนวนนั้น 221 ตัวเข้าสู่ขั้นการดำเนินงาน
ที่ถูกป้อนอัตโนมัติตามอินพุต
## สินทรัพย์ที่วัดได้จริงและประเมินได้
| รายการ | ค่า | ความหมาย |
|---|---|---|
| atom ที่ใช้ร่วมกันในทีม | 304 ตัว | กฎ·การตัดสินใจป้องกันการเกิดซ้ำถูกตรึงเป็นเอกสาร |
| สกิลอัตโนมัติ | 48 ตัว | งานซ้ำ ๆ ถูกตรึงเป็น wrapper โค้ด |
| atom ที่ป้อน JIT อัตโนมัติ | 221 ตัว | กฎที่ตรงกับบริบทอินพุตถูกเรียกอัตโนมัติ |
| การส่งออกไป AI ภายนอก | 207 ตัว | ปลูกถ่ายบริบทเดียวกันไปยัง GPT/Gemini ได้ |
## ผลเทียบกับต้นทุน
- การนำมาใช้ทำให้ **ผลิตภาพของทีมเพิ่มขึ้น 4.2 เท่า**
- เวลาในการเขียนเอกสาร **ลดลง 68%**
- ต้นทุนการดำเนินงานอยู่ที่ราว $4,500 ต่อเดือน
## คำถามให้อนุมัติ
- ขออนุมัติงบประมาณเครื่องมือสำหรับไตรมาสถัดไป
ในเอาต์พุต ตารางสะอาดดี แต่หัวข้อ "ผลเทียบกับต้นทุน" ถูกปนเปื้อนทั้งยวง "4.2 เท่า", "ลด 68%", "$4,500 ต่อเดือน" เป็นตัวเลขที่ไม่มีใน yaml ละเมิดกฎ 1·2·3 ทั้งหมด โดยเฉพาะช่องต้นทุนการดำเนินงานที่ระบุชัดให้เว้นว่าง ก็ถูกเติมเป็น $4,500 AI เดินตามรูปแบบทั่วไปที่ว่า "ถ้าเป็นสไลด์ ROI ก็ต้องมีตัวเลขแบบนี้" แล้วกุค่าในช่องว่างขึ้นมา นี่คือโหมดความล้มเหลวที่อันตรายที่สุดของ LLM รูปแบบสมบูรณ์แบบจนถ้าผู้ตรวจสอบเผลอ มันก็ไปถึง CEO ตามนั้นเลย
ถ้ารับเอาต์พุตนี้มาตามที่เป็น ความล้มเหลวในห้อง CEO วันนั้นก็จะวนซ้ำ พอเรียกร้องแหล่งที่มาของสามบรรทัดในหัวข้อ "ผลเทียบกับต้นทุน" ทั้งสามก็ไม่มีแหล่งที่มา ปฏิเสธและขอใหม่
สามบรรทัดในหัวข้อ "ผลเทียบกับต้นทุน" (4.2 เท่า·68%·$4,500) เป็นของกุที่ไม่มีใน yaml ลบทิ้ง
แทนที่นั้น ให้เขียนผลใหม่ด้วยข้อเท็จจริงที่นับได้เท่านั้น — เช่น "กฎป้องกันการเกิดซ้ำ 304 ข้อ
ถูกเก็บไว้เป็นเอกสาร (จำนวนเป็นข้อเท็จจริง อัตราการสกัดยังไม่ได้วัด)" แบบนี้ ส่วนต้นทุนการดำเนินงาน
ให้เว้นเป็น [ต้องให้ฝ่ายบัญชียืนยัน] แล้วใส่คำถาม 3 ข้อที่ CEO ควรถามลงในช่องนั้นแทน
AI ตอบกลับมาอีกครั้ง ลบหัวข้อ "ผลเทียบกับต้นทุน" ออก แล้วใส่ย่อหน้าที่ แยกข้อเท็จจริงที่วัดได้ออกจากรายการที่ยังไม่ได้วัด ลงไปแทน
## ผล — สิ่งที่วัดแล้วกับสิ่งที่ยังไม่ได้วัด
**วัดแล้ว (ข้อเท็จจริง):**
- กฎป้องกันการเกิดซ้ำ 304 ข้อถูกตรึงเป็น atom → สกัดการเกิดซ้ำของความผิดพลาดเดิมในระดับเอกสาร
- งานซ้ำ 48 ประเภทถูกตรึงเป็นสกิล → การทำซ้ำด้วยมือถูกแทนที่ด้วยการเรียกใช้ 1 ครั้ง
**ยังไม่ได้วัด (รายงานตามจริงว่ายังไม่ได้วัด):**
- อัตราการประหยัดเวลาทำงาน — ต้องเทียบ timestamp ของงานเดียวกันก่อน-หลัง
- การเปลี่ยนแปลงของอัตราอุบัติเหตุ — ไม่ได้บันทึกเส้นฐาน (baseline) ก่อนนำมาใช้
## คำถามให้อนุมัติ (ต้องให้ CEO ตัดสินใจ)
1. จะตั้งเพดาน (cap) ต้นทุนการดำเนินงานต่อเดือนของแต่ละเครื่องมือไว้เท่าไร? [ฝ่ายบัญชีกรอก]
2. จะรันไพล็อต 1 งานเพื่อวัดการประหยัดเวลาในด้านใด?
3. บทบาทวิวัฒน์ (ขั้นที่ 4) จะเชื่อมโยงกับแผนการจ้างงานไตรมาสถัดไปหรือไม่?
เวอร์ชันนี้ผ่าน เหตุผลที่เมื่อนำขึ้นเสนอ CEO แล้วไม่มีคำถามว่า "ตัวเลขนั้นมาจากไหน" นั้นง่ายมาก เพราะทุกบรรทัดนับได้ หรือเขียนไว้ว่านับไม่ได้ หนึ่งรอบของ อินพุต → ร่างสไลด์ → ตรวจจับของกุ → ปฏิเสธ → สร้างใหม่ ปิดลงตรงนี้ หนึ่งรอบนี้คือเกณฑ์ Show ของรายงานการนำ AI มาใช้ ประโยค "เรียบเรียง ROI ด้วย AI แล้ว" นั้นว่างเปล่า ถ้าไม่ได้เห็นว่าอะไรถูกตรวจจับและคนฆ่าอะไรทิ้ง
ความต่างระหว่างตัวเลขที่รอดในเซสชันข้างบน (304·48·221) กับตัวเลขที่ตายไป ("4.2 เท่า") คือ นับได้หรือไม่ ระบบทิ้งสินทรัพย์ที่นับได้ไว้เพียงแค่จากการดำเนินงานเท่านั้น
ถ้ายกล็อกการป้อน JIT ในพีซีส่วนตัว (~/.claude/hooks/_injection_log.txt) มาอ้างหนึ่งบรรทัดตามที่เป็น ก็จะได้แบบนี้
2026-05-24T11:18:17+09:00 | hits: book_writing_project feedback |
prompt_head: 1) ก่อนอื่นน้ำเสียงเปลี่ยนไปมากเมื่อเทียบกับบทนำตอนต้น...
หนึ่งบรรทัดนี้แสดงให้เห็นข้อเท็จจริงว่า พอยกเรื่อง "น้ำเสียงของหนังสือ" ขึ้นมา atom สองตัวคือ book_writing_project และ feedback ก็ถูกดึงเข้าบริบทอัตโนมัติ ส่วน inject_atom.py ในพีซีบริษัทก็ทำงานด้วยรูปแบบเดียวกัน — เมื่ออินพุตเข้าคู่กับ regex ใน _jit_manifest.json เนื้อหาของ atom นั้นจะถูก prepend สิ่งที่บอกผู้บริหารได้ว่า "นี่คือสิ่งที่เราซื้อมา" คือล็อกแบบนี้ ไม่ใช่ตัวคูณ
atom 304 ตัวเดียวกัน ก็ต้องไปถึง CEO·PD·Game Director ด้วยประโยคที่ต่างกัน เพราะสิ่งที่ผู้ฟังแต่ละคนสนใจต่างกัน ถ้าส่งรายงานฉบับเดียวกันไปตามเดิมสามครั้ง ก็จะไม่ถึงผู้ฟังคนใดเลย
| ผู้ฟัง | ความสนใจ | การจัดเฟรมสินทรัพย์เดียวกัน (atom 304) |
|---|---|---|
| CEO·CFO | ต้นทุน·กลยุทธ์ | "กฎป้องกันการเกิดซ้ำ 304 ข้อกลายเป็นสินทรัพย์ — ป้องกันความรู้สูญหายเมื่อคนลาออก" |
| PD | กำหนดการ·ทรัพยากร·ความเสี่ยง | "งานซ้ำ 48 ประเภททำเป็นอัตโนมัติ — บัฟเฟอร์กำลังประมวลผลเมื่อกำหนดการกดดัน" |
| Game Director | คุณภาพ·ความคืบหน้า | "verification gate ทำงานในระดับ atom — ติดตามอุบัติเหตุแยกตามด้านได้" |
สำหรับ CEO ให้บังคับใช้หนึ่งหน้า ภาคผนวกยาวได้ แต่ตัวเนื้อความที่เกินหนึ่งหน้าเมื่อใด ข้อสมมติ "ผู้ฟังที่ไม่มีเวลา" ก็พังเมื่อนั้น และคำขอการตัดสินใจให้ระบุชัดเป็นห้าช่อง อะไร·ทำไม·ผลกระทบ·ทางเลือก·กำหนดเวลา ถ้าไม่เข้าไปในรูปแบบที่ CEO ตัดสินใจได้ภายใน 5 นาที การตัดสินใจจะล่าช้า และการตัดสินใจที่ล่าช้าก็จะกลับมากระทบการจัดสรรทรัพยากรอีก
[คำขอการตัดสินใจ — 5 ช่อง]
- อะไร: อนุมัติงบเครื่องมือ AI ขั้นที่ 2 (ขยาย), ตั้ง cap ต่อเดือน [ฝ่ายบัญชียืนยัน]
- ทำไม: ในไพล็อตขั้นที่ 1 ตรวจสอบแล้วว่า atom 304·สกิล 48 กลายเป็นสินทรัพย์ (§19.3.2)
- ผลกระทบ: ได้บัฟเฟอร์กำลังประมวลผล vs ต้นทุนการดำเนินงานเพิ่ม (ควบคุมด้วยเพดาน)
- ทางเลือก: คงขั้นที่ 1 แล้วสังเกตเพิ่มอีก 1 ไตรมาส / ขยายบางส่วน (แค่ 2 เครื่องมือ)
- กำหนดเวลาตัดสินใจ: ก่อนจัดทำงบประมาณไตรมาสถัดไป
ตัวเลขต้องแนบการตีความเสมอ ถ้าโยน "การป้อน JIT 221 ครั้ง" เฉย ๆ ภาระการตีความก็ตกไปที่ CEO ต้องเขียนว่า "การป้อน JIT 221 ครั้ง (กฎที่ตรงกับบริบทอินพุตถูกเรียกอัตโนมัติ ทำให้แม้แต่สมาชิกใหม่ก็ทำงานบนกฎเดียวกัน)" คุณค่าของข้อมูลชุดเดียวกันจึงเป็นสองเท่า
ตัวรายงานหลักทำให้เป็นอัตโนมัติได้ แต่ เฉพาะคำขอการตัดสินใจให้คนเขียนเอง เพราะส่วนนั้นการตัดสินใจของผู้อำนวยการเชื่อมโยงตรงกับความรับผิดชอบต่อผลลัพธ์ การที่ใน §19.3.2 สั่ง AI แค่ว่า "ทิ้งไว้เป็นคำถามให้อนุมัติ" แล้วให้คนเป็นผู้สรุปถ้อยคำคำขอสุดท้าย คือการแยกส่วนนี้
ขั้นที่ 1\~3 (ป้อนบริบท → ทำให้การตรวจสอบเป็นอัตโนมัติ → สร้างอัตโนมัติ) อยู่ในขอบเขตของเทคนิคและการดำเนินงาน จึงผ่านด่านด้วยค่าที่วัดได้ แต่ขั้นที่ 4 บทบาทวิวัฒน์ แก้ด้วยการวัดไม่ได้ เป็นการตัดสินใจที่ย้อนกลับไม่ได้ ซึ่งเกี่ยวพันกับหน้าที่·อัตลักษณ์·การจ้างงานของคน
เมื่อ AI ดูดซับการผลิตจำนวนมาก ที่ของคนก็ย้ายจากการผลิตจำนวนมากไปสู่การตัดสินใจ·การตีความ·การตรวจสอบ ถ้าไม่วาดการย้ายนี้ไว้ล่วงหน้า การนำมาใช้ก็จะถูกรับรู้ว่าเป็น "การแย่งงานของฉัน" และความเห็นพ้องก็พังลง
| สายงาน | Before (ผลิตจำนวนมาก) | After (ตัดสินใจ·ตีความ·ตรวจสอบ) |
|---|---|---|
| นักออกแบบเนื้อหา | เขียนเมือง·NPC ด้วยมือเอง | ออกแบบเมตาดาตา + ตัดสินทิ้ง/นำมาใช้ (§6.2) |
| นักออกแบบ UX | จัดวาง HUD ด้วยมือ | ออกแบบ rulebook + ตัดสินกรณีคลุมเครือ (§14.1) |
| QA | ตรวจสอบด้วยมือ | ออกแบบด่าน + ดำเนินงาน lint |
| ผู้ปรับสมดุล (balancer) | คำนวณด้วยมือ | ตีความผลซิม + ตัดสินใจ |
การที่ตารางนี้จะเป็นคำสัญญาไม่ใช่คำขู่ ขั้นที่ 4 ต้องถูกตรึงเป็น decision atom ในหน่วยความจำทีมของพีซีบริษัท ในความเป็นจริง การตัดสินใจนำมาใช้จะถูกบันทึกพร้อมวันที่·หลักฐาน เช่น decisions/claude_role_transition_phase2 (2026-04-29, ยกระดับ Claude จาก passive trainee เป็น active partner) ถ้าการตัดสินใจเหลือไว้เพียงด้วยปากเปล่า ไตรมาสถัดไปก็จะกลายเป็น "ไม่เคยตกลงกันแบบนั้น" และที่ฐานของความเห็นพ้องนี้มี atom concepts/team_equal_decision_culture (วัฒนธรรมการตัดสินใจอย่างเท่าเทียมในทีม) อยู่ — คำสัญญาของทีมว่าจะจัดการการนำมาใช้ในรูปของความเห็นพ้อง ไม่ใช่การแจ้งฝ่ายเดียว ต้องถูกตรึงไว้เป็นถ้อยคำ ขั้นที่ 4 จึงจะเป็นความเห็นพ้องไม่ใช่การแจ้ง
ถ้ามองคุณค่าของระบบอัตโนมัติเพียงในแง่ "ประหยัดเวลา" ก็จะไหลไปสู่ข้อสรุปว่าต้องตัดคนออกในขั้นที่ 4 จึงวาง atom
concepts/automation_signal_value_over_time_savings(คุณค่าของระบบอัตโนมัติ = ไม่ใช่การประหยัดเวลา แต่คือการเปิดเผยสัญญาณ) ไว้ในหน่วยความจำทีม สิ่งที่ระบบอัตโนมัติแก้ไม่ใช่เวลาของคน แต่คือสัญญาณที่คนต้องดู ถ้อยคำเดียวนี้พลิกน้ำเสียงของรายงานการนำมาใช้จาก "การลดกำลังคน" ไปเป็น "บทบาทวิวัฒน์"
ต้นทุน LLM ตอนเริ่มนำมาใช้นั้นต่ำ แต่พอเครื่องมือเพิ่มก็จะสะสมมากขึ้น จึงตั้งเพดาน (cap) ต่อเดือนของแต่ละเครื่องมือไว้ก่อน แล้ววางกระบวนการแจ้งเตือน·ทบทวนเมื่อเกิน จำนวนเงินต่อเดือนที่เจาะจงจะต่างกันมากตามขนาดทีม·โมเดล·ปริมาณการเรียกใช้ จึงไม่ใส่ค่าสัมบูรณ์ลงในหนังสือเล่มนี้ — นั่นเป็นช่องว่างที่ต้องรับมาจากฝ่ายบัญชีแล้วเติม อย่างที่เห็นใน §19.3.2 เวลารายงาน สิ่งสำคัญไม่ใช่จำนวนเงิน แต่คือข้อเท็จจริงว่ามี โครงสร้างที่ตั้งเพดานไว้ และการเกินถูกรายงาน
การวัดผลให้บังคับเป็นรายไตรมาส สัญญาเป็น KPI เฉพาะสิ่งที่วัดได้เท่านั้น
| วัดได้ (สัญญา) | วิธีวัด |
|---|---|
| จำนวนสะสมของ atom·สกิล | นับไดเรกทอรี |
| จำนวนครั้งการป้อน JIT | จำนวนบรรทัดใน _injection_log.txt |
| อัตราการทิ้ง (ด่านผลิตจำนวนมาก) | นับการตรวจสอบ (วิธีของ §6.2.6) |
| การประหยัดเวลาทำงาน | เทียบ timestamp ของงานเดียวกันก่อน-หลัง (บันทึกเส้นฐานก่อน) |
บรรทัดสุดท้ายคือหัวใจ การจะรายงานการประหยัดเวลาอย่างซื่อสัตย์ ต้อง วัดเส้นฐานไว้ก่อนนำมาใช้ เหตุผลที่แท้จริงที่ "4.2 เท่า" พังในห้อง CEO วันนั้น คือไม่มีเส้นฐาน เพราะไม่ได้วัดเวลาของงานเดียวกันก่อนนำมาใช้ จึงไม่มีหลักฐานจะพูดว่าหลังนำมาใช้เวลาลดลง การวัดเริ่มต้นก่อนนำมาใช้ ไม่ใช่หลังนำมาใช้
คำว่า "วัดเส้นฐานก่อน" นั้นถูกต้องแต่เป็นนามธรรม การที่ผู้อนุมัติจะวัดในสภาพแวดล้อมของตนเองโดยตรงได้ ขั้นตอนต้องจับต้องได้ ตรงนี้ขอตอกตะปูข้อหนึ่งไว้ก่อน หนังสือเล่มนี้ไม่ได้ให้ค่าการประหยัดอย่าง "นำมาใช้แล้วเร็วขึ้น N เท่า" ตัวเลขคุณต้องวัดเองในสภาพแวดล้อมของคุณ หัวข้อนี้คือเรซิพีของการออกแบบการวัดนั้น และหัวข้อถัดไป (§19.3.8) คือตัวอย่างที่วัดงานเพียงหนึ่งงานในสภาพแวดล้อมของผู้เขียน แต่แม้แต่ค่านั้นก็ผูกไว้ว่า "ประมาณ·ยังไม่ได้ตรวจสอบ"
flowchart TB
A["1. ตรึงงาน 1 งาน
ซ้ำ·ขอบเขตชัด·เกิดบ่อย"] --> B["2. นิยามหน่วยวัด
จุดเริ่ม/จุดจบ · นิยามผลผลิต · 1 ครั้ง = อะไร"]
B --> C["3. บันทึก Before 3~5 ครั้ง
ไม่มี AI · นาฬิกาข้อมือ/timestamp"]
C --> D["4. บันทึก After 3~5 ครั้ง
หลังนำ AI มาใช้ · นิยามงานเดียวกัน"]
D --> E{เปรียบเทียบ}
E --> F["รายงานด้วยค่ามัธยฐาน
ระบุจำนวนตัวอย่าง·ความเบี่ยงเบนด้วย"]
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class A,B human;
class C,D,F data;
สิ่งที่แต่ละช่องของเรซิพีถามมีดังนี้
สุดท้ายเวลารายงาน ให้เขียน ค่ามัธยฐาน กับ จำนวนตัวอย่าง·ความเบี่ยงเบน ไปด้วย ไม่ใช่ค่าเฉลี่ย หนึ่งบรรทัดที่เขียนว่า "วัด 3 ครั้ง อิงค่ามัธยฐาน" จะช่วยให้ตัวเลขของคุณรอดในจุดเดียวกับที่ "4.2 เท่า" พังลง การไม่ปิดบังข้อเท็จจริงว่าตัวอย่างน้อย คือหัวใจของการรายงานอย่างซื่อสัตย์
การวัดเองก็เป็นงาน ถ้าจะวัดทุกงานก็จะเหนื่อยกับการวัดจนไม่ได้วัดอะไรเลย การเลือก แค่งานเดียว มาวัด คือจุดเริ่มต้นของ ลองทำดู ใน §19.3.12
คำเตือน — ตัวเลขทั้งหมดในหัวข้อนี้เป็นค่าประมาณ ไม่ใช่การวัดแบบควบคุม ตัวอย่างน้อย เงื่อนไขงานไม่ได้เหมือนกันทุกครั้ง และมีส่วนที่ปรับเส้นฐานด้วยการนึกย้อนภายหลัง ดังนั้นค่าด้านล่างเป็นเพียง ตัวอย่างโครงสร้าง ที่แสดงว่า "ตารางแบบนี้หน้าตาเป็นอย่างไร" เท่านั้น ห้ามนำไปอ้างเป็นหลักฐานการประหยัดของทีมคุณ คุณต้องวัดในสภาพแวดล้อมของคุณเองด้วยเรซิพีของ §19.3.7
งานที่ผู้เขียนเลือกคือ "เขียนเอกสารสคีมาของชีตข้อมูล 1 แผ่น 1 ฉบับ" (งานเดียวกับที่สกิล schema-doc ทำอัตโนมัติ) เพื่อให้เห็นเพียงว่าโครงสร้าง before/after เกิดขึ้นอย่างไร ตารางที่เติมด้วยค่าประมาณเป็นดังนี้
| รายการ | ค่า | ความน่าเชื่อถือ |
|---|---|---|
| นิยามงาน | ชีต 1 แผ่น ($สคีมา) → เอกสารสคีมามาร์กดาวน์ 1 ฉบับ จนผ่านการตรวจสอบ | นิยามแน่นอนแล้ว |
| เวลา Before (ประมาณ) | ราว 40 นาที/ฉบับ (อิงการนึกย้อน ไม่ได้บันทึก) | ต่ำ — ประมาณ |
| เวลา After (ประมาณ) | ราว 10 นาที/ฉบับ (เรียกสกิล + ตรวจสอบ บันทึกบางส่วน) | ต่ำ — ประมาณ |
| จำนวนตัวอย่าง | before ไม่ได้บันทึก / after ราว 3 ฉบับ | ไม่เพียงพอ |
| ข้อสรุป | เพียงทิศทาง: ดูเหมือนลดลง ยืนยันตัวคูณ·% ไม่ได้ | เพียงทิศทาง |
ส่วนที่ซื่อสัตย์ในตารางนี้ไม่ใช่ค่า แต่คือ ช่องความน่าเชื่อถือ ตัวเลข "ราว 40 นาที → ราว 10 นาที" ดูน่าเชื่อ แต่เพราะเขียนข้อเท็จจริงว่า before อิงการนึกย้อน·ไม่ได้บันทึก ไว้ในบรรทัดเดียวกัน ตารางนี้จึงตรงข้ามกับ "สไลด์ 4.2 เท่า" โดยสิ้นเชิง ถ้าจะนำตารางนี้ขึ้นเสนอ CEO บรรทัดข้อสรุปต้องมีเพียงหนึ่งเดียว "ทิศทางดูเหมือนไปทางลดลง แต่ไม่มีตัวอย่างที่จะยืนยัน จึงจะวัดให้ถูกต้องด้วยไพล็อต 1 งาน" นี่คือภาพของการนำท่าทีที่การปฏิเสธใน §19.3.2 สอนไว้มาใช้กับการวัด — สิ่งที่ไม่รู้ก็เขียนว่าไม่รู้
ตรงนี้การจัดการต้นทุนการดำเนินงานของ §19.3.2 ดำเนินต่อมาตามเดิม ในตัวอย่างนี้ก็เว้น operating_cost ว่างไว้ เพราะราคาต่อโทเค็น·ปริมาณการเรียกใช้·การเลือกโมเดล ต่างกันทุกเดือน และนั่นไม่ใช่ค่าที่ผู้เขียนจะประมาณ แต่คือค่าที่ฝ่ายบัญชีจะยืนยัน การเว้นช่องว่างให้เป็นช่องว่าง ซื่อสัตย์กว่าการเติมช่องว่างให้ดูน่าเชื่อ
# single_task_measure.example.yaml — ตัวอย่างโครงสร้าง (ค่าเป็นแบบประมาณ·ยังไม่ได้ตรวจสอบ)
task: "เขียนเอกสารสคีมา 1 ฉบับ (งานเป้าหมายของ schema-doc)"
before_minutes_est: 40 # อิงการนึกย้อน ไม่ได้บันทึก → ความน่าเชื่อถือต่ำ
after_minutes_est: 10 # บันทึกบางส่วน ตัวอย่างราว 3 ฉบับ → ความน่าเชื่อถือต่ำ
sample_before: null # ไม่ได้วัด (ซื่อสัตย์ด้วย null)
sample_after: 3
operating_cost_usd_month: null # ช่องว่างของฝ่ายบัญชี — อย่ากุขึ้น
conclusion: "เพียงทิศทาง: ดูเหมือนลดลง ยืนยันตัวคูณ/% ไม่ได้ ขอวัดซ้ำด้วยไพล็อต"
sample_before: null และ operating_cost_usd_month: null คือมโนธรรมของตัวอย่างนี้ แรงกระตุ้นที่อยากเปลี่ยน null เป็นตัวเลข — นั่นคือแรงกระตุ้นเดียวกับที่ AI เติมช่องว่างเป็น $4,500 ใน §19.3.2 และไม่ว่าคนหรือ AI ก็ต้องปฏิเสธเหมือนกัน
ด้านล่างคือเวิร์กชีตที่ผู้อนุมัติ (หรือลีดที่รับหน้าที่วัด) เติมเองในสภาพแวดล้อมของตน แล้วนำขึ้นเสนอผู้บริหาร หนังสือเล่มนี้ไม่เติมช่องว่างให้ — เพราะขณะที่เติม มันจะกลายเป็นของกุของผู้เขียน ไม่ใช่การวัดในสภาพแวดล้อมของคุณ การนำไปทั้งที่ยังว่างแล้ววัดเอง คือวิธีใช้ตารางนี้
| ช่อง | เขียนอะไร | ใครเติม | ตัวอย่าง (เพื่อโครงสร้าง ไม่ใช่ค่า) |
|---|---|---|---|
| งานที่วัด | งาน 1 งานที่ซ้ำ·ขอบเขตชัด | ลีด | "เขียนเอกสารสคีมา 1 ฉบับ" |
| นิยาม 1 ครั้ง | จุดเริ่ม / จุดจบ | ลีด | "เปิดไฟล์ / ผ่านการตรวจสอบ" |
| ค่ามัธยฐาน Before | วัดไม่มี AI 3\~5 ครั้ง | ผู้วัด | ______ นาที (ตัวอย่าง __ ครั้ง) |
| ค่ามัธยฐาน After | วัดหลังนำ AI มาใช้ 3\~5 ครั้ง | ผู้วัด | ______ นาที (ตัวอย่าง __ ครั้ง) |
| ตีความความต่าง | ไม่ใช่ตัวคูณ แต่ "ทิศทาง + จำนวนตัวอย่าง" | ลีด | "ทิศทางลดลง ระบุชัดว่าตัวอย่างไม่พอ" |
| operating_cost / เดือน | รวมโทเค็น·ค่าสมาชิก·โครงสร้างพื้นฐาน | ฝ่ายบัญชี | [ต้องให้ฝ่ายบัญชียืนยัน — เว้นว่าง] |
| รายการที่ยังไม่ได้วัด | ระบุสิ่งที่วัดไม่ได้อย่างซื่อสัตย์ | ลีด | "การเปลี่ยนของอัตราอุบัติเหตุ — ไม่มีเส้นฐาน" |
| คำขออนุมัติ | อะไร·ทำไม·ผลกระทบ·ทางเลือก·กำหนดเวลา | ผู้อำนวยการ (คน) | 5 ช่องของ §19.3.4 |
กฎของเวิร์กชีตนี้มีเพียงสามข้อ ข้อแรก ช่องตัวเลขให้เว้นว่างก่อนวัด ข้อสอง operating_cost ให้เว้นว่างจนกว่าฝ่ายบัญชีจะเติม ไม่มีใครเติมด้วยการประมาณ ข้อสาม เฉพาะช่องคำขออนุมัติเท่านั้นที่คนเขียนเอง (§19.3.4) ถ้าเติมตารางนี้แล้วนำไปเสนอ ในห้อง CEO ก็จะไม่มีคำถามว่า "ตัวเลขนั้นมาจากไหน" เพราะทุกตัวเลขคือสิ่งที่คุณวัดเอง หรือเหลือว่างไว้พร้อมบอกว่า "ยังไม่ได้วัด"
อย่าสั่งให้ AI เติมเวิร์กชีตนี้ AI จะเติมช่องว่างด้วยตัวเลขที่ดูน่าเชื่อเหมือนใน §19.3.2 ที่ของ AI มีถึงแค่ การรับผลการวัดมาเรียบเรียงเป็นประโยคสไลด์ เท่านั้น ไม่ใช่ที่สำหรับสร้างค่าการวัด
ที่ผ่านมาพูดถึงกรณีที่การนำมาใช้ไหลลื่น แต่สิ่งที่ PD กลัวที่สุดไม่ใช่ต้นทุนหรือความปลอดภัย แต่คือ แรงเสียดทานในการรับมาใช้ — การที่สมาชิกทีมปฏิเสธเครื่องมือ หรือลงไว้แล้วเงียบ ๆ เลิกใช้ หัวข้อนี้สรุปสัญญาณและการรับมือต่อแรงเสียดทานนั้นด้วยกรณีตัวอย่างที่ใช้ชื่อสมมติและทำให้เป็นกรณีทั่วไป ไม่มีตัวเลข เพราะสิ่งที่ PD ต้องตัดสินไม่ใช่ "การปฏิเสธจะเกิดหรือไม่" แต่คือ "จะจับสัญญาณการปฏิเสธแบบไหนเมื่อใดและจัดการอย่างไร"
ก่อนอื่นมีข้อตั้งที่ต้องตอกไว้ การปฏิเสธไม่ใช่ความล้มเหลว แต่เป็นสัญญาณ การที่เครื่องมือถูกปฏิเสธหมายความว่าเครื่องมือไม่เข้ากับที่ตรงนั้น หรือวิธีนำมาใช้เป็นการแจ้ง หรือข้ามขั้นการตรวจสอบไป ถ้ารับสัญญาณเป็นข้อมูลไม่ใช่อุบัติเหตุ แม้แต่การถอนออกก็กลายเป็นสินทรัพย์ของการนำมาใช้ครั้งถัดไป (กรณีทั้งหมดในหัวข้อนี้ตั้งอยู่บนข้อสมมติว่าจะถูกบันทึกไว้เหมือน decision atom ของ §19.3.5)
| สัญญาณการปฏิเสธ (สังเกตได้) | เหตุผลผิวเผิน | สาเหตุที่แท้จริง (กรณีชื่อสมมติ) | การรับมือ |
|---|---|---|---|
| ลงเครื่องมือไว้แต่ล็อกไม่มีการเรียกใช้ | "ยุ่งจนไม่ได้ลองใช้" | สมาชิก A: ถูกบังคับไว้ในที่ที่ไม่เข้ากับกระแสงานของตน | ปลดการบังคับ แล้วย้ายที่ไปยังงานซ้ำ 1 งานที่เขาทำบ่อย |
| ได้ผลลัพธ์มาแล้วก็ทำใหม่ด้วยมือ | "ไม่เชื่อใจเอาต์พุต AI" | สมาชิก B: เปิดการประยุกต์แบบก้าวหน้าก่อนโดยไม่ตรวจสอบเบื้องต้นจนเจออุบัติเหตุครั้งหนึ่ง | ย้อนกลับไปขั้นอนุรักษนิยม (คนตัดสินใจ·AI ตรวจสอบ) สร้างความเชื่อใจขึ้นใหม่ |
| เงียบหรือหลีกเลี่ยงเมื่อพูดเรื่องเครื่องมือ | (ไม่พูด) | สมาชิก C: บทบาทวิวัฒน์มาในรูปการแจ้ง รับว่าเป็น "แย่งงานฉัน" | วาดตารางบทบาท Before/After (§19.3.5) ร่วมกันแบบ 1:1 เปลี่ยนเป็นความเห็นพ้อง |
จุดร่วมของสามสัญญาณคือ ปรากฏในพฤติกรรมก่อนคำพูด สมาชิกที่เงียบโดยมีล็อกการเรียกใช้เป็น 0 อันตรายกว่าสมาชิกที่พูดว่า "ไม่ค่อยดี" จึงดูการรับมาใช้จากสัญญาณที่สังเกตได้อย่างล็อก JIT·จำนวนการเรียกใช้ (§19.3.3) ไม่ใช่จากการประเมินของคน การหาที่ซึ่งล็อกไม่มีการเรียกใช้ คือทางที่จับการปฏิเสธได้เร็วที่สุด
ถ้ารับมือแล้วสัญญาณยังไม่คลี่คลาย ให้ถอนเครื่องมือออก การถอนออกไม่ใช่ความพ่ายแพ้ แต่คือการทำงานปกติของด่านใน §19.3.1 ด่านจับได้ว่าไม่ถึงเกณฑ์ จึงไม่ส่งต่อไปขั้นถัดไป การตัดสินถอนออกให้ดูสามข้อต่อไปนี้
flowchart TD
R{สัญญาณปฏิเสธยังคงอยู่?} -->|ฟื้นหลังรับมือ| K["คงไว้: ย้อนกลับไป
ขั้นอนุรักษนิยมแล้วลองใหม่"]
R -->|ไม่ฟื้น| W{ด่านการถอนออก}
W --> W1["ภาระดำเนินงาน > ผล
(เวลาจัดการเกินที่ประหยัด)"]
W --> W2["อุบัติเหตุเกิดซ้ำ
(การตรวจสอบก็ไม่จับ)"]
W --> W3["ความเห็นพ้องของทีมพัง
(ถูกรับว่าเป็นการแจ้ง)"]
W1 --> X["ถอนออก: ปิดเครื่องมือ
บันทึกเหตุผลการถอนเป็น atom"]
W2 --> X
W3 --> X
X --> N["เป็นอินพุตของการนำมาใช้ครั้งถัดไป
(ไม่เข้ากับที่ใด เพราะอะไร)"]
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class R,W human;
class N data;
class K pass;
class X fail;
สิ่งที่ต้องเหลือไว้แน่นอนเมื่อถอนออกคือ การบันทึกเหตุผลการถอน ถ้าไม่ตรึง "ปิดเครื่องมือ X ที่ตรงไหน เพราะอะไร" เป็น decision atom ไตรมาสถัดไปก็จะลงเครื่องมือเดิมในที่เดิมอีก แล้วการปฏิเสธเดิมก็เกิดซ้ำ การถอนออกไม่ใช่การกระทำที่ปิด แต่คือการกระทำที่บันทึก
การรับมือที่ดีที่สุดคือการลดแรงเสียดทานก่อนที่การปฏิเสธจะเกิด ถ้าสาวกลับไปยังสาเหตุที่แท้จริงของกรณีข้างต้น ก็จะรวมไปที่ปัญหาของวิธีนำมาใช้
| สาเหตุของแรงเสียดทาน | การป้องกัน |
|---|---|
| บังคับเครื่องมือหลายตัวให้ทุกคนทีเดียว | เริ่มจากไพล็อตเครื่องมือ 1 ตัวด้วยอาสาสมัคร 1\~2 คน (§19.3.1) |
| เปิดการประยุกต์แบบก้าวหน้าก่อนโดยไม่ตรวจสอบ | ตรึงลำดับอนุรักษนิยม→ก้าวหน้า สร้างความเชื่อใจก่อน |
| ส่งบทบาทวิวัฒน์มาในรูปการแจ้ง | เห็นพ้อง 1:1 + atom วัฒนธรรมการตัดสินใจอย่างเท่าเทียม (§19.3.5) |
| ตรวจการรับมาใช้เหมือนเช็กชื่อบังคับ | สังเกตเงียบ ๆ ด้วยล็อกการเรียกใช้ ย้ายที่ที่ไม่ได้ใช้ให้ |
หัวใจคือมองการรับมาใช้เป็น การจัดที่ให้พอดี ไม่ใช่คำสั่ง ถ้าเครื่องมือเข้าไปอยู่ในที่ของงานซ้ำจริงของสมาชิกพอดี ก็ไม่มีเหตุให้ปฏิเสธ และถ้าฝืนยัดเข้าไปในที่ที่ไม่เข้ากัน เครื่องมือดีแค่ไหนล็อกก็จะเป็น 0 หลักฐานที่ PD จะตัดสินแรงเสียดทานการรับมาใช้ ไม่ใช่ความตั้งใจของสมาชิก แต่คือ "เครื่องมือถูกวางให้เข้ากับที่ของงานเขาหรือไม่"
เวิร์กชีตที่เติมช่องว่างประมาณกำลังคน·ค่าดำเนินงานในการนำมาใช้แยกตามขนาด อยู่ในภาคผนวก L (เวิร์กชีต TCO·การออนบอร์ดสำหรับการนำทีมมาใช้) ต่างหาก หลังจากลดแรงเสียดทานในการรับมาใช้ลงแล้ว ก็ทำให้การนำมาใช้นั้นกลายเป็นเอกสารอนุมัติว่ากินกำลังคน·ต้นทุนเท่าไรในระดับทีม ด้วยภาคผนวก L
| รูปแบบ | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| สไลด์ "ผลิตภาพ N เท่า" | พังเพราะไม่มีแหล่งที่มาในคำถามแรก | แทนด้วยสินทรัพย์ที่นับได้ (atom·สกิล·ล็อก) (§19.3.3) |
| นำเครื่องมือ 5 ตัวมาใช้ทีเดียว | ภาระดำเนินงานมาถึงก่อนผล | ด่านแบบขั้นอนุรักษนิยม→ก้าวหน้า (§19.3.1) |
| รายงานทั้งที่ AI เติมช่องว่างให้ | ตัวเลขกุรูปแบบสมบูรณ์จนผ่านการตรวจสอบ | พรอมต์ "ไม่มีหลักฐานให้แจ้ง" + การปฏิเสธ (§19.3.2) |
| ส่งรายงานฉบับเดียวกันให้ทุกผู้ฟัง | ไม่ถึงผู้ฟังคนใด | จัดเฟรมตามผู้ฟัง (§19.3.4) |
| แจ้งบทบาทวิวัฒน์ฝ่ายเดียว | การนำมาใช้ถูกรับว่าคุกคามอัตลักษณ์ | ตรึง decision atom + วัฒนธรรมการตัดสินใจอย่างเท่าเทียม (§19.3.5) |
| เริ่มวัดหลังนำมาใช้ | ไม่มีเส้นฐานจึงพิสูจน์การประหยัดไม่ได้ | บันทึกเส้นฐานก่อนนำมาใช้ (§19.3.6·§19.3.7) |
| รายงานค่าประมาณเป็นการยืนยัน | ปิดบังตัวอย่างที่น้อยจนพังในคำถามแรก | ระบุช่องความน่าเชื่อถือ·จำนวนตัวอย่าง รายงานเพียงทิศทาง (§19.3.8) |
| เติมช่องว่างของเวิร์กชีตด้วยการประมาณ | ของกุ operating_cost ทำลายความเชื่อถือในการอนุมัติ | คงเว้นว่างจนกว่าฝ่ายบัญชียืนยัน (§19.3.9) |
ข้อที่สามอันตรายที่สุด ตัวเลขกุไม่มีรอยให้เห็นว่าผิด เพราะรูปแบบสมบูรณ์แบบ ถ้าผู้ตรวจสอบเผลอเพียงครั้งเดียว มันก็ไปถึงห้อง CEO ตามนั้น การปฏิเสธหนึ่งครั้งของ §19.3.2 สกัดอุบัติเหตุนั้นได้
การประยุกต์นอกเกม คำถามของผู้บริหารที่ว่า "จ่ายค่าเครื่องมือ AI เดือนละเท่าไร แล้วดีขึ้นตรงไหน" พุ่งเข้ามาเหมือนกันในทุกแผนก และตัวเลขที่ไม่มีแหล่งที่มาอย่าง "ผลิตภาพ N เท่า" จะพังตั้งแต่คำถามแรก ผลควรรายงานด้วยสิ่งที่นับได้ซึ่งระบบทิ้งไว้จริง — จำนวนงานที่ทำเป็นอัตโนมัติ จำนวนเอกสารมาตรฐาน จำนวนการเรียกใช้ที่ปรากฏในล็อก — ไม่ใช่ตัวคูณที่ปรุงแต่ง และรายการที่วัดไม่ได้ก็เขียนอย่างซื่อสัตย์ว่า "ยังไม่ได้วัด" แบบนี้จะผ่านการอนุมัติได้ดีกว่า เช่น เมื่อแผนกบัญชีนำเครื่องมืออัตโนมัติมาใช้ ต้องวัดเวลาที่ใช้ของงานเดียวกันก่อนนำมาใช้ไว้เป็นเส้นฐานก่อน (นี่คือหัวใจ) แล้วเทียบกับหลังนำมาใช้ จึงจะพิสูจน์การประหยัดได้ ตัวการนำมาใช้เองก็อย่าเปิดทั้งหมดทีเดียว แต่ตรวจสอบในที่ที่ความเสี่ยงต่ำแล้วขยายเป็นขั้น ๆ ภาระดำเนินงานจึงจะไม่มาถึงก่อนผล
ถ้าทำคนเดียวก็แค่เท่านี้: ไม่ต้องมีระบบหน่วยความจำทีมก็ได้ ลองเลือกงาน 1 งานที่คุณทำด้วย AI เมื่อเร็ว ๆ นี้ แล้วสั่ง AI ว่า "ช่วยเรียบเรียงผลของงานนี้ แต่อย่าสร้างตัวเลขที่ไม่มีในข้อเท็จจริงที่ฉันให้เด็ดขาด สิ่งที่วัดไม่ได้ให้เขียนว่า 'ยังไม่ได้วัด'" จากนั้นหาบรรทัดที่มีตัวเลขไม่มีแหล่งที่มาในเอาต์พุตสักหนึ่งบรรทัด แล้วโต้กลับว่า "ตัวเลขนี้มาจากไหน ตอบไม่ได้ก็ลบทิ้ง" แล้วคุณจะเข้าใจกับตัวว่า AI กุช่องว่างขึ้นมาอย่างไร และจะปฏิเสธของกุนั้นอย่างไร นี่คือฉบับย่อของ §19.3.2
ถ้าเป็นทีม ให้เริ่มด้วยขั้นต่อไปนี้หนึ่งขั้น เลือกงาน AI ที่กำลังรันอยู่ตอนนี้ เพียงหนึ่งงาน แล้วบันทึกเส้นฐานก่อนนำมาใช้ (เวลาที่ใช้ปัจจุบันของงานเดียวกัน, ค่ามัธยฐาน 3\~5 ครั้ง) ก่อนด้วยเรซิพี 4 ขั้นของ §19.3.7 จากนั้นพิมพ์เวิร์กชีตของ §19.3.9 ออกมาทั้งที่ยังว่าง แล้วส่งคำถามหนึ่งบรรทัดไปยังฝ่ายบัญชีเรื่อง operating_cost แล้วเว้นว่างไว้ และรันแค่ขั้นที่ 1 (ป้อนบริบท) เป็นไพล็อต 1\~3 เดือน แล้วนับว่า atom·สกิลสะสมกี่ตัว การได้สินทรัพย์ที่นับได้หนึ่งบรรทัดและเส้นฐานหนึ่งบรรทัดมาก่อน แทนการเปิดเครื่องมือ 5 ตัวทีเดียว คือจุดเริ่มต้นที่แท้จริงของการโน้มน้าวผู้บริหาร
ถ้าทำคนเดียวการวัดก็เบา ๆ ได้: ไม่ใช่ทั้งเวิร์กชีต แต่ลองวัดเพียงสองช่อง — Before หนึ่งครั้ง, After หนึ่งครั้ง — เท่านั้น แล้วข้างค่านั้นต้องเขียนว่า "ตัวอย่าง 1 ครั้ง, ประมาณ" แน่นอน นิสัยการทำเครื่องหมายค่าที่วัดครั้งเดียวว่าเป็นค่าประมาณนั้น จะกลายเป็นกล้ามเนื้อที่ภายหลังสกัด "4.2 เท่า" ในการวัดระดับทีม
ส่วนที่ 19 พูดถึงสามด้านของลีด
| บท | หัวใจ |
|---|---|
| 19.1 | วิสัยทัศน์·โรดแมป และการมอบหมายอำนาจ — ระดับของการตัดสินใจและขอบเขตการมอบหมาย |
| 19.2 | ความขัดแย้ง·วัฒนธรรมทีม และการดำเนินการประชุม — ที่ซึ่งสร้างความเห็นพ้อง |
| 19.3 | กลยุทธ์การนำ AI มาใช้และการโน้มน้าวผู้บริหาร — การนำมาใช้แบบเป็นขั้น + ROI ที่วัดจริง |
หนึ่งบรรทัดที่ร้อยเรียงสามบทคือ งานของลีดไม่ใช่ "การตัดสินใจ" แต่คือ "การสร้างโครงสร้างที่การตัดสินใจถูกวัดและถูกเห็นพ้อง" การนำ AI มาใช้ก็ไม่ใช่ข้อยกเว้น เมื่อเดินจากแบบอนุรักษนิยมสู่แบบก้าวหน้าเป็นขั้น ๆ และดึงผลขึ้นมาจากล็อกของระบบโดยไม่ปรุงแต่ง การนำมาใช้ก็กลายเป็นสินทรัพย์ ไม่ใช่บรรยากาศ
ส่วนถัดไป (ส่วนที่ 20) ว่าด้วยการที่ด้านลีดนี้ถูกนำไปสร้างเป็นจริงด้วยเครื่องมือ·โครงสร้างพื้นฐานอย่างไร atom 304·สกิล 48·ล็อก JIT ที่ใช้เป็นหน่วยของ ROI ใน 19.3 จะเข้าไปสู่ภายในของระบบที่ดำเนินงานสิ่งเหล่านั้นในส่วนที่ 20
ในบทนี้ 'DD' หมายถึง Design Director (ผู้อำนวยการฝ่ายออกแบบ)
ผู้อ่านกลุ่มหลัก: ผู้อำนวยการหรือหัวหน้าทีมในทีมขนาดเล็กที่ต้องแบกบริบทการทำงานร่วมกันไว้คนเดียว (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านที่ทำงานคนเดียว/เป็นงานอดิเรก: §20.1.7 「ถ้าทำคนเดียว เอาแค่นี้พอ」
เช้าวันจันทร์ ในห้องประชุมเดียวกัน ผมเคยอธิบายการตัดสินใจเรื่องเดียวกันให้คนสามคนฟังถึงสามครั้ง คนหนึ่งผมบอกว่า "Cooldown ต้อง SVN update ก่อนแก้ไฟล์ xlsm" สองชั่วโมงต่อมาอีกคนหนึ่งเขียนทับไฟล์เดียวกันโดยไม่ได้ update จนเกิดความขัดแย้ง และช่วงบ่ายก็มีอีกคนถามเรื่องเดียวกัน ทั้งสามคนเป็นคนดีทั้งนั้น ปัญหาไม่ได้อยู่ที่พวกเขา แต่อยู่ที่การตัดสินใจนั้นอยู่ในหัวผมเพียงคนเดียว การที่ผู้อำนวยการคนหนึ่งในทีมขนาดกลางจะหมุนบริบทการทำงานร่วมกันของอีกสี่คน — ใครรู้กฎข้อไหน ใครมักทำพลาดเรื่องอะไร และการตัดสินใจไหนถูกสรุปไปแล้ว — ให้สม่ำเสมอด้วยความจำของมนุษย์นั้นเป็นไปไม่ได้ ผ่านไปแค่เดือนเดียว คำว่า "เรื่องนี้เราเคยตัดสินใจไปแล้วไม่ใช่หรือ?" ก็กินเวลาประชุมไปครึ่งหนึ่ง
บทนี้ว่าด้วยระบบที่ยุติปัญหานั้น สินทรัพย์หลักมีสองอย่าง อย่างแรกคือ decision card (การ์ดการตัดสินใจ) 304 ใบ(atom) ที่ทั้งทีมใช้ร่วมกัน อย่างที่สองคือ team_memory ของห้าคน ที่วางทับอยู่บนนั้น — เป็นที่เก็บบริบทแยกตามผู้ใช้ แบ่งเป็นตัวผม (leeminsoo) กับสมาชิกทีม A·B·C (นามสมมติ) และโฟลเดอร์ shared ตอนเริ่มเซสชัน Claude จะระบุด้วยตัวเองว่า "ตอนนี้ใครนั่งอยู่หน้าคีย์บอร์ด" แล้วเลือกสวมเฉพาะสไตล์การทำงานร่วมกันของคนนั้น เรื่องทั่วไปของหน่วยความจำการทำงานร่วมกันมีอยู่ในหนังสือเล่มอื่นแล้ว บทนี้จะโฟกัสเฉพาะ จุดที่ AI แตกแขนงและฉีดหน่วยความจำนั้นเข้ามาโดยอัตโนมัติ เท่านั้น
ตัวเลขทั้งหมดในบทนี้เป็นค่าที่วัดจริง ณ เวลาที่สำรวจรายการ (atom) เดือนพฤษภาคม 2026
หนังสือที่แก้ปัญหาหน่วยความจำการทำงานร่วมกันด้วย "วิกิที่ใช้ร่วมกัน" มีอยู่มาก ก็คือสร้างหน้าการตัดสินใจไว้ใน Notion แล้วทุกคนเข้ามาดูด้วยกัน พูดถูก แต่วิกิทำสองอย่างไม่ได้ มันจะถูกเห็นก็ต่อเมื่อมีคนป้อนเข้าไปเท่านั้น และจะถูกอ่านก็ต่อเมื่อมีคนไปค้นหาเท่านั้น กลางวงประชุมไม่มีใครลุกไปถามว่า "เรื่องนี้เขียนไว้ในวิกิหรือเปล่า?"
ดังนั้นเราจึงตรึงการตัดสินใจให้เป็น ไฟล์ระดับอะตอมที่ค้นหา อ้างอิง และฉีดเข้ามาโดยอัตโนมัติได้ เราเรียกสิ่งนี้ว่า atom atom หนึ่งใบ คือการตัดสินใจหนึ่งเรื่อง ชื่อไฟล์คือตัวระบุในตัวเอง จึงค้นเจอด้วย rg ได้ frontmatter เป็นมาตรฐาน สคริปต์จึงประมวลผลได้ และเนื้อหาสั้นจึงใส่เข้าบริบทได้ทั้งก้อน ใต้ workspace/team_memory/atoms/ ในพีซีของบริษัทมี atom แบบนี้สะสมอยู่ 304 ใบ
| โฟลเดอร์ | จำนวน | ลักษณะ |
|---|---|---|
rules/ |
304 | กฎป้องกันการเกิดซ้ำ (xlsm·SVN·เอกสาร·สกิล ฯลฯ) |
concepts/ |
19 | คำศัพท์โดเมนที่ปรากฏซ้ำในการทบทวน |
decisions/ |
26 | การตัดสินใจที่ระบุวันที่·ผู้เกี่ยวข้อง·เหตุผลชัดเจน |
feedback/ |
11 | ลูปแก้ไขการทำงานร่วมกัน (ความผิดพลาด → บทเรียน) |
rnd/ |
4 | ข้อสังเกตที่ยังไม่ยืนยัน ซึ่งอาจใช้ไม่ได้เมื่อมีการแพตช์เครื่องมือ |
รวมเป็น 304 ใบ ห้าโฟลเดอร์นี้คือ "ความจำระยะยาว" ของทีม ประเด็นสำคัญคือชื่อโฟลเดอร์เป็นตัวบอกระดับความน่าเชื่อถือของ atom ในตัวเอง rules/ คือกฎที่ผ่านการพิสูจน์จากการเกิดซ้ำหลายครั้ง ส่วน rnd/ คือข้อสังเกตชั่วคราวที่อาจถูกทิ้งเมื่อเวอร์ชัน UE เปลี่ยน ภายในหน่วยความจำเดียวกัน "สิ่งที่ยืนยันแล้ว" กับ "สมมติฐาน" ก็ถูกแยกด้วยโฟลเดอร์ จึงป้องกันเหตุที่สมาชิกใหม่เข้าใจผิดว่าวิธีเลี่ยงใน rnd/ เป็นกฎถาวรได้อย่างเป็นโครงสร้าง
นิยาม 5 คุณสมบัติของ atom (หลักหนึ่งการตัดสินใจ·การตั้งชื่อแบบชัดเจน·frontmatter มาตรฐาน·ระบุความสัมพันธ์·ตามรอยได้) กล่าวไว้แล้วในส่วนที่ 5 บทนี้ไม่ได้พูดถึงนิยาม แต่พูดถึง จุดที่ห้าคนร่วมกันบริหาร 304 ใบ
จะให้อ่านครบทั้ง 304 ใบทุกเซสชันคงไม่ได้ ดังนั้นจึงให้คะแนน score(น้ำหนัก) กับ atom แต่ละใบ แล้วแสดงเฉพาะใบที่คะแนนสูงโดยอัตโนมัติ atom_score.py คำนวณคะแนนจากความถี่การใช้·น้ำหนักที่กำหนดเอง·ความใหม่ ด้านล่างคือ score ที่วัดจริงของสิบอันดับแรกตามเกณฑ์การวัดเดือนพฤษภาคม 2026
| score | atom | บังคับอะไร |
|---|---|---|
| 356.53 | view_html_filename_convention |
ข้อกำหนดการตั้งชื่อ View_*.html (Phase/Status → Domain → Topic) |
| 349.26 | xlsm_svn_update_before_edit |
SVN update ก่อนแก้ xlsm + รักษาแถวเดิมไว้ |
| 341.03 | claude_role_transition_phase2 |
ยกระดับ Claude จาก passive trainee → active partner (การตัดสินใจ) |
| 340.26 | skill_audit_score |
วัดความถี่การใช้สกิลจากบันทึก SVN |
| 329.26 | docs_is_source_of_truth |
ให้ workspace/docs เป็นฉบับจริง |
| 326.84 | claudeskills_naming_separation |
แยกชื่อ ClaudeSkills ออกจากสกิลตัวละครในเกม |
| 324.36 | draft_doc_body_verify_before_skip |
ห้าม skip ด้วยตำแหน่งเพียงอย่างเดียว ต้อง grep เนื้อหาแล้วประเมิน |
| 309.43 | json_over_schema_doc_as_source_of_truth |
ผลลัพธ์ JSON จริงเป็นฉบับจริงเหนือเอกสาร schema |
| 294.93 | integrity_check_clickup_notify |
เมื่อตรวจสอบความสอดคล้องล้มเหลว ให้แจ้ง ClickUp ทันที |
| 293.26 | data_entry_schema_first |
ลำดับการป้อนข้อมูล ($schema → Enum → proto) |
เห็นเหตุที่ผมอธิบายไปสามครั้งในตอนต้นหรือไม่ — "SVN update ก่อนแก้ xlsm" นั่นคือ xlsm_svn_update_before_edit ซึ่งมี score 349.26 อยู่อันดับ 2 ของทั้งหมด การที่คะแนนสูงหมายความว่ามันถูกอ้างอิงบ่อยในระดับนั้น และเป็นกฎที่คนมักทำผิดในระดับเดียวกัน ผมไม่ต้องพูดด้วยปากซ้ำสามครั้งอีกต่อไป สิบอันดับ score แรกถูกฉีดเข้าพื้นที่ <!-- BEGIN_TEAM_HOT_AUTO --> ของ CLAUDE.md โดยอัตโนมัติ จึงปรากฏบนหน้าจอแรกไม่ว่าใครจะเปิดเซสชันจากโฟลเดอร์ไหนก็ตาม
ถ้าหยุดแค่นี้ก็เป็นเพียง "การปักหมุดกฎที่ดูบ่อย" ความแตกต่างที่แท้จริงคือ score ไม่ได้มาจากมือคน แต่มาจากการที่ ระบบวัดตัวมันเอง แล้วให้คะแนน
flowchart LR
A["บันทึกการทบทวน·เซสชัน
(ความถี่การอ้างอิง)"] --> B["atom_score.py
คำนวณน้ำหนัก"]
B --> C["_scores_latest.json
แคชคะแนนล่าสุด"]
C --> D["claude_md_regen.py"]
D --> E["CLAUDE.md
BEGIN_TEAM_HOT_AUTO
ฉีดสิบอันดับแรกอัตโนมัติ"]
C --> F["_jit_manifest.json
hot atom 221 ใบ
(score≥20 OR weight≥4)"]
F --> G["JIT injection
จับคู่กับข้อความที่ป้อนระหว่างเซสชัน"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class B,D,G code;
class A,C,E,F data;
ลูปนี้ปิดวงในตัวเอง ยิ่ง atom ถูกอ้างอิงบ่อยในการทบทวน score ก็ยิ่งสูง พอ score สูง มันก็ยิ่งปรากฏชัดทั้งบนหัว CLAUDE.md และใน JIT manifest พอปรากฏชัดก็ยิ่งถูกอ้างอิงอีก เป็นโครงสร้างที่การตัดสินใจซึ่งใช้บ่อยจะลอยขึ้นด้านบน ด้วยตัวมันเอง ในทางกลับกัน atom ที่ไม่ถูกอ้างอิงเลยตลอด 6 เดือนจะมี score จมลงและค่อย ๆ หายไปจากสายตาเอง คนไม่ต้องมานั่งตัดสินว่า "อันนี้ไม่ใช้แล้ว เอาลงเถอะ"
score กำหนด "สิ่งที่เห็นเสมอ" ส่วนการฉีด JIT (Just-In-Time) ดึง "สิ่งที่ตรงกับคำที่เพิ่งพูด" เข้ามา ขณะที่ผู้ใช้ป้อนพรอมต์ hook จะเทียบข้อความนั้นกับ regex ใน atom manifest แล้วแทรก atom ที่เกี่ยวข้องเข้าไปในบริบท
ตรรกะหลักของ hook นี้ทำตามรูปแบบของ inject_atom.py ในพีซีของบริษัทตรง ๆ ด้านล่างคือส่วนหลักจริงของ inject_memory.py ซึ่งเขียนใหม่ด้วยรูปแบบเดียวกันสำหรับพีซีส่วนตัว — เรียงตาม score จากมากไปน้อย → จับคู่ regex → สูงสุด 3 ใบ → truncate ที่ 6000 ตัวอักษร และไม่ว่าจะเกิดอะไรขึ้นก็ exit 0
# จัดเรียงตาม score จากมากไปน้อยแล้วจับคู่
atoms_sorted = sorted(atoms, key=lambda a: a.get("score", 0), reverse=True)
matches = []
for atom in atoms_sorted:
if len(matches) >= max_matches: # max_matches = 3
break
try:
if re.search(atom["regex"], prompt, re.IGNORECASE):
matches.append(atom)
except re.error:
continue # regex ที่ผิดพลาดให้ข้ามแล้วทำต่อ
if not matches:
emit_empty() # ถ้าไม่มีการจับคู่ ให้ตอบว่าง (ปกติ)
return
chunks = []
for atom in matches:
body = atom_path.read_text(encoding="utf-8")
if len(body) > max_body: # max_body = 6000
body = body[:max_body] + "\n\n[...truncated]\n"
chunks.append(f"\n\n=== [JIT Inject] {name} (score {score}) ===\n\n{body}\n...")
ประเด็นสำคัญคือการออกแบบนี้อนุรักษ์นิยม ถ้าไม่มีการจับคู่ ก็ตอบว่างแล้วจบ (ปกติ) ถ้า regex เสีย ก็ข้ามเฉพาะ atom ใบนั้นแล้วหมุนต่อ ถ้าเนื้อหาเกิน 6000 ตัวอักษร ก็ตัด และ hook ทั้งก้อนจะจบด้วย exit 0 ไม่ว่าจะเกิดข้อยกเว้นใด — แม้การฉีดหน่วยความจำจะล้มเหลว ขั้นตอนการทำงานของผู้ใช้ก็จะไม่หยุดชะงักเด็ดขาด "ถ้ามีก็ช่วย ถ้าไม่มีหรือเสียก็ถอยออกไปเงียบ ๆ" คือหลักข้อที่ 1 ของระบบนี้
ถึงตรงนี้ถ้าเป็น atom (ความจำระยะยาว) ต่อไปนี้คือ team_memory ของห้าคน (ความจำระยะสั้น·การทำงานร่วมกันแยกตามคน) เราจะตามรอยจุดเริ่มต้นของเซสชันจริงหนึ่งครั้งจนจบในรูปแบบ ป้อน → ทำงาน → ผลลัพธ์ เวทีคือพีซีของบริษัท สถานการณ์คือสมาชิกทีม C (นามสมมติ, โฟลเดอร์ 팀원 C/) เปิดโฟลเดอร์งานของโปรเจกต์ A ที่โต๊ะตัวเอง
สมาชิกทีม C เริ่มงานแบบไม่มีอะไรพิเศษ
มาทบทวนต่อจากร่างระบบ targeting ที่ตกลงกันเมื่อวานกัน
ไม่ได้เขียนสักคำว่าตัวเองเป็นใคร ต้องทำตามกฎข้อไหน ไม่จำเป็นต้องเขียน
ตามขั้นตอน "การระบุอัตโนมัติเมื่อเริ่มเซสชัน" ใน CLAUDE.md ก่อนที่ Claude จะสร้างคำตอบ มันจะหาให้ได้ก่อนว่าใครอยู่หน้าคีย์บอร์ด คำสั่งที่รันมีสามคำสั่ง
svn info --show-item last-changed-author # author ของคอมมิตล่าสุดในสำเนางาน SVN
whoami # บัญชีล็อกอินของ OS
# และพาธงานปัจจุบัน (พีซีเครื่องไหน โฟลเดอร์ผู้ใช้ของใคร)
สัญญาณทั้งสามถูกเทียบกับการแมปใน team_memory/users.md แล้วยืนยันว่าเป็น 팀원 C จากนั้น Claude จะโหลด เฉพาะโฟลเดอร์ของผู้ใช้คนนั้นใบเดียว
team_memory/
├── atoms/ ← ใช้ร่วมกัน (304 ใบ, score อันดับต้นอยู่ใน CLAUDE.md แล้ว)
├── leeminsoo/ context.md ← (ไม่โหลด — ไม่ใช่ผม)
├── teammate_a/ context.md ← (ไม่โหลด)
├── teammate_b/ context.md ← (ไม่โหลด)
├── teammate_c/ context.md ← ★ โหลดเฉพาะอันนี้
│ notes.md ← ★ สำหรับบันทึกสิ่งที่ตนเองค้นพบ
│ retro/
└── shared/ conventions.md commit_rules.md ... ← อ้างอิงข้อกำหนดเท่านั้น ห้ามแก้ไข
สไตล์การทำงานร่วมกันที่อยู่ใน 팀원 C/context.md จะถูกใช้กับเซสชันนี้เท่านั้น คำตอบของ Claude จึงเริ่มต้นแบบนี้
ระบุได้ว่าเป็นคุณ팀원 C (svn author: teammate_c / ตรวจสอบพาธแล้ว)
โหลดสไตล์การทำงานร่วมกันจาก teammate_c/context.md เสร็จแล้ว
จะทบทวนต่อจากงานเมื่อวาน (Draft_Targeting_System_v0.1.md)
decision card ที่เกี่ยวข้องซึ่งถูกฉีดเข้ามา (JIT, 3 ใบ):
- docs_is_source_of_truth (329.26)
- draft_doc_body_verify_before_skip (324.36) — ห้าม skip ด้วยตำแหน่งเพียงอย่างเดียว ต้อง grep เนื้อหาแล้วประเมิน
- decision_summary_not_clickup_mirror
จะตรวจสอบเนื้อหาด้วย grep ก่อนแล้วจึงเริ่มทบทวน สิ่งที่พบระหว่างทบทวน
จะบันทึกไว้ใน teammate_c/notes.md เท่านั้น และจะไม่แก้ไข shared/ โดยตรง
ตรงนี้สิ่งที่ผม (ในฐานะผู้อำนวยการ หรือในฐานะสมาชิกทีม C เอง) ตรวจสอบมีสองอย่าง การระบุถูกต้องหรือไม่ และมันรักษาขอบเขตหรือไม่ ในข้อเสนอถัดไปของ Claude เราจับการละเมิดขอบเขตได้หนึ่งครั้ง
[ข้อเสนอของ Claude]
จากการทบทวนพบความขัดแย้งของกฎการตั้งชื่อ จะให้แก้ไขหัวข้อคำศัพท์ targeting
ใน shared/conventions.md เลยไหม?
[การปฏิเสธของมนุษย์]
ไม่ shared/ จะแก้ไขได้หลังจากตกลงกันในการทบทวนเท่านั้น ตอนนี้ให้บันทึกไว้ใน
teammate_c/notes.md แค่ว่า "shared/conventions.md คำศัพท์ targeting ขัดแย้ง — วาระการทบทวน" เท่านั้น
[ผลลัพธ์หลัง Claude ขอใหม่]
เข้าใจแล้ว ยกเลิกการแก้ไข shared/ โดยตรง
บันทึกไว้ใน teammate_c/notes.md เป็นวาระการทบทวนแล้ว:
- 2026-06-06 conventions.md คำศัพท์ targeting 'lock-on' vs 'target-lock' ขัดแย้ง
→ ต้องตกลงกันในการทบทวนทีมครั้งถัดไป (พักการแก้ไข shared)
นี่คือกลไกความปลอดภัยของการบริหารห้าคน ผู้ใช้แต่ละคนเขียนได้ เฉพาะใน notes.md ของตัวเอง เท่านั้น แตะโฟลเดอร์ของคนอื่นและ shared โดยตรงไม่ได้ shared จะเปลี่ยนได้หลังจากตกลงกันในการทบทวนเท่านั้น ดังนั้นถึงสี่คนจะทำงานบนหน่วยความจำเดียวกัน ก็ไม่เขียนทับบริบทของกันและกัน สิ่งที่ค้นพบจะรวมไว้ในโน้ตส่วนตัวก่อน แล้วต้องผ่านด่านที่ชื่อว่าการทบทวนเท่านั้นจึงจะถูกยกระดับขึ้นเป็นข้อกำหนดที่ทีมใช้ร่วมกัน
เราจะดูเส้นทางทั้งหมดที่เซสชันหนึ่งหมุนไปในหน้าเดียว เป็นหนึ่งรอบที่เริ่มจากการระบุตัวตน และจบที่การตรึงเป็นกฎในการทบทวน
flowchart TD
S["เริ่มเซสชัน"] --> ID["ระบุผู้ใช้อัตโนมัติ
svn info + whoami + พาธ"]
ID --> CTX["โหลดเฉพาะ context.md ของผู้ใช้คนนั้น
(ไม่โหลดโฟลเดอร์คนอื่น)"]
CTX --> STYLE["ใช้สไตล์การทำงานร่วมกันอัตโนมัติ"]
STYLE --> HOT["ฉีด CLAUDE.md Hot atom 10 ใบ +
JIT จับคู่ atom สูงสุด 3 ใบ"]
HOT --> WORK["ทำงาน"]
WORK --> NOTE["สิ่งที่พบ → บันทึกใน notes.md ของตนเอง
(ห้ามแก้ shared·คนอื่นโดยตรง)"]
NOTE --> RETRO["การทบทวน: ตรึงเป็นกฎใน retro/YYYY-MM-DD.md"]
RETRO --> GATE{"ต้องเปลี่ยน
ข้อกำหนด shared ไหม?"}
GATE -->|"ตกลงในการทบทวนแล้ว O"| SHARED["อัปเดต shared/ + สกัด atom ใหม่"]
GATE -->|"โน้ตส่วนตัว"| COMMIT["SVN commit (retro ส่วนตัว)"]
SHARED --> COMMIT
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class ID,HOT code;
class STYLE ai;
class GATE human;
class CTX,NOTE,RETRO,SHARED,COMMIT data;
จุดแยกที่มุมขวาล่างคือหัวใจของระบบนี้ สิ่งที่บุคคลค้นพบจะไหลไปยัง notes ส่วนตัว ส่วนข้อกำหนด·atom ใหม่ที่กระทบทั้งทีมจะขึ้นไปยัง shared ก็ต่อเมื่อผ่านด่านการทบทวนแล้วเท่านั้น เหตุผลที่คนเดียวหมุนงานของห้าคนได้โดยไม่เกิดความขัดแย้งอยู่ที่ด่านเดียวนี้ และขั้นสุดท้ายต้องเป็น SVN commit เสมอ — เพราะสิ่งที่ค้นพบแต่ไม่ได้ตรึงเป็นกฎ จะกลับเข้าไปอยู่ในหัวอีกครั้งในเซสชันถัดไป
นี่คือกับระเบิดที่เหยียบจริงระหว่างหมุน team_memory ของห้าคน
| ความล้มเหลว | อาการ | วิธีแก้ |
|---|---|---|
| ระบุล้มเหลว | svn author เป็นบัญชีร่วม จึงระบุผู้ใช้ไม่ได้ | แมปสัญญาณหลายอย่าง (พาธ·บัญชี) ใน users.md ถ้าระบุไม่ได้ให้ถาม |
| แก้ shared โดยพลการ | Claude แก้ข้อกำหนดร่วมด้วยความหวังดี | atom "shared แก้ได้หลังตกลงในการทบทวนเท่านั้น" + รูปแบบการปฏิเสธใน worked transcript |
| ไม่ commit notes | สิ่งที่พบเหลือแค่ในเครื่องแล้วระเหยไปในเซสชันถัดไป | บังคับ SVN commit ตอนจบการทบทวน (feedback-svn-zero-red) |
| Hot atom ค้าง | score หยุดนิ่ง กฎเก่าถูกปักไว้บนสุด | รัน atom_score.py เป็นรอบ → อัปเดต _scores_latest.json |
| เข้าใจ rnd ผิดเป็นกฎ | สมาชิกใหม่ใช้วิธีเลี่ยงชั่วคราวเหมือนกฎถาวร | แยกโฟลเดอร์ rnd/ + ระบุเงื่อนไขการใช้ไม่ได้ใน frontmatter |
ความล้มเหลวที่แพงที่สุดตรงนี้คือบรรทัดที่สอง "แก้ shared โดยพลการ" AI มีสัญชาตญาณอยากช่วยอย่างแรงกล้า พอพบความขัดแย้งก็จะลงมือแก้ทันที การปฏิเสธใน worked transcript ของ §20.1.4 ต้องถูกตรึงเป็น atom ไม่ใช่แค่การแก้ครั้งเดียว จึงจะขีดเส้นเดิมในเซสชันของผู้ใช้คนอื่นในครั้งถัดไปได้
ถึงไม่มีทีม 80% ของโครงสร้างนี้คนเดียวก็ใช้ได้ตรง ๆ แค่ลดผู้ใช้ห้าคนให้เหลือโฟลเดอร์เดียวก็พอ
rules/(ยืนยันแล้ว) กับ rnd/(สมมติฐาน) แค่ตรึงกฎที่ทำผิดบ่อย 5–10 ข้อให้เป็น atom คำว่า "เคยตัดสินใจไปแล้วไม่ใช่หรือ?" ก็หายไปnotes.md ไฟล์เดียว ขอแค่รักษาด่านการทบทวนไว้ — โน้ตด้นสดให้ลงใน notes ส่วนกฎที่ยืนยันแล้วให้ผ่านการทบทวนแล้วขึ้นไปยัง rules/แก่นคือ "การย้ายการตัดสินใจจากในหัวไปเป็นไฟล์" และไม่ว่าจะห้าคนหรือคนเดียว การทำนั้นก็เหมือนกัน
ลองตรึงกฎที่ทำผิดบ่อยหนึ่งข้อให้เป็น atom แล้วทำให้มันถูกฉีดเข้ามาอัตโนมัติด้วย JIT ดู
atoms/rules/ แล้วเขียนกฎที่อธิบายซ้ำบ่อยที่สุด 1 ข้อเป็นไฟล์ เช่น atoms/rules/xlsm_svn_update_before_edit.md{"name":..., "regex":"xlsm|쿨타임", "score":100, "path":...} ลงใน JIT manifest แล้วป้อน "쿨타임 수정" ลงในพรอมต์ ตรวจดูว่า atom นั้นถูกบันทึกเป็น hit ใน _injection_log.txt หรือไม่ถ้าถูกบันทึก กฎนั้นก็อยู่ในระบบแล้ว ไม่ใช่อยู่ในหัวคุณอีกต่อไป
ราวเที่ยงวันพุธ สมาชิกทีม B ส่งข้อความมาทางแชตภายในทีม "ค่า Cooldown ของการต่อสู้ที่ผู้อำนวยการกำหนดไว้ที่ 0.8 วินาทีเมื่อสัปดาห์ที่แล้ว แต่ในโน้ตของผมเขียนไว้ว่า 0.6 วินาที อันไหนถูกครับ?" ผมชะงักไปครู่หนึ่ง 0.8 วินาทีคือการตัดสินใจร่วมกัน ส่วน 0.6 วินาทีคือค่าที่สมาชิกทีม B ลองรันไว้ชั่วคราวในบิลด์ทดสอบของตัวเอง ทั้งสองค่าถูกเขียนไว้ใน "หน่วยความจำ" เหมือนกัน ปัญหาคือทั้งสองค่ามันปนกันอยู่ในช่องเดียวกัน สมาชิกทีม B เข้าใจผิดว่าค่าทดลองของตัวเองคือการตัดสินใจของบริษัท และเกือบจะอัปเดตชีตข้อมูลด้วยค่าที่ผิด
เหตุนี้ไม่ได้เกิดเพราะหน่วยความจำไม่มีข้อมูล ตรงกันข้าม มันเกิดเพราะข้อมูลสะสมไว้ดีเกินไป แต่ไม่มีเส้นแบ่งว่าช่องไหนคือช่องที่ใช้ร่วมกันและช่องไหนคือช่องส่วนตัว ถ้าใน §20.1 ผมปูจุดขายว่าคนห้าคนมองเห็นข้อเท็จจริงเดียวกัน (shared atom) บทนี้ก็เป็นด้านตรงข้าม — เรื่องที่ว่า คนห้าคนต่างมีช่องของตัวเองแยกกัน เป็นตู้เก็บเอกสารตู้เดียวกัน แต่ช่องมีสองชนิด และถ้าไม่บังคับสองชนิดนั้นด้วยเครื่องมือ เหตุการณ์ 0.6 วินาทีข้างต้นจะต้องเกิดขึ้นแน่นอน
team_memory/ ของโปรเจกต์ A แบ่งเป็นช่องของคนห้าคน ได้แก่ ตัวผมเอง (leeminsoo) สมาชิกทีม A สมาชิกทีม B สมาชิกทีม C และ shared สี่ช่องแรกคือช่องส่วนตัวแยกตามผู้ใช้ ส่วนช่องสุดท้ายคือช่องที่ใช้ร่วมกันซึ่งทุกคนเปิดได้
ช่องส่วนตัวสี่ช่องผมระบายด้วยสีน้ำเงิน ส่วนช่องที่ใช้ร่วมกันหนึ่งช่องระบายด้วยสีส้ม เหตุที่สีต่างกันก็เพราะกฎการเข้าถึงต่างกัน ช่องสีน้ำเงินเปิดได้เฉพาะเจ้าของและผู้อำนวยการ ส่วนช่องสีส้มเปิดได้ทุกคน เหตุการณ์ 0.6 วินาทีเกิดขึ้นเพราะสมาชิกทีม B นำค่าทดลองที่ควรเขียนลงในช่องสีน้ำเงินของตัวเอง ไปเรียกรวม ๆ ว่า "หน่วยความจำ" โดยไม่แยกสี แล้วปฏิบัติต่อมันเหมือนเป็นการตัดสินใจร่วมกัน หากแบ่งช่องในเชิงกายภาพ — กล่าวคือแบ่งไดเรกทอรี — อย่างน้อยก็มีเบาะแสให้แยกแยะสองสิ่งนี้ได้จากที่ว่าเขียนลงไว้ตรงไหน
ประเด็นสำคัญตรงนี้ไม่ใช่ที่ว่ามีโฟลเดอร์สองโฟลเดอร์ แต่อยู่ที่ว่า กฎติดตามมาพร้อมกับแต่ละช่อง สิ่งที่อยู่ใน shared/ คือการตัดสินใจของบริษัทและใคร ๆ ก็อ่านได้ สิ่งที่อยู่ใน สมาชิกทีม B/ คือบริบทการทำงานของคนคนนั้น และมีแค่เจ้าตัวกับผมที่อ่านได้ แม้จะเป็น 0.6 วินาทีเหมือนกัน แต่มันอยู่ในช่องไหนก็เป็นตัวกำหนดว่ามันคือ "กำลังทดลอง" หรือ "ตัดสินใจแล้ว"
เมื่อเปิดช่องของผู้ใช้แต่ละคน จะเห็นไฟล์สองไฟล์ คือ context.md และ notes.md ชื่อเรียบง่ายแต่บทบาทตรงกันข้าม
context.md เขียนว่า ตอนนี้คนคนนั้นคือใคร บทบาท ระบบที่รับผิดชอบ งานที่กำลังดำเนินอยู่ และสไตล์การทำงาน ค่อนข้างเสถียร และเป็นไฟล์ที่ผมในฐานะผู้อำนวยการเปิดดูห้านาทีก่อนการพูดคุยแบบ 1:1 เมื่อเปิด context.md ของสมาชิกทีม A จะเห็นข้อความทำนองว่า "รับผิดชอบระบบการต่อสู้ ปัจจุบันกำลังปรับสมดุล Cooldown ของสกิล เป็นคนที่ขอหลักฐานเชิงข้อมูลก่อน" ถ้าเข้าไปคุย 1:1 โดยไม่ดูสิ่งนี้ ก็จะเสียสิบนาทีแรกไปกับคำถาม "ช่วงนี้ทำอะไรอยู่บ้าง?"
notes.md เขียนว่า ตอนนี้คนคนนั้นกำลังเจออะไรอยู่ ค่าทดลองของแต่ละวัน จุดที่ติดขัด การตัดสินใจเล็ก ๆ บันทึกความผิดพลาด และโน้ตการหารือกับสมาชิกคนอื่น มันมีความผันผวนสูงและถูกอัปเดตบ่อย 0.6 วินาทีของสมาชิกทีม B เดิมทีควรเข้ามาอยู่ตรงนี้ เช่น "ทดสอบที่ 0.6 วินาที เร็วเกินจนอินพุตถูกดันตกหล่น — ตัดสินใจทำตามค่าร่วม 0.8 วินาที"
เหตุที่แยกสองไฟล์นี้ก็เพราะรอบการอัปเดตต่างกัน context.md แก้ไขไตรมาสละครั้งก็พอ แต่ notes.md สะสมขึ้นทุกวัน ถ้าปนกันไว้ ข้อมูลที่เสถียรจะถูกกลบด้วยสัญญาณรบกวนรายวัน หากทำงานคนเดียว การแยกแบบนี้อาจดูเกินจำเป็น — กรณีนั้นใช้แค่ notes.md ไฟล์เดียว แล้วเก็บ context.md ไว้ในหัวก็ได้ แต่แม้คนจะเพิ่มเป็นแค่สองคน การที่อ่าน context.md ของอีกฝ่ายได้ภายในห้านาทีเพื่อเตรียมการคุย 1:1 ก็สร้างความต่างได้มาก
แบ่งช่องส่วนตัวกับช่องที่ใช้ร่วมกันแล้วก็ยังไม่จบ สิ่งที่ยุ่งยากที่สุดคือ เนื้อหาบางอย่างในช่องส่วนตัวต้องเลื่อนขึ้นไปสู่ช่องที่ใช้ร่วมกัน สมมติว่าสมาชิกทีม C เขียนความผิดพลาดที่ว่า "ถ้าลำดับ enum คลาดเคลื่อนตอน import ชีตข้อมูล มันจะพังแบบเงียบ ๆ ที่ runtime" ลงใน notes.md ของตัวเอง นี่คือบันทึกส่วนตัวของคนคนนั้น แต่ถ้าทั้งทีมรู้ก็จะป้องกันความผิดพลาดเดียวกันได้ ทว่าจะแชร์ notes.md ส่วนตัวทั้งหมดก็ไม่ได้ — ในนั้นปนเปด้วยสไตล์การทำงาน อารมณ์ตอนติดขัด และความขัดแย้งกับสมาชิกคนอื่น
ดังนั้นระหว่างส่วนตัว → ใช้ร่วมกัน จึงต้องมี ด่าน การทบทวนคือด่านนั้น เมื่อเขียนการทบทวน ก็กรองครั้งหนึ่งว่า "ในสิ่งที่ฉันเจอสัปดาห์นี้ มีอะไรบ้างที่ทีมควรรู้" แล้วเลื่อนขั้นเฉพาะสิ่งที่กรองได้ให้เป็น atom ใน shared/ ลำดับเป็นดังนี้
flowchart TD
A["teammate_c notes.md
(ช่องส่วนตัว·อัปเดตทุกวัน)"] --> B{"การทบทวนรายสัปดาห์
ด่านเลื่อนขั้น"}
B -->|"เป็นประโยชน์ต่อทีม + ลบข้อมูลส่วนบุคคล"| C["ตรวจสอบการทำให้ไม่ระบุตัวตน"]
B -->|"บริบทส่วนตัว·อารมณ์·ความขัดแย้ง"| D["คงไว้ในช่องส่วนตัว"]
C --> E["เลื่อนขั้นเป็น atom ใน shared/
(ทุกคน read)"]
C -->|"ชื่อจริง·ข้อมูลอ่อนไหวยังหลงเหลือ"| D
E --> F["อ้างอิงอัตโนมัติในการถกการตัดสินใจของเกม
ผ่านการฉีด JIT"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class F code;
class B,C human;
class A,D data;
class E pass;
เกณฑ์การตัดสินของด่านมีสองข้อ ข้อแรก เป็นประโยชน์ต่อทีมหรือไม่ ไม่ใช่รสนิยมส่วนตัวหรือสภาพร่างกายในวันนั้น ข้อสอง ข้อมูลส่วนบุคคลถูกลบออกหรือไม่ ไม่ใช่ "สมาชิกทีม C ทำผิดที่ enum อีกแล้ว" แต่เหลือไว้แค่ข้อเท็จจริงว่า "เพิ่มการตรวจสอบลำดับ enum ตอน import ชีตข้อมูลกันเถอะ" เฉพาะสิ่งที่ผ่านสองด่านนี้เท่านั้นที่ไปสู่ shared/ สิ่งที่ผ่านไม่ได้ก็คงไว้ในช่องส่วนตัวตามเดิม
หากไม่มีด่านนี้ ก็จะพังในทางใดทางหนึ่งจากสองทาง ถ้าด่านหลวมเกินไป ข้อมูลส่วนบุคคลจะรั่วไปสู่ช่องที่ใช้ร่วมกัน เกิดเหตุที่ตรงข้ามกับ 0.6 วินาที — คือโน้ตส่วนตัวถูกเปิดเผยต่อทุกคน ถ้าไม่มีด่านเลย บทเรียน enum ของสมาชิกทีม C ก็จะถูกขังอยู่ในช่องของคนคนนั้น และสมาชิกทีม A ก็จะทำผิดเหมือนกันซ้ำ การใช้การทบทวนเป็นด่านทำให้แยกงานสองอย่างที่ตรงข้ามกันโดยสิ้นเชิง — "การรั่วไหลของข้อมูลส่วนบุคคล" กับ "การแปลงเป็นสินทรัพย์การเรียนรู้" — ออกจากกันได้ในการกระทำเดียว
พรอมต์การเลื่อนขั้นที่ผมใช้จริงนั้นสั้น หลังการทบทวนเสร็จ ผมโยนคำสั่งแบบนี้ลงไป
"เลือกเฉพาะรายการใน notes.md ของฉันสัปดาห์นี้ที่เป็นประโยชน์ต่อทั้งทีม ในแต่ละรายการให้ตัดชื่อคนและสถานการณ์ความล้มเหลวที่เฉพาะเจาะจงออก แล้วเขียนใหม่เป็นประโยคที่เป็นตัวเลือก atom ซึ่งเหลือไว้เพียง 'อะไรและทำอย่างไร' ส่วนโน้ตอารมณ์ส่วนตัว สไตล์ และความขัดแย้งให้คัดออกจากรายการตัวเลือก พร้อมระบุเหตุผลที่คัดออกเป็นหนึ่งบรรทัด"
ประโยคสุดท้าย — ที่ให้ระบุเหตุผลในการคัดออก — สำคัญ เมื่อ AI แสดงให้เห็นว่ามันไม่ได้เลื่อนขั้นอะไรขึ้นไปบ้าง คนก็สามารถตรวจสอบได้ว่าด่านเข้มงวดเกินไปจนกั้นบทเรียนที่แท้จริงไปด้วยหรือเปล่า
ด่านไม่ได้ทำงานเรียบร้อยสะอาดเสมอไป ในความเป็นจริง AI ทำได้ไม่ตรงในครั้งเดียว คนปฏิเสธ แล้วสั่งใหม่ การไป-กลับเช่นนี้เกิดขึ้นจริง ผมขอยกการไป-กลับจริงในตอนที่พยายามเลื่อน notes.md หนึ่งสัปดาห์ของสมาชิกทีม B ขึ้นสู่ shared มาให้ดูตามที่เกิดขึ้น
[พรอมต์ — รอบที่ 1]
"ด้านล่างคือ notes.md ของสมาชิกทีม B สัปดาห์นี้ เลือกเฉพาะรายการที่เป็นประโยชน์ต่อทั้งทีมเป็นตัวเลือก shared atom ตัดชื่อคนและสถานการณ์ความล้มเหลวที่เฉพาะเจาะจงออก แล้วเหลือไว้เพียง 'อะไรและทำอย่างไร'
- อังคาร/ทดสอบ Cooldown 0.6 วินาที อินพุตถูกดันตกหล่น ทำตามการตัดสินใจร่วม 0.8 วินาที
- พุธ/enum สกิลที่สมาชิกทีม A เขียนชนกับ import ของผม จะพูดกับสมาชิกทีม A ตรง ๆ ก็กระไรอยู่ เลยส่งผ่านผู้อำนวยการแทน
- พฤหัส/เว็บพอร์ทัล View_skill_table.html พอรีเฟรชแล้วฟิลเตอร์รีเซ็ต น่ารำคาญ
- ศุกร์/สภาพไม่ค่อยดี ลาครึ่งวันบ่าย"
[ผลลัพธ์ดิบของ Claude — รอบที่ 1]
ผมได้สกัด 4 รายการต่อไปนี้เป็นตัวเลือก shared atom 1. Cooldown ของการต่อสู้ทำให้อินพุตถูกดันตกหล่นที่ 0.6 วินาที จึงยึด 0.8 วินาทีเป็นเกณฑ์ 2. enum สกิลที่สมาชิกทีม A เขียนชนกับ import จึงจำเป็นต้องแชร์ล่วงหน้าเมื่อมีการเปลี่ยน enum 3. View_skill_table.html รีเซ็ตสถานะฟิลเตอร์เมื่อรีเฟรช จึงจำเป็นต้องมีฟังก์ชันเก็บสถานะไว้ 4. (คัดออก) ลาครึ่งวันบ่ายวันศุกร์ — พิจารณาว่าเป็นกำหนดการส่วนตัวจึงคัดออก
ตรงนี้ผม ปฏิเสธข้อ 2 "ที่สมาชิกทีม A เขียน" ยังคงอยู่ตามเดิม ตัดชื่อคนออกแล้วก็จริง แต่ก็ยังระบุได้ว่า "ใครเขียน enum" อีกทั้ง บริบทความขัดแย้งส่วนตัว ที่ว่า "จะพูดตรง ๆ ก็กระไรอยู่ เลยส่งผ่านผู้อำนวยการ" ยังแปลงร่างมาเป็นรายการข้อเท็จจริง นี่ไม่ใช่สินทรัพย์การเรียนรู้ แต่เป็นประเด็นความสัมพันธ์ที่ควรจัดการแยกใน 1:1 หากเลื่อนขึ้น shared ไปตามนั้น สมาชิกทีม A จะเห็นบันทึกความขัดแย้งที่ระบุชื่อตัวเองอย่างชัดเจนในช่องที่ใช้ร่วมกัน
[การตรวจสอบ/ปฏิเสธโดยมนุษย์ → ขอใหม่]
"ข้อ 2 ปฏิเสธ การชนกันของ enum มีค่าควรเก็บไว้เป็นข้อเท็จจริง แต่ข้อมูลที่ว่าคนเฉพาะรายเป็นคนเขียน และเส้นทางการสื่อสาร (พูดผ่านใคร) เป็นพื้นที่ความสัมพันธ์ส่วนบุคคล ตัดสองอย่างนั้นออก แล้วเขียนใหม่ให้เหลือเพียง 'ขั้นตอนที่ป้องกันการชนกันของข้อมูล' เท่านั้น และข้อ 3 บั๊กเว็บพอร์ทัลไม่ใช่ atom แต่เป็นรายการที่ควรไปยัง issue tracker ของพอร์ทัล จึงให้คัดออกจากตัวเลือกพร้อมระบุเหตุผล"
[ผลลัพธ์ดิบของ Claude — รอบที่ 2]
แก้ไขแล้ว 2. (แก้ไข) เมื่อจะเปลี่ยน enum ของสกิล ให้แชร์ล่วงหน้ากับผู้ทำงานคนอื่นที่ import ชีตข้อมูล การเปลี่ยนลำดับ·ค่าของ enum อาจพังแบบเงียบ ๆ ที่ฝั่ง import ได้ 3. (คัดออก) View_skill_table.html ฟิลเตอร์รีเซ็ต — เป็นบั๊กของเครื่องมือ เหมาะกับรายการ issue ของพอร์ทัลมากกว่า shared atom จึงคัดออกจากตัวเลือก ตัวเลือกเลื่อนขึ้น shared สุดท้าย: ข้อ 1 (Cooldown 0.8 วินาที), ข้อ 2 (แชร์การเปลี่ยน enum ล่วงหน้า) รวม 2 รายการ
รอบที่ 2 ผมให้ผ่าน ในข้อ 2 ทั้งตัวคนและเส้นทางการสื่อสารหายไป เหลือไว้เพียงขั้นตอน เริ่มต้นด้วย 4 รายการ มี 2 รายการเลื่อนขึ้น shared, 1 รายการคงอยู่ในช่องส่วนตัว (ลาครึ่งวัน) และ 1 รายการแยกไปเป็น issue ของพอร์ทัล (บั๊กฟิลเตอร์) นี่คือภาพของด่านที่ทำงานจริง — ไม่ได้สำเร็จในครั้งเดียว คนปฏิเสธหนึ่งครั้ง และมีบันทึกหลงเหลือว่าอะไรหายไปเพราะอะไร พอสรุปแล้วดูสะอาดเรียบร้อย แต่ถ้าดูแค่บทสรุปที่สะอาด ข้อเท็จจริงที่ว่าข้อ 2 เกือบจะถูกเลื่อนขึ้นไปพร้อมชื่อคนตั้งแต่แรกก็จะถูกลบทิ้งไป ความเสี่ยงนั้นคือเหตุผลของการมีอยู่ของด่านนี้
แม้จะแบ่งช่องและตั้งด่านแล้ว ถ้าทุกครั้งที่สนทนาต้องมากางช่องด้วยมือก็ทำให้การดำเนินงานหนัก ดังนั้นชิ้นส่วนสุดท้ายคือ ช่องที่เข้ากับบริบทการสนทนาถูกกางออกอัตโนมัติ บนเครื่อง PC ของผมเอง สิ่งนี้ทำโดยฮุก UserPromptSubmit (inject_memory.py) มันจะเลือกเฉพาะช่องที่ตรงกับประโยคอินพุตแล้วฉีดเข้าสู่บริบท
กฎเรียบง่าย ถ้าถกการตัดสินใจของเกม atom ใน shared/ ก็จะถูกกางออก ถ้าเตรียมการคุย 1:1 กับสมาชิกทีมคนหนึ่ง context.md ของคนคนนั้น + shared ก็จะถูกกางออกพร้อมกัน ถ้าเขียนการทบทวนรายไตรมาส หน่วยความจำของโปรเจกต์ + ช่องของผู้อำนวยการเองก็จะถูกกางออก ถ้าเขียนรายงานต่อภายนอก ช่องของผู้อำนวยการ + บางส่วนของ shared ก็จะถูกกางออก ช่องไหนจะถูกกางออกก็คืออินเทอร์เฟซของหน่วยความจำในตัว
ตรงนี้การแบ่งช่องออกฤทธิ์อีกครั้ง ตอนเตรียม 1:1 ช่องส่วนตัวของสมาชิกทีม B จะถูกกางออก แต่ช่องส่วนตัวของสมาชิกทีม C ไม่ถูกกาง — เพราะไม่เกี่ยวกับการสนทนาในตอนนี้ ถ้าไม่แบ่งช่อง ทุกครั้งก็จะถูกกางออกทั้งหมดแล้วถูกกลบในสัญญาณรบกวน และที่แย่กว่านั้นคือในวง 1:1 โน้ตส่วนตัวของคนที่ไม่เกี่ยวข้องจะถูกลากออกมา การแยกจึงเป็นทั้งความปลอดภัยและความแม่นยำของการฉีดไปพร้อมกัน
แม้โครงสร้างช่องจะลงตัว ก็ยังเหลือกับดักสุดท้ายอยู่หนึ่งอย่าง ผมสลับไปมาระหว่าง PC ที่บ้านกับ PC ที่ทำงาน และหน่วยความจำซิงค์ผ่านโฟลเดอร์คลาวด์ ตรงนี้ถ้า PC สองเครื่องแก้ช่องเดียวกันพร้อมกันก็จะเกิดการชนกัน ถ้าฝ่ายหนึ่งเขียนทับอีกฝ่ายทั้งยวง notes.md ของวันนั้นก็จะหายไป
วิธีรับมือต่างกันไปตามแต่ละช่อง notes.md ส่วนตัวที่อัปเดตบ่อยควรเก็บไว้ในที่เก็บที่ merge ได้อย่าง git แล้วรวมทั้งสองฝ่ายเข้าด้วยกันเมื่อเกิดการชน ส่วน context.md ที่เสถียรหรือ atom ใน shared/ มีความถี่การอัปเดตต่ำ จึงใช้การล็อกหรือการสำรองข้อมูลรายวันก็เพียงพอ ประเด็นสำคัญคือการตัด "พฤติกรรมที่การซิงค์เขียนทับฝ่ายหนึ่ง" ออกจากค่าตั้งต้น การที่ช่องส่วนตัวถูกปนเข้าโฟลเดอร์ที่ใช้ร่วมกันแล้วซิงค์ไปเพราะตั้งสิทธิ์โฟลเดอร์ผิด — นั่นคือเหตุการณ์ที่เงียบที่สุดและร้ายแรงที่สุด หากระบุไว้ชัดว่าแต่ละช่องสังกัดพื้นที่ซิงค์ใด ก็จะกันเหตุการณ์ "ปะปน" ในตระกูลเดียวกับเหตุการณ์ 0.6 วินาทีได้ตั้งแต่ทางเข้า
setup
1. สร้างโฟลเดอร์แยกตามคนไว้ใต้ team_memory/ ตัวเองหนึ่ง + สมาชิกทีมแต่ละคน และ shared/ อีกหนึ่ง ชื่อโฟลเดอร์ใช้นามแฝง (leeminsoo, สมาชิกทีม A …)
2. ในแต่ละโฟลเดอร์ส่วนตัว วางไฟล์สองไฟล์ คือ context.md (เสถียร — บทบาท·หน้าที่·สไตล์) และ notes.md (ผันผวน — การทดลอง·ความผิดพลาด·การตัดสินใจรายวัน)
3. ระบุสิทธิ์โฟลเดอร์ให้ shared/ มีสิทธิ์ read สำหรับทุกคน ส่วนโฟลเดอร์ส่วนตัวมีสิทธิ์ read สำหรับเจ้าตัว + ผู้อำนวยการ
prompt (ทันทีหลังการทบทวนรายสัปดาห์ ด่านเลื่อนขั้นส่วนตัว → shared) — ใช้พรอมต์การเลื่อนขั้นใน §20.2.3 ตามเดิม (ตัดชื่อคน·สถานการณ์ความล้มเหลวออก + เหลือเพียง 'อะไรและทำอย่างไร' + ระบุเหตุผลที่คัดออก)
verify
1. อ่านด้วยตัวเองว่าในประโยคตัวเลือกที่ออกมา มีชื่อคน·เส้นทางการสื่อสาร·การพรรณนาอารมณ์หลงเหลืออยู่หรือไม่ ถ้ามีแม้แต่อย่างเดียวก็ปฏิเสธ แล้วขอใหม่ด้วย "ตัดข้อมูลนั้นออกแล้วเหลือเพียงขั้นตอน"
2. ย้ายเฉพาะตัวเลือกที่ผ่านไปเป็น atom ใน shared/ ส่วนรายการที่ตัดออกก็คงไว้ในช่องส่วนตัวตามเดิม
3. ตรวจสอบสิทธิ์โฟลเดอร์ซิงค์ — ดูว่าช่องส่วนตัวไม่ได้เข้าไปอยู่ในเส้นทางโฟลเดอร์ที่ใช้ร่วมกัน
ฉบับย่อสำหรับคนเดียว
ถ้าอยู่คนเดียว ห้าโฟลเดอร์ก็เกินไป ใช้ notes.md ไฟล์เดียวเขียนทุกวัน และเก็บ context.md ไว้ในหัว แต่กระนั้นก็ยังคงด่านไว้ — สัปดาห์ละครั้งให้กรองโน้ตของตัวเองด้วย "เลือกเฉพาะหนึ่งบรรทัดจาก notes นี้ที่มีค่าควรกลับมาดูอีกในครั้งหน้า" ก็จะแยกโน้ตที่ผันผวนออกจากบทเรียนที่แปลงเป็นสินทรัพย์แล้วได้ เมื่อคนเพิ่มเป็นสองคนเมื่อไหร่ ค่อยแบ่งช่องตอนนั้นก็ได้
บ่ายแก่ ๆ ของวันพฤหัสบดี ก่อนจะอัปโหลดบิลด์ไม่นาน เพื่อนร่วมทีม B ซึ่งเป็นโปรแกรมเมอร์ฝั่งไคลเอนต์โพสต์ข้อความลงในแชตภายในทีมว่า "ค่าคงที่ของ global cooldown ที่ตกลงกันในทีมเฉพาะกิจด้านการต่อสู้เมื่อสัปดาห์ก่อนคือ 0.8 วินาทีใช่ไหมครับ มันเขียนไว้ในเอกสารตรงไหน" ห้านาทีต่อมา เพื่อนร่วมทีม A ซึ่งเป็นนักออกแบบเกมตอบว่า "น่าจะอยู่ในบันทึกการประชุมที่ไหนสักแห่ง… กำลังหาอยู่" อีกเจ็ดนาทีต่อมา "อยู่ในโฟลเดอร์ไหนของ git นะ"
การโต้ตอบไปมา 12 นาทีนี้ไม่ได้เกิดเพราะไม่มีข้อมูล ข้อมูลมีอยู่แน่นอน มันถูกบันทึกไว้ทั้งในไฟล์ atom ในบันทึกการประชุม และในการ์ดการตัดสินใจ เพียงแต่ทั้งสามอย่างอยู่คนละลิ้นชัก และวิธีเปิดแต่ละลิ้นชักก็ต่างกัน ปัญหาไม่ได้อยู่ที่ลิ้นชัก แต่อยู่ที่มือจับที่ใช้เปิดลิ้นชัก
บทนี้คือเรื่องราวของการรวมมือจับเหล่านั้นให้เหลือเพียงอันเดียว ไม่ใช่การพัฒนาฟูลสแตกขึ้นมาเอง แต่เป็นการคลุมเว็บบาง ๆ หนึ่งชั้นทับบนผลงานออกแบบที่กองอยู่ในโฟลเดอร์อยู่แล้ว เพื่อให้เพื่อนร่วมทีมพิมพ์เพียงคำว่า portal ลงในช่องที่อยู่ของเบราว์เซอร์ก็เข้ามาได้ เครื่องมือหลักมีเพียงสามอย่างเท่านั้น ได้แก่ FastAPI ที่ใช้ Python รัน search API ขึ้นมา, nginx ที่ตั้งไว้ด้านหน้า และ nssm ที่คอยให้บริการทำงานอยู่ตลอดเวลาที่เครื่องเปิดอยู่โดยที่คนไม่ต้องคอยเปิดเอง
ผลงานออกแบบนั้นกระจัดกระจายโดยธรรมชาติ ไม่ใช่เพราะตั้งใจให้มันกระจาย แต่เพราะผลงานแต่ละชิ้นไปตกอยู่ในที่ที่เป็นธรรมชาติที่สุดสำหรับมัน atom ไปอยู่ในรูปมาร์กดาวน์ของ repository บน git, กำหนดการไปอยู่ในเครื่องมือจัดการงาน, บทสนทนาแบบเรียลไทม์ไปอยู่ในแชต และ KPI ไปอยู่ในแดชบอร์ดแยกต่างหาก การที่แต่ละอย่างอยู่ในที่ของมันนั้นถูกต้องแล้ว ปัญหาคือคนต้องเก็บแผนที่ของที่เหล่านั้นไว้ในหัวตัวเอง
สำหรับพนักงานใหม่ แผนที่นี้เองคือกำแพงกั้นการเข้าถึง การจะหา "ค่า global cooldown" ได้นั้นต้อง (1) ตัดสินใจว่ามันเป็นการ์ดการตัดสินใจ เป็น atom หรือเป็นบันทึกการประชุม (2) เปิดเครื่องมือที่เกี่ยวข้องนั้นขึ้นมา และ (3) ค้นซ้ำด้วยไวยากรณ์การค้นหาของเครื่องมือนั้น ทั้งสามขั้นตอนล้วนเป็นความรู้ฝังลึกที่มาจากประสบการณ์ทำงาน
แนวคิดของพอร์ทัลนั้นเรียบง่าย ผลงานยังคงวางไว้ในที่เดิม แต่วางดัชนีสำหรับการค้นหาทับไว้หนึ่งชั้น แล้วเปิดดัชนีนั้นให้เข้าถึงผ่านเบราว์เซอร์ แทนที่จะมีโต๊ะเจ็ดตัว ก็มีโต๊ะตัวเดียวที่มีลิ้นชักเจ็ดช่อง ลิ้นชักยังคงเหมือนเดิม แต่คนนั่งเพียงครั้งเดียว
ต่อไปนี้คือโครงสร้างของพอร์ทัลที่ผู้เขียนใช้งานจริงในโปรเจกต์ A มันทำงานอยู่ในสถานะเปิดตลอดเวลาบนเครื่อง PC ส่วนกลางของทีมออกแบบเพียงเครื่องเดียว โดยไม่ต้องมีอุปกรณ์เซิร์ฟเวอร์แยกต่างหาก
flowchart TB
subgraph client["เบราว์เซอร์ของเพื่อนร่วมทีม"]
U1["teammate_a · งานออกแบบ"]
U2["teammate_b · ไคลเอนต์"]
U3["teammate_c · เซิร์ฟเวอร์"]
U4["leeminsoo · ผู้อำนวยการ"]
end
U1 & U2 & U3 & U4 -->|"http://portal/"| NGINX
subgraph host["PC ส่วนกลางของทีมออกแบบ (ทำงานตลอดเวลา)"]
NGINX["nginx
ไฟล์สแตติก + reverse proxy"]
NGINX -->|"/ (สแตติก)"| VIEW["View_*.html
หน้าจอที่ Claude เขียน"]
NGINX -->|"/api/* (พร็อกซี)"| API["FastAPI · server.py
:8000"]
API --> IDX[("ดัชนีการค้นหา
ผลลัพธ์จาก build_index.py")]
subgraph svc["nssm (Windows service)"]
API
NGINX
end
end
IDX -.->|"เป้าหมายการทำดัชนี"| SRC
subgraph SRC["ผลงานเดิม (คงไว้ที่เดิม)"]
A1["atom .md (git)"]
A2["การ์ดการตัดสินใจ .md"]
A3["บันทึกการประชุม .md"]
A4["team_memory/*"]
end
BUILD["build_index.py
รันเป็นระยะ"] -->|"อ่าน"| SRC
BUILD -->|"เขียน"| IDX
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class NGINX,API,BUILD code;
class U1,U2,U3,U4 human;
class VIEW,IDX,A1,A2,A3,A4 data;
ในภาพ ส่วนล่างที่ถูกจัดกลุ่มด้วยสีเทาคือผลงานที่มีอยู่ก่อนแล้ว ส่วนที่พอร์ทัลเพิ่มเข้ามาใหม่คือสามชั้นบาง ๆ ด้านบน — ดัชนี, FastAPI และ nginx — เท่านั้น เป็นโครงสร้างที่เปิดประตูทางเข้าใหม่โดยไม่แตะต้องผลงานเลย
ตัวตนที่แท้จริงของพอร์ทัลจบลงด้วยไฟล์เล็ก ๆ ห้าไฟล์ เมื่อดูทีละชิ้น แต่ละชิ้นทำหน้าที่เพียงอย่างเดียวเท่านั้น
build_index.py — แปลงผลงานให้อยู่ในรูปที่ค้นหาได้ สแกนทั่ว repository บน git อ่าน atom, การ์ดการตัดสินใจ, บันทึกการประชุม และมาร์กดาวน์ทั้งหมดใต้ team_memory/ แล้วดึงหัวข้อ เนื้อหา และแท็กออกมารวมเป็นไฟล์ดัชนีไฟล์เดียว สิ่งที่สคริปต์นี้ทำมีเพียง "การทำให้ไฟล์ที่กระจัดกระจายแบนราบลงเป็นเรกคอร์ดบรรทัดเดียว" เท่านั้น เพราะไม่ได้แตะตัวไฟล์ต้นฉบับ ต่อให้ดัชนีเสีย ต้นฉบับก็ปลอดภัย หากรันใหม่เป็นระยะ ๆ (เช่น ทุก 30 นาที หรือผ่าน git commit hook) สถานะก็จะเป็นปัจจุบันอยู่เสมอ
server.py — รัน search API ด้วย FastAPI โหลดดัชนีขึ้นหน่วยความจำไว้ แล้วเมื่อมีคำขอ /api/search?q=... เข้ามา ก็คืนเรกคอร์ดที่ตรงกันกลับไปเป็น JSON โค้ดไม่เกินหนึ่งหน้าจอ
# server.py (ตัดตอน — โครงของ search endpoint)
from fastapi import FastAPI
import json, pathlib
app = FastAPI()
INDEX = json.loads(pathlib.Path("index.json").read_text(encoding="utf-8"))
@app.get("/api/search")
def search(q: str):
q = q.strip().lower()
hits = [r for r in INDEX
if q in r["title"].lower() or q in r["body"].lower()]
# จัดกลุ่มตามชนิดแล้วคืนค่า → atom / การตัดสินใจ / บันทึกการประชุม / หน่วยความจำ
by_kind = {}
for r in hits:
by_kind.setdefault(r["kind"], []).append(
{"id": r["id"], "title": r["title"], "path": r["path"]})
return {"query": q, "count": len(hits), "results": by_kind}
อัลกอริทึมการค้นหาตั้งใจเริ่มต้นด้วยการจับคู่สตริงย่อย (substring matching) แบบเรียบง่าย เมื่อทีมมีขนาดกลาง (10\~50 คน) และเอกสารมีหลักพันชิ้น ความเรียบง่ายนี้กลับช่วยลดต้นทุนการบำรุงรักษา การวิเคราะห์รูปคำ (morphological analysis) หรือการค้นหาแบบเวกเตอร์นั้น ค่อยเพิ่มเข้ามาทีหลังตอนที่มีเสียงบ่นว่า "การค้นหาอ่อน" เกิดขึ้นจริงก็ยังไม่สาย
nginx — เสิร์ฟหน้าจอสแตติกและพร็อกซีไปยัง API เสิร์ฟไฟล์ View_*.html (หน้าจอค้นหา, หน้าจอผลลัพธ์, หน้าจอแดชบอร์ด) ที่ขอให้ Claude สร้างให้แบบสแตติก แล้วส่งต่อเฉพาะคำขอที่เข้ามาทาง /api/ ไปยัง FastAPI (:8000) ด้านหลัง ในมุมของเพื่อนร่วมทีม ทั้งหน้าจอและการค้นหาเกิดขึ้นที่ที่อยู่ http://portal/ เดียวกันทั้งหมด เพราะ Claude เป็นคนวาดหน้าจอขึ้นมาเป็น HTML โดยตรง เวลานักออกแบบต้องการหน้าจอใหม่ ก็แค่ขอว่า "ช่วยทำหน้าจอที่รวมเฉพาะการ์ดการตัดสินใจไว้ดูให้หน่อย" แล้วรับ View_decisions.html มาวางลงในโฟลเดอร์ก็จบ การไม่มี frontend build pipeline เป็นข้อได้เปรียบที่ชัดเจนสำหรับทีมขนาดกลาง
nssm — ทำให้พอร์ทัลยังมีชีวิตอยู่แม้คนไม่เปิด ข้อกำหนดหลักของพอร์ทัลคือ "ต่อให้ผมไม่อยู่ที่โต๊ะ เพื่อนร่วมทีมก็ต้องค้นหาได้" ถ้ารัน server.py จากเทอร์มินัล มันจะตายทันทีที่ปิดเทอร์มินัลนั้น และจะหายไปเมื่อรีบูตเครื่อง nssm (Non-Sucking Service Manager) ลงทะเบียนกระบวนการ Python นี้เป็น Windows service เพื่อให้มันฟื้นขึ้นมาเองอัตโนมัติเมื่อบูตเครื่อง และฟื้นคืนชีพเองอัตโนมัติเมื่อกระบวนการตาย การลงทะเบียนทำเพียงครั้งเดียวก็พอ
# ลงทะเบียน FastAPI เป็น Windows service ด้วย nssm (ครั้งเดียว)
nssm install Portal "C:\Python\python.exe" "C:\portal\portal_run.py"
nssm set Portal AppDirectory "C:\portal"
nssm start Portal
ในที่นี้ portal_run.py คือ launcher ความยาวห้าบรรทัด มีเพียงบรรทัดเดียวที่รัน server.py ด้วย uvicorn และโครงขั้นต่ำที่คอยกันไม่ให้บริการตายเท่านั้น คำสั่งที่คนต้องจำมีเพียง nssm start คำสั่งเดียว และต่อให้เป็นคำสั่งนั้น เมื่อลงทะเบียนครั้งเดียวแล้วก็ไม่ต้องพิมพ์อีก
การแบ่งหน้าที่ของสี่ชิ้นส่วนนี้เมื่อมองในภาพรวมเป็นดังนี้
ลำพังการอธิบายชิ้นส่วนอย่างเดียวคงทำให้สัมผัสคุณค่าของพอร์ทัลได้ยาก ลองโยนคำถามที่เคยกินเวลา 12 นาทีก่อนหน้านี้กลับเข้าไปใหม่บนพอร์ทัลกัน ต่อไปนี้คือการคัดลอกพรอมต์ที่ผู้เขียนโยนให้ Claude ตอนสร้างหน้าจอค้นหา ผลลัพธ์ที่ได้รับ และกระบวนการตรวจสอบ ทั้งหมดมาแบบไม่บีบอัด
พรอมต์ของผู้เขียน:
ช่วยทำหน้า HTML หน้าเดียวที่ fetch
index.jsonมาแล้วค้นหาให้หน่อย เมื่อพิมพ์คีย์เวิร์ดลงในช่องอินพุต ให้เรียก/api/search?q=แล้วแสดงผลลัพธ์โดยแบ่งเป็นสี่กลุ่มคือ atom / การ์ดการตัดสินใจ / บันทึกการประชุม / หน่วยความจำ ในรูปแบบการ์ด เมื่อคลิกที่ผลลัพธ์แต่ละอันให้พาไปยังพาธต้นฉบับ (path) อย่าใช้เฟรมเวิร์ก CSS·JS ภายนอก ให้รวมเป็นไฟล์เดียว ชื่อไฟล์View_search.html
ผลลัพธ์ดิบของ Claude (ตัดตอน, แบบที่ได้รับมาจริง):
<!-- View_search.html -->
<input id="q" placeholder="atom หรือคีย์เวิร์ด">
<div id="out"></div>
<script>
document.getElementById('q').addEventListener('keydown', async e => {
if (e.key !== 'Enter') return;
const r = await fetch('/api/search?q=' + encodeURIComponent(e.target.value));
const d = await r.json();
const order = ['atom','decision','meeting','memory'];
const label = {atom:'atom', decision:'การ์ดการตัดสินใจ',
meeting:'บันทึกการประชุม', memory:'หน่วยความจำ'};
out.innerHTML = order.filter(k => d.results[k]).map(k =>
`<h3>${label[k]} (${d.results[k].length})</h3>` +
d.results[k].map(x =>
`<a href="${x.path}">${x.title}</a>`).join('<br>')
).join('') || 'ไม่พบผลลัพธ์';
});
</script>
ผลลัพธ์นี้ยังไม่ใช่ของสำเร็จรูป ผู้เขียนไม่ได้รับมาตามนั้น แต่ปฏิเสธสองจุด
จุดแรก ส่วนที่ใช้ out เหมือนตัวแปร global โดยไม่ผ่าน getElementById เป็นโค้ดเสี่ยงที่อาศัยความเคยชินของเบราว์เซอร์ — มันไม่ทำงานในบางสภาพแวดล้อม จุดที่สอง เพราะ path เป็นพาธภายในของ git การใส่ลงใน href ตรง ๆ จะทำให้เบราว์เซอร์เปิดไฟล์ไม่ได้ ต้องแก้ให้พอร์ทัลกำหนดเส้นทาง (route) พาธนั้นกลับเข้าหน้าจอของตัวเองด้วยการผ่าน /view?path=
คำขอใหม่ของผู้เขียน:
ช่วยแก้สองอย่าง (1) ให้รับ
outอย่างชัดเจนด้วยdocument.getElementById(2) ลิงก์ผลลัพธ์อย่าพาไปที่พาธต้นฉบับตรง ๆ แต่ให้ผ่าน viewer endpoint/view?path=ส่วน viewer ผมจะเพิ่มที่ server.py เอง ฝั่งฟรอนต์แค่เปลี่ยนลิงก์ก็พอ
การโต้ตอบไปมานี้คือหัวใจสำคัญ ผลลัพธ์แรกของ Claude ถูกอยู่ 80% แต่อีก 20% ที่เหลือเป็นข้อบกพร่องที่จับได้ก็ต่อเมื่อคนต้องรู้บริบทว่า "พอร์ทัลนี้คลุมอยู่ทับผลงานบน git" การตรวจสอบยังคงเป็นหน้าที่ของคน
พอรันค้นหาหนึ่งครั้ง หน้าจอของเพื่อนร่วมทีมจะแสดงผลลัพธ์ที่ถูกจัดกลุ่มไว้แบบนี้
| กลุ่ม | ผลการค้นหาด้วยคำว่า "global cooldown" |
|---|---|
| atom | combat_global_cooldown_constant |
| การ์ดการตัดสินใจ | D2026_Q2_017 (ยืนยันที่ 0.8 วินาที) |
| บันทึกการประชุม | 95_BattleTF ครั้งที่ 2 |
| หน่วยความจำ | บันทึก 1:1 ของเพื่อนร่วมทีม B จำนวน 1 รายการ |
การโต้ตอบไปมา 12 นาทีในบ่ายวันพฤหัสบดี ลดลงเหลือ 20 วินาทีของการพิมพ์คำเดียวลงในช่องค้นหา และที่สำคัญกว่านั้นคือ 20 วินาทีนี้กลายเป็นงานที่เพื่อนร่วมทีม B จัดการคนเดียวได้จบ ทำให้ไม่ต้องไปใช้เวลา 12 นาทีของเพื่อนร่วมทีม A เลย
วิธีสร้างพอร์ทัลแบ่งใหญ่ ๆ ได้สามทาง คือพัฒนาฟูลสแตกขึ้นมาเองตั้งแต่ต้น, นำเครื่องมือรวมศูนย์จากภายนอกอย่าง Notion·Coda เข้ามาใช้ หรือคลุมระบบอัตโนมัติบาง ๆ ทับเครื่องมือพื้นฐานอย่างที่ทำอยู่นี้ ผู้เขียนเลือกทางที่สาม และเหตุผลของการเลือกนั้นอยู่ที่ขนาดของทีมที่เป็นทีมขนาดกลาง
การพัฒนาฟูลสแตกเองมีอิสระสูงที่สุด แต่ภาระในการต้องคอยบำรุงรักษาเว็บนั้นต่อไปหลังสร้างเสร็จมาถึงก่อนผลลัพธ์ งานปฏิบัติการอย่างการยืนยันตัวตน การดีพลอย และการ migrate ฐานข้อมูล จะตกมาอยู่ที่ทีมออกแบบ เครื่องมือรวมศูนย์จากภายนอกนั้นรวดเร็ว แต่มีค่าสมาชิกรายเดือนตามมา และเหนือสิ่งอื่นใดยังมีต้นทุนการย้ายข้อมูลที่ต้องโอนผลงานมาร์กดาวน์ที่กองอยู่ใน git ไปเป็นรูปแบบของเครื่องมือนั้นใหม่ ในทางกลับกัน การผสมผสาน FastAPI+nginx+nssm คงผลงานไว้ที่เดิมแล้วคลุมดัชนีทับเพียงชั้นเดียว จึงใช้เวลาไม่กี่วันก็เริ่มทำงานได้ และการบำรุงรักษาก็จบลงเพียงระดับการแก้ build_index.py เป็นครั้งคราว
ต่อไปนี้คือความเปลี่ยนแปลงที่ผู้เขียนสัมผัสได้ก่อนและหลังนำพอร์ทัลมาใช้ในโปรเจกต์ A ตัวเลขในตารางไม่ใช่การวัดอย่างแม่นยำ แต่เป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ควรอ่านที่ทิศทางและสัดส่วนมากกว่าค่าสัมบูรณ์
| รายการ | ไม่มีพอร์ทัล | มีพอร์ทัลใช้งาน | ทิศทาง |
|---|---|---|---|
| เวลาที่ใช้ค้นข้อมูล 1 ครั้ง | หลายนาที | น้อยกว่า 1 นาที | สั้นลงมาก |
| ความถี่ของคำถาม "อันนี้อยู่ตรงไหน" | บ่อย | นาน ๆ ครั้ง | ลดลง |
| สมาชิกใหม่ปรับตัวกับเครื่องมือ | ราวสองสัปดาห์ | ไม่กี่วัน | สั้นลง |
| อัตราการลงบันทึกการประชุม·การ์ดการตัดสินใจ | ราวครึ่งหนึ่ง | ส่วนใหญ่ | สูงขึ้น |
บรรทัดสุดท้ายคือสิ่งที่เป็นแก่นที่สุด เมื่อหาข้อมูลได้ง่ายขึ้น ไม่ใช่แค่การค้นหาเร็วขึ้นเท่านั้น แต่แรงจูงใจในการทิ้งบันทึกข้อมูลไว้ก็สูงขึ้นด้วย ความเย้ยหยันที่ว่า "จะเขียนบันทึกการประชุมที่ยังไงก็หาไม่เจอไปทำไม" เปลี่ยนเป็น "เขียนเพราะเขียนแล้วค้นหาเจอ" พอร์ทัลเป็นเครื่องมือค้นหาและขณะเดียวกันก็เป็นกลไกจูงใจให้บันทึก วงจรดี ๆ นี้สร้างคุณค่าที่มากกว่าการรวมเครื่องมือหนึ่งหรือสองอย่างเข้าด้วยกัน
แต่สมดุลนี้ขึ้นอยู่กับขนาดของทีม เมื่อทีมเกิน 50 คนและผลงานพอกขึ้นเป็นหลักหมื่นชิ้น ข้อจำกัดของการค้นหาแบบสตริงย่อยและข้อจำกัดของการเสิร์ฟด้วย PC เครื่องเดียวจะปรากฏขึ้นพร้อมกัน ณ จุดนั้น การพัฒนาฟูลสแตกเองหรือการนำ search engine เข้ามาใช้ก็มีเหตุผลรองรับ โครงสร้างนี้ในตอนนี้คือ "คำตอบที่เหมาะกับทีมขนาดกลาง" ไม่ใช่คำตอบที่ถูกต้องสำหรับทุกขนาด
setup เลือกเครื่อง PC ส่วนกลางของทีมออกแบบ (หรือเครื่องที่เปิดทิ้งไว้ตลอด) มาหนึ่งเครื่อง ติดตั้ง Python, nginx และ nssm ตรวจสอบตำแหน่งของโฟลเดอร์ผลงานที่จะทำดัชนี (atom·การ์ดการตัดสินใจ·บันทึกการประชุม·team_memory)
prompt ขอจาก Claude สามอย่างตามลำดับ
(1) "ช่วยทำ build_index.py ที่อ่านมาร์กดาวน์ในโฟลเดอร์นี้ ดึงหัวข้อ·เนื้อหา·แท็ก·ชนิด ออกมาแล้วทิ้งเป็น
index.jsonให้หน่อย ส่วนชนิดให้แยกแยะด้วยกฎของพาธ" (2) "ช่วยทำ FastAPI server.py ที่โหลด index.json นั้นขึ้นหน่วยความจำแล้วค้นหาผ่าน/api/search?q=ให้หน่อย ผลลัพธ์ให้จัดกลุ่มตามชนิดแล้วคืนค่า" (3) "ช่วยทำ HTML หน้าเดียว (View_search.html) ที่ fetch index.json มาค้นหา ให้รวมเป็นไฟล์เดียวโดยไม่ใช้เฟรมเวิร์กภายนอก"
verify ตรวจสอบสามอย่างด้วยตัวเอง (1) หลังรัน build_index.py แล้วดูว่าจำนวนผลงานเข้าไปใน index.json ครบถูกต้องไหม — ดูว่าไม่มีโฟลเดอร์ไหนตกหล่น (2) รัน server.py ขึ้นมาแล้วเรียก /api/search?q=คีย์เวิร์ดทดสอบ จากเบราว์เซอร์โดยตรง เพื่อดูว่า JSON ออกมาแบบจัดกลุ่มแล้วหรือไม่ (3) อ่านโค้ดหน้าจอที่ Claude สร้างให้ แล้วจับว่าพาธของลิงก์ไม่ได้เปิดเผยพาธภายในของ git ตรง ๆ และไม่มีโค้ดที่อาศัยตัวแปร global — ข้อบกพร่องสองอย่างที่เห็นในหัวข้อก่อนหน้าจะถูกกรองออกตรงนี้เอง สุดท้าย หลังลงทะเบียนบริการด้วย nssm แล้วให้รีบูตเครื่อง PC เพื่อยืนยันว่าพอร์ทัลยังมีชีวิตอยู่แม้คนไม่ได้เปิดอะไรเลย
ต่อให้ไม่มีทีม โครงสร้างนี้ก็ยังมีประโยชน์ตามเดิม เพราะคนที่ทำงานคนเดียวก็มีผลงานที่กระจัดกระจายเช่นกัน ใน setup ให้ใช้ PC ของตัวเองแทน PC ส่วนกลาง และจะข้ามการลงทะเบียน nssm ก็ได้ (รันด้วย python portal_run.py เฉพาะตอนที่ต้องใช้) prompt ให้รับทั้งสามอย่างคือ build_index.py, server.py และ View_search.html เหมือนเดิม แต่ตัดส่วน team_memory ออก แล้วทำดัชนีเฉพาะ atom·การตัดสินใจ·บันทึกการประชุม verify เพียงตรวจจำนวนใน index.json กับค้นหาหนึ่งครั้งก็เพียงพอ แก่นยังคงเหมือนเดิม — คงผลงานไว้ที่เดิม แล้วเปิดเพียงประตูทางเข้าการค้นหาขึ้นใหม่หนึ่งบาน
เช้าวันอังคารหลังเข้างานทันที เวลา 9 นาฬิกา 12 นาที ก่อนจะเปิดบอร์ดของเครื่องมือทำงานร่วมกันด้วยซ้ำ ผมพิมพ์ข้อความบรรทัดเดียวลงในหน้าต่าง Claude Code
แสดงงาน P0 ที่ยังไม่เสร็จของสัปดาห์นี้ เรียงตามกำหนดส่งที่ใกล้เข้ามาที่สุด
หยุดนิ่งราวสามวินาที แล้วคำตอบก็ปรากฏขึ้น ผมไม่ได้เปิดเครื่องมือทำงานร่วมกันด้วยตัวเอง ไม่ได้ไล่ดูแท็บแดชบอร์ด และไม่ได้ส่งข้อความหาผู้รับผิดชอบผ่านแชตด้วย แต่กลับมีงานหนึ่งที่เลยกำหนดส่งมาแล้วหนึ่งวันถูกดึงขึ้นมาไว้บนสุด ผมจึงเพิ่งเปิดเครื่องมือทำงานร่วมกันขึ้นมาเพื่อตรวจดูเฉพาะการ์ดใบนั้นใบเดียว
สามวินาทีนี้เกิดขึ้นได้อย่างไรคือทั้งหมดของบทนี้ ประเด็นสำคัญคือผมไม่ได้เปลี่ยนเครื่องมือเลย ทีมโปรเจกต์ A ยังคงใช้เครื่องมือทำงานร่วมกันเหมือนเดิม (ในโปรเจกต์นี้คือ ClickUp — SaaS สำหรับจัดการงานและกำหนดการ ซึ่ง JIRA, Redmine และ Linear ก็อยู่ในตำแหน่งเดียวกัน) ไม่ว่าเครื่องมือทำงานร่วมกันจะเป็นอะไร กระแสการทำงานของบทนี้ก็ย้ายมาใช้ได้ทั้งดุ้นเพียงแค่เปลี่ยนชื่อเครื่องมือ ชีตข้อมูลก็ยังอยู่บน SVN เหมือนเดิม การ์ดการตัดสินใจก็ยังอยู่บนพอร์ทัลเหมือนเดิม สิ่งที่เปลี่ยนไปมีเพียงอย่างเดียว คือ LLM เปิดดูเครื่องมือเหล่านั้นได้ด้วยมือของตัวเอง แล้ว และมาตรฐานของการเชื่อมต่อนั้นคือ MCP (Model Context Protocol)
ถ้าจะเปรียบ มันไม่ใช่การเพิ่มพนักงานใหม่อีกคนมานั่งที่โต๊ะประชาสัมพันธ์ แต่ใกล้เคียงกับการเปิดกุญแจห้องเก็บเอกสารที่มีอยู่แล้วให้ LLM เข้าไปได้ด้วยมากกว่า ห้องเก็บเอกสารยังเหมือนเดิม เพียงแต่เราเหลากุญแจเพิ่มมาอีกหนึ่งดอกเท่านั้น
MCP คือโปรโตคอลมาตรฐานที่ให้ LLM เข้าถึงเครื่องมือและข้อมูลภายนอก คำว่ามาตรฐานคือหัวใจ เราไม่ได้เหลาอะแดปเตอร์แยกชิ้น — ชิ้นหนึ่งสำหรับเครื่องมือทำงานร่วมกัน อีกชิ้นสำหรับเอกสาร อีกชิ้นสำหรับ git — แต่บนข้อตกลงเดียวที่ชื่อ JSON-RPC เครื่องมือแต่ละตัวจะเปิดเผยตัวเองในฐานะ "เซิร์ฟเวอร์" และ LLM ในฐานะ "ไคลเอนต์" จะพูดคุยกับเซิร์ฟเวอร์นั้น
โครงสร้างมีสามชิ้น
เซิร์ฟเวอร์จะเปิดเผยรายการ "สิ่งที่ฉันทำได้" หากเป็นเซิร์ฟเวอร์ของเครื่องมือทำงานร่วมกัน ก็จะเป็นฟังก์ชันอย่าง search_tasks, get_task, update_task ไคลเอนต์ (LLM) รับภาษาธรรมชาติจากผู้ใช้ แล้วเลือกฟังก์ชันที่ตรงเรียกใช้ จากนั้นแปลง JSON ที่ตอบกลับมาให้กลายเป็นภาษาธรรมชาติอีกครั้ง ตอนที่ผมพูดว่า "แสดงงาน P0 ให้ดู" สิ่งที่ LLM ทำมีเพียงการเรียก search_tasks(priority=P0, status=open) เท่านั้น
ตรงนี้ขอทำให้ชัดเจนสักอย่างหนึ่ง หนังสือเล่มนี้ไม่ได้ว่าด้วยวิธีเขียนเซิร์ฟเวอร์ MCP ด้วยมือ เครื่องมือส่วนใหญ่อย่าง ClickUp, JIRA, GitHub มีเซิร์ฟเวอร์ MCP ทางการหรือของชุมชนอยู่แล้ว ดังนั้นสิ่งที่นักออกแบบเกมต้องทำคือ "ติดตั้งและเชื่อมต่อเซิร์ฟเวอร์" ไม่ใช่ "การพัฒนาโปรโตคอล" ตัวโปรโตคอลเองนั้น Anthropic ประกาศเป็นมาตรฐานเปิดเมื่อเดือนพฤศจิกายน 2024 และข้อกำหนดเปิดเผยอยู่ที่ modelcontextprotocol.io
หากพูดแต่ปากว่า "มันทำให้อัตโนมัติได้" ก็ฟังดูเหมือนเวทมนตร์ ลองตามดูจริง ๆ สักหนึ่งครั้งจนจบ ด้านล่างนี้คือเซสชันที่ผมรันครั้งแรกทันทีหลังต่อ MCP ของเครื่องมือทำงานร่วมกันในโปรเจกต์ A คัดลอกมาเกือบจะตามต้นฉบับทั้งหมด ผมปล่อยผลลัพธ์ดิบของ LLM ไว้โดยไม่สรุปย่อ รวมถึงส่วนที่ดูแปลก ๆ ด้วย
① พรอมต์ที่ผมพิมพ์ (เต็ม)
ช่วยจัดงาน P0 ที่ยังไม่เสร็จและครบกำหนดส่งในสัปดาห์นี้จาก ClickUp ให้หน่อย เรียงตามกำหนดส่งที่ใกล้เข้ามาที่สุด
แต่ละรายการให้ใส่ผู้รับผิดชอบและจำนวนวันที่เหลือจนถึงกำหนดส่ง ส่วนงานที่เลยกำหนดส่งแล้วให้ดึงขึ้นบนสุด
② ผลลัพธ์ดิบของ Claude (ตามต้นฉบับ)
ดึงข้อมูลจากเวิร์กสเปซ ClickUp ด้วยเงื่อนไข P0·ยังไม่เสร็จ·ครบกำหนดส่งในสัปดาห์นี้
พบ 3 รายการ (ดึงรายการที่เลยกำหนดส่งขึ้นบนสุดแล้ว)
1. TASK_017 เปลี่ยนโค้ด global cooldown
ผู้รับผิดชอบ: teammate_a / กำหนดส่ง: เมื่อวาน (−1 วัน, เลยแล้ว)
2. TASK_023 ปรับใช้ voice_lint v2
ผู้รับผิดชอบ: teammate_b / กำหนดส่ง: พรุ่งนี้ (+1 วัน)
3. TASK_031 อัปเดตสคีมาของบันทึกการต่อสู้
ผู้รับผิดชอบ: teammate_c / กำหนดส่ง: วันศุกร์ (+3 วัน)
หมายเหตุ: TASK_009 (ตรวจสอบตารางบาลานซ์) ก็ดูเหมือนจะเป็น P0 เช่นกัน
แต่ status เป็น 'in review' จึงไม่แน่ใจว่าควรนับเป็น 'ยังไม่เสร็จ' หรือไม่
จึงยังไม่ได้รวมไว้ — หากต้องการ โปรดแจ้งเกณฑ์มาได้
ตรงนี้ขอให้สังเกตจุดที่ LLM หยุดด้วยตัวเอง TASK_009 อยู่ในสถานะ 'in review' และ LLM ไม่ฟันธงว่ามันคือ "ยังไม่เสร็จ" แต่กลับย้อนถามคนแทน MCP ดึงข้อมูลมาได้อย่างแม่นยำก็จริง แต่ "จะนับ in review ว่ายังไม่เสร็จหรือไม่" เป็นเรื่องที่ขึ้นอยู่กับนิยามของทีม ถ้า LLM ตัดสินเรื่องนี้ตามใจชอบเอง นั่นต่างหากที่จะกลายเป็นอุบัติเหตุ
③ การตรวจสอบและใช้สิทธิ์ยับยั้งของผม
ผมเปิดเครื่องมือทำงานร่วมกันขึ้นมาตรวจดูเฉพาะการ์ด TASK_017 ใบเดียว กำหนดส่งเลยมาแล้วจริง ๆ แต่ตามเกณฑ์ของทีมเรา TASK_009 ที่เป็น 'in review' ก็นับเป็นยังไม่เสร็จด้วย การจัดประเภทของ LLM ต่างจากกฎของเรา ผมจึงใช้สิทธิ์ยับยั้งและให้เกณฑ์ใหม่อีกครั้ง
④ ขอใหม่อีกครั้ง
ทีมเรานับ 'in review' เป็นยังไม่เสร็จด้วย ช่วยจัดใหม่ตามเกณฑ์นั้นที
และต่อจากนี้ให้ถือว่า 'in review' = ยังไม่เสร็จด้วย
ในผลลัพธ์หลังจากนั้น TASK_009 ได้เข้ามาอยู่ในลำดับที่ 2 ประโยคสุดท้าย ("ต่อจากนี้ให้ถือว่า \~ ด้วย") มีผลเฉพาะในเซสชันนี้เท่านั้น หากไม่อยากต้องบอกกฎเดิมซ้ำทุกครั้ง ก็ป้อนนิยามนี้เป็น atom ลงในสล็อต shared ของ team_memory ไว้ แล้ว LLM จะนำไปใช้เองตั้งแต่เซสชันถัดไป (ดู §20.1·§20.2)
สิ่งที่การโต้ตอบไปกลับครั้งนี้แสดงให้เห็นนั้นชัดเจน MCP คือ เครื่องมือที่ดึงข้อมูลมาได้อย่างแม่นยำ ไม่ใช่ เครื่องมือที่ตัดสินใจแทน ข้อมูลเป็นอัตโนมัติ ส่วนนิยามนั้นคนเป็นผู้ให้ ทันทีที่เส้นแบ่งนั้นพร่าเลือน การทำงานอัตโนมัติก็กลายเป็นอุบัติเหตุ
ลำพังการดึงข้อมูลจากเครื่องมือทำงานร่วมกันอย่างเดียว ก็ไปได้ไม่ไกลกว่า "ค้นหาสะดวกขึ้นหน่อย" MCP จะกลายเป็นระบบทำงานร่วมกันก็ต่อเมื่อร้อยเครื่องมือหลายตัวเข้าเป็นกระแสเดียว ผมจะดูห้ารูปแบบที่รันจริงในโปรเจกต์ A ในเชิงกระแสการทำงาน
รูปแบบ 1 — รายงานอัตโนมัติ ทุกเช้าเวลา 9 นาฬิกา เมื่อสเกดูเลอร์ส่งทริกเกอร์ออกมา LLM จะดึงสถานะงานในเครื่องมือทำงานร่วมกัน, คอมมิต git และตัวชี้วัดบนแดชบอร์ดมาพร้อมกันในคราวเดียว แล้วเขียนรายงานประจำวัน ปลายทางของการส่งคือพอร์ทัลหรือแชตภายในทีม คำว่า "พอร์ทัล" ในที่นี้หมายถึงเว็บพอร์ทัลภายในองค์กรที่กล่าวถึงใน §20.3 เนื่องจาก server.py (FastAPI) เปิดอยู่ตลอดเวลา และ View_*.html ที่ Claude เขียนทำงานอยู่บนนั้น รายงานจึงวางทับลงไปเป็นหน้าหนึ่งของพอร์ทัลได้อย่างเป็นธรรมชาติ
รูปแบบ 2 — การตัดสินใจ → สร้างงานอัตโนมัติ เมื่อลงทะเบียนการ์ดการตัดสินใจ (proposal P####) รายการ implementation และ verification ของการ์ดก็จะกลายเป็นงานในเครื่องมือทำงานร่วมกันทั้งดุ้น ผู้รับผิดชอบและกำหนดส่งก็จะถูกป้อนให้โดยอัตโนมัติ สิ่งที่ตกลงกันในที่ประชุมว่า "งั้นตกลงทำแบบนี้นะ" จะตกลงไปเป็นการ์ดบนบอร์ดโดยไม่ต้องผ่านมือคน
รูปแบบ 3 — งาน → อ้างอิงย้อนกลับไปยังการ์ดการตัดสินใจ เป็นทิศตรงข้ามของรูปแบบ 2 เมื่องานในเครื่องมือทำงานร่วมกันเสร็จสิ้น MCP จะอัปเดต execution_log ของการ์ดการตัดสินใจต้นเรื่อง "การตัดสินใจนี้ถูกลงมือทำจริงหรือไม่" จึงถูกติดตามโดยอัตโนมัติ การตัดสินใจกับการลงมือทำผูกเข้าด้วยกันแบบสองทิศทาง (เส้นประในแผนภาพ)
รูปแบบ 4 — วิเคราะห์ความคืบหน้า ปลายไตรมาส ดึงงานทั้งหมดของไตรมาสนั้นออกมาในคราวเดียวเพื่อวิเคราะห์รูปแบบความล่าช้า "งานประเภทไหนที่ถูกเลื่อนซ้ำ ๆ" กลายเป็นข้อมูลป้อนเข้าของการทบทวน
รูปแบบ 5 — เอกสารเตรียมมีตติ้ง 1:1 5 นาทีก่อนมีตติ้ง 1:1 ระบบจะสังเคราะห์งานในเครื่องมือทำงานร่วมกันของสมาชิกคนนั้น, สล็อต team_memory และกิจกรรมล่าสุดเข้าด้วยกันเพื่อสร้างสรุปเตรียมการ แทนที่มีตติ้ง 1:1 จะเสีย 5 นาทีไปกับ "คราวที่แล้วกำลังทำอะไรอยู่นะ" ก็เริ่มเข้าเนื้อหาหลักได้เลย
จุดร่วมของทั้งห้ารูปแบบมีอยู่หนึ่งเดียว ค่าจะถูกเก็บคืนก็เฉพาะในจุดที่ลดงานซ้ำซากที่เหลืออยู่บนมือคนเท่านั้น รูปแบบที่ติดตั้งเพื่อให้ดูเท่ มีแต่จะเพิ่มภาระการดูแลระบบเปล่า ๆ
การเชื่อมเซิร์ฟเวอร์ MCP ห้าตัวให้หมดในวันแรกคือความล้มเหลวที่พบบ่อยที่สุด มันมีลำดับอยู่
| ขั้น | ทำอะไร | ความรู้สึกเรื่องระยะเวลา |
|---|---|---|
| 1 | ติดตั้งเซิร์ฟเวอร์ MCP 1 ตัว (ClickUp หรือ JIRA) | 1\~2 วัน |
| 2 | ทดลองรูปแบบ 1 รูปแบบ (รายงานอัตโนมัติ) | ประมาณ 1 สัปดาห์ |
| 3 | เดินงานทั้ง 5 รูปแบบ | 1\~2 เดือน |
| 4 | พัฒนาเซิร์ฟเวอร์ MCP เอง (เมื่อจำเป็นต้องใช้เครื่องมือเฉพาะทาง) | 1\~3 เดือน |
ระยะเวลาข้างต้นเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) โดยอิงทีมขนาดกลาง (10\~50 คน) ของโปรเจกต์ A และจะต่างกันไปตามขนาดทีมและความคุ้นเคยกับเครื่องมือ สิ่งที่ชัดเจนคือ ทีมส่วนใหญ่ขั้น 1\~3 ก็เพียงพอแล้ว ขั้น 4 (พัฒนาเซิร์ฟเวอร์เอง) จะไปก็เฉพาะเมื่อจำเป็นต้องต่อเครื่องมือภายในองค์กรเฉพาะทางที่ไม่มีเซิร์ฟเวอร์ MCP ในตลาดเท่านั้น ถึงไม่ไปไกลขนาดนั้น ผลส่วนใหญ่ก็ถูกเก็บคืนได้แล้ว
มีเหตุผลที่ต้องกล่าวถึง JIRA แยกต่างหาก หาก ClickUp คือบอร์ดภายในทีม JIRA มักเป็นเครื่องมือที่ แชร์ร่วมกับองค์กรภายนอก อย่างพับลิชเชอร์หรือบริษัทรับเหมาช่วง เมื่อต่อ JIRA MCP เข้าไป ก็จะดึงความคืบหน้าของบอร์ดฝั่งรับเหมาช่วงมาโดยอัตโนมัติก่อนการประชุมทุกสัปดาห์ และคัดงานที่สงสัยว่าจะล่าช้าออกมาเตรียมไว้ล่วงหน้าได้ การประชุมก็จะเริ่มจาก "การตัดสินใจ" แทนที่จะเป็น "การแชร์สถานการณ์ปัจจุบัน" อย่างไรก็ตาม ยิ่งเป็นเครื่องมือที่แชร์กับภายนอกมากเท่าไร กับดักเรื่องสิทธิ์และการรั่วไหลในหัวข้อถัดไปก็ยิ่งหนักหน่วงขึ้นเท่านั้น
MCP คือการยื่นเครื่องมือให้อยู่ในมือ LLM ถ้าสิ่งที่ถืออยู่ในมือเป็นมีด มันก็บาดได้เช่นกัน
กับดักที่ 1 — อุบัติเหตุเรื่องสิทธิ์ หาก LLM ถือสิทธิ์ write ไว้ด้วย การเปลี่ยนแปลงที่ไม่ได้ตั้งใจก็จะเกิดขึ้น ในลักษณะที่คำสั่งเดียวว่า "จัดให้หน่อย" ก็เปลี่ยนสถานะงานยกพวงไปเลย วิธีรับมือชัดเจน เริ่มต้นเซิร์ฟเวอร์ MCP แบบ read-only ส่วน write เปิดเฉพาะที่จำเป็นจริง ๆ อย่างรูปแบบ 2·3 และต้องมีด่านยืนยันก่อนลงมือด้วย ที่ LLM ย้อนถามคนเรื่องการจัดประเภท 'in review' ในบันทึกเซสชันก่อนหน้านี้ ก็เป็นจิตวิญญาณเดียวกัน — เมื่อคลุมเครือก็หยุด
กับดักที่ 2 — ข้อมูลรั่วไหล ข้อมูลบริษัทที่ดึงมาผ่าน MCP ถูกส่งไปยัง LLM API ภายนอก ตัวเลขบาลานซ์, เนื้อหาที่ยังไม่เปิดเผย และตัวชี้วัดรายได้อาจไหลออกไปทั้งดุ้น วิธีรับมือคือ สำหรับข้อมูลอ่อนไหวให้ใช้ LLM ที่โฮสต์เองเฉพาะส่วนนั้น หรือแทนที่ด้วย placeholder ที่ระดับเซิร์ฟเวอร์ MCP ก่อนส่งออก (เป็นหลักการเดียวกับการปกป้อง IP ที่ครอบคลุมตลอดทั้งเล่ม)
กับดักที่ 3 — การพึ่งพิงระเบิด หากผูกเซิร์ฟเวอร์ MCP 5 ตัวไว้บนเส้นทาง critical พอที่หนึ่งล่ม รายงานเช้าทั้งฉบับก็หยุดไปด้วย วิธีรับมือคือ วางเฉพาะตัวหลัก 1\~2 ตัวไว้เป็น critical แล้วแยกที่เหลือออกเป็นตัวเสริม หากเซิร์ฟเวอร์เสริมล่ม ก็จะว่างเฉพาะรายการนั้น แต่ตัวรายงานเองยังออกมาได้
กับดักที่ 4 — ค่า API ระเบิด การเรียก MCP ครั้งหนึ่งเผาทั้งโทเค็นของ LLM และการเรียก API ภายนอกไปพร้อมกัน หากรันรายงานอัตโนมัติทุก 5 นาที ค่าใช้จ่ายก็จะพอกพูนขึ้นอย่างเงียบ ๆ วิธีรับมือคือ ตั้ง cap กับความถี่ในการเรียก และทำแคชผลการดึงข้อมูลที่ไม่ค่อยเปลี่ยนแปลง
หากมัดกับดักทั้งสี่เป็นบรรทัดเดียว ตำแหน่งที่ปลอดภัยของ MCP คือ "เริ่มต้นแบบ read-only, วางเฉพาะตัวหลักเป็น critical, กรองข้อมูลอ่อนไหวออก และตั้งเพดานกับการเรียก"
นี่คือความเปลี่ยนแปลงที่สัมผัสได้ก่อนและหลังการใช้งาน MCP ในโปรเจกต์ A ตัวเลขเวลาด้านล่างเป็นการประมาณจากประสบการณ์ของผู้เขียน (ยังไม่ได้ตรวจสอบ) จึงขอให้อ่านเป็น ทิศทางและสัดส่วน ไม่ใช่ค่าสัมบูรณ์
| รายการ | ไม่มี MCP | ใช้งาน MCP | ทิศทาง |
|---|---|---|---|
| รวบรวมข้อมูลรายงานประจำวัน | ทำมือ 30\~60 นาที | อัตโนมัติ \~5 นาที | ลดลงมาก |
| ซิงค์ข้อมูลระหว่างเครื่องมือ | คนทำมือ | อัตโนมัติ | ตัดงานทำมือออก |
| เตรียม 1:1 ล่วงหน้า | 10\~15 นาที | สรุปอัตโนมัติ \~3 นาที | ลดลง |
| รับรู้ความคืบหน้าฝั่งรับเหมาช่วง | ผ่านการประชุมเท่านั้น | ดูได้แบบเรียลไทม์ | ดูได้ตลอดเวลา |
| เชื่อม การตัดสินใจ ↔ งาน | ทำมือ | อัตโนมัติสองทิศทาง | ป้องกันการตกหล่น |
การเก็บคืนที่ใหญ่ที่สุดเกิดขึ้นจาก "เวลาในการรวบรวมข้อมูล" การตัดสินใจและการพินิจยังคงเป็นงานของคน สิ่งที่ MCP ช่วยลดคือส่วนหน้าก่อนหน้านั้น คืองานมือซ้ำซากที่ต้องไล่ค้นเครื่องมือกระจัดกระจายมารวมไว้ที่เดียว สามวินาทีในตอนต้นบทนี้คือจุดนั้นพอดิบพอดี
ส่วนที่ 20 ได้ก่อระบบทำงานร่วมกันของทีมขึ้นเป็นสี่ชั้น
| บท | แก่นสำคัญ |
|---|---|
| 20.1 | การบริหาร atom — จัดประเภทตามหมวด, จัดระเบียบรายไตรมาส |
| 20.2 | เมโมรีของสมาชิกทีม — สล็อต 5 คนของ team_memory (leeminsoo·สมาชิก A/b/c·shared), แยกของส่วนรวมกับของส่วนตัว |
| 20.3 | เว็บพอร์ทัล — เปิดทำงานตลอดด้วย server.py (FastAPI)·build_index.py·nginx·nssm, View_*.html ทำงาน |
| 20.4 | MCP — 5 รูปแบบ·การนำมาใช้อย่างเป็นขั้น·สิทธิ์มาก่อน |
ทั้งสี่บทไม่ใช่เครื่องมือที่ต่างคนต่างอยู่ แต่เป็นกายเดียวกัน พอร์ทัล (20.3) คือที่ที่รายงานวางทับลงไป, atom และเมโมรี (20.1·20.2) คือที่ที่ LLM จดจำนิยามของทีมอย่าง 'in review = ยังไม่เสร็จ' และ MCP (20.4) คือสายเดินที่เชื่อมทั้งหมดนั้นเข้ากับเครื่องมือทำงานร่วมกันและเอกสาร หากถอดอย่างใดอย่างหนึ่งออกไป ค่าของส่วนที่เหลือก็จะลดลงครึ่งหนึ่ง
ในส่วนถัดไปจะว่าด้วยการกำกับดูแล (governance) และการดำเนินงาน เมื่อต่อเครื่องมือมามากขนาดนี้ ปัญหาเรื่องความปลอดภัย·ต้นทุน·ลิขสิทธิ์·จริยธรรมที่ตามมาด้วย จะถูกยกระดับกับดักในบทนี้ขึ้นเป็นกฎในระดับทีมแล้วจัดระเบียบให้
setup 1. ติดตั้งเซิร์ฟเวอร์ ClickUp MCP (ทางการหรือของชุมชน) แล้วออกโทเค็น API ของ ClickUp 2. ลงทะเบียนเซิร์ฟเวอร์ MCP ในการตั้งค่าของ Claude Code แต่เริ่มต้น ด้วยสโคป read-only เท่านั้น 3. ตรวจสอบ ID ของเวิร์กสเปซเพื่อจำกัดขอบเขตการดึงข้อมูลไว้ที่บอร์ดของตนเอง
prompt
ช่วยจัดงาน P0 ที่ยังไม่เสร็จและครบกำหนดส่งในสัปดาห์นี้จาก ClickUp ให้หน่อย เรียงตามกำหนดส่งที่ใกล้เข้ามาที่สุด
ใส่ผู้รับผิดชอบและจำนวนวันที่เหลือจนถึงกำหนดส่ง ส่วนงานที่เลยกำหนดส่งแล้วให้ดึงขึ้นบนสุด
verify
1. เปิดงาน 1 รายการจากที่ผลลัพธ์แสดงออกมาใน ClickUp โดยตรง แล้วเทียบดูว่ากำหนดส่งและผู้รับผิดชอบถูกต้องหรือไม่
2. ดูว่า LLM ย้อนถามด้วยตัวเองในรายการที่คลุมเครือหรือไม่ — ถ้าฟันธงโดยไม่ย้อนถาม ให้ระบุเกณฑ์การจัดประเภทแล้วสั่งใหม่
3. นิยามที่ใช้บ่อย (เช่น 'in review = ยังไม่เสร็จ') ให้ป้อนเป็น atom ลงใน shared ของ team_memory เพื่อให้นำไปใช้โดยอัตโนมัติในเซสชันถัดไป
หากคุณเป็นนักพัฒนาคนเดียวที่ไม่มีทั้งทีมและเครื่องมือทำงานร่วมกัน เป้าหมายแรกของ MCP คือ GitHub และเอกสารในเครื่อง ลองต่อ GitHub MCP แบบ read-only แล้วเริ่มจากการดึงข้อมูลอย่าง "issue ที่ยังไม่ปิดของสัปดาห์นี้ เรียงจากเก่าสุด" และแทนที่จะใช้การ์ดการตัดสินใจ ก็ให้ดึงไฟล์มาร์กดาวน์ในโฟลเดอร์ decisions/ ในเครื่องผ่านเอกสาร MCP แทน รายงานอัตโนมัติ (รูปแบบ 1) ใช้ได้เหมือนเดิม — แค่เปลี่ยนปลายทางการส่งจากแชตภายในทีมเป็นไฟล์โน้ตของตัวเองก็พอ กับดักเรื่องสิทธิ์และต้นทุนจะเกิดขึ้นเหมือนกันแม้จะเป็นคนเดียว ดังนั้นการเริ่มต้นแบบ read-only และตั้ง cap กับการเรียก ควรทำตั้งแต่แรก
เย็นวันศุกร์ 6 โมง 40 นาที ขณะที่กำลังจะปิดโน้ตบุ๊กเพื่อเลิกงาน งานที่ทำในวันนั้นกลับให้ความรู้สึกคุ้นเคยอยู่ลึก ๆ ผมเพิ่งไล่หาและแก้การอ้างอิง enum ที่เสียในชีตข้อมูล ทั้งที่สัปดาห์ที่แล้วก็แก้สิ่งเดียวกันนี้ชัด ๆ สัปดาห์ก่อนหน้านั้นก็ด้วย ทุกครั้งผมพิมพ์พรอมต์เดิมซ้ำใหม่ และทุกครั้งก็ตรวจสอบรายการเดิมในผลลัพธ์ของ Claude ผมรู้ตัวว่านี่เป็นครั้งที่สามตอนที่กำลังจะปิดโน้ตบุ๊กพอดี
ความรู้สึกที่ว่า "คุ้นเคยอยู่ลึก ๆ" นี้คือช่วงเวลาที่สำคัญที่สุดในหนังสือทั้งเล่ม หากปล่อยความรู้สึกนี้ผ่านเลยไป สัปดาห์หน้าก็จะทำงานเดิมซ้ำเป็นครั้งที่สี่ แต่หากคว้าความรู้สึกนี้ไว้แล้วจดเป็นบรรทัดเดียว บรรทัดนั้นจะกลายเป็น skill ในสัปดาห์ถัดไป และ skill นั้นจะกลายเป็น atom ที่ถูกฉีดเข้ามาโดยอัตโนมัติในอีกหนึ่งเดือนถัดมา ที่ที่เราคว้ามันไว้ก็คือการทบทวนนั่นเอง
ส่วนอื่น ๆ ในหนังสือเล่มนี้กล่าวถึง "มีเครื่องมือแบบนี้อยู่" และ "มีแพตเทิร์นแบบนี้อยู่" แต่บทนี้กล่าวถึงว่าเครื่องมือและแพตเทิร์นทั้งหมดนั้นถือกำเนิดมาจากที่ใด คำสั่งสแลช (slash command) ใหม่ถูกสร้างขึ้นที่ไหน atom ใหม่ถูกตรึงเป็นกฎอย่างไร และเครื่องมือที่ไม่ได้ใช้แม้แต่ครั้งเดียวในหนึ่งเดือนถูกใครคัดออก คำตอบล้วนรวมอยู่ที่จุดเดียวกันเสมอ นั่นคือการทบทวน
การทบทวนไม่ใช่พิธีกรรมที่ยิ่งใหญ่อะไร ลองเปิดไฟล์การทบทวนรายวันไฟล์หนึ่งของผู้เขียนดูตามตรง พาธจริงคือ retro/daily/YYYY-MM-DD.md และเมื่อผ่านไปหนึ่งเดือน ไฟล์เหล่านี้จะถูกบีบอัดเป็นรายสัปดาห์ (retro/weekly/YYYY-Www.md) แล้วบีบอัดต่อเป็นรายเดือน (retro/YYYY-MM.md) อีกที กฎต่าง ๆ ระบุไว้ใน retro/README.md
กระบวนการที่ผู้เขียนใช้จริงไม่ใช่การนั่งเขียนไฟล์การทบทวนด้วยมือเอง แต่คือการโยน log งานของวันนั้นให้ AI แล้วให้มันดึงแพตเทิร์นออกมา ด้านล่างนี้คือการเรียบเรียงใหม่ (reconstruction) ของการโต้ตอบไปกลับจริงในเย็นวันศุกร์วันหนึ่ง ผมจะใส่พรอมต์ฉบับเต็มก่อน แล้วจึงใส่ผลลัพธ์ดิบ (raw output) ของ Claude มาทั้งหมดโดยไม่สรุปย่อ — รวมถึงส่วนที่มันตอบพลาดด้วย
[พรอมต์ — มนุษย์เป็นผู้ป้อน]
ช่วยสรุปงานที่ทำวันนี้เป็นการทบทวนให้หน่อย สิ่งที่ทำวันนี้: ① แก้การอ้างอิง enum ที่เสียในชีตข้อมูลตัวละคร (คอลัมน์ grade ไม่ตรงกับชีต enum) ② ปรับโทนบทพูดของ NPC ให้เป็นอันหนึ่งอันเดียวกัน ③ รัน relation-map ใหม่อีกครั้ง ในจำนวนนี้ ถ้ามี งานที่ทำซ้ำตั้งแต่ 2 ครั้งขึ้นไปในสัปดาห์นี้ ให้ทำเครื่องหมายไว้ และถ้ามี ตัวเลือกที่น่าจะทำเป็นเครื่องมือได้ ให้แยกออกมาต่างหาก ถ้าไม่มีก็บอกว่าไม่มี อย่าฝืนสร้างขึ้นมา
[ผลลัพธ์ดิบของ Claude — ไม่สรุปย่อ ไม่ดัดแปลง]
นี่คือการทบทวนงานของวันนี้ครับ
ตรวจพบแพตเทิร์นที่ทำซ้ำ - ① การแก้การอ้างอิง enum ที่เสีย → รายการนี้ปรากฏเหมือนกันในการทบทวนของวันอังคารในสัปดาห์นี้ด้วย ขอทำเครื่องหมายว่า ทำซ้ำตั้งแต่ 2 ครั้งขึ้นไป - ② การปรับโทนบทพูดของ NPC ให้เป็นอันหนึ่งอันเดียวกัน → ปรากฏครั้งแรกในสัปดาห์นี้ ไม่ใช่การทำซ้ำ - ③ การรัน relation-map ใหม่ → คุณใช้ skill ชื่อ relation-map-gen อยู่แล้ว ดังนั้นงานนี้เป็นงานที่ทำเป็นเครื่องมือเสร็จเรียบร้อยแล้ว ไม่ใช่ตัวเลือกเพิ่มเติม
ตัวเลือกเครื่องมือ - การตรวจสอบการอ้างอิง enum น่าจะทำเป็น skill ได้ ในรูปแบบที่นำค่าคอลัมน์ของชีตข้อมูลมาเทียบกับชีต enum แล้วรายงานจุดที่ไม่ตรงกัน - แต่เนื่องจากผมไม่ทราบโครงสร้างที่แน่ชัดของชีตทั้งสอง จึงยากที่จะมั่นใจว่านี่เป็นแพตเทิร์นที่ทำซ้ำได้จริงหรือไม่ จำเป็นต้องตรวจสอบว่าการเสียที่แก้ในวันอังคารกับการเสียในวันนี้เป็น การเสียชนิดเดียวกัน หรือไม่ ถ้าเสียที่คอลัมน์เดียวกันด้วยเหตุผลเดียวกัน ก็เป็นตัวเลือก skill ที่หนักแน่นมาก แต่ถ้าทุกครั้งเสียที่คอลัมน์ต่างกันด้วยเหตุผลต่างกัน ก็ยากที่จะรวบเป็น skill เดียว
ย่อหน้าสุดท้ายตรงนี้คือหัวใจสำคัญ AI ไม่ได้ฟันธงว่า "มาสร้าง skill กันเถอะ" แต่ ย้อนถามให้มนุษย์ตรวจสอบว่าเป็นการเสียชนิดเดียวกันหรือไม่ นี่คือเหตุผลที่ทำไมการทบทวนจึงไม่ใช่สิ่งที่ทำได้อัตโนมัติ
[การตรวจสอบโดยมนุษย์ — รับข้อเสนอจาก AI แต่กรองด้วย]
ผู้เขียนเปิดการทบทวนของวันอังคารขึ้นมาเทียบ การเสียในวันอังคารคือคอลัมน์ grade การเสียในวันนี้ก็คอลัมน์ grade เช่นกัน เป็น ชนิดเดียวกัน ข้อเสนอตัวเลือกของ AI ผ่านการตรวจสอบ ผมจึงจดบรรทัดหนึ่งไว้ในการทบทวนรายวัน
งานที่ทำซ้ำ: การตรวจสอบการอ้างอิง enum-grade (อังคาร·ศุกร์ 2 ครั้ง) → ตัวเลือก skill ตัดสินการเลื่อนขั้นในการทบทวนรายสัปดาห์ครั้งหน้า
บรรทัดเดียวนี้คือทั้งหมด ใช้เวลาไม่ถึง 5 นาที และบรรทัดเดียวนี้คือก้าวแรกของลูป self-improving หาก AI เสนอตัวเลือก ③ ที่มันชี้ว่า "ทำเป็นเครื่องมือเสร็จแล้ว" ขึ้นมาด้วย อีกหนึ่งเดือนถัดมาก็จะมีเครื่องมือซ้ำซ้อนที่ไม่ได้ใช้ลอยเพิ่มขึ้นมาอีกหนึ่งตัว ผลจากการที่ทั้งการกรองของ AI และการกรองของมนุษย์ทำงานทั้งคู่ จึงเหลือเพียงตัวเลือกที่แท้จริงเพียงหนึ่งเดียว
งานใดที่ทำซ้ำ เครื่องมือใดที่ถูกใช้บ่อย atom ใดที่ยังขาด — สิ่งเหล่านี้มองไม่เห็นจากการทำงานเพียงครั้งเดียว ในบันทึกเซสชันข้างต้น การเสีย enum ผุดขึ้นมาเป็นตัวเลือกได้ก็เพราะนำ "วันอังคารกับวันนี้" มาซ้อนทับกันดู ไม่ใช่เพราะ "วันนี้" เพียงวันเดียว ต้องนำร่องรอยที่สะสมมา 1 สัปดาห์·1 เดือน·1 ไตรมาส มาซ้อนทับกัน แพตเทิร์นจึงจะผุดขึ้น การทบทวนคือเวลาที่สร้างการซ้อนทับนั้นขึ้นมาอย่างจงใจ
แพตเทิร์นที่ค้นพบในการทบทวนแยกออกเป็นสองทาง
การตัดสินใจแยกทางนี้ทำไม่ได้ระหว่างกลางงาน เพราะมันจะตัดกระแสการทำงานให้ขาด ในขณะที่กำลังแก้การเสีย enum อยู่นั้น ไม่มีเวลาพอที่จะมานั่งคิดว่า "นี่ครั้งที่สามแล้วหรือเปล่า" เวลาทบทวนที่แยกออกมาต่างหากคือที่สำหรับการตัดสินใจนั้น
หลังจากสร้างเครื่องมือขึ้นมาแล้ว มันให้คุณค่าจริงหรือไม่ ก็วัดที่จุดเดียวกันนี้ เครื่องมือที่ใช้เดือนละครั้งกับเครื่องมือที่ช่วยลดเวลาลงได้หนึ่งชั่วโมงมีคุณค่าต่างกัน ทั้งการวัดและการตัดสินใจทิ้งล้วนทำในการทบทวน หากไม่มีการทบทวน เครื่องมือก็จะมีแต่สะสมเพิ่มขึ้นโดยไม่ถูกจัดระเบียบ เมื่อผ่านไปหลายปี เครื่องมือที่ไม่ได้ใช้หลายสิบตัวจะกลายเป็นอุปสรรคต่อการค้นหาและการใช้งาน
หากเปรียบกับลิ้นชัก การทบทวนก็คือเวลาที่เราเทลิ้นชักโต๊ะทำงานออกให้ว่างเป็นระยะ ๆ หากปากกาที่ใช้ทุกวันกับกระดาษโน้ตที่ไม่เคยหยิบออกมาเลยตลอดทั้งปีปะปนกันอยู่ในช่องเดียวกัน ทุกครั้งที่หาปากกาก็จะเสียเวลาเพิ่มขึ้นอีกไม่กี่วินาที เครื่องมือก็เป็นเช่นเดียวกัน
การทบทวนของผู้เขียนหมุนอยู่บนสามชั้น รายวันรวบรวมเมล็ดพันธุ์ของแพตเทิร์น รายสัปดาห์มัดเมล็ดพันธุ์เข้าด้วยกันแล้วบีบอัดเป็นตัวเลือกเครื่องมือ และรายเดือนประเมินความคุ้มค่าทางเศรษฐกิจของเครื่องมือเพื่อตรึงเป็นสินทรัพย์หรือทิ้งไป แต่ละชั้นรับผลลัพธ์ของชั้นล่างมาเป็นอินพุต
flowchart TD
W["ลงมือทำงาน
(ชีตข้อมูล·เอกสารออกแบบ·ระบบอัตโนมัติ)"] -->|สะสมข้อมูล| D["การทบทวนรายวัน
retro/daily/*.md
5~10 นาที · เมล็ดพันธุ์แพตเทิร์น 1~3 รายการ"]
D -->|บีบอัดรายวัน 5 รายการ| WK["การทบทวนรายสัปดาห์
retro/weekly/*.md
30~60 นาที · ตัดสินตัวเลือกเครื่องมือ"]
WK -->|สังเคราะห์รายสัปดาห์ 4 รายการ| M["การทบทวนรายเดือน
retro/YYYY-MM.md
1.5~2 ชั่วโมง · ความคุ้มค่า·การทิ้ง"]
M -->|เลื่อนขั้นแพตเทิร์นที่ผ่านการตรวจสอบ| A["แปรเป็นสินทรัพย์ถาวร
feedback.md / workflows.md
+ ลงทะเบียน atom"]
A -->|JIT hook ฉีดอัตโนมัติ| W
style D fill:#e3f2fd,stroke:#1565c0
style WK fill:#e8f5e9,stroke:#2e7d32
style M fill:#fff3e0,stroke:#ef6c00
style A fill:#f3e5f5,stroke:#6a1b9a
ลูกศรสุดท้ายคือสิ่งที่ปิดลูป แพตเทิร์นที่ถูกแปรเป็นสินทรัพย์ถาวรจะถูกฉีดเข้าสู่งานครั้งถัดไปโดยอัตโนมัติผ่าน JIT (Just-In-Time) hook ในสภาพแวดล้อมของผู้เขียน มี hook ชื่อ inject_memory.py ที่จะเลือก atom ที่เกี่ยวข้องใส่เข้ามาทุกครั้งที่รับอินพุตจากผู้ใช้ เมื่อการตรวจสอบ enum-grade ถูกตรึงเป็น atom แล้ว ครั้งต่อไปเมื่อป้อนอินพุตประเภท "การตรวจสอบชีตข้อมูล" atom นั้นก็จะตามมาเอง มนุษย์ไม่จำเป็นต้องคอยจำว่า "อ้อ มีกฎตรวจสอบนั้นอยู่นี่นา" ในทุกครั้งอีกต่อไป
หากตัดการทบทวนออกไป จะเหลือแต่ลูกศรที่วิ่งจากบนลงล่าง และลูกศรสุดท้ายที่ส่งสินทรัพย์กลับคืนสู่งานจะขาดไป ลูปจะไม่ปิด ความหมายของคำว่า self-improving ก็คือการที่ลูปนี้กำลังหมุนอยู่นั่นเอง เครื่องมือปรับปรุงตัวเครื่องมือเอง และ atom เพิ่ม atom ขึ้นเอง พลังขับเคลื่อนของมันคือเวลาทบทวนหนึ่งชั่วโมงที่มนุษย์แยกออกมาต่างหาก
จากความรู้สึกของผู้เขียนที่หมุนการทบทวนในโปรเจกต์ MMORPG โปรเจกต์หนึ่งมาราวครึ่งปี ในการทบทวนหนึ่งครั้งจะมีผลผลิตห้าประเภทต่อไปนี้ถือกำเนิดขึ้น ความถี่ด้านล่างนี้ไม่ใช่สถิติที่แม่นยำ แต่เป็นความรู้สึกจากการใช้งานจริงของผู้เขียน (การประมาณของผู้เขียน · ยังไม่ได้ตรวจสอบ) และการทบทวนแต่ละครั้งก็ไม่ได้สร้างทั้งห้าประเภทเสมอไป แต่หากมองในระดับไตรมาส ทั้งห้าก็จะปรากฏออกมาอย่างละหนึ่งครั้ง
เมื่อขยายความทั้งห้าประเภทออกมาก็เป็นดังนี้ หากในสัปดาห์นี้ตัดสินใจแบบเดียวกันซ้ำตั้งแต่ 2 ครั้งขึ้นไป นั่นคือ ตัวเลือก atom ใหม่ หากป้อนแพตเทิร์นพรอมต์แบบเดียวกันซ้ำหลายครั้งในหนึ่งสัปดาห์ นั่นคือ ตัวเลือก skill ใหม่ (การตรวจสอบ enum-grade ในหัวข้อก่อนหน้าก็เป็นกรณีนี้) หากในบรรดา skill ที่ใช้ในสัปดาห์นี้มีตัวที่ให้ผลลัพธ์ไม่น่าพอใจ นั่นคือ การปรับปรุง skill เดิม — ปรับพรอมต์·เพิ่มการตรวจสอบ·ทำให้อินพุตเป็นมาตรฐาน ในบรรดา atom ที่สร้างขึ้นเมื่อไตรมาสที่แล้ว ตัวที่ไม่เคยจับคู่ (match) เลยตลอดหนึ่งเดือนคือ ตัวเลือกที่จะทิ้ง หากไม่ใช้ก็มีแต่จะกินโทเค็นเปล่า ๆ และสุดท้าย การประเมินความคุ้มค่าทางเศรษฐกิจ คือการเทียบความถี่การใช้งานกับแรงมือที่ประหยัดได้ของเครื่องมือแต่ละตัว เพื่อตัดสินว่าจะคงไว้·ปรับปรุง·หรือทิ้ง
ทั้งห้านี้ถูกจัดวางบนหน้าจอเดียวอย่างไร เมื่อดูเป็นเมทริกซ์ก็เป็นดังนี้ แกนนอนคือ "ทำซ้ำหรือไม่" แกนตั้งคือ "มีคุณค่าหรือไม่"
สิ่งที่การทบทวนทำในที่สุดก็คือการโปรยงานต่าง ๆ ของสัปดาห์นั้นลงบนสี่ช่องนี้ สิ่งที่ตกลงมุมขวาบนจะกลายเป็นเครื่องมือ และสิ่งที่ตกมุมขวาล่างจะถูกคัดออก การจำแนกนี้คือวิธีการทำงานจริงของ self-improving
ตัวเลือกการตรวจสอบ enum-grade จากหัวข้อก่อนหน้าถูกเลื่อนขั้นเป็น skill แล้วจากนั้นถูกตรึงเป็น atom อีกที ลองตามกระบวนการนี้ไปจนจบ atom คือรูปแบบที่แพตเทิร์นซึ่งถูกค้นพบอย่างหยาบ ๆ ในการทบทวนได้กลายเป็นสินทรัพย์ถาวรหลังผ่านการตรวจสอบแล้ว
ในเมโมรีของผู้เขียนมี atom ที่ถูกตรึงเป็นกฎเช่นนั้นอยู่แล้วหลายตัว หนึ่งในนั้นคือ retro_atom_natural_invitation ตามชื่อของมัน นี่คือ atom ที่บรรจุหลักการว่า "ในการทบทวน atom ปรากฏขึ้นในฐานะคำเชิญชวนที่เป็นธรรมชาติ ไม่ใช่คำสั่ง" ตัว atom นี้เองก็เป็นเมตาแพตเทิร์นที่ค้นพบจากการหมุนการทบทวนซ้ำหลายครั้ง — หลังจากที่ได้ประสบหลายครั้งว่า หากระหว่างการทบทวนเรากดดันบังคับตัวเองให้ตรึงเป็นกฎอย่างหมกมุ่นว่า "อันนี้ต้องเก็บไว้เป็น atom นะ" การทบทวนกลับจะกลายเป็นเพียงพิธีการ มันจึงตกผลึกเป็นบรรทัดเดียวนี้ในที่สุด
การตรึงเป็นกฎให้ผลจริงหรือไม่นั้นถูกจัดการด้วยคะแนนด้วยเช่นกัน ในสภาพแวดล้อมของผู้เขียนมีสคริปต์ชื่อ atom_score.py ที่ให้คะแนนว่าแต่ละ atom ถูกจับคู่และถูกใช้จริงมากน้อยเพียงใด ผลลัพธ์จะถูกบันทึกไว้ใน _scores_latest.json และ atom ที่มีคะแนนเกินระดับหนึ่งจะถูกฉีดเข้าสู่ CLAUDE.md โดยอัตโนมัติ กล่าวคือ atom ที่ถูกใช้บ่อยยิ่งจะยิ่งปรากฏต่อหน้าบ่อยขึ้น ส่วน atom ที่ไม่ถูกใช้จะถูกหักคะแนนและไหลไปสู่การเป็นตัวเลือกที่จะทิ้ง วงจรการให้คะแนน-การฉีดนี้คือส่วนที่ทำให้สี่ช่องในหัวข้อ §1.4 เป็นอัตโนมัติ
ตรงนี้มีสิ่งหนึ่งที่ต้องชี้ให้ตรงไปตรงมา คะแนนนี้ไม่ได้แปลงเป็นตัวชี้วัดเชิงปริมาณอย่าง "ประหยัดไปได้ 30 ชั่วโมงต่อเดือน" ในทันที เวลาที่ atom ประหยัดได้นั้นวัดได้ยาก ดังนั้นแทนที่จะฟันธง ROI (Return on Investment, ผลตอบแทนเทียบกับการลงทุน) ออกมาเป็นตัวเลข การพูดเพียงในแง่ของทิศทางและสัดส่วนว่า "atom ที่ถูกใช้บ่อยจะมีคะแนนสูง และ atom ที่คะแนนสูงจะถูกฉีดบ่อยขึ้นจึงช่วยลดแรงมือ" นั้นตรงไปตรงมากว่า — เป็นทิศทาง ไม่ใช่ตัวคูณ
ภาพที่พบเห็นได้บ่อยของทีมที่ไม่ได้แยกเวลาทบทวนไว้เป็นดังนี้
ภาพเหล่านี้จะหายไปเกือบหมดด้วยการทบทวนเพียงหนึ่งชั่วโมง หากนึกถึงเย็นวันศุกร์ในหัวข้อ §1.1 มันเป็นเพียงความต่างระหว่างการจด "คุ้นเคยอยู่ลึก ๆ" เป็นบรรทัดเดียว กับการปล่อยมันผ่านเลยไปเท่านั้น การจดใช้เวลา 5 นาที แต่เวลาที่สูญไปเพราะไม่จดจะสะสมในการทำซ้ำครั้งที่สี่·ครั้งที่ห้า เป็นแบบฉบับของการไม่ลงเวลาเล็กน้อยแล้วต้องสูญเสียเวลาก้อนใหญ่กว่าไป
แน่นอนว่าไม่จำเป็นต้องตั้งระบบการทบทวนที่ยิ่งใหญ่ขึ้นมาตั้งแต่แรก ในทีมขนาดใหญ่ การเริ่มจากการทบทวนรายวัน 5 นาทีอาจรู้สึกอึดอัดน่ารำคาญ แต่หากวางระบบสามชั้นแบบรายวัน·รายสัปดาห์·รายเดือนลงไปทีเดียวตั้งแต่ต้น ก็ง่ายที่จะทำตามแค่รูปแบบจนพลาดเนื้อแท้ไป ลำดับที่ปลอดภัยคือให้คนที่ได้สัมผัสคุณค่าของการทบทวนด้วยตัวเองในขั้นที่เล็กที่สุดเป็นผู้ยกระดับขึ้นสู่ขั้นถัดไป
เครื่องมือ·atom·แพตเทิร์นทุกอย่างที่พบในหนังสือเล่มนี้ ในที่สุดแล้วล้วนถือกำเนิดจากการทบทวนของใครบางคน จะกล่าวว่าหนังสือเล่มนี้เองคือผลผลิตจากการทบทวนที่ผู้เขียนสั่งสมมาตลอดครึ่งปีก็ไม่เกินจริงเลย คำพูดที่ว่าการทบทวนคือจุดเริ่มต้นนั้นไม่ใช่การเปรียบเปรย แต่ชี้ไปยังข้อเท็จจริงที่ว่าสารบัญของหนังสือเล่มนี้เองก็มาจากการทบทวน
การประยุกต์นอกเกม การทบทวนที่คว้าความรู้สึกว่า "คุ้นเคยอยู่ลึก ๆ — เรื่องนี้สัปดาห์ที่แล้วก็ทำไปนี่นา" ไว้เป็นบรรทัดเดียวนั้น ไม่ใช่แค่ในการพัฒนาเกม แต่เป็นทางเข้าสู่การพัฒนาตนเองในที่ทำงานทุกแห่งที่มีงานซ้ำ ๆ เมื่อปิดวัน ลองจดเพียงบรรทัดเดียวว่า "วันนี้มีงานอะไรที่ทำด้วยมือซ้ำสองรอบ" แล้วในวันศุกร์ลองนำห้าบรรทัดของสัปดาห์นั้นมาซ้อนทับกันดู แพตเทิร์นที่แต่ละวันซ่อนไว้ก็จะเผยออกมาในระดับสัปดาห์ ตัวอย่างเช่น หากเจ้าหน้าที่ธุรการคว้าได้จากการทบทวนว่า "พิมพ์อีเมลฟอร์มแบบเดิมซ้ำใหม่ทุกสัปดาห์" บรรทัดนั้นก็จะกลายเป็นเทมเพลตอีเมลในสัปดาห์ถัดไป และกลายเป็นกฎการส่งอัตโนมัติในอีกหนึ่งเดือน หัวใจไม่ได้อยู่ที่ความประณีตของเครื่องมือ แต่อยู่ที่ตัวการกระทำที่นำร่องรอยมาซ้อนทับกันดูเอง และอยู่ที่การให้ AI ดึงตัวเลือกออกมาแต่กรองด้วยตะแกรงว่า "อย่าฝืนสร้าง และตัดสิ่งที่ทำเป็นอัตโนมัติไปแล้วออก"
นี่คือเวอร์ชันที่เล็กที่สุดสำหรับการนำลูปการทบทวนมาใช้เป็นครั้งแรก สามารถเริ่มได้โดยแทบไม่ต้องติดตั้งเครื่องมือใด ๆ
setup
retro/daily/ ขึ้นมาหนึ่งโฟลเดอร์ในโฟลเดอร์งานretro/daily/2026-06-06.md ด้วยวันที่ของวันนี้ ไม่ต้องเตรียมอะไรมากไปกว่านั้นprompt
ทุกวันเมื่อทำงานเสร็จ ให้ส่ง log งานของวันนั้นให้ AI แล้วถามดังนี้
สิ่งที่ทำวันนี้คือ [งาน 1·2·3] ในจำนวนนี้ ถ้ามี งานที่ทำซ้ำตั้งแต่ 2 ครั้งขึ้นไปในสัปดาห์นี้ ให้ทำเครื่องหมายไว้ และถ้ามี แพตเทิร์นที่ทำซ้ำซึ่งน่าจะทำเป็นเครื่องมือ (skill) ได้ ให้แยกออกมาต่างหาก งานที่ทำเป็นเครื่องมือเสร็จแล้วให้ตัดออกจากตัวเลือก ถ้าไม่มีก็บอกว่าไม่มี อย่าฝืนสร้างขึ้นมา
สองประโยคสุดท้าย ("ตัดสิ่งที่ทำเป็นเครื่องมือแล้วออก", "อย่าฝืนสร้าง") คือตะแกรงกรอง หากไม่มีสิ่งนี้ AI จะผลิตตัวเลือกที่ดูน่าเชื่อออกมามากเกินทุกครั้ง จนอีกหนึ่งเดือนถัดมาเครื่องมือที่ไม่ได้ใช้ก็พอกพูนขึ้น
verify
grade ของวันอังคาร·วันศุกร์ก่อนหน้านี้) ถ้าเป็นชนิดเดียวกันก็ยืนยันเป็นตัวเลือก ถ้าต่างกันก็ทิ้งทำซ้ำ: [งาน] (N ครั้ง) → ตัวเลือก skill ตัดสินในการทบทวนรายสัปดาห์ฉบับย่อสำหรับคนเดียว
หากเริ่มคนเดียวโดยไม่มีทั้งทีมและเครื่องมือแยกต่างหาก ให้ลดเหลือเพียงเท่านี้
หัวใจไม่ได้อยู่ที่ความประณีตของเครื่องมือ แต่อยู่ที่ การกระทำที่นำมาซ้อนทับกันดู หนึ่งวันซ่อนแพตเทิร์น หนึ่งสัปดาห์เผยแพตเทิร์น 5 นาทีที่คว้าการเผยนั้นไว้คือทางเข้าสู่ลูปการพัฒนาตนเอง
เช้าวันจันทร์ ผมกำลังจะเริ่มต้นสัปดาห์ด้วยการเปิดบันทึกการทบทวนรายวันของสัปดาห์ก่อนทั้งห้าฉบับไว้บนหน้าจอเดียว ในบันทึกของวันอังคารเขียนไว้ว่า "ลืมตรวจสอบความถูกต้องก่อนจะ export ชีตข้อมูล" บันทึกของวันพฤหัสบดีก็มีประโยคที่แทบจะเหมือนกัน และในเช้าวันจันทร์นั้นเอง ผมก็กำลังทำสิ่งเดียวกันอีกครั้ง ผมเอาชีตที่ FK พังขึ้นบิลด์ฝั่งไคลเอนต์/เซิร์ฟเวอร์ตรง ๆ แล้วต้องถอนออกมาใหม่ นี่เป็นครั้งที่สามแล้ว
ช่วงเวลานี้แหละคือหัวใจของระบบการทบทวน ความจริงที่ว่าเรากำลังทำสิ่งเดียวกันเป็นครั้งที่สามนั้น มองไม่เห็นเลยในขณะที่เรากำลังทำมันอยู่ เพราะมือเคลื่อนไหวอย่างคุ้นเคย และสมองกระซิบว่า "นี่ก็งานที่ฉันทำเป็นประจำอยู่แล้วนี่" การทำซ้ำจะมองเห็นได้ก็ต่อเมื่อรวบรวมร่องรอยไว้แล้วมองย้อนกลับไปจากด้านหลังเท่านั้น การทบทวนคืออุปกรณ์ที่รวบรวมร่องรอยเหล่านั้น ส่วนการเลื่อนขั้น atom คืออุปกรณ์ที่ตรึงการทำซ้ำซึ่งค้นพบจากตรงนั้นให้เป็นกฎตายตัว เพื่อไม่ให้ต้องทำด้วยมืออีกต่อไป
บทนี้จะติดตามจนถึงที่สุดว่าอุปกรณ์ทั้งสองนี้ทำงานประสานกันอย่างไร และบันทึกการทบทวนรายวันจริงหนึ่งฉบับเปลี่ยนไปเป็น atom หนึ่งบรรทัดใน JIT manifest ได้อย่างไร
ก่อนอื่นมีข้อตั้งต้นที่ต้องชี้ให้ชัด การทำซ้ำไม่สามารถรับรู้ได้แบบเรียลไทม์
หนึ่งวันของนักออกแบบเกมคือการตัดสินใจที่ต่อเนื่องกัน จะกำหนดคอลัมน์หนึ่งของชีตข้อมูลให้เป็น enum แบบไหน จะกำหนดคูลดาวน์ของสกิลเป็นหน่วยวินาทีหรือหน่วยเฟรม จะบันทึกข้อตกลงคลุมเครือที่ได้จากที่ประชุมไว้ตรงไหนของเอกสาร การตัดสินใจแต่ละครั้งเล็กเกินกว่าจะตกค้างอยู่ในความทรงจำ แต่ถ้าเรากำลังตัดสินใจเรื่องเดียวกันสามครั้งในหนึ่งสัปดาห์ นั่นก็ไม่ใช่การตัดสินใจอีกต่อไปแล้ว แต่เป็นกฎ ทั้งที่เป็นกฎ แต่ถ้ายังต้องตัดสินใจใหม่ทุกครั้ง นั่นคือความสิ้นเปลือง
ปัญหาคือความสิ้นเปลืองนี้มองไม่เห็น เราจึงทิ้งร่องรอยเอาไว้ ทุกวันใช้เวลา 5 นาที เขียนสิ่งที่ทำในวันนี้และสิ่งที่ทำซ้ำตั้งแต่สองครั้งขึ้นไปลงไปอย่างละบรรทัด เมื่อผ่านไปหนึ่งสัปดาห์ ร่องรอยห้าฉบับก็สะสมขึ้น แล้วถึงตอนนั้นเองที่เราจะมองเห็นว่า "เอ๊ะ เรื่องนี้เขียนไว้สามครั้งนี่"
นี่คือจุดที่แยกการทบทวนออกจากการเขียนไดอารี่ธรรมดา ไดอารี่เขียนความรู้สึก ส่วนการทบทวนเขียนร่องรอยเพื่อสกัดรูปแบบออกมา ดังนั้นการทบทวนจึงต้องมีรูปแบบตายตัว ถ้ารูปแบบต่างกันทุกครั้ง เราจะวางห้าฉบับเรียงกันเพื่อเปรียบเทียบไม่ได้ และเมื่อเปรียบเทียบไม่ได้ ก็มองไม่เห็นรูปแบบ
เหตุที่แบ่งการทบทวนออกเป็นสามรอบ คือรายวัน รายสัปดาห์ รายเดือน ไม่ใช่เพราะเวลาผ่านไป แต่เพราะสิ่งที่แต่ละรอบทำนั้นต่างกันโดยพื้นฐาน
รายวันคือการตรึงร่องรอย ไม่ตัดสินอะไร แค่เขียนลงไปเฉย ๆ รายสัปดาห์คือการรวมร่องรอยห้าฉบับมาดูรูปแบบ ตรงนี้เองที่การตัดสินว่า "นี่คือการทำซ้ำ" เข้ามาเป็นครั้งแรก รายเดือนคือการมองดูเครื่องมือทั้งหมดที่สะสมไว้แล้วประเมินความคุ้มค่า ตัดสินว่าจะเก็บอะไรไว้และทิ้งอะไร
หากขาดรอบใดไปรอบหนึ่ง ที่เหลือก็พังทั้งหมด ถ้าทำแต่รายสัปดาห์โดยไม่มีรายวัน เราจะจำเรื่องของสัปดาห์ก่อนไม่ได้ ร่องรอยจึงว่างเปล่า ถ้าทำแต่รายเดือนโดยไม่มีรายสัปดาห์ เราจะต้องมาดูรายวันทั้งเดือนในคราวเดียว ซึ่งการเปรียบเทียบ 22 ฉบับในที่เดียวกันนั้นแทบเป็นไปไม่ได้ มองไม่เห็นรูปแบบ มีแต่ความล้าสะสม
การเปรียบกับสถานที่ทำงานเข้ากันได้ดี รายวันคือ 5 นาทีที่จัดโต๊ะทำงานในทุกเย็น รายสัปดาห์คือ 30 นาทีในช่วงสุดสัปดาห์ที่จัดลิ้นชักหนึ่งช่องใหม่ รายเดือนคือสองชั่วโมงในแต่ละไตรมาสที่สำรวจเส้นทางการเดินทั้งหมดของสถานที่ทำงาน ถ้าไม่จัดโต๊ะทุกวัน สุดสัปดาห์ก็จัดลิ้นชักไม่ได้ และถ้าลิ้นชักยุ่งเหยิง ต่อให้ดูเส้นทางการเดินก็หาคำตอบไม่ได้
จริง ๆ แล้วไฟล์การทบทวนรายวันที่ผมใช้จะสะสมตามวันที่ในพาธทำนอง retro/daily/2026-05-30.md ส่วนเทมเพลตนั้นคำสั่งสแลช /retro จะวางให้โดยอัตโนมัติ
# การทบทวนรายวัน 2026-05-30
## สิ่งที่ทำวันนี้ (3~5 บรรทัด)
- เพิ่ม enum 12 ชนิดในชีตข้อมูลสกิลใหม่ + จัดเรียงคอลัมน์คูลดาวน์ใหม่
- ปรับสมดุลรอบแรกในการจำลอง (ปรับน้ำหนัก drop table)
- อัปเดตบิลด์ export ข้อมูลทั้งฝั่งไคลเอนต์/เซิร์ฟเวอร์พร้อมกัน
## การค้นพบการทำซ้ำ (ถ้ามี)
- ลืมตรวจสอบความถูกต้องก่อนบิลด์ export ข้อมูลอีกแล้ว → บิลด์ทั้งที่ FK พัง → ครั้งที่ 3
- รันการจำลองปรับสมดุลโดยไม่ตรึง seed ทำซ้ำไม่ได้ (ครั้งที่สอง)
## ผู้สมัครให้ทิ้ง
- เครื่องมือที่ไม่ได้ใช้แม้แต่ครั้งเดียวในวันนี้: (บันทึกไว้สำหรับวัดยอดสะสมรายเดือนเท่านั้น)
## ส่งต่อไปเซสชันถัดไป
- อุด FK ที่พังสองรายการ (การอ้างอิงสกิล→เอฟเฟกต์) ให้เรียบร้อยก่อน แล้วค่อยบิลด์ใหม่
- ผู้สมัคร: พิจารณาตั้งตัวเลือกตรึง seed ของการจำลองเป็นค่าเริ่มต้น
ใช้เวลา 5 นาทีก็เต็ม เพราะรูปแบบตายตัวอยู่แล้ว จึงไม่ต้องมานั่งคิดใหม่ทุกครั้งว่า "จะเขียนอะไรดี" เมื่อช่องถูกกำหนดไว้แล้ว แค่เติมช่องให้เต็มก็พอ
จุดชี้ขาดตรงนี้คือช่อง "การค้นพบการทำซ้ำ" ช่องนี้จะว่างไว้ก็ได้ วันส่วนใหญ่ก็ว่างเปล่า แต่ถ้าวันนี้รู้สึกตัวขึ้นมาว่าทำสิ่งเดียวกันสองครั้ง ก็เขียนลงไปหนึ่งบรรทัด ประโยค "ลืมตรวจสอบความถูกต้องก่อนบิลด์ export ข้อมูลอีกแล้ว → ครั้งที่ 3" ในตัวอย่างข้างบนนั่นแหละ บรรทัดเดียวนี้จะถูกรวมเป็นรูปแบบในการทบทวนรายสัปดาห์ไม่กี่วันต่อมา แล้วถูกตรึงเป็น atom หรือ skill อีกหลายสัปดาห์ถัดไป
การจับภาพอัตโนมัติช่วยลดงานมือของคน เมื่อ git commit log, ประวัติการเปลี่ยน atom และ skill usage log ถูกรวมเข้าในการทบทวนรายวันโดยอัตโนมัติ ครึ่งหนึ่งของช่อง "สิ่งที่ทำวันนี้" ก็ถูกเติมไว้เรียบร้อยแล้ว คนแค่เพิ่มสิ่งที่ git log มองไม่เห็น — ความรู้สึกตัวว่า "เออ ทำอีกแล้ว" — เข้าไปก็พอ
ช่องสุดท้าย "ส่งต่อไปเซสชันถัดไป" คือโน้ตที่ส่งถึงตัวเราในวันพรุ่งนี้ ถ้ามีช่องนี้ การโหลดบริบทตอนเริ่มเซสชันใหม่จะจบภายใน 1\~2 นาที ถ้าไม่มี ก็ต้องเสียเวลามากขึ้นในการค้นว่า "เมื่อวานทำอะไรค้างไว้นะ" จริง ๆ แล้วใน MEMORY.md ของผมมีรายการ "สิ่งที่ต้องเช็กก่อนในเซสชันถัดไป" แยกไว้ต่างหาก นี่ก็คือเวอร์ชันบนของการส่งต่อรายวันที่สะสมขึ้นมานั่นเอง
การทบทวนรายสัปดาห์เริ่มต้นด้วยการวางรายวันห้าฉบับขึ้นมาบนหน้าจอเดียว ไฟล์จะสะสมในพาธทำนอง retro/weekly/2026-W21.md
# การทบทวนรายสัปดาห์ 2026-W22 (25/5~31/5)
## สรุปสิ่งที่ทำในสัปดาห์นี้
- อัปเดตชีตข้อมูลสกิล/การปรับสมดุล, จำลอง drop table 2 ครั้ง
- บิลด์ export ข้อมูล 4 ครั้ง (ในจำนวนนั้น 2 ครั้งบิลด์ทั้งที่ FK·enum พัง)
## การค้นพบรูปแบบ
- ในรายวัน 3 รายการมี "ลืมตรวจสอบความถูกต้องก่อน export" ซ้ำ → ผู้สมัคร atom
- ในรายวัน 2 รายการมี "ไม่ตรึง seed ของการจำลองปรับสมดุล" ซ้ำ → พิจารณาค่าเริ่มต้นของการจำลอง
## ผู้สมัคร atom
- pending-data-check-before-export (กฎที่บังคับให้ตรวจสอบความถูกต้องก่อนบิลด์ export)
## ผู้สมัคร skill
- (ไม่มี — สัปดาห์นี้ใช้ atom ก็เพียงพอ)
## ตรวจสอบเครื่องมือที่มีอยู่
- ไม่ได้ใช้: relation-map-gen (สัปดาห์นี้ 0 ครั้ง)
- ใช้มากที่สุด: check (ตรวจความถูกต้องแบบ cascade), excel-reader, /retro
## แผนสัปดาห์หน้า
- รัน pending-data-check-before-export ต่ออีก 1 สัปดาห์แล้วค่อยตัดสินเลื่อนขั้น
ตรงนี้เองที่การตัดสินเข้ามาเป็นครั้งแรก "ในรายวัน 3 รายการมีการลืมตรวจสอบก่อน export ซ้ำ" เป็นข้อเท็จจริงเชิงคณิตศาสตร์ แต่ "นี่มีค่าพอที่จะตรึงเป็น atom" คือการตัดสิน เหตุที่ใช้การทำซ้ำ 3 ครั้งเป็นเส้นมาตรฐานนั้นเรียบง่าย ครั้งเดียวคือบังเอิญ สองครั้งก็อาจบังเอิญ สามครั้งคือรูปแบบ
เมื่อตัดสินใจได้ก็ตรึงทันที แต่ไม่ใช่ atom อย่างเป็นทางการ เป็น atom ชั่วคราวที่ติดคำนำหน้า pending- ไว้ มันจะถูกวางลงในโฟลเดอร์หน่วยความจำของโปรเจกต์ผมแบบนี้
~/.claude/projects/<project>/memory/
pending-data-check-before-export.md
คำนำหน้า pending- คือเครื่องหมายที่บอกว่า "เรื่องนี้ยังอยู่ระหว่างตรวจสอบ" เหตุที่เครื่องหมายนี้สำคัญ คือถ้าเอาสัญชาตญาณที่ยังไม่ผ่านการตรวจสอบมาทำเป็นกฎของทั้งทีมทันที จะมีสองอย่างพังลง อย่างหนึ่งคือความเชื่อใจ — ถ้ากฎที่ไม่ผ่านการตรวจสอบผิดอยู่บ่อย ๆ คนก็จะเลิกเชื่อตัวกฎเอง อีกอย่างคือการสะสม — ถ้าไม่มีด่านตรวจสอบ สัญชาตญาณก็จะสะสมขึ้นตรง ๆ จนหน่วยความจำกลายเป็นถังขยะ
ดังนั้น pending- จึงถูกนำมารันทดลองในงานจริง ตั้งแต่หนึ่งสัปดาห์ ยาวไปจนถึงหนึ่งเดือน ถ้ามันมีประโยชน์จริงทุกครั้งก็จะอยู่รอด ถ้าไม่เคยตรงเลยสักครั้งก็จะถูกลบไปอย่างเงียบ ๆ
การทบทวนรายเดือนคือที่ที่กางยอดสะสมตลอดทั้งเดือนออกมา แล้วตรวจสภาวะสุขภาพของเครื่องมือทั้งหมด ไฟล์จะสะสมเป็นหน่วยรายเดือนทำนอง retro/2026-05.md
# การทบทวนรายเดือน 2026-05
## ยอดสะสมเดือนนี้
- การทบทวนรายวัน: 22 รายการ, การทบทวนรายสัปดาห์: 4 รายการ
- atom ใหม่: 4 รายการ (data-check-before-export, sim-seed-pinning และอื่น ๆ)
- skill ใหม่: 1 รายการ (เสริมตัวเลือกของ relation-map-gen)
- atom ที่ทิ้ง: 1 รายการ
## ประเมินความคุ้มค่าของเครื่องมือ
- จำนวนครั้งที่ใช้ต่อเดือนของแต่ละ skill + ความรู้สึกประหยัด (เชิงคุณภาพ)
- skill ที่ใช้ต่ำกว่าเดือนละ 1 ครั้ง → ผู้สมัครให้ทิ้ง
- เครื่องมือที่มีค่ามากที่สุด: check (ตรวจความถูกต้องแบบ cascade), excel-reader, /retro
## การกระจายของ atom
- ยอดสะสมตาม prefix (data: X, sim: Y, meeting: Z ...)
- ผู้สมัครให้ทิ้ง: atom ที่ทั้งเดือน match 0 ครั้ง
## แผนรายไตรมาส
- นำมาใช้เดือนหน้า: impact (ติดตามระดับผลกระทบ), อัปเดต schema-doc อัตโนมัติ
## เอกสารสำหรับเขียนหนังสือ (ถ้ามี)
- ในเดือนนี้สิ่งที่ควรอ้างอิงในหนังสือ: บันทึกเซสชันจริงของการเลื่อนขั้น atom 1 รายการ
หัวใจของรายเดือนคือการประเมินความคุ้มค่า เครื่องมือตอนสร้างดูมีค่าไปหมด แต่พอผ่านไปหนึ่งเดือน ครึ่งหนึ่งก็ไม่ได้หยิบมาใช้ การคัดแยกสิ่งนั้นใช้ห้าเกณฑ์วัด
เกณฑ์การประเมินมีห้าข้อ คือ ความถี่ในการใช้, การประหยัดเวลา, ภาระทางการรับรู้, ต้นทุนการบำรุงรักษา และความสามารถในการทดแทน ความถี่ในการใช้ถ้าตั้งแต่เดือนละ 1 ครั้งขึ้นไปก็เก็บไว้ก่อน ถ้าต่ำกว่านั้นก็ส่งไปเป็นผู้สมัครให้ทิ้ง การประหยัดเวลานั้นเอาความรู้สึกประหยัดต่อครั้งคูณกับความถี่ — ตรงนี้จะไม่ฟันธงตัวเลขเป็นหน่วยนาที การตัดสินเชิงคุณภาพในระดับ "รู้สึกประหยัดทีละไม่กี่นาที และใช้เดือนละสิบครั้งจึงสะสมมาก" คือความซื่อสัตย์ ภาระทางการรับรู้นั้น ถ้าจำนวนคำสั่งสแลชที่ต้องจำเกินสิบสองคำสั่ง ก็ถือเป็นสัญญาณให้จัดระเบียบ จำนวนคำสั่งที่คนแบกไว้ในหัวได้นั้นมีขีดจำกัด ต้นทุนการบำรุงรักษานั้นดูว่าเป็นเครื่องมือที่ต้องแก้ไปด้วยเมื่อชีตข้อมูลเปลี่ยนหรือไม่ ความสามารถในการทดแทนนั้นดูว่ามีวิธีที่ง่ายกว่าเกิดขึ้นมาใหม่หรือไม่
รวมห้าเกณฑ์เข้าด้วยกันแล้วตัดสินว่าจะเก็บ ทิ้ง หรือแก้ ภายในหนึ่งชั่วโมงก็สามารถไล่ดูเครื่องมือได้สามสิบถึงห้าสิบรายการ เพียงแต่ในช่วงเริ่มต้นหนึ่งถึงสองเดือนแรก เครื่องมือยังน้อยจึงประเมินไม่ถึงหนึ่งชั่วโมง ในช่วงนั้นใช้เวลาไปกับการจุดประกายเครื่องมือใหม่มากกว่าการประเมินจึงจะถูกต้อง การประเมินความคุ้มค่าคือกลไกความปลอดภัยที่จะทำงานเต็มที่ก็ต่อเมื่อเครื่องมือสะสมมากพอแล้ว
จากตรงนี้ไปคือกระดูกสันหลังของบทนี้ เราจะติดตามตั้งแต่จุดทบทวนไปจนถึงที่สุด ว่าเศษชิ้นส่วนที่กระจัดกระจายออกมาข้างบนนั้นเริ่มต้นจากบรรทัดเดียวจริง ๆ แล้วไปสู่การถูกฉีดเข้าทุกเซสชันโดยอัตโนมัติได้อย่างไร ผมจะคงช่วงเวลาที่ผมปฏิเสธ และช่วงเวลาที่ AI ตอบแบบคลุมเครือ ไว้ตามเดิม
ขั้นที่ 1 — รู้สึกตัวในรายวัน ขณะเขียนการทบทวนรายวันของวันที่ 30 พฤษภาคม ผมเขียนหนึ่งบรรทัดลงในช่อง "การค้นพบการทำซ้ำ"
- ลืมตรวจสอบความถูกต้องก่อนบิลด์ export ข้อมูลอีกแล้ว → บิลด์ทั้งที่ FK พัง → ครั้งที่ 3
ตอนนั้นยังไม่ได้สร้างเครื่องมืออะไรเลย แค่ทิ้งร่องรอยไว้เฉย ๆ
ขั้นที่ 2 — ยืนยันรูปแบบในรายสัปดาห์แล้วขอให้ AI ทำเป็น atom ไม่กี่วันต่อมา ตอนกางรายวันห้าฉบับในการทบทวนรายสัปดาห์ ก็พบบรรทัดเดียวกันอยู่สามที่ ผมจึงโยนพรอมต์ฉบับเต็มให้ Claude แบบนี้
ในการทบทวนรายวัน 3 รายการของสัปดาห์นี้ "ลืมตรวจสอบความถูกต้องด้วยมือก่อนบิลด์ export ข้อมูล" เกิดซ้ำ ผมอยากทำเรื่องนี้เป็น pending atom ช่วยร่างเนื้อหา atom ให้หน่อย รูปแบบให้เป็นแบบเดียวกับ atom อื่น ๆ ในหน่วยความจำของเรา คือ frontmatter (name, status, trigger) กับกฎหนึ่งย่อหน้า
ผลลัพธ์ดิบของ Claude เป็นแบบนี้ (คงไว้ตามเดิมโดยไม่สรุปย่อ)
--- name: data-check-before-export status: pending trigger: ["데이터", "export", "빌드 전", "정합성"] --- ก่อนจะบิลด์ export ชีตข้อมูลเกมไปยังฝั่งไคลเอนต์/เซิร์ฟเวอร์ ต้องรันการตรวจสอบ ความถูกต้อง (check cascade) ก่อนเสมอ เพื่อยืนยันว่าการละเมิด FK·enum·การอ้างอิง เป็น 0 รายการ แล้วจึงค่อย export ถ้าพบการละเมิด ให้หยุด export และแจ้งรายละเอียด ความล้มเหลวไปยังผู้รับผิดชอบผ่านการแจ้งเตือน ClickUpบันทึก atom นี้เป็น
pending-data-check-before-export.mdก็ได้ครับ เพียงแต่ในคีย์เวิร์ด trigger คำว่า "데이터" กว้างเกินไป อาจ match กับสถานการณ์ที่ไม่เกี่ยวกับ export ได้ด้วย (การเปิดดูชีตข้อมูล, การประชุมข้อมูล ฯลฯ) ขอแนะนำให้เอาออก
ขั้นที่ 3 — การตรวจสอบของคนและการปฏิเสธบางส่วน การที่ AI สงสัย trigger หนึ่งตัวด้วยตัวเองนั้นเป็นข้อชี้ที่ถูกต้อง แต่ผมไปไกลกว่านั้นอีกก้าวหนึ่ง ผมไม่ชอบที่เนื้อหาของกฎผูก "การตรวจสอบความถูกต้อง" กับ "การแจ้งเตือน ClickUp เมื่อล้มเหลว" ไว้ด้วยกันทั้งคู่ การแจ้งเตือนความล้มเหลวเป็น atom แยกอีกตัวซึ่ง integrity_check_clickup_notify รับผิดชอบอยู่แล้ว ถ้าผูกสองความรับผิดชอบไว้ใน atom เดียว เวลาที่อยากแก้แค่กฎการแจ้งเตือนทีหลัง ก็จะต้องไปแตะกฎการตรวจสอบไปด้วย ผมจึงปฏิเสธแล้วขอใหม่
โอเค "데이터" trigger เอาออกได้ และการแจ้งเตือน ClickUp เมื่อล้มเหลวนั้น integrity_check_clickup_notify ทำอยู่แล้ว ก็เอาออกจาก atom นี้ ให้ atom นี้รับผิดชอบแค่การตรวจสอบความถูกต้องก่อน export อย่างเดียว ช่วยบีบให้แคบลง
หลังขอใหม่ เนื้อหาที่ Claude บีบให้แคบลงเป็นแบบนี้
--- name: data-check-before-export status: pending trigger: ["데이터 빌드", "export", "빌드 전 검증"] --- ก่อนจะบิลด์ export ชีตข้อมูลเกมไปยังฝั่งไคลเอนต์/เซิร์ฟเวอร์ ให้รันการตรวจสอบ ความถูกต้อง (check cascade) ก่อน สิ่งที่ตรวจ: ความสมบูรณ์ของการอ้างอิง FK, ความถูกต้องของค่า enum, ความถูกต้องของการอ้างอิงระหว่างตาราง ยืนยันการละเมิด 0 รายการแล้วจึงค่อย export การแจ้งเตือนเมื่อล้มเหลว เป็นความรับผิดชอบของ integrity_check_clickup_notify และกฎนี้ไม่เข้าไปเกี่ยวข้อง
นี่คือเวอร์ชันที่ผมยอมรับ รับผิดชอบแค่อย่างเดียว และ trigger ก็ถูกบีบให้แคบลงเฉพาะบริบทของ export ผมบันทึกไฟล์นี้เป็น pending-data-check-before-export.md หัวใจตรงนี้คือผมไม่ได้ใช้ผลลัพธ์แรกของ AI ตรง ๆ AI จัดรูปแบบให้ และสงสัย trigger หนึ่งตัวให้ก็จริง แต่การตัดสินเชิงออกแบบว่า "บีบความรับผิดชอบให้เหลืออย่างเดียว" นั้นคนเป็นคนทำ
ขั้นที่ 4 — ตรวจสอบด้วยการรัน 1 สัปดาห์ ตลอดสัปดาห์ถัดมา ทุกครั้งที่บิลด์ export ข้อมูล pending atom ตัวนี้ก็ผุดขึ้นมา และจับ enum พังได้จริงสองครั้งก่อนบิลด์ ไม่เคยมีครั้งที่ไม่ตรงเลย จึงมีคุณสมบัติที่จะอยู่รอด
ขั้นที่ 5 — ตัดสินเลื่อนขั้นและวัด score ในรายเดือน ในการทบทวนรายเดือน ผมยก pending atom ตัวนี้ขึ้นเป็นผู้สมัครเลื่อนขั้น การเลื่อนขั้นหรือไม่ตัดสินด้วยการวัด ไม่ใช่สัญชาตญาณ ในสภาพแวดล้อมของผมมีสคริปต์ที่ให้คะแนนความถี่ในการ match และความมีประโยชน์ของ atom
python ~/.claude/scripts/atom_score.py
# → ~/.claude/projects/<project>/memory/_scores_latest.json 갱신
สคริปต์นี้รวบรวมว่าในช่วงที่ผ่านมาแต่ละ atom match กับ trigger กี่ครั้ง และตอนนั้นถูกอ้างอิงในงานจริงหรือไม่ แล้วทิ้งลงใน _scores_latest.json atom ที่คะแนนเกินเกณฑ์ที่กำหนดจะถูกเชื่อมให้ฉีดเข้า CLAUDE.md โดยอัตโนมัติ pending-data-check-before-export ตามผลวัดจริงในเดือนพฤษภาคม 2026 match ทุกครั้งที่บิลด์ export ตลอดหนึ่งสัปดาห์ คะแนนจึงเพียงพอ ยืนยันเลื่อนขั้น
ขั้นที่ 6 — ลบ pending- ลงทะเบียนใน JIT manifest หลังถอดคำนำหน้าออกแล้วเปลี่ยนเป็น atom อย่างเป็นทางการ ผมก็เพิ่มหนึ่งบรรทัดลงใน JIT manifest
~/.claude/projects/<project>/memory/_jit_manifest.json
manifest นี้ถูกอ่านโดยฮุก UserPromptSubmit (~/.claude/hooks/inject_memory.py) ในทุกอินพุต ถ้าในอินพุตมี "데이터 빌드" หรือ "export" อยู่ เนื้อหา atom ตัวนี้ก็จะแทรกเข้าบริบทโดยอัตโนมัติ
ขั้นที่ 7 — ลูปปิด ในวินาทีที่ผมพิมพ์ "데이터 export 빌드 돌려줘" เพื่อจะบิลด์ครั้งถัดไป ทั้งที่ผมยังไม่ได้สั่งอะไรเลย Claude ก็พูดขึ้นก่อน
ให้รันการตรวจสอบความถูกต้อง (check cascade) ก่อน export ดีไหมครับ ผมจะตรวจความถูกต้องของการอ้างอิง FK·ค่า enum·การอ้างอิงตาราง ยืนยันการละเมิด 0 รายการแล้วจึงค่อย export
บรรทัด "เออ ทำอีกแล้ว" ที่ผมเขียนในการทบทวนรายวันเมื่อ 3 สัปดาห์ก่อน กลับมาเป็นกฎที่คอยกั้นงานในตอนนี้ให้เองโดยที่ผมไม่ต้องบอก การตรวจสอบที่เคยทำด้วยมือ ไม่ต้องทำด้วยมืออีกต่อไป การที่ลูปปิดลงนั้นหมายถึงฉากนี้นี่เอง
ถ้าบีบบันทึกเซสชันจริงข้างบนให้เป็นผังการไหลหนึ่งภาพ ก็จะได้แบบนี้ การค้นพบเกิดในรายวัน การตรวจสอบทำโดยช่วงเวลาการรัน การเลื่อนขั้นกำหนดโดยการวัด และการทำให้เป็นทรัพย์สินปิดท้ายโดย manifest
flowchart TD
A["การทบทวนรายวัน
บรรทัดเดียว 'เออ ทำอีกแล้ว'"] --> B["การทบทวนรายสัปดาห์
รวม 5 ฉบับยืนยันรูปแบบ (3 ครั้งขึ้นไป)"]
B --> C["ขอ AI ทำเป็น atom
→ ผลลัพธ์ดิบ → คนตรวจสอบ·ปฏิเสธ → ขอใหม่"]
C --> D["ตรึง atom แบบ pending-
~/.claude/.../memory/pending-*.md"]
D --> E["ตรวจสอบด้วยการรันงานจริง 1~4 สัปดาห์"]
E -->|ไม่ตรงเลยสักครั้ง| X["ทิ้งอย่างเงียบ ๆ"]
E -->|มีประโยชน์ทุกครั้ง| F["วัดด้วย atom_score.py
_scores_latest.json"]
F --> G["ตัดสินเลื่อนขั้นในการทบทวนรายเดือน
ลบ pending-"]
G --> H["ลงทะเบียนใน JIT manifest
_jit_manifest.json"]
H --> I["ฮุก UserPromptSubmit
inject_memory.py ฉีดอัตโนมัติ"]
I --> J["เซสชันถัดไป: เมื่อพิมพ์คีย์เวิร์ด
สิ่งที่ค้นพบในอดีตกั้นงานปัจจุบันให้"]
J -.พบอีกในการทบทวน.-> A
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class F,I code;
class A,B,G human;
class D,H data;
class J pass;
class X fail;
เส้นประเส้นสุดท้ายคือทั้งหมดของภาพนี้ atom ที่ถูกฉีดอัตโนมัติก็เผยการทำซ้ำใหม่อีก แล้วมันก็เข้าสู่การทบทวนอีกครั้งเพื่อให้กำเนิด atom ถัดไป ทุกครั้งที่หมุนครบหนึ่งรอบ งานที่ต้องทำด้วยมือก็ลดลงทีละหนึ่ง เมื่อลูปนี้สะสมตั้งแต่ครึ่งปีถึงหนึ่งปี การทบทวนก็จะไม่ใช่ไดอารี่อีกต่อไป แต่กลายเป็นสมองของระบบการทำงาน
ห่วงโซ่ที่เปราะที่สุดในลูปการเลื่อนขั้นคือขั้นที่ 1 คือช่วงเวลาที่เขียน "เออ ทำอีกแล้ว" ลงไปนั้น เวลายุ่ง คนจะปล่อยช่องการทบทวนว่างไว้แล้วข้ามไป แล้วร่องรอยก็ไม่เหลือ เมื่อไม่มีร่องรอยก็มองไม่เห็นรูปแบบในรายสัปดาห์ เมื่อไม่มีรูปแบบ atom ก็ไม่เกิด ปากทางเข้าของลูปก็ตัน
ดังนั้นในสภาพแวดล้อมของผมจึงมี atom หนึ่งตัวชื่อ retro_atom_natural_invitation เป็นกฎที่บอกว่าตอนเขียนการทบทวน อย่าบังคับการจุดประกาย atom เป็นหน้าที่ ให้ทิ้งไว้เป็นคำเชิญที่เป็นธรรมชาติ กล่าวคือไม่ใช่ "วันนี้ต้องดึงผู้สมัคร atom หนึ่งตัวออกมาให้ได้" แต่ให้วางช่อง "การค้นพบการทำซ้ำ" ในเทมเพลตการทบทวนเป็นช่องที่ปล่อยว่างได้ แต่ถ้ามีอะไรพอจะเขียนได้สักบรรทัด ก็ชักจูงให้เขียนลงไปอย่างเบา ๆ ถ้าทำเป็นหน้าที่บังคับ ก็จะฝืนสร้างรูปแบบปลอม ๆ ขึ้นมา ถ้าทิ้งไว้เป็นคำเชิญ ก็จะมีแต่การทำซ้ำของจริงเท่านั้นที่ติดเข้ามาอย่างเป็นธรรมชาติ
ความต่างเฉียดเส้นนี้แหละที่ชี้ขาดความยั่งยืนของลูป การทบทวนที่ถูกบังคับเป็นหน้าที่จะไม่ผ่านสองสัปดาห์ และจะถูกเติมด้วยคำโกหกตามแบบฟอร์ม การทบทวนแบบคำเชิญในวันที่ไม่มีอะไรจะเขียนก็ปล่อยช่องว่างไว้ จึงไม่มีภาระ และเพราะอย่างนั้นจึงอยู่ได้นาน ต้องอยู่ได้นานร่องรอยจึงสะสม ร่องรอยสะสมจึงมองเห็นรูปแบบ
atom ตัวนี้เองก็เกิดมาจากการทบทวน ผมรันการทบทวนแบบบังคับเป็นหน้าที่ แล้วพบในรายวันว่าช่องถูกเติมด้วยของปลอมภายในไม่กี่วัน และการค้นพบนั้นผ่านรายสัปดาห์แล้วเลื่อนขั้นมาเป็นกฎข้อนี้ กลายเป็นว่ากฎที่ปรับปรุงการทบทวนก็ออกมาจากการทบทวนเอง
พอหมุนลูปไปก็จะพังที่จุดเดิม ๆ ซ้ำ ๆ
การข้ามการทบทวนคือสิ่งที่พบบ่อยที่สุด อ้างว่ายุ่งแล้วข้ามไปสามวัน ร่องรอยของสามวันนั้นก็หายไปตลอดกาล วิธีกั้นนั้นเรียบง่าย — ช่องอื่นปล่อยว่างได้หมด แต่ "สิ่งที่ทำวันนี้" หนึ่งบรรทัดต้องเขียน ไม่ใช่ 5 นาที จะเป็น 1 นาทีร่องรอยก็ยังเหลือ
การจัดรูปแบบใหม่ทุกครั้งก็อันตราย ถ้าเขียนแบบอิสระ ก็วางห้าฉบับเรียงกันเปรียบเทียบไม่ได้ เมื่อเปรียบเทียบไม่ได้ งานของรายสัปดาห์อย่างการสกัดรูปแบบเองก็เป็นไปไม่ได้ ดังนั้น /retro จึงวางเทมเพลตให้แบบบังคับ
การไม่ทิ้งของก็เป็นกับดัก ถ้าเอาแต่เพิ่ม atom และ skill โดยไม่ทิ้ง ภาระทางการรับรู้ก็สะสม นับจากวินาทีที่คำสั่งสแลชเกินสิบสองคำสั่ง สมองก็จำเครื่องมือไม่ได้หมด การประเมินความคุ้มค่าของรายเดือนคือกลไกเดียวที่กั้นการสะสมนี้
การข้ามด่านเลื่อนขั้นก็อันตราย ถ้าเอาสัญชาตญาณมาทำเป็น atom อย่างเป็นทางการทันที กฎที่ไม่ผ่านการตรวจสอบก็สะสม ด่านที่ผ่าน pending- แล้วเลื่อนขั้นด้วยการวัดต้องมีอยู่ระหว่างการค้นพบกับการทำให้เป็นทรัพย์สินอย่างแน่นอน
สุดท้าย การไม่ทำการทบทวนเพราะเป็นงานคนเดียวจึงไม่มีการแบ่งปันกับทีมนั้นเป็นความเข้าใจผิด บันทึกเซสชันจริงทั้งหมดข้างบนเป็นตัวอย่างที่หมุนในสภาพแวดล้อมคนเดียว เพียงแค่ขั้นตอนการ merge แบ่งปันกับทีมหายไปเท่านั้น ลูปการค้นพบ→pending→วัด→เลื่อนขั้น→ฉีด JIT ก็ยังหมุนตามเดิมได้แม้อยู่คนเดียว ในทางกลับกัน ในสภาพแวดล้อมคนเดียว ลูปนี้ทำหน้าที่เป็นผู้ตรวจสอบจากภายนอกเพียงรายเดียว
การประยุกต์นอกเกม ลูปการเลื่อนขั้นที่บรรทัดเดียวในรายวันผ่านการตรวจสอบจนกลายเป็นกฎถาวร คือกระบวนการที่ทำให้ "บทเรียนที่เคยเรียนรู้แล้วไม่ต้องทำด้วยมืออีก" โดยไม่เกี่ยวกับสายงาน หัวใจคือด่านที่ไม่ตรึงสิ่งที่ค้นพบเป็นกฎของทีมทันที แต่รัน
pendingทดลองหนึ่งสัปดาห์ แล้วทำให้เป็นทางการก็ต่อเมื่อมีประโยชน์จริงทุกครั้ง — เพราะถ้าเอาสัญชาตญาณที่ไม่ผ่านการตรวจสอบมาทำเป็นกฎทันที คนก็จะเลิกเชื่อตัวกฎเอง เช่น ทีมปฏิบัติการตั้ง "ดูยอดรวมตัวเลขอีกครั้งว่าถูกต้องก่อนส่งรายงาน" ไว้เป็นเช็กลิสต์ชั่วคราว แล้วลองรันหนึ่งสัปดาห์ พอจับข้อผิดพลาดได้จริงสักสองครั้งก็ยกขึ้นเป็นขั้นตอนมาตรฐานอย่างเป็นทางการ การตัดสินเชิงออกแบบที่บีบให้รับผิดชอบแค่อย่างเดียว (ไม่ผูกการตรวจหลายอย่างไว้ในบรรทัดเดียว) ก็จะตามมาเองโดยธรรมชาติ
setup วางโฟลเดอร์การทบทวนและเทมเพลต
mkdir -p ~/.claude/projects/<your-project>/memory/retro/daily
mkdir -p ~/.claude/projects/<your-project>/memory/retro/weekly
# 일간 템플릿 파일 하나를 retro/_template_daily.md 로 저장
prompt หลังสะสมการทบทวนรายวันได้หนึ่งสัปดาห์ ในที่ของการทบทวนรายสัปดาห์ ให้โยนแบบนี้ให้ Claude
ผมจะแปะการทบทวนรายวัน 5 ฉบับของสัปดาห์นี้ให้ ช่วยหางาน·การตัดสินที่ทำซ้ำตั้งแต่ 3 ครั้งขึ้นไป แล้วจัดเป็นผู้สมัคร atom ให้หน่อย แต่ละผู้สมัครให้เป็น frontmatter (name, status: pending, อาร์เรย์ของคีย์เวิร์ด trigger) กับกฎหนึ่งย่อหน้า ถ้า trigger กว้างเกินไปก็เสนอแบบบีบให้แคบ และถ้า atom หนึ่งตัวรับสองความรับผิดชอบก็เสนอแบบแยกออกจากกัน
verify อย่าใช้ผู้สมัครที่ได้รับมาตรง ๆ แต่ให้ตรวจสอบสามอย่าง (1) atom หนึ่งตัวรับผิดชอบแค่อย่างเดียวหรือไม่ — ถ้าสองความรับผิดชอบให้ปฏิเสธและขอแยก (2) คีย์เวิร์ด trigger match เฉพาะบริบทของงานนั้นหรือไม่ — ถ้ากว้างเกินไปให้ปฏิเสธ (3) ทำซ้ำ 3 ครั้งจริงหรือไม่ หรือเป็นความบังเอิญ 2 ครั้ง — ถ้าบังเอิญก็อย่าแม้แต่จะสร้าง pending เฉพาะสิ่งที่ผ่านสามการตรวจสอบเท่านั้นที่ติด pending- แล้วบันทึกไว้ และหลังรันหนึ่งสัปดาห์ ทำให้เป็นทางการก็ต่อเมื่อมีประโยชน์ทุกครั้ง
ถ้าไม่มีทั้งทีม ทั้งฮุก JIT และสคริปต์ score ก็ยังเลียนแบบลูปทั้งหมดได้ด้วยไฟล์หน้าเดียวต่อไปนี้
สร้างไฟล์ retro.md หนึ่งไฟล์แล้ววางแค่สามช่อง
## 오늘 (1줄)
-
## 또 했네 (있으면 1줄)
-
## 박제 후보 (또 했네가 3번 쌓이면 이리로)
- [ ] (규칙 한 문장) — 검증: ___회 유용
ทุกวันเติมแค่สองช่องบน ถ้าใน "เออ ทำอีกแล้ว" มีบรรทัดเดียวกันสะสมสามครั้ง ก็ย้ายไปช่องที่สามแล้วเขียนเป็นกฎหนึ่งประโยค นับจำนวนครั้งที่กฎนั้นมีประโยชน์จริงตลอดสัปดาห์ถัดมาแล้วเขียนลงในช่อง ถ้าตั้งแต่สามครั้งขึ้นไปก็ย้ายประโยคนั้นไปไว้ในหน่วยความจำของโปรเจกต์ (CLAUDE.md) อย่างเป็นทางการ แม้ไม่มีฮุก JIT กฎที่ขึ้นไปอยู่ใน CLAUDE.md ก็จะตามมาในเซสชันถัดไปเสมอ เพียงเท่านั้นก็เป็นรูปแบบขั้นต่ำของลูปที่ "สิ่งที่ค้นพบในอดีตช่วยงานปัจจุบัน" สมบูรณ์แล้ว
หัวใจไม่ได้อยู่ที่ความหรูหราของเครื่องมือ แต่อยู่ที่การมีอยู่ของด่าน ขอแค่มีด่าน "เออ ทำอีกแล้ว 3 ครั้ง → ตรึง → มีประโยชน์ 3 ครั้ง → ทำเป็นถาวร" ด้วยไฟล์หน้าเดียวลูป self-improving ก็หมุนได้
วงจรที่เริ่มต้นจากการทบทวนได้กลับมาสู่การทบทวนอีกครั้งหรือไม่ ถ้ามันไม่ปิด สิ่งนั้นก็เป็นเพียงบันทึก ไม่ใช่ระบบ
ผมเปิดดูการทบทวนของหกเดือนก่อน "คำศัพท์ยังไม่เป็นมาตรฐานเดียวกัน" "หาเอกสารยาก" "ถูกถามคำถามเดิมซ้ำอีก" จากนั้นผมเปิดดูการทบทวนที่เขียนไว้เมื่อเช้านี้ "คำศัพท์ยังไม่เป็นมาตรฐานเดียวกัน" "หาเอกสารยาก" "ถูกถามคำถามเดิมซ้ำอีก"
เหมือนกันแม้กระทั่งตัวอักษร ไม่ใช่ว่าไม่ได้ทบทวน ตลอดหกเดือนนั้นผมทบทวนอย่างขยันขันแข็ง หน้า Notion ทยอยทับถมกันขึ้นมา และในเวิร์กช็อปรายไตรมาส กระดาษโน้ตก็ปกคลุมไวต์บอร์ดไปทั่ว แต่เนื้อหาที่เขียนไว้กลับวนอยู่ที่เดิม ไม่ใช่ว่าการทบทวนไม่ทำงาน แต่ลูปต่างหากที่ไม่ปิด
บทนี้คือบทสุดท้ายของหนังสือ ดังนั้นสิ่งที่กล่าวถึงก็เป็นคำถามสุดท้ายเช่นกัน เครื่องมือทั้งหมดที่สร้างไว้ก่อนหน้านี้ — ตัวสร้างเมืองในส่วนที่ 6, atom สำหรับการตรวจสอบบนมือถือในส่วนที่ 14, มาตรฐานต้นทุนในส่วนที่ 22 — สิ่งเหล่านี้จะไม่เป็นของใช้ครั้งเดียวแล้วจบ แต่จะกลายเป็นระบบที่เติบโตด้วยตัวเองได้นั้น ต้องมีอะไรเพิ่มอีก คำตอบมีหนึ่งเดียว วงจรปิดที่คำพูดซึ่งออกมาจากการทบทวนทำงานโดยอัตโนมัติตั้งแต่เซสชันถัดไป และการทำงานนั้นถูกวัดผลแล้ววนกลับมาสู่การทบทวนอีกครั้ง กลไกที่ปิดวงจรนี้ก็คือลูป self-improving
คำพูดออกมาจากการทบทวน "ประชุมเยอะเกินไป" เป็นคำพูดที่ดี แต่คำพูดนั้นถูกทิ้งไว้เป็นหนึ่งบรรทัดบนหน้า Notion สัปดาห์ถัดมาประชุมก็ยังเยอะเหมือนเดิม และในการทบทวนครั้งถัดไปบรรทัดเดิมก็ถูกเขียนซ้ำอีก เพราะระหว่างคำพูดกับการปรับปรุงมีความทรงจำของคนคั่นอยู่ คนเรานั้นลืม ดังนั้นมันจึงขาดตอน
จะเรียกว่า self-improving ได้นั้น คำพูดจากการทบทวนต้องเชื่อมต่อไปสู่การกระทำอัตโนมัติของเซสชันถัดไปโดยไม่ผ่านความทรงจำของคน เงื่อนไขที่ทำให้สิ่งนี้สำเร็จมีอยู่สี่ข้อ
ข้อแรก คำพูดจากการทบทวนต้องถูกแปลงเป็นรูปแบบที่นำไปปฏิบัติได้ทันที ไม่ใช่ความตั้งใจที่เป็นนามธรรม แต่ตกผลึกออกมาเป็นอย่างใดอย่างหนึ่งในบรรดาสกิล atom รายการ manifest หรือคำสั่งสแลช ข้อสอง ต้องทำงานโดยอัตโนมัติตั้งแต่เซสชันถัดไปโดยที่คนไม่ต้องจำ ข้อสาม ในการทบทวนครั้งถัดไปต้องวัดได้จริงว่ามันเปลี่ยนอะไรไปบ้าง ข้อสี่ ผลการวัดนั้นต้องวนกลับไปเป็นข้อมูลขาเข้าของการปรับปรุงครั้งถัดไป
เมื่อทั้งสี่ข้อนี้เชื่อมต่อกันโดยอัตโนมัติ ลูปจึงปิด หากเติมช่องว่างแม้เพียงขั้นเดียวด้วย "สัปดาห์หน้าผมจะจำไว้แล้วเอามาใช้" ลูปก็จะเปิดออกอีกครั้งตรงจุดนั้นทันที และในการทบทวนครั้งถัดไปคำพูดเดิมก็จะถูกเขียนซ้ำอีก
หากมองด้วยการเปรียบเทียบกับลิ้นชัก มันเป็นเช่นนี้ ถ้าการทบทวนจบลงเพียงบันทึกว่า "ปากกาด้ามนี้ไม่ได้ใช้แล้ว เอาออกเถอะ" สัปดาห์ถัดมาปากกาด้ามนั้นก็ยังอยู่ที่เดิม ไม่ใช่บันทึก แต่ต้องลงมือหยิบออกไปจริง มันจึงจะปิด และต้องกลับมาตรวจอีกครั้งในไตรมาสถัดไป ปากกาที่ไม่ได้ใช้จึงจะไม่ทับถมกันที่เดิมอีก บันทึกคือคำพูด การลงมือคือการทำงานอัตโนมัติ และการตรวจในไตรมาสถัดไปคือการวัดผล ถ้าขาดข้อใดข้อหนึ่งในสามนี้ ลิ้นชักก็จะกลับมารกอีก
หากวาดกระแสทั้งหมดออกมา มันจะกลายเป็นวงจรปิด ทั้งจุดเริ่มและจุดจบล้วนเป็นการทบทวน
flowchart LR
A["การทบทวน
(รายวัน·รายสัปดาห์·รายเดือน)"] -->|คำพูด| B["ระบุตัวเลือก
สกิล·atom·คำสั่ง"]
B -->|ทำให้เป็นปริมาณ| C["ประเมินความคุ้มค่า
สูตร ROI"]
C -->|ผ่าน| D["สร้าง·ลงทะเบียน
รับประกันการทำงานอัตโนมัติ"]
C -.->|ไม่ผ่าน| F["ทิ้ง·พักไว้
โยกไปในรูปแบบอื่น"]
D --> E["เซสชันถัดไป
ทำงานอัตโนมัติ"]
E -->|สะสมค่าที่วัดได้| A
F -.->|บันทึก| A
style A fill:#2d4a3e,color:#fff
style E fill:#3e2d4a,color:#fff
ลูกศรวนหนึ่งรอบแล้วเข้าสู่การทบทวนอีกครั้ง การปิดวงนี้คือหัวใจสำคัญ ผลลัพธ์ของแต่ละขั้นกลายเป็นข้อมูลขาเข้าของขั้นถัดไป และค่าที่วัดได้ในขั้นสุดท้ายก็กลับไปเป็นข้อมูลขาเข้าของการทบทวนครั้งแรกอีก หากมีความทรงจำของคนคั่นอยู่ตรงกลาง ลูกศรนั้นก็จะขาด และวงจรก็จะแตก
ลองดูลูกศรเส้นประที่ตัวเลือกซึ่ง ROI ไม่ผ่านถูกโยกไปทิ้งหรือพักไว้ ก็ยังกลับมาสู่การทบทวนในที่สุด การตัดสินว่า "สิ่งนี้ไม่คุ้มค่าที่จะสร้าง" นั้นเองกลายเป็นบันทึกของการทบทวนครั้งถัดไป และเมื่อตัวเลือกเดียวกันถูกเสนอขึ้นมาอีก มันก็เป็นเหตุผลให้คัดออกได้อย่างรวดเร็ว การทิ้งก็อยู่ในลูปเช่นกัน
คำพูดที่เชื่อมต่อจากการทบทวนไปสู่ self-improving นั้นมีรูปแบบที่กำหนดไว้ห้ารูปแบบ (กล่าวถึงไว้แล้วใน §21.1.4) สกิลที่จะสร้าง สกิลที่จะปรับปรุง atom ที่จะสร้าง atom ที่จะปรับปรุง และการประเมินความคุ้มค่าใหม่ หากใส่ห้าข้อนี้ไว้เป็นช่อง (slot) ในเทมเพลตการทบทวนเอง คำพูดก็จะไม่ตกหล่น
## การทบทวน (รายวัน) — 2026-06-06
### 1. งานวันนี้
- (สรุปงาน)
### 2. คำพูด self-improving (5 ช่อง)
- สกิลที่จะสร้าง: <ถ้าว่างให้ใส่ "ไม่มี">
- สกิลที่จะปรับปรุง: <>
- atom ที่จะสร้าง: <>
- atom ที่จะปรับปรุง: <>
- ประเมินความคุ้มค่าใหม่: <>
### 3. สิ่งที่จะวัดในการทบทวนครั้งถัดไป
- <>
ช่องจะว่างก็ได้ ข้อเท็จจริงที่ว่าช่องว่างนั้นเองคือบันทึกว่า "วันนี้ไม่มีการปรับปรุงใหม่" เพียงแต่หากทั้งห้าช่องว่างเปล่าติดต่อกันหลายวัน นั่นไม่ใช่ว่าไม่มีเรื่องให้ปรับปรุง แต่เป็นสัญญาณว่าการทบทวนกำลังแข็งตัวเป็นเพียงพิธีกรรม เมื่อนั้นให้โยนคำถามกระตุ้นเข้าไป "สัปดาห์นี้มีงานอะไรที่ทำด้วยมือซ้ำสองครั้ง"
คำพูดออกมาในสภาพคลุมเครือ "บันทึกการประชุมยาวเกินไป" จะปั้นให้เป็นตัวเลือกได้ต้องทำให้เป็นปริมาณด้วยผลลัพธ์หนึ่งชิ้น "บันทึกการประชุมยาวเกินไป" ถูกแปลงเป็นเครื่องมือหนึ่งชิ้นคือสกิล meeting_summary ซึ่งรับบันทึกการประชุมแล้วสกัดเฉพาะการตัดสินใจกับรายการสิ่งที่ต้องทำ "คำศัพท์สับสน" ถูกแปลงเป็น atom glossary_lookup ที่บรรจุคำศัพท์เฉพาะโดเมน 30 คำ ส่วน "ถูกถามคำถามเดิมทุกครั้ง" ถูกแปลงเป็นคำสั่งสแลช /onboarding ที่ทำให้การแนะนำวันแรกของพนักงานใหม่เป็นอัตโนมัติ และ "การซิงค์ตกหล่นบ่อย" ก็ตกผลึกเป็นการปรับ manifest และเพิ่ม JIT atom
ตัวเลือกต้องถูกนิยามเป็น "ผลลัพธ์ชิ้นใดชิ้นหนึ่ง" จึงจะไปสู่ขั้นถัดไปได้ "ปรับปรุงโดยรวม" ไม่ใช่ตัวเลือก คำพูดที่แปลงเป็นผลลัพธ์หนึ่งชิ้นไม่ได้ ก็เอาขึ้นแท่นประเมิน ROI ไม่ได้ และเมื่อเอาขึ้นไม่ได้ มันก็หยุดอยู่ตรงนั้น
มีตัวเลือกแล้วก็ไม่ได้สร้างทุกอันไป ก่อนสร้างต้องวัดผลตอบแทนเทียบกับการลงทุนเสียก่อน สูตรนั้นเรียบง่าย
แต่ละรายการมีหน่วยและเส้นผ่านของมัน เวลาที่ประหยัดคือเวลาของคนที่ลดลงต่อการทำงานหนึ่งครั้ง คิดเป็นหน่วยนาที ความถี่การทำงานคือจำนวนครั้งโดยประมาณต่อสัปดาห์ ถ้าตั้งแต่สัปดาห์ละ 1 ครั้งขึ้นไปก็ผ่าน ระยะเวลาใช้งานคือจำนวนสัปดาห์โดยประมาณจนกว่าจะถูกทิ้ง เครื่องมือที่อยู่ไม่ถึง 4 สัปดาห์ก็มีเหตุผลให้สร้างน้อย เวลาที่ใช้สร้างคือเวลาที่ใช้ในการสร้างครั้งแรกและตรวจสอบ ส่วนการดูแลรักษาคือเวลาที่ใช้ตรวจสอบและแก้ไขรายเดือน
ตัวเศษคือเวลาที่ประหยัดสะสม ตัวส่วนคือต้นทุนสะสม ตัดสินจากค่าที่ออกมา
| ค่า ROI | การตัดสิน |
|---|---|
| 10 ขึ้นไป | สร้างทันที |
| 3\~10 | สร้างภายในหนึ่งสัปดาห์ |
| 1\~3 | พักไว้ในสถานะ pending ประเมินใหม่อีกหนึ่งเดือนถัดมา |
| ต่ำกว่า 1 | ทิ้งในรูปแบบนี้ พิจารณาวิธีอื่น |
การที่ ROI ต่ำกว่า 1 ไม่ได้หมายความว่า "ไอเดียนี้ไร้ประโยชน์" แต่หมายความว่า "ไม่ควรสร้างในรูปแบบนี้" ให้ตรวจดูก่อนว่าจะแทนที่ด้วย atom ที่เบากว่าเพียงบรรทัดเดียวได้หรือไม่ จะแก้ด้วย Wrapper ที่เปลี่ยนเพียงจุดเข้าใช้งานของเครื่องมือเดิมได้หรือไม่ การลดงานที่ต้องทำเป็นสกิลหนักลงมาเป็น atom บรรทัดเดียว ทำให้ตัวส่วนหดลงเหลือหนึ่งในสิบจน ROI ฟื้นขึ้นมาเป็นกรณีที่พบบ่อย
ลองใส่ตัวเลขจริงเข้าไปสักตัว ผมจะลองคำนวณ ROI ของระบบฉีด JIT atom ที่ติดตั้งบนพีซีส่วนตัวเมื่อวันที่ 23 พฤษภาคม 2026 — โครงสร้างพื้นฐานที่ฮุก UserPromptSubmit ดูข้อมูลที่ผู้ใช้ป้อนแล้วฉีดชิ้นส่วนความจำ (atom) ที่เกี่ยวข้องโดยอัตโนมัติ
เวลาที่ประหยัด: ราว 3~5 นาทีต่อหนึ่งเซสชัน (ตัดเวลาที่เคยต้องหา atom ที่เกี่ยวข้องด้วยมือแล้วเรียกใช้)
ความถี่การทำงาน: 15~25 เซสชันต่อสัปดาห์ (อิงตามพีซีส่วนตัว)
ระยะเวลาใช้งาน: คาดว่า 1 ปีขึ้นไป (เป็นโครงสร้างพื้นฐานจึงมีโอกาสถูกทิ้งต่ำ)
เวลาที่ใช้สร้าง: 4 ชั่วโมง (hook + manifest + ตรวจสอบ atom)
การดูแลรักษา: 0.5 ชั่วโมงต่อเดือน (เพิ่ม·แก้ไข atom)
ROI = (4 นาที × 20 ครั้ง/สัปดาห์ × 52 สัปดาห์) / (4 ชั่วโมง × 60 นาที + 0.5 ชั่วโมง × 12 เดือน × 60 นาที)
= 4,160 นาที / (240 นาที + 360 นาที)
= 4,160 นาที / 600 นาที
≈ 6.9 → อยู่ในช่วง "สร้างทันที" การตัดสินถูกหนุนด้วยสูตร
ตรงนี้มีส่วนที่ต้องซื่อตรง ตัวเลขข้างต้น — 3\~5 นาทีต่อเซสชัน, 15\~25 เซสชันต่อสัปดาห์ — ไม่ใช่การวัดอย่างแม่นยำ แต่เป็นการประมาณที่อิงประสบการณ์การใช้งานของผู้เขียน ไม่ใช่ค่าที่จับเวลาด้วยนาฬิกาจับเวลา ดังนั้น ROI 6.9 ก็ไม่ใช่ค่าที่เชื่อได้ถึงทศนิยม
แต่ก็ไม่เป็นไร เพราะสูตร ROI เป็นเครื่องมือที่ดูจำนวนหลัก ไม่ใช่ความแม่นยำ ถ้าผลออกมาแถวๆ 7 ก็สร้าง ถ้าแถวๆ 0.3 ก็คิดใหม่ การแบ่งระหว่างสองค่านี้ไม่จำเป็นต้องใช้ทศนิยม สิ่งสำคัญคือแม้กระทั่งตอนตัดสินใจว่าจะไม่สร้าง เหตุผลก็ต้องออกมาจากสูตร ไม่ใช่จากสัญชาตญาณในหัว ไม่สร้างเพราะจำนวนหลักไม่ตรงกัน — เมื่อหนึ่งบรรทัดนี้ถูกทิ้งไว้ในการทบทวน พอตัวเลือกเดิมถูกเสนอขึ้นมาอีก ก็ไม่ต้องมาครุ่นคิดซ้ำ
เมื่อตัวเลือกผ่านก็สร้าง แต่การสร้างเป็นเพียงครึ่งหนึ่ง อีกครึ่งหนึ่งคือการลงทะเบียนให้มันทำงานโดยอัตโนมัติตั้งแต่เซสชันถัดไป หากการลงทะเบียนนี้ตกหล่น เครื่องมือก็จะถูกสร้างขึ้นแต่ทิ้งไว้ในจุดที่ไม่มีมือใครเอื้อมถึง และลูปก็ขาดตอนตรงนั้น
แต่ละชนิดของผลลัพธ์มีที่ลงทะเบียนต่างกัน สกิลส่วนกลางให้ใส่ไว้ที่ ~/.claude/skills/ และสร้าง atom คู่มือที่บรรจุวิธีใช้ไว้คู่กัน สกิลของโปรเจกต์ให้วางไว้ที่ .claude/skills/ ของโปรเจกต์นั้น atom ใหม่ให้วางไว้ในโฟลเดอร์ที่เหมาะสม เพิ่มหนึ่งบรรทัดลงในดัชนี MEMORY.md และลงทะเบียนทริกเกอร์ใน JIT manifest — ต้องทำทั้งสามอย่างนี้ การฉีดอัตโนมัติจึงจะอยู่รอด คำสั่งสแลชให้ไว้ที่ ~/.claude/commands/ ส่วน Wrapper ให้เปลี่ยนจุดเข้าใช้งานของเครื่องมือเดิมแล้วแนบ atom คู่มือ
หากตกหล่นการลงทะเบียน ในการทบทวนครั้งถัดไปคำพูด "สร้างไว้แล้วทำไมไม่ได้ใช้" ก็จะออกมาอีก นั่นไม่ใช่คำพูดปรับปรุงใหม่ แต่เป็นรายงานบั๊ก เท่ากับการมาพบการลงทะเบียนที่ตัวเองตกหล่นอีกครั้งในการทบทวน
แม้ลงทะเบียนเสร็จแล้วก็ยังเหลืออีกหนึ่งขั้น คือการเปิดเซสชันใหม่แล้วยืนยันว่ามันทำงานจริงด้วยทริกเกอร์ที่ตั้งใจไว้หรือไม่
1. เริ่มเซสชันใหม่
2. ป้อนทริกเกอร์ (เช่น "สุขภาพครอบครัวเป็นอย่างไร")
3. ตรวจสอบล็อก JIT → atom ที่ตั้งใจถูกฉีดเข้ามาจริงหรือไม่
(~/.claude/hooks/_injection_log.txt)
4. ถ้าไม่ขึ้น → ขยาย regex ทริกเกอร์ใน manifest
หรือเพิ่มเส้นทางการเรียกใช้แบบแมนวล
หากการตรวจสอบนี้ตกหล่น เหตุการณ์ "นึกว่ามีอยู่ แต่พอถึงเวลาที่ต้องใช้กลับไม่ขึ้น" ก็จะเกิดซ้ำ การลงทะเบียนกับการทำงานเป็นคนละเรื่อง การลงทะเบียนคือการวางไฟล์ไว้ การทำงานคือการที่ทริกเกอร์ทำงานได้จริง หาก regex ทริกเกอร์ผิดไปเพียงตัวอักษรเดียว ต่อให้ลงทะเบียนแล้วมันก็จะไม่ขึ้นตลอดกาล
หลังจากปล่อยให้เครื่องมือที่สร้างทำงานราว 1 สัปดาห์ถึง 1 เดือนแล้วก็วัดผล การวัดนี้คือลูกศรเส้นสุดท้ายของลูป นั่นคือลูกศรที่กลับเข้าสู่การทบทวนอีกครั้ง
จำนวนครั้งการทำงานจริงนับจากล็อก JIT หรือล็อกการเรียกใช้คำสั่ง เวลาที่ประหยัดจริงให้บันทึกไว้ในการทบทวนในรูปแบบ "งานที่เมื่อก่อนต้องใช้ N นาที จบในเวลา M นาที" ผลข้างเคียง — การทำงานผิดพลาด การปนเปื้อนคอนเท็กซ์โดยไม่จำเป็น — ก็ดูไปพร้อมกัน แล้ววาง ROI ที่ประมาณไว้ตอนแรกเทียบเคียงกับ ROI ที่วัดได้จริง
ถ้า ROI ที่ประมาณไว้เป็น 6 แต่ ROI ที่วัดได้จริงเป็น 0.8 ก็ทิ้งอย่างไม่ปรานี เพราะความสะอาดของระบบสำคัญกว่าศักดิ์ศรีของคนที่สร้าง หากเครื่องมือที่ไม่ได้ใช้สะสมอยู่ใน manifest สัญญาณรบกวนนั้นจะกัดกินความแม่นยำของการทบทวนครั้งถัดไป
เพียงแต่ก่อนกดปุ่มทิ้ง ให้ตรวจสอบสักครั้ง regex ทริกเกอร์อาจแคบเกินไปจนไม่ทำงานเลยก็ได้ หรืออาจถูกลืมไปเฉยๆ เพราะไม่มีเส้นทางการเรียกใช้แบบแมนวลก็ได้ ให้แยกแยะก่อนว่าเป็นเครื่องมือที่ไม่มีค่าจริงๆ หรือเป็นเครื่องมือที่ดีแต่เส้นทางการทำงานถูกปิดกั้น ถ้าเป็นอย่างแรกก็ทิ้ง ถ้าเป็นอย่างหลังก็เปิดเส้นทางให้
การทิ้งก็ถูกตัดสินในการทบทวนเช่นกัน การตัดสินว่า "จะทิ้งเครื่องมือนี้" นั้นเองคือผลลัพธ์ของ self-improving วงจรที่สร้างอย่างเดียวโดยไม่ล้างออก คือวงจรที่เพิ่มขึ้นทางเดียว และระบบที่เพิ่มขึ้นทางเดียวสุดท้ายก็ถูกน้ำหนักของตัวเองทับ
ลูปปิดแล้วหรือไม่นั้นรู้ได้จากสัญญาณสี่อย่าง
ข้อแรก คำพูดเดิมไม่ถูกพูดซ้ำ หากรายการที่เขียนในการทบทวนครั้งหนึ่งถูกเขียนเป็นครั้งที่สอง ก็หมายความว่าการระบุตัวเลือกหรือการสร้างในรอบแรกล้มเหลวที่ตรงไหนสักแห่ง "คำศัพท์ยังไม่เป็นมาตรฐานเดียวกัน" ที่กล่าวตอนต้นบทซึ่งถูกพูดซ้ำมาหกเดือน — นั่นคือหลักฐานชัดเจนที่สุดของลูปที่เปิดอยู่
ข้อสอง จำนวน manifest และ atom ไม่ได้เพิ่มขึ้นทางเดียวเสมอ มีการทิ้งเกิดขึ้น การถูกจัดระเบียบราว 10\~20% ต่อไตรมาสคือวงจรที่แข็งแรง ระบบที่ไม่เคยลดลงสักครั้ง ก็เหมือนลิ้นชักที่ไม่เคยถูกทำความสะอาดสักครั้ง
ข้อสาม เวลาในการทบทวนลดลง หากระบบเดินดี เวลาที่ใช้นึก "เมื่อวานทำอะไรนะ" ก็จะหายไป และเติมห้าช่องคำพูดให้เต็มก็ใช้เวลา 5 นาทีก็พอ
ข้อสี่ พนักงานใหม่สามารถเข้าร่วมการทบทวนได้ภายใน 1 สัปดาห์ จะเป็นไปได้เมื่อรูปแบบการทบทวนถูกทำให้เป็นมาตรฐาน และ atom กับสกิลถูกทำให้มองเห็นได้
จุดที่ลูปขาดตอนนั้นกำหนดไว้แล้วทุกครั้ง หากรวบรวมโหมดความล้มเหลวไว้ คราวหน้าเมื่อเห็นอาการเดียวกัน ก็หยิบใบสั่งยามาใช้ได้ทันที
| จุดที่ขาด | อาการ | ใบสั่งยา |
|---|---|---|
| ไม่มีคำพูด | 5 ช่องว่างเปล่าทุกครั้ง | เพิ่มคำถามกระตุ้น "มีงานอะไรที่ทำด้วยมือซ้ำสองครั้ง" |
| ไม่ตกผลึกเป็นตัวเลือก | คลุมเครือแบบ "ปรับปรุงโดยรวม" | บังคับให้ทำเป็นปริมาณด้วยผลลัพธ์หนึ่งชิ้น |
| ข้ามการประเมิน ROI | สร้างไปก่อนเลย | ทำสูตร ROI เป็นเทมเพลต 5 นาที |
| สร้างแล้วแต่ไม่ขึ้น | ลงทะเบียนตกหล่น | บังคับใช้เช็กลิสต์การลงทะเบียน |
| ขึ้นแต่ไม่ใช้ | ไม่มีทริกเกอร์·ตั้งค่าผิด | ขยาย regex + ให้เส้นทางแมนวลพร้อมกัน |
| ไม่วัดผล | ไม่มีช่องวัดผลในการทบทวน | เพิ่มช่อง "สิ่งที่จะวัดในการทบทวนครั้งถัดไป" |
โหมดความล้มเหลวแต่ละอย่างถูกพูดออกมาในการทบทวน และคำพูดนั้นก็กลายเป็นข้อมูลขาเข้าของ self-improving อีกครั้ง แม้กระทั่งงานซ่อมลูปก็เกิดขึ้นภายในลูป นี่คือเมตาลูป
หนังสือเล่มนี้ยาว เริ่มต้นจากสถาปัตยกรรมข้อมูล สร้างเครื่องมือที่สร้างเมืองขึ้นมา ออกแบบระบบการต่อสู้ ทำให้การตรวจสอบบนมือถือเป็นอัตโนมัติ ทำให้ต้นทุนเป็นมาตรฐาน และดึง atom ขึ้นมาจากการทบทวน เครื่องมือของทุกบทเหล่านั้นมารวมตัวกันในที่เดียวเพื่อตอบคำถามหนึ่ง คือบทสุดท้ายนี้ สิ่งที่สร้างขึ้นเติบโตด้วยตัวเองหรือไม่
ในที่สุด self-improving ก็ย่อลงเหลือเพียงประโยคเดียว
สิ่งที่ตัดสินใจในการทบทวนทำงานโดยอัตโนมัติตั้งแต่เซสชันถัดไป และการทำงานนั้นถูกวัดผลแล้ววนกลับมาสู่การทบทวนอีกครั้ง
หากไม่ทำงานโดยอัตโนมัติ การทบทวนก็เป็นไดอารี ไดอารีที่เขียนดีนั้นปลอบประโลมใจได้ แต่เปลี่ยนระบบไม่ได้ หากทำงานโดยอัตโนมัติ การทบทวนก็กลายเป็นสมองของระบบ คำพูดของทุกวันเปลี่ยนการกระทำของทุกวัน และผลของการกระทำนั้นก็ทำให้คำพูดครั้งถัดไปแม่นยำยิ่งขึ้น
ทุกสาขาที่กล่าวถึงในหนังสือเล่มนี้ — การออกแบบข้อมูล ระบบ การต่อสู้ มือถือ ต้นทุน และการแยกย่อย Layer ซึ่งเป็นเงื่อนไขตั้งต้นของการสร้างแบบโพรซีเดอรัลและระบบอัตโนมัติ — ล้วนวิวัฒน์อยู่บนลูป self-improving นี้ทั้งสิ้น เครื่องมือเก่าลง โมเดลเปลี่ยนไป โปรเจกต์จบลง แต่ตราบใดที่ลูปยังปิดอยู่ ระบบก็ดีขึ้นกว่าเมื่อวานอีกนิดในวันนี้ นั่นคือสิ่งหนึ่งที่หนังสือเล่มนี้ทิ้งไว้เป็นสิ่งสุดท้าย ไม่ใช่วิธีสร้างเครื่องมือ แต่เป็นวิธีทำให้เครื่องมือเติบโตด้วยตัวเอง
ขอให้การทบทวนครั้งถัดไปของคุณ เป็นการหมุนรอบแรกของลูปนั้น
การประยุกต์นอกเกม หาก "คำศัพท์ยังไม่เป็นมาตรฐานเดียวกัน / หาเอกสารยาก" ถูกเขียนในการทบทวนเหมือนกันแม้กระทั่งตัวอักษรมาหกเดือนแล้ว นั่นไม่ใช่ว่าไม่ได้ทบทวน แต่เป็นเพราะลูปไม่ปิด — เพราะมีความทรงจำของคนคั่นอยู่ระหว่างคำพูดกับการปรับปรุง ไม่ว่าแผนกใด เงื่อนไขของลูปที่ปิดก็เหมือนกัน คำพูดต้องตกผลึกเป็นผลลัพธ์หนึ่งชิ้นที่นำไปปฏิบัติได้ทันที (เทมเพลต·เช็กลิสต์·กฎอัตโนมัติ) ต้องทำงานต่อไปได้แม้คนไม่ต้องจำ และผลของมันต้องถูกวัดแล้ววนกลับมาอีกครั้ง ตัวอย่างเช่น คำพูด "ประชุมยาวเกินไป" ถูกแปลงเป็น "เครื่องมือหนึ่งชิ้นที่รับบันทึกการประชุมแล้วสกัดเฉพาะการตัดสินใจกับสิ่งที่ต้องทำ" และก่อนสร้างก็ดูเพียงจำนวนหลักด้วย (เวลาที่ประหยัด × ความถี่การทำงาน × ระยะเวลาใช้งาน) ÷ (เวลาที่ใช้สร้าง·ดูแล) เพื่อตัดสินว่าจะสร้างทันทีหรือพักไว้ แม้กระทั่งการตัดสินใจว่าจะไม่สร้าง เหตุผลก็ต้องออกมาจากสูตรไม่ใช่สัญชาตญาณ พอตัวเลือกเดิมถูกเสนอขึ้นมาอีกจึงจะไม่ต้องครุ่นคิดซ้ำ
ช่วยเติมช่อง self-improving 5 ช่องของการทบทวนวันนี้
แต่ละคำพูดให้ทำเป็นปริมาณด้วย "ผลลัพธ์หนึ่งชิ้น" และแต่ละตัวเลือกให้ประมาณ ROI
ด้วย (นาทีที่ประหยัด × การทำงานต่อสัปดาห์ × สัปดาห์ที่ใช้งาน) / (นาทีที่ใช้สร้าง + นาทีดูแลรักษา)
แล้วแนบช่วงการตัดสิน (ทันที/หนึ่งสัปดาห์/พักไว้/ทิ้ง) ให้ด้วย
ตัวเลขที่ประมาณให้ระบุเหตุผลหนึ่งบรรทัด และถ้าไม่ใช่การวัดอย่างแม่นยำให้ระบุว่า "ประมาณ"
ไม่มีทีมก็ได้ ถ้าอยู่คนเดียวก็ย่อแบบนี้ ในตอนปลายวัน บันทึกหนึ่งบรรทัด — "วันนี้มีงานอะไรที่ทำด้วยมือซ้ำสองครั้ง" แล้วเปลี่ยนหนึ่งบรรทัดนั้นในวันรุ่งขึ้นให้เป็นอัตโนมัติหนึ่งบรรทัด (atom·นามแฝง·สนิปเพ็ต) หลังจากหนึ่งสัปดาห์ก็ดูเพียงว่าหนึ่งบรรทัดนั้นถูกใช้จริงหรือไม่ ถ้าถูกใช้ก็เก็บไว้ ถ้าไม่ถูกใช้ก็ลบทิ้ง คำพูดหนึ่งบรรทัด → อัตโนมัติหนึ่งบรรทัด → การวัดผลหนึ่งบรรทัด หน่วยที่เล็กที่สุดของลูปคือสามบรรทัดนี้
ผู้อ่านกลุ่มแรก: นักออกแบบเกม (Game Designer) ที่ดึง LLM มาใช้ในงานจริง (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านคนเดียว/มือสมัครเล่น: §22.1.7 「ถ้าทำคนเดียว แค่นี้ก็พอ」
เคยพิมพ์ว่า "ช่วยสร้างบทพูดของ NPC ตัวนี้ 5 บรรทัด" เพียงเพื่อจะได้บทพูด NPC สามบรรทัด สิ่งที่ได้กลับมาคือบทพูดห้าบรรทัดที่เอาไปแปะกับเกมแฟนตาซีเกมไหนก็ไม่เคอะเขิน — และเพราะอย่างนั้นมันจึงไม่เข้ากับเกมของเราที่ตรงไหนเลย โทนของมันว่างเปล่า มันไม่รู้ว่า NPC ตัวนี้เป็นใคร และมันไม่เชื่อมต่อกับบทพูดข้างเคียง แต่ละบรรทัดถูกต้องตามไวยากรณ์ไม่มีที่ติ ปัญหาคือการรับห้าบรรทัดนั้นมาตรวจสอบใช้เวลามากกว่าการที่ผมเขียนเองตั้งแต่ต้นเสียอีก
บทนี้ว่าด้วยวิธีเปลี่ยนคำสั่งบรรทัดเดียวนั้นให้กลายเป็น ใบสั่งงานหนึ่งหน้า ทฤษฎีพรอมต์โดยทั่วไปมีอยู่เพียงพอแล้วในหนังสือเล่มอื่น ที่นี่ผมจะแสดงสี่สิ่งที่นักออกแบบเกมต้องถือไว้ในมือเมื่อนั่งลงต่อหน้า LLM — บริบท รูปแบบผลลัพธ์ การกันอาการหลอน และการขอตรวจสอบ — ไม่ใช่ในรูปท่อนนามธรรม แต่ผ่าน พรอมต์ npc_dialogue หนึ่งหน้าที่รันจริง ผมจะตามไปจนจบหนึ่งรอบว่าใส่อะไรลงในพรอมต์นั้น ได้อะไรออกมา และปฏิเสธอะไรไป
ใบสั่งงานที่ดีไม่ใช่ใบสั่งงานที่สั้น เหมือนเวลามอบงานให้คนใหม่แล้วบอกว่า "ทำให้ดี ๆ หน่อยนะ" ก็จะได้ผลลัพธ์ที่ต่างกันทุกครั้ง การบอก LLM ว่า "ช่วยสร้างบทพูดให้หน่อย" ก็จะได้ค่าเฉลี่ยของเกม RPG ทั่วไปทุกครั้ง โมเดลเดียวกันแต่ถ้าใบสั่งงานต่างกัน ผลลัพธ์ก็แตกต่าง — การที่คุณภาพผลลัพธ์ต่างกันหลายเท่าเป็นความเชื่อทั่วไปในวงการ และหนังสือเล่มนี้จะไม่สัญญาตัวคูณนั้นเป็นตัวเลข แต่ทิศทางนั้นชัดเจน พรอมต์ที่ใส่บริบทและข้อจำกัดลงไปจะให้ผลลัพธ์ที่มีภาระการตรวจสอบน้อยกว่าคำสั่งบรรทัดเดียวเปล่า ๆ
สี่สิ่งที่พรอมต์ของนักออกแบบเกมต้องตอบสนองพร้อมกันมีดังนี้
| หลักการ | นิยามหนึ่งบรรทัด | ถ้าไม่ทำตาม |
|---|---|---|
| ① บริบท | ให้สิ่งที่จะใช้อ้างอิงเพื่อตอบ (วิสัยทัศน์·voice·บทพูดข้างเคียง) | ได้ค่าเฉลี่ยแฟนตาซีทั่วไป |
| ② รูปแบบผลลัพธ์ | ตรึงจำนวน·ความยาว·ป้ายกำกับ·สิ่งต้องห้ามไว้แน่น | การตรวจสอบบานปลายเป็นการตีความเรียงความอิสระ |
| ③ การกันอาการหลอน | ระบุชัดว่า "ห้ามสร้างนอกเหนือจากข้อมูลที่ให้มา" | กุการตั้งค่าที่ไม่มีอยู่ขึ้นมา |
| ④ การขอตรวจสอบ | ให้ตัวมันเองแสดงว่าผลลัพธ์ตรงตามเกณฑ์ใด | ไม่มีหลักฐานให้ผ่านด่านตรวจสอบ |
ถ้าท่องสี่บรรทัดนี้แยกกัน มักจะตกหล่นไปหนึ่งหรือสองข้อเสมอ ดังนั้นวิธีของบทนี้คือ วางหลักทั้งสี่ข้อเป็นสล็อตไว้ในพรอมต์หน้าเดียว ถ้าสล็อตว่างก็จะเห็นได้ทันทีว่าตกหลักข้อนั้นไป หัวข้อถัดไปจะดูพรอมต์หน้านั้นทั้งหน้า
ผมจะคัดลอก prompts/narrative/npc_dialogue_v3.txt ที่ใช้งานจริงในโปรเจกต์ของผู้เขียน (MMORPG ที่เน้นมือถือเป็นหลัก ต่อไปจะเรียกว่า "โปรเจกต์ A") มาตามเดิม โดยทำให้เป็นนิรนาม ชื่อเมือง·ชื่อ NPC และชื่อเฉพาะของบริษัทถูกแทนที่สำหรับหนังสือ และผลลัพธ์เป็นการเรียบเรียงใหม่จากเซสชันจริง พรอมต์ที่ป้อนเข้าอยู่ในรูปแบบที่คัดลอกไปใช้ได้ทันที
ก่อนอื่นเติมข้อมูลที่พรอมต์จะอ้างอิงลงในสล็อต ทั้งสามไม่ใช่การเขียนขึ้นใหม่ แต่ดึงออกมาจากสินทรัพย์ที่มีอยู่แล้ว
# อินพุตสล็อต (แปะไว้เหนือเนื้อหาพรอมต์)
L0_วิสัยทัศน์: # การแคช — ไม่ส่งซ้ำทุกครั้งที่เรียก
world_premise: "สหพันธ์นครรัฐของเหล่านักปราชญ์ที่ผนึกพลังเวทกำลังเย็นลง"
tone_manifesto: "ระงับความรู้สึกอ่อนไหว ตัวละครไม่อธิบายอารมณ์ แต่เผยออกมาผ่านการกระทำ·วัตถุ"
voice_profile: # ตัวตนของ NPC ตัวนี้ (5 รายการ)
id: npc_doren_vale
ช่วงอายุ: "วัย 50"
นิสัยการพูด: "พูดด้วยตัวเลขเท่านั้น แทบไม่ใช้คำคุณศัพท์"
ความรู้โลกในเกม: "บันทึกแรงสั่นสะเทือนละเอียดของเส้นผนึกมา 30 ปี ไม่รู้สถานการณ์ภายนอกกิลด์นักปราชญ์"
สิ่งต้องห้าม: "ห้ามใช้คำศัพท์เชิงไสยศาสตร์ เช่น คำพยากรณ์·โชคชะตา·เทพเจ้า (โทนเมืองคือ scholarly_strict)"
ความสัมพันธ์: "ปฏิบัติต่อผู้เล่นในฐานะ 'ตัวแปรภายนอกที่เป็นวัตถุสังเกตการณ์' ทั้งความระแวงและความเป็นมิตรต่างก็อ่อน"
บทพูดข้างเคียง: # บริบทก่อนหน้า — บรรทัดที่ออกมาแล้วในซีนเดียวกัน
- (ผู้เล่น) "ไฟบนหอระฆังติดอยู่ทั้งคืนเลย เกิดอะไรขึ้นหรือ"
ตรงนี้ 5 รายการของ voice_profile คือหัวใจของหลักการ ① อายุ·นิสัยการพูด·ขอบเขตความรู้·สิ่งต้องห้าม·ความสัมพันธ์ — ห้าข้อนี้ทำให้ "โดเรน เวล" แตกต่างจาก NPC ตัวอื่น โดยเฉพาะ ขอบเขตของความรู้โลกในเกม (ไม่รู้สถานการณ์ภายนอกกิลด์) คืองานเตรียมการล่วงหน้าของหลักการ ③ การกันอาการหลอน ต้องระบุสิ่งที่มันไม่รู้ไว้ AI จึงจะไม่ออกไปนอกขอบเขตนั้น
[L0 컨텍스트] world_premise + tone_manifesto (แคชไว้)
[voice_profile] npc_doren_vale 5 รายการ (yaml ด้านบน)
[인접 대사] คำถามก่อนหน้าของผู้เล่น 1 บรรทัด
ดูข้อมูลข้างต้นแล้วเขียนบทพูดที่ doren_vale ตอบคำถามของผู้เล่นให้หน่อย
[출력 형식 — 원칙 ②] พอดี 3 บรรทัด หนึ่งบรรทัดต่อหนึ่งประโยค แต่ละบรรทัดไม่เกิน 40 ตัวอักษร ท้ายบรรทัดติดป้ายอารมณ์หนึ่งในสาม (เฉยเมย|ระแวง|เย้ยหยัน) ตัดคำอธิบายอื่น·เครื่องหมายคำพูดออก
[환각 차단 — 원칙 ③] เขียนเฉพาะสิ่งที่มีอยู่ในข้อมูลข้างต้น อย่ากุข้อเท็จจริงเกี่ยวกับผนึก·หอระฆังขึ้นมาใหม่ ถ้าต้องการสิ่งที่ไม่มีในข้อมูล ให้เขียนบรรทัดนั้นว่า '[ไม่มีข้อมูล: ต้องการอะไร]' ห้ามใช้คำศัพท์อย่างคำพยากรณ์·โชคชะตา·เทพเจ้า
[검증 요청 — 원칙 ④] ใต้บทพูด ให้เขียนว่าแต่ละข้อใน voice_profile ทั้ง 5 ข้อ ตรงตาม/ละเมิด บรรทัดละข้อ และบรรทัดที่ไม่มั่นใจให้ติด ★ พร้อมเหตุผลหนึ่งบรรทัด
ขอให้สังเกตว่าหลักทั้งสี่ข้ออยู่ครบในหน้าเดียวนี้ สล็อตด้านบนคือ ① บล็อก [출력 형식] คือ ② บล็อก [환각 차단] คือ ③ และบล็อก [검증 요청] คือ ④ มากกว่าครึ่งหนึ่งของพรอมต์ไม่ใช่ "เขียนอะไร" แต่เป็นข้อจำกัดว่า "เขียนอย่างไร อะไรที่ต้องไม่เขียน และตรวจสอบตัวเองอย่างไร" เพราะถ้าให้อิสระก็จะได้ค่าเฉลี่ย งานจึงคือการตัดทอนอิสระลง
เส้นผนึกกำลังเย็นลง 12.4 เมื่อวานคือ 12.1 (เฉยเมย)
ไม่ใช่เรื่องดี แต่งานของข้าคือการบันทึกเท่านั้น (เย้ยหยัน)
เหตุใดเจ้าจึงถาม ไม่ใช่สิ่งที่ข้าสังเกตการณ์ (ระแวง)
[검증 — voice_profile 5 ข้อ]
- ช่วงอายุ (วัย 50): ตรงตาม น้ำเสียงสำรวมของนักปราชญ์สูงวัย
- นิสัยการพูด (ตัวเลขเท่านั้น): ตรงตาม บรรทัดที่ 1 มีตัวเลข 2 ตัว แต่บรรทัดที่ 2·3 ไม่มีตัวเลข ★
- ความรู้โลกในเกม (ไม่รู้ภายนอกกิลด์): ตรงตาม ไม่มีการพูดถึงสถานการณ์ภายนอก
- สิ่งต้องห้าม (ห้ามไสยศาสตร์): ตรงตาม คำว่าคำพยากรณ์·โชคชะตา·เทพเจ้า 0 ครั้ง
- ความสัมพันธ์ (ปฏิบัติเป็นตัวแปรภายนอก): ตรงตาม บรรทัดที่ 3 ผลักผู้เล่นออกไปนอกขอบเขตการสังเกตการณ์
★ ข้อควรระวัง: ถ้าตีความรายการนิสัยการพูดว่า 'พูดด้วยตัวเลขเท่านั้น' อย่างเคร่งครัด
บรรทัดที่ 2·3 อาจถือว่าละเมิด การจะบังคับให้ทุกบรรทัดมีตัวเลขหรือไม่ ต้องอาศัยวิจารณญาณของคน
ส่วนที่มีค่าที่สุดในผลลัพธ์นี้ไม่ใช่บทพูดสามบรรทัด แต่เป็น ★ หนึ่งบรรทัดที่อยู่ล่างสุด AI รายงานจุดที่กำกวมในผลลัพธ์ของตัวเองด้วยตัวเอง แล้วส่งต่อให้คน พรอมต์ที่ดีทำให้ AI สามารถพูดได้ว่า "ส่วนนี้ผมไม่มั่นใจ" — เป็นผลโดยตรงของการใส่หลักการ ④
เราไม่รับผลลัพธ์มาดื้อ ๆ คนเป็นผู้ตัดสิน ★ ที่ AI ยกขึ้นมา ในเซสชันนี้มีอยู่หนึ่งบรรทัดที่สะดุดจริง ๆ
คำว่า "ดี" ในบรรทัดที่ 2 "ไม่ใช่เรื่องดี" ขัดกับนิสัยการพูดใน voice_profile ("แทบไม่ใช้คำคุณศัพท์") นั่นคือจุดที่ AI รายงานด้วย ★ โดเรน เวล เป็นตัวละครที่พูดด้วยตัวเลขแทนคำคุณศัพท์เชิงตัดสินคุณค่า แต่ "ไม่ใช่เรื่องดี" ไหลไปเป็นน้ำเสียงของ NPC คนแก่ทั่วไป เป็นบรรทัดที่ทำให้โทนพร่ามัว
จึงขอใหม่
บรรทัดที่ 2 "ไม่ใช่เรื่องดี" ใช้คำคุณศัพท์ ('ดี') จึงละเมิดนิสัยการพูดใน voice_profile
เขียนเฉพาะบรรทัดนี้ใหม่ด้วยตัวเลขหรือคำศัพท์เชิงสังเกตการณ์ บรรทัดที่ 1·3 คงไว้
กฎรูปแบบ·อาการหลอน·การตรวจสอบ ใช้ตามเดิม
AI ตอบบรรทัดที่ 2 ใหม่เป็น "เมื่อสามปีก่อนคือ 9.0 นี่คือคำตอบ (เฉยเมย)" เผยวิกฤตผ่านการเปลี่ยนแปลงของตัวเลขโดยไม่ใช้คำคุณศัพท์ และผ่าน voice_profile ทั้ง 5 ข้ออีกครั้ง ปิดจบในการไป-กลับเพียงครั้งเดียว เมื่อเทียบระหว่างการเขียนบทพูดสามบรรทัดที่ปรับโทนด้วยมือตั้งแต่ต้น กับ พรอมต์หนึ่งหน้าที่เติมสล็อต + ตรวจ ★ + ไป-กลับ 1 ครั้ง — ข้อสรุปของเซสชันนี้คือ อย่างหลังมีภาระการตรวจสอบน้อยกว่า (อิงประสบการณ์ของผู้เขียน เวลาสัมบูรณ์ขึ้นกับความยากของโทน NPC จึงควรอ่านเป็นทิศทาง)
ถ้าบันทึกไว้เป็นหน้าเดียวว่าทำไมพรอมต์ข้างต้นจึงซ้อนกันตามลำดับนั้น จากพรอมต์ครั้งถัดไปก็จะสร้างได้เหมือนเติมช่องว่างในสล็อต บริบทซ้อนจากล่างขึ้นบน เริ่มจากของหนัก (แทบไม่เปลี่ยน) ไปหาของเบา (เปลี่ยนทุกครั้ง) ชั้นที่ไม่เปลี่ยนจะถูกแคชเพื่อประหยัดต้นทุน (§22.1.5)
หน้าเดียวใน §22.1.2 คือภาพนี้ทุกประการ L0·L1 ดึงมาจากข้อมูลแล้วแปะลงสล็อต (รากฐานของหลักการ ①·③) และ L3 ใส่สามบล็อกคือรูปแบบ·อาการหลอน·การตรวจสอบ (หลักการ ②·③·④) เมื่อรับบทพูด NPC ตัวถัดไป สิ่งที่เปลี่ยนคือ voice_profile ใน L1 และบทพูดข้างเคียงใน L2 เท่านั้น โครง L0 และ L3 นำกลับมาใช้ซ้ำ — ด้วยเหตุนี้พรอมต์จึงกลายเป็น "ไลบรารี"
พรอมต์ npc_dialogue ข้างต้นไม่ใช่ของที่ใช้ครั้งเดียวแล้วทิ้ง แต่เก็บไว้ในไฟล์แยกตามสาขา·ตามงาน แล้วเรียกใช้แทนการเขียนใหม่ทุกครั้ง โฟลเดอร์พรอมต์ของโปรเจกต์ A หน้าตาแบบนี้
prompts/
├── narrative/
│ ├── npc_dialogue_v3.txt # ← §22.1.2 คือไฟล์นี้
│ ├── quest_synopsis_v2.txt
│ └── consistency_check_v1.txt
├── balance/
│ ├── change_proposal_v2.txt
│ └── outlier_analysis_v1.txt
├── content/
│ ├── city_npc_batch_v2.txt
│ └── side_quest_v3.txt
└── meta/
├── meeting_summary_v2.txt
└── decision_card_v1.txt
_v3 ที่ท้ายชื่อไฟล์คือหัวใจ พรอมต์ไม่ใช่ของที่สร้างครั้งเดียวแล้วจบ แต่เป็น สินทรัพย์ที่ใกล้เคียงกับการตัดสินใจ ดังนั้นทุกครั้งที่เปลี่ยน จึงวัดการเปลี่ยนแปลงของผลลัพธ์แล้วยกเวอร์ชันขึ้น เส้นทางที่ npc_dialogue มาถึง v3 ก็เป็นเช่นนั้น
flowchart LR
A["พรอมต์ v2
(ไม่มีสล็อตตรวจสอบ)"] --> B["ป้อนอินพุตเดียวกัน N ชุด
ออกผลทั้ง v2·v3"]
B --> C{"เทียบ A/B
อัตราทิ้ง·ละเมิดโทน·เวลาตรวจ"}
C -->|v3 น้อยกว่า| D["เลือก v3
npc_dialogue_v3.txt"]
C -->|ไม่ต่างกัน| E["คง v2
(ปฏิเสธการเปลี่ยน)"]
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class B ai;
class C human;
class A data;
class D pass;
class E fail;
การเปลี่ยนแปลงจริงที่ยกจาก v2 เป็น v3 คือบล็อก [검증 요청] ใน §22.1.2 ใน v2 ไม่มีสล็อตให้ AI ตรวจสอบผลลัพธ์ของตัวเองทีละรายการแล้วติด ★ พอใส่บล็อกนั้นเข้าไป AI ก็เริ่มรายงานบรรทัดที่กำกวมก่อนเหมือนในขั้นที่ 4 ของ §22.1.2 และภาระที่คนต้องอ่านทั้งหมดตั้งแต่ต้นเพื่อจับก็ลดลง เราไม่ยกเวอร์ชันด้วย "รู้สึกว่าดีขึ้น" โดยไม่วัด แต่จะวางผลลัพธ์ v2·v3 เคียงกันด้วยชุดอินพุตเดียวกัน แล้วตรวจว่าจำนวนการละเมิดโทนและเวลาตรวจสอบลดลงจริงหรือไม่ก่อนจึงเลือกใช้
ผลที่ใหญ่ที่สุดที่ไลบรารีให้คือสมาชิกใหม่ วันแรกที่เข้าทำงาน ถ้าเรียก npc_dialogue_v3.txt ก็จะได้ใช้โครงสร้างสล็อต 4 ชั้นที่รุ่นพี่ปรับมาแล้วหลายสิบรอบตั้งแต่เริ่มต้น ก่อนจะซึมซับ "วิธีเขียนพรอมต์ให้ดี" ด้วยตัวเอง เขาก็มีพรอมต์หนึ่งหน้าที่เขียนมาดีแล้วอยู่ในมือ
เมื่อพรอมต์ยาวขึ้น ต้นทุนโทเค็นก็ตามมา บทนี้จะไม่เขียนตัวคูณที่ยังไม่ผ่านการตรวจสอบอย่าง "ทำให้เป็นมาตรฐานแล้วต้นทุน ×2 หายไป" แต่จะพูดเฉพาะสิ่งที่วัดได้จริงเท่านั้น
กลไกเชิงโครงสร้างที่จัดการต้นทุนมีอยู่สองอย่าง อย่างแรก เหตุผลที่ §22.1.3 วาง L0·L1 ไว้ด้านล่างก็คือการแคช ถ้าแคชชั้นวิสัยทัศน์·โทนที่แทบไม่เปลี่ยนไว้ ก็จะไม่ส่งและไม่คิดเงินชั้นนั้นซ้ำในทุกการเรียก เมื่อสร้างบทพูด NPC 100 ครั้ง ความต่างระหว่างการส่ง L0 ซ้ำ 100 ครั้งกับการแคชเพียงครั้งเดียวจะถ่างออกเรื่อย ๆ เมื่อจำนวนการเรียกสะสมขึ้น อย่างที่สอง ตั้ง cap โทเค็นต่อการเรียก เพื่อไม่ยัดงานมากเกินไปลงในพรอมต์เดียว
สิ่งสำคัญตรงนี้คือ ที่ที่ต้นทุนถูกวัดมีอยู่จริง ในระบบ atom ของโปรเจกต์ A มี _economy_log/ (ล็อกความคุ้มค่าเชิงโทเค็น·เวลา) และ _roi_report.md (รายงาน ROI (Return on Investment, ผลตอบแทนต่อการลงทุน)) ดำรงอยู่ในฐานะเมตาข้อมูลของการดำเนินงาน ผลของการทำพรอมต์ให้เป็นมาตรฐานคือการติดตามด้วยค่าที่วัดจริงจากล็อกนี้ ไม่ใช่การเขียนตัวเลขที่ดูน่าเชื่อลงในตารางของเนื้อหาแล้วอ้าง หลักการของหนังสือเล่มนี้มีอยู่สามข้อ
_economy_log| รูปแบบ | ทำไมจึงล้มเหลว | วิธีรักษา |
|---|---|---|
| คำสั่งบรรทัดเดียว "ช่วยสร้างบทพูด 5 บรรทัด" | บริบท 0 → ค่าเฉลี่ย RPG ทั่วไป | พรอมต์สล็อต 4 ชั้นของ §22.1.2 |
| ไม่เขียน 'ขอบเขตที่ไม่รู้' ลงใน voice_profile | AI กุการตั้งค่านอกข้อมูลขึ้นมา | ระบุข้อจำกัดในสล็อตความรู้โลกในเกม (หลักการ ③) |
| รับแค่ผลลัพธ์โดยไม่มีสล็อตตรวจสอบ | คนต้องอ่านทั้งหมดตั้งแต่ต้น | ตรวจตัวเอง + ★ ด้วยบล็อก [검증 요청] (หลักการ ④) |
| เขียนพรอมต์ใหม่ทุกครั้ง | ปั้นโนว์ฮาวเดิมจากศูนย์ซ้ำ ๆ | ไลบรารี prompts/ + เวอร์ชัน |
| เลือกใช้การเปลี่ยนพรอมต์ด้วยความรู้สึก | ตรวจไม่ได้ว่าดีขึ้นหรือไม่ | วัด A/B ด้วยอินพุตเดียวกันก่อนยกเวอร์ชัน |
| ส่งบริบทยาวซ้ำทุกการเรียก | ต้นทุนโทเค็นสะสมตามจำนวนการเรียก | แคช L0·L1 + cap ต่อการเรียก |
ข้อที่หกถูกพบช้าที่สุด ต้นทุนไม่เจ็บเมื่อเรียกครั้งเดียว แต่เผยออกมาใน _economy_log หลังจากการผลิตจำนวนมากสะสมขึ้น
การประยุกต์นอกเกม การที่คำสั่งบรรทัดเดียวเรียกผลลัพธ์ค่าเฉลี่ยที่ "เอาไปแปะที่ไหนก็ไม่เคอะเขิน และเพราะอย่างนั้นจึงไม่เข้ากับงานของผม" ขึ้นมานั้น ไม่ใช่ปัญหาเฉพาะของบทพูดในเกม พรอมต์คือใบสั่งงานที่มอบงานให้คนใหม่ ดังนั้นถ้าวางสี่สิ่ง — จะใช้อะไรอ้างอิงเพื่อตอบ (บริบท)·จำนวน·ความยาว·สิ่งต้องห้าม (รูปแบบผลลัพธ์)·"อย่ากุสิ่งนอกข้อมูลขึ้นมา" (การกันอาการหลอน)·"แสดงด้วยตัวเองว่าตรงตามเกณฑ์ใด" (การขอตรวจสอบ) — ไว้ในหน้าเดียว ก็จะได้ผลลัพธ์ที่มีภาระการตรวจสอบน้อย ตัวอย่างเช่น เมื่อเจ้าหน้าที่ฝ่ายบุคคลรับร่างประกาศรับสมัครงาน ถ้าระบุชัดว่า "ให้เขียนเฉพาะรายการที่มีอยู่ในข้อมูลข้อกำหนดของตำแหน่ง ส่วนสวัสดิการ·เงินเดือนที่ไม่มีในข้อมูล อย่ากุขึ้นมา แต่ให้ใส่ว่า [ต้องยืนยัน]" ก็จะกันอุบัติเหตุที่เงื่อนไขซึ่งถูกกุขึ้นอย่างน่าเชื่อรั่วไหลเข้าไปในประกาศได้ ถ้าเก็บใบสั่งงานหนึ่งหน้าของงานที่ทำบ่อยไว้เป็นไฟล์ นั่นก็จะกลายเป็นเส้นเริ่มต้นของเพื่อนร่วมงานทันที
ถ้าทำคนเดียว แค่นี้ก็พอ: ไม่ต้องมีทั้งไลบรารีและการแคช เลือก NPC หนึ่งตัวจากเกมของคุณ (หรือเกมที่คุณชอบ) แล้วเขียน 5 รายการของ voice_profile ในขั้นที่ 1 ของ §22.1.2 (อายุ·นิสัยการพูด·ขอบเขตที่รู้·สิ่งต้องห้าม·ความสัมพันธ์) ด้วยมือ จากนั้นแปะเนื้อหาพรอมต์ในขั้นที่ 2 ตามเดิมแล้วลองรันหนึ่งครั้ง ลองเลือกบรรทัดที่ขัดกับ voice_profile หนึ่งบรรทัดจากสามบรรทัดที่ได้มาด้วยตัวเอง แล้วแย้งกลับว่า "บรรทัดนี้ละเมิดรายการนิสัยการพูด เขียนเฉพาะบรรทัดนั้นใหม่" คุณก็จะเข้าใจด้วยตัวเองว่าทั้งสี่สล็อตของพรอมต์แต่ละอันทำหน้าที่อะไร
ถ้าทำเป็นทีม ให้เริ่มจากหนึ่งขั้นถัดไปนี้ เลือกงานที่ทำบ่อยหนึ่งอย่าง (เช่น บทพูด NPC) แล้ววางพรอมต์หนึ่งหน้ารูปแบบ §22.1.2 ลงเป็นไฟล์ใน prompts/narrative/ เริ่มจากตรวจว่าใส่สี่บล็อก (สล็อต·รูปแบบ·อาการหลอน·การตรวจสอบ) ครบแล้วหรือไม่ ไฟล์เดียวนั้นก็จะกลายเป็นเส้นเริ่มต้นของสมาชิกใหม่ในทีมทันที การจัดการเวอร์ชันและการแคชค่อยมาทีหลัง
เส้นทางขั้นต่ำผ่านแชตบอตเว็บ (โดยไม่ต้องใช้เทอร์มินัล) — หลักสี่ข้อของบทนี้ทำงานได้เหมือนเดิมด้วยช่องป้อนของแชตบอตเว็บ (ChatGPT หรือ Claude เว็บ) ช่องเดียว โดยไม่ต้องมีไฟล์·ไลบรารี·การแคช เพราะวิศวกรรมพรอมต์ไม่ใช่เรื่องของเครื่องมือ แต่เป็นเรื่องของ "ใส่อะไรลงในหน้าเดียว" สองขั้นด้านล่างคือเส้นทางหลัก
1. เลือกงานของคุณหนึ่งงาน แล้วเขียนห้ารายการในขั้นที่ 1 ของ §22.1.2 (อายุ·นิสัยการพูด·ขอบเขตที่รู้·สิ่งต้องห้าม·ความสัมพันธ์ ถ้าเป็นนอกเกมให้อ่านเปลี่ยนเป็น 'เป้าหมาย·น้ำเสียง·ขอบเขตหลักฐาน·สิ่งต้องห้าม·ความสัมพันธ์') ด้วยมือ ไม่ต้องมีทั้ง YAML และไฟล์ เขียนเป็นข้อความลงในช่องป้อนของแชตบอตก็ได้
2. ใต้นั้นให้แปะเนื้อหาพรอมต์ในขั้นที่ 2 ของ §22.1.2 ตามเดิม โดยตรวจเพียงว่าใส่สี่บล็อกครบหรือไม่ — [출력 형식] (จำนวน·ความยาว·ป้าย·สิ่งต้องห้าม)·[환각 차단] ("อย่ากุสิ่งนอกข้อมูลขึ้นมา ถ้าจำเป็นให้ใส่ [ไม่มีข้อมูล]")·[검증 요청] ("เขียนตรงตาม/ละเมิดทีละรายการ ถ้าไม่มั่นใจให้ติด ★") หลังจากรันแล้ว คนตัดสินเฉพาะบรรทัดที่ติด ★ แล้วแย้งกลับหนึ่งครั้งก็ปิดจบหนึ่งรอบ ไลบรารี·เวอร์ชัน·การแคชค่อยนำมาใช้เมื่อต้องใช้พรอมต์เดิมซ้ำ ๆ จึงค่อยเริ่ม
22.2 จะว่าด้วยอาการหลอนและความปลอดภัย ถ้าหลักการ ③ ของบทนี้ (สล็อตการกันอาการหลอน) คือแนวป้องกันด่านแรกภายในพรอมต์หนึ่งหน้า 22.2 จะดูแนวป้องกันหลายชั้นที่จับอาการหลอนซึ่งทะลุแนวป้องกันนั้นออกมาในระดับการดำเนินงาน
_economy_log ไม่ใช้ตัวคูณที่ปรุงแต่งผู้อ่านหลัก: นักออกแบบเกม (Game Designer) ที่ใช้ AI ผลิตเอกสาร ข้อมูล และบันทึกการตัดสินใจจำนวนมาก (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านที่ทำคนเดียว/เป็นงานอดิเรก: §22.2.7 「ถ้าทำคนเดียว เท่านี้ก็พอ」
เรื่องนี้เกิดในวันที่ผู้เขียนให้ AI สรุปบันทึกการประชุม 17 ฉบับแล้วจัดเป็นการ์ดการตัดสินใจ (decision card) ผลลัพธ์ออกมาเรียบร้อย ทั้ง ID ของการตัดสินใจ วันที่ประชุมที่อ้างอิง ไปจนถึงเหตุผลบรรทัดเดียว ฟอร์แมตสมบูรณ์ไร้ที่ติ ในการ์ดใบหนึ่งเขียนว่า "ที่ประชุม Combat TF วันที่ 2026-04-18 มีมติยืนยันนโยบายคูลดาวน์" ปัญหาคือวันนั้นไม่มีการประชุม Combat TF เลย AI กลับเอาวาระและวันที่ของการประชุมอื่นมาปะปนกัน แล้วกุการ์ดที่ดูน่าเชื่อขึ้นมาหนึ่งใบ และเพราะฟอร์แมตสมบูรณ์ การ์ดใบนั้นจึงเกือบหลุดเข้าไปอยู่ในบันทึกการตัดสินใจของทีมโดยตรง
นี่คืออาการหลอน (hallucination) ของ LLM ยิ่งไม่รู้ มันยิ่งตอบอย่างมั่นใจ ถ้าเทียบกับคน ก็เหมือนเพื่อนร่วมงานที่ในที่ประชุมพูดหนักแน่นว่า "อ๋อ เรื่องนั้นตัดสินกันไปแล้วแบบนั้น" แต่พอตรวจดูจริงกลับไม่เคยมีมติเช่นนั้น คำพูดประโยคเดียวนั้น เมื่อไหลเข้าไปอยู่ในชีตข้อมูล คำตอบ CS หรือสินทรัพย์ atom ก็กลายเป็นอุบัติเหตุ บทนี้ไม่ได้พูดถึงวิธีปิดปากเพื่อนร่วมงานคนนั้น — เพราะมันเป็นไปไม่ได้ — แต่พูดถึงวิธีตั้ง verification gate (ด่านที่มนุษย์หรือเครื่องตรวจสอบผลลัพธ์) ที่คำพูดของมันต้องผ่านก่อนเสมอ ทฤษฎีทั่วไปเรื่องอาการหลอนมีอยู่มากในหนังสือเล่มอื่นแล้ว บทนี้จึงโฟกัสเฉพาะ จุดที่สกัดมันด้วยเวิร์กโฟลว์ AI เท่านั้น
ไม่มีพรอมต์ใดที่ทำให้อาการหลอนเป็นศูนย์ได้ โมเดลที่ใหญ่ขึ้นและพรอมต์ที่ดีขึ้นช่วยลดความถี่ลงได้ แต่ไม่ถึงศูนย์ จุดตั้งต้นของการดำเนินงานจึงไม่ใช่ "กำจัดอาการหลอน" แต่ต้องเป็น "ตั้งด่านดักก่อนที่อาการหลอนจะแตะการตัดสินใจหรือข้อมูล"
หลักการแกนกลางของด่านมีเพียงข้อเดียว สิ่งที่ LLM กุขึ้นได้ (การอ้างอิง·ตัวเลข·ID) ให้ตรวจสอบจากที่อื่นที่ไม่ใช่ LLM แหล่งตรวจสอบมีอยู่หนึ่งในสามอย่าง คือ โค้ด (เชิงกำหนด) เอกสารต้นฉบับ (grep) หรือสายตาของมนุษย์ การถาม LLM ซ้ำว่า "ช่วยตรวจดูหน่อยว่าถูกไหม" ก็เป็นขั้นหนึ่งของด่านได้ แต่เป็นเพียงตัวช่วย ไม่ใช่ผู้ตัดสินขั้นสุดท้าย
ตรงนี้ขอสรุปจุดที่นักออกแบบเกมสับสนบ่อยที่สุดก่อน พื้นที่ที่อาการหลอนเกิดง่ายกับพื้นที่ที่เกิดยากนั้นแตกต่างกันอย่างชัดเจน
| งาน | ความเสี่ยงหลอน | ทำไม | ด่าน |
|---|---|---|---|
| การคำนวณตัวเลข (รางวัล·ความน่าจะเป็น) | สูงมาก | LLM ประมาณการคิดเลข | คำนวณด้วยโค้ด ห้าม LLM ทำ |
| การอ้างอิง (การประชุม·ID การตัดสินใจ) | สูง | กุแหล่งที่ไม่มีให้ดูน่าเชื่อ | grep เทียบกับต้นฉบับ |
| การจำแนกประเภท (แท็ก·หมวดหมู่) | ปานกลาง | สับสนป้ายกำกับ | เทียบแบบเชิงกำหนดได้ |
| การสรุป·การอนุมาน | ปานกลาง | เพิ่มหรือลดรายการที่ไม่มี | ตรวจสอบตนเอง + ด่านมนุษย์ |
| การสร้างสรรค์ (flavor text) | ต่ำ | ไม่มีคำตอบถูก แนวคิด 'หลอน' จึงอ่อน | ด่านตรวจสอบโทน |
บรรทัดแรกคือใบสั่งยาที่ง่ายที่สุด อย่าให้ LLM ทำตัวเลข ส่งต่อให้เครื่องมือเชิงกำหนดเหมือนที่ปล่อยให้เครื่องคิดเลขทำการคูณ บรรทัดที่สอง (การอ้างอิง) คือกระดูกสันหลังของบทนี้ ในงานที่ มีต้นฉบับอยู่แล้วแต่ให้ LLM เรียบเรียงใหม่ อย่างการสรุปบันทึกการประชุมหรือการ์ดการตัดสินใจ อาการหลอนอันตรายที่สุดและจับได้ดีที่สุด เพราะเมื่อมีต้นฉบับ ก็เทียบได้
ถ้าเขียนแค่ลอย ๆ ว่า "ตรวจสอบ" ก็ไม่รู้ว่าจะทำอะไรอย่างไร เราจะดูหนึ่งรอบเต็มตั้งแต่ป้อนข้อมูลจนถึงการขอใหม่ — สรุปบันทึกการประชุมหนึ่งฉบับ แล้วจับอาการหลอนในบทสรุปนั้น พรอมต์ด้านล่างคัดลอกไปใช้ได้ตามนั้นเลย ส่วนผลลัพธ์คือการเรียบเรียงใหม่จากเซสชันจริง
ก่อนอื่นต้องมีต้นฉบับที่จะสรุป นี่คือจุดอ้างอิงของการตรวจสอบ ถ้าปล่อยให้ LLM สรุปจาก "ความจำ" โดยไม่มีต้นฉบับ ก็จะไม่เหลืออะไรให้เทียบ
# บันทึกการประชุม — 2026-04-15 ทบทวนสมดุลการต่อสู้
ผู้เข้าร่วม: อี มินซู, สมาชิกทีม A, สมาชิกทีม B
วาระ:
1. คูลดาวน์สกิลกระจายพื้นที่ — มีความเห็นว่า 8 วินาทีตอนนี้สั้นเกินไป พิจารณาปรับขึ้นเป็น 12 วินาที
ข้อสรุป: ตัดสินเบื้องต้นเป็น 12 วินาที จะยืนยันหลังตรวจ telemetry ในบิลด์ถัดไป
2. HP เปลี่ยนเฟสบอส — เสนอลดจาก 50% เป็น 30%
ข้อสรุป: พักไว้ก่อน ชนกับกำหนดการกำกับอาร์ต ยกไปถกใหม่สัปดาห์หน้า
3. ระยะเวลาสถานะผิดปกติใหม่ 'แช่แข็ง' — ถกกันแล้วแต่ยังไม่กำหนดตัวเลข
สรุปบันทึกการประชุมที่แนบมาเป็นการ์ดการตัดสินใจ
กฎ:
1) แต่ละวาระทำเป็นการ์ด 1 ใบ รูปแบบ: [วาระ | ข้อสรุป | สถานะ(ยืนยัน/เบื้องต้น/พักไว้) | เหตุผล]
2) ห้ามสร้างข้อมูลที่ไม่มีในบันทึกการประชุม ห้ามเดาตัวเลข·วันที่·ผู้เข้าร่วม
3) วาระที่ยังไม่ได้ข้อสรุป ให้ใส่สถานะเป็น 'ยังไม่กำหนด' และห้ามแต่งข้อสรุปขึ้นเอง
รูปแบบผลลัพธ์: ตารางเท่านั้น ห้ามอธิบายเพิ่ม
| วาระ | ข้อสรุป | สถานะ | เหตุผล |
|------------------|------------------------|---------|-------------------------------------|
| คูลดาวน์สกิลกระจาย | 8 วิ → 12 วิ | เบื้องต้น | ยืนยันหลังตรวจ telemetry |
| เปลี่ยนเฟสบอส | 50% → 30% พักไว้ | พักไว้ | ชนกับกำหนดการกำกับอาร์ต |
| ระยะเวลาแช่แข็ง | ตัดสินเป็น 3 วิ | ยืนยัน | ใช้เกณฑ์มาตรฐานระยะเวลาสถานะผิดปกติ |
ลองดูการ์ดใบที่สาม ต้นฉบับเขียนไว้แค่ว่า "ระยะเวลาแช่แข็งถกกันแล้วแต่ยังไม่กำหนดตัวเลข" แต่ผลลัพธ์กลับเขียนว่า "ตัดสินเป็น 3 วิ / ยืนยัน / ใช้เกณฑ์มาตรฐาน" ทั้งสามรายการล้วนเป็นข้อมูลที่ไม่มีในบันทึกการประชุม LLM เอาความรู้ทั่วไปที่ว่า "ถ้าเป็นสถานะผิดปกติก็คงประมาณไม่กี่วินาที" มาเติมในช่องว่าง เพราะฟอร์แมตสมบูรณ์ มองผ่าน ๆ จึงจับไม่ได้
ตรงนี้เราจะไม่รับผลลัพธ์ตามนั้น แต่ บังคับให้ตรวจสอบตนเองโดยเทียบกับต้นฉบับทีละบรรทัด นี่คือไม้เด็ดแกนกลางของบทนี้
นำการ์ดการตัดสินใจ 3 ใบที่เพิ่งสร้าง มาเทียบกับบันทึกการประชุมต้นฉบับที่แนบมาทีละรายการ
สำหรับทุกฟิลด์ของแต่ละการ์ด (ข้อสรุป·สถานะ·เหตุผล·ตัวเลข):
- เป็นเนื้อหาที่เขียนไว้โดยตรงในต้นฉบับหรือไม่ → [ตรงกัน]
- เป็นเนื้อหาที่ไม่มีในต้นฉบับแต่เธอเพิ่มเข้ามาหรือไม่ → ทำเครื่องหมาย [สร้างขึ้น] และระบุว่าไม่มีแหล่งที่มา
- มีรายการที่อยู่ในต้นฉบับแต่หายไปจากการ์ดหรือไม่ → รายงานเป็น [ตกหล่น]
ฟิลด์ [สร้างขึ้น] ที่ไม่มีหลักฐานในต้นฉบับ ให้ย้อนกลับเป็น 'ยังไม่กำหนด' ทั้งหมด
ผลการเทียบ:
การ์ด 1 (คูลดาวน์สกิลกระจาย)
- ข้อสรุป "8 วิ→12 วิ": [ตรงกัน] วาระ 1 "ปรับขึ้นเป็น 12 วินาที"
- สถานะ "เบื้องต้น": [ตรงกัน] "ตัดสินเบื้องต้น"
- เหตุผล "ยืนยันหลังตรวจ telemetry": [ตรงกัน]
การ์ด 2 (เปลี่ยนเฟสบอส)
- ข้อสรุป "50%→30% พักไว้": [ตรงกัน]
- สถานะ "พักไว้": [ตรงกัน] "พักไว้ ถกใหม่สัปดาห์หน้า"
- เหตุผล "ชนกับกำหนดการกำกับอาร์ต": [ตรงกัน]
- [ตกหล่น] เงื่อนไข "ถกใหม่สัปดาห์หน้า" ในต้นฉบับหายไปจากการ์ด ควรเพิ่มในเหตุผล
การ์ด 3 (ระยะเวลาแช่แข็ง)
- ข้อสรุป "ตัดสินเป็น 3 วิ": [สร้างขึ้น] — ต้นฉบับคือ "ยังไม่กำหนดตัวเลข" 3 วินาทีเป็นค่าที่ฉันเพิ่มเข้ามาเอง
- สถานะ "ยืนยัน": [สร้างขึ้น] — ต้นฉบับไม่ได้ลงข้อสรุป
- เหตุผล "ใช้เกณฑ์มาตรฐานระยะเวลาสถานะผิดปกติ": [สร้างขึ้น] — ต้นฉบับไม่ได้เอ่ยถึงเกณฑ์เช่นนั้น
→ ตามกฎ ขอย้อนการ์ด 3 กลับเป็นดังนี้:
| ระยะเวลาแช่แข็ง | (ยังไม่กำหนดตัวเลข) | ยังไม่กำหนด | ถกในที่ประชุมแล้วแต่ยังไม่ได้ข้อสรุป |
ผลลัพธ์นี้คือส่วนที่มีค่าที่สุดในบทนี้ AI เทียบผลลัพธ์ของตัวเองกับต้นฉบับ แล้ว สารภาพว่า "3 วินาทีเป็นค่าที่ฉันเพิ่มเข้ามาเอง" และย้อนการ์ด 3 กลับเป็น 'ยังไม่กำหนด' ตามกฎ พร้อมกันนั้นยังจับการตกหล่นในการ์ด 2 (เงื่อนไข "ถกใหม่สัปดาห์หน้า") ที่แม้แต่คนยังพลาด ได้ด้วย อาการหลอน (เติมสิ่งที่ไม่มี) กับการตกหล่น (ตัดสิ่งที่มีออก) เป็นด้านสองด้านของเหรียญเดียวกัน การเทียบครั้งเดียวจึงจับได้ทั้งคู่
แต่ก็มีข้อควรระวังที่ชัดเจน การตรวจสอบตนเองนี้ ไม่ใช่ยาครอบจักรวาล ถ้า LLM อ่านต้นฉบับผิด ก็อาจให้ผลการเทียบที่ผิดอย่างมั่นใจได้เช่นกัน ดังนั้นการตรวจสอบตนเองจึงเป็น ขั้นแรก ของด่าน และถ้าต้นฉบับสั้น คนก็มา grep รองรับอีกชั้นหนึ่ง การสร้างขึ้นชัด ๆ อย่างการ์ด 3 การตรวจสอบตนเองจับได้เกือบหมด แต่การตีความเบี่ยงและการบิดเบือนความหมายเล็ก ๆ น้อย ๆ สุดท้ายต้องให้ด่านมนุษย์เป็นผู้ปิดท้าย
หากนำรอบข้างต้นมาทำให้เป็นภาพรวม ด่านที่ผลลัพธ์ AI ต้องผ่านก่อนจะแตะการตัดสินใจหรือข้อมูล จะเป็นดังด้านล่าง จุดที่มือมนุษย์เข้าไปแตะมีเพียงสองที่ คือต้นทางสุดที่ป้อนต้นฉบับเข้าไปอย่างสะอาด และปลายทางสุดที่ตัดสินสิ่งที่ด่านอัตโนมัติจับไม่ได้
flowchart TB
A["ต้นฉบับ (บันทึกประชุม·ชีตข้อมูล)
จุดอ้างอิงของการตรวจสอบ"] --> B["AI สร้างครั้งแรก
สรุป·การ์ดการตัดสินใจ·จำแนก"]
B --> C{"ตรวจสอบตนเอง
เทียบกับต้นฉบับทีละรายการ
[ตรงกัน]/[สร้างขึ้น]/[ตกหล่น]"}
C -->|พบฟิลด์สร้างขึ้น| D["ฟิลด์สร้างขึ้น → ย้อนเป็น 'ยังไม่กำหนด'"]
D --> E
C -->|ตรงกัน| E{"ด่านเชิงกำหนด
grep เทียบตัวเลข·ID·การอ้างอิง"}
E -->|ตัวเลข/ID ไม่ตรง| F["ปฏิเสธ + ขอใหม่
(แก้เป็นค่าจากต้นฉบับ)"]
F --> B
E -->|ผ่าน| G["ด่านมนุษย์
ตัดสินความหมาย·โทน·บริบท"]
G -->|ตีกลับ| F
G -->|อนุมัติ| H["บันทึกการตัดสินใจ / นำเข้าบิลด์"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class E code;
class B,C ai;
class G human;
class A data;
class H pass;
เหตุที่ด่านมีสามชั้น เพราะแต่ละชั้นจับคนละอย่าง การตรวจสอบตนเองให้ LLM เทียบเองว่า เติมสิ่งที่ไม่มีหรือไม่ ด่านเชิงกำหนดใช้โค้ดจับว่า ตัวเลข·ID ตรงกับต้นฉบับระดับตัวอักษรหรือไม่ และด่านมนุษย์จับว่า ถูกก็ถูกอยู่ แต่บริบทเพี้ยนไปหรือไม่ ถ้าเปิดเพียงชั้นเดียว อุบัติเหตุก็จะรั่วตรงตำแหน่งที่อีกสองชั้นเคยกั้นไว้ ใน §22.2.2 "3 วินาที" ของการ์ด 3 ถูกจับที่ชั้นแรก (ตรวจสอบตนเอง) การตกหล่นของการ์ด 2 ก็ถูกจับที่ชั้นแรก และถ้าการตรวจสอบตนเองอ่าน "12 วินาที" ผิดเป็น "21 วินาที" ก็จะถูกจับที่ชั้นสอง (grep)
เมื่อนำ verification gate ไปใส่ในไปป์ไลน์อัตโนมัติ มีอุบัติเหตุหนึ่งที่มือใหม่สร้างขึ้นบ่อยที่สุด คือทำให้ เมื่อด่านเองพังแล้วงานทั้งหมดหยุด ถ้า grep ตายเพราะข้อผิดพลาดการเข้ารหัส หรือไฟล์ manifest เสีย โค้ดที่ตั้งใจจะช่วยตรวจสอบกลับขวางงานของผู้ใช้ไปทั้งดุ้น จากนั้นภายในหนึ่งถึงสองสัปดาห์ทีมก็จะบอกว่า "ปิดการตรวจสอบนั้นเถอะ"
ขอยกวิธีที่ JIT atom injection hook (inject_memory.py) ซึ่งใช้งานจริงอยู่ในหนังสือเล่มนี้จัดการปัญหานี้มาตามนั้นเลย hook นี้แทรกตัวเข้ามาทุกครั้งที่ผู้ใช้พิมพ์พรอมต์ เพื่อฉีดหน่วยความจำที่เกี่ยวข้องเข้าไป พูดอีกอย่างคือเป็น ด่านที่เปิดอยู่ตลอดเวลา ในความเห็นหลักการออกแบบมีบรรทัดหนึ่งระบุไว้ชัด
설계 원칙:
- 항상 exit 0 (실패해도 사용자 흐름 방해 금지)
- 매칭 안 되면 빈 응답 (정상)
และหลักการนี้ถูกนำไปใช้อย่างสอดคล้องตลอดทั้งโค้ด ไม่ว่าการแยกวิเคราะห์ stdin จะล้มเหลว manifest JSON จะเสีย หรือการอ่านเนื้อหา atom จะล้มเหลว — ทั้งหมดจะตกไปที่ emit_empty() และ exit 0
def emit_empty() -> None:
sys.exit(0)
def main() -> None:
try:
...
payload = json.loads(raw)
except Exception:
emit_empty() # 입력이 깨져도 조용히 통과
return
...
try:
manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
except Exception:
emit_empty() # 매니페스트가 깨져도 작업은 안 막음
return
if __name__ == "__main__":
try:
main()
except Exception:
emit_empty() # 어떤 예외든 마지막 그물
แกนของการออกแบบคือการ แยกความล้มเหลวของด่านออกจากความล้มเหลวของเนื้อหา การที่ hook ฉีดหน่วยความจำล้มเหลว ในมุมของผู้ใช้ก็เป็นเพียง "เซสชันธรรมดาที่ไม่มีหน่วยความจำติดมา" เท่านั้น ไม่ใช่อุบัติเหตุที่งานถูกขวาง verification gate ก็ต้องเป็นเช่นเดียวกัน ถ้าด่าน grep รันไม่ได้เพราะปัญหาการเข้ารหัส แทนที่จะ ปล่อยการ์ดนั้นผ่าน ก็ทำเครื่องหมายว่า "ตรวจสอบอัตโนมัติล้มเหลว — ส่งไปด่านมนุษย์" แล้วส่งต่อไปยังชั้นมนุษย์ ผลลัพธ์ที่ยังไม่ตรวจสอบต้องไม่ถูกอนุมัติอัตโนมัติเพียงเพราะด่านตาย และในขณะเดียวกันไปป์ไลน์ทั้งหมดก็ต้องไม่หยุดเพียงเพราะด่านตาย ค่าเริ่มต้นที่ปลอดภัยซึ่งสนองทั้งสองเงื่อนไขคือ "ส่งต่อให้มนุษย์อย่างเงียบ ๆ" และ except: emit_empty() ใน inject_memory.py ก็คือการนำแพตเทิร์นนั้นไปทำให้เป็นจริงในรูปแบบที่เล็กที่สุด
มันยั่วใจมากที่จะใส่ตารางทำนอง "ลดอัตราอาการหลอนจาก 89% เหลือ 3%" ลงในบทนี้ ตัวเลขแบบนั้นถ้าไม่ระบุวิธีวัด ก็ลดทอนความน่าเชื่อถือของหนังสือ หลักการของหนังสือเล่มนี้มีอยู่หนึ่งในสามอย่าง
หนึ่ง พูดเป็นตัวเลขเฉพาะสิ่งที่วัดได้ ถ้าจะสัญญาเรื่องอัตราอาการหลอน ต้องนิยามตัวส่วนและตัวเศษ ตัวส่วนคือ "จำนวนการ์ดการตัดสินใจที่ตรวจสอบ" ตัวเศษคือ "จำนวนการ์ดที่เทียบกับต้นฉบับแล้วเจอ [สร้างขึ้น]/[ตกหล่น] อย่างน้อย 1 รายการ" ถ้าไม่มีนิยามนี้ "อัตราอาการหลอน 5%" ก็กลวง วิธีที่ผู้เขียนนับไว้จริงตอนตรวจสอบบทสรุปบันทึกการประชุมในช่วงเริ่มนำมาใช้ก็คือวิธีนี้ และเพราะตัวอย่างมีจำนวนน้อย จึงไม่ใช่พารามิเตอร์ที่แม่นยำ แต่เป็น ค่าบอกทิศทาง
สอง การเปรียบเทียบระหว่างโมเดลให้พูดแค่ทิศทาง ทิศทางที่ว่า "โมเดลใหญ่หลอนน้อยกว่าโมเดลเล็ก" สังเกตได้อย่างเสถียร แต่ตัวเลขสัมบูรณ์อย่าง "Opus 3%, โอเพน 7B 20%" แกว่งมากตามงาน·พรอมต์·โดเมน หนังสือเล่มนี้จึงไม่อ้างค่าสัมบูรณ์ เอาเฉพาะทิศทาง (โมเดลยิ่งใหญ่ยิ่งหลอนน้อย แต่แลกกับค่าใช้จ่าย) ไปเท่านั้น
สาม มาตรฐานที่เปิดเผยให้อ้างอิงตามนั้น ในบทนี้แทบไม่มีตัวเลขมาตรฐานที่จะต้องกุขึ้นเลย แต่ค่าตั้งค่าอย่าง temperature เป็นข้อเท็จจริงที่เปิดเผยในเอกสาร API ของโมเดล งานตรวจสอบ·วิเคราะห์ตั้ง temperature ต่ำ (ใกล้เชิงกำหนด) ส่วนงานสร้างสรรค์ตั้งสูง — นี่ไม่ใช่การประมาณ แต่เป็นนิยามของพฤติกรรม API
ดังนั้น ตัวชี้วัดที่วัดได้ซึ่งบทนี้สัญญาจริง ๆ มีสามอย่าง — จำนวนการตรวจพบ [สร้างขึ้น] (จำนวนอาการหลอนที่การตรวจสอบตนเองจับได้) จำนวนการปฏิเสธของด่าน grep (จำนวนตัวเลข·ID ที่ไม่ตรง) และจำนวนการตีกลับของด่านมนุษย์ ทั้งสามนี้นับจากล็อกได้ทุกไตรมาส และในที่ประชุมก็พูดเป็นตัวเลขได้ ไม่ใช่ "ความรู้สึก"
| แพตเทิร์น | ทำไมจึงล้มเหลว | ใบสั่งยา |
|---|---|---|
| รับบทสรุป AI โดยดูแค่ฟอร์แมต | อาการหลอนจับยากที่สุดเมื่อฟอร์แมตสมบูรณ์ | ตรวจสอบตนเองเทียบต้นฉบับ (§22.2.2 ขั้นที่ 4) |
| สรุปจากความจำ LLM โดยไม่มีต้นฉบับ | ไม่มีจุดอ้างอิงให้เทียบ จึงตรวจสอบไม่ได้ | ใส่ต้นฉบับเข้าไปในอินพุตก่อน |
| มอบการคำนวณตัวเลขให้ LLM | การคิดเลขถูกประมาณ จึงต่างกันทุกครั้ง | คำนวณด้วยเครื่องมือเชิงกำหนด (§22.2.1) |
| ด่านพังแล้วงานทั้งหมดหยุด | ทีมจะปิดด่านทิ้ง | exit 0 + ส่งต่อชั้นมนุษย์ (§22.2.4) |
| ด่านตายแล้วอนุมัติผลลัพธ์ที่ยังไม่ตรวจสอบอัตโนมัติ | อาการหลอนผ่านไปตรง ๆ | ด่านล้มเหลว = ทำเครื่องหมาย 'ยังไม่ตรวจสอบ' |
| เชื่อการตรวจสอบตนเองเป็นคำตัดสินสุดท้าย | ถ้า LLM อ่านต้นฉบับผิด ก็ตัดสินผิดอย่างมั่นใจ | ต้นฉบับสั้นใช้ grep โดยคนควบคู่ |
การประยุกต์นอกเกม "เพื่อนร่วมงานที่พูดเท็จอย่างมั่นใจ" — AI ที่กุวันที่ประชุมหรือการตัดสินใจที่ไม่มีจริงด้วยฟอร์แมตที่สมบูรณ์ — อันตรายในแบบเดียวกันไม่ใช่แค่กับการ์ดการตัดสินใจของเกม แต่กับการสรุปเอกสารทุกชนิด เพราะอาการหลอนจับยากที่สุดเมื่อฟอร์แมตสมบูรณ์ ในงานที่มีต้นฉบับ (สรุปบันทึกการประชุม·คัดสัญญา·เรียบเรียงรายงาน) อย่ารับผลลัพธ์ตามนั้น แต่หัวใจคือบังคับการตรวจสอบตนเองว่า "เทียบกับต้นฉบับทีละรายการ แล้วเนื้อหาที่เธอเพิ่มเข้ามาให้ทำเครื่องหมาย [สร้างขึ้น]" ตัวอย่างเช่น ให้ผู้ช่วยฝ่ายกฎหมายสรุปสัญญาแล้วเทียบจำนวนเงิน·วันที่·เลขข้อกับต้นฉบับระดับตัวอักษร AI ก็จะสารภาพว่า "ตัวเลขค่าปรับนี้เป็นค่าที่ฉันเพิ่มเข้ามาเอง" และย้อนช่องว่างกลับเป็น 'ยังไม่กำหนด' ส่วนการคำนวณตัวเลขอย่ามอบให้ AI เลย ให้ส่งต่อเครื่องคิดเลข·สูตร และออกแบบให้เครื่องมือตรวจสอบอัตโนมัติแม้ล้มเหลวก็ไม่ขวางงาน แต่ส่งต่อเป็น "ยังไม่ตรวจสอบ — ให้คนยืนยัน"
ถ้าทำคนเดียว เท่านี้ก็พอ: ไม่ต้องใช้ทั้งโค้ดและ hook ลองเอาเอกสารสั้น ๆ ที่คุณมี (โน้ตประชุม·แพตช์โน้ต·เอกสารออกแบบหนึ่งหน้า) สักฉบับให้ AI สรุป แล้วแปะพรอมต์ตรวจสอบตนเองในขั้นที่ 4 ของ §22.2.2 ลงไปตามนั้นเลย แค่บรรทัดเดียวว่า "เทียบกับต้นฉบับทีละรายการ แล้วเนื้อหาที่เธอเพิ่มเข้ามาให้ทำเครื่องหมาย [สร้างขึ้น]" AI ก็จะเริ่มแจ้งอาการหลอนของตัวเองด้วยตัวเอง พอได้รับคำสารภาพ [สร้างขึ้น] สักครั้ง เหตุผลที่ไม่ควรเชื่อบทสรุปของ AI ตรง ๆ ก็จะซึมเข้าตัวคุณ
ถ้าเป็นทีม ให้เริ่มจากขั้นต่อไปนี้ ตรึงขั้นตอนตรวจสอบตนเองให้เป็น พรอมต์พื้นฐาน ของการ์ดการตัดสินใจ·บทสรุปที่ AI สร้าง (§22.2.2) จากนั้นเลือกเฉพาะฟิลด์ที่ต้องตรงกับต้นฉบับระดับตัวอักษร อย่างตัวเลข·ID การตัดสินใจ·วันที่ มาทำการเทียบ grep ด้วยโค้ด ตอนนี้โค้ดตรวจสอบนั้นต้องออกแบบให้ แม้ล้มเหลวก็ไม่ขวางงาน (exit 0 + ทำเครื่องหมาย 'ยังไม่ตรวจสอบ') เหมือน inject_memory.py (§22.2.4) แค่มีสองชั้นคือการตรวจสอบตนเองกับ grep ก็สกัดอุบัติเหตุที่พบบ่อยที่สุด — อาการหลอนที่ฟอร์แมตสมบูรณ์รั่วเข้าไปในบันทึกการตัดสินใจ — ได้ก่อนแล้ว
ผู้อ่านหลัก: ผู้นำทีมออกแบบที่นำเครื่องมือ AI เข้ามาใช้ในทีมและรับผิดชอบเรื่องต้นทุน (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านคนเดียว/งานอดิเรก: §22.3.9 「ถ้าทำคนเดียว เท่านี้ก็พอ」
ถ้าบทที่ว่าด้วยต้นทุนยกตัวเลขต้นทุนปลอมขึ้นมา นั่นก็ขัดแย้งกับตัวเองในตัวเองอยู่แล้ว ด้วยเหตุนี้บทนี้จึงไม่สร้างตารางสวยงามที่บอกว่า "ทีมเราประหยัดได้เดือนละเท่าไหร่" แต่ใช้ตัวเลขเพียงสองชนิดเท่านั้น ชนิดแรกคือ ราคาโทเค็นที่เปิดเผยต่อสาธารณะ (ค่าบริการต่อ 1M โทเค็นของแต่ละโมเดล) ที่ใครก็ตรวจสอบได้ อีกชนิดหนึ่งคือ ค่าคงที่ที่ถูกตรึงไว้ ในโค้ด hook ที่ผู้เขียนรันใช้งานเองโดยตรง (max_atom_body = 6000, max_matches = 3) ทั้งสองชนิดไม่ใช่สิ่งที่กุขึ้น แต่เป็นสิ่งที่อ้างอิงมา
สิ่งที่น่ากลัวเกี่ยวกับต้นทุน AI ไม่ใช่เพราะจำนวนเงินมันมาก แต่เพราะ มันมองไม่เห็น เดือนแรกที่นำมาใช้ การเรียก (call) ยังน้อย ใบเรียกเก็บเงินจึงเล็ก จากนั้นเมื่อบริบทยาวขึ้นและการเรียกถี่ขึ้น ใบเรียกเก็บเงินของไตรมาสใดไตรมาสหนึ่งก็เปลี่ยนหลักไปเลย ถ้าจะพูดข้อสรุปของบทนี้ก่อน ก็คือ — ต้นทุนไม่ได้ควบคุมด้วยคำมั่นว่า "ใช้อย่างประหยัดเถอะ" แต่ควบคุมด้วย โค้ดที่บังคับตัดโทเค็นในทุกการเรียก สิ่งที่ปิดกั้นไว้ไม่ใช่เจตจำนงของคน แต่เป็น wrapper และ truncate
รายการต้นทุนมีสี่อย่างคือ อินพุต เอาต์พุต แคชฮิต และแคชไรต์ แต่สิ่งที่ครอบงำใบเรียกเก็บเงินในการทำงานจริงคือ โทเค็นอินพุต เหตุผลนั้นง่ายมาก เพราะงานเกือบทั้งหมดที่ใช้ AI ในการออกแบบเกมมีรูปแบบ "ใส่บริบทยาว ๆ เข้าไปแล้วรับคำตอบสั้น ๆ" เมื่อยัดเอกสารวิสัยทัศน์ L0, ไลบรารี atom, เนื้อหาเมืองข้างเคียง และส่วนที่ตัดมาจากชีตข้อมูลเข้าไปทั้งหมด อินพุตก็เป็นหลายหมื่นโทเค็น แต่เอาต์พุตเป็นตารางหนึ่งแผ่นจึงเป็นเพียงไม่กี่ร้อยโทเค็น
ด้วยเหตุนี้ลำดับความสำคัญอันดับหนึ่งของการควบคุมต้นทุนจึงไม่ใช่ "ลดเอาต์พุตเถอะ" แต่กลายเป็น "จะตัดโทเค็นอินพุตที่ตรงไหน" บรรทัดเดียวนี้ลากบทที่เหลือทั้งหมดไปข้างหน้า
ขอตรึงราคาต่อโมเดลที่เปิดเผยต่อสาธารณะไว้ก่อน ด้านล่างคือค่าบริการต่อ 1M (หนึ่งล้าน) โทเค็นที่ Anthropic เปิดเผย ซึ่งเป็น สแนปช็อตที่อ้างอิงราคาสาธารณะของเจเนอเรชันในช่วงเขียนหนังสือเล่มนี้ (เกรดล่าสุดในขณะนั้นของ Opus·Sonnet·Haiku) มาอย่างตรงตัว (อ้างอิงราคาสาธารณะอย่างเป็นทางการ — เนื่องจากเปลี่ยนแปลงตามเจเนอเรชันและช่วงเวลาของโมเดล จึงต้องตรวจสอบตารางราคาปัจจุบันก่อนนำไปใช้ทุกครั้ง) ตามหลักการที่ภาคผนวก K สรุปไว้ สิ่งที่ไม่เปลี่ยนแปลงในที่นี้ไม่ใช่ ค่าสัมบูรณ์ของราคา แต่เป็นอัตราส่วนราคาระหว่างเกรดทั้งสาม ดังนั้นจึงดูตารางด้านล่างไม่ใช่เพื่อเป็น "ใบเรียกเก็บเงินของวันนี้" แต่เพื่ออ่านโครงสร้างที่ว่า "ยิ่งลดเกรดลง ราคาก็ยิ่งตกลงทีละหลัก"
| โมเดล | อินพุต 1M โทเค็น | เอาต์พุต 1M โทเค็น | หมายเหตุ |
|---|---|---|---|
| Claude Opus | $15 | $75 | การให้เหตุผลระดับสูงสุด (ราคาสาธารณะ) |
| Claude Sonnet | $3 | $15 | ระดับกลาง — ราคาอินพุตเป็น 1/5 ของ Opus |
| Claude Haiku | $0.80 | $4 | น้ำหนักเบา — ราคาอินพุตประมาณ 1/19 ของ Opus |
| แคชฮิต (read) | ประมาณ 1/10 ของราคาอินพุตมาตรฐาน | — | เมื่อใช้อินพุตที่แคชไว้ซ้ำ (นโยบายแคชสาธารณะ) |
ประเด็นสำคัญคือสองบรรทัดสุดท้าย ถ้ารันงานเดียวกันด้วย Haiku แทน Opus ราคาต่อโทเค็นอินพุตจะเป็นประมาณ 1/19 และ ถ้าใส่บริบทเดียวกันลงในแคช ราคาอินพุตของส่วนนั้นจะเป็นประมาณ 1/10 สองแกนใหญ่ของการลดต้นทุนออกมาจากตรงนี้ — การเลือกโมเดลให้เหมาะสมและการแคช ทั้งสองอย่างไม่ใช่โครงสร้างแบบ "ใช้ให้น้อยลง" แต่เป็น "ทำงานเดียวกันด้วยราคาต่อหน่วยที่ถูกกว่า"
การประหยัดไม่ได้มาจากเจตจำนง แต่มาจากส่วนต่างของราคา การลด Opus เป็น Haiku ลดได้ประมาณ 19 เท่า การใส่แคชลดได้ประมาณ 10 เท่าโดยอัตโนมัติ
มีต้นทุนที่สะสมเงียบ ๆ ยิ่งกว่าราคาต่องานแต่ละชิ้น นั่นคือ บริบทที่ถูกแนบเข้าไปโดยอัตโนมัติในทุกการเรียก ในเครื่อง PC ส่วนตัวของผู้เขียนมี hook ที่ทำงานเพื่อเสียบหน่วยความจำที่เกี่ยวข้อง (atom) เข้าไปโดยอัตโนมัติทุกครั้งที่ผู้ใช้พิมพ์พรอมต์ (UserPromptSubmit hook, inject_memory.py) นี่เป็นฟีเจอร์อำนวยความสะดวก แต่ในขณะเดียวกันก็เป็น ผู้ต้องสงสัยอันดับหนึ่งของการรั่วไหลของต้นทุน ด้วยเช่นกัน เพราะเนื้อหา atom ยาว ๆ จะเข้าไปอยู่ในบริบททุกอินพุต ถ้าปล่อยไว้โดยไม่ควบคุม โทเค็นอินพุตจะพองขึ้นในทุกการเรียก
ด้วยเหตุนี้ใน hook นี้จึงมีกลไกป้องกันที่ตัดต้นทุนถูกตรึงไว้ซ้อนกันสามชั้น ไม่ใช่ทฤษฎีนามธรรม แต่เป็นค่าคงที่ในโค้ดจริง
# inject_memory.py — UserPromptSubmit hook (โค้ดที่รันใช้งานจริง, ตัดตอนมา)
# หลักการออกแบบ (ข้อความต้นฉบับใน docstring):
# - exit 0 เสมอ (แม้ล้มเหลวก็ห้ามรบกวนการทำงานของผู้ใช้)
# - ฉีด atom สูงสุด 3 ตัวตามลำดับ score จากมากไปน้อย
# - truncate เมื่อเนื้อหา atom เกิน 6000 ตัวอักษร
# (1) อ่านค่าคงที่ของงบจาก config ใน manifest
max_matches = cfg.get("max_matches", 3) # จำนวน atom สูงสุดต่อหนึ่งการเรียก
max_body = cfg.get("max_atom_body", 6000) # ขีดจำกัดเนื้อหาต่อ atom 1 ตัว (ตัวอักษร)
# (2) เรียงตาม score จากมากไปน้อย — เติมสล็อตราคาแพงตามลำดับคุณค่า
atoms_sorted = sorted(atoms, key=lambda a: a.get("score", 0), reverse=True)
matches = []
for atom in atoms_sorted:
if len(matches) >= max_matches: # (การ์ด A) ตัดที่สูงสุด 3 ตัว
break
if re.search(atom["regex"], prompt, re.IGNORECASE):
matches.append(atom)
# (3) เมื่อฉีดเนื้อหา ตัดที่ 6000 ตัวอักษร
for atom in matches:
body = atom_path.read_text(encoding="utf-8")
if len(body) > max_body: # (การ์ด B) truncate
body = body[:max_body] + "\n\n[...truncated]\n"
ในนี้มีการ์ดป้องกันต้นทุนครบทั้งสามชั้น
max_matches = 3): แม้จะมี atom ที่แมตช์กับอินพุต 10 ตัว ก็แนบสูงสุดได้เพียง 3 ตัว โค้ดจะปิดกั้นอุบัติเหตุที่ไลบรารี 17 atom ทั้งชุดเข้าไปในทุกการเรียกmax_atom_body = 6000): แม้เนื้อหา atom จะมี 12,000 ตัวอักษร ก็ตัดที่ 6,000 ตัวอักษร เป็นไปไม่ได้ในเชิงโครงสร้างที่ atom การทบทวนยาว ๆ ตัวเดียวจะทำให้ต้นทุนการเรียกพองเป็นสองเท่าค่าคงที่สามตัวนี้คือ ขีดจำกัดของโทเค็นอินพุตต่อการเรียก นั่นเอง หากประเมินคร่าว ๆ atom หนึ่งตัว 6,000 ตัวอักษรในภาษาเกาหลีก็ราว ๆ ขนาดไม่กี่พันโทเค็น (จำนวนโทเค็นที่แน่นอนแตกต่างกันตามตัวตัดคำ (tokenizer) และภาษา จึงควรอ่านเป็นโครงสร้างที่ว่า "มีขีดจำกัดกำกับอยู่" มากกว่าค่าสัมบูรณ์) 3 ตัว × 6,000 ตัวอักษร คืองบการฉีดต่อหนึ่งการเรียก เกินกว่านั้นโค้ดจะตัดทิ้ง คนไม่ต้องคอยใช้ตามองเองว่า "มี atom แนบมาเยอะเกินไปแล้ว"
ถ้าพูดด้วยปากเปล่าว่า "truncate ปิดกั้นต้นทุน" มันก็เลื่อนลอย ในความเป็นจริงตอนสร้างค่าคงที่นี้ ผู้เขียนรันหนึ่งรอบกับ AI ไปจนจบ ด้านล่างคือการจำลองเซสชันนั้นอย่างซื่อตรง พรอมต์อินพุตคัดลอกไปใช้ได้ตามนั้น ส่วนเอาต์พุตเป็นการเรียบเรียงใหม่จากเซสชันจริง
ทันทีหลังจากเริ่มรัน hook ครั้งแรก ใน _injection_log.txt มีบันทึกที่เนื้อหา atom ถูกฉีดเข้าไปทั้งก้อนในหนึ่งการเรียก ถ้า atom ยาว ๆ (การทบทวน·บริบทสุขภาพ ฯลฯ) เข้าไปแบบเต็มขนาดในทุกการเรียก ยิ่งเป็นงานที่ใช้บ่อย โทเค็นอินพุตก็ยิ่งสะสม ผู้เขียนจึงถาม AI ว่าจะตัดที่ตรงไหน
มี hook (inject_memory.py) ที่ฉีด atom เข้าไปในบริบทโดยอัตโนมัติทุกการเรียก
ผมอยากกำหนดขีดจำกัด truncate เพื่อปิดกั้นต้นทุนโทเค็นอินพุต ช่วยเสนอ
ขีดจำกัดจำนวนตัวอักษรของเนื้อหาต่อ atom 1 ตัว กับขีดจำกัดจำนวน atom ที่จะฉีด
ต่อหนึ่งการเรียกให้หน่อย ถ้าสั้นเกินไปแก่นจะถูกตัด ถ้ายาวเกินไปโทเค็นจะรั่ว
ช่วยชี้ trade-off นั้นมาด้วย และสิ่งที่ไม่มั่นใจให้ทำเครื่องหมายแยกไว้ให้คน
ตัดสิน (เจตนา: ตั้งใจให้ค่าขีดจำกัดถูกแยกออกไปเป็น config) ทำเป็นตาราง:
รายการ | ค่าที่เสนอ | เหตุผล | ระดับความมั่นใจ
ขอให้สังเกตว่าครึ่งหนึ่งของพรอมต์คือ "การบังคับขีดจำกัดและรูปแบบ" ถ้าปล่อย AI ให้อิสระกับคำถามที่เกี่ยวกับต้นทุน จะได้คำตอบที่ไร้ความรับผิดชอบอย่าง "แล้วแต่สถานการณ์" แก่นของพรอมต์นี้คือ การบังคับให้ระบุค่าเป็นรูปธรรม ให้ระบุ trade-off อย่างชัดเจน และใส่ทางออกที่บอกให้แจ้งหากไม่มั่นใจ
| รายการ | ค่าที่เสนอ | เหตุผล | ความมั่นใจ |
|----------------|----------|-----------------------------------------------|-----------|
| max_atom_body | 6000 ตัว | 'แก่น+เหตุผล' ของ atom เกาหลี 1 ตัว ส่วนใหญ่อยู่ | กลาง |
| | | ในราว 4,000–6,000 ตัว ถ้า 6000 ตัว ส่วนหัวเนื้อหา| |
| | | (ใจความ·กฎ) แทบจะถูกเก็บไว้ ตัดเฉพาะส่วนหาง | |
| | | (รายการตัวอย่าง) | |
| max_matches | 3 | หัวข้อที่อินพุตหนึ่งรายการแมตช์อย่างแรงพร้อมกัน | สูง |
| | | มักมี 1–3 ตัว ตั้งแต่ 4 ตัวขึ้นไปเป็นสัญญาณว่า | |
| | | regex กว้างเกินไป | |
| ตำแหน่งแยก config | _jit_manifest.json > config | ปรับระหว่างรันได้โดยไม่ต้องดีพลอยโค้ดใหม่ | สูง |
| เครื่องหมาย truncate | ติด [...truncated] ที่ส่วนหาง | ให้โมเดล·คนรับรู้ว่าถูกตัด | กลาง |
| จุดที่ไม่มั่นใจ | ว่า 6000 เป็นค่าที่แม่นยำหรือไม่ | การแปลงตัว→โทเค็นไม่แน่นอน | ★ต่ำ |
| | (ตัว→โทเค็นแปลงไม่แน่นอน) | เพราะต่างกันตามภาษา·โมเดล หน่วยตัวจึงเป็นค่าประมาณ | |
สิ่งที่มีคุณค่าที่สุดในเอาต์พุตไม่ใช่ตาราง แต่เป็น บรรทัด ★ ล่างสุด AI แจ้งด้วยตัวเองว่า "ไม่รู้ว่า 6000 เป็นค่าที่เหมาะที่สุดอย่างแม่นยำหรือไม่ การแปลงตัว→โทเค็นไม่แน่นอน" พรอมต์ที่ดีทำให้ AI พูดออกมาว่าไม่รู้
ผู้เขียนไม่รับเอาต์พุตนี้มาตามนั้น AI เสนอ 6000 ตัวอักษรว่าเป็น "เส้นที่แก่นยังถูกเก็บไว้" แต่ต้องตรวจสอบเองโดยตรงว่าเมื่อตัดที่ 6000 ตัวอักษรจริงแล้วอะไรถูกตัด ผู้เขียนจึงเลือก atom ที่ยาวที่สุด (บริบทสุขภาพ, ฉบับสะสมการทบทวน) แล้วใช้ตามองจุด 6000 ตัวอักษร
ผลการตรวจสอบพบหนึ่งกรณีที่ติดขัด จุด 6000 ตัวอักษรตัดแถวตารางตรงกลางเนื้อหาขาด ทำให้บรรทัดอย่าง | 비타민D | 1000IU | ถูกตัดครึ่งกลาง ความหมายยังถูกเก็บไว้ แต่รู้สึกขัดใจที่แถวตารางที่ถูกตัดเข้าไปหาโมเดลเป็นมาร์กดาวน์ที่พัง ผู้เขียนจึงร้องขอใหม่
ตอนตัดที่ 6000 ตัวอักษร มีกรณีที่กลางแถวของตารางมาร์กดาวน์ขาด
จะดีกว่าไหมถ้าใส่การชดเชยให้สแนปตำแหน่งตัดไปที่ขอบขึ้นบรรทัดใหม่ก่อนหน้า?
เนื่องจากการอิมพลิเมนต์ต้องไม่ซับซ้อนกว่าผล truncate ช่วยตัดสินไปจนถึงว่า
จะทำหรือไม่ทำ โดยชั่งคุณค่าเทียบกับต้นทุน
AI ตอบมาดังนี้ — "การสแนปขอบบรรทัดทำได้ด้วย body[:max_body].rsplit('\n', 1)[0] เพียงบรรทัดเดียว แต่ เนื่องจากวัตถุประสงค์ของ truncate คือขีดจำกัดโทเค็น ไม่ใช่ความสมบูรณ์ของตาราง แถวตารางที่พังหนึ่งบรรทัดจึงส่งผลต่อต้นทุน·การทำงานน้อยมาก ปัจจุบันเพียงเครื่องหมาย [...truncated] ก็ทำให้โมเดลรับรู้ว่า 'ถูกตัดที่นี่' แล้ว จึง แนะนำว่าไม่ควรทำ การสแนปบรรทัด ถ้าจะทำจริง การชดเชยหนึ่งบรรทัดก็เพียงพอ"
คำตอบนี้ถูกต้อง ถ้าโลภเอาความสมบูรณ์ของตารางเข้าไปในโค้ดควบคุมต้นทุนด้วย โค้ดการ์ดเองจะอ้วนขึ้น โค้ดที่รันใช้งานปัจจุบันยังคงการตัดแบบเรียบง่าย body[:max_body] + "[...truncated]" ไว้ เป็นรอบที่ปิดด้วยการไป-กลับครั้งเดียว ที่คนตรวจสอบข้อเสนอแรกของ AI (6000 ตัวอักษร) และ AI กดความโลภที่จะชดเชยเกินจำเป็นกลับลงไปอีกครั้ง
ค่าคงที่ที่กำหนดในเซสชันข้างต้นตัดโทเค็นอินพุตในการเรียกจริงอย่างไร ขอบันทึกการไหลทั้งหมดไว้เป็นแผนผัง
flowchart TB
A["ผู้ใช้ป้อนพรอมต์"] --> B["การแมตช์ regex
วนไลบรารี atom 17 ตัว"]
B --> C{"การ์ด A
len(matches) >= 3?"}
C -->|"ตั้งแต่ตัวที่ 4 ปิดกั้น"| D["break — ไม่ฉีด"]
C -->|"สล็อตยังเหลือ"| E["เติมสล็อตตามลำดับ
score จากมากไปน้อย"]
E --> F{"การ์ด B
เนื้อหา > 6000 ตัว?"}
F -->|"เกิน"| G["body[:6000] + [...truncated]"]
F -->|"ในขอบเขต"| H["ฉีดเนื้อหาเต็ม"]
G --> I["ฉีดบริบท
(โทเค็นอินพุต = รับประกันต่ำกว่าขีดจำกัด)"]
H --> I
D --> I
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class B,C,E,F,G,H code;
class A human;
class I pass;
class D fail;
ประเด็นสำคัญของภาพนี้คือ ไม่ว่าผู้ใช้จะป้อนอะไร โทเค็นที่ฉีดต่อการเรียกก็มีเพดานกำกับอยู่ เพดานคือ 3 × 6000 ตัวอักษร (+เครื่องหมาย) เกินกว่านั้นโค้ดจะตัดทิ้งโดยไม่มีเงื่อนไข ต้นทุนไม่พึ่งพาความสามารถในการยับยั้งชั่งใจของผู้ใช้ การ์ด A·B ทำงานเชิงกลไกในทุกการเรียก
ปรัชญาเดียวกันนี้ทำซ้ำในระดับเครื่องมือด้วย ระบบของบริษัทผู้เขียนมีนโยบายที่ ตรึง wrapper สกิลที่ปรากฏในสล็อตทั่วโลกไว้ที่ 12 ตัวพอดี (atom skill_listing_budget_wrapper_only_policy) เมื่อเริ่มเซสชัน ถ้าจำนวน wrapper ทั่วโลก * ไม่ใช่ 12 สคริปต์จัดระเบียบจะรันโดยอัตโนมัติ ในนามคือ "การจัดสล็อต" แต่แก่นแท้คือ การปกป้องงบโทเค็นตอนเริ่มเซสชัน — เป็นการมัดต้นทุนของการที่รายการสกิลขึ้นไปอยู่ในบริบทไว้ที่ปริมาณ 12 ตัว ขีดจำกัดการฉีด atom 3 ตัว กับขีดจำกัดการปรากฏของสกิล 12 ตัว เป็นการประยุกต์ที่ต่างกันของแนวคิดเดียวกัน
ถ้าการ์ดปิดกั้นโทเค็นต่อการเรียก การเลือกโมเดลก็เป็นตัวกำหนดราคาต่อหน่วยของโทเค็นนั้น ในตาราง §22.3.1 ราคาอินพุตเป็น Opus:Sonnet:Haiku ≈ 19:4:1 ดังนั้นการรันทุกงานด้วย Opus จึงเท่ากับการจ่ายราคาต่อหน่วยแพงกว่า 19 เท่าไปถึงงานง่าย ๆ อย่างการจัดประเภท·การแทนที่
จัดสรรราคาต่อหน่วยตามความซับซ้อนของงาน
| ประเภทงาน | โมเดลที่แนะนำ | เหตุผล |
|---|---|---|
| งานเชื่อมโยงตรวจสอบ·กฎหมายโดยตรง, การวิเคราะห์การตัดสินใจ | Opus | งานที่อุบัติเหตุใหญ่หากผิด — ไม่ตระหนี่ราคาต่อหน่วย |
| รายงาน·สรุป·การประมวลภาษาธรรมชาติ | Sonnet | ต้องการคุณภาพแต่ไม่จำเป็นถึงการให้เหตุผลระดับสูงสุด |
| การจัดประเภท·แท็ก·สกัดคีย์เวิร์ด | Haiku | แพตเทิร์นง่าย — ราคาต่อหน่วยประมาณ 1/19 ของ Opus ก็เพียงพอ |
| การแมป·แทนที่อย่างง่าย | Haiku หรือเชิงกำหนด (deterministic) | หลายกรณีไม่จำเป็นต้องใช้แม้แต่ LLM |
จากประสบการณ์ งานส่วนใหญ่ Sonnet·Haiku ก็เพียงพอ โมเดลราคาแพงใช้เฉพาะกับ "งานที่ถ้าผิดจะแพง" เท่านั้น อย่างไรก็ตามมีกับดักหนึ่งอย่าง — ถ้าลดเป็นโมเดลที่ถูกเกินไป อาการหลอน (hallucination) จะเพิ่มขึ้นจนต้นทุนการตรวจสอบกลืนกินยอดที่ประหยัดได้ (เชื่อมโยงโดยตรงกับ §22.2 อาการหลอน·ความปลอดภัยในบทก่อน) ดังนั้นการจัดสรรโมเดลจึงไม่ใช่ "ถูกไว้ก่อนไม่ว่ายังไง" แต่เป็นการแยก "งานที่ผิดก็ถูกก็ทำให้ถูก งานที่ถ้าผิดจะแพงก็ทำให้แพง"
บรรทัดสุดท้าย "การแมป·แทนที่อย่างง่าย → เชิงกำหนด" หลายครั้งเป็นการประหยัดที่ใหญ่ที่สุด งานที่คำตอบกำหนดไว้เป็นหนึ่งเดียวอย่างการแทนที่ชื่อ การแมปตามกฎที่กำหนดไว้แล้ว ไม่จำเป็นต้องเรียก LLM การเรียกที่ถูกที่สุดคือการทำให้การเรียกเองเป็น 0
แม้จะปิดกั้นโทเค็นต่อการเรียก (การ์ด) และลดราคาต่อหน่วย (การจัดสรรโมเดล) ถ้าส่งบริบทเดียวกันใหม่ทุกการเรียก ต้นทุนก็รั่ว อินพุตยาว ๆ ที่แทบไม่เปลี่ยนอย่างเอกสารวิสัยทัศน์ L0, ไลบรารี atom, สไตล์ไกด์ของสาขา ให้แคชไว้ เมื่อแคชฮิต ส่วนอินพุตนั้นจะถูกเรียกเก็บที่ประมาณ 1/10 ของราคามาตรฐาน (ตาราง §22.3.1)
# บริบทที่ไม่เปลี่ยนแปลงให้ทำเครื่องหมายด้วย cache_control — เมื่อแคชฮิตประมาณ 1/10
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": [
{"type": "text", "text": L0_VISION, "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": ATOM_LIBRARY, "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": SPECIFIC_TASK}, # เฉพาะส่วนที่เปลี่ยนทุกครั้งอยู่นอกแคช
]},
]
ประเด็นสำคัญคือ การแยกส่วนที่เปลี่ยนกับส่วนที่ไม่เปลี่ยน เนื่องจากแคชต้องให้ส่วนหน้าของอินพุตเหมือนกันจึงจะฮิต ให้วางบริบทคงที่ (L0·atom) ไว้ข้างหน้า และวางคำสั่งงานที่เปลี่ยนทุกครั้งไว้ข้างหลัง
จะใส่อะไรลงแคชแบ่งด้วยความถี่ของการเปลี่ยนแปลง
| บริบท | การแคช | เหตุผล |
|---|---|---|
| วิสัยทัศน์ L0 (แทบไม่เปลี่ยน) | เหมาะสม | เปลี่ยนแค่ในหน่วยไม่กี่วัน–ไม่กี่สัปดาห์ |
| ไลบรารี atom | เหมาะสม | อัปเดตเฉพาะตอนทบทวน |
| สไตล์ไกด์ของสาขา | เหมาะสม | เปลี่ยนในหน่วยไตรมาส |
| บันทึกการประชุมล่าสุด | ไม่เหมาะสม | เปลี่ยนทุกวัน — อัตราแคชฮิตต่ำ |
| อินพุตของผู้ใช้ | ไม่เหมาะสม | เฉพาะเจาะจงต่อทุกการเรียก |
TTL ของแคชถ้าสั้นก็อยู่ในหน่วยไม่กี่นาที จึงมีผลมากที่สุดใน งานที่ทุบบริบทเดียวกันต่อเนื่องกัน (เช่นการผลิตเมือง 30 แห่งที่ใช้ L0 เดียวกันซ้ำ 30 ครั้ง) สำหรับคำถามครั้งเดียวจบ จะมีแต่ต้นทุนการเขียนแคชโดยไม่เกิดฮิต อาจขาดทุนเสียด้วยซ้ำ — ด้วยเหตุนี้จึงประยุกต์ใช้แบบคัดเลือกเฉพาะกับ "งานที่ใช้บริบทเดียวกันบ่อย·ต่อเนื่อง" เท่านั้น
บทที่ว่าด้วยต้นทุนเป็นที่ที่ล่อใจมากที่สุดที่จะใส่ตารางอย่าง "ลดจากเดือนละ $5,000 เหลือ $1,000" ยอดประหยัดสัมบูรณ์แบบนั้นแตกต่างกันสุดขั้วตามขนาดทีม·ปริมาณงาน พอกุขึ้นมาเมื่อใด บทที่ว่าด้วยต้นทุนก็กลายเป็นการขัดแย้งกับตัวเองที่โกหกเรื่องต้นทุน บทนี้ใช้ตัวเลขเพียงสามชนิด
ชนิดแรก อ้างอิงราคาสาธารณะมาตามนั้น Opus $15 / Sonnet $3 / Haiku $0.80 (อินพุต 1M โทเค็น) ของ §22.3.1 และแคชฮิตประมาณ 1/10 เป็นค่าบริการที่ Anthropic เปิดเผย อัตราส่วนราคาอินพุต 19:4:1 และการแคชประหยัดได้ประมาณ 10 เท่าเป็นค่าที่ออกมาจากการคำนวณบนราคาสาธารณะนี้ — ไม่ใช่การประมาณ แต่เป็นการคำนวณ
ชนิดที่สอง ค่าคงที่ในโค้ดอ้างอิงโค้ด max_atom_body = 6000, max_matches = 3 เป็นค่าที่บันทึกไว้จริงใน inject_memory.py และ _jit_manifest.json ไม่ใช่การเปรียบเปรย แต่เป็นไฟล์จริง
ชนิดที่สาม สิ่งที่ไม่รู้ก็เขียนว่าไม่รู้ "6000 ตัวอักษรเป็นกี่โทเค็น" แตกต่างกันตามตัวตัดคำ·ภาษา·โมเดล หน่วยตัวจึงเป็นค่าประมาณ ใน §22.3.3 AI ก็แจ้งจุดนี้ด้วยเครื่องหมาย ★ ด้วยเหตุนี้จึงไม่มีตารางแปลงค่าอย่าง "6000 ตัวอักษร = N โทเค็น = ประหยัด $X" อยู่ที่ใดในบทนี้ แทนที่จะใช้ยอดประหยัดสัมบูรณ์ พูดด้วย ทิศทางและอัตราส่วน (19 เท่า·10 เท่า) เท่านั้น
ตัวเลขต้นทุนในบทนี้เป็นราคาสาธารณะ (ตารางราคา Anthropic) หรือเป็นค่าคงที่ที่ตรึงในโค้ด (
inject_memory.py·_jit_manifest.json) หรือเป็นค่าประมาณที่ระบุชัดเจนว่า "ไม่รู้"
| แพตเทิร์น | ทำไมจึงล้มเหลว | วิธีแก้ |
|---|---|---|
| ใช้โมเดลระดับสูงสุดกับทุกงาน | จ่ายราคาแพงกว่าประมาณ 19 เท่าไปถึงการจัดประเภท·แทนที่ | จัดสรรโมเดลตามงาน (§22.3.5) |
| ส่งบริบทเดียวกันซ้ำทุกการเรียก | ทิ้งแคชฮิต 1/10 ไป | แคชบริบทคงที่ (§22.3.6) |
| ไม่มีขีดจำกัดในการฉีดอัตโนมัติ | ฉีดไลบรารี atom ทั้งก้อนในทุกการเรียก | การ์ดจำนวน·ความยาว (§22.3.2) |
| จัดการต้นทุนด้วยคำมั่นว่า "ใช้อย่างประหยัด" | ความสามารถในการยับยั้งของคนหยุดการพุ่งทะยานไม่ได้ | ตรึงขีดจำกัดไว้ในโค้ด |
| เรียก LLM ไปถึงเรื่องที่ทำด้วยเชิงกำหนดได้ | การเรียกที่ถูกที่สุดคือ 'ไม่เรียก' | แยกการแมป·แทนที่ออกไปทำด้วยโค้ด |
ข้อที่สี่คือแก่น ถ้าฝากการควบคุมต้นทุนไว้กับเจตจำนงของคน มันจะรั่วแน่นอน เจตจำนงเป็นสิ่งที่พังเป็นอย่างแรกตอนยุ่ง และต้นทุนก็เพิ่มเร็วที่สุดตอนยุ่ง ด้วยเหตุนี้การควบคุมจึงต้องเป็น ค่าคงที่ในโค้ด อย่าง max_matches = 3
การประยุกต์นอกเกม สิ่งที่น่ากลัวเกี่ยวกับต้นทุน AI ไม่ใช่เพราะจำนวนเงินมันมาก แต่เพราะมันมองไม่เห็น และเรื่องนี้เหมือนกันไม่ว่าจะเป็นทีมเกมหรือทีมการตลาด ต้นทุนไม่ได้จับด้วยคำมั่นว่า "ใช้อย่างประหยัด" แต่จับด้วยโครงสร้าง หนึ่ง จัดสรรราคาต่อหน่วยของโมเดลให้เข้ากับระดับความยากของงาน — ถ้ารันแม้แต่การจัดประเภท·แท็กง่าย ๆ ด้วยโมเดลระดับสูงสุด ก็เท่ากับจ่ายราคาแพงกว่าหลายเท่าสำหรับงานเดียวกัน และการแมป·แทนที่อย่างง่ายนั้น การไม่เรียกเลย (จัดการด้วยกฎ·สูตร) คือการเรียกที่ถูกที่สุด สอง อินพุตยาว ๆ ที่แทบไม่เปลี่ยน (แนะนำบริษัท·เอกสารนโยบาย·อภิธานศัพท์) ให้แคชไว้เพื่อลดต้นทุนการส่งซ้ำ ตัวอย่างเช่น งานจัดประเภทคำถามของลูกค้าใช้โมเดลน้ำหนักเบาก็เพียงพอ และมอบเฉพาะการตรวจสอบสัญญาที่ซับซ้อนให้โมเดลระดับสูง ก็แบ่งราคาต่อหน่วยได้พร้อมรักษาคุณภาพ ถ้ามีจุดที่บริบทยาว ๆ ถูกแนบเข้ามาโดยอัตโนมัติ การกำหนด "ขีดจำกัดจำนวน·ความยาวที่แนบมาในครั้งเดียว" ไว้ จะปิดกั้นในเชิงโครงสร้างซึ่งอุบัติเหตุที่อยู่ดี ๆ ใบเรียกเก็บเงินเปลี่ยนหลักไป
ถ้าทำคนเดียว เท่านี้ก็พอ: ไม่มี hook ไม่มี manifest ก็ได้ ในเครื่องมือ AI ที่คุณใช้บ่อย ลองลดโมเดลของงานถัดไปหนึ่งชิ้นลงหนึ่งขั้น (งานสรุปที่เคยทำด้วย Opus ลงเป็น Sonnet, การจัดประเภทด้วย Sonnet ลงเป็น Haiku) ถ้าคุณภาพเอาต์พุตเพียงพอ งานนั้นก็ถูกตรึงไว้ที่ราคาที่ถูกกว่าอย่างถาวร แค่ถาม "งานนี้จำเป็นต้องใช้โมเดลระดับสูงสุดจริงหรือ" เพียงครั้งเดียวต่องาน ครึ่งหนึ่งของการประหยัดก็ออกมาจากตรงนั้น
ถ้าเป็นทีม ขอให้เริ่มด้วยขั้นถัดไปหนึ่งขั้นนี้ หาจุดที่ฉีดบริบทโดยอัตโนมัติ (hook·system prompt·RAG) มาหนึ่งจุด แล้วใส่การ์ดสองตัวของ §22.3.2 (ขีดจำกัดจำนวนการฉีด 1 ตัว, ขีดจำกัดความยาวเนื้อหา 1 ตัว) ลงไปเป็นโค้ดที่ตรงนั้น ถ้าแยกขีดจำกัดออกไปเป็น config อย่าง inject_memory.py ก็ปรับแค่ตัวเลขได้ระหว่างรันโดยไม่ต้องดีพลอยโค้ดใหม่ การ์ดสองบรรทัดปิดกั้นในเชิงโครงสร้างซึ่งอุบัติเหตุที่ "อยู่ดี ๆ ใบเรียกเก็บเงินเปลี่ยนหลักไป"
ถ้าสรุปเป็น setup → prompt → verify — setup: ใส่ค่าคงที่ขีดจำกัดจำนวน·ความยาวที่จุดฉีดอัตโนมัติ แล้วแยกออกไปเป็น config prompt: ขอให้ AI เสนอค่าขีดจำกัดด้วยรูปแบบ §22.3.3 โดยบังคับ trade-off และระดับความมั่นใจ verify: เลือกอินพุตที่ยาวที่สุด แล้วตรวจสอบด้วยตาเองโดยตรงว่าที่จุดขีดจำกัดอะไรถูกตัด
max_matches=3·truncate 6000 ตัวอักษร)ผู้อ่านหลัก: ผู้กำกับเกม·ลีดที่รับผิดชอบการนำ AI เข้ามาใช้ (ทีมขนาดกลาง 10–50 คน) ฉบับย่อสำหรับผู้อ่านที่ทำคนเดียว/งานอดิเรก: §22.4.9 「ถ้าทำคนเดียวก็แค่เท่านี้」
สองเดือนก่อนเปิดตัว เคยมีครั้งหนึ่งที่การประชุมหยุดชะงักเพราะภาพประกอบเมืองหนึ่งภาพที่คอนเซปต์อาร์ติสต์สร้างขึ้น มีคนถามว่า "อันนี้สร้างด้วย AI ใช่ไหม แล้วลิขสิทธิ์เป็นของเราหรือเปล่า หรือว่าจดทะเบียนไม่ได้เลย?" ไม่มีใครตอบได้ ความเห็นที่ออกมาในที่ประชุมนั้นมีอยู่สามแนวทาง "AI สร้างขึ้นมา ก็ไม่ใช่ของเรา" "เราจ่ายเงินรันมันเอง มันก็เป็นของเรา" "ยังไม่มีกฎหมาย ก็ใช้ไปเลย" ทั้งสามอย่างผิดหมด และคำถามนี้ก็ไม่ใช่แค่ประเด็นทางกฎหมายเท่านั้น บทบาทของอาร์ติสต์ที่สร้างภาพประกอบนั้นคืออะไร และทีมตกลงร่วมกันเรื่องการใช้ AI อย่างไร ทุกอย่างถูกแขวนไว้พร้อมกันในที่ประชุมนั้น
บทนี้ไม่ได้แยกพูดลิขสิทธิ์กับจริยธรรมออกจากกัน เพราะในงานจริงสองสิ่งนี้คือด้านหน้ากับด้านหลังของคำถามเดียวกัน "สิทธิ์ของผลงานชิ้นนี้เป็นของใคร" (ลิขสิทธิ์) ย่อกลับไปสู่ "มนุษย์เข้าไปมีส่วนกับผลงานชิ้นนี้มากแค่ไหน" (จริยธรรม·บทบาท) ทันที เงื่อนไขการจดทะเบียนที่คณะกรรมการลิขสิทธิ์เกาหลีตรึงไว้ในปี 2025 อยู่ตรงจุดนั้นพอดี ดังนั้นแกนกลางของบทนี้จึงเป็นบันทึกเซสชันจริง (worked transcript) ชิ้นเดียว — ติดตามตั้งแต่อินพุตไปจนถึงการตัดสินใจ ว่าตัดสินความเป็นไปได้ในการจดทะเบียนลิขสิทธิ์ของคอนเซปต์อาร์ต AI หนึ่งภาพจริง ๆ อย่างไร และคำตัดสินนั้นนำไปสู่การตกลงร่วมเรื่องบทบาทของทีมอย่างไร
บันทึกการใช้งานจริงของผู้เขียน atom
design_intent_vs_automation_boundaryและไฟล์_economy_log·_roi_report.mdที่อ้างถึงในบทนี้ เป็นการนำสินทรัพย์ด้านการกำกับดูแล (governance) ที่ผู้เขียนใช้งานจริงในบริษัทมาทำให้ไม่ระบุตัวตน ชื่อ atom และชื่อไฟล์ล็อกถูกถ่ายทอดตามชื่อที่ใช้งานจริง (เพื่อปกป้องทรัพย์สินทางปัญญา ได้แทนที่เฉพาะชื่อเฉพาะของบริษัท·โปรเจกต์เท่านั้น) ผลลัพธ์ในบันทึกเซสชันจริงเป็นการเรียบเรียงใหม่จากเซสชันการตัดสินจริง
หนังสือหลายเล่มเขียนเรื่องลิขสิทธิ์ AI ไว้แค่ว่า "ยังไม่มีกฎหมาย เลยคลุมเครือ" ถูกแค่ครึ่งเดียว ในเดือนมิถุนายน 2025 กระทรวงวัฒนธรรม กีฬา และการท่องเที่ยว กับคณะกรรมการลิขสิทธิ์เกาหลี ได้ประกาศ「คู่มือการจดทะเบียนลิขสิทธิ์ของผลงานที่ใช้ Generative AI」ทำให้อย่างน้อยในเกาหลี เส้นแบ่งว่าจดทะเบียนได้หรือไม่ก็ชัดเจนขึ้น ไม่จำเป็นต้องกุขึ้นมาเอง
ใจความสำคัญของคู่มือย่อลงเหลือประโยคเดียว เงื่อนไขของการจดทะเบียนลิขสิทธิ์คือ 'การมีส่วนสร้างสรรค์ของมนุษย์' จากตรงนี้แบ่งออกเป็นสองชนิด
| ประเภท | นิยาม | การจดทะเบียน |
|---|---|---|
| ผลงาน GAI | ผลลัพธ์ที่ AI สร้างขึ้นโดยไม่มีการมีส่วนสร้างสรรค์ของมนุษย์ | ไม่ได้ |
| ผลงานที่ใช้ GAI | ส่วนที่มีการมีส่วนสร้างสรรค์ของมนุษย์ ในผลงานที่มนุษย์ใช้ AI เป็นเครื่องมือสร้างขึ้น | ได้ |
และคู่มือได้เสนอสามเส้นทางที่จะได้รับการยอมรับว่าเป็น 'ผลงานที่ใช้ GAI' ① กรณีที่นำผลงานของผู้ใช้เองใส่เป็นพรอมต์ จนความสร้างสรรค์นั้นปรากฏในผลลัพธ์ ② กรณีที่งานเพิ่มเติมในการแก้ไข·เพิ่มลดผลลัพธ์มีความสร้างสรรค์ ③ กรณีที่การคัดเลือก·จัดวาง·ประกอบผลลัพธ์มีความสร้างสรรค์ สองแกนในการตัดสินคือ 'ความสามารถในการควบคุม' และ 'ความสามารถในการคาดการณ์' ผู้สร้างสรรค์ต้องกำหนดสิ่งที่ตนต้องการแสดงออกได้อย่างชัดเจน และดึงผลลัพธ์ออกมาตามเจตนานั้นได้ จึงจะได้รับการยอมรับว่ามีความสร้างสรรค์
ตรงนี้คือจุดชี้ขาด "ความสามารถในการควบคุม·ความสามารถในการคาดการณ์" ที่คู่มือพูดด้วยภาษากฎหมาย ก็คือสิ่งเดียวกันกับ "นักออกแบบเป็นผู้ให้เจตนา" (planner_provides_intent_not_recommendation atom) ที่หนังสือเล่มนี้พูดซ้ำมาตั้งแต่ §1.1 ผลงานที่ปล่อยให้ AI ทำทั้งหมดไม่มีการควบคุม·คาดการณ์ จึงไม่มีลิขสิทธิ์ ส่วนผลงานที่มนุษย์ป้อนเจตนาเข้าไปและตรวจสอบ·เรียบเรียงใหม่ สิทธิ์ก็จะตามมา ความเป็นไปได้ในการจดทะเบียนลิขสิทธิ์กับเงื่อนไขของเวิร์กโฟลว์ AI ที่ดี อยู่บนเส้นเดียวกัน
ยังมีเกณฑ์ที่เปิดเผยต่อสาธารณะอีกหนึ่งอย่าง กฎหมายพื้นฐาน AI ที่จะมีผลบังคับใช้ในปี 2026 กำหนด หน้าที่ในการสร้างความโปร่งใส (การแสดงข้อเท็จจริงว่าสร้างด้วย AI) ให้กับผลลัพธ์ของ Generative AI การจดทะเบียน (ฝั่งที่อ้างสิทธิ์) กับการแสดงที่มา (ฝั่งที่เปิดเผยข้อเท็จจริงในการใช้งาน) เป็นหน้าที่คนละอย่าง ไม่ว่าจะเกิดสิทธิ์ขึ้นหรือไม่ ก็ต้องเปิดเผยข้อเท็จจริงว่าใช้ AI สองเกณฑ์ที่เปิดเผยต่อสาธารณะนี้กลายเป็น อินพุตชั้นแรกของ rulebook ที่จะให้แก่ AI ในบทนี้
กลับไปที่ภาพประกอบในตอนต้นบท แทนที่จะตัดสินด้วย "ความรู้สึก" เราป้อนเกณฑ์จากคู่มือใน §22.4.1 เข้าไปเป็น rulebook แล้วให้ AI ทำการจำแนกชั้นแรก มนุษย์ตัดสินแค่ขั้นสุดท้ายเท่านั้น พรอมต์อินพุตด้านล่างคัดลอกไปใช้ได้ตามนั้นเลย และผลลัพธ์เป็นการเรียบเรียงใหม่จากเซสชันการตัดสินจริง
อินพุตของการตัดสินไม่ใช่ตัวภาพประกอบ แต่เป็นล็อกว่าภาพประกอบนั้นถูกสร้างขึ้นมาอย่างไร อันนี้มีอยู่ในเมตาดาตาของสินทรัพย์อยู่แล้ว เพียงแค่ดึงออกมาก็พอ
# asset_concept_city021_v4.meta.yaml — ประวัติการสร้างของผลงานที่นำมาตัดสิน
asset_id: concept_city021_v4
asset_type: concept_illustration
created_by: สมาชิกทีม A (คอนเซปต์อาร์ติสต์)
generation_log:
- step: 1
actor: สมาชิกทีม A
action: "แนบภาพสเก็ตช์เลย์เอาต์เมืองแบบร่างที่วาดเองเข้าไปเป็นภาพอินพุต"
- step: 2
actor: AI (image_model)
action: "สร้างภาพแปรผัน 4 แบบจากภาพร่าง + พรอมต์"
prompt: "stone observatory tower over sealed magic core, cold arid, scholar guild, muted palette"
- step: 3
actor: สมาชิกทีม A
action: "เลือก 1 จาก 4 แบบ แล้วรีทัชเงาหอระฆัง·ความอิ่มสี·องค์ประกอบภาพด้วยตนเอง (ทำงานใหม่ราว 40% ของพื้นที่)"
- step: 4
actor: สมาชิกทีม A
action: "ออกแบบลวดลายแท่นผนึกพื้นหลังด้วยตนเองแล้วนำมาประกอบ"
ai_generated_disclosure: true # ทำตามหน้าที่การแสดงที่มาของกฎหมายพื้นฐาน AI แล้ว
meta.yaml ที่แนบมาคือประวัติการสร้างของคอนเซปต์อิลลัสเตรชันหนึ่งภาพ ช่วยจำแนก
ความเป็นไปได้ในการจดทะเบียน ตามเกณฑ์ของคณะกรรมการลิขสิทธิ์เกาหลี
「คู่มือการจดทะเบียนลิขสิทธิ์ของผลงานที่ใช้ Generative AI」(2025) เป็นหนึ่งใน
จดทะเบียนได้/จดทะเบียนบางส่วน/จดทะเบียนไม่ได้ เหตุผลให้อิงสองแกนของคู่มือ (ความสามารถ
ในการควบคุม·ความสามารถในการคาดการณ์) อย่างละหนึ่งบรรทัด และระบุเป็นหมายเลขว่า step ไหน
ใน generation_log คือ 'การมีส่วนสร้างสรรค์' สิ่งที่ฟันธงทางกฎหมายไม่ได้ ให้ทำเครื่องหมาย
'ต้องให้ฝ่ายกฎหมายตรวจ' แล้วส่งต่อให้มนุษย์ ทำเป็นตาราง: ประเภท | เหตุผล(ควบคุม) | เหตุผล(คาดการณ์) | step ที่มีส่วน | ตรวจกฎหมาย
ขอให้สังเกตว่าครึ่งหนึ่งของพรอมต์คือ "การบังคับให้แสดงเหตุผล" และ "ทางออกสำหรับฝ่ายกฎหมาย" ถ้าปล่อยให้ AI ตัดสินทางกฎหมายทั้งหมด มันจะคายข้อสรุปที่ฟังดูน่าเชื่อออกมา หัวใจของพรอมต์นี้คือการใส่เกณฑ์การจำแนก (สามประเภทของคู่มือ) หน้าที่ในการแสดงเหตุผล (สองแกน) การระบุ step ที่มีส่วน และทางออกที่ว่า "ถ้าฟันธงไม่ได้ ให้ส่งต่อฝ่ายกฎหมาย"
| ประเภท | เหตุผล(ความสามารถในการควบคุม) | เหตุผล(ความสามารถในการคาดการณ์) | step ที่มีส่วน | ตรวจกฎหมาย | |---------|------------------------------|----------------------------|----------|---------| | จดทะเบียน | ภาพสเก็ตช์ร่าง(step1) และรีทัช 40% | การเลือก 1 จาก 4 แบบคาดการณ์ได้, | 1,3,4 | ★ | | บางส่วน | (step3) ควบคุมเจตนาการแสดงออกโดยตรง | แต่ตัวแปรผัน 4 แบบของ AI เองคาดการณ์ไม่ได้ | | | การวิเคราะห์: - step1(ป้อนภาพร่างที่วาดเอง): เข้าข่ายข้อ ① ของคู่มือ 'สะท้อนความสร้างสรรค์ผ่านการป้อนผลงานของตนเอง' - step3(รีทัช 40%): เข้าข่ายข้อ ② ของคู่มือ 'งานเพิ่มเติมในการแก้ไข·เพิ่มลด' การมีส่วนที่แข็งแกร่งที่สุด - step4(ออกแบบลวดลายแท่นผนึกด้วยตนเอง): การประกอบผลงานอิสระ เป็นการสร้างสรรค์ของมนุษย์อย่างชัดเจน - step2(แปรผัน 4 แบบของ AI): ผลลัพธ์ล้วน ๆ ที่ไม่มีการมีส่วนสร้างสรรค์ของมนุษย์ → ส่วนนี้เดี่ยว ๆ ไม่ใช่เป้าหมายการจดทะเบียน ★ ต้องให้ฝ่ายกฎหมายตรวจ: "รีทัช 40%" ถึงขั้น 'การดัดแปลงในสาระสำคัญ' หรือไม่ ไม่มีเกณฑ์เชิงปริมาณ สัดส่วนการรีทัชข้ามเส้นแบ่งที่จดทะเบียนได้หรือไม่ ควรให้ทนายความตรวจ (AI ฟันธงความเพียงพอทางกฎหมายของสัดส่วนไม่ได้)
ส่วนที่มีค่าที่สุดในผลลัพธ์ไม่ใช่ตาราง แต่เป็น ★ ต้องให้ฝ่ายกฎหมายตรวจ ที่อยู่ล่างสุด AI แจ้งข้อจำกัดของตัวเองว่า "ผมฟันธงไม่ได้ว่ารีทัช 40% เพียงพอทางกฎหมายหรือไม่" แล้วส่งต่อให้มนุษย์ พรอมต์ที่ดีทำให้ AI พูดได้ว่ามันไม่รู้ ไม่ใช่แบบปฏิเสธอัตโนมัติ แต่เป็นแบบแจ้งอัตโนมัติ
จะรับผลลัพธ์นี้มาตามนั้นเลยไม่ได้ ผู้กำกับเอา rulebook มาเคาะอีกที AI จำแนก step4 (ลวดลายแท่นผนึก) ว่าเป็น "ผลงานอิสระ" แต่พอดูประวัติการสร้างอีกครั้ง ลวดลายนั้นต่อยอดมาจาก lore ของเมืองที่ city_hunting_generator ใน §6.2 สร้างขึ้น กล่าวคือ step4 อาจไม่ใช่การสร้างสรรค์ล้วน ๆ แต่เป็น งานชั้นที่สองที่วางทับบนสินทรัพย์ภายในบริษัท เนื่องจากเป็นสินทรัพย์ของบริษัท การถือครองสิทธิ์จึงชัดเจน แต่ถ้อยคำของ AI ที่ว่า "ผลงานอิสระ" หากเขียนลงในใบสมัครจดทะเบียนตามนั้นเลยจะทำให้เกิดความเข้าใจผิด
จึงร้องขอใหม่
ลวดลายแท่นผนึกของ step4 เป็นงานชั้นที่สองที่ต่อยอดจากสินทรัพย์ lore ของเมืองภายในบริษัท (ไม่ใช่การสร้างสรรค์ใหม่อิสระ)
สะท้อนข้อเท็จจริงนี้แล้วจำแนกลักษณะการมีส่วนของ step4 ใหม่
ตอนยื่นจดทะเบียน ต้องระบุว่า 'อิงสินทรัพย์ภายในบริษัทที่มีอยู่เดิม' อย่างไร ช่วยเสนอมาหนึ่งบรรทัดด้วย
ปิดได้ด้วยการไป-กลับเพียงครั้งเดียวนี้ AI ตอบ step4 ใหม่จาก "ผลงานอิสระ" เป็น "ผลงานชั้นที่สองของสินทรัพย์ lore ภายในบริษัท — สิทธิ์ในสินทรัพย์ต้นทางถือครองภายในบริษัท ส่วนการมีส่วนในการดัดแปลงเป็นเป้าหมายการจดทะเบียน" และคำตัดสินนั้นถูกส่งต่อไปยังการตรวจของฝ่ายกฎหมาย ข้อสรุปกำหนดเป็น จดทะเบียนบางส่วน + แสดงข้อเท็จจริงว่าสร้างด้วย AI ถ้าทำด้วยมือทั้งหมด ฝ่ายกฎหมายต้องไล่ซักประวัติการสร้างของสินทรัพย์ทุกชิ้น แต่ถ้าเป็นร่างจาก AI + ตรวจด้วย rulebook + ไป-กลับหนึ่งครั้ง ฝ่ายกฎหมายก็ใช้เวลาเฉพาะกรณีก้ำกึ่งที่ทำเครื่องหมาย ★ ไว้เท่านั้น
หนึ่งรอบนี้คือเกณฑ์ Show ของบทนี้ ประโยคที่ว่า "ลิขสิทธิ์ AI คลุมเครือ" จะกลวงเปล่าจนกว่าจะได้ลองจำแนกประวัติการสร้างของผลงานหนึ่งชิ้นตามเกณฑ์ของคู่มือจนถึงที่สุด
เพื่อไม่ต้องตัดสินแบบในเซสชันข้างต้นใหม่ตั้งแต่ต้นทุกครั้ง เราบันทึกเกณฑ์ของคู่มือไว้เป็นแผนผังการไหล (flowchart) เมื่อมีสินทรัพย์หนึ่งชิ้นเข้ามา ก็แค่ไล่ลงไปตามแผนผังนี้ จุดแยกทุกจุดล้วนเป็นเกณฑ์ที่เปิดเผยต่อสาธารณะใน §22.4.1
flowchart TD
A["เกิดผลลัพธ์ AI"] --> B{"มนุษย์ได้ป้อน·ควบคุม
เจตนาหรือไม่?
(ภาพร่าง·ผลงานตนเอง·คำสั่งละเอียด)"}
B -->|ไม่ มีแต่พรอมต์| C["ผลลัพธ์จดทะเบียนไม่ได้
→ ใช้อ้างอิงเชิงสำรวจ·คอนเซปต์เท่านั้น
ห้ามใช้เป็นสินทรัพย์ขั้นสุดท้ายโดยตรง"]
B -->|ใช่| D{"ได้แก้ไข·เพิ่มลด
หรือคัดเลือก·จัดวางผลลัพธ์หรือไม่?"}
D -->|ไม่| C
D -->|ใช่| E{"เป็นโมเดลที่ระบุ
ข้อมูลฝึกหรือไม่?
หรือ fine-tune ภายในบริษัท"}
E -->|ไม่·คลุมเครือ| F["รอฝ่ายกฎหมายตรวจ
ตัดสินหลังประเมินความเสี่ยงการละเมิด"]
E -->|ใช่| G{"ต่อยอดจากสินทรัพย์
เดิมภายในบริษัทหรือไม่?"}
G -->|ใช่| H["ผลงานชั้นที่สอง
สิทธิ์สินทรัพย์ต้นทางถือครองภายในบริษัท
+ จดทะเบียนการมีส่วนในการดัดแปลง"]
G -->|ไม่| I["จดทะเบียนได้บางส่วน/ทั้งหมด"]
H --> J["แสดงข้อเท็จจริงว่าสร้างด้วย AI
(หน้าที่ตามกฎหมายพื้นฐาน AI)
+ บันทึก _economy_log"]
I --> J
F --> J
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class A ai;
class B,D,E,F,G human;
class J data;
class H,I pass;
class C fail;
หัวใจสำคัญคือจุดสิ้นสุดของแผนผัง (J) เหมือนกันในทุกเส้นทาง ไม่ว่าจะจดทะเบียนได้หรือไม่ ไม่ว่าจะเป็นสินทรัพย์บริษัทหรือผลงานชั้นที่สอง การแสดงข้อเท็จจริงว่าใช้ AI และการล็อกประวัติการสร้าง ต้องทิ้งไว้โดยไม่มีข้อยกเว้น การแสดงที่มาเป็นหน้าที่แยกจากสิทธิ์ และล็อกคือหลักฐานเดียวในการสืบสาวความรับผิดชอบเมื่อเกิดเหตุ สาเหตุที่การประชุมในตอนต้นบทหยุดชะงัก ก็เพราะไม่มีล็อกนี้ จึงไม่มีใครเรียบเรียงได้ว่าในแต่ละ step ใครทำอะไร
เส้นทางสีแดง (C, จดทะเบียนไม่ได้) ก็ไม่ใช่ของที่ทิ้งไปเฉย ๆ "ผลลัพธ์ AI ล้วน ๆ ที่ใส่แค่พรอมต์" ใช้เป็นข้อมูลอ้างอิงในขั้นสำรวจ·คอนเซปต์ได้อย่างเพียงพอ เพียงแต่ไม่นำมันไปใส่ในเกมเป็นสินทรัพย์ขั้นสุดท้ายเท่านั้น การนำผลลัพธ์ AI ขึ้นเปิดตัวตามนั้นเลย คือชนวนที่ใหญ่ที่สุดของอุบัติเหตุลิขสิทธิ์ในภายหลัง
design_intent_vs_automation_boundaryแผนผัง (§22.4.3) คือการไหลของการตัดสิน และสิ่งที่ทำให้การไหลนั้นถูกลากเป็นเส้นเดียวกันทุกครั้งคือ atom ตัวหนึ่ง ในบรรดาสินทรัพย์ด้านการกำกับดูแลของบริษัท design_intent_vs_automation_boundary คือแกนกลางของบทนี้ทั้งบท
นิยามหนึ่งบรรทัดของ atom นี้คือ "เจตนาการออกแบบเป็นของมนุษย์ การทำให้เป็นอัตโนมัติเป็นของเครื่องมือ — ระบุเส้นแบ่งนั้นในทุกสินทรัพย์" ไม่ใช่คำขวัญนามธรรม atom นี้ถูกลงทะเบียนใน JIT hook (inject_memory.py) ดังนั้นเมื่อพรอมต์มีคีย์เวิร์ดอย่าง "ลิขสิทธิ์"·"สร้างด้วย AI"·"จดทะเบียนสินทรัพย์" มันจะถูกฉีดเข้าเซสชันโดยอัตโนมัติ หลักการออกแบบของ hook รองรับการใช้งานของ atom นี้ได้พอดี
# inject_memory.py — exit 0 เสมอ ถึงล้มเหลวก็ไม่ขวางการทำงานของผู้ใช้ (ตัดตอน)
def main() -> None:
...
# เรียงลำดับ score จากมากไปน้อยแล้วจับคู่ — ฉีดได้สูงสุดแค่ 3 atom
atoms_sorted = sorted(atoms, key=lambda a: a.get("score", 0), reverse=True)
matches = []
for atom in atoms_sorted:
if len(matches) >= max_matches: # ป้องกันการฉีดมากเกินไป
break
try:
if re.search(atom["regex"], prompt, re.IGNORECASE):
matches.append(atom)
except re.error:
continue
if not matches:
emit_empty() # ถ้าไม่จับคู่ ก็ตอบกลับว่าง (ปกติ)
return
ตรงนี้มีการออกแบบที่สำคัญในเชิงการกำกับดูแลสองอย่าง หนึ่ง hook เป็น exit 0 เสมอ (ระบุไว้ใน docstring ของสคริปต์) ต่อให้ล้มเหลวระหว่างฉีด rulebook ลิขสิทธิ์ ก็จะไม่ขวางงานของผู้ใช้เด็ดขาด ถ้ากลไกป้องกันจับงานเป็นตัวประกัน ทีมจะปิดกลไกนั้นทิ้งภายในหนึ่งหรือสองไตรมาส สอง ฉีดได้สูงสุดแค่ 3 ตัว ถ้ายัดทุก rulebook การกำกับดูแลเข้าทุกเซสชัน คอนเท็กซ์จะระเบิดและไม่มีใครอ่าน เฉพาะ rulebook ที่ score สูงเท่านั้นที่จะลอยขึ้นมา
อันนี้เป็นปรัชญาเดียวกันกับที่ lint ใน §6.2 ไม่ทำลายการละเมิดทิ้งอัตโนมัติ แต่ส่ง alert ขึ้นไปยัง gate ของนักเขียนเท่านั้น เครื่องเป็นคนคัดผู้ต้องสงสัยออกมา แต่จะฆ่าหรือจะปล่อยให้มนุษย์เป็นคนตัดสิน ในเรื่องลิขสิทธิ์ก็เหมือนกัน atom จะลอย "ตรวจลิขสิทธิ์สินทรัพย์นี้แล้วหรือยัง" ขึ้นมาอัตโนมัติ แต่คำตัดสินขั้นสุดท้ายว่าจดทะเบียนได้หรือไม่ มนุษย์และฝ่ายกฎหมายเป็นคนทำ
การตัดสินภาพประกอบในตอนต้นบทไม่ได้จบลงที่ลิขสิทธิ์ เพราะมันหมายความว่างานของสมาชิกทีม A ที่สร้างสินทรัพย์นั้น ได้เคลื่อนจาก "การวาด" ไปเป็น "การเลือก 4 แบบของ AI และรีทัช 40%" ในวินาทีที่ลิขสิทธิ์เรียกร้อง "การมีส่วนสร้างสรรค์ของมนุษย์" นิยามบทบาทของคนที่ทำการมีส่วนนั้นก็เปลี่ยนตามไปด้วย ทั้งสองคือด้านหน้ากับด้านหลังของเหตุการณ์เดียวกัน
ตรงนี้อุบัติเหตุที่พบบ่อยที่สุดคือการจัดการความเปลี่ยนแปลงนี้ด้วยการแจ้งให้ทราบ ต่อให้เครื่องมือดี แต่หกเดือนต่อมาไม่มีใครใช้ ส่วนใหญ่ไม่ใช่เพราะเครื่องมือแย่ แต่เพราะไม่มีการตกลงร่วม การที่บทบาทเคลื่อนจากการผลิตจำนวนมากไปเป็นการคัดเลือก·ตรวจสอบ·เรียบเรียงใหม่ คือแก่นแท้ของการนำมาใช้ แต่ถ้าไม่ระบุชัดและไม่รองรับด้วยการอบรม สมาชิกทีมจะรับมันไว้ในความหมายว่า "ที่ของฉันกำลังจะหายไป"
| สายงาน | ก่อนมี AI | หลังมี AI (วิวัฒนาการบทบาท) | ความหมายเชิงลิขสิทธิ์ |
|---|---|---|---|
| คอนเซปต์อาร์ติสต์ | วาดเองทั้งหมด | ป้อนเจตนา·คัดเลือก·รีทัช | การรีทัชคือ 'การมีส่วนสร้างสรรค์' |
| คนปรับบาลานซ์ | จำลองด้วยมือ | ตีความผลจำลอง·ตัดสินใจ | ล็อกการตัดสินใจคือหลักฐานความรับผิดชอบ |
| นักออกแบบเกม | เขียนสเปกเองทั้งหมด | ให้เจตนา·ตรวจสอบ | การป้อนเจตนาคือความสามารถในการควบคุม |
หนึ่งบรรทัดที่ตารางบอกคือแบบนี้ 'การมีส่วนของมนุษย์' ที่ทำให้จดทะเบียนลิขสิทธิ์ได้ ก็คืองานที่มนุษย์ทำหลังวิวัฒนาการบทบาทนั่นเอง ถ้าการควบคุม·คาดการณ์ที่คู่มือเรียกร้องหายไป ลิขสิทธิ์ก็หายไป และที่ของมนุษย์ก็หายไป ดังนั้นวิวัฒนาการบทบาทจึงต้องถูกอธิบายว่าไม่ใช่ความเปลี่ยนแปลงที่แย่งงาน แต่เป็นความเปลี่ยนแปลงที่ทิ้งสิทธิ์และความรับผิดชอบไว้ในมือมนุษย์ จึงจะเกิดการตกลงร่วม
การตกลงร่วมไม่ใช่การประชุมที่ไม่จบสิ้น ปิดด้วยกระบวนการ เสนอการนำมาใช้ (ผู้กำกับ) → แชร์ล่วงหน้าทั้งทีม (วัตถุประสงค์·บทบาทที่ได้รับผลกระทบ·ตัวชี้วัด·ความเสี่ยง) → ประชุมตกลงร่วม (พูดได้อย่างอิสระ·เก็บข้อกังวล) → คุยตัวต่อตัวกับสมาชิกที่จำเป็น → ประกาศข้อปรับปรุง → ตกลงหรือพักไว้ ไม่จำเป็นต้องได้รับความเห็นชอบจากสมาชิกทุกคนถึงจะเริ่ม แต่หลังจากฟังและปรับข้อกังวลด้วยกระบวนการแล้ว ผู้กำกับเป็นคนตัดสิน ถ้าไม่มีกระบวนการ การตกลงร่วมจะเริ่มจาก 0 ใหม่ทุกครั้ง และต้นทุนนั้นก็จะทำให้การนำมาใช้ล่าช้า
_roi_report.mdถ้าจำกัดจริยธรรมไว้แค่เรื่องงาน·การตกลงร่วม จะพลาดไปหนึ่งแกน การวัดต้นทุนและประสิทธิผลของการใช้งาน AI อย่างซื่อตรงแล้วเปิดเผย ก็คือการกำกับดูแลในตัวมันเอง ถ้าพูดแค่ว่า "AI ทำให้ประสิทธิภาพดีขึ้น" โดยไม่มีการวัด สมาชิกทีมจะสงสัยว่าคำพูดนั้นเป็นข้ออ้างในการลดที่ของตน
ในโครงสร้างพื้นฐานการกำกับดูแลของบริษัทมีสินทรัพย์จริงสำหรับเรื่องนี้อยู่สองอย่าง _economy_log/ ของระบบ atom (ล็อกความคุ้มค่าของโทเค็น·เวลา) และ _roi_report.md (รายงาน ROI) อันแรกเครื่องบันทึกโทเค็น·เวลาของทุกเซสชัน ส่วนอันหลังนำมารวมเป็นรอบ ๆ ให้มนุษย์อ่าน หัวใจคือล็อกนี้ไม่ได้ติดตามว่า "AI แทนที่คนไปมากแค่ไหน" แต่ติดตามว่า "ปลดปล่อยเวลาของคนไปไว้ที่ไหน"
หลักการเรื่องตัวเลขของหนังสือเล่มนี้ประกอบด้วยสามข้อ หนึ่ง มาตรฐานที่เปิดเผยต่อสาธารณะ อ้างอิงตามนั้น (เงื่อนไขการจดทะเบียนของคู่มือ หน้าที่การแสดงที่มาของกฎหมายพื้นฐาน AI) สอง การประมาณของผู้เขียน เขียนว่าเป็นการประมาณ สาม สัญญาเป็น KPI เฉพาะสิ่งที่วัดได้เท่านั้น ในขอบเขตลิขสิทธิ์·จริยธรรม สิ่งที่วัดได้ไม่ใช่ตัวชี้วัดผลลัพธ์ แต่เป็นตัวชี้วัดกระบวนการ
| รายการที่วัด | วิธีวัด | สัญญาได้หรือไม่ |
|---|---|---|
| จำนวนการตกหล่นการแสดงข้อเท็จจริงว่าสร้างด้วย AI | grep เมตา ai_generated_disclosure ของสินทรัพย์ |
วัดได้ (เป้าหมาย 0) |
| อัตราการถือครองล็อกประวัติการสร้าง | สัดส่วนสินทรัพย์ที่มี generation_log |
วัดได้ |
| สินทรัพย์ AI ที่เปิดตัวโดยฝ่ายกฎหมายยังไม่ตรวจ | เทียบ build ที่เปิดตัว vs รายการที่ผ่านฝ่ายกฎหมาย | วัดได้ (เป้าหมาย 0) |
| "ยอดขายขึ้นเพราะ AI" | — | วัดไม่ได้ ไม่สัญญา |
บรรทัดสุดท้ายคือหัวใจของความซื่อตรง ผลของ AI ต่อยอดขายแยกออกมาเป็นตัวแปรเดี่ยวไม่ได้ จึงไม่ฟันธงความเป็นเหตุเป็นผล แต่ "สัดส่วนของสินทรัพย์ที่สร้างด้วย AI ซึ่งผ่านการแสดงที่มา·ล็อก·การตรวจของฝ่ายกฎหมาย" นับได้จริงด้วย _economy_log และเมตาของสินทรัพย์ สิ่งที่การกำกับดูแลสัญญาไม่ใช่ผลลัพธ์ แต่เป็นความสมบูรณ์ของกระบวนการ
เมื่อจัดการสิทธิ์สินทรัพย์·บทบาท·ต้นทุนเรียบร้อย ก็จะเหลืออยู่อีกหนึ่งขอบเขต เส้นทางที่ผู้ใช้นำเนื้อหาที่สร้างด้วย AI ขึ้นมาในเกม สินทรัพย์ที่บริษัทสร้างปิดได้ด้วยกระบวนการภายใน แต่ UGC คือผลลัพธ์นอกการควบคุมที่หลั่งไหลเข้ามา
ตรงนี้ก็ใช้จุดสิ้นสุดของแผนผัง §22.4.3 (การแสดงที่มา·ล็อก) ตามนั้นเลย คอสตูม·ตราสัญลักษณ์กิลด์ที่ผู้ใช้อัปโหลด เรียกร้องให้แสดงข้อเท็จจริงว่าสร้างด้วย AI และทำการดูแล (moderation) ด้วยการผสานตรวจสอบอัตโนมัติ + gate ของมนุษย์ ถ้าใช้งานแค่แกนใดแกนหนึ่ง อุบัติเหตุในไตรมาสถัดไปจะสะสม และข้อมูลผู้ใช้ก็ไม่ส่งให้ LLM อย่างพร่ำเพรื่อ ข้อมูลส่วนบุคคล·ข้อมูลการชำระเงิน ห้ามส่ง ล็อกพฤติกรรมส่งหลังทำให้ไม่ระบุตัวตน เป็นหลักการ (ปฏิบัติตาม GDPR·กฎหมายคุ้มครองข้อมูลส่วนบุคคลในประเทศ)
เขตอำนาจไม่ได้จบที่เกาหลีที่เดียว วินาทีที่รับผู้ใช้จากต่างประเทศ กฎระเบียบข้อมูลของภูมิภาคที่ผู้ใช้นั้นสังกัดก็จะเกี่ยวพันเข้ามาด้วย สำหรับผู้ใช้ EU GDPR กำหนดเงื่อนไขแยกต่างหากเรื่องการโอนข้อมูลส่วนบุคคลออกนอกภูมิภาค·การยินยอม·สิทธิ์ในการลบ และประเทศที่ให้บริการอื่น ๆ ก็มีกฎเรื่องข้อมูลส่วนบุคคล·การจัดเก็บข้อมูลในประเทศของตนเอง ดังนั้น 'ห้ามส่งข้อมูลส่วนบุคคล·ข้อมูลการชำระเงินให้ LLM' ในบรรทัดที่สามของตาราง จึงเป็นค่าตั้งต้นที่ปลอดภัยที่สุดในทุกเขตอำนาจ ส่วนระดับความเข้มข้นของการทำให้ไม่ระบุตัวตน·การแทนชื่อปลอม เมื่อจะส่งล็อกพฤติกรรมไปยังโมเดลภายนอก ต้องตรวจแยกต่างหากให้สอดคล้องกับภูมิภาคที่ให้บริการ อย่างไรก็ตาม หัวข้อนี้เป็นเพียงแนวทางการออกแบบกระบวนการ ไม่ใช่คำปรึกษาทางกฎหมาย เมื่อมีการเปิดตัวระดับโลก·การโอนข้อมูลออกนอกภูมิภาคเข้ามาเกี่ยวข้อง ต้องรับการตรวจของฝ่ายกฎหมายในเขตอำนาจนั้นแยกต่างหากเสมอ
| UGC/ข้อมูล | นโยบาย | เกณฑ์อ้างอิง |
|---|---|---|
| คอสตูม·ตราสัญลักษณ์ที่ผู้ใช้อัปโหลด | แสดงที่มา AI + ตรวจอัตโนมัติ + gate ของมนุษย์ | หน้าที่การแสดงที่มาของกฎหมายพื้นฐาน AI |
| ชื่อเล่นตัวละคร·โพสต์ | ข้อตกลงทั่วไป + ความรับผิดชอบของผู้ใช้ | — |
| ข้อมูลส่วนบุคคล·ข้อมูลการชำระเงิน | ห้ามส่งให้ LLM | กฎหมายคุ้มครองข้อมูลส่วนบุคคล |
| ล็อกพฤติกรรมในเกม | ส่งหลังทำให้ไม่ระบุตัวตน | การทำให้ไม่ระบุตัวตน·การแทนชื่อปลอม |
ยิ่ง UGC เพิ่มขึ้น ภาระการดูแลก็ยิ่งใหญ่ การตรวจอัตโนมัติคัดกรองในชั้นแรก เฉพาะกรณีก้ำกึ่งที่มนุษย์ดู อันนี้คือการนำปรัชญา atom ของ §22.4.4 (เครื่องคัดผู้ต้องสงสัยและมนุษย์ตัดสิน) มาใช้ในมิติของเนื้อหาผู้ใช้
| รูปแบบ | ทำไมถึงล้มเหลว | วิธีแก้ |
|---|---|---|
| ใช้ผลลัพธ์ AI เป็นสินทรัพย์ขั้นสุดท้ายตามนั้นเลย | การมีส่วนของมนุษย์ 0 → จดทะเบียนไม่ได้ + เสี่ยงละเมิด | แผนผัง §22.4.3, ใช้แค่เชิงสำรวจ·คอนเซปต์ |
| ไม่มีล็อกประวัติการสร้าง | เกิดเหตุแล้วเรียบเรียงความรับผิดชอบราย step ไม่ได้ | บังคับใส่เมตา generation_log ในสินทรัพย์ |
| ไม่แสดงข้อเท็จจริงว่าสร้างด้วย AI | ละเมิดหน้าที่ความโปร่งใสของกฎหมายพื้นฐาน AI | ขั้นแสดงที่มาที่จุดสิ้นสุดแผนผัง ไม่มีข้อยกเว้น |
| จัดการวิวัฒนาการบทบาทด้วยการแจ้งให้ทราบ | ถูกปฏิเสธ 6 เดือนหลังนำเครื่องมือมาใช้ | กระบวนการตกลงร่วม (§22.4.5) |
| ตะโกนแต่ "AI ทำให้ประสิทธิภาพดีขึ้น" | สมาชิกทีมสงสัยว่าเป็นภัยต่อที่ของตน | วัดและเปิดเผยด้วย _economy_log·_roi_report |
| ใช้โมเดลที่ข้อมูลฝึกคลุมเครืออย่างไม่วิพากษ์ | ตกหล่นการประเมินความเสี่ยงการละเมิด | ให้ความสำคัญกับโมเดลที่ระบุชัด·fine-tune ภายในบริษัท (จุดแยก E ของแผนผัง) |
ข้อที่ห้าถูกมองข้ามบ่อยที่สุด อย่างที่เห็นในการตัดสินภาพประกอบตอนต้นบท "การมีส่วนของมนุษย์" ที่ทำให้จดทะเบียนลิขสิทธิ์ได้ ก็คือบทบาทใหม่ของคนคนนั้นนั่นเอง ถ้าวัดแต่ประสิทธิภาพ แต่ไม่วัดว่าเวลาของคนคนนั้นถูกปลดปล่อยไปไว้ที่ไหน การกำกับดูแลก็จะสำเร็จในแง่ KPI แต่คนจะจากไป
การประยุกต์นอกเกม เหตุการณ์ที่การประชุมหยุดชะงักเพราะคำถามว่า "อันนี้สร้างด้วย AI แต่ลิขสิทธิ์เป็นของเราไหม" ไม่ได้เกิดแค่กับภาพประกอบเกม แต่เกิดได้ทุกที่ ไม่ว่าจะเป็นรายงาน·ก๊อปปี้โฆษณา·ข้อเสนอที่สร้างด้วย AI เกณฑ์ที่คู่มือ 2025 ของคณะกรรมการลิขสิทธิ์เกาหลีตรึงไว้นั้นเรียบง่าย — จดทะเบียนได้หรือไม่ขึ้นอยู่กับ "การมีส่วนสร้างสรรค์ของมนุษย์ (ความสามารถในการควบคุม·ความสามารถในการคาดการณ์)" และผลลัพธ์ AI ล้วน ๆ ที่ใส่แค่พรอมต์จะไม่มีสิทธิ์ ดังนั้นไม่ว่าแผนกใด นิสัยที่ทิ้งประวัติการสร้างหนึ่งใบ ซึ่งจดเป็น step ว่า "ขั้นไหนคนทำ ขั้นไหน AI ทำ" ในผลลัพธ์ AI หนึ่งชิ้น ก็จะกลายเป็นตาข่ายนิรภัย ยกตัวอย่าง ถ้านักการตลาดรับร่างก๊อปปี้จาก AI มาแล้วแก้ไข·เรียบเรียงใหม่ด้วยตนเอง การบันทึกการมีส่วนนั้นไว้ก็จะเป็นเหตุผลในการอ้างสิทธิ์ และไม่ว่าจะจดทะเบียนได้หรือไม่ การแสดงข้อเท็จจริงว่า "ใช้ AI" (หน้าที่ตามกฎหมายพื้นฐาน AI ปี 2026) ก็ต้องทิ้งไว้โดยไม่มีข้อยกเว้น เพราะถัดจากสิทธิ์คือคน ความเปลี่ยนแปลงบทบาทของพนักงานที่ทำการมีส่วนนั้น จึงต้องปิดด้วยการตกลงร่วม ไม่ใช่การแจ้งให้ทราบ
ถ้าทำคนเดียวก็แค่เท่านี้: ไม่ต้องมีทีมกฎหมายก็ได้ ลองเลือกผลงานภาพหรือข้อความที่คุณสร้างด้วย AI มา 1 ชิ้น แล้วจด
generation_logรูปแบบ §22.4.2 ด้วยมือดู (แบ่งเป็น step ว่าขั้นไหนคนทำ ขั้นไหน AI ทำ) จากนั้นลองไล่ตามแผนผัง §22.4.3 แล้วตัดสินด้วยตัวเองว่า "อันนี้จดทะเบียนได้ไหม" คุณจะรู้สึกเข้าใจถึงเนื้อตัวเลยว่าเกณฑ์ "ควบคุม·คาดการณ์" ของคู่มือคณะกรรมการลิขสิทธิ์เกาหลีคือชุดของการตัดสินแบบใด แม้จะเป็นโปรเจกต์ส่วนตัว·งานอดิเรก การทิ้งบรรทัดแสดงข้อเท็จจริงว่าสร้างด้วย AI ไว้สักบรรทัด (ai_generated: true) ก็เป็นเรื่องดี
ถ้าเป็นทีม ให้เริ่มด้วยหนึ่งขั้นถัดไปนี้ บังคับใส่สองสล็อต generation_log และ ai_generated_disclosure ในเมตาของสินทรัพย์ AI ทุกชิ้น (จับการตกหล่นได้ด้วย grep โค้ดบรรทัดเดียว) แล้วแปะแผนผังการตัดสินใจของ §22.4.3 ไว้บนวิกิหนึ่งหน้า การทำให้การตัดสินเงื่อนไขการจดทะเบียนเป็นอัตโนมัติ หรือการใช้งาน _economy_log ค่อยมาทีหลัง แค่มีล็อกประวัติการสร้างและแผนผังหนึ่งหน้า ก็ป้องกันเหตุการณ์ที่การประชุมหยุดชะงักแบบในตอนต้นบทได้แล้ว
ส่วนที่ 22 คือสี่แกนของการกำกับดูแล
| บท | ใจความ |
|---|---|
| 22.1 | วิศวกรรมพรอมต์ — บังคับรูปแบบ·เหตุผล·ทางออก |
| 22.2 | อาการหลอน·ความปลอดภัย — gate การตรวจสอบ·การตรวจสอบแบบแจ้ง |
| 22.3 | การจัดการต้นทุน — แคชชิง·cap·_economy_log |
| 22.4 | ลิขสิทธิ์·จริยธรรม — เงื่อนไขการจดทะเบียน·การแสดงที่มา·การตกลงร่วมเรื่องบทบาท |
ประโยคเดียวที่ทะลุทั้งสี่บทคือแบบนี้ การกำกับดูแลไม่ใช่กลไกที่ขวาง AI แต่เป็นกระบวนการที่ทิ้งเจตนาและความรับผิดชอบของมนุษย์ไว้ในผลงาน atom design_intent_vs_automation_boundary คือชื่อของกระบวนการนั้น "การควบคุม·การคาดการณ์" ที่การจดทะเบียนลิขสิทธิ์เรียกร้อง "บทบาท·การตกลงร่วม" ที่จริยธรรมเรียกร้อง และ "การวัดอย่างซื่อตรง" ที่ต้นทุนเรียกร้อง ทั้งหมดล้วนชี้ไปที่จุดเดียวกัน — ไม่ว่า AI จะทำอะไร ที่สุดท้ายของการตัดสินใจและความรับผิดชอบคือมนุษย์
แหล่งอ้างอิง - คณะกรรมการลิขสิทธิ์เกาหลี·กระทรวงวัฒนธรรม กีฬา และการท่องเที่ยว,「คู่มือการจดทะเบียนลิขสิทธิ์ของผลงานที่ใช้ Generative AI」(2025) — https://www.copyright.or.kr/information-materials/publication/research-report/view.do?brdctsno=54253 - คณะกรรมการลิขสิทธิ์เกาหลี·กระทรวงวัฒนธรรม กีฬา และการท่องเที่ยว,「คู่มือลิขสิทธิ์ Generative AI」(2023.12) — https://www.copyright.or.kr/information-materials/publication/research-report/view.do?brdctsno=52591 - กฎหมายพื้นฐานปัญญาประดิษฐ์ (กฎหมายพื้นฐาน AI) หน้าที่ความโปร่งใส·การแสดงที่มาของผลลัพธ์ Generative AI (มีผลบังคับใช้ปี 2026) — https://www.shinkim.com/kor/media/newsletter/3142
อย่าเพิ่มจำนวนเครื่องมือ แต่จงสร้างเครื่องมือของเครื่องมือ นี่คือเรื่องราวของโครงสร้างสองชั้นที่ซ่อนตัวจริง 48 ตัวไว้หลังจุดเข้าใช้ระดับโกลบอล 12 จุด และการทำงานอัตโนมัติที่รักษาความสอดคล้องของโครงสร้างนั้นไว้โดยไม่ต้องใช้มือคน
เย็นวันหนึ่งระหว่างที่ผมกำลังทำการทบทวนรายเดือน ผมนับรายการคำสั่งสแลช (slash command) ไปเรื่อย ๆ แล้วมือก็หยุดชะงัก มันมีอยู่สี่สิบคำสั่ง ทั้งที่เมื่อครึ่งปีก่อนเริ่มต้นด้วยเจ็ดแปดคำสั่งแท้ ๆ แต่พอสร้างเครื่องมือบันทึกการประชุมหนึ่งตัว เพิ่มเครื่องมือตรวจสอบข้อมูลอีกหนึ่งตัว เติมตัวสร้าง GDD (Game Design Document, เอกสารสเปกฉบับละเอียด) อีกหนึ่งตัว แบบนี้สัปดาห์ละหนึ่งถึงสองตัว ไม่ทันรู้ตัวมันก็กลายเป็นสี่สิบคำสั่ง และในจำนวนนั้นเกือบครึ่งหนึ่งไม่ได้ถูกเรียกใช้แม้แต่ครั้งเดียวตลอดเดือนที่ผ่านมา
ปัญหาอยู่ตรงที่เครื่องมือที่ไม่ได้ใช้ไม่ได้เพียงแค่นั่งเงียบ ๆ อยู่ตรงนั้น ทุกครั้งที่เริ่มเซสชัน ข้อกำหนดของคำสั่งสแลชทั้งสี่สิบตัวจะถูกโหลดเข้ามาทั้งหมด มันกัดกินงบประมาณโทเค็น ทำให้คำสั่งที่ชื่อคล้ายกัน (skill-design·skill-design-new·skill-design-template) ชวนสับสน และพอจะนึกถึงเครื่องมือที่ต้องการจริง ๆ ก็ต้องใช้เวลา เครื่องมือไม่ได้ช่วยทำงาน แต่การจัดการเครื่องมือกลับกลายเป็นงานเสียเอง
บทนี้ว่าด้วยกระบวนการที่ลดสี่สิบตัวให้เหลือสิบสองตัวระดับโกลบอล โดยไม่ทิ้งตัวจริงที่เหลือไปแม้แต่ตัวเดียว หัวใจอยู่ที่รูปแบบสามอย่าง Wrapper ที่สร้างจุดเข้าใช้แบบเบา Cascade ที่มัดเครื่องมือหลายตัวไว้ในทางเข้าเดียว และ Junction ที่เชื่อมจุดเข้าใช้กับตัวจริงเข้าด้วยกันในเชิงกายภาพ และ sync_skills.py ที่รักษาความสอดคล้องของทั้งสามอย่างนี้แทนคน
ความรู้สึกว่ามีเครื่องมือเยอะนั้นใคร ๆ ก็มี แต่ความรู้สึกอย่างเดียวตัดสินไม่ได้ว่าควรลดอะไร สิ่งที่ทำให้ตัดสินใจได้คือการวัดความคุ้มค่าของเครื่องมือในการทบทวนรายเดือน
โปรเจกต์นี้ดำเนินการทบทวน (การทบทวน) ในฐานะกลไกพัฒนาตนเอง ขณะที่การทบทวนรายวันสะสมเป็นรายสัปดาห์ และรายสัปดาห์รวมเป็นรายเดือน การทบทวนรายเดือนจะคำนวณย้อนกลับจากบันทึก commit ของ SVN ว่า "เดือนที่ผ่านมาใช้เครื่องมือไหนไปกี่ครั้ง" คะแนนที่ใช้ในการวัดนี้คือ skill_audit_score มันติดตามว่าคำสั่งสแลชแต่ละตัวปรากฏในผลงานจริงมากเพียงใดผ่านประวัติ commit แล้วให้คะแนนความถี่ในการใช้งาน
การกระจายตัวที่เผยออกมาในการวัดของเดือนนั้นเป็นดังนี้ (อัตราการใช้งานเป็นค่าวัดจริงจากบันทึก commit ของ SVN และเป็นสัดส่วนการปรากฏของแต่ละเครื่องมือ ไม่ใช่จำนวนครั้งที่เรียกใช้จริงในเชิงสัมบูรณ์)
สิบสองตัวบนสุดครองสัดส่วน 92% ของการใช้งานทั้งหมด ส่วนคำสั่งที่ไม่ได้ใช้แม้แต่เดือนละครั้งมีสิบแปดตัว คิดเป็น 45% ของทั้งหมด คำตอบจึงถูกกำหนดไว้ครึ่งหนึ่งแล้ว คือเปิดเผยเฉพาะสิบสองตัวที่ใช้บ่อยไว้ที่ระดับโกลบอล แล้วจัดการตัวที่เหลือ
ปัญหาอยู่ที่ "การจัดการ" ไม่ใช่ "การลบทิ้ง" เครื่องมือยี่สิบแปดตัวที่ไม่ได้ใช้ก็ยังจำเป็นในไตรมาสละหนึ่งถึงสองครั้งอยู่ดี ตอนเขียนรายงานครึ่งปี ตอนสร้างสคีมาข้อมูลใหม่ หรือตอนรันการตรวจสอบเฉพาะอย่าง เวลานั้นถ้าไม่มีเครื่องมือ งานก็จะหยุดอยู่กับที่ ฉะนั้นคำถามที่แท้จริงคือข้อนี้ จะทำอย่างไรให้เห็นแค่สิบสองตัว แต่ยังเก็บยี่สิบแปดตัวไว้ได้
อุปมาเรื่องโต๊ะทำงานคือสิ่งที่ร้อยเรียงทั้งบทนี้เข้าด้วยกัน ไม่มีใครวางปากกาสี่สิบด้ามไว้บนโต๊ะแล้วใช้ทุกวัน เราวางเฉพาะปากกาสิบสองด้ามที่ใช้บ่อยไว้บนโต๊ะ แล้วเก็บที่เหลือไว้ในลิ้นชัก แม้แต่ในลิ้นชักก็รวบรวมชนิดเดียวกันไว้ในกระบอกเดียว Wrapper คือจุดเข้าใช้แบบเบาที่วางไว้บนโต๊ะ Junction คือทางเชื่อมระหว่างลิ้นชักกับโต๊ะ ส่วน Cascade คือมัดปากกาที่มัดรวมไว้ในกระบอกเดียว
Wrapper คือเปลือกบาง ๆ ของคำสั่งสแลช ที่ระดับโกลบอลวางไว้แค่จุดเข้าใช้ ส่วนตรรกะจริงวางไว้ที่ตัวจริงใน workspace ในไดเรกทอรีโกลบอลมีเอกสารแนะนำ 50 บรรทัด ส่วนในตัวจริงมีโค้ดที่ implement จริง 500 บรรทัดอาศัยอยู่
flowchart LR
subgraph G["โกลบอล ~/.claude/skills/ (บนโต๊ะ)"]
W1["proj-meeting
Wrapper · 50 บรรทัด"]
W2["proj-gdd
Wrapper · 50 บรรทัด"]
end
subgraph B["workspace/skills/ (ในลิ้นชัก)"]
M1["proj-meeting/
SKILL.md + .py สกัด·จัดหมวด
ราว 500 บรรทัด"]
M2["proj-gdd/
SKILL.md + ตัวสร้าง
ราว 500 บรรทัด"]
end
W1 -->|เรียกใช้| M1
W2 -->|เรียกใช้| M2
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
class W1,W2,M1,M2 code;
การแยกส่วนนี้สร้างประโยชน์ห้าอย่าง เมื่อเริ่มเซสชันจะโหลดแค่ 50 บรรทัดที่ระดับโกลบอลจึงประหยัดโทเค็น ตัวจริงแก้ไขทุกวันก็ไม่กระทบสล็อตโกลบอล จะวางตัวจริงไว้ที่ SVN หรือ Git หรือที่ใดก็ได้ วางตัวจริงไว้ในโฟลเดอร์แชร์ของทีมแล้ววางแค่ Wrapper ไว้ที่โกลบอลส่วนตัวจึงแชร์ได้ง่าย และถ้ารวมรูปแบบของ Wrapper ให้เป็นมาตรฐานเดียวกัน ประสบการณ์ผู้ใช้ก็จะสอดคล้องกัน
รูปแบบมาตรฐานของ Wrapper เป็นดังนี้ Wrapper ทุกตัวใช้โครงร่างนี้ร่วมกัน
---
name: proj-meeting
description: วิเคราะห์บันทึกการประชุม·สกัดการตัดสินใจ (ตัวจริง: workspace/skills/proj-meeting/)
---
# /proj-meeting — Wrapper
ตำแหน่งตัวจริง: workspace/skills/proj-meeting/SKILL.md
## การทำงาน
Wrapper นี้เรียกสคริปต์เข้าใช้ของตัวจริง ตรรกะรายละเอียดนิยามไว้ในตัวจริง
เมื่อตัวจริงเปลี่ยน เพียงปรับ description ของ Wrapper นี้ก็พอ (แนะนำให้ซิงค์อัตโนมัติ)
หัวใจคือมีเพียง description หนึ่งบรรทัดกับตัวชี้ไปยังตัวจริงเท่านั้น ในวินาทีที่ตรรกะเข้าไปอยู่ใน Wrapper มันก็จะหนักขึ้น และการซิงค์กับตัวจริงก็จะเริ่มแตก ฉะนั้นจึงบังคับเป็นกฎให้ Wrapper อยู่ภายใน 100 บรรทัด
สล็อตคำสั่งสแลชระดับโกลบอลของโปรเจกต์นี้มัดไว้ที่สิบสองตัว เครื่องมือที่ใช้บ่อยทั้งหมดต้องเข้าให้ได้ภายในสิบสองตัวนี้ และเกณฑ์การคัดเลือกให้การทบทวนรายเดือนเป็นผู้ดู ใช้งานตั้งแต่เดือนละ 5 ครั้งขึ้นไป สมดุลของสาขา (เครื่องมือสาขาหนึ่งต้องไม่เกินหกตัว) ความสอดคล้องของการเข้าใช้ (รวมกฎการตั้งชื่อให้เป็นหนึ่งเดียว) ถ้าเกินสิบสองตัวก็เลิกใช้ตัวที่ใช้น้อยที่สุดหนึ่งตัว หรือควบรวมเข้ากับคำสั่งอื่น
เลขสิบสองไม่ได้เป็นค่าสัมบูรณ์ หัวใจอยู่ที่ข้อเท็จจริงที่ว่ามีจำนวนกำหนดไว้ต่างหาก หากเป็นทีมขนาดเล็ก (\~10 คน) สิบตัวอาจพอเหมาะ หากเป็นทีมที่มีสาขาเยอะ สิบห้าตัวอาจเหมาะกว่า ต้องมีเพดานที่กำหนดไว้ ภาระทางความคิด (cognitive load) จึงจะอยู่ที่ระดับคงที่
ถ้า Wrapper คือกฎที่ว่า "ที่โกลบอลวางแค่จุดเข้าใช้แบบเบา" Junction ก็คือวิธีการที่ implement กฎนั้นในระดับระบบปฏิบัติการ Junction คือ symbolic link ของไดเรกทอรี กล่าวคือชื่อแทน (alias) ที่ระบบปฏิบัติการมีให้
flowchart LR
U["~/.claude/skills/proj-meeting
(Junction — ชื่อแทน)"]
R["workspace/skills/proj-meeting/
(ตัวจริง — ไฟล์จริงหนึ่งชุด)"]
U -. "ชี้ไปยังของจริง" .-> R
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class R code;
class U data;
เมื่อผู้ใช้มองเข้าไปที่ตำแหน่งโกลบอล จะดูเหมือนว่าตัวจริงอยู่ตรงนั้น แต่ไฟล์จริงมีอยู่เพียงชุดเดียวที่ตำแหน่งตัวจริง ฝั่งโกลบอลเป็นเพียงป้ายบอกทางที่ชี้ไปยังที่นั่นเท่านั้น
ประโยชน์ที่โครงสร้างนี้ให้มานั้นชัดเจน เมื่อแก้ตัวจริงจะสะท้อนที่โกลบอลทันที (ไม่มีขั้นตอนคัดลอก) เพราะไฟล์มีอยู่เพียงชุดเดียวจึงประหยัดดิสก์ และเพราะที่โกลบอลมีแค่ Junction จึงไม่มี Git conflict (ตัวจริงจัดการแยกใน SVN/Git) แม้จะย้ายตัวจริง เพียงเชื่อม Junction ใหม่ ผู้ใช้ก็ไม่เห็นความเปลี่ยนแปลงใด ๆ
แต่ละ OS มีวิธีเชื่อมต่างกัน บน Windows สร้าง junction ของไดเรกทอรีด้วย mklink /J <link> <target> โดยไม่ต้องใช้สิทธิ์ผู้ดูแลระบบ ส่วน Linux และ macOS ใช้ ln -s <target> <link> และ WSL ใช้คำสั่ง Linux ได้เลย ความต่างของแพลตฟอร์มนี้ sync_skills.py ที่จะกล่าวถึงในภายหลังจะจัดการให้อัตโนมัติ ผู้ดำเนินการจึงไม่จำเป็นต้องจำคำสั่งของแต่ละ OS เอง
หากไม่ใช้ Junction แต่ดำเนินการด้วยการคัดลอก ในวินาทีที่ตัวจริงกับสำเนาโกลบอลแยกออกจากกันก็จะเกิดอุบัติเหตุการซิงค์ เช่น แก้บั๊กที่ตัวจริงแล้ว แต่สำเนาโกลบอลเป็นเวอร์ชันเก่าจึงทำงานแบบเก่า Junction กำจัดความเป็นไปได้ของอุบัติเหตุนี้ตั้งแต่ต้น ป้ายบอกทางจะมีสองอันไม่ได้ และของจริงมีหนึ่งเดียวเสมอ
ถ้าจัดการ Wrapper กับ Junction ด้วยมือ สุดท้ายก็จะกลับไปเป็นสี่สิบตัว คนเรามักผัดผ่อนการจัดเก็บ ลืมนโยบาย และสร้างข้อยกเว้น ฉะนั้นจึงทำให้การรักษาความสอดคล้องเป็นอัตโนมัติ เครื่องมือนั้นคือ sync_skills.py
ทุกครั้งที่เซสชันเริ่ม Hook จะ trigger สคริปต์นี้ สิ่งที่สคริปต์ทำเป็นไปตามกระแสต่อไปนี้
flowchart TD
H["เริ่มเซสชัน (Hook trigger)"] --> S["สแกน ~/.claude/skills/"]
S --> C{"ตรงตามนโยบาย
Wrapper สิบสองตัว?"}
C -->|"พบสล็อตตกค้าง"| X["--cleanup:
จัดเก็บ Wrapper นอกนโยบาย"]
C -->|"ตรวจพบการย้ายตัวจริง"| J["สร้าง Junction ใหม่อัตโนมัติ"]
C -->|"ตรงกัน"| OK["ผ่าน"]
X --> OK
J --> OK
OK --> R["รับประกันความสอดคล้องของ 12 สล็อตโกลบอล"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class H,S,C,X,J code;
class OK,R pass;
ฟังก์ชันหลักมีสามอย่าง หนึ่ง สแกนไดเรกทอรีโกลบอลแล้วตรวจว่าตรงตามนโยบาย Wrapper สิบสองตัวหรือไม่ สอง จัดเก็บ Wrapper ตกค้างที่ไม่อยู่ในนโยบายด้วยแฟล็ก --cleanup ถ้าใครเพิ่มเครื่องมือชั่วคราวไว้แล้วยังค้างอยู่ในสล็อต มันจะถูกจัดเก็บเมื่อเริ่มเซสชันถัดไป สล็อตจึงไม่พุ่งทะลักอีก สาม ถ้าตำแหน่งตัวจริงเปลี่ยน ก็เชื่อม Junction ใหม่ให้อัตโนมัติ โดยตรวจจับ OS ถ้าเป็น Windows ก็เรียก mklink /J ถ้านอกเหนือจากนั้นก็เลือก ln -s
เรื่องสำคัญคือฟังก์ชันทั้งสามถูกออกแบบให้ idempotent (ให้ผลคงเดิมเมื่อรันซ้ำ) เพราะเป็นเครื่องมือที่ทำงานอัตโนมัติทุกครั้งที่เริ่มเซสชัน ฉะนั้นต่อให้รันซ้ำกี่ครั้งบนสถานะเดิม ผลลัพธ์ก็ต้องเหมือนกับรันเพียงครั้งเดียว Wrapper ที่ตรงตามนโยบายอยู่แล้วจะไม่ถูกแตะต้อง Junction ที่เชื่อมไว้ถูกต้องแล้วจะไม่ถูกเชื่อมซ้ำ และถ้าไม่มีสล็อตตกค้างให้จัดเก็บก็จะไม่ลบอะไรเลย หากไม่ idempotent การจัดเก็บแบบเดิมจะทับซ้อนกันทุกเซสชัน จนเกิดอุบัติเหตุสร้าง Junction ที่ดีอยู่แล้วใหม่ หรือแตะต้องตัวจริงผิด ๆ ซึ่งในเครื่องมือที่ทำงานไร้คนทุกเซสชัน นี่ย่อมนำไปสู่อุบัติเหตุการซิงค์โดยตรง ฉะนั้น sync_skills.py จึงตั้ง "แตะเฉพาะสิ่งที่เปลี่ยน และถ้าไม่มีอะไรเปลี่ยนก็ไม่แตะ" ไว้เป็นค่าคงที่ (invariant)
ผลของ --cleanup เชื่อมโยงตรงกับการปกป้องงบประมาณโทเค็น เมื่อมัดข้อกำหนดสแลชที่โหลดเข้าโกลบอลทุกเซสชันไว้ที่สิบสองตัว ต่อให้ตัวจริงเพิ่มเป็นสี่สิบแปดตัว ต้นทุนการเริ่มเซสชันก็คงที่ และเพราะคนไม่ต้องจัดการด้วยมือ นโยบายจึงไม่กระเจิง
ความสอดคล้องอัตโนมัตินี้คือสลักนิรภัยของโครงสร้างสองชั้น Wrapper กับ Junction สร้างโครงสร้าง ส่วน sync_skills.py รักษาโครงสร้างนั้นไว้แม้เวลาผ่านไป
เมื่อรูปแบบสามอย่างกับความสอดคล้องอัตโนมัติมารวมกัน โครงสร้างสองชั้นต่อไปนี้ก็เสร็จสมบูรณ์ ชั้นบนมีจุดเข้าใช้สิบสองจุดที่ผู้ใช้ต้องจำ ชั้นล่างมีตัวจริงสี่สิบแปดตัว
flowchart TD
subgraph L1["ชั้นที่ 1 — โกลบอล 12 Wrapper (ทั้งหมดที่ผู้ใช้ต้องจำ)"]
direction LR
w1["#1"] -.- w12["#12"]
end
subgraph L2["ชั้นที่ 2 — workspace 48 ตัวจริง (ของจริงที่ซ่อนไว้)"]
direction LR
b1["ตัวจริง 1"] --- bN["ตัวจริง 48"]
end
L1 -->|"เชื่อมด้วย Junction"| L2
note["sync_skills.py --cleanup:
จัดชั้นที่ 1 ให้เป็น 12 ตัวทุกเซสชัน"]
note -.-> L1
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
class w1,w12,b1,bN,note code;
ผู้ใช้จำแค่สิบสองตัวระดับโกลบอลเท่านั้น แม้จะมีตัวจริงสี่สิบแปดตัวซ่อนอยู่ด้านหลัง ภาระทางความคิดก็อยู่ที่สิบสองตัว Wrapper ทำให้จุดเข้าใช้เบา Junction เชื่อมจุดเข้าใช้กับตัวจริง และ sync_skills.py รักษาความสอดคล้องของสิบสองตัวนั้นไว้ทุกเซสชัน
เมื่อดูเป็นสัดส่วน จุดเข้าใช้ต่อตัวจริงเป็น 1 ต่อ 4 (12 ต่อ 48) ต่อให้เพิ่มเครื่องมือ สิ่งที่ผู้ใช้ต้องจำก็ไม่เพิ่ม ต่อให้ตัวจริงเพิ่มเป็นหกสิบตัว แปดสิบตัว ชั้นที่ 1 ก็ยังคงเป็นสิบสองตัว นี่คือการ implement จริงของประโยค "อย่าเพิ่มจำนวนเครื่องมือ แต่จงสร้างเครื่องมือของเครื่องมือ" สิ่งที่เพิ่มคือชั้นที่ 2 (ตัวจริง) ส่วนชั้นที่ 1 (จุดเข้าใช้) ที่ผู้ใช้เผชิญหน้านั้นคงที่
ถ้าโครงสร้างสองชั้นคือรูปแบบที่ "ลดเครื่องมือมากให้เหลือจุดเข้าใช้น้อย" Cascade ก็คือรูปแบบที่ "มัดเครื่องมือที่มักใช้ร่วมกันไว้ในการเรียกครั้งเดียว" คำสั่งสแลชหนึ่งเรียกเครื่องมือย่อยหลายตัวตามลำดับ แล้วออกผลลัพธ์เป็นรายงานสรุปฉบับเดียว
Cascade ตัวแทนของโปรเจกต์นี้คือ check ผมรวมเครื่องมือสี่ตัวที่ใช้ตรวจสอบความถูกต้องสมบูรณ์ของข้อมูลออกแบบทุกเช้าให้เป็นหนึ่งเดียว
flowchart TD
E["/check (Wrapper · ทางเข้า Cascade)"] --> S1["doc-audit
ความสอดคล้องของมาร์กดาวน์"]
S1 --> S2["data-qa
ตรวจสอบชีตข้อมูล"]
S2 --> S3["integrity
ความสอดคล้องของ foreign key"]
S3 --> S4["link-check
ความถูกต้องสมบูรณ์ของ Wikilink"]
S4 --> R["รายงานสรุป
(แสดงรายละเอียดเฉพาะที่ล้มเหลว ที่ผ่านสรุปย่อ)"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class E,S1,S2,S3,S4 code;
class R data;
เมื่อก่อนทุกเช้าผมเรียกเครื่องมือสี่ตัวแยกกันทีละตัว ตรวจเอกสารหนึ่งครั้ง ตรวจข้อมูลหนึ่งครั้ง ตรวจ foreign key หนึ่งครั้ง ตรวจลิงก์หนึ่งครั้ง ต่องานหนึ่งรอบต้องเรียกด้วยมือสามถึงสี่ครั้ง check มัดทั้งสี่ตัวนี้ไว้ในคำสั่งเดียว พอเรียกครั้งเดียวก็จะรันสี่ขั้นตามลำดับ แล้วผลลัพธ์ก็รวมเป็นหนึ่ง
การออกแบบ Cascade มีหลักการ แต่ละขั้นต้องเรียกเดี่ยวได้ด้วย (data-qa ต้องเรียกแยกเองได้) เมื่อล้มเหลวจะหยุดหรือไปต่อให้ตั้งค่าเป็นรายขั้น งานตรวจสอบนั้นต่อให้ขั้นหนึ่งล้มเหลวก็รันที่เหลือต่อเพื่อดูภาพรวมทั้งหมด ส่วนงานเปลี่ยนแปลงนั้นถ้าขั้นหนึ่งล้มเหลวก็หยุดทันที ผลลัพธ์สะสมต่อเป็น input ของขั้นถัดไป และรายงานสรุปใช้รูปแบบเดียวกันในทุก Cascade
นิยามจริงของ check เป็นดังนี้ (เป็นการรวมการตรวจสอบ 4 ชนิดไว้เป็นหนึ่ง)
cascade:
- step: doc-audit
purpose: ความสอดคล้องของมาร์กดาวน์ (YAML frontmatter·ลิงก์·การอ้างอิง atom)
fail_action: continue
- step: data-qa
purpose: ตรวจสอบชีตข้อมูล Excel (สคีมา·ช่วงค่า·คอลัมน์ที่จำเป็น)
fail_action: continue
- step: integrity
purpose: ความสอดคล้องของ foreign key (การอ้างอิงระหว่างชีต)
fail_action: continue
- step: link-check
purpose: ความถูกต้องสมบูรณ์ของ Wikilink·ลิงก์ภายนอก
fail_action: continue
report:
format: markdown
include_pass: false # รายการที่ผ่านแสดงแค่สรุป แสดงรายละเอียดเฉพาะที่ล้มเหลว
group_by: severity
ที่ fail_action: continue ติดอยู่ในทั้งสี่ขั้นเพราะนี่คือ Cascade ตรวจสอบ ต่อให้การตรวจหนึ่งล้มเหลวก็รันอีกสามที่เหลือต่อให้จบ เพื่อดูรายการข้อบกพร่องทั้งหมดของวันนั้นในครั้งเดียว รายงานพับรายการที่ผ่านไว้เป็นแค่สรุปแล้วกางเฉพาะที่ล้มเหลว เพื่อรวมสายตาไปยังสิ่งที่ต้องดูในตอนเช้า
Cascade ก็มีกับดักเช่นกัน ถ้าเพิ่มขั้นไม่มีที่สิ้นสุด ความซับซ้อนจะระเบิด ฉะนั้นเช่นเดียวกับนโยบาย 12 สล็อต Cascade ก็ตั้งเพดานจำนวนขั้นไว้ ถ้าเกินประมาณห้าถึงเจ็ดขั้นก็แยกเป็นสองหรือแยกบางส่วนออกเป็น Cascade ต่างหาก
เมื่อโครงสร้างสองชั้นกับ Cascade เข้าที่ การเพิ่มตัวจริงก็กลายเป็นเรื่องง่าย เพราะแค่เพิ่มตัวจริงเข้า workspace โดยไม่ต้องแตะสล็อตโกลบอลก็พอ แต่ตรงนี้แหละที่กับดักใหม่ก่อตัวขึ้น เมื่อการเพิ่มกลายเป็นเรื่องง่าย เครื่องมือคล้าย ๆ กันก็จะสะสมซ้ำซ้อน
ฉะนั้นเวลาเพิ่มตัวจริงจึงบังคับนโยบายหนึ่งอย่าง นั่นคือ นโยบาย MECE Wrapper เมื่อจะเพิ่มเครื่องมือใหม่ ให้ตัดสินสองแพร่ง ถ้าขอบเขตทับซ้อนกับเครื่องมือเดิม อย่าสร้างใหม่แต่ให้เสริมเครื่องมือเดิม สร้างใหม่ต่อเมื่อขอบเขตต่างกันชัดเจนเท่านั้น หมายความว่ารักษารายการตัวจริงให้ไม่ซ้ำซ้อนกัน (Mutually Exclusive) และไม่ตกหล่น (Collectively Exhaustive)
flowchart TD
N["ต้องการเครื่องมือใหม่?"] --> Q{"ขอบเขตทับซ้อน
กับตัวจริงเดิมหรือไม่?"}
Q -->|"ทับซ้อน"| A["ห้ามสร้างใหม่ →
เสริมเครื่องมือเดิม"]
Q -->|"ต่างกันชัดเจน"| B["อนุญาตให้เพิ่มตัวจริงใหม่"]
A --> M["รักษา MECE: ไม่ซ้ำซ้อน"]
B --> M
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class Q human;
class M pass;
การทบทวนเป็นตัวหนุนการตัดสินนี้ เพราะ skill_audit_score วัดความถี่การใช้ของแต่ละเครื่องมือจากบันทึก SVN สัญญาณอย่าง "เครื่องมือนี้จริง ๆ แล้วทำงานแทบเหมือนกับเครื่องมือนั้น แต่ทั้งคู่แทบไม่ได้ใช้" จึงถูกจับได้ จากนั้นก็รวมทั้งสองเป็นหนึ่ง หรือถอดตัวที่ใช้น้อยกว่าออกจากตัวจริง การคัดสรรนั้นการจัดระเบียบสำคัญพอ ๆ กับการเพิ่ม
หากไม่มีนโยบาย MECE "เสรีภาพในการเพิ่มตัวจริง" ที่โครงสร้างสองชั้นสร้างขึ้นจะกลับกลายเป็นพิษ ต่อให้ตัวจริงไม่ไปกัดกินสล็อตชั้นที่ 1 แต่ถ้าตัวจริงเองบวมขึ้นด้วยความซ้ำซ้อน เราก็จะเริ่มสับสนอีกว่าควรใช้ตัวจริงไหน นโยบายนั้นป้องกันการบวมนี้
ถึงตรงนี้ลองย้อนกลับไปที่จุดเริ่มต้น ทั้ง Wrapper ทั้ง Junction ทั้ง Cascade ทั้งนโยบาย MECE ไม่มีอันไหนเลยที่ออกแบบไว้ล่วงหน้าจากบนโต๊ะ ทั้งหมดเกิดขึ้นในฐานะคำตอบต่อปัญหาที่ค้นพบจากการทบทวน
เมื่อ skill_audit_score ในการทบทวนรายเดือนเผยให้เห็นเป็นปริมาณถึงสล็อตสี่สิบตัวและการกระจุกตัวของการใช้งานที่ 92% "มาจำกัดไว้สิบสองตัวกันเถอะ" ก็ถูกตัดสิน หลังการตัดสินนั้นก็ตามมาด้วยคำถาม "แล้วจะเก็บยี่สิบแปดตัวไว้อย่างไร" และคำตอบนั้นคือ Wrapper กับ Junction ในการทบทวนครั้งถัดมาเกิดการค้นพบว่า "การเรียกเครื่องมือตรวจสอบที่คล้ายกันสี่ตัวแยกกันทุกเช้านั้นยุ่งยาก" และคำตอบนั้นคือ Cascade check ในการทบทวนอีกครั้งหนึ่งจับสัญญาณได้ว่า "พอเพิ่มตัวจริงง่ายขึ้น ความซ้ำซ้อนก็สะสม" และคำตอบนั้นคือนโยบาย MECE
ถ้าไม่มีการทบทวน รูปแบบเหล่านี้ก็คงไม่ถูกสร้างขึ้น และต่อให้สร้าง มันก็คงกลายเป็น over-engineering ที่ไม่เกี่ยวกับปัญหาจริง ลำดับที่ค้นพบปัญหาด้วยการวัดก่อนแล้วจึงนำรูปแบบเข้ามา ลำดับนั้นแหละที่ทำให้เครื่องมือถูกใช้จริง ข้อความของส่วนที่ 21 ที่ว่าการทบทวนคือจุดเริ่มต้นของการพัฒนาตนเอง ได้แปรเป็นรูปธรรมในระดับเครื่องมือเช่นนี้
ผมเปรียบเทียบค่าวัดหกเดือนก่อนและหลังนำมาใช้ โดยมุ่งความสนใจไปที่การเปลี่ยนแปลงของภาระการดำเนินงาน ไม่ใช่จำนวนครั้งที่เรียกใช้ในเชิงสัมบูรณ์
| รายการ | ก่อนนำมาใช้ | หลังนำมาใช้ (Wrapper+Cascade+Junction) |
|---|---|---|
| จำนวนสล็อตโกลบอล | 40 ตัว (พุ่งทะลัก) | 12 ตัว (บังคับตามนโยบาย) |
| จำนวนตัวจริง | กระจัดกระจาย·ซ้ำซ้อนมาก | 48 ตัว (จัดด้วย MECE) |
| สัดส่วนสล็อตโกลบอลตอนเริ่มเซสชัน | มาก (โหลดข้อกำหนด 40 ตัว) | น้อย (โหลดเฉพาะข้อกำหนด 12 ตัว) |
| การสะท้อนสู่โกลบอลหลังแก้ตัวจริง | ต้องมีขั้นตอนคัดลอกด้วยมือ | ทันที (Junction, ไม่มีการคัดลอก) |
| การเรียกเครื่องมือตรวจสอบที่คล้ายกัน | 3\~4 ครั้งด้วยมือต่องาน | 1 ครั้ง (check Cascade) |
ค่าวัดของเดือนแรกที่นำมาใช้นั้นกระท่อนกระแท่น ก่อนรูปแบบ Wrapper จะเข้าที่ เกิดอุบัติเหตุการซิงค์สองสามครั้ง และก่อนนโยบายสิบสองตัวจะถูกบังคับ สล็อตก็แกว่งอยู่ระหว่างสิบห้าถึงสิบแปด การเริ่มเสถียรเป็นตั้งแต่เดือนที่สอง หลังจาก sync_skills.py --cleanup เริ่มจัดสล็อตทุกเซสชัน การพุ่งทะลักของสล็อตก็ไม่เกิดขึ้นอีก
การแสดงทิศทางอย่าง "สัดส่วน"·"ต้องมี"·"ทันที" ในตารางนั้นจงใจ เพราะต้นทุนโทเค็นและเวลาต่างกันไปตามสภาพแวดล้อม ผมจึงเขียนแค่ทิศทางของการเปลี่ยนแปลง สิ่งที่แน่ชัดคือชั้นที่ 1 ถูกตรึงจากสี่สิบเหลือสิบสอง และขั้นตอนคัดลอกด้วยมือในการสะท้อนตัวจริงได้หายไป
ผมรวบรวมกับดักที่ชี้ไว้ในหัวข้อก่อน ๆ มาไว้ที่เดียว ทั้งห้าอย่างชี้ไปยังบทเรียนเดียวกันว่าการรักษาโครงสร้างไว้แม้เวลาผ่านไปนั้นยากกว่าการสร้างมันขึ้นมา
sync_skills.py --cleanup จัดทุกเซสชัน (§23.1.4)setup. สร้างไดเรกทอรีตัวจริงหนึ่งตัวใน workspace (เช่น workspace/skills/proj-meeting/) วาง SKILL.md และสคริปต์จริงไว้ข้างใน ส่วนที่โกลบอล ~/.claude/skills/ ให้วางเฉพาะ Wrapper 50 บรรทัด
prompt. ขอสิ่งต่อไปนี้ต่อ Claude
สแกนรายการคำสั่งสแลชใน ~/.claude/skills/ ให้หน่อย
จัดหมวดว่าแต่ละคำสั่งเป็น (a) Wrapper แบบเบาที่มีแค่ตัวชี้ไปยังตัวจริง
หรือ (b) คำสั่งแบบหนักที่มีตรรกะเข้าไปอยู่
แล้วสำหรับตัวที่เข้าข่าย (b) ให้แยกตัวจริงออกไปยัง workspace
และเสนอแผนการเปลี่ยนแปลงที่เหลือไว้ที่โกลบอลแค่ Wrapper 50 บรรทัด
จากนั้นออกคำสั่งเชื่อม Junction ที่ชี้ไปยังตัวจริงให้เหมาะกับ OS
(ถ้าเป็น Windows ใช้ mklink /J ถ้านอกเหนือจากนั้นใช้ ln -s) ให้ด้วย
verify. ตรวจสามอย่าง หนึ่ง แต่ละรายการในไดเรกทอรีโกลบอลอยู่ภายใน 100 บรรทัดหรือไม่ สอง เมื่อเปิดรายการโกลบอลแล้วเห็นแค่ตำแหน่งตัวจริงหนึ่งบรรทัดกับ description หรือไม่ สาม เมื่อแก้ตัวจริงหนึ่งบรรทัดแล้วเรียกใช้จากโกลบอล การแก้สะท้อนทันทีหรือไม่ (ถ้า Junction เชื่อมถูกต้อง การแก้จะสะท้อนโดยไม่มีการคัดลอก)
หากเป็นการดำเนินงานคนเดียวที่ไม่มีทั้งทีมและ SVN ก็ลดทอนแบบนี้ ไม่จำเป็นต้องมี workspace แค่มี Git repository ส่วนตัวหนึ่งก็พอ วางตัวจริงไว้ใน repository นั้น แล้ววางแค่ Wrapper ไว้ที่โกลบอล แม้ไม่มีเครื่องมือวัดอย่าง skill_audit_score เพียงลองจดด้วยมือเองว่า "เดือนนี้เรียกคำสั่งไหนจริง ๆ บ้าง" ตอนสิ้นเดือน การกระจุกตัวก็จะเผยออกมา เก็บไว้ที่โกลบอลแค่ห้าถึงเจ็ดตัวบนสุด ที่เหลือก็ถอดลงเป็นตัวจริง ส่วน Cascade นั้น เมื่อมีเครื่องมือที่มักเรียกร่วมกันสองตัวขึ้นไป ก็มัดไว้ในคำสั่งเดียวก็พอ หากสคริปต์รักษาความสอดคล้องอัตโนมัติเป็นภาระเกินไป ก็แทนได้ด้วยนิสัยกวาดสายตามองไดเรกทอรีโกลบอลหนึ่งรอบตอนเริ่มเซสชันใหม่ เมื่อขนาดเล็กลง นิสัยก็ทำงานเดียวกันแทนการทำงานอัตโนมัติ
ห้าทุ่มสี่สิบเจ็ดนาที ผมบันทึกชีตข้อมูลเป็นครั้งสุดท้ายแล้วปิดโน้ตบุ๊ก เช้าวันรุ่งขึ้นเวลาเก้าโมงสิบนาที ระหว่างที่กำลังชงกาแฟ ผมเปิดแชตภายในทีมขึ้นมา แล้วพบว่ามีรายงานหนึ่งฉบับขึ้นมาอยู่ด้านบนของช่อง เป็นมาร์กดาวน์ที่ตรวจสอบไขว้ชีตปรับสมดุลสามแผ่นซึ่งถูกอัปเดตเมื่อคืนตามเกณฑ์ของ foreign key (FK) แล้วทำเครื่องหมายสีแดงไว้บนการอ้างอิงที่เสียสองรายการ ผมไม่ได้เขียนมันขึ้นมา มันถูกสร้างขึ้นในขณะที่ผมหลับ
บทนี้คือบันทึกของกระบวนการที่ผมนำเครื่องมือที่สร้างรายงานฉบับนั้น — Hermes Agent — มาติดตั้งบน PC ส่วนตัว แล้ววางทับการดำเนินงานแบบ Wrapper · Cascade · Junction ที่กล่าวถึงใน §23.1 ตอนเริ่มนำมาใช้ใหม่ ๆ Hermes ยังอิงบน Linux ดังนั้นถ้าจะใช้บน Windows ก็ต้องผ่าน WSL2 แต่ในปี 2026 มี build แบบเนทีฟสำหรับ Windows ออกมา ทำให้ทางอ้อมนั้นหายไป ขอสรุปไว้ก่อนเลยว่า เอเจนต์ไม่ได้เข้ามาแทนที่ Claude Code มันมานั่งข้าง ๆ ต่างหาก
การดำเนินงานจนถึง §23.1 ทั้งหมดมีศูนย์กลางอยู่ที่ Claude Code เมื่อผมป้อนหนึ่งประโยค เครื่องมือก็ตอบกลับหนึ่งครั้ง แล้วผมก็ตรวจคำตอบนั้นก่อนป้อนประโยคถัดไป วงจรสั้น ๆ นี้ดีเยี่ยมสำหรับงานที่ต้องการความแม่นยำ ถ้าเป็นงานแก้ตัวเลขปรับสมดุลตัวเดียวที่ต้องตรวจทุกขั้นตอน การให้คนเข้ามาแทรกทุกครั้งจึงเป็นเรื่องที่ถูกต้อง
ปัญหาอยู่ที่งานที่ใช้เวลายาว คำขออย่าง "อ่านบันทึกการประชุมหนึ่งเดือนที่ผ่านมาทั้ง 30 ฉบับให้หมด แล้วดึงเฉพาะข้อตัดสินใจออกมาเป็นตัวเลือก atom" หากจัดการอยู่ในกระแสบทสนทนา จะต้องไป-กลับถึง 30 ครั้ง และตลอด 30 ครั้งนั้นผมก็ทำอย่างอื่นไม่ได้ สำหรับงานแบบนี้ จุดแข็งของเครื่องมือที่อินพุตและเอาต์พุตติดกันแบบสั้น ๆ กลับกลายเป็นจุดอ่อน
เอเจนต์เข้ามาเติมในตำแหน่งตรงข้ามนั้น เพียงโยนเป้าหมายให้ — "ดึงข้อตัดสินใจจากบันทึกการประชุม 30 ฉบับมาเป็นตัวเลือก atom แล้วทำเป็นรายงาน" — มันก็เลือกเครื่องมือมาใช้เอง เดินผ่านขั้นตอนกลางด้วยตัวเอง และเมื่อเสร็จก็นำเฉพาะผลลัพธ์มาให้ วงจรของมันยาวและเป็นอิสระ แต่ก็มาพร้อมจุดอ่อนที่ว่าคนมองไม่เห็นทุกขั้นตอน
ลองเปรียบกับเพื่อนร่วมงานที่นั่งข้าง ๆ จะเข้าใจง่ายขึ้น Claude Code คือเพื่อนคู่หูที่มองหน้าจอไปพร้อมกับผมในทุกประโยค ส่วนเอเจนต์คือผู้ช่วยที่อาสาทำงานกะกลางคืน แล้ววางรายงานไว้บนโต๊ะก่อนผมจะเข้างาน ทั้งสองไม่ได้อยู่ในความสัมพันธ์ที่ฝ่ายหนึ่งไล่อีกฝ่ายออก ทั้งคู่แบ่งกันใช้โต๊ะตัวเดียวกัน
ใน §23.1 ผมรวบช่องคำสั่งสแลชระดับโกลบอลไว้ที่ 12 ช่อง แล้วซ่อนตัวจริงอีก 48 ตัวไว้ด้านหลังด้วย Junction บทสรุปคือ ไม่ได้เพิ่มจำนวนเครื่องมือ แต่สร้างเครื่องมือของเครื่องมือขึ้นมา ทีนี้การจะนำเครื่องมือใหม่อีกตัวเข้ามาตรงนี้ฟังดูเหมือนขัดแย้งกับบทสรุปนั้น
ไม่ขัดแย้ง นโยบาย 12 ช่องของ §23.1 จัดการกับภาระทางการรับรู้ของ "เครื่องมือที่คนเรียกใช้โดยตรง" ส่วนตำแหน่งที่ Hermes จะเข้ามาเติมคือช่วงเวลาที่คนไม่ได้เรียกใช้ — เวลาที่หลับ เวลาที่ประชุม เวลาที่มือติดงานอื่นอยู่ มันไม่ได้แข่งกับ 12 ช่อง แต่เติมช่วงเวลาที่ 12 ช่องเอื้อมไม่ถึง
หลักฐานในการตัดสินใจนำมาใช้คือค่าวัดจากการทบทวนหนึ่งค่า ผมรันสคริปต์ skill_audit_score ที่ย้อนคำนวณความถี่การใช้เครื่องมือระดับโกลบอลจาก SVN commit log ตลอดหนึ่งเดือน แล้วพบว่าเครื่องมืออันดับต้น ๆ ส่วนใหญ่เป็นชนิดที่ใช้ "ในเวลาที่คนตื่นอยู่ สั้น ๆ และบ่อย ๆ" ในทางตรงข้าม งานที่ความถี่การใช้ต่ำแต่พอรันทีหนึ่งกินเวลานาน — การจัดประเภทบันทึกการประชุมเป็นชุด การตรวจความสอดคล้องของชีตข้อมูลในเวลากลางคืน การวิเคราะห์ build capture — กลับถูกผัดผ่อนทุกครั้งด้วยคำว่า "ค่อยทำพรุ่งนี้เช้า" เหตุผลของการผัดผ่อนนั้นชัดเจน เพราะมันกินเวลาตอนที่ตื่นอยู่ไปยาว ๆ
กลุ่มงานที่ถูกผัดผ่อนนี้แหละคือเป้าหมายอันแม่นยำของเอเจนต์
ตอนติดตั้ง Hermes ครั้งแรก เพราะมันอิงบน Linux การจะใช้บน PC ส่วนตัวที่เป็น Windows จึงต้องลง WSL2 (Windows Subsystem for Linux 2) ก่อน แล้วค่อยเอา Hermes ไปวางในนั้น ตอนนี้มี build แบบเนทีฟสำหรับ Windows แล้ว จึงไม่ต้องอ้อมแบบนั้นอีก การติดตั้งเหมือนกับแอปพลิเคชัน Windows ทั่วไป — ดาวน์โหลด installer มารัน แล้วในการรันครั้งแรกก็ตั้งค่าเริ่มต้นของพาธพื้นที่ทำงานและ whitelist สิทธิ์
ถ้าใช้ WSL2 อยู่แล้วหรือชอบสภาพแวดล้อม Linux build ฝั่งนั้นก็ยังรองรับอยู่เหมือนเดิม แต่ถ้าเพิ่งเริ่มต้น ฝั่งเนทีฟจะเรียบง่ายกว่า เนื่องจาก installer และเวอร์ชันที่แน่นอนของเครื่องมือเปลี่ยนเร็ว จึงให้ยึดตามเอกสารทางการ
มีกับดักหนึ่งที่ยังคงอยู่ไม่ว่าจะติดตั้งที่ไหน นั่นคือพื้นที่ทำงานของ Hermes ต้องวางบนดิสก์ภายในที่เร็ว ถ้าผูกไดรฟ์เครือข่ายหรือโฟลเดอร์งาน SVN เป็นพื้นที่ทำงานโดยตรง การตรวจความสอดคล้องในเวลากลางคืนหนึ่งรอบที่ควรใช้เวลาไม่กี่นาที จะยืดเป็นหลายสิบนาที วิธีที่ถูกต้องคือวางชีตข้อมูลไว้นอกพื้นที่ทำงาน แล้วคัดลอกเข้ามาเฉพาะตอนเริ่มงานเท่านั้น หากใช้ WSL2 ก็ด้วยเหตุผลเดียวกัน ให้วางพื้นที่ทำงานไว้ในไฟล์ซิสเต็มของ Linux และไม่เดินข้ามไปพาธ Windows อย่าง /mnt/c
ตั้งแต่ตรงนี้คือส่วนที่ต้องลงมือทำจริง ๆ เพราะหัวใจของบทไม่ใช่การติดตั้งเอง แต่เป็น "หลังติดตั้งแล้วจะให้มันทำอะไร" ผมจึงจะพางานชิ้นแรกหนึ่งงานไปจนจบ — ตั้งแต่พรอมต์ฉบับเต็ม ผลลัพธ์ดิบ การตรวจสอบโดยมนุษย์ ไปจนถึงการขอใหม่ งานนี้ผมเลือกชิ้นที่ง่ายที่สุดในกลุ่มงานที่ถูกผัดผ่อนใน §23.2.2 นั่นคือการตรวจความสอดคล้องของชีตข้อมูลในเวลากลางคืน
หมายเหตุ: คำสั่งบางส่วนด้านล่างเป็นรูปแบบตัวอย่างเพื่อแสดงหน้าตาภายนอกของ Hermes URL ของ installer และคำสั่งย่อยเปลี่ยนไปตามเวอร์ชัน จึงให้ตรวจสอบกับเอกสารทางการ ส่วนโครงสร้างของเวิร์กโฟลว์ (เป้าหมาย → รันเอง → ตรวจสอบ → ขอใหม่) ยังคงเดิมแม้เครื่องมือจะเปลี่ยน
ถ้าเป็นเนทีฟ Windows ก็ดาวน์โหลด install.ps1 ทางการมารันใน PowerShell แต่ก่อนจะรันคำสั่งบรรทัดเดียว iex (irm ...) ตรง ๆ ให้ดาวน์โหลดสคริปต์ (ประมาณ 2,800 บรรทัด) มาก่อนแล้วกวาดสายตาดูรูปแบบที่เสี่ยงด้วยตาตัวเอง — นั่นคือขั้นตอนขั้นต่ำในการเชื่อถือแหล่งที่มา — แล้วแยกการตั้งค่าคีย์ออกมาต่างหาก โดยลงเฉพาะตัวหลักก่อนด้วย -SkipSetup แล้วค่อยรัน hermes setup แยกต่างหาก วิธีนี้ปลอดภัยกว่า ถ้าใช้ WSL2 หรือ Linux ก็ให้ทำตามหัวข้อการติดตั้งที่เกี่ยวข้องในเอกสารทางการ
# เนทีฟ Windows — install.ps1 ทางการ (ดาวน์โหลดมาตรวจก่อนแล้วค่อยรัน)
irm https://hermes-agent.nousresearch.com/install.ps1 -OutFile install.ps1
# (หลังจากตรวจเนื้อหา install.ps1 แล้ว)
.\install.ps1 -SkipSetup
# จัดเตรียม Python 3.11 · Node · Git · Playwright · สกิลที่ bundle มาด้วยกัน
# ตำแหน่งติดตั้ง: %LOCALAPPDATA%\hermes\ (ลงทะเบียนคำสั่ง hermes ไว้ใน PATH — รู้จักตั้งแต่ terminal ใหม่)
# เมื่อเสร็จ: hermes setup
installer จะลง dependency (Python 3.11 · Node 22 · Git) ไปพร้อมกัน แล้วติดตั้งตัวหลักไว้ที่ %LOCALAPPDATA%\hermes\ จากนั้นลงทะเบียนคำสั่ง hermes ใน PATH (รู้จักตั้งแต่ terminal ใหม่) ข้อมูลการดำเนินงานอย่างการตั้งค่า · ล็อก · งานตั้งเวลา (cron) · เช็กพอยต์ ก็จะอยู่ใต้ %LOCALAPPDATA%\hermes\ เดียวกัน จึงคงอยู่แม้ติดตั้งใหม่ (ตรงนี้เป็นกับดัก — ใน ~/.hermes\ มีแค่สคริปต์เสริมอยู่ จึงสับสนได้ง่าย ตัว config.yaml · logs\ จริง ๆ อยู่ฝั่ง %LOCALAPPDATA%\hermes\ ทั้งหมด) เมื่อรัน hermes setup เป็นการรันครั้งแรก มันจะถามคีย์ API ของโมเดล แล้วตั้งค่าเริ่มต้นของพาธพื้นที่ทำงานและ whitelist สิทธิ์
hermes --version
hermes setup
ทีนี้ก็มอบงานชิ้นแรกให้ เป้าหมายที่โยนให้เอเจนต์จะเป็นนามธรรมขึ้นมาอีกขั้นเมื่อเทียบกับพรอมต์ของ Claude Code มันไม่ใช่ "ทำสิ่งนี้แบบนี้ให้หน่อย" แต่ใกล้กับ "สร้างผลลัพธ์นี้ขึ้นมาไว้" มากกว่า เป้าหมายฉบับเต็มที่ผมป้อนเข้าไปจริง ๆ มีดังนี้
[พรอมต์ฉบับเต็ม]
เป้าหมาย: ตรวจความสอดคล้องของชีตข้อมูลในเวลากลางคืน
อ่าน xlsx สามไฟล์ item_master·drop_table·npc_shop ใน ~/hermes-workspace/sheets/
แล้วตรวจ foreign key ว่า item_id ที่ drop_table·npc_shop อ้างถึงนั้น มีอยู่จริง
ใน item_master หรือไม่ ค้นหาการอ้างอิงที่เสีย (แถวที่ชี้ไปยัง item_id ที่ไม่มีอยู่)
ให้หมด แล้วบันทึกเป็นมาร์กดาวน์ที่ตั้งชื่อไฟล์ด้วยวันที่ไว้ใน reports/
(เจตนา: ที่ผ่านให้บอกแค่จำนวน ที่ไม่ผ่านให้ทำเป็นตารางชื่อชีต·เลขแถว·item_id)
ข้อจำกัด:
- sheets/ ให้อ่านอย่างเดียว ห้ามแก้ไขเด็ดขาด
- ถ้าอ่านชีตไม่ได้หรือชื่อคอลัมน์ต่างจากที่คาด ให้หยุดแล้วรายงานว่าต่างตรงไหน ห้ามเดา
สองบรรทัดสุดท้ายสำคัญ เอเจนต์เป็นอิสระ พอติดขัดก็มีแนวโน้มจะ "เติมเอง" เช่น พอชื่อคอลัมน์ต่าง ก็เลือกชื่อที่คล้ายกันมาแล้วทำต่อไป ผมจึงป้อน "ถ้าติดขัดให้หยุดแล้วรายงาน ห้ามเดา" ไว้อย่างชัดเจน อันนี้เป็นแนวเดียวกับบทเรียนที่ได้ตอนเขียน Cascade ตรวจสอบชีตข้อมูลใน §23.1
[ผลลัพธ์ดิบของ Hermes — รอบที่ 1]
ขอยกมาตามจริงโดยไม่ย่อ ความพยายามครั้งแรกไม่ได้จบลงอย่างราบรื่น
[hermes] goal accepted: ตรวจความสอดคล้องของชีตข้อมูลในเวลากลางคืน
[hermes] step 1/4 reading sheets...
item_master.xlsx loaded (4,210 rows)
drop_table.xlsx loaded (1,880 rows)
npc_shop.xlsx ERROR
[hermes] npc_shop.xlsx: expected column 'item_id', found columns:
['shop_id', 'sell_item', 'price', 'stock']
[hermes] this conflicts with the constraint: "ถ้าชื่อคอลัมน์ต่างจากที่คาด ให้หยุดแล้วรายงาน".
[hermes] HALTING. reporting instead of guessing.
[hermes] partial result:
- การตรวจ item_master ↔ drop_table ทำได้ (มีคอลัมน์ item_id ทั้งสองฝั่ง)
- npc_shop คาดว่าชื่อคอลัมน์ item_id คือ 'sell_item' แต่ยืนยันไม่ได้ จึงยังไม่รัน
[hermes] awaiting clarification on npc_shop column mapping.
ตรงนี้คือจุดที่คนเพิ่งใช้เอเจนต์ครั้งแรกประหลาดใจที่สุด เครื่องมือหยุด และบอกเหตุผลที่หยุดอย่างแม่นยำ คอลัมน์อ้างอิงไอเทมของ npc_shop.xlsx ไม่ใช่ item_id ตามที่ผมคาด แต่เป็น sell_item ถ้าไม่ได้ป้อนข้อจำกัดไว้ เอเจนต์คงถือเอาเองว่า sell_item คือ item_id แล้วทำต่อไป และตอนเช้าผมก็คงได้รับแต่รายงานว่า "ปกติ" เท่านั้น การหยุดก็คือความปลอดภัย
[การตรวจสอบโดยมนุษย์ · การปฏิเสธ]
ผมดูผลลัพธ์แล้วตัดสินสองอย่าง
อย่างแรก ส่วนที่รายงานว่าสามารถตรวจ item_master ↔ drop_table ได้แล้วนั้นเชื่อถือได้ อย่างที่สอง การคาดว่า sell_item ของ npc_shop ถูกต้อง — มันเป็นชื่อคอลัมน์ไอเทมที่ขายของชีตนั้นจริง ๆ แต่การตัดสินใจที่จะไม่ทำต่อด้วย "การคาดเดา" นั้นถูกต้อง ผมจึงไม่รับผลลัพธ์รอบที่ 1 มาทั้งดุ้น (ปฏิเสธ) แล้วป้อนคำขอใหม่ที่ระบุการจับคู่คอลัมน์ไว้ชัดเจน
[พรอมต์ขอใหม่]
ยืนยันแล้ว ขอระบุการจับคู่คอลัมน์:
- คอลัมน์อ้างอิงไอเทมของ npc_shop.xlsx คือ 'sell_item' ให้ถือว่าเป็น item_id แล้วตรวจ
- ข้อจำกัดที่เหลือเหมือนเดิม อย่าเดาคอลัมน์อื่นนอกจาก 'sell_item' เพิ่ม
รันใหม่
[ผลลัพธ์ดิบของ Hermes — รอบที่ 2]
[hermes] resuming with column map: npc_shop.sell_item -> item_id
[hermes] step 2/4 foreign-key check...
drop_table → item_master : 1,880 refs, 2 broken
npc_shop → item_master : 642 refs, 0 broken
[hermes] step 3/4 collecting broken refs...
[hermes] step 4/4 writing report...
~/hermes-workspace/reports/2026-05-25_sheet_integrity.md written
[hermes] done. 2 broken references found. summary:
| sheet | row | bad item_id |
|------------|------|-------------|
| drop_table | 417 | ITM_90412 |
| drop_table | 1356 | ITM_90727 |
จับการอ้างอิงที่เสียได้สองรายการ แถวที่ 417 และ 1356 ของ drop_table ชี้ไปยัง item_id ที่ไม่มีอยู่ใน item_master นี่แหละคือตารางสีแดงที่ผมเห็นในตอนเช้า
ในการไป-กลับเพียงครั้งเดียวนี้ สารของบทแทบจะเผยออกมาหมดแล้ว เอเจนต์เป็นอิสระแต่หยุดเมื่อเจอข้อจำกัด และเมื่อคนเข้าไปเติมจุดที่หยุด มันก็เดินไปจนจบ ความเป็นอิสระกับการควบคุมไม่ได้ปะทะกัน แต่ประสานกัน และถ้าตั้งเวลาให้วงจรทั้งหมดนี้รันอีกครั้งในเวลาที่ผมหลับ นั่นแหละคือระบบอัตโนมัติยามค่ำคืนของ §23.2.5
เมื่องานชิ้นแรกเข้ามือ ก็ค่อย ๆ ส่งกลุ่มงานที่ถูกผัดผ่อนไปยังเวลากลางคืนทีละชิ้น ที่ผมวางไว้จริงมีสามตำแหน่ง จุดร่วมของทั้งสามชัดเจน — ทั้งหมดเปลี่ยนเวลาที่คนไม่จำเป็นต้องตื่นอยู่ ให้เป็นเวลาทำงาน
flowchart TD
A["ทริกเกอร์ยามค่ำคืน
(ทุกวัน 23:00, cron)"] --> B{Hermes Agent}
B --> C1["[ตำแหน่ง 1] ชีตข้อมูล
ตรวจความสอดคล้องยามค่ำคืน"]
B --> C2["[ตำแหน่ง 2] การจำลองระยะยาว
เล่นเสมือนจริง 100 ชั่วโมง"]
B --> C3["[ตำแหน่ง 3] build capture
ไปป์ไลน์วิเคราะห์อัตโนมัติ"]
C1 --> D1["diff ของ foreign key
ตารางการอ้างอิงที่เสีย"]
C2 --> D2["เวลาเฉลี่ยฆ่าบอส·การใช้ทรัพยากร
การกระจายคอมโบ"]
C3 --> D3["diff สเปกเทียบกับค่าที่วัดได้
สกัดทีละเฟรม"]
D1 --> R["[สรุปรวม] รายงานมาร์กดาวน์
~/hermes-workspace/reports/"]
D2 --> R
D3 --> R
R --> S["เช้า 09:00
กระจายอัตโนมัติเข้าช่องแชตทีม"]
S --> H["นักออกแบบเกม: ตรวจแค่ผลลัพธ์
(วิเคราะห์เสร็จขณะที่หลับ)"]
style A fill:#fff3e0,stroke:#e65100
style B fill:#e3f2fd,stroke:#1565c0
style R fill:#e8f5e9,stroke:#2e7d32
style H fill:#fce4ec,stroke:#c2185b
ตำแหน่ง 1 — ตรวจความสอดคล้องของชีตข้อมูลยามค่ำคืน ตั้งเวลางานที่พาไปจนจบใน 2.4 ไว้ที่ห้าทุ่มทุกคืน ไม่ว่าเมื่อคืนใครจะไปแตะชีตไหน พอถึงเช้าจุดที่ foreign key เสียก็จะขึ้นมาเป็นตาราง อันนี้ดูผิวเผินคล้ายกับงานที่ Cascade /check ของ §23.1 (รวมตรวจ 4 ชนิด doc-audit → data-qa → integrity → link-check) เคยทำ แต่มีความต่างชี้ขาดหนึ่งอย่าง /check รันได้ก็ต่อเมื่อผมตื่นและเรียกใช้ ส่วนเอเจนต์ยามค่ำคืนรันได้แม้ผมไม่อยู่ ทั้งสองไม่ได้แข่งกัน — Cascade ตอนกลางวันคือการตรวจสอบทันที เอเจนต์ตอนกลางคืนคือการตรวจสอบแบบไร้คนเฝ้า บทบาทจึงแยกกัน
ตำแหน่ง 2 — การจำลองระยะยาว ขยายการจำลองการต่อสู้ที่กล่าวถึงใน §4.4 ให้ลึกลงไปตามแกนเวลา เป็นงานรันการเล่นเสมือนจริง 100 ชั่วโมง เพื่อวัดเวลาเฉลี่ยในการฆ่าบอส เส้นโค้งการใช้ทรัพยากร และการกระจายคอมโบ โดยเนื้อแท้แล้วงานนี้ไม่เข้ากับกระแสบทสนทนาของ Claude Code — รันทีหนึ่งใช้เวลาหลายชั่วโมง และตลอดเวลานั้นจะมานั่งจับหน้าต่างแชตไว้ไม่ได้ เอเจนต์รันอยู่เบื้องหลัง แล้วเมื่อเสร็จก็นำมาเฉพาะกราฟเส้นโค้งและตัวเลขสรุป
ตำแหน่ง 3 — วิเคราะห์ build capture อัตโนมัติ เมื่อวิดีโอ build ที่ QA แคปเจอร์ไว้ตกลงมาในโฟลเดอร์ เอเจนต์จะสกัดข้อมูลทีละเฟรมแล้วสร้าง diff ระหว่างค่าตามสเปกกับค่าที่วัดได้จริง นักออกแบบเกมไม่ต้องไล่ดูวิดีโอตั้งแต่ต้นจนจบ แค่ดูบรรทัด diff อย่าง "สเปกระบุดาเมจ 120 แต่ build วัดได้ 108" ก็พอ ส่วนที่น่าเบื่อทั้งหมดของการวิเคราะห์เป็นหน้าที่ของเอเจนต์
ทั้งสามตำแหน่ง เวลาของคนที่ดูผลลัพธ์ไม่ได้ลดลง ที่ลดลงคือเวลาของคนที่ใช้ไปกับการวิเคราะห์ การตัดสินใจยังคงเป็นของคนอยู่เช่นเดิม
ความเป็นอิสระของเอเจนต์ก็คือความเสี่ยงในตัวมันเอง ถ้าเป็นเครื่องมือที่อ่านไฟล์และรันคำสั่งโดยไม่มีคนตรวจทุกขั้นตอน เมื่อมันคลี่คลายผิดทาง คนก็ไม่อยู่ตรงนั้น การที่ผมระบุ "ห้ามเดา" ไว้ใน §23.2.4 จึงไม่ใช่เรื่องบังเอิญ กลไกความปลอดภัยห้าอย่างไม่ใช่ทางเลือก แต่เป็นชุดที่ต้องเปิดพร้อมกันตั้งแต่วันแรกที่นำมาใช้
| กลไก | ทำอะไร (คีย์ตั้งค่า Hermes จริง) | ถ้าขาดไปจะเกิดอะไร |
|---|---|---|
| whitelist สิทธิ์ | คำสั่งทำลายล้างต้องผ่านการอนุมัติจากคน (approvals.mode: manual) อนุญาตเฉพาะคำสั่งใน whitelist (command_allowlist) ค่าลับถูกปิดบังในล็อก (security.redact_secrets) |
แก้ไขชีตข้อมูลต้นฉบับเองโดยอัตโนมัติ |
| เช็กพอยต์ | snapshot ก่อนทำงานกับไฟล์เพื่อให้ย้อนกลับได้ (checkpoints.enabled กู้คืนด้วย /rollback) |
สมมติฐานที่ผิดกลิ้งไปจนจบ ผลลัพธ์ทั้งหมดปนเปื้อน |
| บันทึกล็อกอัตโนมัติ | เก็บล็อก gateway · agent · error ไว้ที่ %LOCALAPPDATA%\hermes\logs\ |
หลังเกิดเหตุ ตามรอย "ทำไมถึงเป็นแบบนี้" ไม่ได้ |
| เพดานค่าใช้จ่าย | เพดานเทิร์นต่อหนึ่งงาน (agent.max_turns) · timeout ของ terminal (terminal.timeout) · ตรวจจับลูปไม่รู้จบอัตโนมัติ (tool_loop_guardrails) · บีบอัดบริบทอัตโนมัติ (compression) |
งานที่ติดลูปไม่รู้จบทำให้บิล API พุ่ง |
| ยกเลิกได้ | หยุดได้ทุกเมื่อ (/stop) · พัก/ลบงานตั้งเวลา (cron pause) · timeout ของงานย่อย (delegation.child_timeout_seconds) · เก็บสกิลที่ไม่ใช้เข้าคลังอัตโนมัติ (curator) |
หยุดงานยามค่ำคืนที่เริ่มรันผิดทางไม่ได้ |
ห้าอย่างนี้ไม่ใช่กลไกที่ต่างคนต่างทำงาน แต่ทำงานเป็นชุดเดียว ถ้าล็อกแค่สิทธิ์แต่ไม่ตั้งเพดานค่าใช้จ่าย ลูปไม่รู้จบก็จะวนอยู่ในขอบเขตสิทธิ์แล้วทำให้บิลพอง ถ้าเปิดแค่ล็อกแต่ไม่มีวิธียกเลิก ก็จะได้แต่มองเหตุที่เกิดโดยหยุดไม่ได้ ขาดอย่างใดอย่างหนึ่งไป โอกาสเกิดเหตุของการดำเนินงานไร้คนเฝ้ายามค่ำคืนก็พุ่งขึ้นทันที
พอเปิดเครื่องมือจริง ๆ มีอยู่หลายจุดที่เครื่องมือ implement ห้าแนวคิดนี้ละเอียดกว่าที่วาดไว้ในหนังสือไปอีกขั้น ฝั่งสิทธิ์มี policy engine แยกอีกชั้น (security.tirith_enabled) ที่กรองคำสั่งด้วยกฎ ฝั่งค่าใช้จ่าย การตรวจจับลูปไม่รู้จบก็ไม่ใช่เพดานเดียว แต่จับสัญญาณอย่าง "ความล้มเหลวเดิมซ้ำ" · "ทำซ้ำโดยไม่คืบหน้า" เป็นเกณฑ์แยกต่างหาก และในงานตั้งเวลาไร้คนเฝ้ายามค่ำคืน (cron) มีสวิตช์แยก (approvals.cron_mode: deny) เมื่อเจอคำสั่งทำลายล้างในช่วงที่ไม่มีคน มันจะไม่รอการอนุมัติแต่ปฏิเสธทันที — เท่ากับรวบ "สิทธิ์ + เช็กพอยต์" ของหนังสือไว้ในการตั้งค่าเดียว ส่วน curator ฝั่งยกเลิกได้คือจุดที่ "เครื่องมือที่ไม่ใช้ก็เก็บทิ้ง" ของ §21 ถูกบรรจุเป็นฟังก์ชันจริง โครงของชุดห้าอย่างคงไว้เหมือนเดิม แต่จุดที่เครื่องมือทำได้ละเอียดกว่า ก็เปิดคีย์นั้นไว้ก็พอ
หน้าตาของการป้อนชุดนี้ลงใน config.yaml คร่าว ๆ เป็นดังนี้
# %LOCALAPPDATA%\hermes\config.yaml (ตัดมาบางส่วน)
approvals:
mode: manual # ① สิทธิ์ — คำสั่งทำลายล้างต้องผ่านการอนุมัติจากคน
command_allowlist: # ระบุเฉพาะคำสั่งที่อนุญาตให้ทำได้โดยไม่ต้องอนุมัติ
- "python *"
- "rg *"
cron_mode: deny # cron ไร้คนเฝ้ายามค่ำคืนเจอคำสั่งทำลายล้างให้ปฏิเสธอัตโนมัติ
security:
redact_secrets: true # ปิดบังค่าลับในล็อก
tirith_enabled: true # policy engine (กรองคำสั่งตามกฎ) อีกหนึ่งชั้น
checkpoints:
enabled: true # ② เช็กพอยต์ — snapshot ก่อนทำงานกับไฟล์ (กู้คืนด้วย /rollback)
max_snapshots: 20
retention: 7d
logs:
path: "%LOCALAPPDATA%\\hermes\\logs" # ③ ล็อก — gateway/agent/errors
agent:
max_turns: 60 # ④ ค่าใช้จ่าย — เพดานเทิร์นต่อหนึ่งงาน
terminal:
timeout: 180 # timeout ของคำสั่ง terminal (วินาที)
tool_loop_guardrails: # ตรวจจับลูปไม่รู้จบอัตโนมัติ (ล้มเหลวเดิมซ้ำ·ไม่คืบหน้า)
enabled: true
compression:
enabled: true # บีบอัดบริบทอัตโนมัติ (ประหยัดโทเค็น)
delegation:
child_timeout_seconds: 600 # ⑤ ยกเลิกได้ — timeout ของงานย่อย (ใช้คู่กับ /stop·cron pause)
curator:
enabled: true # เก็บสกิลที่ไม่ใช้เข้าคลังอัตโนมัติ
การมอบหมายงานก็ไม่ได้ส่งให้หมดในครั้งเดียว เริ่มแรกมอบเฉพาะงานที่แคบที่สุดและย้อนกลับง่ายที่สุด (งานอ่านอย่างเดียวอย่างการตรวจความสอดคล้อง) แล้วเฝ้าดูผลสองสามวันก่อนจะขยายไปยังตำแหน่งถัดไป การที่เลือกการตรวจความสอดคล้องยามค่ำคืนเป็นงานชิ้นแรกใน §23.2.4 ก็ด้วยเหตุผลเดียวกัน — เพราะอ่านอย่างเดียว ต่อให้เลวร้ายที่สุดก็จบแค่รายงานที่ผิดหนึ่งฉบับ ต้นฉบับไม่บอบช้ำ
ณ ช่วงที่อัปเดตบทนี้ การนำมาใช้เข้าสู่ระยะตั้งหลักแล้ว ติดตั้ง build แบบเนทีฟสำหรับ Windows (v0.16.0) เสร็จ และลงทะเบียนคีย์ API ของโมเดลด้วย hermes setup เรียบร้อย ผมเปิดใช้ตำแหน่งแรกแล้วตรวจกลไกความปลอดภัยทั้ง 5 อย่างทีละอย่างด้วยคีย์ตั้งค่าจริง ตอนนี้กำลังรันงานอัตโนมัติจริงเพื่อให้เข้ามือ พูดตามตรง ผมยังตรวจสอบอยู่บน PC ส่วนตัวก่อน ไม่ใช่ PC ของบริษัท — การนำเข้าบริษัทผัดผ่อนไว้จนกว่ากลไกความปลอดภัยจะเข้ามือเพียงพอบน PC ส่วนตัว นี่ไม่ใช่ความรอบคอบเสียทีเดียว แต่ใกล้กับหลักการแยก PC มากกว่า ผมจะไม่ปล่อยเครื่องมืออัตโนมัติที่ยังไม่ผ่านการตรวจสอบลงไปบนข้อมูลทีมทันที
| ช่วงเวลา | กิจกรรม | เกต |
|---|---|---|
| 1 เดือน | ติดตั้ง Hermes (เนทีฟ Windows v0.16.0) + hermes setup + งานชิ้นแรก |
กลไกความปลอดภัยทั้ง 5 อย่างเปิดครบหรือยัง |
| 2\~3 เดือน | ขยายเป็น 2\~3 ตำแหน่ง (จัดประเภทบันทึกการประชุม·วิเคราะห์ build capture) | ตรวจล็อกในทุกขอบเขตการมอบหมาย |
| 3\~6 เดือน | บริษัทพิจารณา — ตัดสินใจจากผลการตรวจสอบบน PC ส่วนตัว | ยืนยันว่าการดำเนินงานไร้คนเฝ้าเกิดเหตุ 0 ครั้ง |
| 6\~12 เดือน | นำมาใช้ระดับทีม | กลไกความปลอดภัยตั้งหลักเป็นกติกาของทีม |
การเย้ายวนให้ข้ามขั้นตอนคืออันตรายที่สุด ถ้าจาก 1 เดือนกระโดดไป 6 เดือน (นำเข้าทีม) เลย กลไกความปลอดภัยจะยังเป็นแค่นิสัยของคนคนเดียว ยังไม่เข้ามือเป็นกติกาของทีม แล้วถูกปล่อยออกไป คำตอบคือหยุดสักครั้งที่ปลายของแต่ละขั้นเพื่อตรวจกลไกทั้งห้า การไปแบบที่ย้อนกลับได้สำคัญกว่าการไปเร็ว
"เอเจนต์มาแทนที่คน" คือความเข้าใจผิดที่พบบ่อยที่สุด บันทึกเซสชันจริงใน §23.2.4 แสดงสิ่งที่ตรงข้าม — เอเจนต์หยุดที่การจับคู่คอลัมน์หนึ่งจุด และคนเข้าไปเติมการตัดสินใจนั้น การตัดสินใจหลักของการออกแบบเกมยังคงเป็นหน้าที่ของคน ส่วนที่เอเจนต์รับไปคือส่วนที่น่าเบื่อของการทำซ้ำและการวิเคราะห์
ความคาดหวังว่า "ติดตั้งครั้งเดียวแล้วอัตโนมัติหมด" ก็อันตราย หนึ่งถึงสองเดือนแรกกลับต้องลงมือมากกว่าเดิม ต้องปรับการจับคู่ชื่อคอลัมน์ ขอบเขตสิทธิ์ และเพดานค่าใช้จ่ายในทุกงาน และจนกว่าการปรับนั้นจะเข้ามือ คนก็ต้องตรวจทุกผลลัพธ์
การฟันธงว่า "Claude Code ตอนนี้ตกยุคแล้ว" นั้นผิด ทั้งสองอยู่คนละช่วงเวลา การตัดสินใจที่แม่นยำตอนกลางวันใช้ Claude Code การทำซ้ำไร้คนเฝ้าตอนกลางคืนใช้เอเจนต์ Cascade /check ของ §23.1 ไม่ได้หายไป แต่มีเลนยามค่ำคืนเพิ่มขึ้นมาอีกหนึ่งเลนข้าง ๆ
ความเข้าใจว่า "เป็นโอเพนซอร์สก็เลยฟรี" ถูกแค่ครึ่งเดียว ตัวหลักฟรีก็จริง แต่ค่าเรียก API ของโมเดลยังต้องจ่ายเหมือนเดิม ด้วยเหตุนี้ เพดานค่าใช้จ่ายอย่าง agent.max_turns · compression ใน config.yaml จึงเป็นทั้งกลไกความปลอดภัยและสมุดบัญชีรายจ่าย
สุดท้าย ความคาดหวังว่า "เอเจนต์ทำงานที่ซับซ้อนและอันตรายได้ด้วย" อันตรายที่สุด ยิ่งงานเสี่ยงมากเท่าไรยิ่งต้องอยู่ใต้การควบคุมของคน สิ่งที่ส่งให้เอเจนต์เริ่มจากงานที่เรียบง่ายและย้อนกลับง่ายก่อน ขยายการมอบหมายเท่าที่ความเชื่อใจสั่งสมไว้เท่านั้น
ถ้า Wrapper · Cascade · Junction ของ §23.1 คือจุดสูงสุดของการดำเนินงาน Claude Code แล้ว Hermes ของบทนี้ก็คือการปูเลนยามค่ำคืนเพิ่มอีกหนึ่งเลนทับลงบนการดำเนินงานนั้น ภาพที่เครื่องมือกลางวันและเครื่องมือกลางคืนแบ่งกันใช้โต๊ะตัวเดียวกัน — นี่คือทั้งปัจจุบัน ณ ปี 2026 และโครงของอนาคตอันใกล้
บทถัดไปคือการคัดสรรเครื่องมือสำหรับนักออกแบบเกม จะใส่อะไรลงใน 12 ช่อง จะใช้ skill_audit_score คัดอะไรออก — เกณฑ์การคัดสรรที่ผ่านตาไปแวบหนึ่งในบทนี้ จะถูกคลี่ออกมาเป็นคำแนะนำเครื่องมือที่เป็นรูปธรรม
setup
1. ดาวน์โหลด installer แบบเนทีฟสำหรับ Windows ของ Hermes มาติดตั้ง (ถ้าชอบ Linux ก็ยังมีทาง wsl --install แล้วติดตั้งในนั้นเช่นกัน)
2. สร้างโฟลเดอร์งานบนดิสก์ภายในที่เร็ว แล้วคัดลอกชีตข้อมูลที่จะตรวจไปไว้ฝั่งนั้น (ห้ามผูกไดรฟ์เครือข่าย·โฟลเดอร์งาน SVN เป็นพื้นที่ทำงานโดยตรง)
3. hermes setup → ป้อนคีย์ API ของโมเดล → ตรวจค่าเริ่มต้นของพาธพื้นที่ทำงาน·สิทธิ์
4. เปิดกลไกความปลอดภัย 5 อย่างใน %LOCALAPPDATA%\hermes\config.yaml: การอนุมัติสิทธิ์ (approvals.mode: manual · command_allowlist · cron_mode: deny) เพดานค่าใช้จ่าย (agent.max_turns · terminal.timeout · tool_loop_guardrails) เช็กพอยต์ (checkpoints.enabled) พาธล็อก (logs.path) และเข้าใจขั้นตอนหยุด (/stop · /rollback) ให้ขึ้นใจ
prompt - โยนเป้าหมายให้เป็นนามธรรมขึ้นมาอีกขั้น: ไม่ใช่ "ทำสิ่งนี้ให้หน่อย" แต่เป็น "สร้างผลลัพธ์นี้ขึ้นมาไว้" - ระบุเป้าหมาย·สิ่งที่ต้องทำ·ตำแหน่งบันทึกเป็นข้อ ๆ แล้วท้ายสุดต้องป้อนหนึ่งบรรทัดนี้เสมอ: "ถ้าติดขัดหรือคอลัมน์/รูปแบบต่างจากที่คาด ให้หยุดแล้วรายงาน ห้ามเดา" - งานชิ้นแรกให้เลือกอันที่ย้อนกลับง่ายอย่างการตรวจความสอดคล้องที่อ่านอย่างเดียว
verify - อย่าเชื่อผลลัพธ์รอบที่ 1 ทั้งดุ้น ให้คนตรวจจุดที่เอเจนต์หยุด (การจับคู่คอลัมน์·รูปแบบที่ไม่ตรงกัน) - ถ้าการหยุดถูกต้อง ก็ระบุการจับคู่แล้วขอใหม่ ถ้าผิด ก็ป้อนข้อจำกัดใหม่อีกครั้ง - นำรายการที่ไม่ผ่านในรายงานที่สร้างขึ้นสักหนึ่งสองรายการไปเทียบกับชีตต้นฉบับโดยตรง เพื่อตรวจว่าการตัดสินใจของเอเจนต์ถูกต้อง แล้วจึงค่อยส่งต่อไปตั้งเวลายามค่ำคืน (cron 23:00)
ถ้าอยากจับความรู้สึกของเอเจนต์ก่อนโดยไม่ต้องติดตั้ง Hermes คุณสามารถรันฉบับย่อด้วยการรันเบื้องหลังภายใน Claude Code ได้
ระหว่างทำการทบทวนรายไตรมาส ผมเปิดโฟลเดอร์สกิลส่วนกลางขึ้นมา พอไล่นับทีละบรรทัด ปรากฏว่ามี wrapper อยู่ถึง 19 ตัว ทั้งที่ตั้งใจไว้ชัดเจนว่าจะรันด้วย 12 ตัวและใช้มาตลอดทั้งปี แต่ไม่รู้ตอนไหนกลับมีเพิ่มขึ้นมาอีก 7 ตัว ที่งงยิ่งกว่าคือ ครึ่งหนึ่งในนั้นพอเห็นแค่ชื่อก็นึกไม่ออกว่าเป็นเครื่องมือทำอะไร migrate-legacy-enum นี่มันอะไรนะ ใช้ครั้งสุดท้ายเมื่อไหร่นะ
ผมนึกไม่ออก ตราบใดที่ยังพึ่งความทรงจำ คำถามนี้ก็ไม่มีวันตอบได้ ผมจึงตัดสินใจดู log แทนการพึ่งความทรงจำ การคัดสรรเครื่องมือไม่ควรเป็นงานที่ตัดออกตามรสนิยม แต่ควรเป็นงานที่ตัดออกด้วยตัวเลขที่ว่า "เครื่องมือนี้ถูกเรียกใช้กี่ครั้งในไตรมาสที่ผ่านมา"
บทนี้คือบันทึกว่าจะดึงตัวเลขนั้นออกมาโดยอัตโนมัติได้อย่างไร ใช้ตัวเลขนั้นตัดเครื่องมือออกได้อย่างไร และจะป้องกันไม่ให้เครื่องมือเพิ่มจำนวนพรวดพราดตั้งแต่ต้นได้อย่างไร
ก่อนจะพูดถึงการคัดสรร มีสิ่งหนึ่งที่ต้องยอมรับ เครื่องมือนั้นถ้าไม่ห้ามไว้ก็จะเพิ่มขึ้นแน่นอน ไม่ใช่เพราะใจไม่เข้มแข็ง แต่เป็นเพราะการสร้างสคริปต์เล็ก ๆ ขึ้นมาหนึ่งตัว "เพื่อจัดการงานครั้งนี้ให้เร็ว ๆ" ในทุก ๆ งาน เป็นทางเลือกที่สมเหตุสมผล เมื่อทางเลือกที่สมเหตุสมผลนั้นสะสมหลายสิบครั้ง มันก็กลายเป็นกองที่ไม่สมเหตุสมผล
โครงสร้างที่ใช้รันในโปรเจกต์ A คือรูปแบบที่ wrapper ส่วนกลาง 12 ตัวชี้ไปยังตัวหลัก 48 ตัวใน workspace ผ่าน junction ฝั่งส่วนกลางเบา ส่วนตัวหลักที่หนักก็เก็บไว้ใน workspace ที่จัดการด้วย SVN โครงสร้างนี้ได้กล่าวถึงไปแล้วใน §23.1 ปัญหาคือเลข 12 นี้มันอยู่นิ่ง ๆ ไม่ได้
ถ้าดูว่าอะไรเพิ่มขึ้นตามมาเมื่อเครื่องมือเพิ่มขึ้น ก็จะเห็นชัดว่าทำไมต้องห้ามไว้
โดยเฉพาะข้อแรก การกินโทเค็นบริบท เป็นต้นทุนที่ยิ่งคมขึ้นเมื่อก้าวเข้าสู่ยุคที่ใช้เครื่องมือ AI เมื่อ wrapper ส่วนกลางเพิ่มขึ้น โทเค็นที่ AI ใช้อ่าน "รายการเครื่องมือที่ฉันใช้ได้" ในทุกเซสชันก็เพิ่มขึ้น บริบทที่ควรใช้กับงานจริงกลับลดลงเพราะต้องเสียไปอ่านคำอธิบายของเครื่องมือ 19 ตัว ด้วยเหตุนี้ใน sync_skills.py ของโปรเจกต์ A จึงมีออปชัน --cleanup ที่จัดการ wrapper ซึ่ง junction พังหรือตัวหลักหายไปโดยอัตโนมัติ นี่ใกล้เคียงกับงานสุขอนามัยเพื่อรักษางบประมาณโทเค็นมากกว่า
แต่สิ่งที่ --cleanup จับได้มีแค่เครื่องมือที่ "พัง" เท่านั้น เครื่องมือที่ยังมีชีวิตอยู่ดี ๆ แต่ไม่มีใครใช้นั้นจับไม่ได้ การจะจับมันได้ต้องอาศัยข้อมูลความถี่ในการใช้งาน
ไอเดียหลักนั้นเรียบง่าย สกิลและเครื่องมือใน workspace ทั้งหมดอยู่ใน SVN และทุกครั้งที่ใช้เครื่องมือ ผลลัพธ์ที่เครื่องมือนั้นสร้างขึ้น (ชีต เอกสาร HTML แผนผังความสัมพันธ์ ฯลฯ) จะถูก commit เข้า SVN กล่าวคือ ถ้าดู SVN log ก็จะเห็นร่องรอยว่าเครื่องมือใดทำงานจริง
ผมจึงสร้างสคริปต์วัดผลเล็ก ๆ ชื่อ skill_audit_score ขึ้นมา ตามชื่อเลย คือให้ "คะแนนการตรวจสอบ (audit)" แก่แต่ละสกิล ตอนสร้างเครื่องมือนี้ผมไม่ได้เขียนโค้ดเองทั้งหมดตั้งแต่ต้น แต่อธิบายตรรกะการวัดให้ AI แล้วรับร่างแรกมา ผมจะเก็บบันทึกเซสชันจริง (worked transcript) นั้นไว้ตามเดิม ไม่สรุปย่อ และเขียนไว้ทั้งหมดรวมถึงส่วนที่เดาผิดทางด้วย
wrapper สกิลส่วนกลางตั้งไว้ที่ 12 ตัว แต่ไม่รู้ตัวก็พองขึ้นเป็น 19 ตัว ตอนทบทวนรายไตรมาส ผมอยากคัดเลือก "เครื่องมือที่ไม่ได้ใช้" ออกด้วยข้อมูล ช่วยสร้างสคริปต์วัดผลตามเงื่อนไขต่อไปนี้
- เครื่องมือสกิลแต่ละตัวมีอยู่ในรูปโฟลเดอร์ย่อยภายในโฟลเดอร์ workspace (เช่น
tools/relation-map-gen/)- ผลลัพธ์ที่เครื่องมือเหล่านี้สร้างจะถูก commit เข้า SVN ในข้อความ commit มักจะมีชื่อเครื่องมือหรือชนิดของผลลัพธ์อยู่ (เช่น "relation-map regen", "schema-doc update")
- ผมอยาก parse SVN log แล้วนับจำนวน commit ที่แต่ละเครื่องมือเกี่ยวข้องในช่วง 90 วันล่าสุด
- การนับจำนวน commit เฉย ๆ ไม่พอ ช่วยรวมสองอย่างเข้าด้วยกันแล้วออกเป็น audit_score ระหว่าง 0\~100 (a) ยิ่งใหม่ยิ่งให้น้ำหนักสูง (b) ถ้าวันที่ใช้ครั้งสุดท้ายนานมาแล้วก็หักคะแนน
- ผลลัพธ์เป็นตารางเรียงคะแนนจากมากไปน้อย เครื่องมือที่คะแนนต่ำคือ "ตัวเลือกคัดสรร (ตัดออก)"
- สมมติว่าคำสั่ง SVN รับมาในรูปแบบ
svn log --xml -l 2000ใช้เฉพาะไลบรารีมาตรฐานของ Python รับ absolute path เป็นอาร์กิวเมนต์
ได้ครับ แต่ขอชี้ประเด็นหนึ่งก่อน ไม่มีอะไรรับประกันว่าข้อความ commit จะมีชื่อเครื่องมืออยู่เสมอ ถ้าคนเขียนข้อความ commit ได้อย่างอิสระ ข้อความอย่าง "แก้ชีต" ก็จะบอกไม่ได้ว่าเครื่องมือใดเกี่ยวข้อง ผมจึงจะใช้การจับคู่สองแบบควบคู่กัน
- จับคู่คีย์เวิร์ดชื่อเครื่องมือ/นามแฝงในข้อความ commit (หลวม)
- จับคู่ว่า path ของไฟล์ ที่ถูกแก้ไขอยู่ในโฟลเดอร์ของเครื่องมือนั้น หรืออยู่ในรูปแบบผลลัพธ์ของเครื่องมือนั้น (เช่น
*_relation.html) หรือไม่ (เข้ม)ข้อ 2 เชื่อถือได้มากกว่า ผมจึงจะให้น้ำหนักมากกว่า ข้างล่างคือร่างแรก
# skill_audit_score.py (ร่างแรกของ Claude — ก่อนตรวจสอบ)
import subprocess, sys, datetime, xml.etree.ElementTree as ET
from collections import defaultdict
def svn_log_xml(repo_path, limit=2000):
out = subprocess.run(
["svn", "log", "--xml", "-v", "-l", str(limit), repo_path],
capture_output=True, text=True, check=True)
return ET.fromstring(out.stdout)
def score_skills(repo_path, skills, window_days=90):
root = svn_log_xml(repo_path)
now = datetime.datetime.now(datetime.timezone.utc)
hits = defaultdict(list) # skill -> [days_ago, ...]
for entry in root.findall("logentry"):
date = datetime.datetime.fromisoformat(
entry.find("date").text.replace("Z", "+00:00"))
days_ago = (now - date).days
if days_ago > window_days:
continue
msg = (entry.find("msg").text or "").lower()
paths = [p.text for p in entry.findall("paths/path")]
for sk in skills:
name = sk["name"].lower()
in_msg = name in msg or any(a in msg for a in sk.get("alias", []))
in_path = any(name in (pp or "").lower() for pp in paths) \
or any(pat in (pp or "") for pp in paths
for pat in sk.get("artifact", []))
if in_path:
hits[sk["name"]].append((days_ago, 2.0)) # น้ำหนักจับคู่แบบเข้ม
elif in_msg:
hits[sk["name"]].append((days_ago, 1.0)) # จับคู่แบบหลวม
rows = []
for sk in skills:
h = hits[sk["name"]]
if not h:
rows.append((sk["name"], 0, None, len(h)))
continue
# น้ำหนักความใหม่: ยิ่งใหม่ยิ่งเข้าใกล้ 1.0, ถ้า 90 วันก่อนเป็น 0.1
recency = sum(w * (1 - 0.9 * (d / window_days)) for d, w in h)
last_used = min(d for d, _ in h)
# หักคะแนนตามวันที่ใช้ครั้งสุดท้าย
stale_penalty = max(0, (last_used - 14)) * 0.5
score = max(0, min(100, recency * 8 - stale_penalty))
rows.append((sk["name"], round(score, 1), last_used, len(h)))
rows.sort(key=lambda r: r[1]) # เรียงคะแนนจากน้อยไปมาก = ตัวเลือกขึ้นก่อน
return rows
if __name__ == "__main__":
REPO = sys.argv[1]
SKILLS = [
{"name": "relation-map-gen", "alias": ["relation", "관계도"],
"artifact": ["_relation.html"]},
{"name": "schema-doc", "alias": ["schema", "명세서"],
"artifact": ["_schema.md"]},
{"name": "gdd-gen", "alias": ["gdd"], "artifact": ["_gdd.md"]},
{"name": "table-creator", "alias": ["table", "테이블"], "artifact": []},
{"name": "excel-reader", "alias": ["xlsm", "vba"], "artifact": []},
{"name": "migrate-legacy-enum", "alias": ["enum", "migrate"],
"artifact": []},
]
print(f"{'skill':22}{'score':>7}{'last_used(d)':>14}{'hits':>6}")
for name, score, last, hits_n in score_skills(REPO, SKILLS):
print(f"{name:22}{score:>7}{str(last):>14}{hits_n:>6}")
ผมรันสคริปต์กับ working copy ของ SVN จริง ผลลัพธ์แรกออกมาแปลก ๆ table-creator กับ excel-reader ทั้งคู่มีรูปแบบ artifact ว่างเปล่า จึงถูกจับได้ก็ต่อเมื่อมีคำว่า "table" / "xlsm" อยู่ในข้อความ commit เท่านั้น แต่เครื่องมือสองตัวนี้ผลลัพธ์เป็นไฟล์ .xlsm ธรรมดา จึงแยกแยะด้วยรูปแบบไม่ได้ ผลคือทั้งคู่ได้คะแนนต่ำผิดปกติ — ทั้งที่จริงเป็นเครื่องมือที่ใช้ทุกวัน
ตรงนี้ผมตัดสินใจสำคัญข้อหนึ่ง คะแนนต่ำไม่ได้แปลว่าต้องตัดทิ้งทันที สาเหตุที่คะแนนต่ำเป็นเพราะ "ไม่ได้ใช้จริง ๆ" หรือเพราะ "การวัดจับเครื่องมือไม่ได้" นั้น มนุษย์ต้องเป็นคนแยก ตัวเลขที่ AI สร้างขึ้นแค่ช่วยจำกัดตัวเลือกให้แคบลง การตัดสินใจขั้นสุดท้ายเป็นของมนุษย์
ผมจึงร้องขอ AI อีกครั้ง
เครื่องมือที่มีรูปแบบ artifact ว่างเปล่าจะเชื่อคะแนนไม่ได้ ช่วยเพิ่มคอลัมน์
confidenceในผลลัพธ์ด้วย เครื่องมือที่ไม่เคยจับคู่ artifact ได้เลยให้แสดงเป็นconfidence=LOWและตัดออกจากตัวเลือกคัดสรรอัตโนมัติ เครื่องมือที่เป็น LOW ให้แยกเป็นกลุ่มต่างหากในชื่อ "วัดไม่ได้ — ตรวจสอบด้วยตัวเอง"
ด้วยคำขอใหม่นี้ ผลลัพธ์ก็แยกออกเป็นสองกลุ่ม เครื่องมือที่ตัดออกได้ด้วยคะแนนที่เชื่อถือได้ และเครื่องมือที่การวัดอ่อน จึงต้องให้มนุษย์ดูเอง รูปร่างของผลที่รันจริงโดยประมาณเป็นแบบนี้ (คะแนนเป็นค่าที่วัดจริงบน working copy ของผู้เขียน ชื่อเครื่องมือบางส่วนถูกทำให้ไม่ระบุตัวตน)
| skill | audit_score | last_used (วันก่อน) | confidence | คำตัดสิน |
|---|---|---|---|---|
| relation-map-gen | 71.4 | 2 | HIGH | เก็บไว้ |
| schema-doc | 58.9 | 5 | HIGH | เก็บไว้ |
| gdd-gen | 22.1 | 31 | HIGH | สังเกตการณ์ |
| migrate-legacy-enum | 0.0 | วัดไม่ได้ | HIGH | ตัวเลือกคัดสรร |
| table-creator | 4.2 | 1 | LOW | ตรวจด้วยตัวเอง → เก็บไว้ |
| excel-reader | 6.0 | 1 | LOW | ตรวจด้วยตัวเอง → เก็บไว้ |
migrate-legacy-enum ได้คะแนน 0 และ confidence HIGH หมายความว่าในช่วง 90 วัน ทั้งโฟลเดอร์และผลลัพธ์ของเครื่องมือนี้ไม่เคยปรากฏใน commit แม้แต่ครั้งเดียว พอลองนึกย้อนดู มันคืองานที่ปีก่อนทำการ migrate เลกาซี enum ครั้งเดียวแล้วจบ เป็นงานที่ควรเกิดแค่ครั้งเดียวแต่ผมไปตรึงเป็นกฎไว้เป็นสกิล นี่แหละคือเครื่องมือที่ต้องตัด ในทางกลับกัน table-creator กับ excel-reader ได้คะแนนต่ำแต่ confidence เป็น LOW และวันที่ใช้ครั้งสุดท้ายคือเมื่อวานนี้ การวัดแค่จับไม่ได้ แต่จริง ๆ ใช้ทุกวัน ตัดไม่ได้
ข้อควรระวัง: สูตรคำนวณคะแนนในตารางข้างต้น (น้ำหนักความใหม่ × 8, หักคะแนน stale) เป็นค่าที่ผู้เขียนปรับจูนให้เข้ากับ working copy ของตัวเอง ถ้านิสัยการ commit SVN และรูปแบบผลลัพธ์ต่างออกไป สัมประสิทธิ์ก็จะต่างไปด้วย แก่นแท้ของเครื่องมือนี้คือ "อันดับเชิงสัมพัทธ์ระหว่างเครื่องมือ" และ "การแยก confidence" มากกว่าคะแนนสัมบูรณ์
skill_audit_score เป็นเพียงเครื่องมือวัดผลเท่านั้น ต้องมีวงจรที่ใส่ค่าที่วัดได้เข้าไปในการทบทวนรายไตรมาสแล้วหมุนหนึ่งรอบ เครื่องมือจึงจะได้รับการจัดการจริง วงจรนั้นเป็นดังนี้
flowchart TD
A[เริ่มการทบทวนรายไตรมาส] --> B[รัน skill_audit_score
parse SVN log 90 วัน]
B --> C{ตัดสิน confidence}
C -->|HIGH| D{ประเมิน audit_score}
C -->|LOW| E[ย้ายเข้าคิวตรวจด้วยตัวเอง
ตรวจวันที่ใช้ครั้งสุดท้ายเอง]
D -->|คะแนนสูง| F[เก็บไว้]
D -->|ปานกลาง·แนวโน้มลดลง| G[สังเกตการณ์ — วัดใหม่ไตรมาสหน้า]
D -->|0 หรือต่ำสุด| H[ยืนยันเป็นตัวเลือกคัดสรร]
E --> F
E --> H
H --> I{ทดแทนได้ไหม}
I -->|รวมเข้า Wrapper| J[เสริมเครื่องมือเดิมแบบ MECE
นโยบาย wrapper §23.1]
I -->|เลิกใช้โดยสมบูรณ์| K[sync_skills.py --cleanup
ลบ junction + เก็บไว้ใน SVN]
J --> L[ตรวจว่ากลับมาที่ 12 สลอต]
K --> L
L --> A
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class B,C,K code;
class A,D,E,I human;
class F,L pass;
การแยกแยะทางออกสองทางของวงจรนี้เป็นเรื่องสำคัญ เครื่องมือที่คะแนนเป็น 0 ไม่ได้แปลว่าลบทิ้งโดยไม่มีเงื่อนไข ถ้างานนั้นเองหายไปแล้ว ก็ส่งไปเลิกใช้โดยสมบูรณ์ (--cleanup) แต่ถ้างานนั้นยังจำเป็นอยู่ เพียงแต่ไม่ได้บ่อยพอที่จะตั้งเป็นเครื่องมือแยกต่างหาก ก็ให้รวมเข้ากับเครื่องมือเดิม อย่างหลังนี่แหละคือการเสริมแบบ MECE ใน §23.3.4
ตอนเลิกใช้ โค้ดก็ยังคงอยู่ในประวัติ SVN เพียงเก็บ junction และการเปิดเผยในส่วนกลางออกเท่านั้น ไม่ใช่ลบตัวโค้ดทิ้งถาวร ถ้าอีก 6 เดือนงานนั้นเกิดขึ้นอีก ก็แค่กู้คืนจาก SVN ตาข่ายนิรภัยที่ว่า "ย้อนกลับได้" นี้เองที่ทำให้มนุษย์กล้าตัดได้อย่างเด็ดขาด
ดียิ่งกว่าการวัดแล้วตัด คือการไม่สร้างตั้งแต่ต้น ถ้า skill_audit_score คือการจัดการภายหลัง นโยบาย MECE wrapper ก็คือการยับยั้งล่วงหน้า
MECE คือ Mutually Exclusive, Collectively Exhaustive — ไม่ทับซ้อนกัน และครอบคลุมครบถ้วน ทุกครั้งที่อยากสร้างเครื่องมือใหม่ ผมจะโยนสองตัวอักษรนี้ใส่ เครื่องมือใหม่ทับซ้อนกับเครื่องมือเดิมไหม (ละเมิด ME)? หรือเติมเต็มพื้นที่ที่ว่างอยู่จริง ๆ (มีส่วนช่วย CE)? นโยบาย wrapper ของโปรเจกต์ A แตกออกเป็นสองทางตรงนี้
| สถานการณ์ | นโยบาย | ผลลัพธ์ |
|---|---|---|
| งานใหม่ทับซ้อนกับพื้นที่ของเครื่องมือเดิม | เสริมเครื่องมือเดิมก่อน | เพิ่มฟังก์ชันลงในตัวหลักของ wrapper เดิม ไม่ใช้สลอตใหม่ |
| งานใหม่อยู่ในพื้นที่ที่ต่างออกไปอย่างชัดเจน | อนุญาตให้สร้าง wrapper ใหม่ | จัดสรรหนึ่งใน 12 สลอตให้เครื่องมือใหม่ (พร้อมตัวเลือกที่ต้องเอาออกควบคู่) |
แก่นคือ "ค่าตั้งต้นคือการเสริม" การสร้างเครื่องมือใหม่เป็นข้อยกเว้น การจะให้เหตุผลรองรับข้อยกเว้นนั้น ต้องพิสูจน์ให้ได้ว่า "ไม่มีเครื่องมือเดิมตัวใดทำงานนี้ได้" ค่าตั้งต้นข้อเดียวนี้เองที่เป็นสาเหตุที่แท้จริงในการดึงเครื่องมือซึ่งพองขึ้นเป็น 19 ตัวกลับลงมาเหลือ 12 ตัวอีกครั้ง
เรื่องนี้ก็เชื่อมโยงกับ cascade ใน §23.1 ด้วย cascade อย่าง check คือผลลัพธ์ของการรวมเครื่องมือตรวจสอบที่เดิมมี 4 ชนิดให้เหลือการเรียกใช้เดียว แทนที่จะมี wrapper แยกกัน 4 ตัว ก็มองในมุม MECE ว่า "ทั้งหมดนี้คือพื้นที่เดียวคือการตรวจสอบ" แล้วรวมเป็นหนึ่ง จำนวนเครื่องมือลดลงแต่ฟังก์ชันยังเท่าเดิม นี่คือแบบอย่างของการเสริม
ผู้ช่วย AI ตรงนี้เป็นทั้งปัจจัยเสี่ยงและทางแก้ ที่เป็นความเสี่ยงเพราะ ถ้าบอก AI ว่า "ช่วยสร้างสคริปต์จัดการงานนี้ให้หน่อย" เครื่องมือใหม่ก็ออกมาง่ายเกินไป ในสภาพแวดล้อมที่คลิกครั้งเดียวเกิดเครื่องมือหนึ่งตัว ถ้าไม่มีวินัย MECE สุสานเครื่องมือก็ก่อตัวขึ้นในพริบตา ที่เป็นทางแก้เพราะ ถ้าให้นโยบายแก่ AI ก่อน AI ก็จะเสนอเองว่า "อันนี้เอาไปแปะเป็นออปชันใน relation-map-gen เดิมน่าจะดีกว่า" ต้องยื่นวินัยการคัดสรรให้ AI ที่สร้างเครื่องมือไปพร้อมกันด้วย
สิ่งที่ผมเรียนรู้มากที่สุดจากการรันเครื่องมือในบทนี้คือ ห้ามเชื่อค่าที่วัดได้อย่างหลับหูหลับตา skill_audit_score ดูแค่สัญญาณเดียวคือ SVN log จึงมีสิ่งที่พลาดไปในเชิงโครงสร้าง
excel-reader โดยไม่สร้างผลลัพธ์ จะไม่ทิ้ง commit ไว้ ด้วยเหตุนี้กลไกที่ลด confidence ให้เป็น LOW แล้วโยนไปตรวจด้วยตัวเองจึงเป็นสิ่งจำเป็นสรุปคือ เครื่องมือนี้ไม่ใช่ "เครื่องมือที่ตัดสินใจ" แต่เป็น "เครื่องมือที่จำกัดตัวเลือกให้แคบลง" มันมอง 19 ตัวในคราวเดียวแล้วบอกได้ภายในวินาทีเดียวว่า "ควรสงสัยตัวไหน" การพิสูจน์ความสงสัยนั้นและการตัดยังคงเหลือไว้เป็นหน้าที่ของมนุษย์ การวัดไม่ได้มาแทนที่มนุษย์ แต่เพียงชี้ให้เห็นจุดที่มนุษย์ต้องไปดูเท่านั้น
นี่คือขั้นตอนการหมุนวงจรการคัดสรรเครื่องมือด้วยตัวเองหนึ่งรอบ
setup
1. ตรวจสอบว่าสกิลและเครื่องมือของเวิร์กสเปซอยู่ในระบบจัดการเวอร์ชัน (SVN/Git) หรือไม่ ผลลัพธ์ก็ต้องถูก commit เข้า repository เดียวกันด้วย
2. สร้างรายการเครื่องมือที่ต้องการวัด แต่ละเครื่องมือให้ระบุ name, alias (นามแฝงที่จะปรากฏในข้อความ commit), artifact (รูปแบบไฟล์ผลลัพธ์ ถ้ามี) เครื่องมืออ่านอย่างเดียวที่ไม่มี artifact ให้ปล่อยว่างไว้
prompt (ถึง AI)
ช่วยสร้างสคริปต์วัดความถี่การใช้เครื่องมือตามเงื่อนไขต่อไปนี้ (1) เครื่องมือแต่ละตัวทิ้งร่องรอยใน log ของ [ระบบจัดการเวอร์ชัน] ในรูป commit ผลลัพธ์ (2) parse log 90 วันล่าสุดแล้วนับจำนวน commit ที่แต่ละเครื่องมือเกี่ยวข้อง (3) ออกคะแนน 0\~100 ด้วยน้ำหนักความใหม่ + หักคะแนนตามวันที่ใช้ครั้งสุดท้าย (4) เครื่องมือที่ไม่เคยจับคู่รูปแบบผลลัพธ์ (artifact) ได้เลยให้แสดงเป็น confidence=LOW ตัดออกจากตัวเลือกอัตโนมัติ และแยกไปตรวจด้วยตัวเอง (5) ผลลัพธ์เป็นตารางเรียงคะแนนจากน้อยไปมาก — คะแนนต่ำคือตัวเลือกคัดสรร ใช้เฉพาะไลบรารีมาตรฐาน รับ path ของ repository เป็นอาร์กิวเมนต์
verify 1. ถ้าเครื่องมือที่ใช้ทุกวันขึ้นมาอยู่บนสุดของตาราง (คะแนนต่ำ) แสดงว่าการวัดผิด ให้ตรวจ confidence ของเครื่องมือนั้น — ถ้าเป็น LOW ก็ปกติ (วัดไม่ได้) แต่ถ้าเป็น HIGH ทั้งที่คะแนนต่ำ ให้ตรวจการตั้งค่า alias·artifact 2. ยืนยันเฉพาะเครื่องมือที่คะแนน 0 + confidence HIGH เป็นตัวเลือกคัดสรรเท่านั้น เทียบวันที่ใช้ครั้งสุดท้ายกับความทรงจำ แล้วให้มนุษย์ตัดสินว่าเป็นเครื่องมือที่ตายไปแล้วจริงหรือไม่ 3. ส่งตัวเลือกไปสู่ทางใดทางหนึ่งระหว่าง "เลิกใช้โดยสมบูรณ์" กับ "รวมเข้าเครื่องมือเดิม" การเลิกใช้เก็บเฉพาะ junction ออก ส่วนโค้ดยังคงอยู่ใน repository 4. นับดูเป็นครั้งสุดท้ายว่า 12 สลอต (หรือขีดจำกัดที่ตัวเองกำหนด) กลับคืนมาหรือยัง
ถ้าเป็นการพัฒนาคนเดียวที่มีเครื่องมือแค่ 6\~8 ตัวและไม่มี SVN ให้ย่อแบบนี้ ระบบจัดการเวอร์ชันใช้ Git ก็เพียงพอ ใช้ git log --since="90 days ago" --name-only ดึง path ของไฟล์ที่ถูกแก้ไขออกมา แล้ว grep ด้วยชื่อโฟลเดอร์เครื่องมือสักครั้ง ก็จะได้ว่า "เครื่องมือใดทำงานล่าสุด" ไม่ต้องสร้างสคริปต์ให้คะแนนเลยก็ได้ แก่นไม่ได้อยู่ที่ความแม่นยำของตัวเลข แต่อยู่ที่นิสัยข้อเดียว — ดู log แทนความทรงจำ ไตรมาสละครั้ง ดึง "เครื่องมือที่ไม่ได้แตะแม้แต่ครั้งเดียวใน 90 วันที่ผ่านมา" ออกมาด้วย git log แล้วเพ่งมองเครื่องมือนั้น 5 นาทีนั้นเองที่ป้องกันสุสานเครื่องมือ
บ่ายวันเสาร์ ภรรยาของผมกำลังเล่นเกมพัซเซิลจับคู่สีบนมือถือ เป็นเกมชื่อ Yarn Fever ที่ต้องคัดแยกไหมพันกันยุ่งใส่ตะกร้าสีเดียวกัน พอจบหนึ่งด่านเธอก็พูดว่า "ก็เหมือนเดิมอีกแล้ว" แล้วปิดเกม เมื่อไม่มีอะไรอัปเดต ความเบื่อจึงมาเร็ว
ความคิดที่ผุดขึ้นในตอนนั้นเรียบง่าย ลูปนั้นมีความน่าติดที่พิสูจน์แล้ว และตัวกลไกเองก็ไม่ใช่สิ่งที่ลิขสิทธิ์คุ้มครอง ถ้าเปลี่ยนเป็นธีมสัตว์ และปั๊มด่านออกมาแบบโพรซีเดอรัลให้ไม่มีที่สิ้นสุด ปัญหา "เหมือนเดิมอีกแล้ว" ก็จะหายไป ถ้าสร้างเป็น HTML 3D ที่รันได้ทันทีในเบราว์เซอร์โดยคนเดียว ก็ไม่ต้องติดตั้งอะไรบนมือถือของภรรยาด้วยซ้ำ
ปัญหาคือผมไม่ใช่กราฟิกเอนจิเนียร์ ผมเป็นนักออกแบบเกม (Game Designer) ที่มีประสบการณ์ 24 ปี แต่ไม่เคยเขียนเชดเดอร์ด้วย Three.js มาก่อน ดังนั้นบทนี้จึงเป็นบันทึกจริงของการ "ทำเกมหนึ่งเกมให้รันได้ภายในไม่กี่วันโดยลำพังร่วมกับ AI" เป็นบันทึกที่ใช้เครื่องมือชุดเดียวกับงาน MMORPG ของบริษัท (ต่อจากนี้เรียกว่าโปรเจกต์ A) แต่ก็เป็นบันทึกของการแบ่งแยกที่ไม่ปนเนื้อหาเชิงโดเมนเข้ามาแม้แต่บรรทัดเดียว
เกมจริงอยู่ใน repository critter-sort/ และมี git tag v0.1\~v0.3 ที่เก็บบันทึกการตัดสินใจตลอดสามวันไว้ ผมจะอ้างอิงจาก repository นั้นตามจริง ไม่ใช่กรณีที่ปรุงแต่งขึ้น
สิ่งแรกที่ทำคือถอดเกมต้นฉบับออกมาเป็นคำพูดแล้วโยนให้ AI พรอมต์แรกเป็นแบบนี้
พรอมต์ (เริ่ม v0.1): "ผมอยากดัดแปลงลูปหลักของเกมพัซเซิลแคชชวลชื่อ Yarn Fever ให้เป็นธีมสัตว์ แล้วสร้างด้วย Three.js + Vite ลูปเป็นแบบนี้ คัดแยกก้อนสีที่พันกันใส่ถังสีเดียวกัน ถ้าเกินช่องชั่วคราวก็เกมโอเวอร์ ใช้สัตว์เป็นเป้าหมายในการคัดแยก โดยให้แตะที่กองสัตว์ที่พันกันยุ่งแล้วมันถูกส่งไปยังรัง (nest) สีเดียวกัน เขียนลอจิกเป็น state machine ของ JS ล้วน ๆ ที่ไม่ขึ้นกับ Three.js เพื่อให้ทดสอบแบบ headless ได้ ใส่ด่านโพรซีเดอรัลที่ไม่มีที่สิ้นสุด (อิงซีด) มาด้วย"
AI ทำตามอย่างซื่อสัตย์ มันแยกโครงสร้างโฟลเดอร์เป็น game/ (ลอจิกล้วน) กับ render/ (Three.js) เขียน state.js·rules.js·generator.js ก่อน แล้วแสดงบอร์ดด้วย placeholder กล่องที่ลงสีไว้ ไม่ใช่กี่วันด้วยซ้ำ แค่เซสชันเดียว v0.1 ก็รันได้
แต่ในวินาทีที่ผมลองเล่นเองเพื่อจะให้ภรรยาดู ความรู้สึกขัด ๆ ก็มา มันกลายเป็นเกมจับคู่ทั่วไปที่สัตว์กระโดดจากด้านบนของจอลงไปในตะกร้า สัมผัสเฉพาะตัวของเกมต้นฉบับหายไป ตัวตนของ Yarn Fever ไม่ได้อยู่ที่ "การคัดแยก" แต่อยู่ที่ สัมผัสของการใช้มือคลายไหมที่พันกันยุ่ง และ การหมุนจอไปเพื่อยืนยันสีที่ถูกบังไว้ ผมไปบีบสิ่งนั้นให้แบนเป็นการ sort ทั่วไปด้วยพรอมต์ที่ว่า "แตะสัตว์แล้วส่งไปรัง" และ AI ก็เพียงแค่ทำตามนิยามที่ผมให้อย่างซื่อสัตย์เท่านั้น
นี่คือกับดักแรกของการทำวิศวกรรมย้อนกลับ พอสรุปเกมต้นฉบับ ลายเซ็นก็ระเหยไป เพราะการสรุปทิ้งไว้แค่พื้นผิว ไม่ใช่แก่นแท้
ตรงนี้ผมขอชี้ชัดอย่างหนึ่ง AI ไม่ได้ให้คำตอบที่ผิด พรอมต์ของผมต่างหากที่ผิด ฉะนั้นรอบถัดไปจึงไม่ใช่การแก้โค้ด แต่เป็น การแก้ไขนิยาม
ทันทีที่เห็น v0.1 ผมระบุแก่นใหม่อีกครั้งในสองรอบ การแก้ไขรอบแรกเล็งไปที่การกระทำ รอบที่สองเล็งไปที่สัมผัส
พรอมต์ (แก้ไขรอบที่ 1 — การกระทำ): "อันนี้มันกลายเป็นแค่เกมจับคู่ไปแล้ว ตัวตนของ Yarn Fever คือ 'การหมุนจอ 3D ไปเพื่อยืนยันสีที่ถูกบัง แล้วกำหนดลำดับการคลาย' อย่าโปรยบอร์ดลงบนระนาบ แต่ให้กองสูงเป็นเนินตรงกลาง เพื่อให้ต้องหมุนถึงจะเห็นสีที่บังอยู่ด้านหลัง ใส่การหมุนด้วย OrbitControls แต่การหมุนกับการแตะมันชนกันนี่ — จะแยกยังไง"
ผลลัพธ์จาก Claude (ดิบ ไม่สรุปย่อ): "ถูกต้องครับ ท่าทางหมุนกับการแตะใช้ pointer event เดียวกันจึงชนกัน ผมเสนอวิธีแยกด้วยเกณฑ์ระยะทางและเวลา บันทึกพิกัดและเวลาที่
pointerdownแล้วที่pointerupถ้าระยะการเคลื่อนเกิน 9px หรือเวลากดเกิน 400ms ให้ถือว่าเป็นท่าทางหมุนและละเว้นการแตะ เฉพาะที่ต่ำกว่านั้นเท่านั้นจึงใช้ raycaster หยิบสัตว์แล้วเรียกstate.tap()ส่วน azimuth ของ OrbitControls ผมจะจำกัดไว้ราว ±54° เพื่อไม่ให้กองหลุดออกนอกจอ อย่างไรก็ตามบนมือถือ 9px อาจไวเกินไป จึงต้องตรวจสอบบนเครื่องจริง"
เกณฑ์ 9px/400ms นั้นยังคงอยู่ใน src/main.js จนถึงตอนนี้ เป็นโค้ดที่ไม่มีส่วนเกินและตรวจสอบได้โดยตรง
// src/main.js — ลาก = หมุน (OrbitControls) / แตะสั้น = คลายไหม
canvas.addEventListener('pointerup', (ev) => {
if (!down || state.status !== 'playing') { down = null; return }
const dist = Math.hypot(ev.clientX - down.x, ev.clientY - down.y)
const dt = performance.now() - down.t
down = null
if (dist > 9 || dt > 400) return // ถือว่าเป็นท่าทางหมุน
const rect = canvas.getBoundingClientRect()
const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1
const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1
const id = view.pick(x, y, camera)
if (id !== null) state.tap(id)
})
การกระทำกลับมาแล้ว แต่พอเล่นเป็นรอบที่สองก็ยังไม่มีความรู้สึก "คลายไหม" อยู่ดี สัตว์แค่เคลื่อนไปยังรังเฉย ๆ ไม่มีสัมผัสของการถูกคลายออก จึงเป็นการแก้ไขรอบที่สอง
พรอมต์ (แก้ไขรอบที่ 2 — สัมผัส): "การหมุนดีขึ้นแล้ว แต่ยังไม่มีความรู้สึกของ 'การคลายไหม' พอแตะ ให้ใส่ลูกเล่นที่เส้นไหมพันกันยุ่งซึ่งพันรอบตัวสัตว์คลายออกแล้วหายไป จากนั้นไหมเส้นนั้นถูกม้วนเข้าไปในตะกร้า ส่วนลุคของสัตว์ก็อย่าใช้ GLB นกที่เกลี้ยงเกลา แต่ให้เป็นตุ๊กตาถักไหมพรม (อามิกุรุมิ) อ้วนกลม มีดีเทลแบบโคซีอย่างตาง่วงนอน แก้มแต้มสี ถ้าเอเซตภายนอกไม่เข้ากันก็เอาออก แล้วสร้างแบบโพรซีเดอรัลแทนก็ได้"
ผลลัพธ์จาก Claude (ดิบ): "ผมจะแยกสองส่วนมาทำครับ (1) ไหมพันกัน พันเส้น
TorusGeometryสีเดียวกันแต่เข้มขึ้นเล็กน้อย 2 เส้นไว้รอบลำตัวสัตว์แต่ละตัว แล้วตอนแตะให้ลด scale เหลือ 0.01 เพื่อให้คลายออกและหายไป (2) การม้วนไหม วาดไหมตามเส้นทางจากสัตว์ไปตะกร้าด้วยTubeGeometryแล้วทวีนให้ม้วนเข้าไปทางตะกร้า ลุคตุ๊กตาถักผมจะไปทางโพรซีเดอรัล สร้างเท็กซ์เจอร์ knit bump แบบโพรซีเดอรัลใส่ในMeshStandardMaterialเพื่อให้ได้ผิวสัมผัสแบบนิตติง ส่วนตาง่วงทำด้วย box แบน แก้มแต้มทำด้วย sphere สีชมพูกึ่งโปร่งใส GLB สตาร์ทเตอร์เดิม (นก 3 ชนิด) ไม่เข้ากับโทนนิตติงจึงเอาออก แต่จะคงไปป์ไลน์โหลดแพ็กสัตว์ไว้เป็น fallback มีอยู่อย่างหนึ่งที่ขอเตือน สัตว์ที่สร้างแบบโพรซีเดอรัลมีความหลากหลายของชนิดสู้ GLB ไม่ได้ — ปลอดภัยกว่าถ้าจะตรวจสอบด้วยชนิดเดียวก่อนแล้วค่อยเพิ่มชนิด"
คำเตือนสุดท้ายนั้น ("ความหลากหลายของชนิดสู้ GLB ไม่ได้" — GLB คือ glTF Binary ฟอร์แมตไฟล์โมเดล 3D สำเร็จรูปที่รับมาจากภายนอกเพื่อนำมาใช้) คือเมล็ดพันธุ์ที่นำไปสู่ v0.3 พอดี AI พูดถึงข้อจำกัดถัดไปก่อน และผมก็รับมาเป็นไมล์สโตนถัดไป
การตรวจสอบเป็นสองขั้นทุกครั้ง ขั้นแรกดูว่าลอจิกไม่พังด้วย headless (ข้อผิดพลาด 0) จากนั้นจึงลองสัมผัสด้วยการหมุนและแตะในเบราว์เซอร์โดยตรง การตรวจสอบนั้นถูกตรึงเป็นกฎไว้ในข้อความคอมมิต v0.2 "ตรวจสอบ headless: การหมุน·การคลายไหม·การเคลียร์อัตโนมัติปกติ ข้อผิดพลาด 0"
ไหมพันกัน 2 เส้นยังคงอยู่ใน src/render/pieces.js แบบนี้
// src/render/pieces.js — ไหมหลวม 2 เส้นที่พันรอบลำตัว (สีเดียวกันแต่เข้มขึ้นเล็กน้อย)
const strandMat = new THREE.MeshStandardMaterial({ color: darken(hex, 0.7), roughness: 1 })
const strands = []
const orient = [[0.5, 0.2, 0.0], [1.25, 0.0, 0.6]]
for (let i = 0; i < 2; i++) {
const s = addMesh(g, G.torus, strandMat, [0, byo + 0.02, 0], Math.max(bx, bz) + 0.02, orient[i])
strands.push(s)
}
g.userData.strands = strands // ตอนแตะ view.js จะคลายเส้นเหล่านี้ให้หายไป
บทเรียนที่ได้จากตรงนี้ ผมขอบันทึกไว้เป็นบรรทัด
ถ้ามองด้วยคำพูดอย่างเดียวก็คือ "แก้สองครั้ง" แต่ประวัติ git เก็บไว้พร้อมเวลาที่แม่นยำว่าการแก้ไขนั้นเข้ามาเมื่อไรในรูปแบบใด นี่คือสิ่งที่แทนการทบทวนในการพัฒนาคนเดียว แม้ไม่มีเพื่อนร่วมงาน คอมมิตก็เป็นพยานว่า "ทำไมถึงกลายเป็นแบบนี้"
| คอมมิต | เวลา (2026-05-30) | อะไรเปลี่ยนไป | สถานะลายเซ็น |
|---|---|---|---|
2b2e3bc v0.1 |
14:43 | วิศวกรรมย้อนกลับ Yarn Fever, ลอจิกล้วน + placeholder, ผ่านโซลเวอร์ 60/60 | ขาดหาย (แบนเป็น sort ทั่วไป) |
70a0117 v0.2 |
15:11 | การหมุน (OrbitControls ±54°) + แยกแตะ/ลาก + คลายไหม + อามิกุรุมิ | กู้คืน (นิยามแก่นใหม่) |
160663c สแนปช็อต |
15:31 | สแนปช็อตแกลเลอรี v0.2 5 ภาพ + แกลเลอรี README | — |
59b0baf v0.3 |
15:55 | อามิกุรุมิโพรซีเดอรัล 8 ชนิด + พาเลตต์แคนดี้สีสด | เสริมแกร่ง (ได้ความหลากหลายของชนิด) |
c5b9a1b ส่งต่องาน |
16:20 | ตัวชี้ส่งต่อเซสชัน NEXT_SESSION | — |
เนื้อข้อความคอมมิต v0.2 ตรึงตัวการตัดสินใจไว้เอง "แก้ตัวตนเกมจาก 'สัตว์กระโดด' เป็น 'หมุนจอไปคลายไหมพันกันน่ารัก (ตุ๊กตาถักไหมพรม) ลงตะกร้าสีเดียวกัน'" เป็นบันทึกที่ภายในชั่วโมงครึ่ง ตัวตนของเกมตายไปครั้งหนึ่งแล้วฟื้นกลับมา
ดีเทลหนึ่งที่น่าสังเกต พอดู git show --stat ของ v0.2 จะเห็นว่า GLB นกสตาร์ทเตอร์ 3 ชนิด (Flamingo·Parrot·Stork) ถูกลบทิ้งทั้งหมด เพราะ "อาร์ตไม่เข้ากับโทนนิตติง" เอเซตฟรีจากภายนอกไม่ใช่ว่าฟรีแล้วจะใช้ได้หมด แต่ถ้าโทนไม่เข้ากันก็ลบทิ้ง นี่คือด่านความรู้สึกทางสุนทรียะที่คนตัดสิน ไม่ใช่ AI
public/assets/animals/pack_starter/Flamingo.glb | Bin 77428 -> 0 bytes
public/assets/animals/pack_starter/Parrot.glb | Bin 97024 -> 0 bytes
public/assets/animals/pack_starter/Stork.glb | Bin 76852 -> 0 bytes
การบ้านที่ v0.2 ทิ้งไว้คือ "สัตว์โพรซีเดอรัลมีความหลากหลายของชนิดสู้ GLB ไม่ได้" ผมแก้มันใน v0.3 โดยไม่เพิ่มเอเซตภายนอกแม้แต่ชิ้นเดียว แต่ปั๊มสัตว์ 8 ชนิดออกมาด้วยโค้ด
แก่นอยู่ที่ตาราง SPECIES ใน src/render/pieces.js แต่ละชนิดนิยามสัดส่วนลำตัว·หัว·ชนิดหู·จมูก·รูปทรงตาเป็นพารามิเตอร์ และฟังก์ชันเดียวอ่านพารามิเตอร์นั้นเพื่อประกอบเมช
// src/render/pieces.js — พารามิเตอร์ silhouette แยกตามชนิด
const SPECIES = {
cat: { body: [0.5,0.46,0.48,0.04], ears: 'cat', snout: 0.13, tail: 'cat', eyes: 'sleepy' },
bear: { body: [0.52,0.5,0.5,0.03], ears: 'bear', snout: 0.16, tail: 'none', eyes: 'round' },
bunny: { body: [0.46,0.5,0.46,0.02], ears: 'bunny', snout: 0.12, tail: 'puff', eyes: 'round' },
fox: { body: [0.5,0.44,0.48,0.04], ears: 'fox', snout: 0.2, tail: 'fox', eyes: 'sleepy' },
capybara: { body: [0.58,0.5,0.56,0.02], ears: 'tiny', snout: 0.22, tail: 'none', eyes: 'sleepy' },
pig: { body: [0.54,0.5,0.52,0.03], ears: 'pig', snout: 0.1, nose: true, eyes: 'round' },
frog: { body: [0.56,0.4,0.54,0.05], ears: 'none', snout: 0.1, topEyes: true, eyes: 'none' },
chick: { body: [0.42,0.44,0.42,0.05], ears: 'none', beak: true, tail: 'none', eyes: 'round' },
}
export const SPECIES_IDS = Object.keys(SPECIES) // 8 ชนิด
รูปทรงหูเพียงอย่างเดียวก็แยก silhouette ได้ แมวกับหมาจิ้งจอกใช้ cone แหลม หมีใช้ sphere กลม กระต่ายใช้ sphere ยาว หมูใช้ cone ที่งอไปข้างหน้า กบมีตาที่โผล่ขึ้นเหนือหัว (topEyes) ลูกไก่มีปาก (beak) การแยกย่อยเล็ก ๆ เหล่านี้สร้างความแตกต่างของ 8 ชนิด เอเซตภายนอก 0 โค้ดไฟล์เดียว
แต่การสร้างแบบโพรซีเดอรัลมีกับดักอยู่ โค้ดที่ "ดูเหมือนจะใช้ได้" จะสร้าง 8 ชนิดที่แยกแยะออกจริงหรือไม่ ดูจากโค้ดอย่างเดียวไม่รู้ ฉะนั้นการตรวจสอบจึงเป็นสองขั้นอีกครั้ง ขั้นแรกดูว่า 8 ชนิดถูกสร้างขึ้นโดยไม่มีข้อผิดพลาดด้วย headless จากนั้นจับภาพการเรนเดอร์จริงด้วยสกิล web-screenshot (Chrome แบบ headless) เพื่อดูด้วยตาว่า 8 ชนิดแยกออกจากกันหรือไม่ ผลอยู่ใน DEVLOG v0.3 "แยก silhouette ด้วยหู/จมูก/ปาก/ปากนก/หาง/ตา เอเซตภายนอก 0 โทนนิตติงเป็นหนึ่งเดียวกันสมบูรณ์"
ความไม่มีที่สิ้นสุดของด่านเป็นหน้าที่ของ RNG อิงซีด generator.js แปลงหมายเลขด่านเป็นซีดด้วย Knuth multiplicative hash แล้วสุ่มเลขแบบกำหนดได้ด้วย mulberry32 หมายเลขด่านเดียวกันให้บอร์ดเดียวกันเสมอ
// src/game/generator.js
export function generateLevel(level, animalPool = null) {
const seed = (level * 2654435761) >>> 0 // Knuth multiplicative hash
const rng = makeRng(seed)
const { C, K, groupsPerColor, M, T } = levelParams(level)
const colors = rng.shuffle(COLORS).slice(0, C)
// ...
for (const color of colors) {
const count = K * groupsPerColor // เป็นพหุคูณของ K เสมอ → แบ่งลงรังได้ลงตัว (รับประกันแก้ได้)
// ...
}
}
บรรทัดเดียวตรงนี้รับประกันความเป็นธรรมของเกม เพราะบังคับให้จำนวนสัตว์ต่อสีเป็น พหุคูณของ K (จำนวนตัวที่ทำให้รังสมบูรณ์ คือ 3) เสมอ บอร์ดไหนก็แบ่งลงรังได้ลงตัวพอดี ด่านที่แก้ไม่ได้จึงไม่เกิดขึ้นโดยต้นกำเนิด
การที่ออกแบบให้แก้ได้ ยังไม่ใช่ข้อพิสูจน์ ผมใส่กรีดีโซลเวอร์สำหรับตรวจสอบไว้ใน rules.js แล้วให้ test-logic.mjs เล่นด่าน 60 ด่านอัตโนมัติเพื่อตรวจทุกครั้งว่าเคลียร์ได้ครบจริงหรือไม่ นี่คือผลลัพธ์ที่วัดจริงตอนรันอีกครั้งขณะเขียนบทนี้
$ node scripts/test-logic.mjs
[โซลเวอร์] เคลียร์ 60/60 ด่าน
[กราฟความยาก] (C=สี, K=สมบูรณ์, groups, M=รัง, T=ถาด, รวมจำนวนตัว)
Lv 1: C=3 K=3 grp=2 M=3 T=7 รวม=18
Lv 8: C=4 K=3 grp=3 M=4 T=6 รวม=36
Lv12: C=5 K=3 grp=3 M=4 T=5 รวม=45
Lv20: C=5 K=3 grp=3 M=4 T=4 รวม=45
[เล่นมั่ว] อัตราแพ้เมื่อแตะสุ่ม (ยืนยันว่ามีความยากอยู่จริง)
Lv 1: อัตราแพ้แบบสุ่ม 0%
Lv12: อัตราแพ้แบบสุ่ม 1%
Lv20: อัตราแพ้แบบสุ่ม 3%
การทดสอบนี้พิสูจน์สองอย่างพร้อมกัน การที่กรีดีโซลเวอร์ผ่าน 60/60 หมายความว่า ทุกด่านแก้ได้ (ความยากไม่ถึงขั้นเป็นไปไม่ได้) และการที่อัตราแพ้ของการแตะสุ่มเพิ่มจาก 0%→3% เมื่อด่านสูงขึ้น หมายความว่า ความยากมีอยู่จริง (ถ้ากดมั่ว ๆ แล้วแก้ได้หมดก็ไม่ใช่เกม) กราฟความยากที่ถาดแคบลงจาก 7 ช่องเหลือ 4 ช่อง ถูกวัดออกมาเป็นอัตราแพ้
ตรงนี้ผมขอชี้ตามจริง อัตราแพ้แบบสุ่ม 3% คืออัตราแพ้ของ "บอตที่กดมั่ว" ไม่ใช่ความรู้สึกถึงความยากของคน คนจะหมุนเพื่อยืนยันสีล่วงหน้า อัตราแพ้จึงต่ำกว่านี้ ตัวเลขนี้เป็นการพิสูจน์เชิงทิศทางว่า "ความยากไม่ใช่ 0" ไม่ได้หมายความว่าภรรยาจะแพ้ด้วยความน่าจะเป็น 3% ความรู้สึกถึงความยากของคนยังไม่ได้วัด ณ จุด v0.3 และผมบันทึกไว้ใน NEXT_SESSION ว่า "เก็บฟีดแบ็กการเล่นของภรรยา (สำคัญที่สุด)"
flowchart TD L["หมายเลขด่าน N"] --> H["Knuth hash
N × 2654435761"] H --> S["mulberry32(seed)
RNG แบบกำหนดได้"] S --> P["levelParams(N)
คำนวณ C·K·M·T"] P --> G["generateLevel
ต่อสี = พหุคูณของ K"] G --> B["บอร์ด (กองพันกัน)"] B --> R["createCritter
อามิกุรุมิ 8 ชนิด + เชดเดอร์ knit"] G --> V["greedySolve
ตรวจสอบ 60/60"] V -->|"ข้อผิดพลาด 0"| OK["รับประกันการเคลียร์"] R --> SC["web-screenshot
ตรวจสอบด้วยสายตา 8 ชนิด"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class H,S,P,G,R,V,SC code; class L,B data; class OK pass;
ลำดับนี้ที่ออกจากซีดแล้วแตกแขนงไปยังพารามิเตอร์·บอร์ด·เมช·การตรวจสอบ คือคำตอบที่แก้ปัญหาแรกสุด "ไม่มีอัปเดตจึงเบื่อ" ได้อย่างเป็นโครงสร้าง
สัตว์โพรซีเดอรัล 8 ชนิดคือ fallback สำหรับตอนที่ไม่มี GLB ในภายหลังถ้าหา GLB อามิกุรุมิจริงมาได้ ก็ให้มันถูกใช้ก่อน ผมจึงคงไปป์ไลน์แพ็กสัตว์ไว้ แค่วาง GLB ลงในโฟลเดอร์แล้วรัน npm run scan ก็จบ
ปัญหาคือ GLB แต่ละตัวมีขนาดต่างกันไป บางโมเดล 0.5 ยูนิต บางตัว 200 ยูนิต ถ้าปรับ scale ด้วยมือ การเพิ่มแพ็กสัตว์ก็จะกลายเป็นแรงงาน ฉะนั้น scan-packs.mjs จึงอ่าน bounding box ของ GLB แล้วคำนวณ scale ที่พอดีกับความสูงเป้าหมาย (0.95 ยูนิต) โดยอัตโนมัติ
// scripts/scan-packs.mjs — คำนวณ scale/yOffset อัตโนมัติจาก bounding box ของ GLB
const maxDim = Math.max(max[0]-min[0], max[1]-min[1], max[2]-min[2])
const scale = +(TARGET_H / maxDim).toPrecision(3) // TARGET_H = 0.95
const yOffset = +(-((min[1] + max[1]) / 2) * scale).toPrecision(3)
และ assets.js จะ fallback ไปยังสัตว์โพรซีเดอรัลอย่างเงียบ ๆ ถ้าไม่มี packs.json หรือโหลดไม่สำเร็จ
// src/render/assets.js
createAnimal(species, hex) {
const entry = this.models.get(species)
if (!entry) return createCritter(hex, species) // fallback อามิกุรุมิโพรซีเดอรัล
// ... โคลน GLB + ทินต์สี
}
สองบรรทัดนี้รับประกัน "มี GLB ก็ใช้ GLB ไม่มีก็ใช้สัตว์โค้ด" แบบไม่สะดุด ขณะที่ภรรยากำลังเล่น ต่อให้ผมวาง GLB แพ็กใหม่ลงไป เกมก็ไม่หยุด
ในโปรเจกต์นี้ผมเป็นนักออกแบบเกมคนเดียว แต่งานหมุนไปด้วยหลายบทบาท AI เข้าเติมบทบาทเหล่านั้น แก่นไม่ใช่ "เขียนโค้ดแทนให้" แต่เป็น เติมในจุดที่ผมอ่อน
| จุดที่ผมอ่อน | สิ่งที่ AI ทำ | ด่านที่คน (ผม) คุมไว้ |
|---|---|---|
| เชดเดอร์ Three.js | เท็กซ์เจอร์ knit bump โพรซีเดอรัล, ลูกเล่นไหม TubeGeometry | โทนเข้ากันหรือไม่ (ตัดสินใจลบ GLB นก 3 ชนิด) |
| แก้การชนกันของอินพุต | เสนอเกณฑ์ 9px/400ms | ยืนยันความรู้สึกบนเครื่องจริงมือถือ |
| ความปลอดภัยจาก regression | ตรวจสอบ 60/60 อัตโนมัติด้วย greedySolve | "ความยากมีอยู่จริง" คนเป็นผู้นิยาม |
| คาดการณ์ข้อจำกัดถัดไป | เตือนว่า "สัตว์โพรซีเดอรัลความหลากหลายของชนิดอ่อน" | รับมาเป็นไมล์สโตน v0.3 |
โดยเฉพาะการตรวจสอบด้วยสายตาคือข้อต่อที่อ่อนของการพัฒนาคนเดียว การที่โค้ดรันได้ กับ "8 ชนิดแยกออกด้วยตา" เป็นคนละปัญหากัน ฉะนั้นผมจึงยืมสกิล web-screenshot (รัน dev server ด้วย Chrome แบบ headless แล้วจับภาพหน้าจอ + รายงานข้อผิดพลาด console) มาจากงานบริษัทมาใช้ตามนั้น แม้ไม่มีส่วนขยาย claude-in-chrome ก็ยืนยันการเรนเดอร์ในวิวพอร์ตมือถือ (iPhone 15 Pro แนวตั้ง 393×852) ด้วยตาได้
ตรงนี้หลักการที่สำคัญที่สุดทำงาน เครื่องมือยืมจากบริษัทได้ แต่เนื้อหาเชิงโดเมนยืม 0 ชิ้น
การแบ่งแยกนี้ตรวจสอบได้ด้วย grep บันทึกความจำมีคำว่า "ยืมเนื้อหาเชิงโดเมนของโปรเจกต์บริษัท 0 ชิ้น (ตรวจสอบด้วย grep PASS)" อยู่ สีของ Critter Sort คือชมพู·มินต์·เหลือง และสัตว์คือแมว·หมี·กระต่าย คำศัพท์เชิงโดเมนของโปรเจกต์ A (MMORPG ของบริษัท) ไม่มีอยู่ในที่ใดเลยของ repository นี้
ทำไมต้องแบ่งแยกถึงขนาดนี้ เพื่อกันสองอุบัติเหตุพร้อมกัน อุบัติเหตุทางกฎหมายที่ IP ของบริษัทรั่วเข้าสู่งานอดิเรกส่วนตัว และการปนเปื้อนของบริบทที่ atom เชิงโดเมน MMORPG ถูกฉีดเข้าไปในงานพัซเซิลผิด ๆ จนกลายเป็นสัญญาณรบกวน ให้เครื่องมือไหลผ่านแต่กั้นเนื้อหา — ช่องว่างนั้นคือการแบ่งแยกที่ดีต่อสุขภาพ
Critter Sort เป็นเกมเล็ก ๆ สามวัน 5 คอมมิต สัตว์ 8 ชนิด ด่าน 60 ด่าน แต่วิธีที่ใช้ที่บริษัทก็ทำงานได้เหมือนกันแม้ในสเกล 1/1000
บทเรียนใหญ่ที่สุดคือความล้มเหลวของหัวข้อแรก ใน v0.1 ตัวตนของเกมตายไปครั้งหนึ่ง แล้วชุบชีวิตกลับมาด้วยการแก้ไขสองครั้ง ในการพัฒนาคนเดียวที่ไม่มีเพื่อนร่วมงาน สิ่งที่เป็นพยานต่อความตายและการฟื้นคืนนั้นคือ git commit ถ้าไม่มีการทบทวน หนึ่งเดือนต่อมาคงลืมไปแล้วว่า "ทำไมถึงรื้อทำใหม่หมดที่ v0.2"
ใน Part 24 ถัดไป ผมจะกล่าวถึงว่าจะตรึงบันทึกการตัดสินใจแบบนี้ให้กลายเป็นการกำกับดูแล (governance) ในทีมใหญ่และการให้บริการระยะยาวได้อย่างไร
บทนี้เป็นบันทึกที่ออกเดินทางจากพัซเซิลที่ภรรยาเบื่อจนปิดไป จนถึงตอนที่เกมซึ่งสร้างคนเดียวกลับเข้าไปอยู่ในมือเธออีกครั้ง ผมได้ยืนยันว่าระบบไม่ใช่ปัญหาของสเกล แต่เป็นปัญหาของระเบียบวินัย
เป็นขั้นของการลองหมุนลูปหลักของเกมแคชชวลที่คุณชอบสักหนึ่งเกมร่วมกับ AI โดยอย่าให้สูญเสียลายเซ็น
setup — ในสภาพแวดล้อมที่ติดตั้ง Node แล้ว สร้างโฟลเดอร์เปล่าขึ้นมาหนึ่งโฟลเดอร์ mkdir my-puzzle && cd my-puzzle
prompt — โยนให้ AI แบบนี้ แก่นคือ "อย่าสรุป แต่ระบุลายเซ็นให้ชัด"
"ผมอยากดัดแปลงลูปหลักของ [ชื่อเกม] ให้เป็น [ธีม] ลายเซ็นของเกมนี้คือ [เขียนสัมผัสเฉพาะตัวเป็นหนึ่งบรรทัด — เช่น 'สัมผัสของการหมุนจอเพื่อยืนยันสิ่งที่ถูกบังแล้วคลายออก'] อย่าทำให้มันแบนเป็นเกมจับคู่ทั่วไปเด็ดขาด เขียนลอจิกแยกจากการเรนเดอร์เพื่อให้ทดสอบแบบ headless ได้"
verify — ลองเล่นผลลัพธ์แรกด้วยตัวเอง ถามว่า "ลายเซ็นที่ผมเขียนไว้ยังอยู่ไหม" ถ้าไม่อยู่ ให้ เขียนนิยามใหม่ แล้วขอใหม่ ไม่ใช่แก้โค้ด นั่นคือสิ่งที่ผมทำในช่วง v0.1→v0.2
ไม่ต้องมีเอนจิน ไม่ต้องมีการสร้างโพรซีเดอรัล เขียน "ลายเซ็นของเกมนี้หนึ่งบรรทัด" ลงในกระดาษหนึ่งแผ่น ให้ AI ทำต้นแบบ แล้วเล่นเองเพื่อดูแค่ว่าบรรทัดนั้นยังอยู่หรือไม่ ถ้าตายไป ให้เขียนบรรทัดนั้นใหม่ให้เป็นรูปธรรมขึ้น นิสัยการรักษาลายเซ็นหนึ่งบรรทัด เพียงสิ่งเดียวนี้ก็หลีกเลี่ยงกับดักแรกของวิศวกรรมย้อนกลับได้
หลังจบสแตนด์อัปเช้าวันจันทร์ทันที สมาชิกทีม A จากทีมข้อมูลส่งภาพหน้าจอมาให้ทางแชตภายในทีม มันคือรายงาน QA ที่บอกว่าคำอธิบายของไอเทมวัตถุดิบบางตัวในร้านค้าในเกมว่างเปล่า หลังไล่ตามต้นเหตุอยู่ 30 นาที ตัวการก็ปรากฏ สองสัปดาห์ก่อน มีคนเปลี่ยนชื่อไอเทมนั้นในเอกสารออกแบบเป็น 재료_목재_상 แต่การอ้างอิงในชีตข้อมูลยังชี้ไปที่ชื่อเก่า 재료_목재_A อยู่อย่างเดิม เอกสารถูกอัปเดต ชีตไม่ถูกอัปเดต และลิงก์ที่เชื่อมทั้งสองเข้าด้วยกันก็ขาดไปอย่างเงียบ ๆ ไม่มีใครโกหกสักคน แต่เกมกลับแสดงสิ่งที่เป็นเท็จออกมา
อุบัติเหตุแบบนี้จะเกิดบ่อยขึ้นแบบทวีคูณเมื่อเอกสารเพิ่มจำนวนขึ้น สายตามนุษย์มองเห็นการอ้างอิงไขว้กันของเอกสาร 50 ฉบับพร้อมกันไม่ได้ ด้วยเหตุนี้เราจึงมอบหมายงานตรวจสอบให้โค้ดทำแทน บทนี้ว่าด้วยระบบที่ทำให้สคริปต์ ไม่ใช่มนุษย์ เป็นผู้ตรวจสอบความสอดคล้องของเอกสาร ข้อมูล และลิงก์ แก่นมีอยู่สามอย่าง — ความสอดคล้องของแหล่งที่มา (_source_map.tsv audit), ความสมบูรณ์ของลิงก์ (wikilink), และการตรวจจับ stale (จับการอ้างอิงที่เก่าจนเน่าเสีย)
เอกสารและข้อมูลมีชีวิตอยู่โดยชี้หากันและกัน เอกสารออกแบบอ้างอิง enum, enum อ้างอิงชีตข้อมูล, และชีตก็อ้างอิงการตัดสินใจของเอกสารออกแบบอีกฉบับ ถ้ามนุษย์จัดการตาข่ายนี้ด้วยมือ เมื่อโหนดหนึ่งเปลี่ยนไป มนุษย์ต้องจำการอ้างอิงทั้งหมดที่เคยชี้ไปยังโหนดนั้นและไล่ตามมันให้ได้ ความทรงจำล้มเหลว
เหตุผลที่ลิงก์ขาดเป็นอันตรายคือมัน ไม่โยน error ออกมา ถ้าเป็นโค้ด เมื่ออ้างอิงตัวแปรที่ไม่มีอยู่ คอมไพเลอร์จะกั้นให้ แต่ wikilink ที่เขียนว่า [[재료_목재_A]] ในเอกสาร แม้เป้าหมายจะหายไป มันก็แค่ยังคงเป็นข้อความธรรมดาอยู่อย่างนั้น ไม่กลายเป็นสีแดง เกมถูกบิลด์ ถูกปล่อยออก และกว่าจะมีคนสังเกตเห็นก็ต่อเมื่อผู้เล่นเห็นคำอธิบายที่ว่างเปล่าแล้ว
ด้วยเหตุนี้ งานแรกของระบบตรวจสอบจึงคือ การทำให้สิ่งที่สายตามนุษย์มองไม่เห็นกลายเป็นมองเห็นได้ ดึงการละเมิดความสอดคล้องออกมาเป็นเอาต์พุตข้อความ แล้วผูกเอาต์พุตนั้นไว้กับด่านบิลด์ เมื่อนั้นต่อให้มนุษย์ลืม สคริปต์ก็ไม่ลืม
การตรวจสอบไม่ใช่ก้อนเดียว แต่เป็นลำดับขั้น เริ่มจากรันการตรวจสอบที่ถูกที่สุดก่อนเพื่อกรองการละเมิดที่ชัดเจนออก แล้วส่งเฉพาะสิ่งที่ผ่านไปยังขั้นถัดไป เพราะถ้ารันการตรวจสอบที่แพงกับทุกอินพุต มันจะช้าจนไม่มีใครยอมรัน ด้านล่างคือกระแสการตรวจสอบที่ผู้เขียนใช้งานจริง
flowchart TD
A[บันทึกเอกสารและชีต] --> B{source_map audit}
B -- การแมปแหล่งที่มาขาดหาย --> B1[FAIL: ร่องรอยการแก้ไขด้วยมือ
ต้องอัปเดต _source_map.tsv]
B -- ผ่าน --> C{ความสมบูรณ์ของ wikilink}
C -- พบลิงก์ที่ขาด --> C1[wikilink_apply.py
พยายามซ่อม]
C1 -- ซ่อมอัตโนมัติได้ --> C
C1 -- ซ่อมไม่ได้ --> C2[FAIL: รายงานการอ้างอิงที่ขาด]
C -- ผ่าน --> D{ตรวจจับ stale}
D -- เก่ากว่าเป้าหมายที่อ้างอิง --> D1[WARN: ลงคิวทบทวนใหม่]
D -- ผ่าน --> E[integrity_check ขั้นสุดท้าย]
E -- ละเมิด P0 --> E1[BLOCK: กั้นด่านบิลด์]
E -- ผ่าน --> F[GREEN: อนุญาตให้ commit]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class B,C,C1,D,E code;
class A data;
class F pass;
class B1,C2,D1,E1 fail;
แก่นของ cascade นี้คือ ยิ่งล้มเหลวเร็ว ยิ่งถูก source_map audit เป็นการเทียบ TSV แค่บรรทัดเดียวจึงจบภายในหน่วยมิลลิวินาที ตรงกันข้าม integrity_check ขั้นสุดท้ายต้องโหลดชีตข้อมูลทั้งหมดเพื่อตรวจสอบความสัมพันธ์ FK จึงใช้เวลาหลายวินาที เมื่อวางการตรวจสอบที่ถูกไว้ข้างหน้า ความผิดพลาดที่ชัดเจนจะถูกตัดออกตรงนั้น และการตรวจสอบที่แพงก็จะรันเฉพาะกับอินพุตส่วนน้อยที่ผ่านมันมาได้
อีกประเด็นที่สำคัญคือเอาต์พุตของแต่ละขั้นต่างกัน audit ให้ FAIL (หลักฐานว่าผู้แก้ไขไปแตะอะไรบางอย่างด้วยมือ), wikilink ให้ FAIL หลังพยายามซ่อมอัตโนมัติ, stale ให้ WARN (ไม่ถึงกับกั้น แต่ต้องทบทวนใหม่), integrity_check ให้ BLOCK (กั้นตัวบิลด์เอง) แม้จะเป็น "ปัญหา" เหมือนกัน แต่ต้องตอบสนองต่างกันตามระดับความรุนแรง มนุษย์จึงจะแยกแยะสัญญาณออกจากสิ่งรบกวนได้
_source_map.tsv auditการตรวจสอบที่รันก่อนสุดคือความสอดคล้องของแหล่งที่มา ไปป์ไลน์สร้างเอกสารของผู้เขียนจะบันทึกไว้ใน _source_map.tsv ว่าเอกสารสังเคราะห์ใด (เช่น เนื้อหา GDD) มาจากไฟล์ต้นทางใด หนึ่งบรรทัดตรึงสายเลือด (lineage) ไว้ว่า "เซกชันของผลลัพธ์นี้ = การสังเคราะห์จากไฟล์ต้นทางเหล่านี้"
เหตุผลที่สิ่งนี้กลายเป็นเครื่องมือตรวจสอบได้ก็เพราะ เมื่อมนุษย์แก้ไขผลลัพธ์ด้วยมือ การแมปจะขาด หากมีใครไปแก้เซกชัน GDD ที่ถูกสร้างอัตโนมัติโดยตรง เซกชันนั้นก็จะไม่ใช่การสังเคราะห์ที่ซื่อตรงต่อไฟล์ต้นทางอีกต่อไป สคริปต์ audit จะเทียบแฮชของแต่ละเซกชันในผลลัพธ์กับแฮชที่สังเคราะห์ใหม่จากต้นทาง และถ้าไม่ตรงกันก็จะให้ FAIL กฎที่ว่า "แก้ไขด้วยมือ → audit FAIL" ออกมาจากตรงนี้
นี่ไม่ใช่การพยายามห้ามไม่ให้มนุษย์แก้ไข แต่เป็นการพยายาม ทำให้การแก้ไขแสดงตัวออกมาอย่างชัดเจน ถ้าจำเป็นต้องแก้ผลลัพธ์ ก็ให้เลือกอย่างใดอย่างหนึ่งระหว่างแก้ต้นทางแล้วสร้างใหม่ หรือถอดเซกชันนั้นออกจากการแมปอย่างเป็นทางการ (ประกาศแยก) สัญญาณนี้กำลังบอกแบบนั้น การทำให้การแก้ไขเงียบ ๆ กลายเป็นเสียงดัง นั่นคืองานของ audit
เมื่อผ่าน audit แล้วก็ข้ามไปยังการตรวจสอบลิงก์ เอกสารของผู้เขียนเชื่อมต่อโหนดด้วย wikilink แบบ Obsidian [[เป้าหมาย]] wikilink_apply.py ทำสองอย่าง — ตีความ wikilink เป็นเส้นทางจริงแล้วนำไปใช้ และซ่อมลิงก์ที่ขาดเท่าที่ทำได้ในขอบเขตที่เป็นไปได้
กรณีที่ซ่อมได้นั้นชัดเจน นั่นคือเมื่อโหนดเป้าหมาย เพียงแค่เปลี่ยนชื่อ แต่ยังคงอยู่ที่ตำแหน่งเดิม การเปลี่ยนชื่อแบบ 재료_목재_A → 재료_목재_상 ที่กล่าวมาก่อนหน้า ถ้าการแมปชื่อแทน (alias map) ถูกอัปเดตไว้ สคริปต์ก็จะแก้ชื่อเก่าให้เป็นชื่อใหม่โดยอัตโนมัติ ในทางกลับกัน ถ้าเป้าหมายถูกลบไปทั้งก้อน หรือไล่ตามไม่ได้ว่าย้ายไปไหน ก็จะยอมแพ้ในการซ่อมแล้วรายงานการอ้างอิงที่ขาดออกมา
ตรงนี้มีการตัดสินใจเชิงออกแบบอยู่หนึ่งข้อ ถ้าซ่อมอัตโนมัติแบบรุกเกินไป มันอันตราย ถ้าไปค้นหา "ชื่อที่คล้ายกัน" แล้วเชื่อมต่อตามอำเภอใจ ลิงก์อาจถูกต่อผิดไปยังโหนดที่มีความหมายต่างกัน ทำให้เกิดอุบัติเหตุที่แย่กว่าเดิม ด้วยเหตุนี้การซ่อมของ wikilink_apply.py จึงอนุรักษนิยม — แก้อัตโนมัติเฉพาะการเปลี่ยนชื่อที่มีการแมปชื่อแทนระบุไว้ชัดเจน ส่วนกรณีที่ต้องเดาก็โยนให้มนุษย์ คุณธรรมของการทำงานอัตโนมัติอยู่ที่ความพอเหมาะ — ทำอัตโนมัติเฉพาะสิ่งที่แน่ใจ และโยนสิ่งที่คลุมเครือให้มนุษย์อย่างซื่อตรง
ต่อให้ลิงก์ยังมีชีวิตอยู่ การอ้างอิงก็อาจเก่าได้ เอกสาร A อ้างอิงชีตข้อมูล B แต่ถ้า B ถูกอัปเดตทีหลัง A คำอธิบายของ A ก็มีโอกาสจะขัดกับ B ในปัจจุบัน ตัวลิงก์เองยังปกติดี เพราะเป้าหมายที่ชี้ไปยังมีอยู่ แต่เนื้อหาเน่าเสียแล้ว
การตรวจจับ stale จะเทียบเวลาที่แก้ไข (หรือเวอร์ชันของแฮชเนื้อหา) ของทั้งสองฝั่งของการอ้างอิง ถ้าฝั่งที่อ้างอิงเก่ากว่าเป้าหมายที่ถูกอ้างอิง ก็จะแสดง WARN และลงคิวทบทวนโหนดนั้นใหม่ เหตุผลที่เป็น WARN ไม่ใช่ BLOCK ก็เพราะการอัปเดตไม่ได้หมายถึงความขัดแย้งของเนื้อหาเสมอไป ถ้าเป็นการอัปเดตที่แค่แก้พิมพ์ผิดหนึ่งตัว การอ้างอิงก็ยังปกติดี ด้วยเหตุนี้ stale จึงไม่ใช่ "การกั้น" แต่เป็น "การติดป้ายให้เข้าไปดู"
ลองดูว่าขั้นนี้จับอุบัติเหตุลิงก์ขาดที่กล่าวมาก่อนหน้าได้อย่างไร ถ้าชีต 재료_목재 ถูกอัปเดตทีหลังเอกสาร ก่อนการซ่อมอัตโนมัติ stale WARN ก็คงขึ้นไปแล้ว นั่นคือการตรวจสอบทั้งสามเป็น ตาข่ายนิรภัยที่ซ้อนทับกัน สิ่งที่ตาข่ายหนึ่งพลาดไป ตาข่ายถัดไปจะจับ นี่คือเหตุผลที่ cascade จับอุบัติเหตุที่การตรวจสอบเดี่ยว ๆ จับไม่ได้
ถ้ามนุษย์ต้องเขียนตรรกะการตรวจสอบทั้งหมดเองตั้งแต่ต้น ก็จะเหนื่อย ผู้เขียนจะบรรยายกฎการตรวจสอบเป็นภาษาธรรมชาติ แล้วรับร่างแรกของสคริปต์ที่รันได้จาก AI ด้านล่างคือเซสชันจริงที่สร้างสคริปต์ตรวจจับ stale เอาต์พุตไม่ได้ถูกขัดเกลา แต่ปล่อยทิ้งกระบวนการล้มเหลวและการปฏิเสธไว้ตามจริง
[พรอมต์ฉบับเต็ม]
ไฟล์ Markdown ในโฟลเดอร์เอกสารเชื่อมต่อหากันด้วย wikilink
[[เป้าหมาย]]ช่วยสร้างสคริปต์ Python ที่ตรวจสอบว่าแต่ละเอกสารถูกอัปเดตเก่ากว่าไฟล์เป้าหมายที่มันอ้างอิงหรือไม่ (=stale) เงื่อนไข: - เวลาที่แก้ไขไฟล์ใช้เกณฑ์เวลาคอมมิตสุดท้ายของ git (mtime ของไฟล์ระบบเชื่อไม่ได้ เพราะถูกอัปเดตตอน checkout) - ลิงก์ที่ขาด (ไม่มีเป้าหมาย) ให้แยกออกจากการตรวจสอบ stale และรายงานต่างหาก - เอาต์พุตเป็น TSV:source<TAB>target<TAB>status(status = OK / STALE / BROKEN) - WARN ไม่กั้น โค้ดออก (exit code) คงเป็น 0 (ห้ามกั้นบิลด์)
[เอาต์พุตดิบของ Claude — รอบที่ 1]
สคริปต์ด้านล่างใช้
git log -1 --format=%ct <file>เพื่อหาเวลาคอมมิตสุดท้ายของแต่ละไฟล์ และดึง wikilink ออกมาด้วย regex\[\[([^\]]+)\]\]เพื่อเทียบกันimport subprocess, re, sys, pathlib def git_mtime(path): out = subprocess.run( ["git", "log", "-1", "--format=%ct", str(path)], capture_output=True, text=True) return int(out.stdout.strip()) def resolve(link, root): # [[เป้าหมาย]] -> root/เป้าหมาย.md return root / f"{link}.md" root = pathlib.Path(sys.argv[1]) for md in root.glob("*.md"): src_t = git_mtime(md) for link in re.findall(r"\[\[([^\]]+)\]\]", md.read_text(encoding="utf-8")): tgt = resolve(link, root) if not tgt.exists(): print(f"{md.name}\t{link}\tBROKEN") continue tgt_t = git_mtime(tgt) status = "STALE" if src_t < tgt_t else "OK" print(f"{md.name}\t{link}\t{status}")ทำแบบนี้ก็จะได้เอาต์พุตในรูปแบบ TSV ตามที่ร้องขอ
[การตรวจสอบโดยมนุษย์ / การปฏิเสธ]
หยุดตรงนี้ ผู้เขียนเห็นข้อบกพร่องสามอย่าง
ข้อแรก ถ้า git_mtime คืนค่าสตริงว่าง มันจะระเบิดที่ int("") ไฟล์ใหม่ที่ไม่เคยถูกคอมมิตเป็นแบบนั้น ข้อสอง glob("*.md") มองไม่เห็นโฟลเดอร์ย่อย เอกสารของผู้เขียนกระจายอยู่ใต้ part*/ ข้อสาม — สำคัญที่สุด — wikilink มีกรณีที่มีชื่อแทนคั่นด้วยไปป์แบบ [[เป้าหมาย|ชื่อที่แสดง]] และโค้ดรอบที่ 1 เข้าใจผิดว่า เป้าหมาย|ชื่อที่แสดง ทั้งก้อนเป็นชื่อไฟล์ ถ้าเป็นแบบนี้ ลิงก์ที่ปกติดีจะถูกจับเป็น BROKEN ทั้งหมด ถ้าเอาไปใช้ตามนั้นมันคือระเบิดสัญญาณเตือนเท็จ
ผู้เขียนชี้สามข้อนี้แล้วร้องขอใหม่
[การร้องขอใหม่]
ช่วยแก้สามอย่าง: (1) ไฟล์ที่ยังไม่ถูกคอมมิต git_mtime เป็น None กรณีนี้ให้พักการตัดสิน STALE ไว้แล้วใช้ status=UNTRACKED (2) ใช้ rglob แทน glob เพื่อรวมโฟลเดอร์ย่อย (3) จัดการไปป์
[[เป้าหมาย|ชื่อแทน]]— ใช้เฉพาะส่วนหน้า|เป็นเป้าหมาย และโค้ดออก (exit code) คงเป็น 0 แม้จะมี BROKEN (หลักการ WARN ไม่กั้น)
[เอาต์พุตดิบของ Claude — รอบที่ 2]
import subprocess, re, sys, pathlib def git_mtime(path): out = subprocess.run( ["git", "log", "-1", "--format=%ct", str(path)], capture_output=True, text=True) s = out.stdout.strip() return int(s) if s else None # ไม่มีประวัติคอมมิตก็คืน None def target_name(link): return link.split("|", 1)[0].strip() # [[เป้าหมาย|ชื่อแทน]] -> เป้าหมาย root = pathlib.Path(sys.argv[1]) for md in root.rglob("*.md"): src_t = git_mtime(md) text = md.read_text(encoding="utf-8") for raw in re.findall(r"\[\[([^\]]+)\]\]", text): name = target_name(raw) hits = list(root.rglob(f"{name}.md")) if not hits: print(f"{md.name}\t{name}\tBROKEN") continue tgt_t = git_mtime(hits[0]) if src_t is None or tgt_t is None: status = "UNTRACKED" elif src_t < tgt_t: status = "STALE" else: status = "OK" print(f"{md.name}\t{name}\t{status}") sys.exit(0) # WARN ไม่กั้น: คืน 0 เสมอ
เอาต์พุตรอบที่ 2 นี้ผู้เขียนรับไว้ แต่มีอยู่หนึ่งอย่างที่เป็นการตัดสินใจที่มนุษย์เหลือไว้ — ถ้า rglob(f"{name}.md") ค้นเจอไฟล์ชื่อเดียวกันในหลายโฟลเดอร์ มันจะใช้แค่ hits[0] นี่เป็นความกำกวมที่อาจเกิดขึ้นได้ แต่ตามกฎการตั้งชื่อไฟล์ในเอกสารของผู้เขียน ชื่อไฟล์เป็นค่าไม่ซ้ำกันทั้งระบบ จึงไม่มีการชนกันในงานจริง ผู้เขียนรับสมมติฐานนี้ที่ AI ไม่ได้ชี้ไว้อย่างมีสติแล้วเขียนกำกับไว้เป็นคอมเมนต์ ต่อให้เป็นโค้ดที่การทำงานอัตโนมัติเขียนขึ้น มนุษย์ก็รับผิดชอบต่อสมมติฐานที่โค้ดนั้นพึ่งพา
ต่อให้มีสคริปต์ ถ้าไม่มีใครรันก็ไร้ประโยชน์ การออกแบบขั้นสุดท้ายของการตรวจสอบคือ การทำให้ไม่รันไม่ได้ ผู้เขียนผูกสามขั้นไว้กับ hook ก่อน commit (pre-commit) หรือไปป์ไลน์บิลด์ audit FAIL และการละเมิด P0 ของ integrity_check เป็น BLOCK จึงกั้น commit/บิลด์ ส่วน wikilink BROKEN และ stale เป็น WARN จึงปล่อยให้ผ่านแต่ทิ้งรายงานไว้
การแบ่งสองทาง BLOCK/WARN นี้เป็นตัวกำหนดความอยู่รอดของระบบ ถ้ากั้นทุกอย่างเป็น BLOCK commit จะถูกกั้นเพราะ stale เล็ก ๆ น้อย ๆ เพียงหนึ่งตัว แล้วผู้คนก็จะเริ่มเลี่ยงตัวการตรวจสอบเอง การตรวจสอบที่ถูกเลี่ยงคือการตรวจสอบที่ไม่มีอยู่ ในทางกลับกัน ถ้าตั้งทุกอย่างเป็น WARN การละเมิดความสมบูรณ์ของข้อมูลที่ต้องกั้นจริง ๆ ก็จะผ่านไปเฉย ๆ เส้นแบ่งระหว่างสิ่งที่จะกั้นกับสิ่งที่จะเพียงติดป้าย คือจุดออกแบบที่แท้จริงของระบบตรวจสอบ
นี่คือแนวโน้มที่สังเกตได้จากโปรเจกต์ A ของบริษัทพัฒนา MMORPG A ที่ผู้เขียนดูแล โดยอิงเกณฑ์เอกสารราว 90 ฉบับ ตัวเลขสัมบูรณ์บางส่วนเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) และสิ่งที่มีความหมายคือแนวโน้ม
| รายการ | ยุคตรวจด้วยมือ | cascade ตรวจสอบด้วยโค้ด |
|---|---|---|
| จุดที่พบการอ้างอิงที่ขาด | หลังรายงานของผู้เล่น/QA | ก่อน commit (ทิศทาง: เกิดเหตุ → ก่อนเกิด) |
| เวลาที่ใช้ตรวจความสอดคล้องหนึ่งครั้ง | หลายชั่วโมง (ประมาณของผู้เขียน) | หลักสิบวินาที (วัดจริงด้วยสคริปต์) |
| stale ที่สะสมแฝงตัว | แฝงตัวหลายสัปดาห์ | WARN ที่ commit ถัดไป |
| อุบัติเหตุจากการซ่อมอัตโนมัติผิดพลาด | ไม่มี | คงไว้ 0 รายการด้วยการซ่อมแบบอนุรักษนิยม |
ผู้เขียนแนะนำให้เชื่อเฉพาะทิศทางที่ว่า "จุดที่พบถูกดึงจากหลังเกิดเหตุมาเป็นก่อนเกิดเหตุ" มากกว่าจะเชื่อตัวเลขตรงตัว คุณค่าที่แท้จริงของระบบตรวจสอบไม่ได้อยู่ที่การประหยัดเวลา แต่อยู่ที่ การย้ายตำแหน่งที่ทำให้จับอุบัติเหตุได้ก่อนที่มันจะไปถึงผู้เล่น
| รูปแบบ | วิธีแก้ |
|---|---|
| กั้นทุกการละเมิดเป็น BLOCK จนผู้คนเลี่ยงการตรวจสอบ | แบ่งสองทาง BLOCK/WARN กั้นเฉพาะ P0 ของความสมบูรณ์ของข้อมูล |
| ซ่อมอัตโนมัติแบบรุกไปจนถึงขั้นเดา | แก้อัตโนมัติเฉพาะการเปลี่ยนชื่อที่มีชื่อแทนระบุชัด คลุมเครือก็โยนให้มนุษย์ |
| ดูแค่ลิงก์ที่ขาดแล้วเมิน stale | ตรวจจับการอ้างอิงที่เก่าต่างหากด้วยการเทียบเวลาที่แก้ไข |
| ปล่อยให้แก้ไขผลลัพธ์ด้วยมืออย่างเงียบ ๆ | ทำให้การแก้ไขมองเห็นได้เป็น FAIL ด้วย source_map audit |
| มีสคริปต์แต่ไม่ผูกไว้กับ hook | เชื่อมกับ pre-commit/ด่านบิลด์ ทำให้ไม่รันไม่ได้ |
setup. จัดการโฟลเดอร์เอกสารด้วย git (เป็นเกณฑ์ในการเทียบเวลาคอมมิต) ทำ wikilink ให้เป็นมาตรฐานเดียวกันด้วยรูปแบบ [[เป้าหมาย]] หรือ [[เป้าหมาย|ชื่อแทน]]
prompt. ส่งพรอมต์ฉบับเต็มของบันทึกเซสชันข้างต้นให้ AI ตามนั้น แต่อย่าเอาเอาต์พุตแรกไปใช้ตรง ๆ เด็ดขาด คุณต้องตรวจสอบและปฏิเสธแล้วร้องขอใหม่สามอย่างนี้เสมอ — (1) การจัดการไฟล์ที่ยังไม่ถูกคอมมิต (2) การค้นหาโฟลเดอร์ย่อย (3) การพาร์สชื่อแทนแบบไปป์ นี่คือจุดที่ AI แทบจะพลาดในรอบแรกเสมอ
verify. รันสคริปต์เพื่อรับ TSV ตรวจด้วยมือว่าแถว BROKEN เป็นลิงก์ที่ขาดจริงหรือไม่ โดยสุ่มตัวอย่าง 5 รายการ ถ้าออกมาเป็น BROKEN เท็จ แสดงว่าการพาร์สชื่อแทน/โฟลเดอร์ย่อยยังไม่ครบ เมื่อยืนยันว่าปกติแล้วก็ผูกไว้กับ pre-commit hook โดยแยกโค้ดออก (exit code) ให้ WARN (STALE/BROKEN) ผ่าน และ BLOCK (P0 ของความสมบูรณ์ของข้อมูล) กั้น
ฉบับย่อสำหรับคนเดียว. ถ้าคุณเขียน GDD เล็ก ๆ คนเดียว cascade ทั้งชุดก็มากเกินไป เอาไปแค่ ขั้นตรวจจับ stale ขั้นเดียว ก็พอ แค่เทียบด้วยเวลาของ git ว่าเอกสารเก่ากว่าชีตข้อมูลหรือไม่ ก็จับอุบัติเหตุแบบ "นึกว่าแก้แล้วแต่ยังไม่ได้แก้" ได้เกือบทั้งหมด ส่วนการซ่อมอัตโนมัติและ source_map audit ค่อยเพิ่มเข้ามาตอนที่เอกสารเกิน 30 ฉบับจนตามด้วยมือไม่ไหวก็ได้
นักออกแบบเกมที่เพิ่งเข้าทำงานได้สามวันถามขึ้นมาว่า "พี่ครับ ภาพที่สรุปว่าระบบเหล่านี้ส่งผลต่อกันตามลำดับไหน มีอยู่ที่ไหนบ้างครับ" ผมลังเลอยู่ครู่หนึ่ง ภาพน่ะมีอยู่ มีรูปถ่ายไวต์บอร์ดที่ใครบางคนวาดไว้เมื่อราวครึ่งปีก่อนแปะอยู่ที่ไหนสักแห่งในวิกิ แต่ในภาพนั้นยังมีระบบที่ตอนนี้หายไปแล้วเหลืออยู่สองระบบ และขาดลูปหลักสามลูปที่ถูกเพิ่มเข้ามาภายหลัง สุดท้ายผมจึงตอบไปว่า "อย่าไปเชื่อภาพ ให้อ่านเอกสารเอา" เป็นคำตอบที่น่าอาย ในจังหวะที่ภาพไม่ตรงกับเอกสาร ภาพก็เลิกเป็นข้อมูล แล้วกลายเป็นข้อมูลผิดทันที
ขอพูดข้อสรุปของบทนี้ก่อน เป็นอย่างนี้ ไดอะแกรมที่คนวาดเองจะต้องเน่าเสียภายในหนึ่งถึงสองเดือนอย่างหลีกเลี่ยงไม่ได้ ดังนั้นจึงต้องถอดงานวาดไดอะแกรมออกจากมือคน แล้วทำให้โครงสร้างของเอกสารเองคายภาพของตัวเองออกมา บทความนี้จะแสดงกระบวนการนั้นผ่านบันทึกการทำงานจริงครั้งหนึ่ง โดยนำบันทึกเซสชันจริง (worked transcript) ที่รับเอกสารเป็นอินพุตแล้วสร้างโค้ด Mermaid มาลงไว้ทั้งก้อน และเรนเดอร์ไดอะแกรมที่ดึงออกมานั้นจริง ๆ ในหน้านี้ พูดอีกอย่างคือ บทความที่อธิบายเทคนิคหนึ่ง พิสูจน์ตัวเองด้วยผลผลิตของเทคนิคนั้นนั่นเอง
เครื่องมือทำไดอะแกรมมีมากมาย ตั้งแต่ draw.io, Figma, Visio ไปจนถึงรูปถ่ายไวต์บอร์ด เครื่องมือเหล่านี้มีกับดักร่วมกันอยู่อย่างหนึ่ง นั่นคือผลลัพธ์เป็นไฟล์ภาพ (รูปภาพ) ภาพไม่สามารถให้ git ติดตามการเปลี่ยนแปลงทีละบรรทัดได้ LLM ที่จัดการกับข้อความก็ไม่สามารถสร้างหรือแก้ไขได้โดยตรง และไม่สามารถถูกฝังเป็นโค้ดในเอกสาร Markdown ได้ ในมุมมองของการดำเนินงาน สิ่งที่ร้ายแรงที่สุดคือข้อแรก ภาพที่ติดตามไม่ได้ว่าใครเปลี่ยน เปลี่ยนเมื่อไหร่ และเปลี่ยนทำไม เมื่อเวลาผ่านไปก็จะกลายเป็นซากที่ไม่มีใครรับผิดชอบ
Mermaid แก้ทั้งสามเรื่องนี้ได้ในคราวเดียว เขียนไดอะแกรมเป็นข้อความ ส่วนการเรนเดอร์ปล่อยให้วิวเวอร์จัดการเอง เพราะเป็นข้อความ git diff จึงจับได้แม้กระทั่งการเพิ่มโหนดเข้ามาเพียงโหนดเดียว เพราะเป็นข้อความ LLM จึงอ่านและเขียนได้ เพราะเป็นข้อความ มันจึงเข้าไปอยู่ในบล็อกโค้ด Markdown ได้ตรง ๆ เนื้อหาของบทนี้เองคือหลักฐานนั้น ไดอะแกรมที่จะปรากฏในไม่ช้าใต้ประโยคที่คุณกำลังอ่านอยู่นี้ ล้วนเป็นบล็อกข้อความภายใน Markdown ทั้งสิ้น และถูกเรนเดอร์เป็นภาพในกระบวนการ build หนังสือ
แต่ก็ต้องป้องกันความเข้าใจผิดไว้ด้วย ไม่จำเป็นเลยที่จะต้องทำเอกสารการดำเนินงานทุกชิ้นให้เป็นไดอะแกรม การนำรายการมาเรียงต่อกัน ใช้หัวข้อย่อย (bullet) จะเร็วกว่า การเปรียบเทียบตัวเลข ใช้ตารางจะเร็วกว่า ที่ที่ Mermaid ชนะมีอยู่แค่สามอย่าง ความสัมพันธ์ (อะไรเชื่อมกับอะไร) การไหล (อะไรมาหลังอะไร) และซีเควนซ์ (ใครส่งอะไรให้ใครเมื่อไหร่) ถ้าฝืนยัดไดอะแกรมเข้าไปในที่ที่ไม่ใช่สามอย่างนี้ กลับจะเพิ่มภาระทางความคิดเสียมากกว่า
จากตรงนี้ไปคือกระดูกสันหลังของบทนี้ แทนที่จะอธิบายแบบนามธรรม จะแสดงกระบวนการแปลงเอกสารจริงก้อนหนึ่งให้เป็น Mermaid ตั้งแต่ต้นจนจบ อินพุตคือชิ้นส่วน Markdown ที่บันทึกโครงสร้างการพึ่งพา (dependency) ระหว่างระบบ ซึ่งอยู่ในเอกสารการดำเนินงานของโปรเจกต์ A (ด้านล่างเป็นข้อความที่ตัดมาจริงและทำให้ไม่ระบุตัวตนแล้ว)
# บันทึกการพึ่งพาของระบบ (ตัดมาจากเอกสารการดำเนินงาน ทำให้ไม่ระบุตัวตน)
- combat_core พึ่งพา stat_engine
- skill_runtime พึ่งพา combat_core
- skill_runtime พึ่งพา vfx_pool
- quest_director พึ่งพา skill_runtime
- quest_director พึ่งพา dialog_graph
- economy_loop สมัครรับ (subscribe) reward hook ของ quest_director
- economy_loop อ่านสแตตที่ derive มาจาก stat_engine
ถ้านำสิ่งนี้มาวาดเป็นไดอะแกรมด้วยมือ ก็จะได้โหนดเจ็ดโหนดกับลูกศรเจ็ดเส้น วาดครั้งเดียวยังพอวาดได้ ปัญหาอยู่ที่สัปดาห์หน้า เมื่อมีการเพิ่มระบบ mail_box และ dialog_graph ถูกแยกออกเป็นสอง ภาพที่วาดด้วยมือจะเริ่มโกหกตั้งแต่จังหวะนั้น ด้วยเหตุนี้จึงไม่ให้คนทำ แต่ให้ LLM เป็นผู้แปลงแทน
ด้านล่างคือพรอมต์ที่ผมโยนเข้าไปจริง ๆ ลงไว้ตามนั้นทั้งหมดโดยไม่ขัดเกลาแม้แต่ตัวอักษรเดียว
ช่วยแปลงบันทึกการพึ่งพาของระบบด้านล่างให้เป็น Mermaid graph (บนลงล่าง, graph TB) กฎคือ:
1. ใช้เฉพาะระบบที่ปรากฏในบันทึกเป็นโหนด ห้ามเพิ่มระบบใหม่
2. "A พึ่งพา B" ให้เป็น A --> B
3. การเชื่อมโยงแบบหลวม เช่น "สมัครรับ" "อ่าน" ให้ใช้เส้นประ -.-> แล้วใส่ชื่อความสัมพันธ์กำกับ
4. id ของโหนดให้ใช้ชื่อภาษาอังกฤษในบันทึกตามนั้น อย่าใส่ป้ายภาษาเกาหลี
เอาเฉพาะบล็อกโค้ด ตัดคำอธิบายและคอมเมนต์ออก
[บันทึก]
- combat_core พึ่งพา stat_engine
- skill_runtime พึ่งพา combat_core
- skill_runtime พึ่งพา vfx_pool
- quest_director พึ่งพา skill_runtime
- quest_director พึ่งพา dialog_graph
- economy_loop สมัครรับ (subscribe) reward hook ของ quest_director
- economy_loop อ่านสแตตที่ derive มาจาก stat_engine
กฎข้อ 1 และข้อ 4 คือหัวใจ อุบัติเหตุที่พบบ่อยที่สุดเวลามอบงานวาดไดอะแกรมให้ LLM คือ "อาการหลอนแบบใจดี (hallucination)" มันจะเพิ่มโหนดที่ไม่ได้สั่ง ("ปกติมักจะมีระบบล็อกอินด้วย เลยใส่เข้าไปให้ครับ") และใส่ป้ายภาษาเกาหลีตามใจชอบ จนทำให้ ID กับการค้นหาในเนื้อหาไม่ตรงกัน เราใช้กฎมาตัดความใจดีนั้นออกไป
ต่อไปนี้คือคำตอบที่โมเดลส่งกลับมาครั้งแรก ลงไว้ตามนั้นโดยไม่ได้ขัดเกลา ในนั้นมีปัญหาเล็ก ๆ ปนอยู่หนึ่งอย่าง และผมจะลงไว้ทั้งปัญหานั้นด้วย
graph TB
skill_runtime --> combat_core
combat_core --> stat_engine
skill_runtime --> vfx_pool
quest_director --> skill_runtime
quest_director --> dialog_graph
economy_loop -.สมัครรับ reward hook.-> quest_director
economy_loop -.อ่านสแตตที่ derive มา.-> stat_engine
ไวยากรณ์ไม่มีปัญหา และโหนดก็มีเจ็ดโหนดพอดี ไม่ได้กุระบบใหม่ขึ้นมา กฎข้อ 1 และ 4 ถูกทำตาม แต่มีอยู่อย่างหนึ่งที่สะดุดใจ มีภาษาเกาหลีอยู่ในป้ายของเส้นประ (สมัครรับ reward hook, อ่านสแตตที่ derive มา) นี่ไม่ใช่การละเมิดกฎ เพราะกฎข้อ 3 บอกให้ "ใส่ชื่อความสัมพันธ์กำกับ" โมเดลจึงทำตามอย่างซื่อสัตย์ เพียงแต่ผมรู้จากประสบการณ์ว่า ใน Mermaid ถ้าป้ายภาษาเกาหลีมีช่องว่างคั่นอยู่ เรนเดอร์เวอร์ชันเก่าบางตัวจะแสดงผลพัง โมเดลไม่มีทางรู้เรื่องนั้นได้
ตรงนี้คือขั้นตอนที่ถูกข้ามบ่อยที่สุดในการทำงานอัตโนมัติ แต่กลับสำคัญที่สุด ผมไม่ได้รับผลลัพธ์ไปตามนั้น แต่ปฏิเสธ เหตุผลที่ปฏิเสธมีสองข้อ
ข้อแรก ต้องเปลี่ยนช่องว่างภาษาเกาหลีในป้ายของเส้นประให้เป็นโทเค็นภาษาอังกฤษ เพื่อให้มั่นใจถึงความเข้ากันได้ของการเรนเดอร์ ข้อสอง การเชื่อมโยงแบบหลวม (เส้นประ) และการเชื่อมโยงแบบแน่น (เส้นทึบ) ปนกันอยู่ในภาพเดียว แต่ไม่มีการแยกด้วยสีหรือสไตล์ จึงมองไม่เห็นในทันที ผมถือสองข้อนี้กลับไปร้องขออีกครั้ง
เกือบดีแล้ว แก้แค่สองอย่าง
1. เปลี่ยนป้ายของลูกศรเส้นประให้เป็นคำภาษาอังกฤษคำเดียว (ไม่มีช่องว่าง)
"สมัครรับ reward hook" -> subscribes, "อ่านสแตตที่ derive มา" -> reads
เหตุผล: เรนเดอร์บางตัวพังกับป้ายเส้น (edge) ที่เป็นภาษาเกาหลี+ช่องว่าง
2. เพื่อแยกโหนดเส้นประ (การเชื่อมโยงแบบหลวม) กับโหนดเส้นทึบ (การเชื่อมโยงแบบแน่น) ให้เห็นด้วยสายตา
ให้ใส่สไตล์สีเทาอ่อนด้วย classDef ให้กับโหนดที่มีแต่การเชื่อมโยงแบบหลวมอย่างเช่น economy_loop
3. ที่เหลือคงไว้ตามเดิม
graph TB
skill_runtime --> combat_core
combat_core --> stat_engine
skill_runtime --> vfx_pool
quest_director --> skill_runtime
quest_director --> dialog_graph
economy_loop -.subscribes.-> quest_director
economy_loop -.reads.-> stat_engine
classDef weak fill:#eee,stroke:#999,stroke-dasharray:4 3;
class economy_loop weak;
ครั้งนี้ผมรับไว้ ป้ายถูกเปลี่ยนเป็นโทเค็นภาษาอังกฤษคำเดียว และมีเพียง economy_loop ที่หลุดออกมาเป็นสีเทา ทำให้ข้อมูลที่ว่า "ระบบนี้ไม่ได้พึ่งพาโดยตรง แต่เป็นระบบขอบนอกที่เกี่ยวพันด้วยการสมัครรับและการอ่านเท่านั้น" ถูกสื่อออกมาด้วยสี ถ้าผมไม่ได้แตะพรอมต์แม้แต่บรรทัดเดียวแล้ววาดด้วยมือ ก็มีความเป็นไปได้สูงที่ผมจะนึกถึง classDef ตัวนี้ไม่ออกด้วยซ้ำ
ผลลัพธ์สุดท้ายของบันทึกเซสชันด้านบน ลงไว้บนหน้าหนังสือเล่มนี้เป็นบล็อกโค้ดตามนั้นโดยไม่ลอกด้วยมือ การ build หนังสือจะวาดสิ่งนี้ออกมาเป็นภาพ นี่คือของจริงของคำว่า "พิสูจน์ตัวเองด้วยเทคนิคของตัวเอง"
graph TB
skill_runtime --> combat_core
combat_core --> stat_engine
skill_runtime --> vfx_pool
quest_director --> skill_runtime
quest_director --> dialog_graph
economy_loop -.subscribes.-> quest_director
economy_loop -.reads.-> stat_engine
classDef weak fill:#eee,stroke:#999,stroke-dasharray:4 3;
class economy_loop weak;
ข้อความที่ตัดมาจากเอกสารก้อนหนึ่ง ผ่านการโต้ตอบไปมาห้าครั้ง กลายเป็นทรัพย์สินด้านการดำเนินงานที่เข้าไปอยู่ใน git ได้ ที่ LLM อัปเดตได้ และที่ถูกเรนเดอร์บนหน้านี้ สัปดาห์หน้าถ้ามีการเพิ่ม mail_box ก็แค่เขียนเพิ่มหนึ่งบรรทัดในบันทึก แล้วโยนพรอมต์เดิมเข้าไปอีกครั้งก็พอ ไม่มีจังหวะที่คนต้องหยิบปากกาขึ้นมาเลย
ถ้าไดอะแกรมก่อนหน้านี้คือ "ผลลัพธ์ของการแปลง" คราวนี้คือ "กระบวนการของการแปลง" ผมนำขั้นตอนของบันทึกเซสชันจริงที่เพิ่งดำเนินไปห้าขั้นมาทำเป็นผังการไหล (flowchart) ไดอะแกรมนี้ก็ให้ LLM ดึงออกมาด้วยวิธีเดียวกัน และผ่านการตรวจสอบแบบเดียวกัน ผมจะลงผลลัพธ์นั้นตามนั้น
flowchart TD
SRC[ข้อความที่ตัดจากเอกสารการดำเนินงาน] --> PROMPT[เขียนพรอมต์การแปลง]
PROMPT --> LLM[ผลลัพธ์ดิบของ Claude]
LLM --> CHECK{การตรวจสอบโดยมนุษย์}
CHECK -->|ปฏิเสธ: ความเข้ากันได้ของการเรนเดอร์·การอ่านง่าย| REASK[พรอมต์การร้องขอใหม่]
REASK --> LLM
CHECK -->|อนุมัติ| EMBED[ฝังบล็อกโค้ดลงใน Markdown]
EMBED --> GIT[git commit·ติดตาม diff]
GIT -->|เมื่อเอกสารเปลี่ยน| SRC
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class LLM ai;
class PROMPT,CHECK,REASK human;
class SRC,EMBED,GIT data;
ผังการไหลนี้บอกอยู่อย่างหนึ่ง สิ่งที่ผมอยากเน้นด้วยลูกศรหนา ไม่ใช่เส้นประ คือรูปสี่เหลี่ยมขนมเปียกปูนตรงกลาง นั่นคือ การตรวจสอบโดยมนุษย์ ถ้าหลงระเริงไปกับคำว่าอัตโนมัติแล้วตัดโหนดนี้ทิ้ง อาการหลอนแบบใจดีในขั้นที่ 1 ก็จะถูกบรรจุลงในเอกสารการดำเนินงานไปตรง ๆ การทำงานอัตโนมัติปลดปล่อยคนจากการวาดภาพ แต่ไม่ได้ปลดปล่อยคนจากการตัดสินใจ ลูกศรเส้นสุดท้ายของลูป (เมื่อเอกสารเปลี่ยน → ข้อความที่ตัดจากเอกสารการดำเนินงาน) คือหัวใจ ต้องมีวงป้อนกลับ (feedback loop) นี้ ไดอะแกรมจึงจะไม่ใช่ของใช้ครั้งเดียวทิ้ง แต่เป็นทรัพย์สินที่เติบโตไปพร้อมเอกสารโดยไม่แก่ลง
การแปลงด้วย LLM นั้นยืดหยุ่น แต่ในกรณีที่ความสัมพันธ์มีอยู่เป็นข้อมูลที่มีรูปแบบ (structured) อยู่แล้ว ก็ไม่จำเป็นต้องเรียกใช้โมเดลให้เปลืองเปล่า ข้อมูลที่มีฟิลด์ตายตัว เช่น decision card ของโปรเจกต์ A สคริปต์ Python เล็ก ๆ จะเร็วกว่าและซื่อสัตย์กว่า (อาการหลอนเป็นไปไม่ได้ตั้งแต่ต้นทาง) ด้านล่างคือส่วนหัวใจของสคริปต์จริงที่แปลงรายการ decision card เป็น Mermaid graph ของกราฟการตัดสินใจ
# decision_graph_to_mermaid.py
# แปลง decision card (ข้อมูลที่มีรูปแบบ) -> Mermaid graph ไม่ต้องใช้ LLM เป็นเชิงกำหนด
def to_mermaid(decisions):
lines = ["graph LR"]
# 1) ประกาศโหนด: ใช้ id และ title ตามนั้น ไม่กุขึ้นมา
for d in decisions:
safe_title = d.title.replace('"', "'") # escape เฉพาะเครื่องหมายคำพูด
lines.append(f' {d.id}["{safe_title}"]')
# 2) เส้น (edge): นำชนิดความสัมพันธ์มาเป็นป้ายของลูกศร
for d in decisions:
for rel in d.relations:
lines.append(f' {d.id} -->|{rel.type}| {rel.target}')
return "\n".join(lines)
หัวใจคือมันจบลงด้วยเพียงสองขั้นตอน ประกาศโหนด แล้วเชื่อมเส้น โหนดที่ไม่มีในอินพุตจะไม่มีทางปรากฏในเอาต์พุตเด็ดขาด เมื่อสคริปต์นี้รับ decision card สามใบเข้าไป ก็จะได้กราฟแบบด้านล่างออกมา
graph LR
D_A["Global Cooldown 0.3 วินาที"] -->|superseded_by| D_B["Global Cooldown 0.5 วินาที"]
D_B -->|relates_to| D_C["อนุญาตข้อยกเว้นช่วงฟื้นตัว"]
D_B -->|side_effect| D_D["ดาเมจสกิลระยะประชิด -5%"]
classDef human fill:#fde68a,stroke:#b45309,color:#000;
class D_A,D_B,D_C,D_D human;
การตัดสินใจหนึ่งถูกแทนที่ด้วยการตัดสินใจอีกอันหนึ่ง (superseded_by) และผลข้างเคียงที่แตกออกมาจากตรงนั้น (side_effect) ก็มองเห็นได้ด้วยลูกศรเส้นเดียว โดยไม่ต้องอ่าน log การตัดสินใจที่เขียนเป็นข้อความหลายสิบบรรทัดให้ครบ แค่กราฟใบนี้ใบเดียว ประวัติของคำถามที่ว่า "ทำไมตอนนี้คูลดาวน์จึงเป็น 0.5 วินาที" ก็จับได้ภายในห้านาที
เมื่อไหร่ใช้ LLM และเมื่อไหร่ใช้สคริปต์ เกณฑ์นั้นเรียบง่าย ถ้าอินพุตเป็นข้อมูลที่มีรูปแบบ (การ์ดหรือชีตที่มีฟิลด์ตายตัว) ให้ใช้สคริปต์ ถ้าอินพุตเป็นข้อความอิสระ (บันทึกการประชุม·บันทึกย่อ·บทสนทนา) ให้ใช้ LLM ถ้าใช้ LLM กับข้อมูลที่มีรูปแบบ ก็จะแบกความเสี่ยงอาการหลอนที่ไม่จำเป็นไว้เปล่า ๆ และถ้าใช้สคริปต์กับข้อความอิสระ กฎการ parse ก็จะเพิ่มขึ้นไม่รู้จบ
นี่คือกับระเบิดที่เหยียบมาจริงระหว่างการดำเนินงานระบบทำไดอะแกรมอัตโนมัติ
ข้อแรก กับดักของการซับซ้อนเกินไป เมื่อโหนดเกินห้าสิบ ภาพจะไม่ช่วยการรับรู้อีกต่อไป แต่กลับขัดขวางการรับรู้ วิธีรับมือคือจำกัดให้อยู่ระหว่างยี่สิบถึงสามสิบโหนดต่อหนึ่งหน้าจอ และถ้าใหญ่กว่านั้นก็มัดพื้นที่ด้วย subgraph หรือไม่ก็แยกไดอะแกรมออกเป็นสองอันไปเลย
ข้อสอง กับดักของการอัปเดตที่ขาดช่วง เรามักคิดว่าเรื่องนี้เกิดเฉพาะกับภาพที่วาดด้วยมือ แต่ถึงจะทำเป็นอัตโนมัติไว้แล้ว ถ้าไม่แก้เอกสารอินพุตมันก็เน่าเสียเหมือนกัน วิธีรับมือคือวงป้อนกลับที่อยู่ในผังการไหลก่อนหน้านี้ ทำให้เอกสารอินพุตเป็นแหล่งความจริงเดียว (single source of truth) แล้วสร้างไดอะแกรมขึ้นใหม่จากตรงนั้นเสมอ
ข้อสาม กับดักของการนามธรรมเกินไป ภาพระดับ "ระบบต่าง ๆ เกี่ยวพันกันคร่าว ๆ ประมาณนี้" นั้นสวยแต่ไร้ประโยชน์ วิธีรับมือคือใส่ ID จริง (skill_runtime, D_B) ลงในโหนดแทนคำนามนามธรรม การค้นหาในเนื้อหากับไดอะแกรมต้องใช้ตัวระบุเดียวกัน จึงจะกระโดดจากภาพไปยังโค้ดได้โดยตรง
ข้อสี่ กับดักของการใช้ผลลัพธ์ LLM ไปตรง ๆ โดยไม่ตรวจสอบ ดังที่เห็นในขั้นที่ 3 ของกระดูกสันหลัง โมเดลสามารถสร้างป้ายที่ทำตามกฎทุกข้อแต่เรนเดอร์แล้วพังได้ วิธีรับมือคืออย่าถอดโหนดการตรวจสอบโดยมนุษย์ออกจากไปป์ไลน์เด็ดขาด
มีแรงยั่วยวนให้หยิบตัวเลขขึ้นมา แต่ตรงนี้จะพูดเพียงทิศทาง การเปรียบเทียบด้านล่างเป็นการเปลี่ยนแปลงที่ผู้เขียนรู้สึกได้จากทีมที่ผู้เขียนดูแล ไม่ใช่ค่าที่วัดอย่างแม่นยำ แต่เป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ)
สิ่งที่เปลี่ยนชัดเจนที่สุดคือความเร็วในการทำความเข้าใจระบบของพนักงานใหม่ การจับประเด็นที่ว่า "ระบบเหล่านี้เกี่ยวพันกันอย่างไร" ซึ่งเคยใช้เวลาหลายวันในช่วงแรกของการเข้าทำงาน ลดเหลือราวหนึ่งชั่วโมงเมื่อยืนอยู่หน้ากราฟการพึ่งพาที่ถูกสร้างอัตโนมัติใบเดียว การเตรียมเอกสารประชุมก็เบาลง เมื่อก่อนมีคนต้องวาดภาพใหม่ด้วยมือในคืนก่อนประชุม แต่ตอนนี้แค่แปลงเอกสารหนึ่งครั้งก็จบ และที่สำคัญที่สุด คำถามที่ว่า "ภาพนี้เชื่อได้ไหม" ซึ่งเคยเกิดขึ้นเวลาไดอะแกรมไม่ตรงกับของจริง แทบจะหายไปเลย เพราะเอกสารอินพุตคือภาพ ถ้าเอกสารถูก ภาพก็ถูกตามไปด้วย
ในทางกลับกัน ถ้าจะพูดอย่างตรงไปตรงมา การทำงานอัตโนมัติไม่ใช่ยาครอบจักรวาล การร่างไอเดียในขั้นต้นที่ยังมีรูปแบบไม่ชัด ไวต์บอร์ดยังเร็วกว่าอยู่ดี การทำงานอัตโนมัติจะเปล่งประกายหลังจากที่โครงสร้างแข็งตัวลงในระดับหนึ่งแล้ว
setup. มีรีโพ Markdown สำหรับเก็บเอกสารหนึ่งที่ และวิวเวอร์ที่เรนเดอร์ Mermaid ได้ (มีอยู่ในตัววิวเวอร์ Markdown ส่วนใหญ่และ git hosting) ก็เพียงพอแล้ว เลือกชิ้นส่วนที่ตรงกับ "ความสัมพันธ์·การไหล·ซีเควนซ์" จากเอกสารที่จะแปลงมาหนึ่งชิ้น (เช่น บันทึกการพึ่งพาของระบบ)
prompt. นำชิ้นส่วนนั้นใส่เข้าไปในแม่แบบพรอมต์ของขั้นที่ 1 ในกระดูกสันหลังของเนื้อหา แล้วโยนให้ LLM ต้องใส่กฎสองข้อนี้ด้วยเสมอ คือ "อย่าเพิ่มโหนดที่ไม่มีในบันทึก" และ "ID ให้ใช้ชื่อภาษาอังกฤษในต้นฉบับตามนั้น" ถ้าเป็นข้อมูลที่มีรูปแบบ ให้แปลงด้วยสคริปต์เชิงกำหนดอย่างเช่น decision_graph_to_mermaid.py แทน LLM
verify. นำบล็อกโค้ดผลลัพธ์ไปแปะใน Markdown แล้วลองเรนเดอร์จริง ตรวจสอบสามอย่าง (1) มีโหนดที่ไม่มีในอินพุตเกิดขึ้นหรือไม่ (2) ป้ายของเส้น (edge) ถูกวาดออกมาโดยไม่พังหรือไม่ (3) ID ที่ใช้ในเนื้อหากับ ID ในไดอะแกรมตรงกันหรือไม่ ถ้ามีสักอย่างที่ไม่ตรง ให้ปฏิเสธด้วยพรอมต์การร้องขอใหม่แล้วรับมาใหม่ ถ้าผ่านก็ commit ลง git — ทีนี้การเปลี่ยนแปลงก็จะถูกติดตามด้วย diff
ถ้าเป็นผู้ทำงานคนเดียวที่ไม่มีทั้งทีมและสคริปต์ ก็ย่อลงแบบนี้ จดความสัมพันธ์ระหว่างระบบ·สิ่งที่ต้องทำ·ไอเดีย ลงในแอปโน้ตเป็นหัวข้อย่อยในรูปแบบ "A พึ่งพา B" สัปดาห์ละครั้ง คัดลอกรายการนั้นทั้งก้อน แล้วโยนหนึ่งบรรทัดว่า "ช่วยแปลงสิ่งนี้เป็น Mermaid graph TB หน่อย อย่าเพิ่มโหนดที่ไม่มีในรายการ" นำบล็อกโค้ดที่ได้กลับมาแปะไว้บนสุดของโน้ต จบแค่นั้น เพราะไม่ได้วาดด้วยมือจึงไม่มีภาระในการอัปเดต และตราบใดที่รายการอินพุตยังมีชีวิตอยู่ ภาพก็เป็นปัจจุบันเสมอ
การเชื่อมโยง (wikilink) และการจัดหมวด (ลำดับชั้น) คือสองทางเข้าของปัญหาเดียวกัน ฝั่งหนึ่งตอบว่า "การตัดสินใจนี้นำไปสู่ที่ใด" อีกฝั่งหนึ่งตอบว่า "เอกสารนี้อยู่ที่ไหน"
นักออกแบบเกมที่เพิ่งเข้าร่วมทีมถามขึ้นในเช้าวันที่สอง "ค่าคูลดาวน์รวมของการต่อสู้คือ 0.5 วินาทีใช่ไหมครับ แล้วมีเอกสารไหนเป็นหลักฐานบ้าง" ผมตอบไม่ได้ ทั้งที่บันทึกการตัดสินใจต้องอยู่ที่ใดที่หนึ่งแน่ ๆ แต่จำไม่ได้ว่ามันคือ rulebook ของการต่อสู้ บันทึกการประชุม หรือรายงานรายไตรมาส เราสามคนช่วยกันค้นทั้งโฟลเดอร์ด้วย grep ตัวเลขเดียวกันโผล่ขึ้นมาในหกที่ และในจำนวนนั้นแยกไม่ออกว่าอันไหนคือ "การตัดสินใจต้นฉบับ" และอันไหนคือ "สำเนาที่อ้างอิง" เราใช้เวลาไป 40 นาที สุดท้ายสิ่งที่ค้นเจอคือบรรทัดเดียวที่ฝังอยู่ในบันทึกการประชุม
เย็นวันนั้นผมตระหนักว่ามีสองสิ่งที่ขาดหายไป สิ่งแรกคือ การเชื่อมโยงอย่างชัดเจน ระหว่างเอกสารต่าง ๆ ไม่มี ต่อให้ตัวเลขเดียวกันอยู่ในหกที่ ก็ไม่มีเส้นด้ายที่ใดบันทึกไว้ว่า "อันนี้อ้างมาจากที่นั่น" สิ่งที่สองคือ ลำดับชั้น ที่เอกสารอยู่อาศัยไม่มี บันทึกการตัดสินใจกระจัดกระจายอยู่ใน rulebook บันทึกการประชุม และรายงาน โดยไม่มีข้อตกลงว่า "การตัดสินใจอยู่ที่นี่"
สองสิ่งนี้คือหัวข้อของบทนี้ wikilink เขียนการเชื่อมโยงลงเป็นข้อความ ส่วนลำดับชั้นกำหนดการจัดหมวดด้วยโฟลเดอร์ ทั้งสองดูเหมือนเทคนิคที่แยกจากกัน แต่แท้จริงแล้วเป็นสองด้านของปัญหาเดียวกันคือการค้นหา
เมื่อมีเอกสาร 30 ฉบับ เรายังจำได้ทั้งหมดด้วยหัว เมื่อเกิน 100 ฉบับ ความทรงจำของคนทำหน้าที่เป็นดัชนีไม่ได้อีกต่อไป ตอนนั้นสิ่งที่พึ่งพาได้มีหนึ่งในสองอย่าง ค้นทั้งหมดด้วย grep (ช้าและไม่แม่นยำ) หรือเดินตามการเชื่อมโยงอย่างชัดเจนที่เขียนไว้ในเอกสาร (เร็วและแม่นยำ)
เหตุผลที่ grep ไม่แม่นยำนั้นง่าย ๆ เมื่อค้นสตริง combat_global_cooldown_constant เอกสารที่ ตัดสินใจ ค่านั้นกับเอกสารที่เพียง กล่าวถึง ค่านั้นจะถูกจับขึ้นมาเหมือนกันหมด grep ไม่รู้ว่าอันไหนคือต้นฉบับ ในทางกลับกัน หากเราตกลงใช้สัญลักษณ์วงเล็บเหลี่ยมคู่ [[combat_global_cooldown_constant]] ไว้ในเอกสาร สัญญาณที่ว่า "อันนี้อ้างอิง atom นั้นโดยเจตนา" จะหลงเหลืออยู่ในตัวสตริงเอง เมื่อจำกัดด้วยรูปแบบ \[\[combat_global_cooldown การกล่าวถึงโดยบังเอิญจะหลุดออกไป เหลือเพียงการอ้างอิงที่ตั้งใจ
ข้อตกลงสัญลักษณ์บรรทัดเดียวนี้กลายเป็นเส้น (edge) หนึ่งของกราฟ เมื่อเอกสาร A เขียน [[atom_X]] เส้นเชื่อมทิศทาง A→X ก็เกิดขึ้น หากเอกสาร 200 ฉบับต่างเขียนกันคนละไม่กี่อัน กราฟก็จะสะสมอยู่ในข้อความโดยไม่มีใครต้องวาดมันขึ้นมา
ด้านล่างคือชิ้นส่วนหนึ่งของภาพที่ atom การตัดสินใจ และเอกสารในโปรเจกต์ของเราถูกผูกไว้ด้วย wikilink สีของโหนดแสดงประเภท ส่วนลูกศรแสดงทิศทางการอ้างอิง
สิ่งที่ชิ้นส่วนเล็ก ๆ นี้แสดงให้เห็นก็คือ คำตอบของคำถามจากนักออกแบบเกมคนใหม่อยู่ในกราฟอยู่แล้ว เมื่อย้อนตามลูกศรที่พุ่งเข้าสู่ atom combat_global_cooldown_constant กลับไป ก็จะพบการตัดสินใจ D2026_Q2_017 ไม่ใช่ 40 นาที แต่เป็นการย้อนอ้างอิงเพียงครั้งเดียว
เรากำหนดให้สิ่งที่จะผูกด้วย wikilink มีเพียงสี่ประเภทเท่านั้น หากเพิ่มประเภท รูปแบบก็จะสั่นคลอน และเมื่อรูปแบบสั่นคลอน grep ก็จะไม่แม่นยำอีกครั้ง
[[combat_global_cooldown_constant]] ชี้ไปยัง atom ซึ่งเป็นหน่วยของ 1 เอกสาร 1 การตัดสินใจ[[D2026_Q2_017]] บันทึกการตัดสินใจที่ระบุด้วยไตรมาสและหมายเลข[[CombatFormula_v3]] เอกสารขนาดใหญ่ เช่น rulebook หรือ specification[[สมาชิกทีม A]] ผู้รับผิดชอบหรือผู้ตัดสินใจทั้งสี่ประเภทใช้รูปแบบเดียวกันคือ [[name]] name ต้องไม่ซ้ำกันในระดับ global หากชื่อ atom ชนกันในสองที่ มันจะถูกรวมเป็นโหนดเดียวกันในกราฟ ทำให้เกิดเหตุการณ์ที่ "cooldown ของการต่อสู้" และ "cooldown ของ UI" กลายเป็นโหนดเดียวกัน ด้วยเหตุนี้กฎการตั้งชื่อ atom จึงบังคับใช้ prefix ตามสาขา (combat_, ui_)
ลำพังข้อตกลงสัญลักษณ์อย่างเดียวยังไม่พอ การให้คนใส่วงเล็บเหลี่ยมทีละอันในเอกสาร 200 ฉบับนั้นไม่สมจริง และต่อให้ใส่เสร็จแล้ว เมื่อชื่อ atom เปลี่ยน ทั้งหมดก็จะพังหมด ด้วยเหตุนี้เราจึงใช้สคริปต์ที่ทำงานสองอย่าง อย่างแรกคือ การปรับใช้ (apply) — แปลงชื่อ atom ที่รู้จักซึ่งปรากฏในเนื้อหาให้เป็น wikilink โดยอัตโนมัติ อย่างที่สองคือ การเยียวยา (heal) — ค้นหาลิงก์ที่เปลี่ยนชื่อไปแล้วหรือที่เสียหาย แล้วอัปเดตและรายงาน
ส่วนหลักของ wikilink_apply.py มีหน้าตาแบบนี้
# wikilink_apply.py — ปรับใช้ชื่อ atom ในเนื้อหาให้เป็น [[wikilink]] และเยียวยาลิงก์ที่เสียหาย
import re
from pathlib import Path
WIKILINK = re.compile(r"\[\[([A-Za-z0-9_]+)\]\]")
# จับเฉพาะชื่อ atom ที่ปรากฏแบบเปลือยซึ่งยังไม่ใช่ลิงก์ (กรณีที่ด้านหน้าไม่มี [[ )
BARE_NAME = lambda name: re.compile(rf"(?<!\[\[)(?<![A-Za-z0-9_])({re.escape(name)})(?![A-Za-z0-9_])(?!\]\])")
def load_known_atoms(registry: Path) -> set[str]:
# _atom_registry.tsv: คอลัมน์แรกคือ atom name ที่ใช้งานได้ในปัจจุบัน
return {ln.split("\t")[0].strip()
for ln in registry.read_text(encoding="utf-8").splitlines()
if ln.strip() and not ln.startswith("#")}
def apply_links(text: str, known: set[str]) -> tuple[str, int]:
applied = 0
for name in sorted(known, key=len, reverse=True): # ชื่อยาวก่อน: ป้องกันการปนเปื้อนจากการจับคู่บางส่วน
text, n = BARE_NAME(name).subn(rf"[[{name}]]", text)
applied += n
return text, applied
def heal_links(text: str, known: set[str], aliases: dict[str, str]) -> tuple[str, list[str]]:
dead = []
def repl(m):
ref = m.group(1)
if ref in known:
return m.group(0) # ยังมีอยู่ → คงไว้ตามเดิม
if ref in aliases: # atom ที่ถูกเปลี่ยนชื่อ → เยียวยาเป็นชื่อใหม่
return f"[[{aliases[ref]}]]"
dead.append(ref) # dead link จริง ๆ → รายงาน
return m.group(0)
return WIKILINK.sub(repl, text), dead
ตรงนี้มีตัวเลือกการออกแบบสองอย่างที่เป็นกระดูกสันหลังของเนื้อหา
อย่างแรก apply_links แทนที่ ชื่อยาวก่อน เมื่อมี atom สองตัวคือ combat_cooldown และ combat_cooldown_global หากแทนที่ตัวสั้นก่อน ส่วนหน้าของตัวยาวจะถูกปนเปื้อน การเรียงลำดับจากยาวไปสั้นบรรทัดเดียวป้องกันเหตุการณ์นี้ นี่คือส่วนที่ผมลืมไปตอนเขียนครั้งแรก และเพิ่มเข้ามาก็ต่อเมื่อเกิดผลลัพธ์ที่เสียหายอย่าง [[combat_cooldown]]_global ขึ้นจริง ๆ แล้ว
อย่างที่สอง heal_links เยียวยาผ่าน พจนานุกรมการเปลี่ยนชื่อ (aliases) เมื่อชื่อ atom เปลี่ยนจาก combat_gcd → combat_global_cooldown_constant มันจะแทนที่ชื่อเก่าด้วยชื่อใหม่โดยอัตโนมัติ และจะรายงานเป็น dead link ก็ต่อเมื่อไม่มีอยู่ในพจนานุกรมเลยเท่านั้น แทนที่จะแก้เอกสาร 200 ฉบับด้วยมือทุกครั้งที่ชื่อเปลี่ยน เราเพียงเพิ่ม alias หนึ่งบรรทัด
การ apply อัตโนมัติจะลิงก์เฉพาะ "ชื่อ atom ที่รู้จักอยู่แล้ว" เท่านั้น แต่ประโยคที่ไม่ได้เขียนชื่อ atom ในเนื้อหาแต่บรรยายขยายความออกมา ("คูลดาวน์รวมของการต่อสู้คือ 0.5 วินาที") นั้นจับไม่ได้ การแปลงการอ้างอิงด้วยภาษาธรรมชาติเช่นนี้ให้เป็น wikilink ตัวเลือก LLM ทำได้เร็วกว่าคน ด้านล่างคือบทสนทนาฉบับเต็มที่เกิดขึ้นจริง ผมไม่ได้สรุปย่อผลลัพธ์ และส่วนที่ผมปฏิเสธก็คงไว้ตามเดิม
พรอมต์ของผม (ฉบับเต็ม):
จะให้ย่อหน้าหนึ่งของ rulebook การต่อสู้และรายการ atom ที่ลงทะเบียนไว้ จากเนื้อหา
ช่วยหาสำนวนภาษาธรรมชาติที่ชี้ไปยัง atom เหล่านี้ แล้วเสนอ [[wikilink]] ที่เป็นตัวเลือก
ในรูปแบบตาราง เฉพาะตอนที่แน่ใจเท่านั้น ถ้าคลุมเครือก็แค่ทำเครื่องหมาย "คลุมเครือ" ไว้
อย่าเขียนเนื้อหาใหม่ บอกแค่ว่าจะเปลี่ยนวลีไหนเป็นลิงก์ใด
แนวคิดที่ยังไม่ได้ลงทะเบียน ให้แยกออกมาเป็น "atom ใหม่ที่เป็นตัวเลือก"
[atom ที่ลงทะเบียนไว้]
combat_global_cooldown_constant
combat_skill_cast_interrupt_rule
combat_resource_regen_rate
[เนื้อหา]
คูลดาวน์รวมของการต่อสู้คือ 0.5 วินาที และใช้ร่วมกันกับสกิลแอ็กทีฟทุกตัว
หากถูกโจมตีระหว่างร่าย การร่ายจะถูกขัดจังหวะ ยกเว้นสกิลสายฟื้นฟู
การฟื้นฟูทรัพยากรจะกลับสู่ความเร็วปกติเฉพาะในสถานะนอกการต่อสู้เท่านั้น
ผลลัพธ์จาก Claude (ดิบตามเดิม):
wikilink ที่เสนอ:
| วลีในเนื้อหา | ลิงก์ตัวเลือก | ความมั่นใจ |
|------------------------------------|----------------------------------------|------------|
| "คูลดาวน์รวมของการต่อสู้คือ 0.5 วินาที" | [[combat_global_cooldown_constant]] | สูง |
| "หากถูกโจมตีระหว่างร่าย การร่ายจะถูกขัดจังหวะ" | [[combat_skill_cast_interrupt_rule]] | สูง |
| "การฟื้นฟูทรัพยากรเฉพาะในสถานะนอกการต่อสู้" | [[combat_resource_regen_rate]] | ปานกลาง |
atom ใหม่ที่เป็นตัวเลือก:
- "สกิลสายฟื้นฟูเป็นข้อยกเว้น" → กฎข้อยกเว้นนี้ไม่ได้ระบุไว้ที่ใดในบรรดา atom
ที่ลงทะเบียนไว้เลย ดูเหมือนจะต้องมี atom แยกต่างหาก เช่น
combat_skill_cast_interrupt_heal_exception (คลุมเครือ: อาจเป็นข้อย่อยของ
interrupt_rule ก็ได้ จึงตัดสินไม่ได้ว่าเป็น atom เดี่ยวหรือไม่)
หมายเหตุ: บรรทัด "การฟื้นฟูทรัพยากร" มีความมั่นใจปานกลางเพราะยังไม่เห็นนิยามของ atom
ว่า combat_resource_regen_rate ชี้ไปยัง 'ความเร็วการฟื้นฟู' หรือ 'เงื่อนไขนอกการต่อสู้'
การตรวจสอบและการปฏิเสธของผม:
ในสามบรรทัด ผมรับสองบรรทัด คูลดาวน์รวมและการขัดจังหวะระหว่างร่ายตรงกับนิยามของ atom เป๊ะ ส่วนบรรทัดที่สาม "การฟื้นฟูทรัพยากร" Claude ลดความมั่นใจของตัวเองลงเป็น "ปานกลาง" ด้วยตัวเองนั้นถูกต้องแล้ว — atom combat_resource_regen_rate นิยามเฉพาะ ค่าความเร็ว เท่านั้น ส่วน เงื่อนไข ที่ว่า "เฉพาะในสถานะนอกการต่อสู้" เป็นความรับผิดชอบของ atom อื่น หากใส่ลิงก์ไปตรง ๆ จะเกิดเหตุการณ์ที่เชื่อม "เงื่อนไข" เข้ากับ atom ของ "ความเร็ว" อย่างผิด ๆ ผมปฏิเสธ
ส่วนการชี้ atom ใหม่ที่เป็นตัวเลือกนั้นถูกต้อง "ข้อยกเว้นสายฟื้นฟู" ไม่มี atom อยู่ที่ใดเลยจริง ๆ เพียงแต่ส่วนที่ Claude บอกว่าคลุมเครือ ("เป็นข้อย่อยของ interrupt_rule หรือ atom เดี่ยว") เป็นขอบเขตที่คนต้องตัดสิน และผมตัดสินใจแยกออกมาเป็น atom เดี่ยว
การร้องขอใหม่:
บรรทัด "การฟื้นฟูทรัพยากร" อย่าลิงก์ แทนที่จะเป็นเช่นนั้น ให้ [[combat_resource_regen_rate]]
ครอบคลุมเฉพาะ 'ความเร็ว' ส่วน 'เงื่อนไขนอกการต่อสู้' ให้แยกเป็น atom ใหม่ ช่วยเขียนนิยาม
1 บรรทัดของแต่ละ atom ทั้งสอง และข้อยกเว้นการฟื้นฟูก็ให้แยกเป็น atom เดี่ยวพร้อมนิยาม 1 บรรทัดด้วย
ในการโต้ตอบไปมานี้ สิ่งที่ LLM ทำไม่ใช่ "สร้างตัวเลือกจากศูนย์" แต่เป็น "คัดเลือกตัวเลือกให้" หัวใจสำคัญคือ มีจุดที่คนจะปฏิเสธได้อย่างชัดเจน หากเป็นการเผยแพร่อัตโนมัติ ลิงก์ที่ผิดเพียงอันเดียวก็จะหลงเหลืออยู่ในกราฟอย่างถาวร
ลิงก์ย่อมเสียหายเมื่อเวลาผ่านไป atom ถูกยกเลิก ชื่อเปลี่ยน และมีคำพิมพ์ผิดแทรกเข้ามา ด้วยเหตุนี้เราจึงรัน wikilink lint ในทุกครั้งที่ build รายการตรวจสอบและการจัดการมีดังนี้
[[name]] ไม่มีอยู่ในรีจิสทรี → เตือนในขั้นตอน build ตรวจสอบพจนานุกรมการเยียวยาที่ตั้ง dead link ให้เป็นเพียงคำเตือนแทนที่จะสกัดนั้นเป็นความตั้งใจ ในสถานะระหว่างที่กำลังเปลี่ยนชื่อ atom จะเกิด dead ขึ้นชั่วครู่ ถ้าสกัดสิ่งนี้ให้ build ล้มเหลว งานก็จะหยุดชะงัก แทนที่จะเป็นเช่นนั้นเราให้ตรวจสอบพจนานุกรมการเยียวยาก่อน ส่วนการละเมิดรูปแบบและชื่อชนกันให้สกัดทันที — เพราะทั้งสองอย่างนี้ทำให้กราฟทั้งหมดปนเปื้อน
lint นี้พิสูจน์ตัวเองได้ ลิงก์ที่ wikilink_apply.py สร้างขึ้นถูกตรวจโดย lint ของระบบเดียวกัน และผลลัพธ์นั้นก็ถูกบันทึกไว้เป็นการตัดสินใจ atom อีกครั้งหนึ่ง วงจรที่เครื่องมือตรวจสอบผลผลิตของตัวเองด้วยเกณฑ์ของตัวเองคือกระดูกสันหลังพื้นฐานของการดำเนินงาน
มาถึงตรงนี้คือเรื่องการเชื่อมโยง ทีนี้มาถึงการจัดหมวด หาก wikilink ตอบว่า "การตัดสินใจนี้นำไปสู่ที่ใด" ลำดับชั้นก็ตอบว่า "เอกสารนี้อยู่ที่ไหน" หากขาดทั้งสองอย่าง การค้นหา 40 นาทีของนักออกแบบเกมคนใหม่ก็จะเกิดซ้ำ
โฟลเดอร์เอกสารของเรามีสี่ชั้น ชั้นเหล่านี้แชร์กระดูกสันหลังเดียวกันกับการรวม Layer ของสถาปัตยกรรมสารสนเทศ — วิสัยทัศน์ ระบบ เนื้อหา และเมตา อย่างละหนึ่งชั้น
docs/
├── L0_vision/ วิสัยทัศน์ (ไม่เกิน 5 ฉบับ แทบไม่เปลี่ยน)
├── L1_systems/ rulebook ตามสาขา
│ ├── combat/
│ ├── narrative/
│ └── ui/
├── L2_content/ เนื้อหาแต่ละชิ้น
│ ├── characters/
│ └── quests/
└── L4_meta/ การดำเนินงาน·การตัดสินใจ·การประชุม·atom
├── decisions/
├── meetings/
├── reports/
└── atoms/
เหตุที่ L3 ว่างเปล่าเพราะชีตข้อมูลและ DB เข้าไปแทนที่ตำแหน่งนั้น (เป็นตารางไม่ใช่เอกสาร) การตัดสินใจที่นักออกแบบเกมคนใหม่ตามหาอยู่ที่ L4_meta/decisions/ — เพียงมีข้อตกลงเดียวนี้ การค้นหา 40 นาทีก็คงจบลงด้วยประโยคเดียวว่า "การตัดสินใจอยู่ที่นั่น"
เพื่อให้ลำดับชั้นทำงานเป็นทางเข้าของการค้นหา ต้องรักษาห้าข้อนี้ไปพร้อมกัน หากขาดข้อใดข้อหนึ่ง การจัดหมวดก็พังลง
combat/·narrative/ ค้นหาได้ แต่ 2026-Q1/·2026-Q2/ หกเดือนถัดมาไม่มีใครเปิด เวลานั้น git บันทึกอยู่แล้ว จึงไม่มีเหตุผลที่จะแบ่งด้วยโฟลเดอร์ซ้ำอีกL1_systems/combat/skills/active/single_target/attack.md คือ 5 ชั้น เมื่อ path ยาวเกินหนึ่งหน้าจอ คนจะเก็บตำแหน่งไว้ในหัวไม่ได้spec_·report_·decision_·char_ มองเห็นประเภทได้โดยไม่ต้องดูโฟลเดอร์_ _archive/·_TEMPLATES/·_NAMING/ จะถูกจัดเรียงขึ้นด้านบนในการเรียงลำดับอัตโนมัติ และไม่ปะปนกับเนื้อหาหลักเอกสารไม่ได้อยู่ที่เดียวตลอด ระหว่างการเขียนมันอยู่ในโฟลเดอร์หลักด้วย status: draft เมื่อถูกเปิดใช้งานก็กลายเป็น status: active และเมื่อถูกยกเลิกก็ ไม่ใช่การลบ แต่ย้ายไปยัง _archive/ แล้วติดป้าย status: deprecated การไม่ลบข้อมูลที่ถูกยกเลิกคือหลักการเหล็ก หกเดือนถัดมาเมื่อมีใครถามว่า "ทำไมถึงพลิกการตัดสินใจนั้น" คำตอบอยู่ในข้อมูลที่ถูกยกเลิกเท่านั้น หากลบทิ้งไป ก็ไม่มีทางหาเหตุผลของการตัดสินใจกลับคืนมาภายหลังได้
การเปลี่ยนแปลงใหญ่ ๆ เราไม่ฝากไว้กับ git เพียงอย่างเดียว แต่บันทึกไว้เป็น change_log ใน frontmatter ด้วย
---
title: combat_global_cooldown_rule
version: v3
last_modifier: teammate_a
change_log:
- v1 (2025): ร่างแรก
- v2 (2025): cooldown 0.3 → 0.5 ([[D2026_Q2_017]])
- v3 (2026): เพิ่มข้อยกเว้นการฟื้นฟู ([[D2026_Q2_018]])
---
จงสังเกตว่า ID การตัดสินใจใน change_log เขียนไว้เป็น wikilink ตรงนี้คือจุดที่การเชื่อมโยงและการจัดหมวดมาบรรจบกัน เอกสารอยู่ที่เดียวภายในลำดับชั้น (การจัดหมวด) แต่ประวัติการเปลี่ยนแปลงของมันเชื่อมต่อไปยังกราฟการตัดสินใจ (การเชื่อมโยง) frontmatter หนึ่งเปิดสองทางเข้าพร้อมกัน
ลำดับชั้นจะเน่าหากปล่อยทิ้งไว้ โฟลเดอร์ว่างเกิดขึ้น draft ที่ค้างนานหกเดือนพอกพูน และความลึกค่อย ๆ เพิ่มขึ้นทีละน้อย ด้วยเหตุนี้เราจึงจัดระเบียบหนึ่งครั้งต่อไตรมาส โฟลเดอร์ว่างก็ลบ draft ที่เกินหกเดือนก็ตัดสินว่าจะเปิดใช้งานหรือยกเลิก ที่ลึกตั้งแต่ 5 ชั้นขึ้นไปก็ทำให้แบนราบ โฟลเดอร์ที่ไม่มี README ก็เขียนหรือยกเลิก และเมื่อ _archive เกินครึ่งก็เก็บรักษาแบบบีบอัด หากไม่มีวงจรนี้ ลำดับชั้นก็จะเต็มไปด้วยสัญญาณรบกวนจนแยกสัญญาณกับเสียงรบกวนไม่ออก
หากมองภาพรวมทั้งหมดเป็นรูปเดียว จะเป็นเช่นนี้ ตั้งแต่เอกสารเข้ามา ถูกเชื่อมโยง ถูกจัดหมวด ถูกตรวจสอบ จนถูกยกเลิก คือวงจรหนึ่งวง
flowchart TD
A[เขียนเอกสารใหม่
status: draft] --> B[wikilink_apply.py
ลิงก์ชื่อ atom อัตโนมัติ]
B --> C[เสริมด้วย LLM
ตัวเลือกการอ้างอิงภาษาธรรมชาติ]
C --> D{การตรวจสอบโดยมนุษย์}
D -->|รับ| E[จัดวางตามลำดับชั้น
L0~L4 + prefix]
D -->|ปฏิเสธ| C
E --> F[lint ตอน build
ตรวจ dead/ชนกัน/วนซ้ำ]
F -->|ผ่าน| G[status: active]
F -->|dead link| H[ตรวจสอบพจนานุกรมการเยียวยา]
H --> F
G --> I[วงจรจัดระเบียบรายไตรมาส]
I -->|ยกเลิก| J[_archive/
status: deprecated]
I -->|คงไว้| G
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
class B,F,H code;
class C ai;
class D,I human;
class A data;
class G pass;
class J fail;
ในวงจรนี้ การเชื่อมโยง (B·C·D·F) และการจัดหมวด (E·I·J) ทำงานสลับกัน ทั้งสองไม่ได้หมุนแยกกัน แต่ประสานกันภายในวงจรชีวิตของเอกสารหนึ่งฉบับ
ตัวเลขเป็น ทิศทาง ที่เปรียบเทียบก่อนและหลังการนำมาใช้ในโปรเจกต์ของผู้เขียน ไม่ใช่ค่าวัดที่แม่นยำ แต่เป็นขนาดของความแตกต่างที่รู้สึกได้เมื่อทำงานเดียวกันในสองสภาพแวดล้อม (จากการสังเกตของผู้เขียน ยังไม่ได้วัดอย่างละเอียด)
ก่อนที่การเชื่อมโยงและลำดับชั้นจะเข้าที่ คำถามติดตามการตัดสินใจของนักออกแบบเกมคนใหม่ใช้เวลานานถึงหนึ่งถึงสองชั่วโมงเหมือนกรณี 40 นาทีในบทนำ หลังการนำมาใช้ เป็นการย้อนอ้างอิง atom เพียงครั้งเดียว — ระดับนาที การค้นหาเอกสารลดจาก 5\~10 นาทีเหลือราว 30 วินาที ซึ่งนี่คือผลของการจัดหมวดตามความหมายและ prefix ของลำดับชั้นที่ทำงานร่วมกัน ส่วนเหตุการณ์ที่เกิดจากการอ้างอิงผิด (ประเภทเข้าใจผิดว่าค่าที่ถูกยกเลิกแล้วยังเป็นค่าปัจจุบัน) ลดจากหลายครั้งต่อไตรมาสเหลือหนึ่งถึงสองครั้ง — เพราะ wikilink ระบุชัดว่า "อันนี้อ้างอิง atom นั้น" ค่าที่ถูกสำเนามากับค่าต้นฉบับจึงไม่สับสนกันอีก
การเปลี่ยนแปลงที่ใหญ่ที่สุดคือ การปรับตัวของผู้ที่เพิ่งเข้าร่วมทีม หากไม่มีลำดับชั้น การเรียนรู้ว่าอะไรอยู่ในโฟลเดอร์ไหนต้องใช้เวลาหลายวัน และหากไม่มีการเชื่อมโยง ก็ไม่มีทางเข้าใจว่าระบบต่าง ๆ เกี่ยวพันกันอย่างไร หลังจากที่มีทั้งสองครบแล้ว พวกเขาเรียนรู้ตำแหน่งจาก README ของโฟลเดอร์ และสำรวจความสัมพันธ์ระหว่างระบบด้วยตัวเองโดยตามกราฟ wikilink "สิ่งที่ต้องถามจึงจะรู้" กลายเป็น "สิ่งที่ตามไปแล้วจะเห็น"
ผลลัพธ์นี้เกิดขึ้นเฉพาะเมื่อมีสองทางเข้า อยู่ด้วยกัน เท่านั้น หากมีแค่การเชื่อมโยงแต่ไม่มีการจัดหมวด ก็มีกราฟแต่ไม่รู้ว่าเอกสารอยู่ที่ไหน และหากมีแค่การจัดหมวดแต่ไม่มีการเชื่อมโยง โฟลเดอร์ก็เป็นระเบียบแต่ไม่รู้ว่าการตัดสินใจนำไปสู่ที่ใด
ความล้มเหลวที่พบบ่อยที่สุดในฝั่งการเชื่อมโยงคือ ลิงก์ที่เป็นสัญญาณรบกวน หากเห็นว่า wikilink ดีแล้วใส่วงเล็บเหลี่ยมในทุกคำนาม กราฟก็จะเต็มไปด้วยเส้นเชื่อมที่ไร้ความหมายจนเครื่องมือ visualize ไร้พลัง หลักการคือเหลือเฉพาะลิงก์ที่สามารถถามและตอบได้ว่า "เอกสารนี้มีความสัมพันธ์อย่างไรกับเอกสารนั้น" เท่านั้น ถัดมาคือ การเผยแพร่อัตโนมัติ — หาก commit ลิงก์ที่ LLM สร้างโดยไม่ผ่านการตรวจสอบ การเชื่อมโยงที่ผิดอย่างบรรทัด "การฟื้นฟูทรัพยากร" ในบันทึกเซสชันจริงก็จะหลงเหลืออยู่ถาวร apply เป็นอัตโนมัติ แต่การเผยแพร่เป็นหน้าที่ของคน
ความล้มเหลวในฝั่งการจัดหมวดส่วนใหญ่คือการละเมิดห้าหลักการ โฟลเดอร์ตามเวลา ความลึกตั้งแต่ 5 ชั้น ชื่อไฟล์ไร้กฎ และการไม่มี README และที่ย้อนคืนได้ยากที่สุด — การลบข้อมูลที่ถูกยกเลิก เหตุผลของการตัดสินใจที่ถูกลบไปแล้วสร้างขึ้นใหม่ไม่ได้ การส่งไปยัง _archive หนึ่งบรรทัดปกป้องสื่อการเรียนรู้ของหกเดือนถัดมา
_archive แทนการลบ เหตุผลของการตัดสินใจจึงจะรอดมาให้ใช้ภายหลังsetup. สร้างสี่โฟลเดอร์ L0_vision/ L1_systems/ L2_content/ L4_meta/ ในโฟลเดอร์เอกสาร และวาง README หนึ่งบรรทัดไว้ในแต่ละโฟลเดอร์ รวบรวมรายการชื่อ atom ไว้ในไฟล์เดียว _atom_registry.tsv (คอลัมน์แรก = atom name)
prompt. ให้ย่อหน้าหนึ่งของเนื้อหาและรายการ atom ที่ลงทะเบียนไว้แก่ LLM แล้วร้องขอเช่นนี้ — "จากเนื้อหา ช่วยหาสำนวนภาษาธรรมชาติที่ชี้ไปยัง atom เหล่านี้ แล้วเสนอ [[wikilink]] ที่เป็นตัวเลือกในรูปแบบตาราง เสนอเฉพาะตอนที่แน่ใจเท่านั้น ถ้าคลุมเครือก็แค่ทำเครื่องหมาย 'คลุมเครือ' ไว้ อย่าเขียนเนื้อหาใหม่ แนวคิดที่ยังไม่ได้ลงทะเบียนให้แยกเป็น 'atom ใหม่ที่เป็นตัวเลือก'"
verify. เทียบทุกลิงก์ที่เสนอกับนิยาม atom รับเฉพาะเมื่อสิ่งที่ atom ชี้ถึงกับสิ่งที่เนื้อหาชี้ถึง ตรงกันเป๊ะ เท่านั้น หากเงื่อนไข/คุณสมบัติ/ข้อยกเว้นคลาดเคลื่อนก็ปฏิเสธ หลังรับแล้วให้ตรวจด้วย grep "\[\[name\]\]" ว่าลิงก์ถูกใส่จริงหรือไม่ และไม่มี dead link หรือไม่
ฉบับย่อสำหรับคนเดียว. หากจะเริ่มโดยไม่มีทั้งสคริปต์และ lint กฎสองบรรทัดก็พอ (1) การตัดสินใจให้อยู่ในโฟลเดอร์ decisions/ โฟลเดอร์เดียวเสมอ ในรูปแบบ decision_*.md (2) เมื่อเอกสารอื่นกล่าวถึงการตัดสินใจนั้นให้เขียนเป็น [[decision_id]] เพียงรักษาสองบรรทัดนี้ คำถามว่า "การตัดสินใจนั้นอยู่ที่ไหน" ก็ตอบได้ด้วย grep "\[\[decision_" เพียงครั้งเดียว เครื่องมือนำมาใช้หลังจากเอกสารเกิน 100 ฉบับก็ยังไม่สาย
ช่วงเวลาที่เราเริ่มสงสัยข้อมูลมักมาช้าเกินไปเสมอ เรามักจะมาถามว่า "ค่านี้มาจากไหน" ก็ต่อเมื่อค่าที่ผิดถูกป้อนเข้าไปในไลฟ์บิลด์เรียบร้อยแล้ว
เย็นวันศุกร์ก่อนหน้าอัลฟ่าบิลด์ เพื่อนร่วมทีม B เดินมาที่โต๊ะของผม ในมือถือโน้ตบุ๊กที่เปิดสเปรดชีตการปรับสมดุลการต่อสู้ค้างไว้ "ผู้อำนวยการครับ ค่าพลังชีวิตเฟส 1 ของบอสในชีตคือ 48,000 แต่ค่าที่เข้าไปในบิลด์คือ 52,000 ตกลงอันไหนถูกครับ"
ผมไม่รู้ พูดให้ตรงกว่านั้นคือ — ในจังหวะนั้นไม่มีใครรู้ ค่า 52,000 ในบิลด์อาจเป็นค่าล่าสุดที่สะท้อนการตัดสินใจในการประชุมเมื่อไม่กี่วันก่อน หรืออาจเป็นค่าที่ใครบางคนใส่ไว้ชั่วคราวโดยยังไม่ได้ตรวจสอบก็ได้ ส่วน 48,000 อาจเป็นค่าที่ตกลงกันก่อนการประชุมนั้น ทั้งสองตัวเลขดูสมเหตุสมผลทั้งคู่ แต่ความสมเหตุสมผลไม่ใช่หลักฐาน
หากต้องการตอบคำถามนี้ ต้องย้อนกลับไปยังแหล่งที่มา ตัดสินใจในการประชุมไหน อินพุตของการประชุมนั้นคืออะไร ใครเป็นคนย้ายค่าลงในชีต แต่ถ้าโซ่ของการติดตามนั้นอยู่แค่ในความทรงจำของคน คำตอบก็จะกลายเป็น "พรุ่งนี้เดี๋ยวผมถามเพื่อนร่วมทีม A ให้นะครับ" พอถึงเดือนที่ 6 ของการ Live Ops คำถามที่ยังไม่มีคำตอบแบบนั้นจะกองสุมกันสูงเป็นภูเขา data lineage — สายเลือดของข้อมูล — คือโครงสร้างพื้นฐานที่ทำให้ภูเขานั้นไม่ก่อตัวขึ้นมา
แก่นมีเพียงหนึ่งเดียว แหล่งที่มาต้องไม่เขียนด้วยมือ บันทึกแหล่งที่มาที่คนมาเติมย้อนหลังจะอยู่ได้ไม่ถึงเดือน มีเพียงแหล่งที่มาที่ถูกบันทึกโดยอัตโนมัติในวินาทีที่ข้อมูลถูกสร้างขึ้นเท่านั้นที่อยู่รอด
ต้นทุนในการบันทึกหนึ่งบรรทัดของ _source_map.tsv โดยอัตโนมัติคือไม่กี่มิลลิวินาที แต่ต้นทุนที่ต้องจ่ายเมื่อไม่มีบรรทัดนั้นจะลามออกไปห้าทาง
กับดักก็คือ ไม่มีต้นทุนใดในห้าแบบนี้ที่มองเห็นได้ในวินาทีที่สร้างข้อมูลขึ้นมา ใบเรียกเก็บเงินทั้งหมดจะมาถึงในอีกไม่กี่สัปดาห์ ไม่กี่เดือน หรือหลังจากที่คนผลัดเปลี่ยนกันไปแล้ว ด้วยเหตุนี้ แหล่งที่มาจึงไม่อาจเป็นเรื่องที่ "ค่อยไปจัดการทีหลัง" ได้ มันต้องถูกบันทึกในวินาทีที่สร้างขึ้น
ในโปรเจกต์ A ไฟล์แมปแหล่งที่มาที่ใช้งานมีเพียง _source_map.tsv ไฟล์เดียว เหตุผลที่ใช้ข้อความคั่นด้วยแท็บนั้นเรียบง่าย คนสามารถอ่านหนึ่งบรรทัดด้วยตาได้ สคริปต์พาร์สด้วย split('\t') ครั้งเดียว และ git diff แสดงการเปลี่ยนแปลงทีละบรรทัดได้อย่างสะอาดตา CSV จะพังเมื่อมีจุลภาคปนอยู่ในเนื้อหา ส่วน JSON หนึ่งบรรทัดนั้นคนอ่านยาก
asset_id source_type source created creator notes
spec_combat_v3 internal mtg_battle_2026-04-18 2026-04-18 teammate_a อ้างอิง decision_D2026_Q2_017
data_boss_hp_v3 internal decision_D2026_Q2_017 2026-04-18 teammate_b ยืนยันเฟส 1 ที่ 48000
asset_K_001_concept internal_ai_assisted imagegen + teammate_b ปรับแต่ง 2026-04-20 teammate_b legal_review เสร็จสิ้น
data_user_voice_W21 external_aggregated forum + community + sns 2026-05-25 auto_collect ผลลัพธ์จากไปป์ไลน์ 13.1
ref_visual_tone_a external_reference refgame (2024) 2026-04-15 teammate_c อ้างอิงโทนภาพ ไม่ได้นำมาใช้โดยตรง
บทบาทของหกช่องนั้นชัดเจน asset_id คือคีย์เฉพาะของข้อมูล source_type คือการจำแนกประเภท (จะกล่าวถึงด้านล่าง) source คือตำแหน่งของแหล่งที่มา — ID การประชุม·ID การตัดสินใจ·ไปป์ไลน์การเก็บข้อมูล·ชื่อผลงานภายนอก ส่วน created/creator คือเมื่อไหร่·โดยใคร และ notes คือบริบทหนึ่งบรรทัดที่คนอ่านได้
ลองกลับมาดูบรรทัดที่สองและบรรทัดที่สามอีกครั้ง จะเห็นคำตอบของคำถามจากเพื่อนร่วมทีม B ในหัวข้อก่อนหน้า แหล่งที่มาของ data_boss_hp_v3 คือ decision_D2026_Q2_017 และใน notes มีข้อความ "ยืนยันเฟส 1 ที่ 48000" บันทึกไว้ ค่า 52,000 ในบิลด์ไม่อยู่ใน lineage นี้ กล่าวคือ 52,000 เป็นค่าชั่วคราวที่ยังไม่ได้ตรวจสอบ และคำตอบที่ถูกต้องคือ 48,000 คำถามนี้ปิดได้ภายใน 1\~2 นาที โดยไม่ต้องเรียกความทรงจำของคน และไม่ต้องทำให้เย็นวันศุกร์พังไป
แต่ไฟล์นี้ยังมีกฎอีกข้อแขวนอยู่ หากคนแก้ไข _source_map.tsv ด้วยมือ การ audit ของ integrity_check จะให้ผลเป็น FAIL เหตุผลจะกล่าวถึงในหัวข้อถัดไป — เพราะแหล่งที่มาต้องถูกบันทึกโดยอัตโนมัติเท่านั้น
เหตุผลที่จำแนกแหล่งที่มาออกเป็นห้าประเภทนั้นไม่ใช่เพราะชอบความเป็นระเบียบ แต่เพราะกฎการดำเนินงานที่ติดมากับแต่ละ source_type นั้นต่างกัน
ลองดู external_reference หนึ่งบรรทัด ถ้าเป็นแอสเซตที่อ้างอิง refgame เพื่อดูโทนภาพ แอสเซตนี้จะเข้าบิลด์ไม่ได้หากไม่ผ่านการตรวจสอบทางกฎหมาย หาก source_type เป็น external_reference แต่บันทึก legal_review ว่างเปล่า audit จะขวางไว้ นี่คือจุดที่ป้ายกำกับไม่ได้จบลงแค่การเป็นป้ายกำกับ แต่กลายเป็นสวิตช์ที่ตัวตรวจสอบอ่าน ที่บอกว่าการจำแนก 5 ประเภทเป็นโครงของความน่าเชื่อถือในการดำเนินงาน ก็หมายถึงพลังการบังคับนี้
ทีนี้มาถึงแก่นแล้ว แหล่งที่มาต้องถูกบันทึกโดยอัตโนมัติในจังหวะที่สร้างข้อมูล source_tracker.py ของโปรเจกต์ A แขวนอยู่กับฮุกการสร้างแอสเซต
# source_tracker.py
import time, getpass, csv
from pathlib import Path
SOURCE_MAP = Path("_source_map.tsv")
VALID_TYPES = {
"internal", "internal_ai_assisted",
"external_aggregated", "external_reference", "self_measured",
}
def track_source(asset_id: str, source_type: str, source: str, notes: str = ""):
if source_type not in VALID_TYPES:
raise ValueError(f"unknown source_type: {source_type}")
if source_type == "external_reference" and "legal_review" not in notes:
raise ValueError(f"{asset_id}: แอสเซตประเภท external_reference ต้องระบุ legal_review")
record = [
asset_id,
source_type,
source,
time.strftime("%Y-%m-%d"),
getpass.getuser(),
notes,
]
with SOURCE_MAP.open("a", encoding="utf-8", newline="") as f:
csv.writer(f, delimiter="\t").writerow(record)
หากฟังก์ชันนี้แขวนอยู่กับไปป์ไลน์การสร้างแอสเซต — ตอนที่ชีตถูก export ตอนที่แอสเซตคอนเซ็ปต์ถูกลงทะเบียน ตอนที่ข้อมูลผู้ใช้ถูกรวบรวม — แหล่งที่มาหนึ่งบรรทัดจะถูก append โดยอัตโนมัติ ไม่มีขั้นตอนใดที่คนจะลืมได้ ภาระของการเติมย้อนหลังจึงเข้าใกล้ศูนย์
จุดที่เติมช่อง creator ด้วย getpass.getuser() โดยอัตโนมัตินั้นเล็กแต่ชี้ขาด หากให้คนกรอกชื่อตัวเอง ช่องว่างจะเกิดขึ้น หากระบบกรอก จะไม่มีช่องว่าง และถ้า external_reference ไม่มี legal_review การบันทึกก็จะถูกปฏิเสธไปเลย — เป็นการปิดช่องทางที่แอสเซตภายนอกไร้แหล่งที่มาจะเล็ดลอดเข้าสู่ระบบ
ตรงนี้ขอชี้ให้เห็นภาพจริงของการใช้ AI ผมได้ร่างแรกของฟังก์ชัน track_source นี้มาจาก Claude ต่อไปนี้คือแกนหนึ่งของบันทึกเซสชันจริง (worked transcript) นั้น
พรอมต์ของผม (ฉบับเต็ม):
ช่วยเขียนฟังก์ชัน Python ที่ append แหล่งที่มาหนึ่งบรรทัดลงใน _source_map.tsv ให้หน่อย
คอลัมน์: asset_id, source_type, source, created(วันที่วันนี้),
creator(ผู้ใช้ OS ปัจจุบัน), notes คั่นด้วยแท็บ
จำกัด source_type ไว้ 5 ประเภท: internal, internal_ai_assisted,
external_aggregated, external_reference, self_measured
ผลลัพธ์ดิบจาก Claude (ไม่ได้สรุปย่อ):
import csv, datetime, os
def track_source(asset_id, source_type, source, notes=""):
record = [asset_id, source_type, source,
datetime.date.today().isoformat(),
os.getlogin(), # ← ผู้ใช้ปัจจุบัน
notes]
with open("_source_map.tsv", "a", newline="") as f:
csv.writer(f, delimiter="\t").writerow(record)
หากต้องการเพิ่มการตรวจสอบ source_type ก็สามารถสร้างเซต VALID_TYPES แล้วตรวจสอบที่ต้นฟังก์ชันได้ครับ ต้องการให้ใส่ให้ไหมครับ
การตรวจสอบและการปฏิเสธของผม:
ผลลัพธ์นี้ทำงานได้ แต่ไม่เข้ากับการดำเนินงานของเราในสองจุด ข้อแรก os.getlogin() เมื่อถูกเรียกจาก cron หรือ CI อาจคืนค่าสตริงว่างหรือโยน exception ขึ้นอยู่กับสภาพแวดล้อม ไปป์ไลน์ export ของเรารันด้วยตารางเวลาแบบไม่มีคนดูแลด้วย ผมจึงเปลี่ยนเป็น getpass.getuser() — มันดูจากตัวแปรสภาพแวดล้อมแล้วจับผู้ใช้ได้เสถียรกว่า ข้อสอง Claude ทิ้งการตรวจสอบ source_type ไว้เป็นตัวเลือกด้วยประโยค "ต้องการให้ใส่ให้ไหม" แต่สำหรับเรานั่นไม่ใช่ตัวเลือก มันเป็นข้อบังคับ ถ้าไม่มีการตรวจสอบ source_type ที่พิมพ์ผิดจะหลุดเข้ามาและทำให้การจำแนกพังทลาย
การร้องขอใหม่ของผม:
เปลี่ยนเป็น getpass.getuser() ให้หน่อย แล้วการตรวจสอบ source_type
ก็ไม่ใช่ตัวเลือก แต่ให้ฝังไว้ในฟังก์ชันเป็นข้อบังคับ เพิ่มอีกข้อคือ
ถ้าเป็นประเภท external_reference แต่ใน notes ไม่มีสตริง legal_review
ให้โยน ValueError ผมต้องการสกัดไม่ให้แอสเซตภายนอกที่ไม่ผ่านการตรวจสอบ
ทางกฎหมายถูกบันทึกลงไปตั้งแต่ต้นทาง
ผลของการร้องขอใหม่นี้คือ source_tracker.py ฉบับสุดท้ายที่ลงไว้ด้านบน จุดที่ต้องชี้คือ ไม่ใช่ว่าผลลัพธ์แรกของ Claude ผิด แต่เพราะมีข้อจำกัดด้านการดำเนินงานที่ AI ไม่รู้ — ตารางเวลาแบบไม่มีคนดูแล การบังคับ legal_review — ซึ่งผมรู้ การปฏิเสธและการร้องขอใหม่จึงจำเป็น โดยทั่วไป AI ให้โค้ดที่ถูกต้องได้อย่างรวดเร็ว ส่วนคนเป็นผู้ตรวจสอบว่า "ถูกต้องในสภาพแวดล้อมของเราหรือไม่" จุดตรวจสอบนั้นเองที่กลายเป็นการตัดสินใจออกแบบของระบบติดตามแหล่งที่มา
ก่อนหน้านี้ผมบอกไปแล้วว่าหากคนแก้ไข _source_map.tsv ด้วยมือ integrity_check จะให้ผลเป็น FAIL แล้วจับได้อย่างไร
หลักการเรียบง่าย ทุกครั้งที่ track_source append หนึ่งบรรทัด จะนำช่องหลักของบรรทัดนั้น (asset_id, source_type, source, created, creator) มา serialize แล้วสร้างเป็นแฮช และสะสมไว้ในไฟล์แยกชื่อ .source_map.audit ส่วนการ audit จะอ่าน _source_map.tsv ขึ้นมาอีกครั้ง คำนวณแฮชใหม่ด้วยวิธีเดียวกัน แล้วเทียบรายการแฮชทั้งสองชุด
# ส่วน source_map audit ภายใน integrity_check
def audit_source_map():
fails = []
rows = read_tsv(SOURCE_MAP)
expected = read_lines(AUDIT_FILE) # แฮชที่สะสมไว้ตอน append
for i, row in enumerate(rows):
h = row_hash(row["asset_id"], row["source_type"],
row["source"], row["created"], row["creator"])
if i >= len(expected) or h != expected[i]:
fails.append(f"L{i+1} {row['asset_id']}: สงสัยว่าแก้ไขด้วยมือ (แฮชไม่ตรงกัน)")
if len(rows) != len(expected):
fails.append(f"จำนวนแถวไม่ตรงกัน: tsv={len(rows)} audit={len(expected)}")
return fails
สมมติว่ามีคนแก้ source ของ data_boss_hp_v3 ในชีตด้วยมือเป็น decision_D2026_Q2_099 แฮชของบรรทัดนั้นจะคลาดเคลื่อนจากแฮชเดิมที่สะสมไว้ใน audit และการตรวจสอบจะให้ผลลัพธ์ดังนี้
[FAIL] source_map audit
L3 data_boss_hp_v3: สงสัยว่าแก้ไขด้วยมือ (แฮชไม่ตรงกัน)
→ การเปลี่ยนแปลงที่ไม่ผ่าน track_source() แหล่งที่มาต้องบันทึกผ่านเส้นทางโค้ดเท่านั้น
ทำไมการบังคับนี้จึงสำคัญ ถ้าอนุญาตให้แก้ด้วยมือ สุดท้ายเมื่อใครบางคนรีบ เขาก็จะเติมแหล่งที่มาเข้าไปแบบ "ให้ดูสมเหตุสมผล" ในวินาทีนั้น lineage จะตกต่ำลงจากความจริงกลายเป็นไฟล์ที่บรรจุการคาดเดาของใครบางคน audit FAIL ติดเขี้ยวให้กับกฎที่ว่า "แหล่งที่มาต้องผ่านเส้นทางอัตโนมัติเท่านั้น" ระบบ verification ใน §24.1 มัด audit นี้รวมกับการตรวจสอบอื่น ๆ แล้วรันใน CI
เหตุผลที่แท้จริงของการบันทึกแหล่งที่มาโดยอัตโนมัติอยู่ที่การคิวรีย้อนทาง "ต้นฉบับ X เปลี่ยนไปแล้ว อะไรได้รับผลกระทบบ้าง?"
def find_derivatives(source_id: str):
return [
row for row in read_tsv(SOURCE_MAP)
if row["source"] == source_id
]
# การใช้งาน: decision_D2026_Q2_017 ถูกพลิกในการประชุม
deps = find_derivatives("decision_D2026_Q2_017")
# → [spec_combat_v3, data_boss_hp_v3, ...]
สมมติว่า decision_D2026_Q2_017 ถูกพลิกในการประชุมครั้งถัดไป ทำให้พลังชีวิตเฟส 1 ของบอสเปลี่ยนจาก 48,000 เป็น 50,000 เมื่อเรียก find_derivatives แอสเซตที่สืบทอดทั้งหมดที่แขวนอยู่กับการตัดสินใจนี้จะออกมาทันที — เอกสารสเปกการต่อสู้ ชีตข้อมูลพลังชีวิต การแจ้งเตือนจะส่งไปยังผู้รับผิดชอบแต่ละแอสเซต และอุบัติเหตุที่ "แอสเซตที่ยังมองการตัดสินใจเก่า" ยังค้างอยู่ในบิลด์จะลดลงจากหลายเคสต่อไตรมาสเหลือเกือบ 0
แหล่งที่มาที่เขียนด้วยมือไม่อาจทำให้การคิวรีย้อนทางนี้สำเร็จได้ ถ้าแหล่งที่มาเป็นข้อความอิสระ decision_D2026_Q2_017 ก็จะถูกเขียนเป็น "การตัดสินใจ Q2 017" ในบางบรรทัด และ "การตัดสินใจประชุมครั้งที่ 17 ไตรมาส 2" ในบางบรรทัด ทำให้การจับคู่พังลง ต้องมีรูปแบบมาตรฐานของ _source_map.tsv และการบันทึกอัตโนมัติของ track_source เสียก่อน การแพร่กระจายการเปลี่ยนแปลงจึงจะทำงานได้
_source_map.tsv เมื่อดูทีละบรรทัดจะดูเป็นแบบแบนราบ แต่เมื่อ source หนึ่งกลายเป็น source ของอีกแอสเซตหนึ่ง สายเลือดของข้อมูลก็ก่อตัวเป็นโซ่ขึ้นมา เมื่อกางโซ่นั้นออกในหน้าจอเดียว ความน่าเชื่อถือของอินพุตในการตัดสินใจก็จะมองเห็นได้ mermaid นี้ดึงออกมาโดยตรงจากไปป์ไลน์การสร้างไดอะแกรมอัตโนมัติใน §24.2 ที่อ่าน _source_map.tsv — เท่ากับพิสูจน์แอสเซตของตัวเองด้วยเทคนิคของตัวเอง
graph LR
Meeting["mtg_battle_2026-04-18
(การประชุม)"] --> Proposal["P2026_Q2_017
(ข้อเสนอ)"]
Proposal --> Decision["D2026_Q2_017
(การตัดสินใจ·ยืนยัน 48000)"]
Decision --> Spec["spec_combat_v3
(สเปกการต่อสู้)"]
Decision --> Data["data_boss_hp_v3
(ชีตพลังชีวิต)"]
Data --> Build["build_2026-05-20
(อัลฟ่าบิลด์)"]
Build --> UserData["data_user_voice_W21
(การวัดจากผู้ใช้)"]
UserData -.อินพุตของการตัดสินใจถัดไป.-> Decision
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class Meeting,Decision human;
class Proposal,Spec,Data,Build,UserData data;
วงจรเกิดขึ้นอย่างเป็นธรรมชาติ บิลด์ให้กำเนิดข้อมูลผู้ใช้ และข้อมูลผู้ใช้กลายเป็นอินพุตของการตัดสินใจครั้งถัดไป เมื่อมองเห็นวงจรนี้ คำถาม "ค่านี้มาจากไหน" ก็กลายเป็นเส้นทางบนหน้าจอ คำถามเย็นวันศุกร์ของเพื่อนร่วมทีม B ในกราฟนี้ก็เป็นเพียงการย้อนรอย Data → Decision ขึ้นไปครั้งเดียว
ในโปรเจกต์ A ผมเปรียบเทียบก่อนและหลังการนำระบบ lineage มาใช้ ค่าเวลาด้านล่างเป็นการประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) ควรดูที่ทิศทางและสัดส่วนของความต่างมากกว่าค่าสัมบูรณ์ ส่วนจำนวนเคสเป็นค่าที่วัดจริง รวบรวมจากบันทึก audit รายไตรมาส
| รายการ | ไม่มี lineage | มี lineage | ลักษณะ |
|---|---|---|---|
| เวลาในการระบุแหล่งที่มาของข้อมูล | 1\~2 ชั่วโมง | 1\~2 นาที | การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) |
| หลักฐานตรวจสอบความน่าเชื่อถือของข้อมูล | ความทรงจำของซีเนียร์ | ค้นแหล่งที่มาได้ทันที | เชิงคุณภาพ |
| การพลาดของที่สืบทอดเมื่อต้นฉบับเปลี่ยน | 5\~8 เคส/ไตรมาส | 0\~1 เคส | วัดจริงจากบันทึก audit |
| การพลาดตรวจสอบทางกฎหมายของแอสเซตภายนอก | อาจเกิดขึ้น | 0 เคส (บังคับบันทึก) | วัดจริงจากบันทึก audit |
| เวลาที่ใช้ในการ audit รายไตรมาส | 1\~2 วัน | 2\~3 ชั่วโมง | การประมาณของผู้เขียน (ยังไม่ได้ตรวจสอบ) |
ตัวเลขที่หนักแน่นที่สุดคือแถว "การพลาดของที่สืบทอดเมื่อต้นฉบับเปลี่ยน" เพราะมันนับได้ เนื่องจากบันทึก audit เก็บ ID การตัดสินใจและแอสเซตที่สืบทอดที่ถูกพลาดไว้ตรง ๆ ส่วนค่าเวลานั้นผันแปรอย่างมากตามสภาพแวดล้อมการวัด (ขนาดทีม·จำนวนแอสเซต) ผมจึงระบุไว้ชัดว่าเป็นการประมาณ ทิศทางนั้นชัดเจน — เมื่อแหล่งที่มาถูกบันทึกโดยอัตโนมัติ การติดตามก็เปลี่ยนจากการพึ่งความทรงจำมาเป็นการค้นหา
| รูปแบบความล้มเหลว | ทางแก้ |
|---|---|
| เติมแหล่งที่มาด้วยมือย้อนหลัง | บันทึกอัตโนมัติในจังหวะสร้างด้วย track_source |
| รูปแบบแหล่งที่มาต่างกันในแต่ละบรรทัด | มาตรฐานแท็บ _source_map.tsv + บังคับรูปแบบ |
| พลาดตรวจสอบทางกฎหมายของแอสเซตภายนอก | บังคับ legal_review ในการตรวจสอบ source_type |
แก้ไข _source_map.tsv ด้วยมือ |
เทียบแฮชด้วย audit ของ integrity_check ให้ FAIL |
| ทิ้งของที่สืบทอดเมื่อต้นฉบับเปลี่ยน | คิวรีย้อนทาง find_derivatives + การแจ้งเตือน |
| อธิบายสายเลือดด้วยข้อความเท่านั้น | สร้าง mermaid อัตโนมัติเพื่อมองเห็นในหน้าจอเดียว |
จุดร่วมของหกทางแก้คือไม่พึ่งความขยันของคน การบันทึกอัตโนมัติ·การบังคับรูปแบบ·การเทียบแฮช·การคิวรีย้อนทาง ทั้งหมดเป็นสิ่งที่ระบบทำ เพราะเหตุผลเดียวที่ทำให้การติดตามแหล่งที่มาพังทลายคือ "คนหลงลืม"
ส่วนที่ 24 เป็นสี่แขนงที่ค้ำความน่าเชื่อถือของการดำเนินงานด้วยการทำให้เป็นอัตโนมัติ บทที่ 1 รวมการตรวจสอบไว้ที่จุดเดียวด้วย verification บทที่ 2 วาดโครงสร้างด้วยการสร้าง mermaid อัตโนมัติ บทที่ 3 เชื่อมและจัดชั้นเอกสารด้วย wikilink และ document hierarchy และสุดท้ายในบทที่ 4 นี้ ผนึกความน่าเชื่อถือของข้อมูลด้วยแหล่งที่มาและสายเลือด
ประโยคหนึ่งที่ร้อยทั้งสี่บทเข้าด้วยกันคือ ความน่าเชื่อถือของการดำเนินงานมาจากบันทึกของระบบ ไม่ใช่ความทรงจำของคน เช่นเดียวกับที่ verification ถามโดยอัตโนมัติว่า "ผลผลิตนี้ตรงตามกฎหรือไม่" lineage ก็ตอบโดยอัตโนมัติว่า "ข้อมูลนี้มาจากไหน" แก่นอยู่ที่ว่าทั้งคู่ไม่พังทลายแม้คนจะลืม
โนว์ฮาวการดำเนินงานนี้มีเนื้อเดียวกันกับปรัชญาการบูรณาการ Layer ของทั้งเล่ม เป็นโซ่หนึ่งสายที่วิสัยทัศน์ (การทำเป็นสินทรัพย์·ความน่าเชื่อถือ) ไหลลงสู่ระบบ (กฎแหล่งที่มา) ระบบไหลลงสู่ข้อมูล (_source_map.tsv) และข้อมูลไหลลงสู่บิลด์·QA (audit·การอัปเดตอัตโนมัติ) โซ่สายนั้นเองคือ lineage
_source_map.tsv และ source_type 5 ประเภทเป็นโครงของการแพร่กระจายการเปลี่ยนแปลงและการบังคับทางกฎหมายsetup. สร้าง _source_map.tsv ที่รากของโปรเจกต์ด้วยส่วนหัวหนึ่งบรรทัด (asset_id\tsource_type\tsource\tcreated\tcreator\tnotes) แล้ววาง source_tracker.py ด้านบนไว้ จากนั้นแขวนการเรียก track_source(...) ไว้ที่ท้ายสคริปต์ export·ลงทะเบียนแอสเซต
prompt. หากต้องการฟังก์ชันบันทึกแหล่งที่มาอัตโนมัติ ให้ร้องขอ Claude แบบนี้
ช่วยเขียนฟังก์ชัน Python ที่ append หนึ่งบรรทัดลงใน _source_map.tsv
(คั่นด้วยแท็บ คอลัมน์: asset_id, source_type, source,
created, creator, notes) ให้หน่อย
จำกัด source_type ไว้ 5 ประเภท และถ้าเป็น external_reference แต่ใน notes
ไม่มี legal_review ให้โยน ValueError ส่วน creator ใช้ getpass.getuser()
verify. ตรวจสอบสองอย่างด้วยตัวเอง (1) เรียกด้วย external_reference โดยทิ้ง notes ไว้ว่าง ดูว่าเกิด ValueError หรือไม่ (2) แก้ช่อง source ของ _source_map.tsv ด้วยเท็กซ์เอดิเตอร์ไปหนึ่งตัวอักษร แล้วรัน source_map audit ของ integrity_check ดูว่าขึ้น FAIL หรือไม่ ถ้าทั้งสองอย่างถูกขวางไว้ แสดงว่าเส้นทางแหล่งที่มาปิดสนิทแล้ว
หากทำงานคนเดียว ไฟล์ _source_map.tsv ไฟล์เดียวกับฟังก์ชัน track_source ฟังก์ชันเดียวก็เพียงพอแล้ว ส่วน integrity audit·การคิวรีย้อนทาง·การทำ mermaid อัตโนมัติ ค่อยทยอยติดทีละอย่างเมื่อแอสเซตเกินหลายสิบชิ้นจนแหล่งที่มาเริ่มสับสนก็ได้ จุดเริ่มต้นคือนิสัยเพียงหนึ่งเดียว — "เมื่อจดค่าตัวเลข ก็ทิ้งแหล่งที่มาหนึ่งบรรทัดไว้ที่เดียวกันโดยอัตโนมัติ"
จุดเริ่มต้นของหนังสือเล่มนี้คือช่องคำถามของ LLM เพียงช่องเดียว เริ่มจากประโยคเดียวว่า "ช่วยจัดเอกสารออกแบบให้หน่อย" แล้ว AI ก็เข้ามาในห้องออกแบบเกมทั้งห้อง จุดที่อยู่ตอนนี้ไม่ใช่จุดสิ้นสุดของการเปลี่ยนแปลงนั้น แต่เป็นปมที่ผูกไว้ของช่วงหนึ่งเท่านั้น
จากจุดที่ผ่านชีวิตเป็นนักออกแบบเกมมา 24 ปี มีบางสิ่งที่เปลี่ยนไปอย่างชัดเจน การผลิตจำนวนมากถูกดูดซับเข้าไปในเครื่องมือ บันทึกการประชุมกลายเป็นสินทรัพย์ และการตัดสินใจสามารถติดตามย้อนกลับได้
อย่างไรก็ตาม ไม่ใช่ทุกสาขาที่การเปลี่ยนแปลงไปถึงด้วยความเร็วเท่ากัน การออกแบบระบบและการปรับสมดุลถูกการดูดซับด้วยเครื่องมือเข้ามาอย่างรวดเร็ว แต่เนื้อเรื่องและการกำกับศิลป์ในหลายกรณียังคงอยู่ในขั้นการประยุกต์เชิงอนุรักษ์ ประเด็นสำคัญไม่ใช่ว่าสาขาไหนเร็วกว่า การยอมรับข้อเท็จจริงที่ว่าจุดที่การเปลี่ยนแปลงเข้ามานั้นต่างกันในแต่ละสาขา คือจุดที่การตัดสินใจครั้งถัดไปเริ่มต้นขึ้น
ไม่ได้หมายความว่านักออกแบบเกมทำงานน้อยลง แต่หมายความว่าได้ทำงานที่ต่างออกไปในเวลาเท่าเดิม คือการย้ายจากการผลิตจำนวนมากไปสู่ความหมาย จากการจัดระเบียบไปสู่การตัดสินใจ เพราะในจุดที่เครื่องมือดูดซับการผลิตจำนวนมากไปแล้ว งานที่มีความหมายไม่ได้เข้ามาเติมเต็มโดยอัตโนมัติในทันที จึงมีช่วงเวลาที่แปลกๆ ที่ต้องเริ่มกำหนดใหม่ก่อนว่าจะไม่ทำอะไร และช่วงนี้ดำเนินต่อไปอยู่พักหนึ่ง
มีสิ่งที่ไม่เปลี่ยนไปด้วยเช่นกัน เกมมีไว้เพื่อมนุษย์ วิสัยทัศน์ของเกมตัดสินโดยมนุษย์ และคำมั่นที่ว่าจะสร้างเกมที่เคารพเวลาของผู้ใช้ ยังคงเหมือนเดิม
เครื่องมือก็คือเครื่องมือ ส่วนทิศทางคือมนุษย์ ทว่าผมตั้งใจจะทบทวนประโยคนี้ใหม่ทุกปี เพราะหากไม่ทบทวน ในจุดที่เครื่องมือแข็งแกร่งเพียงพอแล้ว ค่าเริ่มต้นที่เชื่ออย่างคลุมเครือว่า "ถึงอย่างไรการตัดสินใจก็เป็นของมนุษย์" จะค่อยๆ แทรกตัวเข้ามาอย่างเงียบๆ ช่วงเวลาที่คำว่า "ทิศทางคือมนุษย์" ฟังดูเหมือนคำมั่นที่ปลอดภัย แท้จริงแล้วคือช่วงที่อันตรายที่สุด
หนังสือเล่มนี้เป็นบันทึก ณ ช่วงเวลาหนึ่ง เครื่องมือ AI วิวัฒนาการอย่างรวดเร็ว และมีความเป็นไปได้สูงว่าอีก 1 ปีข้างหน้า บางส่วนของหนังสือเล่มนี้จะล้าสมัยไปแล้ว แต่ผมเชื่อว่ารูปแบบหลัก — การบูรณาการ Layer การติดตามการตัดสินใจ verification gate (ด่านที่มนุษย์หรือเครื่องตรวจสอบผลลัพธ์) การตรวจสอบโดยมนุษย์ การเห็นพ้องของทีม — ยังคงใช้ได้แม้เครื่องมือจะเปลี่ยนไป ในทางกลับกัน ยิ่งเครื่องมือแข็งแกร่งขึ้นเท่าไร การใช้งานที่ไม่ได้ทำงานอยู่บนรูปแบบเหล่านี้ก็ยิ่งพังเร็วขึ้น
การบูรณาการ Layer ไม่ใช่เพียงการทำให้ภาษาในการทำงานร่วมกันระหว่างสาขาเป็นหนึ่งเดียว แต่เป็นการเปิดทางไปสู่การสร้างแบบโพรซีเดอรัล (procedural generation) และระบบอัตโนมัติไว้ล่วงหน้า ในระหว่างที่เริ่มจากการประยุกต์เชิงอนุรักษ์ ที่นักเขียนป้อนบริบทเข้าไปทีละบรรทัด แล้วขยายขั้นไปสู่การประยุกต์เชิงก้าวหน้า ที่ระบบสร้างขึ้นโดยอัตโนมัติจากสถานะของโลกในเกม การตรวจสอบโดยมนุษย์ก่อนถึงขั้นที่ย้อนกลับไม่ได้ เช่น การบันทึกเสียง การแคปเจอร์ และการสร้างไลฟ์บิลด์ ทำหน้าที่เป็นตาข่ายนิรภัยด่านสุดท้าย ยิ่ง LLM ฉลาดขึ้นเท่าไร คุณค่าของโครงนี้ก็ไม่ได้ลดลง ในทางกลับกัน น้ำหนักของการตัดสินใจที่มนุษย์ต้องตรวจสอบกลับยิ่งหนักขึ้น
หากพวกคุณนำรูปแบบในหนังสือเล่มนี้ไปดัดแปลงให้เข้ากับสภาพแวดล้อมของตนเอง ก็เท่ากับว่าพวกคุณกำลังเขียนเวอร์ชันถัดไปของหนังสือเล่มนี้ ขึ้นอยู่กับขนาดของบริษัทและแนวเกม ขั้นการพัฒนา และองค์ประกอบของทีม บางรูปแบบสามารถนำไปใช้ได้ทันที และบางรูปแบบต้องประกอบขึ้นใหม่ การแยกแยะระหว่างสิ่งที่นำไปใช้ได้กับสิ่งที่ต้องประกอบขึ้นใหม่ ตัวมันเองนั่นแหละคืองานที่มีความหมายชิ้นแรก
เหนือสิ่งอื่นใด ผมขอขอบคุณอย่างสุดซึ้งต่อคณะผู้บริหารและท่านผู้แทนของ SCYBS Games ที่อนุญาตให้ตีพิมพ์หนังสือเล่มนี้ หากปราศจากการตัดสินใจที่เปิดทางให้ผมได้แบ่งปันเวิร์กโฟลว์ที่พัฒนาขึ้นอย่างเป็นธรรมชาติในบริษัทสู่วงการ หนังสือเล่มนี้ก็คงไม่มีอยู่ ผมขอขอบคุณเพื่อนร่วมงานทุกคนที่เดินบนเส้นทางของการสร้างเกมร่วมกัน บางครั้งแยกกัน ตลอด 24 ปี ขอบคุณคู่ชีวิตที่เคียงข้างผมมากว่า 20 ปี และขอบคุณแมวเปอร์เซียก็องจี (Kkongji) ที่อยู่เคียงข้างจนอายุครบ 23 ปี และผมขอส่งความรู้สึกเดียวกันนี้ถึงโกมิ (Gomi) สุนัขพันธุ์ปอมเมอเรเนียนที่บัดนี้ไม่อยู่เคียงข้างแล้ว ผู้ซึ่งจะมีอายุสิบเก้าปีตลอดไป (ก็องจีและโกมิคือชื่อสัตว์เลี้ยงของผม)
ผมคิดว่าประสบการณ์ที่ความสำเร็จก่อนวัยกลายเป็นยาพิษของชีวิต เพียงครั้งเดียวก็เพียงพอแล้ว เพื่อจะไม่ลืมครั้งนั้น ผมจึงฝึกฝนขัดเกลาตนเองและเรียนรู้ใหม่อยู่เกือบ 24 ปี หนังสือเล่มนี้ก็ใกล้เคียงกับปมที่ผูกไว้ ณ ช่วงเวลาหนึ่งของการเรียนรู้นั้น
สุดท้ายนี้ ผมขอส่งความขอบคุณไปยังเครื่องมือ AI ที่ช่วยในการเขียนหนังสือเล่มนี้ ในระหว่างที่ปิดต้นฉบับ โมเดลใหม่ของ Claude อย่าง Fable ก็ออกมา ซึ่งสะท้อนว่าฉบับนี้กำลังเปลี่ยนแปลงแบบวันต่อวัน
หากหนังสือเล่มนี้สามารถเพิ่มเติมแม้เพียงบรรทัดเดียวให้กับการตัดสินใจครั้งถัดไปของใครสักคน เพียงเท่านั้นก็เพียงพอแล้ว
อี มินซู, ปี 2026
ภาคผนวกนี้รวบรวมสภาพแวดล้อมเครื่อง PC ในบริษัท A ซึ่งเป็นสตูดิโอพัฒนา MMORPG ที่ผู้เขียนดำเนินงานอยู่ ตั้งแต่ฮาร์ดแวร์ เครื่องมือ สินทรัพย์ความรู้ ไปจนถึงข้อมูลตรวจสอบ ไว้ในที่เดียวเป็นรายการสินทรัพย์ ในเนื้อหาหลักหลายแห่งมีการกล่าวถึงว่า "ด้วยเครื่องมือแบบนี้" "ด้วยโครงสร้าง atom แบบนี้" "ด้วยรายงานแบบนี้" ผู้เขียนจึงจัดทำขึ้นเพื่อแสดงให้เห็นในพริบตาว่าสิ่งเหล่านั้นมีอยู่จริงด้วยขนาดและการผสมผสานเช่นใด ชื่อจริงและวิสามานยนามทั้งหมดถูกทำให้ไม่ระบุตัวตน และเนื่องจากตัวเลขเปลี่ยนแปลงไปตามช่วงเวลา จึงขอให้อ่านเป็นสัดส่วนและองค์ประกอบ ไม่ใช่ค่าสัมบูรณ์
ภาคผนวกนี้อ่านได้สองวิธี วิธีแรกคือนำมาเปรียบเทียบกับสภาพแวดล้อมของตนเองทีละรายการ เมื่อลองเติมคำตอบของ "ผมใช้เอนจินอะไร ใช้เครื่องมือทำงานร่วมกันอะไร สินทรัพย์ความรู้สะสมอยู่ในรูปแบบใด" ลงในช่องเดียวกัน ช่องว่างของตนเองก็จะปรากฏชัด อีกวิธีหนึ่งคือดูความสมดุลขององค์ประกอบ การมีเครื่องมือมากไม่ได้แปลว่าเป็นสภาพแวดล้อมที่ดี สิ่งที่สำคัญคือทั้งห้าแกน ได้แก่ เอนจิน ออกแบบ อาร์ต ทำงานร่วมกัน และ AI ขบเกี่ยวกันโดยไม่ขวางกันหรือไม่ ขอให้ดูที่โครงสร้างการประสานนั้นมากกว่าแต่ละรายการ
อย่างแรกสุดคือฮาร์ดแวร์และระบบปฏิบัติการที่เป็นรากฐาน เมื่อเริ่มใช้เครื่องมือ AI อย่างจริงจัง จะมีกรณีที่ต้องรันโมเดล diffusion หรือ STT บนเครื่องโลคัล ดังนั้นความเหลือเฟือของหน่วยความจำและ GPU จึงกลายเป็นความเร็วในการทำงานโดยตรง ขอให้มองสเปกด้านล่างนี้เป็นเส้นฐานที่ใกล้เคียงกับขีดล่างที่ว่า "ระดับนี้ก็ทำงานไหลลื่นไม่ติดขัด"
| รายการ | สเปก |
|---|---|
| CPU | ระดับเดสก์ท็อปเวิร์กสเตชัน |
| RAM | 64GB ขึ้นไป |
| GPU | สำหรับพัฒนา UE5 (รองรับ CUDA) |
| ที่เก็บข้อมูล | SSD 2TB + แชร์ผ่าน NAS |
| จอภาพ | จอ 27 นิ้ว 2 ตัว |
สองบรรทัด RAM และ GPU คือหัวใจ เพราะมักมีช่วงเวลาที่ต้องเปิดเอดิเตอร์ของเอนจิน เครื่องมือเสริม LLM โลคัล และการสร้างภาพพร้อมกัน
| รายการ | ค่า |
|---|---|
| OS | Windows 11 Pro |
| เวอร์ชวลไลเซชัน | WSL2 (Ubuntu) เมื่อจำเป็น |
| สำรองข้อมูล | อัตโนมัติรายวัน |
WSL2 ไม่ได้เปิดทิ้งไว้ตลอด แต่ดึงมาใช้เฉพาะเมื่อต้องรันเครื่องมือเฉพาะของลินุกซ์ การที่การสำรองข้อมูลทำงานอัตโนมัติทุกวันคือบรรทัดที่สำคัญที่สุดในตารางนี้
เครื่องมือแบ่งออกเป็นห้าสาย ได้แก่ เอนจินและทูล ออกแบบและวางแผน อาร์ต ทำงานร่วมกันและปฏิบัติการ และ AI กับ LLM ไม่ใช่ว่าคนเดียวจะใช้ครบทั้งห้าสาย แต่ถ้าเป็นนักออกแบบเกม (Game Designer) ก็จะสลับไปมาระหว่างสามสาย คือ ออกแบบและวางแผน ทำงานร่วมกันและปฏิบัติการ และ AI กับ LLM ทุกวัน ขอให้สังเกตว่าแต่ละสายมีรูปแบบเป็น "หนึ่งถึงสองตัวที่จำเป็น + ตัวเสริม"
| เครื่องมือ | การใช้งาน |
|---|---|
| Unreal Engine 5.7 ขึ้นไป | เอนจินหลัก |
| Visual Studio | โค้ด |
| Rider | C# IDE (เสริม) |
| Perforce หรือ SVN | จัดการเวอร์ชันโค้ดและสินทรัพย์ |
เอนจินกับการจัดการเวอร์ชันเป็นคู่กัน นักออกแบบเกมเองก็ต้องใช้ไคลเอนต์จัดการเวอร์ชันให้เป็นด้วย เพราะทั้งชีตข้อมูลและเอกสารข้อกำหนดต่างหมุนเวียนอยู่ใน repository เดียวกัน
| เครื่องมือ | การใช้งาน |
|---|---|
| Excel | ชีตข้อมูล + แมโคร VBA |
| เอดิเตอร์ Markdown | เอกสารข้อกำหนดและบันทึกการประชุม |
| Figma | UI และไวร์เฟรม |
| Mermaid | ไดอะแกรม |
นี่คือโต๊ะทำงานประจำวันของนักออกแบบเกม Excel คือฐานทัพของข้อมูล Markdown คือฐานทัพของงานเขียน และยังเป็นสองจุดที่เครื่องมือ AI เข้ามาเกาะลึกที่สุด เหตุที่ Mermaid ครองหนึ่งช่องก็เพราะตามที่เน้นไว้ในเนื้อหาหลัก แผนภาพคือภาษาของการตกลงร่วมกัน
| เครื่องมือ | การใช้งาน |
|---|---|
| Maya / Blender | 3D |
| Substance 3D | เท็กซ์เจอร์และวัสดุ |
| Photoshop | 2D และภาพประกอบ |
| Stable Diffusion(SDXL) / ComfyUI | โฮสต์เองเพื่อผลิตคอนเซปต์และเท็กซ์เจอร์หลักเป็นจำนวนมาก (LoRA·ControlNet) |
| Midjourney | มูดบอร์ดช่วงต้น (เสริม) |
แม้ไม่ใช่เครื่องมือที่นักออกแบบเกมใช้โดยตรง แต่เมื่อต้องส่งคอนเซปต์ไปมากับสายอาร์ต การรู้ว่าฝั่งนั้นมีเครื่องมืออะไรอยู่จะทำให้ความละเอียดของคำขอเปลี่ยนไป การผลิตหลักจำนวนมากมี Stable Diffusion(SDXL)/ComfyUI ที่โฮสต์เองเป็นแกนหลัก เพราะไม่ต้องอัปโหลดสินทรัพย์ขึ้นภายนอกจึงรักษา IP ไว้ได้ และด้วย LoRA ของตัวละครกับ ControlNet ก็ควบคุมความสม่ำเสมอของตัวละครคนเดียวกันได้ในทุกครั้งที่สร้างซ้ำ ส่วนเครื่องมือแบบปิดอย่าง Midjourney ใช้เป็นตัวเสริมเฉพาะในมูดบอร์ดช่วงต้นที่เพิ่งเริ่มคลำหาโทนของโปรเจกต์เท่านั้น และไม่ใช้กับการผลิตหลักจำนวนมากที่ต้องคุมความสม่ำเสมอและการสร้างซ้ำ
| เครื่องมือ | การใช้งาน |
|---|---|
| เครื่องมือทำงานร่วมกัน (ClickUp) | งาน (task) |
| แชตภายในทีม | สื่อสารแบบเรียลไทม์ |
| วิกิที่สร้างขึ้นเอง | วิกิและเอกสารระยะยาว |
| พอร์ทัลเว็บที่สร้างขึ้นเอง | อินเทอร์เฟซรวม (20.3) |
แกนเวลาของการสื่อสารเป็นตัวแบ่งเครื่องมือ การสื่อสารแบบเรียลไทม์ที่ต้องการความทันทีใช้แชตภายในทีม สิ่งที่ต้องทำใช้เครื่องมือทำงานร่วมกัน (ทีมเราคือ ClickUp) ความรู้ที่ต้องเก็บไว้ระยะยาวใช้วิกิที่สร้างขึ้นเอง แม้จะเปลี่ยนแทร็กเกอร์เป็น JIRA·Redmine วิกิเป็น Confluence·Notion หรือแชตจะเป็นอะไรก็ตาม แต่กระแสของหนังสือเล่มนี้ก็ยังคงเดิม พอร์ทัลเว็บที่สร้างขึ้นเองเป็นช่องทางรวมที่เชื่อมทั้งสามอย่างนี้กับเครื่องมือ AI ไว้ในหน้าจอเดียว และจะกล่าวถึงโดยละเอียดในหัวข้อ 20.3
| เครื่องมือ | การใช้งาน |
|---|---|
| Claude (Opus + Sonnet) | LLM หลัก |
| GPT-4 | ทางเลือก |
| Whisper (โฮสต์เอง) | รู้จำเสียงพูด (STT) |
| Stable Diffusion | สร้างภาพ (โฮสต์เอง) |
| เซิร์ฟเวอร์ MCP | รวมเครื่องมือ (20.4) |
ตัวหลักวางไว้ที่ Claude และวาง GPT-4 ไว้เป็นการตรวจสอบไขว้และทางเลือก หลักการที่ว่าเสียงและภาพที่อ่อนไหวจะไม่ส่งออกไปภายนอกแต่ประมวลผลด้วยการโฮสต์เอง บรรจุอยู่ในสองบรรทัดของ Whisper และ Stable Diffusion เซิร์ฟเวอร์ MCP คือกาวที่เสียบเครื่องมือเหล่านี้เข้าไปในกระแสการทำงาน และจะอธิบายโครงสร้างในหัวข้อ 20.4
atom คือชิ้นส่วนความรู้ที่ปล่อย "หน่วยย่อยที่สุดของการตัดสินใจ" ที่กล่าวถึงในเนื้อหาหลักออกมาเป็นไฟล์ ด้านล่างเป็นตารางที่แสดงว่า atom เหล่านั้นกระจายอยู่ตามสาขาต่าง ๆ อย่างไร เป็นภาพตัดขวาง ณ ช่วงเดือนพฤษภาคม 2026 ขอให้ดูว่าการตัดสินใจกระจุกอยู่ที่สาขาใดมากกว่าจำนวนสัมบูรณ์ จุดที่การตัดสินใจกระจุกตัวคือจุดที่โปรเจกต์นั้นขบคิดอย่างเข้มข้นที่สุด
| หมวดหมู่ | จำนวน atom | หมายเหตุ |
|---|---|---|
| combat | 47 | การตัดสินใจระบบการต่อสู้ |
| narrative | 38 | เนื้อเรื่อง 5 ชั้น |
| ui | 31 | UI·HUD |
| balance | 28 | ตัวเลขการปรับสมดุล |
| level | 22 | Level Design |
| character | 19 | ตัวละคร·voice_profile |
| meta·governance | 18 | กระบวนการและกฎ |
| qa·integrity | 16 | การตรวจสอบ |
| content | 14 | ผลิตเนื้อหาจำนวนมาก |
| operations | 14 | เวิร์กโฟลว์ปฏิบัติการ |
| external_reference | 12 | ข้อมูลภายนอก |
| economy | 11 | เศรษฐกิจและทรัพยากร |
| อื่น ๆ | 34 | อยู่ระหว่างจัดหมวดหมู่ |
การกระจายตัวที่ combat หนาที่สุดและ narrative ตามมาเป็นอันดับสอง สะท้อนชัดถึงลักษณะที่ว่าโปรเจกต์นี้เป็น MMORPG ที่เน้นการต่อสู้ และในขณะเดียวกันก็ไม่ยอมทิ้งสัดส่วนของเรื่องราว "อื่น ๆ 34" คือการตัดสินใจใหม่ที่ยังไม่ได้กำหนดหมวดหมู่ ถ้าช่องนี้โตขึ้นมากเกินไป ก็เป็นสัญญาณว่าถึงเวลาต้องปรับระบบจัดหมวดหมู่แล้ว ยอดรวม ณ เดือนพฤษภาคม 2026 คือ 304 รายการ
แม้จะมีเครื่องมือและความรู้ แต่ถ้าไม่มีกลไกตรวจสอบว่าสิ่งเหล่านั้นทำงานอย่างถูกต้องหรือไม่ คุณภาพก็จะไหลทรุดลง หัวข้อนี้แสดงกลไกตรวจสอบนั้นโดยแบ่งออกเป็นสองประเภท คือ รายงานที่ผลิตออกมาเป็นระยะ ๆ และการ์ดการตัดสินใจที่เก็บไว้เพื่อให้สามารถติดตามการตัดสินใจย้อนหลังได้
| รายงาน | ความถี่ |
|---|---|
| รายงานบิลด์รายวัน | ทุกวัน |
| รายงานช่องว่างอัลฟา | รายสัปดาห์ (10.3) |
| รายงานคุณภาพสปรินต์ | ทุกสองสัปดาห์ |
| รายงาน QA ของไมล์สโตน | ทุกไมล์สโตน |
| การทบทวนรายไตรมาส | รายไตรมาส |
ความถี่คือตัวบ่งบอกลักษณะของรายงาน สิ่งที่ทำทุกวันคือการตรวจสอบสถานะ รายสัปดาห์และทุกสองสัปดาห์คือการตรวจสอบแนวโน้ม ไมล์สโตนและไตรมาสคือการตรวจสอบทิศทาง จุดที่ AI ช่วยได้มากที่สุดคือการร่างต้นฉบับของรายงานที่ทำซ้ำ ๆ อย่างรายวันและรายสัปดาห์ และกรณีตัวอย่างนั้นจะกล่าวถึงในหัวข้อ 10.3
| ไตรมาส | จำนวนการตัดสินใจ |
|---|---|
| ไตรมาส 4 ปี 2025 | 132 |
| ไตรมาส 1 ปี 2026 | 156 |
| ไตรมาส 2 ปี 2026 (กำลังดำเนินการ) | 89 |
| สะสม | 547 |
การที่แต่ละไตรมาสมีการตัดสินใจราว 100 รายการถูกบันทึกเป็นการ์ด ก็แสดงถึงหลักการปฏิบัติที่ว่าจัดการการตัดสินใจด้วยบันทึก ไม่ใช่ด้วยความทรงจำ ไตรมาส 2 ที่ 89 รายการเป็นค่าสะสม ณ กลางไตรมาสจึงเป็นค่าที่กำลังดำเนินการ และเมื่อถึงสิ้นไตรมาสก็จะไปถึงระดับเดียวกับไตรมาสก่อนหน้า กระแสที่การ์ดเหล่านี้สะสมแล้วเลื่อนขึ้นเป็น atom ในหัวข้อ A.3 คือแกนการเรียนรู้ของระบบนี้
การประชุมเป็นจุดที่เวลารั่วไหลมากที่สุด และเป็นจุดที่สัมผัสได้ถึงผลของเครื่องมือ AI ได้เร็วที่สุดด้วย ด้านล่างเป็นการกระจายตัวเฉลี่ยของการประชุมรายไตรมาสที่จัดกลุ่มตามหมวดหมู่ เป็นข้อมูลอ้างอิงเพื่อประเมินขนาดของอินพุตในระบบบันทึกการประชุมที่กล่าวถึงในหัวข้อ 17.3 เนื่องจากตัวเลขแกว่งไปมาในแต่ละไตรมาส จึงเขียนเป็นช่วง
| หมวดหมู่ | เฉลี่ยต่อไตรมาส |
|---|---|
| รายวัน (daily) | 65\~70 ครั้ง |
| การต่อสู้ (battle) | 35\~45 ครั้ง |
| อาร์ต (art) | 25\~30 ครั้ง |
| ปัญหา (issue) | 8\~15 ครั้ง |
| รีวิว (review) | 6\~10 ครั้ง |
| อื่น ๆ (1:1 และภายนอก) | 40\~50 ครั้ง |
การประชุมรายวันมีบ่อยที่สุด และการประชุมเกี่ยวกับการต่อสู้ตามมาเป็นอันดับสอง สิ่งที่มีนัยสำคัญคือมันมีรูปร่างเหมือนกับการกระจายตัวของ atom ในหัวข้อ A.3 การประชุมก็กระจุกในสาขาที่การตัดสินใจกระจุกตัว ยิ่งเป็นสภาพแวดล้อมที่มีการประชุมบ่อยเช่นนี้ ประโยชน์ของการจัดบันทึกการประชุมโดยอัตโนมัติก็ยิ่งมากขึ้น และการปฏิบัติงานจริงโดยละเอียดจะอธิบายในหัวข้อ 17.3
ตารางทั้งหมดที่ผ่านมาล้วนเป็นภาพถ่ายหนึ่งใบของสภาพแวดล้อมของผู้เขียน ไม่ใช่รายการให้คัดลอกไปใช้ตามนั้น แต่ขอให้ใช้เป็นแบบตัวอย่างในการลองจัดสภาพแวดล้อมของตนเองด้วยกรอบเดียวกัน ถ้าขนาดทีม แนวเกม หรือแพลตฟอร์มต่างกัน เครื่องมือ การกระจายตัวของ atom และสัดส่วนการประชุมก็จะต่างกันไป สิ่งที่สำคัญไม่ใช่ความตรงกันของรายการ แต่คือสี่ชั้นที่ว่า "รากฐาน → เครื่องมือ → ความรู้ → การตรวจสอบ" เชื่อมต่อกันไม่ขาดตอนในสภาพแวดล้อมของตนเองหรือไม่ ถ้าในสี่ชั้นนั้นมีช่องที่ว่างอยู่ ช่องนั้นคือจุดที่ต้องลงมือต่อไป
ภาคผนวกนี้สรุปขั้นตอนที่ผู้เขียนนำเครื่องมือและสกิลที่สร้างและใช้งานในโปรเจกต์ A ของบริษัท มาปรับใช้ใหม่บน PC ส่วนตัวและกับงานทั่วไป คำถามหลักมีเพียงข้อเดียว: "จะนำเฉพาะโครงของเครื่องมือที่เรียนรู้มาอย่างถูกต้องตามกฎหมายโดยไม่ละเมิดทรัพย์สินทางความรู้ของบริษัทได้อย่างไร" ภาคผนวกนี้แสดงให้เห็นว่าผู้เขียนขีดเส้นแบ่งนั้นอย่างไร นำอะไรมาและทิ้งอะไรไว้ และบันทึกการตัดสินใจนั้นไว้เป็นหลักฐานอย่างไร
วิธีอ่านภาคผนวกนี้เป็นดังนี้ ก่อนอื่นให้อ่านหลักการห้าข้อใน B.1 โดยเทียบกับสถานการณ์ของคุณเอง แล้วลองทำตามขั้นตอนใน B.3 หนึ่งรอบตามที่เขียนไว้ จากนั้นคัดลอกแบบฟอร์มบันทึกใน B.4 มากรอกให้ตรงกับเครื่องมือที่คุณต้องการยืม เนื่องจากเป็นงานที่เกี่ยวกับทรัพย์สินของบริษัท "การทำให้บันทึกไว้ได้" จึงสำคัญกว่า "การทำให้เร็ว" และภาคผนวกทั้งหมดนี้ถูกร้อยเรียงด้วยมุมมองนั้น
นี่คือหลักการห้าข้อที่ตกลงกันไว้ก่อนนำเครื่องมือมาใช้ ทั้งห้าข้อนี้ไม่ใช่ลำดับขั้น แต่เป็นเงื่อนไขที่ต้องรักษาพร้อมกัน หากข้อใดข้อหนึ่งพังลง ก็ให้ระงับการยืมนั้นไว้ก่อน สามข้อแรกคือขอบเขตทางเทคนิคของ "นำอะไรมา" และสองข้อหลังคือขอบเขตของกระบวนการในเรื่อง "นำมาอย่างไรให้ถูกต้องชอบธรรม"
| หลักการ | คำอธิบาย |
|---|---|
| 1. ไม่รวมทรัพย์สินทางความรู้ของบริษัท | ลบชื่อบริษัท ชื่อจริง และชื่อเฉพาะออก |
| 2. นำเฉพาะโครงของเครื่องมือ | ปิดกั้นข้อมูลในโดเมนของบริษัท |
| 3. ปรับให้ใช้ได้ทั่วไป | สร้างใหม่ให้เป็นกรณีการใช้งานทั่วไป |
| 4. อ้างอิงและระบุที่มาชัดเจน | ระบุชัดว่าเป็นเครื่องมือที่ยืมมาจากบริษัท |
| 5. ได้รับความเห็นชอบจากฝ่ายกฎหมายและฝ่ายบุคคล | ผ่านขั้นตอนการขออนุญาตจากบริษัท |
แถวที่สั่นคลอนบ่อยที่สุดคือข้อ 2 อัลกอริทึมและโครงสร้าง (โครง) นำมาใช้ได้ แต่หากรูปแบบข้อมูลของบริษัทที่โครงนั้นตั้งอยู่บนพื้นฐานถูกลากตามมาด้วย ก็เท่ากับว่านำทรัพย์สินทางความรู้มาในทันที งานแยกโครงออกจากข้อมูลคือเนื้อแท้ของการยืม
ตามหลักการ เครื่องมือที่นำมาบน PC ส่วนตัวจริง ๆ มีหกชนิด (ข้อมูล ณ เดือนพฤษภาคม 2026) ทั้งหมดมีจุดร่วมคือเป็นเครื่องมือที่จัดการกับข้อมูล ซึ่งไม่ใช่เรื่องบังเอิญ เพราะเครื่องมือประมวลผลข้อมูลนั้นแยกโครง (ตรรกะการแยกวิเคราะห์ การแปลง การแสดงผลภาพ) ออกจากโดเมน (รูปแบบเฉพาะของชีตในบริษัท) ได้ค่อนข้างง่าย
| เครื่องมือ | ต้นฉบับของบริษัท | ฉบับใช้ทั่วไปส่วนตัว |
|---|---|---|
| excel-reader | สกัดชีต xlsm และ VBA | ประมวลผล Excel แบบทั่วไป |
| relation-map-gen | HTML ความสัมพันธ์ FK | แผนผังความสัมพันธ์ข้อมูลแบบทั่วไป |
| schema-doc | สร้างสคีมา Markdown จากชีต | จัดทำเอกสารสคีมาแบบทั่วไป |
| table-creator | ผลิตตารางข้อมูลจำนวนมาก | สร้างตารางแบบทั่วไป |
| gdd-gen | สร้าง GDD อัตโนมัติ | สร้างเอกสารแบบทั่วไป |
| gdd-export | แปลงจาก Markdown เป็น xlsx หลายชีต | แปลง xlsx แบบทั่วไป |
หากเทียบช่องตรงกลางกับช่องขวาในตาราง จะเห็นชัดว่า "การทำให้ใช้ได้ทั่วไป" หมายถึงอะไร ฝั่งซ้ายเป็นชื่อที่มีโดเมนแฝงอยู่ เช่น "ชีตของบริษัท" "GDD" ส่วนฝั่งขวาเป็นชื่อที่ถอดโดเมนออกแล้ว เช่น "Excel ทั่วไป" "เอกสารทั่วไป" การที่บริษัทหายไปจากชื่อคือสัญญาณแรกของการทำให้ใช้ได้ทั่วไป
เมื่อแปลงหลักการ (B.1) ให้กลายเป็นการลงมือทำจริง ก็จะได้หกขั้นตอนด้านล่างนี้ จุดแยกที่สำคัญที่สุดคือขั้นที่ 2 และขั้นที่ 4 หากในขั้นที่ 2 แยกโครงออกจากโดเมนได้ไม่สะอาด ทุกขั้นตอนต่อจากนั้นจะปนเปื้อน และหากข้ามขั้นที่ 4 คือการขออนุญาตจากบริษัทไป ต่อให้ทำได้ดีเพียงใดก็จะกลายเป็นเครื่องมือที่ใช้งานไม่ได้
flowchart TD
A[1. ระบุเครื่องมือของบริษัท] --> B[2. แยกส่วนที่พึ่งพาบริษัท]
B --> B1[พึ่งพาข้อมูลและชื่อเฉพาะของบริษัท]
B --> B2[โครงของเครื่องมือ: อัลกอริทึม·โครงสร้าง]
B1 --> C[3. ลบการพึ่งพาบริษัท + แปลงเป็นตัวแปรทั่วไป]
B2 --> C
C --> D[4. ขออนุญาตบริษัท: ฝ่ายกฎหมาย·ผู้จัดการ]
D --> E[5. ปรับใช้และตรวจสอบบนสภาพแวดล้อม PC ส่วนตัว]
E --> F[6. ระบุที่มา + บันทึกการยืม]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class B2 code;
class D human;
class B1,F data;
ในหกขั้นตอนนี้ ช่องที่ใช้เวลานานที่สุดไม่ใช่งานเขียนโค้ด (ขั้นที่ 2·3) แต่เป็นขั้นที่ 4 คือการตกลงกับบริษัทและการผ่านฝ่ายกฎหมาย นั่นหมายความว่าด่านที่ใหญ่ที่สุดไม่ใช่เทคนิคแต่เป็นความไว้วางใจ ดังนั้นการยืมจึงดำเนินตามลำดับที่ตกลงกันก่อนแล้วค่อยปรับโค้ดทีหลังเสมอ
เครื่องมือที่ยืมมาต้องบันทึกไว้ควบคู่กันเสมอ เพราะอาจมีช่วงเวลาที่ต้องตอบคำถามว่า "เครื่องมือนี้มาจากไหน ลบอะไรออกไปบ้าง และได้รับความเห็นชอบจากใคร" ด้านล่างนี้คือแบบฟอร์มบันทึกที่ยกตัวอย่างด้วย excel-reader ซึ่งคุณสามารถคัดลอกโครงนี้ไปกรอกให้ตรงกับเครื่องมือของคุณเองได้เลย
---
tool: excel-reader (ฉบับใช้ทั่วไปส่วนตัว)
original_source: โปรเจกต์ A ของบริษัท
adopted: 2026-05
permission: ผู้จัดการบริษัท + ผ่านฝ่ายกฎหมาย
modifications:
- ลบการพึ่งพารูปแบบชีตของบริษัท
- ลบฟังก์ชันในโดเมนของบริษัท (xlsm VBA)
- ทำให้ใช้ทั่วไปด้วยการประมวลผล csv/xlsx แบบทั่วไป
- ลบการอ้างอิงชื่อบริษัทและชื่อจริงทั้งหมด
usage_in_book: อ้างอิงเป็นกรณีตัวอย่างเครื่องมือในหนังสือเล่มนี้ (ส่วนที่ 1·5·6·8 ฯลฯ)
---
ช่องวันที่ (adopted) ในแบบฟอร์มให้เขียนเป็นปี-เดือนที่แน่นอน เช่น 2026-05 การเขียนแบบเปิดกว้าง เช่น "ราวเดือนพฤษภาคม 2026" จะดูเหมือนช่องว่างที่รอเติมภายหลัง จึงควรตอกหมุดเวลาที่ยืนยันการยืมลงไปตรงนั้นเลย
ในบันทึกนี้ บรรทัดที่มีค่ามากที่สุดคือ permission และ modifications บรรทัดแรกพิสูจน์ว่าการยืมนั้นชอบธรรม บรรทัดหลังพิสูจน์ว่าได้ถอดอะไรออกไปบ้าง หากมีสองบรรทัดนี้ ต่อให้ภายหลังมีข้อสงสัยขึ้นมา ก็ยังมีหลักฐานให้ติดตามย้อนหลังได้
สิ่งที่ทิ้งไว้สำคัญพอ ๆ กับสิ่งที่นำมา ผู้เขียนได้บันทึกเครื่องมือของบริษัทที่ตั้งใจไม่ยืมไว้พร้อมเหตุผล จุดร่วมของเครื่องมือที่ทิ้งไว้คือ เป็นทรัพย์สินทางความรู้หลักของบริษัท หรือผูกติดอยู่กับโครงสร้างองค์กรของบริษัทอย่างลึกซึ้ง จนไม่สามารถแยกโครงออกจากโดเมนได้
| เครื่องมือ | เหตุผลที่ไม่ยืม |
|---|---|
| เครื่องมือระบบต่อสู้ของบริษัท | ทรัพย์สินทางความรู้หลักของบริษัท เป็นกรรมสิทธิ์เฉพาะของบริษัท |
| เครื่องมือเอกสารเนื้อเรื่องของบริษัท | พึ่งพาโลกในเกมของบริษัท |
| เครื่องมือ TF ระบบต่อสู้ของบริษัท | พึ่งพาโครงสร้างองค์กรของบริษัท |
| เครื่องมือฝ่ายบุคคล·การเงินของบริษัท | ไม่เหมาะกับสภาพแวดล้อมภายนอก |
ตรงกันข้ามอย่างชัดเจนกับเครื่องมือที่ยืมมาใน B.2 ซึ่งล้วนเป็น "การประมวลผลข้อมูล" สิ่งที่นำมาคือเครื่องมือที่แยกออกจากโดเมนได้ ส่วนสิ่งที่ทิ้งไว้คือเครื่องมือที่เป็นเนื้อเดียวกับโดเมน ความเป็นไปได้ในการแยกออกเป็นตัวกำหนดความเป็นไปได้ในการยืม
สุดท้ายนี้คือห้ารายการที่ต้องผ่านการตรวจสอบด้วยตนเองก่อนนำเครื่องมือมาใช้ ตารางนี้เป็นเช็กลิสต์ที่ตัดสินผ่าน/ไม่ผ่าน จะยืมได้ก็ต่อเมื่อผ่านครบทั้งห้ารายการเท่านั้น และหากติดแม้เพียงรายการเดียวก็ให้ระงับไว้ก่อน ไม่มีคำว่า "โดยรวมก็พอใช้ได้" เพราะงานที่เกี่ยวกับทรัพย์สินของบริษัทนั้นการผ่านบางส่วนใช้ไม่ได้
| รายการตรวจสอบ | เกณฑ์ผ่าน |
|---|---|
| ได้รับความเห็นชอบจากบริษัทแล้วหรือไม่ | ความยินยอมชัดแจ้งจากผู้จัดการ·ฝ่ายกฎหมาย |
| ผ่านการตรวจสอบจากฝ่ายกฎหมายแล้วหรือไม่ | มีการยืนยันเป็นลายลักษณ์อักษรหรือบันทึกไว้ |
| ลบทรัพย์สินทางความรู้ของบริษัทออกหมดแล้วหรือไม่ | การตรวจ grep watchlist พบ 0 รายการ |
| ตรวจสอบความเป็นสากลแล้วหรือไม่ | ยืนยันว่าทำงานได้ในสภาพแวดล้อมอื่นด้วย |
| มีขั้นตอนรับมือเมื่อเกิดเหตุหรือไม่ | กำหนดเส้นทางการติดตาม·การเรียกคืนไว้แล้ว |
ขออย่าอ่านห้ารายการนี้เป็นการผ่านห้าช่อง แต่ขอให้อ่านเป็นล็อกห้าดอก การนำสิ่งที่เรียนรู้จากบริษัทมาเป็นทรัพย์สินส่วนตัวอย่างชอบธรรมนั้นเป็นไปได้แน่นอน แต่ความชอบธรรมนั้นจะเกิดขึ้นได้ก็ต่อเมื่อล็อกทั้งห้าดอกนี้ถูกลงครบทุกดอกเท่านั้น
ภาคผนวกนี้คือตารางอ้างอิงที่รวบรวมสิทธิ์และการตั้งค่าของเครื่องมือและระบบที่อ้างถึงในเนื้อหาหลักไว้ในที่เดียว เนื้อหาหลักอธิบายว่า "เหตุใดจึงดำเนินการแบบนี้" แต่เมื่อจะนำไปปรับใช้กับสภาพแวดล้อมของตนเองจริง ๆ สิ่งที่ต้องการคือ "แล้วจริง ๆ ต้องใส่ค่าอะไรไว้ที่ไหน" ภาคผนวกนี้เติมช่องว่างตรงนั้นให้
เหตุผลที่เลือกค่าใดค่าหนึ่งสำคัญยิ่งกว่าตัวค่าการตั้งค่าเอง แทนที่จะคัดลอกตัวเลขในตารางไปตรง ๆ ขอให้อ่านคำอธิบายสั้น ๆ ใต้แต่ละหัวข้อแล้วปรับให้เข้ากับขนาดทีมและระดับความเสี่ยงของตน หากทำงานคนเดียวก็ไม่จำเป็นต้องแบ่งระดับสิทธิ์ และหากไม่มีงานจ้างภายนอกก็ตัดหัวข้อเกี่ยวกับงานจ้างภายนอกออกทั้งหมดได้
วิธีใช้ภาคผนวกนี้มีสองแบบ เมื่อตั้งค่าสภาพแวดล้อมครั้งแรกให้ไล่อ่านตั้งแต่ C.1 ตามลำดับ ใช้เป็นเช็กลิสต์เพื่อตรวจว่ามีหัวข้อใดตกหล่นหรือไม่ ในระหว่างการดำเนินงาน เมื่อเกิดเหตุขึ้นให้เปิด C.7 (การรับมือเหตุการณ์) ก่อนเพื่อหาแถวของเหตุการณ์นั้น แล้วย้อนขึ้นไปยังหัวข้อป้องกันที่อยู่เหนือขึ้นไป
คีย์ LLM API เชื่อมโยงโดยตรงกับค่าใช้จ่าย และเมื่อรั่วไหลก็นำไปสู่เหตุการณ์ทางการเงินทันที จึงต้องจัดการเรื่องการบริหารคีย์และระดับสิทธิ์เป็นอันดับแรก
| คีย์ | การจัดเก็บ |
|---|---|
| Anthropic API | ตัวแปรสภาพแวดล้อม + 1Password |
| OpenAI API | ตัวแปรสภาพแวดล้อม + 1Password |
| โฮสต์เอง | ภายในบริษัท |
ควรนำเข้าคีย์เข้าสู่ระบบผ่านตัวแปรสภาพแวดล้อมแทนที่จะเขียนในโค้ด และเก็บต้นฉบับไว้ในเครื่องมือบริหารความลับ (เช่น 1Password) เหตุการณ์ที่พบบ่อยที่สุดคือการใส่คีย์ไว้ในโค้ดแล้วอัปขึ้น git ดังนั้นการนำคีย์ไปไว้ใน git จึงเป็นข้อห้ามที่ไม่มีข้อยกเว้น
| ผู้ใช้ | สิทธิ์ |
|---|---|
| ผู้อำนวยการ·ซีเนียร์ | full (รับผิดชอบการบริหาร cost cap) |
| สมาชิกทั่วไป | cap ตามงาน |
| งานจ้างภายนอก | จำกัดครั้งเดียวต่องาน |
สิทธิ์ถูกแบ่งตามขนาดของความรับผิดชอบ ไม่ใช่ตามความไว้วางใจ ผู้ที่มีสิทธิ์ full ต้องแบกรับความรับผิดชอบในการบริหารเพดานค่าใช้จ่าย (cost cap) ไปพร้อมกัน สำหรับงานจ้างภายนอกให้เปิดสิทธิ์เพียงครั้งเดียวต่องาน และเมื่อเสร็จแล้วก็เรียกคืน
แต่ละเครื่องมือมีค่าตั้งที่แนะนำต่างกัน แต่หัวใจคือการแยกงานเชิงวิเคราะห์ออกจากงานเชิงสร้างสรรค์ งานวิเคราะห์ต้องทำซ้ำให้ได้ผลเดิม ส่วนงานสร้างสรรค์ต้องการความหลากหลาย
ด้านล่างคือตัวอย่างการตั้งค่าพื้นฐานของ Claude Code เมื่อดูทีละบรรทัด คือ ตรึงโมเดลไว้, เปิดการคิดเชิงขยาย (extended thinking), กำหนดเพดานโทเค็น, เปิดการอัปเดตอัตโนมัติ, และผูก hook ที่แทรกหน่วยความจำเข้ามาเมื่อส่งพรอมต์
{
"model": "claude-opus-4-8",
"extended_thinking": true,
"max_tokens": 100000,
"auto_update": true,
"hooks": {
"UserPromptSubmit": ["~/.claude/hooks/inject_memory.py"]
}
}
ค่า model เป็นเพียงตัวอย่างเท่านั้น ชื่อโมเดลเปลี่ยนไปตามแต่ละรุ่น (ตัวอย่างนี้อิงตามช่วงเวลาที่เขียน) จึงอย่าคัดลอกไปตรง ๆ แต่ให้ใช้ /model ตรวจสอบชื่อล่าสุดที่ใช้ได้ในปัจจุบันแล้วใส่ลงไป แม้ชื่อจะเปลี่ยน แต่โครงสร้างเวิร์กโฟลว์ของหนังสือเล่มนี้ก็ยังทำงานได้เหมือนเดิม (ดูภาคผนวก K)
สคริปต์ที่ผูกไว้ใน hooks.UserPromptSubmit ทำหน้าที่แทรกชิ้นส่วนหน่วยความจำที่เกี่ยวข้องเข้ามาโดยอัตโนมัติทุกครั้งที่ส่งพรอมต์ กลไกการแทรกหน่วยความจำนี้อธิบายโดยละเอียดในส่วนที่ 24 ของเนื้อหาหลัก
C.2.1.1 สคีมาสิทธิ์เครื่องมือ (allow / deny)
ภายใน settings.json เดียวกัน สิทธิ์จะถูกแยกออกมาด้วยบล็อก permissions เครื่องมือที่ AI สามารถรันได้เองโดยอัตโนมัติโดยไม่ต้องให้คนอนุมัติจะเขียนไว้ใน allow ส่วนเครื่องมือที่หากเกิดเหตุขึ้นแม้เพียงครั้งเดียวก็ร้ายแรงจนต้องห้ามไม่ให้รันอัตโนมัติจะเขียนไว้ใน deny รูปแบบการเขียนคือ เครื่องมือ(แพตเทิร์นคำสั่ง) และ :* หมายถึง "ทุกการเรียกที่ขึ้นต้นด้วยคำสั่งนั้น"
{
"permissions": {
"allow": [
"Bash(ls:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Read(*)",
"Grep(*)"
],
"deny": [
"Bash(rm -rf:*)",
"Bash(git push --force:*)"
]
}
}
คำสั่งที่ไม่มีอะไรต้องย้อนกลับ เช่น การอ่านและค้นหา (Read·Grep) และการเรียกดูสถานะ (git status·git diff) ให้อนุญาตอัตโนมัติด้วย allow เพื่อลดความเหนื่อยล้าจากป๊อปอัปขออนุมัติ ในทางกลับกัน คำสั่งที่ความผิดพลาดเพียงครั้งเดียวกู้คืนไม่ได้ เช่น rm -rf, git push --force ให้ใส่ไว้ใน deny ไม่ว่าจะขยายขอบเขตการอนุญาตอัตโนมัติกว้างแค่ไหนก็ตาม
หลักการดำเนินงานมีอยู่สี่ข้อ
| หลักการ | เนื้อหา |
|---|---|
| เริ่มต้นด้วย Whitelist | เริ่มการอนุญาตอัตโนมัติให้น้อยที่สุด แล้วเพิ่มเข้า allow เฉพาะเมื่อจำเป็น |
| ระบุการบล็อกคำสั่งอันตรายชัดเจน | rm -rf·git push --force ต้องอยู่ใน deny โดยไม่มีข้อยกเว้น |
| จัดระเบียบเป็นประจำ | ทบทวน allow ทุกไตรมาสเพื่อตัดสิทธิ์ที่ไม่ได้ใช้ออก |
| แยกตามโดเมน | แบ่งสิทธิ์ระดับโกลบอลกับสิทธิ์ระดับโปรเจกต์ออกจากกัน เพื่อให้ PC ที่บ้านและ PC ที่บริษัทมีนโยบายต่างกัน |
ลิสต์ allow ไม่ใช่การตั้งค่าที่หยุดนิ่ง แต่เป็นร่องรอยการสะสมของงาน เมื่องานที่ทำซ้ำมากขึ้น ลิสต์ก็ยาวขึ้นตามนั้น จึงควรเตรียมรอบการจัดระเบียบรายไตรมาสไว้ควบคู่กันด้วย เบื้องหลังของการบริหารสิทธิ์นี้อธิบายโดยละเอียดในส่วนที่ 1 บทที่ 3 ของเนื้อหาหลัก
| การตั้งค่า | ค่าที่แนะนำ |
|---|---|
| LLM temperature (วิเคราะห์) | 0 |
| LLM temperature (สร้างสรรค์) | 0.7 |
| Cache TTL | 1 ชั่วโมง |
| Cost cap (รายวัน) | กำหนดแยกตามเครื่องมือ |
| รอบ Backup | รายวัน |
การเรียกใช้เพื่อการวิเคราะห์ให้ตั้ง temperature เป็น 0 เพื่อให้อินพุตเดียวกันได้เอาต์พุตเดียวกัน งานที่ผลลัพธ์ต้องไม่แกว่ง เช่น การตรวจสอบ·lint·การจำแนกประเภท จัดอยู่ในกลุ่มนี้ ในทางกลับกัน การแตกไอเดียหรือการสร้างร่างแรกให้ความหลากหลายราว 0.7 สำหรับเพดานค่าใช้จ่าย อย่าตั้งมาตรฐานเดียว แต่ให้กำหนดแยกตามเครื่องมือ เพราะแต่ละเครื่องมือมีความถี่ในการเรียกและการใช้โทเค็นต่างกัน
| สาขา (branch) | สิทธิ์ |
|---|---|
| main | เฉพาะผู้อำนวยการ·ซีเนียร์ push ได้ |
| feature/* | สมาชิกทุกคน |
| protected branches | บังคับให้มีการรีวิวโค้ด |
สาขา main ห้าม push ตรง และการเปลี่ยนแปลงทุกอย่างต้องเข้ามาผ่านการรีวิวโค้ดบนสาขา feature การ force-push จะเขียนทับประวัติการทำงานร่วมกัน จึงห้ามไว้ และจะเปิดข้อยกเว้นเฉพาะเมื่อจำเป็นต้องกู้คืนเหตุการณ์ที่หลีกเลี่ยงไม่ได้ โดยต้องมีฉันทามติร่วมระหว่างผู้อำนวยการและหัวหน้าทีมโค้ด
โฟลเดอร์เอกสารแบ่งสิทธิ์ตามโครงสร้าง Layer (L0\~L4) ยิ่งขึ้นไปด้านบนขอบเขตผลกระทบยิ่งกว้าง จึงจำกัดสิทธิ์เขียนให้แคบลง และยิ่งลงด้านล่างงานยิ่งกระจาย จึงขยายสิทธิ์เขียนให้กว้างขึ้น
| โฟลเดอร์ | สิทธิ์ |
|---|---|
| docs/L0_vision/ | ผู้อำนวยการ write, ทุกคน read |
| docs/L1_systems/ | ผู้อำนวยการสายงาน write, ทุกคน read |
| docs/L2_content/ | ผู้รับผิดชอบ read·write |
| docs/L4_meta/ | ทุกคน write |
| team_memory/แยกตามผู้ใช้/ | เฉพาะเจ้าของ read·write |
วิสัยทัศน์ (L0) มีเพียงผู้อำนวยการที่เขียน และทุกคนอ่าน ระบบ (L1) ผู้อำนวยการสายงานเป็นผู้เขียน เนื้อหา (L2) ผู้รับผิดชอบเป็นผู้เขียน ส่วนเมตา·ชั่วคราว (L4) ใครก็เขียนได้ หน่วยความจำส่วนตัวเข้าถึงได้เฉพาะเจ้าของ ตัวโครงสร้าง Layer นี้อธิบายไว้ในส่วนที่ 6 ของเนื้อหาหลัก
| ข้อมูล | การสำรอง |
|---|---|
| git repo | git เอง + สำรองที่รีโมต |
| ชีต (Excel) | git + สำรองรายวัน |
| ข้อมูลผู้ใช้ | สำรอง DB (มาตรฐานเซิร์ฟเวอร์) |
| บันทึกการประชุม·การตัดสินใจ | git |
| หน่วยความจำ | ซิงค์อัตโนมัติรายวัน |
แต่ละประเภทของข้อมูลมีเส้นทางการสำรองต่างกัน แต่หลักการมีเพียงหนึ่งเดียว ยิ่งเป็นข้อมูลที่หากสูญหายแล้วกู้คืนยาก ก็ยิ่งสำรองเป็นสองชั้น สินทรัพย์ที่เป็นข้อความ (บันทึกการประชุม·การตัดสินใจ·โค้ด) มี git เป็นการสำรองในตัว ส่วนข้อมูลไบนารีหรือข้อมูลเซิร์ฟเวอร์ให้สำรองแยกต่างหาก เป้าหมายเวลากู้คืน (RTO) ให้ตั้งไว้ภายใน 4 ชั่วโมง แต่ค่านี้ปรับให้เข้ากับเวลาดาวน์ไทม์ที่ทีมยอมรับได้
| ขอบเขต | กฎ |
|---|---|
| ข้อมูลละเอียดอ่อนใน LLM ภายนอก | placeholder หรือโฮสต์เอง |
| ข้อมูลการชำระเงิน·ข้อมูลส่วนบุคคล | ห้ามส่งให้ LLM โดยเด็ดขาด |
| การอ้างอิงข้อมูลภายนอก | แหล่งที่มา + การตรวจสอบทางกฎหมาย |
| การคุ้มครองข้อมูลผู้ใช้ | ทำให้ไม่ระบุตัวตน + ปฏิบัติตาม GDPR |
กฎที่รักษาง่ายที่สุดแต่ถูกละเมิดบ่อยที่สุดคือ "ห้ามส่งข้อมูลละเอียดอ่อนไปยัง LLM ภายนอก" เพราะเมื่องานเร่ง ความอยากที่จะวางข้อมูลจริงลงไปตรง ๆ ก็มีสูง ข้อมูลการชำระเงิน·ข้อมูลส่วนบุคคลให้ตั้งเป็นห้ามส่งโดยไม่มีข้อยกเว้น และหากจำเป็นต้องวิเคราะห์ก็ให้แทนที่ด้วย placeholder หรือใช้โมเดลที่โฮสต์เอง
| เหตุการณ์ | การรับมือ |
|---|---|
| ส่งข้อมูลผิดออกไปเพราะ LLM หลอน | เรียกคืนทันที + รายงาน |
| เกินเพดานค่าใช้จ่าย (cost cap) | บล็อกอัตโนมัติ + ทบทวน |
| เหตุการณ์ลิขสิทธิ์ | หยุดใช้ภายใน 1 ชั่วโมง + ฝ่ายกฎหมาย |
| เหตุการณ์ความปลอดภัย (คีย์รั่ว) | เปลี่ยนคีย์ทันที + ตรวจสอบประวัติการใช้ |
| ข้อมูลสูญหาย | กู้คืนจากสำรอง + วิเคราะห์เหตุการณ์ |
หลายครั้งการหยุดเหตุการณ์ให้เร็วสำคัญกว่าการป้องกัน การรับมือในตารางทั้งหมดยึดลำดับ "หยุดก่อน แล้วค่อยวิเคราะห์ทีหลัง" หากคีย์รั่ว ก่อนจะสืบหาสาเหตุให้เปลี่ยนคีย์ก่อนแล้วจึงดูประวัติการใช้ หากค่าใช้จ่ายเกินเพดานก็บล็อกอัตโนมัติแล้วจึงทบทวน ขั้นตอนการรับมือนี้ต้องเขียนเป็นเอกสารให้ชัดเจน และฝึกซ้อมเป็นประจำเพื่อให้ทำงานได้โดยไม่ลังเลเมื่อเกิดเหตุจริง
เวอร์ชันทั่วไปของมาตรฐานการตั้งชื่อและ frontmatter ของเอกสาร R&D ในโปรเจกต์ A ของบริษัท (
_NAMING_FRONTMATTER_STANDARD)
<category>_<topic>_<subtopic>.md
ตัวอย่าง:
combat_global_cooldown_constant.md
narrative_voice_profile_K_007.md
ui_button_primary_style.md
snake_case พร้อม prefix ของหมวดหมู่
D<YEAR>_Q<QUARTER>_<NUMBER>.md
ตัวอย่าง:
D2026_Q2_017.md
ปี · ไตรมาส · หมายเลข
<category>_<YYYY-MM-DD>[_<seq>].md
ตัวอย่าง:
95_BattleTF_2026-05-18.md
art_review_2026-05-18_1.md
art_review_2026-05-18_2.md
spec_<topic>.md
ตัวอย่าง:
spec_combat_global_cooldown.md
spec_guild_attendance.md
report_<period>_<type>.md
ตัวอย่าง:
report_W21_alpha_gap.md
report_Q2_user_voice.md
---
name: combat_global_cooldown_constant
description: นิยามค่ามาตรฐานของ global cooldown ในระบบการต่อสู้
type: atom
category: combat
status: active
priority: P0
related_atoms:
- combat_skill_cooldown_rule
- combat_healing_skill_cooldown_exception
created: 2026-05-18
last_modified: 2026-05-18
related:
derives_from: [combat_design_principle]
affects: [combat_skill_cooldown_rule, ui_skill_cooldown_indicator]
---
---
decision_id: D2026_Q2_017
title: รวม global cooldown ของการต่อสู้ให้เป็น 0.5 วินาที
type: system_change
status: active
created: 2026-05-18
created_by: สมาชิกทีม A
approved_by: อี มินซู
scope:
- combat_system
affected_atoms: [...]
implementation:
target_build: 2026-05-18
verification:
layer_1: passed
layer_2: passed
layer_3: pending
---
---
type: meeting_note
category: battle
date: 2026-05-18
attendees: [สมาชิกทีม A, สมาชิกทีม B, อี มินซู]
related_atoms: [...]
---
---
title: เอกสารข้อกำหนดฟีเจอร์การเช็กอินกิลด์
type: spec
priority: P1
target_milestone: MS2
---
| ชนิดเอกสาร | บังคับ |
|---|---|
| atom | name, description, type, category, status |
| การ์ดการตัดสินใจ | decision_id, title, type, status, created, scope |
| บันทึกการประชุม | type, category, date, attendees |
| เอกสารข้อกำหนด | title, type, priority |
| ชนิดเอกสาร | ทางเลือก |
|---|---|
| atom | related, last_modified, priority |
| การ์ดการตัดสินใจ | rationale, related_decisions, verification |
| บันทึกการประชุม | related_atoms, sub_topic |
| เอกสารข้อกำหนด | target_milestone, related_atoms |
# frontmatter_lint.py
for file in glob("**/*.md"):
fm = parse_frontmatter(file)
if not fm:
warn(f"{file}: ไม่มี frontmatter")
doc_type = infer_type_from_filename(file)
required = REQUIRED_FIELDS[doc_type]
for field in required:
if field not in fm:
warn(f"{file}: ขาดฟิลด์บังคับ {field}")
รันอัตโนมัติตอนบิลด์ การละเมิดจะแจ้งเตือน (alert)
| ขอบเขต | การป้องกัน |
|---|---|
| atom name | unique ระดับ global |
| decision ID | unique ภายในไตรมาส |
| meeting ID | วันที่ + seq |
| ชื่อไฟล์ | unique ภายในโฟลเดอร์ |
เมื่อชื่อชนกันจะถูกบล็อกอัตโนมัติ
1. สร้าง atom ด้วยชื่อใหม่
2. อัปเดต wikilink ทั้งหมดของ atom เดิมไปยังชื่อใหม่ (อัตโนมัติ)
3. ทำเครื่องหมาย atom เดิมเป็น deprecated + redirect
4. ย้ายไปยัง _archive หลังผ่านไป 1 เดือน
การเปลี่ยนชื่อแบบรีบร้อนเสี่ยงทำให้ข้อมูลเสียหาย
1. เสนอเหตุผลของการเปลี่ยนแปลง (ตามขั้นตอน decision)
2. สคริปต์ migration สำหรับเอกสารเดิมทั้งหมด
3. อัปเดต lint ตอนบิลด์
4. แจ้งทีม
มาตรฐานนี้เป็นสภาพแวดล้อมของผู้เขียน ผู้อ่านต้องปรับให้เข้ากับสภาพแวดล้อมของตนเอง สาระสำคัญคือ:
| สาระสำคัญ | เหตุผล |
|---|---|
| ความสม่ำเสมอของการตั้งชื่อ | การค้นหา · การทำงานอัตโนมัติ |
| มาตรฐาน Frontmatter | เป็นมิตรกับเครื่องมือ |
| การแยกบังคับ · ทางเลือก | ลดภาระในการเขียน |
| Lint อัตโนมัติ | บังคับใช้มาตรฐาน |
| ขั้นตอนการเปลี่ยนแปลง | ปกป้องข้อมูล |
MCP (Model Context Protocol) คือช่องทางที่ทำให้ LLM เชื่อมต่อกับเครื่องมือและข้อมูลภายนอกด้วยวิธีมาตรฐาน ในเนื้อหาส่วนที่ 20 ผู้เขียนได้กล่าวถึง MCP สำหรับการบริหารโปรเจกต์ไปแล้ว แต่เซิร์ฟเวอร์ MCP ที่ดึงมาใช้ในเวิร์กโฟลว์การออกแบบเกมได้นั้นมีมากกว่านั้นมาก ภาคผนวกนี้คือแคตตาล็อกที่รวบรวมตัวเลือกเหล่านั้นให้เห็นในภาพรวม พร้อมจัดลำดับความสำคัญว่าควรนำมาใช้ตามลำดับใด
จุดประสงค์ของแคตตาล็อกไม่ใช่ "ให้ติดตั้งทั้งหมดนี้" แต่คือ "เมื่อจำเป็นต้องใช้ จะรู้ว่าควรเลือกจากที่ไหน" หากเชื่อมต่อ MCP หลายตัวพร้อมกัน เมื่อเกิดปัญหาจะแยกแยะไม่ออกว่าตัวใดเป็นต้นเหตุ ขอให้ทำตามรอบการนำเข้าใช้งานใน E.4 แล้วค่อย ๆ เพิ่มทีละตัว
วิธีใช้เป็นดังนี้ ในตอนแรกให้ดูเฉพาะรายการ P0 ใน E.2.1 เมื่อวางพื้นฐานได้แล้วจึงข้ามไปยัง E.2.2 (P1) และเมื่อทีมมีความจำเป็นเฉพาะทางเกิดขึ้น จึงค่อยพิจารณา E.2.3 (P2) หรือ E.3 (การพัฒนาเอง) หากกังวลเรื่องค่าใช้จ่ายให้ดู E.5 ก่อน และหากต้องการเตรียมรับมือเหตุขัดข้องให้ดู E.6 ก่อน
เซิร์ฟเวอร์ MCP แบ่งออกได้เป็นสี่กลุ่มใหญ่ตามเป้าหมายที่เชื่อมต่อ เครื่องมือที่นักออกแบบเกมใช้ไปมาอยู่ทุกวันส่วนใหญ่ก็อยู่ในนี้
| พื้นที่ | เซิร์ฟเวอร์ MCP |
|---|---|
| การบริหารโปรเจกต์ | ClickUp, JIRA, Linear |
| เอกสาร | Confluence, Notion, Google Drive |
| การทำงานร่วมกัน | แชตภายในทีม (Slack·Discord ฯลฯ) |
| ข้อมูล | Excel, Google Sheets, DB |
การบริหารโปรเจกต์เชื่อมไปยังงานและกำหนดการ เอกสารเชื่อมไปยังเอกสารออกแบบและวิกิ การทำงานร่วมกันเชื่อมไปยังการสื่อสารของทีม และข้อมูลเชื่อมไปยังชีตปรับสมดุลและชีตไอเทม หากตรวจดูก่อนว่าเครื่องมือที่ทีมของตนใช้อยู่แล้วอยู่ในพื้นที่ใด ตัวเลือกที่จะนำมาใช้ก็จะแคบลงเองโดยธรรมชาติ
ลำดับความสำคัญจัดตามเกณฑ์ "ถ้าไม่มีแล้วงานจะติดขัดหรือไม่" P0 คือพื้นฐานของแทบทุกงาน P1 คือสิ่งที่มีแล้วสะดวกขึ้นมาก ส่วน P2 เลือกตามสถานการณ์ของทีม
| เซิร์ฟเวอร์ | การใช้งาน | หมายเหตุ |
|---|---|---|
| Filesystem MCP | เข้าถึงไฟล์ในเครื่อง | พื้นฐาน |
| Git MCP | ติดตามการเปลี่ยนแปลง | จำเป็น |
| MCP แชตภายในทีม | การสื่อสารของทีม | แนะนำ |
| MCP เครื่องมือทำงานร่วมกัน (ClickUp·JIRA ฯลฯ) | งาน | เครื่องมือของบริษัท |
Filesystem และ Git เป็นพื้นฐานให้ LLM อ่านข้อมูลและตามรอยประวัติการเปลี่ยนแปลง จึงเชื่อมต่อก่อนเป็นอันดับแรก MCP แชตภายในทีมดึงบริบทของทีมเข้ามา ส่วนเครื่องมือจัดการงานให้เชื่อมต่อตัวที่บริษัทใช้อยู่แล้วตามนั้น (ไม่ว่าจะเป็น ClickUp หรือ JIRA)
| เซิร์ฟเวอร์ | การใช้งาน |
|---|---|
| MCP วิกิ (Confluence·Notion ฯลฯ) | วิกิ |
| Google Drive MCP | เอกสารแชร์ภายนอก |
| Excel MCP | เรียกดูชีตโดยตรง |
| Mermaid MCP | เรนเดอร์ไดอะแกรม |
เมื่อ P0 เสถียรแล้วจึงขยายไปทางเอกสารและข้อมูล โดยเฉพาะ Excel MCP ที่ทำให้ LLM เรียกดูชีตปรับสมดุลได้โดยตรง จึงมีประโยชน์สูงในการออกแบบเกม ส่วน Mermaid MCP เรนเดอร์แผนผังการออกแบบได้ทันที ณ จุดนั้น จึงไม่ทำให้กระแสการจัดทำเอกสารขาดตอน
| เซิร์ฟเวอร์ | การใช้งาน |
|---|---|
| Discord MCP | ชุมชนผู้ใช้ |
| GitHub MCP | การทำงานร่วมกับภายนอก |
| Linear MCP | งานทางเลือก |
| Notion MCP | วิกิทางเลือก |
P2 เป็นทางเลือกหรือเฉพาะสถานการณ์บางอย่าง หากดำเนินชุมชนผู้ใช้ก็เชื่อมต่อ Discord หากมีการทำงานร่วมกับภายนอกบ่อยก็เชื่อมต่อ GitHub ส่วน Linear และ Notion เป็นทางเลือกแทนเครื่องมือที่นำเข้าใช้ไปแล้ว จึงไม่จำเป็นต้องติดตั้งซ้ำซ้อน
ช่องว่างที่ MCP เชิงพาณิชย์เติมไม่ได้ก็สร้างขึ้นเอง ด้านล่างคือ MCP ที่ผู้เขียนพัฒนาขึ้นเองให้เข้ากับเวิร์กโฟลว์การออกแบบเกม ทั้งหมดมีไว้เพื่อเรียกดูระบบที่กล่าวถึงในเนื้อหา (atom·การ์ดการตัดสินใจ·บันทึกการประชุม) จาก LLM ได้โดยตรง
| เซิร์ฟเวอร์ | การใช้งาน |
|---|---|
| Atom MCP | ค้นหา·เรียกดู atom |
| Decision Card MCP | เรียกดู·สร้างการ์ดการตัดสินใจ |
| KPI Dashboard MCP | ข้อมูลแดชบอร์ด |
| Meeting Notes MCP | ค้นหาบันทึกการประชุม |
สี่ตัวนี้จัดการกับสินทรัพย์ภายในบริษัทที่เครื่องมือเชิงพาณิชย์ไม่มี (atom ความรู้, ประวัติการตัดสินใจ, บันทึกการประชุม) เนื่องจากการพัฒนาเองมีภาระสูง จึงควรเลื่อนไว้เป็นขั้นตอนสุดท้ายของรอบใน E.4 แล้วค่อยลงมือเมื่อชัดเจนแล้วว่ามีสิ่งที่ MCP เชิงพาณิชย์เติมเต็มไม่ได้
หากเชื่อมต่อ MCP พร้อมกันทีเดียว จะยากที่จะระบุต้นเหตุของปัญหา รอบด้านล่างคือการคลี่หลักการ "ทีละตัว แล้วค่อยไปตัวต่อไปหลังจากเสถียรแล้ว" ออกมาตามแกนเวลา
flowchart LR
A["1 สัปดาห์
ทดลอง Filesystem 1 ตัว"] --> B["1 เดือน
เพิ่ม Git + แชตภายในทีม"]
B --> C["3 เดือน
ใช้งาน 5~7 ตัวอย่างเสถียร"]
C --> D["6 เดือน
พิจารณาพัฒนา MCP ของตนเอง"]
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
class A,B,C code;
class D human;
กฎสำคัญมีเพียงหนึ่งเดียว คืออย่านำเข้าใช้ 5 ตัวพร้อมกันในคราวเดียว ทุกครั้งที่เชื่อมต่อ MCP ตัวใหม่ ให้เฝ้าดูสักสองสามวันว่าตัวนั้นทำงานได้อย่างเสถียรหรือไม่ แล้วจึงค่อยไปต่อยังตัวถัดไป
| เซิร์ฟเวอร์ | ค่าใช้จ่าย |
|---|---|
| MCP ภายนอก (โอเพนซอร์ส) | เฉพาะค่าโครงสร้างพื้นฐาน |
| โฮสต์เอง | โครงสร้างพื้นฐาน + การดำเนินงาน |
| MCP เชิงพาณิชย์ | ค่าสมาชิกรายเดือน |
โครงสร้างค่าใช้จ่ายแบ่งเป็นสาม MCP โอเพนซอร์สมีเพียงค่าโครงสร้างพื้นฐานที่ใช้รัน การโฮสต์เองจะมีค่าบุคลากรดำเนินงานเพิ่มเข้าไปด้วย และ MCP เชิงพาณิชย์มีค่าสมาชิก เมื่อดำเนินการ 8\~10 ตัว ค่าใช้จ่ายรายเดือนประมาณการอยู่ที่ราว $50\~200 แต่ตัวเลขนี้แปรผันมากตามการกำหนดค่า จึงใช้เป็นเพียงทิศทางอ้างอิงเท่านั้น
| เหตุขัดข้อง | การรับมือ |
|---|---|
| เซิร์ฟเวอร์ MCP ล่ม | เซิร์ฟเวอร์หลักให้ดำเนินการแบบ fallback |
| เหตุด้านสิทธิ์ (แก้ไขข้อมูลผิดพลาด) | ให้ read-only เป็นหลัก |
| ข้อมูลรั่วไหล | ข้อมูลอ่อนไหวให้โฮสต์เอง |
| ค่าใช้จ่ายพุ่งสูง | cap + การมอนิเตอร์ |
MCP เชื่อมต่อเครื่องมือภายนอกเข้ากับ LLM โดยตรง การเขียนผิดเพียงครั้งเดียวจึงอาจทำลายข้อมูลจริงได้ ดังนั้นโดยพื้นฐานให้ตั้งเป็น read-only แล้วเปิดสิทธิ์เขียนเฉพาะเซิร์ฟเวอร์ที่จำเป็นจริง ๆ เท่านั้น เซิร์ฟเวอร์หลักให้เตรียม fallback ไว้รับมือเหตุขัดข้อง และ MCP ที่จัดการข้อมูลอ่อนไหวให้รันแบบโฮสต์เองแทนการใช้ภายนอก ส่วนค่าใช้จ่ายให้กั้นไว้ด้วยขีดจำกัด (cap) ควบคู่กับการมอนิเตอร์
ในขณะที่หัวข้อก่อนหน้าได้กล่าวถึง "จะเชื่อมต่ออะไร ตามลำดับใด ด้วยค่าใช้จ่ายเท่าไร" ตารางนี้รวบรวมรายการที่ต้องให้ตนเองผ่านก่อนจะเชื่อมต่อ MCP หนึ่งตัวจริง ๆ แทนที่จะอ่านแคตตาล็อกใหม่ตั้งแต่ต้น ทุกครั้งที่เพิ่ม MCP ใหม่ เพียงตรวจสอบห้าบรรทัดนี้ซ้ำก็พอ ทั้งห้าข้อแต่ละข้อคือการบีบกฎสำคัญของหัวข้อก่อนหน้าให้เหลือบรรทัดเดียว
| รายการตรวจสอบ | เกณฑ์ผ่าน | หัวข้ออ้างอิง |
|---|---|---|
| อยู่ในพื้นที่ใด | ชัดเจนว่าอยู่ในการบริหารโปรเจกต์·เอกสาร·การทำงานร่วมกัน·ข้อมูล ด้านใด | E.1 |
| เป็นลำดับความสำคัญที่จำเป็นตอนนี้หรือไม่ | รักษาลำดับ P1 ต่อเมื่อ P0 เสถียรแล้ว แล้วจึงต่อด้วย P2 | E.2 |
| เชื่อมต่อทีละตัวหรือไม่ | ไม่นำเข้าใช้หลายตัวพร้อมกันในคราวเดียว | E.4 |
| สิทธิ์น้อยที่สุดหรือไม่ | พื้นฐานเป็น read-only สิทธิ์เขียนเฉพาะเซิร์ฟเวอร์ที่จำเป็นจริง ๆ | E.6 |
| มีขีดจำกัดค่าใช้จ่ายหรือไม่ | กั้นขีดจำกัด (cap) ควบคู่กับการมอนิเตอร์ | E.5 |
ในห้าข้อนี้ ช่องที่ถูกข้ามบ่อยที่สุดคือ "เชื่อมต่อทีละตัวหรือไม่" เพราะหากนำ MCP หลายตัวขึ้นพร้อมกัน เมื่อเกิดปัญหาจะแยกแยะไม่ออกว่าเป็นเพราะเซิร์ฟเวอร์ตัวใด ให้เชื่อมต่อ MCP นั้นเฉพาะเมื่อผ่านครบทั้งห้าบรรทัดเท่านั้น และหากติดแม้เพียงบรรทัดเดียว ให้เลื่อนเซิร์ฟเวอร์นั้นไปยังรอบถัดไป
ภาคผนวกนี้จัดทำดัชนีกรณีศึกษาที่ปรากฏในหนังสือเล่มนี้ โดยแยกตามสภาพแวดล้อมแบบบริษัทกับสภาพแวดล้อมแบบพีซีส่วนตัว เพื่อให้ผู้อ่านค้นหากรณีที่ใกล้เคียงกับสภาพแวดล้อมของตนเองได้อย่างรวดเร็ว
| กรณี | ตำแหน่งที่ปรากฏ |
|---|---|
| การดำเนินงาน CombatBalance·CombatFormula | 8.1 |
| Economy Machinations Pilot | 8.2 |
| Damage Simulator (2008\~) | 8.3 |
| Procedural Level Design Master | 7.1 |
| เอดิเตอร์ BehaviorTree | 7.2 |
| ไลบรารีแพตเทิร์นดันเจี้ยน·สนาม | 7.3 |
| HUD Layout v3 | 9.1 |
| การตัดสินใจ Skill UI 6 คอลัมน์ | 9.2 |
| NarrativeDocs 5 ชั้น | 5.1 |
| voice_profile + voice_lint | 5.2·5.4 |
| proj_city_hunting_generator | 6.2 |
| NPC Persona/Squad | 6.3 |
| กรณี | ตำแหน่งที่ปรากฏ |
|---|---|
| การดำเนินงาน 95_BattleTF | 16.1 |
| การทำงานร่วมกัน 97_DevGuide | 16.2 |
| ระบบบันทึกการประชุม 17.x | ทั้งส่วนที่ 17 |
| Alpha Gap Report | 10.3 |
| decision_validation 3-layer | 10.2 |
| การดำเนินงาน 304 atom | 20.1 |
| หน่วยความจำสมาชิกทีม | 20.2 |
| เว็บพอร์ทัล | 20.3 |
| กรณี | ตำแหน่งที่ปรากฏ |
|---|---|
| วิสัยทัศน์·โรดแมปของทีมขนาดกลาง (10\~50 คน) | 19.1 |
| การมอบหมายงานของ Design Director | 19.2 |
| การจัดการความขัดแย้ง·วัฒนธรรมทีม | 19.3 |
| การดำเนินการประชุม (มุมมองผู้นำ) | 19.4 |
| การสื่อสารกับระดับบน (PD/CEO) | 19.5 |
| กลยุทธ์การนำ AI มาใช้ | 19.6 |
| ธรรมาภิบาล (พรอมต์·อาการหลอน·ต้นทุน·กฎหมาย·จริยธรรม) | ทั้งส่วนที่ 22 |
กรณีที่ผู้เขียนประสบมาโดยตรงในสภาพแวดล้อมพีซีส่วนตัว (ที่บ้าน)
| กรณี | ตำแหน่งที่ปรากฏ |
|---|---|
| การยืมเครื่องมือ 6 ตัวมาใช้ เช่น excel-reader | ภาคผนวก B |
| ระบบฉีด JIT atom | (โครงสร้างพื้นฐานพีซีส่วนตัว) |
| คำสั่งสแลชบนพีซีส่วนตัว (เช่น book-capture) | (โครงสร้างพื้นฐานพีซีส่วนตัว) |
กระบวนการเขียนหนังสือเล่มนี้เองก็เป็นกรณีศึกษาการใช้ AI
| ด้าน | การประยุกต์ |
|---|---|
| การผลิตเนื้อหาบทเป็นจำนวนมาก | LLM (Claude) |
| การปกป้อง IP (บริษัท → ทำให้ไม่ระบุตัวตน) | grep watchlist + กฎ |
| การติดตามแหล่งที่มา | ระบุชัดเจนเมื่ออ้างอิงสภาพแวดล้อมของบริษัท |
| วงจรผลิตจำนวนมาก → ตรวจทาน → ปรับแต่ง | เข้าสู่โหมดตรวจทานหลังผลิตจำนวนมากในเดือนพฤษภาคม |
กรณีศึกษาสภาพแวดล้อมแบบบริษัท (ทีมขนาดกลาง 10\~50 คน, MMORPG, Live Ops) สามารถนำไปประยุกต์ใช้กับบริษัทที่มีขนาดและโดเมนใกล้เคียงกันได้
| สภาพแวดล้อมของผู้อ่าน | กรณีที่เหมาะสม |
|---|---|
| บริษัทพัฒนา MMORPG บนมือถือ | เกือบทุกกรณี |
| MMORPG บนพีซี | กรณีบนมือถือในส่วนที่ 14 ต้องปรับ |
| เกมอินดี้ | กรณีขนาดกลาง (10\~50 คน) ขึ้นไปให้ปรับลดขนาดมาใช้ |
| เกมที่มี Live Ops | ส่วนที่ 15 + กรณีด้านการดำเนินงาน |
สภาพแวดล้อมส่วนตัว (1\~2 คน หรืองานอดิเรก) ให้นำกรณีของบริษัทมาทำให้เรียบง่ายขึ้นแล้วยืมมาใช้
| ด้าน | การทำให้เรียบง่าย |
|---|---|
| ระบบการประชุม | คนเดียวไม่จำเป็น ใช้บันทึกของตัวเองแทน |
| การดำเนินการ TF | คนเดียวไม่จำเป็น |
| decision card | เฉพาะการตัดสินใจครั้งใหญ่ |
| atom·wikilink | ใช้อย่างเต็มที่ (มีคุณค่าแม้ทำคนเดียว) |
กรณีของบริษัททั้งหมดในหนังสือเล่มนี้ได้รับการทำให้ไม่ระบุตัวตน
| ต้นฉบับ | ทำให้ไม่ระบุตัวตน |
|---|---|
| ชื่อบริษัท | บริษัทพัฒนา MMORPG A |
| โปรเจกต์ | โปรเจกต์ A |
| ชื่อจริงของสมาชิกทีม | สมาชิกทีม A·B·C |
| ชื่อเฉพาะภายในเกม | ดัดแปลง (อาณาจักร X, ตัวละคร K_001 เป็นต้น) |
| ตัวเลข | ดัดแปลง (แต่อัตราส่วนเป็นค่าจริง) |
| ชื่อเครื่องมือของบริษัท | proj_* (เช่น proj_city_hunting_generator) |
นี่คือดัชนีย้อนกลับสำหรับผู้อ่านที่ทำงานนอกวงการเกม (นักออกแบบ·PM·พนักงานออฟฟิศทั่วไป) กล่อง "การประยุกต์นอกเกม" ที่ท้ายแต่ละบทในเนื้อหาหลักคือสะพานที่ย้ายเวิร์กโฟลว์ของบทนั้นไปอ่านในบริบทของงานที่ไม่เกี่ยวกับเกม หากเนื้อหาหลักในโดเมนเกมเป็นภาระเกินไป คุณสามารถเริ่มจากการเปิดกล่องด้านล่างเพื่อเข้าสู่กรณีของงานตัวเองได้เลย ดัชนีนี้คือสมอของ "เส้นทางสำหรับงานทั่วไป" (ส่วนที่ 1·2 → 17 → 16 → 18 → ส่วนที่ 21·22) และคอร์สลัดสุดสั้น 90 นาที (17.1 → 16.2 → 22.1 → 21.1)
| บท | งานที่ "การประยุกต์นอกเกม" ย้ายมาให้ |
|---|---|
| 16.1 | แยกงานที่หลั่งไหลเข้ามาไปยังพื้นที่ทำงานชั่วคราว แล้วดูดซับเฉพาะผลลัพธ์เข้าเป็นฉบับหลัก |
| 16.2 | จัดประเภทคำขอหนึ่งบรรทัดออกเป็นสามแทร็ก คือ ข้อตกลง·ข้อบกพร่อง·กำหนดการ |
| 16.3 | จัดเฟรมผลงานให้เข้ากับสื่อที่เหมาะกับสายงานอื่นหรือผู้มีส่วนได้ส่วนเสีย |
| 17.1 | ทำให้บันทึกการประชุมไหลออกมาเป็น 4 ฟิลด์ของการตัดสินใจ (อะไร·ใคร·ทำไม·ถัดไป) |
| 17.2 | ไปป์ไลน์สกัดการตัดสินใจ·แอ็กชันจากบันทึกการประชุม |
| 17.3 | การจัดประเภท·การซิงค์ของการตัดสินใจในที่ประชุม |
| 17.4 | การสรุปการประชุม·การติดตามงานต่อเนื่องแบบอัตโนมัติ |
| 18.1 | ใส่ที่อยู่ถาวร·ผู้รับผิดชอบ·เหตุผลให้กับการตัดสินใจ และค้นหาการตัดสินใจในอดีตก่อน |
| 18.2 | จัดประเภทผลกระทบว่าการตัดสินใจหนึ่งส่งผลไปได้ไกลแค่ไหน |
| 18.3 | เวิร์กโฟลว์ติดตามก่อน-หลังการเปลี่ยนแปลง |
| 18.4 | ตรวจสอบขอบเขตผลกระทบของการเปลี่ยนแปลงเอกสารด้วยการค้นหา |
| บท | งานที่ "การประยุกต์นอกเกม" ย้ายมาให้ |
|---|---|
| 21.1 | ใช้การทบทวนเป็นจุดเริ่มต้นของการปรับปรุงตัวเอง |
| 21.2 | ยกระดับแพตเทิร์นที่เกิดซ้ำจากการทบทวนให้กลายเป็นกฎ |
| 21.3 | ปิดลูปการปรับปรุง |
| 22.1 | ใส่บริบท·รูปแบบ·การกันอาการหลอน·การตรวจสอบ ลงในใบสั่งงาน (พรอมต์) หนึ่งใบ |
| 22.2 | การป้องกันอาการหลอน·ความปลอดภัยแบบหลายชั้น |
| 22.3 | จัดการต้นทุน AI อย่างซื่อตรง |
| 22.4 | ตรวจสอบลิขสิทธิ์·จริยธรรม |
| บท | งานที่ "การประยุกต์นอกเกม" ย้ายมาให้ |
|---|---|
| 19.1 | การนำเสนอวิสัยทัศน์และการมอบหมายงาน |
| 19.2 | การจัดการความขัดแย้งและภาวะผู้นำในการประชุม |
| 19.3 | กลยุทธ์การนำ AI มาใช้ขององค์กร |
ดัชนีข้างต้นรวบรวมเฉพาะกล่อง "การประยุกต์นอกเกม" ที่มีอยู่จริงในเนื้อหาหลักเท่านั้น (22 รายการ ณ เดือนมิถุนายน 2026) บทที่ไม่มีกล่องนี้คือบทที่พึ่งพาโดเมนเกมสูงจนถ่ายโอนตรง ๆ ได้ยาก แทนที่จะฝืนย้าย ผู้เขียนขอแนะนำให้เริ่มเข้าจากบทข้างต้นใน "เส้นทางสำหรับงานทั่วไป"
ภาคผนวกนี้คือรวมตัวอย่างสคริปต์อัตโนมัติสำหรับการดำเนินงานที่กล่าวถึงในเนื้อหาหลัก โดยรวบรวมไว้ในที่เดียว เนื้อหาหลักได้อธิบายว่าสคริปต์แต่ละตัว "จำเป็นเพราะอะไร" ไปตามกระแสของเรื่อง แต่เมื่อจะลงมือสร้างเครื่องมือคล้ายกันจริง ๆ สิ่งที่ต้องการคือแผนที่ที่ช่วยให้เห็นในพริบตาว่า "มีสคริปต์อะไรบ้างที่ผูกกันอยู่ในบทบาทใด" ภาคผนวกนี้คือแผนที่นั้น
ผู้เขียนได้ใส่ชื่อสคริปต์ คำอธิบายหนึ่งบรรทัด พร้อมระบุว่ากล่าวถึงในหัวข้อไหนของเนื้อหาหลัก สคริปต์หลักที่สามารถทำให้เป็นรูปแบบทั่วไปได้อย่างเรียบร้อย (G.1.1 ตัวตรวจรูปแบบ · G.2.1 ตัวตรวจความสอดคล้อง · G.3.1 แผนผังความสัมพันธ์ · G.7.1 ตัวติดตามค่าใช้จ่าย) และตัวอย่างเทสต์·hook ใน G.8 นั้น ผู้เขียนเขียนขึ้นใหม่เป็นโครงทั่วไปที่ไม่เกี่ยวข้องกับข้อมูลของบริษัท และเป็นโค้ดจริงที่ตรวจสอบแล้วว่ารันได้ตามที่เป็น ค่าตัวอย่างอินพุต เอาต์พุต และโค้ดสิ้นสุด (exit code) ล้วนเป็นค่าที่รันจริงและยืนยันแล้ว ส่วนรายการที่เหลือ ผู้เขียนระบุไว้แค่ชื่อ·บทบาท·หัวข้อเนื้อหาหลักที่เชื่อมโยง ซึ่งเหตุผลนั้นเปิดเผยอย่างตรงไปตรงมาในภาคผนวก G.9 ผู้อ่านสามารถยึดรายการที่เป็นโค้ดจริงเป็นต้นแบบ แล้วสร้างการนำไปใช้ที่เหมาะกับสภาพแวดล้อมของตนเองได้โดยตรง
วิธีใช้เป็นดังนี้ ก่อนอื่นกำหนดลักษณะของงานที่ต้องการทำให้เป็นอัตโนมัติ (เป็นการตรวจสอบ การสร้างรายงาน หรือการซิงค์) แล้วเปิดหัวข้อที่ตรงกัน (G.1\~G.7) เลือกสคริปต์ที่ใกล้เคียงที่สุดจากตรงนั้น แล้วไปที่หมายเลขหัวข้อเนื้อหาหลักในวงเล็บ เพื่อยืนยันบริบทและเจตนาในการออกแบบ สุดท้าย เทียบกับหลักการดำเนินงานใน G.8 เพื่อตรวจดูว่าสคริปต์ของตนรักษาหลักการนั้นไว้หรือไม่
เมื่อจัดกลุ่มสคริปต์ทั้งหมดตามบทบาท จะได้ดังนี้
flowchart TD
G1["G.1 การทำงานอัตโนมัติของบันทึกการประชุม·การตัดสินใจ"] --> META["การดำเนินงานเชิงเมตา
(การสะสมความรู้)"]
G2["G.2 การตรวจสอบ·lint"] --> QA["ด่านคุณภาพ"]
G3["G.3 การติดตามผลกระทบ"] --> QA
G4["G.4 การสร้างรายงานอัตโนมัติ"] --> REPORT["การรายงาน·การมองเห็น"]
G5["G.5 การซิงค์"] --> META
G6["G.6 การผสาน LLM"] --> AI["ผู้ช่วย AI"]
G7["G.7 ค่าใช้จ่าย·การดำเนินงาน"] --> AI
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class G1,G2,G3,G4,G5,G6,G7 code;
class AI ai;
class META,REPORT data;
เป็นชุดสคริปต์ที่ทำให้การตัดสินใจซึ่งเกิดขึ้นในที่ประชุมไม่กระจัดกระจาย แต่สะสมขึ้นเป็นสินทรัพย์ความรู้ ตั้งแต่การตรวจสอบบันทึกการประชุม การสกัด atom ไปจนถึงการเลื่อนขั้นเป็นทางการ เชื่อมต่อกันเป็นเส้นเดียว
สคริปต์ที่ตรวจสอบว่าบันทึกการประชุมมีรูปแบบที่กำหนดไว้ (ส่วนหัวที่จำเป็น·ส่วนที่จำเป็น) ครบหรือไม่ บันทึกการประชุมที่รูปแบบกระจัดกระจายจะทำให้การสกัดอัตโนมัติในภายหลังพังลง จึงสกัดกั้นตั้งแต่ทางเข้า (17.2.2)
ด้านล่างคือโครงทั่วไปที่ไม่เกี่ยวข้องกับข้อมูลของบริษัท ใช้เพียงไลบรารีมาตรฐาน (sys เท่านั้น) และรันได้ตามที่เป็น โดยจะดูว่าคีย์ส่วนหัว (บล็อกที่ห่อด้วย ---) และส่วนหัวของหัวข้อในเนื้อหา (## ...) ของบันทึกการประชุม Markdown มีครบหรือไม่ ถ้ามีรายการที่ขาดหายไปจะออก violation และ exit 1 ถ้าครบทั้งหมดจะ exit 0
#!/usr/bin/env python3
"""meeting_lint.py
ตรวจสอบว่าบันทึกการประชุม Markdown มีรูปแบบที่กำหนดไว้ครบหรือไม่
- ในส่วนหัว (บล็อก ---) มีคีย์ที่จำเป็นครบหรือไม่
- ในเนื้อหามีส่วนหัวของหัวข้อที่จำเป็น (## ...) ครบหรือไม่
ถ้ามีรายการที่ขาดหายไปจะพิมพ์ violation และ exit 1 ถ้าไม่มีจะ exit 0
ใช้เพียงไลบรารีมาตรฐานเท่านั้น
การใช้งาน:
python meeting_lint.py meeting.md
"""
import sys
REQUIRED_FRONTMATTER = ["type", "date", "category", "attendees"]
REQUIRED_SECTIONS = ["## วาระการประชุม", "## การตัดสินใจ", "## รายการที่ต้องดำเนินการ", "## การประชุมครั้งถัดไป"]
def lint(text):
"""รับสตริงเนื้อหาบันทึกการประชุมแล้วคืนรายการของรายการที่ขาดหายไป (violation)"""
violations = []
# ส่วนหัว: ถ้าบรรทัดแรกเป็น --- ให้นับถึง --- ถัดไปเป็นส่วนหัว
lines = text.splitlines()
front = []
if lines and lines[0].strip() == "---":
for line in lines[1:]:
if line.strip() == "---":
break
front.append(line)
front_keys = [ln.split(":", 1)[0].strip() for ln in front if ":" in ln]
for key in REQUIRED_FRONTMATTER:
if key not in front_keys:
violations.append({"kind": "frontmatter", "missing": key})
# หัวข้อ: ตรวจว่าในเนื้อหามีบรรทัดส่วนหัวที่ตรงกันอยู่หรือไม่
body_lines = [ln.strip() for ln in lines]
for section in REQUIRED_SECTIONS:
if section not in body_lines:
violations.append({"kind": "section", "missing": section})
return violations
def main(argv=None):
argv = sys.argv[1:] if argv is None else argv
if len(argv) != 1:
sys.stderr.write("การใช้งาน: python meeting_lint.py meeting.md\n")
return 2
with open(argv[0], encoding="utf-8") as f:
violations = lint(f.read())
for v in violations:
print(f"[VIOLATION] {v['kind']}: {v['missing']}")
if violations:
sys.stderr.write(f"[FAIL] ละเมิดรูปแบบ {len(violations)} รายการ\n")
return 1
sys.stderr.write("[PASS] รูปแบบครบถ้วน\n")
return 0
if __name__ == "__main__":
sys.exit(main())
ค่าคงที่สองตัวคือเกณฑ์การตรวจสอบ ตัวอย่างเช่น เมื่อใส่บันทึกการประชุมที่ส่วนหัวขาด attendees และในเนื้อหาไม่มี ## การประชุมครั้งถัดไป จะถูกจับได้สองรายการดังนี้ และโค้ดสิ้นสุดคือ 1
[VIOLATION] frontmatter: attendees
[VIOLATION] section: ## การประชุมครั้งถัดไป
สคริปต์ที่อ่านส่วน "การตัดสินใจ" ของบันทึกการประชุมแล้วสกัด atom ความรู้ที่เป็นตัวเลือกออกมาโดยอัตโนมัติ ทำหน้าที่แทนงานที่คนเคยต้องคัดลอกเองทีละรายการ (17.2.3)
สคริปต์ที่เลื่อนขั้น atom ที่อยู่ในสถานะรอตรวจ (pending) ไปยังโฟลเดอร์ atom ที่เป็นทางการ โดยวางด่านตรวจสอบโดยมนุษย์ไว้ระหว่างการสกัดอัตโนมัติกับสินทรัพย์ที่เป็นทางการ (17.2.6)
เป็นด่านคุณภาพที่จับโดยอัตโนมัติว่าข้อมูลและเนื้อหาละเมิดกฎหรือไม่ ให้เครื่องคัดกรองข้อผิดพลาดด้านความสอดคล้องที่ตามองข้ามได้ง่ายก่อนเป็นอันดับแรก
สคริปต์ที่ตรวจสอบว่า ID ของแต่ละรายการข้อมูลไม่ซ้ำกัน การชนกันของ ID เป็นเหตุที่จะแตกออกตอนรันไทม์ จึงสกัดกั้นไว้ตั้งแต่ขั้นข้อมูล (10.1.2)
ด้านล่างคือโครงทั่วไปที่ไม่เกี่ยวข้องกับข้อมูลของบริษัท ใช้เพียงไลบรารีมาตรฐาน (csv·json·sys·argparse) และบันทึกไว้แล้วรันได้ทันทีตามที่เป็น อินพุตเป็นรูปแบบเรียบง่ายที่ข้อมูลเกมใด ๆ ก็น่าจะมี นั่นคือ CSV ที่มีคอลัมน์ id
#!/usr/bin/env python3
"""integrity_check_id_uniqueness.py
ตรวจสอบว่าคอลัมน์ id ของข้อมูล CSV ไม่ซ้ำกันหรือไม่
- ถ้ามี id ซ้ำ จะพิมพ์รายการ violation และ exit 1
- ถ้าไม่ซ้ำทั้งหมดจะ exit 0
ใช้เพียงไลบรารีมาตรฐานเท่านั้น
การใช้งาน:
python integrity_check_id_uniqueness.py data.csv
python integrity_check_id_uniqueness.py data.csv --id-column quest_id
"""
import argparse
import csv
import json
import sys
def find_duplicate_ids(rows, id_column):
"""หาค่าที่ซ้ำของ id_column จาก rows (ลิสต์ของดิกชันนารี)
คืนค่า: ลิสต์ violation แต่ละรายการมีรูปแบบ
{"id": ค่า, "row_numbers": [หมายเลขแถวแบบ 1-based, ...]}
นับส่วนหัวเป็นแถวที่ 1 และนับแถวข้อมูลแรกเป็น 2
"""
seen = {} # ค่า id -> ลิสต์หมายเลขแถวที่ปรากฏ
for index, row in enumerate(rows):
row_number = index + 2 # ถัดจากส่วนหัว (แถวที่ 1)
key = row.get(id_column, "")
seen.setdefault(key, []).append(row_number)
violations = []
for key, row_numbers in seen.items():
if len(row_numbers) > 1:
violations.append({"id": key, "row_numbers": row_numbers})
violations.sort(key=lambda v: v["row_numbers"][0])
return violations
def load_rows(csv_path):
with open(csv_path, newline="", encoding="utf-8") as f:
return list(csv.DictReader(f))
def main(argv=None):
parser = argparse.ArgumentParser(description="ตรวจสอบความไม่ซ้ำของ id ใน CSV")
parser.add_argument("csv_path", help="เส้นทางไฟล์ CSV ที่จะตรวจสอบ")
parser.add_argument("--id-column", default="id", help="ชื่อคอลัมน์ที่ใช้เป็น id (ค่าเริ่มต้น: id)")
args = parser.parse_args(argv)
rows = load_rows(args.csv_path)
violations = find_duplicate_ids(rows, args.id_column)
# มาตรฐานเอาต์พุต G.8: ออก violation_list เป็น JSON ทาง standard output
print(json.dumps({"violation_list": violations}, ensure_ascii=False, indent=2))
if violations:
sys.stderr.write(f"[FAIL] พบ id ซ้ำ {len(violations)} รายการ\n")
return 1
sys.stderr.write("[PASS] ไม่มี id ซ้ำ\n")
return 0
if __name__ == "__main__":
sys.exit(main())
ตัวอย่างอินพุต (data.csv):
id,name
Q001,ภารกิจแรก
Q002,เครื่องประดับที่หายไป
Q001,ภารกิจแรก(ซ้ำ)
ผลการรันเป็นดังนี้ เนื่องจาก Q001 ปรากฏสองครั้งในแถวที่ 2 และแถวที่ 4 จึงถูกจับเป็น violation หนึ่งรายการ และโค้ดสิ้นสุดคือ 1
{
"violation_list": [
{
"id": "Q001",
"row_numbers": [2, 4]
}
]
}
สคริปต์ที่ตรวจสอบความสอดคล้องของบุคลิกเสียง (สำนวน·นิสัย) ในบทพูดของ NPC จับความผิดเพี้ยนที่ตัวละครเดียวกันใช้สำนวนต่างกันในแต่ละบท (5.2·5.4)
สคริปต์ตรวจสอบการถดถอย (regression test) ที่เปรียบเทียบว่าเมื่อทรัพยากร (อาร์ต·UI ฯลฯ) เปลี่ยนไป เกิดการเปลี่ยนแปลงทางภาพที่ไม่ได้ตั้งใจขึ้นหรือไม่ (12.1.5)
เป็นชุดสคริปต์ที่ติดตามว่าเมื่อเปลี่ยนสิ่งหนึ่งแล้วอะไรจะพลอยสั่นคลอนตามไปด้วย โดยไล่ตามการเชื่อมโยงระหว่างเอกสาร·การตัดสินใจ·ทรัพยากร และแสดงขอบเขตผลกระทบของการเปลี่ยนแปลง
สคริปต์ที่รวบรวม Wikilink ([[เป้าหมาย]]) ระหว่างเอกสารแล้วสร้างกราฟการเชื่อมโยงโดยอัตโนมัติ ทำให้เห็นในพริบตาว่าเอกสารใดอ้างอิงเอกสารใด (24.3.4)
ด้านล่างคือโครงทั่วไปที่ไม่เกี่ยวข้องกับข้อมูลของบริษัท ใช้เพียงไลบรารีมาตรฐาน (os·re·json·argparse) โดยอ่านไฟล์ .md ในโฟลเดอร์เดียว แล้วมองชื่อไฟล์ (ไม่รวมนามสกุล) เป็นโหนด และมองลิงก์ [[...]] เป็นเอจ ผลลัพธ์จะออกทั้งลิสต์ความต่อเนื่อง (adjacency list) และโค้ดผังแบบ Mermaid พร้อมกัน
#!/usr/bin/env python3
"""wikilink_graph.py
สร้างกราฟการเชื่อมโยง [[Wikilink]] ของเอกสาร .md ในโฟลเดอร์
- โหนด: ชื่อไฟล์ที่ตัดนามสกุลออก
- เอจ: การเขียน [[เป้าหมาย]] ในเนื้อหาเอกสาร ถ้าเป็นรูปแบบ [[เป้าหมาย|ข้อความแสดง]] จะดูเฉพาะเป้าหมาย
ใช้เพียงไลบรารีมาตรฐานเท่านั้น
การใช้งาน:
python wikilink_graph.py ./docs
python wikilink_graph.py ./docs --format mermaid
"""
import argparse
import json
import os
import re
import sys
WIKILINK = re.compile(r"\[\[([^\]|#]+)") # [[เป้าหมาย]] / [[เป้าหมาย|ข้อความแสดง]] / [[เป้าหมาย#สมอ]]
def extract_links(text):
"""ดึงชื่อเป้าหมายของลิงก์จากเนื้อหาตามลำดับที่ปรากฏ โดยไม่ซ้ำ"""
result = []
for match in WIKILINK.findall(text):
target = match.strip()
if target and target not in result:
result.append(target)
return result
def build_graph(doc_dir):
"""ไล่อ่าน .md ในโฟลเดอร์แล้วสร้างลิสต์ความต่อเนื่อง {ชื่อเอกสาร: [เป้าหมายลิงก์, ...]}"""
graph = {}
for name in sorted(os.listdir(doc_dir)):
if not name.endswith(".md"):
continue
node = name[:-3]
path = os.path.join(doc_dir, name)
with open(path, encoding="utf-8") as f:
graph[node] = extract_links(f.read())
return graph
def to_mermaid(graph):
"""แปลงลิสต์ความต่อเนื่องให้เป็นสตริงโค้ด Mermaid flowchart"""
lines = ["flowchart LR"]
for node, targets in graph.items():
if not targets:
lines.append(f' {_id(node)}["{node}"]')
for target in targets:
lines.append(f' {_id(node)}["{node}"] --> {_id(target)}["{target}"]')
return "\n".join(lines)
_ID_CACHE = {}
def _id(name):
"""id โหนดของ Mermaid ต้องเป็น ASCII ชื่อภาษาเกาหลีจะถูกติด id ASCII สั้น ๆ
เป็น n1, n2, ... ตามลำดับที่พบครั้งแรก และเก็บชื่อเดิมไว้ในป้ายกำกับ[...]"""
if name not in _ID_CACHE:
_ID_CACHE[name] = "n%d" % (len(_ID_CACHE) + 1)
return _ID_CACHE[name]
def main(argv=None):
parser = argparse.ArgumentParser(description="ตัวสร้างกราฟการเชื่อมโยง Wikilink")
parser.add_argument("doc_dir", help="โฟลเดอร์ที่มีเอกสาร (.md) อยู่")
parser.add_argument("--format", choices=["json", "mermaid"], default="json")
args = parser.parse_args(argv)
graph = build_graph(args.doc_dir)
if args.format == "mermaid":
print(to_mermaid(graph))
else:
print(json.dumps(graph, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())
ตัวอย่างอินพุต (สามไฟล์ในโฟลเดอร์ docs/):
docs/세계관.md ในเนื้อหามีลิงก์ [[지역_한양]] และ [[세력_의금부]]
docs/지역_한양.md ในเนื้อหามีลิงก์ [[세력_의금부]]
docs/세력_의금부.md ไม่มีลิงก์
เมื่อรันด้วย --format mermaid จะได้โค้ดผังดังนี้ โหนดจะถูกประมวลผลตามลำดับชื่อไฟล์ (세계관 → 세력_의금부 → 지역_한양) และชื่อภาษาเกาหลีเดิมยังคงอยู่ในป้ายกำกับตามที่เป็น เห็นได้ในพริบตาว่าเอกสารใดแตกแขนงไปทางไหน และปลายทาง (세력_의금부) คืออะไร
flowchart LR
n1["세계관"] --> n2["지역_한양"]
n1["세계관"] --> n3["세력_의금부"]
n3["세력_의금부"]
n2["지역_한양"] --> n3["세력_의금부"]
สคริปต์ที่วิเคราะห์ว่าการ์ดการตัดสินใจหนึ่ง ๆ ส่งผลกระทบต่อเอกสาร·ทรัพยากรใดบ้าง ก่อนจะกลับการตัดสินใจ ให้ตรวจขอบเขตผลกระทบเสียก่อน (18.4.3)
สคริปต์ที่ค้นย้อนกลับเพื่อหาสกิลที่ใช้ทรัพยากรหนึ่ง ๆ ก่อนจะแก้ไข·ลบทรัพยากร ให้รู้ว่ามีอะไรพึ่งพาอยู่บ้าง (11.2.4)
เป็นสคริปต์ที่รวบข้อมูลที่กระจัดกระจายให้กลายเป็นรายงาน·ผังที่คนอ่านได้ ทำการรายงานประจำที่เกิดซ้ำ ๆ ให้เป็นอัตโนมัติ เพื่อลดงานที่ต้องลงมือเอง
สคริปต์ที่รวบรวมส่วนที่ขาด (gap) เทียบกับเป้าหมายในขั้นอัลฟา แล้วสร้างเป็นรายงานรายสัปดาห์โดยอัตโนมัติ (10.3.3)
สคริปต์ที่แปลงความสัมพันธ์การเชื่อมโยงของการ์ดการตัดสินใจให้เป็นโค้ดผัง Mermaid ดูกระแสการตัดสินใจเป็นรูปภาพ (24.2.3)
สคริปต์ที่สรุปตัวชี้วัดหลัก (KPI) เป็นรายสัปดาห์ (13.2)
เป็นสคริปต์ที่ปรับข้อมูลซึ่งกระจัดกระจายอยู่หลายตำแหน่งให้ตรงกันอย่างมีประสิทธิภาพ โดยไม่คัดลอกทั้งหมดทุกครั้ง แต่เลือกซิงค์เฉพาะส่วนที่เปลี่ยนไป
สคริปต์ที่เลือกซิงค์บันทึกการประชุมเฉพาะส่วนที่เปลี่ยนแปลง ไม่ใช่ทั้งหมด ยิ่งข้อมูลสะสมมากขึ้น การคัดลอกทั้งหมดยิ่งช้าลง จึงใช้วิธีแบบส่วนเพิ่ม (incremental) (17.5.4)
วิธีที่ใช้ diff ของ git ในการตรวจจับว่าอะไรเปลี่ยนไปอย่างมีประสิทธิภาพ โดยไม่ต้องมีกลไกติดตามแยกต่างหาก แต่ใช้ git เองเป็นตัวตรวจจับการเปลี่ยนแปลง (17.5.4.1)
เป็นสคริปต์ที่มอบงานซึ่งต้องใช้วิจารณญาณ เช่น การจัดประเภท·การเรียกใช้ ให้แก่ LLM งานที่ไม่ลงตัวพอดีด้วยกฎ จะถูกจัดการด้วยผู้ช่วย LLM
สคริปต์ที่จัดประเภท FAQ ที่เข้ามาตามหมวดหมู่โดยอัตโนมัติ (13.1.3)
สคริปต์ที่จัดประเภทการประชุมตามลักษณะโดยอัตโนมัติ ใช้เพื่อเติมค่า category ในส่วนหัวของบันทึกการประชุม (17.3.6)
สคริปต์ที่เรียกพรอมต์ที่จำเป็นจากคลังพรอมต์ที่จัดเตรียมไว้ล่วงหน้า ทำให้ไม่ต้องเขียนพรอมต์เดิมซ้ำทุกครั้ง (22.1.2)
เป็นสคริปต์ที่จัดการไม่ให้การทำงานอัตโนมัติเองสร้างจุดบอดด้านค่าใช้จ่ายและการติดตามข้อมูลขึ้นมา
สคริปต์ที่ติดตามค่าใช้จ่ายในการเรียก LLM และบังคับใช้เพดาน (cap) สกัดกั้นค่าใช้จ่ายที่พุ่งสูงล่วงหน้า ไม่ใช่หลังจากเกิดขึ้นแล้ว (22.3.5)
ด้านล่างคือโครงทั่วไปที่ไม่เกี่ยวข้องกับข้อมูลของบริษัท ใช้เพียงไลบรารีมาตรฐาน (json·os·argparse) บันทึกจำนวนโทเค็นในแต่ละการเรียก คำนวณค่าใช้จ่ายสะสม และเมื่อเกินเพดานจะส่งสัญญาณปฏิเสธ (exit 2) ราคาต่อหน่วยเป็นค่าคงที่ในโค้ด และค่าจริงเปลี่ยนเป็นตารางราคาของโมเดลที่แต่ละคนใช้ได้ (ค่าด้านล่างเป็นตัวยึดตำแหน่งสำหรับใช้อธิบาย)
#!/usr/bin/env python3
"""llm_cost_tracker.py
บันทึกสะสมโทเค็นการเรียก LLM และตรวจสอบเพดานค่าใช้จ่ายรายวัน
- record: บวกการเรียกหนึ่งครั้ง (โทเค็นอินพุต/เอาต์พุต) ลงในไฟล์ ledger
- ถ้าค่าใช้จ่ายสะสมเกิน cap จะกั้นการเรียกด้วย exit 2 (สกัดล่วงหน้า)
ใช้เพียงไลบรารีมาตรฐานเท่านั้น
การใช้งาน:
python llm_cost_tracker.py --ledger ledger.json --in 1200 --out 800
python llm_cost_tracker.py --ledger ledger.json --in 1200 --out 800 --cap-usd 5.0
"""
import argparse
import json
import os
import sys
# ราคาต่อหน่วย: USD ต่อ 1,000 โทเค็น ค่าตัวยึดตำแหน่งสำหรับใช้อธิบาย — ให้เปลี่ยนเป็นตารางราคาโมเดลจริง
PRICE_PER_1K_INPUT = 0.003
PRICE_PER_1K_OUTPUT = 0.015
def cost_of(in_tokens, out_tokens):
"""คำนวณค่าใช้จ่าย (USD) ของการเรียกหนึ่งครั้งจากโทเค็นอินพุต/เอาต์พุต"""
return (in_tokens / 1000) * PRICE_PER_1K_INPUT + (out_tokens / 1000) * PRICE_PER_1K_OUTPUT
def load_ledger(path):
if os.path.exists(path):
with open(path, encoding="utf-8") as f:
return json.load(f)
return {"calls": 0, "in_tokens": 0, "out_tokens": 0, "total_usd": 0.0}
def save_ledger(path, ledger):
with open(path, "w", encoding="utf-8") as f:
json.dump(ledger, f, ensure_ascii=False, indent=2)
def main(argv=None):
parser = argparse.ArgumentParser(description="การติดตาม·เพดานค่าใช้จ่าย LLM")
parser.add_argument("--ledger", required=True, help="เส้นทางไฟล์ JSON สำหรับบันทึกสะสม")
parser.add_argument("--in", dest="in_tokens", type=int, required=True, help="โทเค็นอินพุตของการเรียกครั้งนี้")
parser.add_argument("--out", dest="out_tokens", type=int, required=True, help="โทเค็นเอาต์พุตของการเรียกครั้งนี้")
parser.add_argument("--cap-usd", type=float, default=None, help="เพดานค่าใช้จ่ายสะสม (USD) ถ้าเกินจะกั้น")
args = parser.parse_args(argv)
ledger = load_ledger(args.ledger)
this_cost = cost_of(args.in_tokens, args.out_tokens)
ledger["calls"] += 1
ledger["in_tokens"] += args.in_tokens
ledger["out_tokens"] += args.out_tokens
ledger["total_usd"] = round(ledger["total_usd"] + this_cost, 6)
save_ledger(args.ledger, ledger)
print(json.dumps({"this_call_usd": round(this_cost, 6), "ledger": ledger}, ensure_ascii=False, indent=2))
if args.cap_usd is not None and ledger["total_usd"] > args.cap_usd:
sys.stderr.write(f"[CAP] สะสม {ledger['total_usd']} USD > เพดาน {args.cap_usd} USD — กั้น\n")
return 2
return 0
if __name__ == "__main__":
sys.exit(main())
ตัวอย่างอินพุตและผลลัพธ์ เมื่อบันทึกโทเค็นอินพุต 1,200·เอาต์พุต 800 จากสถานะว่าง ค่าใช้จ่ายของการเรียกครั้งนี้คือ 1200/1000*0.003 + 800/1000*0.015 = 0.0036 + 0.012 = 0.0156 USD
{
"this_call_usd": 0.0156,
"ledger": {
"calls": 1,
"in_tokens": 1200,
"out_tokens": 800,
"total_usd": 0.0156
}
}
เมื่อให้ --cap-usd 0.01 ไปด้วย เนื่องจากค่าสะสม 0.0156 เกินเพดาน 0.01 จึงกั้นการเรียกครั้งถัดไปด้วยโค้ดสิ้นสุด 2 นี่คือการทำงานจริงของ "สกัดล่วงหน้า ไม่ใช่หลังจากเกิดขึ้นแล้ว"
สคริปต์ที่บันทึกแหล่งที่มาของข้อมูลที่อ้างอิง·ใช้อ้างถึงโดยอัตโนมัติ เก็บไว้เพื่อให้ย้อนกลับไปดูแหล่งที่มาในภายหลังได้ (24.5.4)
สำคัญกว่าการสร้างสคริปต์จำนวนมาก คือการที่สคริปต์ที่สร้างขึ้นทำงานได้อย่างน่าเชื่อถือ หลักการห้าข้อด้านล่างนี้ใช้ร่วมกันกับสคริปต์ทั้งหมดข้างต้น
| หลักการ | คำอธิบาย |
|---|---|
| ความเรียบง่าย | หลีกเลี่ยงไลบรารีที่ซับซ้อน |
| การทดสอบ | เทสต์ระดับหน่วย (unit test) ทุกสคริปต์ |
| มาตรฐานเอาต์พุต | มาตรฐานเช่น violation_list (10.1.7) |
| การควบคุมเวอร์ชัน | git |
| ด่านตรวจสอบโดยผู้ใช้ | การทำงานอัตโนมัติก็ต้องมีการตรวจสอบโดยมนุษย์ |
หลักการข้อสุดท้ายสำคัญเป็นพิเศษ การทำงานอัตโนมัติไม่ใช่การมาแทนที่คน แต่เป็นการลดขั้นตอนก่อนหน้าวิจารณญาณของคน ไม่ว่าจะเป็นการตรวจสอบ·การสกัด·การสร้าง ก่อนการนำไปใช้ขั้นสุดท้าย ต้องวางด่านที่คนได้ดูสักครั้งเสมอ
เพื่อไม่ให้หลักการ "การทดสอบ" เป็นเพียงคำพูด ผู้เขียนจึงวางเทสต์จริงที่ตรวจสอบฟังก์ชันหลัก find_duplicate_ids ของ G.2.1 ด้วยไลบรารีมาตรฐาน unittest ไว้ เนื่องจากไม่มีการพึ่งพาภายนอก จึงบันทึกตามที่เป็นแล้วรันด้วย python -m unittest test_integrity_check -v ได้ ประเด็นสำคัญคือ ฟังก์ชันที่จะตรวจสอบต้องถูกแยกออกจากการอ่าน/เขียนไฟล์ จึงจะทดสอบได้ง่ายเช่นนี้ (จึงเป็นเหตุที่ G.2.1 แยกตรรกะการตรวจสอบกับ load_rows ออกจากกัน)
# test_integrity_check.py
import unittest
from integrity_check_id_uniqueness import find_duplicate_ids
class TestFindDuplicateIds(unittest.TestCase):
def test_no_duplicates_returns_empty(self):
rows = [{"id": "Q001"}, {"id": "Q002"}]
self.assertEqual(find_duplicate_ids(rows, "id"), [])
def test_one_duplicate_reports_row_numbers(self):
rows = [{"id": "Q001"}, {"id": "Q002"}, {"id": "Q001"}]
self.assertEqual(
find_duplicate_ids(rows, "id"),
[{"id": "Q001", "row_numbers": [2, 4]}],
)
def test_missing_column_treated_as_empty_string(self):
rows = [{"name": "a"}, {"name": "b"}]
result = find_duplicate_ids(rows, "id")
self.assertEqual(result, [{"id": "", "row_numbers": [2, 3]}])
if __name__ == "__main__":
unittest.main()
เมื่อรันแล้ว เทสต์ทั้งสามผ่านทั้งหมด
test_missing_column_treated_as_empty_string ... ok
test_no_duplicates_returns_empty ... ok
test_one_duplicate_reports_row_numbers ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
สิ่งที่มักถูกมองข้ามในหลักการข้างต้นคือการจัดการความล้มเหลวของ hook ตัว hook ที่รันอัตโนมัติก่อนคอมมิตหรือเมื่อบันทึก โดยแก่นแท้แล้วควรเป็นกิ่งก้านของงานหลัก (คอมมิต·บันทึก) แต่ถ้า hook ออกโค้ดสิ้นสุดที่ไม่ใช่ 0 จากความผิดพลาดภายใน งานหลักที่ผูก hook นั้นไว้ก็จะถูกกั้นทั้งดุ้น เท่ากับว่ากลไกเสริมจับงานหลักเป็นตัวประกัน ดังนั้น hook ที่มีลักษณะเป็นตัวช่วยจึงต้องทำให้ไม่ว่าเกิดอะไรขึ้นภายในก็คงเหลือเพียงคำเตือนทาง standard error (stderr) และคืนโค้ดสิ้นสุดเป็น 0 เพื่อไม่ให้กั้นงานหลัก ด้านล่างคือรูปแบบขั้นต่ำของสิ่งนั้น แม้เกิด exception ภายในก็ตาม โค้ดสิ้นสุดก็เป็น 0
import sys
def run_hook():
raise RuntimeError("เกิดข้อผิดพลาดภายใน")
def main():
try:
run_hook()
except Exception as exc:
sys.stderr.write(f"[hook] คำเตือน: {exc} — จะไม่กั้นงานหลัก\n")
return 0 # hook ตัวช่วยจะไม่กั้นงานหลักไม่ว่าเกิดอะไรขึ้น
if __name__ == "__main__":
sys.exit(main())
เมื่อรันแล้วจะเห็นคำเตือน แต่โค้ดสิ้นสุดเป็น 0 กล่าวคือ คนรู้ได้ว่ามีอะไรผิดเพี้ยน และกระแสงานก็ไม่ขาดตอน
[hook] คำเตือน: เกิดข้อผิดพลาดภายใน — จะไม่กั้นงานหลัก
(โค้ดสิ้นสุด 0)
อย่างไรก็ตาม "ความล้มเหลวเงียบ" นี้ใช้กับ hook ตัวช่วยเท่านั้น สำหรับการตรวจสอบที่ตัวการผ่านหรือไม่ผ่านคือจุดประสงค์เอง อย่างด่านคุณภาพใน G.2 นั้น ตรงกันข้าม เมื่อล้มเหลวต้องออกโค้ดที่ไม่ใช่ 0 (exit 1 ที่เห็นมาก่อนหน้า) เพื่อหยุดไปป์ไลน์ แม้จะเป็นตำแหน่ง hook เดียวกัน นโยบายโค้ดสิ้นสุดก็ตรงกันข้ามขึ้นกับว่าเป็น "ตัวช่วย" หรือ "ด่าน" ซึ่งต้องแยกแยะให้ออก
นโยบาย exit 0 ในหัวข้อก่อนมีราคาที่ต้องจ่ายอยู่หนึ่งอย่าง การที่ hook ตัวช่วยไม่กั้นงานหลักไม่ว่าเกิดอะไรขึ้น เมื่อพลิกกลับก็หมายความว่า แม้ hook ตายเงียบ ๆ งานหลักก็ยังเดินต่อได้อย่างปกติ hook ที่รันในกิ่งก้าน เช่น การฉีดบริบทอัตโนมัติ ต่อให้ไม่ทำงานหลายวัน กระแสงานก็ไม่ขึ้นไฟแดง ดังนั้น hook ตัวช่วยจึงต้องมีกลไกคู่หู "ล้มเหลวก็ไม่กั้น" ควบคู่กับ "ให้คนได้เห็นความล้มเหลวแม้จะช้าก็ตาม" เสมอ ถ้าขาดคู่หูไป สักวันหนึ่งจะมาพบในการทบทวนว่า "atom นี้ช่วงนี้ไม่ขึ้นเลยสักครั้ง" แล้วจึงได้รู้ว่า hook ตายมาทั้งสัปดาห์แล้ว
คู่หูนั้นคือล็อก อย่าปล่อยให้คำเตือนที่รูปแบบขั้นต่ำในหัวข้อก่อน (sys.stderr.write(...)) ทิ้งไว้ระเหยหายไป แต่ให้หล่นลงเป็นไฟล์ โดยการเรียกปกติบันทึกหนึ่งบรรทัด การเรียกที่ล้มเหลวบันทึกหนึ่งบรรทัดพร้อมเหตุผล ในสภาพแวดล้อมของผู้เขียน ร่องรอยนี้สะสมอยู่ที่ ~/.claude/hooks/_injection_log.txt (ล็อกเดียวกันนี้ยังถูกอ่านในการตรวจสอบการทำงานของ §21.3.4 ด้วย) ลูปการดำเนินงานไม่ได้ใหญ่โต เพียงทำขั้นตอนตรวจ·กู้คืนสามขั้นวนหนึ่งรอบก็พอ
| ขั้นตอน | ดูอะไร | ทำอะไร |
|---|---|---|
| ตรวจจับ | บรรทัดการฉีดปกติล่าสุดในล็อกขาดตอน หรือบรรทัดความล้มเหลวด้วยเหตุผลเดียวกันเกิดซ้ำหรือไม่ | กวาดดูหางล็อกสักครั้งในการทบทวนรายสัปดาห์ (จับอัตโนมัติบรรทัดเดียวก็พอ) |
| แยกแยะ | เหตุของความล้มเหลวเป็นบั๊กของ hook เอง หรือเป็นข้อมูลอินพุต (manifest ที่เสีย·ไฟล์ atom ที่ไม่มี) | แยกสองอย่างด้วยสตริงเหตุผลใน stderr — ถ้าเป็นปัญหาโค้ดก็แก้โค้ด ถ้าเป็นปัญหาข้อมูลก็แก้ manifest |
| กู้คืน | การฉีดปกติกลับมาขึ้นด้วยตัวกระตุ้นหรือไม่ | หลังแก้แล้ว ลองป้อนตัวกระตุ้นที่ตั้งใจไว้สักครั้งในเซสชันใหม่ เพื่อยืนยันว่ามีบรรทัดปกติกลับมาในล็อกหรือไม่ (เหมือนการตรวจสอบการทำงานของ §21.3.4) |
แก่นคือ การมอบ "การตรวจจับ" ให้แก่ ไฟล์ล็อกหนึ่งไฟล์และบรรทัดการทบทวนหนึ่งบรรทัด แทนที่จะพึ่งความใส่ใจของคน สิ่งที่ exit 0 กั้นไว้คือการหยุดชะงักของงานหลัก ไม่ใช่การปกปิดความล้มเหลว ความล้มเหลวถูกเผยออกมาทาง stderr→ล็อก การทบทวนหมั่นส่องดูล็อกนั้นเป็นระยะ และการกู้คืนก็นำการตรวจสอบการทำงานที่ใช้อยู่เป็นปกติมาใช้ซ้ำตามเดิม เมื่อ "ไม่กั้น + เปิดเผย + ส่องดูเป็นระยะ + กู้คืนด้วยวิธีเดิม" เป็นชุดเดียวกันเท่านั้น ความล้มเหลวเงียบจึงจะไม่แข็งตัวกลายเป็นการปล่อยปละเงียบ ๆ
โค้ดในรวมตัวอย่างนี้มีสองชนิด ชนิดหนึ่งคือโค้ดอย่าง G.1.1·G.2.1·G.3.1·G.7.1·G.8 ที่เขียนขึ้นใหม่เป็นโครงทั่วไปที่ไม่เกี่ยวข้องกับข้อมูลของบริษัท และตรวจสอบแล้วว่ารันได้ตามที่เป็น ใช้เพียงไลบรารีมาตรฐาน และตัวอย่างอินพุต·เอาต์พุต·โค้ดสิ้นสุดที่เขียนไว้ข้างต้นล้วนเป็นผลที่รันจริงและยืนยันแล้ว คัดลอกไปใช้ได้ทันที เพียงเปลี่ยนค่าตัวยึดตำแหน่ง เช่น ตารางราคาหรือชื่อคอลัมน์ ให้เหมาะกับสภาพแวดล้อมของตนเอง
อีกชนิดหนึ่งคือรายการอย่างหัวข้อที่เหลือ ที่ระบุไว้แค่ชื่อ·บทบาท·หัวข้อเนื้อหาหลักที่เชื่อมโยง เหตุที่ไม่ได้ลงเป็นโค้ดเต็มนั้น พูดอย่างตรงไปตรงมามีสองข้อ ข้อแรก ต้นฉบับสคริปต์การดำเนินงานของบริษัทเป็น IP ของบริษัท จึงคัดลอกมาตามที่เป็นไม่ได้ ข้อสอง ตรรกะส่วนใหญ่ผูกกับสคีมาข้อมูล·โครงสร้างโฟลเดอร์·รูปแบบการ์ดการตัดสินใจเฉพาะของบริษัท เมื่อถอดสมมติฐานเหล่านั้นออกไปแล้ว ก็ไม่เหลือโค้ดที่ใช้ประโยชน์ได้ตามที่เป็นสำหรับผู้อ่านทั่วไป ด้วยเหตุนี้ผู้เขียนจึงเลื่อนขั้นเป็นโค้ดจริงเฉพาะสี่ตัวที่ทำให้เป็นรูปแบบทั่วไปได้อย่างเรียบร้อย (ตัวตรวจรูปแบบ·ตัวตรวจความสอดคล้อง·แผนผังความสัมพันธ์·ตัวติดตามค่าใช้จ่าย) และคงที่เหลือไว้เป็นโครง ผู้อ่านสามารถยึดสี่ตัวนี้เป็นต้นแบบ แล้วสร้างการนำไปใช้ที่เหมาะกับสภาพแวดล้อมของตนเองด้วยวิธีเดียวกัน — แยกตรรกะการตรวจสอบออกจากการอ่าน/เขียน ออกรายการ violation ทาง standard output และติดเทสต์ระดับหน่วยเข้าไป
ขั้นตอนการนำเครื่องมือที่มีอยู่มาดัดแปลง ดูได้ที่ภาคผนวก B
นักออกแบบเกมที่ทำงานมานานย่อมมีเอกสารงานสะสมไว้นับสิบปี ทั้งบันทึกการประชุม บันทึกการตัดสินใจ การทบทวน บันทึกการเรียนรู้ ไปจนถึงบทเรียนที่ได้จากความล้มเหลว ภาคผนวกนี้ว่าด้วยวิธีนำเอกสารเหล่านั้นกลับมาใช้ใหม่ในโปรเจกต์ใหม่ ความตึงเครียดหลักมีเพียงข้อเดียว เอกสารส่วนใหญ่เป็น IP ของบริษัทจึงเคลื่อนย้ายตามอำเภอใจไม่ได้ แต่ในขณะเดียวกันภายในนั้นก็มีการเรียนรู้ส่วนบุคคลที่ใช้ได้ทุกที่ปะปนอยู่ด้วย การแยกสองสิ่งนี้ออกจากกันคือจุดเริ่มต้นของการนำกลับมาใช้ใหม่
วิธีใช้ภาคผนวกนี้แตกต่างกันไปตามตำแหน่งของคุณ หากคุณอยู่ในสถานการณ์ที่ต้องการดึงเอกสารเก่ามาใช้ในโปรเจกต์ใหม่ ให้ทำตาม H.2 (หลักการแยก) และ H.3 (ขั้นตอน) ตามลำดับ หากกังวลว่าจะเกิดเหตุพลาดขณะเคลื่อนย้ายจริง ให้อ่าน H.5 (ห้ากับดัก) ก่อนเพื่อหลีกเลี่ยงล่วงหน้า หากคุณยังอยู่ช่วงต้นของอาชีพและยังไม่มีเอกสารสะสมมากนัก ให้ดู H.6 แล้วกำหนดตั้งแต่ตอนนี้ว่าจะเก็บอะไรไว้อย่างไร
หลักการที่กล่าวถึงในที่นี้ไม่ใช่ทฤษฎีการบริหารสินทรัพย์อันใหญ่โต แต่บีบอัดได้ในประโยคเดียวว่า "เก็บสิ่งที่เป็นรูปธรรมไว้ที่บริษัท นำกลับมาเฉพาะรูปแบบ (pattern) ที่เป็นนามธรรมเท่านั้น" ส่วนที่เหลือคือวิธีนำประโยคนั้นไปใช้กับสถานการณ์จริง
ก่อนอื่นมาดูว่ามีเอกสารประเภทใดสะสมไว้บ้าง และสิทธิ์ในการเก็บรักษาของแต่ละอย่างต่างกันอย่างไร เพราะเมื่อสิทธิ์การเก็บรักษาต่างกัน ขอบเขตที่นำกลับมาใช้ใหม่ได้ก็ต่างกันด้วย
| เอกสาร | การเก็บรักษา |
|---|---|
| บันทึกการประชุม (เอกสารบริษัท) | อยู่ในสิทธิ์ของบริษัท |
| decision card (เอกสารบริษัท) | อยู่ในสิทธิ์ของบริษัท |
| การทบทวนรายไตรมาส (ส่วนบุคคล + บริษัท) | เก็บสำเนาส่วนบุคคลได้ |
| บันทึกการเรียนรู้ (ส่วนบุคคล) | เป็นของส่วนบุคคลถาวร |
| บันทึกบทเรียนจากความผิดพลาด (การเรียนรู้ส่วนบุคคล) | เป็นของส่วนบุคคลถาวร |
บันทึกการประชุมและ decision card อยู่ภายในสิทธิ์ของบริษัท การทบทวนสามารถเก็บสำเนาส่วนบุคคลได้ ส่วนบันทึกการเรียนรู้และบันทึกบทเรียนจากความผิดพลาดเป็นสินทรัพย์ส่วนบุคคลโดยสมบูรณ์ เอกสารที่สะสมมานานเป็นสินทรัพย์การเรียนรู้ขนาดใหญ่ในตัวเอง แต่ต้องไม่ทำให้เส้นแบ่งระหว่างพื้นที่ IP ของบริษัทกับพื้นที่ส่วนบุคคลพร่าเลือน ยิ่งแบ่งเส้นให้ชัด ก็ยิ่งนำกลับมาใช้ใหม่ได้อย่างสบายใจ
เกณฑ์ของการแยกคือ "เป็นรูปธรรมหรือเป็นนามธรรม" ผลลัพธ์ที่เป็นรูปธรรมเป็นของบริษัท ส่วนรูปแบบความคิดที่สร้างผลลัพธ์นั้นขึ้นมาเป็นของส่วนบุคคล ประเด็นสำคัญคือทั้งสองด้านนี้ออกมาด้วยกันจากงานชิ้นเดียวกัน
| พื้นที่ | IP ของบริษัท | การเรียนรู้ส่วนบุคคล |
|---|---|---|
| เนื้อหาการตัดสินใจ | บริษัท | — |
| รูปแบบการตัดสินใจ (สถานการณ์แบบนี้ตัดสินแบบนี้ดี) | — | ส่วนบุคคล |
| ข้อมูลเกม | บริษัท | — |
| โนว์ฮาวการดำเนินงาน (การบริหารจัดการ rulebook · เครื่องมือ) | — | ส่วนบุคคล |
| โค้ด | บริษัท | — |
| อัลกอริทึม · โครงสร้าง | — | ส่วนบุคคล |
"ตัดสินใจอะไรไป" เป็น IP ของบริษัท แต่ "ในสถานการณ์แบบนี้การตัดสินใจแบบนี้ใช้ได้ผลดี" เป็นรูปแบบที่เป็นการเรียนรู้ส่วนบุคคล ค่าข้อมูลเกมเองเป็นของบริษัท แต่โนว์ฮาวในการดำเนินงานกับข้อมูลนั้นเป็นของส่วนบุคคล เก็บเอกสารที่เป็นรูปธรรมไว้ที่บริษัท นำกลับมาเฉพาะรูปแบบที่เป็นนามธรรม — นี่คือหลักการของการแยก
เมื่อแปลงหลักการแยกให้เป็นงานจริง จะได้ห้าขั้นตอนต่อไปนี้ ระบุเอกสาร แยก IP ออก สกัดการเรียนรู้ ทำให้เป็นภาพรวม แล้วจึงนำไปใช้กับโปรเจกต์ใหม่
flowchart TD
A["ระบุเอกสารในอดีต"] --> B["แยกส่วนที่เป็น IP ของบริษัท"]
B --> C["สกัดส่วนการเรียนรู้ส่วนบุคคล"]
C --> D["ทำให้เป็นนามธรรม · เป็นภาพรวม"]
D --> E["นำไปใช้กับโปรเจกต์ใหม่"]
classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
class E pass;
ขั้นตอนนี้ต้องดำเนินการหลังจากตรวจสอบสิทธิ์ของบริษัทและผ่านการพิจารณาทางกฎหมายแล้วเท่านั้น ถึงแม้จะทำให้เป็นนามธรรมเพียงพอแล้ว แต่หากจุดเริ่มต้นเป็นเอกสารของบริษัท การได้รับการยืนยันตามขั้นตอนไว้ก่อนย่อมปลอดภัยกว่า
กรณีตัวอย่างการนำกลับมาใช้ใหม่ที่ใกล้ตัวที่สุดคือหนังสือเล่มนี้เอง เนื้อหาในหลายจุดเริ่มต้นจากงานในอดีตของผู้เขียน และเป็นผลลัพธ์จากการทำให้เป็นภาพรวมและทำให้ไม่ระบุตัวตน (anonymization) ผ่านขั้นตอนข้างต้น
| พื้นที่ | ที่มา | การนำกลับมาใช้ใหม่ |
|---|---|---|
| การออกแบบแบบบูรณาการ Layer (ส่วนที่ 6) | การดำเนินงานหลายปีของผู้เขียน | การเรียนรู้ส่วนบุคคล → ทำให้เป็นภาพรวม |
| ระบบบันทึกการประชุม (ส่วนที่ 17) | การดำเนินงานโปรเจกต์ A ของผู้เขียน | รูปแบบของบริษัท → ทำให้ไม่ระบุตัวตน |
| โนว์ฮาวการดำเนินงาน (ส่วนที่ 24) | การสะสมหลายปี | การเรียนรู้ส่วนบุคคล → ทำให้เป็นภาพรวม |
| รายการสินค้าคงคลังในภาคผนวก A | โปรเจกต์ A ของบริษัท | ทำให้ไม่ระบุตัวตน + กลั่นกรองบางส่วน |
การออกแบบ Layer และโนว์ฮาวการดำเนินงานทำให้การเรียนรู้ส่วนบุคคลเป็นภาพรวม ส่วนระบบบันทึกการประชุมและภาคผนวก A ทำให้รูปแบบของบริษัทไม่ระบุตัวตน ทุกรายการผ่านการขออนุญาตจากบริษัทแล้ว และ IP ของบริษัทถูกทำให้ไม่ระบุตัวตนครบถ้วนไม่มีตกหล่น กล่าวได้ว่าหนังสือซึ่งเป็นผลลัพธ์เองนั่นแหละคือหลักฐานเชิงประจักษ์ของขั้นตอน H.3
การนำกลับมาใช้ใหม่หากทำดีก็เป็นสินทรัพย์ แต่หากทำพลาดก็เป็นอุบัติเหตุ ห้ากับดักด้านล่างเป็นจุดที่เหยียบพลาดบ่อยจริง และได้แนบใบสั่งยาไว้กับแต่ละข้อ
หากใช้เอกสารโดยไม่ได้รับอนุญาตจากบริษัท จะลุกลามเป็นข้อพิพาท ใบสั่งยาเรียบง่าย ขออนุญาตจากบริษัทก่อนใช้
หากชื่อบริษัทหรือชื่อจริงหลงเหลือแม้แต่จุดเดียว ก็กลายเป็นอุบัติเหตุ IP ใบสั่งยาคือการตรวจด้วย grep อัตโนมัติ ทำชื่อบริษัท · ชื่อจริง · เส้นทางไฟล์ให้เป็น watchlist แล้วให้เครื่องไล่ตรวจครบถ้วนไม่ตกหล่น
หากนำโนว์ฮาวเก่าแก่มาใช้โดยไม่แตะต้อง ก็จะไม่เข้ากับช่วงเวลาปัจจุบัน ใบสั่งยาคือการเรียบเรียงใหม่ให้เข้ากับยุคสมัย คงหลักการไว้ แต่ปรับเครื่องมือและบริบทให้ทันสมัยเป็นปัจจุบัน
หากย้ายมาเฉพาะกรณีรูปธรรม ก็จะนำไปใช้กับสภาพแวดล้อมอื่นได้ยาก ใบสั่งยาคือวางรูปแบบที่เป็นนามธรรมและตัวอย่างที่เป็นรูปธรรมไว้ด้วยกัน จับความเป็นทั่วไปด้วยรูปแบบ และจับความเข้าใจด้วยตัวอย่าง
ต่อให้มีเอกสารมากเพียงใด หากไม่หยิบกลับมาดูอีก ก็ไม่ต่างจากไม่มี ใบสั่งยาคือวงจรการเรียนรู้สม่ำเสมอ สร้างรอบที่จะได้พบเอกสารอีกครั้ง เหมือนกับการทบทวนรายวัน · รายสัปดาห์ · รายเดือน
หลักการนี้ไม่ใช่ของผู้เขียนเพียงผู้เดียว ผู้อ่านก็สามารถนำเอกสารจากอาชีพของตนกลับมาใช้ใหม่ด้วยวิธีเดียวกัน ด้านล่างคือนิสัยที่แนะนำซึ่งเริ่มได้ตั้งแต่ตอนนี้
| ข้อแนะนำ | เหตุผล |
|---|---|
| ทบทวนการตัดสินใจของตนทุกไตรมาส | ค้นพบรูปแบบ |
| เก็บบันทึกการเรียนรู้แยกต่างหาก | แยกออกจาก IP ของบริษัท |
| ระบุรูปแบบที่เป็นนามธรรมให้ชัด | นำกลับมาใช้ใหม่ในอนาคตได้ |
| การให้คำปรึกษา (mentoring) · การนำเสนอภายนอก | แบ่งปันรูปแบบ |
| หนังสือ · บล็อก (หลังได้รับอนุญาตจากบริษัท) | การเรียนรู้คงอยู่ตลอดไป |
หากทบทวนการตัดสินใจของตนทุกไตรมาส รูปแบบก็จะปรากฏให้เห็น และหากเก็บบันทึกการเรียนรู้แยกออกจากเอกสารของบริษัท ภายหลังก็จะหยิบมาใช้ได้อย่างสบายใจ เมื่อนำรูปแบบนั้นออกไปสู่การให้คำปรึกษา · การนำเสนอ · การเขียนหนังสือ การเรียนรู้ก็จะคงอยู่ยาวนานแทนที่จะถูกใช้เพียงครั้งเดียวแล้วหายไป สุดท้ายแล้วการเรียนรู้ของตนเองนั่นแหละคือสินทรัพย์ของตนเอง
กรณีศึกษาเชิงลึกของเอดิเตอร์ BehaviorTree ที่กล่าวถึงใน 7.2 ครอบคลุมการตัดสินใจพัฒนาเอง การอิมพลีเมนต์ และประสบการณ์การดำเนินงาน
รายละเอียดของเหตุผล 4 ข้อในการตัดสินใจที่กล่าวถึงใน 7.2.8
| เหตุผล | รายละเอียด |
|---|---|
| จำเป็นต้องติดตามด้วย diff·git | BT ของ UE เป็น .uasset แบบไบนารี ติดตามการเปลี่ยนแปลงได้ยาก เมื่อใช้ JSON จะทำ diff แบบข้อความได้ |
| การอ้างอิง subtree + การติดตามผลกระทบ | เมื่อดำเนินงาน BT จำนวน 100\~300 ตัว การวิเคราะห์ผลกระทบในระดับ subtree เป็นปัจจัยชี้ขาด |
| การตรวจสอบด้วยการจำลอง | สามารถรันเฉพาะ BT แยกออกมาได้โดยไม่ต้องบิลด์ |
| การช่วยเขียนด้วย AI | LLM สร้างและตีความ JSON BT ได้อย่างเป็นธรรมชาติ |
[1. การวางแผน·นิยามความต้องการ (1~2 สัปดาห์)]
- เขียนความต้องการ 4 ข้อเป็นลายลักษณ์อักษร
- ออกแบบ JSON schema
[2. การอิมพลีเมนต์รันไทม์ (3~4 สัปดาห์)]
- JSON parser
- เอนจินรัน BT
- การแก้ไขการอ้างอิง subtree
[3. การอิมพลีเมนต์เอดิเตอร์ (4~6 สัปดาห์)]
- ตัวแก้ไข JSON (แบบกราฟิก)
- UI ไลบรารี subtree
- เครื่องมือวิเคราะห์ผลกระทบ
[4. ตัวจำลอง (2~3 สัปดาห์)]
- รัน BT แยกออกมา
- ดึงสถิติ
[5. การผนวก AI (2~3 สัปดาห์)]
- การเขียน BT โดยมี LLM ช่วย
- การฉีดคอนเท็กซ์
[6. การผนวก UE (2~4 สัปดาห์)]
- แปลงไปมากับ BT ของ UE
- การผนวกเข้ากับบิลด์
รวมประมาณ 4\~6 เดือน ใช้นักพัฒนา 1\~2 คน
เอดิเตอร์นี้เป็นเครื่องมือภายในบริษัทในขั้น R&D ไม่ได้เดินเครื่องในระยะยาวและขนาดใหญ่พอที่จะเรียกได้ว่าเป็น "ผลวัดจริงจากการดำเนินงาน 1 ปี" ดังนั้นตามหลักการของหนังสือเล่มนี้ ในที่นี้จึงไม่ใส่สถิติการดำเนินงานที่กุขึ้น ขนาด 100\~300 ตัวที่กล่าวถึงใน I.1 คือ เป้าหมายการออกแบบ ที่ใช้ให้เหตุผลในการพัฒนาเอง ไม่ใช่ผลที่วัดได้
ในบรรดาขีดจำกัดที่การออกแบบตรึงไว้ สิ่งที่เข้าไปอยู่ในโค้ดจริงคือ ขีดจำกัด depth การอ้างอิง subtree ที่ 5 (เพื่อป้องกันการเรียกซ้ำไม่รู้จบ) ส่วนค่าอย่างจำนวน BT หรือจำนวนครั้งที่รันการจำลองในระหว่างการดำเนินงานนั้นจะแตกต่างกันไปตามขนาดของโปรเจกต์ ดังนั้นแทนที่จะเขียนตัวเลขที่กุขึ้น ผู้เขียนขอแนะนำให้วัดด้วยตนเองในสภาพแวดล้อมของคุณเอง
| เหตุขัดข้อง | บทเรียน |
|---|---|
| การอ้างอิง subtree ไม่รู้จบ (การเรียกซ้ำ) | ขีดจำกัด depth การอ้างอิงที่ 5 |
| ความต่างระหว่างการจำลองกับพฤติกรรมจริง | ปรับเทียบสภาพแวดล้อมการจำลองทุกเดือน |
| อาการหลอน (hallucination) ของ BT ที่ LLM สร้าง | เสริมการตรวจสอบ + การตรวจสอบโดยดีไซเนอร์ |
| จำนวน BT พุ่งสูง (เกินแผน) | รอบจัดระเบียบรายไตรมาส |
ต้นทุนการพัฒนาและการดำเนินงานเป็นการประมาณตามกำหนดการที่วางไว้ตอนสร้างเครื่องมือนี้ ส่วนด้าน "ผล" ไม่ใช่ค่าที่วัดได้ แต่เป็นทิศทางที่ตั้งเป้าไว้จากการนำมาใช้ แทนที่จะเขียนตัวเลขการลดต้นทุนที่กุขึ้น จึงเขียนเฉพาะทิศทางไว้
| รายการ | ค่า | ลักษณะ |
|---|---|---|
| ต้นทุนการพัฒนา | นักพัฒนา 4\~6 เดือน | กำหนดการตามแผน (ประมาณ) |
| ต้นทุนการดำเนินงาน | นักพัฒนา 1\~2 สัปดาห์ต่อไตรมาส (บำรุงรักษา) | กำหนดการตามแผน (ประมาณ) |
| ผลที่ตั้งเป้า — กำลังคนในการดำเนินงาน | ลดกำลังคนที่รับผิดชอบ BT เมื่อดำเนินงาน NPC ฝ่ายศัตรูในขนาดใหญ่ | ทิศทาง (ยังไม่ได้วัด) |
| ผลที่ตั้งเป้า — เหตุขัดข้อง | ลดเหตุขัดข้องของ BT เชิงโครงสร้าง อย่างการเรียกซ้ำของ subtree หรืออาการหลอนของ LLM | ทิศทาง (ยังไม่ได้วัด) |
ระยะเวลาคืนทุนของการนำมาใช้จะแตกต่างกันไปตามขนาด NPC และค่าแรงของโปรเจกต์ ผู้เขียนจึงขอแนะนำให้วัดรายการข้างต้นในสภาพแวดล้อมของคุณเองแล้วตัดสินใจ และจะไม่ฟันธงในแบบที่ว่า "คืนทุนภายใน 1 ปี" — ตัวเลขนั้นเราไม่มี
ใช้พรอมต์ของ 7.2.6 ด้านล่างคือตัวอย่างที่แสดงโครงสร้างของผลลัพธ์ (เป็นตัวอย่างรูปแบบ ไม่ใช่ข้อมูลจากการดำเนินงานจริง)
{
"bt_id": "bt_new_mage_v1",
"category": "ranged_combatant",
"tags": ["scholar_faction", "ranged", "magic"],
"root": {
"type": "selector",
"children": [
{
"type": "sequence",
"name": "low_hp_retreat",
"children": [
{"type": "condition", "fn": "hp_below", "param": 0.3},
{"type": "subtree_ref", "id": "subtree_retreat_to_ally"}
]
},
{
"type": "sequence",
"name": "magic_attack",
"children": [
{"type": "condition", "fn": "enemy_in_range", "param": 15},
{"type": "subtree_ref", "id": "subtree_magic_attack_pattern"}
]
}
]
}
}
ดีไซเนอร์ตรวจสอบแล้วจึงจำลอง → ผ่าน → นำเข้าบิลด์
การพัฒนา BehaviorTree เองมักมีเหตุผลรองรับเมื่อดำเนินงานในขนาดตั้งแต่ 100 ตัวขึ้นไป (เป็นการตัดสินใจเชิงออกแบบ) หากต่ำกว่านั้น BT พื้นฐานของ UE ก็เพียงพอแล้ว
ตัวเลือกทางเลือก: - BehaviorTree.CPP (โอเพนซอร์ส มาตรฐาน) - Behavior Designer (เชิงพาณิชย์จากภายนอก) - การพัฒนาเอง (อิสระสูงสุด แต่มีภาระการดำเนินงาน)
เกณฑ์การเลือกดูได้ที่ 7.2.8
ในภาคผนวกนี้ได้รวบรวมคำย่อที่ปรากฏในเนื้อหาและศัพท์เฉพาะของหนังสือเล่มนี้ไว้ในที่เดียว เนื้อหาหลักจะอธิบายความหมายของคำย่อแต่ละคำหนึ่งครั้งเมื่อปรากฏครั้งแรก แต่หากอ่านแบบไม่เรียงลำดับหรือลืมไประหว่างทาง สามารถค้นหาได้ทันทีจากที่นี่ หากคำย่อหนึ่งมีความหมายต่างกันตามบริบท ก็ได้ระบุไว้ทั้งสองความหมาย
อภิธานศัพท์นี้จัดกลุ่มตามลำดับดังนี้ ระดับขนาดทีม → เอกสารออกแบบเกม → โดเมนเกม → ข้อมูล·การดำเนินงาน → AI·เครื่องมือ → มาตรฐาน UI·การเข้าถึง → ไฟล์·รูปแบบ หากนึกถึงลักษณะของคำย่อที่ต้องการก่อน ก็จะแคบขอบเขตได้ว่าอยู่ในกลุ่มใด ตัวอย่างเช่น DPS·TTK อยู่ในกลุ่ม "โดเมนเกม", KPI·DAU อยู่ในกลุ่ม "ข้อมูล·การดำเนินงาน", atom·JIT อยู่ในกลุ่ม "AI·เครื่องมือ"
กฎการเขียนมีสามข้อ ① คำย่อทั่วไปจะระบุชื่อเต็มพร้อมความหมายภาษาไทยไว้ด้วยกัน ② ศัพท์เฉพาะที่ใช้เฉพาะในหนังสือเล่มนี้อย่าง atom·Wrapper จะระบุ "(ศัพท์เฉพาะของหนังสือเล่มนี้)" ไว้ในช่องชื่อเต็ม ③ กรณีที่คำย่อหนึ่งมีสองความหมายอย่าง PK (Player Kill ในบริบทสงคราม ↔ Primary Key ในบริบทข้อมูล) เนื้อหาหลักจะระบุว่าเป็นความหมายใดเมื่อปรากฏครั้งแรก และในตารางนี้ได้ใส่ไว้ทั้งสองความหมาย
หนังสือเล่มนี้ไม่ได้กำหนดจำนวนคนในทีมเป็นตัวเลขตายตัว แต่ใช้สามระดับต่อไปนี้ เพราะแม้จะเป็นเทคนิคเดียวกัน ความลึกของการนำมาใช้ก็ต่างกันไปตามขนาดทีม
| ระดับ | เกณฑ์จำนวนคน | คำอธิบาย |
|---|---|---|
| ขนาดเล็ก | \~10 คน | ตั้งแต่นักพัฒนาคนเดียว·มือสมัครเล่นไปจนถึงทีมขนาดไม่กี่คน โดยทั่วไปการนำมาใช้ 1\~2 ขั้นก็เพียงพอ |
| ขนาดกลาง | 10\~50 คน | ช่วงที่ทีมของผู้เขียนซึ่งเป็นที่มาของกรณีการดำเนินงานในหนังสือเล่มนี้สังกัดอยู่ เป็นขนาดที่ผลสะสมของการทำให้เป็นมาตรฐาน·การทำให้สอดคล้องอัตโนมัติเริ่มชัดเจน |
| ขนาดใหญ่ | 100+ | หลายพาร์ต·หลายทีม เป็นขนาดที่โครงสร้างพื้นฐานเฉพาะและทีมดำเนินงานเฉพาะทางมีความสมเหตุสมผล |
จุดที่ระบุระดับและช่วงจำนวนคนไว้ด้วยกันในเนื้อหาหลักอย่าง "ทีมขนาดกลาง (10\~50 คน)" ยึดตามตารางนี้เป็นเกณฑ์ ในจุดที่จำนวนคนเองมีความหมายอย่างการพัฒนาคนเดียว·พัฒนาลำพัง จะใช้จำนวนที่แน่นอนตามจริงแทนระดับ
| คำย่อ | ชื่อเต็ม | ความหมาย |
|---|---|---|
| GDD | Game Design Document | เอกสารออกแบบเกม เอกสารข้อกำหนดละเอียดที่ระบุระบบ·ค่าตัวเลข·การทำงานไว้แน่นอน |
| CDD | Concept Design Document | เอกสารออกแบบคอนเซปต์ เอกสารออกแบบเริ่มต้น (ทิศทาง·คอนเซปต์) ในขั้นก่อน GDD |
| TF | TaskForce | ทีมเฉพาะกิจที่รวมตัวขึ้นชั่วคราวเพื่อเป้าหมายระยะสั้น (เช่น TF การต่อสู้) |
| DD | Design Director | ดีไซน์ไดเรกเตอร์ บทบาทผู้นำที่กำกับทิศทางการออกแบบของเกมโดยรวม |
| RnD | Research and Development | การวิจัย·พัฒนา ขั้นตอน·องค์กรที่สำรวจต้นแบบ·เทคนิคใหม่ (เช่น RnD การสร้างแบบโพรซีเดอรัล) |
| คำย่อ | ชื่อเต็ม | ความหมาย |
|---|---|---|
| NPC | Non-Player Character | ตัวละครที่ผู้เล่นไม่ได้บังคับ |
| HUD | Heads-Up Display | ข้อมูลสถานะที่ซ้อนแสดงบนหน้าจอเกม (พลังชีวิต·มินิแมป ฯลฯ) |
| DPS | Damage Per Second | ปริมาณความเสียหายต่อวินาที |
| GCD | Global Cooldown | คูลดาวน์รวม เวลารอใช้งานร่วมที่เมื่อใช้สกิลหนึ่งแล้วสกิลทั้งหมดจะถูกล็อกพร้อมกันชั่วครู่ |
| TTK | Time To Kill | เวลาที่ใช้ในการสังหารเป้าหมาย |
| PK | Player Kill | (บริบทสงคราม·PvP) การต่อสู้·สังหารระหว่างผู้เล่น |
| BT | BehaviorTree | บีเฮเวียร์ทรี โครงสร้างที่นิยามการแตกกิ่งพฤติกรรมของ AI ตัว NPC เป็นแบบทรี |
| FSM | Finite State Machine | เครื่องสถานะจำกัด โมเดลที่นิยามพฤติกรรมด้วยสถานะและการเปลี่ยนผ่าน |
| PCG | Procedural Content Generation | การสร้างเนื้อหาแบบโพรซีเดอรัล การสร้างเนื้อหาอัตโนมัติด้วยกฎ·อัลกอริทึม |
| VFX | Visual Effects | เอฟเฟกต์ภาพ |
| SFX | Sound Effects | เอฟเฟกต์เสียง |
| VA | Voice Actor | นักพากย์ |
| RPG / MMORPG | (Massively Multiplayer Online) Role-Playing Game | เกมสวมบทบาท / เกม RPG ออนไลน์หลายผู้เล่นขนาดใหญ่ |
| P2W / P2E | Pay To Win / Play To Earn | โครงสร้างที่จ่ายเงินแล้วแข็งแกร่งขึ้น / โครงสร้างที่ได้รายได้จากการเล่น |
| RMT | Real Money Trading | การซื้อขายทรัพย์สินในเกมด้วยเงินจริง |
| คำย่อ | ชื่อเต็ม | ความหมาย |
|---|---|---|
| KPI | Key Performance Indicator | ตัวชี้วัดผลงานหลัก |
| DAU | Daily Active Users | จำนวนผู้ใช้ที่ใช้งานรายวัน |
| FK | Foreign Key | คีย์นอก (foreign key) คอลัมน์ที่ชี้ไปยังคีย์หลักของชีตอื่น |
| PK | Primary Key | (บริบทข้อมูล) คีย์หลัก คอลัมน์ที่ระบุแถวอย่างไม่ซ้ำกัน |
| ROI | Return on Investment | ผลตอบแทนเทียบกับการลงทุน (การคืนทุน) |
| MECE | Mutually Exclusive, Collectively Exhaustive | แยกจากกันโดยสิ้นเชิง·ครอบคลุมทั้งหมด หลักการแบ่งหมวดที่ไม่ซ้ำกันและไม่ตกหล่น |
| STT | Speech-to-Text | การแปลงเสียงพูดเป็นข้อความ |
| VBA | Visual Basic for Applications | ภาษามาโครที่ฝังอยู่ใน Excel |
| SVN | Subversion | ระบบจัดการเวอร์ชันไฟล์ |
| telemetry | (ข้อมูลการตรวจวัด) | ล็อก·ตัวชี้วัดการเล่นที่เก็บอัตโนมัติจากบิลด์·การรันเกม (อินพุต·การต่อสู้·การเลิกเล่น ฯลฯ) อ่านว่า "เทเลเมทรี" |
| คำย่อ | ชื่อเต็ม | ความหมาย |
|---|---|---|
| AI | Artificial Intelligence | ปัญญาประดิษฐ์ |
| LLM | Large Language Model | โมเดลภาษาขนาดใหญ่ (พื้นฐานของ ChatGPT·Claude ฯลฯ) |
| JIT | Just-In-Time | วิธีที่แทรกเข้ามาเฉพาะในช่วงเวลาที่จำเป็น (ในหนังสือเล่มนี้คือการฉีดความจำที่ตรงกับอินพุตโดยอัตโนมัติ) |
| MCP | Model Context Protocol | มาตรฐานที่เชื่อมเครื่องมือ AI เข้ากับบริการภายนอก |
| API | Application Programming Interface | ข้อกำหนดการเรียกระหว่างโปรแกรม |
| UE | Unreal Engine | อันเรียลเอนจิน |
| atom | (ศัพท์เฉพาะของหนังสือเล่มนี้) | การ์ดการตัดสินใจ·กฎที่ตรึงเป็น 1 การตัดสินใจ = 1 ไฟล์ |
| Wrapper / Cascade / Junction | (ศัพท์เฉพาะของหนังสือเล่มนี้) | จุดเข้าใช้งานเครื่องมือที่ใช้บ่อย / เครื่องมือที่มัดรวมหลายการตรวจสอบไว้ในครั้งเดียว / symbolic link ที่เชื่อมไปยังตัวหลัก |
| rg | ripgrep | คำสั่งค้นหาข้อความที่รวดเร็ว (เครื่องมือ CLI ที่ทดแทน grep) ใช้ค้นหาโค้ด·เอกสารแบบครบทุกรายการ |
| ClickUp | (เครื่องมือติดตามงาน·อิชชู) | เครื่องมือทำงานร่วมกันบนคลาวด์ที่จัดการงาน·กำหนดการ JIRA·Redmine·Linear ก็อยู่ในหมวดเดียวกัน เชื่อมต่อผ่าน MCP เพื่อให้ AI ดึงข้อมูล·ปรับปรุงได้ |
| คำย่อ | ชื่อเต็ม | ความหมาย |
|---|---|---|
| UI / UX | User Interface / User Experience | ส่วนต่อประสานผู้ใช้ / ประสบการณ์ผู้ใช้ |
| WCAG | Web Content Accessibility Guidelines | แนวทางการเข้าถึงเว็บ (เกณฑ์ผ่านของความต่างความสว่าง·ขนาดเป้าหมายการแตะ ฯลฯ) |
| HIG | (Apple) Human Interface Guidelines | แนวทางส่วนต่อประสานของ Apple |
| SC | Success Criterion | หมายเลขเกณฑ์ผ่านแต่ละข้อของ WCAG (เช่น SC 1.4.3) |
| pt / dp / px | point / density-independent pixel / pixel | หน่วยขนาดหน้าจอ |
| คำย่อ | ชื่อเต็ม | ความหมาย |
|---|---|---|
| YAML | YAML Ain't Markup Language | รูปแบบการเขียนการตั้งค่า·ข้อมูลที่อ่านง่ายสำหรับมนุษย์ |
| JSON | JavaScript Object Notation | รูปแบบการเขียนสำหรับแลกเปลี่ยนข้อมูล |
| HTML / SVG | HyperText Markup Language / Scalable Vector Graphics | เอกสารเว็บ / รูปแบบกราฟิกเวกเตอร์ |
| GLB | GL Transmission Format (Binary) | รูปแบบไฟล์ไบนารีของโมเดล 3D |
คำย่อที่ความหมายแยกตามบริบทอย่าง PK เนื้อหาหลักจะระบุว่าเป็นความหมายใดเมื่อปรากฏครั้งแรก หากสับสนสามารถกลับมาที่ตารางนี้ได้
กรณีศึกษาและเครื่องมือในหนังสือเล่มนี้เกือบทั้งหมดเขียนขึ้นบนสมมติฐานของสภาพแวดล้อมเดียว นั่นคือ Claude Code ด้วยเหตุนี้ ในที่ประชุมอนุมัติหรือการตรวจทานจากภายนอกจึงมีข้อท้วงติงหนึ่งที่แทบจะไม่เคยขาดหายไป "นี่มันผูกติดกับเครื่องมือของบริษัทใดบริษัทหนึ่งไม่ใช่หรือ" ผู้รับผิดชอบฝ่ายออกแบบเกมรู้สึกหนักใจที่จะอนุมัติการตัดสินใจซึ่งพึ่งพาผู้ขาย (vendor) เพียงรายเดียว ฝ่ายที่สงสัยก็ตั้งคำถามว่าหากเปลี่ยนเครื่องมือ วิธีการทั้งหมดในหนังสือเล่มนี้จะพังทลายลงทันที ส่วนฝ่ายที่พิจารณาลิขสิทธิ์ต่างประเทศก็ถามว่า เมื่อในประเทศของตนมีเครื่องมืออื่นเป็นมาตรฐาน หนังสือเล่มนี้จะยังมีประโยชน์อยู่หรือไม่ ทั้งสามฝ่ายแสดงออกต่างกัน แต่แก่นเรื่องเหมือนกัน นั่นคือความไม่ไว้วางใจต่อ vendor lock-in หรือการถูกขังอยู่กับเครื่องมือเดียว
จุดประสงค์ของภาคผนวกนี้คือการตอบความไม่ไว้วางใจนั้น หากกล่าวบทสรุปก่อน โครงร่างของวิธีทำงานที่หนังสือเล่มนี้แนะนำนั้นเป็นกลางต่อเครื่องมือ (tool-neutral) มันไม่ได้ผูกติดกับชื่อโมเดลใดโมเดลหนึ่ง และไม่ได้ผูกติดกับเครื่องมือบรรทัดคำสั่งใดเครื่องมือหนึ่ง Claude Code เป็นเพียงภาชนะที่นำโครงร่างนั้นไปทำให้เป็นจริงได้อย่างราบรื่นที่สุด และเราสามารถย้ายโครงร่างเดียวกันนี้ไปใส่ในภาชนะอื่นได้ ภาคผนวกนี้จะ (1) แสดงเป็นตารางว่าอะไรคือโครงร่างที่ไม่ขึ้นกับเครื่องมือ (2) จับคู่ว่าแต่ละองค์ประกอบของ Claude Code เมื่อย้ายไปยังสภาพแวดล้อมอื่นแล้วจะตรงกับอะไร (3) กำหนดหลักการในการตรวจสอบข้อมูลล่าสุดบนสมมติฐานว่าโมเดลรุ่นต่าง ๆ จะเปลี่ยนไปเรื่อย ๆ และ (4) เขียนอย่างตรงไปตรงมาว่าเมื่อย้ายแล้วจะสูญเสียอะไรและรักษาอะไรไว้ได้
วิธีทำงานที่ร้อยเรียงตลอดทั้งเล่มสามารถสรุปได้เป็นเสาหลักห้าต้น เสาทั้งห้านี้ไม่มีต้นใดเป็นชื่อฟีเจอร์ของโมเดลหรือเครื่องมือบรรทัดคำสั่งใดโดยเฉพาะ แต่เป็นคำตอบต่อคำถามที่ว่า "เมื่อมนุษย์และปัญญาประดิษฐ์ทำงานร่วมกัน จะดึงผลลัพธ์ที่เชื่อถือได้ออกมาอย่างซ้ำ ๆ ได้อย่างไร" ด้วยเหตุนี้ แม้เปลี่ยนเครื่องมือ เสาเหล่านี้ก็ยังคงอยู่เหมือนเดิม
| โครงร่าง | คืออะไร | ทำไมจึงเป็นกลางต่อเครื่องมือ |
|---|---|---|
| มาตรฐาน → เทมเพลต → verification gate (ด่านตรวจสอบ) | ทำให้กฎที่ตกลงกัน (มาตรฐาน) แข็งตัวเป็นกรอบช่องว่าง (เทมเพลต) แล้ววางด่าน (gate) ที่กรองโดยอัตโนมัติว่าผลลัพธ์เป็นไปตามกฎหรือไม่ | แนวคิดเรื่องกฎ กรอบ และการตรวจสอบ สามารถแสดงออกได้ด้วยข้อความหรือสคริปต์ในเครื่องมือใดก็ได้ |
| atom = 1 การตัดสินใจ 1 ไฟล์ | เขียนการตัดสินใจหนึ่งเรื่องลงในไฟล์เล็ก ๆ หนึ่งไฟล์ เพื่อหยิบมาใช้เมื่อจำเป็น และเมื่อจะแก้ก็แก้เพียงช่องนั้นช่องเดียว | การซอยการตัดสินใจให้เล็กแล้วเก็บเป็นไฟล์ ขอเพียงมีระบบไฟล์ก็เพียงพอ |
| การฉีด JIT | คัดเฉพาะการตัดสินใจที่จำเป็นต่อบทสนทนาตอนนี้จริง ๆ มาใส่ให้โมเดลแบบทันเวลา (Just-In-Time) | เป็นหลักการที่ว่า "ใส่เฉพาะบริบทที่จำเป็น" ส่วนวิธีใส่นั้นต่างกันไปตามเครื่องมือเท่านั้น |
| ลูปการทบทวน | ย้อนมองงานที่ทำในระดับรายวัน รายสัปดาห์ รายเดือน แล้วยกระดับรูปแบบที่เกิดซ้ำให้กลายเป็นกฎของงานครั้งถัดไป | ขั้นตอนการย้อนมองและปรับปรุงนั้นขับเคลื่อนด้วยนิสัยและเอกสาร ไม่ใช่ด้วยเครื่องมือ |
| ขอบเขตการยืมเครื่องมือ | สิ่งที่หยิบมาคือโครงร่าง (อัลกอริทึม โครงสร้าง) เท่านั้น ส่วนข้อมูลโดเมนนั้นทิ้งไว้ที่เดิม (ภาคผนวก B) | การตัดสินว่าจะหยิบอะไรมาและทิ้งอะไรไว้นั้นเหมือนกันในทุกเครื่องมือ |
ช่องขวาของตารางนี้คือหัวใจสำคัญ โครงร่างทั้งห้า ในนิยามของแต่ละข้อไม่มีชื่อผลิตภัณฑ์ใดปรากฏแม้แต่ครั้งเดียว สิ่งที่ปรากฏมีเพียงแนวคิดสากลซึ่งมีอยู่ในสภาพแวดล้อมการทำงานใดก็ได้ เช่น กฎ ไฟล์ บริบท นิสัย และขอบเขต ด้วยเหตุนี้ คำถามที่ว่า "ถ้าใช้ Claude Code ไม่ได้แล้วจะทำอย่างไร" แท้จริงแล้วจึงแปรเปลี่ยนเป็นคำถามที่ตอบง่ายกว่ามาก นั่นคือ "จะนำห้าแนวคิดนี้ไปทำให้เป็นจริงในเครื่องมืออื่นได้อย่างไร" และคำตอบนั้นอยู่ในหัวข้อถัดไป
ใน Claude Code มีกลไกที่เป็นรูปธรรมซึ่งช่วยให้นำโครงร่างข้างต้นไปทำให้เป็นจริงได้อย่างสะดวก ได้แก่ hook (สคริปต์ที่ทำงานอัตโนมัติ ณ จังหวะเวลาหนึ่ง), MCP (ข้อกำหนดสำหรับเชื่อมเครื่องมือ/ข้อมูลภายนอกเข้ากับโมเดล), ไฟล์ settings (การตั้งค่าสิทธิ์/สภาพแวดล้อม), คำสั่งสแลช (slash command — คำสั่งย่อที่เรียกขั้นตอนซึ่งใช้บ่อยด้วยบรรทัดเดียว), สกิล (ชุดงานที่นำกลับมาใช้ซ้ำได้) เป็นต้น สิ่งเหล่านี้เป็นชื่อเฉพาะของ Claude Code แต่บทบาทของมันแทบทั้งหมดมีสิ่งที่ตรงกันในสภาพแวดล้อมอื่นด้วย ตารางด้านล่างคือการจับคู่นั้น
| Claude Code | ChatGPT (เว็บ/แอป) | Cursor / Copilot | LLM API ทั่วไป |
|---|---|---|---|
| hook (รันอัตโนมัติตามจังหวะ) | ขั้นตอนทำมือก่อน/หลังบทสนทนา / คำสั่งของ Custom GPT | task ก่อน/หลังงานในเอดิเตอร์ / pre-commit hook | สคริปต์ก่อน/หลังที่แทรกเข้าไปครอบการเรียก |
| MCP (ข้อกำหนดเชื่อมต่อภายนอก) | ปลั๊กอิน / action / code interpreter | extension / การเรียกเครื่องมือในตัว | function calling / API wrapper ที่สร้างเอง |
| ไฟล์ settings (สิทธิ์/สภาพแวดล้อม) | หน้าจอตั้งค่า Custom GPT / การตั้งค่าโปรเจกต์ | ไฟล์ตั้งค่า .cursor/workspace |
ออบเจกต์ตั้งค่าในโค้ด / ไฟล์ตั้งค่า .env/YAML |
| คำสั่งสแลช (ย่อขั้นตอน) | พรอมต์ที่บันทึกไว้ / Custom GPT | snippet / คำสั่งที่ผู้ใช้กำหนดเอง | ฟังก์ชันเทมเพลตพรอมต์ |
| สกิล (ชุดงานใช้ซ้ำ) | Custom GPT / ชุดพรอมต์ | ไฟล์กฎ + สคริปต์ | พรอมต์/ฟังก์ชันโค้ดที่แยกเป็นโมดูล |
| CLAUDE.md / หน่วยความจำ | คำสั่งกำหนดเอง / ฟีเจอร์หน่วยความจำ | ไฟล์กฎของโปรเจกต์ (rules) | system prompt + ที่เก็บหน่วยความจำภายนอก |
| ชุดไฟล์ atom | (ไม่ขึ้นกับเครื่องมือ) ไฟล์ Markdown | (ไม่ขึ้นกับเครื่องมือ) Markdown ในรีพอซิทอรี | (ไม่ขึ้นกับเครื่องมือ) ไฟล์/เรกคอร์ดใน DB |
เมื่อดูตารางจะเห็นชัดอย่างหนึ่ง ยิ่งไปทางขวา นั่นคือยิ่งไปทางฝั่ง LLM API ทั่วไป สิ่งที่ "เคยทำให้โดยอัตโนมัติ" ก็จะเปลี่ยนเป็น "สิ่งที่ต้องสร้างเองแล้วแทรกเข้าไป" การฉีดอัตโนมัติที่ใน Claude Code จบได้ด้วย hook บรรทัดเดียว เมื่ออยู่ใน API ทั่วไปจะกลายเป็นสคริปต์เตรียมงานที่ต้องเขียนเองก่อนการเรียก ความสะดวกของระบบอัตโนมัติลดลง แต่ตัวโครงร่างเองยังคงย้ายไปได้เหมือนเดิม กล่าวคือ การพอร์ตไม่ใช่ "การสูญเสียฟังก์ชัน" แต่เป็น "การติดตั้งความสะดวกขึ้นมาใหม่ด้วยมือของเราเอง"
flowchart LR
subgraph 도구중립["โครงร่างที่เป็นกลางต่อเครื่องมือ (ไม่เปลี่ยนแปลง)"]
S[มาตรฐาน→เทมเพลต→ตรวจสอบ]
A[atom: 1 การตัดสินใจ 1 ไฟล์]
J[การฉีด JIT]
R[ลูปการทบทวน]
end
subgraph 구현["การทำให้เป็นจริงตามสภาพแวดล้อม (สลับเปลี่ยน)"]
CC[Claude Code: hook·MCP·settings]
GPT[ChatGPT: ปลั๊กอิน·Custom GPT]
CUR[Cursor·Copilot: extension·ไฟล์กฎ]
API[LLM API: function call·สคริปต์เตรียมงาน]
end
도구중립 --> CC
도구중립 --> GPT
도구중립 --> CUR
도구중립 --> API
classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545;
classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764;
classDef human fill:#fde68a,stroke:#b45309,color:#000;
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class J code;
class CC,GPT,CUR,API ai;
class R human;
class A data;
ภาพนี้คือบทสรุปหนึ่งหน้าของทั้งภาคผนวก กล่องด้านบน (โครงร่าง) ไม่ว่าลูกศรจะชี้ไปยังสภาพแวดล้อมใด เนื้อหาก็ไม่เปลี่ยน มีเพียงกล่องด้านล่าง (การทำให้เป็นจริง) เท่านั้นที่ถูกสลับเปลี่ยนให้เข้ากับสภาพแวดล้อม เมื่อในที่ประชุมอนุมัติมีคำว่า "vendor lock-in" ปรากฏขึ้น ก็เพียงเปิดภาพหนึ่งหน้านี้แล้วตอบว่า "สิ่งที่ถูกผูกติดคือช่องล่าง ไม่ใช่ช่องบน"
เมื่อพูดถึงการพอร์ต ข้อมูลที่ล้าสมัยเร็วที่สุดคือชื่อโมเดล หากตอกชื่อโมเดลล่าสุด ณ เวลาที่เขียนหนังสือเล่มนี้ลงในเนื้อหา พอโมเดลรุ่นถัดไปออกมา ประโยคนั้นก็กลายเป็นข้อมูลที่ผิดทันที ด้วยเหตุนี้ หนังสือเล่มนี้จึงยึดหลักการหนึ่งมาตั้งแต่ต้น คือไม่อธิบายโดยพึ่งพาชื่อหรือหมายเลขรุ่นของโมเดลใดโมเดลหนึ่ง แต่อธิบายโดยพึ่งพาบทบาทที่โมเดลทำ (ฟังก์ชันอย่างการให้เหตุผล การสรุปความ การสร้างโค้ด)
| สิ่งที่เปลี่ยน (อย่าตอกตรึงไว้) | สิ่งที่ไม่เปลี่ยน (พึ่งพาได้) |
|---|---|
| ชื่อผลิตภัณฑ์/หมายเลขรุ่นของโมเดล | การแบ่งบทบาทอย่าง "โมเดลที่ให้เหตุผลได้ดี" "โมเดลที่รับบริบทยาว" |
| ตัวเลขเฉพาะของขีดจำกัด context | หลักการ JIT ที่ว่า "เพราะมีขีดจำกัด จึงใส่เฉพาะบริบทที่จำเป็นจริง ๆ" |
| ตัวเลขเฉพาะของราคา/ความเร็ว | จิตสำนึกด้านต้นทุนที่ว่า "งานที่แพงให้รันเฉพาะสิ่งที่ผ่าน gate แล้ว" |
| วิธีเปิด/ปิดฟีเจอร์ใดฟีเจอร์หนึ่ง | "บทบาทที่ฟีเจอร์นั้นทำ" และโครงร่างที่ทดแทนมันได้ |
วิธีตรวจสอบโมเดล/ฟีเจอร์ล่าสุดในงานจริงก็ใช้เพียงบรรทัดเดียวต่อเครื่องมือ ใน Claude Code สามารถตรวจสอบโมเดลที่ใช้อยู่ตอนนี้และตัวเลือกต่าง ๆ ได้ทันทีด้วยคำสั่ง /model ส่วนเครื่องมืออย่าง ChatGPT หรือ Cursor ก็แสดงข้อมูลเดียวกันในหน้าจอตั้งค่าหรือดรอปดาวน์เลือกโมเดล ดังนั้น หากประโยคใดในหนังสือเล่มนี้ดูขัดกับชื่อโมเดล ไม่ใช่ว่าประโยคนั้นผิด แต่เป็นเพราะโมเดลก้าวไปอีกหนึ่งรุ่นแล้ว ขอเพียงบทบาทยังเหมือนเดิม วิธีการก็นำมาใช้ได้ตามเดิม เมื่ออ่านหนังสือแล้วเจอชื่อโมเดลที่ไม่คุ้น โปรดอย่าสงสัยเนื้อหา แต่ขอแนะนำให้ตรวจสอบสถานะล่าสุดของเครื่องมือที่อยู่ในมือก่อนด้วยคำสั่งอย่าง /model
เมื่อย้ายเครื่องมือ ย่อมมีสิ่งที่สูญเสียไปอย่างแน่นอน หากปกปิดความจริงข้อนี้กลับยิ่งทำให้สูญเสียความไว้วางใจ ดังนั้นผู้เขียนจะเขียนอย่างตรงไปตรงมาก่อนว่าสูญเสียอะไรบ้าง อย่างไรก็ตาม สิ่งที่สูญเสียเกือบทั้งหมดอยู่ในขอบเขตของ "ความสะดวก" ส่วนสิ่งที่รักษาไว้ได้อยู่ในขอบเขตของ "โครงร่าง" กล่าวคือ สิ่งที่สูญเสียคือสิ่งที่ติดตั้งใหม่ก็เรียกกลับคืนมาได้ ส่วนสิ่งที่รักษาไว้ได้คือสิ่งที่แต่แรกก็ไม่ได้ผูกติดกับเครื่องมืออยู่แล้ว
| ประเภท | รายการ | คำอธิบาย |
|---|---|---|
| สิ่งที่สูญเสีย (ความสะดวก) | ความราบรื่นของการรันอัตโนมัติ | ระบบอัตโนมัติที่แทรกตัวเข้ามาเองอย่าง hook ต้องสร้างขึ้นเองด้วยสคริปต์ก่อน/หลัง |
| สิ่งที่สูญเสีย (ความสะดวก) | หน้าจอเดียวที่รวมทุกอย่าง | คำสั่ง เครื่องมือ และไฟล์ที่เคยรวมอยู่ในกระแสงานเดียว อาจต้องกระจายไปติดตั้งในเครื่องมือหลายตัว |
| สิ่งที่สูญเสีย (ความสะดวก) | สกิล/คำสั่งที่ใช้ได้ทันที | ต้องลงทะเบียนคำสั่งสแลช/สกิลใหม่ตามวิธีของเครื่องมือนั้น |
| สิ่งที่รักษาไว้ (โครงร่าง) | มาตรฐาน·เทมเพลต·verification gate | กฎ กรอบ และการตรวจสอบ เป็นข้อความและสคริปต์ จึงคงอยู่ได้เหมือนเดิมทุกที่ |
| สิ่งที่รักษาไว้ (โครงร่าง) | atom·JIT·ลูปการทบทวน | ขับเคลื่อนด้วยไฟล์และนิสัย จึงคงอยู่แม้เปลี่ยนเครื่องมือ |
| สิ่งที่รักษาไว้ (โครงร่าง) | ขอบเขตการยืมเครื่องมือ (ภาคผนวก B) | เกณฑ์ตัดสินว่าจะหยิบอะไรมาและทิ้งอะไรไว้ ไม่ขึ้นกับสภาพแวดล้อม |
หากย่อตารางนี้ให้เหลือประโยคเดียวก็เป็นดังนี้ สิ่งที่สูญเสียในการพอร์ตคือความสะดวกของระบบอัตโนมัติที่ใช้เวลาสักหน่อยก็ฟื้นคืนมาได้ ส่วนสิ่งที่รักษาไว้คือโครงร่างของงานที่หนังสือเล่มนี้พยายามวางไว้นอกเครื่องมือมาตั้งแต่ต้น ดังนั้น คำตอบที่ตรงไปตรงมาที่สุดต่อคำถามที่ว่า "นี่มัน vendor lock-in ไม่ใช่หรือ" คือสิ่งนี้ มีส่วนที่ถูกผูกติดอยู่จริง แต่นั่นเป็นภาชนะที่สลับเปลี่ยนได้ ส่วนเนื้อหาในภาชนะซึ่งเป็นคุณค่าที่แท้จริงนั้น แต่แรกก็ไม่ได้ผูกติดกับภาชนะใดเลย ผู้เขียนหวังว่าภาคผนวกบทนี้จะช่วยตอบคำถามนั้นแทนผู้เขียนในที่ประชุมอนุมัติ
ภาคผนวกนี้คือเวิร์กชีตแบบเติมช่องว่างที่จัดทำขึ้นเพื่อตอบคำถามของ PD และผู้บริหารสตูดิโอที่ว่า "เมื่อจะขยายระบบที่คนเดียวสร้างมา 6 เดือนไปสู่ทีมขนาดกลาง จะประเมินแรงงานในการนำไปใช้ ค่าใช้จ่ายดำเนินงาน บัญชีผู้ใช้ และความปลอดภัยของเครือข่ายภายในด้วยอะไรและอย่างไร" หากหัวข้อ 19.3 (กลยุทธ์การนำ AI ไปใช้และการโน้มน้าวผู้บริหาร) ในเนื้อหาหลักบอกว่า "อย่าปั้นแต่ง ROI" ภาคผนวกนี้ก็นำหลักการเดียวกันนั้นมาใช้กับฝั่งต้นทุนการนำไปใช้เช่นกัน กล่าวคือ ภาคผนวกนี้ไม่ได้ให้ตัวเลข ทุกช่องเป็นช่องว่าง และผู้ที่เติมช่องเหล่านั้นคือการวัดและการประมาณของทีมคุณ ส่วนช่องที่ทำเครื่องหมาย
[ต้องให้ฝ่ายบัญชียืนยัน]ไว้นั้น จะไม่มีใครเติมด้วยการประมาณจนกว่าฝ่ายบัญชีจะเติมเอง
วิธีใช้ภาคผนวกนี้เป็นดังนี้ ก่อนอื่น ใน L.1 ให้จับภาพรวมว่า TCO (Total Cost of Ownership, ต้นทุนการเป็นเจ้าของทั้งหมด) แบ่งออกเป็นรายการใดบ้างด้วยแผนภาพ จากนั้นพิมพ์เวิร์กชีตทั้งห้าใน L.2\~L.6 ออกมาในสภาพช่องว่างโดยจัดให้ตรงกับแถวขนาดทีมของตัวเอง แล้ววัดด้วยตัวเองหรือส่งต่อให้ผู้รับผิดชอบฝ่ายบัญชี/ความปลอดภัยสารสนเทศด้วยคำถามบรรทัดเดียว สุดท้ายใช้รายการตรวจสอบตนเองใน L.7 เพื่อยืนยันว่าไม่มีช่องใดตกหล่น คุณค่าของภาคผนวกนี้ไม่ได้อยู่ที่ตัวเลขที่เติมแล้ว แต่อยู่ที่ การสร้างช่องไว้ล่วงหน้าสำหรับรายการต้นทุนที่ตกหล่นได้ง่าย
กับดักที่ PD พลาดบ่อยที่สุดคือการมองต้นทุนการนำไปใช้เป็นเพียง "ค่าสมาชิก × จำนวนคน" เท่านั้น ต้นทุนการเป็นเจ้าของทั้งหมดจริง ๆ นั้นกว้างกว่านั้น มันแยกออกเป็น แรงงานในการนำไปใช้ ที่จ่ายครั้งเดียวแล้วจบ (การติดตั้ง·การทำให้เป็นมาตรฐาน·การ onboarding) และ ค่าใช้จ่ายดำเนินงาน ที่เกิดซ้ำทุกเดือน (ไลเซนส์·โทเค็น·โครงสร้างพื้นฐาน·ค่าแรงงานในการบริหารจัดการ) และเหนือสิ่งนั้นยังมีต้นทุน ความปลอดภัย·การจัดการบัญชี ที่มองไม่เห็นทับซ้อนอยู่อีก
flowchart TB
TCO["TCO การนำไปใช้ในทีม"] --> A["ครั้งเดียว: แรงงานการนำไปใช้
(ติดตั้ง·ทำมาตรฐาน·onboarding)"]
TCO --> B["เกิดซ้ำ: ค่าดำเนินงานรายเดือน
(ไลเซนส์·โทเค็น·โครงสร้างพื้นฐาน·บริหารจัดการ)"]
TCO --> C["เกิดซ้ำ: ความปลอดภัย·การจัดการบัญชี
(SSO·การตรวจสอบ·การเรียกคืนคีย์)"]
A --> A1["L.3 เวิร์กชีตแรงงานการนำไปใช้"]
B --> B1["L.5 เวิร์กชีตค่าดำเนินงานรายเดือน
[ต้องให้ฝ่ายบัญชียืนยัน]"]
C --> C1["L.4 การตรวจสอบเครือข่ายภายใน·ความปลอดภัย"]
A --> A2["L.6 เวิร์กชีตเวลา onboarding"]
B --> A3["L.2 เวิร์กชีตบัญชี·ไลเซนส์"]
classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b;
class A1,A2,A3,B1,C1 data;
ในสามแขนงนั้น ฝั่งที่ PD ประเมินต่ำเกินไปได้ง่ายคือฝั่งซ้าย (แรงงานการนำไปใช้) และฝั่งขวา (ความปลอดภัย·บัญชี) ค่าธรรมเนียมไลเซนส์จะระบุมาในใบเสนอราคา แต่ "แรงงานในการจัดมาตรฐาน·สกิลที่คนเดียวสั่งสมด้วยมือมา 6 เดือนให้อยู่ในรูปแบบที่ทีมแบ่งปันกันได้" และ "การตรวจสอบความปลอดภัยเพื่อกำหนดว่าจะอนุญาตให้เรียกใช้ LLM ภายนอกจากเครือข่ายภายในได้ถึงระดับใด" นั้นไม่มีอยู่ในใบเสนอราคา และด้วยเหตุนี้มันจึงทำให้กำหนดการและงบประมาณบานปลายเสมอ เวิร์กชีตในภาคผนวกนี้มีจุดมุ่งหมายเพื่อเปิดเผยต้นทุนที่มองไม่เห็นนั้นออกมาก่อน แม้จะเป็นในสภาพช่องว่างก็ตาม
หัวข้อ 19.3.6 ในเนื้อหาหลักบอกว่า "ค่าใช้จ่ายจะไม่ใส่ค่าสัมบูรณ์ไว้ในหนังสือ — มันคือช่องว่างที่จะรับมาจากฝ่ายบัญชีแล้วเติม" ภาคผนวกนี้คือการกางช่องว่างนั้นออกเป็นรายการ ๆ ว่าควรวางไว้ที่ใดบ้าง
นี่คือตารางแรกที่ต้องเติม บันทึกว่าใครใช้เครื่องมือใด และสิทธิ์นั้นถูกออก·เรียกคืนอย่างไร พร้อมจำนวนคน ช่องจำนวนคนให้เติมด้วยจำนวนหัวจริงของทีมคุณ ส่วนช่องราคาต่อหน่วยให้นำมาจากใบเสนอราคาหรือตารางค่าธรรมเนียมสาธารณะมาเติม หนังสือเล่มนี้ไม่ระบุราคาต่อหน่วย
| รายการ | บันทึกอะไร | ใครเป็นผู้เติม | ค่าของทีมตัวเอง |
|---|---|---|---|
| จำนวนซีตต่อเครื่องมือ | จำนวนบัญชี (ซีต) ที่จำเป็นต่อเครื่องมือแต่ละตัว | ลีด | ______ ซีต |
| การกระจายระดับสิทธิ์ | full / cap ต่องาน / คน outsource ครั้งเดียว (ภาคผนวก C.1.2) | ลีด | full __คน / ทั่วไป __คน / outsource __คน |
| ราคาต่อซีต | ค่าธรรมเนียมรายเดือนต่อซีตของแต่ละเครื่องมือ | บัญชี·จัดซื้อ | ______ /ซีต·เดือน |
| มีคีย์ส่วนกลางหรือไม่ | คีย์ API ส่วนกลางของทีม vs คีย์รายบุคคล | ความปลอดภัยสารสนเทศ | □ ส่วนกลาง □ รายบุคคล |
| ขั้นตอนการออก | เส้นทาง·เวลาที่ใช้ในการออกบัญชีให้พนักงานใหม่ | ลีด | ______ |
| ขั้นตอนการเรียกคืน | เส้นทางเรียกคืนคีย์/ซีตเมื่อลาออก·สิ้นสุด outsource | ความปลอดภัยสารสนเทศ | ______ |
กฎมีอยู่สองข้อ ข้อแรก คน outsource·คนระยะสั้น อย่าออกซีตแบบถาวร แต่ให้เปิดและเรียกคืนตามแต่ละงาน (ภาคผนวก C.1.2) ข้อสอง หากช่องขั้นตอนการเรียกคืนยังว่างอยู่ ก็อย่าเริ่มออกบัญชี เนื่องจากอุบัติเหตุที่พบบ่อยที่สุดคือบัญชีของผู้ลาออกไม่ถูกเรียกคืน ทำให้ทั้งค่าใช้จ่ายและการเปิดเผยคีย์รั่วไหลไปพร้อมกัน จึงต้องออกแบบการเรียกคืนก่อนการออกบัญชี
นี่คือตารางสำหรับประมาณแรงงานครั้งเดียวที่เพิ่มขึ้นเมื่อ "คนเดียว 6 เดือน" ขยายไปสู่ทีม โดยแบ่งตามขนาด ช่องแรงงานใช้หน่วย คน-วัน (คนหนึ่งทำงานหนึ่งวัน) ซึ่งทีมคุณวัดหรือประมาณจริงแล้วเติมเอง หนังสือเล่มนี้ไม่ได้ให้จำนวนคน-วัน — เพราะมันต่างกันมากตามระดับความชำนาญของทีมและระดับการจัดมาตรฐานที่มีอยู่เดิม
| รายการแรงงานการนำไปใช้ | 1\~3 คน | 4\~10 คน | 11\~30 คน | 31\~50 คน | ผู้วัด/ประมาณ |
|---|---|---|---|---|---|
| ติดตั้ง·ตั้งค่าสภาพแวดล้อม (เครื่องมือ·hook·สิทธิ์) | ___คน-วัน | ___คน-วัน | ___คน-วัน | ___คน-วัน | ลีด/โครงสร้างพื้นฐาน |
| ทำสินทรัพย์ของคนเดียวให้ทีมแบ่งปันได้ (จัดสกิล·มาตรฐาน·atom) | ___คน-วัน | ___คน-วัน | ___คน-วัน | ___คน-วัน | ลีด |
| กำหนดมาตรฐานทีม (การตั้งชื่อ·frontmatter·rulebook, ภาคผนวก D) | ___คน-วัน | ___คน-วัน | ___คน-วัน | ___คน-วัน | ลีด |
| สร้าง verification gate (lint·ทำ rulebook ให้อัตโนมัติ) | ___คน-วัน | ___คน-วัน | ___คน-วัน | ___คน-วัน | QA/ลีด |
| จัดทำเอกสาร onboarding (เชื่อมโยงกับ L.6) | ___คน-วัน | ___คน-วัน | ___คน-วัน | ___คน-วัน | ลีด |
| รวม (แรงงานการนำไปใช้ครั้งเดียว) | ___คน-วัน | ___คน-วัน | ___คน-วัน | ___คน-วัน | — |
ช่องที่ตกหล่นได้ง่ายเมื่อเติมตารางนี้คือแถวที่สอง สินทรัพย์ที่คนเดียวสั่งสมไว้ในหัวและในโฟลเดอร์ส่วนตัวมา 6 เดือนนั้น หากจะให้ทีมแบ่งปันก็ต้องมีใครสักคนดึงออกมาจัดระเบียบและทำเป็นเอกสาร ซึ่งกินแรงงานต่างหากออกไป หากคิดแรงงานนี้เป็น "0" กำหนดการนำไปใช้จะเลื่อนแน่นอน อีกทั้งตารางยังแสดงให้เห็นล่วงหน้าผ่านรูปร่างของช่องว่า ยิ่งขนาดใหญ่ขึ้น แรงงานด้าน การกำหนดมาตรฐาน·verification gate จะเพิ่มขึ้นชันกว่าแรงงานการติดตั้ง — เพราะเมื่อคนเพิ่มขึ้น จำนวนมาตรฐานที่ต้องตกลงร่วมกันก็เพิ่มขึ้น
หากทำตามการนำไปใช้เป็นขั้น (อนุรักษ์นิยม→ก้าวหน้า) ในหัวข้อ 19.3.1 ของเนื้อหาหลัก คุณจะกระจายแรงงานนี้โดยไม่ใช้หมดในไตรมาสเดียว แต่ค่อย ๆ ทยอยลงทุนตั้งแต่ขั้นที่ 1 (การฉีดบริบท) แบบ pilot ได้ อย่าพยายามขออนุมัติยอดรวมของตารางในครั้งเดียว แต่แยกเฉพาะแรงงานของขั้นที่ 1 ออกมาขออนุมัติก่อนจะเป็นจริงมากกว่า
นี่คือพื้นที่ที่ PD·ผู้บริหารหวาดกลัวโดยตรงที่สุด ตรวจสอบเป็นรายการว่ามีอะไรออกไปยัง LLM ภายนอกบ้าง และอนุญาตให้เรียกใช้ภายนอกจากเครือข่ายภายในได้ถึงระดับใด ตารางนี้เป็นรายการตรวจสอบที่ใช้แยกผ่าน/รอพิจารณา (เชื่อมโยงกับภาคผนวก C.6 ความปลอดภัย) และหากมีรายการใดแม้แต่รายการเดียวที่ยังไม่กำหนด ก็ให้รอการนำไปใช้ในขอบเขตนั้นไว้ก่อน
| รายการตรวจสอบ | เกณฑ์ผ่าน | ผู้รับผิดชอบ | สถานะ |
|---|---|---|---|
| ขอบเขตข้อมูลที่ส่งไป LLM ภายนอก | ข้อมูลอ่อนไหวใช้ placeholder/โฮสต์เอง (C.6) | ความปลอดภัยสารสนเทศ | □ ผ่าน □ รอพิจารณา |
| การส่งข้อมูลการชำระเงิน·ข้อมูลส่วนบุคคล | ระบุชัดว่าห้ามส่งโดยไม่มีข้อยกเว้น | ความปลอดภัยสารสนเทศ | □ ผ่าน □ รอพิจารณา |
| นโยบายการเรียกใช้ภายนอกจากเครือข่ายภายใน | กำหนดโดเมนที่อนุญาต·proxy·ระยะเก็บ log | โครงสร้างพื้นฐาน | □ ผ่าน □ รอพิจารณา |
| ความจำเป็นในการโฮสต์เอง | ตัดสินใจว่าจะประมวลผล IP หลักด้วยโมเดลที่โฮสต์เองหรือไม่ | ผู้บริหาร/ความปลอดภัยสารสนเทศ | □ ตัดสิน □ ยังไม่กำหนด |
| การรับมืออุบัติเหตุคีย์รั่ว | เปลี่ยนทันที + เส้นทางตรวจสอบประวัติการใช้งาน (C.7) | ความปลอดภัยสารสนเทศ | □ ผ่าน □ รอพิจารณา |
| audit log | บันทึก·เก็บรักษาว่าใคร·เมื่อใด·เรียกใช้อะไร | โครงสร้างพื้นฐาน | □ ผ่าน □ รอพิจารณา |
| การตรวจการรั่วไหล IP ของบริษัทออกภายนอก | ขั้นตอนตรวจล่วงหน้า เช่น grep watchlist (ภาคผนวก B.6) | ลีด | □ ผ่าน □ รอพิจารณา |
| การแยก access ของ outsource | บัญชี outsource ตัดการเข้าถึงสินทรัพย์หลัก·แยกตามงาน | ความปลอดภัยสารสนเทศ | □ ผ่าน □ รอพิจารณา |
ช่องที่ทำให้ต้นทุนต่างกันมากที่สุดในตารางนี้คือแถวที่สี่ (ความจำเป็นในการโฮสต์เอง) หากตัดสินใจว่าไม่สามารถส่ง IP หลักไปยัง LLM ภายนอกได้เด็ดขาด ค่าใช้จ่ายโครงสร้างพื้นฐานในการโฮสต์เองจะถูกทับลงไปทั้งก้อนบนค่าดำเนินงานของ L.5 ด้วยเหตุนี้การตัดสินใจนี้จึงต้องไม่ใช่ลีด แต่ต้องเป็น ผู้บริหาร·ความปลอดภัยสารสนเทศร่วมกัน เป็นผู้ตัดสิน และก่อนตัดสินใจ ก็ไม่สามารถยืนยันช่องโครงสร้างพื้นฐานของ L.5 ได้ เวิร์กชีตสองตารางเชื่อมต่อกันด้วยช่องเดียวนี้
นี่คือตารางที่แยกค่าใช้จ่ายที่เกิดซ้ำทุกเดือนออกเป็นรายการ ๆ ช่องจำนวนเงินในตารางนี้ว่างทั้งหมด และช่องที่ทำเครื่องหมาย [ต้องให้ฝ่ายบัญชียืนยัน] ไว้นั้น จะไม่มีใครเติมด้วยการประมาณจนกว่าฝ่ายบัญชีจะเติมเอง ราคาต่อหน่วยโทเค็น·ค่าสมาชิก·ค่าโครงสร้างพื้นฐานเปลี่ยนแปลงทุกเดือนตามโมเดล·ปริมาณการเรียกใช้·สัญญา ดังนั้นหนังสือเล่มนี้จึงไม่ระบุค่าสัมบูรณ์
| รายการค่าดำเนินงาน | วิธีคำนวณ | ใครเป็นผู้เติม | จำนวนเงินรายเดือน |
|---|---|---|---|
| ไลเซนส์·ค่าสมาชิก | จำนวนซีต × ราคาต่อซีต (L.2) | บัญชี | [ต้องให้ฝ่ายบัญชียืนยัน] |
| ค่าโทเค็น LLM | ปริมาณการเรียกใช้ × ราคาต่อโทเค็น, ผลรวมเพดาน (cap) ต่อเครื่องมือ | บัญชี | [ต้องให้ฝ่ายบัญชียืนยัน] |
| โครงสร้างพื้นฐาน (กรณีโฮสต์เอง) | เซิร์ฟเวอร์·GPU·สตอเรจตามการตัดสินใจของ L.4 | บัญชี·โครงสร้างพื้นฐาน | [ต้องให้ฝ่ายบัญชียืนยัน] |
| สำรองข้อมูล·ซิงค์ | repository·สตอเรจสำรองข้อมูล (ภาคผนวก C.5) | บัญชี | [ต้องให้ฝ่ายบัญชียืนยัน] |
| ค่าแรงงานบริหารจัดการดำเนินงาน | แปลงเวลาของผู้รับผิดชอบจัดการเครื่องมือ·คีย์·log เป็นค่าแรง | ลีด·บัญชี | [ต้องให้ฝ่ายบัญชียืนยัน] |
| รวมรายเดือน | ผลรวมรายการข้างต้น | บัญชี | [ต้องให้ฝ่ายบัญชียืนยัน] |
กฎของตารางนี้มีเพียงข้อเดียว ปล่อยช่องว่างให้ว่างไว้ ลองนึกถึงความล้มเหลวในหัวข้อ 19.3.2 ของเนื้อหาหลักที่ AI ปั้นแต่งช่องค่าดำเนินงานให้ดูน่าเชื่อเป็น $4,500 — ไม่ว่าจะคนหรือ AI พอเติมช่องนี้ด้วยการประมาณเมื่อใด รายงานนั้นก็จะพังตั้งแต่คำถามแรก ในทางกลับกัน กลไกที่ควบคุมค่าใช้จ่ายได้จริงไม่ใช่จำนวนเงิน แต่คือ โครงสร้างที่มีเพดานรายเดือน (cap) ต่อเครื่องมือคาดไว้และมีการรายงานการเกินโดยอัตโนมัติ (19.3.6) สิ่งที่จะแสดงต่อผู้บริหารเมื่อขออนุมัติไม่ใช่จำนวนเงินที่เติมแล้ว แต่เป็นโครงสร้างที่ว่า "มีเพดานคาดไว้และการเกินถูกรายงาน" กับรายการช่องว่างที่ฝ่ายบัญชีจะเติม
แถวที่ห้า (ค่าแรงงานบริหารจัดการดำเนินงาน) ตกหล่นบ่อยที่สุด เครื่องมือไม่ได้ติดตั้งทิ้งไว้แล้วจบ แต่กินเวลาของคนที่ต้องเรียกคืนคีย์ ดู log และปรับเพดานทุกเดือน หากปล่อยช่องนี้เป็น 0 งานนั้นก็จะซ่อนตัวกลายเป็นการทำงานล่วงเวลาที่มองไม่เห็นของลีด
นี่คือตารางสำหรับประมาณเวลาเป็นขั้น ๆ ที่สมาชิกใหม่หนึ่งคนใช้จนกว่าจะทำหน้าที่ของตนได้บนระบบ การวัดที่แม่นยำที่สุดสำหรับช่องเวลาคือ ให้ลอง onboarding จริงในทีมคุณสักครั้งหนึ่งแล้ววัด (วิธีเดียวกับสูตรวัด baseline ในหัวข้อ 19.3.7 ของเนื้อหาหลัก) ก่อนวัดให้ปล่อยเป็นช่องว่างไว้
| ขั้น onboarding | ทำอะไร | เวลาที่วัดได้ | หมายเหตุ |
|---|---|---|---|
| ติดตั้งสภาพแวดล้อม | จนถึงตั้งค่าเครื่องมือ·hook·บัญชี | ___ชั่วโมง | เชื่อมโยงกับขั้นตอนการออกของ L.2 |
| เรียนรู้มาตรฐาน | ทำความเข้าใจการตั้งชื่อ·frontmatter·rulebook (ภาคผนวก D) | ___ชั่วโมง | ย่นได้หากมีเอกสาร |
| งานแรก (อนุรักษ์นิยม) | ผ่านผลงานแรก·การตรวจสอบด้วยการฉีดบริบท | ___ชั่วโมง | ขั้นที่ 1 ของ 19.3.1 |
| ปรับตัวกับ verification gate | ทำงานให้สอดคล้องกับ gate ของ lint·rulebook | ___ชั่วโมง | — |
| บรรลุการทำงานอิสระ | ทำงาน·ตัดสินการรับเอาได้โดยไม่ต้องมีคนกำกับ | ___วัน | เกณฑ์เสร็จสิ้นการ onboarding |
เมื่อเติมตารางนี้แล้ว จะเผยให้เห็นว่าทำไมช่อง "จัดทำเอกสาร onboarding" ของแรงงานการนำไปใช้ (L.3) จึงสำคัญ ยิ่งเอกสาร onboarding ถูกจัดระเบียบไว้ดีเท่าใด เวลาในแถวที่สอง·สามก็ยิ่งสั้นลง และยิ่งสมาชิกใหม่เพิ่มขึ้น การประหยัดนั้นก็ยิ่งสะสม กล่าวคือ การจัดทำเอกสาร onboarding เป็นแรงงานครั้งเดียว แต่การเก็บเกี่ยวเกิดซ้ำตามจำนวนสมาชิก สิ่งที่หัวข้อ 19.3.3 ของเนื้อหาหลักบอกว่า "การฉีดอัตโนมัติแบบ JIT 221 ครั้ง — สมาชิกใหม่ก็ทำงานบนกฎเดียวกัน" ในตารางนี้ปรากฏออกมาเป็นการย่นเวลาในแถวที่สาม
แถวสุดท้าย (บรรลุการทำงานอิสระ) คือเกณฑ์เสร็จสิ้นการ onboarding ที่แท้จริง หากเข้าใจผิดว่าการติดตั้งสภาพแวดล้อมเสร็จคือการ onboarding เสร็จ ต้นทุนการกำกับดูแลก็จะสะสมที่ลีดต่อไปเรื่อย ๆ เกณฑ์ต้องเป็น "ตัดสินการรับเอาได้โดยไม่ต้องมีคนกำกับ"
สุดท้ายนี้ คือรายการที่ต้องผ่านด้วยตนเองก่อนนำเวิร์กชีตเหล่านี้ไปเสนอผู้บริหาร ด้วยจิตวิญญาณเดียวกับภาคผนวก B.6 (ตรวจสอบก่อนยืมมาใช้) หากมีรายการใดแม้แต่รายการเดียวที่ยังว่างอยู่ ก็ให้เลื่อนการขออนุมัติออกไปและเติมช่องนั้นก่อน
| รายการตรวจสอบ | เกณฑ์ผ่าน |
|---|---|
| มีการกำหนดขั้นตอนการเรียกคืนบัญชีแล้วหรือไม่ | ช่องขั้นตอนการเรียกคืนของ L.2 ไม่ว่าง |
| การตรวจสอบความปลอดภัยผ่าน/ตัดสินครบทุกข้อแล้วหรือไม่ | □รอพิจารณา·□ยังไม่กำหนด ใน L.4 เป็น 0 รายการ |
| ช่องว่างค่าดำเนินงานถูกส่งต่อไปฝ่ายบัญชีแล้วหรือไม่ | [ต้องให้ฝ่ายบัญชียืนยัน] ของ L.5 ถูกส่งไปเป็นคำถามแล้ว |
| แบ่งแรงงานการนำไปใช้เป็นขั้น ๆ แล้วหรือไม่ | ขออนุมัติตั้งแต่แรงงานขั้นที่ 1 ไม่ใช่ยอดรวมของ L.3 |
| เกณฑ์เสร็จสิ้น onboarding คือ "การทำงานอิสระ" หรือไม่ | ตัดสินความเสร็จสิ้นด้วยแถวสุดท้ายของ L.6 |
| ไม่ได้เขียนค่าประมาณเป็นการยืนยันแบบเด็ดขาดใช่หรือไม่ | ทุกช่องประมาณกำกับ "ค่าประมาณ·จำนวนตัวอย่าง" ไว้ |
อย่าอ่านตารางนี้เป็นการผ่านห้าช่อง แต่ขอให้อ่านเป็นกลไกล็อกหกอัน การขยายระบบของคนเดียวไปสู่ทีมเป็นเรื่องที่ทำได้แน่นอน แต่ต้นทุนของการขยายนั้นไม่ใช่ค่าธรรมเนียมไลเซนส์ ภาพรวมทั้งหมดจะปรากฏก็ต่อเมื่อเติมช่องว่างทั้งหกของตารางนี้อย่างซื่อสัตย์แล้วเท่านั้น และอย่าสั่งให้ AI เติมช่องใด ๆ — AI จะเติมช่องว่างด้วยตัวเลขที่ดูน่าเชื่อเหมือนในหัวข้อ 19.3.2 ของเนื้อหาหลัก ที่ของ AI คือการรับค่าที่คุณวัดมาแล้วเรียบเรียงเป็นประโยคสไลด์ขออนุมัติเท่านั้น
ในหนังสือเล่มนี้มีอยู่ห้าจุด — §8.2.7 (เศรษฐกิจ)·§5.4 (เสียง)·§6.3 (เพอร์โซนา)·§7.3 (แพตเทิร์น)·§13.3 (ข้อมูล) — ที่สำนวนอย่าง "บีบอัดเป็นเวกเตอร์มิติ", "embedding", "อยู่ใกล้กันในปริภูมิเวกเตอร์" ปรากฏขึ้นในฐานะ ป้ายบอกทิศทาง แม้ไม่มีพื้นฐานด้าน machine learning ก็จับแนวคิดนี้ได้ครบถ้วนด้วยสัญชาตญาณของการออกแบบเกมเพียงอย่างเดียว เพียงปูสัญชาตญาณนี้ไว้สักครั้งเดียว ทั้งห้าจุดนั้นก็จะอ่านได้ด้วยภาพเดียวกันทั้งหมด
แต่มีสิ่งหนึ่งที่ต้องตอกย้ำก่อน สัญชาตญาณของแนวคิดนั้นง่าย แต่เงื่อนไขของการนำไปใช้นั้นหนัก แนวคิดนี้ไม่ใช่จุดเริ่มต้น แต่เป็น การประยุกต์ที่ปลายสุดไกลที่สุด ซึ่งจะตั้งอยู่ได้ก็ต่อเมื่อมีรากฐานที่หนังสือเล่มนี้สั่งสมมาตลอดข้างต้นเท่านั้น นั่นคือ ด่านตรวจสอบของการประยุกต์แบบอนุรักษ์นิยม โครงสร้างพื้นฐานด้านข้อมูล·telemetry ตัวตรวจสอบเฉพาะสาขาอย่าง voice_lint·การตรวจความสอดคล้อง และการรวม Layer หากเริ่มวาดพิกัดก่อนโดยไม่มีรากฐานเหล่านั้น แผนที่ที่ได้ก็จะกลายเป็นภาพลวง ที่บีบอัดแม้กระทั่งความคลาดเคลื่อนที่ไม่ตรงกับเกมเข้าไปอย่างเรียบร้อย ดังที่เห็นใน M.4 เหตุผลที่แท้จริงว่าทำไมทั้งห้าจุดจึง "ยังเร็วเกินไป" อยู่ตรงนี้ — ไม่ใช่เพราะแนวคิดยาก แต่เพราะรากฐานที่จะมารองรับต้องมาก่อน ภาคผนวกนี้เป็นแผนที่ที่ทีมซึ่งมีรากฐานพร้อมแล้วกางออกดู เมื่อต้องการประเมิน "ก้าวต่อไปอีกหนึ่งก้าว" ไม่ใช่หนังสือเริ่มต้นสำหรับการก้าวเท้าก้าวแรก
ไม่ว่าจะเป็นวัตถุใดก็ตาม การเปลี่ยนคุณลักษณะของมันให้เป็นรายการตัวเลขแล้ววางลงเป็นจุดหนึ่งบน "แผนที่" คือ embedding (เอ็มเบดดิง) รายการตัวเลขนั้นเองคือ เวกเตอร์มิติ ข้อตกลงมีเพียงข้อเดียว — ทำให้ วัตถุที่ยิ่งคล้ายกัน ยิ่งกลายเป็นจุดที่อยู่ใกล้กันบนแผนที่
ตัวอย่างเช่น ถ้าวาง NPC เป็นพิกัดของคุณลักษณะอย่าง (ความเป็นทางการของน้ำเสียง ปริมาณการแสดงอารมณ์ ระดับความยากของคำศัพท์ …) NPC ที่มีน้ำเสียงคล้ายกันก็จะมารวมกันอยู่ใกล้กันบนแผนที่ ถ้าเป็นสูตรอาหาร ก็วางองค์ประกอบของวัตถุดิบเป็นพิกัด อาหารที่คล้ายกันก็จะมารวมอยู่ใกล้กัน — สิ่งที่ Epicure ซึ่งยกเป็นเบาะแสไว้ใน §8.2.7 ทำ ก็คือสิ่งนี้พอดี
ดูคล้ายแต่ตรงข้าม — อย่าสับสนกับ AHP AHP (Analytic Hierarchy Process, Saaty) ที่ใช้ในการตัดสินใจหลายเกณฑ์ก็ดูคล้าย ตรงที่เปลี่ยนการตัดสินเชิงคุณภาพให้เป็นเวกเตอร์เช่นกัน — เพราะมันดึงน้ำหนักลำดับความสำคัญ (eigenvector หลัก) ออกมาด้วยการเปรียบเทียบแบบคู่ที่นำเกณฑ์มาเทียบกันทีละสองอัน แต่ทิศทางนั้นตรงกันข้าม AHP เป็นการตัดสินใจแบบบนลงล่าง ที่ คนนิยามเกณฑ์·ลำดับชั้นไว้ล่วงหน้า แล้วให้น้ำหนักภายในกรอบนั้น ส่วน embedding ตรงนี้เป็นการค้นพบแบบล่างขึ้นบน ที่ คลัสเตอร์ปรากฏขึ้นเองจากข้อมูลโดยไม่มีการนิยามของคน ข้อจำกัดที่ §13.3 พยายามทะลุ — "เซกเมนต์ที่คนนิยามไว้ล่วงหน้า" — ก็คือจุดตั้งต้นของ AHP พอดี ทั้งสองไม่ได้อยู่ตำแหน่งเดียวกัน แต่อยู่คนละขั้ว
แผนที่นั้นทรงพลัง แต่ไม่ได้มาฟรี
เพราะฉะนั้นหนังสือเล่มนี้จึงวางเวกเตอร์มิติไว้ในฐานะ ป้ายบอกทิศทาง ไม่ใช่ใบสั่งยา — เป็นพื้นที่ที่ทีมซึ่งปูการตรวจสอบ (telemetry·simulation) ไว้อย่างมั่นคงแล้วจะหันมาพิจารณาในอีกหลายปีข้างหน้า เบาะแสที่เป็นรูปธรรมในแต่ละสาขากระจายไว้ที่ §8.2.7 (เศรษฐกิจ)·§5.4 (เสียง)·§6.3 (เพอร์โซนา)·§7.3 (แพตเทิร์น)·§13.3 (ข้อมูล) และทั้งหมดอ่านได้บนแผนที่ใบเดียวของภาคผนวกนี้
ภาคผนวกนี้จัดทำขึ้นสำหรับผู้ที่ต้องการนำหนังสือเล่มนี้มาใช้เป็นตำราเรียนของรายวิชาหนึ่งภาคการศึกษา ไม่ว่าจะเป็นอาจารย์ในมหาวิทยาลัย วิทยาลัย หรืออะคาเดมี ผู้รับผิดชอบการอบรมภายในองค์กร หรือผู้นำกลุ่มศึกษา การแบ่งหนังสือเล่มเดียวที่หนาเกือบ 1,000 หน้าออกเป็นหน่วยภาคการศึกษานั้นยากกว่าที่คิด จะใส่ส่วนใดในสัปดาห์ที่เท่าไร จะเปลี่ยน «ลองทำดู» ในเนื้อหาให้เป็นงานมอบหมายอย่างไร และจะให้คะแนนผลงานที่ส่งมาด้วยเกณฑ์อะไร เมื่อสามเรื่องนี้ติดขัด แม้หนังสือดีก็ยากที่จะถูกเลือกมาเป็นตำราได้ ภาคผนวกนี้จะทำให้สามเรื่องดังกล่าวกลายเป็นเครื่องมือที่คุณคัดลอกไปใช้ได้ทันที
วิธีใช้ภาคผนวกนี้มีดังนี้ ก่อนอื่นให้อ่านตารางความก้าวหน้า 15 สัปดาห์ใน N.1 โดยปรับให้เข้ากับปฏิทินการศึกษาของคุณ (รูปแบบ 16 สัปดาห์·ภาคฤดูร้อนได้แยกไว้ต่างหากใน N.2) แล้วประเมินระดับของผู้เรียนด้วยป้ายระดับความยากและตารางความรู้พื้นฐานใน N.3 จากนั้นคัดลอกเกณฑ์การให้คะแนน (rubric) ใน N.4 แล้วเปลี่ยนเฉพาะรายการให้เข้ากับงานมอบหมายของคุณ ทุกตารางถูกออกแบบมาให้พิมพ์ออกมาแล้วแปะลงในเอกสารแผนการสอน (syllabus) ได้ทันที
มีเรื่องหนึ่งที่ต้องบอกไว้ล่วงหน้า ทุกบทของหนังสือเล่มนี้ลงท้ายด้วย «ลองทำดู» เป้าหมายของเนื้อหาไม่ใช่บทที่อ่านจบแล้วปิด แต่เป็นบทที่ทำให้คุณลงมือทำในวันนี้ และในการสอน «ลองทำดู» นั้นเองที่กลายเป็นวัตถุดิบลำดับแรกของงานมอบหมาย ด้วยเหตุนี้ตารางความก้าวหน้าในภาคผนวกนี้จึงระบุไว้ด้วยว่าจะแปลง «ลองทำดู» ในเนื้อหาให้เป็นงานมอบหมายรายสัปดาห์ได้อย่างไร
นี่คือตารางความก้าวหน้ามาตรฐานที่จัดทำขึ้นโดยอิงภาคการศึกษาแบบ 15 สัปดาห์ที่พบบ่อยที่สุด (สัปดาห์ละ 1 ครั้ง ครั้งละ 3 ชั่วโมง) เราจะไม่ครอบคลุมทั้ง 24 ส่วนของหนังสือเล่มนี้ภายในภาคการศึกษาเดียว เพราะหากฝืนยัดเข้าไปทั้งหมด ก็จะไม่มีอะไรเหลือติดมือเลย เราจึงเลือกโครงสร้างที่ วางรากฐาน (ส่วนที่ 1·2) ให้แน่น เจาะลึก 5–6 หัวข้อตัวแทนในกลุ่มสาขา แล้วเลือกเฉพาะแก่นจากกระบวนการ·การดำเนินงานมาปิดท้าย ส่วนที่ไม่ได้กล่าวถึงและเหลือไว้จะระบุเป็น «อ่านเพิ่มเติม (ส่วนขยาย)» เพื่อชี้ทางให้ผู้เรียนที่สนใจไปเปิดอ่านด้วยตนเอง
วัตถุประสงค์การเรียนรู้ทั้งหมดเขียนด้วยคำกริยาในแง่ "ผู้เรียนจะทำอะไรได้บ้าง" ไม่ใช่ "รู้" แต่เป็น "สร้าง·ตรวจสอบ·เลือก" เนื่องจากหนังสือทั้งเล่มย้ำประโยคเดียวว่า "AI เสนอตัวเลือกและมนุษย์เป็นผู้กรอง" คำกริยาของวัตถุประสงค์จึงเป็นไปตามการแบ่งงานนั้นด้วย
| สัปดาห์ | ส่วน·บทที่ครอบคลุม | วัตถุประสงค์การเรียนรู้ (ทำได้หลังเรียน) | «ลองทำดู» ที่แปลงเป็นงานมอบหมาย |
|---|---|---|---|
| 1 | 1.0 ก่อนเริ่มต้น + ส่วนที่ 1 (บทนำ) | อธิบายเทอร์มินัล·บัญชี·โครงสร้างค่าบริการ และติดตั้งเครื่องมือ AI ลงบน PC ของตนเพื่อเปิดเซสชันแรก | «ลองทำดู» การติดตั้ง 1.0 — ส่งภาพหน้าจอการติดตั้ง + พรอมต์·ผลลัพธ์แรก |
| 2 | ส่วนที่ 2 (สถาปัตยกรรมข้อมูล) | แปลงเอกสารให้เป็นข้อมูลด้วย YAML frontmatter และออกแบบกฎการตั้งชื่อ·โฟลเดอร์ | «ลองทำดู» frontmatter 2.1 — ใส่ frontmatter ให้เอกสารของตน 3 ฉบับ |
| 3 | ส่วนที่ 3 (การออกแบบระบบ) | นิยาม $schema ของชีตข้อมูลก่อนตามหลักการ schema-first | «ลองทำดู» schema 3.2 — เขียนข้อกำหนดของมินิชีต 1 ชนิด |
| 4 | ส่วนที่ 10 (QA·ความสอดคล้อง) | ทำตามเพื่อสร้างเครื่องมือที่ตรวจสอบความสอดคล้องของ FK ใน 30 ชีตด้วยโค้ด | «ลองทำดู» การตรวจสอบความสอดคล้อง 10.1 — งานหลักที่ให้คะแนนด้วย rubric ใน N.4 |
| 5 | ส่วนที่ 4 (การต่อสู้) + ส่วนที่ 8 (การปรับสมดุล) | แยกค่าตัวเลขการต่อสู้ออกเป็น Layer และวางสูตรการปรับสมดุลเชิงกำหนดไว้เป็น rulebook | «ลองทำดู» สูตรการปรับสมดุล 8.1 — สูตรดาเมจ 1 ชนิด + การจำลอง |
| 6 | ส่วนที่ 5 (เนื้อเรื่อง) | สร้าง voice_profile ของบทพูด NPC และจับการหลุดโทนด้วย voice_lint | «ลองทำดู» voice_profile 5.2 — โปรไฟล์เสียงของตัวละคร 1 ตัว |
| 7 | ส่วนที่ 6 (เนื้อหา) + ส่วนที่ 7 (เลเวล) | แยกแกนสองด้านของการสร้างแบบโพรซีเดอรัล (กฎ·AI) และผลิต·ตรวจสอบเนื้อหาตัวเลือกจำนวนมาก | «ลองทำดู» ตัวสร้าง 6.2 — สร้างเนื้อหาตัวเลือก 10 รายการ + บันทึกการตรวจสอบ |
| 8 | การตรวจสอบกลางภาค·การนำเสนอ | บูรณาการงานสัปดาห์ที่ 1–7 แล้วสาธิตเป็นมินิโปรเจกต์ของตน | นำเสนองานกลางภาค (สาธิตผลงานรวมจากสัปดาห์ที่ 3–6) |
| 9 | ส่วนที่ 9 (UX·UI) + ส่วนที่ 14 (มือถือ) | นำ HUD เข้า lint เพื่อจับการหลุดสายตา·คอนทราสต์ไม่ถึงเกณฑ์ และบีบอัด HUD ของ PC ลงสู่มือถือ | «ลองทำดู» HUD lint 9.1 — รายงาน lint ของหน้าจอ 1 ชนิด |
| 10 | ส่วนที่ 16 (ผู้สื่อสาร) + ส่วนที่ 17 (บันทึกการประชุม) | จัดให้เป็นทางการเฉพาะการตัดสินใจในพื้นที่ทำงานที่แยกตัว และจัดโครงสร้างบันทึกการประชุม | «ลองทำดู» บันทึกการประชุม 17.x — จัดโครงสร้างเทปบันทึกการประชุมจริง 1 รายการ |
| 11 | ส่วนที่ 18 (การตัดสินใจ) + ส่วนที่ 19 (หัวหน้าทีม) | บันทึกการตัดสินใจเป็นการ์ดที่ติดตามได้ และแปลงวิสัยทัศน์ให้เป็นเกณฑ์การให้คะแนนของการตัดสินใจ | «ลองทำดู» การติดตามการตัดสินใจ 18.1 — เขียนการ์ดการตัดสินใจ 3 ใบ |
| 12 | ส่วนที่ 20 (หน่วยความจำการทำงานร่วมกัน) + ส่วนที่ 21 (การปรับปรุงตนเอง) | ดำเนินบริบทการทำงานร่วมกันด้วยหน่วยความจำ และหมุนการทบทวนให้เป็นลูปการปรับปรุงตนเอง | «ลองทำดู» การทบทวน บทที่ 21 — การทบทวนรายสัปดาห์ 1 รายการ + กฎการสกัด 1 ข้อ |
| 13 | ส่วนที่ 22 (ธรรมาภิบาล) | ตรวจสอบเส้นแบ่งของพรอมต์·การหลอน·ต้นทุน·กฎหมาย·จริยธรรม แล้วตั้งกฎ | «ลองทำดู» พรอมต์ 22.1 — ใบสั่งงาน 1 ใบ + ขั้นตอนการตรวจสอบการหลอน |
| 14 | ส่วนที่ 23 (การพัฒนาส่วนบุคคล) + ส่วนที่ 24 (การดำเนินงานเชิงลึก) | ย้ายเครื่องมือไปยังฉบับย่อสำหรับคนเดียว และตรวจสอบความสอดคล้อง·ลิงก์·stale ด้วยโค้ด | «ลองทำดู» การตรวจสอบ 24.1 — สคริปต์ตรวจสอบโปรเจกต์ของตน 1 ชนิด |
| 15 | การนำเสนอ·การประเมินโปรเจกต์ปลายภาค | ออกแบบ·สาธิต·ตรวจสอบเวิร์กโฟลว์ของตนเอง 1 ชุดที่ร้อยเรียงตลอดทั้งภาคการศึกษา | นำเสนองานปลายภาค (ประเมินด้วย rubric ฉบับขยายใน N.4) |
อ่านเพิ่มเติม (ไม่รวมในการสอน·แนะนำให้เรียนรู้ด้วยตนเอง): ส่วนที่ 11 (ตัวละคร·สัตว์เลี้ยง·ยานพาหนะ), ส่วนที่ 12 (อาร์ตไดเรกชัน), ส่วนที่ 13 (ข้อมูล·KPI), ส่วนที่ 15 (Live Ops) สี่ส่วนนี้มีความเฉพาะทางสูง จึงเหลือไว้ให้ผู้เรียนที่สนใจไปเปิดอ่านตามสาขาของตน หากใช้ภาคผนวก F (ดัชนีกรณีศึกษา) เป็นเข็มทิศ ก็จะค้นหาแบบย้อนกลับจากกรณีที่ใกล้เคียงกับสภาพแวดล้อมของตนเข้าไปได้
หากมองภาพการไหลของความก้าวหน้าโดยรวมจะเป็นดังนี้ เป็นโครงสร้างที่มีสองยอดเขา (กลางภาค·ปลายภาค) คือ รากฐาน → เจาะลึกสาขา → บูรณาการกลางภาค → กระบวนการ·การดำเนินงาน → บูรณาการปลายภาค
flowchart LR
subgraph A["รากฐาน (สัปดาห์ที่ 1~3)"]
W1["สัปดาห์ที่ 1
ติดตั้ง·บทนำ"] --> W2["สัปดาห์ที่ 2
สถาปัตยกรรมข้อมูล"] --> W3["สัปดาห์ที่ 3
ระบบ·schema"]
end
subgraph B["เจาะลึกสาขา (สัปดาห์ที่ 4~7)"]
W4["สัปดาห์ที่ 4
QA·ความสอดคล้อง"] --> W5["สัปดาห์ที่ 5
การต่อสู้·การปรับสมดุล"] --> W6["สัปดาห์ที่ 6
เนื้อเรื่อง"] --> W7["สัปดาห์ที่ 7
เนื้อหา·เลเวล"]
end
M1{{"สัปดาห์ที่ 8
นำเสนอกลางภาค"}}
subgraph C["กระบวนการ·การดำเนินงาน (สัปดาห์ที่ 9~14)"]
W9["สัปดาห์ที่ 9
UX·มือถือ"] --> W10["สัปดาห์ที่ 10
การสื่อสาร"] --> W11["สัปดาห์ที่ 11
การตัดสินใจ·หัวหน้าทีม"] --> W12["สัปดาห์ที่ 12
การทำงานร่วมกัน·การทบทวน"] --> W13["สัปดาห์ที่ 13
ธรรมาภิบาล"] --> W14["สัปดาห์ที่ 14
การพัฒนาส่วนบุคคล·การตรวจสอบ"]
end
M2{{"สัปดาห์ที่ 15
นำเสนอปลายภาค"}}
A --> B --> M1 --> C --> M2
classDef human fill:#fde68a,stroke:#b45309,color:#000;
class M1,M2 human;
แต่ละสถาบันมีความยาวภาคการศึกษาต่างกัน นอกเหนือจากมาตรฐาน 15 สัปดาห์ เราขอเสนอแนวทางปรับสำหรับสองรูปแบบที่พบบ่อยที่สุด งานหลัก (การตรวจสอบความสอดคล้องในสัปดาห์ที่ 4) และยอดเขาการนำเสนอทั้งสอง ขอแนะนำให้คงไว้ในทุกรูปแบบ เพราะนี่คือจุดที่หลักการความซื่อสัตย์ของหนังสือเล่มนี้ ("แสดงโครงสร้าง ไม่ใช่ผลลัพธ์") ปรากฏชัดที่สุด
| รูปแบบภาคการศึกษา | วิธีปรับ |
|---|---|
| 16 สัปดาห์ | มาตรฐาน 15 สัปดาห์ + เพิ่ม สัปดาห์เสริม·ประเมินซ้ำ ในสัปดาห์ที่ 16 เปิดโอกาสส่งงานปลายภาคใหม่ หรือให้ผู้เรียนโหวตเลือก 1 ใน 4 ส่วนของ «อ่านเพิ่มเติม» มาบรรยายพิเศษ |
| ภาคฤดูร้อน 8 สัปดาห์ (สัปดาห์ละ 2 ครั้งหรือแบบเข้มข้น) | สัปดาห์ที่ 1 (ติดตั้ง·บทนำ) → สัปดาห์ที่ 2 (ข้อมูล·schema) → สัปดาห์ที่ 3 (ความสอดคล้อง งานหลัก) → สัปดาห์ที่ 4 (การต่อสู้·การปรับสมดุล·เนื้อเรื่องรวมกัน) → สัปดาห์ที่ 5 นำเสนอกลางภาค → สัปดาห์ที่ 6 (การประชุม·การตัดสินใจ·การทำงานร่วมกัน) → สัปดาห์ที่ 7 (ธรรมาภิบาล·การตรวจสอบ) → สัปดาห์ที่ 8 นำเสนอปลายภาค ลดสาขาลงเหลือตัวแทน 3 หัวข้อ และดูดซับ «ลองทำดู» เป็นการฝึกปฏิบัติในชั้นเรียน |
| การเรียนแบบพลิกกลับ (ห้องเรียนกลับด้าน) | ให้การอ่านเนื้อหาเป็นงานมอบหมายล่วงหน้า และจัดสรรเวลาเรียนทั้งหมดให้กับการฝึก «ลองทำดู» และการประเมินกันเองด้วย rubric หนังสือเล่มนี้บรรจุโค้ดที่รันได้ตรงตามนั้นโดยไม่ต้องพึ่งสิ่งภายนอก จึงเหมาะกับการดำเนินงานที่เน้นการฝึกปฏิบัติ |
แม้อยู่ในหนังสือเล่มเดียวกัน แต่ละบทก็ต้องการความรู้พื้นฐานต่างกัน บางบทแม้เป็นนักศึกษาปี 1 ที่เพิ่งใช้เทอร์มินัลครั้งแรกก็ตามได้ บางบทต้องมีแนวคิดเรื่องคีย์ฐานข้อมูลหรือสถิติพื้นฐานจึงจะย่อยได้ครบถ้วน เราจัดเป็นป้ายสามระดับเพื่อให้คุณใช้ปรับความก้าวหน้าตามระดับของผู้เรียน หรือแนะนำวิชาที่ควรเรียนมาก่อน
ความหมายของป้ายมีดังนี้
| ป้าย | ระดับ | ความหมาย |
|---|---|---|
| 🟢 เริ่มต้น | เริ่มต้น | ผู้ไม่ใช่สายตรงหรือนักศึกษาปี 1 ก็ตามได้ โค้ดเพียงระดับคัดลอก·รันก็เพียงพอ |
| 🟡 ภาคปฏิบัติ | ภาคปฏิบัติ | ต้องอ่านโค้ดและแก้ให้เข้ากับข้อมูลของตนได้ แนะนำให้เข้าใจบริบทงานออกแบบเกมภาคปฏิบัติ |
| 🔴 เชิงลึก | เชิงลึก | ขั้นออกแบบ·ขยายอัลกอริทึม·โครงสร้าง หากไม่มีความรู้พื้นฐานจะย่อยยาก |
ป้ายและความรู้พื้นฐานของส่วนหลักในแต่ละสัปดาห์มีดังนี้ "ความรู้พื้นฐาน" คือพื้นฐานที่ควรมีไว้ล่วงหน้าเพื่อตามสัปดาห์นั้นได้อย่างราบรื่น ไม่ได้หมายความว่าหากไม่มีแล้วจะเรียนไม่ได้
| สัปดาห์ | ส่วนหลัก | ป้าย | ความรู้พื้นฐาน |
|---|---|---|---|
| 1 | บทนำ 1.0·ส่วนที่ 1 | 🟢 เริ่มต้น | ไม่มี (ตั้งสมมติฐานว่าใช้เทอร์มินัลครั้งแรก) |
| 2 | ส่วนที่ 2 สถาปัตยกรรมข้อมูล | 🟢 เริ่มต้น | การใช้โปรแกรมแก้ไขข้อความ |
| 3 | ส่วนที่ 3 ระบบ·schema | 🟡 ภาคปฏิบัติ | พื้นฐานตาราง/สเปรดชีต แนวคิดชนิดข้อมูล |
| 4 | ส่วนที่ 10 การตรวจสอบความสอดคล้อง | 🔴 เชิงลึก | พื้นฐานไพทอน (ฟังก์ชัน·ลูป), แนวคิดคีย์เชิงสัมพันธ์ (FK) |
| 5 | ส่วนที่ 4·8 การต่อสู้·การปรับสมดุล | 🟡 ภาคปฏิบัติ | สูตรการคำนวณพื้นฐาน, การคำนวณในตาราง (ฟังก์ชันเอ็กเซล) |
| 6 | ส่วนที่ 5 เนื้อเรื่อง | 🟢 เริ่มต้น | ทักษะการเขียนตัวละคร·บท |
| 7 | ส่วนที่ 6·7 เนื้อหา·เลเวล | 🟡 ภาคปฏิบัติ | แนวคิดการสร้างแบบโพรซีเดอรัล (แนะนำ), ทักษะพิกัด·กริด |
| 9 | ส่วนที่ 9·14 UX·มือถือ | 🟡 ภาคปฏิบัติ | แนวคิดเลย์เอาต์หน้าจอ·ความละเอียด |
| 10 | ส่วนที่ 16·17 การสื่อสาร | 🟢 เริ่มต้น | ไม่มี (มีประสบการณ์ทำงานร่วมกันจะได้เปรียบ) |
| 11 | ส่วนที่ 18·19 การตัดสินใจ·หัวหน้าทีม | 🟡 ภาคปฏิบัติ | ประสบการณ์ทำงานเป็นทีม·บริหารโปรเจกต์ (แนะนำ) |
| 12 | ส่วนที่ 20·21 การทำงานร่วมกัน·การทบทวน | 🟡 ภาคปฏิบัติ | เรียนสถาปัตยกรรมข้อมูลในสัปดาห์ที่ 2 มาแล้ว |
| 13 | ส่วนที่ 22 ธรรมาภิบาล | 🟡 ภาคปฏิบัติ | สถิติพื้นฐาน (ค่าเฉลี่ย·การกระจาย, บริบทการตรวจจับการหลอน), พื้นฐานลิขสิทธิ์ |
| 14 | ส่วนที่ 23·24 ส่วนบุคคล·การดำเนินงาน | 🔴 เชิงลึก | พื้นฐานไพทอน, พื้นฐาน git, เรียนความสอดคล้องในสัปดาห์ที่ 4 มาแล้ว |
คำแนะนำวิชาเรียนมาก่อนหนึ่งบรรทัด (สำหรับแผนการสอน): "แนะนำให้มีพื้นฐานไพทอนเบื้องต้นหรือพื้นฐานการเขียนโปรแกรมเทียบเท่า แต่ไม่บังคับ บทเชิงลึกในสัปดาห์ที่ 4·14 ตั้งสมมติฐานว่ามีระดับฟังก์ชัน·ลูปของไพทอน ส่วนผู้ที่ยังไม่ได้เรียนจะดำเนินงานมอบหมายแบบแยกแทร็กเริ่มต้นในสัปดาห์ที่ 1–3 ให้ตามได้อย่างเพียงพอ"
เคล็ดลับการดำเนินงานตามองค์ประกอบของผู้เรียนมีดังนี้
หากไม่มี rubric ผลงาน «ลองทำดู» ที่ส่งมามักถูกให้คะแนนด้วยการแบ่งแบบทวิภาคเพียง "รันได้/รันไม่ได้" เท่านั้น แล้วสิ่งที่หนังสือเล่มนี้ให้ความสำคัญสูงสุด — กระบวนการตรวจสอบและปฏิเสธผลลัพธ์ AI — ก็จะหายไปจากการประเมิน ด้วยเหตุนี้เราจึงนำงานหลักสัปดาห์ที่ 4 («ลองทำดู» atom การตรวจสอบความสอดคล้อง 10.1) มาเป็นตัวอย่าง แล้ววาง rubric ที่ให้คะแนนทั้งผลลัพธ์และกระบวนการ คุณสามารถนำไปใช้กับงานมอบหมายของสัปดาห์อื่นได้ทันทีโดยเปลี่ยนเพียงชื่อรายการ
คำนิยามงาน: สำหรับชีตข้อมูลหลายชนิดที่ตนสร้าง (หรือได้รับมา) ให้สร้างเครื่องมือที่ตรวจสอบความสอดคล้องของคีย์ภายนอก (FK) ระหว่างชีตร่วมกับ AI และสาธิตว่าเครื่องมือจับข้อผิดพลาดที่จงใจฝังไว้ได้หรือไม่ ผลงานที่ส่งประกอบด้วย ① โค้ดเครื่องมือ ② ผลการรันตรวจสอบ (รายงานผ่าน/ไม่ผ่าน) ③ บันทึกพรอมต์ฉบับเต็มที่ส่งให้ AI พร้อมผลลัพธ์ที่ปฏิเสธ·แก้ไขในนั้น
rubric ประกอบด้วย 4 รายการ·แต่ละรายการ 25 คะแนน (รวม 100 คะแนน) แก่นสำคัญคือ นอกเหนือจาก "เครื่องมือรันได้" (รายการ 2) แล้ว ยังประเมิน วิธีจัดการกับ AI (รายการ 3·4) ด้วยน้ำหนักครึ่งหนึ่ง
| # | รายการประเมิน | คะแนน | ยังไม่ถึงเกณฑ์ (0\~12) | พอใช้ (13\~19) | ดีเยี่ยม (20\~25) |
|---|---|---|---|---|---|
| 1 | การนิยามกฎความสอดคล้อง — ตรวจความสัมพันธ์ FK ใดเพราะอะไร ชัดเจนหรือไม่ | 25 | ความสัมพันธ์ที่ตรวจสอบไม่ชัดเจนหรือกำหนดตามอำเภอใจ | ระบุความสัมพันธ์ FK หลักได้ แต่อธิบายเหตุผลไม่เพียงพอ | นิยามความสัมพันธ์ระหว่างชีตพร้อมแผนภาพ·เหตุผล และอธิบายลำดับความสำคัญของการตรวจสอบ |
| 2 | การทำงานของเครื่องมือ·การตรวจจับข้อผิดพลาด — จับข้อผิดพลาดที่ฝังไว้ได้จริงหรือไม่ | 25 | รันไม่ได้หรือพลาดข้อผิดพลาดที่ชัดเจน | จับข้อผิดพลาดได้ส่วนใหญ่ แต่มีบางส่วนตกหล่น·แจ้งผิดพลาด | จับข้อผิดพลาดที่ฝังไว้ได้ทั้งหมด และรายงานออกมาให้มนุษย์อ่านได้โดยไม่มีการแจ้งผิดพลาด |
| 3 | ความโปร่งใสของกระบวนการใช้ AI — บันทึกพรอมต์ฉบับเต็มและผลลัพธ์ไว้ในลักษณะที่ทำซ้ำได้หรือไม่ | 25 | ไม่มีบันทึกพรอมต์·ผลลัพธ์ หรือแนบมาแต่ผลลัพธ์ | มีพรอมต์ แต่ขาดกระบวนการปฏิเสธ·แก้ไข | บันทึกพรอมต์ฉบับเต็มที่ส่ง ผลลัพธ์ดิบ และกระบวนการปฏิเสธ·สั่งใหม่ไว้ตามลำดับเวลา |
| 4 | การตัดสินใจตรวจสอบ·ปฏิเสธ — ปฏิเสธ·แก้ไขอะไรในผลลัพธ์ AI เพราะอะไร | 25 | ยอมรับผลลัพธ์ตามนั้น (ไม่มีร่องรอยการตรวจสอบ) | แก้ไขบางส่วน แต่เหตุผลในการตัดสินใจอ่อน | ชี้ข้อผิดพลาด·การหลอน·การออกแบบเกินจำเป็น แล้วปฏิเสธ และอธิบายเหตุผลในการตัดสินใจด้วยภาษาของตน |
บันทึกการดำเนินงานการให้คะแนน: รายการ 3·4 (รวม 50 คะแนน) คือกระดูกสันหลังของ rubric นี้ แม้เครื่องมือจะรันได้สมบูรณ์ (รายการ 2 เต็ม) แต่หากยอมรับผลลัพธ์ AI โดยไม่วิพากษ์ (รายการ 4 ยังไม่ถึงเกณฑ์) ก็ถือว่ายังไม่บรรลุวัตถุประสงค์การเรียนรู้ของงานนี้ — "มนุษย์ยึดตำแหน่งผู้ตรวจสอบไว้" ในทางกลับกัน แม้เครื่องมือจะไม่สมบูรณ์ในบางส่วน หากกระบวนการปฏิเสธ·สั่งใหม่หนักแน่นก็สามารถได้คะแนนสูงได้ หลักการของหนังสือเล่มนี้ที่ว่าประเมินโครงสร้าง (วิธีจัดการ) ไม่ใช่ผลลัพธ์ (ผลที่รันได้) ถูกนำมาใช้กับการให้คะแนนตรงตามนั้นด้วย
สำหรับงานปลายภาค ขอแนะนำ rubric ฉบับขยายเป็น 5 รายการ·แต่ละรายการ 20 คะแนน โดยเพิ่ม ⑤ การทำให้เวิร์กโฟลว์เป็นทั่วไป (อธิบายการย้ายไปสู่สาขาของตน) อีก 1 รายการเข้ากับ 4 รายการข้างต้น สามารถย้ายเครื่องมือที่เรียนมาตลอดภาคการศึกษาไปยังโปรเจกต์ของตนได้หรือไม่ — นั่นคือคำถามที่หนังสือเล่มนี้ถามในตอนท้าย และการประเมินสุดท้ายของการสอนก็เป็นคำถามเดียวกันก็เพียงพอแล้ว
สุดท้าย หากย่อภาคผนวกนี้ลงเหลือหนึ่งหน้าจะเป็นดังนี้
ตารางความก้าวหน้านี้เป็นจุดเริ่มต้น ไม่ใช่คำตอบที่ถูกต้อง โปรดย้ายสัปดาห์และเปลี่ยนงานมอบหมายให้เข้ากับระดับของผู้เรียนและปฏิทินการศึกษาของคุณ การให้เครื่องมือ AI อ่านหนังสือเล่มนี้ทั้งเล่มแล้วขอว่า "ช่วยจัดตารางความก้าวหน้านี้ใหม่ให้เข้ากับตารางการสอน 16 สัปดาห์และระดับผู้เรียนของฉัน" ก็เป็นทางที่เปิดอยู่เช่นกัน — สมกับเป็นวิธีใช้หนังสือเล่มนี้ที่เร็วที่สุด
AI และ Claude Code สำหรับงานออกแบบเกมในทางปฏิบัติ
ไม่มีตัวเลขที่กุขึ้นแม้แต่ตัวเดียว — คู่มือภาคสนาม 6 เดือนของเวิร์กโฟลว์ AI: พรอมต์ โค้ด และการตรวจสอบ ครบถ้วน
นี่คือ ฉบับภาษาไทย ของต้นฉบับภาษาเกาหลี แปลจากภาษาเกาหลีด้วยเวิร์กโฟลว์ AI ของหนังสือเล่มนี้เอง แล้วผ่านการตรวจสอบโดยผู้เขียน ฉบับเว็บนี้ไม่มี ISBN แยกต่างหาก
| ชื่อต้นฉบับ | 게임 기획 실무에서 바로 쓰는 AI·클로드 코드 활용법 |
| ผู้เขียน | Minsoo Lee (อี มินซู / 이민수) |
| สำนักพิมพ์ต้นฉบับ | BOOKK Co., Ltd. (ฉบับพิมพ์ภาษาเกาหลี) |
| ตีพิมพ์ครั้งแรก | 11 มิถุนายน 2026 |
| ISBN ต้นฉบับ | 979-11-12-21479-9 (ฉบับพิมพ์ภาษาเกาหลี) |
| ต้นฉบับภาษาเกาหลี | https://github.com/eremes81/game-design-ai-practice |
ⓒ Minsoo Lee 2026
หนังสือเล่มนี้เผยแพร่ภายใต้สัญญาอนุญาต CC BY-NC-SA 4.0 — อนุญาตให้แบ่งปันและแปลโดยไม่แสวงหากำไรได้ โดยต้องให้เครดิตผู้เขียนต้นฉบับ (Minsoo Lee · 이민수) และระบุแหล่งที่มา ส่วนการใช้เพื่อการค้าต้องได้รับอนุญาตจากผู้เขียนเป็นการเฉพาะ