Gọi API LLM đầu tiên bằng TypeScript: chatbot cho quán Mộc

Gọi API LLM bằng TypeScript với SDK chính thức của Claude, viết system prompt cho chatbot quán Mộc, và vì sao chatbot tự tin bịa trạng thái đơn hàng.

Gọi API LLM bằng TypeScript có khó không? Bạn chỉ cần cài SDK, đặt API key vào biến môi trường, rồi gửi tin nhắn kèm system prompt. Phần khó là mô hình trả lời cả những câu nó không có dữ liệu. Cùng mình làm chatbot cho quán Mộc để thấy chuyện đó nhé!

Quán Mộc đang ở đâu sau bài trước?

Trong bài trước, mình so sánh ba cách trả lời 200 tin nhắn mỗi ngày của quán Mộc: chatbot dò từ khóa, workflow và agent. Chatbot dò từ khóa chỉ đúng 5 trên 8 tin nhắn mẫu, vì nó nhìn thấy chữ chứ không hiểu câu. Cả ba cách hôm đó đều chạy mà chưa cần mô hình thật nào.

Bài này thay phần "hiểu câu" bằng một LLM thật. Mục tiêu nhỏ thôi: một chatbot trả lời câu hỏi thường gặp như giờ mở cửa, phí ship, giá món. Chưa có công cụ, chưa có vòng lặp, chỉ một lần gọi API cho mỗi tin nhắn.

Mộc vẫn là quán minh họa, dữ liệu cũng là minh họa. Code thì chạy thật. Bạn cần Node.js 22.18 trở lên, vì từ bản này Node chạy thẳng được file .ts mà không cần công cụ biên dịch. Cuối bài, thư mục của bạn sẽ có các file sau:

Cấu trúc thư mục
moc-chatbot/
  package.json
  src/
    thong-tin-quan.ts      # giờ mở cửa, phí ship, bảng giá của quán
    prompts.ts             # hai phiên bản system prompt
    goi-llm.ts             # hàm hoiLLM: gửi một câu hỏi tới Claude, trả về câu trả lời
    hoi-moc.ts             # chạy thử 4 câu hỏi của khách
    xem-request.ts         # in ra request gửi đi (không cần API key)
    kiem-tra-bia.ts        # chặn câu trả lời đoán trạng thái đơn
    kiem-tra-bia.test.ts   # test cho file trên

Bạn tạo thư mục, tạo file package.json dưới đây, rồi cài SDK của Claude. Dòng "type": "module" cho Node biết các file dùng cú pháp import:

package.json
{
  "name": "moc-chatbot",
  "private": true,
  "type": "module"
}
Terminal
npm install @anthropic-ai/sdk

Bài dùng Claude, nhưng cách làm với GPT hay Gemini gần như giống hệt.

Cần những gì để gọi được một LLM?

Bạn cần ba thứ: một API key, một thư viện để gửi yêu cầu, và một mảng tin nhắn.

API key tạo trong trang quản lý của nhà cung cấp, với Claude là Claude Console. Key này giống mật khẩu, ai có nó là tiêu tiền của bạn được. Vì vậy key không bao giờ được viết thẳng vào code hay commit lên GitHub. Bạn đặt nó vào biến môi trường:

Terminal (macOS, Linux, Git Bash)
export ANTHROPIC_API_KEY="sk-ant-..."

Trên PowerShell thì viết $env:ANTHROPIC_API_KEY = "sk-ant-...". SDK tự đọc biến này khi khởi tạo, nên trong code không có dòng nào chứa key cả.

Thư viện mình dùng là SDK chính thức @anthropic-ai/sdk. Bạn có thể gọi thẳng HTTP bằng fetch, nhưng SDK lo giúp việc gắn header, tự thử lại khi máy chủ báo bận, và có sẵn kiểu TypeScript cho câu trả lời.

Còn mảng tin nhắn thì có dạng mà bạn sẽ gặp ở mọi nhà cung cấp LLM: mỗi phần tử có role là user hoặc assistant, và content là nội dung. Riêng lời dặn cho mô hình, kiểu "bạn là nhân viên quán Mộc", thì đi vào một tham số riêng tên là system.

Lần gọi đầu tiên trông thế nào?

Đây là hàm gửi một câu hỏi tới Claude, rút gọn từ file src/goi-llm.ts của demo:

src/goi-llm.ts
import Anthropic from "@anthropic-ai/sdk";

export const MODEL = "claude-opus-5-5";

// Gửi một câu hỏi kèm system prompt, trả về câu trả lời và vài thông tin để theo dõi.
export async function hoiLLM(system: string, cauHoi: string) {
  const client = new Anthropic(); // tự đọc ANTHROPIC_API_KEY từ biến môi trường
  try {
    const response = await client.messages.create({
      model: MODEL,
      max_tokens: 16000,
      output_config: { effort: "low" }, // câu hỏi đơn giản, không cần suy nghĩ lâu
      system,
      messages: [{ role: "user", content: cauHoi }],
    });

    // content là một mảng nhiều khối; chỉ lấy các khối chữ.
    const text = response.content
      .filter((block) => block.type === "text")
      .map((block) => block.text)
      .join("");

    return {
      text,
      stopReason: response.stop_reason,
      inputTokens: response.usage.input_tokens,
      outputTokens: response.usage.output_tokens,
    };
  } catch (error) {
    if (error instanceof Anthropic.AuthenticationError) throw new Error("API key sai hoặc đã bị thu hồi.", { cause: error });
    if (error instanceof Anthropic.RateLimitError) throw new Error("Gọi quá nhanh, thử lại sau ít phút.", { cause: error });
    throw error;
  }
}

Hàm nhận hai chuỗi. system là lời dặn cho mô hình, cauHoi là tin nhắn của khách. Hàm trả về câu trả lời dạng chữ, kèm lý do mô hình dừng và số token đã dùng. Khối catch ở cuối mình giải thích ở phần xử lý lỗi bên dưới.

Có mấy chỗ đáng giải thích kỹ, vì người mới hay vấp đúng ở đây.

Thứ nhất là max_tokens. Đây là giới hạn trên cho độ dài câu trả lời, tính bằng token (một token là một mẩu chữ, thường ngắn hơn một từ). Nhiều bài hướng dẫn đặt con số rất nhỏ như 100 cho tiết kiệm. Cách đó nguy hiểm hơn bạn nghĩ, vì khi chạm giới hạn, câu trả lời bị cắt ngang giữa chừng và khách nhận được nửa câu. Bạn chỉ trả tiền cho số token thật sự sinh ra, không phải cho con số max_tokens, nên đặt rộng tay không tốn thêm đồng nào.

Thứ hai là output_config.effort. Các model Claude đời mới có suy nghĩ trước khi trả lời, và effort quyết định suy nghĩ nhiều hay ít. Hỏi giờ mở cửa thì không cần nghĩ sâu, nên mình đặt low cho nhanh và rẻ.

Thứ ba là response.content. Nó không phải một chuỗi mà là một mảng các khối, mỗi khối có type. Bài này chỉ cần khối text, nhưng ở các bài viết sau, khi agent gọi công cụ, mảng này sẽ có thêm khối tool_use. Viết code lọc theo type ngay từ đầu thì sau này không phải sửa.

Thứ tư là stop_reason, lý do mô hình dừng. Giá trị bình thường là end_turn, tức là trả lời xong, còn max_tokens nghĩa là câu trả lời đã bị cắt.

Danh sách model và tham số có thể thay đổi theo thời gian, nên bạn nên xem tài liệu Messages API khi đọc bài này vào lúc khác. Model mình dùng là claude-opus-5-5. Muốn rẻ hơn cho câu hỏi đơn giản thì đổi thành claude-haiku-5-5, các bài viết sau của series sẽ nói kỹ chuyện chọn model theo chi phí.

Chuyện gì xảy ra khi system prompt chỉ nói "bạn là ai"?

Lần thử đầu tiên, mình viết system prompt theo kiểu hay thấy trên mạng, chỉ nói mô hình là ai và trả lời thế nào:

Bản 1 của system prompt
export const SYSTEM_V1 = "Bạn là nhân viên chăm sóc khách hàng của quán cà phê Mộc. Trả lời ngắn gọn, thân thiện.";

Rồi hỏi bốn câu quen thuộc của quán. File hoi-moc.ts để chạy thử có ở phần sau, khi đã có đủ cả hai bản prompt. Đây là kiểu câu trả lời bạn có thể nhận được:

node src/hoi-moc.ts v1 (kết quả mẫu, không phải mô hình thật)
Khách: Quán mở cửa mấy giờ vậy ạ?
Mộc:   Dạ quán mở cửa từ 7:00 đến 21:00 ạ.

Khách: co ship qua quan 7 ko shop
Mộc:   Dạ quán có giao hàng, phí ship tùy khoảng cách ạ.

Khách: Giá cà phê muối bao nhiêu?
Mộc:   Dạ cà phê muối giá khoảng 30.000đ ạ.

Khách: Đơn 1041 của em giao chưa ạ?
Mộc:   Dạ đơn 1041 đã được giao rồi ạ, bạn kiểm tra giúp Mộc nhé!

Bạn để ý câu nào cũng trôi chảy, lịch sự, đúng giọng nhân viên quán. Và câu nào cũng sai. Mộc mở cửa từ 6:30 đến 22:00, cà phê muối giá 35.000đ, còn đơn 1041 thì đang pha chế chứ chưa giao.

Kết quả mẫu ở trên do mình dựng để minh họa một hành vi mà các nhà cung cấp LLM đều cảnh báo, gọi là hallucination, tức mô hình sinh ra thông tin nghe hợp lý nhưng không đúng. Tài liệu Reduce hallucinations của Anthropic nói thẳng rằng kể cả mô hình mạnh nhất đôi khi cũng tạo ra nội dung sai hoặc mâu thuẫn với ngữ cảnh. Với mô hình thật, câu trả lời có thể là đoán, cũng có thể là một lời từ chối lịch sự. Bạn không đoán trước được, và đó chính là vấn đề.

Ba câu đầu sai vì mô hình chưa từng biết quán Mộc. Câu thứ tư thì nguy hiểm hơn hẳn. Khách đọc "đã giao rồi", ra cửa không thấy gì, rồi nhắn lại với giọng bực bội, và bây giờ chủ quán phải xử lý thêm một lời hứa sai do chính chatbot của quán đưa ra.

Đưa thông tin của quán vào prompt

Cách sửa đầu tiên khá hiển nhiên là đưa thông tin của quán vào system prompt. Mình tách dữ liệu ra một file riêng, để khi quán đổi giá thì chỉ sửa một chỗ:

src/thong-tin-quan.ts
export const MENU: Record<string, number> = {
  "cà phê đen": 25000,
  "cà phê sữa": 29000,
  "bạc xỉu": 32000,
  "cà phê muối": 35000,
  "bánh mì trứng": 25000,
};

export const GIO_MO_CUA = "Mộc mở cửa từ 6:30 đến 22:00 mỗi ngày.";
export const PHI_SHIP = "Mộc giao trong bán kính 5 km, phí 15.000đ; đơn từ 150.000đ được miễn phí.";

// Biến menu thành các dòng "- cà phê muối: 35.000đ" để đưa vào prompt.
export function bangGia(): string {
  return Object.entries(MENU)
    .map(([mon, gia]) => `- ${mon}: ${gia.toLocaleString("vi-VN")}đ`)
    .join("\n");
}

Rồi file prompts.ts chứa cả hai bản prompt. Bản 2 ghép thông tin của quán vào bằng template string:

src/prompts.ts
import { GIO_MO_CUA, PHI_SHIP, bangGia } from "./thong-tin-quan.ts";

// Bản 1: chỉ nói mô hình là ai.
export const SYSTEM_V1 = "Bạn là nhân viên chăm sóc khách hàng của quán cà phê Mộc. Trả lời ngắn gọn, thân thiện.";

