Skip to content

Learning Plan Model ​

Chiến lược học hiện tại của learner trong một khoảng thời gian — không phải danh sách bài cố định.

Trang này mô tả cả Learning Plan Model lẫn Planning Engine. Engine là phần tính (§4 thuật toán chia buổi học, §5 confidence, §6 changes); Model là phần lưu (§3). buildLearningPlan() và runPlanningEngine() là hai nửa của cùng một thứ.

Thiết kế: SDD-007 §6b · Code: modules/models/planning.ts · Test: planning.test.ts (13 ca) · Snapshot: learner_model_versions (model_kind='learning_plan')


1. Plan là chiến lược, không phải thời khoá biểu ​

Learning PlanNext Best Action (cockpit)
Trả lời"Nên phân bổ việc học thế nào?""Hôm nay làm bài nào?"
Phạm viNhiều tuần (horizon_days)Một buổi
Nội dungGiai đoạn, phân bổ phút, thứ tự ưu tiên5 việc cụ thể, có node_id
Tính lại khiHoàn cảnh đổiMỗi lần mở màn hình

Plan nói "mỗi buổi 45 phút: 10 phút ôn phần sắp quên, 21 phút vá Đại số, 14 phút luyện đề". Cockpit sau đó chọn đúng bài nào cho từng khối đó.

Đây là living plan: tính lại khi learner hoặc hoàn cảnh đổi. Không phải bản kế hoạch in ra dán tường.

2. Model tổng hợp cả sáu model kia ​

Plan là điểm hội tụ của toàn bộ Learner Intelligence:

Đầu vàoTrả lời phần nào của kế hoạch
Learner Model (qua learner_skill_state)Yếu ở mạch nào → giai đoạn "xây lại nền"
Retention ModelBao nhiêu phần sắp quên → phút dành cho ôn
Goal ModelĐang hướng tới đâu, còn bao lâu → horizon + mode
Context Model + Context EventsỐm, bận, sắp thi → cắt giờ, đổi ưu tiên
Constraint ModelQuỹ thời gian → tổng số phút
Readiness ModelCòn cách target bao xa → inputs.readiness_best

Thiếu bất kỳ cái nào plan vẫn chạy, nhưng confidence phải nói rõ đang đoán nhiều tới đâu (§5).


3. Model chứa gì ​

jsonc
{
  "algorithm_version": "plan-v1",
  "confidence": 0.72,
  "confidence_basis": "chưa ai khai quỹ thời gian",
  "horizon_days": 36,
  "goal": { "id": "…", "title": "Thi học kỳ 1 — Toán", "deadline": "2026-09-20" },
  "daily_minutes": 45,
  "minutes_source": "con/bố mẹ khai: \"tuần này con chỉ học được 30 phút\"",
  "mode": { "mode": "exam_prep", "exam_prep_weight": 0.8, "days_to_exam": 36 },
  "priorities": [ { "label": "Vá chỗ hổng trước", "why": "7 bài chưa vững" } ],
  "phases": [
    { "name": "Giai đoạn 1 — Xây lại nền", "weeks": 2, "focus": "Đại số",
      "why": "7 bài chưa vững; học phần mới trên nền chưa chắc thì càng học càng hổng" }
  ],
  "allocation": [
    { "label": "Ôn lại phần sắp quên", "minutes": 11, "why": "3 phần kiến thức đang có nguy cơ quên (Retention Model)" },
    { "label": "Vá chỗ yếu: Đại số", "minutes": 20, "why": "7 bài đang lung lay hoặc hổng (Learner Model)" },
    { "label": "Luyện theo đề và dạng bài của kỳ thi", "minutes": 14, "why": "Còn 36 ngày tới kỳ thi — ưu tiên luyện thi 80%" }
  ],
  "maintenance": [ { "label": "Giữ phần đã vững", "sessions_per_week": 2, "why": "18 bài đang chắc — không ôn thì vài tuần là phai" } ],
  "milestones": [ … ],
  "replanning_triggers": [ … ],
  "inputs": { /* dấu vết để giải thích lần đổi sau */ },
  "changes": { "from_version": 3, "changes": [ … ], "why": [ … ], "triggered_by": "…" }
}

Mọi khối đều có why. Đây là ràng buộc thiết kế, không phải trang trí: một kế hoạch không giải thích được vì sao thì learner không có cách nào tin nó, và bố mẹ không có cách nào phản biện nó.


4. Thuật toán — cách một buổi học được chia ​

Bước 1 — Quỹ thời gian mỗi ngày ​

1. Lời khai time_budget còn hiệu lực   → minutes_per_day          (ưu tiên cao nhất)
2. available_minutes_per_week / 7      → quỹ trong hồ sơ
3. DEFAULT_MINUTES = 45                → "chưa ai khai, tạm lấy 45 phút/ngày"

Lời khai nhất thời thắng hồ sơ: "tuần này con bận" mới là sự thật của tuần này.

minutes_source luôn ghi lại nguồn, và khi là lời khai thì trích nguyên văn — learner đọc thấy chính câu mình vừa nói.

