---
url: https://docs.nemo12.com/reference/context-builder.md
description: >-
  Context Builder: đường duy nhất đưa dữ liệu learner vào prompt LLM, luật cấm
  route tự SELECT rồi nhét vào messages (AS-10.3).
---

# Context Builder

> **Đây là đường duy nhất để dữ liệu của một đứa trẻ đi vào một prompt.** Không route nào được tự `SELECT` learner data rồi nhét thẳng vào `messages`.

Code: `workers/api/src/shared/context-builder.ts` · Test: `context-builder.test.ts` (12 ca) · Chuẩn: AS-10.3.1–.3.5, [SDD-003](../architecture/sdd-003-knowledge-quality.md) §13, **QG-010**

***

## 1. Trạng thái: đã xây xong, **chưa có ai gọi**

::: warning Cổng dựng trước khi có thứ để gác

```bash
grep -rn "buildLearnerContext" workers/api/src   # → chỉ thấy chính file định nghĩa
```

Hôm nay **không lời gọi AI nào mang dữ liệu learner**. Ba prompt spec đang chạy đều là nội dung thuần:

| Prompt | Nhận gì |
| --- | --- |
| `coral.generate-items` | tiêu đề node, mạch, lớp — **không learner** |
| `coral.evaluate-item` | nội dung item + node/môn/lớp — ghi rõ *"KHÔNG nhận dữ liệu learner"* |
| `coral.generate-experience` | blueprint |

Nên Context Builder chưa có việc để làm. **Đó là có chủ đích, không phải sót.**
:::

Vì sao dựng trước: `tutor_hint` và `plan_explanation` là hai tính năng chắc chắn sẽ tới, và cả hai đều cần gửi trạng thái học của một đứa trẻ cho một mô hình bên ngoài. Dựng cổng **sau** khi lời gọi đầu tiên đã chạy thì lời gọi đó sẽ không bao giờ đi qua cổng — không ai quay lại sửa thứ đang chạy tốt.

Khác với [Depth Engine](depth-engine.md) (chưa có gì): ở đây **luật đã có hiệu lực và có test**, chỉ chưa có lưu lượng.

***

## 2. Bốn `purpose` — danh sách này **là** chính sách

```
item_generation   · tutor_hint · plan_explanation · portrait_summary
```

Mỗi purpose khai **trước** đúng những mảng dữ liệu nó được thấy, kèm **lý do có thể tranh luận được**:

| Purpose | Mảng được lấy | Hạn mastery | Lý do |
| --- | --- | --- | --- |
| `item_generation` | profile, mastery | 8 | *"Soạn câu hỏi chỉ cần biết lớp và node nào đang yếu; lịch sử trả lời từng câu không làm câu hỏi tốt hơn."* |
| `tutor_hint` | + recent_evidence | 5 | *"Gợi ý phải bám đúng chỗ vừa sai — nhưng chỉ đúng/sai, không kèm câu trả lời nguyên văn."* |
| `plan_explanation` | profile, mastery, goal | 12 | *"Giải thích kế hoạch cần mục tiêu và bức tranh rộng hơn; không cần từng lần làm bài."* |
| `portrait_summary` | + retention | 12 | *"Chân dung nói về xu hướng dài hạn; vẫn không cần bằng chứng lẻ và tuyệt đối không cần tên."* |

**Thêm trường mới = sửa bảng `PURPOSE_FIELDS`**, không phải sửa lén trong một route. Đây là điểm mấu chốt của tối thiểu hoá (AS-10.3.2): quyết định "AI được thấy gì" nằm ở **một chỗ đọc được**, không rải rác trong 20 handler.

`purpose` không khai báo → **ném lỗi**, không im lặng dựng context rỗng.

***

## 3. Không định danh — hai lớp, không chỉ một

### Lớp 1: không bao giờ `SELECT` cột định danh

```sql
SELECT grade FROM learners WHERE id=?1        -- KHÔNG có display_name, user_id, birth_date
```

Learner được gọi bằng **bí danh ổn định**:

```ts
learnerRef(id) = "nemo_" + SHA256("nemo12:learner-context:" + id).slice(0, 12)
```

Ổn định giữa các lần chạy nên **nối được các run của cùng một đứa trẻ**, nhưng không quay ngược ra `learner_id` và tuyệt đối không mang tên.

### Lớp 2: cổng chặn chạy trên context **đã dựng xong**

```ts
assertNoIdentifiers(ctx, [learnerId]);   // ném ContextLeakError
```

Đi đệ quy toàn bộ object, chặn hai thứ:

* **Tên trường** trong danh sách cấm: `display_name`, `full_name`, `name`, `email`, `phone`, `address`, `dob`, `birth`, `user_id`, `learner_id`, `guardian`, `parent_name`, `school_name`, `avatar_url` — **dù nằm sâu bao nhiêu tầng**.
* **Giá trị**: chính chuỗi `learner_id` bị nhét vào một chuỗi tự do cũng bị bắt.

> Có một cổng chặn ở cuối **chứ không chỉ trông vào kỷ luật người viết.**

Đây là khác biệt giữa "quy ước" và "bảo đảm". Lớp 1 có thể hỏng vì một PR thêm cột vào câu SELECT; lớp 2 thì không.

### Điều không lấy, dù có sẵn

`recent_evidence` **không lấy `payload_json`** — trường đó chứa câu trả lời nguyên văn, mức tự tin và lý do chọn của đứa trẻ. Chỉ lấy `correct` (đúng/sai) và `reliability`.

Gợi ý cần biết em vừa sai chỗ nào; **không** cần biết em đã viết gì.

***

## 4. Cách ly learner (AS-10.3.5 🔴)

> Mọi câu SELECT bên dưới bind **đúng** `learnerId` được truyền vào. Không nhánh nào đọc learner khác, **không JOIN mở rộng sang family/lớp**.

Đây là mức nghiêm trọng cao nhất trong bộ chuẩn audit, và có **hai** test riêng:

* *"context của B không chứa một mẩu nào của A"*
* *"MỌI câu SQL đều bind đúng learner đang xin, không câu nào bỏ trống"*

Câu test thứ hai đáng chú ý: nó kiểm **từng câu SQL** có tham số bind, không chỉ kiểm kết quả cuối. Một câu quên `WHERE learner_id` sẽ trả về dữ liệu của cả bảng — và nếu learner đang test là người duy nhất trong DB test thì kết quả cuối vẫn "đúng".

***

## 5. Truy ngược — `input_ref` (AS-10.3.4)

Mỗi context được ghi thành một object R2:

```
ai-context/2026-08-16/nemo_a3f19c2b04e7/{uuid}.json
```

Khoá đó trả về thành `input_ref`, ghi vào `engine_runs.input_ref` / `workflow_runs.input_ref`. Nhờ vậy sau này trả lời được: *"lời gọi AI đó đã thấy chính xác những gì?"* — bằng cách mở đúng file, không phải dựng lại từ trí nhớ.

Đường dẫn dùng **bí danh**, không dùng `learner_id`: ngay cả cây thư mục R2 cũng không lộ ai là ai.

**Ghi hỏng thì trả `null`**, không chặn:

> *"truy vết là thứ yếu so với việc learner học được."*

Cùng nguyên tắc với [run log](workflows.md#1-hạ-tầng-run-log) và [event publish](queues.md#3-publish--không-được-làm-hỏng-nghiệp-vụ-chính).

***

## 6. `renderContext()` — một chỗ duy nhất quyết định format

```
Học sinh nemo_a3f19c2b04e7, lớp 8. Đang yếu nhất: Hằng đẳng thức (35%), Hệ số góc (42%).
10 lần trả lời gần nhất: đúng 6, sai 4. Mục tiêu: Thi vào 10 — Toán điều kiện. Cần ôn lại: …
```

Có test riêng *"renderContext không in ra learner_id thật"* — vì bước render là chỗ cuối cùng dữ liệu còn có thể rò ra dưới dạng chuỗi tự do, sau khi đã qua mọi cổng kiểu.

***

## 7. Hai lệnh tự kiểm

Ghi ngay đầu file, chạy được bất cứ lúc nào:

```bash
grep -rn "AI.run" workers/api/src | grep -v shared/prompts.ts     # → phải RỖNG
grep -rn "buildLearnerContext" workers/api/src                    # → mọi prompt có learner data
```

Lệnh thứ nhất bảo đảm **chỉ một chỗ chạm model** (`runPrompt` trong `prompts.ts`). Hôm nay nó rỗng — đúng.

***

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

1. Không route nào tự `SELECT` learner data rồi nhét vào prompt.
2. Chỉ `runPrompt()` được gọi `env.AI.run` — mọi lời gọi qua [AI Gateway](ai-registry.md).
3. Mỗi `purpose` khai trước mảng dữ liệu **và lý do**; purpose lạ thì ném lỗi.
4. Không cột định danh nào được SELECT; learner luôn là bí danh.
5. `assertNoIdentifiers` chạy **trên context đã dựng xong**, ngay trước khi trả.
6. Không JOIN sang family/lớp; mọi SQL bind đúng một learner.
7. Không lấy `payload_json` — câu trả lời nguyên văn của trẻ không rời khỏi hệ thống.
8. Ghi `input_ref` hỏng thì trả `null`, không chặn nghiệp vụ.

## 9. Kiểm chứng

12 ca test, khoá từng điều của bộ chuẩn:

| Ca | Khoá gì |
| --- | --- |
| context của B không chứa mẩu nào của A | AS-10.3.5 🔴 |
| mọi SQL bind đúng learner, không câu nào bỏ trống | AS-10.3.5 🔴 |
| mỗi purpose chỉ lấy đúng mảng đã khai · không vượt hạn mastery | AS-10.3.2 |
| bằng chứng chỉ còn đúng/sai, **không câu trả lời nguyên văn** | AS-10.3.3 |
| `learner_ref` là bí danh ổn định, không phải `learner_id` | AS-10.3.3 |
| **query lỡ trả về `learner_id` thì build PHẢI ném lỗi**, không âm thầm gửi đi | AS-10.3.3 |
| chặn trường tên/email **dù nằm sâu bên trong** · chặn cả khi nhét vào chuỗi tự do | AS-10.3.3 |
| có `input_ref` khi bật persist, snapshot ghi đúng nội dung | AS-10.3.4 |

Gate: **QG-010**.

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

| Việc | Trạng thái |
| --- | --- |
| Lời gọi AI đầu tiên **thật sự** dùng learner context (`tutor_hint`) | ⏳ chưa có luồng — xem [Learning Engine §8](learning-engine.md#8-khoảng-trống-lớn-nhất-chưa-có-difficulty-controller) |
| `plan_explanation` nối vào [Learning Plan](learning-plan-model.md) | ⏳ plan hiện giải thích bằng template tất định, chưa cần AI |
| Vòng đời của object R2 `ai-context/` (xoá sau bao lâu) | ⚠️ chưa khai — dữ liệu trẻ em cần chính sách lưu giữ |
| Kiểm `assertNoIdentifiers` chạy trong CI như một lint | ⏳ hiện chỉ chạy lúc runtime + test |

Dòng thứ ba đáng làm sớm: snapshot context là dữ liệu về trẻ em nằm trong R2 **không có hạn xoá**. Cổng chặn định danh làm cho nó ít nhạy cảm hơn nhiều, nhưng "ít nhạy cảm" không phải "không cần chính sách".

## Trace

* QG-010 (AI governance), QG-008 (privacy) · AS-10.3.1–.3.5.
* Nguồn: SRC-105 · Audit #001.
* Thiết kế: [SDD-003](../architecture/sdd-003-knowledge-quality.md) §13.
* Liên quan: [AI Registry](ai-registry.md) · [Quality Engine](quality-engine.md) · [Permissions](permissions.md) · [Config](config.md).
