Перейти к содержанию

06. RAG и База Знаний

Зачем это нужно?

Обычный агент знает только то, чему его научили при тренировке (до даты cut-off). Ваши локальные инструкции вроде "как перезагружать сервер Phoenix по регламенту №5" он сам по себе не знает.

RAG (Retrieval Augmented Generation) — это способ "подглядеть в шпаргалку". Агент сначала находит нужный кусок в базе знаний, а уже потом действует.

Без RAG агент не сможет опираться на вашу документацию, регламенты и базу знаний. С RAG — сможет: найдёт нужное и выполнит шаги по вашим правилам.

Реальный кейс

Ситуация: Пользователь пишет: "Перезагрузи сервер Phoenix согласно регламенту"

Проблема: Агент не знает регламента перезагрузки сервера Phoenix. Он может выполнить стандартную перезагрузку, которая не соответствует вашим процедурам.

Решение: С RAG агент сначала находит регламент в базе знаний и только потом действует. Он достаёт документ "Регламент перезагрузки сервера Phoenix: 1. Выключить балансировщик 2. Перезагрузить сервер 3. Включить балансировщик" и следует шагам.

Теория простыми словами

Как работает RAG?

  1. Агент получает запрос от пользователя
  2. Агент ищет информацию в базе знаний через инструмент поиска
  3. База знаний возвращает релевантные документы
  4. Агент использует информацию для выполнения действия

Как работает RAG? — Магия vs Реальность

Магия:

Агент "знает", что нужно поискать в базе знаний и сам находит нужную информацию

Реальность:

Полный протокол RAG

Шаг 1: Запрос пользователя

messages := []Message{
    {Role: "system", Content: "You are a DevOps assistant. Internal procedures and protocols live in the knowledge base."},
    {Role: "user", Content: "Перезагрузи сервер Phoenix согласно регламенту"},
}

Шаг 2: Модель видит описание инструмента поиска

tools := []ToolDef{
    {
        Name:        "search_knowledge_base",
        Description: "Search the knowledge base for documentation, protocols, and procedures.",
        Parameters: json.RawMessage(`{
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search query"}
            },
            "required": ["query"]
        }`),
    },
    {
        Name:        "restart_server",
        Description: "Restart a server",
        Parameters: json.RawMessage(`{
            "type": "object",
            "properties": {
                "hostname": {"type": "string"}
            },
            "required": ["hostname"]
        }`),
    },
}

Шаг 3: Модель генерирует tool call для поиска

client, err := NewClient(...) // провайдер из конфига (гл. 04)

resp1, err := client.Complete(ctx, Request{
    Model:       "glm-5.3",
    Messages:    messages,
    Tools:       tools,
    Temperature: 0.2, // качество: меньше разброс в выборе инструментов
}) // ошибка обрабатывается циклом (гл. 04)

msg1 := resp1.Choices[0].Message
// msg1.ToolCalls = [{
//     ID: "call_1",
//     Name: "search_knowledge_base",
//     Arguments: "{\"query\": \"Phoenix restart protocol\"}"
// }]

Почему модель сгенерировала tool_call на поиск?

  • Модель сама решила, что для слова «регламент» ей нужны внутренние инструкции, а не общие знания
  • Description инструмента говорит, где искать документацию и процедуры
  • Модель видит слово "регламенту" в запросе и связывает это с инструментом поиска

Здесь нет принуждающей строки в промпте вида «Always search knowledge base before actions»: поиск — обычный tool call, и решение «искать или нет» принимает модель по описанию инструмента. Если в вашей задаче поиск обязателен всегда — это инвариант runtime (код не выполняет действие без поиска), а не строка в system prompt.

Шаг 4: Runtime (ваш код) выполняет поиск

Примечание: Runtime — это код агента, который вы пишете на Go. См. Главу 00: Предисловие для определения.

func searchKnowledgeBase(query string) string {
    // Простой поиск по ключевым словам (в продакшене - векторный поиск)
    knowledgeBase := map[string]string{
        "protocols.txt": "Регламент перезагрузки сервера Phoenix:\n1. Выключить балансировщик\n2. Перезагрузить сервер\n3. Включить балансировщик",
    }

    for filename, content := range knowledgeBase {
        if strings.Contains(strings.ToLower(content), strings.ToLower(query)) {
            return fmt.Sprintf("File: %s\nContent: %s", filename, content)
        }
    }
    return "No documents found"
}

result1 := searchKnowledgeBase("Phoenix restart protocol")
// result1 = "File: protocols.txt\nContent: Регламент перезагрузки сервера Phoenix:\n1. Выключить балансировщик..."

Шаг 5: Результат поиска добавляется в контекст

messages = append(messages, Message{
    Role:       "tool",
    Content:    result1,  // Вся найденная документация!
    ToolCallID: msg1.ToolCalls[0].ID,
})
// Теперь messages содержит:
// [system, user, assistant(tool_call: search_kb), tool("File: protocols.txt\nContent: ...")]

Шаг 6: Модель видит документацию и действует

// Отправляем обновленную историю (с документацией!) в модель
resp2, err := client.Complete(ctx, Request{
    Model:    "glm-5.3",
    Messages: messages,  // Модель видит найденную документацию!
    Tools:    tools,
}) // ошибка обрабатывается циклом (гл. 04)

msg2 := resp2.Choices[0].Message
// Модель видит в контексте:
// "Регламент перезагрузки сервера Phoenix:\n1. Выключить балансировщик..."
// Модель генерирует tool calls согласно регламенту:

// msg2.ToolCalls = [
//     {ID: "call_1", Name: "restart_server", Arguments: "{\"hostname\": \"phoenix\"}"},
//     // Или сначала выключить балансировщик, потом сервер
// ]

Что происходит на деле:

  1. Модель не "знает" регламент — она видит его в контексте после поиска
  2. Поиск — это обычный tool — такой же, как ping или restart_service
  3. Результат поиска добавляется в messages[] — модель видит его как новое сообщение
  4. Модель генерирует действия на основе контекста — она видит документацию и следует ей

Суть: RAG — это не "знание из воздуха", а способ добавить релевантную информацию в контекст модели через обычный tool call.

Простой RAG vs Векторный поиск

Простой RAG (поиск по ключевым словам) реализуется в Lab 07, переведена на нейтральный клиент. В продакшене используется векторный поиск (Semantic Search), который ищет по смыслу, а не по словам.

Простой RAG (Lab 07):

// Поиск по вхождению подстроки
if strings.Contains(content, query) {
    return content
}

Векторный поиск (продакшен):

// 1. Документы разбиваются на чанки и преобразуются в векторы
// embedding-моделью провайдера (размерность d зависит от модели)
chunks := []Chunk{
    {ID: "chunk_1", Text: "Регламент перезагрузки Phoenix...", Embedding: embed("Регламент перезагрузки Phoenix...")},
    {ID: "chunk_2", Text: "Шаг 2: Выключить балансировщик...", Embedding: embed("Шаг 2: Выключить балансировщик...")},
}

// 2. Запрос пользователя тоже преобразуется в вектор
queryEmbedding := embed("Phoenix restart protocol")

// 3. Поиск похожих векторов по косинусному расстоянию
similarDocs := vectorDB.Search(queryEmbedding, topK=3)
// Возвращает 3 наиболее похожих чанка по смыслу (не по словам!)

// 4. Результат добавляется в контекст модели так же, как в простом RAG
result := formatChunks(similarDocs)  // "Chunk 1: ...\nChunk 2: ...\nChunk 3: ..."
messages = append(messages, Message{
    Role:    "tool",
    Content: result,
})

Почему векторный поиск лучше:

  • Ищет по смыслу, а не по словам
  • Найдет "restart Phoenix" даже если в документе написано "перезагрузка сервера Phoenix"
  • Работает с синонимами и разными формулировками

