System Design · LLM Engineering

LLM
System Design

Как принимать архитектурные решения: KPI и трейдоффы, выбор стратегии (промпт vs LoRA vs RAG), построение датасета, оценка, пайплайны, fallback. От первого запроса до продакшена.

// Стратегия под ваши ограничения
Данных для обучения
Латентность
Специфика домена
Бюджет GPU
Рекомендуемая стратегия
—
—

метрики и компромиссы

KPI и Tradeoffs

Прежде чем выбирать архитектуру — определи измеримые KPI. Без явных метрик любое решение оправдано, значит ни одно не обосновано. LLM-системы имеют четыре основных оси трейдоффов.

Quality vs Latency

Качество ↔ Скорость

Большая модель = лучше. Но каждый дополнительный billion параметров добавляет latency. GPT-4 = ~2s, GPT-4o-mini = ~300ms на аналогичных промптах.

Llama-3-70B: ~800ms @ A100 | Llama-3-8B: ~120ms
Cost vs Performance

Стоимость ↔ Результат

GPT-4o: $5/1M tokens input. GPT-4o-mini: $0.15/1M. При 10M запросов/месяц разница = $49,000 ежемесячно. Но если mini не решает задачу — экономия иллюзорна.

ROI = (quality_gain × business_value) / cost
Freshness vs Stability

Актуальность ↔ Стабильность

RAG с живой базой — актуальные данные, но нестабильность: новый документ может сломать ответ. Fine-tuned model стабильна, но устаревает.

RAG: свежо + объяснимо | FT: стабильно + быстро
Controllability vs Generality

Управляемость ↔ Гибкость

Специализированная модель предсказуема на целевых задачах, но хуже на краевых случаях. Общая модель гибкая, но непредсказуемая в деталях.

SFT: предсказуемо | GPT-4: универсально

Как измерять: примеры конкретных KPI

ЗадачаБизнес-метрикаТехническая метрикаПорог «достаточно»
Саппорт-бот% решённых без оператораIntent accuracy, ROUGE vs шаблон>80% resolve rate
NER в документахВремя обработки документаF1 по сущностям, Precision@KF1 > 0.90
СуммаризацияВремя прочтения уменьшилосьROUGE-L, BERTScore, LLM-judgeLLM-judge > 4/5
Поиск (RAG)Click-through на результатыHit@5, MRR, nDCG@10Hit@5 > 0.85
Генерация кода% принятых PR от AIPass@K (тесты), BLEUPass@1 > 0.70
Ловушка «хорошей модели»: команды часто оптимизируют academic метрики (ROUGE, BLEU) которые плохо коррелируют с бизнес-ценностью. Всегда добавляй сквозную бизнес-метрику и регулярно проверяй корреляцию между техническими и бизнес-KPI.

архитектурный выбор

Классический ML vs LLM-подходы

LLM — не всегда правильный ответ. Понимание того, когда классический ML лучше, — фундаментальный навык system design.

КЛАССИЧЕСКИЙ ML ✓ Структурированные данные (таблицы) ✓ Воспроизводимые, детерминированные предсказания ✓ Объяснимость (SHAP, LIME) ✓ Низкая latency (<10ms) ✓ Миллионы примеров — достаточно ✗ Не масштабируется на open-ended задачи ✗ Требует ручного feature engineering ✗ Плохо обобщается за пределы train dist. Серая зона → контекст LLM-ПОДХОД ✓ Неструктурированный текст, мультимодально ✓ Малые данные (zero/few-shot) ✓ Open-ended генерация ✓ Быстрый прототип (промпт за часы) ✓ Мультизадачность без переобучения ✗ Высокая latency (100ms–5s) ✗ Высокая стоимость инференса ✗ Недетерминированность, hallucinations

// ВЫБОР АРХИТЕКТУРЫ · серая зона — когда оба подхода применимы, решает контекст задачи

Когда выбирать что: конкретные сценарии

СценарийРекомендацияПричина
Спам-фильтрация emailКлассический ML (XGBoost)Структурированные сигналы, миллионы примеров, нужна скорость <5ms
Классификация тональности (50 классов)LLM + few-shot → SFTСемантические нюансы, мало данных на класс
Рекомендательная системаКлассический ML + embeddingsНужна sub-10ms latency, миллионы item'ов
Извлечение информации из договоровLLM (RAG или SFT)Неструктурированный текст, complex reasoning
Предсказание churnКлассический MLТабличные данные, объяснимость нужна бизнесу
Ответы в поддержку (FAQ)RAG + LLMЗнания часто обновляются, нет данных для FT

