1. Vì sao lỗi trong ChatGPT App là bình thường, không phải sự cố khẩn cấp
Ở bài trước, chúng ta đã nói về cách chia nhỏ bài toán thành các bước và xây dựng workflow nhiều bước trong ChatGPT App. Giờ hãy thêm vào đó thực tế: lỗi, timeout và việc người dùng tự ý ngắt quá trình.
Trong web cổ điển, logic thường xoay quanh “happy path”, còn lỗi được xem là hiếm và mang tính sự cố — trang đỏ 500 v.v. Trong ChatGPT App thì khác: bạn làm việc trong một hệ thống phân tán với LLM, API bên ngoài, MCP, widget, và cả người dùng có thể đóng tab bất kỳ lúc nào. Lỗi và gián đoạn là chuyện hằng ngày.
Có vài đặc thù làm mọi thứ phức tạp hơn:
- Thứ nhất, LLM không quyết định tất định. Ngay cả với cùng một prompt, nó có thể chọn phương án hơi khác: gọi tool khác, đổi tham số hoặc quyết định rằng tốt hơn là “hỏi lại”.
- Thứ hai, hạn chế về mạng và hạ tầng. Tool‑call từ ChatGPT có timeout (thường vài chục giây), backend Next.js/Vercel của bạn cũng vậy. Nếu API ngoài chậm, mọi thứ có thể đứt giữa chừng.
- Thứ ba là yếu tố UX: người dùng xao nhãng, đóng chat, quay lại sau một ngày, còn bạn thì không thể giữ một giao dịch mở trong DB suốt thời gian đó.
Rút ra mệnh đề chính của bài giảng:
Workflow chịu lỗi = kịch bản trong đó ta mặc nhiên coi bất kỳ bước nào cũng có thể rơi vào lỗi và định nghĩa rõ ràng khi đó sẽ xảy ra điều gì.
Lỗi không chỉ là cái cớ để hiển thị thông báo cho người dùng mà còn là tín hiệu cho mô hình, giúp nó thay đổi chiến lược, đề xuất rollback, thử tool khác hoặc kết thúc kịch bản một cách khéo léo.
2. Bức tranh lỗi trong workflow: có những loại nào
Muốn xử lý sự cố tốt, trước hết phải biết phân biệt chúng. Trong ứng dụng LLM dựa trên ChatGPT Apps thường gặp vài lớp lỗi sau.
Lỗi kỹ thuật. Đây là toàn bộ “kinh điển” của hệ thống phân tán: timeout mạng, 5xx từ API của bạn hoặc bên ngoài, MCP server sập, bug trong code handler của tool. Ví dụ, trong GiftGenius, MCP‑tool search_products gọi vào catalog và nhận về 503 Service Unavailable. Đây là ứng viên cho retry tự động.
Lỗi logic (do mô hình). Bao gồm việc mô hình từ chối (nhận thấy yêu cầu vi phạm chính sách), ảo giác, hoặc JSON bị hỏng khi tool trả về. Mô hình có thể sinh ra tham số không hợp lệ cho tool‑call, và validation JSON của bạn chặn lại. Thường đây là lỗi dữ liệu đầu vào, không phải hạ tầng.
Lỗi nghiệp vụ. Liên quan đến ý nghĩa: hết hàng, ngân sách người dùng quá nhỏ với bộ lọc đã chọn, mã khuyến mãi không hợp lệ, booking đã hết hạn. Trong GiftGenius, đó là tình huống “trong 500 ứng viên thì không có cái nào đáp ứng các ràng buộc”. Retry hiếm khi có ích: cần đổi tham số hoặc giải thích cho người dùng rằng ràng buộc là không thực tế.
Gián đoạn UX. Người dùng tự ngắt kịch bản: đóng ChatGPT, nhấn “Back” trong widget, hủy thao tác, chỉnh câu trả lời ở bước trước. Đây cũng nên được coi là luồng bình thường, không phải lỗi. Quan trọng là khôi phục và rollback trạng thái trong các trường hợp như vậy, phần này sẽ nói sau.
Một ca khó ở giao điểm giữa lỗi logic và kỹ thuật — vòng lặp vô hạn của agent: mô hình nhận lỗi, nghĩ “ừm, thử lại”, rồi lại lỗi, cứ thế cho đến khi hết context hoặc ngân sách. Bảo vệ khỏi hành vi này là phần quan trọng trong thiết kế xử lý lỗi.
3. Chiến lược cơ bản: retry, fail‑fast, rollback, lôi kéo người dùng
Mọi lỗi có thể xem như một điểm rẽ nhánh: hoặc ta thử lặp lại bước, hoặc rollback, hoặc lôi kéo người dùng tham gia. Và quan trọng là các chiến lược này có thể kết hợp.
Với lỗi kỹ thuật và tạm thời (mạng chập chờn, API trả 503) thì hợp lý là retry có giới hạn với backoff. Với lỗi logic và nghiệp vụ (“validator không chấp nhận ngân sách”, “hết hàng”) thì lặp lại vô ích, cần fail‑fast và yêu cầu người dùng chỉnh đầu vào hoặc tham số.
Với các thao tác đã làm thay đổi thế giới bên ngoài (tạo đơn, giữ hàng), cần rollback — hoặc là “lùi một bước” trong UI/context, hoặc là hành động bù trừ thực sự (hủy đơn, hoàn tiền).
Cuối cùng, có những quyết định buộc phải có người dùng: ví dụ, hệ thống thanh toán từ chối vì “ngân hàng bác thẻ” — bạn không thể tự động sửa. Mô hình cần giải thích đúng vấn đề và đề xuất phương án: thử thẻ khác, giảm số tiền hoặc bỏ qua việc mua.
Để workflow bền vững, cực kỳ hữu ích là ghi rõ cho từng bước: những loại lỗi nào có thể xảy ra và bạn sẽ làm gì với mỗi loại — auto‑retry, rollback, hỏi người dùng, hay chỉ log rồi kết thúc nhánh.
4. Retry và backoff: khi nào và làm thế nào
Bắt đầu từ phản xạ tự nhiên của lập trình viên: “Thử lại xem sao”. Ý tưởng đúng, nhưng chi tiết mới là thứ quyết định.
Những lỗi nào có thể retry
Một kinh nghiệm hay từ thực tế tích hợp: lỗi mạng và 5xx có thể thử lại sau một khoảng dừng, còn 4xx — thường là không.
Tức là nếu nhận 503, 504 hoặc không kịp nhận phản hồi từ API ngoài, thì retry với một độ trễ nhỏ là có lý. Còn nếu server trả 400 Bad Request hoặc 422 Unprocessable Entity, khả năng cao vấn đề nằm ở dữ liệu — lặp lại với cùng tham số sẽ không thay đổi gì.
Tiện ích đơn giản callWithRetry bằng TypeScript
Hãy viết một tiện ích nhỏ cho lớp MCP hoặc backend để dùng trong các tool:
type RetryOptions = {
maxRetries: number;
baseDelayMs: number;
};
async function callWithRetry<T>(
fn: () => Promise<T>,
{ maxRetries, baseDelayMs }: RetryOptions
): Promise<T> {
let attempt = 0;
// chúng ta không cần vòng lặp vô hạn
while (true) {
try {
return await fn();
} catch (err: any) {
attempt++;
const status = err?.status ?? err?.response?.status;
// không retry với 4xx
const isClientError = typeof status === "number" && status >= 400 && status < 500;
if (attempt > maxRetries || isClientError) {
throw err;
}
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), 10_000);
// tạm dừng nhỏ để tránh 'đàn' cùng dồn vào API
const jitter = Math.random() * 200;
await new Promise((r) => setTimeout(r, delay + jitter));
}
}
}
Hàm này:
- lặp lại gọi fn với số lần giới hạn;
- dùng backoff lũy thừa với một ít nhiễu ngẫu nhiên (jitter) để tránh “hiệu ứng bầy đàn” khi cùng retry;
- dừng retry với 4xx.
Rất hữu ích khi dùng hàm này bên trong MCP‑tool, vốn đi gọi catalog sản phẩm hoặc API khuyến nghị nội bộ.
Retry ở đâu là hợp lý
Lỗi phổ biến — cố gắng lặp lại mọi request ở mọi nơi, kể cả ở các lớp bạn không kiểm soát. Trong hệ sinh thái ChatGPT, có vài nơi bạn có thể đặt retry:
- bên trong backend/MCP của chính bạn (như trong callWithRetry);
- bên trong worker nền/hàng đợi (ở các mô‑đun sau chúng ta sẽ bàn sâu hơn về job‑queue và DLQ);
- đôi khi — ngay trong widget, khi chỉ là các request nhẹ “làm mới danh sách” không có hiệu ứng phụ.
Quan trọng là không trùng lặp logic: nếu job‑worker của bạn đã retry 3 lần với backoff, thì không cần chồng thêm 5 retry nữa ở widget. Và dĩ nhiên, đừng bao giờ làm while(true) { try ... } — đó là cách chắc chắn để tự DDoS chính mình.
5. Idempotency của các bước: chống trùng lặp
Retry tạo ra vấn đề thứ hai: làm sao để không thực hiện cùng một hành động hai lần. Trong thế giới LLM điều này càng nhức nhối: mô hình có thể vô tình gọi cùng một tool nhiều lần, ChatGPT có thể lặp lại tool‑call sau khi timeout, người dùng nhấn “Regenerate”, rồi UI hoặc agent lại thêm một lần gọi theo cách riêng.
Ý tưởng idempotency rất đơn giản: một bước được xem là idempotent nếu việc thực thi lại với cùng đầu vào không tạo thêm hiệu ứng phụ. Lấy product feed — ổn, tính lại khuyến nghị — ổn, còn trừ tiền hai lần hoặc tạo đơn thứ hai với cùng dữ liệu — hoàn toàn không ổn.
Idempotency key trong ChatGPT App
Mẫu kinh điển: cho mỗi bước logic có hiệu ứng phụ, bạn sinh idempotency_key (thường là UUID), chuyền nó qua mô hình tới MCP‑tool và tại đó lưu ánh xạ “key → kết quả”. Nếu tool được gọi lần hai với cùng key, nó không lặp lại hành động, mà chỉ trả về kết quả đã lưu.
Trong GiftGenius có bước create_order. Hãy tưởng tượng người dùng bấm “Thanh toán”, mô hình gọi tool, thanh toán xong, nhưng phản hồi bị thất lạc đâu đó. Mô hình hoặc nền tảng quyết định gọi lại, và nếu không có idempotency, chúng ta sẽ có đơn hàng trùng hoặc trừ tiền hai lần.
Ví dụ đơn giản về tool idempotent bằng TypeScript
Viết một handler MCP‑tool create_order có idempotency‑key. Để đơn giản, dùng Map trong bộ nhớ; thực tế sẽ là DB hoặc cache.
type CreateOrderInput = {
userId: string;
items: Array<{ sku: string; qty: number }>;
idempotencyKey: string;
};
type CreateOrderResult = { orderId: string; status: "created" };
const idempotencyStore = new Map<
string,
{ paramsHash: string; result: CreateOrderResult }
>();
export async function createOrderTool(input: CreateOrderInput): Promise<CreateOrderResult> {
const { idempotencyKey, ...rest } = input;
const paramsHash = JSON.stringify(rest);
const existing = idempotencyStore.get(idempotencyKey);
if (existing) {
// nếu key đã tồn tại, chắc chắn rằng tham số trùng khớp
if (existing.paramsHash !== paramsHash) {
throw new Error("Idempotency key reuse with different params");
}
return existing.result;
}
// tại đây chúng ta thực hiện tạo đơn và thanh toán thực sự
const result: CreateOrderResult = {
orderId: "order_" + Math.random().toString(36).slice(2),
status: "created",
};
idempotencyStore.set(idempotencyKey, { paramsHash, result });
return result;
}
Tại đây chúng ta:
- yêu cầu idempotencyKey trong input của tool;
- lưu kèm theo hash tham số (đơn giản hóa dùng JSON.stringify);
- khi gọi lại với cùng key nhưng dữ liệu khác — coi đó là lỗi;
- khi gọi lại với cùng dữ liệu — chỉ trả về kết quả cũ.
Trong dự án thực tế, nên:
- lưu key trong DB với TTL (để bảng không phình vô hạn);
- ghi log idempotency_key và đưa nó vào _meta của thông điệp MCP để dễ theo dõi qua Inspector và dashboard.
6. Rollback các bước và mẫu Saga
Idempotency chống trùng lặp, nhưng không giải bài toán khác: làm gì nếu một bước ở giữa kịch bản bị lỗi.
Trong e‑commerce đây là vấn đề kinh điển: bạn đã tạo đơn và giữ hàng trong kho, nhưng ở bước thanh toán thì có trục trặc. Bạn không thể “quên” chuyện đó — cần rollback trạng thái trước đó.
Rollback logic vs rollback kỹ thuật
Trong ChatGPT‑workflow có hai cấp độ rollback.
Rollback logic — là quay về bước trước trong kịch bản và điều chỉnh context. Ví dụ, ở bước “thanh toán” xảy ra lỗi, bạn quyết định quay lại bước “chọn phương thức thanh toán” hoặc thậm chí “chọn quà”. Khi đó cần:
- cập nhật WorkflowContext trên backend (bước hiện tại, tham số đã chọn);
- thông báo cho mô hình về việc đổi bước qua tool‑call/ToolOutput để nó “quên” nhánh cũ và điều chỉnh hành vi tiếp theo;
- cập nhật UI của widget để bước và nút phù hợp với trạng thái mới.
Rollback kỹ thuật — là cấp nghiệp vụ: hủy các thực thể đã tạo, bù trừ các hiệu ứng ngoài. Ví dụ: hủy đơn, bỏ giữ hàng, tạo hoàn tiền. Đây chính là mẫu Saga: với mỗi bước “nguy hiểm” bạn thiết kế sẵn hành động bù trừ.
Sơ đồ forward/compensate cho GiftGenius
Với checkout giản lược của GiftGenius có thể vẽ trình tự như sau:
flowchart TD A[Bước 1: create_order] --> B[Bước 2: reserve_items] B --> C[Bước 3: charge_card] C -->|thành công| D[Trạng thái: completed] C -->|lỗi| E[Bù trừ: cancel_reservation] E --> F[Bù trừ: cancel_order] F --> G[Trạng thái: failed + thông báo cho người dùng]
Mỗi hành động làm thay đổi thế giới bên ngoài (tạo đơn, giữ hàng, thanh toán) đều có hành động bù trừ tương ứng (hủy đơn, bỏ giữ, hoàn tiền). Chúng không phải lúc nào cũng đối xứng 1–1, nhưng nguyên tắc chung là vậy.
Ví dụ mini với hành động bù trừ trong code
Xem một đoạn code nhỏ thực thi các bước này:
async function completeCheckout(ctx: { userId: string }) {
const order = await createOrderInDb(ctx.userId);
try {
await reserveItems(order.id);
await chargeCard(order.id);
return { orderId: order.id, status: "paid" as const };
} catch (err) {
// hành động bù trừ
await safeCancelReservation(order.id);
await safeCancelOrder(order.id);
throw err;
}
}
Tại đây:
- createOrderInDb, reserveItems, chargeCard — là các bước forward;
- safeCancelReservation và safeCancelOrder — là các bước bù trừ, bản thân chúng cũng cần idempotent (nếu cố hủy thứ đã hủy rồi thì cũng không sao).
Lưu ý: khi có lỗi chúng ta không giấu nó, mà ném ra tiếp. Mô hình (qua ToolOutput) cần nhận thông điệp lỗi dễ hiểu, rồi nói lại với người dùng và đề xuất bước tiếp theo.
7. Rollback và đồng bộ trạng thái: tránh lệch pha
Có một dạng “lỗi” dễ bị đánh giá thấp: trạng thái bị lệch giữa UI, backend và mô hình.
Ví dụ điển hình:
- Người dùng đi qua các bước 1 → 2 → 3.
- Ở bước 3 có trục trặc, người dùng bấm nút “Back” trong widget.
- Widget một cách trung thực đưa trạng thái cục bộ về bước 2.
- Nhưng mô hình “nhớ” rằng ta ở bước 3 và đã thử thanh toán. Ở thông điệp kế tiếp, nó vẫn nói về thanh toán, trong khi người dùng đang thấy màn chọn quà.
Để tránh việc này, nên đưa vào sự kiện rollback bước một cách tường minh. Widget gửi sự kiện này tới MCP/mô hình — hoặc như một lần gọi tool, hoặc như ToolOutput.
Ví dụ, ta có thể làm một tool đơn giản user_navigated_to_step để ghi nhận bước hiện tại và trạng thái của nó:
type NavigateInput = {
workflowId: string;
stepId: string;
};
export async function userNavigatedToStep(input: NavigateInput) {
await workflowRepo.setCurrentStep(input.workflowId, input.stepId);
return {
message: `User moved to step ${input.stepId}`,
};
}
Widget khi nhấn “Back” sẽ gọi tool này; mô hình nhìn thấy kết quả trong lịch sử các tool‑call và hiểu rằng từ giờ cần tiếp tục hội thoại dựa trên bước mới.
Về phía UI, handler sẽ trông tương tự như sau:
async function handleBackClick() {
const { workflowId, prevStepId } = widgetState;
await window.openai.tools.call("user_navigated_to_step", {
workflowId,
stepId: prevStepId,
});
setWidgetState((s) => ({ ...s, currentStepId: prevStepId }));
}
Điểm quan trọng: chính backend/agent là nguồn chân lý về bước hiện tại, còn mô hình thấy nó thông qua tools. Nhờ vậy, ngay cả khi khôi phục phiên sau này, bạn vẫn có thể đồng bộ context chính xác.
8. UX của lỗi: người dùng thấy gì và mô hình thấy gì
Chúng ta đã biết cách chống chọi lỗi về mặt kỹ thuật (retry, rollback, idempotency, đồng bộ trạng thái). Giờ cần đảm bảo mọi thứ trông hợp lý cả với người dùng lẫn mô hình.
Retry và rollback hoàn hảo cũng vô ích nếu UX lỗi “như thời servlet Java cổ”: chữ đỏ, stack trace và dòng chữ khó hiểu “Unexpected error”.
Trong ChatGPT App có hai đối tượng nhận thông điệp lỗi:
- người dùng, người cần hiểu chuyện gì đã xảy ra và có thể làm gì tiếp;
- mô hình, cần đủ thông tin có cấu trúc để quyết định: lặp lại, đổi tham số, đề xuất phương án khác hoặc kết thúc kịch bản.
Thực hành tốt:
- ở mức MCP/tool, trả về lỗi có cấu trúc với mã, loại, cờ retryable và mô tả kỹ thuật ngắn;
- đưa cho mô hình đúng cấu trúc này (ví dụ trong result.structuredContent), không phải cả cây stack trace;
- trên UI, hiển thị thông điệp ngắn gọn, dễ hiểu cho người dùng.
Ví dụ mini về cấu trúc lỗi mà tool trả về:
type ToolError = {
code: string; // ví dụ: "PAYMENT_TIMEOUT"
message: string; // mô tả kỹ thuật ngắn gọn
retryable: boolean; // có nên thử lại hay không
};
throw {
isError: true,
error: <ToolError>{
code: "PAYMENT_TIMEOUT",
message: "Payment provider did not respond in time",
retryable: true,
},
};
Mô hình thấy retryable: true và có thể thử tool khác hoặc đề nghị người dùng thử lại.
Trên phía widget, bạn chỉ cần ánh xạ các mã này sang câu chữ dễ hiểu:
function ErrorBanner({ code }: { code: string }) {
const text =
code === "PAYMENT_TIMEOUT"
? "Dịch vụ thanh toán không phản hồi kịp thời. Vui lòng thử lại sau một phút."
: "Đã có lỗi xảy ra. Vui lòng thử lại.";
return <div className="error-banner">{text}</div>;
}
Và thêm một điểm quan trọng: đừng hiển thị stack exception, token, secret cho người dùng. Vừa xấu, vừa không an toàn. Thông tin kỹ thuật hãy log ở phía bạn, còn với người dùng thì đưa thông điệp ngắn gọn, an toàn.
Insight
Trong các hệ thống LLM như ChatGPT, các lần gọi tool không hợp lệ là chuyện thường, không phải ngoại lệ. Mô hình thường xuyên sinh tham số không qua được validation: nhầm kiểu, thiếu trường, giá trị sai, cấu trúc hỏng. Đây không phải lỗi theo nghĩa kỹ sư thuần túy — mà là bản chất của mô hình ngẫu nhiên, và toàn bộ giao diện lỗi cần thích nghi cho phù hợp.
Ý tưởng then chốt: thông điệp lỗi không phải là tín hiệu “đã hỏng”, mà là hướng dẫn để sửa cho lần thử tiếp theo. Đối tượng chính của nó là chính mô hình. Nếu thông điệp được cấu trúc và có chỉ dẫn chính xác, mô hình có thể tự điều chỉnh tham số và gọi lại cho đúng. Đó là nguyên lý của các kỹ thuật Tool‑Reflection: phản hồi đúng đắn giúp hành động kế tiếp của agent tốt hơn mà không cần con người can thiệp.
Nên tuân thủ các yêu cầu sau cho định dạng lỗi:
- thông điệp phải chỉ ra trường cụ thể không qua được validation — tránh kiểu chung chung “Invalid parameters”;
- cần mô tả rõ định dạng mong đợi hoặc các giá trị cho phép, để mô hình chọn được cái phù hợp;
- thông điệp cần ngắn gọn, chuẩn hóa và có cấu trúc: các trường như error_type, field, expected hoặc allowed_values giúp mô hình rất nhiều;
- nếu có thể, hãy đưa một ví dụ tối thiểu về input hợp lệ — thường giúp tăng độ chính xác khi mô hình phục hồi.
Error feedback lý tưởng cho mô hình chứa hai điều: cái gì sai, và cách sửa nó.
9. Log và metric cho lỗi trong workflow
Kể cả khi UX lỗi đã gọn gàng, để thực sự biết cái gì đang hỏng thì chỉ mỗi thông điệp cho người dùng là không đủ. Cần log có cấu trúc và metric theo từng bước.
Bộ tối thiểu hữu ích khi log mỗi bước của workflow:
- user_id hoặc ít nhất session_id;
- workflow_id và step_id;
- trạng thái bước (success, failed, retry, rolled_back);
- error_code (nếu có);
- idempotency_key và correlation_id nếu bước có liên quan đến các cuộc gọi bên ngoài.
Trong MCP và Agents có các trường _meta; rất tiện để đặt idempotency_key và correlation_id vào đó để thấy được cả trong log lẫn Inspector.
Ví dụ log đơn giản bằng Node.js/TypeScript (có thể dùng console, hoặc winston/pino):
function logStepFailure(params: {
userId?: string;
workflowId: string;
stepId: string;
errorCode: string;
idempotencyKey?: string;
}) {
console.error(
JSON.stringify({
level: "error",
event: "workflow_step_failed",
...params,
timestamp: new Date().toISOString(),
})
);
}
Những log như vậy dễ parse, dựng dashboard và tính toán:
- tỉ lệ chuyển đổi giữa các bước;
- các loại lỗi xuất hiện nhiều nhất;
- tỷ trọng bước kết thúc bằng retry so với thất bại cuối cùng.
Không phải lỗi nào cũng cần bắn alert trên production. Lỗi nghiêm trọng — MCP sập, timeout hàng loạt, thất bại hàng loạt ở một bước nhất định — vâng, nên đưa vào monitoring. Còn “không tìm được quà phù hợp” — đó là sự kiện nghiệp vụ, không phải incident.
10. Nâng cấp GiftGenius: bước checkout ổn định
Giờ hãy gom mọi thứ lại: retry, idempotency, Saga, đồng bộ trạng thái, UX lỗi và logging — trên ví dụ một bước trong ứng dụng học tập GiftGenius — bước tạo đơn (checkout).
Hiện đã có gì
Đến thời điểm này chúng ta đã:
- có workflow nhiều bước: thu thập thông tin → gợi ý ý tưởng → chọn quà → checkout;
- thiết lập tool gating: ở bước checkout chỉ mở bộ công cụ thương mại (create_order, get_payment_methods v.v.);
- có WorkflowContext, nơi lưu quà đã chọn, ngân sách, userId và bước hiện tại.
Sẽ bổ sung gì trong bài này
Cho bước checkout, ta sẽ triển khai:
- idempotency_key cho tool create_order;
- retry khi gặp lỗi tạm thời từ nhà cung cấp thanh toán;
- bù trừ (compensation) khi các thao tác chỉ thành công một phần;
- UX lỗi hợp lý trên widget.
Sinh idempotency‑key trong widget khi bấm nút “Thanh toán”:
import { v4 as uuid } from "uuid";
async function handlePayClick() {
const idempotencyKey = uuid();
setWidgetState((s) => ({ ...s, idempotencyKey }));
await window.openai.tools.call("create_order", {
userId: widgetState.userId,
items: [/* ... */],
idempotencyKey,
});
}
Phía tool create_order — chính là handler idempotent mà ta đã viết ở trên: nó lưu key và kết quả, và khi lặp lại sẽ không tạo đơn mới.
Mã gọi API thanh toán có thể bọc trong callWithRetry để thử trừ tiền vài lần khi mạng trục trặc. Và đừng quên thêm cờ retryable: true vào lỗi, để mô hình hiểu có thể đề nghị thử lại.
Nếu sau khi tạo đơn và trừ tiền thành công mà có thứ gì đó hỏng (ví dụ, webhook ngoài không đến kịp), ta log với correlation_id và workflow_id và tiếp tục:
- thử retry nền (sẽ bàn ở mô‑đun tương lai về hàng đợi và sự kiện);
- hoặc đánh dấu bước là failed, gọi các hành động bù trừ và giải thích cho người dùng điều đã xảy ra.
11. Lỗi thường gặp khi thiết kế workflow chịu lỗi
Lỗi số 1: “Retry tất cả cho đến khi chạy được”.
Tự động lặp lại mọi bước đến cùng — cách chắc chắn để tự đẩy mình vào địa ngục. Lỗi mạng và 5xx có thể thử lại với backoff và giới hạn số lần. Nhưng với 4xx, lỗi nghiệp vụ và lỗi logic của mô hình thì cần sửa bằng dữ liệu hoặc giải thích cho người dùng. Nếu không, bạn sẽ có hành vi bất ổn, hóa đơn kỳ quặc và log rác.
Lỗi số 2: Thiếu idempotency ở nơi có tiền và đơn hàng.
Nếu tool kiểu create_order hoặc charge_card không idempotent, bất kỳ lần gọi lại nào (do timeout, Regenerate, bug trong agent) cũng có thể dẫn đến trùng lặp. Trong kịch bản LLM, retry xuất hiện thường xuyên hơn nhiều so với REST frontend cổ điển, nên idempotency_key không phải “tốt thì có”, mà là điều bắt buộc cho bước thanh toán và các bước quan trọng khác.
Lỗi số 3: Không có hành động bù trừ (thiếu Saga).
Đã tạo đơn, đã giữ hàng, đến bước thanh toán thì lỗi và chỉ hiển thị “đã có lỗi xảy ra”. Kết quả là hệ thống treo các nửa‑đơn, giữ hàng, đuôi tài chính. Với mỗi bước thay đổi thế giới bên ngoài, hãy nghĩ trước bạn sẽ làm gì khi bước tiếp theo thất bại: hủy, hoàn, đánh dấu “expired”, v.v.
Lỗi số 4: Để agent rơi vào vòng lặp retry vô tận.
Nếu bạn không giới hạn số lần thử (ví dụ qua maxRetries trong helper hoặc max_iterations trong logic agent) và không đánh dấu lỗi là retryable: false ở nơi retry vô ích, mô hình có thể lặp mãi: “Thử lại… lại thử…”. Điều này đốt token, thời gian và thần kinh.
Lỗi số 5: Lệch trạng thái giữa UI và mô hình khi rollback.
Nhiều lập trình viên chỉ triển khai nút “Back” trên UI, quên đồng bộ bước với backend và mô hình. Hệ quả: người dùng đang ở bước 2, còn mô hình vẫn sống ở bước 3 và đưa ra gợi ý kỳ lạ. Giải pháp — sự kiện tường minh như user_navigated_to_step và cập nhật WorkflowContext ở mỗi lần chuyển bước.
Lỗi số 6: Thông điệp kỹ thuật cho người dùng và thiếu log cho lập trình viên.
Người dùng nhận “Error: ECONNRESET at TcpSocket.onEnd…”, còn bạn — không có thông tin về việc bước nào, workflow_id nào bị hỏng. Cách làm chuẩn: với người dùng — câu chữ ngắn gọn, dễ hiểu và đề xuất bước tiếp theo; với lập trình viên — log có cấu trúc với workflow_id, step_id, error_code, idempotency_key và correlation_id.
Lỗi số 7: Không có chiến lược alert.
Hoặc alert tất cả, kể cả “không có quà phù hợp với bộ lọc quá hẹp của bạn”, hoặc không alert gì, kể cả MCP sập thật. Hãy tách bạch sự cố hệ thống nghiêm trọng (dịch vụ sập, timeout hàng loạt, mất webhook) khỏi sự kiện nghiệp vụ dự kiến. Loại đầu đưa vào monitoring và on‑call, loại sau chỉ tính trong analytics.
GO TO FULL VERSION