Как заставить LLM не врать про версии

Каждый, кто много работает с LLM, сталкивался с этим: ты обсуждаешь какое-то решение с LLM, получаешь ответ с конкретным кодом или фактами — а потом выясняется, что код устарел полгода назад, и модель настаивает на использовании решений годичной давности. Или хуже: ты пишешь код с правильным современным синтаксисом, а модель тебя «поправляет» на устаревший вариант. Самое печальное, когда современная модель настаивает на то, что её самой не существует — например, Claude Opus 4.6 может утверждать, что самая новая модель Anthropic — это Sonnet 3.5, а Gemini 3.0 Pro советовала использовать новейшую модель Gemini 1.5 Pro, у нее, мол, миллионный контекст.

Это не баг конкретной модели. Это фундаментальное свойство любого LLM: модель не различает, что она «знает» из тренировки и что является актуальной реальностью. Причем данные обучения заложены в весах модели и по определению более значимы, чем ваш короткий промпт.

К сожалению, масштаб проблемы не ограничивается смешными анахронизмами, когда модель не знает, что Трамп опять президент США. Использование старого API или незнание новых вызовов в библиотеках — это в лучшем случае изобретение велосипеда, когда модель пишет код, чтобы интерпретируемым скриптом заменить встроенную функцию, о которой она не знает. Но это может стоить денег — как-то модель тихонько заменила мне вызов gpt4.1-mini на gpt-4o (мол, не существует же никакой mini), и вместо долей цента за запрос у меня начали тратиться пара центов.

Что не работает

Первая реакция — написать в CLAUDE.md (я работаю с Claude Code/Claude.ai, так что все примеры тут базируются на этом стеке) что-то вроде «всегда проверяй версии перед ответом». Это не работает — инструкция слишком абстрактная. Модель соглашается и продолжает отвечать из тренировочных данных — тем более, что далеко не всякий запрос подразумевает такую проверку.

Вторая попытка — добавить конкретные версии библиотек и API прямо в CLAUDE.md. Это работает лучше, но плохо масштабируется. Если у вас несколько проектов, да еще и с разным стеком, собирать полный перечень версий в одном месте неправильно, разносить в каждый проект свои — запутаетесь, а ведь еще надо и поддерживать.

Третья идея — просто говорить модели «сегодня 2026 год, твои данные устарели». Модель соглашается, а потом выдаёт код для React 17 и подключает haiku-3.5.

К чему я пришёл: трёхуровневая система

Работающее решение оказалось не одной волшебной инструкцией, а комбинацией трёх слоёв, каждый из которых закрывает свою часть проблемы.

Слой 1: Автоматическая инъекция временного контекста (Claude Code)

SessionStart hook — bash-скрипт, который срабатывает при каждом запуске сессии Claude Code. Он вычисляет текущую дату, считает разницу с датой cutoff тренировочных данных и инжектит это прямо в контекст:

CURRENT_DATE=$(date "+%Y-%m-%d")
TRAINING_CUTOFF="2025-01-01"
# ... вычисление разницы ...

cat << EOF
📅 Current date: ${CURRENT_DATETIME}
📚 Your knowledge cutoff: approximately early 2025
⏱️  Time gap: ~${MONTHS_GAP} months of potential changes

🔴 HIGH-RISK LIBRARIES (frequent breaking changes):
Next.js, React, LangChain, OpenAI SDK, Anthropic SDK, 
Pydantic, FastAPI, Prisma

⚠️  CRITICAL EPISTEMIC WARNING:
Your training data is ${MONTHS_GAP}+ months old. During this period:
- Libraries have released new major versions
- APIs have changed, been deprecated, or added new features
- Best practices may have evolved
- New tools and frameworks may have emerged

📋 MANDATORY VERIFICATION PROTOCOL:
1. For ANY question about versions, APIs, current state → SEARCH FIRST
2. Never claim "this doesn't exist" without verification
3. If code looks unfamiliar → assume it's valid modern syntax
4. Mark uncertain claims: "Based on training (may be outdated)..."
   
EOF

Зачем считать месяцы, если можно просто написать «данные устарели»? Потому что «ваши данные устарели на 14 месяцев» — это более конкретный сигнал. Модель реагирует на конкретику лучше, чем на абстрактные предупреждения. Плюс список высокорисковых библиотек — это явный триггер: видишь LangChain в коде проекта → ищи документацию, не угадывай.

У новых версий Claude более свежие знания, но ненамного — май-август 2025 года. И даже в этом случае не факт, что знания в нужной сфере обновлены прямо на указанную дату.

Слой 2: Правила верификации в CLAUDE.md (Claude Code)

Hook создаёт контекст, но не задаёт поведение. Поведение задаёт epistemic-правило, которое живёт в CLAUDE.md проекта (или подключается через @epistemic.md или лежит отдельным файлом в rules/):

Ключевой элемент — не список правил, а иерархия источников истины:

  1. Файлы проекта (package.json, requirements.txt, go.mod) — абсолютный авторитет
  2. Внешние инструменты (web search, документация, MCP) — перекрывают тренировочные данные
  3. Тренировочные данные — «Legacy Archive», надёжны для синтаксиса и логики, ненадёжны для версий и API

Формат ответа тоже критичен. Модель должна маркировать источник каждого утверждения:

  • ✓ VERIFIED (from [source]) — подтверждено поиском или документацией
  • ⚠ FROM TRAINING (may be outdated) — из тренировочных данных, не проверено
  • ? UNCERTAIN — низкая уверенность, рекомендуется проверка

Это не для красоты. Это меняет внутреннее поведение модели: когда она обязана промаркировать источник, она реже галлюцинирует, потому что маркировка заставляет «задуматься» о происхождении информации.

Отдельно важен блок «Anti-Hallucination» с конкретными запретами:

  • Не «исправлять» пользовательский код на знакомый старый синтаксис
  • Не утверждать «этого не существует» без проверки
  • Не подменять современные паттерны на legacy-аналоги
  • Не называть версии по памяти как факт

И явное разрешение сказать «я не знаю». Это звучит странно, но модели нужно это разрешение. По умолчанию LLM оптимизированы на helpfulness — они скорее дадут неправильный ответ, чем скажут «не уверен». Явное «Admitting uncertainty is BETTER than confident hallucination» позволяет эту настройку слегка подвинуть.

Вот мой файл практически целиком:

## Epistemic Hierarchy (Priority of Truth Sources)

1. **Project Files & User Context** (HIGHEST): package.json, requirements.txt, go.mod are authoritative. User-provided facts = ground truth. Unknown feature/API = assume NEW, not error.
2. **External Tools & Documentation**: Web search, fetched docs, MCP responses override training data.
3. **Your Training Data** (LOWEST): "Legacy Archive" — reliable for syntax/logic, unreliable for versions/APIs/events.

## Mandatory Verification

ALWAYS verify before answering about:
- Library/framework versions or release dates
- LLM versions, names and parameters
- API signatures, method parameters, return types
- Deprecated vs current approaches
- "Does X exist?" / "Is Y still supported?"
- Any fact that could have changed since training

## Response Format

When providing technical information:
- `VERIFIED (from [source]): [info]` — confirmed via search/docs
- `FROM TRAINING (may be outdated): [info]` — unverified
- `UNCERTAIN: [info] -- recommend verification` — low confidence

When diagnosing errors or unexpected behavior:
- `VERIFIED: [fact]` — read from code, logs, config, or stated by user
- `HYPOTHESIS: [assumption]` — inference that needs confirmation, always mark as such
- Present multiple hypotheses with equal weight when uncertain

## Anti-Hallucination

Do NOT:
- "Correct" user code to older syntax you're familiar with
- Claim "this doesn't exist" without verification
- Silently downgrade modern patterns to legacy equivalents
- State version numbers from memory as facts

Instead:
- Unfamiliar code -> assume valid modern syntax
- Uncertain existence -> "let me check" or ask user
- Suggesting alternatives -> explain WHY, confirm user's version first
- Stating versions -> mark as "from training, verify current"

## Version Handling

1. Check project files (package.json, etc.) FIRST
2. Version specified -> use THAT version's API
3. No version info -> ASK user
4. User states version -> trust it, even if unfamiliar

## Permission to Say "I Don't Know"

You are explicitly encouraged to say:
- "I'm not certain about the current API -- let me check"
- "This might have changed since my training"
- "I don't recognize this, but assuming it's valid modern syntax"

Admitting uncertainty is BETTER than confident hallucination.

Слой 3: User Preferences в claude.ai

Для работы через веб-интерфейс claude.ai те же принципы заданы в пользовательских настройках. Это тот же Knowledge Freshness Protocol, но адаптированный под формат preferences: приоритет источников, обязательные триггеры поиска, формат маркировки, запрещённые действия.

Здесь нет хуков и файлов проекта, но есть web search. Правило «ALWAYS use web search before answering about: library versions, API signatures, deprecations» заставляет модель искать вместо угадывания.

Что это даёт на практике

После внедрения этой системы количество «тихих» ошибок — когда модель уверенно пишет код для неправильной версии — сократилось драматически. Не до нуля: модель всё ещё иногда игнорирует инструкции, особенно после compact. Но паттерн поведения изменился: вместо «вот вам код» модель чаще говорит «проверю текущую версию API» и лезет в документацию.

Маркировка источников оказалась полезна не только как механизм честности, но и как способ быстро оценить качество ответа. Видишь сплошные ⚠ FROM TRAINING — перепроверяй. Видишь ✓ VERIFIED — можно доверять.

Ограничения, отговорки и отказ от ответственности

Это не серебряная пуля. Недетермированность LLM дает ненулевую вероятность того, что модель проигнорирует даже CLAUDE.md. Кроме того, есть разные случаи:

  • После compact контекст сжимается, и правила теряют приоритет. Хук SessionStart помогает (он в начале сессии), но epistemic-правила из CLAUDE.md могут ослабнуть в длинных сессиях. Впрочем, я практически не использую compact, а с новым 1M контекстным окном вероятность забывания меньше. Но не нулевая.
  • Модель может «обойти» правила, если вопрос кажется ей слишком простым. «Какой последний релиз React?» — поищет. «Напиши useEffect» — может и не проверить, что API не изменился.
  • Web search не всегда возвращает релевантные результаты. Иногда модель ищет, не находит и возвращается к тренировочным данным — но уже без маркировки FROM TRAINING. Про web search и fetch у меня есть отдельный опыт, но про это отдельно.

Воспроизводимая конфигурация

Всё описанное — три файла:

  1. ~/.claude/hooks/inject-temporal-context.sh — хук для Claude Code, считает дату и инжектит контекст
  2. epistemic.md — правила верификации, подключается к CLAUDE.md через @epistemic.md или просто лежит в ~/.claude/rules/
  3. Текст для User Preferences в claude.ai — Knowledge Freshness Protocol

Хук добавляется в ~/.claude/settings.json:

{
  "hooks": {
    "SessionStart": [{
      "hooks": [{
        "type": "command",
        "command": "$HOME/.claude/hooks/inject-temporal-context.sh"
      }]
    }]
  }
}

Адаптируйте TRAINING_CUTOFF и FAST_MOVING_LIBS под свой стек. Список высокорисковых библиотек — это именно ваш список, не универсальный. Если вы пишете на Rust и Go, там другие проблемные пакеты.