Skip to content

Latest commit

 

History

37 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LD3 AI Chatbot

Trợ lý RAG nội bộ cho Trung Tâm Đổi mới sáng tạo & Chuyển đổi số Lâm Đồng III. Dự án đang trong quá trình hoàn thiện.

English version: README.en.md

Chatbot RAG (retrieval-augmented generation — sinh câu trả lời dựa trên tài liệu truy xuất được) xây dựng cho Trung Tâm Đổi mới sáng tạo và Chuyển đổi số Lâm Đồng III. Lãnh đạo và cán bộ nhà nước dùng hệ thống này để tra cứu nhanh dự toán kinh phí, yêu cầu kỹ thuật và đối tượng áp dụng nằm rải rác trong các văn bản hành chính (.doc / .docx / .pdf — thuyết minh dự án đầu tư, cấu hình thiết bị phòng thí nghiệm, hồ sơ pháp quy) mà không phải đọc thủ công cả tệp.

Tình trạng dự án: dự án do một người phát triển. Không có đội ngũ, không có CI/CD, triển khai thủ công. Hiện chỉ chạy trên máy tính của lập trình viên — chưa triển khai lên máy chủ. Xem mục Triển khai để biết còn thiếu những gì.


Mục lục


Tính năng

  • Chat RAG nhiều đơn vị, trả lời theo luồng (streaming) — người dùng đặt câu hỏi, câu trả lời hiện ra từng chữ một qua Server-Sent Events. Mọi truy vấn đều bị giới hạn theo DepartmentId (mã đơn vị, được gắn vào cookie đăng nhập); bộ truy xuất không bao giờ trả về dữ liệu của đơn vị khác.
  • Truy xuất lai (hybrid retrieval) — kết hợp vector ngữ nghĩa (BAAI/bge-m3) với BM25 thưa (fastembed Qdrant/bm25), hợp nhất bằng RRF ngay trên máy chủ Qdrant trong một lần gọi. Có thêm bước xếp hạng lại bằng cross-encoder (BAAI/bge-reranker-v2-m3, tự bỏ qua khi máy thiếu RAM) và mở rộng câu hỏi bằng HyDE cho câu hỏi ngắn.
  • Quy trình nạp tài liệu — IBM Docling đọc PDF (kèm EasyOCR cho bản scan + TableFormer cho bảng phức tạp, OCR tiếng Việt và tiếng Anh), DOCX, và .doc nhị phân cũ (chuyển sang .docx bằng LibreOffice trước). Bộ chia đoạn tự viết bám theo cấu trúc tiêu đề Markdown, giữ nguyên vẹn từng dòng bảng và giới hạn độ dài đoạn theo đúng số token của bge-m3.
  • Trích dẫn nguồn — câu trả lời của mô hình có chú thích [n] ngay trong nội dung, ánh xạ tới đoạn tài liệu gốc. Giao diện hiển thị ngăn kéo nguồn, chip trích dẫn, và chế độ đọc toàn văn có tô sáng đến từng câu. Mỗi tin nhắn lưu lại danh sách tài liệu đã dùng để trả lời.
  • Trang quản trị cấu hình AI (/admin/ai-settings) — đổi nhà cung cấp và mô hình ngay lúc chạy giữa Ollama (mặc định, chạy nội bộ), OpenAI, Anthropic và Gemini. Khóa API của nhà cung cấp đám mây được mã hóa khi lưu bằng ASP.NET Core Data Protection và không bao giờ trả về trình duyệt.
  • Cơ chế chống bịa đặt — một system prompt bắt buộc luôn được chèn vào đầu mọi cuộc hội thoại, buộc mô hình đọc bảng theo từng dòng, trích xuất số liệu trước khi tính toán, không được tự nghĩ ra con số, và phải nói rõ khi dữ liệu không có.
  • Các trang quản trị — quản lý tài liệu, quản lý người dùng, phản hồi (thích/không thích + bình luận), và nhật ký hệ thống.

Thiết kế hệ thống

Hai tiến trình, ba nơi lưu trữ, một trình duyệt. Ứng dụng .NET là thứ duy nhất trình duyệt giao tiếp; worker Python không bao giờ lộ ra ngoài.

Sơ đồ thành phần

                          ┌────────────────────────────┐
   Trình duyệt            │  .NET 10 (ASP.NET Core)    │
 ┌──────────┐  SSE/HTTP   │                            │
 │ chat.js  │◄───────────►│  Razor Pages (giao diện)   │
 │ Razor UI │  SignalR    │  Web API (/api/*)          │
 └──────────┘◄───────────►│  Identity + cookie auth    │
                          │  DocumentIngestionWorker   │
                          └───────┬──────────┬─────────┘
                                  │          │
                    EF Core       │          │  HTTP + X-Worker-Api-Key
                                  ▼          ▼
                        ┌──────────────┐  ┌──────────────────────────┐
                        │ SQL Server   │  │  Worker Python (FastAPI) │
                        │ (SQLEXPRESS) │  │                          │
                        │              │  │  Docling · bge-m3        │
                        │ người dùng,  │  │  BM25 · reranker · HyDE  │
                        │ tài liệu,    │  │  LlmRouter (4 nhà c.cấp) │
                        │ hội thoại,   │  └────┬───────────────┬─────┘
                        │ nhật ký, cấu │       │               │
                        │ hình AI      │       ▼               ▼
                        └──────────────┘  ┌─────────────────┐  ┌─────────────┐
                                          │     Qdrant      │  │   Ollama    │
                                          │   (bản native)  │  │  qwen2.5:3b │
                                          │ `ld3_knowledge` │  │ (hoặc cloud)│
                                          └─────────────────┘  └─────────────┘

Tệp tải lên nằm trên ổ đĩa (App_Data/uploads/), không nằm trong SQL — SQL chỉ lưu thông tin mô tả.

Luồng xử lý câu hỏi

Trình duyệt (chat.js)
  └─ POST /api/chat/send                      [xác thực cookie, claim DepartmentId]
     └─ ChatController                        mở luồng SSE
        └─ ChatService                        đọc bản ghi AiConfig (nhà c.cấp/mô hình/temp/topK)
           │                                  giải mã khóa API cloud qua ProviderKeyService nếu cần
           └─ AiWorkerClient                  HTTP SSE → worker, gửi kèm X-Worker-Api-Key
              └─ POST /api/query (FastAPI)
                 ├─ _select_llm()             Ollama dùng lại instance sẵn có;
                 │                            cloud tạo LlmRouter mới cho mỗi request
                 ├─ Embedder                  vector bge-m3            (asyncio.to_thread)
                 ├─ Retriever                 Qdrant query_points:     (asyncio.to_thread)
                 │                              nhánh vector ngữ nghĩa ─┐
                 │                              nhánh BM25            ─┴─► hợp nhất RRF
                 │                              + bộ lọc MUST theo department_id (cả hai nhánh)
                 │                              + xếp hạng lại bằng cross-encoder (tùy chọn)
                 ├─ PromptBuilder             prompt Jinja2 + ENFORCED_SYSTEM_PROMPT ở vị trí 0
                 └─ LlmRouter.stream()        luồng chữ trả ngược về trình duyệt

Thứ tự sự kiện SSE gửi về trình duyệt: sources (một lần) → token (N lần) → done, hoặc error. Luồng luôn lưu lại phần trả lời dở dang trong khối finally, nên nếu bị hủy hay lỗi giữa chừng thì tin nhắn vẫn được ghi vào cơ sở dữ liệu.

Luồng nạp tài liệu

Cố ý thiết kế bất đồng bộ — đọc một file PDF scan có OCR có thể mất vài phút, nên yêu cầu tải lên trả về ngay lập tức và trình duyệt được cập nhật sau qua SignalR.

Tải tệp lên
  └─ POST /api/documents                      kiểm tra: danh sách MIME cho phép VÀ phần mở rộng
     │                                        cho phép, kích thước khai báo, rồi kích thước thật
     └─ DocumentService                       → LocalFileSystemStorage (App_Data/uploads/)
        └─ Bản ghi Document, Status = Pending

DocumentIngestionWorker (dịch vụ nền, quét các bản ghi Pending)
  └─ Status = Processing → SignalR đẩy về trình duyệt
     └─ POST /api/ingest (FastAPI)            multipart, kèm X-Worker-Api-Key
        ├─ loader.py    Docling đọc tệp       PDF: EasyOCR (vi+en) + TableFormer ACCURATE
        │                                     .doc → .docx qua LibreOffice trước
        │                                     _assess_pdf_quality() loại bản scan không đọc được
        ├─ chunker.py   chia theo tiêu đề     dòng bảng (|...|) giữ nguyên, không cắt giữa bảng
        │                                     đóng gói và giới hạn theo đúng số token bge-m3
        ├─ embedder.py  vector bge-m3
        └─ fastembed    vector thưa BM25 (IDF)
           └─ Ghi vào Qdrant                  cả hai vector + payload (department_id, v.v.)
     └─ Status = Ready | Failed | PartiallyIngested → SignalR đẩy về trình duyệt

Tệp văn bản thuần .txt dùng thẳng SentenceSplitter của LlamaIndex thay vì bộ chia đoạn tự viết.

RAG hoạt động chi tiết

Hai sơ đồ ở trên cho khung sườn. Phần này đi sâu vào từng bước cụ thể — con số, thuật toán, và lý do đằng sau từng lựa chọn — để hiểu vì sao hệ thống trả lời như nó trả lời.

1. Chia đoạn tài liệu (chunking) — worker/app/services/chunker.py

Đây là bước quyết định chất lượng RAG nhiều nhất, vì mô hình chỉ có thể trả lời đúng nếu đoạn văn bản nó nhận được còn nguyên vẹn ngữ nghĩa.

  • Docling (PDF/DOCX) → chia theo cấu trúc tiêu đề Markdown (đệ quy theo heading), không chia theo độ dài cố định như cách làm phổ biến.
  • An toàn với bảng: mọi dòng có ký tự | liên tiếp (dòng bảng Markdown) được nhận diện và giữ nguyên khối — không bao giờ bị cắt giữa bảng. Đây là lý do bộ chia đoạn được viết riêng thay vì dùng HierarchicalChunker có sẵn của Docling: văn bản hành chính LD3 chủ yếu là bảng (dự toán, danh mục thiết bị), và bộ chia mặc định cắt ngang bảng khiến việc truy xuất gần như vô dụng.
  • Giới hạn theo đúng số token bge-m3 (chunk_size = 1024, chunk_overlap = 250): bộ chia dùng chính AutoTokenizer của BAAI/bge-m3 để đếm token thật, không ước lượng theo số từ. Bước cuối cùng (_enforce_token_cap) quét lại toàn bộ đoạn đã tạo ra và chia nhỏ tiếp bất kỳ đoạn nào vẫn vượt giới hạn — không bao giờ cắt cụt (truncate) văn bản, chỉ chia lại.
  • Lọc nhiễu + gộp đoạn nhỏ: các đoạn quá ngắn (dưới 30 từ, ví dụ dòng tiêu đề trơ trọi) được gộp vào đoạn liền kề thay vì trở thành một chunk riêng vô nghĩa.
  • Tệp .txt thuần không qua Docling nên dùng SentenceSplitter (LlamaIndex) theo cửa sổ token thông thường — đơn giản hơn vì không có bảng cần bảo vệ.

2. Tạo vector và lập chỉ mục — embedder.py, sparse_encoder.py, vectorstore.py

Mỗi đoạn được mã hóa hai lần, thành hai loại vector khác nhau, lưu trong cùng một điểm dữ liệu Qdrant:

Loại vector Mô hình Mục đích
dense (1024 chiều) BAAI/bge-m3 bắt ngữ nghĩa — tìm được đoạn liên quan dù không trùng từ
bm25 (thưa, có trọng số IDF) fastembed Qdrant/bm25 trên văn bản đã tách từ tiếng Việt (pyvi) bắt từ khóa chính xác — mã số, số hiệu quyết định, tên riêng

Vì sao cần cả hai: văn bản hành chính tiếng Việt có rất nhiều mã số, số hiệu nghị định, tên dự án cụ thể — loại thông tin mà vector ngữ nghĩa dễ bỏ sót (nó tối ưu cho "ý nghĩa gần giống", không phải "khớp chính xác"). BM25 bù đắp đúng điểm yếu này.

Payload mỗi điểm lưu kèm: document_id, department_id (dùng để cách ly dữ liệu), chunk_index, text (toàn văn đoạn), original_name. Định danh điểm là uuid5(document_id:chunk_index), nên nạp lại cùng tài liệu sẽ ghi đè đúng chỗ thay vì tạo bản trùng.

3. Truy xuất lai + hợp nhất RRF — retriever.py

Khi có câu hỏi, hệ thống chạy hai truy vấn song song trong cùng một lệnh gọi Qdrant (query_points với hai Prefetch), rồi hợp nhất bằng thuật toán Reciprocal Rank Fusion (RRF) ngay trên máy chủ Qdrant — không phải hợp nhất thủ công phía ứng dụng:

Câu hỏi
  ├─ Nhánh vector ngữ nghĩa (dense, cosine, bge-m3)  ─┐
  │    lấy prefetch_top_n = 50 ứng viên               │
  │                                                    ├─► RRF fusion (k=60)
  ├─ Nhánh BM25 (sparse, IDF)                         │      → điểm số cuối
  │    lấy prefetch_top_n = 50 ứng viên               │
  │    (câu hỏi cũng được tách từ tiếng Việt trước)   ┘
  │
  └─ Bộ lọc MUST department_id — áp lên CẢ HAI nhánh, không có ngoại lệ

RRF không cộng điểm số thô của hai nhánh (chúng không cùng thang đo) — nó chỉ nhìn thứ hạng: điểm của một đoạn = tổng 1 / (k + hạng) trên mỗi nhánh nó xuất hiện. k = 60 là giá trị chuẩn từ bài báo gốc (Cormack et al., 2009), cấu hình qua HYBRID_RRF_K. Một đoạn được cả hai nhánh xếp hạng cao sẽ luôn thắng một đoạn chỉ có mặt ở một nhánh — đúng tinh thần "đồng thuận giữa ngữ nghĩa và từ khóa".

prefetch_top_n = 50: mỗi nhánh lấy 50 ứng viên trước khi hợp nhất — cao hơn hẳn số lượng cuối cùng để RRF có đủ dữ liệu để chọn lọc.

4. Xếp hạng lại bằng cross-encoder (tùy chọn) — reranker.py

RRF cho khả năng tìm được (recall) tốt; cross-encoder cho đúng thứ tự (precision). Sau khi RRF trả về danh sách ứng viên, nếu RERANKER_ENABLED = true (mặc định bật), hệ thống lấy rerank_candidate_k = 20 ứng viên đầu, đưa từng cặp (câu hỏi, toàn văn đoạn) qua mô hình BAAI/bge-reranker-v2-m3 để chấm điểm lại, rồi cắt xuống còn RETRIEVAL_TOP_K = 4 đoạn tốt nhất theo điểm mới.

Chốt chặn RAM có chủ đích: mô hình reranker nặng ~2GB. Trước khi nạp, hệ thống kiểm tra RAM trống; nếu dưới reranker_ram_threshold_gb = 3.0 thì bỏ qua reranking hoàn toàn và giữ nguyên thứ tự + điểm số RRF gốc — không bao giờ bịa điểm số giả để lấp chỗ trống (xem comment reranker.py:116: "this function deliberately does not invent placeholder scores"). Khi bị bỏ qua, log ghi reranker_skipped_low_ram và trường reranked trong log truy vấn = false, để biết chính xác câu trả lời nào được xếp hạng lại và câu nào không.

Vì sao chỉ 4 đoạn cuối cùng (RETRIEVAL_TOP_K): con số này đã được hạ dần qua thực nghiệm — 12 → 8 → 4. Mô hình ngôn ngữ nhỏ chạy nội bộ (qwen2.5:3b) bị "lạc" khi phải đọc quá nhiều đoạn nhiễu cùng lúc; ít đoạn nhưng đúng cho kết quả tốt hơn nhiều đoạn nhưng loãng.

5. HyDE — mở rộng câu hỏi ngắn (tùy chọn, mặc định TẮT) — hyde.py

HYDE_ENABLED = false mặc định. Khi bật, với câu hỏi ngắn (dưới hyde_max_query_chars = 80 ký tự, ví dụ "quyết định 1202"), hệ thống nhờ chính LLM sinh trước một câu trả lời giả định ngắn (2-3 câu), rồi lấy vector của câu trả lời giả định đó — thay vì vector của câu hỏi gốc — để tìm theo nhánh ngữ nghĩa.

Lý do: câu hỏi ngắn như "quyết định 1202" mang rất ít tín hiệu ngữ nghĩa để so khớp với một đoạn văn bản hành chính dài; một câu trả lời giả định (dù có thể sai chi tiết) có "hình dạng ngôn ngữ" gần với tài liệu thật hơn, nên tìm ra đúng đoạn dễ hơn.

Có chủ đích chỉ thay nhánh dense, không đụng vào nhánh BM25 — nhánh từ khóa vẫn dùng đúng từ người dùng gõ, không bị pha loãng bởi nội dung do máy sinh ra. Thất bại ở bước sinh câu trả lời giả định (lỗi mạng, LLM quá tải...) sẽ tự động rơi về dùng vector câu hỏi gốc, không bao giờ chặn luồng truy xuất.

6. Ghép ngữ cảnh + system prompt chống bịa đặt — prompt_builder.py, worker/app/services/prompts/rag_system.j2

4 đoạn cuối cùng (sau reranking) được ghép vào một prompt hệ thống bằng template Jinja2, luôn ở vị trí đầu tiên (index 0) của mọi cuộc hội thoại. Các quy tắc bắt buộc trong prompt (dịch ý, nguyên văn bằng tiếng Việt trong rag_system.j2):

  1. Chỉ đưa ra câu trả lời cuối cùng — không hiện quá trình suy nghĩ, kế hoạch, hay checklist nội bộ.
  2. Chỉ dựa vào CONTEXT được cung cấp — không phỏng đoán, không dùng kiến thức ngoài tài liệu.
  3. Với bảng: đọc theo từng hàng, từng cột — cấm ghép nhầm dữ liệu giữa các hàng khác nhau.
  4. Với câu hỏi về mục đích/công dụng: phải liệt kê đầy đủ thông tin đi kèm trong bảng (đối tượng phân tích, ứng dụng, dự án liên quan...) — cấm tóm tắt quá ngắn làm mất chi tiết quan trọng.
  5. Luôn trích dẫn nguồn cho mỗi ý chính bằng ký hiệu [n] khớp với thứ tự đoạn trong CONTEXT.
  6. Trước khi kết luận "không có thông tin": phải đọc lại toàn bộ từng đoạn trong CONTEXT, quét kỹ số liệu và từng hàng bảng — vì phần lớn câu trả lời nằm giữa đoạn văn hoặc trong bảng, không nằm ở câu đầu tiên.
  7. Chỉ khi đã đọc hết mà thực sự không có thông tin, mới được trả lời đúng câu cố định: "Tôi chưa có đủ thông tin về vấn đề này trong cơ sở dữ liệu nội bộ. Vui lòng liên hệ phòng nghiệp vụ liên quan."

OLLAMA_TEMPERATURE = 0.0 (tất định, không ngẫu nhiên) gắn chặt với quy tắc số 6 ("đọc trước khi kết luận") — nhiệt độ cao hơn sẽ làm mô hình dễ "đoán" thay vì tuân thủ quy tắc đọc kỹ, nên không được tăng nếu chưa xem lại toàn bộ prompt này.

7. Trích dẫn nguồn và tô sáng câu — citation_matcher.py, sentence_splitter.py

Câu trả lời của mô hình chứa các ký hiệu [n] do chính mô hình chèn vào theo quy tắc số 5 ở trên. Khi người dùng bấm vào một chip trích dẫn hoặc mở ngăn kéo nguồn, hệ thống làm thêm một bước để tô sáng đúng câu (hoặc đúng ô bảng) trong đoạn đã trích — không phải cả đoạn:

  • Hai nguồn ứng viên, gộp chung để chấm điểm:
    • Câu văn xuôi — tách bằng split_sentences, loại bỏ dòng bảng/dòng phân cách bằng is_prose_candidate.
    • Ô dữ liệu trong bảng (extract_table_cell_candidates, bổ sung 14-08-26) — phần lớn tài liệu LD3 là bảng dự toán/thống kê, nên nếu chỉ xét câu văn xuôi thì một chunk toàn bảng sẽ không có gì để tô sáng, dù bằng chứng thật nằm ngay trong ô đó. Hàm này tách từng ô của dòng dữ liệu (bỏ qua dòng tiêu đề cột và dòng phân cách |---|---|), bỏ **in đậm** markdown, rồi lọc lại bằng đúng bộ lọc is_prose_candidate — ô quá ngắn hoặc thuần số ("1", "Có = 1") vẫn bị loại như một câu quá ngắn.
  • Toàn bộ ứng viên (câu + ô bảng) của đoạn, cộng câu trả lời, được mã hóa vector trong một lệnh gọi duy nhất (không phải một lệnh cho mỗi đoạn — tránh hàng loạt lượt suy luận CPU nối tiếp gây giật trên máy hạn chế RAM).
  • Một ứng viên chỉ được tô sáng nếu vượt ngưỡng tuyệt đối (cosine ≥ 0.72) nổi bật hẳn so với điểm trung bình cả nhóm ứng viên trong đoạn (cao hơn mean ít nhất 0.08 — không phải so với ứng viên xếp thứ nhì). Chỉ dùng ngưỡng tuyệt đối từng gây lỗi thực tế: với câu hỏi tổng quát, toàn bộ 8/8 câu trong một đoạn "chủ đề gần đúng" đều vượt ngưỡng 0.55 cũ — tô sáng cả đoạn coi như không tô gì cả. Dùng mean thay vì so với hạng nhì là cố ý: ứng viên thắng cuộc càng mạnh thì tự kéo mean lên cao, khiến điều kiện càng khắt khe hơn.
  • Tối đa 2 ứng viên được tô sáng mỗi đoạn — tô quá nhiều cũng là một dạng "không chỉ ra được cái nào mới đúng là nguồn".

Tóm tắt tham số mặc định

Tham số Giá trị Ý nghĩa
CHUNK_SIZE / CHUNK_OVERLAP 1024 / 250 kích thước & độ chồng lấn đoạn (đơn vị: từ, giới hạn cuối theo token thật)
EMBED_MODEL BAAI/bge-m3 mô hình vector ngữ nghĩa, 1024 chiều
RETRIEVAL_TOP_K 4 số đoạn cuối cùng đưa vào prompt
PREFETCH_TOP_N 50 số ứng viên mỗi nhánh trước khi hợp nhất RRF
HYBRID_RRF_K 60 hằng số hợp nhất RRF (giá trị chuẩn học thuật)
RERANKER_ENABLED / ngưỡng RAM bật / 3.0GB bật xếp hạng lại nếu đủ RAM
RERANK_CANDIDATE_K 20 số ứng viên đưa vào reranker trước khi cắt còn 4
HYDE_ENABLED / độ dài tối đa tắt / 80 ký tự mở rộng câu hỏi ngắn bằng câu trả lời giả định
OLLAMA_TEMPERATURE 0.0 tất định — gắn với quy tắc "đọc trước khi kết luận" trong prompt
Ngưỡng tô sáng (câu + ô bảng) cosine ≥ 0.72, cao hơn mean cả đoạn ≥ 0.08 tránh tô sáng cả đoạn khi câu hỏi quá chung chung

Mô hình cách ly dữ liệu theo đơn vị

Đây là phần quan trọng nhất về bảo mật. Tài liệu của mỗi đơn vị phải hoàn toàn vô hình với đơn vị khác.

Tầng Cơ chế
Định danh DepartmentId là claim tùy chỉnh, được ApplicationUserClaimsPrincipalFactory gắn vào cookie khi đăng nhập
API .NET Mọi controller đọc mã đơn vị từ User.FindFirstValue(AppClaimTypes.DepartmentId)không bao giờ lấy từ body, query hay route
EF Core Mọi truy vấn tài liệu đều lọc .Where(d => d.DepartmentId == departmentId)
Hợp đồng với worker .NET truyền mã đơn vị tường minh trong mọi lời gọi; worker không bao giờ tự suy ra
Truy xuất department_id là bộ lọc must của Qdrant, áp lên cả hai nhánh vector ngữ nghĩa và BM25 — một kết quả phải thỏa mãn bộ lọc trên chính nhánh đã tìm ra nó
Nguyên tắc fail-closed retriever.py báo lỗi ValueError nếu department_id rỗng, thay vì trả về kết quả không lọc
Xóa dữ liệu department_id là tham số bắt buộc ở endpoint xóa của worker, được AND vào bộ lọc xóa

Trường hợp xóa dữ liệu quan trọng hơn vẻ ngoài của nó: tên file văn bản hành chính rất hay trùng nhau giữa các đơn vị (hai phòng cùng tải lên Báo cáo 2023.doc), nên một lệnh xóa theo tên file mà không giới hạn đơn vị sẽ âm thầm hủy dữ liệu của đơn vị khác.

Mô hình dữ liệu

SQL Server qua EF Core, 8 migration. Các bảng:

Bảng Mục đích
AspNetUsers + các bảng Identity xác thực; bổ sung FullName, DepartmentId, đường dẫn ảnh đại diện
Departments đơn vị (khóa chính dạng chuỗi, tối đa 20). Dữ liệu mẫu: IT, HR, ADMIN
Documents thông tin tài liệu + Status (Pending/Processing/Ready/Failed/PartiallyIngested)
Conversations phiên hội thoại theo người dùng, tiêu đề sinh tự động từ câu hỏi đầu tiên
ChatMessages dòng thời gian tin nhắn, Role (User/Assistant/System), SourceDocumentIdsJson
Feedbacks thích/không thích + bình luận, duy nhất theo cặp (người dùng, tin nhắn)
SystemLogs nhật ký kiểm toán — hành động, nhóm, mức độ, người dùng, đơn vị, chi tiết JSON
AiConfigs bản ghi duy nhất (Id=1): nhà cung cấp, mô hình, temperature, topK đang dùng
AiProviderKeys khóa chính là tên nhà cung cấp; EncryptedKey là chuỗi đã mã hóa

Chỉ mục chính: (đơn vị, trạng thái) trên Documents, (người dùng, thời gian cập nhật) trên Conversations, (hội thoại, thời gian tạo) trên ChatMessages, thời gian giảm dần trên SystemLogs.

Kho vector: collection Qdrant ld3_knowledge, schema v3 — hai vector có tên (dense 1024 chiều, bm25 dạng thưa với Modifier.IDF) cùng payload document_id, department_id, chunk_index, text, original_name.

Danh sách API

.NET (/api/*, xác thực bằng cookie — mọi controller đều [Authorize])

Method Đường dẫn Ghi chú
POST /api/chat/send luồng SSE
POST /api/chat/citations/expand xem chi tiết trích dẫn
GET · POST /api/documents danh sách (giới hạn theo đơn vị) · tải lên
GET /api/documents/{id} · /{id}/download
DELETE /api/documents/{id}
POST /api/feedback
GET /api/account/avatar
GET /api/admin/notifications yêu cầu quyền RequireAdmin

Worker Python (X-Worker-Api-Key bắt buộc ở mọi route trừ /health)

Method Đường dẫn Ghi chú
GET /health kiểm tra sống, không cần xác thực
POST /api/ingest multipart, một tài liệu
POST /api/query luồng RAG dạng SSE
POST /api/documents/sources/expand lấy nhiều đoạn đầy đủ + tô sáng câu
DELETE /api/documents/delete department_id bắt buộc
GET · POST /api/llm/status · /api/llm/models phục vụ trang quản trị

Tài liệu API tương tác tại http://localhost:8001/docs khi worker đang chạy.

Các quyết định thiết kế và lý do

Quyết định Lý do
Hai tiến trình, không phải một Toàn bộ stack RAG (torch, Docling, EasyOCR, sentence-transformers) chỉ có trên Python. Viết lại bằng .NET là không khả thi; nhúng Python vào cùng tiến trình cũng vậy.
Không lộ worker ra trình duyệt Chỉ một ranh giới tin cậy duy nhất. Trình duyệt chỉ giữ cookie; khóa dùng chung của worker không bao giờ rời khỏi máy chủ.
Bộ chia đoạn tự viết, không dùng HierarchicalChunker của Docling Văn bản hành chính chủ yếu là bảng. Bộ chia tự viết giữ nguyên vẹn từng dòng `
Truy xuất lai vector + BM25, hợp nhất trên máy chủ Văn bản hành chính tiếng Việt đầy mã số, số hiệu nghị định và danh từ riêng mà truy xuất theo ngữ nghĩa hay bỏ sót. Chỉ mục TEXT của Qdrant chỉ dùng để lọc, nên BM25 thật sự đòi hỏi một vector thưa có tên với Modifier.IDF.
RETRIEVAL_TOP_K = 4 Đã hạ dần 12 → 8 → 4. Mô hình nhỏ chạy nội bộ bị "lạc" giữa các đoạn nhiễu; ít đoạn nhưng đúng thì tốt hơn nhiều đoạn.
OLLAMA_TEMPERATURE = 0.0 Cho kết quả ổn định, lặp lại được. Điều này gắn chặt với quy tắc "đọc trước khi trả lời" trong mẫu prompt RAG — đừng tăng lên nếu chưa xem lại quy tắc đó.
Chốt chặn RAM cho OCR và reranker Chạy trên máy tính cá nhân. OCR thực sự cần ~4-5GB trống; reranker cần ~2GB. Cả hai đều kiểm tra RAM trống và giảm chất lượng có kiểm soát thay vì tràn bộ nhớ.
Không dùng Docker Máy ảo WSL2 của Docker Desktop chiếm hơn ~0,7GB RAM chỉ để chạy hai container nhẹ. Qdrant chạy bằng file thực thi Windows trực tiếp.
asyncio.to_thread cho tác vụ nặng CPU Đọc PDF, tạo vector và truy xuất đều là tác vụ chặn luồng. Không có nó, một lần nạp tài liệu sẽ làm nghẽn mọi truy vấn /api/query đang chạy song song.
Razor render phía máy chủ, không dùng SPA Dự án một người. SPA sẽ nhân đôi khối lượng bảo trì mà người dùng không được lợi gì thêm.
Chống XSS: chỉ dùng textContent, không bao giờ innerHTML Nội dung mô hình sinh ra và nội dung tài liệu đều bị coi là không đáng tin. Mọi giá trị động trong chat.js đều đi qua textContent hoặc createTextNode, kể cả phần tô sáng.

Công nghệ sử dụng

Backend (.NET) — ASP.NET Core (.NET 10), Razor Pages + Web API, C# 13 · EF Core 10.0 (SQL Server) · ASP.NET Core Identity · SignalR · SSE

Worker AI (Python 3.11+) — FastAPI 0.115+ / uvicorn · LlamaIndex Core 0.11+ · Docling 2.0+ · sentence-transformers / BAAI/bge-m3 · fastembed (Qdrant/bm25) · BAAI/bge-reranker-v2-m3 · pyvi (tách từ tiếng Việt) · EasyOCR · Jinja2 · pydantic-settings · structlog · psutil

Nhà cung cấp mô hình — Ollama (mặc định, chạy nội bộ, qwen2.5:3b), OpenAI, Anthropic, Gemini, qua factory build_chat_llm() với định tuyến theo từng request

Dữ liệu — Qdrant 1.11+ (file thực thi Windows, đã kiểm thử với server v1.18.2) · SQL Server Express

Giao diện — Razor Pages render phía máy chủ, JavaScript thuần (wwwroot/js/chat.js), không dùng framework


Cấu trúc mã nguồn

chatbot/
  Controllers/Api/      ChatController (SSE), DocumentsController, Feedback, Account, Notifications
  Services/             Ai/ (cấu hình, khóa nhà cung cấp), Chat/, Documents/
  Infrastructure/       AiWorker/ (HttpClient + DTO), Storage/, Identity/, Authorization/, Audit/
  Models/               các entity EF + enum
  data/                 ApplicationDbContext + tài liệu hành chính mẫu
  Migrations/           8 migration EF Core
  Pages/                Razor Pages — Account/, Admin/, Chat/
  Hubs/                 DocumentHub (SignalR)
  Workers/              DocumentIngestionWorker (dịch vụ nền)
  wwwroot/js/
    chat.js             điểm vào — chỉ nối dây (submit, listener lúc tải trang)
    chat/               10 ES module: dom, sse, think-parser, bubbles, highlight,
                        drawer-state, drawer, document-view, citations, feedback

  worker/               Worker AI FastAPI (Python)
    app/api/            health, ingest, query, llm, documents
    app/services/       loader, chunker, embedder, retriever, vectorstore,
                        llm_router, prompt_builder, citation_matcher, sentence_splitter
    app/schemas/        model Pydantic cho request/response

  process/              không gian làm việc của quy trình AI (xem Bản đồ tài liệu)
  RAG_test/             bộ đánh giá chất lượng truy xuất + script migrate collection
  qdrant-native/        file thực thi Qdrant + dữ liệu (không đưa lên git)

Cài đặt

Yêu cầu trước

Công cụ Ghi chú
Windows 10/11 môi trường phát triển hiện tại
.NET 10 SDK https://dotnet.microsoft.com/download/dotnet/10.0
Python 3.11+ môi trường ảo cho worker
SQL Server Express 2017+ instance nội bộ, xác thực Windows (Trusted_Connection=True)
Ollama cho Windows https://ollama.com/download/windows — cài dưới dạng dịch vụ Windows
Qdrant (bản native) tải qdrant-x86_64-pc-windows-msvc.zip từ trang phát hành Qdrant, giải nén vào qdrant-native/
LibreOffice chỉ cần nếu muốn hỗ trợ tệp .doc nhị phân cũ

Không dùng Docker. Không dùng Redis — pipeline nền arq/Redis đã bị gỡ bỏ vì không còn dùng đến.

1. Cấu hình thông tin bí mật (bắt buộc — thiếu bước này hệ thống không chạy được)

Tệp appsettings.json được đưa lên git với các giá trị bí mật để trống có chủ đích. Giá trị thật phải nằm ở nơi không bao giờ được commit. Chọn một trong hai cách:

# Cách A — dùng user-secrets của .NET (khuyến nghị)
dotnet user-secrets init
dotnet user-secrets set "AiWorker:ApiKey"          "<chuỗi ngẫu nhiên dài>"
dotnet user-secrets set "Bootstrap:AdminEmail"     "admin@example.gov.vn"
dotnet user-secrets set "Bootstrap:AdminPassword"  "<mật khẩu mạnh>"

# Cách B — dùng appsettings.Development.json (đã bị git bỏ qua) với cùng các khóa trên

Sau đó đặt cùng giá trị khóa worker vào tệp môi trường của worker (worker/.env, cũng bị git bỏ qua):

WORKER_API_KEY=<đúng bằng giá trị AiWorker:ApiKey>

Lưu ý:

  • Nếu AiWorker:ApiKey không khớp WORKER_API_KEY, mọi lời gọi chat và nạp tài liệu đều lỗi 401.
  • Nếu các khóa Bootstrap:* để trống, RoleSeeder sẽ ghi log bootstrap_admin_skipped reason=no_config và không tạo tài khoản quản trị — bạn vẫn đăng ký được người dùng thường nhưng không vào được trang quản trị.
  • Tài khoản quản trị chỉ được tạo một lần, ở lần chạy đầu, và chỉ khi email đó chưa tồn tại. Đổi mật khẩu trong cấu hình sau này không làm đổi mật khẩu tài khoản đã tạo.

2. Mô hình, môi trường ảo, cơ sở dữ liệu, collection

# Tải mô hình chạy nội bộ mặc định
ollama pull qwen2.5:3b

# Môi trường ảo Python cho worker
cd worker
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e .
pip install -e ".[test]"      # tùy chọn: cài pytest để chạy kiểm thử
deactivate

# Áp dụng migration EF Core
cd ..
dotnet ef database update

# Tạo collection Qdrant lần đầu (chạy qdrant.exe trước — xem mục Chạy hệ thống)
cd worker
$env:PYTHONPATH = "<thư-mục-gốc-dự-án>"
.venv\Scripts\python.exe -m RAG_test.migrate_collection --yes

Chạy hệ thống

Ba cửa sổ dòng lệnh, theo đúng thứ tự này. Quy trình khởi động hằng ngày đầy đủ, các bước kiểm tra trước khi chạy và bảng xử lý sự cố nằm ở RUNBOOK.md — dưới đây là bản rút gọn.

# Cửa sổ 1 — Qdrant (dữ liệu lưu tại qdrant-native\storage\)
cd qdrant-native; .\qdrant.exe

# Cửa sổ 2 — Worker Python. ĐỢI đến khi thấy "Application startup complete" (~20-40 giây lần đầu).
cd worker; .\.venv\Scripts\Activate.ps1; uvicorn app.main:app --port 8001 --reload

# Cửa sổ 3 — Ứng dụng web .NET
dotnet watch --urls http://localhost:5101 --environment Development

Chạy .NET trước khi worker sẵn sàng sẽ gây lỗi 502 ở các request đầu tiên.

Trang Địa chỉ
Đăng nhập / Đăng ký http://localhost:5101/Account/Login · /Account/Register (mã đơn vị: IT, HR, ADMIN)
Chat http://localhost:5101/Chat
Quản lý tài liệu http://localhost:5101/Admin/Documents
Cấu hình AI (quản trị) http://localhost:5101/admin/ai-settings
Tài liệu API worker · kiểm tra sống http://localhost:8001/docs · /health
Bảng điều khiển Qdrant http://localhost:6333/dashboard

Câu trả lời đầu tiên có thể mất 30–60 giây trong lúc qwen2.5:3b được nạp vào bộ nhớ.


Triển khai

Hệ thống chưa được triển khai. Ứng dụng mới chỉ chạy trên máy tính phát triển. Phần này là danh sách những việc còn thiếu, viết thẳng thắn thay vì một quy trình triển khai, để không ai nhầm cấu hình môi trường phát triển là cấu hình đã sẵn sàng cho vận hành thật.

Những việc bắt buộc phải xong trước khi mở cho người dùng thật:

Hạng mục Hiện tại Cần làm
AllowedHosts "*" đặt đúng tên miền — để * là mở đường cho tấn công Host header
HTTPS đang chạy HTTP nội bộ bật TLS, UseHttpsRedirection, HSTS, cờ Secure + SameSite cho cookie đăng nhập (cookie chứa mã đơn vị, nên mất cookie là lộ dữ liệu chéo đơn vị)
Kết nối CSDL Trusted_Connection=True, TrustServerCertificate=True tài khoản riêng và chứng chỉ hợp lệ khi CSDL chuyển sang máy khác
Worker chỉ lắng nghe localhost:8001, bảo vệ bằng một khóa tĩnh đặt trên mạng nội bộ + tường lửa chỉ cho máy .NET gọi; nếu không khóa sẽ đi qua mạng
Qdrant localhost:6333, chưa bật xác thực đặt QDRANT_API_KEY và chặn cổng bằng tường lửa — nơi này chứa vector của mọi đơn vị, truy cập trực tiếp là vượt qua toàn bộ cơ chế cách ly
Quản lý bí mật user-secrets / tệp cục bộ dùng kho bí mật thật hoặc biến môi trường được tiêm vào lúc chạy
Thông báo lỗi trang lỗi chi tiết của môi trường dev kiểm tra môi trường vận hành không lộ stack trace hay đường dẫn tệp
Sao lưu chưa có SQL + qdrant-native/storage/ + App_Data/uploads/ đều chứa dữ liệu không thể tạo lại
Giám sát tiến trình ba cửa sổ dòng lệnh chạy tay dịch vụ Windows / systemd / reverse proxy
Đóng gói container chưa có chưa có phương án container; sẽ phải xem lại quyết định không dùng Docker

Đã ghi nhận trong: process/general-plans/backlog/harden-config-before-server-deploy_NOTE_05-08-26.md.


Bản đồ tài liệu

Tài liệu được chia theo vòng đời, không theo chủ đề. Biết tệp nào là nguồn chuẩn cho nội dung gì sẽ tránh được tình trạng tài liệu trùng lặp rồi lệch nhau.

Tệp Là nguồn chuẩn cho Vòng đời
README.md (tệp này) Giới thiệu, thiết kế hệ thống, cài đặt, tình trạng triển khai Lâu dài
RUNBOOK.md Vận hành hằng ngày: khởi động, kiểm tra, xử lý sự cố, lệnh hay dùng Lâu dài
process/context/all-context.md Kiến trúc chi tiết, mẫu thiết kế, biến môi trường, lược đồ CSDL, quy ước Lâu dài, có định tuyến
process/context/tests/all-tests.md Bộ chạy kiểm thử, câu lệnh, phần còn thiếu Lâu dài
process/context/planning/all-planning.md Chuẩn hóa hình dạng bản kế hoạch Lâu dài
process/development-protocols/ Quy tắc quy trình cho AI agent (23 tệp, all-development-protocols.md là bộ định tuyến) Lâu dài
process/features/{tính-năng}/ Kế hoạch và báo cáo theo từng tính năng Theo tính năng
process/general-plans/active/ Việc đang làm Tạm thời
process/general-plans/completed/ Việc đã xong, lưu trữ Lịch sử
process/general-plans/backlog/ Ghi chú việc hoãn lại Đến khi làm
worker/README.md Hướng dẫn riêng cho worker Lâu dài
CLAUDE.md · AGENTS.md Điểm vào cho AI agent (xem mục dưới) Lâu dài
BAO-CAO-HE-THONG.md Báo cáo tình trạng hệ thống bằng tiếng Việt — dành cho quản lý dự án Theo đợt rà soát
*.en.md Bản tiếng Anh của các tài liệu trên Song song

process/context/all-context.mdprocess/development-protocols/all-development-protocols.mdbộ định tuyến — chúng liệt kê và chỉ đường tới các tệp sâu hơn chứ không chứa toàn bộ nội dung. Đọc bộ định tuyến trước, rồi theo bảng để tới đúng tệp cần.


Quy trình phát triển với AI (RIPER-5)

Dự án này được phát triển với một quy trình AI agent có cấu trúc tên là RIPER-5: RESEARCH → SPEC → INNOVATE → PLAN → VALIDATE → EXECUTE → UPDATE PROCESS (Nghiên cứu → Đặc tả → Đề xuất phương án → Lập kế hoạch → Thẩm định → Thực thi → Cập nhật quy trình). Mỗi giai đoạn có một agent riêng với quyền hạn công cụ riêng, và ranh giới giữa các giai đoạn được thực thi nghiêm ngặt — không được nhảy thẳng vào viết mã với công việc không tầm thường.

Phần hỗ trợ đi kèm: vòng lặp thẩm định kế hoạch và vòng lặp thẩm định thực thi (mỗi vòng có bộ đếm và giới hạn số lần lặp), khoảng 15 agent theo giai đoạn và chuyên trách, khoảng 37 kỹ năng quy trình, cùng một bộ validator tự kiểm tra tính nhất quán của chính quy trình.

Lưu ý quan trọng nếu bạn clone kho mã này: phần tài liệu của quy trình được đưa lên git (process/, CLAUDE.md, AGENTS.md — 95 tệp), nhưng phần cài đặt thực thi thì không. Các thư mục .claude/, .agents/.codex/ đều bị git bỏ qua, nghĩa là:

  • .claude/agents/*.md (định nghĩa agent), .claude/skills/*/SKILL.md (các kỹ năng), các hook và script validator đều không có khi clone mới.
  • CLAUDE.mdprocess/development-protocols/ tham chiếu tới các đường dẫn đó rất nhiều. Những tham chiếu này sẽ không tìm thấy tệp.
  • Vì vậy quy trình này không thể tái lập chỉ từ kho mã. Nó được ghi lại ở đây vì các sản phẩm quy trình trong process/ thực sự hữu ích — các bản kế hoạch, đặc tả và báo cáo giải thích tại sao mã nguồn lại có hình dạng như hiện nay — chứ không phải để bạn clone về rồi chạy được quy trình.

Nếu bạn chỉ quan tâm tới ứng dụng, có thể bỏ qua hoàn toàn thư mục process/. Không có gì trong hệ thống đang chạy phụ thuộc vào nó.


Kiểm thử

Bộ kiểm thử Tình trạng Câu lệnh
Worker Python 186 test, đang pass — bộ chia đoạn, hợp nhất truy xuất, xếp hạng lại, HyDE, khớp trích dẫn, cách ly đơn vị, lược đồ vector, đọc tệp cd worker; .venv\Scripts\python.exe -m pytest app/ -q
Đánh giá truy xuất recall@k / MRR / nDCG so sánh vector thuần vs lai vs lai+xếp hạng lại cd worker; $env:PYTHONPATH="<thư-mục-gốc>"; .venv\Scripts\python.exe -m RAG_test.eval_retrieval EVAL 5
C# chưa có — chưa có project test
JavaScript chưa có — chưa có bộ kiểm thử

Các test của worker dùng Qdrant giả lập nên không cần dịch vụ nào đang chạy. Riêng phần cách ly dữ liệu theo đơn vị có test trực tiếp (test_document_delete_tenant.py, test_citation_expand_tenant.py, test_document_chunks_tenant.py).


Vấn đề đã biết và giới hạn

Về mặt tổ chức

  • Chưa có test cho C# và JavaScript. Phần .NET — nơi chứa toàn bộ xử lý claim đơn vị và bề mặt xác thực — hiện chỉ được kiểm tra thủ công qua trình duyệt. Đã ghi nhận tại process/general-plans/backlog/add-csharp-unit-tests-for-pure-citation-functions_NOTE_16-07-26.md, trong đó đề xuất điểm khởi đầu rẻ nhất (hai hàm thuần không phụ thuộc CSDL).
  • Chưa có CI/CD. Không có gì tự động chạy bộ test hay các validator. Mọi cổng kiểm soát đều thủ công.
  • Chưa triển khai. Xem mục Triển khai.
  • Quy trình AI không tái lập được từ kho mã. Xem lưu ý ở mục trên.

Về ứng dụng

  • loader.py (835 dòng) và chunker.py (564 dòng) vẫn là các tệp lớn. Độ dài phản ánh độ phức tạp thật cộng với phần chú thích khá dày, nên mức ưu tiên tách thấp. (wwwroot/js/chat.js trước đây dài 1088 dòng đã được tách thành 10 ES module trong wwwroot/js/chat/, tệp entry còn 146 dòng.)
  • Độ trễ câu trả lời đầu tiên. qwen2.5:3b nạp lần đầu khiến câu hỏi đầu mất 30–60 giây.
  • OCR và reranker tự giảm chất lượng khi thiếu RAM theo thiết kế — OCR từ chối chạy khi còn dưới ~4GB trống và báo lỗi bằng tiếng Việt; reranker âm thầm bỏ qua khi dưới 3GB và truy xuất quay về thứ tự RRF. Đúng như thiết kế, nhưng dễ gây bất ngờ nếu không biết trước.
  • RETRIEVAL_TOP_K = 4 được tinh chỉnh cho mô hình nhỏ chạy nội bộ. Nếu chuyển sang mô hình đám mây lớn thì con số này nên tăng; hệ thống hiện không tự điều chỉnh theo nhà cung cấp.

Về công cụ và tài liệu

  • Hook pre-commit của code-review-graph báo lỗi UnicodeEncodeError (cp1252) trên Windows khi tệp thay đổi có chứa tiếng Việt. Không chặn — commit vẫn thành công.
  • .agents/skills/ là một bản sao độc lập chứ không phải liên kết tới .claude/skills/, nên hai cây thư mục có thể lệch nhau. Cả hai đều bị git bỏ qua nên chỉ ảnh hưởng máy của lập trình viên.
  • Tên máy của lập trình viên xuất hiện trong chuỗi kết nối SQL Server ở appsettings.jsonRUNBOOK.md. Mức độ nhẹ — đây là instance nội bộ dùng Trusted_Connection, không có mật khẩu — nhưng là chi tiết không cần thiết trong một kho mã công khai, và dù sao cũng phải thay trước khi triển khai thật.
  • Chưa có tệp worker/.env.example. Tệp môi trường của worker phải tạo thủ công; xem config.py để biết các khóa có thể đặt và giá trị mặc định.

Báo cáo rà soát đầy đủ kèm mức độ nghiêm trọng và cách tái hiện nằm ở process/general-plans/active/system-audit_05-08-26/system-audit_REPORT_05-08-26.md, mỗi phát hiện có một ghi chú riêng trong process/general-plans/backlog/. Bản tóm tắt tiếng Việt dành cho quản lý dự án: BAO-CAO-HE-THONG.md.


Lưu ý bảo mật

Những tính chất thiết kế cần giữ gìn — nhiều điểm trong số này rất quan trọng và rất dễ vô tình phá vỡ:

  • Không bao giờ commit thông tin bí mật. appsettings.json nằm trên git và được để trống các giá trị bí mật. Giá trị thật phải nằm ở user-secrets, appsettings.Development.json, hoặc worker/.env — tất cả đều bị git bỏ qua. Kết quả build cũng không bao giờ được commit; một lần cấu hình sai đường dẫn build đã từng vô tình đưa bản sao của tệp cấu hình bí mật lên git.
  • Mã đơn vị lấy từ cookie, không bao giờ lấy từ dữ liệu người dùng gửi lên. Bất kỳ đoạn mã nào nhận department_id từ body hay query để phân quyền đều là lỗi.
  • Bộ lọc must phải áp lên cả hai nhánh truy xuất. Thêm một nhánh truy xuất thứ ba mà quên bộ lọc đơn vị sẽ mở ra lỗ rò dữ liệu chéo mà các test hiện tại không phát hiện được.
  • department_id là bắt buộc khi xóa ở worker. Đặt lại giá trị mặc định cho tham số này sẽ âm thầm mở lại nguy cơ mất dữ liệu chéo đơn vị; đã có test khẳng định tham số này không có giá trị mặc định.
  • Dùng secrets.compare_digest để so khóa worker — dùng != thông thường sẽ để lộ thông tin qua thời gian phản hồi ngay tại ranh giới xác thực duy nhất của worker.
  • Dùng textContent, tuyệt đối không dùng innerHTML cho mọi nội dung đến từ mô hình hoặc từ tài liệu, trong chat.js. Một hàm tiện ích dựng HTML bằng chuỗi sẽ phá vỡ điều này mà không ai nhận ra.
  • Khóa API của nhà cung cấp được mã hóa khi lưu bằng Data Protection (IDataProtector("ai-provider-keys.v1")), chỉ giải mã để chuyển tiếp trong nội bộ máy chủ, và không bao giờ trả về trình duyệt (giao diện chỉ cho ghi).
  • Kiểm tra tệp tải lên theo nhiều lớp — danh sách MIME cho phép danh sách phần mở rộng cho phép, kiểm tra kích thước cả trước và sau khi ghi, và chặn path traversal trong LocalFileSystemStorage.

Để báo lỗi bảo mật, vui lòng mở issue trên kho mã — dự án chưa có kênh liên hệ bảo mật riêng.


Giấy phép

Kho mã chưa có tệp giấy phép. Mặc định mọi quyền được bảo lưu cho đến khi bổ sung.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages