Tantangan Streaming Payload LLM ke UI Agent
Mengintegrasikan keluaran Large Language Model (LLM) langsung ke antarmuka aplikasi interaktif mengharuskan sistem mengurai data terstruktur di tengah aliran teks (streaming). Arsitektur seperti CopilotKit OpenTag menyematkan tag khusus (misalnya <action name="...">{...}</action>) langsung di dalam aliran teks respons. Namun, antarmuka front-end rentan rusak akibat tiga masalah teknis mendasar:
- Payload Parsial: UI mencoba mengurai JSON yang belum selesai ditransmisikan, memicu
SyntaxError: Unexpected end of JSON input. - Schema Mismatch Runtime: Model menghasilkan struktur argumen yang menyimpang dari kontrak API yang diharapkan oleh komponen front-end.
- Eksekusi Ganda: Aliran teks yang memicu re-render React berulang kali dapat mengeksekusi efek samping (seperti mutasi data atau pemanggilan API eksternal) lebih dari sekali.
Solusinya adalah menerapkan state machine parsing inkremental, validasi runtime via Zod, dan manajemen cache status eksekusi berbasis ID unik.
Definisi Kontrak Schema Type-Safe dengan Zod
Front-end dan LLM harus menyepakati kontrak payload yang ketat. Gunakan Zod untuk mendefinisikan skema validasi runtime sekaligus mengekstrak tipe statis TypeScript secara otomatis.
import { z } from 'zod';
export const ShowProductModalSchema = z.object({
actionId: z.string().uuid(),
productId: z.string().min(1),
viewMode: z.enum(['compact', 'detailed']).default('compact'),
metadata: z.record(z.string()).optional(),
});
export const ActionTagPayloadSchema = z.discriminatedUnion('actionName', [
z.object({
actionName: z.literal('show_product_modal'),
payload: ShowProductModalSchema,
}),
z.object({
actionName: z.literal('navigate_route'),
payload: z.object({
actionId: z.string().uuid(),
path: z.string().startsWith('/'),
}),
}),
]);
export type ActionTagPayload = z.infer<typeof ActionTagPayloadSchema>;
Parsing Streaming Tag dan Penanganan JSON Parsial
Selama streaming LLM berlangsung, buffer teks perlu dipindai untuk mendeteksi batas tag pembuka dan penutup. Jika tag penutup belum diterima atau JSON di dalamnya belum lengkap, parser harus menahan eksekusi atau merender status skeleton.
interface ParsedOpenTag {
actionName: string;
rawJson: string;
isComplete: boolean;
}
export function extractOpenTags(streamBuffer: string): ParsedOpenTag[] {
const tagRegex = /<action\s+name=["']([^"']+)["']>([\s\S]*?)(<\/action>|$)/g;
const results: ParsedOpenTag[] = [];
let match: RegExpExecArray | null;
while ((match = tagRegex.exec(streamBuffer)) !== null) {
const [, actionName, innerContent, closingTag] = match;
results.push({
actionName,
rawJson: innerContent.trim(),
isComplete: closingTag === '</action>',
});
}
return results;
}
Runtime Validator dan Idempotensi Eksekusi Side-Effect
Setelah tag berstatus isComplete: true, lakukan parsing JSON yang aman dan validasi skema runtime. Untuk mencegah eksekusi ganda pada setiap chunk token baru, lacak actionId menggunakan Set atau state persistensi.
const executedActions = new Set<string>();
export function processActionStream(rawTag: ParsedOpenTag): void {
if (!rawTag.isComplete) {
return; // Tunggu stream tag selesai sepenuhnya
}
let parsedJson: unknown;
try {
parsedJson = JSON.parse(rawTag.rawJson);
} catch {
console.error(`Malformed JSON inside tag <action name="${rawTag.actionName}">`);
return;
}
const validationResult = ActionTagPayloadSchema.safeParse({
actionName: rawTag.actionName,
payload: parsedJson,
});
if (!validationResult.success) {
console.warn('Payload contract mismatch:', validationResult.error.flatten());
return;
}
const { actionName, payload } = validationResult.data;
// Cek Idempotensi
if (executedActions.has(payload.actionId)) {
return;
}
executedActions.add(payload.actionId);
dispatchActionToUI(actionName, payload);
}
function dispatchActionToUI(name: string, payload: unknown) {
window.dispatchEvent(new CustomEvent('agent-action', { detail: { name, payload } }));
}
Fallback Handling Saat Tag Rusak di Tengah Stream
LLM terkadang berhenti menghasilkan token sebelum tag tertutup secara valid akibat max_tokens terlampaui atau gangguan koneksi HTTP chunked transfer. Penanganan kesalahan harus mencakup tiga lapisan proteksi:
- Stream Abort Detection: Jika stream selesai (EOF) tetapi tag tetap tidak memiliki penutup, parser harus mengabaikan tag tersebut atau memindahkan teks ke dalam alur pesan teks biasa agar UI tidak hang.
- Partial JSON Recovery: Gunakan library deserialisasi toleran (seperti
dirty-jsonatau modul partial-json parser) hanya jika UI memerlukan tampilan spekulatif sebelum stream selesai. - Schema Validation Error Boundary: Tangkap error parsing pada UI component boundary. Jika skema gagal divalidasi, tampilkan pesan fallback non-intrusif ke pengguna alih-alih merusak seluruh visual tree React.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!