Agents de questions-réponses sur plusieurs documents
Indexez un corpus documentaire et répondez aux questions à partir de tous les documents.
Agents de questions-réponses sur plusieurs documents est une leçon AI Agents gratuite sur CoddyKit. Ceci est la leçon 3 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage AI Agents, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours AI Agents comprend 4 leçons au total.
Vue d'ensemble des questions-réponses sur plusieurs documents
Un agent de questions-réponses sur plusieurs documents répond aux questions en récupérant le contenu pertinent d'une collection de N documents, en synthétisant une réponse et en attribuant chaque affirmation à sa source.
Contrairement aux questions-réponses sur un seul document, les agents traitant plusieurs documents doivent gérer les informations contradictoires entre les sources et déterminer quels documents sont les plus pertinents pour la question.
Indexer plusieurs documents
Avant de répondre à la moindre question, tous les documents doivent être indexés : analysés, découpés en segments, vectorisés et stockés dans une base de données vectorielle. Chaque segment est stocké avec des métadonnées qui le relient à son document source.
import chromadb
from chromadb.utils import embedding_functions
import os
client = chromadb.PersistentClient(path='./doc_index')
ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.getenv('OPENAI_API_KEY'),
model_name='text-embedding-3-small'
)
collection = client.get_or_create_collection('documents', embedding_function=ef)
def index_document(doc_id, doc_path, doc_title):
# Parse and chunk
chunks = pdf_to_chunks(doc_path, chunk_size=800, overlap=150)
for i, chunk in enumerate(chunks):
chunk_id = f'{doc_id}_chunk_{i}'
collection.add(
ids=[chunk_id],
documents=[chunk['text']],
metadatas=[{
'doc_id': doc_id,
'title': doc_title,
'page': chunk['page'],
'source_file': doc_path
}]
)
print(f'Indexed {len(chunks)} chunks from: {doc_title}')Récupérer les segments pertinents
Lorsqu'une question est posée, interrogez le magasin vectoriel pour trouver les segments les plus similaires sur le plan sémantique parmi tous les documents indexés. Le paramètre n_results contrôle le nombre de segments à récupérer.
def retrieve_relevant_chunks(question, n_results=8):
results = collection.query(
query_texts=[question],
n_results=n_results,
include=['documents', 'metadatas', 'distances']
)
chunks = []
for i in range(len(results['documents'][0])):
chunks.append({
'text': results['documents'][0][i],
'metadata': results['metadatas'][0][i],
'distance': results['distances'][0][i],
'relevance': 1 - results['distances'][0][i] # cosine similarity proxy
})
# Sort by relevance
chunks.sort(key=lambda x: x['relevance'], reverse=True)
return chunksAttribution des sources dans l'invite
Lorsque vous transmettez les segments récupérés au LLM, indiquez la source documentaire de chaque segment. Le LLM peut alors citer les sources à l'aide de références numérotées dans la réponse.
def format_chunks_for_prompt(chunks, max_chars=4000):
sections = []
used_chars = 0
for i, chunk in enumerate(chunks, 1):
meta = chunk['metadata']
header = f"[Source {i}: {meta['title']}, page {meta.get('page', '?')}]"
content = chunk['text'][:600]
entry = f'{header}\n{content}'
if used_chars + len(entry) > max_chars:
break
sections.append(entry)
used_chars += len(entry)
return '\n\n'.join(sections)
QA_PROMPT = '''Answer the question based on the provided document excerpts.
Cite sources as [Source N]. If sources conflict, mention both views.
{context}
Question: {question}
Answer:'''
def answer_question(question):
chunks = retrieve_relevant_chunks(question, n_results=6)
context = format_chunks_for_prompt(chunks)
return llm_call(QA_PROMPT.format(context=context, question=question))Raisonnement entre documents
Certaines questions nécessitent de synthétiser des informations provenant de plusieurs documents, et pas seulement de trouver un segment correspondant. Par exemple : « Lequel des trois contrats comporte la clause pénale la moins élevée ? »
Utilisez une approche en deux étapes : récupérez les segments pertinents de chaque document, puis demandez au LLM de les comparer et de produire une synthèse.
def cross_document_compare(question, doc_ids):
# Retrieve best chunks per document
per_doc_chunks = {}
for doc_id in doc_ids:
results = collection.query(
query_texts=[question],
n_results=3,
where={'doc_id': {'$eq': doc_id}} # filter by document
)
if results['documents'][0]:
per_doc_chunks[doc_id] = results['documents'][0]
# Format with document labels
context_parts = []
for doc_id, texts in per_doc_chunks.items():
doc_label = f'Document {doc_id}'
combined = ' '.join(texts[:2])[:600]
context_parts.append(f'== {doc_label} ==\n{combined}')
comparison_context = '\n\n'.join(context_parts)
return llm_call(f'Compare these documents to answer: {question}\n\n{comparison_context}')Gestion des informations contradictoires
Différents documents peuvent présenter des faits contradictoires : un contrat indique que le paiement est dû sous 30 jours, tandis qu'un autre indique 60 jours. L'agent doit détecter ces contradictions et les mettre en évidence au lieu d'en choisir une en silence.
CONFLICT_PROMPT = '''You are analyzing multiple document sources.
Some may contain conflicting information.
For each factual claim you make:
1. Cite the source document
2. If another source contradicts it, explicitly note the conflict
3. Indicate which source you believe is more authoritative, if possible
Document excerpts:
{context}
Question: {question}
Answer (with conflict notes where applicable):'''
def answer_with_conflict_detection(question):
chunks = retrieve_relevant_chunks(question, n_results=8)
context = format_chunks_for_prompt(chunks)
return llm_call(CONFLICT_PROMPT.format(
context=context, question=question
))Filtrage par seuil de pertinence
Les segments récupérés ne sont pas tous réellement pertinents : la similarité vectorielle implique un compromis entre rappel et précision. Définissez un seuil minimal de pertinence pour exclure les segments dont la correspondance est faible et qui pourraient induire le LLM en erreur.
MIN_RELEVANCE = 0.72 # cosine similarity threshold
def retrieve_above_threshold(question, n_results=10, threshold=MIN_RELEVANCE):
chunks = retrieve_relevant_chunks(question, n_results=n_results)
relevant = [c for c in chunks if c['relevance'] >= threshold]
print(f'Retrieved: {len(chunks)}, Above threshold: {len(relevant)}')
if not relevant:
# Fallback: use top 3 even if below threshold
return chunks[:3]
return relevant
def answer_with_threshold(question):
chunks = retrieve_above_threshold(question)
if not chunks:
return 'I could not find relevant information in the indexed documents.'
context = format_chunks_for_prompt(chunks)
return llm_call(QA_PROMPT.format(context=context, question=question))Générer des citations de sources
Après avoir généré une réponse, extrayez les documents sources cités et renvoyez-les sous forme de liste structurée. Les utilisateurs peuvent ainsi retrouver les documents originaux pour les vérifier.
import re
def extract_citations(answer_text, chunks):
# Find all [Source N] references in the answer
cited_nums = set(int(m) for m in re.findall(r'\[Source (\d+)\]', answer_text))
citations = []
for num in sorted(cited_nums):
idx = num - 1
if idx < len(chunks):
meta = chunks[idx]['metadata']
citations.append({
'source_num': num,
'title': meta.get('title', 'Unknown'),
'page': meta.get('page', 'N/A'),
'file': meta.get('source_file', '')
})
return citations
def answer_with_citations(question):
chunks = retrieve_above_threshold(question)
context = format_chunks_for_prompt(chunks)
answer = llm_call(QA_PROMPT.format(context=context, question=question))
citations = extract_citations(answer, chunks)
return {'answer': answer, 'citations': citations}Nouveau classement avec un encodeur croisé
La récupération vectorielle initiale utilise des bi-encodeurs (rapides, mais approximatifs). Un encodeur croisé réordonne les meilleurs résultats en évaluant chaque paire (requête, segment) conjointement : il est plus précis, mais plus lent. Cette approche en deux étapes améliore la qualité de la réponse finale.
# pip install sentence-transformers
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
def rerank_chunks(question, chunks, top_k=4):
# Score each chunk against the question
pairs = [(question, c['text']) for c in chunks]
scores = reranker.predict(pairs)
# Attach scores and re-sort
scored_chunks = list(zip(scores, chunks))
scored_chunks.sort(key=lambda x: x[0], reverse=True)
top_chunks = [chunk for _, chunk in scored_chunks[:top_k]]
print(f'Re-ranked {len(chunks)} chunks -> kept top {top_k}')
return top_chunks
def answer_with_reranking(question):
# Retrieve more initially
initial_chunks = retrieve_relevant_chunks(question, n_results=12)
# Re-rank for precision
top_chunks = rerank_chunks(question, initial_chunks, top_k=4)
context = format_chunks_for_prompt(top_chunks)
return llm_call(QA_PROMPT.format(context=context, question=question))Filtrage des métadonnées au niveau du document
Lorsque l'utilisateur indique un document ou une plage de dates particulière, filtrez au niveau des métadonnées avant la recherche vectorielle. Cela empêche les documents non pertinents de fausser les résultats.
def retrieve_filtered(question, filters=None, n_results=8):
query_kwargs = {
'query_texts': [question],
'n_results': n_results,
'include': ['documents', 'metadatas', 'distances']
}
# ChromaDB metadata filters
# Example: {'doc_id': 'contract_2024', 'year': {'$gte': 2023}}
if filters:
query_kwargs['where'] = filters
results = collection.query(**query_kwargs)
return [
{'text': t, 'metadata': m, 'relevance': 1 - d}
for t, m, d in zip(
results['documents'][0],
results['metadatas'][0],
results['distances'][0]
)
]
# Example usage
chunks = retrieve_filtered(
'What are the payment terms?',
filters={'doc_id': {'$in': ['contract_a', 'contract_b']}}
)Mettre à jour l'index avec de nouveaux documents
Les collections de documents évoluent au fil du temps : de nouveaux fichiers sont ajoutés et d'anciens sont mis à jour. La chaîne d'indexation doit prendre en charge les mises à jour incrémentielles : ajoutez les nouveaux documents, réindexez ceux qui ont été mis à jour et supprimez ceux qui ont été supprimés.
import os
import hashlib
# Track indexed documents by file hash
index_registry = {} # {filepath: {hash, doc_id, indexed_at}}
def file_hash(filepath):
with open(filepath, 'rb') as f:
return hashlib.md5(f.read()).hexdigest()
def index_if_new_or_changed(filepath, title):
fhash = file_hash(filepath)
existing = index_registry.get(filepath)
if existing and existing['hash'] == fhash:
print(f'Skipping unchanged: {title}')
return existing['doc_id']
if existing:
# Remove old chunks from vector store
collection.delete(where={'doc_id': {'': existing['doc_id']}})
print(f'Re-indexing updated: {title}')
else:
print(f'Indexing new: {title}')
doc_id = hashlib.md5(filepath.encode()).hexdigest()[:8]
index_document(doc_id, filepath, title)
index_registry[filepath] = {'hash': fhash, 'doc_id': doc_id}
return doc_id
def sync_document_directory(directory):
pdf_files = [f for f in os.listdir(directory) if f.endswith('.pdf')]
for fname in pdf_files:
fpath = os.path.join(directory, fname)
title = fname.replace('.pdf', '').replace('_', ' ').title()
index_if_new_or_changed(fpath, title)
print(f'Sync complete: {len(pdf_files)} files processed')Vérification des connaissances
Dans un système de questions-réponses sur plusieurs documents utilisant une génération augmentée par récupération, qu'est-ce que le seuil de pertinence empêche ?
Récapitulatif : agents de questions-réponses sur plusieurs documents
Questions-réponses sur plusieurs documents : indexez tous les documents (analyse → découpage en segments → vectorisation → stockage avec métadonnées) → récupérez les segments pertinents dans tous les documents → mettez-les en forme avec des libellés de source → synthétisez la réponse avec des citations.
Techniques avancées : comparaison entre documents pour les questions comparatives, invites demandant de détecter les contradictions, filtrage par seuil de pertinence, nouveau classement par encodeur croisé pour améliorer la précision et filtrage des métadonnées pour limiter les requêtes à des documents ou des plages de dates précis.
Questions Fréquemment Posées
La leçon « Agents de questions-réponses sur plusieurs documents » est-elle gratuite ?
Oui — le texte complet de « Agents de questions-réponses sur plusieurs documents » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours AI Agents, passe à CoddyKit PRO. Le cours AI Agents comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Agents de questions-réponses sur plusieurs documents » ?
Indexez un corpus documentaire et répondez aux questions à partir de tous les documents. Tu pratiques AI Agents avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer AI Agents ?
Aucune expérience préalable n'est requise. AI Agents sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 3 sur 4.
Combien de temps prend la leçon « Agents de questions-réponses sur plusieurs documents » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon AI Agents ?
Oui. Chaque leçon AI Agents inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Analyser des PDF avec PyMuPDF et pdfplumber
- OCR pour les documents numérisés
- Agents de questions-réponses sur plusieurs documents
- Classification et routage des documents