Skip to content

SDD-027 · ItemForge, hai mắt xích cuối và sổ chi tiêu model ​

Một phần của SDD-027.

17. ItemForge — sinh kho câu hỏi (WF-20, SRC-638) ​

Chỉ đạo chủ dự án 2026-08-28: dựng workflow sinh 16 câu mỗi bài, kèm nhận định "luôn có rất nhiều ràng buộc để sinh ra các câu đó; càng nhiều ràng buộc thì có thể càng khó tạo ra."

Nhận định ấy có số liệu của chính dự án đứng sau: SRC-398 ghi llama-3.3 sinh MCQ tiếng Việt sai khoảng 50% (1.317 câu phải rà, 542 câu viết lại) khi được hỏi cả chùm câu một lượt. Và ngay trong ngày dựng hệ này, prompt v2 của LessonForge đã hỏng vì cách diễn đạt ràng buộc: chú thích số lượng đặt trong giá trị trường (action: "string (mảng ít nhất 3 mục)") làm model hiểu chính trường ấy là mảng, ba trường cùng sai một kiểu. Càng dồn ràng buộc vào một chỗ, càng dễ hiểu nhầm ràng buộc đang nói về cái gì.

17.1 Bốn cách hạ áp lực ràng buộc ​

CáchLàm gìBớt được ràng buộc nào cho model
Kế hoạch khe (itemSlots.ts)Chốt trước 16 khe, mỗi khe một vai + một hiểu lầm + một mức khóPhủ hết hiểu lầm · đủ bốn vai · rải độ khó — ba ràng buộc khó nhất, do code bảo đảm
Một khe một lượtMỗi lượt gọi model sinh đúng MỘT câuTừ ~11 ràng buộc cùng lúc xuống ~4
Code gánh phần code gánh đượcrotateAnswer xoay vị trí đáp án, độ khó gán theo kheLuật "đáp án không dồn quá 40% một ô" không bao giờ trượt, và không nhắc trong prompt
Hình dạng tách khỏi số lượngKhuôn JSON chỉ nói kiểu; ràng buộc liệt kê riêng bên dướiĐúng lỗi prompt v2 đã trả giá

17.2 Sổ đăng ký ràng buộc (itemRules.ts) ​

Mười một ràng buộc, mỗi cái là mã chạy được kèm lời sửa gửi thẳng cho model — không phải lời dặn trong prompt, vì lời dặn thì model bỏ qua được còn hàm kiểm thì không.

Cứng (trượt là trả lại): R1 đủ 4 phương án · R2 không phương án rỗng · R3 không trùng nhau · R4 đáp án hợp lệ · R5 đề đủ dài · R6 không markdown thô · R7 nhiễu phải gắn một hiểu lầm ĐÃ KHAI của bài · R8 không trùng đề đã có · R9 không gạch dài. Mềm (ghi lại, vẫn nhận): R10 các phương án dài xấp xỉ nhau (chặn mẹo đoán theo độ dài) · R11 cấm "tất cả đều đúng".

Bộ này cố ý trùng khớp với screening.ts bên api ở phần cứng: lệch nhau thì xưởng cho qua thứ mà cổng nạp chặn, tức sinh ra để vứt đi.

17.3 Đo áp lực ràng buộc — câu trả lời cho chỉ đạo ​

measurePressure() đếm mỗi mã ràng buộc trượt bao nhiêu lượt, ghi vào cột content_runs.constraint_pressure (migration 0171). Nhờ vậy một bài tắc không còn là "sinh mãi không ra" mà thành "R7 chặn 7/10 lượt" — một con số để người quyết định nới ràng buộc nào, hay sửa prompt ở chỗ nào. Không đo được thì "càng nhiều ràng buộc càng khó tạo" mãi là một cảm giác, không thành một quyết định.

17.4 Ranh giới ​

Câu sinh ra nằm ở content_item_drafts (0171), không vào thẳng items — cùng luật §1 và xưởng. items còn có trigger chặn publish khi thiếu quality_json (0067), nên một bảng riêng giữ ranh giới rõ: đây là bản thảo, chưa phải câu hỏi.

Cầu nối để câu hỏi bám vào bài: migration 0170 cho mỗi curriculum_lesson một skill_node (id = lesson id, objective_vi = Guiding Question). Đây không phải thiết kế mới mà là thi hành chuẩn có sẵn — CD-8 thành phần 2 đã liệt kê "Knowledge node" là phần bắt buộc của Lesson, CD-4.3 đã chỉ đúng cột objective_vi; chỉ là chưa ai dựng cầu, nên trước đó điều kiện live của CD-5 không đo được. Mọi giá trị đều suy ra từ dữ liệu đã có, không phải người gõ vào.

17.5 Vòng chấm câu hỏi — chỗ ràng buộc máy hết tác dụng (SRC-640) ​

