Skip to content

Learner Model ​

"Học sinh này đang ở đâu về mặt học thuật?" — bức ảnh tổng hợp, chụp tại một thời điểm, có thể xem lại.

Thiết kế: SDD-002 §2 · Code: modules/models/service.ts → runLearnerModelEngine() · Test: models/service.test.ts · Bảng: learner_model_versions (model_kind='learner') + learner_models (bảng gốc 0003)


1. Learner Model không phải nơi lưu mastery ​

Đây là điều dễ hiểu sai nhất và cần nói ngay:

learner_skill_stateLearner Model
Là gìSự thật sống — mastery/confidence từng nodeBức ảnh tổng hợp tại một thời điểm
Ai ghiMastery Engine, mỗi câu trả lờiLearner Model Engine
Có version❌✅ có hash, có lịch sử
Hỏi "con đang thế nào bây giờ"✅ đọc bảng này❌
Hỏi "hôm đó hệ thống nghĩ gì về con"❌✅ đọc version

Learner Model chỉ đọc, không bao giờ ghi learner_skill_state. Nó là lớp tổng hợp + đóng băng: gom mastery từng node thành bức tranh theo môn, rồi lưu lại kèm hash để sau này đối chiếu.

Vì sao cần cả hai: khi phụ huynh hỏi "tháng trước hệ thống bảo con ổn, sao giờ lại bảo yếu?", câu trả lời nằm ở việc so hai version — không nằm ở bảng trạng thái hiện tại (đã bị ghi đè từ lâu).


2. Model chứa gì ​

jsonc
{
  "algorithm_version": "learner-v2",
  "computed_at": "2026-08-15T…",
  "confidence": 0.83,
  "profile": { "grade": 8, "current_school": "…", "province": "…" },
  "academic_state": {
    "nodes_tracked": 154,
    "subjects": [{
      "subject_id": "math",
      "nodes_total": 210,            // SRC-528: mẫu số coverage = CẢ graph, kể cả node chưa chạm
      "coverage": 0.2,               // nodes_assessed / nodes_total — coverage ≠ mastery
      "nodes_assessed": 42,          // chỉ node có evidence_count > 0
      "nodes_mastered": 18,          // mastery ≥ 0.8
      "avg_mastery": 0.61,
      "avg_confidence": 0.72,
      "trajectory": { "improving": 12, "stable": 25, "declining": 5 },
      "strengths": [ /* top 5 theo mastery */ ],
      "gaps":      [ /* bottom 5 theo mastery */ ],
      "nodes":     [ /* SRC-508: MỌI node có evidence_count > 0, xếp mastery giảm dần, trần 500 */ ],
      "nodes_omitted": 0,            // SRC-508: số node bị trần cắt — cắt thì phải đếm được
      "last_evidence_at": "…"
    }]
  },
  "evidence": {                     // SRC-528: State + Evidence
    "total": 318, "avg_reliability": 0.87,
    "first_evidence_at": "…", "last_evidence_at": "…",
    "distinct_sources": 3, "distinct_types": 4,
    "by_type": { "item_response": 290, "self_prediction": 28 }
  },
  "behavior": {                     // SRC-528: cách learner làm, không phải làm được bao nhiêu
    "sessions_total": 40, "sessions_completed": 33, "sessions_abandoned": 7,
    "completion_rate": 0.83, "responses_timed": 310, "avg_response_ms": 18700
  },
  "goals_summary": { "goal_model_version": 4, "total": 3, "empty": false, … },
  "confidence": 0.83
}

Mỗi node mang theo chứng cớ của chính nó (SRC-508) ​

Câu hỏi 2026-08-21: có cần thêm evidence_count, trajectory, last_evidence_at vào từng node không? Đã thêm, và đang chạy — cả ba trường có mặt trong service.ts (hàm dựng node()) và được QG-005 canh bằng test models/service.test.ts ("mỗi node có evidence_count, trajectory và last_evidence_at chứ không chỉ mastery").

Mỗi phần tử của strengths / gaps / nodes có cùng một hình dạng:

jsonc
{
  "node_id": "…", "title": "…",
  "mastery": 0.4, "confidence": 0.62, "state": "gap",
  "evidence_count": 30,               // SRC-508
  "trajectory": "declining",          // SRC-508: improving | stable | declining | unknown
  "last_evidence_at": "2026-08-20T…"  // SRC-508
}

Lý do: mastery: 0.4 một mình không đọc được. 0.4 sau 2 lần làm bài nghĩa là hệ chưa biết gì về node ấy; 0.4 sau 30 lần và đang declining là một đứa trẻ đang tuột. Hai tình huống đòi hai phản ứng khác hẳn nhau, mà bản chụp cũ (chỉ mastery + confidence) không phân biệt nổi. last_evidence_at trả lời câu thứ ba: con số này đo hôm qua, hay đo ba tháng trước rồi nằm im. Giá trị thiếu thì lấy mặc định rõ ràng (evidence_count: 0, trajectory: "unknown", last_evidence_at: null) chứ không bỏ trường — người đọc phải phân biệt được "không có" với "chưa ghi".

nodes[] và luật cắt-thì-phải-đếm (SRC-508) ​

Cùng đợt, bản chụp bỏ luôn thói quen chỉ giữ 5 strength + 5 gap mỗi môn. Một learner có bằng chứng ở 80 node chỉ còn 44 node trong hồ sơ, và không dòng nào nói là đã bỏ bớt — một bản ghi sinh ra để đối chiếu về sau mà im lặng cắt thì hỏng đúng công dụng của nó.

  • nodes[] giữ mọi node có evidence_count > 0, xếp mastery giảm dần.
  • Trần 500 node mỗi môn (MAX_NODES_PER_SNAPSHOT). Có trần vì content_json của mỗi version nằm trong một dòng D1 còn số version thì tăng mãi; không trần thì learner học lâu năm làm phình learner_model_versions không giới hạn.
  • Phần bị trần cắt đếm được: nodes_omitted nằm ngay trong chính bản chụp. Cắt im lặng thì bản chụp nói dối; cắt có ghi số thì nó vẫn trung thực.
  • strengths / gaps giữ nguyên 5 node, không bỏ: chúng là hai khung ngắm và recommendation.ts đọc chúng. nodes[] là bổ sung, không phải thay thế.
  • Node chưa có bằng chứng vẫn không được lọt vào nodes[] (bất biến §6.2) — thêm chúng là kéo dài mô hình bằng node chưa ai đo bao giờ.

Vì sao evidence không chỉ là một con số tổng (SRC-528) ​

Learner Model là State + Evidence: mỗi claim phải trả lời được "vì sao tin". 300 evidence cùng một loại practice yếu hơn hẳn 50 evidence trải trên diagnostic + practice + mastery_check — nên bản chụp mang distinct_sources/distinct_types/by_type chứ không chỉ total.

Freshness cố tình KHÔNG lưu dạng "số ngày": trường đó đổi theo đồng hồ chứ không theo learner — mỗi lần engine chạy lại đẻ một version mới dù không có gì xảy ra, phá luật dedupe (§3.4). Người đọc tự trừ từ last_evidence_at. Cùng lý do, behavior.avg_response_ms làm tròn về bậc 100ms.

behavior quan sát, không phán xét: bỏ dở nhiều là tín hiệu để Learning Engine đổi cách đưa bài (bài ngắn hơn, thử thách nhỏ hơn), không phải một điểm số về learner.

Ba điều dễ đọc sai ​

1. confidence đo lượng bằng chứng, không đo học lực.

confidence = 1 − 0.9^(số evidence)
Số evidenceconfidence
00
100.65
300.96

Học sinh giỏi mới làm 3 câu vẫn có confidence 0.27. Khi confidence thấp, mọi con số khác trong model đều là phỏng đoán — đọc avg_mastery mà bỏ qua confidence là hiểu sai model.

Cùng công thức với mastery confidence (SDD-002 §8) nhưng cơ số khác (0.9 thay vì 0.55): model cấp learner cần nhiều bằng chứng hơn một node lẻ mới đáng tin.

