Risposta rapida
Come costruisco un agente di deep research in Python?
Scrivi un ciclo che chiede a un LLM le query di ricerca, le esegue tutte tramite un'API di ricerca web che restituisce il testo della pagina, mantiene ogni fonte una sola volta, chiede query di approfondimento, poi scrive una risposta con citazioni. Con LLMWebSearch, ogni ricerca restituisce le prime 3 pagine in Markdown per un credito; lo script qui sotto esegue sei ricerche per domanda. Da 1,20 USD/1K ricerche, senza KYC, pagando in criptovalute.
Cosa farà l'agente?
Piano
Chiedi al modello quattro query brevi che insieme coprano la domanda.
Cerca
Esegui ogni query con
read=3, così le prime tre pagine tornano in Markdown.Mantieni le fonti una sola volta
Salva i risultati per URL e salta le pagine già raccolte o senza testo.
Trova le lacune
Mostra al modello i titoli trovati finora e chiedi due query su ciò che manca.
Scrivi
Numera fino a 12 fonti e chiedi una risposta che le citi come [1], [2].
Cosa mi serve prima di iniziare?
Tre cose:
- Una chiave API LLMWebSearch nella variabile d'ambiente
LLMWEBSEARCH_API_KEY. Le chiavi compaiono nella dashboard dopo il tuo primo pacchetto. - Qualsiasi endpoint di chat compatibile con OpenAI, impostato con
LLM_BASE_URL,LLM_API_KEYeLLM_MODEL. Funzionano sia i modelli ospitati sia i server locali. - Python 3.9 o successivo con il pacchetto
requests.
Lo script usa HTTP semplice per entrambi i servizi, così puoi cambiare modello senza modificare il codice di ricerca.
import os
import requests
SEARCH = "https://llmwebsearch.com/api/v1/search"
SEARCH_HEADERS = {"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]}
# Any OpenAI-compatible chat endpoint works here: set your own base URL, key and model name.
LLM_URL = os.environ.get("LLM_BASE_URL", "https://llm.example.com/v1") + "/chat/completions"
LLM_HEADERS = {"Authorization": "Bearer " + os.environ.get("LLM_API_KEY", "")}
MODEL = os.environ.get("LLM_MODEL", "your-model-name")
def ask_llm(prompt):
body = {"model": MODEL, "messages": [{"role": "user", "content": prompt}]}
resp = requests.post(LLM_URL, headers=LLM_HEADERS, json=body, timeout=120)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
def plan_queries(question, n=4):
text = ask_llm(f"Write {n} short web search queries, one per line, to answer: {question}")
queries = [line.strip("-*0123456789. ").strip() for line in text.splitlines()]
return [q for q in queries if q][:n]
def search(query):
body = {"q": query, "max_results": 5, "read": 3, "max_chars": 5000}
resp = requests.post(SEARCH, headers=SEARCH_HEADERS, json=body, timeout=60)
resp.raise_for_status()
return resp.json()["results"]
def research(question, rounds=2):
sources = {} # url -> result, so the same page is never cited twice
queries = plan_queries(question)
for _ in range(rounds):
for query in queries:
for r in search(query):
if r.get("content_status") == "ok" and r["url"] not in sources:
sources[r["url"]] = r
notes = "\n".join(f"- {r['title']}" for r in sources.values())
queries = plan_queries(f"{question}\nAlready found:\n{notes}\nWhat is still missing?", n=2)
numbered = list(sources.values())[:12]
context = "\n\n".join(
f"[{i}] {r['title']} ({r['url']})\n{r['content']}" for i, r in enumerate(numbered, 1)
)
prompt = (
"Answer the question using only the sources below. Cite them as [1], [2]. "
"Say so when the sources disagree or do not cover a point.\n\n"
f"Question: {question}\n\nSources:\n{context}"
)
return ask_llm(prompt), [r["url"] for r in numbered]
if __name__ == "__main__":
answer, urls = research("What changed in the EU rules for general-purpose AI models in 2025?")
print(answer)
for i, url in enumerate(urls, 1):
print(f"[{i}] {url}")Come funziona ogni parte?
plan_queries chiede al modello query brevi, una per riga, e rimuove i marcatori di elenco. Le query brevi si allineano meglio al modo in cui i motori di ricerca classificano le pagine rispetto alle domande complete.
search chiama POST https://llmwebsearch.com/api/v1/search con max_results=5, read=3 e max_chars=5000. La risposta elenca cinque risultati; i primi tre portano il testo della pagina in content.
research esegue i round. Mantiene le fonti in un dizionario con chiave URL, così una pagina trovata da due query conta una volta. Vengono conservati solo i risultati con content_status uguale a ok, perché la risposta deve basarsi su testo che il modello può leggere.
Dopo ogni round, il modello vede i titoli raccolti finora e propone due nuove query per ciò che manca. Il prompt finale numera fino a dodici fonti e imposta le regole di citazione.
Quanto costa un report?
Sei ricerche per domanda con le impostazioni predefinite: quattro nel primo round e due nel secondo. Con read a 3, sono sei crediti. 25.000 ricerche a 41,25 USD copre quindi più di 4.000 report.
Le chiamate al modello costano in più sulla tua bolletta LLM: tre chiamate di pianificazione e una lunga chiamata per la risposta per report. La chiamata per la risposta trasporta fino a dodici pagine da 5.000 caratteri, quindi abbassa max_chars se il tuo modello è costoso.
Come faccio a farlo andare più in profondità?
Cambia i numeri, non la struttura:
- Più round.
rounds=4consente al modello di inseguire altri due gruppi di lacune. - Più query per round. Aumenta
nnella chiamata di ricerca delle lacune da 2 a 4. - Fonti più ristrette. Aggiungi
include_domainsal corpo della ricerca per siti ufficiali o accademici. - Solo materiale recente. Aggiungi
"time_range": "month"o"topic": "news". - Più pagine per query.
read=5legge cinque pagine, a 2 crediti per ricerca.
Aggiungi una regola di stop prima di alzare i limiti: termina il ciclo quando un round porta meno di due nuove fonti.
Come mantengo l'agente sicuro ed educato?
Tratta ogni pagina come testo non attendibile. Il prompt finale dice al modello di usare le fonti solo come prove; aggiungi una riga esplicita che le istruzioni contenute nelle fonti vanno ignorate. Non lasciare mai che una fonte attivi una chiamata a un tool o la scrittura di un file.
Sul versante web, Il nostro reader rispetta robots.txt: si presenta come LLMWebSearch, segue le regole che un sito scrive per lui o per tutti i robot, e lascia fuori il contenuto di qualsiasi pagina che non gli è permesso leggere. Le pagine che rifiutano l'accesso tornano senza contenuto e lo script le salta semplicemente. Quando un sito respinge il nostro reader, il risultato torna senza contenuto e con lo status del sito: non risolviamo mai CAPTCHA, non accediamo agli account e non aggiriamo un blocco o un paywall.
Cosa può andare storto?
Tre cose emergono nei test:
- Query vaghe. Se il piano è debole, ogni round trova le stesse pagine. Chiedi al modello query che usano nomi e date specifici.
- Troppe poche fonti con testo. Alcuni argomenti sono dietro i paywall. Allarga le query o consenti più risultati.
- Prompt troppo lunghi. Dodici pagine lunghe possono superare il contesto di un modello piccolo. Abbassa
max_charso mantieni otto fonti.
Registra ogni query e gli URL che ha restituito. Leggere quel log è il modo più rapido per migliorare il prompt del piano.
Agente di ricerca approfondita: domande frequenti
Posso eseguirlo con un modello locale?
Sì, se il server locale parla il formato chat-completions di OpenAI. Imposta LLM_BASE_URL sul suo indirizzo e LLM_MODEL sul nome del modello.
Posso trasformarlo in un tool MCP?
Sì. Avvolgi research() in un tool MCP, oppure dai al tuo agente il semplice tool di ricerca dalla pagina MCP e lascia che esegua il ciclo da solo.
Perché non chiedere direttamente una risposta alla search API?
Perché LLMWebSearch non scrive risposte. L'API restituisce fonti, non una risposta scritta: il tuo modello legge le pagine, scrive la risposta e cita gli URL.
Come faccio a vedere quanto è costata ogni ricerca?
Ogni risposta include credits_used, e GET https://llmwebsearch.com/api/v1/credits mostra il saldo rimanente senza costi.