Gemma 2 / Gemma 3 (Google, компактность) Семейство компактных моделей от Google для чатов и диалогов. # Gemma 2: docker exec -it ollama ollama pull gemma2:2b # 1.6 GB docker exec -it ollama ollama pull gemma2:9b # 5.5 GB # Gemma 3 (новая версия): docker exec -it ollama ollama pull gemma3:1b # 1.0 GB (по умолчанию в проекте) docker exec -it ollama ollama pull gemma3:3b # 2.0 GB Версии: 1B: минимальная конфигурация, быстрые ответы 2B: баланс скорости и качества 3B: улучшенное понимание контекста 9B: близко к моделям 13B Рекомендации по выбору модели Для разных задач RAG и работа с документами: Первый выбор: Llama 3.1 8B (быстро, точно) Альтернатива: Qwen 2.5 7B (лучше русский) Бюджет: Gemma 2 9B (компактно) Программирование: Первый выбор: Qwen 2.5 Coder 14B Альтернатива: DeepSeek-R1 14B Бюджет: Qwen 2.5 Coder 7B Математика и логика: Первый выбор: DeepSeek-R1 70B Альтернатива: Llama 3.3 70B Бюджет: DeepSeek-R1 14B Универсальные задачи: Первый выбор: Llama 3.3 70B (лучший баланс) Альтернатива: Llama 3.1 70B Бюджет: Mistral 7B CPU-only конфигурация: Первый выбор: Phi-3 Mini (3.8B) Альтернатива: Gemma 3 3B Максимум: Mistral 7B (медленно, но работает) По железу Минимальная конфигурация (2 CPU / 8GB RAM): Gemma 3 1B — работает комфортно Phi-3 Mini — альтернатива Скорость: 20-40 секунд на ответ Средняя конфигурация (4 CPU / 16GB RAM): Mistral 7B — основная модель Qwen 2.5 7B — для русского языка Llama 3.1 8B — универсальная Скорость: 15-25 секунд на ответ RTX 3060 (12GB VRAM): Llama 3.1 8B — быстро (45+ tokens/sec) Qwen 2.5 Coder 14B — программирование Mistral Nemo 12B — универсал Скорость: 1-3 секунды первый токен RTX 4090 (24GB VRAM): Llama 3.3 70B — топ качество DeepSeek-R1 70B — сложные задачи Qwen 2.5 Coder 32B — код Скорость: 15-25 tokens/sec Llama 3.1 405B — максимальное качество Возможна параллельная обработка запросов Автоматизация развёртывания: CTAPT.py Скрипт CTAPT.py полностью автоматизирует процесс установки. Разберём ключевые части: #!/usr/bin/env python3 import os import subprocess import secrets import platform import shutil class LISAInstaller: def __init__(self): self.has_nvidia_gpu = self.detect_nvidia_gpu() self.has_amd_gpu = self.detect_amd_gpu() self.config = {} def detect_nvidia_gpu(self): """Определяет наличие NVIDIA GPU""" try: result = subprocess.run( ['nvidia-smi'], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: print("✅ NVIDIA GPU обнаружен") return True except (FileNotFoundError, subprocess.TimeoutExpired): pass print("❌ NVIDIA GPU не обнаружен") return False def detect_amd_gpu(self): """Определяет наличие AMD GPU""" try: result = subprocess.run( ['rocm-smi'], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: print("✅ AMD GPU обнаружен") return True except (FileNotFoundError, subprocess.TimeoutExpired): pass return False def generate_secrets(self): """Генерирует криптографически стойкие ключи""" secrets_dict = { 'WEBUI_SECRET_KEY': secrets.token_urlsafe(32), 'N8N_ENCRYPTION_KEY': secrets.token_urlsafe(32), 'POSTGRES_PASSWORD': secrets.token_urlsafe(16), 'JWT_SECRET': secrets.token_urlsafe(64), 'ANON_KEY': self.generate_supabase_key('anon'), 'SERVICE_ROLE_KEY': self.generate_supabase_key('service_role') } return secrets_dict def generate_supabase_key(self, role): """Генерирует JWT токены для Supabase""" import jwt import time payload = { 'role': role, 'iss': 'supabase', 'iat': int(time.time()), 'exp': int(time.time()) + (10 * 365 * 24 * 60 * 60) # 10 лет } token = jwt.encode(payload, self.config['JWT_SECRET'], algorithm='HS256') return token def create_env_file(self): """Создаёт .env файл с конфигурацией""" secrets_dict = self.generate_secrets() # Интерактивный ввод доменов use_domains = input("Использовать домены для HTTPS? (y/n): ").lower() == 'y' with open('.env', 'w') as f: # Секреты for key, value in secrets_dict.items(): f.write(f"{key}={value}\n") # Домены if use_domains: n8n_domain = input("N8N домен (например n8n.example.com): ") webui_domain = input("Open WebUI домен (например webui.example.com): ") email = input("Email для Let's Encrypt: ") f.write(f"\nN8N_HOSTNAME={n8n_domain}\n") f.write(f"WEBUI_HOSTNAME={webui_domain}\n") f.write(f"LETSENCRYPT_EMAIL={email}\n") else: f.write("\n# Локальный доступ через порты\n") f.write("N8N_HOSTNAME=localhost\n") f.write("WEBUI_HOSTNAME=localhost\n") # GPU конфигурация if self.has_nvidia_gpu: f.write("\nGPU_TYPE=nvidia\n") elif self.has_amd_gpu: f.write("\nGPU_TYPE=amd\n") else: f.write("\nGPU_TYPE=none\n") print("✅ Файл .env создан") def setup_shared_directory(self): """Создаёт общую директорию для файлов с правильными правами""" shared_dir = os.path.join(os.getcwd(), 'shared') if not os.path.exists(shared_dir): os.makedirs(shared_dir) # Права 777 для доступа из Docker контейнеров os.chmod(shared_dir, 0o777) print(f"✅ Создана директория {shared_dir} с правами 777") def build_custom_images(self): """Собирает кастомные образы (n8n с FFmpeg)""" print("🔨 Сборка кастомных образов…") # Dockerfile для N8N с FFmpeg n8n_dockerfile = """ FROM n8nio/n8n:latest USER root # Установка FFmpeg RUN apk add —no-cache ffmpeg # Создание директории для shared файлов RUN mkdir -p /data/shared && chmod 777 /data/shared USER node """ with open('n8n.Dockerfile', 'w') as f: f.write(n8n_dockerfile) subprocess.run([ 'docker', 'build', '-t', 'custom-n8n-ffmpeg', '-f', 'n8n.Dockerfile', '.' ]) os.remove('n8n.Dockerfile') print("✅ Кастомные образы собраны") def start_services(self): """Запускает Docker Compose""" print("🚀 Запуск сервисов…") subprocess.run([ 'docker-compose', '-p', 'localai', 'up', '-d' ]) print("✅ Сервисы запущены") def download_default_models(self): """Загружает базовые модели""" print("📦 Загрузка базовых моделей…") models = ['gemma3:1b', 'nomic-embed-text'] for model in models: print(f" Загружается {model}…") subprocess.run([ 'docker', 'exec', 'ollama', 'ollama', 'pull', model ]) print("✅ Базовые модели загружены") def run(self): """Главный метод установки""" print("=" * 50) print("Л.И.С.А. — Установщик") print("=" * 50) # Проверки self.check_requirements() # Настройка self.create_env_file() self.setup_shared_directory() # Сборка и запуск self.build_custom_images() self.start_services() # Ожидание запуска Ollama print("⏳ Ожидание запуска Ollama…") import time time.sleep(30) # Загрузка моделей if input("Загрузить базовые модели? (y/n): ").lower() == 'y': self.download_default_models() print("\n" + "=" * 50) print("✅ Установка завершена!") print("=" * 50) print("\n🌐 Доступ к сервисам:") print(" N8N: http://localhost:8001") print(" Open WebUI: http://localhost:8002") print(" Supabase: http://localhost:8005") print("\n📚 Документация: https://github.com/shorin-nikita/lisa") def check_requirements(self): """Проверяет системные требования""" # Проверка Docker try: result = subprocess.run(['docker', '—version'], capture_output=True) if result.returncode != 0: raise Exception("Docker не установлен") except FileNotFoundError: print("❌ Docker не найден. Установите Docker: https://docs.docker.com/get-docker/") exit(1) # Проверка Docker Compose try: result = subprocess.run(['docker-compose', '—version'], capture_output=True) if result.returncode != 0: raise Exception("Docker Compose не установлен") except FileNotFoundError: print("❌ Docker Compose не найден") exit(1) print("✅ Системные требования проверены") if __name__ == "__main__": installer = LISAInstaller() installer.run() Обновление системы: O6HOBA.py Для обновления существующей установки используется O6HOBA.py: #!/usr/bin/env python3 import subprocess import os from datetime import datetime class LISAUpdater: def backup_data(self): """Создаёт резервную копию перед обновлением""" backup_dir = f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" os.makedirs(backup_dir) # Бэкап .env if os.path.exists('.env'): shutil.copy('.env', f"{backup_dir}/.env") print(f"✅ Резервная копия создана: {backup_dir}") def update(self): print("🔄 Обновление Л.И.С.А…") # Бэкап self.backup_data() # Остановка сервисов subprocess.run(['docker-compose', '-p', 'localai', 'down']) # Получение обновлений subprocess.run(['git', 'pull', 'origin', 'main']) # Обновление образов subprocess.run(['docker-compose', '-p', 'localai', 'pull']) # Пересборка кастомных образов subprocess.run([ 'docker', 'build', '-t', 'custom-n8n-ffmpeg', '-f', 'n8n.Dockerfile', '.' ]) # Запуск subprocess.run(['docker-compose', '-p', 'localai', 'up', '-d']) print("✅ Обновление завершено!") if __name__ == "__main__": updater = LISAUpdater() updater.update() RAG: техническая реализация Retrieval-Augmented Generation — технология, при которой LLM отвечает на основе внешних документов, а не только обученных данных. Архитектура RAG-пайплайна Документ → Chunking → Embedding → Vector DB │ Вопрос → Embedding → Similarity Search ┘ │ ▼ Top-K chunks + Вопрос → LLM → Ответ Реализация в Open WebUI Open WebUI имеет встроенную поддержку RAG, но для production использования я доработал пайплайн: from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.embeddings import HuggingFaceEmbeddings from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams class RAGPipeline: def __init__(self): self.text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", " ", ""] ) self.embeddings = HuggingFaceEmbeddings( model_name="sentence-transformers/paraphrase-multilingual-mpnet-base-v2" ) self.qdrant = QdrantClient(host="qdrant", port=6333) def process_document(self, document_text, document_id): """Обрабатывает документ и сохраняет в векторную БД""" # Разбиваем на чанки chunks = self.text_splitter.split_text(document_text) # Создаём эмбеддинги embeddings = self.embeddings.embed_documents(chunks) # Создаём коллекцию если не существует try: self.qdrant.create_collection( collection_name="documents", vectors_config=VectorParams( size=768, # размерность эмбеддингов distance=Distance.COSINE ) ) except: pass # Сохраняем в Qdrant points = [ { "id": f"{document_id}_{i}", "vector": embedding, "payload": { "text": chunk, "document_id": document_id, "chunk_index": i } } for i, (chunk, embedding) in enumerate(zip(chunks, embeddings)) ] self.qdrant.upsert( collection_name="documents", points=points ) def search(self, query, top_k=5): """Ищет релевантные чанки для запроса""" # Создаём эмбеддинг запроса query_embedding = self.embeddings.embed_query(query) # Ищем похожие векторы results = self.qdrant.search( collection_name="documents", query_vector=query_embedding, limit=top_k ) return [hit.payload["text"] for hit in results] def generate_response(self, query, context_chunks, model="llama3.1:8b"): """Генерирует ответ на основе найденных чанков""" context = "\n\n".join(context_chunks) prompt = f"""Используя только информацию из контекста ниже, ответь на вопрос. Если ответа нет в контексте, скажи "Информация не найдена в документах". Контекст: {context} Вопрос: {query} Ответ:""" # Вызов Ollama через API import requests response = requests.post( "http://ollama:11434/api/generate", json={ "model": model, "prompt": prompt, "stream": False } ) return response.json()["response"] Оптимизация RAG для production Проблема 1: Медленный поиск при большом количестве документов. Решение: Используем HNSW индекс в Qdrant: self.qdrant.create_collection( collection_name="documents", vectors_config=VectorParams( size=768, distance=Distance.COSINE ), hnsw_config={ "m": 16, # количество связей "ef_construct": 100 # точность построения индекса } ) Проблема 2: Плохое качество ответов при большом количестве чанков. Решение: Реранжирование результатов: from sentence_transformers import CrossEncoder class ImprovedRAG(RAGPipeline): def __init__(self): super().__init__() self.reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2') def search_with_rerank(self, query, top_k=5, rerank_top=20): # Получаем больше кандидатов candidates = super().search(query, top_k=rerank_top) # Реранжируем pairs = [[query, chunk] for chunk in candidates] scores = self.reranker.predict(pairs) # Возвращаем top-k после реранжирования ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) return [chunk for chunk, score in ranked[:top_k]] Интеграция Open WebUI с N8N через Pipe Functions Уникальная фича проекта — можно вызывать N8N workflows прямо из чата Open WebUI. Архитектура интеграции User в Open WebUI │ ▼ Pipe Function (Python) │ ▼ N8N Webhook │ ▼ Custom Workflow (RAG/API/Automation) │ ▼ Response → Open WebUI Реализация Pipe Function Файл n8n_pipe.py в корне проекта: from typing import List, Union, Generator, Iterator from pydantic import BaseModel import requests class Pipe: class Valves(BaseModel): n8n_url: str = "http://localhost:8001/webhook/your-webhook-id" n8n_bearer_token: str = "" input_field: str = "chatInput" response_field: str = "output" def __init__(self): self.name = "N8N Integration" self.valves = self.Valves() def pipe( self, user_message: str, model_id: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # Подготовка payload для N8N payload = { self.valves.input_field: user_message, "messages": messages, "model": model_id } # Заголовки headers = {"Content-Type": "application/json"} if self.valves.n8n_bearer_token: headers["Authorization"] = f"Bearer {self.valves.n8n_bearer_token}" try: # Вызов N8N webhook response = requests.post( self.valves.n8n_url, json=payload, headers=headers, timeout=120 ) if response.status_code == 200: result = response.json() return result.get(self.valves.response_field, "No response field found") else: return f"N8N Error: {response.status_code} — {response.text}" except Exception as e: return f"Connection error: {str(e)}" Настройка в Open WebUI Откройте Open WebUI → Workspace → Functions Add Function → вставьте код из n8n_pipe.py Настройте параметры: n8n_url: Production webhook URL из N8N n8n_bearer_token: токен безопасности (опционально) Выберите её из списка моделей в чате Примеры N8N Workflows для интеграции 1. RAG с векторным поиском: Webhook → Extract Query → Qdrant Search → Ollama Generate → Response 2. Внешний API + LLM: Webhook → HTTP Request (API) → Format Data → Ollama Analyze → Response 3. Мульти-агентный workflow: Webhook → Agent 1 (Research) → Agent 2 (Analyze) → Agent 3 (Summarize) → Response Готовые Workflows из репозитория Проект включает готовые workflows в n8n/backup/workflows/: 1. Telegram Bot.json Полнофункциональный Telegram бот с памятью и аналитикой. Возможности: ✅ Текстовые ответы с контекстом (PostgreSQL Memory) ✅ Транскрибация голосовых через Whisper ✅ Анализ изображений (GPT-4o Vision API) ✅ Message Buffer (группировка сообщений, 10 сек) ✅ Статус "печатает…" во время обработки ✅ Аналитические команды: /stats, /today, /funnel, /users ✅ Автологирование в PostgreSQL Workflow структура: Telegram Trigger │ ▼ Message Type Check ├─► Text → Memory Load → Ollama → Memory Save → Send ├─► Voice → Whisper → Memory → Ollama → Send └─► Photo → Vision API → Analyze → Send 2. HTTP Handle.json Настройка webhooks для различных платформ. Поддерживаемые платформы: Wazzup24 (WhatsApp, Instagram, VK, Avito) 3. Конвертация WebM → OGG + Транскрибация.json Обработка видео с автоматической транскрибацией. Особенности: ✅ Конвертация через FFmpeg (встроен в N8N) ✅ Экстремальное сжатие: 95% экономия (32 kbit/s, mono) ✅ Оптимизация для голоса ✅ Работа в /tmp без сохранения на диск Workflow: Input (WebM) → FFmpeg Convert → Whisper STT → Return Text + Audio Пример FFmpeg команды: ffmpeg -i input.webm \ -vn \ # Без видео -codec:a libopus \ # Opus кодек -b:a 32k \ # Битрейт 32 kbit/s -ac 1 \ # Mono -ar 16000 \ # Sample rate 16 kHz -application voip \ # Оптимизация для голоса output.ogg 4. RAG AI Agents (3 версии) V1: Local RAG AI Agent Базовый RAG с локальными моделями Qdrant для векторного поиска V2: Supabase RAG AI Agent RAG с pgvector (Supabase) Source: https://habr.com/ru/articles/973456/