Главная / Статьи

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

Небольшой проект на 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, как в примере выше.

Чтобы начать, вам не нужна большая команда или тяжеловесная фреймворковая среда. Один маршрутизатор плюс два-три небольших инструмента, а также модель, позволяющая опираться на них вместо импровизации, помогут вам начать работу. Замените их на свои собственные инструменты, спецификации или агенты, чтобы сделать всё по-своему, а затем создайте инструменты для решения тех проблем, с которыми вы сталкиваетесь.