← Your First AI Agent: From Zero to Autonomy
02 / 07 · 45 min

Your First Agent in 50 Lines

We build a minimal working agent in Python: a while loop, two tools, a call dispatcher, and honest stop conditions.

Lesson content is currently available in Russian.

What you will learn

  • →Собирать полный агентный цикл на чистом Python без фреймворков.
  • →Подключать инструменты через словарь-диспетчер и JSON-схемы.
  • →Реализовывать условия остановки: финальный ответ и потолок шагов.

Скелет: описания инструментов и сами функции

Агент состоит из трёх частей: описания инструментов для модели, реальных функций и цикла. Начнём с первых двух. Описание — это JSON-схема, которую провайдер передаст модели; функция — обычный Python. Держите их рядом, чтобы схема не расходилась с кодом: рассинхрон между схемой и сигнатурой — самый частый баг первых агентов.

import json
from openai import OpenAI

client = OpenAI()

def get_weather(city: str) -> str:
    fake = {"Berlin": "+19, ясно", "Kyiv": "+24, гроза"}
    return fake.get(city, f"нет данных по городу {city}")

def calculator(expression: str) -> str:
    allowed = set("0123456789+-*/(). ")
    if not set(expression) <= allowed:
        return "ошибка: недопустимые символы"
    return str(eval(expression))  # только для учебной песочницы!

TOOLS = [
    {"type": "function", "function": {
        "name": "get_weather",
        "description": "Текущая погода в городе. Аргумент city — название на английском.",
        "parameters": {"type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"]}}},
    {"type": "function", "function": {
        "name": "calculator",
        "description": "Считает арифметическое выражение, например (2+3)*4.",
        "parameters": {"type": "object",
            "properties": {"expression": {"type": "string"}},
            "required": ["expression"]}}},
]

REGISTRY = {"get_weather": get_weather, "calculator": calculator}

Обратите внимание на две детали. Описания написаны как документация для стажёра: что делает функция и в каком формате аргумент — модель прочитает ровно это и ничего больше. REGISTRY — словарь «имя → функция» — это диспетчер: он избавляет от цепочки if/elif и делает добавление третьего инструмента однострочным. Заглушка погоды и «калькулятор» на eval здесь сознательно примитивны: сегодня цель — цикл, а не инструменты; настоящие контракты инструментов — тема следующего урока.

Почему схема выглядит именно так? Формат parameters — это JSON Schema: type задаёт тип значения, properties перечисляет поля, required — обязательные из них. Провайдер проверит аргументы по схеме до того, как они попадут к вам, но полагаться только на это нельзя: модель вполне может прислать город кириллицей, хотя описание просит английский, — для неё описание аргумента рекомендация, а не гарантия. Нормализация и валидация аргументов внутри самой функции — ваша обязанность, а не паранойя.

Цикл: решение, исполнение, наблюдение

Теперь сердце агента — цикл. Модель либо возвращает вызовы инструментов, либо текст; текст означает, что работа закончена. История начинается с system-сообщения — постоянных правил поведения — и задачи пользователя; дальше она пополняется только внутри цикла. Каждая итерация — один «шаг» агента: единственный вызов модели плюс исполнение всех инструментов, которые она запросила на этом шаге.

def run_agent(task: str, max_steps: int = 10) -> str:
    messages = [
        {"role": "system", "content": "Ты аккуратный ассистент. "
            "Пользуйся инструментами, не выдумывай данные."},
        {"role": "user", "content": task},
    ]
    for step in range(max_steps):
        response = client.chat.completions.create(
            model="gpt-4o-mini", messages=messages, tools=TOOLS)
        msg = response.choices[0].message
        if not msg.tool_calls:          # текст = финальный ответ
            return msg.content
        messages.append(msg)             # намерение — в историю
        for call in msg.tool_calls:
            fn = REGISTRY[call.function.name]
            args = json.loads(call.function.arguments)
            print(f"[{step}] {call.function.name}({args})")
            result = fn(**args)
            messages.append({"role": "tool",
                "tool_call_id": call.id, "content": str(result)})
    return "остановлен: превышен лимит шагов"

print(run_agent("Какая погода в Киеве и сколько будет 17*23?"))

Проследите поток данных: намерение модели (msg с tool_calls) обязательно попадает в историю до результатов, а каждый результат привязан к вызову через tool_call_id — провайдер отклонит историю, где ответ инструмента не соответствует вызову или вовсе отсутствует. Оставить вызов «висящим» без результата нельзя: следующий запрос к API с такой историей вернёт ошибку валидации. Модель вполне может запросить оба инструмента за один шаг — и погоду, и умножение, — внутренний for это обрабатывает штатно. Результат приводится к строке (str(result)): содержимое tool-сообщения — всегда текст, даже если функция вернула число или словарь; для сложных структур сериализуйте в JSON явно. Наконец, print с номером шага — ваш первый трейс: привыкайте видеть каждый шаг агента глазами, это сэкономит часы отладки, когда поведение станет неочевидным.

Условия остановки: почему max_steps обязателен

У нашего цикла два выхода: модель вернула текст (штатный) и исчерпан max_steps (аварийный). Второй кажется формальностью, но он обязателен с первого дня. Агент без потолка шагов — это бесконечный цикл, который жжёт деньги: модель может зациклиться, вызывая один и тот же инструмент с теми же аргументами, особенно когда инструмент возвращает ошибку, которую она не понимает. Десять шагов для учебного агента — разумный потолок; в production лимит подбирают под задачу и дополняют бюджетом токенов и денег — этим займётся урок про guardrails.

Важно различать причины остановки и сообщать их честно. «Модель дала финальный ответ» и «нас выкинуло по лимиту» — принципиально разные исходы: первый можно показывать пользователю, второй — сигнал о проблеме, который надо логировать и разбирать. Верните вместе с текстом причину остановки (кортеж или маленький dataclass) — вызывающий код скажет спасибо.

Третий, менее очевидный выход из цикла — ошибка самого инструмента. Если функция бросит исключение, наш код упадёт целиком, и агент умрёт вместе с накопленной историей. Пока мы это допускаем сознательно; в следующем уроке научимся превращать исключение в результат-ошибку, которую модель видит и обходит. Запомните текущее ограничение — это ваш первый пункт технического долга, и вы о нём знаете.

Поэкспериментируйте: дайте задачу, требующую обоих инструментов; дайте задачу без инструментов вообще («переведи фразу»); дайте невыполнимую («погода на Марсе») и посмотрите, признается ли модель или начнёт крутиться. Три этих прогона расскажут о поведении цикла больше, чем любой текст.

Practice

Агент с двумя инструментами и трейсом

Соберите агента из урока и доведите его: добавьте третий инструмент now(), возвращающий текущую дату и время; замените print на запись шагов в список trace (шаг, инструмент, аргументы, результат); верните из run_agent причину остановки (final / max_steps) вместе с ответом. Прогоните три сценария: задача на два инструмента, задача без инструментов, невыполнимая задача.

Deliverable

Рабочий скрипт до ~60 строк плюс вывод трёх прогонов с трейсом шагов и причиной остановки для каждого.

Checkpoint

Объясните путь одного шага агента: что возвращает модель, что делает ваш код и что попадает в историю сообщений.

  • ✓ Описана развилка: tool_calls — исполняем, текст — финальный ответ.
  • ✓ Указано, что и намерение модели, и результат инструмента добавляются в историю, результат — с tool_call_id.
  • ✓ Объяснено, зачем нужен max_steps и чем аварийная остановка отличается от штатной.

Check yourself

Как агент понимает, что работа закончена?

01Как агент понимает, что работа закончена?

Зачем в истории сообщений нужен tool_call_id?

02Зачем в истории сообщений нужен tool_call_id?

Что произойдёт с агентом из урока без параметра max_steps, если модель зациклится?

03Что произойдёт с агентом из урока без параметра max_steps, если модель зациклится?

Какова роль словаря REGISTRY в агенте?

04Какова роль словаря REGISTRY в агенте?

Модель за один шаг запросила и get_weather, и calculator. Что делает правильный цикл?

05Модель за один шаг запросила и get_weather, и calculator. Что делает правильный цикл?

Sources

Your First Agent in 50 Lines · Your First AI Agent: From Zero to Autonomy · VORCL