Продвинутый

DeepSeek Harness: плагины вместо жёсткого кода

Алексей Кузнецов
Алексей Кузнецов
Системный администратор31 октября 2026 г.12 мин чтения

DeepSeek Harness — фреймворк для LLM-агентов, где любой компонент — плагин. Разбираем архитектуру, точки расширения и примеры плагинов на Python.

DeepSeek Harness: плагины вместо жёсткого кода

DeepSeek Harness — фреймворк для LLM-агентов, где любой компонент — плагин. Разбираем архитектуру, точки расширения и примеры плагинов на Python.

Что такое DeepSeek Harness и зачем он нужен

Что такое DeepSeek Harness и зачем он нужен

DeepSeek Harness (далее DSH) — фреймворк для создания ai-agents, в котором нет жёстко зашитых цепочек действий. Каждый компонент — от поставщика модели до системы памяти — оформляется как плагин. Для инженера это означает, что поведение агента настраивается конфигом и набором установленных плагинов, а не правкой исходного кода фреймворка.

На практике это выглядит так: вы подключаете нужные плагины, описываете их взаимодействие в YAML-файле, и получаете предсказуемого агента, который можно аудитировать и модифицировать без пересборки ядра.

Основные концепции: всё — плагин

В DSH нет понятия «базовый агент» с фиксированным набором возможностей. Ядро фреймворка отвечает только за обнаружение плагинов, их инициализацию и передачу управления между компонентами. Сами возможности — подключение к LLM, вызов внешних API, работа с векторной базой, выполнение кода — реализуются отдельными плагинами.

Во-первых, это разделяет ответственность. Плагин LLM-адаптера не знает о том, как агент хранит историю диалога. Плагин инструмента не знает, какая модель принимает решение о его вызове. Каждый плагин экспортирует чёткий интерфейс, и ядро склеивает их в runtime.

С практической точки зрения, плагин на Python — это обычный пакет с точкой входа dsh-plugin. Пример минимального плагина:

python
# dsh_plugin_hello/__init__.py
from dsh import Plugin, hookimpl

class HelloPlugin(Plugin):
    name = "hello"

    @hookimpl
    def register_tools(self, registry):
        registry.register("say_hello", self.say_hello)

    def say_hello(self, name: str) -> str:
        return f"Привет, {name}"

Конфигурация агента описывает, какие плагины загрузить и в каком порядке они участвуют в цепочке:

yaml
# agent.yaml
plugins:
  - dsh.llm.deepseek
  - dsh.memory.sqlite
  - dsh_plugin_hello

llm:
  provider: deepseek
  model: deepseek-chat
  temperature: 0.2

tools:
  - say_hello

Здесь нет магии: плагин dsh_plugin_hello просто регистрирует инструмент say_hello, а ядро делает его доступным для вызова агентом. Если нужно заменить SQLite на PostgreSQL, достаточно сменить плагин памяти в конфиге — код агента не меняется.

Отличие от классических фреймворков для агентов

Классические фреймворки вроде LangChain или LlamaIndex построены иначе. Они предлагают готовые цепочки — AgentExecutor, RetrievalQA, ConversationalAgent — внутри которых логика жёстко связана с конкретными классами. Хотите добавить нестандартный инструмент? Часто приходится наследоваться от базового класса, переопределять метод и править исходный код фреймворка.

В DSH такого нет. Все компоненты равноправны, и связь между ними описывается явно. Это даёт три практических преимущества:

  1. Предсказуемость. Вы видите в конфиге, какие плагины загружены, и можете проследить путь запроса от входа до ответа.
  2. Заменяемость. Лямбда-функцию для обработки запроса можно заменить на внешний сервис, просто подключив другой плагин.
  3. Тестируемость. Каждый плагин проверяется изолированно, без подтягивания всего фреймворка.

Пример: в классическом фреймворке добавление кастомного инструмента часто выглядит как создание нового класса-инструмента и регистрация его в агенте через monkey-patching. В DSH это плагин с одной точкой входа:

python
# dsh_plugin_calculator/__init__.py
from dsh import Plugin, hookimpl

class CalculatorPlugin(Plugin):
    name = "calculator"

    @hookimpl
    def register_tools(self, registry):
        registry.register("calculate", self.calculate)

    def calculate(self, expression: str) -> float:
        return eval(expression)  # упрощённо, в продакшене нужен парсер

Затем в agent.yaml добавляется строка - dsh_plugin_calculator, и агент получает новую возможность без перекомпиляции и без правки ядра.

Ещё по теме агентов и инженерных подходов к их разработке полезно посмотреть материалы про навыки реальных инженеров из .agents и про симуляцию прошивок для AI-агентов.

Архитектура плагинов в DeepSeek Harness

Архитектура плагинов в DeepSeek Harness

