ข้าม​ไป​ยัง​เนื้อหา

Ubiquitous Language: file context แรก​ที่​ควร​ป้อน​ให้ coding agent — ก่อน spec

เกี่ยว​กับ​บทความ​นี้

บทความ​นี้​เป็น บทความ​ต้นฉบับ​ของ​แก่น (Kaen) ไม่ใช่​บท​แปล — เป็นการ​เรียบเรียง​ความคิด​เห็น​เชิง​ปฏิบัติ​เกี่ยว​กับ​การ​ทำงาน​กับ AI coding agent บน​ฐาน​ของ​แนวคิด Domain-Driven Design ที่​ไซต์​นี้​สอน​ไว้​แล้ว

บทความ​ต่อยอด​จาก​เนื้อหา​ภายใน​ไซต์​โดยตรง ได้แก่

ศัพท์​เทคนิค​คง​ไว้​เป็น​ภาษา​อังกฤษ​และ​อธิบาย​เป็น​ไทย​เมื่อ​พบ​ครั้ง​แรก

ก่อน​จะ​บอก agent ว่า “ให้​ทำ​อะไร” คุณ​ต้อง​บอก​ก่อน​ว่า “เรา​เรียก​สิ่ง​ต่าง ๆ ว่า​อะไร”

ลอง​นึก​ภาพ​นี้ คุณ​เปิด coding agent ขึ้น​มา เขียน spec ที่​ละเอียด​พอสมควร​ว่า​อยาก​ได้​ระบบ​คิด​ค่า​บริการ​แบบ​ไหน แล้ว​ปล่อย​ให้​มัน​ลงมือ ห้า​นาที​ต่อ​มา code ก็​ออก​มา ใช้งาน​ได้ ผ่าน test ที่​มัน​เขียน​เอง

แต่​พอ​เปิด file ดู​จริง ๆ คุณ​เจอ class User, class AccountManager, method ชื่อ Process() และ​ตัวแปร data เต็ม​ไป​หมด — ทั้ง​ที่​ใน​ทีม​ของ​คุณ​ไม่มี​ใคร​พูด​คำ​ว่า “user” เลย พวก​เขา​พูด​ว่า “ผู้​กู้” บ้าง “ผู้​สมัคร” บ้าง และ​สอง​คำ​นั้น ไม่ใช่​สิ่ง​เดียวกัน

code ที่​ได้​จึง “ถูก” ใน​แง่​ที่​มัน​รัน​ได้ แต่ “ผิด” ใน​แง่​ที่​มัน​พูด​คนละ​ภาษา​กับ​ทีม และ​ภายใน​ไม่​กี่​สัปดาห์ ช่องว่าง​ระหว่าง​คำ​ใน​หัว​คน​กับ​คำ​ใน code ก็​กว้าง​จน​ไม่มี​ใคร​แน่ใจ​ว่า AccountManager.Process() มัน​ทำ​อะไร​กัน​แน่

บทความ​นี้​เสนอ​วิธี​แก้​ที่​เรียบ​ง่าย​อย่าง​น่า​ตกใจ นั่น​คือ glossary (บัญชี​คำ​ศัพท์​ที่​นิยาม​ความหมาย​และ​บริบท​ของ​แต่ละ​คำ) ของ Ubiquitous Language ควร​เป็น context file แรก​ที่​คุณ​ป้อน​ให้ agent — ป้อน​ก่อน spec เสมอ ไม่ใช่​ป้อน​ทีหลัง ไม่ใช่​ป้อน​เมื่อ​จำเป็น แต่​ป้อน​เป็น​สิ่ง​แรก

Ubiquitous Language — เรียก​ย่อ​ว่า UL หรือ “ภาษา​กลาง” — คือ​ภาษา​ที่ “เข้มงวด” ชุด​หนึ่ง​ซึ่ง​ทุก​คนใน​ทีม​ใช้​ร่วม​กัน ทั้ง​ผู้เชี่ยวชาญ​ธุรกิจ​และ​นัก​พัฒนา โดย​ผูก​กับ domain model โดยตรง

