← Kembali ke Blog

Memori Jangka Panjang buat AI Agent: Mem0, Qdrant, dan Ollama, Full Self-Hosted

Asisten kamu tajam dalam satu sesi, terus blank begitu prosesnya di-restart. Tambalan yang paling umum adalah menempel seluruh transkrip ke setiap prompt, dan cara itu bertahan sampai transkripnya menyentuh 60 ribu atau 80 ribu token. Lewat dari itu kamu bayar riwayat yang sama di setiap giliran, latency naik, dan model mulai kehilangan detail yang terkubur di tengah context window.

Memori sebagai layer terpisah membenahi bentuk masalahnya, bukan cuma gejalanya. Mem0 adalah memory service open source buat aplikasi LLM: kamu serahkan percakapan ke dia, dia menarik fakta yang layak disimpan, dan saat ada pertanyaan kamu ambil balik beberapa memori yang relevan saja.

Tutorial ini menjalankan seluruh stack di mesin kamu sendiri. Mem0 open source sebagai library Python, Qdrant buat vektor, Ollama buat LLM ekstraksi sekaligus model embedding. Tanpa API key, tanpa tagihan per token, dan nggak ada data yang keluar dari mesin kamu.

Yang perlu kamu siapkan

  • Python 3.10 atau lebih baru, karena paket mem0ai butuh >=3.10, <4.0
  • Docker, buat Qdrant
  • Ollama yang sudah terinstall dan jalan (ollama --version)
  • Ruang disk beberapa GB buat volume Qdrant plus model yang kamu pull

Langkah 1: jalankan Qdrant

docker run -d --name qdrant -p 6333:6333 -p 6334:6334 \
  -v "$(pwd)/qdrant_storage:/qdrant/storage:z" \
  qdrant/qdrant

Ada tiga port dan path yang penting di sini. REST API menjawab di localhost:6333, dashboard-nya di localhost:6333/dashboard, dan endpoint gRPC di 6334. Mem0 secara default ngobrol lewat REST, jadi 6333 yang kamu pakai di config.

Langkah 2: pull model Ollama

ollama pull llama3.1:latest
ollama pull nomic-embed-text:latest

llama3.1 yang mengerjakan ekstraksi, artinya dia baca percakapan dan menentukan pernyataan mana yang jadi memori. nomic-embed-text mengubah tiap memori jadi vektor 768 dimensi. Catat angka itu, karena ketidakcocokan antara ukuran output embedder dan ukuran yang diharapkan collection adalah error yang paling sering kamu tabrak.

Langkah 3: install Mem0

python -m venv .venv && source .venv/bin/activate
pip install mem0ai qdrant-client openai

Kalau pencarianmu melibatkan identifier persis seperti nomor order, hostname, atau kode error, install extra yang mengaktifkan BM25 keyword matching dan entity extraction:

pip install "mem0ai[nlp]"
python -m spacy download en_core_web_sm

Query yang sifatnya parafrase tetap jalan tanpa ini. Query yang harus cocok kata per kata nggak.

Langkah 4: konfigurasi instance memory

from mem0 import Memory

OLLAMA = "http://localhost:11434"

CUSTOM_INSTRUCTIONS = """
Extract only durable facts from support conversations:
- Customer identity and contact details
- Product, plan, or order identifiers
- Stated preferences and constraints
- Problems already reported and their current status

Exclude greetings, small talk, and anything hypothetical.

Input: hi
Output: {"facts": []}

Input: My order #A-4471 still shows as packing after nine days.
Output: {"facts": ["Order #A-4471 stuck in packing status after nine days"]}

Return JSON with a single key "facts" as a list of strings.
"""

config = {
    "vector_store": {
        "provider": "qdrant",
        "config": {
            "collection_name": "agent_memory",
            "host": "localhost",
            "port": 6333,
            "embedding_model_dims": 768,
        },
    },
    "llm": {
        "provider": "ollama",
        "config": {
            "model": "llama3.1:latest",
            "temperature": 0,
            "max_tokens": 2000,
            "ollama_base_url": OLLAMA,
        },
    },
    "embedder": {
        "provider": "ollama",
        "config": {
            "model": "nomic-embed-text:latest",
            "ollama_base_url": OLLAMA,
        },
    },
    "custom_instructions": CUSTOM_INSTRUCTIONS,
}

memory = Memory.from_config(config)

Ada tiga detail di config itu yang gampang salah.

custom_instructions adalah key level atas, bukan bagian dari blok llm, dan harus di-set sebelum Memory.from_config() dipanggil. Skrip lama menyebutnya custom_fact_extraction_prompt, nama parameternya sudah diganti, jadi perbarui kalau kamu memindahkan kode yang dulu jalan. Apa pun prompt yang kamu tulis, extractor mengharapkan balasan JSON dengan array facts.

