La récupération contextuelle
Sur cette page
Quand on découpe un document, chaque morceau perd son lien avec le reste. « Le chiffre d’affaires a progressé de 3 % par rapport au trimestre précédent » : de quelle entreprise, et de quel trimestre ? Ni l’embedding ni BM25 ne le savent, et la recherche manque le morceau. La récupération contextuelle (Contextual Retrieval), publiée par Anthropic, répare cette perte : avant l’indexation, elle ajoute à chaque morceau une courte phrase qui le situe dans son document.
Le principe
Pour chaque morceau, on donne à Claude le document entier et le morceau, et on lui demande de situer le morceau dans le document. Sa réponse, de 50 à 100 jetons d’après Anthropic, est placée devant le morceau. Le tout est ensuite indexé dans l’index vectoriel (embeddings contextuels) et dans l’index BM25 (BM25 contextuel).
L’exemple de l’article, traduit : le morceau « Le chiffre d’affaires de l’entreprise a progressé de 3 % par rapport au trimestre précédent » devient « Ce passage provient d’un rapport déposé auprès de la SEC sur les résultats d’ACME Corp au deuxième trimestre 2023 ; le chiffre d’affaires du trimestre précédent était de 314 millions de dollars. Le chiffre d’affaires de l’entreprise a progressé de 3 % par rapport au trimestre précédent. »
Les résultats publiés
| Configuration | Échecs de récupération | Réduction |
|---|---|---|
| Embeddings classiques | 5,7 % | |
| Embeddings contextuels | 3,7 % | 35 % |
| Embeddings et BM25 contextuels | 2,9 % | 49 % |
| Les mêmes, avec reclassement | 1,9 % | 67 % |
Ces mesures, publiées par Anthropic en septembre 2024, portent sur plusieurs domaines (code, fiction, articles scientifiques). Un échec est un document pertinent absent des 20 premiers morceaux renvoyés. Anthropic retient aussi que 20 morceaux dans le prompt donnaient de meilleurs résultats que 5 ou 10.
Une implémentation économique, avec la mise en cache
Le document entier accompagne chaque morceau : sans précaution, un document de 100 morceaux serait facturé 100 fois. La mise en cache des prompts (leçons 46 à 48) règle ce problème. On place le document dans un bloc marqué cache_control : le premier appel l’écrit dans le cache, les suivants le relisent à une fraction du prix normal.
def situate_chunk(document, chunk):
response = client.messages.create(
model="claude-haiku-5-5",
max_tokens=4096,
messages=[{
"role": "user",
"content": [
{
"type": "text",
"text": f"<document>\n{document}\n</document>",
"cache_control": {"type": "ephemeral"},
},
{
"type": "text",
"text": (
f"Voici le passage à situer dans ce document :\n<chunk>\n{chunk}\n</chunk>\n\n"
"Écris un contexte court et précis qui situe ce passage dans l'ensemble du document, "
"afin d'améliorer sa récupération lors d'une recherche. "
"Réponds uniquement par ce contexte."
),
},
],
}],
)
context = "".join(block.text for block in response.content if block.type == "text")
return f"{context}\n\n{chunk}"
for chunk in chunks:
retriever.add_document({"content": situate_chunk(text, chunk)})
Quelques repères tirés de la documentation :
- le préfixe mis en cache doit compter au moins 512 jetons sur les modèles actuels, dont Claude Haiku 5.5 ; en dessous, la requête passe sans cache, et sans erreur ;
- le cache vit 5 minutes, et chaque lecture le prolonge : une boucle sur les morceaux d’un document le réutilise d’un appel à l’autre ;
- l’écriture coûte 1,25 fois le prix d’entrée, une lecture 0,1 fois sur Claude Haiku 5.5 ;
- le champ
usage.cache_read_input_tokensde la réponse montre ce qui a été lu dans le cache.
Le prompt est une adaptation française de celui que publie Anthropic. L’article note qu’un prompt adapté à votre domaine, par exemple avec un glossaire des termes définis ailleurs dans la base, peut faire mieux.
Les documents trop longs
Si le document est trop long, ou trop coûteux à joindre en entier, le cours officiel propose de n’envoyer qu’un extrait : les premiers morceaux, qui contiennent souvent l’introduction ou un résumé, et les morceaux qui précèdent immédiatement celui que l’on situe.
def local_context(chunks, i, num_start=2, num_prev=2):
start = chunks[:num_start]
previous = chunks[max(num_start, i - num_prev):i]
return "\n\n".join(start + previous)
for i, chunk in enumerate(chunks):
retriever.add_document({"content": situate_chunk(local_context(chunks, i), chunk)})
Le contexte change alors d’un morceau à l’autre : la mise en cache n’aide plus guère, et chaque appel est facturé au prix normal.
À vous de jouer
Mesurer le gain. Prenez un long document, découpez-le, et construisez deux Retriever : l’un avec les morceaux bruts, l’autre avec les morceaux situés par situate_chunk. Sur dix questions dont vous connaissez la réponse, comptez combien de fois le bon passage figure dans les cinq premiers résultats. Vérifiez au passage, dans usage, que le document est bien lu depuis le cache.
Sources officielles : Contextual Retrieval (Anthropic : méthode, résultats, coût) · Prompt caching (documentation Claude : seuils, durée, tarifs)
Points clés à retenir
- Un morceau isolé perd son contexte ; la récupération contextuelle lui ajoute une phrase qui le situe.
- Le contexte est ajouté avant l’indexation, dans l’index vectoriel comme dans BM25.
- Résultats publiés par Anthropic : 35 % d’échecs en moins, 49 % avec BM25, 67 % avec reclassement.
- La mise en cache du document rend la préparation abordable, au-delà de 512 jetons sur les modèles actuels.