「最新の情報を反映させるために、LLMにWeb検索機能(Web Browsing)を持たせたい」という要望は、ソフトウェア開発の現場で頻繁に挙がります。ソフトウェア開発者としての経験を経て、現在はAPIドキュメンテーションやクラウド技術の解説を専門とするテクニカルライターの視点から見ても、この課題は多くの開発チームにとって共通の悩みとなっています。
実際に実装を進めると、多くのエンジニアが「回答精度のバラつき」という壁に直面します。AIエージェントが検索結果の上位記事を適当に要約してしまったり、広告やSEOスパム記事の内容を真実として語ったりする現象です。ビジネス利用において、もっともらしい嘘(ハルシネーション)は致命的なリスクとなります。
単に検索APIをツールとしてLLMに渡すだけでは不十分です。人間が調べ物をする時と同じように、「検索し、内容を吟味し、情報の裏を取る」というプロセスをシステム化する必要があります。
本記事では、複雑な技術情報を段階的に理解できるよう、LangChainとAI検索に特化したTavily APIを使用し、「情報の正確性(Accuracy)」と「自律的な検証(Self-Reflection)」に焦点を当てたWeb Browsing制御パイプラインの実装手順を体系的に解説します。具体的なコード例を交えながら、検索結果を鵜呑みにしない、堅牢なAIエージェントを構築していきましょう。
なぜ「ただ検索させる」だけでは失敗するのか:制御なきWeb Browsingのリスク
実装コードに入る前に、なぜ標準的なアプローチ(LLMに検索ツールを渡してagent.run()するだけ)では不十分なのか、その技術的な背景を整理します。
単純なTool useの限界
LLMのTool use(Function Calling)機能は強力ですが、基本的には「ユーザーのプロンプト→検索実行→検索結果の全入力をコンテキストへ注入→回答生成」という線形プロセスをたどります。ここには大きな落とし穴があります。
- クエリの質の低さ: ユーザーの質問(例:「クラウド市場における競合他社の最近の動きは?」)をそのまま検索クエリにすると、企業の公式発表だけでなく、無関係な個人の感想ブログや古いニュースなど、ノイズが多く含まれます。
- コンテキスト汚染: 検索結果に含まれるナビゲーションメニュー、広告テキスト、無関係なサイドバー情報までLLMに読み込ませてしまい、推論を歪めます。例えば、技術記事を検索したはずが、ページ内の「おすすめの転職サイト」という広告テキストまでLLMが読み込んでしまい、回答に無関係な情報が混入するケースです。
- 検証プロセスの欠如: LLMは与えられたコンテキストを使って「回答を作ること」を優先するため、そのコンテキスト自体の信憑性を疑う工程がありません。例えば、古いバージョンのAPI仕様書を検索結果として渡された場合、LLMはそれが最新であると信じ込み、非推奨のコードを提案してしまいます。
信頼性スコアリングの概念
今回構築するアーキテクチャでは、「信頼性スコアリング」と「検証ループ」という概念を導入します。
検索して終わりではなく、取得した情報が「ユーザーの質問に答えるのに十分か?」「信頼できるソースか?」をLLM自身に評価させます。もし不十分であれば、クエリを変えて再検索を行う。この自律的な判断ロジックこそが、実用レベルのAIエージェントには不可欠です。
事前準備:高精度な検索制御に必要な環境とAPI選定
まずは開発環境を整えましょう。今回は検索APIとして、AIエージェント開発に特化したTavily APIを採用します。
Tavily APIがAIエージェントに適している理由
Google Custom Search APIやBing Search APIも一般的ですが、LLM(大規模言語モデル)を組み込んだアプリケーション開発において、Tavilyは非常に強力な選択肢です。
- LLM最適化: JSON形式で、LLMが理解しやすいクリーンなテキストデータ(スクレイピング・解析済み)を返します。
- トークン節約: 不要なHTMLタグ、広告、スクリプトを除去した状態で取得できるため、LLMのコンテキストウィンドウ(入力容量)を圧迫しません。
- 高度なフィルタリング: 特定ドメインの除外や、回答生成に特化した「answer」モードなどが標準装備されており、ハルシネーションのリスクを低減します。
環境構築
Python 3.9以上推奨です。セキュリティと安定性を確保するため、ライブラリは常に最新版を使用してください。特にLangChainは頻繁にアップデートされており、脆弱性修正が含まれる場合があるため注意が必要です。
必要なライブラリをインストールします。langchain-community パッケージも明示的に含めます。
pip install -U langchain langchain-community langchain-openai tavily-python python-dotenv
プロジェクトのルートディレクトリに .env ファイルを作成し、APIキーを設定してください。
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=tvly-...
基本的なセットアップコードは以下の通りです。
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
load_dotenv()
# 推論能力重視ならChatGPT、コストパフォーマンス重視ならChatGPT miniなどの最新モデルを使用
# ※GPT-3.5 Turboは非推奨となり、現在はChatGPT miniへの移行が推奨されています
llm = ChatOpenAI(model="ChatGPT", temperature=0)
# Tavilyツールの初期化(後ほど詳細設定します)
tool = TavilySearchResults()
モデル選定のポイント
ここでのポイントは temperature=0 です。検証や制御を行うタスクでは、創造性よりも決定論的な動作(一貫性)が求められるため、ランダム性を排除します。
また、使用するモデルについては以下の基準で選定することをお勧めします。
- 推論能力重視: 複雑な検証や論理的判断が必要な場合は、ChatGPTの高性能モデル(ChatGPTなど)を選択してください。
- コスト・速度重視: 以前のGPT-3.5 Turboに代わり、現在はChatGPT miniなどの軽量モデルが推奨されます。高速かつ低コストで、十分な性能を発揮します。
※OpenAIのモデルは頻繁に更新されます。実装の際は必ず公式ドキュメントで最新のモデルIDを確認してください。
Step 1:意図を汲み取る「検索クエリ生成ロジック」の実装
ユーザーの入力をそのまま検索エンジンに投げるのは避けましょう。曖昧な質問を、検索エンジンが理解しやすい明確なクエリに変換するプロセス、Query Transformationを実装します。
ユーザー入力をそのまま検索させない理由
例えばユーザーが「KnowledgeFlowの競合と比較してどう?」と聞いた場合、そのまま検索すると「KnowledgeFlow 競合 比較」となりますが、これでは一般的な比較記事しか出ない可能性があります。
より深い情報を得るには、「KnowledgeFlow 機能一覧」「KnowledgeFlow 料金プラン」「KnowledgeFlow 競合他社 シェア」のように、多角的なサブクエリに分解する必要があります。
クエリ生成の実装(Multi-Query)
LangChainのLCEL(LangChain Expression Language)を使って、質問を3つの検索クエリに分解するチェーンを作成します。
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
# 出力構造の定義
class SearchQueries(BaseModel):
queries: list[str] = Field(description="生成された検索クエリのリスト(3つ)")
# プロンプトの定義
system_prompt = """
あなたは熟練したリサーチャーです。
ユーザーの質問に回答するために必要な情報を収集するための検索クエリを3つ生成してください。
多角的な視点(技術的詳細、ビジネス的側面、最新ニュースなど)を含めること。
"""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{question}")
])
# チェーンの構築
query_generator_chain = prompt | llm.with_structured_output(SearchQueries)
# 実行テスト
question = "KnowledgeFlowの競合優位性は?"
result = query_generator_chain.invoke({"question": question})
print(result.queries)
# 出力例:
# ['KnowledgeFlow 競合他社 機能比較',
# 'KnowledgeFlow 導入メリット 事例',
# 'KnowledgeFlow AIナレッジベース 市場シェア']
このように構造化出力(Structured Output)を利用することで、後続の処理で扱いやすいリスト形式のデータを確実に取得できます。これで、単一の視点に偏らない情報収集の準備が整いました。
Step 2:情報の質を担保する「ドメインフィルタリング」と「コンテンツ抽出」
次に、検索の実行部分をチューニングします。Tavilyの強みであるフィルタリング機能を活用し、情報の質を担保します。
信頼できるソースの定義
ビジネスユースケースでは、個人のブログや掲示板(Reddit, Quoraなど)よりも、公式ドキュメントや信頼できるニュースメディアの情報を優先したい場合があります。例えば、APIの実装方法を調べる際、個人の推測が混じったブログ記事よりも、公式のAPIリファレンスやGitHubの公式リポジトリを優先すべきです。Tavilyでは include_domains や exclude_domains でこれを制御できます。
また、search_depth パラメータを使い分けることも重要です。
basic: 高速、低コスト。簡単な事実確認向け。advanced: 詳細なスクレイピングを行う。深い分析向け。
高度な検索ツールの設定
以下は、特定のドメインを除外しつつ、詳細なコンテンツを取得する設定例です。
from langchain_community.tools.tavily_search import TavilySearchResults
# 検索ツールのカスタマイズ
search_tool = TavilySearchResults(
max_results=5,
search_depth="advanced",
include_answer=True, # Tavily側で生成した短い回答も含める
include_raw_content=False, # 生のHTMLは不要
include_images=False,
# 除外したいドメイン(例: SNS、掲示板など)
exclude_domains=[
"twitter.com",
"reddit.com",
"facebook.com",
"youtube.com"
]
)
def execute_search(queries):
"""生成された複数のクエリで検索を実行し、結果を統合・重複排除する関数"""
aggregated_results = []
seen_urls = set()
print(f"Executing search for queries: {queries}")
for query in queries:
try:
# 各クエリで検索実行
results = search_tool.invoke({"query": query})
for item in results:
url = item.get('url')
if url not in seen_urls:
seen_urls.add(url)
aggregated_results.append(item)
except Exception as e:
print(f"Error searching for {query}: {e}")
return aggregated_results
この関数では、先ほど生成した複数のクエリを順次実行し、URLベースで重複を排除しながら結果を統合しています。これにより、情報の網羅性と質のバランスを取ることができます。
Step 3:回答の正確性を担保する「Fact Checking(事実検証)」ループの実装
ここが本記事の核心です。検索結果をLLMに渡して回答を生成させた後、「その回答は本当に検索結果に基づいているか?」を自己検証(Self-Reflection)させます。
Self-Reflection(自己省察)の実装
このプロセスは以下の2段階で行います。
- 回答生成: 検索結果をコンテキストとして回答を作成。
- 事実検証: 生成された回答と検索結果(ソース)を比較し、ハルシネーションがないかチェック。
from langchain_core.output_parsers import StrOutputParser
# 1. 回答生成チェーン
answer_prompt = ChatPromptTemplate.from_template("""
以下の検索結果(Context)のみに基づいて、ユーザーの質問に回答してください。
情報が不足している場合は、正直に「情報が見つかりませんでした」と答えてください。
推測や一般的な知識での補完は禁止です。
Context:
{context}
Question:
{question}
""")
answer_chain = answer_prompt | llm | StrOutputParser()
# 2. 検証(Fact Check)チェーン
verify_prompt = ChatPromptTemplate.from_template("""
あなたは厳格なファクトチェッカーです。
以下の「生成された回答」が、「提供された検索結果」によって裏付けられているか検証してください。
判定基準:
- 回答に含まれるすべての主張が検索結果に存在するか。
- 数値、日付、固有名詞が正確か。
検索結果:
{context}
生成された回答:
{answer}
以下のJSON形式で出力してください:
{{
"is_accurate": boolean, // 正確ならtrue, 不正確または根拠不足ならfalse
"reason": "string", // 判定理由
"corrected_answer": "string" // 不正確な場合の修正案(正確ならnull)
}}
""")
class VerificationResult(BaseModel):
is_accurate: bool
reason: str
corrected_answer: str | None
verify_chain = verify_prompt | llm.with_structured_output(VerificationResult)
検証ループの統合ロジック
これらを組み合わせたメインの処理フローです。検証に失敗した場合、修正案を採用するか、あるいは「信頼性が低いため回答できません」と返すガードレールとして機能します。
def process_query_with_verification(user_question):
# 1. クエリ生成
queries_data = query_generator_chain.invoke({"question": user_question})
queries = queries_data.queries
# 2. 検索実行
search_results = execute_search(queries)
# コンテキスト用にテキスト整形
context_text = "\n\n".join(
[f"Source ({res['url']}): {res['content']}" for res in search_results]
)
# 3. 回答生成
initial_answer = answer_chain.invoke({
"context": context_text,
"question": user_question
})
print("--- Initial Answer ---")
print(initial_answer)
# 4. 事実検証
verification = verify_chain.invoke({
"context": context_text,
"answer": initial_answer
})
print("--- Verification Result ---")
print(f"Accurate: {verification.is_accurate}")
print(f"Reason: {verification.reason}")
if verification.is_accurate:
return initial_answer
else:
# 検証NGの場合、修正案があればそれを返す、なければ警告付きで返す
if verification.corrected_answer:
print("--- Returning Corrected Answer ---")
return verification.corrected_answer
else:
return "申し訳ありません。信頼できる情報源からの裏付けが取れなかったため、回答を控えます。"
このロジックにより、AIは「嘘をつくくらいなら答えない(あるいは修正する)」という振る舞いを身につけます。これはB2Bアプリケーションにおいて信頼を築くための重要な機能です。例えば、金融系のシステムにおいて、不確かな市場データを提供することは重大なリスクにつながります。この検証ループは、そうしたリスクを軽減する安全装置として機能します。
実装テストとトラブルシューティング
実装の完了後、このエージェントを本番環境で運用する際によくある課題とその対処法を解説します。
典型的なエッジケースでの動作確認
- 検索結果ゼロ件: マイナーなトピック(例:社内専用の独自ツール名など)では検索結果が得られないことがあります。
execute_search関数内で結果リストが空の場合の早期リターン処理(「情報が見つかりませんでした」と即答する)を追加することで、無駄なLLM呼び出し(コスト)を防げます。 - 情報の矛盾: 複数のソースで異なる数値(例:異なる調査機関による市場規模の予測値など)が出ている場合、LLMは混乱します。プロンプトに「情報が矛盾している場合は、両方の数値を併記し、それぞれのソースを明記せよ」と指示を追加するのが有効です。
実行速度とコストの最適化テクニック
このパイプラインは「クエリ生成(LLM) → 検索(API) → 回答(LLM) → 検証(LLM)」と、少なくとも3回のLLM呼び出しが発生するため、応答速度(レイテンシ)が課題になりがちです。
- 並列処理: クエリ生成後の検索実行は並列化可能です。LangChainの
RunnableParallelを活用しましょう。 - モデルの使い分け: すべてのステップで最高性能のモデルを使う必要はありません。
- クエリ生成・検証: 高速かつ低コストな最新の軽量モデル(例:ChatGPTの軽量版など)を使用します。近年の軽量モデルは、以前のモデルと比較して推論能力やコンテキスト処理能力が大幅に向上しており、単純なタスクには十分な性能を発揮します。
- 最終回答の生成: 複雑な情報の統合が必要な箇所のみ、推論能力の高いモデル(例:ChatGPTの最新モデルやClaudeの最新モデル)を使用します。
- このように適材適所でモデルを使い分けることで、コストを抑えつつ、ユーザー体験を損なわない速度を実現できます。なお、利用可能なモデルは頻繁に更新されるため、最新の仕様や料金については各プロバイダーの公式ドキュメントを確認することをお勧めします。
まとめ:信頼されるAIエージェントへの進化
今回実装した「検索クエリの最適化」「ドメインフィルタリング」「自己検証ループ」の3ステップにより、単なる検索ボットは、信頼性の高いリサーチアシスタントへと進化しました。
- 意図理解: ユーザーの言葉足らずな質問を補完。
- 品質管理: ノイズを除去し、信頼できるソースのみを参照。
- 自己修正: ハルシネーションを未然に防ぐ安全装置。
技術的な実装は複雑に見えるかもしれませんが、これらはすべて「ユーザーに正確な情報を届ける」というUX向上のための投資です。
今回解説したような高度な検証ロジックやRAG(検索拡張生成)のパイプラインを業務フローに組み込むことで、情報の正確性がビジネスをどう加速させるか、実際の運用を通じて実感できるでしょう。
コメント