---
url: https://docs.nemo12.com/reference/assessment-engine.md
description: >-
  Assessment Engine: thuật toán chẩn đoán và đo mastery của learner, đầu vào,
  đầu ra, cách dùng, nền cho mọi model phía sau.
---

# Assessment Engine

> Mọi con số về đứa trẻ trong hệ thống này đều bắt đầu từ đây. Đo sai thì [mọi model phía sau](models.md) đều sai theo — một cách âm thầm.

Thiết kế: [SDD-002](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §5/§18 · Code: `modules/knowledge/routes.ts` (chẩn đoán) · `modules/learning/routes.ts` (đo mức vững, học vượt) · `modules/exams/routes.ts` (đề thi)

***

## 1. Bốn loại đo

| Loại | `kind` | Số câu | Reliability | Câu hỏi trả lời |
| --- | --- | --- | --- | --- |
| **Chẩn đoán** | `diagnostic` | 15 | `0.8 × attempt` | "Em đang ở đâu trên cả môn?" |
| **Đo mức vững** | `mastery_check` | 6 | `0.8 × attempt` | "Bài này em đã vững chưa?" |
| **Học vượt** | `mastery_check` (`skip`) | 10 | `0.8 × attempt` | "Em có thể bỏ qua cả Unit này không?" |
| **Đề thi** | `exam` | cả đề | **0.85** cố định | "Nếu thi thật thì được bao nhiêu?" |

Đề thi có reliability **cao nhất và cố định**: bài thi có kiểm soát, làm liền mạch, không ngắt quãng — đáng tin hơn từng câu lẻ trong lúc học.

***

## 2. Chẩn đoán — cửa vào của mọi thứ

`POST /v1/diagnostic/start` → `POST /v1/diagnostic/{sessionId}/submit`

### Cổng chặn: phải có Learner Model trước

```ts
const model = await DB.prepare("SELECT version FROM learner_models WHERE learner_id=?1").first();
if (!model) return errorResponse(c, "CONFLICT", "Cần hoàn tất hồ sơ và khởi tạo Learner Model trước khi chẩn đoán");
```

REQ-ONB-04, WF-01 §4. Đo trước khi có hồ sơ thì không biết quy kết bằng chứng vào đâu, và không có ngữ cảnh (lớp mấy, school nào) để chọn câu cho đúng.

### Chọn câu: 15 câu `canonical`, **mỗi node một câu**

```sql
WHERE role='canonical' AND status='published'
GROUP BY node_id
ORDER BY grade, level LIMIT 15
```

`GROUP BY node_id` là điểm mấu chốt: chẩn đoán **trải rộng**, không đào sâu. 15 câu ở 15 node khác nhau cho bức tranh sơ bộ về cả môn; 15 câu ở 3 node thì chỉ biết 3 node.

Đổi lại, mỗi node chỉ có **một** bằng chứng → `confidence` sau chẩn đoán còn rất thấp. Đúng như thiết kế: chẩn đoán là **bản đồ thô**, không phải kết luận.

### `INSUFFICIENT_RELIABLE_EVIDENCE` — chống rác cả phiên

```
timed.length >= 3  AND  (số câu < 2500ms) / timed.length >= 0.5
→ insufficient = true
→ reliability của MỌI câu bị ép xuống 0.05
```

Đây là biện pháp **cấp phiên**, khác với rapid-guess cấp câu (REQ-INT-22). Một đứa trẻ bấm bừa hết 15 câu trong 40 giây sẽ **không** bị ghi nhận là "yếu toàn bộ môn" — model chỉ ghi rằng chưa đo được gì.

Cờ `evidence_reliability: "insufficient"` trả về cùng cockpit để UI nói rõ đây chưa phải kết luận.

Không có lớp bảo vệ này, hậu quả là thật: một phiên bấm bừa sẽ đẩy mastery xuống, kéo readiness xuống, và [Parent Recommendation](parent-model.md) sẽ báo với bố mẹ rằng con đang có lỗ hổng nghiêm trọng.

### Có run log riêng

Chẩn đoán là workflow duy nhất ngoài WF-04/WF-15/WF-17 có [run log](workflows.md) đầy đủ: **WF-03 Diagnosis**, với step `mastery_retention_update` ghi lại `responses / scored / nodes_updated / evidence_reliability`.

Nộp xong → chạy [WF-04](workflows.md#2-wf-04--evidence--model-update) (`trigger: AssessmentCompleted`) → trả về [cockpit](recommendation-engine.md) luôn. Learner làm xong bài chẩn đoán là **thấy ngay việc nên làm**, không phải bấm thêm.

***

## 3. Đo mức vững & Học vượt

Dùng chung `POST /v1/learners/{id}/practice/start` với [Learning Engine](learning-engine.md), khác ở `kind`.

### Lấy đúng phần kho để dành

```
ưu tiên: câu để dành (phân loại/khó nhất)
rồi:     câu chưa gặp
cuối:    câu đã gặp
```

Ngược chiều hoàn toàn với luyện tập — đó là toàn bộ ý nghĩa của [cơ chế reserve](learning-engine.md#cách-sửa-giữ-riêng-câu-để-dành-cho-bài-đo).

### Học vượt: một bong bóng, và **phải chờ 3 tiếng** nếu trượt

```ts
const SKIP_COOLDOWN_MS = 3 * 60 * 60 * 1000;
```

Lỗ hổng đã bịt (SRC-141): trước đây trượt rồi thi lại ngay, và đề ra **y hệt 10 câu cũ** — thử vài lần là thuộc đáp án rồi "vượt" cả unit mà không hề vững.

Hai lớp sửa:

1. **Đề xáo theo seed phiên** → lần thi lại không bao giờ ra đúng đề cũ (có test khoá).
2. **Chờ 3 tiếng** → lần thử sau là **trí nhớ thật**, không phải trí nhớ ngắn hạn.

Thông báo nói rõ đường đi thay thế, không để learner cụt hứng:

> *"Học vượt thử lại được sau 47 phút nữa. Trong lúc chờ, con cứ học đường thường — làm xong "Đo cuối" là mở unit sau."*

`check_available` chỉ bật khi node có **≥ 6 câu** (SRC-077): Assessment Experience chỉ mở khi đủ câu để đo cho ra kết luận.

***

## 4. Đề thi — chấm theo **node**, không theo câu

`POST /v1/exams/{examId}/start` → `POST /v1/exam-attempts/{attemptId}/submit`

Điểm khác biệt lớn nhất với ba loại trên: bằng chứng được **gộp theo node** trước khi vào model.

```
với mỗi câu:  correct → cộng points; ghi exam_attempt_answers
gộp theo node: performance   = số câu đúng / tổng câu của node đó
               difficulty    = trung bình difficulty các câu
một node → MỘT evidence với performance là PHÂN SỐ (0..1), không phải 0/1
```

Vì sao gộp: một đề hỏi 4 câu về "Hằng đẳng thức" thì đúng 3/4 là thông tin tốt hơn nhiều so với bốn bằng chứng nhị phân rời rạc. `updateMastery()` nhận `performance = 0.75` và dịch chuyển đúng mức.

**Retention** thì vẫn cần nhị phân — quy ước: node đạt **≥ 70%** trong đề coi là retrieval thành công ([SDD-017 §4](retention-model.md)).

### Đề miễn nhiễm với việc item gốc bị gỡ

Nội dung câu hỏi đã được **sao vào `exam_questions`** lúc dựng đề, nên item gốc bị gỡ **không làm đề sai**. Chỉ có `difficulty` là còn đọc từ `items`:

```
item_ref trỏ tới item đã lùi khỏi 'published'
  → difficulty về mức trung tính 0.5
  → console.warn("EXAM_ITEM_WITHDRAWN", { exam_id, question_id, item_ref, status })
```

Không còn tin hồ sơ chất lượng của item đó thì không dùng độ khó của nó để cân bằng chứng nữa — và kêu lên để Coral thấy đề nào đang trỏ vào câu đã gỡ.

***

## 5. Ba tầng chống rác đầu vào

Đây là phần đáng giá nhất của engine này. Ba tầng độc lập, áp cho cùng một mục tiêu: **model không được tin vào dữ liệu không đáng tin**.

| Tầng | Phạm vi | Cơ chế | Hậu quả |
| --- | --- | --- | --- |
| **Câu** | một lần trả lời | `attemptReliability()` — < 2500 ms, chuỗi ≥3 | reliability **0.05**, không tính là "đã đo" |
| **Phiên** | cả bài chẩn đoán | ≥ 50% câu rapid | reliability mọi câu ép về **0.05** |
| **Tự khai** | một câu | `justification_kind: "guessed"` | hạ **retention** reliability, giữ nguyên mastery |

Cộng thêm hai lớp bảo vệ nội dung:

* Chỉ phát câu `published` — câu bị learner báo sai hoặc bị sàng lọc máy gắn cờ đã lùi về `candidate` và **không tới tay đứa trẻ tiếp theo** (AS-06.3.2, AS-10.4.1).
* Câu bị gỡ giữa phiên: chấm tiếp nhưng **không ghi evidence** ([Learning §5](learning-engine.md#câu-bị-gỡ-giữa-phiên)).

Và một lối thoát trung thực: **"Chưa biết"** (`selected_index = -1`) có reliability **0.7** — cao hơn hẳn đoán bừa. Nói thật là mình không biết là một hành động có giá trị đo, không phải bỏ cuộc.

***

## 6. Đường đi của một bằng chứng

```
Trả lời
  → attemptReliability()                    ← tầng chống rác
  → updateMastery()                         → learner_skill_state   (sự thật sống)
  → retentionStatementForEvidence()         → learner_retention     (trí nhớ, tách riêng)
  → DB.batch  [responses + evidence + skill_state + retention]      ← MỘT giao dịch
  → publishEvent("learner.evidence.recorded")
  → WF-04 (diagnostic/exam) hoặc updateLearnerModelInBackground (practice)
```

**Gộp `DB.batch` là bắt buộc.** Nếu evidence ghi được mà mastery hỏng, model sẽ vĩnh viễn tin rằng lần trả lời đó chưa từng xảy ra — và không có cách nào phát hiện.

Idempotency bám `event_id` (`diag:{session}:{item}`, `practice:{session}:{item}`, `exam:{attempt}:{node}`) với `INSERT OR IGNORE`. Client bấm nộp hai lần, mạng retry — bằng chứng chỉ vào một lần.

***

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

1. Chẩn đoán **chỉ chạy sau khi có Learner Model** (REQ-ONB-04).
2. Chẩn đoán trải rộng — **mỗi node một câu**, không đào sâu.
3. ≥ 50% câu rapid → cả phiên bị hạ reliability, **không** ghi mastery thấp giả.
4. Đo lấy **câu để dành**, luyện tập không đụng tới.
5. Học vượt trượt → **chờ 3 tiếng** và đề **phải khác**.
6. Đề thi gộp bằng chứng **theo node**, `performance` là phân số.
7. Item gốc bị gỡ **không làm sai đề** — chỉ hạ `difficulty` về trung tính.
8. Mọi bằng chứng vào DB trong **một** `DB.batch`, idempotent theo `event_id`.
9. "Chưa biết" là lựa chọn hợp lệ, reliability 0.7.

## 8. Khoảng trống đã biết

| Việc | Trạng thái |
| --- | --- |
| **Adaptive item selection** — chọn câu tiếp theo theo kết quả câu trước | ❌ chưa; đề cố định lúc `start` |
| Chẩn đoán ưu tiên node thuộc goal blueprint | ⚠️ ghi trong comment nhưng **query không thực hiện** — chỉ `ORDER BY grade, level` |
| Kho câu quá mỏng (trung bình 8,5 câu/bài) | ⚠️ đã cảnh báo qua `pool_warning`, nhưng gốc là thiếu nội dung |
| `mastery_check` chưa có run log riêng như WF-03 | ⏳ chẩn đoán và đề thi có, đo mức vững thì chưa |
| Assessment sinh từ `largest_uncertainty` của [Readiness](readiness-model.md#5-largest_uncertainty--readiness-sinh-ra-nhu-cầu-đo) | ⏳ Readiness đã tính `next_assessment_priority`, chưa ai gọi |

Dòng thứ hai đáng chú ý: comment nói *"ưu tiên node thuộc goal blueprint"* nhưng câu SQL không có mệnh đề nào làm việc đó. Comment mô tả **ý định**, không phải hiện trạng.

## Trace

* REQ-INT-22 (insufficient reliable evidence), REQ-ONB-04 (gate Learner Model), REQ-EXAM-07 (3 lớp đo), REQ-INT-21.
* Nguồn: SRC-003 §5, SRC-077, SRC-141 (reserve + cooldown), SRC-105.
* Thiết kế: [SDD-002](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §5/§18, [SDD-012](../architecture/sdd-012-real-exam-bank.md) §4, [SDD-017](../architecture/sdd-017-retention.md) §4.
* Kiểm chứng: QG-005; `selection.test.ts` khoá luật chọn câu.
* Liên quan: [Learning Engine](learning-engine.md) · [Engines §1](engines.md#1-mastery--confidence-engine) · [Retention](retention-model.md) · [Workflows WF-03](workflows.md).
