№ 67
TalentScreen AI giúp HR sàng lọc ứng viên bằng cách phân tích JD và CV, đối chiếu từng yêu cầu với bằng chứng cụ thể thay vì chỉ đưa ra một điểm số. Hệ thống hỗ trợ HR nhìn rõ điểm mạnh, khoảng trống và chuẩn bị phỏng vấn, trong khi quyết định cuối cùng vẫn do con người thực hiện.
Mã đề tài: EDU-16
Nhóm thực hiện: Team T010
Sản phẩm: Nền tảng AI hỗ trợ sàng lọc CV và phỏng vấn nhân sự
Demo: https://agentrecruiter.id.vn/
Trong tuyển dụng nhân sự công nghệ, một vị trí có thể nhận hàng chục hoặc hàng trăm CV. HR phải đọc từng hồ sơ, đối chiếu với Job Description và xác định ứng viên nào phù hợp để chuyển sang vòng phỏng vấn.
Quá trình này tồn tại nhiều vấn đề:
Nhiều hệ thống CV matching hiện nay dựa trên từ khóa hoặc trả về một similarity score chung. Ví dụ, hệ thống có thể kết luận ứng viên phù hợp 80%, nhưng HR vẫn không biết:
TalentScreen AI tập trung giải quyết câu hỏi:
Làm thế nào để đối chiếu từng requirement trong JD với evidence có thật trong CV, tạo ra kết quả ổn định, có thể giải thích và hỗ trợ HR ra quyết định?
Hệ thống không được chỉ tìm từ khóa giống nhau. Ví dụ:
JD: Yêu cầu tối thiểu 3 năm kinh nghiệm Python Backend.
CV: Có 2 năm phát triển Backend bằng Python.
Ứng viên có đúng kỹ năng nhưng chưa đáp ứng số năm. Vì vậy, kết quả phù hợp phải là partial, không phải supported.
Một ví dụ khác:
JD: Có kinh nghiệm triển khai hệ thống trên production.
CV: Xây dựng một personal project sử dụng Docker.
CV có nhắc đến Docker nhưng không đủ evidence để kết luận ứng viên đã có kinh nghiệm production.
TalentScreen AI hiện tập trung vào hai vai trò chính.
HR sử dụng hệ thống để:
Admin sử dụng hệ thống để:
TalentScreen AI được xây dựng theo nguyên tắc human-in-the-loop.
AI được phép:
AI không được:
TalentScreen AI sử dụng kiến trúc phân lớp, tách riêng AI reasoning khỏi các thành phần cần tính xác định.
flowchart TD
A["HR cung cấp JD và CV"] --> B["Parse, normalize và chunk tài liệu"]
B --> C["Khóa JD và tạo rubric"]
C --> D["AI phân tích từng requirement"]
D --> E["Local Evidence Validator"]
E --> F["Deterministic Python Scoring"]
F --> G["HR review kết quả"]
G --> H["Interview Copilot"]
H --> I["Speech-to-Text và báo cáo"]
Nguyên tắc quan trọng:
AI đề xuất, code local kiểm chứng, Python tính điểm và con người quyết định.
Hệ thống hỗ trợ:
File PDF được xử lý cục bộ bằng PyMuPDF. Hệ thống không gửi trực tiếp byte PDF cho mô hình.
Quá trình xử lý gồm:
Nếu PDF là bản scan và không có text layer, hệ thống có thể sử dụng OCR hoặc trả warning để người dùng biết tài liệu chưa được trích xuất đầy đủ.
Khi HR tạo Job và xác nhận JD, hệ thống phân tích JD để tạo rubric. Sau khi rubric được chốt, nội dung JD không được chỉnh sửa trực tiếp.
HR vẫn có thể sửa các thông tin quản trị như:
Nếu muốn thay đổi nội dung JD hoặc requirement, HR phải tạo phiên bản Job mới.
Đây là một tính năng có chủ đích vì nếu JD bị thay đổi sau khi đã có kết quả đánh giá:
Mỗi rubric có:
rubric_versionrequirement_idRequirement được chia thành ba tier:
must_haveshould_havenice_to_haveRequirement ID và trọng số được khóa trong suốt vòng đời của Job.
Hệ thống sử dụng LangGraph để điều phối các bước phân tích.
Luồng xử lý chính:
parse_documents
→ analyze_jd
→ analyze_cv
→ match_requirements
→ validate_evidence
→ build_evaluation
Các bước phân tích JD và CV có thể chạy song song để giảm thời gian xử lý.
Hệ thống sử dụng provider abstraction, cho phép thay đổi mô hình mà không làm thay đổi API và business logic.
Các chế độ có thể gồm:
Model phải trả về structured output theo JSON Schema. Nếu output sai schema, thiếu requirement hoặc thay đổi ID, hệ thống coi đây là provider error thay vì âm thầm sử dụng kết quả sai.
Mỗi requirement được đánh giá độc lập theo bốn trạng thái:
| Trạng thái | Ý nghĩa |
|---|---|
supported | Ứng viên đáp ứng đầy đủ requirement và có evidence hợp lệ |
partial | Ứng viên đáp ứng một phần nhưng còn thiếu điều kiện |
missing | CV không thể hiện requirement |
insufficient_information | Chưa đủ dữ liệu để kết luận an toàn |
Ví dụ:
Requirement: Python, FastAPI và tối thiểu 3 năm kinh nghiệm Backend.
CV:
- Có Python.
- Có FastAPI.
- Chỉ xác minh được 2 năm kinh nghiệm.
Kết quả:
status = partial
Model không được tự tạo thêm requirement, xóa requirement hoặc thay đổi ID đã được rubric khóa.
Evidence là đoạn trích nguyên văn từ CV được sử dụng để chứng minh assessment.
Evidence validator chạy bằng code local và kiểm tra:
Hệ thống có thể chuẩn hóa:
Tuy nhiên, hệ thống không chấp nhận một câu diễn giải có ý nghĩa gần giống làm evidence nguyên văn.
Nguyên tắc fail-closed:
Không có verified evidence → không nhận điểm
Nếu model trả về supported nhưng evidence không được xác minh, local code sẽ hạ trạng thái hoặc cho requirement nhận 0 điểm.
Final score được tính bằng Python, không sử dụng điểm do AI tự đề xuất.
Quy tắc cơ bản:
| Trạng thái | Hệ số điểm |
|---|---|
supported và có verified evidence | 100% requirement weight |
partial và có verified evidence | 50% requirement weight |
missing | 0% |
insufficient_information | 0% |
| Không có verified evidence | 0% |
Công thức:
earned_points = requirement_weight × status_factor
Tổng điểm nằm trong khoảng từ 0 đến 100.
Recommendation band:
| Tổng điểm | Kết quả |
|---|---|
| 80–100 | strong_match |
| 65–79 | good_match |
| 50–64 | partial_match |
| Dưới 50 | weak_match |
Điểm số chỉ là công cụ sắp xếp và hỗ trợ review, không phải quyết định tuyển dụng cuối cùng.
Sau vòng screening, TalentScreen AI sử dụng kết quả đánh giá để hỗ trợ chuẩn bị phỏng vấn.
Interview Copilot phân tích:
partial.insufficient_information.Từ đó, hệ thống đề xuất câu hỏi phù hợp.
Ví dụ:
CV cho biết ứng viên sử dụng FastAPI nhưng chưa thể hiện kinh nghiệm production.
Câu hỏi đề xuất:
Bạn có thể mô tả một hệ thống FastAPI đã triển khai lên production, quy mô người dùng và vấn đề kỹ thuật khó nhất bạn từng xử lý không?
Câu hỏi nên được truy xuất từ question bank đã được kiểm duyệt hoặc được validator kiểm tra trước khi hiển thị.
AI chỉ đề xuất câu hỏi. Người phỏng vấn vẫn là người lựa chọn và kiểm soát nội dung buổi phỏng vấn.
Trong buổi phỏng vấn, hệ thống hỗ trợ chuyển giọng nói thành văn bản.
Nội dung transcript được liên kết với:
Speech-to-Text giúp:
Do nhận diện giọng nói có thể sai, transcript phải được xem là bản nháp và cho phép con người kiểm tra hoặc chỉnh sửa.
Sau khi kết thúc phiên, hệ thống tổng hợp báo cáo gồm:
AI hỗ trợ tóm tắt nhưng không tự đưa ra quyết định tuyển dụng.
Báo cáo cần lưu cả:
Admin có thể quản lý:
Quyền truy cập được kiểm soát bằng authentication và role-based access control.
Dữ liệu của mỗi tổ chức cần được tách bằng organization_id hoặc tenant_id, tránh người dùng của tổ chức này truy cập dữ liệu của tổ chức khác.
CV chứa dữ liệu cá nhân nên hệ thống cần tuân thủ các nguyên tắc:
.env.TalentScreen AI có tính khả thi cao vì hệ thống được chia thành các module độc lập:
Việc thay đổi model không làm thay đổi toàn bộ hệ thống. Nhóm có thể chuyển giữa model API và model local thông qua provider interface.
Sản phẩm đã có phiên bản web để người dùng trải nghiệm tại:
Hệ thống có thể triển khai theo hai hướng:
Ưu điểm:
Hạn chế:
Ưu điểm:
Hạn chế:
Kiến trúc hiện tại cho phép sử dụng cả hai hướng mà không phải thay đổi API nghiệp vụ.
Giai đoạn đầu có thể sử dụng:
Mỗi case benchmark cần có:
Synthetic data phải được ghi rõ là synthetic, không được gọi là dữ liệu thực tế.
Hệ thống có thể được đánh giá bằng hai nhóm benchmark.
Đo:
Với cùng JD, CV, rubric, model và cấu hình, chạy độc lập 10 lần rồi đo:
Quality gate đề xuất:
JSON Schema success = 100%
Fabricated evidence = 0
Status macro-F1 >= 0.80
Supported precision >= 0.90
Mỗi case có score range <= 2 điểm qua 10 lần chạy
Recommendation agreement = 100%
Protected-attribute invariance = 100%
Đây là ngưỡng mục tiêu và chỉ được công bố là đạt sau khi benchmark thực sự được chạy trên dataset đã review.
TalentScreen AI không yêu cầu doanh nghiệp thay đổi toàn bộ quy trình tuyển dụng.
Hệ thống có thể được sử dụng như một lớp hỗ trợ:
Nhận CV
→ Sàng lọc
→ HR review
→ Phỏng vấn kỹ thuật
→ Báo cáo
→ Con người quyết định
Giá trị chính:
| Rủi ro | Phương án |
|---|---|
| AI bịa evidence | Kiểm chứng evidence bằng code local |
| Kết quả dao động | Khóa rubric, temperature thấp, structured output và scoring bằng Python |
| Model bỏ sót kỹ năng | Benchmark, retrieval và cải thiện dataset |
| Speech-to-Text nhận sai | Cho phép kiểm tra và chỉnh sửa transcript |
| Prompt injection trong CV | Xem JD/CV là untrusted data và không cho tài liệu thay đổi system instruction |
| Lộ dữ liệu cá nhân | RBAC, tenant isolation, không log raw CV và chính sách xóa dữ liệu |
| Rubric thay đổi giữa các ứng viên | Khóa JD và tạo version mới khi cần chỉnh sửa |
| AI ảnh hưởng quyết định tuyển dụng | Human-in-the-loop và không tự động tuyển/loại |
| Chi phí API cao | Provider abstraction, caching phù hợp và local model |
| Model local chậm | Queue xử lý nền, quantization và giới hạn concurrency |
Nhóm có thể phát triển model:
TalentScreen-Qwen
Hướng thực hiện:
Mục tiêu không phải pretrain foundation model từ đầu mà là làm chủ toàn bộ vòng đời:
Trong tương lai, HR có thể tải nhiều CV cho cùng một Job.
Luồng đề xuất:
HR tải batch CV
→ CV vào hàng đợi
→ Worker xử lý nền
→ Cập nhật trạng thái
→ HR xem kết quả khi hoàn thành
Điều này giúp HR không phải tải từng CV và chờ request xử lý đồng bộ.
TalentScreen AI có thể tích hợp với:
CV mới được đồng bộ vào Job và đưa vào hàng đợi đánh giá theo chính sách của doanh nghiệp.
Interview Copilot có thể được mở rộng với:
Các track có thể gồm:
Hướng phát triển:
Báo cáo có thể được mở rộng với:
Admin có thể quản lý các gói:
Mỗi gói có thể giới hạn:
Việc cấp gói và thay đổi giới hạn phải được ghi lại trong audit log.
Các hướng phát triển quan trọng:
Hệ thống cần theo dõi:
Nếu model hoặc prompt mới không đạt benchmark, hệ thống phải giữ phiên bản cũ hoặc rollback.
TalentScreen AI không tập trung vào việc tạo ra một điểm matching thật nhanh. Sản phẩm tập trung vào việc làm cho kết quả có thể tin tưởng và kiểm chứng.
Ba điểm khác biệt chính:
Đánh giá theo từng requirement
HR biết ứng viên đáp ứng và còn thiếu điều gì.
Evidence được xác minh cục bộ
Model không thể tự tạo evidence rồi nhận điểm.
Điểm số deterministic
AI không trực tiếp tính final score hoặc quyết định tuyển dụng.
Thông điệp cốt lõi của sản phẩm:
AI đề xuất — hệ thống kiểm chứng — con người quyết định.
Phần này mô tả cách TalentScreen được thiết kế, đóng gói, kiểm thử, triển khai, giám sát và vận hành như một hệ thống phần mềm hoàn chỉnh — không dừng lại ở mô hình AI, mà bao gồm toàn bộ vòng đời từ commit mã nguồn đến workload chạy thật trên Kubernetes. Mọi mô tả kỹ thuật dưới đây được xác minh trực tiếp trên hệ thống thật (mã nguồn tại nhánh hoanganh, cụm Kubernetes aws-kubespray, namespace talentscreen-dev) tại thời điểm biên soạn tài liệu, không suy đoán.
Team xác định 4 mục tiêu xuyên suốt khi thiết kế hạ tầng, thay vì chỉ "làm cho chạy được":
kubectl apply hay build tay.talentscreen-app), cấu hình triển khai (talentscreen-gitops) và trạng thái runtime (cụm Kubernetes) — ba nguồn sự thật độc lập, không trộn lẫn.Repository ứng dụng được lưu trữ song song ở hai remote với vai trò khác nhau: github (GitHub, mirror công khai phục vụ nộp bài/tham chiếu) và origin (GitLab tự triển khai tại gitlab.agentrecruiter.id.vn, đóng vai trò nguồn kích hoạt CI/CD thật). Đây là chủ đích thiết kế, không phải trùng lặp: pipeline CI/CD chỉ lắng nghe GitLab.
Nhánh làm việc chính trong giai đoạn phát triển là hoanganh. Jenkinsfile khóa cứng điều kiện này ở stage Branch validation: pipeline so khớp branch build với biến ALLOWED_BRANCH = 'hoanganh' và chủ động error() nếu không khớp, kèm chú thích rõ ràng "main is GitHub-publish-only and never flows through this pipeline; no other branch triggers a deploy." Nói cách khác, main chỉ dùng để công bố mã nguồn, còn toàn bộ luồng build → scan → deploy chỉ chạy trên nhánh phát triển đã được chỉ định — tránh trường hợp một nhánh không kiểm soát vô tình đẩy image ra registry hoặc GitOps.
Repository cấu hình triển khai (talentscreen-gitops) được tách hoàn toàn khỏi mã nguồn ứng dụng, chỉ chứa Helm chart và giá trị cấu hình theo môi trường — đúng triết lý GitOps: mã nguồn quyết định ứng dụng là gì, GitOps repo quyết định đang chạy phiên bản nào, ở đâu.
Jenkins được triển khai ngay trong cụm Kubernetes (pod jenkins-0, namespace jenkins), sử dụng Kubernetes Pod Template để cấp phát container xử lý theo từng bước thay vì một agent cố định — mỗi lần build là một pod tạm thời với các container chuyên biệt (kaniko, Trivy, git...), tận dụng chính hạ tầng Kubernetes làm build farm.
Pipeline (Jenkinsfile) gồm 18 stage tuần tự, chia thành 4 nhóm:
| Nhóm | Stage | Vai trò |
|---|---|---|
| Kiểm tra mã nguồn | Checkout, Branch validation, Dependency integrity | Đảm bảo đúng commit, đúng nhánh, lock file khớp với requirements*.txt |
| Kiểm thử backend | Lint (ruff), Test (pytest), AI safety/eval tests, AI Deterministic Baseline | Chặn build nếu vi phạm chuẩn code, test thất bại, hoặc kết quả AI benchmark trôi khỏi baseline đã duyệt |
| Kiểm thử frontend | Frontend npm ci, lint, typecheck, build | Đảm bảo Next.js build sạch trước khi đóng gói image |
| Build & phát hành | Metadata, Build/scan/push (API, Demo, Web), Digest verify, Update/Validate/Commit GitOps | Đóng gói, quét lỗ hổng, đẩy registry, xác minh digest, cập nhật GitOps |
Một số quyết định kỹ thuật đáng chú ý trong pipeline:
AI safety/eval tests và AI Deterministic Baseline chạy độc lập — benchmark AI (bộ case đã duyệt, phát hiện bằng chứng bịa đặt, drift policy) là điều kiện chặn build, không phải chạy nền tham khảo.HIGH_VULNERABILITY_EXCEPTION_OWNER, _TICKET, _EXPIRES) và ngày hết hạn chưa qua — không tồn tại cờ boolean "bỏ qua scan". Exception có audit trail qua ticket và lịch sử cấu hình Jenkins.Dependency integrity): CI dừng ngay nếu requirements*.txt bị sửa mà lock file (requirements-api.lock, requirements-demo.lock, requirements-dev.lock, kèm requirements-lock.inputs.sha256) chưa được regenerate — loại bỏ khả năng cài đặt dependency ngoài kiểm soát trong image production.Ứng dụng gồm 3 thành phần được build thành 3 image độc lập (API/FastAPI, Demo, Web/Next.js), mỗi image dùng multi-stage Dockerfile để tách rõ giai đoạn build (chứa toolchain, dependency dev) khỏi runtime image (chỉ chứa những gì cần chạy). Image production không chứa mã nguồn của các component khác, không chứa test, không chứa Git metadata.
Việc build image trong Jenkins dùng kaniko (gcr.io/kaniko-project/executor) thay vì Docker-in-Docker — cho phép build image container ngay trong một pod Kubernetes không cần đặc quyền --privileged hay socket Docker của node, giảm bề mặt tấn công của build agent.
Mỗi image được gắn nhãn OCI chuẩn, trong đó org.opencontainers.image.revision mang giá trị VCS_REF — chính là Git SHA của commit đã build ra image — được truyền vào qua build-arg và giữ lại tới runtime (ENV VCS_REF=${VCS_REF}), làm cơ sở cho endpoint "Release health" trong Admin Observability đọc ra trực tiếp từ tiến trình đang chạy, không phải suy luận.
Harbor (self-hosted, v2.15.2) đóng vai trò registry riêng tư duy nhất cho toàn bộ image production, chạy trong cụm với các thành phần harbor-core, harbor-registry, harbor-jobservice, harbor-portal, harbor-database (PostgreSQL nội bộ), harbor-redis, harbor-trivy (scanner tích hợp).
Mỗi image được đẩy lên với tag sha-<git-commit-ngắn> — truy vết trực tiếp một-một về commit đã sinh ra nó, không dùng tag nổi (latest) cho bất kỳ image production nào. Sau khi build và scan đạt, Jenkins so khớp digest build cục bộ với digest thật trên Harbor (Digest verify summary) trước khi cho phép ghi vào GitOps — loại bỏ rủi ro registry trả về một artifact khác với artifact vừa build (registry bị thay đổi giữa chừng, hoặc lỗi push âm thầm).
Dung lượng registry được vận hành có kiểm soát: một sự cố thực tế (mục 6.15, sự cố C) buộc phải mở rộng PVC lưu trữ registry và sau đó thực hiện dọn dẹp có kiểm soát (mục 6.16), không xóa artifact bừa bãi.
Argo CD là nguồn triển khai duy nhất vào cụm Kubernetes cho namespace talentscreen-dev — không tồn tại đường nào khác (không CI chạy kubectl apply, không thao tác tay) thay đổi được trạng thái Deployment production. Xác minh trực tiếp trên cụm: Application talentscreen-dev ở trạng thái Synced / Healthy, theo dõi nhánh main của repo talentscreen-gitops.git, đường dẫn charts/talentscreen.
Luồng cập nhật: Jenkins ghi digest 3 image mới vào environments/dev/values.yaml của GitOps repo → commit với message có cấu trúc, chứa rõ nguồn gốc (source: talentscreen-app@<sha>, số build Jenkins, digest từng image) → push với kiểm tra optimistic concurrency: pipeline fetch lại origin/main trước khi push, so sánh với SHA nền đã biết lúc đọc — nếu nhánh GitOps đã bị người khác đẩy thay đổi mới trong lúc build đang chạy, pipeline chủ động dừng lại và báo lỗi thay vì force push đè lên. Credential dùng cho GitOps push được ghi vào file tạm và shred ngay sau khi dùng xong trong cùng step.
Argo CD phát hiện thay đổi trên main, tính diff, và tự đồng bộ vào cụm — hoàn thành vòng lặp GitOps mà không cần Jenkins có quyền truy cập trực tiếp vào Kubernetes API.
Ứng dụng được đóng gói thành một Helm chart (charts/talentscreen) với giá trị cấu hình tách riêng theo môi trường (environments/dev/values.yaml), cho phép cùng một chart phục vụ nhiều môi trường trong tương lai chỉ bằng cách thêm file values mới, không sửa template.
Cụm Kubernetes hiện tại được cấp phát bằng Kubespray trên AWS EC2, gồm 2 node đã xác minh trực tiếp (kubectl get nodes): k8s-master (control-plane) và k8s-worker, cùng chạy containerd làm container runtime và Calico làm CNI, phiên bản Kubernetes v1.35.4. Cụm hiện có một node control-plane duy nhất — đây là mô hình phù hợp cho môi trường DEV/học thuật, không phải triển khai high-availability multi-master.
Trong namespace talentscreen-dev, ba Deployment (talentscreen-api, talentscreen-demo, talentscreen-web) và một StatefulSet (talentscreen-postgres) đều chạy với 1 bản sao (1 replica) — xác nhận trực tiếp qua kubectl get deployment. ConfigMap ứng dụng có annotation checksum/config gắn vào Pod template: khi ConfigMap thay đổi (ví dụ đổi PROMETHEUS_URL), checksum thay đổi theo, buộc Kubernetes tạo rollout mới cho Pod — tránh tình trạng "đổi cấu hình nhưng Pod cũ vẫn chạy config cũ" thường gặp khi dùng ConfigMap trực tiếp.
Namespace của Pod được truyền vào chính container ứng dụng qua Downward API (fieldRef: metadata.namespace → biến môi trường K8S_NAMESPACE) thay vì hard-code — cùng một image chạy đúng ở bất kỳ namespace nào được Helm release vào.
PostgreSQL chạy dưới dạng StatefulSet (talentscreen-postgres-0) trong namespace talentscreen-dev, dữ liệu lưu trên PVC riêng (gp3, mã hoá tại rest theo cấu hình của storage class), kết nối được bảo vệ bằng TLS (certificate talentscreen-postgres-tls do cert-manager cấp, xác nhận READY=True).
Migration schema dùng Alembic, chạy như một PreSync Hook của Argo CD — nghĩa là job alembic upgrade head bắt buộc chạy và thành công trước khi Argo CD cho phép rollout Deployment mới của API. Nếu migration thất bại, rollout ứng dụng bị chặn lại ở đúng bước đó — tránh tình huống kinh điển "code mới chạy trên schema cũ" hoặc ngược lại. Endpoint tự-báo cáo của hệ thống (Alembic / Database schema trên tab Hệ thống của Admin Dashboard) so sánh trực tiếp code_head (đọc từ thư mục alembic/versions đóng gói trong image) với db_current (đọc từ bảng alembic_version thật trong Postgres) và trả về trạng thái IN_SYNC / BEHIND — không suy luận, không cache giả định.
Không có secret nào (mật khẩu DB, API key DeepSeek, service account Firebase...) được hard-code trong mã nguồn, trong Helm values, hay trong GitOps repo. Toàn bộ đi theo một đường duy nhất:
Vault (KV, self-hosted trong cụm)
→ External Secrets Operator (ESO) đọc theo SecretStore "vault-backend"
→ Kubernetes Secret được ESO tạo/đồng bộ tự động (chu kỳ 1h)
→ Mount vào Pod dưới dạng biến môi trường hoặc file (ví dụ credential Firebase)
Xác nhận trực tiếp trên cụm: 5 ExternalSecret đang hoạt động trong namespace talentscreen-dev (talentscreen-database-url, talentscreen-deepseek, talentscreen-harbor-pull, talentscreen-postgres-credentials, talentscreen-runtime), tất cả ở trạng thái SecretSynced=True. GitOps repo — nơi duy nhất ai cũng có thể xem lịch sử Git — vì vậy không bao giờ chứa giá trị secret thật, chỉ chứa tham chiếu đến ExternalSecret.
Firebase Authentication (Google Sign-In + email/password nội bộ) được xác thực hai lớp: phía client lấy Firebase ID token, gửi một lần duy nhất lên /auth/firebase/login (hoặc /auth/firebase/register-hr) dưới dạng JSON body; backend dùng Firebase Admin SDK xác minh token đó, sau đó phát hành session cookie httpOnly riêng của hệ thống cho các request tiếp theo — không dùng Bearer token Firebase lặp lại trên từng request, giảm bề mặt lộ token phía client.
Credential service account Firebase (JSON) được phân phối đúng theo mô hình Vault → ESO → Secret ở mục 6.9, mount read-only vào container tại một đường dẫn cố định, không tồn tại ở bất kỳ đâu trên máy cá nhân trong cấu hình chạy thật. Các biến NEXT_PUBLIC_FIREBASE_* (public theo thiết kế của Firebase Web SDK) được truyền làm build-arg ngay trong bước build image Next.js của Jenkins, đảm bảo frontend build ra đúng project Firebase của môi trường tương ứng.
Firebase Storage (lưu CV) hiện tắt trong môi trường DEV (firebase_cv_storage_enabled=false) — một giới hạn được khai báo rõ ràng trong self-report của hệ thống (AdminSystemHealth), không che giấu.
ingress-nginx là ingress controller duy nhất của cụm, xử lý toàn bộ traffic từ domain công khai vào các Service nội bộ. Chứng chỉ TLS được cert-manager tự động cấp và gia hạn qua 4 ClusterIssuer đã xác nhận (letsencrypt-prod, letsencrypt-staging, platform-internal-ca, selfsigned-bootstrap) — dịch vụ hướng ra domain công khai (agentrecruiter.id.vn, app-dev.agentrecruiter.id.vn, cùng các domain nội bộ khác cho Jenkins/Harbor/Argo CD/Grafana) dùng letsencrypt-prod, xác nhận READY=True cho toàn bộ certificate liên quan.
Ứng dụng expose qua hai Ingress: talentscreen-web (app-dev.agentrecruiter.id.vn) và talentscreen-web-canonical (agentrecruiter.id.vn) — cùng trỏ vào Service Next.js, phục vụ domain phát triển và domain chính thức. Toàn bộ gọi API từ frontend đi qua route proxy cùng-origin /api/backend/... của Next.js thay vì gọi thẳng domain backend riêng — tránh vấn đề CORS và không lộ cấu trúc nội bộ của backend ra trình duyệt.
Hệ thống có một bộ giám sát hạ tầng dùng chung (Prometheus/Grafana/Alertmanager qua kube-prometheus-stack, cùng Loki + Promtail cho log tập trung, xác nhận đang chạy trong namespace monitoring), và thêm một lớp observability riêng do team tự xây ngay trong Admin Dashboard của ứng dụng (tab "Hệ thống") — không nhúng iframe Grafana, không public Prometheus ra ngoài.
Kiến trúc: backend FastAPI đóng vai trò proxy có kiểm soát tới Prometheus nội bộ cụm (kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090) — chỉ gọi được từ trong cụm, không public; frontend Next.js/Recharts hiển thị dữ liệu trả về qua 4 endpoint riêng cho hệ thống (CPU/memory/pod restarts theo từng thành phần, request rate, tỷ lệ lỗi 5xx, độ trễ P95) và tương tự cho LLM Operations. Một số kỹ thuật xử lý dữ liệu đáng chú ý:
or vector(0) trong mọi truy vấn PromQL — đảm bảo một khoảng thời gian thực sự không có sự kiện (ví dụ 0 lỗi 5xx) trả về đúng số 0, phân biệt rõ với "không có dữ liệu" (mất kết nối Prometheus) — UI trước đây từng hiển thị nhầm cả hai trường hợp là biểu đồ trống, gây hiểu lầm.label_replace() gộp tên Pod (đổi liên tục sau mỗi lần rollout) về tên thành phần ổn định (api/web/demo/postgres) trước khi nhóm theo by (...) — tránh tích luỹ vô hạn số chuỗi thời gian (cardinality) qua các lần deploy.postgres_exporter), API trả về {"supported": false, "reason": "no_exporter_configured"} thay vì trả 0 hoặc null giả — UI hiển thị đúng thông điệp "chưa có exporter", không bịa số liệu.Ngoài giám sát hạ tầng, hệ thống theo dõi riêng các lệnh gọi LLM (DeepSeek và mô hình cục bộ) qua một bộ metric Prometheus định nghĩa tường minh trong src/observability/metrics.py, theo hợp đồng nhãn (label) có giới hạn rõ ràng để tránh bùng nổ cardinality:
| Nhóm | Metric | Ý nghĩa |
|---|---|---|
| HTTP (RED) | HTTP_REQUESTS_TOTAL, HTTP_REQUEST_DURATION_SECONDS | Tốc độ, độ trễ request theo endpoint |
| Auth & Database | AUTH_ATTEMPTS_TOTAL, DATABASE_OPERATIONS_TOTAL, DATABASE_OPERATION_DURATION_SECONDS, DATABASE_ERRORS_TOTAL, DATABASE_REACHABLE | Tình trạng xác thực và tầng dữ liệu |
| Evaluation | EVALUATIONS_TOTAL, EVALUATION_DURATION_SECONDS | Số lượt và thời gian chấm điểm CV |
| LLM | LLM_REQUESTS_TOTAL, LLM_REQUEST_DURATION_SECONDS, LLM_ERRORS_TOTAL, LLM_INPUT/OUTPUT/TOTAL_TOKENS_TOTAL, LLM_CACHED_INPUT_TOKENS_TOTAL, LLM_CACHE_MISS_INPUT_TOKENS_TOTAL, LLM_ESTIMATED_COST_USD_TOTAL | Khối lượng, độ trễ, lỗi và token tiêu thụ theo provider/model |
Chi phí ước tính (LLM_ESTIMATED_COST_USD_TOTAL) mặc định tắt — các biến đơn giá (deepseek_input_cost_per_1m_tokens, _output_, _cached_input_) mặc định None, chỉ được bật khi vận hành viên chủ động cấu hình đơn giá thật qua biến môi trường. Khi chưa cấu hình, API trả về {"supported": false, "reason": "pricing_not_configured"} thay vì ước tính sai. Đây là chủ đích thiết kế: không hiển thị một con số chi phí trông có vẻ chính xác nhưng thực chất là suy đoán.
Tab "LLM Operations" trong Admin Dashboard hiển thị: tổng lượt gọi, tỷ lệ lỗi theo provider/loại lỗi/tác vụ, phân vị độ trễ (p50/p95/p99), token theo loại, cache-hit ratio, và chi phí (khi được bật) — toàn bộ tính trực tiếp từ Prometheus thật đang chạy trong cụm, có thể chọn khoảng thời gian (7 ngày / 30 ngày).
Các cơ chế bảo mật được triển khai thật, xác minh được, gồm:
HR, ADMIN), kiểm tra hoàn toàn ở backend qua dependency injection của FastAPI cho từng route — phân quyền ở frontend chỉ phục vụ UX (ẩn nút, không phải cổng kiểm soát thật). Có cơ chế bảo vệ chống tự khoá hệ thống (last-admin-guard): không cho phép hạ quyền/khoá admin cuối cùng còn hoạt động trong một tổ chức.docker-compose.yml cấp local: read_only: true filesystem gốc, cap_drop toàn bộ Linux capability, security_opt bật thêm ràng buộc — không chạy container với quyền ghi tuỳ ý hay capability thừa.fullSession (phiên đăng nhập đầy đủ) từng bị trả về trong log lỗi LLM (get_recent_error_logs/export_error_logs_csv), đã được loại bỏ, chỉ giữ lại bản rút gọn session vốn có.Tất cả sự cố dưới đây là sự cố thật đã xảy ra và được xử lý trong quá trình phát triển — trình bày theo mô hình Problem → Engineering fix → Result, không phải tường thuật thao tác:
A. Guard chặn tự khoá admin cuối cùng bị vô hiệu hoá ngầm bởi lỗi phân loại exception
AuthorizationError — trùng với thông điệp lỗi phân quyền chung của frontend, khiến người dùng không thấy được lý do thật ("đây là admin cuối cùng").InvalidRequestError (mã lỗi invalid_request, HTTP 400) — đúng bản chất của lỗi là request không hợp lệ về nghiệp vụ, không phải thiếu quyền.B. Layout hàng thành viên vỡ (tên bị ngắt từng ký tự)
.team-member-row-main), trình duyệt buộc phải bẻ chữ theo từng ký tự để vừa khung..team-member-actions), theo đúng pattern đã có sẵn (.team-member-plan), không nhồi thêm vào hàng chính.C. Registry Harbor đầy dung lượng, chặn build
harbor-registry (8Gi) đầy 100%, hai lần build liên tiếp thất bại ở bước push image với lỗi "no space left on device".gp3 StorageClass hỗ trợ allowVolumeExpansion: true, mở rộng PVC trực tuyến 8Gi → 30Gi (kubectl patch pvc) — không cần khởi động lại Pod, không mất dữ liệu image đã có.Bound.D. Dung lượng Jenkins/Harbor tích luỹ theo thời gian
E. Rò rỉ dữ liệu phiên đăng nhập đầy đủ trong log lỗi LLM
get_recent_error_logs, export_error_logs_csv) trả về trường fullSession chứa toàn bộ nội dung phiên, vượt quá nhu cầu chẩn đoán lỗi thực tế.fullSession, chỉ giữ trường session đã rút gọn sẵn có từ trước.F. Thiếu dependency firebase-admin trong image production
firebase-admin được khai báo ở requirements.txt (tập dùng cho dev/test) nhưng vắng mặt hoàn toàn trong requirements-api.txt (tập thật sự cài vào image API production) — khiến import firebase_admin thất bại trong Pod thật, mọi request đăng nhập Google/Firebase trả lỗi, trong khi Pod vẫn Running bình thường vì import chỉ được gọi lười (lazy) trong route handler, không ở lúc khởi động.firebase-admin>=6.6.0 vào requirements-api.txt, regenerate requirements-api.lock và requirements-lock.inputs.sha256 bằng đúng quy trình pip-tools đã thiết lập, xác minh import firebase_admin thành công trong venv sạch trước khi đẩy commit.hoanganh (requirements-api.txt:42).vault-raft-snapshot (namespace vault) chạy hằng ngày lúc 03:00, ghi snapshot Raft của Vault vào PVC vault-backup (5Gi) riêng — xác nhận lịch sử job hoàn tất (Completed) trên cụm. Đây là lớp bảo vệ cho toàn bộ hệ thống secret của nền tảng, vì mọi ExternalSecret khác đều phụ thuộc vào Vault còn sống.talentscreen-gitops (đưa digest image về bản trước) — Argo CD tự đồng bộ lại, không cần kubectl rollout undo thủ công và không mất lịch sử thay đổi.flowchart LR
Dev["Developer\nnhánh hoanganh"] --> GitLab["GitLab\n(self-hosted, origin)"]
GitLab -->|"webhook"| Jenkins["Jenkins\n(pod trong cụm)"]
subgraph Pipeline["Jenkins Pipeline"]
direction LR
QA["Lint · Pytest\nAI safety/eval\nDeterministic baseline"] --> FE["Frontend\nnpm ci · lint\ntypecheck · build"]
FE --> Build["Build image\n(kaniko: api/demo/web)"]
Build --> Scan["Trivy scan\n(Critical chặn tuyệt đối)"]
end
Jenkins --> Pipeline
Scan --> Harbor["Harbor Registry\nsha-<git-sha>"]
Harbor --> Verify["Digest verify\n(build vs Harbor)"]
Verify --> GitOpsCommit["Commit values.yaml\n(digest mới) → talentscreen-gitops"]
GitOpsCommit --> ArgoCD["Argo CD\n(phát hiện diff)"]
ArgoCD --> PreSync["PreSync Hook\nalembic upgrade head"]
PreSync --> K8s["Kubernetes\nnamespace talentscreen-dev"]
Dev -.->|"mirror công khai"| GitHub["GitHub"]
flowchart TB
Internet(("Internet")) --> Ingress["ingress-nginx\n+ cert-manager (Let's Encrypt)"]
subgraph AWS["AWS EC2 — Kubespray cluster (containerd, Calico)"]
subgraph NS_APP["namespace: talentscreen-dev"]
Web["talentscreen-web\n(Next.js)"]
Api["talentscreen-api\n(FastAPI)"]
Demo["talentscreen-demo"]
Pg[("talentscreen-postgres\nStatefulSet + TLS")]
Web --> Api
Api --> Pg
end
subgraph NS_PLATFORM["Nền tảng CI/CD & GitOps"]
Jenkins["Jenkins"]
Harbor["Harbor Registry"]
Argo["Argo CD"]
end
subgraph NS_SEC["Bảo mật & Secret"]
Vault["Vault"]
ESO["External Secrets Operator"]
Vault --> ESO
ESO --> NS_APP
end
subgraph NS_OBS["Giám sát"]
Prom["Prometheus"]
Grafana["Grafana"]
Loki["Loki + Promtail"]
end
Api -.->|"metrics"| Prom
NS_APP -.->|"logs"| Loki
Api -->|"proxy nội bộ, không public"| Prom
end
Ingress --> Web
Ingress --> Jenkins
Ingress --> Harbor
Ingress --> Argo
Ingress --> Grafana
Jenkins --> Harbor
Harbor --> Argo
Argo --> NS_APP
| Lớp (Layer) | Công nghệ | Vai trò |
|---|---|---|
| Hạ tầng | AWS EC2, Kubespray, Kubernetes v1.35.4, containerd, Calico CNI | Cấp phát và vận hành cụm Kubernetes |
| CI | Jenkins (Kubernetes Pod Template), kaniko, Trivy | Build, kiểm thử, quét lỗ hổng image |
| Registry | Harbor v2.15.2 | Lưu trữ image riêng tư, scan tích hợp |
| GitOps | Argo CD, Helm | Đồng bộ trạng thái cụm theo Git |
| Database | PostgreSQL (StatefulSet), Alembic | Lưu trữ dữ liệu, quản lý schema migration |
| Secret | HashiCorp Vault, External Secrets Operator | Nguồn sự thật duy nhất cho mọi secret runtime |
| Networking | ingress-nginx, cert-manager (Let's Encrypt) | Định tuyến traffic, TLS tự động |
| Giám sát hạ tầng | kube-prometheus-stack (Prometheus, Grafana, Alertmanager), Loki + Promtail | Metric và log tập trung |
| Giám sát ứng dụng | prometheus-client (Python), Recharts | Native LLMOps/System Observability trong Admin Dashboard |
| Backend | FastAPI, SQLAlchemy (async), asyncpg, LangGraph, Firebase Admin SDK | API, ORM bất đồng bộ, orchestration AI, xác thực |
| Frontend | Next.js 16, React 19 | Giao diện người dùng, BFF proxy tới backend |
| Xác thực | Firebase Authentication (Google Sign-In + email/password) | Định danh người dùng |
Việc xây dựng và vận hành hạ tầng này đòi hỏi và thể hiện các năng lực sau, không dừng lại ở việc biết tên công cụ:
hoanganh đến workload chạy thật trên Kubernetes DEV được tự động hoá hoàn toàn, không có bước kubectl apply thủ công nào trong luồng chính.revision), và mọi thay đổi cấu hình triển khai đều là một commit có thể xem lại trên GitOps repo.