Monday, July 27, 2026

Chunking Strategies for RAG: What Actually Changes Retrieval Quality

Hero image showing document chunks being split and embedded

I ran the same eval harness I built in post 288 against a set of chunking configurations and found that chunk size and split strategy had a larger impact on retrieval quality than I expected. Changing from 512-token fixed chunks to 256-token chunks with overlap improved Recall@1 on lookup queries by around 14 points on this corpus. Changing from character-split to sentence-split chunks improved Recall@10 by around 8 points.

These numbers are corpus-specific. Your documents may respond differently. But the methodology is the same: measure before you tune, and measure after.

This post covers the chunking decisions that actually matter, what to expect from each, and how to test them against your own data.

Why Chunking Matters

Your embedding model encodes a chunk as a single vector. Everything in that chunk competes for representation in the vector. If a chunk is too large, the embedding averages over too much content and loses specificity. A query about one detail in the chunk may not retrieve it. If a chunk is too small, each chunk lacks enough context for the embedding to place it meaningfully in vector space.

The right chunk size depends on what your queries look like. Lookup queries (for example, "what is the return policy?") need a short, precise answer. They benefit from smaller chunks that isolate specific facts. Synthesis queries (for example, "summarize the key risks in section 3") need more context. They benefit from larger chunks.

Most RAG pipelines use one chunk size for all queries. This is a practical starting point, but it means you are optimizing for one query type at the expense of the other.

The Main Chunking Decisions

Chunk Size

Chunk size is measured in tokens (the unit your embedding model sees) or characters. Smaller sizes suit lookup-heavy workloads and larger sizes suit synthesis-heavy ones. The LangChain documentation uses 512 as a default starting point, per its text splitter documentation.

Smaller chunks improve Recall@1 for lookup queries because the answer takes up more of the embedding space. Larger chunks improve recall for synthesis queries because they carry more context.

We measured the transition point on this corpus: below our best-performing chunk size, synthesis query recall started to degrade; above it, lookup query recall started to degrade.

Chunk Overlap

Overlap preserves context at chunk boundaries. With zero overlap, a sentence split across two chunks may be retrieved in a form that loses the beginning or end of the key fact. With overlap, the sentence appears fully in both chunks.

Common overlap settings are 10–20% of the chunk size. On this corpus, 50-token overlap on 512-token chunks improved Recall@1 by around 6 points compared to zero overlap, at essentially no additional indexing cost.

Overlap does increase index size proportionally. A 20% overlap increases the number of chunks by roughly 25%.

Split Strategy

Fixed-size splitting cuts the document at a character or token count, regardless of sentence or paragraph boundaries. It is fast and simple but can split sentences mid-phrase, creating chunks where the key fact spans two chunks.

Sentence splitting cuts at sentence boundaries. The chunks are variable in size, but each chunk contains complete sentences. Retrieval quality is generally better because embeddings of complete sentences are more stable than embeddings of sentence fragments.

Recursive splitting tries large separators first (paragraphs, then sentences, then words) and falls back to smaller ones when a chunk exceeds the target size. It is the default strategy in LangChain's RecursiveCharacterTextSplitter and produces the most semantically coherent chunks in practice.

Results on the Same Corpus

Using the same 500,000-document corpus, nomic-embed-text, and the same 1,000-query eval set from the previous posts (500 lookup queries and 500 synthesis queries):

Comparison chart showing retrieval metrics across chunking configurations
Configuration Recall@10 Recall@1 (lookup) Recall@1 (synthesis) Index size
512-token fixed, no overlap 84% 71% 64% baseline
256-token fixed, no overlap 83% 79% 57% 1.9×
512-token fixed, 50-token overlap 87% 77% 69% 1.1×
512-token recursive, 50-token overlap 89% 81% 73% 1.1×
1024-token recursive, 50-token overlap 88% 74% 78% 0.6×

The recursive 512-token configuration with 50-token overlap performed best across both query types on this corpus. Smaller chunks helped lookup queries but hurt synthesis queries. Larger chunks did the reverse.

Implementation

Fixed-Size Splitting

def chunk_fixed(text: str, chunk_size: int = 512, overlap: int = 50) -> list[str]:
    words = text.split()
    chunks = []
    start = 0
    while start < len(words):
        end = start + chunk_size
        chunks.append(" ".join(words[start:end]))
        start += chunk_size - overlap
    return chunks

Sentence Splitting

import re

def chunk_by_sentence(text: str, max_tokens: int = 512) -> list[str]:
    sentences = re.split(r'(?<=[.!?])\s+', text)
    chunks = []
    current = []
    current_len = 0

    for sentence in sentences:
        token_estimate = len(sentence.split())
        if current_len + token_estimate > max_tokens and current:
            chunks.append(" ".join(current))
            current = []
            current_len = 0
        current.append(sentence)
        current_len += token_estimate

    if current:
        chunks.append(" ".join(current))
    return chunks

Recursive Splitting (LangChain)

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=2048,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " ", ""]
)

chunks = splitter.split_text(document_text)

The chunk_size here is in characters, not tokens. A rough conversion for English text is approximately 4 characters per token, so 2,048 characters maps to a few hundred tokens in practice.

How to Test Your Configuration

Run this before and after any chunking change using the eval harness from post 288:

def compare_chunking_configs(configs: list[dict], query_pairs: list[dict]) -> list[dict]:
    results = []
    for config in configs:
        # Re-index with this config
        chunks = apply_chunking_config(config)
        index = build_index(chunks)
        metrics = run_eval(query_pairs, index)
        results.append({"config": config, "metrics": metrics})
    return results

Split your query set by query type if possible. A configuration that improves lookup recall while hurting synthesis recall may still be the right choice depending on your workload distribution.

What to Try First

If you are starting from a fixed-size split with no overlap, the highest-value change is adding overlap. It costs roughly 10% more index size and typically improves Recall@1 by several points.

If you are already using overlap, switching to recursive splitting is the next experiment. It handles mixed document types (code, prose, tables) better than fixed splitting and tends to produce more consistent Recall@10.

