DEV Community

BAOFUFAN
BAOFUFAN

Posted on

AI Agent Memory Storage Testing Pitfalls: A 4-Hour Debugging Nightmare Caused by Redis Serialization

Friday, 5:00 PM. I was ready to deploy the agent’s memory feature and call it a day. Right after flipping the traffic, I stood up — and Slack started blowing up: “The agent’s responses are completely off,” “Conversation history is missing.” I rolled back immediately and spent the next hour tracing the root cause. The new memory module was storing user contexts in Redis using pickle, and the serialized object version didn’t match what was running in production. Deserialization threw an exception — but all our integration tests were green, because we were using fake-redis, which never even raised a peep. That afternoon I crouched in the server room cursing myself for two solid hours: “I should have added automated tests against real Redis ages ago.”

If you’re using Redis to store AI agent conversation memory, tool-call chains, or vector indexes, this article will walk you through my experience of building a pytest + real Redis test framework from scratch, together with the traps that will make you question your life choices.

Why Redis-Based Memory for Agents Breaks So Easily

In a typical agent loop, every step the LLM takes requires reading “what happened before” from storage. We use Redis as the memory layer, storing three main types of data:

  1. Conversation history – a session’s message list, stored as a list or sorted set with a TTL.
  2. Context summaries – compressed long-conversation summaries, often stored as strings using JSON or pickle.
  3. Tool states – whether a function call has been executed and what its result was, stored as a hash.

Manual testing involves opening a web chat page, exchanging a few messages, and checking if the answers make sense. But this approach completely misses edge cases like:

  • Does concurrent writes to the same session lose messages?
  • After a TTL expires, is the summary rebuilt correctly?
  • Is serialization/deserialization consistent across Python versions and dependency versions?
  • When Redis disconnects and reconnects, will data be incorrectly overwritten?

The common approach of using fakeredis for unit tests is undeniably fast, but it doesn’t support Lua scripts, pub/sub, and its parameter validation for some commands differs from real Redis. It also can’t simulate cluster or sentinel modes used in production. The illusion that all tests pass is more dangerous than no tests at all.

Design Decisions: Real Redis + pytest fixture isolation, and why I rejected the alternatives

The goal was clear: for every test run, spin up an independent Redis instance completely isolated from production, tear it down afterwards, and ensure no interference between tests. Here are the typical approaches I considered:

  • Connect directly to a development Redis: Several colleagues running tests simultaneously would cause key collisions. You’re booking yourself another evening of detective work.
  • Mock the Redis client: Replace redis.Redis methods with unittest.mock. It’s exhausting to maintain and can’t expose real-world behavior of serialization, pipelines, or transactions.
  • fakeredis: As mentioned, it covers a subset of functionality. Good for pure logic checks, not for integration tests.
  • testcontainers: Pulls a Docker Redis image, auto-maps ports, and destroys the container after use. Paired with pytest scope control, it doesn’t pollute environments and catches real issues.

So here’s my final stack:

  • Unit tests: keep using fakeredis, run in the fastest CI stage.
  • Integration tests: pytest + testcontainers-python + real Redis, tagged with integration, executed on demand.
  • Concurrency tests: pytest-asyncio to simulate multiple coroutines writing to memory simultaneously.

Core Implementation: Building a working test framework step by step

Below is a ready-to-run recipe. First, solve the “start a Redis container and get a connection” problem.

conftest.py — provides a reusable Redis connection pool for the entire test session

import pytest
from testcontainers.redis import RedisContainer

@pytest.fixture(scope="session")
def redis_container():
    """启动一个 Redis 容器,整个测试会话只启动一次。"""
    # 可以指定版本,避免线上版本不一致
    container = RedisContainer("redis:7.2-alpine")
    container.start()
    yield container
    container.stop()

@pytest.fixture(scope="session")
def redis_client(redis_container):
    """返回真实 Redis 客户端,session 级别复用。"""
    import redis
    client = redis.Redis(
        host=redis_container.get_container_host_ip(),
        port=redis_container.get_exposed_port(6379),
        decode_responses=False,  # 这里先不自动解码,后面具体用例自己决定
        max_connections=20,
    )
    yield client
    # 连接池会自动关闭,但显式 flush 一下方便下次测试
    client.flushdb()
Enter fullscreen mode Exit fullscreen mode

Test memory write and read — verify basic Set/Get and TTL expiration

import pytest
import json
from datetime import timedelta

pytestmark = pytest.mark.integration  # 标记为集成测试

def test_save_and_retrieve_memory(redis_client):
    """
    验证 Agent 将一个 session 的上下文摘要存入 Redis,
    并在 TTL 内正确读取出来。
    """
    session_id = "session:abc123"
    memory_payload = {
        "summary": "用户在问关于机票改签的问题",
        "entities": ["order_12345", "flight_CA1234"],
        "version": "1.0.0"
    }

    # 存入序列化后的数据,实际可能用 pickle 或 msgpack,这里用 json 示范
    redis_client.setex(
        session_id,
        timedelta(minutes=30),
        json.dumps(memory_payload)
    )

    # 模拟 Agent 读取记忆
    raw = redis_client.get(session_id)
    assert raw is not None, "TTL 内应该能读到记忆"
Enter fullscreen mode Exit fullscreen mode

Top comments (0)