Web検索とRAGの組み合わせ
ローカルのベクトルストアと最新のWeb検索を組み合わせ、最新情報に基づく回答を取得します。
「Web検索とRAGの組み合わせ」はCoddyKit上の無料AI Agentsレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Agents学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Agentsコースには全4レッスンが含まれています。
ハイブリッド検索の課題
現実の多くのエージェントには、2種類の知識が必要です。静的なドメイン知識(社内ドキュメント、製品マニュアル、ポリシー)と、最新情報(ニュース、現在の価格、最近の出来事)です。
前者にはローカルのベクトルストア、後者にはWeb検索を使用します。両方を組み合わせることで、それぞれの長所を活かせます。
アーキテクチャ:ローカルRAG + Web検索
ハイブリッドシステムは、それぞれの質問を適切な検索元に振り分けます。
- ベクトルストア(RAG) — インデックス化された文書、安定した知識、非公開データ
- Web検索 — 現在の出来事、最近のリリース、リアルタイムデータ
- 両方 — 文書のコンテキストと最新情報の両方が必要な質問
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]))Web検索による情報取得
Web検索経路では、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', [])
]ローカル結果とWeb結果のマージ
両方の情報源を使う場合は、結果をマージし、それぞれに出所のタグを付けます。これにより、LLMはそれぞれを適切に重み付けできます。つまり、企業固有の事実にはローカルの文書、最新データにはWebの情報を使用します。
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}"')
情報源間の矛盾への対処
ローカルの文書とWebの結果が異なる内容を示している場合、矛盾が発生します。たとえば、社内の料金文書では月額50ドルと記載されているのに、Webの結果では価格が月額80ドルに変更されたと示されている場合です。
矛盾を明示し、時間に依存する事実についてはWebの情報源を優先するよう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
))ローカル文書の陳腐化の検出
ローカル文書は時間の経過とともに古くなります。陳腐化チェックを追加し、ローカル文書がしきい値より古い場合は、ルーターが質問を「静的」と分類した場合でもWeb検索で補完します。
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知識チェック
ハイブリッド検索エージェントは、どのような場合にローカル文書の結果よりWeb検索の結果を優先すべきですか?
復習:Web検索とRAGの組み合わせ
ハイブリッド検索は、ローカルのベクトルストア(静的、非公開、またはドメイン固有の知識向け)とWeb検索(最新の公開情報向け)を組み合わせます。ルーティング分類器が各質問を適切な情報源に振り分けます。必要に応じて、両方に振り分けることもできます。
主な技術には、キーワードシグナルを使った高速なヒューリスティックルーティング、ローカル文書の陳腐化検出、プロンプト内での矛盾解決の指示、取得したコンテキストに重み付けするための信頼度スコアリングがあります。
よくある質問
「Web検索とRAGの組み合わせ」レッスンは無料ですか?
はい。「Web検索とRAGの組み合わせ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Agentsコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Agentsコースには全4レッスンが含まれています。
「Web検索とRAGの組み合わせ」で何を学びますか?
ローカルのベクトルストアと最新のWeb検索を組み合わせ、最新情報に基づく回答を取得します。 ブラウザで直接実行するハンズオンコードでAI Agentsを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Agentsを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Agentsは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「Web検索とRAGの組み合わせ」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Agentsレッスンでコードを書いて実行できますか?
はい。すべてのAI Agentsレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- エージェント検索向けTavilyとSerpAPI
- 検索結果のランキングとフィルタリング
- ディープリサーチループのパターン
- Web検索とRAGの組み合わせ