Initial import NDC_1C
This commit is contained in:
@@ -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”.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user