Recommendation Engine
"Mở app ra thì hôm nay làm gì?" — biến 154 bài học thành một việc nên làm ngay, kèm lý do.
Thiết kế: SDD-002 §10 · Code: modules/knowledge/routes.ts → buildCockpit() · Workflow: WF-05
1. Chưa phải một module riêng
Đọc trước khi đi tìm file
Không có modules/recommendation/. Recommendation Engine hôm nay chạy inline trong buildCockpit() — cùng hàm với Readiness, trong modules/knowledge/routes.ts.
Nó cũng không sinh model: không có recommendation trong MODEL_KINDS, không có version, không có snapshot. Mỗi lần learner mở màn hình là tính lại từ đầu.
Vì sao hiện tại như vậy: recommendation là hàm thuần của trạng thái. Cùng mastery + cùng goal + cùng ngày → cùng kết quả. Không có gì để lưu mà đọc lại không suy ra được.
Cái mất đi vì không lưu: không trả lời được "hôm thứ Ba hệ thống đã khuyên con làm gì, và vì sao?". SDD-002 §10 yêu cầu lưu reason + input_model_version + algorithm_version — xem §7 khoảng trống.
2. Công thức: SDD nói một đằng, code làm một nẻo
Đây là khác biệt lớn nhất giữa thiết kế và hiện trạng, nên nói thẳng.
SDD-002 §10 thiết kế:
Priority = ExpectedImpact × GoalRelevance × Urgency × PrerequisiteImportance
× ProbabilityOfSuccess ÷ Cost (+ ConfidenceNeed khi thiếu data)Code đang chạy:
priority = round2(gap.weight × gap.deficit)| Thừa số thiết kế | Hiện trạng |
|---|---|
| ExpectedImpact | ✅ chính là weight × deficit |
| GoalRelevance | ✅ ngầm — gap chỉ lấy từ blueprint mục tiêu |
| PrerequisiteImportance | ⚠️ có, nhưng dưới dạng đổi loại hành động (§3) chứ không nhân vào điểm |
| Urgency | ⚠️ chỉ với review (từ Retention) và exam_practice |
| ProbabilityOfSuccess | ❌ chưa có |
| Cost | ❌ chưa có (estimated_minutes chỉ có ở review) |
| ConfidenceNeed | ⚠️ có, dưới dạng loại assess |
Nói cách khác: v1 giữ đúng phần cốt lõi (tác động × liên quan mục tiêu) và bỏ phần chưa đo được. ProbabilityOfSuccess cần mô hình dự đoán chưa có; Cost cần ước lượng thời gian mỗi node mà curriculum chưa gắn đủ.
Ghi ra đây vì đọc SDD rồi đi tìm công thức 6 thừa số trong code sẽ không thấy — và tưởng là bug.
3. Thuật toán thật — 5 bước
Bước 1 — Chọn blueprint mục tiêu (có fallback)
learner_goals active của môn → dùng
không có → blueprint kind='conditional' mặc định của môn
không có nữa → KHÔNG có recommendationFallback là lý do learner chưa khai mục tiêu vẫn thấy việc để làm. Khác với Readiness Model (không fallback, chỉ ghi sự thật).
Bước 2 — Từ top 6 gap, chọn loại hành động
Đây là chỗ PrerequisiteImportance sống — không phải bằng cách nhân điểm mà bằng cách đổi việc:
gap.node có prereq mastery < 0.5 → fix_prerequisite (đề xuất HỌC PREREQ, không phải node gap)
gap.node chưa từng đo (!mastery.has) → assess (làm bài chẩn đoán ngắn)
gap.severity === "critical" → learn
còn lại → practiceĐề xuất prerequisite thay vì node gap là quyết định sư phạm quan trọng nhất của engine. Nếu học sinh yếu "Hàm số" vì chưa vững "Hệ số góc", luyện thêm bài Hàm số là lãng phí thời gian và làm em ấy tin rằng mình dốt. Engine trả về node prereq với lý do nói rõ nó đang chặn cái gì.
Mỗi hành động mang reason viết bằng tiếng người, nói bằng tên bài:
"Cần vững "Hệ số góc" trước — đang chặn "Hàm số & đồ thị""
"Chưa đủ bằng chứng về "Phân tích đa thức" — làm bài chẩn đoán ngắn"
"Củng cố "Hằng đẳng thức" (mastery 35%, cần 80%)"Bước 3 — Dedupe, cắt còn 4
Sắp theo priority giảm dần → giữ node_id đầu tiên gặp → lấy 4. Cùng một node xuất hiện từ nhiều gap thì chỉ vào danh sách một lần, với priority cao nhất.
Bước 4 — Chèn Review từ Retention
const reviews = await topReviewCandidates(env, learnerId, subjectId, 2);
priority = r.urgency === "CRITICAL" ? Math.max(r.priority, 4) : r.priorityHai quyết định về thang điểm chung (SDD-017 §8):
- Review dùng cùng thang với gap action (
weight × deficit, trần ~3.2) để được xếp cạnh chứ không đè bẹp Learn/Repair. Nếu review dùng thang riêng cao hơn, mọi buổi học sẽ biến thành ôn tập. CRITICALcó sàn 4 để nổi lên trên khi thật sự gấp — sắp quên một thứ đang chặn nhiều bài và sắp thi thì phải thắng.
Retention lỗi → catch + log retention_cockpit_degraded, cockpit vẫn trả về bình thường (QG-009). Mất phần ôn còn hơn mất cả màn hình học.
Sau đó sắp lại và cắt còn 5.
Bước 5 — Gần thi thì luyện đề lên đầu
if (mode.mode === "exam_prep") nextActions.unshift({ type: "exam_practice", priority: 999 });priority: 999 = luôn đứng đầu, không cạnh tranh. Đây không phải điểm số mà là một lệnh ghi đè có chủ đích: còn ≤30 ngày tới kỳ thi thì việc đúng là luyện đề, bất kể gap map nói gì.
Xem study mode để biết exam_prep được xác định thế nào.
4. Loại hành động — thật vs thiết kế
| Loại | SDD-002 §10 | Đang sinh |
|---|---|---|
fix_prerequisite | ✅ | ✅ |
assess (Diagnostic) | ✅ | ✅ |
learn | ✅ | ✅ |
practice | ✅ | ✅ |
review | ✅ | ✅ (từ Retention) |
exam_practice (Prepare Exam) | ✅ | ✅ |
| Skip (học vượt) | ✅ | ❌ chưa sinh trong cockpit — có ở Phòng Lab dưới dạng Assessment Experience |
| Ask Mentor | ✅ | ❌ |
| Rest | ✅ | ❌ |
Rest đáng chú ý nhất trong ba cái còn thiếu. Một hệ thống chỉ biết nói "học tiếp" thì không bao giờ nói được "hôm nay nghỉ đi" — trong khi Planning Engine đã biết cắt giờ khi có lời khai ốm/stress (learning-plan §4). Hai bên chưa nối.
5. Bốn engine đề xuất khác trong hệ thống
"Recommendation" xuất hiện ở nhiều nơi với nghĩa khác nhau. Bảng này để không ai đi tìm nhầm chỗ:
| Engine | Trả lời | Cho ai | Ở đâu |
|---|---|---|---|
| Cockpit next actions (trang này) | "Hôm nay học bài nào?" | Learner | knowledge/routes.ts |
| Parent Recommendation (WF-12) | "Tuần này bố mẹ nên làm gì — và không nên làm gì?" | Phụ huynh | parent/routes.ts |
| Need Signal | "Bài này có cần học không?" | Learner | learning/routes.ts |
| Whale recommendations | "Học bổng/câu chuyện nào đáng xem?" | Learner | whale/routes.ts |
| Orca fit | "Con có nên thi cuộc này không?" | Learner + phụ huynh | orca/routes.ts |
Whale — gợi ý tò mò, rule-based
3–5 gợi ý, mỗi cái bắt buộc có reason:
2 học bổng nổi bật → "Học bổng đáng biết sớm — em đang lớp 8, còn thời gian chuẩn bị hồ sơ"
2 câu chuyện khớp school → "Hành trình có thi chuyên giống con đường em đang đi"
1 cơ hội theo school → shark → competition · squid → program · còn lại → summerKhông có model, không có điểm số — chủ đích là khơi tò mò, không phải chấm điểm sự phù hợp.
Orca — fit score có verdict 4 mức
score = eligible×0.25 + skill×0.30 + schedule×0.25 + intensity×0.20
(eligible = 0 hoặc quá hạn đăng ký → score bị ép ≤ 25)
≥80 Strongly Recommended · ≥65 Recommended · ≥45 Optional · <45 Not Recommended Now"Not Recommended Now" là output hợp lệ và hữu ích — cùng tinh thần với dont_do của Parent Recommendation. Mỗi verdict kèm reasons[] nói rõ vì sao: ngoài độ tuổi, hết hạn đăng ký, mức cạnh tranh cao.
Đây vẫn là fit v1 tất định (SDD-016 §3); bản đầy đủ sẽ nối vào Recommendation Engine chính.
6. Cách DÙNG
| Nơi | Endpoint |
|---|---|
learn tab "Hôm nay" | GET /v1/learners/{id}/cockpit?subject_id= |
| Sau khi nộp bài chẩn đoán | POST /v1/diagnostic/{sessionId}/submit trả luôn cockpit |
| Parent (WF-12) | buildCockpit() được Parent Recommendation gọi lại để lấy gap + mode |
Cockpit trả kèm hai tín hiệu quan trọng ngoài next_actions:
| Trường | Ý nghĩa |
|---|---|
evidence_reliability: "insufficient" | Bằng chứng quá mỏng — UI phải nói rõ đây là phỏng đoán |
item_count | Môn có bao nhiêu câu hỏi thật. 0 câu → bài chẩn đoán là cánh cửa khoá chặt: bấm vào không có gì để làm, mà không bấm thì không vào được môn (SRC-189) |
US-03 đặt ràng buộc UI: tab "Hôm nay" hiện đúng một nút hành động chính — 5 việc trong next_actions là để hệ thống chọn, không phải để bày hết ra cho learner tự chọn lại.
7. Khoảng trống đã biết
| Việc | Trạng thái |
|---|---|
Lưu reason + input_model_version + algorithm_version mỗi recommendation (SDD-002 §15) | ❌ chưa — không truy được "hôm đó hệ thống khuyên gì" |
ProbabilityOfSuccess, Cost trong công thức priority | ❌ chưa đo được |
Loại Rest, Ask Mentor, Skip trong cockpit | ❌ chưa sinh |
| Dùng Learning Plan để chọn action | ⏳ hai đường còn độc lập — plan chia phút, cockpit tự chọn bài |
Tách thành modules/recommendation/ với engine thuần + test riêng | ⏳ hiện inline, chưa có file test riêng |
| Global Orchestrator quyết định cuối giữa các school engine (SDD-007 §9) | 🕓 hiện chỉ một môn một lần |
Khoảng trống đầu bảng là nghiêm trọng nhất: mọi model khác đều xem lại được lịch sử, riêng recommendation thì không. Khi phụ huynh hỏi "tuần trước hệ thống bảo con học gì?", hôm nay không có câu trả lời.
8. Bất biến — vi phạm là bug
- Mọi action phải có
reasonviết bằng tên bài, không bằngnode_idhay số mastery thô. - Prereq chưa vững → đề xuất prereq, không đề xuất node gap.
- Review dùng cùng thang điểm với gap action — không được đè bẹp Learn/Repair.
- Retention lỗi không được chặn cockpit.
exam_prep→ luyện đề đứng đầu, bất kể gap map.- Dedupe theo
node_id— không đề xuất cùng một bài hai lần. - Tối đa 5 action.
Trace
- REQ-INT-05 (priority score + explainability), REQ-LRN-02 (next best action kèm lý do) · US-03 · WF-05.
- Nguồn: SRC-003 §10, SRC-105 · SRC-189 (
item_count). - Thiết kế: SDD-002 §10/§15, SDD-017 §8, SDD-016 §3, SDD-007 §9.
- Kiểm chứng: QG-005.
- Liên quan: Readiness · Retention · Learning Plan · Parent Model · Engines.