Наш алгоритм маппинга raw lab parameters в LOINC-коды. См. сам стандарт — loinc. Pipeline — наша реализация.

Контекст

Лаборатории шлют параметры в произвольных названиях/единицах/языках ("Глюкоза, ммоль/л, кровь", "Tsd/µl", "в п/зр"). Чтобы считать тренды и кодировать в FHIR Observation — нужен маппинг в loinc code. Маппинг детерминированный — один input → один code; однажды решённый — навсегда (см. dictionary-first-paradigm про превращение в управляемый справочник).

Алгоритм (3-step)

Input parameter (lab-specific name + units + material)
   │
   ▼
[1] KeywordGenerator (LLM call)
   │   Узкий промпт + chain_of_thought ("реакция in urine context? → pH")
   │   Output: 3-7 keyword candidates
   │
   ▼
[2] Search (deterministic)
   │   Custom inverted index (BM25/TF-IDF) over LOINC catalog
   │   Specimen filter (нормализация input material через blood/urine/etc family)
   │   Property filter (units → SCnc/MCnc dimension)
   │   Output: top-30 candidates ranked by score
   │
   ▼
[3] Selector (LLM call)
   │   Узкий промпт (~392 строки): chain-of-thought, examples, Method rules
   │   Tool calling: select_loinc / no_match
   │   Output: chosen LOINC code or no_match
   │
   ▼
Decision (deterministic)
   │   ConfidenceScorer — 6 checks
   │   Grade A-D + action AUTO_ACCEPT / MANUAL_REVIEW / REJECTED

Дизайн-выбор “3 узких промпта вместо single agent” зафиксирован в agent-vs-workflow (граница: open-ended → agent; structured → decomposed).

Иерархия путей при lookup

В целевой архитектуре (dictionary-first-paradigm) lookup проходит несколько уровней по убыванию скорости / детерминированности:

  1. Dictionary lookup (path-zero) — O(1) hit в Redis по канон-ключу. Маппинг найден ранее, верифицирован → возврат сразу. Это main path в зрелой системе — большинство параметров уже видели.
  2. Pipeline (3-step) — если в Dictionary нет: keyword → search → select. Записать результат в Dictionary.
  3. Agent Fallback — safety net для случаев когда [3] вернул no_match после Selector Cascade.

Чем больше в Dictionary — тем реже срабатывает Pipeline; ещё реже — Agent Fallback. См. dictionary-creation про proactive Dictionary-creation (новая лаба → запросить их parameter list заранее).

Agent Fallback (safety net)

Срабатывает когда [3] не нашёл подходящий LOINC. Это agentic loop:

  • 4 tools: search_loinc / get_loinc / find_methodless_sibling / propose_resolution (terminal)
  • Max 10 iterations, timeout 30s/call, общий ~120s
  • Дедупликация tool-вызовов, budget messages, forced resolution на финальной итерации
  • Hallucination check: после propose_resolution код верифицируется через get_loinc
  • Provider fallback (Gemini Flash → OpenAI и обратно)

Никогда не выдаёт AUTO_ACCEPT — результаты всегда идут на manual review (grade B-C).

Это safety net, не основной путь. См. agent-fallback-role — обсуждение нужен ли он в зрелой Dictionary-First системе.

Production model config

Cascade fallback:

Keywords:   Gemini Flash → GPT-5.2 → Gemini Flash → GPT-5.2
Selection:  Gemini Pro   → GPT-5.2 → Gemini Pro   → GPT-5.2

Цепочка из 4 — это retry-chain: первая попытка Gemini, при ошибке (rate limit, timeout, malformed output) — fallback на GPT-5.2; если и тот fail — повтор Gemini (часто rate limit к этому моменту проходит); если и тот — снова GPT-5.2 как last resort.

Gemini Pro для selection — он более детерминистичен и лучше следует tool-calling instructions, чем GPT-5.2 (на нашем узком промпте). Это про specialization промптов под конкретную модель — узкий instruction увеличивает шансы на качественное выполнение независимо от “силы” модели в общем смысле.

Decision cache → Dictionary

dm:{hash} в Redis сохраняет полный результат маппинга для известных biomarkers — основной вклад в стабильность production benchmark vs локальные runs (16-18/20 vs 14-16/20). Keyword cache (раздельный) почти пуст — не основной фактор.

Артур убрал TTL → это уже не cache, а persistent dictionary layer. Конкретный shape ключа — открытый вопрос (dictionary-first-paradigm open questions).

Specimen families pattern

normalizeInputMaterial("bld")"blood" → expansion в blood family (Bld, Ser/Plas, BldA, etc). Без нормализации specimen filter не находил hgh (somatotropin, system=Ser/Plas) при input "bld". Эта нормализация — критична для recall, легко забывается при портировании.

Method-only signature правило (Rule 5)

“MAP ONLY what you receive. If input does NOT specify a method → MUST select the candidate WITHOUT method.”

Если входные данные не уточняют метод — выбираем 26484-6 (monocytes, без method), а не 742-7 (manual count). Для production, где маппинг сохраняется навсегда, overspecification создаёт проблемы с трендами (один тест → разные коды).

Где живёт сейчас и куда едем

РеализацияГдеStatus
Python productionloinc-harmonization-serviceSource of truth на 2026-04-26, постепенный phase-out
Mastra TS portbloodgpt-for-business/apps/analysis-worker/src/mastra/Scoring/search parity достигнут (post-82806132); production target

Direction: полный TS-порт → Mastra-port становится production. См. loinc-unification-direction.

Состояние Mastra-порта

Scoring и search parity с production Python достигнут. Pipeline в TS даёт корректный код в candidates с правильным ranking. Остающиеся drift — на уровне LLM analyte confusion:

  • albumin → LLM выбирает Microalbumin (14957-5) вместо Albumin (1754-1)
  • kreatin → LLM выбирает Creatine (15045-8) вместо Creatinine (77140-2)

Это уже не scoring issue (оба в candidates с правильным rank), а prompt engineering / similar_resolved hints — Dictionary с уже разрешёнными случаями даёт LLM warm-up. См. dictionary-first-paradigm.

Carry-over: lessons из портажа (compound keyword expansion, UCUM dimensionality bonus, system broadening, zod stripping в Mastra и т.п.) — отдельно записать в meta/уроки.md как portage-patterns. Здесь — состояние и direction.

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

  • LOINC analyte disambiguation (albumin / Microalbumin, Creatine / Creatinine) — prompt engineering или Dictionary similar_resolved hints
  • similar_resolved Redis backend для cold-start gap (production имеет, TS-port — нужно verify)

Связанные решения

Связано

Источники