Autonomiczny agent AI — jak go zbudować krok po kroku

Model językowy to nie agent. Model językowy przyjmuje tekst i zwraca tekst. Agent przyjmuje cel, dobiera narzędzia, wykonuje kroki i zatrzymuje się, gdy cel jest osiągnięty — albo gdy wie, że nie może go osiągnąć. Różnica jest fundamentalna i ma bezpośrednie przełożenie na to, jak projektujesz system.

Ten artykuł przeprowadzi Cię przez budowę działającego agenta w TypeScript z Anthropic SDK — od instalacji po konkretny przykład agenta monitorującego stronę i wysyłającego alerty.

Co sprawia, że agent jest "autonomiczny"

Autonomia agenta wynika z pętli decyzyjnej, nie z możliwości modelu. Zwykłe wywołanie Claude to: prompt → odpowiedź. Koniec. Agent to: cel → [wybór narzędzia → wykonanie → wynik → kolejna decyzja] → zakończenie.

Kluczowe elementy tej pętli:

1. **Tool use** — model może zadeklarować chęć użycia narzędzia (filesystem, HTTP, baza danych) i otrzymać wynik z powrotem 2. **Stan konwersacji** — historia wiadomości jest przekazywana przy każdym wywołaniu, model "widzi" co już zrobił 3. **Warunek stopu** — model zatrzymuje się sam (odpowiedź bez tool_use) albo wymuszasz limit kroków z zewnątrz

Bez pętli masz LLM. Z pętlą masz agenta.

Instalacja Anthropic SDK

```bash npm install @anthropic-ai/sdk ```

Ustaw klucz API jako zmienną środowiskową:

```bash export ANTHROPIC_API_KEY=sk-ant-... ```

Inicjalizacja klienta:

```typescript import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic(); // Klucz pobierany automatycznie z ANTHROPIC_API_KEY ```

Szkielet agenta z pętlą tool_use

Poniżej jest minimalne, działające jądro agenta. To ten wzorzec — nie model — jest tym, co odróżnia agenta od pojedynczego wywołania API.

```typescript import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

type Tool = Anthropic.Tool; type MessageParam = Anthropic.MessageParam;

async function runAgent( goal: string, tools: Tool[], executor: (name: string, input: Record) => Promise, maxSteps = 20 ) { const messages: MessageParam[] = [ { role: 'user', content: goal } ];

for (let step = 0; step < maxSteps; step++) { const response = await client.messages.create({ model: 'claude-opus-4-5', max_tokens: 4096, tools, messages, });

// Model zakończył bez użycia narzędzia — gotowe if (response.stop_reason === 'end_turn') { const lastBlock = response.content.find(b => b.type === 'text'); return lastBlock?.type === 'text' ? lastBlock.text : null; }

// Model chce użyć narzędzi if (response.stop_reason === 'tool_use') { // Dodaj odpowiedź modelu do historii messages.push({ role: 'assistant', content: response.content });

// Wykonaj każde narzędzie i zbierz wyniki const toolResults: Anthropic.ToolResultBlockParam[] = [];

for (const block of response.content) { if (block.type !== 'tool_use') continue;

let result: unknown; try { result = await executor(block.name, block.input as Record); } catch (err) { result = `Błąd: ${err instanceof Error ? err.message : String(err)}`; }

toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result), }); }

// Dodaj wyniki narzędzi do historii messages.push({ role: 'user', content: toolResults }); } }

throw new Error(`Agent przekroczył limit ${maxSteps} kroków`); } ```

To cały rdzeń agenta. Reszta to definicje narzędzi i logika ich wykonania.

Projektowanie narzędzi

Narzędzia to interfejs między modelem a światem zewnętrznym. Każde narzędzie ma nazwę, opis (który model czyta, żeby zdecydować, kiedy go użyć) i schemat parametrów w formacie JSON Schema.

**Narzędzie HTTP fetch:**

```typescript const fetchTool: Anthropic.Tool = { name: 'fetch_url', description: 'Pobiera zawartość strony WWW pod podanym URL. Zwraca HTML lub JSON jako tekst.', input_schema: { type: 'object' as const, properties: { url: { type: 'string', description: 'Pełny URL do pobrania, np. https://example.com', }, headers: { type: 'object', description: 'Opcjonalne nagłówki HTTP', }, }, required: ['url'], }, };

async function executeFetch(input: { url: string; headers?: Record }) { const response = await fetch(input.url, { headers: input.headers }); const text = await response.text(); // Przycinamy, żeby nie przekroczyć okna kontekstu return { status: response.status, body: text.slice(0, 8000), }; } ```

**Narzędzie zapytania do bazy danych:**

```typescript const dbQueryTool: Anthropic.Tool = { name: 'query_database', description: 'Wykonuje zapytanie SELECT do bazy danych. Zwraca tablicę wyników.', input_schema: { type: 'object' as const, properties: { query: { type: 'string', description: 'Zapytanie SQL SELECT. Tylko SELECT — INSERT/UPDATE/DELETE są zablokowane.', }, }, required: ['query'], }, }; ```

**Narzędzie zapisu do pliku:**

```typescript const writeFileTool: Anthropic.Tool = { name: 'write_file', description: 'Zapisuje tekst do pliku na dysku lokalnym.', input_schema: { type: 'object' as const, properties: { path: { type: 'string', description: 'Ścieżka pliku' }, content: { type: 'string', description: 'Zawartość do zapisania' }, }, required: ['path', 'content'], }, }; ```

Opis narzędzia jest krytyczny — to on decyduje, kiedy i jak model go użyje. Bądź konkretny: co narzędzie robi, jakie są ograniczenia, co zwraca.

Wzorzec pętli: message → tool_use → tool_result → message

Przepływ wiadomości w agencie wygląda tak:

1. `user`: cel ("sprawdź status strony X i wyślij alert jeśli jest down") 2. `assistant`: blok `tool_use` — model deklaruje, że chce wywołać `fetch_url` 3. `user`: blok `tool_result` — wynik wykonania fetch (status HTTP, nagłówki, body) 4. `assistant`: kolejny `tool_use` albo odpowiedź tekstowa (koniec pętli)

Historia rośnie przy każdym kroku. Przy długich agentach musisz pilnować okna kontekstu — Claude Opus ma 200k tokenów, ale każdy krok kosztuje. Przycinaj body HTTP, nie przekazuj pełnych odpowiedzi API jeśli model potrzebuje tylko konkretnego pola.

Obsługa błędów i warunki stopu

Dwa typy błędów do obsłużenia:

**Błąd wykonania narzędzia.** Jeśli fetch się nie uda (timeout, DNS error), zwróć opis błędu jako wynik tool_result — nie rzucaj wyjątku w pętli agenta. Model "widzi" błąd i może zdecydować o retry albo innym podejściu.

**Nieskończona pętla.** Zawsze ustaw `maxSteps`. Model może utknąć w pętli retry przy nieosiągalnym celu. Limit kroków to bezpiecznik.

```typescript // Błąd narzędzia zwracamy jako wynik, nie wyjątek try { result = await executor(block.name, block.input); } catch (err) { result = { error: err instanceof Error ? err.message : 'Nieznany błąd' }; } ```

Dodatkowy warunek stopu: możesz przekazać narzędzie `finish` — model wywołuje je explicite, gdy uzna zadanie za zakończone. To bardziej kontrolowane niż poleganie tylko na `end_turn`.

Przykład: agent monitorujący stronę i wysyłający alerty

Poniżej kompletny, działający agent, który sprawdza dostępność URL i loguje alert jeśli status != 200.

```typescript import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

const tools: Anthropic.Tool[] = [ { name: 'check_url', description: 'Sprawdza dostępność URL. Zwraca status HTTP i czas odpowiedzi.', input_schema: { type: 'object' as const, properties: { url: { type: 'string' }, }, required: ['url'], }, }, { name: 'send_alert', description: 'Wysyła alert o problemie. Loguje do konsoli (w produkcji: email/Slack).', input_schema: { type: 'object' as const, properties: { message: { type: 'string', description: 'Treść alertu' }, severity: { type: 'string', enum: ['low', 'medium', 'high'] }, }, required: ['message', 'severity'], }, }, ];

async function executeMonitorTool( name: string, input: Record ): Promise { if (name === 'check_url') { const start = Date.now(); try { const res = await fetch(input.url as string, { signal: AbortSignal.timeout(10000) }); return { status: res.status, latencyMs: Date.now() - start }; } catch (err) { return { error: String(err), latencyMs: Date.now() - start }; } }

if (name === 'send_alert') { const { message, severity } = input as { message: string; severity: string }; console.error(`[ALERT:${severity.toUpperCase()}] ${message}`); // Tu: integracja z Resend / Slack Webhook / PagerDuty return { sent: true }; }

throw new Error(`Nieznane narzędzie: ${name}`); }

// Uruchomienie agenta const urlsToCheck = ['https://mkmlabs.pl', 'https://example.com']; const goal = `Sprawdź dostępność następujących URL: ${urlsToCheck.join(', ')}. Dla każdego URL użyj narzędzia check_url. Jeśli status HTTP != 200 lub wystąpił błąd, użyj send_alert z odpowiednim komunikatem i severity. Na końcu podsumuj wyniki.`;

// runAgent to funkcja zdefiniowana wcześniej // runAgent(goal, tools, executeMonitorTool).then(console.log); ```

Ten agent można uruchamiać cronowo (cron job, Trigger.dev scheduled task) i mieć monitoring strony za cenę kilku centów miesięcznie.

Kiedy NIE używać agentów

Agenty mają sens, gdy zadanie wymaga wielu kroków decyzyjnych z danymi ze świata zewnętrznego. Nie używaj ich do:

**Prostego RAG.** Jeśli masz bazę wiedzy i chcesz odpowiadać na pytania — wektorowe wyszukiwanie + jedno wywołanie Claude jest tańsze, szybsze i prostsze. Agent to overhead.

**Klasyfikacji one-shot.** Jeśli wejście jest znane i wystarczy jedna odpowiedź — po prostu wywołaj model bez pętli.

**Ustrukturyzowanej ekstrakcji.** Wyciąganie danych z dokumentu do JSON? Użyj `tool_choice: { type: 'any' }` z jednym narzędziem do ekstrakcji. Nie potrzebujesz pętli agenta.

Reguła: agent jest potrzebny gdy nie wiesz z góry, ile kroków i jakich narzędzi będzie potrzebować model do osiągnięcia celu.

Koszt i latency

Każdy krok agenta to wywołanie API — koszt i czas. Przy Claude Opus (najsilniejszy model) każdy krok to ~0.015 USD za 1000 tokenów wejściowych + koszt wyników narzędzi. Agent z 10 krokami i historią rosnącą do 8k tokenów na krok to ~1.2 USD za jedno uruchomienie.

Praktyczne optymalizacje:

- Używaj Claude Haiku do prostych kroków (sprawdzenie warunku, formatowanie), Opus tylko do skomplikowanych decyzji - Przycinaj wyniki narzędzi do minimum potrzebnego modelowi - Cache prompt systemowy (`cache_control: { type: 'ephemeral' }`) jeśli jest długi i stały - Ustaw limit tokenów w `max_tokens` adekwatny do kroku — nie 4096 wszędzie

Latency: każde wywołanie API to 1–5 sekund. Agent z 10 krokami to 10–50 sekund czekania. Dla użytkownika interaktywnego to za długo — streamuj wyniki lub uruchamiaj w tle z powiadomieniem po zakończeniu.

Podsumowanie

Agent AI to wzorzec architektoniczny, nie magia. Pętla tool_use z Anthropic SDK ma 30–50 linii kodu. Reszta to projektowanie narzędzi, obsługa błędów i decyzja o tym, kiedy agenta nie używać.

Zacznij od jednego prostego agenta z dwoma narzędziami. Dodaj logowanie każdego kroku. Potem skaluj złożoność — więcej narzędzi, równoległe wykonanie tool_use (Claude może wywołać kilka narzędzi jednocześnie), hierarchia agentów (orchestrator + sub-agenty). Ale fundamentem zawsze jest ta sama pętla.

agent AI Anthropic Claude TypeScript tool use Anthropic SDK
Udostępnij:

Potrzebujesz podobnego rozwiązania?

Porozmawiajmy o Twoim projekcie

Pierwsza rozmowa jest bezpłatna. Opisz nam swój pomysł — odpowiemy w ciągu jednego dnia roboczego.

Umów bezpłatną rozmowę
Wróć do wszystkich artykułów