python --version; поддерживаемую версию сверьте с документацией вашего inference-сервера), FastAPI + uvicorn, Docker и docker compose, PostgreSQL, Qdrant, S3-совместимое хранилище, запущенный сервер LLM (например, vLLM), curl, jq
docker ps без sudo) и слушать локальные порты.fastapi, uvicorn, psycopg[binary], qdrant-client, boto3, pyyaml.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)).
Что сделать: опишите систему как поток данных, а не как «одну модель». Минимальный набор блоков: входной API, обвязка (harness — слой, который знает про пользователя, права, лимиты и правила вызова модели), LLM, RAG-хранилища (S3 — файлы документов, PostgreSQL — метаданные, Qdrant — векторный индекс), интеграции с инструментами, мониторинг.
Зачем: запуск LLM на GPU — только первый шаг; модель сама по себе не знает пользователя, контекст и правила доступа, поэтому рабочий сервис — это модель плюс обвязка. По оценке из разбора про обвязку LLM, из 33 корпоративных ИИ-пилотов до продакшена доходят в среднем только четыре, то есть 88% остаются экспериментом — обычно именно из-за отсутствия спроектированной обвязки.
Зафиксируйте состав в файле system.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-хранилища | документы, метаданные, индекс | ответы «из головы» модели |
| Мониторинг | логи запросов и версий | тихие сбои не видны |
| Конфиг | смена поведения без пересборки | каждая правка = релиз |
Как проверить: распарсите файл и убедитесь, что все блоки на месте.
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 на практике: структура папок, которая делает ИИ-агента управляемым.

Что сделать: поднимите хранилища, затем напишите сервис на FastAPI, который принимает запрос, проверяет лимит, достаёт документы из Qdrant, подставляет их в промпт и логирует версию индекса.
Зачем: обвязка превращает голую модель в сервис: добавляет RAG с корпоративными данными, интеграции с инструментами, лимиты и мониторинг. Второй принцип — конфигурируемость: в проекте Android-SCADA универсальность достигнута за счёт JSON-конфигурации, для нового объекта не нужна пересборка приложения, достаточно заменить файл. В ИИ-системе работает то же правило: новое поведение — новый конфиг, а не новый релиз кода.
Пример: конфиг хранилищ и запуск.
# docker-compose.yml
services:
qdrant:
image: qdrant/qdrant
ports: ["6333:6333"]
postgres:
image: postgres
environment:
POSTGRES_DB: rag
POSTGRES_PASSWORD: <пароль>
ports: ["5432:5432"]
docker compose up -d
curl -s http://127.0.0.1:6333/collections | jq .
Ожидаемый вывод: JSON с полем "result": {"collections": []} — Qdrant жив, коллекций пока нет. Пароль <пароль> замените своим и держите в .env, а не в git.
Минимальный сервис с проверкой лимита:
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: установка, лимиты и чем отличается от терминала.
Как проверить:
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 — сработал лимит, это правильное поведение защиты.

Что сделать: проверьте не только «отвечает ли сервис», но и согласованность набора: версии документов, метаданные, индекс, версии кода и моделей должны соответствовать друг другу. Единица восстановления в RAG-помощнике — именно согласованный набор, а не отдельная база.
Зачем: сбой бывает тихим: сервис жив и отвечает, но по старому индексу — «14 дней» вместо «30». Формально всё работает, фактически вы врёте пользователю.
Пример: регрессионный вопрос в чек-лист приёмки.
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, а не падениеuser_id, версия индекса и версия моделиПроблема. Сервис отвечает, но по устаревшим данным
"source": "policy_v1.pdf", хотя в S3 уже лежит v5; на прямой вопрос система говорит «14 дней» вместо «30».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 ...Сначала фиксируется схема из шести блоков, затем собирается обвязка с лимитами и RAG, и только потом проверяется согласованность версий документов, индекса и модели. Следующий шаг — автоматизировать проверку версий в CI и настроить регулярные снапшоты Qdrant и PostgreSQL в одну точку времени.
Чем 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. Если корпус больше — нужен поиск по индексу, иначе ответы будут без опоры на документы.