Skip to content

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):

KindViệcSố câuNguồn câu
practiceLuyện tập — học10 (PRACTICE_TARGET)Kho của một node, trừ phần để dành đo
checkĐo mức vững — đo6 (CHECK_SIZE)Ưu tiên phần để dành (câu phân loại)
skipHọc vượt — đo cả Unit10 (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ớpTrườngThang
1. Đáp ánselected_index-1 = "Chưa biết" — lựa chọn hợp lệ, không phải bỏ cuộc
2. Tự tinconfidence1–5, khai trước khi xem kết quả
3. Vì saojustification_kindderived · 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). 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ệcChi phí
Ghi mastery/retention/evidence của câu vừa làmCố đị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:

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.

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 ​

  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 §11/§18, SDD-010, SDD-017 §4.
  • Liên quan: Assessment Engine · Recommendation · Engines §1 · Retention.