Marker CTA_FLAG: YES yang Bocor ke Chat
Regex strip di akhir string nggak cukup: marker CTA dari LLM bisa lolos lewat tengah teks dan jalur streaming SSE sebelum post-processing jalan.
Ringkasan
Marker CTA_FLAG: YES bocor ke user lewat dua jalur: LLM naruh marker di posisi acak yang luput dari regex ber-anchor $, dan jalur streaming SSE melewati post-processing. Patch-nya buang anchor dan strip marker di stream juga. Fix fundamentalnya: tombol CTA dirender dari field terstruktur, bukan ditumpang ke teks LLM lalu di-parse regex.
Gue lagi cek demo fitur jawaban AI di staging, scroll ke bawah ke bubble terakhir. Di situ ada tombol WhatsApp verified-check yang harusnya muncul. Tapi di atas tombol itu, ada teks yang nggak seharusnya ada: CTA_FLAG: YES. Tampil mentah, di depan user, di dalam bubble chat.
Gue langsung kira ini masalah rendering frontend, mungkin ada conditional render yang kelewat. Buka AiAnswer.tsx, cek logic-nya. Ternyata nggak. Marker itu emang masuk sebagai bagian dari teks jawaban, bukan dipisah dari awal.
Regex yang Kira-Kira Udah Cukup
Sebelum commit ini, logic-nya simpel. Setelah LLM selesai jawab, ada post-processing yang jalan. Regex strip marker dari akhir string jawaban: \s*CTA_FLAG:\s*YES\s*$. Kalo marker ada di ujung, keangkut, dibuang, dan frontend tinggal render field cta dari knowledge source tanpa gangguan.
Gue kira itu udah cukup. Marker cuma satu kata kunci, LLM cuma perlu tempel di akhir jawaban, regex buang, selesai. Kenyataannya ada dua masalah yang bikin asumsi itu runtuh.
Pertama: LLM nggak selalu naruh marker di akhir. Kadang di tengah paragraf, kadang di awal kalimat kedua, tergantung gimana model "berpikir" waktu generate teks. Regex yang di-anchoring ke $ cuma nangkep yang persis di akhir string. Kalo marker nyempil di tengah, ya lolos.
Kedua, dan ini yang lebih kritis: jalur streaming.
Jawaban AI di aplikasi kita di-stream. Per delta, teks di-yield lewat SSE ke client [3]. Artinya user udah lihat teks sebelum LLM beneran selesai generate seluruh jawaban. Post-processing yang strip marker? Itu cuma jalan di routes.py, di jalur penyimpanan jawaban, setelah stream selesai. Tapi delta yang udah ke-stream ke client? Udah kelewat. Nggak ada yang recall teks yang udah dikirim lewat event stream.
Jadi marker lolos lewat dua jalur sekaligus: posisi yang nggak terprediksi di dalam teks, dan path streaming yang bypass post-processing. Dua gerbang berbeda, dua celah berbeda, satu gejala yang sama.
Gue sadar pola ini juga ada di sisi React. useEffect cuma jalan di client [4], dan kalo komponen termount setelah interaksi tertentu, effect bisa kelewat. Bukan berarti ini masalah yang sama secara teknis, tapi mental model yang sama: ada jalur data yang nggak melewati satu gate tunggal, dan kita asumsikan semua data lewat situ.
Pisahkan Prose dari Data
Solusi di commit ini dua langkah. Pertama, regex di-fix: $ dibuang biar marker kena di posisi mana pun, dan strip dijalankan juga di jalur stream di use_cases.py. Tapi itu cuma patch. Masalah fundamentalnya tetap ada: kita andalkan LLM buat naruh marker teks yang harusnya jadi data terstruktur.
Langkah kedua yang lebih penting: CTA nggak lagi ditempel ke jawaban oleh LLM. Frontend render tombol WhatsApp dari field terstruktur cta yang udah ada di knowledge sources. Marker CTA_FLAG: YES di prompt cuma instruksi buat model — bukan kontrak data. Hasilnya, AiAnswer.tsx baca field itu, render tombol verified-check secara independen dari teks prose.
Ini sejalan dengan prinsip yang menurut gue sering dilupain: keluaran model yang butuh bentuk tertentu itu lebih reliable lewat structured output, bukan parsing teks bebas [1]. JSON mode menjamin JSON valid, tapi kalo kamu mau model patuh sama schema tertentu, structured outputs yang lebih cocok. Prompt bukan kontrak yang bisa diandalkan seratus persen. OWASP juga catat kalo mitigasi via system prompt dan input handling itu membantu, tapi bukan pencegahan penuh [2].
Gue pribadi berpendapat: setiap kali kita tempel "flag" atau "marker" ke dalam teks output LLM dan berharap regex bisa bersihin secara konsisten, kita udah bikin kontrak rapuh. Marker yang kita kontrol penuh (field terstruktur) jauh lebih reliable daripada marker yang kita harap LLM taruh di tempat yang tepat. Prose biar jadi prose. Data terstruktur biar jadi data terstruktur.
Sumber
- [1] Structured model outputs | OpenAI API (akses 2026-09-01)
- [2] LLM01:2025 Prompt Injection | OWASP Gen AI Security Project (akses 2026-09-01)
- [3] Using server-sent events | MDN (akses 2026-09-01)
- [4] useEffect – React (akses 2026-09-01)
- Artikel terkait: Satu Stream SSE, Dua Versi Jawaban