Nhúng AI vào app của bạn
Gọi Claude API từ Worker, Workers AI chạy sẵn trên Cloudflare, tìm kiếm theo ngữ nghĩa bằng Vectorize — và cách kiểm soát chi phí trước khi nó kiểm soát bạn.
Tới đây, AI đã giúp bạn viết app. Bài này nói về việc đưa AI vào trong app — để chính người dùng của bạn dùng nó.
Ba cách, chọn đúng cách
| Claude API | Workers AI | Vectorize + mô hình | |
|---|---|---|---|
| Là gì | Gọi mô hình của Anthropic | Mô hình chạy sẵn trên Cloudflare | Tìm theo ý nghĩa trong tài liệu của bạn |
| Mạnh nhất ở | Hiểu ngữ cảnh dài, tiếng Việt tốt, suy luận | Rẻ, nhanh, không cần khóa ngoài | Trả lời dựa trên dữ liệu riêng |
| Trả tiền | Theo lượng chữ vào/ra | Theo lượt, có hạn mức free | Theo lượt truy vấn + lưu trữ |
| Dùng cho | Soạn thảo, tóm tắt, phân loại, trợ lý | Dịch, nhận dạng ảnh, nhúng, việc nhẹ | Hỏi đáp tài liệu nội bộ |
Và một lựa chọn thứ tư mà người ta hay quên: không dùng AI. Nếu việc đó giải được bằng một câu WHERE trong SQL, đừng gọi mô hình — nó chậm hơn, đắt hơn, và thỉnh thoảng sai.
Gọi Claude API từ Worker
npx wrangler secret put ANTHROPIC_API_KEY
async function hoiClaude(env, cauHoi, boiCanh) {
const r = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-5-5",
max_tokens: 1024,
system: "Bạn trả lời ngắn gọn bằng tiếng Việt, chỉ dựa trên dữ liệu được cung cấp. " +
"Nếu dữ liệu không có thông tin, nói thẳng là không có. Không đoán.",
messages: [{ role: "user", content: `Dữ liệu:\n${boiCanh}\n\nCâu hỏi: ${cauHoi}` }],
}),
signal: AbortSignal.timeout(30000), // luôn có hạn giờ
});
if (!r.ok) {
console.log("Claude API loi:", r.status);
return null; // đường lui: app vẫn chạy
}
const j = await r.json();
return j.content[0].text;
}
Bốn điểm bắt buộc:
- Khóa nằm trong Secret, không bao giờ ở phía trình duyệt. Đưa khóa xuống trình duyệt là bất kỳ ai mở
F12cũng lấy được và tiêu tiền của bạn. - Hạn giờ. Mô hình có lúc chậm; đừng để request của bạn treo theo.
- Câu lệnh hệ thống (
system) chặn bịa. Câu "chỉ dựa trên dữ liệu được cung cấp, không có thì nói không có" là khác biệt giữa trợ lý dùng được và trợ lý nói sảng. - Đường lui. AI hỏng thì app vẫn phải chạy, chỉ mất tính năng AI.
Khi gọi API của một bên thứ ba, dữ liệu của khách rời khỏi hệ thống của bạn. Với app doanh nghiệp, đây là chuyện phải nói trước, không phải chuyện giấu đi.
Ba việc nên làm: 1. Nói rõ trong hợp đồng/điều khoản: dữ liệu nào được gửi đi, gửi cho ai, để làm gì. 2. Gửi ít nhất có thể. Tóm tắt một đơn hàng thì không cần gửi kèm số điện thoại khách. 3. Che thông tin định danh trước khi gửi nếu việc đó không cần tới nó — thay tên thật bằng "Khách A".
Đây cũng là một trong vài chỗ mà Workers AI có lợi thế thật: mô hình chạy trong hạ tầng Cloudflare mà app bạn đã dùng, đường đi của dữ liệu ngắn hơn.
Workers AI — không cần khóa ngoài
[ai]
binding = "AI"
const kq = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
messages: [{ role: "user", content: "Tóm tắt đoạn này trong 2 câu: " + vanBan }],
});
Việc Workers AI làm tốt với giá rẻ: nhúng văn bản (cho tìm kiếm ngữ nghĩa), dịch, nhận dạng chữ trong ảnh, phân loại đơn giản, chuyển giọng nói thành chữ.
Việc nên để Claude API: hiểu ngữ cảnh dài, viết tiếng Việt tự nhiên, suy luận nhiều bước, tuân thủ ràng buộc phức tạp.
Hỏi đáp trên tài liệu của bạn (Vectorize)
Đây là tính năng khách hay yêu cầu nhất: "cho em hỏi app xem quy định nghỉ phép thế nào".
Cách làm, ba bước:
// --- Bước 1: lúc thêm tài liệu, cắt nhỏ rồi lưu vector ---
const doan = catDoan(vanBan, 800); // ~800 ký tự mỗi đoạn, có chồng lấn
for (const [i, d] of doan.entries()) {
const { data } = await env.AI.run("@cf/baai/bge-m3", { text: [d] });
await env.VEC.insert([{ id: `${docId}:${i}`, values: data[0], metadata: { docId, doan: d } }]);
}
// --- Bước 2: lúc hỏi, tìm đoạn liên quan ---
const { data } = await env.AI.run("@cf/baai/bge-m3", { text: [cauHoi] });
const kq = await env.VEC.query(data[0], { topK: 8, returnMetadata: true });
// --- Bước 3: đưa đoạn tìm được cho mô hình trả lời ---
const boiCanh = kq.matches.map(m => m.metadata.doan).join("\n---\n");
const traLoi = await hoiClaude(env, cauHoi, boiCanh);
Đặt topK quá lớn sẽ bị từ chối (giới hạn thay đổi theo cấu hình index, và topK > 20 khi kèm metadata là chỗ hay chạm). Thực tế 8–10 đoạn là đủ và cho câu trả lời tốt hơn — nhiều đoạn quá thì bối cảnh loãng, mô hình trả lời chung chung.
Nếu cần nhiều hơn: lấy topK vừa phải rồi lọc lại bằng mã (theo phòng ban, theo ngày, theo loại tài liệu) trước khi đưa cho mô hình.
Ba điều quyết định chất lượng, theo đúng thứ tự quan trọng:
- Cách cắt đoạn. Cắt giữa câu hoặc giữa một điều khoản là câu trả lời sẽ cụt. Cắt theo đề mục tự nhiên của tài liệu (mỗi điều, mỗi mục) tốt hơn cắt theo số ký tự.
- Trả kèm nguồn. Luôn hiện "theo Điều 12, Nội quy lao động". Người dùng doanh nghiệp không tin câu trả lời không có nguồn, và họ đúng khi không tin.
- Cho phép nói không biết. Bắt buộc trong câu lệnh hệ thống. Một trợ lý nói "tài liệu không đề cập" đáng tin hơn một trợ lý luôn có câu trả lời.
Kiểm soát chi phí trước khi nó kiểm soát bạn
Đây là chỗ app có AI khác hẳn app thường: mỗi lượt bấm của người dùng là một khoản tiền.
Bốn lớp chặn, nên có đủ:
// 1. Trần theo người dùng mỗi ngày
const khoa = `ai:${u.id}:${new Date().toISOString().slice(0, 10)}`;
const n = (await env.PHIEN.get(khoa, { type: "json" })) || 0;
if (n >= 50) return json({ loi: "Ban da dung het luot hoi hom nay" }, 429);
await env.PHIEN.put(khoa, JSON.stringify(n + 1), { expirationTtl: 86400 });
// 2. Chặn đầu vào quá dài
if (cauHoi.length > 2000) return json({ loi: "Cau hoi qua dai" }, 400);
// 3. Nhớ lại câu đã hỏi (rất hiệu quả — nhiều người hỏi giống nhau)
const khoaDem = "ai-dem:" + await bam(cauHoi.trim().toLowerCase());
const cu = await env.PHIEN.get(khoaDem);
if (cu) return json({ traLoi: cu, tuBoNho: true });
// 4. Ghi lại từng lượt để biết tiền đi đâu
await env.DB.prepare(
"INSERT INTO nhat_ky_ai (nguoi_dung, cau_hoi, chu_vao, chu_ra, luc) VALUES (?,?,?,?,datetime('now'))"
).bind(u.id, cauHoi.slice(0, 200), soChuVao, soChuRa).run();
Lớp 3 — nhớ lại câu trả lời — thường cắt được 30–60% chi phí trong app nội bộ, vì mọi người hỏi những câu rất giống nhau ("nghỉ phép thế nào", "quy định đi muộn").
Tôi đã làm vài app có AI cho doanh nghiệp. Bài học về giá trị:
Tính năng bán được: hỏi đáp trên tài liệu nội bộ (nội quy, quy trình, thông báo). Lý do rất cụ thể — trước đó nhân viên phải nhắn hỏi phòng nhân sự, mỗi ngày vài chục câu lặp lại. App trả lời được 80% câu đó là phòng nhân sự tiết kiệm thật, đo được bằng số tin nhắn.
Tính năng nghe hay mà không ai dùng: "trợ lý AI" chung chung đặt ở góc màn hình. Người dùng không biết hỏi gì, hỏi vài lần rồi thôi.
Khác biệt: tính năng thứ nhất thay thế một cuộc hỏi người cụ thể. Tính năng thứ hai thì không thay thế gì cả.
Nên khi khách nói "em muốn thêm AI", câu hỏi của tôi luôn là: "Hiện giờ nhân viên đang phải hỏi ai, câu gì, mỗi ngày bao nhiêu lần?" Trả lời được câu đó thì mới có sản phẩm.
Đừng để AI quyết việc quan trọng
Ba ranh giới, cùng tinh thần với luật ở lớp 5:
- AI đề xuất, người duyệt cho mọi thứ có hậu quả: duyệt đơn, chấm công, tính thưởng, gửi thư cho khách.
- Đừng để AI tính tiền. Mô hình làm toán đôi khi sai, và sai rất tự tin. Tính bằng mã, dùng AI để diễn giải kết quả.
- Luôn hiện nguồn và cho phép kiểm. Người dùng phải bấm được vào để xem tài liệu gốc.
Thêm chức năng hỏi đáp trên tài liệu nội bộ:
- Vectorize index cho các văn bản trong bảng tai_lieu.
- Cắt đoạn THEO ĐỀ MỤC tự nhiên của văn bản (mỗi điều / mỗi mục),
không cắt cứng theo số ký tự. Có chồng lấn 1 câu giữa hai đoạn.
- Nhúng bằng Workers AI, trả lời bằng Claude API.
- topK = 8, kèm metadata, lọc theo phòng ban của người hỏi.
- Câu lệnh hệ thống bắt buộc: chỉ dựa trên đoạn được cung cấp;
không có thì trả lời "Tài liệu hiện không đề cập"; không đoán.
- Câu trả lời LUÔN kèm nguồn: tên văn bản + số điều/mục, bấm được
để mở văn bản gốc.
- Chặn chi phí: trần 50 lượt/người/ngày, câu hỏi tối đa 2000 ký tự,
nhớ lại câu đã hỏi trong 24h, ghi nhật ký từng lượt (người, câu,
số chữ vào/ra).
- Timeout 30s + đường lui: AI hỏng thì hiện "Chức năng hỏi đáp tạm
thời không dùng được", phần còn lại của app vẫn chạy bình thường.
Xong rồi tự kiểm bằng 5 câu hỏi thật, trong đó có 1 câu mà tài liệu
KHÔNG có đáp án — kiểm xem nó có bịa không. Báo kết quả cho tôi.
Phép thử cuối cùng là phép thử quan trọng nhất: hỏi một câu mà tài liệu không có. Nếu nó bịa ra một câu trả lời nghe hợp lý, bạn chưa giao được.
Tôi làm app tra cứu thông báo nội bộ. Hôm nghiệm thu, tôi hỏi nó một chuyện công ty chưa bao giờ ra thông báo: "Công ty có hỗ trợ tiền xăng không?"
Nó trả lời rất trôi chảy, kèm cả mức tiền. Hoàn toàn bịa.
Tôi sửa đúng một câu trong phần hướng dẫn hệ thống — bắt nó nói "không đề cập" khi không tìm thấy — và hỏi lại. Lần này nó trả lời đúng là không có.
Từ đó tôi có một luật riêng: mỗi lần đụng vào phần AI, phải hỏi lại một câu bẫy. Mất 30 giây. Vì một lần nó bịa trước mặt sếp của khách là xong cả dự án.
- Đủ: ba cách nhúng AI, mã gọi Claude API có đủ 4 chốt, Workers AI, hỏi đáp tài liệu bằng Vectorize, bốn lớp chặn chi phí, ba ranh giới.
- Đã trả giá thật: giới hạn
topKcủa Vectorize và bài học "cho phép nói không biết". - Chưa nói: trả lời theo luồng (streaming), gọi công cụ, tinh chỉnh mô hình. Hữu ích, nhưng đừng thêm trước khi tính năng cơ bản đã đáng tin.
- Rủi ro còn lại: tên mô hình và giá đổi thường xuyên. Kiểm lại tài liệu Anthropic và Workers AI trước khi báo giá cho khách; đừng gán cứng một mô hình vào mã mà không có chỗ đổi nhanh.
- Dựng hỏi đáp tài liệu theo đề trên, với tài liệu thật của bạn (nội quy, quy trình, hoặc tài liệu sản phẩm).
- Chạy 10 câu hỏi thật, trong đó 2 câu mà tài liệu không có đáp án. Ghi lại: bao nhiêu câu đúng, bao nhiêu câu bịa.
- Bật lớp nhớ lại, hỏi cùng một câu 5 lần, kiểm nhật ký: chỉ được tính tiền 1 lượt.
- Tắt khóa API (đặt sai cố ý) → app phải hiện thông báo tử tế và mọi chức năng khác vẫn chạy.
Mục 2 là mục quyết định có giao được hay không. Tỉ lệ bịa phải bằng 0 trước khi đưa cho người dùng thật.