Giao diện
Làm rõ có AI hỗ trợ
AI làm giảm độ mơ hồ của yêu cầu. Con người xác nhận những quyết định quan trọng.
Một yêu cầu đi vào hệ thống với nhiều câu hỏi chưa có lời đáp. AI giúp đặt câu hỏi và đề xuất phương án trả lời; nhưng AI không bao giờ là tác giả của câu trả lời cuối hay của một quyết định.
Requirement
↓
Question ← do người thêm, hoặc chấp nhận từ gợi ý AI
↓
AI Suggestions A / B / C (kèm lý do và rủi ro, một phương án được khuyến nghị)
↓
Con người chọn → sửa → xác nhận = Final Answer
↓
Decision ← con người ký tênVòng đời câu hỏi
| Trạng thái | Nghĩa | Chuyển bởi |
|---|---|---|
open | Mới đặt, chưa có gợi ý | POST /v1/questions hoặc chấp nhận một gợi ý câu hỏi |
suggested | Đã có ba phương án A / B / C của AI | POST /v1/questions/:id/suggest-answers |
answered | Một người đã xác nhận câu trả lời cuối | POST /v1/questions/:id/answer |
decided | Câu trả lời đã được nâng thành quyết định D-NNNN | POST /v1/questions/:id/decide |
dropped | Bỏ, nhưng giữ lịch sử | DELETE /v1/questions/:id khi câu hỏi đã có lịch sử |
Có thể trả lời thẳng mà không cần AI (open → answered). Đường thủ công — thêm câu hỏi, viết câu trả lời, quyết, gắn bằng chứng — giống hệt nhau dù AI có được cấu hình hay không.
Giải phẫu một câu hỏi
Bảng questions, ID Q-NNNN. Năm phần của tài liệu đề xuất được giữ tách bạch thành cột:
| Phần | Cột | Ai ghi |
|---|---|---|
| Human Answer | draft_answer_en/vi | Người, qua form thường (PATCH) — trước hoặc thay cho AI |
| AI Suggestions | bảng ai_suggestions, kind = answer, label A / B / C | Chỉ Worker, bất biến |
| Selected Option | selected_suggestion_id | Chỉ route answer |
| Human Modification | hiệu giữa gợi ý đã chọn và final_answer_* | Không lưu riêng — giao diện điền sẵn trình soạn thảo từ phương án đã chọn, nên phần sửa nhìn thấy được |
| Final Answer | final_answer_en/vi, answered_by, answered_at | Chỉ route answer, đóng dấu email từ Cloudflare Access |
Ngoài ra: source_suggestion_id (câu hỏi này được chấp nhận từ gợi ý nào), decision_id (quyết định sinh ra từ nó), requirement_id, brand_id (tự điền từ yêu cầu).
Sai
PATCH /v1/questions/Q-0881 { "final_answer_en": "No", "status": "decided" }
Đúng
final_answer_*, selected_suggestion_id, answered_by/at, status và decision_id không nằm trong danh sách cột của form chung. Chúng chỉ được ghi bởi ba route riêng — answer, decide, và tạo câu hỏi — và Worker luôn đóng dấu user.email. "Con người quyết" là ràng buộc ở server, không phải quy ước.
Hai loại gợi ý
Bảng ai_suggestions, ID AIS-<8 hex>, bất biến (không có updated_at, không phải tài nguyên dùng chung; chỉ status đổi, và chỉ qua route riêng).
| Loại | kind | Sinh bởi | Thành gì |
|---|---|---|---|
| Gợi ý câu hỏi | question (tối đa 5, question_id = NULL) | POST /v1/requirements/:id/suggest-questions | Chưa phải câu hỏi — chỉ thành câu hỏi khi một người bấm Thêm làm câu hỏi → POST /v1/suggestions/:id/accept |
| Gợi ý trả lời | answer (đúng 3, label A / B / C, một recommended) | POST /v1/questions/:id/suggest-answers | Một phương án được chọn khi trả lời; các phương án còn lại rejected |
Mỗi gợi ý mang name_* (tiêu đề), description_* (phương án), rationale_* (phân tích), risks_* (rủi ro, kể cả mâu thuẫn với quyết định đã có), và status proposed · selected · rejected · accepted. Giao diện luôn hiện gợi ý trong một khung được dán nhãn rõ: model, phiên bản prompt, ai yêu cầu.
Ngữ cảnh AI nhìn thấy
Trước mỗi lần gọi, Worker dựng ngữ cảnh từ chuỗi truy vết của yêu cầu: các trường của yêu cầu, sản phẩm (tên, mệnh đề), Brand, feature, năng lực (tên, định nghĩa), sáng kiến; các câu hỏi anh em cùng câu trả lời cuối; và mọi quyết định decided gắn với yêu cầu hoặc sản phẩm đó. Bộ nhớ quyết định là đầu vào — prompt hệ thống bắt AI tôn trọng mọi quyết định đã liệt kê và nêu mâu thuẫn trong risks_*.
Prompt hệ thống (PROMPT_VERSION = clarify-v1) yêu cầu: hỏi về ý nghĩa, người dùng, tần suất, bằng chứng, tình huống hỏng; ba phương án loại trừ lẫn nhau và sẵn sàng để quyết; không bịa sự thật về sản phẩm; tiếng Việt và tiếng Anh nói cùng một điều; chỉ trả JSON.
Workers AI qua AI Gateway
Worker gọi Cloudflare Workers AI với model DeepSeek, đi qua AI Gateway conan1, bằng binding AI. Không có SDK ngoài, không có khoá API, không có secret nào cần đặt.
| Cấu hình | Giá trị | Ở đâu |
|---|---|---|
| Binding | "ai": { "binding": "AI" } | wrangler.jsonc |
| Model | AI_MODEL = @cf/deepseek-ai/deepseek-v4-flash-0731 | wrangler.jsonc → vars (cùng giá trị là mặc định trong src/lib/ai.ts nếu thiếu) |
| Gateway | AI_GATEWAY_ID = conan1 | wrangler.jsonc → vars; rỗng thì gọi thẳng, không qua gateway |
| Hạn mức ngày | AI_DAILY_LIMIT = 50 | wrangler.jsonc → vars; 0 hoặc bỏ trống là không giới hạn |
| Phiên bản prompt | clarify-v1 | src/lib/ai.ts |
Vì sao là flash chứ không phải pro: đo ngày 2026-09-02 trên prompt clarify-v1, @cf/deepseek-ai/deepseek-v4-pro-0813 một lần vượt thời gian chờ của Workers AI (lỗi 3046) và một lần cắt cụt JSON ở max_tokens = 4000 vì phần suy nghĩ ăn hết ngân sách; deepseek-v4-flash-0731 trả lời trong khoảng 19 giây. Muốn quay lại pro thì chỉ đổi vars.AI_MODEL trong wrangler.jsonc khi độ trễ chấp nhận được — code không phụ thuộc vào model cụ thể.
ts
const res = await env.AI.run(model, {
messages: [{ role: 'system', content: SYSTEM_PROMPT + schema }, { role: 'user', content: context }],
max_tokens: 4000, temperature: 0.3,
}, env.AI_GATEWAY_ID ? { gateway: { id: env.AI_GATEWAY_ID, skipCache: true } } : undefined)Phản hồi có dạng OpenAI: res.choices[0].message.content là JSON cần đọc, res.choices[0].message.reasoning_content là phần suy nghĩ của model — không bao giờ hiển thị hay lưu nó như một câu trả lời; res.usage.prompt_tokens / completion_tokens là chi phí. Model tiêu token cho phần suy nghĩ, nên max_tokens phải đủ lớn và Worker đọc JSON khoan dung (bỏ rào ```json, lấy từ { đầu tới } cuối) rồi kiểm số phần tử và nhãn A / B / C.
AI Gateway conan1 cho nhật ký, cache và giới hạn tốc độ tập trung trên Cloudflare Dashboard. Code không phụ thuộc vào sự tồn tại của gateway: thiếu AI_GATEWAY_ID thì gọi Workers AI trực tiếp.
Khi chạy local, wrangler dev chạy Workers AI từ xa bằng phiên đăng nhập Cloudflare sẵn có trên máy; không cần cấu hình thêm.
Mọi lần gọi đều được ghi
Bảng ai_runs, ID AIR-<8 hex>: kind (suggest_questions · suggest_answers), requirement_id, question_id, actor (email người bấm), model (id model Workers AI thực dùng), prompt_version, input_tokens, output_tokens, status (ok · failed), error. Một lần gọi thất bại vẫn là một dòng. Các gợi ý của cùng một lần gọi chia sẻ run_id.
ai_runs và ai_suggestions không nằm trong gói GET /v1/architecture; trang chi tiết yêu cầu tự tải chúng qua GET /v1/requirements/:id/suggestions, nên reload() của ứng dụng không phình theo lượng AI.
Hạn mức mỗi ngày
Mỗi lần gợi ý tốn neuron của Workers AI, và một client kẹt vòng lặp gọi được rất nhiều lần. AI_DAILY_LIMIT đặt trần số lần gọi mỗi ngày UTC cho toàn bộ Worker — không phải mỗi người, không phải mỗi yêu cầu.
| Thuộc tính | Ghi chú |
|---|---|
| Cấu hình | vars.AI_DAILY_LIMIT trong wrangler.jsonc, hiện là "50" |
| Tắt trần | 0, giá trị không đọc được thành số, hoặc bỏ hẳn biến |
| Đếm từ đâu | SELECT COUNT(*) FROM ai_runs WHERE created_at >= date('now') — mọi dòng ai_runs, kể cả lần gọi failed |
| Ranh giới ngày | Ngày UTC. created_at là datetime('now') dạng YYYY-MM-DD …, nên so chuỗi với date('now') chọn đúng hôm nay |
| Áp ở đâu | Cả hai endpoint gợi ý, trước khi mở dòng ai_runs — lần bị chặn không tự đếm chính nó |
Hết trần thì hai endpoint gợi ý trả 429:
json
{
"error": "ai_daily_limit",
"used": 50,
"limit": 50,
"message_en": "Today's 50 AI suggestions are used up. Write the answer yourself, or try tomorrow.",
"message_vi": "Đã dùng hết 50 lượt gợi ý AI của hôm nay. Hãy tự viết câu trả lời, hoặc thử lại ngày mai."
}Đường thủ công không bao giờ bị chặn
Trần chỉ chạm hai route gợi ý. Thêm câu hỏi, viết câu trả lời cuối, quyết, gắn bằng chứng, chấp nhận một gợi ý đã sinh từ trước — tất cả vẫn chạy khi hạn mức đã cạn, y như khi AI chưa được cấu hình.
Trần dùng chung, không phải theo người
Năm mươi lượt là của cả tổ chức trong ngày hôm đó, nên một người có thể tiêu hết phần của người khác. Giới hạn theo người, nếu cần, vẫn phải đặt ở AI Gateway.
Hợp đồng lỗi
GET /v1/ai/status trả { enabled, model, gateway, limit, used }: limit là AI_DAILY_LIMIT đã đọc thành số (0 là không giới hạn), used là số lần gọi hôm nay — luôn 0 khi enabled = false, vì lúc đó không có gì để đếm ngược. Store tải nó lúc khởi động; nếu enabled = false các nút Gợi ý hiện mờ với chú thích AI chưa cấu hình, còn khi có trần thì giao diện hiện phần còn lại ngay cạnh nút và tắt nút khi đã cạn.
| Tình huống | Mã | error | Ghi ai_runs? |
|---|---|---|---|
Binding AI không có | 503 | ai_not_configured | Không |
| Đã dùng hết hạn mức ngày | 429 | ai_daily_limit | Không |
| Workers AI báo giới hạn tốc độ / hạn mức | 429 | ai_rate_limited | Có, failed |
| Gọi thất bại vì lý do khác | 502 | ai_failed | Có, failed |
| Model trả về JSON không đọc được, sai số phương án, sai nhãn | 502 | ai_no_answer | Có, failed |
Giao diện ánh xạ bốn mã này sang thông điệp song ngữ. Mọi thứ khác — thêm câu hỏi, viết câu trả lời, quyết, gắn bằng chứng — vẫn chạy bình thường khi AI vắng mặt.
Ai được bấm Gợi ý
Cùng quy tắc với toàn mặt phẳng Requirements: chỉ architect — và trong phạm vi AI_DAILY_LIMIT của ngày hôm đó.
Trả lời và quyết
- Trả lời —
POST /v1/questions/:id/answervới{ selected_suggestion_id?, final_answer_en, final_answer_vi }. Cả hai câu trả lời cuối bắt buộc; gợi ý (nếu có) phải là gợi ýanswercủa đúng câu hỏi này. Trong một batch: ghi câu trả lời cuối, đóng dấuanswered_by = email,answered_at, đánh dấu gợi ý đã chọnselectedvà các gợi ý cùng lần gọirejected, chuyểnstatus = answered. - Quyết —
POST /v1/questions/:id/decidevới{ name_*, rationale_*, description_*? }, chỉ khi câu hỏi đãanswered. SinhD-NNNN,decided_by = email,decided_by_kind = ai_assistednếu có gợi ý được chọn (ngược lạihuman), sao chép phân tích + rủi ro của gợi ý vàoai_analysis_*, đóng băng dải Liên quan, chuyển câu hỏi sangdecided. Chi tiết ở Quyết định.