Клинический граф знаний BloodGPT — структурированный «справочник связей» между биомаркерами, диагнозами, лекарствами и преаналитическими факторами, с rationale-объяснением каждой связи и цитатами из медицинских guideline’ов. Питает агентов параметр-анализа (diagnostician / retriever) и, через них, опосредованно классификатор актуальности (biomarker-actuality-service).

Где живёт

  • В коде: packages/analysis-core/src/data/biomarker_graph.json (на ветке origin/feat/v2-5, HEAD 64d83432)
  • Размер: ~13 000 строк JSON, сотни биомаркеров; целиком грузится в память на старте (DiagnosticGraphExecutor кэширует) — см. large-data-files-storage
  • Происхождение: исторически создан в Python-сервисе fhir-services (отсюда snake_case-ключи). Кто сегодня де-факто owner content’а — формально не зафиксировано: правки идут от Артура (как разработчика) и Кати (как medical advisor), но «процедуры обновления» нет. Это часть открытого вопроса про админ-интерфейс (см. ниже).

Это не «граф», а ego-network catalog

Хотя файл называется biomarker_graph.json и концептуально кодирует связи между сущностями, структура данных — не global graph, а каталог ego-сетей (1-hop neighborhoods), индексированный по биомаркеру:

  • Каждая запись в biomarkers[name] — это ego-сеть конкретного биомаркера: списки его 1-hop соседей (related_conditions, related_medications, related_parameters, preanalytical_factors) с rationale/quotes/source per ребро.
  • Каждая запись в domains[name] — то же самое, но на уровне домена (Tier 2 fallback).
  • Traversal’а нет — мы никогда не ходим по «графу» от одного биомаркера к другому через посредников; мы только читаем ego-сеть нужного биомаркера.
  • Один и тот же узел (например, statin) дублируется в ego-сетях многих биомаркеров — это OK, потому что мы не оптимизируем под global view, не запрашиваем «кто связан со статином» (inverse-lookup).
  • Identity у рёбер локальная: «ego-сеть X включает Y», а не «глобальное ребро X↔Y».

Это объясняет, почему все операции в коде сегодня — dict.get(biomarker_name) или dict.get(domain_name). KV-store с биомаркер → ego-network — нативная форма этих данных. Что это означает для выбора storage / data-model — см. biomarker-graph-storage.

Структура

type BiomarkerGraph = {
  biomarkers: Record<string, BiomarkerGraphEntry>;  // ключ — display-имя биомаркера ("Hemoglobin (Hb)")
  domains: Record<string, DomainGraphEntry>;        // ключ — домен ("hematology", "thyroid", …)
};
 
type BiomarkerGraphEntry = {
  domain: string;                                   // в какой домен входит этот биомаркер
  section_id?: string;
  related_parameters:      Array<{ name; rationale }>;
  related_conditions:      Array<{ name; context?; rationale? }>;
  related_medications:     Array<{ name; context?; rationale? }>;
  preanalytical_factors:   Array<{ name; context?; rationale? }>;
};
 
type DomainGraphEntry = {                           // используется как Tier-2 fallback в Диагностике
  scope: string;
  related_parameters:    Array<{ name; rationale }>;
  conditions:            Array<{ name; context?; rationale? }>;
  medications:           Array<{ name; context?; rationale? }>;
  preanalytical_factors: Array<{ name; context?; rationale? }>;
};

Каждый item в массиве несёт ещё два опциональных поля (на этапе normalizeContextToRationale они приводятся к единому виду):

  • rationale — короткое клиническое объяснение, почему два понятия связаны (например, для пары «Hemoglobin × Creatinine» — про то, что анемия при CKD многофакторна, EPO-дефицит и т.д.)
  • quotes — verbatim-цитата из guideline’а, на которую опирается rationale
  • source — название и ссылка на guideline (KDIGO 2026, ATA 2017, …)

quotes + source пробрасываются end-to-end и попадают в personalizer’ом сгенерированный текст под пациента — это и есть «трассируемость», о которой пишут в fhir-modeling-ai-content и fhir-meta-tagging.

Два уровня записей — biomarkers + domains

Граф несёт два параллельных типа entries:

  • biomarkers[name] (113 записей) — детализированные ego-сети для конкретных биомаркеров, которые мы успели разметить вручную. Каждая запись несёт собственный список related_* с rationale/quotes/source per ребро + ссылается на свой domain строкой.
  • domains[name] (22 записи) — обобщённые ego-сети на уровне клинической области («hematology», «thyroid», «lipids», …). Каждая со scope-описанием (что покрывает) и domain-уровневыми связями.

Двухуровневая структура — не от хорошей жизни. Реальных лабораторных биомаркеров тысячи (в LOINC десятки тысяч), вручную разметить каждого с rationale/quotes — нереально. Поэтому конкретно расписан только подмножество хорошо известных, а остальные покрываются через домен: «если биомаркера в графе нет, дай хотя бы общую domain-картину».

Как consumers это используют (fallback-каскад от specific к general) — это уже логика самих consumers, см. 3. Сам граф просто хранит два уровня данных; cascade-семантика — за пределами графа.

Использование в BloodGPT

  • diagnostician — единственный код-консьюмер графа. Загружает файл через DiagnosticGraphExecutor (lazy cache по import.meta.url-пути), отдаёт LLM-агенту три инструмента: lookup_biomarker, lookup_domain, submit_diagnostic_plan. LLM выбирает связанные параметры/диагнозы/медикаменты, исполнитель потом дополняет submit-payload graph-данными по graph_biomarker_key / graph_domain («система впрыскивает related_*»).
  • retriever — графа не видит, получает diagnostic plan на вход (со списками not_found сущностей, которые надо найти в FHIR пациента).
  • Классификатор актуальности + Reasoner — графа напрямую не видят, но потребляют его через вывод Ретривера (retrieved / relatedFound*). После Option E-интеграции оба питаются от одного прогона Диагност + Ретривер: классификатор актуальности читает retrieved per биомаркер на gate-этапе, Reasoner — relatedFound* per биомаркер на этапе анализа для survivor’ов после gate’а.

Failure modes / готчи

  • snake_case-ключи в TypeScript-типах — намеренный выбор, чтобы оставаться байт-совместимым с JSON-файлом из Python-пайплайна; cast-ы в camelCase без явной конверсии дают silent-undefined-баги
  • CJS-bundle и import.meta.urlDiagnosticGraphExecutor резолвит data/biomarker_graph.json относительно import.meta.url-пути; под analysis-worker (esbuild, strict-mode-bundle) это потенциальный краш — те же грабли, что и у biomarker-actuality-service
  • Сегодня — JSON.parse 13K строк на каждый старт процесса (cache по first-load). Версионирование = git-история файла; обновить связь = редактировать JSON руками + передеплоить весь пакет — см. ниже

Хранение в production

Сегодняшняя форма (JSON в коде, ~13K строк, parse на каждый старт процесса, правки через PR + redeploy) — это dev-стадия, в production хочется иной форме. Конкретные кандидаты (status quo + admin-UI / Postgres-таблица / hybrid baseline+overlay / graph DB), какие операции нам реально нужны, что в плоской JSON-структуре мешает (editability, cache-invalidation per edge, coverage visibility, ownership) — собрано отдельно: biomarker-graph-storage. Готчи из секции выше про bundle-готовность и parse-на-старте — часть этого же вопроса.

Связано

  • biomarker-graph-key-loinc — предложение сделать LOINC каноническим ключом графа (вместо display-имени) — убирает большую часть LLM-fuzzy-match’а из Диагностика, даёт coverage-метрику «из коробки»
  • diagnostician — единственный код, который сегодня ходит в граф; через него граф питает Retriever-пайплайн
  • retriever — следующий шаг параметр-анализа; графа не видит, но потребляет его «отражение» через diagnostic plan
  • biomarker-actuality-service — целевой потребитель отфильтрованного выхода retriever (поле retrieved); сегодня замокан
  • biomarker-analysis-pipeline — pipeline, для которого Диагностик и Ретривер изначально написаны
  • large-data-files-storage — связан касательно «где хостить большой файл», но смотри секцию «Моделирование графа» — это узкое подмножество вопроса
  • fhir-meta-tagging — почему quotes / source обязательны (трассируемость до guideline’ов)

Источники

Источники: 1.

Сноски

  1. Дайджест созвона 2026-05-12 (Ильдар + Артур), accessed 2026-05-17, https://github.com/Realai-plus/meeting-digests/blob/main/data/digest/2026/05/2026-05-12T11%3A45%3A00.000Z_%D0%A1%D0%B5%D1%80%D0%B2%D0%B8%D1%81_%D0%B2%D0%B0%D0%BB%D0%B8%D0%B4%D0%BD%D0%BE%D1%81%D1%82%D0%B8-%D1%80%D0%B5%D0%BF%D1%80%D0%B5%D0%B7%D0%B5%D0%BD%D1%82%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D0%BE%D1%81%D1%82%D0%B8-%D1%80%D0%B5%D0%BB%D0%B5%D0%B2%D0%B0%D0%BD%D1%82%D0%BD%D0%BE%D1%81%D1%82%D0%B8_%D0%B1%D0%B8%D0%BE%D0%BC%D0%B0%D1%80%D0%BA%D0%B5%D1%80%D0%BE%D0%B2_01KRE02872CTM2V6CEERAQ8EVD.md — блок про хранение графа («JSON-файл не production», Postgres-таблица или отдельный репо).