อย่าง​ที่​บทเรียน Ubiquitous Language ย้ำ​ไว้​ว่า ปัญหา​คลาสสิก​คือ​ผู้เชี่ยวชาญ​ธุรกิจ​พูด​ภาษา​หนึ่ง นัก​พัฒนา​พูด​อีก​ภาษา​หนึ่ง แล้ว​ต้อง “แปล” ไปมา และ​ทุก​การ​แปล​คือ​โอกาส​ที่​ความ​เข้าใจ​จะ​คลาดเคลื่อน

DDD แก้​ด้วย​การ​สร้าง​ภาษา​เดียว​ที่​ใช้​ทั้ง​ใน​การ​คุย​และ​ใน code — ชื่อ class, method, ตัวแปร ควร​สะท้อน​คำ​ใน​ภาษา​นี้

หัวใจ​อีก​ข้อ​คือ ความหมาย​ของ​คำ​ขึ้น​อยู่​กับ บริบท คำ​ว่า “meter” (มิเตอร์) ใน​บริษัท​ไฟฟ้า​มี​ความหมาย​ต่าง​กัน​เล็กน้อย​ใน​แต่ละ​แผนก เรา​ไม่​บังคับ​ให้​ทุก​คนใช้​นิยาม​เดียว แต่​ยอมรับ​ว่า​ความหมาย​ผูก​กับ Bounded Context — ขอบเขต​เชิง​ภาษา​ที่​ห่อ UL หนึ่ง​ชุด​ไว้ ภายใน​ขอบเขต​นั้น​คำ​ทุก​คำ​มี​ความหมาย​เดียว ไม่​กำกวม

นี่​คือ​รายละเอียด​ที่ agent มอง​ข้าม​เสมอ และ​เป็น​เหตุผล​ว่า​ทำไม glossary ที่​ดี​ต้อง​ระบุ bounded context กำกับ​ทุก​คำ

ทีม​ไทย​มี​ช่องว่าง​สอง​ชั้น — agent ทำให้​เป็น​สาม​ชั้น

หัวข้อ​ที่​มีชื่อ​ว่า “ทีม​ไทย​มี​ช่องว่าง​สอง​ชั้น — agent ทำให้​เป็น​สาม​ชั้น”

ทีม​ซอฟต์แวร์​ไทย​จำนวน​มาก​ที่​ผู้​เขียน​เคย​ร่วม​งาน​ด้วย​อยู่​ใน​สถานการณ์​เฉพาะ​ตัว เราคุย​งาน​เป็น​ภาษา​ไทย แต่​เขียน code เป็น​ภาษา​อังกฤษ ใน​ห้อง​ประชุม​มี​คน​พูด​ว่า “ยอด​ค้าง​ชำระ” “การ​เบิก​ใช้​วงเงิน” “งวด​ผ่อน” แต่​พอ​เปิด editor คำ​เหล่า​นั้น​ต้อง​กลาย​เป็น OutstandingBalance, Drawdown, Installment

การ​แปล​ไทย-อังกฤษ​นี้​คือ​ช่องว่าง​ชั้น​ที่​หนึ่ง​ซึ่ง​มี​อยู่​แล้ว​ใน​ทุก​ทีม

เมื่อ​ผู้เชี่ยวชาญ​ธุรกิจ​กับ​นัก​พัฒนา​ยัง​ตีความ​คำ​ไทย​ไม่​ตรง​กัน — เช่น “ลูกค้า” ใน​มุม​ของ​ฝ่าย​ขาย (sales) หมาย​ถึง​คน​ที่​กำลัง​ขอ​สินเชื่อ แต่​ใน​มุม​ของ​นัก​พัฒนา (dev) หมาย​ถึง​คน​ที่​ได้​เงิน​กู้​ไป​แล้ว — นั่น​คือ​ช่องว่าง​ชั้น​ที่​สอง สอง​ชั้น​นี้​คือ​สิ่ง​ที่ DDD ตั้งใจ​ปิด​ด้วย UL ตั้งแต่​ต้น

