In-Context Learning: как LLM учится без обновления весов

opensourceaillmprompt-engineeringiclit
← Back to Blog

Введение: что такое In-Context Learning?

Обычное обучение (Fine-Tuning):
  1. Берём модель
  2. Обновляем веса на данных
  3. Модель запоминает навсегда

In-Context Learning (ICL):
  1. Берём модель (веса НЕ меняем!)
  2. Показываем примеры в промпте
  3. Модель "учится" на время сессии
  4. После окончания сессии — забывает

In-Context Learning — это способность LLM обучаться на примерах, предоставленных в контексте промпта, без обновления весов модели.

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


Почему ICL работает?

LLM — это не просто "предсказатель следующего токена".

Она обладает способностью:
  1. Понимать паттерны из примеров
  2. Применять их к новым задачам
  3. Адаптироваться к формату
  
Это возможно благодаря:
  - Огромному количеству данных при pretraining
  - Attention механизму, который "фокусируется" на примерах
  - Трансформерной архитектуре, которая может имитировать обучение

Научное объяснение

ICL связан с мета-обучением (meta-learning).

LLM обучается при pretraining НЕ ТОЛЬКО:
  "предсказывать следующий токен"
  
НО И:
  "учиться предсказывать следующий токен
   на основе примеров в контексте"

Это как если бы вы учились НЕ ТОЛЬКО решать задачи,
НО И учиться решать новые типы задач по примерам.

Исследование Wei et al., 2022, представившее концепцию ICL, показало что способность к in-context learning возникает при определённом размере модели — примерно от 100B параметров. Меньшие модели практически не способны к ICL.


Zero-Shot vs Few-Shot vs One-Shot

Zero-Shot ICL

Prompt:
  "Переведите на французский: 'Hello'"

Ответ:
  "Bonjour"

Без примеров! Модель использует знания из pretraining.

Zero-shot — самый простой вариант. Вы просто просите модель выполнить задачу без каких-либо примеров. Модель полагается на знания, полученные при pretraining.

One-Shot ICL

Prompt:
  Переведите на французский:
  
  Hello → Bonjour
  
  Goodbye → ?

Ответ:
  "Au revoir"

Один пример показывает паттерн.

One-shot — один пример помогает модели понять формат ответа и специфику задачи.

Few-Shot ICL

Prompt:
  Переведите на французский:
  
  Hello → Bonjour
  Goodbye → Au revoir
  Thank you → Merci
  Please → S'il vous plaît
  
  Sorry → ?

Ответ:
  "Désolé"

Больше примеров = лучше понимание паттерна.

Few-shot — 3-5 примеров обычно дают наилучший результат. Слишком много примеров может "сбить с толку" модель или превысить лимит контекста.

Сравнение

                    | Zero-Shot     | One-Shot      | Few-Shot (3-5)
  ------------------|---------------|---------------|------------------
  Точность          | 60-80%        | 70-85%        | 80-95%
  Длина промпта     | Короткий      | Средний       | Длиннее
  Стоимость         | Дешевле       | Средне        | Дороже
  Надёжность        | Низкая        | Средняя       | Высокая
  Скорость          | Быстрее       | Средне        | Медленнее

Форматирование промптов для ICL

Паттерн 1: Вопрос-Ответ

Q: Какова столица Франции?
A: Париж

Q: Какова столица Японии?
A: Токио

Q: Какова столица Бразилии?
A:

Простой и эффективный паттерн для фактических вопросов. Модель понимает формат и продолжает его.

Паттерн 2: Текст-Метка (Классификация)

Текст: "Отличный продукт! Рекомендую всем."
Метка: позитивный

Текст: "Ужасное обслуживание, никогда больше!"
Метка: негативный

Текст: "Доставка была быстрой, но товар повреждён."
Метка:

Для классификации текстов. Модель понимает что нужно предсказать метку класса.

Паттерн 3: Исходный код-Комментарий

# Исходный код:
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Комментарий: "Рекурсивная реализация чисел Фибоначчи"

# Исходный код:
def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr)//2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)

# Комментарий:

Для генерации комментариев к коду. Модель понимает паттерн форматирования.

Паттерн 4: Structured ICL (JSON)

{
  "input": "Какая погода в Москве?",
  "output": {
    "city": "Москва",
    "temperature": 22,
    "condition": "ясно"
  }
}

{
  "input": "Какая погода в Париже?",
  "output": {
    "city": "Париж",
    "temperature": 18,
    "condition": "облачно"
  }
}

{
  "input": "Какая погода в Токио?",
  "output":

Для структурированных ответов. JSON формат помогает получить предсказуемый вывод.

Паттерн 5: Chain-of-Thought ICL

Q: У меня есть 5 яблок. Я купил ещё 3 и съел 2. Сколько у меня яблок?
A: Давайте посчитаем пошагово:
   1. Было: 5 яблок
   2. Купили: +3 яблока
   3. Съели: -2 яблока
   4. Итого: 5 + 3 - 2 = 6 яблок
   Ответ: 6

Q: У меня есть 10 конфет. Я отдал 4 другу и купил ещё 7. Сколько у меня конфет?
A:

Chain-of-Thought ICL показывает модели как рассуждать пошагово. Это значительно улучшает качество ответов на математические и логические задачи.


Порядок примеров имеет значение

Исследование показывает:
  1. Порядок примеров влияет на результат
  2. Первые примеры (primacy effect) важнее
  3. Последние примеры (recency effect) тоже важны
  4. Случайная перестановка может изменить ответ

Рекомендации:
  - Сортировать примеры по релевантности
  - Использовать разнообразные примеры
  - Избегать однообразных паттернов

Эксперимент с порядком

import random

def test_order_sensitivity(model, examples, question):
    """Тестируем чувствительность к порядку примеров."""
    answers = []
    
    for i in range(10):
        # Перемешиваем примеры
        shuffled = random.sample(examples, len(examples))
        
        prompt = build_prompt(shuffled, question)
        answer = model.generate(prompt)
        answers.append(answer)
    
    # Проверяем согласованность
    unique_answers = set(answers)
    print(f"Уникальных ответов: {len(unique_answers)}")
    print(f"Согласованность: {1 - len(unique_answers)/10:.1%}")
    
    # Если < 80% согласованности — модель нестабильна

Оптимальная стратегия сортировки

1. Сначала самые релевантные примеры
2. Затем разнообразные примеры
3. В конце — примеры с похожим форматом на целевой

Пример для классификации:
  1. Пример из ТОГО ЖЕ домена (релевантность)
  2. Пример из другого домена (разнообразие)
  3. Пример с неоднозначным текстом (сложность)

Выбор примеров для ICL

Критерии выбора

Хорошие примеры для ICL:
  1. Релевантны задаче (тот же домен)
  2. Разнообразны (разные варианты)
  3. Правильные (без ошибок)
  4. Чёткие (однозначные ответы)
  5. В правильном формате

Плохие примеры:
  1. Из другой предметной области
  2. Противоречивые
  3. С ошибками
  4. Двусмысленные
  5. В неправильном формате

Retrieval-Augmented ICL

from sentence_transformers import SentenceTransformer
import faiss
import numpy as np

class RetrievalAugmentedICL:
    """
    ICL с ретривингом примеров из базы знаний.
    
    Вместо случайных примеров — выбираем наиболее
    релевантные текущему запросу.
    """
    
    def __init__(self, example_db):
        self.encoder = SentenceTransformer('all-MiniLM-L6-v2')
        self.examples = example_db
        self.index = self._build_index()
    
    def _build_index(self):
        """Строим FAISS индекс для эмбеддингов примеров."""
        embeddings = self.encoder.encode(
            [ex['input'] for ex in self.examples]
        )
        dimension = embeddings.shape[1]
        
        index = faiss.IndexFlatL2(dimension)
        index.add(embeddings.astype('float32'))
        return index
    
    def retrieve(self, query, k=4):
        """Извлекаем k наиболее релевантных примеров."""
        query_embedding = self.encoder.encode([query])
        distances, indices = self.index.search(
            query_embedding.astype('float32'), k
        )
        
        selected_examples = [
            self.examples[idx] for idx in indices[0]
        ]
        return selected_examples
    
    def build_prompt(self, query, examples):
        """Строим промпт с отретривленными примерами."""
        prompt = ""
        for ex in examples:
            prompt += f"Input: {ex['input']}\nOutput: {ex['output']}\n\n"
        prompt += f"Input: {query}\nOutput:"
        return prompt

# Использование
icl = RetrievalAugmentedICL(example_db=training_data)
examples = icl.retrieve("Какая погода в Москве?")
prompt = icl.build_prompt("Какая погода в Москве?", examples)
response = model.generate(prompt)

Adaptive ICL

class AdaptiveICL:
    """
    ICL с адаптивным выбором количества примеров.
    
    Для простых запросов — меньше примеров (дешевле).
    Для сложных — больше примеров (точнее).
    """
    
    def __init__(self, model, example_db):
        self.model = model
        self.icl = RetrievalAugmentedICL(example_db)
        self.simple_threshold = 0.7
    
    def estimate_complexity(self, query):
        """Оцениваем сложность запроса."""
        # Простой эвристический подход
        words = query.split()
        has_question = '?' in query
        length_penalty = min(len(words) / 20, 1.0)
        
        complexity = 1.0 - (0.3 if has_question else 0.0) + length_penalty
        return min(complexity, 1.0)
    
    def select_num_examples(self, query):
        """Выбираем количество примеров на основе сложности."""
        complexity = self.estimate_complexity(query)
        
        if complexity < self.simple_threshold:
            return 1  # Zero/one-shot для простых
        elif complexity < 0.8:
            return 3  # Few-shot для средних
        else:
            return 5  # More examples для сложных
    
    def generate(self, query):
        """Генерируем ответ с адаптивным ICL."""
        num_examples = self.select_num_examples(query)
        examples = self.icl.retrieve(query, k=num_examples)
        prompt = self.icl.build_prompt(query, examples)
        return self.model.generate(prompt)

ICL для кодирования

Генерация кода

Напиши функцию на Python:

Пример 1:
Задача: найти максимум в списке
Код: def find_max(lst):
        return max(lst)

Пример 2:
Задача: проверить палиндром
Код: def is_palindrome(s):
        return s == s[::-1]

Пример 3:
Задача: посчитать частоту символов
Код:

Code Completion с ICL

# Паттерн проекта: используем dependency injection

class UserService:
    def __init__(self, db: Database):
        self.db = db
    
    def get_user(self, user_id: int) -> User:
        return self.db.query(User).filter_by(id=user_id).first()

class AuthService:
    def __init__(self, db: Database, user_service: UserService):
        self.db = db
        self.user_service = user_service
    
    def authenticate(self, username: str, password: str) -> bool:
        user = self.user_service.get_user(username)
        return user and user.check_password(password)

# Паттерн проекта: используем event sourcing

Code Review с ICL

Код:
def calculate_total(items):
    total = 0
    for item in items:
        total = total + item['price'] * item['quantity']
    return total

Ревью:
- ✅ Логика правильная
- ⚠️ Можно оптимизировать через sum() + generator
- 💡 Добавить типизацию аргументов
- 💡 Обработать пустой список

Улучшенная версия:
def calculate_total(items: list[dict]) -> float:
    return sum(
        item['price'] * item['quantity']
        for item in items
    )

Ограничения ICL

Когда ICL НЕ работает

1. Слишком много примеров (>10-15)
   → Модель "теряет" первые примеры
   
2. Противоречивые примеры
   → Модель путается в паттернах
   
3. Очень специфичные домены
   → Модель не знает контекста (медицина, право)
   
4. Требуются точные числа/факты
   → Модель может "галлюцинировать"
   
5. Очень длинные зависимости
   → Attention не справляется с дальними связями

Эффект длины контекста

Исследование показывает что качество ICL зависит от
позиции примеров в контексте:

Позиция примера    | Качество ICL
-------------------|---------------
1-2 (начало)       | 90-95%  ⭐
3-5 (середина)     | 80-90%   ★
6-8 (конец)        | 75-85%   ★
9+ (очень далеко)  | 50-70%   ⚠️

Вывод: размещайте самые важные примеры в начале.

Галлюцинации в ICL

Проблема: модель может "додумать" пример, которого нет.

Пример:
  Примеры:
    "Apple → компания"
    "Google → компания"
    "Amazon → компания"
  
  Запрос: "Москва → ?"
  Ответ: "компания" ❌ (галлюцинация!)
  
  Правильный ответ: "город"
  
  Модель "увидела" паттерн где его нет.

Практические рекомендации

Для начинающих

1. Начните с zero-shot
   → Быстро, дёшево, просто
   
2. Если результат плохой — добавьте 3-5 примеров
   → Few-shot обычно решает проблему
   
3. Примеры должны быть:
   → Из того же домена
   → В правильном формате
   → Без ошибок
   
4. Экспериментируйте с порядком примеров
   → Попробуйте разные варианты
   → Выберите лучший

Для продвинутых

1. Используйте retrieval-augmented ICL
   → Автоматический выбор лучших примеров
   
2. Адаптируйте количество примеров
   → Простые задачи — 1-2 примера
   → Сложные — 5-8 примеров
   
3. Комбинируйте с chain-of-thought
   → Показывайте рассуждения в примерах
   
4. Используйте structured output
   → JSON, YAML, другие форматы
   
5. Анализируйте ошибки
   → Какие примеры приводят к ошибкам?
   → Удалите или замените их

Шаблон для production ICL

class ProductionICL:
    """
    Production-ready ICL система.
    
    Включает:
    - Кэширование примеров
    - A/B тестирование форматов
    - Мониторинг качества
    - Автоматическое обновление примеров
    """
    
    def __init__(self):
        self.icl = RetrievalAugmentedICL(self.example_db)
        self.cache = {}
        self.metrics = QualityTracker()
    
    def generate_with_quality_check(self, query, max_retries=3):
        """Генерируем ответ с проверкой качества."""
        for attempt in range(max_retries):
            examples = self.icl.retrieve(query)
            prompt = self.icl.build_prompt(query, examples)
            response = self.model.generate(prompt)
            
            # Проверяем качество
            quality_score = self.metrics.evaluate(response)
            
            if quality_score > self.quality_threshold:
                self.metrics.record(query, response, quality_score)
                return response
            
            # Если качество низкое — пробуем с другими примерами
            examples = self.icl.retrieve(query, k=5)
        
        return self.fallback_response(query)
    
    def update_examples(self, new_examples, validation_score):
        """Обновляем базу примеров на основе качества."""
        if validation_score > self.best_score:
            self.example_db.add(new_examples)
            self.best_score = validation_score
            self._rebuild_index()

ICL vs Fine-Tuning vs Pre-training

                    | ICL           | Fine-Tuning   | Pre-training
  ------------------|---------------|---------------|---------------
  Стоимость         | Очень низкая  | Средняя       | Очень высокая
  Время             | Мгновенно     | Часы/дни      | Недели/месяцы
  Нужны данные      | Нет           | Да (100-10K)  | Да (миллиарды)
  Гибкость          | Высокая       | Средняя       | Низкая
  Знания            | Ограничены    | Умеренные     | Полные
  Обновляемость     | Легко         | Сложно        | Невозможно
  Лучшее для        | Быстрые задачи | Специфичные   | Базовые знания

Когда что использовать

ICL:
  ✅ Быстрый прототип
  ✅ Задачи с несколькими примерами
  ✅ Частые изменения формата
  ✅ Нет данных для обучения

Fine-Tuning:
  ✅ Специфичный домен (медицина, право)
  ✅ Много данных для обучения
  ✅ Нужна стабильность поведения
  ✅ Частое использование

Pre-training:
  ✅ Новый язык или домен
  ✅ Огромные ресурсы
  ✅ Создание новой модели с нуля

Заключение

In-Context Learning — это мощная способность LLM, которая позволяет решать задачи без обновления весов. Ключевые выводы:

  1. ICL работает благодаря pretraining — модель учится "учиться на примерах"
  2. Формат промпта критически важен — правильные примеры = правильный ответ
  3. Few-shot (3-5 примеров) обычно оптимален — больше не всегда лучше
  4. Порядок примеров влияет на результат — экспериментируйте
  5. Retrieval-augmented ICL — автоматический выбор лучших примеров для production

ICL — это ваш первый инструмент при работе с LLM. Пробуйте zero-shot, затем few-shot, и только если не помогает — рассмотрите fine-tuning.