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ự
SELECTlearner data rồi nhét thẳng vàomessages.
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 §13, QG-010
1. Trạng thái: đã xây xong, chưa có ai gọi
Cổng dựng trước khi có thứ để gác
grep -rn "buildLearnerContext" workers/api/src # → chỉ thấy chính file định nghĩaHô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 (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_summaryMỗ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
SELECT grade FROM learners WHERE id=?1 -- KHÔNG có display_name, user_id, birth_dateLearner được gọi bằng bí danh ổn định:
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
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_idbị 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}.jsonKhoá đó 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 và event publish.
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:
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 dataLệ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
- Không route nào tự
SELECTlearner data rồi nhét vào prompt. - Chỉ
runPrompt()được gọienv.AI.run— mọi lời gọi qua AI Gateway. - Mỗi
purposekhai trước mảng dữ liệu và lý do; purpose lạ thì ném lỗi. - Không cột định danh nào được SELECT; learner luôn là bí danh.
assertNoIdentifierschạy trên context đã dựng xong, ngay trước khi trả.- Không JOIN sang family/lớp; mọi SQL bind đúng một learner.
- 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. - Ghi
input_refhỏ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 |
plan_explanation nối vào Learning Plan | ⏳ 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 §13.
- Liên quan: AI Registry · Quality Engine · Permissions · Config.