Prérequis
- Ollama installé et en cours d'exécution localement.
- Un modèle d'embedding compatible et un modèle de langue disponibles via Ollama.
Dépendances
pypdf
faiss-cpu
langchain
langchain-community
langchain-openai
tiktoken
langchainhub
Chargement et nettoyage du PDF
La première étape consiste à lire le document et à normaliser le texte avant de le découper en fragments.
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
def normaliser_contenu(documents):
for doc in documents:
doc.page_content = (
doc.page_content.replace("\t", " ")
.replace("\n", " ")
.replace(" ", " ")
.strip()
)
return documents
def decouper_pdf(chemin, taille_bloc=800, chevauchement=150):
documents = PyPDFLoader(chemin).load()
fractionneur = RecursiveCharacterTextSplitter(
chunk_size=taille_bloc,
chunk_overlap=chevauchement,
length_function=len,
)
fragments = fractionneur.split_documents(documents)
return normaliser_contenu(fragments)
Configuration de l'embedder
On choisit ici d'utiliser Ollama comme fournisseur d'embeddings. D'autres fournisseurs peuvent être branchés via une structure de registre.
from langchain_community.embeddings import OllamaEmbeddings
def embedder_ollama(modele="smartwang/bge-large-zh-v1.5-f32.gguf"):
return OllamaEmbeddings(
base_url="http://127.0.0.1:11434",
model=modele,
)
Indexation dans FAISS
Les fragments nettoyés sont convertis en vecteurs et stockés dans un index FAISS pour une recherche rapide par similarité.
from langchain_community.vectorstores import FAISS
def indexer_documents(chemin_pdf, taille_bloc=800, chevauchement=150):
fragments = decouper_pdf(chemin_pdf, taille_bloc, chevauchement)
embeddings = embedder_ollama()
index = FAISS.from_documents(fragments, embeddings)
return index
Création du retriever
Le retriever interroge l'index et retourne les documents les plus proches de la question utilisateur.
def creer_retriever(index, k=2):
return index.as_retriever(search_kwargs={"k": k})
def recuperer_contexte(question, retriever):
resultats = retriever.invoke(question)
return [r.page_content for r in resultats]
Configuration du modèle de langue
On connecte ici un modèle Ollama via l'API compatible OpenAI, ce qui permet d'utiliser ChatOpenAI avec l'endpoint local.
from langchain_openai import ChatOpenAI
def creer_llm(modele="qwen2.5:1.5b"):
return ChatOpenAI(
api_key="token",
base_url="http://127.0.0.1:11434/v1",
model=modele,
temperature=0,
)
Génération de réponses structurées
On définit un modèle de sortie structuré et une chaîne LangChain qui injecte le contexte et la question dans un prompt.
from langchain_core.prompts import PromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
class ReponseContextuelle(BaseModel):
texte: str = Field(
description="Réponse concise à la question, fondée uniquement sur le contexte fourni."
)
def creer_chaine_reponse(llm):
gabarit = """
À l'aide du contexte ci-dessous, répondez de manière concise à la question.
Contexte :
{context}
Question : {question}
"""
prompt = PromptTemplate(
template=gabarit,
input_variables=["context", "question"],
)
return prompt | llm.with_structured_output(ReponseContextuelle)
def repondre(question, contexte, chaine):
sortie = chaine.invoke({"question": question, "context": contexte})
return {
"question": question,
"context": contexte,
"reponse": sortie.texte,
}
Assemblage du pipelnie
Le script final charge le PDF, indexe les fragments, récupère les passages pertinents et génère une réponse.
chemin_pdf = "data/rag.pdf"
index = indexer_documents(chemin_pdf)
retriever = creer_retriever(index)
question = "Présentez OpenAI."
contexte = recuperer_contexte(question, retriever)
llm = creer_llm()
chaine = creer_chaine_reponse(llm)
print(repondre(question, contexte, chaine))