Перейти к содержанию

Lookup-by-number tool

Зафиксировано 29 апреля 2026 вечером. Реализация — после субботней миграции Люми. MVP — 1 день, полная версия — с metadata enrichment в чанках (ре-extract).

Зачем

Главная боль Люми: юзер пишет «Приказ №211» или «ст. 156 ТК РК» — retrieval возвращает не тот документ или с правильным номером, но не той статьёй.

Почему: embeddings плохо работают с цифрами. «№2119» ≈ «№311» ≈ «№411» в векторном пространстве — цифры не несут семантики. bge-m3 fine-tune это НЕ починит (или починит частично — marginal gain). Решение — отдельный детерминированный lookup через registry.

Критичная ловушка: grounding_fix_apr17 делает lookup ПОСЛЕ retrieval, как hint для LLM. Это решает галлюцинацию номеров, но НЕ решает retrieval. Если правильный чанк не достали — hint LLM бесполезен. lookup_tool должен работать ДО retrieval, на этапе query understanding.

Поток обработки (новая ветка в pipeline)

User query
    ↓
[NEW] query_parser — regex детекция identifiers
    ↓
    ├─ identifiers найдены? → lookup_by_number tool
    │       ↓
    │   registry hit?
    │       ├─ Да → достать чанки этого doc_id из ChromaDB
    │       │      по metadata-фильтру → передать в LLM с пометкой
    │       │      «точное совпадение по идентификатору»
    │       └─ Нет → fallback в обычный hybrid retrieval +
    │              hint LLM «идентификатор не найден в реестре»
    │
    └─ identifiers НЕ найдены → существующий hybrid retrieval

5 компонентов

1. query_parser.py — детектор идентификаторов

Regex для всех типов: - Әділет ID: V2300031934, U2300031934 - Закон/Кодекс: №414-V ЗРК, №226-V ЗРК - Статья кодекса (ru/kz/en): ст. 156 ТК РК, 156-бабы, Article 156 - Сокращённая форма: 182 ТК, ҚР ЕК 156 - Приказ министра: Приказ МТСЗН №211 от 25.12.2024 - ТР ТС/ТР ЕАЭС: ТР ТС 004/2011 - OSHA: OSHA 1910.146, 29 CFR 1910.146, или просто 1910.146 - API RP / API стандарты: API RP 75, API 510 - NFPA: NFPA 652 - ISO: ISO 45001 - IOGP: IOGP Report 423

Важно: парсер возвращает список identifiers (может быть несколько в одном запросе).

2. lookup_tool.py — поиск в registry

Индексы для O(1) lookup: - _by_adilet_id: V2300031934 → doc - _by_order_number: ('МТСЗН', '211') → [doc, doc, ...] - _by_law_zrk: '414-V ЗРК' → doc - _by_osha: '1910.146' → doc - _by_iso, _by_nfpa, _by_iogp, _by_api, _by_tr_ts

Критично для order_number: если юзер не указал ведомство («приказ 211»), возвращаем все приказы с этим номером из разных ведомств. Не bug — реальность. LLM в этом случае просит уточнения.

3. Интеграция в bot.py pipeline

async def process_query(query, user_id):
    classification = await classify(query)
    identifiers = parse_identifiers(query)

    if identifiers:
        lookup_results = [{'identifier': i, 'matches': registry.lookup(i)} 
                          for i in identifiers]

        if any(r['matches'] for r in lookup_results):
            chunks = await fetch_chunks_by_doc_ids(
                doc_ids=[d['id'] for r in lookup_results for d in r['matches']],
                query=query,  # для ранжирования ВНУТРИ документа
                k_per_doc=5
            )
            hint = build_lookup_hint(lookup_results)
            return await generate_with_chunks(query, chunks, hint=hint)

    # Fallback
    chunks = await hybrid_retrieval(query, classification)
    return await generate_with_chunks(query, chunks)

4. fetch_chunks_by_doc_ids — гибридный retrieval

Это НЕ vector search. Для каждого doc_id: - ChromaDB filter where={"doc_id": doc_id} — резко ограничивает поиск - Ранжируем выдачу embedding similarity к query внутри этого документа - Берём top-K чанков

Документ фиксирован детерминистично (registry), внутри документа embedding ранжирует (в чём он действительно силён). Гибрид лучшего из двух миров.

5. Hint для LLM

[ТОЧНЫЕ СОВПАДЕНИЯ ПО ИДЕНТИФИКАТОРАМ]
✓ 'ст. 182 ТК РК' → Трудовой кодекс РК, ст. 182 (V1500414...). 
  Контент этой статьи в чанках ниже.
⚠ 'Приказ №211' → соответствует 3 документам:
   • Приказ МТСЗН №211 от ...
   • Приказ МЧС №211 от ...
   • Приказ МЭ №211 от ...
   Спроси юзера какой нужен.
⚠ 'Приказ №999' — НЕ НАЙДЕН в реестре. 
  Скажи:  «этого документа нет в моей базе». Не выдумывай.

Metadata enrichment — предусловие для полной версии

Для where={"doc_id": doc_id} filter в каждом чанке должны быть metadata:

chunk_metadata = {
    "doc_id": "v2300031934",
    "doc_title": "Санитарные правила...",
    "adilet_id": "V2300031934",
    "issuer": "МЗ",
    "order_number": "26",
    "law_zrk": None,
    "date_approved": "21.04.2025",
    "jurisdiction": "kz",
    "doc_type": "приказ",
    "source_url": "https://adilet.zan.kz/...",
    # для статей кодекса
    "code_articles": [156, 157, 158],
    # для международных
    "intl_section": "1910.146",
    "intl_org": "OSHA",
}

Большинство этих полей уже парсится в document_registry.py и лежит в registry, но НЕ передаётся в chunk metadata при индексации. Главный gap — enrich при re-extract.

Две версии — MVP и Full

MVP (1 день после миграции) — покрывает ~30% exact-match-запросов

  1. query_parser.py с 4 регулярками (adilet_id, order_number, code_article, osha) — 2 часа
  2. Простой lookup_tool.py только с _by_adilet_id индексом — 1 час
  3. Интеграция в pipeline только для случая len(matches) == 1 — 2 часа
  4. Без metadata enrichment. Вместо этого: если матч найден, добавить в query expansion полный title документа — у обычного retrieval больше шансов найти правильные чанки

Full (после metadata-enrich re-extract) — покрывает ~80%

  1. Все 11 регулярок
  2. Все индексы
  3. fetch_chunks_by_doc_ids с metadata-фильтром — жёсткая привязка к doc_id
  4. Multi-match handling («приказ 211» → 3 варианта)
  5. Refusal handling (№999 → честный отказ)

Диагностика через golden_retrieval_v1

Наш golden_retrieval_v1.jsonl (83 теста) уже разработан под эту задачу:

  • Категория A_direct_number (25) — чистый exact-match. Прямой тест эффективности lookup_tool.
  • Категория E_edge (8) — тройки 211/311/411 и 156/158/162 — диагностический инструмент на спутанные номера.
  • Категория B_code_article (17) — статьи кодекса.
  • Категория D_mixed (13) — номер + контекст.

Прогон до/после lookup_tool покажет реальный эффект в цифрах.

Перестановка приоритетов

Было (в strategy_2026): 1. bge-m3 fine-tune — главный апгрейд retrieval 2. Tool calling rewrite — следующий этап

Стало (после этого осознания): 1. lookup_tool MVP (1 день после миграции) — решает главную боль быстро 2. metadata enrichment в re-extract (суббота) — обязательно 3. lookup_tool Full версия (5-7 мая) — все 11 регулярок + multi-match 4. bge-m3 fine-tune — остаётся в субботнем плане, но как вторая приоритетная задача. Даёт выигрыш на семантическом retrieval и KZ-cross-lingual, а lookup_tool — на exact-match. 5. Tool calling rewrite (16-17 мая) — lookup_tool фактически уже это приотип tool. /incident agent loop будет использовать эту же архитектуру.

Источник

Архитектура предложена другой сессией Claude Opus 4.7 после анализа реестра и осознания что главная боль Люми — не KZ-запросы, а exact-match по номерам.

Связанные

  • [[lyumi/grounding_fix_apr17]] — существующий пост-retrieval grounding (решает фантомы, но не retrieval)
  • [[lyumi/document_registry]] — registry_v2.json откуда берём доки
  • [[lyumi/migration_ax41]] — миграция перед которой фиксируем
  • [[lyumi/strategy_2026]] — обновить roadmap с учётом этого