embedding_model_dims harus sama dengan output asli embedder-nya. nomic-embed-text menghasilkan 768. Kalau kamu pindah ke embedder OpenAI, text-embedding-3-small bawaan menghasilkan 1536, dan itu berarti kamu butuh nama collection baru, karena Qdrant menolak vektor dengan ukuran berbeda untuk collection yang sudah ada.

Penting juga tahu apa yang terjadi kalau kamu kasih config kosong. Memory() tanpa argumen pakai OpenAI gpt-5-mini buat ekstraksi, text-embedding-3-small buat embedding, menyimpan vektor di instance Qdrant on-disk di /tmp/qdrant, dan mencatat riwayat operasi di SQLite pada ~/.mem0/history.db. Jalur itu tetap minta OPENAI_API_KEY walau kamu merasa sedang jalan lokal.

Langkah 5: loop ambil, jawab, simpan

from openai import OpenAI

# Ollama menyediakan endpoint yang kompatibel dengan OpenAI di /v1
chat = OpenAI(base_url=f"{OLLAMA}/v1", api_key="ollama")

def ask(user_input, user_id):
    hits = memory.search(user_input, filters={"user_id": user_id}, top_k=5)
    facts = [h["memory"] for h in hits["results"]]
    context = "\n".join(facts) if facts else "(nothing stored yet)"

    reply = chat.chat.completions.create(
        model="llama3.1:latest",
        messages=[
            {
                "role": "system",
                "content": f"You are a support agent. Known facts about this user:\n{context}",
            },
            {"role": "user", "content": user_input},
        ],
    ).choices[0].message.content

    memory.add(
        [
            {"role": "user", "content": user_input},
            {"role": "assistant", "content": reply},
        ],
        user_id=user_id,
    )
    return reply

search dan get_all sama-sama mengembalikan dict dengan key results, dan tiap itemnya membawa id, memory, score, serta created_at. Iterasi hits["results"], bukan responsnya langsung, kalau nggak kamu bakal sepuluh menit mengejar error string index.

Langkah 6: buktikan memorinya persist

ask("My order #A-4471 still hasn't shipped", "dewi")
# ... hentikan proses, jalankan lagi ...
print(ask("Any update on my order?", "dewi"))

stored = memory.get_all(filters={"user_id": "dewi"})
for m in stored["results"]:
    print(m["id"], "|", m["memory"])

Panggilan kedua nggak punya riwayat percakapan di prompt-nya tapi tetap menjawab dengan nomor order, karena datanya datang balik dari Qdrant. Dump di bagian akhir itu langkah yang sering dilewatkan lalu disesali. Tanpa custom_instructions, daftar itu pelan-pelan terisi entri seperti "hey" dan "thanks", dan kualitas retrieval-nya turun. Jalankan setiap kali kamu mengubah prompt ekstraksinya.

Kalau kamu sudah tahu sebuah fakta dan ingin disimpan apa adanya, lewati LLM ekstraksinya:

memory.add(
    [{"role": "user", "content": "Customer is on the Enterprise plan, invoice by email."}],
    user_id="dewi",
    infer=False,
)

infer=False menulis teksnya verbatim. Tanpa panggilan ekstraksi, tanpa perubahan kalimat, tanpa biaya.

Langkah 7: rapikan memori pakai metadata

Mem0 open source nggak punya sistem kategori. Kategori itu cuma field metadata yang kamu tentukan sendiri saat add, dan filter hanya melihat key yang benar-benar kamu tulis.

memory.add(
    [{"role": "user", "content": "Prefers invoice by email, not paper."}],
    user_id="dewi",
    metadata={"bucket": "preferences"},
)

prefs = memory.search(
    "billing preferences",
    filters={"user_id": "dewi", "bucket": "preferences"},
)

Mulai dengan dua atau tiga bucket saja. Tiap key tambahan yang kamu filter adalah key tambahan yang harus kamu ingat untuk di-set di setiap penulisan.

Agent yang berbeda tapi berbagi satu user memakai agent_id:

memory.add(messages, user_id="dewi", agent_id="billing_bot")
memory.search("escalation rules", filters={"agent_id": "billing_bot"})

Ada satu jebakan soal filter tanggal: kondisi range di Qdrant hanya berlaku untuk field payload numerik, jadi tanggal berbentuk string di metadata nggak akan pernah cocok dengan filter {"gte": ...}. Simpan epoch integer.

import time

memory.add(
    [{"role": "user", "content": "Reported a duplicate charge on the last invoice."}],
    user_id="dewi",
    metadata={"logged_epoch": int(time.time())},
)

cutoff = int(time.time()) - 30 * 24 * 3600
recent = memory.search(
    "billing problems",
    filters={"user_id": "dewi", "logged_epoch": {"gte": cutoff}},
)

Langkah 8: kedaluwarsakan fakta yang memang sementara

Mem0 open source nggak menghapus apa pun sendiri. Pola yang didokumentasikan adalah menyimpan masa kedaluwarsa bareng memorinya lalu prune secara terjadwal. Pakai epoch detik lagi supaya filter range-nya jalan:

WEEK = 7 * 24 * 3600

memory.add(
    [{"role": "user", "content": "Card was declined, retrying next week."}],
    user_id="dewi",
    metadata={"bucket": "constraints", "expires_at": int(time.time()) + WEEK},
)

def prune(user_id):
    now = int(time.time())
    stale = memory.get_all(filters={"user_id": user_id, "expires_at": {"lt": now}})
    for m in stale["results"]:
        memory.delete(memory_id=m["id"])
    return len(stale["results"])

print("removed", prune("dewi"), "expired memories")

Jalankan dari cron atau dari scheduler yang sudah kamu pakai. Pasang sebelum launch, bukan setelah memori basi pertama bikin jawaban salah ke customer asli.

Error yang kemungkinan besar kamu temui

Invalid input, expected vector size 768, got 1536 artinya embedder dan embedding_model_dims nggak sepakat. Perbaiki angkanya, atau pakai nama collection baru.

OPENAI API key not found padahal kamu yakin sudah setup Ollama berarti ada satu blok provider yang masih default ke OpenAI. Cek llm dan embedder satu per satu.

Pencarian yang nggak balik apa-apa biasanya karena threshold kemiripan bawaan menyaring semua hasil. Pakai threshold=0 saat debugging, dan bandingkan dengan get_all buat lihat apakah memorinya memang tersimpan.

Array results yang kosong dari add bukan kegagalan. Itu artinya custom_instructions kamu bilang ke extractor bahwa pesan itu nggak berisi apa pun yang tahan lama, dan itu jawaban yang benar untuk "hey".

Kapan pakai ini, kapan pilih yang lain

Pakai library mode (pip install mem0ai) kalau satu aplikasi yang punya memorinya dan jalan di proses kamu sendiri. Itu yang dibangun di tutorial ini, dan titik awal yang tepat buat sebagian besar project. Pakai self-hosted server kalau beberapa service atau bahasa perlu baca-tulis memori yang sama lewat REST API, yang disediakan Mem0 sebagai stack Docker Compose. Pakai versi cloud kalau kamu nggak mau mengurus Qdrant sendiri.

Pilih tool lain kalau bentuk masalahnya beda. Kalau kamu butuh workflow yang bisa dilanjutkan dengan step dan checkpoint, LangGraph checkpointer lebih pas, karena yang kamu simpan adalah state eksekusi, bukan fakta tentang user. Kalau kamu mau modelnya sendiri yang merawat dan menulis ulang konteksnya, Letta (dulu MemGPT) dibangun di sekitar memory block yang bisa diedit. Dan kalau kamu sudah jalanin Postgres dan nggak mau ada service baru, pgvector plus prompt ekstraksi buatan sendiri tetap pilihan sah, dengan catatan kamu sekarang yang punya schema ekstraksi, logika dedup, dan ranking-nya.

Soal angka: Mem0 melaporkan algoritma April 2026-nya di 92.5 untuk LoCoMo, naik dari 71.4, dan 94.4 untuk LongMemEval, dengan di bawah 7.000 token per panggilan retrieval. Dua catatan datang langsung dari README Mem0 sendiri. Skor itu berasal dari platform terkelola yang punya optimasi yang nggak ada di SDK open source, jadi ekspektasinya arah yang sama, bukan angka yang sama. Dan benchmark-nya dilaporkan sendiri oleh vendor. Bagian yang paling berguna justru jumlah tokennya, karena pendekatan full-context di benchmark yang sama menghabiskan 25.000+ token per query. Selisih itulah biaya yang kamu hindari dengan menaruh memori di luar prompt.

Langkah berikutnya

Satu collection per environment, supaya data test nggak pernah bercampur dengan data production. Dua atau tiga bucket metadata, jangan lebih. Log setiap respons add selama minggu pertama dan baca fakta yang diekstrak, karena di situ kualitasnya ditentukan. Tambahkan job prune. Setelah retrieval-nya masuk akal di query nyata, install mem0ai[nlp] dan ukur apakah hybrid keyword matching layak dibawa sebagai dependensi.

Referensi

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis