14. Экосистема и Фреймворки¶
Зачем это нужно?¶
При создании AI-агентов вы сталкиваетесь с выбором: писать всё с нуля или использовать фреймворк? Оба подхода имеют плюсы и минусы, и понимание того, когда что выбирать, критично для долгосрочного успеха.
Эта глава поможет принимать архитектурные решения, избегать vendor lock-in и использовать готовые решения там, где они реально подходят.
Реальный кейс¶
Ситуация: Вам нужно создать DevOps-агента. Вы можете:
- Использовать популярный фреймворк, который предоставляет всё из коробки
- Построить собственный runtime, адаптированный под ваши нужды
Проблема:
- Подход с фреймворком: Быстрый старт, но вы заперты в их абстракциях. Когда нужна кастомная логика, вы боретесь с фреймворком.
- Кастомный подход: Полный контроль, но вы изобретаете велосипед. Каждая функция (tool execution, memory, planning) требует реализации.
Решение: Понять компромиссы. Выбирайте фреймворк, когда важна скорость и требования стандартные. Выбирайте кастом, когда нужен специфический контроль или есть уникальные ограничения.
Теория простыми словами¶
Что такое фреймворки для агентов?¶
Фреймворки для агентов — это библиотеки или платформы, которые предоставляют:
- Инфраструктуру выполнения инструментов — обработка function calling, валидация, обработка ошибок
- Управление памятью — контекстные окна, саммаризация, сохранение состояния
- Паттерны планирования — ReAct циклы, оркестрация workflow, декомпозиция задач
- Координация multi-agent — supervisor паттерны, изоляция контекста, маршрутизация
Суть: Фреймворки абстрагируют общие паттерны, но они также накладывают ограничения. Понимание этих ограничений помогает решить, когда использовать их, а когда строить кастом.
Кастомный Runtime vs Фреймворк¶
Кастомный Runtime:
- Полный контроль над каждым компонентом
- Нет vendor lock-in
- Оптимизирован под ваш конкретный use case
- Больше кода для написания и поддержки
- Нужно реализовывать общие паттерны самостоятельно
Фреймворк:
- Быстрая разработка, проверенные паттерны
- Поддержка сообщества и примеры
- Обрабатывает edge cases, которые вы можете пропустить
- Меньше гибкости, сложнее кастомизировать
- Потенциальный vendor lock-in
- Может включать функции, которые вам не нужны
Как выбирать?¶
Критерии решения¶
Выбирайте кастомный Runtime когда:
- Уникальные требования — Ваш use case не вписывается в стандартные паттерны
- Критична производительность — Нужен тонкий контроль над latency/cost
- Минимум зависимостей — Хотите избежать внешних зависимостей
- Цель обучения — Хотите глубоко понять внутренности
- Долгосрочный контроль — Нужно независимо поддерживать и развивать систему
Выбирайте фреймворк когда:
- Стандартный use case — Ваши требования соответствуют общим паттернам
- Скорость выхода на рынок — Нужно быстро запустить
- Знакомство команды — Ваша команда уже знает фреймворк
- Быстрое прототипирование — Исследуете идеи и нужны быстрые итерации
- Поддержка сообщества — Выигрываете от примеров и знаний сообщества
Соображения портабельности¶
Избегайте vendor lock-in через:
- Абстракцию интерфейсов — Определяйте свои интерфейсы для tools, memory, planning
- Минимальную связь с фреймворком — Используйте фреймворк для оркестрации, но держите бизнес-логику отдельно
- Стандартные протоколы — Предпочитайте стандартные форматы (JSON Schema для tools, OpenTelemetry для observability)
- Постепенный путь миграции — Проектируйте так, чтобы можно было менять компоненты позже
Работа с JSON Schema в Go¶
При использовании JSON Schema для определений инструментов предпочитайте пакеты Go для валидации и генерации вместо сырого json.RawMessage. Это обеспечивает типобезопасность и лучшую обработку ошибок.
Пример: Использование github.com/xeipuuv/gojsonschema для валидации:
import (
"github.com/xeipuuv/gojsonschema"
)
// Определяем схему инструмента как JSON Schema
const pingToolSchema = `{
"type": "object",
"properties": {
"host": {
"type": "string",
"description": "Hostname or IP address to ping"
},
"count": {
"type": "integer",
"description": "Number of ping packets",
"default": 4,
"minimum": 1,
"maximum": 10
}
},
"required": ["host"]
}`
// Валидируем аргументы инструмента против схемы
func validateToolArgs(schemaJSON string, args map[string]any) error {
schemaLoader := gojsonschema.NewStringLoader(schemaJSON)
documentLoader := gojsonschema.NewGoLoader(args)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
return fmt.Errorf("ошибка валидации схемы: %w", err)
}
if !result.Valid() {
errors := make([]string, 0, len(result.Errors()))
for _, desc := range result.Errors() {
errors = append(errors, desc.String())
}
return fmt.Errorf("валидация не прошла: %s", strings.Join(errors, "; "))
}
return nil
}
// Использование при выполнении инструмента
func executePing(args map[string]any) (string, error) {
// Валидируем аргументы перед выполнением
if err := validateToolArgs(pingToolSchema, args); err != nil {
return "", err
}
host := args["host"].(string)
count := 4
if c, ok := args["count"].(float64); ok {
count = int(c)
}
// Выполняем ping...
return fmt.Sprintf("Пропинговали %s %d раз", host, count), nil
}
Пример: Использование github.com/invopop/jsonschema для генерации схем:
import (
"encoding/json"
"github.com/invopop/jsonschema"
)
// Определяем параметры инструмента как Go структуру
type PingParams struct {
Host string `json:"host" jsonschema:"required,title=Host,description=Hostname or IP address to ping"`
Count int `json:"count" jsonschema:"default=4,minimum=1,maximum=10,title=Count,description=Number of ping packets"`
}
// Генерируем JSON Schema из структуры
func generateToolSchema(params any) (json.RawMessage, error) {
reflector := jsonschema.Reflector{
ExpandedStruct: true,
DoNotReference: false,
}
schema := reflector.Reflect(params)
schemaJSON, err := json.Marshal(schema)
if err != nil {
return nil, fmt.Errorf("не удалось замаршалить схему: %w", err)
}
return json.RawMessage(schemaJSON), nil
}
// Использование: Генерируем схему для инструмента
func registerPingTool() {
params := PingParams{}
schema, err := generateToolSchema(params)
if err != nil {
panic(err)
}
tool := Tool{
Name: "ping",
Description: "Ping a host to check connectivity",
Schema: schema, // Используем сгенерированную схему вместо сырого JSON
}
registry.Register("ping", tool)
}
Преимущества использования пакетов JSON Schema:
- Типобезопасность — Генерируйте схемы из Go структур
- Валидация — Валидируйте аргументы перед выполнением инструмента
- Сообщения об ошибках — Понятные ошибки валидации
- Поддерживаемость — Единый источник истины (Go структура)
- Документация — Автоматически генерируемые описания схем
Общие паттерны во фреймворках¶
Большинство фреймворков реализуют похожие паттерны:
Паттерн 1: Tool Registry¶
// Абстрактный интерфейс (работает с любым фреймворком или кастомом)
type ToolRegistry interface {
Register(name string, tool Tool) error
Get(name string) (Tool, error)
List() []string
}
// Фреймворк может предоставить:
type FrameworkToolRegistry struct {
tools map[string]Tool
}
func (r *FrameworkToolRegistry) Register(name string, tool Tool) error {
r.tools[name] = tool
return nil
}
Суть: Определяйте свои интерфейсы. Фреймворк становится деталью реализации.
Паттерн 2: Agent Loop¶
// Абстрактный интерфейс agent loop
type AgentLoop interface {
Run(ctx context.Context, input string) (string, error)
AddTool(tool Tool) error
SetMemory(memory Memory) error
}
// Ваш код использует интерфейс, а не фреймворк напрямую
func processRequest(agent AgentLoop, userInput string) (string, error) {
return agent.Run(context.Background(), userInput)
}
Суть: Dependency injection позволяет менять реализации.
Паттерн 3: Memory Abstraction¶
// Абстрактный интерфейс памяти
type Memory interface {
Store(key string, value any) error
Retrieve(key string) (any, error)
Search(query string) ([]any, error)
}
// Память фреймворка реализует ваш интерфейс
type FrameworkMemory struct {
// Реализация, специфичная для фреймворка
}
func (m *FrameworkMemory) Store(key string, value any) error {
// Адаптируем API фреймворка к вашему интерфейсу
}
Суть: Ваши интерфейсы определяют контракт. Фреймворки предоставляют реализации.
Фреймворки и экосистема¶
Обзор реальных фреймворков¶
На практике большинство команд выбирает один из популярных фреймворков. Вот основные игроки:
LangGraph (Python, LangChain). Фреймворк для создания агентов на основе графов состояний. Каждый шаг агента — узел графа, переходы определяются условиями. Подходит для сложных workflow с ветвлениями и циклами.
CrewAI (Python). Фреймворк для мульти-агентных систем. Агенты объединяются в «команды» (crews) с ролями и задачами. Удобен для сценариев, где несколько агентов сотрудничают над общей целью.
AutoGen (Microsoft, Python). Фреймворк для мульти-агентных систем с упором на диалог между агентами. Агенты общаются через сообщения, поддерживает human-in-the-loop.
Semantic Kernel (Microsoft, .NET/Python). Фреймворк-оркестратор, который интегрирует LLM с существующим кодом через «плагины». Ориентирован на enterprise-сценарии. Поддерживает .NET и Python.
Сравнительная таблица¶
| Фреймворк | Язык | Сильные стороны | Слабые стороны |
|---|---|---|---|
| LangGraph | Python | Граф-based workflow, гибкие состояния, стриминг | Сложный API, крутая кривая обучения |
| CrewAI | Python | Простая мульти-агентная координация, роли | Менее гибкий для нестандартных паттернов |
| AutoGen | Python | Диалоговые мульти-агентные системы, Microsoft backing | Тяжеловесный, сложная отладка |
| Semantic Kernel | .NET, Python | Enterprise-ready, интеграция с Azure | Ориентирован на экосистему Microsoft |
Экосистема MCP¶
MCP (Model Context Protocol) — открытый протокол для подключения инструментов к LLM. Экосистема MCP активно растёт: появляются каталоги MCP-серверов для баз данных, файловых систем, API, браузеров и других интеграций.
Преимущество MCP — один протокол для всех инструментов. Агент подключает MCP-сервер и получает доступ к его инструментам без написания кастомного кода интеграции. Подробнее — в Главе 18: Протоколы Инструментов и Tool Servers.
Экосистема A2A¶
A2A (Agent-to-Agent) — протокол от Google для взаимодействия агентов друг с другом. Каждый агент публикует «карточку» (Agent Card) с описанием своих возможностей. Другие агенты находят его и отправляют задачи через стандартный HTTP API.
A2A решает проблему интероперабельности: агенты от разных команд и на разных фреймворках взаимодействуют через единый протокол. Подробнее — в Главе 18: Протоколы Инструментов и Tool Servers.
Почему этот курс учит с нуля¶
Этот курс строит агента с нуля по нескольким причинам:
-
Понимание фундамента. Фреймворк скрывает детали за абстракциями. Когда что-то ломается, вы не знаете, где искать. Написав agent loop, tool registry, memory store своими руками, вы понимаете каждый компонент.
-
Осознанный выбор. После реализации с нуля вы точно знаете, какие проблемы решает фреймворк. Решение «использовать LangGraph» или «написать кастом» становится обоснованным, а не случайным.
-
Портабельность знаний. Фреймворки меняются. Знание принципов (agent loop, Function Calling, context management) переносится на любой фреймворк. Знание конкретного API — нет.
-
Go как явный язык. Большинство фреймворков написаны на Python. Этот курс использует Go, что заставляет реализовывать паттерны явно — без «магии» декораторов и метапрограммирования.
Реальный опыт подтверждает этот выбор. Паттерны из production-агентов — блочная память с compact/recall, прогрессивное 4-уровневое сжатие контекста, SHA256 loop detection, модульная система промптов с тегами и приоритетами — это решения, которые возникают из конкретных проблем. Ни один фреймворк не предоставляет их из коробки. Понимание этих паттернов на уровне кода — главное преимущество custom runtime.
Case Study: Atlas vs Crush¶
Два реальных агента на Go, оба построены на собственных фреймворках (Crush — на Fantasy от Charmbracelet, Atlas — на своём pkg/agent), а не на сторонних LangGraph/CrewAI. Сравнение показывает, какие решения критичны в production:
| Аспект | Crush (Charmbracelet) | Atlas |
|---|---|---|
| Working Memory | Нет | TaskContext + LivePlan + Budget |
| План | Todos (UI-чеклист, теряется при summarize) | LivePlan в Go-структуре (переживает condense) |
| Контекст | Binary summarization (всё или summary) | 4-уровневое прогрессивное сжатие |
| Loop detection | SHA256 сигнатуры (agent-level) | Repetition/pattern/entropy (provider-level) |
| Tool descriptions | .md файлы, не дублируются | Дублируются в system prompt + definitions |
| Тестирование | VCR (записанные ответы) | Нет |
| Persistence | SQLite | JSON files |
| Субагенты | 1 тип (coder) | 6 типизированных ролей |
| Budget | Post-step (StopWhen) | Proactive (перед API-вызовом) |
| LSP | gopls, typescript-ls | Нет |
Ключевые уроки¶
-
Working Memory критична. Без неё агент теряет план и контекст прочитанных файлов между REPL-циклами.
-
План = program state, не messages. Хранение плана в Go-структуре позволяет ему пережить condensation. Хранение в messages — теряет план при суммаризации.
-
Gradual degradation > hard cut. 4-уровневое прогрессивное сжатие (trim → compact → condense → eviction) лучше, чем бинарная суммаризация "всё или ничего".
-
VCR-тесты для агентов. Записывание ответов LLM и их воспроизведение в тестах — единственный надёжный способ тестировать агентный цикл. Без этого — только manual testing.
-
SHA256 loop detection — дёшево и надёжно. Хеш от
(tool + input + output)ловит циклы без LLM-вызовов.
Стратегия¶
Оба агента построены на собственных фреймворках, заточенных под проект. Это подтверждает подход курса: понимание внутренностей позволяет создать свой фреймворк с нужными абстракциями — а не подстраиваться под чужие. Каждый из паттернов выше (Working Memory, LivePlan, progressive compression, SHA256 loop detection, VCR testing) возник из конкретных потребностей проекта. Сторонние фреймворки таких абстракций не предоставляют.
Типовые ошибки¶
Ошибка 1: Vendor Lock-In¶
Симптом: Ваш код тесно связан с API фреймворка. Смена фреймворка требует переписывания всего.
Причина: Использование типов фреймворка напрямую вместо определения своих интерфейсов.
Решение:
// ПЛОХО: Прямая зависимость от фреймворка
func processRequest(frameworkAgent *FrameworkAgent) {
result := frameworkAgent.Execute(userInput)
}
// ХОРОШО: На основе интерфейсов
type Agent interface {
Execute(input string) (string, error)
}
func processRequest(agent Agent, userInput string) (string, error) {
return agent.Execute(userInput)
}
// Адаптер фреймворка реализует ваш интерфейс
type FrameworkAdapter struct {
agent *FrameworkAgent
}
func (a *FrameworkAdapter) Execute(input string) (string, error) {
return a.agent.Execute(input)
}
Ошибка 2: Излишняя инженерия кастомного Runtime¶
Симптом: Вы тратите месяцы на построение функций, которые фреймворки предоставляют из коробки.
Причина: Не оцениваете, действительно ли нужна кастомная реализация.
Решение: Начните с фреймворка для прототипирования. Извлекайте в кастом только когда натыкаетесь на реальные ограничения.
Ошибка 3: Игнорирование ограничений фреймворка¶
Симптом: Вы постоянно боретесь с фреймворком, пытаясь заставить его делать то, для чего он не был предназначен.
Причина: Не понимаете дизайнерские решения и ограничения фреймворка.
Решение: Внимательно читайте документацию фреймворка. Если ограничения слишком сильные, рассмотрите кастомный runtime.
Ошибка 4: Нет пути миграции¶
Симптом: Вы заперты с фреймворком, даже когда он больше не подходит вашим нуждам.
Причина: Тесная связь делает миграцию невозможной без переписывания всего.
Решение: Проектируйте с интерфейсами с самого начала. Держите фреймворк как деталь реализации, а не основную зависимость.
Мини-упражнения¶
Упражнение 1: Определите интерфейс Tool с JSON Schema¶
Создайте абстрактный интерфейс Tool, который работает независимо от любого фреймворка, используя JSON Schema для валидации:
import (
"context"
"encoding/json"
"github.com/invopop/jsonschema"
"github.com/xeipuuv/gojsonschema"
)
type Tool interface {
Name() string
Description() string
Execute(ctx context.Context, args map[string]any) (any, error)
Schema() json.RawMessage
ValidateArgs(args map[string]any) error
}
// Пример реализации с валидацией JSON Schema
type PingTool struct {
schema json.RawMessage
}
func (t *PingTool) Name() string {
return "ping"
}
func (t *PingTool) Description() string {
return "Ping a host to check connectivity"
}
func (t *PingTool) Schema() json.RawMessage {
return t.schema
}
func (t *PingTool) ValidateArgs(args map[string]any) error {
// Используем gojsonschema для валидации
schemaLoader := gojsonschema.NewBytesLoader(t.schema)
documentLoader := gojsonschema.NewGoLoader(args)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
return err
}
if !result.Valid() {
return fmt.Errorf("валидация не прошла: %v", result.Errors())
}
return nil
}
func (t *PingTool) Execute(ctx context.Context, args map[string]any) (any, error) {
// Валидируем перед выполнением
if err := t.ValidateArgs(args); err != nil {
return nil, err
}
// Выполняем инструмент...
return "pong", nil
}
Ожидаемый результат:
- Интерфейс не зависит от фреймворка
- Может быть реализован любым адаптером фреймворка
- Предоставляет всю необходимую информацию для выполнения инструмента
- Включает валидацию JSON Schema
Упражнение 2: Адаптер фреймворка¶
Создайте адаптер, который оборачивает систему инструментов фреймворка для реализации вашего интерфейса Tool:
type FrameworkToolAdapter struct {
frameworkTool FrameworkTool
}
func (a *FrameworkToolAdapter) Name() string {
// Адаптируем имя инструмента фреймворка
}
func (a *FrameworkToolAdapter) Execute(ctx context.Context, args map[string]any) (any, error) {
// Конвертируем ваш интерфейс в API фреймворка
}
Ожидаемый результат:
- Фреймворк становится деталью реализации
- Ваш код использует ваши интерфейсы
- Легко менять фреймворки позже
Упражнение 3: Матрица решений¶
Создайте матрицу решений для выбора между кастомным runtime и фреймворком:
| Критерий | Кастомный Runtime | Фреймворк |
|---|---|---|
| Скорость разработки | ? | ? |
| Гибкость | ? | ? |
| Бремя поддержки | ? | ? |
| Риск vendor lock-in | ? | ? |
Заполните матрицу на основе ваших конкретных требований.
Ожидаемый результат:
- Чёткое понимание компромиссов
- Обоснованное решение для вашего use case
Критерии сдачи / Чек-лист¶
Сдано:
- Понимаете, когда использовать фреймворки vs кастомный runtime
- Знаете, как избежать vendor lock-in через интерфейсы
- Можете оценить фреймворки против ваших требований
- Понимаете общие паттерны во фреймворках
- Знаете реальные различия между production-агентами (Atlas vs Crush)
Не сдано:
- Выбор фреймворка без оценки требований
- Тесная связь с API фреймворка
- Нет пути миграции, если фреймворк не подходит
- Игнорирование ограничений фреймворка
Связь с другими главами¶
- Глава 09: Анатомия Агента — Понимание компонентов агента помогает оценить фреймворки
- Глава 03: Инструменты и Function Calling — Интерфейсы инструментов — ключ к портабельности
- Глава 10: Planning и Workflow-паттерны — Фреймворки часто предоставляют паттерны планирования
- Глава 18: Протоколы Инструментов и Tool Servers — Стандартные протоколы уменьшают vendor lock-in
- Agent Skills — Открытый формат навыков агента (
SKILL.md), поддерживаемый Cursor, Claude Code, VS Code и др. См. Главу 09: Анатомия Агента
Что дальше?¶
После понимания экосистемы переходите к:
- 15. Кейсы из Реальной Практики — Изучите примеры реальных агентов