Bước 2 — Ốm/stress cắt đôi thời gian ​

ts
if (illness) minutes = Math.max(MIN_MINUTES /* 15 */, Math.round(minutes / 2));

Có lời khai illness hoặc stress → giảm một nửa, sàn 15 phút. Không về 0: giữ một nhịp nhỏ tốt hơn là dừng hẳn rồi mất đà.

Bước 3 — Chia ba khối ​

retentionMinutes = at_risk > 0 ? clamp(25% × minutes, 5, 15) : 0
rest             = minutes − retentionMinutes
weakShare        = ốm       → 0.6
                 = exam_prep → 0.5
                 = còn lại   → 0.6
weakMinutes      = rest × weakShare
goalMinutes      = rest − weakMinutes

Ba quyết định đáng chú ý:

  • Ôn lấy trần 15 phút. Chống quên là quan trọng nhưng không được nuốt cả buổi học.
  • Gần thi thì phần "vá chỗ yếu" giảm (0.6 → 0.5), nhường chỗ cho luyện đề. Sát ngày thi mà đi vá nền là hại.
  • Ốm thì phần vá chỗ yếu giữ 0.6. Ít thời gian thì làm việc quan trọng nhất, không phải làm việc dễ nhất.

Bước 4 — Chia giai đoạn ​

Có bài yếu → hai giai đoạn:

Giai đoạn 1 — Xây lại nền      weeks = clamp(ceil(số bài yếu / 4), 2, nửa tổng số tuần)
Giai đoạn 2 — Luyện thi / Đi tiếp phần mới

Lý do ghi thẳng vào why: "học phần mới trên nền chưa chắc thì càng học càng hổng".

Không có bài yếu → một giai đoạn duy nhất theo mode.

Bước 5 — Thứ tự ưu tiên ​

vá chỗ hổng (nếu có bài yếu)
ôn phần sắp quên (nếu retention at_risk > 0)
luyện đúng dạng đề (nếu exam_prep)
ưu tiên phần sẽ thi (nếu có lời khai "sắp thi" mà hệ thống CHƯA có ngày thi)
học ngắn đủ đều (nếu ốm/stress)

Dòng thứ 4 quan trọng: lời khai "sắp thi" vẫn đổi ưu tiên kể cả khi hệ thống không có ngày thi cụ thể. Không có ngày trong DB không có nghĩa là lời nói của gia đình bị bỏ qua.


5. Confidence — bốn chân ​

confidence = 0.40 × (1 − 0.9^số node đã đo)      // bão hoà quanh 20 node
           + 0.25 × (có goal hoặc lịch thi)
           + 0.20 × (có ai khai quỹ thời gian)
           + 0.15 × (có bức tranh trí nhớ)
sàn 0.1

Trọng số theo đúng thứ tự ảnh hưởng: đo được gì quan trọng gấp gần ba lần biết trí nhớ thế nào. Có test khoá riêng thứ tự này.

confidence_basis liệt kê đúng những chân đang thiếu bằng tiếng người: "chưa đo được bài nào; chưa ai khai quỹ thời gian". Learner/bố mẹ có quyền biết plan đang đoán tới đâu (AS-05.5.4).

Sàn 0.1 chứ không phải 0: plan vẫn có giá trị (nó vẫn đúng về mặt nguyên tắc sư phạm), chỉ là chưa cá nhân hoá được.


6. changes — "đổi gì so với bản trước, vì sao" ​

Đây là phần khiến model này khác một hàm tính toán thuần.

Không có phần này thì learner chỉ thấy plan "tự nhiên khác đi" và mất niềm tin.

diffPlans(prev, next, trigger) so những thứ learner thật sự nhìn thấy:

So gìVí dụ
daily_minutes"45 phút → 22 phút"
Các giai đoạntên + số tuần
Phân bổ một buổinhãn + số phút
Thứ tự ưu tiêndanh sách nhãn
Chế độ học"Học chắc → Luyện thi"

Rồi why giải thích nguyên nhân, so chính các inputs đã sinh ra plan:

"Bố mẹ vừa khai: \"con bị ốm tuần này\""
"Lời khai \"con bị ốm\" đã hết hiệu lực"
"Số bài chưa vững đổi từ 9 thành 7 sau các bài con vừa làm"
"Còn 36 ngày tới mốc thi (trước đó còn 50 ngày)"

Ba chi tiết đã trả giá để có:

  1. Nói rõ ai khai — source_role === "parent" → "Bố mẹ vừa khai", không gán cho con.
  2. So theo tập khoá kind:statement, không dùng includes() trên chuỗi nối — vì "con bị ốm" là chuỗi con của "con bị ốm nặng" và sẽ khớp nhầm.
  3. Có thay đổi mà không tìm ra nguyên nhân cụ thể thì vẫn phải nói một câu, không im lặng: "Đầu vào đổi nhẹ nên phân bổ được tính lại cho khớp."

Bản đầu tiên nói thẳng "Đây là kế hoạch đầu tiên của con." — không bịa ra thay đổi.


7. Cơ chế CẬP NHẬT ​

Bốn đường chạy ​

ĐườngTriggerKhi nào
WF-04 step 7AssessmentCompleted, GoalChanged…Sau mọi sự kiện lớn
Mở trang lần đầufirst_viewChưa có bản nào — learner mở là thấy plan, không phải chờ cron
Bản cũ quá 24hstale_viewCó thứ đổi theo thời gian chứ không theo hành động
Bấm tính lạimanual_replanPOST /v1/learners/{id}/learning-plan/replan

Đường stale_view xử lý đúng vấn đề mà Goal và Constraint còn bỏ ngỏ: còn mấy ngày tới kỳ thi, phần nào vừa rơi vào nhóm sắp quên — những thứ này đổi mà không cần ai làm gì.

Hash chỉ tính trên phần kế hoạch ​

ts
saveModelVersion({ content: { ...plan, changes }, hashContent: plan });

changes.from_version và triggered_by đổi mỗi lần chạy. Nếu đưa vào hash thì kế hoạch y hệt vẫn đẻ version mới — mỗi lần learner mở trang, mỗi lần nộp bài, mỗi lượt cron lại thêm một bản, và lịch sử thành rác.

Đây là lý do hashContent tồn tại như một tham số riêng trong saveModelVersion().

replanning_triggers — nói trước cho learner ​

Model tự khai 4 điều sẽ khiến nó phải tính lại. Nói trước để learner không thấy plan đổi là bất ngờ:

  • Con hoặc bố mẹ khai thêm thông tin (ốm, bận, đổi quỹ thời gian…)
  • Đổi hoặc thêm mục tiêu, đổi lịch thi
  • Kết quả bài đo làm đổi mức vững của một phần
  • Có thêm phần kiến thức rơi vào nhóm sắp quên

8. Cách DÙNG ​

EndpointTrả về
GET /v1/learners/{id}/learning-planPlan hiện hành + danh sách version (tự tính nếu chưa có hoặc quá 24h)
GET /v1/learners/{id}/learning-plan/versions/{version}Nội dung đầy đủ một bản cũ
POST /v1/learners/{id}/learning-plan/replanTính lại ngay

Bề mặt: learn (learner xem plan + các bản trước kèm "đổi gì, vì sao" — US-79) và marlins #/child/{id}/plan.

Bố mẹ xem đúng plan của con, không có bản riêng

REQ-PAR-15. Không có "plan cho phụ huynh" tách biệt — cả nhà nhìn cùng một kế hoạch. Bố mẹ khai thêm thông tin cho Planning Engine qua context event, chứ không chỉnh sửa kế hoạch của con.


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

  1. Mọi khối trong plan phải có why.
  2. Hash chỉ tính trên phần kế hoạch, không gồm changes.
  3. Bản đầu tiên nói rõ là bản đầu — không bịa thay đổi.
  4. Có thay đổi thì luôn có ít nhất một câu giải thích.
  5. Lời khai được trích nguyên văn và nói rõ ai khai.
  6. confidence phải liệt kê đúng những chân đang thiếu.
  7. Ốm/stress giảm giờ nhưng không về 0 (sàn 15 phút).
  8. Chạy sau Constraint trong WF-04.

10. Kiểm chứng ​

planning.test.ts — 13 ca test hành vi:

  • bản đầu tiên không bịa thay đổi · plan y hệt thì không có thay đổi nào
  • ốm → giảm giờ, nêu đúng số cũ/mới và trích nguyên văn lời khai
  • bố mẹ khai thì nói rõ là bố mẹ khai, không gán cho con
  • lời khai hết hiệu lực cũng phải được giải thích
  • có thay đổi mà không rõ nguyên nhân thì vẫn phải nói một câu, không im lặng
  • confidence: chạm sàn khi trắng dữ liệu và liệt kê đủ 4 thứ thiếu; không bao giờ vượt 1; có mục tiêu đóng góp nhiều hơn có trí nhớ (khoá thứ tự trọng số)

Gate: QG-005.

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

ViệcTrạng thái
Đọc capacity qua Constraint Model thay vì đọc thẳng learner_context_models⏳ hiện Planning đọc thẳng bảng, chưa dùng snapshot Constraint
Cockpit dùng plan để chọn Next Best Action⏳ hai đường còn độc lập; cockpit tự tính từ readiness
REQ-PAR-11 — đánh dấu hoàn thành việc trong tuần⏳ plan chưa có trạng thái theo việc
energy ảnh hưởng phân bổ🕓 đã thu thập, chưa dùng trong công thức

Trace ​

  • REQ-INT-20 (Planning Engine), REQ-PAR-15 (bố mẹ xem đúng plan của con), REQ-INT-29 · US-79.
  • Nguồn: SRC-105, SRC-130.
  • Thiết kế: SDD-007 §6b; mode blend SDD-011 §6.
  • Kiểm chứng: QG-005 (planning.test.ts).
  • Liên quan: Constraint · Goal · Retention · Readiness · Workflows.