ทีนี้​พอ​เรา​เพิ่ม coding agent เข้า​มา มัน​พก “สมมติฐาน​เดิม” (priors) ของ​มัน​เอง​มา​ด้วย — สถิติ​จาก code นับ​ล้าน​บรรทัด​บน​อินเทอร์เน็ต​ที่​บอกว่า​อะไร​ก็ตาม​ที่​มีชื่อ​และ​ยอด​เงิน​ควร​เรียก​ว่า User หรือ Customer หรือ Account agent ไม่รู้จัก domain ของ​คุณ มัน​เดา​จาก​ค่า​เฉลี่ย​ของ​ทั้ง​โลก

นี่​คือ​ช่องว่าง​ชั้น​ที่​สาม และ​มัน​ร้ายกว่า​สอง​ชั้น​แรก เพราะ agent เดา​ได้ “เร็ว” และ “มั่นใจ” — มัน​จะ​ถม codebase ด้วย​คำ​กลาง ๆ เร็ว​กว่า​ที่​มนุษย์​คน​ไหน​จะ​ถม​ได้

flowchart TB
  subgraph GAP["Without a glossary"]
    direction LR
    t1["Thai discussion"] --> e1["English code"] --> a1["Agent guesses generic names"]
  end
  subgraph FIX["With a UL glossary"]
    direction LR
    g["UL glossary as context"] --> t2["Thai discussion"]
    g --> e2["English code"]
    g --> a2["Agent uses team terms"]
  end

ภาพ 1 — ด้าน​บน: เมื่อ​ไม่มี glossary เส้นทาง​จาก​บทสนทนา​ไทย สู่ code อังกฤษ สู่​ชื่อ​ที่ agent เดา​เอง คือ​ช่องว่าง​สาม​ชั้น​ที่​ต่อ​กัน​เป็น​ทอด ๆ ความ​เพี้ยน​สะสม​ที​ละ​ชั้น · ด้าน​ล่าง: เมื่อ​มี glossary เป็น context กลาง ทั้ง​บทสนทนา code และ agent ต่าง​อ้างอิง​คำ​ชุด​เดียวกัน ช่องว่าง​จึง​ยุบ​ลง glossary ไม่​ได้​แค่​ช่วย agent แต่​ทำ​หน้าที่​เป็น “จุด​ยึด” ให้​ทั้ง​สาม​ฝ่าย

glossary ที่​ป้อน​ให้ agent ได้​จริง​ไม่ใช่​แค่ “คำ​แปล​ศัพท์” แต่ละ​รายการ​ต้อง​มี​อย่าง​น้อย​สาม​ส่วน คือ คำ (term), นิยาม (definition) และ bounded context ที่​คำ​นั้น​ถือ​ครอง​ความหมาย​อยู่ ส่วน​ที่​สาม​นี่แหละ​ที่​มัก​ถูก​ลืม แต่​มัน​คือ​สิ่ง​ที่​แยก glossary ระดับ DDD ออก​จาก​รายการ​ศัพท์​ธรรมดา

ยก​ตัวอย่าง domain สินเชื่อ (lending) ที่​เรา​จะ​ใช้​ตลอด​บทความ ระบบ​แบ่ง​เป็น2 bounded context ได้แก่ Origination (การ​พิจารณา​และ​อนุมัติ​สินเชื่อ) และ Servicing (การ​ดูแล​บัญชี​สินเชื่อ​หลัง​ปล่อย​กู้) glossary ย่อ ๆ อาจ​เป็น​ดังนี้

TermไทยนิยามBounded Context
Applicantผู้​สมัครผู้​ยื่น​ขอ​สินเชื่อ​ที่​ยัง​ไม่​ได้​รับ​อนุมัติ​หรือ​ยัง​ไม่​ได้​เบิก​เงินOrigination
Applicationใบ​คำขอคำขอ​สินเชื่อ​หนึ่ง​ใบ พร้อม​เอกสาร​และ​ผล​การ​พิจารณาOrigination
CreditLimitวงเงินจำนวน​เงิน​สูงสุด​ที่​อนุมัติ​ให้​ผู้​สมัคร​ราย​หนึ่งOrigination
Borrowerผู้​กู้ผู้​สมัคร​ที่​ได้​รับ​การ​เบิก​จ่าย​เงิน​กู้​แล้วServicing
LoanAccountบัญชี​สินเชื่อบัญชี​สินเชื่อ​ที่​เปิด​ใช้งาน​ของ​ผู้​กู้​หนึ่ง​รายServicing
Drawdownการ​เบิก​ใช้​วงเงินการ​เบิก​เงิน​จาก​วงเงิน​แต่ละ​ครั้งServicing
OutstandingBalanceยอด​เงิน​ต้น​คง​ค้างเงิน​ต้น​ที่​ยัง​ไม่​ได้​ชำระ ณ เวลา​ปัจจุบันServicing
Delinquencyการ​ผิดนัด​ชำระสถานะ​ที่​งวด​ผ่อน​เลย​กำหนด​ชำระ​แล้วServicing

