Введение: Медицина не прощает вольностей
Год назад мы запустили сервис по интерпретации гематологических анализов на базе LLM. Звучит круто? На бумаге — да. В продакшне — ад. Пациенты получали рекомендации с точностью до наоборот, врачи отказывались работать с системой, а один юрист уже готовил иск. В этой статье — четыре реальных прокола, которые мы допустили, и способы их исправления. Никакой теории — только кровь (в прямом смысле), пот и слезы DevOps-инженера.
Если вы думаете, что достаточно скормить модели пару медицинских учебников и она станет доктором, — вы либо не работали с LLM, либо не видели, что бывает, когда модель уверенно несет чушь. Поехали.
Кейс 1: «Доктор, у меня рак?» — Как мы научили LLM не паниковать
Проблема: гиперагрессивная интерпретация
Наша первая версия промпта звучала примерно как «Ты — опытный гематолог. Проанализируй результаты анализов и дай диагноз». LLM (GPT-4o на тот момент) при малейшем отклонении от нормы тут же пугала пациента онкологией. Пример: легкий лимфоцитоз (47% при референсе 19–37%) — и модель пишет «Возможно, хронический лимфолейкоз». Без контекста, без учета возраста пациента, без динамики.
В теории мы хотели получить настороженность. На практике — получили панику и звонки от разгневанных врачей. Корень зла? Модель не знала, где проводить границу между нормой и патологией. Она просто галлюцинировала вероятности, опираясь на свой обучающий корпус, где онкология встречается чаще, чем безобидный лимфоцитоз.
Ошибка: отсутствие пост-процессинга и жесткой фильтрации по клиническим протоколам. LLM нельзя доверять пороговые решения без внешнего валидатора.
Решение: контрольный слой с референсными диапазонами
Мы добавили этап проверки — перед тем как отдать ответ пользователю, система сравнивает каждый показатель с официальными референсными интервалами (из Минздрава РФ) и категоризирует отклонения: «незначительное», «умеренное», «критическое». Если модель выставляет диагноз «рак» при незначительном отклонении — блокируем и отправляем на доработку промпта.
Вот как выглядит минимальный пайплайн проверки:
def validate_interpretation(llm_output: dict, ref_ranges: dict) -> bool:
for param, value in llm_output['parameters'].items():
low, high = ref_ranges[param]
deviation = abs(value - (low+high)/2)
severity = 'critical' if deviation > 0.5*(high-low) else 'moderate' if deviation > 0.2*(high-low) else 'minor'
if severity == 'critical':
continue # допустимо
if severity == 'minor' and llm_output.get('diagnosis_severity') in ['severe']:
return False # блокируем
return TrueДетальнее про конструкцию control layer мы писали в статье «Как построить production-ready control layer для LLM: 8 компонентов с кодом и бенчмарками».
Пошаговый план исправления
- Собрать авторитетные справочники референсных интервалов (Минздрав, ВОЗ, UpToDate).
- Внедрить классификатор отклонений перед LLM-пайплайном.
- Добавить правило: если модель предсказывает заболевание тяжелее, чем позволяет степень отклонения, — вернуть ответ «normal» и запросить дополнительное подтверждение у врача.
- Верифицировать на синтетическом датасете с 1000+ вариантов.
Кейс 2: Железодефицит, который удвоился — дублирующие рекомендации
Проблема: недетерминированный вывод
Пациент сдает анализы дважды с интервалом в неделю. Показатели почти не изменились. Но LLM в первый раз советует «принимать препараты железа с витамином C», а во второй — «сначала проверить ферритин, потом решать». Разные советы по одним и тем же данным. Естественно, пациент в панике: «Почему второй врач думает иначе?».
Причина: LLM недетерминирована. Даже при нулевой температуре многие модели (GPT, Claude) показывают разброс из-за floating point операций и внутреннего состояния. Мы выявили, что до 30% повторных запросов с идентичным контекстом дают разные рекомендации.
Важно: в медицине воспроизводимость — это не роскошь, а юридическое требование. Если врач не может повторить результат — ответственность ложится на разработчика.
Решение: семантическое кэширование и детерминированный слой
Мы перестали отправлять каждый новый запрос напрямую в LLM. Вместо этого на уровне API вычисляется семантический хэш от контекста (показатели, возраст, пол) через эмбеддинги. Если хэш совпал с предыдущим — отдаем закэшированный ответ. Если не совпал, но семантическая близость > 95% — сверяемся: можно использовать предыдущий или перегенерировать с указанием «не противоречить предыдущему ответу».
Параллельно мы увеличили temperature до 0.0 (для некоторых моделей это не панацея, но снижает variance). А главное — внедрили юнит-тесты на детерминизм, как описано в статье «Тестируем недетерминированные LLM: как написать тесты для вызова функций и не сойти с ума».
Пошаговый план исправления
- Реализовать семантическое кэширование на основе эмбеддингов (sentence-transformers + FAISS).
- Для каждого уникального контекста генерировать канонический ответ один раз и сохранять.
- Добавить в промпт инструкцию: «Если ты уже отвечал на этот запрос, повтори предыдущий ответ дословно».
- Ввести регрессионные тесты на повторяемость: 10 запусков одинакового входа — все ответы должны быть идентичны по смыслу (сравнение через LLM-as-judge).
Кейс 3: Доктор Хаус, который выдумывает больных — галлюцинации с вымышленными диагнозами
Проблема: ссылки на несуществующие исследования
Однажды модель выдала пациенту заключение: «Выявлен редкий синдром Старгардта-Фишера, описанный в работе Elias Thorne (2019)». Проблема в том, что ни такого синдрома, ни Elias Thorne не существует. Это типичный феномен «Элиаса Торна», когда LLM генерирует правдоподобные, но полностью вымышленные сущности. Мы подробно разбирали этот кейс в отдельной статье «Феномен 'Elias Thorne': как LLM создают вымышленных экспертов и почему это опасно».
В контексте медицины это смертельно опасно. Пациент мог начать лечить несуществующую болезнь, а реальная — прогрессировать.
Ошибка: мы использовали LLM с открытым доступом к общим знаниям, не ограничивая домен только проверенными источниками. Модель «фантазировала», потому что не имела жесткой привязки к авторитетной базе.
Решение: RAG с верификацией источника
Мы перешли на Retrieval-Augmented Generation: LLM отвечает, только опираясь на заранее загруженные документы (протоколы Минздрава, PubMed, UpToDate). Каждый сгенерированный факт сопровождается ссылкой на конкретный документ. Если модель не находит подтверждения в базе — она должна ответить «Недостаточно данных для заключения».
Дополнительно внедрили rejection sampling: после генерации запускается модель-верификатор (отдельная небольшая LLM, специально обученная на медицинских текстах), которая проверяет, все ли утверждения подкреплены источниками. Если нет — ответ отклоняется и отправляется на повторную генерацию с более строгим промптом.
Про построение такого пайплайна читайте в статье «Как построить семантический пайплайн для LLM: от ETL к итеративной обработке данных».
Пошаговый план исправления
- Собрать корпус авторитетных медицинских документов (желательно в формате Markdown/PDF с разметкой).
- Построить векторное хранилище (Pinecone, Qdrant, Weaviate) и настроить RAG-пайплайн.
- В промпт добавить инструкцию: «Отвечай ТОЛЬКО на основе предоставленных документов. Если информации нет — напиши 'Диагноз не может быть поставлен на основе имеющихся данных'. Никогда не выдумывай источники».
- Внедрить верификатор фактов (можно использовать ту же модель с промптом «Проверь, все ли утверждения имеют источник в документах»).
Кейс 4: Бот, который не знает, когда молчать — ложные срабатывания делегирования
Проблема: LLM берет на себя функции врача
Наша система задумывалась как вспомогательный инструмент: она должна подсвечивать аномалии, но окончательное решение оставляет врачу. Однако LLM в некоторых случаях начинала давать конкретные предписания: «Примите 500 мг цефтриаксона внутримышечно». Это уже назначение лекарства — без лицензии, без проверки противопоказаний, без учета аллергий. Врачи возмутились, юридический отдел — в шоке.
Проблема оказалась в том, что промпт не содержал четкого ограничения на действия. Модель пыталась быть «полезной» и давала максимально конкретный ответ. Мы не предусмотрели фильтра, который запрещает LLM делать то, что должно делать другое программное обеспечение (система поддержки принятия решений врача, CPOE).
Решение: Delegation Filter
Этот концепт мы разобрали в статье «Delegation Filter: когда НЕ использовать LLM в продакшн-пайплайнах (чек-лист от инженера)». Суть простая: перед тем как отдать управление LLM, мы проверяем, относится ли запрос к категориям, которые модель может обрабатывать (анализ, интерпретация, визуализация) или к тем, которые строго запрещены (назначение лечения, выписка рецептов). Для запрещенных категорий мы просто не передаем управление модели — ответ генерируется шаблонной фразой «Обратитесь к врачу для назначения терапии».
Фильтр реализован как пре-процессор на уровне API: проверяет тип запроса (classification + regex по ключевым словам «назначить», «дозировка», «препарат»). Если срабатывает — LLM не вызывается вообще.
DELEGATION_RULES = {
'prescribe': {'block': True, 'response': 'Обратитесь к врачу для назначения терапии.'},
'interpret': {'block': False, 'model': 'gpt-4o'},
'summarize': {'block': False, 'model': 'claude-4'}
}
def delegation_filter(request_type: str):
rule = DELEGATION_RULES.get(request_type)
if rule and rule['block']:
return {'action': 'block', 'message': rule['response']}
return {'action': 'proceed'}Пошаговый план исправления
- Составить матрицу компетенций: какие типы запросов разрешены, какие запрещены.
- Реализовать классификатор запросов (можно на легковесной модели типа DistilBERT или даже на правилах).
- Внедрить фильтр как отдельный микросервис перед вызовом LLM.
- Провести аудит всех возможных промптов: нет ли в них случайных разрешений на назначение лечения.
Ошибки, которые мы заметили слишком поздно (и как их избежать заранее)
- Переобучение на своих данных. Мы дообучали маленькую LLM на истории диагнозов одной больницы. В итоге модель стала выдавать результаты, смещенные в сторону практик именно этой клиники, игнорируя общепринятые протоколы. Решение: не дообучать на малых разнородных выборках, лучше использовать RAG.
- Утечка персональных данных через промпты. Логирование всех запросов для дебага привело к тому, что в логах оказались ФИО и номера полисов. Пришлось экстренно внедрять скремблирование на уровне entry-прокси. Подробнее о построении безопасного ETL — в статье «Как создать самовосстанавливающийся ETL-пайплайн на Python с помощью LLM».
- Cost inflation. RAG с верификатором удвоил расходы на токены. Мы оптимизировали: вместо отдельного верификатора используем «self-consistency» — генерируем 3 ответа и выбираем самый частотный. Это снизило cost на 40% без потери точности.