Почему врачи пишут как куры лапой, а LLM должны это читать
Представьте: у вас 5000 отсканированных медицинских карт. Половина - PDF с кривым OCR, другая половина - фотографии рукописных назначений, где слово "аспирин" похоже на "аспирант". База данных ждёт структурированных записей. Вручную - месяц работы. С облачными AI - нарушение закона о персональных данных. Выход один - локальные LLM.
Но здесь начинается ад. Модель путает дозировки. Игнорирует даты. Превращает "принимать 3 раза в день" в "принимать 300 раз в день". А ещё эти бесконечные форматы вывода - то JSON сломается, то SQL-запрос невалидный.
Главная ошибка новичков: пытаться заставить обычную Llama читать медицинские тексты без дообучения. Она начнёт галлюцинировать диагнозы, которые не существуют в природе.
Какая модель не наговорит вам лишнего о здоровье
Выбор модели - это не про "какая лучше". Это про "какая меньше навредит". Медицинские данные требуют точности, а не креатива.
| Модель | Размер | Медицинское обучение | Точность в тестах | Мой вердикт |
|---|---|---|---|---|
| Meditron-7B | 7B параметров | Специализированная | 85% на медицинских QA | Лучше для диагностики, хуже для структурирования |
| Llama 3.2 3B | 3B параметров | Общая | 72% | Быстрее, но требует чётких промптов |
| Gemma 2 9B | 9B параметров | Общая | 78% | Хороший баланс, но жрёт память |
| Qwen2.5 7B | 7B параметров | Смешанная | 81% | Отличное понимание контекста |
Meditron создавалась именно для медицины. Она знает разницу между "IBS" (синдром раздражённого кишечника) и "IBD" (воспалительное заболевание кишечника). Обычная Llama может решить, что это опечатка.
Но есть проблема: Meditron часто даёт развёрнутые объяснения, когда вам нужна просто структура. Вы просите JSON с диагнозом, а получаете эссе на три абзаца о патогенезе.
OCR для почерка, который не читает даже сам врач
Стандартный Tesseract с медицинскими записями справляется так же хорошо, как я с нейрохирургией. Нужен особый подход.
1 Двойной проход OCR
Сначала - общий текст. Потом - специально обученная модель для медицинских аббревиатур. Почему? Потому что "q.d." (quaque die - каждый день) Tesseract прочитает как "qd" или вообще пропустит.
import pytesseract
from PIL import Image
import re
# Первый проход - обычный OCR
def extract_text_first_pass(image_path):
image = Image.open(image_path)
text = pytesseract.image_to_string(image, lang='rus+eng')
return text
# Второй проход - поиск медицинских сокращений
def medical_ocr_correction(text):
medical_abbreviations = {
'qd': 'quaque die',
'bid': 'bis in die',
'tid': 'ter in die',
'qid': 'quater in die',
'po': 'per os',
'prn': 'pro re nata'
}
for abbr, full in medical_abbreviations.items():
text = re.sub(rf'\b{abbr}\b', full, text, flags=re.IGNORECASE)
return text
# Использование
text = extract_text_first_pass('medical_note.jpg')
corrected_text = medical_ocr_correction(text)
2 EasyOCR для особо сложных случаев
Когда Tesseract сдаётся, пробуйте EasyOCR с предобученными весами. Он лучше справляется с кривыми строчками и низким качеством сканов.
import easyocr
reader = easyocr.Reader(['ru', 'en'], gpu=False) # GPU=False для CPU
result = reader.readtext('handwritten_prescription.jpg', detail=0)
text = ' '.join(result)
# EasyOCR возвращает список строк, соединяем их
Никогда не доверяйте OCR на 100%. Всегда добавляйте этап валидации через LLM. Модель может заметить "Принимать 1000 мг каждые 5 минут" и понять, что это ошибка распознавания.
JSON или SQL: битва форматов вывода
Здесь начинается самое интересное. Вы извлекли текст. Теперь его нужно структурировать. Два основных подхода:
- JSON - для хранения, API, дальнейшей обработки
- SQL - для немедленной загрузки в базу данных
JSON кажется очевидным выбором. Но есть нюанс: LLM часто генерируют невалидный JSON. Пропущенные кавычки, лишние запятые, незакрытые скобки.
Промпт для идеального JSON
Не просите просто "верни JSON". Это гарантия ошибки. Нужно жёстко контролировать формат.
json_prompt = """
Извлеки информацию из медицинской записи ниже.
Требования:
1. ВСЕГДА возвращай ВАЛИДНЫЙ JSON
2. Используй ТОЛЬКО эту структуру:
{
"patient_id": "строка или null",
"date": "YYYY-MM-DD или null",
"diagnosis": ["список строк"],
"medications": [
{
"name": "строка",
"dosage": "строка",
"frequency": "строка"
}
],
"procedures": ["список строк"],
"notes": "строка или null"
}
3. Если информация отсутствует - используй null для строк и [] для массивов
4. НИКАКИХ дополнительных полей
5. НИКАКИХ комментариев вне JSON
Запись:
{text}
"""
Этот промпт работает в 90% случаев. Но в оставшихся 10% модель всё равно накосячит. Поэтому нужен валидатор:
import json
import re
def extract_and_validate_json(llm_response):
# Пытаемся найти JSON в ответе
json_match = re.search(r'\{.*\}', llm_response, re.DOTALL)
if not json_match:
raise ValueError("JSON не найден в ответе модели")
json_str = json_match.group()
try:
data = json.loads(json_str)
return data
except json.JSONDecodeError as e:
# Пытаемся починить распространённые ошибки
fixed_json = fix_common_json_errors(json_str)
try:
return json.loads(fixed_json)
except:
raise ValueError(f"Не удалось исправить JSON: {e}")
def fix_common_json_errors(json_str):
# Убирает лишние запятые в конце массивов/объектов
json_str = re.sub(r',\s*\}', '}', json_str)
json_str = re.sub(r',\s*\]', ']', json_str)
# Исправляет незакрытые кавычки
json_str = re.sub(r'(?
А что с SQL?
SQL кажется более прямолинейным. Но здесь свои подводные камни. Модель должна знать структуру вашей базы данных.
sql_prompt = """
Преобразуй медицинскую запись в SQL INSERT запросы.
Структура таблиц:
1. Таблица patients: id, name, birth_date
2. Таблица visits: id, patient_id, visit_date, doctor_id
3. Таблица diagnoses: id, visit_id, diagnosis_code, description
4. Таблица prescriptions: id, visit_id, medication_name, dosage, frequency
Правила:
1. Если пациента нет в базе - сначала INSERT в patients
2. Каждый визит - отдельный INSERT в visits
3. Используй существующие ID там, где они известны
4. Все даты в формате 'YYYY-MM-DD'
5. ТОЛЬКО SQL, без объяснений
Запись:
{text}
"""
Проблема SQL подхода в том, что он менее гибкий. Если структура базы изменится, нужно переписывать промпты. JSON можно потом трансформировать во что угодно.
Полный пайплайн: от скана до структурированных данных
Теперь соберём всё вместе. Вот как выглядит рабочий процесс:
- Загрузка PDF/изображения
- Двойной OCR (Tesseract + медицинская коррекция)
- Валидация текста через маленькую LLM (исправление очевидных ошибок)
- Структурирование через медицинскую LLM в JSON
- Валидация JSON и исправление ошибок
- Преобразование в SQL (опционально)
- Загрузка в базу данных
from pathlib import Path
import sqlite3
class MedicalRecordProcessor:
def __init__(self, llm_client, db_path='medical.db'):
self.llm = llm_client
self.conn = sqlite3.connect(db_path)
def process_file(self, file_path):
# 1. OCR
text = self.extract_text(file_path)
# 2. Валидация через LLM
validated_text = self.validate_with_llm(text)
# 3. Структурирование в JSON
json_data = self.extract_to_json(validated_text)
# 4. Сохранение
self.save_to_db(json_data)
return json_data
def extract_text(self, file_path):
# Реализация OCR с коррекцией
pass
def validate_with_llm(self, text):
prompt = f"""Исправь очевидные ошибки в медицинской записи:
{text}
Ищи:
1. Нереальные дозировки (например, '1000 мг каждые 5 минут')
2. Опечатки в названиях лекарств
3. Неправильные даты
4. Противоречивые инструкции
"""
return self.llm.generate(prompt)
def extract_to_json(self, text):
# Используем промпт для JSON
json_prompt = self.create_json_prompt(text)
response = self.llm.generate(json_prompt)
return extract_and_validate_json(response)
def save_to_db(self, json_data):
# Преобразование JSON в SQL и выполнение
pass
Ошибки, которые сломают вашу систему
Я видел десятки падений медицинских пайплайнов. Вот самые частые причины:
- Доверие без проверки: LLM сказала "дозировка: 500 мг" - значит, так и есть. На самом деле в оригинале могло быть "50 мг".
- Игнорирование контекста: "Аспирин 100 мг" без указания "после еды" или "при болях".
- Смешение пациентов: Когда в одном PDF записи нескольких людей, а модель не разделяет их.
- Потеря отрицаний: "Не принимать аспирин" превращается в "Принимать аспирин".
Защита простая, но её постоянно забывают:
def safety_check(json_data):
"""Проверка на опасные значения"""
warnings = []
for med in json_data.get('medications', []):
# Проверка дозировок
dosage = med.get('dosage', '')
if '1000' in dosage and 'каждые' in dosage and 'час' in dosage:
warnings.append(f"Подозрительно высокая частота: {med['name']}")
# Проверка взаимодействий (упрощённая)
dangerous_combinations = [
('warfarin', 'aspirin'),
('simvastatin', 'clarithromycin')
]
med_names = [m['name'].lower() for m in json_data.get('medications', [])]
for combo in dangerous_combinations:
if all(drug in med_names for drug in combo):
warnings.append(f"Опасное сочетание: {combo[0]} + {combo[1]}")
return warnings
Что делать, когда ничего не работает
Бывают случаи, когда стандартный подход не срабатывает. Рукопись нечитаема. Модель галлюцинирует. JSON ломается на каждом шагу.
Мой emergency plan:
- Переходите на ISON вместо JSON - более устойчивый формат
- Используйте семантический пайплайн для итеративной обработки
- Разбейте документ на части и обрабатывайте отдельно
- Для совсем плохих сканов - ручной ввод с последующей LLM-обработкой
И главное - никогда не удаляйте оригиналы. Всегда храните сканы вместе со структурированными данными. Через месяц обнаружите ошибку в пайплайне - сможете переобработать.
Сколько это стоит на самом деле
Не верьте статьям про "обработка 1000 документов за 5 долларов". С медицинскими записями всё иначе:
- Ollama с Llama 3.2 3B: бесплатно, но требует GPU или быстрого CPU
- Meditron на облачном GPU: ~$0.5 за 1000 страниц
- Ручная проверка 10% записей: 2-5 часов работы специалиста
- Настройка и отладка: от 20 часов инженерного времени
Но когда пайплайн работает - он обрабатывает за день то, на что у человека ушла бы неделя. И делает это без усталости, без ошибок от невнимательности.
Самый дорогой этап - не обработка, а исправление ошибок. Выделите 30% времени на валидацию и тестирование. Сэкономите 300% времени на исправлениях потом.
Начните с маленькой тестовой выборки - 50 документов. Отладьте на них весь пайплайн. Убедитесь, что точность превышает 95% (для медицины меньше нельзя). И только потом масштабируйтесь.
И помните: даже самая умная LLM - всего лишь инструмент. Последнее слово всегда должно оставаться за врачом. Особенно когда на кону - человеческая жизнь.