21 KiB
Ниже даю ТЗ прямо текстом, без файлов.
ТЗ: Router / Orchestration Fix для бухгалтерского ассистента
1. Контекст этапа
Ontology/mapping слой после semantic-v2 ремонта вытащен из красной зоны: semantic_coverage_pct вырос с 61.1917 до 94.9279, unknown_links снизились с 1016 до 102, relation_types_total вырос с 1 до 25, covered_entity_classes — с 33 до 42, source_id_unknown ушёл в 0. Regression tests: 17 passed.
При этом orchestration/router слой остаётся узким местом. В benchmark по 35 вопросам:
route_mismatch_count = 7degraded_answers_count = 0batch_route_count = 0store_feature_riskиспользуется 19 разlive_mcp_drilldown— 4 разаhybrid_store_plus_live— 4 разаstore_canonical— 8 раз.
Остаточные промахи распределены так:
heavy_analytical = 4cross_entity = 2drilldown_explain = 1. Основная confusion matrix:batch_refresh_then_store -> store_feature_risk: 4hybrid_store_plus_live -> store_canonical: 2live_mcp_drilldown -> hybrid_store_plus_live: 1.
Сама policy уже описывает правильную целевую логику:
- exact object trace →
live_mcp_drilldown - simple factual in loaded slice →
store_canonical - trend/anomaly/risk →
store_feature_risk - heavy whole-slice with freshness gap →
batch_refresh_then_store - low confidence fallback →
hybrid_store_plus_live.
Проблема текущего этапа: policy на бумаге правильная, но runtime-router выбирает route слишком грубо и слишком рано downcast’ит сложные запросы в store-only ветки.
2. Цель этапа
Довести router/orchestration слой до состояния, в котором:
- heavy whole-slice запросы реально уходят в
batch_refresh_then_store, а не вstore_feature_risk; - cross-entity causal/join запросы не downcast’ятся в
store_canonical; - точечный drilldown по конкретному объекту стабильно уходит в
live_mcp_drilldown; - появляется прозрачный decision log по каждому route-решению;
batch_refresh_then_storeперестаёт быть декларацией и начинает реально исполняться в runtime.
3. Scope работ
В scope этапа входят:
- refactor query classifier / route selector;
- внедрение decision flags;
- внедрение store sufficiency checker;
- внедрение explicit route guards;
- реализация runtime handoff для
batch_refresh_then_store; - внедрение route explanation logging;
- повторный benchmark-run на тех же 35 вопросах.
В scope не входят:
- новый overhaul ontology/mapping слоя;
- новый redesign canonical model;
- расширение snapshot domain beyond June-2020;
- тяжёлая оптимизация latency сверх baseline этого этапа. Ontology уже починен до уровня, достаточного для данного этапа; оставшиеся unknown-поля точечные и не являются основным блокером для router-фазы.
4. Проблемы, которые надо исправить
4.1 Heavy analytical downcast
Сейчас 4 heavy-вопроса, которые должны идти в batch_refresh_then_store, реально уходят в store_feature_risk. Это касается whole-slice heavy analytical запросов, включая:
Полный риск-срез за июньРейтинг риск-счетовРейтинг риск-контрагентовCompany anomaly summary.
Это противоречит policy, где для heavy_analytical задан приоритет batch_refresh_then_store -> feature_store -> risk_store.
4.2 Cross-entity downcast
Два cross-entity вопроса ожидают hybrid_store_plus_live, но реально уходят в store_canonical:
Свяжи документы покупателей и проводкиСвяжи контрагентов, договоры и проводки.
Это означает, что router недостаточно хорошо распознаёт chain-join / causal cross-entity shape и преждевременно считает, что canonical store уже достаточен.
4.3 Drilldown boundary bug
Один drilldown вопрос ожидает live_mcp_drilldown, но реально идёт в hybrid_store_plus_live. Это означает, что граница между exact object trace и mixed live/store explain сейчас определена недостаточно жёстко.
4.4 Отсутствие реально работающего batch runtime
batch_route_count = 0, хотя benchmark содержит heavy-вопросы, ожидающие batch_refresh_then_store. Это прямо подтверждает, что batch-маршрут не активируется как реальный runtime-path.
5. Требуемая архитектура решения
5.1 Query classifier v2
Нужно внедрить отдельный слой decision classification перед выбором маршрута.
Требуемый интерфейс:
def classify_query_for_route(
question_text: str,
parsed_intent: dict,
store_metadata: dict,
) -> RouteDecisionFlags:
...
Classifier обязан вычислять минимум такие flags:
@dataclass
class RouteDecisionFlags:
needs_exact_object_trace: bool
needs_causal_chain: bool
needs_cross_entity_join: bool
needs_full_period_aggregation: bool
needs_ranking: bool
needs_anomaly_summary: bool
needs_runtime_truth: bool
freshness_sensitive: bool
ambiguous_object_scope: bool
store_sufficiency_confident: bool
precomputed_aggregate_available: bool
Назначение: отделить semantic classification вопроса от конечного route selection. Сейчас они, по сути, схлопнуты, из-за чего router переоценивает store sufficiency и недооценивает heavy/cross-entity требования. Это видно по текущим mismatch’ам.
6. Правила классификации
6.1 live_mcp_drilldown
Запрос должен классифицироваться в live_mcp_drilldown, если одновременно выполняется:
- запрос относится к exact object trace, posting chain, source-of-record или object-level why/explain;
- объект явно указан или легко нормализуется до конкретного документа/проводки/строки/регистра/субконто;
- ответ требует runtime evidence, а не только store summary. Это согласуется с policy: “exact object trace or posting chain -> live_mcp_drilldown”.
Триггеры:
- “почему именно эта проводка”
- “источник этой строки”
- “цепочка документ -> проводки -> субконто”
- “почему выбрано это субконто”
- “документ по номеру и его ссылка”.
6.2 hybrid_store_plus_live
Запрос должен классифицироваться в hybrid_store_plus_live, если:
- требуется cross-entity join между 3+ сущностями;
- требуется causal stitching по нескольким источникам;
- store сам по себе не гарантирует достаточную explainability;
- но запрос не является exact object trace в узком смысле.
Триггеры:
- документ ↔ проводки
- контрагент ↔ договор ↔ проводки
- регистр ↔ первичный документ
- explain через движения без указания одного точечного source-of-record.
6.3 store_canonical
Разрешён только для:
- simple factual within loaded slice;
- материализованных и однозначных canonical facts;
- cross-entity вопросов без causal stitching и без runtime evidence need.
Запрещён для:
- chain joins;
- “свяжи X и Y через Z”;
- source-of-record explanations;
- heavy full-period aggregations.
6.4 store_feature_risk
Разрешён только для:
- trend / anomaly / risk вопросов;
- ambiguous fuzzy risk/tax questions;
- heavy-вопросов только при явном наличии готового и свежего precomputed aggregate.
Не должен быть default-веткой для heavy_analytical whole-slice вопросов.
6.5 batch_refresh_then_store
Обязателен для heavy whole-slice вопросов, если:
- нужен full-period aggregation;
- нужен ranking;
- нужен company-wide anomaly/risk summary;
- нужна свежесть выше текущего store snapshot;
- нет подтверждённого precomputed aggregate нужного уровня.
7. Store sufficiency checker
Нужно реализовать отдельный модуль проверки достаточности store перед выбором маршрута.
Требуемый интерфейс:
def check_store_sufficiency(
question_shape: RouteDecisionFlags,
store_metadata: dict,
) -> StoreSufficiencyResult:
...
Минимальная модель результата:
@dataclass
class StoreSufficiencyResult:
canonical_sufficient: bool
feature_sufficient: bool
risk_sufficient: bool
freshness_ok: bool
aggregate_level_ok: bool
ranking_ready: bool
explanation_ready: bool
reason_codes: list[str]
Checker обязан учитывать:
- покрытие периода;
- наличие нужной группировки;
- наличие precomputed ranking aggregate;
- наличие risk/anomaly aggregate нужного уровня;
- freshness snapshot;
- достаточность store для explainability.
Ключевая цель — убрать ложные решения вида:
- “store_feature_risk и так хватит”
- “store_canonical и так хватит” в тех кейсах, где store на самом деле не покрывает shape запроса. Именно это сейчас ломает heavy и cross-entity маршруты.
8. Explicit route guards
После classifier и sufficiency-checker route должен выбираться не scoring’ом “в среднем”, а guard-правилами.
Требуемая логика:
def choose_route(flags: RouteDecisionFlags, suff: StoreSufficiencyResult) -> str:
if flags.needs_exact_object_trace:
return "live_mcp_drilldown"
if flags.needs_full_period_aggregation or flags.needs_ranking or flags.needs_anomaly_summary:
if not (flags.precomputed_aggregate_available and suff.freshness_ok and suff.aggregate_level_ok):
return "batch_refresh_then_store"
if flags.needs_cross_entity_join and flags.needs_causal_chain:
if not suff.explanation_ready:
return "hybrid_store_plus_live"
if suff.feature_sufficient and not flags.needs_runtime_truth and (
flags.needs_anomaly_summary or parsed_as_trend_or_risk
):
return "store_feature_risk"
if suff.canonical_sufficient and not flags.needs_causal_chain:
return "store_canonical"
return "hybrid_store_plus_live"
Смысл:
- drilldown имеет жёсткий приоритет;
- heavy whole-slice с ranking/summary и недостаточным aggregate обязан уйти в batch;
- causal cross-entity не имеет права downcast’иться в
store_canonical; - fallback остаётся
hybrid_store_plus_live.
9. Runtime batch handoff
Нужно реализовать реальный handoff для batch_refresh_then_store.
Требуемый поток:
-
Router выбирает
batch_refresh_then_store. -
Создаётся orchestration job с типом
refresh_and_answer. -
Job:
- проверяет актуальность snapshot/feature/risk stores;
- при необходимости пересчитывает refresh/features/risk;
- фиксирует run ids;
- передаёт управление answer synthesis.
-
Answer synthesis читает уже обновлённые stores и формирует ответ.
-
Route/log сохраняют факт batch execution.
Минимальный payload job:
{
"job_type": "refresh_and_answer",
"question_id": "Q28",
"slice_window": "2020-06",
"requested_outputs": ["feature_store", "risk_store"],
"reason": ["needs_full_period_aggregation", "needs_ranking", "aggregate_not_sufficient"]
}
Обязательное требование: batch_refresh_then_store должен стать исполняемым runtime-path, а не декларацией в policy. Сейчас это не так, что видно по batch_route_count = 0.
10. Route explanation logging
Нужно добавить прозрачный лог принятия маршрута.
Требуемый формат:
@dataclass
class RouteDecisionLog:
question_id: str
question_text: str
parsed_class: str
decision_flags: dict
sufficiency_snapshot: dict
candidate_routes: list[str]
rejected_routes: dict[str, str]
chosen_route: str
execution_mode: str
batch_job_id: str | None
Пример лог-записи:
{
"question_id": "Q30",
"parsed_class": "heavy_analytical",
"decision_flags": {
"needs_full_period_aggregation": true,
"needs_ranking": false,
"needs_anomaly_summary": true,
"needs_runtime_truth": false
},
"sufficiency_snapshot": {
"feature_sufficient": false,
"risk_sufficient": false,
"freshness_ok": false,
"aggregate_level_ok": false
},
"rejected_routes": {
"store_feature_risk": "aggregate_not_sufficient",
"store_canonical": "wrong_query_shape"
},
"chosen_route": "batch_refresh_then_store"
}
Это нужно для последующего дебага benchmark mismatch’ов. Иначе команда и дальше будет видеть только конечный маршрут, но не причину выбора.
11. Конкретные ошибки benchmark, которые должны быть закрыты
11.1 Heavy
Нужно закрыть как минимум:
- Q26
Полный риск-срез за июнь - Q27
Рейтинг риск-счетов - Q28
Рейтинг риск-контрагентов - Q30
Company anomaly summaryОжидаемый результат: эти вопросы должны идти вbatch_refresh_then_store, а не вstore_feature_risk.
11.2 Cross-entity
Нужно закрыть:
- Q11
Свяжи документы покупателей и проводки - Q12
Свяжи контрагентов, договоры и проводкиОжидаемый результат:hybrid_store_plus_live, а неstore_canonical.
11.3 Drilldown
Нужно закрыть 1 mismatch по границе live_mcp_drilldown / hybrid_store_plus_live.
Ожидаемый результат: точечный object-trace вопрос не должен уходить в hybrid, если вопрос явно указывает на source-of-record / posting chain.
12. Предлагаемая структура модулей
Вариант минимальной структуры
router/query_classifier.py
classify_query_for_route()- правила выделения decision flags
router/store_sufficiency.py
check_store_sufficiency()- оценка достаточности canonical / feature / risk stores
router/route_selector.py
choose_route()- explicit route guards
orchestration/batch_runtime.py
enqueue_refresh_and_answer_job()run_refresh_and_answer_job()
router/decision_log.py
build_route_decision_log()- сериализация route decision trace
tests/test_router_decision_flags.py
- флаги для benchmark-вопросов
tests/test_store_sufficiency.py
- sufficiency cases
tests/test_route_guards.py
- guard logic
tests/test_batch_runtime_handoff.py
- batch execution path
13. Требования к тестам
Нужно добавить unit + integration тесты.
Unit tests
Покрыть:
- correct flag extraction for heavy queries;
- correct flag extraction for drilldown queries;
- correct flag extraction for cross-entity causal queries;
- store sufficiency false/true cases;
- route guards for each route;
- batch handoff payload building.
Integration tests
Покрыть:
- heavy query without ready aggregate →
batch_refresh_then_store - heavy query with ready fresh aggregate →
store_feature_risk - cross-entity causal query →
hybrid_store_plus_live - exact object trace →
live_mcp_drilldown - simple factual in loaded slice →
store_canonical
Отдельно нужно прогнать benchmark subset по вопросам:
- Q06–Q12
- Q26–Q30 потому что именно там сейчас сосредоточены остаточные mismatch’и.
14. Acceptance criteria
Этап считается принятым, если после повторного benchmark-run достигнуты условия:
route_mismatch_count <= 2вместо текущих7heavy_analytical mismatches = 0cross_entity mismatches = 0drilldown_explain mismatches <= 1, целевое —0batch_route_count > 0degraded_answers_count = 0adopt_with_improvementsсохраняется минимум, желательно с качественным улучшением рекомендаций- route decision logs сохраняются для всех 35 benchmark-вопросов.
Латентность допускается немного выше baseline по heavy-вопросам, так как batch route по профилю дороже: у него baseline retrieval около 1240 ms против 190 ms у store_feature_risk. Но рост должен быть локализован только на heavy-path и не должен ломать обычные store-first сценарии.
15. Нефункциональные требования
Нужно сохранить текущие плюсы системы:
store-first retrieval policy;- bounded context;
- отсутствие деградированных ответов;
- отсутствие uncapped heavy live scans.
Запрещается:
- заменять batch на тяжёлый live-scan;
- насильно переводить все cross-entity вопросы в live;
- ломать простые store-only factual ответы;
- убирать fallback
hybrid_store_plus_live.
16. Порядок реализации
Сначала сделать classifier v2 и decision flags. Потом — store sufficiency checker. После этого — route guards. Затем — runtime handoff для batch. Потом — decision logging. И только после этого — повторный benchmark-run и точечная подстройка порогов по остаточным кейсам. Такой порядок минимизирует расползание архитектуры и даёт сразу диагностируемую систему.
17. Ожидаемый результат этапа
После выполнения этого ТЗ система должна перейти из состояния:
- “ontology уже хорошая, но router маршрутизирует местами по упрощённой логике”
в состояние:
- “ontology + router + orchestration согласованы между собой, а route choice воспроизводим, объясним и исполняется реально, включая batch”.