LLM-нагрузка Диагноста сегодня сосредоточена в трёх местах: (1) fuzzy-match имени биомаркера с keys графа на Tier 1, (2) выбор домена для unknown биомаркера на Tier 2, (3) генерация связей из медицинских знаний на Tier 3. Это страница про два подвопроса — можно ли (1) и (2) перевести на детерминистический lookup через LOINC. Tier 3 остаётся LLM-only (по определению — для биомаркеров вне графа).

Контекст

LOINC (про сам стандарт и наше использование) — внешний стандарт лабораторных кодов; каждое лабораторное измерение имеет уникальный идентификатор. У нас observations получают LOINC во время распознавания (normalization-loinc через любой из endpoint’ов — сегодня Python-сервис, carry-over: будет мигрирован в TS как часть общей portage) и сохраняются в FHIR с кодом — то есть на стороне наблюдений LOINC уже есть.

В графе же ключи — display-имена, выбранные руками. Observation и граф используют разные системы координат для одного и того же биомаркера, и LLM Диагноста — наш мост.

Два подвопроса ниже пересекаются (оба строятся вокруг LOINC), но отдельные: один меняет ключ графа, другой строит внешний classifier. Можно сделать первый без второго, и наоборот.


Подвопрос 1 — перевод Tier 1 (биомаркер → запись графа) на детерминистический lookup

Что меняется

Сделать LOINC каноническим ключом графа:

  • Каждой записи биомаркера приписать LOINC как ключ. Два варианта shape (детально в «Открытые вопросы → Shape ключа»): (a) loincCodes: string[] — массив синонимичных LOINC-кодов в самой записи, граф ведёт свой собственный синонимы-словарь; (b) trending_group_id: string — переиспользовать существующую production-группу синонимов из trending-groups, один ID per биомаркер. Вариант (b) leading — codification уже за нас.
  • Граф индексируется по LOINC: lookup — прямое hash-обращение по ключу.
  • Display-имя сохраняется как human-readable атрибут (нужно для prompt’ов / UI), но не служит ключом.
  • В Диагносте: если у observation есть LOINC → graph.byLoinc(observation.loincCode), без LLM-цикла.

Задача «эти разные LOINC-коды описывают один и тот же биомаркер» — уже решена в production через trending-groups. На каждом FHIR Observation пишется extension http://bloodgpt.com/fhir/StructureDefinition/trending-group-id с deterministic ID, который собирает синонимичные LOINC-коды (разные units, specimens, methods, mass-molar pairs) в одну группу. То есть «один payload на множество синонимичных кодов» наполовину решено — наблюдательная сторона уже несёт этот ключ.

Это даёт второй вариант формы ключа графа: вместо собственного массива loincCodes: string[] на запись можно переиспользовать существующий trending_group_id — один string per биомаркер, без отдельного синонимы-словаря в графе.

Доводы за переиспользование trending_group_id:

  • Единый ключ между observations и графом, без separate maintenance двух списков синонимов (на observations через extension, в графе через loincCodes).
  • Coverage-метрика становится прямым Set<trending_group_id> в графе ∩ Set<trending_group_id> на observations — без посредника-LOINC-кода.
  • Augment над native LOINC (Mass-Molar merge, property-pair, system normalization) уже учтён generator’ом trending_groups — не надо повторять в графе.

Caveats:

  • Один trending_group_id потенциально может объединить два clinically-различных биомаркера (если LOINC сгруппировал по codification, но мы хотим разделить по clinical meaning). На текущей реальности не наблюдалось, но 1:1-предпосылка не гарантирована, см. Открытые вопросы.
  • L4 singletons (~LOINC без native группы и без augmentation) — у них trending_group_id в production пуст; read-сторона подставляет synthetic fhir-${loincCode}. Если такой биомаркер должен быть в графе, нужен alternate key или extend trending_groups CSV для L4.

За

  • Детерминистический Tier 1. Биомаркер с LOINC находится прямым lookup’ом по ключу. Никакого LLM-fuzzy-matching’а, retry’ев, перебора синонимов.
  • Coverage-метрика «из коробки». Если у observation есть LOINC, но в графе его нет — это явный gap, измеримый счётчик. Карта «какие LOINC встречаются в проде, какие из них покрыты» — прямая intersection-query’ёй, не fuzzy-сравнением. Раньше эту метрику нельзя было строить — данные были в разных системах координат; с LOINC-ключом она появляется естественно.
  • Граф встраивается в LOINC-справочник как clinical enrichment. LOINC уже у нас на стороне observations (через normalization-loinc); если граф тоже keyed by LOINC, две системы используют один словарь. Запись графа фактически становится «LOINC-кодом + clinical-overlay» — на неё можно ссылаться через публичный код из любого слоя (FHIR resources, документация, аналитика, audit-логи). Подробнее об этом подходе и о слиянии графа с LOINC-каталогом в один артефакт: Граф и LOINC-каталог — стоит ли объединить.
  • Потребители получают упрощённый join. Сервис актуальности сегодня делает match по analyte имени; evidence-enrichment — по _matchedGraphName. С LOINC-ключом эти join’ы становятся такими же детерминистическими, как Tier 1 lookup в Диагносте.

Против

  • Зависимость от качества normalization-loinc — working assumption. Если recognition прислал имя в формате, которое нормализация не разрулила, Tier 1 не сработает. Это решение принимает как working assumption, что upstream-нормализация работает качественно. Если позже выяснится иначе — это отдельный pipeline upgrade, не блокер LOINC-key подхода.
  • Переключение ключа графа. Сегодняшний JSON надо обойти и приписать LOINC к каждой записи — разовая работа + expert-валидация. Автоматизируется большей частью через наш внутренний LOINC-сервис (тот же, что используется в normalization-loinc); medical advisor spot-check’ает случаи multi-match / no-match.

Кандидат, который рассматривали в «Против», но передумали: «биомаркеры без LOINC». Это не реальный contra — если у биомаркера нет LOINC, его скорее всего нет и в графе (источники одни и те же). Естественно падает на Tier 3 / model_knowledge. LOINC-key подход эту ситуацию не ухудшает.

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

  • Shape ключа: trending_group_id (один string) vs loincCodes: string[] (массив). Два кандидата: (a) переиспользовать существующий production [[trending-groups|trending_group_id]] (см. раздел «Что уже сделано на codification-слое» выше) — один ID per биомаркер, codification уже за нас; (b) loincCodes: string[] — массив LOINC-кодов на запись, граф ведёт собственный синонимы-словарь. Leading кандидат — (a), но требует confirm от medical advisor что 1:1 trending_group:биомаркер holds для clinically значимых случаев и решения по L4-singletons. Заодно открывается путь: граф становится обогащением существующего LOINC/trending-каталога (см. Граф и LOINC-каталог — стоит ли объединить).
  • Миграция текущего JSON. Через наш внутренний LOINC-сервис: автомат-пройти по display-именам, заполнить loincCode. Spot-check medical advisor’ом для multi-match / no-match.

Подвопрос 2 — перевод Tier 2 (биомаркер → домен) на детерминистический lookup

Контекст

Tier 2 срабатывает когда биомаркера в графе нет. Сегодня LLM выбирает домен, читая <scope>-описания всех 22 доменов и подбирая подходящий по медицинской семантике. Для известных биомаркеров (Tier 1) этот вопрос не возникает — domain тривиально берётся из entry.domain записи. Tier 2 — единственное место domain-классификации, где работает LLM.

Чтобы такой случай детерминистически отнести к одному из доменов без LLM, нужен внешний механизм классификации (граф про unknown ничего не знает по определению).

Варианты

  • LOINC CLASS как proxy. Использовать LOINC-классификацию (CHEM / HEM/BC / COAG / ENDO / …) напрямую. Слишком грубо: CHEM покрывает наши lipids / renal / enzymes / proteins / electrolytes одновременно; ENDO — thyroid / adrenal / reproductive / parathyroid / pituitary. Не 1:1 с нашей таксономией.
  • Component-name regex / pattern matching. Регулярки на LOINC component name (Cholesterol* → lipids; Thyrotropin|Thyroxine|Triiodothyronine → thyroid). Работает на знакомых паттернах, heuristic, brittle на cross-domain биомаркерах (LDH одновременно hemolysis-context в hematology И cardiac).
  • LOINC→domain mapping в нашем LOINC-сервисе, привязка домена при записи. Сервис уже несёт codification-knowledge (LOINC коды, standardized names); естественное расширение — добавить «к какому из наших 22 доменов относится этот LOINC». Lead кандидат: separation of concerns (граф = clinical knowledge, LOINC-сервис = codification), single source of truth, reusable Диагностом / recognize / classifier / UI / analytics. Привязка домена идёт во время распознавания (write-time), как у LOINC сегодня — детали ниже.
  • Status quo — LLM остаётся на Tier 2. Принять, что unknown биомаркер → domain через LLM-чтение <scope>-описаний. Допустимо как interim пока LOINC-сервис расширяется; объём Tier 2 может оказаться небольшим.

Нюансы lead кандидата (LOINC→domain в LOINC-сервисе)

Варианты сбора unknown LOINC:

  • По мере появления. Когда приходит observation с LOINC, которого нет ни в графе, ни в таблице сервиса — LLM один раз классифицирует его в один из 22 доменов, результат сохраняется. Все последующие observations с тем же кодом используют готовое значение. Human-review опционален, по флагу.
  • Предзаполнение по LOINC-таксономии. Пройтись по известному списку лабораторных LOINC-кодов наперёд (Top-N по частоте в реальных лабах), для каждого предложить domain (через LOINC CLASS + component name + LLM-judge), human-аннотатор валидирует. Получаем готовую таблицу до того, как unknown’ы появятся в проде.
  • Гибрид. Предзаполнение даёт основу (быстро покрывает common cases), edge cases добиваются по мере появления.

