DeepSeek Harness: всё — плагин для LLM-агентов
Разбираем архитектуру DeepSeek Harness, где каждый компонент — плагин. Как расширять фреймворк, писать свои плагины и интегрировать с LLM-агентами.
Что такое DeepSeek Harness и почему «всё — плагин»
Что такое DeepSeek Harness и почему «всё — плагин»
Когда я в 2013 году только начинал с Docker, контейнеры казались мне способом изолировать всё подряд. Спустя годы я пришёл к выводу, что изоляция сама по себе не даёт предсказуемости. Предсказуемость появляется, когда каждый компонент имеет чёткую границу и известный интерфейс. DeepSeek Harness (DSH) построен по такому же принципу: в фреймворке нет жёстко зашитых сущностей. Каждый компонент — будь то LLM-агент, инструмент для работы с файлами или модуль памяти — реализован как плагин.
Ядро называется cordis. Оно не знает, как именно агент общается с моделью или где хранятся логи. Оно только управляет жизненным циклом плагинов и маршрутизирует сообщения между ними.
Если провести параллель с домашней лабораторией, то DSH — это как серверная стойка, в которую можно вставить любой модуль, не пересобирая всю стойку. Плагин добавил — функционал появился. Убрал — осталось только ядро.
Основные концепции фреймворка
Во-первых, в DSH нет разницы между встроенным и сторонним компонентом. Плагин, который поставляется с фреймворком, и плагин, который вы написали сами, используют одну и ту же точку входа. Ядро cordis не делает исключений для своих модулей.
Во-вторых, взаимодействие между плагинами идёт через шину сообщений. Один плагин публикует событие, другой подписывается на него. Это убирает прямые зависимости: плагин для работы с файловой системой не должен знать о существовании плагина для LLM-агента.
В-третьих, конфигурация декларативная. Вы описываете в YAML, какие плагины загружать, с какими параметрами и в каком порядке. Ядро берёт на себя инициализацию и остановку.
Пример манифеста плагина (dsh-plugin):
yamlname: filesystem-reader
version: "0.1.0"
entrypoint: plugins.filesystem:reader
hooks:
- agent.message_received
- agent.tool_called
config:
allowed_paths:
- /home/user/projects
- /var/log/dsh
Здесь entrypoint указывает на объект, который ядро вызовет при загрузке. hooks — это события, на которые плагин подписывается. config — параметры, которые передаются в конструктор.
С практической точки зрения, такая схема напоминает Docker Compose для микросервисов: всё собирается из отдельных блоков, но у каждого блока свои нюансы. Главное — не пытаться запихнуть логику нескольких плагинов в один. Если вам нужно читать файлы и отправлять уведомления в Telegram, это два разных плагина, а не один универсальный.
Архитектура плагинов и точка расширения
Точка расширения — это место в коде ядра, где cordis вызывает код плагина. В DSH таких точек несколько: инициализация, обработка входящего сообщения от агента, вызов инструмента, завершение сессии.
Если мы посмотрим на устройство типичного плагина, он выглядит как класс, наследующийся от базового интерфейса. Ниже пример на Python, который показывает структуру, а не готовое решение для копирования:
pythonfrom dsh.plugin import BasePlugin, hook
from dsh.types import AgentMessage
class FilesystemReader(BasePlugin):
name = "filesystem-reader"
version = "0.1.0"
def __init__(self, config: dict):
self.allowed_paths = config.get("allowed_paths", [])
@hook("agent.message_received")
def on_message(self, message: AgentMessage) -> None:
if not message.tool_call:
return
path = message.tool_call.arguments.get("path")
if not self._is_allowed(path):
message.reply("Доступ к этому пути ограничен")
return
# Чтение файла и формирование ответа
...
def _is_allowed(self, path: str) -> bool:
return any(path.startswith(p) for p in self.allowed_paths)
Ключевой момент здесь — декоратор @hook. Он говорит ядру cordis: когда произойдёт событие agent.message_received, вызовите метод on_message этого плагина. Ядро само передаёт объект сообщения, плагин не должен сам запрашивать его из шины.
Регистрация плагина происходит в конфиге фреймворка:
yamlplugins:
- name: filesystem-reader
path: ./plugins/filesystem_reader.py
enabled: true
После этого при старте DSH подгрузит модуль, создаст экземпляр класса и подпишет его на указанные хуки.
Связанные материалы:
- Если вас интересуют готовые решения для поиска багов, посмотрите разбор QA-агентов Momentic ищут баги в веб и мобильных приложениях.
- Для оптимизации агентных оболочек полезна статья ECC: оптимизация и безопасность агентных оболочек.
- Если нужно углубиться в производительность, читайте Оптимизация производительности системы ECC для агентов.
Как написать свой плагин для DeepSeek Harness
Раздел: Как написать свой плагин для DeepSeek Harness
Плагин в DeepSeek Harness — это не отдельная утилита, а компонент, который фреймворк подгружает в рантайм и вызывает по заданному контракту. Если коротко: вы пишете код, который реализует интерфейс dsh.Plugin, кладете его в отдельную директорию и прописываете в конфиге. Harness сам подхватит его при старте и начнет передавать запросы от агентов.
Ниже разберу на примере, как это выглядит на практике. Я буду использовать Go — фреймворк собран на нём, и плагины пишутся на том же языке, чтобы не возиться с мостами и сериализацией между процессами.
Структура плагина и точки входа
Минимальный плагин — это один Go-пакет с экспортируемой структурой, реализующей интерфейс dsh.Plugin. Интерфейс небольшой, но от него зависит, как агент будет взаимодействовать с вашим кодом.
Структура каталога типичного плагина:
textdsh-plugin-example/ ├── go.mod ├── plugin.go └── README.md
Сам интерфейс выглядит примерно так:
gopackage dsh
type Plugin interface {
// Имя плагина — используется в логах и конфиге
Name() string
// Версия — полезна для отладки, когда плагинов становится больше двух
Version() string
// Init вызывается один раз при загрузке. Здесь читаем конфиг, открываем соединения
Init(config map[string]interface{}) error
// Execute — основная точка входа. Агент передаёт входные данные, плагин возвращает результат
Execute(ctx context.Context, input PluginInput) (PluginOutput, error)
}
PluginInput и PluginOutput — структуры из пакета dsh, в них лежат поля вроде Prompt, Context, Metadata на входе и Response, TokensUsed, Error на выходе.
Вот как может выглядеть реальная реализация простого плагина, который оборачивает вызов к внешнему эндпоинту:
gopackage main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/deepseek-harness/dsh"
)
type examplePlugin struct {
endpoint string
client *http.Client
}
func (p *examplePlugin) Name() string {
return "example"
}
func (p *examplePlugin) Version() string {
return "0.1.0"
}
func (p *examplePlugin) Init(config map[string]interface{}) error {
raw, ok := config["endpoint"]
if !ok {
return fmt.Errorf("endpoint is required in plugin config")
}
p.endpoint = raw.(string)
p.client = &http.Client{Timeout: 10 * time.Second}
return nil
}
func (p *examplePlugin) Execute(ctx context.Context, input dsh.PluginInput) (dsh.PluginOutput, error) {
payload, _ := json.Marshal(map[string]string{"prompt": input.Prompt})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.endpoint, bytes.NewReader(payload))
if err != nil {
return dsh.PluginOutput{}, err
}
resp, err := p.client.Do(req)
if err != nil {
return dsh.PluginOutput{}, err
}
defer resp.Body.Close()
var result struct {
Text string `json:"text"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return dsh.PluginOutput{}, err
}
return dsh.PluginOutput{
Response: result.Text,
}, nil
}
// Экспортируемая переменная — точка входа для загрузчика Harness
var Plugin dsh.Plugin = &examplePlugin{}
С практической точки зрения, важно помнить про predictability конфигурации. Если плагин читает переменные окружения, делайте это явно и документируйте в README.md. Я уже сталкивался с случаем, когда плагин в продакшене вёл себя иначе, чем на разработке, потому что автор забыл прописать одну переменную в systemd-юните.
Регистрация и загрузка плагина
Написать плагин — только половина дела. Harness должен знать, что он существует, и загрузить его перед стартом агентов. Регистрация происходит через конфигурационный файл, обычно это harness.yaml в корне проекта.
Пример конфига:
yamlversion: "1"
agents:
default:
model: deepseek-chat
temperature: 0.7
plugins:
- name: example
path: ./plugins/dsh-plugin-example
enabled: true
config:
endpoint: http://localhost:8080/v1/completion
timeout: 10s
- name: cordis-audit
path: ./plugins/cordis-audit
enabled: true
config:
log_level: debug
Поле path — относительный или абсолютный путь к директории с плагином. Harness при старте сканирует указанные пути, ищет скомпилированный плагин (или собирает его сам, если указан go.mod) и регистрирует в внутреннем реестре.
После того как конфиг готов, проверьте, что плагин подхвачен:
bashdsh plugin list
Ожидаемый вывод:
NAME VERSION STATUS
example 0.1.0 loaded
cordis-audit 0.2.1 loaded
Если плагин не apparaît в списке, проверьте логи Harness — чаще всего проблема в несовпадении версии API или в том, что путь к плагину указан неверно.
С точки зрения архитектуры, каждый плагин изолирован: если один плагин падает с паникой, это не должен гасить весь фреймворк. Harness перехватывает такие ситуации и помечает плагин как неисправный, продолжая работу с остальными. Это даёт предсказуемость при эксплуатации — вы точно знаете, какой компонент виноват, если что-то пошло не так.
Если вы планируете интегрировать плагины с агентными оболочками, посмотрите материал ECC: оптимизация и безопасность агентных оболочек — там разобраны границы доверия между плагином и агентом. Для общего контекста по фреймворку полезен обзор DeepSeek Harness: всё как плагин в DeepSeek.
Интеграция плагинов с LLM-агентами
Когда каждый компонент фреймворка — плагин, агент перестаёт быть монолитом. В DeepSeek Harness (dsh) это буквальная архитектура: модель, инструмент, память, роутер — каждый элемент реализован как dsh-plugin с чётко описанным интерфейсом. Для ai-agents это означает, что можно заменить плагин поиска документов, не трогая логику генерации, или добавить новый инструмент, не переписывая системный промпт. Подробнее об общей концепции можно прочитать в обзоре DeepSeek Harness: всё как плагин в DeepSeek.
Передача контекста между плагинами
Передача контекста — место, где большинство агентных фреймворков ломается. Один плагин отдаёт JSON, следующий ждёт строку, третий требует историю диалога в своём формате. В dsh контекст проходит через общую шину: каждый плагин deklariert, какие топики он читает и в какие пишет. В экосистеме cordis шина реализует publish-subscribe: плагин публикует сообщение в топик, а все подписчики получают его асинхронно.
На практике это выглядит так. Плагин извлечения документов подписан на топик documents.retrieved, плагин генерации ответа — на context.ready. Между ними стоит посредник, который проверяет схему и не пропускает битые данные дальше.
Пример манифеста плагина в dsh-plugin.yaml:
yamlapiVersion: dsh/v1
kind: Plugin
metadata:
name: context-aggregator
version: "0.1.0"
spec:
inputs:
- topic: documents.retrieved
type: object
schema:
type: object
properties:
chunks:
type: array
items:
type: string
source:
type: string
outputs:
- topic: context.ready
type: object
schema:
type: object
properties:
aggregated:
type: string
metadata:
type: object
handler: src/aggregator.py
С практической точки зрения, схема важнее кода. Если переименовать поле chunks в passages, все плагины-потребители сломаются молча — или начнут возвращать пустые ответы. Поэтому я всегда описываю контракт в YAML до того, как приступлю к handler.
Проверить совместимость перед запуском можно так:
bashdsh plugin validate --file dsh-plugin.yaml --strict
При несовпадении схем утилита вернёт ненулевой код выхода и список конфликтов. Это даёт предсказуемость: сборка не пройдёт, пока контракт не исправлен.
Во-первых, не передавайте контекст через глобальные переменные или временные файлы. Это антипаттерн, который в продакшене приводит к гонкам данных и усложняет масштабирование агента по нодам. Во-вторых, историю диалога храните в отдельном плагине памяти, а не размазывайте её по всем компонентам.
Пример взаимодействия с моделью DeepSeek
Модель в dsh — тоже плагин. Поэтому подключить DeepSeek Chat, DeepSeek Coder или локальную модель можно через один интерфейс.
Конфиг плагина модели:
yamlapiVersion: dsh/v1
kind: Plugin
metadata:
name: deepseek-chat
version: "1.0.0"
spec:
inputs:
- topic: llm.generate
type: object
schema:
type: object
properties:
messages:
type: array
items:
type: object
properties:
role:
type: string
content:
type: string
temperature:
type: number
max_tokens:
type: integer
outputs:
- topic: llm.response
type: object
handler: src/deepseek_handler.py
env:
- name: DEEPSEEK_API_KEY
required: true
- name: DEEPSEEK_BASE_URL
default: "https://api.deepseek.com/v1"
Упрощённый handler на Python:
pythonimport os
import httpx
from dsh import PluginBase, Context
class DeepSeekHandler(PluginBase):
async def handle(self, ctx: Context) -> dict:
api_key = os.environ["DEEPSEEK_API_KEY"]
base_url = os.environ.get(
"DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1"
)
payload = {
"model": "deepseek-chat",
"messages": ctx.input["messages"],
"temperature": ctx.input.get("temperature", 0.7),
"max_tokens": ctx.input.get("max_tokens", 1024),
}
async with httpx.AsyncClient() as client:
response = await client.post(
f"{base_url}/chat/completions",
headers={"Authorization": f"Bearer {api_key}"},
json=payload,
timeout=60.0,
)
response.raise_for_status()
data = response.json()
return {
"topic": "llm.response",
"payload": {
"content": data["choices"][0]["message"]["content"],
"usage": data.get("usage"),
},
}
Вызов из агента выглядит как публикация в топик:
pythonawait bus.publish("llm.generate", {
"messages": [
{"role": "system", "content": "Ты — инженер поддержки. Отвечай по существу."},
{"role": "user", "content": "Почему сервер не отвечает на пинг?"},
],
"temperature": 0.3,
"max_tokens": 512,
})
Плагин памяти может обогатить запрос предыдущими сообщениями до того, как он дойдёт до модели. В этом и заключается преимущество архитектуры «всё — плагин»: новый компонент вставляется в цепочку без изменения кода плагина модели.
В итоге агент в DeepSeek Harness — это конфигурируемый конвейер плагинов, а не скрипт с хардкодом. Контекст передаётся через типизированную шину, модель подключается как отдельный компонент, а добавление инструмента сводится к описанию схемы и реализации handler. При соблюдённом контракте замена одного плагина на другой не ломает остальную систему.
Если интересуют примеры агентных оболочек, см. ECC: оптимизация и безопасность агентных оболочек и QA-агенты Momentic ищут баги в веб и мобильных приложениях.
Практика: развёртывание и отладка плагинов
Развёртывание DeepSeek Harness не отличается от запуска любого другого контейнеризованного сервиса, но есть нюанс: в dsh каждый компонент — плагин, и плагин должен быть доступен ещё до того, как агент попытается с ним связаться. Ниже покажу, как я собираю окружение вручную, без GUI, и где искать причину, если плагин молчит.
Подробнее об архитектуре самого фреймворка читайте в материале DeepSeek Harness: всё как плагин в DeepSeek.
Запуск через Docker Compose
Во-первых, определимся со структурой. Я держу конфиги рядом с кодом плагинов, а не в отдельной директории «etc» где-то в глубине файловой системы. Это даёт предсказуемость: открыл репозиторий — увидел и docker-compose.yml, и плагин.
Пример docker-compose.yml для локальной разработки:
yamlversion: "3.8"
services:
dsh-core:
image: deepseek/harness:latest
ports:
- "8080:8080"
volumes:
- ./plugins:/opt/dsh/plugins:ro
- ./config/dsh.yaml:/etc/dsh/config.yaml:ro
environment:
- DS_PLUGIN_PATH=/opt/dsh/plugins
- DS_LOG_LEVEL=debug
depends_on:
- cordis
cordis:
image: deepseek/cordis:latest
ports:
- "9090:9090"
volumes:
- cordis-data:/var/lib/cordis
dsh-plugin-example:
build: ./plugins/example
volumes:
- ./plugins/example:/app:ro
environment:
- DS_CORE_URL=http://dsh-core:8080
restart: unless-stopped
volumes:
cordis-data:
Команда для запуска:
bashdocker compose up -d
Сначала убедитесь, что порт 8080 на хосте не занят другим сервисом. Cordis здесь работает как реестр плагинов и точка обнаружения — без него агент не найдёт только что собранный dsh-plugin. Если плагин написан на Go или Rust, собираю образ отдельным Dockerfile и монтирую исходники только для отладки; в продакшене использую собранный образ без исходников.
Конфиг dsh.yaml пишу вручную:
yamlagent:
name: "Пятачок"
endpoint: http://dsh-core:8080
plugins:
- name: example
url: http://dsh-plugin-example:8081
timeout: 30s
Имя агента — отсылка к моей домашней номенклатуре серверов (Винни-Пух, Пятачок, Сова, Кролик), но в продакшене лучше что-то более нейтральное.
Логи и диагностика плагинов
Если агент не видит плагин, первым делом смотрю логи ядра dsh:
bashdocker logs -f dsh-core
Плагины пишут в stdout, поэтому их логи тоже доступны через Docker:
bashdocker logs -f dsh-plugin-example
Для более глубокой диагностики поднимаю временный контейнер с отладочной оболочкой и проверяю связь между сервисами:
bashdocker run --rm --network dsh_default -it alpine sh
# внутри контейнера:
wget -qO- http://dsh-core:8080/health
wget -qO- http://dsh-plugin-example:8081/metrics
С практической точки зрения, полезно включить уровень debug в конфиге плагина, а не только в ядре. В dsh.yaml добавляю:
yamlplugins:
- name: example
debug: true
Если плагин молчит, проверяю три вещи:
- Доступен ли порт плагина изнутри сети Docker.
- Совпадает ли имя плагина в cordis с тем, что ожидает агент.
- Не блокирует ли firewall трафик между контейнерами (редко, но бывает).
Для постоянного мониторинга подключаю Loki и Prometheus — это уже отдельная история, но без алертов на «плагин недоступен» отладка превращается в гадание на кофейной гуще.
Плагин запущен, логи текут, можно переходить к написанию собственного расширения.
Часто задаваемые вопросы
Как написать первый плагин для DeepSeek Harness?
Начните с манифеста в YAML: укажите имя, версию и точку входа. Cordis загружает плагин по этому файлу и не делает различий между встроенным и сторонним кодом — для ядра все плагины одинаковы. Самый надёжный способ — реализовать плагин как отдельный процесс, который читает сообщения из шины и публикует ответы в ту же шину.
На каком языке лучше писать плагины?
DSH не навязывает язык. Главное — плагин должен обмениваться сообщениями через шину и понимать формат манифеста. Я сам предпочитаю Go за предсказуемость и стабильную компиляцию, но для быстрого прототипа подойдёт Python или даже Bash-скрипт.
Как плагины обмениваются данными, если они не знают друг о друге?
Через шину сообщений по принципу издатель — подписчик. Один плагин публикует событие, другой получает его, не зная, кто отправитель. Это убирает прямые зависимости, как в сетевом хранилище: сервер не знает, какие клиенты подключены к NFS-шаре.
Можно ли использовать DSH без LLM-агентов?
Можно. Ядро cordis управляет жизненным циклом и маршрутизацией, а LLM — всего лишь один из возможных плагинов. Вы можете собрать конвейер для обработки файлов, бэкапов или мониторинга, не подключая языковую модель вообще.
Как отлаживать плагин, если cordis не видит его внутреннюю логику?
Плагин пишет логи сам, ядро видит только события запуска и остановки. Настройте вывод в journald или Loki и отслеживайте трафик на шине сообщений. Если плагин не стартует, проверьте манифест и права на исполнение файла с точкой входа.