Чанкинг (Chunking)

Документы разбиваются на чанки (куски) для эффективного поиска.

Пример:

Документ: "Регламент перезагрузки сервера Phoenix..."
Чанк 1: "Регламент перезагрузки сервера Phoenix: шаг 1..."
Чанк 2: "Шаг 2: Выключить балансировщик..."
Чанк 3: "Шаг 3: Перезагрузить сервер..."

Продвинутые техники RAG

Базовый RAG работает так: запрос пользователя → поиск → найденные документы → ответ. Но на практике этого недостаточно. Запрос может быть нечётким, поиск — неточным, а результаты — нерелевантными.

Рассмотрим техники, которые решают эти проблемы.

Эволюция RAG

Basic RAG          Advanced RAG           Agentic RAG
┌──────────┐       ┌──────────────┐       ┌─────────────────┐
│ Query    │       │ Query        │       │ Agent решает:   │
│   ↓      │       │   ↓          │       │ - Искать или нет│
│ Search   │       │ Transform    │       │ - Где искать    │
│   ↓      │       │   ↓          │       │ - Достаточно ли │
│ Retrieve │       │ Route        │       │ - Искать ещё    │
│   ↓      │       │   ↓          │       │   ↓             │
│ Generate │       │ Hybrid Search│       │ Итеративный     │
│          │       │   ↓          │       │ поиск в цикле   │
│          │       │ Rerank       │       │                 │
│          │       │   ↓          │       │                 │
│          │       │ Generate     │       │                 │
└──────────┘       └──────────────┘       └─────────────────┘

Пред- и пост-обработка запроса: когда она оправдана

Между запросом пользователя и поиском (и между поиском и ответом) можно вставить дополнительные LLM-вызовы: переформулировать запрос, сгенерировать гипотетический документ, выбрать источник, оценить релевантность найденного. Это рабочие приёмы — но у каждого один и тот же интерфейс цены: лишний LLM-вызов до или после поиска. Он добавляет латентность и токены в каждом запросе, даже когда базовый поиск и так справился.

Приём Что делает Когда оправдан
Query Rewriting LLM переформулирует запрос под язык документов: «сервер лежит» → «процедура восстановления сервера после отказа» Запросы пользователей разговорные, документы канцелярские; поиск часто промахивается
Sub-Query Decomposition Сложный запрос разбивается на несколько простых: «сравни настройки nginx на prod и staging» → два независимых поиска Запросы агрегируют несколько независимых вопросов
HyDE (Hypothetical Document Embeddings) LLM генерирует гипотетический ответ, и поиск идёт по его embedding: embedding документа ближе к embedding другого документа, чем embedding короткого запроса Короткие запросы к длинным каноническим документам; редкая терминология
Routing Классификация запроса и выбор источника: вики, SQL-база, API метрик Несколько по-настоящему разных источников, которые нельзя опросить все
Self-RAG Модель оценивает релевантность и достаточность найденного: ответить / уточнить запрос / искать ещё Шумный корпус, где промах поиска стоит дороже дополнительной итерации

Общее правило: включайте приём, когда он даёт дешёвую победу именно на вашем корпусе (проверьте на evals — гл. 23), а не «потому что так делают в статьях». Порядок эскалации: сначала чините сам поиск (чанкинг, hybrid, reranking ниже), и только потом добавляйте LLM-обвязку вокруг него.

В agentic-режиме (раздел ниже) часть этих приёмов достаётся бесплатно: модель сама переформулирует запрос, сама выбирает источник и сама решает, искать ли ещё — потому что поиск для неё обычный tool call, а не зашитый в код шаг пайплайна.

Hybrid Search (Гибридный поиск)

Проблема: Векторный поиск хорошо ищет по смыслу, но плохо по точным терминам. Keyword-поиск — наоборот. Запрос "ошибка ORA-12154" нужно искать и по ключевому слову "ORA-12154", и по смыслу "ошибка подключения к Oracle".

Решение: Комбинируем оба подхода и объединяем результаты.

type SearchResult struct {
    ChunkID string
    Text    string
    Score   float64
}

func hybridSearch(query string, topK int) []SearchResult {
    // 1. Keyword search (BM25)
    keywordResults := bm25Search(query, topK)

    // 2. Vector search (Semantic)
    embedding := embedQuery(query)
    vectorResults := vectorDB.Search(embedding, topK)

    // 3. Reciprocal Rank Fusion (RRF) — объединение результатов
    return reciprocalRankFusion(keywordResults, vectorResults, topK)
}

// RRF: комбинирует ранги из разных списков
func reciprocalRankFusion(lists ...[]SearchResult) []SearchResult {
    scores := make(map[string]float64) // chunkID → combined score
    k := 60.0 // Константа RRF (стандартное значение)

    for _, list := range lists {
        for rank, result := range list {
            scores[result.ChunkID] += 1.0 / (k + float64(rank+1))
        }
    }

    // Сортируем по combined score (убывание)
    // ... сортировка и возврат top-K ...
}

Когда что работает лучше:

Тип запроса Keyword Vector Hybrid
"ORA-12154" Отлично Плохо Отлично
"не могу подключиться к базе" Плохо Отлично Отлично
"ORA-12154 не подключается" Средне Средне Отлично

Reranking (Переранжирование)

Проблема: Поиск вернул 20 результатов. Не все одинаково релевантны. Передавать все 20 в контекст — расход токенов.

Решение: После первичного поиска переранжируем результаты с помощью более точной (но медленной) модели.

func rerankResults(query string, results []SearchResult, topK int) []SearchResult {
    // Используем LLM для оценки релевантности каждого результата
    type scored struct {
        Result SearchResult
        Score  float64
    }
    var scored []scored

    for _, r := range results {
        score := scoreRelevance(query, r.Text) // cross-encoder или LLM
        scored = append(scored, scored{Result: r, Score: score})
    }

    // Сортируем по score (убывание)
    sort.Slice(scored, func(i, j int) bool {
        return scored[i].Score > scored[j].Score
    })

    // Возвращаем top-K
    result := make([]SearchResult, 0, topK)
    for i := 0; i < topK && i < len(scored); i++ {
        result = append(result, scored[i].Result)
    }
    return result
}

Двухэтапный пайплайн: Быстрый поиск (BM25/vector, top-100) → Точный reranking (cross-encoder, top-5).

Agentic RAG (RAG как инструмент агента)

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

Решение: RAG встраивается в Agent Loop (он же ReAct Loop — см. Главу 04). Агент сам решает, когда искать, где искать и достаточно ли информации.

// Agentic RAG: RAG — это просто инструменты агента
tools := []ToolDef{
    // Инструмент 1: Поиск в документации
    {
        Name:        "search_docs",
        Description: "Search documentation and runbooks. Use when you need procedures or technical details.",
        Parameters:  searchParamsSchema,
    },
    // Инструмент 2: SQL-запрос к базе метрик
    {
        Name:        "query_metrics",
        Description: "Query metrics database. Use when you need historical data or statistics.",
        Parameters:  sqlParamsSchema,
    },
    // Инструмент 3: Поиск похожих инцидентов
    {
        Name:        "search_incidents",
        Description: "Search past incidents for similar issues and their resolutions.",
        Parameters:  searchParamsSchema,
    },
}

// Агент сам решает:
// Итерация 1: search_docs("nginx 502 error troubleshooting")
// Итерация 2: query_metrics("SELECT avg(latency) FROM requests WHERE status=502 AND time > now()-1h")
// Итерация 3: search_incidents("nginx 502 upstream timeout")
// Итерация 4: Финальный ответ с объединением информации из всех источников

Multi-hop RAG (цепочка поисков):

Запрос: "Почему вчера упал сервис payments?"

Шаг 1: search_incidents("payments service outage yesterday")
       → "Инцидент INC-4521: payments упал из-за таймаута к БД"

Шаг 2: search_docs("payments database connection configuration")
       → "Payments использует PostgreSQL на db-prod-03, connection pool = 20"

Шаг 3: query_metrics("SELECT connections FROM pg_stat WHERE time = yesterday")
       → "Пик подключений: 150 (при лимите 100)"

Шаг 4: Финальный ответ: "Payments упал из-за исчерпания connection pool.
        Пик — 150 подключений при лимите 100. Рекомендация: увеличить pool до 200."

Разница между подходами:

Подход Кто принимает решения Где логика
Basic RAG Жёсткий pipeline Код (hardcoded)
Advanced RAG Pipeline с ветвлениями Код + конфиг
Self-RAG Модель оценивает качество Модель (assessment)
Agentic RAG Агент управляет всем Agent Loop

Много инструментов ≠ нужен их каталог

До сих пор мы говорили о RAG для документов (регламенты, инструкции). Частая следующая мысль: «а не применить ли RAG к пространству действий» — у агента ведь потенциально бесконечное количество команд и инструментов. Разберём, почему каталог инструментов с retrieval по нему — почти всегда over-engineering, и какая форма правильная.

Проблема: "Бесконечные" команды

Ситуация: Агент должен работать с Linux-командами для траблшутинга. В Linux тысячи команд (grep, awk, sed, jq, sort, uniq, head, tail и т.д.), и они комбинируются в пайплайны.

Проблема:

  • Нельзя передать все команды в tools[] — это тысячи токенов в каждом запросе
  • Модель хуже выбирает из большого списка (больше галлюцинаций)
  • Нет контроля безопасности (какие команды опасны?)

Наивное решение (не работает): один универсальный инструмент run_shell(command) без ограничений. Нет валидации (можно выполнить rm -rf /), нет аудита (непонятно, какие команды выполнялись), нет контроля.

Антипаттерн: каталог tool-обёрток и Tool RAG над shell-командами

Первая идея: сделать каталог инструментов — по обёртке на каждую команду (grep, sort, head...) с описаниями и тегами — и перед планированием извлекать релевантные. Это называется Tool RAG (Action-Space Retrieval), и для shell-команд это over-engineering:

  • Команда уже умеет быть найденной. Знание «для фильтрации логов нужен grep | sort | head» живёт в документации (README, скил) и в знаниях самой модели — а не в каталоге схем. Retrieval по описаниям команд решает проблему, которой не существует.
  • Retrieval не проверяет корректность. Поиск по каталогу вернёт команды, «похожие по описанию», но не гарантирует, что команда подходит и её аргументы корректны. За корректность всё равно отвечает runtime (валидация, allowlist) — система безопасности не меняется, а retrieval-слой добавляется сверху как вторая подсистема, которую нужно синхронизировать с реальными командами и прогонять через evals.
  • Каталог из 20+ обёрток над одной системой (shell, kubectl, docker) — это не «богатое пространство действий», а дублирование одного инструмента exec двадцатью схемами, каждая из которых платит за себя в каждом запросе.

Отдельный вид этого антипаттерна — JSON DSL-пайплайны: модель генерирует JSON-план из шагов (grep → sort → head), а runtime его исполняет. Внешне «надёжнее», на деле — второй язык описания действий, который нужно валидировать, отлаживать и поддерживать, вместо обычных tool calls, которые модель и так генерирует хорошо.

Правильная форма: скилы + exec с allowlist

