AI Chatbot

ทำไม RAG Pipeline ของคุณถึงยังหลุดข้อมูลที่ไม่ควรหลุด: การควบคุมสิทธิ์การเข้าถึงที่ชั้น Retrieval

แอปพลิเคชันของคุณมีระบบสิทธิ์ตามบทบาท (role-based permission) ระบบจัดการเอกสารของคุณมี ACL ระดับโฟลเดอร์ แต่ RAG chatbot ของคุณไม่มีทั้งสองอย่าง — มันมี vector index เดียวที่ใช้ร่วมกัน และพร้อมจะส่งข้อมูลเงินเดือนให้ใครก็ตามที่ถามคำถามถูกจังหวะ

นี่ไม่ใช่เรื่องสมมติ นี่คือข้อผิดพลาดด้านสถาปัตยกรรมที่พบบ่อยที่สุดในระบบ RAG ระดับองค์กร และแทบไม่เคยถูกจับได้ตอน demo เพราะ demo ใช้ชุดเอกสารเดียวกับผู้ใช้คนเดียว แต่ระบบจริงมีข้อมูล HR สัญญาทางกฎหมาย เอกสารบอร์ดบริหาร และพนักงาน 200 คน — และทันทีที่พนักงานสองคนมีสิทธิ์ต่างกันบนโฟลเดอร์เดียวกัน RAG pipeline ส่วนใหญ่ก็จะเงียบๆ หยุดบังคับใช้สิทธิ์ใดๆ เลย

ช่องโหว่: UI รู้เรื่องสิทธิ์ แต่ retrieval ไม่รู้

รูปแบบที่พบซ้ำๆ คือ:

  1. บริษัทสร้าง RAG chatbot บนคลังเอกสารภายใน (Google Drive, SharePoint, Confluence หรือระบบ DMS)
  2. หน้าแชทตรวจสอบตัวตนผู้ใช้ถูกต้อง และแสดง รายการ เอกสารตามสิทธิ์ได้อย่างสวยงาม
  3. แต่ vector index เบื้องหลัง chatbot ถูกสร้างขึ้นครั้งเดียวจากทุกอย่าง — HR การเงิน กฎหมาย วิศวกรรม — โดยไม่มีบันทึกระดับ chunk ว่าใครมีสิทธิ์เห็นอะไรบ้าง
  4. ผู้ใช้ถามคำถาม ขั้นตอน retrieval ค้นหาทั่วทั้ง index หาความคล้ายเชิงความหมายที่ใกล้ที่สุด เจอ chunk จากเอกสารที่ผู้ใช้ไม่เคยได้รับสิทธิ์เข้าถึง แล้วส่งให้ LLM เป็น context ทันที
  5. LLM ทำหน้าที่ของมันถูกต้อง — ตอบคำถามโดยใช้ chunk นั้น รวมถึงรายละเอียดที่ผู้ใช้ไม่ควรเห็น

ไม่มี error ไม่มีการแจ้งเตือนการตรวจสอบ หน้าจอไม่แม้แต่จะแสดงเอกสารต้นทาง จึงไม่มีใครสังเกตว่า retrieval ข้ามขอบเขตสิทธิ์ไปแล้ว — จนกว่าจะกลายเป็นข้อค้นพบในการตรวจสอบความปลอดภัย หรือแย่กว่านั้นคือการสอบถามจากหน่วยงานกำกับดูแล

นี่คือเหตุผลที่ "เรามี SSO และ role-based access บนแอปแล้ว" ไม่ใช่คำกล่าวเดียวกับ "RAG pipeline ของเราบังคับใช้การควบคุมสิทธิ์การเข้าถึงจริง" SSO ควบคุมว่าใคร login ได้ RBAC บนแอปควบคุมว่าเห็นหน้าจอไหนได้ แต่ไม่มีอันไหนแตะขั้นตอน retrieval ที่การค้นหาความคล้ายเชิงความหมายไม่รู้และไม่สนใจว่ากำลังอ่านเอกสารของใคร

ภายใต้ PDPA และมาตรา 59 ของ พ.ร.บ. การรักษาความมั่นคงปลอดภัยไซเบอร์ การรั่วไหลแบบนี้ไม่ใช่แค่ความน่าอายภายใน — มันคือเหตุการณ์ที่อาจต้องแจ้งต่อหน่วยงานกำกับดูแล โดยเฉพาะเมื่อข้อมูลที่รั่วเป็นข้อมูลส่วนบุคคลของพนักงานหรือลูกค้า

ทำไมช่องโหว่นี้ไม่ปรากฏตอนทดสอบ

สามเหตุผลที่ช่องโหว่การควบคุมสิทธิ์ใน RAG รอดจนถึง production:

  • ข้อมูลทดสอบแบบ single-tenant — pilot ส่วนใหญ่ทดสอบกับชุดเอกสาร demo ที่ไม่มีระดับความลับต่างกัน จึงไม่มีอะไรให้รั่ว
  • ทดสอบด้วยผู้ใช้คนเดียว — developer คนเดียว บัญชี admin คนเดียว ทดสอบเฉพาะ happy path ปัญหาจะปรากฏก็ต่อเมื่อมีผู้ใช้คนที่สองที่มีสิทธิ์แคบกว่าเริ่มถามคำถาม
  • LLM ตอบด้วยความมั่นใจไม่ว่าจะถูกหรือผิด — การรั่วไหลของสิทธิ์ไม่มีลักษณะเหมือน error มันดูเหมือนระบบทำงานได้ดี chatbot ตอบได้แม่นยำ นั่นแหละคือปัญหา

สามรูปแบบที่ชั้น retrieval ที่บังคับใช้สิทธิ์ได้จริง

1. การกรองด้วย metadata ตอน query

วิธีแก้ขั้นต่ำสุด: ติด metadata การควบคุมสิทธิ์ที่สืบทอดจากเอกสารต้นทางให้ทุก chunk แล้วใช้เป็นตัวกรองแบบบังคับ ภายใน การค้นหาความคล้ายเชิงความหมาย — ไม่ใช่หลังจากนั้น

ต่อยอดจากโครงสร้าง document_chunks ใน บทความสอน pgvector ของเรา เพิ่มตาราง access เข้าไป:

-- กลุ่ม/บทบาทที่ได้รับอนุญาตให้เห็นเอกสารแต่ละฉบับ
CREATE TABLE document_acl (
    document_id  BIGINT REFERENCES documents(id) ON DELETE CASCADE,
    principal    TEXT NOT NULL,   -- เช่น 'role:finance', 'user:alice@company.com', 'group:leadership'
    PRIMARY KEY (document_id, principal)
);

CREATE INDEX ON document_acl (principal);
def semantic_search_with_acl(
    query: str,
    user_principals: list[str],   # เช่น ["user:alice@company.com", "role:finance", "group:apac"]
    top_k: int = 5,
) -> list[dict]:
    query_embedding = get_embedding(query)
    conn = psycopg2.connect(DB_URL)
    cur = conn.cursor()
    try:
        cur.execute(
            """
            SELECT dc.id, dc.content, d.title, d.source_url,
                   1 - (dc.embedding <=> %s::vector) AS similarity
            FROM document_chunks dc
            JOIN documents d ON d.id = dc.document_id
            WHERE EXISTS (
                SELECT 1 FROM document_acl a
                WHERE a.document_id = d.id
                AND a.principal = ANY(%s)
            )
            ORDER BY dc.embedding <=> %s::vector
            LIMIT %s
            """,
            [query_embedding, user_principals, query_embedding, top_k],
        )
        return cur.fetchall()
    finally:
        cur.close()
        conn.close()

ตัวกรองทำงาน ภายใน query ไม่ใช่การตรวจสอบผลลัพธ์หลังจากดึงมาแล้ว ความแตกต่างนี้สำคัญ: การกรองหลัง retrieval หมายความว่าเนื้อหาที่ต้องห้ามถูกดึงเข้าหน่วยความจำของแอปพลิเคชันไปแล้วก่อนตัดสินใจทิ้ง — การรั่วไหลเกิดขึ้นแล้วที่ชั้นเครือข่าย และหาก post-filter มีบั๊ก เนื้อหานั้นก็ยังไปถึง LLM อยู่ดี

2. Row-level security เป็นแนวป้องกันสำรอง

การกรองที่ชั้นแอปพลิเคชันน่าเชื่อถือเท่ากับทุก code path ที่ query ตารางนั้นเท่านั้น สคริปต์ใดๆ เครื่องมือ admin หรือการเชื่อมต่อในอนาคตที่ลืมเงื่อนไข WHERE EXISTS จะเปิดช่องโหว่อีกครั้ง Postgres Row-Level Security (RLS) บังคับใช้กฎเดียวกันที่ชั้นฐานข้อมูล จึงครอบคลุมแม้กระทั่ง query ที่นักพัฒนาแอปไม่ได้คาดคิดไว้:

ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY;

CREATE POLICY chunk_access_policy ON document_chunks
USING (
    EXISTS (
        SELECT 1 FROM document_acl a
        WHERE a.document_id = document_chunks.document_id
        AND a.principal = ANY(current_setting('app.user_principals')::text[])
    )
);
# ตั้งค่า principals ของ session ก่อนรัน query ใดๆ บน connection นี้
cur.execute("SET app.user_principals = %s", [user_principals])

นี่คือการป้องกันหลายชั้น ไม่ใช่การแทนที่ตัวกรองระดับแอปพลิเคชัน — มันคือสิ่งที่หยุดบั๊กด้านสิทธิ์ใน endpoint หนึ่งไม่ให้กลายเป็นการรั่วไหลข้อมูลทั้งระบบ

3. จัดการกับการเปลี่ยนแปลงสิทธิ์และ embedding ที่ล้าสมัย

Chunk และ embedding เป็นข้อมูลนิ่งเมื่อถูกเขียนแล้ว แต่สิทธิ์ไม่นิ่ง — พนักงานย้ายออกจากทีมการเงิน สัญญาถูกจัดประเภทใหม่ สิทธิ์ของผู้รับเหมาที่พ้นสภาพควรหายไปทันที มีสองสิ่งที่ต้องทำ:

  • ซิงค์ ACL ไม่ใช่ re-embed เนื้อหา — เมื่อสิทธิ์เปลี่ยน ให้อัปเดต document_acl โดยตรง ไม่จำเป็นต้อง re-embed เอกสาร เพราะตาราง ACL แยกจากข้อมูล vector การเพิกถอนสิทธิ์คือการเขียน metadata ไม่ใช่การรัน pipeline ใหม่
  • งาน sync จากแหล่งข้อมูลจริง — หากเอกสารอยู่ใน Google Drive หรือ SharePoint ให้รันงานตามกำหนดเวลาที่ดึงสิทธิ์โฟลเดอร์/ไฟล์ปัจจุบันมากระทบยอดกับ document_acl เพราะ ACL มักเบี่ยงเบนจากระบบต้นทางเร็วกว่าที่ทีมส่วนใหญ่คาดไว้ โดยเฉพาะเมื่อมีการสืบทอดสิทธิ์แบบซ้อนโฟลเดอร์

ภาพรวมสถาปัตยกรรม

flowchart TD
    A["คำถามผู้ใช้ + identity token"] --> B["ระบุ principals ของผู้ใช้\nบทบาท กลุ่ม สิทธิ์ตรง"]
    B --> C["Embed คำถาม"]
    C --> D["ค้นหาความคล้ายแบบกรอง\nตรวจสอบ ACL ภายใน query"]
    E["ตาราง document_acl"] --> D
    F["ซิงค์สิทธิ์จากระบบต้นทาง\nDrive, SharePoint, DMS"] --> E
    D --> G["เฉพาะ chunk ที่ได้รับอนุญาต"]
    G --> H["LLM สร้างคำตอบ"]
    H --> I["บันทึกการตรวจสอบ retrieval\nใคร อะไร เมื่อไหร่ principal ที่ตรงกัน"]

การตรวจสอบย้อนหลัง: สิ่งที่ทีม compliance ต้องการจริงๆ

ชั้น retrieval ที่รู้จักสิทธิ์ให้สิ่งที่ชั้นที่ไม่รู้จักสิทธิ์ให้ไม่ได้ — log ที่สามารถอธิบายได้ ทุกเหตุการณ์ retrieval ควรบันทึกตัวตนผู้ query, principal ที่ระบุสำหรับตัวตนนั้น และเอกสารใดบ้างที่มีสิทธิ์ถูกส่งคืน — ไม่ว่าจะเป็น match อันดับต้นหรือไม่ก็ตาม เมื่อการตรวจสอบความปลอดภัยหรือหน่วยงานกำกับดูแลถามว่า "คุณรู้ได้อย่างไรว่าระบบ AI ของคุณไม่ได้เปิดเผยข้อมูลที่จำกัดสิทธิ์" log นี้คือคำตอบ ไม่ใช่คำสัญญา

คำถามที่พบบ่อย

นี่ไม่ใช่สิ่งที่การตรวจสอบสิทธิ์ที่ front-end จัดการอยู่แล้วหรือ?

ไม่ใช่ การตรวจสอบที่ front-end ควบคุมว่าผู้ใช้คลิกอะไรได้ แต่ไม่มีการมองเห็นว่าขั้นตอน retrieval ดึงอะไรเข้า context window ของ LLM ภายใน นี่คือจุดบังคับใช้สองจุดที่แยกกัน และปกติมีแค่จุดเดียวที่ถูกสร้างขึ้นจริง

วิธีนี้ทำให้ retrieval ช้าลงไหม?

ตัวกรอง EXISTS แบบมี index บน document_acl เพิ่ม overhead ระดับหลักหน่วยมิลลิวินาที เมื่อ document_acl.principal มี index ซึ่งน้อยมากเมื่อเทียบกับเวลาสร้าง embedding และการประมวลผลของ LLM

ถ้าสิทธิ์ซับซ้อนกว่า flat role — โฟลเดอร์ซ้อนกัน กลุ่มที่สืบทอด สิทธิ์ชั่วคราว — ทำยังไง?

จำลองแต่ละเส้นทางการสืบทอดเป็นแถวหนึ่งใน document_acl ตอน sync แทนที่จะคำนวณการสืบทอดตอน query แผ่ลำดับชั้นให้แบนตอนงาน sync แล้วให้ query เองเป็นแค่การตรวจสอบสมาชิกภาพแบบง่าย

ถ้าเอกสารของเราไม่ได้ลับขนาดนั้น ยังต้องทำไหม?

ถ้าคลังเอกสารผสมเนื้อหาที่มีระดับความลับใดๆ — HR กฎหมาย การเงิน ข้อมูลส่วนบุคคลลูกค้า — กับเนื้อหาทั่วไป ต้องทำ รูปแบบความล้มเหลวไม่ใช่แค่ "น่าอายเป็นครั้งคราว" แต่เป็นการเปิดเผยข้อมูลที่อธิบายยากภายหลัง เพราะไม่มีอะไรในระบบตั้งค่าสถานะว่าผิดปกติ


กำลังสร้างหรือตรวจสอบ RAG pipeline ที่ต้องบังคับใช้การควบคุมสิทธิ์การเข้าถึงจริง — ไม่ใช่แค่ UI ที่มีสิทธิ์อยู่หน้า index ที่ไม่มีสิทธิ์อะไรเลย? ติดต่อเราที่ hello@simplico.net — เราออกแบบและติดตั้งสถาปัตยกรรม RAG ระดับ production ให้ลูกค้าองค์กรทั่วไทย ญี่ปุ่น และเอเชียตะวันออกเฉียงใต้