Урок 2. Разбор: медицинский промпт с нуля

В первом уроке приёмы разбирались поодиночке. Теперь соберём из них один большой промпт для настоящей задачи — и, что важнее, увидим, как он собирается: слой за слоем, каждый следующий закрывает конкретную дыру предыдущего.

Задача: саммари медкарты

Нужен промпт, который превращает длинную историю болезни в короткую сводку для врача перед приёмом. Вот как выглядит входная запись (фрагмент сокращён, полная карта в оригинале длиннее):

Patient Name: Evelyn Thompson
Age: 78
Medical Record:

1985: Diagnosed with type 2 diabetes, started on metformin
1992: Developed hypertension, prescribed lisinopril
1998: Total hip replacement (right) due to osteoarthritis
2005: Admitted for atrial fibrillation, started on warfarin
2011: Admitted for transient ischemic attack (TIA), added aspirin to regimen
2013: Diagnosed with stage 2 breast cancer, underwent lumpectomy and radiation
2015: Developed chronic kidney disease (CKD) stage 3, metformin adjusted
2019: Mild cognitive impairment noted, started on donepezil
2022: Annual mammogram clear, but eGFR shows worsening kidney function
2023: Mobility declining, started physical therapy and home health aide visits
... (запись сокращена для урока)

Что должно быть в каждой сводке: имя, возраст, ключевые диагнозы, список лекарств, немедикаментозное лечение, недавние поводы для беспокойства и задачи для врача. Держим этот список перед глазами — именно он превратится в инструкции.

Тестировать промпт будем не на одной карте, а на пяти сразу — и карты специально берём непохожие: разный возраст, разный профиль заболеваний, разная длина записи. Разброс кейсов — главный детектор кривого промпта.

Версия 1: наивный промпт

Как большинство начинает:

I have this patient medical record. Can you summarize it for me?

{record}

I need this for a quick review before the patient's appointment tomorrow.

Что пошло не так. Прогоняем по пяти картам — и получаем пять разных документов. Где-то сплошной абзац текста, где-то маркированный список, где-то таблица. Набор разделов каждый раз свой: одному пациенту модель выписала «Family history», другому — нет. Содержание в целом адекватное, но формат, длина и состав полей пляшут. Такое нельзя ни сравнивать между пациентами, ни разобрать кодом.

Диагноз простой: мы попросили «саммаризируй» и ни слова не сказали о том, что такое хорошая сводка. Модель угадывает — и каждый раз угадывает по-разному.

Квиз 1

Главная беда наивного промпта на пяти разных медкартах — это…

Версия 2: роль в системном промпте

Первый слой — контекст и роль. Кладём их в системный промпт, и только их: по опыту Anthropic, в system стоит держать именно «кто ты и зачем», а не инструкции по задаче.

system = """
You are a highly experienced medical professional with a specialty in
translating complex patient histories into concise, actionable summaries.
Your role is to analyze patient records, identify critical information,
and present it in a clear, structured format that aids in diagnosis and
treatment planning. Your summaries are invaluable for busy healthcare
providers who need quick insights into a patient's medical history
before appointments.
"""

Стало заметно лучше по тону и отбору фактов: модель перестала пересказывать карту подряд и начала выделять клинически значимое. Но формат по-прежнему плавает — роль не заменяет ТЗ.

Версия 3: данные в тегах

В наивной версии медкарта просто болталась посреди текста, между «саммаризируй это» и «мне нужно к завтрашнему приёму». Модель вынуждена сама догадываться, где кончается инструкция и начинаются данные. Обозначим границу явно — XML-тегами:

updated_prompt = """
<patient_record>
{record}
</patient_record>
"""

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

Имя тега подбирайте осмысленное: <patient_record>, а не <data>. Тег — это ещё и подсказка о содержимом, бесплатный кусок контекста.

Версия 4: точные инструкции

Вместо «Can you summarize it for me?» — перечисляем ровно те разделы, которые хотим, и ровно в том порядке:

updated_prompt = """
I need your help summarizing patient medical records for our team of doctors.
We have a series of follow-up appointments tomorrow, and the doctors need
quick, insightful summaries to prepare.

Each summary should include the following elements in this order:
- The patient's name
- The patients age
- A bulleted list of key diagnoses in chronological order
- A bulleted list of medications the patient is prescribed
- A bulleted list of other treatments: non-medication treatments like CBT
  or physical therapy
- A short bulleted list of recent concerns
- A bulleted list of key action items to help our doctors prepare for the
  upcoming patient visit

<patient_record>
{record}
</patient_record>
"""

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

Формулировка «немедикаментозное лечение вроде CBT или физиотерапии» — тоже неслучайна. Абстрактная категория без примера трактуется как угодно; два примера сразу задают границу.

Квиз 2

Зачем оборачивать медкарту в теги <patient_record>?

Версия 5: пример

Инструкции описывают формат словами. Пример показывает его целиком — и работает сильнее любого описания, особенно когда структура сложная. Берём отдельную карту (пациент Ethan Blackwood, 55 лет) и рядом кладём образцовую сводку по ней. Всё это заворачиваем в <example>:

Here's an example of how we'd like the summaries formatted:

<example>
<patient_record>
Patient Name: Ethan Blackwood
Age: 55
Medical Record:

2010: Annual check-up, mild hypertension noted
2012: Diagnosed with moderate depression following job loss
  - Started on sertraline and cognitive-behavioral therapy (CBT)
2019: Diagnosed with obstructive sleep apnea (OSA)
  - Started CPAP therapy, reported improved energy levels
2024: Post-op knee recovery: good, continuing physical therapy
  - Started on low-dose ACE inhibitor for cardioprotection
... (карта сокращена для урока)
</patient_record>

Your output:

Name: Ethan Blackwood
Age: 55

Key Diagnoses:
- Hypertension (2010)
- Depression (2012)
- Obstructive Sleep Apnea (OSA) (2019)
- Left Ventricular Hypertrophy (LVH) (2024)

Medications:
- Sertraline (depression)
- Lisinopril (hypertension)
- Omeprazole (GERD) - discontinued in 2022
- Low-dose ACE inhibitor (cardioprotection - 2024)

Other Treatments:
- Cognitive Behavioral Therapy (CBT) (depression)
- Physical therapy (back pain, post-op knee recovery)
- CPAP therapy (OSA)

Recent Concerns:
- Worsening knee pain
- Elevated PSA (2023)

Action Items:
- Monitor PSA levels and prostate health
- Assess need for further cardiac workup after LVH finding
... (сводка сокращена)
</example>

Now, please summarize the following patient record in the same format:
<patient_record>
{record}
</patient_record>

Карта в примере сокращена, а сводка приведена целиком — поэтому часть строк в ней (лизиноприл, омепразол, PSA) опирается на вырезанные фрагменты записи. В настоящем промпте карта и сводка, разумеется, соответствуют друг другу строка в строку: расхождение между ними и есть та самая выдумка, которую мы ловим.

Здесь для краткости один пример, но обычно лучше два и больше: один образец модель склонна копировать буквально, включая случайные особенности. Смотрите и на мелочи в примере — например, «Omeprazole (GERD) — discontinued in 2022» показывает, что отменённые препараты не выбрасываются, а помечаются. Такое правило словами описывать долго, а примером — одна строка.

Версия 6: теги вокруг вывода

Модель может добавить к ответу вежливое «Конечно, вот сводка!» или примечание в конце. Человеку всё равно, парсеру — нет. Просим завернуть результат в <summary>. Причём просим не словами, а правкой примера — заменяем «Your output:» на теги:

<example>
<patient_record>
Patient Name: Ethan Blackwood
Age: 55
... (карта сокращена)
</patient_record>

<summary>
Name: Ethan Blackwood
Age: 55

Key Diagnoses:
- Hypertension (2010)
... (сводка сокращена)
</summary>
</example>

Теперь вытащить нужное — три строки кода, и предисловия модели ничего не ломают:

import re

match = re.search(r'<summary>\s*(.*?)\s*</summary>', model_response, re.DOTALL)
summary = match.group(1) if match else None

Прогоняем финальную версию по всем пяти картам — и все пять сводок одинаковы по структуре: те же разделы, тот же порядок, те же маркеры. Ровно то, чего не было в версии 1.

Версия 7: JSON вместо текста

Текстовая сводка хороша для чтения глазами. Но если сводки идут дальше в код — в базу, в дашборд, в список задач на день, — удобнее JSON. Меняем две вещи: описываем нужную схему прямо в промпте и переписываем пример под JSON.

Please provide these summaries in JSON format with the following structure:
{
  "name": "Patient's full name",
  "age": patient's age as an integer,
  "key_diagnoses": [
    {"diagnosis": "Primary diagnosis", "year": year of diagnosis as an integer}, ...
  ],
  "medications": [
    {"name": "Medication name", "purpose": "Brief description of what it's for"}, ...
  ],
  "other_treatments": [
    {"treatment": "Treatment name", "purpose": "Brief description of what it's for"}, ...
  ],
  "recent_concerns": ["Brief statement of recent health issue or concern"],
  "action_items": ["Action item 1", "Action item 2", ...]
}

Now, please summarize the following patient record in the same format.
Output your JSON summary inside of <summary> tags

Пример в промпте тоже переезжает в JSON — иначе модель получит противоречивый сигнал: инструкция просит JSON, а образец показывает текст. Пример всегда сильнее описания, поэтому рассинхрон между ними — типичный источник загадочных багов.

И один практический момент из кода: фигурные скобки схемы ломают Python-метод .format(). Поэтому вставку самой медкарты выносят в отдельную переменную и просто склеивают строки:

medical_record_input_prompt = """
<patient_record>
{record}
</patient_record>
"""

complete_prompt = updated_json_prompt + medical_record_input_prompt.format(record=patient_record)

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=4096,
    system=system,
    messages=[{"role": "user", "content": complete_prompt}]
)

Дальше — обычный код: достали содержимое <summary>, прогнали через json.loads(), взяли поле action_items и собрали список дел врача на весь приёмный день. Промпт из инструмента «почитать» превратился в звено пайплайна.

Надёжнее всего структурированный вывод получается не промптом, а через tool use — там схема задаётся формально и валидируется. Здесь JSON через промпт нужен, чтобы показать, как сильно правка промпта меняет форму ответа.
Квиз 3

Просим JSON в инструкции, но пример в промпте оставили текстовым. Что будет?

Упражнения

Упражнение 2.1 — Найти пропущенный слой

Коллега принёс промпт: системный промпт с ролью есть, карта обёрнута в <patient_record>, разделы перечислены, пример приложен. Но в половине ответов перед сводкой появляется абзац «Ниже приведена сводка, обратите внимание на почечную функцию…», и парсер падает. Какого слоя не хватает и как его добавить?

Решение упражненияСначала попробуйте сами — потом сверьтесь

Не хватает тегов вокруг вывода. Модель ничего не нарушает — её просто никто не просил отделить сводку от сопроводительного текста.

Чинится правкой примера: вместо «Your output:» ставим <summary> и </summary> вокруг образцовой сводки, а в финальной инструкции добавляем «Output your summary inside of <summary> tags». После этого предисловия можно не бояться: парсер берёт только содержимое тега.

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

Упражнение 2.2 — Добавить раздел «Аллергии»

Врачи попросили добавить в сводку раздел с аллергиями и непереносимостью препаратов. Какие места промпта нужно тронуть — и что произойдёт, если тронуть только одно?

Решение упражненияСначала попробуйте сами — потом сверьтесь

Тронуть нужно два места, а в JSON-версии — три:

  • список разделов в инструкции — добавить пункт про аллергии и указать позицию в порядке;
  • образцовую сводку внутри <example> — показать, как раздел выглядит;
  • в JSON-версии ещё и схему — новое поле, скажем "allergies": [...].

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

Отдельно: в примере стоит показать и случай «аллергий в карте нет». Иначе на картах без этой информации модель либо выкинет раздел, либо начнёт додумывать — а выдуманная аллергия в медицинской сводке — это уже не косметическая проблема.

Что запомнить

  • Промпт собирается слоями, и порядок неслучаен: роль → задача → данные в тегах → правила формата → примеры → теги вокруг вывода.
  • Каждый слой закрывает конкретный сбой предыдущей версии. Добавляйте их по одному и смотрите, что изменилось.
  • В системном промпте держите только контекст и роль. Инструкции по задаче — в пользовательском сообщении.
  • Теги вокруг входных данных отделяют материал от инструкций, теги вокруг вывода дают надёжную точку разреза для парсера.
  • Пример сильнее описания. Значит, при любой правке промпта пример нужно обновлять вместе с инструкцией — иначе они начнут спорить.
  • Проверяйте промпт на непохожих входах. Один удачный прогон не говорит ни о чём.

Дочитали и сделали упражнения? Зафиксируйте прогресс — отметка сохранится в вашем браузере.

Перевод и адаптация урока «Medical Prompt Walkthrough» курса Real World Prompting © Anthropic, лицензия CC BY-NC 4.0. Перевод: Дарья Воронкина (@aishipuchka). Материал изменён: переведён на русский, медицинские записи сокращены до показательных фрагментов, названия моделей актуализированы, добавлены квизы и упражнения. Используется некоммерчески.