สังเกต​ว่า column Bounded Context ไม่​ได้​ตกแต่ง​ให้​สวย มัน​คือ กติกา​การ​ตั้ง​ชื่อ ตรง ๆ คำ​ว่า CreditLimit เป็น​เรื่อง​ของ Origination เท่านั้น ส่วน OutstandingBalance อยู่​ฝั่ง Servicing การ​เอา​สอง​คำ​นี้​มา​ปน​กัน​ใน class เดียว​คือ​สัญญาณ​ว่า​เรา​กำลัง​ทำ bounded context รั่ว ซึ่ง agent จะ​ทำ​โดย​ไม่รู้ตัว​ถ้า​เรา​ไม่​บอก

จุด​ที่ glossary แสดง​พลัง​ชัด​ที่สุด​คือ​คำ​หรือ​แนวคิด​เดียว​ที่​มี​ความหมาย​ต่าง​กัน​ไป​ตาม​บริบท (polyseme) ใน domain สินเชื่อ “คน ๆ เดียวกัน” ที่​เดิน​เข้า​มา​ขอ​กู้ มีชื่อ​ไม่​เหมือน​กัน​ใน​สอง​บริบท ก่อน​เงิน​ออก​เขา​คือ Applicant (ผู้​สมัคร) หลัง​เงิน​ออก​แล้ว​เขา​กลาย​เป็น Borrower (ผู้​กู้)

มัน​ไม่ใช่​แค่​เปลี่ยน​คำ​เล่น ๆ — Applicant มี Application และ CreditLimit ส่วน Borrower มี LoanAccount และ OutstandingBalance 2 entity นี้​มี​กฎ (invariant) คนละ​ชุด มี life cycle คนละ​แบบ

flowchart TB
  person["Same real person"]
  subgraph ORIG["Origination context"]
    applicant["Applicant"]
  end
  subgraph SERV["Servicing context"]
    borrower["Borrower"]
  end
  person --> applicant
  person --> borrower
  applicant -->|after disbursement| borrower

ภาพ 2 — บุคคล​จริง​คน​เดียวกัน​ปรากฏ​เป็น​คนละ​แนวคิด​ใน​คนละ bounded context ใน​บริบท Origination เขา​คือ Applicant ใน​บริบท Servicing เขา​คือ Borrower เส้น​เชื่อม “after disbursement” คือ​เหตุการณ์​เปลี่ยน​สถานะ​เมื่อ​เงิน​กู้​ถูก​เบิก​จ่าย นี่​คือ​สิ่ง​ที่ agent จะ​ยุบ​ให้​เหลือ User ตัว​เดียว​เสมอ​ถ้า​ไม่มี glossary กำกับ — และ​เมื่อ​ยุบ​แล้ว กฎ​ธุรกิจ​ของ​สอง​สถานะ​ก็​จะ​ปน​กัน​จน​แยก​ไม่​ออก

agent ที่​ไม่มี context จะ​สร้าง class User ก้อน​เดียว​ที่​มี​ทั้ง CreditLimit และ OutstandingBalance อยู่​ด้วย​กัน ซึ่ง​เท่ากับ​ลบ​เส้น​แบ่ง bounded context ทิ้ง​ตั้งแต่​บรรทัด​แรก glossary ที่​ระบุ​บริบท​กำกับ​จึง​ไม่ใช่​เอกสาร​ประดับ แต่​คือ​การ​บังคับ​ให้ agent เคารพ​ขอบเขต​ที่​ทีม​ออกแบบ​ไว้

ประเด็น​สำคัญ​ของ​บทความ​นี้​ไม่ใช่​แค่ “ควร​มี glossary” แต่​คือ ลำดับ spec ทุก​ฉบับ​ตั้ง​อยู่​บน​ข้อ​สมมติ​แฝง​ว่า​ผู้​อ่าน​รู้​อยู่​แล้ว​ว่า​คำ​ต่าง ๆ หมาย​ถึง​อะไร

เวลา​เรา​เขียน spec ว่า “เมื่อ​ผู้​กู้​เบิก​ใช้​วงเงิน​เกิน​ยอด​คง​เหลือ ให้​ปฏิเสธ​รายการ” ประโยค​นี้​จะ​มี​ความหมาย​ก็​ต่อ​เมื่อ​ผู้​อ่าน​รู้​แล้ว​ว่า “ผู้​กู้” “เบิก​ใช้​วงเงิน” และ “ยอด​คง​เหลือ” คือ​อะไร

spec คือ​ชั้น​ของ “ทำ​อะไร” ส่วน glossary คือ​ชั้น​ของ “เรา​เรียก​สิ่ง​ต่าง ๆ ว่า​อะไร” — และ​ชั้น​หลัง​อยู่ ใต้ ชั้น​แรก​เสมอ

ถ้า​คุณ​ป้อน spec โดย​ไม่​ป้อน glossary ก่อน agent จะ​เติม​ช่องว่าง​ของ​คำ​ศัพท์​ด้วย​สมมติฐาน​เดิม​ของ​มัน​เอง แล้ว​ค่อย​ทำ​ตาม spec บน​คำ​ที่​มัน​เดา​ขึ้น​มา ผล​คือ code ทำ​สิ่ง​ที่ spec บอก แต่​ใช้​คำ​ที่​ทีม​ไม่​ยอมรับ ใน​ทาง​กลับ​กัน ถ้า glossary มา​ก่อน​เสมอ ตัว spec เอง​ก็​จะ​ถูก​ตีความ​บน​คำ​ศัพท์​ชุด​ที่​ถูกต้อง​ตั้งแต่​ต้น

flowchart LR
  gloss["UL Glossary"] --> spec["Spec"]
  spec --> code["Generated Code"]
  gloss -.->|shared vocabulary| code

ภาพ 3 — glossary อยู่​ต้นน้ำ​ของ spec ไม่ใช่​ข้าง ๆ spec ถูก​เขียน​และ​ตีความ​บน​คำ​ศัพท์​ที่ glossary นิยาม​ไว้ และ​คำ​ศัพท์​ชุด​เดียวกัน​นั้น​ก็​ไหล​ตรง​ลง​ไป​ถึง code ที่ generate ออก​มา (เส้น​ประ) เมื่อ​คำ​ต้นทาง​ถูก​ล็อก​ไว้ ความ​กำกวม​จะ​ไม่​ถูก​เลื่อน​ไป​สะสม​ที่​ปลาย​น้ำ

นี่​คล้าย​กับ​หลักการ spec-anchored ใน​บทความ Spec-Driven Development แต่​ไม่ใช่​สิ่ง​เดียวกัน​เสียที​เดียว — spec-anchored ใน​บทความ​นั้น​หมาย​ถึง​การ “คง spec ไว้​ใช้​ดูแล/พัฒนา​ต่อ” คือ​เก็บ spec ไว้​เป็น​ชิ้น​งาน​ที่​มี​ชีวิต​ข้าม​รอบ​การ​ดูแล​รักษา ส่วน​สิ่ง​ที่​บทความ​นี้​พูด​ถึง​คือ​ความ​มั่นคง​ของ​คำ​ศัพท์​ที่ spec อิง​อยู่

ทั้ง​สอง​อย่าง​เสริม​กัน spec จะ​เป็น​จุด​ยึด​ที่​มั่นคง​ได้​ก็​ต่อ​เมื่อ​มัน​ตั้ง​อยู่​บน​คำ​ศัพท์​ที่​มั่นคง​กว่า glossary จึง​เป็น “จุด​ยึด​ของ​จุด​ยึด” ที่​อยู่​ใต้ spec ลง​ไป​อีก​ชั้น

