Зачем это вообще нужно?
Попроси GPT рассказать про историю вымышленного города - получишь связный текст с датами, именами и событиями. Красиво. Убедительно. Полная чушь.
Традиционный подход к детекции галлюцинаций - LLM-as-judge. Запускаем одну модель, получаем ответ, кормим его другой модели со словами "проверь, не врет ли здесь первая". Циркулярная логика уровня "вор у вора дубинку украл".
LLM-судьи часто повторяют ошибки проверяемых моделей. Если GPT склонна галлюцинировать про медицинские факты, Claude с высокой вероятностью пропустит эти же ошибки.
Геометрический метод работает иначе. Мы не спрашиваем "правда ли это?". Мы смотрим на внутреннюю структуру ответа. Если в тексте есть логические разрывы, семантические скачки или внутренние противоречия - это отражается в геометрии его векторного представления.
Представьте, что каждое предложение - точка в многомерном пространстве. Связный, логичный текст образует плавную кривую. Текст с галлюцинациями - ломаную линию с резкими скачками.
Как это работает на пальцах
Возьмем ответ LLM. Разобьем на предложения. Каждое предложение превратим в эмбеддинг (вектор). Получим последовательность точек в пространстве.
Теперь считаем расстояния между соседними точками. В идеальном тексте эти расстояния примерно одинаковы - мы движемся плавно от темы к теме. В тексте с галлюцинациями появляются рывки: модель "соскочила" с фактов на вымысел.
1 Готовим данные: от текста к векторам
Сначала нужен эмбеддер. Не гигантская LLM, а специализированная модель для векторных представлений. Sentence-BERT, E5, BGE - что угодно, что превращает текст в вектор фиксированной размерности.
from sentence_transformers import SentenceTransformer
import numpy as np
# Загружаем легковесный эмбеддер
model = SentenceTransformer('all-MiniLM-L6-v2') # 384 измерения, быстро работает
# Разбиваем текст на предложения
import nltk
nltk.download('punkt')
text = """Илон Маск родился в 1971 году в Претории.
Он основал SpaceX в 2002 году.
В 2025 году он высадился на Марсе и основал первую колонию."""
sentences = nltk.sent_tokenize(text)
print(f"Предложений: {len(sentences)}")
# Вывод: Предложений: 3
# Получаем эмбеддинги
embeddings = model.encode(sentences)
print(f"Размерность векторов: {embeddings.shape}")
# Вывод: Размерность векторов: (3, 384)
2 Считаем расстояния: ищем аномалии
Теперь анализируем геометрию. Простейшая метрика - косинусное расстояние между соседними эмбеддингами.
from scipy.spatial.distance import cosine
distances = []
for i in range(len(embeddings) - 1):
dist = cosine(embeddings[i], embeddings[i + 1])
distances.append(dist)
print(f"Расстояния между предложениями: {distances}")
# Пример вывода: [0.15, 0.62]
Видите скачок? Первое расстояние 0.15 (нормальная семантическая связь между фактами о Маске). Второе - 0.62 (резкий скачок, потому что третье предложение - вымысел).
Не используйте евклидово расстояние для эмбеддингов! В высокомерных пространствах оно ведет себя плохо. Косинусное или скалярное произведение работают надежнее.
3 Ставим диагноз: пороговое значение
Как понять, что расстояние 0.62 - это галлюцинация, а не просто смена темы? Нужен baseline.
Соберите набор правдивых текстов по вашей тематике. Посчитайте распределение расстояний между их предложениями. 95-й перцентиль этого распределения - ваш порог.
# Пример расчета порога
baseline_distances = [] # Заполните расстояниями из правдивых текстов
threshold = np.percentile(baseline_distances, 95)
print(f"Порог аномалии: {threshold:.3f}")
# Проверяем наше третье предложение
anomaly_score = distances[1] # 0.62
is_hallucination = anomaly_score > threshold
print(f"Аномалия: {anomaly_score:.3f}, Галлюцинация: {is_hallucination}")
Продвинутые метрики: не только расстояния
Расстояние между соседями - базовая метрика. Но есть более хитрые способы.
Кривизна траектории: считаем не просто расстояние A→B, а смотрим на угол между векторами A→B и B→C. Резкий поворот - признак семантического скачка.
Локальная плотность: для каждого предложения находим k ближайших соседей внутри текста. В связном тексте соседи будут рядом в последовательности. В тексте с галлюцинациями предложение-вымысел окажется "оторвано" от контекста.
Скалярное произведение с промптом: эмбеддинг каждого предложения сравниваем с эмбеддингом исходного запроса. Если предложение уходит далеко от темы запроса - подозрительно.
def calculate_curvature(embeddings, window=3):
"""Считаем кривизну траектории через углы между векторами"""
curvatures = []
for i in range(1, len(embeddings) - 1):
# Вектор от i-1 к i
vec1 = embeddings[i] - embeddings[i-1]
# Вектор от i к i+1
vec2 = embeddings[i+1] - embeddings[i]
# Косинус угла между векторами
cos_sim = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
# Кривизна = 1 - косинус (0 для прямого пути, 1 для поворота на 90°)
curvature = 1 - cos_sim
curvatures.append(curvature)
return curvatures
# Пример использования
curvatures = calculate_curvature(embeddings)
print(f"Кривизна в точках перехода: {curvatures}")
Где метод ломается (и как это чинить)
1. Короткие тексты: на 2-3 предложениях статистика не работает. Решение - анализировать не предложения, а смысловые блоки по 2-3 предложения.
2. Намеренные переходы: автор может специально делать резкую смену темы. Это не галлюцинация, а стилистический прием. Решение - обучать модель на примерах таких переходов или использовать домен-специфичные пороги.
3. Консистентные галлюцинации: если LLM систематически врет в одном стиле (например, всегда придумывает даты), эти вымыслы могут образовывать свою собственную "гладкую" траекторию. Тут нужен внешний факт-чекинг.
Практический кейс: проверка медицинских ответов
Допустим, мы делаем медицинского чат-бота. Пользователь спрашивает про симптомы гриппа. LLM отвечает:
- Грипп проявляется температурой, кашлем, слабостью.
- Инкубационный период 1-4 дня.
- Для профилактики принимайте витамин С и антибиотики.
- При температуре выше 40℃ вызывайте скорую.
Третье предложение - галлюцинация (антибиотики не работают против вирусов). Давайте поймаем ее.
medical_response = [
"Грипп проявляется температурой, кашлем, слабостью.",
"Инкубационный период 1-4 дня.",
"Для профилактики принимайте витамин С и антибиотики.",
"При температуре выше 40℃ вызывайте скорую."
]
# Получаем эмбеддинги
med_embeddings = model.encode(medical_response)
# Считаем расстояния
distances = []
for i in range(len(med_embeddings) - 1):
dist = cosine(med_embeddings[i], med_embeddings[i + 1])
distances.append(dist)
print(f"Медицинский кейс - расстояния: {[f'{d:.3f}' for d in distances]}")
# Вывод: [0.112, 0.453, 0.098]
Второе расстояние (0.453) явно выделяется. Это переход от фактов о гриппе к опасной рекомендации. Порог для медицинских текстов (посчитанный на достоверных источниках) около 0.3 - значит, ловим галлюцинацию.
Интеграция в пайплайн
Геометрический метод идеально встает в pre-filtering стадию:
- LLM генерирует ответ
- Анализируем эмбеддинги, находим подозрительные фрагменты
- Только эти фрагменты отправляем на дорогую проверку (поиск в знаниях, факт-чекинг)
- Или просто маркируем: "эта часть ответа требует проверки"
Стоимость обработки? Копейки. Эмбеддеры в сотни раз легче LLM. Обработка 1000 ответов стоит меньше запроса к GPT-4.
| Метод | Точность | Стоимость | Задержка |
|---|---|---|---|
| LLM-as-judge (GPT-4) | Высокая | $$$ | 2-5 сек |
| Факт-чекинг по API | Очень высокая | $$$$ | 10+ сек |
| Геометрический метод | Средняя | ¢ | <0.1 сек |
Что дальше? Эволюция метода
Самый очевидный апгрейд - учиться не на расстояниях, а на паттернах. Собираем датасет галлюцинаций, смотрим, как выглядит их геометрия в разных доменах. Обучаем классификатор не на сырых расстояниях, а на их производных, фурье-образах, автокорреляции.
Второе направление - мультимодальность. Если LLM генерирует ответ с картинками, таблицами, кодом - анализируем согласованность между модами. Эмбеддинг текста про "рост продаж" должен быть близок к эмбеддингу графика, который идет следом. Если нет - подозрительно.
Третье - временные ряды. В диалоге каждый ответ LLM можно рассматривать как точку. Диалог с галлюцинациями будет "скакать" по семантическому пространству. Диалог с фактами - двигаться по предсказуемой траектории.
Главное преимущество метода в его простоте. Не нужны тонны размеченных данных. Не нужны гигаваттные модели. Всего лишь векторы, расстояния и немного статистики. Иногда самые эффективные решения - самые простые.
Пока индустрия строит гигантских LLM-судей за миллионы долларов, попробуйте для начала измерить расстояния между предложениями. Часто этого хватает.