DEV Community

BAOFUFAN
BAOFUFAN

Posted on

Moving Agent Memory to Redis: From 30% to 95% Integration Test Coverage

At 2 a.m., I found myself staring at the CI pipeline’s fourth consecutive integration test failure—this time it wasn’t a timeout or a data mismatch; the AI agent had simply “forgotten” which tool it had called halfway through the test, causing the remaining steps to go completely off the rails. That was my wake-up call: I had been running all my tests using LangChain’s default in‑memory memory, the equivalent of asking someone who wipes their memory every five minutes to execute a complex business flow. No wonder things went wrong.

Problem Breakdown: Why Agent Amnesia Is the Silent Killer of Testing

Here’s the setup: we’d added an AI agent to our customer‑service ticketing system. Its job was to look up a customer’s order history, decide whether to issue a refund or ship a replacement based on the order status, and finally record the outcome by calling the ticketing API. For integration testing, I spun up a realistic environment with pytest (including Docker containers for internal APIs and databases) and wanted the agent to complete the entire workflow from end to end.

Day one: the test barely passed. Day two: after adding a “check shipment” tool, tests started failing randomly. The logs showed the agent sometimes repeatedly called “lookup order” and at other times skipped “create ticket” entirely. I inspected the prompt and tool definitions—everything looked fine. The real culprit was hidden: LangChain’s default ConversationBufferMemory stores conversation history in the Python process heap, and my test framework re‑initialized the agent for every test method to enforce isolation—wiping the memory clean each time. The agent had no clue what it had done in previous steps, so it could only guess from the immediate prompt context, making its behavior inherently unstable.

Worse, even inside a single test case, when I tried to simulate multi‑turn conversations (say, a user asking about logistics first and then requesting a refund), the agent occasionally lost its chain of thought. Some tool calls involved async operations, and without concurrency protection, shared in‑memory history got accidentally overwritten across threads.

This is the classic testing dilemma for stateful agents: tests demand repeatability and isolation, but the very value of a stateful agent lies in its cross‑turn context. Using in‑process memory gave me at best single‑turn interaction coverage (roughly 30%). Multi‑turn and cross‑session scenarios were practically impossible to test stably. Storing conversations in files or databases? Too heavy and they required extra cleanup. The usual approaches just didn’t cut it.

Solution Design: Turning Redis into the Agent’s “External Brain”

The requirement was clear: I needed a low‑latency, persistent conversation memory backend with built‑in support for time‑to‑live and session isolation. That way, I could inject the same conversation history into an agent at the start of each test and wipe it clean with a single command when the test finished.

Why I didn’t go with the alternatives:

  • File storage: concurrent writes lead to locking headaches and cleanup means manual rm -rf—not exactly elegant; don’t even get me started on path mapping pitfalls inside Docker.
  • Relational database: creating tables, writing SQL, and managing connection pools for a handful of chat messages felt like using a sledgehammer to crack a nut, plus it introduced schema migration overhead.
  • In‑memory + mocks: they can’t simulate real multi‑session scenarios, gutting the test’s value.

Redis fits this scenario almost perfectly. The LangChain community already provides a ready‑made integration: RedisChatMessageHistory. Under the hood, it uses a Redis List keyed by session ID to store conversation messages, with support for TTL and persistence, naturally isolating different sessions. More importantly, during testing I could:

  • Pre‑seed a few historical messages for a given session ID during setup (simulating an existing conversation).
  • Delete the corresponding key in teardown via DELETE, ensuring complete isolation between test cases.
  • Rely on Redis’s expiration mechanism to prevent leftover test data from blowing up memory in CI.

The architecture is straightforward: when creating the agent, I specify a memory whose chat_history is backed by RedisChatMessageHistory. Different test cases just use different session_id values and automatically get isolated memory spaces. Together with stubbed external dependencies (like the ticketing API), this forms a complete, reproducible test sandbox.

Core Implementation: Giving the Agent a Persistent Memory, Step by Step

1. Infrastructure: Spin Up Redis and Configure the Connection

I used docker-compose to run a Redis instance in the test environment—though you could also use fakeredis for unit tests. The key is to supply a redis_url; LangChain handles the connection management for you.

Here’s how to create a reusable memory factory function that solves the “every test case needs independent memory” problem:

# memory_factory.py
import redis
from langchain.memory import ConversationBufferMemory
from langchain_community.chat_message_histories import RedisChatMessageHistory

def create_redis_memory(session_id: str, redis_url: str = "redis://localhost:6379/0") -> ConversationBufferMemory:
    """
    Create Redis‑backed memory for a given session. Different session_ids are fully isolated.
    """
    # 1. Initialize Redis client (LangChain uses this connection internally)
    # In a real project you'd pull from a connection pool; here’s the minimal example
    redis_client = redis.from_url(redis_url, decode_responses=True)

    # 2. Create Redis message history. key_prefix controls the Redis key pattern.
    # For example, session_id="test_001" yields key "message_store:test_001"
    chat_history = RedisChatMessageHistory(
        session_id=session_id,
        url=redis_url,          # can also pass redis_client directly
        key_prefix="message_store:"
    )

    # 3. Build memory with default human/AI message keys
    memory = ConversationBufferMemory(
        memory_key="chat_history",
        return_messages=True,   # return Message objects for easier downstream use
        chat_memory=chat_history
    )
    return memory
Enter fullscreen mode Exit fullscreen mode

The key_prefix is important—when cleaning up test data, I can simply match message_store:* and delete everything in one shot without keeping track of individual session IDs.

2. Building an Agent with Memory and Exposing It to Tests

The next step was to create...

Top comments (0)