DeepSeek Harness строится вокруг идеи, что любой компонент — плагин. Это не маркетинговая фраза, а архитектурное решение. Когда я впервые посмотрел на код, вспомнил свой первый Dell PowerEdge R730: там тоже всё собиралось из отдельных блоков, и каждый блок можно было заменить, не разбирая сервер целиком. В Harness то же самое — ядро отвечает за оркестрацию, а бизнес-логика вынесена в плагины.

Если вы уже читали про навыки реальных инженеров из .agents, там тоже говорилось о разделении ответственности. В Harness это разделение реализовано через чёткие точки расширения.

Точки расширения и интерфейсы плагинов

Точка расширения — это место в ядре, где плагин может подключиться. В Harness таких точек несколько, и каждая имеет свой интерфейс. Если мы посмотрим на исходники, увидим базовые классы, от которых нужно наследоваться.

Во-первых, есть интерфейс для инструментов (tools). Плагин-инструмент позволяет агенту взаимодействовать с внешним миром: делать HTTP-запросы, читать файлы, запускать команды. С практической точки зрения, это самый частый тип плагина.

python
from dsh.plugin import ToolPlugin
from dsh.types import ToolResult

class GitStatusPlugin(ToolPlugin):
    name = "git_status"
    description = "Показывает статус git-репозитория в рабочей директории"

    async def execute(self, args: dict) -> ToolResult:
        repo_path = args.get("path", ".")
        # выполнение команды, обработка вывода, возврат результата
        ...

Второй тип — плагины-провайдеры моделей. Они абстрагируют доступ к LLM. Не важно, используете вы DeepSeek, локальную модель или что-то ещё — интерфейс одинаковый.

python
from dsh.plugin import ModelProviderPlugin
from dsh.types import ChatCompletion

class LocalLlamaProvider(ModelProviderPlugin):
    name = "local-llama"
    
    async def chat(self, messages: list, **kwargs) -> ChatCompletion:
        ...

Третий тип — плагины-хендлеры событий. Они подписываются на события ядра: начало сессии, завершение задачи, ошибка выполнения. Полезно для логирования и алертинга.

python
from dsh.plugin import EventHandlerPlugin
from dsh.events import SessionStarted, TaskCompleted

class AuditLogger(EventHandlerPlugin):
    name = "audit-logger"
    
    async def handle(self, event):
        if isinstance(event, SessionStarted):
            self.log.info("Сессия начата: %s", event.session_id)

С точки зрения предсказуемости, важно чётко разделять эти типы. Плагин-инструмент не должен лезть в управление сессией, а хендлер событий не должен пытаться заменить модель. Я видел, как в одном проекте смешали ответственности — в итоге плагин стал невозможно тестировать изолированно.

Жизненный цикл плагина: регистрация и выполнение

Жизненный цикл плагина в Harness состоит из трёх этапов: обнаружение, регистрация и выполнение.

Обнаружение происходит при запуске ядра. Harness сканирует указанные директории и пакеты, ищет классы, унаследованные от базовых плагинов. Регистрация — это добавление плагина в реестр ядра, после чего он становится доступен агенту.

Пример конфигурации плагинов (dsh-plugins.yaml):

yaml
plugins:
  - name: git_status
    path: ./plugins/git_status.py
    class: GitStatusPlugin
    enabled: true
    config:
      repo_path: /opt/repos/project
  
  - name: local-llama
    path: ./providers/llama.py
    class: LocalLlamaProvider
    enabled: true
    config:
      endpoint: http://localhost:11434
      model: deepseek-coder:6.7b

Затем ядро вызывает метод register у каждого плагина. В этот момент плагин может проверить зависимости, подключиться к внешним сервисам, зарегистрировать свои команды в реестре.

python
from dsh.plugin import ToolPlugin

class GitStatusPlugin(ToolPlugin):
    name = "git_status"
    
    async def register(self, registry):
        import shutil
        if not shutil.which("git"):
            raise RuntimeError("git не найден в PATH")
        
        registry.register_tool(self.name, self.execute)
        self.log.info("Плагин %s зарегистрирован", self.name)

Выполнение происходит, когда агент вызывает инструмент по имени. Ядро ищет плагин в реестре, передаёт аргументы и ждёт результата. Если плагин асинхронный (а в Harness почти все плагины асинхронные), выполнение не блокирует основной поток.

Пример вызова из кода агента:

python
from dsh.agent import Agent
from dsh.core import Harness

harness = Harness(config_path="dsh-plugins.yaml")
await harness.load_plugins()

agent = Agent(harness=harness, model_provider="local-llama")
result = await agent.call_tool("git_status", {"path": "/opt/repos/project"})
print(result.output)

С практической точки зрения, я всегда запускаю плагины в отдельных виртуальных окружениях или контейнерах. Если плагин требует специфических зависимостей, они не должны ломать основное окружение Harness. Это то же самое, что и с systemd-сервисами: каждый сервис в своём изоляторе, и если один упадёт, остальные продолжат работать.

