LLM агенты без фреймворков: Python + OpenAI API — гайд 2026 | AiManual
AiManual Logo Ai / Manual.
17 Июн 2026 Гайд

Как создавать LLM-агентов без фреймворков: прототипирование workflow на Python и OpenAI API

Пошаговое руководство по созданию LLM-агентов на чистом Python и OpenAI API без LangChain и CrewAI. Прототипирование workflow, инструменты, structured outputs и

Реклама
cliv1

Фреймворки — это костыли, а не серебряная пуля

Каждый второй пост в LinkedIn начинается с «Я запилил агента на LangGraph за 15 минут». Круто. А потом этот агент жрет токены, теряет контекст на третьем шаге и падает с cryptic error из-за скрытой зависимости в графе. Знакомо?

Фреймворки вроде LangChain, CrewAI или AutoGen обещают абстракцию, а по факту дают слой неопределенности. Когда ваш агент делает что-то не то, вы тратите часы на дебаг чужого кода вместо того, чтобы просто поправить логику. В 2026 году, когда модели стали дешевле и умнее (GPT-4.5 Turbo, Claude 3.7 Sonnet, Gemini 2.5 Pro), а OpenAI Responses API обзавелся Structured Outputs и стримингом «из коробки», писать агентов на чистом Python — не мазохизм, а рациональный выбор. Особенно на этапе прототипирования.

В этой статье я покажу, как собрать рабочего LLM-агента с нуля: без зависимостей, без магии, с полным контролем над каждым вызовом. Мы пройдем путь от простого «болтательного» цикла до агента с инструментами и структурированным выводом. И да, я покажу, как НЕ надо — на реальных граблях, которые я сам набил.

💡
Почему не LangGraph / CrewAI? Они хороши для production-оркестрации сложных многоагентных систем, но для прототипа дают оверхед: асинхронные пайпы, state-менеджмент, лишние зависимости. Агент без фреймворка — это 150 строк кода, которые вы понимаете от первой до последней. И это тот самый MVP, который потом можно либо выкинуть, либо обернуть в фреймворк, когда задача усложнится.

Архитектура простого автономного агента

Любой LLM-агент — это цикл: получил задачу -> подумал -> сделал действие -> получил результат -> подумал снова -> ... -> финальный ответ. Без фреймворков этот цикл пишется одним while True. Давайте разберем на конкретном примере: агент, который умеет искать информацию в интернете и отвечать на вопросы.

1Настраиваем клиент и модель

Используем openai версии >= 1.70 (на июнь 2026 актуальна 1.73). Ключ API можно брать напрямую или через прокси — например, AITunnel, который дает стабильный доступ к OpenAI из регионов с блокировками.

import os
from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
MODEL = "gpt-4.5-turbo"  # или gpt-4o, если бюджет меньше

2Базовый ReAct-цикл

Классический ReAct: агент получает промпт, модель генерирует Thought и Action (в виде JSON), мы парсим, выполняем инструмент, возвращаем Observation. Повторяем, пока не получится Final Answer.

SYSTEM_PROMPT = """Ты — полезный ассистент. У тебя есть доступ к инструментам:
- search(query: str) — поиск в интернете

Отвечай в формате JSON:
{"reasoning": "...", "action": "tool_name", "parameters": {...}}
или
{"reasoning": "...", "final_answer": "..."}
"""

def run_agent(user_query: str, max_steps: int = 5):
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    messages.append({"role": "user", "content": user_query})
    
    for step in range(max_steps):
        response = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            response_format={"type": "json_object"}
        )
        reply = response.choices[0].message.content
        parsed = json.loads(reply)
        
        if "final_answer" in parsed:
            return parsed["final_answer"]
        
        # Выполняем инструмент
        tool_name = parsed.get("action")
        params = parsed.get("parameters", {})
        if tool_name == "search":
            observation = search(params["query"])
        else:
            observation = "Unknown tool"
        
        messages.append({"role": "assistant", "content": reply})
        messages.append({"role": "user", "content": f"Observation: {observation}"})
    
    return "Max steps reached. Partial result."

⚠️ Ошибка, которую я часто видел: не добавлять в историю собственное сообщение ассистента. Если пропустить messages.append с ответом модели, она не увидит предыдущий вывод и начнет «галлюцинировать» (или повторяться). Это частая причина контекстного дрейфа, о которой мы писали в статье «RLM: практическое руководство по созданию LLM-агентов без контекстного дрейфа».

3Структурированный вывод вместо хаоса

Парсить JSON из сырого ответа модели — рискованно: модель может забыть кавычки, поменять типы. Structured Outputs в OpenAI API (с мая 2025) решают это: вы задаете схему через Pydantic, и модель гарантированно возвращает валидный JSON.

from pydantic import BaseModel
from typing import Optional

class Action(BaseModel):
    reasoning: str
    action: Optional[str] = None
    parameters: Optional[dict] = None
    final_answer: Optional[str] = None

# В запросе указываем response_format
response = client.beta.chat.completions.parse(
    model=MODEL,
    messages=messages,
    response_format=Action
)
parsed = response.choices[0].message.parsed  # уже объект Action

Больше не нужно ловить json.JSONDecodeError. Это повышает надежность агента в разы. А если нужно передать сложные параметры — наследники Pydantic работают как швейцарские часы.

💡
Совет: Включайте поле reasoning во все шаги — оно помогает дебажить. А в production можно убрать из финального ответа, оставив только результат.

Как не скатиться в адскую рекурсию

Проблема, описанная в «Почему ломаются LLM-агенты» — агенты могут зацикливаться. Без фреймворков вы контролируете это сами:

  • Лимит шагов (max_steps) — обязательно.
  • Детектор повторений: если модель трижды подряд выдает одинаковое reasoning или action — прерываем.
  • Circuit breaker: если стоимость вызовов превысила бюджет — стоп.

Вот простой детектор:

last_actions = []
for step in range(max_steps):
    action = parsed.action
    if action and action in last_actions[-2:]:
        print(f"Detected loop at step {step}")
        break
    last_actions.append(action)

Этого достаточно для прототипа. В статье «Как правильно использовать суб-агентов» мы показывали, как делегировать подзадачи — это тоже снижает риск циклов, потому что каждый суб-агент живет свою короткую жизнь и умирает.

Добавляем настоящие инструменты: поиск, API, базы

В примере выше был абстрактный search(). Реальный инструмент — это простая функция, которая возвращает строку. Например, обертка над Google Search API или обертка над вашей внутренней базой.

Секрет в том, что инструменты не должны быть частью фреймворка. Вы сами решаете, как передавать результат — строкой, структурой, файлом. Для прототипа я бы рекомендовал всегда возвращать строку (observation). Модели удобнее работать с текстом, чем с бинарными данными.

def search_web(query: str) -> str:
    # какой-то настоящий поиск
    result = requests.get(f"https://api.duckduckgo.com/?q={query}&format=json")
    return result.json().get("AbstractText", "No results")

⚠️ Ошибка: передавать слишком длинные результаты. Если модель получает на вход 10 000 токенов «наблюдения», она начинает игнорировать их и полагаться на свою память. Обрезайте результаты до 1000 символов, либо используйте суб-агент для суммаризации — как в «Skills, MCP и сабагенты».

Когда самописный агент превращается в монстра — и что делать

Наш простой цикл отлично работает для 3-5 шагов. Но как только вы захотите:

  • поддерживать долгие диалоги (часы работы),
  • параллельно вызывать несколько инструментов,
  • иметь state-машину с памятью о предыдущих сессиях,
  • оркестрировать десятки агентов —

ваш код распухнет, и вы начнете изобретать велосипед. Именно в этот момент стоит посмотреть в сторону легковесных фреймворков, но не перегруженных. Или разбить систему на микроагентов по принципу orchestrator-workers, описанному в статье «Как спроектировать современного AI-агента».

Но для прототипа — первый MVP, который вы покажете заказчику или протестируете гипотезу — 150 строк без фреймворков это идеальный старт. Вы получите полный контроль, а когда поймете, что именно нужно, — перепишете на фреймворк осознанно, а не по инерции.

Итог: голый код дисциплинирует

Фреймворки скрывают сложность, но они же скрывают и проблемы. Писать агента без них — значит каждый раз видеть, где модель ошибается, где контекст раздувается, где инструменты возвращают мусор. Это единственный способ научиться чувствовать агентную архитектуру. А когда навык сформирован — оборачивайте хоть в LangGraph, хоть в AutoGen.

Совет напоследок: начните прототип с одного файла и одного цикла. Не думайте о масштабировании на старте. Через день у вас будет работающий агент, который вы полностью понимаете. И это стоит дороже любого фреймворка.

🔮
Прогноз на 2027: модели научатся сами генерировать себе инструменты на лету (function calling станет динамическим). Фреймворки будут пытаться это абстрагировать, но тот, кто понимает, что под капотом, сможет выжимать максимум из каждой копейки API. Не теряйте этот навык.

Подписаться на канал