AI Agents · Урок

Объединение веб-поиска с RAG

Гибридное извлечение: локальное векторное хранилище и актуальный веб-поиск для получения свежих ответов.

Урок 4 из 413 шагов

«Объединение веб-поиска с RAG» — бесплатный урок AI Agents на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Agents, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Agents содержит 4 уроков всего.

Задача гибридного извлечения

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

Локальное векторное хранилище работает с первым типом, а веб-поиск — со вторым. Их объединение позволяет получить преимущества обоих подходов.

Архитектура: локальный RAG + веб-поиск

Гибридная система направляет каждый вопрос к подходящему источнику:

  • Векторное хранилище (RAG) — проиндексированные документы, стабильные знания, закрытые данные
  • Веб-поиск — текущие события, недавние выпуски, данные в реальном времени
  • Оба источника — когда вопросу нужен контекст из документов AND актуальная информация
class HybridRetrievalAgent:
    def __init__(self, vector_store, search_client):
        self.vector_store = vector_store  # e.g., ChromaDB or FAISS
        self.search_client = search_client  # e.g., TavilyClient

    def answer(self, question):
        route = self.classify_question(question)

        if route == 'static':
            context = self.vector_store.query(question, n_results=5)
        elif route == 'current':
            context = self.web_search(question)
        else:  # 'both'
            local = self.vector_store.query(question, n_results=3)
            web = self.web_search(question)
            context = local + web

        return self.generate_answer(question, context)

if __name__ == '__main__':
    class DemoAgent(HybridRetrievalAgent):
        def classify_question(self, question):
            return 'static' if 'company' in question.lower() else 'current'
        def web_search(self, question):
            return [('Web result about ' + question, {})]
        def generate_answer(self, question, context):
            return f'Answer using {len(context)} context item(s).'

    class FakeVectorStore:
        def query(self, question, n_results=5):
            return [('Local doc snippet', {})]

    agent = DemoAgent(FakeVectorStore(), search_client=None)
    print(agent.answer('What is our company policy on refunds?'))

Классификатор маршрутизации

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

ROUTING_PROMPT = '''Classify this question into one of three categories:
- "static": answered from company documents, product docs, or stable technical knowledge
- "current": requires up-to-date information (news, prices, recent events, latest releases)
- "both": needs both company context and current information

Question: {question}

Respond with exactly one word: static, current, or both.'''

def classify_question(question):
    response = llm_call(ROUTING_PROMPT.format(question=question))
    route = response.strip().lower()
    if route not in ('static', 'current', 'both'):
        return 'both'  # safe default
    return route

Настройка локального векторного хранилища

Для базы статических знаний используйте ChromaDB — лёгкую векторную базу данных, работающую внутри процесса. Один раз проиндексируйте документы, а затем выполняйте запросы во время работы приложения.

Установите её командой pip install chromadb openai.

import chromadb
from chromadb.utils import embedding_functions
import os

client = chromadb.PersistentClient(path='./vector_db')

ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.getenv('OPENAI_API_KEY'),
    model_name='text-embedding-3-small'
)

collection = client.get_or_create_collection(
    name='company_docs',
    embedding_function=ef
)

def index_document(doc_id, text, metadata=None):
    collection.add(
        ids=[doc_id],
        documents=[text],
        metadatas=[metadata or {}]
    )

def local_retrieve(question, n_results=5):
    results = collection.query(
        query_texts=[question],
        n_results=n_results
    )
    return list(zip(results['documents'][0], results['metadatas'][0]))

Извлечение данных с помощью веб-поиска

Для веб-поиска используется Tavily, который получает актуальную информацию. Форматируйте результаты единообразно, чтобы их можно было объединять с результатами локального RAG в одной структуре запроса.

from tavily import TavilyClient
import os

tavily = TavilyClient(api_key=os.getenv('TAVILY_API_KEY'))

def web_retrieve(question, n_results=3):
    results = tavily.search(
        query=question,
        max_results=n_results,
        search_depth='basic'
    )
    # Normalize to same format as local results
    return [
        (
            r['content'][:600],  # text
            {'source': r['url'], 'title': r['title'], 'type': 'web'}  # metadata
        )
        for r in results.get('results', [])
    ]

Объединение локальных и веб-результатов

Если используются оба источника, объедините результаты и укажите происхождение каждого из них. Это позволит LLM правильно оценить их вес: локальные документы использовать для фактов о компании, а веб-источники — для актуальных данных.

def merge_results(local_results, web_results):
    merged = []

    for text, meta in local_results:
        merged.append({
            'content': text,
            'source': meta.get('source', 'internal document'),
            'type': 'local',
            'title': meta.get('title', 'Company Document')
        })

    for text, meta in web_results:
        merged.append({
            'content': text,
            'source': meta.get('source', 'web'),
            'type': 'web',
            'title': meta.get('title', 'Web Result')
        })

    return merged

def format_merged_for_prompt(merged_results):
    parts = []
    for i, r in enumerate(merged_results, 1):
        tag = '[INTERNAL]' if r['type'] == 'local' else '[WEB]'
        parts.append(f'[{i}] {tag} {r["title"]}\n{r["content"]}')
    return '\n\n'.join(parts)

if __name__ == '__main__':
    local = [('Refunds are processed within 5 business days.', {'source': 'handbook', 'title': 'Refund Policy'})]
    web = [('Company X reported Q2 earnings today.', {'source': 'reuters.com', 'title': 'Q2 Earnings'})]
    merged = merge_results(local, web)
    print(format_merged_for_prompt(merged))

Выявление вопросов, зависящих от актуальности

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

CURRENT_EVENTS_SIGNALS = [
    'latest', 'current', 'today', 'now', 'recent',
    'this week', 'this month', 'this year',
    'just released', 'new version', 'updated',
    'price', 'stock', 'news', 'announcement',
    '2024', '2025'
]

STATIC_SIGNALS = [
    'how does', 'what is', 'explain', 'tutorial',
    'documentation', 'our product', 'company policy',
    'internal', 'handbook'
]

def fast_route(question):
    lower = question.lower()
    current_score = sum(1 for s in CURRENT_EVENTS_SIGNALS if s in lower)
    static_score = sum(1 for s in STATIC_SIGNALS if s in lower)

    if current_score > static_score:
        return 'current'
    elif static_score > current_score:
        return 'static'
    else:
        return 'both'

if __name__ == '__main__':
    for q in ['What is our company handbook policy on PTO?', 'What is the latest stock price today?']:
        print(f'{fast_route(q)!r} <- "{q}"')

Обработка противоречий между источниками

Противоречие возникает, когда в локальных документах сказано одно, а в результате веб-поиска — другое. Например, во внутреннем документе о ценах указано 50 долларов в месяц, а веб-результат сообщает, что цена изменилась до 80 долларов.

Попросите LLM отмечать противоречия и отдавать предпочтение веб-источникам для фактов, зависящих от времени.

HYBRID_ANSWER_PROMPT = '''You are answering a question using two types of sources:
- [INTERNAL] sources: company documents (may be outdated)
- [WEB] sources: current web information

For factual claims about current state (prices, versions, availability):
  PREFER [WEB] sources over [INTERNAL] ones.
For company-specific processes, policies, and architecture:
  PREFER [INTERNAL] sources.

If sources conflict, note the discrepancy in your answer.

Sources:
{sources}

Question: {question}
Answer:'''

def generate_hybrid_answer(question, merged_results):
    sources_text = format_merged_for_prompt(merged_results)
    return llm_call(HYBRID_ANSWER_PROMPT.format(
        sources=sources_text,
        question=question
    ))

Выявление устаревания локальных документов

Со временем локальные документы устаревают. Добавьте проверку актуальности: если локальный документ старше заданного порога, дополните его веб-поиском, даже если маршрутизатор классифицировал вопрос как «статический».

from datetime import datetime, timedelta

STALENESS_THRESHOLD_DAYS = 90

def check_staleness(metadata):
    indexed_at = metadata.get('indexed_at')
    if not indexed_at:
        return False  # unknown age — assume fresh
    indexed_date = datetime.fromisoformat(indexed_at)
    age = datetime.now() - indexed_date
    return age > timedelta(days=STALENESS_THRESHOLD_DAYS)

def smart_retrieve(question, route):
    local_results = []
    web_results = []

    if route in ('static', 'both'):
        local_results = local_retrieve(question, n_results=4)
        # Check if any local results are stale
        stale = any(check_staleness(meta) for _, meta in local_results)
        if stale:
            print('Stale local docs — adding web search')
            web_results = web_retrieve(question, n_results=2)

    if route in ('current', 'both'):
        web_results = web_retrieve(question, n_results=3)

    return merge_results(local_results, web_results)

Оценка степени уверенности

Добавьте оценку степени уверенности к каждому извлечённому фрагменту контекста. Источники с высокой уверенностью (недавние, с авторитетного домена, с высоким сходством эмбеддингов) должны иметь больший вес в итоговом ответе.

def score_result(result, query_embedding):
    score = 0.5  # base score

    # Recency bonus for web results
    if result.get('type') == 'web':
        pub_date = result.get('published_date', '')
        if '2024' in pub_date or '2025' in pub_date:
            score += 0.2

    # Embedding similarity to query
    if result.get('content'):
        result_emb = embed(result['content'][:500])
        sim = cosine_similarity(query_embedding, result_emb)
        score += sim * 0.3

    # Domain authority
    from urllib.parse import urlparse
    domain = urlparse(result.get('source', '')).netloc
    if any(auth in domain for auth in ['docs.', 'developer.', 'official.']):
        score += 0.1

    return min(score, 1.0)

Полный процесс гибридного извлечения

Объединим всё вместе: быстрая маршрутизация → интеллектуальное извлечение из одного или обоих источников → дополнение с учётом устаревания → объединение → оценка → форматирование → формирование ответа.

def hybrid_answer(question):
    # 1. Route (fast heuristic first, LLM fallback for ambiguous)
    route = fast_route(question)
    if route == 'both':
        route = classify_question(question)  # LLM for ambiguous cases

    print(f'Route: {route}')

    # 2. Retrieve
    merged = smart_retrieve(question, route)

    if not merged:
        return 'I could not find relevant information to answer your question.'

    # 3. Generate
    answer = generate_hybrid_answer(question, merged)
    return answer

# Usage
print(hybrid_answer('What is our refund policy?'))   # -> static/local
print(hybrid_answer('What is GPT-4 pricing today?')) # -> current/web

Проверка знаний

Когда агент гибридного извлечения должен отдавать предпочтение результатам веб-поиска перед результатами из локальных документов?

Повторение: объединение веб-поиска с RAG

Гибридное извлечение объединяет локальное векторное хранилище (для статических, закрытых знаний или знаний предметной области) с веб-поиском (для актуальной общедоступной информации). Классификатор маршрутизации направляет каждый вопрос к подходящему источнику — или к обоим, если это необходимо.

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

Можно начать бесплатно

Изучай AI Agents с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
60
Уроки
239

Часто задаваемые вопросы

Урок «Объединение веб-поиска с RAG» бесплатный?

Да — полный текст урока «Объединение веб-поиска с RAG» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Agents, подпишись на CoddyKit PRO. Курс AI Agents содержит 4 уроков всего.

Чему я научусь в уроке «Объединение веб-поиска с RAG»?

Гибридное извлечение: локальное векторное хранилище и актуальный веб-поиск для получения свежих ответов. Ты практикуешь AI Agents с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать AI Agents?

Предыдущий опыт не требуется. AI Agents на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Объединение веб-поиска с RAG»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке AI Agents?

Да. Каждый урок AI Agents включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Tavily и SerpAPI для поиска агентов
  2. Ранжирование и фильтрация результатов поиска
  3. Шаблон цикла глубокого исследования
  4. Объединение веб-поиска с RAG
← Назад к AI Agents