Неделя 9. Productionize the Capstone
- Что уже умеем
- Спроектировать автоматизацию и обосновать архитектуру Capstone.
- Какую проблему решаем
- Рабочий workflow ещё не доступен другим системам и не сохраняет состояние между HTTP-запросами.
- Что нового появится
- Production Layer для существующего Capstone: API, persistent state, внешняя интеграция, async execution, Docker, observability, CI и KPI.
- Что получится в конце
- Production-like сервис, связанный с измеримым outcome из solution design Week 8.
🧭 Где мы: LLM → Context / State → Tools → [Runtime / Orchestration] → Policy / Permissions → Evaluation / Observability.
Продолжаем тот же Capstone
У нас уже есть спроектированный и оценённый Capstone. Теперь превращаем его в production-like сервис: добавляем границу API, persistence, безопасную интеграцию, эксплуатационную проверку и сопоставляем результат с бизнес-целями из Week 8.
Вспомни прошлую неделю
- Какой основной выбор Week 8 фиксирует границу автоматизации для Capstone?
- Где в предыдущем workflow уже есть
State,RunPolicyиHumanGate?
Core vs Extension
Core: productionize the same Capstone, not build a new agent from scratch. Для зачёта важно показать, как Workflow из прошлых недель становится service boundary: API, persistent state, side effects through tool policy, async execution, observability.
Extension: конкретные SDK/provider details, optional async framework tuning, advanced deployment patterns, extra monitoring and queue tools. Это полезно, но не является обязательным для понимания Week 9.
Главный смысл: runtime и business workflow уже известны; Week 9 показывает, как их безопасно вынести в работающий production-like сервис.
Production Layer: от agent workflow к production-сервису
Capstone не заканчивается работающим agent workflow. Следующий инженерный шаг — productionize то, что построено: завернуть в API, сохранить состояние, подключить реальную внешнюю систему, безопасно выполнять side effects, контейнеризировать, наблюдать, тестировать и измерить бизнес-эффект.
Принцип: студент не строит нового агента — он productionizes уже существующего.
Архитектура Production Layer
%%{init: {"flowchart": {"useMaxWidth": false, "nodeSpacing": 20, "rankSpacing": 30, "wrappingWidth": 500}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart TD
Client["Client\n(HTTP)"] --> FastAPI["FastAPI Service\nPOST /tasks\nGET /tasks/{id}\nGET /health"]
FastAPI --> TaskStore[("Task Store\nPostgreSQL")]
FastAPI --> Runtime["AgentRuntime"]
Runtime --> Planner["ScriptedPlanner\n(deterministic)"]
Planner --> Policy["Policy + HumanGate"]
Policy --> Tools["Tool execution\nGitHub / API / shell"]
Tools --> External["External API\nGitHub / др."]
Runtime --> Results[("Results\nPersistent State")]
Runtime --> Observability["Logs / Traces / Eval\n(Week 7)"]
Model["ModelClient\n(Week 4 extension point;\nnot connected)"]
classDef svc fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
classDef db fill:#e3f2fd,stroke:#1565c0,stroke-width:2px;
classDef ext fill:#fff3e0,stroke:#ef6c00,stroke-width:2px;
classDef core fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px;
class FastAPI,Runtime,Planner,Policy svc;
class TaskStore,Results db;
class Model,External ext;
Production Layer uses deterministic ScriptedPlanner; ModelClient remains an extension point introduced in Week 4 and is not connected to this runtime.
Этап 1 — Service Boundary: FastAPI
Минимальный production-facing API вокруг существующего AgentRuntime:
POST /tasks → 202 Accepted, создаёт execution, возвращает task_id
GET /tasks/{task_id} → статус, timestamps, result, error, execution_id
GET /health → liveness probe
POST /tasks не блокирует HTTP request на всё время работы агента. Возвращает сразу:
{
"task_id": "uuid",
"status": "queued"
}
GET /tasks/{task_id} возвращает:
- status: queued | running | completed | failed | needs_approval | blocked
- created_at, started_at, completed_at
- result / error
- execution_id (run_id из trace)
Контракт статусов:
| Статус | Значение | completed_at |
|---|---|---|
queued |
Задача создана, выполнение не началось | null |
running |
Выполняется в background | null |
completed |
Успешно завершено | установлено |
failed |
Ошибка выполнения | установлено |
needs_approval |
Сохранённый non-terminal snapshot остановленного execution на HumanGate | null |
blocked |
Действие отклонено до исполнения: policy, неизвестный tool, repository вне allowlist, недостающий scope, invalid arguments или отклонённое human approval | установлено |
blocked отличается от failed:
- failed — системная ошибка после старта выполнения, например исключение, timeout, provider unavailable или исчерпание retry budget;
- blocked — политика отклонила действие до исполнения, причина в error/trace.
blocked и failed — terminal-статусы и фиксируют completed_at. needs_approval — non-terminal persisted snapshot остановленного execution на HumanGate; completed_at = null. Текущий сервис не предоставляет resume endpoint, поэтому это не полноценное возобновляемое состояние очереди, не success и не completed execution.
Важно: после исчерпания retry budget для безопасной операции execution заканчивается как
failed, а не какblocked.blockedозначает «действие не было разрешено до исполнения».
Цель: показать, как agent workflow становится сервисом, доступным другим системам.
Этап 2 — Persistent State: PostgreSQL
Текущий State курса (in-memory) ≠ persistent application state.
Минимальные сущности:
| Таблица | Поля |
|---------|------|
| tasks | id (PK), status, created_at, started_at, completed_at, payload (JSON), result (JSON), error (text) |
| executions | id (PK), task_id (FK), run_id, status, started_at, completed_at, trace (JSONB) |
Стек: SQLAlchemy 2.x + psycopg (async). Миграции через Alembic (опционально для учебного курса — create_all достаточно).
Короткие и длинные запуски агента
Короткий agent run завершается в одном ограниченном цикле. Долгий запуск может прерваться, исчерпать контекст или продолжиться с устаревшими предположениями. Одного while not done: call_model() недостаточно — нужен внешний harness (обвязка), которая сохраняет прогресс:
flowchart LR
T["Task"] --> R["Run"] --> C[("Checkpoint: состояние сохранено")]
C --> F["Сбой или пауза"] --> RS["Resume с checkpoint"] --> N["Next step"] --> R
Checkpoint хранит, какой узел завершён, какие артефакты существуют и что ждёт подтверждения. Compaction сокращает контекст модели, но не сохраняет результат действия и не заменяет checkpoint. Idempotency — свойство действия, которое можно повторить без вреда; она определяет, допустим ли повтор после таймаута.
Для долгого запуска нужны persistent state, проверяемое восстановление, обновление устаревших предположений, bounded retries и условия остановки. Свяжите восстановление с human approval для значимых side effects и trace: после сбоя должно быть понятно, что уже произошло и почему. Подробная практика checkpoint/resume и idempotency остаётся расширением в треке автономных агентов.
Этап 3 — Реальная внешняя интеграция
🤔 Predict
Внешний API получил запрос на запись, но соединение оборвалось до ответа. Можно ли безопасно повторить такой запрос автоматически, если неизвестно, применилось ли первое действие?
Используем существующий IssueApiClient (GitHub API) из Integration Lab. Уже реализовано:
- Authentication (Bearer token)
- Explicit tool schemas (ToolSpec)
- Read vs write tools (list_issues/get_issue vs create_issue/close_issue)
- Permissions через Policy (allowlist репозиториев, scopes)
- Error handling с маппингом HTTP кодов
- Timeout/retry с идемпотентностью
- Side effects требуют human approval
- Tracing через TraceEvent
Достаточно одной реальной интеграции. Не подключайте GitHub + Telegram + CRM одновременно.
Этап 4 — Async / Background Execution
Production agent не выполняется внутри HTTP request:
POST /tasks
↓
202 Accepted (task_id)
↓
task = queued
↓
BackgroundTasks / asyncio task
↓
task = running
↓
AgentRuntime.run(task)
↓
task = completed / failed
↓
Result persisted
Механизм: FastAPI BackgroundTasks или asyncio.create_task(). Не добавляйте Kafka/RabbitMQ/Celery — цель понять async execution model, а не изучать очереди.
Ограничение BackgroundTasks
Production Layer использует FastAPI BackgroundTasks — это process-local execution.
restart/crashпроцесса не является durable queue- queued/running jobs не имеют automatic recovery
- для production deployment используйте Celery/RabbitMQ/Kafka
Async execution details (#3):
execute_agent_task — async function, но runtime.run() вызывает IssueApiClient, который использует blocking urllib.request.urlopen(). Blocking I/O выносится в thread через asyncio.to_thread, чтобы event loop оставался responsive. Если retry budget исчерпан после повторов безопасного чтения и результат всё ещё не получен, выполнение сохраняется как failed и не маскируется под blocked:
# runtime.run() → IssueApiClient → urllib.request.urlopen() is blocking I/O.
report = await asyncio.to_thread(runtime.run, task_description)
Это минимальное изменение: не требует async HTTP framework, не меняет архитектуру IssueApiClient. Альтернатива — полный переход на httpx.AsyncClient, но она увеличивает scope без необходимости для учебного курса.
Тесты и sleep (#15):
Интеграционные тесты используют deterministic polling вместо фиксированных sleep():
# Хорошо: polling с timeout
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if condition:
return result
await asyncio.sleep(poll_interval)
# Плохо: фиксированный sleep
await asyncio.sleep(2) # flaky
IssueApiClient использует time.sleep() только внутри exponential backoff между retry — это не тест, а часть production logic.
Этап 5 — Configuration и Secrets
Разделение:
Code
≠
Configuration (.env, pydantic-settings)
≠
Secrets (environment variables only)
Покажите:
- .env.example с placeholder'ами
- DATABASE_URL, GITHUB_TOKEN
- Никаких секретов в коде, промптах, коммитах, .env.example
Безопасность GitHub credentials (#10):
Рекомендуется fine-grained personal access token (не classic PAT):
one token
→ one repository
→ minimum required permissions
Минимальные permissions для Production Layer (Issues API): - Issues — read + write (list, get, create, close) - Metadata — read (требуется GitHub API) - Contents — не требуется (этот flow работает только через Issues)
Classic PAT со scope repo не рекомендуется — он даёт доступ ко всем репозиториям, к которым у токена есть доступ. Для учебного курса достаточно одного репозитория с минимальными permissions.
Используйте .env (в .gitignore) и environment variables — никаких токенов в коде, коммитах или .env.example.
Используйте pydantic-settings для type-safe config.
Этап 6 — Docker Compose
Минимальное production-like окружение:
# docker-compose.yml
services:
api:
build: .
ports: ["8000:8000"]
environment:
- DATABASE_URL=postgresql+psycopg://postgres:postgres@db:5432/agent_course
- GITHUB_TOKEN=${GITHUB_TOKEN}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 10s
timeout: 5s
retries: 5
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=agent_course
ports: ["5432:5432"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Команда одинакова в Windows PowerShell, macOS Terminal и Linux shell; её выполняют из каталога с compose-файлом:
docker compose up
Этап 7 — Observability
Используйте существующие материалы курса (tracing, evaluation). Production Layer показывает end-to-end trace:
HTTP request
↓
task_id
↓
agent execution
↓
tool calls
↓
external side effect (GitHub API)
↓
result
Минимальное логирование (structured JSON):
| Поле | Что это | Где хранится |
|---|---|---|
task_id |
UUID задачи (PK Task) | Task, Execution, логи |
run_id |
Идентификатор agent run (run-{uuid}) |
Execution.run_id, TraceEvent |
execution_id |
В API = run_id (совпадает с Week 9) |
GET /tasks/{id} |
status |
queued / running / completed / failed / needs_approval / blocked | Task, Execution |
latency_ms |
Время выполнения | логи |
tool_calls |
Имя, аргументы (redacted), статус | TraceEvent, Execution.trace |
errors |
Текст ошибки и тип | Task.error, Execution.trace |
request_id |
Генерируется middleware для каждого HTTP request; привязан к structlog context. Не сохраняется в БД. | логи |
request_id и run_id коррелируют: один HTTP request → один request_id; один background execution → один run_id. Для корреляции request → task используйте task_id в логах.
Не добавляйте ELK/Grafana/Prometheus — цель: видеть, что произошло при одном production execution.
Этап 8 — Error Handling
Production service не падает исключением. Ошибки делятся на два уровня:
Синхронные ошибки — возвращаются непосредственно в ответ на HTTP request:
| Ошибка | HTTP Status | Response |
|---|---|---|
| Invalid input (validation) | 422 | {"detail": [...]} |
| Task not found | 404 | {"detail": "Task not found"} |
| Permission denied | 403 | {"detail": "Insufficient permissions"} |
| Database error | 500 | {"detail": "Internal server error"} |
Ошибки в background execution — не становятся HTTP-статусом исходного POST. POST /tasks всегда возвращает 202 Accepted. Ошибка сохраняется в Task/Execution, и GET /tasks/{task_id} возвращает её в поле error:
| Ошибка | Где видна |
|---|---|
| Provider failure (model) | GET /tasks/{id} → error |
| Timeout | GET /tasks/{id} → error |
| Tool failure (external API) | GET /tasks/{id} → error |
| Agent execution error | GET /tasks/{id} → error, status=failed |
POST /tasks
→ 202 Accepted (task_id, status=queued)
background execution
→ error persisted in Task/Execution
GET /tasks/{task_id}
→ current status + error
Различайте: - Expected business error → 4xx, понятное сообщение, retry не поможет - Unexpected system failure → 5xx, логируется, можно ретраить
Этап 9 — CI
Минимальный pipeline (GitHub Actions):
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: agent_course
ports: [5432:5432]
options: --health-cmd="pg_isready -U postgres" --health-interval=5s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: {python-version: "3.11"}
- run: pip install -e ".[dev]"
- run: pytest
- run: python -m pyright # если настроен
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t agent-course-capstone .
Docker image проверяется через build. Публикация в registry — опционально.
Этап 10 — Business KPI
Финальный отчёт capstone должен показывать не «сервис работает», а «сервис улучшает бизнес-процесс»:
Baseline (ручной процесс)
↓
Production agent
↓
Quality (success rate, error rate)
↓
Latency (p50, p95)
↓
Cost ($/task, tokens/task)
↓
Human intervention rate (%)
↓
Business impact (time saved, automation rate)
Пример KPI report:
## KPI Report
**Manual process:** 15 min / task
**Agent-assisted:** 4 min / task
**Automation rate:** 78%
**Human review required:** 22%
**Success rate:** 91%
**Average cost:** $0.08 / task (model tokens)
**p50 latency:** 12s
**p95 latency:** 28s
Цифры — реальные для учебного эксперимента или явно помечены как example.
Production Checklist
В конце Week 9 студент проходит checklist. Это production-oriented educational service — не production-ready система.
[ ] API: POST /tasks, GET /tasks/{id}, GET /health
[ ] Persistent state: PostgreSQL + SQLAlchemy
[ ] External integration: ≥1 real API (GitHub)
[ ] Permissions: allowlist + scopes + approval
[ ] Secrets: .env, pydantic-settings, no secrets in code
[ ] Async execution: BackgroundTasks / asyncio
[ ] Retries: exponential backoff (idempotent reads), no automatic retry for writes
[ ] Structured logs: JSON, task_id, run_id, execution_id, trace correlation
[ ] Tracing: end-to-end execution trace
[ ] Evaluation: quality + cost + latency metrics
[ ] Docker: docker-compose up works
[ ] CI: pytest + lint + typecheck + docker build
[ ] KPI: baseline vs agent report
Режимы работы:
| Режим | RUNNER_MODE | External API | Side effects |
|---|---|---|---|
| Test / CI | test |
none | none |
| Demo | demo |
mock or real (opt-in) | deterministic ToolCall, DEMO_APPROVE_WRITES |
| Real integration | demo + real token |
real GitHub | explicit opt-in required |
Демонстрация:
Команда ниже одинакова в Windows PowerShell, macOS и Linux и выполняется из корня репозитория:
docker compose -f labs/integration-lab/docker-compose.yml up --build
Текущий Compose-файл запускает безопасный RUNNER_MODE=test; shell-переменная перед командой Compose не включает demo. Кроссплатформенная инструкция для локального demo с явной настройкой .env и opt-in записи приведена в Production README лаборатории Integration Lab в исходном репозитории; описание лаборатории на сайте — Integration Lab.
Это не новые концепции — итоговый checklist всего курса.
Retry / backoff (#8)
Production Layer использует exponential backoff с jitter в IssueApiClient:
backoff(attempt) = min(backoff_base × 2^(attempt-1), backoff_max)
jittered = backoff × uniform(0.5, 1.0)
Для DEFAULT_MAX_RETRIES=3: задержки ~0.25–0.5s, 0.5–1s, 1–2s (каприруются на backoff_max=4s).
Jitter предотвращает синхронные повторы при rate limit — несколько клиентов не бьют в API одновременно. Ограничен: тесты проверяют диапазон, а не конкретное значение.
Явно разделены retryable и non-retryable ошибки:
| Ошибка | Retryable? | Поведение |
|---|---|---|
5xx на чтении (GET) |
Да | Повтор с backoff в пределах бюджета, затем failed |
| Connection reset на чтении | Да | Повтор с backoff |
5xx на записи (POST/PATCH) |
Нет | WriteOutcomeUnknownError — эффект мог примениться, повтор не выполняется автоматически |
| Connection lost на записи | Нет | WriteOutcomeUnknownError — повтор не выполняется |
| Timeout на чтении | Да | Повтор с backoff, затем TransportTimeoutError и failed |
| Timeout на записи | Нет | WriteOutcomeUnknownError — запрос отправлен, эффект мог примениться |
401 / 403 |
Нет | AuthenticationError / PermissionDeniedError — тихих повторов нет |
404 |
Нет | NotFoundError |
400 / 409 / 413 / 422 |
Нет | ValidationRejectedError |
Правило: повторы разрешены только для идемпотентных чтений (GET). Записи (POST/PATCH/DELETE) не повторяются автоматически — это защищает от дубликатов.
Практика
Возьмите solution design из Week 8 и проверьте его как сервис:
- Запустите
RUNNER_MODE=testи проверьте/healthбез GitHub token. - Создайте задачу через
POST /tasks; получите её состояние черезGET /tasks/{task_id}. - Проследите
task_id,execution_id, status, timestamps и trace до результата. - Запустите deterministic demo на локальном mock API и проверьте policy, approval и side effect.
- Заполните measured значения KPI и сравните их с baseline и target из Week 8.
Не включайте реальный GitHub token для smoke test и не выполняйте demo-запись в реальном репозитории без явного разрешения.
🎉 What you can do now
Вы можете предоставить существующий workflow как наблюдаемый сервис, сохранив policy и human approval на внешних действиях.
Результат недели
- тот же Capstone доступен через API и сохраняет lifecycle в persistent state;
- внешнее действие проходит через tools, policy и human approval;
- Docker smoke и CI проверяют сервис, а trace объясняет выполнение;
- measured KPI сопоставлены с целями solution design Week 8.
Итог Week 9: выбранное решение поставлено как проверяемый production-like сервис; архитектура остаётся той, что была обоснована в Week 8.
Проверь себя
- Чем
blockedотличается отfailedв production execution? - Почему текущий Production Layer работает с
ScriptedPlanner, аModelClientостаётся неподключённой extension point? - Какой outcome из Week 8 должен стать измеримым KPI в Week 9?
Резюме: Productionize Capstone
Capstone — не новый проект, а production-ization уже спроектированной системы. Week 9 фокусируется на поставке solution design из Week 8 как сервиса:
| Фокус Week 9 | Уже освоено раньше |
|---|---|
| Service boundary — API, async execution | Workflow, handoff, runtime — Weeks 2, 5 |
| Persistence — PostgreSQL, SQLAlchemy | State, context — Week 3, 5 |
| Packaging — Docker, Docker Compose, CI | Mock provider, tests — Weeks 4, 7 |
| Observability — structured logs, traces, KPI | Trace, eval, метрики — Week 7 |
| KPI report — measured business impact | Baseline, comparison — Weeks 1, 7, 8 |
Обоснование solution design уже оформлено в Week 8. Здесь мы реализуем выбранную границу автоматизации, проверяем эксплуатационный путь и сравниваем измеренный outcome с целями.
Итог курса
После основного курса вы умеете:
- определить, нужен ли для задачи агент вообще, и выбрать single-agent или multi-agent архитектуру, обосновав выбор baseline/eval;
- декомпозировать workflow на роли и проверяемые handoff-контракты;
- проектировать state/context и подключать tools через runtime;
- ограничивать permissions, применять allowlist и ставить human approval;
- задавать отказные переходы, bounded retry, escalation и конечное состояние;
- количественно оценивать задачу, регрессии, scope, стоимость и latency;
- анализировать traces и находить причину неверного перехода;
- отделять неизменную архитектуру от заменяемых моделей, провайдеров и фреймворков;
- завернуть agent workflow в production API (FastAPI);
- сохранять состояние в PostgreSQL через SQLAlchemy;
- подключать внешнюю систему с auth, permissions и retries;
- запускать agent асинхронно вне HTTP request;
- управлять конфигурацией и секретами через environment variables;
- контейнеризировать сервис через Docker Compose;
- наблюдать за execution через structured logs и traces;
- настроить единый CI pipeline;
- измерить бизнес-эффект через KPI report.
✅ Definition of Done
- Объяснить, как API, persistence и background execution оборачивают прежний Capstone workflow, не заменяя его новой архитектурой.
- Проследить задачу через API, persistent status, execution trace и результат.
- Запустить проверки сервиса на mock/deterministic режиме и подтвердить, что запрещённое действие не исполняется.
- Различить
blocked,failedиneeds_approval; не повторять неоднозначную запись без гарантии идемпотентности. - Сопоставить измеренный KPI с baseline и target из Week 8 и назвать эксплуатационный риск.
Навигация
| ← | Week 8: Business Automation & Solution Design |
| → | К итоговому Capstone |