586 lines
20 KiB
Markdown
586 lines
20 KiB
Markdown
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 без пояснительного мяса.
|