écrits/tutorial/2026/05
Tutorial26 mai 2026·28 min

DSPy 3 : construire des pipelines LLM auto-optimisants en Python

Un guide pratique de DSPy 3, le framework né à Stanford pour programmer les modèles de langage de manière déclarative. Construisez, évaluez et optimisez automatiquement un pipeline RAG en Python sans rédiger un seul prompt à la main.

Introduction

La plupart des équipes livrent leurs fonctionnalités LLM de la même façon : un prompt géant, quelques exemples collés, et l'espoir que cela généralise. Quand le modèle change ou que la tâche dérive, tout doit être réécrit à la main. DSPy inverse cette logique. Au lieu d'écrire des prompts, vous définissez des signatures et des modules que le framework compile ensuite en prompts optimisés et en démonstrations few-shot, en fonction d'une métrique que vous spécifiez.

DSPy 3, publié par Stanford NLP début 2026, intègre un optimiseur plus rapide (MIPROv2), un support natif de l'asynchrone, le streaming, et des adaptateurs pour OpenAI, Anthropic, Google, Groq et tout fournisseur compatible LiteLLM. C'est le framework qui alimente de nombreux systèmes de récupération et de raisonnement en production chez JetBlue, Databricks ou Replit.

Dans ce tutoriel, vous allez construire un pipeline de questions-réponses factuelles qui :

  • Récupère des passages depuis une petite base de connaissances
  • Raisonne pas à pas grâce au Chain-of-Thought
  • S'optimise automatiquement selon des métriques de précision et de latence
  • S'améliore de façon mesurable sans que vous ne touchiez à un seul prompt

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Python 3.10 ou plus récent
  • Une clé API OpenAI ou Anthropic (ou tout fournisseur compatible LiteLLM)
  • Une connaissance de pip et des environnements virtuels
  • Les bases des concepts LLM (tokens, embeddings, RAG)
  • Environ 30 minutes de temps concentré

Ce que vous allez construire

Un système de questions-réponses auto-optimisant qui répond à des questions sur l'écosystème tech tunisien à partir d'un petit corpus. À la fin vous aurez :

  • Un pipeline typé en Python pur
  • Une fonction de métrique pour l'évaluation automatique
  • Un programme compilé qui dépasse la baseline zero-shot de plus de 15 points de précision
  • Des artefacts sauvegardés que vous pouvez charger et servir depuis FastAPI

Étape 1 : installer DSPy et configurer un modèle

Créez un nouveau projet et installez DSPy 3 :

mkdir dspy-qa && cd dspy-qa
python -m venv .venv
source .venv/bin/activate
pip install "dspy-ai>=3.0.0" "litellm" "datasets" "fastapi" "uvicorn"

Créez main.py et configurez DSPy avec votre modèle préféré. La librairie utilise LiteLLM en interne, donc le même code fonctionne avec OpenAI, Anthropic, Groq, Ollama ou vLLM.

import os
import dspy
 
lm = dspy.LM(
    "openai/gpt-4o-mini",
    api_key=os.environ["OPENAI_API_KEY"],
    temperature=0.0,
    max_tokens=512,
)
dspy.configure(lm=lm)

Pour changer de fournisseur, seule la chaîne du modèle change — tout le reste est agnostique.

# Anthropic
lm = dspy.LM("anthropic/claude-haiku-4-5", api_key=os.environ["ANTHROPIC_API_KEY"])
 
# Groq (inférence rapide)
lm = dspy.LM("groq/llama-3.3-70b-versatile", api_key=os.environ["GROQ_API_KEY"])
 
# Ollama local
lm = dspy.LM("ollama/llama3.1", api_base="http://localhost:11434")

Étape 2 : définir une signature

Une signature est une déclaration typée de la tâche : entrées, sorties et docstring qui décrit l'intention. DSPy s'en sert pour construire le prompt réel à l'exécution.

class GenerateAnswer(dspy.Signature):
    """Answer questions about Tunisian tech companies and startups using the provided context."""
 
    context: list[str] = dspy.InputField(desc="Relevant passages from the knowledge base")
    question: str = dspy.InputField()
    answer: str = dspy.OutputField(desc="A concise factual answer, one to two sentences")

Remarquez que vous n'écrivez jamais « tu es un assistant utile » ou « réponds de façon concise » dans un prompt — la docstring et la description des champs suffisent. L'optimiseur de DSPy raffinera plus tard cette spécification en le prompt le plus performant.

Étape 3 : composer des modules

Les modules enveloppent une signature avec une stratégie de raisonnement. Les trois modules que vous utiliserez le plus sont :

  • dspy.Predict — prédiction directe (un appel LLM)
  • dspy.ChainOfThought — ajoute un champ de raisonnement avant la sortie
  • dspy.ReAct — entrelace raisonnement et appels d'outils

