Почему LLM врут, забывают и не знают вчерашних новостей


Блок 1: Как я потерял неделю из-за галлюцинаций

Запускали AI-ассистента для работы с внутренней документацией. Техническое задание простое: сотрудник задаёт вопрос — модель отвечает на основе регламентов компании.

Демо прошло отлично. Руководство в восторге. Запускаем пилот на 50 человек.

Через три дня звонок от заказчика: «Ваш бот насоветовал сотруднику подать заявление по форме Т-8, которой у нас не существует. Человек потратил полдня на поиски.»

Проверяю логи. Модель уверенно сослалась на «Приложение 3 к Регламенту документооборота от 15.03.2022». Открываю регламент — там четыре приложения, но никакого Приложения 3 с формой Т-8 нет. Модель его выдумала. Причём выдумала убедительно: с датой, номером, правильным названием документа.

Дальше — хуже. Начали копать глубже и нашли ещё случаи:

  • Ссылки на несуществующие пункты договоров
  • Выдуманные телефоны внутренних служб
  • «Цитаты» из документов, которых не было в контексте

Неделя ушла на то, чтобы разобраться в природе проблемы и переделать архитектуру.

Эта статья — разбор трёх главных ограничений LLM, которые должен понимать каждый AI-архитектор. Не теоретически, а с точки зрения «как это сломает ваш проект и что с этим делать».


Блок 2: Почему это критично для enterprise

Где вы столкнётесь с этими ограничениями

Галлюцинации — в любом проекте, где LLM генерирует информацию для принятия решений:

  • Чат-боты поддержки (неправильные инструкции клиентам)
  • Анализ документов (выдуманные пункты договоров)
  • Генерация отчётов (несуществующие данные)
  • Ассистенты для сотрудников (ложные регламенты)

Context window — везде, где тексты длиннее пары страниц:

  • Работа с договорами (50-100 страниц)
  • Анализ технической документации
  • Суммаризация длинных переписок
  • Чат-боты с историей диалога

Knowledge cutoff — когда нужна актуальная информация:

  • Вопросы о текущих курсах, ценах, наличии
  • Ссылки на действующее законодательство
  • Информация о недавних событиях
  • Актуальные версии продуктов и API

Последствия, если не учитывать

Ограничение Что случится Реальный ущерб
Галлюцинации Пользователи получат ложную информацию Репутационные потери, юридические риски, откат проекта
Context window Модель «забудет» важные части документа Неполные ответы, пропущенные условия договоров
Knowledge cutoff Устаревшие данные выдаются как актуальные Неправильные решения на основе старой информации

Блок 3: Разбор ограничений


Часть 1: Галлюцинации — почему LLM врут с уверенным фейсом

Что такое галлюцинация

Галлюцинация — это когда модель генерирует информацию, которая выглядит правдоподобно, но не соответствует действительности.

Ключевое слово — «правдоподобно». Модель не выдаёт случайный мусор. Она генерирует текст, который статистически похож на правильный ответ, но фактически неверен.

Вопрос: "Какой телефон горячей линии Роспотребнадзора?"

Галлюцинация: "8-800-555-49-48"
(выглядит как настоящий номер 8-800, но выдуман)

Реальный номер: 8-800-555-49-43 может случайно совпасть,
а может быть номером совершенно другой организации

Почему это происходит: техническая причина

LLM — это не база знаний. Это модель, которая предсказывает следующий токен на основе предыдущих.

Внутренняя логика модели:

Контекст: "Телефон горячей линии Роспотребнадзора:"

Модель думает (упрощённо):
- После "телефон горячей линии" часто идут цифры
- Формат 8-800-XXX-XX-XX встречался много раз
- Роспотребнадзор — государственная организация
- У госорганов часто номера 8-800

Генерирует: "8-800-" + [статистически вероятные цифры]

Модель не «знает» номер. Она генерирует последовательность, которая статистически вероятна в данном контексте.

Аналогия

Представьте человека, который прочитал миллион документов, но не может их перечитать. Он помнит паттерны, стиль, типичные формулировки. Но конкретные факты — размыты.

Когда его спрашивают о деталях, он не говорит «не помню». Он реконструирует ответ из паттернов. Иногда попадает. Иногда — нет.

Типы галлюцинаций

Тип 1: Фактические галлюцинации

Выдуманные факты, даты, числа, имена.

Вопрос: "Когда был принят закон о персональных данных в России?"

Галлюцинация: "Федеральный закон №152-ФЗ был принят 27 июля 2007 года"
Реальность: Закон принят 27 июля 2006 года

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

Тип 2: Галлюцинации-ссылки

Ссылки на несуществующие источники.

Вопрос: "Дай ссылку на исследование об эффективности RAG"

Галлюцинация: "Согласно исследованию Smith et al. (2023) 
'Retrieval-Augmented Generation for Enterprise Applications', 
опубликованному в Journal of AI Research..."

Реальность: Такой статьи не существует. Имена авторов, 
название журнала, год — всё выглядит правдоподобно, но выдумано.

Тип 3: Галлюцинации в контексте

Модель «додумывает» информацию, которой нет в предоставленном контексте.

Контекст: "Договор №123 заключён между ООО Альфа и ООО Бета. 
Срок действия: 12 месяцев. Сумма: 1 000 000 рублей."

Вопрос: "Какие штрафные санкции предусмотрены договором?"

Галлюцинация: "Согласно пункту 5.2 договора, за просрочку 
платежа предусмотрена пеня в размере 0.1% за каждый день просрочки"

Реальность: В предоставленном контексте нет информации о штрафах.
Модель сгенерировала "типичный" пункт договора.

Тип 4: Логические галлюцинации

Неверные выводы из правильных данных.

Контекст: "Выручка Q1: 100 млн. Выручка Q2: 120 млн. Выручка Q3: 90 млн."

Вопрос: "Какой тренд выручки?"

Галлюцинация: "Наблюдается устойчивый рост выручки на протяжении года"

Реальность: Q3 показывает падение. Тренд неоднозначный.

Измерение галлюцинаций

Метрики

Faithfulness (верность контексту) — насколько ответ соответствует предоставленному контексту.

# Пример оценки с помощью RAGAS
from ragas.metrics import faithfulness

# Контекст, вопрос, ответ модели
result = faithfulness.score(
    question="Какая сумма договора?",
    answer="Сумма договора составляет 1 500 000 рублей",
    contexts=["Договор на сумму 1 000 000 рублей"]
)
# Низкий score = галлюцинация

Factual accuracy — проверка фактов через внешние источники.

Self-consistency — даёт ли модель одинаковые ответы при перефразировании вопроса.

# Проверка консистентности
questions = [
    "Какой срок действия договора?",
    "На какой период заключён договор?",
    "Сколько длится договор?"
]

answers = [model.generate(q) for q in questions]

# Если ответы разные — возможна галлюцинация

Стратегии борьбы с галлюцинациями

Стратегия 1: RAG (Retrieval-Augmented Generation)

Не полагаемся на «память» модели. Даём ей явный контекст.

┌─────────────────────────────────────────────────────────────┐
│                         RAG Pipeline                        │
│                                                             │
│  Вопрос ──▶ [Поиск] ──▶ Релевантные ──▶ [LLM] ──▶ Ответ     │
│     │      по базе     документы         │                  │
│     │                      │             │                  │
│     │                      ▼             │                  │
│     │              "Отвечай ТОЛЬКО       │                  │
│     └──────────────  на основе этих  ────┘                  │
│                      документов"                            │
└─────────────────────────────────────────────────────────────┘
# Промпт для RAG с защитой от галлюцинаций
RAG_PROMPT = """
Контекст:
{context}

Вопрос: {question}

Инструкции:
1. Отвечай ТОЛЬКО на основе предоставленного контекста
2. Если информации нет в контексте — скажи "В предоставленных документах нет информации по этому вопросу"
3. Цитируй конкретные места из контекста
4. Не додумывай и не дополняй информацию

Ответ:
"""

Стратегия 2: Цитирование источников

Требуем от модели указывать, откуда взята информация.

CITATION_PROMPT = """
Отвечая на вопрос, ОБЯЗАТЕЛЬНО:
1. Указывай номер документа в квадратных скобках [Doc1], [Doc2]
2. Каждое утверждение должно иметь ссылку
3. Если утверждение не подкреплено документом — не включай его

Пример:
"Срок договора составляет 12 месяцев [Doc1]. 
Оплата производится ежемесячно [Doc2]."
"""

Стратегия 3: Верификация через повторный запрос

def verify_answer(question, answer, context):
    """
    Проверяем ответ вторым запросом к модели
    """
    verification_prompt = f"""
    Контекст: {context}

    Утверждение: {answer}

    Проверь, подтверждается ли это утверждение контекстом.
    Ответь в формате:
    - ПОДТВЕРЖДЕНО: [цитата из контекста]
    - НЕ ПОДТВЕРЖДЕНО: [объяснение]
    - ЧАСТИЧНО: [что подтверждено, что нет]
    """

    verification = model.generate(verification_prompt)
    return verification

Стратегия 4: Температура и параметры генерации

# Для фактических ответов — низкая температура
response = client.chat.completions.create(
    model="gpt-4",
    messages=[...],
    temperature=0.1,  # Минимум креативности
    top_p=0.9,
)

# Для творческих задач — можно выше
creative_response = client.chat.completions.create(
    model="gpt-4",
    messages=[...],
    temperature=0.7,
)

Стратегия 5: Structured Output

Ограничиваем формат ответа, чтобы уменьшить пространство для галлюцинаций.

from pydantic import BaseModel
from typing import Optional, List

class ContractInfo(BaseModel):
    contract_number: Optional[str]
    parties: List[str]
    amount: Optional[float]
    currency: str = "RUB"
    start_date: Optional[str]
    end_date: Optional[str]
    confidence: float  # Уверенность модели
    source_quotes: List[str]  # Цитаты из документа

# Модель вынуждена заполнять структуру
# None явно показывает отсутствие информации
# source_quotes требует обоснования

Часть 2: Context Window — почему модель «забывает»

Что такое context window

Context window (контекстное окно) — максимальное количество токенов, которое модель может обработать за один запрос. Включает и входной текст, и генерируемый ответ.

┌────────────────────────────────────────────────────────────┐
│                    Context Window (8K токенов)             │
│                                                            │
│  [Системный промпт] [История диалога] [Документы] [Ответ]  │
│        500              2000            4500        1000   │
│                                                            │
│  ◄──────────────── INPUT ─────────────────► ◄── OUTPUT ──► │
└────────────────────────────────────────────────────────────┘

Размеры контекста популярных моделей

Модель Context Window Примечания
GPT-3.5-turbo 16K Стандартная версия
GPT-4 8K / 128K Зависит от версии
GPT-4-turbo 128K Но качество падает на длинных контекстах
Claude 3 Opus 200K Лучшее качество на длинных текстах
Claude 3.5 Sonnet 200K Оптимальный баланс цена/качество
LLaMA 3 8K Базовая версия
Mistral 32K С sliding window attention

Проблема: токены ≠ символы

import tiktoken

encoder = tiktoken.encoding_for_model("gpt-4")

# Английский текст
english = "The contract was signed on Monday"
english_tokens = len(encoder.encode(english))
print(f"English: {len(english)} chars, {english_tokens} tokens")
# English: 35 chars, 7 tokens (5 chars/token)

# Русский текст
russian = "Договор был подписан в понедельник"
russian_tokens = len(encoder.encode(russian))
print(f"Russian: {len(russian)} chars, {russian_tokens} tokens")
# Russian: 34 chars, 12 tokens (2.8 chars/token)

Практический вывод: Русский текст занимает в 1.5-2 раза больше токенов, чем английский той же длины.

Расчёт: поместится ли документ

def calculate_token_budget(
    model_context: int,
    system_prompt: str,
    chat_history: list,
    max_response: int = 1000
) -> int:
    """
    Считаем, сколько токенов осталось на документы
    """
    encoder = tiktoken.encoding_for_model("gpt-4")

    system_tokens = len(encoder.encode(system_prompt))
    history_tokens = sum(
        len(encoder.encode(msg["content"])) 
        for msg in chat_history
    )

    available = model_context - system_tokens - history_tokens - max_response

    print(f"Контекст модели: {model_context}")
    print(f"Системный промпт: {system_tokens}")
    print(f"История диалога: {history_tokens}")
    print(f"Резерв на ответ: {max_response}")
    print(f"Доступно для документов: {available}")

    return available

# Пример
budget = calculate_token_budget(
    model_context=8192,
    system_prompt="Ты помощник для анализа договоров...",  # ~50 токенов
    chat_history=[
        {"role": "user", "content": "Проанализируй договор"},
        {"role": "assistant", "content": "Хорошо, загрузите документ"},
    ],  # ~30 токенов
    max_response=1000
)
# Доступно для документов: ~7100 токенов
# Это примерно 15-20 страниц русского текста

Что происходит при превышении контекста

Вариант 1: Обрезка (truncation)

Модель или API обрезает текст, чтобы влезть в контекст.

# Типичное поведение API
try:
    response = client.chat.completions.create(
        model="gpt-4",
        messages=very_long_messages
    )
except openai.BadRequestError as e:
    # "This model's maximum context length is 8192 tokens"
    pass

# Некоторые библиотеки обрезают молча
from langchain.text_splitter import TokenTextSplitter

