delphi
Field note · 0112 min read

The context engine is the product

Our benchmark was comparing vectors from two different embedding models. Here is how we found it, and what fixing it was actually worth.

By Synthetic Sciences

EvaluationRetrievalDeveloper agents

We are publishing the retrieval results behind Delphi, the corpora they were measured on, and the per-query traces they came from. On Agent Retrieval Bench v2, the shipped build returns more of the answer set than the leading hosted context engine and does it 5.7 times faster on a laptop. It does not lead on every metric, and this piece is specific about which.

Results

Delphi is an open-source context engine: it indexes repositories, documentation, papers, and datasets, and answers an agent's question with the files that answer it. The number that matters for an agent is whether the file it needs is in the window it gets, which is what these measure.

Shipped build against the hosted comparator60 paired cases · 95% interval
MetricNiaDelphiDifferenceResolved
MRR0.3010.241-0.060 [-0.142, +0.020]no
Recall@50.3210.326+0.005 [-0.097, +0.108]no
Recall@200.3920.499+0.107 [+0.008, +0.207]yes
BCY@8k0.3440.362+0.019 [-0.081, +0.119]no
Query latency31.8s5.6s5.7x fasteryes
Paired per case over the 60 final-split cases the comparator completed without error, 4000 bootstrap resamples. A difference counts only when its interval excludes zero, which here is Recall@20 and latency. Both engines indexed the same corpora and were scored by the benchmark's own code.

Recall@20 is the difference that survives: 0.107 in Delphi's favour, 95% interval [0.008, 0.207], winning 16 cases to 6. Latency is the other: 5.6s against 31.8s.

MRR reads 0.241 against 0.301, which looks like a loss and is not one we can claim: the interval runs from -0.142 to 0.020 and the cases split 18 to 24 with 18 ties. At sixty cases that is a coin flip. We say so rather than reporting it either way.1

The two engines also answer differently. The comparator returns about 8 files per query and Delphi returns 20. Short lists concentrate on rank one, long lists cover more of the answer. Which you want depends on whether your agent gets one shot or can keep reading.

Against the benchmark's own published baselines on the development split, scored by the same code:

Against published baselines75 cases · higher is better
SystemMRRRecall@5Recall@20
Delphi0.2200.3690.551
Nia0.2280.3910.449
grep0.1800.3020.578
RepoMap0.1690.2400.551
lexical0.1270.1980.451
BM250.1160.1360.429
Baselines are the benchmark's own published runs on the same split and candidate filter, scored by the same code. Gold marks the leader in each column. grep still holds Recall@20.

Delphi does not lead Recall@20 outright either. Plain grep gets 0.578. We would rather print that than drop the column. If your engine finds the right file somewhere in twenty results, you have not beaten grep -r. The argument has to be won at the top of the list, and that is where we still have work to do.

A corpus orthogonal to itself

The symptom was that semantic queries returned nonsense. We asked a 68-repository corpus about TLS configuration in a gRPC proxy and got back an interval tree, a systemd journal wrapper, and some file-locking utilities. Exact symbol lookups worked perfectly. Anything that went through the embedding did not.

Two embedding models can produce vectors of the same width. When they do, pgvector will compute a cosine between them without complaining. The query succeeds, results come back ranked, and the ranking is noise, because the two spaces have nothing to do with each other. There is no error to catch. The only thing wrong is the answer.

The check that found it

The check is embarrassingly simple. Take a chunk out of the index. Embed its own content with whatever model answers queries today. Compare that vector against the one already stored. If the two sides agree, a chunk has to be nearly identical to itself.

Self-retrieval check768-dim both sides

0.010

Mismatched models

0.943

Aligned models

Cosine between a chunk's stored vector and a fresh embedding of that chunk's own exact content. A matched space returns ~1.0. Because both models emit 768-dimensional vectors, pgvector accepted the comparison and 68 repositories scored as noise without raising anything.

It scored 0.0101. That is orthogonal, the number you get from two random vectors. Pointing the query path at text-embedding-3-small, the model that had actually built the index, moved the same comparison to 0.943. The vector branch was weighted 0.5, the largest weight in the pipeline. Half the ranking signal had been random for the entire evaluation.

The database had known all along. repositories.embedding_model records which model indexed each repository; it simply was never compared against the model answering queries. Delphi now makes that comparison on every search and reports it on /backend-health, because an engine that returns confident nonsense is worse than one that returns an error.

Comparing incomparable numbers

With the embedding fixed, a second problem surfaced. Delphi fans a query out across vector, BM25, symbol, path, and trigram branches, then fuses the results. Each branch normalised its own scores by dividing by that branch's top score. So the best hit of every branch got pinned to exactly 1.0, however bad it was.

A query with no good semantic match still produces a vector branch, and its first result is rank one by definition. Rescaled, that least-bad hit became a perfect 1.0, and at weight 0.5 it outranked chunks that three branches independently agreed on. The fingerprint was a suspiciously round 0.5000 at the top of result lists: weight times a manufactured perfect score.

Reciprocal rank fusion exists for this reason. Ranks compare across branches. Raw scores do not. A cosine and a ts_rank_cd were never the same unit. Fusion now scores position instead of magnitude.

The path nobody indexed

The third problem was the most mundane. BM25 indexes chunk content. Nothing indexed file_path. A query naming a file could not retrieve that file's neighbours lexically at all. Searching for etcd_grpcproxy_test returned fileutil.go, because the filename existed nowhere in the searchable text.

This matters because agents anchor on paths constantly: “what tests cover grpc_proxy.go”, “why did tokens.py change”. Delphi now has a path-affinity branch that matches on separator-stripped lowercase, so an anchor of grpc_proxy reaches etcd_grpcproxy_test.go, which underscore-sensitive comparison misses. Results are capped per directory, because a stem like grpc_proxy matches thirty sibling files at a perfect score and would otherwise fill the branch before the one test in tests/e2e/ ever appeared.

The reranker that never ran

Delphi had a cross-encoder reranker. It had never once executed during the evaluation. The model loaded lazily on first query, the load was a multi-hundred-megabyte download, and the call site wrapped it in a try/except that fell back silently to fused ranking. Every query took the fallback.

Warming it at startup and reporting readiness on the health endpoint fixed the availability problem and produced a genuinely surprising result once we could measure it.

Cross-encoder selectionquality and latency together
ModelParamsMRRLatency
none0.1761.0s
bge-reranker-base278M0.17321.3s
ms-marco-MiniLM-L-622M0.1932.9s
The 22M-parameter model beats the 278M code-aware one on every metric while running roughly seven times faster, so the larger model cost latency and quality at once.

The 22M-parameter ms-marco-MiniLM model beats 278M-parameter bge-reranker-base on every metric while running about seven times faster. Preferring the larger, code-aware model, which is what the default did, cost latency and quality at the same time. Reranking depth behaves the same way: at a window of 100 with the cross-encoder deciding the order outright, Recall@20 collapses to 0.444, because the model confidently promotes plausible-looking files from the tail. Blending it over the fused score, shallowly, is what makes it useful.

What each fix was worth

What each change was worthsame 75 cases
As previously benchmarked0.161

query and index in different embedding spaces · mrr 0.055 · 1.0s

Embedding space aligned0.549

same model indexing and querying · mrr 0.173 · 0.9s

+ rank fusion, path affinity0.552

reciprocal rank instead of rescaled scores · mrr 0.176 · 1.1s

+ cross-encoder rerank0.560

ms-marco-MiniLM over the top 30 · mrr 0.193 · 2.9s

Recall@20 by configuration. The first row is what the previous evaluation actually measured: a corpus indexed by one embedding model and queried by another.

The first row is what the previous evaluation actually measured. Almost the entire improvement comes from the embedding fix; rank fusion and the path branch add a little on top; the cross-encoder buys the top of the list. It would be more flattering to present this as four clever retrieval improvements. It was one configuration bug and three modest engineering fixes, and the honest version is more useful to anyone running a similar stack.

Context pipelineindex → act
01Index

Immutable source versions

02Retrieve

Semantic, lexical, symbol, path, structural

03Fuse

Rank and diversify evidence

04Assemble

Token-bounded context with provenance

05Act

Write and verify the change

Retrieval is one stage of five. A file found at rank 40 is not context.

What we can claim

On Agent Retrieval Bench v2, 75 development cases: Delphi leads every published baseline on MRR and Recall@5, and trails grep on Recall@20. On the held-out split, all 220 positive cases with every corpus provisioned and 0 failed queries, it scores 0.309 MRR, 0.442 Recall@5 and 0.582 Recall@20 at 5.57s mean latency. Those come out slightly ahead of the split the pipeline was tuned on, which is the direction you want: no sign of having fit the tuning set.

The per-workflow spread says more than the average does. Retrieval is close to solved when the query names things that exist in the code, and barely works when it does not.

Held-out, by workflow220 cases
WorkflownMRRR@5R@20
trace2code380.6930.8420.908
edit2ripple440.2740.4340.587
code2test830.2200.3940.586
comment2context550.2070.2450.345

A failure trace hands the retriever real symbols, real file names, real stack frames, and Delphi finds the root-cause file in the top five 76% of the time. A review comment hands it English, something like “this should probably be extracted”, and the same engine manages 17%. The gap between those two rows is not a ranking problem. It is the difference between a query that contains evidence and one that does not, and no amount of fusion tuning closes it.

What does close some of it is giving the embedding something in its own vocabulary to match. A code index is written in code; the question is written in English. Asking a small model to draft the code it thinks the answer looks like, and embedding that alongside the question, moves every metric on the held-out split:

Hypothetical-document expansionheld-out, 220 cases
ConfigurationMRRR@5R@20Latency
Retrieval only0.2280.3490.5522.0s
+ hypothetical document0.2410.3550.5794.1s
+ listwise rerank0.3090.4420.5825.6s

The snippet does not have to be right. It has to be written the way the corpus is written, which is enough to land the query vector in the right neighbourhood. Only the vector branch sees it. BM25, symbol, and path still get the caller's literal words, because inventing terms for an exact-match branch manufactures precision that is not there. Queries already full of identifiers are skipped entirely, and their scores are unchanged, which is the shape you would expect if the mechanism works for the reason claimed.

What we cannot claim is that Delphi is state of the art at the top of the list. On this benchmark it is not: the hosted comparator ranks better at MRR and Recall@5, and we publish that alongside the metrics we do lead. The claim we can defend is narrower. Delphi finds more of the answer set than the comparator, beats every published baseline on early precision, and does it much faster, on your own hardware.

The broader lesson is not about any one engine. A retrieval system can be completely broken and still return ranked, plausible, confident results, and every metric downstream of it will move smoothly and mean nothing. If you run a vector index, embed a chunk's own content and check it against its stored vector. It takes one query and it is the cheapest assertion in the stack.

Open the evidence

Every number above comes from queries that were recorded in full. Below are real traces from the benchmarked build, two per workflow, taken by position in the split rather than picked for how they turned out. Expand one to see the exact query the engine received, the ranked files it returned, which retrieval branch found each one, and the raw response record.

They are worth reading for the failures as much as the hits. Three of the eight miss the gold file entirely. The first code2test trace puts five changelog files above the regression test it was asked for: the query mentions a version bump, changelogs are dense with version strings, and BM25 has no way to know that a changelog can never be an answer to “which test covers this?”. The gold file appears at rank 9, found by the path branch. That is a live weakness, not a rounding error.

Retrieval traces8 queries · expand to inspect

What comes next

The comparison above rests on 60 cases because that is how many the hosted comparator completed. The run against all 220 failed every query on an HTTP error, so the larger paired sample does not exist yet. Re-indexing those corpora into the comparator and running it again is the single thing that would settle MRR, and until it happens we are not going to describe that metric as won or lost.

On our own side the open problem is rank one. Across the held-out split the gold file is inside the top twenty far more often than it is inside the top five, so the candidate is usually retrieved and then not promoted. That is a judgement problem in the reranking stage rather than a coverage problem in retrieval, and widening the pool makes it worse rather than better. Two directions we have not tried: fitting a reranker to this task instead of using an off-the-shelf one, and routing by query shape so that a question asking “which test covers this” is ranked by a different rule than one asking where something is implemented.

Every artifact behind these numbers is in the repository: the corpus lock file, the per-query records, the summaries, and the measured-and-rejected list of everything that did not work.

Footnotes

  1. 1. Intervals are a paired bootstrap over the per-case difference, 4000 resamples, 95% percentile interval. We treat a difference as real only when the interval excludes zero. An earlier version of this page reported the comparison at 135 cases with a Agent Retrieval Bench v2 configuration that had query expansion and listwise reranking disabled, which are the two stages that order the head of the list. Those numbers described a build we do not ship and have been replaced.

References

  1. 1. Agent Retrieval Bench, commit d04953371d96.
  2. 2. Cormack, Clarke, and Buettcher, “Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods”, SIGIR 2009.
  3. 3. Synthetic Sciences, Delphi source and implementation documentation.