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

Resources vs tools — เลือก​ให้​ถูก

บท​ที่ 3 มอง tool เป็น ACL — ชั้น​แปล​ที่​รักษา model ของ agent ไม่​ให้​ปน​เปื้อน​ความ​รก​ของ backend แต่​พอ​ลงมือ​ออกแบบ server จริง คำถาม​แรก​ที่​ค้าง​คา​ไม่ใช่ “จะ​ตั้ง​ชื่อ tool ว่า​อะไร” — มัน​คือ “ของ​ชิ้น​นี้​ควร​เป็น tool หรือ resource กัน​แน่” และ​นี่​คือ​จุด​ที่​คอนเทนต์ MCP ส่วน​ใหญ่​พา​ไป​ผิด​ทาง ด้วย​กฎ​ง่ายๆ ที่​ฟัง​ดู​สม​เหตุ​สม​ผล​แต่​ผิด: “อ่าน​ใช้ resource เขียน​ใช้ tool” บท​นี้​ทั้ง​บท​มีหน้าที่​เดียว คือ​ลบ​กฎ​นั้น​ทิ้ง แล้ว​แทน​ด้วย​เส้น​แบ่ง​ที่ spec พูด​จริง

📦 code ตัวอย่าง

คอร์ส​นี้​เป็น คอร์ส​ออกแบบ — code ใน​บท​นี้​คือ ภาพร่าง ของ tool schema เทียบ​กับ resource template สำหรับ lookup ตัว​เดียวกัน ไม่ใช่ server ที่​รัน​ได้​จริง ตัวอย่าง​ต่อยอด​จาก domain เดิม​ที่ repo kaen-food-ordering (กำลัง​จัด​ทำ): server ออเดอร์ ที่​ต้อง​ตัดสิน​ใจ​ว่า​จะ​เปิด​การ​ค้น​ออเดอร์​ให้ ops-copilot ใน​รูปแบบ tool (model เรียก​เอง) หรือ resource (app แนบ​เข้า context)

spec ไม่​ได้​แบ่ง primitive ด้วย “อ่าน vs เขียน” — มัน​แบ่ง​ด้วย ใคร​เป็น​ผู้​ตัดสิน​ใจ​ว่า​จะ​เรียก​เมื่อไร หน้า tools ระบุ​ว่า tool ถูก​ออกแบบ​ให้​เป็น model-controlled“เครื่องมือ​ใน MCP ถูก​ออกแบบ​ให้ model ควบคุม หมายความ​ว่า model ภาษา​สามารถ​ค้น​พบ​และ​เรียก​ใช้​เครื่องมือ​ได้​เอง​โดย​อัตโนมัติ” (tools, MCP spec 2025-11-25) ส่วน​หน้า resources ระบุ​ตรง​ข้าม​ว่า resource เป็น application-driven“ทรัพยากร​ใน MCP ถูก​ออกแบบ​ให้ application ขับ​เคลื่อน โดย app ที่​เป็น host เป็น​ผู้​กำหนด​ว่า​จะ​นำ​บริบท​เข้า​มา​ผสาน​อย่างไร​ตาม​ความ​ต้องการ​ของ​ตน” (resources, MCP spec 2025-11-25)

