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-запросов
query_parser.pyс 4 регулярками (adilet_id, order_number, code_article, osha) — 2 часа- Простой
lookup_tool.pyтолько с_by_adilet_idиндексом — 1 час - Интеграция в pipeline только для случая
len(matches) == 1— 2 часа - Без metadata enrichment. Вместо этого: если матч найден, добавить в query expansion полный title документа — у обычного retrieval больше шансов найти правильные чанки
Full (после metadata-enrich re-extract) — покрывает ~80%
- Все 11 регулярок
- Все индексы
fetch_chunks_by_doc_idsс metadata-фильтром — жёсткая привязка к doc_id- Multi-match handling («приказ 211» → 3 варианта)
- 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 с учётом этого