Почему 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, и условия штрафов другие.
Причина: Модель обучена на тысячах договоров и генерирует «типичный» ответ вместо анализа конкретного документа.
Решения:
- Строгий промпт с запретом домысливания:
prompt = """ СТРОГИЕ ПРАВИЛА: - Отвечай ТОЛЬКО на основе текста договора ниже
- Если информации нет — отвечай «В договоре не указано»
- ОБЯЗАТЕЛЬНО цитируй конкретный пункт: «Пункт X.X: [цитата]»
- Если не уверен — скажи об этом явно
Текст договора:
{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: Извлечение релевантных пунктов 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)
- Периодическая суммаризация:
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 модели.
Решения:
- Классификация вопросов:
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" - Интеграция с внешними 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 плохо выполняют арифметические операции, особенно с большими числами.
Решения:
- Вынос вычислений в код:
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 и давать ответы. «Не знаю» — редкий паттерн в обучающих данных.
Решения:
- Явное разрешение не знать:
prompt = """ Ты — ассистент компании X.
ВАЖНЫЕ ПРАВИЛА:
- Если ты не уверен в ответе — скажи «Я не уверен, рекомендую уточнить у специалиста»
- Если информации нет в твоих знаниях — скажи «У меня нет этой информации»
- НИКОГДА не выдумывай телефоны, адреса, имена сотрудников
- Лучше признать незнание, чем дать неверную информацию
Вопрос пользователя: {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
- 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. Оцените риск галлюцинаций для каждого и предложите меры митигации.
- Чат-бот для ответов на вопросы по FAQ (50 типовых вопросов)
- Генерация черновиков юридических документов
- Суммаризация новостей из RSS-лент
Вопросы:
- Ранжируйте use cases по уровню риска (низкий/средний/высокий)
- Для каждого предложите архитектурное решение
Решение:
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 токенов
- Документация на русском языке
Вопросы:
- Сколько токенов доступно для контекста из документов?
- Сколько это примерно страниц русского текста?
- Какой 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 токенов (с запасом)
""")
Ответ:
- ~7300 токенов доступно для документов
- ~9 страниц русского текста
- chunk_size=4000 символов, top_k=4
Задача 3: Проектирование системы с учётом knowledge cutoff
Уровень: архитектурный
Условие:
Компания хочет AI-ассистента для HR-отдела. Функции:
- Ответы на вопросы о трудовом законодательстве
- Расчёт отпускных, больничных
- Информация о внутренних политиках компании
Проблема: трудовое законодательство меняется, МРОТ обновляется ежегодно.
Вопросы:
- Спроектируйте архитектуру с учётом актуальности данных
- Какие данные хранить в RAG, какие получать в реальном времени?
- Как обеспечить корректность расчётов?
Решение:
АРХИТЕКТУРА 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 месяца. Пользователи жалуются на неточные ответы, но конкретных примеров мало. Нужно построить систему мониторинга качества.
Вопросы:
- Какие метрики собирать?
- Как автоматически детектировать потенциальные галлюцинации?
- Как организовать процесс улучшения?
Решение:
# Система мониторинга качества 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: Теория (обязательно)
- Объясните разницу между фактической галлюцинацией и галлюцинацией в контексте. Приведите по одному примеру каждого типа из вашей предметной области.
- Рассчитайте, сколько страниц русского текста поместится в контекст модели Claude 3 Sonnet (200K токенов), если системный промпт занимает 500 токенов, а на ответ нужно зарезервировать 2000 токенов.
- Составьте таблицу сравнения стратегий работы с длинными документами:
| Стратегия | Когда использовать | Плюсы | Минусы |
|---|---|---|---|
| Chunking + RAG | |||
| Map-Reduce | |||
| Sliding Window | |||
| Иерархический индекс |
Уровень 2: Практика (рекомендуется)
- Напишите функцию валидации ответа модели, которая:
- Извлекает все ссылки на пункты/статьи из ответа
- Проверяет их наличие в исходном контексте
- Возвращает список невалидных ссылок
- Проведите эксперимент с «Lost in the Middle»:
- Возьмите текст на 5000+ токенов
- Вставьте уникальный факт в начало, середину и конец
- Задайте модели вопрос об этом факте
- Сравните качество ответов
Уровень 3: Применение (для продвинутых)
- Спроектируйте систему мониторинга галлюцинаций для production RAG-системы:
- Какие метрики собирать
- Как автоматически детектировать проблемы
- Как организовать алерты
- Как строить процесс улучшения на основе данных
Блок 7: Чек-лист самопроверки
После этого урока вы должны уметь:
- [ ] Объяснить, почему LLM галлюцинируют (техническая причина)
- [ ] Классифицировать типы галлюцинаций
- [ ] Выбрать стратегию митигации для конкретного use case
- [ ] Рассчитать бюджет токенов для RAG-системы
- [ ] Спроектировать работу с документами, превышающими context window
- [ ] Определить, когда нужны актуальные данные vs. знания модели
- [ ] Настроить промпт для минимизации галлюцинаций
- [ ] Построить систему валидации ответов








