Resources vs tools — เลือกให้ถูก
บทที่ 3 มอง tool เป็น ACL — ชั้นแปลที่รักษา model ของ agent ไม่ให้ปนเปื้อนความรกของ backend แต่พอลงมือออกแบบ server จริง คำถามแรกที่ค้างคาไม่ใช่ “จะตั้งชื่อ tool ว่าอะไร” — มันคือ “ของชิ้นนี้ควรเป็น tool หรือ resource กันแน่” และนี่คือจุดที่คอนเทนต์ MCP ส่วนใหญ่พาไปผิดทาง ด้วยกฎง่ายๆ ที่ฟังดูสมเหตุสมผลแต่ผิด: “อ่านใช้ resource เขียนใช้ tool” บทนี้ทั้งบทมีหน้าที่เดียว คือลบกฎนั้นทิ้ง แล้วแทนด้วยเส้นแบ่งที่ spec พูดจริง
คอร์สนี้เป็น คอร์สออกแบบ — 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 คือคนกดสวิตช์ อ่านเหมือนกันทั้งคู่ ต่างกันแค่ “มือใครอยู่บนสวิตช์”
resource = GET (อ่าน) ← ผิด: ไปยึดกริยา read/writetool = POST (เขียน) ← ผิด: ไปยึดกริยา read/write# เส้นแบ่งจริงคือ "ใครควบคุมการเรียก" ไม่ใช่ read/write หรือ GET/POST# find_order อ่านอย่างเดียว แต่เป็น tool ได้ ถ้าอยากให้ model เรียกเอง# แบบ Tool (Model-controlled): model ตัดสินใจเรียกเองระหว่างให้เหตุผลtool find_order(orderId) -> Order # tools/call · มี human gate (SHOULD)
# แบบ Resource (App-driven): app/ผู้ใช้เลือกแนบเข้า contextresource orders://{orderId} # resources/read ตามรูปแบบ URIsubscribe 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 ไม่มี: identity + subscription
หัวข้อที่มีชื่อว่า “Resource มีอะไรที่ tool ไม่มี: identity + subscription”ถ้าเส้นแบ่งคือ “ใครควบคุม” แล้ว 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 จึง resources/subscribe กับมันได้ แล้วรับ notifications/resources/updated เมื่อข้อมูลเบื้องหลังเปลี่ยน (resources, MCP spec 2025-11-25) นี่คือความสามารถที่ tool ไม่มีเลย: tool เป็นการเรียกแล้วจบเป็นครั้งๆ ไม่มีสาย “คอยฟังการอัปเดต” ใน domain ฟู้ดเดลิเวอรี resource orders://8842 ที่ subscribe ไว้จะดันสถานะใหม่เข้า context ของ ops-copilot เองเมื่อออเดอร์ขยับจาก “กำลังปรุง” เป็น “ออกส่ง” โดยที่ model ไม่ต้องวนถามซ้ำ — เป็นบริบทสด ที่ app เป็นคนคุมจังหวะ ไม่ใช่ model
human-in-the-loop บน tool เป็น SHOULD ไม่ใช่ MUST
หัวข้อที่มีชื่อว่า “human-in-the-loop บน tool เป็น SHOULD ไม่ใช่ MUST”จุดสุดท้ายที่พลาดกันบ่อยเป็นเรื่องของ คำกริยาช่วยเชิงบรรทัดฐาน (modal verb) หน้า tools บอกว่า “ควร (SHOULD) มีมนุษย์อยู่ในวงจรเสมอ โดยมีความสามารถปฏิเสธการเรียกใช้เครื่องมือได้” (tools, MCP spec 2025-11-25) คำสำคัญคือ SHOULD ไม่ใช่ MUST
ความต่างนี้ไม่ใช่การจับผิดคำ มันเปลี่ยนสิ่งที่เราพูดได้จริง: implementation ที่ ตรงตาม spec สามารถละเว้น human gate ได้ — มันแค่ไม่ใช่แนวปฏิบัติที่ดี spec ไม่ได้ รับประกัน ว่าจะมีมนุษย์คอยกดอนุมัติทุกการเรียก tool ดังนั้นอย่าออกแบบระบบโดยคิดว่า “MCP การันตี human gate ให้แล้ว” — ถ้าความปลอดภัยของคุณต้องพึ่งมนุษย์อนุมัติ คุณต้องเป็นคนสร้างชั้นนั้นขึ้นมาเอง ไม่ใช่รอให้โพรโทคอลยกให้
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 โดยเฉพาะ):
- tools (MCP spec 2025-11-25) — tool เป็น model-controlled และ human-in-the-loop เป็น SHOULD
- resources (MCP spec 2025-11-25) — resource เป็น app-driven มี URI/Resource Template และ subscribe → updated
- คอร์ส Agent as Bounded Context — เลนส์ DDD (tool เป็น ACL, server surface เป็น published language) ที่การเลือก primitive ต่อยอด
เช็กความเข้าใจ — บทที่ 4
ข้อ 1 / 3เส้นแบ่งที่แท้จริงระหว่าง resource กับ tool ตาม spec 2025-11-25 คืออะไร?