Grounded AI Trip Planner
Teknik mengikat output Large Language Model (LLM) agar hanya merekomendasikan destinasi nyata yang tersimpan di database.
Membaca sekitar 2 menit
Teknik mengikat output Large Language Model (LLM) agar hanya merekomendasikan destinasi nyata yang tersimpan di database.
Menyaring maksimal 60 destinasi terbaik sesuai minat pengguna, memperkecil ukuran payload JSON di prompt, lalu menyuntikkan kembali URL dan foto asli dari database setelah AI merespons.
Tanpa pembatasan konteks dan rekonsiliasi slug pasca-inferensi, AI mudah berhalusinasi membuat nama wisata fiktif atau menembus batas token.
Masalahnya dulu
Saat meminta AI menyusun rencana perjalanan (itinerary) ke Yogyakarta, masalah klasik yang langsung muncul adalah halusinasi dan pembengkakan token. Jika dibiarkan bebas, LLM bisa mengarang nama tempat wisata yang sudah tutup atau tidak ada di aplikasi kita. Sebaliknya, jika seluruh isi tabel destinasi beserta deskripsi panjangnya dikirim mentah-mentah ke dalam prompt, ukuran request membengkak, waktu tunggu menjadi lama, dan sering gagal karena batas konteks.
Menyaring konteks dan mengikat output dengan slug
Di TripPlannerController, sebelum memanggil API Groq (model llama-3.3-70b-versatile), aplikasi memetakan minat yang dipilih pengguna ke kategori database, mengurutkannya berdasarkan rating tertinggi, dan membatasinya maksimal 60 destinasi. Setiap objek destinasi pun dipadatkan menggunakan kunci satu huruf (s untuk slug, n untuk nama, k untuk kategori, w untuk wilayah, dan r untuk rating). Setelah AI membalas dengan JSON, sistem mencocokkan kembali setiap slug dengan data asli di database untuk menyuntikkan URL halaman detail dan foto sampul (hero).
Jangan biarkan AI mengarang data yang sudah kita miliki di database; jadikan AI sebagai penyusun strategi, lalu rekonsiliasi hasilnya dengan kunci primer atau slug asli.
Implementasi di ngodingan ini
Berikut adalah proses pemadatan konteks destinasi, pembersihan respons JSON dari markdown, hingga penyuntikan data database kembali ke dalam struktur itinerary di TripPlannerController:
$destinations = $destinationsForPromptQuery
->get()
->sortByDesc(fn($d) => $d->averageRating())
->take($maxDestinationsForPrompt)
->map(fn($d) => [
's' => $d->slug,
'n' => $d->title,
'k' => $d->category->name,
'w' => $d->district,
'r' => $d->averageRating(),
]);
// Bersihkan bungkus markdown jika AI masih menyertakannya
$content = preg_replace('/```json\s*/i', '', $content);
$content = preg_replace('/```\s*/i', '', $content);
$itinerary = json_decode(trim($content), true);
// Rekonsiliasi dengan data destinasi asli dari DB
$destMap = Destination::approved()->with('hero')->get()->keyBy('slug');
foreach ($itinerary['hari'] as &$hari) {
foreach ($hari['destinasi'] as &$dest) {
$dbDest = $destMap->get($dest['slug'] ?? '');
$dest['url'] = $dbDest ? route('destinations.show', $dest['slug']) : null;
$dest['hero_url'] = $dbDest?->hero ? asset('storage/' . $dbDest->hero->path) : null;
$dest['found'] = (bool) $dbDest;
}
}Jebakan dan detail yang harus dijaga
- Meski sudah diinstruksikan di system prompt agar membalas hanya dengan JSON murni, LLM sesekali tetap membungkus output dengan ```json atau kalimat pengantar. Pembersihan berlapis dengan preg_replace dan ekstraksi kurung kurawal menggunakan regex /{.*\}/s wajib dipasang sebelum json_decode.