Strona główna / Artykuły

Stwórz od zera małego asystenta AI przy użyciu agentów, narzędzi i umiejętności.

Mały projekt w Pythonie, który kieruje zapytanie do agentów, wywołuje narzędzie kalkulatora i konwertera jednostek, uruchamia funkcję podsumowywania oraz wysyła ponownie sformułowane zapytanie do LLM.

Wydawca: REACTAPP.TOP12 min czytania2649 słów

Źródła: Agents, Tools, and Skills for a Working Mini AI Assistant

Zadaj chatbotowi pytanie o to, jaka była pogoda w Chicago w zeszłym tygodniu, a on odgadnie. Daj temu samemu chatbotowi narzędzie, które faktycznie sprawdza dane, a on będzie wiedział. Różnica między modelem, który tylko zgaduje, a systemem, który wie, to właśnie to, co zamienia agenty, narzędzia i umiejętności w skuteczne rozwiązania, i to tłumaczy, dlaczego sztuczna inteligencja generatywna stała się tak często omawianym elementem IT.

Bariery wejścia są niższe, niż się wydaje. Jeśli potrafisz przeprowadzić przyzwoite wyszukiwanie w Google, możesz napisać dobry prompt. Jednak jeden prompt sam w sobie niewiele daje. Naprawdę ciekawe zaczyna się wtedy, gdy przestajesz oczekiwać, że model wie wszystko, i dajesz mu agenty, narzędzia oraz umiejętności, na które może się oprzeć. Poniżej znajdziesz wyjaśnienie wszystkich trzech elementów, wraz z przykładem programowania w Pythonie, który możesz sam uruchomić, opartym na prostym przykładzie planowania podróży od początku do końca.

Zdefiniowane narzędzia, umiejętności i agenci

Narzędzia zazwyczaj pochodzą ze zewnętrznego źródła, takiego jak serwer MCP (Model Context Protocol). Połącz się z tym serwerem i zazwyczaj otrzymasz listę narzędzi, które pomogą w obsłudze twojego zapytania. Załóżmy, że chcesz uzyskać informacje pogodowe w ramach swojego zapytania. Serwer MCP do pogody może udostępniać takie narzędzia, przy czym get_forecast zwraca aktualną prognozę. Jeśli twoje zapytanie dotyczy zmian pogody w Chicago w ciągu ostatniego tygodnia, takie narzędzie doda odpowiednie instrukcje, aby sprawdzić to zapytanie.

get_current_conditions
get_forecast
get_alerts
get_historical_weather
get_air_quality
get_marine_conditions
get_river_conditions
get_wildfire_info
search_location
check_service_status

Ta lista to jedynie przykład tego, co może oferować serwer narzędzi. Przykład praktyczny poniżej zastępuje je dwoma prostszymi, dostosowanymi narzędziami: kalkulatorem i konwerterem odległości.

Umiejętności przypominają bardziej przepisy kulinarskie. Jeśli piszesz dla Medium, jedną z twoich umiejętności może być przewodnik stylu. W wielu projektach inżynierii danych są to listy obliczeń lub porównywalnych wartości, co sprawia, że przypominają one tabele wyszukiwawcze. Jeśli prompt poprosi o ceny wszystkich lotów z obszaru Chicago, umiejętność może podać kod ORD (O’Hare), MDW (Midway) lub CHI, który obejmuje wszystkie lotniska w tym mieście. Oto rodziny miar, które może zawierać umiejętność:

Temperature = Celsius, Fahrenheit, Kelvin

Length / Distance = Inches, Feet, Yards, Miles, Millimeters, Centimeters, Meters, Kilometers

Mass / Weight = Ounces, Pounds, Stone, Grams, Kilograms, Metric Tons

Volume / Capacity = Fluid Ounces, Cups, Pints, Quarts, Gallons, Milliliters, Liters, Cubic Meters

Area =  Square Feet, Square Meters, Acres, Hectares

Speed =  Miles per Hour (mph), Kilometers per Hour (km/h), Knots, Meters per Second (m/s)

Time Zones =  UTC/GMT offsets, Daylight Saving Time (DST) transitions, Unix timestamps to human-readable dates

Storage =  Bytes, Kilobytes (KB), Megabytes (MB), Gigabytes (GB), Terabytes (TB)

Ponownie, to jest ogólny przykład tego, co może zawierać tabela wyszukiwania umiejętności. W tym przykładzie potrzebne są jedynie wiersze z odległościami, umożliwiające konwersję między milami, kilometrami, metrami i stopami.

To właśnie agenci sprawiają, że proces realizowany przez sztuczną inteligencję wygląda, jakby ta rozumowała. W praktyce Twoje polecenie przechodzi przez kilka pętli, z których każda buduje na poprzedniej. Pierwsze polecenie dostarcza systemowi wystarczających wskazówek do wykonania pierwszego kroku. Następnie system decyduje, jaki powinien być następny krok, i kontynuuje działanie tak długo, jak to pozwala kod. Pomyśl o książce typu „Wybierz własną przygodę”: każda iteracja może skręcić w innym kierunku, aby znaleźć odpowiednią odpowiedź.

Budowanie tego w kodzie

Architektura jest celowo prosta. Możesz pisać kod w notatniku Jupyter lub Google Colab – nie kosztuje to nic i łatwo jest go dostosować. Przewodnik najpierw omawia poszczególne fragmenty kodu, w odwrotnej kolejności wykonywania, a następna sekcja pokazuje, jak wszystko działa, gdy trafia do niego rzeczywisty prompt.

Krok 1: imports

Biblioteki requests, getpass i os pobierają klucz i wywołują model Groq LLM. Biblioteka re pomaga w znajdowaniu wzorców znaków w promptzie, a każde narzędzie i umiejętność od niej zależy. Potrzebujesz Pythona 3.10 lub nowszego. Jeśli requests nie jest zainstalowany, najpierw uruchom !pip install requests.

import re
import os
import getpass
import requests

Krok 2: kalkulator

Regularny wyrażenie regularne wyodrębnia każdą kwotę w dolarach z podanego tekstu, a funkcja sumuje je. Po prostu szuka znaku $ poprzedzonego cyframi i dodaje te liczby. Jeśli pracujesz z brytyjskimi funtami, będziesz musiał zmienić wzorzec.

def calculator_tool(prompt: str) -> str:
    print("    [Tool 1: Calculator] scanning prompt for dollar amounts...")
    dollar_amounts = re.findall(r'\$\s?(\d+(?:\.\d{1,2})?)', prompt)

    if not dollar_amounts:
        result = "no dollar amounts found"
        print(f"    [Tool 1: Calculator] {result}")
        return result

    values = [float(a) for a in dollar_amounts]
    total = sum(values)
    breakdown = " + ".join(f"${v:g}" for v in values)
    result = f"{breakdown} = ${total:.2f}"
    print(f"    [Tool 1: Calculator] found {len(values)} amount(s) {values} -> {result}")
    return result

Krok 3: narzędzie do konwertowania jednostek

Narzędzie to służy do konwertowania popularnych jednostek odległości. Najpierw normalizuje jednostkę użytą przez użytkownika za pomocą słownika UNIT_ALIASES, a następnie przekształca każdą odległość na mile za pomocą słownika DISTANCE_TO_MILES, co ułatwia obliczanie sumy. W zależności od Twoich potrzeb możesz dodać również konwersje kilometrów lub liczby kroków.

UNIT_ALIASES = {
    "kilometers": "km", "kilometer": "km", "km": "km",
    "mile": "miles", "miles": "miles",
    "m": "meters", "meter": "meters", "meters": "meters",
    "ft": "feet", "foot": "feet", "feet": "feet",
}

DISTANCE_TO_MILES = {"km": 0.621371, "miles": 1.0, "meters": 0.000621371, "feet": 0.000189394}

def unit_converter_tool(prompt: str) -> str:
    print("    [Tool 2: Unit Converter] scanning prompt for distance legs...")
    legs = re.findall(r'(\d+(?:\.\d+)?)\s*(kilometers?|km|miles?|meters?|feet|ft)\b',
                       prompt, re.IGNORECASE)

    if not legs:
        result = "no distances found"
        print(f"    [Tool 2: Unit Converter] {result}")
        return result

    total_miles = 0.0
    breakdown = []
    for value, unit in legs:
        value = float(value)
        unit_norm = UNIT_ALIASES.get(unit.lower(), unit.lower())
        total_miles += value * DISTANCE_TO_MILES.get(unit_norm, 1.0)
        breakdown.append(f"{value:g} {unit_norm}")
    result = f"{' + '.join(breakdown)} = {round(total_miles, 2)} miles total"
    print(f"    [Tool 2: Unit Converter] found {len(legs)} leg(s) -> {result}")
    return result

Krok 4: umiejętność streszczania

Lekki narzędzie do streszczania. Ocenia każde zdanie poprzez usunięcie słów stop wylistowanych w zbiórze stopwords i rankuje pozostałe według liczby znaczących słów. Celem jest usunięcie zdań, które nic nie dodają do treści wejściowej, oraz ograniczenie nadmiernego użycia tokenów.

def summarizer_skill(text: str, max_sentences: int = 2) -> str:
    print("    [Skill: Summarizer] scanning message for key sentence(s)...")
    sentences = [s for s in re.split(r'(?<=[.!?])\s+', text.strip()) if s]

    if len(sentences) <= max_sentences:
        print(f"    [Skill: Summarizer] only {len(sentences)} sentence(s) -- returning as-is")
        return text.strip()

    stopwords = {"the","a","an","is","are","was","were","in","on","at","to","of","and",
                 "or","for","it","this","that","i","you","he","she","they","we","really"}
    words = re.findall(r'\b\w+\b', text.lower())
    freq = {}
    for w in words:
        if w not in stopwords:
            freq[w] = freq.get(w, 0) + 1

    scored = []
    for idx, sentence in enumerate(sentences):
        s_words = re.findall(r'\b\w+\b', sentence.lower())
        score = sum(freq.get(w, 0) for w in s_words)
        scored.append((score, idx, sentence))

    top = sorted(scored, key=lambda x: x[0], reverse=True)[:max_sentences]
    top_in_order = sorted(top, key=lambda x: x[1])
    summary = " ".join(s for _, _, s in top_in_order)
    print(f"    [Skill: Summarizer] kept {len(top_in_order)} of {len(sentences)} sentence(s) -> {summary}")
    return summary

Krok 5: połączenie z LLM

Tutaj kod komunikuje się z rzeczywistym modelem. Wykorzystano Groq, ponieważ oferuje darmową wersję, ale aby to zadziałało, potrzebujesz własnego klucza API z konsoli Groq.

GROQ_API_KEY = os.environ.get("GROQ_API_KEY") or getpass.getpass(
    "Enter your free Groq API key (from https://console.groq.com/keys), "
    "or press Enter to skip: "
)

GROQ_MODEL = "openai/gpt-oss-20b"
GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"

def call_llm(augmented_prompt: str,
             system_prompt: str = "You are a helpful, concise assistant.") -> str:
    if not GROQ_API_KEY:
        return ("[No LLM reply -- no Groq API key was provided. Get a free one at "
                 "https://console.groq.com/keys, then re-run the setup cell above.]\n"
                 f"Here is the augmented prompt that would have been sent:\n\"\"\"\n{augmented_prompt}\n\"\"\"")
    try:
        response = requests.post(
            GROQ_ENDPOINT,
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {GROQ_API_KEY}",
            },
            json={
                "model": GROQ_MODEL,
                "messages": [
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": augmented_prompt},
                ],
                "temperature": 0.7,
                "max_tokens": 400,
            },
            timeout=30,
        )
        response.raise_for_status()
        data = response.json()
        return data["choices"][0]["message"]["content"].strip()
    except requests.exceptions.RequestException as e:
        return f"[LLM request failed -- {e}]"
    except (KeyError, IndexError, ValueError):
        return "[LLM returned an unexpected response format.]"

print("LLM configured." if GROQ_API_KEY else "No key entered -- running in fallback mode.")

Krok 6: agenci

Tutaj konfiguruje się agenty. Dodają one kontekst do twojego zapytania, a ty zobaczysz, jak wykorzystują narzędzia i umiejętności, aby uzyskać dokładne dane.

def show_step(step_num: int, label: str, content: str) -> None:
    """Small helper so every agent prints its pipeline the same, readable way."""
    print(f"\n  STEP {step_num} - {label}:")
    for line in str(content).splitlines() or [""]:
        print(f"    {line}")


def trip_planner_agent(prompt: str) -> str:
    """Agent 1. Condition: 2+ dollar costs AND 2+ distances -- a multi-stop itinerary."""
    print("[Router] -> Agent 1: Trip Planner Agent activated (detected an itinerary: multiple costs + multiple distances)")
    show_step(1, "Original prompt", prompt)

    cost_result = calculator_tool(prompt)
    show_step(2, "Tool result (Calculator -- total cost)", cost_result)

    distance_result = unit_converter_tool(prompt)
    show_step(3, "Tool result (Unit Converter -- total distance)", distance_result)

    augmented_prompt = (
        f"The user asked: \"{prompt}\"\n\n"
        f"A calculator tool already computed the total cost: {cost_result}\n"
        f"A distance tool already computed the total distance traveled: {distance_result}\n\n"
        "Using those two verified totals (don't redo either calculation yourself), give the "
        "user a short, friendly trip summary that reports both totals clearly."
    )
    show_step(4, "Prompt has changed -- now the augmented prompt sent to the LLM", augmented_prompt)

    reply = call_llm(augmented_prompt)
    show_step(5, "Final response from Groq", reply)

    return f"🧳 Agent 1: Trip Planner Agent:\n{reply}"


def text_agent(prompt: str) -> str:
    """Agent 2. Condition: default for any prompt, OR chained after Agent 1
    when the itinerary prompt also has an extra narrative sentence."""
    print("[Router] -> Agent 2: Text Analysis Agent activated")
    show_step(1, "Original prompt", prompt)

    summary = summarizer_skill(prompt)
    show_step(2, "Skill result (Summarizer)", summary)

    augmented_prompt = (
        f"The user wrote: \"{prompt}\"\n\n"
        f"Automatic summary of their message: {summary}\n\n"
        "Write a short, thoughtful, natural-sounding reply to the user that responds "
        "to what they actually said, informed by (but not just repeating) this summary."
    )
    show_step(3, "Prompt has changed -- now the augmented prompt sent to the LLM", augmented_prompt)

    reply = call_llm(augmented_prompt)
    show_step(4, "Final response from Groq", reply)

    return f"📝 Agent 2: Text Analysis Agent:\n{reply}"

Krok 7: router

Router jest „mózgiem” całej operacji. Szuka określonych cech w zapytaniu i kieruje je do odpowiedniego agenta lub agentów. Jeden szczegół: has_extra_narrative to warunek sprawdzany dopiero po tym, jak trip_planner_agent już dokonał dopasowania. text_agent działa albo jako standard dla każdego zapytania, albo obok planera podróży, gdy ten warunek jest spełniony.

def looks_like_itinerary(prompt: str) -> bool:
    dollar_amounts = re.findall(r'\$\s?\d+(?:\.\d{1,2})?', prompt)
    distance_legs = re.findall(r'\d+(?:\.\d+)?\s*(?:kilometers?|km|miles?|meters?|feet|ft)\b',
                                prompt, re.IGNORECASE)
    return len(dollar_amounts) >= 2 and len(distance_legs) >= 2

def has_extra_narrative(prompt: str) -> bool:
    sentences = [s for s in re.split(r'(?<=[.!?])\s+', prompt.strip()) if s]
    extra = [
        s for s in sentences
        if "quot; not in s
        and not re.search(r'\b(?:miles?|km|kilometers?|feet|ft)\b', s, re.IGNORECASE)
        and not s.strip().endswith("?")
    ]
    return len(extra) >= 1

def route_prompt(prompt: str) -> str:
    if looks_like_itinerary(prompt):
        reply = trip_planner_agent(prompt)
        if has_extra_narrative(prompt):
            print("[Router] -> also routing to Agent 2: Text Analysis Agent (extra narrative sentence detected)")
            reply += "\n\n" + text_agent(prompt)
        return reply
    else:
        return text_agent(prompt)

Krok 8: próba działania

Nadszedł czas, aby przetestować wszystko.

prompt = input("Ask me anything: ")

print("=" * 60)
print(f"PROMPT: {prompt}")
print("=" * 60)
final_answer = route_prompt(prompt)
print(f"\n  >>> RETURNED: {final_answer}")
print("-" * 60 + "\n")

Przebieg jednego zapytania w systemie

Weź to zapytanie i prześledź każdy etap:

"Dzień rozpoczął się od meczu Cubs na Wrigley Field (bilet za 55 dolarów). Następnie podjęto 5,5-milową podróż do Art Institute of Chicago (wejście za 32 dolary), potem 1,5 mili do akwarium Shedd (wejście za 20 dolarów) oraz jeszcze 2,3 mili do restauracji Giordano’s w River North, gdzie zjedzono kolację w stylu deep-dish za 30 dolarów. Ile łącznie kosztował ten dzień i jaka była długość trasy? Baseball, sztuka, życie morskie i pizza sprawiły, że to był intensywny dzień – krótki podsumowanie do dziennika podróży byłoby wspaniałe."

Trasa

Wszystko zaczyna się od routera. Jeśli wykryje coś przypominającego walutę, np. $, wraz z odległością wyrażoną w milach lub kilometrach, kieruje zapytanie do planera podróży, Agenta 1. Następnie sprawdza wartość has_extra_narrative, która jest wykonywana tylko wtedy, gdy planer podróży znalazł odpowiedni wynik, i szuka dodatkowego zdania niespowiązanego z kosztem lub odległością. Jeśli takie zdanie istnieje, router wywołuje również agenta tekstowego, Agenta 2. Gdy ani wzorzec planu podróży, ani dodatkowe zdanie nie zostaną znalezione, agent tekstowy sam zajmuje się zapytaniem. W tym przypadku obie warunki są spełnione, więc działają oba agenci. Wyświetlenie zapytania w kolorze pomaga zrozumieć, które części spowodowały aktywację każdego z nich.

Planer podróży wywołuje swoje narzędzia

Agent planera podróży wywołuje zarówno kalkulator, jak i konwerter jednostek.

Kalkulator znajduje wszystkie kwoty w dolarach i zwraca jeden ciąg znaków:

$55 + $32 + $20 + $30 = $137.00

Konwerter jednostek oblicza odległości i zwraca:

5,5 mil, 1,5 mil oraz 2,3 mil dają razem 9,3 mil

Zapytanie jest odbudowywane

Oba wyniki trafiają z powrotem do planeru podróży, który wykorzystuje je do odbudowy zapytania, dodając obliczone sumy do tekstu oryginalnego. To nowe zapytanie jest wysyłane do Groq, modelu językowego użytego w demonstracji, który może odpowiedzieć mniej więcej w ten sposób:

Cóż za wspaniały dzień w Chicago.

  • Wydane: $137
  • Odległość: 9,3 mil

Zachowaj wspomnienia oraz danie deep-dish.

Agent tekstu i podsumowujący

Gdy pierwszy agent skończył pracę, wartość has_extra_narrative znów stała się prawdziwa, więc aktywowano agent odpowiedzialny za tekst, a oryginalne polecenie trafiło do umiejętności sformułowania streszczenia. Ponieważ polecenie zawierało więcej zdań niż dopuszczalna granica dwóch zdań dla tej umiejętności, zachowano dwa zdania o najwyższej ocenie, a środkowe pytanie całkowicie pominięto. Porównanie oryginalnego polecenia z tym, które powstało w wyniku przetwarzania, pokazuje, że pytanie dotyczące kosztów i mil zniknęło, a pozostały jedynie zdania narracyjne.

To polecenie zawierające wyłącznie treść narracyjną trafia do Groq po raz drugi, a model tworzy przemyślaną refleksję w formie dziennika bez konieczności ponownych obliczeń. Wartości dotyczące dolarów i mil w jego odpowiedzi pochodziły od Agenta 1 z wcześniejszego etapu procesu, a nie z tego kroku – to właśnie ma na celu podział pracy między agentami:

Twoje codzienne wydatki wyniosły łącznie 137 dolarów, a przebyta odległość to 9,3 mili.

Podsumowanie dziennika:

  • Mecz na Wrigley Field: 55 dolarów
  • Institut Sztuki, 5,5 mil stąd: 32 dolary
  • Akwarium Shedd, 1,5 mil stąd: 20 dolarów
  • Pizza deep-dish w Giordano's, 2,3 mil stąd: 30 dolarów

Beyzbol, sztuka, morskie stworzenia i pizza z Chicago – to wszystko sprawia, że dzień jest pełen atrakcji.

Dlaczego taki model jest przydatny

Przykład pokazuje, że agenci, umiejętności i narzędzia pozwalają systemowi odbudować Twoje polecenie w tle, bez konieczności widzenia dodatkowych kroków. Router i agenci współpracują, aby określić, jakie narzędzia lub umiejętności są potrzebne, zebrać informacje, dostosować polecenie i przekazać wynik modelowi LLM. W rzeczywistych zastosowaniach można łączyć kilka rund, pozwalając systemowi wzbogacać i udoskonalać polecenie za każdym razem, gdy dowiaduje się czegoś nowego.

Narzędzia tutaj to zwykłe funkcje. W środowisku produkcyjnym częściej znajdują się na serwerze MCP stworzonym przez kogoś innego, gotowym do wywołania. Umiejętności również będą nieco inne, ponieważ programiści zazwyczaj zapisują takie instrukcje w pliku markdown, a nie w funkcji Pythona jak ta powyżej.

Aby zacząć, nie potrzebujesz dużego zespołu ani ciężkiego frameworka. Jeden router w połączeniu z dwoma lub trzema małymi narzędziami oraz model, który może na nich polegać zamiast improwizować, pozwoli ci ruszyć z miejsca. Zastąp je własnymi narzędziami, umiejętnościami lub agentami i ukształtuj to według swoich potrzeb, a następnie stwórz rozwiązania do dowolnego problemu, z którym się mierzysz.