ai newsDo., 15. Okt.
EilmeldungMeta and Apple team up to make Muse AI models run better on Macs

Contextual Retrieval mit 80 Zeilen Code: Anthropics Chunk-Prefix-Trick im Praxischeck

Ein Praxistest zeigt, wie sich Anthropics Ansatz zum kontextualisierten Chunking fuer RAG-Pipelines mit wenigen Zeilen selbst implementieren laesst. Gemessen wurde der Effekt auf eigenen Dokumenten.

dev.toDonnerstag 11:01 · 15.10.DE12 Min. Lesezeit
Bild: dev.to

Hier ist ein Ausschnitt aus einem Quartalsbericht: „Der Umsatz des Unternehmens wuchs im Vergleich zum Vorquartal um 3 %.“ Welches Unternehmen? Welches Quartal? Gewachsen wovon? Innerhalb des Berichts ergab der Satz Sinn. Herausgeschnitten, eingebettet und eigenständig indexiert trägt er nichts davon mehr, und eine Anfrage nach dem Q2-2023-Umsatz von ACME findet fast nichts, woran sie anknüpfen könnte.

Das Beispiel stammt aus Anthropics Ausarbeitung zu Contextual Retrieval, veröffentlicht am 19. September 2024. Die vorgeschlagene Lösung ist klein. Bevor man einen Chunk einbettet oder für BM25 indexiert, bittet man ein Modell, anhand des gesamten Dokuments einen kurzen Kontext dafür zu schreiben, und setzt diesen Kontext an den Anfang des Chunks. Der Chunk lautet dann: „Dieser Chunk stammt aus einem SEC-Filing zur Q2-2023-Performance von ACME corp; der Umsatz des Vorquartals lag bei 314 Millionen Dollar. Der Umsatz des Unternehmens wuchs im Vergleich zum Vorquartal um 3 %.“

Auf ihren Testmengen senkten contextual embeddings plus contextual BM25 die Top-20-Retrieval-Fehlerrate um 49 Prozent, von 5,7 % auf 2,9 %. Ein zusätzlicher Reranker brachte die Reduktion auf 67 %, also auf 1,9 %. Das sind ihre Zahlen auf ihren Daten. Dieser Beitrag liefert Ihnen Ihre eigenen: etwa 80 Zeilen Python, die Ihre Dokumente auf beide Weisen indexieren, jeden Chunk mithilfe von Prompt Caching kontextualisieren, sodass jedes Dokument nur einmal bezahlt wird, und recall@5 vor und nachher auf 30 Fragen ausgeben, die Sie selbst beschriften.

Die Kennzahl in der Ausarbeitung ist 1 minus recall@20: der Anteil relevanter Chunks, die nicht in den Top-20-Ergebnissen auftauchten. Die Testdaten umfassten Codebasen, Belletristik, ArXiv-Papiere und Wissenschaftspapiere. Die zentralen Zahlen verwendeten Gemini Text 004 Embeddings und Claude 3 Haiku zum Schreiben der Kontexte, die üblicherweise 50 bis 100 Token lang waren (gleiche Quelle).

Das begleitende Cookbook-Notebook berichtet bei k=5, näher an dem, was dieser Beitrag misst. Sein protokollierter Lauf zeigt Pass@5, das von 80,92 % auf 88,12 % steigt, allein mit contextual embeddings. Die Zusammenfassungstabelle und der Text desselben Notebooks stimmen nicht vollständig mit seiner protokollierten Ausgabe überein, was ein weiterer Grund ist, jede veröffentlichte Zahl nur als Hinweis auf die Richtung zu betrachten.

Nichts davon sagt Ihnen, ob Ihre Dokumente das Problem haben. Chunks, die auf ihre Umgebung angewiesen sind („der oben genannte Endpunkt“, „diese Einstellung“, eine Tabelle, deren Überschrift zwei Chunks weiter hinten liegt), haben am meisten zu gewinnen. Ein Ordner mit in sich geschlossenen FAQ-Einträgen bewegt sich womöglich kaum. Das finden Sie durch Messen heraus.

Anthropics Ausarbeitung von 2024 schlägt außerdem vor, dass Sie bei einer Wissensbasis von unter 200.000 Token alles in den Prompt legen und auf Retrieval verzichten.

Der ganze Trick: Der Chunk behält eine kurze Notiz darüber, woher er stammt.

Installieren Sie die vier Abhängigkeiten und setzen Sie ANTHROPIC_API_KEY und OPENAI_API_KEY:

pip install anthropic openai rank-bm25 numpy Das Skript verwendet das Anthropic Python SDK für die Kontexte, das OpenAI Python SDK für Embeddings, rank-bm25 für die Schlüsselwortsuche und NumPy für die Vektormathematik. Speichern Sie es als ctx_eval.py:

import json, re, sys import numpy as np from anthropic import Anthropic from openai import OpenAI from rank_bm25 import BM25Okapi claude, oai = Anthropic (), OpenAI () usage = { " written ": 0, " read ": 0, " uncached ": 0 } PROMPT = ( " Here is the chunk we want to situate within the whole " " document \n <chunk> \n {chunk} \n </chunk> \n " " Please give a short succinct context to situate this " " chunk within the overall document for the purposes of " " improving search retrieval of the chunk. Answer only " " with the succinct context and nothing else. " ) def chunk ( text, size = 500 ): words = text. split () return [ " ". join ( words [ i: i + size ]) for i in range ( 0, len ( words ), size )] def situate ( doc, piece ): resp = claude. messages. create ( model = " claude-haiku-5-5 ", max_tokens = 300, thinking = { " type ": " disabled " }, messages = [{ " role ": " user ", " content ": [ { " type ": " text ", " text ": f " <document> \n { doc } \n </document> ", " cache_control ": { " type ": " ephemeral " }}, { " type ": " text ", " text ": PROMPT. format ( chunk = piece )}, ]}], ) u = resp. usage usage [ " written " ] += u. cache_creation_input_tokens or 0 usage [ " read " ] += u. cache_read_input_tokens or 0 usage [ " uncached " ] += u. input_tokens return resp. content [ 0 ]. text. strip () def embed ( texts ): vecs = [] for i in range ( 0, len ( texts ), 100 ): r = oai. embeddings. create ( model = " text-embedding-3-small ", input = texts [ i: i + 100 ]) vecs += [ d. embedding for d in r. data ] return np. array ( vecs ) def toks ( s ): return re. findall ( r " \w+ ", s. lower ()) class Index: def __init__ ( self, texts ): self. vecs = embed ( texts ) self. bm25 = BM25Okapi ([ toks ( t ) for t in texts ]) def search ( self, q, k = 5, pool = 20 ): dense = np. argsort ( - ( self. vecs @ embed ([ q ])[ 0 ]))[: pool ] bm = self. bm25. get_scores ( toks ( q )) sparse = [ i for i in np. argsort ( - bm )[: pool ] if bm [ i ] > 0 ] fused = {} for ranking in ( dense, sparse ): for rank, i in enumerate ( ranking, 1 ): fused [ i ] = fused. get ( i, 0 ) + 1 / ( 60 + rank ) return sorted ( fused, key = fused. get, reverse = True )[: k ] def build ( docs, contextual ): ids, texts = [], [] for name, doc in docs. items (): for n, piece in enumerate ( chunk ( doc )): if contextual: piece = situate ( doc, piece ) + " \n\n " + piece ids. append ( f " { name } # { n } " ) texts. append ( piece ) return ids, Index ( texts ) def recall ( ids, index, qa, k = 5 ): def hit ( item ): gold = set ( item [ " gold " ]) top = { ids [ i ] for i in index. search ( item [ " q " ], k )} return len ( top & gold ) / len ( gold ) return sum ( hit ( x ) for x in qa ) / len ( qa ) if __name__ == " __main__ ": qa = json. load ( open ( sys. argv [ 1 ])) docs = { p: open ( p ). read () for p in sys. argv [ 2:]} for contextual in ( False, True ): ids, index = build ( docs, contextual ) r = recall ( ids, index, qa ) print ( f " contextual= { contextual } recall@5= { r:.

1 % } " ) print ( " prompt tokens: ", usage ) Starten Sie es mit python ctx_eval.py qa.json docs/*.md. Hier ist, was jeder Teil macht.

