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ì.
- Tính năng
- Thiết kế hệ thống
- Công nghệ sử dụng
- Cấu trúc mã nguồn
- Cài đặt
- Chạy hệ thống
- Triển khai
- Bản đồ tài liệu
- Quy trình phát triển với AI (RIPER-5)
- Kiểm thử
- Vấn đề đã biết và giới hạn
- Lưu ý bảo mật
- Giấy phép
- 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 (fastembedQdrant/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à
.docnhị phân cũ (chuyển sang.docxbằ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.
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.
┌────────────────────────────┐
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ả.
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.
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.
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.
Đâ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ùngHierarchicalChunkercó 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ínhAutoTokenizercủaBAAI/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
.txtthuần không qua Docling nên dùngSentenceSplitter(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ệ.
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.
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.
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.
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):
- 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ộ.
- 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.
- 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.
- 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.
- 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. - 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.
- 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.
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ằngis_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ọcis_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.
- Câu văn xuôi — tách bằng
- 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) VÀ 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".
| 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 |
Đâ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.
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.
.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.
| 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. |
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
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ô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.
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ênSau đó đặ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:ApiKeykhông khớpWORKER_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,RoleSeedersẽ ghi logbootstrap_admin_skipped reason=no_configvà 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.
# 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 --yesBa 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 DevelopmentChạ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ớ.
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.
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.md và process/development-protocols/all-development-protocols.md là bộ đị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.
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/ và .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.mdvàprocess/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ó.
| 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ề 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.jstrước đây dài 1088 dòng đã được tách thành 10 ES module trongwwwroot/js/chat/, tệp entry còn 146 dòng.)- Độ trễ câu trả lời đầu tiên.
qwen2.5:3bnạ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-graphbáo lỗiUnicodeEncodeError(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.jsonvàRUNBOOK.md. Mức độ nhẹ — đây là instance nội bộ dùngTrusted_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; xemconfig.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.
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.jsonnằ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ặcworker/.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_idtừ body hay query để phân quyền đều là lỗi. - Bộ lọc
mustphả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_idlà 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ùnginnerHTMLcho mọi nội dung đến từ mô hình hoặc từ tài liệu, trongchat.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 và 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.
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.