Шаг 2 пайплайна параметр-анализа. Берёт «диагностический план» от diagnostician и фактически собирает запрошенные данные пациента из FHIR — на выходе обогащённый пакет V2GenerationReadyPackage, в котором для каждого биомаркера разделено: что граф сказал искать → что у пациента нашлось (relatedFound), и что граф сказал искать → что НЕ нашлось (relatedMissing). С графом напрямую не работает — оперирует тем, что Диагностик уже вытащил оттуда и положил в diagnostic plan.

Концептуально — Ретривер верифицирует узлы гипотетического графа связей, который построил Диагностик (см. там § «Концептуально — гипотезы о рёбрах графа»). Диагностик предлагает «вот эти сущности теоретически могут быть связаны с биомаркером X»; Ретривер проверяет, какие из них реально есть в FHIR-записях пациента. Узел подтверждён → ребро подтверждено; узел отсутствует → гипотеза отброшена. Часть верификации — детерминистическая (intersection графа с patient-context’ом), часть требует clinical reasoning (какое из подтверждённых рёбер реально объясняет конкретную аномалию) — здесь и нужен LLM.

runRetrieverAgent(...) на ветке feat/v2-5 делегирует в runCodeRetrieverAgent(...) (code-execution-вариант, см. ниже). Mastra ReAct-вариант (runMastraRetrieverAgent) — оставлен в файле как однокоммитный revert.

Открытое — что в проде сейчас и куда направление. Ильдар отмечает, что v2-5 — staging-ветка с экспериментальным code-execution; в main / в проде сейчас может всё ещё использоваться ReAct-вариант (надо проверить call-site’ы). И отдельный архитектурный вопрос: должен ли Ретривер вообще быть агентом, или это лучше выразить как workflow (детерминированный шаг, FHIR-запросы по списку без LLM-цикла)? У Ильдара есть заметки по этому переносу — TBD оформить отдельной decision-страницей.

Где живёт

  • Entry-точка: packages/analysis-core/src/agents/parameter-analysis/retriever.mastra.tsrunRetrieverAgent(patientId, patientContext, diagnosticPlan, fhirClient, options?)
  • Текущая реализация: packages/analysis-core/src/agents/parameter-analysis/code-retriever/ — code-execution вариант
  • Legacy Mastra ReAct: runMastraRetrieverAgent в том же retriever.mastra.ts (недостижим из публичного API, ждёт удаления)
  • Промпты: prompts/parameter-analysis/retriever.md (ReAct), prompts/parameter-analysis/code-retriever.md (code-execution)
  • FHIR-tools (для ReAct-варианта): tools/retriever-fhir.tools.tsRetrieverToolExecutor
  • Post-process: lib/retriever-postprocess.tsbuildPackageFromCache, injectPlannerData, postFilterRelatedItems

I/O-контракт

Вход:

  • patientId: string
  • patientContext: V2PatientContext (тот же, что у diagnostician: demographics + abnormal/normal observations + active conditions/medications)
  • diagnosticPlan: V2DiagnosticPlan — выход Диагностика; для каждого abnormal-биомаркера несёт related_parameters / related_conditions / related_medications / preanalytical_factors со списками имён, которые граф предложил проверить
  • fhirClient: FhirClient — обёртка над Google Healthcare API

ВыходV2GenerationReadyPackage:

type V2GenerationReadyPackage = {
  perParameter: Array<{
    parameterName: string;
    fromPlanner: {...};                     // вьюшка assessment'а от Диагностика
    retrievedContext: {
      relatedFound: {
        observations: V2RetrievedObservation[];  // имя + value + interpretation + date + status
        conditions:   {...}[];
        medications:  {...}[];
      };
      relatedMissing: {                          // граф просил, у пациента нет (с rationale/quotes/source — для трассируемости)
        observations: Array<string | { name? }>;
        conditions:   Array<string | { name? }>;
        medications:  Array<string | { name? }>;
      };
      staleData: {...}[];                        // V2.5: лабы есть, но устарели
      groupMatches: {...}[];
    };
  }>;
  patientContext: { demographics; activeConditions; activeMedications; allergies };
  crossDomainPatterns: {...}[];
  retrievalSummary: {...};
  followUpTests: {...}[];
};

relatedFound / relatedMissing — это и есть две главные оси для downstream. По relatedFound Reasoner строит «вот что у пациента есть из связанного», по relatedMissing — «вот чего не хватает, и какой guideline это объясняет» (rationale + quotes пробрасываются от Диагностика до этого этапа).

Для biomarker-actuality-service: matchedConditions / matchedMedications в RetrievedRelevance — это подмножество relatedFound.conditions / relatedFound.medications, отфильтрованное на «активные сейчас».

Code-execution retriever (текущая реализация)

Шаги, выполняемые в runCodeRetrieverAgent:

runCodeRetrieverAgent(patientId, patientContext, diagnosticPlan, fhirClient)
  │  prefetchPatientData(fhirClient, patientId)
  │     → подтягивает все Observation, Condition, MedicationStatement,
  │       AllergyIntolerance, Procedure, DiagnosticReport пациента в память (PatientData)
  ↓
  Sandbox-runtime готов: buildHelpers() + buildSchemaSummary()
  │  LLM получает в промпте: список названий полей PatientData, hint-helpers (findByName, ...),
  │  и diagnostic plan со списком not_found-имён, которые надо найти
  ↓
  Agentic loop (≤ DEFAULT_MAX_ITERATIONS=8):
    LLM эмитит ChatCompletion с одним из двух tool-calls:
      - execute_javascript_code(code)            — запустить JS в sandbox-runtime (Node vm)
      - submit_generation_ready_package(package) — терминал
    Лимиты: SANDBOX_TIMEOUT_MS=30000, MAX_CONSECUTIVE_ERRORS=3, MAX_OUTPUT_LENGTH=80000
    Модель по умолчанию: vertex/gemini-3-flash-preview
  ↓
  После submit'а — детерминистический post-processing:
    1. buildPackageFromCache(...) — coverage guard: если LLM что-то пропустил, добираем из кэша
    2. postFilterRelatedItems(...) — фильтр по статусам / возрасту / релевантности
    3. injectPlannerData(...) — вшиваем `fromPlanner` (assessment'ы Диагностика) обратно в perParameter
  ↓
{ success: true, generationReadyPackage, toolCallsCount, iterations }

Почему code-execution вместо ReAct FHIR-tools. Контекст: code-execution-вариант — на ветке feat/v2-5 (staging-эксперимент); в main / на проде сейчас, скорее всего, всё ещё ReAct-вариант. Описанное ниже — целевая архитектура, к которой движемся, не текущий прод.

В ReAct-варианте LLM пишет цепочку FHIR-запросов сам (~по 5-10 round-trip’ов на каждый biomarker × множество биомаркеров), что медленно и дорого. Code-execution делает префетч всех данных пациента один раз, после чего LLM фильтрует их через короткие JS-сниппеты в sandbox’е — это многократно быстрее и стабильнее. См. code-mode-pattern и code-execution-sandbox.

ReAct retriever (в проде сегодня)

runMastraRetrieverAgent — путь с FHIR-tools (get_observations, get_conditions, get_medications, get_allergies, get_procedures, get_diagnostic_reports, finish_retrieval) через Mastra Agent. На ветке v2-5 в файле сидит без публичного входа (entry-обёртка runRetrieverAgent делегирует в code-execution-вариант), но в main, скорее всего, всё ещё используется. После переезда code-execution в прод и стабильного релизного окна — удалится.

Failure modes

  • Sandbox-таймаут / ошибка JS — LLM получает error-сообщение и ретраит (counter MAX_CONSECUTIVE_ERRORS=3); три подряд → terminate.
  • Не сабмитнул packagesuccess: false. Caller обязан обработать.
  • Coverage gap — LLM пропустил часть not_found-items; buildPackageFromCache доберёт из локального кэша (PatientData) или пометит как relatedMissing если данных нет.
  • Те же Mastra/ai-sdk / import.meta.url-готчи, что и у diagnostician — actuator резолвит промпт от своего пути, под CJS-bundle крэшнет. (Code-execution-вариант использует чистый openai SDK, не Mastra, так что часть готчей не касается.)

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

  • Шаг 2 в biomarker-analysis-pipeline (Diagnostician → Retriever → Reasoner → Writer / Personalizer).
  • Концептуальный источник retrieved? поля для biomarker-actuality-service. Сегодня интеграция не сделана — в коде Артура retrieved? замокан напрямую graph-фикстурами; реальный путь будет: Validity-classifier берёт RetrievedRelevance из подмножества relatedFound (по materialRelevance: yes-фильтру) выхода Ретривера.

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

  • Удалить legacy runMastraRetrieverAgent — после стабильного релизного окна code-execution-варианта.
  • Префетч-стратегия — сейчас тянем все Observation/Condition/Medication/Allergy/Procedure/DiagnosticReport пациента; для «толстых» пациентов это много данных и токенов. Возможно стоит сужать по parameter_assessments[].graphDomain (тянем только relevant-домены).
  • Cross-app reuse / scope. Validity-классификатор хотел бы дёргать Ретривер на patient-scope (по всем биомаркерам пациента сразу). Привязан ли Ретривер именно к одному тесту — открыто: формально на вход он берёт V2DiagnosticPlan любой ширины, FHIR-tools / sandbox patient-id-агностичны. Но в проде сегодня его вызывают per-test (потому что Диагностик выдаёт план под один отчёт). Возможно достаточно просто построить «широкий» diagnostic plan на всего пациента и пропустить через тот же Ретривер; возможно — нужен отдельный slim-вариант. См. biomarker-actuality-integration.
  • maxOutputTokens / mid-decode timeout — те же грабли, что и у diagnostician / biomarker-actuality-service.

Связано

  • diagnostician — поставляет V2DiagnosticPlan на вход
  • biomarker-graph — источник связей; Ретривер их не читает напрямую, видит только «отражение» через diagnostic plan
  • biomarker-analysis-pipeline — pipeline, шаг 2
  • biomarker-actuality-service — концептуальный downstream-потребитель (через мокнутый retrieved-блок)
  • code-mode-pattern / code-execution-sandbox — паттерн «LLM пишет JS, sandbox выполняет», на котором построен текущий ретривер
  • mastra — фреймворк (legacy ReAct-вариант сидит на нём; code-execution-вариант — на чистом openai SDK)
  • biomarker-actuality-integration — куда воткнуть Validity-классификатор vs. Retriever-вывод

Источники