Szybka odpowiedź
Jak zbudować agenta deep research w Pythonie?
Napisz pętlę, która prosi LLM o zapytania do wyszukiwania, przepuszcza każde przez API wyszukiwania w sieci zwracające tekst stron, trzyma każde źródło tylko raz, prosi o zapytania uzupełniające, a następnie pisze odpowiedź z cytatami. Z LLMWebSearch każde wyszukiwanie zwraca pierwsze 3 strony jako Markdown za jeden kredyt; poniższy skrypt wykonuje sześć wyszukiwań na pytanie. Od 1,20 USD/1K wyszukiwań, bez KYC, płatność kryptowalutą.
Co będzie robił agent?
Plan
Poproś model o cztery krótkie zapytania, które razem pokrywają pytanie.
Wyszukiwanie
Uruchom każde zapytanie z
read=3, żeby pierwsze trzy strony wróciły jako Markdown.Trzymaj źródła tylko raz
Przechowuj wyniki według URL i pomijaj strony już zebrane lub bez tekstu.
Znajdź luki
Pokaż modelowi dotychczas znalezione tytuły i poproś o dwa zapytania o to, czego brakuje.
Napisz
Ponumeruj do 12 źródeł i poproś o odpowiedź cytującą je jako [1], [2].
Czego potrzebuję przed rozpoczęciem?
Trzy rzeczy:
- Klucz API LLMWebSearch w zmiennej środowiskowej
LLMWEBSEARCH_API_KEY. Klucze pojawiają się w panelu po pierwszym pakiecie. - Dowolny endpoint czatu zgodny z OpenAI, ustawiony przez
LLM_BASE_URL,LLM_API_KEYiLLM_MODEL. Modele hostowane i lokalne serwery działają równie dobrze. - Python 3.9 lub nowszy z pakietem
requests.
Skrypt używa zwykłego HTTP dla obu usług, więc możesz podmienić model bez zmiany kodu wyszukiwania.
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}")Jak działa każda część?
plan_queries prosi model o krótkie zapytania, jedno na linię, i usuwa znaczniki listy. Krótkie zapytania lepiej pasują do tego, jak wyszukiwarki rankują strony, niż pełne pytania.
search wywołuje POST https://llmwebsearch.com/api/v1/search z max_results=5, read=3 i max_chars=5000. Odpowiedź zawiera pięć wyników; pierwsze trzy niosą tekst strony w content.
research prowadzi rundy. Trzyma źródła w słowniku kluczowanym po URL, więc strona znaleziona przez dwa zapytania liczy się raz. Zachowywane są tylko wyniki z content_status równym ok, bo odpowiedź musi opierać się na tekście, który model może przeczytać.
Po każdej rundzie model widzi dotychczas zebrane tytuły i proponuje dwa nowe zapytania o to, czego brakuje. Końcowy prompt numeruje do dwunastu źródeł i ustala zasady cytowania.
Ile kosztuje jeden raport?
Sześć wyszukiwań na pytanie przy domyślnych ustawieniach: cztery w pierwszej rundzie i dwa w drugiej. Przy read równym 3 to sześć kredytów. 25 000 wyszukiwań przy 41,25 USD pokrywa więc ponad 4 000 raportów.
Wywołania modelu kosztują dodatkowo na Twoim rachunku za LLM: trzy wywołania planujące i jedno długie wywołanie odpowiedzi na raport. Wywołanie odpowiedzi niesie do dwunastu stron po 5 000 znaków, więc obniż max_chars, jeśli Twój model jest drogi.
Jak sprawić, żeby szło głębiej?
Zmień liczby, nie strukturę:
- Więcej rund.
rounds=4pozwala modelowi ścigać jeszcze dwa zestawy luk. - Więcej zapytań na rundę. Podnieś
nw wywołaniu szukającym luk z 2 do 4. - Węższe źródła. Dodaj
include_domainsdo treści zapytania wyszukiwania, aby ograniczyć się do oficjalnych lub akademickich stron. - Tylko świeże materiały. Dodaj
"time_range": "month"lub"topic": "news". - Więcej stron na zapytanie.
read=5czyta pięć stron, po 2 kredyty za wyszukiwanie.
Dodaj regułę zatrzymania, zanim podniesiesz limity: zakończ pętlę, gdy runda przyniesie mniej niż dwa nowe źródła.
Jak zadbać o bezpieczeństwo i kulturę agenta?
Traktuj każdą stronę jako niezaufany tekst. Końcowy prompt mówi modelowi, aby traktował źródła wyłącznie jako dowody; dodaj wyraźną linię, że instrukcje zawarte w źródłach należy ignorować. Nigdy nie pozwól, aby źródło wywołało narzędzie lub zapis do pliku.
Po stronie sieci Nasz czytnik respektuje robots.txt: przedstawia się jako LLMWebSearch, przestrzega reguł, które witryna zapisuje dla niego lub dla wszystkich robotów, i pomija treść każdej strony, której nie wolno mu czytać. Strony, które odmawiają dostępu, wracają bez treści, a skrypt po prostu je pomija. Gdy witryna odrzuca nasz czytnik, wynik wraca bez treści i ze statusem witryny: nigdy nie rozwiązujemy CAPTCHA, nie logujemy się na konta ani nie obchodzimy blokady czy paywalla.
Co może pójść nie tak?
W testach wychodzą trzy rzeczy:
- Niejasne zapytania. Jeśli plan jest słaby, każda runda znajduje te same strony. Poproś model o zapytania z konkretnymi nazwami i datami.
- Zbyt mało źródeł z tekstem. Niektóre tematy są za paywallem. Poszerz zapytania lub pozwól na więcej wyników.
- Zbyt długie prompty. Dwanaście długich stron może przekroczyć kontekst małego modelu. Zmniejsz
max_charslub zostaw osiem źródeł.
Loguj każde zapytanie i zwrócone przez nie URL-e. Czytanie tego logu to najszybszy sposób na poprawę promptu planu.
Agent do pogłębionego researchu: najczęstsze pytania
Czy mogę uruchomić to na lokalnym modelu?
Tak, jeśli lokalny serwer obsługuje format OpenAI chat-completions. Ustaw LLM_BASE_URL na jego adres, a LLM_MODEL na nazwę modelu.
Czy mogę zamienić to w narzędzie MCP?
Tak. Owiń research() w narzędzie MCP albo daj swojemu agentowi zwykłe narzędzie wyszukiwania ze strony MCP i pozwól mu samodzielnie prowadzić pętlę.
Dlaczego nie zapytać search API bezpośrednio o odpowiedź?
Ponieważ LLMWebSearch nie pisze odpowiedzi. API zwraca źródła, a nie gotową odpowiedź: Twój własny model czyta strony, pisze odpowiedź i cytuje adresy URL.
Jak sprawdzić, ile kosztowało każde wyszukiwanie?
Każda odpowiedź zawiera credits_used, a GET https://llmwebsearch.com/api/v1/credits pokazuje Twoje pozostałe saldo bez żadnych kosztów.