惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

雷峰网
雷峰网
宝玉的分享
宝玉的分享
I
InfoQ
P
Privacy International News Feed
V
V2EX
IT之家
IT之家
S
SegmentFault 最新的问题
D
Darknet – Hacking Tools, Hacker News & Cyber Security
V2EX - 技术
V2EX - 技术
C
CERT Recently Published Vulnerability Notes
C
Check Point Blog
The Register - Security
The Register - Security
爱范儿
爱范儿
博客园 - 三生石上(FineUI控件)
AWS News Blog
AWS News Blog
M
MIT News - Artificial intelligence
C
Cyber Attacks, Cyber Crime and Cyber Security
F
Fortinet All Blogs
B
Blog
N
Netflix TechBlog - Medium
B
Blog RSS Feed
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Last Week in AI
Last Week in AI
T
Threatpost
Forbes - Security
Forbes - Security
U
Unit 42
A
Arctic Wolf
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Palo Alto Networks Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recorded Future
Recorded Future
L
Lohrmann on Cybersecurity
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Proofpoint News Feed
月光博客
月光博客
Spread Privacy
Spread Privacy
MongoDB | Blog
MongoDB | Blog
Jina AI
Jina AI
I
Intezer
V
Visual Studio Blog
阮一峰的网络日志
阮一峰的网络日志
The Hacker News
The Hacker News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
L
LangChain Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
博客园_首页
MyScale Blog
MyScale Blog
腾讯CDC
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
量子位

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Собираем AI-агента нового поколения: Python, RAG и внешние инструменты через MCP (Model Context Protocol)
kardanShurup · 2026-04-20 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение13 мин

Охват и читатели3.5K

Туториал

Введение: от простых цепочек к агентам, которые действуют

Ещё пару лет назад типичное LLM-приложение выглядело как последовательная цепочка вызовов: взяли промпт, добавили контекст из векторной базы, отправили в модель, получили ответ. LangChain популяризировал эту парадигму — chains, retrievers, memory — и это работало для простых сценариев вроде «ответь на вопрос по документации».
Но бизнес-задачи редко укладываются в линейный пайплайн. Пользователь хочет не просто получить ответ, а чтобы система совершила действие: создала тикет в Jira, отправила письмо, запросила данные из CRM, проверила погоду и только потом сформулировала ответ. Именно здесь на сцену выходят AI-агенты — системы, которые не просто генерируют текст, а автономно принимают решение, какой инструмент вызвать, в каком порядке, и интерпретируют результат. Проблема в том, что до недавнего времени подключение каждого нового инструмента требовало написания «клея» — кастомных функций, обёрнутых в @tool декоратор LangChain, с ручным управлением аутентификацией, обработкой ошибок и сериализацией данных. Для продакшена это быстро превращалось в зоопарк нестандартных интеграций, который сложно поддерживать и масштабировать.
Model Context Protocol (MCP) от Anthropic решает эту проблему, предлагая единый стандарт для подключения инструментов и источников данных к LLM-приложениям. Вместо того чтобы для каждого API писать свой адаптер, мы просто запускаем MCP-сервер, который предоставляет инструменты по стандартизированному протоколу. Агент подключается к этому серверу через MCP-клиент и получает доступ ко всем инструментам без лишнего кода.
В этой статье мы соберём полноценного агента, который:

  1. Умеет работать с внешним миром через MCP (узнавать погоду и создавать GitHub Issues);

  2. Имеет доступ к внутренней базе знаний через RAG;

  3. Принимает решения по ReAct-подходу с использованием LangGraph.

Теоретический минимум: что нужно знать перед погружением в код

AI-агент и ReAct-подход

В контексте LLM агент — это система, которая работает в цикле: Мысль → Действие → Наблюдение. Этот паттерн называется ReAct (Reasoning + Acting).

  1. Агент получает задачу от пользователя.

  2. Модель анализирует («думает»), какой инструмент нужно вызвать и с какими параметрами.

  3. Агент вызывает инструмент и получает результат («наблюдение»).

  4. Цикл повторяется, пока модель не решит, что информации достаточно для финального ответа.

Ключевое отличие от обычного chain — модель сама решает, когда и какие инструменты использовать, а не следует жёстко заданной последовательности.

RAG (Retrieval-Augmented Generation)

RAG — это техника, при которой перед генерацией ответа мы извлекаем из внешнего хранилища (векторной базы) релевантные документы и добавляем их в контекст модели. Для агента RAG — это просто ещё один инструмент. Когда агенту нужна информация из внутренней документации, он вызывает инструмент search_documentation, а не пытается ответить по памяти (и, возможно, галлюцинировать).

Model Context Protocol (MCP)

MCP — это открытый протокол, построенный поверх JSON-RPC, который стандартизирует взаимодействие между AI-приложениями и внешними источниками данных и инструментами. Архитектура включает три ключевых компонента:

  • MCP Host — AI-приложение (например, Claude Desktop или наш Python-агент).

  • MCP Client — компонент внутри хоста, который поддерживает соединение с одним MCP-сервером.

  • MCP Server — программа, которая предоставляет инструменты, ресурсы и промпты по стандартизированному протоколу.

Проще говоря: MCP делает для AI-агентов то же, что REST API сделал для веб-сервисов — универсальный способ взаимодействия, не зависящий от конкретной реализации.

Обзор стека: что и почему мы будем использовать

Компонент

Выбор

Обоснование

Python

3.11+

Поддержка asyncio, современный синтаксис, стабильность

Фреймворк агента

LangGraph + LangChain

LangGraph даёт явный контроль над циклом ReAct и состоянием; LangChain предоставляет удобные абстракции для инструментов и моделей-

MCP-сервер

FastMCP

Высокоуровневая Python-библиотека, которая сводит создание MCP-сервера к декорированию функций. FastMCP 1.0 был включён в официальный MCP Python SDK, а версия 2.0 активно развивается и добавляет возможности клиента

MCP-адаптер

langchain-mcp-adapters

Официальная библиотека от LangChain, которая конвертирует MCP-инструменты в формат, понятный LangChain/LangGraph-

Векторная БД

ChromaDB

Лёгкая, встраиваемая, отлично работает для прототипов и небольших проектов

Эмбеддинги

OpenAI text-embedding-3-small

Качественные и недорогие эмбеддинги

LLM

Claude (через Anthropic API) или GPT-4

Любая модель, поддерживающая function calling

Практическая часть: пишем код

Часть 1. Создание MCP-сервера для работы с внешним миром

Начнём с создания MCP-сервера, который предоставит агенту два инструмента: получение погоды и создание GitHub Issue.
Установим FastMCP:

pip install fastmcp httpx

Создадим файл tools_server.py:

# tools_server.py
import os
import httpx
from fastmcp import FastMCP

# Инициализируем MCP-сервер с именем "ExternalTools"
mcp = FastMCP("ExternalTools")

# ========== Инструмент 1: Получение погоды ==========
@mcp.tool()
async def get_weather(city: str) -> str:
    """
    Получает текущую погоду для указанного города.
    
    Args:
        city: Название города (например, "Moscow" или "London")
    
    Returns:
        Строка с описанием погоды
    """
    # Используем бесплатный Open-Meteo API (не требует ключа)
    # Сначала получаем координаты города через geocoding API
    async with httpx.AsyncClient() as client:
        geo_response = await client.get(
            "https://geocoding-api.open-meteo.com/v1/search",
            params={"name": city, "count": 1, "language": "en", "format": "json"},
            timeout=10.0
        )
        geo_response.raise_for_status()
        geo_data = geo_response.json()
        
        if not geo_data.get("results"):
            return f"Город '{city}' не найден"
        
        location = geo_data["results"][0]
        lat, lon = location["latitude"], location["longitude"]
        city_name = location["name"]
        
        # Получаем погоду по координатам
        weather_response = await client.get(
            "https://api.open-meteo.com/v1/forecast",
            params={
                "latitude": lat,
                "longitude": lon,
                "current_weather": True,
                "timezone": "auto"
            },
            timeout=10.0
        )
        weather_response.raise_for_status()
        weather_data = weather_response.json()
        
        current = weather_data["current_weather"]
        return (
            f"Погода в {city_name}:\n"
            f"Температура: {current['temperature']}°C\n"
            f"Скорость ветра: {current['windspeed']} км/ч\n"
            f"Код погоды: {current['weathercode']}"
        )

# ========== Инструмент 2: Создание GitHub Issue ==========
@mcp.tool()
async def create_github_issue(
    repo: str,
    title: str,
    body: str,
    labels: list[str] | None = None
) -> str:
    """
    Создаёт новый Issue в указанном GitHub-репозитории.
    
    Args:
        repo: Полное имя репозитория (например, "username/repo-name")
        title: Заголовок Issue
        body: Текст Issue (поддерживает Markdown)
        labels: Список меток (опционально)
    
    Returns:
        Строка с результатом операции
    """
    github_token = os.environ.get("GITHUB_TOKEN")
    if not github_token:
        return "Ошибка: переменная окружения GITHUB_TOKEN не установлена"
    
    url = f"https://api.github.com/repos/{repo}/issues"
    headers = {
        "Authorization": f"Bearer {github_token}",
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28"
    }
    payload = {"title": title, "body": body}
    if labels:
        payload["labels"] = labels
    
    async with httpx.AsyncClient() as client:
        response = await client.post(
            url,
            json=payload,
            headers=headers,
            timeout=30.0
        )
        
        if response.status_code == 201:
            data = response.json()
            return f"Issue успешно создан: {data['html_url']}"
        elif response.status_code == 404:
            return f"Ошибка: репозиторий '{repo}' не найден или нет доступа"
        else:
            return f"Ошибка GitHub API: {response.status_code} - {response.text}"


if __name__ == "__main__":
    # Запускаем сервер с транспортом stdio (стандартный ввод/вывод)
    # Это позволяет клиенту запускать сервер как подпроцесс
    mcp.run(transport="stdio")

Что здесь происходит:

  1. Мы создаём экземпляр FastMCP и декорируем функции @mcp.tool() — FastMCP автоматически генерирует JSON-схему для параметров на основе сигнатуры функции и docstring.

  2. get_weather использует бесплатный Open-Meteo API (не требует API-ключа).

  3. create_github_issue требует токен GitHub с правами на создание issues (создайте его в настройках GitHub с scope repo).

  4. Сервер запускается с транспортом stdio — это значит, что клиент будет запускать этот скрипт как подпроцесс и общаться с ним через стандартный ввод/вывод по протоколу JSON-RPC.


Запуск сервера (локально, для теста):

export GITHUB_TOKEN="your_github_personal_access_token"
python tools_server.py

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

Часть 2. Настройка RAG-модуля

Теперь создадим RAG-систему, которая будет индексировать PDF-документацию и предоставлять агенту инструмент для поиска.
Установим зависимости:

pip install langchain langchain-openai chromadb pypdf

Создадим файл rag_tool.py:

# rag_tool.py
import os
from pathlib import Path
from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.tools import tool

# Путь для хранения векторной базы
CHROMA_PATH = "./chroma_db"
DATA_PATH = "./data"  # Папка с PDF-файлами


def initialize_vector_store(force_reload: bool = False) -> Chroma:
    """
    Инициализирует или загружает векторное хранилище ChromaDB.
    
    Args:
        force_reload: Если True, пересоздаёт базу заново из PDF-файлов
    
    Returns:
        Инстанс Chroma с загруженными документами
    """
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    
    # Если база уже существует и не требуется перезагрузка — просто загружаем
    if os.path.exists(CHROMA_PATH) and not force_reload:
        return Chroma(
            persist_directory=CHROMA_PATH,
            embedding_function=embeddings
        )
    
    # Иначе — создаём новую базу из PDF-файлов
    documents = []
    data_folder = Path(DATA_PATH)
    
    if not data_folder.exists():
        data_folder.mkdir(parents=True)
        print(f"Создана папка {DATA_PATH}. Поместите туда PDF-файлы и запустите снова.")
        # Возвращаем пустую базу
        return Chroma(
            persist_directory=CHROMA_PATH,
            embedding_function=embeddings
        )
    
    for pdf_file in data_folder.glob("*.pdf"):
        loader = PyPDFLoader(str(pdf_file))
        documents.extend(loader.load())
    
    if not documents:
        print(f"В папке {DATA_PATH} нет PDF-файлов")
        return Chroma(
            persist_directory=CHROMA_PATH,
            embedding_function=embeddings
        )
    
    # Разбиваем документы на чанки
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200,
        separators=["\n\n", "\n", ". ", " ", ""]
    )
    chunks = text_splitter.split_documents(documents)
    
    # Создаём и сохраняем векторную базу
    vector_store = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=CHROMA_PATH
    )
    # В новых версиях Chroma persist() вызывать не нужно — from_documents уже сохраняет
    print(f"Векторная база создана: {len(chunks)} чанков из {len(documents)} документов")
    
    return vector_store


# Глобальный экземпляр векторного хранилища
_vector_store: Chroma | None = None


def get_vector_store() -> Chroma:
    """Ленивая инициализация векторного хранилища."""
    global _vector_store
    if _vector_store is None:
        _vector_store = initialize_vector_store()
    return _vector_store


@tool
async def search_documentation(query: str) -> str:
    """
    Ищет информацию во внутренней документации (базе знаний).
    Используй этот инструмент, когда нужно ответить на вопрос, требующий
    обращения к документации компании или техническим спецификациям.
    
    Args:
        query: Поисковый запрос на естественном языке
    
    Returns:
        Релевантные фрагменты из документации
    """
    vector_store = get_vector_store()
    
    # Выполняем семантический поиск
    docs = vector_store.similarity_search(query, k=3)
    
    if not docs:
        return "В документации не найдено информации по вашему запросу."
    
    # Формируем ответ из найденных фрагментов
    results = []
    for i, doc in enumerate(docs, 1):
        source = doc.metadata.get("source", "неизвестный источник")
        results.append(f"[Фрагмент {i} из {source}]\n{doc.page_content}\n")
    
    return "\n".join(results)


# Функция для первоначальной загрузки документов (вызывается один раз при настройке)
def setup_rag():
    """Инициализирует RAG-систему, загружая все PDF из папки data/"""
    print("Инициализация RAG-системы...")
    store = initialize_vector_store(force_reload=True)
    print(f"Готово. В базе {store._collection.count()} документов.")

Ключевые моменты:

  1. Мы используем PyPDFLoader для загрузки PDF и RecursiveCharacterTextSplitter для разбивки на чанки с перекрытием.

  2. Векторное хранилище сохраняется на диск в папке chroma_db/ — при повторных запусках не нужно переиндексировать документы.

  3. Инструмент search_documentation обёрнут в декоратор @tool из LangChain — это делает его совместимым с LangGraph-агентом.

  4. Важно: docstring инструмента — это часть промпта для модели. Чем точнее описано, когда и зачем использовать инструмент, тем лучше агент будет принимать решения.

Часть 3. Сборка агента с доступом к MCP

Теперь соберём всё вместе: агент на LangGraph, который использует и MCP-инструменты, и RAG. Установим оставшиеся зависимости:

pip install langchain-mcp-adapters langgraph langchain-anthropic

Создадим файл agent.py:

# agent.py
import asyncio
import os
from pathlib import Path

from langchain_anthropic import ChatAnthropic
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent

# Импортируем наш RAG-инструмент
from rag_tool import search_documentation, setup_rag


async def main():
    # ========== 1. Инициализация RAG ==========
    print("Загрузка RAG-системы...")
    setup_rag()  # Индексирует PDF при первом запуске
    
    # ========== 2. Подключение к MCP-серверу ==========
    # MultiServerMCPClient управляет подключениями к нескольким MCP-серверам
    # В нашем случае сервер один, но архитектура позволяет легко добавлять новые
    mcp_client = MultiServerMCPClient(
        {
            "external_tools": {
                "transport": "stdio",  # Сервер запускается как подпроцесс
                "command": "python",
                # Указываем абсолютный путь к нашему серверу
                "args": [str(Path(__file__).parent / "tools_server.py")],
                # Передаём переменные окружения в подпроцесс
                "env": {
                    **os.environ,
                    # Убеждаемся, что токен GitHub передаётся
                }
            }
        }
    )
    
    # ========== 3. Получение инструментов ==========
    # Получаем инструменты из MCP-сервера
    mcp_tools = await mcp_client.get_tools()
    print(f"Загружено MCP-инструментов: {len(mcp_tools)}")
    for tool in mcp_tools:
        print(f"  - {tool.name}: {tool.description[:50]}...")
    
    # Объединяем MCP-инструменты с нашим RAG-инструментом
    all_tools = mcp_tools + [search_documentation]
    print(f"Всего инструментов: {len(all_tools)}")
    
    # ========== 4. Создание агента ==========
    # Используем Claude Sonnet 4 — у него отличная поддержка function calling
    llm = ChatAnthropic(
        model="claude-sonnet-4-6",
        temperature=0,
        max_tokens=4096
    )
    # Для использования OpenAI можно раскомментировать:
    # from langchain_openai import ChatOpenAI
    # llm = ChatOpenAI(model="gpt-4.1", temperature=0)
    
    # create_react_agent из LangGraph создаёт готовый ReAct-агент
    # Под капотом — граф с узлами: agent (вызов LLM) -> tools (выполнение) -> agent
    agent = create_react_agent(
        model=llm,
        tools=all_tools,
        # Опционально: можно передать system prompt с дополнительными инструкциями
        # prompt=...
    )
    
    # ========== 5. Запуск агента ==========
    print("\n" + "="*50)
    print("Агент готов к работе!")
    print("Примеры запросов:")
    print("  1. Узнай погоду в Москве и создай тикет в GitHub")
    print("  2. Поищи в документации, как настроить MCP сервер")
    print("  3. Создай issue о баге в репозитории username/repo")
    print("Введите 'exit' для выхода")
    print("="*50 + "\n")
    
    while True:
        user_input = input("\n Ваш запрос: ")
        if user_input.lower() in ("exit", "quit", "выход"):
            break
        
        # Запускаем агента с историей сообщений
        # LangGraph автоматически управляет состоянием разговора
        result = await agent.ainvoke(
            {"messages": [{"role": "user", "content": user_input}]}
        )
        
        # Извлекаем последнее сообщение от агента
        messages = result.get("messages", [])
        if messages:
            last_message = messages[-1]
            print(f"\n Ответ: {last_message.content}")
        else:
            print("\n Агент не вернул ответа")


if __name__ == "__main__":
    asyncio.run(main())

Разбор ключевых моментов:

  1. MultiServerMCPClient — центральный компонент для подключения к MCP-серверам. Он принимает конфигурацию, в которой для каждого сервера указывается транспорт (stdio или http) и параметры подключения.

  2. Транспорт stdio означает, что клиент запускает Python-скрипт как подпроцесс и общается с ним через стандартный ввод/вывод. Это удобно для локальной разработки. Для продакшена можно использовать HTTP-транспорт, запустив сервер отдельно.

  3. create_react_agent из LangGraph — это готовая реализация ReAct-агента. Под капотом создаётся граф с двумя узлами:
    1. Agent Node: вызывает LLM с текущим состоянием и инструментами.
    2. Tools Node: выполняет выбранные инструменты и возвращает результат.

  4. Агент работает асинхронно (async/await), что важно для эффективной работы с внешними API.

Часть 4. Демонстрация работы

Поместим в папку data/ PDF-файл с документацией (например, руководство по работе с GitHub API или внутренний регламент создания тикетов) и запустим агента:

export ANTHROPIC_API_KEY="your_anthropic_api_key"
export GITHUB_TOKEN="your_github_token"
export OPENAI_API_KEY="your_openai_api_key"  # для эмбеддингов

python agent.py

Ваш запрос: Узнай погоду в Москве и создай тикет в репозитории myorg/docs с отчётом о погоде. Перед этим проверь в документации, как правильно оформлять такие тикеты.

[AGENT LOG] Thought: Мне нужно выполнить три действия:

  1. Проверить документацию по оформлению тикетов

  2. Узнать погоду в Москве

  3. Создать тикет в GitHub с отчётом

Action: search_documentation Action Input: {“query”: “как оформлять тикеты о погоде”} Observation: [Фрагмент 1 из docs/internal-guide.pdf] Все отчёты о погоде должны содержать:

  • Город и дату

  • Температуру и скорость ветра

  • Рекомендации на основе погодных условий…

Thought: Теперь я знаю формат. Получу погоду в Москве.

Action: get_weather Action Input: {“city”: “Moscow”} Observation: Погода в Moscow: Температура: 18.5°C Скорость ветра: 12 км/ч…

Thought: Теперь создам тикет с отчётом.

Action: create_github_issue Action Input: { “repo”: “myorg/docs”, “title”: “Ежедневный отчёт о погоде в Москве”, “body”: “## Погода в Москве\n- Температура: 18.5°C\n- Ветер: 12 км/ч\n- Рекомендации: комфортная погода для прогулок”, “labels”: [“weather-report”, “automated”] } Observation: Issue успешно создан.

Ответ: Я выполнил все запрошенные действия:

  1. Проверил документацию по оформлению тикетов — все отчёты должны включать температуру, ветер и рекомендации.

  2. Узнал текущую погоду в Москве: 18.5°C, ветер 12 км/ч.

  3. Создал тикет в репозитории myorg/docs.

Что произошло:

  1. Агент самостоятельно спланировал последовательность действий.

  2. Вызвал RAG-инструмент для получения правил оформления.

  3. Вызвал MCP-инструмент для получения погоды.

  4. Вызвал ещё один MCP-инструмент для создания Issue.

  5. Синтезировал финальный ответ.

Сравнение с альтернативами и выводы

ритерий

Классический подход (@tool)

Подход с MCP

Интеграция нового API

Писать функцию-обёртку, декорировать @tool, управлять ошибками вручную

Запустить готовый MCP-сервер или написать свой с FastMCP

Переиспользование

Инструмент жёстко привязан к коду агента

Один MCP-сервер могут использовать несколько агентов и даже разные AI-приложения (Claude Desktop, Cursor, etc.)

Безопасность

Ключи API хранятся в коде агента или его окружении

MCP-сервер может работать в изолированном окружении со своими секретами

Стандартизация

Каждый инструмент — уникальная реализация

Единый протокол JSON-RPC, понятная структура

Сложность для простых задач

Минимальная

Требуется запуск отдельного процесса (для stdio) или HTTP-сервера

Плюсы MCP

  1. Масштабируемость: Экосистема MCP-серверов растёт — уже есть готовые серверы для GitHub, Slack, Google Drive, PostgreSQL и десятков других систем.

  2. Разделение ответственности: Команда, отвечающая за интеграцию с внешними сервисами, может разрабатывать и поддерживать MCP-серверы независимо от команды, строящей агентов.

  3. Единый стандарт: MCP поддерживается Anthropic, OpenAI (через Agents SDK) и другими крупными игроками-.

Минусы MCP

  1. Дополнительная прослойка: Для простых прототипов запуск отдельного MCP-сервера может быть избыточным.

  2. Молодость протокола: Спецификация всё ещё эволюционирует, могут быть breaking changes.

  3. Отладка: Отлаживать взаимодействие через JSON-RPC сложнее, чем просто вызвать Python-функцию (хотя MCP Inspector частично решает эту проблему).

Заключение

Model Context Protocol — это не просто очередной фреймворк, а стандарт взаимодействия, который меняет подход к построению AI-агентов. Вместо того чтобы каждый раз изобретать велосипед для интеграции с очередным API, мы можем использовать готовые MCP-серверы или быстро создавать свои с помощью FastMCP.

В этой статье мы построили агента, который:

  • Использует внешние инструменты через стандартизированный MCP-протокол;

  • Имеет доступ к внутренней базе знаний через RAG;

  • Принимает решения автономно, следуя ReAct-паттерну.

Это архитектура, которая легко масштабируется: добавить поддержку нового сервиса — значит просто подключить ещё один MCP-сервер в конфигурации MultiServerMCPClient. Добавить новые документы — бросить PDF в папку data/.

Будущее за модульными AI-системами, и MCP — один из ключевых кирпичиков в этом фундаменте. Пора внедрять.