Обработка медицинских записей LLM: сравнение моделей, JSON/SQL вывод, OCR | AiManual
AiManual Logo Ai / Manual.
04 Янв 2026 Гайд

Медицинские записи в JSON за 15 минут: как заставить локальные LLM читать почерк врачей

Практический гайд по обработке медицинских записей локальными LLM. Сравнение моделей (Llama 3.2, Meditron), OCR для рукописного текста, структурирование в JSON/

Почему врачи пишут как куры лапой, а 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 с диагнозом, а получаете эссе на три абзаца о патогенезе.

💡
Начните с Llama 3.2 3B в GGUF формате. Она достаточно умна для структурирования, но достаточно мала, чтобы работать на ноутбуке. Если нужна максимальная точность - переходите на Meditron, но готовьтесь к танцам с бубном вокруг промптов.

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 можно потом трансформировать во что угодно.

💡
Начинайте с JSON. Он прощает ошибки структуры базы данных. Когда пайплайн отлажен - добавляйте генерацию SQL как дополнительный шаг. Но никогда не полагайтесь на SQL напрямую из LLM без валидации.

Полный пайплайн: от скана до структурированных данных

Теперь соберём всё вместе. Вот как выглядит рабочий процесс:

  1. Загрузка PDF/изображения
  2. Двойной OCR (Tesseract + медицинская коррекция)
  3. Валидация текста через маленькую LLM (исправление очевидных ошибок)
  4. Структурирование через медицинскую LLM в JSON
  5. Валидация JSON и исправление ошибок
  6. Преобразование в SQL (опционально)
  7. Загрузка в базу данных
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:

  1. Переходите на ISON вместо JSON - более устойчивый формат
  2. Используйте семантический пайплайн для итеративной обработки
  3. Разбейте документ на части и обрабатывайте отдельно
  4. Для совсем плохих сканов - ручной ввод с последующей LLM-обработкой

И главное - никогда не удаляйте оригиналы. Всегда храните сканы вместе со структурированными данными. Через месяц обнаружите ошибку в пайплайне - сможете переобработать.

Сколько это стоит на самом деле

Не верьте статьям про "обработка 1000 документов за 5 долларов". С медицинскими записями всё иначе:

  • Ollama с Llama 3.2 3B: бесплатно, но требует GPU или быстрого CPU
  • Meditron на облачном GPU: ~$0.5 за 1000 страниц
  • Ручная проверка 10% записей: 2-5 часов работы специалиста
  • Настройка и отладка: от 20 часов инженерного времени

Но когда пайплайн работает - он обрабатывает за день то, на что у человека ушла бы неделя. И делает это без усталости, без ошибок от невнимательности.

Самый дорогой этап - не обработка, а исправление ошибок. Выделите 30% времени на валидацию и тестирование. Сэкономите 300% времени на исправлениях потом.

Начните с маленькой тестовой выборки - 50 документов. Отладьте на них весь пайплайн. Убедитесь, что точность превышает 95% (для медицины меньше нельзя). И только потом масштабируйтесь.

И помните: даже самая умная LLM - всего лишь инструмент. Последнее слово всегда должно оставаться за врачом. Особенно когда на кону - человеческая жизнь.