สอง​ประโยค​นี้​คือ​ทั้ง​บท:

  • Model-controlledModel-controlledลักษณะ​ของ Primitive ที่​ตัว model เป็น​ผู้​ตัดสิน​ใจ​เอง​ว่า​จะ​เรียก​เมื่อไร​และ​เรียก​อย่างไร เช่น Tool — ต่าง​จาก App-driven ที่ app เป็น​ผู้​ตัดสิน​ใจ​แทน ความ​แตก​ต่าง​นี้​กำหนด​ว่า​ควร​ตรวจสอบ​สิทธิ์​และ​ผลลัพธ์​เข้มงวด​แค่​ไหน ไม่ใช่​แค่​ดู​ว่า​ข้อมูล​นั้น 'อ่าน​อย่าง​เดียว' หรือ 'แก้ไข​ได้'Architecture — model เป็น​ผู้​ตัดสิน​ใจ​เอง​ระหว่าง​ให้​เหตุผล​ว่า​จะ​เรียก​เมื่อไร​และ​เรียก​อย่างไร นี่​คือ​ธรรมชาติ​ของ tool: agent มอง​เห็น​มัน​ใน​รายการ tools/list แล้ว​เลือก​ยิง tools/call เอง โดยที่ app ไม่​ได้​สั่ง
  • App-drivenApp-drivenลักษณะ​ของ Primitive ที่ Host/Client เป็น​ผู้​ตัดสิน​ใจ​ว่า​จะ​ดึง​เข้า​มา​เมื่อไร​และ​อัน​ไหน ไม่ใช่ model เรียก​เอง เช่น Resource — ความ​เสี่ยง​จึง​ต่าง​จาก Tool เพราะ model ไม่​ได้​เลือก​เอง แต่​เนื้อหา​ที่​ดึง​มายัง​ปน​เปื้อน prompt injection ได้​เหมือน​กันArchitecture — host หรือ​ผู้​ใช้​เป็น​ผู้​ตัดสิน​ใจ​ว่า​จะ​ดึง​อะไร​เข้า context เมื่อไร ไม่ใช่ model นี่​คือ​ธรรมชาติ​ของ resource: app เลือก​ว่า​จะ​แนบ resource ไหน​เข้าไป​ใน​บทสนทนา แล้ว server แค่​ยื่น​ให้​ตาม​ที่​ถูก​ขอ​ผ่าน resources/read

พูด​อีก​แบบ: ความ​ต่าง​ไม่​ได้​อยู่​ที่ ข้อมูล​ไหล​ไป​ทาง​ไหน แต่​อยู่​ที่ มือ​ใคร​อยู่​บน​สวิตช์ tool วาง​สวิตช์​ไว้​ใน​มือ model resource วาง​ไว้​ใน​มือ app/ผู้​ใช้ เมื่อ​เข้าใจ​ตรง​นี้ การ​เลือก primitive จึง​กลาย​เป็น​คำถาม​เชิง​ออกแบบ​ที่​แหลมคม: สำหรับ​ของ​ชิ้น​นี้ เรา​อยาก​ให้​ใคร​เป็น​คน​ตัดสิน​ว่า​มัน​จะ​เข้า​มา​เมื่อไร?

ผล​พวง​ที่​สำคัญ​ที่สุด​ของ​เส้น​แบ่ง​นี้: lookup อ่าน​อย่าง​เดียว​เป็น​ได้​ทั้ง​สอง​แบบ — และ​เลือก​ถูก​ได้​ก็​ต่อ​เมื่อ​ถาม​ว่า “ใคร​ควร​ควบคุม” ไม่ใช่ “มัน​อ่าน​หรือ​เขียน”

ลอง​นึกถึง​การ​ค้น​ออเดอร์​ใน platform ฟู้ด​เดลิ​เวอรี มัน​อ่าน​อย่าง​เดียว ไม่มี​ผล​ข้าง​เคียง — กฎ​ดิบๆ จะ​บอก​ทันที​ว่า “ก็ resource สิ” แต่​นั่น​คือ​การ​ยึด​กริยา​ผิด​จุด ถ้า​เรา​อยาก​ให้ ops-copilot ตัดสิน​ใจ​เอง​ระหว่าง​สืบสวน ว่า​จะ​ค้น​ออเดอร์​ไหน​ดี มัน​ต้อง​เป็น tool (model-controlled) — เพราะ model คือ​คน​ที่​ต้อง​กด​สวิตช์ ถ้า​เรา​อยาก​ให้ หน้า​จอ ops แนบ​ออเดอร์​ที่​ผู้​ใช้​กำลัง​เปิด​ดู​อยู่ เข้า context ให้​อัตโนมัติ อัน​เดียวกันเป๊ะ​กลาย​เป็น resource (app-driven) — เพราะ app คือ​คน​กด​สวิตช์ อ่าน​เหมือน​กัน​ทั้ง​คู่ ต่าง​กัน​แค่ “มือ​ใคร​อยู่​บน​สวิตช์”