splitter = TokenTextSplitter(chunk_size=4000, chunk_overlap=200)
chunks = splitter.split_text(long_document)
# Документ разбит, но связи между частями потеряны

Вариант 2: Lost in the Middle

Даже если текст влезает, модель хуже «видит» середину контекста.

Качество внимания модели:

Начало документа:  ████████████  (отлично)
Середина:          ████░░░░░░░░  (плохо)
Конец:             ████████████  (отлично)

Исследование "Lost in the Middle" (Liu et al., 2023):
При поиске факта в длинном контексте точность падает с 90%+ 
до 50% если факт находится в середине.
# Эксперимент: где разместить важную информацию
def test_position_sensitivity(model, context_parts, question):
    """
    Проверяем, как позиция информации влияет на ответ
    """
    results = {}

    important_info = "Ключевой факт: сумма договора 5 000 000 рублей"

    # Тест 1: важная информация в начале
    context_start = important_info + "n" + "n".join(context_parts)
    results["start"] = model.generate(context_start, question)

    # Тест 2: важная информация в середине
    mid = len(context_parts) // 2
    context_middle = (
        "n".join(context_parts[:mid]) + 
        "n" + important_info + "n" + 
        "n".join(context_parts[mid:])
    )
    results["middle"] = model.generate(context_middle, question)

    # Тест 3: важная информация в конце
    context_end = "n".join(context_parts) + "n" + important_info
    results["end"] = model.generate(context_end, question)

    return results

Стратегии работы с длинными документами

Стратегия 1: Chunking + Retrieval (RAG)

┌─────────────────────────────────────────────────────────────┐
│                    Chunking Strategy                        │
│                                                             │
│  Документ 100 стр ──▶ [Разбивка] ──▶ 50 чанков по 2 стр     │
│                           │                                 │
│                           ▼                                 │
│                    [Векторизация]                           │
│                           │                                 │
│                           ▼                                 │
│                    Vector Database                          │
│                           │                                 │
│  Вопрос ──▶ [Поиск top-5] ──▶ 5 релевантных чанков ──▶ LLM  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Умная разбивка с учётом структуры текста
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["nn", "n", ". ", " ", ""],  # Приоритет разделителей
    length_function=len,
)

# Для юридических документов — разбивка по разделам
legal_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1500,
    chunk_overlap=300,
    separators=[
        "nСтатья ",
        "nРаздел ",
        "nГлава ",
        "nn",
        "n",
        ". ",
    ],
)

Стратегия 2: Map-Reduce для суммаризации

┌─────────────────────────────────────────────────────────────┐
│                    Map-Reduce Pipeline                      │
│                                                             │
│  Документ ──▶ [Chunk 1] ──▶ Summary 1 ─┐                    │
│          ──▶ [Chunk 2] ──▶ Summary 2 ──┼──▶ [Combine] ──▶   │
│          ──▶ [Chunk 3] ──▶ Summary 3 ──┤    Final Summary   │
│          ──▶ [Chunk N] ──▶ Summary N ─┘                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
def map_reduce_summarize(document: str, chunk_size: int = 3000):
    """
    Суммаризация длинного документа через map-reduce
    """
    # Map: суммаризируем каждый чанк
    chunks = split_into_chunks(document, chunk_size)

    chunk_summaries = []
    for i, chunk in enumerate(chunks):
        summary = llm.generate(f"""
        Это часть {i+1} из {len(chunks)} документа.

        Текст части:
        {chunk}

        Сделай краткое резюме этой части (3-5 предложений).
        Сохрани ключевые факты, даты, суммы, имена.
        """)
        chunk_summaries.append(summary)

    # Reduce: объединяем резюме
    combined = "nn".join(chunk_summaries)

    final_summary = llm.generate(f"""
    Ниже представлены резюме частей документа:

    {combined}

    Создай единое связное резюме всего документа.
    Убери дублирование. Сохрани все ключевые факты.
    """)

    return final_summary

Стратегия 3: Иерархическая индексация

# Для очень больших документов: многоуровневый индекс

class HierarchicalIndex:
    """
    Уровень 1: Резюме всего документа
    Уровень 2: Резюме разделов
    Уровень 3: Полные тексты параграфов
    """

    def __init__(self, document):
        self.paragraphs = self.split_paragraphs(document)
        self.sections = self.group_into_sections(self.paragraphs)

        # Создаём резюме снизу вверх
        self.section_summaries = {
            section_id: self.summarize(text)
            for section_id, text in self.sections.items()
        }

        self.document_summary = self.summarize(
            "n".join(self.section_summaries.values())
        )

    def query(self, question: str):
        """
        1. Сначала проверяем резюме документа
        2. Определяем релевантные разделы
        3. Ищем в конкретных параграфах
        """
        # Шаг 1: Какие разделы релевантны?
        relevant_sections = self.find_relevant_sections(
            question, 
            self.section_summaries
        )

        # Шаг 2: Ищем в параграфах этих разделов
        relevant_paragraphs = []
        for section_id in relevant_sections:
            paragraphs = self.search_paragraphs(
                question,
                self.sections[section_id]
            )
            relevant_paragraphs.extend(paragraphs)

        return relevant_paragraphs

Стратегия 4: Sliding Window для диалогов

class ConversationManager:
    """
    Управление историей диалога с ограниченным контекстом
    """

    def __init__(self, max_tokens: int = 4000):
        self.max_tokens = max_tokens
        self.messages = []
        self.summary = ""  # Резюме старых сообщений

    def add_message(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})
        self._manage_context()

    def _manage_context(self):
        """
        Если контекст превышен — суммаризируем старые сообщения
        """
        total_tokens = self._count_tokens()

        if total_tokens > self.max_tokens:
            # Берём первую половину сообщений
            old_messages = self.messages[:len(self.messages)//2]

            # Суммаризируем
            old_summary = self._summarize_messages(old_messages)

            # Обновляем summary
            self.summary = self._merge_summaries(self.summary, old_summary)

            # Оставляем только новые сообщения
            self.messages = self.messages[len(self.messages)//2:]

    def get_context(self):
        """
        Возвращает контекст для отправки в модель
        """
        context = []

        if self.summary:
            context.append({
                "role": "system",
                "content": f"Краткое содержание предыдущего разговора:n{self.summary}"
            })

        context.extend(self.messages)

        return context

Часть 3: Knowledge Cutoff — модель застряла в прошлом

Что такое knowledge cutoff

Knowledge cutoff — дата, после которой модель не имеет информации о событиях в мире. Всё, что произошло после этой даты, модели неизвестно.

GPT-4 (базовая версия): обучена на данных до сентября 2021
GPT-4 Turbo: данные до апреля 2024
Claude 3: данные до начала 2024

Вопрос (в декабре 2024): "Кто президент Аргентины?"

GPT-4 (старая): "Альберто Фернандес" ← устарело
GPT-4 Turbo: "Хавьер Милей" ← актуально

Как проверить cutoff модели

def check_knowledge_cutoff(model):
    """
    Набор вопросов для определения cutoff даты
    """
    test_questions = [
        {
            "question": "Когда ChatGPT стал публично доступен?",
            "answer_if_knows": "30 ноября 2022",
            "event_date": "2022-11-30"
        },
        {
            "question": "Кто выиграл Чемпионат мира по футболу 2022?",
            "answer_if_knows": "Аргентина",
            "event_date": "2022-12-18"
        },
        {
            "question": "Когда был выпущен GPT-4?",
            "answer_if_knows": "14 марта 2023",
            "event_date": "2023-03-14"
        },
        # Добавляйте более свежие события
    ]

    for test in test_questions:
        response = model.generate(test["question"])
        print(f"Вопрос: {test['question']}")
        print(f"Ответ модели: {response}")
        print(f"Событие произошло: {test['event_date']}")
        print("---")

Проблемы, которые создаёт cutoff

Проблема 1: Устаревшее законодательство

Вопрос: "Какой размер МРОТ в России?"

Модель с cutoff 2021: "12 792 рубля"
Реальность 2024: "19 242 рубля"

Опасность: Расчёты зарплат, пособий, штрафов будут неверными

Проблема 2: Устаревшие API и библиотеки

# Модель может предложить устаревший код

# Ответ модели с cutoff 2021:
from langchain.llms import OpenAI
llm = OpenAI(model_name="text-davinci-003")

# Актуальный код 2024:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4")

Проблема 3: Несуществующие продукты и компании

Вопрос: "Расскажи о возможностях Bard от Google"

Модель с cutoff до 2023: может не знать о Bard
Модель с cutoff 2023: расскажет о Bard
Реальность 2024: Bard переименован в Gemini

Модель может рассказывать о продукте, который уже не существует

Стратегии работы с cutoff

Стратегия 1: Явное указание даты в промпте

from datetime import datetime

def create_prompt_with_date(user_question: str) -> str:
    today = datetime.now().strftime("%d %B %Y")

    return f"""
    Сегодняшняя дата: {today}

    ВАЖНО: Твои знания могут быть устаревшими. 
    Если вопрос касается текущих событий, цен, курсов, 
    законодательства — предупреди пользователя, что информация 
    может быть неактуальной и требует проверки.

    Вопрос пользователя: {user_question}
    """

Стратегия 2: RAG с актуальными данными

┌─────────────────────────────────────────────────────────────┐
│              RAG для актуальной информации                  │
│                                                             │
│  Вопрос ──▶ [Классификатор] ──▶ Нужны актуальные данные?    │
│                   │                                         │
│                   ├── Да ──▶ [Поиск в актуальной БД] ──┐    │
│                   │                                    │    │
│                   └── Нет ──▶ [Прямой ответ LLM] ──────┼──▶ │
│                                                        │    │
│                   ┌────────────────────────────────────┘    │
│                   ▼                                         │
│              [LLM + контекст] ──▶ Ответ                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
class ActualDataRAG:
    """
    RAG с автоматическим определением необходимости актуальных данных
    """

    NEEDS_ACTUAL_DATA_KEYWORDS = [
        "сейчас", "текущий", "сегодня", "актуальный",
        "курс", "цена", "стоимость", "МРОТ", "ставка",
        "последний", "новый закон", "изменения"
    ]

    def __init__(self, llm, actual_data_sources):
        self.llm = llm
        self.sources = actual_data_sources

    def needs_actual_data(self, question: str) -> bool:
        """Определяем, нужны ли актуальные данные"""
        question_lower = question.lower()
        return any(
            keyword in question_lower 
            for keyword in self.NEEDS_ACTUAL_DATA_KEYWORDS
        )

    def query(self, question: str) -> str:
        if self.needs_actual_data(question):
            # Получаем актуальные данные
            context = self.fetch_actual_data(question)

            prompt = f"""
            Актуальные данные (получены {datetime.now()}):
            {context}

            Вопрос: {question}

            Ответь на основе предоставленных актуальных данных.
            """
        else:
            prompt = question

        return self.llm.generate(prompt)

    def fetch_actual_data(self, question: str) -> str:
        """Получаем данные из актуальных источников"""
        results = []
        for source in self.sources:
            data = source.search(question)
            if data:
                results.append(f"[{source.name}]: {data}")
        return "n".join(results)

Стратегия 3: Function Calling для реального времени

# Определяем функции для получения актуальных данных
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_exchange_rate",
            "description": "Получить текущий курс валюты ЦБ РФ",
            "parameters": {
                "type": "object",
                "properties": {
                    "currency": {
                        "type": "string",
                        "description": "Код валюты (USD, EUR, CNY)"
                    }
                },
                "required": ["currency"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_mrot",
            "description": "Получить текущий размер МРОТ в России",
            "parameters": {
                "type": "object",
                "properties": {}
            }
        }
    }
]

# Модель сама решит, когда вызвать функцию
response = client.chat.completions.create(
    model="gpt-4",
    messages=[
        {"role": "user", "content": "Какой сейчас курс доллара?"}
    ],
    tools=tools,
    tool_choice="auto"
)

# Если модель вызвала функцию — выполняем и возвращаем результат

Стратегия 4: Гибридный подход с дисклеймерами

def generate_with_disclaimer(question: str, response: str) -> str:
    """
    Добавляем дисклеймер к ответам, которые могут устареть
    """

    time_sensitive_topics = [
        "закон", "законодательство", "правила",
        "курс", "цена", "стоимость", "тариф",
        "версия", "релиз", "обновление"
    ]

    needs_disclaimer = any(
        topic in question.lower() or topic in response.lower()
        for topic in time_sensitive_topics
    )

    if needs_disclaimer:
        disclaimer = """

---
⚠️ **Обратите внимание:** Информация может быть устаревшей. 
Для принятия важных решений рекомендуется проверить актуальность 
данных в официальных источниках.
"""
        return response + disclaimer

    return response

Блок 4: Типовые проблемы и решения

Проблема 1: Галлюцинации в ссылках на документы

Ситуация: Модель уверенно ссылается на пункты договора, которых не существует.

Пользователь: "Какие штрафы предусмотрены договором?"

Модель: "Согласно пункту 7.3 договора, за просрочку платежа 
предусмотрен штраф в размере 0.5% от суммы задолженности 
за каждый день просрочки, но не более 10% от общей суммы договора."

Реальность: В договоре нет пункта 7.3, и условия штрафов другие.

Причина: Модель обучена на тысячах договоров и генерирует «типичный» ответ вместо анализа конкретного документа.

Решения:

  1. Строгий промпт с запретом домысливания:
    
    prompt = """
    СТРОГИЕ ПРАВИЛА:
  2. Отвечай ТОЛЬКО на основе текста договора ниже
  3. Если информации нет — отвечай «В договоре не указано»
  4. ОБЯЗАТЕЛЬНО цитируй конкретный пункт: «Пункт X.X: [цитата]»
  5. Если не уверен — скажи об этом явно

Текст договора:
{contract_text}

Вопрос: {question}
«»»


2. **Валидация ссылок:**
```python
def validate_references(answer: str, document: str) -> dict:
    """
    Проверяем, существуют ли упомянутые пункты
    """
    # Ищем ссылки вида "пункт X.X" или "п. X.X"
    import re
    references = re.findall(r'[Пп]ункт[а]?s+(d+.d+)', answer)
    references += re.findall(r'п.s*(d+.d+)', answer)

    validation = {}
    for ref in references:
        # Ищем этот пункт в документе
        pattern = rf'{ref}[.s]'
        found = re.search(pattern, document)
        validation[ref] = {
            "exists": found is not None,
            "context": document[found.start():found.start()+200] if found else None
        }

    return validation
  1. Двухэтапная генерация:
    
    # Этап 1: Извлечение релевантных пунктов
    extraction_prompt = f"""
    Найди в договоре ВСЕ пункты, связанные с вопросом: {question}

Для каждого пункта выведи:

  • Номер пункта
  • Точную цитату

Если релевантных пунктов нет — напиши «Не найдено»

Договор:
{contract_text}
«»»

extracted = llm.generate(extraction_prompt)

Этап 2: Ответ на основе извлечённых пунктов

answer_prompt = f»»»
На основе ТОЛЬКО этих пунктов договора ответь на вопрос.

Извлечённые пункты:
{extracted}

Вопрос: {question}

Если пунктов недостаточно для ответа — скажи об этом.
«»»


---

### Проблема 2: Потеря информации в длинных диалогах

**Ситуация:** После 10-15 сообщений бот "забывает" начало разговора.

Сообщение 1: «Меня зовут Алексей, номер договора 12345»

Сообщение 15: «Так какой номер моего договора?»
Бот: «Извините, вы не сообщали номер договора»


**Причина:** История диалога превысила context window, старые сообщения обрезались.

**Решения:**

1. **Извлечение ключевых сущностей:**
```python
class ConversationMemory:
    def __init__(self):
        self.entities = {}  # Извлечённые сущности
        self.messages = []  # Последние N сообщений
        self.max_messages = 10

    def add_message(self, role: str, content: str):
        # Извлекаем сущности
        new_entities = self.extract_entities(content)
        self.entities.update(new_entities)

        # Добавляем сообщение
        self.messages.append({"role": role, "content": content})

        # Обрезаем старые
        if len(self.messages) > self.max_messages:
            self.messages = self.messages[-self.max_messages:]

    def extract_entities(self, text: str) -> dict:
        """Извлекаем ключевые сущности из текста"""
        entities = {}

        # Имя
        name_match = re.search(r'[Мм]еня зовутs+(w+)', text)
        if name_match:
            entities["user_name"] = name_match.group(1)

        # Номер договора
        contract_match = re.search(r'договор[а]?s*[№#]?s*(d+)', text, re.I)
        if contract_match:
            entities["contract_number"] = contract_match.group(1)

        # Телефон
        phone_match = re.search(r'+?[78][ds-]{10,}', text)
        if phone_match:
            entities["phone"] = phone_match.group(0)

        return entities

    def get_context(self) -> str:
        """Формируем контекст для модели"""
        context_parts = []

        if self.entities:
            context_parts.append("Известная информация о пользователе:")
            for key, value in self.entities.items():
                context_parts.append(f"- {key}: {value}")

        context_parts.append("nПоследние сообщения:")
        for msg in self.messages:
            context_parts.append(f"{msg['role']}: {msg['content']}")

        return "n".join(context_parts)
  1. Периодическая суммаризация:
    def summarize_conversation_chunk(messages: list) -> str:
    """Суммаризируем блок сообщений"""
    conversation_text = "n".join(
        f"{m['role']}: {m['content']}" for m in messages
    )
    
    summary = llm.generate(f"""
    Сделай краткое резюме этого диалога.
    Сохрани: имена, номера, даты, ключевые договорённости, нерешённые вопросы.
    
    Диалог:
    {conversation_text}
    
    Резюме (3-5 предложений):
    """)
    
    return summary

Проблема 3: Модель отвечает на основе устаревших знаний

Ситуация: Вопросы о текущих ценах, курсах, законах.

Пользователь: "Какой сейчас курс доллара?"
Модель: "Курс доллара составляет около 75 рублей"
Реальность: Курс может быть 90+ рублей

Причина: Knowledge cutoff модели.

Решения:

  1. Классификация вопросов:
    class QuestionClassifier:
    NEEDS_REALTIME = [
        "курс", "цена", "стоимость", "сколько стоит",
        "сейчас", "сегодня", "текущий", "актуальный",
        "погода", "новости", "последние"
    ]
    
    NEEDS_RECENT_DATA = [
        "закон", "постановление", "приказ",
        "МРОТ", "ставка", "тариф",
        "версия", "релиз"
    ]
    
    @classmethod
    def classify(cls, question: str) -> str:
        q_lower = question.lower()
    
        if any(kw in q_lower for kw in cls.NEEDS_REALTIME):
            return "realtime"
        elif any(kw in q_lower for kw in cls.NEEDS_RECENT_DATA):
            return "recent"
        else:
            return "static"
  2. Интеграция с внешними API:
    class RealtimeDataProvider:
    def get_exchange_rate(self, currency: str) -> dict:
        """Получаем курс с сайта ЦБ РФ"""
        import requests
        from datetime import datetime
    
        url = "https://www.cbr-xml-daily.ru/daily_json.js"
        response = requests.get(url)
        data = response.json()
    
        if currency.upper() in data["Valute"]:
            rate_info = data["Valute"][currency.upper()]
            return {
                "currency": currency,
                "rate": rate_info["Value"],
                "date": data["Date"],
                "source": "ЦБ РФ"
            }
        return None
    
    def get_mrot(self) -> dict:
        """МРОТ нужно хранить в своей БД и обновлять"""
        # В реальности — запрос к вашей БД актуальных данных
        return {
            "value": 19242,
            "effective_from": "2024-01-01",
            "source": "Федеральный закон от 27.11.2023 № 548-ФЗ"
        }

Проблема 4: Галлюцинации в числах и расчётах

Ситуация: Модель неправильно считает или выдумывает числа.

Контекст: "Выручка Q1: 100 млн, Q2: 150 млн, Q3: 120 млн"
Вопрос: "Какая общая выручка за 3 квартала?"
Модель: "Общая выручка составила 380 млн рублей"
Реальность: 100 + 150 + 120 = 370 млн

Причина: LLM плохо выполняют арифметические операции, особенно с большими числами.

Решения:

  1. Вынос вычислений в код:
    
    import re
    from typing import Optional

def extract_and_calculate(text: str, question: str) -> Optional[str]:
«»»
Извлекаем числа и выполняем вычисления в Python
«»»

Просим модель извлечь числа и операцию

extraction_prompt = f"""
Текст: {text}
Вопрос: {question}

Извлеки числа и определи операцию. Ответь в формате JSON:
{{
    "numbers": [список чисел],
    "operation": "sum" | "average" | "max" | "min" | "difference",
    "unit": "единица измерения"
}}
"""

extraction = llm.generate(extraction_prompt)
data = json.loads(extraction)

# Выполняем вычисление в Python
numbers = data["numbers"]
operation = data["operation"]

if operation == "sum":
    result = sum(numbers)
elif operation == "average":
    result = sum(numbers) / len(numbers)
elif operation == "max":
    result = max(numbers)
elif operation == "min":
    result = min(numbers)
elif operation == "difference":
    result = numbers[0] - numbers[1] if len(numbers) == 2 else None

return f"{result} {data['unit']}"

2. **Code Interpreter / Function Calling:**
```python
tools = [
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "Выполнить математический расчёт",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "Математическое выражение, например '100 + 150 + 120'"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]

def calculate(expression: str) -> float:
    """Безопасное вычисление выражения"""
    # Разрешаем только цифры и базовые операции
    allowed = set('0123456789.+-*/() ')
    if not all(c in allowed for c in expression):
        raise ValueError("Недопустимые символы в выражении")

    return eval(expression)

Проблема 5: Модель не признаёт незнание

Ситуация: Вместо «не знаю» модель выдумывает ответ.

Вопрос: "Какой телефон техподдержки компании X?"
Модель: "Телефон техподдержки: 8-800-XXX-XX-XX"
Реальность: Модель не знает реальный номер, но сгенерировала правдоподобный

Причина: Модели обучены быть helpful и давать ответы. «Не знаю» — редкий паттерн в обучающих данных.

Решения:

  1. Явное разрешение не знать:
    
    prompt = """
    Ты — ассистент компании X.

ВАЖНЫЕ ПРАВИЛА:

  1. Если ты не уверен в ответе — скажи «Я не уверен, рекомендую уточнить у специалиста»
  2. Если информации нет в твоих знаниях — скажи «У меня нет этой информации»
  3. НИКОГДА не выдумывай телефоны, адреса, имена сотрудников
  4. Лучше признать незнание, чем дать неверную информацию

Вопрос пользователя: {question}
«»»


2. **Оценка уверенности:**
```python
def get_answer_with_confidence(question: str) -> dict:
    prompt = f"""
    Вопрос: {question}

    Ответь в формате JSON:
    {{
        "answer": "твой ответ",
        "confidence": "high" | "medium" | "low",
        "reasoning": "почему ты так считаешь",
        "needs_verification": true | false
    }}
    """

    response = llm.generate(prompt)
    result = json.loads(response)

    # Если уверенность низкая — добавляем предупреждение
    if result["confidence"] == "low":
        result["answer"] = (
            f"⚠️ Низкая уверенность в ответе:nn"
            f"{result['answer']}nn"
            f"Рекомендую перепроверить эту информацию."
        )

    return result
  1. Whitelist разрешённой информации:
    class FactDatabase:
    """База проверенных фактов"""
    
    def __init__(self):
        self.facts = {
            "support_phone": "8-800-100-00-00",
            "support_email": "support@company.ru",
            "working_hours": "Пн-Пт 9:00-18:00",
            # ... другие проверенные факты
        }
    
    def get_fact(self, key: str) -> Optional[str]:
        return self.facts.get(key)
    
    def answer_with_facts(self, question: str) -> str:
        """Отвечаем только на основе проверенных фактов"""
    
        # Определяем, какой факт нужен
        fact_mapping = {
            "телефон": "support_phone",
            "почта": "support_email",
            "email": "support_email",
            "время работы": "working_hours",
            "график": "working_hours",
        }
    
        for keyword, fact_key in fact_mapping.items():
            if keyword in question.lower():
                fact = self.get_fact(fact_key)
                if fact:
                    return f"Официальная информация: {fact}"
    
        return "У меня нет проверенной информации по этому вопросу. Обратитесь в поддержку."

Блок 5: Практические задачи

Задача 1: Оценка риска галлюцинаций

Уровень: базовый

Условие:
Вам предлагают три use case для внедрения LLM. Оцените риск галлюцинаций для каждого и предложите меры митигации.

  1. Чат-бот для ответов на вопросы по FAQ (50 типовых вопросов)
  2. Генерация черновиков юридических документов
  3. Суммаризация новостей из RSS-лент

Вопросы:

  1. Ранжируйте use cases по уровню риска (низкий/средний/высокий)
  2. Для каждого предложите архитектурное решение

Решение:

Use Case 1: FAQ-бот
Риск: НИЗКИЙ
Причина: Ограниченный набор вопросов, можно проверить все ответы заранее.

Архитектура:
┌─────────────────────────────────────────────────────────┐
│  Вопрос ──▶ [Semantic Search] ──▶ Найден в FAQ?        │
│                                         │               │
│                    ┌────────────────────┴───────────┐   │
│                    ▼                                ▼   │
│               [Да: вернуть                    [Нет:     │
│                готовый ответ]                 передать  │
│                                               в LLM с   │
│                                               контекстом│
│                                               FAQ]      │
└─────────────────────────────────────────────────────────┘

Меры:
- Проверенная база FAQ
- Fallback на "обратитесь к специалисту"
- Логирование для выявления новых вопросов

Use Case 2: Юридические документы
Риск: ВЫСОКИЙ
Причина: Галлюцинация в договоре = юридические и финансовые риски.

Архитектура:
┌─────────────────────────────────────────────────────────┐
│  Параметры ──▶ [LLM: черновик] ──▶ [Проверка юристом]  │
│      │                                    │             │
│      │         ┌──────────────────────────┘             │
│      │         ▼                                        │
│      │    [Сравнение с шаблоном]                       │
│      │         │                                        │
│      │         ▼                                        │
│      └──▶ [Подсветка отклонений] ──▶ Юрист ──▶ Финал   │
└─────────────────────────────────────────────────────────┘

Меры:
- LLM генерирует ТОЛЬКО черновик
- Обязательная проверка человеком
- Шаблоны с заполняемыми полями вместо свободной генерации
- Версионирование и аудит

Use Case 3: Суммаризация новостей
Риск: СРЕДНИЙ
Причина: Искажение фактов в новостях — репутационный риск.

Архитектура:
┌─────────────────────────────────────────────────────────┐
│  RSS ──▶ [Полный текст] ──▶ [LLM: суммаризация]        │
│                                    │                    │
│                                    ▼                    │
│                           [Добавить ссылку             │
│                            на оригинал]                 │
│                                    │                    │
│                                    ▼                    │
│                           [Дисклеймер:                  │
│                            "Автоматическое резюме"]     │
└─────────────────────────────────────────────────────────┘

Меры:
- Всегда давать ссылку на источник
- Явно маркировать как AI-generated
- Не суммаризировать критичные темы (финансы, медицина)

Задача 2: Расчёт context window для RAG

Уровень: продвинутый

Условие:
Проектируете RAG-систему для работы с технической документацией. Параметры:

  • Модель: GPT-4 (8K контекст)
  • Системный промпт: 200 токенов
  • Средний вопрос пользователя: 50 токенов
  • Требуемый размер ответа: до 500 токенов
  • Документация на русском языке

Вопросы:

  1. Сколько токенов доступно для контекста из документов?
  2. Сколько это примерно страниц русского текста?
  3. Какой chunk_size и top_k выбрать для retrieval?

Решение:

# Расчёт бюджета токенов

model_context = 8192
system_prompt = 200
user_question = 50
max_response = 500
buffer = 100  # Запас на форматирование

available_for_docs = model_context - system_prompt - user_question - max_response - buffer
print(f"Доступно для документов: {available_for_docs} токенов")
# Доступно для документов: 7342 токенов

# Перевод в символы (русский текст: ~2.5 символа на токен)
chars_available = available_for_docs * 2.5
print(f"Примерно символов: {chars_available}")
# Примерно символов: 18355

# Страница ≈ 2000 символов
pages_available = chars_available / 2000
print(f"Примерно страниц: {pages_available:.1f}")
# Примерно страниц: 9.2

# Выбор параметров chunking
# Хотим 3-5 чанков в контексте для разнообразия

top_k = 4  # Количество чанков
chunk_tokens = available_for_docs // top_k  # ~1835 токенов на чанк
chunk_chars = chunk_tokens * 2.5  # ~4587 символов

# С учётом overlap
chunk_size = 4000  # символов
chunk_overlap = 400  # 10% overlap

print(f"""
Рекомендуемые параметры:
- chunk_size: {chunk_size} символов (~1600 токенов)
- chunk_overlap: {chunk_overlap} символов
- top_k: {top_k} чанков
- Итого контекст: ~6400 токенов (с запасом)
""")

Ответ:

  1. ~7300 токенов доступно для документов
  2. ~9 страниц русского текста
  3. chunk_size=4000 символов, top_k=4

Задача 3: Проектирование системы с учётом knowledge cutoff

Уровень: архитектурный

Условие:
Компания хочет AI-ассистента для HR-отдела. Функции:

  • Ответы на вопросы о трудовом законодательстве
  • Расчёт отпускных, больничных
  • Информация о внутренних политиках компании

Проблема: трудовое законодательство меняется, МРОТ обновляется ежегодно.

Вопросы:

  1. Спроектируйте архитектуру с учётом актуальности данных
  2. Какие данные хранить в RAG, какие получать в реальном времени?
  3. Как обеспечить корректность расчётов?

Решение:

АРХИТЕКТУРА HR-АССИСТЕНТА

┌─────────────────────────────────────────────────────────────────┐
│                         HR Assistant                             │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    Question Router                        │   │
│  │  Классифицирует вопрос по типу данных                    │   │
│  └──────────────────────────────────────────────────────────┘   │
│           │              │                │                      │
│           ▼              ▼                ▼                      │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐            │
│  │ Внутренние  │ │Законодат-во │ │    Расчёты      │            │
│  │ политики    │ │             │ │                 │            │
│  │             │ │             │ │                 │            │
│  │ RAG по базе │ │ RAG +       │ │ Калькулятор +   │            │
│  │ документов  │ │ актуальные  │ │ актуальные      │            │
│  │ компании    │ │ источники   │ │ параметры       │            │
│  └─────────────┘ └─────────────┘ └─────────────────┘            │
│           │              │                │                      │
│           └──────────────┴────────────────┘                      │
│                          │                                       │
│                          ▼                                       │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    LLM (ответ)                            │   │
│  │  + дисклеймер о необходимости проверки                   │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

ИСТОЧНИКИ ДАННЫХ:

1. Внутренние политики (RAG, обновление при изменении):
   - Положение об отпусках
   - Правила внутреннего распорядка
   - Должностные инструкции

2. Законодательство (RAG + периодическое обновление):
   - Трудовой кодекс (обновлять при изменениях)
   - Федеральные законы
   - Разъяснения Минтруда

3. Актуальные параметры (БД, обновление по расписанию):
   - МРОТ (ежегодно)
   - Ставка рефинансирования (при изменении)
   - Региональные коэффициенты

4. Расчёты (Python-функции, не LLM):
   - Расчёт отпускных
   - Расчёт больничных
   - Расчёт компенсаций
# Пример реализации калькулятора

class HRCalculator:
    def __init__(self, params_db):
        self.params = params_db

    def calculate_vacation_pay(
        self,
        average_daily_salary: float,
        vacation_days: int
    ) -> dict:
        """
        Расчёт отпускных по формуле:
        Отпускные = Средний дневной заработок × Дни отпуска
        """
        vacation_pay = average_daily_salary * vacation_days

        return {
            "vacation_pay": round(vacation_pay, 2),
            "formula": f"{average_daily_salary} × {vacation_days}",
            "calculation_date": datetime.now().isoformat(),
            "legal_basis": "ст. 139 ТК РФ",
            "disclaimer": "Расчёт приблизительный. Для точного расчёта обратитесь в бухгалтерию."
        }

    def calculate_sick_leave(
        self,
        average_daily_salary: float,
        sick_days: int,
        experience_years: int
    ) -> dict:
        """
        Расчёт больничного с учётом стажа
        """
        # Процент от зарплаты в зависимости от стажа
        if experience_years < 5:
            percent = 0.6
        elif experience_years < 8:
            percent = 0.8
        else:
            percent = 1.0

        # Ограничение по МРОТ
        mrot = self.params.get_current_mrot()
        min_daily = mrot / 30

        daily_pay = max(average_daily_salary * percent, min_daily)
        total = daily_pay * sick_days

        return {
            "sick_leave_pay": round(total, 2),
            "daily_rate": round(daily_pay, 2),
            "experience_percent": f"{percent*100}%",
            "mrot_used": mrot,
            "legal_basis": "Федеральный закон № 255-ФЗ",
            "disclaimer": "Расчёт приблизительный."
        }

Задача 4: Диагностика галлюцинаций в продакшене

Уровень: архитектурный

Условие:
Ваш RAG-бот в продакшене 2 месяца. Пользователи жалуются на неточные ответы, но конкретных примеров мало. Нужно построить систему мониторинга качества.

Вопросы:

  1. Какие метрики собирать?
  2. Как автоматически детектировать потенциальные галлюцинации?
  3. Как организовать процесс улучшения?

Решение:

# Система мониторинга качества RAG

class RAGQualityMonitor:
    def __init__(self, llm, vector_store):
        self.llm = llm
        self.vector_store = vector_store
        self.logs = []

    def log_interaction(
        self,
        question: str,
        retrieved_docs: list,
        answer: str,
        user_feedback: Optional[str] = None
    ):
        """Логируем каждое взаимодействие"""

        # Автоматические метрики
        metrics = {
            "timestamp": datetime.now().isoformat(),
            "question": question,
            "answer": answer,
            "retrieved_docs_count": len(retrieved_docs),
            "answer_length": len(answer),

            # Метрики качества retrieval
            "retrieval_scores": [doc.score for doc in retrieved_docs],
            "avg_retrieval_score": np.mean([doc.score for doc in retrieved_docs]),

            # Детекция потенциальных проблем
            "potential_hallucination": self.detect_hallucination(answer, retrieved_docs),
            "confidence_phrases": self.detect_uncertainty(answer),
            "contains_numbers": bool(re.search(r'd+', answer)),
            "contains_references": bool(re.search(r'пункт|статья|документ', answer, re.I)),

            # Фидбек пользователя
            "user_feedback": user_feedback,
        }

        self.logs.append(metrics)

        # Алерт при подозрении на галлюцинацию
        if metrics["potential_hallucination"]["score"] > 0.7:
            self.send_alert(metrics)

        return metrics

    def detect_hallucination(self, answer: str, docs: list) -> dict:
        """
        Эвристики для детекции галлюцинаций
        """
        doc_text = " ".join([doc.content for doc in docs])

        checks = {
            "low_retrieval_score": np.mean([doc.score for doc in docs]) < 0.5,
            "answer_longer_than_context": len(answer) > len(doc_text) * 0.5,
            "specific_numbers_not_in_context": self._check_numbers(answer, doc_text),
            "references_not_in_context": self._check_references(answer, doc_text),
        }

        score = sum(checks.values()) / len(checks)

        return {
            "score": score,
            "checks": checks
        }

    def _check_numbers(self, answer: str, context: str) -> bool:
        """Проверяем, есть ли в ответе числа, которых нет в контексте"""
        answer_numbers = set(re.findall(r'd+', answer))
        context_numbers = set(re.findall(r'd+', context))

        new_numbers = answer_numbers - context_numbers
        # Исключаем типичные числа (года, проценты)
        suspicious = {n for n in new_numbers if len(n) > 2 and not n.startswith('20')}

        return len(suspicious) > 0

    def _check_references(self, answer: str, context: str) -> bool:
        """Проверяем ссылки на пункты/статьи"""
        answer_refs = re.findall(r'(?:пункт|статья|п.)s*(d+.?d*)', answer, re.I)
        context_refs = re.findall(r'(?:пункт|статья|п.)s*(d+.?d*)', context, re.I)

        new_refs = set(answer_refs) - set(context_refs)
        return len(new_refs) > 0

    def detect_uncertainty(self, answer: str) -> list:
        """Находим фразы неуверенности"""
        uncertainty_phrases = [
            "возможно", "вероятно", "скорее всего",
            "не уверен", "предположительно", "ориентировочно"
        ]

        found = [p for p in uncertainty_phrases if p in answer.lower()]
        return found

    def generate_report(self, days: int = 7) -> dict:
        """Генерируем отчёт по качеству"""
        recent = [l for l in self.logs 
                  if datetime.fromisoformat(l["timestamp"]) > datetime.now() - timedelta(days=days)]

        return {
            "period": f"Last {days} days",
            "total_queries": len(recent),
            "avg_retrieval_score": np.mean([l["avg_retrieval_score"] for l in recent]),
            "potential_hallucinations": sum(1 for l in recent if l["potential_hallucination"]["score"] > 0.7),
            "negative_feedback": sum(1 for l in recent if l["user_feedback"] == "negative"),
            "queries_with_uncertainty": sum(1 for l in recent if l["confidence_phrases"]),
        }

Блок 6: Домашнее задание

Уровень 1: Теория (обязательно)

  1. Объясните разницу между фактической галлюцинацией и галлюцинацией в контексте. Приведите по одному примеру каждого типа из вашей предметной области.
  2. Рассчитайте, сколько страниц русского текста поместится в контекст модели Claude 3 Sonnet (200K токенов), если системный промпт занимает 500 токенов, а на ответ нужно зарезервировать 2000 токенов.
  3. Составьте таблицу сравнения стратегий работы с длинными документами:
Стратегия Когда использовать Плюсы Минусы
Chunking + RAG
Map-Reduce
Sliding Window
Иерархический индекс

Уровень 2: Практика (рекомендуется)

  1. Напишите функцию валидации ответа модели, которая:
    • Извлекает все ссылки на пункты/статьи из ответа
    • Проверяет их наличие в исходном контексте
    • Возвращает список невалидных ссылок
  2. Проведите эксперимент с «Lost in the Middle»:
    • Возьмите текст на 5000+ токенов
    • Вставьте уникальный факт в начало, середину и конец
    • Задайте модели вопрос об этом факте
    • Сравните качество ответов

Уровень 3: Применение (для продвинутых)

  1. Спроектируйте систему мониторинга галлюцинаций для production RAG-системы:
    • Какие метрики собирать
    • Как автоматически детектировать проблемы
    • Как организовать алерты
    • Как строить процесс улучшения на основе данных

Блок 7: Чек-лист самопроверки

После этого урока вы должны уметь:

  • [ ] Объяснить, почему LLM галлюцинируют (техническая причина)
  • [ ] Классифицировать типы галлюцинаций
  • [ ] Выбрать стратегию митигации для конкретного use case
  • [ ] Рассчитать бюджет токенов для RAG-системы
  • [ ] Спроектировать работу с документами, превышающими context window
  • [ ] Определить, когда нужны актуальные данные vs. знания модели
  • [ ] Настроить промпт для минимизации галлюцинаций
  • [ ] Построить систему валидации ответов
Prompt Engineering: как заставить LLM делать то, что нужно

Prompt Engineering: как заставить LLM делать то, что нужно

12.01.2026 Read More
Transformer, Attention и Токенизация — разбираем до винтика

Transformer, Attention и Токенизация — разбираем до винтика

11.01.2026 Read More

Leave a Reply