1. Ngữ cảnh workflow là gì và tại sao cần nó
Trong một ứng dụng web thông thường, bạn có cái nhìn khá rõ ràng về nơi trạng thái “sống”: cơ sở dữ liệu, cache, cộng thêm một chút trên front như Redux hoặc state cục bộ của React. Trong ChatGPT App thì “vui” hơn: trạng thái được “trải” ra ba thế giới — bên trong mô hình (lịch sử hội thoại), bên trong widget (UI state) và trên máy chủ/MCP của bạn (dữ liệu nghiệp vụ).
Với ngữ cảnh workflow, chúng ta hiểu đó là toàn bộ tập dữ liệu cần để trả lời các câu hỏi “chúng ta đang ở bước nào” và “điều gì đã biết rồi”. Nếu nói về GiftGenius trong bài học này, ngữ cảnh bao gồm:
- hồ sơ người nhận quà: tuổi, giới tính, sở thích;
- ngân sách và có thể cả đơn vị tiền tệ;
- danh sách ý tưởng đã sinh và những ý tưởng người dùng đã thích hoặc ẩn;
- các mục kỹ thuật: mã phiên hoặc workflow, trạng thái (“profile_collected”, “ideas_shown”, “checkout_started”).
Ngữ cảnh này không chỉ cần cho bạn với vai trò lập trình viên backend. Nó còn cần cho chính mô hình để hiểu những câu hỏi nào đã được hỏi, công cụ nào đã được gọi và hiện đang nói về điều gì. Và nó cần cho người dùng để khi quay lại chat không phải bắt đầu từ con số 0.
Người dùng thường nghĩ rằng “ChatGPT nhớ hết”. Thực tế mô hình chỉ nhớ văn bản hội thoại, và chỉ khi nó còn nằm trong cửa sổ ngữ cảnh. Những thứ có cấu trúc như order_id, cart_id hay “danh sách ý tưởng đã thích” cần được lưu trên máy chủ của bạn, nếu không bạn sẽ nhận được một cỗ máy tạo ra những khẳng định rất tự tin nhưng lại sai.
2. Ba lớp trạng thái: UI, LLM, business
Dễ hiểu nhất là xem việc lưu ngữ cảnh qua mô hình ba lớp trạng thái. Còn gọi là “State Triad”.
Bảng các lớp
Dùng một bảng nhỏ:
| Lớp | Lưu ở đâu | Vòng đời | Chịu trách nhiệm cho | Ví dụ trong GiftGenius |
|---|---|---|---|---|
| UI State | Widget (React, widgetState) | Trong khi chat/thông điệp kèm widget đang mở | Trạng thái hiển thị, nhập liệu cục bộ | Thẻ nào đang được tô sáng, trạng thái form |
| LLM Context | Lịch sử chat trong OpenAI | Chừng nào thông điệp còn “vừa” cửa sổ ngữ cảnh | Hiểu hội thoại và suy luận | “Đang tìm quà cho mẹ, ngân sách $50” |
| Business State | MCP / backend của bạn (CSDL/Redis) | Bao lâu tùy bạn (lưu bền) | Nguồn sự thật: dữ liệu đã xác thực, trạng thái | { step: "ideas", budget: 50, liked: [42, 51] } |
Lớp UI nhanh và phản hồi tốt nhưng rất mong manh: ChatGPT có thể “unmount” iframe với widget khi bạn cuộn lên trên lịch sử, rồi lại mount lại. Chính vì vậy mới có widgetState, sống lâu hơn một chút so với React component, và đồng bộ với host client của ChatGPT.
Lớp LLM cho mô hình cảm giác hội thoại liên tục, nhưng chỉ lưu văn bản và các lần gọi tool. Bạn có thể đặt JSON giỏ hàng vào đó, nhưng thực chất là chèn JSON vào văn bản — mô hình sẽ không xem nó như một cơ sở dữ liệu.
Lớp business là thứ bạn, với vai trò kỹ sư, có thể kiểm soát: ở đó nằm dữ liệu đã được kiểm chứng, chỉ mục, trạng thái đơn hàng. Khi có kịch bản nghiêm túc (quà tặng, đặt chỗ, học tập), chính lớp này phải trở thành nguồn sự thật về trạng thái.
Vấn đề kỹ thuật chính — đảm bảo ba lớp này không “lệch pha”. Người dùng đổi ngân sách trong widget, mô hình vẫn nghĩ về giá trị cũ, còn trong cơ sở dữ liệu lại là giá trị thứ ba — đây là công thức cổ điển cho hành vi kỳ quặc.
3. Chúng ta lưu gì: cấu trúc WorkflowContext
Để nói cụ thể, hãy mô tả trong TypeScript interface ngữ cảnh cho GiftGenius. Giả sử đã có vài bước: thu thập hồ sơ, chọn ngân sách, sinh ý tưởng và xem/thích.
Bắt đầu với cấu trúc đơn giản:
// backend/types/workflow.ts
export type GiftWorkflowStep =
| "profile"
| "budget"
| "ideas"
| "checkout";
export interface GiftWorkflowContext {
id: string; // workflowId — định danh kịch bản
userId?: string; // nếu đã cấu hình xác thực
currentStep: GiftWorkflowStep;
profile?: {
age?: number;
gender?: string;
interests?: string[];
};
budget?: {
min?: number;
max?: number;
currency: string;
};
ideas?: {
id: string;
title: string;
}[];
likedIdeaIds: string[];
hiddenIdeaIds: string[];
updatedAt: number; // timestamp dùng cho TTL/dọn dẹp
}
Đây chưa phải schema cuối cùng, nhưng các phần quan trọng đã có. Bao gồm:
- định danh workflow để chúng ta tìm ngữ cảnh theo đó;
- bước hiện tại giúp cả widget và mô hình hiểu chúng ta đã đi đến đâu;
- bộ trường sẽ được điền ở các bước riêng;
- các trường kỹ thuật như thời điểm cập nhật.
Về định danh. Trong bài này, workflowId chính là định danh của kịch bản cụ thể trong backend/MCP của chúng ta. Nó có thể trùng với mã phiên hội thoại ChatGPT (sessionId), nhưng chúng ta không phụ thuộc vào điều đó. userId — định danh người dùng từ hệ thống xác thực của bạn (nếu có); một người dùng có thể có nhiều workflow đang hoạt động. Trường id chính là workflowId này, theo đó chúng ta tìm và cập nhật ngữ cảnh.
Ở các phần tiếp theo, chúng ta sẽ xem ba điều: lưu các đối tượng này ở đâu, cách ghi chúng vào đó và cách lấy chúng trở lại — cho widget và cho mô hình.
4. Lưu trạng thái ở đâu: các lựa chọn và đánh đổi
Nghĩ về lưu trạng thái tiện nhất theo hai chiều: nơi nó được lưu và sống bao lâu. Phần này tập trung vào nơi lưu, còn thời gian sống sẽ quay lại trong checklist và phần lỗi phổ biến.
Trước hết, nơi lưu.
Trong hội thoại (trong prompt)
Đôi khi bạn muốn nói: “Thôi thì mỗi lần cứ trả về cho mô hình JSON trạng thái hiện tại, để nó tự lo”. Cách này hoạt động với các kịch bản rất đơn giản và chuỗi bước ngắn, nhưng nhanh chóng đụng hai vấn đề: giới hạn độ dài ngữ cảnh và không có bất kỳ đảm bảo toàn vẹn dữ liệu nào.
Ngoài ra, giao thức MCP về bản chất là stateless: như HTTP, mặc định nó không lưu trạng thái giữa các request. Để gắn một lần gọi tool với phiên cụ thể, bạn phải truyền rõ ràng định danh — workflow hoặc session id — hoặc trong tham số của tool, hoặc qua metadata/headers.
Vì vậy, chỉ lưu trạng thái nghiệp vụ trong hội thoại — đúng hơn là một bài thực hành, không phải kiến trúc.
Trong widget: UI + widgetState
Ở cấp UI, chúng ta dùng state React thông thường (useState, useReducer v.v.), nhưng như đã nói, component có thể bị unmount. Trong Apps SDK có cơ chế widgetState để giải quyết, nó sống ngoài React và đồng bộ với host ChatGPT. Khi mount widget, bạn lấy giá trị đã lưu từ đó; khi thay đổi — ghi ngược lại, ta có một kho cục bộ khá tiện.
Kho này rất phù hợp cho trạng thái thuần hiển thị: thẻ nào đang gập, bạn đang ở tab nào, người dùng đã nhập gì vào form trước khi nhấn “Tiếp”. Nhưng nó không thay thế máy chủ: khi người dùng mở chat trên thiết bị khác hoặc sau một tuần, widgetState có thể không giúp được nữa. Và dựng nghiệp vụ trên đó — là một quyết định đáng tranh luận.
Trên server/MCP: Map, Redis, CSDL
Cuối cùng, lựa chọn chính cho production: chúng ta lưu GiftWorkflowContext phía MCP server hoặc backend service. Vì MCP client và server theo giao thức stateless, ta phải truyền workflowId (hoặc state_token) trong mỗi lần gọi tool để biết ngữ cảnh nào cần cập nhật.
Có vài phương án triển khai:
- Map in‑memory trong Node.js — phù hợp cho demo và môi trường dev: nhanh, nhưng mất khi restart;
- Redis hoặc in‑memory cache khác với TTL — tốt cho wizard ngắn (vài bước): sống 1–2 giờ rồi có thể xóa;
- CSDL SQL/NoSQL bình thường — bắt buộc cho kịch bản kiểu “quay lại sau một tuần” hoặc “bản nháp và giỏ hàng”.
Trong bài này, chúng ta không đi sâu vào CSDL cụ thể, mà tập trung vào interface và hiểu rõ những gì cần được lưu.
5. Kho lưu trữ đơn giản nhất trên MCP‑server: Map theo workflowId
Bắt đầu với thứ thực tế: một Map in‑memory trên MCP‑server, nơi khóa là workflowId. Trong demo học tập, có thể đơn giản đặt bằng sessionId của hội thoại, nhưng ở production nên giữ workflowId như định danh kịch bản riêng. Giá trị trong Map này sẽ là GiftWorkflowContext. Trong production thực, bạn thay bằng Redis hoặc CSDL, còn API vẫn như cũ.
Giả sử chúng ta có MCP‑server bằng TypeScript. Thêm gần đoạn khởi tạo:
// mcp/workflowStore.ts
import { GiftWorkflowContext } from "../backend/types/workflow";
const workflows = new Map<string, GiftWorkflowContext>();
export function getWorkflow(id: string): GiftWorkflowContext | undefined {
return workflows.get(id);
}
export function saveWorkflow(ctx: GiftWorkflowContext): void {
workflows.set(ctx.id, { ...ctx, updatedAt: Date.now() });
}
Tiếp theo — một tool lưu hồ sơ người nhận. Quan trọng là nó nhận workflowId và dữ liệu hồ sơ, bên trong cập nhật/tạo ngữ cảnh tương ứng:
// mcp/tools/setProfile.ts
import { jsonSchema } from "@modelcontextprotocol/sdk"; // bí danh
import { getWorkflow, saveWorkflow } from "../workflowStore";
export const setProfileTool = {
name: "gift_set_profile",
description: "Lưu hồ sơ của người nhận quà",
inputSchema: jsonSchema.object({
workflowId: jsonSchema.string(),
age: jsonSchema.number().optional(),
gender: jsonSchema.string().optional(),
interests: jsonSchema.array(jsonSchema.string()).optional()
}),
async run(input: any) {
const existing = getWorkflow(input.workflowId);
const ctx = existing ?? {
id: input.workflowId,
currentStep: "profile",
likedIdeaIds: [],
hiddenIdeaIds: []
};
ctx.profile = {
age: input.age,
gender: input.gender,
interests: input.interests ?? []
};
ctx.currentStep = "budget";
saveWorkflow(ctx);
return {
structuredContent: {
type: "profileSaved",
workflowId: ctx.id,
profile: ctx.profile,
nextStep: ctx.currentStep
}
};
}
};
Tool này đã giải quyết hai việc: lưu hồ sơ và đẩy currentStep sang bước tiếp theo. Trong dự án thực, có thể bạn muốn tách tool “lưu dữ liệu” và “chuyển bước”, nhưng để hiểu khái niệm thì như vậy là ổn.
Chú ý workflowId trong tham số: chính tham số này gắn lần gọi tool với ngữ cảnh cần thiết. Phần client (widget hoặc agent) phải lưu ở đâu đó và truyền xuống.
6. Tích hợp với Apps SDK: lấy workflowId và sessionId ở đâu
Câu hỏi “lấy workflowId từ đâu” trong ChatGPT Apps hơi mang tính triết học. Khả năng phụ thuộc vào việc bạn có dùng xác thực, MCP trực tiếp hay Agents SDK. Nhìn chung có hai lựa chọn: sinh ở phía máy chủ trong lần gọi tool đầu tiên hoặc sinh trong widget rồi truyền xuống.
Cho ví dụ học tập, giả sử bước đầu là gọi MCP‑tool tạo workflow, widget sau đó chỉ nhận và giữ id.
Phương án đơn giản nhất:
// mcp/tools/startWorkflow.ts
import { randomUUID } from "crypto";
import { saveWorkflow } from "../workflowStore";
export const startWorkflowTool = {
name: "gift_start_workflow",
description: "Tạo workflow mới cho việc chọn quà",
inputSchema: { type: "object", properties: {} },
async run() {
const id = randomUUID();
saveWorkflow({
id,
currentStep: "profile",
likedIdeaIds: [],
hiddenIdeaIds: [],
updatedAt: Date.now()
});
return {
structuredContent: {
type: "workflowStarted",
workflowId: id,
currentStep: "profile"
}
};
}
};
Sau đó, khi mô hình nhận workflowId trong phản hồi tool, nó có thể:
- giữ nó ở dạng ẩn trong ngữ cảnh;
- truyền nó vào widget qua structuredContent để widget lưu vào widgetState và tự động chèn vào các lần gọi tool tiếp theo.
Ở phía widget, mã sẽ tương tự như sau.
7. Lưu trữ workflowId và UI state cục bộ trong widget
Giả sử chúng ta có widget danh sách ý tưởng, muốn biết nó hiển thị workflow nào, và nhớ các “like” cục bộ ngay cả khi component bị unmount. Ở dạng đơn giản:
// app/widgets/GiftIdeasWidget.tsx
import { useEffect, useState } from "react";
interface Idea {
id: string;
title: string;
}
interface WidgetProps {
widgetId: string;
workflowId: string; // đi kèm từ structuredContent
ideas: Idea[];
}
interface UiState {
liked: string[];
}
export function GiftIdeasWidget(props: WidgetProps) {
const [uiState, setUiState] = useState<UiState>({ liked: [] });
useEffect(() => {
window.openai.getWidgetState<UiState>(props.widgetId).then(saved => {
if (saved) setUiState(saved);
});
}, [props.widgetId]);
function toggleLike(id: string) {
const exists = uiState.liked.includes(id);
const next: UiState = {
liked: exists
? uiState.liked.filter(x => x !== id)
: [...uiState.liked, id]
};
setUiState(next);
window.openai.setWidgetState(props.widgetId, next);
// cũng có thể gọi MCP‑tool "gift_like_idea" tại đây
}
return (
<ul>
{props.ideas.map(idea => (
<li key={idea.id}>
{idea.title}
<button onClick={() => toggleLike(idea.id)}>
{uiState.liked.includes(idea.id) ? "★" : "☆"}
</button>
</li>
))}
</ul>
);
}
Ở đây widgetState được dùng đúng như một lớp UI: chúng ta nhớ những ý tưởng nào đang được tô sáng. Đúng ra các “like” cũng nên gửi lên máy chủ (qua MCP‑tool hoặc API endpoint trong Next.js) để lớp business cũng biết người dùng đã chọn gì.
Điều quan trọng là đừng cố dựng toàn bộ workflow trên widgetState. Nó phải là lớp bổ trợ cho ngữ cảnh nghiệp vụ ở phía máy chủ.
8. Khôi phục kịch bản: người dùng quay lại
Giờ đến tình huống thú vị hơn: người dùng đóng ChatGPT, quay lại sau vài giờ hoặc vài ngày và mở lại chính cuộc chat đó. Điều gì nên xảy ra?
UX lý tưởng là: mô hình và App hiểu rằng người dùng đã có một workflow chưa hoàn tất, kéo ngữ cảnh của nó về và nói đại loại: “Bạn đã khai hồ sơ và ngân sách, hãy tiếp tục chọn ý tưởng”.
Về kiến trúc, trông như sau:
- Trên máy chủ bạn lưu GiftWorkflowContext, gắn với một userId nào đó hoặc ít nhất là workflowId nội bộ.
- Khi có request mới (hoặc cuộc gọi tool đầu tiên trong phạm vi hội thoại), App hỏi máy chủ: “Người dùng này có workflow đang hoạt động không?”.
- Nếu có, máy chủ trả về nó và có thể thêm cờ resume để mô hình dùng trong câu trả lời.
Trong demo nguyên khối đơn giản, có thể coi MCP‑server và ứng dụng Next.js sống trong cùng repository (hoặc thậm chí cùng process), nên ta chỉ dùng lại workflowStore từ MCP trong các API route.
Trong Next.js, đây có thể là một API route đơn giản:
// app/api/gift/workflow/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getWorkflow } from "@/mcp/workflowStore"; // trong demo này MCP và Next.js dùng chung một kho
export async function GET(req: NextRequest) {
const id = req.nextUrl.searchParams.get("workflowId");
if (!id) return NextResponse.json({ error: "Missing workflowId" }, { status: 400 });
const ctx = getWorkflow(id);
if (!ctx) return NextResponse.json({ exists: false });
return NextResponse.json({
exists: true,
context: ctx
});
}
Widget (hoặc MCP‑tool) có thể gọi endpoint này khi cần cập nhật trạng thái: ví dụ, lúc mount lần đầu hoặc khi chuyển bước. Trong cấu hình học tập, chỉ cần cặp workflowId + storage trong Map; ở production thực, bạn sẽ thêm xác thực và kiểm tra quyền sở hữu người dùng.
Nếu bạn dùng Agents SDK hoặc điều phối phức tạp hơn, có thể mở rộng ý tưởng thành “checkpoint” — lưu trạng thái ở ranh giới các bước lớn để agent có thể tiếp tục khi khởi động lại. Nhưng đó là chủ đề của module tiếp theo.
9. Đi tới – lùi và lịch sử các bước
Câu hỏi chắc chắn xuất hiện: “Có thể quay lại một bước không?” Với người dùng, đây là mong muốn rất tự nhiên: đổi ngân sách, chỉnh sở thích, bỏ bớt sản phẩm khỏi gợi ý.
Về kỹ thuật, điều này có hai ý:
- cần lưu không chỉ bước hiện tại, mà còn lịch sử các quyết định;
- cần tính lại cẩn thận các dữ liệu dẫn xuất sau khi rollback.
Một phương án — thêm trường history vào ngữ cảnh, chứa các snapshot của bước. Ví dụ:
export interface StepSnapshot {
step: GiftWorkflowStep;
payload: any; // dữ liệu cụ thể của bước
createdAt: number;
}
export interface GiftWorkflowContext {
// ...các trường trước đó
history: StepSnapshot[];
}
Khi người dùng điền hồ sơ, bạn thêm vào lịch sử một snapshot với step: "profile". Khi đổi ngân sách — thêm snapshot nữa. Khi quay lại hồ sơ, bạn:
- cập nhật currentStep = "profile";
- tùy chọn cắt lịch sử đến chỉ số cần thiết;
- tính lại các giá trị dẫn xuất (ví dụ xóa ý tưởng và “like” nếu chúng phụ thuộc ngân sách).
Ở cấp mô hình, cần đồng bộ: nếu người dùng bấm nút “Quay lại” trong widget, phải gửi một lần gọi tool cập nhật ngữ cảnh nghiệp vụ và trả về mô tả tường minh về trạng thái mới. Nếu không, bạn sẽ gặp vấn đề mất đồng bộ kinh điển: UI hiển thị bước 2, còn mô hình chắc chắn rằng bạn đang ở bước 3.
Ở phía widget, rollback có thể là một nút đơn giản:
async function goBackToProfile() {
await fetch("/api/gift/workflow/back", {
method: "POST",
body: JSON.stringify({ workflowId, targetStep: "profile" })
});
// cập nhật UI, dọn local state
}
Phần máy chủ sẽ quyết định cần xóa gì trong ngữ cảnh và gửi thông điệp nào cho mô hình qua phản hồi tool.
10. Liên kết tất cả với mô hình: ngữ cảnh cho reasoning
Tất cả những gì chúng ta làm với state, cuối cùng không chỉ dành cho người dùng mà còn cho LLM. Mô hình cần hiểu:
- điều gì đã biết (ví dụ hồ sơ người nhận và ngân sách);
- những bước nào đã hoàn thành;
- có quy trình nào chưa hoàn tất hay không.
Cách chuyển thông tin này vào mô hình phụ thuộc vào kiến trúc App: bạn có thể inject vào system prompt, trả về trong ToolOutput ở dạng có cấu trúc hoặc dùng các trường đặc biệt _meta/annotations nếu SDK hỗ trợ.
Mẫu điển hình như sau:
- MCP‑tool trả về trong structuredContent một snapshot ngắn của ngữ cảnh: bước hiện tại, các trường chính và có thể cả workflowId.
- Apps SDK biến nó thành widget hoặc văn bản + dữ liệu ẩn.
- Mô hình, khi thấy structuredContent, hiểu rằng kịch bản đã tiếp tục và thực hiện hành động tiếp theo dựa trên đó.
Trong một số trường hợp, nếu mô hình “quên” tham số quan trọng hoặc bắt đầu “huyễn tưởng”, bạn có thể buộc cập nhật ngữ cảnh: gọi tool chuyên biệt trả về trạng thái hiện tại, và mô hình sẽ “vào lại ngữ cảnh”.
Quan trọng là đừng cố nhồi toàn bộ GiftWorkflowContext đến trường cuối cùng vào mô hình. Chỉ cần các điểm chính: đang tìm quà cho ai, ngân sách bao nhiêu, đã hiển thị bao nhiêu ý tưởng, có checkout chưa hoàn tất hay không.
11. Mini checklist khi thiết kế WorkflowContext
Trước khi chuyển sang các lỗi phổ biến, hữu ích khi có một bộ câu hỏi ngắn mà bạn nên tự trả lời khi thiết kế ngữ cảnh workflow (có thể ghi chú ngay cạnh interface):
- Kịch bản có những bước nào và tối thiểu cần dữ liệu gì ở mỗi bước?
Điều này giúp tránh những JSON “quái vật” khổng lồ kiểu “để phòng”. - Cần nhớ những gì chỉ trong phạm vi một chat, và những gì giữa các phiên, thiết bị?
Cái đầu có thể để trong widgetState và prompt, cái sau bắt buộc lưu vào CSDL phía máy chủ. - Định danh ngữ cảnh trông như thế nào?
Có thể là cặp userId + scenario, một workflowId riêng hoặc cả hai. Quan trọng là bạn có thể tìm ngữ cảnh trong CSDL một cách đơn nghĩa. - Bạn sẽ dọn các workflow cũ như thế nào?
Với demo có thể “không bao giờ dọn”, nhưng ở production bạn sẽ cần TTL hoặc các job chạy nền để xóa workflow cũ. - Người dùng có cần quay lại không và bạn sẽ triển khai ra sao?
Bạn sẽ lưu cây nhánh hay chỉ cần danh sách bước tuyến tính với khả năng rollback.
Cuối cùng: hãy thử “chạy” trong đầu kịch bản “người dùng quay lại sau một tuần trong chat khác”. Nếu bạn không thể giải thích App sẽ biết workflow cũ và hiển thị gì, thì cần củng cố phần lưu trữ bền vững.
12. Các lỗi thường gặp khi làm việc với ngữ cảnh giữa các bước
Lỗi số 1: chỉ lưu mọi thứ trong lịch sử hội thoại.
Đôi lúc có cám dỗ: “Mô hình thấy tất cả trong văn bản mà, cứ liệt kê mỗi lần trong prompt: ngân sách, sản phẩm, và người dùng đã chọn gì”. Cách này nhanh va vào giới hạn ngữ cảnh và hoàn toàn không đảm bảo toàn vẹn: mô hình có thể “quên” một fact quan trọng hoặc nhầm định danh. Những thứ trọng yếu cho business (tiền bạc, đặt chỗ, đơn hàng) phải sống trong backend/MCP của bạn như nguồn sự thật.
Lỗi số 2: cố xây cả workflow chỉ trên widgetState.
widgetState trong Apps SDK giải bài toán sống sót của UI state giữa unmount và mount lại widget, chứ không phải lưu trữ workflow dài hạn. Nếu cố lưu hồ sơ, giỏ hàng và lịch sử bước trong đó, bạn sẽ có hỗn loạn khi đổi thiết bị và không thể khôi phục sau thời gian dài. Widget chịu trách nhiệm cho các tiểu tiết hiển thị và sự tiện lợi cục bộ. Toàn bộ logic kịch bản phải sống trên máy chủ.
Lỗi số 3: thiếu workflowId hoặc khóa tường minh khác.
Đôi khi lập trình viên dựa vào các định danh không tường minh như conversation_id mà không đưa ra khái niệm workflow riêng. Kết quả là không thể phân biệt các kịch bản với nhau, tách nhiều workflow song song hoặc khôi phục đúng cái cần. Một chuỗi workflowId đơn giản ở mọi nơi có tool và API endpoint giải quyết hàng loạt vấn đề, đặc biệt là trong MCP — giao thức vốn stateless.
Lỗi số 4: trộn UI state và logic nghiệp vụ.
Tình huống kinh điển: người ta nhét không chỉ “tab nào mở” vào widgetState, mà còn cả “sản phẩm nào trong giỏ”, rồi dựa vào state đó để quyết định trên máy chủ. Hệ quả là chỉ cần lệch nhịp nhỏ (widget render rồi nhưng request chưa tới, hoặc ngược lại) là mô hình thấy một thực tại, UI thấy thực tại khác, còn CSDL lại là thực tại thứ ba. Ranh giới trách nhiệm phải rõ: máy chủ lưu và xác thực dữ liệu nghiệp vụ, widget hiển thị và cho người dùng cách tiện lợi để thay đổi chúng.
Lỗi số 5: thiếu kịch bản khôi phục và rollback.
Rất dễ vẽ “con đường hạnh phúc” nơi người dùng đi hoàn hảo qua các bước, không gì hỏng, ChatGPT không khởi động lại, và kết nối không rớt. Thực tế, mỗi bước đều có thể fail, người dùng có thể rời đi giữa chừng và quay lại sau một tuần. Nếu bạn không thiết kế cấu trúc WorkflowContext, không nghĩ cách tìm “workflow đang hoạt động” và không dự liệu nút “Quay lại” và “Tiếp tục sau”, kịch bản của bạn sẽ mong manh và gây khó chịu. Ngữ cảnh được thiết kế kỹ lưỡng là nền tảng cho khả năng chịu lỗi, sẽ được bàn trong bài giảng tiếp theo.
GO TO FULL VERSION