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
AbnormalDiag → Retr → Reasoner → DoctorWriter → PersonalizerПолная интерпретация для пациента; Personalizer персонализирует connections
NormalDiag → Retr → Reasoner → DoctorWriterConnections для нормалов скрыты в 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.
  • evidenceTieralgorithm / 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-контекст, но явно для вывода не используются.

Где живёт в коде

АртефактРасположение
Diagnosticianpackages/analysis-core/src/agents/biomarker-analysis/diagnostician.{mastra,raw}.ts (diagnostician)
Retrieverpackages/analysis-core/src/agents/biomarker-analysis/retriever.mastra.ts + code-retriever/ (retriever)
Reasonerpackages/analysis-core/src/agents/biomarker-analysis/reasoner.fn.ts
Evidence builderpackages/analysis-core/src/lib/retriever-postprocess.ts (входит в Retriever post-process) + Reasoner-output transforms
DoctorWriteragents/biomarker-analysis/writer-doctor.fn.ts
Personalizeragents/biomarker-analysis/personalizer.{mastra}.ts
FHIR serializerlib/fhir-clinical-impression-builder.ts + смежные builders
Сервисservices/biomarker-analysis.service.ts
Typestypes/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
Branchfeat/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 gapbiomarker-analysis LLM-вызовы не имеют 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.

Связано

Сноски

  1. Прежнее решение (отброшено). 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

  2. packages/analysis-core/src/types/biomarker-analysis.types.ts:195EvidenceItemOutput schema, BiomarkerAnalysis record (бывш. V2EvidenceItemOutput / V2SingleParameterAnalysis до Phase A rename); types/knowledge.types.tsBiomarkerInference.

  3. 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

  4. 2026-04-09 thread Артура — fix про передачу клинических обоснований из алгоритмов вместо internal planner reasoning. https://realaicorp.slack.com/archives/C094GRT3CBY/p1775756745630469