DEV Community

BAOFUFAN
BAOFUFAN

Posted on

API Regression Testing Nightmare: A 48-Hour Debugging Journey with LangChain + Chroma Memory

At 3 a.m., CI alerts started screaming that the score field was missing from our user API responses. I scrolled through the automated test results — all green, status 200, user_id present, name present, but the score field we had just shipped yesterday was gone. The whole release got rolled back, and my manager @mentioned me in the group chat three times. Staring at the screen, I wondered: Is there a test that could catch an API silently “losing weight” without relying on human eyes?

That’s exactly how this “automated testing agent with persistent memory” came to life. The journey was far from smooth — I hit enough pitfalls to fill a war story — but once it was working, the granularity of our API regression tests levelled up dramatically. Here’s the full breakdown, including runnable code and all the things the official documentation won’t tell you.


Problem Breakdown: Why Traditional API Tests Are Practically Useless

In our day-to-day test writing, we essentially do “given an input, assert the output”: assert resp.status_code == 200, then check a few key field values. This works when the API is stable. But when you refactor, when a downstream microservice releases a new version, or when a product manager quietly changes a field definition, traditional exact-match assertions expose three fatal flaws:

  1. Missing fields go unnoticed: If a field isn’t part of the assertions, it can vanish and the tests stay green.
  2. Type / precision changes are ignored: For instance, price changes from int to float, or the number of decimal places shifts — business‑critical, yet the tests don’t complain.
  3. No historical reference point: You compare against a fixed expected value, and you can’t automatically answer “What’s different between this response and a normal historical one?”

The most obvious fix is to store every response and diff them next time, right? But JSON diff is sensitive to field order, null values, and whitespace — the false‑positive rate is enough to drive anyone insane. Writing custom comparison scripts solves part of the problem, but the maintenance cost grows linearly with the number of endpoints, and nobody on the team wants that dirty work.

We need something with persistent memory + semantic understanding — store historical responses in a structured way, and every time the tests run, use AI to compare “this response” with “the most similar historical responses for the same endpoint” and surface real, substantive differences.


Solution Design: LangChain Orchestration + Chroma Persistent Memory

The choices matter. I ended up with LangChain as the orchestration layer and Chroma as the vector memory — here’s why, and why I rejected the alternatives:

Component Choice Why not the others
Vector store Chroma FAISS is more powerful but requires extra work for persistence; Qdrant is too heavy. Chroma has a built‑in persist_directory, disk persistence with one parameter, and natively supports metadata filtering — perfect for storing HTTP method and path so we can retrieve similar requests from the same endpoint.
Orchestration framework LangChain You can do this directly with the OpenAI SDK, but LangChain’s Agent system and Tool abstraction let me quickly wire “call API,” “store response,” and “compare differences” into a conversational test assistant. Later you can even integrate it into Slack for QA.
Comparison engine GPT‑3.5/4 Not for generating tests — here the model performs a structured semantic comparison, which is far more flexible than writing rules. But you have to constrain the prompt to prevent hallucinations.

The whole architecture in three sentences:

The test agent receives a request definition → calls the API and gets the latest response → uses Chroma to retrieve similar historical responses from the same endpoint (by vector similarity) → feeds both responses to the LLM, asking it to report only field additions/removals, value changes, and type changes, ignoring order and whitespace.


Core Implementation (Complete Runnable Code)

1. Build the persistent memory first — this code shows how to store and retrieve historical responses

import os
import json
import time
from chromadb import PersistentClient
from chromadb.utils import embedding_functions
import openai
from langchain.embeddings.openai import OpenAIEmbeddings

# 用 PersistentClient 自动落盘,指向本地目录
client = PersistentClient(path="./chroma_db")
embed_fn = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.environ["OPENAI_API_KEY"],
    model_name="text-embedding-ada-002"
)

# 获取或创建集合,metadata 用来存请求标识
collection = client.get_or_create_collection(
    name="api_responses",
    embedding_function=embed_fn,
    metadata={"hnsw:space": "cosine"}
)

def store_response(method: str, url: str, request_body: dict, response_body: dict):
    """存储一次 API 响应,并立即持久化,避免进程异常丢失数据"""
    doc_text = json.dumps(response_body, ensure_ascii=False)
    meta = {
        "method": method,
        "url": url,
        "request_body": json.dumps(request_body),
        "timestamp": time.time()
    }
    collection.add(
        documents=[doc_text],
        metadatas=[meta],
        ids=[f"{method}_{url}_{time.time()}"]  # 简易唯一ID
    )
    # ⚠️ Chroma 持久化关键:每次 add 后必须手动 persist,不然重启就“失忆”
    collection.persist()

def query_history(method: str, url: str, current_resp: dict, top_k=3):
    """检索同接口的历史响应,按语义相似度排序"""
    query_text = json.dumps(current_resp, ensure_ascii=False)
    results = collection.query(
        query_texts=[query_text],
        n_results=top_k,
        where={"method": method, "url": url}  # metadata 过滤,只取同一接口的历史
    )
    return resul
Enter fullscreen mode Exit fullscreen mode

Top comments (0)