# Как проектировать ИИ-систему: схема, компоненты и типичные ошибки

> AI Release · @ai_release1 · https://ai-release.org/guides/kak-proektirovat-ii-sistemu.html

## TL;DR

- Время выполнения: 90 минут (схема + минимальная обвязка + проверка)
- Уровень сложности: Intermediate
- Понадобится: Python 3 (`python --version`; поддерживаемую версию сверьте с документацией вашего inference-сервера), FastAPI + uvicorn, Docker и docker compose, PostgreSQL, Qdrant, S3-совместимое хранилище, запущенный сервер LLM (например, vLLM), `curl`, `jq`

## Требования и подготовка
![Иллюстрация к разделу «Требования и подготовка»](https://ai-release.org/guides/img/kak-proektirovat-ii-sistemu-ru-1.jpg?v=1793781342)


- ОС: Linux или macOS; для локальных экспериментов с GPU — машина с достаточной видеопамятью (в разборе Tiel-Coder-35B-A3B-MTP речь о RTX 4070 Ti SUPER с 16 ГБ GDDR6X).
- Права: возможность поднимать контейнеры (`docker ps` без sudo) и слушать локальные порты.
- Пакеты Python: `fastapi`, `uvicorn`, `psycopg[binary]`, `qdrant-client`, `boto3`, `pyyaml`.
- API-ключ или локальный endpoint вашей модели; сам ключ в репозиторий не коммитить.
- Свободное место: под веса модели, снапшоты PostgreSQL и коллекцию Qdrant — оцените по размеру ваших документов, конкретное число зависит от корпуса.
- Понимание двух терминов: LLM — языковая модель, которая генерирует текст; RAG — когда модель сначала ищет нужные документы в вашей базе, а потом отвечает на их основе.

```bash
python -m venv .venv && source .venv/bin/activate
pip install fastapi uvicorn "psycopg[binary]" qdrant-client boto3 pyyaml
python -c "import fastapi, qdrant_client, yaml; print('deps ok')"
```

Ожидаемый вывод: `deps ok`. Если пакет не ставится — проверьте, что venv активирован (в приглашении появится `(.venv)`).

## Шаг 1. Зафиксировать схему и границы системы

**Что сделать:** опишите систему как поток данных, а не как «одну модель». Минимальный набор блоков: входной API, обвязка (harness — слой, который знает про пользователя, права, лимиты и правила вызова модели), LLM, RAG-хранилища (S3 — файлы документов, PostgreSQL — метаданные, Qdrant — векторный индекс), интеграции с инструментами, мониторинг.

**Зачем:** запуск LLM на GPU — только первый шаг; модель сама по себе не знает пользователя, контекст и правила доступа, поэтому рабочий сервис — это модель плюс обвязка. По оценке из разбора про обвязку LLM, из 33 корпоративных ИИ-пилотов до продакшена доходят в среднем только четыре, то есть 88% остаются экспериментом — обычно именно из-за отсутствия спроектированной обвязки.

Зафиксируйте состав в файле `system.yaml`:

```yaml
project: support-rag
components:
  api:        {type: fastapi, port: 8000}
  llm:        {type: vllm, endpoint: "http://127.0.0.1:8000/v1"}
  rag_store:  {type: qdrant, port: 6333, collection: docs}
  meta_store: {type: postgresql, port: 5432, db: rag}
  object_store: {type: s3, bucket: docs-raw}
  monitoring: {type: logs}
limits:
  max_input_tokens: 8000
  request_timeout_s: 60
```

Порты `8000/6333/5432` — стандартные для uvicorn, Qdrant и PostgreSQL в примерах конфигурации; если они заняты, замените значения и сверьтесь с документацией образов.

**Пример:** таблица компонентов, которую вы переносите в тикет или README.

| Компонент | Роль | Что потеряется без него |
|---|---|---|
| API | входная точка, права, лимиты | любой пользователь может звать модель напрямую |
| RAG-хранилища | документы, метаданные, индекс | ответы «из головы» модели |
| Мониторинг | логи запросов и версий | тихие сбои не видны |
| Конфиг | смена поведения без пересборки | каждая правка = релиз |

**Как проверить:** распарсите файл и убедитесь, что все блоки на месте.

```bash
python -c "import yaml;d=yaml.safe_load(open('system.yaml'));print(sorted(d['components']))"
```

Ожидаемый вывод: `['api', 'index'... ]` — ровно шесть ключей: `api, llm, meta_store, monitoring, object_store, rag_store`. Если ключей меньше — блок потерян на схеме, и его не будет в коде.

Структуру папок агента (где лежат промпты, инструменты, конфиги) удобно взять из готового шаблона: [ICM на практике: структура папок, которая делает ИИ-агента управляемым](https://ai-release.org/guides/icm-praktika-struktura-papok-agenta.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

## Шаг 2. Собрать обвязку: API, RAG, лимиты, конфигурируемость
![Иллюстрация к разделу «Шаг 2. Собрать обвязку: API, RAG, лимиты, конфигурируемость»](https://ai-release.org/guides/img/kak-proektirovat-ii-sistemu-ru-2.jpg?v=1793781347)


**Что сделать:** поднимите хранилища, затем напишите сервис на FastAPI, который принимает запрос, проверяет лимит, достаёт документы из Qdrant, подставляет их в промпт и логирует версию индекса.

**Зачем:** обвязка превращает голую модель в сервис: добавляет RAG с корпоративными данными, интеграции с инструментами, лимиты и мониторинг. Второй принцип — конфигурируемость: в проекте Android-SCADA универсальность достигнута за счёт JSON-конфигурации, для нового объекта не нужна пересборка приложения, достаточно заменить файл. В ИИ-системе работает то же правило: новое поведение — новый конфиг, а не новый релиз кода.

**Пример:** конфиг хранилищ и запуск.

```yaml
# docker-compose.yml
services:
  qdrant:
    image: qdrant/qdrant
    ports: ["6333:6333"]
  postgres:
    image: postgres
    environment:
      POSTGRES_DB: rag
      POSTGRES_PASSWORD: <пароль>
    ports: ["5432:5432"]
```

```bash
docker compose up -d
curl -s http://127.0.0.1:6333/collections | jq .
```

Ожидаемый вывод: JSON с полем `"result": {"collections": []}` — Qdrant жив, коллекций пока нет. Пароль `<пароль>` замените своим и держите в `.env`, а не в git.

Минимальный сервис с проверкой лимита:

```python
import os, time
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

MAX_INPUT_CHARS = int(os.getenv("MAX_INPUT_CHARS", "8000"))
app = FastAPI()

class Query(BaseModel):
    user_id: str
    text: str

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/v1/answer")
def answer(q: Query):
    if len(q.text) > MAX_INPUT_CHARS:
        raise HTTPException(status_code=413, detail="input too long")
    t0 = time.time()
    # здесь: поиск в Qdrant -> сборка промпта -> вызов LLM
    return {"user_id": q.user_id, "latency_ms": int((time.time()-t0)*1000)}
```

Почему это работает: лимит отсекает запросы до вызова модели, поэтому стоимость и задержка предсказуемы; `/health` даёт мониторингу точку, по которой видно, что сервис поднялся.

Про выбор агентского инструмента для обвязки: в разборе Tiel-Coder-35B автор дробит задачи через `plan.md` и лёгкий агент Pi вместо Codex или Claude Code — сравнение вариантов есть в гайде [Claude Code Desktop: установка, лимиты и чем отличается от терминала](https://ai-release.org/guides/claude-code-desktop-ustanovka.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

**Как проверить:**

```bash
uvicorn app:app --port 8000 &
curl -s http://127.0.0.1:8000/health
curl -s -X POST http://127.0.0.1:8000/v1/answer \
  -H "Content-Type: application/json" \
  -d '{"user_id":"u1","text":"сколько хранятся документы?"}'
```

Ожидаемый вывод: `{"status":"ok"}` и `{"user_id":"u1","latency_ms":<число>}`. Если второй запрос вернул `413` — сработал лимит, это правильное поведение защиты.

## Шаг 3. Проверить работу и согласованность версий
![Иллюстрация к разделу «Шаг 3. Проверить работу и согласованность версий»](https://ai-release.org/guides/img/kak-proektirovat-ii-sistemu-ru-3.jpg?v=1793781353)


**Что сделать:** проверьте не только «отвечает ли сервис», но и согласованность набора: версии документов, метаданные, индекс, версии кода и моделей должны соответствовать друг другу. Единица восстановления в RAG-помощнике — именно согласованный набор, а не отдельная база.

**Зачем:** сбой бывает тихим: сервис жив и отвечает, но по старому индексу — «14 дней» вместо «30». Формально всё работает, фактически вы врёте пользователю.

**Пример:** регрессионный вопрос в чек-лист приёмки.

```bash
curl -s http://127.0.0.1:6333/collections/docs | jq '.result.points_count'
curl -s -X POST http://127.0.0.1:8000/v1/answer \
  -H "Content-Type: application/json" \
  -d '{"user_id":"test","text":"Сколько дней хранятся документы по политике v5?"}'
```

Ожидаемый вывод: число точек больше нуля и ответ, совпадающий с актуальной версией политики. Успех выглядит так: в ответе указан фрагмент с номером версии документа, а `points_count` меняется после переиндексации.

- [ ] `system.yaml` содержит все шесть компонентов
- [ ] `/health` отвечает `{"status":"ok"}`
- [ ] лимит входа возвращает `413`, а не падение
- [ ] номер версии документа в ответе совпадает с последней загрузкой в S3
- [ ] в логах есть `user_id`, версия индекса и версия модели
- [ ] снапшот Qdrant и дамп PostgreSQL сняты в одну точку времени

## Возможные проблемы и решения

**Проблема. Сервис отвечает, но по устаревшим данным**

- Ошибка: в ответе приходит `"source": "policy_v1.pdf"`, хотя в S3 уже лежит v5; на прямой вопрос система говорит «14 дней» вместо «30».
- Причина: обновили файлы в S3 и метаданные в PostgreSQL, но не пересобрали коллекцию Qdrant — индекс остался старым.
- Решение: пересобрать индекс и привязать его к версии документов, затем проверить: `curl -s http://127.0.0.1:6333/collections/docs | jq '.result.points_count'` до и после. Версию индекса писать в каждый ответ API.

**Проблема. Коллекция не создана**

- Ошибка: `qdrant_client.http.exceptions.UnexpectedResponse: Not found: Collection 'docs' doesn't exist!`
- Причина: сервис стартовал раньше, чем создали коллекцию, либо имя в `system.yaml` и в коде различаются.
- Решение: создать коллекцию с тем же именем, что в конфиге, и перезапустить сервис: `curl -s -X PUT http://127.0.0.1:6333/collections/docs -H "Content-Type: application/json" -d '{"vectors":{"size":<размерность>,"distance":"Cosine"}}'`. Размерность `<размерность>` возьмите из документации вашей embedding-модели.

**Проблема. Модель не влезает в память**

- Ошибка: `torch.OutOfMemoryError: CUDA out of memory. Tried to allocate ...`
- Причина: выбранная конфигурация не соответствует объёму видеопамяти; плотная большая модель не помещается на одну карту.
- Решение: взять квантованную MoE-модель (в разборе Tiel-Coder-35B-A3B-MTP в кванте Q2_K_XL даёт 125 токенов/с на 16 ГБ) или разложить модель по нескольким GPU — в официальном примере vLLM Mistral Large 4 разложена по восьми GPU. Точные флаги разложения сверьте с документацией vLLM.

## Резюме и следующие шаги

Сначала фиксируется схема из шести блоков, затем собирается обвязка с лимитами и RAG, и только потом проверяется согласованность версий документов, индекса и модели. Следующий шаг — автоматизировать проверку версий в CI и настроить регулярные снапшоты Qdrant и PostgreSQL в одну точку времени.

## FAQ

**Чем JEV от TypeSafe AI отличается от обычной LLM?**
JEV выбирает действие внутри кода, а не сочиняет текст: она не ведёт диалог и не пишет код, а относится к классу System One Models. Открытый аналог — Laya; в разборе на Хабре Laya и Gemma 4 запущены на своём сервере в сервисе на FastAPI.

**Зачем бэкапить векторную базу, если исходные документы лежат в S3?**
Потому что единица восстановления — согласованный набор: версии документов, метаданные, индекс, версии кода и моделей. Без индекса сервис поднимется, но будет отвечать по устаревшим данным.

**Сколько корпоративных ИИ-пилотов доходит до продакшена?**
По приведённой в разборе оценке — в среднем четыре из 33, то есть 88% остаются экспериментом. Причина обычно не в модели, а в отсутствии обвязки.

**Можно ли обойтись без RAG?**
Да, если нужные данные целиком помещаются в контекст: у Tiel-Coder-35B-A3B-MTP контекст 65 535 токенов, и длинные задачи в разборе дробятся через `plan.md`. Если корпус больше — нужен поиск по индексу, иначе ответы будут без опоры на документы.
