Когда что выбрать, как настроить, как сочетать. On-prem ВМ + Kubernetes + cloud.
// 01
Главная ошибка 2025–2026: команды задают вопрос «RAG или fine-tuning?» — и пытаются выбрать одно. Правильный вопрос: где должен жить каждый тип знания и поведения?
Ответ: волатильные факты → в retrieval, стабильное поведение → в весах модели. Эти слои не конкурируют — они дополняют друг друга.
Ключевое правило из практики 2026: fine-tuning не фиксирует проблему галлюцинаций с фактами. Модель, fine-tuned на ваших данных, всё равно будет галлюцинировать детали. Если проблема — устаревшие или неточные факты, это проблема retrieval, а не обучения.
Если весь корпус знаний влезает в контекст (<200K токенов) — full-context prompting + prompt caching часто быстрее и дешевле, чем строить retrieval. Anthropic рекомендует этот подход для небольших внутренних knowledge base.
Перед любым вложением в RAG или FT: попробовать улучшенный промпт. Часто 90% качества достигается правильным системным промптом за 1 день vs 6 недель обучения.
Prompt engineering → RAG → Fine-tuning + RAG. Каждый уровень значительно дороже предыдущего по setup и поддержке. Двигаться вперёд только с доказанной необходимостью.
// 02
Вопросы упорядочены по убыванию приоритета. Ответы ведут к конкретной рекомендации архитектуры.
| Параметр | Fine-tuning | RAG | Hybrid |
|---|---|---|---|
| Time to first value | 4–8 недель (данные+обучение+eval) | 1–2 недели до MVP | 2–4 недели (RAG сначала, FT потом) |
| Обновление знаний | Retraining (дорого, медленно) | Мгновенно: обновил документ | Знания: мгновенно; поведение: retraining |
| Hallucinations | Не решает проблему фактов | Снижает при хорошем retrieval | Лучший результат из двух |
| TTFT / Latency | Ниже: нет retrieval overhead | +50–200ms на retrieval+rerank | RAG latency + FT inference |
| Стоимость setup | GPU время + data labeling | Vector DB + embedding costs | Оба + инженерный overhead ×1.6 |
| Explainability | Черный ящик: почему так ответил? | Прозрачно: видны retrieved chunks | Прозрачно в части знаний |
| Data requirements | 500–5K размеченных примеров | Raw документы в любом формате | Оба типа |
// 03
Наивный RAG (embed → retrieve → generate) работает в demo. В production он failsы в 40% случаев из-за semantic gap, плохого chunking и отсутствия reranking. Правильная архитектура сложнее.
80% провалов RAG — на этапе ingestion и chunking, не на этапе генерации. Если модель даёт плохие ответы, сначала проверь: правильно ли парсятся документы, правильный ли размер чанков, включены ли headings в чанки. Это не проблема LLM.
6ms P50 latency. Лучший self-hosted выбор 2026. Hybrid search из коробки. Docker + Helm chart. HNSW индекс. Rust-based = стабильная производительность под нагрузкой.
Нет дополнительной инфры. ACID транзакции. Metadata filtering через SQL. Медленнее Qdrant при больших объёмах (>1M векторов). Для <500K документов — отличный выбор.
Встроенный BM25, hybrid search из коробки. GraphQL API. Self-hosted через Helm. Хорош для multi-tenancy. Более сложная операционная нагрузка.
Простейший старт, нет production-grade фич. Нет horizontal scaling. Нет hybrid search. Для прототипа и dev — ок. Для production — заменить.
// 04
services: # Vector Database qdrant: image: qdrant/qdrant:v1.10.0 ports: ["6333:6333", "6334:6334"] volumes: - qdrant_data:/qdrant/storage - ./qdrant-config.yaml:/qdrant/config/production.yaml:ro environment: QDRANT__SERVICE__API_KEY: ${QDRANT_API_KEY} restart: unless-stopped # Embedding model (BGE-M3, self-hosted) embedding-service: image: ghcr.io/huggingface/text-embeddings-inference:latest command: --model-id BAAI/bge-m3 --port 8080 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] ports: ["8080:8080"] # Reranker (BGE-Reranker, self-hosted) reranker-service: image: ghcr.io/huggingface/text-embeddings-inference:latest command: --model-id BAAI/bge-reranker-v2-m3 --port 8081 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] ports: ["8081:8081"] # RAG API layer (LangChain/LlamaIndex приложение) rag-api: build: ./rag-service environment: QDRANT_URL: http://qdrant:6333 QDRANT_API_KEY: ${QDRANT_API_KEY} EMBEDDING_URL: http://embedding-service:8080 RERANKER_URL: http://reranker-service:8081 LLM_URL: http://vllm:8000 # ваш vLLM endpoint ports: ["9000:9000"] restart: unless-stopped volumes: qdrant_data:
from langchain.text_splitter import RecursiveCharacterTextSplitter from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct import httpx, uuid # Конфигурация EMBEDDING_URL = "http://embedding-service:8080/embed" QDRANT_URL = "http://qdrant:6333" CHUNK_SIZE = 512 # оптимальный по 2026 бенчмаркам CHUNK_OVERLAP = 64 # ~12.5% overlap client = QdrantClient(url=QDRANT_URL, api_key="...") def create_collection(name: str, dim: int = 1024): # BGE-M3 даёт 1024-dim embeddings client.create_collection(name, vectors_config=VectorParams( size=dim, distance=Distance.COSINE )) def ingest_document(text: str, metadata: dict, collection: str): # 1. Chunking splitter = RecursiveCharacterTextSplitter( chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP, separators=["\n\n", "\n", ". ", " "] ) chunks = splitter.split_text(text) # 2. Добавить breadcrumb prefix к каждому чанку breadcrumb = f"{metadata.get('title','')} → {metadata.get('section','')}" chunks_with_ctx = [f"{breadcrumb}\n{c}" for c in chunks] # 3. Embedding (batch) resp = httpx.post(EMBEDDING_URL, json={"inputs": chunks_with_ctx}) embeddings = resp.json() # 4. Upsert в Qdrant points = [ PointStruct( id=str(uuid.uuid4()), vector=emb, payload={"text": chunk, **metadata} ) for chunk, emb in zip(chunks, embeddings) ] client.upsert(collection_name=collection, points=points) return len(points) # Hybrid search с reranking def retrieve_and_rerank(query: str, collection: str, top_k: int = 5): # 1. Embed query q_emb = httpx.post(EMBEDDING_URL, json={"inputs": [query]}).json()[0] # 2. Vector search (top 20 candidates) results = client.search(collection, query_vector=q_emb, limit=20) candidates = [r.payload["text"] for r in results] # 3. Rerank с BGE-Reranker rerank_resp = httpx.post( "http://reranker-service:8081/rerank", json={"query": query, "texts": candidates} ).json() # 4. Вернуть top_k по rerank score ranked = sorted( zip(candidates, rerank_resp["scores"]), key=lambda x: x[1], reverse=True ) return [text for text, _ in ranked[:top_k]]
// 05
В 2026 году fine-tuning — не «загрузить JSONL и надеяться». Стандарт: LoRA / QLoRA + строгий eval loop. Полное fine-tuning — только для специальных случаев.
Обновляет 0.1–1% параметров через низкоранговые матрицы. 7B: нужно ~20GB VRAM в BF16. Качество близко к full FT. Основной выбор для production.
База в 4-bit (NF4), адаптеры в BF16. 7B: под 10GB VRAM. 70B: на одной 24GB GPU. 5–10% slower vs LoRA. Выбор при дефиците VRAM.
Обновляет все параметры. 7B требует 4–8×A100. Лучшее качество, но редко оправдано. Нужно при кардинальном изменении reasoning модели.
DoRA: LoRA с weight decomposition — +1–3% vs LoRA при том же VRAM. GaLore: gradient low-rank projection — приближает full FT при LoRA памяти.
| Модель | LoRA (BF16) | QLoRA (INT4) | Inference после |
|---|---|---|---|
| 7B | 20GB (1×A100 40GB) | 8–10GB (1×RTX 3090) | 14GB FP16 / 4GB INT4 |
| 13B | 40GB (1×A100 80GB) | 16GB (1×RTX 4090) | 26GB FP16 / 7GB INT4 |
| 70B | 160GB (4×A100 40GB) | 24GB (1×A100 40GB) | 140GB FP16 / 35GB INT4 |
QLoRA позволяет дообучить 70B модель на одной 24GB GPU. Это изменило landscape в 2024–2026: fine-tuning frontier-class моделей стал доступен без кластера A100. При 2-GPU NVLink setup 70B QLoRA занимает 12–18 часов vs 24–36 часов на одной GPU.
// 06
base_model: meta-llama/Llama-3.1-8B-Instruct model_type: LlamaForCausalLM tokenizer_type: AutoTokenizer # QLoRA config load_in_4bit: true adapter: lora lora_r: 16 # rank — баланс качество/VRAM lora_alpha: 16 # обычно = lora_r lora_dropout: 0.05 lora_target_modules: # все linear слои = лучшее качество - q_proj - k_proj - v_proj - o_proj - gate_proj - up_proj - down_proj # Данные datasets: - path: data/train.jsonl type: chat_template val_set_size: 0.1 sequence_len: 2048 # Training micro_batch_size: 2 gradient_accumulation_steps: 4 num_epochs: 3 # 1–3 epochs, не переобучить learning_rate: 2e-4 # безопасный LR для LoRA lr_scheduler: cosine warmup_steps: 10 # Оптимизации flash_attention: true gradient_checkpointing: true bf16: true # Output output_dir: ./checkpoints/llama-8b-domain save_safetensors: true # Запуск: axolotl train axolotl_config.yml
# Каждая строка — один пример (conversations format) {"conversations": [ {"role": "system", "content": "You are a financial analyst..."}, {"role": "user", "content": "Analyze this P&L statement..."}, {"role": "assistant", "content": "Based on the P&L data: ..."} ]} # Минимальные требования к качеству датасета: # 1. Разнообразие примеров — не одинаковые формулировки # 2. Consistent system prompt — одинаковый во всех примерах # 3. Качество ответов — лучше 500 хороших, чем 5000 плохих # 4. Negative examples — примеры правильного отказа от нерелевантных запросов # 5. Validation split — минимум 10%, лучше 15%
Адаптер сливается с базовой моделью в один файл. Zero overhead при inference. Деплой как обычная модель через vLLM.
Один base model, несколько LoRA адаптеров загружены одновременно. Routing по запросу.
--enable-lora --max-loras 4# Слить адаптер с базовой моделью python -c " from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base = AutoModelForCausalLM.from_pretrained( 'meta-llama/Llama-3.1-8B-Instruct', torch_dtype='bfloat16') model = PeftModel.from_pretrained(base, './checkpoints/llama-8b-domain') merged = model.merge_and_unload() merged.save_pretrained('./models/llama-8b-domain-merged') AutoTokenizer.from_pretrained( 'meta-llama/Llama-3.1-8B-Instruct').save_pretrained( './models/llama-8b-domain-merged') print('Done') " # Запуск через vLLM (merged model = обычная модель) vllm serve ./models/llama-8b-domain-merged \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.90 \ --enable-prefix-caching \ --port 8001 # другой порт — рядом с базовой моделью # Multi-LoRA вариант (base + несколько адаптеров) vllm serve meta-llama/Llama-3.1-8B-Instruct \ --enable-lora \ --lora-modules domain-finance=./adapters/finance \ domain-legal=./adapters/legal \ --max-loras 4 \ --max-lora-rank 16
// 07
60% production систем в 2026 используют оба подхода. Три основных паттерна. Hybrid достигает 96% accuracy в задачах где pure RAG или pure FT дают 80–88%.
Fine-tune на домен/стиль. RAG для текущих знаний. Fine-tuned модель лучше использует retrieved контекст потому что уже понимает доменную терминологию.
Обучить модель на примерах (question, retrieved-docs-with-distractors, correct-answer). Модель учится игнорировать нерелевантные retrieved чанки.
Lightweight classifier маршрутизирует: простые запросы → fine-tuned small model (8B); сложные → large model + RAG. 70–90% cost reduction vs pure frontier model.
// 08
Без evaluation невозможно знать стало ли лучше после изменения chunking, смены embedding модели или нового LoRA checkpoint. Teams that don't measure can't improve.
Насколько ответ опирается на retrieved документы. Метрика галлюцинаций. Цель: >0.85. Ниже 0.7 — серьёзная проблема.
Насколько ответ отвечает на вопрос. Цель: >0.80. Если низкий — проблема в generation prompt или размере контекста.
Насколько полно retrieval покрывает нужную информацию. Если низкий — чанки слишком маленькие или embedding не находит нужное.
Доля retrieved чанков, действительно полезных для ответа. Если низкий — reranker слишком мягкий, много шума в контексте.
from ragas import evaluate from ragas.metrics import ( faithfulness, answer_relevancy, context_recall, context_precision ) from datasets import Dataset # Тестовый набор: вопросы + ожидаемые ответы + ground truth eval_data = { "question": ["Какова политика возврата?", ...], "answer": ["Возврат в течение 30 дней...", ...], # ответ RAG "contexts": [["Документ о возврате..."], ...], # retrieved чанки "ground_truth": ["Правильный ответ...", ...] } results = evaluate( Dataset.from_dict(eval_data), metrics=[faithfulness, answer_relevancy, context_recall, context_precision] ) print(results) # {faithfulness: 0.87, answer_relevancy: 0.82, ...} # Минимум 50 вопросов для значимой оценки. # Запускать после каждого изменения в pipeline.
📊 RAG метрики в production
🔧 Fine-tuning метрики
⚠️ Сигналы деградации
🚨 Когда переобучать
// 09
Определяет use case и constraint. Принимает решение RAG vs FT vs hybrid на основе data/update/latency требований. Устанавливает eval criteria. Budget.
Реализует RAG pipeline и/или fine-tuning. Eval loop, chunking эксперименты, hyperparameter tuning. Production деплой и мониторинг метрик качества.
Подготовка и labeling данных для FT. Качество ingested документов для RAG. Без качественных данных ни подход не работает.
Manager + Engineer: определить проблему. Это знание или поведение? Собрать 50 реальных примеров запросов. Попробовать улучшенный промпт — если решает 80% проблем, остановиться здесь.
Engineer: Qdrant + embedding model + базовый retrieval. Ingestion 100–500 ключевых документов. Базовый eval dataset (50 Q&A). Измерить baseline faithfulness и context_recall.
Добавить hybrid search (BM25 + dense). Добавить BGE-Reranker. Измерить improvement. Типично: +15–35% context precision. Настроить chunking если recall низкий.
Подготовить 500+ примеров. QLoRA обучение (несколько часов на A100). Eval: task accuracy, catastrophic forgetting check. Merge адаптера. A/B тест vs base model.
Автоматические алерты: retrieval latency spike, empty retrieval rate, error rate. Никакого ручного контроля в штатном режиме.
RAGAS метрики за неделю: trend faithfulness, answer_relevancy. Есть ли новые документы для ingestion? FT: format compliance rate, task success.
Нужно ли переобучение? Устарели ли документы в RAG? Стоит ли добавить hybrid паттерн? User satisfaction метрики. Cost per query анализ.
// 10
Главный антипаттерн 2026: тратить 6 недель на fine-tuning когда проблема решается за 3 дня через RAG с хорошим chunking + reranking. Начинать с наименее дорогого решения. Fine-tuning — это инструмент для поведения, не для знаний. Не путать задачу.