Работа самозапрашивающего ретривера
Самозапрашивающий ретривер — это ретривер, способный обращаться к самому себе. Такой ретривер принимает запрос на естественном языке, преобразует его в структурированный запрос с помощью LLM-цепочки, после чего применят полученный запрос к заданному векторному хранилищу. Это позволяет ретриверу как использовать запрос пользователя для семантического поиска по содержимому документов, так и применять извлеченные из запроса фильтры по метаданным хранимых документов.
Начало работы
Для демонстрации в разделе используется векторное хранилище Chromа и набор документов, которые содержат краткое описание фильмов.
Для работы самозапрашивающего ретривера нужно установить пакет lark
.
Установите необходимые зависимости.
pip install --upgrade --quiet lark gigachain-community gigachain-chroma
from langchain_chroma import Chroma
from langchain_gigachat.embeddings import GigaChatEmbeddings
from langchain_core.documents import Document
docs = [
Document(
page_content="Трагедия войны глазами солдатской невесты",
metadata={"year": 1957, "rating": 8.7, "genre": "драма"},
),
Document(
page_content="Заурядный семьянин Василий Кузякин заводит роман с эффектной коллегой",
metadata={"year": 1985, "director": "Владимир Меньшов", "rating": 8.2},
),
Document(
page_content="Встреча Алисы и Коли становится началом ярких приключений, в которых они вступят в схватку с космическими пиратами",
metadata={"year": 2024, "director": "Александр Андрющенко", "rating": 7.2},
),
Document(
page_content="Легендарный советский шпионский сериал Татьяны Лиозновой о штандартенфюрере Штирлице",
metadata={"year": 1973, "director": "Татьяна Лиознова", "rating": 8.3},
),
Document(
page_content="Непутевый богатырь случайно упускает орду тугар со всем золотом Ростова и теперь спешит догнать и одолеть варваров",
metadata={"year": 2004, "genre": "мультфильм"},
),
Document(
page_content="Мистическое путешествие через Зону к комнате, где исполняются желания",
metadata={
"year": 1979,
"director": "Андрей Тарковский",
"genre": "фантастика",
"rating": 9.9,
},
),
]
embedding = GigaChatEmbeddings(
credentials="<ключ_авторизации>",
scope="GIGACHAT_API_PERS",
verify_ssl_certs=False,
)
vectorstore = Chroma.from_documents(docs, embedding)
Создание самозапрашивающего ретривера
Добавьте описание фильтров, которые поддерживают документы, и инициализируйте ретривер.
from langchain.chains.query_constructor.base import AttributeInfo
from langchain_gigachat.chat_models import GigaChat
from langchain.retrievers.self_query.base import SelfQueryRetriever
metadata_field_info = [
AttributeInfo(
name="genre",
description="Жанр кино или мультфильма. Возможные значения ['фантастика', 'комедия', 'драма', 'триллер', 'мелодрама', 'экшн', 'мультфильм']",
type="string",
),
AttributeInfo(
name="year",
description="Год выпуска",
type="integer",
),
AttributeInfo(
name="director",
description="Имя режиссера",
type="string",
),
AttributeInfo(
name="rating",
description="Рейтинг кино или мультфильма от 1 до 10",
type="float",
),
]
document_content_description = "Краткое описание кино или мультфильма"
model = GigaChat(
credentials="<ключ_авторизации>",
scope="GIGACHAT_API_PERS",
model="GigaChat-Pro",
verify_ssl_certs=False,
)
retriever = SelfQueryRetriever.from_llm(
model,
vectorstore,
document_content_description,
metadata_field_info,
)
Использование ретривера
Теперь вы можете проверить работу созданного ретривера.
# Пример фильтра
retriever.invoke("Хочу посмотреть фильм с рейтингом больше 8.5")
[Document(page_content='Трагедия войны глазами солдатской невесты', metadata={'genre': 'драма', 'rating': 8.7, 'year': 1957}),
Document(page_content='Мистическое путешествие через Зону к комнате, где исполняются желания', metadata={'director': 'Андрей Тарковский', 'genre': 'фантастика', 'rating': 9.9, 'year': 1979})]
# Пример запроса и фильтра
retriever.invoke("Фильм Татьяны Лиозновой про Штирлица")
[Document(page_content='Легендарный советский шпионский сериал Татьяны Лиозновой о штандартенфюрере Штирлице', metadata={'director': 'Татьяна Лиознова', 'rating': 8.3, 'year': 1973})]
# Пример составного фильтра
retriever.invoke(
"Есть какие-нибудь высокооцененные (с рейтингом выше 8.5) фантастические фильмы?"
)
[Document(page_content='Мистическое путешествие через Зону к комнате, где исполняются желания', metadata={'director': 'Андрей Тарковский', 'genre': 'фантастика', 'rating': 9.9, 'year': 1979})]
# Пример запроса и составного фильтра
retriever.invoke("мультфильм про богатыря, который вышел с 1999 по 2007")
[Document(page_content='Непутевый богатырь случайно упускает орду тугар со всем золотом Ростова и теперь спешит догнать и одолеть варваров', metadata={'genre': 'мультфильм', 'year': 2004})]
Ограничение количества запрашиваемых документов
Чтобы задать количество документов, которые нужно получить, используйте параметр enable_limit=True
.
retriever = SelfQueryRetriever.from_llm(
model,
vectorstore,
document_content_description,
metadata_field_info,
enable_limit=True,
)
# Пример релевантного запроса
retriever.invoke("Один фильм про войну")
[Document(page_content='Трагедия войны глазами солдатской невесты', metadata={'genre': 'драма', 'rating': 8.7, 'year': 1957})]
Создание ретривера с помощью LCEL
Вы можете переписать свой ретривер с использованием LCEL. Реализация на LCEL даст больше контроля за работой ретривера и информации о том, что происходит «под капотом».
Сначала создайте цепочку, которая будет отвечать за формирование запроса.
Эта цепочка будет преобразовывать запрос пользователя в структурированный запрос (объект StructuredQuery
), который содержит заданные пользователем фильтры.
Для создания промпта и парсера в примере используются вспомогательные функции get_query_constructor_prompt()
и from_components()
соответственно.
from langchain.chains.query_constructor.base import (
StructuredQueryOutputParser,
get_query_constructor_prompt,
)
prompt = get_query_constructor_prompt(
document_content_description,
metadata_field_info,
)
output_parser = StructuredQueryOutputParser.from_components()
query_constructor = prompt | model | output_parser
Теперь вы можете посмотреть, какой промпт используется при вызове модели:
print(prompt.format(query="заглушка"))
Твоя задача — структурировать запрос пользователя, чтобы он соответствовал схеме запроса, представленной ниже.
<< Схема структурированного запроса >>
При ответе используйте фрагмент кода markdown с объектом JSON, отформатированным по следующей схеме:
```json
{
"query": string \ текстовая строка для сравнения с содержимым документа
"filter": string \ логическое условие для фильтрации документов
}
```
Строка запроса должна содержать только текст, который ожидается в содержимом документов. Любые условия в фильтре не должны упоминаться в запросе.
Логическое условие состоит из одного или нескольких операторов сравнения и логических операций.
Оператор сравнения имеет форму: `comp(attr, val)`:
- `comp` (eq | ne | gt | gte | lt | lte | contain | like | in | nin): оператор сравнения;
- `attr` (string): имя атрибута, к которому применяется сравнение;
- `val` (string): значение для сравнения.
Логическая операция имеет форму `op(statement1, statement2, ...)`:
- `op` (and | or | not): логический оператор;
- `statement1`, `statement2`, ... (операторы сравнения или логические операции): одно или несколько утверждений, к которым применяется операция.
Убедитесь, что вы используете только перечисленные выше операторы сравнения и логические операторы и никакие другие.
Убедитесь, что фильтры относятся только к атрибутам, которые существуют в источнике данных.
Убедитесь, что фильтры используют только имена атрибутов с их именами функций, если на них применяются функции.
Убедитесь, что фильтры используют только формат `YYYY-MM-DD` при обработке значений типа данных временной метки.
Убедитесь, что фильтры учитывают описания атрибутов и делают только те сравнения, которые возможны с учетом типа хранимых данных.
Убедитесь, что фильтры используются только по мере необходимости. Если нет фильтров, которые следует применить, верните "NO_FILTER" для значения фильтра.
<< Пример 1. >>
Источник данных:
```json
{
"content": "Текст песни",
"attributes": {
"artist": {
"type": "string",
"description": "Имя исполнителя песни"
},
"length": {
"type": "integer",
"description": "Длительность песни в секундах"
},
"genre": {
"type": "string",
"description": "Жанр песни, один из "pop", "rock" или "rap""
}
}
}
```
Запрос пользователя:
Какие песни Тейлор Свифт или Кэти Перри о подростковой любви длительностью менее 3 минут в жанре поп?
Структурированный запрос:
```json
{
"query": "teenager love",
"filter": "and(or(eq(\"artist\", \"Taylor Swift\"), eq(\"artist\", \"Katy Perry\")), lt(\"length\", 180), eq(\"genre\", \"pop\"))"
}
```
<< Пример 2. >>
Источник данных:
```json
{
"content": "Текст песни",
"attributes": {
"artist": {
"type": "string",
"description": "Имя исполнителя песни"
},
"length": {
"type": "integer",
"description": "Длительность песни в секундах"
},
"genre": {
"type": "string",
"description": "Жанр песни, один из "pop", "rock" или "rap""
}
}
}
```
Запрос пользователя:
Какие песни не были опубликованы на Spotify
Структурированный запрос:
```json
{
"query": "",
"filter": "NO_FILTER"
}
```
<< Пример 3. >>
Источник данных:
```json
{
"content": "Краткое описание кино или мультфильма",
"attributes": {
"genre": {
"description": "Жанр кино или мультфильма. Возможные значения ['фантастика', 'комедия', 'драма', 'триллер', 'мелодрама', 'экшн', 'мультфильм']",
"type": "string"
},
"year": {
"description": "Год выпуска",
"type": "integer"
},
"director": {
"description": "Имя режиссера",
"type": "string"
},
"rating": {
"description": "Рейтинг кино или мультфильма от 1 до 10",
"type": "float"
}
}
}
```
Запрос пользователя:
заглушка
Структурированный запрос:
Результат работы цепочки:
query_constructor.invoke(
{"query": "Научно-фантастические фильмы Андрея Тарковского снятые в семидесятых"}
)
StructuredQuery(query='научно-фантастический фильм', filter=Operation(operator=<Operator.AND: 'and'>, arguments=[Comparison(comparator=<Comparator.EQ: 'eq'>, attribute='genre', value='фантастика'), Comparison(comparator=<Comparator.EQ: 'eq'>, attribute='director', value='Андрей Тарковский'), Comparison(comparator=<Comparator.GTE: 'gte'>, attribute='year', value=1970), Comparison(comparator=<Comparator.LTE: 'lte'>, attribute='year', value=1979)]), limit=None)
Конструктор запросов — ключевой элемент самозапрашивающего ретривера. Чтобы добиться хорошей работы конструктора, зачастую требуется настройка промпта, использование образцов в промпте и описание атрибутов.
Другим важным элементом является преобразователь структурированного запроса (транслятор).
Он преобразует объект StructuredQuery
в фильтр метаданных согласно синтаксису векторного хранилища, которое вы используете.
GigaChain предоставляет доступ к преобразователям, встроенным в LangChain.
Подробнее о них можно прочитать в официальной документации.
from langchain.retrievers.self_query.chroma import ChromaTranslator
retriever = SelfQueryRetriever(
query_constructor=query_constructor,
vectorstore=vectorstore,
structured_query_translator=ChromaTranslator(),
)
retriever.invoke(
"Есть какие-нибудь высокооцененные (с рейтингом выше 8.5) фантастические фильмы?"
)
[Document(page_content='Мистическое путешествие через Зону к комнате, где исполняются желания', metadata={'director': 'Андрей Тарковский', 'genre': 'фантастика', 'rating': 9.9, 'year': 1979})]