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

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

Identity — ResourceResourceข้อมูล​ที่ 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 จึง​เฝ้า​มัน​ได้ คือ​ขอรับ notifications/resources/updated เมื่อ​ข้อมูล​เบื้อง​หลัง​เปลี่ยน (resources, MCP spec 2026-07-28) นี่​คือ​ความ​สามารถ​ที่ tool ไม่มี​เลย: tool เป็นการ​เรียก​แล้ว​จบ​เป็น​ครั้งๆ ไม่มี​สาย “คอย​ฟัง​การ​อัปเดต” ใน domain ฟู้ด​เดลิ​เว​อรี resource orders://8842 ที่ subscribe ไว้​จะ​ดัน​สถานะ​ใหม่​เข้า context ของ ops-copilot เอง​เมื่อ​ออ​เด​อร์​ขยับ​จาก “กำลัง​ปรุง” เป็น “ออก​ส่ง” โดยที่ model ไม่​ต้อง​วน​ถาม​ซ้ำ — เป็น​บริบท​สด ที่ app เป็น​คน​คุม​จังหวะ ไม่ใช่ model

ชื่อ​ของ​กลไก​เปลี่ยน​ไป​แล้ว​หนึ่ง​รอบ — แต่​ข้อ​ที่​บท​นี้​สอน​ไม่​เปลี่ยน

ฉบับ 2025-11-25 ให้ host เรียก resources/subscribe ทีละ resource แล้ว​เลิก​เฝ้า​ด้วย resources/unsubscribe · ฉบับ 2026-07-28 ถอด​สอง method นั้น​ทิ้ง แล้ว​รวม​การ​รับ notification ที่ server เป็น​ฝ่าย​ส่ง ไว้​ที่​สาย​เดียว​ชื่อ subscriptions/listen — client บอก​ว่า​จะ​รับ​ชนิด​ไหน​บ้าง (toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions) โดย​ส่ง URI ที่​อยาก​เฝ้า​ไป​ใน notifications.resourceSubscriptions แล้ว server ตอบ​รับ​และ​ติด io.modelcontextprotocol/subscriptionId ให้ notification ที่​ตาม​มา (resources, MCP spec 2026-07-28)

สิ่ง​ที่ไม่เปลี่ยน​มี​สาม​อย่าง: server ยัง​ประกาศ​ความ​สามารถ​ด้วย resources: { subscribe: true } เหมือน​เดิม · notification ที่​ได้​ยัง​ชื่อ notifications/resources/updated เหมือน​เดิม · และ​เหตุผล​ที่ resource เฝ้า​ได้​ส่วน tool เฝ้า​ไม่​ได้ ยัง​เป็น​เหตุผล​เดิม คือ resource มี identity ส่วน tool เป็นการ​เรียก​แล้ว​จบ​เป็น​ครั้งๆ

ชื่อ method คือ​ชั้น​ที่​เปลี่ยน ส่วน​ชั้น​ที่​บท​นี้​สอน​ไม่​ขยับ​เลย — นี่​คือ​กรณี​ตัวอย่าง​จริง​ของ​สิ่ง​ที่​บท​ที่ 1 ประกาศ​ไว้​ว่า​คอร์ส​นี้​จงใจ​สอน​ชั้น​ไหน และ​บท​ที่ 7 จะ​กลับ​มา​นับ​ให้​ดู​ว่า​ฉบับ​ใหม่​ทำให้​คอร์ส​นี้​ต้อง​แก้​กี่​ข้อ

จุด​สุดท้าย​ที่​พลาด​กัน​บ่อย​เป็น​เรื่อง​ของ คำ​กริยา​ช่วย​เชิง​บรรทัดฐาน (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 คืออะไร?