2. strengths/gaps chỉ tính trên node đã đo (evidence_count > 0).

Node chưa đo không bao giờ bị gọi là "điểm yếu". Đây là luật đạo đức, không phải chi tiết kỹ thuật: gọi một đứa trẻ là yếu ở thứ chưa từng hỏi nó là vu oan.

Hệ quả: learner mới có subjects: [] — không phải "yếu mọi thứ", mà là chưa biết gì cả.

3. goals_summary là REF, không phải bản sao.

jsonc
"goals_summary": { "goal_model_version": 4, …summary của Goal Model }

Nhúng goal_model_version để truy ngược. Không copy goals[] vào đây — Goal Model là nguồn sự thật duy nhất. Đây là lý do thứ tự trong WF-04 bắt buộc Goal chạy trước Learner.


3. Cơ chế CẬP NHẬT — hai đường, nặng nhẹ khác nhau ​

3.1 Đường đầy đủ: WF-04 ​

WF-04 step 3, sau Goal → Context. Chạy khi: nộp bài chẩn đoán, nộp đề thi, khai/sửa mục tiêu, admin bấm tính lại.

3.2 Đường nhẹ: cập nhật theo từng câu trả lời (SRC-130) ​

ts
updateLearnerModelInBackground(c, { learnerId, trigger: "EvidenceRecorded:practice", requestId });

Chỉ chạy hai engine: Learner Model + Readiness — hai model duy nhất đổi theo mastery. Goal/Context/Constraint không đổi vì một câu trả lời, chạy cả chuỗi mỗi câu là phí.

Vì sao móc vào từng câu chứ không đợi "nộp bài"

Yêu cầu của chủ dự án (SRC-130): Learner Model phải theo kịp learner ngay sau mỗi Experience — và cả khi em bỏ dở giữa chừng.

Bỏ dở nghĩa là không bao giờ có sự kiện hoàn thành. Nếu chỉ móc vào "nộp bài", một đứa trẻ làm 7 câu rồi đóng máy sẽ để lại 7 bằng chứng mà model không bao giờ nhìn thấy. Móc vào từng câu trả lời thì bằng chứng đã có là model đã biết.

Chạy nền, không chặn response: gọi qua c.executionCtx.waitUntil(). Learner thấy kết quả câu vừa làm ngay lập tức, engine chạy sau lưng. Không có executionCtx (test, queue consumer) thì chạy thẳng.

Ba điểm móc, tất cả trong modules/learning/routes.ts:

TriggerKhi nào
EvidenceRecorded:practiceTrả lời một câu luyện tập
EvidenceRecorded:self_predictionKhai khởi động đầu bài
ExperienceCompleted:{status}Xong (hoặc trượt) một Experience

3.3 Lỗi không bao giờ chặn learner ​

ts
catch (e) { logEvent("error", "learner_model_update_degraded", { learner_id, trigger, request_id, … }); }

Engine nổ thì chỉ có một dòng log. Học sinh vẫn làm bài bình thường. Cùng nguyên tắc với run log và event publish.

Theo dõi learner_model_update_degraded trong Workers logs — tăng đột biến nghĩa là engine đang hỏng dù chưa ai kêu.

3.4 Version chỉ tăng khi nội dung đổi ​

Qua saveModelVersion(): JSON.stringify → SHA-256 → so với version active. Trả lời một câu mà kết quả tổng hợp không đổi (làm tròn 2 chữ số nuốt mất thay đổi nhỏ) → có dòng engine_runs nhưng không có version mới.

Đây là điều làm cho đường 3.2 an toàn: chạy engine sau mỗi câu không tạo ra hàng nghìn version rác.

3.5 Đồng bộ ngược về learner_models ​

ts
if (saved.changed && saved.version > 0) { INSERT … ON CONFLICT DO UPDATE }

learner_models là bảng gốc từ migration 0003, có trước hệ thống version tổng quát. Giữ đồng bộ để code cũ không gãy. Chỉ ghi khi changed — không có version mới thì không có gì để đồng bộ.


