{
  "schema": "aka-gst.course/1",
  "id": "llm-service-lab",
  "stage": 3,
  "language": "ru",
  "title": "Настройка LLM и AI-агентов под задачу",
  "subtitle": "Ollama · structured output · инструменты · память · RAG · evaluation · доставка клиенту",
  "generated_at": "2026-08-28",
  "source": {
    "repository": "https://github.com/aka-gst/ai-agent-service-lab",
    "document": "labs/*.md",
    "downloads": []
  },
  "units": [
    {
      "id": "overview",
      "kind": "guide",
      "title": "О практикуме",
      "sections": [
        {
          "id": "intro",
          "title": "",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Учебно-практический проект aka-gst: разобраться, как устроены AI-агенты, научиться собирать их под реальные задачи и превратить этот навык в понятную услугу для клиентов."
            }
          ]
        },
        {
          "id": "chto-uzhe-rabotaet",
          "title": "Что уже работает",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "локальная генерация через Ollama и <code>qwen3:8b</code>;",
                "structured output с JSON Schema и Pydantic;",
                "agent loop с allowlist безопасных инструментов;",
                "память сессий и audit log в SQLite;",
                "RAG по Markdown-документам с проверяемыми источниками;",
                "evaluation-набор: 6/6 сценариев, retrieval hit rate 100%;",
                "FastAPI, API-key guard, Docker Compose и health checks;",
                "backup/restore с SHA-256 и защита от path traversal;",
                "76 автоматических тестов;",
                "первый модуль помощника аналитика маркетплейса на искусственном CSV.",
                "контекстный навигатор по демонстрационной структуре кабинета продавца.",
                "безопасная основа read-only интеграции с официальным WB API."
              ]
            }
          ]
        },
        {
          "id": "veb-versiya-praktikuma",
          "title": "Веб-версия практикума",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Лабораторные работы читаются не только из репозитория. Тот же материал собирается в статический сайт: с телефона он читается, а поисковики его индексируют."
            },
            {
              "type": "code",
              "language": "bash",
              "text": "sh tools/build.sh"
            },
            {
              "type": "text",
              "html": "Команда пересобирает <code>course/llm-service-lab.json</code> и страницы в <code>web/</code>. CI выполняет её же и падает, если закоммиченный результат разошёлся с Markdown-исходниками."
            },
            {
              "type": "text",
              "html": "JSON описан общей схемой <code>aka-gst.course/1</code> — по ней QA Quest подключает этот практикум как ступень 3, не переписывая уроки заново."
            },
            {
              "type": "text",
              "html": "Опубликовано: <a href=\"https://aka-gst.ru/praktikum/llm/\">https://aka-gst.ru/praktikum/llm/</a>."
            }
          ]
        },
        {
          "id": "arhitektura",
          "title": "Архитектура",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "mermaid",
              "text": "flowchart LR\n    Client[\"CLI / HTTP-клиент\"] --> API[\"FastAPI + Pydantic\"]\n    API --> Guard[\"API key + лимиты\"]\n    Guard --> Agent[\"Agent loop\"]\n    Agent --> Tools[\"Разрешённые инструменты\"]\n    Agent --> Memory[\"SQLite: память и audit\"]\n    Agent --> RAG[\"RAG-поиск\"]\n    RAG --> Docs[\"Документы клиента\"]\n    RAG --> Embed[\"qwen3-embedding:0.6b\"]\n    Agent --> Model[\"Ollama / qwen3:8b\"]"
            }
          ]
        },
        {
          "id": "bystryi-zapusk",
          "title": "Быстрый запуск",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Требования: Python 3.12+, <a href=\"https://docs.astral.sh/uv/\">uv</a>, Ollama и модели <code>qwen3:8b</code>, <code>qwen3-embedding:0.6b</code>."
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv sync --python 3.12\nuv run python -m agent_lab.rag index\nuv run pytest -q\nuv run uvicorn agent_lab.service:app --host 127.0.0.1 --port 8000"
            },
            {
              "type": "text",
              "html": "Проверка сервиса:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "curl http://127.0.0.1:8000/health\ncurl http://127.0.0.1:8000/ready\ncurl http://127.0.0.1:8000/v1/integrations/wb/status"
            },
            {
              "type": "text",
              "html": "Настоящий токен WB хранится только в локальном <code>.env</code> как <code>WB_API_TOKEN</code> и никогда не добавляется в Git. До получения разрешения продавца интеграция работает без токена и продолжает использовать демонстрационные CSV."
            },
            {
              "type": "text",
              "html": "Веб-интерфейс помощника: <a href=\"http://127.0.0.1:8000/marketplace\">http://127.0.0.1:8000/marketplace</a>."
            },
            {
              "type": "text",
              "html": "Вопрос по демонстрационному отчёту маркетплейса:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "curl -X POST http://127.0.0.1:8000/v1/marketplace/ask \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"question\":\"Почему процент выкупа маленький?\",\"report\":\"sales-report.csv\"}'"
            },
            {
              "type": "text",
              "html": "Запуск через Docker Desktop:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "docker compose build\ndocker compose up -d"
            }
          ]
        },
        {
          "id": "konechnyi-rezultat",
          "title": "Конечный результат",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "После прохождения проекта должны появиться три демонстрационных решения:"
            },
            {
              "type": "list",
              "ordered": true,
              "items": [
                "<strong>Локальный помощник с инструментами</strong> — принимает задачу, выбирает разрешённый инструмент, выполняет действие и сохраняет проверяемый отчёт.",
                "<strong>RAG-помощник по документам</strong> — отвечает только на основе загруженных материалов и указывает источники.",
                "<strong>Клиентский агент под ключ</strong> — установка, конфигурация, тесты, инструкция, резервное копирование и передача заказчику."
              ]
            }
          ]
        },
        {
          "id": "chto-zdes-oznachaet-obuchit-agenta",
          "title": "Что здесь означает «обучить агента»",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "В большинстве клиентских проектов модель не обучают с нуля. Используются четыре уровня настройки:"
            },
            {
              "type": "list",
              "ordered": true,
              "items": [
                "<strong>Инструкция и примеры</strong> — задаём роль, ограничения и формат ответа.",
                "<strong>Инструменты</strong> — разрешаем агенту вызывать API, искать документы или работать с файлами.",
                "<strong>RAG</strong> — подключаем собственную базу знаний без изменения весов модели.",
                "<strong>Fine-tuning</strong> — дообучаем модель на подготовленном датасете, только если первые три уровня недостаточны."
              ]
            },
            {
              "type": "text",
              "html": "Обучение модели с нуля в этот проект не входит: оно требует больших датасетов, GPU-кластера и бюджета, а для большинства частных заказов не окупается."
            }
          ]
        },
        {
          "id": "tehnologicheskii-marshrut",
          "title": "Технологический маршрут",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "Python 3.12+",
                "HTTP и OpenAI-compatible API",
                "Ollama для локальных моделей",
                "Pydantic для схем данных",
                "pytest для проверок",
                "FastAPI для локального сервиса",
                "SQLite и векторное хранилище для памяти/RAG",
                "Docker — после того, как локальная версия работает",
                "GitHub Actions для автоматических проверок"
              ]
            }
          ]
        },
        {
          "id": "pravila-proekta",
          "title": "Правила проекта",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "Сначала формулируем задачу и критерии качества, затем выбираем модель.",
                "Секреты хранятся только в локальном <code>.env</code>, который не попадает в Git.",
                "Агент получает минимально необходимые разрешения.",
                "Каждый важный результат проверяется тестом или сохраняемым evidence.",
                "Для опасных и внешних действий требуется подтверждение человека.",
                "Нельзя обещать клиенту «обученную нейросеть», если фактически сделан prompt или RAG."
              ]
            }
          ]
        },
        {
          "id": "struktura",
          "title": "Структура",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "text",
              "text": "docs/       теория, архитектура и памятки\nlabs/       последовательные учебные лабораторные работы\nprojects/   итоговые проекты для портфолио\ntemplates/  анкета клиента, ТЗ, отчёт и инструкция передачи\ntests/      автоматические проверки"
            },
            {
              "type": "text",
              "html": "Начинать следует с ROADMAP.md и лабораторной работы <code>labs/01-model-api</code>."
            }
          ]
        },
        {
          "id": "tekuschii-rezultat",
          "title": "Текущий результат",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Пройдены лабораторные 1–10: Ollama API, structured output, безопасные инструменты, память SQLite, RAG, evaluation, FastAPI/Docker, передача клиенту, помощник аналитика маркетплейса и навигатор по интерфейсу. Проверяемый набор содержит 76 автоматических тестов."
            },
            {
              "type": "text",
              "html": "Подробный портфельный разбор: docs/portfolio-case-study.md."
            },
            {
              "type": "text",
              "html": "Следующий прикладной проект: помощник аналитика маркетплейса. Он считает показатели отчёта проверяемым кодом, а затем будет использовать RAG и локальную модель для объяснения результата."
            }
          ]
        },
        {
          "id": "licenziya",
          "title": "Лицензия",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Исходный код проекта опубликован по лицензии <a href=\"LICENSE\">MIT</a>. Модели, Ollama, Docker и сторонние библиотеки распространяются на собственных условиях и не включены в эту лицензию."
            }
          ]
        }
      ],
      "order": 1,
      "summary": "Учебно-практический проект aka-gst: разобраться, как устроены AI-агенты, научиться собирать их под реальные задачи и превратить этот навык в понятную услугу для клиентов.",
      "words": 650,
      "estimate_minutes": 5
    },
    {
      "id": "roadmap",
      "kind": "guide",
      "title": "Маршрут обучения",
      "sections": [
        {
          "id": "intro",
          "title": "",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Рекомендуемая нагрузка: 5–7 часов в неделю небольшими сессиями. Полный маршрут занимает примерно 8 недель."
            },
            {
              "type": "table",
              "head": [
                "Неделя",
                "Тема",
                "Зачем",
                "Практический результат"
              ],
              "rows": [
                [
                  "1",
                  "LLM, токены, контекст, API, Ollama",
                  "Понять, где находится модель и как приложение получает ответ",
                  "Скрипт отправляет запрос локальной и облачной модели"
                ],
                [
                  "2",
                  "Prompts, structured output, Pydantic",
                  "Получать предсказуемый результат вместо свободного текста",
                  "Ответ модели валидируется по JSON-схеме"
                ],
                [
                  "3",
                  "Инструменты и agent loop",
                  "Научить модель выбирать действие, но не давать ей неограниченный доступ",
                  "Агент с двумя безопасными инструментами"
                ],
                [
                  "4",
                  "Состояние, память и ограничения",
                  "Продолжать задачу между шагами и контролировать расходы",
                  "История сессии, лимиты и журнал действий"
                ],
                [
                  "5",
                  "Embeddings и RAG",
                  "Подключать документы клиента без переобучения модели",
                  "Помощник отвечает с цитатами из документов"
                ],
                [
                  "6",
                  "Evaluation и pytest",
                  "Измерять качество, а не верить красивому демо",
                  "Набор эталонных вопросов и автоматический отчёт"
                ],
                [
                  "7",
                  "FastAPI, Docker и установка",
                  "Передавать решение клиенту воспроизводимым способом",
                  "Локальный сервис, health check и инструкция запуска"
                ],
                [
                  "8",
                  "Безопасность и упаковка услуги",
                  "Не раскрывать секреты и чётко ограничивать ответственность",
                  "Клиентское ТЗ, acceptance criteria, rollback и портфолио"
                ]
              ]
            }
          ]
        },
        {
          "id": "kontrolnye-tochki",
          "title": "Контрольные точки",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "После 2-й недели: можно брать небольшие заказы на prompts, structured output и настройку Ollama.",
                "После 4-й недели: можно делать локальных помощников с ограниченными инструментами.",
                "После 6-й недели: можно предлагать RAG по документам с измеримой проверкой качества.",
                "После 8-й недели: можно продавать установку AI-агента под ключ с тестами и документацией."
              ]
            }
          ]
        },
        {
          "id": "pervyi-rabochii-cikl",
          "title": "Первый рабочий цикл",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Каждая лабораторная работа выполняется одинаково:"
            },
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Сформулировать пользовательскую задачу.",
                "Записать критерии PASS/FAIL.",
                "Собрать минимальную реализацию.",
                "Проверить её вручную.",
                "Добавить автоматические тесты.",
                "Зафиксировать ограничения и ошибки.",
                "Обновить инструкцию запуска."
              ]
            }
          ]
        }
      ],
      "order": 2,
      "summary": "Рекомендуемая нагрузка: 5–7 часов в неделю небольшими сессиями. Полный маршрут занимает примерно 8 недель.",
      "words": 95,
      "estimate_minutes": 3
    },
    {
      "id": "lab-01",
      "kind": "experiment",
      "title": "Лабораторная работа 1: модель как API",
      "sections": [
        {
          "id": "cel",
          "title": "Цель",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Научиться отличать модель, клиентское приложение и агента. На этом этапе агент ещё не создаётся: сначала нужно воспроизводимо получить ответ модели."
            }
          ]
        },
        {
          "id": "zadachi",
          "title": "Задачи",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Установить и проверить Ollama.",
                "Выбрать небольшую модель, подходящую компьютеру.",
                "Выполнить запрос через CLI.",
                "Выполнить тот же запрос через HTTP API.",
                "Сохранить только обезличенный пример запроса и ответа.",
                "Измерить время ответа и записать ограничения."
              ]
            }
          ]
        },
        {
          "id": "kriterii-zaversheniya",
          "title": "Критерии завершения",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "health check Ollama успешен;",
                "модель отвечает на один и тот же тестовый запрос через CLI и API;",
                "известны model id, время ответа и объём доступного контекста;",
                "секреты и персональные пути не попали в Git;",
                "написана короткая инструкция повторного запуска."
              ]
            }
          ]
        },
        {
          "id": "rezultat-vypolneniya",
          "title": "Результат выполнения",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Команды, обезличенные ответы, измерения и ограничения сохранены в RESULTS.md."
            }
          ]
        },
        {
          "id": "sleduyuschii-shag",
          "title": "Следующий шаг",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Во второй лабораторной работе свободный ответ будет заменён на проверяемую JSON-структуру."
            }
          ]
        },
        {
          "id": "rezultaty-vypolneniya",
          "title": "Результаты выполнения",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Дата проверки: 2026-08-14."
            }
          ]
        },
        {
          "id": "sreda",
          "title": "Среда",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "macOS на Apple Silicon;",
                "объединённая память: 24 ГБ;",
                "Ollama: <code>0.32.12</code>;",
                "модель: <code>qwen3:8b</code>;",
                "размер модели на диске: около 5,2 ГБ;",
                "заявленное моделью контекстное окно: 40K токенов;",
                "локальный API: <code>http://127.0.0.1:11434</code>;",
                "ускорение: Apple Metal."
              ]
            },
            {
              "type": "text",
              "html": "Персональные пути, идентификаторы устройства и ключи в отчёт не включены."
            }
          ]
        },
        {
          "id": "testovyi-prompt",
          "title": "Тестовый prompt",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "text",
              "text": "Ответь одним коротким предложением: что такое HTTP API?"
            }
          ]
        },
        {
          "id": "proverka-cherez-cli",
          "title": "Проверка через CLI",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "ollama run qwen3:8b \\\n  \"Ответь одним коротким предложением: что такое HTTP API?\""
            },
            {
              "type": "text",
              "html": "Полученный ответ:"
            },
            {
              "type": "code",
              "language": "text",
              "text": "HTTP API — это набор правил и методов для взаимодействия с веб-сервисами через HTTP-запросы."
            }
          ]
        },
        {
          "id": "proverka-cherez-http-api",
          "title": "Проверка через HTTP API",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "curl -s http://127.0.0.1:11434/api/chat \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"model\": \"qwen3:8b\",\n    \"messages\": [\n      {\n        \"role\": \"user\",\n        \"content\": \"Ответь одним коротким предложением: что такое HTTP API?\"\n      }\n    ],\n    \"stream\": false\n  }' | python3 -m json.tool"
            },
            {
              "type": "text",
              "html": "Полученный <code>message.content</code>:"
            },
            {
              "type": "code",
              "language": "text",
              "text": "HTTP API — это интерфейс для взаимодействия между клиентом и сервером через HTTP-запросы и ответы, позволяющий обмениваться данными в веб-приложениях."
            },
            {
              "type": "text",
              "html": "Ключевые признаки успешного ответа:"
            },
            {
              "type": "code",
              "language": "json",
              "text": "{\n  \"model\": \"qwen3:8b\",\n  \"done\": true,\n  \"done_reason\": \"stop\",\n  \"prompt_eval_count\": 27,\n  \"eval_count\": 235\n}"
            }
          ]
        },
        {
          "id": "izmereniya",
          "title": "Измерения",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Метрики первого сохранённого HTTP-запроса:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "полное время: около 10,85 с;",
                "загрузка модели: около 0,09 с;",
                "обработка prompt: около 0,05 с;",
                "генерация: около 10,70 с;",
                "скорость генерации: около 22 токенов/с."
              ]
            },
            {
              "type": "text",
              "html": "Время вычислено из полей Ollama, значения которых возвращаются в наносекундах. Скорость рассчитана как <code>eval_count / eval_duration</code>."
            }
          ]
        },
        {
          "id": "nablyudeniya-i-ogranicheniya",
          "title": "Наблюдения и ограничения",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "ответы на одинаковый prompt могут отличаться, потому что генерация вероятностная;",
                "<code>qwen3:8b</code> может возвращать отдельное поле <code>thinking</code>, увеличивающее время и число сгенерированных токенов;",
                "локальная модель не гарантирует фактическую точность ответа;",
                "скорость зависит от свободной памяти, температуры устройства, длины контекста и параметров генерации;",
                "при остановленном Ollama запрос к порту <code>11434</code> завершится ошибкой подключения."
              ]
            }
          ]
        },
        {
          "id": "povtornyi-zapusk",
          "title": "Повторный запуск",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Запустить приложение Ollama.",
                "Проверить сервер: <code>curl http://127.0.0.1:11434/api/version</code>.",
                "Проверить модель: <code>ollama list</code>.",
                "Повторить CLI- и HTTP-запросы выше."
              ]
            }
          ]
        },
        {
          "id": "itog",
          "title": "Итог",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Одна локальная модель успешно вызвана двумя клиентами: командой Ollama и HTTP-клиентом <code>curl</code>. Следующий этап — заменить свободный текст проверяемым structured output."
            }
          ]
        }
      ],
      "number": "1",
      "order": 3,
      "summary": "Научиться отличать модель, клиентское приложение и агента. На этом этапе агент ещё не создаётся: сначала нужно воспроизводимо получить ответ модели.",
      "words": 397,
      "estimate_minutes": 3,
      "task": {
        "objective": [
          "Научиться отличать модель, клиентское приложение и агента. На этом этапе агент ещё не создаётся: сначала нужно воспроизводимо получить ответ модели."
        ],
        "scope": [
          "Установить и проверить Ollama.",
          "Выбрать небольшую модель, подходящую компьютеру.",
          "Выполнить запрос через CLI.",
          "Выполнить тот же запрос через HTTP API.",
          "Сохранить только обезличенный пример запроса и ответа.",
          "Измерить время ответа и записать ограничения."
        ],
        "done_when": [
          "health check Ollama успешен;",
          "модель отвечает на один и тот же тестовый запрос через CLI и API;",
          "известны model id, время ответа и объём доступного контекста;",
          "секреты и персональные пути не попали в Git;",
          "написана короткая инструкция повторного запуска."
        ],
        "artifacts": [
          "Команды, обезличенные ответы, измерения и ограничения сохранены в RESULTS.md."
        ],
        "next": [
          "Во второй лабораторной работе свободный ответ будет заменён на проверяемую JSON-структуру."
        ],
        "limits": [
          "ответы на одинаковый prompt могут отличаться, потому что генерация вероятностная;",
          "qwen3:8b может возвращать отдельное поле thinking, увеличивающее время и число сгенерированных токенов;",
          "локальная модель не гарантирует фактическую точность ответа;",
          "скорость зависит от свободной памяти, температуры устройства, длины контекста и параметров генерации;",
          "при остановленном Ollama запрос к порту 11434 завершится ошибкой подключения."
        ],
        "snippets": [
          {
            "section": "Тестовый prompt",
            "language": "text",
            "text": "Ответь одним коротким предложением: что такое HTTP API?"
          },
          {
            "section": "Проверка через CLI",
            "language": "bash",
            "text": "ollama run qwen3:8b \\\n  \"Ответь одним коротким предложением: что такое HTTP API?\""
          },
          {
            "section": "Проверка через CLI",
            "language": "text",
            "text": "HTTP API — это набор правил и методов для взаимодействия с веб-сервисами через HTTP-запросы."
          },
          {
            "section": "Проверка через HTTP API",
            "language": "bash",
            "text": "curl -s http://127.0.0.1:11434/api/chat \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"model\": \"qwen3:8b\",\n    \"messages\": [\n      {\n        \"role\": \"user\",\n        \"content\": \"Ответь одним коротким предложением: что такое HTTP API?\"\n      }\n    ],\n    \"stream\": false\n  }' | python3 -m json.tool"
          },
          {
            "section": "Проверка через HTTP API",
            "language": "text",
            "text": "HTTP API — это интерфейс для взаимодействия между клиентом и сервером через HTTP-запросы и ответы, позволяющий обмениваться данными в веб-приложениях."
          },
          {
            "section": "Проверка через HTTP API",
            "language": "json",
            "text": "{\n  \"model\": \"qwen3:8b\",\n  \"done\": true,\n  \"done_reason\": \"stop\",\n  \"prompt_eval_count\": 27,\n  \"eval_count\": 235\n}"
          }
        ]
      }
    },
    {
      "id": "lab-02",
      "kind": "experiment",
      "title": "Лабораторная работа 2: structured output",
      "sections": [
        {
          "id": "cel",
          "title": "Цель",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Получить от локальной модели не свободный текст, а данные по строгой JSON-схеме и проверить их перед использованием в программе."
            }
          ]
        },
        {
          "id": "polzovatelskaya-zadacha",
          "title": "Пользовательская задача",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Классифицировать обращение службы поддержки по трём полям:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "<code>category</code>: <code>access</code>, <code>billing</code>, <code>technical</code> или <code>other</code>;",
                "<code>priority</code>: <code>low</code>, <code>medium</code> или <code>high</code>;",
                "<code>summary</code>: непустое краткое описание."
              ]
            }
          ]
        },
        {
          "id": "kriterii-pass-fail",
          "title": "Критерии PASS/FAIL",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "PASS:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "Ollama возвращает JSON по схеме <code>SupportTicket</code>;",
                "Pydantic принимает корректный ответ;",
                "отсутствующие, лишние и недопустимые значения отклоняются;",
                "для проблемы со входом выбирается категория <code>access</code>."
              ]
            },
            {
              "type": "text",
              "html": "FAIL — нарушен хотя бы один из этих пунктов."
            }
          ]
        },
        {
          "id": "podgotovka",
          "title": "Подготовка",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Из корня проекта:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv sync --python 3.12"
            },
            {
              "type": "text",
              "html": "Команда создаст <code>.venv</code>, установит Python 3.12, Pydantic и pytest. Системный Python при этом не изменяется."
            }
          ]
        },
        {
          "id": "zapusk",
          "title": "Запуск",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Сначала запустите приложение Ollama, затем выполните:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.structured_output \\\n  \"Не могу войти в аккаунт, а завтра нужно отправить важный отчёт.\""
            },
            {
              "type": "text",
              "html": "Ожидаемая структура:"
            },
            {
              "type": "code",
              "language": "json",
              "text": "{\n  \"category\": \"access\",\n  \"priority\": \"high\",\n  \"summary\": \"Клиент не может войти в аккаунт перед отправкой важного отчёта.\"\n}"
            },
            {
              "type": "text",
              "html": "Точная формулировка <code>summary</code> может отличаться."
            }
          ]
        },
        {
          "id": "avtomaticheskaya-proverka",
          "title": "Автоматическая проверка",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run pytest"
            },
            {
              "type": "text",
              "html": "Тесты намеренно передают как правильные, так и неправильные данные. Pydantic должен отклонить неизвестную категорию, неправильный приоритет, отсутствующее поле и лишнее поле."
            }
          ]
        },
        {
          "id": "chto-zdes-proishodit",
          "title": "Что здесь происходит",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "<code>SupportTicket</code> описывает допустимые данные в Python.",
                "<code>model_json_schema()</code> превращает описание в JSON Schema для Ollama.",
                "Ollama ограничивает структуру ответа модели этой схемой.",
                "<code>model_validate_json()</code> повторно проверяет фактический ответ.",
                "Только после успешной проверки программа использует результат."
              ]
            }
          ]
        },
        {
          "id": "ogranichenie",
          "title": "Ограничение",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Structured output гарантирует форму данных, но не истинность и не правильность классификации. Поэтому правила задачи уточняются в system prompt, а смысловая корректность отдельно проверяется evaluation-тестами."
            }
          ]
        }
      ],
      "number": "2",
      "order": 4,
      "summary": "Получить от локальной модели не свободный текст, а данные по строгой JSON-схеме и проверить их перед использованием в программе.",
      "words": 225,
      "estimate_minutes": 3,
      "task": {
        "objective": [
          "Получить от локальной модели не свободный текст, а данные по строгой JSON-схеме и проверить их перед использованием в программе."
        ],
        "commands": [
          {
            "language": "bash",
            "text": "uv run python -m agent_lab.structured_output \\\n  \"Не могу войти в аккаунт, а завтра нужно отправить важный отчёт.\""
          },
          {
            "language": "json",
            "text": "{\n  \"category\": \"access\",\n  \"priority\": \"high\",\n  \"summary\": \"Клиент не может войти в аккаунт перед отправкой важного отчёта.\"\n}"
          }
        ],
        "command_notes": [
          "Сначала запустите приложение Ollama, затем выполните:",
          "Ожидаемая структура:",
          "Точная формулировка summary может отличаться."
        ],
        "snippets": [
          {
            "section": "Подготовка",
            "language": "bash",
            "text": "uv sync --python 3.12"
          },
          {
            "section": "Запуск",
            "language": "bash",
            "text": "uv run python -m agent_lab.structured_output \\\n  \"Не могу войти в аккаунт, а завтра нужно отправить важный отчёт.\""
          },
          {
            "section": "Запуск",
            "language": "json",
            "text": "{\n  \"category\": \"access\",\n  \"priority\": \"high\",\n  \"summary\": \"Клиент не может войти в аккаунт перед отправкой важного отчёта.\"\n}"
          },
          {
            "section": "Автоматическая проверка",
            "language": "bash",
            "text": "uv run pytest"
          }
        ]
      }
    },
    {
      "id": "lab-03",
      "kind": "experiment",
      "title": "Лабораторная работа 3: инструменты и agent loop",
      "sections": [
        {
          "id": "cel",
          "title": "Цель",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Дать модели две строго ограниченные функции и реализовать цикл «модель → инструмент → результат → модель»."
            }
          ]
        },
        {
          "id": "instrumenty",
          "title": "Инструменты",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "<code>lookup_order</code> читает только два фиктивных заказа из встроенного словаря.",
                "<code>calculate_order_total</code> принимает ID заказа и количество от 1 до 100, а цену берёт из доверенного справочника."
              ]
            },
            {
              "type": "text",
              "html": "Агент не получает shell, сеть, произвольное чтение файлов или изменение данных."
            }
          ]
        },
        {
          "id": "zapusk",
          "title": "Запуск",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Запустите приложение Ollama и из корня проекта выполните:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.tool_agent \\\n  \"Узнай статус заказа DEMO-1001 и посчитай стоимость трёх таких заказов.\""
            },
            {
              "type": "text",
              "html": "Программа выводит журнал вызванных инструментов, итоговый ответ и число шагов."
            }
          ]
        },
        {
          "id": "kak-rabotaet-cikl",
          "title": "Как работает цикл",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Python отправляет модели задачу и JSON-описания разрешённых инструментов.",
                "Модель отвечает обычным текстом либо массивом <code>tool_calls</code>.",
                "Python проверяет имя функции по allowlist.",
                "Pydantic проверяет аргументы и запрещает лишние поля.",
                "Python выполняет функцию и возвращает результат модели с ролью <code>tool</code>.",
                "Цикл повторяется, но не более четырёх шагов."
              ]
            }
          ]
        },
        {
          "id": "kriterii-pass-fail",
          "title": "Критерии PASS/FAIL",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "PASS:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "известный заказ найден без выдуманных данных;",
                "неизвестный заказ возвращает <code>found: false</code>;",
                "итоговая стоимость считается по доверенной цене без <code>eval</code>;",
                "неизвестный инструмент и лишние аргументы отклоняются;",
                "цикл завершается текстовым ответом в пределах лимита."
              ]
            }
          ]
        },
        {
          "id": "testy",
          "title": "Тесты",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run pytest"
            },
            {
              "type": "text",
              "html": "Тест <code>run_shell</code> специально имитирует опасный вызов модели. Диспетчер обязан отклонить его, поскольку такой функции нет в allowlist."
            }
          ]
        },
        {
          "id": "ogranicheniya",
          "title": "Ограничения",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "модель может выбрать ненужный инструмент или не вызвать нужный;",
                "схема проверяет аргументы, но бизнес-правила всё равно реализуются в Python;",
                "демонстрационная база заказов хранится в памяти и не содержит реальных данных;",
                "для внешних и изменяющих действий в реальном проекте потребуется подтверждение человека."
              ]
            },
            {
              "type": "text",
              "html": "Универсальный калькулятор намеренно не используется: на первом сквозном тесте модель перепутала цену <code>2490</code> с числом <code>1001</code> из ID заказа. Узкий бизнес-инструмент не позволяет модели самостоятельно подставлять цену и устраняет этот класс ошибки."
            }
          ]
        }
      ],
      "number": "3",
      "order": 5,
      "summary": "Дать модели две строго ограниченные функции и реализовать цикл «модель → инструмент → результат → модель».",
      "words": 257,
      "estimate_minutes": 3,
      "task": {
        "objective": [
          "Дать модели две строго ограниченные функции и реализовать цикл «модель → инструмент → результат → модель»."
        ],
        "commands": [
          {
            "language": "bash",
            "text": "uv run python -m agent_lab.tool_agent \\\n  \"Узнай статус заказа DEMO-1001 и посчитай стоимость трёх таких заказов.\""
          }
        ],
        "command_notes": [
          "Запустите приложение Ollama и из корня проекта выполните:",
          "Программа выводит журнал вызванных инструментов, итоговый ответ и число шагов."
        ],
        "limits": [
          "модель может выбрать ненужный инструмент или не вызвать нужный;",
          "схема проверяет аргументы, но бизнес-правила всё равно реализуются в Python;",
          "демонстрационная база заказов хранится в памяти и не содержит реальных данных;",
          "для внешних и изменяющих действий в реальном проекте потребуется подтверждение человека.",
          "Универсальный калькулятор намеренно не используется: на первом сквозном тесте модель перепутала цену 2490 с числом 1001 из ID заказа. Узкий бизнес-инструмент не позволяет модели самостоятельно подставлять цену и устраняет этот класс ошибки."
        ],
        "snippets": [
          {
            "section": "Запуск",
            "language": "bash",
            "text": "uv run python -m agent_lab.tool_agent \\\n  \"Узнай статус заказа DEMO-1001 и посчитай стоимость трёх таких заказов.\""
          },
          {
            "section": "Тесты",
            "language": "bash",
            "text": "uv run pytest"
          }
        ]
      }
    },
    {
      "id": "lab-04",
      "kind": "experiment",
      "title": "Лабораторная работа 4: состояние, память и ограничения",
      "sections": [
        {
          "id": "cel",
          "title": "Цель",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Продолжать диалог между отдельными запусками программы, изолировать сессии и сохранять проверяемый журнал действий."
            }
          ]
        },
        {
          "id": "chto-hranitsya",
          "title": "Что хранится",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "сообщения пользователя и финальные ответы агента;",
                "ID сессии;",
                "вызовы инструментов и их результаты;",
                "число шагов и объём использованной истории."
              ]
            },
            {
              "type": "text",
              "html": "Данные записываются в <code>data/private/agent_memory.sqlite3</code>. Папка <code>data/private/</code> исключена из Git."
            }
          ]
        },
        {
          "id": "pervyi-zapros",
          "title": "Первый запрос",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.memory_agent \\\n  --session training \\\n  \"Запомни: меня интересует заказ DEMO-1001.\""
            }
          ]
        },
        {
          "id": "prodolzhenie-sessii",
          "title": "Продолжение сессии",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.memory_agent \\\n  --session training \\\n  \"Какой у него статус и сколько стоят две штуки?\""
            },
            {
              "type": "text",
              "html": "Во втором запуске программа загрузит историю сессии <code>training</code>, поэтому модель сможет восстановить ID заказа."
            }
          ]
        },
        {
          "id": "prosmotr-dannyh",
          "title": "Просмотр данных",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.memory_agent --session training --show-history\nuv run python -m agent_lab.memory_agent --session training --show-audit"
            }
          ]
        },
        {
          "id": "ochistka",
          "title": "Очистка",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Удаление ограничено одной явно названной сессией:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.memory_agent --session training --clear"
            }
          ]
        },
        {
          "id": "ogranicheniya",
          "title": "Ограничения",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "в модель передаётся не более 10 последних сообщений по умолчанию;",
                "допустимо установить лимит истории только от 1 до 50;",
                "agent loop ограничен четырьмя шагами;",
                "ID сессии содержит только латинские буквы, цифры, <code>_</code> и <code>-</code>, максимум 64 символа;",
                "разные сессии не видят историю друг друга;",
                "память локальная и не синхронизируется между компьютерами."
              ]
            }
          ]
        },
        {
          "id": "vazhno",
          "title": "Важно",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Память агента — не изменение модели. Перед каждым запросом программа извлекает прошлые сообщения из SQLite и снова передаёт их модели как контекст."
            },
            {
              "type": "text",
              "html": "Реальные секреты, пароли, платёжные данные и лишние персональные сведения сохранять в такую память нельзя."
            }
          ]
        },
        {
          "id": "testy",
          "title": "Тесты",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run pytest"
            },
            {
              "type": "text",
              "html": "Тесты используют временную SQLite-базу и проверяют изоляцию сессий, лимит истории, audit log, очистку и недопустимые ID."
            }
          ]
        }
      ],
      "number": "4",
      "order": 6,
      "summary": "Продолжать диалог между отдельными запусками программы, изолировать сессии и сохранять проверяемый журнал действий.",
      "words": 223,
      "estimate_minutes": 3,
      "task": {
        "objective": [
          "Продолжать диалог между отдельными запусками программы, изолировать сессии и сохранять проверяемый журнал действий."
        ],
        "limits": [
          "в модель передаётся не более 10 последних сообщений по умолчанию;",
          "допустимо установить лимит истории только от 1 до 50;",
          "agent loop ограничен четырьмя шагами;",
          "ID сессии содержит только латинские буквы, цифры, _ и -, максимум 64 символа;",
          "разные сессии не видят историю друг друга;",
          "память локальная и не синхронизируется между компьютерами."
        ],
        "snippets": [
          {
            "section": "Первый запрос",
            "language": "bash",
            "text": "uv run python -m agent_lab.memory_agent \\\n  --session training \\\n  \"Запомни: меня интересует заказ DEMO-1001.\""
          },
          {
            "section": "Продолжение сессии",
            "language": "bash",
            "text": "uv run python -m agent_lab.memory_agent \\\n  --session training \\\n  \"Какой у него статус и сколько стоят две штуки?\""
          },
          {
            "section": "Просмотр данных",
            "language": "bash",
            "text": "uv run python -m agent_lab.memory_agent --session training --show-history\nuv run python -m agent_lab.memory_agent --session training --show-audit"
          },
          {
            "section": "Очистка",
            "language": "bash",
            "text": "uv run python -m agent_lab.memory_agent --session training --clear"
          },
          {
            "section": "Тесты",
            "language": "bash",
            "text": "uv run pytest"
          }
        ]
      }
    },
    {
      "id": "lab-05",
      "kind": "experiment",
      "title": "Лабораторная работа 5: embeddings и RAG",
      "sections": [
        {
          "id": "cel",
          "title": "Цель",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Отвечать по локальным документам, которые можно обновлять без переобучения модели, и указывать проверяемые источники."
            }
          ]
        },
        {
          "id": "komponenty",
          "title": "Компоненты",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "демонстрационные документы: <code>data/demo/client-docs/</code>;",
                "embedding-модель: <code>qwen3-embedding:0.6b</code>;",
                "генеративная модель: <code>qwen3:8b</code>;",
                "локальный индекс: <code>data/private/rag.sqlite3</code>;",
                "поиск: cosine similarity;",
                "формат ответа: Pydantic-модель <code>RagAnswer</code>."
              ]
            }
          ]
        },
        {
          "id": "postroenie-indeksa",
          "title": "Построение индекса",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.rag index"
            },
            {
              "type": "text",
              "html": "Программа разбивает Markdown по заголовкам <code>##</code>, получает embedding каждого фрагмента и сохраняет вектор вместе с текстом и источником в SQLite."
            }
          ]
        },
        {
          "id": "vopros",
          "title": "Вопрос",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.rag ask \\\n  \"Сколько стоит доставка заказа на 4 000 рублей?\""
            },
            {
              "type": "text",
              "html": "Ожидается ответ <code>490 рублей</code> со ссылкой на <code>delivery.md#Стоимость</code>."
            }
          ]
        },
        {
          "id": "kak-rabotaet-rag",
          "title": "Как работает RAG",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Документы разбиваются на небольшие фрагменты.",
                "Embedding-модель превращает каждый фрагмент в числовой вектор.",
                "Вопрос пользователя тоже превращается в вектор.",
                "По cosine similarity выбираются три ближайших фрагмента.",
                "Только эти фрагменты передаются генеративной модели.",
                "Модель формирует ответ и возвращает список источников.",
                "Python отклоняет ссылку на источник, которого не было среди найденных фрагментов."
              ]
            }
          ]
        },
        {
          "id": "obnovlenie-dokumentov",
          "title": "Обновление документов",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "После изменения Markdown-файлов индекс нужно построить повторно:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.rag index"
            },
            {
              "type": "text",
              "html": "Это и есть важное отличие RAG от fine-tuning: новые факты подключаются переиндексацией документов, а веса модели не меняются."
            }
          ]
        },
        {
          "id": "ogranicheniya",
          "title": "Ограничения",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "учебный поиск перебирает все векторы в SQLite и не рассчитан на миллионы фрагментов;",
                "качество зависит от разбиения, embedding-модели и формулировки вопроса;",
                "наличие источника не доказывает, что модель правильно истолковала текст;",
                "демонстрационные документы вымышлены и не являются правилами реального магазина;",
                "приватный индекс исключён из Git, но исходные demo-документы публикуются вместе с проектом."
              ]
            }
          ]
        },
        {
          "id": "testy",
          "title": "Тесты",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run pytest"
            },
            {
              "type": "text",
              "html": "Тесты проверяют разбиение документов, сравнение векторов, ранжирование и блокировку выдуманного источника без обращения к модели."
            }
          ]
        }
      ],
      "number": "5",
      "order": 7,
      "summary": "Отвечать по локальным документам, которые можно обновлять без переобучения модели, и указывать проверяемые источники.",
      "words": 230,
      "estimate_minutes": 3,
      "task": {
        "objective": [
          "Отвечать по локальным документам, которые можно обновлять без переобучения модели, и указывать проверяемые источники."
        ],
        "limits": [
          "учебный поиск перебирает все векторы в SQLite и не рассчитан на миллионы фрагментов;",
          "качество зависит от разбиения, embedding-модели и формулировки вопроса;",
          "наличие источника не доказывает, что модель правильно истолковала текст;",
          "демонстрационные документы вымышлены и не являются правилами реального магазина;",
          "приватный индекс исключён из Git, но исходные demo-документы публикуются вместе с проектом."
        ],
        "snippets": [
          {
            "section": "Построение индекса",
            "language": "bash",
            "text": "uv run python -m agent_lab.rag index"
          },
          {
            "section": "Вопрос",
            "language": "bash",
            "text": "uv run python -m agent_lab.rag ask \\\n  \"Сколько стоит доставка заказа на 4 000 рублей?\""
          },
          {
            "section": "Обновление документов",
            "language": "bash",
            "text": "uv run python -m agent_lab.rag index"
          },
          {
            "section": "Тесты",
            "language": "bash",
            "text": "uv run pytest"
          }
        ]
      }
    },
    {
      "id": "lab-06",
      "kind": "experiment",
      "title": "Лабораторная работа 6: evaluation и pytest",
      "sections": [
        {
          "id": "cel",
          "title": "Цель",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Проверять качество RAG на наборе заранее определённых вопросов, а не судить по одному удачному демо."
            }
          ]
        },
        {
          "id": "etalonnyi-nabor",
          "title": "Эталонный набор",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Файл <code>evals/rag_cases.json</code> содержит шесть сценариев:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "стоимость и сроки доставки;",
                "срок возврата товара и денег;",
                "работа поддержки в выходные;",
                "вопрос о гарантии, ответа на который в документах нет."
              ]
            },
            {
              "type": "text",
              "html": "Каждый положительный сценарий задаёт ожидаемый источник и обязательные фрагменты ответа. Отрицательный сценарий требует отказа от ответа без источников."
            }
          ]
        },
        {
          "id": "zapusk",
          "title": "Запуск",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Сначала должен быть построен RAG-индекс:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.rag index"
            },
            {
              "type": "text",
              "html": "Затем запустите evaluation:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.evaluation"
            },
            {
              "type": "text",
              "html": "Команда возвращает код <code>0</code>, только если прошли все сценарии. Отчёты сохраняются в:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "<code>artifacts/evals/rag-eval.json</code> — данные для автоматической обработки;",
                "<code>artifacts/evals/rag-eval.md</code> — краткий отчёт для человека."
              ]
            }
          ]
        },
        {
          "id": "metriki",
          "title": "Метрики",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "<code>pass_rate</code> — доля полностью успешных сценариев;",
                "<code>retrieval_hit_rate</code> — доля положительных вопросов, где ожидаемый источник попал в top-3;",
                "<code>source_pass</code> — модель сослалась на ожидаемый источник;",
                "<code>answer_pass</code> — ответ содержит обязательные контрольные фрагменты;",
                "<code>average_latency_seconds</code> — среднее время полного запроса."
              ]
            }
          ]
        },
        {
          "id": "chto-eti-testy-ne-dokazyvayut",
          "title": "Что эти тесты не доказывают",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Проверка ключевых фрагментов не понимает весь смысл ответа. Она может пропустить убедительно написанную ошибку или отклонить правильную переформулировку. Для клиентского проекта набор расширяют реальными обезличенными вопросами и периодически проверяют ответы вручную."
            }
          ]
        },
        {
          "id": "modulnye-testy",
          "title": "Модульные тесты",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run pytest"
            },
            {
              "type": "text",
              "html": "Они не вызывают модель и быстро проверяют саму логику подсчёта PASS/FAIL и метрик."
            }
          ]
        }
      ],
      "number": "6",
      "order": 8,
      "summary": "Проверять качество RAG на наборе заранее определённых вопросов, а не судить по одному удачному демо.",
      "words": 187,
      "estimate_minutes": 3,
      "task": {
        "objective": [
          "Проверять качество RAG на наборе заранее определённых вопросов, а не судить по одному удачному демо."
        ],
        "commands": [
          {
            "language": "bash",
            "text": "uv run python -m agent_lab.rag index"
          },
          {
            "language": "bash",
            "text": "uv run python -m agent_lab.evaluation"
          }
        ],
        "command_notes": [
          "Сначала должен быть построен RAG-индекс:",
          "Затем запустите evaluation:",
          "Команда возвращает код 0, только если прошли все сценарии. Отчёты сохраняются в:",
          "artifacts/evals/rag-eval.json — данные для автоматической обработки;",
          "artifacts/evals/rag-eval.md — краткий отчёт для человека."
        ],
        "metrics": [
          "pass_rate — доля полностью успешных сценариев;",
          "retrieval_hit_rate — доля положительных вопросов, где ожидаемый источник попал в top-3;",
          "source_pass — модель сослалась на ожидаемый источник;",
          "answer_pass — ответ содержит обязательные контрольные фрагменты;",
          "average_latency_seconds — среднее время полного запроса."
        ],
        "limits": [
          "Проверка ключевых фрагментов не понимает весь смысл ответа. Она может пропустить убедительно написанную ошибку или отклонить правильную переформулировку. Для клиентского проекта набор расширяют реальными обезличенными вопросами и периодически проверяют ответы вручную."
        ],
        "snippets": [
          {
            "section": "Запуск",
            "language": "bash",
            "text": "uv run python -m agent_lab.rag index"
          },
          {
            "section": "Запуск",
            "language": "bash",
            "text": "uv run python -m agent_lab.evaluation"
          },
          {
            "section": "Модульные тесты",
            "language": "bash",
            "text": "uv run pytest"
          }
        ]
      }
    },
    {
      "id": "lab-07",
      "kind": "experiment",
      "title": "Лабораторная работа 7: FastAPI и упаковка",
      "sections": [
        {
          "id": "cel",
          "title": "Цель",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Предоставить готовые компоненты как локальный HTTP API с health check, проверяемыми контрактами и воспроизводимой конфигурацией."
            }
          ]
        },
        {
          "id": "lokalnyi-zapusk",
          "title": "Локальный запуск",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run uvicorn agent_lab.service:app --host 127.0.0.1 --port 8000"
            },
            {
              "type": "text",
              "html": "Интерактивная документация будет доступна по адресу <code>http://127.0.0.1:8000/docs</code>."
            }
          ]
        },
        {
          "id": "endpointy",
          "title": "Эндпоинты",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "<code>GET /health</code> — работает ли сам FastAPI-процесс;",
                "<code>GET /ready</code> — доступны ли Ollama и RAG-индекс;",
                "<code>POST /v1/tickets/classify</code> — structured output классификатора;",
                "<code>POST /v1/rag/ask</code> — ответ по документам с источниками."
              ]
            },
            {
              "type": "text",
              "html": "Проверка:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "curl http://127.0.0.1:8000/health\ncurl http://127.0.0.1:8000/ready"
            },
            {
              "type": "text",
              "html": "RAG-запрос:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "curl -s http://127.0.0.1:8000/v1/rag/ask \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"question\":\"Сколько стоит доставка заказа на 4 000 рублей?\"}' \\\n  | python3 -m json.tool"
            }
          ]
        },
        {
          "id": "konfiguraciya",
          "title": "Конфигурация",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Переменные перечислены в <code>.env.example</code>. Реальный <code>.env</code> не попадает в Git."
            }
          ]
        },
        {
          "id": "docker",
          "title": "Docker",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "На macOS контейнер обращается к Ollama хоста через <code>host.docker.internal</code>:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "docker compose build\ndocker compose run --rm api python -m agent_lab.rag index\ndocker compose up -d"
            },
            {
              "type": "text",
              "html": "SQLite-индекс монтируется из <code>data/private/</code> и сохраняется между перезапусками контейнера."
            },
            {
              "type": "text",
              "html": "Остановка:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "docker compose down"
            }
          ]
        },
        {
          "id": "testy",
          "title": "Тесты",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run pytest"
            },
            {
              "type": "text",
              "html": "API-тесты подменяют модель и проверяют HTTP-контракты без сетевых запросов: health, degraded readiness, structured output, RAG и запрет лишних полей."
            }
          ]
        },
        {
          "id": "ogranicheniya",
          "title": "Ограничения",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "сервис привязан к <code>127.0.0.1</code>, пока явно не настроены аутентификация и TLS;",
                "API не предназначен для публичного интернета;",
                "Docker CLI требуется установить отдельно;",
                "<code>/health</code> проверяет только процесс, а <code>/ready</code> — внешние зависимости;",
                "изменение документов требует повторного построения RAG-индекса."
              ]
            }
          ]
        }
      ],
      "number": "7",
      "order": 9,
      "summary": "Предоставить готовые компоненты как локальный HTTP API с health check, проверяемыми контрактами и воспроизводимой конфигурацией.",
      "words": 194,
      "estimate_minutes": 3,
      "task": {
        "objective": [
          "Предоставить готовые компоненты как локальный HTTP API с health check, проверяемыми контрактами и воспроизводимой конфигурацией."
        ],
        "limits": [
          "сервис привязан к 127.0.0.1, пока явно не настроены аутентификация и TLS;",
          "API не предназначен для публичного интернета;",
          "Docker CLI требуется установить отдельно;",
          "/health проверяет только процесс, а /ready — внешние зависимости;",
          "изменение документов требует повторного построения RAG-индекса."
        ],
        "snippets": [
          {
            "section": "Локальный запуск",
            "language": "bash",
            "text": "uv run uvicorn agent_lab.service:app --host 127.0.0.1 --port 8000"
          },
          {
            "section": "Эндпоинты",
            "language": "bash",
            "text": "curl http://127.0.0.1:8000/health\ncurl http://127.0.0.1:8000/ready"
          },
          {
            "section": "Эндпоинты",
            "language": "bash",
            "text": "curl -s http://127.0.0.1:8000/v1/rag/ask \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"question\":\"Сколько стоит доставка заказа на 4 000 рублей?\"}' \\\n  | python3 -m json.tool"
          },
          {
            "section": "Docker",
            "language": "bash",
            "text": "docker compose build\ndocker compose run --rm api python -m agent_lab.rag index\ndocker compose up -d"
          },
          {
            "section": "Docker",
            "language": "bash",
            "text": "docker compose down"
          },
          {
            "section": "Тесты",
            "language": "bash",
            "text": "uv run pytest"
          }
        ]
      }
    },
    {
      "id": "lab-08",
      "kind": "experiment",
      "title": "Лабораторная работа 8: безопасность и передача клиенту",
      "sections": [
        {
          "id": "cel",
          "title": "Цель",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Подготовить сервис к контролируемой передаче: доступ, backup, acceptance criteria, rollback и честное описание ограничений."
            }
          ]
        },
        {
          "id": "api-klyuch",
          "title": "API-ключ",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Для локальной разработки ключ можно не задавать. Если сервис доступен другим пользователям, создайте секрет вне Git:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "python3 -c 'import secrets; print(secrets.token_urlsafe(32))'"
            },
            {
              "type": "text",
              "html": "Запишите значение в локальный <code>.env</code>:"
            },
            {
              "type": "code",
              "language": "text",
              "text": "SERVICE_API_KEY=полученное-значение"
            },
            {
              "type": "text",
              "html": "Клиент передаёт ключ заголовком:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "curl http://127.0.0.1:8000/v1/rag/ask \\\n  -H \"X-API-Key: $SERVICE_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"question\":\"Каковы сроки доставки?\"}'"
            },
            {
              "type": "text",
              "html": "<code>/health</code> и <code>/ready</code> не требуют ключа, чтобы инфраструктура могла проверять состояние. Рабочие <code>/v1/*</code> защищены, когда <code>SERVICE_API_KEY</code> непустой."
            }
          ]
        },
        {
          "id": "rezervnaya-kopiya",
          "title": "Резервная копия",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.backup create"
            },
            {
              "type": "text",
              "html": "Архив содержит только <code>*.sqlite3</code>, манифест и SHA-256. Он сохраняется в исключённой из Git папке <code>artifacts/private/backups/</code>."
            }
          ]
        },
        {
          "id": "bezopasnoe-vosstanovlenie",
          "title": "Безопасное восстановление",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.backup restore BACKUP.zip \\\n  --target data/private-restored"
            },
            {
              "type": "text",
              "html": "Программа отказывается писать в непустую папку и блокирует path traversal внутри ZIP. Рабочие базы автоматически не перезаписываются."
            }
          ]
        },
        {
          "id": "komplekt-peredachi",
          "title": "Комплект передачи",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "<code>templates/client-questionnaire.md</code>;",
                "<code>templates/acceptance-criteria.md</code>;",
                "<code>templates/handoff-checklist.md</code>;",
                "<code>templates/incident-rollback.md</code>;",
                "<code>docs/portfolio-case-study.md</code>."
              ]
            }
          ]
        },
        {
          "id": "kriterii-pass-fail",
          "title": "Критерии PASS/FAIL",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "неправильный или отсутствующий API-ключ даёт HTTP 401 без раскрытия секрета;",
                "health check остаётся доступным;",
                "backup восстанавливается с совпадающими контрольными суммами;",
                "непустая папка назначения и небезопасный ZIP отклоняются;",
                "все тесты и evaluation проходят;",
                "инструкция rollback не уничтожает единственную рабочую копию."
              ]
            }
          ]
        }
      ],
      "number": "8",
      "order": 10,
      "summary": "Подготовить сервис к контролируемой передаче: доступ, backup, acceptance criteria, rollback и честное описание ограничений.",
      "words": 169,
      "estimate_minutes": 3,
      "task": {
        "objective": [
          "Подготовить сервис к контролируемой передаче: доступ, backup, acceptance criteria, rollback и честное описание ограничений."
        ],
        "snippets": [
          {
            "section": "API-ключ",
            "language": "bash",
            "text": "python3 -c 'import secrets; print(secrets.token_urlsafe(32))'"
          },
          {
            "section": "API-ключ",
            "language": "text",
            "text": "SERVICE_API_KEY=полученное-значение"
          },
          {
            "section": "API-ключ",
            "language": "bash",
            "text": "curl http://127.0.0.1:8000/v1/rag/ask \\\n  -H \"X-API-Key: $SERVICE_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"question\":\"Каковы сроки доставки?\"}'"
          },
          {
            "section": "Резервная копия",
            "language": "bash",
            "text": "uv run python -m agent_lab.backup create"
          },
          {
            "section": "Безопасное восстановление",
            "language": "bash",
            "text": "uv run python -m agent_lab.backup restore BACKUP.zip \\\n  --target data/private-restored"
          }
        ]
      }
    },
    {
      "id": "lab-09",
      "kind": "experiment",
      "title": "Лабораторная 09: помощник аналитика маркетплейса",
      "sections": [
        {
          "id": "zadacha",
          "title": "Задача",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Сделать прототип бокового AI-помощника, который отвечает на вопросы по открытому отчёту и справочным материалам. Используем только искусственные данные — без закрытых документов и данных Wildberries."
            },
            {
              "type": "text",
              "html": "Первый сценарий: «Почему процент выкупа маленький?»"
            }
          ]
        },
        {
          "id": "pochemu-snachala-schitaem-bez-llm",
          "title": "Почему сначала считаем без LLM",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Языковая модель хорошо объясняет результат, но не должна самостоятельно придумывать цифры. Поэтому приложение:"
            },
            {
              "type": "list",
              "ordered": true,
              "items": [
                "проверяет структуру CSV;",
                "считает показатели обычным Python-кодом;",
                "выделяет товары с отклонением;",
                "разделяет факты, гипотезы и недостающие данные;",
                "передаёт проверенный результат модели только для понятного объяснения."
              ]
            }
          ]
        },
        {
          "id": "zapusk-pervogo-etapa",
          "title": "Запуск первого этапа",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -c 'from pathlib import Path; from agent_lab.marketplace_analytics import analyze_low_buyout; print(analyze_low_buyout(Path(\"data/demo/marketplace/sales-report.csv\")).model_dump_json(indent=2))'"
            }
          ]
        },
        {
          "id": "kriterii-gotovnosti",
          "title": "Критерии готовности",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "процент считается из исходных чисел;",
                "повреждённый или неполный CSV отклоняется;",
                "возможные причины не смешиваются с доказанными фактами;",
                "ответ содержит имя использованного файла;",
                "тесты работают без Ollama и интернета."
              ]
            }
          ]
        },
        {
          "id": "http-api",
          "title": "HTTP API",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "После запуска FastAPI вопрос можно отправить так:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "curl -X POST http://127.0.0.1:8000/v1/marketplace/ask \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"question\": \"Почему процент выкупа маленький?\",\n    \"report\": \"sales-report.csv\",\n    \"low_threshold\": 70\n  }'"
            },
            {
              "type": "text",
              "html": "На этом этапе API намеренно поддерживает только вопросы о выкупе. Неизвестные виды анализа отклоняются, а доступ к файлам ограничен папкой <code>MARKETPLACE_REPORTS_PATH</code>."
            }
          ]
        },
        {
          "id": "rag-i-dialogovoe-obyasnenie",
          "title": "RAG и диалоговое объяснение",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Один раз постройте отдельный индекс справочника:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.rag index \\\n  --docs data/demo/marketplace \\\n  --db data/private/marketplace-rag.sqlite3"
            },
            {
              "type": "text",
              "html": "После этого <code>/v1/marketplace/chat</code> объединит три части: точный расчёт Python, найденный фрагмент справочника и понятное объяснение Ollama."
            },
            {
              "type": "code",
              "language": "bash",
              "text": "curl -X POST http://127.0.0.1:8000/v1/marketplace/chat \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"question\":\"Почему процент выкупа маленький?\",\"report\":\"sales-report.csv\"}'"
            },
            {
              "type": "text",
              "html": "Модель не получает права менять рассчитанные метрики, а указанные ею источники проверяются по результатам RAG-поиска."
            },
            {
              "type": "text",
              "html": "Результат живого запуска зафиксирован в RESULTS.md."
            }
          ]
        },
        {
          "id": "veb-interfei-s",
          "title": "Веб-интерфейс",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Запустите сервис и откройте <a href=\"http://127.0.0.1:8000/marketplace\">http://127.0.0.1:8000/marketplace</a>:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv run uvicorn agent_lab.service:app --host 127.0.0.1 --port 8000"
            },
            {
              "type": "text",
              "html": "Страница читает выбранный CSV в браузере и отправляет содержимое в <code>/v1/marketplace/chat-upload</code> без сохранения файла на сервере. API объединяет точный расчёт, RAG и объяснение Ollama. Поддерживаются вопросы о проценте выкупа и возвратах. Для них используются отдельные пороги: 70% для низкого выкупа и 15% для высокой доли возвратов. Ограничение запроса — 5 МБ."
            },
            {
              "type": "text",
              "html": "Для сравнения периодов выберите текущий и предыдущий CSV. Страница вызовет <code>/v1/marketplace/compare-chat-upload</code>, рассчитает изменение выкупа в процентных пунктах, выделит товары со снижением и попросит Ollama объяснить результат по RAG-справочнику."
            },
            {
              "type": "text",
              "html": "Маршрут выбирается по смыслу вопроса, а не только по наличию файлов. Посторонние вопросы отклоняются с подсказкой о трёх поддерживаемых сценариях: выкуп, возвраты и сравнение периодов."
            },
            {
              "type": "text",
              "html": "После RAG-поиска источники дополнительно фильтруются по сценарию, поэтому ответ о возвратах не должен ссылаться на соседний раздел о проценте выкупа."
            }
          ]
        },
        {
          "id": "rezultaty-vypolneniya",
          "title": "Результаты выполнения",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Дата проверки: 15 августа 2026 года."
            }
          ]
        },
        {
          "id": "konfiguraciya",
          "title": "Конфигурация",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "генерация: <code>qwen3:8b</code> через локальную Ollama;",
                "embeddings: <code>qwen3-embedding:0.6b</code>;",
                "отчёт: искусственный <code>sales-report.csv</code>;",
                "справочник: <code>metrics-guide.md</code>;",
                "RAG-индекс: 2 фрагмента."
              ]
            }
          ]
        },
        {
          "id": "vopros",
          "title": "Вопрос",
          "level": 1,
          "blocks": [
            {
              "type": "note",
              "html": "Почему процент выкупа маленький?"
            }
          ]
        },
        {
          "id": "proverennye-fakty-v-otvete",
          "title": "Проверенные факты в ответе",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "общий процент выкупа: 69,13% — 159 из 230;",
                "кроссовки: 58% — 29 из 50;",
                "белая футболка: 62% — 62 из 100;",
                "модель не назвала гипотезы доказанными причинами;",
                "модель сообщила, каких данных не хватает для точной диагностики;",
                "указаны только реально найденные источники:",
                "<code>metrics-guide.md#Процент выкупа</code>;",
                "<code>metrics-guide.md#Ограничения</code>."
              ]
            }
          ]
        },
        {
          "id": "vyvod",
          "title": "Вывод",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Цепочка «проверяемый расчёт → RAG → локальная модель → валидация источников» работает на демонстрационном сценарии. Данные отчёта не отправляются во внешний облачный API."
            }
          ]
        },
        {
          "id": "scenarii-vozvratov",
          "title": "Сценарий возвратов",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Вопрос «У какого товара больше всего возвратов и что стоит проверить?» также проверен через Docker и Ollama. Помощник рассчитал:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "кроссовки: 20% — 5 возвратов из 25 выкупленных;",
                "футболка: 10% — 8 из 80;",
                "общий уровень: 12,38% — 13 из 105."
              ]
            },
            {
              "type": "text",
              "html": "Модель правильно выделила кроссовки, перечислила необходимые для диагностики данные и не назвала предполагаемую причину доказанным фактом."
            }
          ]
        },
        {
          "id": "sravnenie-periodov",
          "title": "Сравнение периодов",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Проверен сценарий с двумя CSV. Для тестовых данных система определила:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "кроссовки: снижение выкупа с 80% до 50%, то есть −30 п.п.;",
                "футболка: снижение с 80% до 70%, то есть −10 п.п.;",
                "самое сильное падение произошло у кроссовок."
              ]
            },
            {
              "type": "text",
              "html": "Сравнение выполняется проверяемым Python-кодом. Затем Ollama объясняет результат по RAG-справочнику. В живой проверке модель сохранила направление и величину изменений, не назвала гипотезы установленными причинами и сослалась на раздел <code>metrics-guide.md#Сравнение периодов</code>."
            }
          ]
        }
      ],
      "number": "9",
      "order": 11,
      "summary": "Сделать прототип бокового AI-помощника, который отвечает на вопросы по открытому отчёту и справочным материалам. Используем только искусственные данные — без закрытых документов и данных Wildberries.",
      "words": 580,
      "estimate_minutes": 4,
      "task": {
        "snippets": [
          {
            "section": "Запуск первого этапа",
            "language": "bash",
            "text": "uv run python -c 'from pathlib import Path; from agent_lab.marketplace_analytics import analyze_low_buyout; print(analyze_low_buyout(Path(\"data/demo/marketplace/sales-report.csv\")).model_dump_json(indent=2))'"
          },
          {
            "section": "HTTP API",
            "language": "bash",
            "text": "curl -X POST http://127.0.0.1:8000/v1/marketplace/ask \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"question\": \"Почему процент выкупа маленький?\",\n    \"report\": \"sales-report.csv\",\n    \"low_threshold\": 70\n  }'"
          },
          {
            "section": "RAG и диалоговое объяснение",
            "language": "bash",
            "text": "uv run python -m agent_lab.rag index \\\n  --docs data/demo/marketplace \\\n  --db data/private/marketplace-rag.sqlite3"
          },
          {
            "section": "RAG и диалоговое объяснение",
            "language": "bash",
            "text": "curl -X POST http://127.0.0.1:8000/v1/marketplace/chat \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"question\":\"Почему процент выкупа маленький?\",\"report\":\"sales-report.csv\"}'"
          },
          {
            "section": "Веб-интерфейс",
            "language": "bash",
            "text": "uv run uvicorn agent_lab.service:app --host 127.0.0.1 --port 8000"
          }
        ]
      }
    },
    {
      "id": "lab-10",
      "kind": "experiment",
      "title": "Лабораторная 10: контекстный помощник по кабинету продавца",
      "sections": [
        {
          "id": "osnovanie",
          "title": "Основание",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "После интервью и просмотра обезличенной демонстрации кабинета стало понятно, что главный сценарий — не только анализ выгрузок. Пользователю нужен помощник рядом с открытым отчётом, который:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "знает текущий экран;",
                "объясняет показатели и состав столбцов;",
                "подсказывает, где найти отчёт или метрику;",
                "даёт маршрут по интерфейсу;",
                "не выдумывает формулу, если её нет в проверенной базе знаний."
              ]
            },
            {
              "type": "text",
              "html": "Исходные видео и аудио не входят в репозиторий."
            }
          ]
        },
        {
          "id": "pervyi-etap",
          "title": "Первый этап",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Добавлен демонстрационный каталог экранов, увиденных в записи:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "воронка продаж;",
                "аналитика остатков;",
                "еженедельная динамика;",
                "отчёты реализации;",
                "перечень номенклатур;",
                "цены и скидки."
              ]
            },
            {
              "type": "text",
              "html": "<code>POST /v1/portal/ask</code> принимает вопрос и необязательный <code>current_page_id</code>. Навигационные вопросы получают маршрут, а вопросы о формулах помечаются как требующие официальной базы знаний."
            }
          ]
        },
        {
          "id": "primer",
          "title": "Пример",
          "level": 1,
          "blocks": [
            {
              "type": "code",
              "language": "bash",
              "text": "curl -X POST http://127.0.0.1:8000/v1/portal/ask \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"question\":\"Где посмотреть остатки по маркетплейсу?\"}'"
            }
          ]
        },
        {
          "id": "sleduyuschii-etap",
          "title": "Следующий этап",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Собрать проверенные определения показателей и подключить их через RAG. До этого помощник не должен отвечать на вопросы о составе и формулах метрик как на установленные факты."
            }
          ]
        }
      ],
      "number": "10",
      "order": 12,
      "summary": "После интервью и просмотра обезличенной демонстрации кабинета стало понятно, что главный сценарий — не только анализ выгрузок. Пользователю нужен помощник рядом с открытым отчётом, который:",
      "words": 143,
      "estimate_minutes": 3,
      "task": {
        "snippets": [
          {
            "section": "Пример",
            "language": "bash",
            "text": "curl -X POST http://127.0.0.1:8000/v1/portal/ask \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"question\":\"Где посмотреть остатки по маркетплейсу?\"}'"
          }
        ]
      }
    },
    {
      "id": "architecture",
      "kind": "guide",
      "title": "Архитектура сервиса",
      "sections": [
        {
          "id": "bazovye-komponenty",
          "title": "Базовые компоненты",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "<strong>Модель</strong> формирует решение или следующий шаг.",
                "<strong>Системная инструкция</strong> задаёт роль, цель и запреты.",
                "<strong>Контекст</strong> содержит текущую задачу и необходимые данные.",
                "<strong>Инструменты</strong> выполняют строго определённые действия вне модели.",
                "<strong>Agent loop</strong> повторяет цикл «решение → инструмент → результат» до завершения или лимита.",
                "<strong>Память</strong> хранит только нужное состояние между шагами или сессиями.",
                "<strong>Guardrails</strong> проверяют вход, выход, разрешения и лимиты.",
                "<strong>Evaluation</strong> показывает, насколько система решает реальные задачи."
              ]
            }
          ]
        },
        {
          "id": "kak-vybirat-model",
          "title": "Как выбирать модель",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Проверяем не рейтинг в интернете, а собственный набор задач:"
            },
            {
              "type": "list",
              "ordered": true,
              "items": [
                "качество на нужном языке и в нужной предметной области;",
                "корректность structured output и tool calling;",
                "скорость и требования к памяти/GPU;",
                "стоимость запроса;",
                "допустимость передачи клиентских данных;",
                "лицензия модели и условия провайдера."
              ]
            },
            {
              "type": "text",
              "html": "Локальные модели удобно запускать через Ollama. Облачные модели подключаются через официальный API или OpenAI-compatible провайдер. Секретные и персональные данные нельзя отправлять в облако без согласованного основания."
            }
          ]
        },
        {
          "id": "kogda-nuzhen-rag",
          "title": "Когда нужен RAG",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "RAG подходит, если ответы должны опираться на документы клиента, которые часто меняются. Документы разбиваются на фрагменты, для них считаются embeddings, затем к запросу подбираются релевантные фрагменты. Модель получает только найденный контекст и должна ссылаться на источник."
            }
          ]
        },
        {
          "id": "kogda-nuzhen-fine-tuning",
          "title": "Когда нужен fine-tuning",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Fine-tuning рассматривается, если нужно устойчиво изменить стиль, формат или повторяющееся поведение и это не удаётся получить инструкциями и примерами. Для фактов, инструкций и обновляемой базы знаний обычно лучше RAG."
            },
            {
              "type": "text",
              "html": "Перед fine-tuning необходимы:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "очищенный и законно полученный датасет;",
                "разделение train/validation/test;",
                "исходный benchmark;",
                "понятная метрика улучшения;",
                "оценка стоимости обучения и дальнейшего inference."
              ]
            }
          ]
        }
      ],
      "order": 13,
      "summary": "Проверяем не рейтинг в интернете, а собственный набор задач:",
      "words": 215,
      "estimate_minutes": 3
    },
    {
      "id": "service",
      "kind": "guide",
      "title": "Как это превращается в услугу",
      "sections": [
        {
          "id": "chto-prodae-tsya",
          "title": "Что продаётся",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Не «магический интеллект», а ограниченное решение конкретной задачи:"
            },
            {
              "type": "list",
              "ordered": false,
              "items": [
                "интервью и фиксация требований;",
                "выбор локальной или облачной модели;",
                "установка и конфигурация;",
                "подключение согласованных инструментов или документов;",
                "тестовый набор и отчёт о качестве;",
                "инструкция запуска, остановки и восстановления;",
                "передача клиенту и один согласованный период поддержки."
              ]
            }
          ]
        },
        {
          "id": "voprosy-klientu-do-ocenki",
          "title": "Вопросы клиенту до оценки",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Какую одну задачу должен решать агент?",
                "Как выглядит правильный результат?",
                "Какие данные он будет читать и где они хранятся?",
                "Какие действия ему разрешено выполнять?",
                "Должен ли он работать полностью локально?",
                "Какая ОС и характеристики компьютера?",
                "Какой допустимый бюджет на API?",
                "Какие ошибки недопустимы?",
                "Кто подтверждает внешние и необратимые действия?",
                "Как измеряется приёмка результата?"
              ]
            }
          ]
        },
        {
          "id": "minimalnye-pakety",
          "title": "Минимальные пакеты",
          "level": 1,
          "blocks": []
        },
        {
          "id": "diagnostika",
          "title": "Диагностика",
          "level": 2,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "анализ задачи и компьютера;",
                "выбор архитектуры;",
                "короткий proof of concept;",
                "письменные рекомендации."
              ]
            }
          ]
        },
        {
          "id": "lokalnyi-pomoschnik",
          "title": "Локальный помощник",
          "level": 2,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "установка модели и приложения;",
                "одна роль и до двух безопасных инструментов;",
                "тесты и инструкция."
              ]
            }
          ]
        },
        {
          "id": "agent-po-dokumentam",
          "title": "Агент по документам",
          "level": 2,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "импорт согласованного набора документов;",
                "RAG с указанием источников;",
                "набор контрольных вопросов;",
                "отчёт о качестве и ограничениях."
              ]
            }
          ]
        },
        {
          "id": "chto-yavno-ne-vhodit",
          "title": "Что явно не входит",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "обучение фундаментальной модели с нуля;",
                "круглосуточная эксплуатация без отдельного договора;",
                "действия от имени клиента без подтверждения;",
                "работа с персональными или секретными данными без согласованной защиты;",
                "гарантия безошибочности генеративной модели."
              ]
            }
          ]
        }
      ],
      "order": 14,
      "summary": "Не «магический интеллект», а ограниченное решение конкретной задачи:",
      "words": 167,
      "estimate_minutes": 3
    },
    {
      "id": "case-study",
      "kind": "guide",
      "title": "Разбор кейса",
      "sections": [
        {
          "id": "zadacha",
          "title": "Задача",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Собрать локальный сервис, который классифицирует обращения, безопасно вызывает разрешённые инструменты и отвечает по документам с проверяемыми источниками."
            }
          ]
        },
        {
          "id": "reshenie",
          "title": "Решение",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "Ollama и <code>qwen3:8b</code> для локальной генерации;",
                "Pydantic и JSON Schema для контрактов;",
                "allowlist из двух узких инструментов;",
                "SQLite для памяти, audit log и небольшого векторного индекса;",
                "<code>qwen3-embedding:0.6b</code> для поиска по русскоязычным документам;",
                "FastAPI и Docker Compose для локального API;",
                "pytest и эталонный RAG-набор для измеримой проверки."
              ]
            }
          ]
        },
        {
          "id": "proverennyi-rezultat",
          "title": "Проверенный результат",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "41 тест после добавления API-защиты и backup/restore;",
                "RAG evaluation: 6/6 сценариев, retrieval hit rate 100%;",
                "корректный отказ на вопрос, ответа на который нет в документах;",
                "контейнер ARM64 проходит health/readiness и обращается к Ollama хоста."
              ]
            },
            {
              "type": "text",
              "html": "Цифры относятся к демонстрационному набору и не переносятся автоматически на данные клиента."
            }
          ]
        },
        {
          "id": "obnaruzhennaya-oshibka",
          "title": "Обнаруженная ошибка",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "На первом сквозном тесте модель перепутала цену <code>2490</code> с числом <code>1001</code> из ID заказа. Универсальный калькулятор был заменён узким инструментом <code>calculate_order_total</code>, который получает цену из доверенного справочника. Это устранило возможность самостоятельно подставить цену."
            }
          ]
        },
        {
          "id": "ogranicheniya",
          "title": "Ограничения",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "demo-документы вымышлены;",
                "SQLite-поиск рассчитан на небольшой объём;",
                "локальная модель может ошибаться;",
                "публичный доступ требует TLS, управления секретами, лимитов запросов и мониторинга;",
                "качество нового клиентского набора должно измеряться отдельно."
              ]
            }
          ]
        }
      ],
      "order": 15,
      "summary": "Собрать локальный сервис, который классифицирует обращения, безопасно вызывает разрешённые инструменты и отвечает по документам с проверяемыми источниками.",
      "words": 170,
      "estimate_minutes": 3
    },
    {
      "id": "templates",
      "kind": "appendix",
      "title": "Шаблоны для работы с клиентом",
      "sections": [
        {
          "id": "funkcionalnost",
          "title": "Функциональность",
          "level": 1,
          "blocks": [
            {
              "type": "checklist",
              "items": [
                "<code>GET /health</code> возвращает HTTP 200.",
                "<code>GET /ready</code> подтверждает Ollama и RAG-индекс.",
                "Эталонный evaluation-набор согласован с клиентом.",
                "Достигнут согласованный pass rate: ___%.",
                "Каждый RAG-ответ содержит разрешённый источник или честный отказ."
              ]
            }
          ]
        },
        {
          "id": "bezopasnost",
          "title": "Безопасность",
          "level": 1,
          "blocks": [
            {
              "type": "checklist",
              "items": [
                "Сервис слушает только согласованный сетевой интерфейс.",
                "Рабочие эндпоинты защищены API-ключом, если доступны не только локально.",
                "Секреты отсутствуют в Git и образе контейнера.",
                "Инструменты ограничены allowlist и проверяемыми аргументами.",
                "Внешние и изменяющие действия требуют подтверждения человека."
              ]
            }
          ]
        },
        {
          "id": "ekspluataciya",
          "title": "Эксплуатация",
          "level": 1,
          "blocks": [
            {
              "type": "checklist",
              "items": [
                "Инструкция запуска проверена на компьютере клиента.",
                "Создана и проверена резервная копия.",
                "Восстановление выполнено в отдельную пустую папку.",
                "Назначены ответственные за документы, доступ и обновления.",
                "Зафиксирована процедура rollback."
              ]
            },
            {
              "type": "text",
              "html": "Дата: __________  Клиент: __________  Исполнитель: __________"
            }
          ]
        },
        {
          "id": "anketa-klienta",
          "title": "Анкета клиента",
          "level": 1,
          "blocks": []
        },
        {
          "id": "zadacha",
          "title": "Задача",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Какую одну задачу должен решать агент?",
                "Кто будет им пользоваться?",
                "Как выглядит правильный результат?",
                "Какие ошибки недопустимы?"
              ]
            }
          ]
        },
        {
          "id": "dannye",
          "title": "Данные",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Какие документы и системы разрешено читать?",
                "Есть ли персональные, коммерческие или другие защищаемые данные?",
                "Где данные должны храниться и как долго?",
                "Кто отвечает за актуальность исходных документов?"
              ]
            }
          ]
        },
        {
          "id": "dei-stviya-i-dostup",
          "title": "Действия и доступ",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Какие инструменты разрешены?",
                "Какие действия требуют подтверждения человека?",
                "Должен ли сервис быть доступен за пределами одного компьютера?",
                "Кто выдаёт и отзывает доступ?"
              ]
            }
          ]
        },
        {
          "id": "prie-mka-i-ekspluataciya",
          "title": "Приёмка и эксплуатация",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Какие контрольные вопросы войдут в evaluation-набор?",
                "Какой минимальный pass rate принимается?",
                "Кто следит за журналом ошибок и резервными копиями?",
                "Какой срок поддержки нужен после передачи?"
              ]
            }
          ]
        },
        {
          "id": "chek-list-peredachi",
          "title": "Чек-лист передачи",
          "level": 1,
          "blocks": [
            {
              "type": "checklist",
              "items": [
                "Репозиторий или архив исходников передан клиенту.",
                "Версии Ollama, моделей, Docker и Python записаны.",
                "Модели скачаны на целевой компьютер.",
                "<code>.env</code> создан локально и не добавлен в Git.",
                "RAG-индекс построен из согласованных документов.",
                "Все тесты и evaluation пройдены.",
                "Клиент умеет запустить, проверить и остановить сервис.",
                "Клиент знает, где находятся логи и приватные данные.",
                "Создан backup; восстановление проверено отдельно.",
                "Ограничения и известные ошибки объяснены.",
                "Срок и канал поддержки согласованы."
              ]
            }
          ]
        },
        {
          "id": "incident-i-rollback",
          "title": "Инцидент и rollback",
          "level": 1,
          "blocks": []
        },
        {
          "id": "kogda-ostanovit-servis",
          "title": "Когда остановить сервис",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": false,
              "items": [
                "ответы систематически не соответствуют документам;",
                "обнаружена утечка секрета или приватных данных;",
                "агент вызывает неожиданные инструменты;",
                "повреждён индекс или база памяти;",
                "обновление ухудшило evaluation ниже согласованного порога."
              ]
            }
          ]
        },
        {
          "id": "nemedlennye-dei-stviya",
          "title": "Немедленные действия",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Остановить контейнер: <code>docker compose down</code>.",
                "Не удалять логи и повреждённые данные до анализа.",
                "Отозвать скомпрометированный API-ключ и создать новый вне Git.",
                "Зафиксировать время, симптомы и последние изменения."
              ]
            }
          ]
        },
        {
          "id": "rollback-koda",
          "title": "Rollback кода",
          "level": 1,
          "blocks": [
            {
              "type": "list",
              "ordered": true,
              "items": [
                "Найти последний принятый Git-коммит.",
                "Создать отдельную ветку или рабочую копию на этом коммите.",
                "Собрать образ и выполнить тесты/evaluation.",
                "Переключить сервис только после успешной проверки."
              ]
            },
            {
              "type": "text",
              "html": "Не используйте <code>git reset --hard</code> на единственной рабочей копии с несохранёнными данными."
            }
          ]
        },
        {
          "id": "vosstanovlenie-dannyh",
          "title": "Восстановление данных",
          "level": 1,
          "blocks": [
            {
              "type": "text",
              "html": "Восстановление выполняется в новую пустую папку:"
            },
            {
              "type": "code",
              "language": "bash",
              "text": "uv run python -m agent_lab.backup restore BACKUP.zip \\\n  --target data/private-restored"
            },
            {
              "type": "text",
              "html": "После проверки файлов путь <code>RAG_DB_PATH</code> меняется на восстановленную базу. Исходная база не перезаписывается автоматически."
            }
          ]
        }
      ],
      "order": 16,
      "summary": "Дата: __________  Клиент: __________  Исполнитель: __________",
      "words": 373,
      "estimate_minutes": 3
    }
  ],
  "totals": {
    "units": 16,
    "experiments": 10,
    "words": 4275,
    "estimate_minutes": 51
  }
}
