Сімнадцять невеликих інструментів MCP замість одного величезного агента: система написання на основі ШІ.
Як система досліджень та написання розділяє процеси отримання відео, пошуку, підготовки проекту, оцінки та перевірки коду на окремі інструменти MCP із чіткими межами та можливістю відстеження дій.
Джерела: 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 інструментів. Це приклад декомпозиції: де семантичні оцінки мають здійснюватися моделлю, де звичайний код має залишатися основним засобом обробки даних, як артефакти можуть замінювати складний стан розмови та чому генерований код має пройти детерміністичну перевірку перед тим, як його побачить користувач.
Виконання від початку до кінця
Розгляньмо тему виклику функцій LLM та структурованих результатів, починаючи з набору початкових керівництв: однієї презентації на 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 для створення стислого опису репозиторію, придатного для роботи з LLM.
Перевірка AST — це окрема справа. gitingest лише завантажує дані, надаючи дослідницькій моделі карту каталогів та вміст файлів, тоді як парсер AST у Python діє значно пізніше, під час перевірки генерованого коду у процесі публікації. Змішування цих двох підходів ускладнить архітектуру.
Розділення їх також полегшує розширення. Завантажувач з GitLab чи локальної папки згодом просто муситиме генерувати той самий дослідницький результат, адже автор не звертає уваги на те, звідки походить код.
Tavily та Firecrawl разом
Ці інструменти вирішують різні аспекти веб-досліджень. Tavily допомагає з пошуком, перетворюючи запитання „Що ще мені бракує?“ на низку цілеспрямованих запитів. Firecrawl допомагає після того, як сервер обирає певну сторінку та хоче чисто витягнути її вміст.
Проєкт використовує цю різницю, щоб робити дослідження ітеративними. Отримання початкових посилань — це не кінець. Агент переглядає корпус, створює нові запити щодо того, що ще бракує, виконує їх, додає отримані результати та може повторювати цей процес до трьох раундів.
Дизайн навмисно асиметричний: ШІ вирішує, що бракує, а Tavily та Firecrawl виконують пошук інформації. Така сама схема повторюється постійно — модель використовується там, де корисним є семантичне судження, а звичайний код та сервіси — там, де краще підходить детерміністичний підхід. Діяльність Firecrawl під час дослідження показує, що він сканує різнорідні технічні джерела ще до початку роботи над проєктом.
Файли у форматі .nova/ замість однієї великої конверсації
Дослідження можуть легко створювати десятки тисяч токенів, і зберігання кожної вилученої сторінки та проміжних рішень у кожному наступному виклику інструменту було б марнотратним.
Натомість система створює невеликі керуючі файли у каталозі .nova/, такі як guidelines_filenames.json та next_queries.json, а великий контент зберігає у файлах уривків, наприклад research.md. Модель отримує лише ту частину інформації, яка потрібна для поточної операції.
Це менш „чарівно“, ніж „пам’ять агента“, але набагато простіше для перевірки. Коли виконання зупиняється на півдорозі, каталог стану показує, які посилання були зібрані чи який етап пошуку мав відбутися далі. Фактично файлова система виступає як приблизна, але функціональна угода між різними етапами.
Автор — це конвеєр
Як тільки з’являється файл research.md, виникає спокуса надіслати весь його вміст великій моделі та отримати статтю обсягом 2 000 слів. Проте проект уникав цього з самого початку, адже обмеження на обсяг вихідних даних та складність обробки довгих контекстів ускладнюють одночасний контроль структури, охоплення коду та посилань.
Натомість на етапі написання завдання розділяється між чотирма інструментами:
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, і дозволяє помилкам пройти ще один етап обробки за допомогою інструменту для виправлення за допомогою LLM.
Статичний парсинг та виконання виявляють різні проблеми. 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, а запропоноване виправлення знову проходить ту саму процедуру перевірки. Модель ніколи не схвалює власні виправлення. ЛММ пропонує варіант, а ast.parse() разом із виконанням вирішують, чи він прийнятний. Лише після завершення циклу функція export_publication_bundle перетворює перевірений результат на самостійний HTML-вихідний файл.
Одна особливість заслуговує на увагу. Шар виконання є засобом для запуску підпроцесів із захистом, а не посиленою безпековою сандбокс-системою. Таймаут та дочірні процеси ефективно допомагають у перевірці якості контенту, виявляючи синтаксичні помилки, проблеми під час виконання та зациклення. Для справді ненадійного коду потрібен сильніший ізоляційний механізм, такий як контейнери, простори імен та seccomp, мікро-ВМ або спеціалізована служба сандбоксу з обмеженнями на файлову систему, мережу, системні виклики, процесор та пам’ять.
Два спільні шари: маршрутизація моделей та відстеження
Видима архітектура складається з трьох фаз, проте два спільні елементи мають таку саму важливість у щоденному використанні.
Маршрутизація моделей із урахуванням квот
Одна дослідницька операція може викликати багато моделей, і легко можна вичерпати квоти безкоштовного тарифу. Щоб уникнути ініціалізації, специфічної для кожного постачальника, у кожному обробнику, створення моделей централізовано здійснюється у файлі 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.
Зміцнення системи для продакшну почнеться з кращої ізоляції в сандбоксі, потім — з фіксованого набору інструментів для оцінки, який охоплює дослідження, посилання та якість коду, більш чітких метаданих про можливості постачальника та надійних точок перевірки роботи. Вже сьогодні проект підкреслює, що модель може обрати наступну дію, тоді як якість системи залежить від інструментів, обмежень, результатів обробки та циклів зворотного зв’язку, які її оточують.
Три інженерні уроки можна застосувати й до інших систем агентів:
- Обмеження інструментів важливіші за їхня кількість. Надайте кожній можливості власні обмеження, якщо вона взаємодіє з іншою системою, може зазнати невдачі або повторювати спроби по-різному, або створює результати, які хтось буде використовувати знову.
- Моделі пропонують варіанти, а детерміністичні перевірки приймають рішення, коли це можливо. Штучний інтелект може виправити фрагмент коду, але остаточне рішення приймається після його аналізу та виконання.