Если мы посмотрим на имена серверов в моей домашней лаборатории — Винни-Пух, Пятачок, Сова — то каждый из них отвечает за свою зону ответственности. Плагины в Harness должны быть такими же: один плагин — одна ответственность, чёткие границы, предсказуемое поведение.

Архитектура плагинов в DeepSeek Harness даёт гибкость без хаоса. Ключ к стабильности — чёткое разделение типов плагинов, явная регистрация и изоляция зависимостей. Если соблюдать эти правила, добавление нового компонента не превращается в переделку ядра.

Как написать собственный плагин

Структура плагина и файл конфигурации

Плагин в DeepSeek Harness — это каталог с манифестом и кодом. Фреймворк ищет плагины в каталоге, указанном в конфигурации dsh, и загружает их при старте агента.

Во-первых, нужно создать каталог плагина. Имя каталога станет идентификатором плагина в системе.

Внутри каталога должен быть файл dsh-plugin.yaml — манифест, который описывает плагин фреймворку.

Пример структуры:

my-plugin/
├── dsh-plugin.yaml
└── plugin.py

Файл конфигурации dsh-plugin.yaml содержит метаданные и точки входа. С практической точки зрения, минимальный манифест выглядит так:

yaml
name: my-plugin
version: "0.1.0"
description: «Пример плагина для DeepSeek Harness»
entrypoint: plugin.py
hooks:
  - agent.start
  - tool.execute

Поле entrypoint указывает на Python-файл с кодом плагина. Поле hooks перечисляет события фреймворка, на которые плагин подписывается. Если мы посмотрим на документацию cordis, там описаны все доступные хуки — от инициализации агента до выполнения отдельного инструмента.

Фреймворк ожидает, что в файле точки входа есть класс, наследующийся от базового плагина, и методы, соответствующие объявленным хукам.

Пример плагина на Python с комментариями

Напишем простой плагин, который логирует каждый вызов инструмента агента. Это полезно для отладки — когда не понятно, почему агент принял такое решение, смотришь лог плагина и видишь цепочку вызовов.

python
from dsh_plugin import BasePlugin, HookContext
import logging

logger = logging.getLogger("dsh.plugin.tool_logger")


class ToolLoggerPlugin(BasePlugin):
    """
    Плагин для логирования вызовов инструментов агента.
    Подписывается на хук tool.execute и записывает в лог
    имя инструмента и переданные аргументы.
    """

    def on_tool_execute(self, ctx: HookContext) -> None:
        """
        Метод вызывается фреймворком перед выполнением любого инструмента.
        ctx содержит информацию о текущем агенте, инструменте и аргументах.
        """
        tool_name = ctx.tool.name
        arguments = ctx.arguments

        logger.info(
            "Инструмент %s вызван с аргументами: %s",
            tool_name,
            arguments,
        )

        # Возвращаем None, чтобы фреймворк продолжил выполнение.
        # Если вернуть объект ответа, фреймворк подставит его вместо реального вызова.
        return None

Чтобы плагин заработал, нужно зарегистрировать его в конфигурации dsh. Обычно это делается в файле dsh.yaml в корне проекта:

yaml
plugins:
  - path: ./plugins/my-plugin

После перезапуска агента фреймворк подхватит новый плагин, и в логах начнут появляться записи о каждом вызове инструмента.

С практической точки зрения, начинать стоит с простых плагинов — логирования, валидации аргументов или обёртки над внешними API. Сложную логику лучше выносить в отдельные сервисы, а плагин использовать только как точку интеграции. Это сохраняет предсказуемость конфигурации и упрощает отладку.

Если мы посмотрим на опыт разработки ai-agents, то плагины позволяют не менять ядро фреймворка при добавлении новых возможностей. Вместо того чтобы править исходный код dsh, вы пишете плагин, кладете его в каталог plugins/ и подключаете через конфиг.

Вот более сложный пример — плагин, который проверяет аргументы инструмента перед выполнением и блокирует опасные операции:

python
from dsh_plugin import BasePlugin, HookContext, HookResult
import re

ALLOWED_PATTERNS = [
    r"^ls\s",
    r"^cat\s",
    r"^grep\s",
]


class SafetyPlugin(BasePlugin):
    """
    Плагин безопасности: блокирует выполнение инструментов,
    аргументы которых не соответствуют разрешённым шаблонам.
    """

    def on_tool_execute(self, ctx: HookContext) -> HookResult:
        command = ctx.arguments.get("command", "")

        if not any(re.match(pattern, command) for pattern in ALLOWED_PATTERNS):
            return HookResult.blocked(
                reason="Команда не соответствует разрешённым шаблонам"
            )

        return HookResult.allow()

Здесь HookResult.blocked отменяет выполнение инструмента и возвращает ошибку агенту. HookResult.allow() пропускает вызов дальше.

Если нужны дополнительные материалы по архитектуре агентов, полезно посмотреть mattpocock/skills: Skills for Real Engineers — там разбираются практические подходы к построению агентных систем.

Теперь плагин можно установить и проверить:

bash
dsh plugin install ./plugins/my-plugin
dsh agent restart

После этого фреймворк загрузит плагин, и он начнёт обращаться к объявленным хукам.

Когда плагинная архитектура становится проблемой

Когда плагинная архитектура становится проблемой

Плагины решают проблему жёсткой привязки кода, но вводят новую — неопределённость в том, как компоненты взаимодействуют между собой. Когда в системе десять плагинов, каждый из которых подписан на три события и модифицирует общий контекст, предсказать поведение становится сложнее, чем в монолите.

Отладка взаимодействия плагинов

Главная сложность не в отдельном плагине, а в цепочке вызовов. Плагин А меняет контекст, плагин Б читает этот контекст и передаёт дальше, плагин В падает, потому что ожидал другую структуру. В логах вы видите только последнее звено.

Во-первых, порядок загрузки плагинов часто не гарантирован. Если один плагин регистрирует обработчик события, а другой пытается использовать его при инициализации, вы получите ошибку, которая зависит от алфавитного порядка имён файлов или даты создания записи в базе. Это классическая проблема «works on my machine», которая проявляется только в продакшене.

С практической точки зрения, отладка такого взаимодействия требует трассировки, а не просто просмотра логов. В DeepSeek Harness можно включить подробное логирование цепочки вызовов через переменную окружения:

bash
export DSH_DEBUG=1
export DSH_LOG_PLUGIN_CALLS=1
python -m dsh run --config config.yaml

Пример вывода, который поможет найти проблемный плагин:

[2024-01-15 10:23:41] Plugin 'cordis-memory' loaded (order: 1)
[2024-01-15 10:23:41] Plugin 'dsh-plugin-webhook' loaded (order: 2)
[2024-01-15 10:23:42] Event 'agent.start' -> cordis-memory (1ms)
[2024-01-15 10:23:42] Event 'agent.start' -> dsh-plugin-webhook (45ms)
[2024-01-15 10:23:43] ERROR in dsh-plugin-webhook: context missing key 'session_id'

Если подозреваете, что плагины конфликтуют из-за общего состояния, отключайте их по одному, пока проблема не исчезнет. В конфиге это выглядит так:

yaml
plugins:
  - name: cordis-memory
    enabled: true
  - name: dsh-plugin-webhook
    enabled: false

Ещё один реальный нюанс — плагины могут модифицировать объекты на месте, а не создавать копии. Если плагин А добавляет ключ в словарь контекста, плагин Б увидит это изменение, даже если не подписан на событие от плагина А. Это не баг, а особенность реализации, но отлаживать такое без знания исходного кода всех плагинов почти невозможно.

Альтернативы и границы применения

Плагинная архитектура не всегда оправдана. Если у вас два-три компонента, которые редко меняются, проще написать их напрямую в основном коде, чем поддерживать инфраструктуру загрузки плагинов, манифестов и точек расширения.

Сравните два подхода. Жёсткий код:

python
# agent.py
def process_message(message: str) -> str:
    response = llm.generate(message)
    save_to_database(response)
    return response

Плагины:

python
# agent.py
from dsh import PluginManager

manager = PluginManager()
manager.load_from_config("plugins.yaml")

def process_message(message: str) -> str:
    context = {"message": message}
    manager.emit("before_process", context)
    response = llm.generate(context["message"])
    context["response"] = response
    manager.emit("after_process", context)
    return context["response"]

Второй вариант даёт гибкость, но требует поддержки plugins.yaml, документации по событиям и обратной совместимости при изменении ядра.

Граница применения простая: выносите в плагины то, что действительно будет меняться независимо от ядра. Если вы планируете поддерживать только один тип хранилища и один канал уведомлений, плагины — избыточная абстракция. Если же вы строите платформу, где разные команды будут добавлять свои интеграции без доступа к основному репозиторию, плагины оправданы.

Иногда лучшей альтернативой плагину является отдельный микросервис. Если плагин требует собственных зависимостей, отдельного цикла развёртывания или работает с чужими данными, проще вынести его в отдельный процесс и общаться по HTTP или очереди сообщений. Это ухудшает производительность из-за сетевых задержек, но даёт изоляцию: сбой плагина не уронит основное приложение.

Связанные материалы, которые помогут посмотреть на проблему со стороны:

Предсказуемость конфигурации важнее количества точек расширения. Если плагин делает систему сложнее, не давая при этом реальной выгоды, его стоит убрать.

Поделиться:TelegramX / TwitterVK

Читайте также