chunk teilt in feste 500-Wort-Fenster. Tauschen Sie Ihren produktiven Chunker ein; beide Läufe müssen nur denselben verwenden, weil die Gold-Labels auf Chunk-Positionen verweisen.

situate sendet Anthropics Kontextualisierungs-Prompt, kopiert aus dem Cookbook. Das Dokument geht in den ersten Content-Block mit cache_control, und der Chunk geht in einen zweiten Block, der nicht gecacht wird. Diese Reihenfolge ist wichtig: Der Cache deckt das Präfix bis zum markierten Block ab, daher muss das Dokument zuerst kommen. Das Modell ist Claude Haiku 5.5, bei dem thinking standardmäßig aktiviert ist. Die Thinking-Dokumentation besagt, dass Haiku 5.5 {"type": "disabled"} bei high effort oder darunter akzeptiert, und die Modelle-Übersicht listet seinen Default-Effort als medium. Das Schreiben eines einzigen Kontextsatzes braucht keine als Output abgerechneten Reasoning-Tokens.

Index.search betreibt beide Retriever. Die Dense-Seite ist ein einfaches Punktprodukt, das als Kosinusähnlichkeit funktioniert, weil OpenAI-Embeddings auf die Länge 1 normiert sind. Die BM25-Seite konvertiert selbst in Kleinbuchstaben und splittet an Wortzeichen, da rank-bm25 kein eigenes Preprocessing durchführt. Chunks mit einem BM25-Score von null werden verworfen, damit sie nicht in die Fusion einschleichen. Anthropics Ausarbeitung sagt, die beiden Ergebnismengen mit „rank fusion techniques“ zu verschmelzen, ohne eine zu nennen; daher verwendet dieser Code reciprocal rank fusion mit der Konstante k=60, die Elasticsearch standardmäßig nutzt.

build ist der Ort, an dem das Experiment lebt. Der contextual-Flag ändert den Text, der eingebettet und indexiert wird, und sonst nichts. Die Chunk-IDs bleiben in beiden Läufen gleich, sodass eine Label-Datei beide bewertet.

recall mittelt über alle Fragen den Anteil der Gold-Chunks, die in den Top 5 landen. Bei einer Frage mit einem Gold-Chunk ist das Treffer oder Fehlschlag.

Ohne Caching sendet jeder Chunk-Aufruf das gesamte Dokument erneut mit. Zehn Chunks aus einem Dokument bedeuten, dass man dieses Dokument zehnmal als Input bezahlt. Mit cache_control auf dem Dokumentblock schreibt der erste Aufruf es in den Cache, und die nächsten neun lesen es zurück.

Die Prompt-Caching-Dokumentation preist einen 5-Minuten-Cache-Schreibvorgang mit dem 1,25-fachen des Basis-Inputs und einen Cache-Lesevorgang mit dem 0,1-fachen. Auf der Preisseite Stand Oktober 2026 kostet Claude Haiku 5.5 für Prompts bis 100.000 Token 0,10 Dollar pro Million Input-Token, 0,125 Dollar für 5-Minuten-Cache-Schreibvorgänge, 0,01 Dollar für Cache-Treffer und 0,50 Dollar für Output. Prompts über 100.000 Token wechseln zu einem fünfmal höheren Satz, daher kosten sehr lange Dokumente mehr pro Chunk.

Einige Rechnungen dazu, in der Form, die Anthropic für seine eigene Schätzung verwendete: ein 8.000-Token-Dokument, zehn 800-Token-Chunks, etwa 50 Token Anweisungen und 100 Token generierter Kontext pro Chunk. Mit Caching sind das grob 0,001 Dollar für den einen Cache-Schreibvorgang, 0,0007 Dollar für neun Cache-Lesevorgänge, 0,0009 Dollar für den nicht gecachten Chunk-Text und 0,0005 Dollar für Output. Etwa 0,003 Dollar pro Dokument, also rund 0,38 Dollar pro Million Dokument-Token. Ohne Caching kostet derselbe Auftrag grob 1,17 Dollar pro Million. Das ist meine Rechnung auf Basis der Listenpreise, keine gemessene Abrechnung. Anthropics Zahl von 2024 für dieselbe Konstellation lag bei 1,02 Dollar pro Million Dokument-Token, auf Basis der Claude 3 Haiku Preise.

Einmal schreiben, viele guenstige Lesezugriffe: Das Dokument wird zwischengespeichert und jeder Chunk-Aufruf verwendet es erneut.

Drei Details aus der Caching-Dokumentation entscheiden darueber, ob Sie diese Lesezugriffe tatsaechlich erhalten:

Die Zeile mit den Prompt-Tokens am Ende verraet Ihnen, wie es in Ihrem Durchlauf gelaufen ist. Zum Vergleich: Die Aufnahme von 737 Chunks im Cookbook selbst meldete, dass 61,83 Prozent aller Eingabe-Tokens aus dem Cache gelesen wurden, weniger als die 70 bis 80 Prozent, die der Fliesstext suggeriert. Lesen Sie Ihre Zaehler, bevor Sie der Schaetzung irgendjemandes vertrauen, auch der obigen.

Die Zahl, die dieses Skript ausgibt, ist nur so ehrlich wie die Datei qa.json. Um etwas zu labeln, muessen Sie zunaechst die Chunk-IDs sehen. Ein Helfer, der denselben Chunker wiederverwendet:

import sys from ctx_eval import chunk for path in sys. argv [ 1:]: for n, piece in enumerate ( chunk ( open ( path ). read ())): print ( f " { path } # { n } \t { piece [: 90 ] } " ) Enter fullscreen mode Exit fullscreen mode Das Importieren von ctx_eval erzeugt beide API-Clients, lassen Sie also die Umgebungsvariablen gesetzt. Schreiben Sie dann die Label-Datei:

[ { "q": "How long do refunds take on annual plans?", "gold": [ "docs/billing.md#3" ]}, { "q": "Which header carries the idempotency key?", "gold": [ "docs/api.md#7", "docs/api.md#8" ]} ] Enter fullscreen mode Exit fullscreen mode Einige Regeln verhindern, dass der Satz einen der beiden Durchlaeufe schmeichelt:

Bei 30 Fragen ist jede einzelne etwa 3,3 Punkte von recall@5 wert. Eine Differenz von ein bis zwei Fragen zwischen den Durchlaeufen liegt innerhalb des Rauschens. Schauen Sie sich daher neben den Summen auch die Wechsel an. Innerhalb des __main__-Blocks gibt das Skript nach jedem Aufbau einen Treffer oder Misserfolg pro Frage aus:

for item in qa: top = { ids [ i ] for i in index. search ( item [ " q " ])} print ( bool ( top & set ( item [ " gold " ])), item [ " q " ][: 60 ]) Enter fullscreen mode Exit fullscreen mode Fragen, die vom Misserfolg zum Treffer wechseln, zeigen Ihnen, wo der Praefix geholfen hat. Fragen, die vom Treffer zum Misserfolg wechseln, verdienen eine genaue Lektuere: Wenn jeder Chunk eines Dokuments einen aehnlichen Einstiegssatz erhaelt, koennen diese Chunks fuer den Retriever aehnlich aussehen.

Bewegt sich recall@5 kaum, ist das ein echtes Ergebnis. Ihre Chunks sind moeglicherweise bereits in sich abgeschlossen, oder Ihre Baseline ist bereits hoch, und Sie haben sich einfach eine Indexierungsschritte bei jeder Dokumentaktualisierung gespart.

Bewegt sich der Wert, nennt Anthropics Ausfuehrung die naechsten Hebel. Sie haben durch das Abrufen der Top 150 und das Verengen auf 20 mit einem Cohere-Reranker neu sortiert, daher stammt die Zahl von 67 Prozent. Sie haben uebergeben von 5, 10 und 20 Chunks getestet und fanden, dass 20 fuer sie am besten funktionierte, wiesen die Leser aber gleichzeitig an, dies an ihrem eigenen Anwendungsfall zu pruefen. Sie empfehlen zudem einen contextualizer prompt, der auf Ihre Domain zugeschnitten ist, etwa einen, der ein Glossar von Begriffen enthaelt, die an anderer Stelle der Knowledge Base definiert sind. Aendern Sie jeweils nur einen Hebel und fuehren Sie dieselben eingefrorenen 30 Fragen erneut aus.

Behalten Sie die Indexierungskosten im Blick, sobald dies ueber ein Experiment hinausgeht. Kontexte werden zur Indexierungszeit aus dem gesamten Dokument erzeugt, sodass sich bei einer Aenderung eines Dokuments der Kontext seiner Chunks veralten kann. Kontextualisieren Sie das gesamte Dokument neu, nicht nur den bearbeiteten Chunk.

Das Skript ist das Messinstrument; die Entscheidungen drumherum, wie Sie chunking betreiben, wann hybride Suche besser ist als rein dichte Suche und wann sich ein Reranker seine Latenzzeit verdient, behandelt der RAG Pocket Guide unter seinem Untertitel ueber Retrieval-, Chunking- und Reranking-Muster fuer die Produktion. Fuehren Sie zuerst die 30 Fragen aus und waehlen Sie dann das Kapitel, das zu den Misserfolgen passt.

Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.

Original (Englisch) anzeigen

Here is a chunk from a quarterly filing: "The company's revenue grew by 3% over the previous quarter." Which company? Which quarter? Grew from what? Inside the filing, the sentence made sense. Cut out, embedded and indexed on its own, it carries none of that, and a query about ACME's Q2 2023 revenue has almost nothing to match against.

That example comes from Anthropic's Contextual Retrieval write-up, published September 19, 2024. The fix they proposed is small. Before you embed a chunk or index it for BM25, ask a model to write a short context for it using the whole document, and glue that context to the front of the chunk. Their chunk becomes: "This chunk is from an SEC filing on ACME corp's performance in Q2 2023; the previous quarter's revenue was $314 million. The company's revenue grew by 3% over the previous quarter."

On their test sets, contextual embeddings plus contextual BM25 cut the top-20 retrieval failure rate by 49%, from 5.7% to 2.9%. Adding a reranker on top took the reduction to 67%, down to 1.9%. Those are their numbers on their data. This post gets you yours: about 80 lines of Python that index your docs both ways, contextualize every chunk with prompt caching so each document is paid for once, and print recall@5 before and after on 30 questions you label yourself.

The metric in the write-up is 1 minus recall@20: the share of relevant chunks that did not show up in the top 20 results. The test data covered codebases, fiction, ArXiv papers and science papers. The headline figures used Gemini Text 004 embeddings and Claude 3 Haiku to write the contexts, which usually came out at 50 to 100 tokens ( same source ).

The companion cookbook notebook reports at k=5, closer to what this post measures. Its printed run shows Pass@5 going from 80.92% to 88.12% with contextual embeddings alone. The same notebook's summary table and prose don't fully agree with its printed output, which is one more reason to treat any published number as a hint about direction.

None of this tells you whether your docs have the problem. Chunks that lean on their surroundings ("the endpoint above", "this setting", a table whose header lives two chunks back) have the most to gain. A folder of self-contained FAQ entries may barely move. You find out by measuring.

Anthropic's 2024 write-up also suggests that if your whole knowledge base is under 200,000 tokens, you put all of it in the prompt and skip retrieval.

The whole trick: the chunk keeps a short note about where it came from.

Install the four dependencies and set ANTHROPIC_API_KEY and OPENAI_API_KEY:

pip install anthropic openai rank-bm25 numpy Enter fullscreen mode Exit fullscreen mode The script uses the Anthropic Python SDK for the contexts, the OpenAI Python SDK for embeddings, rank-bm25 for keyword search and NumPy for the vector math. Save it as ctx_eval.py:

import json, re, sys import numpy as np from anthropic import Anthropic from openai import OpenAI from rank_bm25 import BM25Okapi claude, oai = Anthropic (), OpenAI () usage = { " written ": 0, " read ": 0, " uncached ": 0 } PROMPT = ( " Here is the chunk we want to situate within the whole " " document \n <chunk> \n {chunk} \n </chunk> \n " " Please give a short succinct context to situate this " " chunk within the overall document for the purposes of " " improving search retrieval of the chunk. Answer only " " with the succinct context and nothing else. " ) def chunk ( text, size = 500 ): words = text. split () return [ " ". join ( words [ i: i + size ]) for i in range ( 0, len ( words ), size )] def situate ( doc, piece ): resp = claude. messages. create ( model = " claude-haiku-5-5 ", max_tokens = 300, thinking = { " type ": " disabled " }, messages = [{ " role ": " user ", " content ": [ { " type ": " text ", " text ": f " <document> \n { doc } \n </document> ", " cache_control ": { " type ": " ephemeral " }}, { " type ": " text ", " text ": PROMPT. format ( chunk = piece )}, ]}], ) u = resp. usage usage [ " written " ] += u. cache_creation_input_tokens or 0 usage [ " read " ] += u. cache_read_input_tokens or 0 usage [ " uncached " ] += u. input_tokens return resp. content [ 0 ]. text. strip () def embed ( texts ): vecs = [] for i in range ( 0, len ( texts ), 100 ): r = oai. embeddings. create ( model = " text-embedding-3-small ", input = texts [ i: i + 100 ]) vecs += [ d. embedding for d in r. data ] return np. array ( vecs ) def toks ( s ): return re. findall ( r " \w+ ", s. lower ()) class Index: def __init__ ( self, texts ): self. vecs = embed ( texts ) self. bm25 = BM25Okapi ([ toks ( t ) for t in texts ]) def search ( self, q, k = 5, pool = 20 ): dense = np. argsort ( - ( self. vecs @ embed ([ q ])[ 0 ]))[: pool ] bm = self. bm25. get_scores ( toks ( q )) sparse = [ i for i in np. argsort ( - bm )[: pool ] if bm [ i ] > 0 ] fused = {} for ranking in ( dense, sparse ): for rank, i in enumerate ( ranking, 1 ): fused [ i ] = fused. get ( i, 0 ) + 1 / ( 60 + rank ) return sorted ( fused, key = fused. get, reverse = True )[: k ] def build ( docs, contextual ): ids, texts = [], [] for name, doc in docs. items (): for n, piece in enumerate ( chunk ( doc )): if contextual: piece = situate ( doc, piece ) + " \n\n " + piece ids. append ( f " { name } # { n } " ) texts. append ( piece ) return ids, Index ( texts ) def recall ( ids, index, qa, k = 5 ): def hit ( item ): gold = set ( item [ " gold " ]) top = { ids [ i ] for i in index. search ( item [ " q " ], k )} return len ( top & gold ) / len ( gold ) return sum ( hit ( x ) for x in qa ) / len ( qa ) if __name__ == " __main__ ": qa = json. load ( open ( sys. argv [ 1 ])) docs = { p: open ( p ). read () for p in sys. argv [ 2:]} for contextual in ( False, True ): ids, index = build ( docs, contextual ) r = recall ( ids, index, qa ) print ( f " contextual= { contextual } recall@5= { r:.

1 % } " ) print ( " prompt tokens: ", usage ) Enter fullscreen mode Exit fullscreen mode Run it as python ctx_eval.py qa.json docs/*.md. Here is what each part is doing.

chunk splits on fixed 500-word windows. Swap in your production chunker; both runs just have to use the same one, because the gold labels point at chunk positions.

situate sends Anthropic's contextualizer prompt, copied from the cookbook. The document goes in the first content block with cache_control, and the chunk goes in a second block that is not cached. That ordering matters: the cache covers the prefix up to the marked block, so the document has to come first. The model is Claude Haiku 5.5, whose thinking is on by default. The thinking docs say Haiku 5.5 accepts {"type": "disabled"} at high effort or below, and the models overview lists its default effort as medium. Writing one sentence of context does not need reasoning tokens billed as output.

Index.search runs both retrievers. The dense side is a plain dot product, which works as cosine similarity because OpenAI embeddings are normalized to length 1. The BM25 side lowercases and splits on word characters itself, since rank-bm25 does no preprocessing of its own. Chunks with a zero BM25 score are dropped so they don't sneak into the fusion. Anthropic's write-up says to merge the two result sets with "rank fusion techniques" without naming one, so this uses reciprocal rank fusion with the k=60 constant Elasticsearch defaults to.

build is where the experiment lives. The contextual flag changes the text that gets embedded and indexed, and nothing else. Chunk IDs stay the same in both runs, so one label file scores both.

recall averages, over all questions, the fraction of gold chunks that land in the top 5. For a question with one gold chunk, that is hit or miss.

Without caching, every chunk call re-sends the entire document. Ten chunks from one document means paying for that document ten times as input. With cache_control on the document block, the first call writes it to the cache and the next nine read it back.

The prompt caching docs price a 5-minute cache write at 1.25 times base input and a cache read at 0.1 times. On the pricing page as of October 2026, Claude Haiku 5.5 for prompts up to 100,000 tokens is $0.10 per million input tokens, $0.125 for 5-minute cache writes, $0.01 for cache hits and $0.50 for output. Prompts over 100,000 tokens move to a rate five times higher, so very long documents cost more per chunk.

Some arithmetic from that table, using the shape Anthropic used for its own estimate: an 8,000-token document, ten 800-token chunks, about 50 tokens of instructions and 100 tokens of generated context per chunk. With caching, that is roughly $0.001 for the one cache write, $0.0007 for nine cache reads, $0.0009 for the uncached chunk text and $0.0005 for output. About $0.003 per document, or around $0.38 per million document tokens. Without caching, the same job comes to roughly $1.17 per million. That is my arithmetic from list prices, not a measured bill. Anthropic's 2024 figure for the same shape was $1.02 per million document tokens, on Claude 3 Haiku pricing.

One write, many cheap reads: the document is cached and each chunk call reuses it.

Three details from the caching docs decide whether you actually get those reads:

The prompt tokens line at the end tells you how it went on your run. For comparison, the cookbook's own 737-chunk ingestion reported that 61.83% of all input tokens were read from cache, lower than the 70 to 80% its prose suggests. Read your counters before you trust anyone's estimate, including the one above.

The number this script prints is only as honest as qa.json. To label anything, you first need to see the chunk IDs. A helper that reuses the same chunker:

import sys from ctx_eval import chunk for path in sys. argv [ 1:]: for n, piece in enumerate ( chunk ( open ( path ). read ())): print ( f " { path } # { n } \t { piece [: 90 ] } " ) Enter fullscreen mode Exit fullscreen mode Importing ctx_eval creates both API clients, so keep the environment variables set. Then write the label file:

[ { "q": "How long do refunds take on annual plans?", "gold": [ "docs/billing.md#3" ]}, { "q": "Which header carries the idempotency key?", "gold": [ "docs/api.md#7", "docs/api.md#8" ]} ] Enter fullscreen mode Exit fullscreen mode A few rules keep the set from flattering either run:

With 30 questions, each one is worth about 3.3 points of recall@5. A gap of one or two questions between runs is within noise. So look at the flips as well as the totals. Inside the __main__ block, after each build, this prints a hit or miss per question:

for item in qa: top = { ids [ i ] for i in index. search ( item [ " q " ])} print ( bool ( top & set ( item [ " gold " ])), item [ " q " ][: 60 ]) Enter fullscreen mode Exit fullscreen mode Questions that went from miss to hit tell you where the prefix helped. Questions that went from hit to miss deserve a closer read: when every chunk of one document gets a similar opening sentence, those chunks may start to look alike to the retriever.

If recall@5 barely moves, that is a real result. Your chunks may already be self-contained, or your baseline may already be high, and you just saved yourself an indexing step on every document update.

If it moves, Anthropic's write-up lists the next levers. They reranked by pulling the top 150 and narrowing to 20 with a Cohere reranker, which is where the 67% figure came from. They tried passing 5, 10 and 20 chunks and found 20 worked best for them, while telling readers to test it on their own use case. They also suggest a contextualizer prompt tailored to your domain, such as one that includes a glossary of terms defined elsewhere in the knowledge base. Change one lever at a time and rerun the same frozen 30 questions.

Keep the indexing cost in mind once this goes past an experiment. Contexts are generated at index time from the whole document, so when a document changes, any of its chunks' contexts can go stale. Re-contextualize the whole document, not only the edited chunk.

The script is the measuring stick; the decisions around it (how you chunk, when hybrid search beats dense alone, when a reranker earns its latency) are what the RAG Pocket Guide covers, under its subtitle of retrieval, chunking and reranking patterns for production. Run the 30 questions first, then pick the chapter that matches the misses.

Originaltitel: Contextual Retrieval in 80 Lines: Anthropic's Chunk-Prefix Trick, Measured on Your DocsQuelle: dev.to (dev.to). Original öffnen