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
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
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
**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
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.
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ę