06. RAG и База Знаний¶
Зачем это нужно?¶
Обычный агент знает только то, чему его научили при тренировке (до даты cut-off). Ваши локальные инструкции вроде "как перезагружать сервер Phoenix по регламенту №5" он сам по себе не знает.
RAG (Retrieval Augmented Generation) — это способ "подглядеть в шпаргалку". Агент сначала находит нужный кусок в базе знаний, а уже потом действует.
Без RAG агент не сможет опираться на вашу документацию, регламенты и базу знаний. С RAG — сможет: найдёт нужное и выполнит шаги по вашим правилам.
Реальный кейс¶
Ситуация: Пользователь пишет: "Перезагрузи сервер Phoenix согласно регламенту"
Проблема: Агент не знает регламента перезагрузки сервера Phoenix. Он может выполнить стандартную перезагрузку, которая не соответствует вашим процедурам.
Решение: С RAG агент сначала находит регламент в базе знаний и только потом действует. Он достаёт документ "Регламент перезагрузки сервера Phoenix: 1. Выключить балансировщик 2. Перезагрузить сервер 3. Включить балансировщик" и следует шагам.
Теория простыми словами¶
Как работает RAG?¶
- Агент получает запрос от пользователя
- Агент ищет информацию в базе знаний через инструмент поиска
- База знаний возвращает релевантные документы
- Агент использует информацию для выполнения действия
Как работает 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\"}"},
// // Или сначала выключить балансировщик, потом сервер
// ]
Что происходит на деле:
- Модель не "знает" регламент — она видит его в контексте после поиска
- Поиск — это обычный tool — такой же, как
pingилиrestart_service - Результат поиска добавляется в
messages[]— модель видит его как новое сообщение - Модель генерирует действия на основе контекста — она видит документацию и следует ей
Суть: RAG — это не "знание из воздуха", а способ добавить релевантную информацию в контекст модели через обычный tool call.
Простой RAG vs Векторный поиск¶
Простой RAG (поиск по ключевым словам) реализуется в Lab 07, переведена на нейтральный клиент. В продакшене используется векторный поиск (Semantic Search), который ищет по смыслу, а не по словам.
Простой RAG (Lab 07):
Векторный поиск (продакшен):
// 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.
- Discovery. При старте runtime читает все
SKILL.mdи добавляет в system prompt одну строку на скил: имя +description. 10 скилов ≈ 10 строк — это вся цена обнаружения. - Activation. Когда модель решает, что навык нужен, она читает тело
SKILL.mdсама — обычным инструментомread. Текст входит в историю как tool_result. - 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: Чанки слишком большие¶
Симптом: Поиск возвращает слишком большие документы, которые не влезают в контекст.
Причина: Размер чанков слишком большой (больше контекстного окна).
Решение:
Ошибка 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 переходите к:
- 07. Multi-Agent Systems — как создать команду агентов