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-цикла.
Что уже сделано в production — группировка синонимов через trending_group_id
Задача «эти разные 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-сторона подставляет syntheticfhir-${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) vsloincCodes: 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 — потребитель
retrievedper биомаркер; 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