ทีนี้​มา​ถึง​ส่วน​ที่​ปฏิบัติ​ได้​จริง glossary ใน​หัว​หรือ​ใน Confluence ไม่​ช่วย​อะไร agent เลย มัน​ต้อง​อยู่​ใน​ที่​ที่ agent อ่าน​ทุก​ครั้ง นั่น​คือ file context ประจำ project เช่น CLAUDE.md หรือ​ส่วน​หนึ่ง​ของ system prompt

วิธี​ที่​ได้​ผล​คือ​แปลง​ตาราง glossary ให้​เป็น block ที่​มี กติกา​การ​ตั้ง​ชื่อ และ คำ​ต้อง​ห้าม ชัดเจน ไม่ใช่​แค่​รายการ​นิยาม

## Ubiquitous Language (อ่านก่อนเขียน code เสมอ)
กติกา: ตั้งชื่อ class, method, ตัวแปร ตามคำในหมวดนี้เท่านั้น
ห้ามใช้คำกลาง ๆ อย่าง User, Data, Manager, Info, Handler แทนคำใน domain
ทุกคำผูกกับ bounded context หนึ่งเดียว ห้ามข้ามบริบท
### Bounded Context: Origination (การพิจารณาสินเชื่อ)
- Applicant — ผู้ยื่นขอสินเชื่อที่ยังไม่ได้เบิกเงิน
- Application — ใบคำขอสินเชื่อหนึ่งใบ
- CreditLimit — วงเงินสูงสุดที่อนุมัติให้
### Bounded Context: Servicing (การดูแลบัญชีสินเชื่อ)
- Borrower — Applicant ที่ได้รับการเบิกจ่ายเงินกู้แล้ว
- LoanAccount — บัญชีสินเชื่อที่เปิดใช้งาน
- Drawdown — การเบิกใช้วงเงินแต่ละครั้ง
- OutstandingBalance — ยอดเงินต้นคงค้าง ณ ปัจจุบัน
### คำต้องห้าม (ถ้าจะใช้ ให้หยุดแล้วถามก่อน)
- User, Customer → ต้องระบุว่าเป็น Applicant หรือ Borrower
- Account → ต้องระบุว่าเป็น Application หรือ LoanAccount
- Manager, Service ลอย ๆ → ตั้งชื่อตามพฤติกรรมใน domain แทน

เคล็ด​ลับ​สาม​ข้อ​ที่​ทำให้ block นี้​ได้​ผล​กับ agent จริง

  • หนึ่ง — จัด​กลุ่ม​คำใต้​หัวข้อ bounded context เสมอ เพื่อ​ให้ agent เห็น​เส้น​แบ่ง​บริบท​เป็น​โครงสร้าง ไม่ใช่​แค่​รายการ​ยาว ๆ
  • สอง — มี​หมวด “คำ​ต้อง​ห้าม” ที่​บอก​ตรง ๆ ว่า​เจอ​คำ​กลาง ๆ แล้ว​ให้​ทำ​อย่างไร เพราะ​การ​ห้าม​เชิง​รุก​ได้​ผลกว่าการ​บอก​แต่​สิ่ง​ที่​ควร​ทำ
  • สาม — สั่ง​ให้ “หยุด​แล้ว​ถาม​ก่อน” เมื่อ​ไม่มี​คำ​ที่​ตรง แทนที่​จะ​ปล่อย​ให้ agent คิด​คำ​ใหม่​ขึ้น​เอง เพราะ​คำ​ใหม่​ที่​ไม่​ผ่าน​ทีม​คือ​จุด​เริ่ม​ของ​การ​เพี้ยน​รอบ​ใหม่

ต่อ​ไป​นี้​คือ​ความ​ต่าง​ที่​จับ​ต้อง​ได้ สมมติ​เรา​ขอ​ให้ agent implement การ​บันทึก​การ​เบิก​ใช้​วงเงิน code ที่ agent มัก​เขียน เมื่อ​ไม่มี glossary จะ​หน้าตา​ประมาณ​นี้

// สิ่งที่ agent เดาเอาเองเมื่อไม่มี context — ชื่อกลาง ๆ ไร้บริบท
public class User
{
public decimal Balance { get; set; } // Balance ของอะไร วงเงิน หรือ ยอดค้าง
}
public class AccountManager
{
// Process อะไร ใครอ่านก็ต้องไปเดาจาก implementation
public void Process(User user, int amount)
{
user.Balance += amount;
}
}

ปัญหา​ไม่ใช่​ว่า​มัน​รัน​ไม่​ได้ ปัญหา​คือ​ทุก​ชื่อ​ใน​นี้​บังคับ​ให้​คน​อ่าน​ต้อง​เดา Balance เป็น​วงเงิน​หรือ​ยอด​ค้าง Process ทำ​อะไร User คือ Applicant หรือ Borrower และ​เพราะ​ทุก​อย่าง​ถูก​ยัด​ใน User ก้อน​เดียว เส้น​แบ่ง bounded context ก็​หาย​ไป​แล้ว

code ที่ agent เขียน เมื่อ​ป้อน glossary ก่อน จะ​พูด​ภาษา​เดียว​กับ​ทีม

// Servicing context — ทุกชื่อมาจาก Ubiquitous Language ของทีม
public sealed class LoanAccount
{
public Money OutstandingBalance { get; private set; }
// "การเบิกใช้วงเงิน" = RecordDrawdown ไม่ใช่ "add amount"
public void RecordDrawdown(Drawdown drawdown)
{
OutstandingBalance = OutstandingBalance.Add(drawdown.Principal);
}
}

อ่าน method RecordDrawdown แล้ว​ออก​เสียง​เป็น​ประโยค​ธุรกิจ​ได้​เลย​ว่า “บัญชี​สินเชื่อ​บันทึก​การ​เบิก​ใช้​วงเงิน” — นั่น​คือ​สัญญาณ​ของ UL ที่​ดี​ตาม​ที่​บทเรียน​ใน​คอร์ส​อธิบาย​ไว้

และ​ที่​สำคัญ LoanAccount ไม่มี CreditLimit ปน​เข้า​มา เพราะ glossary บอก​ชัด​ว่า​คำ​นั้น​อยู่​ฝั่ง Origination agent จึง​เคารพ​ขอบเขต​โดย​ไม่​ต้อง​มี​ใคร​มา​รีวิว​จับ​ทีหลัง

ถ้า​คุณ​อ่าน​บทความ Structured-Prompt-Driven Development (SPDD) มา​แล้ว จะจำ REASONS Canvas ได้ ซึ่ง​มี​ช่อง E — Entities (entity ของ domain และ​ความ​สัมพันธ์) และ N — Norms (บรรทัดฐาน​ทาง​วิศวกรรม เช่น การ​ตั้ง​ชื่อ)

glossary ของ UL คือ “วัตถุดิบ” ที่​เติม​สอง​ช่อง​นั้น​ได้​ตรง​ที่สุด เพราะ​มัน​คือ​รายการ entity พร้อม​บริบท และ​คือ​กติกา​การ​ตั้ง​ชื่อ​ใน​ตัว​เดียวกัน พูด​อีก​อย่าง​คือ SPDD มี​ช่อง​รอ​ไว้​ให้ UL อยู่​แล้ว เรา​แค่​ต้องเตรียม glossary ให้​พร้อม​ก่อน​จะ​เริ่ม​กรอก Canvas

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

ลำดับ​ที่​แนะนำ​สำหรับ​ทีม​ที่​ทำงาน​กับ agent จึง​เป็น glossary → spec/prompt → code โดย​มี glossary เป็น file context ที่​โหลด​อยู่​ตลอด​ทุก​ขั้น ไม่ใช่​ป้อน​ครั้ง​เดียว​แล้ว​ลืม

glossary ไม่ใช่​หิน​สลัก Evans ย้ำ​ว่า “การ​เปลี่ยน​ภาษา​คือ​การ​เปลี่ยน model” เมื่อ​ทีม​ค้น​พบ​ว่า​คำ​เดิม​สื่อ​ความ​ไม่​ตรง หรือ​พบ​แนวคิด​ใหม่​ที่​ยัง​ไม่มี​ชื่อ นั่น​คือ​สัญญาณ​ให้​ปรับ glossary — และ​เมื่อ glossary เปลี่ยน file context ที่​ป้อน agent ก็​ต้อง​เปลี่ยน​ตาม

