Learning Engine
Recommendation Engine chọn học bài nào. Learning Engine quyết định học thế nào.
Thiết kế: SDD-002 §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 — 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:
retentionReliability = justification_kind === "guessed" ? min(reliability, 0.4) : reliabilityLearner 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). 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). 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 | Đọ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:
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 checkVà: 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.
tutor_hint đã có trong Context Builder 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
- Luyện tập không bao giờ dùng câu để dành cho bài đo (trừ khi kho < 6 câu).
- Không lặp câu trong cùng một phiên để cho đủ số — thà phiên ngắn hơn.
- Kho mỏng thì nói ra (
pool_warning), không im lặng. - Chỉ phát câu đang
published. - Câu bị gỡ giữa phiên: chấm tiếp nhưng không ghi evidence/mastery/retention.
- Chỉ lần trả lời đầu tiên thành bằng chứng năng lực.
- Khai "đoán" hạ retention reliability, không hạ mastery.
- Không dựng lại Learner Model sau mỗi câu.
- 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 §11/§18, SDD-010, SDD-017 §4.
- Liên quan: Assessment Engine · Recommendation · Engines §1 · Retention.