Введение: от простых цепочек к агентам, которые действуют
Ещё пару лет назад типичное LLM-приложение выглядело как последовательная цепочка вызовов: взяли промпт, добавили контекст из векторной базы, отправили в модель, получили ответ. LangChain популяризировал эту парадигму — chains, retrievers, memory — и это работало для простых сценариев вроде «ответь на вопрос по документации».
Но бизнес-задачи редко укладываются в линейный пайплайн. Пользователь хочет не просто получить ответ, а чтобы система совершила действие: создала тикет в Jira, отправила письмо, запросила данные из CRM, проверила погоду и только потом сформулировала ответ. Именно здесь на сцену выходят AI-агенты — системы, которые не просто генерируют текст, а автономно принимают решение, какой инструмент вызвать, в каком порядке, и интерпретируют результат. Проблема в том, что до недавнего времени подключение каждого нового инструмента требовало написания «клея» — кастомных функций, обёрнутых в @tool декоратор LangChain, с ручным управлением аутентификацией, обработкой ошибок и сериализацией данных. Для продакшена это быстро превращалось в зоопарк нестандартных интеграций, который сложно поддерживать и масштабировать.
Model Context Protocol (MCP) от Anthropic решает эту проблему, предлагая единый стандарт для подключения инструментов и источников данных к LLM-приложениям. Вместо того чтобы для каждого API писать свой адаптер, мы просто запускаем MCP-сервер, который предоставляет инструменты по стандартизированному протоколу. Агент подключается к этому серверу через MCP-клиент и получает доступ ко всем инструментам без лишнего кода.
В этой статье мы соберём полноценного агента, который:
Умеет работать с внешним миром через MCP (узнавать погоду и создавать GitHub Issues);
Имеет доступ к внутренней базе знаний через RAG;
Принимает решения по ReAct-подходу с использованием LangGraph.
Теоретический минимум: что нужно знать перед погружением в код
AI-агент и ReAct-подход
В контексте LLM агент — это система, которая работает в цикле: Мысль → Действие → Наблюдение. Этот паттерн называется ReAct (Reasoning + Acting).
Агент получает задачу от пользователя.
Модель анализирует («думает»), какой инструмент нужно вызвать и с какими параметрами.
Агент вызывает инструмент и получает результат («наблюдение»).
Цикл повторяется, пока модель не решит, что информации достаточно для финального ответа.
Ключевое отличие от обычного 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+ |
Поддержка |
Фреймворк агента |
LangGraph + LangChain |
LangGraph даёт явный контроль над циклом ReAct и состоянием; LangChain предоставляет удобные абстракции для инструментов и моделей- |
MCP-сервер |
FastMCP |
Высокоуровневая Python-библиотека, которая сводит создание MCP-сервера к декорированию функций. FastMCP 1.0 был включён в официальный MCP Python SDK, а версия 2.0 активно развивается и добавляет возможности клиента |
MCP-адаптер |
|
Официальная библиотека от LangChain, которая конвертирует MCP-инструменты в формат, понятный LangChain/LangGraph- |
Векторная БД |
ChromaDB |
Лёгкая, встраиваемая, отлично работает для прототипов и небольших проектов |
Эмбеддинги |
OpenAI |
Качественные и недорогие эмбеддинги |
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")
Что здесь происходит:
Мы создаём экземпляр
FastMCPи декорируем функции@mcp.tool()— FastMCP автоматически генерирует JSON-схему для параметров на основе сигнатуры функции и docstring.get_weatherиспользует бесплатный Open-Meteo API (не требует API-ключа).create_github_issueтребует токен GitHub с правами на создание issues (создайте его в настройках GitHub с scoperepo).Сервер запускается с транспортом
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()} документов.")
Ключевые моменты:
Мы используем
PyPDFLoaderдля загрузки PDF иRecursiveCharacterTextSplitterдля разбивки на чанки с перекрытием.Векторное хранилище сохраняется на диск в папке
chroma_db/— при повторных запусках не нужно переиндексировать документы.Инструмент
search_documentationобёрнут в декоратор@toolиз LangChain — это делает его совместимым с LangGraph-агентом.Важно: 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())
Разбор ключевых моментов:
MultiServerMCPClient — центральный компонент для подключения к MCP-серверам. Он принимает конфигурацию, в которой для каждого сервера указывается транспорт (
stdioилиhttp) и параметры подключения.Транспорт stdio означает, что клиент запускает Python-скрипт как подпроцесс и общается с ним через стандартный ввод/вывод. Это удобно для локальной разработки. Для продакшена можно использовать HTTP-транспорт, запустив сервер отдельно.
create_react_agentиз LangGraph — это готовая реализация ReAct-агента. Под капотом создаётся граф с двумя узлами:
1. Agent Node: вызывает LLM с текущим состоянием и инструментами.
2. Tools Node: выполняет выбранные инструменты и возвращает результат.Агент работает асинхронно (
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: Мне нужно выполнить три действия:
Проверить документацию по оформлению тикетов
Узнать погоду в Москве
Создать тикет в 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 успешно создан.
Ответ: Я выполнил все запрошенные действия:
Проверил документацию по оформлению тикетов — все отчёты должны включать температуру, ветер и рекомендации.
Узнал текущую погоду в Москве: 18.5°C, ветер 12 км/ч.
Создал тикет в репозитории myorg/docs.
Что произошло:
Агент самостоятельно спланировал последовательность действий.
Вызвал RAG-инструмент для получения правил оформления.
Вызвал MCP-инструмент для получения погоды.
Вызвал ещё один MCP-инструмент для создания Issue.
Синтезировал финальный ответ.
Сравнение с альтернативами и выводы
ритерий |
Классический подход ( |
Подход с MCP |
|---|---|---|
Интеграция нового API |
Писать функцию-обёртку, декорировать |
Запустить готовый MCP-сервер или написать свой с FastMCP |
Переиспользование |
Инструмент жёстко привязан к коду агента |
Один MCP-сервер могут использовать несколько агентов и даже разные AI-приложения (Claude Desktop, Cursor, etc.) |
Безопасность |
Ключи API хранятся в коде агента или его окружении |
MCP-сервер может работать в изолированном окружении со своими секретами |
Стандартизация |
Каждый инструмент — уникальная реализация |
Единый протокол JSON-RPC, понятная структура |
Сложность для простых задач |
Минимальная |
Требуется запуск отдельного процесса (для stdio) или HTTP-сервера |
Плюсы MCP
Масштабируемость: Экосистема MCP-серверов растёт — уже есть готовые серверы для GitHub, Slack, Google Drive, PostgreSQL и десятков других систем.
Разделение ответственности: Команда, отвечающая за интеграцию с внешними сервисами, может разрабатывать и поддерживать MCP-серверы независимо от команды, строящей агентов.
Единый стандарт: MCP поддерживается Anthropic, OpenAI (через Agents SDK) и другими крупными игроками-.
Минусы MCP
Дополнительная прослойка: Для простых прототипов запуск отдельного MCP-сервера может быть избыточным.
Молодость протокола: Спецификация всё ещё эволюционирует, могут быть breaking changes.
Отладка: Отлаживать взаимодействие через JSON-RPC сложнее, чем просто вызвать Python-функцию (хотя MCP Inspector частично решает эту проблему).
Заключение
Model Context Protocol — это не просто очередной фреймворк, а стандарт взаимодействия, который меняет подход к построению AI-агентов. Вместо того чтобы каждый раз изобретать велосипед для интеграции с очередным API, мы можем использовать готовые MCP-серверы или быстро создавать свои с помощью FastMCP.
В этой статье мы построили агента, который:
Использует внешние инструменты через стандартизированный MCP-протокол;
Имеет доступ к внутренней базе знаний через RAG;
Принимает решения автономно, следуя ReAct-паттерну.
Это архитектура, которая легко масштабируется: добавить поддержку нового сервиса — значит просто подключить ещё один MCP-сервер в конфигурации MultiServerMCPClient. Добавить новые документы — бросить PDF в папку data/.
Будущее за модульными AI-системами, и MCP — один из ключевых кирпичиков в этом фундаменте. Пора внедрять.
valentinvvv
Мисье, вы все изложили в заключении, где основное "Молодость протокола". В прод такое пихать страшно. Завтра наказание обновление langchain, он напишнь deprecated in next release, а послезавтра будет перелопачивать всё взаимодействие. А без обнов страшно. AI среда слишком уязвима.
Так же, сразу видно, что вы используете ai для написания статьи и кода. Текст похож на chatgpt, но по коду вроде бы claude sonnet 3.6.
Python 3.11, как пример. Знак "+" - это конечно хорошо, но 3.11 не надо использовать. Он уже не поддерживается и имеет не пофикшееные уязвимости. Так что по этому нужно использовать хотя бы 3.12-slim.
kardanShurup Автор
Про «молодость протокола» и прод. Я специально вынес это в минусы - чтобы сразу подсветить риски. Статья не инструкция «внедрять завтра в продакшен», а туториал для знакомства с подходом. Идея MCP (стандартный интерфейс агента с инструментами) выглядит перспективной, но самому протоколу ещё взрослеть и взрослеть.
Про LangChain.
В статье намеренно использовал
langchain-mcp-adapters(официальную библиотеку) и LangGraph вместо старогоAgentExecutor. Сейчас это «рекомендованный путь», и ломают его не так часто, как ранние версии LangChain. Ну и фиксация версий вrequirements.txt.Есть такое. Статья - результат совместной работы: структуру и логику выстраивал я, код тестировал руками, а черновики текста помогал готовить AI. Считаю такой подход рабочим: LLM берёт на себя рутину с формулировками и docstring'ами, а живой разработчик отвечает за то, чтобы всё было корректно и не галлюцинировало. В 2026-м я считаю это нормальным.
Python 3.11+.
Я написал «3.11+» по инерции, потому что многие консервативные проекты до сих пор сидят на 3.11 из-за старых зависимостей. Но для нового кода рекомендовать нужно минимум 3.12.
Если после прочтения ты такой «ну его в пень, пусть полежит годик» - значит, я свою задачу выполнил: предупредил о рисках. А когда протокол устаканится (а судя по тому, как Anthropic и другие его форсят, это вопрос времени), у тебя уже будет готовая ментальная модель.