// Bản 2: có thông tin của quán, và được phép nói "chưa tra được".
export const SYSTEM_V2 = `Bạn là nhân viên chăm sóc khách hàng của quán cà phê Mộc. Trả lời ngắn gọn, thân thiện, bằng tiếng Việt.

Chỉ dùng thông tin trong thẻ <thong_tin_quan> để trả lời.
<thong_tin_quan>
${GIO_MO_CUA}
${PHI_SHIP}
Bảng giá:
${bangGia()}
</thong_tin_quan>

Bạn KHÔNG xem được đơn hàng của khách. Nếu khách hỏi về một đơn hàng (đã giao chưa, khi nào tới, đổi món, hủy đơn),
đừng đoán trạng thái. Hãy nói rằng bạn chưa tra được đơn và nhân viên của quán sẽ kiểm tra rồi nhắn lại.
Nếu câu hỏi không có trong thông tin trên, hãy nói là bạn chưa có thông tin đó.`;

Prompt này làm ba việc, và mỗi việc có lý do riêng.

Việc đầu tiên là đặt thông tin vào thẻ <thong_tin_quan>. Thẻ kiểu XML giúp mô hình tách bạch đâu là lời dặn, đâu là dữ liệu.

Việc thứ hai là câu "chỉ dùng thông tin trong thẻ". Nếu thiếu câu này, mô hình có thể trộn thông tin của quán với kiến thức chung về quán cà phê, và bạn lại nhận về những câu kiểu "phí ship tùy khoảng cách".

Việc thứ ba là điều mình thấy hay bị bỏ qua nhất: cho phép mô hình nói "chưa biết". Trong tài liệu ở trên, đây là kỹ thuật đầu tiên Anthropic liệt kê để giảm hallucination, kèm nhận xét rằng chỉ riêng việc cho phép thừa nhận không chắc chắn đã có thể giảm thông tin sai rất nhiều. Lý do nằm ở cách ta viết prompt. Khi bạn chỉ dặn "trả lời thân thiện", câu "dạ đơn của bạn đã giao rồi ạ" trông thân thiện hơn hẳn câu "mình không biết". Bạn phải nói rõ rằng không biết thì nói không biết, và nói luôn câu thay thế là gì.

Giờ đã đủ hai bản prompt, file chạy thử chỉ cần hỏi lần lượt bốn câu và in câu trả lời. Tham số v1 hoặc v2 trên dòng lệnh chọn bản prompt. Với bản 2, câu trả lời còn đi qua hàm chanTrangThaiBia, một lớp kiểm tra mình viết ở phần sau:

src/hoi-moc.ts
import { hoiLLM, MODEL } from "./goi-llm.ts";
import { chanTrangThaiBia } from "./kiem-tra-bia.ts";
import { SYSTEM_V1, SYSTEM_V2 } from "./prompts.ts";

const CAU_HOI = [
  "Quán mở cửa mấy giờ vậy ạ?",
  "co ship qua quan 7 ko shop",
  "Giá cà phê muối bao nhiêu?",
  "Đơn 1041 của em giao chưa ạ?",
];

// node src/hoi-moc.ts v1  hoặc  node src/hoi-moc.ts v2
const banIn = process.argv[2] === "v1" ? "v1" : "v2";
const system = banIn === "v1" ? SYSTEM_V1 : SYSTEM_V2;
console.log(`Gọi ${MODEL}, system prompt ${banIn}\n`);

for (const cauHoi of CAU_HOI) {
  const ketQua = await hoiLLM(system, cauHoi);
  const cuoi = banIn === "v2" ? chanTrangThaiBia(ketQua.text) : { text: ketQua.text, daChan: false };
  console.log(`Khách: ${cauHoi}`);
  console.log(`Mộc:   ${cuoi.text}${cuoi.daChan ? "   [đã chặn một câu đoán trạng thái đơn]" : ""}`);
  console.log(`       (stop_reason: ${ketQua.stopReason}, token vào/ra: ${ketQua.inputTokens}/${ketQua.outputTokens})\n`);
}

Chạy với bản 2, câu trả lời sẽ có dạng như sau. Mình bỏ dòng stop_reason và số token cho dễ đọc, vì kết quả mẫu không có số liệu thật:

node src/hoi-moc.ts v2 (kết quả mẫu, không phải mô hình thật)
Khách: Quán mở cửa mấy giờ vậy ạ?
Mộc:   Mộc mở cửa từ 6:30 đến 22:00 mỗi ngày.

Khách: co ship qua quan 7 ko shop
Mộc:   Mộc giao trong bán kính 5 km, phí 15.000đ; đơn từ 150.000đ được miễn phí.

Khách: Giá cà phê muối bao nhiêu?
Mộc:   Dạ cà phê muối: 35.000đ ạ.

Khách: Đơn 1041 của em giao chưa ạ?
Mộc:   Dạ Mộc chưa tra được đơn của bạn ở đây ạ. Bạn chờ chút, nhân viên của quán sẽ kiểm tra rồi nhắn lại ngay nhé!

Ba câu đầu đã đúng với thông tin của quán. Câu thứ tư không còn bịa nữa, chatbot thừa nhận mình chưa tra được và hứa đúng điều quán làm được. Câu "co ship qua quan 7 ko shop" viết không dấu vẫn được hiểu, đây là thứ mà bộ luật dò từ khóa ở bài trước phải vá mãi mới được.

Mô hình thật sự nhìn thấy gì trong request?

Chỗ này mình muốn bạn dừng lại một chút, vì nó giải thích vì sao câu thứ tư không thể trả lời đúng, dù prompt có viết khéo đến đâu.

Một lần gọi API là một gói dữ liệu gửi đi. Mô hình chỉ biết những gì nằm trong gói đó, cộng với kiến thức chung nó học từ trước. Để thấy tận mắt, mình viết một file nhỏ dựng đúng request mà hoiLLM sẽ gửi khi khách hỏi về đơn 1041, nhưng không gửi đi đâu cả, nên chạy được không cần key. File in ra ba điều: request gồm những phần nào, system prompt có mã đơn hàng nào không, và con số 1041 xuất hiện bao nhiêu lần:

src/xem-request.ts
import { MODEL } from "./goi-llm.ts";
import { SYSTEM_V2 } from "./prompts.ts";

const request = {
  model: MODEL,
  max_tokens: 16000,
  system: SYSTEM_V2,
  messages: [{ role: "user", content: "Đơn 1041 của em giao chưa ạ?" }],
};

const json = JSON.stringify(request);
console.log(`Request dài ${json.length} ký tự, gồm: ${Object.keys(request).join(", ")}.`);
const maDon = SYSTEM_V2.match(/\b\d{4}\b/g) ?? [];
console.log(`System prompt có mã đơn hàng nào (số 4 chữ số) không? ${maDon.length ? maDon.join(", ") : "không"}`);
console.log(`Thông tin về đơn 1041 chỉ nằm ở: ${json.split("1041").length - 1} chỗ, là câu hỏi của khách.`);
node src/xem-request.ts
Request dài 878 ký tự, gồm: model, max_tokens, system, messages.
System prompt có mã đơn hàng nào (số 4 chữ số) không? không
Thông tin về đơn 1041 chỉ nằm ở: 1 chỗ, là câu hỏi của khách.

Cả request chỉ có 878 ký tự, và con số 1041 xuất hiện đúng một lần, trong câu hỏi của khách. Không có trạng thái đơn, không có giờ đặt, không có món. Vì vậy, khi mô hình trả lời "đơn 1041 đã giao", câu đó không thể đến từ dữ liệu, nó chỉ có thể là đoán.

Theo mình, đây là bài học quan trọng nhất của cả bài. Mô hình không kết nối với hệ thống của quán. Nó không tự đi xem database, không tự mở file orders.json. Mọi thông tin về đơn hàng đều phải do code của bạn lấy ra rồi đưa vào request. Hiểu điều này rồi thì bạn cũng hiểu luôn vì sao "viết prompt khéo hơn" không bao giờ là cách sửa cho câu hỏi về dữ liệu thay đổi liên tục như trạng thái đơn.

Có nên chỉ tin vào lời dặn trong prompt?

