
Quick Answer: แคชคำตอบของ RAG ควรแยกตามบริบทที่ทำให้ผลลัพธ์หรือสิทธิ์เปลี่ยน และต้องตรวจสิทธิ์ปัจจุบันของเอกสารทุกชิ้นที่คำตอบอ้างถึงก่อนส่งคำตอบจาก cache กลับไปให้ผู้ใช้ คีย์ที่มี tenant ช่วยแยกช่องเก็บ แต่ไม่ได้พิสูจน์ตัวตนหรืออนุญาตให้อ่านข้อมูล
ถ้าระบบค้นเอกสารให้พนักงานสองทีม การเพิ่ม cache เพื่อให้ตอบเร็วขึ้นอาจสร้างช่องทางข้ามสิทธิ์โดยไม่ตั้งใจ เส้นทางปกติมักกรองเอกสารก่อนนำเนื้อหาไปสร้างคำตอบ แต่ cache hit สามารถข้ามขั้นค้นและกรองนั้นได้ บทเรียนนี้จำกัดอยู่ที่ทักษะเดียว: ออกแบบ cache-aside สำหรับคำตอบ RAG ที่เก็บรหัสแหล่งข้อมูลไว้ และเรียกการตรวจสิทธิ์ปัจจุบันทุกครั้งที่พบ cache hit
ผลลัพธ์การเรียนรู้
เมื่อจบบทนี้ ผู้อ่านจะวาดเส้นทาง miss/hit/deny ได้ สร้างคีย์ตัวอย่างจาก trusted scope และ query แบบ exact bytes ได้ และอธิบายได้ว่าเหตุใด hit จึงต้องตรวจสิทธิ์เอกสารอีกครั้งก่อนคืนข้อความ คำว่า trusted scope ในตัวอย่างหมายถึงค่าที่เซิร์ฟเวอร์สมมติว่าได้มาจากระบบยืนยันตัวตนและนโยบายที่เชื่อถือได้ ไม่ใช่ tenant ที่ผู้ใช้ส่งมาเอง
คำถามที่ต้องแยกให้ออก
บทความนี้พูดถึง application RAG answer cache หรือ cache ระดับแอปพลิเคชันที่เก็บคำตอบซึ่งประกอบจากผลค้นคืนแล้ว ไม่ใช่ cache ของ prompt หรือ KV state จากผู้ให้บริการโมเดล ไม่ใช่ HTTP intermediary cache ไม่ใช่ cache ผล vector search และไม่ใช่การ cache ผลตัดสินสิทธิ์ การแยกคำเหล่านี้สำคัญ เพราะผู้ดูแลแต่ละชั้นอาจมีขอบเขตความเชื่อถือ อายุข้อมูล และกลไก invalidation คนละแบบ
สมมติว่า query “ขั้นตอนหมุนเวียนกุญแจบริการคืออะไร” ทำให้ระบบพบเอกสาร doc-17 แล้วได้คำตอบหนึ่งข้อความ รอบถัดไปถ้า cache พบ query เดิม ระบบไม่ต้องเรียก retrieval และ model ซ้ำ แต่ยังต้องรู้ว่าใครถาม อยู่ใน tenant ใด policy ปัจจุบันอนุญาตให้อ่าน doc-17 หรือไม่ และข้อมูลที่ใช้สร้างคำตอบยังเป็น revision เดิมหรือไม่ หากตอบไม่ได้ให้ปฏิเสธการคืน cache หรือไปสร้างคำตอบใหม่ภายใต้ผลค้นและสิทธิ์ปัจจุบันตามนโยบายที่ระบบกำหนด

