Per-biomarker AI-цепочка поверх FHIR-данных пациента: на каждый биомаркер выдаёт BiomarkerAnalysis — record со structured evidence, clinical interpretation для врача и personalized prose для пациента. Это Phase 2 внутри Health Report Pipeline; Phase 1 (Snapshot Building) уже выполнил Diagnostician + Retriever один проход patient-wide, и Phase 2 переиспользует этот пакет — собственный прогон Diag+Retr внутри этой стадии не делается.
Single fork point — Personalizer
| Biomarker | Цепочка | Rationale |
|---|---|---|
| Abnormal | Diag → Retr → Reasoner → DoctorWriter → Personalizer | Полная интерпретация для пациента; Personalizer персонализирует connections |
| Normal | Diag → Retr → Reasoner → DoctorWriter | Connections для нормалов скрыты в UI → Personalizer-вызов был бы wasted; в остальном симметрично с abnormal |
Канон зафиксирован на normal-biomarker-pipeline-coverage (active, 2026-05-18). Архитектурно это single fork point — Personalizer запускается if-and-only-if biomarker abnormal. Все остальные стадии работают одинаково для обеих категорий.
Прежний short-cut Generator-only-для-нормалов (cost economy on 30-100 параметров) — отброшен1; причина — actuality-классификатору нужен retrieved симметрично для нормалов и аномалов («ему неважно норма / абнорма» — он смотрит graph-связи), а для этого нужен полный Diag+Retr+Reasoner на обоих ветках.
Стадии pipeline’а
для каждого биомаркера (норма ИЛИ аномалия):
[1] Diagnostician (из Phase 1) → DiagnosticPlan
triage + evidenceTier + related_* списки имён
читает [[biomarker-graph]]
— детали [[diagnostician]]
[2] Retriever (из Phase 1) → GenerationReadyPackage
relatedFound* (template ∩ patient FHIR)
relatedMissing* (template \ patient)
— детали [[retriever]]
[3] Reasoner → BiomarkerInference
synthesizes: triage · concurrentLabContext ·
inContextStatus.explainingFactors · mechanisticChain ·
dataGaps · clarifyingDataItems · triageRationale
[4] Evidence builder (deterministic) → 4 секции evidence items
foundContext[] / missingContext[] / referenceContext[] / additionalWorkup[]
каждый item: { name, type, rationale, quotes, source }
[5] DoctorWriter → clinicalInterpretation (2 абзаца)
+ reasoning (FDA logic trace, INTERNAL)
+ perItemPersonalizedRationale[] per evidence item
─── ↓ Только если biomarker abnormal: ───
[6] Personalizer → whatAdditionalDataWouldClarify
(1-3 абзаца patient-prose)
─── ↓ всегда: ───
[7] FHIR serializer (deterministic) → ClinicalImpression + DetectedIssue + Provenance
+ References на existing patient bag
DoctorWriter и Personalizer формально могут идти параллельно — оба читают BiomarkerInference независимо. FHIR serializer — строго после всех LLM-стадий. На каждый биомаркер пациента 3 новых FHIR-ресурса (CI + DetectedIssue per gap + Provenance).
Output — BiomarkerAnalysis record
Это финальный record per биомаркер. Поля группируются по «что они описывают»:
Приоритет и происхождение (структурные сигналы, мини-метаданные):
triage— enum приоритета (норма / watch / urgent / critical). Источник: Diagnostician, passthrough через Reasoner.evidenceTier—algorithm/domain_inference/model_knowledge. Уровень доверия к graph-выводу: prescriptive guideline-правило, ego-сеть домена в графе, или эрудиция LLM. Источник: Diagnostician.triageRationale— текстовое объяснение «почему такой triage». Источник: Reasoner.
Что найдено вокруг этого биомаркера (4 evidence-секции — структурные списки с цитатами):
foundContext[]— связи подтверждены в FHIR пациента (этот биомаркер + это condition / medication / другой биомаркер встречаются у пациента вместе).missingContext[]— связи, которые ожидались по графу, но в FHIR не нашлись («нет данных»).referenceContext[]— клинические пороги и нормы для этого биомаркера.additionalWorkup[]— тесты, которые стоит назначить на основе текущей картины.
Каждый item внутри — { name, type, rationale, quotes, source }; type: "observation" | "condition" | "medication". FHIR-mapping этих секций — foundcontext-fhir-mapping, кодирование роли связи — edge-role-encoding. Producer: EvidenceBuilder (детерминистический, не LLM — собирает items из Retriever-package + графа).
Reasoner-синтез (что биомаркер означает в контексте этого пациента):
concurrentLabContext[]— другие labs у пациента, релевантные этому биомаркеру, со status + implicationFact.inContextStatus.explainingFactors[]— какие conditions/medications пациента объясняют текущее значение.mechanisticChain[]— driver→effect causal-цепь (LLM строит механизм). INTERNAL, попадает в audit-Provenance, не в patient-facing UI.dataGaps[]— какой longitudinal или demographic-context отсутствует у пациента.clarifyingDataItems[]— что нужно ещё узнать, чтобы дальше различить варианты.
Текст для людей (prose-стадии):
clinicalInterpretation— 2 абзаца doctor-prose. Источник: DoctorWriter.reasoning— FDA logic trace (INTERNAL, audit). Источник: DoctorWriter.foundContext[i].personalizedRationale— 2-4 предложения per evidence item, пишутся DoctorWriter’ом per-item.whatAdditionalDataWouldClarify— 1-3 абзаца patient-prose про gaps. Источник: Personalizer (только для abnormal).
См. types/biomarker-analysis.types.ts:205 (BiomarkerAnalysis interface) для actual schema2.
Два класса полей по источнику знания
BiomarkerInference фильтруется по тому, откуда берётся факт:
- «На основе данных» (structured-derived) —
concurrentLabContext[].status(из observation value + referenceRange),inContextStatus.explainingFactors[](изactiveConditions[]),dataGaps[].gapType(изrelatedMissing[]). Тестируется deterministic-фикстурами. - «На основе знаний» (LLM-reasoning) —
triage/triageRationale,mechanisticChain[],implicationFactтексты,whatItWouldDistinguish. Tacit-knowledge модели. Тестируется golden-датасетами с врачом-судьёй (medical-expert-loop).
Разделение полезно для eval’ов — деривируемые поля проверяются механически, knowledge-поля требуют ручной верификации.
Found vs Hypothetical — асимметрия рёбер
Концептуальная основа — hypothesis-and-check разделение труда между Диагностиком и Ретривером (детали — retriever § «Концептуально» и diagnostician § «Концептуально — связи между сущностями пациента»). Здесь — что это значит для output-семантики pipeline’а.
После Retriever’а у нас фактически два графа связей с разной семантикой и разным FHIR-mapping’ом:
- Found graph — связь подтверждена в FHIR пациента (Диагностик предложил → Ретривер нашёл узел). Аннотация: «опиши роль ребра и механизм».
- Hypothetical graph — связь предсказана графом, но не подтверждена в FHIR (
inferred_absent— косвенно вывели «вероятно нет», илиunknown). Аннотация: «опиши state и пути закрытия».
Это две разные оси рёбер, не одна типология. Смешивать их в один enum — терять различие между «observation о реальном ребре» и «hypothesis о missing ребре». FHIR-маппинг разный (см. fhir-mapping-conventions); конкретный пример — fhir-mapping-walkthrough.
Контекст-инжекшн — два режима «нахождения»
Medical context собирается не одним блоком. Работают два режима, оба используются в одном проходе:
- Структурный поиск (graph-lookup) — Диагностик идёт в biomarker-graph через детерминированный matching, Ретривер сматчивает template с FHIR строковым join’ом. Stochastic-зоны нет — нашёл / не нашёл.
- Модельное сопоставление (LLM-matching) — Reasoner читает полный
activeConditions[]/activeMedications[](не template-фильтр) и LLM сам решает «что объясняет значение». Stochastic — зависит от prompt-а и контекста.
Контраст с single-prompt-analysis: там весь patient context инжектится inline до начала анализа, без структурной этапизации. Здесь structural retrieval делает Retriever, а LLM-judgment накладывает Reasoner поверх обоих типов связей.
Temporal scope — что pipeline видит во времени
| Слой | Что использует | Видит историю? |
|---|---|---|
| Single-biomarker analysis | Текущее значение + diagnostic plan | Нет |
| Trend analysis | Тот же биомаркер — текущее + предыдущие значения | Да, только этого биомаркера |
| Panel overview | Биомаркеры внутри этой панели | Нет |
| Test overview | Все параметры теста, опционально межпанельные связи | Зависит от карты связей |
Pipeline сегодня использует историю узко — только в trend analysis на конкретный биомаркер («было vs стало»). Синхрония (соседние биомаркеры в один момент) и cross-temporal (история одного в контексте другого) — не учитываются. Алгоритм с межпанельными связями разработан (Apr 13 2026), не задеплоен.
Технический access Retriever-а ко всему vs реальное использование. Retriever-tools (get_observations / get_conditions / get_medications) могут возвращать всю историю пациента, но что попадает в финальный контекст определяется Diagnostic plan; исторические наблюдения технически могут попасть в LLM-контекст, но явно для вывода не используются.
Где живёт в коде
| Артефакт | Расположение |
|---|---|
| Diagnostician | packages/analysis-core/src/agents/biomarker-analysis/diagnostician.{mastra,raw}.ts (diagnostician) |
| Retriever | packages/analysis-core/src/agents/biomarker-analysis/retriever.mastra.ts + code-retriever/ (retriever) |
| Reasoner | packages/analysis-core/src/agents/biomarker-analysis/reasoner.fn.ts |
| Evidence builder | packages/analysis-core/src/lib/retriever-postprocess.ts (входит в Retriever post-process) + Reasoner-output transforms |
| DoctorWriter | agents/biomarker-analysis/writer-doctor.fn.ts |
| Personalizer | agents/biomarker-analysis/personalizer.{mastra}.ts |
| FHIR serializer | lib/fhir-clinical-impression-builder.ts + смежные builders |
| Сервис | services/biomarker-analysis.service.ts |
| Types | types/biomarker-analysis.types.ts (BiomarkerAnalysis, EvidenceItemOutput); types/knowledge.types.ts (BiomarkerInference) |
| Graph (input для Диагностика) | data/biomarker_graph.json (biomarker-graph) |
| Promptы | prompts/biomarker-analysis/{diagnostician,retriever,code-retriever,reasoner,writer-doctor,writer-patient,personalizer}.md |
| Branch | feat/actuality-mvp-dirty (текущий канон после Phase A-G renames); исторический snapshot feat/v2-5-on-staging (BG-1323) |
Свойства
- Granular debug — каждая стадия проверяется отдельно (Reasoner output →
BiomarkerInference; Writer/Personalizer — отдельные prose-вызовы) - Audience-split в архитектуре — DoctorWriter и Personalizer как отдельные стадии, не через style-hint в одном промпте
- Structured evidence — foundContext items с quotes из guidelines прикреплены к конкретному выводу
- Dynamic retrieval — Reasoner сам выбирает subset patient-data через tool-calls
- Audit trail — каждая стадия отдельная Provenance, multi-agent chain виден в FHIR (fhir-provenance)
- Higher cost / latency — несколько LLM round-trip’ов; baseline ~6-7 минут end-to-end на staging
- Сложнее эволюционировать prompt’ы — несколько мест изменений вместо одного
Failure modes
- Retriever дампит всё в cache + не справляется с
finish_retrieval— fallback копирует все conditions/medications во все биомаркеры (noise + потенциально ложные связи)3. Verify нужен после Apr 16 правок Артура. - Single-LLM matcher для graph↔FHIR имён — один LLM-вызов решает судьбу evidence по conditions/medications всех биомаркеров; failure modes: пропуск item’а молча, swap полей, transport-fail с silent skip3. Кандидат на детерминистический matcher.
- Reasoning planner vs algorithm-text — раньше в генерацию передавался internal reasoning планировщика вместо клинических обоснований из алгоритмов4. Apr 9 Артур правил, verify нужен.
- Agentic loop застревает — пустой
finish_retrieval({}),MAX_STEPS=8у Retriever’а; sequential-mode Gemini у Диагностика на больших пациентах (Failure modes); doom-loop / MAX_TOKENS / hang у Gemini structured-output вообще (gemini-doom-loop). - Telemetry gap —
biomarker-analysisLLM-вызовы не имеютexperimental_telemetry: { isEnabled: true }в Vercel AI SDK → Langfuse trace показывает только legacy Generator path (bifrost). - Backward-compat — старые
Observation.note[]записи single-prompt’а не имеют structured evidence; читатели должны fall back graceful. - mechanisticChain — ground-truth quality — driver/effect biology генерируется LLM без verifier; eval pipeline в работе.
Открытые вопросы
- Retriever filter по diagnostic plan — на уровне tools (filter перед return) или post-process? Сегодня полагаемся на LLM
finish_retrieval— как это работает на проде, open. - Replacement single-LLM-matcher на детерминистический — explicit mapping таблица / vector similarity / structured tool call.
- Schema substrate в FHIR — где сохранять
BiomarkerInferenceкак промежуточный артефакт (ClinicalImpression.summary/Observation.note[]aggregation / 3-resource pattern). Пересекается с foundcontext-fhir-mapping, fhir-provenance, fhir-modeling-ai-content. - Input scope для generation — что pipeline берёт как substrate для snapshot: только новые данные / последний DiagnosticReport / вся история / observation-level без DR-границ. Пересекается с unified-upload-flow и audience matrix. См. health-report-vision.
- Latency оптимизация — thinking_level / model selection (Gemini 3 flash vs pro), reasoning-rewriter в bifrost. Не settled.
Связано
- health-report-pipeline — outer orchestrator (4 фазы; этот pipeline = Phase 2)
- diagnostician / retriever / biomarker-graph — entity-страницы про шаги 1-2 и их input
- biomarker-actuality-service — соседний pipeline (валидность наблюдения как gate перед biomarker-анализом)
- single-prompt-analysis — параллельный подход (один LLM-вызов, flat output), legacy
- staged-output-fhir-storage — куда output ложится в FHIR (host / granularity / distribution)
- fhir-mapping-conventions — правила маппинга output’а в FHIR (Found→CI / Hypothetical→DetectedIssue)
- fhir-mapping-walkthrough — конкретный кейс HbA1c-31086 через все 7 стадий с mermaid-диаграммами
- foundcontext-fhir-mapping — куда конкретно идут evidence items
- edge-role-encoding — кодирование роли связи на edges
- doctor-patient-prompts-not-fork — почему Writer-doctor и Personalizer-patient разные стадии
- health-facts-as-generation-substrate — Variant D: extract dry medical instruction → writer-промпты per audience — active
- fhir-clinical-impression / fhir-provenance — FHIR-ресурсы где output живёт
- medical-expert-loop — как «на основе знаний» поля калибруются с врачом
- mastra / bifrost — execution engine и LLM proxy
- gemini-doom-loop — repetition/MAX_TOKENS/hang failures, влияющие на agentic-стадии
Сноски
-
Прежнее решение (отброшено). 2026-04-02 Ильдар + 2026-04-23 Артур: cost-driven компромисс — full chain только для аномальных, нормалы через Generator-only single-call. Отменено в 2026-05-18 на сцепке с actuality-классификатором — нужен
retrievedсимметрично для всех биомаркеров, см. normal-biomarker-pipeline-coverage. Slack thread исходного решения: https://realaicorp.slack.com/archives/C094GTAU8HK/p1775154642254939 ↩ -
packages/analysis-core/src/types/biomarker-analysis.types.ts:195—EvidenceItemOutputschema,BiomarkerAnalysisrecord (бывш.V2EvidenceItemOutput/V2SingleParameterAnalysisдо Phase A rename);types/knowledge.types.ts—BiomarkerInference. ↩ -
2026-04-16 thread Артура в
#ai-engineering— детальный разбор Retriever issues (cache-dump fallback, single-LLM matcher, single point of failure для condition/medication matching). https://realaicorp.slack.com/archives/C094GRT3CBY/p1776356279568619, https://realaicorp.slack.com/archives/C094GRT3CBY/p1776357655904679 ↩ ↩2 -
2026-04-09 thread Артура — fix про передачу клинических обоснований из алгоритмов вместо internal planner reasoning. https://realaicorp.slack.com/archives/C094GRT3CBY/p1775756745630469 ↩