Свой AI-агент с блекджеком: собираем из того, что есть

Принцип гласит: создавайте небольшие и простые программы, которые решают конкретную задачу, а сложные системы стройте, комбинируя эти программы вместе, посредством стандартных интерфейсов.
Особенно, я бы выделил следующие правила, которыми вдохновлялся при проектировании: — Правило модульности: пишите простые части, соединяемые понятными интерфейсами. — Правило композиции: разрабатывайте программы так, чтобы их можно было соединить с другими программами.
Отдельное правило для критиков: — Правило многообразия: отвергайте все утверждения о «единственно правильном пути».
И еще одно правило, которое было сформулировано еще в начале века, но по-настоящему актуально стало только сегодня: — Правило генерации: избегайте ручного набора кода, при любом удобном случае пишите программы, которые бы писали программы.
Проблема
Я, как можно догадаться, большой сторонник открытого, расширяемого, модульного ПО. Фанат Linux, микросервисной архитектуры и не большой любитель больших и сложных фреймворков, которые предлагают решить все возможные проблемы из коробки (хоть и пишу на .NET — каюсь). Поэтому меня во многом угнетает тенденция все большей зависимости от крупных корпораций в части программирования, когда за само создание программ или доступ к знаниям нужно платить деньги за подписку. Возможно, с дальнейшим развитием технологий, железо и сами модели станут гораздо более доступны для запуска на локальных устройствах и начнет появляться больше инструментов для этого, но я не хочу сидеть и ждать, когда эти времена настанут. По этой причине возникла идея создать своего модульного агента из доступных Open Source компонентов в том виде, в каком я вижу направление развития.
Определения
AI Agent — автономная программа, которая использует большую языковую модель (LLM) для достижения поставленной цели — агент не просто отвечает на вопрос, а планирует последовательность шагов, выбирает инструменты, выполняет действия и анализирует результат, чтобы решить задачу.
Tools — внешние функции или программные интерфейсы, которые агент может вызывать для взаимодействия с миром за пределами языковой модели — это его «руки», «глаза» и «уши».
OpenAI API — контракт REST API для доступа к LLM от компании OpenAI, который стал де-факто стандартом для множества движков запуска моделей (не является общепризнанным стандартом, но имеем, что имеем).
Llama.cpp — открытый движок для локального запуска больших языковых моделей (LLM), который поддерживает квантование, аппаратное ускорение на различных видеокартах, запуск в гибридном режиме (CPU + GPU) и предоставляет OpenAI API для взаимодействия с моделями.
Подготовка
Для запуска агента имеется домашний сервер, собранный из б/у компонентов:
Motherboard: Supermicro H11SSL-i
CPU: AMD EPYC 7601 (32 ядра, 64 потока, 8 каналов памяти)
RAM: 512 GB DDR4-2400
SSD: 2 TB
OS: Ubuntu Server 26.04.1 LTS
Также я использовал следующие LLM которые можно скачать с сайта Hugging Face:
Qwen3.8-27B-GGUF с квантом BF16
Qwen3.5-122B-A10B-GGUF с квантом UD-Q8_K_XL
Qwen3-Coder-Next-GGUF с квантом UD-Q8_K_XL
Скачиваем GGUF файлы этих моделей в отдельные папки для каждой модели и кладем в общую папку models.
Архитектура
Схема взаимодействия компонентов выглядит следующим образом:

Архитектура построена по модульному принципу. Инструменты могут подключаться/отключаться к аугменторам (о них подробнее будет ниже) независимо. Также сам состав аугменторов может быть изменен в зависимости от задачи и принципов композиции наборов инструментов. Взаимодействие между компонентами происходит через контракт OpenAI Responses API, а подключение инструментов через стандартный MCP веб протокол. Таким образом llama.cpp может быть заменена на любой сервер, поддерживающий запросы через OpenAI контракт. Клиент, который в данной реализации слушает входящие email, также может быть заменен на любого другого агента поддерживающего отправку запросов к OpenAI API, даже имея свой набор инструментов и алгоритм их вызова, т.к. клиент в этой схеме вообще о присутствии каких-либо инструментов не догадывается — их вызов и обработка происходят абсолютно непрозрачно для него. Каждый компонент отвечает строго за выполнение только своей функции и не требует для работы наличия каких-либо других связанных компонентов.
Для наглядности хочу показать дополнительные конфигурации, которые возможны в зависимости от требований:

или

Реализация
Полный скрипт для запуска агентской системы выглядит следующим образом:
services:
llama:
image: ghcr.io/ggml-org/llama.cpp:server
restart: unless-stopped
ports:
- "11434:8080"
volumes:
- ./models:/models
command:
- "--models-dir"
- "/models"
- "--port"
- "8080"
- "--host"
- "0.0.0.0"
mentat:
image: gendalf90/mentat:latest
restart: unless-stopped
depends_on:
- thinking-tools-augmentor
environment:
OpenAIUrl: "http://thinking-tools-augmentor:8080"
OpenAIModel: "Qwen3.5-122B-A10B-GGUF"
MailImapHost: "imap.mail.ru"
MailSmtpHost: "smtp.mail.ru"
MailUsers: "user-to-be-answered@mail.com"
MailLogin: "user-to-login@mail.ru"
MailPassword: "<mail server app password>"
programmer-buddy-mcp:
image: gendalf90/buddy:latest
restart: unless-stopped
environment:
OpenAIUrl: "http://common-tools-augmentor:8080"
OpenAIModel: "Qwen3-Coder-Next-GGUF"
OpenAIPrompt: "You are a good programmer."
ToolName: "ask_programmer"
ToolDescription: "Call special programming LLM. Use this when you need to solve any programming problem. The model can use common files in workspace and search or open pages from internet."
PromptDescription: "The free text for request."
office-buddy-mcp:
image: gendalf90/buddy:latest
restart: unless-stopped
environment:
OpenAIUrl: "http://common-tools-augmentor:8080"
OpenAIModel: "Qwen3.8-27B-GGUF"
OpenAIPrompt: "You are an office worker who can do various routine extensive work."
ToolName: "ask_office"
ToolDescription: "Call simple LLM. Use this when you need do various routine extensive work. The model can use common files in workspace and search or open pages from internet."
PromptDescription: "The free text for request."
files-mcp:
image: supercorp/supergateway:latest
restart: unless-stopped
volumes:
- ./workspace:/workspace
command:
- "--stdio"
- "npx -y @modelcontextprotocol/server-filesystem /workspace"
- "--outputTransport"
- "sse"
- "--port"
- "8080"
thinking-mcp:
image: supercorp/supergateway:latest
restart: unless-stopped
command:
- "--stdio"
- "npx -y @modelcontextprotocol/server-sequential-thinking"
- "--outputTransport"
- "sse"
- "--port"
- "8080"
fetcher-mcp:
image: ghcr.io/jae-jae/fetcher-mcp:latest
restart: unless-stopped
search-mcp:
image: supercorp/supergateway:latest
restart: unless-stopped
environment:
- SEARXNG_INSTANCES=http://searxng:8080
depends_on:
- searxng
command:
- "--stdio"
- "npx -y @kevinwatt/mcp-server-searxng"
- "--outputTransport"
- "sse"
- "--port"
- "8080"
thinking-tools-augmentor:
image: gendalf90/augmentor:latest
restart: unless-stopped
ports:
- "8822:8080"
depends_on:
- thinking-mcp
- programmer-buddy-mcp
- office-buddy-mcp
- common-tools-augmentor
environment:
- Endpoint=http://common-tools-augmentor:8080
- Mcp__Thinking__Endpoint=http://thinking-mcp:8080/sse
- Mcp__Programmer__Endpoint=http://programmer-buddy-mcp:8080
- Mcp__Office__Endpoint=http://office-buddy-mcp:8080
common-tools-augmentor:
image: gendalf90/augmentor:latest
restart: unless-stopped
ports:
- "8811:8080"
depends_on:
- files-mcp
- fetcher-mcp
- search-mcp
- llama
environment:
- Endpoint=http://llama:8080
- Mcp__Files__Endpoint=http://files-mcp:8080/sse
- Mcp__Fetcher__Endpoint=http://fetcher-mcp:3000/mcp
- Mcp__Search__Endpoint=http://search-mcp:8080/sse
searxng:
image: searxng/searxng:latest
container_name: searxng
restart: unless-stopped
volumes:
- ./searxng:/etc/searxng
Docker в данном случае выбран как средство контейнеризации и данная система или ее части могут быть развернуты например в Kubernetes или как самостоятельные процессы. В том числе очень удобно производить отладку частей системы запуская отдельные сервисы в debug режиме или с расширенным логированием.
Сервис, единственной задачей которого является обработка входящих email сообщений и формирование тредов из связанных ответов для последующей отправки модели в качестве истории. Электронную почту для коммуникации я выбрал как наиболее распространенный стандарт, доступный в виде множества реализаций и клиентов от разных компаний и не подверженный блокировкам, т.к. авторизация очень многих сервисов зависит от этого протокола. Также уже имеется встроенный механизм формирования тредов сообщений из которого легко формируется контекст для истории переписки с моделью.
Существует множество реализаций агентов, которые позволяют подключать MCP инструменты и обрабатывать их вызовы при запросах от модели. Но каждый раз при создании своего агента требуется реализовывать этот механизм заново. Данный сервис выносит механизм подключения и обработки запросов от модели во вне и позволяет переиспользовать независимо от агента. Клиентское приложение даже не догадывается, что какие-либо инструменты подключены и получает уже готовый ответ, независимо от того, сколько и какие инструменты вызывались моделью. Более того, данное решение меняет парадигму взаимодействия: инструменты подключаются не к клиенту, а являются как бы расширением сервера. Сервис выставляет наружу OpenAI интерфейс и выглядит для клиента как обычный движок для обработки запросов, скрывая за собой любое количество подключенных инструментов или даже цепочку из нескольких аугменторов.
Единственной задачей данного сервиса является создание обертки вокруг OpenAI интерфейса в виде MCP инструмента. Это дает возможность одной модели стандартным образом вызывать другую, более специализированную, модель. Т.е. это по сути адаптер позволяющий подключить и расширить существующего агента до полноценной агентской системы, где разные модели могут решать задачи совместно, координируюясь через общую историю вызовов.
Я изначально принял решение, что компоненты будут взаимодействовать через сеть. Но не все существующие реализации MCP инструментов это поддерживают. Стандартом сегодня является возможность подключения через stdio. Данный сервис является адаптером, который создает веб MCP интерфейс и транслирует вызовы к stdio инструмента.
Движок поиска в интернете, поддерживающий несколько провайдеров таких как: bing, duckduckgo, wikipedia и другие. Взаимодействие происходит через веб интерфейс, а результат поиска возвращается в json формате со списком найденных ресурсов. Сам по себе не имеет возможности подключения в виде инструмента, но добрые люди сделали MCP инструмент, который является адаптером к его интерфейсу.
Пример настроек из файла searxng/settings.yml, которые используются при запуске контейнера:
use_default_settings:
engines:
keep_only:
- bing
- duckduckgo
- wikipedia
server:
bind_address: "0.0.0.0"
secret_key: "<some generated key>"
port: 8080
search:
safe_search: 0
formats:
- html
- json
Результат
В результате мы получили локальную агентскую систему, способную гибко конфигурироваться и пересобираться под конкретное требование и решать широкий круг задач. Система находится полностью под нашим контролем, собрана из открытых и бесплатных компонентов. Каждая часть этой системы заменяема с возможностью подключать множество существующих доступных и совместимых компонентов.
Возможности:
принимает запросы по email;
поддерживает контекст в рамках треда;
может искать информацию через локальный SearXNG;
может открывать и читать содержимое веб-страницы через fetcher MCP;
может работать с файлами в изолированной workspace папке;
может делегировать рутинные, объемные задачи отдельной небольшой модели;
может делегировать программистские задачи отдельной coding-модели;
может при сложных запросах использовать sequential thinking как отдельный инструмент.
Что дальше?
Следующим шагом я вижу решение проблемы сохранения новых знаний и использования моделью долговременной памяти. Также мою фантазию будоражат возможности автоматизированного проектирования агентных систем (ADAS) когда целая агентная система создается под конкретную задачу специальным отдельным агентом-проектировщиком (MAS). При использовании подобного подхода к сборке системы из отдельных универсальных совместимых модулей такое проектирование значительно облегчается.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.