Гибридный RAG: реализация с Amazon Bedrock и OpenSearch в 2026 | AiManual
AiManual Logo Ai / Manual.
06 Апр 2026 Гайд

Гибридный RAG с Amazon Bedrock и OpenSearch: пошаговая реализация интеллектуального поиска для агентных ассистентов

Полный гайд по созданию агентного ассистента с гибридным RAG на Amazon Bedrock и OpenSearch. Пошаговая реализация, код, настройки и ловушки.

Ваш RAG тупит? Пора дать ему стероиды

Вы построили RAG-систему. Она работает. Но отвечает как студент-троечник на экзамене – вроде что-то знает, но глубины ноль. Знакомо? Я тоже через это проходил.

Обычный векторный поиск ловит семантику, но теряет конкретику. Лексический поиск находит точные термины, но не понимает смысл. Агентный ассистент, построенный на такой основе, будет постоянно ошибаться в деталях или давать общие ответы.

Проблема в 2026 году не в моделях – они уже умные. Проблема в том, как их кормить данными. Слабый поиск = глупая модель, даже если это GPT-5 Turbo.

В статье про DeepResearch мы разобрали, почему статичный RAG устарел для корпоративных данных. Сегодня я покажу решение для production: гибридный RAG, где векторы и ключевые слова не спорят, а работают вместе.

Архитектура: Почему именно Bedrock + OpenSearch?

Выбор инструментов – это не религиозный спор, а инженерный расчет. Почему эта связка работает в 2026 году лучше других?

💡
Amazon Bedrock на начало 2026 года дает доступ к Claude 3.5 Sonnet, GPT-5 (через API Amazon), и собственным моделям Amazon Titan Text G1 Express. Вы получаете стабильный API, управление токенами и встроенную безопасность без головной боли с инфраструктурой.

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
💡
Почему 60 в формуле RRF? Это константа k в формуле 1/(k + rank). Она предотвращает доминирование одного из поисков. На практике подбирается экспериментально. Для большинства случаев 60 работает хорошо.

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 часа ночи.

Подписаться на канал