ออกแบบ 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 เห็น แปลอะไรทิ้ง และปิดอะไรไว้
คอร์สนี้เป็น คอร์สออกแบบ ไม่ใช่ทัวร์ SDK — code ในบทนี้คือ ภาพร่าง ของ schema และ tool signature (pseudo-config สั้นๆ) ไม่ใช่ server ที่รันได้จริง ตัวอย่างต่อยอดจาก domain เดิมที่ repo kaen-food-ordering (กำลังจัดทำ): ระบบภายในของ platform ฟู้ดเดลิเวอรี (ออเดอร์ ครัว การเงิน) ที่ถูกห่อเป็น MCP server ให้ ops-copilot เรียกใช้
ต่างจากบทที่พูดถึงบทบาทหรือความปลอดภัย บทนี้ ไม่ได้อ้าง spec MCP เป็นข้อบังคับ — กฎการออกแบบเกือบทั้งหมดมาจากคู่มือวิศวกรรมของ Anthropic เรื่อง Writing effective tools for agents ให้อ่านมันว่าเป็น “ออกแบบอย่างไรให้ดี” ไม่ใช่ “spec สั่งให้ทำ” — spec เปิดช่องให้คุณออกแบบ tool ห่วยๆ ได้สบายๆ วินัยที่กันไม่ให้ห่วยคือเนื้อหาของบทนี้
tool คือ Anti-Corruption Layer
หัวข้อที่มีชื่อว่า “tool คือ Anti-Corruption Layer”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)
❌ version ดิบ vs ✅ tool ที่เป็น ACL
หัวข้อที่มีชื่อว่า “❌ version ดิบ vs ✅ tool ที่เป็น ACL”ตัวอย่างที่ชัดที่สุดคือ tool ที่เปิด backend ทั้งใบ:
run_sql(query: string) -> แถวดิบจากตารางไหนก็ได้
# model ต้องรู้ schema เอง, join เอง, จำ id ดิบเอง# และถ้ามันอยาก ก็ยิง DELETE / UPDATE ได้ — พื้นผิวกว้างเท่าทั้ง DBrun_sql ไม่มีชั้นแปลอะไรเลย: มันคือ backend ดิบที่ถูกห่อด้วยชื่อ function บางๆ model ต้องแบกภาระของ DBA และ error ที่ได้กลับก็เป็นภาษาเครื่อง เทียบกับ tool ที่พูดภาษา 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
กฎการออกแบบ 5 ข้อ (ทั้งหมดคือชั้นแปล)
หัวข้อที่มีชื่อว่า “กฎการออกแบบ 5 ข้อ (ทั้งหมดคือชั้นแปล)”กฎด้านล่างมาจากคู่มือ Writing effective tools for agents ของ Anthropic — อ่านทุกข้อในฐานะ “ชั้นแปลกันของดิบรั่ว” แล้วมันจะเข้าที่
1. ตั้งชื่อเป็น namespace ตาม service/resource
หัวข้อที่มีชื่อว่า “1. ตั้งชื่อเป็น namespace ตาม service/resource”อย่าเทกอง 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 ไหน
2. ใช้ field ที่คนอ่านรู้เรื่อง แทน id ดิบ
หัวข้อที่มีชื่อว่า “2. ใช้ field ที่คนอ่านรู้เรื่อง แทน id ดิบ”รับและคืนค่าที่มีความหมายกับมนุษย์ — restaurant_name, zone, status — แทน id ทึบอย่าง restaurant_uuid: 4f3c9a2e... คู่มือระบุว่าการใช้ field ที่สื่อความหมาย “ช่วยเพิ่มความแม่นของ Claude อย่างมีนัยสำคัญ ด้วยการลด hallucination” เหตุผลตรงไปตรงมา: model ให้เหตุผลกับ “ร้านโกเด้ง โซนสุขุมวิท” ได้ ส่วน UUID ไม่มีอะไรให้ยึด มันจึงมีแนวโน้ม เดา id ขึ้นมาเอง ถ้าจำเป็นต้องมี id จริงในระบบ ให้เก็บไว้ในชั้นแปล — ให้ tool รับชื่อ แล้ว มันแปลเป็น id เองข้างใน ไม่ยื่น id ดิบออกมาให้ model ถือ
3. คุม verbosity + ทำ pagination (ระวังเพดาน ~25k token)
หัวข้อที่มีชื่อว่า “3. คุม verbosity + ทำ pagination (ระวังเพดาน ~25k token)”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 ใช้จริง”
{ "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": "รอครัว | กำลังส่ง | ส่งแล้ว" }}4. ยุบ CRUD บางๆ เป็น workflow tool
หัวข้อที่มีชื่อว่า “4. ยุบ CRUD บางๆ เป็น workflow tool”อย่าห่อ 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
5. error ที่ actionable — ไม่ใช่ error code ดิบ
หัวข้อที่มีชื่อว่า “5. error ที่ actionable — ไม่ใช่ error code ดิบ”error message ก็เป็นพื้นผิวที่ model อ่าน อย่าปล่อย error ดิบของ backend ทะลุออกมา ให้ แปลเป็นข้อความที่บอกทางแก้:
❌ "SQLSTATE 23503: foreign key violation on orders.zone_id"✅ "ไม่พบโซน 'สุขวิท' — โซนที่มีคือ: สุขุมวิท, สีลม, อารีย์. เรียกใหม่ด้วยชื่อที่ตรง"error แบบ ✅ ทำให้ model กู้สถานการณ์เองได้ในเทิร์นถัดไป ส่วน SQLSTATE 23503 มีแต่จะทำให้มันเดามั่วหรือยอมแพ้ — และยังเผลอเปิด schema ภายใน (orders.zone_id) ให้เห็นอีกด้วย การแปล error คือชั้น ACL ด่านสุดท้ายก่อนของดิบถึง model
พื้นผิวเดียวกันคือ usability และ security
หัวข้อที่มีชื่อว่า “พื้นผิวเดียวกันคือ usability และ security”สังเกตว่าทุกกฎข้างบนให้ผลสองทางพร้อมกัน: orders_find_late ที่แคบ ใช้ง่ายกว่า สำหรับ model และ อันตรายน้อยกว่า ถ้ามีใครพยายามใช้มันในทางที่ผิด — เพราะมันทำได้แค่อ่านออเดอร์ช้า ไม่ใช่ยิง SQL อะไรก็ได้ นี่คือเหตุผลที่บทนี้เป็นสะพานเข้าบทที่ 6: schema ที่คุณออกแบบวันนี้คือ trust boundary ของคุณพรุ่งนี้ ยิ่งพื้นผิวแคบและพูดภาษา domain มากเท่าไร ทั้งความแม่นและความปลอดภัยก็ยิ่งได้ไปด้วยกัน — แต่ระวังไว้ก่อนว่า ACL ที่ออกแบบดี ลด พื้นผิวการโจมตี ไม่ได้ ลบ มัน (บทที่ 6 จะว่าด้วยสิ่งที่ ACL อย่างเดียวกันไม่อยู่ เช่น คำอธิบาย tool ที่ถูกฝังคำสั่งแฝง)
แนวคิด Anti-Corruption Layer ที่บทนี้ยืมมามองงานออกแบบ tool มาจาก DDD โดยตรง — อ่านนิยามต้นทางได้ที่:
- Anti-Corruption Layer (ACL) — DevIQ — ชั้นแปลระหว่าง bounded context ที่กัน model ของระบบอื่นไม่ให้รั่วเข้ามาปน model ของเรา
แหล่งอ้างอิงหลักของกฎการออกแบบในบทนี้:
- Writing effective tools for agents (Anthropic) — namespace, field ที่สื่อความหมาย (ลด hallucination), verbosity + pagination, workflow tool และ error ที่ actionable
เช็กความเข้าใจ — บทที่ 3
ข้อ 1 / 3ทำไมถึงบอกว่า tool ที่ดีคือ Anti-Corruption Layer (ACL)?