How to Build a Multi-Session AI Assistant with Zep

September 26, 2026

One chat session is easy. The hard part is when the same person comes back on Tuesday and your agent forgot everything from Monday. Zep solves this with a user-and-session model, and this guide focuses on the wiring that trips people up.

Direct answer: Create one Zep user per person, one session per conversation thread, then search across all sessions with memory.search to pull cross-thread context at answer time.

Why Does One Big Session Break Down?

New builders dump every message into a single session. It works for a demo, then falls apart. Summaries get noisy, latency grows, and Zep cannot tell a quick throwaway question from a project you have been running for months.

The fix is to treat sessions as threads, not as users. A user is a person. A session is one line of work — a support ticket, a coding task, a weekly check-in.

How Should You Model Users and Sessions?

Use stable IDs you already own. Do not let Zep invent them.

Concept Maps to Example
user_id Your account/user record u_8421
session_id One conversation thread s_8421_2026_09_draft
graph Optional shared knowledge pool org_iaifeed

If several sessions should share facts — say an org-wide policy — attach them to the same graph ID. Facts written there become visible to every session in that graph.

How Do You Write and Read Across Sessions?

Write with memory.add as messages happen. Read with memory.search when you want semantic lookup rather than just the last N messages.

hits = client.memory.search(
    user_id="u_8421",
    query="preferred deployment target",
    limit=5)
for h in hits:
    print(h.fact, h.created_at)

Note the parameter: user_id, not session_id. That is what makes search span threads. Searching by session only returns facts from that single thread — a common mistake that makes memory seem broken.

Zep Memory Search vs Plain Vector Search

Both retrieve facts. They differ in what they do with time.

Capability Vector store Zep
Semantic similarity Yes Yes
Invalidates outdated facts No Yes (temporal graph)
Auto summary per session No Yes
Setup effort Low Medium

If your users never change their mind, a vector store is cheaper. Once facts evolve, the temporal layer earns its keep.

How Do You Handle Latency and Token Cost?

Three practical levers worked for us on a real assistant:

  • Request mode="summary" and cap lastn at 4-6 messages.
  • Trigger writes right after the user turn, not after the assistant reply, so ingestion overlaps with generation.
  • Cache the returned context string for the session and reuse it within one turn instead of calling memory.get twice.

Skipping these pushed p95 latency from roughly 400 ms to over 1.5 s in our tests, mostly from unnecessary ingestion calls.

Is the Graph Layer Worth the Complexity?

Only sometimes. If your assistant answers questions about a single person’s history, a user-scoped memory is enough. The graph starts paying off when you need relationships — “which customers reported this bug” — where entities connect to each other, not just to a user.

One honest limitation: you cannot hand-edit the graph. Facts are extracted by a model, so bad input produces bad facts, and the only fix is re-ingesting corrected messages.

Quick Summary

  • Model people as users and threads as sessions; never merge them.
  • Use memory.search with user_id to pull context across sessions.
  • Share facts between sessions through a common graph ID.
  • Keep token cost down with summary mode and a small lastn.
  • Skip the graph until you actually need entity relationships.
Tags: