DEV Community

BAOFUFAN
BAOFUFAN

Posted on

LLM Memory Storage: 3 Days of Manual Testing Missed 8 Bugs – Automation Caught Them in 1 Hour

It was 2 a.m. when a customer call jolted me awake: "Your customer service AI has assigned my order to someone else again – the third time this week!" As I apologized and dug into the logs, I found two conversation summaries in the memory store with a vector similarity of 0.98 but completely mismatched user IDs. The root cause was consistency issues in the LLM memory storage. Our team had spent three days on manual regression testing and didn’t catch a single one of these problems.


Why manual testing can’t handle memory storage

The setup looks straightforward: use an LLM for customer service, store a summary of each conversation into a vector database (we used ChromaDB), and on the next visit retrieve the user’s historical memories to prepend into the prompt – a simple “write once, query once” pattern. In practice, five hidden traps break that illusion:

  1. Write visibility delay – Vector databases often buffer index writes; add() returns immediately, but a following query() might not see the just-written data.
  2. Multi-turn append race – When a user sends messages in quick succession, two goroutines can read the old memory simultaneously, each append new summaries, and write back, with the later write silently overwriting the earlier one.
  3. Similarity mismatch – If the top-ranked vector from a search belongs to another user, that stranger’s memory gets injected into the prompt.
  4. Unstable summaries – Running the same conversation through the LLM twice can produce slightly different summaries, causing vector drift on subsequent retrievals.
  5. Metadata omission – Fields like timestamps or session IDs get lost during writes, so the retrieved memory is incomplete.

Manual testing only covers the happy path: single user, single conversation turn, wait a bit, then query. The traps above are all timing, concurrency, and edge-case scenarios – in other words, manual testing is self-deception.


Designing an automated test that forces consistency to surface

Stack: pytest + chromadb (in‑memory mode) + asyncio for concurrency simulation.

We don’t hit the production database because the tests need to finish within minutes in CI; an in‑memory instance spins up and tears down instantly. To rule out model instability, we replace ChromaDB’s embedding function with a mock that returns fixed vectors, so the target is the storage layer itself.

The tests cover three core contracts:

  • Read‑after‑write consistency: immediately after add(memory), a query must return that record with exactly matching content and metadata.
  • Multi‑session isolation: a retrieval for user A must never contain any memory belonging to user B.
  • Concurrent append integrity: simulate 5 concurrent requests from the same user; the final state must contain exactly 5 appended memories, no drops and no overwrites.

These three form the consistency iron triangle for LLM memory. We baked them into a fixture and run each test case 100 times with random data to smoke out intermittent bugs.


Core implementation: test fixture + cases you can run

The snippet below builds a pluggable memory‑storage abstraction and a test fixture – the goal is to isolate ChromaDB’s pitfalls and expose them directly to the tests.

# test_memory_consistency.py
import uuid, asyncio, chromadb
from chromadb.config import Settings
import pytest

class MemoryStore:
    """封装 ChromaDB,提供 add / query 接口"""
    def __init__(self, collection_name: str = "test_mem"):
        self.client = chromadb.Client(Settings(anonymized_telemetry=False))
        self.collection = self.client.create_collection(
            name=collection_name,
            embedding_function=lambda texts: [[0.1, 0.2, 0.3] for _ in texts]  # 固定向量,排除模型影响
        )

    def add(self, user_id: str, content: str, metadata: dict = None):
        doc_id = str(uuid.uuid4())
        meta = {"user_id": user_id, **(metadata or {})}
        self.collection.add(ids=[doc_id], documents=[content], metadatas=[meta])
        return doc_id

    def query(self, user_id: str, query_text: str, top_k: int = 5) -> list:
        results = self.collection.query(
            query_texts=[query_text],
            n_results=top_k,
            where={"user_id": user_id}   # 关键:按用户隔离,防止串号
        )
        # 返回 (id, content, meta) 三元组列表
        return [
            (doc_id, doc, meta)
            for doc_id, doc, meta in zip(
                results["ids"][0], results["documents"][0], results["metadatas"][0]
            )
        ]

@pytest.fixture
def store():
    s = MemoryStore()
    yield s
    s.client.delete_collection("test_mem")  # 每个用例结束后清理
Enter fullscreen mode Exit fullscreen mode

Then come the three core test cases, each cycling through 100 random payloads in the fixture to specifically attack timing issues.

class TestMemoryConsistency:

    def test_write_then_read(self, store):
        """写后立刻读,必须一致"""
        for _ in range(100):
            uid = str(uuid.uuid4())
            content = f"memory_{uuid.uuid4().hex[:8]}"
            meta = {"timestamp": 1716000000}
            store.add(uid, content, meta)
            # 关键:直接 query,不等索引刷新
            results = store.query(uid, content, top_k=1)
            assert
Enter fullscreen mode Exit fullscreen mode

Top comments (0)