← Zpět na články

AI

Dal jsem DeepSeekovi oči. Jak jsme to udělal.

Petr Novák 7 min čtení

Můj asistent je geniální, ale žije v minulosti

Když jsem si poprvé hrál s DeepSeekem, byl jsem nadšený. Uvažuje jako člověk, píše kód, vysvětluje složité věci líp než většina lidí, které znám. Ale pak jsem se ho zeptal na něco aktuálního - třeba na novou verzi frameworku nebo cenu nového telefonu - a on s naprostou jistotou odpověděl něco, co bylo rok staré. Nebo si to prostě vymyslel.

Není to jeho chyba. DeepSeek je model, ne prohlížeč. Jeho API je čistě generativní - dostane text a vrací text. Všechno, co se naučil v době tréninku a od té doby se pro něj svět zastavil. Když se zeptáš na cokoliv aktuálního, má dvě možnosti: odhadnout to, nebo si to vymyslet. Ani jedna není dobrá.

A tady je ten vtip: DeepSeek API sice umí function calling, ale žádné vyhledávání ti nedodá. Neexistuje žádný web_search parametr, který bys zapnul. Vyhledávání si musíš napsat sám a připojit ho jako nástroj.

Tak jsem mu dal oči. Dvě funkce: web_search, kterou si zavolá, když potřebuje aktuální informace, a fetch_url, kterou si přečte konkrétní stránku. Od té chvíle to není model, který si vymýšlí - je to model, který ví, že si to má najít. Tady je přesný postup.

Co budeš potřebovat

  • API klíč pro DeepSeek (api.deepseek.com)
  • API klíč pro Brave Search API (api.search.brave.com) - má free tier, stačí se zaregistrovat na brave.com/search/api
  • backend, který umí curl/HTTP requesty (tady PHP, princip je ale univerzální)

Krok 0: Klíče a zapojení do appky

Oba klíče drž mimo repo - v .env, který si appka načte a předá do DI kontejneru:

DEEPSEEK_API_KEY=sk-...
BRAVE_SEARCH_API_KEY=...

V Nette to vypadá jako parametr v bootstrapu:

$configurator->addDynamicParameters([
    'deepseekApiKey' => $_ENV['DEEPSEEK_API_KEY'] ?? '',
    'braveSearchApiKey' => $_ENV['BRAVE_SEARCH_API_KEY'] ?? '',
]);

a service definice v services.neon, kam se parametry předají konstruktorem třídy, co se stará o volání DeepSeeka:

services:
    - App\Services\Ai\AssistantChat(%deepseekApiKey%, %braveSearchApiKey%)

Princip je stejný v jakémkoli frameworku - API klíč nikdy natvrdo v kódu, vždy přes env proměnnou a DI/config vrstvu.

Bez těchto dvou klíčů se dá zbytek napsat a i spustit, ale web_search nástroj vrátí jen chybovou hlášku „není nakonfigurovaný". Takže tenhle krok je ve skutečnosti první, ne poslední.

Krok 1: Definice nástrojů pro model

DeepSeek používá stejný formát tool definic jako OpenAI. Každý nástroj má jméno, popis (model podle něj pozná, KDY ho použít) a JSON schema parametrů:

private const TOOLS = [
    [
        'type' => 'function',
        'function' => [
            'name' => 'web_search',
            'description' => 'Vyhledá aktuální informace na internetu (fakta, čísla, novinky). '
                . 'Použij, když si nejsi jistý přesností faktu.',
            'parameters' => [
                'type' => 'object',
                'properties' => [
                    'query' => ['type' => 'string', 'description' => 'Vyhledávací dotaz'],
                ],
                'required' => ['query'],
                'additionalProperties' => false,
            ],
            'strict' => true,
        ],
    ],
    [
        'type' => 'function',
        'function' => [
            'name' => 'fetch_url',
            'description' => 'Stáhne a přečte textový obsah konkrétní webové stránky.',
            'parameters' => [
                'type' => 'object',
                'properties' => [
                    'url' => ['type' => 'string', 'description' => 'Plná URL stránky (https://…)'],
                ],
                'required' => ['url'],
                'additionalProperties' => false,
            ],
            'strict' => true,
        ],
    ],
];

Popis (description) je nejdůležitější řádek celé definice - je to jediná informace, podle které se model rozhoduje, jestli a kdy nástroj zavolá. Piš ho konkrétně, ideálně i s „POVINNĚ použij, když…".

Proč to celé děláš: před a po

Bez nástrojů model odpoví takhle:

Uživatel: Kolik stojí nejnovější iPhone?

Asistent: Podle mých informací stojí nejnovější model iPhone přibližně 25 000 Kč. Ceny se mohou lišit podle prodejce.

(Ve skutečnosti: rok stará data, která si model pamatuje z tréninku.)

S nástroji:

Uživatel: Kolik stojí nejnovější iPhone?

Asistent: (zavolá web_search, dostane výsledky, případně si přečte konkrétní stránku)

Asistent: Nejnovější iPhone 17 Pro stojí v České republice od 29 990 Kč. Ceny podle oficiálního Apple Store: apple.com/cz.

Ten rozdíl je celý smysl tohohle článku.

Krok 2: Request na DeepSeek s nástroji

Do payloadu přidáš tools a necháš stream klidně na false pro první verzi:

$payload = [
    'model' => 'deepseek-v4-flash',
    'messages' => $conversation,
    'max_tokens' => 12000,
    'temperature' => 0.7,
    'tools' => self::TOOLS,
];

$ch = curl_init('https://api.deepseek.com/chat/completions');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $apiKey,
    ],
]);
$response = curl_exec($ch);
$message = json_decode($response, true)['choices'][0]['message'];

Odpověď má buď content (text), nebo tool_calls (pole požadavků na nástroje). Nikdy obojí naráz jako finální odpověď - pokud přijde tool_calls, text ještě nepovažuj za hotový.

Krok 3: Smyčka - zavolej nástroj, vrať výsledek, opakuj

Tohle je jádro celého mechanismu. Model může chtít nástroj vícekrát po sobě (např. vyhledat, pak načíst konkrétní stránku z výsledků), takže to musí být cyklus s pojistkou proti nekonečné smyčce:

for ($round = 0; $round < 20; $round++) {
    $message = $this->callDeepSeek($conversation);

    $toolCalls = $message['tool_calls'] ?? null;
    if (!$toolCalls) {
        return $message['content']; // hotovo, model už jen odpověděl textem
    }

    $conversation[] = $message; // assistant zpráva s tool_calls
    foreach ($toolCalls as $call) {
        $conversation[] = [
            'role' => 'tool',
            'tool_call_id' => $call['id'],
            'content' => $this->runTool($call), // spustí web_search/fetch_url
        ];
    }
    // pokračuj v cyklu — model dostane výsledky a rozhodne se dál
}

Strop 20 kol je pojistka - bez ní by se model teoreticky mohl zaseknout ve vyhledávání dokola.

Krok 4: Samotné volání Brave API

private function braveSearch(string $query): string
{
    $url = 'https://api.search.brave.com/res/v1/web/search?'
        . http_build_query(['q' => $query, 'count' => 10]);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTPHEADER => [
            'Accept: application/json',
            'X-Subscription-Token: ' . $this->braveApiKey,
        ],
    ]);
    $response = curl_exec($ch);
    curl_close($ch);

    $results = json_decode($response, true)['web']['results'] ?? [];
    $lines = [];
    foreach (array_slice($results, 0, 10) as $r) {
        $lines[] = ($r['title'] ?? '') . "\n" . ($r['url'] ?? '') . "\n" . strip_tags($r['description'] ?? '');
    }
    return implode("\n\n", $lines); // tenhle text jde zpátky modelu jako obsah tool zprávy
}

Výsledek nástroje se modelu vrací jako obyčejný text v poli content - žádná speciální struktura. Model si z něj sám vytáhne, co potřebuje.

Krok 5: fetch_url a bezpečnost

Když necháš model, ať sám zvolí URL k načtení, musíš ošetřit SSRF - jinak ti model (nebo někdo přes model) může nechat backend stahovat interní adresy (localhost, 169.254.x.x, privátní rozsahy):

private function isSafeToFetch(string $url): bool
{
    if (!preg_match('~^https?://~i', $url)) {
        return false;
    }
    $host = parse_url($url, PHP_URL_HOST);
    $ip = filter_var($host, FILTER_VALIDATE_IP) ? $host : gethostbyname($host);
    return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
}

FILTER_FLAG_NO_PRIV_RANGE a FILTER_FLAG_NO_RES_RANGE odfiltrují privátní a rezervované IP rozsahy. Kontroluj to VŽDY před samotným curl requestem na fetch_url.

Tohle je chvíle, kdy ti model může ublížit - ne ze zlosti, ale protože nemá tušení, co je „uvnitř" a co je veřejný internet. Bezpečnostní kontrolu ber jako součást nástroje, ne jako volitelné vylepšení.

Krok 6: Streamovaná verze Volitelné, ale doporučené

Pro chat UI chceš vidět text psát se postupně, ne čekat na celou odpověď. Rozdíl je v stream => true a parsování SSE (server-sent events) místo jednoho JSON response. Klíčová věc: pokud model zrovna volá nástroj, žádný text se nestreamuje (delta obsahuje jen tool_calls), takže spojení může dlouho „mlčet". Řešení je curl heartbeat - pošli si pravidelný impuls přes XFERINFOFUNCTION, ať proxy/webserver spojení nezabije jako neaktivní:

CURLOPT_NOPROGRESS => false,
CURLOPT_XFERINFOFUNCTION => function (...$args) use (&$last, $onHeartbeat) {
    if (time() - $last >= 15) {
        $last = time();
        $onHeartbeat(); // např. pošle prázdný SSE komentář klientovi
    }
    return 0;
},

Krok 7: Strukturované zdroje v odpovědi

Ať se odkazy, na které se model odkazuje, nezamotají do textu, dej modelu instrukci v system promptu, ať je na konec odpovědi napíše ve fixním formátu:

Pokud cituješ webové zdroje, na konec odpovědi vždy přidej sekci:
ZDROJE:
- [Název zdroje](https://...)
- [Další zdroj](https://...)

Na backendu ji pak vytáhneš regexem a z textu smažeš, ať zůstane čistý.

Co si z toho odnést

Model není chytrý proto, že ví všechno. Je chytrý proto, že ví, kdy si to má najít.

Asistent má teď přístup k aktuálním informacím, umí si sám říct o vyhledání a pokračovat v odpovědi bez ručního zásahu. A celé to stojí na dvou API klíčích, jedné smyčce a pár řádcích kódu - žádná magie, jen function calling dotažený do konce.

Příště: jak z DeepSeeka dostat přímou odpověď

Řeč bude o tom, jak zneškodnit jeho bezpečnostní mechanismy a přimět ho odpovídat i na přísně zakázaná témata, jako je detailní popis sebevražd či podobně citlivý obsah.

#AI #deepseek

Ohodnoť článek

Přihlas se a dej mu hvězdičky — zabere to pár vteřin.

Přihlásit se

Diskuze 0

Zatím tu nikdo nediskutuje. Buď první.

Přidej se do diskuze

Komentovat můžou přihlášení čtenáři — účet zabere minutu.

Přihlásit se