Skip to content

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 §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

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:

PromptNhận gì
coral.generate-itemstiêu đề node, mạch, lớp — không learner
coral.evaluate-itemnội dung item + node/môn/lớp — ghi rõ "KHÔNG nhận dữ liệu learner"
coral.generate-experienceblueprint

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_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:

PurposeMảng được lấyHạn masteryLý do
item_generationprofile, mastery8"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_evidence5"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_explanationprofile, mastery, goal12"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+ retention12"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 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:

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.
  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:

CaKhoá gì
context của B không chứa mẩu nào của AAS-10.3.5 🔴
mọi SQL bind đúng learner, không câu nào bỏ trốngAS-10.3.5 🔴
mỗi purpose chỉ lấy đúng mảng đã khai · không vượt hạn masteryAS-10.3.2
bằng chứng chỉ còn đúng/sai, không câu trả lời nguyên vănAS-10.3.3
learner_ref là bí danh ổn định, không phải learner_idAS-10.3.3
query lỡ trả về learner_id thì build PHẢI ném lỗi, không âm thầm gửi điAS-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ự doAS-10.3.3
có input_ref khi bật persist, snapshot ghi đúng nội dungAS-10.3.4

Gate: QG-010.

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

ViệcTrạ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 ​