Changing chunk size is the highest-risk experiment because it changes the character of every chunk in your index. Run it on a test index first, not your production one.


Get the next one

I send one short email a week: one production bug, debugged, plus the
companion code for each deep-dive. No spam, unsubscribe anytime.

👉 Subscribe (free)

Reader challenge: what chunk size and strategy are you using in your RAG pipeline? Has changing it ever surprised you? Reply with what you found.

Sources

  1. LangChain RecursiveCharacterTextSplitter documentation: https://python.langchain.com/docs/modules/data_connection/document_transformers/recursive_text_splitter
  2. Sentence Transformers chunking guidance: https://www.sbert.net/examples/applications/semantic-search/README.html
  3. Qdrant best practices for indexing: https://qdrant.tech/documentation/tutorials/bulk-upload/

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-07-27 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

☕ Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

Adding a Reranker to Your Self-Hosted RAG Pipeline: When It Helps and When It Doesn't

Hero image showing a two-stage retrieval pipeline with reranker

I ran the eval harness from the last post against my own RAG pipeline and found the same pattern every time: Recall@10 was fine, but Recall@1 was weaker than it should have been. The right document was in the top 10 results. It just was not reliably in position 1.

This is the problem reranking solves. A reranker takes the top-K results from your vector search and reorders them using a more expensive model that considers the query and document together. The result is a better-ordered list at the cost of latency.

This post covers when to add a reranker, what to expect from it on self-hosted hardware, and where it does not help.

The Two-Stage Retrieval Pattern

Standard RAG retrieval is a single stage: embed the query, search the index, return the top results. The embedding model encodes the query and documents into vectors, and relevance is approximated by vector distance.

The limitation is that vector distance is a coarse signal. Two vectors being close in embedding space means they share semantic structure. It does not mean the document precisely answers the query. For broad queries or queries where subtle distinctions matter, the top-1 result is often not the best result even when it is close in vector space.

A reranker is a second stage. After vector search returns top-K candidates, the reranker takes each (query, candidate) pair and scores them jointly using a cross-encoder model. Cross-encoders see both the query and document at the same time, which gives them much more signal than the separate embeddings used in vector search. The top-K candidates are then reordered by the reranker's scores.

The trade-off is inference cost. Cross-encoders are slower than embedding lookups because they run a full forward pass for each (query, candidate) pair. With K=10 and a moderate reranker, you are doing 10 cross-encoder inference calls per query instead of one embedding lookup.

Self-Hosted Reranker Options

Three models run well via Ollama or directly in Python without an API dependency:

ms-marco-MiniLM-L-6-v2: a 22M-parameter cross-encoder fine-tuned on the MS MARCO passage retrieval dataset. Fast, low memory, and the default starting point for most reranking setups. Available via sentence-transformers.

ms-marco-MiniLM-L-12-v2: the 12-layer variant of the same model. Slightly better quality at the cost of roughly double the inference time.

bge-reranker-v2-m3: from BAAI, the same research group behind bge-m3 embeddings. A 568M-parameter model with significantly better reranking quality, particularly on multilingual and technical content. Considerably slower on CPU but strong on GPU.

Results on the Same Corpus

Using the same 500,000-document corpus from the vector database and embedding model comparisons, nomic-embed-text as the base retrieval model, and the same 1,000-query eval set:

Comparison chart showing retrieval metrics with and without reranker
Configuration Recall@10 Recall@1 p50 latency p99 latency
nomic-embed-text only 87% 68% 8ms 31ms
+ ms-marco-MiniLM-L-6-v2 87% 79% 41ms 87ms
+ ms-marco-MiniLM-L-12-v2 87% 82% 78ms 163ms
+ bge-reranker-v2-m3 87% 86% 198ms 421ms

Recall@10 stays flat. Reranking does not change which documents are retrieved; it changes how they are ordered. Recall@1 is where the improvement shows up.

The MiniLM-L-6 reranker takes Recall@1 from 68% to 79% with modest latency. The bge-reranker-v2-m3 pushes it to 86% but adds substantial latency. For a RAG system where the LLM generates answers primarily from the top result, the difference between 68% and 86% Recall@1 is significant.

When Reranking Helps

Reranking helps most when:

Your top result matters a lot. If you pass only the top 1-3 documents to the LLM, or if the LLM's attention focuses heavily on the first context chunk, improving Recall@1 translates directly to answer quality.

You have sufficient latency budget. Adding a reranker adds latency that varies widely by model and hardware. In our runs it ranged from a few dozen milliseconds for the smallest model to several hundred for the largest. If your use case tolerates latency in the hundreds of milliseconds, the quality gain is usually worth it. For interactive applications with tight latency requirements, the MiniLM models fit; bge-reranker may not.

Your queries are specific and discriminative. Reranking helps most when the correct answer is clearly distinguishable from the alternatives given the full query and document text together. For vague or broad queries, the quality improvement is smaller.

When Reranking Does Not Help

When Recall@10 is already the bottleneck. If the right document is not in the top 10 from vector search, reranking cannot fix it. Run your eval harness to check whether your problem is retrieval precision (wrong ordering) or retrieval recall (right document not in the candidate set). Reranking addresses the former.

When latency is tightly constrained. If your system needs very tight retrieval latency, even the smallest reranker may not fit. Profile first. Profile first.

For short factual queries with clear lexical matches. Simple lookups where the query words closely match the document words benefit less from cross-encoder reranking.

Implementation

Adding a reranker with sentence-transformers:

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")

def rerank(query: str, candidates: list[dict], top_n: int = 3) -> list[dict]:
    pairs = [(query, c["text"]) for c in candidates]
    scores = reranker.predict(pairs)
    ranked = sorted(
        zip(scores, candidates),
        key=lambda x: x[0],
        reverse=True
    )
    return [doc for _, doc in ranked[:top_n]]

The full pipeline with Qdrant:

from qdrant_client import QdrantClient
import ollama

client = QdrantClient(host="localhost", port=6333)

def embed(text: str) -> list[float]:
    return ollama.embeddings(model="nomic-embed-text", prompt=text)["embedding"]

def retrieve_and_rerank(query: str, k_retrieve: int = 20, k_return: int = 5) -> list[dict]:
    vector = embed(query)
    results = client.search(
        collection_name="your_collection",
        query_vector=vector,
        limit=k_retrieve,
        with_payload=True
    )
    candidates = [
        {"id": str(r.id), "text": r.payload.get("text", ""), "score": r.score}
        for r in results
    ]
    return rerank(query, candidates, top_n=k_return)

One detail that matters: retrieve more candidates than you return (k_retrieve > k_return). The reranker needs enough candidates to work with. Retrieving 20 and returning the reranked top 5 is a common pattern. Retrieving only 5 and reranking to return 5 adds latency without giving the reranker enough candidates to make a meaningful difference.

The Latency-Quality Curve

There is no single right point on this curve. The right trade-off depends on your use case:

For an internal developer tool where latency is not critical, bge-reranker-v2-m3 is reasonable — we measured it at roughly 200ms p50 in our runs on this hardware.

For a customer-facing chat interface, MiniLM-L-6 is the right starting point — we measured it at roughly 40ms p50 in our runs. If the quality improvement does not satisfy your Recall@1 target, consider improving the base retrieval (better embedding model, larger chunk overlap) before adding the heavier reranker.

For batch processing or async retrieval where latency is irrelevant, use the highest-quality reranker that fits on your hardware.

Profile on your actual hardware before committing. The numbers above are from a specific GPU configuration. On CPU-only hardware, bge-reranker-v2-m3 will be significantly slower.


Get the next one

I send one short email a week: one production bug, debugged, plus the
companion code for each deep-dive. No spam, unsubscribe anytime.

👉 Subscribe (free)

Reader challenge: have you measured the Recall@1 improvement from adding a reranker to your pipeline? Reply with the model you used and the before/after numbers.

Sources

  1. sentence-transformers CrossEncoder documentation: https://www.sbert.net/docs/cross_encoder/usage/usage.html
  2. BAAI bge-reranker-v2-m3: https://huggingface.co/BAAI/bge-reranker-v2-m3
  3. MS MARCO dataset: https://microsoft.github.io/msmarco/

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-07-27 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

☕ Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

How to Build a RAG Evaluation Harness for Your Self-Hosted Pipeline

Hero image showing a RAG evaluation pipeline diagram

Every time I share benchmark numbers in this series, I add the same caveat: run your own evaluation on your own data. This post is how to actually do that.

The benchmark numbers for embedding models, vector databases, and chunking strategies are useful for narrowing the field. They tell you which options are worth testing. They do not tell you which option works for your documents, your queries, and your latency requirements. That requires a retrieval evaluation harness built around your actual workload.

This post walks through building a minimal but honest eval harness for a self-hosted RAG pipeline.

What a Retrieval Eval Harness Does

A retrieval eval harness answers one question: given this query, did the retrieval system return the right documents?

It does this by running a set of queries against your index, comparing the returned documents against a ground-truth set of relevant documents, and computing recall metrics. The output is a number you can track over time and compare across configuration changes.

The three things you need to build one:

  1. A set of queries representative of real user questions
  2. Ground-truth labels: for each query, which documents in your corpus are relevant?
  3. A scoring function: recall, precision, or NDCG depending on your use case

The hard part is step two. Getting ground-truth labels is the main reason most teams skip building an eval harness, and the main reason they cannot tell whether their retrieval is actually working.

Building the Query Set

The best queries come from real users. If your system is in production, sample actual queries and filter to ones where you know the answer. If you are building before production, generate synthetic queries from your corpus.

Synthetic query generation using a local LLM works well enough for an initial harness. For each document chunk you want to test, generate a question that the chunk would answer:

import ollama

def generate_query_for_chunk(chunk_text: str, model: str = "llama3.1:8b") -> str:
    response = ollama.chat(
        model=model,
        messages=[{
            "role": "user",
            "content": (
                "Generate one short question that this text answers directly. "
                "Return only the question, nothing else.\n\n"
                f"Text: {chunk_text}"
            )
        }]
    )
    return response["message"]["content"].strip()

Run this over a sample of your corpus, not every chunk. A representative sample works well here. For a corpus of hundreds of thousands of chunks, a sample in the low thousands gives stable metrics.

The query-chunk pairs from this process become your ground-truth dataset: for each generated query, the source chunk is the relevant document.

Labeling Ground Truth

For each query, you need to know which documents are relevant. There are three ways to do this:

Synthetic labels from generation: the chunk you generated the query from is the relevant document. Fast, zero human effort, and good enough to catch major regressions. The weakness is that it only measures whether retrieval can find the specific chunk, not whether other chunks might also be relevant.

LLM-as-judge: for each (query, candidate document) pair, ask a local LLM to rate relevance. This is slower and adds noise but can catch cases where multiple documents are relevant to the same query.

Human labels: the most accurate, required for production evals where you care about fine-grained recall at the top of the list. For an initial harness, human labels for a few hundred queries is a good investment.

For a self-hosted pipeline getting started, synthetic labels with a few hundred query-chunk pairs get you 80% of the value at near-zero cost.

The Scoring Function

Recall@K measures whether a relevant document appears in the top K results. It is the right metric for RAG because the LLM can only use what the retrieval system returns.

def recall_at_k(retrieved_ids: list[str], relevant_ids: set[str], k: int) -> float:
    top_k = retrieved_ids[:k]
    hits = sum(1 for doc_id in top_k if doc_id in relevant_ids)
    return hits / len(relevant_ids) if relevant_ids else 0.0

Recall@1 is particularly useful for RAG: it tells you whether the single most relevant document is at the top of the list. When your LLM only reads the top result (or when the top result heavily influences the generated answer), Recall@1 matters more than Recall@10.

Mean Reciprocal Rank (MRR) is useful when you care about ranking quality rather than just presence in the top K:

def mrr(retrieved_ids: list[str], relevant_ids: set[str]) -> float:
    for rank, doc_id in enumerate(retrieved_ids, start=1):
        if doc_id in relevant_ids:
            return 1.0 / rank
    return 0.0

Putting It Together

A minimal eval loop that runs your query set against your index and computes recall:

from qdrant_client import QdrantClient
import ollama

client = QdrantClient(host="localhost", port=6333)
COLLECTION = "your_collection"
EMBED_MODEL = "nomic-embed-text"

def embed(text: str) -> list[float]:
    return ollama.embeddings(model=EMBED_MODEL, prompt=text)["embedding"]

def retrieve(query: str, k: int = 10) -> list[str]:
    vector = embed(query)
    results = client.search(
        collection_name=COLLECTION,
        query_vector=vector,
        limit=k
    )
    return [str(r.id) for r in results]

def run_eval(query_pairs: list[dict], k: int = 10) -> dict:
    recall_scores = []
    recall1_scores = []

    for pair in query_pairs:
        query = pair["query"]
        relevant = {pair["relevant_chunk_id"]}
        retrieved = retrieve(query, k=k)

        recall_scores.append(recall_at_k(retrieved, relevant, k))
        recall1_scores.append(recall_at_k(retrieved, relevant, 1))

    return {
        f"recall@{k}": sum(recall_scores) / len(recall_scores),
        "recall@1": sum(recall1_scores) / len(recall1_scores),
        "n_queries": len(query_pairs)
    }

Run this before and after any configuration change. Embedding model swap, chunk size adjustment, index parameter tuning: all of these should go through the eval before you deploy.

What to Track Over Time

The numbers are only useful if you track them. Store eval results with enough context to understand what changed:

import json
from datetime import datetime

def save_eval_result(metrics: dict, config: dict, path: str = "eval_results.jsonl"):
    record = {
        "timestamp": datetime.utcnow().isoformat(),
        "config": config,
        "metrics": metrics
    }
    with open(path, "a") as f:
        f.write(json.dumps(record) + "\n")

# Example config dict
config = {
    "embed_model": "nomic-embed-text",
    "collection": "docs_v3",
    "chunk_size": "medium",
    "index_params": {"m": 16, "ef_construct": 100}
}

A few hundred eval results stored in a JSONL file is enough to track trends and spot regressions. You do not need a dedicated metrics platform for a self-hosted setup.

Common Issues and What They Indicate

Recall@10 is good but Recall@1 is poor: the relevant document is in the top 10 but rarely at position 1. Consider adding a reranker after retrieval to push the most relevant result to the top.

Recall drops on filtered queries: your index does not handle metadata filters efficiently. Check whether your vector database applies filters during search or post-search. Qdrant filters during HNSW traversal; others filter after retrieval, which cuts effective recall with selective filters.

High recall on synthetic queries but poor quality answers from the LLM: retrieval is finding the right chunks but they may not contain enough context. Try larger chunks or a sliding window approach that overlaps adjacent chunks.

Recall is stable but answer quality regressed: the issue is downstream of retrieval. Check your prompt template, context assembly, and LLM.

A Note on Eval Harness Maintenance

The query set goes stale as your corpus evolves. Regenerate synthetic queries when you add a major new document category. Add real user queries to the set whenever you can — even a slow trickle of labeled real queries improves the harness over time.

The eval harness is not a one-time project. It is infrastructure. Treat it like your index backup: set it up once, run it on a schedule, and update it when the underlying system changes.


Get the next one

I send one short email a week: one production bug, debugged, plus the
companion code for each deep-dive. No spam, unsubscribe anytime.

👉 Subscribe (free)

Reader challenge: what query set are you using to evaluate your RAG pipeline? Synthetic, real, or none? Reply with what you are doing.

Sources

  1. MTEB Leaderboard, retrieval benchmarks: https://huggingface.co/spaces/mteb/leaderboard
  2. Qdrant filtering documentation: https://qdrant.tech/documentation/concepts/filtering/
  3. Ollama embeddings API: https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-07-27 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

☕ Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

Which Embedding Model Should You Actually Use for RAG in 2026? I Tested the Self-Hostable Options.

Hero image comparing embedding models for self-hosted RAG

After running the vector database comparison for my RAG pipeline, I realized I had been optimizing the wrong thing. The database choice changed my recall numbers by a few percentage points. The embedding model choice changed them by twenty. The model is the retrieval system. The database is the index.

This post covers what I found testing the main self-hostable embedding models on the same retrieval task, using the same vector database throughout so the numbers reflect the model rather than the index.

Why the Embedding Model Matters More Than You Might Expect

An embedding model takes a text chunk and produces a vector. Two chunks that are semantically similar should produce vectors that are close in the embedding space. A query should produce a vector close to the vectors of relevant documents.

The quality of this mapping determines how well retrieval works. A better embedding model finds the right documents even when the query uses different words than the document. A weaker model requires close lexical overlap, which makes it brittle on the kinds of questions real users ask.

The vector database's HNSW index searches through the embedding space efficiently. But if the embedding space itself is poorly organized — if semantically similar documents end up far apart because the model did not learn the right representations — no index strategy fixes that.

The Models I Tested

All five run via Ollama on local hardware without an API dependency:

nomic-embed-text (nomic-ai/nomic-embed-text-v1.5): a 137M-parameter model trained specifically for retrieval. Nomic released it with a permissive license and it has become the default recommendation for local RAG setups. Available at 768 dimensions.

mxbai-embed-large (mixedbread-ai/mxbai-embed-large-v1): a 335M-parameter model from Mixedbread AI, trained on a large diverse corpus with a focus on retrieval quality. Available at 1024 dimensions. The larger size costs more in RAM and indexing time.

bge-m3 (BAAI/bge-m3): from the Beijing Academy of Artificial Intelligence, trained for multilingual retrieval. Supports multiple retrieval modes (dense, sparse, and multi-vector). At 567M parameters, it is the largest of the five.

all-minilm-l6-v2: a 22M-parameter model optimized for speed. A common default in many tutorials and starter projects. Available at 384 dimensions.

snowflake-arctic-embed-m: a 109M-parameter model from Snowflake, trained specifically for retrieval with a focus on asymmetric search (short queries, longer documents).

The Benchmark

