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

ออกแบบ tool ที่​ดี (tool = ACL)

ใน​บท​ที่ 6 ของคอร์ส Agent as Bounded Context เรา​สรุป​ไว้​ประโยค​หนึ่ง​ที่​บท​นี้​จะ​ขยาย​ให้​เต็ม: tool ทุก​ตัว​คือ Anti-Corruption Layer — ชั้น​แปล​ที่​กั้น​ความ​รก​ของ​ระบบ​เดิม​ไม่​ให้​ไหล​เข้า context ของ agent บท​ที่ 2 เพิ่ง​ชี้​ว่า tool เป็น primitive แบบ model-controlled — model เป็น​คน​ตัดสิน​ใจ​เรียก​เอง แปล​ว่า schema ของ tool คือ​พื้น​ผิว​ที่ model อ่าน​และ​ให้​เหตุผล​ด้วยจริงๆ ไม่ใช่​แค่ signature ที่​โปรแกรมเมอร์​เห็น บท​นี้​จึง​เป็น​เรื่อง​ของ​การ​ออกแบบ​พื้น​ผิว​นั้น​ให้​ดี: ยื่น​อะไร​ให้ model เห็น แปล​อะไร​ทิ้ง และ​ปิด​อะไร​ไว้

📦 code ตัวอย่าง (กำลัง​จัด​ทำ)

คอร์ส​นี้​เป็น คอร์ส​ออกแบบ ไม่ใช่​ทัวร์ SDK — code ใน​บท​นี้​คือ ภาพร่าง ของ schema และ tool signature (pseudo-config สั้นๆ) ไม่ใช่ server ที่​รัน​ได้​จริง ตัวอย่าง​ต่อยอด​จาก domain เดิม​ที่ repo kaen-food-ordering (กำลัง​จัด​ทำ): ระบบ​ภายใน​ของ platform ฟู้ด​เดลิ​เวอรี (ออเดอร์ ครัว การเงิน) ที่​ถูก​ห่อ​เป็น MCP server ให้ ops-copilot เรียก​ใช้

⚠️ บท​นี้​คือ best-practice เชิง​วิศวกรรม ไม่ใช่​ข้อ​บังคับ​ของ spec

ต่าง​จาก​บท​ที่​พูด​ถึง​บทบาท​หรือ​ความ​ปลอดภัย บท​นี้ ไม่​ได้​อ้าง spec MCP เป็น​ข้อ​บังคับ — กฎ​การ​ออกแบบ​เกือบ​ทั้งหมด​มา​จาก​คู่มือ​วิศวกรรม​ของ Anthropic เรื่อง Writing effective tools for agents ให้​อ่าน​มัน​ว่า​เป็น “ออกแบบ​อย่างไร​ให้​ดี” ไม่ใช่ “spec สั่ง​ให้​ทำ” — spec เปิด​ช่อง​ให้​คุณ​ออกแบบ tool ห่วยๆ ได้สบายๆ วินัย​ที่​กัน​ไม่​ให้​ห่วย​คือ​เนื้อหา​ของ​บท​นี้

Anti-Corruption LayerAnti-Corruption Layer (ACL)แนวคิด​จาก DDD ที่​ยืม​มา​ใช้​มอง​งาน​ออกแบบ MCP Server: มอง Tool แต่ละ​ตัว​เป็น​ชั้น​แปล​ระหว่าง model กับ​ระบบ​ภายนอก​จริง ไม่​ปล่อย​ให้ model ของ​ระบบ​เดิม (schema, error code, id ที่​ไม่มี​ความหมาย) รั่ว​ตรง​เข้า​สู่​บริบท​ของ agent — Tool ที่​ดี​คือ ACL ที่​รักษา model ของ agent ไม่​ให้​ปน​เปื้อนStrategic Design (ACL) เป็น​แนวคิด​จาก DDD: เมื่อ bounded context ของ​เรา​ต้อง​คุย​กับ​ระบบ​ภายนอก​ที่​มี model คนละ​แบบ เรา​ไม่​ปล่อย​ให้ model ของ​ระบบ​นั้น (schema, ศัพท์, id, error code) รั่ว​ตรง​เข้า​มา​ปน model ของ​เรา — เรา​วาง ชั้น​แปล คั่น​ไว้ ให้​ด้าน​ใน​เห็น​แต่​ภาษา​ของ​เรา​เอง

ToolToolfunction ที่ Server ประกาศ​ให้ model เรียก​เอง​ได้​ระหว่าง​ให้​เหตุผล เช่น ค้น​หา​ออเดอร์ หรือ​ยิง refund มัก​มี​ผล​ข้าง​เคียง​จริง (side effect) — เป็น Primitive แบบ Model-controlled ต้อง​ระวัง parameter ผิด สิทธิ์​เกิน​จำเป็น และ​คำ​อธิบาย tool ที่​ถูก​ฝัง​คำ​สั่ง​แฝงTactical Design ที่​ดี​ทำ​หน้าที่​นั้น​เป๊ะๆ ระหว่าง model (LLM) กับ backend ดิบ ฝั่ง​ใน​ของ server มี​ตาราง orders จริง มี foreign key มี id ยาวเหยียด มี error code แบบ SQLSTATE 23503 — ของ​พวก​นี้​เป็น​ภาษา​ของ​ฐาน​ข้อมูล ไม่ใช่​ภาษา​ที่ model ควร​ต้อง​เดา ถ้า​เรา​เปิด backend ทั้ง​ใบ​ให้ model เห็นตรงๆ เรา​กำลัง​บังคับ​ให้​มัน​เรียนรู้ schema ภายใน​ของ​เรา​แล้ว​ให้​เหตุผล​บน​ของ​ที่​มัน​ไม่​ควร​ต้อง​รู้ tool ที่​ออกแบบ​ดี​จะ แปล​ของ​ดิบ​พวก​นั้น​ทิ้ง แล้ว​ยื่น​แต่​พื้น​ผิว domain ที่​แคบ อ่าน​รู้เรื่อง และ​ทำได้​แค่​ที่​อนุญาต

พูด​อีก​แบบ: schema ของ tool เป็น​ทั้ง พื้น​ผิว​การ​ใช้งาน (usability) และ พื้น​ผิว​ความ​ปลอดภัย (security) พร้อม​กัน tool ที่​แคบ ตั้ง​ชื่อ​ดี คืน​ผล​น้อย ทั้ง​ใช้​ผิด​โดย​บังเอิญ​ยาก​ขึ้น​และ​ถูก​เอา​ไป​ใช้​เป็น​อาวุธ​ยาก​ขึ้น — ออกแบบ tool ให้​ดี​คือ​การ​วาง ACL นั่นเอง (เรา​จะ​กลับ​มา​ที่​ด้าน​ความ​ปลอดภัย​เต็มๆ ใน​บท​ที่ 6)

ตัวอย่าง​ที่​ชัด​ที่สุด​คือ tool ที่​เปิด backend ทั้ง​ใบ:

❌ version ดิบ: tool เดียว​ยื่น​ฐาน​ข้อมูล​ทั้ง​ใบ​ให้ model
run_sql(query: string) -> แถวดิบจากตารางไหนก็ได้
# model ต้องรู้ schema เอง, join เอง, จำ id ดิบเอง
# และถ้ามันอยาก ก็ยิง DELETE / UPDATE ได้ — พื้นผิวกว้างเท่าทั้ง DB

run_sql ไม่มี​ชั้น​แปล​อะไร​เลย: มัน​คือ backend ดิบ​ที่​ถูก​ห่อ​ด้วย​ชื่อ function บางๆ model ต้อง​แบก​ภาระ​ของ DBA และ error ที่​ได้​กลับ​ก็​เป็น​ภาษา​เครื่อง เทียบ​กับ tool ที่​พูด​ภาษา domain:

