---
url: https://docs.nemo12.com/reference/recommendation-engine.md
description: >-
  Recommendation Engine: biến kho bài học thành một việc nên làm hôm nay kèm lý
  do (buildCockpit, SDD-002 §10).
---

# 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](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §10 · Code: `modules/knowledge/routes.ts` → `buildCockpit()` · Workflow: [WF-05](../workflows/recommendation-cockpit.md)

***

## 1. Chưa phải một module riêng

::: warning Đọ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](readiness-model.md#71-hai-đường-tính-readiness--cố-ý-khác-nhau), 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](#7-khoảng-trống-đã-biết).

***

## 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:**

```ts
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ó recommendation
```

Fallback 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](readiness-model.md#71-hai-đường-tính-readiness--cố-ý-khác-nhau) (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

```ts
const reviews = await topReviewCandidates(env, learnerId, subjectId, 2);
priority = r.urgency === "CRITICAL" ? Math.max(r.priority, 4) : r.priority
```

Hai 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.
* `CRITICAL` có **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

```ts
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](readiness-model.md#73-study-mode--cùng-dữ-liệu-đổi-cách-dùng) để 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](engines.md#7-need-signal-engine-chọn-bài-trong-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](learning-plan-model.md#bước-2--ốmstress-cắt-đôi-thời-gian)). 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](parent-model.md#5-cách-dùng-2-parent-recommendation--wf-12) (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](engines.md#7-need-signal-engine-chọn-bài-trong-phòng-lab) | "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 → summer
```

Khô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](parent-model.md) 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](learning-plan-model.md) để 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ử](workflows.md#4-xem-lại-một-run), 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

1. Mọi action **phải có `reason`** viết bằng tên bài, không bằng `node_id` hay số mastery thô.
2. Prereq chưa vững → đề xuất **prereq**, không đề xuất node gap.
3. Review dùng **cùng thang điểm** với gap action — không được đè bẹp Learn/Repair.
4. Retention lỗi **không** được chặn cockpit.
5. `exam_prep` → luyện đề đứng đầu, bất kể gap map.
6. Dedupe theo `node_id` — không đề xuất cùng một bài hai lần.
7. 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](../workflows/recommendation-cockpit.md).
* Nguồn: SRC-003 §10, SRC-105 · SRC-189 (`item_count`).
* Thiết kế: [SDD-002](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §10/§15, [SDD-017](../architecture/sdd-017-retention.md) §8, [SDD-016](../architecture/sdd-016-orca-competitions.md) §3, [SDD-007](../architecture/sdd-007-learner-orchestration.md) §9.
* Kiểm chứng: QG-005.
* Liên quan: [Readiness](readiness-model.md) · [Retention](retention-model.md) · [Learning Plan](learning-plan-model.md) · [Parent Model](parent-model.md) · [Engines](engines.md).
