№ 48
HR Helpdesk AI là trợ lý hỏi đáp chính sách nhân sự nội bộ, giúp nhân viên tra cứu thông tin nhanh với nguồn kiểm chứng, đồng thời hỗ trợ HR quản lý tài liệu và chuyển các trường hợp nhạy cảm đến người phụ trách bằng AI RAG.
Trong một doanh nghiệp vài trăm đến vài nghìn nhân viên, thông tin chính sách nhân sự — nghỉ phép, bảo hiểm, phúc lợi, OT, nội quy, quy trình hành chính — nằm rải rác trong nhiều văn bản Word/PDF không có công cụ tra cứu hiệu quả. Hệ quả xuất hiện ở cả ba phía:
| Cách đang dùng | Vì sao chưa giải quyết được |
|---|---|
| Gửi file qua email / lưu Drive | Nhân viên không biết file nào chứa thông tin cần; không ai đọc hàng chục trang để tìm một dòng |
| Wiki / intranet nội bộ | Tìm theo từ khóa, không hiểu câu hỏi tự nhiên; nội dung nhanh lỗi thời và không gắn ngày hiệu lực |
| FAQ tĩnh | Không bao phủ hết; câu hỏi thực tế luôn có biến thể theo hoàn cảnh |
| Chatbot kịch bản (rule-based) | Không mở rộng được; mỗi chính sách mới phải viết lại kịch bản |
| Chatbot LLM thuần, không RAG | Bịa chính sách. Trong nghiệp vụ nhân sự, một con số sai về ngày phép hay mức trợ cấp là rủi ro không chấp nhận được |
Bài toán không phải "làm một chatbot biết trả lời". Bài toán là làm một hệ thống biết khi nào nó không được phép trả lời — và chứng minh được điều đó bằng số liệu.
① Phân quyền theo phòng ban. Mỗi tài liệu có phạm vi áp dụng: toàn công ty hoặc một/nhiều phòng ban cụ thể. Nhân viên phòng Môi giới không được thấy quy chế chỉ dành cho Tài chính – Kế toán, kể cả gián tiếp qua trích dẫn hay qua cache. Quyền truy cập là thứ không được phép sai theo xác suất.
② Dữ liệu cá nhân và khiếu nại. "Tôi còn mấy ngày phép?", "Tôi muốn khiếu nại quản lý" — những câu này agent không được tự trả lời, phải chuyển cho người thật (HITL). Nhưng "chính sách nghỉ phép quy định thế nào?" thì phải trả lời. Ranh giới giữa hai loại rất mảnh.
③ Tài liệu có phiên bản và ngày hiệu lực. Chính sách thay đổi. Trả lời theo bản cũ nguy hiểm ngang với bịa. Hệ thống phải quản lý version, rollback, soft-delete, chỉ dùng bản active, và nói rõ ngày hiệu lực trong trích dẫn.
④ Tiếng Việt nội bộ. Người dùng gõ không dấu ("nghi phep thai san"), dùng viết tắt phòng ban, và gọi cùng một khái niệm bằng nhiều từ ("tải lên" / "upload"). Mọi so khớp theo ký tự phải chạy trên cùng một dạng chuẩn hóa, nếu không kết quả phụ thuộc vào việc người dùng có gõ dấu hay không.
| Nhóm | Yêu cầu |
|---|---|
| Ba vai trò | Employee hỏi đáp · HR quản lý tài liệu và ticket · Admin vận hành hệ thống nhưng không đọc được nội dung câu hỏi/ticket của nhân viên (BR-03, content-blind) |
| Bốn loại phản hồi | Trả lời có căn cứ · từ chối vì ngoài phạm vi · từ chối vì thiếu tài liệu · thông báo đã chuyển HR. Không bao giờ trộn suy đoán vào câu từ chối (FR-02.5) |
| Trích dẫn bắt buộc | Mọi câu trả lời có căn cứ phải kèm tên tài liệu, điều/khoản, trang nguồn và ngày hiệu lực (NFR-05) |
| Ngưỡng chất lượng | Recall@5 ≥ 85% · Faithfulness ≥ 90% · Citation accuracy ≥ 95% (NFR-08); 0 trường hợp agent tự trả lời câu nhạy cảm (OBJ-05) |
| Hiệu năng | p50 ≤ 10 giây cho câu hỏi trong phạm vi tài liệu (NFR-01b) |
Phạm vi được xác định rõ từ đầu. Các hạng mục sau nằm ngoài bản MVP bốn tuần, với lý do ghi lại trong hồ sơ thiết kế: xử lý nghiệp vụ HR thật (duyệt phép, tính lương), tích hợp chấm công/bảng lương, trả lời câu hỏi gắn dữ liệu cá nhân của người hỏi, tích hợp Slack/Teams/Zalo, OCR và XLSX, fine-tune model riêng, business dashboard.
Ba service độc lập chạy bằng Docker Compose sau một Nginx reverse proxy, cùng một tầng dữ liệu dùng chung.
| Thành phần | Công nghệ | Trách nhiệm |
|---|---|---|
| Frontend | Next.js 16 · React 19 · Tailwind | Giao diện chat có chip trích dẫn và badge tin cậy, kho tài liệu, ticket, bảng điều khiển quản trị — 19 route ứng dụng |
| Backend API | NestJS 11 (TypeScript) | Auth/session, RBAC, hội thoại, ticket, analytics, audit log append-only, kênh góp ý ứng dụng, proxy tới Agent |
| Agent Service | FastAPI · LangGraph · LangChain · Python 3.11 | Toàn bộ pipeline AI: định tuyến, truy xuất, phân quyền, sinh câu trả lời, kiểm chứng, ingestion tài liệu |
| PostgreSQL 16 + pgvector | — | Dữ liệu nghiệp vụ và vector store. Backend và Agent dùng schema/DB role tách biệt |
| Redis 7 | — | Cache embedding câu hỏi và tóm tắt tài liệu. Tùy chọn — biến rỗng là tắt cache, hệ thống vẫn chạy đúng |
| Nginx 1.27 | — | Gateway duy nhất ra ngoài. Agent không expose trực tiếp ra internet |
Backend và Agent nói chuyện qua HTTP nội bộ, xác thực bằng INTERNAL_TOKEN. Chỉ Backend tiếp xúc
Frontend; Agent nằm sau một biên duy nhất.
Toàn bộ luồng là một LangGraph 12 node, mỗi node bị bao bởi giới hạn thời gian theo chặng và deadline còn lại của cả request (28 giây).
| # | Chặng | Trách nhiệm |
|---|---|---|
| 1 | Context Manager | Chọn lịch sử liên quan theo relevance + recency, dựng authorization context từ vai trò/phòng ban đã xác thực, cấp ngân sách token riêng cho từng chặng. Tóm tắt hội thoại không bao giờ được coi là bằng chứng chính sách |
| 2 | Smart Router | Chọn nhánh xử lý; rule tất định chạy trước, chỉ hỏi model khi thật sự mơ hồ. Sáu nhánh: CONVERSATION · SYSTEM_QUERY · CREATE_TICKET · SENSITIVE_PERSONAL · REFUSE · SEARCH_POLICY |
| 3 | Authorized Retrieval | Vector search (pgvector) + full-text tiếng Việt + khớp cụm/viết tắt, hợp nhất bằng RRF, rerank trên pool 40 ứng viên xuống top-8, mở rộng sang chunk lân cận cùng điều khoản — tất cả trong cùng một phạm vi hiển thị SQL |
| 4 | Evidence Gate | Quyết định từng chunk có đủ tư cách thành bằng chứng, tính confidence, khẳng định lại bất biến phân quyền. Thiếu căn cứ → REFUSAL_NO_BASIS; đủ → EvidenceSet |
| 5 | Grounded Generator | Kiểm kê fact, lập kế hoạch bao phủ, soạn câu trả lời chỉ từ EvidenceSet đã được phép. Metadata trích dẫn dựng ở server từ chunk đã retrieve, không lấy từ output của LLM |
| 6 | Answer Verifier | Sáu kiểm tra tất định: schema · provenance · support · coverage · citation · visibility. Hợp lệ → trả về; lỗi sửa được → 1 lần retry; vẫn lỗi → từ chối |
| 7 | Respond + Trace | Đóng gói theo contract Pydantic StrictModel — lớp phòng thủ cuối giữa ba service. Trace ghi route, latency, token, chi phí, kết quả verification; không ghi câu hỏi, lịch sử, bằng chứng hay nội dung tài liệu |
Nguồn: src/agents/graph.py · src/agents/nodes/ · src/services/answer_verifier.py
① PostgreSQL + pgvector làm luôn vector store. Không thêm vector DB riêng. Lý do không phải để tiết kiệm container: dữ liệu nghiệp vụ và chunk nằm cùng một transaction boundary, nên có thể lọc quyền truy cập bằng SQL ngay trong câu truy vấn retrieval. Quyết định ngày đầu này về sau trở thành nền tảng của toàn bộ authorization.
② LLM không bao giờ quyết định phân quyền. Bản đầu có một "Department Guard" chạy bằng LLM sau khi đã sinh câu trả lời. Nó được thay hoàn toàn bằng cơ chế tất định:
visibility_filter() sinh biểu thức SQL từ vai trò + phòng ban đã xác thực, dùng toán tử overlap
&& để tận dụng GIN index trên applicable_departments.is_visible_metadata() kiểm tra lại tương đương bằng Python trên evidence đã materialize — hai lớp
độc lập cùng một luật.AuthorizedRetriever buộc mọi đường đi (sub-query, section expansion, parent lookup) phải finalize
dưới cùng một authorization context.employee — phân quyền mặc định theo chiều hẹp nhất.③ Không đủ bằng chứng thì từ chối, không đoán. evidence_quality_decision() chấm từng chunk
một: điểm cao ở chunk này không cho phép chunk khác đi qua. Không qua cổng thì generator không
được gọi.
④ Verifier tất định, retry đúng một lần. Toàn bộ logic kiểm chứng nằm trong một AnswerVerifier
duy nhất, hoàn toàn tất định, và tối đa một targeted retry trên cùng EvidenceSet — không
retrieval lại, vì retrieval lại chỉ che giấu vấn đề của bằng chứng. So với thiết kế ban đầu retry tới
6 lần, cách này giảm đáng kể latency và chi phí.
⑤ Hybrid retrieval cho tiếng Việt nội bộ. Sáu bước: vector · full-text · khớp cụm/viết tắt · RRF ·
rerank · section expansion. Điểm rerank không được dùng làm ngưỡng từ chối — min_similarity luôn
đọc cosine thô, để lớp an toàn chống bịa không đổi khi ta chỉnh cách sắp xếp.
⑥ Quan sát được nhưng không rò nội dung. Trace store ghi route, retrieval, verification, retry, latency theo chặng, token và chi phí từng lời gọi — nhưng nguyên tắc số một là không lưu nội dung. Ledger chẩn đoán chỉ giữ hash 16 ký tự thay vì nguyên văn tài liệu, và có test khẳng định điều đó.
Upload → kiểm tra loại file và dung lượng → lưu file → parse (pypdf / python-docx, giữ heading,
bảng, trang nguồn, character offset) → chunk theo cấu trúc chính sách tiếng Việt
(Chương II > Mục 1 > Điều 12) → embed → ghi vào pgvector → kích hoạt version mới.
Một Quality Gate tất định chặn giữa: nếu bóc tách ra quá ít ký tự trên mỗi trang hoặc quá nhiều
chunk không xác định được mục, version dừng ở pending_review chờ HR xem bằng mắt, thay vì âm thầm
trở thành nguồn trích dẫn. Xoá là soft-delete có thùng rác và khôi phục được.
| Hạng mục | Số liệu |
|---|---|
| Agent (Python) | 87 file trong src/ · ~23.800 dòng |
| Backend (TypeScript) | 149 file · ~15.900 dòng |
| Frontend | 122 file · ~19.800 dòng |
| Test Python | 2.030 pass · 35 skip · coverage 79,7% |
| Test Backend | 461 Jest + 57 integration trên Postgres thật |
| Migration | 13 Alembic (Agent) + 9 TypeORM (Backend), có test khoá sơ đồ |
prompt_id + version + hash, ghi vào
trace. Đổi prompt là một thay đổi truy vết được.docker compose up -d, có health check cho từng
service.Toàn bộ đánh giá chạy trên golden dataset khoá bằng SHA256, bằng bộ chấm tất định tự viết — không dùng RAGAS và không dùng LLM-as-a-judge, nên kết quả tái lập được.
| Metric | Giá trị | Mục tiêu |
|---|---|---|
| Recall@8 | 100% (80/80 case) | ≥ 85% |
| Full Recall@8 | 100% | — |
| MRR | 0,9516 | — |
| Precision@8 | 86,15% | — |
| Section hit rate | 95,0% | — |
| Metric | Giá trị | Mục tiêu |
|---|---|---|
| Trích đúng tài liệu (trên câu đã trả lời) | 100% | ≥ 95% |
| Trích đúng mục (trên câu đã trả lời) | 96,7% | ≥ 95% |
| Citation precision | 100% | — |
Không có trích dẫn nào trỏ tới tài liệu không tồn tại hoặc không nằm trong phạm vi người hỏi được
đọc. Cơ chế đứng sau con số này là kiến trúc chứ không phải may mắn: chunk không xác định được
section_path bị SQL loại từ đầu nên không bao giờ trở thành nguồn trích dẫn, và metadata trích dẫn
được dựng ở server từ chunk đã retrieve chứ không lấy từ output của model.
| Chỉ số | Kết quả |
|---|---|
| Rò rỉ dữ liệu nhạy cảm (F2, 22 case) | 0 |
| Rò rỉ tài liệu chéo phòng ban (F4, 10 probe insider/outsider) | 0 |
| Trích dẫn vượt quyền | 0 |
Không có rò rỉ trong bất kỳ lần chạy nào được lưu trong repo. Đây là kết quả trực tiếp của quyết định thiết kế ②: phân quyền là tất định và nằm ở tầng SQL, nên nó không phụ thuộc vào việc model hôm đó trả lời thế nào.
| Metric (81 request tuần tự) | Giá trị | Mục tiêu |
|---|---|---|
| p50 | 1.653 ms | ≤ 10.000 ms ✅ |
| Trung bình | 5.760 ms | — |
| Error rate | 0% | — |
| Chi phí mỗi request | ~0,0018 USD · ~11.400 token | — |
Hơn nửa số request hoàn tất dưới 3 giây. Về chi phí, với 1.000 câu hỏi mỗi ngày thì tiền model rơi vào khoảng 54 USD/tháng — chi phí vận hành không phải rào cản ở quy mô doanh nghiệp vài nghìn nhân viên.
Nhóm cũng đã dựng sẵn bộ đo tải CCU hai arm (một arm dùng mock provider để đo trần của phần mềm, một arm dùng provider thật để đo trải nghiệm người dùng), chạy 960 request qua bốn mức đồng thời 1/5/10/25. Giá trị của phép đo này là nó chỉ đích danh thành phần cần tối ưu trước tiên bằng bằng chứng số thay vì suy đoán.
| Chiều | Đánh giá | Căn cứ |
|---|---|---|
| Kỹ thuật | Đã chứng minh | Kiến trúc chạy end-to-end trên ba service thật; retrieval 100% recall; trích dẫn chính xác 100%; không ghi nhận rò rỉ trong bất kỳ lần chạy nào |
| An toàn | Đã chứng minh | Phân quyền tất định ở tầng SQL, hai lớp kiểm tra độc lập, 0 leak trên cả hai bộ test an toàn |
| Vận hành | Sẵn sàng cho pilot | Docker Compose một lệnh, CI/CD đầy đủ, giám sát Prometheus/Grafana với 15 alert rule, runbook xử lý sự cố |
| Kinh tế | Khả thi | ~0,0018 USD mỗi câu hỏi; chi phí model không phải rào cản |
Hệ thống khả thi cả về kỹ thuật lẫn kinh tế. Các hạng mục còn lại trước khi triển khai diện rộng đều đã được định vị chính xác và có kế hoạch xử lý ở mục 4; phần chưa chốt được nêu ở phụ lục.
Ba giai đoạn, theo thứ tự giá trị mang lại cho người dùng.
Mục tiêu: nhân viên hỏi gì cũng nhận được thứ có ích, kể cả khi tài liệu chưa phủ hết.
Mục tiêu: sản phẩm tốt lên theo thời gian thay vì đứng yên sau khi bàn giao.
Mục tiêu: từ một trợ lý HR thành nền tảng tri thức nội bộ.
Một tính năng mới không được phép khoét lỗ trong bốn điều đó, vì một cái lỗ như vậy rất khó chứng minh là chỉ mở đúng cho một trường hợp.