Микросервис на FastAPI, реализующий AI-логику финансового помощника для массовой аудитории. Часть хакатон-проекта — работает независимо на порту 8001, не пересекаясь с основным Backend (8000).
| Компонент | Технология |
|---|---|
| Фреймворк | FastAPI + Uvicorn (HTTPS) |
| Граф агентов | LangGraph |
| LLM | Ollama · qwen2.5:7b-instruct-q4_K_M |
| RAG | ChromaDB (embedded) · all-MiniLM-L6-v2 |
| База данных | MongoDB Atlas (pymongo) |
| Поиск | Serper / Tavily / DuckDuckGo |
| Контейнеризация | Docker + Docker Compose |
| Конфигурация | .env + prompts.yaml |
┌─────────────────────────────────────────────────────┐
│ FastAPI (8001) │
│ │
│ /ai/process ──→ LangGraph ──→ Planner │
│ /ai/stream ──→ (граф) ──→ Search (если нужно) │
│ ──→ RAG (ChromaDB) │
│ ──→ Analyst (LLM) │
│ │
│ /ai/onboarding ──→ OnboardingFlow ──→ MongoDB │
│ /ai/bank-offers ──→ Scorer (детерминированный) │
│ /ai/cashflow ──→ Calculator (без LLM) │
│ /ai/patterns ──→ Calculator + LLM-инсайт │
│ /ai/daily-action ──→ Calculator + LLM-совет │
└─────────────────────────────────────────────────────┘
Принцип: калькуляторы считают цифры, LLM только объясняет результаты. LLM никогда не выполняет арифметику.
Очередь: глобальный семафор пропускает к Ollama один LLM-запрос за раз. Детерминированные эндпоинты (/ai/bank-offers, /ai/cashflow/calculate) работают вне очереди и отвечают мгновенно.
- Python 3.11+
- Ollama с загруженной моделью
qwen2.5:7b-instruct-q4_K_M - MongoDB Atlas (опционально — без него онбординг работает в памяти)
- SSL-сертификат (файлы в
certificates/)
cd ai-service
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txtpython -m uvicorn app.main:app \
--host 0.0.0.0 \
--port 8001 \
--ssl-certfile certificates/server.crt \
--ssl-keyfile certificates/server.keyПроверка: curl -k https://localhost:8001/health
Ollama запускается на хосте отдельно — в compose не включена.
docker compose up --buildСервис поднимается на https://localhost:8001. ChromaDB-индекс сохраняется в data/chroma/ (volume ./data:/app/data).
Проверка конфига без запуска: docker compose config
OLLAMA_BASE_URL=http://localhost:11434
PLANNER_MODEL=qwen2.5:7b-instruct-q4_K_M
ANALYST_MODEL=qwen2.5:7b-instruct-q4_K_M
AI_SERVICE_PORT=8001
LOG_LEVEL=INFO
# Поиск (используется в /ai/process при запросах актуальных данных)
SERPER_API_KEY= # google.serper.dev — основной
TAVILY_API_KEY= # app.tavily.com — резерв
# MongoDB (без URI онбординг хранится в памяти процесса)
MONGODB_URI=mongodb+srv://...Все LLM-промпты — в app/prompts.yaml. Меняются без перезапуска сервиса.
RAG инициализируется при первом запросе: ChromaDB скачивает модель
all-MiniLM-L6-v2(~79 МБ) один раз и кэширует в~/.cache/chroma/. Повторные старты мгновенные — индекс читается с диска.
| Метод | Путь | Очередь | Время |
|---|---|---|---|
POST |
/ai/process |
да | 15–60 с |
POST |
/ai/stream |
да | первый байт < 1 с, SSE-поток |
Пример запроса:
POST /ai/process
{
"user_id": "uuid",
"query": "Стоит ли мне брать кредит на машину?",
"context": {
"user_profile": { "monthly_income": 150000, "monthly_debt_payments": 10000 },
"history": []
}
}Пример ответа:
{
"text": "С учётом вашего дохода кредитная нагрузка составит 31.4% — жёлтая зона...",
"table": { "headers": ["Вариант", "Платёж", "PTI"], "rows": [["...", "...", "..."]] },
"structured": { "summary": "...", "recommendations": ["..."], "risks": ["..."] },
"intent": "advice",
"sources": [],
"error": null
}Поле table появляется по решению LLM — когда ответ выигрывает от структуры (сравнение вариантов кредита, разбивка расходов и т.п.). В остальных случаях — null.
Девятишаговый диалог (3 фазы: личность → образ жизни → финансы), который формирует UserFinancialProfile. Профиль сохраняется в MongoDB и используется для персонализации чата.
| Метод | Путь | Описание |
|---|---|---|
POST |
/ai/onboarding |
Один шаг диалога |
GET |
/ai/onboarding/{user_id}/status |
Статус и готовый профиль |
Фазы:
Фаза 1 — ЛИЧНОСТЬ (2–3 вопроса): интересы, образ жизни → доверие
Фаза 2 — ОБРАЗ ЖИЗНИ (3–4 вопроса): кафе, подписки, транспорт → косвенные траты
Фаза 3 — ФИНАНСЫ (4–5 вопросов): доход, расходы, кредиты, цели → профиль
Пример шага:
POST /ai/onboarding
{ "user_id": "uuid", "message": "Работаю разработчиком, увлекаюсь спортом" }
→ {
"question": "Как часто ходишь в кафе или рестораны?",
"suggested_answers": ["Редко", "Пару раз в месяц", "Часто"],
"phase": 2,
"complete": false
}После complete: true забери профиль через /status и передавай в context.user_profile каждого запроса к /ai/process.
| Метод | Путь | Описание |
|---|---|---|
POST |
/ai/cashflow/calculate |
Кэшфлоу по балансу и дням до зарплаты |
GET |
/ai/cashflow/{user_id} |
То же, но баланс берётся из профиля |
GET |
/ai/patterns/{user_id} |
Паттерн трат + LLM-инсайт |
GET |
/ai/daily-action/{user_id} |
Персональный совет на сегодня |
Кэшфлоу возвращает поднёвной прогноз баланса до зарплаты с выделением рисковых событий:
POST /ai/cashflow/calculate
{ "user_id": "uuid", "current_balance": 15000, "days_to_salary": 18 }
→ {
"projected_balance": -9000.0,
"will_be_negative": true,
"shortage": 9000.0,
"critical_day": 12,
"forecast": [
{ "day": 0, "balance": 15000.0, "event": null },
{ "day": 5, "balance": 9666.67, "event": "аренда" },
{ "day": 12, "balance": -166.67, "event": null }
],
"risk_events": [{ "day": 5, "balance": 9666.67, "event": "аренда" }]
}POST /ai/bank-offers
Детерминированный скоринг без LLM и без внешних запросов. Из базы 48 предложений 28 банков отбирает топ-6 наиболее подходящих под запрос пользователя. Отвечает за < 50 мс.
Запрос:
{
"user_id": "uuid",
"loan_amount": 500000,
"loan_rate": 15.0,
"loan_months": 24
}Ответ:
{
"offers": [
{
"bank_name": "Сбербанк",
"domain": "sber.ru",
"rate": 14.5,
"loan_months": 24,
"monthly_payment": 24124.71,
"score": 99,
"logo_url": "https://img.logo.dev/sber.ru?token=free",
"offer_url": "https://sber.ru/credits/consumer"
}
],
"search_query": "кредит 500000 ₽ на 24 мес. под 15.0% годовых"
}Как считается score:
score = max(0, 1 − |offer.rate − loan_rate| / loan_rate) × 40 ← ставка (40%)
+ max(0, 1 − |offer.months − loan_months| / loan_months) × 60 ← срок (60%)
Предложение с точным совпадением по обоим параметрам → score = 100.
Покрытие (28 банков, 48 предложений):
Т-Банк · Сбербанк · Альфа-Банк · ВТБ · Газпромбанк · Россельхозбанк · МТС Банк · Почта Банк · Совкомбанк · Росбанк · Уралсиб · Промсвязьбанк · Ренессанс Кредит · МКБ · ОТП Банк · Ак Барс Банк · Банк БСПБ · Абсолют Банк · Хоум Банк · УБРиР · Банк Зенит · СКБ-Банк · Банк ДОМ.РФ · РНКБ Банк · Русский Стандарт · Кредит Европа · Синара Банк · Экспобанк · Банк Авангард · Металлинвестбанк · Кубань Кредит · СДМ-Банк · Банк Центр-Инвест · Левобережный Банк · Инбанк · АТБ
Перед вызовом LLM в узле analyst граф автоматически ищет релевантные статьи из базы знаний и добавляет их в промпт.
Запрос → ChromaDB (cosine similarity) → top-2 статьи → промпт аналитика → LLM
База знаний — data/knowledge.json, 22 статьи по темам:
| Тема | ID |
|---|---|
| PTI и долговая нагрузка | pti_explained, pti_calculation |
| Подушка безопасности | emergency_fund, emergency_fund_vs_investing |
| Аннуитет vs дифференцированный | annuity_vs_differential |
| Инфляция | inflation |
| Вклады и накопительные счета | deposits |
| ОФЗ | ofz_basics |
| Кредитная история и скоринг | credit_history |
| Рефинансирование | refinancing |
| Ипотека | mortgage_terms |
| ETF и индексные фонды | etf_index_funds |
| Налоговый вычет 13% | tax_deduction |
| Правило 50/30/20 | 50_30_20_rule |
| Сложный процент | compound_interest |
| Диверсификация | diversification |
| ИИС | iis_basics |
| Управление кэшфлоу, накопления, долги, бюджет, страхование | … |
Порог фильтрации: cosine distance > 0.625 — результат не добавляется в промпт (нерелевантный запрос).
Индекс хранится в data/chroma/ (в .gitignore). Строится автоматически при первом старте, затем читается с диска.
ai-service/
├── Dockerfile # python:3.11-slim, EXPOSE 8001, HTTPS через uvicorn
├── docker-compose.yml # ai-service, порт 8001, volumes для cert и data
└── .dockerignore # исключает .venv, .git, tests, data/chroma
Ollama не входит в compose — она работает на хосте Windows. Сервис обращается к ней через host.docker.internal:11434.
# ключевые части docker-compose.yml
environment:
- OLLAMA_BASE_URL=http://host.docker.internal:11434
volumes:
- ./certificates:/app/certificates:ro # SSL
- ./data:/app/data # ChromaDB (без :ro — пишет индекс)
healthcheck:
test: ["CMD", "curl", "-fk", "https://localhost:8001/health"]pytest tests/test_calculators.py tests/test_onboarding.py tests/test_bank_offers.py -vРезультат: 36 passed
| Файл | Тестов | Что покрыто |
|---|---|---|
test_calculators.py |
16 | PTI green/yellow/red, zero income; health score critical/medium/good/excellent; savings realistic/unrealistic/no money; cashflow enough/not-enough/danger-day/fixed-payments/zero-balance |
test_onboarding.py |
12 | _count_questions_by_phase (пустая, смешанная история); _next_question (переходы 1→2→3→complete); _build_profile (мёрж, override, defaults); _build_summary (полный/пустой профиль) |
test_bank_offers.py |
8 | score=100 при точном совпадении; ≤6 офферов на выходе; сортировка по убыванию score; обязательные поля в ответе; пустой вход |
Фикстуры sample_user_profile и sample_onboarding_history — в tests/conftest.py.
ai-service/
├── app/
│ ├── main.py # FastAPI-приложение, все эндпоинты, middleware логирования
│ ├── schemas.py # Pydantic-модели запросов/ответов
│ ├── graph.py # LangGraph: Planner → Search → RAG → Analyst
│ ├── rag.py # FinancialRAG: ChromaDB embedded, lazy singleton
│ ├── llm.py # OllamaClient с retry-логикой
│ ├── onboarding.py # Трёхфазный онбординг-диалог
│ ├── calculators.py # PTI, аннуитет, индексы здоровья, кэшфлоу
│ ├── bank_offers.py # База 48 офферов + детерминированный скоринг
│ ├── patterns.py # Анализ паттернов трат
│ ├── daily_action.py # Ежедневный персональный совет
│ ├── connectors.py # Serper → Tavily → DuckDuckGo
│ ├── database.py # MongoDB: сессии онбординга и профили
│ ├── queue.py # Глобальный семафор для Ollama
│ └── prompts.yaml # Все LLM-промпты
├── data/
│ ├── knowledge.json # 22 финансовые статьи для RAG
│ └── chroma/ # ChromaDB-индекс (генерируется, в .gitignore)
├── tests/
│ ├── conftest.py # Фикстуры pytest
│ ├── test_calculators.py # 16 тестов калькуляторов
│ ├── test_onboarding.py # 12 тестов логики онбординга
│ └── test_bank_offers.py # 8 тестов скоринга офферов
├── docs/
│ └── backend-contract.md # Полный API-контракт для Backend
├── certificates/
│ ├── server.crt
│ └── server.key
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env
├── requirements.txt
└── README.md
PTI = ежемесячные_платежи / доход × 100%
Зелёный PTI < 30% — нагрузка в норме
Жёлтый 30% ≤ PTI < 50% — повышенная нагрузка
Красный PTI ≥ 50% — критическая нагрузка
P = S × r(1+r)^n / ((1+r)^n − 1)
S = сумма кредита
r = годовая_ставка / 12 / 100
n = срок в месяцах
expense_score = f(расходы / доход) → 0 / 10 / 20 / 33
debt_score = f(долги / доход) → 0 / 10 / 20 / 33
savings_score = f(накопления / месяцев) → 0 / 10 / 20 / 34
Уровни:
critical (0–40) — критическое состояние
medium (41–65) — средний уровень, есть зоны роста
good (66–85) — хорошее состояние
excellent (86–100) — отличное финансовое здоровье
Middleware логирует входной и выходной JSON каждого запроса:
2026-05-30 19:33:40 [INFO] app.main: [IN] POST /ai/bank-offers
{
"user_id": "alex",
"loan_amount": 500000,
"loan_rate": 15.0,
"loan_months": 24
}
2026-05-30 19:33:40 [INFO] app.main: [OUT] POST /ai/bank-offers
{
"offers": [...],
"search_query": "кредит 500000 ₽ на 24 мес. под 15.0% годовых"
}
SSE-стрим (/ai/stream) — логируется только входной JSON (тело ответа нельзя буферизовать без потери стриминга).
Большинство эндпоинтов всегда возвращают HTTP 200. Ошибки передаются через поле error в теле ответа — никогда через HTTP 500.
Исключение — POST /ai/cashflow/calculate возвращает HTTP 422 если онбординг не пройден.
Подробный контракт со всеми полями, примерами и таймаутами — в docs/backend-contract.md.