Initial import NDC_1C

This commit is contained in:
2026-03-26 10:38:25 +03:00
commit a162d77ef7
2943 changed files with 3615871 additions and 0 deletions
+545
View File
@@ -0,0 +1,545 @@
Ниже даю ТЗ прямо текстом, без файлов.
# ТЗ: 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 по вопросам:
* Q06–Q12
* Q26–Q30
потому что именно там сейчас сосредоточены остаточные 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”.