NODEDC_1C/IN/TZ_Router_Orchestration_Fix.md

546 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Ниже даю ТЗ прямо текстом, без файлов.
# ТЗ: 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 перед выбором маршрута.
Требуемый интерфейс:
```python
def classify_query_for_route(
question_text: str,
parsed_intent: dict,
store_metadata: dict,
) -> RouteDecisionFlags:
...
```
Classifier обязан вычислять минимум такие flags:
```python
@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 перед выбором маршрута.
Требуемый интерфейс:
```python
def check_store_sufficiency(
question_shape: RouteDecisionFlags,
store_metadata: dict,
) -> StoreSufficiencyResult:
...
```
Минимальная модель результата:
```python
@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-правилами.
Требуемая логика:
```python
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:
```json
{
"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
Нужно добавить прозрачный лог принятия маршрута.
Требуемый формат:
```python
@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
```
Пример лог-записи:
```json
{
"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”.