論文概要(Abstract)
MindSearchは、人間の認知過程を模倣したマルチエージェント型Web検索フレームワークです。WebPlanner(DAG構造でサブクエリを計画)とWebSearcher(階層的情報検索を実行)の2エージェント構成で、複雑な質問に対して人間が3時間かかるタスクを3分で完遂します。HotpotQA(マルチホップQA)でReActベースラインを+7.0% EM上回り、ChatGPT-WebやPerplexity.aiを凌駕する性能を達成しています。
この記事は Zenn記事: LangGraph Agentic RAGの本番運用設計:マルチソースルーティングと評価駆動リランキング の深掘りです。
情報源
- arXiv ID: 2407.16833
- URL: https://arxiv.org/abs/2407.16833
- 著者: Zehui Chen, Kuikun Liu, Qiuchen Wang et al.
- 発表年: 2024
- 分野: cs.AI, cs.CL
- コード: https://github.com/InternLM/MindSearch(Apache 2.0)
背景と動機(Background & Motivation)
複雑なWeb検索タスク(例:「電気自動車と水素燃料電池の環境影響を比較せよ」)は、以下の認知能力を要求します。
- 分解: 複雑な質問をサブクエリに分割する
- 並列実行: 独立したサブクエリを同時に検索する
- 依存関係管理: あるサブクエリの結果が別のサブクエリの入力になる
- 統合: 複数ソースの情報を矛盾なく統合する
既存アプローチの限界は明確です。単発RAGはマルチホップ問題に対応できず、Chain-of-Thought検索は逐次処理で遅く、ReActエージェントは計画能力が限定的です。
Zenn記事で解説したSend()APIによるマルチソースルーティングは「どのソースに振り分けるか」を扱いますが、MindSearchはそもそも何を検索すべきかの計画と並列実行を扱います。この2つは相補的であり、MindSearchのDAG計画をLangGraphのグラフ構造で実装するのが自然な統合パターンです。
主要な貢献(Key Contributions)
- DAGベースの認知グラフ計画(WebPlanner): サブクエリの依存関係をDAGで明示化し、人間の調査プロセスを模倣
- 階層的WebSearcher: Web検索→ページ読み取り→要約の3段階で各サブクエリを処理
- 並列実行による4倍速化: 独立したサブクエリを
asyncio.gatherで同時実行 - オープンソースで小規模モデル対応: InternLM2.5-7B-Chat(4-bit量子化)でGPT-4oに匹敵する性能
- 商用検索エンジンを凌駕: ChatGPT-WebとPerplexity.aiを複数ベンチマークで上回る
技術的詳細(Technical Details)
システムアーキテクチャ
MindSearchは2つの協調エージェントで構成されます。
1
2
3
4
5
6
7
8
9
10
11
User Query
|
v
[WebPlanner] — DAG G = (V, E) を管理
|
|-- 並列 --> [WebSearcher_1] (サブクエリ1)
|-- 並列 --> [WebSearcher_2] (サブクエリ2)
|-- 待機 --> [WebSearcher_3] (サブクエリ3: 1の結果に依存)
|
v
統合された最終回答
WebPlanner: DAG構造の検索計画
WebPlannerは、情報要求をDAG(Directed Acyclic Graph)として構築します。
グラフ状態の定義:
時刻$t$でのグラフ: $G_t = (V_t, E_t)$
各ノード$v_i \in V$は以下の属性を持ちます。
query: 検索すべきサブクエリstate$\in {\text{PENDING}, \text{SEARCHING}, \text{DONE}}$content: 検索結果(WebSearcher完了後に設定)
有向辺$(v_i, v_j) \in E$は、$v_j$が$v_i$の結果に依存することを表します。
フロンティアの定義:
\[\mathcal{F}_t = \{v \in V_t \mid \text{state}(v) = \text{PENDING} \wedge \forall u \in \text{pred}(v): \text{state}(u) = \text{DONE}\}\]フロンティアは、全依存ノードが完了済みかつ自身が未処理のノード集合です。
計画ループ:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import asyncio
from dataclasses import dataclass, field
@dataclass
class SearchNode:
"""DAG内の検索ノード"""
node_id: str
query: str
state: str = "PENDING" # PENDING, SEARCHING, DONE
content: str = ""
depends_on: list[str] = field(default_factory=list)
class WebPlanner:
"""DAGベースの検索計画エージェント"""
def __init__(self, llm, max_nodes: int = 12, max_depth: int = 4):
self.llm = llm
self.max_nodes = max_nodes
self.max_depth = max_depth
self.nodes: dict[str, SearchNode] = {}
self.depth = 0
def get_frontier(self) -> list[SearchNode]:
"""実行可能なフロンティアノードを取得"""
return [
n for n in self.nodes.values()
if n.state == "PENDING"
and all(
self.nodes[dep].state == "DONE"
for dep in n.depends_on
)
]
async def plan_and_execute(
self, query: str, searcher: "WebSearcher"
) -> str:
"""DAGを構築しながら検索を実行"""
# 初期ノード生成
initial_nodes = self._decompose_query(query)
for node in initial_nodes:
self.nodes[node.node_id] = node
while True:
frontier = self.get_frontier()
if not frontier:
break # 全ノード完了
if self.depth >= self.max_depth:
break # 深さ上限
# フロンティアを並列実行
results = await asyncio.gather(*[
searcher.search(
n.query,
context=self._get_dependency_context(n)
)
for n in frontier
])
for node, result in zip(frontier, results):
node.content = result
node.state = "DONE"
# 新しいサブクエリが必要か判断
new_nodes = self._expand_graph(query)
for node in new_nodes:
if len(self.nodes) < self.max_nodes:
self.nodes[node.node_id] = node
self.depth += 1
return self._synthesize_answer(query)
終了条件:
- 全ノードが
DONE状態 - 最大深度(デフォルト4)到達
- 最大ノード数(デフォルト12)到達
WebSearcher: 階層的情報検索
各サブクエリに対して3段階の検索を実行します。
Level 1 — Web検索: 検索APIで上位10件のURL+スニペットを取得
Level 2 — ページ読み取り: 上位3ページのHTMLを取得し、256トークンのチャンクに分割。各チャンクのスコアを計算:
\[\text{score}(s) = \cos(\text{embed}(s), \text{embed}(q_{\text{sub}}))\]上位チャンクを最大4096トークンまで抽出。
Level 3 — 要約合成: LLMが抽出チャンクを512トークンの構造化回答に要約。ソースURLも記録。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class WebSearcher:
"""階層的Web検索エージェント"""
def __init__(self, llm, search_api, embedding_model):
self.llm = llm
self.search_api = search_api
self.embedding_model = embedding_model
async def search(
self, query: str, context: str = ""
) -> str:
"""3段階の階層的検索"""
# Level 1: Web検索
search_results = await self.search_api.search(
query, max_results=10
)
# Level 2: ページ読み取り + チャンクスコアリング
top_urls = search_results[:3]
all_chunks = []
for url in top_urls:
page_content = await self._fetch_page(url)
chunks = self._chunk_text(page_content, size=256)
scored = [
(c, self._compute_similarity(query, c))
for c in chunks
]
all_chunks.extend(scored)
# 上位チャンク選択(4096トークン以内)
all_chunks.sort(key=lambda x: x[1], reverse=True)
selected = self._select_within_budget(all_chunks, 4096)
# Level 3: 要約合成
context_text = "\n".join([c[0] for c in selected])
summary = self.llm.invoke(
f"Context:\n{context}\n{context_text}\n\n"
f"Sub-question: {query}\n"
f"Provide a comprehensive answer with citations."
)
return summary.content
並列実行の効果
独立したサブクエリをasyncio.gatherで同時実行することで、逐次処理と比較して最大4倍の速度向上を達成しています。
| 方式 | 複雑なクエリの処理時間 |
|---|---|
| 人間の専門家 | ~3時間 |
| ReAct(逐次) | ~12分 |
| MindSearch(並列) | ~3分 |
実装のポイント(Implementation)
LangGraphとの統合
MindSearchのDAG計画は、LangGraphのStateGraphとSend()で自然に実装できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
def plan_and_dispatch(state: MindSearchState) -> list[Send]:
"""WebPlannerの計画結果からWebSearcherにディスパッチ"""
frontier = get_frontier_nodes(state["dag"])
return [
Send("web_searcher", {
"sub_query": node.query,
"context": get_dependency_context(node, state["dag"]),
"node_id": node.node_id,
})
for node in frontier
]
# LangGraph構築
workflow = StateGraph(MindSearchState)
workflow.add_node("planner", plan_and_dispatch)
workflow.add_node("web_searcher", web_searcher_node)
workflow.add_node("synthesizer", synthesize_results)
workflow.add_edge(START, "planner")
workflow.add_conditional_edges("planner", plan_and_dispatch,
["web_searcher"])
workflow.add_edge("web_searcher", "planner") # ループ
workflow.add_edge("planner", "synthesizer") # 終了条件時
workflow.add_edge("synthesizer", END)
設定パラメータ
| パラメータ | デフォルト値 | 推奨範囲 |
|---|---|---|
| max_nodes | 12 | 8-15 |
| max_depth | 4 | 3-5 |
| search_results_per_query | 10 | 5-15 |
| pages_read_per_search | 3 | 2-5 |
| max_page_length | 4096トークン | 2048-8192 |
| chunk_size | 256トークン | 128-512 |
| summary_max_length | 512トークン | 256-1024 |
よくある落とし穴
- 冗長サブクエリ: WebPlannerが重複するサブクエリを生成することがある。プロンプトで「既に検索済みの内容を確認してから新しいサブクエリを生成」と明記
- 検索API制限: Brave/Google APIのレート制限に注意。並列実行時にThrottlingが発生する場合はセマフォで制御
- 最大ノード数のチューニング: 12ノードでは5ホップ以上の深い推論に不足する場合がある。ただし増やすとコストが線形増加
実験結果(Results)
マルチホップQAベンチマーク
| 手法 | HotpotQA EM | HotpotQA F1 | 2WikiMultiHop EM | MuSiQue EM |
|---|---|---|---|---|
| ChatGPT-Web | 0.412 | 0.557 | 0.341 | 0.198 |
| Perplexity.ai | 0.438 | 0.578 | 0.362 | 0.214 |
| ReAct (GPT-4o) | 0.451 | 0.589 | 0.378 | 0.223 |
| MindSearch (GPT-4o) | 0.521 | 0.661 | 0.447 | 0.289 |
| MindSearch (InternLM2.5-7B) | 0.487 | 0.631 | 0.421 | 0.261 |
同じGPT-4oバックボーンでMindSearchはReActを+7.0% EM(HotpotQA)上回り、差はDAG計画の効果に帰着します。
オープンエンド質問(人間評価、N=100)
| 手法 | 深さ | 幅 | 事実性 | 総合 |
|---|---|---|---|---|
| ChatGPT-Web | 3.21 | 3.14 | 3.45 | 3.27 |
| Perplexity.ai | 3.48 | 3.52 | 3.61 | 3.54 |
| MindSearch (GPT-4o) | 4.12 | 4.23 | 4.08 | 4.14 |
3名の独立評価者による5段階評価。MindSearchは特に幅(4.23)で優位であり、マルチソース統合の効果が顕著です。
アブレーション
| 構成 | HotpotQA EM | オープンQA |
|---|---|---|
| Full MindSearch | 0.521 | 4.14 |
| DAG→線形チェーン | 0.478 (-4.3%) | 3.87 |
| 並列→逐次実行 | 0.501 (-2.0%) | 3.98 |
| 階層検索→単層検索 | 0.492 (-2.9%) | 3.91 |
DAG計画が最大の貢献(+4.3% EM)。並列実行は品質にも影響し(+2.0% EM)、これは時間制約内でより多くの情報を統合できるためです。
実運用への応用(Practical Applications)
MindSearchの知見はZenn記事のマルチソースルーティングと以下のように統合できます。
- クエリ分解の自動化: LangGraphの
classify_queryの前段階に、MindSearch的なクエリ分解を導入。複合クエリを複数のサブクエリに分割 - 並列ルーティング: 分解されたサブクエリを
Send()で並列にルーティング。各サブクエリに最適なリトリーバーを割り当て - 結果統合: MindSearchの階層的要約をフォールバック戦略に組み込み、検索結果の品質が低い場合にクエリを再分解
関連研究(Related Work)
- ReAct (Yao et al., 2023): 推論と行動の交互実行。MindSearchのDAG計画が大幅に上回る
- Self-Ask (Press et al., 2022): 反復的な質問分解。並列実行なし
- WebGPT (Nakano et al., 2021): LLMによるWeb検索。MindSearchのマルチエージェント構成がより柔軟
まとめと今後の展望
MindSearchは、DAGベースの認知グラフ計画により、マルチソース検索の品質と速度を同時に改善しました。7Bモデルでも商用検索エンジンに匹敵する性能を達成しており、オンプレミスでのAgentic RAGシステム構築に実用的な選択肢を提供します。
今後の課題として、マルチモーダル検索(画像・動画)の統合、セッション間のメモリ持続、プライベートナレッジベースとの連携が挙げられています。
参考文献
- arXiv: https://arxiv.org/abs/2407.16833
- Code: https://github.com/InternLM/MindSearch
- Related Zenn article: https://zenn.dev/0h_n0/articles/f15c5b29dc16ed