---
url: https://docs.nemo12.com/reference/quality-engine.md
description: >-
  Quality Engine: cổng cuối kiểm câu hỏi có xứng đáng trước khi tới tay trẻ,
  thuật toán và cách dùng (SDD-003 §6-§10).
---

# Quality Engine

> Cổng cuối cùng trước khi một câu hỏi tới tay đứa trẻ. [Assessment Engine](assessment-engine.md) lo *"đo có đáng tin không"*; engine này lo *"cái đem ra đo có xứng đáng không"*.

Thiết kế: [SDD-003](../architecture/sdd-003-knowledge-quality.md) §6–§10 · Code: `modules/content/quality.ts` · `screening.ts` · `lifecycle.ts` · `review-queue.ts` · Test: `quality.test.ts` (22 ca) · Rubric: **CC-QAF-1.0** (kế thừa chuyenchon)

***

## 1. Bốn lớp, không lớp nào thay được lớp nào

```
1. Sàng lọc máy      screening.ts    12 luật cấu trúc, tất định, rẻ
2. Chấm rubric       quality.ts      8 chiều × ≥2 evaluator độc lập, mỗi chiều kèm bằng chứng
3. Cổng publish      quality.ts      6 điều kiện SỐ, trả lý do bằng tiếng người
4. Vòng phản hồi     review-queue.ts learner báo sai → hàng đợi người soát → item lùi khỏi published
```

Lớp 1 bắt lỗi **hình thức** (đáp án ngoài phạm vi, lựa chọn trùng). Lớp 2 bắt lỗi **nội dung** (sai kiến thức, đoán được đáp án mà không cần biết bài). Lớp 3 quyết định. Lớp 4 sửa những gì ba lớp trên bỏ sót — vì sẽ luôn có.

***

## 2. Lớp 1 — Sàng lọc máy (12 luật)

**Chặn (blocking) — 9 luật.** Sai cấu trúc thì không có điểm số nào cứu được:

| Mã | Nghĩa |
| --- | --- |
| `options_unparsable` | `options_json` không parse được |
| `options_too_few` | ít hơn 2 lựa chọn |
| `options_duplicate` | hai lựa chọn trùng nhau |
| `option_empty` | có lựa chọn rỗng — trên màn hình là **một nút trắng không đọc được** |
| `correct_index_out_of_range` | `correct_index` ngoài phạm vi |
| `prompt_empty` / `prompt_too_short` | đề bài rỗng hoặc dưới 10 ký tự |
| `prompt_duplicate_conflicting` | trùng đề **và** trùng lựa chọn nhưng **khác đáp án đúng** → chắc chắn một bản sai |
| `node_missing` | `node_id` trỏ tới skill node không tồn tại → không biết dạy/đo kỹ năng nào |

**Cảnh báo (warn) — 3 luật:** `prompt_duplicate`, `difficulty_out_of_range`, `discriminator_missing_misconception`.

### Ba cái bẫy đã bịt (có test riêng)

| Bẫy | Luật đúng |
| --- | --- |
| Lựa chọn chỉ khác hoa/thường | **Không** phải trùng — chính chữ hoa có thể là nội dung được hỏi (chính tả, tên riêng) |
| Cùng đề, **khác** bộ lựa chọn | Là **họ câu biến thể hợp lệ** → chỉ `warn` |
| Cùng đề, **cùng** lựa chọn, khác đáp án | **Blocking** — chắc chắn một bản sai |

Ba bẫy này là lý do sàng lọc phải là code có test, không phải một câu prompt gửi cho LLM.

***

## 3. Lớp 2 — Rubric CC-QAF, 8 chiều

Đúng 8 chiều theo SDD-003 §6 — **không thêm, không bớt, không đổi tên khoá**:

| Chiều | Nhãn | Trọng số |
| --- | --- | --- |
| `accuracy` | Chính xác | **3** |
| `assessment_validity` | Giá trị đo lường | **2** |
| `clarity` | Rõ ràng | 1.5 |
| `pedagogy` | Sư phạm | 1.5 |
| `alignment` | Bám chương trình | 1 |
| `cognitive_demand` | Mức tư duy | 1 |
| `accessibility` | Tiếp cận được | 1 |
| `engagement` | Cuốn hút | 0.5 |

Trọng số nói lên thứ tự giá trị: **sai kiến thức hoặc không đo được gì thì mọi chiều khác vô nghĩa.** Một câu cuốn hút mà sai kiến thức là câu tệ hơn một câu khô khan mà đúng.

### Evidence-gated scoring — luật gốc của rubric

```ts
score > 0 && evidence.trim() === ""
  → score = 0, reason += "· điểm bị hạ về 0: chấm không kèm evidence"
```

> Điểm trần không lý do là cách nhanh nhất để "đạt chuẩn" mà nội dung vẫn sai.

Mỗi `DimensionScore` bắt buộc có `reason` (vì sao chấm mức đó) **và** `evidence` (trích từ chính item). Ví dụ evaluator tất định chấm `pedagogy`:

```
score: 0.9
reason: "Có ghi lỗi sai học sinh hay mắc → sai còn học được điều gì đó"
evidence: 'misconception: "Nhầm dấu khi chuyển vế"'
```

### Hai loại evaluator

**Evaluator 1 — tất định** (`rule-engine@1`). Không gọi AI, chạy được mọi lúc, **kết quả tái lập được**. Suy điểm từ những gì máy đo được: độ dài đề bài, có misconception không, `role`/`difficulty`, chênh lệch độ dài các lựa chọn.

Một luật đáng chú ý trong `assessment_validity`:

```
chênh lệch độ dài lựa chọn > 60 ký tự → 0.55
"Đáp án đúng dài hơn hẳn các phương án nhiễu — learner đoán được mà không cần biết bài"
```

**Evaluator 2 — ngữ nghĩa (LLM)**, qua [AI Gateway](ai-registry.md). Đây là chỗ tinh tế nhất:

> **Chấm hai lần cùng một câu hỏi thì chỉ là cùng một ý kiến nói to hơn, không phải multi-evaluator.**

Nên hai lượt chấm dùng **hai góc nhìn khác nhau** (AS-10.4.2):

| Góc nhìn | Soi gì |
| --- | --- |
| `teacher` — giáo viên chấm đề | Đúng sai kiến thức, chất lượng phương án nhiễu, câu có đo đúng kỹ năng cần đo |
| `struggling_learner` — học sinh yếu đọc thử | Có hiểu được không, có **mẹo đoán đáp án mà không cần biết bài** không, chỗ nào gây rối |

Góc nhìn thứ hai bắt được loại lỗi mà giáo viên hay bỏ sót: câu đúng về kiến thức nhưng đoán được.

SDD-003 §7.3 **cấm hỏi chung "câu này có tốt không"** — phải hỏi từng chiều kèm bằng chứng. Output AI qua zod schema trước khi vào `profileFromRubricScores()`.

***

## 4. Lớp 3 — Cổng publish, 6 điều kiện bằng số

Ngưỡng khai tập trung, đổi thì đổi một chỗ (AS-06.3.3):

```
PUBLISH_MIN_QUALITY_SCORE      0.72     điểm tổng hợp
PUBLISH_MIN_DIMENSION_SCORE    0.50     không chiều nào được thủng sàn
PUBLISH_MIN_EVALUATORS         2        lượt đánh giá ĐỘC LẬP
PUBLISH_MAX_EVALUATOR_SPREAD   0.25     chênh điểm tổng giữa các evaluator
PUBLISH_MAX_DIMENSION_SPREAD   0.40     chênh trên MỘT chiều
REVIEW_QUEUE_QUALITY_SCORE     0.55     dưới mức này đẩy thẳng vào hàng đợi người
```

Sáu điều kiện, thiếu bất kỳ cái nào là không qua:

1. Sàng lọc máy không còn lỗi chặn — **cờ chặn thắng mọi điểm số**
2. Đủ ≥2 evaluator độc lập
3. Điểm tổng ≥ 0.72
4. Các evaluator không lệch nhau quá 0.25 — **lệch nhiều nghĩa là cần người soát, không phải lấy trung bình**
5. Không chiều nào lệch quá 0.40 giữa các evaluator
6. Mỗi evaluator chấm **đủ 8 chiều**; và trung bình **từng chiều** ≥ 0.5

Điều kiện 6 bịt một lỗ thật: **điểm tổng cao vẫn có thể che một chiều bằng 0.** Câu sai kiến thức nhưng rõ ràng, cuốn hút, dễ đọc vẫn có thể vượt 0.72 nếu chỉ nhìn điểm tổng.

### Trả lý do, không trả true/false

```jsonc
{
  "ok": false,
  "reasons": [
    "Mới có 1 lượt đánh giá, cần tối thiểu 2 lượt độc lập",
    "Chiều \"Giá trị đo lường\" chỉ 0.4 < sàn 0.5"
  ]
}
```

Coral hiện thẳng những câu này cho người soạn. Một cổng chỉ nói "không được" mà không nói vì sao thì người soạn chỉ có thể đoán.

***

## 5. Vòng đời nội dung — không ai sửa thẳng vào `items`

```
candidate  → đang chờ đủ điểm + đủ evaluator
published  → bản đang nằm trong `items`, đang phục vụ learner
superseded → từng published, bị version mới thay
rejected   → người soát bác bỏ
```

**Luật gốc:** bảng `items` là **bản đang phục vụ learner**, không ai sửa thẳng vào đó. Mọi thay đổi tạo một dòng mới trong `item_versions` ở trạng thái `candidate`; **publish là hành động sao chép** version đã đạt ngưỡng sang `items`; **rollback là publish lại một version cũ**.

Nhờ vậy luôn trả lời được: *nội dung này version mấy, ai duyệt, lúc nào, dựa trên hồ sơ chất lượng nào.*

SDD-003 §10 thiết kế 7 trạng thái (Draft → Review → Candidate → Evaluated → Approved → Published → Deprecated); schema 0032 rút về **4** — đủ để phân biệt những gì hệ thống thật sự cần hành xử khác nhau.

***

## 6. Lớp 4 — Vòng phản hồi từ learner

> *"Learner báo sai thì đi về đâu"* — audit #001 chấm trượt đúng vì báo cáo rơi vào hư vô.

Hai nguồn đổ vào `content_review_queue`:

| Nguồn | Từ đâu |
| --- | --- |
| Người báo | `content_reports` (forum/interaction) + nút báo sai trong app learn |
| Máy sàng lọc | `scripts/screen-items.mjs` + `screening.ts` trong luồng sinh nội dung |

**Một target chỉ nằm một dòng khi còn mở**: 5 learner báo cùng một câu thì người soát thấy **một** việc, không phải năm việc. Lý do mới được nối thêm vào dòng đang mở.

Trạng thái: `open` → `in_review` → `resolved` / `dismissed`.

Hệ quả tức thì khi một item bị gắn cờ: nó **lùi khỏi `published`** và [không tới tay đứa trẻ tiếp theo](assessment-engine.md#5-ba-tầng-chống-rác-đầu-vào). Đứa trẻ **đang** làm dở thì vẫn làm hết bài, nhưng câu đó không ghi evidence ([Learning §5](learning-engine.md#câu-bị-gỡ-giữa-phiên)).

### Spot-check bởi người — 20%, chọn bằng hash

```ts
HUMAN_SPOT_CHECK_RATE = 0.2
isSpotCheckSelected(key) → FNV-1a hash % 1000 / 1000 < rate
```

Chọn bằng hash nên **tái lập được**: cùng một item luôn cho cùng kết luận, và đếm lại được "đã spot-check bao nhiêu %" từ chính id.

> Máy chấm đạt ngưỡng **vẫn không thay được** việc người đọc thử một phần nội dung (AS-10.4.3).

***

## 7. Cách DÙNG

| Nơi | Endpoint |
| --- | --- |
| Sinh item có chấm chất lượng | `POST /v1/coral/generate-items` |
| Sinh cả bộ theo blueprint | `POST /v1/coral/blueprints/{id}/generate` |
| Hàng đợi rà soát | `GET /v1/coral/review-queue` · `PATCH /v1/coral/review-queue/{id}` |
| Xem prompt đang dùng | `GET /v1/coral/prompts` |

Giao diện: `coral.nemo12.com`. Rate limit `ai-generate`: 30 lượt/giờ/user ([config §4](config.md)).

`quality_json` lưu **nguyên từng lượt chấm**, không nuốt evidence — để sau này trả lời được *"ai chấm, dựa vào cái gì"*.

***

## 8. Bất biến — vi phạm là bug

1. Điểm > 0 mà **không có evidence** → hạ về 0, ghi lý do.
2. Đủ **8 chiều** hoặc không tính là hồ sơ hoàn chỉnh.
3. **≥2 evaluator độc lập**, và phải **khác góc nhìn**.
4. Cờ chặn của sàng lọc máy **thắng mọi điểm số**.
5. Evaluator lệch nhau nhiều → **chặn**, không lấy trung bình cho qua.
6. Trung bình từng chiều phải ≥ sàn — điểm tổng cao không che được một chiều 0.
7. **Không ai sửa thẳng vào `items`** — luôn qua `item_versions`.
8. Cổng publish trả **lý do**, không trả true/false trần.
9. Báo cáo của learner **không được rơi vào hư vô**.

## 9. Kiểm chứng

`quality.test.ts` — 22 ca, gồm ba ca "bẫy" ở §2 và những ca khoá cổng publish:

* evaluator tất định chấm **đủ 8 chiều**, đúng rubric version
* mỗi chiều kèm evidence + reason, **không chỉ điểm số trần**
* chấm điểm dương mà evidence rỗng → **hạ về 0**
* `accuracy` được cân nặng nhất — sai kiến thức thì mọi chiều khác không cứu nổi
* **một evaluator là chưa đủ, dù điểm cao**
* hai evaluator lệch nhau quá nhiều thì **chặn dù điểm trung bình đẹp**
* thiếu một chiều là hồ sơ chưa hoàn chỉnh → chặn
* cờ chặn của sàng lọc máy **thắng mọi điểm số**

Gate: **QG-006**.

## 10. Khoảng trống đã biết

| Việc | Trạng thái |
| --- | --- |
| Evaluator `learner_evidence` — dùng kết quả làm bài thật để chấm lại chất lượng câu | ⚠️ kiểu đã khai trong `evaluator_kind`, **chưa có luồng nào sinh** |
| Evaluator `graph` — kiểm tính nhất quán với skill graph | ⚠️ như trên |
| Kho câu mỏng (trung bình 8,5 câu/bài) | ⚠️ gốc là thiếu nội dung, không phải lỗi engine |
| Spot-check 20% có bảng theo dõi "đã soát bao nhiêu" | ⏳ hàm chọn đã có, chưa có báo cáo độ phủ |
| Quality profile cho **lab** và **đề thi** | ⏳ hiện rubric chạy trên `items` |

Dòng đầu đáng chú ý nhất: `learner_evidence` là evaluator **mạnh nhất có thể có** — 200 đứa trẻ làm một câu nói lên nhiều hơn hai lượt chấm. Dữ liệu đã có trong `assessment_responses`; chưa có luồng nối ngược về quality profile.

## Trace

* REQ-KNW-07/08/09 (quality profile, multi-evaluator, lifecycle).
* Nguồn: SRC-004 (Knowledge & Quality), SRC-105 · Audit #001 (AS-06.2.x, AS-06.3.x, AS-10.4.x).
* Thiết kế: [SDD-003](../architecture/sdd-003-knowledge-quality.md) §6/§7/§8/§10/§13, [SDD-013](../architecture/sdd-013-coral-content-plane.md).
* Kiểm chứng: **QG-006**; AI governance: [QG-010](ai-registry.md).
* Liên quan: [Assessment Engine](assessment-engine.md) · [Learning Engine](learning-engine.md) · [AI Registry](ai-registry.md) · [State Machines §9](state-machines.md).