4. Bảo đảm mọi learner đều có model (SRC-130) ​

CORE_MODEL_KINDS — 7 model mà một learner đã dùng hệ thống phải có:

goal · context · learner · readiness · retention · constraint · learning_plan

missingCoreModels() tìm model thiếu; ensureAllModels() dựng bù, đúng thứ tự phụ thuộc của WF-04, và chỉ chạy engine của model còn thiếu.

"Chưa build" và "đã build, chưa có gì" là hai chuyện khác nhau — admin phải phân biệt được. Vì vậy engine luôn sinh version kể cả khi nội dung rỗng, thay vì để trống trong admin và không ai biết là chưa chạy hay không có gì.

Tình huống thật gây ra nhu cầu này: learner có hoạt động trước khi engine được nối dây → không sự kiện nào từng chạy WF-04 cho em ấy. Cron dựng bù.


5. Cách DÙNG ​

NơiĐọc gì
GET /v1/learners/{id}/model-versionsDanh sách version mọi model (admin/mentor)
GET /v1/learners/{id}/model-versions/{version}Nội dung đầy đủ một version — REQ-INT-29
admin.nemo12.comDuyệt model theo learner, xem lịch sử
Planning EngineVersion mới nhất, cùng Readiness + Retention + Constraint
Lighthouse (/v1/learners/{id}/lighthouse)Tổng hợp trạng thái

Màn hình học không đọc Learner Model

Cockpit, Phòng Lab, tiến độ đều đọc thẳng learner_skill_state — cần số hiện tại, không cần bức ảnh.

Learner Model phục vụ ba việc khác: xem lại lịch sử, giải thích quyết định, và cấp dữ liệu cho Planning. Nhầm vai trò này sẽ dẫn tới việc hiển thị số cũ cho learner sau khi em vừa làm bài xong.


6. Bất biến — vi phạm là bug ​

  1. Learner Model chỉ đọc, không bao giờ ghi learner_skill_state.
  2. strengths/gaps chỉ tính trên node có evidence_count > 0.
  3. goals_summary là tham chiếu kèm goal_model_version — không sao chép goal.
  4. confidence đo lượng bằng chứng, không đo học lực.
  5. Version chỉ tăng khi content_hash đổi.
  6. Cập nhật model không bao giờ chặn hoặc làm hỏng đường làm bài.
  7. Bản chụp không được cắt im lặng: đã cắt thì phải đếm được (nodes_omitted) — SRC-508.
  8. Chạy sau Goal và Context trong WF-04 — đảo thứ tự cho ra model tham chiếu version cũ.

7. Khoảng trống đã biết ​

ViệcTrạng thái
avg_mastery là trung bình không trọng số — node quan trọng và node phụ tính ngang nhau🕓 muốn có trọng số thì phải gắn blueprint, mà thế là readiness
strengths/gaps cứng 5 node✅ vẫn cứng 5 có chủ đích (khung ngắm cho recommendation.ts); phần đầy đủ nằm ở nodes[] từ SRC-508
Trajectory của cả model theo thời gian🕓 dựng được từ các version nhưng chưa có view
Transfer — làm được ở context mới không (SRC-528)🕓 cần item gắn nhãn "novel context" trước, chưa có nguồn đo thì không bịa
Preferences quan sát được — help-seeking, retry pattern (SRC-528)🕓 chờ event hành vi chi tiết hơn; hiện chỉ có behavior mức phiên

Trace ​

  • REQ-INT-29 (model version inspectable).
  • Nguồn: SRC-032, SRC-105, SRC-130 (cập nhật liên tục + đủ 7 model lõi), SRC-508 (chứng cớ từng node + nodes[]/nodes_omitted), SRC-528 (learner-v2: State + Evidence, coverage, behavior).
  • Thiết kế: SDD-002 §2/§8/§15.
  • Kiểm chứng: QG-005 (models/service.test.ts).
  • Liên quan: Context Model · Goal · Readiness · Engines §1/§3 · Workflows.