Bài học đắt nhất của cả đợt. Mười sáu câu ItemForge sinh ra ngày 2026-08-28 thoả gần hết ràng buộc máy (chỉ một ràng buộc MỀM trượt 2 lượt), mà đọc kỹ vẫn thấy:

  • đáp án SAI về mặt toán học: "đếm từ 1 đến 20, sau đó đếm tiếp từ 20 đến tổng số" — đếm trùng số 20;
  • đề ra NGOÀI phạm vi bài: bài "Đếm đến 100" bậc 1 mà hỏi "hơn 100 học sinh".

Không một ràng buộc đếm-được nào bắt được hai lỗi ấy. Nghĩa là nhận định "càng nhiều ràng buộc càng khó tạo" đúng, nhưng chưa đủ: cái khó thật nằm ở chỗ những ràng buộc quyết định nhất không đo được bằng hàm thuần. Chúng cần một người đọc — hoặc một model đóng vai người đọc.

Thành phầnCách làm
Rubric8 chiều CC-QAF-1.0, chép đúng trọng số của quality.ts bên api. Hai thước khác nhau cho cùng một loại nội dung thì sớm muộn cho hai kết luận khác nhau
Evidence-gatedChấm mà không trích được bằng chứng từ chính câu hỏi thì điểm chiều ấy về 0. Không có luật này, model chấm mọi thứ 0.9 kèm lời khen chung chung
Hai góc nhìnteacher (đáp án đúng chưa, có trong phạm vi bài không) và struggling_learner (có mẹo nào đoán trúng mà không cần hiểu bài không) — góc thứ hai bắt đúng loại lỗi mà người soi chuẩn xác hay bỏ qua
Bắt TỰ GIẢIPrompt chấm buộc model giải câu hỏi trước khi cho điểm accuracy; đáp án đánh dấu đúng mà thật ra sai thì accuracy = 0
Chiều thiếuTính là 0, không bỏ qua — bỏ qua thì né chiều khó lại được điểm cao hơn
Ngưỡng≥0,72 tổng · ≥0,5 từng chiều · ≥2 evaluator — trùng đúng cổng nạp bên api, lệch là sinh ra để vứt đi

Kết quả ghi vào content_item_drafts.quality_json / gate_ok / gate_reasons (migration 0172), nên người rà đọc được bằng chứng chứ không chỉ con số.

18. Hai mắt xích cuối: câu hỏi vào items, và điều kiện live (SRC-641) ​

Trước mục này chuỗi đứt ở hai chỗ, và vì thế không khoá nào có thể live được dù sinh bao nhiêu nội dung:

Chỗ đứtHệ quả
Câu hỏi ở content_item_drafts, learner làm bài trên itemsKho câu sinh ra không ai làm được
Điều kiện CD-5 ("mọi bài ≥16 câu published") không mã nào kiểm, không mã nào đổi statusMọi khoá đứng mãi ở draft — đúng chữ "Đang soạn" learner thấy

POST /items/promote nạp câu từ bản thảo vào items ở status candidate, không phải published. Hai lý do: luật §1 (người duyệt mới đưa nội dung tới learner), và items có trigger chặn publish khi thiếu quality_json (0067) — nạp thẳng published sẽ bị DB chặn, và DB đang làm đúng. Từ candidate, đường publish sẵn có bên api lo phần còn lại với đúng bộ ngưỡng của nó. Câu chưa qua cổng chất lượng (§17.5) thì không được nạp, và hàm nói ra lý do từng câu.

GET|POST /courses/{id}/live-check kiểm CD-5: GET chỉ xem còn thiếu gì (bài nào thiếu bao nhiêu câu), POST nâng draft → live nếu đủ. Không hạ ngưỡng bao giờ — thiếu thì giữ nguyên status và trả về khoảng cách, vì một khoá live mà bài chưa đủ câu thì learner mở ra gặp bài trống.

19. Sổ chi tiêu từng lượt gọi model (SRC-1225, 04.10.2026) ​

Trần chi phí ngày (FOUNDRY_DAILY_USD_CAP) cộng content_run_trials và content_item_drafts, nhưng hai bảng ấy chỉ giữ chi phí của LẦN THỬ CUỐI mỗi step: Workflows retry một step hỏng tới hai lần và chi phí các lần trước mất theo exception (audit 03.10.2026). Một model trượt liên tục bị tính tiền ba lần mà sổ ghi một.

  • Bảng foundry_spend_ledger (migration 0329): mỗi lượt gọi tốn tiền một dòng (run_id, model_key, step, usd, ok, at), ghi ngay trong callModel (workers/foundry/src/models.ts), tức bên trong callback của step: mỗi lần thử một dòng, step phát lại từ cache thì không ghi trùng.
  • Trần ở cả ba chỗ (chốt của startRun, câu INSERT có điều kiện, /items/runs) lấy MAX(cách cộng cũ, tổng sổ theo giờ gọi thật trong 24 giờ). Cách cũ giữ cho giai đoạn chuyển tiếp, khi sổ chưa có dữ liệu của hôm trước.
  • Ghi sổ hỏng (bảng chưa có vì code lên trước migration) thì bỏ qua: sổ để đếm tiền, không được làm đổ lượt sinh. Test: models.test.ts (ba lượt gọi hỏng là ba dòng, ok=0).