0Pricing
AI Agents · 강의

웹 검색과 RAG 결합

하이브리드 검색: 최신 답변을 위해 로컬 벡터 저장소와 실시간 웹 검색을 결합합니다.

웹 검색과 RAG 결합은(는) CoddyKit의 무료 AI Agents 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 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 결합

하이브리드 검색은 로컬 벡터 저장소(정적이거나 비공개이거나 도메인에 특화된 지식용)와 웹 검색(최신 공개 정보용)을 결합합니다. 라우팅 분류기는 각 질문을 적절한 출처로 전달하며, 필요한 경우 두 출처 모두 사용합니다.

주요 기법은 키워드 신호를 사용한 빠른 규칙 기반 라우팅, 로컬 문서의 최신성 저하 감지, 프롬프트에 포함하는 충돌 해결 지침, 검색한 문맥의 가중치를 조정하기 위한 신뢰도 점수 계산입니다.

자주 묻는 질문

“웹 검색과 RAG 결합” 강의는 무료인가요?

네 — “웹 검색과 RAG 결합” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Agents 강의 전체를 잠금 해제할 수 있습니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.

“웹 검색과 RAG 결합”에서 뭘 배우나요?

하이브리드 검색: 최신 답변을 위해 로컬 벡터 저장소와 실시간 웹 검색을 결합합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Agents을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

AI Agents을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 AI Agents은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“웹 검색과 RAG 결합” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 AI Agents 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 AI Agents 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 에이전트 검색을 위한 Tavily 및 SerpAPI
  2. 검색 결과 순위 지정 및 필터링
  3. 심층 연구 반복 패턴
  4. 웹 검색과 RAG 결합
← AI Agents(으)로 돌아가기