Реализация Self-Query RAG с метаданными
Self-Query RAG — техника, при которой LLM анализирует запрос пользователя и автоматически конструирует структурированный фильтр по метаданным в дополнение к векторному поиску. Вместо поиска только по семантике, система точечно фильтрует документы по дате, типу, автору, отделу и другим атрибутам.
Проблема без Self-Query
Без Self-Query запрос «политики безопасности, выпущенные в 2024 году» ищет все документы по семантике «безопасность», не фильтруя по году. Пользователь получает смешанные результаты разных периодов. С Self-Query: LLM извлекает фильтр date >= 2024-01-01 AND doc_type = "security_policy" и применяет его совместно с векторным поиском.
Реализация через LangChain SelfQueryRetriever
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Qdrant
# Описание метаданных для LLM
metadata_field_info = [
AttributeInfo(
name="doc_type",
description="Тип документа: contract, regulation, policy, faq, procedure",
type="string",
),
AttributeInfo(
name="department",
description="Отдел или подразделение: hr, legal, finance, it, security",
type="string",
),
AttributeInfo(
name="year",
description="Год публикации документа",
type="integer",
),
AttributeInfo(
name="status",
description="Статус документа: active, archived, draft",
type="string",
),
AttributeInfo(
name="author",
description="Автор или ответственный за документ",
type="string",
),
]
document_content_description = "Корпоративная документация компании: регламенты, политики, договоры, процедуры"
llm = ChatOpenAI(model="gpt-4o", temperature=0)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
retriever = SelfQueryRetriever.from_llm(
llm=llm,
vectorstore=vectorstore,
document_contents=document_content_description,
metadata_field_info=metadata_field_info,
enable_limit=True, # Позволяет LLM указать limit в запросе
verbose=True,
)
Пример работы Self-Query
# Пример 1: Фильтр по году и типу
result = retriever.invoke(
"Какие политики безопасности действовали в 2023 году?"
)
# LLM генерирует фильтр: {"doc_type": "policy", "department": "security", "year": 2023, "status": "active"}
# Затем выполняется векторный поиск с этим фильтром
# Пример 2: Фильтр по отделу
result = retriever.invoke(
"Покажи регламенты HR отдела"
)
# Фильтр: {"doc_type": "regulation", "department": "hr"}
# Пример 3: Без фильтра (обычный векторный поиск)
result = retriever.invoke(
"Как подготовиться к аудиту?"
)
# LLM не извлекает структурированных фильтров — чистый semantic search
Кастомная реализация Self-Query без LangChain
from pydantic import BaseModel, Field
from typing import Optional
from openai import OpenAI
import json
class SearchFilter(BaseModel):
semantic_query: str = Field(description="Чисто семантическая часть запроса для векторного поиска")
doc_type: Optional[str] = Field(default=None, description="Тип документа")
department: Optional[str] = Field(default=None, description="Отдел")
year_from: Optional[int] = Field(default=None, description="Год от (включительно)")
year_to: Optional[int] = Field(default=None, description="Год до (включительно)")
status: Optional[str] = Field(default=None, description="Статус: active/archived")
def parse_query_to_filter(user_query: str, client: OpenAI) -> SearchFilter:
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[{
"role": "system",
"content": "Извлеки из запроса пользователя структурированные фильтры для поиска документов."
}, {
"role": "user",
"content": user_query
}],
response_format=SearchFilter,
temperature=0,
)
return response.choices[0].message.parsed
def self_query_search(user_query: str, vectorstore, top_k: int = 5) -> list:
filter_obj = parse_query_to_filter(user_query, openai_client)
# Строим фильтр для Qdrant
qdrant_filter = build_qdrant_filter(filter_obj)
return vectorstore.similarity_search(
filter_obj.semantic_query,
k=top_k,
filter=qdrant_filter,
)
Практический кейс: корпоративная база знаний с метаданными
Задача: поисковый ассистент для 15 000 внутренних документов с метаданными (тип, отдел, год, статус, автор).
До Self-Query: 42% запросов возвращали архивные документы вместо актуальных.
После Self-Query:
- Архивные документы в результатах для «актуальных» запросов: 42% → 3%
- Precision@5: 0.68 → 0.89
- Пользовательская удовлетворённость: +31%
Failure cases: LLM иногда неверно интерпретирует параметры фильтра при неоднозначных запросах. Решение — добавить confidence threshold и fallback на pure semantic search при низкой уверенности.
Сроки
- Разметка метаданных документов: 1–3 недели (зависит от наличия данных)
- Реализация Self-Query Retriever: 3–5 дней
- Тестирование и подбор промпта: 3–5 дней
- Итого: 2–5 недель







