Шаг 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.ts—runRetrieverAgent(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.ts—RetrieverToolExecutor - Post-process:
lib/retriever-postprocess.ts—buildPackageFromCache,injectPlannerData,postFilterRelatedItems
I/O-контракт
Вход:
patientId: stringpatientContext: 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. - Не сабмитнул package —
success: false. Caller обязан обработать. - Coverage gap — LLM пропустил часть
not_found-items;buildPackageFromCacheдоберёт из локального кэша (PatientData) или пометит какrelatedMissingесли данных нет. - Те же Mastra/ai-sdk /
import.meta.url-готчи, что и у diagnostician — actuator резолвит промпт от своего пути, под CJS-bundle крэшнет. (Code-execution-вариант использует чистыйopenaiSDK, не 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-вывод