# Esempi in Python: ricerca, query in parallelo, risposta con citazioni

> Esempi di API di ricerca web in Python: requests, httpx asincrono con semaforo e ciclo di citazioni per qualsiasi LLM entro un budget di token. Senza KYC.

- URL: https://llmwebsearch.com/it/docs/python/
- Ultimo aggiornamento: 2026-10-22
- Editore: LLMWebSearch (https://llmwebsearch.com)

Tre script coprono ciò che serve alla maggior parte degli agenti Python: una ricerca con `requests`, molte ricerche insieme con `httpx` e un semaforo, e un breve ciclo che passa le fonti a un modello linguistico e riceve indietro una risposta con citazioni numerate.

**Risposta rapida — Come chiamo un'API di ricerca web per agenti AI da Python?**

Invia la query in POST a `https://llmwebsearch.com/api/v1/search` con `requests` o `httpx` e una chiave Bearer. **LLMWebSearch** restituisce risultati ordinati con le prime 3 pagine in Markdown, che puoi numerare e passare a qualsiasi LLM per rispondere con citazioni. Una ricerca costa 1 credito, da 1,20 USD/1K ricerche a partire da ottobre 2026. Python 3.9+ e nessun SDK. Senza KYC; paghi in criptovalute.

## Cosa ti serve

| Attributo | Valore |
| --- | --- |
| Python | 3.9 o superiore |
| Pacchetti | `requests` per lo script semplice, `httpx` per quello asincrono |
| Chiave | `LLMWEBSEARCH_API_KEY` nel tuo ambiente |
| Per il ciclo di risposta | Qualsiasi endpoint chat compatibile con OpenAI: `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL` |
| Fatturazione | Un credito paga una ricerca: fino a 20 risultati classificati, con le pagine dei primi 3 lette e restituite come Markdown. Leggere da 4 a 10 pagine costa 2 crediti. Una chiamata che termina in errore non costa nulla |

## Come eseguo una ricerca con requests?

Costruisci un body JSON con `q` e le opzioni che ti servono, invialo in POST e cicla su `results`. Controlla `content_status` prima di toccare `content`: solo i risultati marcati `ok` contengono il testo della pagina. Lo script qui sotto salva ogni pagina letta come file Markdown con il nome della sua posizione, con l'URL di origine sulla prima riga.

*search_once.py*

```python
import os
import pathlib

import requests

resp = requests.post(
    "https://llmwebsearch.com/api/v1/search",
    headers={"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]},
    json={"q": "how to size a postgres connection pool", "max_results": 5, "read": 3},
    timeout=60,
)
resp.raise_for_status()
data = resp.json()
out = pathlib.Path("sources")
out.mkdir(exist_ok=True)
for r in data["results"]:
    if r["content_status"] != "ok":
        print("skip", r["position"], r["content_status"], r["url"])
        continue
    path = out / f"{r['position']:02d}.md"
    path.write_text("Source: " + r["url"] + "\n\n" + r["content"], encoding="utf-8")
    print("saved", path, r["word_count"], "words")
print("credits used:", data["credits_used"])

```

## Come eseguo molte ricerche insieme?

Usa `httpx.AsyncClient` e un `asyncio.Semaphore`. Il semaforo limita quante chiamate sono in corso, il che ti tiene sotto il limite al minuto del tuo account e distribuisce il carico. Da quattro a otto chiamate concorrenti vanno bene per la maggior parte degli agenti di ricerca. Ogni account ha un limite di chiamate al minuto, mostrato nella dashboard; chiedi al supporto e può essere alzato.

L'esempio invia una ricerca per sotto-domanda, un passo tipico di un agente di ricerca, e raccoglie gli URL delle pagine lette. Ogni chiamata è indipendente: un fallimento non ferma le altre.

*many_searches.py*

```python
import asyncio
import os

import httpx

API = "https://llmwebsearch.com/api/v1/search"
HEADERS = {"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]}
QUESTIONS = [
    "what is connection pooling in postgres",
    "recommended pool size per cpu core",
    "pgbouncer transaction mode limitations",
    "connection pool exhaustion symptoms",
]


async def search(client, sem, q):
    async with sem:
        resp = await client.post(API, json={"q": q, "read": 2, "max_chars": 4000})
        if resp.status_code != 200:
            return q, resp.json().get("error", {}).get("code"), []
        data = resp.json()
        read = [r["url"] for r in data["results"] if r["content_status"] == "ok"]
        return q, "ok", read


async def main():
    sem = asyncio.Semaphore(4)
    async with httpx.AsyncClient(headers=HEADERS, timeout=60) as client:
        for q, status, urls in await asyncio.gather(*(search(client, sem, q) for q in QUESTIONS)):
            print(status, q, urls)


asyncio.run(main())

```

## Come ottengo una risposta con citazioni da qualsiasi LLM?

Prima cerca, poi dai al modello le fonti numerate e chiedigli di citarle. L'API restituisce fonti, non una risposta scritta: il tuo modello legge le pagine, scrive la risposta e cita gli URL, così scegli tu il modello e lo stile di citazione. Il ciclo qui sotto funziona con qualsiasi endpoint chat che parla il formato `chat/completions` compatibile con OpenAI; imposta indirizzo, chiave e modello in tre variabili d'ambiente.

Tieni il contesto sotto controllo. Lo script riduce ogni fonte affinché il totale resti entro un budget di circa 6.000 token, contando all'incirca quattro caratteri per token. Un risultato senza contenuto viene passato come snippet, etichettato come tale, così il modello non finge di aver letto la pagina.

*answer_with_citations.py*

```python
import os

import requests

SEARCH = "https://llmwebsearch.com/api/v1/search"
KEY = os.environ["LLMWEBSEARCH_API_KEY"]
BUDGET_CHARS = 6000 * 4  # about 6,000 tokens of sources


def sources_for(question):
    resp = requests.post(SEARCH, headers={"Authorization": "Bearer " + KEY},
                         json={"q": question, "max_results": 5, "read": 3}, timeout=60)
    resp.raise_for_status()
    results = resp.json()["results"]
    share = BUDGET_CHARS // max(len(results), 1)
    blocks = []
    for i, r in enumerate(results, start=1):
        if r["content_status"] == "ok":
            body = r["content"][:share]
        else:
            body = "(snippet only) " + r["snippet"]
        blocks.append(f"[{i}] {r['title']} - {r['url']}\n{body}")
    return "\n\n".join(blocks)


def answer(question):
    prompt = ("Answer the question using only the sources below. "
              "Cite them as [1], [2] and so on. Say so if the sources do not answer it.\n\n"
              + sources_for(question) + "\n\nQuestion: " + question)
    resp = requests.post(
        os.environ["LLM_BASE_URL"].rstrip("/") + "/chat/completions",
        headers={"Authorization": "Bearer " + os.environ["LLM_API_KEY"]},
        json={"model": os.environ["LLM_MODEL"], "messages": [{"role": "user", "content": prompt}]},
        timeout=120,
    )
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]["content"]


print(answer("How should I size a Postgres connection pool?"))

```

## Quanto testo devo inviare al modello?

Il minimo che basta a rispondere alla domanda. Tre pagine ridotte a 2.000 token ciascuna di solito battono dieci pagine intere: il modello si concentra meglio e ogni risposta costa meno. Abbassa `max_chars` nella ricerca stessa quando sai che ridurrai, così viaggiano meno dati.

Ridurre dall'inizio di una pagina funziona per la maggior parte degli articoli, perché il punto principale tende a venire prima. Per le pagine di riferimento lunghe, dividi il contenuto per intestazioni e tieni le sezioni i cui titoli corrispondono alla domanda.

## Cosa deve fare il codice quando manca una pagina?

Prosegui con le altre fonti. Una pagina marcata `blocked_by_robots`, `blocked_by_site_policy` o `target_refused` non verrà letta a un secondo tentativo, quindi non ciclare su di essa. Una pagina `fetch_timeout` o `fetch_failed` potrebbe funzionare più tardi tramite `https://llmwebsearch.com/api/v1/extract`.

Gli errori HTTP sono diversi: `429`, `502` e `504` meritano un retry con una pausa crescente. La [pagina degli errori](https://llmwebsearch.com/it/docs/errors/) ha un helper di retry pronto all'uso.

## Python: domande frequenti

### Esiste un pacchetto pip per LLMWebSearch?

No. L'API è semplice HTTPS e JSON, quindi ti bastano `requests` o `httpx`. Gli script in questa pagina sono l'integrazione: copiali nel tuo progetto e adattali.

### Posso usare questi script con un modello locale?

Sì. Qualsiasi server che espone una route `chat/completions` compatibile con OpenAI funziona con il ciclo di citazioni: imposta `LLM_BASE_URL` sul suo indirizzo e `LLM_MODEL` sul nome del modello che si aspetta.

### Come conto i crediti da Python?

Somma `credits_used` di ogni risposta, oppure chiama `GET https://llmwebsearch.com/api/v1/credits` prima e dopo un job; quella chiamata è gratuita.

---

LLMWebSearch: LLMWebSearch è un'API di ricerca web per agenti AI che restituisce risultati classificati con il contenuto delle pagine principali in Markdown, pagata in criptovalute senza caricare documenti d'identità. I crediti prepagati da 1,20 USD/1K ricerche non scadono mai. Senza KYC — niente documento, niente selfie, niente documenti. Paghi in criptovalute.
