# 92% đúng là 8% mất khách: chỗ sửa không nằm ở model, mà ở cái khuôn mình đặt quanh nó
Có một tuần mình ngồi xem log và mọi thứ đều đẹp.
Chuỗi tự động hoá chạy đủ các bước. Không có dòng đỏ nào. Mỗi mục việc ra khỏi hệ đều mang trạng thái "xong". Mình còn mở dashboard ra ngắm, kiểu tự thưởng.
Rồi mình mở đúng ba kết quả ra đọc bằng mắt người.
Một cái sai tên trường. Một cái thiếu nửa tham số. Một cái gửi vào hệ thống ngoài với một giá trị nằm ngoài danh sách cho phép, nên hệ kia nhận rồi làm chuyện khác với ý mình.
Ba trên ba mươi. Mình tưởng mình gặp vận may, nên rà cả tháng: khoảng 8 trên 100 lượt gọi tool trả về cấu trúc lệch. Không sập. Không cảnh báo. Chỉ lệch.
Con số đó nghe nhỏ. Nó không nhỏ.
Tám phần trăm không nằm rải đều, và đó là lý do nó nguy hiểm
Nếu 8% rơi ngẫu nhiên, mình sẽ thấy nó ngay: mở mười kết quả là bắt được một cái sai. Nhưng lỗi kiểu này không rơi ngẫu nhiên. Nó tụ lại ở đúng ba chỗ:
- ▹Tham số lồng nhau. Một tầng thì model viết đúng. Ba tầng object lồng nhau thì bắt đầu trượt — nó quên một ngoặc, hoặc đặt danh sách ở chỗ đáng lẽ là object.
- ▹Trường tuỳ chọn. Chỗ nào schema cho phép "có hoặc không" là chỗ model tự quyết định có, và tự quyết định luôn kiểu dữ liệu.
- ▹Giá trị có danh sách cho phép. Enum, mã trạng thái, mã quốc gia, tên kênh. Model không bịa số, nhưng rất hay viết một biến thể gần đúng — viết hoa, thêm hậu tố, dịch sang tiếng Anh.
Ba chỗ đó có một điểm chung: chúng là chỗ mình không kiểm tra bằng mắt trong lúc chạy thử. Lúc thử, mình nhìn kết quả cuối. Kết quả cuối vẫn ra một bài đọc được, vì phần lớn nội dung đúng. Cái sai nằm ở cái phong bì, không nằm ở lá thư.
Và đây là phép nhân mình phải viết lên giấy mới thấy: một workflow năm bước, mỗi bước 92% đúng, thì xác suất cả năm bước đúng là 0,92⁵ ≈ 66%. Tức là cứ ba lần chạy thì có một lần hỏng ở đâu đó. Mình không có hệ 92%. Mình có hệ 66%, và mình đang quảng cáo nó là "tự động".
Tệ hơn: khách không mua 92%. Khách mua "chạy một nghìn lần không phải sửa tay". Từ 92% lên 100% không phải cải tiến 8%. Nó là ranh giới giữa một dịch vụ bán được và một dịch vụ phải đi xin lỗi.
Mình đã sửa sai chỗ suốt mấy tháng: dặn nó, thay vì ép nó
Phản xạ đầu tiên của mình — và mình đoán của gần như tất cả mọi người — là viết prompt chặt hơn.
"CHỈ trả về JSON hợp lệ." "KHÔNG thêm lời giải thích." "Trường status phải là một trong các giá trị sau." Viết hoa. Thêm ví dụ. Thêm mẫu. Thêm dòng "đây là lỗi nghiêm trọng nếu sai".
Nó đỡ hơn. Từ 8% xuống 5%. Rồi mình thêm ba lớp nhắc nữa, xuống 4%, và mình bắt đầu thấy mình đang làm một việc rất quen: thương lượng với một hệ thống xác suất bằng lời khuyên.
Đây là chỗ mình phải đổi cách nghĩ, và nó là ý chính của bài này:
Một model sinh ra văn bản bằng cách chọn từng token tiếp theo theo xác suất. Khi mình viết "hãy trả về JSON đúng schema", mình đang thêm trọng số cho những token dẫn tới JSON đúng. Mình đang nghiêng bàn cân. Mình không hề cấm token sai.
Còn có một tầng khác, nằm dưới prompt: tầng chọn token. Ở đó mình có thể làm một việc khác hẳn — đặt một cái khuôn lên không gian lựa chọn. Ở mỗi vị trí, chỉ những token nào còn hợp lệ với cấu trúc đang viết dở mới được phép xuất hiện. Viết tới chỗ cần dấu hai chấm thì chỉ dấu hai chấm được chọn. Viết tới chỗ cần một giá trị trong danh sách thì chỉ các giá trị trong danh sách được chọn.
Kỹ thuật này có nhiều tên (constrained decoding, structured output, ép theo ngữ pháp). Tên không quan trọng. Cái quan trọng là tính chất: nó không làm tăng xác suất đúng. Nó làm cho việc sai trở thành không thể biểu diễn.
Khác biệt giữa hai cách, viết cho gọn:
# pseudo-code, không phải nguồn triển khai
# cách cũ: khuyên
output = model(prompt + "hãy trả về JSON đúng schema sau ...")
if not valid(output): retry() # vẫn có thể sai, vẫn có thể sai mãi
# cách mới: khuôn
grammar = compile(schema) # mọi đầu ra hợp lệ được sinh ra từ đây
output = model(prompt, grammar=grammar)
# không cần retry vì "sai cấu trúc" không còn là một khả năngLần đầu mình chạy cái này, cảm giác rất lạ. Không phải "hay quá nó đúng rồi". Mà là: hoá ra bao lâu nay mình đang trả tiền cho một lớp lỗi mà mình kiểm soát được 100%. Mình đã đổ lỗi cho model yếu, cho prompt chưa đủ hay, cho việc chọn model rẻ. Trong khi cái lỗi đó nằm ở chỗ mình để ngỏ một khoảng không gian mà lẽ ra phải đóng lại từ đầu.
Và chỗ này nối thẳng vào chuyện tiền: khi agent chạy local trên máy nhỏ đang thành mốt (model open-weight vừa 8GB RAM, chạy offline, không tốn token), người ta thường kết luận "model nhỏ thì không làm được việc thật". Sai. Model nhỏ làm được, nếu mình không bắt nó vừa nghĩ vừa tự bịa cái phong bì. Model nhỏ + khuôn cứng thường thắng model lớn + prompt khéo, vì cái thứ đang thiếu không phải trí tuệ, mà là tính kỷ luật — và kỷ luật thì không cần tham số.
Nhưng cái khuôn cũng có giá, và giá đó trả bằng thứ đắt hơn tiền
Mình không muốn bán cái này như một viên đạn bạc, vì mình đã tự bắn vào chân mình hai lần.
Lần một: ép quá tay. Mình ép cả bước suy luận vào schema, tức là bắt model vừa phải nghĩ vừa phải nhét suy nghĩ vào các ô có sẵn. Kết quả: chất lượng phân tích tụt rõ. Không phải vì nó ngu đi, mà vì mình đã cắt mất chỗ để nó nghĩ. Suy luận cần chỗ rộng; khuôn thì hẹp. Hai thứ đó phải để ở hai tầng khác nhau: tầng nghĩ thì tự do, tầng giao thì cứng. Trộn lại là hỏng cả hai.
Lần hai — và cái này nghiêm trọng hơn: mình ép mất cái cửa "tôi không biết".
Đây là bài học mình muốn anh em đọc kỹ nhất. Khi mình đóng không gian đầu ra lại, mình không chỉ cấm cấu trúc sai. Mình cấm luôn mọi câu trả lời không nằm trong khuôn — trong đó có câu "trường này tôi không đọc ra được".
Model vẫn phải trả về một object đúng schema. Nó không có cửa nào để nói "thiếu". Nên nó làm đúng cái việc mà mọi hệ thống bị bịt đường thoát đều làm: điền một giá trị hợp lệ về hình thức.
Mình đã có một tuần mà mọi object đều đúng schema, và trong đó có những object mà một trường được điền bằng một giá trị hoàn toàn bịa — tròn trịa, đúng kiểu dữ liệu, đúng danh sách cho phép. Cái khuôn đã biến "tôi không biết" thành "tôi đoán một cái nghe hợp lý". Về mặt kỹ thuật, hệ mình đạt 100%. Về mặt thực tế, nó tệ hơn 92%, vì lỗi cũ thì mình thấy, lỗi mới thì mình không thấy.
Nên schema nào cũng phải có một cái cửa, và cái cửa đó phải là một phần của cấu trúc, không phải một ngoại lệ:
# pseudo-code, không phải nguồn triển khai
{
"ket_qua": {...} | null,
"do_tin": "cao" | "thap",
"khong_doc_duoc": ["ten_truong", ...], # cửa thoát, BẮT BUỘC có
"can_nguoi_xem": true | false
}
# biên nhận hàng: null hoặc có trường không đọc được -> đẩy sang hàng chờ người
# KHÔNG đoán. Không đoán chính là một giá trị hợp lệ.Đây đúng là thứ mình đã viết ở bài trước về cổng "thà không giao còn hơn giao rác". Giờ mình hiểu nó ở tầng thấp hơn: cổng đó phải được thiết kế vào schema, không phải dán thêm ở ngoài. Nếu cấu trúc không có ô cho sự không chắc chắn, thì sự không chắc chắn sẽ tự tìm một ô khác để chui vào — và ô đó luôn là ô trông đẹp nhất.
Schema không phải định dạng, nó là hợp đồng
Có một chuyển biến nhỏ nhưng đổi hẳn cách mình viết mấy file này.
Hồi trước mình coi schema là định dạng trao đổi: hai bên nói chuyện với nhau bằng cái này, miễn đúng cú pháp là xong. Nên mình viết schema tối giản, chỉ đủ để parse được.
Giờ mình coi schema là hợp đồng, và câu hỏi để viết nó không phải "kiểu dữ liệu là gì" mà là "nếu bên nhận chỉ có đúng file này, nó có tự xử lý đúng không". Ba thứ mình thêm vào, và cả ba đều rẻ:
- 1.Mô tả nằm trong chính trường đó, không nằm trong prompt. Vì prompt bị cắt, bị nén, bị tóm tắt qua từng bước; còn schema thì đi theo dữ liệu tới tận cùng. Mô tả đặt cạnh dữ liệu là mô tả không bao giờ bị thất lạc.
- 2.Đơn vị và múi giờ ghi rõ ngay tên trường. Ngày tháng là chỗ model sai nhiều thứ hai sau enum, và sai theo kiểu im lặng: đúng định dạng, lệch một ngày.
- 3.Trường "vì sao". Một dòng lý do ngắn cho mỗi giá trị quan trọng. Không phải để trang trí — để lúc hệ chạy sai, mình đọc được nó đã nghĩ gì, thay vì phải dựng lại cả phiên.
Và ở biên, có một luật cứng mình không cho phép vi phạm: cấu trúc sai thì từ chối, không sửa hộ. Không "thử đoán xem nó định nói gì". Không "tự thêm giá trị mặc định". Vì mỗi lần mình sửa hộ là một lần mình tự nguyện nhận lấy lỗi của nó, và từ đó về sau không còn cách nào biết hệ đúng bao nhiêu phần trăm.
Chỗ này ra tiền, và nó không nằm ở demo
Mình để ý một chuyện khi đi bán mấy cái workflow.
Demo mười lần chạy, mười lần đúng, khách gật gù. Nhưng với tỷ lệ 92%, xác suất mười lần đều đúng chỉ khoảng 43%. Nghĩa là hơn một nửa số demo mình "diễn" sẽ có ít nhất một lần trượt — và mình sẽ không biết trước là lần nào, nên mình chỉ còn cách demo ngắn, demo trên dữ liệu đẹp, demo đúng luồng mình đã thử.
Đó không phải bán hàng. Đó là chọn mẫu.
Trong khi đó, thứ khách thật sự mua lại rất khác: không phải "con bot thông minh", mà là "tôi giao việc, nó làm, tôi không phải mở ra kiểm". Câu này quy về đúng một con số: số lần khách phải sửa tay trên 1000 lần chạy. Mình đã nói chuyện này ở mấy bài trước rồi, nhưng giờ mình thấy nó có một hệ quả thực dụng: nếu muốn bán được, việc rẻ nhất để làm không phải nâng model, mà là đóng không gian đầu ra lại.
Vì chi phí hai bên lệch nhau rất xa. Nâng model = tăng chi phí mỗi lần chạy, mãi mãi, và chỉ nghiêng bàn cân thêm một chút. Đóng khuôn = làm một lần, miễn phí mỗi lần chạy về mặt token, và xoá hẳn một lớp lỗi. Đó là kiểu cải tiến hiếm: rẻ hơn, nhanh hơn, và không phải đánh đổi.
Và nó cũng nói lên một chuyện về thị trường. Nhu cầu support tự động đang tăng rõ — người dùng bị đẩy sang hỏi máy những việc lẽ ra có người trả lời, có khi máy còn chỉ họ đi nhờ một toà soạn. Ai nhảy vào mảng đó cũng sẽ gặp đúng bài toán này, vì support là loại việc mà sai một lần là mất niềm tin, còn đúng chín mươi chín lần thì không ai khen. Nơi đó, 92% không phải điểm trừ nhỏ. Nó là toàn bộ câu chuyện.
Cái khuôn, và cái đầu mình
Nghĩ lại thì cái làm mình mất nhiều thời gian nhất không phải là kỹ thuật. Kỹ thuật mất một buổi chiều.
Cái mất thời gian là thói quen nghĩ rằng kiểm soát một hệ AI nghĩa là nói với nó hay hơn. Mình đã đọc bao nhiêu bài về prompt, về cách "dặn cho rõ", về nhân vật, về ví dụ mẫu — và tất cả những thứ đó đều là thương lượng. Chúng làm hệ tốt lên theo kiểu đường trung bình. Chúng không bao giờ đưa mình tới chỗ "việc này không xảy ra được nữa".
Trong khi đó, cái khuôn — cái thứ nghe rất kỹ thuật và khô khan — lại là chỗ duy nhất mình có được tính quyết định tuyệt đối.
Rồi mình tự hỏi một câu rộng hơn: trong cả chuỗi tự động hoá của mình, còn bao nhiêu chỗ mình đang thương lượng mà lẽ ra phải đặt khuôn?
Mình đang tìm dần. Và mình khá chắc là vẫn còn.
Còn anh em thì sao — hệ của anh em hỏng ở đâu? Nếu anh em có một chỗ mà log luôn xanh nhưng kết quả thỉnh thoảng phải sửa tay, mình cá là chỗ đó không nằm ở model. Nó nằm ở một khoảng không gian ai đó đã để mở.
Bài viết dùng trải nghiệm vận hành của mình, con số đã làm tròn để minh hoạ phạm vi. Code trong bài là pseudo-code, không phải nguồn triển khai.