Construisez un module de QA en Chain-of-Thought :

class TunisianTechQA(dspy.Module):
    def __init__(self, num_passages: int = 3):
        super().__init__()
        self.retrieve = dspy.Retrieve(k=num_passages)
        self.generate_answer = dspy.ChainOfThought(GenerateAnswer)
 
    def forward(self, question: str):
        context = self.retrieve(question).passages
        prediction = self.generate_answer(context=context, question=question)
        return dspy.Prediction(context=context, answer=prediction.answer)

La méthode forward compose la récupération et la génération. Aucun prompt écrit à la main, nulle part.

Étape 4 : configurer la récupération

DSPy fournit des retrievers pour ChromaDB, Pinecone, Weaviate, Qdrant, et un store vectoriel en mémoire nommé Embeddings. Pour ce tutoriel, vous utiliserez la version en mémoire avec un petit corpus.

corpus = [
    "InstaDeep is a Tunisian AI startup founded in 2014, acquired by BioNTech in 2023 for around 562 million euros.",
    "Expensya, founded in Tunis in 2014, was acquired by Medius in 2023.",
    "Tunisia hosts the annual Maghreb Emerging conference focused on AI and deep tech investment.",
    "Noqta is a Tunisian software studio focused on AI-driven web platforms and developer tooling.",
    "Datavora, founded in Tunis, was acquired by Semrush in 2020.",
    "The Smart Tunisia program subsidizes salaries for tech graduates working in export-oriented companies.",
]
 
embedder = dspy.Embedder("openai/text-embedding-3-small")
retriever = dspy.retrievers.Embeddings(embedder=embedder, corpus=corpus, k=3)
dspy.configure(rm=retriever)

Désormais tout module utilisant dspy.Retrieve puisera dans ce corpus. En production, vous remplacerez ceci par une base vectorielle managée.

Étape 5 : préparer les données d'entraînement et d'évaluation

L'optimisation a besoin de données. Vingt à cinquante exemples suffisent pour observer des gains significatifs — DSPy fait le gros du travail via le bootstrapping.

from dspy import Example
 
trainset = [
    Example(question="Who acquired InstaDeep and when?",
            answer="BioNTech acquired InstaDeep in 2023.").with_inputs("question"),
    Example(question="What does Noqta build?",
            answer="Noqta builds AI-driven web platforms and developer tooling.").with_inputs("question"),
    Example(question="Which company acquired Expensya?",
            answer="Medius acquired Expensya in 2023.").with_inputs("question"),
    Example(question="What is Smart Tunisia?",
            answer="A program subsidizing salaries for tech graduates in export-oriented companies.").with_inputs("question"),
    # ... ajoutez 16 exemples supplémentaires pour un total de 20
]
 
devset = [
    Example(question="In what year was Datavora acquired?",
            answer="2020.").with_inputs("question"),
    Example(question="What sector is InstaDeep in?",
            answer="Artificial intelligence.").with_inputs("question"),
    # ... ajoutez 8 exemples supplémentaires pour un total de 10
]

L'appel .with_inputs("question") indique à DSPy quels champs sont des entrées par rapport aux sorties attendues.

Étape 6 : définir une métrique

Une métrique est une fonction Python qui prend un exemple de référence et une prédiction, et renvoie un score. Pour la QA factuelle, la correspondance exacte est trop stricte ; utilisez une métrique de similarité sémantique ou laissez un LLM juger.

def answer_correctness(example, prediction, trace=None) -> float:
    """LLM-as-judge metric. Returns 1.0 if the prediction is factually equivalent to the gold answer."""
    judge = dspy.Predict("question, gold_answer, predicted_answer -> is_correct: bool")
    result = judge(
        question=example.question,
        gold_answer=example.answer,
        predicted_answer=prediction.answer,
    )
    return 1.0 if result.is_correct else 0.0

Cette métrique est elle-même un programme DSPy. Bienvenue dans la récursion.

Étape 7 : compiler avec MIPROv2

C'est ici que DSPy mérite son nom. L'optimiseur explore l'espace des instructions de prompt et des démonstrations few-shot pour maximiser votre métrique sur le jeu d'entraînement.

from dspy.teleprompt import MIPROv2
 
qa = TunisianTechQA()
 
# Baseline avant optimisation
baseline_score = sum(answer_correctness(ex, qa(question=ex.question)) for ex in devset) / len(devset)
print(f"Zero-shot baseline: {baseline_score:.2%}")
 
optimizer = MIPROv2(
    metric=answer_correctness,
    auto="medium",  # "light", "medium" ou "heavy"
    num_threads=8,
)
 
compiled_qa = optimizer.compile(
    qa,
    trainset=trainset,
    requires_permission_to_run=False,
    minibatch=True,
)
 
# Score après optimisation
optimized_score = sum(answer_correctness(ex, compiled_qa(question=ex.question)) for ex in devset) / len(devset)
print(f"Optimized: {optimized_score:.2%}")

Sur un jeu de 20 exemples, attendez-vous à des baselines autour de 60 à 70 pour cent et à des scores optimisés au-delà de 85 pour cent. L'optimisation prend typiquement de deux à cinq minutes selon la taille des données et le réglage auto.

Étape 8 : inspecter ce qui a été appris

DSPy est transparent — vous pouvez voir le prompt exact que l'optimiseur a produit.

compiled_qa.generate_answer.demos  # les exemples few-shot sélectionnés
compiled_qa.generate_answer.signature.instructions  # l'instruction réécrite

Précieux pour le debug, et aussi pour porter le prompt compilé vers d'autres systèmes si vous décidez un jour de quitter DSPy.

Étape 9 : sauvegarder et charger

La compilation est coûteuse, donc persistez le résultat.

compiled_qa.save("./compiled_qa.json")
 
# Plus tard, en production
qa = TunisianTechQA()
qa.load("./compiled_qa.json")
answer = qa(question="Who acquired InstaDeep?").answer

Le fichier sauvegardé est un petit JSON contenant des instructions et des démonstrations — pas de poids de modèle, pas d'état propriétaire.

Étape 10 : servir avec FastAPI

Enveloppez le programme compilé dans une petite API.

from fastapi import FastAPI
from pydantic import BaseModel
 
app = FastAPI()
 
qa = TunisianTechQA()
qa.load("./compiled_qa.json")
 
class Query(BaseModel):
    question: str
 
@app.post("/ask")
async def ask(q: Query):
    result = await qa.acall(question=q.question)
    return {"answer": result.answer, "context": result.context}

Lancez :

uvicorn main:app --reload

DSPy 3 a ajouté un support natif de l'asynchrone, donc acall est non bloquant et s'intègre proprement avec FastAPI, Starlette ou tout stack async.

Tester votre implémentation

Un contrôle rapide avant déploiement :

test_questions = [
    "Who founded InstaDeep?",
    "When was Expensya acquired?",
    "What does the Smart Tunisia program do?",
]
 
for q in test_questions:
    result = qa(question=q)
    print(f"Q: {q}\nA: {result.answer}\n")

Si les réponses citent des faits absents du corpus, votre récupération est trop étroite — augmentez k dans dspy.Retrieve ou élargissez le corpus.

Dépannage

L'optimiseur est trop lent. Mettez auto="light" pour un run rapide ou réduisez num_threads si vous êtes limité par les quotas d'API.

La métrique renvoie des scores plats. Votre modèle juge est probablement trop petit. Utilisez un modèle plus fort uniquement pour l'évaluation en passant lm= à l'appel Predict du juge.

Les prédictions ignorent le contexte. Vérifiez que la docstring de votre signature demande au modèle d'utiliser le contexte, et augmentez le nombre de passages récupérés.

Limites de taux OpenAI pendant la compilation. MIPROv2 avec auto="heavy" peut déclencher des milliers d'appels. Commencez par auto="light" ou utilisez un modèle Ollama local pour les prototypes.

ModuleNotFoundError: dspy.retrievers. Vous êtes sur une ancienne version. Lancez pip install -U "dspy-ai>=3.0.0".

Pour aller plus loin

Étendez ce pipeline avec :

  • Usage d'outils : remplacez ChainOfThought par dspy.ReAct et donnez-lui une calculatrice ou un outil de recherche web
  • Raisonnement multi-sauts : enchaînez deux modules dspy.ChainOfThought pour que le modèle décompose des questions complexes
  • Récupération en production : branchez un vector store managé. Voir notre tutoriel Pinecone pour l'indexation, ou le guide LangFuse pour surveiller la latence et la qualité en production
  • Évaluation plus robuste : utilisez dspy.evaluate.SemanticF1 intégré ou branchez Promptfoo
  • Optimisation continue : relancez l'optimiseur chaque semaine sur un jeu de données en croissance et publiez le nouvel artefact via votre CI

Conclusion

Vous avez construit un pipeline de questions-réponses auto-optimisant sans rédiger un seul prompt à la main. DSPy a transformé une signature typée, une métrique et 20 exemples en un système qui surpasse de manière mesurable la baseline zero-shot.

Le motif se généralise bien au-delà de la QA. Classification, résumé, extraction structurée, routage multi-agents — tout ce que vous auriez normalement prompt-engineeré peut être exprimé comme une signature et compilé. Quand vous passez de GPT-4o-mini à Claude Haiku ou à un Llama local, l'optimiseur réajuste tout pour vous au lieu d'imposer une réécriture manuelle.

C'est le pari de DSPy : arrêtez de régler des prompts, commencez à programmer.