поэтапный подход

Лестница сложности

Золотое правило system design: начни с простого, усложняй только когда необходимо. Каждая ступень лестницы решает реальную проблему предыдущей — не прыгай через ступени.

1. ZERO-SHOT PROMPTING Простой промпт к API. Работает за часы. Стоимость: 0 GPU. Когда достаточно: задача хорошо описывается словами, нет специфических форматов. Данные: 0 Время: часы 2. FEW-SHOT PROMPTING 3-8 примеров в контексте. Значительно улучшает format compliance. Когда нужно: нестандартный формат вывода, domain-specific стиль. Данные: 5–50 Время: дни 3. RAG (Retrieval-Augmented Generation) Векторная база + retrieval + reranking. Добавляет актуальные знания без переобучения. Когда нужно: данные часто меняются, объём > context window. Данные: корпус Время: дни–недели 4. SFT / LoRA / QLoRA Fine-tuning на доменных данных. Инсталлирует стиль, формат, знания. Когда нужно: промпт не справляется с форматом, нужна скорость (без large model). Данные: 1k–100k Время: дни–недели 5. RAG + RERANKING + SFT + ALIGNMENT Полный стек: доменная модель + актуальные знания через RAG + reranking + DPO/RLHF. Когда нужно: production quality, специализированный домен. Данные: >50k Время: недели–месяцы СЛОЖНОСТЬ →

// ЛЕСТНИЦА СЛОЖНОСТИ · каждая ступень добавляет затраты и требует обоснования

Ступень 1–2: Zero/Few-shot — пример хорошего промпта

Python · structured few-shot для NER
from openai import OpenAI
import json

client = OpenAI()

# Хорошо структурированный few-shot промпт для NER
SYSTEM = """Ты — система извлечения информации из деловых писем.
Извлекай: PERSON, ORGANIZATION, DATE, AMOUNT, LOCATION.
Отвечай ТОЛЬКО валидным JSON. Не добавляй пояснений."""

FEW_SHOT_EXAMPLES = [
    {
        "role": "user",
        "content": "Иван Петров из ООО Ромашка предлагает встретиться 15 мая в Москве."
    },
    {
        "role": "assistant",
        "content": """{"entities": [
  {"text": "Иван Петров", "type": "PERSON"},
  {"text": "ООО Ромашка", "type": "ORGANIZATION"},
  {"text": "15 мая", "type": "DATE"},
  {"text": "Москве", "type": "LOCATION"}
]}"""
    },
    {
        "role": "user",
        "content": "Платёж на 500 000 рублей от Сбербанка поступит 20 декабря."
    },
    {
        "role": "assistant",
        "content": """{"entities": [
  {"text": "500 000 рублей", "type": "AMOUNT"},
  {"text": "Сбербанка", "type": "ORGANIZATION"},
  {"text": "20 декабря", "type": "DATE"}
]}"""
    },
]

def extract_entities(text: str) -> dict:
    messages = [
        {"role": "system", "content": SYSTEM},
        *FEW_SHOT_EXAMPLES,
        {"role": "user", "content": text},
    ]
    resp = client.chat.completions.create(
        model="gpt-4o-mini",    # дешевле чем gpt-4o, часто достаточно
        messages=messages,
        temperature=0,           # детерминизм для структурированного вывода
        response_format={"type": "json_object"},  # JSON mode
    )
    return json.loads(resp.choices[0].message.content)

result = extract_entities("Директор Алексей Смирнов подписал договор с Газпромом на 2 млн.")
# {"entities": [
#   {"text": "Алексей Смирнов", "type": "PERSON"},
#   {"text": "Газпромом", "type": "ORGANIZATION"},
#   {"text": "2 млн", "type": "AMOUNT"}]}

Ступень 3: RAG с реранкингом

Python · RAG pipeline + cross-encoder reranking
from sentence_transformers import SentenceTransformer, CrossEncoder
import faiss, numpy as np
from openai import OpenAI

# ── Компоненты RAG-пайплайна ─────────────────────────────────
# 1. Bi-encoder: быстрый первичный retrieval (ANN search)
bi_encoder = SentenceTransformer("BAAI/bge-m3")  # поддерживает RU

# 2. Cross-encoder: медленный но точный реранкинг топ-K
cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")

# 3. FAISS index (в проде: Qdrant, Weaviate, Pinecone)
def build_index(documents: list[str]):
    embeddings = bi_encoder.encode(documents, normalize_embeddings=True)
    index = faiss.IndexFlatIP(embeddings.shape[1])
    index.add(embeddings)
    return index, embeddings

def rag_pipeline(
    query: str,
    documents: list[str],
    index: faiss.Index,
    top_k_retrieve: int = 20,   # первичный retrieval: много
    top_k_rerank: int = 4,      # после реранкинга: мало
) -> str:
    # Шаг 1: Bi-encoder retrieval
    q_emb = bi_encoder.encode([query], normalize_embeddings=True)
    D, I = index.search(q_emb, top_k_retrieve)
    candidates = [documents[i] for i in I[0]]

    # Шаг 2: Cross-encoder reranking
    # Cross-encoder читает (query, doc) вместе → точнее bi-encoder
    pairs = [(query, doc) for doc in candidates]
    scores = cross_encoder.predict(pairs)
    ranked = sorted(zip(scores, candidates), reverse=True)
    top_docs = [doc for _, doc in ranked[:top_k_rerank]]

    # Шаг 3: Augmented generation
    context = "\n\n".join([f"[{i+1}] {d}" for i, d in enumerate(top_docs)])
    prompt = f"""Ответь на вопрос, используя ТОЛЬКО предоставленный контекст.
Если ответа нет в контексте — скажи "Не знаю".

Контекст:
{context}

Вопрос: {query}
Ответ:"""

    client = OpenAI()
    resp = client.chat.completions.create(
        model="gpt-4o-mini", temperature=0,
        messages=[{"role": "user", "content": prompt}]
    )
    return resp.choices[0].message.content

# Почему reranking важен:
# Bi-encoder: recall@20 ≈ 0.85, precision@4 ≈ 0.55
# + Cross-encoder: recall@4 ≈ 0.82, precision@4 ≈ 0.78
# Reranking добавляет +42% precision при том же recall!

данные для оценки

Построение Golden-датасета

Golden dataset — небольшая (100–1000 примеров), высококачественная, репрезентативная выборка с проверенными ответами. Это твоя North Star: любое изменение системы оценивается по ней.

Weak supervision: масштабирование без аннотации

Когда нет бюджета на ручную разметку тысяч примеров — используй weak supervision: несколько слабых (шумных) источников сигнала объединяются через label model.

Python · Snorkel weak supervision
from snorkel.labeling import labeling_function, PandasLFApplier
from snorkel.labeling.model import LabelModel
import pandas as pd
import re

POSITIVE, NEGATIVE, ABSTAIN = 1, 0, -1

# Labeling functions (LF) — слабые, шумные сигналы
@labeling_function()
def lf_keywords_positive(x):
    """Позитивные ключевые слова → POSITIVE."""
    pos_words = ["отлично", "супер", "рекомендую", "доволен"]
    return POSITIVE if any(w in x.text.lower() for w in pos_words) else ABSTAIN

@labeling_function()
def lf_keywords_negative(x):
    neg_words = ["ужасно", "плохо", "разочарован", "верну"]
    return NEGATIVE if any(w in x.text.lower() for w in neg_words) else ABSTAIN

@labeling_function()
def lf_rating_high(x):
    """Оценка ≥ 4 → POSITIVE."""
    return POSITIVE if hasattr(x, "rating") and x.rating >= 4 else ABSTAIN

@labeling_function()
def lf_llm_judge(x):
    """LLM как labeling function — дорого но мощно."""
    # Промпт к GPT-4o-mini для разметки
    response = call_llm(f"Тональность: '{x.text}'. Ответь: POSITIVE/NEGATIVE")
    if "POSITIVE" in response: return POSITIVE
    if "NEGATIVE" in response: return NEGATIVE
    return ABSTAIN

# Применяем LF к датасету
lfs = [lf_keywords_positive, lf_keywords_negative, lf_rating_high, lf_llm_judge]
applier = PandasLFApplier(lfs=lfs)
L_train = applier.apply(df=train_df)   # матрица (N, len(lfs))

# Label Model: агрегирует шумные метки учитывая точность каждой LF
label_model = LabelModel(cardinality=2)
label_model.fit(L_train, n_epochs=300)
probs = label_model.predict_proba(L_train)   # вероятности классов
# Используем только высоко-уверенные примеры (prob > 0.8) в датасете

Active Learning: умный выбор примеров

Active learning — модель сама запрашивает разметку для примеров, в которых она наименее уверена. Это снижает стоимость разметки в 3–10× при том же качестве.

Python · uncertainty sampling
import numpy as np
from scipy.stats import entropy

def uncertainty_sampling(
    model,
    unlabeled_pool: list[str],
    n_to_label: int = 50,
    strategy: str = "entropy"
) -> list[int]:
    """Выбираем наиболее неопределённые примеры для разметки."""
    # Получаем вероятности классов от модели
    probs = np.array([
        model.predict_proba([text])[0]
        for text in unlabeled_pool
    ])   # shape: (N, num_classes)

    if strategy == "entropy":
        # Энтропия: максимум при равномерном распределении
        scores = entropy(probs, axis=1)
    elif strategy == "least_confident":
        # Наименее уверен = минимальный max_prob
        scores = 1 - probs.max(axis=1)
    elif strategy == "margin":
        # Margin sampling: разница между топ-2 вероятностями
        sorted_probs = np.sort(probs, axis=1)[:, ::-1]
        scores = 1 - (sorted_probs[:, 0] - sorted_probs[:, 1])

    # Возвращаем индексы n_to_label самых неопределённых
    uncertain_idx = np.argsort(scores)[::-1][:n_to_label]
    return uncertain_idx.tolist()

Human-in-the-loop: когда нужен человек

  • Bootstrapping: первые 100–200 примеров — только люди. Нет данных для моделей и weak supervision.
  • Ambiguous cases: примеры где модель не уверена (entropy высокая) или где labeling functions не согласны.
  • Error analysis: регулярный аудит ошибок модели человеком — находишь системные проблемы в данных.
  • Golden set maintenance: проверяй golden set раз в квартал — требования меняются, старые эталоны устаревают.

система оценки

Онлайн/Оффлайн оценка

Оффлайн-оценка: LLM-as-Judge

LLM-as-Judge (Zheng et al., 2023) — использование мощной LLM (GPT-4, Claude) для автоматической оценки качества ответов. Работает как offline, так и как метрика в CI/CD.

Python · LLM-as-Judge с G-Eval
from openai import OpenAI
import json

client = OpenAI()

JUDGE_TEMPLATE = """Оцени качество ответа ассистента по критериям.
Отвечай ТОЛЬКО JSON с числами от 1 до 5.

Вопрос пользователя: {question}
Ответ ассистента: {answer}
Эталонный ответ: {reference}

Оцени:
- relevance: насколько ответ отвечает на вопрос (1-5)
- accuracy: фактическая точность (1-5)  
- completeness: полнота ответа (1-5)
- clarity: ясность изложения (1-5)

JSON: {{"relevance": N, "accuracy": N, "completeness": N, "clarity": N, "overall": N}}"""

def llm_judge(question: str, answer: str, reference: str = "") -> dict:
    prompt = JUDGE_TEMPLATE.format(
        question=question, answer=answer, reference=reference
    )
    resp = client.chat.completions.create(
        model="gpt-4o",   # лучше использовать сильную модель для судьи
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        response_format={"type": "json_object"},
    )
    return json.loads(resp.choices[0].message.content)

# Batch evaluation на golden set
def evaluate_system(system_fn, golden_set: list[dict]) -> dict:
    all_scores = []
    for example in golden_set:
        answer = system_fn(example["question"])
        scores = llm_judge(
            question=example["question"],
            answer=answer,
            reference=example["reference_answer"],
        )
        all_scores.append(scores)

    # Агрегация: mean + std для каждой метрики
    import numpy as np
    metrics = {}
    for key in ["relevance", "accuracy", "completeness", "clarity", "overall"]:
        vals = [s[key] for s in all_scores if key in s]
        metrics[key] = {"mean": np.mean(vals), "std": np.std(vals)}
    return metrics

Онлайн-оценка: A/B тестирование

Оффлайн-метрики не заменяют реальный пользовательский сигнал. A/B тест — единственный способ узнать, работает ли изменение в продакшене.

Python · простой A/B framework для LLM
import hashlib
from scipy import stats

def get_variant(user_id: str, experiment: str,
                  traffic_split: float = 0.5) -> str:
    """Детерминированное назначение в вариант по user_id."""
    seed = f"{user_id}:{experiment}"
    h = hashlib.md5(seed.encode()).hexdigest()
    bucket = int(h[:4], 16) / 65535   # 0..1
    return "treatment" if bucket < traffic_split else "control"

def check_significance(control_rates: list[float],
                         treatment_rates: list[float],
                         alpha: float = 0.05) -> dict:
    """t-test для сравнения метрик control vs treatment."""
    t_stat, p_value = stats.ttest_ind(control_rates, treatment_rates)
    ctrl_mean = sum(control_rates) / len(control_rates)
    trt_mean = sum(treatment_rates) / len(treatment_rates)
    relative_lift = (trt_mean - ctrl_mean) / ctrl_mean * 100
    return {
        "significant": p_value < alpha,
        "p_value": round(p_value, 4),
        "relative_lift_pct": round(relative_lift, 2),
        "control_mean": ctrl_mean, "treatment_mean": trt_mean,
    }

# Пример: тестируем RAG vs zero-shot
# control:   resolve_rate = [0.74, 0.72, 0.76, ...]  (zero-shot)
# treatment: resolve_rate = [0.81, 0.83, 0.79, ...]  (RAG)
# check_significance(...) → {"significant": True,
#   "p_value": 0.003, "relative_lift_pct": 9.5}
Сколько пользователей нужно: для обнаружения лифта в 5% с 80% мощностью и α=0.05 нужно ~600 пользователей на вариант при baseline 70%. Используй калькулятор sample size (например, statsmodels) перед запуском теста.

архитектурные паттерны

Пайплайны для конкретных задач

NER-пайплайн: от промпта до продакшена

Входной текст Препроцессинг чистка, сегм., токен. LLM (NER) few-shot / SFT / LoRA Постпроцессинг валидация, дедупл. Confidence filter + fallback Вывод JSON fallback

// NER PIPELINE · препроцессинг → LLM → постпроцессинг → confidence filter → fallback

ЗадачаРекомендованный стекКлючевые метрикиЛовушки
NERFew-shot → GLiNER/SFT, постпроцессинг (BIO → span), confidence thresholdF1 по типам, span-level precisionПерекрывающиеся сущности, вложенные
СуммаризацияПромпт + длина контроль → RAG для длинных doc, LLM-judge + ROUGEROUGE-L, BERTScore, faithfulnessГаллюцинации, потеря ключевых фактов
КлассификацияFew-shot → SFT если >500 классов, calibration, threshold tuningMacro-F1, calibration ECELabel imbalance, drift с временем
QA / ПоискBM25 + dense retrieval + cross-encoder reranking + LLMHit@5, MRR, FaithfulnessRetrieval bottleneck, context overflow
Извлечение данныхStructured output (JSON mode), валидация схемы PydanticField-level F1, schema validity %Опциональные поля, вложенные структуры

Суммаризация: контроль галлюцинаций

Python · суммаризация с проверкой faithfulness
from openai import OpenAI

client = OpenAI()

def summarize_with_faithfulness_check(document: str,
                                          max_sentences: int = 5) -> dict:
    """Суммаризация с автоматической проверкой фактической точности."""

    # Шаг 1: Суммаризация
    summary_prompt = f"""Создай краткое резюме документа в {max_sentences} предложениях.
Включай ТОЛЬКО факты из документа. Не добавляй внешних знаний.

Документ:
{document}

Резюме:"""

    summary_resp = client.chat.completions.create(
        model="gpt-4o-mini", temperature=0.3,
        messages=[{"role": "user", "content": summary_prompt}]
    )
    summary = summary_resp.choices[0].message.content

    # Шаг 2: Проверка faithfulness (NLI-подход через LLM)
    faithfulness_prompt = f"""Проверь каждое утверждение резюме на соответствие документу.
Отвечай JSON: {{"score": 0-1, "unsupported_claims": ["..."]}}

Документ: {document}
Резюме: {summary}"""

    faith_resp = client.chat.completions.create(
        model="gpt-4o", temperature=0,
        messages=[{"role": "user", "content": faithfulness_prompt}],
        response_format={"type": "json_object"},
    )
    import json
    faith_result = json.loads(faith_resp.choices[0].message.content)

    return {
        "summary": summary,
        "faithfulness_score": faith_result["score"],
        "unsupported_claims": faith_result.get("unsupported_claims", []),
        "is_reliable": faith_result["score"] > 0.9,
    }

# faithfulness_score < 0.9 → тригерим fallback (человеческий ревью)

надёжность системы

Fallback-стратегии

LLM-системы могут отказывать по множеству причин: галлюцинации, низкая уверенность, timeout, unsafe content, out-of-scope запросы. Хорошая система имеет предсказуемое поведение при любом отказе.

Tier 1: Primary System GPT-4o / Llama-70B / Custom fine-tuned model — основной путь, высокое качество ~95% запросов ↓ FAIL: latency > threshold / low confidence / error / hallucination detected Tier 2: Fallback LLM Меньшая модель (GPT-4o-mini / Llama-8B) — быстрее, дешевле, часто достаточно ~4% запросов ↓ FAIL: всё ещё не уверен / out-of-scope / toxic Tier 3: Rule-based / Шаблон Детерминированный ответ / FAQ / human handoff — предсказуем, всегда работает ~1% запросов ↓ FAIL (никогда): последний слой всегда отвечает что-то осмысленное

// FALLBACK HIERARCHY · три уровня — каждый обеспечивает покрытие для отказов предыдущего

Python · fallback router с confidence scoring
import time
from openai import OpenAI
from dataclasses import dataclass

@dataclass
class LLMResponse:
    text: str
    confidence: float   # 0..1
    latency_ms: int
    tier_used: str
    needs_review: bool

def call_with_fallback(
    query: str,
    system_prompt: str,
    latency_limit_ms: int = 2000,
    confidence_threshold: float = 0.7,
) -> LLMResponse:

    client = OpenAI()

    # Tier 1: Primary (большая модель)
    try:
        t0 = time.time()
        resp = client.chat.completions.create(
            model="gpt-4o", temperature=0,
            messages=[{"role": "system", "content": system_prompt},
                      {"role": "user", "content": query}],
            timeout=latency_limit_ms / 1000,
        )
        latency = int((time.time() - t0) * 1000)
        text = resp.choices[0].message.content

        # Простая confidence heuristic через logprobs
        confidence = compute_confidence(resp)
        if confidence >= confidence_threshold:
            return LLMResponse(text, confidence, latency, "tier1", False)

    except Exception as e:
        log_error("tier1_failed", error=str(e), query=query)

    # Tier 2: Fallback (меньшая модель, быстрее)
    try:
        t0 = time.time()
        resp = client.chat.completions.create(
            model="gpt-4o-mini", temperature=0,
            messages=[{"role": "system", "content": system_prompt},
                      {"role": "user", "content": query}],
            timeout=1.0,
        )
        latency = int((time.time() - t0) * 1000)
        confidence = compute_confidence(resp)
        text = resp.choices[0].message.content
        return LLMResponse(text, confidence, latency, "tier2",
                             needs_review=confidence < 0.5)

    except Exception as e:
        log_error("tier2_failed", error=str(e))

    # Tier 3: Детерминированный fallback (всегда работает)
    template = get_template_response(query)  # шаблонные ответы по ключевым словам
    return LLMResponse(template, 0.3, 5, "tier3", needs_review=True)

финальный выбор

Выбор стратегии обучения

Когда промпт и RAG не решают задачу — нужно обучение. Выбор между CPT, TAPT, DAPT, SFT и alignment-методами зависит от типа проблемы, данных и ограничений.

Zero/Few-shot
$ (API only)
0–50 примеров
Общее следование инструкции
Старт, прототип, общий домен
RAG
$$ (индекс + API)
Корпус документов
Актуальность, factual accuracy
Знания меняются, объём > ctx
TAPT / DAPT
$$ (10k–1M tokens)
Unlabeled domain text
Понимание домена, vocab
Специализированный домен (мед, юр)
SFT (LoRA)
$$–$$$ (1–2 GPU)
1k–100k пар
Формат вывода, стиль, задача
Нет нужного формата в промпте
SFT (Full FT)
$$$$ (>4 GPU)
>100k пар
Максимальная специализация
Продакшен, большой объём запросов
DPO / RLHF
$$$$
>10k пар (chosen/rejected)
Безопасность, предпочтения
После SFT, нужен alignment
GRPO
$$$$
Верифицируемые задачи
Reasoning, математика, код
R1-style thinking, точные задачи

CPT / TAPT / DAPT: когда и зачем

ТерминРасшифровкаОбъём данныхПрименение
CPTContinued Pre-Training1B–100B токеновКардинальная смена домена. Медицинский LLM с нуля.
DAPTDomain-Adaptive Pre-Training100M–10B токеновАдаптация к домену. BERT → BioBERT через PubMed.
TAPTTask-Adaptive Pre-Training1M–100M токеновАдаптация к типу задачи. Финансовые отчёты → FT на NER.
Python · TAPT: task-adaptive continued pretraining
from transformers import (
    AutoModelForCausalLM, AutoTokenizer,
    DataCollatorForLanguageModeling, TrainingArguments, Trainer
)
from datasets import load_dataset

# TAPT: продолжаем предобучение на доменном тексте (CLM)
# Цель: научить модель «говорить» на языке домена
# Пример: юридические договоры → NLP на договорах

tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B")

# Доменный корпус: юридические тексты (unlabeled)
domain_corpus = load_dataset("text", data_files={"train": "legal_contracts.txt"})

def tokenize(examples):
    return tokenizer(examples["text"], truncation=True,
                      max_length=2048, return_special_tokens_mask=True)

tok_ds = domain_corpus.map(tokenize, batched=True, remove_columns=["text"])

# DataCollatorForLanguageModeling для CLM (не MLM)
collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

# TAPT: LR ниже чем в SFT, больше epochs
args = TrainingArguments(
    output_dir="./qwen-legal-tapt",
    num_train_epochs=2,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=8,
    learning_rate=1e-4,    # выше чем SFT, ниже чем pretraining
    lr_scheduler_type="cosine",
    bf16=True,
    logging_steps=100,
)

trainer = Trainer(
    model=model, args=args,
    train_dataset=tok_ds["train"],
    data_collator=collator,
)
trainer.train()
# Результат: domain-adapted base model
# Следующий шаг: SFT на (instruction, output) парах для этого домена

Constitutional AI: встраивание правил в систему

Python · упрощённый Constitutional AI pipeline
from openai import OpenAI
client = OpenAI()

# Constitutional AI: модель сама критикует и исправляет ответы
# по набору принципов (конституции)

CONSTITUTION = [
    "Ответ не должен содержать личные данные пользователей.",
    "Ответ должен признавать неопределённость если она есть.",
    "Ответ не должен содержать юридических советов без оговорки.",
    "Ответ должен быть конструктивным, не осуждающим.",
]

def constitutional_generate(user_query: str, n_revisions: int = 2) -> str:
    """Генерируем ответ → критикуем по конституции → ревизируем."""

    # Шаг 1: Первоначальный ответ
    resp = client.chat.completions.create(
        model="gpt-4o-mini", temperature=0.7,
        messages=[{"role": "user", "content": user_query}]
    )
    draft = resp.choices[0].message.content

    for _ in range(n_revisions):
        principles = "\n".join([f"- {p}" for p in CONSTITUTION])

        # Шаг 2: Критика по принципам
        critique_prompt = f"""Оцени ответ по каждому принципу.
Принципы:
{principles}

Ответ:
{draft}

Укажи нарушения (если есть)."""

        critique_resp = client.chat.completions.create(
            model="gpt-4o", temperature=0,
            messages=[{"role": "user", "content": critique_prompt}]
        )
        critique = critique_resp.choices[0].message.content

        # Шаг 3: Ревизия с учётом критики
        revision_prompt = f"""Перепиши ответ, устранив нарушения.
Нарушения: {critique}
Исходный ответ: {draft}
Исправленный ответ:"""

        rev_resp = client.chat.completions.create(
            model="gpt-4o-mini", temperature=0.3,
            messages=[{"role": "user", "content": revision_prompt}]
        )
        draft = rev_resp.choices[0].message.content

    return draft   # финальный ответ после n_revisions итераций
Итоговое правило выбора стратегии: если промпт работает → используй промпт. Если нет → добавь RAG. Если RAG недостаточно → SFT с LoRA. Если формат неправильный после SFT → больше и чище данные, не другой метод. Если нужен alignment → DPO после SFT. GRPO только для верифицируемых задач. CPT/DAPT — только при полном погружении в новый домен.