Головна / Статті

Створіть міні-допоможника ШІ з нуля за допомогою агентів, інструментів та функцій.

Невеликий проект на Python, який направляє запит до агентів, викликає інструменти для обчислень та перетворення одиниць вимірювання, запускає функцію підсумовування та надсилає оновлений запит до LLM.

Видавець: REACTAPP.TOP12 хв читання2649 слів

Джерела: Agents, Tools, and Skills for a Working Mini AI Assistant

Запитайте у чат-бота, яка була погода в Чикаго минулого тижня, і він спробує вгадати. Дайте цьому ж чат-боту інструмент, який справді перевіряє інформацію, і він знатиме правильну відповідь. Різниця між моделлю, яка лише вгадує, та системою, яка справді знає, — це те, що скорочують агенти, інструменти та навички, і це пояснює, чому генеративний ШІ став такою популярною темою в IT.

Бар’єр для входу нижчий, ніж здається. Якщо ви можете здійснити належний пошук у Google, ви можете написати хороший запит. Однак один лише запит має обмежені можливості. Цікава частина починається тоді, коли ви перестаєте очікувати, що модель знає все, і надаєте їй агентів, інструментів та навичок для роботи. Нижче ви знайдете пояснення всіх трьох елементів, а також приклад на Python, який можна запустити самостійно, створений на основі простого прикладу планування подорожей від початку до кінця.

Визначення інструментів, навичок та агентів

Інструменти зазвичай надходять ззовні, наприклад, з сервера MCP (Model Context Protocol). Якщо ви звернетеся до такого сервера, ви, як правило, отримаєте список інструментів, які допоможуть обробити ваш запит. Припустимо, ви хочете отримати інформацію про погоду у своєму запиті. Сервер MCP для погоди може надати такі інструменти, серед яких get_forecast повертає поточний прогноз. Якщо у вашому запиті йдеться про те, наскільки змінилась погода в Чикаго протягом минулого тижня, такий інструмент додасти необхідні інструкції для успішного пошуку.

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

Цей список — лише зразок того, що може пропонувати сервер інструментів. Приклад на практиці нижче замінює їх на два простіші користувацькі інструменти: калькулятор та перетворювач відстаней.

Навички більше схожі на рецепти. Якщо ви пишете для Medium, однією з ваших навичок може бути посібник зі стилю. У багатьох проєктах з обробки даних це списки розрахунків або порівнянних значень, що робить їх чимось на кшталт таблиць пошуку. Якщо запит вимагає інформації про ціни на всі рейси з району Чикаго, навичка може вказати ORD (О’Хер), MDW (Мідвей) або CHI — коди, що охоплюють усі аеропорти міста. Ось категорії вимірювань, які може містити навичка:

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)

Ще раз кажу, це загальний опис того, що може містити таблиця пошуку навички. У цьому прикладі потрібні лише рядки з відстанями, які дозволяють перетворювати значення між милями, кілометрами, метрами та футами.

Саме агенти роблять так, що процес обробки даних ШІ виглядає так, ніби він міркує. На практиці ваш запит проходить крізь кілька циклів, кожен з яких ґрунтується на попередньому. Перший запит надає системі достатньо орієнтацій для першого кроку. Потім система вирішує, яким має бути наступний крок, і продовжує цей процес стільки, скільки дозволяє код. Уявіть собі книгу типу «Виберіть свою власну пригоду»: кожен прохід може розгалужуватися в різному напрямку, щоб знайти відповідь.

Створення за допомогою коду

Архітектура навмисно проста. Ви можете писати код у Jupyter Notebook або Google Colab – це не коштує нічого та легко підлаштовується. У поясненні спочатку розглядаються окремі частини коду у зворотному порядку виконання, а наступний розділ показує, як усе працює при обробці справжнього запиту.

Крок 1: імпорти

Бібліотеки requests, getpass та os отримують ключ та викликають Groq LLM. Бібліотека re допомагає знаходити шаблони символів у запиті, і кожен інструмент та навичка залежать від неї. Вам потрібен Python 3.10 або новіша версія. Якщо requests не встановлений, спочатку виконайте !pip install requests.

import re
import os
import getpass
import requests

Крок 2: калькулятор

Регулярний вираз виділяє кожну суму в доларах у вхідних даних, а функція підсумовує їх. Він просто шукає символ $, за яким йдуть цифри, та підсумовує їх. Якщо ви працюєте з британськими фунтами, вам доведеться змінити шаблон.

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

Крок 3: інструмент для перетворення одиниць

Цей інструмент перетворює поширені одиниці вимірювання відстаней. Спочатку він нормалізує будь-яку одиницю, яку використав користувач, за допомогою словника UNIT_ALIASES, а потім перетворює кожну відстань у милі за допомогою словника DISTANCE_TO_MILES, що полегшує підсумовування. Залежно від вашої мети, ви також можете додати перетворення кілометрів чи кількості кроків.

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

Крок 4: навичка стиснення

Легкий інструмент для стиснення. Він оцінює кожне речення шляхом видалення слов-зупинок, перелічених у множині stopwords, та ранжує залишки за кількістю значущих слів. Мета — видалити речення, які нічого не додають до початкового запиту, та скоротити зайве використання елементів тексту.

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

Крок 5: підключення LLM

Тут код взаємодіє з реальною моделлю. Використовується Groq через наявність у нього безкоштовного тарифу, але для функціонування потрібен власний API-ключ з консолі 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.")

Крок 6: агенти

Саме тут налаштовуються агенти. Вони додають контекст до вашого запиту, і ви побачите, як вони використовують інструменти та навички для отримання точних даних.

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}"

Крок 7: роутер

Роутер є „мозком“ цієї системи. Він шукає певні характеристики в запиті та надсилає його до відповідного агента чи агентів. Одна деталь: умова has_extra_narrative перевіряється лише після того, як trip_planner_agent вже знайшов відповідь. Агент text_agent працює або як стандартний варіант для будь-якого запиту, або разом із планувальником поїздок, коли ця умова є істинною.

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)

Крок 8: пробування

Час перевірити все.

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")

Виконання одного запиту в системі

Візьміть цей запит та простежте кожен етап:

"День розпочався з матчу команди Cubs на стадіоні Wrigley Field (квиток за $55). Далі — поїздка на 5,5 милі до Інституту мистецтв Чикаго (вхідний квиток за $32), ще 1,5 милі до акваріуму Shedd (квиток за $20) та ще 2,3 милі до ресторану Giordano's у районі River North, де коштувала вечеря типу deep-dish за $30. Скільки всього коштував цей день та якою була загальна відстань маршруту? Бейсбол, мистецтво, морські тварини та піца зробили цей день насиченим, тож короткий огляд подорожі для щоденника був би чудовим."

Маршрут

Усе починається з маршрутизатора запитів. Якщо він помічає щось, схоже на валюту, наприклад $, разом із відстанню у милях або км, він направляє запит до планувальника поїздок, Агента 1. Далі він оцінює значення has_extra_narrative, яке запускається лише після того, як планувальник поїздок знайшов відповідний результат, та шукає додаткове речення, не пов’язане з вартістю чи відстанню. Якщо таке речення існує, маршрутизатор також викликає текстового агента, Агента 2. Коли не знаходиться ні шаблону маршруту, ні додаткового речення, текстовий агент самостійно обробляє запит. Тут обидві умови справджуються, тому працюють обидва агенти. Підсвічування запиту кольором допомагає зрозуміти, які частини запиту спричинили роботу кожного з агентів.

Планувальник поїздок викликає свої інструменти

Агент планувальника поїздок викликає як калькулятор, так і перетворювач одиниць вимірювання.

Калькулятор знаходить усі суми в доларах та повертає один рядок:

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

Перевідник одиниць вимірювання обчислює відстані та повертає:

5.5 милі, 1.5 милі та 2.3 милі дорівнюють 9.3 милі

Запит перебудовується

Обидва результати повертаються до планувальника поїздок, який використовує їх для перебудови запиту, додаючи обчислені суми до початкового тексту. Цей новий запит надсилається до Groq — штучного інтелекту, який використовується для демонстрації, і він може відповісти приблизно так:

Який чудовий день у Чикаго.

  • Витрачено: $137
  • Відстань: 9.3 милі

Зберігайте спогади та страву „deep-dish“.

Текстовий агент та узагальнювач

Після того, як перший агент завершив роботу, значення has_extra_narrative знову стало істинним, тож був активований текстовий агент, а початкове запитання надійшло до функції підсумовування. Оскільки запитання містило більше речень, ніж дозволяло обмеження у два речення цієї функції, було збережено два речення з найвищими оцінками, а середнє запитання повністю видалено. Порівняння початкового запитання з тим, що отримуємо у результаті, показує, що запитання про вартість та кілометраж зникли, а залишилися лише речення з оповіданням.

Цей запит, що містить лише оповідання, надсилається до Groq ще раз, і модель генерує ретельний щоденниковий запис без необхідності перерахунку чого-небудь. Цифри щодо доларів та кілометрів у її відповіді походять від Агента 1 на ранньому етапі обробки, а не від цього кроку, що і є суттю розподілу завдань між агентами:

Ваша денна витрата склала загалом $137, що покрило 9,3 милі.

Підсумок щоденника:

  • Гра на стадіоні Wrigley Field: $55
  • Художній інститут, за 5,5 милі: $32
  • Акваріум Shedd – ще 1,5 милі: $20
  • Ресторан Giordano's із піцою deep-dish – за 2,3 милі: $30

Бейсбол, мистецтво, морські тварини та піца з Чикаго – ідеальний склад для насиченого дня.

Чому така схема є корисною

Цей приклад показує, що агенти, навички та інструменти дозволяють системі перебудувати ваш запит у тлі, при цьому ви ніколи не бачите додаткових кроків. Маршрутизатор та агенти працюють разом, щоб визначити, які інструменти чи навички потрібні, зібрати необхідну інформацію, переформувати запит відповідно до неї та передати результат ШИ. У реальних сценаріях використання можна поєднати кілька етапів, щоб система з кожним разом, коли дізнається щось нове, покращувала та уточнювала запит.

Інструменти тут — це звичайні функції. У продакшені вони частіше знаходяться на сервері MCP, створеному хтось іншим, і готові до виклику. Скрипти також будуть трохи іншими, адже розробники зазвичай записують ці інструкції у файлі Markdown, а не у функції Python, як у прикладі вище.

Щоб почати, вам не потрібна велика команда чи важкий фреймворк. Один роутер плюс два-три невеликі інструменти та модель, яка може покладатися на них замість імпровізації, допоможуть вам розпочати. Замініть їх на свої інструменти, навички чи агентів та зробіть все це своїм, а потім створіть інструменти для будь-якої проблеми, яка у вас є.