---
url: https://docs.nemo12.com/reference/learning-plan-model.md
description: >-
  Learning Plan Model và Planning Engine: chiến lược học theo khoảng thời gian,
  thuật toán chia buổi, confidence và changes.
---

# 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](../architecture/sdd-007-learner-orchestration.md) §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 Plan | Next 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 vi | Nhiều tuần (`horizon_days`) | Một buổi |
| Nội dung | Giai đoạn, phân bổ phút, thứ tự ưu tiên | 5 việc cụ thể, có node_id |
| Tính lại khi | Hoàn cảnh đổi | Mỗ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](readiness-model.md#72-cockpit--gap--hành-động-cụ-thể) 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ào | Trả lời phần nào của kế hoạch |
| --- | --- |
| [Learner Model](learner-model.md) (qua `learner_skill_state`) | Yếu ở mạch nào → giai đoạn "xây lại nền" |
| [Retention Model](retention-model.md) | Bao nhiêu phần sắp quên → phút dành cho ôn |
| [Goal Model](goal-model.md) | Đang hướng tới đâu, còn bao lâu → horizon + mode |
| [Context Model](learner-context-model.md) + Context Events | Ốm, bận, sắp thi → cắt giờ, đổi ưu tiên |
| [Constraint Model](constraint-model.md) | Quỹ thời gian → tổng số phút |
| [Readiness Model](readiness-model.md) | Cò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ạn | tên + số tuần |
| Phân bổ một buổi | nhãn + số phút |
| Thứ tự ưu tiên | danh 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

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

Đường `stale_view` xử lý đúng vấn đề mà [Goal](goal-model.md#5-trigger--khi-nào-model-được-tính-lại) và [Constraint](constraint-model.md) 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

| Endpoint | Trả về |
| --- | --- |
| `GET /v1/learners/{id}/learning-plan` | Plan 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/replan` | Tí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`.

::: tip 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ệc | Trạng thái |
| --- | --- |
| Đọc capacity qua [Constraint Model](constraint-model.md) 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](../architecture/sdd-007-learner-orchestration.md) §6b; mode blend [SDD-011](../architecture/sdd-011-school-models.md) §6.
* Kiểm chứng: QG-005 (`planning.test.ts`).
* Liên quan: [Constraint](constraint-model.md) · [Goal](goal-model.md) · [Retention](retention-model.md) · [Readiness](readiness-model.md) · [Workflows](workflows.md).
