Conception d’une boucle de recherche en plusieurs étapes
Planifier → rechercher → lire → extraire → synthétiser → recommencer jusqu’à obtenir une profondeur suffisante.
Conception d’une boucle de recherche en plusieurs étapes est une leçon AI Agents gratuite sur CoddyKit. Ceci est la leçon 1 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.
Qu’est-ce qu’une boucle de recherche ?
Une boucle de recherche est un schéma agentique dans lequel un LLM planifie, effectue des recherches, lit, extrait des informations, identifie les lacunes et recommence jusqu’à ce que l’objectif de recherche soit atteint.
Contrairement à une recherche effectuée en une seule passe, la boucle s’adapte en fonction de ses découvertes : elle suit les pistes inattendues et abandonne les impasses.
Phase 1 : décomposition de la question
La première étape consiste à décomposer la question de recherche en sous-questions. Cela crée un plan de recherche dirigé et empêche l’agent de naviguer sans but.
import openai, json
client = openai.OpenAI(api_key='YOUR_OPENAI_KEY')
def decompose_question(question: str) -> list[str]:
prompt = (
f'Break this research question into 3-5 focused sub-questions.\n'
f'Each sub-question should be independently searchable.\n'
f'Question: "{question}"\n'
f'Return JSON: {{"sub_questions": ["..."]}}'
)
resp = client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': prompt}],
response_format={'type': 'json_object'}
)
return json.loads(resp.choices[0].message.content)['sub_questions']
questions = decompose_question('What are the main causes of inflation in 2024?')
print(questions)Phase 2 : recherche sur le Web
Pour chaque sous-question, effectuez une recherche sur le Web. Utilisez une API de recherche, comme Serper, Brave ou Bing, pour obtenir une liste d’URL et d’extraits. Ne lisez pas toutes les pages : donnez d’abord la priorité aux sources de qualité.
import requests
SERPER_KEY = 'YOUR_SERPER_API_KEY'
def search_web(query: str, num_results: int = 5) -> list[dict]:
resp = requests.post(
'https://google.serper.dev/search',
headers={'X-API-KEY': SERPER_KEY, 'Content-Type': 'application/json'},
json={'q': query, 'num': num_results}
)
resp.raise_for_status()
results = resp.json().get('organic', [])
return [
{'title': r['title'], 'url': r['link'], 'snippet': r.get('snippet', '')}
for r in results
]Phase 3 : lecture et extraction des faits
Récupérez chaque URL et extrayez les faits clés pertinents pour la sous-question. Utilisez un LLM pour lire l’article et produire une liste de faits accompagnés de l’URL source.
import httpx
from bs4 import BeautifulSoup
def fetch_text(url: str, max_chars: int = 4000) -> str:
try:
resp = httpx.get(url, timeout=10, follow_redirects=True,
headers={'User-Agent': 'ResearchAgent/1.0'})
soup = BeautifulSoup(resp.text, 'html.parser')
for tag in soup(['script', 'style', 'nav', 'footer']):
tag.decompose()
return soup.get_text(separator=' ', strip=True)[:max_chars]
except Exception:
return ''
def extract_facts(text: str, question: str, url: str) -> list[dict]:
prompt = (
f'Extract key facts from the text that answer: "{question}"\n'
f'Return JSON: {{"facts": ["fact1", ...]}}\n\n'
f'TEXT:\n{text[:3000]}'
)
resp = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': prompt}],
response_format={'type': 'json_object'}
)
facts = json.loads(resp.choices[0].message.content).get('facts', [])
return [{'fact': f, 'source': url} for f in facts]Phase 4 : identification des lacunes de connaissances
Après la lecture, demandez au LLM d’examiner les faits accumulés et d’identifier ce qui reste inconnu. Ces lacunes deviennent les requêtes de recherche du tour suivant.
def identify_gaps(original_question: str, facts: list[dict]) -> list[str]:
fact_text = '\n'.join(f'- {f["fact"]}' for f in facts[:20])
prompt = (
f'Original question: "{original_question}"\n'
f'Facts gathered so far:\n{fact_text}\n\n'
f'What key aspects are still unanswered? '
f'Return 0-3 follow-up search queries (0 if research is complete).\n'
f'JSON: {{"gaps": ["search query 1", ...]}}'
)
resp = client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': prompt}],
response_format={'type': 'json_object'}
)
return json.loads(resp.choices[0].message.content).get('gaps', [])Conditions d’arrêt
La boucle a besoin d’une condition d’arrêt claire pour éviter de s’exécuter indéfiniment. Arrêtez-la lorsque :
- Aucune nouvelle lacune n’est identifiée
- Aucun nouveau fait n’a été ajouté lors de la dernière itération
- Le nombre maximal d’itérations est atteint, conformément à la limite de sécurité
- Le nombre total de faits dépasse un seuil, indiquant une profondeur suffisante
def should_continue(gaps: list[str], new_facts_this_round: int,
iteration: int, total_facts: int) -> bool:
if iteration >= 5:
return False # Hard cap: 5 iterations
if new_facts_this_round == 0:
return False # No new information found
if not gaps:
return False # LLM says research is complete
if total_facts >= 50:
return False # Sufficient depth reached
return True
if __name__ == '__main__':
print('Continue (has gaps, new facts)?', should_continue(['gap1'], 4, iteration=1, total_facts=10))
print('Continue (no new facts)?', should_continue(['gap1'], 0, iteration=1, total_facts=10))
Déduplication des faits
Plusieurs sources rapportent souvent le même fait. Dédupliquez-les en demandant au LLM de fusionner les faits sémantiquement équivalents et de conserver la version étayée par les meilleures sources.
def deduplicate_facts(facts: list[dict]) -> list[dict]:
if len(facts) <= 3:
return facts
fact_text = '\n'.join(
f'{i}: {f["fact"]} (source: {f["source"]})' for i, f in enumerate(facts)
)
prompt = (
f'Remove duplicate or near-duplicate facts. Keep the most informative version.\n'
f'Return JSON: {{"keep_indices": [0, 1, ...]}}\n\n'
f'FACTS:\n{fact_text}'
)
resp = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': prompt}],
response_format={'type': 'json_object'}
)
indices = json.loads(resp.choices[0].message.content).get('keep_indices', [])
return [facts[i] for i in indices if i < len(facts)]La boucle de recherche complète
Combinez toutes les phases dans une seule fonction research() qui pilote la boucle jusqu’à son arrêt.
def research(question: str) -> dict:
all_facts = []
iteration = 0
# Phase 1: Decompose into sub-questions
queries = decompose_question(question)
while True:
new_facts_this_round = 0
for query in queries:
results = search_web(query, num_results=3)
for result in results:
text = fetch_text(result['url'])
if not text:
continue
facts = extract_facts(text, query, result['url'])
all_facts.extend(facts)
new_facts_this_round += len(facts)
all_facts = deduplicate_facts(all_facts)
gaps = identify_gaps(question, all_facts)
iteration += 1
if not should_continue(gaps, new_facts_this_round, iteration, len(all_facts)):
break
queries = gaps # next iteration searches for the gaps
return {'facts': all_facts, 'iterations': iteration}Suivi de la provenance
Chaque fait doit conserver son URL source afin que le rapport final puisse inclure des citations. Ne supprimez jamais les métadonnées de source pendant le traitement : elles sont nécessaires à la couche de citations.
def add_fact(fact_list: list, fact_text: str, source_url: str, iteration: int):
fact_list.append({
'fact': fact_text,
'source': source_url,
'iteration': iteration,
'verified': False # set to True after cross-referencing
})
# Example:
facts_store = []
add_fact(facts_store, 'Global inflation peaked at 9.1% in June 2022',
'https://bls.gov/news.release/cpi.htm', iteration=1)
print(f'Logged {len(facts_store)} fact(s):')
for f in facts_store:
print(f" - {f['fact']} (source: {f['source']})")
Recherche parallèle pour gagner en rapidité
Les recherches séquentielles sont lentes : 5 requêtes × 3 URL × 1 récupération chacune = 15 appels HTTP séquentiels. Utilisez concurrent.futures pour paralléliser les récupérations au sein de chaque itération.
from concurrent.futures import ThreadPoolExecutor, as_completed
def parallel_research_round(queries: list[str]) -> list[dict]:
collected = []
def process_query(query):
results = search_web(query, num_results=3)
facts = []
for r in results:
text = fetch_text(r['url'])
if text:
facts.extend(extract_facts(text, query, r['url']))
return facts
with ThreadPoolExecutor(max_workers=4) as ex:
futures = {ex.submit(process_query, q): q for q in queries}
for future in as_completed(futures):
collected.extend(future.result())
return collectedSuivi de la progression de la boucle
Consignez chaque itération afin de pouvoir déboguer les raisons pour lesquelles la boucle s’est arrêtée ou a duré plus longtemps que prévu. Incluez le nombre de requêtes, les faits ajoutés et les lacunes identifiées.
import logging
log = logging.getLogger('research_loop')
import sys
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
def log_iteration(iteration: int, queries: list[str],
new_facts: int, total_facts: int, gaps: list[str]):
log.info(
'Iteration %d | Queries: %d | New facts: %d | Total: %d | Gaps: %d',
iteration, len(queries), new_facts, total_facts, len(gaps)
)
if gaps:
for g in gaps:
log.debug(' Gap query: %s', g)
if __name__ == '__main__':
log_iteration(iteration=2, queries=['who are our top competitors?'], new_facts=4, total_facts=12, gaps=['pricing data'])
Qu’est-ce qui met fin à la boucle de recherche lorsqu’aucune nouvelle information n’est trouvée ?
Comprendre les conditions d’arrêt permet d’éviter les boucles infinies et garantit que l’agent fournit des résultats dans un délai raisonnable.
Récapitulatif de la conception de la boucle de recherche
La boucle de recherche en plusieurs étapes suit le processus suivant : décomposer → rechercher → lire → extraire → identifier les lacunes → recommencer. Arrêtez-la lorsque les lacunes sont épuisées, qu’aucun nouveau fait n’est trouvé ou que la limite stricte d’itérations est atteinte.
Associez toujours les URL sources à chaque fait, parallélisez les recherches pour gagner en rapidité et dédupliquez les faits avant la synthèse.
Questions Fréquemment Posées
La leçon « Conception d’une boucle de recherche en plusieurs étapes » est-elle gratuite ?
Oui — le texte complet de « Conception d’une boucle de recherche en plusieurs étapes » 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 « Conception d’une boucle de recherche en plusieurs étapes » ?
Planifier → rechercher → lire → extraire → synthétiser → recommencer jusqu’à obtenir une profondeur suffisante. 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 1 sur 4.
Combien de temps prend la leçon « Conception d’une boucle de recherche en plusieurs étapes » ?
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
- Conception d’une boucle de recherche en plusieurs étapes
- Vérification des sources et citations
- Génération de rapports structurés
- Vérification des faits et prévention des hallucinations