✅ tool = ACL: แคบ ตาม​เจตนา domain แปล​ของ​ดิบ​ทิ้ง
orders_find_late(zone: string, since: datetime)
-> [{ order_id, restaurant_name, minutes_late, status }]
# model บอก "หาออเดอร์ช้าในโซนสุขุมวิทตั้งแต่เที่ยง" ได้ตรง ๆ
# ไม่ต้องรู้ schema, ทำได้แค่ 'อ่านออเดอร์ช้า' — ยิง DELETE ไม่ได้

พื้น​ผิว​ที่ model เห็น​หด​จาก “ทั้ง​ฐาน​ข้อมูล” เหลือ “หา​ออเดอร์​ที่​ส่ง​ช้า” — นั่น​คือ ACL ทำงาน

flowchart LR
  subgraph BE["Backend ดิบ (ฝั่งในของ server)"]
    DB["ตาราง orders / restaurants<br/>id ดิบ, foreign key, schema จริง"]
    RAW["run_sql, REST ทั้งใบ<br/>error code แบบ SQLSTATE"]
  end
  subgraph ACL["Tool = ACL (ชั้นแปล)"]
    T["orders_find_late(zone, since)<br/>• namespace ตาม service<br/>• ชื่อร้านแทน uuid<br/>• verbosity + pagination<br/>• error ที่ actionable"]
  end
  subgraph M["พื้นผิวที่ model เห็น"]
    V["tool แคบ ตาม domain<br/>ผลลัพธ์สั้น อ่านรู้เรื่อง<br/>ทำได้แค่ที่อนุญาต"]
  end
  DB --> RAW
  RAW -->|"แปล + จำกัด"| T
  T --> V
  classDef raw fill:#fecaca,stroke:#991b1b,color:#1a1a1f;
  classDef acl fill:#fde68a,stroke:#92400e,color:#1a1a1f;
  classDef clean fill:#bbf7d0,stroke:#065f46,color:#1a1a1f;
  class DB,RAW raw;
  class T acl;
  class V clean;

คำ​บรรยาย​ภาพ: backend ดิบ (แดง) มี schema จริง id ดิบ และ error code ของ​เครื่อง — ของ​พวก​นี้​ไม่​ควร​ไหล​ถึง model ชั้น tool/ACL (เหลือง) รับ​หน้าที่ แปล​และ​จำกัด ก่อน​ส่ง​ออก เหลือ​แต่​พื้น​ผิว​สะอาด (เขียว) ที่ model เห็น: tool แคบ ตาม domain ผลลัพธ์​สั้น และ​ทำได้​แค่​ที่​อนุญาต ลูกศร​จาก​ดิบ​ไป​เขียน​ต้อง​ผ่าน​ชั้น​เหลือง​เสมอ นั่น​คือ​หัวใจ​ของ ACL

กฎ​ด้าน​ล่าง​มา​จาก​คู่มือ Writing effective tools for agents ของ Anthropic — อ่าน​ทุก​ข้อ​ใน​ฐานะ “ชั้น​แปล​กัน​ของ​ดิบ​รั่ว” แล้ว​มัน​จะ​เข้า​ที่

อย่า​เท​กอง tool แบน​ราบ​ชื่อ​กำกวม​อย่าง search, get, update ให้​ตั้ง namespace ตาม service หรือ resource — คู่มือ​ใช้​ตัวอย่าง asana_projects_search แทน search ลอยๆ ใน domain เรา: orders_find_late, kitchen_queue_status, finance_issue_refund เมื่อ ops-copilot ต่อ​หลาย server พร้อม​กัน ชื่อ​ที่​มี namespace ช่วย​ให้ model เลือก tool ถูก​ตัว ไม่​สับสน​ว่า search อัน​ไหน​ของ​ใคร — นี่​คือ​ชั้น​แปล​ระดับ ชื่อ: มัน​บอกว่า tool นี้​อยู่ bounded context ไหน

รับ​และ​คืน​ค่าที่​มี​ความหมาย​กับ​มนุษย์ — restaurant_name, zone, status — แทน id ทึบ​อย่าง restaurant_uuid: 4f3c9a2e... คู่มือ​ระบุ​ว่าการ​ใช้ field ที่​สื่อ​ความหมาย “ช่วย​เพิ่ม​ความ​แม่น​ของ Claude อย่าง​มี​นัย​สำคัญ ด้วย​การ​ลด hallucination” เหตุผล​ตรง​ไป​ตรง​มา: model ให้​เหตุผล​กับ “ร้าน​โก​เด้ง โซน​สุขุมวิท” ได้ ส่วน UUID ไม่มี​อะไร​ให้​ยึด มัน​จึง​มี​แนวโน้ม เดา id ขึ้น​มา​เอง ถ้า​จำเป็น​ต้อง​มี id จริง​ใน​ระบบ ให้​เก็บ​ไว้​ใน​ชั้น​แปล — ให้ tool รับ​ชื่อ แล้ว มัน​แปล​เป็น id เอง​ข้าง​ใน ไม่​ยื่น id ดิบ​ออก​มา​ให้ model ถือ

output ของ tool กิน​เนื้อ context เสมอ ให้​เปิด response-format / verbosity control — คู่มือ​ยก​ตัวอย่าง​ผลลัพธ์​ระดับ concise ที่ 72 token เทียบ​กับ detailed ที่ 206 token สำหรับ​ข้อมูล​เดียวกัน — และ​ทำ pagination / truncation เพราะ Claude Code ตัด​ผลลัพธ์​ของ tool ที่​ราว 25,000 token อยู่​แล้ว ถ้า orders_find_late คืน​มา​ห้า​พัน​แถว​รวด context พัง​ก่อน model จะ​ได้​ทำงาน ให้ default เป็น concise + หน้า​ละ ≤ 25 แถว แล้ว​ให้​ขอ detailed เฉพาะ​ตอน​ต้องการ นี่​คือ​ชั้น​แปล​ระดับ ปริมาณ: แปล “ทั้ง​ผลลัพธ์” ให้​เหลือ “เท่า​ที่ model ใช้​จริง”

ภาพร่าง schema ของ tool ที่​เป็น ACL
{
"name": "orders_find_late", // namespace: <service>_<action>
"description": "หาออเดอร์ที่ส่งช้ากว่ากำหนดในโซนที่ระบุ",
"input": {
"zone": "string", // 'สุขุมวิท' ไม่ใช่ zone_id = 4f3c...
"since": "datetime",
"verbosity": "concise | detailed", // default: concise (คุมความยาว output)
"page_size": "int (<= 25)" // กัน 25k token cap
},
"output_row": {
"order_id": "string",
"restaurant_name": "string", // ชื่อร้าน ไม่ใช่ restaurant_uuid
"minutes_late": "int",
"status": "รอครัว | กำลังส่ง | ส่งแล้ว"
}
}

อย่า​ห่อ endpoint ที​ละ​ตัว​เป็น tool ที​ละ​ตัว (list_orders, get_customer, create_refund, notify_customer) แล้ว​โยน​ภาระ​ให้ model ร้อย​เอง​ตาม​ลำดับ — คู่มือ​แนะ​ให้ ยุบ​เป็น workflow tool ตัว​เดียว​ที่​สูง​กว่า: หนึ่ง schedule_event ดี​กว่า list_users + list_events + create_event สาม​ตัว ใน domain เรา แทนที่​จะ​เปิด​สี่​ตัว​ข้าง​ต้น ให้​เปิด finance_issue_refund(order_id, reason) ตัว​เดียว​ที่​ทำ​ครบ​ทั้ง​เช็ค​สิทธิ์ คืน​เงิน และ​แจ้ง​ลูกค้า​ข้าง​ใน ข้อดี: model เรียก​ครั้ง​เดียว​จบ​ตาม​เจตนา domain ไม่​หลง​ขั้นตอน ใช้ token น้อย​ลง และ​พื้น​ผิว​ที่​เปิด​ออก​ก็ แคบ​และ​คุม​สิทธิ์​ง่าย​กว่า — CRUD ดิบ​สี่​ตัว​คือ backend รั่ว, workflow tool ตัว​เดียว​คือ ACL

error message ก็​เป็น​พื้น​ผิว​ที่ model อ่าน อย่า​ปล่อย error ดิบ​ของ backend ทะลุ​ออก​มา ให้ แปล​เป็น​ข้อความ​ที่​บอก​ทาง​แก้:

❌ version ดิบ vs ✅ error ที่ actionable
❌ "SQLSTATE 23503: foreign key violation on orders.zone_id"
✅ "ไม่พบโซน 'สุขวิท' — โซนที่มีคือ: สุขุมวิท, สีลม, อารีย์. เรียกใหม่ด้วยชื่อที่ตรง"

error แบบ ✅ ทำให้ model กู้​สถานการณ์​เอง​ได้​ใน​เทิร์น​ถัด​ไป ส่วน SQLSTATE 23503 มี​แต่​จะ​ทำให้​มัน​เดา​มั่ว​หรือ​ยอม​แพ้ — และ​ยัง​เผลอ​เปิด schema ภายใน (orders.zone_id) ให้​เห็น​อีก​ด้วย การ​แปล error คือ​ชั้น ACL ด่าน​สุดท้าย​ก่อน​ของ​ดิบ​ถึง model

สังเกต​ว่า​ทุก​กฎ​ข้าง​บน​ให้​ผล​สอง​ทาง​พร้อม​กัน: orders_find_late ที่​แคบ ใช้​ง่าย​กว่า สำหรับ model และ อันตราย​น้อย​กว่า ถ้า​มี​ใคร​พยายาม​ใช้​มัน​ใน​ทาง​ที่​ผิด — เพราะ​มัน​ทำได้​แค่​อ่านออเดอร์​ช้า ไม่ใช่​ยิง SQL อะไร​ก็ได้ นี่​คือ​เหตุผล​ที่​บท​นี้​เป็น​สะพาน​เข้า​บท​ที่ 6: schema ที่​คุณ​ออกแบบ​วัน​นี้​คือ trust boundary ของ​คุณ​พรุ่งนี้ ยิ่ง​พื้น​ผิว​แคบ​และ​พูด​ภาษา domain มาก​เท่าไร ทั้ง​ความ​แม่น​และ​ความ​ปลอดภัย​ก็​ยิ่ง​ได้​ไป​ด้วย​กัน — แต่​ระวัง​ไว้​ก่อน​ว่า ACL ที่​ออกแบบ​ดี ลด พื้น​ผิว​การ​โจมตี ไม่​ได้ ลบ มัน (บท​ที่ 6 จะ​ว่าด้วย​สิ่ง​ที่ ACL อย่าง​เดียวกัน​ไม่​อยู่ เช่น คำ​อธิบาย tool ที่​ถูก​ฝัง​คำ​สั่ง​แฝง)


🔗 อ้างอิง​เพิ่มเติม​ใน DevIQ

แนวคิด Anti-Corruption Layer ที่​บท​นี้​ยืม​มา​มอง​งาน​ออกแบบ tool มา​จาก DDD โดยตรง — อ่าน​นิยาม​ต้นทาง​ได้ที่:

  • Anti-Corruption Layer (ACL) — DevIQ — ชั้น​แปล​ระหว่าง bounded context ที่​กัน model ของ​ระบบ​อื่น​ไม่​ให้​รั่ว​เข้า​มา​ปน model ของ​เรา

แหล่ง​อ้างอิง​หลัก​ของ​กฎ​การ​ออกแบบ​ใน​บท​นี้:

เช็กความเข้าใจ — บทที่ 3

ข้อ 1 / 3

ทำไมถึงบอกว่า tool ที่ดีคือ Anti-Corruption Layer (ACL)?