# Python-voorbeelden: zoeken, zoekopdrachten parallel uitvoeren, antwoorden met citaten

> Python web search API-voorbeelden: requests, async httpx met semaphore en loop die antwoordt met citaten voor elke LLM binnen tokenbudget, zonder KYC.

- URL: https://llmwebsearch.com/nl/docs/python/
- Laatst bijgewerkt: 2026-10-22
- Uitgever: LLMWebSearch (https://llmwebsearch.com)

Drie scripts dekken wat de meeste Python-agents nodig hebben: één zoekopdracht met `requests`, veel zoekopdrachten tegelijk met `httpx` en een semaphore, en een korte loop die de bronnen aan een taalmodel geeft en een antwoord met genummerde citaten terugkrijgt.

**Snel antwoord — Hoe roep ik een web search API voor AI-agents aan vanuit Python?**

POST de zoekopdracht naar `https://llmwebsearch.com/api/v1/search` met `requests` of `httpx` en een Bearer-sleutel. **LLMWebSearch** geeft gerangschikte resultaten terug met de top 3 pagina's als Markdown, die je kunt nummeren en aan elke LLM kunt geven om met citaten te antwoorden. Eén zoekopdracht kost 1 credit, vanaf US$ 1,20/1K zoekopdrachten per oktober 2026. Python 3.9+ en geen SDK. Zonder KYC; betalen met crypto.

## Wat je nodig hebt

| Attribuut | Waarde |
| --- | --- |
| Python | 3.9 of nieuwer |
| Packages | `requests` voor het eenvoudige script, `httpx` voor het async-script |
| Sleutel | `LLMWEBSEARCH_API_KEY` in je omgeving |
| Voor de antwoordloop | Elk OpenAI-compatibel chat-endpoint: `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL` |
| Facturering | Eén credit betaalt één zoekopdracht: tot 20 gerangschikte resultaten, waarbij de pagina's van de eerste 3 worden gelezen en als Markdown teruggegeven. Het lezen van 4 tot 10 pagina's kost 2 credits. Een call die op een fout eindigt, kost niets |

## Hoe voer ik één zoekopdracht uit met requests?

Bouw een JSON-body met `q` en de opties die je nodig hebt, post die, en loop over `results`. Controleer `content_status` voordat je `content` aanraakt: alleen resultaten gemarkeerd met `ok` bevatten paginatekst. Het onderstaande script slaat elke gelezen pagina op als Markdown-bestand, genoemd naar zijn positie, met de bron-URL op de eerste regel.

*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"])

```

## Hoe voer ik veel zoekopdrachten tegelijk uit?

Gebruik `httpx.AsyncClient` en een `asyncio.Semaphore`. De semaphore begrenst hoeveel aanroepen er tegelijk lopen, wat je onder de limiet per minuut van je account houdt en de belasting spreidt. Vier tot acht gelijktijdige aanroepen passen bij de meeste research-agents. Elk account heeft een calllimiet per minuut, te zien in het dashboard; vraag het supportteam en de limiet kan worden verhoogd.

Het voorbeeld stuurt één zoekopdracht per subvraag, een typische stap van een research-agent, en verzamelt de URL's van de gelezen pagina's. Elke aanroep is onafhankelijk: één fout stopt de andere niet.

*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())

```

## Hoe krijg ik een antwoord met citaten van elke LLM?

Zoek eerst, geef het model daarna genummerde bronnen en vraag het ze te citeren. De API geeft bronnen terug, geen geschreven antwoord: je eigen model leest de pagina's, schrijft het antwoord en citeert de URL's, zodat jij het model en de citatiestijl kiest. De onderstaande loop werkt met elk chat-endpoint dat het OpenAI-compatibele `chat/completions`-formaat spreekt; stel het adres, de sleutel en het model in via drie omgevingsvariabelen.

Houd de context onder controle. Het script trimt elke bron zodat het totaal binnen een budget van ongeveer 6.000 tokens blijft, met ruwweg vier tekens per token. Een resultaat zonder inhoud wordt als zijn snippet doorgegeven, als zodanig gelabeld, zodat het model niet doet alsof het de pagina heeft gelezen.

*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?"))

```

## Hoeveel tekst moet ik naar het model sturen?

Zo weinig als nodig is om de vraag te beantwoorden. Drie pagina's getrimd naar 2.000 tokens elk verslaan meestal tien volledige pagina's: het model focust beter en elk antwoord kost minder. Verlaag `max_chars` in de zoekopdracht zelf als je weet dat je gaat trimmen, zodat er minder data onderweg is.

Trimmen vanaf het begin van een pagina werkt voor de meeste artikelen, omdat het hoofdpunt meestal vooraan staat. Voor lange referentiepagina's splits je de inhoud op koppen en houd je de secties waarvan de titels bij de vraag passen.

## Wat moet de code doen als een pagina ontbreekt?

Ga verder met de andere bronnen. Een pagina gemarkeerd met `blocked_by_robots`, `blocked_by_site_policy` of `target_refused` wordt bij een tweede poging niet gelezen, dus loop er niet op door. Een pagina met `fetch_timeout` of `fetch_failed` kan later wel werken via `https://llmwebsearch.com/api/v1/extract`.

HTTP-fouten zijn anders: `429`, `502` en `504` verdienen een retry met een groeiende pauze. De [errors-pagina](https://llmwebsearch.com/nl/docs/errors/) heeft een kant-en-klare retry-helper.

## Python: veelgestelde vragen

### Is er een pip-package voor LLMWebSearch?

Nee. De API is gewoon HTTPS en JSON, dus je hebt alleen `requests` of `httpx` nodig. De scripts op deze pagina zijn de integratie: kopieer ze naar je project en pas ze aan.

### Kan ik deze scripts met een lokaal model gebruiken?

Ja. Elke server die een OpenAI-compatibele `chat/completions`-route aanbiedt, werkt met de citatielus: stel `LLM_BASE_URL` in op het adres ervan en `LLM_MODEL` op de modelnaam die hij verwacht.

### Hoe tel ik credits vanuit Python?

Tel `credits_used` van elke response bij elkaar op, of roep `GET https://llmwebsearch.com/api/v1/credits` aan vóór en na een taak; die aanroep is gratis.

---

LLMWebSearch: LLMWebSearch is een webzoek-API voor AI-agents die gerangschikte resultaten teruggeeft met de inhoud van de belangrijkste pagina's als Markdown, betaald met crypto zonder ID-upload. Voorafbetaalde credits vanaf US$ 1,20/1K zoekopdrachten verlopen nooit. Zonder KYC — geen ID, geen selfie, geen documenten. Betalen met crypto.
