"Сделал AI-ассистента на RAG: Go + Next.js + локальная LLM"

01.06.2026
#Go#Next.js#RAG#Ollama#PostgreSQL#pgvector#AI

Скриншот

demo-screenshot.png

📦 GitHub

Репозиторий: github.com/dmironovru/rag-portfolio

Локальный AI-помощник. Никаких облачных API — все данные остаются на твоём компьютере.

🚀 Быстрый старт

git clone git@github.com:dmironovru/rag-portfolio.git
cd rag-portfolio
./start.sh
# Открой: http://localhost:3000

Скрипт сам проверит зависимости, скачает модели (~4.5 ГБ), создаст базу и поднимет всё окружение. Первый запуск — 10-15 минут, повторные — 30 секунд.

Что за проект

Это AI-ассистент, который отвечает на вопросы по технической документации. Но не просто «спросил у ChatGPT» — он ищет ответы в реальных документах через векторный поиск и генерирует связный ответ через локальную языковую модель.

Архитектура называется RAG (Retrieval-Augmented Generation):

  1. Ты задаёшь вопрос
  2. Система находит релевантные куски документации (чанки) через векторный поиск
  3. Локальная LLM (Mistral 7B) генерирует ответ на основе найденных кусков
  4. Ты получаешь ответ с цитатами и ссылками на источники

Главная фишка — всё работает локально. Никаких OpenAI API, никаких ключей, никаких утечек данных. Документация и модель лежат на твоём компьютере.

Что использовал

Frontend: Next.js 16, React 19, Tailwind CSS
Backend:  Go 1.23, chi, pgx
AI:       Ollama (mistral:7b + nomic-embed-text)
Database: PostgreSQL 16 + pgvector (векторное расширение)
Infra:    start.sh, Makefile

Пример кода: векторный поиск

Самое интересное — это как работает поиск. Документация разбивается на чанки, каждый чанк превращается в вектор (эмбеддинг) и сохраняется в PostgreSQL с расширением pgvector.

Когда приходит вопрос, он тоже превращается в вектор, и мы ищем ближайшие по косинусному расстоянию:

func (s *Store) Search(ctx context.Context, queryVec []float32, limit int) ([]Chunk, error) {
    // Превращаем вектор в строку для SQL
    vecStr := "[" + strings.Trim(strings.Join(strings.Fields(fmt.Sprint(queryVec)), ","), "[]") + "]"
    
    query := `
        SELECT id, content, source, url, 
               1 - (embedding <=> $1::vector) AS similarity
        FROM chunks
        WHERE 1 - (embedding <=> $1::vector) > 0.3
        ORDER BY embedding <=> $1::vector
        LIMIT $2
    `
    
    rows, err := s.pool.Query(ctx, query, vecStr, limit)
    // ... обработка результатов
}

Оператор <=> — это косинусное расстояние в pgvector. Чем ближе к 0, тем больше похожи векторы. Фильтр > 0.3 отсеивает совсем нерелевантные куски.

С чем столкнулся

1. Выбор модели

По умолчанию стоит mistral:7b — быстрая и качественная на английском. Но для русских вопросов лучше работает qwen2.5:7b. Сделал переключение через .env:

LLM_MODEL=qwen2.5:7b  # Для вопросов на русском

2. Размер моделей

Модели весят ~4.5 ГБ. При первом запуске ./start.sh сам их скачивает через Ollama. Это долго, но один раз — и дальше всё летает.

3. Блокировка Docker Hub

Как и с Todo Manager, Docker Hub в РФ заблокирован. Решение — зеркало:

// /etc/docker/daemon.json
{
  "registry-mirrors": ["https://dockerhub.timeweb.cloud/"]
}

Но для RAG я вообще не стал использовать Docker — слишком много зависимостей (Ollama, PostgreSQL с pgvector, Go, Node). Нативный запуск через ./start.sh оказался стабильнее и проще в отладке.

4. Качество ответов

Сначала ответы были «плавали» — модель выдумывала то, чего нет в документации. Помогло:

  • Увеличить размер чанков при индексации
  • Добавить жёсткий промпт: «Отвечай только на основе найденных источников»
  • Показывать источники прямо в интерфейсе, чтобы пользователь мог проверить

Как устроена архитектура

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Frontend      │     │   Go Backend    │     │   Ollama        │
│   (Next.js 16)  │────▶│   (RAG API)     │────▶│   (LLM)         │
│   :3000         │     │   :8080         │     │   :11434        │
└─────────────────┘     └────────┬────────┘     └─────────────────┘
                                 │
                    ┌────────────▼────────────┐
                    │   PostgreSQL + pgvector │
                    │   ~1800 чанков доков    │
                    └─────────────────────────┘
  1. Frontend — чат-интерфейс на Next.js с тёмной темой
  2. Backend — Go-сервер, который принимает вопрос, ищет чанки, отправляет их в LLM
  3. Ollama — локальный движок для LLM и эмбеддингов
  4. PostgreSQL + pgvector — хранит чанки и их векторные представления

Как индексируется документация

По умолчанию база пустая. Чтобы добавить документацию React и Next.js:

cd backend
make ingest

Скрипт:

  1. Скачивает markdown-файлы из официальных репозиториев GitHub
  2. Разбивает на чанки по ~500 токенов с перекрытием
  3. Прогоняет каждый чанк через nomic-embed-text (модель для эмбеддингов)
  4. Сохраняет в PostgreSQL вместе с вектором

Получается ~1800 чанков. Поиск по ним работает мгновенно.

Планы на развитие

  • Базовый RAG с векторным поиском — сделано
  • Умный скрипт запуска — сделано
  • Добавить загрузку PDF и своих документов через UI
  • Поддержать несколько моделей одновременно
  • История диалогов с контекстом
  • Веб-интерфейс для управления источниками

Если коротко

RAG — это не так страшно, как кажется. Главная сложность — не код, а понимание процесса: как разбивать текст, как выбирать модель, как настраивать промпты.

Самое крутое — когда задаёшь вопрос и видишь, что ассистент не выдумывает, а цитирует реальные куски документации со ссылками. Это совсем другой уровень по сравнению с обычным чат-ботом.

Совет: если хочешь разобраться в AI-разработке — начни с RAG. Это проще, чем файн-тюнинг, и даёт реальный результат. А локальные модели (Ollama) позволяют экспериментировать бесплатно и без ограничений.