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.
Почему такая фигура — три аргумента:
-
Actuality-классификатор требует
retrievedсимметрично для нормальных и аномальных. Ильдар: «Для фильтра актуальности, классификатор актуальности как раз получает на вход от ретривера и уже на базе этого делает решение о тех и других. Ему неважно, норма, это обнорма»1. Артур: «Да, если бы не фильтр актуальности, то мы могли бы связь для нормалов не искать»1. Значит Diag+Retr нужны для нормалов. -
Reasoner + DoctorWriter работают на нормалов out of the box. Промпты
prompts/biomarker-analysis/reasoner.mdиwriter-doctor.mdуже generic — они различают statusnormal / abnormal / missing(reasoner.md:50) и явно обрабатывают кейс «normal-value-abnormal-context» (reasoner.md:67). Никаких prompt-правок не требуется. -
Personalizer для нормалов — wasted LLM-вызов. Personalizer персонализирует connections — добавляет
personalizedRationaleна каждый itemfoundContext/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.md | heavily 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.ts | explicit 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 renameabnormal_observations→observations. Самый 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
- Удалить short-circuit
-
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 для нормалов скрыта) видна пользователю
Сноски
-
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