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

Семнадцать небольших инструментов MCP вместо одного гигантского агента: система написания на основе ИИ

Как система исследований и написания разделяет процессы загрузки видео, поиска, составления черновиков, рецензирования и проверки кода на отдельные инструменты MCP с четкими границами, включая генерацию результатов и отслеживание действий.

Издатель: REACTAPP.TOP13 мин чтения2957 слов

Источники: Why I Built MaxyAI as 17 Small MCP Tools Instead of One Giant Agent

Самая сложная часть агента для написания текстов с использованием ИИ заключается не в том, чтобы заставить языковую модель писать. Сложность кроется в определении того, за что модель не должна нести ответственность. Ранние попытки создания таких агентов обычно сводились к одной архитектуре: передавать отдельной модели тему, присоединять инструмент поиска и просить её исследовать, рассуждать, писать, проверять качество текста и восстанавливаться после ошибок в рамках одной длинной последовательности действий. Подсказки становятся всё более сложными для обработки, отслеживание процесса — трудным, а детерминистические сбои переплетаются с семантическими решениями. MaxyAI — система для исследований и написания текстов с использованием ИИ — была создана на основе другой идеи: каждая функция, имеющая отдельные входные данные, способы сбоев, правила повторной попытки или возможность повторного использования результата, должна стать самостоятельным инструментом в рамках единого сервера MCP.

Входные данные редко бывают организованы. Это может быть лекция на YouTube, репозиторий на GitHub, страница с документацией и несколько ссылок для справки. MaxyAI принимает всё это, расширяет исследование с помощью поиска в интернете, сохраняет увеличивающийся корпус данных на диске и преобразует его в структуру оглавления. Затем он составляет текст раздел за разделом, передаёт черновик цифровому анализатору, извлекает все блоки кода на Python, проверяет и запускает код, исправляет синтаксические ошибки и экспортирует готовую статью в формате HTML.

За всем этим не стоит какой-то сложный промпт. Система представляет собой сервер FastMCP с 17 зарегистрированными инструментами, 12 из которых предназначены для исследований. Инструменты специально сделаны небольшими по размеру. Один из них транскрибирует видео, другой понимает структуру репозиториев, третий выполняет поиск в интернете, четвёртый создаёт оглавление, пятый анализирует статью, а шестой проверяет код. Модель решает, какой инструмент использовать, а Python-код, находящийся внутри каждого инструмента, выполняет определённые действия.

Именно это разделение является самым ценным аспектом данной конструкции. Модель может меняться без вмешательства в процесс загрузки контента с YouTube. Поисковый скрейпер можно заменить, не нарушая работу модуля формирования текста. Этап проверки кода можно отладить, не перезапуская всё исследование. К тому же, поскольку каждый важный шаг оставляет какой-либо результат или след, длительную автономную работу можно изучить позже, вместо того чтобы рассматривать её как один огромный текст чата.

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

Почему не использовать один большой инструмент? Потому что система решает несколько разных инженерных задач, а не одну задачу логического вывода. Каждая из этих задач (принятие видео, анализ репозиториев, поиск в Интернете, управление состоянием, составление черновиков, критика, запуск кода, его исправление, публикация) имеет свои собственные входные данные, затраты, возможные сбои и потребности в повторных попытках. Один мега-инструмент сократил бы объем команд, но затруднил бы наблюдение за системой и её расширение.

Всё это не означает, что каждому агенту нужно 17 инструментов. Это пример разбиения задач на составные части: где семантическое суждение должно осуществляться моделью, где должен оставаться обычный код, как артефакты могут заменять объемное состояние диалога и почему генерируемый код должен пройти детерминистическую проверку перед тем, как его увидит пользователь.

От начала до конца

Рассмотрим тему вызова функций больших языковых моделей и структурированного вывода, начав с набора руководящих принципов: одного видео на YouTube, одного репозитория на GitHub и нескольких URL с документацией.

Этап 1 не включает написание прозы. Его задача — собирать доказательства. Сервер MCP выбирает URL-адреса, передает видео в мультимодальный API Gemini, преобразует репозитории в формат, удобный для работы модели, с помощью инструмента gitingest, сканирует страницы с помощью Firecrawl, заполняет пробелы в информации путем поиска в Tavily и отслеживает список источников в файлах состояния внутри папки .nova/. В результате формируется файл research.md — большой документ исследования, который любой может открыть и прочитать, вместо скрытой памяти разговора.

Этап 2 берет этот результат и планирует публикацию. Функция generate_article_outline определяет структуру разделов, целевое количество слов и строк кода, после чего функция draft_article_section при каждом вызове пишет один определенный по границам раздел. Затем функция critic_evaluate_article оценивает полный черновик с точки зрения технической глубины, качества кода, структуры и точности цитирования, а функция refine_and_save_article вносит требуемые изменения перед созданием файла article.md.

Третий этап предполагает, что код в формате Markdown должен корректно работать при выполнении, а не просто выглядеть убедительно. Блоки кода на Python извлекаются и анализируются с помощью ast.parse(). Валидные фрагменты кода сохраняются в папке .nova/sandbox/ и запускаются с временем ожидания в пять секунд. При синтаксической ошибке ошибка и фрагмент кода передаются в функцию fix_code_snippets, после чего результат снова проверяется на соответствие требованиям. После того как код проходит проверку, функция export_publication_bundle генерирует отдельный HTML-документ.

Система отслеживания Opik охватывает весь процесс выполнения. Каждый модель и инструмент предоставляют информацию о задержках, метаданных поставщика, иерархии инструментов и промежуточных результатах. Если рассматривать архитектуру в виде диаграммы, она состоит из трех этапов (исследования, написание и проверка кода, публикация), которые используют общие механизмы обработки сбоев моделей и мониторинга.

Сервер MCP в качестве основы

Первый выбор в дизайне был сделан на уровне протокола: возможности предоставляются через Model Context Protocol, и каждая функция не встраивается напрямую в единую циклическую структуру приложения.

MCP предоставляет клиенту стандартизированный набор терминов для поиска и вызова инструментов. FastMCP упрощает работу с этим в Python, поскольку обычная типизированная функция превращается в инструмент MCP с помощью декоратора, а её сигнатура становится частью контракта инструмента. Реализация может оставаться обычным Python-кодом даже в том случае, когда клиент представляет собой агента.

В проекте mcp_server/src/routers/tools.py является точкой входа в слой инструментов. Упрощённая процедура регистрации выглядит следующим образом:

import opik
from fastmcp import FastMCP

from ..tools import run_web_research_tool

def register_mcp_tools(mcp: FastMCP) -> None:
    @mcp.tool(name="run_web_research")
    @opik.track(type="tool", tags=["research", "tavily"])
    async def run_web_research(
        queries: list[str],
        research_directory: str,
    ) -> dict:
        return await run_web_research_tool(
            queries=queries,
            research_directory=research_directory,
        )

Декоратор выполняет не только функцию улучшения синтаксиса. Клиент видит инструмент с именем и аргументами определенного типа, в то время как реализация остается в слое инструментов и обработчиков. Такое разделение становится важным, когда сервер развивается за пределы нескольких простых функций.

Сознательно избегали использования единственного инструмента do_everything(topic), поскольку это привело бы к восстановлению связей, устранение которых было целью проектирования. Более мелкие инструменты позволяют модели комбинировать действия: извлекать источники, проверять отсутствующие элементы, снова искать, составлять план, писать черновик, анализировать, исправлять ошибки. Они также делают отчеты гораздо более читаемыми, поскольку каждый отрезок времени соответствует одному значимому действию.

Следовательно, сервер с 17 инструментами — это не 17 отдельных команд. Это набор ограниченных возможностей. На исследования приходится наибольшая доля — 12 инструментов, поскольку именно при получении источников рабочий процесс требует наибольшего разнообразия действий.

Исследования: разные источники, разные инструменты

На исследования ушло наибольше усилий в процессе дизайна, поскольку функция «поиск в интернете» скрывает несколько этапов обработки. Видео, репозиторий на GitHub и сайт с документацией, написанной преимущественно на JavaScript, создают три разных типа проблем с данными, и использование одного скрапера с самого начала приводит у автора к недостаточно точному пониманию контекста.

Gemini для YouTube

Обработка длинных видео локально требует значительных ресурсов: необходимо скачать аудио, преобразовать его в текст с помощью технологии распознавания речи, управлять памятью GPU, а затем составить краткое содержание. Модуль youtube_handler.py выбирает более простой подход: он передаёт публичный URL YouTube непосредственно в Gemini в качестве мультимодального входных данных и запрашивает структурированный Markdown с указанием временных меток.

При выборе Gemini было решено не потому, что это лучшая модель для всего проекта, а потому, что эта способность соответствует требованиям задачи: возможность работы с мультимодальными данными нативно позволяет серверу обрабатывать видео без необходимости использования локальной структуры Whisper или GPU на клиентском устройстве.

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

@retry(
    retry=retry_if_exception_type(errors.ServerError),
    wait=wait_exponential(multiplier=1, min=5, max=60),
    stop=stop_after_attempt(3),
)
async def transcribe_youtube(url: str, output_path: Path, timestamp: int = 30):
    client = track_genai_client(
        genai.Client(api_key=settings.google_api_key.get_secret_value())
    )

    parts = [
        types.Part(file_data=types.FileData(file_uri=url)),
        types.Part(text=PROMPT_YOUTUBE_TRANSCRIPTION.format(timestamp=timestamp)),
    ]

    response = await client.aio.models.generate_content(
        model=settings.youtube_transcription_model,
        contents=parts,
    )

    output_path.write_text(response.text, encoding="utf-8")

В одном из зафиксированных примеров 39-минутная лекция была преобразована в структурированные заметки за 17,8 секунды без использования памяти GPU на стороне клиента. Это результат конкретного теста, а не универсальный показатель, поскольку нагрузка на сервер и условия сети могут его изменить. Записанный отчет показал идентификатор модели gemini-3.7-flash и сохранённый артефакт состояния .nova/guidelines_filenames.json.

gitingest для репозиториев

Репозиторий — это не веб-страница. Модели необходима структура файлов и сам исходный код, а не навигационная структура GitHub, поэтому обработчик GitHub использует gitingest для создания краткого описания репозитория, подходящего для работы с большими языками моделирования.

Проверка AST — это отдельная задача. Инструмент gitingest лишь загружает данные, предоставляя исследовательской модели карту каталогов и содержимое файлов, тогда как парсер AST в Python вступает в действие гораздо позже, когда в процессе публикации проверяется сгенерированный код. Слияние этих двух компонентов затруднит архитектуру.

Разделение их также облегчает расширение функционала. Инструменты для загрузки данных из GitLab или локальных папок в будущем будут просто должны генерировать тот же исследовательский результат, поскольку для автора не имеет значения, откуда взялся код.

Tavily и Firecrawl вместе

Эти инструменты решают разные аспекты веб-исследований. Tavily помогает с поиском, превращая вопрос «Что у меня ещё не есть?» в набор целенаправленных запросов. Firecrawl используется после того, как сервер выбрал конкретную страницу и требует её чистого извлечения содержимого.

Проект использует эту разницу для того, чтобы сделать исследования итеративными. Прием первоначальных ссылок — это не конец. Агент просматривает корпус, формирует новые запросы на то, что ещё отсутствует, выполняет их, добавляет полученные результаты и может повторять этот процесс до трех раундов.

Дизайн намеренно асимметричен: большая языковая модель определяет, чего не хватает, а Tavily и Firecrawl занимаются поиском информации. Тот же принцип применяется на протяжении всего процесса: модель используется там, где полезен семантический анализ, а обычный код и сервисы — там, где лучше подходит детерминированный подход. Данные о деятельности Firecrawl во время исследовательской работы показывают, что он сканирует разнообразные технические источники ещё до начала работы над проектом.

Файлы в .nova/, а не один огромный диалог

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

Вместо этого система создаёт небольшие файлы управления в папке .nova/, такие как guidelines_filenames.json и next_queries.json, а крупный контент хранит в файлах вроде research.md. Модель получает только ту часть информации, которая необходима для текущей операции.

Это менее сложно, чем концепция «памяти агента», но гораздо проще для анализа. Когда выполнение останавливается на полпути, каталог состояния показывает, какие ссылки были собраны или какой этап поиска должен был следовать дальше. По сути, файловая система служит примитивным, но функциональным механизмом согласования между этапами.

Система написания — это конвейер

Как только появляется файл research.md, возникает сильное искушение отправить его целиком в большую модель и попросить написать статью объемом 2000 слов. Однако проект с самого начала избегал этого подхода, поскольку ограничения на объем вывода и сложности обработки длинных контекстов затрудняют одновременный контроль за структурой, охватом кода и цитатами.

Вместо этого на этапе написания задача распределяется между четырьмя инструментами:

  • generate_article_outline преобразует исследовательский корпус в список разделов с установленным лимитом слов и целями по коду.
  • draft_article_section пишет отдельные разделы вместо одного огромного текста.
  • critic_evaluate_article оценивает собранный черновик по структурированной шкале от 0 до 100.
  • refine_and_save_article выполняет необходимые правки, создаёт оглавление, проверяет структуру цитат и сохраняет файл article.md.

Важно, чтобы критик не просто сказал «всё хорошо». Его вывод представляет собой структурированный отзыв, на основе которого может работать другой инструмент:

{
 "overall_score": 85,
 "status": "APPROVED",
 "category_scores": {
 "technical_depth": 27,
 "code_quality": 26,
 "structure_clarity": 17,
 "citation_fidelity": 15
 },
 "specific_critiques": [
 "Section 3.2 mentions timeout handling but omits the asyncio.wait_for example.",
 "Citation [14] is cited twice with conflicting author names."
 ],
 "required_revisions": [
 "Add the timeout example.",
 "Deduplicate bibliography reference [14]."
 ]
}

Эта схема с каждым разом сужает роль модели. Модель-планер определяет структуру, автор текста синтезирует отдельный раздел, критик ищет недостатки, а на этапе доработки применяется чек-лист. Ни одна модель не должна выполнять все четыре задачи за один процесс.

Оценка 85 из 100 не является научной метрикой качества. Это сигнал для контроля рабочего процесса, а ценным элементом является машинно читаемый список критик и правок.

След настоящей цепочки авторов включает структурированный запрос, вызов чат-модели и парсер Pydantic — именно такие записи позволяют понять автономную систему написания текстов в последующем.

Код как исполняемый объект

Техническая статья может быть хорошо написана, но всё равно не удовлетворить читателей, если её код не работает. Именно поэтому существует третий этап.

Разработчик открывает файл article.md, извлекает из него код на Python, заключённый в теги, проверяет каждый блок с помощью ast.parse(), сохраняет проверенные фрагменты в папку .nova/sandbox/, запускает их в качестве дочернего процесса с захватом вывода в стандартные потоки stdout и stderr, а в случае сбоев позволяет использовать инструмент для исправления с помощью большой языковой модели.

Статическая обработка и выполнение выявляют разные проблемы. ast.parse() работает быстро и сразу отклоняет невалидный Python-код. Дочерний процесс ловит ошибки во время выполнения, которые невозможно обнаружить только на уровне синтаксиса.

def validate_python_syntax(code: str):
 try:
 ast.parse(code)
 return True, None
 except SyntaxError as exc:
 return False, f"SyntaxError at line {exc.lineno}: {exc.msg}"
result = subprocess.run(
 [sys.executable, str(file_path)],
 capture_output=True,
 text=True,
 timeout=5,
 env={"PYTHONUNBUFFERED": "1"},
)

Неудачный фрагмент кода исправляется и снова отправляется на проверку перед компиляцией статьи. В одном случае сгенерированная статья содержала строку pip install google-genai внутри блока Python. В формате Markdown это выглядело правдоподобно, но Python сразу же отклонил её. Функция fix_code_snippets получила отчет об ошибках и поврежденный блок, переписала фрагмент кода и отправила его на повторную проверку.

# Broken
pip install google-genai
from google import genai

# Repaired
# Requires: pip install google-genai
import os
from google import genai

client = genai.Client(
    api_key=os.environ.get("GEMINI_API_KEY", "mock-key")
)

Цикл ремонта намеренно ограничивается локальными действиями. Модели не разрешается переписывать всю статью только из-за сбоя одного фрагмента. Она берет самую маленькую полезную единицу — код и связанную с ним ошибку, причем детерминистичные проверки остаются контрольным звеном.

Проще говоря: файл article.md сканируется на наличие кода на Python, каждый фрагмент анализируется, сохраняется в папке .nova/sandbox/ и выполняется в ограниченном дочернем процессе. При сбое код и трейс-код упаковываются для обработки функцией fix_code_snippets, после чего предложенный вариант ремонта снова проходит ту же процедуру проверки. Модель никогда не утверждает свой собственный ремонт — LLM предлагает вариант, а функции ast.parse() и его выполнение принимают решение. Только после завершения цикла функция export_publication_bundle преобразует проверенный результат в отдельный HTML-файл.

Один аспект заслуживает особого внимания. Слой выполнения представляет собой механизм запуска подпроцессов с защитой, а не усиленную среду безопасности в виде песочницы. Использование таймаутов и дочерних процессов эффективно помогает в проверке качества контента, позволяя выявлять синтаксические ошибки, сбои во время выполнения и заедания программы. Для действительно ненадежного кода потребуется более строгая изоляция, такая как контейнеры, пространства имен и инструменты вроде seccomp, микро-ВМ или специализированные сервисы песочниц с ограничениями на файловую систему, сеть, системные вызовы, производительность CPU и объем памяти.

Два общих слоя: маршрутизация моделей и отслеживание

Видимая архитектура состоит из трех этапов, однако два общих компонента играют не менее важную роль в повседневной работе.

Маршрутизация моделей с учетом квот

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

Проект развивался на основе серии моделей Gemini Flash, Gemini Flash Lite и Llama, размещённых на платформе Groq. Эти конкретные модели не являются постоянными, но это не имеет значения. Каждый инструмент взаимодействует с абстракцией модели, причём в коде исследований и написания не содержится информации о конкретных поставщиках.

Эксперименты также становятся дешевле, поскольку перенос задачи на более дешёвую или в настоящее время доступную модель не влияет на соглашение интерфейса инструмента MCP.

def get_chat_model(model_id: str, schema=None):
    selected = settings.llm_configs[model_id]
    identifier = selected["identifier"]
    params = selected.get("params", {}).copy()

    # resolve provider-specific API key
    ...

    model = init_chat_model(identifier, **params)
    return track_langchain_model(model)

Opik делает длинные циклы выполнения видимыми

Логи теряют свою пользу, как только сервер MCP вызывает модель, которая, в свою очередь, вызывает инструмент, записывающий результат, триггерирующий работу другой модели. Использование функций отслеживания Opik для моделей-клиентов и инструментов MCP позволяет непосредственно отображать эту иерархию, без необходимости собирать информацию из сообщений о выводе.

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

Чему учит проект по проектированию агентов

Способность генерировать статьи — это не то, что делает MaxyAI интересным, ведь уже существующие передовые модели могут преобразовывать запросы в приемлемые тексты. Особое внимание привлекает то, как разделена работа: ввод видео, обработка хранилища, поиск, составление плана, написание черновика, критика, извлечение кода, проверка, редактирование и экспорт — всё это отдельные инструменты. MCP обеспечивает единое пространство для их использования, файлы на диске позволяют передавать большие объемы данных между этапами без необходимости сохранять их в памяти чата, а Opik делает всю последовательность действий видимой.

Точки расширения легко обнаружить: инструмент для ввода PDF предоставляет первому этапу другой тип исходных данных. Замена Tavily не влияет на модуль написания текста. В третий этап можно добавить проверку с помощью JavaScript, при этом двигатель поиска остается нетронутым. Изменение поставщиков моделей не влияет на интерфейс MCP.

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

Три инженерных принципа находят применение и в других системах агентов:

  1. Качество ограничений инструментов важнее их количества. Необходимо определить собственные границы для каждой функции, если она обращается к другой системе, действует иначе при сбоях или повторных попытках, или генерирует результат, который кто-то собирается использовать снова.
  2. Модели предлагают варианты, а детерминистичные проверки принимают решения, когда это возможно. ГЛМ может исправить фрагмент кода, но окончательное решение принимается в процессе парсинга и выполнения.
  • Указание конкретного состояния, которое можно проверить, лучше, чем расплывчатое упоминание «памяти». Сохранённые исследования, контрольные файлы и следы делают длительные запуски отладочными и позволяют повторно выполнить отдельные этапы без необходимости воссоздания огромной истории чатов.