❌ version ดิบ: ตัดสิน primitive จาก HTTP verb
resource = GET (อ่าน) ← ผิด: ไปยึดกริยา read/write
tool = POST (เขียน) ← ผิด: ไปยึดกริยา read/write
# เส้นแบ่งจริงคือ "ใครควบคุมการเรียก" ไม่ใช่ read/write หรือ GET/POST
# find_order อ่านอย่างเดียว แต่เป็น tool ได้ ถ้าอยากให้ model เรียกเอง
ภาพร่าง: lookup เดียวกัน เปิด​ได้​สอง​แบบ — ต่าง​ที่ 'ใคร​เรียก'
# แบบ Tool (Model-controlled): model ตัดสินใจเรียกเองระหว่างให้เหตุผล
tool find_order(orderId) -> Order # tools/call · มี human gate (SHOULD)
# แบบ Resource (App-driven): app/ผู้ใช้เลือกแนบเข้า context
resource orders://{orderId} # resources/read ตามรูปแบบ URI
subscribe orders://{orderId} -> updated # ฟังการอัปเดตแบบสด
flowchart TB
  START["ต้องการดึงข้อมูล/บริบทเข้ามา"]
  Q{"ใครควรตัดสินว่าจะดึงเข้ามาเมื่อไร?"}
  START --> Q
  Q -->|"model ตัดสินเองระหว่างให้เหตุผล"| TOOL["Tool — Model-controlled<br/>model ยิง tools/call เอง<br/>human-in-the-loop = SHOULD"]
  Q -->|"app/ผู้ใช้เลือกแนบเข้า context"| RES["Resource — App-driven<br/>app สั่ง resources/read<br/>มี URI + subscribe"]
  TOOL --> READ["หมายเหตุ: อ่านอย่างเดียวก็เป็น Tool ได้<br/>ถ้าอยากให้ model เรียกเอง"]
  RES --> READ2["หมายเหตุ: read/write ไม่ใช่เส้นแบ่ง<br/>'ใครควบคุม' ต่างหากที่ตัดสิน"]
  classDef start fill:#e9d5ff,stroke:#6b21a8,color:#1a1a1f;
  classDef tool fill:#c7d2fe,stroke:#3730a3,color:#1a1a1f;
  classDef res fill:#bbf7d0,stroke:#065f46,color:#1a1a1f;
  class START,Q start;
  class TOOL,READ tool;
  class RES,READ2 res;

คำ​บรรยาย​ภาพ: คำถาม​ที่​ตัดสิน​ว่า​ของ​ชิ้น​หนึ่ง​ควร​เป็น tool หรือ resource ไม่ใช่ “อ่าน​หรือ​เขียน” แต่​คือ ใคร​ตัดสิน​ว่า​บริบท​จะ​เข้า​มา​เมื่อไร — ถ้า model ควร​เป็น​คน​ตัดสิน​เอง​ระหว่าง​ให้​เหตุผล มัน​คือ tool (สีน้ำเงิน, model-controlled) ถ้า app หรือ​ผู้​ใช้​ควร​เป็น​คน​เลือก​แนบ มัน​คือ resource (สี​เขียว, app-driven) lookup อ่าน​อย่าง​เดียว​ลง​ได้​ทั้ง​สอง​ฝั่ง ขึ้น​กับ​ว่า​มือ​ใคร​อยู่​บน​สวิตช์

ถ้า​เส้น​แบ่ง​คือ “ใคร​ควบคุม” แล้ว resource ให้​อะไร​เพิ่ม​ที่ tool ให้​ไม่​ได้? สอง​อย่าง​ที่มา​จาก​ธรรมชาติ app-driven ของ​มัน — identity และ subscription

IdentityResourceResourceข้อมูล​ที่ Server เปิด​ให้ Host แนบ​เข้า context ได้ เช่น file log แถว​ใน​ฐาน​ข้อมูล หรือ​เอกสาร​นโยบาย — เป็น Primitive แบบ App-driven: app หรือ​ผู้​ใช้​เป็น​ผู้​เลือก​ว่า​จะ​แนบ​อัน​ไหน​เมื่อไร ไม่ใช่ model เรียก​เอง คล้าย endpoint อ่าน​อย่าง​เดียว​ที่​ไม่มี​ผล​ข้าง​เคียงTactical Design แต่ละ​ตัว​มี​ตัว​ระบุ​เป็น URI (ตาม​มาตรฐาน RFC 3986) เช่น orders://8842 ไม่ใช่​แค่​ผลลัพธ์​ที่​โผล่​มา​แล้ว​หาย​ไป​อย่าง​ค่าที่ tool คืน​กลับ พอมี identity เสถียร app จึง​อ้าง​ถึง ผูก และ​แนบ​มัน​ซ้ำ​ได้ และ​เมื่อ resource หลาย​ตัว​มี​รูปแบบ​เดียวกัน server ประกาศ​เป็น Resource TemplateResource TemplateURI ที่​มี parameter (เช่น orders://{orderId}) ให้ Server ประกาศ​ชุด Resource ที่​มี​รูปแบบ​เดียวกัน​ได้​โดย​ไม่​ต้อง​แจง​ที​ละ​รายการ — Host เติม​ค่า parameter ก่อน​ขอ Resource จริง​ตาม​รูปแบบ​นั้น รองรับ​การ subscribe ฟัง​การ​อัปเดต​ได้​ด้วยTactical Design — URI ที่​มี parameter อย่าง orders://{orderId} (ตาม​มาตรฐาน URI Template, RFC 6570) — แล้ว​ให้ host เติม​ค่า​ก่อน​ขอ​จริง แทนที่​จะ​แจงออเดอร์​ที​ละล้านรายการ

Subscription — เพราะ resource มี identity เสถียร host จึง resources/subscribe กับ​มัน​ได้ แล้ว​รับ notifications/resources/updated เมื่อ​ข้อมูล​เบื้องหลัง​เปลี่ยน (resources, MCP spec 2025-11-25) นี่​คือ​ความ​สามารถ​ที่ tool ไม่มี​เลย: tool เป็นการ​เรียก​แล้ว​จบ​เป็นครั้งๆ ไม่มี​สาย “คอย​ฟัง​การ​อัปเดต” ใน domain ฟู้ด​เดลิ​เวอรี resource orders://8842 ที่ subscribe ไว้​จะ​ดัน​สถานะ​ใหม่​เข้า context ของ ops-copilot เอง​เมื่อ​ออเดอร์​ขยับ​จาก “กำลัง​ปรุง” เป็น “ออก​ส่ง” โดยที่ model ไม่​ต้อง​วน​ถาม​ซ้ำ — เป็น​บริบท​สด ที่ app เป็น​คน​คุม​จังหวะ ไม่ใช่ model

จุด​สุดท้าย​ที่​พลาด​กัน​บ่อย​เป็น​เรื่อง​ของ คำ​กริยา​ช่วย​เชิง​บรรทัดฐาน (modal verb) หน้า tools บอกว่า “ควร (SHOULD) มี​มนุษย์​อยู่​ใน​วงจร​เสมอ โดย​มี​ความ​สามารถ​ปฏิเสธ​การ​เรียก​ใช้​เครื่องมือ​ได้” (tools, MCP spec 2025-11-25) คำ​สำคัญ​คือ SHOULD ไม่ใช่ MUST

ความ​ต่าง​นี้​ไม่ใช่​การ​จับผิด​คำ มัน​เปลี่ยน​สิ่ง​ที่​เรา​พูด​ได้​จริง: implementation ที่ ตรง​ตาม spec สามารถ​ละเว้น human gate ได้ — มัน​แค่​ไม่ใช่​แนว​ปฏิบัติ​ที่​ดี spec ไม่​ได้ รับประกัน ว่า​จะ​มี​มนุษย์​คอย​กด​อนุมัติ​ทุก​การ​เรียก tool ดังนั้น​อย่า​ออกแบบ​ระบบ​โดย​คิด​ว่า “MCP การันตี human gate ให้​แล้ว” — ถ้า​ความ​ปลอดภัย​ของ​คุณ​ต้อง​พึ่ง​มนุษย์​อนุมัติ คุณ​ต้อง​เป็น​คน​สร้าง​ชั้น​นั้น​ขึ้น​มา​เอง ไม่ใช่​รอ​ให้​โพรโทคอล​ยก​ให้

⚠️ SHOULD ≠ MUST: อย่า​พึ่ง human gate ที่ spec ไม่​ได้​บังคับ

spec ใช้​คำ​ว่า SHOULD (ข้อ​แนะนำ) กับ human-in-the-loop ของ tool ไม่ใช่ MUST (ข้อ​บังคับ) — server หรือ host ที่​ตรง​ตาม spec ละเว้น​การ​ให้​มนุษย์​อนุมัติ​ได้ นี่​คือ​เหตุผล​ที่​บท​ที่ 6 จะ​ย้ำ​ว่า per-call approval เพียง​อย่าง​เดียว​ไม่​พอ ต้อง​จับ​คู่​กับ​การ​จำกัด​สิทธิ์​และ sandbox ที่​เป็น​ตัว​ควบคุม​แบบ​กำหนด​ผล​ได้​จริง ไม่ใช่​หวัง​พึ่ง​ข้อ​แนะนำ​เชิง​บรรทัดฐาน​ที่ implementation ปิด​ได้

กลับ​มา​ที่ ops-copilot: การ​เลือก primitive คือ​การ​วาง​เส้น​ควบคุม

หัวข้อ​ที่​มีชื่อ​ว่า “กลับ​มา​ที่ ops-copilot: การ​เลือก primitive คือ​การ​วาง​เส้น​ควบคุม”

ร้อย​กลับ​เข้า​เลนส์​เดิม​ของ​คอร์ส​พี่ (Agent as Bounded Context): server ออเดอร์​คือ bounded context หนึ่ง และ​สิ่ง​ที่​มัน​เปิด​ออก​มา​คือ​ภาษา​ที่​เผยแพร่​ของ context นั้น การ​เลือก​ระหว่าง tool กับ resource จึง​ไม่ใช่​เรื่อง​เทคนิค​ปลีกย่อย — มัน​คือ​การ​ตัดสิน​ว่า ใคร​ถือ​สิทธิ์​ตัดสิน​ใจ​ว่า​บริบท​ชิ้น​ไหน​จะ​เข้า​มา​เมื่อไร ซึ่ง​เป็นการ​วาง​เส้น​ควบคุม (และ​เส้น​ความ​ปลอดภัย) ตรงๆ

เปิด​การ​ค้น​ออเดอร์​เป็น tool = มอบ​สวิตช์​ให้ model นั่น​คือ​พลัง (copilot สืบสวน​เอง​ได้) แต่​ก็​คือ​พื้น​ผิว​การ​โจมตี (model ถูก prompt injection หลอก​ให้​เรียก​ได้) — จึง​ย้อน​กลับ​ไป​ที่​บท​ที่ 3: tool ตัว​นั้น​ต้อง​เป็น ACL ที่​แคบ ตั้ง​ชื่อ​ดี รักษา​ขอบเขต​ให้​ปลอดภัย เปิด​ออเดอร์​เป็น resource = เก็บ​สวิตช์​ไว้​ใน​มือ app model เห็น​มัน​ก็​ต่อ​เมื่อ app เลือก​แนบ​ให้ — พื้น​ผิว​แคบ​ลง แต่ copilot ก็​สืบสวน​เอง​ไม่​ได้ ทั้ง​สอง​ทาง​ถูก​ได้​ทั้ง​คู่ ผิด​ก็ได้​ทั้ง​คู่ อยู่​ที่​ว่า​เรา​ตอบ​คำถาม​เดียว​ถูก​ไหม: ของ​ชิ้น​นี้ เรา​อยาก​ให้​ใคร​กด​สวิตช์? — ไม่ใช่ มัน​อ่าน​หรือ​เขียน


🔗 แหล่ง​อ้างอิง​หลัก​ของ​บท​นี้

บท​นี้​อ้างอิง​แหล่ง​ปฐม​ภูมิ​โดยตรง (ยัง​ไม่มี​การ์ด DevIQ สำหรับ​หัวข้อ resource/tool ของ MCP โดย​เฉพาะ):

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

ข้อ 1 / 3

เส้นแบ่งที่แท้จริงระหว่าง resource กับ tool ตาม spec 2025-11-25 คืออะไร?