Архив рубрики ~Лента новостей~

Что извлекает парсер вопросов из пользовательской строки: ключевые слова, область действия, структура, декомпозиция, уточнение.

Что извлекает парсер вопросов из пользовательской строки: ключевые слова, область действия, структура, декомпозиция, уточнение.
Что извлекает парсер вопросов из пользовательской строки: ключевые слова, область действия, структура, декомпозиция, уточнение.

Enterprise Document Intelligence [Том 1 #6b] – Пять семейств полей, которые парсер считывает непосредственно из вопроса пользователя, вместе с кодом, который заполняет каждое из них.

Делиться

Фотография Мерве Баяр, Pexels.

Эта статья — вторая часть цикла статей о синтаксическом анализе вопросов в рамках серии «Интеллектуальное управление документами предприятия», которая строит корпоративную систему RAG из четырех компонентов: синтаксический анализ, синтаксический анализ вопросов, поиск и генерация. В статье 6a (тезис) обосновывалась необходимость синтаксического анализа вопроса и демонстрировались два типа запросов потребителей, на которые разбивается проанализированная строка. В этой статье рассматривается , что синтаксический анализатор извлекает из пользовательской строки: ключевые слова, ожидаемую форму и тип ответа, подсказки по области действия, декомпозицию для составных вопросов и поле уточнения для слишком расплывчатых входных данных, на которые нельзя воздействовать. В статье 6c (диспетчеризация) рассматривается, какое решение принимает синтаксический анализатор на основе этих полей, используя профиль документа.

67f55e4394dbc267a43b8300abb5a60b
Место данной статьи в серии: Статья 6 (анализ вопросов), часть, посвященная извлечению информации, внутри Части II (четыре кирпича) – Изображение предоставлено автором

Пользователь вводит одну строку: «Какова максимальная сумма покрытия? Не путайте её с франшизой, они часто указываются вместе». Парсер преобразует её в строку типизированных столбцов: тема, ожидаемая форма ответа (сумма), подсказка области действия (этот контракт), отрицательный сигнал (не франшиза), направляемый в бриф генерации, и подсказка макета (часто указываются вместе), которую может использовать поиск. Каждый элемент становится отдельным столбцом в question_df . В этой статье рассматриваются пять семейств полей по очереди, с кодом, который заполняет каждое из них, и типизированной схемой, которая его содержит.

cf6c6463f78955aee72a51269dcc9da0
В результате анализа вопроса создается одна строка в таблице question_df плюс дополнительные таблицы, а для извлечения и генерации данных используются два производных представления. – Изображение предоставлено автором.

1. Пять семейств полей, которые заполняет парсер.

Вопрос — это больше, чем просто слова. Он также указывает, какую форму должен принять ответ, где искать его в документе, является ли он составным или слишком расплывчатым, чтобы на него реагировать. Парсер фиксирует каждый из этих параметров и записывает его в виде столбца в question_df . Читайте остальную часть статьи как меню доступных функций, а не как контрольный список.

Колонны делятся на две группы.

То, что парсер считывает из самого вопроса.

  • Ключевые слова: Токены для поиска. Используются несколько источников: явные (названия пользователя), прямые (извлеченные из вопроса), переформулировки LLM, экспертный словарь понятий и высокоэффективные регулярные выражения, такие как L131-1 .
  • Форма и тип ответа: Две ортогональные оси: ожидаемая мощность множества ( single , listing , table , tree , nested_json ) и тип значения ( text , amount , date , iban , address , …).
  • Область поиска: Где в документе искать: на странице, в главе, в разделе, в структуре документа (таблица/изображение), в диапазоне дат, в юрисдикции.
  • Разложение: Подвопросы, когда вопрос является составным.
  • Уточнение: короткий уточняющий вопрос, когда полученная информация слишком расплывчата, чтобы на нее можно было отреагировать.

Затем парсер принимает решение (используя профиль документа в дополнение к вышеизложенному).

  • Диспетчеризация: какой объем окружающего контекста следует прочитать и вернуть, какую стратегию обработки фрагментов использовать, какую модель вызвать. Все это определяется типом ответа, соответствующей концепцией и настройками проекта по умолчанию.
  • Активации: Какие блоки следует запускать (навигация по оглавлению, встраивание текста, перекрестные ссылки и т. д.), в зависимости от того, что поддерживает документ.

Каждая категория становится одним или несколькими столбцами в question_df . Проекты выбирают то, что им нужно, пропускают остальное и добавляют новые столбцы по мере появления типов сбоев: policy_number для страхового брокера, patient_id для медицинской RAG, regulation_year для юридических вопросов. Подразделы обрабатывают каждый из них.

1.1 Ключевые слова

Для поиска нужны слова, по которым будет производиться поиск в документе. Парсер выбирает их из предложенных вариантов и предоставляет. Формулировка пользователя почти никогда не совпадает с формулировкой документа с первого раза, поэтому парсер собирает данные из нескольких источников одновременно.

Вот минимальная схема, которую мы будем расширять по мере изучения этого раздела:

 class ParsedQuestion(BaseModel): original_question: str keywords: list[str]

В ответ на запрос «Каков максимальный объем покрытия?» парсер выдает следующий результат:

 ParsedQuestion( original_question="What is the maximum coverage amount?", keywords=["maximum", "coverage", "amount"], )

Ключевые слова наследуют все опечатки, содержащиеся в вопросе. Извлекаются токены непосредственно из запроса «Как многоголовое внимание соотносится с самовниманием ?», и поиск выполняет поиск по слову atention , которое никогда не встречается в документе. Результатов нет, система ничего не возвращает, пользователь делает вывод, что тема не освещена. Решение — это простой предварительный шаг, который выполняется перед извлечением ключевых слов: один вызов LLM, который исправляет опечатки и грамматику, не изменяя смысла, поэтому ключевые слова получаются чистыми.

 def correct_spelling(question: str) -> str: """Fix typos and grammar without changing meaning.""" prompt = f"""Fix any typos and grammar mistakes in the question below. Do not change the meaning. Do not add or remove information. Return only the corrected question, nothing else. Question: {question}""" resp = client.responses.create(model="gpt-4.1-mini", input=prompt) return resp.output_text.strip()

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

Позвольте пользователям самим задавать ключевые слова. Некоторые пользователи (аналитики, помощники юристов, все, кто хорошо знаком с лексикой документа) уже точно знают, какие термины они хотят сопоставить. Подсказка в пользовательском интерфейсе: «Укажите точные термины для поиска, разделенные запятыми» открывает наиболее точный путь поиска, доступный системе. Токены пользователя вводятся дословно: вес 1.0, direct источник, без LLM, без расширения синонимов (если пользователь не выберет этот вариант). Для запроса «Пожалуйста, найдите 'форс-мажор', 'расторжение', 'событие неисполнения обязательств' в этом договоре» парсер извлекает три цитируемые фразы как есть. Это быстрее, дешевле и точнее, чем любая перефразировка LLM, когда пользователь может указать термины. Важна и продуктовая составляющая: поле «поисковые термины» рядом с полем вопроса или подсказка системы («укажите точные термины, которые вы хотите сопоставить») перенаправляет значительную долю запросов на этот путь.

Когда пользователь не указывает термины явно, в качестве источников на стороне парсера используются три компонента: переформулировки LLM, словарь понятий экспертов и якорные регулярные выражения.

Несоответствие терминологии — это первое, что начинает давать сбой. Пользователь спрашивает о «пределе суммы, которую выплатит страховщик», а в документе указано «лимит возмещения за каждый страховой случай». Такое несоответствие встречается повсюду на предприятиях:

  • Страхование: «предельная сумма выплат страховщика» → «лимит возмещения убытков за каждый страховой случай».
  • Юридический аспект: «что произойдет, если мы досрочно расторгнем договор» → «положения о досрочном расторжении договора» или «права на расторжение договора».
  • Финансы: «сколько нам вернут» → «график погашения основной суммы долга» или «условия погашения».
  • Медицинский термин: «побочные эффекты» → «нежелательные явления» или «противопоказания».

В столбце ключевых слов указаны неверные токены; поиск ничего не находит. Три источника в совокупности заполняют его терминами, используемыми в документе.

Источник A: Переформулировка вопросов в рамках программы LLM: переформулируйте вопрос в 3-5 вариантов, соответствующих вероятной формулировке ответа в документе. Хитрость заключается в создании языка, окружающего ответ, а не самого ответа. (В этом и заключается идея HyDE (Hypothetical Document Embeddings): создание правдоподобного фрагмента текста и его последующее встраивание, а не непосредственное встраивание вопроса.)

 # src/question/rewrite.py def rewrite_query(question: str, domain_hint: str = "") -> list[str]: """Rewrite a user question into 3-5 queries phrased the way the relevant passage is likely to appear in the document.""" prompt = f"""You are translating a user question into search queries that match how the answer would be phrased in a {domain_hint or 'professional'} document. Return 3 to 5 alternative phrasings. Use vocabulary the document is likely to use, not the user's casual phrasing. Output one phrasing per line, no numbering. User question: {question}""" resp = client.responses.create(model="gpt-4.1-mini", input=prompt) return [line.strip() for line in resp.output_text.splitlines() if line.strip()]

Для запроса «Что произойдет, если мы расторгнем договор досрочно?» с domain_hint="commercial contract" LLM переформулирует его в пять вариантов, которые, вероятно, будут использоваться в документе: положения о досрочном расторжении, условия расторжения соглашения до окончания срока его действия, расторжение по взаимному согласию, плата за расторжение и штрафы за досрочное расторжение, права на расторжение договора и требования к уведомлению.

В наших развертываниях небольшая переработка кода, основанная на domain_hint неизменно дает больший эффект при оценке каждого режима отказа, чем выбор следующей модели встраивания в таблице лидеров. Это недорого в добавлении, но легко отменить, если это негативно скажется на результате.

Источник B: экспертный словарь, наши первые вспомогательные таблицы. Переписывания LLM обрабатывают стандартную лексику. Они не обрабатывают «премия» → «прайм» (французское страхование), «котизация» (общества взаимного страхования) или «DDPE» (специфический код страхового продукта), поскольку LLM редко встречал эти соответствия в своих обучающих данных. Эксперты в предметной области знают значимые синонимы в своей области. Они поддерживают их в двух вспомогательных таблицах, которые вместе образуют словарь ключевых слов проекта. Именно здесь система усиливает экспертные знания в масштабе: год за годом собранные ими синонимы накапливаются в реляционный актив, которым владеет проект, который может расширяться и который может проверяться в любое время.

В первом разделе представлены сами понятия: по одной строке на каждое понятие, с его определением и семейством документов, к которому оно принадлежит.

98b4fe7a984c4e5f1f46dfbbceafcf07
Концепции, а также параметры диспетчеризации по умолчанию для каждой концепции, которые переопределяют параметры по умолчанию для типа ответа – Изображение предоставлено автором

Второй тип данных содержит варианты ключевых слов: по одной строке на каждый параметр (concept, language, keyword) , объединенные с concepts_df по столбцу concept .

25dc1721a368270a6b7f3f9088563170
Одна строка на каждый (концепт, язык, ключевое слово) – Изображение предоставлено автором

Столбец language позволяет словарю работать с корпусами, содержащими словари на разных языках. Представьте себе французскую страховую компанию, контракты которой поступают на французском, английском, а иногда и испанском языках. Без этого столбца парсер выдал бы неверные варианты. Параметр keyword_priority разделяет сильные совпадения ( primary ) от более слабых ( secondary ); weight — это числовой параметр, используемый непосредственно в лексической оценке. Такое разделение понятий и ключевых слов позволяет хранить метаданные каждого понятия (определение, тип документа) в одном месте, вместо того чтобы повторять их в каждой строке ключевого слова.

Как парсер использует две таблицы: Когда ключевое слово в вопросе совпадает со строкой в concept_keywords_df , парсер ищет это понятие и извлекает все варианты (все языки, все приоритеты). Затем поиск выполняется одновременно для всех вариантов. Если ключевое слово может подходить к нескольким понятиям ( prime может быть страховой премией, бонусом или основным числом), парсер запрашивает у LLM выбор, передавая столбец definition из concepts_df для каждого кандидата:

 def disambiguate_concept( question: str, candidates: pd.DataFrame, *, system_prompt: str = ( "Pick the concept that best fits the user's question. " "Reply with the concept name only, no explanation." ), ) -> str: """`candidates`: rows from concepts_df that share a matched keyword.""" options = "n".join( f"- {r['concept']}: {r['definition']}" for _, r in candidates.iterrows() ) user_msg = f"Question: {question}nnCandidates:n{options}" resp = client.responses.create( model="gpt-4.1-mini", input=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}], ) return resp.output_text.strip()

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

По мере развития проекта обе таблицы расширяются: каждая неудачная попытка поиска, связанная с несоответствием словарного запаса, становится новой строкой.

Эмбеддинги помогают находить кандидатов: встраивают каждое уникальное именное словосочетание в корпус, кластеризуют (HDBSCAN, размер кластера около 5 минут), находят кластеры, которые перекрываются с существующими ключевыми словами известного понятия, и позволяют эксперту решить, какие новые варианты следует включить. Поиск считывает проверенную таблицу, а не исходные эмбеддинги. Эксперт проверяет каждую запись, прежде чем она попадет в словарь. Именно это обеспечивает надежное соответствие системы предметной области.

В стандартных руководствах RAG предполагается, что встраивание сходства автоматически обрабатывает синонимы. Для некоторых областей это так. Для специализированной корпоративной лексики — нет.

Здесь проявляется редакционная позиция серии по поводу эмбеддингов. Обычно выбирают «лучшую» модель эмбеддингов, измеряя показатель полноты поиска (recall@k) на размеченном наборе запросов/документов, а затем используют эту модель для сопоставления синонимов. Мы поступаем наоборот: определяем синонимы с помощью проверенного словаря, используем эмбеддинги в качестве запасного варианта для случаев, которые словарь еще не охватывает, и используем их в качестве инструмента поиска, а не основного сигнала для поиска. В блоке поиска подробно описано, где именно эмбеддинги находятся в воронке.

Когда множество значений замкнуто, перечислите его. Некоторые понятия имеют конечный, известный список значений: названия стран, коды валют, названия штатов США с аббревиатурами, коды страховых продуктов, названия лекарств из справочника компании, марки и модели автомобилей, которые продает брокер. Для них словарь перестает органически расти и превращается в одноразовую массовую вставку: перечислите каждое значение, каждое распространенное написание, каждый перевод, каждую аббревиатуру, каждый вариант, встречающийся в реальных документах.

Возьмем страны. Если пользователь спрашивает: «Какое покрытие есть в Германии?», документ почти наверняка содержит Germany , Allemagne , Deutschland , DE или DEU в качестве буквального обозначения. Нет никакой закономерности, которую можно было бы обнаружить, нет регулярного выражения, которое бы точно отражало принадлежность к стране среди 195 стран. Решение состоит в том, чтобы загрузить все эти страны (или подмножество, используемое корпусом) в concept_keywords_df с concept = "country" , по одной строке на каждую страну (язык, орфография). Любое совпадение в вопросе сообщает парсеру, что пользователь спрашивает о стране, и область поиска определяется соответствующим образом.

Контраст с answer_types_df разительный. Суммы, даты, IBAN, проценты — все они имеют общие структурные закономерности, которые улавливаются регулярными выражениями. Названия стран не имеют общей структуры. Две вспомогательные таблицы решают два разных типа задач: одна для объектов с закономерностями, другая для замкнутых наборов, которые можно перечислить от начала до конца.

Источник C: ключевые слова-якоря: Иногда пользователь предоставляет вам токены, которые слишком важны, чтобы рисковать их потерей при расчете среднего значения встраивания: внутренние коды продуктов, ссылки на нормативные документы, номера пунктов, идентификаторы.

«Применяется ли здесь статья L131-1 страхового кодекса?»

Токен «L131-1» представляет собой весь запрос. Если вы встроите всё предложение целиком, этот токен будет разбавлен словами «article», «insurance code», «apply here». Извлеките наиболее информативные токены и направьте их в лексический индекс: BM25, классический алгоритм оценки ключевых слов, который придает больший вес редким терминам, — вместе с запросом на встраивание.

 # src/question/keywords.py import re ANCHOR_PATTERNS = [ r"b[AZ]+d+(?:[-/]d+)*b", # L131-1, ISO-9001, RC-2024 (any number of leading caps) r"b[AZ]{2,}.[AZ]{2,}(?:-d+)?b", # NIST-style codes: ID.AM, PR.AC-1 r"b[AZ]{3,}b", # GDPR, RCP, SLA r"bd{4,}b", # year, identifier numbers ] def extract_anchor_keywords(question: str) -> list[str]: """Extract high-signal tokens for lexical retrieval.""" found: list[str] = [] for pattern in ANCHOR_PATTERNS: found.extend(re.findall(pattern, question)) return list(dict.fromkeys(found)) # de-dup, preserve order

В результате анализа данных объединяются три источника:

 class Keyword(BaseModel): text: str weight: float = 1.0 source: Literal["direct", "llm_expansion", "expert_dictionary", "anchor"] semantic_group: str | None = None is_regex: bool = False class ParsedQuestion(BaseModel): original_question: str keywords: list[Keyword] # now structured

Почему работает HyDE и почему явные ключевые слова обеспечивают тот же выигрыш. Диагностика на реальном примере, описанная в статье 2 (режимы сбоев встраивания), раздел 3.2, показала, что исходный запрос «как мне отменить свой полис» проигрывает лексической приманке. Переписанный с помощью HyDE запрос добавил rescission, terminate, written notice, renewal , и целевой запрос выиграл с преимуществом в 0,169. Механизм, лежащий в основе этого эффекта, в точности совпадает с тем, что уже делают приведенные выше таблицы-спутники, без обратной связи с встраиванием.

При работе HyDE одновременно запускаются три механизма:

  1. Расширение ключевых слов и синонимов: LLM, генерируя гипотетический ответ, естественным образом использует лексику предметной области, которой не хватает в вопросе. Например, на вопрос «Как расторгнуть договор досрочно?» выдается ответ: «досрочное расторжение, срок уведомления, плата за расторжение, письменное уведомление». Это слова, которые содержатся в реальных текстах. Встраивание HyDE их фиксирует, а поиск находит соответствующие совпадения.
  2. Сопоставление регистров: гипотетический ответ принимает регистр документа (формальный, технический, предметно-ориентированный). Разговорный регистр вопроса заменяется регистром ответа. Расстояние в пространстве вложений сокращается. Реальный механизм, но меньший, чем механизм 1, применяется к корпоративным корпусам с ограниченным словарем.
  3. Скрытые семантические ассоциации: LLM активирует обученные ассоциации, которые не являются лексическими. Это наиболее часто упоминаемая в литературе причина повышения эффективности HyDE, но наименьший вклад в ограниченной области.

В контексте предприятий (одна ограниченная область: страхование, юриспруденция, медицина) механизм 1 объясняет большую часть преимуществ HyDE. Ключевые слова, которые LLM генерирует в гипотетическом ответе, в точности совпадают с теми, которые используются в документе. Вымышленный текст и документ используют один и тот же словарь. Этап встраивания фиксирует этот общий словарь, и ничего больше.

Если механизм 1 доминирует, прямое извлечение ключевых слов обеспечивает тот же эффект при меньших затратах. Отпадает необходимость в двустороннем встраивании. Поиск становится проверяемым (эксперт видит, какие ключевые слова совпали). Проект создает постоянный ресурс (словарь в concept_keywords_df ) вместо того, чтобы заново генерировать гипотезу при каждом запросе.

Именно поэтому явное извлечение ключевых слов превосходит HyDE в контекстах, на которые ориентирована эта серия: ограниченная предметная область, наличие эксперта, необходимость проверки результатов поиска. В поиске для потребителей в открытой предметной области без этих ограничений HyDE сохраняет преимущество, поскольку механизмы 2 и 3 имеют больший вес.

На практике сравнение простое. HyDE запрашивает переписывание, встраивает его, запускает косинусное преобразование, один цикл обработки на запрос, без журнала аудита. Описанный выше подход делает один вызов LLM для извлечения ключевых слов, ищет их в словаре, а журналом аудита являются сами найденные строки. Словарь можно использовать повторно в разных запросах, и он растет вместе с проектом.

1.2. Обозначение формы ответа и типа ответа.

Даже при наличии большого количества ключевых слов поиск может выдавать фрагменты текста, которые соответствуют словам, но не содержат фактического ответа. Например, запрос «Какова годовая премия?» может соответствовать предложению о качестве премии без указания денежной суммы. Или же он может соответствовать фрагменту текста о несвязанном простом проценте (бонус, первичный процент). Система не может отличить правильный результат от неправильного, потому что не знает, какой именно ответ она ищет.

Решение состоит в том, чтобы пометить каждый вопрос по двум независимым осям: answer_shape (как представлен ответ: одно значение? список? таблица?) и answer_type (что содержит каждое значение: текст? сумма? дата?). «Какова годовая премия?» — это (single, amount) . «Перечислите годовые премии по годам» — это (listing, amount) . «Перечислите исключения из договора» — это (listing, text) . Один и тот же тип может передаваться под любой формой, и одна и та же форма может содержать любой тип. Разделение этих двух осей позволяет парсеру помечать каждый вопрос отдельно.

Возьмем, к примеру, суммы: вопрос типа amount запускает проверку с помощью регулярных выражений параллельно с поиском по ключевому слову, сканируя числовые токены с символами валют ( d[ds.,]*s*(?:EUR|€|USD|$) ). Запрос «Какова годовая премия?» теперь соответствует строке «Prime annuelle: 125 000 €» сразу по двум сигналам : ключевое слово prime присутствует, И регулярное выражение для суммы соответствует 125 000 € в той же строке. Совпадение по двум сигналам намного надежнее, чем совпадение только по ключевому слову. И наоборот, если поиск находит совпадения по ключевому слову, но нигде в рассматриваемых фрагментах не обнаруживается денежная сумма, ответ, вероятно, отсутствует в документе; система может с уверенностью вернуть «сумма не найдена», вместо того чтобы гадать.

Та же логика применима и к датам: запрос «Когда начинается действие страхового полиса?» ожидает date . Поиск сканирует поля с указанием шаблонов дат (ISO, локаль, текстовая информация) наряду с ключевыми словами, такими как date d'effet, commencement, start. Если в поле ключевых слов содержится понятная дата, это почти наверняка правильный ответ; в противном случае, поле отсутствует в договоре.

Типы данных открыты: вы можете зарегистрировать всё, для чего можете дать имя и написать регулярное выражение (или проверку LLM): text , amount , date , boolean , email , iban , policy_number , siren , percentage , duration , address и т. д. Реестр находится в answer_types_df , по одной строке на каждый зарегистрированный тип:

042bd4e50cc5eda4135ad2389c73fdfd
Потиповая регистрация по оси значений – Изображение предоставлено автором

Столбец retrieval_patterns (хранится в активном DataFrame, опущен на изображении из-за ширины) используется для подтверждения типа при извлечении данных. Столбец output_schema_ref указывает на класс Pydantic, в который выполняется рендеринг; блок генерации отвечает за эту сторону. Столбец default_model — это модель, к которой парсер возвращается, когда не применяется переопределение на уровне концепции: небольшие типы (amount, date, iban) относятся к модели nano , текст произвольной формы — к mini . Добавление нового типа в проект осуществляется одной операцией вставки. Все, что парсер не может классифицировать, возвращается к text и пропускает подтверждение с помощью регулярных выражений.

Ось формы замкнута и очень мала: пять значений охватывают то, что мы видели в реальных корпусах: single (одно значение, значение по умолчанию), listing (плоское перечисление), table (строки × столбцы), tree (вложенная иерархия), nested_json (структурированный объект с именованными подполями, например, адрес в формате {street, city, zip} ). Реестр настолько мал, что находится в дочернем сателлите с двумя столбцами значений по умолчанию:

24ca9b615bc0a612b6d84766fe7072a1
Настройки по умолчанию для каждой фигуры по оси кардинальности – Изображение предоставлено автором

Отдельные факты почти всегда располагаются на одной строке в блоке с наивысшим рейтингом, поэтому sequential размещение экономит ⅔ токенов при k=3; списки, таблицы и деревья необходимо синтезировать в разных фрагментах, поэтому combined является более безопасным вариантом по умолчанию. Разделение позволяет вопросам «Какова годовая премия?» (вопрос типа (single, amount) ) и «Перечислите годовые премии по годам» (вопрос типа (listing, amount) ) использовать один и тот же тип ( amount , одно и то же регулярное выражение, один и тот же синтаксический анализ значений), но при этом маршрутизировать их по-разному во время генерации.

 class ParsedQuestion(BaseModel): original_question: str keywords: list[Keyword] answer_shape: Literal["single", "listing", "table", "tree", "nested_json"] = "single" answer_type: str = "text" # FK into answer_types_df

Метка является свойством вопроса, а не документа . «Какова сумма премии?» — это вопрос (single, amount) независимо от того, состоит ли договор из двух страниц или двухсот. Два поля независимы: «Перечислите исключения» — это (listing, text) , «Перечислите годовые премии по договору» — это (listing, amount) .

Shape — это замкнутое перечисление (пять значений, фиксированных для проекта); type — открытое (одна строка на каждый зарегистрированный тип в answer_types_df ). Сама классификация включена в объединенный вызов parse_question , описанный в статье 6_c (dispatch), при этом оба реестра внедряются в запрос LLM, поэтому добавление нового типа или новой формы представляет собой вставку строки, а не изменение кода.

1.3 Область применения: где искать в документе

В вопросе часто указывается, где именно в документе следует искать. Парсер фиксирует эти подсказки в двух типизированных полях, которые применяются до начала поиска по ключевым словам. StructuralHints содержит структурные подсказки (страница, раздел оглавления, макет). ScopeFilters содержит фильтры на уровне корпуса: прикладной уровень может передавать их (если ему известна юрисдикция пользователя, диапазон дат и т. д.), или парсер может извлекать их из вопроса, если он явно указывает один из них.

  • Страницы: «Показать страницу 3», «Свести страницы с 5 по 7», «Сравнить страницу 2 и страницу 9». Поиск по одной странице, диапазону и явному списку сводится к плоскому списку целых чисел. pages_hint = [3] , pages_hint = [5, 6, 7] , pages_hint = [2, 9] . Затем поиск фильтруется одним выражением ( page_df[page_df.page_num.isin(pages_hint)] ) и не разветвляется в зависимости от формы подсказки. Страницы с подсказками сохраняются, даже если ни одно ключевое слово им не соответствует: пользователь явно их закрепил, то есть это поверхность ответа.
  • Глава/раздел: «Какие исключения предусмотрены данным договором?». Указывает на запись в оглавлении. toc_section_hint = "Exclusions" . Результат поиска соответствует фактическому оглавлению документа.
  • Макет: «таблица расписания в конце». Ответ находится в таблице, изображении или заголовке. layout_hint = "table" . Указывает механизму поиска обращать внимание на структурированные зоны, а не на повествовательный текст.
  • Диапазон дат / стороны / юрисдикция: «Что мы подписали с Acme в период с 2022 по 2024 год?». Фильтры на уровне корпуса, которые отсеивают документы-кандидаты перед выполнением поиска (обрабатывается индексом на уровне корпуса перед обработкой документов). ScopeFilters(date_range=..., parties=["Acme"]) .

Та же самая конвенция распространяется и на другие форматы. sheets_hint: list[str] содержит названия листов Excel, закрепленных в вопросе («на листе с ценами»); slides_hint: list[int] содержит номера слайдов PowerPoint («на слайде 4», «слайды с 7 по 9»). Том 2 использует оба варианта. Объединяющая идея заключается в том, что формулировка пользователя определяет область действия. Стоит отметить следующее: в коротких документах (резюме, одностраничный счет-фактура, служебная записка на 1-2 страницы) оператор закрепляет единственную страницу ( page 1 ) внутри вопроса, и конвейер выполняется без изменений. Нет режима «короткого документа», нет переключения стратегии фрагментации, нет обхода поиска. Тот же путь выполнения кода, который обрабатывает запрос к корпусу из 1000 страниц, оказывается ограниченным одностраничным документом. Статья 8 (генерация) развивает эту конвенцию со стороны диспетчера.

 class ScopeFilters(BaseModel): sections: list[str] = Field(default_factory=list) date_range: tuple[str, str] | None = None parties: list[str] = Field(default_factory=list) jurisdictions: list[str] = Field(default_factory=list) page_range: tuple[int, int] | None = None custom: dict = Field(default_factory=dict) class StructuralHints(BaseModel): # WHERE the answer lives toc_section_hint: str | None = None # The likely TOC section or chapter ("Exclusions", "Schedule A"). # Retrieval matches against the document's actual TOC. pages_hint: list[int] | None = None # Pages pinned by the question. Single ("page 3" -> [3]), # range ("pages 5 to 7" -> [5, 6, 7]), or list ("page 2 and 9" # -> [2, 9]) all collapse to a flat list at parse time. sheets_hint: list[str] | None = None # XLSX, Volume 2 slides_hint: list[int] | None = None # PPTX, Volume 2 layout_hint: Literal["text", "table", "image", "header"] | None = None document_version: str | None = None # HOW MUCH context to read and return (retrieval consumes these) detection_context: Literal["line", "sentence", "paragraph"] = "line" # Granularity of the regex confirmation zone (section 2.2). # "line" for amount/date, "paragraph" for narrative. answer_context: Literal["line", "paragraph", "page", "section", "chapter", "document"] = "paragraph" # How much surrounding text the generator receives. needs_summary: bool = False # True when the answer spans more than fits in a verbatim quote.

chunk_strategy и suggested_model также являются решениями, принимаемыми для каждого вопроса отдельно, но они не носят структурного характера (они не описывают документ, а описывают, как конвейер вызывает LLM). Они находятся на верхнем уровне ParsedQuestion , а не в StructuralHints , и соответствующее поле (Article 6_c) проходит по каскаду, который их заполняет. То же самое относится к трем полям, хранящимся в StructuralHints ( detection_context , answer_context , needs_summary ), которые описывают, какой объем текста обрабатывается регулярным выражением и считывается генератором, а не где находится ответ. В Article 6_c также рассматриваются их значения по умолчанию.

Вот несколько примеров того, что выдает парсер:

d2e9800fad72598b29f7fb5bf70d2bc7
Пять примеров вопросов и контекстные столбцы, которые заполняет парсер – Изображение предоставлено автором

Обнаружение осуществляется одним вызовом LLM со структурированным выводом Pydantic. Регулярные выражения были заманчивы (catch “page 3” with r"pages+(d+)" ) и работают для нескольких тривиальных случаев, но дают сбой в остальных: “в разделе гарантии” (модификатор перед существительным), “таблица сводки в конце” (без номера), “глава об ответственности” (синоним), “приложение” (без ключевого слова). LLM охватывает все эти случаи за один цикл, возвращает чистый вывод Pydantic и остается удобным для сопровождения.

 # src/question/hints.py HINTS_PROMPT = ( "Read the user's question and extract structural hints about WHERE the answer " "lives in the document AND HOW MUCH context the answer needs.nn" "- toc_section_hint: the section or chapter the user pointed at, matched against " "typical document TOC entries (eg 'Exclusions', 'Limits', 'Schedule A'). null if " "no section is implied.n" "- pages_hint: flat list of page numbers the user pinned. Single ('page 3' -> [3]), range ('pages 5 to 7' -> [5, 6, 7]) and list ('page 2 and 9' -> [2, 9]) all collapse to a list. null otherwise.n" "- layout_hint: 'table' / 'image' / 'header' if the question implies a layout.n" "- detection_context: granularity of the regex confirmation zone. 'line' for a " "single fact, 'sentence' for short prose, 'paragraph' for narrative.n" "- answer_context: how much surrounding text the generator receives. 'line' for " "a single value, 'paragraph' for an explanation, 'page' for a recap, 'section' " "for a topic, 'chapter' or 'document' for a broad summary.n" "- needs_summary: True if the answer spans more than fits in a verbatim quote." ) def extract_hints(question: str, *, system_prompt: str = HINTS_PROMPT) -> StructuralHints: resp = client.responses.parse( model="gpt-4.1-mini", input=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": question}, ], text_format=StructuralHints, ) return StructuralHints.model_validate_json(resp.output_text)

Подсказки относительно расположения текста важнее, чем кажется на первый взгляд. Если пользователь говорит: «Обычно это находится на изображении», это очень важная подсказка. Большинство парсеров удаляют изображения или заменяют их заполнителями. Знание того, что ответ находится на изображении, подсказывает вам посмотреть на результат распознавания текста на рисунках или пометить вопрос для обработки с помощью компьютерного зрения и языка. Без подсказки конвейер будет искать только в тексте и ничего не найдет.

Один вызов LLM на каждый аспект или один вызов LLM в общей сложности? В статье каждый аспект рассматривается отдельно, чтобы вы могли понять (и протестировать) каждый компонент по отдельности. Так же строится и конвейер обработки запросов: по одному вспомогательному компоненту за раз, проверяя каждый столбец перед добавлением следующего. В производственной среде, как только вы убедитесь в правильности схемы, вы объединяете все в один консолидированный вызов: один цикл обработки, один запрос, одно место, где LLM имеет полный контекст. В статье 6_c (диспетчеризация), раздел 3.1, показан консолидированный parse_question от начала до конца.

На уровне одного документа фильтры области поиска ограничивают область поиска в документе. На уровне корпуса (Часть IV) они становятся SQL-запросами к индексу корпуса. Та же идея, другой механизм.

1.4 Составные вопросы

На некоторые вопросы нельзя ответить, просто проанализировав какой-либо фрагмент текста, как бы хорошо вы ни сформулировали запрос. Они содержат множество подвопросов, объединенных в один.

«Соответствуют ли в данном контракте ограничения по возмещению убытков и ответственности?»

Сравнение. Найдите пункт о возмещении убытков, найдите пункт об ограничении ответственности, а затем сравните их.

«Содержит ли данный договор пункт о неконкуренции, и если да, то на какой срок?»

Условный вопрос. Шаг первый: существует ли соглашение о неконкуренции? Шаг второй (только если да): какова продолжительность действия соглашения?

«Каков размер годовой страховой премии и каковы основные исключения из страхового покрытия?»

Два несвязанных факта, соединенных союзом «и». Разные отрывки текста, независимые ответы.

Четыре закономерности встречаются достаточно часто, чтобы их назвать.

Независимый факт: Два несвязанных факта, соединенных союзом «и»: «Что такое премия и какие есть исключения?» Оркестратор запускает pdf_qa дважды параллельно; слияние осуществляется просто по ключу {"sub_questions": [{"q": ..., "answer": ...}, ...]} полученному из разобранного подвопроса.

Последовательный подход: Вторая часть зависит от первой: «Кто является застрахованным лицом и каков его адрес?» Адрес — это адрес застрахованного лица: сначала нужно определить это лицо, а затем найти его адрес. Организатор последовательно обрабатывает подвопросы и подставляет предыдущий ответ в ключевые слова следующего подвопроса.

Единый подход: два термина, обозначающие одно и то же понятие, а не два вопроса: «Каковы исключения и ограничения?» В большинстве нормативных документов исключения и ограничения фигурируют вместе в одном разделе. Разделение этого на два подвопроса поможет избежать дублирования. Лучше оставить это как один вопрос, при этом оба термина будут повышены в результатах поиска по ключевым словам.

Условное условие: Условие сужает область действия: «Если полис распространяется на коммерческую недвижимость, каков лимит страхового покрытия от пожара?» Условие становится фильтром области действия; фактический вопрос звучит так: «Каков лимит страхового покрытия от пожара?», и применяется к подмножеству документа, которое соответствует условиям.

Простой эмпирический способ определения шаблона — это тест «и»: замените «и» на «; также». Если текст по-прежнему читается естественно, части независимы . Если читается неестественно, они объединены . В более сложных случаях используется классификация LLM. has_compound_indicators — это простой предварительный фильтр (регулярное выражение, ищущее bandb , borb , ?...? , множественные императивы); llm_classify_decomposition — это вызов структурированного вывода, возвращающий объект Decomposition :

 class Decomposition(BaseModel): pattern: Literal['single', 'independent', 'sequential', 'unified', 'conditional'] = 'single' sub_questions: list[str] = Field(default_factory=list) conditional_filter: dict | None = None def decompose(question: str) -> Decomposition: if not has_compound_indicators(question): return Decomposition(pattern='single') return llm_classify_decomposition(question)

Разложение увеличивает задержку и стоимость. Сначала определите составную структуру; разлагайте только тогда, когда этого требует закономерность.

В одной из производственных версий примерно 30% вопросов пользователей в первый месяц бета-тестирования были составными, и конвейер обработки возвращал неполные ответы по большинству из них. Добавление составного разложения на уровне парсинга резко повысило удовлетворенность пользователей без каких-либо других изменений в конвейере.

1.5 Уточнение: когда система запрашивает ответ

Некоторые вопросы слишком расплывчаты, чтобы на них вообще можно было что-либо предпринять, и никакая их обработка не позволяет вывести их на уровень, пригодный для решения конкретных задач.

«Каков лимит?» Лимит на что? Ответственность? Возмещение убытков? Ущерб? Премия? «Покажите последнюю версию». Последняя версия какого документа? Последняя на какой момент? «Сравните с прошлогодней». Прошлогодней какой?

Если система слишком увлекается угадыванием, она выдает завуалированные ошибки, которые пользователи либо не замечают (что хуже), либо замечают и перестают доверять системе (что тоже хуже). Решение самое дешевое: обнаружить, что на вопрос нельзя ответить, и запросить ответ у пользователя повторно, вместо запуска конвейера обработки.

В проанализированном вопросе этот случай рассматривается в двух полях:

 class ParsedQuestion(BaseModel): # ... other fields suggested_clarification: str | None = None ambiguity_reason: str | None = None

Если suggested_clarification задан, оркестратор возвращает его перед запуском конвейера:

«Некоторые аспекты этого вопроса неоднозначны. Не могли бы вы уточнить: какой лимит (покрытие, франшиза, сублимит) и какой полис, если у вас их несколько?»

Простое правило: если в вопросе используется слово, указывающее на что-то другое (это, то, последнее, прошлогоднее, крышка), и контекст недоступен из истории разговора или области видимости, задайте вопрос. Обнаружение выполняется внутри объединенного вызова parse_question, описанного в сопроводительном документе к диспетчеризации (статья 6_c), как одна из подзадач общего запроса parse: «вернуть короткий уточняющий вопрос, если входные данные слишком расплывчаты, в противном случае — null». Интерфейс должен сделать ответ недорогим: короткий уточняющий вопрос, два или три предложенных варианта — готово.

Немногие производственные системы так поступают. По умолчанию обычно происходит следующее: система сразу же выдает ответ. В результате — постоянный поток завуалированных неверных ответов и постепенное снижение доверия пользователей к системе. Выявление неоднозначности на этапе синтаксического анализа обходится дешевле, чем выявление её после того, как поиск выдал неверные фрагменты текста.

2. Заключение

Анализ вопроса преобразует одну зашумленную строку пользователя в типизированное, реляционное краткое изложение. Пять семейств столбцов в question_df содержат то, что парсер считывает непосредственно из вопроса пользователя:

  • Ключевые слова (с расширением с помощью экспертного словаря) служат основой для поиска.
  • Форма и тип ответа указывают генератору, какую схему следует вернуть.
  • Подсказки области поиска позволяют определить, в какой части документа следует искать информацию.
  • Метод составного разложения при необходимости разбивает вопрос на подвопросы.
  • Функция уточнения запрашивает у пользователя обратную связь, если вопрос слишком неоднозначен.

Каждый столбец — это место, где сходятся экспертная лексика проекта, структура документа и намерения пользователя. Добавление возможности синтаксического анализа означает добавление столбца, а не новой функции.

Над ними располагаются еще две группы переменных, определяемые парсером после анализа профиля документа: решения о распределении (стратегия обработки фрагментов, модель, контекстное окно ответа) и флаги активации, которые отключают блоки, если они не подходят. Обе относятся к сопутствующей функции распределения (статья 6_c), которая также описывает маршрутизацию каждого столбца к блоку, который его обрабатывает.

Источники и дополнительная литература

В статье метод HyDE, предложенный Гао и др. (HyDE, ACL 2023), переосмысливается как работа с ключевыми словами в автономном режиме: основной составляющей являются ключевые слова, содержащиеся в гипотетическом ответе, а не сам этап встраивания. Метод переписывания запросов (Ma et al., RRR, EMNLP 2023) является ближайшим опубликованным прецедентом для части структурированного плана, corrected_question + rewrites . Четыре шаблона составных вопросов соответствуют Self-Ask (Press et al., Self-Ask, EMNLP Findings 2023) и IRCoT (Trivedi et al., IRCoT, ACL 2023). Наиболее близким аналогом метода «вопрос становится типизированным объектом, который обрабатывается последующим кодом» является семантический анализ текста в SQL (Yu et al., Spider, EMNLP 2018). В третьем томе (Agentic Bricks) мы возвращаемся к выбору инструментов в режиме реального времени на основе структурированного плана, описанного здесь.

В том же направлении, что и в статье:

  • Гао, Ма, Лин, Каллан, Точный поиск с нулевым количеством примеров без меток релевантности (HyDE), ACL 2023 (arXiv:2212.10496). Техника HyDE, предложенная в статье, переосмысливается: работа с ключевыми словами в офлайн-режиме, а не встраивание гипотетических документов в онлайн-режиме.
  • Ма, Гун, Хэ, Чжао, Дуань, Переписывание запросов для расширенных языковых моделей поиска (RRR), EMNLP 2023 (arXiv:2305.14283). Наиболее близкий опубликованный прецедент для части структурированного плана, corrected_question + rewrites .
  • Пресс и др., Измерение и сужение разрыва в композиционности языковых моделей (самостоятельный вопрос), Результаты EMNLP 2023 (arXiv:2210.03350). Модель разложения составного вопроса, которую расширяют четыре модели, описанные в этой статье.
  • Триведи и др., Чередование поиска информации с логическим мышлением для многошаговых вопросов, требующих интенсивного использования знаний (IRCoT), ACL 2023 (arXiv:2212.10509). Последовательные подвопросы; дополняет самозапрос для составных вопросов.
  • Ю и др., Spider: Крупномасштабный набор данных с человеческими метками для сложного и междоменного семантического анализа и задачи преобразования текста в SQL, EMNLP 2018 (arXiv:1809.08887). Канонический эталон преобразования текста в SQL; наиболее близкая родословная к «вопрос становится типизированным объектом, который обрабатывается последующим кодом».

Другой ракурс, другой контекст:

  • Ван и др., Query2doc: Расширение запроса с помощью больших языковых моделей, EMNLP 2023 (arXiv:2303.07678). Одного расширения пользовательского запроса, сгенерированного большой языковой моделью, достаточно для повышения эффективности поиска. Контекст — открытые вопросы и ответы в рамках предметной области; в данной статье рассматриваются корпоративные корпуса, где полноценный структурированный план (исправленный вопрос + предлагаемые подсказки + форма ответа + краткое описание генерации) оправдывает себя.
  • Шик и др., Toolformer: Языковые модели могут научиться использовать инструменты, NeurIPS 2023 (arXiv:2302.04761). Модель решает, когда и какой инструмент вызывать непосредственно в коде, без предварительного анализа вопросов. В третьем томе (Agentic Bricks) эта линия развивается на основе структурированного плана, определенного здесь.

Ранее в серии:

  • Документальная разведка: введение к серии. Что представляет собой серия, кирпичик за кирпичиком, и в каком порядке.
  • Базовая модель Enterprise RAG: от PDF-файла до выделенного ответа. Четырехэтапный конвейер от начала до конца: PDF-файл на входе, выделенный ответ на выходе.
  • Эмбеддинги — это не магия: предсказуемые сбои в поиске RAG. Где сходство эмбеддингов приносит пользу (синонимы, опечатки, перефразирование), где оно предсказуемо дает сбой (неизвестные термины, отрицание, релевантность термина и ответа) и как его все равно использовать.
  • Переранжировщики тоже не волшебство: когда слой кросс-кодировщика оправдывает затраты. Что добавляет кросс-кодировщик по сравнению с встраиванием на основе двух кодировщиков (измеренные показатели) и когда оправдана задержка.
  • RAG — это не машинное обучение, и инструментарий машинного обучения решает не ту задачу. Почему оптимизация по размеру фрагментов и тонкая настройка оптимизируют не то, что нужно; вместо этого следует ориентироваться на тип вопроса.
  • От регулярных выражений до моделей компьютерного зрения: какой метод RAG подходит для какой задачи? Две оси — сложность документа и контроль вопросов — определяют, какой метод подходит для каждого конкретного случая.
  • 10 распространенных ошибок RAG, которые мы постоянно видим на производстве. Десять производственных ошибок, расположенные по пунктам, с указанием способа их исправления.
  • Помимо функции extract_text: два слоя PDF-файла, определяющие качество RAG. Первая половина блока анализа: характер документа, сигналы и резюме.
  • Прекратите возвращать плоский текст из PDF-файла: необходима реляционная структура RAG. Вторая половина блока парсинга: реляционные таблицы, которые считывает каждый последующий блок.

Анджела Ши. Все материалы от Анджелы Ши.

Источник: towardsdatascience.com

❌ Нет похожих статей с такими тегами

Оцените материал:

Поделиться
Понравилась статья? Расскажите другим
ВКонтакте
Читайте также
Новости робототехники Работает ли Caveman? Тестируем модный скилл для экономии токенов Архив рубрики ~Обо всем~ Шмели с ходу решили новую задачу. Им не потребовалось обучение Архив рубрики ~Коротко из Telegram~ MiniMax Hub: Нейросетевой конвейер на бесконечном холсте MiniMax презентовали Hub… Архив рубрики ~Коротко из Telegram~ Готовые loop-сценарии для AI-агентов Вместо того чтобы каждый раз вручную… Архив рубрики ~Обо всем~ Эти полезные гаджеты от Amazon продаются со скидкой до 68% — вот почему я их рекомендую. Архив рубрики ~Коротко из Telegram~ Про рынок, который поделили еще до Яндекса. Forbes на днях… Архив рубрики ~Коротко из Telegram~ Fable 5 стала слишком опасной, чтобы её не хотели купить… Новости робототехники Секрет создания человекоподобных роботов, способных побеждать в марафонах. Архив рубрики ~Коротко из Telegram~ Разбираемся в чужом коде за считанные МИНУТЫ — на GitHub… Архив рубрики ~Обо всем~ В Великобритании запретят социальные сети для детей младше 16 лет и могут ввести комендантский час в ночное время. Архив рубрики ~Коротко из Telegram~ Если ваш Mac греется и батарея тает на глазах, попробуйте… Архив рубрики ~Коротко из Telegram~ ExcelDashboardAI — помощник для анализа данных на основе искусственного интеллекта…. Архив рубрики ~Коротко из Telegram~ Превращаем своего агента в ленивого синьора — скилл Ponytail для… Архив рубрики ~Коротко из Telegram~ 🛡ИИ, как защита В новостях все чаще мелькают страшилки о… Новости робототехники Работает ли Caveman? Тестируем модный скилл для экономии токенов Архив рубрики ~Обо всем~ Шмели с ходу решили новую задачу. Им не потребовалось обучение Архив рубрики ~Коротко из Telegram~ MiniMax Hub: Нейросетевой конвейер на бесконечном холсте MiniMax презентовали Hub… Архив рубрики ~Коротко из Telegram~ Готовые loop-сценарии для AI-агентов Вместо того чтобы каждый раз вручную… Архив рубрики ~Обо всем~ Эти полезные гаджеты от Amazon продаются со скидкой до 68% — вот почему я их рекомендую. Архив рубрики ~Коротко из Telegram~ Про рынок, который поделили еще до Яндекса. Forbes на днях… Архив рубрики ~Коротко из Telegram~ Fable 5 стала слишком опасной, чтобы её не хотели купить… Новости робототехники Секрет создания человекоподобных роботов, способных побеждать в марафонах. Архив рубрики ~Коротко из Telegram~ Разбираемся в чужом коде за считанные МИНУТЫ — на GitHub… Архив рубрики ~Обо всем~ В Великобритании запретят социальные сети для детей младше 16 лет и могут ввести комендантский час в ночное время. Архив рубрики ~Коротко из Telegram~ Если ваш Mac греется и батарея тает на глазах, попробуйте… Архив рубрики ~Коротко из Telegram~ ExcelDashboardAI — помощник для анализа данных на основе искусственного интеллекта…. Архив рубрики ~Коротко из Telegram~ Превращаем своего агента в ленивого синьора — скилл Ponytail для… Архив рубрики ~Коротко из Telegram~ 🛡ИИ, как защита В новостях все чаще мелькают страшилки о…

Оставить комментарий