Mục lục (15 phần)
§1Định vị đề tài
Bài toán không phải “làm website cho sinh viên nộp bài rồi AI chấm”. Bài toán là xây một Assessment Engine có thể cấu hình: giảng viên khai báo kỹ năng, bài đánh giá, test case, rubric và trọng số; hệ thống tự thu bài, chạy nhiều bộ chấm độc lập, tổng hợp điểm theo công thức công khai và cập nhật hồ sơ kỹ năng của từng người học.
Đề tài KHÔNG phải là
- Một LMS — không quản lý khoá học, bài giảng, điểm danh.
- Một hệ thống thi online chỉ có trắc nghiệm.
- Một chatbot gửi câu trả lời sang ChatGPT rồi nhận về “8/10”.
Đề tài LÀ
- Một engine chấm đa nguồn: Test Rule AI Human.
- Một môi trường thực thi an toàn cho mã không tin cậy.
- Một mô hình điểm kỹ năng giải thích được tới từng tiêu chí.
Bốn lớp giá trị
Đọc từ dưới lên: lớp dưới cung cấp dữ liệu cho lớp trên. Lớp Engine là “trái tim” — mọi loại bài đều đi qua nó.
Bốn nguyên tắc thiết kế xuyên suốt
- Skill là thực thể trung tâm. Mỗi item chấm điểm phải khai báo nó đóng góp cho kỹ năng nào, bao nhiêu phần trăm.
- Mọi điểm đều truy vết được nguồn. Bấm vào bất kỳ con số nào cũng xem được tiêu chí, bộ chấm và bằng chứng tạo ra nó.
- AI không bao giờ là nguồn điểm duy nhất cho phần có thể kiểm chứng bằng test hoặc luật; AI chỉ là một evaluator trong pipeline.
- Mã bài làm là không tin cậy. Chỉ được build và chạy trong container bị giới hạn, trên máy worker tách khỏi máy chủ chính.
§2Phạm vi & MVP
Rủi ro lớn nhất của đề tài là phình phạm vi. Danh sách MVP dưới đây được khoá ở cuối tuần 2; mọi thứ ngoài danh sách chỉ làm khi MVP đã chạy end-to-end.
MVP Bắt buộc
- Đăng nhập JWT, phân quyền RBAC 3 vai trò
- Quản lý Skill dạng cây 2 cấp
- Quản lý Assessment / Section / Item, câu hỏi, test case, rubric
- Trắc nghiệm một và nhiều đáp án
- Bài lập trình stdin/stdout, 2 ngôn ngữ
- Bài REST API: dự án đóng gói Docker, test HTTP khai báo
- Phỏng vấn kỹ thuật với AI dạng văn bản
- AI feedback cho bài lập trình và REST API
- Hàng đợi + worker + sandbox Docker
- Score engine, skill profile, biểu đồ tiến bộ
- GV duyệt / ghi đè điểm, audit log
- CI trên GitHub Actions, triển khai Docker Compose
Mở rộng Nếu còn thời gian
- AI code review chi tiết theo từng dòng
- Bài SQL chạy trên database sandbox
- Gợi ý học tập theo luật, AI viết lời giải thích
- Phát hiện trùng lặp mã giữa các bài nộp
- Thêm ngôn ngữ C/C++, Java
- Phỏng vấn bằng giọng nói
- Báo cáo nâng cao, xuất PDF
Ngoài phạm vi
- Quản lý khoá học, bài giảng (LMS)
- Ghép việc làm, tuyển dụng
- Giám sát thi qua webcam
- Ứng dụng di động
- Microservices, Kubernetes
16 tuần làm việc, mỗi sinh viên khoảng 12–15 giờ/tuần. Quy mô demo: một lớp khoảng 100 sinh viên. Hạ tầng: 1 máy chủ Linux (4 vCPU, 8 GB RAM) cho API + DB và 1 máy worker riêng. Nếu thực tế là 20 tuần, phần dư dùng cho mục “Mở rộng”, không kéo dài sprint.
§3Tác nhân & chức năng
Ba tác nhân người dùng, hai tác nhân hệ thống. Giảng viên là tác nhân quan trọng nhất vì mọi cấu hình chấm điểm đều do họ tạo ra.
| Tác nhân | Mục tiêu | Chức năng chính |
|---|---|---|
| Administrator | Vận hành hệ thống |
|
| Instructor | Thiết kế và kiểm soát việc đánh giá |
|
| Student | Được đánh giá và biết mình cần cải thiện gì |
|
| Worker (hệ thống) | Chấm bài | Nhận job từ hàng đợi, điều khiển sandbox, chạy test, gọi AI evaluator, ghi kết quả. |
| AI Provider (bên ngoài) | Cung cấp mô hình ngôn ngữ | OpenAI hoặc Gemini, được gọi qua một interface chung để thay thế được. |
Hành trình chính của sinh viên
Hành trình chính của giảng viên
Bước “chạy thử bằng lời giải mẫu” là bắt buộc trước khi publish: lời giải mẫu phải đạt 100% test, nếu không test case đang sai.
§4Mô hình miền
Mười một khái niệm dưới đây là ngôn ngữ chung của cả nhóm. Tên trong code, tên bảng và tên trong báo cáo dùng đúng các từ này.
| Khái niệm | Ý nghĩa | Ví dụ |
|---|---|---|
| Skill | Năng lực đo được, tổ chức thành cây Category → Skill | Backend Development → REST API |
| Assessment | Một bài đánh giá hoàn chỉnh: thời lượng, số lượt làm, đối tượng | Backend Developer – Level 1 |
| Section | Nhóm item cùng chủ đề trong một assessment | Part 2: Database |
| Item | Đơn vị chấm nhỏ nhất, có loại (Quiz, Coding, RestApi, Sql, Interview) và trọng số | Bài 1: CRUD Products |
| ItemSkill | Item đóng góp cho skill nào, tỉ lệ bao nhiêu (nhiều–nhiều) | CRUD Products → REST API 70%, Database 30% |
| Rubric · Criterion | Tiêu chí chấm của một item: trọng số, mô tả từng mức điểm, nguồn chấm | Error handling · 10% · Test |
| TestCase | Đầu vào + kỳ vọng + trọng số; công khai hoặc ẩn | TC02: GET /api/products/abc → 404 |
| Attempt | Một lần sinh viên làm một assessment | Lượt 2 của SV A, 14/03 |
| Submission | Bài nộp cho một item trong một attempt | File zip dự án, mã nguồn, câu trả lời |
| Evaluation | Kết quả của một bộ chấm trên một tiêu chí, kèm bằng chứng | Functional · Test · 85 · “17/20 test đạt” |
| SkillScore · Snapshot | Điểm kỹ năng hiện tại và ảnh chụp theo thời gian để vẽ tiến bộ | REST API = 78.5 (tháng 3) |
// Quan hệ cốt lõi — phía cấu hình (giảng viên tạo) Skill ◄──ItemSkill (ratio)──► Item ∈ Section ∈ Assessment Item ──► Rubric ──► Criterion[] (weight, source) Item ──► TestCase[] | Question[] // Quan hệ cốt lõi — phía thực thi (sinh viên tạo, hệ thống chấm) Attempt ──► Submission ──► Evaluation[] ──► ItemResult ──► AssessmentResult └──ItemSkill──► SkillScore ──► SkillSnapshot
ItemSkill là bảng biến “điểm bài thi” thành “điểm kỹ năng”. Thiếu nó, skill profile chỉ là con số trang trí. Ngoài ra, assessment đã publish và đã có bài nộp thì bất biến: muốn sửa test case hay trọng số phải tạo phiên bản mới, để hai sinh viên cùng làm một bài luôn được chấm cùng một bộ tiêu chí.
§5Các loại bài đánh giá
Năm loại item dùng chung một pipeline chấm; khác nhau ở bộ chấm nào tham gia và tỉ trọng của chúng.
| Loại | Sinh viên nộp | Cách chấm | Nguồn điểm | Tự động | Phạm vi |
|---|---|---|---|---|---|
| Trắc nghiệm | Đáp án đã chọn | So đáp án; câu nhiều đáp án chấm từng phần, chọn sai bị trừ | Rule | 100% | MVP |
| Lập trình | Mã nguồn một file | Compile → chạy từng test qua stdin → so stdout → điểm theo trọng số test; AI nhận xét chất lượng mã | Test AI ≤ 20% | ~90% | MVP |
| REST API | Dự án có Dockerfile (zip hoặc repo Git) | Build image → chạy container → health check → chạy bộ test HTTP → kiểm luật thiết kế → AI review | Test Rule AI ≤ 20% | ~85% | MVP |
| SQL | Câu truy vấn | Chạy trên DB sandbox dựng từ script seed, so tập kết quả với lời giải mẫu | Test | 100% | Mở rộng |
| Phỏng vấn kỹ thuật | Câu trả lời văn bản qua nhiều lượt | AI hỏi từ ngân hàng câu hỏi, hỏi nối tiếp tối đa 2 lần, chấm theo rubric; GV duyệt trước khi chốt | AI Human | ~60% | MVP |
Hợp đồng nộp bài REST API
Đây là điểm nhấn của đề tài và cũng dễ hỏng nhất nếu không chuẩn hoá đầu vào. Sinh viên bắt đầu từ starter template do giảng viên cung cấp và phải tuân thủ:
Dockerfileở thư mục gốc; ứng dụng lắng nghe cổng8080.GET /healthtrả200trong vòng 60 giây sau khi container khởi động.- Dữ liệu nằm trong container (SQLite hoặc in-memory); không gọi dịch vụ bên ngoài.
- File nộp tối đa 20 MB. Trước khi nộp chính thức, sinh viên bấm Kiểm tra build để chạy thử build + health check, không tính điểm.
Giảng viên khai báo test case dạng YAML. Test chạy tuần tự, giá trị capture ở test trước được dùng lại ở test sau, nhờ đó kiểm tra được cả luồng CRUD và luồng đăng nhập lấy token:
id: TC03 name: Tạo sản phẩm hợp lệ weight: 2 criterion: functional request: method: POST path: /api/products headers: { Authorization: "Bearer {{token}}" } # token capture từ TC01 /api/auth/login body: { name: "Keyboard", price: 25.5 } expect: status: 201 headers: Location: { exists: true } json: - { path: "$.id", op: exists } - { path: "$.name", op: equals, value: "Keyboard" } - { path: "$.price", op: gt, value: 0 } capture: productId: "$.id" # TC04: GET /api/products/{{productId}} → 200
Toán tử kiểm tra cần hỗ trợ: equals, notEquals, exists, gt/lt, contains, notContains, matches (regex), count (độ dài mảng), schema (JSON Schema cho toàn bộ response).
Hợp đồng bài lập trình
| Thuộc tính | Giá trị mặc định | Ghi chú |
|---|---|---|
| Ngôn ngữ | Python 3.12, C# (.NET 8) | Mỗi ngôn ngữ một runner image dựng sẵn |
| Thời gian mỗi test | 2 s | Vượt → TimeLimitExceeded, test đó 0 điểm |
| Bộ nhớ | 256 MB | Vượt → MemoryLimitExceeded |
| So khớp output | Bỏ khoảng trắng cuối dòng | Cấu hình được: khớp tuyệt đối, hoặc số thực sai số 1e-6 |
| Test công khai / ẩn | 2 công khai, còn lại ẩn | Sinh viên chỉ thấy chi tiết test công khai; test ẩn chỉ báo đạt/không |
§6Luồng chấm bất đồng bộ
API không bao giờ chấm bài trong một request. Nộp bài chỉ lưu và xếp hàng, trả 202 Accepted ngay; việc build, chạy và gọi AI diễn ra trên worker, có thể mất từ vài giây tới vài phút.
- Web app gửi
POST /api/submissions. - API trả
202 Accepted+Locationngay lập tức. - Bài nộp được lưu với trạng thái
Queued. - Job chỉ chứa
submissionId, không chứa mã nguồn. - Worker nhận job, giữ lease 5 phút có heartbeat.
- Build và chạy trong container giới hạn tài nguyên (§8).
- Test runner, rule engine, AI evaluator chấm độc lập; chỉ tiến trình AI evaluator được gọi ra ngoài tới AI provider.
- Score engine tổng hợp theo rubric rồi ghi kết quả.
- Giao diện nhận trạng thái qua SignalR, dự phòng bằng polling.
Vòng đời một bài nộp
| Nhánh | Khi nào | Xử lý |
|---|---|---|
| Running→Failed | Lỗi hệ thống: worker chết, Docker lỗi, AI provider timeout | Retry tối đa 3 lần, giãn cách 30 s / 2 phút / 10 phút; hết lượt → DeadLetter, báo Admin |
| Evaluated→NeedsReview | AI chấm không ổn định, bài phỏng vấn, hoặc sinh viên gửi phúc khảo | Giảng viên duyệt hoặc ghi đè, rồi chuyển Finalized |
Lỗi của sinh viên — không compile được, timeout, sai output, container không qua health check — không phải Failed. Đó là kết quả chấm hợp lệ: bài vẫn đi tới Evaluated với điểm tương ứng. Failed chỉ dành cho lỗi của hệ thống, và chỉ lỗi hệ thống mới được retry. Worker kiểm tra trạng thái trước khi chạy để một job bị giao hai lần không chấm hai lần.
§7Mô hình tính điểm
Câu hội đồng chắc chắn hỏi: “Backend = 83% được tính thế nào?”. Câu trả lời là bốn tầng công thức, mỗi tầng do giảng viên cấu hình, không tầng nào để AI tự quyết.
Tầng 1 · Tiêu chí c ∈ [0, 100] Test : c = 100 × Σ w(test đạt) / Σ w(mọi test của tiêu chí) Rule : c = 100 × Σ w(luật đạt) / Σ w(mọi luật) AI : c = 10 × điểm AI (thang 0–10, bước 0.5, đã qua kiểm soát) Human: c = điểm giảng viên nhập, thay thế nguồn gốc, bắt buộc có lý do Tầng 2 · Item I = Σ c_k × w_k (Σ w_k = 1, theo rubric của item) Tầng 3 · Assessment A = Σ I_j × W_j / Σ W_j (W_j: trọng số item trong bài) Tầng 4 · Skill s S = Σ I_j × m_js / Σ m_js (m_js: tỉ lệ ItemSkill; mỗi item lấy lượt gần nhất)
Phiếu tính điểm mẫu — kỹ năng REST API của một sinh viên
Ba item gắn với skill REST API, tỉ lệ đóng góp 20% / 50% / 30% như đề xuất ban đầu.
| Thành phần | Nguồn | Kết quả thô | Điểm | Trọng số | Đóng góp |
|---|---|---|---|---|---|
| A · Quiz REST & HTTP | Rule | 8/10 câu đúng | 80.00 | 80.00 | |
| B · REST API Practice — CRUD Products | |||||
| Functional | Test | 17/20 trọng số test đạt | 85.00 | 60% | 51.00 |
| API design | Rule | 7/10 luật (tên số nhiều, mã 201 + Location…) | 70.00 | 15% | 10.50 |
| Error handling | Test | 4/5 test lỗi (404, 400…) | 80.00 | 10% | 8.00 |
| Security | Rule | 3/5 kiểm tra (401 khi thiếu token…) | 60.00 | 10% | 6.00 |
| Code quality | AI | AI 7.5/10, có trích dẫn | 75.00 | 5% | 3.75 |
| Điểm item B | 100% | 79.25 | |||
| C · Phỏng vấn kỹ thuật REST | |||||
| Technical accuracy | AI | 8.0/10 · GV đã duyệt | 80.00 | 30% | 24.00 |
| Completeness | AI | 7.0/10 | 70.00 | 25% | 17.50 |
| Reasoning | AI | 7.5/10 | 75.00 | 25% | 18.75 |
| Clarity | AI | 8.0/10 | 80.00 | 20% | 16.00 |
| Điểm item C | 100% | 76.25 | |||
| Tầng 4 · Skill REST API = Σ I × m | |||||
| A · Quiz | 80.00 | 20% | 16.000 | ||
| B · Practice | 79.25 | 50% | 39.625 | ||
| C · Interview | 76.25 | 30% | 22.875 | ||
| REST API | 100% | 78.50 | |||
Trong item B, phần do AI chấm chỉ chiếm 5%, dưới trần 20%: dù AI chấm lệch hẳn 2 điểm thì điểm item chỉ đổi 1 điểm trên thang 100. Skill profile chỉ hiển thị một kỹ năng khi có ít nhất 2 item làm bằng chứng; ít hơn thì ghi “chưa đủ dữ liệu” thay vì một con số ảo. Mỗi lần có kết quả mới, hệ thống lưu một SkillSnapshot để vẽ biểu đồ tiến bộ.
Tám cơ chế kiểm soát điểm AI
| Cơ chế | Cách làm cụ thể |
|---|---|
| Structured output | Bắt buộc JSON theo schema {criterionId, score, evidence[], feedback}; sai schema → gọi lại 1 lần → vẫn sai → NeedsReview |
| Mô tả mức điểm | Rubric có mô tả cho các mức 0 / 2.5 / 5 / 7.5 / 10, đưa nguyên văn vào prompt; điểm làm tròn bước 0.5 |
| Chấm hai lần | Temperature 0, chấm 2 lần độc lập; lệch > 1.5 điểm ở bất kỳ tiêu chí → NeedsReview |
| Bằng chứng | Mỗi điểm phải trích dẫn đoạn bài làm làm căn cứ; không có trích dẫn → không nhận điểm |
| Trần tỉ trọng | AI ≤ 20% điểm item với Coding/REST; bài Interview do AI chấm phải qua giảng viên duyệt trước khi chốt |
| Lưu vết | Lưu model, phiên bản prompt, số token, response gốc — tái lập được mọi lần chấm |
| Human override | Giảng viên sửa điểm tiêu chí, bắt buộc ghi lý do; nguồn đổi thành Human, ghi audit log |
| Chống prompt injection | Bài làm đặt trong delimiter, system prompt yêu cầu bỏ qua mọi chỉ dẫn nằm trong bài làm; chuỗi đáng ngờ → gắn cờ |
Giảng viên chấm tay một gold set 30–50 câu trả lời phỏng vấn và bài code. Mỗi khi đổi prompt hoặc model, chạy lại AI trên gold set và đo sai số tuyệt đối trung bình (MAE) so với giảng viên. Mục tiêu: MAE ≤ 1.0 trên thang 10. Con số này biến “AI chấm khá ổn” thành một kết quả đo được để đưa vào báo cáo.
§8Sandbox & bảo mật thực thi
Chạy mã tuỳ ý là rủi ro bảo mật thật, không phải lý thuyết. Mỗi mối đe doạ dưới đây phải có một cấu hình cụ thể và một bài nộp độc hại tương ứng trong bộ kiểm thử.
| Mối đe doạ | Ví dụ bài nộp | Biện pháp |
|---|---|---|
| Chạy vô hạn | while(true){} | Worker kill container sau 2 s/test (coding), 60 s khởi động + 120 s chạy test (REST) |
| Ngốn CPU / RAM | Cấp phát mảng 10 GB | --cpus 1 · --memory 256m --memory-swap 256m (REST: 512m) |
| Fork bomb | os.fork() trong vòng lặp | --pids-limit 128 |
| Phá hệ thống file | Ghi đè /etc, đọc bài người khác | --read-only, --tmpfs /tmp:size=64m, mount bài làm chỉ đọc, mỗi job một thư mục |
| Truy cập mạng | Gửi dữ liệu ra ngoài, tấn công máy khác | --network none cho coding; REST dùng mạng nội bộ riêng mỗi job, chỉ test runner vào được |
| Leo thang đặc quyền | Khai thác quyền root trong container | --user 10001, --cap-drop ALL, --security-opt no-new-privileges, seccomp mặc định |
| Output khổng lồ | In 1 GB ra stdout | Chỉ đọc 64 KB đầu, phần còn lại bỏ |
| Chiếm máy chủ | Thoát khỏi container | Worker chạy trên VM riêng; API server không có quyền Docker; container --rm, dọn image sau job |
# Lệnh worker dùng cho một test case bài Python (qua Docker.DotNet SDK)
docker run --rm -i --network none --cpus 1 --memory 256m --memory-swap 256m \
--pids-limit 128 --read-only --tmpfs /tmp:size=64m \
--user 10001:10001 --cap-drop ALL --security-opt no-new-privileges \
-v /jobs/8f2c/src:/app:ro asap-runner-python:3.12 python /app/main.py
Dự án REST API phải tải thư viện (NuGet, npm) khi build. Tách thành hai bước: build chỉ được ra mạng tới registry qua proxy danh sách trắng, giới hạn 180 s; run hoàn toàn không có Internet. Starter template nên dùng base image có sẵn thư viện phổ biến để build nhanh và ổn định.
Mỗi worker chạy tối đa 4 job song song (semaphore); cần nhiều hơn thì thêm worker thứ hai trỏ vào cùng hàng đợi — không phải sửa code.
§9Kiến trúc & công nghệ
Modular monolith: một API ASP.NET Core chia module theo nghiệp vụ, mỗi module một project, giao tiếp qua interface; cộng một tiến trình Worker riêng. Với 5 sinh viên và 1 VPS, cách này cho ranh giới module rõ ràng mà không phải trả chi phí vận hành microservices.
| Tầng | Lựa chọn | Lý do |
|---|---|---|
| Frontend | React 18 + TypeScript + Vite, Ant Design, Monaco Editor, Recharts | Monaco là editor của VS Code; Recharts đủ cho radar và line chart |
| Backend | ASP.NET Core 8 Web API, EF Core, FluentValidation | Theo đề xuất; hệ sinh thái test tốt |
| Database | SQL Server 2022 | Chạy bằng container ở môi trường dev và CI |
| Hàng đợi | Hangfire (storage Redis) — hoặc Redis Streams nếu muốn tự làm | Hangfire có sẵn retry, dashboard, nhiều worker; giảm rủi ro cho SV3 |
| Thời gian thực | SignalR | Đẩy trạng thái chấm, dự phòng polling |
| Sandbox | Docker Engine + Docker.DotNet, runner image cho từng ngôn ngữ | Điều khiển container từ C# |
| HTTP test runner | Tự viết: HttpClient + JsonPath.Net + JsonSchema.Net | Phần “tự xây” thể hiện năng lực của nhóm |
| AI | Interface IAiProvider, cài đặt OpenAI và Gemini, thêm bản mock cho test | Đổi provider không sửa nghiệp vụ |
| Xác thực | JWT access 15 phút + refresh token, băm mật khẩu PBKDF2 | RBAC dạng policy |
| Kiểm thử | xUnit, Testcontainers, Playwright, k6 | Xem §13 |
| CI/CD | GitHub Actions → GHCR → Docker Compose trên VPS | Mỗi PR phải xanh mới được merge |
| Giám sát | Serilog + Seq, health checks | Truy lỗi chấm bài theo submissionId |
# Cấu trúc solution đề xuất (Asap = tên tạm) src/ Asap.Api/ # host, controller, xác thực, SignalR hub Asap.Modules.Identity/ # SV1 Asap.Modules.Assessment/ # SV2 · Skill, Assessment, Item, Rubric, TestCase Asap.Modules.Submission/ # SV3 · Attempt, Submission, trạng thái, enqueue Asap.Modules.AI/ # SV4 · IAiProvider, prompt, interview Asap.Modules.Scoring/ # SV5 · Score Engine, SkillScore, gợi ý Asap.Worker/ # SV3 · nhận job, Docker, HTTP test runner Asap.Shared/ # DTO, enum trạng thái, hợp đồng giữa module web/ # React + TS runners/ # Dockerfile runner: python, dotnet tests/ deploy/docker-compose.yml .github/workflows/
§10Dữ liệu & API
Thực thể theo module
| Module | Thực thể | Trường và ràng buộc đáng chú ý |
|---|---|---|
| Identity | User, Role, Permission, UserRole, RefreshToken, AuditLog | AuditLog(actorId, action, entity, entityId, before/after JSON, ip, at) |
| Assessment | SkillCategory, Skill, Assessment, AssessmentSection, AssessmentItem, ItemSkill, Question, QuestionOption, TestCase, Rubric, RubricCriterion | Item(type, weight, config JSON) · ItemSkill(itemId, skillId, ratio) · Criterion(weight, source, levelDescriptors) · Assessment(version, status: Draft/Published/Archived) |
| Submission | Assignment, Attempt, Submission, ExecutionJob, TestResult | Submission(status, artifactPath, sha256) · ExecutionJob(tryCount, workerId, leaseUntil, lastError) · TestResult(testCaseId, passed, actual, durationMs) |
| AI | AiProviderConfig, PromptTemplate, InterviewSession, InterviewTurn, AiEvaluation | PromptTemplate(key, version) · AiEvaluation(model, promptVersion, rawResponse, score, evidence, tokens) |
| Scoring | Evaluation, ItemResult, AssessmentResult, ScoreOverride, SkillScore, SkillSnapshot, LearningResource, Recommendation | Evaluation(criterionId, source, score, evidence) · ScoreOverride(by, reason, oldScore) · SkillSnapshot(studentId, skillId, score, evidenceCount, at) |
Các endpoint chính
| Nhóm | Endpoint | Ghi chú |
|---|---|---|
| Auth | POST /api/auth/login · /refresh · /logout | Rate limit 5 lần/phút cho login |
| Users | /api/users · /api/roles | Chỉ Admin |
| Skills | GET /api/skills?tree=true · CRUD /api/skills | Instructor |
| Assessments | CRUD /api/assessments · POST /{id}/publish · POST /{id}/assignments · POST /{id}/dry-run | Publish khoá cấu hình; dry-run chạy lời giải mẫu |
| Nội dung item | /api/items/{id}/questions · /testcases · /rubric | Test case nhận cả JSON và YAML |
| Attempts | POST /api/attempts · POST /api/attempts/{id}/finish | Kiểm tra hạn nộp và số lượt |
| Submissions | POST /api/submissions → 202 + Location · GET /api/submissions/{id} | Trả trạng thái, không bao giờ chấm đồng bộ |
| Results | GET /api/results/{id} · PUT /api/results/{id}/criteria/{cid}/override | Override bắt buộc reason |
| Interviews | POST /api/interviews · POST /api/interviews/{id}/answers | Mỗi câu trả lời nhận về câu hỏi tiếp theo |
| Skill profile | GET /api/skill-profiles/me · GET /api/skill-profiles/{studentId}/progress?from=&to= | SV chỉ xem được của mình |
| Khác | GET /api/recommendations/me · GET /api/admin/audit-logs · hub /hubs/submissions |
§11Phân công 5 thành viên
Chia theo module nghiệp vụ, không chia “người làm frontend, người làm backend”. Mỗi người sở hữu module của mình từ database tới giao diện.
- Sở hữu
- User, Role, Permission, RefreshToken, AuditLog
- Màn hình
- Đăng nhập, quản lý người dùng và vai trò, hồ sơ cá nhân, audit log
- Kỹ thuật
- JWT + refresh token, RBAC dạng policy, rate limiting, audit middleware, GitHub Actions, Docker Compose, triển khai VPS
- Sở hữu
- Skill, Assessment, Section, Item, ItemSkill, Question, TestCase, Rubric
- Màn hình
- Cây skill, trình soạn assessment, soạn test case REST (form + YAML), soạn rubric, màn làm bài quiz
- Kỹ thuật
- Phiên bản hoá và khoá khi publish, kiểm tra hợp lệ cấu hình (tổng trọng số = 100%), chấm quiz, dry-run lời giải mẫu
- Sở hữu
- Attempt, Submission, ExecutionJob, TestResult
- Màn hình
- Nộp code (Monaco), upload dự án REST, trạng thái chấm, chi tiết từng test
- Kỹ thuật
- Hàng đợi + worker, Docker sandbox, code runner, HTTP test runner (JSONPath, schema, capture), retry và lease
- Sở hữu
- AiProviderConfig, PromptTemplate, InterviewSession, InterviewTurn, AiEvaluation
- Màn hình
- Phỏng vấn dạng chat, hiển thị nhận xét AI, màn giảng viên duyệt điểm AI
- Kỹ thuật
IAiProvidervới 2 provider + mock, structured output, 8 cơ chế kiểm soát (§7), gold set đo MAE
- Sở hữu
- Evaluation, ItemResult, AssessmentResult, ScoreOverride, SkillScore, SkillSnapshot, Recommendation
- Màn hình
- Trang kết quả truy vết tới tiêu chí, skill profile (radar), tiến bộ (line), dashboard lớp cho giảng viên
- Kỹ thuật
- Score Engine 4 tầng — module có unit test dày nhất, mô hình skill, gợi ý học theo luật, báo cáo
Score Engine được chuyển từ SV3 sang SV5. SV3 đã gánh phần khó nhất (sandbox + hai loại runner); còn Score Engine là nơi đầu ra của SV2, SV3, SV4 hội tụ, nên đặt ở người làm skill profile là hợp lý nhất.
Hợp đồng tích hợp phải chốt ở tuần 2
| Hợp đồng | Bên cung cấp → bên dùng | Nội dung |
|---|---|---|
| TestCase schema | SV2 → SV3 | Định dạng YAML/JSON ở §5, danh sách toán tử, cú pháp capture |
| Rubric schema | SV2 → SV4, SV5 | Tiêu chí, trọng số, nguồn chấm, mô tả mức điểm |
| Evaluation record | SV3, SV4 → SV5 | {submissionId, criterionId, source, score, evidence} |
| Trạng thái Submission | SV3 → cả nhóm | Enum trạng thái §6 và sự kiện SignalR |
| Claims & policies | SV1 → cả nhóm | Tên role, tên policy, cách lấy userId từ token |
Quy tắc chung: mỗi PR có ít nhất 1 người review chéo; ai viết module nào thì viết unit test và integration test cho module đó.
§12Lộ trình 16 tuần
Mỗi sprint kết thúc bằng một lát cắt dọc chạy được từ giao diện tới database. Không dồn tích hợp vào cuối kỳ.
| Tuần | Sprint | Mục tiêu | Demo cuối sprint |
|---|---|---|---|
| 1–2 | S0 · Phân tích | SRS, use case, ERD, kiến trúc, 5 hợp đồng tích hợp, repo + CI khung | Duyệt đề cương với GVHD |
| 3–4 | S1 · Nền tảng | Auth/RBAC, CRUD skill và assessment, Docker Compose, worker nhận job “hello” | GV đăng nhập, tạo assessment |
| 5–6 | S2 · Quiz end-to-end | SV làm quiz → chấm → kết quả; Score Engine v1; khung IAiProvider | Lát cắt dọc đầu tiên |
| 7–8 | S3 · Lập trình | Sandbox, code runner Python + C#, TestResult, AI feedback v1 | Kiểm tra giữa kỳ |
| 9–10 | S4 · REST API | Build image, health check, HTTP test runner, capture + auth | Chấm tự động dự án CRUD Products |
| 11–12 | S5 · Phỏng vấn AI | Interview session, chấm theo rubric, màn duyệt, gold set + MAE | Phỏng vấn 5 câu có điểm từng tiêu chí |
| 13–14 | S6 · Skill profile | SkillScore, snapshot, dashboard, gợi ý theo luật, override + audit | Hồ sơ kỹ năng và biểu đồ tiến bộ |
| 15–16 | S7 · Hoàn thiện | Kiểm thử tải và bảo mật, sửa lỗi, triển khai, báo cáo, slide, kịch bản demo | Bảo vệ |
Mốc cắt phạm vi: nếu hết tuần 8 bài lập trình chưa chạy end-to-end, bỏ toàn bộ mục “Mở rộng” và dồn người hỗ trợ SV3.
§13Phi chức năng & kiểm thử
Mỗi yêu cầu phi chức năng phải có con số để kiểm chứng; nếu không đo được thì hội đồng không có lý do để tin.
| Nhóm | Yêu cầu đo được |
|---|---|
| Hiệu năng | API CRUD p95 < 300 ms với 100 người dùng đồng thời; nộp bài trả 202 trong < 500 ms |
| Thông lượng chấm | 100 bài nộp dồn cùng lúc được xếp hàng không mất bài nào; 1 worker × 4 slot chấm xong 100 bài lập trình trong ≤ 10 phút |
| Độ tin cậy | Worker chết giữa chừng → job được nhận lại khi lease 5 phút hết hạn; retry tối đa 3 lần |
| Bảo mật | Mọi endpoint yêu cầu xác thực trừ login; SV không đọc được kết quả của SV khác; mã bài làm không bao giờ chạy ngoài sandbox |
| Kiểm toán | Mọi thao tác đổi điểm, publish, phân quyền đều có bản ghi audit |
| Giải thích được | Mọi điểm hiển thị truy được tới tiêu chí, nguồn chấm và bằng chứng trong ≤ 2 lần bấm |
| Độ ổn định AI | MAE AI so với GV ≤ 1.0/10 trên gold set; hai lần chấm lệch ≤ 1.5 điểm |
“Hỗ trợ 100 submission đồng thời” trong đề xuất gốc nên hiểu là 100 bài được nhận và xếp hàng đồng thời, không phải 100 container chạy cùng lúc — một VPS không chạy nổi con số đó, và cũng không cần.
Chiến lược kiểm thử
| Mức | Đối tượng | Công cụ | Mục tiêu |
|---|---|---|---|
| Unit | ScoreEngine, RubricEngine, SkillCalculator, so khớp output, toán tử JSON | xUnit | Coverage ≥ 80% cho Scoring và runner |
| Integration | Submission → hàng đợi → worker → kết quả với container thật | Testcontainers | Luồng chính xanh trên CI |
| API | Auth, assessment, submission, result | WebApplicationFactory hoặc Newman | Mọi endpoint có test mã trạng thái |
| E2E | Đăng nhập → làm bài → nộp → xem kết quả | Playwright | 5 kịch bản chính |
| Bảo mật | Không token, JWT sai/hết hạn, SV gọi API của GV, xem kết quả người khác (IDOR), bộ bài nộp độc hại ở §8 | xUnit + bộ mẫu độc hại | Chạy trên mỗi PR |
| Tải | Nộp bài và xem kết quả | k6 | Đạt ngưỡng hiệu năng ở trên |
| Hồi quy AI | Gold set khi đổi prompt hoặc model | Script đo MAE | MAE ≤ 1.0 |
# Pipeline CI/CD
Pull request → build → unit test → integration test (Testcontainers) → test bảo mật → docker build
merge main → push image lên GHCR → SSH vào VPS → docker compose pull && docker compose up -d
§14Rủi ro
| Rủi ro | Dấu hiệu sớm | Giảm thiểu |
|---|---|---|
| Phình phạm vi | Tuần 8 chưa có bài lập trình chạy end-to-end | Khoá MVP ở tuần 2; cắt mục “Mở rộng” theo mốc ở §12 |
| AI chỉ là “gọi API” | Điểm AI đi thẳng vào kết quả | AI là một evaluator trong pipeline, có trần tỉ trọng, 8 cơ chế kiểm soát, gold set |
| Sandbox không an toàn | Bài độc hại làm treo worker | Chỉ 2 ngôn ngữ, runner image dựng sẵn, worker trên VM riêng, bộ bài độc hại trên CI |
| AI chấm không ổn định | Hai lần chấm lệch nhiều | Mô tả mức điểm, chấm hai lần, NeedsReview, human override |
| Điểm skill “ảo” | Không giải thích được một con số | Công thức 4 tầng công khai, phiếu truy vết, cần ≥ 2 item mới hiển thị |
| Chi phí và hạn mức AI | Hết quota trước buổi demo | Giới hạn token mỗi bài, provider dự phòng, mock provider cho dev và CI |
| Dự án REST của SV không build | Tỉ lệ lỗi build cao ở lần nộp đầu | Starter template, nút “Kiểm tra build” trước khi nộp |
| Tích hợp muộn | Module chạy riêng được nhưng ghép lại hỏng | 5 hợp đồng ở tuần 2, lát cắt dọc mỗi sprint |
| Gian lận | Nhiều bài nộp giống hệt nhau | Lưu hash bài nộp; phỏng vấn hỏi nối tiếp về chính bài làm; so trùng mã nằm ở “Mở rộng” |
§15Cần chốt & chuẩn bị bảo vệ
Sáu quyết định cần chốt với giảng viên hướng dẫn
- Ngôn ngữ cho bài lập trình — đề xuất Python + C#.
- Stack cho bài REST API — đề xuất ASP.NET Core và Node/Express, mỗi stack một starter template.
- AI provider và ngân sách — OpenAI hay Gemini, hạn mức token mỗi tháng.
- Chính sách điểm AI — trần 20% cho Coding/REST có được chấp nhận không; bài phỏng vấn duyệt 100% hay duyệt mẫu.
- Hạ tầng demo — VPS của trường hay cloud; có được một máy riêng cho worker không.
- Thời lượng thực tế — 16 hay 20 tuần; quyết định có làm mục “Mở rộng” hay không.
Câu hỏi hội đồng dễ hỏi
- “Điểm Backend 83% được tính thế nào?”
- Mở phiếu tính điểm (§7): công thức 4 tầng, tỉ lệ ItemSkill do giảng viên cấu hình, bấm tới từng tiêu chí và bằng chứng.
- “Nếu AI chấm sai thì sao?”
- AI tối đa 20% điểm bài có test; lệch giữa hai lần chấm → giảng viên duyệt; mọi điểm có thể ghi đè kèm lý do; độ chính xác đã đo bằng MAE trên gold set.
- “Sinh viên nộp mã độc thì sao?”
- Trình diễn bộ bài độc hại ở §8 chạy trên CI: fork bomb, vòng lặp vô hạn, đọc file hệ thống, gọi mạng — tất cả bị chặn và vẫn nhận điểm 0 hợp lệ.
- “Worker chết giữa chừng thì sao?”
- Lease hết hạn, job được worker khác nhận lại, retry tối đa 3 lần, rồi vào DeadLetter; không bài nộp nào mất.
- “Khác gì LeetCode hay HackerRank?”
- Chấm cả dự án REST API thật và kỹ năng trình bày qua phỏng vấn, theo rubric giảng viên tự cấu hình, và quy tất cả về một hồ sơ kỹ năng theo thời gian.