Урок 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», другому — нет. Содержание в целом адекватное, но формат, длина и состав полей пляшут. Такое нельзя ни сравнивать между пациентами, ни разобрать кодом.
Диагноз простой: мы попросили «саммаризируй» и ни слова не сказали о том, что такое хорошая сводка. Модель угадывает — и каждый раз угадывает по-разному.
Главная беда наивного промпта на пяти разных медкартах — это…
Версия 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 или физиотерапии» — тоже неслучайна. Абстрактная категория без примера трактуется как угодно; два примера сразу задают границу.
Зачем оборачивать медкарту в теги <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 и собрали список дел врача на весь приёмный день. Промпт из инструмента «почитать» превратился в звено пайплайна.
Просим JSON в инструкции, но пример в промпте оставили текстовым. Что будет?
Упражнения
Упражнение 2.1 — Найти пропущенный слой
Коллега принёс промпт: системный промпт с ролью есть, карта обёрнута в <patient_record>, разделы перечислены, пример приложен. Но в половине ответов перед сводкой появляется абзац «Ниже приведена сводка, обратите внимание на почечную функцию…», и парсер падает. Какого слоя не хватает и как его добавить?
Решение упражненияСначала попробуйте сами — потом сверьтесь
Не хватает тегов вокруг вывода. Модель ничего не нарушает — её просто никто не просил отделить сводку от сопроводительного текста.
Чинится правкой примера: вместо «Your output:» ставим <summary> и </summary> вокруг образцовой сводки, а в финальной инструкции добавляем «Output your summary inside of <summary> tags». После этого предисловия можно не бояться: парсер берёт только содержимое тега.
Важный момент: борьба с предисловием через «не пиши ничего лишнего» работает хуже. Запрет модель может проигнорировать, а тег даёт устойчивую точку разреза даже если предисловие всё-таки появилось.
Упражнение 2.2 — Добавить раздел «Аллергии»
Врачи попросили добавить в сводку раздел с аллергиями и непереносимостью препаратов. Какие места промпта нужно тронуть — и что произойдёт, если тронуть только одно?
Решение упражненияСначала попробуйте сами — потом сверьтесь
Тронуть нужно два места, а в JSON-версии — три:
- список разделов в инструкции — добавить пункт про аллергии и указать позицию в порядке;
- образцовую сводку внутри
<example>— показать, как раздел выглядит; - в JSON-версии ещё и схему — новое поле, скажем
"allergies": [...].
Если поправить только инструкцию, модель будет вставлять раздел через раз и в разных местах: пример продолжит показывать сводку без него, и на противоречии модель начнёт колебаться. Если поправить только пример — раздел появится, но правил его наполнения не будет, и содержимое поплывёт.
Отдельно: в примере стоит показать и случай «аллергий в карте нет». Иначе на картах без этой информации модель либо выкинет раздел, либо начнёт додумывать — а выдуманная аллергия в медицинской сводке — это уже не косметическая проблема.
Что запомнить
- Промпт собирается слоями, и порядок неслучаен: роль → задача → данные в тегах → правила формата → примеры → теги вокруг вывода.
- Каждый слой закрывает конкретный сбой предыдущей версии. Добавляйте их по одному и смотрите, что изменилось.
- В системном промпте держите только контекст и роль. Инструкции по задаче — в пользовательском сообщении.
- Теги вокруг входных данных отделяют материал от инструкций, теги вокруг вывода дают надёжную точку разреза для парсера.
- Пример сильнее описания. Значит, при любой правке промпта пример нужно обновлять вместе с инструкцией — иначе они начнут спорить.
- Проверяйте промпт на непохожих входах. Один удачный прогон не говорит ни о чём.
Дочитали и сделали упражнения? Зафиксируйте прогресс — отметка сохранится в вашем браузере.