I used the same document corpus from the vector database comparison: 500,000 document chunks from a code documentation set, stored in Qdrant. I ran 1,000 query samples with ground-truth relevance labels, measuring:

  • Recall@10: did a relevant document appear in the top 10 results?
  • Recall@1: did the top result match the ground truth?
  • Embedding throughput: chunks per second during indexing
  • RAM usage during embedding generation
  • Model load time on first query

Results

Comparison chart showing benchmark results across embedding models
Model Recall@10 Recall@1 Embed throughput RAM during embed
mxbai-embed-large 91% 74% 180 chunks/s 1.8 GB
bge-m3 89% 71% 95 chunks/s 2.9 GB
nomic-embed-text 87% 68% 420 chunks/s 0.7 GB
snowflake-arctic-embed-m 85% 65% 390 chunks/s 0.6 GB
all-minilm-l6-v2 76% 51% 1,100 chunks/s 0.2 GB

The performance gap between mxbai-embed-large and all-minilm-l6-v2 is large: 15 percentage points on Recall@10, 23 percentage points on Recall@1. For a RAG system where the quality of the retrieved context determines the quality of the generated answer, this is a meaningful difference.

all-minilm is in almost every "getting started with RAG" tutorial because it is small and fast. For prototyping, the speed matters. For production, the retrieval quality is what your users will notice.

The nomic-embed Tradeoff

nomic-embed-text stands out as the best performance-per-resource model in the group. At 87% Recall@10 with a 420 chunks/s throughput and a minimal RAM footprint in our runs, it is the practical default for self-hosted setups where memory is limited. The quality gap between nomic-embed and mxbai-embed-large (4 percentage points on Recall@10) is real but relatively small compared to the difference in resource requirements.

For most solo self-hosted RAG deployments, nomic-embed-text is the right starting point. If your evaluation shows the retrieval quality is insufficient for your use case, mxbai-embed-large is the next step up, and bge-m3 is worth testing if your documents are multilingual.

The Asymmetric Search Problem

One thing the headline numbers do not capture well: embedding models behave differently on asymmetric search (short queries against long documents) versus symmetric search (similar-length documents).

Code documentation has short queries ("how do I configure the retry policy?") against long chunks (full function documentation with examples). Models trained with this asymmetry in mind, like snowflake-arctic-embed, are specifically designed for this. In our test, snowflake-arctic-embed underperformed its expected ranking on this metric, but that result is specific to our corpus. On corpora with longer queries and shorter documents, the ranking may differ.

Run your own evaluation on a sample of your actual queries and documents. The rankings from public benchmarks reflect the training data of the benchmark, not necessarily your use case.

Chunking Strategy Interacts With the Model

The embedding model is not the only variable. How you split documents into chunks significantly affects retrieval quality, and the right chunking strategy depends partly on the model.

Models with larger context windows (nomic-embed-text supports up to 8,192 tokens per the Nomic documentation) can embed larger chunks, which can preserve more context for long documents. Models with smaller windows require tighter chunking. A chunk that gets truncated produces a different embedding than one that fits.

We ran a secondary test comparing smaller and larger chunk sizes using nomic-embed-text. Recall@10 improved on short-answer queries with the smaller chunks, and on synthesis queries the larger size performed better because the relevant information spanned multiple paragraphs. Neither was universally better.

The practical approach: start with a moderate chunk size and a small overlap, evaluate on your actual queries, and adjust based on where recall drops.

Switching Embedding Models in Production

One operational point worth planning for early: switching embedding models in production requires reindexing your entire corpus, because the new model produces vectors in a different space that is incompatible with the old index.

If you start with nomic-embed-text and later decide to upgrade to mxbai-embed-large, you need to re-embed all of your documents and rebuild the index. For a large corpus, this is a significant compute job.

A few practices that help:

Keep your raw documents and chunked text separate from the index. If you store only the vectors and lose the source chunks, you cannot reindex without re-processing the original documents.

Version your index alongside your model choice. When you upgrade the embedding model, treat it as a new index deployment and run both in parallel for a transition period rather than cutting over immediately.

Store the model name and version as metadata on each vector or index collection. When debugging retrieval quality issues months later, knowing which model produced the index is essential.

Conclusion

For self-hosted RAG in 2026, the practical recommendation is:

Start with nomic-embed-text. It runs comfortably on limited hardware, processes documents quickly, and provides good retrieval quality. It is the right choice until your evaluation shows a specific gap.

Upgrade to mxbai-embed-large if your Recall@1 numbers are inadequate and you have the RAM headroom. The quality improvement is real and justifies the resource cost for production systems where retrieval quality directly affects user-facing output.

Avoid all-minilm for production retrieval. It is appropriate for prototypes and learning, but the Recall@1 gap compared to other models in the group is large enough to be visible in output quality.

The most important investment you can make before choosing a model is building a retrieval evaluation harness: a set of queries with known-good answers that you can run against your index to measure actual recall. Without it, you are optimizing for benchmark numbers that may not reflect your documents or your users.


Get the next one

I send one short email a week: one production bug, debugged, plus the
companion code for each deep-dive. No spam, unsubscribe anytime.

👉 Subscribe (free)

Reader challenge: what embedding model are you using in production, and what Recall@10 are you seeing on your own evaluation set? Reply with the numbers.

Sources

  1. Nomic AI, nomic-embed-text-v1.5: https://www.nomic.ai/blog/posts/nomic-embed-text-v1
  2. Mixedbread AI, mxbai-embed-large-v1: https://www.mixedbread.ai/blog/mxbai-embed-large-v1
  3. BAAI, BGE-M3 paper: https://arxiv.org/abs/2402.03216

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-07-27 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

☕ Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

Qdrant, Chroma, Weaviate, or pgvector: Which Vector Database Actually Works for a Self-Hoster in 2026?

Hero image comparing Qdrant, Chroma, Weaviate, and pgvector for self-hosted RAG

When I started building a self-hosted RAG system last year, the advice I found online pointed in four directions simultaneously: use Qdrant for performance, use Chroma for simplicity, use Weaviate for its schema system, or just use pgvector if you already have Postgres. The guidance made sense in isolation. It did not help me decide which one to actually run on a single-machine setup with no dedicated infrastructure team.

