TZ_LLM_Normalizer_v2.md

Да, всё, картина понятна.
И по тому, что ты сейчас написал, **ТЗ уже можно делать нормально**, без новых срезов 1С.

Главное, что я из твоего сообщения беру как опору:

* **стабильная база у вас — `v1.1.2`**;
* `v1.1.2.1` показал, что бесконечный prompt-tuning — тупик;
* **допустимый контур** у вас определяется не “всей бухгалтерией мира”, а так:

  * вопросы **по текущему предприятию**,
  * вопросы **по его данным**,
  * вопросы **по сущностям вашей онтологии / учётного контура**;
* **недопустимый контур**:

  * абстрактная бухгалтерия,
  * законы, формы, “как вообще по ФСБУ/НК РФ”,
  * вопросы не про текущее предприятие,
  * вопросы не про данные этой базы;
* если вопрос не по контуру — **в 1С не идём**;
* если пользователь пишет “потоком”, система должна:

  * разложить это на куски,
  * отфильтровать мусор,
  * оставить только валидные task fragments,
  * уже из них собрать человекочитаемый ответ.

Это уже достаточно, чтобы зафиксировать **правильное ТЗ на `Normalizer v2 / Decomposition-first Pre-Router`**.

---

# ТЗ: `Normalizer v2`

## Decomposition-first pre-router для бухгалтерического ассистента

---

## 1. Цель этапа

Перестроить текущий LLM-normalizer из схемы:

**сырой вопрос → один intent_class → один route_hint**

в более устойчивую схему:

**сырой вопрос → decomposition → semantic fragments → route-critical flags → deterministic routing in code**

Цель:

* перестать бесконечно дотюнивать prompt под каждую новую пачку вопросов;
* устойчиво обрабатывать:

  * свободную человеческую речь,
  * длинные сообщения,
  * смешанные запросы,
  * multi-intent сообщения,
  * “поток сознания” пользователя;
* не допускать поход в бухгалтерский контур для вопросов вне допустимой области.

Комментарий:
Это уже не “ещё один prompt patch”, а **смена архитектуры нормализации**.

---

## 2. Главная идея новой схемы

LLM больше **не должна пытаться угадать один главный класс вопроса** как основу всей логики.

Вместо этого LLM должна:

1. определить, относится ли сообщение к допустимому бухгалтерскому контуру;
2. разложить сообщение на один или несколько **task fragments**;
3. для каждого фрагмента вернуть:

   * смысловые признаки,
   * флаги,
   * границы уверенности,
   * domain relevance;
4. передать это в код;
5. код уже сам решает:

   * пускать ли это в 1С-контур;
   * какому маршруту это соответствует;
   * нужно ли разбивать ответ на несколько частей;
   * нужно ли вернуть fallback/уточнение.

Комментарий:
То есть LLM здесь — **semantic parser**,
а не “магический final decision engine”.

---

## 3. Что именно было не так в `v1.x`

На `v1.x` одна модель одновременно пыталась:

* понять intent;
* понять causal;
* понять route;
* понять period;
* понять output shape;
* выбрать один taxonomy label.

На стабильном типизированном наборе это работало хорошо.
На новых 30 вопросах всё поплыло:

* `intent_class_accuracy = 70`
* `route_hint_accuracy = 80`
* `causal_flag_accuracy = 60`

Комментарий:
Это не значит, что LLM плохая.
Это значит, что **схема слишком хрупкая**:
она слишком зависит от того, как именно человек сформулировал вопрос.

---

## 4. Новая архитектура v2

---

## 4.1. Вход

Входом является **сырое сообщение пользователя**, которое может быть:

* коротким;
* длинным;
* многочастным;
* неструктурированным;
* частично бухгалтерским, частично нет;
* содержать:

  * вопрос,
  * сомнение,
  * гипотезу,
  * просьбу проверить,
  * комментарий,
  * мусор,
  * лирическое отступление.

---

## 4.2. Выход LLM

LLM должна возвращать **не один итоговый intent**, а structured decomposition.

### Ключевая идея

На выходе нужен объект вида:

```json
{
  "schema_version": "normalized_query_v2",
  "message_in_scope": true,
  "scope_confidence": "high",
  "fragments": [...],
  "discarded_fragments": [...],
  "global_notes": {...}
}
```

---

## 4.3. Фрагменты

Каждый фрагмент — это самостоятельный кандидат на задачу.

Пример:

```json
{
  "fragment_id": "F1",
  "raw_fragment_text": "по поставщикам не бьются взаиморасчеты",
  "normalized_fragment_text": "Проверка расхождений по взаиморасчетам с поставщиками",
  "domain_relevance": "in_scope",
  "business_scope": "company_specific_accounting",
  "entity_hints": ["supplier", "settlements", "payments", "documents"],
  "account_hints": ["60"],
  "time_scope": {
    "type": "explicit",
    "value": "2020-06",
    "confidence": "medium"
  },
  "flags": {
    "has_multi_entity_scope": true,
    "asks_for_chain_explanation": true,
    "asks_for_ranking_or_top": false,
    "asks_for_period_summary": false,
    "asks_for_rule_check": false,
    "asks_for_anomaly_scan": false,
    "asks_for_exact_object_trace": false,
    "asks_for_evidence": true
  },
  "candidate_labels": ["cross_entity"],
  "confidence": "medium"
}
```

Комментарий:
Фрагмент — это не “ответ”, а **единица смыслового разбора**.

---

# 5. Главный принцип: domain gating

Это критично.

## 5.1. Что считается допустимым контуром

Допустимыми считаются только вопросы:

1. про **текущее предприятие**;
2. про **его данные**;
3. про сущности и связи внутри вашей онтологии/учётного контура;
4. про:

   * документы,
   * проводки,
   * взаиморасчёты,
   * остатки,
   * хвосты,
   * закрытие периода,
   * аномалии,
   * правила учёта,
   * признаки ошибок,
   * проблемные связи в конкретной базе.

Комментарий:
То есть допустимость определяется **не общей темой “бухгалтерия”**, а связью с **данными и онтологией текущего предприятия**.

## 5.2. Что считается недопустимым контуром

Недопустимыми считаются:

* общие вопросы по бухгалтерии;
* законы, кодексы, формы, инструкции “вообще”;
* абстрактные вопросы без опоры на данные предприятия;
* вопросы не про текущее предприятие;
* бытовой трёп;
* оффтоп;
* всё, что не маппится в ваш учётный контур.

## 5.3. Обязательное правило

Если фрагмент вне контура:

* он **не должен** идти в 1С / retrieval / analytics pipeline;
* по нему возвращается safe fallback.

---

# 6. Что должно происходить с “потоком сознания”

Если пользователь пишет длинно и хаотично, система не должна пытаться сделать вид, что это один аккуратный вопрос.

Она должна:

1. разбить сообщение на фрагменты;
2. определить для каждого:

   * это task fragment или шум;
   * in-scope или out-of-scope;
   * требует ли похода в контур;
3. собрать **valid task set**;
4. выполнить только допустимые части;
5. на выходе сделать один человекочитаемый ответ, где:

   * каждая допустимая часть обработана;
   * недопустимые части вежливо отбиты.

Комментарий:
Да, конечный ответ должен быть **связанным по диалекту**, а не “пять сухих буллетов из ада”.
Но это уже задача **response composer**, а не нормализатора.

---

# 7. Новый output contract: `normalized_query_v2`

---

## 7.1. Верхний уровень

```json
{
  "schema_version": "normalized_query_v2",
  "user_message_raw": "string",
  "message_in_scope": true,
  "scope_confidence": "high | medium | low",
  "contains_multiple_tasks": true,
  "fragments": [],
  "discarded_fragments": [],
  "global_notes": {
    "needs_clarification": false,
    "clarification_reason": null
  }
}
```

---

## 7.2. Поля fragment-level

Для каждого фрагмента:

```json
{
  "fragment_id": "F1",
  "raw_fragment_text": "string",
  "normalized_fragment_text": "string",
  "domain_relevance": "in_scope | out_of_scope | unclear",
  "business_scope": "company_specific_accounting | generic_accounting | offtopic | unclear",
  "entity_hints": ["string"],
  "account_hints": ["string"],
  "document_hints": ["string"],
  "register_hints": ["string"],
  "time_scope": {
    "type": "explicit | inferred | missing",
    "value": "string | null",
    "confidence": "high | medium | low"
  },
  "flags": {
    "has_multi_entity_scope": true,
    "asks_for_chain_explanation": true,
    "asks_for_ranking_or_top": false,
    "asks_for_period_summary": false,
    "asks_for_rule_check": false,
    "asks_for_anomaly_scan": false,
    "asks_for_exact_object_trace": false,
    "asks_for_evidence": false,
    "mentions_period_close_context": false
  },
  "candidate_labels": ["cross_entity", "period_close_risk"],
  "confidence": "high | medium | low"
}
```

Комментарий:
Обрати внимание:
здесь **нет обязательного одного `intent_class`**.
Есть `candidate_labels` и `flags`.
Это ключевое изменение v2.

---

# 8. Маршрутизация больше не выбирается LLM напрямую

## 8.1. Что делает LLM

LLM только возвращает decomposition + flags.

## 8.2. Что делает код

Код детерминированно выбирает route на основе flags.

### Пример логики

* если `asks_for_exact_object_trace = true` → `live_mcp_drilldown`
* если `asks_for_ranking_or_top = true` или `asks_for_period_summary = true` → `batch_refresh_then_store`
* если `has_multi_entity_scope = true` и `asks_for_chain_explanation = true` → `hybrid_store_plus_live`
* если `asks_for_rule_check = true` и нет causal chain → `store_feature_risk`
* если `asks_for_anomaly_scan = true` и нет heavy/causal признаков → `store_feature_risk`
* если fragment out-of-scope → no-route / fallback

Комментарий:
Это главный способ уйти от бесконечной гонки за “идеальным prompt”.

---

# 9. Fallback policy

Нужно формализовать три типа fallback.

## 9.1. Out-of-scope fallback

Если вопрос не относится к данным текущего предприятия и его учётному контуру:

Пример ответа:

> Я работаю только с данными и бухгалтерическим контуром текущей компании.
> Вопрос не относится к данным этого предприятия или не попадает в доступную предметную область.

## 9.2. Clarification fallback

Если вопрос в контуре, но недостаточно определён:

Пример ответа:

> Могу проверить это в контуре компании, но нужно уточнить период, документ, счёт или участок, который нужно смотреть.

## 9.3. Partial fallback

Если сообщение смешанное:

* часть in-scope,
* часть out-of-scope.

Пример:

> Проверю часть запроса, которая относится к данным компании.
> Остальная часть выходит за пределы доступного бухгалтерического контура.

Комментарий:
Формулировки должны быть:

* профессиональные,
* не сухие как бетон,
* без “писечки-попочки”,
* но и без канцелярита.

---

# 10. Что делать с сообщением, если там несколько задач

Нужно в коде реализовать `message execution planner`.

## 10.1. Что он делает

* получает fragments;
* выбрасывает out-of-scope;
* группирует in-scope fragments;
* решает:

  * это один aggregated response,
  * или серия mini-answers;
* передаёт дальше route decisions по каждому фрагменту.

## 10.2. Важное правило

Если пользователь навалил 4 задачи в одном сообщении,
система **не должна** насильно сводить это к одному intent.

---

# 11. Что нужно реализовать в GUI / playground

Текущую GUI можно использовать, но её надо расширить под v2.

Нужны новые вкладки:

### A. Fragment View

Показывает:

* сколько фрагментов выделено;
* тексты фрагментов;
* какие отброшены;
* какие in-scope.

### B. Scope View

Показывает:

* global in-scope / out-of-scope;
* business_scope;
* clarification need.

### C. Flags View

Показывает по каждому фрагменту:

* `has_multi_entity_scope`
* `asks_for_chain_explanation`
* `asks_for_ranking_or_top`
* `asks_for_period_summary`
* `asks_for_rule_check`
* `asks_for_anomaly_scan`
* `asks_for_exact_object_trace`
* `asks_for_evidence`

### D. Route Simulation

Показывает уже не LLM hint, а **что выбрал deterministic code**.

---

# 12. Какие данные нужны для реализации

## Для написания ТЗ — достаточно текущего контекста

Новых срезов базы не нужно.

## Для реализации желательно иметь

1. `normalizer_v1.1.2` prompt set
2. calibration set
3. новая challenge-30
4. 2–3 живых примера длинного “потока”
5. текущие fallback-тексты, если уже есть

Комментарий:
То есть не данные предприятия нужны, а **данные по самому языковому поведению пользователя**.

---

# 13. Новый eval подход

Нужно перестать мерить всё только через `intent_class_accuracy`.

## Основные метрики v2

* `schema_validation_pass_rate`
* `scope_detection_accuracy`
* `fragment_split_accuracy`
* `out_of_scope_filter_accuracy`
* `route_flag_accuracy`
* `route_decision_accuracy`
* `false_cross_entity_activation_rate`
* `false_causal_activation_rate`
* `multi-intent_handling_accuracy`

Комментарий:
Вот это уже будет реальная метрика устойчивости.

---

# 14. Что нельзя делать

Codex запрещено:

1. снова пытаться лечить всё одним prompt patch;
2. сохранять один mandatory `intent_class` как ядро всей логики;
3. отправлять out-of-scope вопросы в бухгалтерский контур;
4. смешивать decomposition и final user answer в один слой;
5. делать дорогие массовые прогоны на старте.

---

# 15. Ограничения по бюджету

Этап проектировать экономно.

## Допустимый режим

* сначала сделать spec + schema + deterministic router rules;
* потом сделать маленький pilot eval;
* не жечь API на sweep’ы.

Комментарий:
Сейчас самый умный путь — **архитектурное переустройство**, а не дорогая перестрелка запросами.

---

# 16. Артефакты, которые должен выдать Codex

1. `docs/normalizer_v2_spec.md`
2. `schemas/normalized_query_v2.json`
3. `docs/domain_scope_policy.md`
4. `docs/fallback_policy.md`
5. `docs/fragment_execution_policy.md`
6. `prompts/developer/normalizer_v2.txt`
7. `prompts/fewshot/normalizer_v2.txt`
8. `reports/v2_pilot_eval_plan.md`

---

# 17. Ключевой смысл этапа

Нужно уйти от философии:

> “давайте подгоним модель под очередные 30 вопросов”

к философии:

> “давайте сделаем такую схему, где новые 30 вопросов не ломают систему, потому что система опирается не на один магический ярлык, а на decomposition + flags + deterministic routing.”

---

# 18. Очень короткий вывод

Сейчас **не надо** пытаться добить новую тридцатку patch’ем.
Сейчас надо:

* зафиксировать `v1.1.2` как стабильный baseline;
* новую тридцатку считать challenge set;
* перейти к `Normalizer v2`, где:

  * multi-label,
  * fragment decomposition,
  * scope filter,
  * deterministic routing after LLM.

---

Если хочешь, следующим сообщением я могу дать тебе **ещё более жёсткую короткую версию этого ТЗ для Codex**, уже как job prompt без пояснительного мяса.
