Skip to main content

Vendi-RAG

Adaptively trading off diversity and quality significantly improves retrieval-augmented generation with LLMs

arXiv Python 3.9+ License: MIT

Similarity search scores every document on its own, so what comes back is usually several restatements of the same fact. Multi-hop questions need the opposite: a few documents that say different things. Vendi-RAG scores the retrieved set, trading query relevance against set-level diversity, and puts that behind one knob.

Vendi retrieval sweeping the diversity weight

At s = 0 the retriever takes a slice off the right-hand edge: eight documents, fewer than four distinct facts between them, and the evidence missed. Raising s fans the selection out until the evidence is in context. Reproduce it with vendirag demo --gif out.gif.


Install

pip install vendirag

The retriever, the Vendi Score, the pipeline, the toy corpus, and the offline backend all run on numpy alone. Everything else is opt-in:

pip install "vendirag[openai]"      # OpenAI models
pip install "vendirag[anthropic]"   # Claude models
pip install "vendirag[embeddings]"  # sentence-transformers encoders
pip install "vendirag[viz]"         # plots and the animation above
pip install "vendirag[research]"    # everything needed to rerun the paper

The retriever, on its own

Index some text, get a set back. No LLM, no vector store, no API key.

from vendirag import VendiRetriever

retriever = VendiRetriever.from_texts(documents, s=0.8, k=5)
docs = retriever.retrieve("Which instrument did the expedition carry?")

s is the only knob: 0 is pure relevance (identical to top-k similarity search), 1 ignores relevance within the pool entirely, and 0.8 is the paper's default. Ask for the diagnostics when you want to see what happened:

result = retriever.retrieve_details("Which instrument did the expedition carry?")
result.vendi_score      # effective number of unique documents in the set
result.mean_similarity  # mean query-document cosine similarity
result.documents        # the selected documents, in selection order

VendiRetriever uses sentence-transformers when it is installed and a built-in deterministic hashing embedder otherwise. Anything with an encode(texts) -> (n, d) method works:

from vendirag import SentenceTransformerEmbedder

retriever = VendiRetriever(
    embedder=SentenceTransformerEmbedder("all-mpnet-base-v2"),
    s=0.8, k=5, candidate_pool=50,
)

Already have a vector store?

Keep it. VendiReranker adds only the selection step, so the corpus stays wherever it lives — Chroma, FAISS, Pinecone, Elasticsearch, pgvector.

from vendirag import VendiReranker

def candidates(query, n):
    """Return (documents, doc_embeddings, query_embedding)."""
    hits = my_index.search(query, n)
    return hits.docs, hits.vectors, my_encoder(query)

reranker = VendiReranker(candidates, s=0.8, k=5, candidate_pool=50)
docs = reranker.retrieve("who led the expedition?")

Or reach for the selection function directly, if you would rather own the loop:

from vendirag import vendi_rerank

docs = vendi_rerank(query_embedding, pool_embeddings, pool_documents, k=5, s=0.8)

The full pipeline

Retrieval is half the method. The other half is a loop that treats s as something to steer rather than tune — when the answer is good, s falls and retrieval consolidates; when it is poor, s rises and retrieval fans out to find what is missing.

from vendirag import VendiRAG, OpenAILLM

rag = VendiRAG(retriever, llm=OpenAILLM("gpt-4o-mini"))
result = rag.answer("In which town is the observatory that houses the instrument?")

print(result.answer)              # best answer across all iterations
print(result.quality)             # judge score in [0, 1]
print(result.s_trajectory)        # how the diversity weight moved
print(result.documents)           # the evidence behind the answer

Claude works the same way, and the judge can be a different, cheaper model — the quality target is relative to the running best rather than absolutely calibrated, so the judge only has to rank, not score:

from vendirag import AnthropicLLM

rag = VendiRAG(
    retriever,
    llm=AnthropicLLM("claude-sonnet-5"),
    judge_llm=OpenAILLM("gpt-4o-mini"),
)

Anything callable is accepted, so a local model or a LangChain runnable drops straight in:

rag = VendiRAG(retriever, llm=lambda prompt: my_local_model(prompt))

Algorithm 1

Initialize  q₁ ← q,  s₁ ← 0.8,  best ← 0
for i = 1 … N:
    C   ← TopK-Similar(qᵢ, 𝒦, |C|)              candidate pool
    Dᵢ  ← GreedyVRS(C, sᵢ, k)                   Eq. 2, below
    rᵢ  ← GenerateCoT(qᵢ, Dᵢ)
    âᵢ  ← GenerateAnswer(q, Dᵢ, r₁…ᵢ)           the ORIGINAL question
    Qᵢ  ← LLM-Judge(âᵢ, q, Dᵢ)                  mean(coherence, relevance, alignment) / 10
    if Qᵢ > best:  â*, best ← âᵢ, Qᵢ
    if Qᵢ ≥ τ:     return â*
    s_target ← clip(1 − Qᵢ / max(best, ε), 0, 1)
    sᵢ₊₁     ← β·sᵢ + (1−β)·s_target            Eq. 3, an EMA
    qᵢ₊₁     ← RefineQuery(qᵢ, âᵢ, rᵢ)
return â*

The selection objective is the Vendi Retrieval Score,

VRS(D) = s · ṼS(D) + (1 − s) · S̃S(q, D)

where ṼS is the Vendi Score of the set — the exponential of the Shannon entropy of the eigenvalues of its similarity kernel, interpretable as the effective number of unique documents — rescaled to [0, 1], and S̃S is the mean query-document cosine similarity min-max normalized over the candidate pool. Documents are added greedily, since maximizing over all k-subsets is intractable.

Cost is O(k · |C| · k³): the eigendecomposition is on the selected subset, of size at most k, never on the corpus.

Configuration

Argument Default Meaning
initial_s 0.8 s₁, the starting diversity weight
k_docs 5 k, documents selected per iteration
k_candidates 50 |C|, candidate pool size
max_iterations 5 N, iteration cap
quality_threshold 0.85 τ, the early-stop threshold
beta 0.3 β, EMA momentum
dynamic_s True False pins s — the fixed-s variant
early_stopping True False always runs N iterations
refine_query True False re-retrieves with the original question
judge_llm None separate judge model; defaults to the generator
on_iteration None callback fired per iteration, for logging or progress

The paper recommends fixed s = 0.8 as the default configuration: s is the substantive knob and a well-chosen constant captures most of the gain at no extra inference cost. Adapting it online is a second-order refinement whose benefit concentrates on hard queries.

rag = VendiRAG(retriever, llm=..., initial_s=0.8, dynamic_s=False)

Just the score

The Vendi Score stands alone as a way to measure how redundant any retriever's output is:

from vendirag import vendi_score

vendi_score(embeddings)   # in [1, n]: 1 if identical, n if mutually orthogonal

Try it

A synthetic multi-hop benchmark ships with the package and runs in seconds with no API key, no model download, and no network:

vendirag demo                              # the numbers below
vendirag demo --gif assets/demo.gif        # and the animation
python examples/toy_demo.py                # the same, annotated

Or work through examples/vendi_rag_toy_demo.ipynb, which builds the whole argument step by step with plots.

The corpus is a fictional world of Arctic expeditions. Answering a question means chaining four facts that live in four separate documents, and around each chain sits a thicket of near-duplicate documents repeating the expedition's name — the way a real corpus accumulates coverage of a notable entity.

Retrieval (k = 8, |C| = 50, 60 questions, deterministic hashing embedder):

method evidence found effectively unique docs
top-k similarity 7% 3.7 / 8
MMR (λ = 0.5) 73% 7.4 / 8
MMR (λ = 0.7) 78% 7.3 / 8
Vendi retrieval (s = 0.8) 80% 7.4 / 8
Vendi retrieval (s = 1.0) 57% 7.5 / 8

End to end, using the built-in model-free reader so the run is free and deterministic:

configuration exact match
single-shot, top-k 0%
single-shot, Vendi s = 0.8 2%
iterative, top-k 13%
Vendi-RAG (fixed s = 0.8) 78%
Vendi-RAG (dynamic s) 73%

Neither half is sufficient alone: one diverse retrieval cannot reach hop 3, and iterating over a redundant set just re-reads the same thicket.

These are synthetic numbers illustrating the mechanism, not benchmark results — the corpus was built to exhibit the failure mode. They are reproducible, since both the corpus and the embedder are deterministic. For real-data results see the paper, or research/ to rerun them.

Reproducing the paper

HotpotQA, MuSiQue, and 2WikiMultiHopQA, with the Chroma index and scoring harness: see research/README.md.

pip install -e ".[research]"
bash research/download/processed_data.sh
python research/ingestion.py --dataset hotpotqa --build
python research/run_benchmark.py --dataset hotpotqa --model gpt-4o-mini
python research/evaluation/cal_f1_em.py --predictions results/<file>.csv

Layout

vendirag/            the library
  vendi.py           Vendi Score primitives          (numpy only)
  retriever.py       VRS selection, VendiRetriever, VendiReranker
  pipeline.py        VendiRAG — Algorithm 1
  llm.py             OpenAI / Anthropic adapters, prompted and offline backends
  embeddings.py      hashing and sentence-transformers embedders
  prompts.py         the paper's prompts, including the exact judge prompt
  toy.py             the synthetic multi-hop corpus and its metrics
  viz.py             plots and the animation above
  cli.py             the `vendirag` command
examples/            runnable demo script and notebook
research/            paper reproduction: ingestion, benchmarks, baselines, scoring
tests/               pytest suite

Citation

@misc{rezaei2025vendiragadaptivelytradingoffdiversity,
      title={Vendi-RAG: Adaptively Trading-Off Diversity And Quality Significantly
             Improves Retrieval Augmented Generation With LLMs},
      author={Mohammad Reza Rezaei and Adji Bousso Dieng},
      year={2025},
      eprint={2502.11228},
      archivePrefix={arXiv},
      primaryClass={cs.CL},
      url={https://arxiv.org/abs/2502.11228},
}

The Vendi Score was introduced in:

@article{friedman2023vendi,
  title={The Vendi Score: A Diversity Evaluation Metric for Machine Learning},
  author={Friedman, Dan and Dieng, Adji Bousso},
  journal={Transactions on Machine Learning Research},
  year={2023},
}

License

MIT © Mohammad Reza Rezaei, Adji Bousso Dieng

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vendirag-0.2.0.tar.gz (50.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vendirag-0.2.0-py3-none-any.whl (45.6 kB view details)

Uploaded Python 3

File details

Details for the file vendirag-0.2.0.tar.gz.

File metadata

  • Download URL: vendirag-0.2.0.tar.gz
  • Upload date:
  • Size: 50.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for vendirag-0.2.0.tar.gz
Algorithm Hash digest
SHA256 1b40c852f0d06fa3f548d3f80258fb8f1ab959664ef6e6608cfecd29bfeadf62
MD5 87e80554d1f3b47ec5669d8b47836047
BLAKE2b-256 21e28acd55246858e039802acabd1b2a199f7b1c6182fe16c0f319351c916969

See more details on using hashes here.

File details

Details for the file vendirag-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: vendirag-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 45.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for vendirag-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3cc145842f48302cb8c97e87c29eb198bdd366488b1db02fe24c11168504a269
MD5 603650decc3fdad28136519b9236e370
BLAKE2b-256 e48f5ba0ed3577dcb8c21a09c87f0c132762cc0b56d798c53142ff413ec819fc

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page