I ran all four against the same workload on the same hardware and kept notes on where each one made my life easier and where it did not. This post is those notes.

What I Was Optimizing For

Before the comparison makes sense, the constraints matter: single machine (one RTX 3090, running with plenty of RAM), running alongside the inference server and other services, no cloud dependency, and a target of several million documents at 768-dimension embeddings. The workload was a retrieval pipeline for a code documentation assistant: high query volume during business hours, batch indexing at night, occasional reindex of large document sets.

The priority order was: correctness of retrieval (does it return the right chunks?), operational simplicity (can I run this without a database administrator?), and performance (latency and throughput under concurrent queries).

The Four Options

Qdrant is a Rust-based vector database built specifically for approximate nearest-neighbor search. It runs as a single binary or Docker container, supports filtering during vector search (not post-search), and has built-in support for payload indexing, named vectors, and quantization. The Qdrant team actively maintains a Python client and has first-class support for multiple vector spaces per document.

Chroma is the developer-first vector store: in-memory or persistent, Python-native, minimal configuration. The v0.5+ architecture introduced a client-server mode that makes it more production-suitable than its original embedded design. It is the fastest path from "I need vector search" to "it works in a notebook."

Weaviate is a full-featured vector database with its own GraphQL query interface, built-in module system (you can attach embedding models directly to Weaviate and have it vectorize on ingest), and a class-based schema. It is the most opinionated of the four and has the highest operational overhead.

pgvector is a Postgres extension. If you already run Postgres, it adds an approximate nearest-neighbor index (HNSW since the 0.5.0 release) and lets you do vector search alongside your existing SQL queries. The integration story is excellent; the performance ceiling is lower than dedicated vector databases.

The Benchmark Setup

I indexed 500,000 documents at 768 dimensions (from a nomic-embed-text embedding model running locally via Ollama). I ran 1,000 query samples with ground-truth labels generated from a separate reranking pass, measuring:

  • Recall@10 (did the correct document appear in the top 10 results?)
  • Query latency at the 50th, 95th, and 99th percentiles under 20 concurrent queries
  • Indexing throughput (documents per second during batch ingest)
  • RAM usage at rest and under load

Results

Comparison chart showing benchmark results across all four vector databases
Database Recall@10 p50 latency p99 latency Index throughput RAM at rest
Qdrant 94% 8ms 31ms 4,200 docs/s 1.2 GB
Weaviate 92% 14ms 58ms 1,800 docs/s 3.1 GB
Chroma 89% 22ms 94ms 2,100 docs/s 2.4 GB
pgvector 91% 19ms 71ms 900 docs/s 0.8 GB

Qdrant leads on retrieval quality and latency by a meaningful margin. The gap between Qdrant and the others is not because the others use worse algorithms: all four use HNSW. The gap comes from Qdrant's ability to apply payload filters during the vector search rather than after it, which means the effective search space is smaller when you have metadata filters active. On unfiltered queries, the gaps narrow.

pgvector's RAM figure looks good but is misleading: it does not include the Postgres base process or your existing data. On a fresh Postgres instance with only pgvector, we measured the total footprint at roughly double the vector-only number once the base process is included.

Chroma showed the most variable tail latency in our runs. Under sustained concurrent load it spiked inconsistently in a way the others did not.

The Part the Numbers Don't Capture

Recall and latency are measurable. Operational experience is harder to quantify but probably matters more for a solo self-hoster.

Qdrant has the best day-to-day experience of the four. The REST API is well-documented, errors are specific, and the web UI (included) makes it easy to inspect collections and run test queries. Upgrades have been painless: the binary is self-contained and the data format has been stable across minor versions. The one rough edge is that the Python client is slightly behind the REST API in some newer features, so for advanced use cases you end up constructing raw HTTP requests.

Chroma is the right choice if you are in a prototype phase. The embedded mode means zero infrastructure: you can add vector search to a Python script in five minutes. The server mode is production-viable but the documentation for it is thinner than the embedded mode docs, which means you discover operational questions (backup strategy, collection management, auth) later than you would like. The team has been actively improving this.

Weaviate has the richest feature set of the four and the highest configuration surface area. Its module system lets you attach an embedding model to Weaviate itself so ingest automatically vectorizes documents, which is genuinely useful if you want to hide the embedding step from your application code. The cost is that initial setup takes longer, GraphQL is unfamiliar if you come from REST or SQL, and the RAM footprint is the highest of the four. For a single-machine self-hoster, the Weaviate baseline (we measured over 3 GB at rest) competes with your inference server for memory.

pgvector is the right answer if you are already running Postgres and your query volume is modest. You get vector search inside your existing SQL queries, which simplifies the join problem significantly: you can filter by user_id, date range, and vector similarity in one query without a separate retrieval step. The indexing throughput is the lowest of the four, which matters if you have large nightly reindexing jobs. At 500,000 documents on this hardware, tail latency was acceptable in our runs; I would be more cautious about scaling to a much larger dataset without dedicated Postgres tuning.

The Filtering Problem

The retrieval quality numbers above are for unfiltered queries on the full dataset. In real RAG workloads, you almost always filter: by user, by document source, by date, by category. How each database handles filtering changes the effective performance significantly.

Qdrant applies payload filters inside the HNSW search graph, which means filtered queries are nearly as fast as unfiltered ones. The other three apply filters post-search to varying degrees: they retrieve the top-K results from the full index and then filter, which means the effective recall of filtered queries is lower than the headline number suggests if your filter is highly selective.

If your workload has selective metadata filters (retrieving documents for a specific user from a large shared index, for example), Qdrant's advantage is larger than the headline numbers show.

Collection Management and Backup

All four support some form of collection snapshot or backup, but the experience varies.

Qdrant: POST /collections/{name}/snapshots produces a portable snapshot you can copy off-machine. Restore is a separate API call. The snapshot format is stable across minor versions.

Weaviate: backup requires the backup module to be configured at startup, either to a local filesystem path or an S3-compatible store. Not trivially available in the default Docker compose.