Когда действий много, их описание выносят из tools[] в файлы, а исполнение — в один инструмент exec с allowlist. Это механизм скилов (гл. 09): по одному SKILL.md на область знаний + progressive disclosure.

  1. Discovery. При старте runtime читает все SKILL.md и добавляет в system prompt одну строку на скил: имя + description. 10 скилов ≈ 10 строк — это вся цена обнаружения.
  2. Activation. Когда модель решает, что навык нужен, она читает тело SKILL.md сама — обычным инструментом read. Текст входит в историю как tool_result.
  3. Execution. Модель действует по инструкциям скила через exec — с allowlist команд и подтверждением опасных операций.

Пример: скил «k8s-debug»

skills/k8s-debug/SKILL.md
---
name: k8s-debug
description: Debug Kubernetes pods — check status, logs, describe.
  Use when user reports pod crashes or restart loops.
---
1. kubectl get pods -n <namespace> — найти проблемный под
2. kubectl describe pod <pod> -n <namespace> — события и причины рестартов
3. kubectl logs <pod> -n <namespace> --tail=100 — последние логи
Опасное (delete, rollout undo) — только через подтверждение.

В system prompt — одна строка k8s-debug: Debug Kubernetes pods.... Дальше модель сама: read("skills/k8s-debug/SKILL.md") → exec("kubectl", "get", "pods", "-n", "prod") → exec("kubectl", "logs", ...). Никакого каталога из сотен tool-обёрток и никакого retrieval по нему — при этом модель видит все шаги диагностики и выполняет их через один контролируемый инструмент:

// exec с allowlist — единственный «исполнитель» команд
func execCommand(command string, args ...string) (string, error) {
    allowed := []string{"kubectl", "grep", "sort", "head", "tail", "jq"} // allowlist, не denylist
    if !slices.Contains(allowed, command) {
        return "", fmt.Errorf("command %q is not in the allowlist", command)
    }
    cmd := exec.Command(command, args...)
    // ... выполнение, логирование для аудита, лимит на размер вывода
    return string(output), nil
}

Сайдбар: как это устроено в реальном SDK. Каталоги инструментов с retrieval — редкость именно потому, что их вытеснили скилы: одна строка на скил в system prompt вместо сотен схем в tools[]. Главный «retrieval»-инструмент агента — read: модель сама читает нужный файл, когда он нужен. Чтение ограничено FSHooks-политикой: какие директории можно читать, что логируется и что попадает в контекст — решает runtime, а не модель.

Если tool'ов реально много: tool retrieval как вынужденная мера

Бывают случаи, когда инструмент — действительно отдельный tool со своей схемой: десятки SaaS-интеграций, сотни эндпоинтов за tool servers (гл. 18). Тогда динамический выбор подмножества инструментов перед запросом (tool retrieval) — оправданная вынужденная мера. Помните цену: блок tools[] отправляется и оплачивается в каждом запросе (гл. 03); retrieval уменьшает блок, но не отменяет каталог, который нужно версионировать и проверять на evals. Учебная реализация — Lab 13.

Типовые ошибки

Ошибка 1: Агент не ищет в базе знаний

Симптом: Агент выполняет действия без поиска в базе знаний, используя только общие знания.

Причина: Описание инструмента поиска не говорит модели, что в нём лежит: непонятно, какие задачи требуют поиска.

Решение: Не принуждайте промптом («Always search knowledge base before actions» — антипаттерн: строка либо игнорируется, либо заставляет модель искать даже там, где не нужно). Дайте модели принять решение самой — через точное описание инструмента:

// ХОРОШО: Описание говорит, ЧТО лежит в базе знаний — модель сама решает, когда это нужно
Description: "Search the internal knowledge base: runbooks, procedures, protocols. Use when the task involves internal systems or company-specific procedures."

// ПЛОХО: Принуждение промптом
systemPrompt := "... Always search knowledge base before performing actions ..."

Если поиск в вашей задаче обязателен всегда (например, действие нельзя выполнять без регламента) — это инвариант runtime, а не строка в промпте: код не выполняет действие, пока в истории нет результата поиска.

Ошибка 2: Плохой поисковый запрос

Симптом: Агент не находит нужную информацию в базе знаний.

Причина: Поисковый запрос слишком общий или не содержит ключевых слов из документа.

Решение:

// ПЛОХО: Слишком общий запрос
query := "server"

// ХОРОШО: Конкретный запрос с ключевыми словами
query := "Phoenix server restart protocol"

Ошибка 3: Чанки слишком большие

Симптом: Поиск возвращает слишком большие документы, которые не влезают в контекст.

Причина: Размер чанков слишком большой (больше контекстного окна).

Решение:

// ХОРОШО: Размер чанка ~500-1000 токенов
chunkSize := 500  // Токенов

Ошибка 4: Каталог из 20+ tool-обёрток над одной системой

Симптом: У агента десятки инструментов grep/sort/head/tail — по обёртке на каждую команду. Блок tools[] огромный, модель путается, каждый запрос дорожает.

Причина: Пространство действий перепутано с пространством инструментов: одна система (shell) обёрнута двадцатью схемами.

Решение: Описание действий вынести в скилы (SKILL.md + progressive disclosure), исполнение — в один exec с allowlist (раздел «Много инструментов ≠ нужен их каталог» выше). Каталог инструментов с retrieval — только если tool'ы реально разные и их много (SaaS-интеграции).

Ошибка 5: Универсальный run_shell без контроля

Симптом: Агент использует один инструмент run_shell(command) для всех команд. Нет валидации, нет аудита.

Причина: Упрощение архитектуры за счет безопасности.

Решение:

// ПЛОХО: Универсальный shell без ограничений
tools := []ToolDef{{
    Name:        "run_shell",
    Description: "Execute any shell command",
}}

// ХОРОШО: exec с allowlist (не denylist!) и подтверждением опасных операций
func execCommand(command string, args ...string) (string, error) {
    allowed := map[string]bool{
        "grep": true, "sort": true, "head": true, "kubectl": true,
        // rm, dd и другие опасные — НЕ в allowlist
    }
    if !allowed[command] {
        return "", fmt.Errorf("command %q is not in the allowlist", command)
    }
    // ... выполнение, логирование для аудита
    return string(output), nil
}

Ошибка 6: Опасные операции без подтверждения

Симптом: Агент выполняет деструктивные команды (rm, kubectl delete) без проверки и подтверждения человеком.

Причина: Валидация «в среднем» не отличается опасные вызовы от безопасных.

Решение: allowlist отвечает на вопрос «разрешено ли вообще», а подтверждение (confirm, гл. 05) — на вопрос «разрешено ли сейчас, с человеком в цикле»:

// ХОРОШО: две разные проверки перед выполнением
if !allowedCommands[cmd.Command] {
    return fmt.Errorf("command %q is not allowed", cmd.Command)
}
if destructive(cmd.Command, cmd.Args) { // delete, drop, rollout undo...
    return confirmWithHuman(cmd) // без подтверждения — не выполняется
}
return run(cmd)

Мини-упражнения

Упражнение 1: Реализуйте простой поиск

Реализуйте функцию простого поиска по ключевым словам:

func searchKnowledgeBase(query string) string {
    // Простой поиск по ключевым словам
    // Верните релевантные документы
}

Ожидаемый результат:

  • Функция находит документы, содержащие ключевые слова из запроса
  • Функция возвращает первые N релевантных документов

Упражнение 2: Реализуйте чанкинг

Реализуйте функцию разбиения документа на чанки:

func chunkDocument(text string, chunkSize int) []string {
    // Разбейте документ на чанки размером chunkSize токенов
    // Верните список чанков
}

Ожидаемый результат:

  • Функция разбивает документ на чанки заданного размера
  • Чанки не перекрываются (или перекрываются минимально)

Упражнение 3: Реализуйте поиск по каталогу скилов

Runtime читает все SKILL.md и должен выбрать релевантные. Реализуйте функцию поиска:

type SkillDef struct {
    Name        string // "k8s-debug"
    Description string // "Debug Kubernetes pods — check status, logs, describe..."
}

func searchSkills(query string, skills []SkillDef, topK int) []SkillDef {
    // Ищите по описаниям
    // Верните top-k наиболее релевантных скилов
}

Ожидаемый результат:

  • Функция находит скилы, релевантные запросу
  • Возвращает не более topK скилов
  • Учитывает описания скилов

Для продвинутых: Реализуйте векторный поиск по описаниям (аналогично векторному поиску документов выше) — полезно, когда скилов десятки. Тот же механизм лежит в основе tool retrieval для действительно больших наборов разных инструментов (Lab 13).

Упражнение 4: Валидация exec-команды

Реализуйте функцию валидации команды перед выполнением:

func validateCommand(command string, args []string, allowed map[string]bool) error {
    // Проверьте, что command есть в allowlist
    // Верните ошибку, если команда не разрешена
}

Ожидаемый результат:

  • Функция возвращает ошибку для команд вне allowlist
  • Функция возвращает nil для разрешенных команд

Критерии сдачи / Чек-лист

Сдано:

  • Агент ищет в базе знаний, когда задача требует внутренних процедур
  • Поисковые запросы конкретные и содержат ключевые слова
  • Документы разбиты на чанки подходящего размера
  • Инструмент поиска имеет четкое описание (что лежит в базе — модель решает сама, без «Always search» в промпте)
  • Большое пространство действий оформлено скилами (SKILL.md + progressive disclosure) и exec с allowlist; tool retrieval — только для реально разных и многочисленных tool'ов
  • Команды проходят allowlist перед выполнением; опасные операции требуют подтверждения
  • Результат поиска укладывается в бюджет tool-результата (~128 KB); чанки хранятся с path/ID, чтобы полный документ можно было вынести в артефакт

Не сдано:

  • Агент не ищет в базе знаний (использует только общие знания)
  • Поисковые запросы слишком общие (не находит нужную информацию)
  • Чанки слишком большие (не влезают в контекст)
  • Каталог из 20+ tool-обёрток над одной системой вместо скилов и exec
  • Используется универсальный run_shell без контроля безопасности
  • Команды выполняются без allowlist; опасные — без подтверждения

Прод-заметки

При использовании RAG в продакшене учитывайте:

  • Версионирование документов: Отслеживайте версии документов и дату обновления (updated_at). Это помогает понять, какая версия использовалась в ответе.
  • Freshness (актуальность): Фильтруйте устаревшие документы (например, старше 30 дней) перед использованием в контексте.
  • Grounding: Требуйте от агента ссылаться на найденные документы в ответе. Это снижает галлюцинации и повышает доверие.
  • Права на чтение: read — главный инструмент доступа к знаниям, и он должен быть ограничен политикой: какие директории можно читать, что логируется и что попадает в контекст, решает runtime (FSHooks-политика), а не модель.
  • Бюджет tool-результата: Результат поиска — это обычный tool-результат: ориентир ~128 KB на результат (гл. 13). Что не влезло — выдержка в контексте + полный документ в артефакте (гл. 11); для этого храните чанки с path/ID.

Подробнее о прод-готовности: Глава 19: Observability, Глава 23: Evals в CI/CD.

Связь с другими главами

  • Инструменты: Как инструмент поиска интегрируется в агента, см. Главу 03: Инструменты. Большое пространство действий оформляется скилами и exec (раздел «Много инструментов ≠ нужен их каталог» выше; скилы — гл. 09).
  • Автономность: Как RAG работает в цикле агента, см. Главу 04: Автономность
  • Tool Servers: Как получать инструменты динамически через tool servers, см. Главу 18: Протоколы Инструментов
  • MCP Resources: MCP предоставляет стандартный механизм для доступа к данным (Resources) — Model Context Protocol

Что дальше?

После изучения RAG переходите к: