Головна / Статті / MCP для агентів ШІ: стандартизація інтеграції інструментів у LangGraph

MCP для агентів ШІ: стандартизація інтеграції інструментів у LangGraph

У цій статті пояснюється, що саме стандартизує MCP у системах агентного ШІ, порівнюючи інтеграції інструментів ad hoc з тими, що базуються на MCP у механізмі оркестрації LangGraph.

4323 слів

Вступ та підсумок

До кінця Частини 2 система досягла справді координованого стану: набір спеціалізованих агентів, кожен з яких відповідає за певну частину роботи, працював у спільному стані під керуванням оркестратора, який вирішував, що має бути виконано далі. Однак у кожному прикладі існувала передумова, що кожен агент вже мав доступ до всього необхідного у цьому спільному стані, яке можна було прочитати у будь-який час, коли це було потрібно.

Це припущення рідко справджується в реальних умовах. Агент зазвичай потребує чогось, що знаходиться поза графом: рядка з бази даних, відповіді від зовнішнього API, уривку з бази знань чи іншого ресурсу, який знаходиться за межами власних кордонів системи. Кожного разу, коли агенту потрібно отримати інформацію ззовні, йому потрібен власний шлях для цього, і якщо кожен з цих шляхів створюється вручну, доводиться повторювати однакові завдання з інтеграції, але трохи по-різному, для кожного агента та кожного зовнішнього ресурсу.

Саме це повторення є проблемою, яку розглядається в цій статті, і воно пояснює, чому MCP став такою постійною темою протягом останнього року. Перш ніж вирішити, чи варто його впроваджувати, корисно точно визначити, яку проблему він вирішує, та розглянути, що включалося у під’єднання інструменту до агента ще до появи MCP.

Проблема, яку, за твердженням MCP, він вирішує

До появи MCP надання агенту доступу до зовнішніх ресурсів означало створення індивідуальної інтеграції, адаптованої саме під цей ресурс, у формі, яка здавалася доцільною на той момент. Один ресурс можна було отримати через REST API, інший — через клієнт бази даних, ще інший — через SDK з власними правилами автентифікації та обробки помилок. Кожна з цих відмінностей доводилося безпосередньо включати до коду самого агента.

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

Ці інтеграції рідко схожі одна на одну, тому що ніщо не змушує їх до цього. Один обгортковий модуль може автоматично повторювати невдалі виклики; інший — зовсім не повторювати їх. Один може відображати проблеми у вигляді кинутих винятків; інший — приховувати їх у полі статусу, яке користувач мусить пам’ятати перевірити. Не існує спільної мови для того, що насправді означає „під’єднати агента до інструменту“ — кожна інтеграція вирішує це питання по-своєму.

Саме цю прогалину і намагається подолати MCP. Він не впроваджує нові функції, яких раніше не було у агентах; він стандартизує механізм доступу до вже існуючих можливостей, щоб підключення нового агента до існуючого інструменту чи нового інструменту до існуючого агента більше не означало необхідності створення ще однієї індивідуальної інтеграції з нуля. Чи виправдовує він цю обіцянку на практиці, стає значно легше оцінити, як тільки ви побачите, як насправді виглядає спонтанний підхід у коді — саме тут продовжується обговорення.

До появи MCP: імпровізований спосіб підключення інструменту

Розгляньмо досить звичайну інтеграцію: агента, який потребує запитувати зовнішню систему, об’єднаного будь-яким кодом-зчеплювачем, який забезпечує працездатність з’єднання.

import requests
class LookupToolClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.api_key = api_key
    def lookup(self, query: str) -> dict:
        response = requests.get(
            f"{self.base_url}/search",
            params={"q": query},
            headers={"Authorization": f"Bearer {self.api_key}"},
        )
        if response.status_code != 200:
            return {"error": f"lookup failed: {response.status_code}"}
        return response.json()
def agent_node(state: GraphState) -> dict:
    client = LookupToolClient(base_url="https://internal-tool.example.com", api_key="...")
    result = client.lookup(state["extracted_fields"]["query"])
    return {"tool_result": result}

У самій по собі цій конструкції немає нічого поганого. Це компактний HTTP-клієнт, простий механізм обробки помилок та функція, яка викликає його зсередини Node. Проблеми починаються, коли з’являється ще один інструмент — цього разу не ще один REST API, а клієнт бази даних з зовсім іншою структурою:

import psycopg2
class RecordsClient:
    def __init__(self, connection_string: str):
        self.conn = psycopg2.connect(connection_string)
    def fetch_record(self, record_id: str) -> dict:
        with self.conn.cursor() as cur:
            cur.execute("SELECT * FROM records WHERE id = %s", (record_id,))
            row = cur.fetchone()
            if row is None:
                raise ValueError(f"no record found for {record_id}")
            return dict(zip([desc[0] for desc in cur.description], row))

Ці два клієнти не мають спільного інтерфейсу, жодної конвенції найменувань та навіть неоднакових способів сигналізації про помилки: один повертає словник з помилками, а інший просто кидає виняток. Будь-який агент, якому потрібно використовувати обидва, змушений індивідуально вивчати ці особливості та враховувати кожну з них по-своєму. Якщо до цього додати кожен наступний інструмент, який з часом потрібен системі — власний клієнт, власну схему автентифікації, власний спосіб обробки помилок — тоді те, що спочатку було кількома невеликими інтеграціями, перетворюється на справжню тягар для обслуговування, без жодної спільної структури, яка б їх об’єднувала.

Ось базовий принцип, до якого варто триматися: це не погано написана інтеграція, а звичайна, створена у тому форматі, в якому зазвичай виглядають інтеграції інструментів, коли немає жодних правил щодо їхнього спільного форматування.

Що насправді стандартизує MCP

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

У своїй суті MCP визначає спільний протокол для надання інструментів агенту, незалежно від того, що інструмент робить усередині чи якою мовою чи фреймворком він був створений. Замість того, щоб кожен інструмент поставляв власний клієнт із власними правилами, він доступний через сервер MCP, який оголошує про свої можливості у передбачуваному, стандартному форматі: назва, опис, схема вхідних даних та схема вихідних даних. Будь-який агент, який розуміє цей протокол, може знайти такий інструмент та викликати його так само, як і будь-який інший інструмент, незалежно від того, чи є основою REST-кінцева точка, база даних чи щось зовсім інше.

Ця стандартизація охоплює саме три сфери, і варто чітко вказати, які саме, адже існує спокуса вважати, що обсяг MCP ширший, ніж він є насправді.

  • Виявлення: Агент може запитати сервер MCP щодо списку інструментів, які він надає, та отримати структуровану відповідь, замість того щоб покладатися на інформацію, закодовану якось окремо чи задокументовану окремо від фактичної реалізації.
  • Виклик: Кожен виклик інструменту дотримується однакової схеми, незалежно від того, який це інструмент — запит з визначеною структурою та відповідь з визначеною структурою, замість того щоб кожен клієнт визначав власний сигнатур методу та власний тип повернення.
  • Обробка помилок: Помилки повідомляються у єдиному форматі, тож агенту не потрібно стежити за тим, чи генерує певний інструмент виняток, повертає поле з помилкою чи зазнає невдачі іншим способом. Формат завжди залишається однаковим.
  • MCP не усуває необхідності написання справжньої логіки інструменту, а також не гарантує правильної роботи інструменту лише через те, що він інтегрований у цей протокол. Він стандартизує контракт між агентом та інструментом, але не якість чи надійність того, що знаходиться за цим контрактом — на це прямо наголошується у статті пізніше, після того як порівняння зробили цю різницю конкретною.

    Анатомія сервера MCP

    З огляду на щойно описану стандартизацію, варто розглянути, що насправді її реалізує. За своєю структурою сервер MCP — це не що інше, як набір оголошених інструментів, кожен з яких має власну схему, запакований у шар протоколу, який дозволяє агенту знаходити та викликати їх у послідовний спосіб.

    Щонайменше, для визначення інструменту на сервері MCP необхідно оголосити три елементи: назву, яку використовує агент для його посилання, схему, що описує очікуваний вхідний даний, та функцію, яка виконується під час фактичного виклику інструменту.

    from mcp.server import Server
    from mcp.types import Tool
    server = Server("lookup-tools")
    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return [
            Tool(
                name="lookup",
                description="Search for a record by query string",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"}
                    },
                    "required": ["query"],
                },
            )
        ]
    @server.call_tool()
    async def call_tool(name: str, arguments: dict) -> dict:
        if name == "lookup":
            return perform_lookup(arguments["query"])
        raise ValueError(f"unknown tool: {name}")
    

    При порівнянні з раніше обговореними спеціальними клієнтами тут виділяються два аспекти. По-перше, схема вхідних даних оголошується явно на початку, а не виводиться з параметрів функції — це дозволяє як агенту, так і людському рецензентові бачити точно, що потрібно інструменту, не заглиблюючись у його реалізацію. По-друге, серверу потрібно реалізувати лише два вхідні точки — list_tools та call_tool — незалежно від кількості інструментів у ньому чи від того, наскільки різно вони працюють усередині. Те, чи інструмент lookup взаємодіє з REST API, запитує базу даних чи робить щось зовсім інше, залишається повністю прихованим за цією двофункційною архітектурою.

    З боку агента підключення до цього сервера виглядає однаково, незалежно від того, які інструменти він надає:

    from mcp.client import ClientSession
    async def call_lookup_tool(query: str) -> dict:
        async with ClientSession(server_params) as session:
            result = await session.call_tool("lookup", {"query": query})
            return result
    

    Порівняйте це з двома тимчасовими клієнтами, створеними раніше: один на основі requests, інший — на основі psycopg2; кожен з них має свою унікальну структуру та конвенції. Тут код агента залишається незмінним, незалежно від того, як інструмент працює всередині — він викликає session.call_tool з ім’ям та набором аргументів, і щоразу отримує результат у тій самій структурі. Саме ця однорідність є справжньою перевагою архітектури сервера, а не самої логіки інструменту, яку все одно доводиться писати комусь.

    Перепідключення тієї ж інтеграції через MCP

    Найкращий спосіб побачити практичну різницю — це взяти точно таку саму інтеграцію, створену раніше, а саме інструмент пошуку та клієнт записів, і перебудувати її з використанням MCP. Функціональність залишається ідентичною, базові системи також залишаються незмінними, змінюється лише інтерфейс.

    Інструмент пошуку, який спочатку був окремим клієнтом на основі requests, тепер стає заявкою на інструмент, зареєстрованою на сервері MCP:

    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return [
            Tool(
                name="lookup",
                description="Search for a record by query string",
                inputSchema={
                    "type": "object",
                    "properties": {"query": {"type": "string"}},
                    "required": ["query"],
                },
            ),
            Tool(
                name="fetch_record",
                description="Fetch a record by ID",
                inputSchema={
                    "type": "object",
                    "properties": {"record_id": {"type": "string"}},
                    "required": ["record_id"],
                },
            ),
        ]
    @server.call_tool()
    async def call_tool(name: str, arguments: dict) -> dict:
        if name == "lookup":
            return perform_lookup(arguments["query"])
        elif name == "fetch_record":
            return fetch_record_from_db(arguments["record_id"])
        raise ValueError(f"unknown tool: {name}")
    

    Жодна з внутрішніх логік не була змінена: perform_lookup все ще викликає той самий зовнішній API, а fetch_record_from_db все ще виконує той самий запит до бази даних. Різниця полягає у тому, що ці два інструменти, хоча й базуються на абсолютно різних системах, тепер оголошуються поруч один з одним, мають ідентичну структуру схеми та доступні через ті самі два точки входу.

    Найбільша помітна зміна відбувається у вузлі, відповідальному за їх виклик, оскільки йому більше не потрібна інформація про requests чи psycopg2:

    async def agent_node(state: GraphState) -> dict:
        async with ClientSession(server_params) as session:
            result = await session.call_tool(
                "lookup", {"query": state["extracted_fields"]["query"]}
            )
        return {"tool_result": result}
    

    Якщо порівнювати з попередньою версією, яка імпортувала певну бібліотеку HTTP, парсила певний формат помилок та вимагала абсолютно окремого шляху коду лише для того, щоб викликати fetch_record замість lookup, то в цій версії виклик другого інструменту полягає у заміні рядка та словника аргументів, а не у створенні окремого клієнта з власними особливостями.

    Нічого з того, що насправді роблять ці інструменти, не змінилося. Змінилося те, що агент, який їх використовує, більше зовсім не повинен розуміти їхню реалізацію — достатньо знати лише їхню назву та оголошену схему. Це і є стандартизація, про яку йшлося раніше в цій серії; тепер її можна показати в реальному коді, замість того щоб покладатися на слова.

    Порівняння: Ad Hoc проти MCP

    Коли обидва підходи повністю реалізовані, можна припинити обговорювати теоретичні переваги та безпосередньо подивитися, у чому саме полягають відмінності між версією Ad Hoc та версією MCP.

    • Зусилля, необхідні для кожного нового інструменту: У ad hoc підході кожен додатковий інструмент мав власного клієнта, власну логіку автентифікації, власну структуру помилок та свій набір правил, які потрібно було опанувати, перш ніж можна було безпечно ним скористатися. За MCP додавання інструменту означає додавання ще одного елемента до list_tools та ще однієї гілки всередині call_tool, причому кожен з них дотримується ідентичної структури, як і попередній інструмент.
    • Що має розуміти код, який викликає інструмент: У ad hoc підході вузол агента мусив імпортувати певну бібліотеку клієнта та обробляти специфічний формат помилок цієї бібліотеки. Вузол агента MCP не імпортує нічого, що було б специфічним для конкретного інструменту; він просто викликає session.call_tool з ім’ям та словником аргументів, і цей виклик поводиться однаково незалежно від того, чи є базовим інструментом REST-кінець, база даних чи щось інше.
  • Наскільки можливе повторне використання конфігурації між агентами: У спонтанному підході, якщо другому агенту потрібна така сама функція пошуку, він або імпортує той самий клієнт безпосередньо, що призводить до прив’язки до цієї конкретної реалізації, або хтось зрештою створює спільний обгорток, щоб уникнути дублювання. З сервером MCP другий агент просто під’єднується до цього сервера та успадковує той самий набір інструментів, які можна знаходити та викликати так само, без необхідності знань, які вже були у першого агента.
  • Скільки потрібно підтримувати: Щоб налаштувати роботу інструменту пошуку, додати політику повторних спроб або змінити таймаут, необхідно безпосередньо редагувати LookupToolClient, і кожен агент, який від нього залежить, автоматично отримує ці зміни. MCP не усуває цей тягар підтримки, але об’єднує його: тепер поведінка кожного інструменту знаходиться за допомогою двох однакових функцій — list_tools та call_tool, замість того щоб бути розкиданою між різними формами клієнтів, кількість яких дорівнює кількості інструментів.
  • Жоден з цих факторів не спрощує справжню логіку інструменту. Функції perform_lookup та fetch_record_from_db все одно потрібно написати та забезпечити їхню правильну роботу. Змінюється лише все, що оточує цю логіку: спосіб її виявлення, спосіб її виклику, спосіб виявлення помилок та те, наскільки багато з цього тягаря має нести власний код агента, порівняно з автоматичним обробленням за допомогою спільного протоколу.

    Інтеграція інструменту MCP у систему LangGraph (з частини 2)

    До цього моменту кожен приклад стосувався окремого агента, який ізольовано викликав один інструмент. Варто заповнити цю прогалину, повернувши інструмент на основі MCP у багатоагентну структуру, створену у частині 2, адже саме там справді зустрічаються підходи з усіх трьох статей цієї серії.

    Згадайте граф з Частини 2: крок отримання даних, інтерпретація за допомогою LLM, двигун правил, представлений у Частині 1, та умовне направлення потоку або до вузла сповіщень, або на людський огляд. Уявіть собі, що тепер двигун правил перед ухваленням рішення залежить від перевірки у якійсь зовнішній системі — така залежність раніше вимагала створення спеціалізованого клієнта, подібного до того, що було створено раніше в цій статті, підключеного безпосередньо до цього вузла.

    Коли сервер MCP надає цей самий механізм пошуку як інструмент, обов’язки вузла майже не змінюються. Він все ще читає дані з стану графа та записує своє рішення назад у цей стан, просто для взаємодії з зовнішньою системою використовує функцію session.call_tool, замість того щоб працювати через спеціалізований клієнт:

    async def rules_engine_node(state: GraphState) -> dict:
        async with ClientSession(server_params) as session:
            lookup_result = await session.call_tool(
                "lookup", {"query": state["extracted_fields"]["category"]}
            )
        decision = evaluate_rules(state["extracted_fields"], lookup_result)
        return {"decision": decision["decision"], "decision_reason": decision["reason"]}
    

    Позиція вузла у загальному потоці не змінюється під впливом усього цього. Він продовжує знаходитися на тому самому місці — нижче за llm_interpretation та вище за умовну гілку, яка вже існувала у Частині 2; він все ще обмежений діапазоном дозволів, визначеним у розділі про правила цього документа, та все ще записується під тим самим ідентифікатором сліду, описаним у розділі про спостережливість. Єдиною справжньою зміною є спосіб взаємодії вузла з зовнішнім світом: замість того, щоб покладатися на клієнта, створеного спеціально для цього вузла, тепер він здійснює виклик до інструменту, який будь-який інший вузол, чи то в цій схемі, чи в майбутній, може використати через той самий шлях.

    Саме тут з’єднуються три основні складові цієї серії. Агент, який приймає рішення лише у тих випадках, коли це прямо дозволено, працює всередині графу, який координує його діяльність разом з іншими агентами, та підключається до зовнішніх систем через ту саму стандартизовану інтерфейс, якою користуються всі агенти. Жодній з цих трьох складових — двигуну правил, графу чи MCP — не потрібна детальна інформація про дві інші. Їм достатньо дотримуватися тієї ж дисципліни, яку підкреслювала ця серія протягом усього: чіткі обов’язки, прямі угоди та відсутність припущень.

    Витрати та затримки: що насправді коштує оркестрація

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

    Розгляньмо окремий запит, який проходить через граф, представлений у Частині 2, тепер розширений за допомогою засобу пошуку на основі MCP, про який йшлося вище. Шлях обробки запиту можна описати приблизно так: етап прийому запиту виконує легке форматування без зовнішніх викликів, тож це коштує щонайбільше кількох мілісекунд. На етапі інтерпретації за допомогою LLM викликається модель для вилучення структурованих полів з необробленого запиту, і це зазвичай є найбільшим етапом у всьому процесі, що додає кілька сотень мілісекунд залежно від вибору моделі та довжини запиту. Виклик інструменту пошуку з боку двигуна правил через MCP додає до часу, необхідного самому інструменту для відповіді, ще один маршрут у мережі; це значуща витрата, але зазвичай менша, ніж на етапі роботи LLM. Оцінка самих правил, оскільки це просто детерміністична логіка, майже не коштує. Етапи маршрутизації та надсилання підсумкового повідомлення додають лише незначну кількість часу до загального показника.

    Якщо підсумувати ці показники, стає зрозуміло: запит, який виконує лише одну операцію з моделлю та один виклик інструменту, має загальний час виконання, який майже повністю визначається саме цими двома операціями, причому власна координація графа майже не впливає на цей час. Вузли, ребра та спільний стан, представлені у Частині 2, надають структуру без створення значного затримки: читання та запис у об’єкт спільного стану є дешевими операціями. Те, що насправді спричиняє затримку, — це звернення до моделі чи зовнішньої системи.

    Це змінює підхід до оптимізації продуктивності. Додавання більшої кількості вузлів, гілок маршрутизації чи перевірок у графі майже не впливає на швидкість, адже це просто виклики функцій та пошук у словнику. Насправді систему уповільнюють кожен виклик ШІ та кожен виклик зовнішнього інструменту, які знаходяться на критичному шляху запиту. Система, побудована з трьох агентів, які по черзі викликають модель, працюватиме значно повільніше, ніж система, яка викликає модель лише один раз, незалежно від того, наскільки ефективним є оточуючий механізм оркестрації.

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

    Що MCP не вирішує

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

    • Стандартизація процесу пошуку та виклику інструментів не виправляє проблеми погано створеного інструменту: стандарт MCP стандартизує спосіб пошуку та виклику інструменту, але не те, що відбувається всередині нього. Інструмент із повільною, нестабільною чи погано структурованою реалізацією залишатиметься повільним, нестабільним та погано структурованим навіть після розміщення за сервером MCP. Протокол переміщує цю несумісність з коду виклику у власну реалізацію інструменту, але не змушує її зникнути.
  • Єдина форма помилки не означає ефективного керування помилками: збої все одно трапляються, інструменти все ще досягають тайм-ауту, а зовнішні системи все ще вимикаються. MCP надає цим збоям передбачувану, послідовну форму під час їх виникнення, але код, який їх викликає, все одно несе відповідальність за прийняття рішення про подальші дії — чи спробувати ще раз, перейти на альтернативний варіант чи передати помилку вище, як і раніше. Послідовний формат помилки не є еквівалентом фактичного оброблення збою.
  • Використання протоколу має власні операційні витрати: сервер MCP — це ще один процес, який потрібно запускати, розгортати та підтримувати у належному стані. Для окремого агента, який використовує простий інструмент, це справжня додаткова навантаженість, а підхід з ad hoc-клієнтом, описаний раніше в серії, був би швидшим у налаштуванні та зрозумілішим. Цю навантаженість посилює молодий вік екосистеми: інструментарій, підтримка у вирішенні проблем та усталені конвенції все ще відстають від таких зрілих рішень, як звичайний REST API. На практиці це означає необхідність витрачати більше часу на пряме читання вихідного коду та специфікацій, оскільки менше є перевірених патернів, на які можна покластися.
  • Усе це не заперечує доцільності впровадження MCP. Це нагадування про те, що вибір протоколу не замінює інженерної роботи, яка все одно має бути виконана під ним. Зміни, які приносить MCP, є реальними, як показано у попередніх розділах, але вони є вужчими, ніж концепція „агенти, які підключаються до інструментів“ в цілому, тож варто бути точними щодо того, де саме проходить ця межа, перш ніж вирішувати, чи має сенс її впровадження, про що йтиметься у наступному розділі.

    Коли варто впроваджувати MCP

    З огляду на викладені вище компроміси, тут немає однозначної відповіді — жодного чіткого „завжди впроваджуйте його“ чи „навіть не намагайтесь“. Натомість важливо перевірити кілька конкретних умов перед тим, як впроваджувати його в будь-якій системі.

    • Більше одного агента буде потребувати одних і тих самих інструментів. Майже весь ефект, описаний раніше, досягається завдяки повторному використанню: другий агент може під’єднатися до вже створеного сервера, замість того щоб копіювати клієнта чи створювати його пізніше. Коли є лише один агент та один інструмент, такого ефекту просто ще немає — немає чого ділити, — тож підхід ad hoc, розглянутий раніше, залишається простішим вибором у цьому конкретному випадку.
    • Очікується зростання кількості інструментів. Стандартизований інтерфейс стає ціннішим у міру того, як за ним з’являється все більше інструментів. З двома чи трьома інструментами запуск спеціалізованого сервера може не вартувати всіх зусиль. Коли ж йдеться про десять чи двадцять інструментів — кожен з яких інакше потребував би власного спеціального клієнта — тягар обслуговування за підходом ad hoc починає ставати справжньою проблемою.
  • Система має вижити після свого першого випуску. Однією з переваг MCP є те, що додавання нового агента чи інструменту згодом не змушує перепроектовувати все, що вже існує. Ця перевага накопичується протягом усього терміну існування системи, і її легко не помітити, якщо враховувати лише вартість початкової налаштування.
  • З іншого боку, це не підходить для ситуації, коли один агент взаємодіє з одним стабільним, добре зрозумілим інструментом, який не збирається змінюватися. У такому випадку створення спеціального клієнта є швидшим, залишається на одну менше деталь для обслуговування, а координація, яку пропонує MCP, не має іншого користувача, який би її фактично використовував.

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

    Висновок: завершення серії

    Протягом трьох статей поступово розвивалася одна система, де кожен шар ґрунтувався на попередньому. У першій частині було створено окремого агента, достатньо надійного для прийняття реальних рішень, шляхом суворого розділення процесу інтерпретації та самого акту прийняття рішення. У другій частині цьому агенту була надана підтримка шляхом об’єднання кількох спеціалізованих агентів у мережу, яка дозволяла їм обмінюватися інформацією, захищена чіткими дозволами та забезпечувала прозорість від початку до кінця, щоб можна було відстежувати кожен запит з моменту його надходження до моменту виконання. У цій статті була подолана остання проблема — як ці агенти можуть діяти поза межами мережі — шляхом стандартизації доступу через MCP у ситуаціях, де це справді є доцільним, водночас чітко визначивши ситуації, де це не потрібно.

    None of these three pieces is especially complex on its own. What actually makes the resulting system dependable is one habit, repeated at every layer without exception: responsibilities stay narrow, contracts stay explicit, and nothing is left to guesswork or allowed to happen quietly in the background. That habit is the real subject of this series, more than any particular tool—LangGraph and MCP just happened to be the frameworks used to put it into practice, but the underlying principles hold regardless of which tools you reach for.