---
url: https://docs.nemo12.com/reference/learning-engine.md
description: >-
  Learning Engine: quyết định học thế nào sau khi Recommendation Engine chọn bài
  nào, thuật toán và cách dùng (SDD-002 §11).
---

# Learning Engine

> [Recommendation Engine](recommendation-engine.md) chọn **học bài nào**. Learning Engine quyết định **học thế nào**.

Thiết kế: [SDD-002](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §11 · Code: `modules/learning/routes.ts` + `modules/learning/selection.ts` · Test: `selection.test.ts` (20 ca)

***

## 1. Đơn vị học là **Experience**, không phải "bài"

Ba loại Experience, và **chúng khác nhau ở cách chọn câu, không chỉ ở nhãn** (SRC-077):

| Kind | Việc | Số câu | Nguồn câu |
| --- | --- | --- | --- |
| `practice` | Luyện tập — **học** | **10** (`PRACTICE_TARGET`) | Kho của một node, **trừ** phần để dành đo |
| `check` | Đo mức vững — **đo** | 6 (`CHECK_SIZE`) | Ưu tiên phần để dành (câu phân loại) |
| `skip` | Học vượt — **đo cả Unit** | 10 (`SKIP_SIZE`) | Câu phân loại trải trên mọi node của unit |

`check` và `skip` là [Assessment Experience](assessment-engine.md) — trang này lo phần `practice`, phần thật sự **dạy**.

**10 câu là nhịp cố định** (SRC-153): trước đây số câu nhảy 4/6/8 tuỳ kho mà learner không thấy lý do nào. Biết trước *"10 câu là xong"* thì đứa trẻ mới quyết định bắt đầu. Nhịp ổn định đáng giá với trẻ hơn là tối ưu từng bài.

Kho chưa đủ 10 thì phiên **ngắn hơn** và nói thật con số — **không độn câu trùng cho đủ**. Lặp câu là dạy đáp án, không dạy khái niệm.

***

## 2. Chọn câu — bài toán khó nhất của engine này

### Vấn đề đã xảy ra thật (SRC-141)

Đo ngày 2026-08-15: **251/252 bài có dưới 12 câu**, trung bình **8,5 câu/bài**.

Luật cũ: Luyện tập lấy `LIMIT 12` → learner thấy gần như **toàn bộ kho**. Rồi "Đo cuối" lấy 6 câu từ đúng kho đó → bài đo chỉ đo xem em ấy **có nhớ mấy câu vừa làm cách đó vài phút không**.

Mastery, readiness và Learning Plan đều ăn con số đó. Đây là lỗi làm hỏng **giá trị đo**, không phải chuyện nhỏ.

### Cách sửa: giữ riêng "câu để dành cho bài đo"

```
RESERVE_RATIO = 0.35        // 35% kho để dành cho việc ĐO
RESERVE_MIN = 2
POOL_MIN_FOR_RESERVE = 6    // kho nhỏ hơn 6 câu thì KHÔNG để dành gì cả
```

Chọn câu để dành: ưu tiên **câu phân loại** (`role = discriminator | boundary`), thiếu thì lấy câu **khó nhất** bù vào — đó là những câu nói lên nhiều nhất về trình độ thật.

**Luyện tập không bao giờ đụng vào phần để dành.** Có test khoá riêng.

Kho dưới 6 câu thì không tách — *"thà cho learner luyện đủ còn hơn cắt cả hai bên đều thiếu"*. Đổi lại, `poolWarning()` nói thẳng ra:

> *"Bài này mới có 4 câu nên chưa tách được câu luyện và câu đo — kết quả đo chỉ mang tính tham khảo."*

Câu này đi thẳng tới learner và phụ huynh, không giấu trong log.

### Thứ tự câu trong một lượt luyện

```
vòng 1        → sắp theo difficulty TĂNG DẦN (dễ → khó, học có nhịp)
vòng 2 trở đi → câu CHƯA GẶP trước, rồi mới tới câu cũ; cả hai đều xáo theo seed
```

"Đã gặp" = đã trả lời trong **30 ngày** gần đây (`assessment_responses`).

Xáo dùng **seed tất định** (id phiên), không dùng `Math.random()` — để điều tra sự cố còn lần lại được đúng đề learner đã thấy.

Chính sách chọn câu nằm ở `selection.ts` dưới dạng **hàm thuần**, không trốn trong `ORDER BY`/`LIMIT` của SQL. Route kéo cả kho về (một bài chỉ ~8,5 câu nên rẻ) rồi để hàm thuần quyết định. Đổi lại: luật test được và giải thích được.

***

## 3. Ba lớp đo mỗi câu trả lời

Hiện **tuần tự** (SRC-069): chọn/đổi đáp án thoải mái → Tiếp → khai tự tin → Tiếp → vì sao chọn.

| Lớp | Trường | Thang |
| --- | --- | --- |
| 1. Đáp án | `selected_index` | `-1` = **"Chưa biết"** — lựa chọn hợp lệ, không phải bỏ cuộc |
| 2. Tự tin | `confidence` | 1–5, khai **trước khi** xem kết quả |
| 3. Vì sao | `justification_kind` | `derived` · `recalled` · `eliminated` · `guessed` |

**`justification_kind: "guessed"` có hệ quả thật:**

```ts
retentionReliability = justification_kind === "guessed" ? min(reliability, 0.4) : reliability
```

Learner tự khai "đoán" → lần đúng này **không** được tính là retrieval độc lập, hạ xuống mức `assisted` ([Retention §3](retention-model.md)). **Mastery giữ nguyên cách tính cũ** — đoán trúng vẫn là một bằng chứng về năng lực, nhưng **không** phải bằng chứng về trí nhớ.

Đây là phần thưởng cho sự trung thực: khai thật thì hệ thống hiểu đúng hơn, và không ai bị phạt.

### Rapid-guess

Đếm chuỗi trả lời < 2500 ms liền trước **trong cùng phiên** → `attemptReliability()` ([Engines §1](engines.md#reliability-của-một-lần-trả-lời-attemptreliability)). Chuỗi ≥ 3 → reliability **0.05**, gần như không dịch chuyển model, và **không tính là "đã đo"**.

***

## 4. Sai thì làm lại tới khi đúng — nhưng chỉ lần đầu tính điểm

Luật do chủ dự án đặt (2026-08-14): sai một câu thì phải làm lại câu đó tới khi đúng, kể cả trong bài đo.

Nhưng **bằng chứng năng lực chỉ tính lần trả lời đầu tiên**. Các lần chọn lại **không gửi lên server** — client giữ feedback của lần đầu để chấm tại chỗ.

Vì sao tách bạch: làm lại là để **học**, không phải để **sửa điểm**. Nếu lần thứ ba mới đúng mà vẫn ghi là đúng, mastery sẽ nói dối về trình độ thật.

### Bong bóng — 5 mạng

`MAX_LIVES = 5` bong bóng khí (thay cho "trái tim" — đúng chất biển của Nemo12). Sai một câu mất một bong bóng; hết thì dừng bài.

> Dừng **không phải để phạt mà để đổi cách học** — làm tiếp 20 câu nữa khi đang sai liên tục thì chỉ củng cố cái sai.

Học vượt (`skip`) chỉ có **một** bong bóng: sai một câu là trượt.

***

## 5. Ghi nhận một câu trả lời

```
1. Đọc item (kèm status vòng đời)
2. correct = selected_index === correct_index && !dontKnow
3. rapid-guess streak → attemptReliability()
4. updateMastery(prev, { performance, difficulty, reliability })
5. retentionStatementForEvidence(...)   ← gộp vào cùng DB.batch
6. DB.batch:  assessment_responses + learner_evidence + learner_skill_state (+ retention)
7. publishEvent("learner.evidence.recorded")
```

Evidence reliability = **`0.8 × attemptReliability`** — trần 0.8 vì một câu luyện tập không bao giờ chắc bằng một bài đo có kiểm soát.

### Câu bị gỡ giữa phiên

Learner vừa báo sai một câu, hoặc sàng lọc máy vừa gắn cờ → item lùi khỏi `published` **trong lúc em ấy đang làm dở**.

Xử lý (AS-06.3.2, AS-10.4.5):

| | Hành vi |
| --- | --- |
| Trả 404? | ❌ **không** — đứa trẻ đang làm dở sẽ kẹt cứng giữa bài |
| Chấm và cho xem đáp án? | ✅ để phiên đi tiếp |
| Ghi `assessment_responses`? | ✅ để người soát đọc lại được "đứa trẻ đã gặp câu hỏng này và chọn gì" |
| Ghi evidence / mastery / retention? | ❌ **tuyệt đối không** |

> Một câu hỏi hỏng **không được phép kéo tụt hồ sơ năng lực của trẻ.**

Trả về mastery **đang có** chứ không phải mastery vừa tính — nếu không, client hiển thị một con số mà D1 không hề lưu.

***

## 6. Không dựng lại Learner Model sau **mỗi câu**

Đây là quyết định hiệu năng có tính toán (SRC-138):

| Việc | Chi phí |
| --- | --- |
| Ghi mastery/retention/evidence của câu vừa làm | **Cố định** — không phụ thuộc lịch sử |
| Dựng lại [Learner Model](learner-model.md) | Đọc ~(số evidence + số node) dòng |

Chạy Learner Model sau mỗi câu thì một phiên 10 câu tốn **gấp 10 lần** cho cùng một kết quả — và **càng học lâu càng đắt**: chi phí một câu tăng theo tổng số câu đã từng làm.

Ba điểm dựng lại: **kết thúc Experience** · **quay lại học** (stale check) · **job đêm**.

Learner vẫn thấy phản hồi đúng ngay sau mỗi câu vì mastery đã được ghi ở bước 6 phía trên.

***

## 7. Kết thúc một Experience

`POST /v1/learners/{id}/experience/complete` ghi vào `learner_experience_state` — Phòng Lab cần biết cái nào đã làm để tô màu đường đi của Unit; mastery theo node không nói được điều đó.

Hai luật:

```sql
WHERE learner_experience_state.status <> 'completed' OR excluded.status = 'completed'
```

**Trượt Học vượt không xoá dấu đã hoàn thành trước đó** — chỉ nâng lên, không hạ xuống.

Và **`failed` cũng kích hoạt cập nhật Learner Model**:

> *"Trượt cũng là bằng chứng: nó nói lên trình độ hiện tại đúng như làm đúng."*

***

## 8. Khoảng trống lớn nhất: chưa có Difficulty Controller

SDD-002 §11 thiết kế:

```
>90% đúng   → tăng độ khó
65–90%      → tiếp tục
40–65%      → scaffold
<40%        → prerequisite check
```

Và: *scaffold vẫn fail → gọi Assessment Engine tìm prerequisite gap; **cấm** ném thêm 20 bài cùng dạng.*

**Hiện chưa có gì trong số này.** Độ khó của một phiên được cố định lúc `start` (vòng 1 sắp dễ → khó), rồi **không điều chỉnh theo diễn biến**. Learner đúng 10/10 vẫn nhận đúng bộ câu đó; sai 8/10 cũng vậy — chỉ có bong bóng dừng bài lại.

Tám output của Learning Engine trong SDD (Explanation · Example · Scaffold · Practice · Worked Example · Hint · Challenge · Reflection) — hiện chỉ có **Practice**, cộng phần Khám phá của [Lab](../architecture/sdd-010-lab-platform/index.md).

`tutor_hint` đã có trong [Context Builder](ai-registry.md) như một scope hợp lệ, nhưng chưa có luồng nào gọi nó.

***

## 9. Bất biến — vi phạm là bug

1. Luyện tập **không bao giờ** dùng câu để dành cho bài đo (trừ khi kho < 6 câu).
2. **Không lặp câu** trong cùng một phiên để cho đủ số — thà phiên ngắn hơn.
3. Kho mỏng thì **nói ra** (`pool_warning`), không im lặng.
4. Chỉ phát câu đang `published`.
5. Câu bị gỡ giữa phiên: chấm tiếp nhưng **không ghi evidence/mastery/retention**.
6. Chỉ **lần trả lời đầu tiên** thành bằng chứng năng lực.
7. Khai "đoán" hạ retention reliability, **không** hạ mastery.
8. Không dựng lại Learner Model sau mỗi câu.
9. Xáo câu bằng **seed tất định**, không `Math.random()`.

## 10. Kiểm chứng

`selection.test.ts` — 20 ca test hành vi, gồm những ca khoá đúng các luật trên:

* kho 10 câu để dành ~35%, ưu tiên câu phân loại
* **KHÔNG bao giờ đưa câu để dành vào lượt luyện**
* kho quá mỏng thì không để dành — thà luyện đủ
* vòng 1 dễ → khó · vòng 2 ưu tiên câu chưa gặp
* **không câu nào bị lặp trong cùng một phiên**
* kho mỏng thì phiên ngắn hơn 10, **không độn câu trùng**
* cùng seed thì tái lập y hệt — điều tra sự cố lần lại được

Gate: **QG-005**.

## Trace

* REQ-INT-06 (Learning Engine), REQ-LRN-03/04, REQ-LRN-15 (khởi động), REQ-INT-21/22 (reliability), REQ-EXAM-07 (3 lớp đo).
* Nguồn: SRC-003 §11, SRC-069 (3 lớp tuần tự), SRC-070 (mã lý do chọn), SRC-077 (Experience khác nhau ở cách chọn câu), SRC-138 (chi phí dựng model), SRC-141 (tách câu luyện/câu đo), SRC-153 (10 câu), SRC-083 (bong bóng), SRC-105.
* Thiết kế: [SDD-002](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §11/§18, [SDD-010](../architecture/sdd-010-lab-platform/index.md), [SDD-017](../architecture/sdd-017-retention.md) §4.
* Liên quan: [Assessment Engine](assessment-engine.md) · [Recommendation](recommendation-engine.md) · [Engines §1](engines.md#1-mastery--confidence-engine) · [Retention](retention-model.md).
