Tampilan
10.4 Bot Telegram Personal
Topologi: A — Agent tunggal Materi yang dipakai: 3.2, 3.3Prasyarat: 10.3
Masalahnya
Bot WhatsApp dari kasus sebelumnya mengingat percakapan, tapi ingatannya bergulir. Dengan lastMessages: 10, apa pun yang disebut sebelas pesan lalu sudah hilang.
Untuk asisten HR itu cukup. Untuk asisten pribadi, tidak.
Bayangkan pengguna berkata di hari Senin: "panggil aku Rian, aku di Surabaya, jawab pakai bahasa santai ya." Hari Kamis ia bertanya sesuatu yang sama sekali berbeda. Ia berharap bot masih tahu namanya, kotanya, dan gaya bahasanya — padahal percakapan Senin sudah jauh di belakang.
Menaikkan lastMessages menjadi 200 bukan jawaban: kamu membayar 200 pesan di setiap panggilan hanya untuk mempertahankan empat fakta.
Keputusan arsitektur
Keputusan 1 — Fakta pengguna: message history atau working memory?
lastMessages besar | Working memory | |
|---|---|---|
| Yang disimpan | Seluruh percakapan | Ringkasan fakta terpilih |
| Ukuran per panggilan | Tumbuh terus | Tetap kecil |
| Bertahan lintas thread | Tidak | Ya (scope resource) |
| Bisa dibaca manusia | Sulit | Ya — blok Markdown rapi |
| Risiko fakta hilang | Tinggi saat percakapan panjang | Rendah |
Pilihan: working memory dengan scope: 'resource'.
Ini persis masalah yang dirancang untuk diselesaikannya, seperti dibahas di 3.2: memisahkan fakta yang bertahan dari percakapan yang bergulir.
Keputusan 2 — Lingkup resource atau thread?
scope: 'resource' | scope: 'thread' | |
|---|---|---|
| Ingatan bertahan | Lintas semua percakapan pengguna | Hanya di satu percakapan |
| Cocok untuk | Asisten pribadi | Sesi konsultasi, perangkat bersama |
Risiko bila resource salah | Profil bocor antar pengguna | Terbatas satu thread |
Pilihan: resource.
Telegram memberi chat.id yang stabil per pengguna, jadi identitasnya jelas. Tapi perhatikan kolom terakhir — konsekuensi salah di sini lebih besar daripada di kasus 10.3.
Working memory memperbesar akibat resource yang salah
Di 10.3, resource yang tertukar berarti riwayat percakapan tercampur. Di sini, dengan working memory berlingkup resource, yang tercampur adalah profil pribadi — nama, kota, preferensi.
Pastikan resource berasal dari chat.id yang dikirim Telegram, bukan dari nama yang diketik pengguna dalam pesan.
Keputusan 3 — Perlu semantic recall juga?
| Cukup working memory | + Semantic recall | |
|---|---|---|
| Komponen tambahan | Nol | Vector store + embedder |
| Biaya per giliran | Nol tambahan | 2 panggilan embedding |
| Latensi | Tidak berubah | +1 langkah jaringan |
| Bisa temukan percakapan lama | Tidak | Ya, berdasarkan makna |
Pilihan: mulai tanpa semantic recall.
Working memory menyelesaikan 80% kebutuhan asisten pribadi dengan biaya nol tambahan. Semantic recall baru dibutuhkan saat pengguna merujuk percakapan lama secara spesifik — "waktu itu kita pernah bahas lensa apa ya?"
Cara menambahkannya nanti ditunjukkan di bagian akhir kasus ini.
Struktur direktori
text
asisten-telegram/
├── src/
│ ├── infra/
│ │ └── telegram.ts ← kirim & baca pesan Telegram
│ ├── server/
│ │ └── webhook.ts ← pola sama seperti 10.3
│ └── mastra/
│ ├── agents/
│ │ └── tg-agent.ts ← agent + working memory
│ └── index.ts
├── .env
└── package.jsonStrukturnya identik dengan kasus 10.3 — hanya infra/ yang berganti isi. Itu bukti pemisahan lapisan di kasus sebelumnya bekerja.
Alur
Kuncinya ada di panah A->>WM pada sesi kedua: profil dimuat meski thread-nya berbeda.
Implementasi
src/mastra/agents/tg-agent.ts
ts
import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/memory'
export const tgAgent = new Agent({
id: 'tg-agent',
name: 'Asisten Pribadi',
instructions: `Kamu asisten pribadi di Telegram.
Perhatikan preferensi pengguna dan sesuaikan jawabanmu: bahasa, panjang
jawaban, dan hal-hal yang ia minati.
Jawab ringkas kecuali diminta rinci.`,
model: 'openai/gpt-5-mini',
memory: new Memory({
options: {
lastMessages: 5,
workingMemory: {
enabled: true,
scope: 'resource',
template: `# Profil Pengguna
- **Nama**:
- **Kota**:
- **Preferensi bahasa**:
- **Minat**:
`,
},
},
}),
})Tiga hal yang menentukan kualitas di berkas ini.
lastMessages justru diturunkan ke 5. Karena fakta penting sekarang disimpan di working memory, riwayat pesan tidak perlu panjang. Ini menghemat biaya, bukan menambah.
Template hanya 4 field. Godaannya menulis 15 field "untuk jaga-jaga". Jangan — working memory ikut dikirim di setiap panggilan, dan field yang tidak pernah mempengaruhi jawaban adalah biaya murni.
Nama field tidak ambigu. Preferensi bahasa lebih baik daripada Bahasa, yang bisa diartikan "bahasa yang dikuasai" atau "bahasa yang diinginkan".
src/infra/telegram.ts
ts
const API = `https://api.telegram.org/bot${process.env.TG_TOKEN}`
export async function kirimPesan(chatId: number, teks: string): Promise<void> {
const res = await fetch(`${API}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, text: teks }),
})
if (!res.ok) console.error('gagal kirim TG', res.status, await res.text())
}
export function bacaPesanMasuk(body: unknown): { chatId: number; teks: string } | null {
const m = (body as any)?.message
if (!m?.text) return null
return { chatId: m.chat.id, teks: m.text }
}Bentuknya sengaja dibuat sama persis dengan infra/whatsapp.ts: kirimPesan dan bacaPesanMasuk. Antarmuka yang seragam membuat server/webhook.ts hampir tidak berubah antar kanal.
src/server/webhook.ts
ts
import { mastra } from '../mastra/index.ts'
import { kirimPesan, bacaPesanMasuk } from '../infra/telegram.ts'
async function prosesPesan(chatId: number, teks: string): Promise<void> {
const agent = mastra.getAgentById('tg-agent')
const hasil = await agent.generate(teks, {
memory: {
resource: `tg:${chatId}`, // profil — bertahan lintas sesi
thread: `tg:${chatId}:${hariIni()}`, // percakapan — direset harian
},
maxSteps: 5,
})
await kirimPesan(chatId, hasil.text)
}
function hariIni(): string {
return new Date().toISOString().slice(0, 10) // 2026-09-06
}
export async function handleWebhook(body: unknown): Promise<void> {
const pesan = bacaPesanMasuk(body)
if (!pesan) return
void prosesPesan(pesan.chatId, pesan.teks).catch(err => {
console.error('gagal memproses pesan', err)
})
}Perbedaan halus dari kasus 10.3
Perhatikan thread di sini memuat tanggal, sementara resource tidak.
Akibatnya: percakapan direset tiap hari — pertanyaan kemarin tidak mengotori konteks hari ini — tapi profil tetap bertahan karena ia berlingkup resource.
Ini pola yang berguna untuk asisten pribadi, dan berbeda dari WhatsApp di kasus 10.3 yang memang cocok satu utas abadi.
Hasil nyata
Dijalankan sungguhan. Sesi kedua memakai thread yang benar-benar baru:
text
===== sesi-1 =====
> Halo, aku Rian dari Surabaya. Tolong selalu jawab pakai bahasa Indonesia
santai ya.
< Halo Rian! Sip—aku bakal selalu jawab pakai Bahasa Indonesia santai.
Ada yang mau ditanyain atau dibantu sekarang?
> aku lagi belajar fotografi malam
< Keren, Rian — mantap! Aku catat kamu lagi belajar fotografi malam.
===== sesi-2 (THREAD BARU, resource sama) =====
> rekomendasiin kegiatan akhir pekan dong
< Oke Rian—nih beberapa ide akhir pekan di Surabaya yang pas buat kamu yang
suka fotografi malam. Santai aja, pilih sesuai mood:
1) Sesi foto malam di Jembatan Suramadu (sisi Surabaya)
- Kenapa: lampu jembatan + garis panjang cocok buat long exposure.
- Tips: bawa tripod, lensa wide + tele, remote shutter, datang 30–60
menit sebelum blue hour.
2) Surabaya North Quay / area pelabuhan
...Pesan di sesi-2 hanya berbunyi "rekomendasiin kegiatan akhir pekan dong" — tidak menyebut nama, kota, minat, atau gaya bahasa. Tapi jawabannya memuat keempatnya:
| Yang muncul di jawaban | Berasal dari field |
|---|---|
| "Oke Rian" | Nama |
| "di Surabaya" | Kota |
| "yang suka fotografi malam" | Minat |
| "nih", "santai aja", "buat kamu" | Preferensi bahasa |
Empat field template terisi di sesi-1 dan terbawa utuh ke sesi-2. Itu yang tidak bisa dilakukan message history, berapa pun besar lastMessages-nya — karena thread-nya memang berbeda.
Menambahkan semantic recall
Saat pengguna mulai merujuk percakapan lama secara spesifik, tambahkan komponennya. Perubahannya terbatas di konstruktor Memory:
ts
import { Memory } from '@mastra/memory'
import { LibSQLStore, LibSQLVector } from '@mastra/libsql'
import { ModelRouterEmbeddingModel } from '@mastra/core/llm'
memory: new Memory({
storage: new LibSQLStore({ id: 'tg-storage', url: 'file:./tg.db' }),
vector: new LibSQLVector({ id: 'tg-vector', url: 'file:./tg.db' }),
embedder: new ModelRouterEmbeddingModel('openai/text-embedding-3-small'),
options: {
lastMessages: 5,
semanticRecall: true,
workingMemory: { enabled: true, scope: 'resource', template: '...' },
},
})| Untung | Rugi |
|---|---|
| Bisa menemukan percakapan lama berdasarkan makna | 2 panggilan embedding tiap giliran |
| Tidak perlu pengguna mengingat kata persisnya | Latensi bertambah satu langkah jaringan |
| Bekerja berdampingan dengan working memory | Mengganti model embedding jadi migrasi data |
Perhatikan storage dan vector menunjuk berkas yang sama — untuk pengembangan lokal itu praktis, seperti dibahas di 3.3.
Best practice yang dipakai di sini
1. Antarmuka infra/ yang seragam antar kanal. kirimPesan dan bacaPesanMasuk punya bentuk sama di WhatsApp dan Telegram, sehingga lapisan di atasnya tidak peduli kanal mana.
2. Template working memory sependek mungkin. Empat field yang benar-benar mempengaruhi jawaban, bukan lima belas yang "mungkin berguna".
3. thread dan resource boleh punya aturan berbeda. Thread bertanggal
- resource stabil adalah kombinasi yang memberi percakapan bersih tiap hari tanpa kehilangan profil.
4. Turunkan lastMessages saat working memory aktif. Keduanya menyelesaikan masalah berbeda; membiarkan lastMessages besar setelah memasang working memory berarti membayar dua kali untuk hal yang sama.
Kapan pola ini salah
| Situasi | Kenapa gagal | Ke mana |
|---|---|---|
| Perangkat dipakai bergantian banyak orang | Profil orang sebelumnya terbawa | Pakai scope: 'thread' |
| Sesi berlangsung berhari-hari tanpa putus | Working memory tidak merangkum jalannya pekerjaan | 3.4 Observational memory |
| Pengguna bertanya isi dokumen | Ini pengetahuan, bukan profil | 10.5 Helpdesk |
| Bot harus proaktif mengirim pesan | Pola ini hanya bereaksi | 10.6 Terjadwal |
Coba sendiri
Bangun asisten belajar bahasa di Telegram yang menyesuaikan diri dengan tiap murid.
Template working memory harus memuat: nama, tingkat kemampuan, topik yang sudah dikuasai, dan pola kesalahan yang berulang.
Selesai kalau
- [ ] Setelah 4 giliran di sesi-1, working memory di Studio memuat keempat field terisi
- [ ] Di thread baru, agent menyapa dengan nama dan menyesuaikan tingkat kesulitan
- [ ] Field "pola kesalahan" terisi hasil pengamatan agent, bukan karena murid menyebutkannya
- [ ] Dua
resourceberbeda tidak saling melihat profil - [ ]
lastMessagestidak lebih dari 6
Kalau macet
Field "pola kesalahan" selalu kosong? Nama field-nya kurang tegas. Model tidak tahu ia diharapkan menyimpulkan. Coba: Pola kesalahan yang kamu amati (isi sendiri berdasarkan jawaban murid, jangan tunggu diberitahu).
Profil tidak terbawa ke thread baru? Periksa scope: 'resource' — kalau thread atau tidak diisi, ingatannya terkurung di satu percakapan.
Lanjut ke mana
Bot ini mengenal penggunanya, tapi pengetahuannya masih terbatas apa yang ada di instruksi. Kasus berikutnya memberinya akses ke ratusan halaman dokumen — 10.5 Helpdesk Dokumen.