Галоўная / Артыкулы

Міні-асістэнт з нуля: агенты, інструменты і навыкі

Невялікі праект на 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 (O’Hare), MDW (Midway) або 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, LLM, які вікорыстовуецца для дэмонстраціі, і можа відповісты такімі словамі:

Які чудовій дэнь у Чыкаго.

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

Зберэжыць спогады і глібокі страві.

Пасля таго, як першы агент завершыў сваю працу, значэнне has_extra_narrative зноў стала правдзівым, таму быў актывацый тэкстовы агент, а первоначальны запіт быў перадан у мастацтво для стварэння паспакты. Паколькі запіт меў больш за два рэчэнні, чым дазволена межа для гэтага мастацтва, былі застаўлены два рэчэнні з найвышымі баламі, а серэдняе запітанне было выключана. Порэшанне перадваротнага запіту з тым, што выйшла паспакта, паказвае, што запытанне пра косць і кіламетры знікло, а засталіся толькі рэчэнні з наратываў.

Гэты запіт, які містыць толькі наратыв, зноў падае ў Groq, і модэль стварывае розсудлыя запісы з дзенніка без падтрымкі новых расчытак. Цыфры ў доларах і кіламетрах у яго адпаведзі прыйшлі з Агента 1 раней у працэсе апрацоўкі, а не з гэтага крока, і самэ гэта ёсць сенсам раздзеленыя працы між агентамі:

Ваша дзеньовая косць склала ў суме $137, а праўернуты расстоек — 9.3 мілі.

Падзець дневніка:

  • Мячовая гра на Wrigley Field: $55
  • Музей мастацтва, 5,5 мілья ўдаляў: $32
  • Акварыум Shedd, 1,5 мілья ўдаляў: $20
  • Піцца deep-dish у Giordano's, 2,3 мілья ўдаляў: $30

Бейсбол, мастацтва, морскія стварэння і піцца з Чыкаго — усё гэта стварае насыцены дзень.

Чаму такі падход здаецца корыстным

Этот прыклад паказвае, што агенты, навыкі і інструменты дазволяюць сістэме перзбудаваць ваш запіт у тылу, пры чым вы ніколі не бачыце дадатковых крокаў. Маршрутызатор і агенты разам вяршаюць працу, каб выбраць неабходныя інструменты чыў навыкі, збраць факты, пераформатаваць запіт на ўзмацнення іх і перадаць рэзультат LLM. У рэальных застосоўваннях вы моглі б адмашчваць кальцы, дазволяючы сістэме абагачаць і удосконаляць запіт з кожным разам, калі яна вучыцца чагось новага.

Інструменты, які тут показаны, — гэта простыя функцыі. У рэальных умовах яны часташ заўсёды будуць розмешаны на серверэ MCP, створанам кім-небудзь іншым, і будуць готавыя да выклікання. Навыкі таксама будуць аднакоўскія, адтолькі распрацоўшчыкаў зазвычай пішуць такія інструкцыі у файле markdown, а не ў функцыі на Python, як тая, што паказана вышэй.

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