Собираем AI-агента нового поколения: Python, RAG и внешние инструменты через MCP (Model Context Protocol). ai.. ai. chromadb.. ai. chromadb. FastMCP.. ai. chromadb. FastMCP. github.. ai. chromadb. FastMCP. github. langchain.. ai. chromadb. FastMCP. github. langchain. langgraph.. ai. chromadb. FastMCP. github. langchain. langgraph. llm.. ai. chromadb. FastMCP. github. langchain. langgraph. llm. machine learning.. ai. chromadb. FastMCP. github. langchain. langgraph. llm. machine learning. mcp.. ai. chromadb. FastMCP. github. langchain. langgraph. llm. machine learning. mcp. python.. ai. chromadb. FastMCP. github. langchain. langgraph. llm. machine learning. mcp. python. rag.. ai. chromadb. FastMCP. github. langchain. langgraph. llm. machine learning. mcp. python. rag. искусственный интеллект.. ai. chromadb. FastMCP. github. langchain. langgraph. llm. machine learning. mcp. python. rag. искусственный интеллект. Машинное обучение.. ai. chromadb. FastMCP. github. langchain. langgraph. llm. machine learning. mcp. python. rag. искусственный интеллект. Машинное обучение. Программирование.

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

Ещё пару лет назад типичное 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-приложениями и внешними источниками данных и инструментами. Архитектура включает три ключевых компонента:
1. MCP Host — AI-приложение (например, Claude Desktop или наш Python-агент).
2. MCP Client — компонент внутри хоста, который поддерживает соединение с одним MCP-сервером.
3. 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']}°Cn"
            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=["nn", "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°Cn- Ветер: 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 — один из ключевых кирпичиков в этом фундаменте. Пора внедрять.

Автор: kardanShurup

Источник