В любом подходе LLM не уходит совсем, но сужается до одного места — внутри сервиса при пополнении таблицы, один раз на LOINC-код, с возможностью human-review. На каждый Диагност-запрос LLM больше не зовётся.

Когда привязывается domain — при распознавании (write-time), как с LOINC сегодня. Сейчас observation получает LOINC во время normalization-loinc и записывается с этим кодом. Тот же шаг привязывает domain: lookup в LOINC-сервисе → domain записывается в нашу internal metadata (Postgres-колонка observation’а или meta-tag). Decision переезжает из runtime каждой Диагност-сессии в один write-step при записи наблюдения. Это та же модель, что мы уже применяем для LOINC: codification делается раз, потом все потребители читают готовое значение.

Что если меняется taxonomy (новые домены, splits, merges) — пройтись по всем LOINC’ам в сервисе, обновить mapping, пересчитать domain для затронутых observations в Postgres. Это контролируемая dev-time операция, не runtime-неожиданность: domain принадлежит LOINC-коду, и понять «какие LOINC переехали из старого домена в новый» — прямая задача. Решение переносится с runtime на этап разработки, как мы это делаем для LOINC.

За

  • Детерминистический Tier 2 для биомаркеров, у которых LOINC есть, но нет записи в графе. LLM-нагрузка на этот шаг уходит.
  • LOINC→domain — переиспользуемое знание. Тот же mapping нужен не только Диагносту: recognize-pipeline для routing’а, classifier актуальности для caveat’ов, аналитика, UI для группировки на patient summary. Single source of truth в LOINC-сервисе → no duplicates.

Против

  • LLM не уходит полностью — он переносится из runtime в pre-processing. На пополнении таблицы в LOINC-сервисе LLM может всё ещё помогать выбирать domain для нового LOINC’а; разница в том, что это делается один раз на код (с возможностью human-review), а не на каждый Диагност-запрос в runtime. Maintenance не исчезает — формализуется как curated mapping table.
  • Domain taxonomy у нас своя. Наши 22 домена — clinical-meaningful группировка, не стандартная. Любая внешняя классификация (LOINC CLASS, embedding-based) не даст готового маппинга — придётся всё равно мейнтенить нашу таблицу.

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

  • Стоит ли инвестировать в LOINC-сервисный classifier, или статус-кво (LLM) приемлем — зависит от частоты Tier 2 на реальной нагрузке. Без замеров неясно, насколько Tier 2 объёмен.
  • Какая модель внутри сервиса — статическая таблица, embedding+similarity, мини-LLM на каждое решение, hybrid? Detail-вопрос когда (и если) решим строить.

Связь между двумя подвопросами

Оба построены вокруг LOINC, но самостоятельны:

  • Подвопрос 1 (LOINC-ключ графа) — clear win для Tier 1 + получаем coverage-метрику. Можно сделать без второго.
  • Подвопрос 2 (LOINC→domain classifier) — отдельный шаг, не зависит от первого технически. Можно построить classifier-сервис даже если граф остаётся на display-именах (просто меньше bootstrap-данных).

Делать первый без второго — устраняется Tier 1 LLM-нагрузка, остаётся Tier 2 LLM. Это уже большой win.

Делать второй без первого — Tier 2 детерминистический, но Tier 1 fuzzy-match всё ещё LLM. Асимметрично, но технически возможно — LOINC-классификатор можно строить параллельно с любым ключом графа.

Делать оба — Tier 1 + Tier 2 deterministic, LLM остаётся только для true Tier 3 (биомаркера в графе нет, в LOINC-mapping’е нет). Полный win.

Естественный путь: подвопрос 1 первым (он самостоятелен и open вопросов меньше), затем замеры — насколько часто реально срабатывает Tier 2, и оправдывается ли LOINC-сервис как classifier.

Связано

  • biomarker-graph — что такое граф, его структура
  • diagnostician — потребитель, чья LLM-нагрузка сужается
  • retriever — следующий потребитель, тоже выигрывает (deterministic name-resolution через план)
  • biomarker-actuality-service — потребитель retrieved per биомаркер; join переезжает на LOINC
  • biomarker-graph-storage — параллельная decision-страница про как хранить граф (Postgres / file / hybrid). Связана: переход на LOINC-ключ может слить граф с LOINC-каталогом в один артефакт.
  • trending-groups — codification-слой production’а, который уже собирает синонимичные LOINC-коды в один deterministic ID; кандидат trending_group_id как ключ графа опирается на эту страницу
  • biomarker — что такое «биомаркер» как clinical entity (в отличие от codification-слоя trending groups)
  • biomarker-actuality-integration — открытый вопрос «покрытие нормальных биомаркеров» проще решить, если ключ deterministic