L’IA agente avec LangChain – Partie 1
Partie 1 de l’IA agente avec LangChain : comment un LLM obtient des outils, de la mémoire et un cycle de raisonnement, où RAG s’intègre dans un agent, ainsi que les vérifications à effectuer avant de le déployer.
Sources: Agentic AI with LangChain — Part 1: Building A Semantic RAG Pipeline
Ce guide reconstitue le parcours allant des matières premières à un système fonctionnel pour : L’IA agente avec LangChain — Partie 1 : Création d’un pipeline RAG sémantique. L’accent est mis sur des étapes opérationnelles, des vérifications explicites et du code que vous pouvez intégrer directement dans un dépôt sans devoir deviner l’intention. Pour une vue d’ensemble, définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans avoir à deviner l’état caché. Documentez conjointement le parcours normal et les scénarios de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’une mise en forme ultérieure.
Stack technique
Lorsque vous travaillez sur la pile technologique, notez d’abord les spécifications : entrées requises, signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Préférez des unités petites et testables aux scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Évaluez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.
1. Configurations du système
Lorsque vous travaillez sur la section 1. Configurations du système, notez d’abord les conditions contractuelles : entrées requises, signal de succès, et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux éléments générés, définez des vérifications de succès, et refusez les terminaisons partielles silencieuses. Évaluez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un problème de capacité de récupération insuffisante.
OPENAI_API_KEY="<Your OpenAI API Key>"
VECTOR_DB=pinecone
Hugging_Face_TOKEN=<your huggingface token>
PINECONE_API_KEY=<your Pinecone API Key>
PINECONE_ENVIRONMENT=gcp-starter
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
LANGSMITH_API_KEY="<Your LangSmith API Key"
LANGSMITH_PROJECT="agent-ai"
TAVILY_API_KEY=<Your TAVILY API Key>
class BaseConfig(BaseSettings):
OPENAI_API_KEY: Optional[str]
PINECONE_API_KEY: Optional[str]
TAVILY_API_KEY: Optional[str]
"""Loads the dotenv file. Including this is necessary to get
pydantic to load a .env file."""
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
2. Structure du projet
Lorsque vous travaillez sur la section 2. Structure du projet, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’honnêteté des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le système passe de l’environnement de démonstration à des environnements partagés. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent de prompts ne résout que rarement un système de récupération insuffisant.
project/
├── rag_process/
│ ├── __init__.py
│ ├── extractor.py
| └── service.py
│
├── chains.py # llm chains
├── nodes.py # python functions utlizing llm chains
|── schemas.py # pydantic data models
|── upload.py # python functions to upload files asynchronously
├── main.py # fastAPI app
├── config.py # pydantic BaseConfig
│── dependencies.py # dependency inject to fastAPI endpoint function
├── test_chains.py # pytests
├── .env # environment variable definition
├── client.py # streamlit client
├── .gitignore
└── requirements.txt # package requirements
Lorsque vous travaillez sur la section 2. Structure du projet, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’honnêteté des modifications ultérieures du code. Documentez en même temps le parcours normal et le parcours de récupération. Les tentatives de réexécution, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations apportées ultérieurement.
2.1. Chaînes LLM
2.1. Les chaînes LLM fonctionnent le mieux lorsqu’elles sont considérées comme une surface mesurable. Capturez un transcript parfait, un cas d’échec et la note de réversion avant d’élargir le périmètre. Préférez des unités petites et testables plutôt que des scripts complexes. Lorsqu’une étape échoue, l’erreur doit pointer vers une seule responsabilité et non vers un processus embrouillé. Fixez un budget de tokens par tour et par session. Les outils agents élargissent lourdement le contexte ; des plafonds stricts empêchent que les démonstrations ne se transforment en factures inattendues.
2.2. Symantic RAG
2.2. Le RAG sémantique fonctionne le mieux lorsqu’il est considéré comme une surface mesurable. Capturez un transcript parfait, un cas d’échec et la note de réversion avant d’élargir le périmètre. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Nommez les artefacts, définites des critères de succès et refusez toute complétion partielle silencieuse. Séparez la politique de segmentation des données de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.
2.3. Backend FastAPI
2.3. Le backend FastAPI fonctionne le mieux lorsqu’il est considéré comme une entité mesurable. Capturez un exemple idéal, un cas d’échec et des notes de réversion avant d’élargir le périmètre. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le système passe d’un environnement de démonstration à des environnements partagés. Séparez la politique de segmentation des données de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent. 2.3. Le backend FastAPI fonctionne le mieux lorsqu’il est considéré comme une entité mesurable. Capturez un exemple idéal, un cas d’échec et des notes de réversion avant d’élargir le périmètre. Documentez ensemble le parcours normal et les procédures de récupération. Les tentatives de réexécution, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’améliorations apportées ultérieurement.
2.4. Client Streamlit
Pour 2.4. Le client Streamlit, définissez les entrées, le responsable de l’étape et les critères de sortie avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Préférez des unités petites et testables à des scripts volumineux. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un pipeline embrouillé. Citez les passages qui ont réellement servi de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.
3. Chaînes LLM avec LCEL
Pour les chaînes LLM avec LCEL, définissez les entrées, le responsable de chaque étape ainsi que les critères d’arrêt avant de modifier du code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des vérifications de succès et refusez les terminations partielles silencieuses. Préférez des sorties structurées avec validation de schéma plutôt que du texte libre lorsque l’étape suivante consiste en du code ou une appel à outil.
3.1. LangChain Expression Language (LCEL)
Pour 3.1. LangChain Expression Language (LCEL), définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût en tokens ou en requêtes à côté des résultats fonctionnels. Une visibilité précoce du coût évite les factures inattendues lorsque le parcours passe d’un environnement de démonstration à des environnements partagés. Citez les passages qui ont réellement servi de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation. Pour 3.1. LangChain Expression Language (LCEL), définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez ensemble le parcours optimal et les procédures de récupération. Les tentatives de réessai, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non en fin de compte.
r polissage.class GradeDocuments(BaseModel):
"""Binary score for relevance check on retrieved documents."""
binary_score: bool = Field(
description="Documents are relevant to the question, 'yes' or 'no'"
)
async def retrieval_grader(document: Document, question: str):
llm = ChatOpenAI(api_key=api_key, model="gpt-4o-mini", temperature=0)
structured_llm_grader = llm.with_structured_output(GradeDocuments)
system = """You are a grader assessing relevance of a retrieved document to a user question. \n
If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant.\n
Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \n
if the document is relevant, give it a score 'yes', otherwise give it a score 'no'
"""
grade_prompt = ChatPromptTemplate.from_messages(
[
("system", system),
("human", "Retrieved document: \n\n {document} User question: {question}"),
]
)
retrieval_grader = grade_prompt | structured_llm_grader
grade = await retrieval_grader.ainvoke({"document": document, "question": question})
return grade.binary_score
4. RAG sémantique avec PineCone
4.1. Flux de travail d’ingestion
4.2. Flux de travail de récupération
4.3. Classe VectorService
4.3.1. Initialisation
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore
class VectorService:
def __init__(self, index_name: str = "knowledgebase"):
self.index_name = index_name
# Embedding model
self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small", api_key=api_key)
# Pinecone vectorstore wrapper
self.vectorstore = PineconeVectorStore(
index_name=index_name,
embedding=self.embeddings,
pinecone_api_key=pinecone_key,
)
# Retriever
self.retriever = self.vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3}
)
# Text splitter (LangChain-native)
self.splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
separators=["\n\n", "\n", ".", " ", ""]
)
4.3.2. Stockage du contenu du fichier
async def store_file_content_in_db(self, filepath: str) -> None:
"""
Read the uploaded text file, split the file, embed chunks and save chunks to vector database
:param filepath: filepath to load to vectordb
:return: None
"""
logger.debug(f"Loading file: {filepath}")
# Load entire file (safe for most RAG use cases)
async with aiofiles.open(filepath, "r", encoding="utf-8") as f:
raw_text = await f.read()
# Minimal cleaning (preserve structure)
raw_text = raw_text.replace("\r", "")
# Split into LangChain Documents
docs = self.splitter.create_documents(
[raw_text],
metadatas=[{"source": os.path.basename(filepath)}]
)
logger.debug(f"Storing {len(docs)} chunks into Pinecone")
# Ingest into Pinecone
PineconeVectorStore.from_documents(
documents=docs,
embedding=self.embeddings,
index_name=self.index_name,
pinecone_api_key=pinecone_key,
)
4.3.3. Recherche de documents
async def search_documents(self, query: str) -> list[Document]:
logger.debug(f"Searching for: {query}")
return self.retriever.invoke(query)
5. Backend FastAPI
5.1. Téléversement de fichiers
async def save_file(file: UploadFile) -> str:
"""
accept a UploadFile from FastAPI and save the file in specified location
:param file: UploadFile specified by users from fastAPI interface
:return: path of the saved file
"""
await makedirs("uploads", exist_ok=True)
filepath = os.path.join("uploads", file.filename)
async with aiofiles.open(filepath, "wb") as f:
while chunk := await file.read(DEFAULT_CHUNK_SIZE):
await f.write(chunk)
return filepath
5.2. Modèles de demande/réponse
class RAGRequest(BaseModel):
prompt: str
class RAGResponse(BaseModel):
answer: str
num_original_documents: int
5.3. Point de terminaison de génération de réponses
# dependencies.py
from fastapi import Body, HTTPException
from schemas import RAGRequest
from nodes import generate
async def get_generation(body: RAGRequest=Body(...)) -> dict:
try:
generation = await generate(body.prompt)
return {
"answer": generation.get("generation", ""),
"num_original_documents": generation.get("num_original_documents", 0)
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# main.py
@app.post("/generate_text", response_model=RAGResponse)
async def query_by_RAG_controller(generation: dict = Depends(get_generation)) -> RAGResponse:
return RAGResponse(**generation)
5.4. Traitement en arrière-plan
@app.post("/upload")
async def file_upload_controller(
file: Annotated[UploadFile, File(description="Uploaded PDF documents")],
bg_text_processor: BackgroundTasks,
):
if file.content_type != "application/pdf":
raise HTTPException(
detail=f"Only uploading PDF documents are supported",
status_code=status.HTTP_400_BAD_REQUEST,
)
try:
filepath = await save_file(file)
bg_text_processor.add_task(pdf_text_extractor, filepath)
bg_text_processor.add_task(
vector_service.store_file_content_in_db,
filepath.replace("pdf", "txt")
)
except Exception as e:
raise HTTPException(
detail=f"An error occurred while saving file - Error: {e}",
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return {"filename": file.filename, "message": "File uploaded successfully"}
6. Frontend Streamlit
6.1. Téléversement de fichiers
st.write("Upload a file to FastAPI")
file = st.file_uploader("Choose a file", type=["pdf"])
if st.button("Submit"):
if file is not None:
files = {"file": (file.name, file, file.type)}
response = requests.post("http://localhost:8000/upload", files=files)
st.write(response.text)
else:
st.write("No file uploaded.")
6.2. Historique des conversations
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
6.3. Flux de travail de chat
if prompt := st.chat_input("Write your prompt in this input field"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.text(prompt)
question = prompt
response = requests.post(
f"http://localhost:8000/generate_text",
json={"prompt": question}
)
response.raise_for_status()
response_json = response.json()
answer = response_json["answer"]
st.session_state.messages.append({"role": "assistant", "content": answer})
with st.chat_message("assistant"):
st.markdown(answer)