CodeGym /Các khóa học /ChatGPT Apps /Lỗi, tính idempotent và thiết kế công cụ “an toàn”

Lỗi, tính idempotent và thiết kế công cụ “an toàn”

ChatGPT Apps
Mức độ , Bài học
Có sẵn

1. Lỗi và tính idempotent trong ChatGPT App

Trong web cổ điển, nhiều người vẫn sống trong mô hình “người dùng nhấn nút → một yêu cầu HTTP → một phản hồi”. Trong thế giới LLM thì không còn như vậy nữa. Mô hình có thể quyết định gọi tool của bạn nhiều lần, có thể tạo lại câu trả lời sau khi người dùng bấm Regenerate, có thể hỏi lại, hoặc gặp lỗi mạng trên đường đi. Kết quả là cùng một công cụ hoàn toàn có thể bị gọi hai hoặc ba lần với các tham số rất giống nhau.

Đồng thời, bỗng dưng mỗi lỗi lại có hai “khách hàng”. Một mặt là mô hình, cần một giải thích có thể đọc bằng máy, rõ ràng về việc đã sai ở đâu để nó sửa tham số và thử lại. Mặt khác là UI của người dùng (widget và chính chat), nơi cần hiển thị một thông điệp thân thiện và gợi ý bước tiếp theo, chứ không phải “Error: 500 (see logs)”.

Một điểm quan trọng nữa: kiến trúc cổ điển hiếm khi giả định rằng ai đó sẽ ấn “lặp lại câu trả lời” hàng loạt và do đó tăng số lần gọi lại (retry). Trong ChatGPT, kịch bản này là bình thường. Thêm vào đó, nền tảng tự thân có thể thực hiện gọi lại khi gặp vấn đề mạng tạm thời. Vì vậy, khái niệm idempotent trong hệ sinh thái này không phải là tùy chọn bổ sung, mà là yêu cầu cơ bản, đặc biệt với các công cụ “làm điều gì đó thật sự” — tạo đơn hàng, trừ tiền, gửi email, v.v.

Bài giảng này chính là về cách không để một lần gọi tool thất bại phá hỏng tâm trạng người dùng và phá hỏng production của bạn.

Insight

ChatGPT không truyền tham số vào hàm của bạn theo nghĩa đen, mà đúng hơn là ước đoán bộ tham số theo schema của bạn. Nó nhìn vào JSON Schema, ngữ cảnh hội thoại và chọn giá trị theo thống kê — và khá thường xuyên đoán sai. Những lỗi như “sai kiểu”, “quên trường bắt buộc”, “tham số mâu thuẫn” — là phần bình thường của cuộc sống tool-call, không phải bất khả kháng. Theo dữ liệu mở và telemetry, các cú trượt như vậy dễ dàng chiếm tới ~30% lượt gọi với các schema phức tạp.

Với mô hình thì không sao: nó coi phản hồi của bạn như tín hiệu “tham số chưa đúng” và đơn giản thử lại, có thể 2–3 lần liên tiếp, hơi thay đổi đầu vào. Với bạn, điều đó có nghĩa khác: mỗi công cụ phải được thiết kế như thể gần như chắc chắn sẽ bị gọi nhiều lần với các tham số rất giống nhau.

Đó là lý do vì sao tính idempotent lại quan trọng đến vậy. ChatGPT sẽ thử hết lần này đến lần khác để đoán nên gọi hàm của bạn với tham số nào. 2–3 lần thử cho mỗi lần gọi là bình thường.

2. Cấu hình widget an toàn: text/html+skybridge_meta

Trước khi đi vào các câu chuyện thuần máy chủ (lỗi, retry, idempotent), hãy chốt một điểm riêng của Apps SDK về bảo mật UI: làm thế nào để widget của bạn render an toàn trong chat, chứ không như “một trang web đáng sợ từ Internet”.

registerResource và MIME type text/html+skybridge

Widget của bạn theo quan điểm ChatGPT là một tài nguyên HTML đặc biệt, được đưa vào sandbox phía client ChatGPT chứ không trực tiếp vào trình duyệt người dùng. Để nền tảng hiểu rằng đây là widget chứ không chỉ là HTML, dùng MIME type text/html+skybridge.

Ở cấp MCP/máy chủ, bạn đăng ký resource bằng thứ gì đó như (pseudo-TS):

// đâu đó trong cấu hình máy chủ MCP
registerResource({
  name: "giftgenius-widget",
  path: "/widget",
  mimeType: "text/html+skybridge", // quan trọng!
});

mimeType này là tín hiệu cho client ChatGPT: “đây không chỉ là HTML, mà là template dạng component cho widget nhúng, cần chạy trong môi trường cách ly”. Nếu chỉ định text/html thông thường, nền tảng có thể hiển thị HTML thô hoặc thậm chí từ chối render.

_meta và kiểm soát an toàn: CSP, domain và khung viền

Tiếp theo là metadata được gửi kèm với phản hồi của tool hoặc resource — _meta. Thông qua đó bạn kiểm soát widget có thể tải tài nguyên ngoài từ đâu, nó hiển thị ra sao và thậm chí mô hình sẽ mô tả nó như thế nào.

Ví dụ cấu trúc thường gặp:

const toolResult = {
  content: "<!-- HTML của widget -->",
  _meta: {
    "openai/widgetCSP": "default-src 'self'; img-src https://cdn.example.com",
    "openai/widgetDomain": "https://chatgpt.com",
    "openai/widgetPrefersBorder": true,
    "openai/widgetDescription": "GiftGenius hiển thị gợi ý quà tặng dưới dạng thẻ."
  }
};

Phân tích các trường chính.

  • openai/widgetCSP thiết lập Content Security Policy cho widget. Đây là “tường lửa nhỏ” cho trình duyệt bên trong ChatGPT: bạn liệt kê rõ ràng được phép tải script, style, ảnh, XHR từ đâu, v.v. Nền tảng kỳ vọng chính sách chặt chẽ, không dùng wildcard *; bạn cần chỉ rõ các domain dùng (chat, API của bạn, CDN).
  • openai/widgetDomain đặt origin trong ngữ cảnh mà widget sẽ hoạt động. Thông thường đây là domain của ChatGPT; bạn không thay thế nó bằng site của bạn, mà chỉ thông báo cách nó nên trông như thế trong môi trường cách ly.
  • openai/widgetPrefersBorder — cờ thuần hiển thị: có vẽ khung viền quanh widget hay không. Với GiftGenius, giữ khung viền là hợp lý để tách khối gợi ý khỏi các tin nhắn thông thường trong chat.
  • openai/widgetDescription — mô tả dạng text dành cho mô hình. Thay vì tự “bịa” lời giải thích, mô hình có thể dùng chuỗi này khi nói với người dùng về giao diện vừa mở. Điều này giảm rủi ro các bình luận lạ hoặc rườm rà của mô hình.

Kết luận thực tiễn: chỉ cần cấu hình cẩn thận một lần mimeType_meta, bạn sẽ có một UI an toàn, cách ly, không “đi lung tung”, và hành xử dự đoán được cả với người dùng lẫn nền tảng. Phần bảo mật frontend đã rõ: widget sống trong sandbox và chỉ đi đến nơi bạn cho phép. Giờ chuyển sang phía server — các loại lỗi và cách mô tả chúng, cũng như làm tool idempotent.

Insight: bộ nhớ đệm của widget

ChatGPT cache HTML của widget tại thời điểm đăng ký ứng dụng. HTML widget của ChatGPT không phải “frontend sống”, mà là một artefact build cố định. Khi xuất bản ứng dụng (Store hoặc Dev Mode), nền tảng đọc resource HTML (text/html+skybridge) và sau đó luôn dùng đúng phiên bản đó. Bất kỳ thay đổi nào — dù chỉ một dòng chữ hay khoảng cách trong thẻ — trên thực tế đồng nghĩa một bản phát hành mới.

Suy ra: chỉnh sửa cấu trúc HTML, slot, thuộc tính data-* và hợp đồng structuredContentDOM — không phải “quick fix”, mà là một cuộc di trú frontend đầy đủ. Nếu hôm nay bạn render danh sách từ items[], còn ngày mai chuyển sang results[], widget cũ sẽ không biết: nó sẽ tiếp tục nhận JSON cũ và hoạt động không đúng.

3. Các loại lỗi khi vận hành tool

Giờ vào phần “thịt”: có những loại lỗi nào cho tool và chúng khác nhau ra sao xét trên góc độ UX và backend. Thuận tiện nhất là nghĩ theo bốn lớp lỗi.

Lỗi xác thực đầu vào

Mức cơ bản nhất — khi tham số đầu vào không khớp hợp đồng.

Ví dụ với ứng dụng học tập GiftGenius và tool suggest_gifts (chọn quà theo sở thích và ngân sách):

  • tuổi nhỏ hơn 0 hoặc lớn hơn 120;
  • ngân sách âm;
  • thiếu trường bắt buộc relationship_type;
  • budget_min > budget_max.

Trường hợp JSON không phù hợp schema cũng nằm ở đây. Lý tưởng là Apps SDK và JSON Schema sẽ lọc các lượt gọi “quá tệ” trước khi đến code của bạn, nhưng phần xác thực nghiệp vụ (như quan hệ giữa budget_min/budget_max) thì vẫn phải tự làm.

Lỗi logic nghiệp vụ

Ở đây đầu vào có vẻ đúng, nhưng theo luật nghiệp vụ bạn không thể trả về kết quả tốt.

Các tình huống điển hình:

  • với sở thích và ngân sách đã cho, bạn không tìm được món quà nào;
  • người dùng vượt hạn mức số lần gợi ý trong ngày;
  • món hàng mà mô hình muốn mua không còn bán nữa.

Đây không phải “máy chủ hỏng”, mà là các tình huống dự kiến, cần trình bày cho người dùng và mô hình ở dạng dễ hiểu, chứ không phải 500 Internal Server Error.

Lỗi hạ tầng bên ngoài

Lớp này là “địa ngục kỹ thuật”: DB không truy cập được, API bên ngoài timeout, trong code của bạn ném ra exception chưa bắt.

Ví dụ:

  • yêu cầu tới danh mục quà tặng trả về 503 hoặc không phản hồi;
  • MongoDB bất chợt “tạm dừng”;
  • trong code lọc quà, bạn chia cho 0.

Về mặt UX, thường sẽ nói: “Dịch vụ tạm thời không khả dụng, vui lòng thử lại sau”, đôi khi — thử retry ẩn. Nhưng quan trọng là đừng im lặng và đừng hiển thị stack trace thô cho người dùng.

Lỗi nền tảng/mạng

Cuối cùng, có lớp lỗi xảy ra ngoài code của bạn: tool-call không tới đích, kết nối đứt giữa chừng khi trả lời, kịch bản streaming bị ngắt. Chuyện này xảy ra thường xuyên hơn bạn nghĩ. Ví dụ, nếu dùng đường hầm miễn phí, vào giờ cao điểm tốc độ giảm đến mức các tool call của ChatGPT rớt vì timeout.

Bạn không thể kiểm soát hoàn toàn, nhưng chắc chắn có thể thiết kế tool và widget sao cho các lần gọi lặp và gián đoạn không biến hệ thống thành hỗn loạn. Đó là lý do chúng ta nói về idempotent và xử lý lỗi cẩn thận, chứ không chỉ “try/catch là xong”.

4. Cách mô tả và trả về lỗi: cho mô hình lẫn UI

Một thay đổi tư duy quan trọng: lỗi của bạn không chỉ là thứ bạn ghi log trong console.error. Đó là một phần của hợp đồng tool, mà cả mô hình và giao diện sẽ làm việc với nó.

Cấu trúc lỗi

Thường tiện nhất là tuân theo một cấu trúc đơn giản:

type ToolError = {
  code: string;        // "VALIDATION_ERROR", "NO_RESULTS", "UPSTREAM_TIMEOUT"
  message: string;     // dễ đọc cho người dùng hoặc gọn cho mô hình
  retryable: boolean;  // có nên thử lại hay không
};

Và kết quả của tool có thể gói thành union phân biệt:

type SuggestGiftsResult =
  | { ok: true; gifts: GiftCard[] }
  | { ok: false; error: ToolError };

Trong giao thức MCP còn có cờ riêng “đây là lỗi”, nhưng nội bộ vẫn hữu ích khi tuân theo định dạng của chính bạn, để UI và mô hình có thể diễn giải giống nhau về những gì đã xảy ra.

Chiến lược “fail gracefully”

Không phải tình huống khó chịu nào cũng cần đóng khung thành lỗi “cứng”. Đôi khi trả về kết quả rỗng nhưng không có lỗi, kèm lời giải thích, lại hữu ích hơn nhiều.

Ví dụ, nếu không tìm thấy món quà nào, hợp lý là trả về ok: true, mảng rỗng gifts: [] và một trường noResultsReason cho UI và mô hình, thay vì một lỗi "NO_RESULTS". Khi đó mô hình có thể tiếp tục đối thoại: “Mình không tìm thấy trong mức ngân sách này, bạn có muốn tăng ngân sách hoặc làm rõ sở thích không?”.

Còn nếu API bên ngoài sập hẳn, đó có lẽ là ok: false với code: "UPSTREAM_UNAVAILABLE"retryable: true, để mô hình có cơ hội thử lại sau hoặc với tham số khác.

Nhắc lại, ở mục 3 ta có bốn lớp lỗi. Lỗi xác thực thường là ok: falseretryable: false — mô hình không nên lặp lại cùng lượt gọi với cùng tham số. Các tình huống nghiệp vụ như “không tìm thấy gì” thường đóng gói dưới dạng ok: true với kết quả rỗng và lời giải thích. Sự cố hạ tầng của dịch vụ bên ngoài — ok: false với retryable: true, để mô hình có thể thử lại an toàn. Còn lỗi nền tảng/mạng có thể xảy ra trước hoặc sau code của bạn và trên thực tế thường thể hiện dưới dạng một lượt gọi tool lặp lại — đó là lý do tính idempotent cẩn trọng rất quan trọng, sẽ nói tiếp ngay sau đây.

Đừng để lộ chi tiết nội bộ

Trong code server, rất dễ “tiện tay” đẩy thẳng error.toString() vào phản hồi. Với các tool cho LLM, đây không phải ý hay: bạn sẽ có rác trong hội thoại và có thể lộ thông tin nhạy cảm (URL dịch vụ nội bộ, stack trace, tên bảng). Khuyến nghị — bắt exception và chuyển chúng thành mã lỗi gọn và thông điệp chỉnh chu.

Ví dụ một lớp bọc tối thiểu:

try {
  const gifts = await loadGiftsFromCatalog(input);
  return { ok: true, gifts };
} catch (err) {
  console.error("suggest_gifts failed", err);
  return {
    ok: false,
    error: {
      code: "UPSTREAM_ERROR",
      message: "Catalog service is unavailable",
      retryable: true
    }
  };
}

Mô hình nhận tín hiệu gọn gàng, UI — đoạn text dễ hiểu, còn chi tiết ở lại trong log.

Hiển thị lỗi trong widget

Ở góc nhìn React widget, bài toán đơn giản: kiểm tra ok, nếu là false, hiển thị thông điệp thân thiện và nếu có thể, gợi ý cách tiếp tục.

function GiftResults({ result }: { result: SuggestGiftsResult }) {
  if (!result.ok) {
    return (
      <div>
        <p>Không thể chọn quà: {result.error.message}</p>
        {result.error.retryable && <p>Hãy thử thay đổi tham số hoặc thử lại yêu cầu.</p>}
      </div>
    );
  }

  if (result.gifts.length === 0) {
    return <p>Không tìm thấy quà với các điều kiện này. Hãy thử thay đổi ngân sách hoặc sở thích.</p>;
  }

  return <GiftCardsList gifts={result.gifts} />;
}

Đây chính là trường hợp mà một thông điệp đơn giản, thẳng thắn giúp UX dễ chịu hơn nhiều so với “đã có lỗi xảy ra”.

Ta đã thống nhất rằng một phần lỗi có thể được đánh dấu thẳng là retryable: true và đề nghị người dùng “thử lại”. Khi trong hệ thống xuất hiện các retry (rõ ràng ở UI hoặc ẩn ở phía nền tảng), câu hỏi tiếp theo là: chuyện gì sẽ xảy ra nếu cùng một tool được gọi hai lần với cùng dữ liệu? Đây là câu chuyện về idempotent.

5. Idempotent: bảo vệ khỏi “một lượt gọi nữa giống hệt”

Giờ đến phần thú vị nhất. Chính xác thì idempotent là thuộc tính mà việc gọi lặp với dữ liệu đầu vào như nhau không làm thay đổi trạng thái hệ thống và kết quả. Ở nghĩa nghiêm ngặt, nó vừa nói tới việc không có tác dụng phụ trùng lặp, vừa về phản hồi giống nhau. Trong thực tế ChatGPT Apps, điều ta quan tâm nhất là điều đầu: để các lượt gọi lặp không làm hỏng dữ liệu và không tạo thực thể mới, ngay cả khi chính phản hồi có thể hơi khác.

Trong bối cảnh ChatGPT Apps, idempotent là tấm khiên trước tất cả những gì xảy ra khi có retry, Regenerate và logic khó đoán của LLM.

Nơi idempotent đặc biệt quan trọng

Các tool chỉ đọc thường an toàn mặc định: gọi suggest_gifts bao nhiêu lần với cùng tham số, bạn cũng chỉ nhận thêm một danh sách quà. Dù có khác đôi chút, nó không làm đổi trạng thái hệ thống và không gây tác dụng phụ.

Những tool chỉnh sửa trạng thái hệ thống bên ngoài mới là thứ nhạy cảm:

  • tạo đơn hàng (create_order);
  • xử lý thanh toán (charge_card, submit_payment);
  • gửi email và thông báo (send_email, send_sms);
  • tạo thực thể có tác dụng phụ (ví dụ, đặt chỗ).

Nếu tool như vậy bị gọi hai lần liên tiếp với tham số gần như giống hệt, bạn có thể gặp đơn hàng trùng, trừ tiền hai lần, và những niềm “vui” kế toán khác.

Mẫu idempotency_key

Cách cổ điển: thêm một tham số bổ sung idempotency_key — định danh chuỗi của thao tác. Nếu yêu cầu với khóa này đã xử lý thành công, server không thực hiện lại hành động, mà trả về kết quả đã lưu.

Ví dụ schema mở rộng cho tool giả định create_checkout_session trong GiftGenius:

const CreateCheckoutSchema = {
  type: "object",
  properties: {
    giftId: {
      type: "string",
      description: "ID của món quà đã chọn"
    },
    idempotency_key: {
      type: "string",
      description: "Khóa duy nhất của thao tác để chống trùng lặp"
    }
  },
  required: ["giftId", "idempotency_key"]
} as const;

Trên server, handler làm đại khái như sau:

async function createCheckoutSession(input: CreateCheckoutInput) {
  const existing = await db.checkoutSessions.findOne({ idempotencyKey: input.idempotency_key });
  if (existing) {
    return existing; // trả về kết quả cũ
  }

  const session = await paymentProvider.createSession({ giftId: input.giftId });
  await db.checkoutSessions.insert({ idempotencyKey: input.idempotency_key, session });
  return session;
}

Nếu vì lý do nào đó mô hình gọi tool lần hai với cùng idempotency_key, người dùng sẽ không bị thanh toán lần nữa, mà chỉ thấy lại cùng checkout.

Tách preparecommit

Với các hành động đặc biệt nhạy cảm (thanh toán, thay đổi không thể đảo ngược) thường dùng cách hai pha: tool riêng cho bước chuẩn bị (prepare_*), tool riêng cho commit (commit_*).

Ví dụ:

  • prepare_order — kiểm tra tồn kho, tính chi phí, trả về “bản nháp đơn hàng”;
  • commit_order — dựa vào ID của nháp để tạo đơn hàng thật và khởi tạo thanh toán.

Thiết kế này cho vài lợi ích. Thứ nhất, có thể làm bước đầu hoàn toàn idempotent: gọi lại prepare_order với tham số như nhau sẽ trả cùng một nháp. Thứ hai, có thể chỉ cho phép gọi commit_order sau khi người dùng xác nhận rõ ràng — vừa tốt cho UX, vừa tốt cho bảo mật.

6. Thiết kế tool an toàn

Idempotent là điều kiện cần, nhưng không phải duy nhất cho an toàn. Thiết kế bộ tool bạn cung cấp cho mô hình cũng quyết định rất nhiều.

Nguyên tắc đặc quyền tối thiểu

Ý tưởng đơn giản: mỗi tool chỉ nên làm đúng thứ cần cho kịch bản, không hơn một dòng. Đừng có một hàm do_anything_with_user_account kiểu:

  • có thể đọc, cập nhật, xóa đủ thứ;
  • nhận một chuỗi operation và JSON payload một cách “hên xui”.

Tốt hơn là có các tool riêng, mô tả rõ ràng:

  • get_user_profile;
  • update_user_preferences;
  • create_order;
  • cancel_order.

Cũng chính logic đó cho GiftGenius: suggest_gifts chỉ gợi ý lựa chọn; create_checkout_session không biết gì về hủy đơn hay đổi email người dùng.

Tách công cụ “read” và “write”

Một mẫu hay — tách rõ các tool chỉ đọc dữ liệu và các tool có thay đổi. Truy vấn danh mục quà (search_products, suggest_gifts) tự thân an toàn, kể cả khi mô hình lạm dụng. Còn create_order hoặc charge_payment thì cần thận trọng hơn.

Trong phần mô tả của các tool như vậy, nên ghi rõ chúng làm gì và trong ngữ cảnh nào có thể gọi. Ví dụ:

{
  "name": "create_checkout_session",
  "description": "Tạo một phiên thanh toán cho một món quà. CHỈ gọi SAU KHI người dùng xác nhận rõ ràng lựa chọn của họ.",
  "parameters": { /* ... */ }
}

Đây không phải bảo vệ 100% (LLM vẫn có thể sai), nhưng ít nhất bạn đưa cho nó tín hiệu rõ ràng về rủi ro.

Human-in-the-loop và xác nhận

Với các hành động “nguy hiểm” thực sự, hãy xây dựng kịch bản có bước xác nhận. Ví dụ, mô hình:

  1. Trước tiên gọi tool để chuẩn bị dữ liệu mua và trả về ở dạng thuận tiện cho UI (tên quà, giá, địa chỉ giao hàng).
  2. Nền tảng hiển thị widget với nút “Xác nhận mua hàng”.
  3. Chỉ sau khi bấm nút mới gọi tool commit, tool này mới thực hiện thanh toán thật.

Như vậy, bạn không cho mô hình khả năng “lén” đặt hàng mà không có sự tham gia của người dùng, kể cả khi nó bỗng nghĩ đó là quyết định rất khôn ngoan.

Ngữ nghĩa rủi ro trong mô tả và chú thích

Trong một số phiên bản nền tảng có các chú thích như destructiveHint, báo hiệu tool có thể làm hành động không thể đảo ngược. Dù có hay chưa ổn định các trường như vậy, bạn vẫn có thể truyền tải ngữ nghĩa này ngay trong description và tên tham số.

Ví dụ, thay vì:

{
  "name": "delete_user_data",
  "description": "Xóa dữ liệu người dùng."
}

hãy làm:

{
  "name": "request_user_data_deletion",
  "description": "Đánh dấu tài khoản người dùng để xóa dữ liệu cá nhân theo chính sách dịch vụ. CHỈ sử dụng SAU KHI người dùng đã yêu cầu xóa một cách rõ ràng."
}

Và đồng thời xây dựng một UX xác nhận thân thiện xung quanh đó.

7. Một cải tiến thực hành nhỏ cho GiftGenius

Hãy liên hệ tất cả điều này với ứng dụng học tập GiftGenius — App gợi ý quà tặng. Giả sử chúng ta thêm một tool nữa — create_checkout_session, để người dùng không chỉ chọn quà mà còn có thể chuyển sang thanh toán.

Về JSON Schema và an toàn, ta làm như sau.

Trước hết, thêm idempotency_key và mô tả cẩn thận:

const CreateCheckoutTool = {
  name: "create_checkout_session",
  description:
    "Tạo một phiên thanh toán cho một món quà đã chọn. " +
    "Chỉ gọi SAU KHI người dùng xác nhận rằng họ muốn mua món quà này.",
  parameters: {
    type: "object",
    properties: {
      gift_id: {
        type: "string",
        description: "Định danh quà tặng từ kết quả suggest_gifts."
      },
      idempotency_key: {
        type: "string",
        description: "Khóa duy nhất của thao tác. Dùng cùng một khóa khi gọi lại."
      }
    },
    required: ["gift_id", "idempotency_key"]
  }
} as const;

Thứ hai, trên server triển khai handler idempotent:

async function handleCreateCheckout(input: CreateCheckoutInput) {
  const existing = await db.checkout.findOne({ idempotencyKey: input.idempotency_key });
  if (existing) {
    return { ok: true, checkout: existing };
  }

  const checkout = await payments.createSession({ giftId: input.gift_id });
  await db.checkout.insert({ idempotencyKey: input.idempotency_key, ...checkout });

  return { ok: true, checkout };
}

Thứ ba, xử lý lỗi:

try {
  return await handleCreateCheckout(input);
} catch (err) {
  console.error("create_checkout_session failed", err);
  return {
    ok: false,
    error: {
      code: "PAYMENT_PROVIDER_ERROR",
      message: "Không thể tạo phiên thanh toán. Vui lòng thử lại sau.",
      retryable: true
    }
  };
}

Còn ở widget, hiển thị trạng thái lỗi dễ hiểu và, nếu phù hợp, một nút “Thử lại” ở tầng UI, nút này sẽ khởi đầu một hội thoại mới với mô hình.

Cứ thế, từng bước một, dự án học tập đáng yêu của ta sẽ thôi là “đồ chơi demo” và dần trở thành thứ mà về lý thuyết có thể đưa lên production.

8. Những sai lầm thường gặp khi xử lý lỗi và idempotent cho tool

Lỗi №1: Lỗi = chỉ throw và 500.
Nếu với mọi trục trặc, tool của bạn chỉ ném exception và biến thành “đã có lỗi xảy ra”, mô hình và UI đều thiếu thông tin. Mô hình không biết có nên lặp lại lượt gọi với tham số khác không, còn người dùng không biết cần làm gì tiếp theo. Tốt hơn nhiều là trả về lỗi có cấu trúc với mã, thông điệp ngắn và cờ retryable, còn chi tiết thì log trong server.

Lỗi №2: Không phân biệt các loại lỗi.
Trộn lẫn lỗi xác thực, lỗi nghiệp vụ và lỗi hạ tầng vào một nồi là ý tưởng tệ. Cuối cùng, tình huống “không tìm thấy gì” lại trông giống hệt “cơ sở dữ liệu sập” đối với mô hình và người dùng. Điều này làm hỏng UX và cản trở mô hình phản ứng phù hợp: thay vì gợi ý thay đổi truy vấn, nó sẽ chuyển sang chế độ “xin lỗi, dịch vụ hỏng”. Điều này đặc biệt đau khi bạn trộn, chẳng hạn, lỗi nghiệp vụ và lỗi hạ tầng như ở mục 3.

Lỗi №3: Thao tác không idempotent trong thế giới đầy retry.
Thiết kế tool create_order như thể nó luôn chỉ được gọi đúng một lần — là con đường thẳng tới đơn hàng trùng, nhất là khi người dùng bấm Regenerate liên tục hoặc kết nối đứt giữa chừng. Nếu tool có tác dụng phụ, gần như luôn nên thêm idempotency_key và lưu kết quả, để lượt gọi lặp không tạo thực thể mới.

Lỗi №4: Một tool “vạn năng” khổng lồ.
Đôi khi lập trình viên cố làm một super-tool với tham số action, có thể làm tất cả: tìm kiếm, tạo, sửa, xóa. Với LLM, đây gần như chắc chắn dẫn tới hành vi khó đoán: mô hình khó học khi nào gọi cái gì, và hậu quả lỗi trở nên nặng nề hơn. Đúng đắn là tách thành các tool nhỏ, mô tả rõ, càng read-only càng tốt, và riêng — các tool thay đổi trạng thái được thiết kế cẩn thận kèm xác nhận.

Lỗi №5: Rò rỉ chi tiết nội bộ vào phản hồi.
Ném ra mô hình và UI stack trace thô hoặc toàn bộ text exception — là thói quen lười biếng phổ biến. Điều này bất tiện với người dùng, có thể lộ cấu trúc nội bộ và không giúp mô hình sửa sai. Hãy bắt exception, ánh xạ chúng thành mã gọn và thông điệp đơn giản, còn mọi chi tiết để lại cho log và hệ thống giám sát.

Lỗi №6: Không liên kết lỗi với UX của widget.
Thường backend trả về mã lỗi rất cẩn thận, còn widget UI thì rơi vào spinner bất tận hoặc một khối trống. Người dùng thấy “chẳng có gì xảy ra”, mô hình thấy tool-call hoàn tất và tiếp tục hội thoại như chẳng có gì. Tốt hơn nhiều là thiết kế riêng các trạng thái errorempty, hiển thị thông điệp dễ hiểu và, nếu có thể, gợi ý hành động (đổi tham số, thử lại sau).

Lỗi №7: Bỏ qua nguyên tắc đặc quyền tối thiểu.
Dù bạn đã làm idempotent và xử lý lỗi tốt, nhưng lại mô tả một tool kiểu execute_sql_anywhere có thể làm mọi thứ, rủi ro vẫn cực lớn. LLM có thể gọi nó trong ngữ cảnh sai hoặc với tham số lỗi. Mỗi tool nên càng hẹp càng tốt và làm đúng một hành động dễ hiểu — đặc biệt khi liên quan đến tiền bạc hoặc dữ liệu cá nhân.

1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Công cụ App và callTool
Công cụ App và callTool: liên kết UI ↔ backend
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION