DEV Community

fiercedash
fiercedash

Posted on

How DeepSeek and ChromaDB Became Our Default RAG Stack

How DeepSeek and ChromaDB Became Our Default RAG Stack

I want to talk about a decision I made six months ago that completely changed how my engineering team thinks about retrieval-augmented generation. We were burning cash. A lot of cash. And the worst part? We weren't even getting great results. So I did what any startup CTO does at 2 AM with a $40K monthly OpenAI bill: I ripped the stack apart and started over.

That decision led us to DeepSeek V4 Pro and V4 Flash running through Global API, paired with ChromaDB as our vector store. Six months in production, the numbers are in. This is what I learned, what broke, and why I'd do it all over again tomorrow.

The Breaking Point

Last year we were running what I'd generously call a "best practice" RAG setup. GPT-4o for embeddings, GPT-4o for generation, Pinecone for vectors, LangChain orchestrating the whole thing. It worked. It also cost us a small fortune. The bill kept climbing in lockstep with usage, and every time I looked at the per-query economics, I felt sick.

Here's the thing nobody tells you about the "default" RAG stack: it's optimized for demos, not for production scale. When you're processing 200,000 queries a day, every millisecond of latency and every tenth of a cent per token matters. We were getting 1.8 second average latency, throughput that bottlenecked around 180 tokens/second, and a bill that grew faster than our user base.

I sat down with my team and said: we're going to rebuild this. Not because GPT-4o is bad. It isn't. But because vendor lock-in to a single provider at our scale is existential risk, and the cost-per-query math just didn't work for a startup trying to hit profitability.

The Vendor Lock-In Question

This is the part most blog posts skip, and frankly, it's the most important part for any CTO. When 90% of your inference cost flows through one vendor, you don't have an architecture. You have a hostage situation. The day that vendor raises prices, has an outage, or deprecates your model, you're done. I've lived through this before at a previous company, and I was determined not to repeat it.

So the first design principle was simple: every component in the RAG pipeline must be swappable in under an hour. The model. The vector store. The orchestration layer. All of it. This is why ChromaDB won the vector store bake-off. It's open source, it runs locally, it scales horizontally, and there's no "enterprise tier" to lock us in. We could move to Qdrant or Milvus tomorrow with minimal pain.

The model layer is where Global API came in. They expose 184 AI models through a single OpenAI-compatible endpoint, and that's the part that sold me. When we want to test DeepSeek V4 Flash against Qwen3-32B against GLM-4 Plus, we change one string in our config. No new SDK. No new auth flow. No new billing relationship. That's the kind of architecture that survives the next 18 months.

The Cost Math That Made the Decision

Let's talk dollars, because at the end of the day, this is a cost story. Here's what I was paying per million tokens before:

  • GPT-4o: $2.50 input, $10.00 output, 128K context
  • Pinecone: roughly $70/month per pod for our scale, plus storage

Here's what I'm paying now:

Model Input Output Context
DeepSeek V4 Flash 0.27 1.10 128K
DeepSeek V4 Pro 0.55 2.20 200K
Qwen3-32B 0.30 1.20 32K
GLM-4 Plus 0.20 0.80 128K
GPT-4o 2.50 10.00 128K

Read that table again. DeepSeek V4 Pro, our primary generation model, costs $0.55 input and $2.20 output. GPT-4o costs $2.50 and $10.00. That's roughly a 4.5x reduction on input and a 4.5x reduction on output. The context window is also larger at 200K, which means we can stuff more retrieved context into each prompt without truncating.

We run a tiered model strategy. DeepSeek V4 Pro for complex multi-step queries that need reasoning. DeepSeek V4 Flash for the 80% of traffic that's straightforward retrieval-and-summarize. GLM-4 Plus as a fallback for edge cases. The economics let us be aggressive about quality because the cost-per-query is so low that we can afford multiple passes when needed.

End result: our monthly inference bill dropped by 62%. Latency dropped to 1.2 seconds average. Throughput climbed to 320 tokens/second. The quality, measured by user satisfaction scores and our internal eval suite, actually went up — we sit at 84.6% on our benchmark suite, which is a 3-point improvement over the GPT-4o baseline. Why? Because we can afford to use a 200K context window and include more retrieved chunks, which means fewer hallucinations.

The Implementation, For Real

Here's the actual code we run. This is production, not a tutorial. Note the base URL — this is the only integration point you need:

import os
from openai import OpenAI
import chromadb
from chromadb.config import Settings

# Single client for all 184 models on Global API
client = OpenAI(
    base_url="https://global-apis.com/v1",
    api_key=os.environ["GLOBAL_API_KEY"],
)

# Local ChromaDB instance, persisted to disk
chroma_client = chromadb.PersistentClient(path="./chroma_store")
collection = chroma_client.get_or_create_collection(
    name="knowledge_base",
    metadata={"hnsw:space": "cosine"}
)

def embed_documents(texts: list[str]) -> list[list[float]]:
    response = client.embeddings.create(
        model="deepseek-ai/DeepSeek-V4-Flash",
        input=texts,
    )
    return [item.embedding for item in response.data]

def query_rag(user_query: str, top_k: int = 8) -> str:
    # Step 1: embed the query
    query_embedding = embed_documents([user_query])[0]

    # Step 2: retrieve from ChromaDB
    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=top_k,
    )

    context_chunks = "\n\n".join(results["documents"][0])

    # Step 3: generate with the Pro model
    response = client.chat.completions.create(
        model="deepseek-ai/DeepSeek-V4-Pro",
        messages=[
            {
                "role": "system",
                "content": "Answer the user's question using only the provided context. "
                           "If the context is insufficient, say so."
            },
            {
                "role": "user",
                "content": f"Context:\n{context_chunks}\n\nQuestion: {user_query}"
            }
        ],
        temperature=0.2,
    )

    return response.choices[0].message.content
Enter fullscreen mode Exit fullscreen mode

That's the whole RAG loop. Embed, retrieve, generate. No LangChain. No LlamaIndex. No orchestration framework. Just OpenAI-compatible calls and a local vector store. The fewer moving parts, the fewer things break at 3 AM.

The Production Pattern That Actually Saves Money

The naive version of the code above works, but it leaks money. Here's the version we actually run, with caching, fallback, and a tiered model strategy:

import hashlib
import json
from functools import lru_cache

CACHE_TTL = 3600  # 1 hour

def cache_key(query: str, context_hash: str) -> str:
    return hashlib.sha256(f"{query}::{context_hash}".encode()).hexdigest()

def tiered_query_rag(user_query: str, complexity_hint: str = "simple") -> str:
    query_embedding = embed_documents([user_query])[0]

    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=10 if complexity_hint == "complex" else 5,
    )

    context_chunks = "\n\n".join(results["documents"][0])
    ctx_hash = hashlib.sha256(context_chunks.encode()).hexdigest()[:16]

    # Check cache first
    key = cache_key(user_query, ctx_hash)
    cached = redis_client.get(key)
    if cached:
        return cached.decode()

    # Pick model based on complexity
    if complexity_hint == "complex":
        model = "deepseek-ai/DeepSeek-V4-Pro"
    else:
        model = "deepseek-ai/DeepSeek-V4-Flash"

    try:
        response = client.chat.completions.create(
            model=model,
            messages=[
                {
                    "role": "system",
                    "content": "Answer using only the provided context. Be concise."
                },
                {
                    "role": "user",
                    "content": f"Context:\n{context_chunks}\n\nQuestion: {user_query}"
                }
            ],
            temperature=0.1,
            stream=True,
        )

        output = ""
        for chunk in response:
            if chunk.choices[0].delta.content:
                output += chunk.choices[0].delta.content

        # Cache the result
        redis_client.setex(key, CACHE_TTL, output)
        return output

    except Exception as e:
        # Fallback to a different model
        fallback = client.chat.completions.create(
            model="THUDM/glm-4-plus",
            messages=[{"role": "user", "content": user_query}],
        )
        return fallback.choices[0].message.content
Enter fullscreen mode Exit fullscreen mode

Three things to notice. First, the cache. We hit 40% on common queries, which directly translates to 40% cost savings on those queries. Second, the tiered model selection. Most queries don't need a 200K context Pro model. Flash handles them fine. Third, the fallback. When DeepSeek rate-limits us (rare, but it happens), we fail over to GLM-4 Plus. Production-ready means graceful degradation, not 500 errors.

What I'd Tell My Past Self

A few things I wish I'd known on day one. ChromaDB's HNSW index is fast, but the default settings aren't tuned for our scale. We ended up with 8 ef_construction and 16 M for our 2M-vector collection, and the recall went from 91% to 96%. Embedding costs are sneaky. They look small until you realise you're embedding 50,000 documents every time you reindex. We batch aggressively and only reindex on a schedule, not on every doc change.

Streaming is not optional. The 1.2s average latency I quoted includes time to first token with streaming. Without streaming, perceived latency is closer to 2.5s, and users notice. With streaming, the first token arrives in 180ms, and the user sees the system working. UX matters even at the API level.

Quality monitoring is the part nobody wants to build. We track user satisfaction via thumbs up/down, and we sample 5% of responses for manual review. The 84.6% benchmark score I mentioned is from our internal eval suite, which runs weekly. Without that loop, you're flying blind.

The Vendor Lock-In Insurance Policy

I want to come back to this because it's the reason I sleep at night. Our entire inference layer is a config string. If Global API disappears tomorrow, I change the base URL to OpenAI, Together, or Groq, and I update the model names. Total migration time: maybe 90 minutes. If I want to A/B test Qwen3-32B against DeepSeek V4 Pro next quarter, it's a 10-line config change and a 24-hour shadow traffic run.

That's what production-ready actually means. Not "it works on day one." It means "it works on day one and you can swap any component without rewriting the system." ChromaDB gives us that on the vector side. Global API's unified SDK gives us that on the model side. That's the architecture I want, and that's the architecture that scales.

The Bottom Line

We're paying 40-65% less than we were on the GPT-4o stack. Quality is up. Latency is down. Throughput is up. And we have zero vendor lock-in. The 84.6% benchmark score and 320 tokens/second throughput are nice, but the real win is the optionality. We can move to a better model the day one drops, and we can do it without a six-week migration project.

If you're a CTO running RAG in production and your OpenAI bill makes you wince, I'd seriously consider the DeepSeek + ChromaDB + Global API stack. The setup took my team under 10 minutes for the initial integration, and we've been iterating on the prompt engineering and retrieval strategy ever since. The cost savings funded two additional engineering hires in Q1. That's ROI you can take to the board.

Global API is worth checking out if you want to test all 184 models without signing up for 184 different vendor accounts. The unified endpoint is the unlock — one SDK, one auth, one bill. It's how RAG infrastructure should have worked from the start.

Top comments (0)