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 такого нет. Все компоненты равноправны, и связь между ними описывается явно. Это даёт три практических преимущества:
- Предсказуемость. Вы видите в конфиге, какие плагины загружены, и можете проследить путь запроса от входа до ответа.
- Заменяемость. Лямбда-функцию для обработки запроса можно заменить на внешний сервис, просто подключив другой плагин.
- Тестируемость. Каждый плагин проверяется изолированно, без подтягивания всего фреймворка.
Пример: в классическом фреймворке добавление кастомного инструмента часто выглядит как создание нового класса-инструмента и регистрация его в агенте через 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-запросы, читать файлы, запускать команды. С практической точки зрения, это самый частый тип плагина.
pythonfrom 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, локальную модель или что-то ещё — интерфейс одинаковый.
pythonfrom dsh.plugin import ModelProviderPlugin
from dsh.types import ChatCompletion
class LocalLlamaProvider(ModelProviderPlugin):
name = "local-llama"
async def chat(self, messages: list, **kwargs) -> ChatCompletion:
...
Третий тип — плагины-хендлеры событий. Они подписываются на события ядра: начало сессии, завершение задачи, ошибка выполнения. Полезно для логирования и алертинга.
pythonfrom 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):
yamlplugins:
- 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 у каждого плагина. В этот момент плагин может проверить зависимости, подключиться к внешним сервисам, зарегистрировать свои команды в реестре.
pythonfrom 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 почти все плагины асинхронные), выполнение не блокирует основной поток.
Пример вызова из кода агента:
pythonfrom 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 содержит метаданные и точки входа. С практической точки зрения, минимальный манифест выглядит так:
yamlname: my-plugin
version: "0.1.0"
description: «Пример плагина для DeepSeek Harness»
entrypoint: plugin.py
hooks:
- agent.start
- tool.execute
Поле entrypoint указывает на Python-файл с кодом плагина. Поле hooks перечисляет события фреймворка, на которые плагин подписывается. Если мы посмотрим на документацию cordis, там описаны все доступные хуки — от инициализации агента до выполнения отдельного инструмента.
Фреймворк ожидает, что в файле точки входа есть класс, наследующийся от базового плагина, и методы, соответствующие объявленным хукам.
Пример плагина на Python с комментариями
Напишем простой плагин, который логирует каждый вызов инструмента агента. Это полезно для отладки — когда не понятно, почему агент принял такое решение, смотришь лог плагина и видишь цепочку вызовов.
pythonfrom 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 в корне проекта:
yamlplugins:
- path: ./plugins/my-plugin
После перезапуска агента фреймворк подхватит новый плагин, и в логах начнут появляться записи о каждом вызове инструмента.
С практической точки зрения, начинать стоит с простых плагинов — логирования, валидации аргументов или обёртки над внешними API. Сложную логику лучше выносить в отдельные сервисы, а плагин использовать только как точку интеграции. Это сохраняет предсказуемость конфигурации и упрощает отладку.
Если мы посмотрим на опыт разработки ai-agents, то плагины позволяют не менять ядро фреймворка при добавлении новых возможностей. Вместо того чтобы править исходный код dsh, вы пишете плагин, кладете его в каталог plugins/ и подключаете через конфиг.
Вот более сложный пример — плагин, который проверяет аргументы инструмента перед выполнением и блокирует опасные операции:
pythonfrom 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 — там разбираются практические подходы к построению агентных систем.
Теперь плагин можно установить и проверить:
bashdsh plugin install ./plugins/my-plugin dsh agent restart
После этого фреймворк загрузит плагин, и он начнёт обращаться к объявленным хукам.
Когда плагинная архитектура становится проблемой
Когда плагинная архитектура становится проблемой
Плагины решают проблему жёсткой привязки кода, но вводят новую — неопределённость в том, как компоненты взаимодействуют между собой. Когда в системе десять плагинов, каждый из которых подписан на три события и модифицирует общий контекст, предсказать поведение становится сложнее, чем в монолите.
Отладка взаимодействия плагинов
Главная сложность не в отдельном плагине, а в цепочке вызовов. Плагин А меняет контекст, плагин Б читает этот контекст и передаёт дальше, плагин В падает, потому что ожидал другую структуру. В логах вы видите только последнее звено.
Во-первых, порядок загрузки плагинов часто не гарантирован. Если один плагин регистрирует обработчик события, а другой пытается использовать его при инициализации, вы получите ошибку, которая зависит от алфавитного порядка имён файлов или даты создания записи в базе. Это классическая проблема «works on my machine», которая проявляется только в продакшене.
С практической точки зрения, отладка такого взаимодействия требует трассировки, а не просто просмотра логов. В DeepSeek Harness можно включить подробное логирование цепочки вызовов через переменную окружения:
bashexport 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'
Если подозреваете, что плагины конфликтуют из-за общего состояния, отключайте их по одному, пока проблема не исчезнет. В конфиге это выглядит так:
yamlplugins:
- 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 или очереди сообщений. Это ухудшает производительность из-за сетевых задержек, но даёт изоляцию: сбой плагина не уронит основное приложение.
Связанные материалы, которые помогут посмотреть на проблему со стороны:
- mattpocock/skills: Skills for Real Engineers — о практических навыках инженеров, включая работу с расширяемыми системами.
- Навыки реальных инженеров: практические практики из .agents — примеры организации кода для агентов.
- Навыки реальных инженеров: практические практики из .agents — продолжение разбора архитектурных решений.
- Simantic: симуляция прошивок для AI-агентов — о тестировании агентов в изолированной среде.
- Автономная разработка на Rust с помощью Claw-Code — о границах автоматизации и ручного управления кодом.
Предсказуемость конфигурации важнее количества точек расширения. Если плагин делает систему сложнее, не давая при этом реальной выгоды, его стоит убрать.