นี่​คือ​วินัย sync แบบ​เดียว​กับ​ที่ SPDD ใช้​กับ prompt กับ code เพียง​แต่​ขยับ​ขึ้น​ไป​อีก​ชั้น​หนึ่ง​คือ​ชั้น​ของ​คำ​ศัพท์

flowchart LR
  model["Domain model"] --> gloss["UL glossary"]
  gloss --> prompt["CLAUDE.md context"]
  prompt --> code["Generated code"]
  code -.->|new term surfaced| gloss
  gloss -.->|sharper language| model

ภาพ 4 — วง​รอบ​ของ glossary ที่​มี​ชีวิต domain model หล่อเลี้ยง glossary, glossary กลาย​เป็น context ใน CLAUDE.md, context นำทาง code ที่ generate ออก​มา และ​เมื่อ​ระหว่าง​เขียน code มี​คำ​ใหม่​โผล่​ขึ้น (เส้น​ประ​ล่าง) คำ​นั้น​ถูก​ป้อน​กลับ​เข้า glossary ซึ่ง​ใน​ทาง​กลับ​กัน​ก็​ทำให้ domain model คม​ขึ้น ทั้ง​วง​ต้อง​หมุน​ไป​ด้วย​กัน ไม่ใช่​แช่แข็ง glossary ไว้​ตั้งแต่​วัน​แรก

ใน​ทาง​ปฏิบัติ ให้​ถือว่า CLAUDE.md ส่วน Ubiquitous Language เป็น file ที่​ผ่าน code review เหมือน code ทั่วไป ทุก​ครั้ง​ที่​มี​คำ​ใหม่​เข้า PR หรือ​คำ​เดิม​เปลี่ยน​ความหมาย ให้​แก้​ที่ glossary ก่อน แล้ว​ค่อย​ให้ agent generate code รอบ​ใหม่​บน glossary ที่​อัปเดต​แล้ว

วิธี​นี้​ทำให้​คำ​ศัพท์​ของ​ทีม​กับ​คำ​ใน code ไม่มี​วัน​เบน​ออก​จาก​กัน​แบบ​เงียบ ๆ

ก่อน​จะ​เปิด agent แล้ว​โยน spec ให้​มัน​ใน​ครั้ง​หน้า ลอง​ไล่​เช็ก list สั้น ๆ นี้​ก่อน

  • มี glossary ที่​ทุก​รายการ​มี​ครบ​สาม​ส่วน — term, นิยาม, และ bounded context — หรือ​ยัง
  • glossary อยู่​ใน file context ที่ agent อ่าน​ทุก​ครั้ง (เช่น CLAUDE.md) หรือ​ยัง​ค้าง​อยู่​ใน Confluence
  • มี​หมวด คำ​ต้อง​ห้าม ที่​บอก agent ว่า​เจอ User/Account/Manager ลอย ๆ แล้ว​ให้​หยุด​ถาม​หรือ​ยัง
  • คำ​ที่​เป็น polyseme (เช่น Applicant กับ Borrower) ถูก​แยก​ตาม​บริบท​ชัดเจน​ไหม
  • glossary ถูก​ป้อน ก่อน spec ไม่ใช่​หลัง
  • มี​วินัย sync ที่​แก้ glossary ก่อน​แก้ code เมื่อ​ภาษา​ของ​ทีม​เปลี่ยน​หรือ​ยัง

ทีม​ไทย​ที่​คุย​งาน​เป็น​ไทย​แต่ code เป็น​อังกฤษ​มี​ช่องว่าง​สอง​ชั้น​อยู่​แล้ว และ agent พร้อม​จะ​เปลี่ยน​มัน​เป็น​สาม​ชั้น​ด้วย​ความเร็ว​ที่​มนุษย์​ตาม​ไม่ทัน glossary ของ Ubiquitous Language คือ​เครื่องมือ​ที่​ปิด​ช่องว่าง​ทั้ง​สาม​ชั้น​พร้อม​กัน — ขอ​เพียง​คุณ​ป้อน​มัน​เป็น​สิ่ง​แรก ก่อน spec เสมอ


📄 อ่าน​ต่อ​ใน​ไซต์: