- BrainTools - https://www.braintools.ru -
Всем привет! Меня зовут Николай Луняка. В прошлой статье [1] мы строили локальную систему для транскрибации аудио, и многие из вас откликнулись на тему цифровой независимости. Сегодня продолжим эту линию и соберем агентную AI систему, которая работает локально.
Обычный чат с LLM ограничивается текстом, но как только нужны файлы, БД или пайплайны, то уже требуются инструменты и обвязки. Например, в ChatGPT можно прикладывать файлы прямо в диалог, и это удобно для разовой задачи. Но когда файлов много, все равно приходится загружать их в сессию, плюс есть ограничения по размеру и привязка к конкретному чату.
Одним из решений такого сценария являются AI-агенты — системы, которые умеют вызывать инструменты, работать с файловой системой и выполнять сложные многошаговые сценарии.
Представьте, что вам не нужно вручную копировать данные из таблиц в чат или пересказывать боту содержание своих документов. Вместо этого вы просто пишете: «Проверь отчеты в папке и выпиши главное». И агент сам открывает нужные файлы, анализирует их и выдает результат.
Почему мы не делали так раньше? Все упиралось в три простые проблемы:
Порог входа. Чтобы дать нейронке доступ к вашим файлам, приходилось писать сложный код. Теперь это можно просто «собрать» из готовых блоков.
Страшно за свои данные. Мало кто захочет давать доступ к своим личным документам или почте облачным сервисам. Мы же все будем запускать локально, на своем компьютере.
Программы плохо понимали друг друга. Раньше подружить разные инструменты было головной болью [2]. Но появился новый стандарт, как протокол MCP, который работает как универсальный переходник для любых AI-инструментов.
В этой статье я покажу, как собрать локальную агентную систему из трех основных компонентов:
1. LibreChat — open-source UI для работы с LLM (провайдеры моделей + подключение MCP-серверов через librechat.yaml).
2. Langflow — low-code платформа и визуальный редактор, flow как последовательность компонентов/шагов.
3. MCP серверы — сервер, который публикует tools/ресурсы по стандартному транспорту (HTTP/SSE/stdio).
Статья построена по принципу «по нарастающей», где каждый новый уровень — это рабочий инструмент. Можно остановиться на любом этапе, а можно пройти все, и тогда получите связку UI + инструменты + централизованная логика [3] (и дальше её можно развивать под свои сценарии).
|
Уровень |
Что получаем |
Сложность |
|
1 |
LibreChat + локальная LLM (Ollama) |
Простой чат с историей |
|
2 |
+ MCP filesystem |
Агент может читать ваши файлы |
|
3 |
+ Langflow |
Последовательное воркфлоу с валидациями |
|
4 |
Langflow как MCP сервер |
Централизация логики в Langflow |
|
5 |
Langflow со своими правилами как модель для LibreChat |
Полная кастомизация через FastAPI прокси и контроль над поведением [4] агентов |
Важно: все примеры рассчитаны на локальный запуск в изолированной docker сети. Для публичного деплоя потребуется дополнительная настройка безопасности (TLS, reverse proxy и т.д.)
Также выражаю благодарность коллегам — Михаилу Моисееву (@msfs11 [5] и Михаилу Войтко (@mvoytko [6]) за ревью статьи. Ваши комментарии помогли сделать материал лучше.
LibreChat это open-source веб UI чат для работы с различными LLM в одном интерфейсе. Он умеет подключаться к OpenAI-совместимым API и поддерживает работу с агентами и протокол MCP (о котором мы поговорим в следующем уровне).
Архитектура:
На этом уровне мы запустим три контейнера:
MongoDB — для хранения истории диалогов и настроек пользователей.
Ollama — для запуска локальных LLM.
LibreChat — веб UI чата.
Все контейнеры будут работать в изолированной Docker сети и общаться по внутренним именам (service names).
Создаем структуру проекта:
ai-agent/
├── docker-compose.yml
├── librechat.yaml
├── .env
└── documents/
Создайте файл docker-compose.yml со следующим содержимым:
services:
mongo:
image: mongo:7.0.5
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
volumes:
- ./mongo_data:/data/db
networks:
- ai-network
healthcheck:
test: ["CMD-SHELL", "mongosh "mongodb://$${MONGO_INITDB_ROOT_USERNAME}:$${MONGO_INITDB_ROOT_PASSWORD}@localhost:27017/admin" --quiet --eval "db.adminCommand('ping').ok" | grep 1 >/dev/null"]
interval: 10s
timeout: 5s
retries: 5
ollama:
image: ollama/ollama:latest
restart: unless-stopped
ports:
- "11434:11434"
volumes:
- ./ollama_data:/root/.ollama
networks:
- ai-network
# Если у вас нет NVIDIA GPU удалите секцию deploy
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
app:
image: ghcr.io/danny-avila/librechat:latest
restart: unless-stopped
ports:
- "3080:3080"
depends_on:
mongo:
condition: service_healthy
ollama:
condition: service_started
environment:
MONGO_URI: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/?authSource=admin
JWT_SECRET: ${JWT_SECRET}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
ALLOW_REGISTRATION: "true"
volumes:
- ./librechat.yaml:/app/librechat.yaml
networks:
- ai-network
networks:
ai-network:
driver: bridge
version: 1.3.3
cache: true
endpoints:
custom:
- name: "Ollama (Local)"
apiKey: "ollama"
baseURL: "http://ollama:11434/v1/"
models:
default:
- "llama3.1"
fetch: true
titleConvo: true
summarize: false
Важно: строка модели в default должна точно совпадать с тем, как она называется (см. шаг 6 (ollama list)). Если модель хранится как llama3.1:8b или llama3.1:latest, то именно так и надо указывать.
Создайте .env с переменными окружения:
MONGO_USER=admin
MONGO_PASSWORD=CHANGE_ME_STRONG_PASSWORD_HERE
JWT_SECRET=CHANGE_ME_RANDOM_STRING_1
JWT_REFRESH_SECRET=CHANGE_ME_RANDOM_STRING_2
Теперь запускаем все сервисы:
# Запускаем в фоновом режиме
docker compose up -d
# Проверяем статус контейнеров
docker compose ps
Если что-то пошло не так:
# Смотрим логи всех сервисов
docker compose logs
# Или логи конкретного сервиса
docker compose logs app
docker compose logs mongo
Если после старта app не поднимается, первым делом смотрите логи mongo, чаще всего проблема в том, что Mongo еще не прошел healthcheck
Ollama запущен, но в нем еще нет моделей. Можно выбрать другую модель из библиотеки Ollama [8] или подключить другой OpenAI совместимый endpoint.
Скачаем Llama 3.1:
docker compose exec ollama ollama list
Как выбрать модель (в двух словах)?
|
Задача |
Рекомендация |
Пример |
|
Быстрый чат на слабом железе |
Легкая модель (3B-8B, квантованная) |
llama3.2:3b-q4_K_M |
|
Генерация кода |
Модели coder или instruct |
deepseek-coder:6.7b |
|
Работа с инструментами (MCP) |
Модели с поддержкой tool calling |
llama3.1:8b |
|
Сложные флоу (многошаговые) |
Модели с reasoning |
deepseek-r1:14b |
|
Максимальное качество |
Большие модели (70B+) |
llama3.1:70b |
Проверяем, что модель скачалась:
docker compose exec ollama ollama list
Вы должны увидеть:

Переходим в браузере http://localhost:3080/ [9].
При первом запуске вы увидите экран регистрации. Создайте аккаунт (данные хранятся локально в MongoDB).
Настройка чата:
В верхнем меню выберите endpoint: Ollama.
В выпадающем списке моделей выберите llama3.1.
(Опционально) Нажмите на иконку настроек и настройте параметры:
– Temperature — креативность ответов,
– Max Tokens — максимальная длина ответа,
– System Prompt — промт для модели.
Теперь можно общаться с локальной LLM через удобный интерфейс

LibreChat предоставляет расширенный инструментарий для работы с LLM:
Пресеты (Presets).
Мультимодальность.
Переключение между провайдерами.
Экспорт диалогов.


На этом уровне мы развернули базовую инфраструктуру для работы с локальными LLM. Этого достаточно, если вам нужен просто удобный локальный чат с сохранением истории. Но пока это еще без файлов и инструментов.
В следующем уровне мы добавим MCP filesystem сервер, и наша модель научится работать с файлами на диске.
MCP (Model Context Protocol [10]) — это новый открытый протокол, который описывает как разным LLM подключаться к инструментам (вашими файлами, базами данных или API) через единый стандарт.
Вам больше не нужно настраивать отдельную интеграцию для каждой задачи достаточно одного MCP-сервера. Хотите подключить файлы, почту или календарь? Просто возьмите готовый сервер из каталога (например, есть reference серверы в GitHub: modelcontextprotocol [11]а для поиска сторонних, есть реестры вроде Smithery.ai [12]). При этом вы сохраняете полный контроль, так как агент работает в изолированной среде и получает доступ только к тем папкам, которые вы ему открыли (в конфиге), не обращаясь к остальной системе.
Для работы с MCP нужен хост / клиент / сервер.
MCP Host — приложение, с которым общается пользователь, он принимает запрос, знает какие MCP сервера доступны, и в каких случаях их вызывать.
MCP Client — часть хоста, которая обеспечивает связь между хостом и сервером – по одному соединению на каждый сервер.
MCP Server — это отдельный сервис прослойка, который умеет работать с внешней системой на ее языке (API, файлы и т.д.), и наружу отдает единый набор инструментов доступный для хоста.
Для связи с MCP сервером существует два основных вида транспорта:
stdio [13]— локальный транспорт, где хост запускает MCP сервер как процесс и общается с ним через stdin / stdout.
Streamable HTTP [14]— удаленный транспорт. MCP сервер работает как отдельный сервис и отдает один HTTP endpoint (обычно /mcp) для GET / POST. При необходимости внутри этого транспорта можно стримить события через SSE.
Дальше я покажу всё на примере filesystem MCP server. Это готовый сервер, который дает набор инструментов для работы с файлами в папке (показать список, прочитать файл и т.д.). По умолчанию это stdio сервер и HTTP порта не поднимает.
Чтобы подключить такой stdio сервер по сети, нужен мост, который конвертирует stdio ↔ сетевой MCP транспорт (в статье показываю на примере Streamable HTTP). Для этого я использую supergateway [15]. Он запускает filesystem сервер как дочерний stdio-процесс и публикует его наружу как Streamable HTTP сервер на /mcp.
Добавьте новый сервис mcp-filesystem в ваш docker-compose.yml:
Важно: всегда монтируйте только рабочую папку. По умолчанию используйте :ro. Разрешайте запись (:rw) только если это действительно необходимо и вы понимаете риски, так как агент сможет удалять и изменять файлы.
services:
# ... (mongo, ollama, app - оставляем без изменений)
mcp-filesystem:
image: node:20-slim
restart: unless-stopped
volumes:
- ./documents:/data:ro # замените на :rw, если хотите разрешить запись
networks:
- ai-network
command:
- sh
- -lc
- |
set -e
npx --yes supergateway@3.4.3
--port 8000
--outputTransport streamableHttp
--stateful
--sessionTimeout 600000
--streamableHttpPath /mcp
--healthEndpoint /healthz
--stdio "npx --yes @modelcontextprotocol/server-filesystem@2026.1.14 /data"
healthcheck:
test:
[
"CMD-SHELL",
"node -e "require('http').get('http://localhost:8000/healthz', r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))""
]
interval: 10s
timeout: 5s
retries: 3
Добавьте конфигурацию MCP в librechat.yaml:
mcpSettings:
allowedDomains:
- "mcp-filesystem"
mcpServers:
filesystem:
type: "streamable-http"
url: "http://mcp-filesystem:8000/mcp"
Убедитесь, что в папке documents есть файлы для тестирования. Для примера можно создать два файла в формате .txt и наполнить их данными.
# Перезапускаем только измененные сервисы
docker compose up -d mcp-filesystem app
# Проверяем статус
docker compose ps
Проверяем, что MCP сервер запустился, смотрим логи:
docker compose logs mcp-filesystem
Откройте LibreChat: http://localhost:3080 [16]
Создайте новый чат или откройте существующий
Нажмите на иконку «Настройки MCP» в меню справа
Вы должны увидеть доступный MCP сервер: filesystem
Включите его, нажав на переключатель справа
Теперь модель может вызывать инструменты для работы с файлами.
Пример некоторых команд:

LibreChat подключается к MCP-серверу по streamable-http, получает список доступных tools, а дальше уже модель решает вызывать инструмент или отвечать текстом. Сам MCP-сервер при вызове инструмента читает файлы только из того, что вы смонтировали (в нашем примере /data).
Схема упрощена для общего видения картины:


На этом уровне у модели появились tools (filesystem). За счет этого уже можно решать простые многошаговые задачи (прочитать несколько файлов, сравнить, составить отчет). Плюс такого подхода в том, что дальше можно подключать другие MCP серверы (база данных, API, почта) и делается это примерно одинаково для любого совместимого сервера.
Но если нужен предсказуемый результат (проверки, ветвления, обработка ошибок, фиксированный формат ответа) на одном LibreChat+MCP — это неудобно. На этом уровне модель сама решает, когда вызывать инструмент, и это зависит и от промта, и от выбранной модели. А если вы работаете с чувствительными данными, то есть риск, что модель может галлюцинировать.
В следующем уровне мы добавим Langflow — для создания последовательных флоу с валидациями и кастомной логикой.
Langflow [17] — это open-source платформа, где флоу собирается через визуальный интерфейс. В отличие от обычного чата с LLM, здесь можно зафиксировать порядок шагов. Каждый флоу можно запускать не только через UI, но и через REST API. Чтобы избежать галлюцинации при работе с данными существует возможность использовать свои Python-компоненты (например, для расчетов). Плюс в Langflow есть нативная поддержка MCP Tools [18].
Далее я покажу простой практический кейс с двумя xlsx файлами, которые содержат расходы за разные месяцы. В этом флоу будет четкая последовательность действий: найти нужные .xlsx в папке с помощью mcp-filesystem, прочитать их и посчитать суммы по категориям и создать таблицу — кодом в кастомном компоненте, а уже потом оформить итог в виде коротких выводов при помощи LLM.
Добавляем сервис langflow (остальные сервисы остаются без изменений):
langflow:
image: langflowai/langflow:latest
restart: unless-stopped
ports:
- "7860:7860"
environment:
LANGFLOW_AUTO_LOGIN: "True"
LANGFLOW_CONFIG_DIR: /app/langflow
LANGFLOW_DATABASE_URL: sqlite:////app/langflow/langflow.db
LANGFLOW_COMPONENTS_PATH: /app/custom_components
volumes:
- ./langflow_data:/app/langflow
- ./custom_components:/app/custom_components
- ./documents:/data:ro
networks:
- ai-network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:7860/health_check').status==200 else 1)"]
interval: 10s
timeout: 5s
retries: 5
Важно: в данном коде, в рамках локальной сборки, я использую автоматический вход без пароля LANGFLOW_AUTO_LOGIN: «True».
# Запускаем сервис
docker compose up -d
# Проверяем статус
docker compose ps langflow
# Смотрим логи
docker compose logs -f langflow
Откройте браузер: http://localhost:7860 [19].
Если LANGFLOW_AUTO_LOGIN: «True», вы сразу попадете в интерфейс. Иначе создайте аккаунт.
В Langflow создаем новый Flow, нажав «New Flow» и добавляем компоненты:
Chat Input.
Agent.
New Custom components (в самом низу).
MCP Tools.
Chat Output.


Нажимаем на «Code» и добавляем свой код компонента.

У меня кастомный компонент будет выполнять следующие функции:
Принимает путь к нужному файлу формата xlsx.
Проверяет наличие нужных колонок (Категория и Расход).
Приводит данные из колонки «Расход» к числовому формату.
Группирует строки по «Категория» и считает суммы по «Расход».
Считает общий итог и количество строк.
Формирует результат в виде таблицы.
После добавления кода — сохраняем и переводим его в режим «tool mode».

Важно: чтобы кастомные компоненты подгружались автоматически и сохранялись при перезапуске контейнеров, их необходимо сохранять в папке custom_components. Также надо создать файл __init__.py, чтобы директория превратилась в Python-пакет и Langflow смог корректно индексировать инструменты.
Добавьте MCP Server:
Type: Streamable HTTP/SSE.
Name: любое значение.
URL: http://mcp-filesystem:8000/mcp [20].

Теперь сам компонент MCP Tools можно перевести в режим tool mode и отредактировать инструменты в настройках, если есть в этом необходимость.
Внутри docker сети общаемся по service name ollama, не по localhost [21].
Ollama API URL: http://ollama:11434 [22].
Model Name: выбрать из выпадающего списка llama3.1 (или то, что вы скачали).
Temperature: 0.3.
Agent Instructions: пишем промт, который четко фиксирует последовательность действий и вызовов tools.
В настройках самого компонента есть ещё много полезных функций.

Chat Input → Agent (Input).
MCP Tools → Agent (Tools).
Custom component (Tools) → Agent (Tools).
Agent → Chat Output.

Нажимаем на Playground и просим сделать отчет:


|
Критерий |
LibreChat + MCP (Уровень 2) |
Langflow + MCP (Уровень 3) |
|
Сложность настройки |
Низкая (добавить MCP сервер в конфиг) |
Средняя (нужно собрать Flow) |
|
Детерминизм |
Зависит от модели |
Фиксированная логика |
|
Валидация данных |
Только через промт |
Отдельные компоненты |
|
Кастомная логика |
Нет |
Python компоненты |
|
Формат вывода |
Произвольный |
Контролируемый |
|
Отладка |
Сложно (черный ящик) |
Легко (видно каждый шаг) |
|
UI для пользователя |
Готовый чат |
Нужна интеграция (или Playground) |
|
Подходит для |
Простые задачи, чат |
Сложные воркфлоу, автоматизация |

На этом уровне мы добавили Langflow и получили не просто чат с инструментами, а фиксированный сценарий выполнения. MCP по-прежнему отвечает за доступ к файлам, а расчеты и проверки уносим в Python-компоненты, поэтому итог получается стабильнее. В работе можно применять в различных кейсах где нужна точность и большой объем данных (например, в работе с ролевой моделью). Плюсом, что в данном процессе шаги не меняются от запуска к запуску и формат вывода проще держать под контролем, а ошибки [23] можно отлавливать на этапе обработки данных.
С другой стороны, здесь увеличивается порог входа, да и флоу нужно собрать и поддерживать. Готового пользовательского UI тут нет (Playground скорее для тестов). Также весь созданный процесс можно встроит в свой сайт через REST API, но если нужен функционал как у чата (хранение истории, авторизация, пресеты и т.п.), то все это придется реализовывать дополнительно.
В следующем уровне подключим Langflow к LibreChat, чтобы совместить последовательную логику и готовый чат-интерфейс.
На Уровне 4 настроим Langflow в режиме MCP-сервера, чтобы добавить его как инструмент LibreChat (как это делали на уровне 2). Таким образом, мы сможем получить привычное окно чата от LibreChat и гарантированную точность выполнения шагов от Langflow.
Открываем проект в Langflow — http://localhost:7860 [19].
В верхнем меню нажмите на вкладку MCP Server.

3. Убеждаемся, что нужный flow добавлен в Flows/Tools (например, NEW_FLOW).

4. В блоке справа выбираем транспорт:
В Langflow в JSON показывает пример с localhost [21], типа:
Streamable: http://localhost:7860/api/v1/mcp/project/<id>/streamable [24].
SSE: http://localhost:7860/api/v1/mcp/project/<id>/sse [25].
ВАЖНО: если LibreChat у в Docker, то для него localhost [21] это сам контейнер LibreChat.
Поэтому делаем замену:
было: http://localhost:7860/ [26]…
стало: http://langflow:7860/ [27]…
…(где langflow — имя сервиса из docker-compose).
Если Langflow доступен не только вам (или вы просто хотите нормальную защиту API), включите доступ по API ключу:
Langflow → Settings → API Keys → Create.
Langflow сгенерирует ключ вида: sk-langflow-abc123def456…
Скопируйте ключ – он больше не будет показан!
Вставьте его в .env как LANGFLOW_API_KEY.

Сохраняем ключ auth в .env
Если вы оставили Auth: None (public), этот шаг можно пропустить.
Добавьте в файл .env:
LANGFLOW_API_KEY=sk-...
services:
. . .
app:
image: ghcr.io/danny-avila/librechat:latest
restart: unless-stopped
ports:
- "3080:3080"
depends_on:
mongo:
condition: service_healthy
ollama:
condition: service_started
langflow:
condition: service_healthy
environment:
MONGO_URI: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/?authSource=admin
JWT_SECRET: ${JWT_SECRET}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
ALLOW_REGISTRATION: "true"
LANGFLOW_API_KEY: ${LANGFLOW_API_KEY}
volumes:
- ./librechat.yaml:/app/librechat.yaml
networks:
- ai-network
# ... (остальные сервисы без изменений)
version: 1.3.3
cache: true
endpoints:
custom:
- name: "Ollama (Local)"
apiKey: "ollama"
baseURL: "http://ollama:11434/v1/"
models:
default:
- "llama3.1"
fetch: true
titleConvo: true
summarize: false
mcpSettings:
allowedDomains:
- "mcp-filesystem"
- "langflow"
mcpServers:
filesystem:
type: "streamable-http"
url: "http://mcp-filesystem:8000/mcp"
langflow:
type: "streamable-http"
# замените 2a2a5035-2b67-4d20-8713-4d97527dd6c6 на ваш project ID из Langflow
url: "http://langflow:7860/api/v1/mcp/project/2a2a5035-2b67-4d20-8713-4d97527dd6c6/streamable"
headers:
# если включили Auth в Langflow, иначе удалите эту строку
x-api-key: "${LANGFLOW_API_KEY}"
# Перезапускаем LibreChat для применения изменений
docker compose up -d app
# Проверяем логи
docker compose logs -f app
Открываем LibreChat: http://localhost:3080 [16]:
Настройки → MCP Servers → Add MCP server.
Заполняем поля:
Имя: любое значение
URL адрес сервера MCP: вставляем наше скопированное значение
Transport:
Если URL заканчивается на /sse → выбираем SSE.
Если URL заканчивается на /streamable → выбираем Streamable HTTP.
Auth:
если в Langflow стоит Auth: None (public) – выбираем No Auth,
если нажали Add Auth в Langflow – тогда уже API key/OAuth (по ситуаци) из .env.
3. Проставляем галочку в чекбоксе, что доверяете приложению.
Если всё настроено правильно, то сервер появится в списке доступных MCP серверов.

Теперь в чате LibreChat:
Выбираем модель (например, локальную llama 3.1).
Выбираем MCP сервер (тот, что добавили).
В настройках чата включите ранее добавленный MCP сервер (Langflow).

Если просто подключить флоу из предыдущего уровня «как есть», то появится проблема, как LLM над LLM. LibreChat отправит запрос своей модели, та вызовет инструмент Langflow, внутри которого другая модель начнет генерировать свой ответ. В итоге мы получаем избыточный расход токенов, лишние задержки и риск, что финальный ответ отобразится некорректно из-за двойной интерпретации.
Схематично это выглядит вот так:


Чтобы система работала корректно, нужно изменить подход к проектированию флоу в Langflow:
Убираем компонент Agent из Langflow.
Оставляем в Langflow только логику: сбор данных через MCP, Python-скрипты для расчетов и форматирование.
Роль оркестратора полностью отдаем модели в LibreChat.
В такой конфигурации Langflow превращается в чистый MCP-инструмент. Он возвращает сухие факты или готовую таблицу, а модель в LibreChat уже сама решает, как преподнести эти данные пользователю.


На этом уровне Langflow выступает как инструмент, а оркестратор (LibreChat или любая другая среда) вызывает его через MCP как одну из функций.
Главный минус такой схемы это архитектурная избыточность. Если LibreChat вызывает Langflow-tool, а внутри Langflow еще крутится LLM, получается «LLM над LLM».
Ну и вдобавок два сервиса сложнее администрировать и дебажить. Для простых задач это слишком трудозатратно, но на сложных флоу схема работает хорошо.
Несмотря на то, что интеграция через MCP на предыдущем этапе полностью функциональна, она имеет свои ограничения в плане гибкости управления. На уровне 5 мы внедрим прослойку на FastAPI, которая даст возможность обернуть весь процесс Langflow в OpenAI совместимую модель. В интерфейсе LibreChat это будет выглядеть как выбор новой кастомной LLM.
На уровне 5 оркестрация переезжает в Langflow через прокси, а LibreChat становится в основном UI. Для этого нужен совместимый запрос/ответ, который ожидает LibreChat. Langflow при этом запускает процесс своим API и возвращает свой формат ответа, а его нужно адаптировать: взять сообщение, положить текст в нужный input flow, затем извлечь результат и отдать его как ответ модели.
Готовые решения обычно являются шлюзом к LLM-провайдерам, а не запускают конкретный flow с маппингом input/output. Получается, что нужен адаптер, и, как по мне, так проще и прозрачнее сделать его отдельным небольшим FastAPI-сервисом.
По итогу прокси просто транслирует запросы, а вся логика происходит внутри Flow. Это даёт нам полный контроль над поведением [28] агента, можно классифицировать интент пользователя и подбирать под него оптимальную модель (например, дешёвую для простых вопросов и мощную для кодинга, а при конкретных установочных фразах — запускать флоу с инструментами).
Здесь же решается вопрос единого стиля ответов через централизованные системные промпты и нормализацию входных данных — обрезку истории или фильтрацию контекста. По сути, мы создаём единую точку входа, где Langflow выступает не просто цепочкой шагов, а полноценным оркестратором политик.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY fastapi_proxy.py .
CMD ["uvicorn", "fastapi_proxy:app", "--host", "0.0.0.0", "--port", "8001"]
# FastAPI и Uvicorn (веб-фреймворк и сервер)
fastapi==0.115.0
uvicorn[standard]==0.30.0
# HTTP-клиент для асинхронных запросов
httpx==0.27.2
# Валидация данных
pydantic==2.8.2
Прокси делает следующие вещи:
берет последнее пользовательское сообщение из messages[],
вызывает Langflow POST /api/v1/run/<FLOW_ID>?stream=false с указанным tweaks и API-ключом,
ищет текстовый ответ в JSON от Langflow, игнорируя технические логи и входные параметры,
возвращает ответ либо JSON-ом, либо как SSE-стримом (имитируя чанки OpenAI, чтобы LibreChat не ругался).
from __future__ import annotations
import asyncio
import json
import os
import time
import logging
from typing import Any, List, Optional, AsyncIterator
import httpx
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import StreamingResponse, JSONResponse
from pydantic import BaseModel, ConfigDict
logger = logging.getLogger("langflow-proxy")
logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"))
class Message(BaseModel):
model_config = ConfigDict(extra="allow")
role: str
content: str
class ChatPayload(BaseModel):
model_config = ConfigDict(extra="allow")
messages: List[Message]
model: str
stream: Optional[bool] = False
app = FastAPI(title="Langflow Proxy")
LANGFLOW_BASE_URL = os.getenv("LANGFLOW_BASE_URL", "http://langflow:7860")
LANGFLOW_FLOW_ID = os.getenv("LANGFLOW_FLOW_ID")
LANGFLOW_INPUT_ID = os.getenv("LANGFLOW_INPUT_ID")
LANGFLOW_API_KEY = os.getenv("LANGFLOW_API_KEY")
def find_text_recursively(data: Any) -> str:
if isinstance(data, dict):
if data.get("text") and isinstance(data["text"], str) and data.get("sender") == "Machine":
return data["text"]
if isinstance(data.get("message"), str) and data["message"].strip():
return data["message"]
for key, value in data.items():
if key in ["logs", "inputs", "input_value"]:
continue
result = find_text_recursively(value)
if result:
return result
elif isinstance(data, list):
for item in data:
result = find_text_recursively(item)
if result:
return result
return ""
async def forward_to_langflow(user_input: str) -> str:
if not LANGFLOW_FLOW_ID or not LANGFLOW_INPUT_ID:
raise HTTPException(status_code=500, detail="Configuration error")
url = f"{LANGFLOW_BASE_URL}/api/v1/run/{LANGFLOW_FLOW_ID}?stream=false"
payload = {
"output_type": "chat",
"input_type": "chat",
"tweaks": {LANGFLOW_INPUT_ID: {"input_value": user_input}},
}
headers = {"x-api-key": LANGFLOW_API_KEY} if LANGFLOW_API_KEY else {}
timeout = httpx.Timeout(300.0, connect=15.0)
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.post(url, json=payload, headers=headers)
if resp.status_code >= 400:
logger.error(f"Langflow error {resp.status_code}: {resp.text[:800]}")
return f"Error {resp.status_code}"
try:
data = resp.json()
except Exception:
return "Invalid JSON response"
return find_text_recursively(data).strip() or "Empty response"
def sse(data: dict) -> str:
return f"data: {json.dumps(data, ensure_ascii=False)}nn"
async def stream_content(model: str, content: str) -> AsyncIterator[str]:
created = int(time.time())
chat_id = f"chatcmpl-{created}"
yield sse({
"id": chat_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
})
chunk_size = 200
for i in range(0, len(content), chunk_size):
part = content[i : i + chunk_size]
yield sse({
"id": chat_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{"index": 0, "delta": {"content": part}, "finish_reason": None}],
})
await asyncio.sleep(0)
yield sse({
"id": chat_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
})
yield "data: [DONE]nn"
@app.post("/v1/chat/completions")
async def chat_router(payload: ChatPayload, request: Request):
if not payload.messages:
raise HTTPException(status_code=400, detail="Empty messages")
user_input = payload.messages[-1].content
if payload.stream:
async def gen():
try:
content = await forward_to_langflow(user_input)
async for x in stream_content(payload.model, content):
if await request.is_disconnected():
return
yield x
except Exception as e:
logger.exception(e)
return StreamingResponse(
gen(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
)
content = await forward_to_langflow(user_input)
created = int(time.time())
return JSONResponse({
"id": f"chatcmpl-{created}",
"object": "chat.completion",
"created": created,
"model": payload.model,
"choices": [{"index": 0, "message": {"role": "assistant", "content": content}, "finish_reason": "stop"}],
}
)
ВАЖНО: В данном примере реализован упрощенная эмуляция стриминга. Для полноценного Real-time ответа необходимо переписать прокси на асинхронное чтение потока от Langflow.
Если поменяли компоненты или добавили промежуточные шаги, то возможно потребуется адаптировать извлечение результата.
Текущая схема: LibreChat → прокси → Langflow (ждем полный ответ) → имитируем стрим.

Реальный стриминг: LibreChat → прокси → Langflow (stream=true) → читаем SSE чанки → парсим формат Langflow → конвертируем в формат OpenAI SSE → отдаем LibreChat.

При помощи такого компонента, как if-else, можно сделать ветвление прописав правила зависящие от входного сообщения. Существуют и другие компоненты: позволяющие более гибко и тонко настраивать роутинг флоу.
Какие правила обычно выносят в Langflow:
Умный выбор модели (экономия бюджета), например, простые вопросы — дешёвая/быстрая модель, сложные уже более сильная, а для tool calling — отдельный профиль/модель.
Маршрутизация по типу запроса, где можно реализовать классификация интента в виде выбора ветки/flow (суммаризация, Q&A или отчет).
Здесь есть единый стиль общения и форматы ответа (системные промпты, тон, шаблоны/JSON‑схемы и валидации).
Так называемый чистый вход, где еще до вызова инструментов можно сделать обрезку истории, ограничения размера и нормализацию.

По итогу получается зафикисрованная логика — если запрос от пользователя совпадает со словами «сделай», «сформируй» и т.д то процесс идет по ветке True (ранее созданному флоу из уровня 4), иначе запрос пойдет по ветке False в простую LLM модель (можно выбрать и настроить, как нужно).
Проверяем еще раз сам флоу в langflow:

Чтобы LibreChat мог вызывать конкретный flow в Langflow через наш прокси, то он должен знать куда отправлять запрос (Flow ID) и в какой входной блок положить текст пользователя (ChatInput ID).
Найти их можно, если открыть API access (в Langflow у flow обычно рядом Share — в верхнем правом углу).
Внутри вы увидите готовый пример запроса.

Нам нужно два значения:
Flow ID находится прямо в URL вызова: …/api/v1/run/<FLOW_ID>.

ChatInput ID находится в tweaks как ключ: “ChatInput-XXXXX”: { “input_value”: “…” }.
ВАЖНО: ChatInput ID может не отображаться, пока вы не включите/не настроите Input Schema для flow.
В правом верхнем углу нажмите Input Schema.
Включите схему и выберите:
– Input: ваш блок Chat Input,
– Output: ваш блок Chat Output (или нужный output-компонент).
Сохраните изменения (Apply/Save).
Снова откройте API access после этого в примере запроса появится секция tweaks, где и будет нужный ID.

Если Langflow доступен не только вам (или вы просто хотите нормальную защиту API), включите доступ по ключу:
Langflow → Settings → API Keys → Create.
Скопируйте ключ.
Вставьте его в .env как LANGFLOW_API_KEY.

Прокси автоматически начнет отправлять его в заголовке: x-api-key: <ваш ключ>.
После правок .env перезапустите только прокси:
docker compose up -d --no-deps --build fastapi-proxy
services:
...
fastapi-proxy:
build:
context: .
dockerfile: Dockerfile.fastapi
restart: unless-stopped
ports:
- "8001:8001"
environment:
LANGFLOW_BASE_URL: http://langflow:7860
LANGFLOW_FLOW_ID: <ВАШ_FLOW_ID_ИЗ_LANGFLOW>
LANGFLOW_INPUT_ID: <ВАШ_CHATINPUT_ID_ИЗ_LANGFLOW>
LANGFLOW_API_KEY: ${LANGFLOW_API_KEY}
networks:
- ai-network
app:
...
depends_on:
fastapi-proxy:
condition: service_started
environment:
LANGFLOW_API_KEY: ${LANGFLOW_API_KEY}
...
Добавьте новый endpoint для FastAPI прокси:
...
endpoints:
custom:
...
# Ваше название модели
- name: "Langflow model"
apiKey: "fastapi-proxy"
baseURL: "http://fastapi-proxy:8001/v1"
models:
default:
# Имя модели, которое будет отображаться в LibreChat
- "Langflow model"
fetch: false
titleConvo: true
summarize: false
...
# Перезапускаем все сервисы для применения изменений
docker compose up -d --build
# Проверяем логи
docker compose logs -f app fastapi-proxy
В LibreChat выберите endpoint Langflow model (тот, что смотрит на fastapi-proxy).

Пишем в чат наш запрос и через пару мгновений получаем нужный нам ответ.

Если ответ пришел — значит всё работает — поздравляю)
Чтобы окончательно определиться с выбором архитектуры, давайте сопоставим два наиболее продвинутых метода интеграции.
|
Критерий |
Уровень 4 (MCP сервер) |
Уровень 5 (FastAPI прокси) |
|
UX |
Нужно выбрать модель + MCP сервер |
Только выбор модели |
|
Простота для пользователя |
Средняя (2 шага) |
Высокая (1 шаг) |
|
Стандартизация |
✅ MCP — открытый стандарт |
❌ Кастомное решение |
|
Контроль над форматом |
❌ Ограничен MCP протоколом |
✅ Полный контроль |
|
Поддержка |
Нет дополнительного кода |
Нужно поддерживать прокси |
|
Кастомизация |
❌ Сложно |
✅ Легко (middleware, логи, rate limiting) |
|
Подходит для |
Технические пользователи |
Конечные пользователи |

На этом уровне Langflow подключается в LibreChat как обычная модель через OpenAI-совместимый FastAPI-прокси. Для пользователя это один выбор в списке моделей, а дальше запрос уходит в прокси и запускает нужную логику во flow.
Прокси даёт точку контроля: можно сделать роутинг по интенту, переключать модели под задачу, добавить политики безопасности (rate limiting, фильтрация, валидация входа) и нормализовать формат ответа. При этом вся логика находится в Langflow. Новые ветки, источники и проверки добавляются без изменений конфигурации LibreChat.
Минус тоже есть, например, появляется дополнительный сервис, который нужно поддерживать и донастраивать OpenAI-совместимый формат, корректный SSE-стриминг, таймауты/отмену запросов, логирование и безопасность.
|
Вам нужно |
Берите |
Почему |
|
Просто чат + иногда открыть файл/папку и задать вопрос |
LibreChat + MCP |
минимум сервисов, быстрый старт |
|
Процесс/пайплайн: шаги, проверки, ветвления, повторяемость |
Langflow + MCP |
Логика зафиксирована во flow, меньше |
|
UX LibreChat + сложная логика, которую хочется держать централизованно |
LibreChat + Langflow + MCP |
UI + оркестрация + инструменты |
|
Подключить Langflow как обычную модель в LibreChat и иметь место для правил/роутинга |
FastAPI прокси |
Нативное подключение LibreChat + контроль входа/выхода + политики |
Главная идея: инструмент = MCP‑сервер. Хотите добавить новый источник данных — добавляете/пишете MCP‑сервер и подключаете его либо в LibreChat (если LibreChat оркестратор), либо в Langflow (если Langflow оркестратор).
На практике один и тот же стек можно использовать в трех режимах, здесь отличается не «логика», а подготовка окружения и безопасность.
№1. Полностью локально. Подходит для личных документов и задач, где важна приватность: отчеты по Excel, поиск по локальной базе заметок/документов, сравнение файлов и версий, разбор логов/дампов, генерация сводок.
№2. Закрытая/безопасная сеть (корпоративный контур, без интернета). Можно использовать с Jira/Confluence (если доступны внутри), отчеты по ролевым моделям/матрицам доступов, анализ инцидентов, обработка выгрузок, унификация ответов/шаблонов.
№3. Онлайн/гибрид (часть источников снаружи). Подходит для AI ассистентов: почта/календарь, внешние API, задачи/трекеры, умные уведомления.
Но важно заранее донастроить безопасность: TLS/reverse proxy, аутентификацию, ограничение прав инструментов и хранение секретов.
В статье мы прошли путь от простого чат-бота к системе, где роли разделены. LibreChat отвечает за интерфейс и работу с диалогом, MCP дает единый способ подключать инструменты, а Langflow нужен там, где важны шаги, ветвления, проверки и повторяемость результата.
Файловая система, это лишь минимальный пример, чтобы показать принцип. Дальше по той же схеме подключаются HTTP-сервисы, базы данных и внутренние API. Для этого добавляется нужный MCP-сервер и встраивается в цепочку там, где у вас находится оркестрация либо в LibreChat, либо в Langflow. А на последних уровнях появляется стандартный API-вход, поэтому собранную логику можно использовать не только через чат, но и как сервис для веб/мобайла или внутренних автоматизаций.
Если вы уже собирали похожие связки или решали иначе, поделитесь в комментариях: интересно сравнить разные подходы и кейсы.
Подписывайтесь на Телеграм-канал Alfa Digital [29] — там мы постим новости, опросы, видео с митапов, краткие выжимки из статей, иногда шутим.
Автор: lynikol
Источник [30]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/25850
URLs in this post:
[1] прошлой статье: https://habr.com/ru/companies/alfa/articles/909498/
[2] болью: http://www.braintools.ru/article/9901
[3] логика: http://www.braintools.ru/article/7640
[4] поведением: http://www.braintools.ru/article/9372
[5] @msfs11: https://www.braintools.ru/users/msfs11
[6] @mvoytko: https://www.braintools.ru/users/mvoytko
[7] Image: https://sourcecraft.dev/
[8] библиотеки Ollama: https://ollama.com/library
[9] http://localhost:3080/: http://localhost:3080/
[10] Model Context Protocol: https://modelcontextprotocol.io/
[11] GitHub: modelcontextprotocol : https://github.com/modelcontextprotocol
[12] Smithery.ai: http://Smithery.ai
[13] stdio : https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#stdio
[14] Streamable HTTP : https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http
[15] supergateway: https://github.com/supercorp-ai/supergateway
[16] http://localhost:3080: http://localhost:3080
[17] Langflow: https://www.langflow.org/
[18] ативная поддержка MCP Tools: https://docs.langflow.org/mcp-tools
[19] http://localhost:7860: http://localhost:7860
[20] http://mcp-filesystem:8000/mcp: http://mcp-filesystem:8000/mcp
[21] localhost: http://localhost
[22] http://ollama:11434: http://ollama:11434
[23] ошибки: http://www.braintools.ru/article/4192
[24] http://localhost:7860/api/v1/mcp/project/<id>/streamable: http://localhost:7860/api/v1/mcp/project/<id>/streamable
[25] http://localhost:7860/api/v1/mcp/project/<id>/sse: http://localhost:7860/api/v1/mcp/project/<id>/sse
[26] http://localhost:7860/: http://localhost:7860/
[27] http://langflow:7860/: http://langflow:7860/
[28] поведением: http://www.braintools.ru/article/5593
[29] Alfa Digital: https://t.me/alfadigital_jobs
[30] Источник: https://habr.com/ru/companies/alfa/articles/1000342/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1000342
Нажмите здесь для печати.