Classic RAG pushes the top-k chunks at the model before it has thought about the question. Claude Code works the other way round: a small index is always in context, and the model pulls exact content on demand with tools. ContextPull packages that pattern for any document corpus as an MCP server, and measures it honestly against the pipeline it replaces.
A comparison question, the shape naive RAG fails hardest on. Press play, or step through. Amber marks a decision the model makes itself.
What changed in the refund window between the 2024 and 2025 policy?
[0.12, -0.44, 0.08, …]MEMORY.md in Claude Code.searchsearch("refund window", in: ["policy-2024.md", "policy-2025.md"]) → two section IDs, with headings.read twice[policy-2024.md#3] [policy-2025.md#3]”The refund policy corpus is an illustrative example. Measured numbers, including two pull rows, are further down.
Ingest once into a store with stable section IDs. Emit a compact index that lives in context. Expose four tools. The client can be Claude Code or any MCP host.
| Push · classic RAG | Pull · ContextPull | |
|---|---|---|
| Who decides what is retrieved | The pipeline, from one embedding of the raw question | The model, after reading the index and thinking about the question |
| What the model sees | k chunks it did not choose, relevant or not | Only sections it asked for, verbatim, with IDs |
| Comparison and aggregation questions | One blended query returns a mush of both documents, or misses a source entirely | Several targeted reads, then reasoning across them |
| Exact lookups | Embeddings blur identifiers; needs a separate BM25 path | grep is a first-class tool |
| Tables | Chunking cuts the row from its header | neighbours returns the header with the row |
| Citations | Post-hoc, often approximate | The section ID the model read |
| Cost | Predictable: one embed, one prompt | Variable: more calls, more latency, more tokens on hard questions |
| Failure mode | Silent. The answer is fluent from the wrong chunks | Visible. The trace shows what was read, and the model can say it found nothing |
| Where it can still fail | Retrieval | Retrieval. A bad index or a weak search still misses. The loop fixes ranking, not recall |
We already have the measuring instrument. ragbisect builds an eval set from the corpus, scores each retrieval stage on its own, and prints what a run spent. An agentic retriever fits its one-method adapter: run the loop, return the section IDs the model read.
| config | recall@5 | mrr@5 | ndcg@5 given hit | tokens / query |
|---|---|---|---|---|
| bm25 | 0.923 | 0.805 | 0.905 | 0 |
| dense, text-embedding-3-small | 0.851 | 0.738 | 0.901 | ≈ 15 |
| hybrid dense + bm25, RRF | 0.964 | 0.821 | 0.889 | ≈ 15 |
| ContextPull pull, Claude Code (10-question sample) | 1.000 | 0.883 | 0.913 | ≈ 86,700 |
| ContextPull pull, gpt-5.4-mini reasoning off | 0.615 | 0.585 | 0.964 | ≈ 10,900 |
Markdown, text, then PDF. Heading-aware sectioning with stable IDs, heading paths, neighbours, a BM25 index. Reuse the ragbisect chunker and corpus code.
Five tools: index, search, read, grep, neighbours. Stdio transport first. Ships as contextpull on PyPI and npm. Works in Claude Code with one config line.
A ragbisect adapter that runs the tool loop against ContextPull and returns the section IDs read. Fill the blank row. Include tokens and latency per query.
All five question shapes, several corpora, both pipelines. Where pull does not help, say so. That is the whole positioning.
search tool makes the whole loop weak. The store still has to be good.