WF-03 — Diagnosis Workflow
Tiền đề bắt buộc: Learner Model + Context Model đã khởi tạo (WF-01 §4). Chẩn đoán đọc model/context để chọn phạm vi — không chạy khi chưa có model.
Thuật toán kế thừa chuyenchon (đã kiểm chứng qua learner thật)
Nguồn: app.chuyenchon.com/app/lib/assess-engine.ts. Nemo12 dùng lại (mastery.ts) với khung SDD-002:
- Prior: node chưa có bằng chứng → mastery 0.5, confidence 0 (không phải 0).
- Cập nhật mỗi câu (surprise-weighted delta):
surprise = correct ? difficulty : (1 - difficulty) // đúng-câu-khó / sai-câu-dễ = bất ngờ lớn decay = 1 / (1 + 0.6 * prevEvidenceCount) // câu đầu tác động mạnh nhất lr = (0.35 + 0.35*surprise) * decay mastery' = clamp(old + lr*(outcome - old)) confidence' = clamp(1 - 0.55^newEvidenceCount) // chỉ phụ thuộc lượng bằng chứng - State suy từ (mastery, confidence): confidence<0.25→unknown; mastery≥0.8→solid; ≥0.5→shaky; else gap.
- Prerequisite propagation (depth-1, chỉ node inferred): node
solid(confident) → nâng prereq ≥0.65; nodegap→ hạ dependent ≤0.45.
Luồng
sequenceDiagram
participant N as Nemo (learn app)
participant API as api.nemo12.com
participant D1
participant Q as Queue nemo12-events
N->>API: POST /v1/diagnostic/start {subject}
API->>D1: đọc Context Model (grade→anchor, school→blueprint)
API->>D1: chọn item (adaptive theo mastery hiện có, không lộ đáp án)
API-->>N: session + đợt câu 1 (một câu mỗi Package) + adaptive/rounds
loop 2 đợt còn lại (SRC-515)
N->>API: POST /v1/diagnostic/:id/next {responses đợt trước}
API->>D1: server CHẤM, cập nhật mốc lo/hi từng Package (adaptive_json)
API-->>N: đợt sau, hoặc done + ceilings (vững tới lớp / hổng từ lớp)
end
N->>API: POST /v1/diagnostic/:id/submit {responses}
loop mỗi câu
API->>D1: chấm → updateMastery(prev) → learner_skill_state
API->>D1: ghi learner_evidence (idempotent event_id)
API->>Q: EvidenceRecorded
end
API->>D1: session completed
API->>Q: AssessmentCompleted
API->>API: build cockpit (readiness + next best action)
API-->>N: cockpitChọn câu (adaptive) — REQ-INT-04
Assessment Engine = information acquisition: chọn câu theo InformationGain (ưu tiên node thuộc goal blueprint, độ khó gần mastery hiện tại, node đòn bẩy cao). Ba mục dưới đây là luật đang chạy, không phải dự định. Code: modules/knowledge/diagnosticPick.ts + modules/knowledge/routes.ts · Test: diagnosticPick.test.ts.
1. Một bài đo cho cả môn, KHÁC NHAU theo lớp (SRC-514)
Hai câu hỏi của chủ dự án 2026-08-22, trả lời theo mã đang chạy:
- Có ứng xử khác theo lớp không? — CÓ. Lớp learner khai trong hồ sơ (
learners.grade, REQ-ONB-02 bắt khai trước mọi hoạt động học) là tham số trung tâm của việc chọn câu. Thiếu lớp thì bài đo vẫn chạy nhưng lùi về bản tĩnh, không chặn learner. - Phủ rộng nhiều Package hay mỗi Package một test riêng? — MỘT bài đo duy nhất cho cả môn, trải qua MỌI Package. Không có bài đo riêng cho từng Package: learner đi khám một lần, ra kết luận cho tất cả các mảng. Package đang ẩn khỏi giao diện bị loại khỏi kho câu (
HIDDEN_SQL_LIST) — đo cái learner không nhìn thấy là đo phí.
Lỗi được vá ở đây đáng ghi lại vì nó là sai mục đích, không phải kém tối ưu: mã cũ kết bằng ORDER BY n.grade, n.level LIMIT 15, tức 15 node dễ nhất của môn, giống hệt nhau cho mọi learner. Đo thật trên Toán ngày 2026-08-22: cả 15 câu đều lớp 1–3, kể cả với learner lớp 9. Bài đo sinh ra để khỏi bắt learner học lại thứ đã biết, mà learner lớp 9 đúng hết mấy câu lớp 1 rồi hệ vẫn mù về mọi thứ từ lớp 4 trở lên; và ấn tượng đầu tiên đọc ra đúng một câu: chỗ này không dành cho mình.
Luật của bản tĩnh pickDiagnostic() — vẫn là phương án lùi khi hồ sơ thiếu lớp:
| Hằng số | Giá trị | Vì sao |
|---|---|---|
PER_PACKAGE | 3 | SDD-021 §3.1 "2 tới 3 câu mỗi Package" |
MAX_ITEMS | 15 | trần cả bài |
| xếp hạng trong Package | theo khoảng cách tuyệt đối giữa lớp của node và lớp của learner; hoà thì ưu tiên câu DƯỚI lớp | đo là để tìm nền hổng, không phải thử xem learner vượt tới đâu |
| thứ tự lấy | vòng tròn qua các Package (hết lượt một mới sang lượt hai) | lấy đủ 3 câu Package đầu rồi mới sang Package sau thì mảng nào nhiều câu sẽ nuốt hết chỗ — đúng lỗi cũ |
Chọn ở TypeScript chứ không nhồi vào SQL vì luật chọn phải test được bằng dữ liệu bịa, và vì cửa sổ SQLite không diễn được "lấy vòng tròn qua các Package". Đo lại trên dữ liệu thật: lớp 9 nhận câu lớp 8–10, lớp 5 nhận lớp 2–6, cả năm Package đều có mặt.
2. "Khám toàn diện" = tìm RANH GIỚI, 15 câu thích ứng (SRC-515)
"Mọi người đều quan tâm việc TÔI CẦN KHÁM TOÀN DIỆN, để biết tôi hổng ở những đâu một cách nhanh nhất. Có thể là một bài 15 câu, nhưng adaptive. Và vẫn cần dùng thông tin lớp mấy hiện tại." — chủ dự án 2026-08-22
Mười lăm câu tĩnh không trả lời được câu đó: chúng chỉ nói learner làm đúng mấy câu quanh lớp mình, không nói TRẦN nằm ở đâu. Muốn biết hổng từ đâu thì phải đi tìm ranh giới, và tìm bằng ba câu cho mỗi Package chính là tìm kiếm nhị phân theo lớp:
| Câu | Hỏi ở lớp nào | Vì sao |
|---|---|---|
| 1 | đúng lớp hiện tại của learner | biết ngay em có theo kịp lớp mình không |
| 2 | đúng → nhảy LÊN 2 lớp; sai → lùi XUỐNG 2 lớp | nới nhanh, vì chưa biết trần ở đâu |
| 3 | đã có cả mốc đúng lẫn mốc sai → hỏi ngay GIỮA hai mốc; chưa có thì đi tiếp cùng hướng 4 lớp | khép khoảng |
Con số thật trong diagnosticPick.ts: ROUNDS = 3 vòng, mỗi vòng đúng một câu mỗi Package. Tổng số câu = 3 × số Package của môn — với Toán năm Package thì đúng 15 câu, đúng con số chủ dự án nêu; môn ít Package hơn thì bài ngắn hơn chứ không độn thêm câu cho tròn 15. Lớp đích luôn kẹp trong 1..12. Trạng thái từng Package là hai mốc: lo = lớp cao nhất đã làm ĐÚNG, hi = lớp thấp nhất đã làm SAI. Ba câu đủ khoanh trần trong khoảng hai lớp, và kết quả là một câu đọc được cho từng mảng: vững tới lớp 7, hổng từ lớp 8.
Server chấm, không tin client. Bài đo ra câu theo ĐỢT qua POST /v1/diagnostic/{sessionId}/next: client gửi selected_index, server tự tra correct_index rồi mới quyết đợt sau. Nếu để client tự khai đúng/sai thì mở tab thứ hai là lái được cả bài đo về vùng dễ. Trạng thái phiên ({ grade, asked[] }) nằm ở cột assessment_sessions.adaptive_json, migration 0069. Câu client gửi mà không có trong kho thì bỏ qua, không làm hỏng cả phiên: dữ liệu lạ không được phép chặn learner đang làm bài dở.
Cố ý KHÔNG dùng IRT / ước lượng Bayes ở vòng này. IRT cần ngân hàng câu đã hiệu chỉnh độ khó; thứ Nemo12 đang có là LỚP của node — một thang thô nhưng có thật. Dùng thang thô cho đúng việc còn hơn một mô hình đẹp chạy trên tham số bịa. Đây là quyết định "không làm", ghi lại để đừng ai mở lại nó trước khi có ngân hàng câu đã calibrate.
Đường lùi. Hồ sơ thiếu lớp, hoặc D1 chưa chạy migration 0069 (push thường deploy code trước migration) → phiên mở bình thường nhưng adaptive_json là NULL, /next trả done: true ngay, và bài rơi về bản tĩnh trải rộng của SRC-514. Không đo được vẫn tệ hơn đo thô.
3. Kết quả đo bày theo MẢNG, và engine chọn bài dùng lớp hiện tại (SRC-516)
Màn kết quả khám. Trước đây chấm xong là nhảy thẳng vào trang học — learner làm 15 câu rồi không được biết mình vừa được kết luận gì, đúng câu hỏi lớn nhất của người đi khám. Nay dừng lại một màn, mỗi mảng một dòng:
- Nói bằng lời, không bằng điểm: "Vững tới lớp 7. Cần vá từ lớp 8" — dựng từ
ceilingByStrand()(solidUpTo=lo,gapFrom=hi). - Kèm TÊN mảng chứ không phải mã:
withStrandNames()trastrands.name_vi. Learner và phụ huynh không tra được "B3" nghĩa là gì. Thiếu bảngstrandsthì trả mã, còn hơn không trả gì. - Không xếp hạng, không phần trăm, không so với ai. Bài đo sinh ra để chỉ chỗ bắt đầu; một con số tổng chỉ làm learner tự chấm điểm mình.
- Chi tiết kỹ thuật đáng nhớ ở
apps/learn/src/App.tsx: phải giữ kết quả bằnguseRefsong song với state, vì hàm chấm chạy ngay trong cùng một tick vớisetStatenên đọc state thì màn kết quả không bao giờ hiện.
Chọn bài theo lớp. Chiến lược nền móng (Unit dễ nhất mà chặn nhiều Unit khác nhất) trước đây quét toàn môn, nên learner lớp 9 chưa có bằng chứng nào được mời học "Đếm, gộp và tách" của lớp 1. SRC-516 dựng cửa sổ lớp G-3..G thử trước rồi mới nới ra toàn môn; đo trên dữ liệu thật: cùng một Unit chặn 10 Unit khác, learner lớp 9 nay nhận bản lớp 6 thay vì bản lớp 1. Cửa sổ là ưu tiên, không phải bộ lọc cứng — môn chưa có nội dung ở khoảng đó thì vòng sau vẫn tìm được bài, thà học bài xa lớp còn hơn màn hình trống.
Cửa sổ này đã được SRC-545 (2026-08-24) siết lại thành ba vòng nới dần: (1) G-1..G xếp lớp thấp trước — ôn nền năm ngoái trước rồi tới lớp hiện tại, (2) G-3..G cho learner hổng sâu, (3) toàn môn. Bài lớp thấp hơn cửa sổ vẫn tới được với learner, nhưng qua đường ĐO thấy hổng (repair/urgent) hoặc tự chọn trên bản đồ, không qua lời mời mặc định. Chi tiết ở SDD-021.
Async (REQ-INT-13)
Baseline hiện đồng bộ trong request (đủ cho ~15 câu). Khi mở rộng (propagation, recompute readiness nhiều môn) → chuyển sang WF-04 (Cloudflare Workflow) qua AssessmentCompleted.
Sự kiện
EvidenceRecorded · SkillMasteryChanged · AssessmentCompleted.
Trace
REQ-ONB-04→gate model · REQ-INT-04→chọn câu · REQ-INT-01/02→evidence+state · REQ-INT-13→async.
Nguồn: SRC-003, SRC-013 · SRC-514 (bài đo bám lớp learner, trải đều Package) · SRC-515 (15 câu thích ứng, server chấm, không dùng IRT) · SRC-516 (màn kết quả theo mảng + cửa sổ lớp khi chọn bài).