Skip to content

Giving Agents Memory: Storage & Retrieval

An LLM has no memory of its own, every call starts from zero except for whatever text you put in the prompt. "Agent memory" means building a system that finds the right facts and hands them to the model at the right moment. This module covers both halves: where you store that information (a vector database), and how you make sure you retrieve the right pieces of it (retrieval quality).

Owl mascot

What you'll learn

  • Picking a vector database without overthinking the choice
  • Why bigger chunks and smaller chunks each fail in different ways
  • The one upgrade (reranking) that fixes most "wrong answer" retrieval bugs

1. The problem

Two different things go wrong with agent memory, and they need different fixes.

Storage: if you don't index your data for similarity search, "does this document relate to the user's question" means scanning everything, every time, slow, and it doesn't scale past a handful of documents.

Retrieval quality: even with a database in place, similarity search returns the closest matches, not necessarily the right ones. An agent that retrieves the wrong three paragraphs will confidently answer from the wrong context, and it won't tell you it did that. This is the failure mode that actually costs you: not "no results," but "confident, wrong results."

2. Options & when to use each

Where to store it

OptionGood forCosts youWhen to pick it
pgvector (Postgres)Most projects, especially if you already use PostgresNothing extra to run, it's an extensionDefault choice. You get SQL filtering (WHERE customer_id = ...) alongside similarity search for free.
A managed vector DB (Pinecone, etc.)Teams that don't want to run their own databaseMonthly cost, network round-trip per queryYou're not already running Postgres and don't want to start
An embedded/local store (Chroma)Prototypes, single-user tools, local-first appsDoesn't scale well past one processFast to try something out; not for production with concurrent users

If you're already running Postgres for anything else in your stack, pgvector is almost always the right call, one less system to operate, and you can combine a normal WHERE clause with the similarity search (e.g., "similar documents, but only from this customer's account").

How to chunk and retrieve

ApproachGood forCosts youWhen to pick it
Straightforward chunking + top-k searchGetting started, most FAQs/docs use casesOccasionally retrieves a chunk that's close but not quite rightDefault starting point, build this first
Add a reranking stepAnywhere wrong answers are costly (support, legal, financial)One extra model call per query (adds latency)Your top-k search is finding relevant material but not always surfacing the best piece of it first

3. Build it

Store and search with pgvector

To initialize a vector search store in a PostgreSQL database, enable the vector extension and set up a document table with a vector column and HNSW index:

sql
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    content TEXT NOT NULL,
    customer_id INT,
    embedding VECTOR(768)
);

CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);

Next, write the Python integration code to generate embeddings using the Google GenAI SDK and run a cosine similarity query against the database table:

python
from google import genai

client = genai.Client()

def embed(text: str) -> list[float]:
    result = client.models.embed_content(
        model="gemini-embedding-2",
        contents=text,
        config={"output_dimensionality": 768},
    )
    return result.embeddings[0].values

def search(query: str, customer_id: int, top_k: int = 5):
    query_vec = embed(query)
    return db.execute(
        """
        SELECT content, 1 - (embedding <=> %s) AS similarity
        FROM documents
        WHERE customer_id = %s
        ORDER BY embedding <=> %s
        LIMIT %s
        """,
        [query_vec, customer_id, query_vec, top_k],
    )

The WHERE customer_id = %s alongside the similarity ordering is the practical reason to use Postgres over a pure vector store: you get relational filtering and semantic search in one query, so an agent can never accidentally retrieve another customer's data just because it's semantically similar.

Chunk documents sensibly

Don't index whole documents as single chunks (too big, you'll retrieve a 10-page PDF when the user asked about one paragraph) and don't index single sentences (too small, you lose surrounding context). A reasonable default:

python
def chunk_document(text: str, chunk_size: int = 800, overlap: int = 100) -> list[str]:
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start = end - overlap  # overlap keeps context from breaking mid-idea
    return chunks

This is a good starting point for most text. If you outgrow it, chunks routinely cut off mid-thought, or retrieval quality plateaus, the next step up is splitting on paragraph/section boundaries instead of a fixed character count, but don't add that complexity before you've confirmed you need it.

Add reranking when retrieval quality isn't good enough

Vector search alone finds the k closest matches fast, but "closest by embedding distance" and "actually most relevant to the question" aren't always the same thing. A reranker re-scores your top candidates using a model that looks at the query and each document together, not independently:

python
import cohere

co = cohere.Client()

def search_and_rerank(query: str, customer_id: int, top_k: int = 3):
    # Cast a slightly wider net with vector search first
    candidates = search(query, customer_id, top_k=15)

    reranked = co.rerank(
        query=query,
        documents=[c["content"] for c in candidates],
        top_n=top_k,
        model="rerank-english-v3.0",
    )
    return [candidates[r.index] for r in reranked.results]

Retrieve more candidates than you need (15, say), then let the reranker narrow it down to the best 3. This costs one extra API call per query, worth it anywhere a wrong answer is expensive, skippable for low-stakes internal tools.

4. What goes wrong

MistakeHow you notice itThe fix
No index on the embedding columnQueries get slower as the table growsAdd the hnsw index shown above, Postgres won't use it automatically until it exists
Chunks too largeAgent answers seem to pull in irrelevant tangents from the same documentReduce chunk size; make sure overlap isn't zero
Chunks too smallAgent's answers feel fragmented, missing context that was right next to the retrieved pieceIncrease chunk size or overlap
Filtering forgottenOne customer's agent can retrieve another customer's dataAlways scope retrieval with a WHERE clause on tenant/customer ID, never rely on the embedding alone
Retrieval looks fine but answers are still wrongTop result by similarity isn't actually the best answer to the questionAdd a reranking step (above) , this is the single highest-leverage fix for "technically found something, but not the right thing"
Embedding model mismatchvector dimensions do not match error, or silently bad results after switching modelsStore which embedding model/version was used per row; re-embed everything if you switch models, old and new embeddings aren't comparable

5. Confirm it worked

To verify that your query execution plans are performing fast vector index scans rather than expensive full table scans, analyze your queries using EXPLAIN ANALYZE:

sql
-- Confirm the index is actually being used, not a full table scan
EXPLAIN ANALYZE
SELECT content FROM documents
WHERE customer_id = 1
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 5;
-- Look for "Index Scan using documents_embedding_idx" in the output.
-- If you see "Seq Scan" instead, the index isn't being used.

Additionally, run a quick integration test in Python to ensure that queries return expected matches on basic semantic lookups:

python
# Confirm retrieval actually finds relevant content for a known question
results = search("What's your refund policy?", customer_id=1)
assert any("refund" in r["content"].lower() for r in results[:3])

If reranking is in place, spot-check a handful of real queries and confirm the top result after reranking is a better answer than the top result from vector search alone, that comparison is the actual test of whether reranking was worth adding.

Next Step: proceed to Long Context & Token Economics to manage context and cost as this agent's tasks grow.