FDE PulseViệc làm FDE đang mở 641Mới trong 7 ngày 50Công ty đang tuyển 98Nhận làm từ xa 24%Lương trung vị (Mỹ) $203kTuyển nhiều nhất Databricks 122
EN

Tờ báo của nghề Forward Deployed Engineer

Bách khoa

Thực hành: đồng bộ SharePoint, Confluence vào RAG với job quyền riêng

Đồng bộ nội dung thì gần như team nào cũng làm được. Khó hơn nhiều là bắt kịp lúc khách rút quyền đọc của một người, nên hướng dẫn này dựng hai job riêng cho hai việc đó.

Ảnh kỹ sư làm việc với laptop hiển thị code trong văn phòng doanh nghiệp, gợi bối cảnh đồng bộ tài liệu nội bộ.
Ảnh: Emile Perron / Unsplash
Chia sẻLinkedInFacebookX

Tóm tắt nhanh

  • Webhook chỉ báo là có thay đổi, dữ liệu thật phải lấy từ delta. Vẫn nên chạy delta định kỳ làm lưới an toàn.
  • Theo dõi item theo id, upsert idempotent, gỡ item có facet deleted, và luôn có sẵn đường resync toàn phần khi gặp 410.
  • Quyền cần một job riêng: change feed không tự phản ánh việc thu hồi, và quyền space của Confluence không hiện qua CQL.
Cây quyết định ở bước retrieval. Câu hỏi đầu tiên: lần đối soát quyền gần nhất của nguồn có cũ hơn 15 phút không? Nếu có, hệ thống loại hết tài liệu của nguồn đó và báo "Tạm thời không truy cập được". Nếu không, hệ thống xét tiếp người hỏi có trong ACL không: có thì dùng tài liệu, không thì bỏ tài liệu.
Fail-closed: khi job quyền tụt lại quá ngưỡng, bước retrieval không dùng ACL cũ mà loại hết tài liệu của nguồn đó. Ngưỡng 15 phút chỉ là ví dụ, bộ phận bảo mật của khách là bên chọn con số này. Nguồn: hướng dẫn trong bài, dựa trên tài liệu Microsoft Graph và Wavect.

Thử hình dung: 9 giờ sáng, một nhân viên bị rút quyền đọc thư mục lương trên SharePoint. Nếu pipeline RAG của bạn chỉ đồng bộ nội dung, chatbot vẫn trích tài liệu lương cho người đó, vì file không hề bị sửa.

Bài phân tích của Wavect về RAG trên SharePoint và Confluence nhấn mạnh đúng điểm này: quyền đã chép vào chỉ mục không tự cập nhật khi nguồn thu hồi.

Một sự cố như vậy là đủ để khách dừng hẳn pilot. Vì thế, nhiều khả năng câu hỏi đầu tiên từ bộ phận bảo mật của khách sẽ xoay quanh quyền truy cập, chứ chưa phải chất lượng câu trả lời.

Hướng dẫn này dựng hai job. Job nội dung dùng delta query để đưa thay đổi của file vào chỉ mục vector. Job quyền chạy riêng, có nguồn tín hiệu riêng và có ngưỡng fail-closed. Code bên dưới là Python giả lập, đã đơn giản hóa cho dễ đọc, chưa phải SDK chạy được ngay.

Cần chuẩn bị gì trước khi viết dòng code đầu tiên?

Bạn cần một app đăng ký trong tenant Microsoft 365 của khách, một tài khoản Confluence Cloud có API token, và một chỉ mục vector cho phép upsert và xóa theo khóa. Phần khó nhất lại không nằm ở kỹ thuật. Tài liệu hướng dẫn quét quy mô lớn của Microsoft ghi rằng muốn xử lý quyền cho đúng, app cần quyền Sites.FullControl.All.

Quyền này rất rộng, nên admin bên khách thường cần thời gian để duyệt. Hãy xin ngay tuần đầu, kèm một trang giải thích app đọc gì, ghi gì, và vì sao thiếu nó thì chatbot không lọc được tài liệu theo người hỏi.

Lần đầu bạn gọi delta không kèm tham số, Graph sẽ duyệt toàn bộ drive. Kết quả trả về nhiều trang, mỗi trang có @odata.nextLink. Chỉ trang cuối mới có @odata.deltaLink, và đó là thứ bạn cất đi cho lần đồng bộ sau.

# Đơn giản hóa: chưa có retry, auth, phân trang song song
def sync_drive(state, drive_id):
    url = state.get("delta_link") or f"{GRAPH}/drives/{drive_id}/root/delta"
    while url:
        body = graph_get(url).json()
        for item in body["value"]:
            handle_item(item)
        url = body.get("@odata.nextLink")
        if not url:
            state["delta_link"] = body["@odata.deltaLink"]
            save(state)

Kiểm tra: chỉ lưu delta_link khi đã xử lý xong trang cuối. Nếu lưu sớm mà job chết giữa chừng, lần chạy sau sẽ bỏ qua luôn những trang chưa kịp xử lý.

Bước 2: theo id, không theo đường dẫn

Delta feed chỉ trả trạng thái mới nhất của item, và một item có thể xuất hiện nhiều lần. Microsoft dặn luôn theo dõi item theo id. Lý do: khi đổi tên một thư mục, feed không trả lại các file con bên trong.

Nếu bạn lấy path làm khóa trong chỉ mục, chỉ cần đổi tên một thư mục là sinh ra cả loạt bản ghi mồ côi. Hãy lấy id làm khóa, còn path chỉ là metadata để hiển thị trích dẫn.

def handle_item(item):
    doc_id = item["id"]
    if "deleted" in item:
        index.delete(doc_id)              # gỡ khỏi trạng thái cục bộ
        return
    if item.get("@microsoft.graph.sharedChanged") == "True":
        permission_queue.put(doc_id)      # để job quyền xử lý
    if "file" in item:
        content_queue.put(doc_id)         # tải, chunk, embed, upsert

Để ý hai lệnh if tách rời nhau. Vì feed chỉ trả trạng thái mới nhất, một file vừa bị sửa nội dung vừa bị đổi quyền có thể chỉ hiện ra một lần, mang annotation sharedChanged.

Nếu dùng elif, phần nội dung mới của file đó sẽ bị bỏ rơi mà không ai hay. Đẩy vào cả hai hàng đợi thì an toàn hơn, và cái giá chỉ là embed thừa một lần, vô hại nếu bước 3 làm đúng.

Kiểm tra: xóa một file trên SharePoint rồi chạy lại. Item có facet deleted phải biến mất khỏi chỉ mục chứ không chỉ khỏi bảng metadata.

Bước 3: upsert phải chạy lại được nhiều lần

Tài liệu tổng quan về delta query của Graph nói rõ ứng dụng phải chịu được replay, tức cùng một thay đổi có thể quay lại trong những lần phản hồi sau. Vì thế bước ghi vào chỉ mục phải idempotent. Hãy xóa toàn bộ chunk cũ của doc_id rồi ghi chunk mới, hoặc đặt khóa chunk theo dạng doc_id + số thứ tự.

Lỗi hay gặp: append chunk mỗi lần thấy item. Sau một tuần, cùng một đoạn văn xuất hiện ba lần trong top-k, và người dùng phàn nàn câu trả lời lặp ý.

Bước 4: 410 Gone không phải lỗi để bỏ qua

Khi token quá cũ hoặc hết hạn, Graph trả HTTP 410 Gone kèm header Location chứa một nextLink mới để duyệt lại từ đầu. Lần duyệt lại này phải đi kèm bước đối chiếu với trạng thái cục bộ: id nào có trong chỉ mục mà không xuất hiện trong lần duyệt toàn phần thì là tài liệu đã mất ở nguồn.

if resp.status_code == 410:
    url = resp.headers["Location"]
    state["full_resync"] = True       # cuối vòng: so tập id, xóa phần thừa
    seen_ids.clear()

Thêm một quy tắc: khi gặp 429 hoặc 503, chờ đúng thời gian ghi trong Retry-After và dừng mọi request, không chỉ luồng vừa bị chặn. SharePoint throttle theo từng ứng dụng trên mỗi tenant, nên mười worker tranh nhau retry chỉ khiến cả app bị chặn lâu hơn.

Bước 5: webhook để biết sớm, delta để lấy dữ liệu

Microsoft gợi ý một mô hình ba phần: crawl bằng delta, nhận webhook, và chạy thêm delta định kỳ, tối đa một lần mỗi ngày, để không sót thay đổi nào. Webhook chỉ báo “có gì đó vừa đổi”. Khi nhận được nó, bạn gọi delta bằng delta_link đã cất. Đừng bao giờ dùng payload webhook làm nguồn dữ liệu.

Bước 6: job quyền là một hệ thống riêng

Mặc định, delta kèm thông tin sharing cho mọi item thay đổi, và bạn phải gọi thêm để lấy quyền của từng item. Header đầu tiên nên thêm là Prefer: deltashowsharingchanges. Nó gắn annotation sharedChanged cho những item xuất hiện vì đổi quyền, nhưng chỉ có trên SharePoint và OneDrive for Business.

Header thứ hai là Prefer: hierarchicalsharing, được Microsoft giới thiệu như cách để hiểu rõ hơn quyền đã thay đổi như thế nào. Mục đích thực tế của nó là cắt bớt số lần phải gọi thêm để lấy quyền cho từng item một.

Job quyền lấy id từ permission_queue, gọi API liệt kê quyền của file rồi dựng ACL. Quyền hiệu lực gồm quyền gán trực tiếp cộng quyền kế thừa từ thư mục cha, và thuộc tính inheritedFrom cho biết quyền đó kế thừa từ đâu.

# Đơn giản hóa: list_permissions() bọc API liệt kê quyền của file
def permission_worker():
    while True:
        doc_id = permission_queue.get()
        acl = {"direct": [], "inherited": []}
        for perm in list_permissions(doc_id):
            if perm.get("inheritedFrom"):
                acl["inherited"].append(perm)
            else:
                acl["direct"].append(perm)
        index.set_acl(doc_id, acl)   # chỉ ghi trường ACL, không embed lại

Kiểm tra: đổi quyền một thư mục cha có vài file con. Mọi file con đang kế thừa từ thư mục đó phải được tính lại ACL, không chỉ riêng thư mục.

Còn một nguồn tín hiệu nữa: SharePoint và OneDrive có webhook riêng cho sự kiện bảo mật, báo khi quyền của một item thay đổi. Hãy nối webhook này thẳng vào job quyền, không cho đi qua hàng đợi nội dung, vì việc thu hồi quyền không nên phải xếp hàng sau một file PDF 200 trang đang chờ embed.

Confluence: CQL không thấy quyền space

Với Confluence, job nội dung có thể truy vấn CQL theo lastModified. Nhưng một câu trả lời trên diễn đàn developer của Atlassian chỉ ra rằng thay đổi quyền space không hiện qua CQL, vì đó không phải cập nhật nội dung. Muốn bắt được, bạn cần Audit API hoặc một job quyền chạy theo lịch.

Ở cấp trang, nhóm API content restrictions của Confluence REST trả về các hạn chế trên từng nội dung, đủ để dựng ACL cho từng tài liệu. Khi gặp 429, Atlassian khuyên dùng exponential backoff có jitter và tôn trọng Retry-After.

Job nội dung Job quyền
Tín hiệu SharePoint Webhook file, delta định kỳ Annotation sharedChanged, webhook bảo mật
Tín hiệu Confluence CQL lastModified Audit API, quét restrictions theo lịch
Ghi vào Chunk và embedding theo id Trường ACL của từng tài liệu
Khi tụt lại phía sau Câu trả lời cũ Lộ dữ liệu: phải fail-closed

Ngưỡng fail-closed: chậm thì từ chối

Change feed chỉ là tín hiệu, nên bạn vẫn phải reconcile quyền định kỳ và đặt ngưỡng độ trễ. Giả sử bạn chọn 15 phút: nếu lần reconcile thành công gần nhất của một nguồn đã cũ hơn 15 phút, bước retrieval loại hết tài liệu của nguồn đó thay vì tin vào ACL cũ.

Người dùng sẽ thấy “tạm thời không truy cập được”, một kết quả dễ giải thích hơn nhiều so với một vụ lộ bảng lương.

Con số ngưỡng là chuyện thỏa thuận với khách. Hãy để bộ phận bảo mật của họ chọn, rồi ghi nó vào tài liệu nghiệm thu.

Mang kỹ năng này đến site khách và vào CV

Quay lại kịch bản thư mục lương ở đầu bài: câu hỏi khó nhất ở site khách nhiều khả năng không nằm ở recall, mà là “nếu tôi rút quyền của anh A thì bao lâu sau chatbot mới biết”. FDE trả lời được bằng một con số, một cơ chế và một hành vi rõ ràng khi hệ thống chậm sẽ dễ được tin hơn.

Khi đọc JD, hãy để ý các cụm như “permission-aware retrieval”, “connectors” hay “enterprise data sync”. Trong CV, viết cụ thể: “Dựng connector SharePoint/Confluence cho RAG bằng delta query, tách job ACL, fail-closed khi quyền trễ quá ngưỡng.” Một repo demo có test cho replay, 410 và item bị xóa thuyết phục hơn mọi tính từ.

Đồng bộ nội dung giúp chatbot trả lời đúng. Còn job quyền mới quyết định khách có dám mở chatbot cho cả công ty dùng hay không.

Bài này có hữu ích không?

Dùng cùng trợ lý AIHỏi Claude ↗Hỏi ChatGPT ↗
8 nguồn
Đọc tiếp trên lộ trình · Chặng 3: AI ứng dụngThực hành RAG: khi hai tài liệu của khách nói khác nhau, bắt hệ thống nêu cả hai nguồnTrên các benchmark có người gán nhãn, mô hình nào cũng vấp khi ngữ cảnh tự mâu thuẫn, nên câu trả lời có thể chỉ kể một nửa câu chuyện mà người dùng không hề biết.