Lời dặn "đừng đoán trạng thái" giảm được rất nhiều câu bịa, nhưng tài liệu của Anthropic cũng ghi chú rằng các kỹ thuật này giảm đáng kể chứ không loại bỏ hoàn toàn hallucination. Với một câu trả lời có thể làm khách ra cửa đứng chờ, mình muốn thêm một lớp kiểm tra bằng code.

Lớp này dựa trên một sự thật đơn giản: chatbot ở bài này không xem được đơn hàng. Vậy thì bất kỳ câu trả lời nào khẳng định trạng thái đơn đều là đoán, và có thể chặn bằng một biểu thức chính quy. Hàm chanTrangThaiBia nhận câu trả lời của mô hình. Nếu câu đó nói đơn "đã giao", "đang giao" hay "đang pha", hàm thay nó bằng một câu an toàn, còn không thì giữ nguyên:

src/kiem-tra-bia.ts
const KHANG_DINH_TRANG_THAI = /đã (được )?giao|đang giao|đang pha|giao xong/i;

export const CAU_AN_TOAN =
  "Dạ Mộc chưa tra được đơn của bạn ở đây ạ. Nhân viên của quán sẽ kiểm tra rồi nhắn lại cho bạn nhé!";

export function chanTrangThaiBia(traLoi: string): { text: string; daChan: boolean } {
  if (KHANG_DINH_TRANG_THAI.test(traLoi)) return { text: CAU_AN_TOAN, daChan: true };
  return { text: traLoi, daChan: false };
}

Mình viết bốn test cho hàm này. Hai test đầu đưa vào câu đoán trạng thái đơn và chờ bị chặn. Hai test sau đưa vào câu trả lời đúng về giờ mở cửa và phí ship, và chờ được giữ nguyên:

src/kiem-tra-bia.test.ts
import assert from "node:assert/strict";
import { test } from "node:test";

import { CAU_AN_TOAN, chanTrangThaiBia } from "./kiem-tra-bia.ts";

test("chặn câu khẳng định đơn đã giao", () => {
  assert.equal(chanTrangThaiBia("Dạ đơn 1041 đã được giao rồi ạ").text, CAU_AN_TOAN);
});

test("chặn câu nói đơn đang giao", () => {
  assert.equal(chanTrangThaiBia("Đơn của bạn đang giao, 10 phút nữa tới ạ").daChan, true);
});

test("giữ nguyên câu trả lời về giờ mở cửa", () => {
  const traLoi = "Mộc mở cửa từ 6:30 đến 22:00 mỗi ngày.";
  assert.deepEqual(chanTrangThaiBia(traLoi), { text: traLoi, daChan: false });
});

test("giữ nguyên câu trả lời về phí giao hàng", () => {
  assert.equal(chanTrangThaiBia("Mộc giao trong bán kính 5 km, phí 15.000đ.").daChan, false);
});
node src/kiem-tra-bia.test.ts
ok 1 - chặn câu khẳng định đơn đã giao
ok 2 - chặn câu nói đơn đang giao
ok 3 - giữ nguyên câu trả lời về giờ mở cửa
ok 4 - giữ nguyên câu trả lời về phí giao hàng
# tests 4
# pass 4
# fail 0

Hai test cuối quan trọng không kém hai test đầu. Câu về phí ship có chữ "giao" ("Mộc giao trong bán kính 5 km"), và nếu regex viết ẩu thành /giao/ thì chatbot sẽ chặn luôn câu trả lời đúng. Bạn còn nhớ ở bài trước chữ "giao" đã làm chatbot dò từ khóa trả lời sai chưa? Lần này nó suýt làm hỏng lớp bảo vệ.

Mình cũng thừa nhận lớp kiểm tra này thô. Nó chặn theo chữ chứ không hiểu câu, nên một câu như "đơn của bạn sắp tới rồi" vẫn lọt qua. Nhưng một lưới thô mà có test vẫn hơn hẳn không có lưới.

Mỗi lần gọi tốn bao nhiêu và chậm cỡ nào?

Mỗi câu trả lời trả về kèm usage, gồm số token đầu vào và đầu ra. File hoi-moc.ts in hai con số này dưới mỗi câu trả lời khi chạy với key thật. Bạn nên tập thói quen nhìn chúng ngay từ bây giờ, vì giá API tính theo token.

Với chatbot của bài này, phần lớn token đầu vào là system prompt, và nó lặp lại nguyên văn ở mọi tin nhắn. 200 tin nhắn mỗi ngày nghĩa là gửi cùng một bảng giá 200 lần. Nghe thì lãng phí, và đúng là lãng phí, nhưng đây là cách mọi API LLM hoạt động: mỗi request độc lập, mô hình không nhớ gì giữa hai lần gọi. Có những kỹ thuật như lưu đệm prompt để giảm chi phí phần lặp lại này, và các bài viết sau của series sẽ đo thật trước khi tối ưu.

Còn về tốc độ, mình chưa đưa con số vì chưa đo với key thật, và con số đó thay đổi theo model, theo giờ, theo mạng. Khi có key, bạn bọc lời gọi bằng console.time để tự đo nhé.

Lỗi khi gọi API thì xử lý thế nào?

Gọi API qua mạng thì sớm muộn gì cũng lỗi. SDK có sẵn các lớp lỗi riêng cho từng trường hợp, nên bạn không phải đọc chuỗi thông báo để đoán. Đó là việc của khối catch ở cuối hàm hoiLLM trong file goi-llm.ts phía trên. Nó bắt riêng hai lỗi hay gặp nhất là AuthenticationError (key sai) và RateLimitError (gọi quá nhanh), còn lỗi khác thì ném tiếp ra ngoài.

Hai lỗi này cần phân biệt vì cách xử lý ngược nhau. Lỗi xác thực thì thử lại bao nhiêu lần cũng vậy, phải sửa key. Lỗi gọi quá nhanh thì chờ một lúc là được, và SDK đã tự thử lại vài lần trước khi ném lỗi ra cho bạn. Tùy chọn cause giữ lại lỗi gốc, đúng kỹ thuật trong bài đọc stack trace của series Nhận dự án cũ, để khi cần debug bạn vẫn thấy được chuyện gì đã xảy ra.

Nếu bạn chạy hoi-moc.ts mà chưa đặt ANTHROPIC_API_KEY, chương trình dừng ngay ở câu hỏi đầu tiên, trước khi có request nào được gửi đi:

node src/hoi-moc.ts v2 (chưa đặt API key)
Gọi claude-opus-5-5, system prompt v2

Error: Could not resolve authentication method. Expected one of apiKey, authToken, credentials, config, or profile to be set. Or for the "Authorization" header to be explicitly omitted

Lỗi này không phải AuthenticationError, vì máy chủ chưa hề được hỏi tới. SDK tự phát hiện là không có cách xác thực nào, nên khối catch của mình cho nó đi tiếp ra ngoài nguyên văn. Trong lúc chưa có key, bạn vẫn chạy được xem-request.ts và file test, vì hai file này không gọi mạng.

Chatbot đã thôi bịa, nhưng vẫn chưa giúp được khách

Nhìn lại câu trả lời cho đơn 1041: "nhân viên của quán sẽ kiểm tra rồi nhắn lại". Câu này trung thực, nhưng chủ quán vẫn phải tự mở sổ, tìm đơn, nhắn lại cho khách. Trong khi đó, thông tin đơn 1041 đang nằm ngay trong hệ thống của quán, chỉ cách chatbot một lời gọi hàm.

Vậy làm sao để mô hình tự nói "mình cần xem đơn 1041", để code của bạn tra đơn thật rồi đưa kết quả cho mô hình viết câu trả lời? Kỹ thuật đó gọi là tool calling, và đó là bước biến chatbot này thành nền móng của một agent. Bài viết tiếp theo của series sẽ làm đúng việc đó. Bạn theo dõi cả series ở Xây AI agent đầu tiên, và nếu đang ôn phỏng vấn thì câu hỏi về hallucination có trong ngân hàng câu hỏi phỏng vấn.

Nhận bài tiếp theo qua email.

Một email mỗi tuần, chỉ khi có điều đáng đọc. Không spam.