Галлюцинации LLM: почему модели врут и как с этим бороться

opensourceaillmhallucinationsreliabilityit
← Back to Blog

Введение: когда LLM уверенно врет

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

Факт: По разным оценкам, от 5% до 40% ответов LLM содержат фактические ошибки.

Пример галлюцинации:
  User: "Кто написал 'Мастер и Маргарита'?"
  LLM: "Роман 'Мастер и Маргарита' был написан Львом Толстым в 1878 году."
  ✗ Автор: Михаил Булгаков
  ✗ Год: 1928-1940
  ✗ Модель УВЕРЕННО врет

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

Определение

Галлюцинация = правдоподобный + неверный + уверенный

LLM не "врет" в человеческом смысле.
Он предсказывает следующий токен на основе статистических паттернов.

Когда паттерн есть, а факта нет:
  → LLM заполняет пробел самым вероятным продолжением
  → Результат звучит как факт, но его нет

Почему это происходит?

Причины на уровне архитектуры:

1. Autoregressive generation
   LLM предсказывает токен за токеном
   Каждый токен зависит от предыдущих
   Ошибка в начале → цепочка ошибок

2. Statistical next-token prediction
   Модель не "знает" факты
   Модель знает вероятности
   "Наиболее вероятный" ≠ "правильный"

3. Training data limitations
   Данные содержат ошибки
   Данные устарели
   Данные не покрывают все факты

4. No ground truth connection
   LLM не подключен к реальности
   Нет доступа к фактам во время генерации
   Всё — статистика паттернов текста

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

Фактические галлюцинации

Модель выдумывает факты:

User: "Какая столица Австралии?"
LLM: "Сидней — крупнейший город и столица Австралии."
      ✗ Столица: Канберра

User: "Сколько планет в Солнечной системе?"
LLM: "9 планет, включая Плутон."
      ✗ Официально: 8 (Плутон — карликовая планета с 2006)

Галлюцинации контекста

Модель искажает предоставленный контекст:

Context: "Компания X заработала $100M в 2024"
User: "Сколько заработала компания X?"
LLM: "Компания X заработала $150M в 2024 году."
      ✗ Число изменено
      ✗ Модель "улучшила" данные

Context: "Исследование показало рост на 15%"
User: "Что показало исследование?"
LLM: "Исследование показало рост на 15% среди мужчин и 22% среди женщин."
      ✗ Детали изобретены
      ✗ В контексте не было гендерного разделения

Галлюцинации кода

Модель генерирует несуществующие API:

User: "Как создать таблицу в SQLite с Python?"
LLM:
```python
import sqlite
db = sqlite.connect("test.db")
db.create_table("users", {"name": "TEXT", "age": "INT"})
  ✗ Модуль: sqlite3 (не sqlite)
  ✗ Метод: execute() с CREATE TABLE (не create_table)
  ✗ Типы: TEXT, INTEGER (не INT)

### Цитаты и источники

Самый опасный тип:

User: "Найди исследование про эффективность LLM в медицине" LLM: "Согласно Smith et al. (2024) в Nature Medicine, LLM показали 94% точность в диагностике..." ✗ Исследование не существует ✗ Авторы вымышленные ✗ Журнал может не публиковать такую статью ✗ Звучит абсолютно убедительно


---

## Метрики оценки галлюцинаций

### Hallucination Rate

```python
def calculate_hallucination_rate(model, test_cases):
    """
    Hallucination Rate = % ответов с фактическими ошибками
    """
    hallucinations = 0
    
    for case in test_cases:
        response = model.generate(case.question)
        facts = extract_facts(response)
        
        for fact in facts:
            if not verify_fact(fact, case.ground_truth):
                hallucinations += 1
                break
    
    return hallucinations / len(test_cases)

# Пример результатов:
# GPT-4:      15% hallucination rate
# Llama 3 8B: 28% hallucination rate
# Mistral 7B: 32% hallucination rate

Factuality Score

def factuality_score(response, ground_truth):
    """
    Фактуальность: сколько утверждений в ответе
    совпадают с ground truth
    """
    response_facts = extract_facts(response)
    truth_facts = extract_facts(ground_truth)
    
    correct = 0
    for fact in response_facts:
        if fact in truth_facts:
            correct += 1
    
    # Precision: из сказанного — сколько правды
    precision = correct / len(response_facts) if response_facts else 0
    # Recall: из правды — сколько сказано
    recall = correct / len(truth_facts) if truth_facts else 0
    
    return {
        'precision': precision,
        'recall': recall,
        'f1': 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
    }

LLM-as-Judge для детекции

def hallucination_detection_llm_judge(response, context):
    """
    Используем одну LLM для проверки другой
    """
    judge_prompt = f"""
Проверь ответ на фактическую точность относительно контекста.

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

Ответ:
{response}

Для каждого утверждения в ответе определи:
1. Подтверждается контекстом: YES/NO
2. Противоречит контексту: YES/NO
3. Не может быть проверено: YES/NO

Формат: JSON
{{
  "statements": [
    {{"text": "...", "supported": true, "contradicted": false}},
    ...
  ],
  "hallucination_rate": 0.25,
  "verdict": "contains_hallucinations"
}}
"""
    return judge_llm.generate(judge_prompt)

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

Уровень 1: Temperature и параметры генерации

# Низкая temperature → более детерминированные ответы
response = llm.generate(
    prompt="Столица Австралии?",
    temperature=0.0,      # Минимальная случайность
    top_p=0.1,            # Только самые вероятные токены
    max_tokens=50         # Ограничиваем длину
)
# → "Канберра"

# Высокая temperature → больше креатива, больше галлюцинаций
response = llm.generate(
    prompt="Столица Австралии?",
    temperature=1.0,      # Максимальная случайность
    top_p=0.95
)
# → "Сидней, расположенный на восточном побережье..."
Влияние temperature на галлюцинации:

Temperature | Hallucination Rate | Качество
------------|-------------------|----------
0.0         | 8%                | Сухой, но точный
0.3         | 12%               | Баланс
0.7         | 22%               | Креативный
1.0         | 35%               | Хаотичный

Для фактоидных задач: temperature ≤ 0.3
Для креативных задач: temperature 0.7-1.0 (галлюцинации допустимы)

Уровень 2: RAG (Retrieval-Augmented Generation)

Архитектура RAG:

  User Query
      │
      ▼
  ┌─────────┐
  │ Search  │  ← Ищем в реальной базе знаний
  │ Engine  │
  └────┬────┘
       │
       ▼ Relevant Documents
  ┌─────────┐
  │   LLM   │  ← Генерируем ответ на основе документов
  │ + Docs  │
  └────┬────┘
       │
       ▼
  Verified Response
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.chains import RetrievalQA

# 1. Создаем базу знаний из реальных документов
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(documents, embeddings)

# 2. Создаем RAG chain
llm = ChatOpenAI(temperature=0)
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=vectorstore.as_retriever(
        search_kwargs={"k": 3}  # Берём 3 лучших документа
    ),
    return_source_documents=True  # Возвращаем источники
)

# 3. Задаем вопрос
result = qa_chain.invoke({"query": "Какова политика компании по отпуску?"})

# Результат содержит:
# - Ответ на основе документов
# - Ссылки на источники
# - Если документа нет → модель скажет "не знаю"

Уровень 3: Self-Consistency

def self_consistency_check(question, n_responses=5):
    """
    Генерируем несколько ответов, сравниваем их.
    Если ответы расходятся → возможная галлюцинация.
    """
    responses = []
    for _ in range(n_responses):
        r = llm.generate(question, temperature=0.7)
        responses.append(r)
    
    # Извлекаем факты из каждого ответа
    all_facts = [extract_facts(r) for r in responses]
    
    # Факты, которые повторяются в большинстве ответов
    consistent_facts = []
    for fact in set(f for facts in all_facts for f in facts):
        count = sum(1 for facts in all_facts if fact in facts)
        if count >= n_responses * 0.6:  # 60%+ согласия
            consistent_facts.append(fact)
    
    return consistent_facts, responses

Уровень 4: Verification Pipeline

Verification Pipeline:

  ┌──────────┐     ┌──────────┐     ┌──────────┐
  │ Generate │────▶│ Extract  │────▶│ Verify   │
  │ Response │     │ Facts    │     │ against  │
  └──────────┘     └──────────┘     │ sources  │
                                    └────┬─────┘
                                         │
                                  ┌──────▼──────┐
                                  │   Are facts │
                                  │   correct?  │
                                  └──────┬──────┘
                                        ╱ ╲
                                      YES   NO
                                      │    │
                                  ┌────┐ ┌────────┐
                                  │OK  │ │ Regenerate │
                                  └────┘ │ with docs  │
                                         └────────┘
def verification_pipeline(query, max_attempts=3):
    for attempt in range(max_attempts):
        # Генерируем ответ
        response = llm.generate(query)
        
        # Извлекаем факты
        claims = extract_claims(response)
        
        # Проверяем каждый факт
        verified = []
        for claim in claims:
            search_results = web_search(claim)
            is_verified = verify_claim(claim, search_results)
            verified.append({
                'claim': claim,
                'verified': is_verified,
                'source': search_results[0] if is_verified else None
            })
        
        # Если все факты подтверждены — готово
        if all(v['verified'] for v in verified):
            return response, verified
        
        # Иначе перегенерируем с контекстом
        context = "\n".join(
            v['source'] for v in verified if v['verified']
        )
        query = f"Контекст: {context}\n\nВопрос: {query}"
    
    return response, verified  # Возвращаем с предупреждением

Уровень 5: Guardrails и валидация

import instructor
from pydantic import BaseModel, Field
from typing import List

# Используем структурированный вывод для контроля
class FactResponse(BaseModel):
    answer: str = Field(description="Ответ на вопрос")
    confidence: float = Field(ge=0.0, le=1.0, description="Уверенность 0-1")
    sources: List[str] = Field(description="Источники фактов")
    unsupported_claims: List[str] = Field(
        default_factory=list,
        description="Утверждения без источников"
    )

# LLM теперь ОБЯЗАН возвращать эту структуру
client = instructor.from_openai(OpenAI())
response = client.chat.completions.create(
    model="gpt-4",
    response_model=FactResponse,
    messages=[{"role": "user", "content": "Кто выиграл ЧМ по футболу 2022?"}]
)

# Если unsupported_claims не пустой → галлюцинация!
if response.unsupported_claims:
    print(f"ВНИМАНИЕ: {len(response.unsupported_claims)} неподтверждённых утверждений")

Сравнительная таблица методов

Метод                  | Снижение галлюцинаций | Сложность | Затраты
-----------------------|----------------------|-----------|--------
Temperature = 0        | 30-40%               | Низкая    | $
RAG                    | 50-70%               | Средняя   | $$
Self-Consistency       | 20-35%               | Средняя   | $$
Verification Pipeline  | 60-80%               | Высокая   | $$$
Guardrails + Schema    | 25-40%               | Низкая    | $
Fine-tuning на фактах  | 15-30%               | Высокая   | $$$
Human-in-the-loop      | 80-95%               | Высокая   | $$$$

Комбинированный подход

def robust_llm_answer(query, knowledge_base, tools):
    """
    Комбинируем несколько методов для максимальной надёжности.
    """
    
    # 1. RAG: ищем в знаниях
    context = knowledge_base.search(query, top_k=5)
    
    # 2. Если контекст нашлён — RAG генерация
    if context:
        response = llm.generate(
            f"Ответь на основе контекста:\n{context}\n\nВопрос: {query}\n\n"
            f"Если ответа нет в контексте, скажи 'Не знаю'.",
            temperature=0.1
        )
        
        # 3. Проверка: ответ использует контекст?
        if not uses_context(response, context):
            # 4. Верификация через поиск
            external_facts = tools.web_search(query)
            response = llm.generate(
                f"Контекст: {context}\nПоиск: {external_facts}\n{query}",
                temperature=0.1
            )
    
    # 5. Извлекаем факты и помечаем уверенность
    facts = extract_and_score_facts(response)
    
    return {
        'answer': response,
        'facts': facts,
        'confidence': min(f['score'] for f in facts) if facts else 0,
        'has_unsupported': any(f['score'] < 0.5 for f in facts)
    }

Заключение

Галлюцинации — фундаментальная проблема LLM, а не баг.

Ключевые выводы:

  1. Галлюцинации неизбежны — LLM предсказывает токены, а не извлекает факты
  2. Temperature=0 снижает, но не устраняет проблему
  3. RAG — самый эффективный метод (50-70% снижение)
  4. Verification pipeline даёт 60-80% снижение, но дорог
  5. Комбинация методов лучше одного
  6. Для критичных задач: LLM + валидация + человек

Ресурсы: