Ваш RAG тупит? Пора дать ему стероиды
Вы построили RAG-систему. Она работает. Но отвечает как студент-троечник на экзамене – вроде что-то знает, но глубины ноль. Знакомо? Я тоже через это проходил.
Обычный векторный поиск ловит семантику, но теряет конкретику. Лексический поиск находит точные термины, но не понимает смысл. Агентный ассистент, построенный на такой основе, будет постоянно ошибаться в деталях или давать общие ответы.
Проблема в 2026 году не в моделях – они уже умные. Проблема в том, как их кормить данными. Слабый поиск = глупая модель, даже если это GPT-5 Turbo.
В статье про DeepResearch мы разобрали, почему статичный RAG устарел для корпоративных данных. Сегодня я покажу решение для production: гибридный RAG, где векторы и ключевые слова не спорят, а работают вместе.
Архитектура: Почему именно Bedrock + OpenSearch?
Выбор инструментов – это не религиозный спор, а инженерный расчет. Почему эта связка работает в 2026 году лучше других?
OpenSearch (версия 2.14 на 2026 год) – это не просто поисковик. Это полноценный движок для гибридного поиска с нейросетевыми ранжированием, встроенной векторной индексацией и фильтрацией по метаданным. И да, он бесплатный в сравнении с некоторыми специализированными векторными базами.
Агентный ассистент на такой основе умеет:
- Понимать контекст запроса (семантический поиск)
- Находить точные термины и цифры (лексический поиск)
- Ранжировать результаты по релевантности, используя оба подхода
- Динамически планировать поисковые итерации как настоящий агент
Пошаговый разбор: От пустого AWS аккаунта до работающего агента
Теория закончилась. Переходим к практике. Я разберу каждый шаг так, будто объясняю коллеге у доски.
1 Подготовка: Настраиваем Bedrock и OpenSearch в AWS
Сначала активируем нужные сервисы. В консоли AWS ищем Bedrock и включаем его. Важный момент: по умолчанию многие модели в Bedrock отключены – их нужно активировать вручную в разделе Model access.
# Устанавливаем AWS CLI и настраиваем профиль
aws configure
# Проверяем доступные модели в Bedrock (на 06.04.2026)
aws bedrock list-foundation-models --region us-east-1
# Ожидаемый вывод должен включать:
# anthropic.claude-3-5-sonnet-20241022
# amazon.titan-text-express-v2:0
# cohere.command-r-plus-v2:0
# meta.llama3-2-90b-instruct-v1:0
Теперь OpenSearch. Создаем домен через консоль или CloudFormation. Для продакшена берите версию 2.14 (последнюю стабильную на 2026 год). Не экономьте на инстансах – для векторного поиска нужна память.
Ошибка новичка: создавать OpenSearch в публичной подсети. Никогда так не делайте. Размещайте в приватных подсетях VPC и используйте Security Groups для строгого контроля доступа.
2 Индексация: Готовим данные для гибридного поиска
Это самый важный этап. Плохая индексация похоронит даже самую крутую архитектуру.
Создаем индекс в OpenSearch с поддержкой и векторных полей, и текстовых:
import boto3
from opensearchpy import OpenSearch, RequestsHttpConnection
from requests_aws4auth import AWS4Auth
# Настройка клиента OpenSearch
host = 'your-opensearch-domain.es.amazonaws.com'
region = 'us-east-1'
service = 'es'
credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(credentials.access_key, credentials.secret_key,
region, service, session_token=credentials.token)
# Клиент OpenSearch 2.x
client = OpenSearch(
hosts=[{'host': host, 'port': 443}],
http_auth=awsauth,
use_ssl=True,
verify_certs=True,
connection_class=RequestsHttpConnection
)
# Создание индекса с гибридной схемой
index_body = {
"settings": {
"index": {
"knn": True, # Включаем k-NN поиск для векторов
"knn.algo_param.ef_search": 512
}
},
"mappings": {
"properties": {
"text": {
"type": "text",
"analyzer": "standard"
},
"embedding": {
"type": "knn_vector", # Векторное поле
"dimension": 1536, # Размерность для текстовых эмбеддингов
"method": {
"name": "hnsw",
"space_type": "cosinesimil",
"engine": "nmslib"
}
},
"metadata": {
"type": "object",
"properties": {
"source": {"type": "keyword"},
"timestamp": {"type": "date"},
"doc_id": {"type": "keyword"}
}
}
}
}
}
# Создаем индекс
response = client.indices.create(index="hybrid-rag-index", body=index_body)
print(f"Индекс создан: {response}")
Теперь индексируем документы. Генерируем эмбеддинги через Bedrock Titan Embeddings (или другой моделью):
import json
from typing import List
import boto3
bedrock = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')
def get_embeddings(texts: List[str]) -> List[List[float]]:
"""Генерация эмбеддингов через Amazon Titan Embeddings"""
# На 2026 год используем модель Amazon Titan Embeddings G1 Text v2.0
model_id = 'amazon.titan-embed-text-v2:0'
embeddings = []
for text in texts:
body = json.dumps({
"inputText": text,
"dimensions": 1536,
"normalize": True
})
response = bedrock.invoke_model(
body=body,
modelId=model_id,
accept='application/json',
contentType='application/json'
)
response_body = json.loads(response['body'].read())
embeddings.append(response_body['embedding'])
return embeddings
# Пример индексации документа
doc_text = "Как настроить деплой микросервиса в AWS EKS с blue-green стратегией"
embedding = get_embeddings([doc_text])[0]
document = {
"text": doc_text,
"embedding": embedding,
"metadata": {
"source": "internal-wiki",
"timestamp": "2026-01-15T10:30:00Z",
"doc_id": "deploy-guide-eks-001"
}
}
# Сохраняем в OpenSearch
client.index(index="hybrid-rag-index", body=document, id=document['metadata']['doc_id'])
3 Поиск: Реализуем гибридный ранжирующий алгоритм
Вот где начинается магия. Мы не просто делаем два поиска и складываем результаты – мы используем Reciprocal Rank Fusion (RRF), который умно комбинирует ранги.
def hybrid_search(query: str, k: int = 10):
"""Гибридный поиск: семантический + лексический"""
# 1. Генерируем эмбеддинг для запроса
query_embedding = get_embeddings([query])[0]
# 2. Векторный (семантический) поиск
vector_query = {
"size": k,
"query": {
"knn": {
"embedding": {
"vector": query_embedding,
"k": k
}
}
}
}
# 3. Лексический (текстовый) поиск
text_query = {
"size": k,
"query": {
"match": {
"text": query
}
}
}
# Выполняем оба поиска параллельно
vector_results = client.search(index="hybrid-rag-index", body=vector_query)
text_results = client.search(index="hybrid-rag-index", body=text_query)
# 4. Применяем Reciprocal Rank Fusion
fused_scores = {}
# Обрабатываем результаты векторного поиска
for rank, hit in enumerate(vector_results['hits']['hits'], 1):
doc_id = hit['_id']
fused_scores[doc_id] = fused_scores.get(doc_id, 0) + 1.0 / (60 + rank)
# Обрабатываем результаты текстового поиска
for rank, hit in enumerate(text_results['hits']['hits'], 1):
doc_id = hit['_id']
fused_scores[doc_id] = fused_scores.get(doc_id, 0) + 1.0 / (60 + rank)
# Сортируем по комбинированному score
sorted_docs = sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
# Получаем полные документы для топ результатов
top_doc_ids = [doc_id for doc_id, _ in sorted_docs[:k]]
final_results = []
for doc_id in top_doc_ids:
doc = client.get(index="hybrid-rag-index", id=doc_id)
final_results.append({
"text": doc['_source']['text'],
"metadata": doc['_source']['metadata'],
"score": fused_scores[doc_id]
})
return final_results
4 Агентная логика: Превращаем поиск в интеллектуального ассистента
Теперь делаем шаг от поисковика к агенту. Наш ассистент должен уметь переформулировать запросы, задавать уточняющие вопросы и планировать несколько итераций поиска.
Интегрируем Bedrock для генерации ответов и анализа:
class AgenticRAGAssistant:
def __init__(self):
self.bedrock = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')
self.llm_model = 'anthropic.claude-3-5-sonnet-20241022' # Актуально на 06.04.2026
def generate_search_queries(self, user_query: str) -> list:
"""Агент анализирует запрос и генерирует варианты для поиска"""
prompt = f"""Человек спрашивает: {user_query}
Разбей этот запрос на подзапросы для поиска в технической документации.
Верни JSON массив с 3-5 вариантами поисковых запросов.
Пример:
["настройка AWS EKS кластера", "blue-green deployment в Kubernetes", "микросервис деплой best practices"]
Только JSON, без пояснений."""
response = self.bedrock.invoke_model(
modelId=self.llm_model,
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 500,
"messages": [
{
"role": "user",
"content": prompt
}
]
})
)
result = json.loads(response['body'].read())
queries = json.loads(result['content'][0]['text'])
return queries
def generate_answer(self, query: str, contexts: list) -> str:
"""Генерация ответа на основе найденных контекстов"""
context_text = "\n\n".join([f"[{i+1}] {ctx['text']}" for i, ctx in enumerate(contexts)])
prompt = f"""Используй следующие документы, чтобы ответить на вопрос.
Если информации недостаточно, скажи об этом честно.
Вопрос: {query}
Документы:
{context_text}
Ответ: """
response = self.bedrock.invoke_model(
modelId=self.llm_model,
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1500,
"messages": [
{
"role": "user",
"content": prompt
}
]
})
)
result = json.loads(response['body'].read())
return result['content'][0]['text']
def answer_question(self, user_query: str) -> dict:
"""Полный цикл агентного RAG"""
# 1. Планирование: генерируем поисковые запросы
search_queries = self.generate_search_queries(user_query)
# 2. Поиск по всем запросам
all_contexts = []
for query in search_queries:
contexts = hybrid_search(query, k=5)
all_contexts.extend(contexts)
# 3. Дедупликация контекстов
unique_contexts = {ctx['metadata']['doc_id']: ctx for ctx in all_contexts}.values()
# 4. Ранжирование по релевантности (можно добавить дополнительную LLM-ранжировку)
sorted_contexts = sorted(unique_contexts, key=lambda x: x['score'], reverse=True)[:10]
# 5. Генерация ответа
answer = self.generate_answer(user_query, sorted_contexts)
return {
"answer": answer,
"sources": sorted_contexts,
"search_queries_used": search_queries
}
Ловушки и подводные камни: Что сломается в продакшене
Я видел десятки таких систем в бою. Вот что ломается чаще всего:
| Проблема | Почему возникает | Как фиксить |
|---|---|---|
| Токены кончаются до ответа | Слишком много контекста в промпте | Используйте динамическое сжатие контекста или summarization |
| Поиск возвращает мусор | Неправильные веса в гибридном поиске | Настройте RRF константу на ваших данных |
| Задержки в 10+ секунд | Последовательные вызовы Bedrock | Распараллеливайте генерацию запросов и поиск |
| Cost explosion | Много запросов к дорогим моделям | Кэшируйте эмбеддинги, используйте более дешевые модели для некоторых задач |
Частые вопросы (FAQ)
Q: Эта архитектура дорогая в эксплуатации?
A: Зависит от объема. Для 10К документов и 100 запросов в день – $200-500/месяц. Для 1М документов – уже $2000+. Основные затраты: Bedrock (генерация и эмбеддинги) и OpenSearch инстансы.
Q: Можно ли заменить OpenSearch на Pinecone или Weaviate?
A: Технически да, но потеряете гибридный поиск из коробки. Придется строить его самостоятельно. OpenSearch дает это бесплатно.
Q: Как обрабатывать PDF и изображения?
A: Добавьте шаг предобработки: Amazon Textract для PDF, Bedrock Titan Multimodal Embeddings для изображений. Это тема для отдельной статьи.
Q: Система не находит свежие документы
A: Проверьте политику индексации. Добавьте временные фильтры в поиск: "filter": {"range": {"metadata.timestamp": {"gte": "now-30d/d"}}}.
Что дальше? Куда развивать систему
Когда базовый гибридный RAG работает, самое время добавить стероидов:
- Graph RAG: Извлекайте связи между документами и стройте граф знаний. Об этом я писал в обзоре Ragex.
- Мультимодальность: Добавьте поиск по диаграммам, схемам, скриншотам через Bedrock Titan Multimodal.
- Active Learning: Когда пользователь исправляет ответ, используйте это для улучшения поиска.
- SQL + векторы: Комбинируйте структурированные данные из БД с неструктурированными документами. Архитектура такого пайплайна сложна, но мощна.
Мой главный совет: не строить монолит. Разбивайте систему на микросервисы: индексатор, поисковик, агентный планировщик, генератор ответов. В 2026 году это единственный способ поддерживать систему, которая не превратится в legacy за 6 месяцев.
Гибридный RAG – не серебряная пуля. Это фундамент, на котором строятся интеллектуальные агенты. Следующий шаг – добавить поиск в интернет и работу с SQL, чтобы агент стал по-настоящему автономным.
Начните с малого: настройте гибридный поиск на 100 документах. Когда он будет работать лучше, чем обычный векторный – масштабируйте. И помните: лучшая архитектура та, которую вы понимаете настолько, что можете починить в 3 часа ночи.