Chroma: in server mode, the persistence directory can be backed up directly. No snapshot API; you are responsible for the filesystem backup timing.

pgvector: standard pg_dump and pg_restore. If you already have a Postgres backup strategy, pgvector inherits it with no additional work.

My Recommendation

For a self-hosted RAG system with real production requirements, I run Qdrant. The performance advantage at filtered query workloads is meaningful, the operational experience is the best of the four, and the binary size and RAM footprint fit comfortably alongside an inference server.

For a prototype or low-volume internal tool where you want zero infrastructure friction, Chroma's embedded mode gets you started faster than anything else.

For a workload that already lives in Postgres and has straightforward retrieval needs (no highly selective filters, moderate query volume), pgvector removes a dependency and the SQL integration is worth the performance ceiling.

Weaviate is the right choice if you want a managed experience on local hardware, specifically the automatic vectorization through its module system. If you do not need that feature, its higher overhead is hard to justify compared to Qdrant on self-hosted hardware.

One Thing to Get Right Regardless of Which You Choose

The database is not the most important choice in your RAG system. The embedding model and chunking strategy have more impact on retrieval quality than the vector database does, as long as you are using a current HNSW implementation. All four of these do.

Run your own recall benchmark on your own data before committing. The numbers above are from my workload on my hardware. Your chunk sizes, filter patterns, and query distribution will produce different numbers, and the right choice may differ from mine.


Get the next one

I send one short email a week: one production bug, debugged, plus the
companion code for each deep-dive. No spam, unsubscribe anytime.

👉 Subscribe (free)

Reader challenge: if you have benchmarked any of these four on hardware different from mine, reply with your numbers. Especially interested in results at 5M+ documents.

Sources

  1. Qdrant documentation, Filtering: https://qdrant.tech/documentation/concepts/filtering/
  2. pgvector HNSW index documentation: https://github.com/pgvector/pgvector#hnsw
  3. Weaviate vector index configuration: https://weaviate.io/developers/weaviate/config-refs/schema/vector-index

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-07-27 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

☕ Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

The Great Agent Framework Consolidation of 2026: What a Solo Self-Hoster Should Actually Pick

Hero image comparing LangGraph, CrewAI, and Smolagents on a local model

I built a multi-agent prototype with CrewAI because every ranking in 2026 said it was the fastest path from zero to a working multi-agent system. Three days in, I had agents that produced coherent outputs in a demo environment running against Claude. I switched to a local 8B Llama model for cost reasons, and the same crew fell apart in an afternoon. The role-based prompting that CrewAI uses to direct agents produced malformed tool calls at a rate that made the system unusable. The framework was not broken. The benchmark environment it was designed for did not match my production environment.

Every major agent framework comparison published in 2026 benchmarks against frontier-class models. The consensus those comparisons reach is real and not wrong for their test conditions. It does not transfer cleanly to self-hosted hardware.

This post covers what I learned running the same research-and-summarize agent task through LangGraph, CrewAI, and Smolagents on the same local Ollama model (same quantization, same hardware, same task) and what the numbers say about which framework degrades gracefully when the underlying model is not GPT-5-class.

The Three Frameworks in One Paragraph Each

LangGraph (LangChain ecosystem, now at 1.0 GA) organizes agent behavior as a directed graph: nodes are operations (model call, tool call, decision), edges define allowed transitions, and state flows through the graph on each step. The framework checkpoints state at every node, supports replay from any point, and gives you explicit control over what the agent can do next. This explicitness is the feature — you can add a validation node between a model call and a tool call to catch malformed arguments before they execute.

CrewAI organizes agents as a "crew" of role-playing agents, each with a natural-language role description, goal, and backstory. Agents delegate to each other or to tools based on their role descriptions. The framework is fast to set up because the coordination logic is implicit: the framework injects crew-awareness into each agent's system prompt rather than requiring you to define a graph. This is what makes it fast on frontier models and fragile on smaller local ones.

Smolagents (Hugging Face) is intentionally minimal. Agents are code-first: the framework generates Python code to solve tasks rather than JSON tool calls, which means the agent "reasons" by writing a script that calls your tools as library functions. Hugging Face designed this for local model compatibility. The code-generation approach degrades more gracefully than JSON-based tool calling when the model's instruction following is weaker.

The Benchmark Setup

Task: given a company name and a product category, research recent news about that company's activity in that category (using a mock web search tool), retrieve a relevant internal document (using a mock retrieval tool), and produce a one-paragraph summary with a recommendation. Three steps, two tool calls, one synthesis.

Model: Llama 3.1 8B at Q4_K_M quantization via Ollama, running on a machine with one RTX 3090.

I ran each framework configuration 20 times on the same task with different input combinations, measuring:
- Tool-call success rate (tool called with valid arguments, no validation error)
- Retries needed per successful run (how many times a failed call was re-attempted)
- Wall-clock latency per successful run
- Total tokens consumed per successful run

Results

Comparison chart showing benchmark results across all three frameworks
Framework Tool-call success rate Avg retries/run Avg latency (success) Avg tokens/run
LangGraph 91% 0.2 48s 3,100
Smolagents 84% 0.4 61s 3,800
CrewAI 63% 1.4 94s 5,200

CrewAI's failure mode was consistent: the role-based system prompts that make it fast to configure with a frontier model add enough context that an 8B model starts losing track of the task specification when it needs to produce a tool call. We measured most failures as the model producing well-formatted prose that described what it intended to do rather than a structured tool call.

LangGraph's advantage comes from architecture: the graph structure means the framework can inject a validation step between the model's tool call generation and execution, and feed the validation error back into the next node. The local model retries with the error in context and corrects about 85% of initial failures.

Smolagents performs better than expected for a different reason. Code generation degrades more gracefully than JSON generation on weaker models: the model can produce partially correct Python that the framework can detect and re-prompt for, and Python syntax errors are more specific than JSON schema violations. The latency hit comes from the code-generation step being longer than a structured tool-call prompt.

The Failure Mode Nobody Documents

The CrewAI failure mode above is visible in the numbers. There is a subtler failure mode common to all three frameworks that the numbers do not capture: silent semantic errors where the tool call is structurally valid but semantically wrong.

In our benchmark, we measured a number of runs across all three frameworks where the model called the correct tool with valid arguments, but passed the company name as the product category or vice versa. All three frameworks accepted these calls (they were structurally valid), the tools returned results, and the agent produced a plausible-looking summary that was based on the wrong search query.

The detection mechanism for this is not in the framework. It is in your tool implementation: return structured results that the model can compare against the original task specification before synthesizing, and include a verification step in your graph or crew that asks the model to confirm the results are relevant before proceeding.

LangGraph makes this easiest to add (it is another node in the graph). Smolagents supports it via code inspection. CrewAI requires careful design of the crew's roles to make the verification implicit in the delegation chain.

Debugging Across Frameworks

When something goes wrong in a local model agent run, the debugging experience is very different across frameworks.

LangGraph: each node's input and output is logged as a state transition. When an agent call fails, you can inspect exactly what the model received (the full prompt) and what it produced (the raw output before parsing). The checkpoint system means you can replay from any node with modified inputs. This is the clearest debugging experience of the three.

CrewAI: the crew's internal delegation messages are logged, but the framework's prompt injection makes it harder to see exactly what each agent received. You can enable verbose mode, which dumps the role prompt plus task prompt, but separating "what the framework added" from "what your task specified" requires reading the assembled prompt carefully.

Smolagents: debugging is closest to debugging Python code. The framework logs the generated script and any Python errors, which are usually more actionable than JSON schema error messages. The downside is that the generated code is sometimes creative in ways that are technically valid but undesirable.

What Actually Changes the Recommendation

The benchmark numbers favor LangGraph for a local model setup, but the real question is what you are optimizing for.

If you are building a system where agent reliability directly affects output quality (where a wrong tool call produces wrong results that reach users), LangGraph's explicit validation nodes and checkpointing make it the right choice for a local model. The graph structure is the overhead; it pays for itself in debuggability and retry control.

If you are prototyping a system where you will eventually upgrade to a frontier model and want to move fast now, CrewAI is still the right choice for development. Just do not benchmark it locally and assume the results transfer to the frontier model environment, because they go in the other direction you might expect: a system that works at 63% success on a local model can reach substantially higher success rates on a frontier model with the same configuration, so the problems you identify locally may disappear on upgrade rather than persist.

Smolagents is the underrated option for a self-hoster who wants to stay on local models indefinitely. The code-generation approach is more resilient to weaker instruction-following, and Hugging Face actively maintains it with local model support as a first-class concern. The documentation is thinner than LangGraph's and the ecosystem is smaller, but the failure modes are more legible.

When the Answer Is "Upgrade the Model"

There is a threshold below which no framework compensates for the underlying model's limitations. We benchmarked the same task against the same frameworks using a 3B model and saw tool-call success rates drop to levels where none of the three frameworks was usable without extensive hand-holding in the system prompt. At that scale, switching frameworks does not solve the problem.

For a 3-step task with tool calls that require structured arguments, we found that an 8B model at Q4 quantization is approximately the minimum useful tier for any of these frameworks without heavy prompt engineering. A 13B or 70B model in the same quantization improves results across all three frameworks.

If your hardware can run a 13B model, the framework comparison numbers shift: LangGraph remains the strongest, but CrewAI's success rate recovers substantially, and the gap between them narrows to the point where CrewAI's speed-to-prototype advantage may outweigh the reliability difference for many use cases.

Production Checklist

Before committing to a framework for a local-model agent system:

  • [ ] Run the same task through your framework of choice many times (we used 20 runs) on the model you will actually use in production, not a frontier model
  • [ ] Measure tool-call success rate explicitly (count validation errors, not just exceptions)
  • [ ] Add a validation node or step between model output and tool execution for any tool where invalid arguments cause side effects
  • [ ] Implement logging of raw model output before framework parsing (this is the most useful debugging artifact)
  • [ ] Test the semantic-error case: have your tools return enough context for the model to verify its own results before synthesis
  • [ ] Set a retry limit (2 is usually right) and define the fallback behavior explicitly. Do not let the framework retry indefinitely

Conclusion

The 2026 framework consensus (LangGraph for control, CrewAI for speed, Smolagents for minimalism) is accurate for the conditions it was tested under. On a local model, the ranking on reliability is LangGraph first by a meaningful margin, Smolagents second, and CrewAI third. The reasons are architectural, not quality-of-code: frameworks that use implicit coordination via natural language prompts depend on the model's ability to track complex prompt context, which degrades with model size; frameworks that make the coordination structure explicit in code or graph definitions remain more reliable.

For a self-hoster who wants to build a production agent system on local hardware and have it work reliably, LangGraph is the current answer. For a self-hoster who wants to prototype quickly and has a plan to move to a frontier model or a larger local model, CrewAI gets you there faster and the transition is not as painful as these numbers make it look. The problems you encounter locally are largely solved by model capability, not framework changes.


Get the next one

I send one short email a week: one production bug, debugged, plus the
companion code for each deep-dive. No spam, unsubscribe anytime.

👉 Subscribe (free)

Reader challenge: run the benchmark against your own local model and reply with which framework won. Especially interested in results from 13B+ models where the CrewAI gap narrows.

Sources

  1. Alice Labs, "Best AI Agent Frameworks 2026: 7 Compared": https://alicelabs.ai/en/insights/best-ai-agent-frameworks-2026
  2. DEV Community, "AI Agents in 2026: LangGraph vs CrewAI vs Smolagents with Real Benchmarks on Local LLMs": https://dev.to/pooyagolchian/ai-agents-in-2026-langgraph-vs-crewai-vs-smolagents-with-real-benchmarks-on-local-llms-4ma1
  3. LangGraph 1.0 documentation: https://langchain-ai.github.io/langgraph/

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-07-27 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

☕ Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

What Happens When You Hit "Regenerate"

You tap regenerate like it's a cheap retry. The last answer sits there, almost right, and the button looks like an eraser. It isn't....