am. Andrea Migliavacca

Completed project / research, RAG and agents

RAG: choosing how to search and answer.

I compared retrieval, reranking and generation on financial questions. A decision lab brings together quality, latency and cost, separating project measurements from external data.

The guiding question: which sources actually reach the answer within time and cost constraints?

01 / STARTING POINT

Finding the right sources

A RAG system searches a corpus for relevant passages and passes them to the model that writes the answer. To assess retrieval, I used the financial subset of MTRAG, which links questions to annotated sources.

Annotations help measure retrieval, but they do not list every passage that could help answer a question.

in the code: recall and nDCG

These are the two measures used throughout the project. Recall counts relevant sources among the top ten results; nDCG also rewards their position, giving more credit to a relevant source near the top.

def recall_at(ranked_ids, relevant, k):
    return len(set(ranked_ids[:k]) & relevant) / len(relevant)


def ndcg_at(ranked_ids, relevant, k):
    dcg = sum(1 / math.log2(rank + 2)
              for rank, pid in enumerate(ranked_ids[:k]) if pid in relevant)
    ideal = sum(1 / math.log2(rank + 2) for rank in range(min(k, len(relevant))))
    return dcg / ideal if ideal else 0.0

From scripts/run_d1_pilot.py.

A question close to several corpus passages: some are annotated as answers, while others are topically relevant but unannotated.
Topically similar passages and annotated sources do not always coincide. Open the figure.
02 / METHOD

A lab for comparing options

The lab compares embedding models, retrieval, reranking and generation. For each option it shows quality, time, cost and data provenance; context constraints filter the alternatives.

Open the lab and try the filters.

Il laboratorio reale: vincoli, indicizzazione e confronto degli embedding
The actual lab interface, with constraints and evidence labels. Open the lab to inspect the comparisons.
03 / RETRIEVAL

How many candidates should be reranked?

Retrieval proposes passages, then a reranker re-examines a subset and moves the most useful ones to the top. I compared 20, 30, 50 and 60 candidates on the same 180 questions, measuring the share of annotated sources that reached the top ten.

I chose 50 candidates: the highest observed result was 48.9%, versus 47.8% at 30. The difference is modest, and the uncertainty intervals overlap.

in the code: passage vectors and search

Each corpus passage is encoded as a vector once. For each question, only the question is encoded and compared with all passage vectors by dot product. The model version is fixed because updated weights would change the results.

self.model = SentenceTransformer(model_name, revision=revision, device="cpu")

# Vettori normalizzati a lunghezza 1: il prodotto scalare è la similarità coseno.
self.embeddings = self.model.encode(
    [p.indexed_text for p in passages],
    batch_size=batch_size,
    normalize_embeddings=True,
)

def search(self, query, top_k=10):
    query_vector = self.model.encode(
        [self.query_instruction + query], normalize_embeddings=True)[0]
    scores = self.embeddings @ query_vector      # la domanda contro tutti i brani
    order = np.argsort(-scores, kind="stable")[:top_k]
    return [RankedPassage(self.passages[i].passage_id, float(scores[i])) for i in order]

From src/retrieval.py.

in the code: the reranker

In the final configuration, I kept models and questions fixed and varied only the number of passages passed to the reranker.

for d in RERANK_DEPTHS:  # 20, 30, 50, 60
    per_q = []
    for i, g in enumerate(gold_ranks):
        order = np.argsort(-sc[i][:d], kind="stable")
        pos = {int(p): r + 1 for r, p in enumerate(order)}
        per_q.append(metrics([pos[r - 1] if r <= d else r for r in g]))
    record(f"reranking sui primi {d}", per_q)

Adapted from esperimenti/studio-embedding-retrieval/16_configurazione_finale.py.

Top-ten recall: 46.9% with 20 candidates, 47.8% with 30, 48.9% with 50 and 48.4% with 60; expanded vertical axis.
The 46.5–49.5% vertical scale makes a difference of roughly one percentage point visible. Open the figure.
04 / GENERATION

From sources to the answer

A source can appear in retrieval results yet be omitted from the context the model reads. I tracked this step and compared generated answers, alongside retrieval metrics.

The configuration with the best average score brought in fewer useful sources for some questions; the resulting answers differed mainly in completeness.

Bar chart showing sources at the top of the list, within the top ten, retrieved but excluded from the answer context, and never retrieved.
A source can be retrieved yet never reach the answer because it is left out of the context sent to the model. Open the figure.
05 / ALTERNATIVES

Multi-step search and advance synthesis

In the RAG setup I studied, retrieval precedes the answer. An agent can search again based on what it finds, which can help with multi-hop questions. A WikiLLM prepares cross-document summaries ahead of recurring queries.

The choice depends on the question, the available time and how often sources change.

Side-by-side diagram: the agent chooses search steps, while the wiki prepares summaries before questions.
Search guided during the question versus synthesis prepared before it. Open the diagram.

Back to projects