Пайплайн, который превращает свободный медицинский текст (анамнез, диагнозы, лекарства, аллергии, процедуры, family history, симптомы, vital signs) — в типизированные FHIR R4 ресурсы. Это наш конвертер «клинический нарратив → структурированный patient state», и заодно — единственная formal-ish граница в команде между тем, что попадает в Condition vs Observation vs прочие ресурсы.
Source-of-truth — engineering docs в репо: docs/NARRATIVE_TO_FHIR.md (общий пайплайн), docs/BG-1342-MIGRATION-PLAYBOOK.md (FHIR-шейпы per resource), packages/analysis-core/src/prompts/narrative_to_entities.md (правила извлечения). Эта страница — обзор и нюансы; за деталями шейпов и параметров запуска — туда.
Не для лаб-таблиц
Жёсткая граница: narrative-to-fhir не обрабатывает строки лабораторной таблицы. Те идут через другой пайплайн (services/image-recognition/ + save-fhir-preliminary.function.ts). Defense in depth — в коде стоит фильтр category !== "laboratory", и оркестратор логирует WARN если LLM попыталась вытащить лаб-row из narrative (drift промпта).
В observations[] сюда попадают только prose-mentioned показатели: BP в тексте «BP 122/77», BMI / вес / рост из клинической записи, imaging findings, physical exam, survey-шкалы (PHQ-9 и т.д.).
Три фазы пайплайна
NarrativeBlock[]
│
[1] extractEntitiesFromNarrative — multimodal LLM (текст + опционально base64-страницы PNG) →
│ typed JSON через Zod (ExtractedMedicalData)
↓
ExtractedMedicalData { conditions, medications, allergies, procedures, family_history, symptoms, observations }
│
[2a] SNOMED coding — quick-lookup table (~120 терминов) + LLM fallback на промахи
│
[2b] FHIR builders — отдельный билдер на тип:
Condition / MedicationStatement / AllergyIntolerance / Procedure /
FamilyMemberHistory / Observation (не-lab)
↓
FhirResourceContainer[] { resource_type, resource_id, fhir_json, snomed_code?, snomed_display? }
Полные сигнатуры функций, schema’ы, гранулярность retries — в engineering-doc’ах (ссылки в lede).
Концепт → FHIR resource: правила маппинга
Это правила, которые LLM применяет в Phase 1. Не все границы жёсткие — некоторые серые зоны разрешает сама модель (см. дальше). Базовая таблица:
| Концепт из narrative | FHIR resource | Категория / уточнение |
|---|---|---|
| Диагноз, хроническое заболевание, синдром (sepsis, DKA), problem-list (alcohol use disorder) | [[../domain/fhir-condition|Condition]] | clinical_status ∈ {active, resolved}, verification_status ∈ {confirmed, provisional, unconfirmed} |
| Препарат (текущий / прошлый / planned) | [[../domain/fhir-medication-statement|MedicationStatement]] | status ∈ {active, stopped, intended, unknown} |
| Аллергия / непереносимость | [[../domain/fhir-allergy-intolerance|AllergyIntolerance]] | category ∈ {food, medication, environment} |
| Хирургическая или диагностическая процедура (колоноскопия, эндоскопия) | [[../domain/fhir-procedure|Procedure]] | status = completed (для исторических); date обязательно если есть |
| Family history (родственники + их условия) | FamilyMemberHistory | relation через V3RoleCode (FTH/MTH/BRO/SIS/…) |
| Симптом (current complaint: fatigue, digestive issues, weight change) | [[../domain/fhir-observation|Observation]] | category = exam; value_string если qualitative |
| Vital signs (BP, BMI, HR, рост, вес из текста) | Observation | category = vital-signs |
| Imaging finding из прозы | Observation | category = imaging |
| Survey / risk score (PHQ-9, MOCA) | Observation | category = survey |
| Social history (smoking status, alcohol use по US Core) | Observation | category = social-history |
| Past illness, помеченное «resolved» | Condition (не Procedure) | clinical_status = resolved |
| Lab-значение, упомянутое в прозе (НЕ из таблицы) | Observation | category = laboratory — редкое исключение; lab-таблицы идут другим пайплайном |
Из этого видно главное — Observation это широкое ведро, и категория (vital-signs / social-history / exam / survey / imaging / laboratory) несёт ровно столько же смысла, сколько resource type. Поэтому пара (resource_type, category) — это минимальная единица маппинга, а не resource_type сам по себе.1
Серые зоны — где LLM решает
Condition vs Observation:social-history — формально обе трактовки могут быть легальны для одной и той же сущности. У нас жёсткого правила нет, и LLM в Phase 1 принимает решение по контексту (что чаще — это повод для разметки экспертом и закрепления правила).
Известные кандидаты на серую зону (Никита, обсуждение в треде1):
- Беременность. По US Core 6.1+ статус беременности из чек-листа / чата идёт в
Observationсcategory = social-history, а не вCondition. Но осложнения беременности (гестационный диабет, преэклампсия) — этоCondition. Граница тонкая. - Менопауза. Тоже легально и как
Condition(problem-list entry), и какObservation:social-history(физиологический статус). У нас неоднозначно. - Ожирение / underweight. Аналогично — может быть и Condition (clinical problem), и Observation:vital-signs (через BMI).
Что НЕ идёт в Condition (правило, которое в команде звучит чаще всего):
- Преаналитические факторы (fasting, recent meal, post-exercise) — это не состояние пациента, а контекст замера; не моделируются как Condition вообще
- Демографические факторы (age, sex как standalone) — уже кодируются в reference-range selection /
Patientресурсе - Transient behaviors, которые не попадают в структурированные записи
Где применяется
- Миграция narrative-полей из .NET — Stage 2 в активном
.NET → новая системаcutover (используетimageScope: "none", чистый текст). Подробности run-flow — в BG-1342-MIGRATION-PLAYBOOK. - Multimodal recognition в b2b-api / b2c-dashboard upload-flow — пайплайн вызывается на текстовых блоках + page-images recognition’а.
- Source-of-truth для разметки графа. Артур договорился размечать связи в biomarker-graph через FHIR resource type — вместо своих ad-hoc лейблов; теперь маппинг этой страницы (
Condition/Observation:vital-signs/Observation:social-history/ …) можно переиспользовать как канон.1
Открытые вопросы
- Хардкодить ли серые зоны (беременность → social-history; менопауза → ?), или продолжать давать LLM принимать решение по контексту. Trade-off: hard-rule даёт детерминизм, но фиксирует один из двух легальных вариантов на стороне FHIR; LLM-decides гибкое, но создаёт inconsistency между прогонами.
- Cross-app contract.
narrative-to-fhirсегодня живёт в@repo/analysis-coreи используется вb2b-apiupload-flow + миграции; если появится третий потребитель (расширенная b2c-форма / external import) — нужен публичный API или версия контракта. - Confidence-threshold для SNOMED. Сегодня coding с
confidence: 0.3проходит как есть. Для миграции стоит добавить cutoff — иначе в FHIR попадают мусорные коды. - Idempotency.
resource_id— UUID на каждый прогон; для рерана миграции нужны deterministic ids (хэш(patient + blocks)). Stage 2 в BG-1342 уже это делает; ad-hoc запуски — нет. - Расширенная карта маппинга — стоит, чтобы и Артур (разметчик графа), и LLM-пайплайны, и человеческие ревьюеры читали один артефакт. Эта таблица — кандидат на роль такого канона; альтернатива — отдельная страница для системного промпта
narrative_to_entities.md.
Связано
Концептуальные границы — на каждой FHIR resource page: fhir-condition, fhir-observation, fhir-medication-statement, fhir-allergy-intolerance, fhir-procedure. Категории внутри Observation/Condition — fhir-resource-categories.
Сшивки и решения: fhir-resource-origin-and-lifecycle — как помечать AI-generated vs user-uploaded; storing-biomarker-actuality — где Condition-like модель для actuality предлагается; biomarker-graph — куда уходит размеченный граф; medical-expert-loop — как Катя помогает закрывать серые зоны на конкретных кейсах; recognition — upstream-пайплайн, который кормит narrative-to-fhir текстовыми блоками.
- Engineering-doc’и в репо (см. lede) — source-of-truth для шейпов и параметров запуска.
Сноски
-
Slack thread в
#fhir(Артур + Никита + Ильдар, 2026-05-12) — https://realaicorp.slack.com/archives/C094G7UG82J/p1778507499494019?thread_ts=1778507499.494019&cid=C094G7UG82J — обсуждение «Conditionили нет» как границы разметки графа; нюансы US Core про беременность / менопаузу / ожирение; пара(resource_type, category)как минимальная единица маппинга; HH/LL критические значения out of scope (только N/H/L/A). ↩ ↩2 ↩3