Галлюцинации LLM: почему модели врут и как с этим бороться
opensourceaillmhallucinationsreliabilityit
Введение: когда 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, а не баг.
Ключевые выводы:
- Галлюцинации неизбежны — LLM предсказывает токены, а не извлекает факты
- Temperature=0 снижает, но не устраняет проблему
- RAG — самый эффективный метод (50-70% снижение)
- Verification pipeline даёт 60-80% снижение, но дорог
- Комбинация методов лучше одного
- Для критичных задач: LLM + валидация + человек
Ресурсы: