Mở đầu — vì sao bài này quan trọng
Khi bạn viết spec cho một feature truyền thống, ví dụ "nút Thêm vào giỏ hàng", bạn đối mặt với một hệ thống tất định (deterministic): cùng một input luôn cho ra cùng một output. Engineer đọc spec, code đúng theo, QA test theo acceptance criteria, xong. Nhưng khi feature của bạn có một mô hình ngôn ngữ lớn (LLM) ở giữa, mọi thứ đảo lộn. Cùng một câu hỏi của người dùng, model có thể trả về năm câu trả lời khác nhau ở năm lần gọi. Cái "hợp đồng" giữa input và output không còn là một dòng code, mà là một thỏa thuận mềm dẻo bạn phải mô tả cực kỳ cẩn thận để đội ngũ kỹ thuật biết: cái gì đưa vào, cái gì chắc chắn phải ra, cái gì được phép thay đổi, và cái gì tuyệt đối không được phép xảy ra.
Đây chính là lý do Input/Output Contract (hợp đồng đầu vào / đầu ra) là trái tim của một AI feature spec. Nếu bạn viết phần này lỏng lẻo, hậu quả không phải là "feature hơi sai" mà là engineer hiểu sai phạm vi, model trả về định dạng mà frontend không parse được, hệ thống downstream sập, hoặc tệ hơn — người dùng nhận về câu trả lời bịa đặt mà không ai phát hiện. Trong cả khóa học này, các bài sau sẽ nói về eval criteria, fallback, latency, hallucination guards. Nhưng tất cả chúng đều dựng trên nền móng của bài này: bạn phải định nghĩa rõ ràng input bạn đưa vào model gồm những gì, và output bạn kỳ vọng nhận về có hình dạng ra sao. Không có hợp đồng này, mọi thứ phía sau chỉ là phỏng đoán.
Khái niệm cốt lõi
Input/Output Contract là phần trong spec mô tả chính xác cấu trúc dữ liệu đi vào model và đi ra khỏi model, kèm các ràng buộc (constraint) bắt buộc. Hãy hình dung model như một hộp đen có hợp đồng: bạn cam kết cung cấp đầu vào theo đúng khuôn, và bạn đòi hỏi đầu ra theo đúng khuôn. Phần input và output cần được tách bạch và đặc tả riêng.
Input schema — ba lớp cấu thành
Input đưa vào một LLM hầu như luôn gồm ba lớp, và spec của bạn phải tách bạch chúng:
1. System prompt (chỉ dẫn cố định). Đây là phần "tính cách" và "luật chơi" của model, do bạn — PM/kỹ sư — viết, người dùng không nhìn thấy và không sửa được. Ví dụ: "Bạn là trợ lý chăm sóc khách hàng của ngân hàng X. Chỉ trả lời về sản phẩm thẻ tín dụng. Không tư vấn đầu tư. Trả lời bằng tiếng Việt, lịch sự, tối đa 3 câu." Trong spec, system prompt nên được coi như một artifact có version riêng, vì thay đổi một từ trong đây có thể đổi hành vi toàn bộ feature.
2. User input (đầu vào người dùng). Đây là cái người dùng thực sự cung cấp: câu hỏi gõ vào, file upload, ảnh, lựa chọn từ dropdown. Spec phải nêu rõ: định dạng (text thuần? markdown? có giới hạn ký tự?), độ dài tối đa, và cái gì được sanitize trước khi đưa vào model (ví dụ loại bỏ HTML, cắt còn 2.000 token).
3. Context (ngữ cảnh). Đây là phần thường bị viết ẩu nhất. Context gồm: lịch sử hội thoại gần đây (recent messages), tài liệu được truy xuất (retrieved documents trong hệ RAG), dữ liệu hồ sơ người dùng (tên, gói dịch vụ, lịch sử mua hàng), và thời gian/địa điểm. Mỗi mảnh context bạn nhồi vào đều ngốn token và ảnh hưởng câu trả lời, nên spec phải nói rõ: lấy bao nhiêu tin nhắn gần nhất? lấy bao nhiêu document? sắp xếp theo thứ tự nào? cắt bớt thế nào khi vượt context window?
Output schema — hình dạng và ràng buộc
Output contract trả lời câu hỏi: model trả về cái gì, và phần mềm của tôi nhận nó ra sao? Có hai chế độ chính:
- Free-form text — model trả về văn xuôi tự nhiên, hiển thị thẳng cho người dùng (chatbot tư vấn). Ràng buộc ở đây là về nội dung: ngôn ngữ, độ dài, giọng điệu, những thứ cấm nói.
- Structured output — model trả về JSON theo schema cố định để hệ thống xử lý tiếp (ví dụ phân loại email thành
{"category": "complaint", "priority": "high", "needs_human": true}). Đây là kiểu output mà bạn bắt buộc phải định nghĩa schema chặt chẽ với từng field, kiểu dữ liệu, giá trị enum cho phép, và field nào optional.
null? trả "unknown"?), và giới hạn (độ dài tối đa, không được chứa thông tin cá nhân, v.v.).Vì sao phải coi đây là "hợp đồng"
Gọi là contract vì nó ràng buộc hai phía. Phía bạn cam kết input luôn đúng khuôn. Phía model (qua prompt và cấu hình) cam kết output đúng khuôn. Khi một bên vi phạm — ví dụ user nhập 50.000 ký tự, hoặc model trả JSON thiếu dấu ngoặc — spec phải đã định sẵn chuyện gì xảy ra. Hợp đồng không chỉ mô tả trường hợp đẹp; nó mô tả cả vi phạm.
Tình huống thực tế
Ví dụ 1 — Trợ lý hỏi đáp đơn hàng của một sàn TMĐT Việt
Một sàn thương mại điện tử (gọi là ShopViet) muốn ra mắt chatbot trả lời câu hỏi "Đơn hàng của tôi đang ở đâu?". PM ban đầu viết spec rất sơ sài: "Người dùng hỏi, AI trả lời tình trạng đơn." Engineer dựng thử, và thảm họa xảy ra: model bịa ra mã vận đơn không tồn tại, vì nó không được cấp context về đơn hàng thật.
PM viết lại với Input/Output Contract rõ ràng. System prompt: "Bạn là trợ lý ShopViet, chỉ trả lời dựa trên dữ liệu đơn hàng được cung cấp trong CONTEXT. Nếu không có dữ liệu, nói 'Mình chưa tìm thấy thông tin đơn này'. Tuyệt đối không bịa mã vận đơn." User input: câu hỏi text, tối đa 500 ký tự. Context: hệ thống truy vấn database, nhồi vào JSON tối đa 3 đơn hàng gần nhất của user đó với các field order_id, status, tracking_code, eta. Output: free-form tiếng Việt, tối đa 4 câu, bắt buộc nhắc lại order_id cụ thể nếu có.
Bài học: vấn đề "model bịa đặt" không được giải bằng cách bảo model "đừng bịa" — nó được giải bằng việc đưa context thật vào input và ràng buộc output phải dựa trên context đó. Hợp đồng input quyết định chất lượng output.
Ví dụ 2 — Phân loại ticket hỗ trợ tại một fintech Đông Nam Á
Một công ty fintech (giả định là PayNgay, hoạt động tại Việt Nam và Indonesia) muốn dùng LLM tự động phân loại ticket khách hàng để định tuyến. Đây là feature structured output thuần. PM định nghĩa output schema cực kỳ chặt:
{
"category": "payment_failed | refund | account_locked | other",
"language": "vi | id | en",
"priority": "low | medium | high",
"needs_human": true,
"confidence": 0.0
}
Spec ghi rõ: mọi field bắt buộc có mặt; category chỉ được nhận 4 giá trị enum, nếu không khớp phải trả "other"; confidence là số thực 0–1; nếu confidence < 0.6 thì hệ thống bỏ qua kết quả AI và đẩy ticket cho người thật. Quan trọng nhất, spec quy định: nếu model trả về JSON không parse được, hệ thống coi như AI thất bại và route mặc định về hàng đợi human — chứ không được crash.
Kết quả: trong tháng đầu, 12% output có confidence thấp được chuyển cho người thật, tránh được hàng nghìn ca định tuyến sai. Bài học: với structured output, enum và hành vi khi không chắc chắn quan trọng hơn cả độ chính xác trung bình. Một schema chặt biến đầu ra ngẫu nhiên của model thành thứ phần mềm tin cậy được.
Ví dụ 3 — Tính năng tóm tắt cuộc họp bị "trôi" context
Một startup SaaS làm công cụ ghi chú cuộc họp thêm tính năng "Tóm tắt 5 ý chính". Ban đầu hoạt động tốt với cuộc họp 15 phút, nhưng với cuộc họp 2 tiếng, transcript dài 40.000 token vượt giới hạn context, và model âm thầm chỉ tóm tắt nửa cuối cuộc họp — người dùng không hề biết phần đầu bị bỏ.
Nguyên nhân: spec không định nghĩa chiến lược cắt context khi vượt giới hạn. PM bổ sung vào Input Contract: nếu transcript vượt 30.000 token, chia thành các đoạn (chunk), tóm tắt từng đoạn rồi tóm tắt lại các bản tóm tắt (map-reduce); và output phải kèm field coverage cho biết đã xử lý bao nhiêu phần trăm transcript. Bài học: input contract không chỉ nói "đưa transcript vào" — nó phải nói đưa vào thế nào khi dữ liệu quá lớn, vì context window là hữu hạn và sự cắt xén âm thầm là loại lỗi nguy hiểm nhất.
Hướng dẫn từng bước
Bước 1 — Xác định chế độ output trước. Hỏi: output này hiển thị thẳng cho người dùng (free-form) hay được phần mềm xử lý tiếp (structured)? Câu trả lời quyết định toàn bộ độ chặt của contract. Structured cần JSON schema; free-form cần ràng buộc nội dung.
Bước 2 — Viết system prompt như một artifact có version. Đặc tả vai trò, luật cấm, ngôn ngữ, độ dài. Đặt nó trong spec ở một mục riêng, đánh version (v1.0, v1.1) để khi nó thay đổi cả team biết.
Bước 3 — Đặc tả user input. Định dạng, giới hạn ký tự/token, bước sanitize (loại HTML, cắt độ dài, chặn nội dung độc hại). Nêu rõ chuyện gì xảy ra khi input rỗng hoặc quá dài.
Bước 4 — Liệt kê từng nguồn context và quy tắc nhồi. Với mỗi nguồn (lịch sử chat, RAG docs, hồ sơ user): lấy bao nhiêu, sắp xếp ra sao, cắt thế nào khi vượt context window. Đây là nơi quyết định chất lượng — đừng viết tắt.
Bước 5 — Định nghĩa output schema chính xác. Với structured: liệt kê từng field, kiểu, enum, optional/required. Với free-form: ngôn ngữ, độ dài, giọng điệu, điều cấm. Luôn kèm 1–2 ví dụ output hợp lệ.
Bước 6 — Định nghĩa hành vi khi không chắc chắn và khi vi phạm. Model trả null, "unknown", hay confidence thấp? JSON hỏng thì sao? Đây là phần biến contract từ "mô tả lý tưởng" thành "hợp đồng thật".
Bước 7 — Thêm ví dụ end-to-end. Một cặp input đầy đủ (system + user + context) → output mong đợi. Engineer sẽ dùng nó làm test case đầu tiên.
Lỗi thường gặp & mẹo
Lỗi 1: Gộp system prompt và user input làm một. Nhiều PM viết "prompt là: hãy trả lời câu hỏi của user về đơn hàng". Engineer không biết phần nào cố định, phần nào động. Hãy tách bạch ba lớp input rõ ràng.
Lỗi 2: Bỏ quên context window là hữu hạn. Spec giả định "nhồi hết lịch sử chat vào". Đến production, hội thoại dài 100 lượt, model hoặc lỗi hoặc âm thầm bỏ phần đầu. Luôn định nghĩa giới hạn và chiến lược cắt.
Lỗi 3: Output schema không có "đường thoát". Quên định nghĩa giá trị cho trường hợp model không biết. Model buộc phải chọn đại một enum, sinh ra dữ liệu sai mà nhìn rất "tự tin". Luôn có "other", null, hoặc field confidence.
Lỗi 4: Tin rằng model luôn trả JSON đúng. Không. Model thi thoảng trả JSON thiếu dấu, kèm lời dẫn "Đây là kết quả:...". Spec phải quy định parse thất bại thì fallback ra sao (bài 31 sẽ đào sâu fallback).
Mẹo: Viết output schema dưới dạng có thể dán thẳng vào code (JSON Schema hoặc TypeScript type). Khi PM và engineer cùng nhìn một schema chạy được, hiểu lầm gần như biến mất.
Mẹo: Với mỗi field structured output, tự hỏi "nếu field này sai/thiếu thì hệ thống downstream làm gì?". Câu trả lời thường lộ ra ràng buộc bạn quên đặc tả.
Bài tập thực hành
Chọn một feature AI giả định: "Trợ lý gợi ý món ăn" cho một app giao đồ ăn Việt Nam. Người dùng gõ "Tôi muốn ăn gì đó cay, dưới 100k", và AI gợi ý 3 món từ menu các quán gần đó.
- Viết System prompt (vai trò, luật cấm, ngôn ngữ, giới hạn — ví dụ chỉ gợi ý món có thật trong context).
- Đặc tả User input: định dạng, giới hạn ký tự, bước sanitize.
- Liệt kê các nguồn Context cần nhồi (menu quán gần, vị trí user, lịch sử đặt món) và quy tắc cắt khi quá nhiều quán.
- Quyết định chế độ Output: structured hay free-form? Nếu structured, viết JSON schema với field như
dish_name,restaurant,price,spicy_level, và định nghĩa hành vi khi không có món nào khớp (null? mảng rỗng? thông báo gì?). - Viết một cặp ví dụ end-to-end: input đầy đủ → output mong đợi.
Tóm tắt
Input/Output Contract là phần định nghĩa chính xác cái gì đưa vào model và cái gì kỳ vọng nhận về. Input gồm ba lớp tách bạch: system prompt (chỉ dẫn cố định, có version), user input (đầu vào người dùng, có giới hạn và sanitize), và context (lịch sử, tài liệu, hồ sơ — kèm quy tắc nhồi và cắt khi vượt context window). Output có hai chế độ: free-form (ràng buộc nội dung) và structured (JSON schema chặt với enum, type, và hành vi khi không chắc chắn). Một contract tốt luôn mô tả cả trường hợp vi phạm — input quá dài, model không chắc, JSON hỏng — chứ không chỉ trường hợp đẹp. Ba ví dụ thực tế cho thấy cùng một bài học: chất lượng output không đến từ việc "dặn model cố gắng", mà từ việc thiết kế input contract kỹ và ràng buộc output schema chặt. Nắm vững bài này, bạn đã có nền móng cho mọi phần AI spec phía sau.