NODEDC_1C/IN/TZ_Router_Orchestration_Fix.md

21 KiB
Raw Permalink Blame History

Ниже даю ТЗ прямо текстом, без файлов.

ТЗ: 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 = 7
  • degraded_answers_count = 0
  • batch_route_count = 0
  • store_feature_risk используется 19 раз
  • live_mcp_drilldown — 4 раза
  • hybrid_store_plus_live — 4 раза
  • store_canonical — 8 раз.

Остаточные промахи распределены так:

  • heavy_analytical = 4
  • cross_entity = 2
  • drilldown_explain = 1. Основная confusion matrix:
  • batch_refresh_then_store -> store_feature_risk: 4
  • hybrid_store_plus_live -> store_canonical: 2
  • live_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 слой до состояния, в котором:

  1. heavy whole-slice запросы реально уходят в batch_refresh_then_store, а не в store_feature_risk;
  2. cross-entity causal/join запросы не downcastятся в store_canonical;
  3. точечный drilldown по конкретному объекту стабильно уходит в live_mcp_drilldown;
  4. появляется прозрачный decision log по каждому route-решению;
  5. 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.

Требуемый поток:

  1. Router выбирает batch_refresh_then_store.

  2. Создаётся orchestration job с типом refresh_and_answer.

  3. Job:

    • проверяет актуальность snapshot/feature/risk stores;
    • при необходимости пересчитывает refresh/features/risk;
    • фиксирует run ids;
    • передаёт управление answer synthesis.
  4. Answer synthesis читает уже обновлённые stores и формирует ответ.

  5. 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 по вопросам:

  • Q06Q12
  • Q26Q30 потому что именно там сейчас сосредоточены остаточные mismatchи.

14. Acceptance criteria

Этап считается принятым, если после повторного benchmark-run достигнуты условия:

  • route_mismatch_count <= 2 вместо текущих 7
  • heavy_analytical mismatches = 0
  • cross_entity mismatches = 0
  • drilldown_explain mismatches <= 1, целевое — 0
  • batch_route_count > 0
  • degraded_answers_count = 0
  • adopt_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”.