คีย์ช่วยเลือกช่อง แต่ไม่ใช่ด่านอนุญาต
OWASP แนะนำให้ cache ระดับแอปพลิเคชันบังคับ authorization ทุก request ก่อนส่งข้อมูลที่ cache ไว้ และเตือนว่า cache hit ต้องไม่ข้าม object-level หรือ tenant-level authorization แนวทาง multi-tenant ยังย้ำว่าบริบท tenant ควรผูกกับตัวตนและ membership ที่เซิร์ฟเวอร์ตรวจสอบแล้ว ส่วน tenant ID จาก header หรือ parameter ที่ผู้ใช้ควบคุมไม่ใช่หลักฐานสิทธิ์ ดังนั้น tenant-a: ที่แปะไว้หน้าคีย์เป็นเพียงชื่อช่องทาง หากแอปเชื่อค่าจากผู้ใช้ตรง ๆ การแยกด้วย prefix ก็ไม่ช่วยยืนยันว่าผู้เรียกเป็นสมาชิก tenant-a
Microsoft อธิบาย shared cache ว่า prefix ช่วยแยกชื่อ key ได้ แต่แอปที่ใช้ cache ร่วมกันยังรับผิดชอบการแยกข้อมูลและ authorization เอง จุดนี้เป็นหลักฐานชัดว่าการวาง tenant ไว้ในคีย์กับการตรวจสิทธิ์เป็นคนละหน้าที่ กรณี cache แบบ HTTP ใน RFC 9111 เป็นเพียงอุปมาเรื่องการใช้มิติของ request ที่มีผลต่อ representation และการตรวจสอบความสด ไม่ใช่สูตรคีย์สำหรับ RAG หรือข้อกำหนดด้านสิทธิ์ของคำตอบ RAG
ตัวอย่างนี้ใช้คีย์แบบ versioned canonical tuple ที่มี tenant ID, principal ID, policy revision, corpus revision, retrieval profile และ digest ของ query bytes ที่แน่นอน มิติเหล่านี้เป็นข้อเสนอเชิงวิศวกรรมสำหรับตัวอย่าง ไม่ใช่ schema บังคับจากมาตรฐาน ตัวอย่างสมมติว่า profile บอก configuration ของเส้นทางสร้างคำตอบด้วย หาก model, prompt template หรือภาษาคำตอบเปลี่ยน ต้องเปลี่ยน profile หรือเพิ่มมิติในคีย์ด้วย ทุกระบบต้องพิจารณามิติอื่นที่มีผลต่อคำตอบหรือสิทธิ์ด้วย เช่น namespace ของแอปหรือ filter ที่เปลี่ยนชุดเอกสาร หากละมิติสำคัญ ผลที่ต่างกันอาจชนคีย์เดียวกัน
ทางเดินของ cache hit
บน cache miss ระบบดำเนิน retrieval ตามตัวกรองที่ได้รับอนุญาต สร้างคำตอบ เก็บข้อความพร้อม source_ids และมิติ revision ที่ใช้ แล้วส่งคำตอบตามปกติ ส่วน hit จะอ่าน entry จาก key ที่คำนวณด้วย trusted context และ query เดิม จากนั้นตรวจว่า scope กับ digest ภายใน entry ตรงกับ request ตรวจว่า entry ยังไม่หมดอายุ และถาม authorization layer ปัจจุบันเกี่ยวกับทุก source ID ที่ entry ระบุว่ามีส่วนสร้างคำตอบ
หาก source ID สักรายการถูกเพิกถอน หรือ policy service ใช้งานไม่ได้ ให้ไม่คืนข้อความที่ cache ไว้ การตอบ fail closed ในตัวอย่างหมายถึงเลือกผลปลอดภัยเมื่อไม่สามารถพิสูจน์เงื่อนไขที่ต้องมีได้ ในระบบจริงอาจเลือกเริ่ม retrieval ใหม่ภายใต้สิทธิ์ปัจจุบันแทนการตอบ deny ได้ แต่ต้องนิยามให้ชัดว่าการคำนวณใหม่จะกรองก่อนอ่านเนื้อหาอย่างไร และต้องไม่เผลอส่งคำตอบเก่าก่อนตรวจสิทธิ์

ทำไมเก็บ source IDs และตรวจทุกชิ้น
ข้อความคำตอบที่ประกอบจากหลายเอกสารอาจรวมข้อมูลที่ผู้ใช้มีสิทธิ์อ่านบางส่วนกับข้อมูลที่ถูกถอนสิทธิ์ไปแล้ว การตรวจเพียงว่า “ผู้ใช้อยู่ใน tenant เดิม” ไม่ได้บอกว่าเขายังอ่านเอกสารแต่ละชิ้นได้หรือไม่ การเก็บรหัสเอกสารที่ระบบใช้เป็นแหล่งประกอบคำตอบทำให้ gate มีรายการที่จะตรวจได้ หากรายการใดไม่อยู่ใน current allowed set ก็ห้ามคืนข้อความทั้งก้อน เพราะเราไม่สามารถแยกได้อย่างน่าเชื่อถือว่าส่วนใดของคำตอบพึ่งพาเอกสารนั้น
มีสมมติฐานสำคัญที่โค้ดตัวอย่างพิสูจน์ไม่ได้: source_ids ต้องครบและตรงกับเอกสารที่มีผลต่อคำตอบจริง หาก retrieval pipeline ลืมบันทึกแหล่งหนึ่ง gate ก็ไม่มีทางตรวจสิทธิ์แหล่งที่หายไปได้ การเก็บ ID จึงเป็นการพกพา provenance ที่ระบบประกาศ ไม่ใช่การตรวจ semantic provenance หรือพิสูจน์ว่าโมเดลใช้เฉพาะแหล่งดังกล่าว
ค่า policy revision ในคีย์ทำให้ policy รุ่นใหม่สร้าง cache miss ลดโอกาสนำคำตอบจากกติกาเก่ามาใช้ แต่ revision อาจไม่ถูกเพิ่มทันทีเมื่อ membership เปลี่ยน หรือผู้ให้บริการนโยบายอาจมีการแพร่การเปลี่ยนแปลงล่าช้า จึงยังต้องถาม authorization ปัจจุบันเมื่อ hit ทุกครั้ง การเพิกถอนที่เกิดพร้อมกับ request ต้องมี consistency contract ในระบบจริง เช่น ต้องการ linearizable check หรือยอมรับหน้าต่างความล่าช้าเท่าใด ตัวอย่างนี้ไม่ได้รับประกันเรื่อง race หรือ revocation freshness

ตัวอย่างโค้ด: mock gate ขนาดเล็ก
ไฟล์ code/cache_gate.py เป็นตัวอย่าง Python 3.12 ใช้เฉพาะ standard library โครงสร้าง TrustedScope รับค่าตัวตนและ revision จาก fixture ฝั่งเซิร์ฟเวอร์ ฟังก์ชัน cache_key สร้าง JSON แบบจัดลำดับคีย์และ separator คงที่ก่อน hash เพื่อให้ตัวแทนข้อมูลเหมือนเดิมให้ digest เดิม ส่วน query ถูก hash จาก bytes ตรง ๆ โดยไม่ normalize ช่องว่างหรือ Unicode ให้เท่ากันโดยปริยาย
การ hash ไม่ได้เข้ารหัส query และไม่ได้ทำให้ query ที่เดาได้ยากขึ้น การใช้ SHA-256 ในที่นี้เป็นวิธีสร้าง digest สำหรับ lookup เท่านั้น ไม่ควรใส่ bearer token หรือ secret ในคีย์หรือ log โค้ดไม่ log query และจำกัด query ไว้ไม่เกิน 16 KiB, scope string ไม่เกิน 256 ตัวอักษร, คำตอบไม่เกิน 65,536 ตัวอักษร, source ID ไม่เกิน 128 รายการ และ TTL ต้องเป็นค่าบวกและ finite
ฟังก์ชัน put เก็บ entry ใน dict พร้อมเวลา expiry ที่คำนวณจาก now ซึ่งผู้เรียกฉีดเข้ามา ส่วน get_authorized คืนคำตอบได้เมื่อ scope และ query digest ตรง entry ยังไม่หมดอายุ และ source IDs ทั้งหมดเป็น subset ของ current allowed source IDs หากไม่มี scope หรือ current authorization context จะคืน None แทนการเปิดทาง ตัวอย่างทดสอบที่เขียนไว้ครอบคลุม scope ต่างกัน, revision ต่างกัน, การเพิกถอน, empty allow-list, expiry และ entry ที่ผิดรูป
ขอบเขตจำลอง: dict และเวลา monotonic เป็นของ process เดียว ไม่มีฐานข้อมูล ไม่มี distributed invalidation ไม่มี lock หรือการรับประกันเรื่อง race ไม่มี identity provider, policy engine, vector database, model หรือ cache service จริง การทดสอบยังไม่ถูกรัน สถานะการทดสอบจึงเป็น NOT RUN และไม่ควรตีความ acceptance examples เป็นผลทดสอบ runtime
งานฝึกที่คัดลอกไปทำได้
เป้าหมาย: สร้าง mock RAG answer cache ที่ตรวจสิทธิ์ source ทุกชิ้นก่อนคืน HIT
ข้อมูล/เครื่องมือ: Python 3.12+ standard library; trusted scope, query UTF-8 bytes, policy snapshot และ fixtures
สิทธิ์/ขอบเขต: offline process memory; ห้าม auth/provider/API/DB/network/deploy จริง
สิ่งส่งมอบ: key, cache entry, get_authorized และ acceptance cases พร้อม expected HIT/MISS/DENY
เกณฑ์ตรวจรับ: เปลี่ยน tenant/principal/revision/profile/query แล้ว MISS; ถอนสิทธิ์ source หรือไม่มี context แล้วไม่คืนคำตอบ; ห้าม source list ว่าง
จุดหยุด: จบที่ bounded mock และสมมติฐาน provenance/TTL/race; tests NOT RUN จนมีผลรันจริง
เกณฑ์รับงานและจุดหยุด
งานผ่านเมื่อ same scope และ input เดิมคืน mock hit ได้; tenant หรือ principal ต่างกันไม่ reuse entry; policy, corpus หรือ retrieval profile revision ที่เปลี่ยนทำให้ miss; การถอนสิทธิ์ source ใด source หนึ่งทำให้คำตอบไม่ถูกคืน; missing authorization context และ cache entry ที่หมดอายุหรือผิดรูปต้อง deny; คีย์ไม่มี credential หรือ secret; และคำอธิบายแยก simulation จากการทดสอบจริงชัดเจน
หยุดเมื่อกรณี hit หนึ่งกรณีและกรณี miss/deny ที่กำหนดแสดงได้ครบพร้อม assumptions และ failure path ไม่ต้องเพิ่ม provider จริง, vector database, model API, deployment หรือ distributed cache เพื่อให้แบบฝึกนี้ “ดู production” ขึ้น เพราะสิ่งเหล่านั้นเปลี่ยนโจทย์และต้องมีสิทธิ์ ข้อมูล และข้อตกลงทดสอบคนละชุด
ฝึกเอง
เปลี่ยนทีละค่าและทำนายผลก่อนดู code: tenant, principal, policy revision, corpus revision, retrieval profile และ query bytes ควรทำให้เกิด hit หรือ miss อย่างไร จากนั้นคงคีย์เดิมไว้แต่เอา doc-17 ออกจาก allowed set แล้วทำนายว่า hit path จะคืนคำตอบหรือไม่ ปิดท้ายด้วยการเอา current policy context ออก ลองอธิบายว่าทำไมการเปลี่ยน hash คีย์อย่างเดียวจึงไม่แทนการตรวจสิทธิ์ปัจจุบัน
RAG Answer Cache Flow Lab
ภาพ interactive ที่แยกจาก Video Lab ให้ผู้เรียนเลือก tenant/principal, policy และ corpus revision, retrieval profile, query digest และ grant ของ source IDs แล้วแสดงผลเป็น hit, miss หรือ deny พร้อมเหตุผล เมื่อเพิกถอนแหล่งข้อมูลให้เห็นว่า cache key เดิมยังมีอยู่ได้ แต่ gate ปฏิเสธการคืนคำตอบ Lab นี้ติดป้ายว่าเป็น synthetic simulation ไม่ใช่การทดสอบ security ของระบบจริง ควบคุมทุกช่องได้ด้วยคีย์บอร์ด มี label ที่อ่านได้ มี reduced-motion mode และมีตารางข้อความเป็น fallback เมื่อ JavaScript หรือ canvas ใช้งานไม่ได้
ภาพจำลองแบบโต้ตอบ · ข้อมูลสังเคราะห์
แคชคำตอบ RAG: แบ่งขอบเขต แล้วตรวจสิทธิ์แหล่งข้อมูลทุกครั้ง
ลองเปลี่ยนมิติของคีย์เพื่อดูว่าแคชจะพลาดเมื่อใด จากนั้นตรวจสิทธิ์ปัจจุบันก่อนคืนคำตอบจาก HIT
ขอบเขต: เป็นตัวอย่างจำลอง ไม่มีการยืนยันตัวตน ฐานข้อมูล เวกเตอร์สโตร์ หรือแคชจริง คีย์ใช้ชื่อมิติแทน digest จริง ไม่มีการวัดเวลา คะแนนความปลอดภัย หรือการรับประกัน ระบบสมมติว่า source IDs ครบและเชื่อถือได้
v1 | tenant-A | principal-1 | policy-r7 | corpus-r12 | profile-hybrid-2 | query-digest
แสดงมิติเท่านั้น ไม่คำนวณหรือเปิดเผย hash จริง
HIT จำลอง: คีย์ตรงและยังไม่หมดอายุ แต่ต้องตรวจ source IDs กับสิทธิ์ปัจจุบันก่อนคืนคำตอบ
เกิดข้อผิดพลาดในตัวอย่าง กรุณาอ่านคำอธิบายข้อความและตารางด้านล่าง
การทดลองแยก · คีย์เดิม
HIT เดิมก็ต้องตรวจสิทธิ์ใหม่
คงคีย์และรายการเดิมไว้ แล้วเปลี่ยนสิทธิ์ปัจจุบันของเอกสารแต่ละรายการ สังเกตว่าคีย์ไม่เปลี่ยน แต่คำตอบถูกปฏิเสธเมื่อ source ID ใดอ่านไม่ได้
รายการ HIT สังเคราะห์มี source IDs: source-A และ source-B
คีย์และรายการยังเดิม: ตรวจ source-A และ source-B กับสิทธิ์ปัจจุบัน ทั้งคู่ผ่าน จึงคืนคำตอบสังเคราะห์ได้
entry-key: baseline v1 (คงเดิม)
ผลลัพธ์ข้อความและสมมติฐาน
เมื่อคีย์ต่างหรือหมดอายุ ให้ไปเส้นทาง MISS เพื่อค้นคืนและสร้างรายการใหม่ตามนโยบายระบบจริง ตัวอย่างนี้ไม่จำลองการค้นคืน โมเดล หรือการเขียนแคช
การตรวจสิทธิ์ครอบคลุมเฉพาะ source IDs ที่บันทึกไว้ และสมมติว่ารายการเหล่านั้นครบถ้วน หาก provenance ขาดหาย หรือระบบสิทธิ์ล้าสมัย การทดลองนี้ตรวจไม่พบปัญหานั้น
คีย์เป็นตัวช่วยแบ่งขอบเขตและค้นรายการ ไม่ใช่หลักฐานยืนยันตัวตนหรืออนุญาต การ hash ข้อความค้นหาไม่ใช่การเข้ารหัส และตัวอย่างนี้ไม่รับรองการจัดการ race condition หรือการเผยแพร่การเพิกถอนใน production
| เงื่อนไข | ผลตามแบบจำลอง |
|---|---|
| ทุกมิติตรงและยังไม่หมดอายุ | HIT แล้วตรวจสิทธิ์ของทุก source ID |
| tenant / principal / revision / profile / query เปลี่ยน | คีย์ต่าง จึง MISS |
| ขาดบริบทนโยบายหรือรายการหมดอายุ | DENY หรือ MISS; ไม่คืนคำตอบ HIT |
| source ใด source หนึ่งถูกเพิกถอน | คีย์เดิม แต่ปฏิเสธคำตอบ |
Video Lab: วางแผน brief ของวิดีโอ RAG อย่างมีขอบเขต
ใช้ Video Lab ที่มีอยู่เพื่อฝึกเขียน brief สำหรับวิดีโอสอนเส้นทาง miss/hit/deny ของ RAG โดยระบุ tenant ที่เชื่อถือได้, policy revision, corpus revision, retrieval profile, ตัวอย่าง source IDs, จุด reauthorize และเงื่อนไขหยุดเมื่อ context หาย อธิบายให้ชัดว่า Lab นี้ช่วยวางแผนลำดับการเล่าและ prompt ของวิดีโอเท่านั้น ไม่ได้จำลอง cache, ตรวจ ACL, ยืนยัน provenance หรือรับรองสิทธิ์ของปลั๊กอิน อย่านำผลจาก Lab ไปอ้างเป็นการทดสอบระบบจริง
ลองตั้งหัวข้อวิดีโอเกี่ยวกับการใช้คำตอบ RAG เดิมในโครงการที่ได้รับอนุญาต แล้วตรวจ prompt ที่ Lab สร้างเทียบ brief หกส่วนด้านบน: ระบุ project/policy revision และจุดตรวจ source IDs ก่อน reuse หรือไม่ หากขาดให้เติมจาก brief เอง ยังไม่ได้ทดสอบผล prompt หรือปุ่มบนหน้าเว็บจริง
จากคลิปดิบ
สู่จักรวาลคอนเทนต์
ChatGPT ตัดต่อวิดีโอแทนคนได้จริงแค่ไหน? สำรวจระบบ AI Video Editing Agent ผ่านฉากสามมิติที่โต้ตอบได้ พร้อมแยกข้อเท็จจริง สร้างพรอมป์ต์ และจำลองต้นทุนก่อนลงมือทำจริง
ออกแบบระบบตัดต่อในมุมมอง 3D
ลากบนฉากเพื่อหมุนมุมมอง แตะโหนดเพื่อดูหน้าที่ของ AI ในแต่ละขั้นตอน
จัดระเบียบไฟล์ ตรวจคุณภาพภาพและเสียง และกำหนดสิทธิ์เข้าถึงให้ AI Agent
แยกความสามารถจริงจากคำโฆษณา
กดแต่ละประเด็นเพื่ออ่านรายละเอียดและแหล่งข้อมูล
ChatGPT Work เข้าถึงไฟล์ในเครื่องได้ตรวจสอบแล้ว / มีเงื่อนไข+
Astra ใช้โปรแกรมคอมพิวเตอร์ทำงานได้ตรวจสอบแล้ว / ไม่ใช่โปรแกรมตัดต่อในตัว+
ตัดเสร็จใน 5–10 นาทีทุกกรณียังพิสูจน์เป็นเกณฑ์ทั่วไปไม่ได้+
ถูกกว่าจ้างคน 10 เท่าต้องกำหนดต้นทุนให้เทียบกันได้+
หนึ่งไอเดีย สามรูปแบบคอนเทนต์
ปรับรูปแบบวิดีโอ แล้วดูกรอบภาพตัวอย่างและ Prompt เปลี่ยนตามแบบเรียลไทม์
ช่วยตัดต่อ?
พร้อมนำไปใช้กับ AI Agent
ตัวสร้าง Prompt นี้ไม่อัปโหลดคลิป ไม่เรียก API เสียเงิน และไม่ตัดต่อวิดีโอจริง ต้องใช้กับเครื่องมือที่ผู้ใช้อนุญาตภายหลัง
จำลองเวลาและต้นทุนของคุณเอง
เลื่อนตัวเลขเพื่อเปรียบเทียบสถานการณ์สมมติ ไม่ใช่ผล benchmark ที่ยืนยันแล้ว
ตัวเลขเป็นตัวอย่างเพื่อช่วยตัดสินใจ ไม่ใช่ผลประหยัดที่รับประกัน
AI ที่ตัดคลิปเป็น ไม่ได้ทำงานลำพัง
สิ่งที่น่าสนใจของ GPT-6 Astra และ GPT-6.1 Sol ไม่ได้อยู่ที่ว่าตัวโมเดลมีปุ่ม Render MP4 ซ่อนอยู่ แต่คือความสามารถในการจัดการงานหลายขั้นตอนร่วมกับเครื่องมืออื่น: อ่านไฟล์ วางลำดับเรื่อง ใช้แอป ตรวจผลลัพธ์ และแก้งานตามข้อเสนอแนะของมนุษย์
สำหรับคนทำคอนเทนต์ ความต่างสำคัญคือการเปลี่ยนจาก “ส่งคลิปให้ AI แล้วหวังว่าจะได้ผลงาน” ไปเป็น Workflow ที่ตรวจรับได้ทุกขั้น ตั้งแต่การเลือกช่วงเด็ด การสร้างซับ การตรวจคำผิด จนถึงการเรนเดอร์ไฟล์ตามสัดส่วนของแต่ละแพลตฟอร์ม
ข้อควรระวังคือระบบ Work ที่รันบนเว็บหรือมือถือไม่ได้หมายความว่า AI เข้าถึงคลิปบนคอมพิวเตอร์บ้านได้ทันที ต้องทำงานในสภาพแวดล้อมที่เปิดให้เข้าถึงไฟล์จริง มีเครื่องมือตัดต่อ และมีสิทธิ์ใช้งานอย่างชัดเจน
รวบรวมไฟล์ในโฟลเดอร์ที่เข้าถึงได้ ระบุสิทธิ์และข้อจำกัด
ให้ Agent เสนอ Hook, ช่วงตัด และโครงเรื่องก่อนตัดต่อ
ตรวจชื่อเฉพาะ ตัวเลข ภาษาไทย และความถูกต้องของคำกล่าวอ้าง
ตรวจความละเอียด เสียง ระยะเวลา safe zone และคำบรรยาย
Brief สำหรับ Lab: ทำวิดีโออธิบาย RAG answer-cache hit gate ด้วยข้อมูลสมมติ แสดง key partition, lookup, current authorization ของ source IDs และผล deny เมื่อแหล่งหนึ่งถูกเพิกถอน ห้ามใช้ข้อมูลจริงหรือเรียก API จริง ระบุข้อจำกัดเรื่อง source provenance และ race condition ในฉากสรุป
คำตอบสั้นสำหรับทบทวน
คีย์ช่วยไม่ให้ผลของ tenant, principal, policy, corpus, retrieval profile หรือ query ที่ต่างกันมาชนกันโดยตั้งใจ ส่วน current per-source authorization เป็นด่านที่ตัดสินว่าคำตอบที่พบใน cache ยังส่งให้ผู้เรียกคนนี้ได้หรือไม่ ทั้งสองอย่างทำคนละหน้าที่ และไม่มีอย่างใดพิสูจน์ว่า source list ครบถ้วนหรือข้อความถูกต้อง
คำถามที่พบบ่อย
ถ้าใส่ tenant ID ในคีย์แล้ว ยังต้องตรวจสิทธิ์หรือไม่?
ต้องตรวจ เพราะ tenant ID อาจเป็นเพียง label และ membership หรือสิทธิ์ระดับเอกสารเปลี่ยนได้ การตรวจต้องอิงตัวตนที่เซิร์ฟเวอร์ยืนยันและ policy ปัจจุบัน
ใช้ principal ID แทน tenant ได้ไหม?
ในตัวอย่างมีทั้งคู่เพื่อแสดงการแยกขอบเขต ตัวจริงอาจใช้ authorization-equivalence class ได้ถ้าพิสูจน์ได้ว่าสมาชิกทุกคนมีผลลัพธ์และสิทธิ์เทียบเท่ากัน การจัดกลุ่มแบบนั้นเป็นการตัดสินใจเฉพาะแอป ห้ามสมมติว่า tenant เดียวกันแปลว่าสิทธิ์เท่ากัน
ทำไม policy revision ไม่พอ?
revision ช่วยทำให้ cache entry เก่ากลายเป็น miss เมื่อระบบเพิ่ม revision แต่การเปลี่ยน membership อาจยังไม่สะท้อน revision หรือมี propagation delay การตรวจ source IDs ด้วยข้อมูลสิทธิ์ปัจจุบันยังจำเป็น และระบบจริงต้องกำหนดความสดและ consistency ที่ยอมรับ
query hash ซ่อนเนื้อหาได้หรือไม่?
ไม่ hash เป็น digest ไม่ใช่ encryption หาก query มีค่าน้อยหรือเดาได้ ผู้สังเกตอาจเดาแล้วเทียบ digest ได้ จึงไม่ควรบันทึก query หรือ secret ลง log หรือมอง digest เป็นมาตรการรักษาความลับ
ถ้า policy service ล่มควรทำอย่างไร?
ตัวอย่างปฏิเสธ cache hit เพราะตรวจสิทธิ์ไม่ได้ ระบบอาจคืน error หรือทำ retrieval ใหม่ผ่านทางที่ยังบังคับ policy ได้ตามข้อตกลง แต่ไม่ควรคืนคำตอบเก่าราวกับอนุญาตแล้ว
ตัวอย่างนี้พิสูจน์ว่า cache ของเราปลอดภัยแล้วหรือยัง?
ยังไม่ได้ ตัวอย่างเป็น mock ในหน่วยความจำที่จำลองการตัดสินใจ ไม่ได้ทดสอบระบบ identity, policy, provenance, concurrency, cache backend หรือการเผยแพร่ revocation จริง และไม่ได้ตรวจความถูกต้องของคำตอบ
อ่านต่อ
- OWASP Web Cache Security Cheat Sheet — การบังคับ authorization ก่อนคืนข้อมูลจาก application cache
- OWASP Multi-Tenant Security Cheat Sheet — การผูก tenant context กับ identity และ membership ที่ตรวจสอบแล้ว
- Microsoft Azure Managed Redis: multitenant service guidance — ขอบเขตของ shared cache และความรับผิดชอบด้าน separation/authorization
- RFC 9111: HTTP Caching — ใช้เป็นอุปมาเรื่องการจับคู่ request dimensions เท่านั้น ไม่ใช่ข้อกำหนดคีย์ RAG
- Python hashlib documentation — API สำหรับ digest ของ bytes; digest ไม่ได้เข้ารหัสข้อมูล
- RAG: ตรวจสิทธิ์เอกสารก่อน hydration — ด่านก่อนนำเนื้อหาเอกสารเข้าบริบท
- RAG citation gate กับ revision ปัจจุบัน — ตรวจหลักฐาน citation และ revision ของเอกสาร
- Prompt cache 5 นาทีเทียบ 1 ชั่วโมง — คนละชั้นกับ application answer cache ในบทนี้
Source Code และวิธีลองด้วยตนเอง · NOT RUN
code/cache_gate.py · ยังไม่ได้รัน
"""A small, process-local RAG answer-cache authorization gate (Python 3.12+).
This module is an educational mock. It does not retrieve documents, authenticate
users, or establish that a source list is complete or semantically correct.
"""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import json
import math
from typing import Mapping
MAX_SCOPE_CHARS = 256
MAX_QUERY_BYTES = 16 * 1024
MAX_ANSWER_CHARS = 64 * 1024
MAX_SOURCE_IDS = 128
class CacheInputError(ValueError):
"""Raised when the bounded mock input contract is violated."""
def _finite_number(name: str, value: object) -> float:
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise CacheInputError(f"{name} must be a finite number")
try:
result = float(value)
except OverflowError as exc:
raise CacheInputError(f"{name} must be a finite number") from exc
if not math.isfinite(result):
raise CacheInputError(f"{name} must be a finite number")
return result
@dataclass(frozen=True)
class TrustedScope:
"""Values supplied by a trusted server-side auth/policy layer."""
tenant_id: str
principal_id: str
policy_revision: str
corpus_revision: str
retrieval_profile: str
@dataclass(frozen=True)
class CacheEntry:
"""Cached answer plus the source IDs the producing pipeline declared."""
scope: TrustedScope
query_digest: str
answer: str
declared_source_ids: tuple[str, ...]
expires_at: float
def _bounded_text(name: str, value: object, limit: int) -> str:
if not isinstance(value, str) or not value or len(value) > limit:
raise CacheInputError(f"{name} must be non-empty text of at most {limit} chars")
if "\x00" in value:
raise CacheInputError(f"{name} contains a forbidden NUL character")
return value
def _validate_scope(scope: TrustedScope) -> None:
if not isinstance(scope, TrustedScope):
raise CacheInputError("trusted scope is required")
for name in ("tenant_id", "principal_id", "policy_revision", "corpus_revision", "retrieval_profile"):
value = _bounded_text(name, getattr(scope, name), MAX_SCOPE_CHARS)
try:
value.encode("utf-8", errors="strict")
except UnicodeEncodeError as exc:
raise CacheInputError(f"{name} must be valid UTF-8 text") from exc
def query_digest(query: bytes) -> str:
"""Hash exact valid UTF-8 query bytes; intentionally performs no normalization."""
if not isinstance(query, bytes) or not query or len(query) > MAX_QUERY_BYTES:
raise CacheInputError(f"query must be 1..{MAX_QUERY_BYTES} exact bytes")
try:
query.decode("utf-8", errors="strict")
except UnicodeDecodeError as exc:
raise CacheInputError("query must contain valid UTF-8 bytes") from exc
return hashlib.sha256(query).hexdigest()
def cache_key(scope: TrustedScope, query: bytes) -> str:
"""Return a versioned digest over canonical JSON; digest is not a secret."""
_validate_scope(scope)
payload = {
"v": 1,
"tenant_id": scope.tenant_id,
"principal_id": scope.principal_id,
"policy_revision": scope.policy_revision,
"corpus_revision": scope.corpus_revision,
"retrieval_profile": scope.retrieval_profile,
"query_sha256": query_digest(query),
}
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
def _valid_source_ids(source_ids: object) -> tuple[str, ...]:
if not isinstance(source_ids, tuple) or not 1 <= len(source_ids) <= MAX_SOURCE_IDS:
raise CacheInputError(f"declared source IDs must be a tuple of 1..{MAX_SOURCE_IDS} IDs")
if any(not isinstance(item, str) or not item or len(item) > MAX_SCOPE_CHARS for item in source_ids):
raise CacheInputError("each declared source ID must be non-empty bounded text")
if len(set(source_ids)) != len(source_ids):
raise CacheInputError("declared source IDs must be unique")
return source_ids
def put(
store: dict[str, CacheEntry],
scope: TrustedScope,
query: bytes,
answer: str,
declared_source_ids: tuple[str, ...],
now: float,
ttl_seconds: float,
) -> str:
"""Store one mock answer with a bounded, injected process-local TTL."""
_validate_scope(scope)
digest = query_digest(query)
_bounded_text("answer", answer, MAX_ANSWER_CHARS)
source_ids = _valid_source_ids(declared_source_ids)
now = _finite_number("now", now)
ttl_seconds = _finite_number("TTL", ttl_seconds)
if ttl_seconds <= 0:
raise CacheInputError("now and positive TTL must be finite numbers")
expires_at = now + ttl_seconds
if now < 0 or not math.isfinite(expires_at) or expires_at <= now:
raise CacheInputError("invalid monotonic time or TTL overflow")
key = cache_key(scope, query)
store[key] = CacheEntry(scope, digest, answer, source_ids, expires_at)
return key
def get_authorized(
store: Mapping[str, CacheEntry],
scope: TrustedScope | None,
query: bytes,
current_authorized_source_ids: frozenset[str] | None,
now: float,
) -> str | None:
"""Return an answer only after a current fixture authorization check.
``None`` means miss or deny. An empty allowed-source set is valid and can
authorize only entries with no declared sources.
"""
if not isinstance(store, Mapping) or scope is None or current_authorized_source_ids is None:
return None
try:
_validate_scope(scope)
digest = query_digest(query)
except CacheInputError:
return None
try:
now = _finite_number("now", now)
except CacheInputError:
return None
if now < 0:
return None
key = cache_key(scope, query)
entry = store.get(key)
if not isinstance(entry, CacheEntry):
return None
# Defense against a wrong/corrupt mapping entry; never trust the key alone.
if entry.scope != scope or entry.query_digest != digest:
return None
try:
expires_at = _finite_number("expiry", entry.expires_at)
except CacheInputError:
return None
if now >= expires_at:
return None
if not isinstance(current_authorized_source_ids, frozenset):
return None
try:
declared_ids = _valid_source_ids(entry.declared_source_ids)
except CacheInputError:
return None
try:
answer = _bounded_text("answer", entry.answer, MAX_ANSWER_CHARS)
except CacheInputError:
return None
if not set(declared_ids).issubset(current_authorized_source_ids):
return None
return answer
code/test_cache_gate.py · ยังไม่ได้รัน
"""Authored acceptance examples for cache_gate; intentionally not executed."""
import unittest
from dataclasses import replace
from cache_gate import CacheInputError, TrustedScope, cache_key, get_authorized, put
class CacheGateTests(unittest.TestCase):
def setUp(self):
self.scope = TrustedScope("tenant-a", "user-a", "policy-4", "corpus-9", "hybrid-v2")
self.query = b"How do I rotate the service key?"
self.store = {}
put(self.store, self.scope, self.query, "Use the approved rotation runbook.", ("doc-17",), 100.0, 30.0)
self.allowed = frozenset({"doc-17", "doc-18"})
def test_same_scope_and_current_source_permission_returns_cached_answer(self):
self.assertEqual(get_authorized(self.store, self.scope, self.query, self.allowed, 101.0),
"Use the approved rotation runbook.")
def test_tenant_or_principal_change_is_a_miss(self):
for field, value in (("tenant_id", "tenant-b"), ("principal_id", "user-b")):
with self.subTest(field=field):
other = replace(self.scope, **{field: value})
self.assertNotEqual(cache_key(self.scope, self.query), cache_key(other, self.query))
self.assertIsNone(get_authorized(self.store, other, self.query, self.allowed, 101.0))
def test_policy_corpus_or_retrieval_revision_change_is_a_miss(self):
for field, value in (("policy_revision", "policy-5"), ("corpus_revision", "corpus-10"),
("retrieval_profile", "hybrid-v3")):
with self.subTest(field=field):
other = replace(self.scope, **{field: value})
self.assertIsNone(get_authorized(self.store, other, self.query, self.allowed, 101.0))
def test_exact_query_bytes_change_is_a_miss(self):
self.assertIsNone(get_authorized(self.store, self.scope, self.query + b" ", self.allowed, 101.0))
def test_current_reauthorization_denies_removed_source(self):
self.assertIsNone(get_authorized(self.store, self.scope, self.query, frozenset({"doc-18"}), 101.0))
def test_empty_allowed_set_denies_entry_with_sources(self):
self.assertIsNone(get_authorized(self.store, self.scope, self.query, frozenset(), 101.0))
def test_expired_entry_is_a_miss_at_expiry_boundary(self):
self.assertIsNone(get_authorized(self.store, self.scope, self.query, self.allowed, 130.0))
def test_inconsistent_entry_scope_is_rejected_even_under_expected_key(self):
key = cache_key(self.scope, self.query)
entry = self.store[key]
self.store[key] = replace(entry, scope=replace(self.scope, tenant_id="tenant-b"))
self.assertIsNone(get_authorized(self.store, self.scope, self.query, self.allowed, 101.0))
def test_malformed_cached_source_ids_are_rejected(self):
key = cache_key(self.scope, self.query)
self.store[key] = replace(self.store[key], declared_source_ids=["doc-17"])
self.assertIsNone(get_authorized(self.store, self.scope, self.query, self.allowed, 101.0))
def test_empty_provenance_is_rejected_when_storing_and_reading(self):
with self.assertRaises(CacheInputError):
put(self.store, self.scope, b"query two", "answer", (), 100.0, 10.0)
key = cache_key(self.scope, self.query)
self.store[key] = replace(self.store[key], declared_source_ids=())
self.assertIsNone(get_authorized(self.store, self.scope, self.query, frozenset(), 101.0))
def test_corrupt_cached_answer_is_rejected(self):
key = cache_key(self.scope, self.query)
self.store[key] = replace(self.store[key], answer="")
self.assertIsNone(get_authorized(self.store, self.scope, self.query, self.allowed, 101.0))
def test_invalid_utf8_query_is_rejected(self):
with self.assertRaises(CacheInputError):
cache_key(self.scope, b"\xff")
def test_ttl_overflow_is_rejected(self):
with self.assertRaises(CacheInputError):
put(self.store, self.scope, b"query two", "answer", ("doc-17",), 1.0e308, 1.0e308)
def test_missing_authorization_context_denies(self):
self.assertIsNone(get_authorized(self.store, self.scope, self.query, None, 101.0))
self.assertIsNone(get_authorized(self.store, None, self.query, self.allowed, 101.0))
if __name__ == "__main__":
unittest.main()
README-code.md · ยังไม่ได้รัน
# RAG cache gate mock: usage and boundaries
This teaching example demonstrates one rule: a cache key scopes a possible reuse, while current source authorization gates the answer on every hit. It uses Python 3.12+ and the standard library only.
## Runtime and setup
- **Runtime:** Python 3.12 or newer.
- **Dependencies:** none beyond the standard library.
- **Environment variables / credentials:** none.
- **Installation:** none; use an existing Python 3.12 installation. Do not install packages for this example.
- **Network, API, model, vector store, cache service, or GPU:** none.
- **Permissions:** local read-only computation and process memory only; no file, database, or network writes.
- **Cost:** no API or GPU cost. Local CPU and memory use only; no performance claim is made.
## Run the example
From the `code` directory, the following one-line command creates a trusted fixture, stores a mock answer, then checks it against the current allowed source IDs:
```sh
python -c 'from cache_gate import *; s=TrustedScope("tenant-a","user-a","policy-4","corpus-9","hybrid-v2"); q=b"How do I rotate the service key?"; d={}; put(d,s,q,"Use the approved rotation runbook.",("doc-17",),100.0,30.0); print(get_authorized(d,s,q,frozenset({"doc-17"}),101.0))'
```
Expected output:
```text
Use the approved rotation runbook.
```
Changing the allowed set to `frozenset()` should produce `None` because the entry declares `doc-17`. Changing tenant, principal, policy revision, corpus revision, retrieval profile, or exact query bytes should also produce a miss. These are expected behaviors authored into the acceptance examples; they have **not been executed**.
## Acceptance examples
From the `code` directory, the test command is:
```sh
python -m unittest -v test_cache_gate.py
```
Expected result when run in a supported environment: fourteen passing `unittest` cases covering same-scope access, tenant/principal/revision/query misses, current reauthorization, empty permissions, expiry, missing context, malformed entries, empty provenance, corrupt answers, invalid UTF-8, and TTL overflow. **Testing status: NOT RUN.** Do not interpret the authored expectations as runtime evidence.
## Important limits
The trusted identity and allowed source IDs are fixtures, not authentication or authorization integrations. The declared source list is assumed complete; semantic provenance is not checked. The dict and injected monotonic TTL are process-local, without distributed invalidation, persistence, locking, or a revocation-freshness guarantee. Hashes are not secrets or permission proofs. The mock does not establish that cached answers are correct or production safe.

ใส่ความเห็น