Status: active. Решение зафиксировано на созвоне Ильдара+Артура 2026-05-181 и уточнено в последующей сессии: вместо «делаем одинаково пока» оформляем clean architectural cut — Personalizer пропускается для нормалов с самого начала.

Контекст

Actuality-классификатор (biomarker-actuality-service) спрашивает «биомаркер всё ещё репрезентативен у этого пациента?». Для решения ему нужен граф-контекст (retrieved: какие active conditions / medications влияют на этот аналит) для всех биомаркеров, включая нормальные — нормальный биомаркер может иметь активного драйвера (медикамент, сдвигающий baseline), и тогда «нормально» нужно читать с поправкой («значение нормально благодаря препарату»).

V2.5 Diag+Retr был спроектирован под другую задачу — «разбери, почему отклонение» (biomarker-analysis-pipeline). До actuality-сшивки нормальные биомаркеры в Diag+Retr не попадали — это было осознанным решением Васи («для нормальных же вообще не будем»1). Сейчас это пересмотрено.

Выбрали + Почему

Pipeline для нормалов = Diag → Retriever → Reasoner → DoctorWriter. Personalizer пропускается. Pipeline для аномальных не меняется: Diag → Retriever → Reasoner → DoctorWriter → Personalizer.

Архитектурно это single fork point: Personalizer запускается if-and-only-if biomarker abnormal.

Почему такая фигура — три аргумента:

  1. Actuality-классификатор требует retrieved симметрично для нормальных и аномальных. Ильдар: «Для фильтра актуальности, классификатор актуальности как раз получает на вход от ретривера и уже на базе этого делает решение о тех и других. Ему неважно, норма, это обнорма»1. Артур: «Да, если бы не фильтр актуальности, то мы могли бы связь для нормалов не искать»1. Значит Diag+Retr нужны для нормалов.

  2. Reasoner + DoctorWriter работают на нормалов out of the box. Промпты prompts/biomarker-analysis/reasoner.md и writer-doctor.md уже generic — они различают status normal / abnormal / missing (reasoner.md:50) и явно обрабатывают кейс «normal-value-abnormal-context» (reasoner.md:67). Никаких prompt-правок не требуется.

  3. Personalizer для нормалов — wasted LLM-вызов. Personalizer персонализирует connections — добавляет personalizedRationale на каждый item foundContext/missingContext плюс общий whatAdditionalDataWouldClarify. Для нормалов connection map в UI скрывается (см. UI-hide ниже), значит output Personalizer’а никто не читает. Артур на созвоне согласился: «А персоналайзер тоже не работает, получается, да? Потому что он связи персонализирует. — Да»1.

Ильдар на созвоне выбрал «сделаю одинаково пока» из прагматизма (меньше изменений в коде), но в follow-up разобрали что architecturally cleaner — explicit skip Personalizer с первой итерации; нет latent wasted call.

UI-hide: фильтр в API-endpoint, не в React-компонентах

Когда нормальные биомаркеры идут через полный Diag+Retr, их BiomarkerAnalysis.foundContext / missingContext будут непустыми (граф-связи доставлены Retriever’ом). Чтобы они не отображались в UI как connection map для нормалов, фильтр накладывается на response-границе API-endpoint’а (Health Report read-routes — /api/patient-summary, b2b-аналоги), а не в React-компонентах. Логика: при возвращении BiomarkerAnalysis[] если interpretation === "N" (или эквивалент isHealthy=true), foundContext и missingContext стрипаются до пустых массивов или удаляются из shape.

Преимущества фильтра-в-endpoint:

  • Чистый API-контракт: клиент получает данные в финальной shape, ему ничего не надо знать про «связи есть, но не показывать»
  • Один source-of-truth для policy (один файл) vs логика, размазанная по нескольким UI-компонентам
  • Surface-agnostic — b2c-dashboard, b2b-platform, любые будущие потребители получают одинаковую shape

Рассматривали (альтернативы — отвергнуты)

Старый two-tier (Generator-Normals + abnormal full chain) — отвергнут

Сегодняшнее состояние кода (см. § «Как это работает сегодня» ниже): нормалы идут через short path runGeneratorNormals (single-LLM call без Reasoner/Writer/Personalizer), Diag+Retr для них не запускается. Отвергнут на этом созвоне: actuality-классификатор не может работать без retrieved симметрично, и держать два разных кодопути (short для нормалов, full для аномалов) — лишний maintenance overhead.

Полный pipeline включая Personalizer для нормалов («одинаково пока») — отвергнут

Initial proposal Ильдара на созвоне — для simplicity сделать pipeline одинаковым (включая Personalizer). Отвергнут в follow-up: Personalizer персонализирует то, что в UI скрыто, его output на нормалах — wasted. Single fork at Personalizer чище.

Двойной Retriever — отвергнут

Pass-1 на всех биомаркерах для классификатора (ephemeral), pass-2 на отфильтрованных survivor’ах для downstream. Тот же шаблон, что Вариант B из biomarker-actuality-integration (один проход для гейта, второй для downstream).

Отвергнут: двойная стоимость LLM-вызовов без compensating benefit — Reasoner/Writer всё равно нужны для нормалов после gate’а; ephemeral pass-1 не даёт экономии.

Детерминистический graph-lookup для нормальных без LLM — отвергнут

Гипотеза: для нормальных не нужен LLM-цикл Retriever’а — достаточно intersection между граф-узлами биомаркера и activeConditions / activeMedications пациента.

Отвергнут: если нормалы всё равно идут через Reasoner/Writer (нужны для пациент-понятной интерпретации с учётом active drivers), отдельный детерминистический matcher только для actuality-входа усложняет код без явной выгоды. Может вернуться как cost-optimization если token-budget Retriever’а на нормалах окажется проблемой в проде.

Status quo — отвергнут

Нормальные классифицируются без graph-drivers (default materialRelevance: no для всех active states). Отвергнут: для пациентов на постоянных препаратах (статины, левотироксин, инсулин и т.п.) пропуск graph-driver’ов для нормальных = пропуск caveat’а «значение нормально благодаря препарату». Семантическая дыра неприемлема.

Как это работает сегодня (current code state)

Решение принято на дизайн-уровне, но в коде ещё не реализовано. Что прямо сейчас в feat/actuality-mvp-dirty:

СлойСостояние
prompts/biomarker-analysis/diagnostician.mdheavily abnormal-focused: abnormal_observations это input-поле; "Process ALL abnormal_observations in batched phases"; "FOR EACH parameter IN abnormal_observations"; "if abnormal_observations is empty → submit empty plan immediately". Нормалы лежат в patientContext как «фон», но в parameterAssessments плана для них нет записей
biomarker-analysis.service.tsexplicit short-circuit if (!hasAbnormals && hasNormals) (lines 992-1030) — для пациентов без аномалов сразу идёт в runGeneratorNormals, Diag+Retr пропускается. Для смешанных пациентов Diag запускается, но planит только для аномальных
runGeneratorNormals (generator.fn.ts)отдельный single-LLM путь, промпт generator-normal.md. Output: BiomarkerAnalysis с clinicalInterpretation (single short prose), isHealthy: true, foundContext/missingContext/referenceContext все пустые (т.к. Diag plan не строился, evidence joins не было)
Personalizerзапускается на [...enriched, ...enrichedNormal], но т.к. contexts для нормалов пустые — applyPersonalization не находит ничего матчить → personalizedRationale на нормалах уже сегодня пустой (wasted LLM-вызов уже происходит, миграция эту проблему делает заметной не создаёт)
API-endpointвозвращает BiomarkerAnalysis as-is; foundContext уже пустой для нормалов, фильтра не требуется
UIрендерит foundContext как карту связей — для нормалов карта пустая, специальной логики нет

Следствия

Решение active, но переход — отдельная implementation-задача (см. Открытые вопросы про сроки).

  • Diagnostician-промпт нужно переписать под «plan for all biomarkers; deep-analyze only abnormals; для нормалов — light enumeration “какие conditions/medications graph связывает с этим биомаркером”». LLM по полю interpretation (N/H/L/etc.) различает abnormal vs normal. Input field rename abnormal_observationsobservations. Самый heavy lift миграции.

  • biomarker-analysis.service.ts нужно перестроить:

    • Удалить short-circuit if (!hasAbnormals && hasNormals) (lines 992-1030)
    • В Stage 3 batch: запускать Reasoner+DoctorWriter для всех биомаркеров (abnormals + normals)
    • Personalizer запускать только на abnormals (filter по interpretation !== "N")
    • Удалить runGeneratorNormals из Stage 3 batch
  • runGeneratorNormals функция становится unused — удалить после миграции вместе с промптом generator-normal.md.

  • API-endpoint фильтр для UI-hide: при возврате BiomarkerAnalysis[] для нормалов стрипать foundContext / missingContext (либо до пустых массивов, либо удалять поля из shape). Implementation TBD — выбрать конкретный endpoint и shape. /api/patient-summary — главный кандидат.

  • Retriever input растёт линейно по числу нормальных биомаркеров — у среднего пациента нормалов в разы больше аномалов. Token-cost увеличивается. Carry-over: измерить в проде после rollout, при критичном росте — fallback на Позицию 3 (deterministic lookup для нормалов).

  • Trend-данные (same-biomarker history) — отдельная нить, обсуждалась тем же созвоном1: тренд анализируемого биомаркера в Reasoner передаётся плохо (поле даты пропало после Python→TS миграции), Personalizer имеет пустой formatHistory(hist) слот. Артур взял задачу пересмотреть — отдельный workstream, не блокирует это решение.

Открытые вопросы

  • Когда переписывать Diagnostician-промпт. В коде сегодня — TODO-комментарии на ключевых местах (промпт, short-circuit, Generator-Normals call), функциональных изменений нет. Сроки миграции — отдельный приоритет, не блокер production-выкатки actuality MVP. Перепись — improvement над текущим acceptable fallback (нормалы получают default materialRelevance: no для всех active states).
  • API-endpoint shape для UI-hide — оставлять foundContext: [] (пустые массивы) или удалять поля из response shape целиком. Влияет на frontend (приходится или не приходится handle’ить undefined).
  • Token-cost замер — насколько input Retriever’а растёт. Сегодня нормалов у пациента ~10× больше аномалов, прирост может быть существенным.

Связано

  • biomarker-actuality-integration — родительская страница про сшивку actuality в pipeline (Вариант A — один Diag+Retr проход на всех биомаркеров)
  • biomarker-actuality-service — actuality-классификатор как consumer этого retrieved
  • biomarker-analysis-pipeline — Diag+Retr pipeline V2.5, который теперь покрывает all biomarkers
  • biomarker-graph — граф-источник
  • health-report-pipeline — где Diag+Retr placement в Phase 1
  • diagnostician / retriever — компоненты, чей scope расширяется
  • my-health — UI поверхность, где UI-hide эффект (connection map для нормалов скрыта) видна пользователю

Сноски

  1. 2026-05-18 «Перед дейликом» (Артур, Влад, Никита, Ильдар, Катя, Макс) — обсуждение pipeline-coverage для нормалов; lines 2119-2218 транскрипта, accessed 2026-05-18, https://github.com/Realai-plus/meeting-digests/blob/main/data/digest/2026/05/2026-05-18T07%3A29%3A48.000Z_%D0%9F%D0%B5%D1%80%D0%B5%D0%B4_%D0%B4%D0%B5%D0%B9%D0%BB%D0%B8%D0%BA%D0%BE%D0%BC_01KRWZVD91SQ5WWM8WKNAK22XC.md. 2 3 4 5 6